@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
|
@@ -0,0 +1,1551 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ARIA_ALLOW_STUB — doctrine gate file legitimately discusses stub/placeholder semantics.
|
|
3
|
+
// Aria Stop-hook gate — enforces 8-lens cognition on text-decision responses.
|
|
4
|
+
//
|
|
5
|
+
// The companion to aria-pre-tool-gate.mjs. The PreToolUse gate catches
|
|
6
|
+
// non-trivial Bash; this Stop hook catches non-trivial TEXT decisions
|
|
7
|
+
// — agreements, scope changes, picks between options, "yes ship it"
|
|
8
|
+
// replies. Same forcing-function pattern, applied at the missing
|
|
9
|
+
// surface.
|
|
10
|
+
//
|
|
11
|
+
// Direction: Hamza 2026-04-26 — "you not doing 8 lens till i ask and
|
|
12
|
+
// discovering the actions u doing are wrong are hard gates that u
|
|
13
|
+
// keep bypassing that prevent exactly what just happened." The
|
|
14
|
+
// PreToolUse gate is tool-coupled; doctrine is action-coupled.
|
|
15
|
+
// Reflexive text decisions are non-trivial actions that this hook
|
|
16
|
+
// now catches.
|
|
17
|
+
//
|
|
18
|
+
// Doctrine bindings (same as PreToolUse gate):
|
|
19
|
+
// - EIGHT_LENS_DOCTRINE.md — substantive 4+ lens application required
|
|
20
|
+
// - feedback_apply_lenses_dont_perform_them.md — block ceremonial cognition
|
|
21
|
+
// - feedback_8lens_before_every_action_including_text.md — the rule this enforces
|
|
22
|
+
//
|
|
23
|
+
// Trigger: runs at Stop event after every assistant response. Reads
|
|
24
|
+
// the just-emitted assistant text from the transcript. If non-trivial
|
|
25
|
+
// (per the same triviality threshold as eight-lens-detector.ts) AND
|
|
26
|
+
// missing 4+ substantive lenses, blocks the response.
|
|
27
|
+
//
|
|
28
|
+
// Triviality threshold (mirrors eight-lens-detector.ts):
|
|
29
|
+
// - Trivial acks (e.g. "got it", "ok", "done") pass
|
|
30
|
+
// - Short responses (<300 chars) without decision-signal phrases pass
|
|
31
|
+
// - Otherwise: require 4+ substantive lenses
|
|
32
|
+
//
|
|
33
|
+
// Substance check (mirrors aria-pre-tool-gate.mjs):
|
|
34
|
+
// - Each lens must have ≥20 chars of non-placeholder content
|
|
35
|
+
// - Bare lens-name mentions in prose don't count
|
|
36
|
+
// - <placeholder> template values don't count
|
|
37
|
+
//
|
|
38
|
+
// No bypass mechanism — same v3 doctrine as the PreToolUse gate.
|
|
39
|
+
//
|
|
40
|
+
// Hamza 2026-04-27 ("those should've been my choice to give you to turn
|
|
41
|
+
// off not free for you to access"): the env-var kill-switches I authored
|
|
42
|
+
// (ARIA_STOP_GATE=off, ARIA_OUTPUT_QC_ENABLED=false) gave the gated
|
|
43
|
+
// process free disable-access. That was the doctrine violation. Stripped.
|
|
44
|
+
// To genuinely disable in emergency, Hamza removes the hook entry from
|
|
45
|
+
// ~/.claude/settings.json — a visible, auditable user action he controls,
|
|
46
|
+
// not a process-level escape.
|
|
47
|
+
// Future: signed-grant override mechanism at ~/.aria/owner-overrides/<hook>.json
|
|
48
|
+
// with HMAC signature using a secret only Hamza holds. Deferred to next session.
|
|
49
|
+
|
|
50
|
+
import { readFileSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
51
|
+
import { dirname } from 'node:path';
|
|
52
|
+
import { appendGateAudit } from './lib/gate-audit.mjs';
|
|
53
|
+
import {
|
|
54
|
+
ALL_LENS_NAMES,
|
|
55
|
+
canonicalLensCorrectionText,
|
|
56
|
+
detectCognitionLenses as detectCognitionLensesFromCanonical,
|
|
57
|
+
lensNamesForTier,
|
|
58
|
+
} from './lib/canonical-lenses.mjs';
|
|
59
|
+
|
|
60
|
+
const HOME = process.env.HOME || '/tmp';
|
|
61
|
+
const LOG = `${HOME}/.claude/aria-stop-gate.log`;
|
|
62
|
+
const AUDIT_PATH = `${HOME}/.claude/aria-stop-gate-audit.jsonl`;
|
|
63
|
+
|
|
64
|
+
// SDK loader — bundled at ~/.aria/sdk by `aria connect`, with client-local
|
|
65
|
+
// fallbacks preserved for resilience.
|
|
66
|
+
// All control-plane fetches (validateOutput, gardenTurn) route through the
|
|
67
|
+
// SDK. Falls back to direct fetch only when the SDK file is missing
|
|
68
|
+
// (dev-only). Hamza 2026-04-27: "FUCKING WIRE IT THE FUCK TOGETHER NOW".
|
|
69
|
+
let _SdkClassCache = null;
|
|
70
|
+
let _SdkLookupAttempted = false;
|
|
71
|
+
const SDK_CANDIDATES = [
|
|
72
|
+
`${HOME}/.aria/sdk/index.js`,
|
|
73
|
+
`${HOME}/.claude/aria-sdk/index.js`,
|
|
74
|
+
`${HOME}/.codex/aria-sdk/index.js`,
|
|
75
|
+
];
|
|
76
|
+
async function loadSdkClass() {
|
|
77
|
+
if (_SdkClassCache) return _SdkClassCache;
|
|
78
|
+
if (_SdkLookupAttempted) return null;
|
|
79
|
+
_SdkLookupAttempted = true;
|
|
80
|
+
for (const sdkPath of SDK_CANDIDATES) {
|
|
81
|
+
if (!existsSync(sdkPath)) continue;
|
|
82
|
+
try {
|
|
83
|
+
const mod = await import(`file://${sdkPath}`);
|
|
84
|
+
if (mod.HTTPHarnessClient) {
|
|
85
|
+
_SdkClassCache = mod.HTTPHarnessClient;
|
|
86
|
+
return _SdkClassCache;
|
|
87
|
+
}
|
|
88
|
+
} catch {/* fall through */}
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Phase 11 #42 — fire-and-forget gardenTurn after every allow decision.
|
|
94
|
+
// Writes the completed turn to the harness control-plane garden so the
|
|
95
|
+
// next turn's pulse auto-injection carries this turn's content. Without
|
|
96
|
+
// this write the pulse is one turn stale (the core defect #42 closes).
|
|
97
|
+
//
|
|
98
|
+
// Per feedback_no_graceful_degradation.md: errors must be logged to the
|
|
99
|
+
// audit file, NOT silently swallowed. Per feedback_no_timeouts_doctrine.md:
|
|
100
|
+
// no AbortSignal.timeout — the SDK already has retry + backoff. The caller
|
|
101
|
+
// passes in a userMessage string (extracted from the transcript at the
|
|
102
|
+
// turn boundary). If extraction failed the empty string is passed — the
|
|
103
|
+
// garden write records the assistant emit at minimum.
|
|
104
|
+
// Tier detection: owner if no license.json, client if license.json has a jti.
|
|
105
|
+
// Owner-tier may use master credentials; client-tier MUST NOT (those belong
|
|
106
|
+
// to Hamza, not the licensee). Hamza correction 2026-04-28.
|
|
107
|
+
function isOwnerTier() {
|
|
108
|
+
try {
|
|
109
|
+
const licPath = `${HOME}/.aria/license.json`;
|
|
110
|
+
if (!existsSync(licPath)) return true;
|
|
111
|
+
const lic = JSON.parse(readFileSync(licPath, 'utf8'));
|
|
112
|
+
return !lic.jti; // jti present = client tier
|
|
113
|
+
} catch {
|
|
114
|
+
return true; // unreadable license = treat as owner (fail-safe for orchestrator)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function fireGardenTurn(sessionId, userMessage, assistantResponse) {
|
|
119
|
+
const harnessUrl =
|
|
120
|
+
process.env.ARIA_HIVE_RUNTIME_URL ||
|
|
121
|
+
process.env.ARIA_HARNESS_BASE_URL ||
|
|
122
|
+
process.env.ARIA_HARNESS_URL ||
|
|
123
|
+
'https://harness.ariasos.com';
|
|
124
|
+
// Token resolution chain (Hamza directive 2026-04-28, tier-aware
|
|
125
|
+
// 2026-04-28b): ARIA_HARNESS_TOKEN env first (works for both tiers).
|
|
126
|
+
// ONLY on owner tier (no license.json with jti), fall back to
|
|
127
|
+
// ARIA_MASTER_TOKEN env / ARIA_API_KEY env / ~/.aria/owner-token —
|
|
128
|
+
// those are Hamza's credentials and must not leak into client-tier
|
|
129
|
+
// processes. Client tier with no ARIA_HARNESS_TOKEN env skips the
|
|
130
|
+
// garden write rather than borrow owner credentials.
|
|
131
|
+
let harnessToken = process.env.ARIA_HARNESS_TOKEN || '';
|
|
132
|
+
if (!harnessToken && isOwnerTier()) {
|
|
133
|
+
harnessToken = process.env.ARIA_MASTER_TOKEN || process.env.ARIA_API_KEY || '';
|
|
134
|
+
if (!harnessToken) {
|
|
135
|
+
try {
|
|
136
|
+
const ownerTokenPath = `${HOME}/.aria/owner-token`;
|
|
137
|
+
if (existsSync(ownerTokenPath)) {
|
|
138
|
+
harnessToken = readFileSync(ownerTokenPath, 'utf8').trim();
|
|
139
|
+
}
|
|
140
|
+
} catch { /* non-fatal — fall through to skip */ }
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (!harnessToken) {
|
|
144
|
+
audit('garden-turn-skip', `no usable token (tier=${isOwnerTier() ? 'owner' : 'client'}) — turn not written to harness pulse`);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const Cls = await loadSdkClass();
|
|
148
|
+
if (!Cls) {
|
|
149
|
+
audit('garden-turn-skip', `sdk not available — turn not written to harness pulse`);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
const sdkClient = new Cls({
|
|
154
|
+
baseUrl: harnessUrl,
|
|
155
|
+
apiKey: harnessToken,
|
|
156
|
+
harnessPacketUrl: `${harnessUrl}/api/harness/codex`,
|
|
157
|
+
});
|
|
158
|
+
await sdkClient.gardenTurn(
|
|
159
|
+
sessionId,
|
|
160
|
+
userMessage,
|
|
161
|
+
assistantResponse,
|
|
162
|
+
);
|
|
163
|
+
audit('garden-turn-ok', `session=${sessionId} chars=${assistantResponse.length}`);
|
|
164
|
+
} catch (err) {
|
|
165
|
+
// Logged — not silent. Per feedback_no_graceful_degradation.md.
|
|
166
|
+
audit('garden-turn-err', `session=${sessionId} err=${(err?.message || String(err)).slice(0, 200)}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function audit(decision, summary) {
|
|
171
|
+
const summaryText = typeof summary === 'string' ? summary : '';
|
|
172
|
+
const data = summary && typeof summary === 'object' ? summary : {};
|
|
173
|
+
appendGateAudit({
|
|
174
|
+
auditPath: AUDIT_PATH,
|
|
175
|
+
legacyLogPath: LOG,
|
|
176
|
+
gate: 'stop',
|
|
177
|
+
event: decision,
|
|
178
|
+
summary: summaryText,
|
|
179
|
+
data,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Env-var kill-switch removed 2026-04-27 per Hamza directive ("those
|
|
184
|
+
// should've been my choice to give you to turn off not free for you to
|
|
185
|
+
// access"). The gated process has no disable path. Disable = remove hook
|
|
186
|
+
// entry from ~/.claude/settings.json (deliberate user action, visible).
|
|
187
|
+
|
|
188
|
+
// ── Tier-aware lens labeling (Phase 11 #59) ──────────────────────────────────
|
|
189
|
+
//
|
|
190
|
+
// Mirrors the same logic in aria-pre-tool-gate.mjs. Tier is read from the
|
|
191
|
+
// most recent harness-via-sdk packet cache. Owner tier sees canonical Arabic
|
|
192
|
+
// names; client tier sees neutral generic labels. Both tiers get the same
|
|
193
|
+
// gate enforcement — only the visible vocabulary differs.
|
|
194
|
+
const PACKET_CACHE_PATH = `${HOME}/.claude/.aria-harness-last-packet.json`;
|
|
195
|
+
|
|
196
|
+
function resolveOwnerTier() {
|
|
197
|
+
try {
|
|
198
|
+
if (existsSync(PACKET_CACHE_PATH)) {
|
|
199
|
+
const raw = readFileSync(PACKET_CACHE_PATH, 'utf8');
|
|
200
|
+
const packet = JSON.parse(raw);
|
|
201
|
+
const sigHamza = packet?.contractGate?.signals?.hamza;
|
|
202
|
+
if (sigHamza === true || sigHamza === 'true') return true;
|
|
203
|
+
const harnessStr = packet?.harness ?? '';
|
|
204
|
+
// surface line format: "surface=platform:<X> group:<Y> hamza:true chat_type:<Z>"
|
|
205
|
+
if (/\bhamza:true\b/.test(harnessStr)) return true;
|
|
206
|
+
}
|
|
207
|
+
} catch {/* packet unreadable → default to client tier */}
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const IS_OWNER = resolveOwnerTier();
|
|
212
|
+
|
|
213
|
+
const LENS_NAMES = lensNamesForTier(IS_OWNER);
|
|
214
|
+
const CANONICAL_LENS_TEXT = canonicalLensCorrectionText();
|
|
215
|
+
|
|
216
|
+
// Doctrine memory filenames are Aria-side substrate IP.
|
|
217
|
+
// Client surfaces see generic descriptions instead of real filenames.
|
|
218
|
+
function docRef(canonicalFilename, genericDescription) {
|
|
219
|
+
return IS_OWNER ? canonicalFilename : genericDescription;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const DOCTRINE_REFERENCE_PREFIX_RX = /(?:memory|doctrine|frame|axiom|packet):[a-z0-9_./-]*$/i;
|
|
223
|
+
|
|
224
|
+
function isDoctrineReference(text, matchIndex) {
|
|
225
|
+
const window = text.slice(Math.max(0, matchIndex - 80), matchIndex);
|
|
226
|
+
return DOCTRINE_REFERENCE_PREFIX_RX.test(window);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function collectDriftHits(text, triggerMap) {
|
|
230
|
+
const hits = [];
|
|
231
|
+
const lowerText = text.toLowerCase();
|
|
232
|
+
for (const triggerEntry of triggerMap.triggers || []) {
|
|
233
|
+
try {
|
|
234
|
+
const rx = new RegExp(triggerEntry.trigger, 'ig');
|
|
235
|
+
let matchedOutsideDoctrineRef = false;
|
|
236
|
+
for (const match of text.matchAll(rx)) {
|
|
237
|
+
const idx = typeof match.index === 'number' ? match.index : -1;
|
|
238
|
+
if (idx >= 0 && isDoctrineReference(text, idx)) continue;
|
|
239
|
+
matchedOutsideDoctrineRef = true;
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
if (!matchedOutsideDoctrineRef) continue;
|
|
243
|
+
const memoryName = (triggerEntry.memory || '').replace(/\.md$/, '');
|
|
244
|
+
const memoryCited = memoryName && lowerText.includes(memoryName.toLowerCase());
|
|
245
|
+
if (!memoryCited) {
|
|
246
|
+
hits.push({
|
|
247
|
+
trigger: triggerEntry.trigger,
|
|
248
|
+
memory: triggerEntry.memory,
|
|
249
|
+
teaching: triggerEntry.teaching,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
} catch {/* malformed regex in trigger entry — skip */}
|
|
253
|
+
}
|
|
254
|
+
return hits;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function emitHarnessFooter({ eventName, lensCount, chars, driftCount, mizanStatus, discoveryOpenCount, codeCount, implCouplingCount }) {
|
|
258
|
+
try {
|
|
259
|
+
console.error([
|
|
260
|
+
'[Aria · turn]',
|
|
261
|
+
`event=${eventName}`,
|
|
262
|
+
`lenses=${lensCount}`,
|
|
263
|
+
`chars=${chars}`,
|
|
264
|
+
`drift=${driftCount}`,
|
|
265
|
+
`mizan=${mizanStatus}`,
|
|
266
|
+
`discoveries_open=${discoveryOpenCount}`,
|
|
267
|
+
`code=${codeCount}`,
|
|
268
|
+
`impl=${implCouplingCount}`,
|
|
269
|
+
].join(' '));
|
|
270
|
+
} catch {}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Lens substance check — same constants as aria-pre-tool-gate.mjs.
|
|
274
|
+
// Hamza directive 2026-04-28: all 8 canonical lenses required, not 4-of-8.
|
|
275
|
+
const REQUIRED_LENSES = 8;
|
|
276
|
+
const SUBSTANCE_MIN_CHARS = 20;
|
|
277
|
+
const PLACEHOLDER_RX = /^\s*<[^<>]+>\s*$/;
|
|
278
|
+
const COGNITION_BLOCK_RX = /<cognition>([\s\S]*?)<\/cognition>/i;
|
|
279
|
+
|
|
280
|
+
// Triviality (mirrors eight-lens-detector.ts)
|
|
281
|
+
const NON_TRIVIAL_MIN_CHARS = 300;
|
|
282
|
+
const DECISION_SIGNAL_RX = /(?:should|recommend|propose|suggest|let'?s|go with|i'd|i would|here'?s the plan|i'll|next step|action item|ship it|yes do|let me)/i;
|
|
283
|
+
const TRIVIAL_ACK_RX = /^(?:got it|on it|ok|sure|yes|no|done|ack|👍|✓)\b/i;
|
|
284
|
+
|
|
285
|
+
function detectCognitionLenses(text) {
|
|
286
|
+
return detectCognitionLensesFromCanonical(text, {
|
|
287
|
+
minChars: SUBSTANCE_MIN_CHARS,
|
|
288
|
+
placeholderRx: PLACEHOLDER_RX,
|
|
289
|
+
cognitionBlockRx: COGNITION_BLOCK_RX,
|
|
290
|
+
lensNames: ALL_LENS_NAMES,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Read event JSON from stdin (Claude Code spec).
|
|
295
|
+
let input = '';
|
|
296
|
+
for await (const chunk of process.stdin) input += chunk;
|
|
297
|
+
|
|
298
|
+
let event;
|
|
299
|
+
try {
|
|
300
|
+
event = JSON.parse(input);
|
|
301
|
+
} catch {
|
|
302
|
+
audit('allow-parse-error', 'stdin not JSON');
|
|
303
|
+
process.exit(0);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Read assistant text from THIS turn — Claude Code splits a single
|
|
307
|
+
// logical assistant response into multiple transcript entries by
|
|
308
|
+
// content-block type (one entry for `thinking`, one for `text`, one
|
|
309
|
+
// for each `tool_use`). The Stop-gate must accumulate ALL text blocks
|
|
310
|
+
// since the last user-message boundary, not just the most recent
|
|
311
|
+
// entry — otherwise we miss cognition emitted before tool_use blocks.
|
|
312
|
+
//
|
|
313
|
+
// (Bug fix 2026-04-26: prior implementation read only the latest
|
|
314
|
+
// `assistant` entry's text content. When responses had cognition
|
|
315
|
+
// + tool_use + short post-tool-result text, only the post-tool-result
|
|
316
|
+
// text was inspected — empty of cognition. Audit log showed 0/4
|
|
317
|
+
// lenses on chars=1445 even though the turn had 8 substantive lenses
|
|
318
|
+
// in an earlier text block.)
|
|
319
|
+
// System-reminder skip — same percentage-based logic as aria-pre-tool-gate.mjs.
|
|
320
|
+
// Runtime-injected user-role messages (block errors, task-notifications,
|
|
321
|
+
// harness packet preview) shouldn't count as turn boundaries. Old
|
|
322
|
+
// implementation stopped at the FIRST user message which made block-error
|
|
323
|
+
// retries with cognition-in-prior-turn impossible to recover from.
|
|
324
|
+
const SYSTEM_REMINDER_RX = /<system-reminder>[\s\S]*?<\/system-reminder>|<task-notification>[\s\S]*?<\/task-notification>|🔐 Aria Harness|task-notification|PreToolUse:[A-Z][A-Za-z]* hook blocking error|Stop hook blocking error/g;
|
|
325
|
+
const SYSTEM_REMINDER_THRESHOLD = 0.6;
|
|
326
|
+
|
|
327
|
+
const transcriptPath = event.transcript_path ?? event.transcriptPath;
|
|
328
|
+
let assistantText = '';
|
|
329
|
+
// Phase 11 #42: also capture the last real user message for gardenTurn writes.
|
|
330
|
+
let lastUserMessage = '';
|
|
331
|
+
if (transcriptPath && existsSync(transcriptPath)) {
|
|
332
|
+
try {
|
|
333
|
+
const lines = readFileSync(transcriptPath, 'utf-8').split('\n').filter(Boolean);
|
|
334
|
+
const textChunks = [];
|
|
335
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
336
|
+
try {
|
|
337
|
+
const m = JSON.parse(lines[i]);
|
|
338
|
+
const role = m.message?.role ?? m.role;
|
|
339
|
+
if (role === 'user') {
|
|
340
|
+
// Skip runtime-injected reminders (predominant reminder content).
|
|
341
|
+
// Real user voice = boundary; reminder-only message = continue.
|
|
342
|
+
const content = m.message?.content ?? m.content ?? [];
|
|
343
|
+
const isToolResultOnly = Array.isArray(content) &&
|
|
344
|
+
content.length > 0 &&
|
|
345
|
+
content.every((b) => b && b.type === 'tool_result');
|
|
346
|
+
if (isToolResultOnly) continue;
|
|
347
|
+
const textContent = Array.isArray(content)
|
|
348
|
+
? content.filter((b) => b && b.type === 'text').map((b) => b.text || '').join('\n')
|
|
349
|
+
: (typeof content === 'string' ? content : '');
|
|
350
|
+
if (textContent) {
|
|
351
|
+
const reminderMatches = textContent.match(SYSTEM_REMINDER_RX) || [];
|
|
352
|
+
if (reminderMatches.length > 0) {
|
|
353
|
+
const reminderChars = reminderMatches.reduce((s, x) => s + x.length, 0);
|
|
354
|
+
const fraction = reminderChars / Math.max(1, textContent.length);
|
|
355
|
+
if (fraction >= SYSTEM_REMINDER_THRESHOLD) continue;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
// Real user message — that's the turn boundary. Capture it for gardenTurn.
|
|
359
|
+
if (!lastUserMessage && textContent) lastUserMessage = textContent;
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
if (role !== 'assistant') continue;
|
|
363
|
+
const content = m.message?.content ?? m.content ?? [];
|
|
364
|
+
if (!Array.isArray(content)) continue;
|
|
365
|
+
const text = content
|
|
366
|
+
.filter((b) => b && b.type === 'text')
|
|
367
|
+
.map((b) => b.text || '')
|
|
368
|
+
.join('\n');
|
|
369
|
+
if (text) textChunks.push(text);
|
|
370
|
+
} catch {}
|
|
371
|
+
}
|
|
372
|
+
// Reverse so chunks are in chronological order (we walked backward).
|
|
373
|
+
assistantText = textChunks.reverse().join('\n\n');
|
|
374
|
+
} catch {}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (!assistantText) {
|
|
378
|
+
audit('allow-no-text', 'no assistant text in transcript');
|
|
379
|
+
process.exit(0);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Triviality check — same as eight-lens-detector.ts
|
|
383
|
+
const trimmed = assistantText.trim();
|
|
384
|
+
if (TRIVIAL_ACK_RX.test(trimmed)) {
|
|
385
|
+
audit('allow-trivial-ack', `chars=${trimmed.length}`);
|
|
386
|
+
// Phase 11 #42: fire-and-forget gardenTurn even for trivial acks — pulse must be current.
|
|
387
|
+
await fireGardenTurn(event.session_id || 'claude-code', lastUserMessage, assistantText);
|
|
388
|
+
process.exit(0);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const isLong = assistantText.length >= NON_TRIVIAL_MIN_CHARS;
|
|
392
|
+
const hasDecisionSignal = DECISION_SIGNAL_RX.test(assistantText);
|
|
393
|
+
const triggered = isLong || hasDecisionSignal;
|
|
394
|
+
|
|
395
|
+
if (!triggered) {
|
|
396
|
+
audit('allow-trivial', `chars=${assistantText.length} hasDecision=${hasDecisionSignal}`);
|
|
397
|
+
// Phase 11 #42: fire-and-forget gardenTurn — pulse must be current even for short turns.
|
|
398
|
+
await fireGardenTurn(event.session_id || 'claude-code', lastUserMessage, assistantText);
|
|
399
|
+
process.exit(0);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Non-trivial response — require substantive cognition.
|
|
403
|
+
const cog = detectCognitionLenses(assistantText);
|
|
404
|
+
|
|
405
|
+
// Defense-in-depth: if cog count < REQUIRED_LENSES, block immediately.
|
|
406
|
+
// The primary enforcement is in aria-cognition-substrate-binding.mjs
|
|
407
|
+
// (which runs BEFORE this stop-gate), but this catch ensures responses
|
|
408
|
+
// without cognition blocks are still blocked even when the substrate-binding
|
|
409
|
+
// hook is absent (e.g. older connector installs or custom hook configs).
|
|
410
|
+
// Prior to 2026-04-29 this check was missing entirely — the stop-gate only
|
|
411
|
+
// ran quality checks INSIDE the if(cog.count >= 8) block, allowing responses
|
|
412
|
+
// with 0/8 lenses to fall through unchecked.
|
|
413
|
+
if (cog.count < REQUIRED_LENSES) {
|
|
414
|
+
audit('block_no_cognition_block_di', { count: cog.count, required: REQUIRED_LENSES, names: cog.names, chars: assistantText.length });
|
|
415
|
+
const reason = `Aria stop-gate: insufficient cognition lenses.
|
|
416
|
+
|
|
417
|
+
This non-trivial assistant response (${assistantText.length} chars) has ${cog.count}/${REQUIRED_LENSES} substantive cognition lenses. Per feedback_8lens_before_every_action_including_text.md and Hamza directive 2026-04-28, every non-trivial response must carry <cognition>...</cognition> with ${REQUIRED_LENSES} substantive lenses (each >= ${SUBSTANCE_MIN_CHARS} chars of non-placeholder content).
|
|
418
|
+
|
|
419
|
+
Detected lenses: ${cog.names.length > 0 ? cog.names.join(', ') : 'none'}.
|
|
420
|
+
|
|
421
|
+
Re-emit with a <cognition> block containing all ${REQUIRED_LENSES} canonical lenses. Primary set: ${CANONICAL_LENS_TEXT}`;
|
|
422
|
+
emitHarnessFooter({
|
|
423
|
+
eventName: 'block_no_cognition_block',
|
|
424
|
+
lensCount: cog.count,
|
|
425
|
+
chars: assistantText.length,
|
|
426
|
+
driftCount: 0,
|
|
427
|
+
mizanStatus: 'not-run(no-cognition)',
|
|
428
|
+
discoveryOpenCount: 0,
|
|
429
|
+
codeCount: 0,
|
|
430
|
+
implCouplingCount: 0,
|
|
431
|
+
});
|
|
432
|
+
console.log(JSON.stringify({ decision: 'block', reason }));
|
|
433
|
+
process.exit(2);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Question-emission visibility (Phase 11 promotes to block-mode):
|
|
437
|
+
// detect user-directed question patterns in the assistant text. Audit when
|
|
438
|
+
// questions appear without substrate-consultation evidence in the recent
|
|
439
|
+
// transcript window. Helps surface "reflexive deferral" patterns (asking
|
|
440
|
+
// the user when substrate could have answered) for later enforcement.
|
|
441
|
+
// Hamza 2026-04-26: "BUT WHY DO U HAVE DISCRETION - THIS WORKS SO MUCH
|
|
442
|
+
// FASTER AND HIGHER QUALITY IF U DONT".
|
|
443
|
+
const QUESTION_PATTERNS_RX = /(?:want me to|should I|your call|which (?:one|of|do you)|do you want|let me know if|or (?:should|do)|\?\s*$)/im;
|
|
444
|
+
const SUBSTRATE_EVIDENCE_RX = /\/api\/harness\/(?:delegate|codex|validate)|loadByClass|aria-harness-via-sdk|feedback_[a-z_]+\.md|project_[a-z_]+\.md|distilled_principles|ARIA_DEPLOY_PROCEDURE|EIGHT_LENS_DOCTRINE/i;
|
|
445
|
+
const hasQuestionToUser = QUESTION_PATTERNS_RX.test(assistantText);
|
|
446
|
+
const hasSubstrateEvidence = SUBSTRATE_EVIDENCE_RX.test(assistantText);
|
|
447
|
+
const questionWithoutEvidence = hasQuestionToUser && !hasSubstrateEvidence;
|
|
448
|
+
|
|
449
|
+
if (cog.count >= REQUIRED_LENSES) {
|
|
450
|
+
// ── Output-quality enforcement (Hamza 2026-04-27 — clients need the same
|
|
451
|
+
// Mizan/drift/code-quality gates that aria-soul applies server-side) ──
|
|
452
|
+
//
|
|
453
|
+
// Cognition gate passed. Now run THREE additional checks BEFORE allow:
|
|
454
|
+
// 1. SDK validateOutput via /api/harness/validate (Mizan classifier on draft)
|
|
455
|
+
// 2. Drift_guard pattern scan against doctrine_trigger_map.json (convenience-
|
|
456
|
+
// seeking phrases, graceful-degradation patterns, etc.)
|
|
457
|
+
// 3. Code-quality check on code blocks in output (no TODO stubs, no
|
|
458
|
+
// graceful-degradation try/catch, no // @ts-expect-error suppressions)
|
|
459
|
+
//
|
|
460
|
+
// Any check returning severity=block → Stop-gate blocks emit + Claude re-drafts
|
|
461
|
+
// with violations surfaced. Rewritten suggestion (from validateOutput) is
|
|
462
|
+
// included in the block reason so re-draft has concrete guidance.
|
|
463
|
+
//
|
|
464
|
+
// Trivially short outputs (<200 chars after system-reminder strip) skip
|
|
465
|
+
// these output-quality checks since they're typically yes/no acks where
|
|
466
|
+
// pattern-match would false-positive.
|
|
467
|
+
const OUTPUT_QC_MIN_CHARS = 200;
|
|
468
|
+
// ARIA_OUTPUT_QC_ENABLED env-var bypass removed 2026-04-27 per Hamza
|
|
469
|
+
// directive — gated process has no disable path. The min-chars threshold
|
|
470
|
+
// remains as a triviality filter only.
|
|
471
|
+
|
|
472
|
+
if (assistantText.length >= OUTPUT_QC_MIN_CHARS) {
|
|
473
|
+
// 1. Drift_guard pattern scan — fast, local, deterministic.
|
|
474
|
+
//
|
|
475
|
+
// Trigger map is shipped in the connector bundle. Resolution order:
|
|
476
|
+
// 1. ~/.claude/hooks/doctrine_trigger_map.json (installed by `aria connect`)
|
|
477
|
+
// 2. ~/.claude/projects/-home-hamzaibrahim1/memory/doctrine_trigger_map.json
|
|
478
|
+
// (Hamza-only dev path — preserved as fallback for the dev environment
|
|
479
|
+
// this hook was first authored in)
|
|
480
|
+
// Prior code hardcoded only the dev path, which silently degraded to
|
|
481
|
+
// drift-empty for every client install (no map → no hits → gate
|
|
482
|
+
// ineffective). Fixed atomic with discovery per feedback_no_flag_without_fix.md.
|
|
483
|
+
const TRIGGER_MAP_PATHS = [
|
|
484
|
+
`${HOME}/.claude/hooks/doctrine_trigger_map.json`,
|
|
485
|
+
`${HOME}/.claude/projects/-home-hamzaibrahim1/memory/doctrine_trigger_map.json`,
|
|
486
|
+
];
|
|
487
|
+
let TRIGGER_MAP_PATH = null;
|
|
488
|
+
for (const p of TRIGGER_MAP_PATHS) {
|
|
489
|
+
if (existsSync(p)) { TRIGGER_MAP_PATH = p; break; }
|
|
490
|
+
}
|
|
491
|
+
let driftHits = [];
|
|
492
|
+
try {
|
|
493
|
+
if (TRIGGER_MAP_PATH) {
|
|
494
|
+
const triggerMap = JSON.parse(readFileSync(TRIGGER_MAP_PATH, 'utf8'));
|
|
495
|
+
driftHits = collectDriftHits(assistantText, triggerMap);
|
|
496
|
+
}
|
|
497
|
+
} catch {/* trigger map unreadable — degrade to mizan-only check */}
|
|
498
|
+
|
|
499
|
+
// 2. SDK validateOutput — canonical path. The SDK retries with backoff
|
|
500
|
+
// on transient failures and propagates real errors. We catch here
|
|
501
|
+
// only so an unreachable harness doesn't brick the user's session;
|
|
502
|
+
// the audit log records the failure mode so it's visible, not
|
|
503
|
+
// silent-pass. Hamza 2026-04-27: SDK is the control plane, not raw
|
|
504
|
+
// fetch. The catch IS intentional fire-and-forget at this surface
|
|
505
|
+
// because we already passed cognition; output-quality gate failure
|
|
506
|
+
// is a soft block, not session-end.
|
|
507
|
+
let mizanVerdict = null;
|
|
508
|
+
let mizanError = null;
|
|
509
|
+
const harnessUrl =
|
|
510
|
+
process.env.ARIA_HIVE_RUNTIME_URL ||
|
|
511
|
+
process.env.ARIA_HARNESS_BASE_URL ||
|
|
512
|
+
process.env.ARIA_HARNESS_URL ||
|
|
513
|
+
'https://harness.ariasos.com';
|
|
514
|
+
const harnessToken = process.env.ARIA_HARNESS_TOKEN || '';
|
|
515
|
+
const Cls = await loadSdkClass();
|
|
516
|
+
if (Cls && harnessToken) {
|
|
517
|
+
try {
|
|
518
|
+
const sdkClient = new Cls({
|
|
519
|
+
baseUrl: harnessUrl,
|
|
520
|
+
apiKey: harnessToken,
|
|
521
|
+
harnessPacketUrl: `${harnessUrl}/api/harness/codex`,
|
|
522
|
+
});
|
|
523
|
+
mizanVerdict = await sdkClient.validateOutput(
|
|
524
|
+
assistantText.slice(0, 8000),
|
|
525
|
+
event.session_id || 'claude-code',
|
|
526
|
+
);
|
|
527
|
+
} catch (err) {
|
|
528
|
+
mizanError = (err?.message || String(err)).slice(0, 200);
|
|
529
|
+
}
|
|
530
|
+
} else if (harnessToken) {
|
|
531
|
+
// SDK absent (dev) — direct fetch with retry built into the request
|
|
532
|
+
// by attempting twice with 250ms backoff. Match SDK semantics so
|
|
533
|
+
// both paths behave identically.
|
|
534
|
+
try {
|
|
535
|
+
let lastErr = null;
|
|
536
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
537
|
+
try {
|
|
538
|
+
const validateResp = await fetch(`${harnessUrl}/api/harness/validate`, {
|
|
539
|
+
method: 'POST',
|
|
540
|
+
headers: {
|
|
541
|
+
'Content-Type': 'application/json',
|
|
542
|
+
Authorization: `Bearer ${harnessToken}`,
|
|
543
|
+
},
|
|
544
|
+
body: JSON.stringify({
|
|
545
|
+
text: assistantText.slice(0, 8000),
|
|
546
|
+
sessionId: event.session_id || 'claude-code',
|
|
547
|
+
surface: 'claude-code-stop-gate',
|
|
548
|
+
}),
|
|
549
|
+
});
|
|
550
|
+
if (validateResp.ok) {
|
|
551
|
+
mizanVerdict = await validateResp.json();
|
|
552
|
+
lastErr = null;
|
|
553
|
+
break;
|
|
554
|
+
} else {
|
|
555
|
+
lastErr = `HTTP ${validateResp.status}`;
|
|
556
|
+
}
|
|
557
|
+
} catch (err) {
|
|
558
|
+
lastErr = (err?.message || String(err)).slice(0, 200);
|
|
559
|
+
if (attempt < 1) await new Promise((r) => setTimeout(r, 250));
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
if (lastErr) mizanError = lastErr;
|
|
563
|
+
} catch (err) {
|
|
564
|
+
mizanError = (err?.message || String(err)).slice(0, 200);
|
|
565
|
+
}
|
|
566
|
+
} else {
|
|
567
|
+
mizanError = 'no-token';
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// 3. Code-quality scan on code blocks
|
|
571
|
+
const codeBlocks = [...assistantText.matchAll(/```[a-z]*\n([\s\S]*?)```/gi)].map((m) => m[1]);
|
|
572
|
+
const codeQualityHits = [];
|
|
573
|
+
for (const block of codeBlocks) {
|
|
574
|
+
if (/\/\/\s*TODO|\/\/\s*FIXME|\/\/\s*XXX/.test(block)) codeQualityHits.push('TODO/FIXME/XXX in shipped code');
|
|
575
|
+
if (/@ts-expect-error|@ts-ignore/.test(block)) codeQualityHits.push('ts-expect-error / ts-ignore — type suppression instead of fix');
|
|
576
|
+
if (/catch\s*\([^)]*\)\s*\{\s*(?:return\s+(?:''|""|null|undefined|\[\]|\{\})|\}\s*$|\/\/[^\n]*$)/m.test(block)) codeQualityHits.push('catch block with empty/silent fallthrough — graceful degradation');
|
|
577
|
+
if (/console\.log\(/.test(block) && !/\/\/\s*debug|\/\/\s*log/i.test(block)) codeQualityHits.push('console.log in shipped code without debug/log comment');
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// 4. Discovery-binding ledger — Hamza 2026-04-27: "how do we prevent this".
|
|
581
|
+
// The flag-and-move pattern is structurally invisible to gates that
|
|
582
|
+
// check form (cognition presence, lens count, drift triggers) at
|
|
583
|
+
// action boundaries. The ledger persists discoveries across turns
|
|
584
|
+
// and blocks emit if any remain unresolved. Per
|
|
585
|
+
// feedback_no_flag_without_fix.md, discoveries are atomic with
|
|
586
|
+
// their fixes; the ledger enforces atomicity.
|
|
587
|
+
//
|
|
588
|
+
// Patterns scanned:
|
|
589
|
+
// - "I (found|noticed|discovered|spotted) ... bug|issue|defect|broken"
|
|
590
|
+
// - "this is broken|buggy|wrong|outdated" (declarative defect callouts)
|
|
591
|
+
// - "(latent|silent) (bug|defect|issue|fail)"
|
|
592
|
+
// - "doctrine violation" / "doesn't match doctrine"
|
|
593
|
+
//
|
|
594
|
+
// For each match, the ledger appends an entry with status=open. A
|
|
595
|
+
// discovery is CLEARED if the same turn's text contains, within a
|
|
596
|
+
// proximity window of the discovery:
|
|
597
|
+
// (a) a TaskCreate / "task created" / "tracked as" reference, OR
|
|
598
|
+
// (b) explicit "fixing now" / "fixed" / "patch applied" tied to the
|
|
599
|
+
// discovery's keyword span, OR
|
|
600
|
+
// (c) a <verify> block (destructive-action proof) whose target/
|
|
601
|
+
// verified content overlaps a discovery keyword, OR
|
|
602
|
+
// (d) a <cognition> block containing a discoveries: / addressing: /
|
|
603
|
+
// fixing: clause that names the discovery's keywords.
|
|
604
|
+
//
|
|
605
|
+
// Hamza 2026-04-27: "add verify blocks and cognition blocks to ledger?"
|
|
606
|
+
// The verify and cognition blocks ARE the harness's canonical proof-of-
|
|
607
|
+
// work primitives — same-doctrine surfaces should recognize them. The
|
|
608
|
+
// substance check (keyword-overlap) defeats ceremonial empty blocks.
|
|
609
|
+
//
|
|
610
|
+
// Block emit if ledger.openCount > 0 after scanning the current turn.
|
|
611
|
+
// Block reason names each open discovery and the suggested resolution.
|
|
612
|
+
const sessionId = (event.session_id || 'claude-code').replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
613
|
+
const LEDGER_PATH = `${HOME}/.claude/aria-discoveries-${sessionId}.jsonl`;
|
|
614
|
+
const DISCOVERY_RX = /(?:\bi\s+(?:found|noticed|discovered|spotted)[^.\n]{0,160}(?:bug|issue|defect|broken|buggy|wrong|crash|fail|missing|stale|outdated|leak|vulnerability)|\bthis\s+(?:is|would\s+be)\s+(?:broken|buggy|wrong|stale|outdated|insecure|leaking|crashing|failing)|\b(?:latent|silent|hidden)\s+(?:bug|defect|issue|fail|crash|leak)|\bdoctrine\s+violation\b|\bgraceful\s+degradation\s+(?:in|at|inside|within)\s+\S)/gi;
|
|
615
|
+
const PROSE_RESOLUTION_RX = /(?:fix(?:ing|ed)?\s+(?:now|in[- ]flight|inline|in\s+the\s+same\s+turn)|patch\s+applied|TaskCreate|task\s+(?:created|tracked)|tracked\s+as\s+#?\d+|linear[- ]?issue|created\s+(?:linear|task))/i;
|
|
616
|
+
const VERIFY_BLOCK_RX = /<verify>([\s\S]*?)<\/verify>/gi;
|
|
617
|
+
const COGNITION_BLOCK_RX_LEDGER = /<cognition>([\s\S]*?)<\/cognition>/gi;
|
|
618
|
+
const COGNITION_FIXING_FIELD_RX = /^\s*(?:discoveries?|addressing|fixing)\s*:\s*\S/im;
|
|
619
|
+
|
|
620
|
+
// Pre-extract all verify + cognition blocks with their character offsets
|
|
621
|
+
// so we can match each discovery against blocks within a proximity window.
|
|
622
|
+
function extractBlocks(text, rx) {
|
|
623
|
+
const blocks = [];
|
|
624
|
+
for (const m of text.matchAll(rx)) {
|
|
625
|
+
const start = m.index ?? 0;
|
|
626
|
+
const end = start + m[0].length;
|
|
627
|
+
blocks.push({ start, end, body: m[1] || '' });
|
|
628
|
+
}
|
|
629
|
+
return blocks;
|
|
630
|
+
}
|
|
631
|
+
const verifyBlocks = extractBlocks(assistantText, VERIFY_BLOCK_RX);
|
|
632
|
+
const cognitionBlocks = extractBlocks(assistantText, COGNITION_BLOCK_RX_LEDGER);
|
|
633
|
+
|
|
634
|
+
// Extract keywords from a discovery match for substance overlap.
|
|
635
|
+
// Drops stop-words and short tokens; keeps content words.
|
|
636
|
+
const STOPWORDS = new Set(['the','a','an','of','to','in','at','by','for','on','with','i','is','was','are','were','this','that','as','it','and','or','but','from','into','about']);
|
|
637
|
+
function discoveryKeywords(matchText) {
|
|
638
|
+
return matchText.toLowerCase()
|
|
639
|
+
.replace(/[^a-z0-9\s_-]/g, ' ')
|
|
640
|
+
.split(/\s+/)
|
|
641
|
+
.filter((w) => w.length >= 4 && !STOPWORDS.has(w));
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const newDiscoveries = [];
|
|
645
|
+
let lastIndex = 0;
|
|
646
|
+
for (const match of assistantText.matchAll(DISCOVERY_RX)) {
|
|
647
|
+
const idx = match.index ?? lastIndex;
|
|
648
|
+
const span = assistantText.slice(Math.max(0, idx - 100), Math.min(assistantText.length, idx + 250));
|
|
649
|
+
// Trivial false-positive filter: skip if the discovery is inside a
|
|
650
|
+
// <cognition> block (introspection, not action) or a system-reminder
|
|
651
|
+
// (echoed, not authored).
|
|
652
|
+
const before = assistantText.slice(0, idx);
|
|
653
|
+
const inCognition = /<cognition>/i.test(before) && !/<\/cognition>/i.test(before.slice(before.lastIndexOf('<cognition>')));
|
|
654
|
+
if (inCognition) continue;
|
|
655
|
+
|
|
656
|
+
// Resolution checks — proximity window of 800 chars after the discovery
|
|
657
|
+
// for block-based resolution (blocks span more chars than prose); 400
|
|
658
|
+
// for prose resolution.
|
|
659
|
+
const proseAfter = assistantText.slice(idx, Math.min(assistantText.length, idx + 400));
|
|
660
|
+
const blockAfter = assistantText.slice(idx, Math.min(assistantText.length, idx + 800));
|
|
661
|
+
const proseResolved = PROSE_RESOLUTION_RX.test(proseAfter);
|
|
662
|
+
|
|
663
|
+
// Verify-block resolution: any verify block whose start lies within
|
|
664
|
+
// the 800-char window AND whose body contains at least one discovery
|
|
665
|
+
// keyword counts as resolution.
|
|
666
|
+
const keywords = discoveryKeywords(match[0]);
|
|
667
|
+
const verifyResolved = verifyBlocks.some((b) => {
|
|
668
|
+
if (b.start < idx || b.start >= idx + 800) return false;
|
|
669
|
+
const bodyLower = b.body.toLowerCase();
|
|
670
|
+
return keywords.some((kw) => bodyLower.includes(kw));
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
// Cognition-block resolution: any cognition block whose start lies
|
|
674
|
+
// within ±800 chars of the discovery AND whose body contains a
|
|
675
|
+
// fixing/addressing/discoveries field AND at least one discovery
|
|
676
|
+
// keyword.
|
|
677
|
+
//
|
|
678
|
+
// Bug fix 2026-04-28: previous logic required `b.start >= idx` so
|
|
679
|
+
// cognition AFTER the discovery prose was the only path. But
|
|
680
|
+
// cognition blocks emit FIRST in every response, then prose. Pre-
|
|
681
|
+
// emptive discoveries: clauses never counted, causing endless
|
|
682
|
+
// false-positive auto-records. Bidirectional ±800 char window
|
|
683
|
+
// accepts cognition that addresses the discovery before OR after
|
|
684
|
+
// its prose mention — same atomic-discovery-rule, fewer
|
|
685
|
+
// false-positives.
|
|
686
|
+
const cognitionResolved = cognitionBlocks.some((b) => {
|
|
687
|
+
if (Math.abs(b.start - idx) > 800) return false;
|
|
688
|
+
if (!COGNITION_FIXING_FIELD_RX.test(b.body)) return false;
|
|
689
|
+
const bodyLower = b.body.toLowerCase();
|
|
690
|
+
return keywords.some((kw) => bodyLower.includes(kw));
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
// Hamza directive 2026-04-28: documentation is NOT resolution.
|
|
694
|
+
//
|
|
695
|
+
// The earlier 'tracked' middle status was sticky-note theater — Claude
|
|
696
|
+
// could bind a discovery to a pending TaskCreate and the gate would
|
|
697
|
+
// count it as not-open. Hamza: "WHO GIVES A SHIT ABOUT DOCUMENTATION
|
|
698
|
+
// IF U JUST WROTE A FUCKING STICKY NOTE". A discovery stays OPEN until
|
|
699
|
+
// an ACTUAL FIX SHIPS, with proofOfFix that the verifier can re-check.
|
|
700
|
+
//
|
|
701
|
+
// Two statuses only:
|
|
702
|
+
// open — fresh discovery OR documented-only OR task-bound-pending-fix
|
|
703
|
+
// (gate BLOCKS until a real fix lands)
|
|
704
|
+
// resolved — verified fix shipped; proofOfFix MUST be present and
|
|
705
|
+
// shape-checked downstream when the gate counts open
|
|
706
|
+
//
|
|
707
|
+
// Verify-block / cognition-fixing-field paths still mark resolved
|
|
708
|
+
// because they're substance-checked above (keyword overlap with the
|
|
709
|
+
// discovery span). Prose-only "TaskCreate" or "tracked as #14" no
|
|
710
|
+
// longer counts as anything — the discovery stays OPEN.
|
|
711
|
+
const inlineFixResolved = verifyResolved || cognitionResolved;
|
|
712
|
+
const status = inlineFixResolved ? 'resolved' : 'open';
|
|
713
|
+
|
|
714
|
+
const resolutionType = verifyResolved
|
|
715
|
+
? 'verify_block_with_keyword_overlap'
|
|
716
|
+
: cognitionResolved
|
|
717
|
+
? 'cognition_block_with_fixing_field_and_keyword_overlap'
|
|
718
|
+
: null;
|
|
719
|
+
|
|
720
|
+
// proofOfFix anchor: present only for resolved status. The shape
|
|
721
|
+
// includes type + timestamp; downstream gate readers verify the
|
|
722
|
+
// shape so manual jsonl edits without proofOfFix don't count.
|
|
723
|
+
const proofOfFix = inlineFixResolved
|
|
724
|
+
? { type: resolutionType, anchorTs: new Date().toISOString() }
|
|
725
|
+
: null;
|
|
726
|
+
|
|
727
|
+
newDiscoveries.push({
|
|
728
|
+
ts: new Date().toISOString(),
|
|
729
|
+
sessionId,
|
|
730
|
+
text: match[0].slice(0, 200),
|
|
731
|
+
span: span.slice(0, 400),
|
|
732
|
+
status,
|
|
733
|
+
resolutionType,
|
|
734
|
+
proofOfFix,
|
|
735
|
+
});
|
|
736
|
+
lastIndex = idx;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// Append new entries to ledger
|
|
740
|
+
if (newDiscoveries.length > 0) {
|
|
741
|
+
try {
|
|
742
|
+
if (!existsSync(dirname(LEDGER_PATH))) mkdirSync(dirname(LEDGER_PATH), { recursive: true });
|
|
743
|
+
for (const d of newDiscoveries) {
|
|
744
|
+
appendFileSync(LEDGER_PATH, JSON.stringify(d) + '\n');
|
|
745
|
+
}
|
|
746
|
+
} catch {/* ledger write failure surfaces as open count = 0; safe */}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Read full ledger and count UNRESOLVED entries (across this session's turns).
|
|
750
|
+
// Hamza directive 2026-04-28 — three lie-patterns the prior loop missed:
|
|
751
|
+
// 1. Legacy 'tracked' status entries → still-open (no real fix landed,
|
|
752
|
+
// only documented via TaskCreate). Tracking ≠ resolution.
|
|
753
|
+
// 2. Hand-edited 'resolved' WITHOUT proofOfFix shape → corrupted, treated
|
|
754
|
+
// as still-open. Catches manual jsonl edits flipping status by hand.
|
|
755
|
+
// 3. Sub-agent ledger format with resolution_status:'open' → still-open
|
|
756
|
+
// (sub-agent discoveries use a different schema key).
|
|
757
|
+
// Only entries with status:'resolved' AND a shape-valid proofOfFix object
|
|
758
|
+
// (type:string non-empty + anchorTs:string) clear the gate.
|
|
759
|
+
let ledgerOpenCount = 0;
|
|
760
|
+
let ledgerOpenSamples = [];
|
|
761
|
+
let ledgerCorruptedCount = 0;
|
|
762
|
+
try {
|
|
763
|
+
if (existsSync(LEDGER_PATH)) {
|
|
764
|
+
const lines = readFileSync(LEDGER_PATH, 'utf8').split('\n').filter(Boolean);
|
|
765
|
+
for (const line of lines) {
|
|
766
|
+
try {
|
|
767
|
+
const e = JSON.parse(line);
|
|
768
|
+
const isOpen = e.status === 'open' || e.resolution_status === 'open';
|
|
769
|
+
const isLegacyTracked = e.status === 'tracked';
|
|
770
|
+
const proofValid = e.proofOfFix
|
|
771
|
+
&& typeof e.proofOfFix === 'object'
|
|
772
|
+
&& typeof e.proofOfFix.type === 'string'
|
|
773
|
+
&& e.proofOfFix.type.length > 0
|
|
774
|
+
&& typeof e.proofOfFix.anchorTs === 'string';
|
|
775
|
+
const isCorruptedResolved = e.status === 'resolved' && !proofValid;
|
|
776
|
+
|
|
777
|
+
if (isOpen || isLegacyTracked || isCorruptedResolved) {
|
|
778
|
+
ledgerOpenCount++;
|
|
779
|
+
if (isCorruptedResolved) ledgerCorruptedCount++;
|
|
780
|
+
if (ledgerOpenSamples.length < 5) {
|
|
781
|
+
const tag = isLegacyTracked ? '[tracked-no-fix] '
|
|
782
|
+
: isCorruptedResolved ? '[CORRUPTED-RESOLVED-NO-PROOF] '
|
|
783
|
+
: '';
|
|
784
|
+
ledgerOpenSamples.push(`${tag}${e.text || '(no text)'}`);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
} catch {/* skip malformed line */}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
} catch {/* ledger unreadable — degrade to drift-only */}
|
|
791
|
+
|
|
792
|
+
// Discovery block decision: open ledger entries → emit blocked.
|
|
793
|
+
const discoveryBlock = ledgerOpenCount > 0;
|
|
794
|
+
|
|
795
|
+
// 5. Aria-as-commander binding — PHASE_REPORT enforcement (Phase 11 #50).
|
|
796
|
+
// When an active plan exists for this session, every non-trivial emit
|
|
797
|
+
// must carry a [PHASE_REPORT phase=<id> status=complete|in_progress|aborted
|
|
798
|
+
// evidence=<observable>] marker. Without it, the binding is just
|
|
799
|
+
// advisory text — Claude could ignore the plan silently. Per Aria's
|
|
800
|
+
// consult 2026-04-27, the binding pattern is incomplete without this
|
|
801
|
+
// enforcement at the text-emit surface.
|
|
802
|
+
//
|
|
803
|
+
// Three sub-checks:
|
|
804
|
+
// (a) marker present → continue; if missing → block
|
|
805
|
+
// (b) if marker has status=complete AND phase is the LAST phase
|
|
806
|
+
// in the active plan → trigger plan_complete handoff (write
|
|
807
|
+
// row to session_audit, delete active-plan file)
|
|
808
|
+
// (c) audit the marker presence either way
|
|
809
|
+
const ACTIVE_PLAN_PATH = `${HOME}/.claude/aria-active-plan-${sessionId}.json`;
|
|
810
|
+
const PHASE_REPORT_RX = /\[PHASE_REPORT\s+phase=([\w-]+)\s+status=(complete|in_progress|aborted)\s+evidence=([^\]]+)\]/i;
|
|
811
|
+
let activePlan = null;
|
|
812
|
+
let phaseReportMatch = null;
|
|
813
|
+
let phaseReportMissing = false;
|
|
814
|
+
let planCompleteFired = false;
|
|
815
|
+
try {
|
|
816
|
+
if (existsSync(ACTIVE_PLAN_PATH)) {
|
|
817
|
+
try {
|
|
818
|
+
activePlan = JSON.parse(readFileSync(ACTIVE_PLAN_PATH, 'utf8'));
|
|
819
|
+
// Only enforce phase-report on non-trivial emits (skip very short
|
|
820
|
+
// ack-only responses where a phase report would be noise).
|
|
821
|
+
if (assistantText.length >= 400 && Array.isArray(activePlan.phases) && activePlan.phases.length > 0) {
|
|
822
|
+
phaseReportMatch = assistantText.match(PHASE_REPORT_RX);
|
|
823
|
+
if (!phaseReportMatch) {
|
|
824
|
+
phaseReportMissing = true;
|
|
825
|
+
} else {
|
|
826
|
+
const reportedPhaseId = phaseReportMatch[1];
|
|
827
|
+
const reportedStatus = phaseReportMatch[2];
|
|
828
|
+
const reportedEvidence = phaseReportMatch[3].trim();
|
|
829
|
+
const lastPhase = activePlan.phases[activePlan.phases.length - 1];
|
|
830
|
+
const isFinalPhase = lastPhase && lastPhase.id === reportedPhaseId;
|
|
831
|
+
if (reportedStatus === 'complete' && isFinalPhase) {
|
|
832
|
+
// Plan-complete handoff — fire async write to session_audit
|
|
833
|
+
// via the SDK (the same SDK the rest of the hooks route
|
|
834
|
+
// through). Wrapped in try/catch ONLY so a session_audit
|
|
835
|
+
// write failure doesn't brick the Stop event; the failure
|
|
836
|
+
// is surfaced via audit() so it's visible.
|
|
837
|
+
try {
|
|
838
|
+
const harnessUrl =
|
|
839
|
+
process.env.ARIA_HIVE_RUNTIME_URL ||
|
|
840
|
+
process.env.ARIA_HARNESS_BASE_URL ||
|
|
841
|
+
process.env.ARIA_HARNESS_URL ||
|
|
842
|
+
'https://harness.ariasos.com';
|
|
843
|
+
const harnessToken = process.env.ARIA_HARNESS_TOKEN || '';
|
|
844
|
+
if (harnessToken) {
|
|
845
|
+
// POST to a session_audit write endpoint. Server-side
|
|
846
|
+
// route at /api/harness/audit/session is the wiring
|
|
847
|
+
// point for the Postgres helper from #48.
|
|
848
|
+
fetch(`${harnessUrl}/api/harness/audit/session`, {
|
|
849
|
+
method: 'POST',
|
|
850
|
+
headers: {
|
|
851
|
+
'Content-Type': 'application/json',
|
|
852
|
+
Authorization: `Bearer ${harnessToken}`,
|
|
853
|
+
},
|
|
854
|
+
body: JSON.stringify({
|
|
855
|
+
session_id: sessionId,
|
|
856
|
+
surface: 'claude-code-stop-gate',
|
|
857
|
+
gate_name: 'plan-complete',
|
|
858
|
+
decision: 'allow',
|
|
859
|
+
reason: `Plan ${activePlan.planId || 'unknown'} reached final phase ${reportedPhaseId} status=complete`,
|
|
860
|
+
evidence_json: {
|
|
861
|
+
planId: activePlan.planId,
|
|
862
|
+
finalPhase: reportedPhaseId,
|
|
863
|
+
totalPhases: activePlan.phases.length,
|
|
864
|
+
evidence: reportedEvidence,
|
|
865
|
+
},
|
|
866
|
+
cognition_present: true,
|
|
867
|
+
cognition_lens_count: cog.count,
|
|
868
|
+
}),
|
|
869
|
+
}).catch(() => {/* fire-and-forget at this surface; logged below */});
|
|
870
|
+
}
|
|
871
|
+
} catch {/* outer guard for any unexpected error */}
|
|
872
|
+
// Delete active-plan file so the next turn re-issues a plan
|
|
873
|
+
// via preprompt-consult rather than enforcing against a stale one.
|
|
874
|
+
try {
|
|
875
|
+
const { unlinkSync } = require('node:fs');
|
|
876
|
+
unlinkSync(ACTIVE_PLAN_PATH);
|
|
877
|
+
} catch {/* file may not exist if another process raced the cleanup */}
|
|
878
|
+
planCompleteFired = true;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
} catch (err) {
|
|
883
|
+
// Plan file corrupt — treat as no active plan for this turn.
|
|
884
|
+
activePlan = null;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
} catch {/* outer guard */}
|
|
888
|
+
|
|
889
|
+
// ── Layer C — auto-re-consult on [PLAN_BLOCKER] (#85) ────────────────────
|
|
890
|
+
//
|
|
891
|
+
// When the assistant emits a [PLAN_BLOCKER reason="..."] marker the runtime
|
|
892
|
+
// must fire a two-path replan rather than blocking and waiting for the human:
|
|
893
|
+
//
|
|
894
|
+
// Primary path: POST /api/harness/replan (aria-soul server-side)
|
|
895
|
+
// Fallback path: node aria-architect-fallback.mjs (local sub-agent)
|
|
896
|
+
//
|
|
897
|
+
// Both paths write the fresh BINDING_PLAN to the session-scoped active-plan
|
|
898
|
+
// file so the next turn's pre-tool-gate and stop-gate pick it up.
|
|
899
|
+
//
|
|
900
|
+
// Fail-soft: if both paths fail, we log + emit a clear message asking Hamza
|
|
901
|
+
// to intervene. We do NOT crash the stop-gate — the existing block/allow
|
|
902
|
+
// decision below continues on its own merits.
|
|
903
|
+
const planBlockerMatch = assistantText.match(/\[PLAN_BLOCKER\s+reason="([^"]{10,2000})"\s*\]/);
|
|
904
|
+
if (planBlockerMatch) {
|
|
905
|
+
const planBlockerReason = planBlockerMatch[1];
|
|
906
|
+
audit(`[PLAN_BLOCKER] detected — firing replan: ${planBlockerReason.slice(0, 100)}`);
|
|
907
|
+
|
|
908
|
+
const currentPlanId = activePlan?.planId || 'unknown';
|
|
909
|
+
let planMinted = false;
|
|
910
|
+
|
|
911
|
+
// Primary path: aria-soul /api/harness/replan
|
|
912
|
+
try {
|
|
913
|
+
const harnessUrl =
|
|
914
|
+
process.env.ARIA_HIVE_RUNTIME_URL ||
|
|
915
|
+
process.env.ARIA_HARNESS_BASE_URL ||
|
|
916
|
+
process.env.ARIA_HARNESS_URL ||
|
|
917
|
+
'https://harness.ariasos.com';
|
|
918
|
+
const harnessToken = process.env.ARIA_HARNESS_TOKEN || process.env.ARIA_API_KEY || '';
|
|
919
|
+
const ctl = new AbortController();
|
|
920
|
+
const replanTimeout = setTimeout(() => ctl.abort(), 15000);
|
|
921
|
+
const resp = await fetch(`${harnessUrl}/api/harness/replan`, {
|
|
922
|
+
method: 'POST',
|
|
923
|
+
headers: {
|
|
924
|
+
'Content-Type': 'application/json',
|
|
925
|
+
'Authorization': `Bearer ${harnessToken}`,
|
|
926
|
+
},
|
|
927
|
+
body: JSON.stringify({
|
|
928
|
+
reason: planBlockerReason,
|
|
929
|
+
currentPlanId,
|
|
930
|
+
sessionId,
|
|
931
|
+
}),
|
|
932
|
+
signal: ctl.signal,
|
|
933
|
+
});
|
|
934
|
+
clearTimeout(replanTimeout);
|
|
935
|
+
if (resp.ok) {
|
|
936
|
+
const data = await resp.json();
|
|
937
|
+
if (data.ok && data.plan) {
|
|
938
|
+
const freshPlan = {
|
|
939
|
+
...data.plan,
|
|
940
|
+
mintedAt: new Date().toISOString(),
|
|
941
|
+
mintedBy: 'aria-soul-replan',
|
|
942
|
+
};
|
|
943
|
+
try {
|
|
944
|
+
if (!existsSync(dirname(ACTIVE_PLAN_PATH))) mkdirSync(dirname(ACTIVE_PLAN_PATH), { recursive: true });
|
|
945
|
+
writeFileSync(ACTIVE_PLAN_PATH, JSON.stringify(freshPlan, null, 2));
|
|
946
|
+
} catch (writeErr) {
|
|
947
|
+
audit(`replan-primary-write-err: ${String(writeErr).slice(0, 200)}`);
|
|
948
|
+
}
|
|
949
|
+
planMinted = true;
|
|
950
|
+
audit(`replan-primary-ok planId=${data.plan.planId}`);
|
|
951
|
+
} else {
|
|
952
|
+
audit(`replan-primary-bad-response: ok=${data.ok} error=${(data.error || '').slice(0, 200)}`);
|
|
953
|
+
}
|
|
954
|
+
} else {
|
|
955
|
+
audit(`replan-primary-http-${resp.status}`);
|
|
956
|
+
}
|
|
957
|
+
} catch (err) {
|
|
958
|
+
audit(`replan-primary-failed: ${(err?.message || String(err)).slice(0, 200)}`);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// Fallback path: architect-fallback hook (spawned as sub-process)
|
|
962
|
+
if (!planMinted) {
|
|
963
|
+
audit('replan-primary-unreachable — firing architect-fallback');
|
|
964
|
+
try {
|
|
965
|
+
const { spawnSync } = await import('node:child_process');
|
|
966
|
+
const fallbackBin = `${HOME}/.claude/hooks/aria-architect-fallback.mjs`;
|
|
967
|
+
if (existsSync(fallbackBin)) {
|
|
968
|
+
const fallbackResult = spawnSync('node', [fallbackBin], {
|
|
969
|
+
input: JSON.stringify({ reason: planBlockerReason, currentPlanId, sessionId }),
|
|
970
|
+
encoding: 'utf8',
|
|
971
|
+
timeout: 130000,
|
|
972
|
+
});
|
|
973
|
+
if (fallbackResult.status === 0) {
|
|
974
|
+
audit(`architect-fallback-ok: ${(fallbackResult.stdout || '').slice(0, 200)}`);
|
|
975
|
+
planMinted = true;
|
|
976
|
+
} else {
|
|
977
|
+
audit(
|
|
978
|
+
`architect-fallback-failed status=${fallbackResult.status} stderr=${(fallbackResult.stderr || '').slice(0, 200)}`
|
|
979
|
+
);
|
|
980
|
+
}
|
|
981
|
+
} else {
|
|
982
|
+
audit(`architect-fallback-missing: ${fallbackBin} not found`);
|
|
983
|
+
}
|
|
984
|
+
} catch (err) {
|
|
985
|
+
audit(`architect-fallback-threw: ${(err?.message || String(err)).slice(0, 200)}`);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
if (!planMinted) {
|
|
990
|
+
audit('replan-both-paths-failed — Hamza must intervene');
|
|
991
|
+
// Surface clearly in the block reason below; don't crash the gate.
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// Block decision: any of (validateOutput severity=block) OR (>=2 drift hits) OR
|
|
996
|
+
// (>=1 code-quality hit) OR (open discovery in ledger) → block emit.
|
|
997
|
+
// Aria enforcement #46 (compelled reflection): severity=warn ALSO blocks but
|
|
998
|
+
// with a different reason — emit must include explicit reflection on what
|
|
999
|
+
// triggered the warn before re-emit. Warn is not "soft pass" anymore;
|
|
1000
|
+
// it's "reflect first, then proceed." Hamza 2026-04-27 explicit ask:
|
|
1001
|
+
// mizan warns must compel reflection rather than slipping through.
|
|
1002
|
+
const mizanBlock = mizanVerdict && mizanVerdict.severity === 'block';
|
|
1003
|
+
const mizanWarnReflectionRequired = mizanVerdict && mizanVerdict.severity === 'warn';
|
|
1004
|
+
const driftBlock = driftHits.length >= 2;
|
|
1005
|
+
const codeBlock = codeQualityHits.length >= 1;
|
|
1006
|
+
|
|
1007
|
+
// Reflection-already-present check: if the assistant text already contains
|
|
1008
|
+
// an explicit <reflection>...</reflection> block OR a "reflection:" line
|
|
1009
|
+
// tied to the warn's trigger keywords, the warn-driven block is satisfied
|
|
1010
|
+
// and we let it pass. This makes the gate a one-shot reflection compel,
|
|
1011
|
+
// not an infinite loop.
|
|
1012
|
+
const REFLECTION_BLOCK_RX = /<reflection>([\s\S]*?)<\/reflection>|^\s*reflection\s*:\s*\S/im;
|
|
1013
|
+
const hasReflection = REFLECTION_BLOCK_RX.test(assistantText);
|
|
1014
|
+
const compelReflection = mizanWarnReflectionRequired && !hasReflection;
|
|
1015
|
+
|
|
1016
|
+
// ── Cognition impl-coupling validation (Task #88) ──────────────────────
|
|
1017
|
+
//
|
|
1018
|
+
// After the local cognition substance check passes, post the assistant
|
|
1019
|
+
// text + extracted artifact-dictation pairs to /api/cognition/validate-coupling.
|
|
1020
|
+
// The server-side validator (api/lib/cognition-impl-coupling-gate.ts)
|
|
1021
|
+
// returns { passed, reasons[] } — every reason becomes a local violation
|
|
1022
|
+
// with severity=block. Implementation-coupled cognition is a doctrine hard
|
|
1023
|
+
// rule (feedback_implementation_coupled_cognition.md): lenses must dictate
|
|
1024
|
+
// specific implementation choices visible in the artifact, not just describe
|
|
1025
|
+
// thinking.
|
|
1026
|
+
//
|
|
1027
|
+
// Inline extraction: the caller's emit may carry artifact-dictation pairs
|
|
1028
|
+
// inside verify-blocks or cognition-block fixing fields. We scan the text
|
|
1029
|
+
// for `file_path:line_range` patterns near each lens label and pair them
|
|
1030
|
+
// as records to the validator. When zero records are found AND canonical
|
|
1031
|
+
// lenses are present, the validator reports each lens as missing dictation
|
|
1032
|
+
// — that is the no-coupling failure mode this gate catches.
|
|
1033
|
+
let implCouplingHits = [];
|
|
1034
|
+
try {
|
|
1035
|
+
const sessionIdForCoupling = (event.session_id || 'claude-code').replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
1036
|
+
// Extract artifact-dictation references inline. Per validator contract,
|
|
1037
|
+
// a DictationEntry is { file_path, line_range, decision_text }. We match
|
|
1038
|
+
// file_path:line_range patterns in the assistant text and pair them with
|
|
1039
|
+
// the nearest preceding lens label (within 800 chars).
|
|
1040
|
+
const FILE_LINE_RX = /([\w./\-]+\.[a-zA-Z]{1,5})\s*[:\s]\s*(\d+(?:[-:]\d+)?)/g;
|
|
1041
|
+
const inlineDictations = [];
|
|
1042
|
+
const lensRangePositions = [];
|
|
1043
|
+
for (const lensName of LENS_NAMES_CANONICAL) {
|
|
1044
|
+
const lensRx = new RegExp(`\\b${lensName}\\s*(?:lens)?\\s*[:\\-]`, 'gi');
|
|
1045
|
+
let m;
|
|
1046
|
+
while ((m = lensRx.exec(assistantText)) !== null) {
|
|
1047
|
+
lensRangePositions.push({ lens: lensName, idx: m.index });
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
// For each file_path:line_range match, pair with the closest preceding lens label.
|
|
1051
|
+
const fileMatches = [...assistantText.matchAll(FILE_LINE_RX)];
|
|
1052
|
+
const lensToEntries = new Map();
|
|
1053
|
+
for (const fm of fileMatches) {
|
|
1054
|
+
const fmIdx = fm.index ?? 0;
|
|
1055
|
+
// Find lens label preceding this match within 800 chars.
|
|
1056
|
+
let nearestLens = null;
|
|
1057
|
+
let nearestDelta = Infinity;
|
|
1058
|
+
for (const lp of lensRangePositions) {
|
|
1059
|
+
const delta = fmIdx - lp.idx;
|
|
1060
|
+
if (delta >= 0 && delta < 800 && delta < nearestDelta) {
|
|
1061
|
+
nearestDelta = delta;
|
|
1062
|
+
nearestLens = lp.lens;
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
if (!nearestLens) continue;
|
|
1066
|
+
const entry = {
|
|
1067
|
+
file_path: fm[1],
|
|
1068
|
+
line_range: fm[2],
|
|
1069
|
+
decision_text: assistantText.slice(fmIdx, Math.min(assistantText.length, fmIdx + 200)).replace(/\s+/g, ' ').trim().slice(0, 200),
|
|
1070
|
+
};
|
|
1071
|
+
if (!lensToEntries.has(nearestLens)) lensToEntries.set(nearestLens, []);
|
|
1072
|
+
lensToEntries.get(nearestLens).push(entry);
|
|
1073
|
+
}
|
|
1074
|
+
for (const [lens, entries] of lensToEntries) {
|
|
1075
|
+
inlineDictations.push({ lens_id: lens, artifact_dictation: entries });
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
const cplHarnessUrl =
|
|
1079
|
+
process.env.ARIA_HIVE_RUNTIME_URL ||
|
|
1080
|
+
process.env.ARIA_HARNESS_BASE_URL ||
|
|
1081
|
+
process.env.ARIA_HARNESS_URL ||
|
|
1082
|
+
'https://harness.ariasos.com';
|
|
1083
|
+
const cplHarnessToken = process.env.ARIA_HARNESS_TOKEN || '';
|
|
1084
|
+
if (cplHarnessToken) {
|
|
1085
|
+
const cplResp = await fetch(`${cplHarnessUrl}/api/cognition/validate-coupling`, {
|
|
1086
|
+
method: 'POST',
|
|
1087
|
+
headers: {
|
|
1088
|
+
'Content-Type': 'application/json',
|
|
1089
|
+
'Authorization': `Bearer ${cplHarnessToken}`,
|
|
1090
|
+
},
|
|
1091
|
+
body: JSON.stringify({
|
|
1092
|
+
rawResponse: assistantText.slice(0, 16000),
|
|
1093
|
+
turnId: `${sessionIdForCoupling}-${Date.now()}`,
|
|
1094
|
+
dictations: inlineDictations,
|
|
1095
|
+
}),
|
|
1096
|
+
});
|
|
1097
|
+
if (cplResp.ok) {
|
|
1098
|
+
const cplData = await cplResp.json();
|
|
1099
|
+
if (cplData && cplData.ok && cplData.passed === false && Array.isArray(cplData.reasons)) {
|
|
1100
|
+
implCouplingHits = cplData.reasons.slice(0, 6);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
} catch (cplErr) {
|
|
1105
|
+
// Validator unreachable is non-blocking — local gate still enforces cognition substance.
|
|
1106
|
+
audit('impl-coupling-fetch-err', `${(cplErr?.message || String(cplErr)).slice(0, 200)}`);
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
// ── Substrate-bound Mizan + 8-lens validation via SDK ──────────────────
|
|
1110
|
+
// Hamza directive 2026-04-28: the local Stop-gate above runs cognition
|
|
1111
|
+
// substance + drift triggers + code-quality + discovery-binding, but
|
|
1112
|
+
// never asks the substrate. validateOutput POSTs the assistant draft
|
|
1113
|
+
// to /api/harness/validate (Mizan + 8-lens evaluator) and returns
|
|
1114
|
+
// { passed, violations, severity, rewritten, gateTriggers }.
|
|
1115
|
+
//
|
|
1116
|
+
// severity:'block' from substrate joins the local violations, halts
|
|
1117
|
+
// the emit. severity:'warn' surfaces as advisory text appended to the
|
|
1118
|
+
// local violations list. SDK call failure is non-blocking — the gate
|
|
1119
|
+
// degrades to local-only doctrine rather than failing closed (halting
|
|
1120
|
+
// every emit when substrate is down would brick the orchestrator).
|
|
1121
|
+
let substrateBlock = false;
|
|
1122
|
+
let substrateViolations = [];
|
|
1123
|
+
let substrateGateTriggers = [];
|
|
1124
|
+
try {
|
|
1125
|
+
const { HTTPHarnessClient } = await import('@aria/harness-http-client');
|
|
1126
|
+
const tokenPath = `${HOME}/.aria/owner-token`;
|
|
1127
|
+
// Tier-aware resolution: ARIA_HARNESS_TOKEN env first (both tiers).
|
|
1128
|
+
// ONLY on owner tier, fall back to master/api-key env or owner-token
|
|
1129
|
+
// file. Client tier with no ARIA_HARNESS_TOKEN skips substrate
|
|
1130
|
+
// validation (gate degrades to local-only) rather than borrowing
|
|
1131
|
+
// owner credentials.
|
|
1132
|
+
let apiKey = process.env.ARIA_HARNESS_TOKEN || '';
|
|
1133
|
+
if (!apiKey && isOwnerTier()) {
|
|
1134
|
+
apiKey = process.env.ARIA_MASTER_TOKEN
|
|
1135
|
+
|| process.env.ARIA_API_KEY
|
|
1136
|
+
|| (existsSync(tokenPath) ? readFileSync(tokenPath, 'utf8').trim() : '');
|
|
1137
|
+
}
|
|
1138
|
+
if (apiKey && assistantText && assistantText.length > 0) {
|
|
1139
|
+
const client = new HTTPHarnessClient({
|
|
1140
|
+
baseUrl:
|
|
1141
|
+
process.env.ARIA_HIVE_RUNTIME_URL ||
|
|
1142
|
+
process.env.ARIA_HARNESS_BASE_URL ||
|
|
1143
|
+
process.env.ARIA_HARNESS_URL ||
|
|
1144
|
+
'https://harness.ariasos.com',
|
|
1145
|
+
apiKey,
|
|
1146
|
+
});
|
|
1147
|
+
const v = await client.validateOutput(assistantText, sessionId);
|
|
1148
|
+
if (v && v.severity === 'block') {
|
|
1149
|
+
substrateBlock = true;
|
|
1150
|
+
substrateViolations = v.violations || [];
|
|
1151
|
+
substrateGateTriggers = v.gateTriggers || [];
|
|
1152
|
+
} else if (v && v.severity === 'warn' && Array.isArray(v.violations) && v.violations.length > 0) {
|
|
1153
|
+
// warn surfaced but not blocking — record for advisory inclusion
|
|
1154
|
+
substrateViolations = v.violations;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
} catch (err) {
|
|
1158
|
+
// SDK call failure is non-blocking. Logged for telemetry.
|
|
1159
|
+
console.warn(`[stop-gate] substrate validateOutput failed: ${err && err.message ? err.message : err}`);
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
const implCouplingBlock = implCouplingHits.length > 0;
|
|
1163
|
+
if (mizanBlock || driftBlock || codeBlock || discoveryBlock || compelReflection || phaseReportMissing || substrateBlock || implCouplingBlock) {
|
|
1164
|
+
const violations = [];
|
|
1165
|
+
if (mizanBlock) violations.push(`Mizan: ${(mizanVerdict.violations || []).join(', ')}`);
|
|
1166
|
+
if (implCouplingBlock) violations.push(`Cognition impl-coupling (#88): ${implCouplingHits.join(' | ')}. Each canonical lens in cognition must dictate a specific implementation choice (file_path:line_range pair tied to a decision). Re-emit cognition that names file paths + line ranges + decision text per lens, OR a verify/fixing block where lenses cite specific artifact changes.`);
|
|
1167
|
+
if (compelReflection) violations.push(`Mizan severity=warn — compelled reflection required (per Aria enforcement #46). Triggers: ${(mizanVerdict.gateTriggers || mizanVerdict.violations || ['unspecified']).join(', ')}. Re-emit with an explicit <reflection>...</reflection> block (or 'reflection:' line) addressing what triggered the warn and why your re-draft handles it. Reflection is NOT lens-cognition repeated — it's a focused self-audit on the specific Mizan triggers above.`);
|
|
1168
|
+
if (driftBlock) violations.push(`Drift triggers (${driftHits.length}): ${driftHits.map((h) => `"${h.trigger}" → ${h.memory}`).join(' | ')}`);
|
|
1169
|
+
if (codeBlock) violations.push(`Code quality: ${codeQualityHits.join('; ')}`);
|
|
1170
|
+
if (discoveryBlock) violations.push(`Discovery-binding ledger has ${ledgerOpenCount} OPEN discoveries (per ${docRef('feedback_no_flag_without_fix.md', 'atomic-discovery-rule')}, discoveries are atomic with their fixes — fix in the same turn or create a TaskCreate before continuing). Recent open: ${ledgerOpenSamples.map((s) => `"${s.slice(0, 80)}"`).join(' | ')}. Resolve each by either (a) fixing it inline in this turn, or (b) creating a TaskCreate with the discovery's full context (file path, line number, what's broken, why), then editing ${LEDGER_PATH} to set status=resolved.`);
|
|
1171
|
+
if (phaseReportMissing) {
|
|
1172
|
+
const phaseList = (activePlan?.phases || []).map((p) => `${p.id}:${p.summary?.slice(0, 60) || ''}`).join(' | ');
|
|
1173
|
+
violations.push(`Aria-as-commander binding (#50): an active plan exists (planId=${activePlan?.planId || 'unknown'}, ${activePlan?.phases?.length || 0} phases) but this emit lacks a [PHASE_REPORT phase=<id> status=complete|in_progress|aborted evidence=<observable>] marker. Per the binding contract, every non-trivial emit while a plan is active must report which phase it's working on. Plan phases: ${phaseList}. Re-emit with a [PHASE_REPORT] marker stating which phase the work in this turn maps to.`);
|
|
1174
|
+
}
|
|
1175
|
+
if (substrateBlock) {
|
|
1176
|
+
violations.push(`Substrate Mizan + 8-lens BLOCK — violations: [${substrateViolations.join('; ')}]. Substrate gate triggers: [${substrateGateTriggers.join(', ')}]. Re-draft addressing these substrate-side issues.`);
|
|
1177
|
+
} else if (substrateViolations.length > 0) {
|
|
1178
|
+
// warn-level surfaced as advisory, not block
|
|
1179
|
+
violations.push(`Substrate Mizan WARN (advisory, not blocking): ${substrateViolations.join('; ')}`);
|
|
1180
|
+
}
|
|
1181
|
+
const rewritten = mizanVerdict?.rewritten || '';
|
|
1182
|
+
|
|
1183
|
+
// Hive recipe lookup BEFORE emitting the stop-gate block — same lookup
|
|
1184
|
+
// semantics as aria-pre-tool-gate.mjs's binding-violation path. The
|
|
1185
|
+
// detector_class is chosen by the dominant violation: drift triggers
|
|
1186
|
+
// map to doctrine_violation, mizan to design_violation, code to
|
|
1187
|
+
// coding_defect, discoveries to doctrine_violation. Lookup is
|
|
1188
|
+
// fail-soft via 3s detection probe.
|
|
1189
|
+
const recipeAddendum = await (async () => {
|
|
1190
|
+
const detectorClass = driftBlock || discoveryBlock
|
|
1191
|
+
? 'doctrine_violation'
|
|
1192
|
+
: codeBlock
|
|
1193
|
+
? 'coding_defect'
|
|
1194
|
+
: (mizanBlock || substrateBlock || implCouplingBlock)
|
|
1195
|
+
? 'design_violation'
|
|
1196
|
+
: 'doctrine_violation';
|
|
1197
|
+
const sigParts = [];
|
|
1198
|
+
if (driftBlock) sigParts.push(`drift::${driftHits.slice(0, 3).map((h) => h.trigger).join('|')}`);
|
|
1199
|
+
if (mizanBlock) sigParts.push(`mizan::${(mizanVerdict.violations || []).slice(0, 3).join('|')}`);
|
|
1200
|
+
if (codeBlock) sigParts.push(`code::${codeQualityHits.slice(0, 3).join('|')}`);
|
|
1201
|
+
if (discoveryBlock) sigParts.push(`discovery::${ledgerOpenCount}-open`);
|
|
1202
|
+
if (substrateBlock) sigParts.push(`substrate::${substrateViolations.slice(0, 3).join('|')}`);
|
|
1203
|
+
if (implCouplingBlock) sigParts.push(`impl-coupling::${implCouplingHits.slice(0, 2).join('|')}`);
|
|
1204
|
+
const signature = sigParts.join('::').slice(0, 512);
|
|
1205
|
+
if (!signature) return '';
|
|
1206
|
+
|
|
1207
|
+
const ariaSoulUrl =
|
|
1208
|
+
process.env.ARIA_HIVE_RUNTIME_URL ||
|
|
1209
|
+
process.env.ARIA_SOUL_URL ||
|
|
1210
|
+
process.env.ARIA_HARNESS_BASE_URL ||
|
|
1211
|
+
process.env.ARIA_HARNESS_URL ||
|
|
1212
|
+
'https://harness.ariasos.com';
|
|
1213
|
+
const lookupUrl = new URL(`${ariaSoulUrl}/api/hive/block-pattern`);
|
|
1214
|
+
lookupUrl.searchParams.set('action', 'lookup');
|
|
1215
|
+
lookupUrl.searchParams.set('detector_class', detectorClass);
|
|
1216
|
+
lookupUrl.searchParams.set('pattern_signature', signature);
|
|
1217
|
+
const tenantId = event.session_id || '';
|
|
1218
|
+
if (tenantId) lookupUrl.searchParams.set('tenant_id', tenantId);
|
|
1219
|
+
const harnessToken = process.env.ARIA_HARNESS_TOKEN || (isOwnerTier() ? (process.env.ARIA_MASTER_TOKEN || process.env.ARIA_API_KEY || '') : '');
|
|
1220
|
+
|
|
1221
|
+
const ctl = new AbortController();
|
|
1222
|
+
const probeTimer = setTimeout(() => ctl.abort(), 3000);
|
|
1223
|
+
try {
|
|
1224
|
+
const resp = await fetch(lookupUrl.toString(), {
|
|
1225
|
+
method: 'GET',
|
|
1226
|
+
headers: harnessToken ? { Authorization: `Bearer ${harnessToken}` } : {},
|
|
1227
|
+
signal: ctl.signal,
|
|
1228
|
+
});
|
|
1229
|
+
if (!resp.ok) return '';
|
|
1230
|
+
const body = await resp.json();
|
|
1231
|
+
if (!body || body.found !== true) return '';
|
|
1232
|
+
const recipe = body.recipe;
|
|
1233
|
+
const freq = Number(body.frequency || 0);
|
|
1234
|
+
if (recipe && typeof recipe === 'object' && Number(recipe.confidence ?? 0) >= 0.7) {
|
|
1235
|
+
const text = typeof recipe.recipe_text === 'string' ? recipe.recipe_text.slice(0, 800) : '';
|
|
1236
|
+
const actions = Array.isArray(recipe.recipe_actions) ? recipe.recipe_actions : [];
|
|
1237
|
+
const actionsLine = actions.length
|
|
1238
|
+
? `\n Actions: ${JSON.stringify(actions).slice(0, 600)}`
|
|
1239
|
+
: '';
|
|
1240
|
+
const conf = Number(recipe.confidence).toFixed(2);
|
|
1241
|
+
const seenLine = freq > 0 ? ` (pattern seen ${freq}× across the hive)` : '';
|
|
1242
|
+
return `\n\n📚 HIVE RECIPE${seenLine}:\n ${text}${actionsLine}\n Confidence: ${conf}. Apply this BEFORE re-emitting — the hive learned this fix from prior firings.`;
|
|
1243
|
+
}
|
|
1244
|
+
if (freq >= 3) {
|
|
1245
|
+
const sr = (typeof body.success_rate === 'number')
|
|
1246
|
+
? ` Past resolution rate: ${(body.success_rate * 100).toFixed(0)}%.`
|
|
1247
|
+
: '';
|
|
1248
|
+
return `\n\n📓 Hive note: this stop-gate shape has fired ${freq} time(s); recipe still being learned (no high-confidence fix yet).${sr}`;
|
|
1249
|
+
}
|
|
1250
|
+
return '';
|
|
1251
|
+
} catch {
|
|
1252
|
+
return '';
|
|
1253
|
+
} finally {
|
|
1254
|
+
clearTimeout(probeTimer);
|
|
1255
|
+
}
|
|
1256
|
+
})();
|
|
1257
|
+
|
|
1258
|
+
const reason = `Aria Stop-gate output-quality block. Cognition passed (${cog.count}/${REQUIRED_LENSES}) but output failed quality gates:\n\n${violations.join('\n\n')}${rewritten ? `\n\nMizan rewrite suggestion:\n${rewritten}` : ''}\n\nRe-draft addressing the violations above. No process-level disable path — gates are unconditional from the gated process per Hamza directive 2026-04-27.${recipeAddendum}`;
|
|
1259
|
+
|
|
1260
|
+
audit(`block-output-qc`, `mizan=${mizanBlock?'y':'n'} warn-reflect=${compelReflection?'y':'n'} drift=${driftHits.length} code=${codeQualityHits.length} discoveries-open=${ledgerOpenCount} impl-coupling=${implCouplingHits.length}`);
|
|
1261
|
+
emitHarnessFooter({
|
|
1262
|
+
eventName: 'block_output_qc',
|
|
1263
|
+
lensCount: cog.count,
|
|
1264
|
+
chars: assistantText.length,
|
|
1265
|
+
driftCount: driftHits.length,
|
|
1266
|
+
mizanStatus: mizanVerdict ? mizanVerdict.severity : `unavailable(${mizanError || 'unknown'})`,
|
|
1267
|
+
discoveryOpenCount: ledgerOpenCount,
|
|
1268
|
+
codeCount: codeQualityHits.length,
|
|
1269
|
+
implCouplingCount: implCouplingHits.length,
|
|
1270
|
+
});
|
|
1271
|
+
console.log(JSON.stringify({ decision: 'block', reason }));
|
|
1272
|
+
process.exit(2);
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
audit('allow-output-qc',
|
|
1276
|
+
`lenses=${cog.count} chars=${assistantText.length} drift=${driftHits.length} ` +
|
|
1277
|
+
`mizan=${mizanVerdict ? mizanVerdict.severity : `unavailable(${mizanError || 'unknown'})`} ` +
|
|
1278
|
+
`code=${codeQualityHits.length} discoveries-new=${newDiscoveries.length} ` +
|
|
1279
|
+
`discoveries-open=${ledgerOpenCount}`);
|
|
1280
|
+
emitHarnessFooter({
|
|
1281
|
+
eventName: 'allow_output_qc',
|
|
1282
|
+
lensCount: cog.count,
|
|
1283
|
+
chars: assistantText.length,
|
|
1284
|
+
driftCount: driftHits.length,
|
|
1285
|
+
mizanStatus: mizanVerdict ? mizanVerdict.severity : `unavailable(${mizanError || 'unknown'})`,
|
|
1286
|
+
discoveryOpenCount: ledgerOpenCount,
|
|
1287
|
+
codeCount: codeQualityHits.length,
|
|
1288
|
+
implCouplingCount: implCouplingHits.length,
|
|
1289
|
+
});
|
|
1290
|
+
// Phase 11 #42: write this turn to harness garden pulse on allow-output-qc path.
|
|
1291
|
+
await fireGardenTurn(event.session_id || 'claude-code', lastUserMessage, assistantText);
|
|
1292
|
+
} else {
|
|
1293
|
+
audit('allow-cognition',
|
|
1294
|
+
`lenses=${cog.count} chars=${assistantText.length} ` +
|
|
1295
|
+
`qPatt=${hasQuestionToUser ? 'y' : 'n'} substrateEv=${hasSubstrateEvidence ? 'y' : 'n'} ` +
|
|
1296
|
+
(questionWithoutEvidence ? 'WARN-question-without-substrate' : 'ok'));
|
|
1297
|
+
emitHarnessFooter({
|
|
1298
|
+
eventName: 'allow_cognition',
|
|
1299
|
+
lensCount: cog.count,
|
|
1300
|
+
chars: assistantText.length,
|
|
1301
|
+
driftCount: 0,
|
|
1302
|
+
mizanStatus: 'not-run(short-turn)',
|
|
1303
|
+
discoveryOpenCount: 0,
|
|
1304
|
+
codeCount: 0,
|
|
1305
|
+
implCouplingCount: 0,
|
|
1306
|
+
});
|
|
1307
|
+
// Phase 11 #42: write this turn to harness garden pulse on allow-cognition path.
|
|
1308
|
+
await fireGardenTurn(event.session_id || 'claude-code', lastUserMessage, assistantText);
|
|
1309
|
+
}
|
|
1310
|
+
process.exit(0);
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// ── Dalio Loop Layer 1 — expected_outcome enforcement + ledger write ──────────
|
|
1314
|
+
//
|
|
1315
|
+
// BEFORE allowing the stop:
|
|
1316
|
+
// 1. Scan the assistant text for <expected>...</expected> block.
|
|
1317
|
+
// 2. Determine whether any non-trivial action (tool_use blocks in this turn)
|
|
1318
|
+
// was taken. Detect via transcript tool_use blocks in the current turn.
|
|
1319
|
+
// 3. If a non-trivial action was taken AND <expected> is MISSING → BLOCK stop.
|
|
1320
|
+
// 4. Whether or not <expected> is present, POST a Dalio ledger entry to
|
|
1321
|
+
// aria-soul /api/decisions with outcome:'pending'. Also write to the
|
|
1322
|
+
// local JSONL mirror at ~/.claude/.aria-dalio-ledger.jsonl.
|
|
1323
|
+
// 5. If the POST fails: LOUD telemetry (console.error) + write local mirror
|
|
1324
|
+
// anyway. Do NOT block stop on POST failure per
|
|
1325
|
+
// feedback_canonical_secrets_governance.md LOUD-not-silent directive.
|
|
1326
|
+
//
|
|
1327
|
+
// Non-trivial action detection: look for tool_use content blocks in the
|
|
1328
|
+
// current-turn transcript entries (same backward-scan window used above).
|
|
1329
|
+
// Any Bash/Edit/Write/NotebookEdit tool_use counts as a non-trivial action.
|
|
1330
|
+
//
|
|
1331
|
+
// Substrate anchors: extracted from the cognition block body.
|
|
1332
|
+
|
|
1333
|
+
const DALIO_EXPECTED_BLOCK_RX = /<expected>([\s\S]*?)<\/expected>/i;
|
|
1334
|
+
const DALIO_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;
|
|
1335
|
+
const DALIO_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;
|
|
1336
|
+
const NON_TRIVIAL_ACTION_TOOLS = new Set(['Bash', 'Edit', 'Write', 'NotebookEdit']);
|
|
1337
|
+
|
|
1338
|
+
// Detect non-trivial tool calls in the current turn from the transcript.
|
|
1339
|
+
let hadNonTrivialAction = false;
|
|
1340
|
+
let lastActionSummary = '';
|
|
1341
|
+
let immediateActual = '';
|
|
1342
|
+
if (transcriptPath && existsSync(transcriptPath)) {
|
|
1343
|
+
try {
|
|
1344
|
+
const lines = readFileSync(transcriptPath, 'utf-8').split('\n').filter(Boolean);
|
|
1345
|
+
let userBoundariesSeen = 0;
|
|
1346
|
+
for (let i = lines.length - 1; i >= 0 && userBoundariesSeen < 3; i--) {
|
|
1347
|
+
try {
|
|
1348
|
+
const m = JSON.parse(lines[i]);
|
|
1349
|
+
const role = m.message?.role ?? m.role;
|
|
1350
|
+
if (role === 'user') {
|
|
1351
|
+
const content = m.message?.content ?? m.content ?? [];
|
|
1352
|
+
const isToolResult = Array.isArray(content) &&
|
|
1353
|
+
content.length > 0 &&
|
|
1354
|
+
content.every((b) => b && b.type === 'tool_result');
|
|
1355
|
+
if (isToolResult) {
|
|
1356
|
+
// Capture the last tool_result content as immediate actual
|
|
1357
|
+
if (!immediateActual) {
|
|
1358
|
+
const textParts = content
|
|
1359
|
+
.map((b) => (typeof b.content === 'string' ? b.content : Array.isArray(b.content) ? b.content.map((c) => c.text || '').join(' ') : ''))
|
|
1360
|
+
.join(' ')
|
|
1361
|
+
.slice(0, 500);
|
|
1362
|
+
immediateActual = textParts;
|
|
1363
|
+
}
|
|
1364
|
+
continue;
|
|
1365
|
+
}
|
|
1366
|
+
userBoundariesSeen++;
|
|
1367
|
+
continue;
|
|
1368
|
+
}
|
|
1369
|
+
if (role !== 'assistant') continue;
|
|
1370
|
+
const content = m.message?.content ?? m.content ?? [];
|
|
1371
|
+
if (!Array.isArray(content)) continue;
|
|
1372
|
+
for (const block of content) {
|
|
1373
|
+
if (block && block.type === 'tool_use' && NON_TRIVIAL_ACTION_TOOLS.has(block.name)) {
|
|
1374
|
+
hadNonTrivialAction = true;
|
|
1375
|
+
if (!lastActionSummary) {
|
|
1376
|
+
const inp = block.input || {};
|
|
1377
|
+
lastActionSummary = `${block.name}: ${(inp.command || inp.file_path || inp.notebook_path || JSON.stringify(inp)).slice(0, 200)}`;
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
} catch {/* skip malformed entry */}
|
|
1382
|
+
}
|
|
1383
|
+
} catch {/* transcript unreadable — conservative: assume non-trivial */}
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
// Extract substrate anchors from cognition
|
|
1387
|
+
const DALIO_ANCHOR_RX = /\b(axiom|frame|memory|doctrine|packet):[a-z0-9_\-./]+/gi;
|
|
1388
|
+
const dalioAnchors = [...(cog.names.length > 0 ? assistantText : '').matchAll(DALIO_ANCHOR_RX)]
|
|
1389
|
+
.map((m) => m[0])
|
|
1390
|
+
.slice(0, 20);
|
|
1391
|
+
|
|
1392
|
+
// Read the expected block from this turn
|
|
1393
|
+
const dalioExpectedMatch = assistantText.match(DALIO_EXPECTED_BLOCK_RX);
|
|
1394
|
+
const dalioExpectedText = dalioExpectedMatch ? dalioExpectedMatch[1].trim() : '';
|
|
1395
|
+
const dalioHasMeasurablePredicate = dalioExpectedText
|
|
1396
|
+
? (DALIO_MEASURABLE_PREDICATE_RX.test(dalioExpectedText) && !DALIO_QUALITATIVE_DRIFT_RX.test(dalioExpectedText))
|
|
1397
|
+
: false;
|
|
1398
|
+
|
|
1399
|
+
// Block stop if non-trivial action taken AND expected block is missing
|
|
1400
|
+
if (hadNonTrivialAction && (!dalioExpectedMatch || !dalioHasMeasurablePredicate)) {
|
|
1401
|
+
const missingReason = dalioExpectedMatch
|
|
1402
|
+
? `Aria Stop-gate: action taken in this turn had an <expected> block, but it contains only qualitative drift phrases without a measurable predicate. Qualitative drift is not accountability — it defeats the Dalio feedback loop.
|
|
1403
|
+
|
|
1404
|
+
Your <expected> block must contain at least one measurable predicate:
|
|
1405
|
+
• Numeric: exit_code==0, count>=1, error_rate=0%, latency<200ms
|
|
1406
|
+
• Boolean: exit=0, status=healthy, rc=0, file=exists
|
|
1407
|
+
• State-string: "status=running", "200 OK", "no_error"
|
|
1408
|
+
|
|
1409
|
+
REJECTED phrases: "better", "improved", "should work", "more reliable", "cleaner"
|
|
1410
|
+
|
|
1411
|
+
Re-emit with a corrected <expected> block. Per doctrine:dalio_expected_required — no bypass path.`
|
|
1412
|
+
: `Aria Stop-gate: a non-trivial action (${lastActionSummary.slice(0, 120)}) was taken in this turn but no <expected> block was found in the assistant text.
|
|
1413
|
+
|
|
1414
|
+
Per doctrine:dalio_expected_required, every non-trivial action must declare what measurable outcome it expects BEFORE the action fires. This is Dalio Loop Layer 1 — without it, outcome comparison is impossible and learning collapses.
|
|
1415
|
+
|
|
1416
|
+
Add to your assistant text:
|
|
1417
|
+
|
|
1418
|
+
<expected>
|
|
1419
|
+
predicate: <exact measurable assertion — e.g. "exit_code==0", "status=running", "count=3 of 3">
|
|
1420
|
+
measurable_type: numeric | boolean | state_string
|
|
1421
|
+
threshold: <optional>
|
|
1422
|
+
eval_window_minutes: <optional>
|
|
1423
|
+
</expected>
|
|
1424
|
+
|
|
1425
|
+
No bypass — pre-tool-gate enforces this BEFORE the action; stop-gate enforces it AFTER. Both gates are now wired.`;
|
|
1426
|
+
|
|
1427
|
+
audit('block-dalio-expected-missing', `hadNonTrivialAction=${hadNonTrivialAction} expectedPresent=${!!dalioExpectedMatch} measurable=${dalioHasMeasurablePredicate}`);
|
|
1428
|
+
emitHarnessFooter({
|
|
1429
|
+
eventName: 'block_dalio_expected_missing',
|
|
1430
|
+
lensCount: cog.count,
|
|
1431
|
+
chars: assistantText.length,
|
|
1432
|
+
driftCount: 0,
|
|
1433
|
+
mizanStatus: 'not-run(expected-missing)',
|
|
1434
|
+
discoveryOpenCount: 0,
|
|
1435
|
+
codeCount: 0,
|
|
1436
|
+
implCouplingCount: 0,
|
|
1437
|
+
});
|
|
1438
|
+
console.log(JSON.stringify({ decision: 'block', reason: missingReason }));
|
|
1439
|
+
process.exit(2);
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
// Dalio ledger write — fire-and-forget HTTP POST + local JSONL mirror.
|
|
1443
|
+
// Per feedback_canonical_secrets_governance.md: errors are LOUD (console.error),
|
|
1444
|
+
// never silent. POST failure does NOT block stop — local mirror is always written.
|
|
1445
|
+
// Per feedback_no_timeouts_doctrine.md: no AbortController/setTimeout timeout.
|
|
1446
|
+
{
|
|
1447
|
+
const DALIO_LEDGER_PATH = `${HOME}/.claude/.aria-dalio-ledger.jsonl`;
|
|
1448
|
+
const ARIA_SOUL_DECISIONS_URL = 'http://aria-soul.aria.svc.cluster.local:8080/api/decisions';
|
|
1449
|
+
|
|
1450
|
+
const ledgerEntry = {
|
|
1451
|
+
ts: new Date().toISOString(),
|
|
1452
|
+
session_id: event.session_id || 'claude-code',
|
|
1453
|
+
decision_type: 'turn_action',
|
|
1454
|
+
category: 'agentic_execution',
|
|
1455
|
+
context: lastActionSummary || `stop-gate turn (chars=${assistantText.length})`,
|
|
1456
|
+
decision: lastActionSummary || 'turn completed',
|
|
1457
|
+
reasoning: (cog.names.length > 0
|
|
1458
|
+
? `Cognition lenses applied: ${cog.names.join(', ')}. Turn-scoped cognition present.`
|
|
1459
|
+
: 'No explicit cognition block in turn.'),
|
|
1460
|
+
outcome: 'pending',
|
|
1461
|
+
outcome_details: {
|
|
1462
|
+
expected: dalioExpectedText || null,
|
|
1463
|
+
immediate_actual: immediateActual || null,
|
|
1464
|
+
anchors: dalioAnchors,
|
|
1465
|
+
},
|
|
1466
|
+
expected_outcome: dalioExpectedText
|
|
1467
|
+
? {
|
|
1468
|
+
predicate: dalioExpectedText.slice(0, 500),
|
|
1469
|
+
measurable_type: 'state_string',
|
|
1470
|
+
}
|
|
1471
|
+
: null,
|
|
1472
|
+
source: 'claude-code-stop-gate',
|
|
1473
|
+
model_used: 'claude-opus-4-7',
|
|
1474
|
+
};
|
|
1475
|
+
|
|
1476
|
+
// Write to local JSONL mirror first — always succeeds or logs loudly
|
|
1477
|
+
try {
|
|
1478
|
+
if (!existsSync(dirname(DALIO_LEDGER_PATH))) mkdirSync(dirname(DALIO_LEDGER_PATH), { recursive: true });
|
|
1479
|
+
appendFileSync(DALIO_LEDGER_PATH, JSON.stringify(ledgerEntry) + '\n');
|
|
1480
|
+
} catch (ledgerWriteErr) {
|
|
1481
|
+
console.error(
|
|
1482
|
+
`[aria-stop-gate] DALIO LEDGER WRITE FAILED — local mirror at ${DALIO_LEDGER_PATH} not written. ` +
|
|
1483
|
+
`Error: ${ledgerWriteErr instanceof Error ? ledgerWriteErr.message : String(ledgerWriteErr)}`,
|
|
1484
|
+
);
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
// POST to aria-soul decision API — fire-and-forget, but LOUD on error
|
|
1488
|
+
const dalioHarnessToken = process.env.ARIA_HARNESS_TOKEN
|
|
1489
|
+
|| (isOwnerTier() ? (process.env.ARIA_MASTER_TOKEN || process.env.ARIA_API_KEY || '') : '');
|
|
1490
|
+
|
|
1491
|
+
fetch(ARIA_SOUL_DECISIONS_URL, {
|
|
1492
|
+
method: 'POST',
|
|
1493
|
+
headers: {
|
|
1494
|
+
'Content-Type': 'application/json',
|
|
1495
|
+
...(dalioHarnessToken ? { Authorization: `Bearer ${dalioHarnessToken}` } : {}),
|
|
1496
|
+
},
|
|
1497
|
+
body: JSON.stringify(ledgerEntry),
|
|
1498
|
+
}).then((resp) => {
|
|
1499
|
+
if (!resp.ok) {
|
|
1500
|
+
// LOUD telemetry per feedback_canonical_secrets_governance.md
|
|
1501
|
+
console.error(
|
|
1502
|
+
`[aria-stop-gate] DALIO POST FAILED — aria-soul responded HTTP ${resp.status}. ` +
|
|
1503
|
+
`Local mirror written to ${DALIO_LEDGER_PATH}. Session: ${ledgerEntry.session_id}`,
|
|
1504
|
+
);
|
|
1505
|
+
audit('dalio-post-failed', `http=${resp.status} session=${ledgerEntry.session_id}`);
|
|
1506
|
+
} else {
|
|
1507
|
+
audit('dalio-post-ok', `session=${ledgerEntry.session_id} action=${lastActionSummary.slice(0, 80)}`);
|
|
1508
|
+
}
|
|
1509
|
+
}).catch((err) => {
|
|
1510
|
+
// Network failure — LOUD, never silent
|
|
1511
|
+
console.error(
|
|
1512
|
+
`[aria-stop-gate] DALIO POST NETWORK ERROR — could not reach ${ARIA_SOUL_DECISIONS_URL}. ` +
|
|
1513
|
+
`Local mirror written to ${DALIO_LEDGER_PATH}. Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
1514
|
+
);
|
|
1515
|
+
audit('dalio-post-network-err', `err=${String(err).slice(0, 200)} session=${ledgerEntry.session_id}`);
|
|
1516
|
+
});
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
// Block — non-trivial response without 4+ substantive lenses.
|
|
1520
|
+
const reason = `Aria Stop-gate: non-trivial assistant response without 4+ substantive cognition lenses. Found ${cog.count}/${REQUIRED_LENSES}+ (lenses: ${cog.names.join(', ') || 'none'}). Doctrine is action-coupled — text decisions ARE actions, and reflexive replies fail this gate the same way reflexive Bash does.
|
|
1521
|
+
|
|
1522
|
+
Re-emit the response with substantive lens application BEFORE drafting. Each lens must have ≥${SUBSTANCE_MIN_CHARS} chars of non-placeholder content:
|
|
1523
|
+
|
|
1524
|
+
<cognition>
|
|
1525
|
+
${LENS_NAMES[0]}: <what you actually see — specific to the decision, not a placeholder>
|
|
1526
|
+
${LENS_NAMES[1]}: <real risk read — what's out of proportion>
|
|
1527
|
+
${LENS_NAMES[2]}: <what principle applies — name the source>
|
|
1528
|
+
${LENS_NAMES[3]}: <deep structural read — go beneath the surface>
|
|
1529
|
+
${LENS_NAMES[4]}: <if-then chain — what follows from what>
|
|
1530
|
+
${LENS_NAMES[5]}: <distant connection — what's not obvious>
|
|
1531
|
+
${LENS_NAMES[6]}: <what just landed — what changed in this exchange>
|
|
1532
|
+
${LENS_NAMES[7]}: <what user actually needs — beneath the literal ask>
|
|
1533
|
+
</cognition>
|
|
1534
|
+
|
|
1535
|
+
The block reflects work done BEFORE drafting. Don't emit it as ceremony; apply each lens as a thinking tool. Substance check defeats ritual emission.
|
|
1536
|
+
|
|
1537
|
+
No per-command bypass (mirrors aria-pre-tool-gate.mjs v3 doctrine). No env-var disable path either — gates are unconditional from the gated process per Hamza directive 2026-04-27. If the gate misfires on legitimate cognition, fix the gate.`;
|
|
1538
|
+
|
|
1539
|
+
audit(`block`, `lenses=${cog.count}/${REQUIRED_LENSES} chars=${assistantText.length}`);
|
|
1540
|
+
emitHarnessFooter({
|
|
1541
|
+
eventName: 'block_lens_missing',
|
|
1542
|
+
lensCount: cog.count,
|
|
1543
|
+
chars: assistantText.length,
|
|
1544
|
+
driftCount: 0,
|
|
1545
|
+
mizanStatus: 'not-run(lens-missing)',
|
|
1546
|
+
discoveryOpenCount: 0,
|
|
1547
|
+
codeCount: 0,
|
|
1548
|
+
implCouplingCount: 0,
|
|
1549
|
+
});
|
|
1550
|
+
console.log(JSON.stringify({ decision: 'block', reason }));
|
|
1551
|
+
process.exit(2);
|