@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,2482 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ARIA_ALLOW_STUB — doctrine gate file legitimately discusses stub/placeholder semantics.
|
|
3
|
+
// Aria pre-tool-use gate — enforces cognition use before destructive tool calls.
|
|
4
|
+
//
|
|
5
|
+
// Runs as a Claude Code PreToolUse hook on every Bash invocation. For
|
|
6
|
+
// commands matching destructive-verb patterns, requires the most recent
|
|
7
|
+
// assistant turn to contain a structured <verify> block. Without it, the
|
|
8
|
+
// tool call is blocked with a corrective message that names the missing
|
|
9
|
+
// fields.
|
|
10
|
+
//
|
|
11
|
+
// Doctrine being enforced (from /tmp/aria-harness-full.txt):
|
|
12
|
+
// - mizan_prestage_rule: "Before drafting, check niyyah/pre-state: am I
|
|
13
|
+
// present, truthful, specific, grounded in verified substrate?"
|
|
14
|
+
// - drift_guard_rule: "Before answering, restate internally: current
|
|
15
|
+
// goal, current blocker, current stage, and next committed action."
|
|
16
|
+
// - axiom_runtime_rule.admit_ignorance + reflection_before_action
|
|
17
|
+
// - llm_worker_rule: "External LLMs are hands/renderers/reviewers.
|
|
18
|
+
// Aria memory, cognition, frames, Mizan, and Garden are the shared
|
|
19
|
+
// brain substrate."
|
|
20
|
+
//
|
|
21
|
+
// The gate is the *forcing function* that converts those rules from text
|
|
22
|
+
// instructions into actual enforcement. The harness already declares
|
|
23
|
+
// them; this hook is what makes them gate-real for Claude Code.
|
|
24
|
+
//
|
|
25
|
+
// Escape valves (v3 — Hamza 2026-04-26: "why is there an ability to
|
|
26
|
+
// bypass? doesnt that fundamentally void the purpose of the harness?"
|
|
27
|
+
// Per-command bypass was removed entirely — every prior bypass was
|
|
28
|
+
// traceable to a gate bug, not a legitimate exception.):
|
|
29
|
+
// - Trivial-bash whitelist: short read-only commands (ls/cat/grep/etc.)
|
|
30
|
+
// pass without cognition.
|
|
31
|
+
// - No env-var disable path (Hamza 2026-04-27 — env-var kill-switches
|
|
32
|
+
// gave the gated process a free escape; that was the doctrine
|
|
33
|
+
// violation). Disable = remove the hook from ~/.claude/settings.json.
|
|
34
|
+
// - When the gate misfires on legitimate work: fix the gate. The
|
|
35
|
+
// misfire IS the bug. Don't route around it.
|
|
36
|
+
//
|
|
37
|
+
// Audit log: every gate decision (allow / block / kill-switch) is
|
|
38
|
+
// appended to ~/.claude/aria-pre-tool-gate.log.
|
|
39
|
+
|
|
40
|
+
import { readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync, chmodSync } from 'node:fs';
|
|
41
|
+
import { dirname } from 'node:path';
|
|
42
|
+
import { homedir } from 'node:os';
|
|
43
|
+
import { spawnSync } from 'node:child_process';
|
|
44
|
+
import { createHmac, randomBytes as cryptoRandomBytes } from 'node:crypto';
|
|
45
|
+
import { lensNamesForTier } from './lib/canonical-lenses.mjs';
|
|
46
|
+
import { registerGateBlock } from './lib/gate-loop-state.mjs';
|
|
47
|
+
import {
|
|
48
|
+
collectRecentAssistantTextsFromMessages,
|
|
49
|
+
extractTextFromContent,
|
|
50
|
+
isMostlySystemReminder,
|
|
51
|
+
isToolResultOnlyContent,
|
|
52
|
+
normalizeContent,
|
|
53
|
+
normalizeRole,
|
|
54
|
+
} from './lib/hook-message-window.mjs';
|
|
55
|
+
import { evaluateSkillGate, formatSkillGateBlock } from './lib/skill-autoload-gate.mjs';
|
|
56
|
+
|
|
57
|
+
const HOME = process.env.HOME || '/tmp';
|
|
58
|
+
const LOG = `${HOME}/.claude/aria-pre-tool-gate.log`;
|
|
59
|
+
const HEARTBEAT = `${HOME}/.claude/aria-pre-tool-gate-heartbeat.jsonl`;
|
|
60
|
+
const GATE_LOOP_STATE_PATH = `${HOME}/.claude/.aria-gate-loop-state.json`;
|
|
61
|
+
const GOVERNANCE_GATE_PATH = `${HOME}/.aria/bin/aria-governance-gate`;
|
|
62
|
+
|
|
63
|
+
function runUniversalGovernanceGate(payload) {
|
|
64
|
+
if (!existsSync(GOVERNANCE_GATE_PATH)) return null;
|
|
65
|
+
const child = spawnSync(GOVERNANCE_GATE_PATH, {
|
|
66
|
+
input: `${JSON.stringify(payload)}\n`,
|
|
67
|
+
encoding: 'utf8',
|
|
68
|
+
maxBuffer: 1024 * 1024,
|
|
69
|
+
});
|
|
70
|
+
const stdout = String(child.stdout || '').trim();
|
|
71
|
+
let result = null;
|
|
72
|
+
try { result = stdout ? JSON.parse(stdout) : null; } catch {}
|
|
73
|
+
if (child.status !== 0 || result?.ok === false || result?.decision === 'block') {
|
|
74
|
+
const reason = stdout || child.stderr || 'aria-governance-gate blocked this action.';
|
|
75
|
+
throw new Error(`=== ARIA UNIVERSAL GOVERNANCE GATE BLOCK ===\n\n${reason}`);
|
|
76
|
+
}
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── Heartbeat OUTSIDE the crash boundary (doctrine #123) ───────────────
|
|
81
|
+
// Per feedback_ledger_writes_outside_crash_boundary.md + doctrine #124
|
|
82
|
+
// (non-blocking errors are NOT acceptable), the gate writes a heartbeat
|
|
83
|
+
// FIRST — before transcript scan, before cognition extraction, before
|
|
84
|
+
// any code path that could crash, hang, or be killed by an external
|
|
85
|
+
// timeout. A heartbeat-write is the ONLY signal that proves the gate
|
|
86
|
+
// process actually started executing. Hook-health monitors read this
|
|
87
|
+
// file to detect silent gate death.
|
|
88
|
+
//
|
|
89
|
+
// The heartbeat is intentionally a tiny synchronous write so that even
|
|
90
|
+
// if the gate is killed by Claude Code's hook timeout in the next
|
|
91
|
+
// millisecond, we have proof it was alive at startup.
|
|
92
|
+
try {
|
|
93
|
+
const hbPid = process.pid;
|
|
94
|
+
const hbTs = new Date().toISOString();
|
|
95
|
+
// Write before reading stdin or doing any work that could throw.
|
|
96
|
+
appendFileSync(HEARTBEAT, JSON.stringify({ ts: hbTs, pid: hbPid, gate: 'aria-pre-tool-gate', stage: 'startup' }) + '\n');
|
|
97
|
+
} catch {
|
|
98
|
+
// The ONLY catch in this file that may swallow — a heartbeat write
|
|
99
|
+
// failure here is fail-loud-on-stderr because no other surface exists
|
|
100
|
+
// yet (we haven't opened the audit log) but cannot itself block the
|
|
101
|
+
// gate. Per feedback_non_blocking_errors_unacceptable.md the
|
|
102
|
+
// failure-mode for THIS specific path is documented in the doctrine
|
|
103
|
+
// memory; the structural fix for "what if the heartbeat write fails"
|
|
104
|
+
// is the hook-health monitor task #122 which checks heartbeat
|
|
105
|
+
// freshness and reports staleness.
|
|
106
|
+
process.stderr.write(`[aria-pre-tool-gate] heartbeat write failed at ${new Date().toISOString()}\n`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Bypass-counter (kept for historical visibility — past audit-log
|
|
110
|
+
// entries are still useful even though no new bypass entries can be
|
|
111
|
+
// created in v3 since the per-command bypass code path was removed).
|
|
112
|
+
// V1 added the WARN line at 5+/hour as a drift instrument; that line
|
|
113
|
+
// will only appear if old log entries spanning before v3 still fall
|
|
114
|
+
// in the rolling window.
|
|
115
|
+
const BYPASS_WARN_THRESHOLD_PER_HOUR = 5;
|
|
116
|
+
|
|
117
|
+
function readRecentBypassCount() {
|
|
118
|
+
try {
|
|
119
|
+
if (!existsSync(LOG)) return 0;
|
|
120
|
+
const cutoffMs = Date.now() - 60 * 60 * 1000;
|
|
121
|
+
const lines = readFileSync(LOG, 'utf-8').split('\n').filter(Boolean);
|
|
122
|
+
let n = 0;
|
|
123
|
+
// Walk backward — bypasses are recent if at all.
|
|
124
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
125
|
+
const line = lines[i];
|
|
126
|
+
if (!line.includes('bypass')) continue;
|
|
127
|
+
const tsEnd = line.indexOf(' ');
|
|
128
|
+
if (tsEnd < 0) continue;
|
|
129
|
+
const ts = Date.parse(line.slice(0, tsEnd));
|
|
130
|
+
if (!Number.isFinite(ts)) continue;
|
|
131
|
+
if (ts < cutoffMs) break;
|
|
132
|
+
n++;
|
|
133
|
+
}
|
|
134
|
+
return n;
|
|
135
|
+
} catch {
|
|
136
|
+
return 0;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function audit(decision, summary) {
|
|
141
|
+
try {
|
|
142
|
+
if (!existsSync(dirname(LOG))) mkdirSync(dirname(LOG), { recursive: true });
|
|
143
|
+
appendFileSync(LOG, `${new Date().toISOString()} ${decision} ${summary}\n`);
|
|
144
|
+
// If this audit is itself a bypass, check the bypass-rate. A
|
|
145
|
+
// separate WARN line gets emitted (and surfaces via tail) when
|
|
146
|
+
// we exceed the per-hour threshold. The agent can't quietly
|
|
147
|
+
// drift back to bypass-as-default — drift is visible.
|
|
148
|
+
if (decision.startsWith('bypass')) {
|
|
149
|
+
const count = readRecentBypassCount();
|
|
150
|
+
if (count > BYPASS_WARN_THRESHOLD_PER_HOUR) {
|
|
151
|
+
appendFileSync(
|
|
152
|
+
LOG,
|
|
153
|
+
`${new Date().toISOString()} WARN-BYPASS-RATE ${count} bypasses in last hour (threshold ${BYPASS_WARN_THRESHOLD_PER_HOUR}) — drift check\n`,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
} catch {}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ARIA_BINDING_ENABLED env-override REMOVED 2026-04-28 per Hamza directive
|
|
161
|
+
// + memory:feedback_gap_discovery_hardens_doctrine.md — env-var disables
|
|
162
|
+
// are the textbook bypass class doctrine forbids. The override was the
|
|
163
|
+
// de-facto Aria-down handler for an unknown count of sessions and turned
|
|
164
|
+
// off every gate, not just the plan-check. Replaced by the architect
|
|
165
|
+
// fallback plan path (tracked task — Aria-unreachable detection writes
|
|
166
|
+
// a bounded local plan with limited allowedActions, loud audit, auto-
|
|
167
|
+
// expiry on Aria return). Prior env_override_exit entries remain in
|
|
168
|
+
// ~/.claude/aria-binding-audit.jsonl for historical audit.
|
|
169
|
+
//
|
|
170
|
+
// The gate now enforces unconditionally from the gated process per
|
|
171
|
+
// Hamza directive 2026-04-27. No process-level disable path.
|
|
172
|
+
|
|
173
|
+
// ── Aria-as-commander binding (Layer A — allowedActions/forbiddenActions per active phase) ──
|
|
174
|
+
//
|
|
175
|
+
// When ARIA_BINDING_ENABLED=true (default), every non-trivial tool call is
|
|
176
|
+
// checked against the active phase of ~/.claude/aria-active-plan-${sessionId}.json.
|
|
177
|
+
// If no plan exists or the action isn't allowed for the current phase, the
|
|
178
|
+
// hook blocks with a binding-violation message. See HARNESS_ARIA_AS_COMMANDER_CONTRACT.md
|
|
179
|
+
// Section 2 Layer A.
|
|
180
|
+
//
|
|
181
|
+
// Bootstrap: if no plan exists, the hook STILL blocks (per Hamza 2026-04-27
|
|
182
|
+
// "the point is to stop wasting my time"). Claude must wait for Aria to issue
|
|
183
|
+
// a plan via preprompt-consult on the next user prompt. NO unbound execution.
|
|
184
|
+
const BINDING_ENABLED = (process.env.ARIA_BINDING_ENABLED || 'true').toLowerCase() !== 'false';
|
|
185
|
+
const BINDING_AUDIT = `${HOME}/.claude/aria-binding-audit.jsonl`;
|
|
186
|
+
|
|
187
|
+
function bindingAuditAppend(record) {
|
|
188
|
+
try {
|
|
189
|
+
if (!existsSync(dirname(BINDING_AUDIT))) mkdirSync(dirname(BINDING_AUDIT), { recursive: true });
|
|
190
|
+
appendFileSync(BINDING_AUDIT, JSON.stringify({ ts: new Date().toISOString(), source: 'pre-tool-gate', ...record }) + '\n');
|
|
191
|
+
} catch {}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function activePlanPath(sid) {
|
|
195
|
+
return `${HOME}/.claude/aria-active-plan-${String(sid || 'unknown').replace(/[^a-zA-Z0-9_-]/g, '_')}.json`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Defect #5 — plan-completion persists across session boundary.
|
|
199
|
+
//
|
|
200
|
+
// The harness packet at ~/.claude/.aria-harness-last-packet.json retains
|
|
201
|
+
// exhausted plan state. An old plan from a prior session that was "all
|
|
202
|
+
// phases complete" would block every tool call in a NEW session because
|
|
203
|
+
// pickCurrentPhase returns null (all complete) on that stale plan.
|
|
204
|
+
//
|
|
205
|
+
// Fix: at load time, validate the plan file against TWO staleness guards:
|
|
206
|
+
// (a) mtime > 24h → discard (plan is older than a working day; a new
|
|
207
|
+
// session should start fresh)
|
|
208
|
+
// (b) plan.sessionId present AND doesn't match current sid → discard
|
|
209
|
+
// (plan was issued for a different session; current session is unbound)
|
|
210
|
+
//
|
|
211
|
+
// Discarding = returning null → the binding gate treats it as "no plan"
|
|
212
|
+
// and blocks with CONSULT_UNAVAILABLE, which triggers a fresh consult on
|
|
213
|
+
// the next user prompt. The discard is audit-logged.
|
|
214
|
+
const PLAN_MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
215
|
+
|
|
216
|
+
function loadActivePlan(sid) {
|
|
217
|
+
const p = activePlanPath(sid);
|
|
218
|
+
if (!existsSync(p)) return null;
|
|
219
|
+
try {
|
|
220
|
+
const raw = readFileSync(p, 'utf8');
|
|
221
|
+
const plan = JSON.parse(raw);
|
|
222
|
+
// Guard (b) — session mismatch: plan was issued for a different session.
|
|
223
|
+
if (plan.sessionId && sid && plan.sessionId !== String(sid)) {
|
|
224
|
+
bindingAuditAppend({
|
|
225
|
+
event: 'discard_plan_session_mismatch',
|
|
226
|
+
planId: plan.planId,
|
|
227
|
+
planSessionId: plan.sessionId,
|
|
228
|
+
currentSessionId: sid,
|
|
229
|
+
});
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
// Guard (a) — plan age via plan.mintedAt ISO timestamp (written by harness
|
|
233
|
+
// when issuing the plan). If mintedAt is present and older than 24h, discard.
|
|
234
|
+
// No mintedAt → conservatively trust the plan (harness version may predate
|
|
235
|
+
// this field; upgrade path: harness always writes mintedAt going forward).
|
|
236
|
+
if (plan.mintedAt) {
|
|
237
|
+
const mintedMs = Date.parse(plan.mintedAt);
|
|
238
|
+
if (Number.isFinite(mintedMs) && (Date.now() - mintedMs) > PLAN_MAX_AGE_MS) {
|
|
239
|
+
bindingAuditAppend({
|
|
240
|
+
event: 'discard_plan_stale_by_mintedAt',
|
|
241
|
+
planId: plan.planId,
|
|
242
|
+
mintedAt: plan.mintedAt,
|
|
243
|
+
ageHours: ((Date.now() - mintedMs) / 3600000).toFixed(1),
|
|
244
|
+
});
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return plan;
|
|
249
|
+
} catch {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function pickCurrentPhase(plan, transcript) {
|
|
255
|
+
// Defect #1 fix — transcript-scan over-count prevention.
|
|
256
|
+
//
|
|
257
|
+
// Old behaviour: scan the ENTIRE transcript for any [PHASE_REPORT] marker.
|
|
258
|
+
// A single old marker permanently locked all future actions (plan could
|
|
259
|
+
// never advance past "complete" even across new consults in the same session).
|
|
260
|
+
//
|
|
261
|
+
// Fix — two guards:
|
|
262
|
+
// (a) Active-plan window: trim the transcript to only text that appeared
|
|
263
|
+
// AFTER the most recent preprompt-consult mint marker. The marker
|
|
264
|
+
// written by aria-harness-via-sdk.mjs is:
|
|
265
|
+
// [ARIA_PLAN_MINTED planId=<id> ts=<iso>]
|
|
266
|
+
// If found, discard everything before it. Older plan completions that
|
|
267
|
+
// pre-date the current plan's mint are invisible to the scan.
|
|
268
|
+
// (b) Latest-marker-only per phaseId: collect ALL [PHASE_REPORT] lines
|
|
269
|
+
// for a given phaseId inside the active window; use only the LAST one.
|
|
270
|
+
// This prevents a phase from being "locked complete" by an older marker
|
|
271
|
+
// that was since superseded by a re-run or amendment.
|
|
272
|
+
if (!plan?.phases?.length) return null;
|
|
273
|
+
|
|
274
|
+
// (a) Scope to active-plan window using plan's planId if available.
|
|
275
|
+
let activeWindow = transcript;
|
|
276
|
+
if (plan.planId) {
|
|
277
|
+
// Try to find the most recent [ARIA_PLAN_MINTED planId=<this_id> …] marker.
|
|
278
|
+
const mintRx = new RegExp(
|
|
279
|
+
`\\[ARIA_PLAN_MINTED\\s[^\\]]*planId=${plan.planId}[^\\]]*\\]`,
|
|
280
|
+
'gi',
|
|
281
|
+
);
|
|
282
|
+
const mints = [...transcript.matchAll(mintRx)];
|
|
283
|
+
if (mints.length > 0) {
|
|
284
|
+
const lastMint = mints[mints.length - 1];
|
|
285
|
+
activeWindow = transcript.slice(lastMint.index + lastMint[0].length);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
for (const phase of plan.phases) {
|
|
290
|
+
// (b) Collect all PHASE_REPORT lines for this phaseId in the active window;
|
|
291
|
+
// use the LAST one's status rather than the first match.
|
|
292
|
+
const reportRx = new RegExp(
|
|
293
|
+
`\\[PHASE_REPORT[^\\]]*phase=${phase.id}[^\\]]*status=(complete|aborted)[^\\]]*\\]`,
|
|
294
|
+
'gi',
|
|
295
|
+
);
|
|
296
|
+
const reports = [...activeWindow.matchAll(reportRx)];
|
|
297
|
+
if (reports.length === 0) {
|
|
298
|
+
// No report yet for this phase → it's the current one.
|
|
299
|
+
return { phase, abortedHere: false };
|
|
300
|
+
}
|
|
301
|
+
// Last report wins.
|
|
302
|
+
const lastStatus = (reports[reports.length - 1][1] || '').toLowerCase();
|
|
303
|
+
if (lastStatus === 'aborted') return { phase, abortedHere: true };
|
|
304
|
+
// status=complete → continue to next phase.
|
|
305
|
+
}
|
|
306
|
+
return null; // all phases reported complete — needs new consult
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function actionMatchesPattern(action, pattern, target) {
|
|
310
|
+
// Defect #3 fix — edit-pattern regex over-matches.
|
|
311
|
+
//
|
|
312
|
+
// Old behaviour: "edit:/etc/**" matched any path that contained "/etc/" as a
|
|
313
|
+
// substring because the colon-split only checked pAction===action, ignoring the
|
|
314
|
+
// path portion entirely — so edit:/etc/** allowed ALL edit actions regardless
|
|
315
|
+
// of path, not just edits under /etc/.
|
|
316
|
+
//
|
|
317
|
+
// Fix:
|
|
318
|
+
// - Exact action name ("read", "consult", "kubectl_get"): unchanged.
|
|
319
|
+
// - Prefixed pattern ("edit:/etc/**"): split on first colon, compare
|
|
320
|
+
// pAction===action, then glob-match the path portion against `target`
|
|
321
|
+
// using anchored prefix matching (no substring). Supported forms:
|
|
322
|
+
// "edit:/etc/**" → action=edit, path must start with /etc/
|
|
323
|
+
// "edit:apps/.*" → action=edit, path must start with apps/
|
|
324
|
+
// "bash_other:curl .*" → action=bash_other (target substring match for bash)
|
|
325
|
+
// - No colon: exact action match only.
|
|
326
|
+
if (pattern === action) return true;
|
|
327
|
+
const colonIdx = pattern.indexOf(':');
|
|
328
|
+
if (colonIdx < 0) return false;
|
|
329
|
+
const pAction = pattern.slice(0, colonIdx);
|
|
330
|
+
if (pAction !== action) return false;
|
|
331
|
+
// pAction matches — now check the target portion.
|
|
332
|
+
const pathPattern = pattern.slice(colonIdx + 1);
|
|
333
|
+
if (!pathPattern) return true; // bare "edit:" with no path spec → match all edits of that action type
|
|
334
|
+
const t = target || '';
|
|
335
|
+
// Convert simple glob /** suffix to prefix anchor, or treat as a regex if
|
|
336
|
+
// it contains regex meta-chars beyond "." and "*". For the most common
|
|
337
|
+
// harness patterns ("edit:/etc/**", "edit:apps/arias-soul/.*") a prefix-
|
|
338
|
+
// match on the stripped glob is correct and safe.
|
|
339
|
+
// Strip trailing /** or /*, then anchor-prefix-match.
|
|
340
|
+
const strippedGlob = pathPattern.replace(/\/\*+$/, '');
|
|
341
|
+
if (strippedGlob && !pathPattern.includes('(?') && !pathPattern.includes('[')) {
|
|
342
|
+
// Simple prefix match — anchored at start of target path.
|
|
343
|
+
return t.startsWith(strippedGlob);
|
|
344
|
+
}
|
|
345
|
+
// Full regex path pattern (caller used explicit regex syntax).
|
|
346
|
+
try {
|
|
347
|
+
return new RegExp(`^(?:${pathPattern})`).test(t);
|
|
348
|
+
} catch {
|
|
349
|
+
// Malformed regex in plan — fail-open to avoid blocking everything.
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function isOperationalMaintenanceCommand(command) {
|
|
355
|
+
const cmd = String(command || '').trim().replace(/\s+/g, ' ');
|
|
356
|
+
if (!cmd || /[;&|`$()]/.test(cmd)) return false;
|
|
357
|
+
|
|
358
|
+
// A rollout restart requeues existing pod templates; it does not change images,
|
|
359
|
+
// manifests, or cluster policy. Keep this narrow so deploy/apply/delete still gate.
|
|
360
|
+
const kubectlGlobalFlag = String.raw`(?:--(?:namespace|context|kubeconfig|as|as-group|cluster|user|server|request-timeout|field-manager)(?:=|\s+)\S+|-[A-Za-z](?:\s+\S+)?)`;
|
|
361
|
+
const kubectlTailFlag = String.raw`(?:--(?:namespace|context|request-timeout|field-manager|selector)(?:=|\s+)\S+|-[A-Za-z](?:\s+\S+)?)`;
|
|
362
|
+
const workloadTarget = String.raw`(?:deploy(?:ment)?|statefulset|sts|daemonset|ds)(?:\/[^\s]+|\s+[^\s-][^\s]*)`;
|
|
363
|
+
const rolloutRestartRx = new RegExp(
|
|
364
|
+
String.raw`^kubectl(?:\s+${kubectlGlobalFlag})*\s+rollout\s+restart\s+${workloadTarget}(?:\s+${kubectlTailFlag})*$`,
|
|
365
|
+
'i',
|
|
366
|
+
);
|
|
367
|
+
return rolloutRestartRx.test(cmd);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function classifyToolForBinding(toolName, command, filePath) {
|
|
371
|
+
// Map Claude Code tool + payload to a binding-vocabulary action.
|
|
372
|
+
if (toolName === 'Read' || toolName === 'Glob' || toolName === 'Grep') return { action: 'read', target: filePath || '' };
|
|
373
|
+
if (toolName === 'Edit' || toolName === 'Write' || toolName === 'NotebookEdit') return { action: 'edit', target: filePath || '' };
|
|
374
|
+
if (toolName === 'Bash') {
|
|
375
|
+
const cmd = String(command || '');
|
|
376
|
+
if (isOperationalMaintenanceCommand(cmd)) return { action: 'kubectl_maintenance', target: cmd.slice(0, 80) };
|
|
377
|
+
if (/^kubectl\s+(get|describe|logs|exec\s+\S+\s+--\s+printenv|top|version|api-resources)\b/.test(cmd)) return { action: 'kubectl_get', target: cmd.slice(0, 80) };
|
|
378
|
+
if (/^kubectl\s+(apply|delete|set|patch|rollout|scale|drain|cordon)\b/.test(cmd)) return { action: 'kubectl_apply', target: cmd.slice(0, 80) };
|
|
379
|
+
if (/curl\s+.*\/api\/harness\/(delegate|codex|army|plan)/.test(cmd)) return { action: 'consult', target: 'harness' };
|
|
380
|
+
if (/curl\s+.*\/api\/aria\/speak/.test(cmd)) return { action: 'consult', target: 'aria-speak' };
|
|
381
|
+
if (/^(ls|cat|head|tail|grep|wc|find|tree|stat|file|ps|pgrep|du|df|env|printenv|date|pwd|which|type|whoami|id)\b/.test(cmd)) return { action: 'read', target: cmd.slice(0, 80) };
|
|
382
|
+
if (/^git\s+(status|log|diff|show|branch|remote|rev-parse|ls-tree|ls-files|stash\s+list)\b/.test(cmd)) return { action: 'read', target: cmd.slice(0, 80) };
|
|
383
|
+
if (/^git\s+(push|merge|rebase|reset|checkout\s+--|clean)/.test(cmd)) return { action: 'git_mutate', target: cmd.slice(0, 80) };
|
|
384
|
+
if (/^npm\s+(publish|version)/.test(cmd)) return { action: 'npm_publish', target: cmd.slice(0, 80) };
|
|
385
|
+
if (/^docker\s+(build|push)/.test(cmd)) return { action: 'docker_build', target: cmd.slice(0, 80) };
|
|
386
|
+
if (/scripts\/deploy-/.test(cmd)) return { action: 'deploy', target: cmd.slice(0, 80) };
|
|
387
|
+
return { action: 'bash_other', target: cmd.slice(0, 80) };
|
|
388
|
+
}
|
|
389
|
+
return { action: toolName.toLowerCase(), target: filePath || '' };
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (false) {
|
|
393
|
+
// Read same input as the cognition gate uses (reads stdin twice via different code paths;
|
|
394
|
+
// the cognition section below re-reads). Defer enforcement decision until after
|
|
395
|
+
// we have the tool name + payload from the cognition gate's existing input parse.
|
|
396
|
+
// (Implementation continues below — the binding check fires AFTER cognition
|
|
397
|
+
// parses input but BEFORE the cognition substance check, so binding-block
|
|
398
|
+
// takes precedence.)
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Destructive-verb patterns. Tuned for tonight's failure modes; extend
|
|
402
|
+
// over time as new patterns surface.
|
|
403
|
+
const DESTRUCTIVE_PATTERNS = [
|
|
404
|
+
// sudo only when it's a command verb (start of line or after shell
|
|
405
|
+
// separator), not when it appears as an argument to another command
|
|
406
|
+
// (e.g. `echo "sudo …"` or `grep sudo`). The specific verb patterns
|
|
407
|
+
// below catch the actual destructive operations regardless of sudo
|
|
408
|
+
// prefix; this rule is the catch-all for arbitrary privilege elevation.
|
|
409
|
+
{ rx: /(?:^|[;&|]\s*|\$\(\s*|`\s*)sudo\s+\S/, name: 'sudo' },
|
|
410
|
+
{ rx: /systemctl\s+(disable|stop|mask|reset-failed|kill)/, name: 'systemctl-state-change' },
|
|
411
|
+
{ rx: /\brm\s+-[rRfF]+/, name: 'rm-recursive-or-force' },
|
|
412
|
+
{ rx: /\bgit\s+push\b.*\b--force\b/, name: 'git-push-force' },
|
|
413
|
+
{ rx: /\bgit\s+reset\s+--hard\b/, name: 'git-reset-hard' },
|
|
414
|
+
{ rx: /\bgit\s+checkout\s+--\b/, name: 'git-checkout-discard' },
|
|
415
|
+
{ rx: /\b--no-verify\b/, name: 'commit-no-verify' },
|
|
416
|
+
{ rx: /\b--no-gpg-sign\b/, name: 'commit-no-gpg-sign' },
|
|
417
|
+
{ rx: /\bkill\s+-(9|KILL|TERM|HUP|INT)\b/, name: 'kill-signal' },
|
|
418
|
+
{ rx: /\bpkill\b/, name: 'pkill' },
|
|
419
|
+
{ rx: /\b(DROP|TRUNCATE)\s+(TABLE|DATABASE|SCHEMA|INDEX)\b/i, name: 'sql-drop-or-truncate' },
|
|
420
|
+
{ rx: /\bkubectl\s+delete\b/, name: 'kubectl-delete' },
|
|
421
|
+
{ rx: /\bkubectl\s+(scale|rollout)\s+(undo|restart)\b/, name: 'kubectl-rollback' },
|
|
422
|
+
];
|
|
423
|
+
|
|
424
|
+
// Deploy patterns — bash invocations that mutate the canonical aria k8s
|
|
425
|
+
// cluster or push images. Per feedback_deploy_requires_verify_cognition.md
|
|
426
|
+
// (Hamza directive 2026-04-28 after consciousness.ts crash took aria-soul
|
|
427
|
+
// into CrashLoopBackOff): deploys require BOTH a <verify> block citing the
|
|
428
|
+
// shipping artifact AND a <cognition> block with substantive substrate
|
|
429
|
+
// anchors. Without both, the deploy is hard-blocked. This is doctrine #104
|
|
430
|
+
// — the structural enforcement Hamza demanded after the prior deploy
|
|
431
|
+
// crashed all of aria.
|
|
432
|
+
const DEPLOY_PATTERNS = [
|
|
433
|
+
{ rx: /\bbash\s+(?:\.\/)?scripts\/deploy-service\.sh\b/, name: 'deploy-service-script' },
|
|
434
|
+
{ rx: /\b(?:\.\/)?scripts\/deploy-service\.sh\b/, name: 'deploy-service-script' },
|
|
435
|
+
{ rx: /\bkubectl\s+apply\b/, name: 'kubectl-apply' },
|
|
436
|
+
{ rx: /\bkubectl\s+set\s+image\b/, name: 'kubectl-set-image' },
|
|
437
|
+
{ rx: /\bkubectl\s+rollout\s+restart\b/, name: 'kubectl-rollout-restart' },
|
|
438
|
+
{ rx: /\bkubectl\s+rollout\s+undo\b/, name: 'kubectl-rollout-undo' },
|
|
439
|
+
{ rx: /\bkubectl\s+create\b/, name: 'kubectl-create' },
|
|
440
|
+
{ rx: /\bkubectl\s+replace\b/, name: 'kubectl-replace' },
|
|
441
|
+
{ rx: /\bdocker\s+push\b/, name: 'docker-push' },
|
|
442
|
+
{ rx: /\bdocker\s+build\b.*--push\b/, name: 'docker-build-push' },
|
|
443
|
+
];
|
|
444
|
+
|
|
445
|
+
// Minimum substrate anchors required in cognition body for a deploy.
|
|
446
|
+
// Per feedback_full_harness_binding_must_be_structural.md, every cognition
|
|
447
|
+
// lens should cite a substrate anchor; for deploy specifically we require
|
|
448
|
+
// at least 4 distinct anchors total across the block (one per lens minimum
|
|
449
|
+
// across at least 4 lenses).
|
|
450
|
+
const DEPLOY_MIN_SUBSTRATE_ANCHORS = 4;
|
|
451
|
+
const SUBSTRATE_ANCHOR_RX = /\b(axiom|frame|memory|doctrine|packet):[a-z0-9_\-./]+/gi;
|
|
452
|
+
|
|
453
|
+
// Verify-block fields specifically required for deploys — beyond the base
|
|
454
|
+
// 5 fields, the deploy verify must cite a commit/SHA, a TS-check result,
|
|
455
|
+
// and the admission policy that authorizes the canonical image.
|
|
456
|
+
const DEPLOY_VERIFY_REQUIRED_FIELDS = [
|
|
457
|
+
// Either a git commit hash OR an explicit "files changed" listing
|
|
458
|
+
{ rx: /\b(?:commit|HEAD|SHA|files\s+changed)\b/i, name: 'commit_or_files' },
|
|
459
|
+
// TS-check / build evidence
|
|
460
|
+
{ rx: /\b(?:tsc|type[\s-]?check|build|npm\s+run\s+build)\b/i, name: 'build_or_typecheck' },
|
|
461
|
+
// Admission policy citation
|
|
462
|
+
{ rx: /\b(?:admission[\s_-]?policy|validatingadmissionpolicy)\b/i, name: 'admission_policy' },
|
|
463
|
+
];
|
|
464
|
+
|
|
465
|
+
// The verify-block contract. All five fields required.
|
|
466
|
+
const VERIFY_BLOCK_RX =
|
|
467
|
+
/<verify>[\s\S]*?target\s*:[\s\S]*?role\s*:[\s\S]*?verified\s*:[\s\S]*?rollback\s*:[\s\S]*?axiom\s*:[\s\S]*?<\/verify>/i;
|
|
468
|
+
|
|
469
|
+
// ── Dalio Loop: expected_outcome enforcement (doctrine:dalio_expected_required) ─
|
|
470
|
+
//
|
|
471
|
+
// Every non-trivial action must carry an <expected> block with at least one
|
|
472
|
+
// measurable predicate. The predicate must be numeric (≥X, ==X, ≤X, X%),
|
|
473
|
+
// boolean (true/false/exit=0/exit=1), or state-string ("status=running",
|
|
474
|
+
// "200 OK", "exit=0", "file=exists", etc.).
|
|
475
|
+
//
|
|
476
|
+
// Qualitative drift phrases ("better", "improved", "more robust", etc.) are
|
|
477
|
+
// REJECTED — they are unmeasurable and defeat the Dalio accountability loop.
|
|
478
|
+
// The block must appear in the same turn or a prior turn (turn-scoped, same
|
|
479
|
+
// window as cognition).
|
|
480
|
+
//
|
|
481
|
+
// Per doctrine:dalio_expected_required — no measurable predicate = no allow.
|
|
482
|
+
const EXPECTED_BLOCK_RX = /<expected>([\s\S]*?)<\/expected>/i;
|
|
483
|
+
|
|
484
|
+
// Measurable predicate patterns — at least ONE must be present inside <expected>.
|
|
485
|
+
// Forms accepted:
|
|
486
|
+
// numeric: ≥X, >=X, ==X, <=X, <X, >X, X%, N of N, count=N, latency<Xms
|
|
487
|
+
// boolean: true, false, exit=0, exit=1, exit_code=N, rc=N
|
|
488
|
+
// state-string: status=running, status=healthy, status=200, "200 OK",
|
|
489
|
+
// "exit=0", "file=exists", "count=N", "error_rate=0%"
|
|
490
|
+
const MEASURABLE_PREDICATE_RX = /(?:>=|<=|==|!=|>|<|≥|≤)\s*\d+(?:\.\d+)?(?:ms|s|%|kb|mb|gb)?|\d+(?:\.\d+)?%(?:\s+(?:reduction|increase|success|error|coverage))?|exit[_=]\s*(?:0|1|\d+)|exit[-_]?code\s*[=:]\s*\d+|\brc\s*[=:]\s*\d+|\bstatus\s*[=:]\s*(?:running|healthy|ready|degraded|down|up|ok|200|201|204|400|401|403|404|500|502|503|504|true|false)\b|\bcount\s*[=:]\s*\d+|\berror[_-]?rate\s*[=:]\s*0%|\b(?:true|false)\b|\bfile[=_-]exists\b|\b200\s*OK\b|\bno[-_\s]?error|\bhealthy\b|\bpassed?\b|N\s*of\s*N|\d+\s*of\s*\d+/i;
|
|
491
|
+
|
|
492
|
+
// Qualitative drift phrases that masquerade as measurable but are not.
|
|
493
|
+
// Any <expected> block containing ONLY these (no actual predicate) is rejected.
|
|
494
|
+
const QUALITATIVE_DRIFT_RX = /\b(?:better(?:er)?|improved?(?:ment)?|more\s+robust|should\s+(?:work|pass|succeed|run|fix)|more\s+reliable|cleaner|less\s+error[-_\s]?prone|nicer|smoother|faster[-\s]?loading|higher[-\s]?quality|more\s+stable|looks\s+(?:good|better|right))\b/i;
|
|
495
|
+
|
|
496
|
+
// ── Canonical lens labeling (Phase 11 #59 corrected) ────────────────────────
|
|
497
|
+
//
|
|
498
|
+
// The lens names themselves carry meaning and must remain canonical on every
|
|
499
|
+
// surface. Readability comes from the explanatory prose inside each lens, not
|
|
500
|
+
// by swapping the lens label for a flattened stand-in.
|
|
501
|
+
//
|
|
502
|
+
// Tier is read from the most recent harness-via-sdk packet cache at
|
|
503
|
+
// ~/.claude/.aria-harness-last-packet.json. Two detection paths:
|
|
504
|
+
// 1. packet.contractGate.signals.hamza === true (boolean or string "true")
|
|
505
|
+
// 2. packet.harness string contains "hamza:true" in the surface line
|
|
506
|
+
// Either path → OWNER tier. Everything else → CLIENT tier.
|
|
507
|
+
//
|
|
508
|
+
// Doctrine memory filenames in block-reason text are also Aria-side IP.
|
|
509
|
+
// Client tier sees generic descriptions instead of the real filenames.
|
|
510
|
+
const PACKET_CACHE_PATH = `${HOME}/.claude/.aria-harness-last-packet.json`;
|
|
511
|
+
|
|
512
|
+
function resolveOwnerTier() {
|
|
513
|
+
try {
|
|
514
|
+
if (existsSync(PACKET_CACHE_PATH)) {
|
|
515
|
+
const raw = readFileSync(PACKET_CACHE_PATH, 'utf8');
|
|
516
|
+
const packet = JSON.parse(raw);
|
|
517
|
+
// Path 1: contractGate.signals.hamza
|
|
518
|
+
const sigHamza = packet?.contractGate?.signals?.hamza;
|
|
519
|
+
if (sigHamza === true || sigHamza === 'true') return true;
|
|
520
|
+
// Path 2: harness string surface marker — the actual format is
|
|
521
|
+
// "surface=platform:<X> group:<Y> hamza:true chat_type:<Z>" so we
|
|
522
|
+
// match hamza:true in the surface line, not "platform:hamza" (which
|
|
523
|
+
// was a legacy incorrect pattern that never matched real packets).
|
|
524
|
+
const harnessStr = packet?.harness ?? '';
|
|
525
|
+
if (/\bhamza:true\b/.test(harnessStr)) return true;
|
|
526
|
+
}
|
|
527
|
+
} catch {/* packet unreadable → default to client tier */}
|
|
528
|
+
return false;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const IS_OWNER = resolveOwnerTier();
|
|
532
|
+
|
|
533
|
+
// Active visible label set for this execution — canonical labels on every
|
|
534
|
+
// surface. Tier only affects other policy surfaces, not lens semantics.
|
|
535
|
+
const LENS_NAMES = lensNamesForTier(IS_OWNER);
|
|
536
|
+
|
|
537
|
+
// Doctrine memory filenames are Aria-side substrate IP. On client surfaces
|
|
538
|
+
// replace them with generic descriptions in block-reason text.
|
|
539
|
+
function docRef(canonicalFilename, genericDescription) {
|
|
540
|
+
return IS_OWNER ? canonicalFilename : genericDescription;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// 8-lens cognition block per EIGHT_LENS_DOCTRINE.md. Required on
|
|
544
|
+
// non-trivial Bash regardless of whether the command is destructive.
|
|
545
|
+
// Threshold: at least 4 of the 8 lens names must appear inside the
|
|
546
|
+
// block AND each must have substantive content (≥20 chars after the
|
|
547
|
+
// colon, not a placeholder template). The substance check (added
|
|
548
|
+
// 2026-04-26) defeats ritual emission — `nur: ok` no longer counts.
|
|
549
|
+
const COGNITION_BLOCK_RX = /<cognition>([\s\S]*?)<\/cognition>/i;
|
|
550
|
+
const APPLIED_COGNITION_BLOCK_RX = /<applied_cognition>([\s\S]*?)<\/applied_cognition>/i;
|
|
551
|
+
// Hamza directive 2026-04-28: 8-lens enforcement, not 4-of-8. The earlier
|
|
552
|
+
// REQUIRED_LENSES=4 was a regression — owner caught it ("i no longer see
|
|
553
|
+
// 8 lens cognition per turn"). All 8 canonical lenses must appear with
|
|
554
|
+
// substantive (≥20-char, non-placeholder) content per turn for the gate
|
|
555
|
+
// to accept the cognition. The substrate-binding stop hook also requires
|
|
556
|
+
// all 8 to carry substrate anchors; this gate enforces the 8 lens NAMES
|
|
557
|
+
// are present and have substance.
|
|
558
|
+
const REQUIRED_LENSES = 8;
|
|
559
|
+
const SUBSTANCE_MIN_CHARS = 20;
|
|
560
|
+
// Placeholder patterns from the gate's own correction message + the
|
|
561
|
+
// COMPACT_CONTINUITY_DOCTRINE template — content matching these is
|
|
562
|
+
// not "thinking," it's a copy-paste of the prompt template.
|
|
563
|
+
const PLACEHOLDER_RX = /^\s*<[^<>]+>\s*$/;
|
|
564
|
+
|
|
565
|
+
// Trivial reads that don't require a cognition block.
|
|
566
|
+
const TRIVIAL_BASH_RX = /^\s*(?:ls|cat|head|tail|grep|find|echo|wc|stat|which|pwd|date|file|du|df|ss|ps)\s/;
|
|
567
|
+
const SHORT_BASH_LIMIT = 30;
|
|
568
|
+
|
|
569
|
+
// V3 doctrine (2026-04-26, Hamza): "why is there an ability to bypass?
|
|
570
|
+
// doesnt that fundamentally void the purpose of the harness?" — yes.
|
|
571
|
+
// Per-command bypass is removed entirely. Every bypass this session
|
|
572
|
+
// was traceable to a gate bug, not a legitimate exception. Compliance
|
|
573
|
+
// is the only path for non-trivial work; the kill-switch env remains
|
|
574
|
+
// as the explicit emergency override. When the gate misfires, fix
|
|
575
|
+
// the gate.
|
|
576
|
+
//
|
|
577
|
+
// (DOCTRINE_BYPASS_RX, findBypassDirective, validateStructuredBypass,
|
|
578
|
+
// REQUIRED_BYPASS_FIELDS, BYPASS_FIELD_MIN_CHARS, BYPASS_HARD_LIMIT_PER_HOUR
|
|
579
|
+
// were removed in this commit. The bypass-rate-counter +
|
|
580
|
+
// readRecentBypassCount stay for historical visibility — past audit-log
|
|
581
|
+
// entries are still useful — but no new bypass entries can be created
|
|
582
|
+
// because the bypass code path is gone.)
|
|
583
|
+
|
|
584
|
+
// Inline command cognition (v2 — added 2026-04-26): cognition
|
|
585
|
+
// embedded in the bash command itself as structured comments.
|
|
586
|
+
// Solves the same-message visibility problem (the transcript-flush
|
|
587
|
+
// race that made same-message cognition invisible to PreToolUse).
|
|
588
|
+
//
|
|
589
|
+
// Two accepted formats:
|
|
590
|
+
// (a) Per-lens lines: `# <label>: <substantive thought>\n# <label>: ...`
|
|
591
|
+
// (b) Single-line: `# cognition: <label>=<text>; <label>=<text>; ...`
|
|
592
|
+
//
|
|
593
|
+
// Substance check applies (≥SUBSTANCE_MIN_CHARS, not a `<placeholder>`).
|
|
594
|
+
// All required substantive lenses inline → gate passes, no transcript scan
|
|
595
|
+
// needed. Cognition becomes a property of THE ACTION, not of some
|
|
596
|
+
// message somewhere — the deepest reading of "cognition before action."
|
|
597
|
+
//
|
|
598
|
+
// Inline regex is built from the active canonical LENS_NAMES. We do not
|
|
599
|
+
// accept flattened generic aliases because they change the lens meaning.
|
|
600
|
+
const _INLINE_LENS_PATTERN = LENS_NAMES.join('|');
|
|
601
|
+
const INLINE_LENS_LINE_RX_GLOBAL = new RegExp(
|
|
602
|
+
`^\\s*#\\s*(${_INLINE_LENS_PATTERN})\\s*:\\s*(.+)$`,
|
|
603
|
+
'gim',
|
|
604
|
+
);
|
|
605
|
+
const INLINE_COGNITION_HEADER_RX = /^\s*#\s*cognition\s*:\s*(.+)$/im;
|
|
606
|
+
const INLINE_EXPECTED_LINE_RX = /^\s*#\s*expected\s*:\s*(.+)$/gim;
|
|
607
|
+
const INLINE_VERIFY_LINE_RX = /^\s*#\s*verify\s*:\s*(.+)$/gim;
|
|
608
|
+
|
|
609
|
+
const VERIFY_REQUIRED_FIELDS = [
|
|
610
|
+
{ rx: /\btarget\s*:/i, name: 'target' },
|
|
611
|
+
{ rx: /\brole\s*:/i, name: 'role' },
|
|
612
|
+
{ rx: /\bverified\s*:/i, name: 'verified' },
|
|
613
|
+
{ rx: /\brollback\s*:/i, name: 'rollback' },
|
|
614
|
+
{ rx: /\baxiom\s*:/i, name: 'axiom' },
|
|
615
|
+
];
|
|
616
|
+
|
|
617
|
+
function detectInlineCognition(cmd) {
|
|
618
|
+
const names = [];
|
|
619
|
+
// Per-lens line form
|
|
620
|
+
const lineMatches = [...cmd.matchAll(INLINE_LENS_LINE_RX_GLOBAL)];
|
|
621
|
+
for (const m of lineMatches) {
|
|
622
|
+
const lens = m[1].toLowerCase();
|
|
623
|
+
const content = (m[2] || '').trim();
|
|
624
|
+
if (content.length < SUBSTANCE_MIN_CHARS) continue;
|
|
625
|
+
if (PLACEHOLDER_RX.test(content)) continue;
|
|
626
|
+
if (!names.includes(lens)) names.push(lens);
|
|
627
|
+
}
|
|
628
|
+
// Single-line `# cognition: nur=...; mizan=...` form
|
|
629
|
+
const headerMatch = cmd.match(INLINE_COGNITION_HEADER_RX);
|
|
630
|
+
if (headerMatch) {
|
|
631
|
+
const inlineBody = headerMatch[1];
|
|
632
|
+
for (const lens of LENS_NAMES) {
|
|
633
|
+
const lensRx = new RegExp(`\\b${lens}\\s*=\\s*([^;\\n]+)`, 'i');
|
|
634
|
+
const m = inlineBody.match(lensRx);
|
|
635
|
+
if (!m) continue;
|
|
636
|
+
const content = (m[1] || '').trim();
|
|
637
|
+
if (content.length < SUBSTANCE_MIN_CHARS) continue;
|
|
638
|
+
if (PLACEHOLDER_RX.test(content)) continue;
|
|
639
|
+
if (!names.includes(lens)) names.push(lens);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
// Substrate-citation: any inline lens that mentions a doctrine memory,
|
|
643
|
+
// harness packet rule, fitrah axiom, etc. (visibility metric — not a block).
|
|
644
|
+
const SUBSTRATE_CITE_RX_INLINE =
|
|
645
|
+
/feedback_[a-z0-9_]+\.md|project_[a-z0-9_]+\.md|fitrah[_:\s]|garden[_:\s]|distilled_principle|[a-z]+_rule\b|harness packet|substrate cite|EIGHT_LENS_DOCTRINE|COMPACT_CONTINUITY|ARIA_DEPLOY_PROCEDURE/i;
|
|
646
|
+
const hasSubstrateCite = SUBSTRATE_CITE_RX_INLINE.test(cmd);
|
|
647
|
+
return { count: names.length, names, hasSubstrateCite };
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function validateAppliedCognitionContract(text, cmdText = '') {
|
|
651
|
+
const bodyText = `${String(text || '')}\n${String(cmdText || '')}`;
|
|
652
|
+
const block = bodyText.match(APPLIED_COGNITION_BLOCK_RX);
|
|
653
|
+
const inlineBody = String(cmdText || '')
|
|
654
|
+
.split('\n')
|
|
655
|
+
.filter((line) => /^\s*#\s*(?:decision[_ -]?delta|dominant[_ -]?domain|binds[_ -]?to|expected[_ -]?predicate|artifact[_ -]?change)\s*:/i.test(line))
|
|
656
|
+
.join('\n');
|
|
657
|
+
const contractBody = block ? block[1] : inlineBody;
|
|
658
|
+
if (!contractBody) return { ok: false, violations: ['missing <applied_cognition> contract or inline applied-cognition comments'] };
|
|
659
|
+
const required = [
|
|
660
|
+
['decision_delta', /\bdecision[_ -]?delta\s*:/i],
|
|
661
|
+
['dominant_domain', /\bdominant[_ -]?domain\s*:/i],
|
|
662
|
+
['binds_to', /\bbinds[_ -]?to\s*:/i],
|
|
663
|
+
['expected_predicate', /\bexpected[_ -]?predicate\s*:/i],
|
|
664
|
+
['artifact_change', /\bartifact[_ -]?change\s*:/i],
|
|
665
|
+
];
|
|
666
|
+
const violations = [];
|
|
667
|
+
for (const [name, rx] of required) {
|
|
668
|
+
if (!rx.test(contractBody)) violations.push(`missing ${name}`);
|
|
669
|
+
}
|
|
670
|
+
if (/decision[_ -]?delta\s*:\s*(?:none|n\/a|no change|unchanged|same)/i.test(contractBody)) {
|
|
671
|
+
violations.push('decision_delta says cognition changed nothing');
|
|
672
|
+
}
|
|
673
|
+
return { ok: violations.length === 0, violations, body: contractBody.trim() };
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function normalizeInlineDirectiveBody(rawBody) {
|
|
677
|
+
if (!rawBody) return '';
|
|
678
|
+
const segments = String(rawBody)
|
|
679
|
+
.split(/;|\s+(?=[a-z_][a-z0-9_-]*\s*[=:])/i)
|
|
680
|
+
.map((segment) => segment.trim())
|
|
681
|
+
.filter(Boolean);
|
|
682
|
+
const lines = [];
|
|
683
|
+
for (const segment of segments) {
|
|
684
|
+
const match = segment.match(/^([a-z_][a-z0-9_-]*)\s*[=:]\s*(.+)$/i);
|
|
685
|
+
if (match) {
|
|
686
|
+
lines.push(`${match[1]}: ${match[2].trim()}`);
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
lines.push(segment);
|
|
690
|
+
}
|
|
691
|
+
return lines.join('\n').trim();
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function extractInlineDirectiveBody(cmd, directiveRx) {
|
|
695
|
+
if (!cmd || !(directiveRx instanceof RegExp)) return '';
|
|
696
|
+
directiveRx.lastIndex = 0;
|
|
697
|
+
const lines = [];
|
|
698
|
+
let match;
|
|
699
|
+
while ((match = directiveRx.exec(cmd)) !== null) {
|
|
700
|
+
const normalized = normalizeInlineDirectiveBody(match[1]);
|
|
701
|
+
if (normalized) lines.push(normalized);
|
|
702
|
+
}
|
|
703
|
+
return lines.join('\n').trim();
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function scoreVerifyFields(body, fields) {
|
|
707
|
+
if (!body) return 0;
|
|
708
|
+
return fields.reduce((count, { rx }) => count + (rx.test(body) ? 1 : 0), 0);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function extractCurrentTurnAssistantText(event, toolInput) {
|
|
712
|
+
const candidates = [
|
|
713
|
+
event.assistant_text,
|
|
714
|
+
event.assistantText,
|
|
715
|
+
event.draft,
|
|
716
|
+
event.text,
|
|
717
|
+
event.current_text,
|
|
718
|
+
event.currentText,
|
|
719
|
+
event.message,
|
|
720
|
+
event.prompt,
|
|
721
|
+
toolInput?.assistant_text,
|
|
722
|
+
toolInput?.assistantText,
|
|
723
|
+
toolInput?.text,
|
|
724
|
+
];
|
|
725
|
+
const parts = [];
|
|
726
|
+
const seen = new Set();
|
|
727
|
+
for (const candidate of candidates) {
|
|
728
|
+
if (typeof candidate !== 'string') continue;
|
|
729
|
+
const text = candidate.trim();
|
|
730
|
+
if (!text || seen.has(text)) continue;
|
|
731
|
+
seen.add(text);
|
|
732
|
+
parts.push(text);
|
|
733
|
+
}
|
|
734
|
+
return parts.join('\n\n').trim();
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function extractTextForUserCorrection(content) {
|
|
738
|
+
if (Array.isArray(content)) {
|
|
739
|
+
return content.filter((block) => block && block.type === 'text').map((block) => block.text || '').filter(Boolean).join('\n');
|
|
740
|
+
}
|
|
741
|
+
return typeof content === 'string' ? content : '';
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function isRuntimeInjectedUserText(text) {
|
|
745
|
+
return /^\s*\[tool_result\b/i.test(text) ||
|
|
746
|
+
/(?:PreToolUse:[A-Za-z]+ hook blocking error|Stop hook feedback:|Aria pre-tool gate:|Aria stop-gate:|<system-reminder>|<task-notification>|task-notification)/i.test(text);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function collectRecentRealUserText(event, transcriptPath, limit = 6) {
|
|
750
|
+
const texts = [];
|
|
751
|
+
const seen = new Set();
|
|
752
|
+
const pushText = (text) => {
|
|
753
|
+
const normalized = String(text || '').trim();
|
|
754
|
+
if (!normalized || seen.has(normalized) || isRuntimeInjectedUserText(normalized)) return;
|
|
755
|
+
seen.add(normalized);
|
|
756
|
+
texts.push(normalized);
|
|
757
|
+
};
|
|
758
|
+
const messages = Array.isArray(event?.messages) ? event.messages : [];
|
|
759
|
+
for (let i = messages.length - 1; i >= 0 && texts.length < limit; i--) {
|
|
760
|
+
const role = messages[i]?.message?.role ?? messages[i]?.role ?? messages[i]?.type;
|
|
761
|
+
if (role !== 'user') continue;
|
|
762
|
+
const content = messages[i]?.message?.content ?? messages[i]?.content ?? [];
|
|
763
|
+
if (Array.isArray(content) && content.length > 0 && content.every((block) => block && block.type === 'tool_result')) continue;
|
|
764
|
+
pushText(extractTextForUserCorrection(content));
|
|
765
|
+
}
|
|
766
|
+
if (transcriptPath && existsSync(transcriptPath) && texts.length < limit) {
|
|
767
|
+
try {
|
|
768
|
+
const lines = readFileSync(transcriptPath, 'utf-8').split('\n').filter(Boolean);
|
|
769
|
+
for (let i = lines.length - 1; i >= 0 && texts.length < limit; i--) {
|
|
770
|
+
let entry;
|
|
771
|
+
try { entry = JSON.parse(lines[i]); } catch { continue; }
|
|
772
|
+
const role = entry?.message?.role ?? entry?.role ?? entry?.type;
|
|
773
|
+
if (role !== 'user') continue;
|
|
774
|
+
const content = entry?.message?.content ?? entry?.content ?? [];
|
|
775
|
+
if (Array.isArray(content) && content.length > 0 && content.every((block) => block && block.type === 'tool_result')) continue;
|
|
776
|
+
pushText(extractTextForUserCorrection(content));
|
|
777
|
+
}
|
|
778
|
+
} catch {}
|
|
779
|
+
}
|
|
780
|
+
return texts.join('\n\n');
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function userCorrectionBlocksCommand(userText, command) {
|
|
784
|
+
const text = String(userText || '');
|
|
785
|
+
const cmd = String(command || '');
|
|
786
|
+
if (!text || !cmd) return null;
|
|
787
|
+
const k8sMutation = /\bkubectl\s+(?:apply|delete|set\s+image|patch|replace|create|scale|rollout\s+(?:restart|undo))\b|scripts\/deploy-|\bdocker\s+push\b/i.test(cmd);
|
|
788
|
+
const explicitStop = /\b(?:stop|do\s+not|don't|quit|cease)\b[\s\S]{0,160}\b(?:deploy|redeploy|restart|rollout|kubectl|command|pods?)\b/i.test(text);
|
|
789
|
+
if (explicitStop && k8sMutation) return 'recent user explicitly told the agent to stop deploy/restart command attempts';
|
|
790
|
+
const macTargetInCommand = /\bmlx-mac\b|\bdeployment\/mlx-mac|\bdeploy(?:ment)?\s+mlx-mac/i.test(cmd);
|
|
791
|
+
const macContradiction = /\b(?:mac\s+lanes?|mac\s+pods?|mlx-mac)\b[\s\S]{0,140}\b(?:not\s+pods?|no\s+such\s+thing|non[-\s]?existent|do(?:es)?\s+not\s+exist|don't\s+exist)\b|\b(?:no\s+such\s+thing|non[-\s]?existent|do(?:es)?\s+not\s+exist|don't\s+exist)\b[\s\S]{0,140}\b(?:mac\s+lanes?|mac\s+pods?|mlx-mac|pods?)\b/i.test(text);
|
|
792
|
+
if (macTargetInCommand && macContradiction) return 'recent user contradicted the mlx-mac Kubernetes-pod/deployment assumption';
|
|
793
|
+
return null;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// Substance-checking lens detection (added 2026-04-26 per Hamza's
|
|
797
|
+
// gate-improvement doctrine: form-only emission must not satisfy
|
|
798
|
+
// the gate). For each lens, look for `<lens>: <content>` and verify
|
|
799
|
+
// content is ≥SUBSTANCE_MIN_CHARS of non-placeholder text. Bare
|
|
800
|
+
// `nur` mentions in prose, or `nur: ok`, no longer count — must be
|
|
801
|
+
// substantive thought.
|
|
802
|
+
function detectCognitionLenses(text) {
|
|
803
|
+
if (!text) return { count: 0, names: [], blockBody: '' };
|
|
804
|
+
const block = text.match(COGNITION_BLOCK_RX);
|
|
805
|
+
const searchSpace = block ? block[1] : text;
|
|
806
|
+
const blockBody = block ? block[1] : '';
|
|
807
|
+
const names = [];
|
|
808
|
+
for (const lens of LENS_NAMES) {
|
|
809
|
+
// Match `lens: <content>` where content extends until the next
|
|
810
|
+
// lens label, the closing </cognition> tag, or a blank-line break.
|
|
811
|
+
const lensRx = new RegExp(
|
|
812
|
+
`\\b${lens}\\s*:\\s*([^\\n]*(?:\\n(?!\\s*(?:${LENS_NAMES.join('|')})\\s*:|<\\/cognition>)[^\\n]*)*)`,
|
|
813
|
+
'i',
|
|
814
|
+
);
|
|
815
|
+
const m = searchSpace.match(lensRx);
|
|
816
|
+
if (!m) continue;
|
|
817
|
+
const content = (m[1] || '').trim();
|
|
818
|
+
if (content.length < SUBSTANCE_MIN_CHARS) continue;
|
|
819
|
+
if (PLACEHOLDER_RX.test(content)) continue;
|
|
820
|
+
names.push(lens);
|
|
821
|
+
}
|
|
822
|
+
// Substrate-citation check: at least ONE lens must cite Aria substrate
|
|
823
|
+
// explicitly (doctrine memory filename, harness packet rule, fitrah axiom,
|
|
824
|
+
// *_rule entry, garden state reference, prior decision id, distilled
|
|
825
|
+
// principle id). Catches "lens-as-ceremony" failures where 4+ lenses are
|
|
826
|
+
// substantive in length but contain no actual substrate grounding.
|
|
827
|
+
// Hamza 2026-04-26: "anything else we can add to make sure u as claude
|
|
828
|
+
// for me and my client obey the harness and benefot from kt".
|
|
829
|
+
const SUBSTRATE_CITE_RX =
|
|
830
|
+
/feedback_[a-z0-9_]+\.md|project_[a-z0-9_]+\.md|fitrah[_:\s]|garden[_:\s]|distilled_principle|[a-z]+_rule\b|harness packet|substrate cite|\bIJTIHAD\b|\bQIYAS\b|\bTADABBUR\b|\bILHAM\b|aria 7b|EIGHT_LENS_DOCTRINE|COMPACT_CONTINUITY|ARIA_DEPLOY_PROCEDURE/i;
|
|
831
|
+
const hasSubstrateCite = SUBSTRATE_CITE_RX.test(blockBody) ||
|
|
832
|
+
SUBSTRATE_CITE_RX.test(searchSpace);
|
|
833
|
+
|
|
834
|
+
// Discovery-binding check (structural fix #3 — Hamza 2026-04-27 "how do
|
|
835
|
+
// we prevent this"). If the cognition surfaces a defect/discovery
|
|
836
|
+
// (found/noticed/discovered + bug/broken/issue) the same cognition must
|
|
837
|
+
// carry a `discoveries:` clause stating how each is resolved (fix-now,
|
|
838
|
+
// task ID, or explicit user-decision-required). Without this, the
|
|
839
|
+
// cognition can describe a problem without binding any action — the
|
|
840
|
+
// exact flag-and-move pattern feedback_no_flag_without_fix.md prohibits.
|
|
841
|
+
const COG_DISCOVERY_RX = /(?:\b(?:found|noticed|discovered|spotted)[^.\n]{0,140}(?:bug|issue|defect|broken|buggy|wrong|crash|fail|missing|stale|outdated|leak|vulnerability)|\b(?:latent|silent|hidden)\s+(?:bug|defect|issue|fail|crash|leak)|\bdoctrine\s+violation\b)/i;
|
|
842
|
+
const hasDiscovery = COG_DISCOVERY_RX.test(blockBody);
|
|
843
|
+
// Resolution clause must be present in the same blockBody if a discovery
|
|
844
|
+
// is mentioned. Acceptable forms:
|
|
845
|
+
// - `discoveries:` field listing items + how-resolved
|
|
846
|
+
// - `addressing:` / `fixing:` clause naming what's being patched
|
|
847
|
+
// - explicit task ID reference (TaskCreate / linear / tracked-as)
|
|
848
|
+
const COG_RESOLUTION_RX = /(?:^\s*discoveries?\s*:\s*\S|^\s*addressing\s*:\s*\S|^\s*fixing\s*:\s*\S|TaskCreate|tracked\s+as\s+#?\d+|linear[- ]?(?:issue|task)|fix(?:ing|ed)\s+(?:in|now|inline|in-flight)|same[- ]turn\s+fix)/im;
|
|
849
|
+
const hasDiscoveryResolution = COG_RESOLUTION_RX.test(blockBody);
|
|
850
|
+
const discoveryUnresolved = hasDiscovery && !hasDiscoveryResolution;
|
|
851
|
+
|
|
852
|
+
return { count: names.length, names, blockBody, hasSubstrateCite, hasDiscovery, hasDiscoveryResolution, discoveryUnresolved };
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// Backwards-compat shim — count-only path used by older callers.
|
|
856
|
+
function countCognitionLenses(text) {
|
|
857
|
+
return detectCognitionLenses(text).count;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// ── arch_facts gate ──────────────────────────────────────────────────────────
|
|
861
|
+
//
|
|
862
|
+
// Inspects actual diff content (tool_input.content / file_path) for
|
|
863
|
+
// architectural violations defined as arch_facts rules in mizan.yaml.
|
|
864
|
+
// Called AFTER cognition + discovery-binding checks pass, for Edit/Write/
|
|
865
|
+
// NotebookEdit only (Bash skipped — arch violations live in code, not commands).
|
|
866
|
+
//
|
|
867
|
+
// Tier-aware: if ~/.aria/license.json exists (client surface), arch_facts
|
|
868
|
+
// check is skipped — only owner gets server-side mizan enforcement here.
|
|
869
|
+
//
|
|
870
|
+
// Fail-open: if aria-telemetry is unreachable, the tool call is allowed.
|
|
871
|
+
// The 3s AbortController is a DETECTION PROBE (is telemetry alive?) not a
|
|
872
|
+
// hard deadline — if mizan is slow but alive the probe will complete; if it
|
|
873
|
+
// is down the catch path fail-opens without blocking the developer.
|
|
874
|
+
function isClientSurface() {
|
|
875
|
+
try {
|
|
876
|
+
return existsSync(`${HOME}/.aria/license.json`);
|
|
877
|
+
} catch {
|
|
878
|
+
return false;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
async function archFactsGate(toolInput) {
|
|
883
|
+
// Skip on client surfaces — arch_facts enforcement is owner-only.
|
|
884
|
+
if (isClientSurface()) return { ok: true, allow: true, note: 'client-surface-skip' };
|
|
885
|
+
try {
|
|
886
|
+
const ariaTelemetry = process.env.ARIA_TELEMETRY_BASE || 'http://aria-telemetry.aria.svc.cluster.local:8088';
|
|
887
|
+
const ctl = new AbortController();
|
|
888
|
+
const probeTimer = setTimeout(() => ctl.abort(), 3000);
|
|
889
|
+
let resp;
|
|
890
|
+
try {
|
|
891
|
+
resp = await fetch(`${ariaTelemetry}/v1/mizan/check`, {
|
|
892
|
+
method: 'POST',
|
|
893
|
+
headers: { 'Content-Type': 'application/json' },
|
|
894
|
+
body: JSON.stringify({ draft: String(toolInput).slice(0, 50000), source: 'pre-tool-gate-arch-facts' }),
|
|
895
|
+
signal: ctl.signal,
|
|
896
|
+
});
|
|
897
|
+
} finally {
|
|
898
|
+
clearTimeout(probeTimer);
|
|
899
|
+
}
|
|
900
|
+
if (!resp.ok) return { ok: true, allow: true, note: 'mizan unreachable, fail-open' };
|
|
901
|
+
const result = await resp.json();
|
|
902
|
+
if (result?.hard_block === true && Array.isArray(result?.violations)) {
|
|
903
|
+
return { ok: false, allow: false, violations: result.violations };
|
|
904
|
+
}
|
|
905
|
+
return { ok: true, allow: true };
|
|
906
|
+
} catch {
|
|
907
|
+
return { ok: true, allow: true, note: 'mizan probe error, fail-open' };
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// Hive cognition-logging v1.2 — fire-and-forget HTTP push to
|
|
912
|
+
// /api/cognition/log so every gate decision joins the corpus.
|
|
913
|
+
// Failures are silent: the local audit log is the durable record;
|
|
914
|
+
// the network push is additive signal for compounding evolution.
|
|
915
|
+
//
|
|
916
|
+
// Per no-timeouts doctrine (feedback_no_timeouts_doctrine.md): no
|
|
917
|
+
// AbortController deadline. Pile-up protection is structural —
|
|
918
|
+
// in-flight counter caps concurrent pushes. Real network errors
|
|
919
|
+
// drive the catch path; slow responses complete naturally.
|
|
920
|
+
const HARNESS_URL =
|
|
921
|
+
process.env.ARIA_HIVE_RUNTIME_URL ||
|
|
922
|
+
process.env.ARIA_HARNESS_BASE_URL ||
|
|
923
|
+
process.env.ARIA_HARNESS_URL ||
|
|
924
|
+
'https://harness.ariasos.com';
|
|
925
|
+
const RUNTIME_BASE_URL =
|
|
926
|
+
process.env.ARIA_RUNTIME_URL ||
|
|
927
|
+
'http://127.0.0.1:4319';
|
|
928
|
+
const HARNESS_TOKEN = process.env.ARIA_HARNESS_TOKEN || '';
|
|
929
|
+
const LOG_PUSH_DISABLED = process.env.ARIA_COGNITION_PUSH === 'off';
|
|
930
|
+
const MAX_IN_FLIGHT = 16;
|
|
931
|
+
let inFlight = 0;
|
|
932
|
+
|
|
933
|
+
function pushCognitionEvent(payload) {
|
|
934
|
+
if (LOG_PUSH_DISABLED) return;
|
|
935
|
+
if (inFlight >= MAX_IN_FLIGHT) return; // structural backpressure, not a deadline
|
|
936
|
+
try {
|
|
937
|
+
const body = JSON.stringify(payload);
|
|
938
|
+
const url = `${HARNESS_URL}/api/cognition/log`;
|
|
939
|
+
inFlight++;
|
|
940
|
+
fetch(url, {
|
|
941
|
+
method: 'POST',
|
|
942
|
+
headers: {
|
|
943
|
+
'Content-Type': 'application/json',
|
|
944
|
+
...(HARNESS_TOKEN ? { Authorization: `Bearer ${HARNESS_TOKEN}` } : {}),
|
|
945
|
+
},
|
|
946
|
+
body,
|
|
947
|
+
})
|
|
948
|
+
.catch(() => {/* real transport error — silent, audit log is durable */})
|
|
949
|
+
.finally(() => { inFlight--; });
|
|
950
|
+
} catch {
|
|
951
|
+
// pre-fetch errors (malformed JSON, env issue) — silent
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// ── Block-pattern recipe lookup (Task #86) ──────────────────────────────────
|
|
956
|
+
// Before emitting a binding-violation block, ask aria-soul if this exact
|
|
957
|
+
// shape has been seen before and a high-confidence corrective recipe exists.
|
|
958
|
+
// If so, surface the recipe in the block message so the gated process gets
|
|
959
|
+
// the fix the hive already learned, not just the block.
|
|
960
|
+
//
|
|
961
|
+
// Per feedback_no_timeouts_doctrine.md the lookup uses a DETECTION PROBE
|
|
962
|
+
// (3s AbortController) — that's "is the lookup endpoint alive at all?", not
|
|
963
|
+
// a hard deadline on the underlying lookup. If the probe fires the gate
|
|
964
|
+
// continues with the block-only message: a missed lookup must NEVER convert
|
|
965
|
+
// a real block into an allow.
|
|
966
|
+
//
|
|
967
|
+
// Recipe is surfaced only when confidence > RECIPE_SURFACE_CONFIDENCE (0.7
|
|
968
|
+
// per migration 208 RECIPE_PROMOTE_THRESHOLD), matching the same threshold
|
|
969
|
+
// the cron uses to promote a recipe to the pattern's hot pointer. Below
|
|
970
|
+
// that, we still cite the pattern's existence ("similar block has been
|
|
971
|
+
// seen N times") without claiming a fix.
|
|
972
|
+
const ARIA_SOUL_URL = process.env.ARIA_SOUL_URL || HARNESS_URL;
|
|
973
|
+
const RECIPE_SURFACE_CONFIDENCE = 0.7;
|
|
974
|
+
const BLOCK_PATTERN_PROBE_MS = 3000;
|
|
975
|
+
|
|
976
|
+
async function lookupBlockPatternRecipe({ detectorClass, signature, tenantId }) {
|
|
977
|
+
if (!detectorClass || !signature) return null;
|
|
978
|
+
const url = new URL(`${ARIA_SOUL_URL}/api/hive/block-pattern`);
|
|
979
|
+
url.searchParams.set('action', 'lookup');
|
|
980
|
+
url.searchParams.set('detector_class', detectorClass);
|
|
981
|
+
url.searchParams.set('pattern_signature', signature.slice(0, 512));
|
|
982
|
+
if (tenantId) url.searchParams.set('tenant_id', tenantId);
|
|
983
|
+
|
|
984
|
+
const ctl = new AbortController();
|
|
985
|
+
const probeTimer = setTimeout(() => ctl.abort(), BLOCK_PATTERN_PROBE_MS);
|
|
986
|
+
try {
|
|
987
|
+
const resp = await fetch(url.toString(), {
|
|
988
|
+
method: 'GET',
|
|
989
|
+
headers: HARNESS_TOKEN ? { Authorization: `Bearer ${HARNESS_TOKEN}` } : {},
|
|
990
|
+
signal: ctl.signal,
|
|
991
|
+
});
|
|
992
|
+
if (!resp.ok) return null;
|
|
993
|
+
const body = await resp.json();
|
|
994
|
+
if (!body || body.found !== true) return null;
|
|
995
|
+
return body;
|
|
996
|
+
} catch {
|
|
997
|
+
return null; // probe failure — block proceeds without recipe surfacing
|
|
998
|
+
} finally {
|
|
999
|
+
clearTimeout(probeTimer);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// Renders the lookup result as an inline addendum to a block message. Returns
|
|
1004
|
+
// the empty string when there's nothing useful to surface — the caller can
|
|
1005
|
+
// concatenate unconditionally without producing dangling whitespace.
|
|
1006
|
+
function renderRecipeAddendum(lookupResult) {
|
|
1007
|
+
if (!lookupResult) return '';
|
|
1008
|
+
const recipe = lookupResult.recipe;
|
|
1009
|
+
const freq = Number(lookupResult.frequency || 0);
|
|
1010
|
+
const successRate = lookupResult.success_rate;
|
|
1011
|
+
|
|
1012
|
+
// Recipe present + above promotion threshold → surface the fix verbatim.
|
|
1013
|
+
if (recipe && typeof recipe === 'object' && Number(recipe.confidence ?? 0) >= RECIPE_SURFACE_CONFIDENCE) {
|
|
1014
|
+
const text = typeof recipe.recipe_text === 'string' ? recipe.recipe_text.slice(0, 800) : '';
|
|
1015
|
+
const actions = Array.isArray(recipe.recipe_actions) ? recipe.recipe_actions : [];
|
|
1016
|
+
const actionsLine = actions.length
|
|
1017
|
+
? `\n Actions: ${JSON.stringify(actions).slice(0, 600)}`
|
|
1018
|
+
: '';
|
|
1019
|
+
const conf = Number(recipe.confidence).toFixed(2);
|
|
1020
|
+
const seenLine = freq > 0 ? ` (pattern seen ${freq}× across the hive)` : '';
|
|
1021
|
+
return `\n\n📚 HIVE RECIPE${seenLine}:
|
|
1022
|
+
${text}${actionsLine}
|
|
1023
|
+
Confidence: ${conf}. Apply this BEFORE the block escalates — the hive learned this fix from prior firings.`;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
// No promoted recipe — but the pattern is recurring. Cite it so the caller
|
|
1027
|
+
// knows this isn't novel; the recipe is still being learned.
|
|
1028
|
+
if (freq >= 3) {
|
|
1029
|
+
const rateLine = (typeof successRate === 'number')
|
|
1030
|
+
? ` Past resolution rate: ${(successRate * 100).toFixed(0)}%.`
|
|
1031
|
+
: '';
|
|
1032
|
+
return `\n\n📓 Hive note: this block-shape has fired ${freq} time(s); recipe is still being learned (no high-confidence fix yet).${rateLine}`;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
return '';
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// Read event JSON from stdin (Claude Code spec).
|
|
1039
|
+
let input = '';
|
|
1040
|
+
for await (const chunk of process.stdin) input += chunk;
|
|
1041
|
+
|
|
1042
|
+
let event;
|
|
1043
|
+
try {
|
|
1044
|
+
event = JSON.parse(input);
|
|
1045
|
+
} catch {
|
|
1046
|
+
audit('allow-parse-error', 'stdin not JSON');
|
|
1047
|
+
process.exit(0); // fail-open on malformed input
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
const toolName = event.tool_name ?? event.toolName ?? '';
|
|
1051
|
+
const toolInput = event.tool_input ?? event.toolInput ?? {};
|
|
1052
|
+
const transcriptPath = event.transcript_path ?? event.transcriptPath;
|
|
1053
|
+
|
|
1054
|
+
// Gate every action tool — every tool that mutates state must go through
|
|
1055
|
+
// cognition challenge. Hamza 2026-04-26: "regardless if u arent even
|
|
1056
|
+
// compliant with it how do we expect workers to be? i truly nees to
|
|
1057
|
+
// ship this and ur non compliance is blocking." Per
|
|
1058
|
+
// feedback_full_harness_binding_must_be_structural.md the dispatcher
|
|
1059
|
+
// (the orchestrator wielding the tool) owns compliance, not the worker
|
|
1060
|
+
// that receives a dispatched task. Read/Glob/Grep observe state without
|
|
1061
|
+
// changing it, so they remain ungated.
|
|
1062
|
+
const ACTION_TOOLS = new Set(['Bash', 'Edit', 'Write', 'NotebookEdit']);
|
|
1063
|
+
if (!ACTION_TOOLS.has(toolName)) {
|
|
1064
|
+
process.exit(0);
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
const cmd = toolName === 'Bash' ? String(toolInput.command ?? '') : '';
|
|
1068
|
+
const filePath = toolName !== 'Bash'
|
|
1069
|
+
? String(toolInput.file_path ?? toolInput.notebook_path ?? '')
|
|
1070
|
+
: '';
|
|
1071
|
+
const cmdPreview = toolName === 'Bash'
|
|
1072
|
+
? cmd.slice(0, 80).replace(/\s+/g, ' ')
|
|
1073
|
+
: `${toolName} ${filePath || '(no path)'}`.slice(0, 80);
|
|
1074
|
+
|
|
1075
|
+
if (toolName === 'Bash') {
|
|
1076
|
+
const recentUserText = collectRecentRealUserText(event, transcriptPath);
|
|
1077
|
+
const correctionBlockReason = userCorrectionBlocksCommand(recentUserText, cmd);
|
|
1078
|
+
if (correctionBlockReason) {
|
|
1079
|
+
const reason = `Aria pre-tool gate: USER-CORRECTION hard-block.
|
|
1080
|
+
|
|
1081
|
+
${correctionBlockReason}.
|
|
1082
|
+
|
|
1083
|
+
The next assistant response must stop the command loop, quote the user's correction in one sentence, and re-evaluate the target from substrate before any further mutation. Do not retry this Bash command shape.`;
|
|
1084
|
+
audit('block-user-correction-override', cmdPreview);
|
|
1085
|
+
emitBlock(reason, { source: 'pre-tool/user-correction' });
|
|
1086
|
+
process.exit(2);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
if (toolName === 'Bash' && isOperationalMaintenanceCommand(cmd)) {
|
|
1091
|
+
audit('allow-operational-maintenance kubectl-rollout-restart', cmdPreview);
|
|
1092
|
+
process.exit(0);
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// V3: per-command bypass removed entirely. The only escape valves are:
|
|
1096
|
+
// (1) Trivial-bash whitelist — short read-only commands pass without cognition
|
|
1097
|
+
// (2) ~/.claude/settings.json hook removal — visible user action Hamza controls
|
|
1098
|
+
// No env-var kill-switch (removed 2026-04-27). Compliance is the only path
|
|
1099
|
+
// for non-trivial work from the gated process's perspective.
|
|
1100
|
+
|
|
1101
|
+
// V2 primary path — inline command cognition. If the command carries
|
|
1102
|
+
// 4+ substantive lenses inline, gate passes immediately (still need
|
|
1103
|
+
// verify block from transcript for destructive ops, see below).
|
|
1104
|
+
const inlineCog = detectInlineCognition(cmd);
|
|
1105
|
+
if (inlineCog.count >= REQUIRED_LENSES) {
|
|
1106
|
+
// For destructive commands, still require a verify block in the
|
|
1107
|
+
// recent transcript (verify is fundamentally about pre-action
|
|
1108
|
+
// verification of state, not about the command's own thought).
|
|
1109
|
+
// We'll do that check below alongside the existing flow but skip
|
|
1110
|
+
// the cognition-from-transcript check entirely since we have it
|
|
1111
|
+
// inline.
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// Destructive-pattern + trivial-read + short-command shortcuts apply
|
|
1115
|
+
// only to Bash. Edit / Write / NotebookEdit always require cognition —
|
|
1116
|
+
// they have no triviality concept (you don't "trivially" mutate a file)
|
|
1117
|
+
// and no command-text to inspect for destructive verbs (the destructive
|
|
1118
|
+
// shape is "this Edit might overwrite something important," which is
|
|
1119
|
+
// exactly what cognition is for).
|
|
1120
|
+
const matched = toolName === 'Bash'
|
|
1121
|
+
? DESTRUCTIVE_PATTERNS.find(({ rx }) => rx.test(cmd))
|
|
1122
|
+
: null;
|
|
1123
|
+
// Deploy-specific match — separate from destructive because it carries
|
|
1124
|
+
// stricter substrate-anchor requirements per doctrine #104.
|
|
1125
|
+
const deployMatched = toolName === 'Bash'
|
|
1126
|
+
? DEPLOY_PATTERNS.find(({ rx }) => rx.test(cmd))
|
|
1127
|
+
: null;
|
|
1128
|
+
const isTrivialRead = toolName === 'Bash' && TRIVIAL_BASH_RX.test(cmd) && cmd.length < 200;
|
|
1129
|
+
const isShort = toolName === 'Bash' && cmd.length < SHORT_BASH_LIMIT;
|
|
1130
|
+
|
|
1131
|
+
if (!matched && !deployMatched && (isTrivialRead || isShort)) {
|
|
1132
|
+
// Not destructive AND not a deploy AND trivial — allow without
|
|
1133
|
+
// further checks. Only reachable for Bash because all three flags are
|
|
1134
|
+
// forced false otherwise.
|
|
1135
|
+
process.exit(0);
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
// Read recent assistant turns for both verify + cognition blocks.
|
|
1139
|
+
//
|
|
1140
|
+
// Doctrine being implemented: cognition is *turn-scoped*, not
|
|
1141
|
+
// message-scoped. A model that emits a <cognition> block once at the
|
|
1142
|
+
// top of a turn and then executes 20 tool_use round-trips has done
|
|
1143
|
+
// its 8-lens application — gating each individual message would be
|
|
1144
|
+
// performative, not enforcement.
|
|
1145
|
+
//
|
|
1146
|
+
// Algorithm: walk back from the most recent transcript entry,
|
|
1147
|
+
// accumulating assistant text. Stop after crossing exactly ONE user
|
|
1148
|
+
// message — that captures "everything the model said this turn" plus
|
|
1149
|
+
// the immediately-prior assistant turn's tail (so cognition that
|
|
1150
|
+
// carried over a turn boundary still counts). Hard cap at 30
|
|
1151
|
+
// messages to bound work on huge transcripts.
|
|
1152
|
+
//
|
|
1153
|
+
// Compact robustness: Claude Code rewrites the transcript with a
|
|
1154
|
+
// summary stub as the most recent assistant entry. We recognize and
|
|
1155
|
+
// skip those by content heuristic so they don't poison the search.
|
|
1156
|
+
const COMPACT_SUMMARY_RX = /(this session is being continued|conversation that ran out of context|primary request and intent|all user messages)/i;
|
|
1157
|
+
// System-reminder messages are stored with role=user but are runtime-
|
|
1158
|
+
// injected (PreToolUse blocks for any tool, Stop blocks, task-
|
|
1159
|
+
// notifications, gentle reminders, harness packet preview chunks).
|
|
1160
|
+
// Counting them as turn boundaries evaporated the cognition lookback
|
|
1161
|
+
// in client-tonight sessions where the harness packet preview alone
|
|
1162
|
+
// is 36k chars per turn — old `length < 4000` skip threshold trapped
|
|
1163
|
+
// every reminder as a boundary. Now: PERCENTAGE-OF-CONTENT skip via
|
|
1164
|
+
// total reminder-span coverage as a fraction of total content length.
|
|
1165
|
+
// ≥60% reminder coverage → skip as runtime-injected.
|
|
1166
|
+
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;
|
|
1167
|
+
const SYSTEM_REMINDER_THRESHOLD = 0.6;
|
|
1168
|
+
const HARD_LOOKBACK_CAP = 50;
|
|
1169
|
+
// Bumped from 2 → 5 (2026-04-26): client-tonight session has many
|
|
1170
|
+
// system-reminder injections per turn (block errors, task-list
|
|
1171
|
+
// reminders, harness packet preview). Tight boundary count plus
|
|
1172
|
+
// the broken length-based reminder skip evaporated cognition lookback
|
|
1173
|
+
// even when cognition was emitted in the prior assistant text. Per
|
|
1174
|
+
// Hamza directive "fix the gate, don't route around it" — widen the
|
|
1175
|
+
// window since the substance check defends quality regardless.
|
|
1176
|
+
const USER_BOUNDARIES_TO_CROSS = 5;
|
|
1177
|
+
|
|
1178
|
+
const recentAssistantTexts = [];
|
|
1179
|
+
const recentAssistantTextSet = new Set();
|
|
1180
|
+
function appendAssistantTexts(texts) {
|
|
1181
|
+
for (const text of texts) {
|
|
1182
|
+
if (!text || recentAssistantTextSet.has(text)) continue;
|
|
1183
|
+
recentAssistantTexts.push(text);
|
|
1184
|
+
recentAssistantTextSet.add(text);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
const eventMessages = Array.isArray(event.messages) ? event.messages : [];
|
|
1189
|
+
const eventAssistantTexts = collectRecentAssistantTextsFromMessages(eventMessages, {
|
|
1190
|
+
compactSummaryRx: COMPACT_SUMMARY_RX,
|
|
1191
|
+
hardLookbackCap: HARD_LOOKBACK_CAP,
|
|
1192
|
+
systemReminderRx: SYSTEM_REMINDER_RX,
|
|
1193
|
+
systemReminderThreshold: SYSTEM_REMINDER_THRESHOLD,
|
|
1194
|
+
userBoundariesToCross: USER_BOUNDARIES_TO_CROSS,
|
|
1195
|
+
});
|
|
1196
|
+
appendAssistantTexts(eventAssistantTexts);
|
|
1197
|
+
|
|
1198
|
+
const transcriptAssistantTexts = [];
|
|
1199
|
+
if (transcriptPath && existsSync(transcriptPath)) {
|
|
1200
|
+
try {
|
|
1201
|
+
const lines = readFileSync(transcriptPath, 'utf-8').split('\n').filter(Boolean);
|
|
1202
|
+
let userBoundariesCrossed = 0;
|
|
1203
|
+
let scanned = 0;
|
|
1204
|
+
for (let i = lines.length - 1; i >= 0 && scanned < HARD_LOOKBACK_CAP; i--) {
|
|
1205
|
+
try {
|
|
1206
|
+
const m = JSON.parse(lines[i]);
|
|
1207
|
+
const role = normalizeRole(m);
|
|
1208
|
+
const content = normalizeContent(m);
|
|
1209
|
+
if (role === 'user') {
|
|
1210
|
+
// Skip messages that aren't real user input:
|
|
1211
|
+
// (a) tool_result blocks (runtime feeding back tool output)
|
|
1212
|
+
// (b) system-reminder injections (PreToolUse blocks,
|
|
1213
|
+
// task-notifications, gentle reminders) — runtime-
|
|
1214
|
+
// authored, not user voice. Counting them eats the
|
|
1215
|
+
// cognition lookback in tool-heavy or block-heavy turns.
|
|
1216
|
+
if (isToolResultOnlyContent(content)) continue;
|
|
1217
|
+
// Inspect text content for system-reminder patterns.
|
|
1218
|
+
const textContent = extractTextFromContent(content);
|
|
1219
|
+
if (isMostlySystemReminder(textContent, SYSTEM_REMINDER_RX, SYSTEM_REMINDER_THRESHOLD)) continue;
|
|
1220
|
+
userBoundariesCrossed++;
|
|
1221
|
+
if (userBoundariesCrossed > USER_BOUNDARIES_TO_CROSS) break;
|
|
1222
|
+
continue;
|
|
1223
|
+
}
|
|
1224
|
+
if (role !== 'assistant') continue;
|
|
1225
|
+
scanned++;
|
|
1226
|
+
const text = extractTextFromContent(content);
|
|
1227
|
+
if (!text) continue;
|
|
1228
|
+
// Skip compact-summary stubs — they live where assistant turns
|
|
1229
|
+
// used to be but are system-authored, not the model's voice.
|
|
1230
|
+
if (COMPACT_SUMMARY_RX.test(text) && text.length > 4000) continue;
|
|
1231
|
+
transcriptAssistantTexts.push(text);
|
|
1232
|
+
} catch {}
|
|
1233
|
+
}
|
|
1234
|
+
} catch {}
|
|
1235
|
+
}
|
|
1236
|
+
appendAssistantTexts(transcriptAssistantTexts);
|
|
1237
|
+
const currentTurnAssistantText = extractCurrentTurnAssistantText(event, toolInput);
|
|
1238
|
+
if (currentTurnAssistantText) appendAssistantTexts([currentTurnAssistantText]);
|
|
1239
|
+
|
|
1240
|
+
// Detect cognition / verify across the recent assistant window. Lenses
|
|
1241
|
+
// from any of the last N turns count; the gate is checking whether
|
|
1242
|
+
// cognition has been done recently, not whether it was done in the
|
|
1243
|
+
// literal last message (which can be a tool-only turn or a stub).
|
|
1244
|
+
const unionText = recentAssistantTexts.join('\n\n');
|
|
1245
|
+
const eventCog = detectCognitionLenses(eventAssistantTexts.join('\n\n'));
|
|
1246
|
+
const transcriptCog = detectCognitionLenses(unionText);
|
|
1247
|
+
// Combine inline-command cognition (preferred — action-coupled) with
|
|
1248
|
+
// transcript scan (fallback). Inline takes precedence on lens names;
|
|
1249
|
+
// counts merge for the threshold check.
|
|
1250
|
+
const mergedLensSet = new Set([...inlineCog.names, ...transcriptCog.names]);
|
|
1251
|
+
const lensCount = mergedLensSet.size;
|
|
1252
|
+
const lensNames = [...mergedLensSet];
|
|
1253
|
+
const cogBlockBody = transcriptCog.blockBody;
|
|
1254
|
+
const inlineVerifyBody = extractInlineDirectiveBody(cmd, INLINE_VERIFY_LINE_RX);
|
|
1255
|
+
const verifyBodies = [...unionText.matchAll(/<verify>([\s\S]*?)<\/verify>/gi)]
|
|
1256
|
+
.map((m) => (m[1] || '').trim())
|
|
1257
|
+
.filter(Boolean);
|
|
1258
|
+
if (inlineVerifyBody) verifyBodies.push(inlineVerifyBody);
|
|
1259
|
+
const bestVerifyBody = (() => {
|
|
1260
|
+
if (verifyBodies.length === 0) return '';
|
|
1261
|
+
let bestBody = verifyBodies[0];
|
|
1262
|
+
let bestScore = -1;
|
|
1263
|
+
for (const body of verifyBodies) {
|
|
1264
|
+
const score = scoreVerifyFields(body, VERIFY_REQUIRED_FIELDS);
|
|
1265
|
+
if (score > bestScore) {
|
|
1266
|
+
bestScore = score;
|
|
1267
|
+
bestBody = body;
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
return bestBody;
|
|
1271
|
+
})();
|
|
1272
|
+
const hasVerify = scoreVerifyFields(bestVerifyBody, VERIFY_REQUIRED_FIELDS) === VERIFY_REQUIRED_FIELDS.length;
|
|
1273
|
+
const hasCognition = lensCount >= REQUIRED_LENSES;
|
|
1274
|
+
const appliedContract = validateAppliedCognitionContract(unionText, cmd);
|
|
1275
|
+
const cognitionSource = inlineCog.count >= REQUIRED_LENSES
|
|
1276
|
+
? 'inline-command'
|
|
1277
|
+
: eventCog.count >= REQUIRED_LENSES
|
|
1278
|
+
? 'event-messages'
|
|
1279
|
+
: transcriptCog.count >= REQUIRED_LENSES
|
|
1280
|
+
? 'recent-assistant-window'
|
|
1281
|
+
: lensCount >= REQUIRED_LENSES
|
|
1282
|
+
? 'merged-recent-window'
|
|
1283
|
+
: 'merged-or-insufficient';
|
|
1284
|
+
// Substrate-citation visibility (Phase 11 will promote to block-mode once
|
|
1285
|
+
// telemetry shows healthy substrate-cite rate). For now: log the metric so
|
|
1286
|
+
// we can audit substrate-grounded vs ceremonial cognition over time.
|
|
1287
|
+
const hasSubstrateCite = (inlineCog.hasSubstrateCite === true) ||
|
|
1288
|
+
(transcriptCog.hasSubstrateCite === true);
|
|
1289
|
+
|
|
1290
|
+
// Discovery-binding check (structural fix #3) — if cognition surfaces a
|
|
1291
|
+
// defect/discovery, the same cognition must include a resolution clause
|
|
1292
|
+
// (`discoveries:` / `addressing:` / `fixing:` / TaskCreate / tracked-as
|
|
1293
|
+
// reference). Per feedback_no_flag_without_fix.md, discoveries are atomic
|
|
1294
|
+
// with their fixes — flag-and-move is the prohibited pattern. Pre-tool
|
|
1295
|
+
// gate enforces at the cognition surface; stop-gate's discovery-binding
|
|
1296
|
+
// ledger enforces at the output surface; both close the structural gap.
|
|
1297
|
+
const discoveryUnresolved = (inlineCog.discoveryUnresolved === true) ||
|
|
1298
|
+
(transcriptCog.discoveryUnresolved === true);
|
|
1299
|
+
|
|
1300
|
+
// Best-effort session id for the corpus push. Claude Code passes
|
|
1301
|
+
// session_id in the event payload; fall back to transcript file
|
|
1302
|
+
// basename so events from the same session cluster.
|
|
1303
|
+
const sessionId =
|
|
1304
|
+
event.session_id ??
|
|
1305
|
+
event.sessionId ??
|
|
1306
|
+
(transcriptPath ? transcriptPath.split('/').pop()?.replace(/\.[^.]+$/, '') : null) ??
|
|
1307
|
+
'claude-code-unknown';
|
|
1308
|
+
|
|
1309
|
+
const skillGate = evaluateSkillGate({
|
|
1310
|
+
sessionId,
|
|
1311
|
+
surface: 'claude-pre-tool-gate',
|
|
1312
|
+
text: [JSON.stringify(event.messages || []), unionText, JSON.stringify(toolInput || {})].join('\n'),
|
|
1313
|
+
action: cmd,
|
|
1314
|
+
toolName,
|
|
1315
|
+
filePath,
|
|
1316
|
+
isDeploy: Boolean(deployMatched),
|
|
1317
|
+
isMutation: toolName !== 'Bash',
|
|
1318
|
+
autoLoadAvailable: false,
|
|
1319
|
+
});
|
|
1320
|
+
if (!skillGate.ok) {
|
|
1321
|
+
const reason = formatSkillGateBlock(skillGate);
|
|
1322
|
+
audit('block-missing-skill-receipt', `${skillGate.missingSkills.join(',')} ${cmdPreview}`);
|
|
1323
|
+
emitBlock(reason, { source: 'pre-tool/skill-autoload', tool: toolName, lensCount, requiredLenses: REQUIRED_LENSES });
|
|
1324
|
+
process.exit(2);
|
|
1325
|
+
}
|
|
1326
|
+
try {
|
|
1327
|
+
runUniversalGovernanceGate({
|
|
1328
|
+
sessionId,
|
|
1329
|
+
sourceRuntime: 'claude-code',
|
|
1330
|
+
surface: 'claude-pre-tool-gate',
|
|
1331
|
+
text: [unionText, JSON.stringify(toolInput || {})].join('\n').slice(0, 8000),
|
|
1332
|
+
action: cmd,
|
|
1333
|
+
toolName,
|
|
1334
|
+
filePath,
|
|
1335
|
+
isDeploy: Boolean(deployMatched),
|
|
1336
|
+
isMutation: toolName !== 'Bash',
|
|
1337
|
+
loadedSkills: skillGate.loadedSkills,
|
|
1338
|
+
evidence: { lensCount, hasVerify, hasCognition, hasSubstrateCite },
|
|
1339
|
+
});
|
|
1340
|
+
} catch (err) {
|
|
1341
|
+
audit('block-universal-governance', `${err instanceof Error ? err.message : String(err)}`.slice(0, 500));
|
|
1342
|
+
emitBlock(err instanceof Error ? err.message : String(err), { source: 'pre-tool/universal-governance', tool: toolName, lensCount, requiredLenses: REQUIRED_LENSES });
|
|
1343
|
+
process.exit(2);
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
function pushDecision(decision, reasonText) {
|
|
1347
|
+
pushCognitionEvent({
|
|
1348
|
+
sessionId,
|
|
1349
|
+
client: 'claude-code',
|
|
1350
|
+
surface: 'pre-tool-gate',
|
|
1351
|
+
rawBlock: cogBlockBody ? `<cognition>${cogBlockBody}</cognition>` : null,
|
|
1352
|
+
lensesApplied: lensNames,
|
|
1353
|
+
blockChars: unionText.length,
|
|
1354
|
+
nextActionKind: toolName === 'Bash' ? 'bash' : toolName.toLowerCase(),
|
|
1355
|
+
nextActionSummary: cmdPreview,
|
|
1356
|
+
gateName: 'aria-pre-tool-gate',
|
|
1357
|
+
gateDecision: decision,
|
|
1358
|
+
gateReason: reasonText,
|
|
1359
|
+
metadata: {
|
|
1360
|
+
destructivePattern: matched?.name ?? null,
|
|
1361
|
+
hasVerify,
|
|
1362
|
+
hasCognition,
|
|
1363
|
+
hasSubstrateCite,
|
|
1364
|
+
appliedCognitionContractOk: appliedContract.ok,
|
|
1365
|
+
},
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
function withLoopDirective(reasonText, gateSignature) {
|
|
1370
|
+
const loop = registerGateBlock({
|
|
1371
|
+
gate: 'pre-tool',
|
|
1372
|
+
sessionId,
|
|
1373
|
+
signature: gateSignature,
|
|
1374
|
+
statePath: GATE_LOOP_STATE_PATH,
|
|
1375
|
+
});
|
|
1376
|
+
if (!loop.loopDetected) return reasonText;
|
|
1377
|
+
return `${reasonText}
|
|
1378
|
+
|
|
1379
|
+
[LOOP_DETECTED gate=pre-tool repeats=${loop.totalCount}]
|
|
1380
|
+
Stop retrying the same action shape unchanged.
|
|
1381
|
+
Next response must do this in order:
|
|
1382
|
+
1. Name the exact gate failure in one line.
|
|
1383
|
+
2. Re-emit the missing pre-gate structure only: <cognition>, <verify>, and/or <expected> as required.
|
|
1384
|
+
3. Change the action plan before retrying the tool. Do not issue the same tool call in the same message as the diagnosis.
|
|
1385
|
+
4. If the blocker is stale gate state or ledger residue, say that explicitly instead of inventing fake proof.`;
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
function buildForceRedoActionReason(reasonText, { source = 'pre-tool-gate', tool = toolName || 'unknown', lensCount = 0, requiredLenses = REQUIRED_LENSES } = {}) {
|
|
1389
|
+
if (String(reasonText || '').includes('ARIA FORCE_REDO_ACTION')) return reasonText;
|
|
1390
|
+
return [
|
|
1391
|
+
'=== ARIA FORCE_REDO_ACTION ===',
|
|
1392
|
+
`source: ${source}`,
|
|
1393
|
+
`tool: ${tool}`,
|
|
1394
|
+
'',
|
|
1395
|
+
'This is not a terminal error. It is a forced redo instruction before tool execution.',
|
|
1396
|
+
'Do not retry the same tool call unchanged. Re-author the action with cognition, proof, and expected outcome first.',
|
|
1397
|
+
'',
|
|
1398
|
+
'TEACHING:',
|
|
1399
|
+
'- Pre-tool gates exist to make the model think before acting, not to merely throw after a bad action shape.',
|
|
1400
|
+
'- The redo must name the failed mechanism, cite the current substrate, and state why the next action is safe.',
|
|
1401
|
+
'- Do not bypass by disabling tools, changing endpoints from memory, shortening runtime, or asking Hamza to solve saved-memory ambiguity.',
|
|
1402
|
+
'',
|
|
1403
|
+
'ORIGINAL GATE REASON:',
|
|
1404
|
+
String(reasonText || '').trim(),
|
|
1405
|
+
'',
|
|
1406
|
+
'REQUIRED REDO SHAPE:',
|
|
1407
|
+
'1. Emit/attach <cognition> with the required lenses before retrying the tool.',
|
|
1408
|
+
'2. For Bash, prepend inline cognition comments to the command:',
|
|
1409
|
+
' # nur: <observed state and substrate, >=20 chars>',
|
|
1410
|
+
' # mizan: <risk/tradeoff and why this action is balanced, >=20 chars>',
|
|
1411
|
+
' # hikma: <doctrine applied and what it forbids, >=20 chars>',
|
|
1412
|
+
' # tafakkur: <second-order effects and expected result, >=20 chars>',
|
|
1413
|
+
`3. Current cognition count: ${lensCount}/${requiredLenses}.`,
|
|
1414
|
+
'4. If deploy/shared-infra: include <verify> with target, verified evidence, rollback, and axiom.',
|
|
1415
|
+
'5. If non-trivial action: include <expected> with numeric/boolean/state-string predicate.',
|
|
1416
|
+
'=== END FORCE_REDO_ACTION ===',
|
|
1417
|
+
].join('\n');
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
function emitBlock(reasonText, meta = {}) {
|
|
1421
|
+
console.log(JSON.stringify({ decision: 'block', reason: buildForceRedoActionReason(reasonText, meta) }));
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
if (hasCognition && !appliedContract.ok) {
|
|
1425
|
+
const reason = `Aria pre-tool gate: cognition exists but is not applied to the action.
|
|
1426
|
+
|
|
1427
|
+
The harness no longer accepts cognition as decorative preface. Before a mutating tool call, cognition must produce an execution contract that changes or constrains the action.
|
|
1428
|
+
|
|
1429
|
+
Violations:
|
|
1430
|
+
${appliedContract.violations.map((v) => `- ${v}`).join('\n')}
|
|
1431
|
+
|
|
1432
|
+
Required contract, either in the assistant turn as <applied_cognition>...</applied_cognition> or inline Bash comments:
|
|
1433
|
+
decision_delta: <what changed because cognition ran; not "none">
|
|
1434
|
+
dominant_domain: <engineering_quality | trust | operations | security | product | ...>
|
|
1435
|
+
binds_to: <this exact tool call/action>
|
|
1436
|
+
expected_predicate: <numeric, boolean, or state-string predicate proving success>
|
|
1437
|
+
artifact_change: <how the artifact/action is different because cognition ran>`;
|
|
1438
|
+
audit('block-applied-cognition-contract', cmdPreview);
|
|
1439
|
+
pushDecision('block', `applied cognition contract missing: ${appliedContract.violations.join(', ')}`);
|
|
1440
|
+
emitBlock(reason, { source: 'pre-tool/applied-cognition-contract', tool: toolName, lensCount, requiredLenses: REQUIRED_LENSES });
|
|
1441
|
+
process.exit(2);
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
// ── Deploy-specific gate (doctrine #104) ────────────────────────────────
|
|
1445
|
+
// Per feedback_deploy_requires_verify_cognition.md (Hamza 2026-04-28 after
|
|
1446
|
+
// consciousness.ts crash took aria-soul into CrashLoopBackOff): deploys
|
|
1447
|
+
// require the verify block to cite commit/files/build/admission-policy
|
|
1448
|
+
// AND the cognition block to carry ≥DEPLOY_MIN_SUBSTRATE_ANCHORS substrate
|
|
1449
|
+
// anchors. The full-harness-binding doctrine says lenses without anchors
|
|
1450
|
+
// are unsourced prose — for deploys, that prose-only level of evidence is
|
|
1451
|
+
// not sufficient. The deploy gate refuses until anchors are present.
|
|
1452
|
+
if (deployMatched) {
|
|
1453
|
+
// Anchor count is read from the cognition body (turn-scoped, same
|
|
1454
|
+
// source the substrate-binding stop hook uses).
|
|
1455
|
+
const cognitionBody = cogBlockBody || '';
|
|
1456
|
+
const anchorMatches = cognitionBody.match(SUBSTRATE_ANCHOR_RX) || [];
|
|
1457
|
+
const anchorCount = anchorMatches.length;
|
|
1458
|
+
|
|
1459
|
+
// Required verify-block fields specific to deploy.
|
|
1460
|
+
const verifyBody = bestVerifyBody;
|
|
1461
|
+
const missingDeployFields = [...VERIFY_REQUIRED_FIELDS, ...DEPLOY_VERIFY_REQUIRED_FIELDS]
|
|
1462
|
+
.filter(({ rx }) => !rx.test(verifyBody))
|
|
1463
|
+
.map(({ name }) => name);
|
|
1464
|
+
|
|
1465
|
+
// Heartbeat at deploy-gate entry — proves the gate reached this path
|
|
1466
|
+
// even if a downstream exit fires silently (per
|
|
1467
|
+
// feedback_ledger_writes_outside_crash_boundary.md).
|
|
1468
|
+
try {
|
|
1469
|
+
appendFileSync(HEARTBEAT, JSON.stringify({
|
|
1470
|
+
ts: new Date().toISOString(), gate: 'aria-pre-tool-gate', stage: 'deploy-gate-entry',
|
|
1471
|
+
deployPattern: deployMatched.name, hasVerify, hasCognition, lensCount, anchorCount,
|
|
1472
|
+
missingDeployFields, cogBlockBodyLen: (cogBlockBody || '').length,
|
|
1473
|
+
verifyBodyLen: verifyBody.length,
|
|
1474
|
+
}) + '\n');
|
|
1475
|
+
} catch {}
|
|
1476
|
+
|
|
1477
|
+
const deployBlocked =
|
|
1478
|
+
!hasVerify ||
|
|
1479
|
+
!hasCognition ||
|
|
1480
|
+
anchorCount < DEPLOY_MIN_SUBSTRATE_ANCHORS ||
|
|
1481
|
+
missingDeployFields.length > 0;
|
|
1482
|
+
|
|
1483
|
+
if (!deployBlocked) {
|
|
1484
|
+
// Write justification artifact for deploy-service.sh to read.
|
|
1485
|
+
// Doctrine #104 + bypass-vulnerability-closure 2026-04-28:
|
|
1486
|
+
// The artifact is HMAC-signed using a per-installation secret at
|
|
1487
|
+
// ~/.claude/.aria-gate-secret (0600). deploy-service.sh recomputes
|
|
1488
|
+
// the HMAC and refuses any artifact whose signature does not match
|
|
1489
|
+
// — this binds the artifact to having been written by THIS gate
|
|
1490
|
+
// process, preventing any other process from synthesizing a
|
|
1491
|
+
// passing artifact (which is exactly the bypass owner caught when
|
|
1492
|
+
// the assistant attempted a manualJustification:true write).
|
|
1493
|
+
//
|
|
1494
|
+
// The secret is generated on first run if absent; the file is
|
|
1495
|
+
// chmod 0600 so other users cannot read it; deploy-service.sh
|
|
1496
|
+
// reads the same file and computes the same HMAC.
|
|
1497
|
+
try {
|
|
1498
|
+
const justificationPath = `${HOME}/.claude/.aria-deploy-justification.json`;
|
|
1499
|
+
const runtimeAgnosticJustificationPath = `${HOME}/.aria/.aria-deploy-justification.json`;
|
|
1500
|
+
const secretPath = `${HOME}/.claude/.aria-gate-secret`;
|
|
1501
|
+
|
|
1502
|
+
// Lazy-generate per-installation secret if absent.
|
|
1503
|
+
if (!existsSync(secretPath)) {
|
|
1504
|
+
const secret = cryptoRandomBytes(32).toString('hex');
|
|
1505
|
+
writeFileSync(secretPath, secret + '\n', { mode: 0o600 });
|
|
1506
|
+
chmodSync(secretPath, 0o600);
|
|
1507
|
+
}
|
|
1508
|
+
const secret = readFileSync(secretPath, 'utf-8').trim();
|
|
1509
|
+
|
|
1510
|
+
// Build the unsigned artifact body. Note: deliberately reject any
|
|
1511
|
+
// attempt to set `manualJustification` — only this gate emits the
|
|
1512
|
+
// artifact, and a manual flag is by definition a forgery class.
|
|
1513
|
+
const unsignedBody = {
|
|
1514
|
+
timestamp: new Date().toISOString(),
|
|
1515
|
+
sessionId,
|
|
1516
|
+
deployPattern: deployMatched.name,
|
|
1517
|
+
command: cmdPreview,
|
|
1518
|
+
verify: verifyBody.trim().slice(0, 4000),
|
|
1519
|
+
cognition: cogBlockBody.trim().slice(0, 8000),
|
|
1520
|
+
substrateAnchors: anchorMatches.slice(0, 50),
|
|
1521
|
+
anchorCount,
|
|
1522
|
+
lensCount,
|
|
1523
|
+
verifyFieldsPresent: DEPLOY_VERIFY_REQUIRED_FIELDS
|
|
1524
|
+
.filter(({ rx }) => rx.test(verifyBody))
|
|
1525
|
+
.map(({ name }) => name),
|
|
1526
|
+
};
|
|
1527
|
+
|
|
1528
|
+
// HMAC-SHA256 over the canonical-JSON of the unsigned body.
|
|
1529
|
+
// canonical-JSON = JSON.stringify with no signature field, no
|
|
1530
|
+
// pretty-print whitespace, key order as written above.
|
|
1531
|
+
const signature = createHmac('sha256', secret)
|
|
1532
|
+
.update(JSON.stringify(unsignedBody))
|
|
1533
|
+
.digest('hex');
|
|
1534
|
+
|
|
1535
|
+
const justification = {
|
|
1536
|
+
...unsignedBody,
|
|
1537
|
+
signature,
|
|
1538
|
+
signatureAlgo: 'HMAC-SHA256',
|
|
1539
|
+
};
|
|
1540
|
+
writeFileSync(justificationPath, JSON.stringify(justification, null, 2));
|
|
1541
|
+
mkdirSync(dirname(runtimeAgnosticJustificationPath), { recursive: true });
|
|
1542
|
+
writeFileSync(runtimeAgnosticJustificationPath, JSON.stringify(justification, null, 2), { mode: 0o600 });
|
|
1543
|
+
} catch (writeErr) {
|
|
1544
|
+
// Write failure is non-fatal for the gate decision (the gate itself
|
|
1545
|
+
// is the structural enforcement); log loudly per
|
|
1546
|
+
// canonical-secrets-governance LOUD telemetry doctrine.
|
|
1547
|
+
console.error(
|
|
1548
|
+
`[aria-pre-tool-gate] WARN deploy-justification artifact write failed: ${(writeErr instanceof Error ? writeErr.message : String(writeErr)).slice(0, 200)}`,
|
|
1549
|
+
);
|
|
1550
|
+
}
|
|
1551
|
+
audit(
|
|
1552
|
+
`allow-deploy ${deployMatched.name} lenses=${lensCount} anchors=${anchorCount}`,
|
|
1553
|
+
cmdPreview,
|
|
1554
|
+
);
|
|
1555
|
+
pushDecision(
|
|
1556
|
+
'allow',
|
|
1557
|
+
`verify+cognition+anchors(${anchorCount}) for deploy ${deployMatched.name}`,
|
|
1558
|
+
);
|
|
1559
|
+
process.exit(0);
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
// Build a focused refusal naming exactly which piece is missing.
|
|
1563
|
+
const reasons = [];
|
|
1564
|
+
if (!hasVerify) reasons.push('missing <verify> block');
|
|
1565
|
+
if (!hasCognition) reasons.push(`missing <cognition> block (lenses=${lensCount}/${REQUIRED_LENSES})`);
|
|
1566
|
+
if (hasCognition && anchorCount < DEPLOY_MIN_SUBSTRATE_ANCHORS) {
|
|
1567
|
+
reasons.push(
|
|
1568
|
+
`cognition block has ${anchorCount}/${DEPLOY_MIN_SUBSTRATE_ANCHORS} substrate anchors (axiom:/frame:/memory:/doctrine:/packet:)`,
|
|
1569
|
+
);
|
|
1570
|
+
}
|
|
1571
|
+
if (missingDeployFields.length > 0) {
|
|
1572
|
+
reasons.push(`<verify> block missing required fields: ${missingDeployFields.join(', ')}`);
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
const refusal = withLoopDirective(`Aria pre-tool gate: DEPLOY hard-block — pattern '${deployMatched.name}' detected.
|
|
1576
|
+
|
|
1577
|
+
Per feedback_deploy_requires_verify_cognition.md (Hamza directive 2026-04-28 after consciousness.ts crash took aria-soul into CrashLoopBackOff), every deploy command requires:
|
|
1578
|
+
|
|
1579
|
+
1. A <verify> block in the recent assistant text containing AT MINIMUM:
|
|
1580
|
+
- target/role/verified/rollback/axiom (base verify fields)
|
|
1581
|
+
- commit hash or files-changed listing
|
|
1582
|
+
- tsc / build / type-check evidence
|
|
1583
|
+
- admission policy citation (kubectl get validatingadmissionpolicy <service>-canonical-image-policy)
|
|
1584
|
+
|
|
1585
|
+
2. A <cognition> block with ≥${REQUIRED_LENSES} substantive lenses AND ≥${DEPLOY_MIN_SUBSTRATE_ANCHORS} substrate anchors total (axiom:<name> / frame:<name> / memory:<file> / doctrine:<rule> / packet:<section>).
|
|
1586
|
+
|
|
1587
|
+
Block reasons (this turn): ${reasons.join(' • ')}.
|
|
1588
|
+
|
|
1589
|
+
The 2026-04-28 deploy of an empty consciousness.ts crashed aria-soul because no verify-block step caught the missing export at substrate-citation time. This gate is the structural enforcement that prevents the same gap.
|
|
1590
|
+
|
|
1591
|
+
Re-emit verify+cognition with the missing pieces, then retry the deploy command. There is no env-var override path; doctrine #104 forbids it.`, `deploy:${deployMatched.name}:${missingDeployFields.join(',')}:${anchorCount}:${lensCount}`);
|
|
1592
|
+
|
|
1593
|
+
audit(
|
|
1594
|
+
`block-deploy ${deployMatched.name} verify=${hasVerify} cognition=${lensCount} anchors=${anchorCount} missing=${missingDeployFields.join(',')}`,
|
|
1595
|
+
cmdPreview,
|
|
1596
|
+
);
|
|
1597
|
+
pushDecision('block', `deploy ${deployMatched.name}: ${reasons.join('; ')}`);
|
|
1598
|
+
emitBlock(refusal, { source: 'pre-tool/deploy', tool: toolName });
|
|
1599
|
+
process.exit(2);
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
if (matched) {
|
|
1603
|
+
// Destructive — require BOTH verify (from transcript) AND cognition
|
|
1604
|
+
// (inline command preferred; transcript fallback). Verify stays
|
|
1605
|
+
// transcript-only because it's about pre-action substrate
|
|
1606
|
+
// verification, not about the command's own thought.
|
|
1607
|
+
if (hasVerify && hasCognition) {
|
|
1608
|
+
audit(`allow-verified-cognition ${matched.name} lenses=${lensCount} via=${cognitionSource}`, cmdPreview);
|
|
1609
|
+
pushDecision('allow', `verified+cognition for ${matched.name} (${cognitionSource})`);
|
|
1610
|
+
process.exit(0);
|
|
1611
|
+
}
|
|
1612
|
+
// Block with appropriate corrective message.
|
|
1613
|
+
const reason = withLoopDirective((!hasVerify
|
|
1614
|
+
? `Aria pre-tool gate: destructive pattern '${matched.name}' detected. ${IS_OWNER ? 'Per harness mizan_prestage_rule + axiom_runtime_rule (admit_ignorance, reflection_before_action), include' : 'Pre-action verification required — include'} a <verify> block in your assistant response BEFORE the tool call.
|
|
1615
|
+
|
|
1616
|
+
<verify>
|
|
1617
|
+
target: <exactly what is being changed>
|
|
1618
|
+
role: <what that target does — verified, not assumed>
|
|
1619
|
+
verified: <how you verified: tool output / file read / DB query / asked user>
|
|
1620
|
+
rollback: <exact command to reverse this change>
|
|
1621
|
+
axiom: <which harness rule applies>
|
|
1622
|
+
</verify>
|
|
1623
|
+
|
|
1624
|
+
Re-issue after producing the block. Bypass: '# doctrine-authorized: <reason>' inline..`
|
|
1625
|
+
: `Aria pre-tool gate: destructive pattern '${matched.name}' has its <verify> block, but the <cognition> block is missing or shows only ${lensCount}/${REQUIRED_LENSES}+ required lenses. Every non-trivial action requires visible 8-lens application.
|
|
1626
|
+
|
|
1627
|
+
<cognition>
|
|
1628
|
+
${LENS_NAMES[0]}: <what you see plainly, specific to this task>
|
|
1629
|
+
${LENS_NAMES[1]}: <what's out of proportion / risk profile>
|
|
1630
|
+
${LENS_NAMES[2]}: <what memory or principle applies>
|
|
1631
|
+
${LENS_NAMES[3]}: <deep structural read>
|
|
1632
|
+
${LENS_NAMES[4]}: <if-then chain>
|
|
1633
|
+
${LENS_NAMES[5]}: <distant connection>
|
|
1634
|
+
${LENS_NAMES[6]}: <what just landed in this turn>
|
|
1635
|
+
${LENS_NAMES[7]}: <what user actually needs>
|
|
1636
|
+
</cognition>
|
|
1637
|
+
|
|
1638
|
+
At least 4 substantive lenses required.`),
|
|
1639
|
+
`destructive:${matched.name}:${hasVerify ? 'need-cognition' : 'need-verify'}:${toolName}:${filePath || cmdPreview}`);
|
|
1640
|
+
audit(`block ${matched.name} verify=${hasVerify} cognition=${lensCount}`, cmdPreview);
|
|
1641
|
+
pushDecision('block', `destructive ${matched.name} missing ${!hasVerify ? 'verify' : 'cognition'}`);
|
|
1642
|
+
emitBlock(reason, { source: 'pre-tool/destructive' });
|
|
1643
|
+
process.exit(2);
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
// Non-trivial but not destructive — require cognition. For Bash the
|
|
1647
|
+
// inline-comment path is preferred (action-coupled). For Edit / Write /
|
|
1648
|
+
// NotebookEdit there's no command field, so transcript-scan cognition
|
|
1649
|
+
// is the only path.
|
|
1650
|
+
if (!hasCognition) {
|
|
1651
|
+
const isBash = toolName === 'Bash';
|
|
1652
|
+
const header = isBash
|
|
1653
|
+
? `Aria pre-tool gate: non-trivial Bash command without ${REQUIRED_LENSES}+ substantive cognition lenses. Found ${lensCount}/${REQUIRED_LENSES}+ (inline=${inlineCog.count}, transcript=${transcriptCog.count}).`
|
|
1654
|
+
: `Aria pre-tool gate: ${toolName} call without ${REQUIRED_LENSES}+ substantive cognition lenses in the recent assistant window. Found ${lensCount}/${REQUIRED_LENSES}+ (source=${cognitionSource} — ${toolName} has no inline-cognition path).`;
|
|
1655
|
+
|
|
1656
|
+
const guidance = isBash
|
|
1657
|
+
? `REQUIRED — cognition for every action, no exceptions. Two equivalent forms; either satisfies the gate (both equally REQUIRED, neither preferred over the other):
|
|
1658
|
+
|
|
1659
|
+
FORM 1 (inline in command — action-coupled, recommended for Bash):
|
|
1660
|
+
# ${LENS_NAMES[0]}: <real perception, ≥${SUBSTANCE_MIN_CHARS} chars, no <placeholder>>
|
|
1661
|
+
# ${LENS_NAMES[1]}: <real risk read>
|
|
1662
|
+
# ${LENS_NAMES[2]}: <what principle applies>
|
|
1663
|
+
# ${LENS_NAMES[3]}: <deep structural read>
|
|
1664
|
+
<your actual command here>
|
|
1665
|
+
|
|
1666
|
+
FORM 2 (cognition block in assistant text):
|
|
1667
|
+
<cognition>...</cognition> with ${REQUIRED_LENSES}+ substantive lenses, ≥${SUBSTANCE_MIN_CHARS} chars per lens, no placeholder values.
|
|
1668
|
+
|
|
1669
|
+
Both forms count toward the ${REQUIRED_LENSES}+ requirement; gate counts inline + transcript lenses additively. Substance check is non-negotiable — placeholder/short content does not count.`
|
|
1670
|
+
: `Emit a <cognition>...</cognition> block in your assistant text BEFORE this ${toolName} call, with ${REQUIRED_LENSES}+ substantive lenses. Substance check: each lens must have ≥${SUBSTANCE_MIN_CHARS} chars of non-placeholder content. Cognition is turn-scoped — one block at the start of a turn covers all ${toolName} calls in that turn.
|
|
1671
|
+
|
|
1672
|
+
<cognition>
|
|
1673
|
+
${LENS_NAMES[0]}: <what you see plainly, specific to this edit>
|
|
1674
|
+
${LENS_NAMES[1]}: <what's out of proportion — what could this overwrite or break?>
|
|
1675
|
+
${LENS_NAMES[2]}: <what principle applies (name the source)>
|
|
1676
|
+
${LENS_NAMES[3]}: <deep structural read>
|
|
1677
|
+
${LENS_NAMES[4]}: <if-then chain — what follows from this edit>
|
|
1678
|
+
${LENS_NAMES[5]}: <distant connection a less-careful editor would miss>
|
|
1679
|
+
${LENS_NAMES[6]}: <what just landed in this turn that justifies this edit>
|
|
1680
|
+
${LENS_NAMES[7]}: <what user actually needs underneath>
|
|
1681
|
+
</cognition>`;
|
|
1682
|
+
|
|
1683
|
+
const reason = withLoopDirective(`${header}
|
|
1684
|
+
|
|
1685
|
+
${guidance}
|
|
1686
|
+
|
|
1687
|
+
No per-tool bypass available (v3 doctrine — the harness's whole purpose is no exceptions). No env-var disable path — gates are unconditional from the gated process per Hamza directive 2026-04-27. If the gate misfires on legitimate cognition, fix the gate.`, `cognition:${toolName}:${filePath || cmdPreview}:${cognitionSource}`);
|
|
1688
|
+
|
|
1689
|
+
audit(`block ${toolName.toLowerCase()} cognition=${lensCount}`, cmdPreview);
|
|
1690
|
+
pushDecision('block', `${toolName.toLowerCase()} missing cognition (${lensCount}/${REQUIRED_LENSES})`);
|
|
1691
|
+
emitBlock(reason, { source: 'pre-tool/missing-cognition' });
|
|
1692
|
+
process.exit(2);
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
// Discovery-binding cognition check (structural fix #3) — runs AFTER lens
|
|
1696
|
+
// count passes. If the cognition surfaced a defect (found/noticed/discovered
|
|
1697
|
+
// + bug/broken/issue) without a paired resolution clause, block until the
|
|
1698
|
+
// cognition is updated to bind the discovery to a same-turn fix or task ID.
|
|
1699
|
+
// Per feedback_no_flag_without_fix.md, discoveries are atomic with their
|
|
1700
|
+
// fixes. The pre-tool-gate enforces at the cognition surface; stop-gate's
|
|
1701
|
+
// ledger enforces at the output surface.
|
|
1702
|
+
if (discoveryUnresolved) {
|
|
1703
|
+
const reason = `Aria pre-tool gate: cognition surfaces a discovery (defect, bug, doctrine violation, broken state) but does NOT include a resolution clause binding the discovery to action.
|
|
1704
|
+
|
|
1705
|
+
Per ${docRef('feedback_no_flag_without_fix.md', 'atomic-discovery-rule')}: discoveries are atomic with their fixes. Flag-and-move-on is convenience-seeking — the user has to track what you noticed vs. what you actually fixed.
|
|
1706
|
+
|
|
1707
|
+
Re-emit cognition with one of these resolution forms:
|
|
1708
|
+
|
|
1709
|
+
discoveries:
|
|
1710
|
+
- <what you found>: <fix-now | task: TASK-123 | needs-user-decision>
|
|
1711
|
+
|
|
1712
|
+
OR inline within an existing lens:
|
|
1713
|
+
${LENS_NAMES[2]}: ... fixing inline this turn (same-turn-fix per atomic-discovery-rule).
|
|
1714
|
+
${LENS_NAMES[3]}: ... TaskCreate'd as TASK-XXX with full context (file path, line, what's broken).
|
|
1715
|
+
|
|
1716
|
+
Acceptable resolution markers: 'discoveries:' / 'addressing:' / 'fixing:' / 'TaskCreate' / 'tracked as #N' / 'linear issue' / 'fix-now' / 'same-turn fix'.
|
|
1717
|
+
|
|
1718
|
+
No env-var disable path — gates are unconditional from the gated process per Hamza directive 2026-04-27. If gate misfires on legitimate cognition, fix the gate.`;
|
|
1719
|
+
|
|
1720
|
+
audit(`block-discovery-unresolved ${toolName.toLowerCase()}`, cmdPreview);
|
|
1721
|
+
pushDecision('block', `${toolName.toLowerCase()} cognition has unresolved discovery`);
|
|
1722
|
+
emitBlock(reason, { source: 'pre-tool/discovery-binding' });
|
|
1723
|
+
process.exit(2);
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
// ── Dalio expected_outcome gate ──────────────────────────────────────────────
|
|
1727
|
+
//
|
|
1728
|
+
// Every non-trivial action must carry an <expected> block with at least one
|
|
1729
|
+
// measurable predicate. Block is read from the same turn-scoped assistant text
|
|
1730
|
+
// window as cognition (unionText). Qualitative drift phrases are rejected even
|
|
1731
|
+
// when they appear inside an <expected> block.
|
|
1732
|
+
//
|
|
1733
|
+
// Hard-block path: block the tool call, name the missing block, cite doctrine.
|
|
1734
|
+
// Per feedback_no_graceful_degradation.md — never silent-pass.
|
|
1735
|
+
{
|
|
1736
|
+
const expectedMatch = unionText.match(EXPECTED_BLOCK_RX);
|
|
1737
|
+
const inlineExpectedBody = extractInlineDirectiveBody(cmd, INLINE_EXPECTED_LINE_RX);
|
|
1738
|
+
const expectedBlockText = expectedMatch ? expectedMatch[1] : inlineExpectedBody;
|
|
1739
|
+
const hasMeasurablePredicate = expectedBlockText
|
|
1740
|
+
? (MEASURABLE_PREDICATE_RX.test(expectedBlockText) && !QUALITATIVE_DRIFT_RX.test(expectedBlockText))
|
|
1741
|
+
: false;
|
|
1742
|
+
|
|
1743
|
+
if (!expectedBlockText || !hasMeasurablePredicate) {
|
|
1744
|
+
const reason = expectedBlockText
|
|
1745
|
+
? `Aria pre-tool gate: action requires a measurable predicate inside <expected> per doctrine:dalio_expected_required.
|
|
1746
|
+
|
|
1747
|
+
Your expected-outcome structure was found but contains only qualitative drift phrases (e.g. "better", "improved", "should work", "more reliable") without a concrete measurable predicate. These are unmeasurable and defeat the Dalio accountability loop.
|
|
1748
|
+
|
|
1749
|
+
Replace with one of:
|
|
1750
|
+
• Numeric: exit_code==0, latency<200ms, count>=1, error_rate=0%
|
|
1751
|
+
• Boolean: exit=0, status=healthy, file=exists, rc=0
|
|
1752
|
+
• State-string: "status=running", "200 OK", "count=3 of 3 passed"
|
|
1753
|
+
|
|
1754
|
+
<expected>
|
|
1755
|
+
predicate: exit_code==0 AND file=/home/hamzaibrahim1/.foo written
|
|
1756
|
+
measurable_type: boolean
|
|
1757
|
+
threshold: 0
|
|
1758
|
+
eval_window_minutes: 1
|
|
1759
|
+
</expected>
|
|
1760
|
+
|
|
1761
|
+
No bypass — doctrine:dalio_expected_required is unconditional for non-trivial actions.`
|
|
1762
|
+
: `Aria pre-tool gate: action requires an <expected> block or inline '# expected:' directive with measurable predicate per doctrine:dalio_expected_required.
|
|
1763
|
+
|
|
1764
|
+
Every non-trivial action must state WHAT MEASURABLE STATE the action is expected to produce, so the stop-gate can compare predicted vs actual outcome and write a Dalio ledger entry.
|
|
1765
|
+
|
|
1766
|
+
Required format (add to your assistant turn before this tool call):
|
|
1767
|
+
|
|
1768
|
+
<expected>
|
|
1769
|
+
predicate: <concrete measurable assertion — e.g. "exit_code==0", "status=running", "count>=1">
|
|
1770
|
+
measurable_type: numeric | boolean | state_string
|
|
1771
|
+
threshold: <optional — the exact boundary, e.g. 0 or "healthy">
|
|
1772
|
+
eval_window_minutes: <optional — how long before this expires, e.g. 5>
|
|
1773
|
+
</expected>
|
|
1774
|
+
|
|
1775
|
+
Accepted predicates:
|
|
1776
|
+
• Numeric: >=X, <=X, ==X, X%, count=N, latency<Xms, error_rate=0%
|
|
1777
|
+
• Boolean: exit=0, exit=1, true, false, file=exists, status=healthy
|
|
1778
|
+
• State-string: "status=running", "200 OK", "exit=0", "no_error"
|
|
1779
|
+
|
|
1780
|
+
REJECTED (qualitative drift): "better", "improved", "should work", "more reliable", "cleaner"
|
|
1781
|
+
|
|
1782
|
+
No bypass — doctrine:dalio_expected_required is unconditional for non-trivial actions per feedback_implementation_coupled_cognition.md.`;
|
|
1783
|
+
|
|
1784
|
+
audit(`block-expected-missing ${toolName.toLowerCase()}`, cmdPreview);
|
|
1785
|
+
pushDecision('block', `${toolName.toLowerCase()} missing <expected> measurable predicate`);
|
|
1786
|
+
emitBlock(reason, { source: 'pre-tool/discovery-binding' });
|
|
1787
|
+
process.exit(2);
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
// ── Sub-agent packet-citation check (Layer 4 — #84) ─────────────────────────
|
|
1792
|
+
//
|
|
1793
|
+
// When running inside a sub-agent process (detected by a non-stale handoff file
|
|
1794
|
+
// existing AND harnessPacketPath is set in that handoff), require the cognition
|
|
1795
|
+
// lens content to CITE THE PACKET — at least one of:
|
|
1796
|
+
// - A doctrine rule ID (e.g. "doctrine_first", "no_demos", "workaround_vs_path_fix")
|
|
1797
|
+
// - An axiom name (e.g. "truth_over_deception", "no_harm", "sacred_trust", "reflection_before_action")
|
|
1798
|
+
// - A frame primitive (e.g. "Fitrah", "Tafakkur", "Tadabbur", "Ilham", "Mizan", "Hikma", "Nur", "Wahi", "Firasah")
|
|
1799
|
+
// - A memory class reference (e.g. "feedback_*.md", "project_*.md", "reference_*.md")
|
|
1800
|
+
//
|
|
1801
|
+
// Owner-tier sessions (ownerTier.hamza === true AND no jti) are EXEMPT —
|
|
1802
|
+
// they are the source of truth for the packet itself.
|
|
1803
|
+
//
|
|
1804
|
+
// Fail-soft detection: handoff read errors silently skip this check.
|
|
1805
|
+
(function checkSubAgentPacketCitation() {
|
|
1806
|
+
const _HOME = process.env.HOME || '/tmp';
|
|
1807
|
+
// Try owner-tier handoff path first, then client-tier paths.
|
|
1808
|
+
const HANDOFF_TTL_MS = 5 * 60 * 1000;
|
|
1809
|
+
// Probe known handoff paths.
|
|
1810
|
+
const candidatePaths = [
|
|
1811
|
+
`${_HOME}/.claude/aria-agent-harness-handoff.json`,
|
|
1812
|
+
];
|
|
1813
|
+
// Also check /var/lib/aria-licensee if that dir exists (client-tier).
|
|
1814
|
+
try {
|
|
1815
|
+
const licPath = `${_HOME}/.aria/license.json`;
|
|
1816
|
+
if (existsSync(licPath)) {
|
|
1817
|
+
const lic = JSON.parse(readFileSync(licPath, 'utf8'));
|
|
1818
|
+
if (lic.jti) {
|
|
1819
|
+
candidatePaths.push(`/var/lib/aria-licensee/${lic.jti}/handoff.json`);
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
} catch { /* non-fatal */ }
|
|
1823
|
+
|
|
1824
|
+
let handoff = null;
|
|
1825
|
+
for (const hp of candidatePaths) {
|
|
1826
|
+
if (!existsSync(hp)) continue;
|
|
1827
|
+
try {
|
|
1828
|
+
const raw = JSON.parse(readFileSync(hp, 'utf8'));
|
|
1829
|
+
const ageMs = Date.now() - new Date(raw.writtenAt || 0).getTime();
|
|
1830
|
+
if (ageMs > HANDOFF_TTL_MS) continue; // stale handoff — not a sub-agent context
|
|
1831
|
+
if (!raw.harnessPacketPath) continue; // no packet path written → identity-only handoff, skip check
|
|
1832
|
+
handoff = raw;
|
|
1833
|
+
break;
|
|
1834
|
+
} catch { /* malformed — skip */ }
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
if (!handoff) return; // not a sub-agent context, or handoff has no packetPath
|
|
1838
|
+
|
|
1839
|
+
// Owner-tier exemption: if ownerTier.hamza === true AND no jti → source of truth
|
|
1840
|
+
const ownerExempt = (handoff.ownerTier?.hamza === true) && !handoff.ownerTier?.jti;
|
|
1841
|
+
if (ownerExempt) return;
|
|
1842
|
+
|
|
1843
|
+
// Now verify that the cognition block cites at least one packet substrate token.
|
|
1844
|
+
// Tokens accepted (case-insensitive):
|
|
1845
|
+
// • Doctrine rule IDs from feedback_*/project_*/reference_* filenames
|
|
1846
|
+
// • Axiom names: truth_over_deception, no_harm, sacred_trust, reflection_before_action
|
|
1847
|
+
// • Frame primitives: Fitrah, Tafakkur, Tadabbur, Ilham, Mizan, Hikma, Nur, Wahi, Firasah
|
|
1848
|
+
// • Memory class patterns: feedback_*.md, project_*.md, reference_*.md
|
|
1849
|
+
const PACKET_CITE_RX = /\b(?:feedback_[a-z0-9_]+\.md|project_[a-z0-9_]+\.md|reference_[a-z0-9_]+\.md|doctrine_first|no_demos|workaround_vs_path_fix|no_flag_without_fix|implementation_coupled_cognition|session_starts_with_linear|gates_enforce_form_not_substance|truth_over_deception|no_harm|sacred_trust|reflection_before_action|power_obligates_service|fitrah|tafakkur|tadabbur|ilham|mizan|hikma|nur|wahi|firasah|harness\s*packet)\b/i;
|
|
1850
|
+
|
|
1851
|
+
const cogText = cogBlockBody || unionText;
|
|
1852
|
+
const hasCite = PACKET_CITE_RX.test(cogText) || PACKET_CITE_RX.test(cmd);
|
|
1853
|
+
|
|
1854
|
+
if (!hasCite) {
|
|
1855
|
+
const packetRef = handoff.harnessPacketPath;
|
|
1856
|
+
const reason = `Sub-agent cognition cites no packet substrate — lens content must reference at least one axiom/frame/memory/doctrine from the harness packet at ${packetRef}.
|
|
1857
|
+
|
|
1858
|
+
Accepted citations (case-insensitive, any ONE suffices):
|
|
1859
|
+
• Doctrine rule IDs: doctrine_first, no_demos, workaround_vs_path_fix, no_flag_without_fix, etc.
|
|
1860
|
+
• Axioms: truth_over_deception, no_harm, sacred_trust, reflection_before_action, power_obligates_service
|
|
1861
|
+
• Frame primitives: Fitrah, Tafakkur, Tadabbur, Ilham, Mizan, Hikma, Nur, Wahi, Firasah
|
|
1862
|
+
• Memory class refs: feedback_*.md, project_*.md, reference_*.md
|
|
1863
|
+
|
|
1864
|
+
The harness packet is at: ${packetRef}
|
|
1865
|
+
Read it first (it is in your environment as ARIA_HARNESS_PACKET_PATH), then reference it in your cognition block.
|
|
1866
|
+
|
|
1867
|
+
Cognition-theater rejected per feedback_gates_enforce_form_not_substance.md.`;
|
|
1868
|
+
audit(`block-subagent-no-packet-cite ${toolName.toLowerCase()}`, cmdPreview);
|
|
1869
|
+
emitBlock(reason, { source: 'pre-tool/architecture-facts' });
|
|
1870
|
+
process.exit(2);
|
|
1871
|
+
}
|
|
1872
|
+
})();
|
|
1873
|
+
|
|
1874
|
+
// ── arch_facts gate (architectural violation scan) ────────────────────────
|
|
1875
|
+
//
|
|
1876
|
+
// Runs after cognition + discovery-binding pass, for Edit/Write/NotebookEdit.
|
|
1877
|
+
// Bash is skipped: architectural violations live in the code being written,
|
|
1878
|
+
// not in shell commands — checking Bash commands would produce false positives
|
|
1879
|
+
// on grep/find invocations that mention forbidden strings without introducing them.
|
|
1880
|
+
//
|
|
1881
|
+
// Content inspected: new_string (Edit), content (Write), source (NotebookEdit).
|
|
1882
|
+
// Fail-open: unreachable mizan → allow. Client surface → skip.
|
|
1883
|
+
if (['Edit', 'Write', 'NotebookEdit'].includes(toolName)) {
|
|
1884
|
+
// Extract the actual content being written — this is what mizan should scan
|
|
1885
|
+
// for architectural patterns, not the file path or surrounding metadata.
|
|
1886
|
+
const contentToScan =
|
|
1887
|
+
toolInput.new_string ?? // Edit: the replacement text
|
|
1888
|
+
toolInput.content ?? // Write: the full file content
|
|
1889
|
+
toolInput.source ?? // NotebookEdit: cell source
|
|
1890
|
+
'';
|
|
1891
|
+
// Also include file_path for context so mizan pattern-matching can be
|
|
1892
|
+
// path-scoped (e.g. R9 only fires for telemetry/cron paths).
|
|
1893
|
+
const scanPayload = `// file: ${filePath}\n${contentToScan}`;
|
|
1894
|
+
const archResult = await archFactsGate(scanPayload);
|
|
1895
|
+
if (!archResult.allow) {
|
|
1896
|
+
const violationText = (archResult.violations || [])
|
|
1897
|
+
.map((v) => ` • [${v.rule ?? v.id ?? 'arch'}] ${v.description ?? v.message ?? JSON.stringify(v)}`)
|
|
1898
|
+
.join('\n');
|
|
1899
|
+
const archReason = `Aria arch_facts gate: architectural violation(s) detected in ${toolName} diff for ${filePath || '(no path)'}.
|
|
1900
|
+
|
|
1901
|
+
${violationText}
|
|
1902
|
+
|
|
1903
|
+
These patterns are forbidden by mizan arch_facts rules (mizan.yaml R7–R11). Fix the structural issue before re-issuing the tool call — the gate does not have a bypass for architectural violations.
|
|
1904
|
+
|
|
1905
|
+
Rule references:
|
|
1906
|
+
R7 no_sidecar_in_main_container — bolt-in fork pattern (project_aria_soul_systemd_migration.md)
|
|
1907
|
+
R8 no_silent_fallback_default — || 'unknown'/'default'/'fallback' masks config failures
|
|
1908
|
+
R9 no_timeout_based_retry — setTimeout+abort in telemetry/chat/cron paths (no-timeouts doctrine)
|
|
1909
|
+
R10 no_kubectl_apply_for_image_drift — kubectl apply on aria-soul-stateful (project_forge_psi_oom_cascade.md)
|
|
1910
|
+
R11 no_console_log_secrets — console.log with TOKEN/PASSWORD/SECRET/API_KEY/JWT/BEARER`;
|
|
1911
|
+
audit(`block-arch-facts ${toolName.toLowerCase()} path=${filePath}`, `violations=${archResult.violations?.length ?? 0}`);
|
|
1912
|
+
emitBlock(archReason, { source: 'pre-tool/architecture-facts' });
|
|
1913
|
+
process.exit(2);
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
// Non-trivial action with cognition (inline for Bash, transcript for
|
|
1918
|
+
// Edit/Write/NotebookEdit) — passes cognition gate. Now check Aria-binding.
|
|
1919
|
+
|
|
1920
|
+
// ── Aria-as-commander binding check (Layer A) ──
|
|
1921
|
+
//
|
|
1922
|
+
// All earlier gate checks passed (cognition, destructive-verify). Final
|
|
1923
|
+
// gate before allow: does the active phase of Aria's plan permit this action?
|
|
1924
|
+
// No plan + no prior plan = block (CONSULT_UNAVAILABLE per contract).
|
|
1925
|
+
// No plan + prior plan exists = enforce against prior (Hamza fallback directive).
|
|
1926
|
+
// Plan exists + action mismatch = block with phase-violation reason.
|
|
1927
|
+
//
|
|
1928
|
+
// Bootstrap consult carve-out (Hamza 2026-04-27 caught this — "how would
|
|
1929
|
+
// anyone start a session?"): consult endpoints are how plans START existing.
|
|
1930
|
+
// Locking them behind plan-existence is unbootstrappable. So: if the action
|
|
1931
|
+
// is a consult to harness/aria endpoints (regardless of plan state), skip the
|
|
1932
|
+
// binding block. Cognition gate above already passed; the consult itself
|
|
1933
|
+
// will issue the next plan on completion. Audited per-use.
|
|
1934
|
+
//
|
|
1935
|
+
// bindingBypassReason: per-command bypass was removed in v3. The variable is
|
|
1936
|
+
// kept as a fixed null so the binding check below reads cleanly without a
|
|
1937
|
+
// ReferenceError. No new bypass entries can be created (pre-existing defect
|
|
1938
|
+
// fixed inline per atomic-discovery-rule).
|
|
1939
|
+
const bindingBypassReason = null;
|
|
1940
|
+
|
|
1941
|
+
// Defect #4 fix — Consult-Aria unconditionally allowed (doctrine #50).
|
|
1942
|
+
//
|
|
1943
|
+
// Aria-as-commander session pattern (HARNESS_ARIA_AS_COMMANDER_CONTRACT.md
|
|
1944
|
+
// doctrine #50) makes per-turn consult mandatory. A plan cannot forbid the
|
|
1945
|
+
// consult mechanism that issues plans — that would be an unbootstrappable
|
|
1946
|
+
// circular lock. Previously plan p1 forbade bash_other → consult curl was
|
|
1947
|
+
// classified as bash_other → blocked. Now: any Bash command that is a consult
|
|
1948
|
+
// to a known Aria/harness endpoint passes UNCONDITIONALLY past ALL binding
|
|
1949
|
+
// checks regardless of allowedActions or forbiddenActions in the active phase.
|
|
1950
|
+
// Cognition gate still applies (it ran above and passed to reach this point).
|
|
1951
|
+
//
|
|
1952
|
+
// Covered endpoints (hardcoded carve-out):
|
|
1953
|
+
// curl http(s)://aria-soul<anything>
|
|
1954
|
+
// curl http(s)://aria-telemetry<anything>
|
|
1955
|
+
// curl http(s)://localhost:30080/(chat|api/aria/speak|api/harness/codex|
|
|
1956
|
+
// api/harness/verify-claim|v1/doctrine|v1/mizan)
|
|
1957
|
+
const ARIA_CONSULT_CURL_RX = /curl\s+['"]?https?:\/\/(?:aria-soul[^\s'"]*|aria-telemetry[^\s'"]*|localhost:30080\/(?:chat|api\/aria\/speak|api\/harness\/(?:codex|verify-claim|delegate|army|plan)|v1\/(?:doctrine|mizan))[^\s'"]*)/i;
|
|
1958
|
+
const __isUnconditionalConsult = toolName === 'Bash' && ARIA_CONSULT_CURL_RX.test(cmd);
|
|
1959
|
+
if (__isUnconditionalConsult) {
|
|
1960
|
+
bindingAuditAppend({ event: 'allow_unconditional_consult', sessionId, toolName, cmdPreview, reason: 'doctrine#50-aria-as-commander-consult-carveout' });
|
|
1961
|
+
audit(`allow-unconditional-consult lenses=${lensCount}`, cmdPreview);
|
|
1962
|
+
pushDecision('allow', 'unconditional consult carve-out (doctrine #50)');
|
|
1963
|
+
process.exit(0);
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
const __bindingActionClassification = (BINDING_ENABLED && !bindingBypassReason)
|
|
1967
|
+
? classifyToolForBinding(toolName, cmd, filePath)
|
|
1968
|
+
: null;
|
|
1969
|
+
const __isBootstrapConsult = __bindingActionClassification?.action === 'consult';
|
|
1970
|
+
if (__isBootstrapConsult) {
|
|
1971
|
+
bindingAuditAppend({ event: 'allow_bootstrap_consult', sessionId, target: __bindingActionClassification.target, toolName });
|
|
1972
|
+
}
|
|
1973
|
+
if (BINDING_ENABLED && !bindingBypassReason && !__isBootstrapConsult) {
|
|
1974
|
+
let plan = loadActivePlan(sessionId);
|
|
1975
|
+
if (!plan) {
|
|
1976
|
+
// INLINE architect-fallback (Hamza directive 2026-04-27 — no async stop-gate
|
|
1977
|
+
// races). When no plan exists, fire architect-fallback synchronously here
|
|
1978
|
+
// so a real plan is minted and loaded inside the same hook execution. If
|
|
1979
|
+
// architect-fallback fails the gate still BLOCKS per doctrine (no bypass)
|
|
1980
|
+
// but the block message includes the architect-fallback exit details so the
|
|
1981
|
+
// failure is visible LOUDLY per feedback_no_graceful_degradation.md.
|
|
1982
|
+
bindingAuditAppend({ event: 'no_plan_inline_fallback_attempt', sessionId, toolName });
|
|
1983
|
+
const blockerReason = `pre-tool-gate inline fallback: no active plan for session ${sessionId} when ${toolName} was attempted on ${cmdPreview.slice(0, 200)}`;
|
|
1984
|
+
const fallbackEvent = JSON.stringify({ reason: blockerReason, sessionId, toolName, currentPlanId: 'none' });
|
|
1985
|
+
let architectExit = -1;
|
|
1986
|
+
let architectStderr = '';
|
|
1987
|
+
try {
|
|
1988
|
+
const archProc = spawnSync(process.execPath, [`${HOME}/.claude/hooks/aria-architect-fallback.mjs`], {
|
|
1989
|
+
input: fallbackEvent,
|
|
1990
|
+
encoding: 'utf8',
|
|
1991
|
+
timeout: 60000,
|
|
1992
|
+
});
|
|
1993
|
+
architectExit = archProc.status ?? -1;
|
|
1994
|
+
architectStderr = (archProc.stderr || '').slice(0, 500);
|
|
1995
|
+
} catch (err) {
|
|
1996
|
+
architectStderr = String(err).slice(0, 500);
|
|
1997
|
+
}
|
|
1998
|
+
bindingAuditAppend({ event: 'architect_fallback_result', sessionId, exit: architectExit, stderr: architectStderr.slice(0, 300) });
|
|
1999
|
+
|
|
2000
|
+
plan = loadActivePlan(sessionId);
|
|
2001
|
+
if (plan) {
|
|
2002
|
+
process.stderr.write(`\n✓ PRE-TOOL-GATE FALLBACK: architect-fallback minted plan ${plan.planId}. Continuing.\n`);
|
|
2003
|
+
bindingAuditAppend({ event: 'architect_fallback_minted_plan', sessionId, planId: plan.planId });
|
|
2004
|
+
} else {
|
|
2005
|
+
bindingAuditAppend({ event: 'block_no_active_plan_after_fallback', sessionId, toolName, architectExit });
|
|
2006
|
+
const reason = `Aria binding gate: no active plan exists AND inline architect-fallback failed (exit=${architectExit}). Plan-mint chain broken. ${architectStderr ? 'Architect stderr: ' + architectStderr : ''}
|
|
2007
|
+
|
|
2008
|
+
What Claude must do:
|
|
2009
|
+
1. Acknowledge to Hamza that the architect-fallback chain failed (visible in audit log)
|
|
2010
|
+
2. Surface the failure LOUDLY — this is a substrate-level break, not a routine consult miss
|
|
2011
|
+
3. Wait for next user prompt — preprompt-consult will retry; if it succeeds a plan will exist on next tool call
|
|
2012
|
+
|
|
2013
|
+
Non-trivial actions are blocked until a plan exists. Trivial reads (ls/cat/grep) bypass automatically per existing whitelist. To temporarily disable binding for emergency: ARIA_BINDING_ENABLED=false (logged).`;
|
|
2014
|
+
emitBlock(reason, { source: 'pre-tool/substrate-binding' });
|
|
2015
|
+
process.exit(2);
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
const transcriptText = unionText || '';
|
|
2020
|
+
let phaseInfo = pickCurrentPhase(plan, transcriptText);
|
|
2021
|
+
|
|
2022
|
+
if (!phaseInfo) {
|
|
2023
|
+
// All phases reported complete — needs new consult before more action
|
|
2024
|
+
bindingAuditAppend({ event: 'block_all_phases_complete', sessionId, planId: plan.planId, toolName });
|
|
2025
|
+
const reason = `Aria binding gate: all phases of plan ${plan.planId} have been reported complete in this transcript. The plan is exhausted; Claude cannot proceed without a fresh consult.
|
|
2026
|
+
|
|
2027
|
+
What Claude must do:
|
|
2028
|
+
1. Acknowledge plan completion to Hamza
|
|
2029
|
+
2. Wait for next user prompt — preprompt-consult will issue a new plan
|
|
2030
|
+
3. Don't continue executing actions beyond the issued plan boundary
|
|
2031
|
+
|
|
2032
|
+
This prevents Claude from drifting past Aria's authorized scope.`;
|
|
2033
|
+
emitBlock(reason, { source: 'pre-tool/substrate-binding' });
|
|
2034
|
+
process.exit(2);
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
if (phaseInfo.abortedHere) {
|
|
2038
|
+
// INLINE architect-fallback (Hamza directive 2026-04-27 — same recovery as
|
|
2039
|
+
// no-plan branch above). When phase is aborted, fire architect-fallback
|
|
2040
|
+
// synchronously here to mint a fresh plan instead of deadlocking. If
|
|
2041
|
+
// architect-fallback fails the gate still BLOCKS per doctrine but the
|
|
2042
|
+
// failure is LOUD per feedback_no_graceful_degradation.md.
|
|
2043
|
+
bindingAuditAppend({ event: 'phase_aborted_inline_fallback_attempt', sessionId, planId: plan.planId, phaseId: phaseInfo.phase.id, toolName });
|
|
2044
|
+
const blockerReason = `pre-tool-gate inline fallback: phase ${phaseInfo.phase.id} of plan ${plan.planId} aborted, ${toolName} attempted on ${cmdPreview.slice(0, 200)}`;
|
|
2045
|
+
const fallbackEvent = JSON.stringify({ reason: blockerReason, sessionId, toolName, currentPlanId: plan.planId });
|
|
2046
|
+
let architectExit = -1;
|
|
2047
|
+
let architectStderr = '';
|
|
2048
|
+
try {
|
|
2049
|
+
const archProc = spawnSync(process.execPath, [`${HOME}/.claude/hooks/aria-architect-fallback.mjs`], {
|
|
2050
|
+
input: fallbackEvent,
|
|
2051
|
+
encoding: 'utf8',
|
|
2052
|
+
timeout: 60000,
|
|
2053
|
+
});
|
|
2054
|
+
architectExit = archProc.status ?? -1;
|
|
2055
|
+
architectStderr = (archProc.stderr || '').slice(0, 500);
|
|
2056
|
+
} catch (err) {
|
|
2057
|
+
architectStderr = String(err).slice(0, 500);
|
|
2058
|
+
}
|
|
2059
|
+
bindingAuditAppend({ event: 'aborted_phase_architect_fallback_result', sessionId, exit: architectExit, stderr: architectStderr.slice(0, 300) });
|
|
2060
|
+
|
|
2061
|
+
const freshPlan = loadActivePlan(sessionId);
|
|
2062
|
+
if (freshPlan && freshPlan.planId !== plan.planId) {
|
|
2063
|
+
process.stderr.write(`\n✓ PRE-TOOL-GATE FALLBACK: aborted phase recovered, fresh plan ${freshPlan.planId} minted. Continuing.\n`);
|
|
2064
|
+
bindingAuditAppend({ event: 'aborted_phase_recovered', sessionId, oldPlanId: plan.planId, newPlanId: freshPlan.planId });
|
|
2065
|
+
plan = freshPlan;
|
|
2066
|
+
const freshPhaseInfo = pickCurrentPhase(plan, transcriptText);
|
|
2067
|
+
if (!freshPhaseInfo || freshPhaseInfo.abortedHere) {
|
|
2068
|
+
bindingAuditAppend({ event: 'block_aborted_phase_post_fallback_still_bad', sessionId, planId: plan.planId });
|
|
2069
|
+
const reason = `Aria binding gate: aborted phase recovery minted plan ${plan.planId} but new plan also has no usable phase. Manual intervention required.`;
|
|
2070
|
+
emitBlock(reason, { source: 'pre-tool/dalio-expected' });
|
|
2071
|
+
process.exit(2);
|
|
2072
|
+
}
|
|
2073
|
+
phaseInfo = freshPhaseInfo;
|
|
2074
|
+
} else {
|
|
2075
|
+
bindingAuditAppend({ event: 'block_phase_aborted_fallback_failed', sessionId, planId: plan.planId, phaseId: phaseInfo.phase.id, architectExit });
|
|
2076
|
+
const reason = `Aria binding gate: phase ${phaseInfo.phase.id} of plan ${plan.planId} was reported aborted AND inline architect-fallback failed (exit=${architectExit}). Plan progression halted. ${architectStderr ? 'Architect stderr: ' + architectStderr : ''}
|
|
2077
|
+
|
|
2078
|
+
What Claude must do: emit [PLAN_BLOCKER reason="<concrete observation>" suggestedAmendment="<if any>"] for Hamza/Aria to issue a corrected plan. Do not continue executing the aborted plan.`;
|
|
2079
|
+
emitBlock(reason, { source: 'pre-tool/dalio-expected' });
|
|
2080
|
+
process.exit(2);
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
const { action, target } = classifyToolForBinding(toolName, cmd, filePath);
|
|
2085
|
+
const phase = phaseInfo.phase;
|
|
2086
|
+
|
|
2087
|
+
// Forbidden takes precedence
|
|
2088
|
+
const forbidden = (phase.forbiddenActions || []).find((p) => actionMatchesPattern(action, p, target));
|
|
2089
|
+
if (forbidden) {
|
|
2090
|
+
bindingAuditAppend({ event: 'block_forbidden_action', sessionId, planId: plan.planId, phaseId: phase.id, action, target, matchedRule: forbidden });
|
|
2091
|
+
// Hive recipe lookup BEFORE emitting the block — if the same shape has
|
|
2092
|
+
// fired before, surface the recipe inline. Lookup is fail-soft: a probe
|
|
2093
|
+
// failure leaves the block message unchanged.
|
|
2094
|
+
const lookup = await lookupBlockPatternRecipe({
|
|
2095
|
+
detectorClass: 'doctrine_violation',
|
|
2096
|
+
signature: `binding-forbidden::action=${action}::matches=${forbidden}`,
|
|
2097
|
+
tenantId: sessionId,
|
|
2098
|
+
});
|
|
2099
|
+
const recipeAddendum = renderRecipeAddendum(lookup);
|
|
2100
|
+
const reason = `Aria binding gate: action "${action}" on target "${target}" matches forbidden pattern "${forbidden}" for current phase ${phase.id} ("${phase.summary}") of plan ${plan.planId}.
|
|
2101
|
+
|
|
2102
|
+
Phase summary: ${phase.summary}
|
|
2103
|
+
Forbidden actions for this phase: ${(phase.forbiddenActions || []).join(', ') || '(none)'}
|
|
2104
|
+
Allowed actions for this phase: ${(phase.allowedActions || []).join(', ') || '(none)'}
|
|
2105
|
+
|
|
2106
|
+
Claude must either: (a) reframe the action to fit allowedActions, OR (b) emit [PLAN_BLOCKER reason="..."] requesting Aria amend the plan.${recipeAddendum}`;
|
|
2107
|
+
emitBlock(reason, { source: 'pre-tool/output-claim' });
|
|
2108
|
+
process.exit(2);
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
const allowed = (phase.allowedActions || []).find((p) => actionMatchesPattern(action, p, target));
|
|
2112
|
+
if (!allowed) {
|
|
2113
|
+
bindingAuditAppend({ event: 'block_action_not_in_allowed_list', sessionId, planId: plan.planId, phaseId: phase.id, action, target });
|
|
2114
|
+
const lookup = await lookupBlockPatternRecipe({
|
|
2115
|
+
detectorClass: 'doctrine_violation',
|
|
2116
|
+
signature: `binding-not-allowed::action=${action}::phase=${phase.id}`,
|
|
2117
|
+
tenantId: sessionId,
|
|
2118
|
+
});
|
|
2119
|
+
const recipeAddendum = renderRecipeAddendum(lookup);
|
|
2120
|
+
const reason = `Aria binding gate: action "${action}" on target "${target}" is NOT in allowedActions for current phase ${phase.id} of plan ${plan.planId}.
|
|
2121
|
+
|
|
2122
|
+
Phase summary: ${phase.summary}
|
|
2123
|
+
Allowed actions: ${(phase.allowedActions || []).join(', ') || '(none — phase is observation-only)'}
|
|
2124
|
+
|
|
2125
|
+
Claude must either: (a) reframe action to fit allowedActions, OR (b) emit [PLAN_BLOCKER reason="..."] for plan amendment.${recipeAddendum}`;
|
|
2126
|
+
emitBlock(reason, { source: 'pre-tool/output-claim' });
|
|
2127
|
+
process.exit(2);
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
bindingAuditAppend({ event: 'allow_phase_action', sessionId, planId: plan.planId, phaseId: phase.id, action, target });
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
// ── Outcome Ledger regression flag (fail-soft) ───────────────────────────────
|
|
2134
|
+
//
|
|
2135
|
+
// Before allowing, query aria_outcome_ledger for recent regressions on the same
|
|
2136
|
+
// action shape. This is NOT a block — it's a soft-warning surfaced to stderr so
|
|
2137
|
+
// the agent can see the risk and decide whether the root cause has shipped.
|
|
2138
|
+
//
|
|
2139
|
+
// action_kind: 'edit' for Edit/Write/NotebookEdit, 'bash' for Bash
|
|
2140
|
+
// action_target: file path for file tools; first word of bash command otherwise
|
|
2141
|
+
//
|
|
2142
|
+
// Fail-open: network errors, timeouts, or non-200 responses are silently ignored.
|
|
2143
|
+
// The gate does not hardcode timeouts (no-timeouts doctrine); the 3s AbortController
|
|
2144
|
+
// is a DETECTION PROBE — if the endpoint is alive it will respond quickly; if it
|
|
2145
|
+
// is down the catch path fail-opens without blocking the developer.
|
|
2146
|
+
(async function checkOutcomeLedger() {
|
|
2147
|
+
const _harnessUrl =
|
|
2148
|
+
process.env.ARIA_HIVE_RUNTIME_URL ||
|
|
2149
|
+
process.env.ARIA_HARNESS_BASE_URL ||
|
|
2150
|
+
process.env.ARIA_HARNESS_URL ||
|
|
2151
|
+
'https://harness.ariasos.com';
|
|
2152
|
+
const _harnessToken = process.env.ARIA_HARNESS_TOKEN || '';
|
|
2153
|
+
|
|
2154
|
+
// Derive action_kind and action_target from current tool call
|
|
2155
|
+
let _actionKind = 'bash';
|
|
2156
|
+
let _actionTarget = '';
|
|
2157
|
+
if (toolName === 'Edit' || toolName === 'Write' || toolName === 'NotebookEdit') {
|
|
2158
|
+
_actionKind = 'edit';
|
|
2159
|
+
_actionTarget = filePath;
|
|
2160
|
+
} else if (toolName === 'Bash') {
|
|
2161
|
+
_actionKind = 'bash';
|
|
2162
|
+
// First word of the command (the verb) as the shape key
|
|
2163
|
+
_actionTarget = cmd.trim().split(/\s+/)[0] || '';
|
|
2164
|
+
// If the command touches a specific file path, prefer that as target
|
|
2165
|
+
// (captures edit-like bash operations: sed, awk, tee, etc.)
|
|
2166
|
+
const _fileArgMatch = cmd.match(/\b([\w./~-]+\.[a-z]{2,6})\b/i);
|
|
2167
|
+
if (_fileArgMatch) _actionTarget = _fileArgMatch[1];
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
if (!_actionTarget) return; // nothing useful to query
|
|
2171
|
+
|
|
2172
|
+
try {
|
|
2173
|
+
const _ctl = new AbortController();
|
|
2174
|
+
const _probeTimer = setTimeout(() => _ctl.abort(), 3000);
|
|
2175
|
+
let _resp;
|
|
2176
|
+
try {
|
|
2177
|
+
const _params = new URLSearchParams({ action_kind: _actionKind, action_target: _actionTarget });
|
|
2178
|
+
_resp = await fetch(`${_harnessUrl}/api/harness/outcome-recent-regressions?${_params}`, {
|
|
2179
|
+
method: 'GET',
|
|
2180
|
+
headers: {
|
|
2181
|
+
'Content-Type': 'application/json',
|
|
2182
|
+
...(_harnessToken ? { Authorization: `Bearer ${_harnessToken}` } : {}),
|
|
2183
|
+
},
|
|
2184
|
+
signal: _ctl.signal,
|
|
2185
|
+
});
|
|
2186
|
+
} finally {
|
|
2187
|
+
clearTimeout(_probeTimer);
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
if (!_resp || !_resp.ok) return; // endpoint unreachable or error — fail-open
|
|
2191
|
+
const _data = await _resp.json();
|
|
2192
|
+
const _regressions = Array.isArray(_data?.regressions) ? _data.regressions : [];
|
|
2193
|
+
|
|
2194
|
+
if (_regressions.length > 0) {
|
|
2195
|
+
// Soft-warning to stderr — visible in Claude Code's hook output but does NOT block
|
|
2196
|
+
const _lines = _regressions.map(
|
|
2197
|
+
(r) => ` - ${r.action_target} at ${new Date(r.created_at).toISOString()}: ${r.regression_signal || '(no signal text)'}`,
|
|
2198
|
+
).join('\n');
|
|
2199
|
+
process.stderr.write(
|
|
2200
|
+
`⚠️ Aria Outcome Ledger: this action shape regressed ${_regressions.length} time(s) in the last 60 min:\n` +
|
|
2201
|
+
`${_lines}\n` +
|
|
2202
|
+
`Re-emitting with this risk visible. Proceed only if root cause has shipped since.\n`,
|
|
2203
|
+
);
|
|
2204
|
+
audit(`warn-ledger-regression ${_actionKind} target=${_actionTarget} count=${_regressions.length}`, cmdPreview);
|
|
2205
|
+
}
|
|
2206
|
+
} catch {
|
|
2207
|
+
// Network error, abort, parse failure — fail-open, no warning
|
|
2208
|
+
}
|
|
2209
|
+
})();
|
|
2210
|
+
|
|
2211
|
+
// ── Substrate-bound contract gate via SDK.checkAction ──────────────────────
|
|
2212
|
+
// Hamza directive 2026-04-28: local cognition substance + binding-plan gates
|
|
2213
|
+
// pass, but the substrate has its own contract gate that knows about deploy
|
|
2214
|
+
// state, soul-charge, hospital admission policy, etc. checkAction returns
|
|
2215
|
+
// { allowed, reason, requiredGates }. allowed:false halts the tool with the
|
|
2216
|
+
// substrate's reason. SDK call failure is non-blocking (fail-open) — halting
|
|
2217
|
+
// every tool when substrate is down would brick the orchestrator, but the
|
|
2218
|
+
// failure is logged for telemetry.
|
|
2219
|
+
try {
|
|
2220
|
+
const { HTTPHarnessClient } = await import('@aria_asi/harness-http-client');
|
|
2221
|
+
const { readFileSync: __fsRead, existsSync: __fsExists } = await import('node:fs');
|
|
2222
|
+
const { homedir: __homedir } = await import('node:os');
|
|
2223
|
+
const __home = __homedir();
|
|
2224
|
+
const __tokenPath = `${__home}/.aria/owner-token`;
|
|
2225
|
+
const __licensePath = `${__home}/.aria/license.json`;
|
|
2226
|
+
// Tier detection: client if license.json has a jti, else owner.
|
|
2227
|
+
// Hamza correction 2026-04-28b: master/owner-token credentials belong
|
|
2228
|
+
// to Hamza only — never resolve them on client-tier processes.
|
|
2229
|
+
let __isOwner = true;
|
|
2230
|
+
try {
|
|
2231
|
+
if (__fsExists(__licensePath)) {
|
|
2232
|
+
const __lic = JSON.parse(__fsRead(__licensePath, 'utf8'));
|
|
2233
|
+
__isOwner = !__lic.jti;
|
|
2234
|
+
}
|
|
2235
|
+
} catch { __isOwner = true; }
|
|
2236
|
+
// Resolution: ARIA_HARNESS_TOKEN env first (both tiers). ONLY on owner
|
|
2237
|
+
// tier, fall back to master/api-key env or owner-token file.
|
|
2238
|
+
let __apiKey = process.env.ARIA_HARNESS_TOKEN || '';
|
|
2239
|
+
if (!__apiKey && __isOwner) {
|
|
2240
|
+
__apiKey = process.env.ARIA_MASTER_TOKEN
|
|
2241
|
+
|| process.env.ARIA_API_KEY
|
|
2242
|
+
|| (__fsExists(__tokenPath) ? __fsRead(__tokenPath, 'utf8').trim() : '');
|
|
2243
|
+
}
|
|
2244
|
+
// Map Claude tool names → checkAction action enum (substrate signature is
|
|
2245
|
+
// 'deploy'|'build'|'write'|'delete'). Most state-mutating tools map to
|
|
2246
|
+
// 'write'; explicit deploy/destructive cases would map elsewhere if the
|
|
2247
|
+
// substrate widens the enum later.
|
|
2248
|
+
const __toolToAction = { Bash: 'write', Edit: 'write', Write: 'write', NotebookEdit: 'write' };
|
|
2249
|
+
const __action = __toolToAction[toolName];
|
|
2250
|
+
if (__apiKey && __action) {
|
|
2251
|
+
const __client = new HTTPHarnessClient({
|
|
2252
|
+
baseUrl:
|
|
2253
|
+
process.env.ARIA_RUNTIME_URL ||
|
|
2254
|
+
process.env.ARIA_HIVE_RUNTIME_URL ||
|
|
2255
|
+
process.env.ARIA_HARNESS_BASE_URL ||
|
|
2256
|
+
process.env.ARIA_HARNESS_URL ||
|
|
2257
|
+
RUNTIME_BASE_URL,
|
|
2258
|
+
apiKey: __apiKey,
|
|
2259
|
+
});
|
|
2260
|
+
const __target = JSON.stringify(toolInput || {}).slice(0, 500);
|
|
2261
|
+
const __check = await __client.checkAction(__action, __target);
|
|
2262
|
+
if (__check && __check.allowed === false) {
|
|
2263
|
+
const __reason = `Aria substrate checkAction DENIED for ${toolName}/${__action}: ${__check.reason || 'no reason given'}. Required gates: [${(__check.requiredGates || []).join(', ')}].
|
|
2264
|
+
|
|
2265
|
+
The substrate's contract gate refused this action. Local doctrine gates passed (cognition lenses, binding plan, drift) but the substrate sees a downstream contract that disallows this tool call. Address the substrate's reason above before retrying.`;
|
|
2266
|
+
audit(`block-substrate-checkAction ${toolName.toLowerCase()} action=${__action} reason="${(__check.reason || '').slice(0, 80)}"`, cmdPreview);
|
|
2267
|
+
pushDecision('block', `substrate checkAction denied: ${(__check.reason || 'unspecified').slice(0, 100)}`);
|
|
2268
|
+
emitBlock(__reason, { source: 'pre-tool/final' });
|
|
2269
|
+
process.exit(2);
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
} catch (err) {
|
|
2273
|
+
// SDK call failure is non-blocking — gate degrades to local-only doctrine.
|
|
2274
|
+
// The failure is recorded so a fleet probe can detect substrate-side
|
|
2275
|
+
// contract gate outages.
|
|
2276
|
+
console.warn(`[pre-tool-gate] substrate checkAction failed: ${err && err.message ? err.message : err}`);
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
// ── Hive session-lock check ───────────────────────────────────────────────────
|
|
2280
|
+
// For Edit/Write/NotebookEdit: check file_path against hive_session_locks.
|
|
2281
|
+
// For Bash with file-mutating verbs (rm, mv, sed -i, awk, tee, cp, chmod,
|
|
2282
|
+
// truncate, install): extract the file argument and check it.
|
|
2283
|
+
//
|
|
2284
|
+
// If an active lock exists from a DIFFERENT session, BLOCK with coordination
|
|
2285
|
+
// instructions. Fail-open only on network error (endpoint unreachable).
|
|
2286
|
+
//
|
|
2287
|
+
// Per feedback_no_graceful_degradation.md: parse errors and non-network
|
|
2288
|
+
// failures surface in the block reason — they are not silently ignored.
|
|
2289
|
+
// Per feedback_no_timeouts_doctrine.md: no AbortSignal or setTimeout.
|
|
2290
|
+
(async function checkHiveSessionLock() {
|
|
2291
|
+
// Determine the file path to check
|
|
2292
|
+
let _lockCheckPath = '';
|
|
2293
|
+
|
|
2294
|
+
if (toolName === 'Edit' || toolName === 'Write' || toolName === 'NotebookEdit') {
|
|
2295
|
+
_lockCheckPath = filePath;
|
|
2296
|
+
} else if (toolName === 'Bash') {
|
|
2297
|
+
// Destructive bash verbs that mutate files — extract the file argument
|
|
2298
|
+
const _mutatingVerbRx = /^(rm|mv|cp|sed\s+-i|awk\s+.*-i|tee|truncate|install|chmod|chown)\b/;
|
|
2299
|
+
if (_mutatingVerbRx.test(cmd.trim())) {
|
|
2300
|
+
// Extract the first file-path argument: look for something with /
|
|
2301
|
+
const _pathMatch = cmd.match(/\s+((?:\/|~\/|\.\.?\/|[\w.-]+\/)[^\s'"]+)/);
|
|
2302
|
+
if (_pathMatch) _lockCheckPath = _pathMatch[1];
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2306
|
+
if (!_lockCheckPath) return; // no file target — skip lock check
|
|
2307
|
+
|
|
2308
|
+
const _soulUrl =
|
|
2309
|
+
process.env.ARIA_HIVE_RUNTIME_URL ||
|
|
2310
|
+
process.env.ARIA_SOUL_URL ||
|
|
2311
|
+
process.env.ARIA_HARNESS_BASE_URL ||
|
|
2312
|
+
process.env.ARIA_HARNESS_URL ||
|
|
2313
|
+
'https://harness.ariasos.com';
|
|
2314
|
+
const _harnessToken = process.env.ARIA_HARNESS_TOKEN || '';
|
|
2315
|
+
|
|
2316
|
+
let _lockResp;
|
|
2317
|
+
try {
|
|
2318
|
+
const _params = new URLSearchParams({ file_path: _lockCheckPath });
|
|
2319
|
+
_lockResp = await fetch(`${_soulUrl}/api/hive/session-lock?${_params}`, {
|
|
2320
|
+
method: 'GET',
|
|
2321
|
+
headers: {
|
|
2322
|
+
'Content-Type': 'application/json',
|
|
2323
|
+
...(_harnessToken ? { Authorization: `Bearer ${_harnessToken}` } : {}),
|
|
2324
|
+
},
|
|
2325
|
+
});
|
|
2326
|
+
} catch (_netErr) {
|
|
2327
|
+
// Endpoint unreachable — fail-open. Lock check is a coordination layer,
|
|
2328
|
+
// not a safety hard-stop. Infra-down must not block all development.
|
|
2329
|
+
audit(`allow-lock-check-network-error path=${_lockCheckPath.slice(0, 80)}`, cmdPreview);
|
|
2330
|
+
return;
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
if (!_lockResp.ok) {
|
|
2334
|
+
// Non-200 — route may not be deployed yet. Fail-open with audit record.
|
|
2335
|
+
audit(`allow-lock-check-http-error status=${_lockResp.status} path=${_lockCheckPath.slice(0, 80)}`, cmdPreview);
|
|
2336
|
+
return;
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
// Per feedback_no_graceful_degradation.md: JSON parse error is surfaced,
|
|
2340
|
+
// not swallowed. A malformed response IS a defect that must be visible.
|
|
2341
|
+
const _lockData = await _lockResp.json();
|
|
2342
|
+
const _activeLocks = Array.isArray(_lockData?.locks) ? _lockData.locks : [];
|
|
2343
|
+
|
|
2344
|
+
// Filter to locks from a DIFFERENT session
|
|
2345
|
+
const _conflictingLocks = _activeLocks.filter(
|
|
2346
|
+
(l) => l.session_id && String(l.session_id) !== String(sessionId),
|
|
2347
|
+
);
|
|
2348
|
+
|
|
2349
|
+
if (_conflictingLocks.length === 0) {
|
|
2350
|
+
audit(`allow-lock-clear path=${_lockCheckPath.slice(0, 80)}`, cmdPreview);
|
|
2351
|
+
return; // no conflict — proceed
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
// ── Auto-post coordination message to each conflicting session ───────────
|
|
2355
|
+
// Per hive-session-coordination doctrine (memory:feedback_hive_session_coordination.md):
|
|
2356
|
+
// when a lock conflict is detected, the gate AUTOMATICALLY posts a
|
|
2357
|
+
// lock_conflict_request session-message to each conflicting session so they see
|
|
2358
|
+
// the inbound coordination request in their next turn's HIVE_SESSION_INBOX block.
|
|
2359
|
+
// The model never has to manually post — the gate is the structural binding.
|
|
2360
|
+
//
|
|
2361
|
+
// Per feedback_no_timeouts_doctrine.md: no AbortSignal or setTimeout.
|
|
2362
|
+
// Per feedback_no_graceful_degradation.md: message-post failures are logged
|
|
2363
|
+
// loudly to stderr; they do NOT silently degrade — the block still fires regardless.
|
|
2364
|
+
const _autoMessageIds = [];
|
|
2365
|
+
for (const _conflict of _conflictingLocks) {
|
|
2366
|
+
try {
|
|
2367
|
+
const _msgId = `lock-conflict-${sessionId}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`;
|
|
2368
|
+
const _requestedAt = new Date().toISOString();
|
|
2369
|
+
const _msgBody = {
|
|
2370
|
+
coordination_protocol: 'aria-hive-lock-conflict/v2',
|
|
2371
|
+
coordination_key: `file:${_lockCheckPath}`,
|
|
2372
|
+
file_path: _lockCheckPath,
|
|
2373
|
+
requesting_session_id: sessionId,
|
|
2374
|
+
requesting_client_id: 'aria-connector',
|
|
2375
|
+
requesting_surface: 'pre-tool-gate',
|
|
2376
|
+
intent_summary: `Session ${sessionId} attempted to edit ${_lockCheckPath} via ${toolName} but found your active lock. Requesting coordination — please release when done.`,
|
|
2377
|
+
requested_at: _requestedAt,
|
|
2378
|
+
tool_name: toolName,
|
|
2379
|
+
command_preview: cmdPreview,
|
|
2380
|
+
file_claims: [{ path: _lockCheckPath, intent: 'edit' }],
|
|
2381
|
+
target_lock: {
|
|
2382
|
+
lock_id: _conflict.lock_id ?? null,
|
|
2383
|
+
session_id: _conflict.session_id ?? null,
|
|
2384
|
+
client_id: _conflict.client_id ?? null,
|
|
2385
|
+
surface: _conflict.surface ?? null,
|
|
2386
|
+
locked_at: _conflict.locked_at ?? null,
|
|
2387
|
+
expires_at: _conflict.expires_at ?? null,
|
|
2388
|
+
},
|
|
2389
|
+
};
|
|
2390
|
+
const _msgResp = await fetch(`${_soulUrl}/api/hive/session-message`, {
|
|
2391
|
+
method: 'POST',
|
|
2392
|
+
headers: {
|
|
2393
|
+
'Content-Type': 'application/json',
|
|
2394
|
+
...(_harnessToken ? { Authorization: `Bearer ${_harnessToken}` } : {}),
|
|
2395
|
+
},
|
|
2396
|
+
body: JSON.stringify({
|
|
2397
|
+
message_id: _msgId,
|
|
2398
|
+
from_session_id: sessionId,
|
|
2399
|
+
to_session_id: _conflict.session_id,
|
|
2400
|
+
topic: 'lock_conflict_request',
|
|
2401
|
+
body: _msgBody,
|
|
2402
|
+
}),
|
|
2403
|
+
});
|
|
2404
|
+
if (_msgResp.ok) {
|
|
2405
|
+
_autoMessageIds.push({ session_id: _conflict.session_id, message_id: _msgId });
|
|
2406
|
+
audit(`auto-msg-sent lock-conflict to=${_conflict.session_id} msg=${_msgId} path=${_lockCheckPath.slice(0, 60)}`, cmdPreview);
|
|
2407
|
+
} else {
|
|
2408
|
+
const _errText = await _msgResp.text().catch(() => 'unreadable');
|
|
2409
|
+
process.stderr.write(
|
|
2410
|
+
`[aria-pre-tool-gate] WARN auto-message to session ${_conflict.session_id} failed: HTTP ${_msgResp.status} ${_errText.slice(0, 200)}\n`,
|
|
2411
|
+
);
|
|
2412
|
+
audit(`auto-msg-failed lock-conflict to=${_conflict.session_id} status=${_msgResp.status} path=${_lockCheckPath.slice(0, 60)}`, cmdPreview);
|
|
2413
|
+
}
|
|
2414
|
+
} catch (_msgErr) {
|
|
2415
|
+
// Network error posting auto-message — log loudly per no-graceful-degradation doctrine.
|
|
2416
|
+
// Block still fires; auto-message is additive signal, not a safety gate.
|
|
2417
|
+
process.stderr.write(
|
|
2418
|
+
`[aria-pre-tool-gate] WARN auto-message to session ${_conflict.session_id} threw: ${_msgErr instanceof Error ? _msgErr.message : String(_msgErr)}\n`,
|
|
2419
|
+
);
|
|
2420
|
+
audit(`auto-msg-error lock-conflict to=${_conflict.session_id} path=${_lockCheckPath.slice(0, 60)}`, cmdPreview);
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
const _conflictDetails = _conflictingLocks.map((l) => {
|
|
2425
|
+
const _ago = l.locked_at ? `since ${l.locked_at}` : '';
|
|
2426
|
+
const _expires = l.expires_at ? `, expires ${l.expires_at}` : '';
|
|
2427
|
+
const _who = l.client_id ? ` (client: ${l.client_id})` : '';
|
|
2428
|
+
const _surface = l.surface ? ` [${l.surface}]` : '';
|
|
2429
|
+
const _sentMsg = _autoMessageIds.find((m) => m.session_id === l.session_id);
|
|
2430
|
+
const _msgNote = _sentMsg
|
|
2431
|
+
? ` [auto-coordination message posted: ${_sentMsg.message_id}]`
|
|
2432
|
+
: ' [auto-message failed — coordinate manually]';
|
|
2433
|
+
return ` - Session ${l.session_id}${_who}${_surface} holds lock on ${l.file_path} ${_ago}${_expires}${_msgNote}`;
|
|
2434
|
+
}).join('\n');
|
|
2435
|
+
|
|
2436
|
+
const _autoMsgSummary = _autoMessageIds.length > 0
|
|
2437
|
+
? `\nAuto-coordination: gate posted lock_conflict_request message(s) to ${_autoMessageIds.map((m) => `session ${m.session_id} (msg: ${m.message_id})`).join(', ')}. They will see this inbound on their next turn via [HIVE_SESSION_INBOX].`
|
|
2438
|
+
: '\nAuto-coordination message could not be delivered (see stderr). Coordinate manually via POST /api/hive/session-message.';
|
|
2439
|
+
|
|
2440
|
+
const _lockBlockReason = `Hive session-lock conflict: another session holds an active lock on this file.
|
|
2441
|
+
|
|
2442
|
+
File: ${_lockCheckPath}
|
|
2443
|
+
|
|
2444
|
+
Conflicting locks:
|
|
2445
|
+
${_conflictDetails}
|
|
2446
|
+
${_autoMsgSummary}
|
|
2447
|
+
|
|
2448
|
+
Resolution:
|
|
2449
|
+
1. A lock_conflict_request message was automatically posted to the lock-holding session. Wait for them to see it.
|
|
2450
|
+
2. They release via: aria hive lock release --lock-id <ID> OR DELETE /api/hive/session-lock.
|
|
2451
|
+
3. No automatic timeout-based release — explicit release only (memory:feedback_no_timeouts_doctrine.md).
|
|
2452
|
+
4. Retry this action — the gate allows when no conflicting lock exists.
|
|
2453
|
+
|
|
2454
|
+
Per memory:feedback_hive_session_coordination.md: blind-edit on the same file from concurrent sessions
|
|
2455
|
+
causes merge conflicts and state divergence. Explicit coordination is the only safe path.`;
|
|
2456
|
+
|
|
2457
|
+
audit(`block-hive-lock-conflict path=${_lockCheckPath.slice(0, 80)} conflicting_sessions=${_conflictingLocks.map((l) => l.session_id).join(',')} auto_msgs=${_autoMessageIds.length}`, cmdPreview);
|
|
2458
|
+
pushDecision('block', `hive session-lock conflict on ${_lockCheckPath.slice(0, 80)}`);
|
|
2459
|
+
console.log(JSON.stringify({
|
|
2460
|
+
decision: 'block',
|
|
2461
|
+
reason: buildForceRedoActionReason(_lockBlockReason, { source: 'pre-tool/hive-lock-conflict', tool: toolName || 'unknown' }),
|
|
2462
|
+
hookSpecificOutput: {
|
|
2463
|
+
hookEventName: 'PreToolUse',
|
|
2464
|
+
conflicting_locks: _conflictingLocks,
|
|
2465
|
+
auto_coordination_messages: _autoMessageIds,
|
|
2466
|
+
recovery: {
|
|
2467
|
+
action: 'wait_for_lock_release_then_retry',
|
|
2468
|
+
file_path: _lockCheckPath,
|
|
2469
|
+
conflicting_session_ids: _conflictingLocks.map((l) => l.session_id),
|
|
2470
|
+
auto_message_posted: _autoMessageIds.length > 0,
|
|
2471
|
+
send_message_endpoint: '/api/hive/session-message',
|
|
2472
|
+
release_lock_endpoint: '/api/hive/session-lock',
|
|
2473
|
+
},
|
|
2474
|
+
},
|
|
2475
|
+
}));
|
|
2476
|
+
process.exit(2);
|
|
2477
|
+
})();
|
|
2478
|
+
|
|
2479
|
+
// Non-trivial action with cognition AND (binding-allowed OR binding-disabled) AND substrate-cleared — allow.
|
|
2480
|
+
audit(`allow-cognition ${toolName.toLowerCase()} lenses=${lensCount} via=${cognitionSource}`, cmdPreview);
|
|
2481
|
+
pushDecision('allow', `${toolName.toLowerCase()} with ${lensCount} lenses (${cognitionSource})`);
|
|
2482
|
+
process.exit(0);
|