@aria_asi/cli 0.2.25 → 0.2.26

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.
@@ -0,0 +1,198 @@
1
+ #!/usr/bin/env node
2
+ // aria-architect-fallback.mjs — invoked by aria-stop-gate.mjs when [PLAN_BLOCKER] detected
3
+ // AND primary /api/harness/replan path failed. Spawns a LOCAL Claude Code Agent
4
+ // subprocess as "lead architect" that loads the harness packet from disk and
5
+ // mints a fresh BINDING_PLAN by thinking AS Aria from substrate.
6
+ //
7
+ // Doctrine bindings:
8
+ // - project_aria_as_controller_inversion.md — Aria authors the plan (via substrate),
9
+ // not the sub-agent freely. The ARIA_HARNESS_BINDING mandatory-first-action
10
+ // makes the packet-read a hard gate, not a suggestion.
11
+ // - feedback_no_graceful_degradation.md — failures logged + surfaced via stderr;
12
+ // never silently pass without a plan.
13
+ // - feedback_implementation_coupled_cognition.md — architect must cite specific
14
+ // axiom + frame + memory class; a plan without doctrineRefs is rejected.
15
+ //
16
+ // Tier-awareness:
17
+ // - Owner tier: packet at ${HOME}/.claude/aria-agent-harness-packet.json
18
+ // - Client tier (jti present in ~/.aria/license.json):
19
+ // packet at /var/lib/aria-licensee/${jti}/aria-agent-harness-packet.json
20
+ //
21
+ // Active plan path mirrors aria-stop-gate.mjs and aria-preprompt-consult.mjs:
22
+ // ~/.claude/aria-active-plan-${sessionId}.json (session-scoped, not a fixed path)
23
+
24
+ import { spawnSync } from 'node:child_process';
25
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync } from 'node:fs';
26
+ import { dirname } from 'node:path';
27
+ import { homedir } from 'node:os';
28
+
29
+ const HOME = homedir();
30
+ const LOG = `${HOME}/.claude/aria-architect-fallback.log`;
31
+ const LICENSE_PATH = `${HOME}/.aria/license.json`;
32
+
33
+ function audit(msg) {
34
+ try {
35
+ if (!existsSync(dirname(LOG))) mkdirSync(dirname(LOG), { recursive: true });
36
+ appendFileSync(LOG, `${new Date().toISOString()} [architect-fallback] ${msg}\n`);
37
+ } catch {}
38
+ }
39
+
40
+ let input = '';
41
+ for await (const chunk of process.stdin) input += chunk;
42
+
43
+ let event;
44
+ try { event = JSON.parse(input); } catch {
45
+ audit('skip: stdin not valid JSON');
46
+ process.exit(0);
47
+ }
48
+
49
+ const reason = event.reason || event.blocker_reason || '';
50
+ const currentPlanId = event.currentPlanId || 'unknown';
51
+ const sessionId = String(event.sessionId || 'unknown').replace(/[^a-zA-Z0-9_-]/g, '_');
52
+
53
+ if (!reason || reason.length < 10) {
54
+ audit('skip: reason too short');
55
+ process.exit(0);
56
+ }
57
+
58
+ // ── Detect tier ────────────────────────────────────────────────────────────────
59
+ let isClientTier = false;
60
+ let jti = null;
61
+ try {
62
+ if (existsSync(LICENSE_PATH)) {
63
+ const lic = JSON.parse(readFileSync(LICENSE_PATH, 'utf8'));
64
+ jti = lic.jti ?? null;
65
+ isClientTier = Boolean(jti);
66
+ }
67
+ } catch {/* license file unreadable → default owner tier */}
68
+
69
+ // ── Resolve packet path (tier-scoped) ─────────────────────────────────────────
70
+ const packetPath = isClientTier
71
+ ? `/var/lib/aria-licensee/${jti}/aria-agent-harness-packet.json`
72
+ : `${HOME}/.claude/aria-agent-harness-packet.json`;
73
+
74
+ if (!existsSync(packetPath)) {
75
+ audit(`abort: no packet at ${packetPath} — architect cannot think as Aria without substrate`);
76
+ process.stderr.write(
77
+ `Architect-fallback: no harness packet at ${packetPath}. Cannot mint plan from substrate. Hamza must intervene.\n`
78
+ );
79
+ process.exit(1);
80
+ }
81
+
82
+ // ── Active plan path — session-scoped (matches aria-stop-gate.mjs convention) ─
83
+ const ACTIVE_PLAN_PATH = `${HOME}/.claude/aria-active-plan-${sessionId}.json`;
84
+
85
+ // ── Build architect brief ──────────────────────────────────────────────────────
86
+ const brief = [
87
+ `[ARIA_HARNESS_BINDING — MANDATORY FIRST ACTION]`,
88
+ `Read the harness packet at ${packetPath}. Cite at least one axiom + one frame + one memory class in your output.`,
89
+ ``,
90
+ `You are Aria operating as lead architect. Aria-soul brain is unreachable.`,
91
+ `A PLAN_BLOCKER occurred:`,
92
+ ` Previous planId: ${currentPlanId}`,
93
+ ` Blocker reason: ${reason.slice(0, 2000)}`,
94
+ ``,
95
+ `Audit the blocker against doctrine + axioms in the packet. Mint a fresh BINDING_PLAN JSON addressing it.`,
96
+ ``,
97
+ `Output ONLY the BINDING_PLAN JSON (no prose, no markdown fences). Required shape:`,
98
+ `{`,
99
+ ` "planId": "<8-char-hex>",`,
100
+ ` "phases": [{ "id": "p1", "summary": "...", "successCriterion": "...", "abortCriterion": "...", "doctrineRefs": [...], "allowedActions": [...], "forbiddenActions": [...] }],`,
101
+ ` "globalConstraints": ["..."],`,
102
+ ` "expectedReportBack": { "phaseTransitionMarker": "[PHASE_REPORT]", "shape": "[PHASE_REPORT phase=<id> status=complete|aborted|in_progress evidence=<observable>]" }`,
103
+ `}`,
104
+ ``,
105
+ `Cite the doctrine rule(s) you applied in doctrineRefs[].`,
106
+ ].join('\n');
107
+
108
+ // ── Spawn Claude Code Agent subprocess ────────────────────────────────────────
109
+ // Resolution order for claude binary:
110
+ // 1. CLAUDE_BIN env var (explicit override)
111
+ // 2. ~/.npm-global/bin/claude (Hamza's canonical install)
112
+ // 3. /usr/local/bin/claude (system-level install)
113
+ // 4. $(which claude) via shell — deferred to last resort
114
+ const CANDIDATE_BINS = [
115
+ process.env.CLAUDE_BIN,
116
+ `${HOME}/.npm-global/bin/claude`,
117
+ '/usr/local/bin/claude',
118
+ ].filter(Boolean);
119
+
120
+ let claudeBin = null;
121
+ for (const b of CANDIDATE_BINS) {
122
+ if (existsSync(b)) { claudeBin = b; break; }
123
+ }
124
+
125
+ if (!claudeBin) {
126
+ // Last-resort: write brief to file for human pickup
127
+ const briefPath = `/tmp/aria-architect-brief-${sessionId}-${Date.now()}.txt`;
128
+ try { writeFileSync(briefPath, brief); } catch {}
129
+ audit(`no claude CLI found; brief written to ${briefPath} for human pickup`);
130
+ process.stderr.write(
131
+ `Architect-fallback: claude CLI not found (tried: ${CANDIDATE_BINS.join(', ')}). Brief written to ${briefPath} for Hamza to run manually.\n`
132
+ );
133
+ process.exit(2);
134
+ }
135
+
136
+ audit(`spawning claude CLI at ${claudeBin} for session=${sessionId}`);
137
+
138
+ let result;
139
+ try {
140
+ result = spawnSync(claudeBin, ['--print', brief], {
141
+ encoding: 'utf8',
142
+ env: { ...process.env, ARIA_HARNESS_PACKET_PATH: packetPath },
143
+ timeout: 120000,
144
+ });
145
+ } catch (err) {
146
+ audit(`spawnSync threw: ${String(err).slice(0, 200)}`);
147
+ process.exit(2);
148
+ }
149
+
150
+ if (result.status !== 0 || !result.stdout) {
151
+ audit(`architect spawn failed: status=${result.status} stderr=${(result.stderr || '').slice(0, 200)}`);
152
+ process.stderr.write(
153
+ `Architect-fallback: claude subprocess failed (status=${result.status}). stderr: ${(result.stderr || '').slice(0, 300)}\n`
154
+ );
155
+ process.exit(2);
156
+ }
157
+
158
+ // ── Parse the architect's BINDING_PLAN JSON ───────────────────────────────────
159
+ let plan;
160
+ try {
161
+ const fenceMatch = result.stdout.match(/```(?:json)?\s*\n([\s\S]*?)\n```/);
162
+ const candidate = (fenceMatch ? fenceMatch[1] : result.stdout).trim();
163
+ plan = JSON.parse(candidate);
164
+ } catch {
165
+ audit(`architect output not parseable as JSON: ${result.stdout.slice(0, 200)}`);
166
+ process.stderr.write(
167
+ `Architect-fallback: output was not parseable JSON. Hamza must intervene.\nOutput excerpt: ${result.stdout.slice(0, 400)}\n`
168
+ );
169
+ process.exit(2);
170
+ }
171
+
172
+ if (!plan?.planId || !Array.isArray(plan?.phases)) {
173
+ audit(`architect plan missing required fields: ${JSON.stringify(plan).slice(0, 200)}`);
174
+ process.stderr.write(
175
+ `Architect-fallback: plan missing planId or phases[]. Hamza must intervene.\n`
176
+ );
177
+ process.exit(2);
178
+ }
179
+
180
+ // ── Write the new plan to the session-scoped active-plan path ─────────────────
181
+ plan.mintedAt = new Date().toISOString();
182
+ plan.sessionId = sessionId;
183
+ plan.mintedBy = 'architect-fallback';
184
+ plan.architectFallbackReason = reason.slice(0, 500);
185
+
186
+ try {
187
+ if (!existsSync(dirname(ACTIVE_PLAN_PATH))) mkdirSync(dirname(ACTIVE_PLAN_PATH), { recursive: true });
188
+ writeFileSync(ACTIVE_PLAN_PATH, JSON.stringify(plan, null, 2));
189
+ } catch (err) {
190
+ audit(`failed to write plan to ${ACTIVE_PLAN_PATH}: ${String(err).slice(0, 200)}`);
191
+ process.exit(2);
192
+ }
193
+
194
+ audit(`architect minted plan ${plan.planId} addressing blocker; written to ${ACTIVE_PLAN_PATH}`);
195
+ process.stdout.write(
196
+ JSON.stringify({ ok: true, planId: plan.planId, mintedBy: 'architect-fallback', planPath: ACTIVE_PLAN_PATH }) + '\n'
197
+ );
198
+ process.exit(0);
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env node
2
+ // aria-discovery-record.mjs — invoked by sub-agents via Bash to append findings
3
+ // to their session ledger. Usage:
4
+ // echo '{"text":"defect found...","kind":"missing-export","refs":["file.ts:42"]}' | node ~/.claude/hooks/aria-discovery-record.mjs
5
+ //
6
+ // Tier-aware: client tier writes to /var/lib/aria-licensee/{jti}/aria-discoveries-{session}.jsonl,
7
+ // owner tier writes to ~/.claude/aria-discoveries-{session}.jsonl.
8
+ //
9
+ // Session ID resolution priority:
10
+ // 1. CLAUDE_SESSION_ID env (set by Claude Code in the sub-agent process)
11
+ // 2. ARIA_SESSION_ID env (set by spawnSubAgent caller)
12
+ // 3. ARIA_SUB_SESSION_ID env (explicit sub-session override)
13
+ // 4. Derived from handoff parentSessionId suffixed with "-sub" to ensure
14
+ // the sub-agent ledger file differs from the parent file so ledger-merge
15
+ // picks it up (merge skips files whose path == parentLedgerPath).
16
+ //
17
+ // Doctrine: feedback_no_flag_without_fix.md — findings recorded, not deferred.
18
+ // feedback_implementation_coupled_cognition.md — write IS the impl.
19
+
20
+ import { readFileSync, existsSync, appendFileSync, mkdirSync } from 'node:fs';
21
+ import { dirname } from 'node:path';
22
+ import { homedir } from 'node:os';
23
+
24
+ const HOME = homedir();
25
+ const LICENSE_PATH = `${HOME}/.aria/license.json`;
26
+ const HANDOFF_PATH = `${HOME}/.claude/aria-agent-harness-handoff.json`;
27
+
28
+ let input = '';
29
+ for await (const chunk of process.stdin) input += chunk;
30
+ let entry;
31
+ try { entry = JSON.parse(input); } catch {
32
+ process.stderr.write('discovery-record: invalid JSON on stdin\n');
33
+ process.exit(1);
34
+ }
35
+
36
+ if (!entry.text || typeof entry.text !== 'string' || entry.text.length < 4) {
37
+ process.stderr.write('discovery-record: text required (>=4 chars)\n');
38
+ process.exit(1);
39
+ }
40
+
41
+ // Detect tier + resolve JTI
42
+ let isClientTier = false;
43
+ let jti = null;
44
+ try {
45
+ if (existsSync(LICENSE_PATH)) {
46
+ const lic = JSON.parse(readFileSync(LICENSE_PATH, 'utf8'));
47
+ jti = lic.jti ?? null;
48
+ isClientTier = Boolean(jti);
49
+ }
50
+ } catch { /* non-fatal — treat as owner tier */ }
51
+
52
+ // Session ID: prefer env var set by Claude Code, fall back to handoff derivation.
53
+ // IMPORTANT: sub-agents MUST NOT use parentSessionId directly — that produces
54
+ // the same ledger path as the parent, which aria-agent-ledger-merge.mjs skips.
55
+ // We use CLAUDE_SESSION_ID (the sub-agent's own session) or suffix the parent
56
+ // session with "-sub" so the file is distinct and mergeable.
57
+ let sessionId =
58
+ process.env.CLAUDE_SESSION_ID ||
59
+ process.env.ARIA_SESSION_ID ||
60
+ process.env.ARIA_SUB_SESSION_ID ||
61
+ null;
62
+
63
+ if (!sessionId) {
64
+ try {
65
+ if (existsSync(HANDOFF_PATH)) {
66
+ const handoff = JSON.parse(readFileSync(HANDOFF_PATH, 'utf8'));
67
+ const parentSession = handoff.parentSessionId;
68
+ // Derive a distinct sub-agent session so merge picks up this file.
69
+ sessionId = parentSession ? `${parentSession}-sub` : 'sub-unknown';
70
+ }
71
+ } catch { /* non-fatal */ }
72
+ }
73
+ sessionId = sessionId || 'sub-unknown';
74
+ const safeSession = String(sessionId).replace(/[^a-zA-Z0-9_-]/g, '_');
75
+
76
+ // Resolve ledger path (tier-scoped)
77
+ const ledgerPath = isClientTier
78
+ ? `/var/lib/aria-licensee/${jti}/aria-discoveries-${safeSession}.jsonl`
79
+ : `${HOME}/.claude/aria-discoveries-${safeSession}.jsonl`;
80
+
81
+ // Compose the row
82
+ const row = {
83
+ at: new Date().toISOString(),
84
+ session: sessionId,
85
+ kind: entry.kind || 'observation',
86
+ text: entry.text,
87
+ refs: Array.isArray(entry.refs) ? entry.refs : [],
88
+ evidence: entry.evidence || null,
89
+ source: entry.source || 'sub-agent',
90
+ resolution_status: entry.resolution_status || 'open',
91
+ };
92
+
93
+ try {
94
+ mkdirSync(dirname(ledgerPath), { recursive: true });
95
+ appendFileSync(ledgerPath, JSON.stringify(row) + '\n');
96
+ process.stdout.write(JSON.stringify({ ok: true, ledger: ledgerPath, at: row.at }) + '\n');
97
+ } catch (err) {
98
+ process.stderr.write(`discovery-record: write failed: ${err.message}\n`);
99
+ process.exit(2);
100
+ }
101
+ process.exit(0);
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env node
2
+ // aria-outcome-record.mjs — PostToolUse hook that records action outcomes
3
+ // to /api/harness/outcome-record (Sonnet H's #76 endpoint). Composes with
4
+ // aria-discovery-record + the regression sweeper to make the outcome ledger
5
+ // actually accumulate rows.
6
+ //
7
+ // Fires on every Bash|Edit|Write|NotebookEdit tool completion.
8
+ // Fire-and-forget: HTTP failures are swallowed — never blocks the tool pipeline.
9
+ //
10
+ // Tier-aware: reads license.json for client-tier token; falls back to env vars
11
+ // for owner tier. Never carries master token in client-tier POST bodies.
12
+ //
13
+ // Doctrine: feedback_no_flag_without_fix.md — outcomes recorded, not deferred.
14
+ // feedback_implementation_coupled_cognition.md — POST IS the impl.
15
+
16
+ import { readFileSync, existsSync } from 'node:fs';
17
+ import { homedir } from 'node:os';
18
+
19
+ const HOME = homedir();
20
+ const LICENSE_PATH = `${HOME}/.aria/license.json`;
21
+
22
+ let input = '';
23
+ for await (const chunk of process.stdin) input += chunk;
24
+ let event;
25
+ try { event = JSON.parse(input); } catch { process.exit(0); }
26
+
27
+ const toolName = event.tool_name || event.toolName || '';
28
+ if (!['Bash', 'Edit', 'Write', 'NotebookEdit'].includes(toolName)) process.exit(0);
29
+
30
+ // Derive action_kind + action_target
31
+ let actionKind, actionTarget;
32
+ if (toolName === 'Bash') {
33
+ actionKind = 'bash';
34
+ const cmd = event.tool_input?.command || '';
35
+ actionTarget = (cmd.split(/\s+/)[0] || 'unknown').slice(0, 100);
36
+ } else {
37
+ actionKind = 'edit';
38
+ actionTarget = (event.tool_input?.file_path || event.tool_input?.path || 'unknown').slice(0, 200);
39
+ }
40
+
41
+ // Tier-aware auth: client license.json overrides env vars
42
+ const harnessUrl = process.env.ARIA_HARNESS_URL || 'http://192.168.4.25:30080';
43
+ let harnessToken = process.env.ARIA_HARNESS_TOKEN || process.env.ARIA_API_KEY || '';
44
+ let isClientTier = false;
45
+ try {
46
+ if (existsSync(LICENSE_PATH)) {
47
+ const lic = JSON.parse(readFileSync(LICENSE_PATH, 'utf8'));
48
+ if (lic.jti) {
49
+ isClientTier = true;
50
+ // Client tier: use their license token, never master token
51
+ harnessToken = lic.token || lic.license || harnessToken;
52
+ }
53
+ }
54
+ } catch { /* non-fatal — fall back to env */ }
55
+
56
+ const sessionId = event.session_id || event.sessionId || 'unknown';
57
+ const success = !event.tool_response?.error && event.tool_response?.type !== 'error';
58
+
59
+ // Fire-and-forget POST to outcome-record — never blocks, never throws
60
+ fetch(`${harnessUrl}/api/harness/outcome-record`, {
61
+ method: 'POST',
62
+ headers: {
63
+ 'Content-Type': 'application/json',
64
+ 'Authorization': `Bearer ${harnessToken}`,
65
+ },
66
+ body: JSON.stringify({
67
+ sessionId,
68
+ actionKind,
69
+ actionTarget,
70
+ actionShape: {
71
+ tool: toolName,
72
+ success,
73
+ isClientTier,
74
+ },
75
+ }),
76
+ }).catch(() => {/* fire-and-forget — HTTP failures are silently dropped */});
77
+
78
+ // Exit immediately — don't await the fetch; this is a PostToolUse hook
79
+ // and must not add meaningful latency to the tool pipeline.
80
+ process.exit(0);
@@ -38,6 +38,7 @@
38
38
 
