@aiwg/cli 2026.10.0 → 2026.10.1

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.
@@ -1,10 +1,11 @@
1
- import { decisionCapabilities, evaluateRequestPath, listPatterns, livePlan, materializeSyntheticClassificationSetup, runOfflinePattern, showPattern, syntheticClassificationSetup, validateDecisionInput, } from '../../decision/driver.js';
1
+ import { askDecision, DecisionAskUsageError, decisionCapabilities, evaluateRequestPath, listPatterns, livePlan, materializeSyntheticClassificationSetup, readDecisionContextFile, runOfflinePattern, setupJev, showPattern, syntheticClassificationSetup, validateDecisionInput, } from '../../decision/driver.js';
2
2
  function jsonResult(value, exitCode = 0) {
3
3
  return { exitCode, message: JSON.stringify(value, null, 2), rawOutput: true };
4
4
  }
5
5
  function usage() {
6
6
  return [
7
7
  'Usage: aiwg decision <capabilities|status|patterns|validate|evaluate|setup> [options]',
8
+ ' aiwg decision ask --question "<q>" (--yes-no | --choices a,b,c | --scale 1-5) [--context <text> | --context-file <path> | --context-stdin] [--threshold 0.8] [--timeout-ms 15000] [--json]',
8
9
  '',
9
10
  'Commands:',
10
11
  ' capabilities|status Show offline readiness, primitives, config and feature availability',
@@ -15,6 +16,8 @@ function usage() {
15
16
  ' validate <request|definition|ruleset|binding> <path>',
16
17
  ' evaluate --request <path> [--host-policy-module <path>]',
17
18
  ' setup synthetic-classification [--output-dir <dir>]',
19
+ ' setup jev [--token-stdin] [--region <r>] [--endpoint <url>] [--verify] [--remove]',
20
+ ' ask --question "<q>" (--yes-no | --choices a,b,c | --scale 1-5)',
18
21
  ].join('\n');
19
22
  }
20
23
  function takeOption(args, name) {
@@ -23,11 +26,90 @@ function takeOption(args, name) {
23
26
  return undefined;
24
27
  return args[index + 1];
25
28
  }
29
+ function takeRequiredOption(args, name) {
30
+ const value = takeOption(args, name);
31
+ if (!value || value.startsWith('--'))
32
+ throw new DecisionAskUsageError(`missing-${name.slice(2)}`, `${name} requires a value`);
33
+ return value;
34
+ }
26
35
  function patternId(value) {
27
36
  if (!value)
28
37
  throw new Error('Pattern id is required');
29
38
  return value;
30
39
  }
40
+ function flagCount(args, names) {
41
+ return args.filter(arg => names.includes(arg)).length;
42
+ }
43
+ async function readStdin() {
44
+ const chunks = [];
45
+ for await (const chunk of process.stdin)
46
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
47
+ return Buffer.concat(chunks).toString('utf8');
48
+ }
49
+ function parseNumber(value, name) {
50
+ if (value === undefined)
51
+ throw new DecisionAskUsageError(`missing-${name.slice(2)}`, `${name} requires a value`);
52
+ const parsed = Number(value);
53
+ if (!Number.isFinite(parsed))
54
+ throw new DecisionAskUsageError(`invalid-${name.slice(2)}`, `${name} must be numeric`);
55
+ return parsed;
56
+ }
57
+ function parseScale(value) {
58
+ if (!value)
59
+ throw new DecisionAskUsageError('invalid-scale', '--scale requires a range like 1-5');
60
+ const match = /^(-?\d+)-(-?\d+)$/.exec(value);
61
+ if (!match)
62
+ throw new DecisionAskUsageError('invalid-scale', '--scale requires a range like 1-5');
63
+ return { low: Number(match[1]), high: Number(match[2]) };
64
+ }
65
+ async function decisionSetupJev(rest) {
66
+ const token = rest.includes('--token-stdin') ? (await readStdin()).trim() : undefined;
67
+ const result = await setupJev({
68
+ token,
69
+ region: takeOption(rest, '--region'),
70
+ endpoint: takeOption(rest, '--endpoint'),
71
+ verify: rest.includes('--verify'),
72
+ remove: rest.includes('--remove'),
73
+ }, { env: process.env });
74
+ return jsonResult(result);
75
+ }
76
+ async function decisionAsk(ctx) {
77
+ const args = ctx.args.slice(1);
78
+ const modeCount = flagCount(args, ['--yes-no']) + (args.includes('--choices') ? 1 : 0) + (args.includes('--scale') ? 1 : 0);
79
+ if (modeCount !== 1)
80
+ throw new DecisionAskUsageError('invalid-mode', 'Select exactly one of --yes-no, --choices, or --scale');
81
+ const contextModes = flagCount(args, ['--context', '--context-file', '--context-stdin']);
82
+ if (contextModes > 1)
83
+ throw new DecisionAskUsageError('invalid-context', 'Select at most one context source');
84
+ const question = args.includes('--question') ? takeRequiredOption(args, '--question') : '';
85
+ let context = takeOption(args, '--context');
86
+ if (args.includes('--context') && (context === undefined || context.startsWith('--'))) {
87
+ throw new DecisionAskUsageError('missing-context', '--context requires text');
88
+ }
89
+ if (args.includes('--context-file')) {
90
+ const filePath = takeRequiredOption(args, '--context-file');
91
+ context = await readDecisionContextFile(filePath);
92
+ }
93
+ if (args.includes('--context-stdin'))
94
+ context = await readStdin();
95
+ const choices = args.includes('--choices') ? takeRequiredOption(args, '--choices') : undefined;
96
+ const scale = args.includes('--scale') ? parseScale(takeOption(args, '--scale')) : null;
97
+ const input = {
98
+ question,
99
+ mode: args.includes('--yes-no') ? { kind: 'yes-no' }
100
+ : choices !== undefined ? { kind: 'choices', choices: choices.split(',').map(value => value.trim()).filter(Boolean) }
101
+ : { kind: 'scale', ...scale },
102
+ ...(context === undefined ? {} : { context }),
103
+ ...(args.includes('--threshold') ? { threshold: parseNumber(takeOption(args, '--threshold'), '--threshold') } : {}),
104
+ ...(args.includes('--timeout-ms') ? { timeoutMs: parseNumber(takeOption(args, '--timeout-ms'), '--timeout-ms') } : {}),
105
+ };
106
+ const result = await askDecision(input, { env: process.env });
107
+ if (args.includes('--json'))
108
+ return jsonResult(result);
109
+ if (result.status === 'fallback')
110
+ return { exitCode: 0, message: `FALLBACK to LLM: ${result.reason}`, rawOutput: true };
111
+ return { exitCode: 0, message: `${String(result.answer)} (confidence ${result.confidence?.toFixed(2) ?? 'unknown'}, ${result.model ?? 'unknown-model'})`, rawOutput: true };
112
+ }
31
113
  async function executeDecision(ctx) {
32
114
  const [command, subcommand, ...rest] = ctx.args;
33
115
  const options = { cwd: ctx.cwd, frameworkRoot: ctx.frameworkRoot, env: process.env };
@@ -36,6 +118,8 @@ async function executeDecision(ctx) {
36
118
  return { exitCode: 0, message: usage(), rawOutput: true };
37
119
  if (command === 'capabilities' || command === 'status')
38
120
  return jsonResult(decisionCapabilities(options));
121
+ if (command === 'ask')
122
+ return await decisionAsk(ctx);
39
123
  if (command === 'patterns') {
40
124
  if (subcommand === 'list')
41
125
  return jsonResult(listPatterns());
@@ -70,9 +154,14 @@ async function executeDecision(ctx) {
70
154
  return jsonResult(await materializeSyntheticClassificationSetup(outputDir, {}, options));
71
155
  return jsonResult(syntheticClassificationSetup({}, options));
72
156
  }
157
+ if (command === 'setup' && subcommand === 'jev')
158
+ return await decisionSetupJev(rest);
73
159
  return { exitCode: 2, message: usage(), rawOutput: true };
74
160
  }
75
161
  catch (error) {
162
+ if (error instanceof DecisionAskUsageError) {
163
+ return { exitCode: 2, message: JSON.stringify({ schema: 'aiwg-decision-error/v1', reason: error.reason, message: error.message }, null, 2), rawOutput: true };
164
+ }
76
165
  return { exitCode: 1, message: error instanceof Error ? error.message : String(error) };
77
166
  }
78
167
  }
@@ -453,7 +453,11 @@ function normalizeAnswer(request, body, questionId) {
453
453
  throw new DecisionValidationError('Score legend does not match declared levels');
454
454
  });
455
455
  const mean = Object.entries(distribution).reduce((sum, [index, probability]) => sum + Number(index) * probability, 0);
456
- if (Math.abs(mean - answer.score) > 0.02)
456
+ // Jev reports probabilities and the score to two decimals, so each probability carries up to
457
+ // 0.005 of rounding error, weighted by its index, plus 0.005 on the score itself.
458
+ const indexSum = Object.keys(distribution).reduce((sum, index) => sum + Number(index), 0);
459
+ const tolerance = Math.max(0.02, 0.005 * indexSum + 0.005) + 1e-9;
460
+ if (Math.abs(mean - answer.score) > tolerance)
457
461
  throw new DecisionValidationError('Score is not the distribution weighted mean');
458
462
  validateDecisionValue(request.definition, answer.score);
459
463
  return success(answer.score, model, usage, uncertainty(answer.confidence, distribution, 'typesafe-distribution-v1'));
@@ -0,0 +1,352 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { JEV_ENDPOINT, JevDecisionAdapter } from './adapters/jev.js';
3
+ import { evaluateDecisionRuleset } from './evaluate.js';
4
+ import { artifactPin } from './validate.js';
5
+ import { JEV_CREDENTIAL_REF, JEV_DEFAULT_REGION, lookupJevCredential, removeJevCredentialFile, resolveJevCredentialBytes, writeJevCredentialFile, } from './jev-credentials.js';
6
+ const CONTEXT_LIMIT_BYTES = 32 * 1024;
7
+ const MODEL = 'jev-latest';
8
+ const PURPOSE = 'agent-bounded-decision';
9
+ const INPUT_SCHEMA = {
10
+ type: 'object',
11
+ properties: { message: { type: 'string', minLength: 1 } },
12
+ required: ['message'],
13
+ additionalProperties: false,
14
+ };
15
+ export class DecisionAskUsageError extends Error {
16
+ reason;
17
+ constructor(reason, message) {
18
+ super(message);
19
+ this.reason = reason;
20
+ this.name = 'DecisionAskUsageError';
21
+ }
22
+ }
23
+ function usageError(reason, message) {
24
+ throw new DecisionAskUsageError(reason, message);
25
+ }
26
+ function byteLength(value) {
27
+ return Buffer.byteLength(value, 'utf8');
28
+ }
29
+ function validateQuestion(question) {
30
+ if (!question.trim())
31
+ usageError('question-required', 'decision ask requires --question <q>');
32
+ }
33
+ export function validateDecisionAskInput(input) {
34
+ validateQuestion(input.question);
35
+ if (input.context !== undefined && byteLength(input.context) > CONTEXT_LIMIT_BYTES) {
36
+ usageError('context-too-large', 'decision ask context exceeds 32 KiB');
37
+ }
38
+ const threshold = input.threshold ?? 0.8;
39
+ if (!Number.isFinite(threshold) || threshold < 0 || threshold > 1) {
40
+ usageError('invalid-threshold', '--threshold must be a probability from 0 to 1');
41
+ }
42
+ const timeoutMs = input.timeoutMs ?? 15_000;
43
+ if (!Number.isInteger(timeoutMs) || timeoutMs <= 0)
44
+ usageError('invalid-timeout', '--timeout-ms must be a positive integer');
45
+ if (input.mode.kind === 'choices') {
46
+ if (input.mode.choices.length < 2 || input.mode.choices.length > 255)
47
+ usageError('invalid-choices', '--choices requires 2..255 ids');
48
+ const seen = new Set();
49
+ for (const { id, description } of input.mode.choices.map(parseChoice)) {
50
+ if (!/^[A-Za-z0-9_.-]{1,64}$/.test(id))
51
+ usageError('invalid-choices', 'choice ids must match [A-Za-z0-9_.-]{1,64}');
52
+ if (description !== undefined && (!description || description.length > 200)) {
53
+ usageError('invalid-choices', 'choice descriptions after = must be 1..200 characters');
54
+ }
55
+ if (seen.has(id))
56
+ usageError('invalid-choices', 'choice ids must not contain duplicates');
57
+ seen.add(id);
58
+ }
59
+ }
60
+ if (input.mode.kind === 'scale') {
61
+ const count = input.mode.high - input.mode.low + 1;
62
+ if (!Number.isInteger(input.mode.low) || !Number.isInteger(input.mode.high) || input.mode.low > input.mode.high || count < 2 || count > 10) {
63
+ usageError('invalid-scale', '--scale must be an integer range like 1-5 with at most 10 levels');
64
+ }
65
+ }
66
+ }
67
+ function usage(usageValue) {
68
+ if (!usageValue)
69
+ return null;
70
+ return { inputTokens: usageValue.inputTokens, outputTokens: usageValue.outputTokens };
71
+ }
72
+ function fallback(reason, latencyMs, attempt) {
73
+ return {
74
+ schema: 'aiwg-decision-ask/v1',
75
+ status: 'fallback',
76
+ answer: null,
77
+ confidence: null,
78
+ fallback: 'llm',
79
+ reason,
80
+ model: attempt?.actualModel ?? null,
81
+ usage: usage(attempt?.usage),
82
+ latencyMs,
83
+ };
84
+ }
85
+ /** A choice is `id` or `id=description`; the description tells Jev what the option means. */
86
+ function parseChoice(entry) {
87
+ const separator = entry.indexOf('=');
88
+ if (separator < 0)
89
+ return { id: entry.trim() };
90
+ return { id: entry.slice(0, separator).trim(), description: entry.slice(separator + 1).trim() };
91
+ }
92
+ function scaleLevel(value, low, high) {
93
+ const edge = value === low ? ' (lowest)' : value === high ? ' (highest)' : '';
94
+ return `Level ${value} on a ${low} to ${high} scale${edge}.`;
95
+ }
96
+ function answerKind(mode) {
97
+ if (mode.kind === 'yes-no')
98
+ return {
99
+ kind: 'truth-probability',
100
+ trueDescription: 'Yes.',
101
+ falseDescription: 'No.',
102
+ };
103
+ if (mode.kind === 'choices')
104
+ return {
105
+ kind: 'choice',
106
+ options: mode.choices.map(parseChoice).map(({ id, description }) => ({ id, description: description ?? id })),
107
+ };
108
+ return {
109
+ kind: 'ordinal-score',
110
+ levels: Array.from({ length: mode.high - mode.low + 1 }, (_, index) => scaleLevel(mode.low + index, mode.low, mode.high)),
111
+ };
112
+ }
113
+ function definition(input) {
114
+ return {
115
+ apiVersion: 'decision.aiwg.io/v1alpha2',
116
+ kind: 'DecisionDefinition',
117
+ metadata: { id: 'aiwg.ask.answer', version: '1.0.0', description: 'Bounded advisory ask decision' },
118
+ spec: {
119
+ purpose: PURPOSE,
120
+ inputSchema: INPUT_SCHEMA,
121
+ question: input.question,
122
+ answer: answerKind(input.mode),
123
+ requiredCapabilities: [input.mode.kind === 'yes-no' ? 'truth-probability' : input.mode.kind === 'choices' ? 'choice' : 'ordinal-score'],
124
+ },
125
+ };
126
+ }
127
+ function ruleset(definitionValue) {
128
+ return {
129
+ apiVersion: 'decision.aiwg.io/v1alpha2',
130
+ kind: 'DecisionRuleset',
131
+ metadata: { id: 'aiwg.ask.ruleset', version: '1.0.0', description: 'Bounded advisory ask ruleset' },
132
+ spec: {
133
+ purpose: PURPOSE,
134
+ inputSchema: INPUT_SCHEMA,
135
+ evaluations: [{ alias: 'answer', decision: artifactPin(definitionValue), inputPointer: '' }],
136
+ rules: [{
137
+ id: 'answered',
138
+ priority: 1,
139
+ when: { op: 'exists', left: { source: 'decision', alias: 'answer', pointer: '/value' } },
140
+ outcome: 'answered',
141
+ }],
142
+ composition: 'first-match',
143
+ conflict: 'review',
144
+ defaultOutcome: 'fallback',
145
+ failureOutcome: 'fallback',
146
+ outputSchema: { enum: ['answered', 'fallback'] },
147
+ },
148
+ };
149
+ }
150
+ function binding(rulesetValue, primitive, timeoutMs) {
151
+ const target = {
152
+ adapter: 'jev',
153
+ adapterVersion: '1.0.0',
154
+ model: MODEL,
155
+ requiredCapabilities: [primitive],
156
+ acceptance: { mode: 'typed-value' },
157
+ timeoutMs,
158
+ retry: { maxRetries: 0, initialDelayMs: 250, maxDelayMs: 2_000 },
159
+ credentialRef: JEV_CREDENTIAL_REF,
160
+ };
161
+ return {
162
+ apiVersion: 'decision.aiwg.io/v1alpha2',
163
+ kind: 'DecisionBinding',
164
+ metadata: { id: 'aiwg.ask.binding', version: '1.0.0', description: 'Bounded advisory ask Jev binding' },
165
+ spec: {
166
+ ruleset: artifactPin(rulesetValue),
167
+ totalTimeoutMs: timeoutMs,
168
+ maxAttempts: 1,
169
+ concurrency: 1,
170
+ evaluations: { answer: { targets: [target], fallbackOn: [] } },
171
+ },
172
+ };
173
+ }
174
+ function projectionPolicy(region, endpoint) {
175
+ const origin = new URL(endpoint ?? JEV_ENDPOINT).origin;
176
+ return {
177
+ version: '1.0.0',
178
+ provider: 'jev',
179
+ model: MODEL,
180
+ origin,
181
+ region,
182
+ purpose: PURPOSE,
183
+ allowIncompleteContext: false,
184
+ fields: [{
185
+ pointer: '/message',
186
+ output: 'message',
187
+ source: 'operator',
188
+ subject: 'aiwg-decision-ask',
189
+ trust: 'untrusted',
190
+ sensitivity: 'internal',
191
+ purpose: PURPOSE,
192
+ retentionClass: 'ephemeral',
193
+ accessScopes: ['decision-runtime'],
194
+ exportPolicy: 'sanitized',
195
+ deletionPolicy: 'erase',
196
+ backupPolicy: 'not-persisted',
197
+ allowedProviders: ['jev'],
198
+ allowedModels: [MODEL],
199
+ allowedOrigins: [origin],
200
+ allowedRegions: [region],
201
+ }],
202
+ };
203
+ }
204
+ function contextMessage(input) {
205
+ return input.context === undefined || input.context === ''
206
+ ? input.question
207
+ : `${input.question}\n\nContext:\n${input.context}`;
208
+ }
209
+ function confidenceFor(mode, evaluation) {
210
+ const uncertainty = evaluation.spec.uncertainty;
211
+ const value = evaluation.spec.value;
212
+ if (mode.kind === 'yes-no' && typeof value === 'number')
213
+ return Math.max(value, 1 - value);
214
+ if (uncertainty?.distribution && mode.kind === 'choices' && typeof value === 'string')
215
+ return uncertainty.distribution[value] ?? uncertainty.confidence;
216
+ if (uncertainty?.distribution && mode.kind === 'scale')
217
+ return Math.max(...Object.values(uncertainty.distribution));
218
+ return uncertainty?.confidence ?? null;
219
+ }
220
+ function mostLikelyIndex(distribution) {
221
+ if (!distribution)
222
+ return null;
223
+ let best = null;
224
+ for (const [index, probability] of Object.entries(distribution)) {
225
+ if (best === null || probability > (distribution[String(best)] ?? -1))
226
+ best = Number(index);
227
+ }
228
+ return best;
229
+ }
230
+ function publicAnswer(mode, value, distribution) {
231
+ if (mode.kind === 'yes-no') {
232
+ const probability = typeof value === 'number' ? value : null;
233
+ return probability === null ? { answer: null } : { answer: probability >= 0.5, probability };
234
+ }
235
+ if (mode.kind === 'scale') {
236
+ // Jev returns the distribution-weighted mean index; report the most likely level instead.
237
+ const index = mostLikelyIndex(distribution) ?? (typeof value === 'number' ? Math.round(value) : null);
238
+ return index === null ? { answer: null } : { answer: mode.low + index };
239
+ }
240
+ return typeof value === 'string' ? { answer: value } : { answer: null };
241
+ }
242
+ export async function askDecision(input, options = {}) {
243
+ validateDecisionAskInput(input);
244
+ const started = options.now?.() ?? Date.now();
245
+ let credential;
246
+ try {
247
+ credential = await lookupJevCredential(options);
248
+ }
249
+ catch {
250
+ return fallback('not-configured', 0);
251
+ }
252
+ if (!credential?.enabled || !credential.token)
253
+ return fallback('not-configured', 0);
254
+ const timeoutMs = input.timeoutMs ?? 15_000;
255
+ const def = definition(input);
256
+ const rules = ruleset(def);
257
+ const bind = binding(rules, def.spec.answer.kind, timeoutMs);
258
+ const adapter = options.adapter ?? options.createAdapter?.({ region: credential.region, endpoint: credential.endpoint })
259
+ ?? new JevDecisionAdapter({ region: credential.region, ...(credential.endpoint ? {
260
+ endpoint: credential.endpoint,
261
+ allowedOrigins: [new URL(credential.endpoint).origin],
262
+ } : {}) });
263
+ let result;
264
+ try {
265
+ result = await (options.evaluate ?? evaluateDecisionRuleset)({
266
+ ruleset: rules,
267
+ binding: bind,
268
+ definitions: { [def.metadata.id]: def },
269
+ input: { message: contextMessage(input) },
270
+ runId: options.runId ?? 'aiwg-decision-ask',
271
+ invocationId: options.invocationId ?? `ask-${Date.now()}`,
272
+ adapters: { jev: adapter },
273
+ resolveCredential: options.resolveCredential ?? (logicalRef => resolveJevCredentialBytes(logicalRef, options)),
274
+ projection: { resolve: () => projectionPolicy(credential.region, credential.endpoint) },
275
+ });
276
+ }
277
+ catch {
278
+ return fallback('jev-error', Math.max(0, (options.now?.() ?? Date.now()) - started));
279
+ }
280
+ const latencyMs = Math.max(0, (options.now?.() ?? Date.now()) - started);
281
+ const evaluation = result.spec.evaluations.answer;
282
+ const attempt = evaluation?.spec.attempts.at(-1);
283
+ if (!evaluation || evaluation.spec.status !== 'success')
284
+ return fallback(evaluation?.spec.reason ?? result.spec.reason, latencyMs, attempt);
285
+ const confidence = confidenceFor(input.mode, evaluation);
286
+ const threshold = input.threshold ?? 0.8;
287
+ if (confidence === null || confidence < threshold)
288
+ return fallback('low-confidence', latencyMs, attempt);
289
+ const answer = publicAnswer(input.mode, evaluation.spec.value, evaluation.spec.uncertainty?.distribution);
290
+ return {
291
+ schema: 'aiwg-decision-ask/v1',
292
+ status: 'answered',
293
+ answer: answer.answer,
294
+ confidence,
295
+ ...(answer.probability === undefined ? {} : { probability: answer.probability }),
296
+ fallback: null,
297
+ reason: 'none',
298
+ model: attempt?.actualModel ?? null,
299
+ usage: usage(attempt?.usage),
300
+ latencyMs,
301
+ };
302
+ }
303
+ export async function readDecisionContextFile(filePath) {
304
+ const content = await readFile(filePath, 'utf8');
305
+ if (byteLength(content) > CONTEXT_LIMIT_BYTES)
306
+ usageError('context-too-large', 'decision ask context exceeds 32 KiB');
307
+ return content;
308
+ }
309
+ export async function setupJev(input = {}, options = {}) {
310
+ const region = input.region?.trim() || JEV_DEFAULT_REGION;
311
+ if (input.remove) {
312
+ await removeJevCredentialFile(options);
313
+ return {
314
+ schema: 'aiwg-decision-jev-setup/v1',
315
+ configured: false,
316
+ credentialSource: 'file',
317
+ region,
318
+ next: ['Run aiwg decision setup jev --token-stdin to configure Jev again.'],
319
+ };
320
+ }
321
+ const environment = options.env ?? process.env;
322
+ const envToken = environment.JEV_API_KEY?.trim() || environment.AIWG_DECISION_JEV_API_KEY?.trim();
323
+ const token = input.token?.trim() || envToken;
324
+ if (!token)
325
+ usageError('token-required', 'Provide --token-stdin or set JEV_API_KEY or AIWG_DECISION_JEV_API_KEY.');
326
+ await writeJevCredentialFile({
327
+ token,
328
+ region,
329
+ ...(input.endpoint?.trim() ? { endpoint: input.endpoint.trim() } : {}),
330
+ enabled: true,
331
+ }, options);
332
+ const result = {
333
+ schema: 'aiwg-decision-jev-setup/v1',
334
+ configured: true,
335
+ credentialSource: input.token?.trim() ? 'file' : 'env',
336
+ region,
337
+ next: ['Run aiwg decision ask --question "<q>" --yes-no --json.'],
338
+ };
339
+ if (input.verify) {
340
+ const verified = await askDecision({
341
+ question: 'Is this Jev decision setup verification request well formed?',
342
+ mode: { kind: 'yes-no' },
343
+ threshold: 0,
344
+ timeoutMs: 15_000,
345
+ }, options);
346
+ result.verified = verified.status === 'answered';
347
+ result.model = verified.model;
348
+ result.usage = verified.usage;
349
+ }
350
+ return result;
351
+ }
352
+ //# sourceMappingURL=ask.js.map
@@ -2,7 +2,9 @@ import { existsSync } from 'node:fs';
2
2
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import { fileURLToPath, pathToFileURL } from 'node:url';
5
+ import { askDecision, setupJev, DecisionAskUsageError, readDecisionContextFile, validateDecisionAskInput, } from './ask.js';
5
6
  import { assertDecisionEvaluateDispatcherConfig, decisionPatternPacks, getDecisionPatternPack, governedPatternArtifacts, listDecisionPatterns, patternInputSchema, patternOutputSchema, planLiveDecisionPattern, runOfflineDecisionPattern, validateAgainstSchema, validateDecisionDocument, validateDefinition, validateRuleset, } from './index.js';
7
+ export { askDecision, setupJev, readDecisionContextFile, validateDecisionAskInput, DecisionAskUsageError, };
6
8
  const DECISION_ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
7
9
  const VALIDATION_TARGETS = ['request', 'definition', 'ruleset', 'binding'];
8
10
  const OFFLINE_ADAPTER = 'agentic/code/addons/decision-engine/examples/fixture-jev-adapter.mjs';
@@ -53,6 +53,8 @@ export * from './sensitivity/index.js';
53
53
  export * from './issue-triage/index.js';
54
54
  export * from './routing/index.js';
55
55
  export * from './interop.js';
56
+ export * from './ask.js';
57
+ export * from './jev-credentials.js';
56
58
  export { collectHeldoutStudy, scoreHeldoutStudy } from './heldout/collector.js';
57
59
  export { heldoutDigest, heldoutExecutionDigest, planHeldoutCollection, validateHeldoutBundle, heldoutRowsInScope } from './heldout/contract.js';
58
60
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,121 @@
1
+ import { constants } from 'node:fs';
2
+ import { access, chmod, mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { randomUUID } from 'node:crypto';
6
+ import { JevCredentialError } from './adapters/jev.js';
7
+ export const JEV_CREDENTIAL_REF = 'typesafe-api';
8
+ export const JEV_DEFAULT_REGION = 'us';
9
+ function env(options) {
10
+ return options.env ?? process.env;
11
+ }
12
+ export function jevCredentialPath(options = {}) {
13
+ if (options.credentialPath)
14
+ return options.credentialPath;
15
+ const configuredHome = options.configHome ?? env(options).XDG_CONFIG_HOME;
16
+ const base = configuredHome && configuredHome.trim() ? configuredHome : path.join(options.homeDir ?? os.homedir(), '.config');
17
+ return path.join(base, 'aiwg', 'credentials', 'jev.json');
18
+ }
19
+ function isInsecureMode(mode) {
20
+ return (mode & 0o077) !== 0;
21
+ }
22
+ function parseRecord(raw) {
23
+ const parsed = JSON.parse(raw);
24
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
25
+ throw new JevCredentialError('configuration');
26
+ const record = parsed;
27
+ if (typeof record.token !== 'string' || !record.token.trim())
28
+ throw new JevCredentialError('configuration');
29
+ if (record.region !== undefined && (typeof record.region !== 'string' || !record.region.trim())) {
30
+ throw new JevCredentialError('configuration');
31
+ }
32
+ if (record.endpoint !== undefined && (typeof record.endpoint !== 'string' || !record.endpoint.trim())) {
33
+ throw new JevCredentialError('configuration');
34
+ }
35
+ if (record.enabled !== undefined && typeof record.enabled !== 'boolean')
36
+ throw new JevCredentialError('configuration');
37
+ return {
38
+ token: record.token,
39
+ region: typeof record.region === 'string' ? record.region : JEV_DEFAULT_REGION,
40
+ ...(typeof record.endpoint === 'string' ? { endpoint: record.endpoint } : {}),
41
+ ...(typeof record.enabled === 'boolean' ? { enabled: record.enabled } : {}),
42
+ };
43
+ }
44
+ async function readCredentialFile(options = {}) {
45
+ const credentialPath = jevCredentialPath(options);
46
+ try {
47
+ const [fileStat, parentStat] = await Promise.all([stat(credentialPath), stat(path.dirname(credentialPath))]);
48
+ if (isInsecureMode(fileStat.mode) || isInsecureMode(parentStat.mode))
49
+ throw new JevCredentialError('denied');
50
+ return parseRecord(await readFile(credentialPath, 'utf8'));
51
+ }
52
+ catch (error) {
53
+ if (error.code === 'ENOENT')
54
+ return null;
55
+ if (error instanceof JevCredentialError)
56
+ throw error;
57
+ if (error instanceof SyntaxError)
58
+ throw new JevCredentialError('configuration');
59
+ throw error;
60
+ }
61
+ }
62
+ export async function lookupJevCredential(options = {}) {
63
+ const environment = env(options);
64
+ const forced = environment.AIWG_DECISION_ENABLED;
65
+ const envToken = environment.JEV_API_KEY?.trim() || environment.AIWG_DECISION_JEV_API_KEY?.trim();
66
+ const fileRecord = envToken && (forced === '0' || forced === '1') ? null : await readCredentialFile(options);
67
+ const enabled = forced === '0' ? false : forced === '1' ? true : fileRecord?.enabled === true;
68
+ if (envToken)
69
+ return {
70
+ source: 'env',
71
+ token: envToken,
72
+ region: environment.AIWG_DECISION_JEV_REGION?.trim() || fileRecord?.region || JEV_DEFAULT_REGION,
73
+ ...(environment.AIWG_DECISION_JEV_ENDPOINT?.trim() ? { endpoint: environment.AIWG_DECISION_JEV_ENDPOINT.trim() }
74
+ : fileRecord?.endpoint ? { endpoint: fileRecord.endpoint } : {}),
75
+ enabled,
76
+ };
77
+ if (!fileRecord)
78
+ return null;
79
+ return { source: 'file', token: fileRecord.token, region: fileRecord.region, ...(fileRecord.endpoint ? { endpoint: fileRecord.endpoint } : {}), enabled };
80
+ }
81
+ export async function resolveJevCredentialBytes(logicalRef, options = {}) {
82
+ if (logicalRef !== JEV_CREDENTIAL_REF)
83
+ throw new JevCredentialError('configuration');
84
+ const credential = await lookupJevCredential(options);
85
+ if (!credential?.enabled)
86
+ throw new JevCredentialError('denied');
87
+ if (!credential.token)
88
+ throw new JevCredentialError('missing');
89
+ return new TextEncoder().encode(credential.token);
90
+ }
91
+ export async function writeJevCredentialFile(record, options = {}) {
92
+ const credentialPath = jevCredentialPath(options);
93
+ const directory = path.dirname(credentialPath);
94
+ await mkdir(directory, { recursive: true, mode: 0o700 });
95
+ await chmod(directory, 0o700);
96
+ const tmp = path.join(directory, `.jev.${process.pid}.${randomUUID()}.tmp`);
97
+ const body = `${JSON.stringify(record, null, 2)}\n`;
98
+ try {
99
+ await writeFile(tmp, body, { mode: 0o600, flag: 'wx' });
100
+ await chmod(tmp, 0o600);
101
+ await rename(tmp, credentialPath);
102
+ await chmod(credentialPath, 0o600);
103
+ return credentialPath;
104
+ }
105
+ catch (error) {
106
+ await rm(tmp, { force: true }).catch(() => undefined);
107
+ throw error;
108
+ }
109
+ }
110
+ export async function removeJevCredentialFile(options = {}) {
111
+ const credentialPath = jevCredentialPath(options);
112
+ try {
113
+ await access(credentialPath, constants.F_OK);
114
+ }
115
+ catch {
116
+ return false;
117
+ }
118
+ await rm(credentialPath, { force: true });
119
+ return true;
120
+ }
121
+ //# sourceMappingURL=jev-credentials.js.map
@@ -82,6 +82,21 @@ const SYNTHETIC_SETUP = z.object({
82
82
  pattern: z.object({ id: z.string(), version: z.string() }).passthrough(),
83
83
  files: z.record(z.string(), z.unknown()),
84
84
  }).passthrough();
85
+ const ASK = z.object({
86
+ schema: z.literal('aiwg-decision-ask/v1'),
87
+ status: z.enum(['answered', 'fallback']),
88
+ answer: z.union([z.string(), z.boolean(), z.number()]).nullable(),
89
+ confidence: z.number().min(0).max(1).nullable(),
90
+ probability: z.number().min(0).max(1).optional(),
91
+ fallback: z.enum(['llm']).nullable(),
92
+ reason: z.string(),
93
+ model: z.string().nullable(),
94
+ usage: z.object({
95
+ inputTokens: z.number().int().nonnegative().nullable(),
96
+ outputTokens: z.number().int().nonnegative().nullable(),
97
+ }).nullable(),
98
+ latencyMs: z.number().nonnegative(),
99
+ }).passthrough();
85
100
 
86
101
  let driverPromise;
87
102
 
@@ -176,6 +191,31 @@ export function registerDecisionToolset(server) {
176
191
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
177
192
  }, async ({ profile, opt_in }) => (await driver()).evaluateMcpProfile(profile, opt_in, options()), EVALUATE);
178
193
 
194
+ registerJsonTool(server, 'decision-ask', {
195
+ title: 'Ask bounded Jev decision',
196
+ description: 'Ask a bounded yes/no, choice, or small ordinal decision. Falls back to the caller on missing setup or low confidence.',
197
+ inputSchema: {
198
+ question: z.string().min(1),
199
+ yes_no: z.boolean().default(false),
200
+ choices: z.array(z.string()).min(2).max(255).optional().describe('Choice ids, each optionally written as id=description so Jev knows what the option means'),
201
+ scale: z.object({ low: z.number().int(), high: z.number().int() }).optional(),
202
+ context: z.string().optional(),
203
+ threshold: z.number().min(0).max(1).default(0.8),
204
+ timeout_ms: z.number().int().positive().default(15000),
205
+ },
206
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
207
+ }, async ({ question, yes_no, choices, scale, context, threshold, timeout_ms }) => {
208
+ const modeCount = (yes_no ? 1 : 0) + (choices ? 1 : 0) + (scale ? 1 : 0);
209
+ if (modeCount !== 1) throw new Error('Select exactly one of yes_no, choices, or scale');
210
+ return (await driver()).askDecision({
211
+ question,
212
+ mode: yes_no ? { kind: 'yes-no' } : choices ? { kind: 'choices', choices } : { kind: 'scale', low: scale.low, high: scale.high },
213
+ ...(context === undefined ? {} : { context }),
214
+ threshold,
215
+ timeoutMs: timeout_ms,
216
+ }, options());
217
+ }, ASK);
218
+
179
219
  registerJsonTool(server, 'decision-setup-synthetic-classification', {
180
220
  title: 'Prepare synthetic classification artifacts',
181
221
  description: 'Return reviewable pinned synthetic classification artifacts and a dispatcher request.',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiwg/cli",
3
- "version": "2026.10.0",
3
+ "version": "2026.10.1",
4
4
  "description": "Lightweight AIWG CLI for signed, versioned web-backed resources.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1363,13 +1363,13 @@ export function computeAllArtifactBasenames(srcRoot, type) {
1363
1363
  // Soul companions live alongside agents and are deployed with them —
1364
1364
  // keep their stems in the desired set so the prune never removes them.
1365
1365
  add(frameworkArtifacts.souls || []);
1366
- add(getAddonAgentFiles(aiwgRoot));
1366
+ add(getAddonAgentFiles(aiwgRoot, [], { includeExplicit: true }));
1367
1367
  } else if (type === 'commands') {
1368
1368
  add(frameworkArtifacts.commands);
1369
- add(getAddonCommandFiles(aiwgRoot));
1369
+ add(getAddonCommandFiles(aiwgRoot, [], { includeExplicit: true }));
1370
1370
  } else if (type === 'rules') {
1371
1371
  add(frameworkArtifacts.rules);
1372
- add(getAddonRuleFiles(aiwgRoot));
1372
+ add(getAddonRuleFiles(aiwgRoot, [], { includeExplicit: true }));
1373
1373
  }
1374
1374
 
1375
1375
  return stems;
@@ -2264,9 +2264,10 @@ export function collectFrameworkArtifacts(srcRoot, mode, options = {}) {
2264
2264
  /**
2265
2265
  * Discover all addons in the agentic/code/addons directory
2266
2266
  * @param {string} srcRoot - Source root directory
2267
+ * @param {{includeExplicit?: boolean}} [options] - includeExplicit also returns explicitInstall addons
2267
2268
  * @returns {Array<{name: string, path: string, manifest: object}>} - Array of addon info
2268
2269
  */
2269
- export function discoverAddons(srcRoot) {
2270
+ export function discoverAddons(srcRoot, { includeExplicit = false } = {}) {
2270
2271
  const addonsDir = path.join(srcRoot, 'agentic', 'code', 'addons');
2271
2272
  if (!fs.existsSync(addonsDir)) return [];
2272
2273
 
@@ -2290,7 +2291,9 @@ export function discoverAddons(srcRoot) {
2290
2291
  if (manifest.devOnly === true) continue;
2291
2292
  // explicitInstall addons deploy only when named (`aiwg use <addon>`), never
2292
2293
  // as part of a framework or `all` bulk deploy (#2641).
2293
- if (manifest.explicitInstall === true) continue;
2294
+ // The stale-artifact prune passes includeExplicit so a named install's own
2295
+ // files stay in its desired set and are not deleted right after deployment.
2296
+ if (manifest.explicitInstall === true && !includeExplicit) continue;
2294
2297
 
2295
2298
  addons.push({
2296
2299
  name: entry.name,
@@ -2308,8 +2311,8 @@ export function discoverAddons(srcRoot) {
2308
2311
  * @param {string[]} excludeAddons - Addon names to exclude (default: none)
2309
2312
  * @returns {string[]} - Array of agent file paths
2310
2313
  */
2311
- export function getAddonAgentFiles(srcRoot, excludeAddons = []) {
2312
- const addons = discoverAddons(srcRoot);
2314
+ export function getAddonAgentFiles(srcRoot, excludeAddons = [], options = {}) {
2315
+ const addons = discoverAddons(srcRoot, options);
2313
2316
  const files = [];
2314
2317
 
2315
2318
  for (const addon of addons) {
@@ -2330,8 +2333,8 @@ export function getAddonAgentFiles(srcRoot, excludeAddons = []) {
2330
2333
  * @param {string[]} excludeAddons - Addon names to exclude (default: none)
2331
2334
  * @returns {string[]} - Array of command file paths
2332
2335
  */
2333
- export function getAddonCommandFiles(srcRoot, excludeAddons = []) {
2334
- const addons = discoverAddons(srcRoot);
2336
+ export function getAddonCommandFiles(srcRoot, excludeAddons = [], options = {}) {
2337
+ const addons = discoverAddons(srcRoot, options);
2335
2338
  const files = [];
2336
2339
 
2337
2340
  for (const addon of addons) {
@@ -2651,8 +2654,8 @@ export function writeOnDemandRuleIndex(destDir, onDemandFiles, opts = {}) {
2651
2654
  return names.length + demotedNames.length;
2652
2655
  }
2653
2656
 
2654
- export function getAddonRuleFiles(srcRoot, excludeAddons = []) {
2655
- const addons = discoverAddons(srcRoot);
2657
+ export function getAddonRuleFiles(srcRoot, excludeAddons = [], options = {}) {
2658
+ const addons = discoverAddons(srcRoot, options);
2656
2659
  const files = [];
2657
2660
 
2658
2661
  for (const addon of addons) {