@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.
- package/bin/aria.js +168 -0
- package/dist/aria-connector/src/auth-commands.d.ts +28 -0
- package/dist/aria-connector/src/auth-commands.d.ts.map +1 -0
- package/dist/aria-connector/src/auth-commands.js +129 -0
- package/dist/aria-connector/src/auth-commands.js.map +1 -0
- package/dist/aria-connector/src/auth.d.ts +12 -0
- package/dist/aria-connector/src/auth.d.ts.map +1 -0
- package/dist/aria-connector/src/auth.js +31 -0
- package/dist/aria-connector/src/auth.js.map +1 -0
- package/dist/aria-connector/src/auto-mcp.d.ts +23 -0
- package/dist/aria-connector/src/auto-mcp.d.ts.map +1 -0
- package/dist/aria-connector/src/auto-mcp.js +994 -0
- package/dist/aria-connector/src/auto-mcp.js.map +1 -0
- package/dist/aria-connector/src/chat.d.ts +21 -0
- package/dist/aria-connector/src/chat.d.ts.map +1 -0
- package/dist/aria-connector/src/chat.js +332 -0
- package/dist/aria-connector/src/chat.js.map +1 -0
- package/dist/aria-connector/src/codebase-scanner.d.ts +7 -0
- package/dist/aria-connector/src/codebase-scanner.d.ts.map +1 -0
- package/dist/aria-connector/src/codebase-scanner.js +6 -0
- package/dist/aria-connector/src/codebase-scanner.js.map +1 -0
- package/dist/aria-connector/src/cognition-log.d.ts +17 -0
- package/dist/aria-connector/src/cognition-log.d.ts.map +1 -0
- package/dist/aria-connector/src/cognition-log.js +19 -0
- package/dist/aria-connector/src/cognition-log.js.map +1 -0
- package/dist/aria-connector/src/config.d.ts +41 -0
- package/dist/aria-connector/src/config.d.ts.map +1 -0
- package/dist/aria-connector/src/config.js +50 -0
- package/dist/aria-connector/src/config.js.map +1 -0
- package/dist/aria-connector/src/connectors/claude-code.d.ts +4 -0
- package/dist/aria-connector/src/connectors/claude-code.d.ts.map +1 -0
- package/dist/aria-connector/src/connectors/claude-code.js +204 -0
- package/dist/aria-connector/src/connectors/claude-code.js.map +1 -0
- package/dist/aria-connector/src/connectors/cursor.d.ts +4 -0
- package/dist/aria-connector/src/connectors/cursor.d.ts.map +1 -0
- package/dist/aria-connector/src/connectors/cursor.js +63 -0
- package/dist/aria-connector/src/connectors/cursor.js.map +1 -0
- package/dist/aria-connector/src/connectors/opencode.d.ts +4 -0
- package/dist/aria-connector/src/connectors/opencode.d.ts.map +1 -0
- package/dist/aria-connector/src/connectors/opencode.js +102 -0
- package/dist/aria-connector/src/connectors/opencode.js.map +1 -0
- package/dist/aria-connector/src/connectors/shell.d.ts +4 -0
- package/dist/aria-connector/src/connectors/shell.d.ts.map +1 -0
- package/dist/aria-connector/src/connectors/shell.js +58 -0
- package/dist/aria-connector/src/connectors/shell.js.map +1 -0
- package/dist/aria-connector/src/garden-client.d.ts +19 -0
- package/dist/aria-connector/src/garden-client.d.ts.map +1 -0
- package/dist/aria-connector/src/garden-client.js +85 -0
- package/dist/aria-connector/src/garden-client.js.map +1 -0
- package/dist/aria-connector/src/garden-control-plane.d.ts +22 -0
- package/dist/aria-connector/src/garden-control-plane.d.ts.map +1 -0
- package/dist/aria-connector/src/garden-control-plane.js +43 -0
- package/dist/aria-connector/src/garden-control-plane.js.map +1 -0
- package/dist/aria-connector/src/harness-client.d.ts +166 -0
- package/dist/aria-connector/src/harness-client.d.ts.map +1 -0
- package/dist/aria-connector/src/harness-client.js +344 -0
- package/dist/aria-connector/src/harness-client.js.map +1 -0
- package/dist/aria-connector/src/hive-client.d.ts +32 -0
- package/dist/aria-connector/src/hive-client.d.ts.map +1 -0
- package/dist/aria-connector/src/hive-client.js +69 -0
- package/dist/aria-connector/src/hive-client.js.map +1 -0
- package/dist/aria-connector/src/index.d.ts +19 -0
- package/dist/aria-connector/src/index.d.ts.map +1 -0
- package/dist/aria-connector/src/index.js +13 -0
- package/dist/aria-connector/src/index.js.map +1 -0
- package/dist/aria-connector/src/install-hooks.d.ts +18 -0
- package/dist/aria-connector/src/install-hooks.d.ts.map +1 -0
- package/dist/aria-connector/src/install-hooks.js +224 -0
- package/dist/aria-connector/src/install-hooks.js.map +1 -0
- package/dist/aria-connector/src/model-context.d.ts +8 -0
- package/dist/aria-connector/src/model-context.d.ts.map +1 -0
- package/dist/aria-connector/src/model-context.js +83 -0
- package/dist/aria-connector/src/model-context.js.map +1 -0
- package/dist/aria-connector/src/persona.d.ts +27 -0
- package/dist/aria-connector/src/persona.d.ts.map +1 -0
- package/dist/aria-connector/src/persona.js +86 -0
- package/dist/aria-connector/src/persona.js.map +1 -0
- package/dist/aria-connector/src/providers/anthropic.d.ts +4 -0
- package/dist/aria-connector/src/providers/anthropic.d.ts.map +1 -0
- package/dist/aria-connector/src/providers/anthropic.js +92 -0
- package/dist/aria-connector/src/providers/anthropic.js.map +1 -0
- package/dist/aria-connector/src/providers/deepseek.d.ts +3 -0
- package/dist/aria-connector/src/providers/deepseek.d.ts.map +1 -0
- package/dist/aria-connector/src/providers/deepseek.js +28 -0
- package/dist/aria-connector/src/providers/deepseek.js.map +1 -0
- package/dist/aria-connector/src/providers/google.d.ts +3 -0
- package/dist/aria-connector/src/providers/google.d.ts.map +1 -0
- package/dist/aria-connector/src/providers/google.js +38 -0
- package/dist/aria-connector/src/providers/google.js.map +1 -0
- package/dist/aria-connector/src/providers/ollama.d.ts +3 -0
- package/dist/aria-connector/src/providers/ollama.d.ts.map +1 -0
- package/dist/aria-connector/src/providers/ollama.js +28 -0
- package/dist/aria-connector/src/providers/ollama.js.map +1 -0
- package/dist/aria-connector/src/providers/openai.d.ts +4 -0
- package/dist/aria-connector/src/providers/openai.d.ts.map +1 -0
- package/dist/aria-connector/src/providers/openai.js +84 -0
- package/dist/aria-connector/src/providers/openai.js.map +1 -0
- package/dist/aria-connector/src/providers/openrouter.d.ts +3 -0
- package/dist/aria-connector/src/providers/openrouter.d.ts.map +1 -0
- package/dist/aria-connector/src/providers/openrouter.js +30 -0
- package/dist/aria-connector/src/providers/openrouter.js.map +1 -0
- package/dist/aria-connector/src/providers/types.d.ts +20 -0
- package/dist/aria-connector/src/providers/types.d.ts.map +1 -0
- package/dist/aria-connector/src/providers/types.js +2 -0
- package/dist/aria-connector/src/providers/types.js.map +1 -0
- package/dist/aria-connector/src/setup-wizard.d.ts +2 -0
- package/dist/aria-connector/src/setup-wizard.d.ts.map +1 -0
- package/dist/aria-connector/src/setup-wizard.js +140 -0
- package/dist/aria-connector/src/setup-wizard.js.map +1 -0
- package/dist/aria-connector/src/types.d.ts +30 -0
- package/dist/aria-connector/src/types.d.ts.map +1 -0
- package/dist/aria-connector/src/types.js +5 -0
- package/dist/aria-connector/src/types.js.map +1 -0
- package/dist/aria-web/src/lib/codebase-scanner.d.ts +127 -0
- package/dist/aria-web/src/lib/codebase-scanner.d.ts.map +1 -0
- package/dist/aria-web/src/lib/codebase-scanner.js +1730 -0
- package/dist/aria-web/src/lib/codebase-scanner.js.map +1 -0
- package/dist/cli-0.2.0.tgz +0 -0
- package/dist/install.sh +13 -0
- package/hooks/aria-harness-via-sdk.mjs +317 -0
- package/hooks/aria-pre-tool-gate.mjs +596 -0
- package/hooks/aria-preprompt-consult.mjs +175 -0
- package/hooks/aria-stop-gate.mjs +222 -0
- package/package.json +47 -0
- package/src/__tests__/auth-commands.test.ts +132 -0
- package/src/auth-commands.ts +175 -0
- package/src/auth.ts +33 -0
- package/src/auto-mcp.ts +1172 -0
- package/src/chat.ts +387 -0
- package/src/codebase-scanner.ts +18 -0
- package/src/cognition-log.ts +30 -0
- package/src/config.ts +94 -0
- package/src/connectors/claude-code.ts +213 -0
- package/src/connectors/cursor.ts +75 -0
- package/src/connectors/opencode.ts +115 -0
- package/src/connectors/shell.ts +72 -0
- package/src/garden-client.ts +98 -0
- package/src/garden-control-plane.ts +108 -0
- package/src/harness-client.ts +454 -0
- package/src/hive-client.ts +104 -0
- package/src/index.ts +26 -0
- package/src/install-hooks.ts +259 -0
- package/src/model-context.ts +88 -0
- package/src/persona.ts +113 -0
- package/src/providers/anthropic.ts +120 -0
- package/src/providers/deepseek.ts +40 -0
- package/src/providers/google.ts +57 -0
- package/src/providers/ollama.ts +43 -0
- package/src/providers/openai.ts +108 -0
- package/src/providers/openrouter.ts +42 -0
- package/src/providers/types.ts +35 -0
- package/src/setup-wizard.ts +177 -0
- 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
|
+
});
|