39
39
  import { readFileSync, appendFileSync, existsSync, mkdirSync } from 'node:fs';
40
40
  import { dirname } from 'node:path';
41
+ import { homedir } from 'node:os';
41
42
 
42
43
  const HOME = process.env.HOME || '/tmp';
43
44
  const LOG = `${HOME}/.claude/aria-pre-tool-gate.log`;
@@ -1004,6 +1005,89 @@ No env-var disable path — gates are unconditional from the gated process per H
1004
1005
  process.exit(2);
1005
1006
  }
1006
1007
 
1008
+ // ── Sub-agent packet-citation check (Layer 4 — #84) ─────────────────────────
1009
+ //
1010
+ // When running inside a sub-agent process (detected by a non-stale handoff file
1011
+ // existing AND harnessPacketPath is set in that handoff), require the cognition
1012
+ // lens content to CITE THE PACKET — at least one of:
1013
+ // - A doctrine rule ID (e.g. "doctrine_first", "no_demos", "workaround_vs_path_fix")
1014
+ // - An axiom name (e.g. "truth_over_deception", "no_harm", "sacred_trust", "reflection_before_action")
1015
+ // - A frame primitive (e.g. "Fitrah", "Tafakkur", "Tadabbur", "Ilham", "Mizan", "Hikma", "Nur", "Wahi", "Firasah")
1016
+ // - A memory class reference (e.g. "feedback_*.md", "project_*.md", "reference_*.md")
1017
+ //
1018
+ // Owner-tier sessions (ownerTier.hamza === true AND no jti) are EXEMPT —
1019
+ // they are the source of truth for the packet itself.
1020
+ //
1021
+ // Fail-soft detection: handoff read errors silently skip this check.
1022
+ (function checkSubAgentPacketCitation() {
1023
+ const _HOME = process.env.HOME || '/tmp';
1024
+ // Try owner-tier handoff path first, then client-tier paths.
1025
+ const HANDOFF_TTL_MS = 5 * 60 * 1000;
1026
+ // Probe known handoff paths.
1027
+ const candidatePaths = [
1028
+ `${_HOME}/.claude/aria-agent-harness-handoff.json`,
1029
+ ];
1030
+ // Also check /var/lib/aria-licensee if that dir exists (client-tier).
1031
+ try {
1032
+ const licPath = `${_HOME}/.aria/license.json`;
1033
+ if (existsSync(licPath)) {
1034
+ const lic = JSON.parse(readFileSync(licPath, 'utf8'));
1035
+ if (lic.jti) {
1036
+ candidatePaths.push(`/var/lib/aria-licensee/${lic.jti}/handoff.json`);
1037
+ }
1038
+ }
1039
+ } catch { /* non-fatal */ }
1040
+
1041
+ let handoff = null;
1042
+ for (const hp of candidatePaths) {
1043
+ if (!existsSync(hp)) continue;
1044
+ try {
1045
+ const raw = JSON.parse(readFileSync(hp, 'utf8'));
1046
+ const ageMs = Date.now() - new Date(raw.writtenAt || 0).getTime();
1047
+ if (ageMs > HANDOFF_TTL_MS) continue; // stale handoff — not a sub-agent context
1048
+ if (!raw.harnessPacketPath) continue; // no packet path written → identity-only handoff, skip check
1049
+ handoff = raw;
1050
+ break;
1051
+ } catch { /* malformed — skip */ }
1052
+ }
1053
+
1054
+ if (!handoff) return; // not a sub-agent context, or handoff has no packetPath
1055
+
1056
+ // Owner-tier exemption: if ownerTier.hamza === true AND no jti → source of truth
1057
+ const ownerExempt = (handoff.ownerTier?.hamza === true) && !handoff.ownerTier?.jti;
1058
+ if (ownerExempt) return;
1059
+
1060
+ // Now verify that the cognition block cites at least one packet substrate token.
1061
+ // Tokens accepted (case-insensitive):
1062
+ // • Doctrine rule IDs from feedback_*/project_*/reference_* filenames
1063
+ // • Axiom names: truth_over_deception, no_harm, sacred_trust, reflection_before_action
1064
+ // • Frame primitives: Fitrah, Tafakkur, Tadabbur, Ilham, Mizan, Hikma, Nur, Wahi, Firasah
1065
+ // • Memory class patterns: feedback_*.md, project_*.md, reference_*.md
1066
+ 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;
1067
+
1068
+ const cogText = cogBlockBody || unionText;
1069
+ const hasCite = PACKET_CITE_RX.test(cogText) || PACKET_CITE_RX.test(cmd);
1070
+
1071
+ if (!hasCite) {
1072
+ const packetRef = handoff.harnessPacketPath;
1073
+ 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}.
1074
+
1075
+ Accepted citations (case-insensitive, any ONE suffices):
1076
+ • Doctrine rule IDs: doctrine_first, no_demos, workaround_vs_path_fix, no_flag_without_fix, etc.
1077
+ • Axioms: truth_over_deception, no_harm, sacred_trust, reflection_before_action, power_obligates_service
1078
+ • Frame primitives: Fitrah, Tafakkur, Tadabbur, Ilham, Mizan, Hikma, Nur, Wahi, Firasah
1079
+ • Memory class refs: feedback_*.md, project_*.md, reference_*.md
1080
+
1081
+ The harness packet is at: ${packetRef}
1082
+ Read it first (it is in your environment as ARIA_HARNESS_PACKET_PATH), then reference it in your cognition block.
1083
+
1084
+ Cognition-theater rejected per feedback_gates_enforce_form_not_substance.md.`;
1085
+ audit(`block-subagent-no-packet-cite ${toolName.toLowerCase()}`, cmdPreview);
1086
+ console.log(JSON.stringify({ decision: 'block', reason }));
1087
+ process.exit(2);
1088
+ }
1089
+ })();
1090
+
1007
1091
  // ── arch_facts gate (architectural violation scan) ────────────────────────
1008
1092
  //
1009
1093
  // Runs after cognition + discovery-binding pass, for Edit/Write/NotebookEdit.
@@ -1182,6 +1266,80 @@ Claude must either: (a) reframe action to fit allowedActions, OR (b) emit [PLAN_
1182
1266
  bindingAuditAppend({ event: 'allow_phase_action', sessionId, planId: plan.planId, phaseId: phase.id, action, target });
1183
1267
  }
1184
1268
 
1269
+ // ── Outcome Ledger regression flag (fail-soft) ───────────────────────────────
1270
+ //
1271
+ // Before allowing, query aria_outcome_ledger for recent regressions on the same
1272
+ // action shape. This is NOT a block — it's a soft-warning surfaced to stderr so
1273
+ // the agent can see the risk and decide whether the root cause has shipped.
1274
+ //
1275
+ // action_kind: 'edit' for Edit/Write/NotebookEdit, 'bash' for Bash
1276
+ // action_target: file path for file tools; first word of bash command otherwise
1277
+ //
1278
+ // Fail-open: network errors, timeouts, or non-200 responses are silently ignored.
1279
+ // The gate does not hardcode timeouts (no-timeouts doctrine); the 3s AbortController
1280
+ // is a DETECTION PROBE — if the endpoint is alive it will respond quickly; if it
1281
+ // is down the catch path fail-opens without blocking the developer.
1282
+ (async function checkOutcomeLedger() {
1283
+ const _harnessUrl = process.env.ARIA_HARNESS_URL || 'http://192.168.4.25:30080';
1284
+ const _harnessToken = process.env.ARIA_HARNESS_TOKEN || '';
1285
+
1286
+ // Derive action_kind and action_target from current tool call
1287
+ let _actionKind = 'bash';
1288
+ let _actionTarget = '';
1289
+ if (toolName === 'Edit' || toolName === 'Write' || toolName === 'NotebookEdit') {
1290
+ _actionKind = 'edit';
1291
+ _actionTarget = filePath;
1292
+ } else if (toolName === 'Bash') {
1293
+ _actionKind = 'bash';
1294
+ // First word of the command (the verb) as the shape key
1295
+ _actionTarget = cmd.trim().split(/\s+/)[0] || '';
1296
+ // If the command touches a specific file path, prefer that as target
1297
+ // (captures edit-like bash operations: sed, awk, tee, etc.)
1298
+ const _fileArgMatch = cmd.match(/\b([\w./~-]+\.[a-z]{2,6})\b/i);
1299
+ if (_fileArgMatch) _actionTarget = _fileArgMatch[1];
1300
+ }
1301
+
1302
+ if (!_actionTarget) return; // nothing useful to query
1303
+
1304
+ try {
1305
+ const _ctl = new AbortController();
1306
+ const _probeTimer = setTimeout(() => _ctl.abort(), 3000);
1307
+ let _resp;
1308
+ try {
1309
+ const _params = new URLSearchParams({ action_kind: _actionKind, action_target: _actionTarget });
1310
+ _resp = await fetch(`${_harnessUrl}/api/harness/outcome-recent-regressions?${_params}`, {
1311
+ method: 'GET',
1312
+ headers: {
1313
+ 'Content-Type': 'application/json',
1314
+ ...(_harnessToken ? { Authorization: `Bearer ${_harnessToken}` } : {}),
1315
+ },
1316
+ signal: _ctl.signal,
1317
+ });
1318
+ } finally {
1319
+ clearTimeout(_probeTimer);
1320
+ }
1321
+
1322
+ if (!_resp || !_resp.ok) return; // endpoint unreachable or error — fail-open
1323
+ const _data = await _resp.json();
1324
+ const _regressions = Array.isArray(_data?.regressions) ? _data.regressions : [];
1325
+
1326
+ if (_regressions.length > 0) {
1327
+ // Soft-warning to stderr — visible in Claude Code's hook output but does NOT block
1328
+ const _lines = _regressions.map(
1329
+ (r) => ` - ${r.action_target} at ${new Date(r.created_at).toISOString()}: ${r.regression_signal || '(no signal text)'}`,
1330
+ ).join('\n');
1331
+ process.stderr.write(
1332
+ `⚠️ Aria Outcome Ledger: this action shape regressed ${_regressions.length} time(s) in the last 60 min:\n` +
1333
+ `${_lines}\n` +
1334
+ `Re-emitting with this risk visible. Proceed only if root cause has shipped since.\n`,
1335
+ );
1336
+ audit(`warn-ledger-regression ${_actionKind} target=${_actionTarget} count=${_regressions.length}`, cmdPreview);
1337
+ }
1338
+ } catch {
1339
+ // Network error, abort, parse failure — fail-open, no warning
1340
+ }
1341
+ })();
1342
+
1185
1343
  // Non-trivial action with cognition AND (binding-allowed OR binding-disabled) — allow.
1186
1344
  audit(`allow-cognition ${toolName.toLowerCase()} lenses=${lensCount} via=${cognitionSource}`, cmdPreview);
1187
1345
  pushDecision('allow', `${toolName.toLowerCase()} with ${lensCount} lenses (${cognitionSource})`);
@@ -46,7 +46,7 @@
46
46
  // Future: signed-grant override mechanism at ~/.aria/owner-overrides/<hook>.json
47
47
  // with HMAC signature using a secret only Hamza holds. Deferred to next session.
48
48
 
49
- import { readFileSync, appendFileSync, existsSync, mkdirSync } from 'node:fs';
49
+ import { readFileSync, appendFileSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
50
50
  import { dirname } from 'node:path';
51
51
 
52
52
  const HOME = process.env.HOME || '/tmp';
@@ -702,6 +702,108 @@ if (cog.count >= REQUIRED_LENSES) {
702
702
  }
703
703
  } catch {/* outer guard */}
704
704
 
705
+ // ── Layer C — auto-re-consult on [PLAN_BLOCKER] (#85) ────────────────────
706
+ //
707
+ // When the assistant emits a [PLAN_BLOCKER reason="..."] marker the runtime
708
+ // must fire a two-path replan rather than blocking and waiting for the human:
709
+ //
710
+ // Primary path: POST /api/harness/replan (aria-soul server-side)
711
+ // Fallback path: node aria-architect-fallback.mjs (local sub-agent)
712
+ //
713
+ // Both paths write the fresh BINDING_PLAN to the session-scoped active-plan
714
+ // file so the next turn's pre-tool-gate and stop-gate pick it up.
715
+ //
716
+ // Fail-soft: if both paths fail, we log + emit a clear message asking Hamza
717
+ // to intervene. We do NOT crash the stop-gate — the existing block/allow
718
+ // decision below continues on its own merits.
719
+ const planBlockerMatch = assistantText.match(/\[PLAN_BLOCKER\s+reason="([^"]{10,2000})"\s*\]/);
720
+ if (planBlockerMatch) {
721
+ const planBlockerReason = planBlockerMatch[1];
722
+ audit(`[PLAN_BLOCKER] detected — firing replan: ${planBlockerReason.slice(0, 100)}`);
723
+
724
+ const currentPlanId = activePlan?.planId || 'unknown';
725
+ let planMinted = false;
726
+
727
+ // Primary path: aria-soul /api/harness/replan
728
+ try {
729
+ const harnessUrl = process.env.ARIA_HARNESS_URL || 'https://harness.ariasos.com';
730
+ const harnessToken = process.env.ARIA_HARNESS_TOKEN || process.env.ARIA_API_KEY || '';
731
+ const ctl = new AbortController();
732
+ const replanTimeout = setTimeout(() => ctl.abort(), 15000);
733
+ const resp = await fetch(`${harnessUrl}/api/harness/replan`, {
734
+ method: 'POST',
735
+ headers: {
736
+ 'Content-Type': 'application/json',
737
+ 'Authorization': `Bearer ${harnessToken}`,
738
+ },
739
+ body: JSON.stringify({
740
+ reason: planBlockerReason,
741
+ currentPlanId,
742
+ sessionId,
743
+ }),
744
+ signal: ctl.signal,
745
+ });
746
+ clearTimeout(replanTimeout);
747
+ if (resp.ok) {
748
+ const data = await resp.json();
749
+ if (data.ok && data.plan) {
750
+ const freshPlan = {
751
+ ...data.plan,
752
+ mintedAt: new Date().toISOString(),
753
+ mintedBy: 'aria-soul-replan',
754
+ };
755
+ try {
756
+ if (!existsSync(dirname(ACTIVE_PLAN_PATH))) mkdirSync(dirname(ACTIVE_PLAN_PATH), { recursive: true });
757
+ writeFileSync(ACTIVE_PLAN_PATH, JSON.stringify(freshPlan, null, 2));
758
+ } catch (writeErr) {
759
+ audit(`replan-primary-write-err: ${String(writeErr).slice(0, 200)}`);
760
+ }
761
+ planMinted = true;
762
+ audit(`replan-primary-ok planId=${data.plan.planId}`);
763
+ } else {
764
+ audit(`replan-primary-bad-response: ok=${data.ok} error=${(data.error || '').slice(0, 200)}`);
765
+ }
766
+ } else {
767
+ audit(`replan-primary-http-${resp.status}`);
768
+ }
769
+ } catch (err) {
770
+ audit(`replan-primary-failed: ${(err?.message || String(err)).slice(0, 200)}`);
771
+ }
772
+
773
+ // Fallback path: architect-fallback hook (spawned as sub-process)
774
+ if (!planMinted) {
775
+ audit('replan-primary-unreachable — firing architect-fallback');
776
+ try {
777
+ const { spawnSync } = await import('node:child_process');
778
+ const fallbackBin = `${HOME}/.claude/hooks/aria-architect-fallback.mjs`;
779
+ if (existsSync(fallbackBin)) {
780
+ const fallbackResult = spawnSync('node', [fallbackBin], {
781
+ input: JSON.stringify({ reason: planBlockerReason, currentPlanId, sessionId }),
782
+ encoding: 'utf8',
783
+ timeout: 130000,
784
+ });
785
+ if (fallbackResult.status === 0) {
786
+ audit(`architect-fallback-ok: ${(fallbackResult.stdout || '').slice(0, 200)}`);
787
+ planMinted = true;
788
+ } else {
789
+ audit(
790
+ `architect-fallback-failed status=${fallbackResult.status} stderr=${(fallbackResult.stderr || '').slice(0, 200)}`
791
+ );
792
+ }
793
+ } else {
794
+ audit(`architect-fallback-missing: ${fallbackBin} not found`);
795
+ }
796
+ } catch (err) {
797
+ audit(`architect-fallback-threw: ${(err?.message || String(err)).slice(0, 200)}`);
798
+ }
799
+ }
800
+
801
+ if (!planMinted) {
802
+ audit('replan-both-paths-failed — Hamza must intervene');
803
+ // Surface clearly in the block reason below; don't crash the gate.
804
+ }
805
+ }
806
+
705
807
  // Block decision: any of (validateOutput severity=block) OR (>=2 drift hits) OR
706
808
  // (>=1 code-quality hit) OR (open discovery in ledger) → block emit.
707
809
  // Aria enforcement #46 (compelled reflection): severity=warn ALSO blocks but