@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.
- package/dist/src/cli/handlers/decision.js +90 -1
- package/dist/src/decision/adapters/jev.js +5 -1
- package/dist/src/decision/ask.js +352 -0
- package/dist/src/decision/driver.js +2 -0
- package/dist/src/decision/index.js +2 -0
- package/dist/src/decision/jev-credentials.js +121 -0
- package/dist/src/mcp/tools/decision.mjs +40 -0
- package/package.json +1 -1
- package/tools/agents/providers/base.mjs +14 -11
|
@@ -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
|
-
|
|
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
|
@@ -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
|
-
|
|
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) {
|