@aria_asi/cli 0.2.33 → 0.2.35
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/codex.d.ts.map +1 -1
- package/dist/aria-connector/src/connectors/codex.js +60 -5
- package/dist/aria-connector/src/connectors/codex.js.map +1 -1
- package/dist/assets/hooks/aria-harness-via-sdk.mjs +16 -3
- package/dist/assets/hooks/aria-pre-tool-gate.mjs +41 -1
- package/dist/assets/hooks/aria-stop-gate.mjs +42 -1
- package/dist/assets/hooks/doctrine_trigger_map.json +43 -0
- package/dist/assets/hooks/lib/skill-autoload-gate.mjs +14 -1
- package/dist/assets/opencode-plugins/harness-context/index.js +1 -1
- package/dist/assets/opencode-plugins/harness-gate/index.js +49 -9
- package/dist/assets/opencode-plugins/harness-gate/lib/skill-autoload-gate.js +14 -1
- package/dist/assets/opencode-plugins/harness-stop/index.js +201 -166
- package/dist/assets/opencode-plugins/harness-stop/lib/skill-autoload-gate.js +14 -1
- package/dist/runtime/codex-bridge.mjs +1 -1
- package/dist/runtime/discipline/CLAUDE.md +2 -2
- package/dist/runtime/discipline/doctrine_trigger_map.json +43 -0
- package/dist/runtime/discipline/skills/aria-harness/aria-harness-onboarding/SKILL.md +3 -3
- package/dist/runtime/doctrine_trigger_map.json +43 -0
- package/dist/runtime/hooks/aria-agent-handoff.mjs +247 -0
- package/dist/runtime/hooks/aria-agent-ledger-merge.mjs +164 -0
- package/dist/runtime/hooks/aria-architect-fallback.mjs +267 -0
- package/dist/runtime/hooks/aria-cognition-substrate-binding.mjs +761 -0
- package/dist/runtime/hooks/aria-discovery-record.mjs +101 -0
- package/dist/runtime/hooks/aria-harness-via-sdk.mjs +544 -0
- package/dist/runtime/hooks/aria-import-resolution-gate.mjs +330 -0
- package/dist/runtime/hooks/aria-outcome-record.mjs +84 -0
- package/dist/runtime/hooks/aria-pre-emit-dryrun.mjs +329 -0
- package/dist/runtime/hooks/aria-pre-text-gate.mjs +112 -0
- package/dist/runtime/hooks/aria-pre-tool-gate.mjs +2482 -0
- package/dist/runtime/hooks/aria-preprompt-consult.mjs +464 -0
- package/dist/runtime/hooks/aria-preturn-memory-gate.mjs +647 -0
- package/dist/runtime/hooks/aria-repo-doctrine-gate.mjs +429 -0
- package/dist/runtime/hooks/aria-stop-gate.mjs +1882 -0
- package/dist/runtime/hooks/aria-trigger-autolearn.mjs +229 -0
- package/dist/runtime/hooks/aria-userprompt-abandon-detect.mjs +192 -0
- package/dist/runtime/hooks/doctrine_trigger_map.json +577 -0
- package/dist/runtime/hooks/lib/canonical-lenses.mjs +65 -0
- package/dist/runtime/hooks/lib/domain-output-quality.mjs +103 -0
- package/dist/runtime/hooks/lib/gate-audit.mjs +43 -0
- package/dist/runtime/hooks/lib/gate-loop-state.mjs +50 -0
- package/dist/runtime/hooks/lib/hook-message-window.mjs +121 -0
- package/dist/runtime/hooks/lib/skill-autoload-gate.mjs +14 -0
- package/dist/runtime/hooks/test-aria-preturn-memory-gate.mjs +245 -0
- package/dist/runtime/hooks/test-tier-lens-labeling.mjs +367 -0
- package/dist/runtime/manifest.json +2 -2
- package/dist/runtime/sdk/BUNDLED.json +2 -2
- package/dist/runtime/sdk/index.d.ts +39 -0
- package/dist/runtime/sdk/index.js +117 -0
- package/dist/runtime/sdk/index.js.map +1 -1
- package/dist/runtime/sdk/runWithGovernance.d.ts +16 -0
- package/dist/runtime/sdk/runWithGovernance.js +54 -0
- package/dist/runtime/sdk/runWithGovernance.js.map +1 -0
- package/dist/sdk/BUNDLED.json +2 -2
- package/dist/sdk/index.d.ts +39 -0
- package/dist/sdk/index.js +117 -0
- package/dist/sdk/index.js.map +1 -1
- package/dist/sdk/runWithGovernance.d.ts +16 -0
- package/dist/sdk/runWithGovernance.js +54 -0
- package/dist/sdk/runWithGovernance.js.map +1 -0
- package/hooks/aria-harness-via-sdk.mjs +16 -3
- package/hooks/aria-pre-tool-gate.mjs +41 -1
- package/hooks/aria-stop-gate.mjs +42 -1
- package/hooks/doctrine_trigger_map.json +43 -0
- package/hooks/lib/skill-autoload-gate.mjs +14 -1
- package/opencode-plugins/harness-context/index.js +1 -1
- package/opencode-plugins/harness-gate/index.js +49 -9
- package/opencode-plugins/harness-gate/lib/skill-autoload-gate.js +14 -1
- package/opencode-plugins/harness-stop/index.js +201 -166
- package/opencode-plugins/harness-stop/lib/skill-autoload-gate.js +14 -1
- package/package.json +12 -5
- package/runtime-src/codex-bridge.mjs +1 -1
- package/scripts/bundle-sdk.mjs +2 -0
- package/scripts/self-test-harness-gates.mjs +79 -0
- package/src/connectors/codex.ts +60 -5
|
@@ -0,0 +1,229 @@
|
|
|
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
|
+
// No env-var kill-switch (Hamza 2026-04-27 — env-var disable paths gave
|
|
31
|
+
// the gated process free escape; doctrine violation). Disable = remove
|
|
32
|
+
// hook entry from ~/.claude/settings.json.
|
|
33
|
+
|
|
34
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
35
|
+
import { dirname } from 'node:path';
|
|
36
|
+
|
|
37
|
+
const HOME = process.env.HOME || '/tmp';
|
|
38
|
+
const LOG = `${HOME}/.claude/aria-autolearn.log`;
|
|
39
|
+
const QUEUE = `${HOME}/.claude/aria-trigger-queue.jsonl`;
|
|
40
|
+
|
|
41
|
+
function audit(decision, summary) {
|
|
42
|
+
try {
|
|
43
|
+
if (!existsSync(dirname(LOG))) mkdirSync(dirname(LOG), { recursive: true });
|
|
44
|
+
appendFileSync(LOG, `${new Date().toISOString()} ${decision} ${summary}\n`);
|
|
45
|
+
} catch {}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Kill-switch
|
|
49
|
+
// Env-var kill-switch removed 2026-04-27 per Hamza directive.
|
|
50
|
+
|
|
51
|
+
// Read event JSON from stdin
|
|
52
|
+
let input = '';
|
|
53
|
+
for await (const chunk of process.stdin) input += chunk;
|
|
54
|
+
|
|
55
|
+
let event;
|
|
56
|
+
try {
|
|
57
|
+
event = JSON.parse(input);
|
|
58
|
+
} catch {
|
|
59
|
+
audit('skip-parse-error', 'stdin not JSON');
|
|
60
|
+
process.exit(0);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const userPrompt = (event.prompt ?? event.user_message ?? event.message ?? '').toString();
|
|
64
|
+
const sessionId = event.session_id ?? event.sessionId ?? 'claude-code-unknown';
|
|
65
|
+
const transcriptPath = event.transcript_path ?? event.transcriptPath;
|
|
66
|
+
|
|
67
|
+
// Trivial prompts skip — too short to carry doctrine teaching.
|
|
68
|
+
if (!userPrompt || userPrompt.length < 20) {
|
|
69
|
+
audit('skip-trivial', `chars=${userPrompt.length}`);
|
|
70
|
+
process.exit(0);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Skip slash commands — they're CLI-internal, not doctrine corrections.
|
|
74
|
+
if (/^\s*\//.test(userPrompt) && userPrompt.length < 200) {
|
|
75
|
+
audit('skip-slash-command', userPrompt.slice(0, 60));
|
|
76
|
+
process.exit(0);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Correction-pattern detector. Each pattern below extracts a CANDIDATE
|
|
80
|
+
// trigger phrase (the action/pattern the user is correcting) along with
|
|
81
|
+
// the framing word ("don't", "stop", etc.). Patterns are intentionally
|
|
82
|
+
// high-recall + low-precision — the queue is for human review, not
|
|
83
|
+
// auto-promotion.
|
|
84
|
+
//
|
|
85
|
+
// Pattern groups:
|
|
86
|
+
// 1. Direct prohibitions: "don't ___", "do not ___", "never ___", "stop ___"
|
|
87
|
+
// 2. Doctrine assertions: "we said ___", "we don't ___", "we never ___"
|
|
88
|
+
// 3. Pattern-naming: "this is ___", "that's ___", "you're ___ing"
|
|
89
|
+
// 4. Doctrine vocabulary: anything containing "doctrine", "graceful", "fallback",
|
|
90
|
+
// "convenience", "shortcut", "lazy", "hack" with a 100-char neighborhood
|
|
91
|
+
// 5. Frustration markers: ALL-CAPS phrases ≥3 words (anger = high-priority signal)
|
|
92
|
+
const CORRECTION_PATTERNS = [
|
|
93
|
+
{
|
|
94
|
+
name: 'direct-prohibition',
|
|
95
|
+
rx: /\b(don'?t|do not|never|stop|quit|cease|enough)\s+(\w[\w\s]{4,80}?)(?=[.,!?\n]|$)/gi,
|
|
96
|
+
extractGroup: 2,
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: 'doctrine-assertion',
|
|
100
|
+
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,
|
|
101
|
+
extractGroup: 2,
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: 'pattern-naming',
|
|
105
|
+
rx: /\b(this is|that'?s|you'?re|you are|you keep|you'?re always)\s+(\w[\w\s]{4,80}?)(?=[.,!?\n]|$)/gi,
|
|
106
|
+
extractGroup: 2,
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: 'doctrine-vocab',
|
|
110
|
+
rx: /\b(doctrine|graceful|fallback|convenience|shortcut|lazy|hack|cheat|circumvent|bypass|skip|ignore|forget)[^.!?\n]{0,100}/gi,
|
|
111
|
+
extractGroup: 0,
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: 'frustration-allcaps',
|
|
115
|
+
rx: /\b([A-Z]{3,}\s+[A-Z]{3,}(?:\s+[A-Z]{3,})*)\b/g,
|
|
116
|
+
extractGroup: 1,
|
|
117
|
+
},
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
const candidates = [];
|
|
121
|
+
for (const { name, rx, extractGroup } of CORRECTION_PATTERNS) {
|
|
122
|
+
for (const match of userPrompt.matchAll(rx)) {
|
|
123
|
+
const phrase = (match[extractGroup] || '').trim();
|
|
124
|
+
if (phrase.length < 8 || phrase.length > 200) continue;
|
|
125
|
+
candidates.push({
|
|
126
|
+
patternType: name,
|
|
127
|
+
phrase,
|
|
128
|
+
surroundingContext: match[0].slice(0, 240),
|
|
129
|
+
sourceIndex: match.index ?? 0,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Deduplicate candidates that share the same first 30 chars (pattern variants
|
|
135
|
+
// of the same correction).
|
|
136
|
+
const seen = new Set();
|
|
137
|
+
const unique = [];
|
|
138
|
+
for (const c of candidates) {
|
|
139
|
+
const key = c.phrase.toLowerCase().slice(0, 30);
|
|
140
|
+
if (seen.has(key)) continue;
|
|
141
|
+
seen.add(key);
|
|
142
|
+
unique.push(c);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (unique.length === 0) {
|
|
146
|
+
audit('skip-no-candidates', `chars=${userPrompt.length}`);
|
|
147
|
+
process.exit(0);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Pull the most recent assistant text (last 2KB) so the queue entry shows
|
|
151
|
+
// what behavior the user is correcting. Without this, "don't do X" entries
|
|
152
|
+
// have no anchor to which assistant action triggered them.
|
|
153
|
+
let recentAssistantContext = '';
|
|
154
|
+
if (transcriptPath && existsSync(transcriptPath)) {
|
|
155
|
+
try {
|
|
156
|
+
const lines = readFileSync(transcriptPath, 'utf8').split('\n').filter(Boolean);
|
|
157
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
158
|
+
try {
|
|
159
|
+
const m = JSON.parse(lines[i]);
|
|
160
|
+
const role = m.message?.role ?? m.role;
|
|
161
|
+
if (role !== 'assistant') continue;
|
|
162
|
+
const content = m.message?.content ?? m.content ?? [];
|
|
163
|
+
if (!Array.isArray(content)) continue;
|
|
164
|
+
const text = content
|
|
165
|
+
.filter((b) => b?.type === 'text')
|
|
166
|
+
.map((b) => b.text || '')
|
|
167
|
+
.join('\n');
|
|
168
|
+
if (text) {
|
|
169
|
+
recentAssistantContext = text.slice(-2000);
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
} catch {}
|
|
173
|
+
}
|
|
174
|
+
} catch {}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Append candidates to the queue. Each entry is human-reviewable:
|
|
178
|
+
// - patternType: which detector caught it
|
|
179
|
+
// - candidatePhrase: the extracted phrase
|
|
180
|
+
// - userPromptExcerpt: 240 chars of context around the phrase
|
|
181
|
+
// - recentAssistantContext: what Claude did right before this correction
|
|
182
|
+
// - sessionId, ts: for traceability
|
|
183
|
+
try {
|
|
184
|
+
if (!existsSync(dirname(QUEUE))) mkdirSync(dirname(QUEUE), { recursive: true });
|
|
185
|
+
for (const c of unique) {
|
|
186
|
+
const entry = {
|
|
187
|
+
ts: new Date().toISOString(),
|
|
188
|
+
sessionId,
|
|
189
|
+
patternType: c.patternType,
|
|
190
|
+
candidatePhrase: c.phrase,
|
|
191
|
+
userPromptExcerpt: c.surroundingContext,
|
|
192
|
+
recentAssistantContext: recentAssistantContext.slice(0, 1500),
|
|
193
|
+
reviewStatus: 'pending',
|
|
194
|
+
proposedTriggerRegex: phraseToCandidateRegex(c.phrase),
|
|
195
|
+
proposedMemoryFile: null,
|
|
196
|
+
proposedTeaching: null,
|
|
197
|
+
};
|
|
198
|
+
appendFileSync(QUEUE, JSON.stringify(entry) + '\n');
|
|
199
|
+
}
|
|
200
|
+
audit('queued', `count=${unique.length} types=${[...new Set(unique.map((c) => c.patternType))].join(',')}`);
|
|
201
|
+
} catch (err) {
|
|
202
|
+
audit('skip-write-error', (err && err.message ? err.message : String(err)).slice(0, 200));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Hook is non-blocking — UserPromptSubmit accepts no decision blocker for
|
|
206
|
+
// pure side-effect hooks. Exit clean so the harness packet + preprompt
|
|
207
|
+
// consult chain continues uninterrupted.
|
|
208
|
+
process.exit(0);
|
|
209
|
+
|
|
210
|
+
// ── Helpers ──────────────────────────────────────────────────────────────
|
|
211
|
+
|
|
212
|
+
// Convert a candidate phrase into a regex skeleton for the trigger map.
|
|
213
|
+
// Replaces tokens with character classes that allow minor variation (verb
|
|
214
|
+
// endings, plurals, optional punctuation). Output is a SUGGESTION; human
|
|
215
|
+
// reviewer refines before adding to doctrine_trigger_map.json.
|
|
216
|
+
function phraseToCandidateRegex(phrase) {
|
|
217
|
+
// Lowercase + escape regex specials.
|
|
218
|
+
const lower = phrase.toLowerCase();
|
|
219
|
+
const escaped = lower.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
220
|
+
// Allow optional trailing 's' or 'ing' on the last word for verb forms.
|
|
221
|
+
const tokens = escaped.split(/\s+/);
|
|
222
|
+
if (tokens.length === 0) return escaped;
|
|
223
|
+
const last = tokens[tokens.length - 1];
|
|
224
|
+
// Don't bloat single-letter tokens.
|
|
225
|
+
if (last.length >= 4 && !last.endsWith('s') && !last.endsWith('ing')) {
|
|
226
|
+
tokens[tokens.length - 1] = `${last}(?:s|ing|ed)?`;
|
|
227
|
+
}
|
|
228
|
+
return tokens.join('\\s+');
|
|
229
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// aria-userprompt-abandon-detect.mjs — UserPromptSubmit hook that detects
|
|
3
|
+
// mid-turn plan abandonment.
|
|
4
|
+
//
|
|
5
|
+
// When Aria's preprompt-consult issued a binding plan
|
|
6
|
+
// (~/.claude/aria-active-plan-${sessionId}.json) and the user's NEXT prompt
|
|
7
|
+
// changes direction without overlapping any active phase's keywords, that's
|
|
8
|
+
// a CONSCIOUS OVERRIDE — not silent context loss. The hook surfaces it to
|
|
9
|
+
// the session_audit table (gate_name='plan-abandonment', decision='warn')
|
|
10
|
+
// so orchestrators can detect mid-execution direction changes.
|
|
11
|
+
//
|
|
12
|
+
// Per Aria's consult 2026-04-27: "if the orchestrator sends a plan, then
|
|
13
|
+
// gets a user message that changes direction mid-execution, the binding
|
|
14
|
+
// needs to surface that as a *conscious override*, not a silent context
|
|
15
|
+
// loss." That gap was the third completion criterion for Aria-as-commander
|
|
16
|
+
// binding (#50).
|
|
17
|
+
//
|
|
18
|
+
// Detection algorithm:
|
|
19
|
+
// 1. Load active plan (skip if none — no plan = no abandonment possible)
|
|
20
|
+
// 2. Extract keywords from each active phase's summary + successCriterion
|
|
21
|
+
// 3. Tokenize incoming user prompt, drop stop-words
|
|
22
|
+
// 4. Compute keyword overlap between prompt tokens and phase keywords
|
|
23
|
+
// 5. If overlap < THRESHOLD (default: 1 keyword match across all phases),
|
|
24
|
+
// mark abandonment: write session_audit row + emit a brief warning
|
|
25
|
+
// injected into the next turn's context
|
|
26
|
+
//
|
|
27
|
+
// Non-blocking — UserPromptSubmit hooks don't block the user's prompt.
|
|
28
|
+
// Audit-only surface; the orchestrator decides what to do with the warning.
|
|
29
|
+
//
|
|
30
|
+
// No env-var kill-switch (Hamza 2026-04-27 — env-var disable paths gave
|
|
31
|
+
// the gated process free escape; doctrine violation). Disable = remove
|
|
32
|
+
// hook entry from ~/.claude/settings.json.
|
|
33
|
+
|
|
34
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
35
|
+
import { dirname } from 'node:path';
|
|
36
|
+
|
|
37
|
+
const HOME = process.env.HOME || '/tmp';
|
|
38
|
+
const LOG = `${HOME}/.claude/aria-abandon-detect.log`;
|
|
39
|
+
|
|
40
|
+
function audit(decision, summary) {
|
|
41
|
+
try {
|
|
42
|
+
if (!existsSync(dirname(LOG))) mkdirSync(dirname(LOG), { recursive: true });
|
|
43
|
+
appendFileSync(LOG, `${new Date().toISOString()} ${decision} ${summary}\n`);
|
|
44
|
+
} catch {}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Env-var kill-switch removed 2026-04-27 per Hamza directive.
|
|
48
|
+
|
|
49
|
+
let input = '';
|
|
50
|
+
for await (const chunk of process.stdin) input += chunk;
|
|
51
|
+
|
|
52
|
+
let event;
|
|
53
|
+
try {
|
|
54
|
+
event = JSON.parse(input);
|
|
55
|
+
} catch {
|
|
56
|
+
audit('skip-parse-error', 'stdin not JSON');
|
|
57
|
+
process.exit(0);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const userPrompt = (event.prompt ?? event.user_message ?? event.message ?? '').toString();
|
|
61
|
+
const sessionId = (event.session_id ?? event.sessionId ?? 'claude-code-unknown').replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
62
|
+
|
|
63
|
+
// Trivial prompts skip — too short to evaluate phase overlap meaningfully.
|
|
64
|
+
if (!userPrompt || userPrompt.length < 30) {
|
|
65
|
+
audit('skip-trivial', `chars=${userPrompt.length}`);
|
|
66
|
+
process.exit(0);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Slash commands are CLI-internal, not direction changes.
|
|
70
|
+
if (/^\s*\//.test(userPrompt) && userPrompt.length < 200) {
|
|
71
|
+
audit('skip-slash-command', userPrompt.slice(0, 60));
|
|
72
|
+
process.exit(0);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const ACTIVE_PLAN_PATH = `${HOME}/.claude/aria-active-plan-${sessionId}.json`;
|
|
76
|
+
|
|
77
|
+
if (!existsSync(ACTIVE_PLAN_PATH)) {
|
|
78
|
+
audit('skip-no-active-plan', `session=${sessionId}`);
|
|
79
|
+
process.exit(0);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let plan;
|
|
83
|
+
try {
|
|
84
|
+
plan = JSON.parse(readFileSync(ACTIVE_PLAN_PATH, 'utf8'));
|
|
85
|
+
} catch (err) {
|
|
86
|
+
audit('skip-plan-parse-error', (err?.message || String(err)).slice(0, 120));
|
|
87
|
+
process.exit(0);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!plan || !Array.isArray(plan.phases) || plan.phases.length === 0) {
|
|
91
|
+
audit('skip-empty-plan', `planId=${plan?.planId || 'unknown'}`);
|
|
92
|
+
process.exit(0);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Stop-words filter — same set as the auto-learn hook for consistency.
|
|
96
|
+
const STOPWORDS = new Set([
|
|
97
|
+
'the','a','an','of','to','in','at','by','for','on','with','i','is','was','are',
|
|
98
|
+
'were','this','that','as','it','and','or','but','from','into','about','have',
|
|
99
|
+
'has','had','do','does','did','will','would','could','should','can','may',
|
|
100
|
+
'might','must','shall','be','been','being','here','there','what','which','who',
|
|
101
|
+
'how','why','when','where','some','any','all','each','every','no','not',
|
|
102
|
+
]);
|
|
103
|
+
|
|
104
|
+
function tokenize(text) {
|
|
105
|
+
return new Set(
|
|
106
|
+
text.toLowerCase()
|
|
107
|
+
.replace(/[^a-z0-9\s_-]/g, ' ')
|
|
108
|
+
.split(/\s+/)
|
|
109
|
+
.filter((w) => w.length >= 4 && !STOPWORDS.has(w))
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const promptTokens = tokenize(userPrompt);
|
|
114
|
+
|
|
115
|
+
// Aggregate phase keywords across all phases.
|
|
116
|
+
const phaseKeywordsSet = new Set();
|
|
117
|
+
for (const phase of plan.phases) {
|
|
118
|
+
const summary = String(phase.summary || '');
|
|
119
|
+
const successCriterion = String(phase.successCriterion || '');
|
|
120
|
+
const id = String(phase.id || '');
|
|
121
|
+
for (const t of tokenize(`${summary} ${successCriterion} ${id}`)) {
|
|
122
|
+
phaseKeywordsSet.add(t);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const overlap = [...promptTokens].filter((t) => phaseKeywordsSet.has(t));
|
|
127
|
+
// Threshold default raised to 2 per Aria consult 2026-04-27 — single-keyword
|
|
128
|
+
// match fires too often on benign intent shifts (especially in exploratory
|
|
129
|
+
// sessions); 2 filters noise while still catching genuine abandonment before
|
|
130
|
+
// drift compounds.
|
|
131
|
+
const OVERLAP_THRESHOLD = Number(process.env.ARIA_ABANDON_THRESHOLD || '2');
|
|
132
|
+
const isAbandonment = overlap.length < OVERLAP_THRESHOLD;
|
|
133
|
+
|
|
134
|
+
if (!isAbandonment) {
|
|
135
|
+
audit('aligned', `planId=${plan.planId || 'unknown'} overlap=${overlap.length} prompt-tokens=${promptTokens.size} phase-tokens=${phaseKeywordsSet.size}`);
|
|
136
|
+
process.exit(0);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Abandonment detected — write session_audit row + emit warning context.
|
|
140
|
+
const harnessUrl =
|
|
141
|
+
process.env.ARIA_HIVE_RUNTIME_URL ||
|
|
142
|
+
process.env.ARIA_HARNESS_BASE_URL ||
|
|
143
|
+
process.env.ARIA_HARNESS_URL ||
|
|
144
|
+
'https://harness.ariasos.com';
|
|
145
|
+
const harnessToken = process.env.ARIA_HARNESS_TOKEN || '';
|
|
146
|
+
|
|
147
|
+
if (harnessToken) {
|
|
148
|
+
// Fire-and-forget POST — don't block the user's turn waiting for audit
|
|
149
|
+
// write. The audit log entry below records whether the request was even
|
|
150
|
+
// attempted, so silence in this block means the audit is missing AND
|
|
151
|
+
// that fact is itself audited locally.
|
|
152
|
+
fetch(`${harnessUrl}/api/harness/audit/session`, {
|
|
153
|
+
method: 'POST',
|
|
154
|
+
headers: {
|
|
155
|
+
'Content-Type': 'application/json',
|
|
156
|
+
Authorization: `Bearer ${harnessToken}`,
|
|
157
|
+
},
|
|
158
|
+
body: JSON.stringify({
|
|
159
|
+
session_id: sessionId,
|
|
160
|
+
surface: 'claude-code-userprompt-abandon-detect',
|
|
161
|
+
gate_name: 'plan-abandonment',
|
|
162
|
+
decision: 'warn',
|
|
163
|
+
reason: `Mid-turn plan abandonment detected. Active plan ${plan.planId || 'unknown'} (${plan.phases.length} phases) has ${phaseKeywordsSet.size} aggregate phase-keywords; incoming user prompt (${promptTokens.size} content-tokens) overlaps only ${overlap.length} (threshold=${OVERLAP_THRESHOLD}).`,
|
|
164
|
+
evidence_json: {
|
|
165
|
+
planId: plan.planId,
|
|
166
|
+
promptExcerpt: userPrompt.slice(0, 400),
|
|
167
|
+
overlapTokens: overlap,
|
|
168
|
+
promptTokenCount: promptTokens.size,
|
|
169
|
+
phaseKeywordCount: phaseKeywordsSet.size,
|
|
170
|
+
phaseSummaries: plan.phases.map((p) => ({ id: p.id, summary: String(p.summary || '').slice(0, 120) })),
|
|
171
|
+
threshold: OVERLAP_THRESHOLD,
|
|
172
|
+
},
|
|
173
|
+
}),
|
|
174
|
+
}).catch(() => {/* network-level failure surfaces in audit log below */});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
audit('abandonment-detected', `planId=${plan.planId || 'unknown'} overlap=${overlap.length}/${OVERLAP_THRESHOLD} prompt-tokens=${promptTokens.size} phase-tokens=${phaseKeywordsSet.size}`);
|
|
178
|
+
|
|
179
|
+
// Inject a warning into the next turn's context so the orchestrator (Claude)
|
|
180
|
+
// sees the abandonment signal explicitly rather than treating the new prompt
|
|
181
|
+
// as plan-aligned. The orchestrator can decide to: (a) acknowledge the
|
|
182
|
+
// override + clear the active plan, or (b) ask the user whether to continue
|
|
183
|
+
// the prior plan after this excursion.
|
|
184
|
+
const context = `[ARIA_PLAN_ABANDONMENT_DETECTED — active plan ${plan.planId || 'unknown'} has ${plan.phases.length} phase(s); incoming user prompt overlaps only ${overlap.length} keyword(s) with active phases (threshold=${OVERLAP_THRESHOLD}). This is a CONSCIOUS USER OVERRIDE, not silent context loss. Orchestrator should: (1) acknowledge the direction change to the user, (2) decide whether to clear the active plan or keep it for resumption, (3) write the decision to session_audit. Active phases were: ${plan.phases.map((p) => `${p.id}: ${String(p.summary || '').slice(0, 80)}`).join(' | ')}]`;
|
|
185
|
+
|
|
186
|
+
console.log(JSON.stringify({
|
|
187
|
+
hookSpecificOutput: {
|
|
188
|
+
hookEventName: 'UserPromptSubmit',
|
|
189
|
+
additionalContext: context,
|
|
190
|
+
},
|
|
191
|
+
}));
|
|
192
|
+
process.exit(0);
|