@aria_asi/cli 0.2.9 → 0.2.10
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/hooks/aria-stop-gate.mjs +106 -4
- package/package.json +1 -1
package/hooks/aria-stop-gate.mjs
CHANGED
|
@@ -206,10 +206,112 @@ const hasSubstrateEvidence = SUBSTRATE_EVIDENCE_RX.test(assistantText);
|
|
|
206
206
|
const questionWithoutEvidence = hasQuestionToUser && !hasSubstrateEvidence;
|
|
207
207
|
|
|
208
208
|
if (cog.count >= REQUIRED_LENSES) {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
209
|
+
// ── Output-quality enforcement (Hamza 2026-04-27 — clients need the same
|
|
210
|
+
// Mizan/drift/code-quality gates that aria-soul applies server-side) ──
|
|
211
|
+
//
|
|
212
|
+
// Cognition gate passed. Now run THREE additional checks BEFORE allow:
|
|
213
|
+
// 1. SDK validateOutput via /api/harness/validate (Mizan classifier on draft)
|
|
214
|
+
// 2. Drift_guard pattern scan against doctrine_trigger_map.json (convenience-
|
|
215
|
+
// seeking phrases, graceful-degradation patterns, etc.)
|
|
216
|
+
// 3. Code-quality check on code blocks in output (no TODO stubs, no
|
|
217
|
+
// graceful-degradation try/catch, no // @ts-expect-error suppressions)
|
|
218
|
+
//
|
|
219
|
+
// Any check returning severity=block → Stop-gate blocks emit + Claude re-drafts
|
|
220
|
+
// with violations surfaced. Rewritten suggestion (from validateOutput) is
|
|
221
|
+
// included in the block reason so re-draft has concrete guidance.
|
|
222
|
+
//
|
|
223
|
+
// Trivially short outputs (<200 chars after system-reminder strip) skip
|
|
224
|
+
// these output-quality checks since they're typically yes/no acks where
|
|
225
|
+
// pattern-match would false-positive.
|
|
226
|
+
const OUTPUT_QC_MIN_CHARS = 200;
|
|
227
|
+
const OUTPUT_QC_ENABLED = (process.env.ARIA_OUTPUT_QC_ENABLED || 'true').toLowerCase() !== 'false';
|
|
228
|
+
|
|
229
|
+
if (OUTPUT_QC_ENABLED && assistantText.length >= OUTPUT_QC_MIN_CHARS) {
|
|
230
|
+
// 1. Drift_guard pattern scan — fast, local, deterministic
|
|
231
|
+
const TRIGGER_MAP_PATH = `${HOME}/.claude/projects/-home-hamzaibrahim1/memory/doctrine_trigger_map.json`;
|
|
232
|
+
let driftHits = [];
|
|
233
|
+
try {
|
|
234
|
+
if (existsSync(TRIGGER_MAP_PATH)) {
|
|
235
|
+
const triggerMap = JSON.parse(readFileSync(TRIGGER_MAP_PATH, 'utf8'));
|
|
236
|
+
const lowerText = assistantText.toLowerCase();
|
|
237
|
+
for (const t of triggerMap.triggers || []) {
|
|
238
|
+
try {
|
|
239
|
+
const rx = new RegExp(t.trigger, 'i');
|
|
240
|
+
if (rx.test(lowerText)) {
|
|
241
|
+
// Trigger present — check if the counter-doctrine memory is also
|
|
242
|
+
// cited in the response (justification). If not, count as drift.
|
|
243
|
+
const memoryName = (t.memory || '').replace(/\.md$/, '');
|
|
244
|
+
const memoryCited = memoryName && lowerText.includes(memoryName.toLowerCase());
|
|
245
|
+
if (!memoryCited) {
|
|
246
|
+
driftHits.push({ trigger: t.trigger, memory: t.memory, teaching: t.teaching });
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
} catch {/* malformed regex in trigger entry — skip */}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
} catch {/* trigger map unreadable — degrade to mizan-only check */}
|
|
253
|
+
|
|
254
|
+
// 2. SDK validateOutput via /api/harness/validate (best-effort POST)
|
|
255
|
+
let mizanVerdict = null;
|
|
256
|
+
try {
|
|
257
|
+
const harnessUrl = process.env.ARIA_HARNESS_URL || 'https://harness.ariasos.com';
|
|
258
|
+
const harnessToken = process.env.ARIA_HARNESS_TOKEN || '';
|
|
259
|
+
const validateResp = await fetch(`${harnessUrl}/api/harness/validate`, {
|
|
260
|
+
method: 'POST',
|
|
261
|
+
headers: {
|
|
262
|
+
'Content-Type': 'application/json',
|
|
263
|
+
...(harnessToken ? { Authorization: `Bearer ${harnessToken}` } : {}),
|
|
264
|
+
},
|
|
265
|
+
body: JSON.stringify({
|
|
266
|
+
text: assistantText.slice(0, 8000),
|
|
267
|
+
sessionId: event.session_id || 'claude-code',
|
|
268
|
+
surface: 'claude-code-stop-gate',
|
|
269
|
+
}),
|
|
270
|
+
});
|
|
271
|
+
if (validateResp.ok) {
|
|
272
|
+
mizanVerdict = await validateResp.json();
|
|
273
|
+
}
|
|
274
|
+
} catch {/* network or endpoint unavailable — degrade to drift-only */}
|
|
275
|
+
|
|
276
|
+
// 3. Code-quality scan on code blocks
|
|
277
|
+
const codeBlocks = [...assistantText.matchAll(/```[a-z]*\n([\s\S]*?)```/gi)].map((m) => m[1]);
|
|
278
|
+
const codeQualityHits = [];
|
|
279
|
+
for (const block of codeBlocks) {
|
|
280
|
+
if (/\/\/\s*TODO|\/\/\s*FIXME|\/\/\s*XXX/.test(block)) codeQualityHits.push('TODO/FIXME/XXX in shipped code');
|
|
281
|
+
if (/@ts-expect-error|@ts-ignore/.test(block)) codeQualityHits.push('ts-expect-error / ts-ignore — type suppression instead of fix');
|
|
282
|
+
if (/catch\s*\([^)]*\)\s*\{\s*(?:return\s+(?:''|""|null|undefined|\[\]|\{\})|\}\s*$|\/\/[^\n]*$)/m.test(block)) codeQualityHits.push('catch block with empty/silent fallthrough — graceful degradation');
|
|
283
|
+
if (/console\.log\(/.test(block) && !/\/\/\s*debug|\/\/\s*log/i.test(block)) codeQualityHits.push('console.log in shipped code without debug/log comment');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Block decision: any of (validateOutput severity=block) OR (>=2 drift hits) OR
|
|
287
|
+
// (>=1 code-quality hit) → block emit, surface violations + rewritten suggestion.
|
|
288
|
+
const mizanBlock = mizanVerdict && mizanVerdict.severity === 'block';
|
|
289
|
+
const driftBlock = driftHits.length >= 2;
|
|
290
|
+
const codeBlock = codeQualityHits.length >= 1;
|
|
291
|
+
|
|
292
|
+
if (mizanBlock || driftBlock || codeBlock) {
|
|
293
|
+
const violations = [];
|
|
294
|
+
if (mizanBlock) violations.push(`Mizan: ${(mizanVerdict.violations || []).join(', ')}`);
|
|
295
|
+
if (driftBlock) violations.push(`Drift triggers (${driftHits.length}): ${driftHits.map((h) => `"${h.trigger}" → ${h.memory}`).join(' | ')}`);
|
|
296
|
+
if (codeBlock) violations.push(`Code quality: ${codeQualityHits.join('; ')}`);
|
|
297
|
+
const rewritten = mizanVerdict?.rewritten || '';
|
|
298
|
+
|
|
299
|
+
const reason = `Aria Stop-gate output-quality block. Cognition passed (${cog.count}/${REQUIRED_LENSES}) but output failed quality gates:\n\n${violations.join('\n\n')}${rewritten ? `\n\nMizan rewrite suggestion:\n${rewritten}` : ''}\n\nRe-draft addressing the violations above. ARIA_OUTPUT_QC_ENABLED=false to disable in emergency (logged).`;
|
|
300
|
+
|
|
301
|
+
audit(`block-output-qc`, `mizan=${mizanBlock?'y':'n'} drift=${driftHits.length} code=${codeQualityHits.length}`);
|
|
302
|
+
console.log(JSON.stringify({ decision: 'block', reason }));
|
|
303
|
+
process.exit(2);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
audit('allow-output-qc',
|
|
307
|
+
`lenses=${cog.count} chars=${assistantText.length} drift=${driftHits.length} ` +
|
|
308
|
+
`mizan=${mizanVerdict ? mizanVerdict.severity : 'unavailable'} code=${codeQualityHits.length}`);
|
|
309
|
+
} else {
|
|
310
|
+
audit('allow-cognition',
|
|
311
|
+
`lenses=${cog.count} chars=${assistantText.length} ` +
|
|
312
|
+
`qPatt=${hasQuestionToUser ? 'y' : 'n'} substrateEv=${hasSubstrateEvidence ? 'y' : 'n'} ` +
|
|
313
|
+
(questionWithoutEvidence ? 'WARN-question-without-substrate' : 'ok'));
|
|
314
|
+
}
|
|
213
315
|
process.exit(0);
|
|
214
316
|
}
|
|
215
317
|
|