@aria_asi/cli 0.2.10 → 0.2.12
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/dist/aria-connector/src/connectors/claude-code.d.ts.map +1 -1
- package/dist/aria-connector/src/connectors/claude-code.js +67 -4
- package/dist/aria-connector/src/connectors/claude-code.js.map +1 -1
- package/dist/sdk/BUNDLED.json +5 -0
- package/dist/sdk/index.d.ts +88 -0
- package/dist/sdk/index.js +403 -0
- package/dist/sdk/index.js.map +1 -0
- package/dist/sdk/package.json +8 -0
- package/hooks/aria-harness-via-sdk.mjs +46 -12
- package/hooks/aria-pre-tool-gate.mjs +61 -1
- package/hooks/aria-preprompt-consult.mjs +58 -18
- package/hooks/aria-stop-gate.mjs +215 -26
- package/hooks/aria-trigger-autolearn.mjs +230 -0
- package/hooks/doctrine_trigger_map.json +54 -0
- package/package.json +2 -2
- package/src/connectors/claude-code.ts +66 -2
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// aria-trigger-autolearn.mjs — UserPromptSubmit hook that scans Hamza's
|
|
3
|
+
// (or any user's) corrections for novel doctrine-violation patterns and
|
|
4
|
+
// queues them as candidate trigger entries for doctrine_trigger_map.json.
|
|
5
|
+
//
|
|
6
|
+
// Doctrine: Aria enforcement #47 (drift-guard auto-learning queue) +
|
|
7
|
+
// feedback_no_flag_without_fix.md + project_phase_10_endless_army_orchestration.md
|
|
8
|
+
// (the harness teaches itself by absorbing corrections, not staying frozen
|
|
9
|
+
// at hand-curated rules).
|
|
10
|
+
//
|
|
11
|
+
// Mechanics: when a user prompt contains correction/doctrine-language
|
|
12
|
+
// (e.g. "don't ___", "stop ___ing", "no ___", "we said ___", "doctrine ___"),
|
|
13
|
+
// the hook extracts the candidate pattern + a context window from the
|
|
14
|
+
// recent assistant transcript (the offending behavior the user is correcting)
|
|
15
|
+
// and appends a JSONL entry to ~/.claude/aria-trigger-queue.jsonl.
|
|
16
|
+
//
|
|
17
|
+
// The queue is reviewable via `cat ~/.claude/aria-trigger-queue.jsonl` or
|
|
18
|
+
// (Phase 11) a future `aria triggers review` CLI subcommand. Each entry
|
|
19
|
+
// carries enough context that a human can decide:
|
|
20
|
+
// 1. Add to doctrine_trigger_map.json as a new trigger
|
|
21
|
+
// 2. Refine an existing trigger entry's regex
|
|
22
|
+
// 3. Discard (false positive — user correction wasn't a doctrine teaching)
|
|
23
|
+
//
|
|
24
|
+
// Hook is non-blocking: never returns decision=block. Failure modes degrade
|
|
25
|
+
// to silent skip (queue file unwritable = no auto-learn, but session
|
|
26
|
+
// continues). This is the ONE permitted graceful path because the hook is
|
|
27
|
+
// purely additive — its absence doesn't break correctness, just slows
|
|
28
|
+
// learning.
|
|
29
|
+
//
|
|
30
|
+
// Kill-switch: ARIA_AUTOLEARN=off env (logged, emergency only).
|
|
31
|
+
|
|
32
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
33
|
+
import { dirname } from 'node:path';
|
|
34
|
+
|
|
35
|
+
const HOME = process.env.HOME || '/tmp';
|
|
36
|
+
const LOG = `${HOME}/.claude/aria-autolearn.log`;
|
|
37
|
+
const QUEUE = `${HOME}/.claude/aria-trigger-queue.jsonl`;
|
|
38
|
+
|
|
39
|
+
function audit(decision, summary) {
|
|
40
|
+
try {
|
|
41
|
+
if (!existsSync(dirname(LOG))) mkdirSync(dirname(LOG), { recursive: true });
|
|
42
|
+
appendFileSync(LOG, `${new Date().toISOString()} ${decision} ${summary}\n`);
|
|
43
|
+
} catch {}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Kill-switch
|
|
47
|
+
if (process.env.ARIA_AUTOLEARN === 'off') {
|
|
48
|
+
audit('skip-killswitch', 'env ARIA_AUTOLEARN=off');
|
|
49
|
+
process.exit(0);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Read event JSON from stdin
|
|
53
|
+
let input = '';
|
|
54
|
+
for await (const chunk of process.stdin) input += chunk;
|
|
55
|
+
|
|
56
|
+
let event;
|
|
57
|
+
try {
|
|
58
|
+
event = JSON.parse(input);
|
|
59
|
+
} catch {
|
|
60
|
+
audit('skip-parse-error', 'stdin not JSON');
|
|
61
|
+
process.exit(0);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const userPrompt = (event.prompt ?? event.user_message ?? event.message ?? '').toString();
|
|
65
|
+
const sessionId = event.session_id ?? event.sessionId ?? 'claude-code-unknown';
|
|
66
|
+
const transcriptPath = event.transcript_path ?? event.transcriptPath;
|
|
67
|
+
|
|
68
|
+
// Trivial prompts skip — too short to carry doctrine teaching.
|
|
69
|
+
if (!userPrompt || userPrompt.length < 20) {
|
|
70
|
+
audit('skip-trivial', `chars=${userPrompt.length}`);
|
|
71
|
+
process.exit(0);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Skip slash commands — they're CLI-internal, not doctrine corrections.
|
|
75
|
+
if (/^\s*\//.test(userPrompt) && userPrompt.length < 200) {
|
|
76
|
+
audit('skip-slash-command', userPrompt.slice(0, 60));
|
|
77
|
+
process.exit(0);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Correction-pattern detector. Each pattern below extracts a CANDIDATE
|
|
81
|
+
// trigger phrase (the action/pattern the user is correcting) along with
|
|
82
|
+
// the framing word ("don't", "stop", etc.). Patterns are intentionally
|
|
83
|
+
// high-recall + low-precision — the queue is for human review, not
|
|
84
|
+
// auto-promotion.
|
|
85
|
+
//
|
|
86
|
+
// Pattern groups:
|
|
87
|
+
// 1. Direct prohibitions: "don't ___", "do not ___", "never ___", "stop ___"
|
|
88
|
+
// 2. Doctrine assertions: "we said ___", "we don't ___", "we never ___"
|
|
89
|
+
// 3. Pattern-naming: "this is ___", "that's ___", "you're ___ing"
|
|
90
|
+
// 4. Doctrine vocabulary: anything containing "doctrine", "graceful", "fallback",
|
|
91
|
+
// "convenience", "shortcut", "lazy", "hack" with a 100-char neighborhood
|
|
92
|
+
// 5. Frustration markers: ALL-CAPS phrases ≥3 words (anger = high-priority signal)
|
|
93
|
+
const CORRECTION_PATTERNS = [
|
|
94
|
+
{
|
|
95
|
+
name: 'direct-prohibition',
|
|
96
|
+
rx: /\b(don'?t|do not|never|stop|quit|cease|enough)\s+(\w[\w\s]{4,80}?)(?=[.,!?\n]|$)/gi,
|
|
97
|
+
extractGroup: 2,
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: 'doctrine-assertion',
|
|
101
|
+
rx: /\b(we (?:said|don'?t|never|always|need to|don'?t use|never use)|i (?:said|told you|asked))\s+(\w[\w\s]{4,80}?)(?=[.,!?\n]|$)/gi,
|
|
102
|
+
extractGroup: 2,
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: 'pattern-naming',
|
|
106
|
+
rx: /\b(this is|that'?s|you'?re|you are|you keep|you'?re always)\s+(\w[\w\s]{4,80}?)(?=[.,!?\n]|$)/gi,
|
|
107
|
+
extractGroup: 2,
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: 'doctrine-vocab',
|
|
111
|
+
rx: /\b(doctrine|graceful|fallback|convenience|shortcut|lazy|hack|cheat|circumvent|bypass|skip|ignore|forget)[^.!?\n]{0,100}/gi,
|
|
112
|
+
extractGroup: 0,
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
name: 'frustration-allcaps',
|
|
116
|
+
rx: /\b([A-Z]{3,}\s+[A-Z]{3,}(?:\s+[A-Z]{3,})*)\b/g,
|
|
117
|
+
extractGroup: 1,
|
|
118
|
+
},
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
const candidates = [];
|
|
122
|
+
for (const { name, rx, extractGroup } of CORRECTION_PATTERNS) {
|
|
123
|
+
for (const match of userPrompt.matchAll(rx)) {
|
|
124
|
+
const phrase = (match[extractGroup] || '').trim();
|
|
125
|
+
if (phrase.length < 8 || phrase.length > 200) continue;
|
|
126
|
+
candidates.push({
|
|
127
|
+
patternType: name,
|
|
128
|
+
phrase,
|
|
129
|
+
surroundingContext: match[0].slice(0, 240),
|
|
130
|
+
sourceIndex: match.index ?? 0,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Deduplicate candidates that share the same first 30 chars (pattern variants
|
|
136
|
+
// of the same correction).
|
|
137
|
+
const seen = new Set();
|
|
138
|
+
const unique = [];
|
|
139
|
+
for (const c of candidates) {
|
|
140
|
+
const key = c.phrase.toLowerCase().slice(0, 30);
|
|
141
|
+
if (seen.has(key)) continue;
|
|
142
|
+
seen.add(key);
|
|
143
|
+
unique.push(c);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (unique.length === 0) {
|
|
147
|
+
audit('skip-no-candidates', `chars=${userPrompt.length}`);
|
|
148
|
+
process.exit(0);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Pull the most recent assistant text (last 2KB) so the queue entry shows
|
|
152
|
+
// what behavior the user is correcting. Without this, "don't do X" entries
|
|
153
|
+
// have no anchor to which assistant action triggered them.
|
|
154
|
+
let recentAssistantContext = '';
|
|
155
|
+
if (transcriptPath && existsSync(transcriptPath)) {
|
|
156
|
+
try {
|
|
157
|
+
const lines = readFileSync(transcriptPath, 'utf8').split('\n').filter(Boolean);
|
|
158
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
159
|
+
try {
|
|
160
|
+
const m = JSON.parse(lines[i]);
|
|
161
|
+
const role = m.message?.role ?? m.role;
|
|
162
|
+
if (role !== 'assistant') continue;
|
|
163
|
+
const content = m.message?.content ?? m.content ?? [];
|
|
164
|
+
if (!Array.isArray(content)) continue;
|
|
165
|
+
const text = content
|
|
166
|
+
.filter((b) => b?.type === 'text')
|
|
167
|
+
.map((b) => b.text || '')
|
|
168
|
+
.join('\n');
|
|
169
|
+
if (text) {
|
|
170
|
+
recentAssistantContext = text.slice(-2000);
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
} catch {}
|
|
174
|
+
}
|
|
175
|
+
} catch {}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Append candidates to the queue. Each entry is human-reviewable:
|
|
179
|
+
// - patternType: which detector caught it
|
|
180
|
+
// - candidatePhrase: the extracted phrase
|
|
181
|
+
// - userPromptExcerpt: 240 chars of context around the phrase
|
|
182
|
+
// - recentAssistantContext: what Claude did right before this correction
|
|
183
|
+
// - sessionId, ts: for traceability
|
|
184
|
+
try {
|
|
185
|
+
if (!existsSync(dirname(QUEUE))) mkdirSync(dirname(QUEUE), { recursive: true });
|
|
186
|
+
for (const c of unique) {
|
|
187
|
+
const entry = {
|
|
188
|
+
ts: new Date().toISOString(),
|
|
189
|
+
sessionId,
|
|
190
|
+
patternType: c.patternType,
|
|
191
|
+
candidatePhrase: c.phrase,
|
|
192
|
+
userPromptExcerpt: c.surroundingContext,
|
|
193
|
+
recentAssistantContext: recentAssistantContext.slice(0, 1500),
|
|
194
|
+
reviewStatus: 'pending',
|
|
195
|
+
proposedTriggerRegex: phraseToCandidateRegex(c.phrase),
|
|
196
|
+
proposedMemoryFile: null,
|
|
197
|
+
proposedTeaching: null,
|
|
198
|
+
};
|
|
199
|
+
appendFileSync(QUEUE, JSON.stringify(entry) + '\n');
|
|
200
|
+
}
|
|
201
|
+
audit('queued', `count=${unique.length} types=${[...new Set(unique.map((c) => c.patternType))].join(',')}`);
|
|
202
|
+
} catch (err) {
|
|
203
|
+
audit('skip-write-error', (err && err.message ? err.message : String(err)).slice(0, 200));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Hook is non-blocking — UserPromptSubmit accepts no decision blocker for
|
|
207
|
+
// pure side-effect hooks. Exit clean so the harness packet + preprompt
|
|
208
|
+
// consult chain continues uninterrupted.
|
|
209
|
+
process.exit(0);
|
|
210
|
+
|
|
211
|
+
// ── Helpers ──────────────────────────────────────────────────────────────
|
|
212
|
+
|
|
213
|
+
// Convert a candidate phrase into a regex skeleton for the trigger map.
|
|
214
|
+
// Replaces tokens with character classes that allow minor variation (verb
|
|
215
|
+
// endings, plurals, optional punctuation). Output is a SUGGESTION; human
|
|
216
|
+
// reviewer refines before adding to doctrine_trigger_map.json.
|
|
217
|
+
function phraseToCandidateRegex(phrase) {
|
|
218
|
+
// Lowercase + escape regex specials.
|
|
219
|
+
const lower = phrase.toLowerCase();
|
|
220
|
+
const escaped = lower.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
221
|
+
// Allow optional trailing 's' or 'ing' on the last word for verb forms.
|
|
222
|
+
const tokens = escaped.split(/\s+/);
|
|
223
|
+
if (tokens.length === 0) return escaped;
|
|
224
|
+
const last = tokens[tokens.length - 1];
|
|
225
|
+
// Don't bloat single-letter tokens.
|
|
226
|
+
if (last.length >= 4 && !last.endsWith('s') && !last.endsWith('ing')) {
|
|
227
|
+
tokens[tokens.length - 1] = `${last}(?:s|ing|ed)?`;
|
|
228
|
+
}
|
|
229
|
+
return tokens.join('\\s+');
|
|
230
|
+
}
|
|
@@ -190,6 +190,60 @@
|
|
|
190
190
|
"memory": "feedback_dont_override_user_stack_with_training.md",
|
|
191
191
|
"teaching": "Training data is stale. User's stack is authoritative.",
|
|
192
192
|
"counter_action": "Read codebase config / consult Aria. Substrate hierarchy: user statement > codebase > training. Always."
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
"trigger": "flagging (this|for now|for later)",
|
|
196
|
+
"memory": "feedback_no_flag_without_fix.md",
|
|
197
|
+
"teaching": "Flag without fix is convenience-seeking. Discoveries are atomic with their fixes.",
|
|
198
|
+
"counter_action": "Fix in the same turn OR create a TaskCreate with file path, line number, what's broken, why it matters."
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
"trigger": "(noting|note) this( and|, ?then)? moving on",
|
|
202
|
+
"memory": "feedback_no_flag_without_fix.md",
|
|
203
|
+
"teaching": "Noting-and-moving destroys partnership trust. The user has to track what you noticed vs. what you fixed.",
|
|
204
|
+
"counter_action": "Same-turn fix is default for small defects (<30 lines). Larger ones get tracked tasks before you proceed."
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
"trigger": "i'?ll come back|circle back|address(ing)? (this )?later|come back to (this|that|it)",
|
|
208
|
+
"memory": "feedback_no_flag_without_fix.md",
|
|
209
|
+
"teaching": "'I'll come back' is the lie that makes future-you treat the discovery as already-handled.",
|
|
210
|
+
"counter_action": "Decide now: fix in this turn, or create the task. No middle ground. The flag is not a substitute for action."
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
"trigger": "(found|noticed|discovered|spotted)[^.]{0,80}\\bbut\\b[^.]{0,80}(continue|continuing|moving on|won'?t|will not|skip|ignor)",
|
|
214
|
+
"memory": "feedback_no_flag_without_fix.md",
|
|
215
|
+
"teaching": "Discovery + continuation = abandonment. The 'but I'll keep going' clause hides the violation.",
|
|
216
|
+
"counter_action": "Stop. Fix the discovery in this turn or create a tracked task with the discovery as its description, then resume."
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
"trigger": "we should also (fix|address|handle|update|deal with)",
|
|
220
|
+
"memory": "feedback_no_flag_without_fix.md",
|
|
221
|
+
"teaching": "'We should also fix' tells the user about a problem you found while volunteering them to fix it.",
|
|
222
|
+
"counter_action": "If the fix is in your scope, fix it now. If it's truly out of scope, create the task — don't hand it back as a verbal note."
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
"trigger": "let me (note|flag)|(?<!please )leaving (this )?for follow.?up|out of scope here",
|
|
226
|
+
"memory": "feedback_no_flag_without_fix.md",
|
|
227
|
+
"teaching": "Verbal flags evaporate when the conversation moves on. Tasks persist.",
|
|
228
|
+
"counter_action": "Convert the flag into a TaskCreate or Linear save_issue immediately. No flag survives without a tracker ID."
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
"trigger": "TODO:?[^a-z0-9]|FIXME:?[^a-z0-9]|XXX:?[^a-z0-9]",
|
|
232
|
+
"memory": "feedback_no_flag_without_fix.md",
|
|
233
|
+
"teaching": "TODO comments in shipped code are flag-without-fix in source-form. Reviewers can't tell if it's tracked or forgotten.",
|
|
234
|
+
"counter_action": "Either implement now or reference a task ID inline (e.g., '// TODO(LINEAR-123): ...'). Bare TODOs fail the doctrine."
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
"trigger": "latent (bug|issue|defect|problem)|broken[^.]{0,40}continu",
|
|
238
|
+
"memory": "feedback_no_flag_without_fix.md",
|
|
239
|
+
"teaching": "Calling something 'latent' or 'broken' and then continuing is the explicit form of flag-and-move.",
|
|
240
|
+
"counter_action": "Latent defects in code you're touching get fixed in-flight. The 'I'm just here for X' framing is convenience-seeking."
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
"trigger": "(unrelated|separate concern|different (issue|topic|file|module))[^.]{0,60}(fix|address|handle)",
|
|
244
|
+
"memory": "feedback_no_flag_without_fix.md",
|
|
245
|
+
"teaching": "'Unrelated' is a frame the user gets to apply, not you. If you found it during your work, it's related to your work.",
|
|
246
|
+
"counter_action": "Surface the discovery, propose fix-now vs. task-it, let the user decide. Don't pre-decide that it's out of scope."
|
|
193
247
|
}
|
|
194
248
|
]
|
|
195
249
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aria_asi/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.12",
|
|
4
4
|
"description": "Aria Smart CLI — the world's first harness-powered terminal companion",
|
|
5
5
|
"bin": {
|
|
6
6
|
"aria": "./bin/aria.js"
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"url": "git+https://github.com/REI-Nationwide/cowork-sandbox.git"
|
|
18
18
|
},
|
|
19
19
|
"scripts": {
|
|
20
|
-
"build": "tsc",
|
|
20
|
+
"build": "tsc && node scripts/bundle-sdk.mjs",
|
|
21
21
|
"prepare": "npm run build",
|
|
22
22
|
"dev": "tsc --watch",
|
|
23
23
|
"publish:all": "bash scripts/publish-all.sh",
|
|
@@ -26,10 +26,31 @@ const HOOK_FILES = [
|
|
|
26
26
|
'aria-pre-tool-gate.mjs',
|
|
27
27
|
'aria-stop-gate.mjs',
|
|
28
28
|
'aria-preprompt-consult.mjs',
|
|
29
|
+
'aria-trigger-autolearn.mjs',
|
|
30
|
+
'doctrine_trigger_map.json',
|
|
29
31
|
];
|
|
32
|
+
// Compiled location: <pkg>/dist/aria-connector/src/connectors/claude-code.js
|
|
33
|
+
// (tsc preserves the src/ rooted layout under outDir). From this file:
|
|
34
|
+
// .. → src/
|
|
35
|
+
// .. → aria-connector/
|
|
36
|
+
// .. → dist/
|
|
37
|
+
// .. → <pkg>/
|
|
38
|
+
// Hooks ship at <pkg>/hooks/ → 4 ..s + 'hooks'.
|
|
39
|
+
// SDK bundled into <pkg>/dist/sdk/ at build time → 3 ..s + 'sdk'.
|
|
40
|
+
//
|
|
41
|
+
// Prior code used 3 ..s for hooks (landed at <pkg>/dist/hooks — missing)
|
|
42
|
+
// and 2 ..s for SDK (landed at <pkg>/dist/aria-connector/sdk — missing).
|
|
43
|
+
// Latent bug since the dist layout settled at this depth; caught by
|
|
44
|
+
// pre-publish smoke-test on 0.2.11. Per feedback_no_flag_without_fix.md
|
|
45
|
+
// the fix lands in the same turn as the discovery.
|
|
30
46
|
function packageHooksDir(): string {
|
|
31
47
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
32
|
-
return path.resolve(here, '..', '..', '..', 'hooks');
|
|
48
|
+
return path.resolve(here, '..', '..', '..', '..', 'hooks');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function packageSdkDir(): string {
|
|
52
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
53
|
+
return path.resolve(here, '..', '..', '..', 'sdk');
|
|
33
54
|
}
|
|
34
55
|
|
|
35
56
|
// Hook wiring for ~/.claude/settings.json. Mirrors what
|
|
@@ -65,6 +86,17 @@ const HOOKS_BLOCK = {
|
|
|
65
86
|
timeout: 12,
|
|
66
87
|
statusMessage: 'Pre-consulting Aria substrate for direction...',
|
|
67
88
|
},
|
|
89
|
+
{
|
|
90
|
+
// Aria trigger auto-learn — scans the user's prompt for correction
|
|
91
|
+
// patterns ("don't ___", "stop ___", ALL-CAPS frustration) and
|
|
92
|
+
// appends candidate trigger entries to ~/.claude/aria-trigger-queue.jsonl
|
|
93
|
+
// for human review. Phase 11 enforcement #47: harness teaches itself
|
|
94
|
+
// from corrections rather than staying frozen at hand-curated rules.
|
|
95
|
+
// Non-blocking — pure side effect, fast.
|
|
96
|
+
type: 'command',
|
|
97
|
+
command: 'node $HOME/.claude/hooks/aria-trigger-autolearn.mjs',
|
|
98
|
+
timeout: 3,
|
|
99
|
+
},
|
|
68
100
|
],
|
|
69
101
|
}],
|
|
70
102
|
PreToolUse: [{
|
|
@@ -123,11 +155,42 @@ function installHooks(claudeDir: string, logs: string[], opts: { force?: boolean
|
|
|
123
155
|
try { copyFileSync(dst, backup); logs.push(`Backed up existing ${name} → ${path.basename(backup)}`); } catch {}
|
|
124
156
|
}
|
|
125
157
|
copyFileSync(src, dst);
|
|
126
|
-
|
|
158
|
+
// Only the .mjs scripts need executable perms; data files (e.g. the
|
|
159
|
+
// doctrine_trigger_map.json shipped alongside hooks) get default 0o644.
|
|
160
|
+
if (name.endsWith('.mjs') || name.endsWith('.js')) {
|
|
161
|
+
try { chmodSync(dst, 0o755); } catch {}
|
|
162
|
+
}
|
|
127
163
|
logs.push(`Installed hook: ${name}`);
|
|
128
164
|
}
|
|
129
165
|
}
|
|
130
166
|
|
|
167
|
+
// Install bundled SDK to ~/.claude/aria-sdk/. Hooks dynamic-import from this
|
|
168
|
+
// absolute path, so every fetch (validateOutput, gardenTurn, getHarnessPacket,
|
|
169
|
+
// inject, checkAction) goes through the SDK control plane. Re-running connect
|
|
170
|
+
// idempotently overwrites with the latest bundled version.
|
|
171
|
+
function installSdk(claudeDir: string, logs: string[]): void {
|
|
172
|
+
const sdkSrc = packageSdkDir();
|
|
173
|
+
if (!existsSync(sdkSrc)) {
|
|
174
|
+
logs.push(`⚠ SDK bundle missing: ${sdkSrc} (run npm run build)`);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const sdkDst = path.join(claudeDir, 'aria-sdk');
|
|
178
|
+
if (!existsSync(sdkDst)) {
|
|
179
|
+
mkdirSync(sdkDst, { recursive: true, mode: 0o700 });
|
|
180
|
+
}
|
|
181
|
+
let copied = 0;
|
|
182
|
+
const fs = require('fs') as typeof import('fs');
|
|
183
|
+
for (const name of fs.readdirSync(sdkSrc)) {
|
|
184
|
+
const src = path.join(sdkSrc, name);
|
|
185
|
+
const stat = fs.statSync(src);
|
|
186
|
+
if (!stat.isFile()) continue;
|
|
187
|
+
const dst = path.join(sdkDst, name);
|
|
188
|
+
copyFileSync(src, dst);
|
|
189
|
+
copied++;
|
|
190
|
+
}
|
|
191
|
+
logs.push(`Installed Aria SDK (${copied} files) → ${sdkDst}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
131
194
|
function isSamePath(a: string, b: string): boolean {
|
|
132
195
|
try { return path.resolve(a) === path.resolve(b); } catch { return false; }
|
|
133
196
|
}
|
|
@@ -254,6 +317,7 @@ export async function connectClaudeCode(
|
|
|
254
317
|
// what makes the connector a real runtime control plane rather than
|
|
255
318
|
// just a system-prompt addendum.
|
|
256
319
|
installHooks(claudeDir, logs, { force: opts.force });
|
|
320
|
+
installSdk(claudeDir, logs);
|
|
257
321
|
wireHooksBlock(settings, logs);
|
|
258
322
|
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', { mode: 0o600 });
|
|
259
323
|
|