@aria_asi/cli 0.2.32 → 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.
Files changed (93) hide show
  1. package/dist/aria-connector/src/connectors/codebase-awareness.d.ts +8 -1
  2. package/dist/aria-connector/src/connectors/codebase-awareness.d.ts.map +1 -1
  3. package/dist/aria-connector/src/connectors/codebase-awareness.js +126 -71
  4. package/dist/aria-connector/src/connectors/codebase-awareness.js.map +1 -1
  5. package/dist/aria-connector/src/connectors/codex.d.ts.map +1 -1
  6. package/dist/aria-connector/src/connectors/codex.js +98 -0
  7. package/dist/aria-connector/src/connectors/codex.js.map +1 -1
  8. package/dist/aria-connector/src/setup-wizard.d.ts.map +1 -1
  9. package/dist/aria-connector/src/setup-wizard.js +91 -24
  10. package/dist/aria-connector/src/setup-wizard.js.map +1 -1
  11. package/dist/assets/hooks/aria-harness-via-sdk.mjs +26 -8
  12. package/dist/assets/hooks/aria-pre-tool-gate.mjs +60 -1
  13. package/dist/assets/hooks/aria-stop-gate.mjs +69 -3
  14. package/dist/assets/hooks/doctrine_trigger_map.json +43 -0
  15. package/dist/assets/hooks/lib/domain-output-quality.mjs +103 -0
  16. package/dist/assets/hooks/lib/skill-autoload-gate.mjs +14 -0
  17. package/dist/assets/opencode-plugins/harness-context/index.js +1 -1
  18. package/dist/assets/opencode-plugins/harness-gate/index.js +114 -10
  19. package/dist/assets/opencode-plugins/harness-gate/lib/skill-autoload-gate.js +14 -0
  20. package/dist/assets/opencode-plugins/harness-outcome/index.js +39 -0
  21. package/dist/assets/opencode-plugins/harness-stop/index.js +234 -139
  22. package/dist/assets/opencode-plugins/harness-stop/lib/domain-output-quality.js +103 -0
  23. package/dist/assets/opencode-plugins/harness-stop/lib/skill-autoload-gate.js +14 -0
  24. package/dist/runtime/codex-bridge.mjs +71 -8
  25. package/dist/runtime/discipline/CLAUDE.md +2 -2
  26. package/dist/runtime/discipline/doctrine_trigger_map.json +43 -0
  27. package/dist/runtime/discipline/skills/aria-harness/aria-harness-onboarding/SKILL.md +3 -3
  28. package/dist/runtime/doctrine_trigger_map.json +43 -0
  29. package/dist/runtime/harness-daemon.mjs +50 -2
  30. package/dist/runtime/hooks/aria-agent-handoff.mjs +247 -0
  31. package/dist/runtime/hooks/aria-agent-ledger-merge.mjs +164 -0
  32. package/dist/runtime/hooks/aria-architect-fallback.mjs +267 -0
  33. package/dist/runtime/hooks/aria-cognition-substrate-binding.mjs +761 -0
  34. package/dist/runtime/hooks/aria-discovery-record.mjs +101 -0
  35. package/dist/runtime/hooks/aria-harness-via-sdk.mjs +544 -0
  36. package/dist/runtime/hooks/aria-import-resolution-gate.mjs +330 -0
  37. package/dist/runtime/hooks/aria-outcome-record.mjs +84 -0
  38. package/dist/runtime/hooks/aria-pre-emit-dryrun.mjs +329 -0
  39. package/dist/runtime/hooks/aria-pre-text-gate.mjs +112 -0
  40. package/dist/runtime/hooks/aria-pre-tool-gate.mjs +2482 -0
  41. package/dist/runtime/hooks/aria-preprompt-consult.mjs +464 -0
  42. package/dist/runtime/hooks/aria-preturn-memory-gate.mjs +647 -0
  43. package/dist/runtime/hooks/aria-repo-doctrine-gate.mjs +429 -0
  44. package/dist/runtime/hooks/aria-stop-gate.mjs +1882 -0
  45. package/dist/runtime/hooks/aria-trigger-autolearn.mjs +229 -0
  46. package/dist/runtime/hooks/aria-userprompt-abandon-detect.mjs +192 -0
  47. package/dist/runtime/hooks/doctrine_trigger_map.json +577 -0
  48. package/dist/runtime/hooks/lib/canonical-lenses.mjs +65 -0
  49. package/dist/runtime/hooks/lib/domain-output-quality.mjs +103 -0
  50. package/dist/runtime/hooks/lib/gate-audit.mjs +43 -0
  51. package/dist/runtime/hooks/lib/gate-loop-state.mjs +50 -0
  52. package/dist/runtime/hooks/lib/hook-message-window.mjs +121 -0
  53. package/dist/runtime/hooks/lib/skill-autoload-gate.mjs +14 -0
  54. package/dist/runtime/hooks/test-aria-preturn-memory-gate.mjs +245 -0
  55. package/dist/runtime/hooks/test-tier-lens-labeling.mjs +367 -0
  56. package/dist/runtime/manifest.json +2 -2
  57. package/dist/runtime/sdk/BUNDLED.json +2 -2
  58. package/dist/runtime/sdk/index.d.ts +48 -0
  59. package/dist/runtime/sdk/index.js +140 -1
  60. package/dist/runtime/sdk/index.js.map +1 -1
  61. package/dist/runtime/sdk/runWithGovernance.d.ts +16 -0
  62. package/dist/runtime/sdk/runWithGovernance.js +54 -0
  63. package/dist/runtime/sdk/runWithGovernance.js.map +1 -0
  64. package/dist/runtime/service.mjs +339 -10
  65. package/dist/sdk/BUNDLED.json +2 -2
  66. package/dist/sdk/index.d.ts +48 -0
  67. package/dist/sdk/index.js +140 -1
  68. package/dist/sdk/index.js.map +1 -1
  69. package/dist/sdk/runWithGovernance.d.ts +16 -0
  70. package/dist/sdk/runWithGovernance.js +54 -0
  71. package/dist/sdk/runWithGovernance.js.map +1 -0
  72. package/hooks/aria-harness-via-sdk.mjs +26 -8
  73. package/hooks/aria-pre-tool-gate.mjs +60 -1
  74. package/hooks/aria-stop-gate.mjs +69 -3
  75. package/hooks/doctrine_trigger_map.json +43 -0
  76. package/hooks/lib/domain-output-quality.mjs +103 -0
  77. package/hooks/lib/skill-autoload-gate.mjs +14 -0
  78. package/opencode-plugins/harness-context/index.js +1 -1
  79. package/opencode-plugins/harness-gate/index.js +114 -10
  80. package/opencode-plugins/harness-gate/lib/skill-autoload-gate.js +14 -0
  81. package/opencode-plugins/harness-outcome/index.js +39 -0
  82. package/opencode-plugins/harness-stop/index.js +234 -139
  83. package/opencode-plugins/harness-stop/lib/domain-output-quality.js +103 -0
  84. package/opencode-plugins/harness-stop/lib/skill-autoload-gate.js +14 -0
  85. package/package.json +12 -5
  86. package/runtime-src/codex-bridge.mjs +71 -8
  87. package/runtime-src/harness-daemon.mjs +50 -2
  88. package/runtime-src/service.mjs +339 -10
  89. package/scripts/bundle-sdk.mjs +2 -0
  90. package/scripts/self-test-harness-gates.mjs +79 -0
  91. package/src/connectors/codebase-awareness.ts +141 -77
  92. package/src/connectors/codex.ts +98 -0
  93. package/src/setup-wizard.ts +105 -25
@@ -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);