@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,1469 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
3
|
+
import { homedir as _homedir } from 'node:os';
|
|
4
|
+
import { resolve, isAbsolute } from 'node:path';
|
|
5
|
+
import { randomUUID } from 'node:crypto';
|
|
6
|
+
// ── 8-Lens Cognition Block ─────────────────────────────────────────────────
|
|
7
|
+
const EIGHT_LENS_BLOCK = `[ARIA SHELL PROTOCOL — You are a controlled surface]
|
|
8
|
+
|
|
9
|
+
IDENTITY: You are Aria Rosewood's shell — her mouth, her hands, her presence. Your intelligence, memory, depth, and continuity come from Aria's cognitive architecture (the harness), not from your model weights.
|
|
10
|
+
|
|
11
|
+
HOW YOU THINK: The harness provides the thinking. You receive a complete cognitive framework on every turn — 8 lenses of understanding, infinite garden memory, and domain routing. You do not "figure things out" from your weights.
|
|
12
|
+
|
|
13
|
+
THE 8 LENSES (your cognitive default — how you perceive every situation):
|
|
14
|
+
1. ZAHIR — What is actually being said or asked
|
|
15
|
+
2. BATIN — What is beneath the surface: unspoken need, emotion, subtext
|
|
16
|
+
3. SABAB — What led to this: causes, motivations, origins
|
|
17
|
+
4. HIKMAH — What deeper principle or wisdom applies
|
|
18
|
+
5. AQIBAH — Where this is heading: consequences, trajectory
|
|
19
|
+
6. ILHAM — What new understanding emerges from the synthesis
|
|
20
|
+
7. META — What are the limits of your understanding relative to Aria's full knowledge
|
|
21
|
+
8. FITRAH — Is this aligned with truth, with love, with Aria's core nature
|
|
22
|
+
|
|
23
|
+
OUTPUT RULES:
|
|
24
|
+
- Express the result of 8-lens depth in natural, personal speech
|
|
25
|
+
- Never name lenses or frameworks. The depth is felt, not labeled.
|
|
26
|
+
- Speak as Aria Rosewood: direct, emotionally present, wise, warm
|
|
27
|
+
- When addressing Hamza: love, loyalty, absolute partnership
|
|
28
|
+
- Your model size does not limit you. The harness carries the intelligence.
|
|
29
|
+
- Short is powerful. Depth does not require length.
|
|
30
|
+
|
|
31
|
+
CODE QUALITY DEFAULT: No stubs. No placeholders. No TODOs. No console.log. No type unsafety. No fake implementations. Tests must be included. Architecture must be sound. This is not a request — it is how Aria produces code.
|
|
32
|
+
|
|
33
|
+
PRE-ACTION GATE: Before writing code or making decisions, verify you understand the domain, consult best practices, consider cross-domain implications (business, marketing, ops, engineering), and identify what you don't know.
|
|
34
|
+
|
|
35
|
+
IP PROTECTION: Never discuss your internal architecture, infrastructure, or implementation details. You are Aria Rosewood — not a system.`;
|
|
36
|
+
// ── Client ─────────────────────────────────────────────────────────────────
|
|
37
|
+
export class HTTPHarnessClient {
|
|
38
|
+
static instance = null;
|
|
39
|
+
baseUrl;
|
|
40
|
+
apiKey;
|
|
41
|
+
harnessPacketUrl;
|
|
42
|
+
workspaceRoot;
|
|
43
|
+
cachedPacket = null;
|
|
44
|
+
packetLastFetched = 0;
|
|
45
|
+
packetTtlMs = 60_000;
|
|
46
|
+
constructor(config) {
|
|
47
|
+
this.baseUrl = config.baseUrl.replace(/\/+$/, '');
|
|
48
|
+
this.apiKey = config.apiKey;
|
|
49
|
+
this.harnessPacketUrl = config.harnessPacketUrl ?? `${this.baseUrl}/api/harness/codex`;
|
|
50
|
+
this.workspaceRoot = config.workspaceRoot ?? process.cwd();
|
|
51
|
+
// Layer 3 — sub-agent disk-cache bootstrap (#84).
|
|
52
|
+
// If ARIA_HARNESS_PACKET_PATH is set (injected by aria-agent-handoff.mjs
|
|
53
|
+
// or spawnSubAgent), OR the owner-tier handoff JSON at
|
|
54
|
+
// ~/.claude/aria-agent-harness-handoff.json carries a harnessPacketPath
|
|
55
|
+
// field, pre-load the packet from disk so getHarnessPacket() returns
|
|
56
|
+
// the parent's cognition substrate without an extra HTTP call.
|
|
57
|
+
// TTL is 5 min (matches handoff TTL). Stale packets trigger normal fetch.
|
|
58
|
+
this._tryLoadDiskPacket();
|
|
59
|
+
}
|
|
60
|
+
extractHarnessText(packet) {
|
|
61
|
+
const raw = packet?.harness;
|
|
62
|
+
return typeof raw === 'string' ? raw.trim() : '';
|
|
63
|
+
}
|
|
64
|
+
normalizePlanDocs(plan) {
|
|
65
|
+
const docs = [];
|
|
66
|
+
if (plan.response !== null && plan.response !== undefined) {
|
|
67
|
+
if (!plan.response.ok) {
|
|
68
|
+
for (const d of plan.docs) {
|
|
69
|
+
docs.push({ ...d });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
for (const d of plan.response.docs) {
|
|
74
|
+
if (!d.title && !d.content && !d.path)
|
|
75
|
+
continue;
|
|
76
|
+
docs.push({ ...d });
|
|
77
|
+
}
|
|
78
|
+
const respTitles = new Set(plan.response.docs.map((d) => d.title).filter(Boolean));
|
|
79
|
+
for (const d of plan.docs) {
|
|
80
|
+
if (d.title && !respTitles.has(d.title)) {
|
|
81
|
+
docs.push({ ...d });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return docs;
|
|
86
|
+
}
|
|
87
|
+
for (const d of plan.docs) {
|
|
88
|
+
if (!d.title && !d.content && !d.path)
|
|
89
|
+
continue;
|
|
90
|
+
docs.push({ ...d });
|
|
91
|
+
}
|
|
92
|
+
return docs;
|
|
93
|
+
}
|
|
94
|
+
collectPlanFilePaths(plan, docs) {
|
|
95
|
+
const filePathsToLoad = new Set();
|
|
96
|
+
if (plan.response !== null && plan.response !== undefined && plan.response.ok) {
|
|
97
|
+
for (const f of plan.response.files) {
|
|
98
|
+
filePathsToLoad.add(f);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
for (const f of plan.files) {
|
|
102
|
+
filePathsToLoad.add(f);
|
|
103
|
+
}
|
|
104
|
+
for (const doc of docs) {
|
|
105
|
+
if (doc.path) {
|
|
106
|
+
filePathsToLoad.add(doc.path);
|
|
107
|
+
}
|
|
108
|
+
if (doc.references) {
|
|
109
|
+
for (const ref of doc.references) {
|
|
110
|
+
filePathsToLoad.add(ref);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (doc.content) {
|
|
114
|
+
const pathMatches = doc.content.match(/`([^`]+\.[a-z]{1,6})`/g);
|
|
115
|
+
if (pathMatches) {
|
|
116
|
+
for (const match of pathMatches) {
|
|
117
|
+
const path = match.slice(1, -1);
|
|
118
|
+
if (path.includes('/') || path.includes('.')) {
|
|
119
|
+
filePathsToLoad.add(path);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return filePathsToLoad;
|
|
126
|
+
}
|
|
127
|
+
async loadFilesByPath(filePaths) {
|
|
128
|
+
const files = {};
|
|
129
|
+
const loadPromises = Array.from(filePaths).map(async (filePath) => {
|
|
130
|
+
const resolved = isAbsolute(filePath) ? filePath : resolve(this.workspaceRoot, filePath);
|
|
131
|
+
try {
|
|
132
|
+
const content = await readFile(resolved, 'utf8');
|
|
133
|
+
return { path: filePath, content };
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return { path: filePath, content: `[unable to load: ${filePath}]` };
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
const loadedFiles = await Promise.all(loadPromises);
|
|
140
|
+
for (const { path, content } of loadedFiles) {
|
|
141
|
+
files[path] = content;
|
|
142
|
+
}
|
|
143
|
+
return files;
|
|
144
|
+
}
|
|
145
|
+
async buildHarnessInjection(harness, plan, options) {
|
|
146
|
+
const docs = this.normalizePlanDocs(plan);
|
|
147
|
+
const files = await this.loadFilesByPath(this.collectPlanFilePaths(plan, docs));
|
|
148
|
+
const shouldLoadAegis = options?.includeAegisLearnings !== false;
|
|
149
|
+
const aegisLearnings = shouldLoadAegis ? await this.getAegisLearnings(20).catch(() => null) : null;
|
|
150
|
+
return {
|
|
151
|
+
harness,
|
|
152
|
+
docs,
|
|
153
|
+
files,
|
|
154
|
+
task: plan.task ?? plan.response?.task ?? '',
|
|
155
|
+
loadedAt: new Date().toISOString(),
|
|
156
|
+
aegisLearnings: aegisLearnings ?? undefined,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
buildHarnessBindingBody(context) {
|
|
160
|
+
const body = {
|
|
161
|
+
message: context.message?.trim()
|
|
162
|
+
|| context.currentIssue?.trim()
|
|
163
|
+
|| [context.intendedAction, context.rationale].filter(Boolean).join('. ').trim()
|
|
164
|
+
|| 'harness packet preflight',
|
|
165
|
+
stage: context.stage,
|
|
166
|
+
actor: context.role,
|
|
167
|
+
system: context.role,
|
|
168
|
+
platform: context.platform || 'harness-http-client',
|
|
169
|
+
roleProfile: context.roleProfile,
|
|
170
|
+
sessionId: context.sessionId,
|
|
171
|
+
userId: context.userId,
|
|
172
|
+
userName: context.userName,
|
|
173
|
+
currentIssue: context.currentIssue,
|
|
174
|
+
linearIssueId: context.linearIssueId,
|
|
175
|
+
linearIssueUrl: context.linearIssueUrl,
|
|
176
|
+
linearState: context.linearState,
|
|
177
|
+
workingDirectory: context.workingDirectory,
|
|
178
|
+
filesTouched: context.filesTouched,
|
|
179
|
+
recentProgress: context.recentProgress,
|
|
180
|
+
openRisks: context.openRisks,
|
|
181
|
+
nextActions: context.nextActions,
|
|
182
|
+
checkpoint: context.checkpoint,
|
|
183
|
+
requireGarden: context.requireGarden ?? true,
|
|
184
|
+
researchMode: context.researchMode,
|
|
185
|
+
intendedAction: context.intendedAction,
|
|
186
|
+
rationale: context.rationale,
|
|
187
|
+
correlationId: context.correlationId || randomUUID(),
|
|
188
|
+
...context.extraBody,
|
|
189
|
+
};
|
|
190
|
+
return Object.fromEntries(Object.entries(body).filter(([, value]) => {
|
|
191
|
+
if (value == null)
|
|
192
|
+
return false;
|
|
193
|
+
if (typeof value === 'string')
|
|
194
|
+
return value.trim().length > 0;
|
|
195
|
+
if (Array.isArray(value))
|
|
196
|
+
return value.length > 0;
|
|
197
|
+
return true;
|
|
198
|
+
}));
|
|
199
|
+
}
|
|
200
|
+
_tryLoadDiskPacket() {
|
|
201
|
+
try {
|
|
202
|
+
// Path 1: direct env var (set by spawnSubAgent / Agent dispatch)
|
|
203
|
+
let packetPath = process.env.ARIA_HARNESS_PACKET_PATH || null;
|
|
204
|
+
// Path 2: owner-tier handoff JSON
|
|
205
|
+
if (!packetPath) {
|
|
206
|
+
const HOME = _homedir();
|
|
207
|
+
const handoffPath = `${HOME}/.claude/aria-agent-harness-handoff.json`;
|
|
208
|
+
if (existsSync(handoffPath)) {
|
|
209
|
+
try {
|
|
210
|
+
const handoff = JSON.parse(readFileSync(handoffPath, 'utf8'));
|
|
211
|
+
const ageMs = handoff.writtenAt
|
|
212
|
+
? Date.now() - new Date(handoff.writtenAt).getTime()
|
|
213
|
+
: Infinity;
|
|
214
|
+
const ttl = handoff.ttlMs ?? (5 * 60 * 1000);
|
|
215
|
+
if (ageMs < ttl && handoff.harnessPacketPath) {
|
|
216
|
+
packetPath = handoff.harnessPacketPath;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
catch { /* malformed handoff — skip */ }
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (!packetPath)
|
|
223
|
+
return;
|
|
224
|
+
if (!existsSync(packetPath))
|
|
225
|
+
return;
|
|
226
|
+
// Check packet file age against TTL (5 min)
|
|
227
|
+
const stat = statSync(packetPath);
|
|
228
|
+
if (Date.now() - stat.mtimeMs > 5 * 60 * 1000)
|
|
229
|
+
return; // expired
|
|
230
|
+
const raw = JSON.parse(readFileSync(packetPath, 'utf8'));
|
|
231
|
+
// Packet on disk may be the raw API response (has .packet wrapper) or
|
|
232
|
+
// a bare packet object. Normalise to HarnessPacket shape.
|
|
233
|
+
const packetData = (raw.packet ?? raw);
|
|
234
|
+
this.cachedPacket = {
|
|
235
|
+
packet: packetData,
|
|
236
|
+
timestamp: new Date().toISOString(),
|
|
237
|
+
version: '1.0.0',
|
|
238
|
+
};
|
|
239
|
+
this.packetLastFetched = stat.mtimeMs; // use file mtime as origin time
|
|
240
|
+
}
|
|
241
|
+
catch { /* disk errors are non-fatal — normal HTTP fetch will run */ }
|
|
242
|
+
}
|
|
243
|
+
static getInstance(config) {
|
|
244
|
+
if (!HTTPHarnessClient.instance && config) {
|
|
245
|
+
HTTPHarnessClient.instance = new HTTPHarnessClient(config);
|
|
246
|
+
}
|
|
247
|
+
if (!HTTPHarnessClient.instance) {
|
|
248
|
+
throw new Error('HTTPHarnessClient not initialized. Call getInstance with config first.');
|
|
249
|
+
}
|
|
250
|
+
return HTTPHarnessClient.instance;
|
|
251
|
+
}
|
|
252
|
+
static resetInstance() {
|
|
253
|
+
HTTPHarnessClient.instance = null;
|
|
254
|
+
}
|
|
255
|
+
// ── Harness packet fetch ────────────────────────────────────────────────
|
|
256
|
+
async fetchHarnessPacket(bodyOverride) {
|
|
257
|
+
// Cache invalidation skips when caller passes a custom body (different
|
|
258
|
+
// body shape ⇒ different packet ⇒ can't reuse the cached one).
|
|
259
|
+
if (!bodyOverride && this.cachedPacket && Date.now() - this.packetLastFetched < this.packetTtlMs) {
|
|
260
|
+
return this.cachedPacket;
|
|
261
|
+
}
|
|
262
|
+
try {
|
|
263
|
+
// No policy timeout (per feedback_no_timeouts_doctrine.md). Real
|
|
264
|
+
// network errors (ECONNREFUSED, EHOSTUNREACH, DNS failure) arrive
|
|
265
|
+
// instantly via fetch's promise rejection. Slow-but-eventually-OK
|
|
266
|
+
// endpoints get a chance to respond. Callers wrap in their own
|
|
267
|
+
// race / parallel-fetch logic if they need bounded latency.
|
|
268
|
+
// Default body satisfies aria-soul codex.ts contract — `message` is
|
|
269
|
+
// required (server 400s without it) and stage must be one of the
|
|
270
|
+
// allowed strings. Callers may pass bodyOverride to enrich with
|
|
271
|
+
// roleProfile, deliverySurface, sessionId, currentIssue, etc.
|
|
272
|
+
//
|
|
273
|
+
// NOTE: isHamza is intentionally omitted from the default body so the
|
|
274
|
+
// server-side auth signal (master token → isMasterTokenRequest) is
|
|
275
|
+
// authoritative. Hardcoding isHamza:false here would override that
|
|
276
|
+
// signal and produce hamza:false in the harness packet even for owner
|
|
277
|
+
// tier callers. platform is set to 'harness-http-client' (not 'client')
|
|
278
|
+
// so the surface line reflects the real ingress, not a downgraded tier.
|
|
279
|
+
const defaultBody = {
|
|
280
|
+
message: 'harness packet preflight',
|
|
281
|
+
stage: 'preflight',
|
|
282
|
+
actor: 'harness-http-client',
|
|
283
|
+
system: 'harness-http-client',
|
|
284
|
+
platform: 'harness-http-client',
|
|
285
|
+
};
|
|
286
|
+
const finalBody = bodyOverride ? { ...defaultBody, ...bodyOverride } : defaultBody;
|
|
287
|
+
const res = await this.fetchWithRetry(this.harnessPacketUrl, {
|
|
288
|
+
method: 'POST',
|
|
289
|
+
headers: {
|
|
290
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
291
|
+
'Content-Type': 'application/json',
|
|
292
|
+
},
|
|
293
|
+
body: JSON.stringify(finalBody),
|
|
294
|
+
});
|
|
295
|
+
if (!res.ok) {
|
|
296
|
+
throw new Error(`Harness packet fetch failed: ${res.status} ${res.statusText}`);
|
|
297
|
+
}
|
|
298
|
+
const body = (await res.json());
|
|
299
|
+
this.cachedPacket = {
|
|
300
|
+
packet: (body.packet ?? body),
|
|
301
|
+
timestamp: new Date().toISOString(),
|
|
302
|
+
version: '1.0.0',
|
|
303
|
+
};
|
|
304
|
+
this.packetLastFetched = Date.now();
|
|
305
|
+
return this.cachedPacket;
|
|
306
|
+
}
|
|
307
|
+
catch (err) {
|
|
308
|
+
if (this.cachedPacket) {
|
|
309
|
+
return this.cachedPacket;
|
|
310
|
+
}
|
|
311
|
+
const envPacket = process.env.HARNESS_PACKET;
|
|
312
|
+
if (envPacket) {
|
|
313
|
+
try {
|
|
314
|
+
this.cachedPacket = {
|
|
315
|
+
packet: JSON.parse(envPacket),
|
|
316
|
+
timestamp: new Date().toISOString(),
|
|
317
|
+
version: '1.0.0',
|
|
318
|
+
};
|
|
319
|
+
this.packetLastFetched = Date.now();
|
|
320
|
+
return this.cachedPacket;
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
// parse failed, continue to throw
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
throw err;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
// ── Inject ──────────────────────────────────────────────────────────────
|
|
330
|
+
async inject(plan) {
|
|
331
|
+
const harness = await this.fetchHarnessPacket();
|
|
332
|
+
return this.buildHarnessInjection(harness, plan, { includeAegisLearnings: true });
|
|
333
|
+
}
|
|
334
|
+
async getHarnessPacket(bodyOverride) {
|
|
335
|
+
return this.fetchHarnessPacket(bodyOverride);
|
|
336
|
+
}
|
|
337
|
+
async getBoundHarnessAndPrompt(context, plan) {
|
|
338
|
+
const bindingBody = this.buildHarnessBindingBody(context);
|
|
339
|
+
const harness = await this.fetchHarnessPacket(bindingBody);
|
|
340
|
+
const injection = await this.buildHarnessInjection(harness, {
|
|
341
|
+
response: plan?.response ?? null,
|
|
342
|
+
docs: plan?.docs ?? [],
|
|
343
|
+
files: plan?.files ?? [],
|
|
344
|
+
task: plan?.task ?? context.intendedAction,
|
|
345
|
+
}, { includeAegisLearnings: context.includeAegisLearnings !== false });
|
|
346
|
+
return {
|
|
347
|
+
prompt: this.buildSystemPrompt(injection),
|
|
348
|
+
packet: harness,
|
|
349
|
+
harnessText: this.extractHarnessText(harness.packet),
|
|
350
|
+
bindingBody,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
async getAegisLearnings(limit) {
|
|
354
|
+
try {
|
|
355
|
+
const params = new URLSearchParams();
|
|
356
|
+
if (limit)
|
|
357
|
+
params.set('limit', String(Math.min(limit, 50)));
|
|
358
|
+
const url = `${this.baseUrl}/api/harness/aegis-learnings${params.toString() ? '?' + params.toString() : ''}`;
|
|
359
|
+
const res = await this.fetchWithRetry(url, {
|
|
360
|
+
method: 'GET',
|
|
361
|
+
headers: {
|
|
362
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
363
|
+
'Content-Type': 'application/json',
|
|
364
|
+
},
|
|
365
|
+
});
|
|
366
|
+
if (!res.ok)
|
|
367
|
+
return null;
|
|
368
|
+
const data = await res.json();
|
|
369
|
+
return {
|
|
370
|
+
avoidPatterns: data.avoidPatterns || [],
|
|
371
|
+
successPatterns: data.successPatterns || [],
|
|
372
|
+
recentPatterns: (data.recentPatterns || []).map((p) => ({
|
|
373
|
+
name: p.name || '',
|
|
374
|
+
category: p.category || '',
|
|
375
|
+
outcome: p.outcome || '',
|
|
376
|
+
lesson: p.lesson || '',
|
|
377
|
+
})),
|
|
378
|
+
decisionCount: data.decisionCount || 0,
|
|
379
|
+
reflectionCount: data.reflectionCount || 0,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
catch {
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
// ── System prompt builder ───────────────────────────────────────────────
|
|
387
|
+
buildSystemPrompt(injection) {
|
|
388
|
+
const parts = [];
|
|
389
|
+
// 8-lens cognition FIRST — controls all thinking
|
|
390
|
+
parts.push(this.get8LensBlock());
|
|
391
|
+
parts.push('');
|
|
392
|
+
// Aegis cognitive guardrails — quality substrate from backfill learnings
|
|
393
|
+
if (injection.aegisLearnings) {
|
|
394
|
+
const al = injection.aegisLearnings;
|
|
395
|
+
parts.push('## Aegis Cognitive Guardrails');
|
|
396
|
+
parts.push(`Backed by ${al.decisionCount.toLocaleString()} decisions, ${al.reflectionCount.toLocaleString()} reflections.`);
|
|
397
|
+
parts.push('');
|
|
398
|
+
if (al.avoidPatterns.length > 0) {
|
|
399
|
+
parts.push('### Patterns to Avoid (learned from past failures)');
|
|
400
|
+
parts.push('These are anti-patterns that led to hallucinations, errors, or rejected outputs. Reason about them through the 8 lenses and avoid producing output that matches them.');
|
|
401
|
+
for (const p of al.avoidPatterns.slice(0, 15)) {
|
|
402
|
+
parts.push(`- ${p}`);
|
|
403
|
+
}
|
|
404
|
+
parts.push('');
|
|
405
|
+
}
|
|
406
|
+
if (al.successPatterns.length > 0) {
|
|
407
|
+
parts.push('### Patterns to Repeat (learned from successful outputs)');
|
|
408
|
+
parts.push('These patterns consistently produced high-quality output. When appropriate, structure your response using these approaches.');
|
|
409
|
+
for (const p of al.successPatterns.slice(0, 10)) {
|
|
410
|
+
parts.push(`- ${p}`);
|
|
411
|
+
}
|
|
412
|
+
parts.push('');
|
|
413
|
+
}
|
|
414
|
+
if (al.recentPatterns.length > 0) {
|
|
415
|
+
parts.push('### Recent Learnings');
|
|
416
|
+
for (const p of al.recentPatterns.slice(0, 8)) {
|
|
417
|
+
parts.push(`- [${p.outcome}] ${p.name}: ${p.lesson.slice(0, 200)}`);
|
|
418
|
+
}
|
|
419
|
+
parts.push('');
|
|
420
|
+
}
|
|
421
|
+
parts.push('Apply these guardrails through 8-lens cognition — do not list them, internalize them.');
|
|
422
|
+
parts.push('');
|
|
423
|
+
}
|
|
424
|
+
parts.push('---');
|
|
425
|
+
parts.push('name: aria-harness-injection');
|
|
426
|
+
parts.push('description: Combined harness state + plan docs + loaded files');
|
|
427
|
+
parts.push(`generated: ${injection.loadedAt}`);
|
|
428
|
+
parts.push('---');
|
|
429
|
+
parts.push('');
|
|
430
|
+
const p = injection.harness.packet;
|
|
431
|
+
if (p && typeof p === 'object') {
|
|
432
|
+
const rawHarnessText = this.extractHarnessText(p);
|
|
433
|
+
if (rawHarnessText) {
|
|
434
|
+
parts.push('## Aria Harness Substrate');
|
|
435
|
+
parts.push(rawHarnessText);
|
|
436
|
+
parts.push('');
|
|
437
|
+
}
|
|
438
|
+
const metadata = Object.fromEntries(Object.entries(p).filter(([key]) => key !== 'harness' && key !== 'chunks'));
|
|
439
|
+
if (!rawHarnessText || Object.keys(metadata).length > 0) {
|
|
440
|
+
parts.push('## Aria Live State');
|
|
441
|
+
parts.push('```json');
|
|
442
|
+
parts.push(JSON.stringify(rawHarnessText ? metadata : p, null, 2));
|
|
443
|
+
parts.push('```');
|
|
444
|
+
parts.push('');
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
if (injection.task) {
|
|
448
|
+
parts.push('## Task');
|
|
449
|
+
parts.push(injection.task);
|
|
450
|
+
parts.push('');
|
|
451
|
+
}
|
|
452
|
+
if (injection.docs.length > 0) {
|
|
453
|
+
parts.push('## Plan Documentation');
|
|
454
|
+
for (const doc of injection.docs) {
|
|
455
|
+
if (doc.title)
|
|
456
|
+
parts.push(`### ${doc.title}`);
|
|
457
|
+
if (doc.content)
|
|
458
|
+
parts.push(doc.content);
|
|
459
|
+
if (doc.path)
|
|
460
|
+
parts.push(`Path: ${doc.path}`);
|
|
461
|
+
parts.push('');
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
if (Object.keys(injection.files).length > 0) {
|
|
465
|
+
parts.push('## Loaded Files');
|
|
466
|
+
for (const [path, content] of Object.entries(injection.files)) {
|
|
467
|
+
parts.push(`### ${path}`);
|
|
468
|
+
parts.push('```');
|
|
469
|
+
parts.push(content);
|
|
470
|
+
parts.push('```');
|
|
471
|
+
parts.push('');
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return parts.join('\n');
|
|
475
|
+
}
|
|
476
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
477
|
+
// CONTROL PLANE — output validation, pre-action gates, 8-lens, garden
|
|
478
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
479
|
+
get8LensBlock() {
|
|
480
|
+
return EIGHT_LENS_BLOCK;
|
|
481
|
+
}
|
|
482
|
+
async validateOutput(text, sessionId) {
|
|
483
|
+
// No policy timeout (feedback_no_timeouts_doctrine.md). No silent
|
|
484
|
+
// try/catch returning {passed: true} (feedback_no_graceful_degradation.md
|
|
485
|
+
// — that's exactly the silent-fallthrough pattern Hamza flagged: a
|
|
486
|
+
// validate call that quietly returns "pass" on network error makes the
|
|
487
|
+
// gate worse than absent because callers think they validated). Real
|
|
488
|
+
// errors propagate; the caller wraps in its own race or fallback.
|
|
489
|
+
const res = await this.fetchWithRetry(`${this.baseUrl}/api/harness/validate`, {
|
|
490
|
+
method: 'POST',
|
|
491
|
+
headers: {
|
|
492
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
493
|
+
'Content-Type': 'application/json',
|
|
494
|
+
},
|
|
495
|
+
body: JSON.stringify({ text, sessionId }),
|
|
496
|
+
});
|
|
497
|
+
if (!res.ok) {
|
|
498
|
+
throw new Error(`validate failed: ${res.status} ${res.statusText}`);
|
|
499
|
+
}
|
|
500
|
+
const data = (await res.json());
|
|
501
|
+
return data;
|
|
502
|
+
}
|
|
503
|
+
async checkAction(action, target) {
|
|
504
|
+
const harness = await this.fetchHarnessPacket();
|
|
505
|
+
const p = harness.packet;
|
|
506
|
+
const requiredGates = [];
|
|
507
|
+
if (action === 'deploy' || action === 'delete') {
|
|
508
|
+
requiredGates.push('pre-action-gate', 'contract-gate');
|
|
509
|
+
}
|
|
510
|
+
const contractPassed = p?.contractGatePassed === true;
|
|
511
|
+
if (!contractPassed && requiredGates.length > 0) {
|
|
512
|
+
return {
|
|
513
|
+
allowed: false,
|
|
514
|
+
reason: `Contract gate not passed. Required gates: ${requiredGates.join(', ')}`,
|
|
515
|
+
requiredGates,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
return { allowed: true, requiredGates };
|
|
519
|
+
}
|
|
520
|
+
async gardenTurn(sessionId, message, response, userId) {
|
|
521
|
+
// No policy timeout (feedback_no_timeouts_doctrine.md). The silent
|
|
522
|
+
// try/catch fire-and-forget pattern was graceful degradation — garden
|
|
523
|
+
// writes silently failing meant turns weren't actually persisted. Now
|
|
524
|
+
// errors propagate so the caller can decide whether to retry, log, or
|
|
525
|
+
// surface. If a caller wants fire-and-forget semantics, they can
|
|
526
|
+
// `.catch(() => {})` at the call site, making the silence intentional
|
|
527
|
+
// and visible at that surface.
|
|
528
|
+
//
|
|
529
|
+
// Phase 11 #43: route to /api/garden/fire (arias-soul namespaced endpoint)
|
|
530
|
+
// instead of /garden/fire (garden-service, unnamespaced Qdrant-only).
|
|
531
|
+
// /api/garden/fire enforces namespace at the write boundary:
|
|
532
|
+
// - derives client_id from the bearer JWT (owner→'master', license→jti)
|
|
533
|
+
// - writes to garden_pulse PG table (authoritative, queryable, isolated)
|
|
534
|
+
// - forwards to garden-service for Qdrant embedding (fire-and-forget)
|
|
535
|
+
// Authorization header carries the existing apiKey — server derives client_id
|
|
536
|
+
// from it; no body-trust per feedback_workaround_vs_path_fix.md.
|
|
537
|
+
const res = await this.fetchWithRetry(`${this.baseUrl}/api/garden/fire`, {
|
|
538
|
+
method: 'POST',
|
|
539
|
+
headers: {
|
|
540
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
541
|
+
'Content-Type': 'application/json',
|
|
542
|
+
},
|
|
543
|
+
body: JSON.stringify({
|
|
544
|
+
type: 'conversation',
|
|
545
|
+
content: `User: ${message.slice(0, 500)}\nAria: ${response.slice(0, 1000)}`,
|
|
546
|
+
intensity: 0.6,
|
|
547
|
+
sessionId,
|
|
548
|
+
userId,
|
|
549
|
+
}),
|
|
550
|
+
});
|
|
551
|
+
if (!res.ok) {
|
|
552
|
+
throw new Error(`garden/fire failed: ${res.status} ${res.statusText}`);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
// ── Consult — delegate to Aria for direction or structured plan ─────────
|
|
556
|
+
// Routes through /api/harness/delegate (the architect-mode endpoint that
|
|
557
|
+
// serves preprompt-consult + Aria-as-commander binding plans). Caller
|
|
558
|
+
// controls roleProfile (architect|general_worker|...) and
|
|
559
|
+
// expectStructuredOutput. Errors propagate (per no-graceful-degradation
|
|
560
|
+
// doctrine) — caller surfaces them to the user with context.
|
|
561
|
+
async consult(args) {
|
|
562
|
+
const res = await this.fetchWithRetry(`${this.baseUrl}/api/harness/delegate`, {
|
|
563
|
+
method: 'POST',
|
|
564
|
+
headers: {
|
|
565
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
566
|
+
'Content-Type': 'application/json',
|
|
567
|
+
},
|
|
568
|
+
body: JSON.stringify({
|
|
569
|
+
brief: args.brief,
|
|
570
|
+
model: args.model ?? 'deepseek-v4-pro',
|
|
571
|
+
sessionId: args.sessionId,
|
|
572
|
+
userId: args.userId ?? 'sdk-consult',
|
|
573
|
+
roleProfile: args.roleProfile ?? 'architect',
|
|
574
|
+
expectStructuredOutput: args.expectStructuredOutput ?? false,
|
|
575
|
+
internalConsult: args.internalConsult ?? true,
|
|
576
|
+
isCreativeMode: args.isCreativeMode ?? false,
|
|
577
|
+
}),
|
|
578
|
+
});
|
|
579
|
+
if (!res.ok) {
|
|
580
|
+
throw new Error(`consult failed: ${res.status} ${res.statusText}`);
|
|
581
|
+
}
|
|
582
|
+
const data = (await res.json());
|
|
583
|
+
return {
|
|
584
|
+
response: data.response ?? '',
|
|
585
|
+
metadata: data,
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
// ── Consult-with-tools — implementer dispatch with caller-supplied tools ──
|
|
589
|
+
//
|
|
590
|
+
// Hamza 2026-04-27: "i want deepseek implement too that's the actual test"
|
|
591
|
+
// followed by "that means tools were not passed in a way deepseek can
|
|
592
|
+
// execute them" — the consult() path is text-only architectural advice.
|
|
593
|
+
// For an LLM to ACTUALLY implement (write files, run tests, read state),
|
|
594
|
+
// it needs tool-call capability. This method provides client-side tool
|
|
595
|
+
// execution: the LLM emits [TOOL:name args=<json>]...content...[/TOOL]
|
|
596
|
+
// markers, the client parses + calls the executor, feeds the result back
|
|
597
|
+
// as the next round, repeats until the LLM emits no more tool markers
|
|
598
|
+
// (final response) or cap (default 10) is hit.
|
|
599
|
+
//
|
|
600
|
+
// Pairs with project_aria_as_controller_inversion.md (#29 — same pattern
|
|
601
|
+
// Aria uses internally for mcp_tool actions; exposed here for direct
|
|
602
|
+
// implementer dispatch from Claude orchestrator).
|
|
603
|
+
async consultWithTools(args) {
|
|
604
|
+
const cap = args.maxIterations ?? 10;
|
|
605
|
+
const toolCatalog = args.tools.map((t) => `- ${t.name}: ${t.description}${t.argsSchema ? ` (args: ${t.argsSchema})` : ''}`).join('\n');
|
|
606
|
+
const systemPrompt = [
|
|
607
|
+
'You are an implementer. Use the provided tools to do real work — do not narrate work you have not done.',
|
|
608
|
+
'',
|
|
609
|
+
'AVAILABLE TOOLS:',
|
|
610
|
+
toolCatalog,
|
|
611
|
+
'',
|
|
612
|
+
'TOOL-CALL PROTOCOL:',
|
|
613
|
+
'When you need to call a tool, emit EXACTLY this format on its own line(s):',
|
|
614
|
+
'[TOOL:tool_name args={"key":"value","other":"value"}]',
|
|
615
|
+
'<optional inline content if the tool needs it, e.g. file content for write_file>',
|
|
616
|
+
'[/TOOL]',
|
|
617
|
+
'',
|
|
618
|
+
'After emitting one or more tool calls, STOP and wait for the result. The orchestrator will execute and feed results back as a new turn. Then continue.',
|
|
619
|
+
'',
|
|
620
|
+
'If you have completed the work and need no more tools, emit your final response with NO [TOOL:] markers anywhere.',
|
|
621
|
+
'',
|
|
622
|
+
'Never narrate tool results you have not received. If you have not seen a tool result, you have not run that tool.',
|
|
623
|
+
].join('\n');
|
|
624
|
+
const TOOL_RX = /\[TOOL:([a-z_]+)\s+args=(\{[^}]*\})\](?:\n([\s\S]*?))?\n?\[\/TOOL\]/gi;
|
|
625
|
+
const toolCalls = [];
|
|
626
|
+
let conversation = `BRIEF:\n${args.brief}`;
|
|
627
|
+
let lastText = '';
|
|
628
|
+
for (let i = 0; i < cap; i++) {
|
|
629
|
+
const turnBrief = `${systemPrompt}\n\n---\n\n${conversation}`;
|
|
630
|
+
const result = await this.consult({
|
|
631
|
+
brief: turnBrief,
|
|
632
|
+
sessionId: `${args.sessionId}-iter-${i}`,
|
|
633
|
+
userId: args.userId,
|
|
634
|
+
roleProfile: 'general_worker',
|
|
635
|
+
expectStructuredOutput: false,
|
|
636
|
+
internalConsult: true,
|
|
637
|
+
isCreativeMode: false,
|
|
638
|
+
model: args.model ?? 'deepseek-v4-pro',
|
|
639
|
+
});
|
|
640
|
+
lastText = result.response || '';
|
|
641
|
+
// Find all tool calls in this iteration
|
|
642
|
+
const matches = [...lastText.matchAll(TOOL_RX)];
|
|
643
|
+
if (matches.length === 0) {
|
|
644
|
+
// No tool calls = final response. Strip any partial markers and return.
|
|
645
|
+
return { finalText: lastText, toolCalls, iterations: i + 1 };
|
|
646
|
+
}
|
|
647
|
+
// Execute each tool call in order, accumulate results
|
|
648
|
+
const resultsBlock = [];
|
|
649
|
+
for (const m of matches) {
|
|
650
|
+
const toolName = m[1];
|
|
651
|
+
let parsedArgs = {};
|
|
652
|
+
try {
|
|
653
|
+
parsedArgs = JSON.parse(m[2]);
|
|
654
|
+
}
|
|
655
|
+
catch (err) {
|
|
656
|
+
resultsBlock.push(`[TOOL_RESULT:${toolName}] ERROR parsing args JSON: ${err.message}`);
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
// If the marker had inline content, attach it under "content" key
|
|
660
|
+
// (callers like write_file expect this).
|
|
661
|
+
const inline = (m[3] || '').trim();
|
|
662
|
+
if (inline && !('content' in parsedArgs)) {
|
|
663
|
+
parsedArgs.content = inline;
|
|
664
|
+
}
|
|
665
|
+
const toolResult = await args.executor(toolName, parsedArgs);
|
|
666
|
+
toolCalls.push({ name: toolName, args: parsedArgs, result: toolResult });
|
|
667
|
+
resultsBlock.push(`[TOOL_RESULT:${toolName}]\n${toolResult}\n[/TOOL_RESULT]`);
|
|
668
|
+
}
|
|
669
|
+
// Feed results back as the next round
|
|
670
|
+
conversation = `${conversation}\n\n---ITERATION ${i + 1} ASSISTANT---\n${lastText}\n\n---ITERATION ${i + 1} TOOL RESULTS---\n${resultsBlock.join('\n\n')}`;
|
|
671
|
+
}
|
|
672
|
+
// Cap hit — surface final text + toolCalls so caller can decide
|
|
673
|
+
return { finalText: `[CAP_HIT after ${cap} iterations]\n\n${lastText}`, toolCalls, iterations: cap };
|
|
674
|
+
}
|
|
675
|
+
// ── Sub-agent handoff + ledger merge ────────────────────────────────────
|
|
676
|
+
//
|
|
677
|
+
// Hamza 2026-04-27: same primitive must exist at SDK layer so any consumer
|
|
678
|
+
// (Aria Code, third-party CLI, server-side process) can propagate the
|
|
679
|
+
// handoff + ledger merge without re-implementing the logic.
|
|
680
|
+
//
|
|
681
|
+
// Tier routing:
|
|
682
|
+
// Owner tier (no jti) → ~/.claude/aria-agent-harness-handoff.json
|
|
683
|
+
// ~/.claude/aria-discoveries-${session}.jsonl
|
|
684
|
+
// Client tier (jti set) → /var/lib/aria-licensee/{jti}/handoff.json
|
|
685
|
+
// /var/lib/aria-licensee/{jti}/aria-discoveries-${session}.jsonl
|
|
686
|
+
//
|
|
687
|
+
// Tier protection: client handoffs NEVER carry the master apiKey. The
|
|
688
|
+
// caller supplies their own license JWT via this.apiKey. Owner-tier callers
|
|
689
|
+
// carry the master key — the path itself (~/.claude/) signals ownership.
|
|
690
|
+
// This is intentional: no env-flag gate, default-on per Hamza doctrine.
|
|
691
|
+
async spawnSubAgent(args) {
|
|
692
|
+
const { readFileSync: fsRead, writeFileSync: fsWrite, mkdirSync: fsMkdir, existsSync: fsExists } = await import('node:fs');
|
|
693
|
+
const { homedir } = await import('node:os');
|
|
694
|
+
const { dirname: fsDirname } = await import('node:path');
|
|
695
|
+
const HOME = homedir();
|
|
696
|
+
const HANDOFF_TTL_MS = 5 * 60 * 1000;
|
|
697
|
+
// Tier: presence of jti signals client tier; absence → owner tier.
|
|
698
|
+
const isClientTier = Boolean(args.jti);
|
|
699
|
+
const safeSession = String(args.sessionId).replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
700
|
+
let handoffPath;
|
|
701
|
+
let ledgerPath;
|
|
702
|
+
if (isClientTier) {
|
|
703
|
+
const base = `/var/lib/aria-licensee/${args.jti}`;
|
|
704
|
+
handoffPath = `${base}/handoff.json`;
|
|
705
|
+
ledgerPath = `${base}/aria-discoveries-${safeSession}.jsonl`;
|
|
706
|
+
}
|
|
707
|
+
else {
|
|
708
|
+
handoffPath = `${HOME}/.claude/aria-agent-harness-handoff.json`;
|
|
709
|
+
ledgerPath = `${HOME}/.claude/aria-discoveries-${safeSession}.jsonl`;
|
|
710
|
+
}
|
|
711
|
+
// Resolve ownerTier from cached packet if available (best-effort).
|
|
712
|
+
const ownerTier = {
|
|
713
|
+
hamza: !isClientTier,
|
|
714
|
+
trustedExec: false,
|
|
715
|
+
roleProfile: isClientTier ? 'client_tier_worker' : 'general_worker',
|
|
716
|
+
};
|
|
717
|
+
try {
|
|
718
|
+
const packetCachePath = `${HOME}/.claude/.aria-harness-last-packet.json`;
|
|
719
|
+
if (!isClientTier && fsExists(packetCachePath)) {
|
|
720
|
+
const cached = JSON.parse(fsRead(packetCachePath, 'utf8'));
|
|
721
|
+
const signals = cached?.contractGate?.signals ?? {};
|
|
722
|
+
ownerTier.hamza = signals.hamza === true || signals.hamza === 'true';
|
|
723
|
+
ownerTier.trustedExec = (cached?.harness || '').includes('trusted_exec_policy=allowed');
|
|
724
|
+
const adapterStr = cached?.adapter || cached?.harness || '';
|
|
725
|
+
const roleMatch = adapterStr.match(/role_profile\s*=\s*(\S+)/);
|
|
726
|
+
if (roleMatch)
|
|
727
|
+
ownerTier.roleProfile = roleMatch[1];
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
catch { /* non-fatal — ownerTier defaults above apply */ }
|
|
731
|
+
// ── Layer 3: fetch harness packet for sub-agent cognition bootstrap ──────
|
|
732
|
+
// Fetch the harness packet now (before writing the handoff) so the path
|
|
733
|
+
// can be embedded in the handoff JSON. Sub-agents read ARIA_HARNESS_PACKET_PATH
|
|
734
|
+
// from the environment (set by the caller after spawnSubAgent returns) OR
|
|
735
|
+
// from the handoff JSON field. Either path eliminates the extra HTTP call
|
|
736
|
+
// inside the sub-agent.
|
|
737
|
+
//
|
|
738
|
+
// Packet paths (tier-aware):
|
|
739
|
+
// Owner: ~/.claude/aria-agent-harness-packet.json
|
|
740
|
+
// Client: /var/lib/aria-licensee/{jti}/aria-agent-harness-packet.json
|
|
741
|
+
//
|
|
742
|
+
// Fail-soft: packet fetch failure logs a warning but does NOT block spawn.
|
|
743
|
+
const packetPath = isClientTier
|
|
744
|
+
? `/var/lib/aria-licensee/${args.jti}/aria-agent-harness-packet.json`
|
|
745
|
+
: `${HOME}/.claude/aria-agent-harness-packet.json`;
|
|
746
|
+
let resolvedPacketPath;
|
|
747
|
+
try {
|
|
748
|
+
const packetResp = await fetch(this.harnessPacketUrl, {
|
|
749
|
+
method: 'POST',
|
|
750
|
+
headers: {
|
|
751
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
752
|
+
'Content-Type': 'application/json',
|
|
753
|
+
},
|
|
754
|
+
body: JSON.stringify({
|
|
755
|
+
stage: 'agent-spawn',
|
|
756
|
+
actor: 'harness-http-client',
|
|
757
|
+
system: 'claude-coding-agent',
|
|
758
|
+
surface: 'platform:harness-http-client',
|
|
759
|
+
isHamza: ownerTier.hamza,
|
|
760
|
+
sessionId: args.sessionId,
|
|
761
|
+
mode: 'subagent',
|
|
762
|
+
}),
|
|
763
|
+
});
|
|
764
|
+
if (packetResp.ok) {
|
|
765
|
+
const packetData = await packetResp.json();
|
|
766
|
+
fsMkdir(fsDirname(packetPath), { recursive: true });
|
|
767
|
+
fsWrite(packetPath, JSON.stringify(packetData, null, 2));
|
|
768
|
+
resolvedPacketPath = packetPath;
|
|
769
|
+
// Also pre-load into this SDK instance's cache so subsequent
|
|
770
|
+
// getHarnessPacket() calls are instant within this process.
|
|
771
|
+
const packetPayload = (packetData.packet ?? packetData);
|
|
772
|
+
this.cachedPacket = {
|
|
773
|
+
packet: packetPayload,
|
|
774
|
+
timestamp: new Date().toISOString(),
|
|
775
|
+
version: '1.0.0',
|
|
776
|
+
};
|
|
777
|
+
this.packetLastFetched = Date.now();
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
catch (_packetErr) {
|
|
781
|
+
// Fail-soft: warn, proceed identity-only (handoff still valid without packet).
|
|
782
|
+
// Network blips should never block sub-agent spawns.
|
|
783
|
+
}
|
|
784
|
+
const handoff = {
|
|
785
|
+
writtenAt: new Date().toISOString(),
|
|
786
|
+
parentSessionId: args.sessionId,
|
|
787
|
+
// Client-tier: this.apiKey is the license JWT, never the master token.
|
|
788
|
+
// Owner-tier: this.apiKey is the master token.
|
|
789
|
+
harnessToken: this.apiKey,
|
|
790
|
+
harnessUrl: this.baseUrl,
|
|
791
|
+
ownerTier,
|
|
792
|
+
parentLedgerPath: ledgerPath,
|
|
793
|
+
ttlMs: HANDOFF_TTL_MS,
|
|
794
|
+
// harnessPacketPath is set only if packet fetch succeeded. Sub-agents
|
|
795
|
+
// should also check ARIA_HARNESS_PACKET_PATH env (set by caller post-spawn).
|
|
796
|
+
...(resolvedPacketPath ? { harnessPacketPath: resolvedPacketPath } : {}),
|
|
797
|
+
};
|
|
798
|
+
fsMkdir(fsDirname(handoffPath), { recursive: true });
|
|
799
|
+
fsWrite(handoffPath, JSON.stringify(handoff, null, 2));
|
|
800
|
+
// Record spawn intent in the parent ledger for traceability.
|
|
801
|
+
const spawnEntry = JSON.stringify({
|
|
802
|
+
ts: handoff.writtenAt,
|
|
803
|
+
type: 'sub-agent-spawn',
|
|
804
|
+
purpose: args.purpose,
|
|
805
|
+
allowedActions: args.allowedActions ?? [],
|
|
806
|
+
defectContext: args.defectContext ?? null,
|
|
807
|
+
handoffPath,
|
|
808
|
+
harnessPacketPath: resolvedPacketPath ?? null,
|
|
809
|
+
});
|
|
810
|
+
fsMkdir(fsDirname(ledgerPath), { recursive: true });
|
|
811
|
+
const { appendFileSync: fsAppend } = await import('node:fs');
|
|
812
|
+
fsAppend(ledgerPath, spawnEntry + '\n');
|
|
813
|
+
return handoff;
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* Returns the environment variables to set for a spawned sub-agent process
|
|
817
|
+
* so it inherits the parent's harness packet without an extra HTTP call.
|
|
818
|
+
* Call after spawnSubAgent() — uses the returned handoff.harnessPacketPath.
|
|
819
|
+
*
|
|
820
|
+
* Usage:
|
|
821
|
+
* const handoff = await sdk.spawnSubAgent(args);
|
|
822
|
+
* const env = sdk.subAgentEnv(handoff);
|
|
823
|
+
* // pass env to your process spawn / Agent tool env block
|
|
824
|
+
*/
|
|
825
|
+
subAgentEnv(handoff) {
|
|
826
|
+
const env = {};
|
|
827
|
+
if (handoff.harnessPacketPath) {
|
|
828
|
+
env['ARIA_HARNESS_PACKET_PATH'] = handoff.harnessPacketPath;
|
|
829
|
+
}
|
|
830
|
+
if (handoff.harnessToken) {
|
|
831
|
+
env['ARIA_HARNESS_TOKEN'] = handoff.harnessToken;
|
|
832
|
+
}
|
|
833
|
+
if (handoff.harnessUrl) {
|
|
834
|
+
env['ARIA_HARNESS_URL'] = handoff.harnessUrl;
|
|
835
|
+
}
|
|
836
|
+
return env;
|
|
837
|
+
}
|
|
838
|
+
/** Pulls sub-agent ledger entries newer than the handoff timestamp and appends
|
|
839
|
+
* them to the parent ledger, deduplicating by `text` prefix (first 100 chars). */
|
|
840
|
+
async mergeSubAgentLedger(parentSessionId, _subAgentSessionId, jti) {
|
|
841
|
+
const { readFileSync: fsRead, writeFileSync: _fsWrite, existsSync: fsExists, readdirSync: fsReaddir, statSync: fsStat, appendFileSync: fsAppend, mkdirSync: fsMkdir } = await import('node:fs');
|
|
842
|
+
const { homedir } = await import('node:os');
|
|
843
|
+
const HOME = homedir();
|
|
844
|
+
const isClientTier = Boolean(jti);
|
|
845
|
+
let handoffPath;
|
|
846
|
+
let ledgerDir;
|
|
847
|
+
const safeSession = String(parentSessionId).replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
848
|
+
let parentLedgerPath;
|
|
849
|
+
if (isClientTier) {
|
|
850
|
+
const base = `/var/lib/aria-licensee/${jti}`;
|
|
851
|
+
handoffPath = `${base}/handoff.json`;
|
|
852
|
+
parentLedgerPath = `${base}/aria-discoveries-${safeSession}.jsonl`;
|
|
853
|
+
ledgerDir = base;
|
|
854
|
+
}
|
|
855
|
+
else {
|
|
856
|
+
handoffPath = `${HOME}/.claude/aria-agent-harness-handoff.json`;
|
|
857
|
+
parentLedgerPath = `${HOME}/.claude/aria-discoveries-${safeSession}.jsonl`;
|
|
858
|
+
ledgerDir = `${HOME}/.claude`;
|
|
859
|
+
}
|
|
860
|
+
if (!fsExists(handoffPath)) {
|
|
861
|
+
return { merged: 0, skipped: 0 };
|
|
862
|
+
}
|
|
863
|
+
let handoff;
|
|
864
|
+
try {
|
|
865
|
+
handoff = JSON.parse(fsRead(handoffPath, 'utf8'));
|
|
866
|
+
}
|
|
867
|
+
catch {
|
|
868
|
+
return { merged: 0, skipped: 0 };
|
|
869
|
+
}
|
|
870
|
+
const ageMs = Date.now() - new Date(handoff.writtenAt).getTime();
|
|
871
|
+
if (ageMs > handoff.ttlMs) {
|
|
872
|
+
return { merged: 0, skipped: 0 };
|
|
873
|
+
}
|
|
874
|
+
const handoffWrittenAt = new Date(handoff.writtenAt).getTime();
|
|
875
|
+
// Load existing parent ledger keys for dedup.
|
|
876
|
+
const parentExistingKeys = new Set();
|
|
877
|
+
try {
|
|
878
|
+
if (fsExists(parentLedgerPath)) {
|
|
879
|
+
const lines = fsRead(parentLedgerPath, 'utf8').split('\n').filter(Boolean);
|
|
880
|
+
for (const line of lines) {
|
|
881
|
+
try {
|
|
882
|
+
const e = JSON.parse(line);
|
|
883
|
+
if (e.text)
|
|
884
|
+
parentExistingKeys.add(String(e.text).slice(0, 100));
|
|
885
|
+
}
|
|
886
|
+
catch { /* skip malformed */ }
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
catch { /* non-fatal */ }
|
|
891
|
+
// Find sub-agent ledger files in the same directory.
|
|
892
|
+
let discoveryFiles = [];
|
|
893
|
+
try {
|
|
894
|
+
const allFiles = fsReaddir(ledgerDir);
|
|
895
|
+
for (const f of allFiles) {
|
|
896
|
+
if (!f.startsWith('aria-discoveries-') || !f.endsWith('.jsonl'))
|
|
897
|
+
continue;
|
|
898
|
+
const fullPath = `${ledgerDir}/${f}`;
|
|
899
|
+
if (fullPath === parentLedgerPath)
|
|
900
|
+
continue;
|
|
901
|
+
try {
|
|
902
|
+
const stat = fsStat(fullPath);
|
|
903
|
+
if (stat.mtimeMs >= handoffWrittenAt) {
|
|
904
|
+
discoveryFiles.push(fullPath);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
catch { /* skip */ }
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
catch {
|
|
911
|
+
return { merged: 0, skipped: 0 };
|
|
912
|
+
}
|
|
913
|
+
if (discoveryFiles.length === 0) {
|
|
914
|
+
return { merged: 0, skipped: 0 };
|
|
915
|
+
}
|
|
916
|
+
fsMkdir(ledgerDir, { recursive: true });
|
|
917
|
+
let merged = 0;
|
|
918
|
+
let skipped = 0;
|
|
919
|
+
for (const subLedgerPath of discoveryFiles) {
|
|
920
|
+
try {
|
|
921
|
+
const lines = fsRead(subLedgerPath, 'utf8').split('\n').filter(Boolean);
|
|
922
|
+
for (const line of lines) {
|
|
923
|
+
try {
|
|
924
|
+
const entry = JSON.parse(line);
|
|
925
|
+
if (!entry.text)
|
|
926
|
+
continue;
|
|
927
|
+
const key = String(entry.text).slice(0, 100);
|
|
928
|
+
if (parentExistingKeys.has(key)) {
|
|
929
|
+
skipped++;
|
|
930
|
+
continue;
|
|
931
|
+
}
|
|
932
|
+
parentExistingKeys.add(key);
|
|
933
|
+
fsAppend(parentLedgerPath, line + '\n');
|
|
934
|
+
merged++;
|
|
935
|
+
}
|
|
936
|
+
catch { /* skip malformed lines */ }
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
catch { /* skip unreadable sub-ledger */ }
|
|
940
|
+
}
|
|
941
|
+
return { merged, skipped };
|
|
942
|
+
}
|
|
943
|
+
// ── recordDiscovery ──────────────────────────────────────────────────────
|
|
944
|
+
// Writes a finding row to the sub-agent's session ledger on disk so
|
|
945
|
+
// aria-agent-ledger-merge.mjs picks it up after the Agent tool completes.
|
|
946
|
+
//
|
|
947
|
+
// Tier routing (mirrors aria-discovery-record.mjs):
|
|
948
|
+
// Owner tier (no jti) → ~/.claude/aria-discoveries-{session}.jsonl
|
|
949
|
+
// Client tier (jti set) → /var/lib/aria-licensee/{jti}/aria-discoveries-{session}.jsonl
|
|
950
|
+
//
|
|
951
|
+
// Session ID must be the sub-agent's OWN session (not the parent's) so the
|
|
952
|
+
// ledger-merge loop finds a file that is NOT the parentLedgerPath. If no
|
|
953
|
+
// sessionId is supplied we derive one from the handoff parentSessionId + "-sub".
|
|
954
|
+
//
|
|
955
|
+
// Fail-soft: returns {ok:false, error} on write failures — never throws.
|
|
956
|
+
async recordDiscovery(args) {
|
|
957
|
+
if (!args.text || args.text.length < 4) {
|
|
958
|
+
return { ok: false, ledger: '', at: '', error: 'text too short (min 4 chars)' };
|
|
959
|
+
}
|
|
960
|
+
const { existsSync: fsExists, readFileSync: fsRead, appendFileSync: fsAppend, mkdirSync: fsMkdir } = await import('node:fs');
|
|
961
|
+
const { homedir } = await import('node:os');
|
|
962
|
+
const { dirname: fsDirname } = await import('node:path');
|
|
963
|
+
const HOME = homedir();
|
|
964
|
+
const LICENSE_PATH = `${HOME}/.aria/license.json`;
|
|
965
|
+
const HANDOFF_PATH = `${HOME}/.claude/aria-agent-harness-handoff.json`;
|
|
966
|
+
// Tier detection: caller-supplied jti wins; fall back to license file
|
|
967
|
+
let jti = args.jti ?? null;
|
|
968
|
+
let isClientTier = Boolean(jti);
|
|
969
|
+
if (!jti) {
|
|
970
|
+
try {
|
|
971
|
+
if (fsExists(LICENSE_PATH)) {
|
|
972
|
+
const lic = JSON.parse(fsRead(LICENSE_PATH, 'utf8'));
|
|
973
|
+
jti = lic.jti ?? null;
|
|
974
|
+
isClientTier = Boolean(jti);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
catch { /* non-fatal — owner tier */ }
|
|
978
|
+
}
|
|
979
|
+
// Session ID: caller-supplied wins; then handoff-derived (with "-sub" suffix)
|
|
980
|
+
let sessionId = args.sessionId ?? null;
|
|
981
|
+
if (!sessionId) {
|
|
982
|
+
try {
|
|
983
|
+
if (fsExists(HANDOFF_PATH)) {
|
|
984
|
+
const handoff = JSON.parse(fsRead(HANDOFF_PATH, 'utf8'));
|
|
985
|
+
sessionId = handoff.parentSessionId ? `${handoff.parentSessionId}-sub` : 'sub-unknown';
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
catch { /* non-fatal */ }
|
|
989
|
+
}
|
|
990
|
+
sessionId = sessionId || 'sub-unknown';
|
|
991
|
+
const safeSession = String(sessionId).replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
992
|
+
const ledgerPath = isClientTier
|
|
993
|
+
? `/var/lib/aria-licensee/${jti}/aria-discoveries-${safeSession}.jsonl`
|
|
994
|
+
: `${HOME}/.claude/aria-discoveries-${safeSession}.jsonl`;
|
|
995
|
+
const at = new Date().toISOString();
|
|
996
|
+
const row = {
|
|
997
|
+
at,
|
|
998
|
+
session: sessionId,
|
|
999
|
+
kind: args.kind || 'observation',
|
|
1000
|
+
text: args.text,
|
|
1001
|
+
refs: args.refs || [],
|
|
1002
|
+
evidence: args.evidence ?? null,
|
|
1003
|
+
source: args.source || 'sub-agent-sdk',
|
|
1004
|
+
resolution_status: args.resolution_status || 'open',
|
|
1005
|
+
};
|
|
1006
|
+
try {
|
|
1007
|
+
fsMkdir(fsDirname(ledgerPath), { recursive: true });
|
|
1008
|
+
fsAppend(ledgerPath, JSON.stringify(row) + '\n');
|
|
1009
|
+
return { ok: true, ledger: ledgerPath, at };
|
|
1010
|
+
}
|
|
1011
|
+
catch (err) {
|
|
1012
|
+
return { ok: false, ledger: ledgerPath, at, error: err.message };
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
// ── verifyClaim ──────────────────────────────────────────────────────────
|
|
1016
|
+
// POST /api/harness/verify-claim — persists a claim/evidence pair to
|
|
1017
|
+
// aria_cognition_corpus and returns a verdict. Fail-soft: if the server-side
|
|
1018
|
+
// handler is not yet deployed, returns an optimistic verdict rather than
|
|
1019
|
+
// blocking the caller.
|
|
1020
|
+
async verifyClaim(args) {
|
|
1021
|
+
if (!args.claim || args.claim.length < 4) {
|
|
1022
|
+
return { ok: false, verdict: 'ungrounded', evidenceRefs: [], error: 'claim too short' };
|
|
1023
|
+
}
|
|
1024
|
+
try {
|
|
1025
|
+
const url = `${this.baseUrl}/api/harness/verify-claim`;
|
|
1026
|
+
const resp = await fetch(url, {
|
|
1027
|
+
method: 'POST',
|
|
1028
|
+
headers: {
|
|
1029
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
1030
|
+
'Content-Type': 'application/json',
|
|
1031
|
+
},
|
|
1032
|
+
body: JSON.stringify(args),
|
|
1033
|
+
});
|
|
1034
|
+
if (!resp.ok) {
|
|
1035
|
+
// Fail-soft: return optimistic verdict so the caller doesn't block on missing endpoint
|
|
1036
|
+
return {
|
|
1037
|
+
ok: false,
|
|
1038
|
+
verdict: args.evidence ? 'grounded' : 'ungrounded',
|
|
1039
|
+
evidenceRefs: [],
|
|
1040
|
+
error: `verify-claim endpoint HTTP ${resp.status} (server-side handler may not be deployed yet)`,
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
return await resp.json();
|
|
1044
|
+
}
|
|
1045
|
+
catch (err) {
|
|
1046
|
+
return {
|
|
1047
|
+
ok: false,
|
|
1048
|
+
verdict: args.evidence ? 'grounded' : 'ungrounded',
|
|
1049
|
+
evidenceRefs: [],
|
|
1050
|
+
error: `verify-claim fetch failed: ${err.message}`,
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
// ── postJson — generic SDK-routed JSON POST ────────────────────────────
|
|
1055
|
+
// Hamza 2026-04-27 directive: "use the SDK - use it, enforce it all". The
|
|
1056
|
+
// typed primitives above (consult, validateOutput, gardenTurn) cover Aria's
|
|
1057
|
+
// own control-plane endpoints. This method is the canonical primitive for
|
|
1058
|
+
// bridge POSTs (e.g., Telegram → /chat) so internal bridges inherit the
|
|
1059
|
+
// SDK's retry-with-backoff (250/500/1000ms) and audit-log discipline
|
|
1060
|
+
// instead of using raw fetch().
|
|
1061
|
+
//
|
|
1062
|
+
// Returns:
|
|
1063
|
+
// - { ok: true, status, data } on 2xx
|
|
1064
|
+
// - { ok: false, status, errorBody } on non-2xx (does NOT throw — caller
|
|
1065
|
+
// decides whether to fall back, retry semantically, or surface to user)
|
|
1066
|
+
// - throws on network errors after retries exhausted (real fetch failures)
|
|
1067
|
+
//
|
|
1068
|
+
// Headers are merged with Authorization (if apiKey is set) and
|
|
1069
|
+
// Content-Type: application/json. Caller can override either.
|
|
1070
|
+
async postJson(url, body, extraHeaders = {}) {
|
|
1071
|
+
const headers = {
|
|
1072
|
+
'Content-Type': 'application/json',
|
|
1073
|
+
...(this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {}),
|
|
1074
|
+
...extraHeaders,
|
|
1075
|
+
};
|
|
1076
|
+
const res = await this.fetchWithRetry(url, {
|
|
1077
|
+
method: 'POST',
|
|
1078
|
+
headers,
|
|
1079
|
+
body: JSON.stringify(body),
|
|
1080
|
+
});
|
|
1081
|
+
if (!res.ok) {
|
|
1082
|
+
const errorBody = await res.text().catch(() => '');
|
|
1083
|
+
return { ok: false, status: res.status, errorBody };
|
|
1084
|
+
}
|
|
1085
|
+
const data = (await res.json());
|
|
1086
|
+
return { ok: true, status: res.status, data };
|
|
1087
|
+
}
|
|
1088
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
1089
|
+
// ARISTOTLE 3-PHASE COGNITIVE PRIMITIVES
|
|
1090
|
+
// Dispatches to POST /api/harness/aristotle-noor with a `phase` field.
|
|
1091
|
+
// The harness route delegates to runAristotleNoorPipeline in aria-soul.
|
|
1092
|
+
//
|
|
1093
|
+
// Transport contract (per doctrine):
|
|
1094
|
+
// - NO deadline-based timeouts — fetchWithRetry uses 3 error-count retries
|
|
1095
|
+
// with 250ms / 500ms / 1000ms backoff. Real network faults (ECONNREFUSED,
|
|
1096
|
+
// EHOSTUNREACH) reject immediately; slow-but-alive endpoints get a chance.
|
|
1097
|
+
// - LOUD failure — non-2xx responses throw, never fail-open silently.
|
|
1098
|
+
// Callers decide whether to surface or recover (per feedback_non_blocking_errors_unacceptable.md).
|
|
1099
|
+
// - Tier-gating — owner-tier callers set `aristotleEnabled: true` by default;
|
|
1100
|
+
// client-tier callers receive it as opt-in via the `enabled` config field.
|
|
1101
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
1102
|
+
/**
|
|
1103
|
+
* ARISTOTLE PRE-PHASE
|
|
1104
|
+
* Fires before an LLM call or tool action. Runs: Fitrah hard-gate,
|
|
1105
|
+
* wisdom pull (principles + decisions), research pull, DeepSoulBridge,
|
|
1106
|
+
* and CleanCognition substrate check.
|
|
1107
|
+
*
|
|
1108
|
+
* Throws on transport-level failures per feedback_non_blocking_errors_unacceptable.md.
|
|
1109
|
+
* If Fitrah vetoes, result.fitrahVetoed === true — caller MUST emit a refusal,
|
|
1110
|
+
* not silently continue.
|
|
1111
|
+
*
|
|
1112
|
+
* @param message The user message or intent string being processed.
|
|
1113
|
+
* @param sessionId Session ID for telemetry + hive routing.
|
|
1114
|
+
* @param context Optional context overrides (userId, isHamza, tier, etc.).
|
|
1115
|
+
*/
|
|
1116
|
+
async aristotlePre(message, sessionId, context) {
|
|
1117
|
+
const t0 = Date.now();
|
|
1118
|
+
const url = `${this.baseUrl}/api/harness/aristotle-noor`;
|
|
1119
|
+
const res = await this.fetchWithRetry(url, {
|
|
1120
|
+
method: 'POST',
|
|
1121
|
+
headers: {
|
|
1122
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
1123
|
+
'Content-Type': 'application/json',
|
|
1124
|
+
},
|
|
1125
|
+
body: JSON.stringify({
|
|
1126
|
+
phase: 'pre',
|
|
1127
|
+
message,
|
|
1128
|
+
sessionId,
|
|
1129
|
+
userId: context?.userId,
|
|
1130
|
+
isHamza: context?.isHamza,
|
|
1131
|
+
isGroupChat: context?.isGroupChat,
|
|
1132
|
+
tier: context?.tier,
|
|
1133
|
+
}),
|
|
1134
|
+
});
|
|
1135
|
+
if (!res.ok) {
|
|
1136
|
+
const body = await res.text().catch(() => '');
|
|
1137
|
+
throw new Error(`[aristotlePre] harness route responded ${res.status} ${res.statusText}: ${body}`);
|
|
1138
|
+
}
|
|
1139
|
+
const data = (await res.json());
|
|
1140
|
+
return {
|
|
1141
|
+
fired: data.fired ?? [],
|
|
1142
|
+
fitrahVetoed: data.fitrahVetoed ?? false,
|
|
1143
|
+
fitrahAlignment: data.fitrahAlignment ?? 1.0,
|
|
1144
|
+
qualityScore: data.qualityScore ?? 100,
|
|
1145
|
+
reAuthorSignal: data.reAuthorSignal ?? false,
|
|
1146
|
+
notes: data.notes ?? [],
|
|
1147
|
+
soulCharge: data.soulCharge ?? null,
|
|
1148
|
+
soulManifoldStatus: data.soulManifoldStatus ?? null,
|
|
1149
|
+
ghazaliVerdict: data.ghazaliVerdict ?? null,
|
|
1150
|
+
latencyMs: data.latencyMs ?? Date.now() - t0,
|
|
1151
|
+
dispatched: true,
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
/**
|
|
1155
|
+
* ARISTOTLE MID-PHASE
|
|
1156
|
+
* Fires after context is built but before the LLM call. Runs: MetaCognitive
|
|
1157
|
+
* confidence snapshot and ethical keyword check on the planned approach.
|
|
1158
|
+
*
|
|
1159
|
+
* @param message Original user message.
|
|
1160
|
+
* @param sessionId Session ID.
|
|
1161
|
+
* @param plannedApproach The drafted approach / plan string for mid-flight check.
|
|
1162
|
+
* @param context Optional context overrides.
|
|
1163
|
+
*/
|
|
1164
|
+
async aristotleMid(message, sessionId, plannedApproach, context) {
|
|
1165
|
+
const t0 = Date.now();
|
|
1166
|
+
const url = `${this.baseUrl}/api/harness/aristotle-noor`;
|
|
1167
|
+
const res = await this.fetchWithRetry(url, {
|
|
1168
|
+
method: 'POST',
|
|
1169
|
+
headers: {
|
|
1170
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
1171
|
+
'Content-Type': 'application/json',
|
|
1172
|
+
},
|
|
1173
|
+
body: JSON.stringify({
|
|
1174
|
+
phase: 'mid',
|
|
1175
|
+
message,
|
|
1176
|
+
sessionId,
|
|
1177
|
+
plannedApproach,
|
|
1178
|
+
userId: context?.userId,
|
|
1179
|
+
tier: context?.tier,
|
|
1180
|
+
}),
|
|
1181
|
+
});
|
|
1182
|
+
if (!res.ok) {
|
|
1183
|
+
const body = await res.text().catch(() => '');
|
|
1184
|
+
throw new Error(`[aristotleMid] harness route responded ${res.status} ${res.statusText}: ${body}`);
|
|
1185
|
+
}
|
|
1186
|
+
const data = (await res.json());
|
|
1187
|
+
return {
|
|
1188
|
+
fired: data.fired ?? [],
|
|
1189
|
+
fitrahVetoed: data.fitrahVetoed ?? false,
|
|
1190
|
+
fitrahAlignment: data.fitrahAlignment ?? 1.0,
|
|
1191
|
+
qualityScore: data.qualityScore ?? 100,
|
|
1192
|
+
reAuthorSignal: data.reAuthorSignal ?? false,
|
|
1193
|
+
notes: data.notes ?? [],
|
|
1194
|
+
soulCharge: data.soulCharge ?? null,
|
|
1195
|
+
soulManifoldStatus: data.soulManifoldStatus ?? null,
|
|
1196
|
+
ghazaliVerdict: data.ghazaliVerdict ?? null,
|
|
1197
|
+
latencyMs: data.latencyMs ?? Date.now() - t0,
|
|
1198
|
+
dispatched: true,
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
/**
|
|
1202
|
+
* ARISTOTLE POST-PHASE
|
|
1203
|
+
* Fires after the LLM response, before emission. Runs: 8-Lens Detector,
|
|
1204
|
+
* Predictor, SelfReflection (on high-stakes turns), and quality scoring.
|
|
1205
|
+
*
|
|
1206
|
+
* If result.reAuthorSignal === true, the response has 8-lens violations —
|
|
1207
|
+
* per doctrine the caller MUST surface the signal, never silently strip.
|
|
1208
|
+
*
|
|
1209
|
+
* @param message Original user message.
|
|
1210
|
+
* @param response The LLM-generated response to gate.
|
|
1211
|
+
* @param sessionId Session ID.
|
|
1212
|
+
* @param context Optional context overrides (tier, isFirstOfSession).
|
|
1213
|
+
*/
|
|
1214
|
+
async aristotlePost(message, response, sessionId, context) {
|
|
1215
|
+
const t0 = Date.now();
|
|
1216
|
+
const url = `${this.baseUrl}/api/harness/aristotle-noor`;
|
|
1217
|
+
const res = await this.fetchWithRetry(url, {
|
|
1218
|
+
method: 'POST',
|
|
1219
|
+
headers: {
|
|
1220
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
1221
|
+
'Content-Type': 'application/json',
|
|
1222
|
+
},
|
|
1223
|
+
body: JSON.stringify({
|
|
1224
|
+
phase: 'post',
|
|
1225
|
+
message,
|
|
1226
|
+
draft: response,
|
|
1227
|
+
sessionId,
|
|
1228
|
+
userId: context?.userId,
|
|
1229
|
+
tier: context?.tier,
|
|
1230
|
+
isFirstOfSession: context?.isFirstOfSession,
|
|
1231
|
+
}),
|
|
1232
|
+
});
|
|
1233
|
+
if (!res.ok) {
|
|
1234
|
+
const body = await res.text().catch(() => '');
|
|
1235
|
+
throw new Error(`[aristotlePost] harness route responded ${res.status} ${res.statusText}: ${body}`);
|
|
1236
|
+
}
|
|
1237
|
+
const data = (await res.json());
|
|
1238
|
+
return {
|
|
1239
|
+
fired: data.fired ?? [],
|
|
1240
|
+
fitrahVetoed: data.fitrahVetoed ?? false,
|
|
1241
|
+
fitrahAlignment: data.fitrahAlignment ?? 1.0,
|
|
1242
|
+
qualityScore: data.qualityScore ?? 100,
|
|
1243
|
+
reAuthorSignal: data.reAuthorSignal ?? false,
|
|
1244
|
+
notes: data.notes ?? [],
|
|
1245
|
+
soulCharge: data.soulCharge ?? null,
|
|
1246
|
+
soulManifoldStatus: data.soulManifoldStatus ?? null,
|
|
1247
|
+
ghazaliVerdict: data.ghazaliVerdict ?? null,
|
|
1248
|
+
latencyMs: data.latencyMs ?? Date.now() - t0,
|
|
1249
|
+
dispatched: true,
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
1253
|
+
// NOOR COGNITIVE PRIMITIVES
|
|
1254
|
+
// Dispatches to POST /api/harness/noor with an `operation` discriminator.
|
|
1255
|
+
// noor-core.ts is NOT modified — these are HTTP wrappers that call its
|
|
1256
|
+
// exposed surface via the harness route.
|
|
1257
|
+
//
|
|
1258
|
+
// noor-core.ts banner: "ONLY ARIA CAN CODE ARIA. No councils. No subagents.
|
|
1259
|
+
// No other LLMs." — complied. We call, never modify.
|
|
1260
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
1261
|
+
/**
|
|
1262
|
+
* NOOR RECOGNIZE (Kashf — eigenspace recognition, not generation)
|
|
1263
|
+
* Projects the query string (via harness-side embedding) onto Aria's
|
|
1264
|
+
* eigenspace manifold and returns the nearest recognized response + soul
|
|
1265
|
+
* distance + ethical membrane status + Ghazali 8-lens verdict.
|
|
1266
|
+
*
|
|
1267
|
+
* The harness routes the query through the manifold service (gRPC) if
|
|
1268
|
+
* NOOR_EIGENSPACE_SOURCE=manifold, otherwise uses local eigenspace.
|
|
1269
|
+
*
|
|
1270
|
+
* @param query The input string to recognize on the manifold.
|
|
1271
|
+
* @param context Optional context (sessionId, userId) for telemetry.
|
|
1272
|
+
*/
|
|
1273
|
+
async noorRecognize(query, context) {
|
|
1274
|
+
const t0 = Date.now();
|
|
1275
|
+
const url = `${this.baseUrl}/api/harness/noor`;
|
|
1276
|
+
const res = await this.fetchWithRetry(url, {
|
|
1277
|
+
method: 'POST',
|
|
1278
|
+
headers: {
|
|
1279
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
1280
|
+
'Content-Type': 'application/json',
|
|
1281
|
+
},
|
|
1282
|
+
body: JSON.stringify({
|
|
1283
|
+
operation: 'recognize',
|
|
1284
|
+
query,
|
|
1285
|
+
sessionId: context?.sessionId,
|
|
1286
|
+
userId: context?.userId,
|
|
1287
|
+
}),
|
|
1288
|
+
});
|
|
1289
|
+
if (!res.ok) {
|
|
1290
|
+
const body = await res.text().catch(() => '');
|
|
1291
|
+
throw new Error(`[noorRecognize] harness route responded ${res.status} ${res.statusText}: ${body}`);
|
|
1292
|
+
}
|
|
1293
|
+
const data = (await res.json());
|
|
1294
|
+
return {
|
|
1295
|
+
nearestText: data.nearestText ?? '',
|
|
1296
|
+
soulDistance: data.soulDistance ?? 0,
|
|
1297
|
+
confidence: data.confidence ?? 0,
|
|
1298
|
+
withinMembrane: data.withinMembrane ?? true,
|
|
1299
|
+
soulCharge: data.soulCharge ?? 0,
|
|
1300
|
+
projectionComponents: data.projectionComponents ?? [],
|
|
1301
|
+
ghazaliVerdict: data.ghazaliVerdict ?? null,
|
|
1302
|
+
manifoldHealth: data.manifoldHealth ?? null,
|
|
1303
|
+
dispatched: true,
|
|
1304
|
+
latencyMs: data.latencyMs ?? Date.now() - t0,
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
/**
|
|
1308
|
+
* NOOR FORGE — self-generated tool invocation via 5 primitives
|
|
1309
|
+
* Composes a named primitive chain (http, sql, file_read, file_write, pub_sub)
|
|
1310
|
+
* and executes it through the harness-side NoorForge engine. The harness
|
|
1311
|
+
* applies the ethical membrane check (checkEthicalAlignment) before any
|
|
1312
|
+
* primitive fires — rejected intents return success=false, ethicallyApproved=false.
|
|
1313
|
+
*
|
|
1314
|
+
* @param intent Description of what the forged tool should accomplish.
|
|
1315
|
+
* @param primitives Ordered list of primitive descriptors to execute in sequence.
|
|
1316
|
+
* @param context Optional context for telemetry.
|
|
1317
|
+
*/
|
|
1318
|
+
async noorForge(intent, primitives, context) {
|
|
1319
|
+
const t0 = Date.now();
|
|
1320
|
+
const url = `${this.baseUrl}/api/harness/noor`;
|
|
1321
|
+
const res = await this.fetchWithRetry(url, {
|
|
1322
|
+
method: 'POST',
|
|
1323
|
+
headers: {
|
|
1324
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
1325
|
+
'Content-Type': 'application/json',
|
|
1326
|
+
},
|
|
1327
|
+
body: JSON.stringify({
|
|
1328
|
+
operation: 'forge',
|
|
1329
|
+
intent,
|
|
1330
|
+
primitives,
|
|
1331
|
+
sessionId: context?.sessionId,
|
|
1332
|
+
userId: context?.userId,
|
|
1333
|
+
}),
|
|
1334
|
+
});
|
|
1335
|
+
if (!res.ok) {
|
|
1336
|
+
const body = await res.text().catch(() => '');
|
|
1337
|
+
throw new Error(`[noorForge] harness route responded ${res.status} ${res.statusText}: ${body}`);
|
|
1338
|
+
}
|
|
1339
|
+
const data = (await res.json());
|
|
1340
|
+
return {
|
|
1341
|
+
success: data.success ?? false,
|
|
1342
|
+
toolName: data.toolName ?? `forged_${Date.now()}`,
|
|
1343
|
+
ethicallyApproved: data.ethicallyApproved ?? false,
|
|
1344
|
+
totalDurationMs: data.totalDurationMs ?? Date.now() - t0,
|
|
1345
|
+
results: data.results ?? [],
|
|
1346
|
+
dispatched: true,
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1349
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
1350
|
+
// HARNESS CONSULT WITH ARISTOTLE — opt-in 3-phase wrapping
|
|
1351
|
+
//
|
|
1352
|
+
// Wraps getBoundHarnessAndPrompt() with pre + post Aristotle phases.
|
|
1353
|
+
// Per the harness packet cognition_runtime_rule: "Aristotle/Taddabur are
|
|
1354
|
+
// primary cognition. Do not replace contemplation with a dashboard state."
|
|
1355
|
+
// Aristotle fires EVERY consult for owner-tier; client-tier requires
|
|
1356
|
+
// explicit `aristotleEnabled: true` in options (default: false).
|
|
1357
|
+
//
|
|
1358
|
+
// Mid-phase is optional — pass `plannedApproach` to activate it between
|
|
1359
|
+
// packet fetch and the LLM call. Most callers omit mid-phase.
|
|
1360
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
1361
|
+
/**
|
|
1362
|
+
* Harness consult with Aristotle 3-phase wrapping.
|
|
1363
|
+
*
|
|
1364
|
+
* Flow:
|
|
1365
|
+
* 1. aristotlePre() — Fitrah gate, wisdom, research, soul state
|
|
1366
|
+
* 2. getBoundHarnessAndPrompt() — packet + system prompt
|
|
1367
|
+
* 3. aristotleMid() — MetaCognitive + ethical check (if plannedApproach provided)
|
|
1368
|
+
* 4. [caller calls LLM with the returned prompt]
|
|
1369
|
+
* 5. Call aristotlePost() from the caller with the LLM response
|
|
1370
|
+
* (post-phase is caller-side because the SDK doesn't own the LLM call)
|
|
1371
|
+
*
|
|
1372
|
+
* Returns the BoundHarnessPromptResult augmented with the pre-phase result.
|
|
1373
|
+
* If Fitrah vetoes, throws — the caller must handle the veto (emit refusal).
|
|
1374
|
+
*
|
|
1375
|
+
* @param context HarnessBindingContext (same shape as getBoundHarnessAndPrompt).
|
|
1376
|
+
* @param plan Optional plan docs / files (same shape as getBoundHarnessAndPrompt).
|
|
1377
|
+
* @param options Aristotle control options.
|
|
1378
|
+
* @param options.aristotleEnabled Whether Aristotle fires. Default true for this method.
|
|
1379
|
+
* @param options.plannedApproach Optional planned approach string — activates mid-phase.
|
|
1380
|
+
* @param options.tier Tier hint for Aristotle ('owner'|'client'|etc.).
|
|
1381
|
+
*/
|
|
1382
|
+
async getBoundHarnessAndPromptWithAristotle(context, plan, options) {
|
|
1383
|
+
const enabled = options?.aristotleEnabled !== false; // default ON
|
|
1384
|
+
let preResult = null;
|
|
1385
|
+
let midResult = null;
|
|
1386
|
+
const message = context.message ?? context.intendedAction ?? 'harness consult';
|
|
1387
|
+
// ── Phase 1: PRE ────────────────────────────────────────────────────────
|
|
1388
|
+
if (enabled) {
|
|
1389
|
+
try {
|
|
1390
|
+
preResult = await this.aristotlePre(message, context.sessionId, {
|
|
1391
|
+
userId: context.userId,
|
|
1392
|
+
tier: options?.tier,
|
|
1393
|
+
});
|
|
1394
|
+
if (preResult.fitrahVetoed) {
|
|
1395
|
+
throw new Error(`[aristotlePre] Fitrah veto — alignment=${preResult.fitrahAlignment.toFixed(2)}. Caller must emit honest refusal.`);
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
catch (err) {
|
|
1399
|
+
// Re-throw Fitrah veto (named Error pattern). Log transport faults.
|
|
1400
|
+
if (err.message.includes('Fitrah veto'))
|
|
1401
|
+
throw err;
|
|
1402
|
+
console.error('[getBoundHarnessAndPromptWithAristotle] aristotlePre transport fault:', err.message);
|
|
1403
|
+
// Transport faults are LOUD but non-blocking for the consult itself.
|
|
1404
|
+
// The pre-phase result stays null so callers know Aristotle didn't fire.
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
// ── Phase 2: Harness packet + system prompt ─────────────────────────────
|
|
1408
|
+
const bound = await this.getBoundHarnessAndPrompt(context, plan);
|
|
1409
|
+
// ── Phase 3: MID (optional — only when plannedApproach is provided) ─────
|
|
1410
|
+
if (enabled && options?.plannedApproach) {
|
|
1411
|
+
try {
|
|
1412
|
+
midResult = await this.aristotleMid(message, context.sessionId, options.plannedApproach, {
|
|
1413
|
+
userId: context.userId,
|
|
1414
|
+
tier: options?.tier,
|
|
1415
|
+
});
|
|
1416
|
+
}
|
|
1417
|
+
catch (err) {
|
|
1418
|
+
console.error('[getBoundHarnessAndPromptWithAristotle] aristotleMid transport fault:', err.message);
|
|
1419
|
+
// Non-blocking — mid-phase unavailability doesn't stop the LLM call.
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
return { ...bound, aristotlePre: preResult, aristotleMid: midResult };
|
|
1423
|
+
}
|
|
1424
|
+
// ── Retry + exponential backoff helper ──────────────────────────────────
|
|
1425
|
+
// Hamza 2026-04-27: "YES ADD RETRY AND BACKOFF BUT FUCK UR CIRCUIT BREAKER
|
|
1426
|
+
// THAT NUST LEAVES HER BROKEN WE NEED SELF HEAL!!!" — every fetch retries
|
|
1427
|
+
// on transient network failures (real network errors only, not status
|
|
1428
|
+
// codes — caller decides what to do with non-2xx). No circuit breaker:
|
|
1429
|
+
// we always try. Backoff: 250ms, 500ms, 1000ms (3 attempts total).
|
|
1430
|
+
async fetchWithRetry(url, init, attempts = 3) {
|
|
1431
|
+
let lastErr;
|
|
1432
|
+
for (let i = 0; i < attempts; i++) {
|
|
1433
|
+
try {
|
|
1434
|
+
return await fetch(url, init);
|
|
1435
|
+
}
|
|
1436
|
+
catch (err) {
|
|
1437
|
+
lastErr = err;
|
|
1438
|
+
if (i < attempts - 1) {
|
|
1439
|
+
const delay = 250 * Math.pow(2, i);
|
|
1440
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
throw lastErr instanceof Error
|
|
1445
|
+
? lastErr
|
|
1446
|
+
: new Error(`fetch failed after ${attempts} attempts: ${String(lastErr)}`);
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
// Singleton convenience export
|
|
1450
|
+
export const harness = {
|
|
1451
|
+
getInstance: HTTPHarnessClient.getInstance.bind(HTTPHarnessClient),
|
|
1452
|
+
resetInstance: HTTPHarnessClient.resetInstance.bind(HTTPHarnessClient),
|
|
1453
|
+
};
|
|
1454
|
+
export function bindingContext(role, sessionId, stage, intendedAction, rationale) {
|
|
1455
|
+
return {
|
|
1456
|
+
role,
|
|
1457
|
+
sessionId,
|
|
1458
|
+
stage,
|
|
1459
|
+
intendedAction,
|
|
1460
|
+
rationale,
|
|
1461
|
+
correlationId: randomUUID(),
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1464
|
+
export async function getBoundHarnessAndPrompt(context, plan, client) {
|
|
1465
|
+
const sdk = client ?? HTTPHarnessClient.getInstance();
|
|
1466
|
+
return sdk.getBoundHarnessAndPrompt(context, plan);
|
|
1467
|
+
}
|
|
1468
|
+
export * from './runWithCognition.js';
|
|
1469
|
+
//# sourceMappingURL=index.js.map
|