@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,175 @@
1
+ #!/usr/bin/env node
2
+ // aria-preprompt-consult.mjs — UserPromptSubmit hook that auto-fires
3
+ // /api/harness/delegate in architect mode BEFORE Claude processes the
4
+ // user's message, then injects Aria's substrate-grounded direction as
5
+ // an [ARIA_DIRECTION] context chunk so Claude has a pre-loaded read
6
+ // before deciding anything.
7
+ //
8
+ // Direction: Hamza 2026-04-26 — "BUT WHY DO U HAVE DISCRETION - THIS
9
+ // WORKS SO MUCH FASTER AND HIGHER QUALITY IF U DONT PLZ TELLL ME WHATS
10
+ // MISSING." This hook closes the structural gap: pre-action discretion
11
+ // (Claude deciding what to do, including "decide to ask the user") was
12
+ // the unwired surface. Existing gates intercept ACTIONS (Bash, Edit,
13
+ // text-emit) but not the decision boundary BEFORE the action.
14
+ //
15
+ // Doctrine bindings:
16
+ // - feedback_use_harness_to_architect.md — when uncertain, consult Aria
17
+ // - feedback_aria_does_work.md — Aria is brain, Claude is hands
18
+ // - feedback_gates_enforce_form_not_substance.md — gates check form not
19
+ // substance; this hook puts substance into Claude's context BEFORE
20
+ // Claude has a chance to decide reflexively from training-prior
21
+ // - project_harness_research_first.md — Phase 8 research-pull is the
22
+ // analogue applied INBOUND (research before model drafts); this
23
+ // hook is the OUTBOUND analogue (Aria-decides-direction before
24
+ // Claude decides)
25
+ //
26
+ // Mechanics: reads the user's message from the hook event JSON, POSTs
27
+ // to /api/harness/delegate with role=architect (auto-elevates tier per
28
+ // delegate.ts when expectStructuredOutput=true; here we use plain prose
29
+ // so we explicitly request deepseek-v4-pro), receives Aria's read,
30
+ // outputs to stdout as a JSON object with `additionalContext` field
31
+ // per Claude Code hooks contract — that string is injected into the
32
+ // system context for THIS turn.
33
+ //
34
+ // Per no-timeouts doctrine (feedback_no_timeouts_doctrine.md): no
35
+ // AbortSignal.timeout. The hook itself has a Claude Code timeout (12s
36
+ // in settings.json) — if the consultation takes longer, the hook is
37
+ // killed and Claude proceeds without the direction. Real-error driven,
38
+ // no graceful-degradation rituals.
39
+ //
40
+ // Kill-switch: ARIA_PREPROMPT_CONSULT=off env (logged, emergency only).
41
+
42
+ import { appendFileSync, existsSync, mkdirSync } from 'node:fs';
43
+ import { dirname } from 'node:path';
44
+
45
+ const HOME = process.env.HOME || '/tmp';
46
+ const LOG = `${HOME}/.claude/aria-preprompt-consult.log`;
47
+
48
+ const HARNESS_URL = process.env.ARIA_HARNESS_URL || 'http://192.168.4.25:30080';
49
+ const HARNESS_TOKEN = process.env.ARIA_HARNESS_TOKEN || '30b18f0a302b03ae862de8a75021238d23e47464fd5d8f6a9324240933745587';
50
+ const MIN_PROMPT_CHARS = 40; // skip auto-consult on trivial prompts
51
+ const MAX_DIRECTION_CHARS = 4000; // cap injected chunk size
52
+
53
+ function audit(decision, summary) {
54
+ try {
55
+ if (!existsSync(dirname(LOG))) mkdirSync(dirname(LOG), { recursive: true });
56
+ appendFileSync(LOG, `${new Date().toISOString()} ${decision} ${summary}\n`);
57
+ } catch {}
58
+ }
59
+
60
+ // Kill-switch
61
+ if (process.env.ARIA_PREPROMPT_CONSULT === 'off') {
62
+ audit('skip-killswitch', 'env ARIA_PREPROMPT_CONSULT=off');
63
+ process.exit(0);
64
+ }
65
+
66
+ // Read event JSON from stdin
67
+ let input = '';
68
+ for await (const chunk of process.stdin) input += chunk;
69
+
70
+ let event;
71
+ try {
72
+ event = JSON.parse(input);
73
+ } catch {
74
+ audit('skip-parse-error', 'stdin not JSON');
75
+ process.exit(0);
76
+ }
77
+
78
+ const userPrompt = (event.prompt ?? event.user_message ?? event.message ?? '').toString();
79
+ const sessionId = event.session_id ?? event.sessionId ?? 'claude-code-unknown';
80
+
81
+ // Trivial prompts skip auto-consult — short acks, slash commands, single-word
82
+ // messages don't benefit from architectural consultation.
83
+ if (!userPrompt || userPrompt.length < MIN_PROMPT_CHARS) {
84
+ audit('skip-trivial', `chars=${userPrompt.length}`);
85
+ process.exit(0);
86
+ }
87
+
88
+ // Skip slash-command-only prompts (these are CLI-internal, not architectural)
89
+ if (/^\s*\//.test(userPrompt) && userPrompt.length < 200) {
90
+ audit('skip-slash-command', userPrompt.slice(0, 60));
91
+ process.exit(0);
92
+ }
93
+
94
+ // Compose the consultation brief. Architect role + thinker tier explicitly
95
+ // requested via model='deepseek-v4-pro'. expectStructuredOutput=false because
96
+ // we want plain prose direction we can inject as text, not a JSON file-list.
97
+ const brief = `Pre-prompt direction request from Claude orchestrator.
98
+
99
+ The user just submitted this prompt:
100
+
101
+ ---
102
+ ${userPrompt.slice(0, 2000)}
103
+ ---
104
+
105
+ Apply your 8 lenses and your substrate (distilled_principles, prior decisions,
106
+ garden state, harness packet rules, doctrine memories) to give the orchestrator
107
+ substrate-grounded DIRECTION on how to handle this prompt before Claude starts
108
+ thinking from training reflex. Be concrete:
109
+
110
+ 1. What's the user actually asking for, beneath the literal words?
111
+ 2. What substrate is relevant — name specific doctrine memories
112
+ (feedback_*.md / project_*.md), prior decisions, or fitrah axioms.
113
+ 3. What's the right next action — code / consult / clarify / refuse?
114
+ If clarify: what specific substrate-grounded question reduces ambiguity
115
+ (NOT a reflexive "want me to" deferral).
116
+ 4. Mizan check: any risk patterns in this prompt — over-scope creep,
117
+ over-replacement temptation, tier-substitution temptation, etc.
118
+
119
+ Keep direction under 1500 chars. This is the pre-load context for Claude's
120
+ turn — not the final response. Claude will still emit cognition + action;
121
+ this primes the substrate so reflexive deferral isn't the path of least
122
+ resistance.`;
123
+
124
+ const body = JSON.stringify({
125
+ brief,
126
+ model: 'deepseek-v4-pro',
127
+ sessionId: `preprompt-${sessionId}-${Date.now()}`,
128
+ userId: 'claude-orchestrator-preprompt',
129
+ roleProfile: 'architect',
130
+ expectStructuredOutput: false,
131
+ isCreativeMode: false,
132
+ });
133
+
134
+ let directionText = '';
135
+ try {
136
+ const resp = await fetch(`${HARNESS_URL}/api/harness/delegate`, {
137
+ method: 'POST',
138
+ headers: {
139
+ 'Content-Type': 'application/json',
140
+ Authorization: `Bearer ${HARNESS_TOKEN}`,
141
+ },
142
+ body,
143
+ });
144
+
145
+ if (!resp.ok) {
146
+ audit('skip-http-error', `status=${resp.status}`);
147
+ process.exit(0);
148
+ }
149
+
150
+ const data = await resp.json();
151
+ directionText = (data.response || '').toString().slice(0, MAX_DIRECTION_CHARS);
152
+ } catch (err) {
153
+ audit('skip-network-error', (err && err.message ? err.message : String(err)).slice(0, 200));
154
+ process.exit(0);
155
+ }
156
+
157
+ if (!directionText || directionText.length < 60) {
158
+ audit('skip-empty-direction', `chars=${directionText.length}`);
159
+ process.exit(0);
160
+ }
161
+
162
+ const context = `[ARIA_DIRECTION — substrate-grounded read on this prompt, pre-loaded before Claude's reasoning. Use this as the starting point, not generic Claude reflexes.]
163
+
164
+ ${directionText}
165
+
166
+ [/ARIA_DIRECTION]`;
167
+
168
+ audit('inject', `chars=${directionText.length} prompt-chars=${userPrompt.length}`);
169
+ console.log(JSON.stringify({
170
+ hookSpecificOutput: {
171
+ hookEventName: 'UserPromptSubmit',
172
+ additionalContext: context,
173
+ },
174
+ }));
175
+ process.exit(0);
@@ -0,0 +1,222 @@
1
+ #!/usr/bin/env node
2
+ // Aria Stop-hook gate — enforces 8-lens cognition on text-decision responses.
3
+ //
4
+ // The companion to aria-pre-tool-gate.mjs. The PreToolUse gate catches
5
+ // non-trivial Bash; this Stop hook catches non-trivial TEXT decisions
6
+ // — agreements, scope changes, picks between options, "yes ship it"
7
+ // replies. Same forcing-function pattern, applied at the missing
8
+ // surface.
9
+ //
10
+ // Direction: Hamza 2026-04-26 — "you not doing 8 lens till i ask and
11
+ // discovering the actions u doing are wrong are hard gates that u
12
+ // keep bypassing that prevent exactly what just happened." The
13
+ // PreToolUse gate is tool-coupled; doctrine is action-coupled.
14
+ // Reflexive text decisions are non-trivial actions that this hook
15
+ // now catches.
16
+ //
17
+ // Doctrine bindings (same as PreToolUse gate):
18
+ // - EIGHT_LENS_DOCTRINE.md — substantive 4+ lens application required
19
+ // - feedback_apply_lenses_dont_perform_them.md — block ceremonial cognition
20
+ // - feedback_8lens_before_every_action_including_text.md — the rule this enforces
21
+ //
22
+ // Trigger: runs at Stop event after every assistant response. Reads
23
+ // the just-emitted assistant text from the transcript. If non-trivial
24
+ // (per the same triviality threshold as eight-lens-detector.ts) AND
25
+ // missing 4+ substantive lenses, blocks the response.
26
+ //
27
+ // Triviality threshold (mirrors eight-lens-detector.ts):
28
+ // - Trivial acks (e.g. "got it", "ok", "done") pass
29
+ // - Short responses (<300 chars) without decision-signal phrases pass
30
+ // - Otherwise: require 4+ substantive lenses
31
+ //
32
+ // Substance check (mirrors aria-pre-tool-gate.mjs):
33
+ // - Each lens must have ≥20 chars of non-placeholder content
34
+ // - Bare lens-name mentions in prose don't count
35
+ // - <placeholder> template values don't count
36
+ //
37
+ // No bypass mechanism — same v3 doctrine as the PreToolUse gate.
38
+ // Kill-switch: ARIA_STOP_GATE=off (env, audit-logged).
39
+
40
+ import { readFileSync, appendFileSync, existsSync, mkdirSync } from 'node:fs';
41
+ import { dirname } from 'node:path';
42
+
43
+ const HOME = process.env.HOME || '/tmp';
44
+ const LOG = `${HOME}/.claude/aria-stop-gate.log`;
45
+
46
+ function audit(decision, summary) {
47
+ try {
48
+ if (!existsSync(dirname(LOG))) mkdirSync(dirname(LOG), { recursive: true });
49
+ appendFileSync(LOG, `${new Date().toISOString()} ${decision} ${summary}\n`);
50
+ } catch {}
51
+ }
52
+
53
+ // Kill-switch
54
+ if (process.env.ARIA_STOP_GATE === 'off') {
55
+ audit('bypass-killswitch', 'env ARIA_STOP_GATE=off');
56
+ process.exit(0);
57
+ }
58
+
59
+ // Lens substance check — same constants as aria-pre-tool-gate.mjs
60
+ const LENS_NAMES = ['nur', 'mizan', 'hikma', 'tafakkur', 'tadabbur', 'ilham', 'wahi', 'firasah'];
61
+ const REQUIRED_LENSES = 4;
62
+ const SUBSTANCE_MIN_CHARS = 20;
63
+ const PLACEHOLDER_RX = /^\s*<[^<>]+>\s*$/;
64
+ const COGNITION_BLOCK_RX = /<cognition>([\s\S]*?)<\/cognition>/i;
65
+
66
+ // Triviality (mirrors eight-lens-detector.ts)
67
+ const NON_TRIVIAL_MIN_CHARS = 300;
68
+ 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;
69
+ const TRIVIAL_ACK_RX = /^(?:got it|on it|ok|sure|yes|no|done|ack|👍|✓)\b/i;
70
+
71
+ function detectCognitionLenses(text) {
72
+ if (!text) return { count: 0, names: [] };
73
+ const block = text.match(COGNITION_BLOCK_RX);
74
+ const searchSpace = block ? block[1] : text;
75
+ const names = [];
76
+ for (const lens of LENS_NAMES) {
77
+ const lensRx = new RegExp(
78
+ `\\b${lens}\\s*:\\s*([^\\n]*(?:\\n(?!\\s*(?:${LENS_NAMES.join('|')})\\s*:|<\\/cognition>)[^\\n]*)*)`,
79
+ 'i',
80
+ );
81
+ const m = searchSpace.match(lensRx);
82
+ if (!m) continue;
83
+ const content = (m[1] || '').trim();
84
+ if (content.length < SUBSTANCE_MIN_CHARS) continue;
85
+ if (PLACEHOLDER_RX.test(content)) continue;
86
+ names.push(lens);
87
+ }
88
+ return { count: names.length, names };
89
+ }
90
+
91
+ // Read event JSON from stdin (Claude Code spec).
92
+ let input = '';
93
+ for await (const chunk of process.stdin) input += chunk;
94
+
95
+ let event;
96
+ try {
97
+ event = JSON.parse(input);
98
+ } catch {
99
+ audit('allow-parse-error', 'stdin not JSON');
100
+ process.exit(0);
101
+ }
102
+
103
+ // Read assistant text from THIS turn — Claude Code splits a single
104
+ // logical assistant response into multiple transcript entries by
105
+ // content-block type (one entry for `thinking`, one for `text`, one
106
+ // for each `tool_use`). The Stop-gate must accumulate ALL text blocks
107
+ // since the last user-message boundary, not just the most recent
108
+ // entry — otherwise we miss cognition emitted before tool_use blocks.
109
+ //
110
+ // (Bug fix 2026-04-26: prior implementation read only the latest
111
+ // `assistant` entry's text content. When responses had cognition
112
+ // + tool_use + short post-tool-result text, only the post-tool-result
113
+ // text was inspected — empty of cognition. Audit log showed 0/4
114
+ // lenses on chars=1445 even though the turn had 8 substantive lenses
115
+ // in an earlier text block.)
116
+ // System-reminder skip — same percentage-based logic as aria-pre-tool-gate.mjs.
117
+ // Runtime-injected user-role messages (block errors, task-notifications,
118
+ // harness packet preview) shouldn't count as turn boundaries. Old
119
+ // implementation stopped at the FIRST user message which made block-error
120
+ // retries with cognition-in-prior-turn impossible to recover from.
121
+ 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;
122
+ const SYSTEM_REMINDER_THRESHOLD = 0.6;
123
+
124
+ const transcriptPath = event.transcript_path ?? event.transcriptPath;
125
+ let assistantText = '';
126
+ if (transcriptPath && existsSync(transcriptPath)) {
127
+ try {
128
+ const lines = readFileSync(transcriptPath, 'utf-8').split('\n').filter(Boolean);
129
+ const textChunks = [];
130
+ for (let i = lines.length - 1; i >= 0; i--) {
131
+ try {
132
+ const m = JSON.parse(lines[i]);
133
+ const role = m.message?.role ?? m.role;
134
+ if (role === 'user') {
135
+ // Skip runtime-injected reminders (predominant reminder content).
136
+ // Real user voice = boundary; reminder-only message = continue.
137
+ const content = m.message?.content ?? m.content ?? [];
138
+ const isToolResultOnly = Array.isArray(content) &&
139
+ content.length > 0 &&
140
+ content.every((b) => b && b.type === 'tool_result');
141
+ if (isToolResultOnly) continue;
142
+ const textContent = Array.isArray(content)
143
+ ? content.filter((b) => b && b.type === 'text').map((b) => b.text || '').join('\n')
144
+ : (typeof content === 'string' ? content : '');
145
+ if (textContent) {
146
+ const reminderMatches = textContent.match(SYSTEM_REMINDER_RX) || [];
147
+ if (reminderMatches.length > 0) {
148
+ const reminderChars = reminderMatches.reduce((s, x) => s + x.length, 0);
149
+ const fraction = reminderChars / Math.max(1, textContent.length);
150
+ if (fraction >= SYSTEM_REMINDER_THRESHOLD) continue;
151
+ }
152
+ }
153
+ // Real user message — that's the turn boundary.
154
+ break;
155
+ }
156
+ if (role !== 'assistant') continue;
157
+ const content = m.message?.content ?? m.content ?? [];
158
+ if (!Array.isArray(content)) continue;
159
+ const text = content
160
+ .filter((b) => b && b.type === 'text')
161
+ .map((b) => b.text || '')
162
+ .join('\n');
163
+ if (text) textChunks.push(text);
164
+ } catch {}
165
+ }
166
+ // Reverse so chunks are in chronological order (we walked backward).
167
+ assistantText = textChunks.reverse().join('\n\n');
168
+ } catch {}
169
+ }
170
+
171
+ if (!assistantText) {
172
+ audit('allow-no-text', 'no assistant text in transcript');
173
+ process.exit(0);
174
+ }
175
+
176
+ // Triviality check — same as eight-lens-detector.ts
177
+ const trimmed = assistantText.trim();
178
+ if (TRIVIAL_ACK_RX.test(trimmed)) {
179
+ audit('allow-trivial-ack', `chars=${trimmed.length}`);
180
+ process.exit(0);
181
+ }
182
+
183
+ const isLong = assistantText.length >= NON_TRIVIAL_MIN_CHARS;
184
+ const hasDecisionSignal = DECISION_SIGNAL_RX.test(assistantText);
185
+ const triggered = isLong || hasDecisionSignal;
186
+
187
+ if (!triggered) {
188
+ audit('allow-trivial', `chars=${assistantText.length} hasDecision=${hasDecisionSignal}`);
189
+ process.exit(0);
190
+ }
191
+
192
+ // Non-trivial response — require substantive cognition.
193
+ const cog = detectCognitionLenses(assistantText);
194
+
195
+ if (cog.count >= REQUIRED_LENSES) {
196
+ audit('allow-cognition', `lenses=${cog.count} chars=${assistantText.length}`);
197
+ process.exit(0);
198
+ }
199
+
200
+ // Block — non-trivial response without 4+ substantive lenses.
201
+ const reason = `Aria Stop-gate: non-trivial assistant response without 4+ substantive cognition lenses. Found ${cog.count}/${REQUIRED_LENSES}+ (lenses: ${cog.names.join(', ') || 'none'}). Per EIGHT_LENS_DOCTRINE.md, doctrine is action-coupled — text decisions ARE actions, and reflexive replies fail this gate the same way reflexive Bash does.
202
+
203
+ Re-emit the response with substantive lens application BEFORE drafting. Each lens must have ≥${SUBSTANCE_MIN_CHARS} chars of non-placeholder content:
204
+
205
+ <cognition>
206
+ nur: <what you actually see — specific to the decision, not a placeholder>
207
+ mizan: <real risk read — what's out of proportion>
208
+ hikma: <doctrine that applies — name the source>
209
+ tafakkur: <deep structural read — go beneath the surface>
210
+ tadabbur: <if-then chain — what follows from what>
211
+ ilham: <distant connection — what's not obvious>
212
+ wahi: <what just landed — what changed in this exchange>
213
+ firasah: <what user actually needs — beneath the literal ask>
214
+ </cognition>
215
+
216
+ 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.
217
+
218
+ No per-command bypass (mirrors aria-pre-tool-gate.mjs v3 doctrine). Kill-switch: ARIA_STOP_GATE=off env (logged, emergency only). If the gate misfires on legitimate cognition, fix the gate.`;
219
+
220
+ audit(`block`, `lenses=${cog.count}/${REQUIRED_LENSES} chars=${assistantText.length}`);
221
+ console.log(JSON.stringify({ decision: 'block', reason }));
222
+ process.exit(2);
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@aria_asi/cli",
3
+ "version": "0.2.0",
4
+ "description": "Aria Smart CLI — the world's first harness-powered terminal companion",
5
+ "bin": {
6
+ "aria": "./bin/aria.js"
7
+ },
8
+ "main": "./dist/aria-connector/src/index.js",
9
+ "types": "./dist/aria-connector/src/index.d.ts",
10
+ "type": "module",
11
+ "publishConfig": {
12
+ "registry": "https://npm.pkg.github.com",
13
+ "access": "restricted"
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/REI-Nationwide/cowork-sandbox.git"
18
+ },
19
+ "scripts": {
20
+ "build": "tsc",
21
+ "prepare": "npm run build",
22
+ "dev": "tsc --watch",
23
+ "publish:all": "bash scripts/publish-all.sh",
24
+ "publish:npm": "npm publish --registry https://registry.npmjs.org --access public",
25
+ "publish:github": "npm publish --registry https://npm.pkg.github.com",
26
+ "publish:docker": "bash scripts/publish-docker.sh",
27
+ "prepublishOnly": "npm run build"
28
+ },
29
+ "dependencies": {
30
+ "chokidar": "^4.0.0"
31
+ },
32
+ "devDependencies": {
33
+ "@types/node": "^22.0.0",
34
+ "typescript": "^5.7.0"
35
+ },
36
+ "files": [
37
+ "bin",
38
+ "dist",
39
+ "src",
40
+ "hooks"
41
+ ],
42
+ "engines": {
43
+ "node": ">=20.0.0"
44
+ },
45
+ "license": "UNLICENSED",
46
+ "private": false
47
+ }
@@ -0,0 +1,132 @@
1
+ import { login, status, logout, revoke } from '../auth-commands';
2
+ import { loadLicense, saveLicense } from '../auth';
3
+ import { harnessClient } from '../harness-client';
4
+ import * as fs from 'fs';
5
+ import * as path from 'path';
6
+
7
+ jest.mock('../auth');
8
+ jest.mock('../harness-client');
9
+
10
+ const LICENSE_PATH = path.join(require('os').homedir(), '.aria', 'license.json');
11
+
12
+ const mockClaims = {
13
+ jti: 'test-jti-123',
14
+ sub: 'user-test',
15
+ tier: 'pro',
16
+ exp: Math.floor(Date.now() / 1000) + 86400,
17
+ iat: Math.floor(Date.now() / 1000),
18
+ };
19
+
20
+ beforeEach(() => {
21
+ jest.clearAllMocks();
22
+ // Clean up any leftover license file
23
+ try { fs.unlinkSync(LICENSE_PATH); } catch {}
24
+ });
25
+
26
+ describe('login', () => {
27
+ it('should validate token and persist license', async () => {
28
+ (harnessClient.get as jest.Mock).mockResolvedValue({
29
+ ok: true,
30
+ json: () => Promise.resolve({ claims: mockClaims }),
31
+ });
32
+
33
+ const result = await login('valid-token');
34
+
35
+ expect(result.ok).toBe(true);
36
+ expect(result.tier).toBe('pro');
37
+ expect(result.jti).toBe('test-jti-123');
38
+ expect(result.expiresAt).toBeDefined();
39
+
40
+ // Verify file was written with mode 0600
41
+ const written = JSON.parse(fs.readFileSync(LICENSE_PATH, 'utf-8'));
42
+ expect(written.jti).toBe('test-jti-123');
43
+ });
44
+
45
+ it('should return error on invalid token', async () => {
46
+ (harnessClient.get as jest.Mock).mockResolvedValue({
47
+ ok: false,
48
+ json: () => Promise.resolve({ error: 'Invalid token' }),
49
+ });
50
+
51
+ const result = await login('bad-token');
52
+ expect(result.ok).toBe(false);
53
+ expect(result.error).toBe('Invalid token');
54
+ });
55
+ });
56
+
57
+ describe('status', () => {
58
+ it('should return logged out when no local license', async () => {
59
+ (loadLicense as jest.Mock).mockReturnValue(null);
60
+
61
+ const result = await status();
62
+ expect(result.loggedIn).toBe(false);
63
+ });
64
+
65
+ it('should return status from server when licensed', async () => {
66
+ (loadLicense as jest.Mock).mockReturnValue(mockClaims);
67
+ (harnessClient.get as jest.Mock).mockResolvedValue({
68
+ ok: true,
69
+ status: 200,
70
+ json: () => Promise.resolve({ claims: mockClaims }),
71
+ });
72
+
73
+ const result = await status();
74
+ expect(result.loggedIn).toBe(true);
75
+ expect(result.tier).toBe('pro');
76
+ expect(result.revoked).toBe(false);
77
+ });
78
+
79
+ it('should detect revoked license from 410', async () => {
80
+ (loadLicense as jest.Mock).mockReturnValue(mockClaims);
81
+ (harnessClient.get as jest.Mock).mockResolvedValue({
82
+ ok: false,
83
+ status: 410,
84
+ });
85
+
86
+ const result = await status();
87
+ expect(result.loggedIn).toBe(true);
88
+ expect(result.revoked).toBe(true);
89
+ });
90
+ });
91
+
92
+ describe('logout', () => {
93
+ it('should delete license file', async () => {
94
+ // Create a fake license file
95
+ fs.writeFileSync(LICENSE_PATH, JSON.stringify(mockClaims), { mode: 0o600 });
96
+ expect(fs.existsSync(LICENSE_PATH)).toBe(true);
97
+
98
+ const result = await logout();
99
+ expect(result.ok).toBe(true);
100
+ expect(fs.existsSync(LICENSE_PATH)).toBe(false);
101
+ });
102
+
103
+ it('should succeed even without license file', async () => {
104
+ const result = await logout();
105
+ expect(result.ok).toBe(true);
106
+ });
107
+ });
108
+
109
+ describe('revoke', () => {
110
+ it('should revoke license server-side and clear local', async () => {
111
+ (loadLicense as jest.Mock).mockReturnValue(mockClaims);
112
+ (harnessClient.post as jest.Mock).mockResolvedValue({
113
+ ok: true,
114
+ json: () => Promise.resolve({}),
115
+ });
116
+
117
+ const result = await revoke('Test revocation');
118
+ expect(result.ok).toBe(true);
119
+ expect(result.jti).toBe('test-jti-123');
120
+ expect(harnessClient.post).toHaveBeenCalledWith('/api/license/revoke', {
121
+ body: { jti: 'test-jti-123', reason: 'Test revocation', revokedBy: 'self' },
122
+ });
123
+ });
124
+
125
+ it('should fail without active license', async () => {
126
+ (loadLicense as jest.Mock).mockReturnValue(null);
127
+
128
+ const result = await revoke();
129
+ expect(result.ok).toBe(false);
130
+ expect(result.error).toBe('No active license to revoke');
131
+ });
132
+ });