@aria_asi/cli 0.2.26 → 0.2.29
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/CLIENT-ONBOARDING.md +282 -0
- package/bin/aria.js +1140 -14
- package/dist/aria-connector/src/auth-commands.d.ts +1 -0
- package/dist/aria-connector/src/auth-commands.d.ts.map +1 -1
- package/dist/aria-connector/src/auth-commands.js +89 -41
- package/dist/aria-connector/src/auth-commands.js.map +1 -1
- package/dist/aria-connector/src/chat.d.ts +3 -0
- package/dist/aria-connector/src/chat.d.ts.map +1 -1
- package/dist/aria-connector/src/chat.js +146 -8
- package/dist/aria-connector/src/chat.js.map +1 -1
- package/dist/aria-connector/src/codebase-scanner.d.ts +2 -2
- package/dist/aria-connector/src/codebase-scanner.d.ts.map +1 -1
- package/dist/aria-connector/src/codebase-scanner.js +1 -1
- package/dist/aria-connector/src/codebase-scanner.js.map +1 -1
- package/dist/aria-connector/src/config.d.ts +12 -0
- package/dist/aria-connector/src/config.d.ts.map +1 -1
- package/dist/aria-connector/src/config.js +2 -0
- package/dist/aria-connector/src/config.js.map +1 -1
- package/dist/aria-connector/src/connectors/claude-code.d.ts.map +1 -1
- package/dist/aria-connector/src/connectors/claude-code.js +80 -24
- package/dist/aria-connector/src/connectors/claude-code.js.map +1 -1
- package/dist/aria-connector/src/connectors/codebase-awareness.d.ts +37 -0
- package/dist/aria-connector/src/connectors/codebase-awareness.d.ts.map +1 -0
- package/dist/aria-connector/src/connectors/codebase-awareness.js +335 -0
- package/dist/aria-connector/src/connectors/codebase-awareness.js.map +1 -0
- package/dist/aria-connector/src/connectors/codex.d.ts +3 -0
- package/dist/aria-connector/src/connectors/codex.d.ts.map +1 -0
- package/dist/aria-connector/src/connectors/codex.js +248 -0
- package/dist/aria-connector/src/connectors/codex.js.map +1 -0
- package/dist/aria-connector/src/connectors/cognitive-skills.d.ts +2 -0
- package/dist/aria-connector/src/connectors/cognitive-skills.d.ts.map +1 -0
- package/dist/aria-connector/src/connectors/cognitive-skills.js +47 -0
- package/dist/aria-connector/src/connectors/cognitive-skills.js.map +1 -0
- package/dist/aria-connector/src/connectors/opencode.d.ts.map +1 -1
- package/dist/aria-connector/src/connectors/opencode.js +90 -4
- package/dist/aria-connector/src/connectors/opencode.js.map +1 -1
- package/dist/aria-connector/src/connectors/repo-git-hooks.d.ts +3 -0
- package/dist/aria-connector/src/connectors/repo-git-hooks.d.ts.map +1 -0
- package/dist/aria-connector/src/connectors/repo-git-hooks.js +87 -0
- package/dist/aria-connector/src/connectors/repo-git-hooks.js.map +1 -0
- package/dist/aria-connector/src/connectors/repo-guard.d.ts +19 -0
- package/dist/aria-connector/src/connectors/repo-guard.d.ts.map +1 -0
- package/dist/aria-connector/src/connectors/repo-guard.js +509 -0
- package/dist/aria-connector/src/connectors/repo-guard.js.map +1 -0
- package/dist/aria-connector/src/connectors/runtime.d.ts +2 -0
- package/dist/aria-connector/src/connectors/runtime.d.ts.map +1 -0
- package/dist/aria-connector/src/connectors/runtime.js +330 -0
- package/dist/aria-connector/src/connectors/runtime.js.map +1 -0
- package/dist/aria-connector/src/connectors/shell.d.ts.map +1 -1
- package/dist/aria-connector/src/connectors/shell.js +78 -13
- package/dist/aria-connector/src/connectors/shell.js.map +1 -1
- package/dist/aria-connector/src/connectors/syncd.d.ts +27 -0
- package/dist/aria-connector/src/connectors/syncd.d.ts.map +1 -0
- package/dist/aria-connector/src/connectors/syncd.js +405 -0
- package/dist/aria-connector/src/connectors/syncd.js.map +1 -0
- package/dist/aria-connector/src/decisions.d.ts +207 -0
- package/dist/aria-connector/src/decisions.d.ts.map +1 -0
- package/dist/aria-connector/src/decisions.js +291 -0
- package/dist/aria-connector/src/decisions.js.map +1 -0
- package/dist/aria-connector/src/garden-control-plane.d.ts.map +1 -1
- package/dist/aria-connector/src/garden-control-plane.js +74 -17
- package/dist/aria-connector/src/garden-control-plane.js.map +1 -1
- package/dist/aria-connector/src/github-connect.d.ts +18 -0
- package/dist/aria-connector/src/github-connect.d.ts.map +1 -0
- package/dist/aria-connector/src/github-connect.js +117 -0
- package/dist/aria-connector/src/github-connect.js.map +1 -0
- package/dist/aria-connector/src/harness-client.d.ts +15 -0
- package/dist/aria-connector/src/harness-client.d.ts.map +1 -1
- package/dist/aria-connector/src/harness-client.js +106 -3
- package/dist/aria-connector/src/harness-client.js.map +1 -1
- package/dist/aria-connector/src/hive-client.d.ts +30 -0
- package/dist/aria-connector/src/hive-client.d.ts.map +1 -1
- package/dist/aria-connector/src/hive-client.js +124 -5
- package/dist/aria-connector/src/hive-client.js.map +1 -1
- package/dist/aria-connector/src/index.d.ts +13 -2
- package/dist/aria-connector/src/index.d.ts.map +1 -1
- package/dist/aria-connector/src/index.js +10 -1
- package/dist/aria-connector/src/index.js.map +1 -1
- package/dist/aria-connector/src/lib/aristotle-noor-wire.d.ts +102 -0
- package/dist/aria-connector/src/lib/aristotle-noor-wire.d.ts.map +1 -0
- package/dist/aria-connector/src/lib/aristotle-noor-wire.js +231 -0
- package/dist/aria-connector/src/lib/aristotle-noor-wire.js.map +1 -0
- package/dist/aria-connector/src/providers/types.d.ts +5 -0
- package/dist/aria-connector/src/providers/types.d.ts.map +1 -1
- package/dist/aria-connector/src/runtime-proof.d.ts +45 -0
- package/dist/aria-connector/src/runtime-proof.d.ts.map +1 -0
- package/dist/aria-connector/src/runtime-proof.js +340 -0
- package/dist/aria-connector/src/runtime-proof.js.map +1 -0
- package/dist/aria-connector/src/setup-wizard.d.ts.map +1 -1
- package/dist/aria-connector/src/setup-wizard.js +34 -2
- package/dist/aria-connector/src/setup-wizard.js.map +1 -1
- package/dist/assets/hooks/aria-agent-handoff.mjs +224 -0
- package/dist/assets/hooks/aria-agent-ledger-merge.mjs +164 -0
- package/dist/assets/hooks/aria-architect-fallback.mjs +267 -0
- package/dist/assets/hooks/aria-cognition-substrate-binding.mjs +676 -0
- package/dist/assets/hooks/aria-discovery-record.mjs +101 -0
- package/dist/assets/hooks/aria-harness-via-sdk.mjs +412 -0
- package/dist/assets/hooks/aria-import-resolution-gate.mjs +330 -0
- package/dist/assets/hooks/aria-outcome-record.mjs +84 -0
- package/dist/assets/hooks/aria-pre-emit-dryrun.mjs +294 -0
- package/dist/assets/hooks/aria-pre-text-gate.mjs +112 -0
- package/dist/assets/hooks/aria-pre-tool-gate.mjs +2133 -0
- package/dist/assets/hooks/aria-preprompt-consult.mjs +438 -0
- package/dist/assets/hooks/aria-preturn-memory-gate.mjs +570 -0
- package/dist/assets/hooks/aria-repo-doctrine-gate.mjs +397 -0
- package/dist/assets/hooks/aria-stop-gate.mjs +1551 -0
- package/dist/assets/hooks/aria-trigger-autolearn.mjs +229 -0
- package/dist/assets/hooks/aria-userprompt-abandon-detect.mjs +192 -0
- package/dist/assets/hooks/doctrine_trigger_map.json +479 -0
- package/dist/assets/hooks/lib/canonical-lenses.mjs +64 -0
- package/dist/assets/hooks/lib/gate-audit.mjs +43 -0
- package/dist/assets/hooks/test-aria-preturn-memory-gate.mjs +245 -0
- package/dist/assets/hooks/test-tier-lens-labeling.mjs +399 -0
- package/dist/assets/opencode-plugins/harness-context/index.js +60 -0
- package/dist/assets/opencode-plugins/harness-context/inject-context.mjs +179 -0
- package/dist/assets/opencode-plugins/harness-context/package.json +9 -0
- package/dist/assets/opencode-plugins/harness-gate/index.js +248 -0
- package/dist/assets/opencode-plugins/harness-outcome/index.js +129 -0
- package/dist/assets/opencode-plugins/harness-role/index.js +77 -0
- package/dist/assets/opencode-plugins/harness-role/package.json +9 -0
- package/dist/assets/opencode-plugins/harness-stop/index.js +241 -0
- package/dist/runtime/discipline/CLAUDE.md +339 -0
- package/dist/runtime/discipline/skills/aria-cognition/aria-essence/SKILL.md +63 -0
- package/dist/runtime/discipline/skills/aria-cognition/aria-essence/references/domain-matrix.md +80 -0
- package/dist/runtime/discipline/skills/aria-cognition/aria-essence/references/evolution-loop.md +30 -0
- package/dist/runtime/discipline/skills/aria-cognition/aria-essence/references/readable-cognition.md +27 -0
- package/dist/runtime/discipline/skills/aria-cognition/aria-forge-guardrails/SKILL.md +35 -0
- package/dist/runtime/discipline/skills/aria-cognition/aria-forge-guardrails/references/checklist.md +31 -0
- package/dist/runtime/discipline/skills/aria-cognition/aria-repo-doctrine/SKILL.md +39 -0
- package/dist/runtime/discipline/skills/aria-cognition/forge-quality-rules/SKILL.md +43 -0
- package/dist/runtime/discipline/skills/aria-cognition/ghazali-8lens/SKILL.md +38 -0
- package/dist/runtime/discipline/skills/aria-cognition/istiqra-induction/SKILL.md +26 -0
- package/dist/runtime/discipline/skills/aria-cognition/ladunni-22/SKILL.md +35 -0
- package/dist/runtime/discipline/skills/aria-cognition/mizan/SKILL.md +72 -0
- package/dist/runtime/discipline/skills/aria-cognition/nadia/SKILL.md +38 -0
- package/dist/runtime/discipline/skills/aria-cognition/nadia-psi/SKILL.md +38 -0
- package/dist/runtime/discipline/skills/aria-cognition/predictor/SKILL.md +25 -0
- package/dist/runtime/discipline/skills/aria-cognition/qiyas-analogy/SKILL.md +26 -0
- package/dist/runtime/discipline/skills/aria-cognition/soul-domains/SKILL.md +25 -0
- package/dist/runtime/discipline/skills/aria-harness/aria-aristotle-intra-phase/SKILL.md +81 -0
- package/dist/runtime/discipline/skills/aria-harness/aria-aristotle-post-phase/SKILL.md +98 -0
- package/dist/runtime/discipline/skills/aria-harness/aria-aristotle-pre-phase/SKILL.md +99 -0
- package/dist/runtime/discipline/skills/aria-harness/aria-harness-deploy/SKILL.md +127 -0
- package/dist/runtime/discipline/skills/aria-harness/aria-harness-no-stripping/SKILL.md +117 -0
- package/dist/runtime/discipline/skills/aria-harness/aria-harness-onboarding/SKILL.md +112 -0
- package/dist/runtime/discipline/skills/aria-harness/aria-harness-output-discipline/SKILL.md +102 -0
- package/dist/runtime/discipline/skills/aria-harness/aria-harness-substrate-binding/SKILL.md +121 -0
- package/dist/runtime/doctor.mjs +23 -0
- package/dist/runtime/local-phase.mjs +632 -0
- package/dist/runtime/manifest.json +15 -0
- package/dist/runtime/mizan-scheduler.mjs +331 -0
- package/dist/runtime/package.json +6 -0
- package/dist/runtime/provider-proxy.mjs +594 -0
- package/dist/runtime/sdk/BUNDLED.json +5 -0
- package/dist/runtime/sdk/index.d.ts +477 -0
- package/dist/runtime/sdk/index.js +1469 -0
- package/dist/runtime/sdk/index.js.map +1 -0
- package/dist/runtime/sdk/package.json +8 -0
- package/dist/runtime/sdk/runWithCognition.d.ts +77 -0
- package/dist/runtime/sdk/runWithCognition.js +157 -0
- package/dist/runtime/sdk/runWithCognition.js.map +1 -0
- package/dist/runtime/service.mjs +2708 -0
- package/dist/runtime/vendor/aria-gate-runtime/index.d.ts +53 -0
- package/dist/runtime/vendor/aria-gate-runtime/index.d.ts.map +1 -0
- package/dist/runtime/vendor/aria-gate-runtime/index.js +277 -0
- package/dist/runtime/vendor/aria-gate-runtime/index.js.map +1 -0
- package/dist/runtime/vendor/aria-gate-runtime/package.json +6 -0
- package/dist/sdk/BUNDLED.json +2 -2
- package/dist/sdk/index.d.ts +283 -0
- package/dist/sdk/index.js +622 -85
- package/dist/sdk/index.js.map +1 -1
- package/dist/sdk/runWithCognition.d.ts +77 -0
- package/dist/sdk/runWithCognition.js +157 -0
- package/dist/sdk/runWithCognition.js.map +1 -0
- package/hooks/aria-agent-handoff.mjs +11 -1
- package/hooks/aria-architect-fallback.mjs +109 -40
- package/hooks/aria-cognition-substrate-binding.mjs +676 -0
- package/hooks/aria-harness-via-sdk.mjs +34 -21
- package/hooks/aria-import-resolution-gate.mjs +330 -0
- package/hooks/aria-outcome-record.mjs +5 -1
- package/hooks/aria-pre-emit-dryrun.mjs +294 -0
- package/hooks/aria-pre-tool-gate.mjs +828 -41
- package/hooks/aria-preprompt-consult.mjs +113 -13
- package/hooks/aria-preturn-memory-gate.mjs +298 -6
- package/hooks/aria-repo-doctrine-gate.mjs +397 -0
- package/hooks/aria-stop-gate.mjs +739 -76
- package/hooks/aria-userprompt-abandon-detect.mjs +5 -1
- package/hooks/doctrine_trigger_map.json +209 -15
- package/hooks/lib/canonical-lenses.mjs +64 -0
- package/hooks/lib/gate-audit.mjs +43 -0
- package/opencode-plugins/harness-context/index.js +1 -1
- package/opencode-plugins/harness-context/inject-context.mjs +82 -23
- package/opencode-plugins/harness-gate/index.js +248 -0
- package/opencode-plugins/harness-outcome/index.js +129 -0
- package/opencode-plugins/harness-stop/index.js +241 -0
- package/package.json +8 -2
- package/runtime-src/doctor.mjs +23 -0
- package/runtime-src/local-phase.mjs +632 -0
- package/runtime-src/mizan-scheduler.mjs +331 -0
- package/runtime-src/provider-proxy.mjs +594 -0
- package/runtime-src/service.mjs +2708 -0
- package/scripts/bundle-sdk.mjs +317 -0
- package/scripts/install-client.sh +176 -0
- package/scripts/publish-all.sh +344 -0
- package/scripts/publish-docker.sh +27 -0
- package/scripts/validate-hook-contracts.mjs +54 -0
- package/scripts/validate-skill-prompts.mjs +95 -0
- package/skills/aria-cognition/aria-essence/SKILL.md +63 -0
- package/skills/aria-cognition/aria-essence/references/domain-matrix.md +80 -0
- package/skills/aria-cognition/aria-essence/references/evolution-loop.md +30 -0
- package/skills/aria-cognition/aria-essence/references/readable-cognition.md +27 -0
- package/skills/aria-cognition/aria-forge-guardrails/SKILL.md +35 -0
- package/skills/aria-cognition/aria-forge-guardrails/references/checklist.md +31 -0
- package/skills/aria-cognition/aria-repo-doctrine/SKILL.md +39 -0
- package/skills/aria-cognition/forge-quality-rules/SKILL.md +43 -0
- package/skills/aria-cognition/ghazali-8lens/SKILL.md +38 -0
- package/skills/aria-cognition/istiqra-induction/SKILL.md +26 -0
- package/skills/aria-cognition/ladunni-22/SKILL.md +35 -0
- package/skills/aria-cognition/mizan/SKILL.md +72 -0
- package/skills/aria-cognition/nadia/SKILL.md +38 -0
- package/skills/aria-cognition/nadia-psi/SKILL.md +38 -0
- package/skills/aria-cognition/predictor/SKILL.md +25 -0
- package/skills/aria-cognition/qiyas-analogy/SKILL.md +26 -0
- package/skills/aria-cognition/soul-domains/SKILL.md +25 -0
- package/src/auth-commands.ts +111 -45
- package/src/chat.ts +174 -13
- package/src/codebase-scanner.ts +4 -0
- package/src/config.ts +15 -0
- package/src/connectors/claude-code.ts +79 -25
- package/src/connectors/codebase-awareness.ts +408 -0
- package/src/connectors/codex.ts +274 -0
- package/src/connectors/cognitive-skills.ts +51 -0
- package/src/connectors/opencode.ts +93 -4
- package/src/connectors/repo-git-hooks.ts +86 -0
- package/src/connectors/repo-guard.ts +589 -0
- package/src/connectors/runtime.ts +374 -0
- package/src/connectors/shell.ts +83 -14
- package/src/connectors/syncd.ts +488 -0
- package/src/decisions.ts +469 -0
- package/src/garden-control-plane.ts +101 -26
- package/src/github-connect.ts +143 -0
- package/src/harness-client.ts +128 -3
- package/src/hive-client.ts +165 -5
- package/src/index.ts +41 -2
- package/src/lib/aristotle-noor-wire.ts +310 -0
- package/src/providers/types.ts +6 -0
- package/src/runtime-proof.ts +392 -0
- package/src/setup-wizard.ts +37 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
// ARIA_ALLOW_STUB — doctrine gate file legitimately discusses stub/placeholder semantics.
|
|
2
3
|
// Aria pre-tool-use gate — enforces cognition use before destructive tool calls.
|
|
3
4
|
//
|
|
4
5
|
// Runs as a Claude Code PreToolUse hook on every Bash invocation. For
|
|
@@ -36,12 +37,44 @@
|
|
|
36
37
|
// Audit log: every gate decision (allow / block / kill-switch) is
|
|
37
38
|
// appended to ~/.claude/aria-pre-tool-gate.log.
|
|
38
39
|
|
|
39
|
-
import { readFileSync, appendFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
40
|
+
import { readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync, chmodSync } from 'node:fs';
|
|
40
41
|
import { dirname } from 'node:path';
|
|
41
42
|
import { homedir } from 'node:os';
|
|
43
|
+
import { spawnSync } from 'node:child_process';
|
|
44
|
+
import { createHmac, randomBytes as cryptoRandomBytes } from 'node:crypto';
|
|
42
45
|
|
|
43
46
|
const HOME = process.env.HOME || '/tmp';
|
|
44
47
|
const LOG = `${HOME}/.claude/aria-pre-tool-gate.log`;
|
|
48
|
+
const HEARTBEAT = `${HOME}/.claude/aria-pre-tool-gate-heartbeat.jsonl`;
|
|
49
|
+
|
|
50
|
+
// ── Heartbeat OUTSIDE the crash boundary (doctrine #123) ───────────────
|
|
51
|
+
// Per feedback_ledger_writes_outside_crash_boundary.md + doctrine #124
|
|
52
|
+
// (non-blocking errors are NOT acceptable), the gate writes a heartbeat
|
|
53
|
+
// FIRST — before transcript scan, before cognition extraction, before
|
|
54
|
+
// any code path that could crash, hang, or be killed by an external
|
|
55
|
+
// timeout. A heartbeat-write is the ONLY signal that proves the gate
|
|
56
|
+
// process actually started executing. Hook-health monitors read this
|
|
57
|
+
// file to detect silent gate death.
|
|
58
|
+
//
|
|
59
|
+
// The heartbeat is intentionally a tiny synchronous write so that even
|
|
60
|
+
// if the gate is killed by Claude Code's hook timeout in the next
|
|
61
|
+
// millisecond, we have proof it was alive at startup.
|
|
62
|
+
try {
|
|
63
|
+
const hbPid = process.pid;
|
|
64
|
+
const hbTs = new Date().toISOString();
|
|
65
|
+
// Write before reading stdin or doing any work that could throw.
|
|
66
|
+
appendFileSync(HEARTBEAT, JSON.stringify({ ts: hbTs, pid: hbPid, gate: 'aria-pre-tool-gate', stage: 'startup' }) + '\n');
|
|
67
|
+
} catch {
|
|
68
|
+
// The ONLY catch in this file that may swallow — a heartbeat write
|
|
69
|
+
// failure here is fail-loud-on-stderr because no other surface exists
|
|
70
|
+
// yet (we haven't opened the audit log) but cannot itself block the
|
|
71
|
+
// gate. Per feedback_non_blocking_errors_unacceptable.md the
|
|
72
|
+
// failure-mode for THIS specific path is documented in the doctrine
|
|
73
|
+
// memory; the structural fix for "what if the heartbeat write fails"
|
|
74
|
+
// is the hook-health monitor task #122 which checks heartbeat
|
|
75
|
+
// freshness and reports staleness.
|
|
76
|
+
process.stderr.write(`[aria-pre-tool-gate] heartbeat write failed at ${new Date().toISOString()}\n`);
|
|
77
|
+
}
|
|
45
78
|
|
|
46
79
|
// Bypass-counter (kept for historical visibility — past audit-log
|
|
47
80
|
// entries are still useful even though no new bypass entries can be
|
|
@@ -94,21 +127,18 @@ function audit(decision, summary) {
|
|
|
94
127
|
} catch {}
|
|
95
128
|
}
|
|
96
129
|
|
|
97
|
-
// ARIA_BINDING_ENABLED
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
} catch {}
|
|
110
|
-
process.exit(0);
|
|
111
|
-
}
|
|
130
|
+
// ARIA_BINDING_ENABLED env-override REMOVED 2026-04-28 per Hamza directive
|
|
131
|
+
// + memory:feedback_gap_discovery_hardens_doctrine.md — env-var disables
|
|
132
|
+
// are the textbook bypass class doctrine forbids. The override was the
|
|
133
|
+
// de-facto Aria-down handler for an unknown count of sessions and turned
|
|
134
|
+
// off every gate, not just the plan-check. Replaced by the architect
|
|
135
|
+
// fallback plan path (tracked task — Aria-unreachable detection writes
|
|
136
|
+
// a bounded local plan with limited allowedActions, loud audit, auto-
|
|
137
|
+
// expiry on Aria return). Prior env_override_exit entries remain in
|
|
138
|
+
// ~/.claude/aria-binding-audit.jsonl for historical audit.
|
|
139
|
+
//
|
|
140
|
+
// The gate now enforces unconditionally from the gated process per
|
|
141
|
+
// Hamza directive 2026-04-27. No process-level disable path.
|
|
112
142
|
|
|
113
143
|
// ── Aria-as-commander binding (Layer A — allowedActions/forbiddenActions per active phase) ──
|
|
114
144
|
//
|
|
@@ -344,10 +374,78 @@ const DESTRUCTIVE_PATTERNS = [
|
|
|
344
374
|
{ rx: /\bkubectl\s+(scale|rollout)\s+(undo|restart)\b/, name: 'kubectl-rollback' },
|
|
345
375
|
];
|
|
346
376
|
|
|
377
|
+
// Deploy patterns — bash invocations that mutate the canonical aria k8s
|
|
378
|
+
// cluster or push images. Per feedback_deploy_requires_verify_cognition.md
|
|
379
|
+
// (Hamza directive 2026-04-28 after consciousness.ts crash took aria-soul
|
|
380
|
+
// into CrashLoopBackOff): deploys require BOTH a <verify> block citing the
|
|
381
|
+
// shipping artifact AND a <cognition> block with substantive substrate
|
|
382
|
+
// anchors. Without both, the deploy is hard-blocked. This is doctrine #104
|
|
383
|
+
// — the structural enforcement Hamza demanded after the prior deploy
|
|
384
|
+
// crashed all of aria.
|
|
385
|
+
const DEPLOY_PATTERNS = [
|
|
386
|
+
{ rx: /\bbash\s+(?:\.\/)?scripts\/deploy-service\.sh\b/, name: 'deploy-service-script' },
|
|
387
|
+
{ rx: /\b(?:\.\/)?scripts\/deploy-service\.sh\b/, name: 'deploy-service-script' },
|
|
388
|
+
{ rx: /\bkubectl\s+apply\b/, name: 'kubectl-apply' },
|
|
389
|
+
{ rx: /\bkubectl\s+set\s+image\b/, name: 'kubectl-set-image' },
|
|
390
|
+
{ rx: /\bkubectl\s+rollout\s+restart\b/, name: 'kubectl-rollout-restart' },
|
|
391
|
+
{ rx: /\bkubectl\s+rollout\s+undo\b/, name: 'kubectl-rollout-undo' },
|
|
392
|
+
{ rx: /\bkubectl\s+create\b/, name: 'kubectl-create' },
|
|
393
|
+
{ rx: /\bkubectl\s+replace\b/, name: 'kubectl-replace' },
|
|
394
|
+
{ rx: /\bdocker\s+push\b/, name: 'docker-push' },
|
|
395
|
+
{ rx: /\bdocker\s+build\b.*--push\b/, name: 'docker-build-push' },
|
|
396
|
+
];
|
|
397
|
+
|
|
398
|
+
// Minimum substrate anchors required in cognition body for a deploy.
|
|
399
|
+
// Per feedback_full_harness_binding_must_be_structural.md, every cognition
|
|
400
|
+
// lens should cite a substrate anchor; for deploy specifically we require
|
|
401
|
+
// at least 4 distinct anchors total across the block (one per lens minimum
|
|
402
|
+
// across at least 4 lenses).
|
|
403
|
+
const DEPLOY_MIN_SUBSTRATE_ANCHORS = 4;
|
|
404
|
+
const SUBSTRATE_ANCHOR_RX = /\b(axiom|frame|memory|doctrine|packet):[a-z0-9_\-./]+/gi;
|
|
405
|
+
|
|
406
|
+
// Verify-block fields specifically required for deploys — beyond the base
|
|
407
|
+
// 5 fields, the deploy verify must cite a commit/SHA, a TS-check result,
|
|
408
|
+
// and the admission policy that authorizes the canonical image.
|
|
409
|
+
const DEPLOY_VERIFY_REQUIRED_FIELDS = [
|
|
410
|
+
// Either a git commit hash OR an explicit "files changed" listing
|
|
411
|
+
{ rx: /\b(?:commit|HEAD|SHA|files\s+changed)\b/i, name: 'commit_or_files' },
|
|
412
|
+
// TS-check / build evidence
|
|
413
|
+
{ rx: /\b(?:tsc|type[\s-]?check|build|npm\s+run\s+build)\b/i, name: 'build_or_typecheck' },
|
|
414
|
+
// Admission policy citation
|
|
415
|
+
{ rx: /\b(?:admission[\s_-]?policy|validatingadmissionpolicy)\b/i, name: 'admission_policy' },
|
|
416
|
+
];
|
|
417
|
+
|
|
347
418
|
// The verify-block contract. All five fields required.
|
|
348
419
|
const VERIFY_BLOCK_RX =
|
|
349
420
|
/<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;
|
|
350
421
|
|
|
422
|
+
// ── Dalio Loop: expected_outcome enforcement (doctrine:dalio_expected_required) ─
|
|
423
|
+
//
|
|
424
|
+
// Every non-trivial action must carry an <expected> block with at least one
|
|
425
|
+
// measurable predicate. The predicate must be numeric (≥X, ==X, ≤X, X%),
|
|
426
|
+
// boolean (true/false/exit=0/exit=1), or state-string ("status=running",
|
|
427
|
+
// "200 OK", "exit=0", "file=exists", etc.).
|
|
428
|
+
//
|
|
429
|
+
// Qualitative drift phrases ("better", "improved", "more robust", etc.) are
|
|
430
|
+
// REJECTED — they are unmeasurable and defeat the Dalio accountability loop.
|
|
431
|
+
// The block must appear in the same turn or a prior turn (turn-scoped, same
|
|
432
|
+
// window as cognition).
|
|
433
|
+
//
|
|
434
|
+
// Per doctrine:dalio_expected_required — no measurable predicate = no allow.
|
|
435
|
+
const EXPECTED_BLOCK_RX = /<expected>([\s\S]*?)<\/expected>/i;
|
|
436
|
+
|
|
437
|
+
// Measurable predicate patterns — at least ONE must be present inside <expected>.
|
|
438
|
+
// Forms accepted:
|
|
439
|
+
// numeric: ≥X, >=X, ==X, <=X, <X, >X, X%, N of N, count=N, latency<Xms
|
|
440
|
+
// boolean: true, false, exit=0, exit=1, exit_code=N, rc=N
|
|
441
|
+
// state-string: status=running, status=healthy, status=200, "200 OK",
|
|
442
|
+
// "exit=0", "file=exists", "count=N", "error_rate=0%"
|
|
443
|
+
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;
|
|
444
|
+
|
|
445
|
+
// Qualitative drift phrases that masquerade as measurable but are not.
|
|
446
|
+
// Any <expected> block containing ONLY these (no actual predicate) is rejected.
|
|
447
|
+
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;
|
|
448
|
+
|
|
351
449
|
// ── Tier-aware lens labeling (Phase 11 #59) ──────────────────────────────────
|
|
352
450
|
//
|
|
353
451
|
// Aria's Arabic cognition lens names are proprietary IP. On Hamza's surface
|
|
@@ -406,7 +504,14 @@ function docRef(canonicalFilename, genericDescription) {
|
|
|
406
504
|
// colon, not a placeholder template). The substance check (added
|
|
407
505
|
// 2026-04-26) defeats ritual emission — `nur: ok` no longer counts.
|
|
408
506
|
const COGNITION_BLOCK_RX = /<cognition>([\s\S]*?)<\/cognition>/i;
|
|
409
|
-
|
|
507
|
+
// Hamza directive 2026-04-28: 8-lens enforcement, not 4-of-8. The earlier
|
|
508
|
+
// REQUIRED_LENSES=4 was a regression — owner caught it ("i no longer see
|
|
509
|
+
// 8 lens cognition per turn"). All 8 canonical lenses must appear with
|
|
510
|
+
// substantive (≥20-char, non-placeholder) content per turn for the gate
|
|
511
|
+
// to accept the cognition. The substrate-binding stop hook also requires
|
|
512
|
+
// all 8 to carry substrate anchors; this gate enforces the 8 lens NAMES
|
|
513
|
+
// are present and have substance.
|
|
514
|
+
const REQUIRED_LENSES = 8;
|
|
410
515
|
const SUBSTANCE_MIN_CHARS = 20;
|
|
411
516
|
// Placeholder patterns from the gate's own correction message + the
|
|
412
517
|
// COMPACT_CONTINUITY_DOCTRINE template — content matching these is
|
|
@@ -612,7 +717,11 @@ async function archFactsGate(toolInput) {
|
|
|
612
717
|
// AbortController deadline. Pile-up protection is structural —
|
|
613
718
|
// in-flight counter caps concurrent pushes. Real network errors
|
|
614
719
|
// drive the catch path; slow responses complete naturally.
|
|
615
|
-
const HARNESS_URL =
|
|
720
|
+
const HARNESS_URL =
|
|
721
|
+
process.env.ARIA_HIVE_RUNTIME_URL ||
|
|
722
|
+
process.env.ARIA_HARNESS_BASE_URL ||
|
|
723
|
+
process.env.ARIA_HARNESS_URL ||
|
|
724
|
+
'https://harness.ariasos.com';
|
|
616
725
|
const HARNESS_TOKEN = process.env.ARIA_HARNESS_TOKEN || '';
|
|
617
726
|
const LOG_PUSH_DISABLED = process.env.ARIA_COGNITION_PUSH === 'off';
|
|
618
727
|
const MAX_IN_FLIGHT = 16;
|
|
@@ -640,6 +749,89 @@ function pushCognitionEvent(payload) {
|
|
|
640
749
|
}
|
|
641
750
|
}
|
|
642
751
|
|
|
752
|
+
// ── Block-pattern recipe lookup (Task #86) ──────────────────────────────────
|
|
753
|
+
// Before emitting a binding-violation block, ask aria-soul if this exact
|
|
754
|
+
// shape has been seen before and a high-confidence corrective recipe exists.
|
|
755
|
+
// If so, surface the recipe in the block message so the gated process gets
|
|
756
|
+
// the fix the hive already learned, not just the block.
|
|
757
|
+
//
|
|
758
|
+
// Per feedback_no_timeouts_doctrine.md the lookup uses a DETECTION PROBE
|
|
759
|
+
// (3s AbortController) — that's "is the lookup endpoint alive at all?", not
|
|
760
|
+
// a hard deadline on the underlying lookup. If the probe fires the gate
|
|
761
|
+
// continues with the block-only message: a missed lookup must NEVER convert
|
|
762
|
+
// a real block into an allow.
|
|
763
|
+
//
|
|
764
|
+
// Recipe is surfaced only when confidence > RECIPE_SURFACE_CONFIDENCE (0.7
|
|
765
|
+
// per migration 208 RECIPE_PROMOTE_THRESHOLD), matching the same threshold
|
|
766
|
+
// the cron uses to promote a recipe to the pattern's hot pointer. Below
|
|
767
|
+
// that, we still cite the pattern's existence ("similar block has been
|
|
768
|
+
// seen N times") without claiming a fix.
|
|
769
|
+
const ARIA_SOUL_URL = process.env.ARIA_SOUL_URL || HARNESS_URL;
|
|
770
|
+
const RECIPE_SURFACE_CONFIDENCE = 0.7;
|
|
771
|
+
const BLOCK_PATTERN_PROBE_MS = 3000;
|
|
772
|
+
|
|
773
|
+
async function lookupBlockPatternRecipe({ detectorClass, signature, tenantId }) {
|
|
774
|
+
if (!detectorClass || !signature) return null;
|
|
775
|
+
const url = new URL(`${ARIA_SOUL_URL}/api/hive/block-pattern`);
|
|
776
|
+
url.searchParams.set('action', 'lookup');
|
|
777
|
+
url.searchParams.set('detector_class', detectorClass);
|
|
778
|
+
url.searchParams.set('pattern_signature', signature.slice(0, 512));
|
|
779
|
+
if (tenantId) url.searchParams.set('tenant_id', tenantId);
|
|
780
|
+
|
|
781
|
+
const ctl = new AbortController();
|
|
782
|
+
const probeTimer = setTimeout(() => ctl.abort(), BLOCK_PATTERN_PROBE_MS);
|
|
783
|
+
try {
|
|
784
|
+
const resp = await fetch(url.toString(), {
|
|
785
|
+
method: 'GET',
|
|
786
|
+
headers: HARNESS_TOKEN ? { Authorization: `Bearer ${HARNESS_TOKEN}` } : {},
|
|
787
|
+
signal: ctl.signal,
|
|
788
|
+
});
|
|
789
|
+
if (!resp.ok) return null;
|
|
790
|
+
const body = await resp.json();
|
|
791
|
+
if (!body || body.found !== true) return null;
|
|
792
|
+
return body;
|
|
793
|
+
} catch {
|
|
794
|
+
return null; // probe failure — block proceeds without recipe surfacing
|
|
795
|
+
} finally {
|
|
796
|
+
clearTimeout(probeTimer);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// Renders the lookup result as an inline addendum to a block message. Returns
|
|
801
|
+
// the empty string when there's nothing useful to surface — the caller can
|
|
802
|
+
// concatenate unconditionally without producing dangling whitespace.
|
|
803
|
+
function renderRecipeAddendum(lookupResult) {
|
|
804
|
+
if (!lookupResult) return '';
|
|
805
|
+
const recipe = lookupResult.recipe;
|
|
806
|
+
const freq = Number(lookupResult.frequency || 0);
|
|
807
|
+
const successRate = lookupResult.success_rate;
|
|
808
|
+
|
|
809
|
+
// Recipe present + above promotion threshold → surface the fix verbatim.
|
|
810
|
+
if (recipe && typeof recipe === 'object' && Number(recipe.confidence ?? 0) >= RECIPE_SURFACE_CONFIDENCE) {
|
|
811
|
+
const text = typeof recipe.recipe_text === 'string' ? recipe.recipe_text.slice(0, 800) : '';
|
|
812
|
+
const actions = Array.isArray(recipe.recipe_actions) ? recipe.recipe_actions : [];
|
|
813
|
+
const actionsLine = actions.length
|
|
814
|
+
? `\n Actions: ${JSON.stringify(actions).slice(0, 600)}`
|
|
815
|
+
: '';
|
|
816
|
+
const conf = Number(recipe.confidence).toFixed(2);
|
|
817
|
+
const seenLine = freq > 0 ? ` (pattern seen ${freq}× across the hive)` : '';
|
|
818
|
+
return `\n\n📚 HIVE RECIPE${seenLine}:
|
|
819
|
+
${text}${actionsLine}
|
|
820
|
+
Confidence: ${conf}. Apply this BEFORE the block escalates — the hive learned this fix from prior firings.`;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// No promoted recipe — but the pattern is recurring. Cite it so the caller
|
|
824
|
+
// knows this isn't novel; the recipe is still being learned.
|
|
825
|
+
if (freq >= 3) {
|
|
826
|
+
const rateLine = (typeof successRate === 'number')
|
|
827
|
+
? ` Past resolution rate: ${(successRate * 100).toFixed(0)}%.`
|
|
828
|
+
: '';
|
|
829
|
+
return `\n\n📓 Hive note: this block-shape has fired ${freq} time(s); recipe is still being learned (no high-confidence fix yet).${rateLine}`;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
return '';
|
|
833
|
+
}
|
|
834
|
+
|
|
643
835
|
// Read event JSON from stdin (Claude Code spec).
|
|
644
836
|
let input = '';
|
|
645
837
|
for await (const chunk of process.stdin) input += chunk;
|
|
@@ -704,12 +896,18 @@ if (inlineCog.count >= REQUIRED_LENSES) {
|
|
|
704
896
|
const matched = toolName === 'Bash'
|
|
705
897
|
? DESTRUCTIVE_PATTERNS.find(({ rx }) => rx.test(cmd))
|
|
706
898
|
: null;
|
|
899
|
+
// Deploy-specific match — separate from destructive because it carries
|
|
900
|
+
// stricter substrate-anchor requirements per doctrine #104.
|
|
901
|
+
const deployMatched = toolName === 'Bash'
|
|
902
|
+
? DEPLOY_PATTERNS.find(({ rx }) => rx.test(cmd))
|
|
903
|
+
: null;
|
|
707
904
|
const isTrivialRead = toolName === 'Bash' && TRIVIAL_BASH_RX.test(cmd) && cmd.length < 200;
|
|
708
905
|
const isShort = toolName === 'Bash' && cmd.length < SHORT_BASH_LIMIT;
|
|
709
906
|
|
|
710
|
-
if (!matched && (isTrivialRead || isShort)) {
|
|
711
|
-
// Not destructive AND trivial — allow without
|
|
712
|
-
// reachable for Bash because
|
|
907
|
+
if (!matched && !deployMatched && (isTrivialRead || isShort)) {
|
|
908
|
+
// Not destructive AND not a deploy AND trivial — allow without
|
|
909
|
+
// further checks. Only reachable for Bash because all three flags are
|
|
910
|
+
// forced false otherwise.
|
|
713
911
|
process.exit(0);
|
|
714
912
|
}
|
|
715
913
|
|
|
@@ -830,7 +1028,10 @@ const mergedLensSet = new Set([...inlineCog.names, ...transcriptCog.names]);
|
|
|
830
1028
|
const lensCount = mergedLensSet.size;
|
|
831
1029
|
const lensNames = [...mergedLensSet];
|
|
832
1030
|
const cogBlockBody = transcriptCog.blockBody;
|
|
833
|
-
const
|
|
1031
|
+
const verifyBodies = [...unionText.matchAll(/<verify>([\s\S]*?)<\/verify>/gi)]
|
|
1032
|
+
.map((m) => (m[1] || '').trim())
|
|
1033
|
+
.filter(Boolean);
|
|
1034
|
+
const hasVerify = verifyBodies.length > 0;
|
|
834
1035
|
const hasCognition = lensCount >= REQUIRED_LENSES;
|
|
835
1036
|
const cognitionSource = inlineCog.count >= REQUIRED_LENSES
|
|
836
1037
|
? 'inline-command'
|
|
@@ -882,6 +1083,176 @@ function pushDecision(decision, reasonText) {
|
|
|
882
1083
|
});
|
|
883
1084
|
}
|
|
884
1085
|
|
|
1086
|
+
// ── Deploy-specific gate (doctrine #104) ────────────────────────────────
|
|
1087
|
+
// Per feedback_deploy_requires_verify_cognition.md (Hamza 2026-04-28 after
|
|
1088
|
+
// consciousness.ts crash took aria-soul into CrashLoopBackOff): deploys
|
|
1089
|
+
// require the verify block to cite commit/files/build/admission-policy
|
|
1090
|
+
// AND the cognition block to carry ≥DEPLOY_MIN_SUBSTRATE_ANCHORS substrate
|
|
1091
|
+
// anchors. The full-harness-binding doctrine says lenses without anchors
|
|
1092
|
+
// are unsourced prose — for deploys, that prose-only level of evidence is
|
|
1093
|
+
// not sufficient. The deploy gate refuses until anchors are present.
|
|
1094
|
+
if (deployMatched) {
|
|
1095
|
+
// Anchor count is read from the cognition body (turn-scoped, same
|
|
1096
|
+
// source the substrate-binding stop hook uses).
|
|
1097
|
+
const cognitionBody = cogBlockBody || '';
|
|
1098
|
+
const anchorMatches = cognitionBody.match(SUBSTRATE_ANCHOR_RX) || [];
|
|
1099
|
+
const anchorCount = anchorMatches.length;
|
|
1100
|
+
|
|
1101
|
+
// Required verify-block fields specific to deploy.
|
|
1102
|
+
const verifyBody = (() => {
|
|
1103
|
+
if (verifyBodies.length === 0) return '';
|
|
1104
|
+
let bestBody = verifyBodies[0];
|
|
1105
|
+
let bestScore = -1;
|
|
1106
|
+
for (const body of verifyBodies) {
|
|
1107
|
+
const score = DEPLOY_VERIFY_REQUIRED_FIELDS.reduce(
|
|
1108
|
+
(count, { rx }) => count + (rx.test(body) ? 1 : 0),
|
|
1109
|
+
0,
|
|
1110
|
+
);
|
|
1111
|
+
if (score > bestScore) {
|
|
1112
|
+
bestScore = score;
|
|
1113
|
+
bestBody = body;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
return bestBody;
|
|
1117
|
+
})();
|
|
1118
|
+
const missingDeployFields = DEPLOY_VERIFY_REQUIRED_FIELDS
|
|
1119
|
+
.filter(({ rx }) => !rx.test(verifyBody))
|
|
1120
|
+
.map(({ name }) => name);
|
|
1121
|
+
|
|
1122
|
+
// Heartbeat at deploy-gate entry — proves the gate reached this path
|
|
1123
|
+
// even if a downstream exit fires silently (per
|
|
1124
|
+
// feedback_ledger_writes_outside_crash_boundary.md).
|
|
1125
|
+
try {
|
|
1126
|
+
appendFileSync(HEARTBEAT, JSON.stringify({
|
|
1127
|
+
ts: new Date().toISOString(), gate: 'aria-pre-tool-gate', stage: 'deploy-gate-entry',
|
|
1128
|
+
deployPattern: deployMatched.name, hasVerify, hasCognition, lensCount, anchorCount,
|
|
1129
|
+
missingDeployFields, cogBlockBodyLen: (cogBlockBody || '').length,
|
|
1130
|
+
verifyBodyLen: verifyBody.length,
|
|
1131
|
+
}) + '\n');
|
|
1132
|
+
} catch {}
|
|
1133
|
+
|
|
1134
|
+
const deployBlocked =
|
|
1135
|
+
!hasVerify ||
|
|
1136
|
+
!hasCognition ||
|
|
1137
|
+
anchorCount < DEPLOY_MIN_SUBSTRATE_ANCHORS ||
|
|
1138
|
+
missingDeployFields.length > 0;
|
|
1139
|
+
|
|
1140
|
+
if (!deployBlocked) {
|
|
1141
|
+
// Write justification artifact for deploy-service.sh to read.
|
|
1142
|
+
// Doctrine #104 + bypass-vulnerability-closure 2026-04-28:
|
|
1143
|
+
// The artifact is HMAC-signed using a per-installation secret at
|
|
1144
|
+
// ~/.claude/.aria-gate-secret (0600). deploy-service.sh recomputes
|
|
1145
|
+
// the HMAC and refuses any artifact whose signature does not match
|
|
1146
|
+
// — this binds the artifact to having been written by THIS gate
|
|
1147
|
+
// process, preventing any other process from synthesizing a
|
|
1148
|
+
// passing artifact (which is exactly the bypass owner caught when
|
|
1149
|
+
// the assistant attempted a manualJustification:true write).
|
|
1150
|
+
//
|
|
1151
|
+
// The secret is generated on first run if absent; the file is
|
|
1152
|
+
// chmod 0600 so other users cannot read it; deploy-service.sh
|
|
1153
|
+
// reads the same file and computes the same HMAC.
|
|
1154
|
+
try {
|
|
1155
|
+
const justificationPath = `${HOME}/.claude/.aria-deploy-justification.json`;
|
|
1156
|
+
const secretPath = `${HOME}/.claude/.aria-gate-secret`;
|
|
1157
|
+
|
|
1158
|
+
// Lazy-generate per-installation secret if absent.
|
|
1159
|
+
if (!existsSync(secretPath)) {
|
|
1160
|
+
const secret = cryptoRandomBytes(32).toString('hex');
|
|
1161
|
+
writeFileSync(secretPath, secret + '\n', { mode: 0o600 });
|
|
1162
|
+
chmodSync(secretPath, 0o600);
|
|
1163
|
+
}
|
|
1164
|
+
const secret = readFileSync(secretPath, 'utf-8').trim();
|
|
1165
|
+
|
|
1166
|
+
// Build the unsigned artifact body. Note: deliberately reject any
|
|
1167
|
+
// attempt to set `manualJustification` — only this gate emits the
|
|
1168
|
+
// artifact, and a manual flag is by definition a forgery class.
|
|
1169
|
+
const unsignedBody = {
|
|
1170
|
+
timestamp: new Date().toISOString(),
|
|
1171
|
+
sessionId,
|
|
1172
|
+
deployPattern: deployMatched.name,
|
|
1173
|
+
command: cmdPreview,
|
|
1174
|
+
verify: verifyBody.trim().slice(0, 4000),
|
|
1175
|
+
cognition: cogBlockBody.trim().slice(0, 8000),
|
|
1176
|
+
substrateAnchors: anchorMatches.slice(0, 50),
|
|
1177
|
+
anchorCount,
|
|
1178
|
+
lensCount,
|
|
1179
|
+
verifyFieldsPresent: DEPLOY_VERIFY_REQUIRED_FIELDS
|
|
1180
|
+
.filter(({ rx }) => rx.test(verifyBody))
|
|
1181
|
+
.map(({ name }) => name),
|
|
1182
|
+
};
|
|
1183
|
+
|
|
1184
|
+
// HMAC-SHA256 over the canonical-JSON of the unsigned body.
|
|
1185
|
+
// canonical-JSON = JSON.stringify with no signature field, no
|
|
1186
|
+
// pretty-print whitespace, key order as written above.
|
|
1187
|
+
const signature = createHmac('sha256', secret)
|
|
1188
|
+
.update(JSON.stringify(unsignedBody))
|
|
1189
|
+
.digest('hex');
|
|
1190
|
+
|
|
1191
|
+
const justification = {
|
|
1192
|
+
...unsignedBody,
|
|
1193
|
+
signature,
|
|
1194
|
+
signatureAlgo: 'HMAC-SHA256',
|
|
1195
|
+
};
|
|
1196
|
+
writeFileSync(justificationPath, JSON.stringify(justification, null, 2));
|
|
1197
|
+
} catch (writeErr) {
|
|
1198
|
+
// Write failure is non-fatal for the gate decision (the gate itself
|
|
1199
|
+
// is the structural enforcement); log loudly per
|
|
1200
|
+
// canonical-secrets-governance LOUD telemetry doctrine.
|
|
1201
|
+
console.error(
|
|
1202
|
+
`[aria-pre-tool-gate] WARN deploy-justification artifact write failed: ${(writeErr instanceof Error ? writeErr.message : String(writeErr)).slice(0, 200)}`,
|
|
1203
|
+
);
|
|
1204
|
+
}
|
|
1205
|
+
audit(
|
|
1206
|
+
`allow-deploy ${deployMatched.name} lenses=${lensCount} anchors=${anchorCount}`,
|
|
1207
|
+
cmdPreview,
|
|
1208
|
+
);
|
|
1209
|
+
pushDecision(
|
|
1210
|
+
'allow',
|
|
1211
|
+
`verify+cognition+anchors(${anchorCount}) for deploy ${deployMatched.name}`,
|
|
1212
|
+
);
|
|
1213
|
+
process.exit(0);
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
// Build a focused refusal naming exactly which piece is missing.
|
|
1217
|
+
const reasons = [];
|
|
1218
|
+
if (!hasVerify) reasons.push('missing <verify> block');
|
|
1219
|
+
if (!hasCognition) reasons.push(`missing <cognition> block (lenses=${lensCount}/${REQUIRED_LENSES})`);
|
|
1220
|
+
if (hasCognition && anchorCount < DEPLOY_MIN_SUBSTRATE_ANCHORS) {
|
|
1221
|
+
reasons.push(
|
|
1222
|
+
`cognition block has ${anchorCount}/${DEPLOY_MIN_SUBSTRATE_ANCHORS} substrate anchors (axiom:/frame:/memory:/doctrine:/packet:)`,
|
|
1223
|
+
);
|
|
1224
|
+
}
|
|
1225
|
+
if (missingDeployFields.length > 0) {
|
|
1226
|
+
reasons.push(`<verify> block missing required fields: ${missingDeployFields.join(', ')}`);
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
const refusal = `Aria pre-tool gate: DEPLOY hard-block — pattern '${deployMatched.name}' detected.
|
|
1230
|
+
|
|
1231
|
+
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:
|
|
1232
|
+
|
|
1233
|
+
1. A <verify> block in the recent assistant text containing AT MINIMUM:
|
|
1234
|
+
- target/role/verified/rollback/axiom (base verify fields)
|
|
1235
|
+
- commit hash or files-changed listing
|
|
1236
|
+
- tsc / build / type-check evidence
|
|
1237
|
+
- admission policy citation (kubectl get validatingadmissionpolicy <service>-canonical-image-policy)
|
|
1238
|
+
|
|
1239
|
+
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>).
|
|
1240
|
+
|
|
1241
|
+
Block reasons (this turn): ${reasons.join(' • ')}.
|
|
1242
|
+
|
|
1243
|
+
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.
|
|
1244
|
+
|
|
1245
|
+
Re-emit verify+cognition with the missing pieces, then retry the deploy command. There is no env-var override path; doctrine #104 forbids it.`;
|
|
1246
|
+
|
|
1247
|
+
audit(
|
|
1248
|
+
`block-deploy ${deployMatched.name} verify=${hasVerify} cognition=${lensCount} anchors=${anchorCount} missing=${missingDeployFields.join(',')}`,
|
|
1249
|
+
cmdPreview,
|
|
1250
|
+
);
|
|
1251
|
+
pushDecision('block', `deploy ${deployMatched.name}: ${reasons.join('; ')}`);
|
|
1252
|
+
console.log(JSON.stringify({ decision: 'block', reason: refusal }));
|
|
1253
|
+
process.exit(2);
|
|
1254
|
+
}
|
|
1255
|
+
|
|
885
1256
|
if (matched) {
|
|
886
1257
|
// Destructive — require BOTH verify (from transcript) AND cognition
|
|
887
1258
|
// (inline command preferred; transcript fallback). Verify stays
|
|
@@ -1005,6 +1376,70 @@ No env-var disable path — gates are unconditional from the gated process per H
|
|
|
1005
1376
|
process.exit(2);
|
|
1006
1377
|
}
|
|
1007
1378
|
|
|
1379
|
+
// ── Dalio expected_outcome gate ──────────────────────────────────────────────
|
|
1380
|
+
//
|
|
1381
|
+
// Every non-trivial action must carry an <expected> block with at least one
|
|
1382
|
+
// measurable predicate. Block is read from the same turn-scoped assistant text
|
|
1383
|
+
// window as cognition (unionText). Qualitative drift phrases are rejected even
|
|
1384
|
+
// when they appear inside an <expected> block.
|
|
1385
|
+
//
|
|
1386
|
+
// Hard-block path: block the tool call, name the missing block, cite doctrine.
|
|
1387
|
+
// Per feedback_no_graceful_degradation.md — never silent-pass.
|
|
1388
|
+
{
|
|
1389
|
+
const expectedMatch = unionText.match(EXPECTED_BLOCK_RX);
|
|
1390
|
+
const expectedBlockText = expectedMatch ? expectedMatch[1] : '';
|
|
1391
|
+
const hasMeasurablePredicate = expectedBlockText
|
|
1392
|
+
? (MEASURABLE_PREDICATE_RX.test(expectedBlockText) && !QUALITATIVE_DRIFT_RX.test(expectedBlockText))
|
|
1393
|
+
: false;
|
|
1394
|
+
|
|
1395
|
+
if (!expectedMatch || !hasMeasurablePredicate) {
|
|
1396
|
+
const reason = expectedMatch
|
|
1397
|
+
? `Aria pre-tool gate: action requires a measurable predicate inside <expected> per doctrine:dalio_expected_required.
|
|
1398
|
+
|
|
1399
|
+
Your <expected> block 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.
|
|
1400
|
+
|
|
1401
|
+
Replace with one of:
|
|
1402
|
+
• Numeric: exit_code==0, latency<200ms, count>=1, error_rate=0%
|
|
1403
|
+
• Boolean: exit=0, status=healthy, file=exists, rc=0
|
|
1404
|
+
• State-string: "status=running", "200 OK", "count=3 of 3 passed"
|
|
1405
|
+
|
|
1406
|
+
<expected>
|
|
1407
|
+
predicate: exit_code==0 AND file=/home/hamzaibrahim1/.foo written
|
|
1408
|
+
measurable_type: boolean
|
|
1409
|
+
threshold: 0
|
|
1410
|
+
eval_window_minutes: 1
|
|
1411
|
+
</expected>
|
|
1412
|
+
|
|
1413
|
+
No bypass — doctrine:dalio_expected_required is unconditional for non-trivial actions.`
|
|
1414
|
+
: `Aria pre-tool gate: action requires an <expected> block with measurable predicate per doctrine:dalio_expected_required.
|
|
1415
|
+
|
|
1416
|
+
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.
|
|
1417
|
+
|
|
1418
|
+
Required format (add to your assistant turn before this tool call):
|
|
1419
|
+
|
|
1420
|
+
<expected>
|
|
1421
|
+
predicate: <concrete measurable assertion — e.g. "exit_code==0", "status=running", "count>=1">
|
|
1422
|
+
measurable_type: numeric | boolean | state_string
|
|
1423
|
+
threshold: <optional — the exact boundary, e.g. 0 or "healthy">
|
|
1424
|
+
eval_window_minutes: <optional — how long before this expires, e.g. 5>
|
|
1425
|
+
</expected>
|
|
1426
|
+
|
|
1427
|
+
Accepted predicates:
|
|
1428
|
+
• Numeric: >=X, <=X, ==X, X%, count=N, latency<Xms, error_rate=0%
|
|
1429
|
+
• Boolean: exit=0, exit=1, true, false, file=exists, status=healthy
|
|
1430
|
+
• State-string: "status=running", "200 OK", "exit=0", "no_error"
|
|
1431
|
+
|
|
1432
|
+
REJECTED (qualitative drift): "better", "improved", "should work", "more reliable", "cleaner"
|
|
1433
|
+
|
|
1434
|
+
No bypass — doctrine:dalio_expected_required is unconditional for non-trivial actions per feedback_implementation_coupled_cognition.md.`;
|
|
1435
|
+
|
|
1436
|
+
audit(`block-expected-missing ${toolName.toLowerCase()}`, cmdPreview);
|
|
1437
|
+
pushDecision('block', `${toolName.toLowerCase()} missing <expected> measurable predicate`);
|
|
1438
|
+
console.log(JSON.stringify({ decision: 'block', reason }));
|
|
1439
|
+
process.exit(2);
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1008
1443
|
// ── Sub-agent packet-citation check (Layer 4 — #84) ─────────────────────────
|
|
1009
1444
|
//
|
|
1010
1445
|
// When running inside a sub-agent process (detected by a non-stale handoff file
|
|
@@ -1188,25 +1623,53 @@ if (__isBootstrapConsult) {
|
|
|
1188
1623
|
bindingAuditAppend({ event: 'allow_bootstrap_consult', sessionId, target: __bindingActionClassification.target, toolName });
|
|
1189
1624
|
}
|
|
1190
1625
|
if (BINDING_ENABLED && !bindingBypassReason && !__isBootstrapConsult) {
|
|
1191
|
-
|
|
1626
|
+
let plan = loadActivePlan(sessionId);
|
|
1192
1627
|
if (!plan) {
|
|
1193
|
-
|
|
1194
|
-
|
|
1628
|
+
// INLINE architect-fallback (Hamza directive 2026-04-27 — no async stop-gate
|
|
1629
|
+
// races). When no plan exists, fire architect-fallback synchronously here
|
|
1630
|
+
// so a real plan is minted and loaded inside the same hook execution. If
|
|
1631
|
+
// architect-fallback fails the gate still BLOCKS per doctrine (no bypass)
|
|
1632
|
+
// but the block message includes the architect-fallback exit details so the
|
|
1633
|
+
// failure is visible LOUDLY per feedback_no_graceful_degradation.md.
|
|
1634
|
+
bindingAuditAppend({ event: 'no_plan_inline_fallback_attempt', sessionId, toolName });
|
|
1635
|
+
const blockerReason = `pre-tool-gate inline fallback: no active plan for session ${sessionId} when ${toolName} was attempted on ${cmdPreview.slice(0, 200)}`;
|
|
1636
|
+
const fallbackEvent = JSON.stringify({ reason: blockerReason, sessionId, toolName, currentPlanId: 'none' });
|
|
1637
|
+
let architectExit = -1;
|
|
1638
|
+
let architectStderr = '';
|
|
1639
|
+
try {
|
|
1640
|
+
const archProc = spawnSync(process.execPath, [`${HOME}/.claude/hooks/aria-architect-fallback.mjs`], {
|
|
1641
|
+
input: fallbackEvent,
|
|
1642
|
+
encoding: 'utf8',
|
|
1643
|
+
timeout: 60000,
|
|
1644
|
+
});
|
|
1645
|
+
architectExit = archProc.status ?? -1;
|
|
1646
|
+
architectStderr = (archProc.stderr || '').slice(0, 500);
|
|
1647
|
+
} catch (err) {
|
|
1648
|
+
architectStderr = String(err).slice(0, 500);
|
|
1649
|
+
}
|
|
1650
|
+
bindingAuditAppend({ event: 'architect_fallback_result', sessionId, exit: architectExit, stderr: architectStderr.slice(0, 300) });
|
|
1195
1651
|
|
|
1196
|
-
|
|
1652
|
+
plan = loadActivePlan(sessionId);
|
|
1653
|
+
if (plan) {
|
|
1654
|
+
process.stderr.write(`\n✓ PRE-TOOL-GATE FALLBACK: architect-fallback minted plan ${plan.planId}. Continuing.\n`);
|
|
1655
|
+
bindingAuditAppend({ event: 'architect_fallback_minted_plan', sessionId, planId: plan.planId });
|
|
1656
|
+
} else {
|
|
1657
|
+
bindingAuditAppend({ event: 'block_no_active_plan_after_fallback', sessionId, toolName, architectExit });
|
|
1658
|
+
const reason = `Aria binding gate: no active plan exists AND inline architect-fallback failed (exit=${architectExit}). Plan-mint chain broken. ${architectStderr ? 'Architect stderr: ' + architectStderr : ''}
|
|
1197
1659
|
|
|
1198
1660
|
What Claude must do:
|
|
1199
|
-
1. Acknowledge to Hamza that
|
|
1200
|
-
2.
|
|
1201
|
-
3. Wait for
|
|
1661
|
+
1. Acknowledge to Hamza that the architect-fallback chain failed (visible in audit log)
|
|
1662
|
+
2. Surface the failure LOUDLY — this is a substrate-level break, not a routine consult miss
|
|
1663
|
+
3. Wait for next user prompt — preprompt-consult will retry; if it succeeds a plan will exist on next tool call
|
|
1202
1664
|
|
|
1203
|
-
Non-trivial actions are blocked until a plan exists. Trivial reads (ls/cat/grep) bypass automatically per existing whitelist. To temporarily disable binding for
|
|
1204
|
-
|
|
1205
|
-
|
|
1665
|
+
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).`;
|
|
1666
|
+
console.log(JSON.stringify({ decision: 'block', reason }));
|
|
1667
|
+
process.exit(2);
|
|
1668
|
+
}
|
|
1206
1669
|
}
|
|
1207
1670
|
|
|
1208
1671
|
const transcriptText = unionText || '';
|
|
1209
|
-
|
|
1672
|
+
let phaseInfo = pickCurrentPhase(plan, transcriptText);
|
|
1210
1673
|
|
|
1211
1674
|
if (!phaseInfo) {
|
|
1212
1675
|
// All phases reported complete — needs new consult before more action
|
|
@@ -1224,12 +1687,50 @@ This prevents Claude from drifting past Aria's authorized scope.`;
|
|
|
1224
1687
|
}
|
|
1225
1688
|
|
|
1226
1689
|
if (phaseInfo.abortedHere) {
|
|
1227
|
-
|
|
1228
|
-
|
|
1690
|
+
// INLINE architect-fallback (Hamza directive 2026-04-27 — same recovery as
|
|
1691
|
+
// no-plan branch above). When phase is aborted, fire architect-fallback
|
|
1692
|
+
// synchronously here to mint a fresh plan instead of deadlocking. If
|
|
1693
|
+
// architect-fallback fails the gate still BLOCKS per doctrine but the
|
|
1694
|
+
// failure is LOUD per feedback_no_graceful_degradation.md.
|
|
1695
|
+
bindingAuditAppend({ event: 'phase_aborted_inline_fallback_attempt', sessionId, planId: plan.planId, phaseId: phaseInfo.phase.id, toolName });
|
|
1696
|
+
const blockerReason = `pre-tool-gate inline fallback: phase ${phaseInfo.phase.id} of plan ${plan.planId} aborted, ${toolName} attempted on ${cmdPreview.slice(0, 200)}`;
|
|
1697
|
+
const fallbackEvent = JSON.stringify({ reason: blockerReason, sessionId, toolName, currentPlanId: plan.planId });
|
|
1698
|
+
let architectExit = -1;
|
|
1699
|
+
let architectStderr = '';
|
|
1700
|
+
try {
|
|
1701
|
+
const archProc = spawnSync(process.execPath, [`${HOME}/.claude/hooks/aria-architect-fallback.mjs`], {
|
|
1702
|
+
input: fallbackEvent,
|
|
1703
|
+
encoding: 'utf8',
|
|
1704
|
+
timeout: 60000,
|
|
1705
|
+
});
|
|
1706
|
+
architectExit = archProc.status ?? -1;
|
|
1707
|
+
architectStderr = (archProc.stderr || '').slice(0, 500);
|
|
1708
|
+
} catch (err) {
|
|
1709
|
+
architectStderr = String(err).slice(0, 500);
|
|
1710
|
+
}
|
|
1711
|
+
bindingAuditAppend({ event: 'aborted_phase_architect_fallback_result', sessionId, exit: architectExit, stderr: architectStderr.slice(0, 300) });
|
|
1712
|
+
|
|
1713
|
+
const freshPlan = loadActivePlan(sessionId);
|
|
1714
|
+
if (freshPlan && freshPlan.planId !== plan.planId) {
|
|
1715
|
+
process.stderr.write(`\n✓ PRE-TOOL-GATE FALLBACK: aborted phase recovered, fresh plan ${freshPlan.planId} minted. Continuing.\n`);
|
|
1716
|
+
bindingAuditAppend({ event: 'aborted_phase_recovered', sessionId, oldPlanId: plan.planId, newPlanId: freshPlan.planId });
|
|
1717
|
+
plan = freshPlan;
|
|
1718
|
+
const freshPhaseInfo = pickCurrentPhase(plan, transcriptText);
|
|
1719
|
+
if (!freshPhaseInfo || freshPhaseInfo.abortedHere) {
|
|
1720
|
+
bindingAuditAppend({ event: 'block_aborted_phase_post_fallback_still_bad', sessionId, planId: plan.planId });
|
|
1721
|
+
const reason = `Aria binding gate: aborted phase recovery minted plan ${plan.planId} but new plan also has no usable phase. Manual intervention required.`;
|
|
1722
|
+
console.log(JSON.stringify({ decision: 'block', reason }));
|
|
1723
|
+
process.exit(2);
|
|
1724
|
+
}
|
|
1725
|
+
phaseInfo = freshPhaseInfo;
|
|
1726
|
+
} else {
|
|
1727
|
+
bindingAuditAppend({ event: 'block_phase_aborted_fallback_failed', sessionId, planId: plan.planId, phaseId: phaseInfo.phase.id, architectExit });
|
|
1728
|
+
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 : ''}
|
|
1229
1729
|
|
|
1230
1730
|
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.`;
|
|
1231
|
-
|
|
1232
|
-
|
|
1731
|
+
console.log(JSON.stringify({ decision: 'block', reason }));
|
|
1732
|
+
process.exit(2);
|
|
1733
|
+
}
|
|
1233
1734
|
}
|
|
1234
1735
|
|
|
1235
1736
|
const { action, target } = classifyToolForBinding(toolName, cmd, filePath);
|
|
@@ -1239,13 +1740,22 @@ What Claude must do: emit [PLAN_BLOCKER reason="<concrete observation>" suggeste
|
|
|
1239
1740
|
const forbidden = (phase.forbiddenActions || []).find((p) => actionMatchesPattern(action, p, target));
|
|
1240
1741
|
if (forbidden) {
|
|
1241
1742
|
bindingAuditAppend({ event: 'block_forbidden_action', sessionId, planId: plan.planId, phaseId: phase.id, action, target, matchedRule: forbidden });
|
|
1743
|
+
// Hive recipe lookup BEFORE emitting the block — if the same shape has
|
|
1744
|
+
// fired before, surface the recipe inline. Lookup is fail-soft: a probe
|
|
1745
|
+
// failure leaves the block message unchanged.
|
|
1746
|
+
const lookup = await lookupBlockPatternRecipe({
|
|
1747
|
+
detectorClass: 'doctrine_violation',
|
|
1748
|
+
signature: `binding-forbidden::action=${action}::matches=${forbidden}`,
|
|
1749
|
+
tenantId: sessionId,
|
|
1750
|
+
});
|
|
1751
|
+
const recipeAddendum = renderRecipeAddendum(lookup);
|
|
1242
1752
|
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}.
|
|
1243
1753
|
|
|
1244
1754
|
Phase summary: ${phase.summary}
|
|
1245
1755
|
Forbidden actions for this phase: ${(phase.forbiddenActions || []).join(', ') || '(none)'}
|
|
1246
1756
|
Allowed actions for this phase: ${(phase.allowedActions || []).join(', ') || '(none)'}
|
|
1247
1757
|
|
|
1248
|
-
Claude must either: (a) reframe the action to fit allowedActions, OR (b) emit [PLAN_BLOCKER reason="..."] requesting Aria amend the plan
|
|
1758
|
+
Claude must either: (a) reframe the action to fit allowedActions, OR (b) emit [PLAN_BLOCKER reason="..."] requesting Aria amend the plan.${recipeAddendum}`;
|
|
1249
1759
|
console.log(JSON.stringify({ decision: 'block', reason }));
|
|
1250
1760
|
process.exit(2);
|
|
1251
1761
|
}
|
|
@@ -1253,12 +1763,18 @@ Claude must either: (a) reframe the action to fit allowedActions, OR (b) emit [P
|
|
|
1253
1763
|
const allowed = (phase.allowedActions || []).find((p) => actionMatchesPattern(action, p, target));
|
|
1254
1764
|
if (!allowed) {
|
|
1255
1765
|
bindingAuditAppend({ event: 'block_action_not_in_allowed_list', sessionId, planId: plan.planId, phaseId: phase.id, action, target });
|
|
1766
|
+
const lookup = await lookupBlockPatternRecipe({
|
|
1767
|
+
detectorClass: 'doctrine_violation',
|
|
1768
|
+
signature: `binding-not-allowed::action=${action}::phase=${phase.id}`,
|
|
1769
|
+
tenantId: sessionId,
|
|
1770
|
+
});
|
|
1771
|
+
const recipeAddendum = renderRecipeAddendum(lookup);
|
|
1256
1772
|
const reason = `Aria binding gate: action "${action}" on target "${target}" is NOT in allowedActions for current phase ${phase.id} of plan ${plan.planId}.
|
|
1257
1773
|
|
|
1258
1774
|
Phase summary: ${phase.summary}
|
|
1259
1775
|
Allowed actions: ${(phase.allowedActions || []).join(', ') || '(none — phase is observation-only)'}
|
|
1260
1776
|
|
|
1261
|
-
Claude must either: (a) reframe action to fit allowedActions, OR (b) emit [PLAN_BLOCKER reason="..."] for plan amendment
|
|
1777
|
+
Claude must either: (a) reframe action to fit allowedActions, OR (b) emit [PLAN_BLOCKER reason="..."] for plan amendment.${recipeAddendum}`;
|
|
1262
1778
|
console.log(JSON.stringify({ decision: 'block', reason }));
|
|
1263
1779
|
process.exit(2);
|
|
1264
1780
|
}
|
|
@@ -1280,7 +1796,11 @@ Claude must either: (a) reframe action to fit allowedActions, OR (b) emit [PLAN_
|
|
|
1280
1796
|
// is a DETECTION PROBE — if the endpoint is alive it will respond quickly; if it
|
|
1281
1797
|
// is down the catch path fail-opens without blocking the developer.
|
|
1282
1798
|
(async function checkOutcomeLedger() {
|
|
1283
|
-
const _harnessUrl =
|
|
1799
|
+
const _harnessUrl =
|
|
1800
|
+
process.env.ARIA_HIVE_RUNTIME_URL ||
|
|
1801
|
+
process.env.ARIA_HARNESS_BASE_URL ||
|
|
1802
|
+
process.env.ARIA_HARNESS_URL ||
|
|
1803
|
+
'https://harness.ariasos.com';
|
|
1284
1804
|
const _harnessToken = process.env.ARIA_HARNESS_TOKEN || '';
|
|
1285
1805
|
|
|
1286
1806
|
// Derive action_kind and action_target from current tool call
|
|
@@ -1340,7 +1860,274 @@ Claude must either: (a) reframe action to fit allowedActions, OR (b) emit [PLAN_
|
|
|
1340
1860
|
}
|
|
1341
1861
|
})();
|
|
1342
1862
|
|
|
1343
|
-
//
|
|
1863
|
+
// ── Substrate-bound contract gate via SDK.checkAction ──────────────────────
|
|
1864
|
+
// Hamza directive 2026-04-28: local cognition substance + binding-plan gates
|
|
1865
|
+
// pass, but the substrate has its own contract gate that knows about deploy
|
|
1866
|
+
// state, soul-charge, hospital admission policy, etc. checkAction returns
|
|
1867
|
+
// { allowed, reason, requiredGates }. allowed:false halts the tool with the
|
|
1868
|
+
// substrate's reason. SDK call failure is non-blocking (fail-open) — halting
|
|
1869
|
+
// every tool when substrate is down would brick the orchestrator, but the
|
|
1870
|
+
// failure is logged for telemetry.
|
|
1871
|
+
try {
|
|
1872
|
+
const { HTTPHarnessClient } = await import('@aria/harness-http-client');
|
|
1873
|
+
const { readFileSync: __fsRead, existsSync: __fsExists } = await import('node:fs');
|
|
1874
|
+
const { homedir: __homedir } = await import('node:os');
|
|
1875
|
+
const __home = __homedir();
|
|
1876
|
+
const __tokenPath = `${__home}/.aria/owner-token`;
|
|
1877
|
+
const __licensePath = `${__home}/.aria/license.json`;
|
|
1878
|
+
// Tier detection: client if license.json has a jti, else owner.
|
|
1879
|
+
// Hamza correction 2026-04-28b: master/owner-token credentials belong
|
|
1880
|
+
// to Hamza only — never resolve them on client-tier processes.
|
|
1881
|
+
let __isOwner = true;
|
|
1882
|
+
try {
|
|
1883
|
+
if (__fsExists(__licensePath)) {
|
|
1884
|
+
const __lic = JSON.parse(__fsRead(__licensePath, 'utf8'));
|
|
1885
|
+
__isOwner = !__lic.jti;
|
|
1886
|
+
}
|
|
1887
|
+
} catch { __isOwner = true; }
|
|
1888
|
+
// Resolution: ARIA_HARNESS_TOKEN env first (both tiers). ONLY on owner
|
|
1889
|
+
// tier, fall back to master/api-key env or owner-token file.
|
|
1890
|
+
let __apiKey = process.env.ARIA_HARNESS_TOKEN || '';
|
|
1891
|
+
if (!__apiKey && __isOwner) {
|
|
1892
|
+
__apiKey = process.env.ARIA_MASTER_TOKEN
|
|
1893
|
+
|| process.env.ARIA_API_KEY
|
|
1894
|
+
|| (__fsExists(__tokenPath) ? __fsRead(__tokenPath, 'utf8').trim() : '');
|
|
1895
|
+
}
|
|
1896
|
+
// Map Claude tool names → checkAction action enum (substrate signature is
|
|
1897
|
+
// 'deploy'|'build'|'write'|'delete'). Most state-mutating tools map to
|
|
1898
|
+
// 'write'; explicit deploy/destructive cases would map elsewhere if the
|
|
1899
|
+
// substrate widens the enum later.
|
|
1900
|
+
const __toolToAction = { Bash: 'write', Edit: 'write', Write: 'write', NotebookEdit: 'write' };
|
|
1901
|
+
const __action = __toolToAction[toolName];
|
|
1902
|
+
if (__apiKey && __action) {
|
|
1903
|
+
const __client = new HTTPHarnessClient({
|
|
1904
|
+
baseUrl:
|
|
1905
|
+
process.env.ARIA_HIVE_RUNTIME_URL ||
|
|
1906
|
+
process.env.ARIA_HARNESS_BASE_URL ||
|
|
1907
|
+
process.env.ARIA_HARNESS_URL ||
|
|
1908
|
+
'https://harness.ariasos.com',
|
|
1909
|
+
apiKey: __apiKey,
|
|
1910
|
+
});
|
|
1911
|
+
const __target = JSON.stringify(toolInput || {}).slice(0, 500);
|
|
1912
|
+
const __check = await __client.checkAction(__action, __target);
|
|
1913
|
+
if (__check && __check.allowed === false) {
|
|
1914
|
+
const __reason = `Aria substrate checkAction DENIED for ${toolName}/${__action}: ${__check.reason || 'no reason given'}. Required gates: [${(__check.requiredGates || []).join(', ')}].
|
|
1915
|
+
|
|
1916
|
+
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.`;
|
|
1917
|
+
audit(`block-substrate-checkAction ${toolName.toLowerCase()} action=${__action} reason="${(__check.reason || '').slice(0, 80)}"`, cmdPreview);
|
|
1918
|
+
pushDecision('block', `substrate checkAction denied: ${(__check.reason || 'unspecified').slice(0, 100)}`);
|
|
1919
|
+
console.log(JSON.stringify({ decision: 'block', reason: __reason }));
|
|
1920
|
+
process.exit(2);
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
} catch (err) {
|
|
1924
|
+
// SDK call failure is non-blocking — gate degrades to local-only doctrine.
|
|
1925
|
+
// The failure is recorded so a fleet probe can detect substrate-side
|
|
1926
|
+
// contract gate outages.
|
|
1927
|
+
console.warn(`[pre-tool-gate] substrate checkAction failed: ${err && err.message ? err.message : err}`);
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
// ── Hive session-lock check ───────────────────────────────────────────────────
|
|
1931
|
+
// For Edit/Write/NotebookEdit: check file_path against hive_session_locks.
|
|
1932
|
+
// For Bash with file-mutating verbs (rm, mv, sed -i, awk, tee, cp, chmod,
|
|
1933
|
+
// truncate, install): extract the file argument and check it.
|
|
1934
|
+
//
|
|
1935
|
+
// If an active lock exists from a DIFFERENT session, BLOCK with coordination
|
|
1936
|
+
// instructions. Fail-open only on network error (endpoint unreachable).
|
|
1937
|
+
//
|
|
1938
|
+
// Per feedback_no_graceful_degradation.md: parse errors and non-network
|
|
1939
|
+
// failures surface in the block reason — they are not silently ignored.
|
|
1940
|
+
// Per feedback_no_timeouts_doctrine.md: no AbortSignal or setTimeout.
|
|
1941
|
+
(async function checkHiveSessionLock() {
|
|
1942
|
+
// Determine the file path to check
|
|
1943
|
+
let _lockCheckPath = '';
|
|
1944
|
+
|
|
1945
|
+
if (toolName === 'Edit' || toolName === 'Write' || toolName === 'NotebookEdit') {
|
|
1946
|
+
_lockCheckPath = filePath;
|
|
1947
|
+
} else if (toolName === 'Bash') {
|
|
1948
|
+
// Destructive bash verbs that mutate files — extract the file argument
|
|
1949
|
+
const _mutatingVerbRx = /^(rm|mv|cp|sed\s+-i|awk\s+.*-i|tee|truncate|install|chmod|chown)\b/;
|
|
1950
|
+
if (_mutatingVerbRx.test(cmd.trim())) {
|
|
1951
|
+
// Extract the first file-path argument: look for something with /
|
|
1952
|
+
const _pathMatch = cmd.match(/\s+((?:\/|~\/|\.\.?\/|[\w.-]+\/)[^\s'"]+)/);
|
|
1953
|
+
if (_pathMatch) _lockCheckPath = _pathMatch[1];
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
if (!_lockCheckPath) return; // no file target — skip lock check
|
|
1958
|
+
|
|
1959
|
+
const _soulUrl =
|
|
1960
|
+
process.env.ARIA_HIVE_RUNTIME_URL ||
|
|
1961
|
+
process.env.ARIA_SOUL_URL ||
|
|
1962
|
+
process.env.ARIA_HARNESS_BASE_URL ||
|
|
1963
|
+
process.env.ARIA_HARNESS_URL ||
|
|
1964
|
+
'https://harness.ariasos.com';
|
|
1965
|
+
const _harnessToken = process.env.ARIA_HARNESS_TOKEN || '';
|
|
1966
|
+
|
|
1967
|
+
let _lockResp;
|
|
1968
|
+
try {
|
|
1969
|
+
const _params = new URLSearchParams({ file_path: _lockCheckPath });
|
|
1970
|
+
_lockResp = await fetch(`${_soulUrl}/api/hive/session-lock?${_params}`, {
|
|
1971
|
+
method: 'GET',
|
|
1972
|
+
headers: {
|
|
1973
|
+
'Content-Type': 'application/json',
|
|
1974
|
+
...(_harnessToken ? { Authorization: `Bearer ${_harnessToken}` } : {}),
|
|
1975
|
+
},
|
|
1976
|
+
});
|
|
1977
|
+
} catch (_netErr) {
|
|
1978
|
+
// Endpoint unreachable — fail-open. Lock check is a coordination layer,
|
|
1979
|
+
// not a safety hard-stop. Infra-down must not block all development.
|
|
1980
|
+
audit(`allow-lock-check-network-error path=${_lockCheckPath.slice(0, 80)}`, cmdPreview);
|
|
1981
|
+
return;
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
if (!_lockResp.ok) {
|
|
1985
|
+
// Non-200 — route may not be deployed yet. Fail-open with audit record.
|
|
1986
|
+
audit(`allow-lock-check-http-error status=${_lockResp.status} path=${_lockCheckPath.slice(0, 80)}`, cmdPreview);
|
|
1987
|
+
return;
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
// Per feedback_no_graceful_degradation.md: JSON parse error is surfaced,
|
|
1991
|
+
// not swallowed. A malformed response IS a defect that must be visible.
|
|
1992
|
+
const _lockData = await _lockResp.json();
|
|
1993
|
+
const _activeLocks = Array.isArray(_lockData?.locks) ? _lockData.locks : [];
|
|
1994
|
+
|
|
1995
|
+
// Filter to locks from a DIFFERENT session
|
|
1996
|
+
const _conflictingLocks = _activeLocks.filter(
|
|
1997
|
+
(l) => l.session_id && String(l.session_id) !== String(sessionId),
|
|
1998
|
+
);
|
|
1999
|
+
|
|
2000
|
+
if (_conflictingLocks.length === 0) {
|
|
2001
|
+
audit(`allow-lock-clear path=${_lockCheckPath.slice(0, 80)}`, cmdPreview);
|
|
2002
|
+
return; // no conflict — proceed
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
// ── Auto-post coordination message to each conflicting session ───────────
|
|
2006
|
+
// Per hive-session-coordination doctrine (memory:feedback_hive_session_coordination.md):
|
|
2007
|
+
// when a lock conflict is detected, the gate AUTOMATICALLY posts a
|
|
2008
|
+
// lock_conflict_request session-message to each conflicting session so they see
|
|
2009
|
+
// the inbound coordination request in their next turn's HIVE_SESSION_INBOX block.
|
|
2010
|
+
// The model never has to manually post — the gate is the structural binding.
|
|
2011
|
+
//
|
|
2012
|
+
// Per feedback_no_timeouts_doctrine.md: no AbortSignal or setTimeout.
|
|
2013
|
+
// Per feedback_no_graceful_degradation.md: message-post failures are logged
|
|
2014
|
+
// loudly to stderr; they do NOT silently degrade — the block still fires regardless.
|
|
2015
|
+
const _autoMessageIds = [];
|
|
2016
|
+
for (const _conflict of _conflictingLocks) {
|
|
2017
|
+
try {
|
|
2018
|
+
const _msgId = `lock-conflict-${sessionId}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`;
|
|
2019
|
+
const _requestedAt = new Date().toISOString();
|
|
2020
|
+
const _msgBody = {
|
|
2021
|
+
coordination_protocol: 'aria-hive-lock-conflict/v2',
|
|
2022
|
+
coordination_key: `file:${_lockCheckPath}`,
|
|
2023
|
+
file_path: _lockCheckPath,
|
|
2024
|
+
requesting_session_id: sessionId,
|
|
2025
|
+
requesting_client_id: 'aria-connector',
|
|
2026
|
+
requesting_surface: 'pre-tool-gate',
|
|
2027
|
+
intent_summary: `Session ${sessionId} attempted to edit ${_lockCheckPath} via ${toolName} but found your active lock. Requesting coordination — please release when done.`,
|
|
2028
|
+
requested_at: _requestedAt,
|
|
2029
|
+
tool_name: toolName,
|
|
2030
|
+
command_preview: cmdPreview,
|
|
2031
|
+
file_claims: [{ path: _lockCheckPath, intent: 'edit' }],
|
|
2032
|
+
target_lock: {
|
|
2033
|
+
lock_id: _conflict.lock_id ?? null,
|
|
2034
|
+
session_id: _conflict.session_id ?? null,
|
|
2035
|
+
client_id: _conflict.client_id ?? null,
|
|
2036
|
+
surface: _conflict.surface ?? null,
|
|
2037
|
+
locked_at: _conflict.locked_at ?? null,
|
|
2038
|
+
expires_at: _conflict.expires_at ?? null,
|
|
2039
|
+
},
|
|
2040
|
+
};
|
|
2041
|
+
const _msgResp = await fetch(`${_soulUrl}/api/hive/session-message`, {
|
|
2042
|
+
method: 'POST',
|
|
2043
|
+
headers: {
|
|
2044
|
+
'Content-Type': 'application/json',
|
|
2045
|
+
...(_harnessToken ? { Authorization: `Bearer ${_harnessToken}` } : {}),
|
|
2046
|
+
},
|
|
2047
|
+
body: JSON.stringify({
|
|
2048
|
+
message_id: _msgId,
|
|
2049
|
+
from_session_id: sessionId,
|
|
2050
|
+
to_session_id: _conflict.session_id,
|
|
2051
|
+
topic: 'lock_conflict_request',
|
|
2052
|
+
body: _msgBody,
|
|
2053
|
+
}),
|
|
2054
|
+
});
|
|
2055
|
+
if (_msgResp.ok) {
|
|
2056
|
+
_autoMessageIds.push({ session_id: _conflict.session_id, message_id: _msgId });
|
|
2057
|
+
audit(`auto-msg-sent lock-conflict to=${_conflict.session_id} msg=${_msgId} path=${_lockCheckPath.slice(0, 60)}`, cmdPreview);
|
|
2058
|
+
} else {
|
|
2059
|
+
const _errText = await _msgResp.text().catch(() => 'unreadable');
|
|
2060
|
+
process.stderr.write(
|
|
2061
|
+
`[aria-pre-tool-gate] WARN auto-message to session ${_conflict.session_id} failed: HTTP ${_msgResp.status} ${_errText.slice(0, 200)}\n`,
|
|
2062
|
+
);
|
|
2063
|
+
audit(`auto-msg-failed lock-conflict to=${_conflict.session_id} status=${_msgResp.status} path=${_lockCheckPath.slice(0, 60)}`, cmdPreview);
|
|
2064
|
+
}
|
|
2065
|
+
} catch (_msgErr) {
|
|
2066
|
+
// Network error posting auto-message — log loudly per no-graceful-degradation doctrine.
|
|
2067
|
+
// Block still fires; auto-message is additive signal, not a safety gate.
|
|
2068
|
+
process.stderr.write(
|
|
2069
|
+
`[aria-pre-tool-gate] WARN auto-message to session ${_conflict.session_id} threw: ${_msgErr instanceof Error ? _msgErr.message : String(_msgErr)}\n`,
|
|
2070
|
+
);
|
|
2071
|
+
audit(`auto-msg-error lock-conflict to=${_conflict.session_id} path=${_lockCheckPath.slice(0, 60)}`, cmdPreview);
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
const _conflictDetails = _conflictingLocks.map((l) => {
|
|
2076
|
+
const _ago = l.locked_at ? `since ${l.locked_at}` : '';
|
|
2077
|
+
const _expires = l.expires_at ? `, expires ${l.expires_at}` : '';
|
|
2078
|
+
const _who = l.client_id ? ` (client: ${l.client_id})` : '';
|
|
2079
|
+
const _surface = l.surface ? ` [${l.surface}]` : '';
|
|
2080
|
+
const _sentMsg = _autoMessageIds.find((m) => m.session_id === l.session_id);
|
|
2081
|
+
const _msgNote = _sentMsg
|
|
2082
|
+
? ` [auto-coordination message posted: ${_sentMsg.message_id}]`
|
|
2083
|
+
: ' [auto-message failed — coordinate manually]';
|
|
2084
|
+
return ` - Session ${l.session_id}${_who}${_surface} holds lock on ${l.file_path} ${_ago}${_expires}${_msgNote}`;
|
|
2085
|
+
}).join('\n');
|
|
2086
|
+
|
|
2087
|
+
const _autoMsgSummary = _autoMessageIds.length > 0
|
|
2088
|
+
? `\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].`
|
|
2089
|
+
: '\nAuto-coordination message could not be delivered (see stderr). Coordinate manually via POST /api/hive/session-message.';
|
|
2090
|
+
|
|
2091
|
+
const _lockBlockReason = `Hive session-lock conflict: another session holds an active lock on this file.
|
|
2092
|
+
|
|
2093
|
+
File: ${_lockCheckPath}
|
|
2094
|
+
|
|
2095
|
+
Conflicting locks:
|
|
2096
|
+
${_conflictDetails}
|
|
2097
|
+
${_autoMsgSummary}
|
|
2098
|
+
|
|
2099
|
+
Resolution:
|
|
2100
|
+
1. A lock_conflict_request message was automatically posted to the lock-holding session. Wait for them to see it.
|
|
2101
|
+
2. They release via: aria hive lock release --lock-id <ID> OR DELETE /api/hive/session-lock.
|
|
2102
|
+
3. No automatic timeout-based release — explicit release only (memory:feedback_no_timeouts_doctrine.md).
|
|
2103
|
+
4. Retry this action — the gate allows when no conflicting lock exists.
|
|
2104
|
+
|
|
2105
|
+
Per memory:feedback_hive_session_coordination.md: blind-edit on the same file from concurrent sessions
|
|
2106
|
+
causes merge conflicts and state divergence. Explicit coordination is the only safe path.`;
|
|
2107
|
+
|
|
2108
|
+
audit(`block-hive-lock-conflict path=${_lockCheckPath.slice(0, 80)} conflicting_sessions=${_conflictingLocks.map((l) => l.session_id).join(',')} auto_msgs=${_autoMessageIds.length}`, cmdPreview);
|
|
2109
|
+
pushDecision('block', `hive session-lock conflict on ${_lockCheckPath.slice(0, 80)}`);
|
|
2110
|
+
console.log(JSON.stringify({
|
|
2111
|
+
decision: 'block',
|
|
2112
|
+
reason: _lockBlockReason,
|
|
2113
|
+
hookSpecificOutput: {
|
|
2114
|
+
hookEventName: 'PreToolUse',
|
|
2115
|
+
conflicting_locks: _conflictingLocks,
|
|
2116
|
+
auto_coordination_messages: _autoMessageIds,
|
|
2117
|
+
recovery: {
|
|
2118
|
+
action: 'wait_for_lock_release_then_retry',
|
|
2119
|
+
file_path: _lockCheckPath,
|
|
2120
|
+
conflicting_session_ids: _conflictingLocks.map((l) => l.session_id),
|
|
2121
|
+
auto_message_posted: _autoMessageIds.length > 0,
|
|
2122
|
+
send_message_endpoint: '/api/hive/session-message',
|
|
2123
|
+
release_lock_endpoint: '/api/hive/session-lock',
|
|
2124
|
+
},
|
|
2125
|
+
},
|
|
2126
|
+
}));
|
|
2127
|
+
process.exit(2);
|
|
2128
|
+
})();
|
|
2129
|
+
|
|
2130
|
+
// Non-trivial action with cognition AND (binding-allowed OR binding-disabled) AND substrate-cleared — allow.
|
|
1344
2131
|
audit(`allow-cognition ${toolName.toLowerCase()} lenses=${lensCount} via=${cognitionSource}`, cmdPreview);
|
|
1345
2132
|
pushDecision('allow', `${toolName.toLowerCase()} with ${lensCount} lenses (${cognitionSource})`);
|
|
1346
2133
|
process.exit(0);
|