@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,330 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Aria import-resolution gate — pre-deploy check that every TypeScript
|
|
3
|
+
// import in the deploy candidate resolves against an actual exported
|
|
4
|
+
// symbol on disk.
|
|
5
|
+
//
|
|
6
|
+
// Doctrine being enforced (memory:feedback_no_assumption_without_verification.md):
|
|
7
|
+
// "every behavioral claim must cite a verifiedAgainst source."
|
|
8
|
+
// The empty consciousness.ts crash on 2026-04-28 happened because
|
|
9
|
+
// event-bus.ts:11 imported `consciousness` from a file that existed
|
|
10
|
+
// but exported nothing. tsc allowed it (because the file matched as a
|
|
11
|
+
// module), the deploy passed admission policy, the runtime crashed.
|
|
12
|
+
//
|
|
13
|
+
// This gate scans every import statement in the deploy candidate's
|
|
14
|
+
// source tree; for each, it verifies that the named symbol is
|
|
15
|
+
// actually exported from the resolved file. If any import targets a
|
|
16
|
+
// symbol that isn't exported, the gate fails and refuses the deploy.
|
|
17
|
+
//
|
|
18
|
+
// Runs as a PostToolUse hook on Edit/Write/NotebookEdit AND as a
|
|
19
|
+
// pre-deploy assertion in scripts/deploy-service.sh.
|
|
20
|
+
//
|
|
21
|
+
// Mode 1 (post-tool-use): scan only the file just edited; report broken
|
|
22
|
+
// imports inline so the model sees them in the next turn's context.
|
|
23
|
+
// Mode 2 (pre-deploy via CLI args): scan a whole subtree (passed as
|
|
24
|
+
// first positional arg); exit 13 if any broken import found.
|
|
25
|
+
//
|
|
26
|
+
// Audit log: ~/.claude/aria-import-resolution-gate.log
|
|
27
|
+
|
|
28
|
+
import { readFileSync, existsSync, statSync, readdirSync, appendFileSync } from 'node:fs';
|
|
29
|
+
import { dirname, resolve, join, extname } from 'node:path';
|
|
30
|
+
import { homedir } from 'node:os';
|
|
31
|
+
|
|
32
|
+
const HOME = process.env.HOME || homedir() || '/tmp';
|
|
33
|
+
const LOG = `${HOME}/.claude/aria-import-resolution-gate.log`;
|
|
34
|
+
|
|
35
|
+
function audit(decision, summary) {
|
|
36
|
+
try {
|
|
37
|
+
appendFileSync(LOG, `${new Date().toISOString()} ${decision} ${summary}\n`);
|
|
38
|
+
} catch {}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Per feedback_no_timeouts_doctrine.md — no deadlines, real-error driven.
|
|
42
|
+
// Per feedback_no_graceful_degradation.md — fail loudly, not silent-pass.
|
|
43
|
+
|
|
44
|
+
const TS_IMPORT_RX =
|
|
45
|
+
/\bimport\s+(?:type\s+)?(?:(\w+)\s*,\s*)?(?:\{([^}]+)\}|\*\s+as\s+(\w+)|(\w+))\s+from\s+['"]([^'"]+)['"]/g;
|
|
46
|
+
const TS_REEXPORT_RX =
|
|
47
|
+
/\bexport\s+(?:type\s+)?(?:\{([^}]+)\}|\*(?:\s+as\s+\w+)?)\s+from\s+['"]([^'"]+)['"]/g;
|
|
48
|
+
|
|
49
|
+
const SOURCE_EXTS = ['.ts', '.tsx', '.js', '.mjs', '.cjs'];
|
|
50
|
+
const INDEX_NAMES = SOURCE_EXTS.map((e) => `index${e}`);
|
|
51
|
+
|
|
52
|
+
function resolveSpecifier(fromFile, specifier) {
|
|
53
|
+
// Skip node_modules / package imports — only resolve relative imports
|
|
54
|
+
// for now (those are the failure mode the consciousness.ts crash hit).
|
|
55
|
+
if (!specifier.startsWith('.') && !specifier.startsWith('/')) return null;
|
|
56
|
+
|
|
57
|
+
const baseDir = dirname(fromFile);
|
|
58
|
+
const abs = resolve(baseDir, specifier);
|
|
59
|
+
|
|
60
|
+
// Try exact path first.
|
|
61
|
+
if (existsSync(abs) && statSync(abs).isFile()) return abs;
|
|
62
|
+
|
|
63
|
+
// Try with each extension.
|
|
64
|
+
for (const ext of SOURCE_EXTS) {
|
|
65
|
+
const withExt = abs + ext;
|
|
66
|
+
if (existsSync(withExt) && statSync(withExt).isFile()) return withExt;
|
|
67
|
+
// Strip .js suffix and try .ts (TS resolution quirk).
|
|
68
|
+
if (specifier.endsWith('.js')) {
|
|
69
|
+
const tsCandidate = abs.replace(/\.js$/, '.ts');
|
|
70
|
+
if (existsSync(tsCandidate) && statSync(tsCandidate).isFile()) return tsCandidate;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Try directory/index.{ts,tsx,...}.
|
|
75
|
+
if (existsSync(abs) && statSync(abs).isDirectory()) {
|
|
76
|
+
for (const idx of INDEX_NAMES) {
|
|
77
|
+
const indexPath = join(abs, idx);
|
|
78
|
+
if (existsSync(indexPath) && statSync(indexPath).isFile()) return indexPath;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function readFileSafe(path) {
|
|
86
|
+
try {
|
|
87
|
+
return readFileSync(path, 'utf-8');
|
|
88
|
+
} catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function extractExports(content) {
|
|
94
|
+
// Very conservative export extraction — matches:
|
|
95
|
+
// export { foo, bar }
|
|
96
|
+
// export const|let|var|function|class|interface|type|enum NAME
|
|
97
|
+
// export default ...
|
|
98
|
+
// export * from '...'
|
|
99
|
+
// re-exports `export { foo } from '...'`
|
|
100
|
+
// Returns { named: Set<string>, hasDefault: boolean, hasStar: boolean }
|
|
101
|
+
const named = new Set();
|
|
102
|
+
let hasDefault = false;
|
|
103
|
+
let hasStar = false;
|
|
104
|
+
|
|
105
|
+
if (!content) return { named, hasDefault, hasStar };
|
|
106
|
+
|
|
107
|
+
// Strip block comments + line comments (rough; good enough for export detection).
|
|
108
|
+
const stripped = content
|
|
109
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
110
|
+
.replace(/\/\/[^\n]*/g, '');
|
|
111
|
+
|
|
112
|
+
// export { foo, bar as baz }
|
|
113
|
+
const namedListRx = /\bexport\s+(?:type\s+)?\{([^}]+)\}/g;
|
|
114
|
+
let m;
|
|
115
|
+
while ((m = namedListRx.exec(stripped))) {
|
|
116
|
+
for (const part of m[1].split(',')) {
|
|
117
|
+
const trimmed = part.trim();
|
|
118
|
+
if (!trimmed) continue;
|
|
119
|
+
const asMatch = trimmed.match(/^\S+\s+as\s+(\S+)$/);
|
|
120
|
+
const name = asMatch ? asMatch[1] : trimmed.split(/\s+/)[0];
|
|
121
|
+
if (name && /^\w+$/.test(name)) named.add(name);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// export const|let|var|function|async function|class|interface|type|enum NAME
|
|
126
|
+
const declRx =
|
|
127
|
+
/\bexport\s+(?:type\s+|async\s+)?(?:default\s+)?(?:const|let|var|function|class|interface|type|enum)\s+(\w+)/g;
|
|
128
|
+
while ((m = declRx.exec(stripped))) {
|
|
129
|
+
named.add(m[1]);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// export default
|
|
133
|
+
if (/\bexport\s+default\b/.test(stripped)) hasDefault = true;
|
|
134
|
+
|
|
135
|
+
// export * from '...'
|
|
136
|
+
if (/\bexport\s+\*(?:\s+as\s+\w+)?\s+from\b/.test(stripped)) hasStar = true;
|
|
137
|
+
|
|
138
|
+
return { named, hasDefault, hasStar };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function checkFileImports(filePath, visited = new Set()) {
|
|
142
|
+
if (visited.has(filePath)) return [];
|
|
143
|
+
visited.add(filePath);
|
|
144
|
+
|
|
145
|
+
const content = readFileSafe(filePath);
|
|
146
|
+
if (!content) return [];
|
|
147
|
+
|
|
148
|
+
// Skip non-source files.
|
|
149
|
+
if (!SOURCE_EXTS.includes(extname(filePath))) return [];
|
|
150
|
+
|
|
151
|
+
const violations = [];
|
|
152
|
+
let match;
|
|
153
|
+
TS_IMPORT_RX.lastIndex = 0;
|
|
154
|
+
while ((match = TS_IMPORT_RX.exec(content))) {
|
|
155
|
+
const [, defaultBinding, namedList, namespaceBinding, simpleNamed, specifier] = match;
|
|
156
|
+
const resolved = resolveSpecifier(filePath, specifier);
|
|
157
|
+
if (!resolved) continue; // package import — skip
|
|
158
|
+
|
|
159
|
+
const targetContent = readFileSafe(resolved);
|
|
160
|
+
if (!targetContent) {
|
|
161
|
+
violations.push({
|
|
162
|
+
file: filePath,
|
|
163
|
+
specifier,
|
|
164
|
+
resolved,
|
|
165
|
+
kind: 'unreadable',
|
|
166
|
+
message: `import '${specifier}' resolves to ${resolved} but file is unreadable`,
|
|
167
|
+
});
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (targetContent.trim().length === 0) {
|
|
172
|
+
violations.push({
|
|
173
|
+
file: filePath,
|
|
174
|
+
specifier,
|
|
175
|
+
resolved,
|
|
176
|
+
kind: 'empty_target',
|
|
177
|
+
message: `import '${specifier}' resolves to ${resolved} but file is EMPTY (no exports) — this is the consciousness.ts-class crash`,
|
|
178
|
+
});
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const { named, hasDefault, hasStar } = extractExports(targetContent);
|
|
183
|
+
|
|
184
|
+
if (defaultBinding && !hasDefault && !hasStar) {
|
|
185
|
+
violations.push({
|
|
186
|
+
file: filePath,
|
|
187
|
+
specifier,
|
|
188
|
+
resolved,
|
|
189
|
+
kind: 'missing_default',
|
|
190
|
+
message: `import default '${defaultBinding}' from '${specifier}' but ${resolved} has no default export`,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (namespaceBinding) {
|
|
195
|
+
// import * as ns — passes as long as the file has any exports.
|
|
196
|
+
if (named.size === 0 && !hasDefault && !hasStar) {
|
|
197
|
+
violations.push({
|
|
198
|
+
file: filePath,
|
|
199
|
+
specifier,
|
|
200
|
+
resolved,
|
|
201
|
+
kind: 'empty_namespace',
|
|
202
|
+
message: `import * as '${namespaceBinding}' from '${specifier}' but ${resolved} exports nothing`,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const explicitNamed = (namedList || simpleNamed || '').trim();
|
|
208
|
+
if (explicitNamed) {
|
|
209
|
+
const wantedNames = (namedList || simpleNamed)
|
|
210
|
+
.split(',')
|
|
211
|
+
.map((s) => s.trim())
|
|
212
|
+
.filter(Boolean)
|
|
213
|
+
.map((s) => {
|
|
214
|
+
const asMatch = s.match(/^(\S+)\s+as\s+\S+$/);
|
|
215
|
+
return asMatch ? asMatch[1] : s.split(/\s+/)[0];
|
|
216
|
+
})
|
|
217
|
+
.filter((s) => /^\w+$/.test(s));
|
|
218
|
+
|
|
219
|
+
for (const wanted of wantedNames) {
|
|
220
|
+
if (!named.has(wanted) && !hasStar) {
|
|
221
|
+
violations.push({
|
|
222
|
+
file: filePath,
|
|
223
|
+
specifier,
|
|
224
|
+
resolved,
|
|
225
|
+
kind: 'missing_named',
|
|
226
|
+
message: `import { ${wanted} } from '${specifier}' but ${resolved} does not export '${wanted}' (named exports: ${[...named].slice(0, 8).join(', ')}${named.size > 8 ? '…' : ''})`,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return violations;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function walkDir(dir, out = []) {
|
|
237
|
+
let entries;
|
|
238
|
+
try {
|
|
239
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
240
|
+
} catch {
|
|
241
|
+
return out;
|
|
242
|
+
}
|
|
243
|
+
for (const ent of entries) {
|
|
244
|
+
if (ent.name === 'node_modules' || ent.name === 'dist' || ent.name.startsWith('.')) continue;
|
|
245
|
+
const p = join(dir, ent.name);
|
|
246
|
+
if (ent.isDirectory()) walkDir(p, out);
|
|
247
|
+
else if (SOURCE_EXTS.includes(extname(ent.name))) out.push(p);
|
|
248
|
+
}
|
|
249
|
+
return out;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ── Mode dispatch ────────────────────────────────────────────────────────
|
|
253
|
+
|
|
254
|
+
const args = process.argv.slice(2);
|
|
255
|
+
|
|
256
|
+
if (args.length > 0) {
|
|
257
|
+
// Mode 2: pre-deploy CLI scan.
|
|
258
|
+
const target = resolve(args[0]);
|
|
259
|
+
const targetIsDir = existsSync(target) && statSync(target).isDirectory();
|
|
260
|
+
const files = targetIsDir ? walkDir(target) : [target];
|
|
261
|
+
|
|
262
|
+
let totalViolations = 0;
|
|
263
|
+
const allViolations = [];
|
|
264
|
+
for (const f of files) {
|
|
265
|
+
const violations = checkFileImports(f);
|
|
266
|
+
if (violations.length > 0) {
|
|
267
|
+
allViolations.push(...violations);
|
|
268
|
+
totalViolations += violations.length;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (totalViolations > 0) {
|
|
273
|
+
console.error(`\n❌ aria-import-resolution-gate: ${totalViolations} broken imports across ${files.length} files`);
|
|
274
|
+
for (const v of allViolations) {
|
|
275
|
+
console.error(` [${v.kind}] ${v.file}: ${v.message}`);
|
|
276
|
+
}
|
|
277
|
+
audit(`block-deploy violations=${totalViolations} target=${target}`, '');
|
|
278
|
+
process.exit(13);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
console.log(`✓ aria-import-resolution-gate: ${files.length} files scanned, all imports resolve to real exports`);
|
|
282
|
+
audit(`allow-deploy files=${files.length} target=${target}`, '');
|
|
283
|
+
process.exit(0);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Mode 1: PostToolUse hook on Edit/Write/NotebookEdit.
|
|
287
|
+
let stdin = '';
|
|
288
|
+
try {
|
|
289
|
+
for await (const chunk of process.stdin) stdin += chunk;
|
|
290
|
+
} catch {}
|
|
291
|
+
|
|
292
|
+
let event;
|
|
293
|
+
try {
|
|
294
|
+
event = JSON.parse(stdin);
|
|
295
|
+
} catch {
|
|
296
|
+
process.exit(0); // Malformed input — let it through; this gate is informational in hook mode.
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const toolName = event.tool_name ?? event.toolName ?? '';
|
|
300
|
+
if (!['Edit', 'Write', 'NotebookEdit'].includes(toolName)) process.exit(0);
|
|
301
|
+
|
|
302
|
+
const filePath =
|
|
303
|
+
event.tool_input?.file_path ??
|
|
304
|
+
event.toolInput?.file_path ??
|
|
305
|
+
event.tool_input?.notebook_path ??
|
|
306
|
+
event.toolInput?.notebook_path;
|
|
307
|
+
|
|
308
|
+
if (!filePath || !SOURCE_EXTS.includes(extname(filePath))) process.exit(0);
|
|
309
|
+
if (!existsSync(filePath)) process.exit(0);
|
|
310
|
+
|
|
311
|
+
const violations = checkFileImports(filePath);
|
|
312
|
+
if (violations.length === 0) {
|
|
313
|
+
audit(`allow-edit ${filePath}`, '');
|
|
314
|
+
process.exit(0);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const message = [
|
|
318
|
+
`aria-import-resolution-gate detected ${violations.length} broken imports in ${filePath}:`,
|
|
319
|
+
...violations.map((v) => ` [${v.kind}] ${v.message}`),
|
|
320
|
+
'',
|
|
321
|
+
'These imports compile under tsc but will crash at runtime — the consciousness.ts-class failure mode.',
|
|
322
|
+
'Per memory:feedback_no_assumption_without_verification.md, fix the export side BEFORE shipping.',
|
|
323
|
+
].join('\n');
|
|
324
|
+
|
|
325
|
+
audit(`flag-edit ${filePath} violations=${violations.length}`, '');
|
|
326
|
+
console.error(message);
|
|
327
|
+
// Hook mode is INFORMATIONAL (exit 0) so the model sees the message but
|
|
328
|
+
// can decide whether to fix immediately or proceed. The pre-deploy mode
|
|
329
|
+
// is the structural enforcement (exit 13 blocks the deploy).
|
|
330
|
+
process.exit(0);
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// aria-outcome-record.mjs — PostToolUse hook that records action outcomes
|
|
3
|
+
// to /api/harness/outcome-record (Sonnet H's #76 endpoint). Composes with
|
|
4
|
+
// aria-discovery-record + the regression sweeper to make the outcome ledger
|
|
5
|
+
// actually accumulate rows.
|
|
6
|
+
//
|
|
7
|
+
// Fires on every Bash|Edit|Write|NotebookEdit tool completion.
|
|
8
|
+
// Fire-and-forget: HTTP failures are swallowed — never blocks the tool pipeline.
|
|
9
|
+
//
|
|
10
|
+
// Tier-aware: reads license.json for client-tier token; falls back to env vars
|
|
11
|
+
// for owner tier. Never carries master token in client-tier POST bodies.
|
|
12
|
+
//
|
|
13
|
+
// Doctrine: feedback_no_flag_without_fix.md — outcomes recorded, not deferred.
|
|
14
|
+
// feedback_implementation_coupled_cognition.md — POST IS the impl.
|
|
15
|
+
|
|
16
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
17
|
+
import { homedir } from 'node:os';
|
|
18
|
+
|
|
19
|
+
const HOME = homedir();
|
|
20
|
+
const LICENSE_PATH = `${HOME}/.aria/license.json`;
|
|
21
|
+
|
|
22
|
+
let input = '';
|
|
23
|
+
for await (const chunk of process.stdin) input += chunk;
|
|
24
|
+
let event;
|
|
25
|
+
try { event = JSON.parse(input); } catch { process.exit(0); }
|
|
26
|
+
|
|
27
|
+
const toolName = event.tool_name || event.toolName || '';
|
|
28
|
+
if (!['Bash', 'Edit', 'Write', 'NotebookEdit'].includes(toolName)) process.exit(0);
|
|
29
|
+
|
|
30
|
+
// Derive action_kind + action_target
|
|
31
|
+
let actionKind, actionTarget;
|
|
32
|
+
if (toolName === 'Bash') {
|
|
33
|
+
actionKind = 'bash';
|
|
34
|
+
const cmd = event.tool_input?.command || '';
|
|
35
|
+
actionTarget = (cmd.split(/\s+/)[0] || 'unknown').slice(0, 100);
|
|
36
|
+
} else {
|
|
37
|
+
actionKind = 'edit';
|
|
38
|
+
actionTarget = (event.tool_input?.file_path || event.tool_input?.path || 'unknown').slice(0, 200);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Tier-aware auth: client license.json overrides env vars
|
|
42
|
+
const harnessUrl =
|
|
43
|
+
process.env.ARIA_HIVE_RUNTIME_URL ||
|
|
44
|
+
process.env.ARIA_HARNESS_BASE_URL ||
|
|
45
|
+
process.env.ARIA_HARNESS_URL ||
|
|
46
|
+
'https://harness.ariasos.com';
|
|
47
|
+
let harnessToken = process.env.ARIA_HARNESS_TOKEN || process.env.ARIA_API_KEY || '';
|
|
48
|
+
let isClientTier = false;
|
|
49
|
+
try {
|
|
50
|
+
if (existsSync(LICENSE_PATH)) {
|
|
51
|
+
const lic = JSON.parse(readFileSync(LICENSE_PATH, 'utf8'));
|
|
52
|
+
if (lic.jti) {
|
|
53
|
+
isClientTier = true;
|
|
54
|
+
// Client tier: use their license token, never master token
|
|
55
|
+
harnessToken = lic.token || lic.license || harnessToken;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
} catch { /* non-fatal — fall back to env */ }
|
|
59
|
+
|
|
60
|
+
const sessionId = event.session_id || event.sessionId || 'unknown';
|
|
61
|
+
const success = !event.tool_response?.error && event.tool_response?.type !== 'error';
|
|
62
|
+
|
|
63
|
+
// Fire-and-forget POST to outcome-record — never blocks, never throws
|
|
64
|
+
fetch(`${harnessUrl}/api/harness/outcome-record`, {
|
|
65
|
+
method: 'POST',
|
|
66
|
+
headers: {
|
|
67
|
+
'Content-Type': 'application/json',
|
|
68
|
+
'Authorization': `Bearer ${harnessToken}`,
|
|
69
|
+
},
|
|
70
|
+
body: JSON.stringify({
|
|
71
|
+
sessionId,
|
|
72
|
+
actionKind,
|
|
73
|
+
actionTarget,
|
|
74
|
+
actionShape: {
|
|
75
|
+
tool: toolName,
|
|
76
|
+
success,
|
|
77
|
+
isClientTier,
|
|
78
|
+
},
|
|
79
|
+
}),
|
|
80
|
+
}).catch(() => {/* fire-and-forget — HTTP failures are silently dropped */});
|
|
81
|
+
|
|
82
|
+
// Exit immediately — don't await the fetch; this is a PostToolUse hook
|
|
83
|
+
// and must not add meaningful latency to the tool pipeline.
|
|
84
|
+
process.exit(0);
|