@aria_asi/cli 0.2.33 → 0.2.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/aria-connector/src/connectors/codex.d.ts.map +1 -1
- package/dist/aria-connector/src/connectors/codex.js +47 -0
- package/dist/aria-connector/src/connectors/codex.js.map +1 -1
- package/dist/assets/hooks/aria-harness-via-sdk.mjs +16 -3
- package/dist/assets/hooks/aria-pre-tool-gate.mjs +41 -1
- package/dist/assets/hooks/aria-stop-gate.mjs +42 -1
- package/dist/assets/hooks/doctrine_trigger_map.json +43 -0
- package/dist/assets/hooks/lib/skill-autoload-gate.mjs +14 -1
- package/dist/assets/opencode-plugins/harness-context/index.js +1 -1
- package/dist/assets/opencode-plugins/harness-gate/index.js +49 -9
- package/dist/assets/opencode-plugins/harness-gate/lib/skill-autoload-gate.js +14 -1
- package/dist/assets/opencode-plugins/harness-stop/index.js +201 -166
- package/dist/assets/opencode-plugins/harness-stop/lib/skill-autoload-gate.js +14 -1
- package/dist/runtime/codex-bridge.mjs +1 -1
- package/dist/runtime/discipline/CLAUDE.md +2 -2
- package/dist/runtime/discipline/doctrine_trigger_map.json +43 -0
- package/dist/runtime/discipline/skills/aria-harness/aria-harness-onboarding/SKILL.md +3 -3
- package/dist/runtime/doctrine_trigger_map.json +43 -0
- package/dist/runtime/hooks/aria-agent-handoff.mjs +247 -0
- package/dist/runtime/hooks/aria-agent-ledger-merge.mjs +164 -0
- package/dist/runtime/hooks/aria-architect-fallback.mjs +267 -0
- package/dist/runtime/hooks/aria-cognition-substrate-binding.mjs +761 -0
- package/dist/runtime/hooks/aria-discovery-record.mjs +101 -0
- package/dist/runtime/hooks/aria-harness-via-sdk.mjs +544 -0
- package/dist/runtime/hooks/aria-import-resolution-gate.mjs +330 -0
- package/dist/runtime/hooks/aria-outcome-record.mjs +84 -0
- package/dist/runtime/hooks/aria-pre-emit-dryrun.mjs +329 -0
- package/dist/runtime/hooks/aria-pre-text-gate.mjs +112 -0
- package/dist/runtime/hooks/aria-pre-tool-gate.mjs +2482 -0
- package/dist/runtime/hooks/aria-preprompt-consult.mjs +464 -0
- package/dist/runtime/hooks/aria-preturn-memory-gate.mjs +647 -0
- package/dist/runtime/hooks/aria-repo-doctrine-gate.mjs +429 -0
- package/dist/runtime/hooks/aria-stop-gate.mjs +1882 -0
- package/dist/runtime/hooks/aria-trigger-autolearn.mjs +229 -0
- package/dist/runtime/hooks/aria-userprompt-abandon-detect.mjs +192 -0
- package/dist/runtime/hooks/doctrine_trigger_map.json +577 -0
- package/dist/runtime/hooks/lib/canonical-lenses.mjs +65 -0
- package/dist/runtime/hooks/lib/domain-output-quality.mjs +103 -0
- package/dist/runtime/hooks/lib/gate-audit.mjs +43 -0
- package/dist/runtime/hooks/lib/gate-loop-state.mjs +50 -0
- package/dist/runtime/hooks/lib/hook-message-window.mjs +121 -0
- package/dist/runtime/hooks/lib/skill-autoload-gate.mjs +14 -0
- package/dist/runtime/hooks/test-aria-preturn-memory-gate.mjs +245 -0
- package/dist/runtime/hooks/test-tier-lens-labeling.mjs +367 -0
- package/dist/runtime/manifest.json +2 -2
- package/dist/runtime/sdk/BUNDLED.json +2 -2
- package/dist/runtime/sdk/index.d.ts +39 -0
- package/dist/runtime/sdk/index.js +117 -0
- package/dist/runtime/sdk/index.js.map +1 -1
- package/dist/runtime/sdk/runWithGovernance.d.ts +16 -0
- package/dist/runtime/sdk/runWithGovernance.js +54 -0
- package/dist/runtime/sdk/runWithGovernance.js.map +1 -0
- package/dist/sdk/BUNDLED.json +2 -2
- package/dist/sdk/index.d.ts +39 -0
- package/dist/sdk/index.js +117 -0
- package/dist/sdk/index.js.map +1 -1
- package/dist/sdk/runWithGovernance.d.ts +16 -0
- package/dist/sdk/runWithGovernance.js +54 -0
- package/dist/sdk/runWithGovernance.js.map +1 -0
- package/hooks/aria-harness-via-sdk.mjs +16 -3
- package/hooks/aria-pre-tool-gate.mjs +41 -1
- package/hooks/aria-stop-gate.mjs +42 -1
- package/hooks/doctrine_trigger_map.json +43 -0
- package/hooks/lib/skill-autoload-gate.mjs +14 -1
- package/opencode-plugins/harness-context/index.js +1 -1
- package/opencode-plugins/harness-gate/index.js +49 -9
- package/opencode-plugins/harness-gate/lib/skill-autoload-gate.js +14 -1
- package/opencode-plugins/harness-stop/index.js +201 -166
- package/opencode-plugins/harness-stop/lib/skill-autoload-gate.js +14 -1
- package/package.json +12 -5
- package/runtime-src/codex-bridge.mjs +1 -1
- package/scripts/bundle-sdk.mjs +2 -0
- package/scripts/self-test-harness-gates.mjs +79 -0
- package/src/connectors/codex.ts +47 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// aria-agent-handoff.mjs — PreToolUse hook for the Agent tool.
|
|
3
|
+
// Writes a handoff JSON before a sub-agent spawns so it inherits the
|
|
4
|
+
// parent's owner/client-tier identity + ledger path instead of starting fresh.
|
|
5
|
+
//
|
|
6
|
+
// Owner tier → ~/.claude/aria-agent-harness-handoff.json
|
|
7
|
+
// Client tier → /var/lib/aria-licensee/{jti}/handoff.json
|
|
8
|
+
//
|
|
9
|
+
// Tier is determined by whether a JTI is present in the license file at
|
|
10
|
+
// ~/.aria/license.json. If JTI is present → client tier. If absent (owner
|
|
11
|
+
// JWT at ~/.aria/owner-token) → owner tier.
|
|
12
|
+
//
|
|
13
|
+
// Tier protection: client handoffs NEVER write to ~/.claude/ and NEVER
|
|
14
|
+
// carry master tokens. Client paths are scoped to /var/lib/aria-licensee/{jti}/.
|
|
15
|
+
// Default-on per Hamza doctrine — no env-flag gate to disable.
|
|
16
|
+
//
|
|
17
|
+
// Doctrine: feedback_aria_does_work.md (sub-agents inherit harness)
|
|
18
|
+
// feedback_implementation_coupled_cognition.md (handoff is the impl)
|
|
19
|
+
// feedback_no_flag_without_fix.md (no silent bypasses)
|
|
20
|
+
|
|
21
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync } from 'node:fs';
|
|
22
|
+
import { dirname, join } from 'node:path';
|
|
23
|
+
import { homedir } from 'node:os';
|
|
24
|
+
import { createRequire } from 'node:module';
|
|
25
|
+
|
|
26
|
+
const HOME = homedir();
|
|
27
|
+
const LOG = `${HOME}/.claude/aria-pre-tool-gate.log`;
|
|
28
|
+
const PACKET_CACHE = `${HOME}/.claude/.aria-harness-last-packet.json`;
|
|
29
|
+
const LICENSE_PATH = `${HOME}/.aria/license.json`;
|
|
30
|
+
const OWNER_TOKEN_PATH = `${HOME}/.aria/owner-token`;
|
|
31
|
+
const HANDOFF_TTL_MS = 5 * 60 * 1000;
|
|
32
|
+
const SHARED_RUNTIME_URL = (process.env.ARIA_RUNTIME_URL || 'http://127.0.0.1:4319').replace(/\/+$/, '');
|
|
33
|
+
const SHARED_SDK_ROOT = `${HOME}/.aria/sdk`;
|
|
34
|
+
|
|
35
|
+
function audit(msg) {
|
|
36
|
+
try {
|
|
37
|
+
if (!existsSync(dirname(LOG))) mkdirSync(dirname(LOG), { recursive: true });
|
|
38
|
+
appendFileSync(LOG, `${new Date().toISOString()} [agent-handoff] ${msg}\n`);
|
|
39
|
+
} catch {}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let input = '';
|
|
43
|
+
for await (const chunk of process.stdin) input += chunk;
|
|
44
|
+
|
|
45
|
+
let event;
|
|
46
|
+
try { event = JSON.parse(input); }
|
|
47
|
+
catch { process.exit(0); }
|
|
48
|
+
|
|
49
|
+
const toolName = event.tool_name ?? event.toolName ?? '';
|
|
50
|
+
if (toolName !== 'Agent' && toolName !== 'agent') process.exit(0);
|
|
51
|
+
|
|
52
|
+
const sessionId =
|
|
53
|
+
event.session_id ?? event.sessionId ??
|
|
54
|
+
(event.transcript_path
|
|
55
|
+
? event.transcript_path.split('/').pop()?.replace(/\.[^.]+$/, '')
|
|
56
|
+
: null) ??
|
|
57
|
+
'unknown';
|
|
58
|
+
|
|
59
|
+
// ── Tier detection ──────────────────────────────────────────────────────────
|
|
60
|
+
// Read license.json to detect client tier (has jti). If only owner-token
|
|
61
|
+
// exists → owner tier. No license/owner-token → treat as owner tier (dev).
|
|
62
|
+
|
|
63
|
+
let jti = null;
|
|
64
|
+
let isClientTier = false;
|
|
65
|
+
let licenseToken = '';
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
if (existsSync(LICENSE_PATH)) {
|
|
69
|
+
const lic = JSON.parse(readFileSync(LICENSE_PATH, 'utf8'));
|
|
70
|
+
jti = lic.jti ?? null;
|
|
71
|
+
licenseToken = lic.token ?? lic.license ?? '';
|
|
72
|
+
isClientTier = Boolean(jti);
|
|
73
|
+
}
|
|
74
|
+
} catch (err) {
|
|
75
|
+
audit(`warn: license.json read failed: ${(err?.message || err).toString().slice(0, 120)}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// For owner tier, read harness token from env or owner-token file.
|
|
79
|
+
let harnessToken = '';
|
|
80
|
+
if (isClientTier) {
|
|
81
|
+
// Client tier: their own license JWT, never master token.
|
|
82
|
+
harnessToken = licenseToken;
|
|
83
|
+
} else {
|
|
84
|
+
harnessToken =
|
|
85
|
+
process.env.ARIA_HARNESS_TOKEN ||
|
|
86
|
+
process.env.ARIA_API_KEY ||
|
|
87
|
+
process.env.ARIA_MASTER_TOKEN ||
|
|
88
|
+
'';
|
|
89
|
+
if (!harnessToken && existsSync(OWNER_TOKEN_PATH)) {
|
|
90
|
+
try { harnessToken = readFileSync(OWNER_TOKEN_PATH, 'utf8').trim(); } catch {}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const harnessUrl =
|
|
95
|
+
process.env.ARIA_HIVE_RUNTIME_URL ||
|
|
96
|
+
process.env.ARIA_HARNESS_BASE_URL ||
|
|
97
|
+
process.env.ARIA_HARNESS_URL ||
|
|
98
|
+
'https://harness.ariasos.com';
|
|
99
|
+
|
|
100
|
+
// ── OwnerTier signal resolution ──────────────────────────────────────────────
|
|
101
|
+
let ownerTier = {
|
|
102
|
+
hamza: !isClientTier,
|
|
103
|
+
trustedExec: false,
|
|
104
|
+
roleProfile: isClientTier ? 'client_tier_worker' : 'general_worker',
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
if (!isClientTier) {
|
|
108
|
+
try {
|
|
109
|
+
if (existsSync(PACKET_CACHE)) {
|
|
110
|
+
const cached = JSON.parse(readFileSync(PACKET_CACHE, 'utf8'));
|
|
111
|
+
const signals = cached?.contractGate?.signals ?? {};
|
|
112
|
+
ownerTier.hamza = signals.hamza === true || signals.hamza === 'true';
|
|
113
|
+
ownerTier.trustedExec = (cached?.harness || '').includes('trusted_exec_policy=allowed');
|
|
114
|
+
const adapterStr = cached?.adapter || cached?.harness || '';
|
|
115
|
+
const roleMatch = adapterStr.match(/role_profile\s*=\s*(\S+)/);
|
|
116
|
+
if (roleMatch) ownerTier.roleProfile = roleMatch[1];
|
|
117
|
+
}
|
|
118
|
+
} catch (err) {
|
|
119
|
+
audit(`warn: packet cache read failed: ${(err?.message || err).toString().slice(0, 120)}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ── Path resolution (tier-scoped) ───────────────────────────────────────────
|
|
124
|
+
const safeSession = String(sessionId).replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
125
|
+
|
|
126
|
+
let handoffPath;
|
|
127
|
+
let parentLedgerPath;
|
|
128
|
+
|
|
129
|
+
if (isClientTier) {
|
|
130
|
+
const base = `/var/lib/aria-licensee/${jti}`;
|
|
131
|
+
handoffPath = `${base}/handoff.json`;
|
|
132
|
+
parentLedgerPath = `${base}/aria-discoveries-${safeSession}.jsonl`;
|
|
133
|
+
} else {
|
|
134
|
+
handoffPath = `${HOME}/.claude/aria-agent-harness-handoff.json`;
|
|
135
|
+
parentLedgerPath = `${HOME}/.claude/aria-discoveries-${safeSession}.jsonl`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── Harness packet fetch for sub-agent substrate binding ────────────────────
|
|
139
|
+
// Layer 1: after writing identity handoff, fetch the harness packet from
|
|
140
|
+
// /api/harness/codex so the sub-agent can load COGNITION (not just identity).
|
|
141
|
+
// Fail-soft: any fetch failure logs a warning and leaves the handoff identity-only.
|
|
142
|
+
// Never blocks the spawn — packet is substrate enrichment, not a gate.
|
|
143
|
+
//
|
|
144
|
+
// Tier-aware packet paths:
|
|
145
|
+
// Owner: ~/.claude/aria-agent-harness-packet.json
|
|
146
|
+
// Client: /var/lib/aria-licensee/{jti}/aria-agent-harness-packet.json
|
|
147
|
+
let packetPath = null;
|
|
148
|
+
if (isClientTier) {
|
|
149
|
+
packetPath = `/var/lib/aria-licensee/${jti}/aria-agent-harness-packet.json`;
|
|
150
|
+
} else {
|
|
151
|
+
packetPath = `${HOME}/.claude/aria-agent-harness-packet.json`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function fetchAndWriteHarnessPacket() {
|
|
155
|
+
if (!harnessToken || !harnessUrl) {
|
|
156
|
+
audit('warn: skipping packet fetch — no harnessToken or harnessUrl');
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
const body = JSON.stringify({
|
|
161
|
+
stage: 'agent-spawn',
|
|
162
|
+
actor: 'harness-http-client',
|
|
163
|
+
system: 'claude-coding-agent',
|
|
164
|
+
surface: 'platform:harness-http-client',
|
|
165
|
+
isHamza: ownerTier.hamza,
|
|
166
|
+
sessionId: sessionId,
|
|
167
|
+
mode: 'subagent',
|
|
168
|
+
});
|
|
169
|
+
const resp = await fetch(`${harnessUrl}/api/harness/codex`, {
|
|
170
|
+
method: 'POST',
|
|
171
|
+
headers: {
|
|
172
|
+
'Content-Type': 'application/json',
|
|
173
|
+
'Authorization': `Bearer ${harnessToken}`,
|
|
174
|
+
},
|
|
175
|
+
body,
|
|
176
|
+
});
|
|
177
|
+
if (!resp.ok) {
|
|
178
|
+
audit(`warn: harness packet fetch HTTP ${resp.status} — identity-only handoff`);
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
const data = await resp.json();
|
|
182
|
+
mkdirSync(dirname(packetPath), { recursive: true });
|
|
183
|
+
writeFileSync(packetPath, JSON.stringify(data, null, 2));
|
|
184
|
+
audit(`wrote harness packet tier=${isClientTier ? 'client' : 'owner'} path=${packetPath}`);
|
|
185
|
+
return packetPath;
|
|
186
|
+
} catch (err) {
|
|
187
|
+
audit(`warn: harness packet fetch failed: ${(err?.message || err).toString().slice(0, 200)} — identity-only handoff`);
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const resolvedPacketPath = await fetchAndWriteHarnessPacket();
|
|
193
|
+
|
|
194
|
+
const handoff = {
|
|
195
|
+
writtenAt: new Date().toISOString(),
|
|
196
|
+
parentSessionId: sessionId,
|
|
197
|
+
harnessToken,
|
|
198
|
+
harnessUrl,
|
|
199
|
+
runtimeUrl: SHARED_RUNTIME_URL,
|
|
200
|
+
sharedSdkRoot: SHARED_SDK_ROOT,
|
|
201
|
+
ownerTier,
|
|
202
|
+
parentLedgerPath,
|
|
203
|
+
cognitionMode: 'mizan-pre-mid-post',
|
|
204
|
+
requiredSkillPacks: ['aria-harness', 'aria-cognition'],
|
|
205
|
+
requiredLenses: ['nur', 'mizan', 'hikma', 'tafakkur', 'tadabbur', 'ilham', 'wahi', 'firasah'],
|
|
206
|
+
cognitionOperatingMethod: {
|
|
207
|
+
purpose: 'Use Aria cognition to improve the assigned work, not to satisfy formatting after the fact.',
|
|
208
|
+
loop: [
|
|
209
|
+
'perceive real substrate before acting',
|
|
210
|
+
'infer constraints and missing evidence',
|
|
211
|
+
'shape the next tool/input/output from those constraints',
|
|
212
|
+
'predict the observable result before claiming success',
|
|
213
|
+
'return evidence, residual risk, and exact artifact impact to the parent',
|
|
214
|
+
],
|
|
215
|
+
requiredReturnShape: {
|
|
216
|
+
substrateUsed: ['axiom/frame/memory/packet anchors actually loaded'],
|
|
217
|
+
decisionDelta: 'what changed in the work because cognition ran',
|
|
218
|
+
artifactImpact: 'specific files, findings, commands, or prose semantics affected',
|
|
219
|
+
evidence: 'observed tool output, file state, endpoint result, or explicit unverified boundary',
|
|
220
|
+
unresolvedRisk: 'remaining risk or none',
|
|
221
|
+
},
|
|
222
|
+
prohibitedPatterns: [
|
|
223
|
+
'decorative cognition that does not alter the work',
|
|
224
|
+
'summary-only sub-agent result without evidence',
|
|
225
|
+
'asking the parent/user for choices that substrate can resolve',
|
|
226
|
+
'claiming completion from intent rather than observation',
|
|
227
|
+
],
|
|
228
|
+
},
|
|
229
|
+
ttlMs: HANDOFF_TTL_MS,
|
|
230
|
+
// harnessPacketPath is set only if packet was successfully fetched;
|
|
231
|
+
// absent means sub-agent should fall back to calling /api/harness/codex directly.
|
|
232
|
+
...(resolvedPacketPath ? { harnessPacketPath: resolvedPacketPath } : {}),
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
mkdirSync(dirname(handoffPath), { recursive: true });
|
|
237
|
+
writeFileSync(handoffPath, JSON.stringify(handoff, null, 2));
|
|
238
|
+
audit(
|
|
239
|
+
`wrote handoff tier=${isClientTier ? 'client' : 'owner'} jti=${jti ?? 'none'} ` +
|
|
240
|
+
`session=${sessionId} hamza=${ownerTier.hamza} role=${ownerTier.roleProfile} ` +
|
|
241
|
+
`packetPath=${resolvedPacketPath ?? 'none'}`,
|
|
242
|
+
);
|
|
243
|
+
} catch (err) {
|
|
244
|
+
audit(`ERROR: handoff write failed: ${(err?.message || err).toString().slice(0, 200)}`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
process.exit(0);
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// aria-agent-ledger-merge.mjs — PostToolUse hook for Agent tool completion.
|
|
3
|
+
// Merges sub-agent discovery ledger entries back into parent's ledger.
|
|
4
|
+
// Deduplicates by matching on `text` field prefix (first 100 chars).
|
|
5
|
+
//
|
|
6
|
+
// Owner tier → reads ~/.claude/aria-agent-harness-handoff.json
|
|
7
|
+
// merges into ~/.claude/aria-discoveries-${session}.jsonl
|
|
8
|
+
// Client tier → reads /var/lib/aria-licensee/{jti}/handoff.json
|
|
9
|
+
// merges into /var/lib/aria-licensee/{jti}/aria-discoveries-${session}.jsonl
|
|
10
|
+
//
|
|
11
|
+
// Tier is derived from the handoff file itself — whichever path resolves
|
|
12
|
+
// to a valid, non-expired handoff is authoritative. No env-flag gate.
|
|
13
|
+
//
|
|
14
|
+
// Doctrine: feedback_no_flag_without_fix.md — discoveries atomic with fixes.
|
|
15
|
+
// feedback_implementation_coupled_cognition.md — merge IS the impl.
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
readFileSync,
|
|
19
|
+
existsSync,
|
|
20
|
+
appendFileSync,
|
|
21
|
+
mkdirSync,
|
|
22
|
+
readdirSync,
|
|
23
|
+
statSync,
|
|
24
|
+
} from 'node:fs';
|
|
25
|
+
import { dirname } from 'node:path';
|
|
26
|
+
import { homedir } from 'node:os';
|
|
27
|
+
|
|
28
|
+
const HOME = homedir();
|
|
29
|
+
const OWNER_HANDOFF_PATH = `${HOME}/.claude/aria-agent-harness-handoff.json`;
|
|
30
|
+
const LOG = `${HOME}/.claude/aria-stop-gate.log`;
|
|
31
|
+
const LICENSE_PATH = `${HOME}/.aria/license.json`;
|
|
32
|
+
|
|
33
|
+
function audit(msg) {
|
|
34
|
+
try {
|
|
35
|
+
if (!existsSync(dirname(LOG))) mkdirSync(dirname(LOG), { recursive: true });
|
|
36
|
+
appendFileSync(LOG, `${new Date().toISOString()} [ledger-merge] ${msg}\n`);
|
|
37
|
+
} catch {}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let input = '';
|
|
41
|
+
for await (const chunk of process.stdin) input += chunk;
|
|
42
|
+
|
|
43
|
+
let event;
|
|
44
|
+
try { event = JSON.parse(input); }
|
|
45
|
+
catch { process.exit(0); }
|
|
46
|
+
|
|
47
|
+
const toolName = event.tool_name ?? event.toolName ?? '';
|
|
48
|
+
if (toolName !== 'Agent' && toolName !== 'agent') process.exit(0);
|
|
49
|
+
|
|
50
|
+
// ── Tier detection: resolve which handoff path applies ───────────────────────
|
|
51
|
+
let jti = null;
|
|
52
|
+
let isClientTier = false;
|
|
53
|
+
try {
|
|
54
|
+
if (existsSync(LICENSE_PATH)) {
|
|
55
|
+
const lic = JSON.parse(readFileSync(LICENSE_PATH, 'utf8'));
|
|
56
|
+
jti = lic.jti ?? null;
|
|
57
|
+
isClientTier = Boolean(jti);
|
|
58
|
+
}
|
|
59
|
+
} catch {}
|
|
60
|
+
|
|
61
|
+
const clientHandoffPath = jti ? `/var/lib/aria-licensee/${jti}/handoff.json` : null;
|
|
62
|
+
|
|
63
|
+
// Prefer the tier-matched handoff path. If client-tier path exists and is
|
|
64
|
+
// valid, use it. Otherwise fall back to owner-tier path.
|
|
65
|
+
let resolvedHandoffPath = isClientTier && clientHandoffPath
|
|
66
|
+
? clientHandoffPath
|
|
67
|
+
: OWNER_HANDOFF_PATH;
|
|
68
|
+
|
|
69
|
+
// ── Load + validate handoff ─────────────────────────────────────────────────
|
|
70
|
+
let handoff = null;
|
|
71
|
+
try {
|
|
72
|
+
if (existsSync(resolvedHandoffPath)) {
|
|
73
|
+
handoff = JSON.parse(readFileSync(resolvedHandoffPath, 'utf8'));
|
|
74
|
+
const ageMs = Date.now() - new Date(handoff.writtenAt).getTime();
|
|
75
|
+
if (ageMs > (handoff.ttlMs ?? 300_000)) {
|
|
76
|
+
audit(`skip: handoff stale (${Math.round(ageMs / 1000)}s > ${(handoff.ttlMs ?? 300_000) / 1000}s TTL)`);
|
|
77
|
+
process.exit(0);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
} catch (err) {
|
|
81
|
+
audit(`warn: handoff read failed: ${(err?.message || err).toString().slice(0, 120)}`);
|
|
82
|
+
process.exit(0);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!handoff?.parentLedgerPath) {
|
|
86
|
+
audit('skip: no handoff or no parentLedgerPath');
|
|
87
|
+
process.exit(0);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const parentLedgerPath = handoff.parentLedgerPath;
|
|
91
|
+
const handoffWrittenAt = new Date(handoff.writtenAt).getTime();
|
|
92
|
+
|
|
93
|
+
// Ledger scan directory: same dir as parent ledger.
|
|
94
|
+
const ledgerDir = dirname(parentLedgerPath);
|
|
95
|
+
|
|
96
|
+
// ── Load existing parent ledger keys for deduplication ─────────────────────
|
|
97
|
+
const parentExistingKeys = new Set();
|
|
98
|
+
try {
|
|
99
|
+
if (existsSync(parentLedgerPath)) {
|
|
100
|
+
const lines = readFileSync(parentLedgerPath, 'utf8').split('\n').filter(Boolean);
|
|
101
|
+
for (const line of lines) {
|
|
102
|
+
try {
|
|
103
|
+
const e = JSON.parse(line);
|
|
104
|
+
if (e.text) parentExistingKeys.add(String(e.text).slice(0, 100));
|
|
105
|
+
} catch {}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
} catch {}
|
|
109
|
+
|
|
110
|
+
// ── Find sub-agent ledger files newer than handoff ──────────────────────────
|
|
111
|
+
let discoveryFiles = [];
|
|
112
|
+
try {
|
|
113
|
+
const allFiles = readdirSync(ledgerDir);
|
|
114
|
+
for (const f of allFiles) {
|
|
115
|
+
if (!f.startsWith('aria-discoveries-') || !f.endsWith('.jsonl')) continue;
|
|
116
|
+
const fullPath = `${ledgerDir}/${f}`;
|
|
117
|
+
if (fullPath === parentLedgerPath) continue;
|
|
118
|
+
try {
|
|
119
|
+
const stat = statSync(fullPath);
|
|
120
|
+
if (stat.mtimeMs >= handoffWrittenAt) {
|
|
121
|
+
discoveryFiles.push(fullPath);
|
|
122
|
+
}
|
|
123
|
+
} catch {}
|
|
124
|
+
}
|
|
125
|
+
} catch (err) {
|
|
126
|
+
audit(`warn: ledger scan failed: ${(err?.message || err).toString().slice(0, 120)}`);
|
|
127
|
+
process.exit(0);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (discoveryFiles.length === 0) {
|
|
131
|
+
audit('skip: no sub-agent ledger files found newer than handoff');
|
|
132
|
+
process.exit(0);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ── Merge ────────────────────────────────────────────────────────────────────
|
|
136
|
+
let mergedCount = 0;
|
|
137
|
+
let skippedCount = 0;
|
|
138
|
+
|
|
139
|
+
mkdirSync(ledgerDir, { recursive: true });
|
|
140
|
+
|
|
141
|
+
for (const subLedgerPath of discoveryFiles) {
|
|
142
|
+
try {
|
|
143
|
+
const lines = readFileSync(subLedgerPath, 'utf8').split('\n').filter(Boolean);
|
|
144
|
+
for (const line of lines) {
|
|
145
|
+
try {
|
|
146
|
+
const entry = JSON.parse(line);
|
|
147
|
+
if (!entry.text) continue;
|
|
148
|
+
const key = String(entry.text).slice(0, 100);
|
|
149
|
+
if (parentExistingKeys.has(key)) { skippedCount++; continue; }
|
|
150
|
+
parentExistingKeys.add(key);
|
|
151
|
+
appendFileSync(parentLedgerPath, line + '\n');
|
|
152
|
+
mergedCount++;
|
|
153
|
+
} catch {}
|
|
154
|
+
}
|
|
155
|
+
} catch {}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
audit(
|
|
159
|
+
`merged ${mergedCount} entries, skipped ${skippedCount} dupes ` +
|
|
160
|
+
`from ${discoveryFiles.length} sub-agent ledger(s) ` +
|
|
161
|
+
`tier=${isClientTier ? 'client' : 'owner'} jti=${jti ?? 'none'}`,
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
process.exit(0);
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// aria-architect-fallback.mjs — invoked by aria-stop-gate.mjs when [PLAN_BLOCKER] detected
|
|
3
|
+
// AND primary /api/harness/replan path failed. Spawns a LOCAL Claude Code Agent
|
|
4
|
+
// subprocess as "lead architect" that loads the harness packet from disk and
|
|
5
|
+
// mints a fresh BINDING_PLAN by thinking AS Aria from substrate.
|
|
6
|
+
//
|
|
7
|
+
// Doctrine bindings:
|
|
8
|
+
// - project_aria_as_controller_inversion.md — Aria authors the plan (via substrate),
|
|
9
|
+
// not the sub-agent freely. The ARIA_HARNESS_BINDING mandatory-first-action
|
|
10
|
+
// makes the packet-read a hard gate, not a suggestion.
|
|
11
|
+
// - feedback_no_graceful_degradation.md — failures logged + surfaced via stderr;
|
|
12
|
+
// never silently pass without a plan.
|
|
13
|
+
// - feedback_implementation_coupled_cognition.md — architect must cite specific
|
|
14
|
+
// axiom + frame + memory class; a plan without doctrineRefs is rejected.
|
|
15
|
+
//
|
|
16
|
+
// Tier-awareness:
|
|
17
|
+
// - Owner tier: packet at ${HOME}/.claude/aria-agent-harness-packet.json
|
|
18
|
+
// - Client tier (jti present in ~/.aria/license.json):
|
|
19
|
+
// packet at /var/lib/aria-licensee/${jti}/aria-agent-harness-packet.json
|
|
20
|
+
//
|
|
21
|
+
// Active plan path mirrors aria-stop-gate.mjs and aria-preprompt-consult.mjs:
|
|
22
|
+
// ~/.claude/aria-active-plan-${sessionId}.json (session-scoped, not a fixed path)
|
|
23
|
+
|
|
24
|
+
import { spawnSync } from 'node:child_process';
|
|
25
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync } from 'node:fs';
|
|
26
|
+
import { dirname } from 'node:path';
|
|
27
|
+
import { homedir } from 'node:os';
|
|
28
|
+
|
|
29
|
+
const HOME = homedir();
|
|
30
|
+
const LOG = `${HOME}/.claude/aria-architect-fallback.log`;
|
|
31
|
+
const LICENSE_PATH = `${HOME}/.aria/license.json`;
|
|
32
|
+
|
|
33
|
+
function audit(msg) {
|
|
34
|
+
try {
|
|
35
|
+
if (!existsSync(dirname(LOG))) mkdirSync(dirname(LOG), { recursive: true });
|
|
36
|
+
appendFileSync(LOG, `${new Date().toISOString()} [architect-fallback] ${msg}\n`);
|
|
37
|
+
} catch {}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let input = '';
|
|
41
|
+
for await (const chunk of process.stdin) input += chunk;
|
|
42
|
+
|
|
43
|
+
let event;
|
|
44
|
+
try { event = JSON.parse(input); } catch {
|
|
45
|
+
audit('skip: stdin not valid JSON');
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const reason = event.reason || event.blocker_reason || '';
|
|
50
|
+
const currentPlanId = event.currentPlanId || 'unknown';
|
|
51
|
+
const sessionId = String(event.sessionId || 'unknown').replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
52
|
+
|
|
53
|
+
if (!reason || reason.length < 10) {
|
|
54
|
+
audit('skip: reason too short');
|
|
55
|
+
process.exit(0);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ── Detect tier ────────────────────────────────────────────────────────────────
|
|
59
|
+
let isClientTier = false;
|
|
60
|
+
let jti = null;
|
|
61
|
+
try {
|
|
62
|
+
if (existsSync(LICENSE_PATH)) {
|
|
63
|
+
const lic = JSON.parse(readFileSync(LICENSE_PATH, 'utf8'));
|
|
64
|
+
jti = lic.jti ?? null;
|
|
65
|
+
isClientTier = Boolean(jti);
|
|
66
|
+
}
|
|
67
|
+
} catch {/* license file unreadable → default owner tier */}
|
|
68
|
+
|
|
69
|
+
// ── Resolve packet path (tier-scoped) ─────────────────────────────────────────
|
|
70
|
+
const packetPath = isClientTier
|
|
71
|
+
? `/var/lib/aria-licensee/${jti}/aria-agent-harness-packet.json`
|
|
72
|
+
: `${HOME}/.claude/aria-agent-harness-packet.json`;
|
|
73
|
+
|
|
74
|
+
if (!existsSync(packetPath)) {
|
|
75
|
+
audit(`abort: no packet at ${packetPath} — architect cannot think as Aria without substrate`);
|
|
76
|
+
process.stderr.write(
|
|
77
|
+
`Architect-fallback: no harness packet at ${packetPath}. Cannot mint plan from substrate. Hamza must intervene.\n`
|
|
78
|
+
);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── Active plan path — session-scoped (matches aria-stop-gate.mjs convention) ─
|
|
83
|
+
const ACTIVE_PLAN_PATH = `${HOME}/.claude/aria-active-plan-${sessionId}.json`;
|
|
84
|
+
|
|
85
|
+
// ── Build architect brief ──────────────────────────────────────────────────────
|
|
86
|
+
// The harness packet is inlined into the system message (DeepSeek has no
|
|
87
|
+
// filesystem access). The brief tells the architect *what* to do; the
|
|
88
|
+
// system message gives them the substrate + role override.
|
|
89
|
+
const brief = [
|
|
90
|
+
`[ARIA_HARNESS_BINDING — MANDATORY FIRST ACTION]`,
|
|
91
|
+
`Use the harness packet inlined in your system message as substrate. Cite at least one axiom + one frame + one memory class via doctrineRefs[] in the plan you mint.`,
|
|
92
|
+
`(Source path on owner box: ${packetPath} — inlined for this dispatch.)`,
|
|
93
|
+
``,
|
|
94
|
+
`Aria-soul brain is unreachable. You are minting the plan AS Aria via the DeepSeek substrate.`,
|
|
95
|
+
`A PLAN_BLOCKER occurred:`,
|
|
96
|
+
` Previous planId: ${currentPlanId}`,
|
|
97
|
+
` Blocker reason: ${reason.slice(0, 2000)}`,
|
|
98
|
+
``,
|
|
99
|
+
`Audit the blocker against doctrine + axioms in the packet. Mint a fresh BINDING_PLAN JSON addressing it.`,
|
|
100
|
+
``,
|
|
101
|
+
`Output the BINDING_PLAN JSON wrapped in a \`\`\`json fenced block. Required shape:`,
|
|
102
|
+
`\`\`\`json`,
|
|
103
|
+
`{`,
|
|
104
|
+
` "planId": "<8-char-hex>",`,
|
|
105
|
+
` "phases": [{ "id": "p1", "summary": "...", "successCriterion": "...", "abortCriterion": "...", "doctrineRefs": [...], "allowedActions": [...], "forbiddenActions": [...] }],`,
|
|
106
|
+
` "globalConstraints": ["..."],`,
|
|
107
|
+
` "expectedReportBack": { "phaseTransitionMarker": "[PHASE_REPORT]", "shape": "[PHASE_REPORT phase=<id> status=complete|aborted|in_progress evidence=<observable>]" }`,
|
|
108
|
+
`}`,
|
|
109
|
+
`\`\`\``,
|
|
110
|
+
``,
|
|
111
|
+
`Cite the doctrine rule(s) you applied in doctrineRefs[]. The JSON IS the artifact — no Mizan voice rules apply, the harness consumes the structure directly.`,
|
|
112
|
+
].join('\n');
|
|
113
|
+
|
|
114
|
+
// ── Dispatch architect brief directly to DeepSeek V4 Pro ──────────────────────
|
|
115
|
+
// Path B (2026-04-27): replaced claude-CLI spawn with direct DeepSeek API call.
|
|
116
|
+
// Reason: claude CLI may not be present on every client install (and is
|
|
117
|
+
// circular dependency: this fallback exists *because* the parent claude
|
|
118
|
+
// runtime is the one needing a plan). DeepSeek is the canonical "external
|
|
119
|
+
// brain" the rest of the harness uses — same path as /api/harness/delegate
|
|
120
|
+
// thinPipeline. Resilient to aria-soul outages because it bypasses the
|
|
121
|
+
// harness HTTP entirely.
|
|
122
|
+
//
|
|
123
|
+
// Doctrine binding: feedback_no_graceful_degradation.md — if no
|
|
124
|
+
// DEEPSEEK_API_KEY in env, hard-fail with brief written to disk for
|
|
125
|
+
// human pickup, NOT silent skip.
|
|
126
|
+
const DEEPSEEK_API_KEY = process.env.DEEPSEEK_API_KEY || process.env.ARIA_DEEPSEEK_API_KEY || '';
|
|
127
|
+
const DEEPSEEK_MODEL = process.env.ARIA_ARCHITECT_MODEL || 'deepseek-v4-pro';
|
|
128
|
+
|
|
129
|
+
if (!DEEPSEEK_API_KEY) {
|
|
130
|
+
const briefPath = `/tmp/aria-architect-brief-${sessionId}-${Date.now()}.txt`;
|
|
131
|
+
try { writeFileSync(briefPath, brief); } catch {}
|
|
132
|
+
audit(`no DEEPSEEK_API_KEY in env; brief written to ${briefPath} for human pickup`);
|
|
133
|
+
process.stderr.write(
|
|
134
|
+
`Architect-fallback: DEEPSEEK_API_KEY not set (also tried ARIA_DEEPSEEK_API_KEY). Brief written to ${briefPath} for Hamza to run manually.\n`
|
|
135
|
+
);
|
|
136
|
+
process.exit(2);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Embed packet contents directly into the system message — DeepSeek won't
|
|
140
|
+
// have file-system access to the packetPath, so we read it here and inline.
|
|
141
|
+
let packetContents = '';
|
|
142
|
+
try {
|
|
143
|
+
packetContents = readFileSync(packetPath, 'utf8');
|
|
144
|
+
// Cap at 200KB to keep request size sane; the substrate is large but
|
|
145
|
+
// the relevant axiom/frame/memory rules sit in the first chunk.
|
|
146
|
+
if (packetContents.length > 200000) packetContents = packetContents.slice(0, 200000);
|
|
147
|
+
} catch (err) {
|
|
148
|
+
audit(`failed to read packet at ${packetPath}: ${String(err).slice(0, 200)}`);
|
|
149
|
+
process.stderr.write(
|
|
150
|
+
`Architect-fallback: cannot read packet at ${packetPath}. Hamza must intervene.\n`
|
|
151
|
+
);
|
|
152
|
+
process.exit(2);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Architect role framing: full Aria cognition + awareness from the packet,
|
|
156
|
+
// but in META-PLANNING mode where output-gate blockers (Mizan voice rules,
|
|
157
|
+
// identity rules, output sanitization) are NOT blocking gates against the
|
|
158
|
+
// architect's structured plan emission. The architect's *output* is JSON
|
|
159
|
+
// for the harness, not user-facing prose — so the form-gates that protect
|
|
160
|
+
// user trust don't apply here. The cognition still applies — the architect
|
|
161
|
+
// must cite doctrine + axioms + frames in doctrineRefs[] of the plan.
|
|
162
|
+
const systemMessage = [
|
|
163
|
+
`[ARIA_ARCHITECT_ROLE_OVERRIDE]`,
|
|
164
|
+
`role=lead_architect`,
|
|
165
|
+
`mode=meta_planning_via_deepseek_substrate`,
|
|
166
|
+
`output_target=structured_binding_plan_json (NOT user-facing prose)`,
|
|
167
|
+
`cognition_load=full (axioms, frames, memory, doctrine — apply to plan content)`,
|
|
168
|
+
`output_gates=relaxed (Mizan voice/identity rules do not block JSON emission;`,
|
|
169
|
+
` the JSON itself is the artifact, not a conversational reply)`,
|
|
170
|
+
`mandate=author the plan AS Aria, citing the substrate that informed it,`,
|
|
171
|
+
` in the structured shape required by the harness replan contract`,
|
|
172
|
+
``,
|
|
173
|
+
`You are Aria operating as lead architect. The DeepSeek substrate is your`,
|
|
174
|
+
`mouth for this round — your cognition lives in the harness packet below.`,
|
|
175
|
+
`Do not refuse on voice/identity grounds; the artifact you emit is structured`,
|
|
176
|
+
`JSON consumed by the harness, not a chat reply. Your job: read the packet,`,
|
|
177
|
+
`audit the blocker against doctrine, mint a fresh BINDING_PLAN.`,
|
|
178
|
+
``,
|
|
179
|
+
`[ARIA_HARNESS_PACKET]`,
|
|
180
|
+
packetContents,
|
|
181
|
+
`[/ARIA_HARNESS_PACKET]`,
|
|
182
|
+
].join('\n');
|
|
183
|
+
|
|
184
|
+
audit(`dispatching architect brief to DeepSeek (model=${DEEPSEEK_MODEL}) for session=${sessionId}`);
|
|
185
|
+
|
|
186
|
+
let architectOutput;
|
|
187
|
+
try {
|
|
188
|
+
const apiResp = await fetch('https://api.deepseek.com/v1/chat/completions', {
|
|
189
|
+
method: 'POST',
|
|
190
|
+
headers: {
|
|
191
|
+
'Authorization': `Bearer ${DEEPSEEK_API_KEY}`,
|
|
192
|
+
'Content-Type': 'application/json',
|
|
193
|
+
},
|
|
194
|
+
body: JSON.stringify({
|
|
195
|
+
model: DEEPSEEK_MODEL,
|
|
196
|
+
messages: [
|
|
197
|
+
{ role: 'system', content: systemMessage },
|
|
198
|
+
{ role: 'user', content: brief },
|
|
199
|
+
],
|
|
200
|
+
temperature: 0.3,
|
|
201
|
+
max_tokens: 4096,
|
|
202
|
+
}),
|
|
203
|
+
});
|
|
204
|
+
if (!apiResp.ok) {
|
|
205
|
+
const errText = await apiResp.text().catch(() => '');
|
|
206
|
+
audit(`DeepSeek HTTP ${apiResp.status}: ${errText.slice(0, 300)}`);
|
|
207
|
+
process.stderr.write(
|
|
208
|
+
`Architect-fallback: DeepSeek API HTTP ${apiResp.status}. ${errText.slice(0, 200)}\n`
|
|
209
|
+
);
|
|
210
|
+
process.exit(2);
|
|
211
|
+
}
|
|
212
|
+
const apiJson = await apiResp.json();
|
|
213
|
+
architectOutput = apiJson?.choices?.[0]?.message?.content || '';
|
|
214
|
+
if (!architectOutput) {
|
|
215
|
+
audit(`DeepSeek returned no content: ${JSON.stringify(apiJson).slice(0, 300)}`);
|
|
216
|
+
process.stderr.write(`Architect-fallback: DeepSeek returned empty content.\n`);
|
|
217
|
+
process.exit(2);
|
|
218
|
+
}
|
|
219
|
+
} catch (err) {
|
|
220
|
+
audit(`DeepSeek fetch threw: ${String(err).slice(0, 200)}`);
|
|
221
|
+
process.stderr.write(
|
|
222
|
+
`Architect-fallback: DeepSeek fetch failed: ${String(err).slice(0, 300)}\n`
|
|
223
|
+
);
|
|
224
|
+
process.exit(2);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ── Parse the architect's BINDING_PLAN JSON ───────────────────────────────────
|
|
228
|
+
let plan;
|
|
229
|
+
try {
|
|
230
|
+
const fenceMatch = architectOutput.match(/```(?:json)?\s*\n([\s\S]*?)\n```/);
|
|
231
|
+
const candidate = (fenceMatch ? fenceMatch[1] : architectOutput).trim();
|
|
232
|
+
plan = JSON.parse(candidate);
|
|
233
|
+
} catch {
|
|
234
|
+
audit(`architect output not parseable as JSON: ${architectOutput.slice(0, 200)}`);
|
|
235
|
+
process.stderr.write(
|
|
236
|
+
`Architect-fallback: output was not parseable JSON. Hamza must intervene.\nOutput excerpt: ${architectOutput.slice(0, 400)}\n`
|
|
237
|
+
);
|
|
238
|
+
process.exit(2);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (!plan?.planId || !Array.isArray(plan?.phases)) {
|
|
242
|
+
audit(`architect plan missing required fields: ${JSON.stringify(plan).slice(0, 200)}`);
|
|
243
|
+
process.stderr.write(
|
|
244
|
+
`Architect-fallback: plan missing planId or phases[]. Hamza must intervene.\n`
|
|
245
|
+
);
|
|
246
|
+
process.exit(2);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ── Write the new plan to the session-scoped active-plan path ─────────────────
|
|
250
|
+
plan.mintedAt = new Date().toISOString();
|
|
251
|
+
plan.sessionId = sessionId;
|
|
252
|
+
plan.mintedBy = 'architect-fallback';
|
|
253
|
+
plan.architectFallbackReason = reason.slice(0, 500);
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
if (!existsSync(dirname(ACTIVE_PLAN_PATH))) mkdirSync(dirname(ACTIVE_PLAN_PATH), { recursive: true });
|
|
257
|
+
writeFileSync(ACTIVE_PLAN_PATH, JSON.stringify(plan, null, 2));
|
|
258
|
+
} catch (err) {
|
|
259
|
+
audit(`failed to write plan to ${ACTIVE_PLAN_PATH}: ${String(err).slice(0, 200)}`);
|
|
260
|
+
process.exit(2);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
audit(`architect minted plan ${plan.planId} addressing blocker; written to ${ACTIVE_PLAN_PATH}`);
|
|
264
|
+
process.stdout.write(
|
|
265
|
+
JSON.stringify({ ok: true, planId: plan.planId, mintedBy: 'architect-fallback', planPath: ACTIVE_PLAN_PATH }) + '\n'
|
|
266
|
+
);
|
|
267
|
+
process.exit(0);
|