leerness 1.36.183 → 1.36.185
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/CHANGELOG.md +8 -0
- package/LICENSE +21 -21
- package/README.ko.md +15 -8
- package/README.md +25 -7
- package/SECURITY.md +66 -66
- package/bin/leerness.js +1184 -206
- package/docs/role-agent-routing-v2.md +411 -0
- package/lib/agents.js +1236 -100
- package/lib/diagnostics.js +184 -184
- package/lib/file-leases.js +845 -0
- package/lib/health.js +350 -350
- package/lib/io.js +152 -1
- package/lib/mcp-tools.js +121 -104
- package/lib/pure-utils.js +52 -4
- package/lib/referee.js +270 -270
- package/lib/review-request.js +339 -339
- package/lib/role-agent-schema.js +1179 -0
- package/lib/role-catalog.js +31 -1
- package/lib/role-fallback.js +1611 -0
- package/lib/role-store.js +730 -0
- package/lib/routing.js +109 -21
- package/lib/session-close.js +49 -1
- package/package.json +10 -5
- package/scripts/command-flags-probe.js +40 -3
- package/scripts/e2e.js +121 -44
- package/scripts/file-lease-probe.js +1139 -0
- package/scripts/i18n-next-cluster-probe.js +436 -5
- package/scripts/installed-cleanroom-probe.js +39 -12
- package/scripts/lint.js +73 -73
- package/scripts/provider-capacity-probe.js +425 -0
- package/scripts/release-runtime-probe.js +16 -1
- package/scripts/role-agent-schema-probe.js +834 -0
- package/scripts/role-fallback-probe.js +2010 -0
- package/scripts/role-store-loader-probe.js +882 -0
package/lib/agents.js
CHANGED
|
@@ -5,9 +5,20 @@
|
|
|
5
5
|
'use strict';
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const fs = require('fs');
|
|
8
|
+
const crypto = require('crypto');
|
|
8
9
|
const { log, ok, warn, fail, failJson, today, now, absRoot, exists, read, readBuf, mkdirp, writeUtf8, append, rel } = require('./io');
|
|
9
10
|
const { EXTERNAL_AGENTS, AGENT_SLASH_COMMANDS } = require('./agent-registry');
|
|
10
|
-
const { spawnPortable
|
|
11
|
+
const { spawnPortable } = require('./portable-process');
|
|
12
|
+
const { classify, applyDeclaredRiskFloor } = require('./routing');
|
|
13
|
+
const roleStore = require('./role-store');
|
|
14
|
+
const {
|
|
15
|
+
resolveRoleFallback, selectFallbackOption, sessionIdentityFromEnv,
|
|
16
|
+
normalizeExecutorIdentity, canonicalProviderIdentity, canonicalLegacyProviderIdentity, assessReviewerIndependence, isValidModelIdentifier,
|
|
17
|
+
FALLBACK_POLICIES,
|
|
18
|
+
normalizeExecutionEvent, appendExecutionEvent, readExecutionEvents, executionLedgerPath,
|
|
19
|
+
appendAvailabilityObservation, appendAvailabilityClear,
|
|
20
|
+
readAvailabilityObservations, availabilityExtrasForCandidate, normalizeAvailability,
|
|
21
|
+
} = require('./role-fallback');
|
|
11
22
|
|
|
12
23
|
function _benchLaunchSpec(agent, task, writeMode) {
|
|
13
24
|
if (!agent || !agent.id) return { unsupported: 'provider definition missing' };
|
|
@@ -40,29 +51,443 @@ function _benchLaunchSpec(agent, task, writeMode) {
|
|
|
40
51
|
}
|
|
41
52
|
}
|
|
42
53
|
|
|
54
|
+
const _AGENT_VALUE_FLAGS = new Set([
|
|
55
|
+
'--to', '--provider', '--model', '-m', '--role', '--only', '--task', '--tier', '--preset',
|
|
56
|
+
'--approved-by', '--id', '--result', '--summary', '--status', '--agent', '--target', '--evidence',
|
|
57
|
+
'--limit', '--timeout', '--path', '--language',
|
|
58
|
+
'--session-id', '--session-provider', '--session-model', '--session-model-family',
|
|
59
|
+
'--model-family', '--agent-model', '--agent-model-family', '--agent-session',
|
|
60
|
+
'--auth-state', '--entitlement', '--quota-state', '--policy-state', '--reachability',
|
|
61
|
+
'--retry-after', '--expires-at', '--ttl-min', '--reason',
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
// Parse task/summary text without confusing option values for user text. The former
|
|
65
|
+
// value-based filter could drop a legitimate task whose text happened to equal a model
|
|
66
|
+
// name, and it did not cover newly added fallback/provenance flags. This parser follows
|
|
67
|
+
// argv positions instead: known value flags consume exactly their following argument,
|
|
68
|
+
// boolean/unknown flags are skipped, and `--` explicitly starts literal text.
|
|
69
|
+
function _parseAgentPositional(argv, start = 0) {
|
|
70
|
+
const list = Array.isArray(argv) ? argv : [];
|
|
71
|
+
const out = [];
|
|
72
|
+
for (let i = Math.max(0, start | 0); i < list.length; i++) {
|
|
73
|
+
const token = String(list[i] == null ? '' : list[i]);
|
|
74
|
+
if (token === '--') {
|
|
75
|
+
for (let j = i + 1; j < list.length; j++) out.push(String(list[j] == null ? '' : list[j]));
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
if (_AGENT_VALUE_FLAGS.has(token)) { if (i + 1 < list.length) i++; continue; }
|
|
79
|
+
if (/^--[^=]+=/.test(token) || token.startsWith('-')) continue;
|
|
80
|
+
out.push(token);
|
|
81
|
+
}
|
|
82
|
+
return out.join(' ').trim();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function _findAgentAction(argv, allowedActions) {
|
|
86
|
+
const allowed = allowedActions instanceof Set ? allowedActions : new Set(allowedActions || []);
|
|
87
|
+
const list = Array.isArray(argv) ? argv : [];
|
|
88
|
+
for (let i = 0; i < list.length; i++) {
|
|
89
|
+
const token = String(list[i] == null ? '' : list[i]);
|
|
90
|
+
if (token === '--') break;
|
|
91
|
+
if (_AGENT_VALUE_FLAGS.has(token)) { if (i + 1 < list.length) i++; continue; }
|
|
92
|
+
if (/^--[^=]+=/.test(token) || token.startsWith('-')) continue;
|
|
93
|
+
const action = token.trim().toLowerCase();
|
|
94
|
+
if (allowed.has(action)) return { action, index: i };
|
|
95
|
+
}
|
|
96
|
+
return { action: null, index: -1 };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function _singletonFlagIssue(argv, flag) {
|
|
100
|
+
const values = [];
|
|
101
|
+
const list = Array.isArray(argv) ? argv : [];
|
|
102
|
+
for (let i = 0; i < list.length; i++) {
|
|
103
|
+
const token = String(list[i] == null ? '' : list[i]);
|
|
104
|
+
if (token === '--') break;
|
|
105
|
+
if (token === flag) {
|
|
106
|
+
const next = list[i + 1];
|
|
107
|
+
if (next == null || String(next).startsWith('--')) values.push(null);
|
|
108
|
+
else { values.push(String(next)); i += 1; }
|
|
109
|
+
} else if (token.startsWith(flag + '=')) {
|
|
110
|
+
values.push(token.slice(flag.length + 1));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (values.length > 1) return { code: 'duplicate_flag', error: `${flag} may be specified only once` };
|
|
114
|
+
if (values.length === 1 && values[0] == null) return { code: 'missing_flag_value', error: `${flag} requires a value` };
|
|
115
|
+
if (values.length === 1 && !values[0].trim()) return { code: 'empty_flag_value', error: `${flag} requires a non-empty value` };
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function _singletonFlagGroupIssue(argv, flags, label) {
|
|
120
|
+
const list = Array.isArray(argv) ? argv : [];
|
|
121
|
+
let count = 0;
|
|
122
|
+
for (let i = 0; i < list.length; i++) {
|
|
123
|
+
const token = String(list[i] == null ? '' : list[i]);
|
|
124
|
+
if (token === '--') break;
|
|
125
|
+
const matched = flags.find(flag => token === flag || token.startsWith(flag + '='));
|
|
126
|
+
if (!matched) continue;
|
|
127
|
+
count += 1;
|
|
128
|
+
if (token === matched && i + 1 < list.length && !String(list[i + 1]).startsWith('--')) i += 1;
|
|
129
|
+
}
|
|
130
|
+
return count > 1 ? { code: 'duplicate_flag', error: `${label || flags.join('/')} may be specified only once` } : null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function _identityAssertionMatches(assertedValue, actualValue) {
|
|
134
|
+
const asserted = normalizeExecutorIdentity(assertedValue, { allowLegacyProviderIds: true });
|
|
135
|
+
const actual = normalizeExecutorIdentity(actualValue, { allowLegacyProviderIds: true });
|
|
136
|
+
const comparable = [
|
|
137
|
+
['provider', value => String(value).toLowerCase()],
|
|
138
|
+
['model', value => String(value)],
|
|
139
|
+
['modelFamily', value => String(value).toLowerCase()],
|
|
140
|
+
['sessionId', value => String(value)],
|
|
141
|
+
];
|
|
142
|
+
for (const [key, normalize] of comparable) {
|
|
143
|
+
if (asserted[key] && (!actual[key] || normalize(asserted[key]) !== normalize(actual[key]))) return false;
|
|
144
|
+
}
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function _generatedTaskId(prefix = 'task') {
|
|
149
|
+
return `${prefix}-${crypto.randomUUID ? crypto.randomUUID() : crypto.randomBytes(16).toString('hex')}`;
|
|
150
|
+
}
|
|
151
|
+
|
|
43
152
|
function agentsCmd(root, sub, args = [], deps = {}) {
|
|
44
|
-
const { VERSION, has, arg, _agentSlashHint, _allProviders, _checkAgent, _cliChat, _dispatchCommand, _harnessBrief, _loadEnvFile, _normalizeRole, _policyEnforce, _readUserProviders, _recommendAgent, _recordRun, _resolveRole, lessonsPath, taskLogPath } = deps;
|
|
153
|
+
const { VERSION, has, arg, _agentSlashHint, _allProviders, _checkAgent, _cliChat, _dispatchCommand, _harnessBrief, _loadEnvFile, _normalizeRole, _policyEnforce, _readUserProviders, _recommendAgent, _recordRun, _resolveRole, _withLock, lessonsPath, taskLogPath } = deps;
|
|
45
154
|
const _spawnPortable = deps._spawnPortable || spawnPortable;
|
|
46
155
|
root = absRoot(root || process.cwd());
|
|
47
156
|
const en = deps.uiLang === 'en';
|
|
48
157
|
const t = (ko, enText) => (en ? enText : ko);
|
|
158
|
+
// Security-sensitive selectors are singletons. `arg()` intentionally returns
|
|
159
|
+
// the first occurrence for broad backwards compatibility, but role/policy and
|
|
160
|
+
// provenance gates must never let a later duplicate be silently ignored.
|
|
161
|
+
const singletonFlags = new Set(_AGENT_VALUE_FLAGS);
|
|
162
|
+
const rawArgv = Array.isArray(deps.rawArgv) ? deps.rawArgv : process.argv.slice(2);
|
|
163
|
+
for (const flag of singletonFlags) {
|
|
164
|
+
const issue = _singletonFlagIssue(rawArgv, flag);
|
|
165
|
+
if (issue) { failJson(has('--json'), issue.code, issue.error); return; }
|
|
166
|
+
}
|
|
167
|
+
for (const [flags, label] of [
|
|
168
|
+
[['--to', '--provider'], '--to/--provider'],
|
|
169
|
+
[['--model', '-m'], '--model/-m'],
|
|
170
|
+
]) {
|
|
171
|
+
const issue = _singletonFlagGroupIssue(rawArgv, flags, label);
|
|
172
|
+
if (issue) { failJson(has('--json'), issue.code, issue.error); return; }
|
|
173
|
+
}
|
|
49
174
|
// 1.9.435 (11th 외부평가 Codex P2, UR-0137): dispatch/multi task 파싱 — flag 값이 task 본문에 흡수되던 버그 수정.
|
|
50
175
|
// 상위(bin)에서 args 는 '--to' flag 만 제거되고 값(codex)은 positional 로 남아 기존 filter 가 task 에 흡수시켰음.
|
|
51
176
|
// → flag 에서 break + lib 가 소비하는 값-flag(--to/--model/--role/--only) 값을 제외. 명시 task 는 --task 폴백.
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
for (const a of args) { if (a.startsWith('-')) break; if (consumed.has(a)) continue; p.push(a); }
|
|
56
|
-
return p.join(' ').trim() || arg('--task', null);
|
|
57
|
-
};
|
|
177
|
+
const _positionalText = (start = 0) => _parseAgentPositional(args, start);
|
|
178
|
+
const _taskArg = () => _positionalText(0) || arg('--task', null);
|
|
179
|
+
const _taskArgFrom = (start = 0) => _positionalText(start) || arg('--task', null);
|
|
58
180
|
// .env 자동 로드 (1.9.22)
|
|
59
181
|
_loadEnvFile(root);
|
|
60
182
|
_loadEnvFile(path.join(root, '..'));
|
|
61
183
|
|
|
184
|
+
|
|
185
|
+
const _ledgerEvent = (event) => {
|
|
186
|
+
try { return appendExecutionEvent(root, event, { withLock: _withLock }); }
|
|
187
|
+
catch (error) { return { ok: false, error: error && error.message ? error.message : String(error), code: error && error.code ? error.code : 'ledger_write_failed' }; }
|
|
188
|
+
};
|
|
189
|
+
const _commitRoleResolutionEvent = (result, event) => {
|
|
190
|
+
if (!result || !result.snapshot) return _ledgerEvent(event);
|
|
191
|
+
if (typeof _withLock !== 'function') {
|
|
192
|
+
return { ok: false, code: 'resolution_commit_unavailable', error: 'role resolution commit requires the canonical lock' };
|
|
193
|
+
}
|
|
194
|
+
try {
|
|
195
|
+
return _withLock(roleStore.rolesFile(root), () => {
|
|
196
|
+
const currentRoleState = roleStore.readRoleStore(root);
|
|
197
|
+
if (!currentRoleState.ok) throw new roleStore.RoleStoreError(currentRoleState);
|
|
198
|
+
if (currentRoleState.revision !== result.snapshot.roleStoreRevision) {
|
|
199
|
+
return { ok: false, code: 'resolution_stale', error: 'role configuration changed after resolution; resolve again' };
|
|
200
|
+
}
|
|
201
|
+
return _withLock(executionLedgerPath(root), () => {
|
|
202
|
+
const currentAvailability = readAvailabilityObservations(root, 2000);
|
|
203
|
+
if (currentAvailability.ok === false || currentAvailability.partial) {
|
|
204
|
+
return {
|
|
205
|
+
ok: false,
|
|
206
|
+
code: currentAvailability.code || 'availability_history_partial',
|
|
207
|
+
error: currentAvailability.error || 'availability observation history is incomplete',
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
if (currentAvailability.revision !== result.snapshot.availabilityLedgerRevision) {
|
|
211
|
+
return { ok: false, code: 'resolution_stale', error: 'availability policy changed after resolution; resolve again' };
|
|
212
|
+
}
|
|
213
|
+
return appendExecutionEvent(root, {
|
|
214
|
+
...event,
|
|
215
|
+
resolutionId: result.resolutionId,
|
|
216
|
+
roleStoreRevision: result.snapshot.roleStoreRevision,
|
|
217
|
+
availabilityLedgerRevision: result.snapshot.availabilityLedgerRevision,
|
|
218
|
+
riskDowngrade: result.cls && result.cls.riskDowngrade || null,
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
} catch (error) {
|
|
223
|
+
if (error instanceof roleStore.RoleStoreError) {
|
|
224
|
+
const publicFailure = roleStore.publicError(error.roleStoreState);
|
|
225
|
+
return { ok: false, code: publicFailure.code, error: publicFailure.error, roleStore: publicFailure };
|
|
226
|
+
}
|
|
227
|
+
return { ok: false, code: (error && error.code) || 'resolution_commit_failed', error: (error && error.message) || String(error) };
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
const _commitTerminalEvent = (event) => {
|
|
231
|
+
if (typeof _withLock !== 'function') {
|
|
232
|
+
return { ok: false, code: 'provenance_commit_unavailable', error: 'terminal provenance requires the canonical ledger lock' };
|
|
233
|
+
}
|
|
234
|
+
try {
|
|
235
|
+
return _withLock(executionLedgerPath(root), () => {
|
|
236
|
+
let canonicalEvent;
|
|
237
|
+
try { canonicalEvent = normalizeExecutionEvent(event); }
|
|
238
|
+
catch (error) {
|
|
239
|
+
return { ok: false, code: (error && error.code) || 'provenance_event_invalid', error: (error && error.message) || String(error) };
|
|
240
|
+
}
|
|
241
|
+
const history = readExecutionEvents(root, 2000, { preserveAll: true, maxBytes: 64 * 1024 * 1024 });
|
|
242
|
+
if (history.ok === false || history.truncated) {
|
|
243
|
+
return { ok: false, code: history.code || 'ledger_history_partial', error: history.error || 'execution ledger history is incomplete' };
|
|
244
|
+
}
|
|
245
|
+
const normalizedHistory = history.events.map(item => normalizeExecutionEvent(item));
|
|
246
|
+
const duplicate = normalizedHistory.some(item => item
|
|
247
|
+
&& item.attemptId === canonicalEvent.attemptId
|
|
248
|
+
&& ['execution.completed', 'execution.failed', 'review.completed', 'validation.completed'].includes(item.event));
|
|
249
|
+
if (duplicate) return { ok: false, code: 'attempt_already_terminal', error: `attempt already has a terminal event: ${canonicalEvent.attemptId}` };
|
|
250
|
+
|
|
251
|
+
const resolveParent = (targetId, allowedEvents, prefix, noun, matchEventId = false) => {
|
|
252
|
+
const candidates = normalizedHistory.filter(item => item && allowedEvents.includes(item.event)
|
|
253
|
+
&& (item.attemptId === targetId || (matchEventId && item.eventId === targetId)));
|
|
254
|
+
const sameTask = candidates.filter(item => item.taskId === canonicalEvent.taskId);
|
|
255
|
+
if (!sameTask.length) {
|
|
256
|
+
return {
|
|
257
|
+
error: candidates.length
|
|
258
|
+
? { ok: false, code: `${prefix}_target_task_mismatch`, error: `${noun} target ${targetId} belongs to a different task` }
|
|
259
|
+
: { ok: false, code: `${prefix}_target_not_found`, error: `${noun} target not found: ${targetId}` },
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
if (sameTask.length !== 1) {
|
|
263
|
+
return { error: { ok: false, code: `${prefix}_target_ambiguous`, error: `${noun} target is ambiguous: ${targetId}` } };
|
|
264
|
+
}
|
|
265
|
+
return { target: sameTask[0] };
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
if (canonicalEvent.event === 'review.completed') {
|
|
269
|
+
const targetId = canonicalEvent.reviewOfAttemptId;
|
|
270
|
+
const resolved = resolveParent(targetId, ['execution.completed', 'execution.failed'], 'review', 'review implementation');
|
|
271
|
+
if (resolved.error) return resolved.error;
|
|
272
|
+
const target = resolved.target;
|
|
273
|
+
if (!target.actualExecutor || !target.actualExecutor.provider) {
|
|
274
|
+
return { ok: false, code: 'review_target_executor_missing', error: `review target has no executor identity: ${targetId}` };
|
|
275
|
+
}
|
|
276
|
+
const assertedImplementer = canonicalEvent.review && canonicalEvent.review.reviewOf;
|
|
277
|
+
if (assertedImplementer && !_identityAssertionMatches(assertedImplementer, target.actualExecutor)) {
|
|
278
|
+
return { ok: false, code: 'review_target_identity_mismatch', error: `declared implementer does not match review target: ${targetId}` };
|
|
279
|
+
}
|
|
280
|
+
const independence = assessReviewerIndependence(canonicalEvent.actualExecutor, target.actualExecutor);
|
|
281
|
+
canonicalEvent = {
|
|
282
|
+
...canonicalEvent,
|
|
283
|
+
parentAttemptId: target.attemptId,
|
|
284
|
+
reviewOfAttemptId: target.attemptId,
|
|
285
|
+
reviewerIndependent: independence.reviewerIndependent,
|
|
286
|
+
review: {
|
|
287
|
+
reviewOfAttemptId: target.attemptId,
|
|
288
|
+
reviewOf: independence.implementer,
|
|
289
|
+
reviewerIndependent: independence.reviewerIndependent,
|
|
290
|
+
reviewerIndependence: independence.status,
|
|
291
|
+
independenceBasis: independence.basis,
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
} else if (canonicalEvent.event === 'validation.completed') {
|
|
295
|
+
const targetId = canonicalEvent.parentAttemptId;
|
|
296
|
+
const resolved = resolveParent(
|
|
297
|
+
targetId,
|
|
298
|
+
['execution.completed', 'execution.failed', 'review.completed'],
|
|
299
|
+
'validation',
|
|
300
|
+
'validation',
|
|
301
|
+
);
|
|
302
|
+
if (resolved.error) return resolved.error;
|
|
303
|
+
canonicalEvent = { ...canonicalEvent, parentAttemptId: resolved.target.attemptId };
|
|
304
|
+
} else if (['execution.completed', 'execution.failed'].includes(canonicalEvent.event)
|
|
305
|
+
&& canonicalEvent.parentAttemptId) {
|
|
306
|
+
const targetId = canonicalEvent.parentAttemptId;
|
|
307
|
+
const resolved = resolveParent(
|
|
308
|
+
targetId,
|
|
309
|
+
['routing.proposed', 'fallback.selected', 'dispatch.prepared', 'execution.started'],
|
|
310
|
+
'execution',
|
|
311
|
+
'role execution',
|
|
312
|
+
true,
|
|
313
|
+
);
|
|
314
|
+
if (resolved.error) return resolved.error;
|
|
315
|
+
const target = resolved.target;
|
|
316
|
+
const targetRole = target.requestedRole || null;
|
|
317
|
+
if (canonicalEvent.requestedRole && targetRole && canonicalEvent.requestedRole !== targetRole) {
|
|
318
|
+
return { ok: false, code: 'execution_target_role_mismatch', error: `terminal role does not match parent role: ${targetId}` };
|
|
319
|
+
}
|
|
320
|
+
if (canonicalEvent.requestedRole && !targetRole) {
|
|
321
|
+
return { ok: false, code: 'execution_target_role_mismatch', error: `role terminal event targets a non-role parent: ${targetId}` };
|
|
322
|
+
}
|
|
323
|
+
const expectedExecutor = target.actualExecutor && target.actualExecutor.provider
|
|
324
|
+
? target.actualExecutor : target.requestedExecutor;
|
|
325
|
+
if (!expectedExecutor || !expectedExecutor.provider) {
|
|
326
|
+
return { ok: false, code: 'execution_target_executor_missing', error: `execution parent has no executor identity: ${targetId}` };
|
|
327
|
+
}
|
|
328
|
+
if (!_identityAssertionMatches(expectedExecutor, canonicalEvent.actualExecutor)) {
|
|
329
|
+
return { ok: false, code: 'execution_target_identity_mismatch', error: `terminal executor does not match execution parent: ${targetId}` };
|
|
330
|
+
}
|
|
331
|
+
canonicalEvent = {
|
|
332
|
+
...canonicalEvent,
|
|
333
|
+
parentAttemptId: target.attemptId || target.eventId,
|
|
334
|
+
requestedRole: targetRole || canonicalEvent.requestedRole || null,
|
|
335
|
+
role: targetRole || canonicalEvent.requestedRole || null,
|
|
336
|
+
requestedExecutor: target.requestedExecutor || canonicalEvent.requestedExecutor || null,
|
|
337
|
+
};
|
|
338
|
+
} else if (['execution.completed', 'execution.failed'].includes(canonicalEvent.event)
|
|
339
|
+
&& canonicalEvent.requestedRole) {
|
|
340
|
+
return { ok: false, code: 'execution_target_required', error: 'role terminal event requires a parent routing or dispatch target' };
|
|
341
|
+
}
|
|
342
|
+
return appendExecutionEvent(root, canonicalEvent);
|
|
343
|
+
});
|
|
344
|
+
} catch (error) {
|
|
345
|
+
return { ok: false, code: (error && error.code) || 'provenance_commit_failed', error: (error && error.message) || String(error) };
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
const _sessionIdentity = () => {
|
|
349
|
+
const base = sessionIdentityFromEnv(process.env);
|
|
350
|
+
const explicit = [
|
|
351
|
+
'--session-id', '--session-provider', '--session-model', '--session-model-family',
|
|
352
|
+
].some(flag => has(flag));
|
|
353
|
+
return normalizeExecutorIdentity({
|
|
354
|
+
sessionId: arg('--session-id', base.sessionId),
|
|
355
|
+
provider: arg('--session-provider', base.provider),
|
|
356
|
+
model: arg('--session-model', base.model),
|
|
357
|
+
modelFamily: arg('--session-model-family', base.modelFamily),
|
|
358
|
+
identitySource: explicit ? 'user-declared' : base.identitySource,
|
|
359
|
+
}, { allowLegacyProviderIds: true });
|
|
360
|
+
};
|
|
361
|
+
const _failRoleResolution = (result) => {
|
|
362
|
+
if (result && result.roleStoreFailure) {
|
|
363
|
+
const payload = { ok: false, ...result.roleStoreFailure };
|
|
364
|
+
if (has('--json')) log(JSON.stringify(payload, null, 2));
|
|
365
|
+
else fail(payload.error || '역할 설정 파일이 유효하지 않습니다');
|
|
366
|
+
process.exitCode = 1;
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
failJson(has('--json'), result && result.code || 'role_resolution_failed', result && result.error || 'role resolution failed');
|
|
370
|
+
};
|
|
371
|
+
const _roleResolution = (roleArg, task) => {
|
|
372
|
+
const role = _normalizeRole(roleArg);
|
|
373
|
+
let resolved;
|
|
374
|
+
try { resolved = _resolveRole(root, role); }
|
|
375
|
+
catch (error) {
|
|
376
|
+
if (error instanceof roleStore.RoleStoreError) {
|
|
377
|
+
const roleStoreFailure = roleStore.publicError(error.roleStoreState);
|
|
378
|
+
return { ok: false, code: roleStoreFailure.code, error: roleStoreFailure.error, roleStoreFailure, role };
|
|
379
|
+
}
|
|
380
|
+
return { ok: false, code: (error && error.code) || 'role_store_error', error: (error && error.message) || 'agent-roles.json을 읽을 수 없습니다', role };
|
|
381
|
+
}
|
|
382
|
+
if (!resolved) return { ok: false, code: 'role_unconfigured', error: `역할 미설정: ${role}`, role };
|
|
383
|
+
let roleStoreRevision = resolved.storeRevision || null;
|
|
384
|
+
if (!roleStoreRevision) {
|
|
385
|
+
const roleState = roleStore.readRoleStore(root);
|
|
386
|
+
if (!roleState.ok) {
|
|
387
|
+
const roleStoreFailure = roleStore.publicError(roleState);
|
|
388
|
+
return { ok: false, code: roleStoreFailure.code, error: roleStoreFailure.error, roleStoreFailure, role };
|
|
389
|
+
}
|
|
390
|
+
roleStoreRevision = roleState.revision;
|
|
391
|
+
}
|
|
392
|
+
const requestedPreset = arg('--preset', null);
|
|
393
|
+
if (requestedPreset != null && !FALLBACK_POLICIES.includes(String(requestedPreset).trim().toLowerCase())) {
|
|
394
|
+
return { ok: false, code: 'invalid_fallback_policy', error: `--preset must be one of ${FALLBACK_POLICIES.join('|')}`, role };
|
|
395
|
+
}
|
|
396
|
+
const classified = classify(task, { tier: arg('--tier', '') || '' });
|
|
397
|
+
const cls = applyDeclaredRiskFloor(classified, {
|
|
398
|
+
approvedBy: arg('--approved-by', null),
|
|
399
|
+
reason: arg('--reason', null),
|
|
400
|
+
});
|
|
401
|
+
if (!cls.ok) return { ok: false, code: cls.code, error: cls.error, role };
|
|
402
|
+
const providers = _allProviders(root);
|
|
403
|
+
const availabilityState = readAvailabilityObservations(root, 2000);
|
|
404
|
+
if (availabilityState.ok === false || availabilityState.partial) {
|
|
405
|
+
return {
|
|
406
|
+
ok: false,
|
|
407
|
+
code: availabilityState.code || 'availability_history_partial',
|
|
408
|
+
error: availabilityState.error || 'availability observation history is incomplete',
|
|
409
|
+
role,
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
const resolution = resolveRoleFallback({
|
|
413
|
+
role,
|
|
414
|
+
roleDefinition: resolved,
|
|
415
|
+
allowLegacyProviderIds: true,
|
|
416
|
+
providers,
|
|
417
|
+
tier: cls.tier,
|
|
418
|
+
policy: requestedPreset == null ? (resolved.fallbackPolicy || 'balanced') : String(requestedPreset).trim().toLowerCase(),
|
|
419
|
+
sessionIdentity: _sessionIdentity(),
|
|
420
|
+
implementerIdentity: normalizeExecutorIdentity({
|
|
421
|
+
provider: arg('--agent', null),
|
|
422
|
+
model: arg('--agent-model', null),
|
|
423
|
+
modelFamily: arg('--agent-model-family', null),
|
|
424
|
+
sessionId: arg('--agent-session', null),
|
|
425
|
+
identitySource: 'user-declared',
|
|
426
|
+
}, { allowLegacyProviderIds: true }),
|
|
427
|
+
checkProvider: (definition) => _checkAgent(definition, {
|
|
428
|
+
auth: !!(definition && definition.envFlag && process.env[definition.envFlag] === '1'),
|
|
429
|
+
}),
|
|
430
|
+
availabilityExtras: candidate => availabilityExtrasForCandidate(availabilityState, candidate),
|
|
431
|
+
});
|
|
432
|
+
resolution.availabilityObservationState = {
|
|
433
|
+
activeCount: availabilityState.activeCount,
|
|
434
|
+
partial: availabilityState.partial,
|
|
435
|
+
truncated: availabilityState.truncated,
|
|
436
|
+
};
|
|
437
|
+
const resolutionId = `res-${crypto.randomUUID ? crypto.randomUUID() : crypto.randomBytes(16).toString('hex')}`;
|
|
438
|
+
resolution.resolutionId = resolutionId;
|
|
439
|
+
resolution.roleStoreRevision = roleStoreRevision;
|
|
440
|
+
resolution.availabilityLedgerRevision = availabilityState.revision || null;
|
|
441
|
+
resolution.riskDowngrade = cls.riskDowngrade || null;
|
|
442
|
+
return {
|
|
443
|
+
ok: true,
|
|
444
|
+
role,
|
|
445
|
+
resolved,
|
|
446
|
+
cls,
|
|
447
|
+
resolution,
|
|
448
|
+
resolutionId,
|
|
449
|
+
snapshot: {
|
|
450
|
+
roleStoreRevision,
|
|
451
|
+
availabilityLedgerRevision: availabilityState.revision || null,
|
|
452
|
+
},
|
|
453
|
+
};
|
|
454
|
+
};
|
|
455
|
+
const _renderResolution = (result, task) => {
|
|
456
|
+
const r = result.resolution;
|
|
457
|
+
log(t(`# 역할 실행자 해석: ${r.role} · 위험도 ${r.tier} · 정책 ${r.policy}`, `# Role executor resolution: ${r.role} · risk ${r.tier} · policy ${r.policy}`));
|
|
458
|
+
if (r.primary) {
|
|
459
|
+
log(t(
|
|
460
|
+
`기본: ${r.primary.provider}${r.primary.model ? ' / ' + r.primary.model : ''} [${r.primary.availability.status}]`,
|
|
461
|
+
`Primary: ${r.primary.provider}${r.primary.model ? ' / ' + r.primary.model : ''} [${r.primary.availability.status}]`,
|
|
462
|
+
));
|
|
463
|
+
} else log(t('기본 실행자: 미설정', 'Primary executor: unconfigured'));
|
|
464
|
+
log(t(`추천: ${r.decision.recommendedOptionId} (${r.decision.recommendationReason})`, `Recommended: ${r.decision.recommendedOptionId} (${r.decision.recommendationReason})`));
|
|
465
|
+
log('');
|
|
466
|
+
log(t('## 선택지', '## Choices'));
|
|
467
|
+
for (const option of r.options) {
|
|
468
|
+
const mark = option.selectable ? '✓' : '×';
|
|
469
|
+
const executor = option.executor && option.executor.provider
|
|
470
|
+
? `${option.executor.provider}${option.executor.model ? ' / ' + option.executor.model : ''}`
|
|
471
|
+
: option.executor && option.executor.identitySource
|
|
472
|
+
? `${option.executor.provider || 'unknown'} / ${option.executor.model || 'unknown'} (${option.executor.identitySource})`
|
|
473
|
+
: '-';
|
|
474
|
+
const blockers = option.availability && option.availability.blockingReasons && option.availability.blockingReasons.length
|
|
475
|
+
? ` · blocked=${option.availability.blockingReasons.join(',')}` : '';
|
|
476
|
+
log(` ${mark} ${option.id} → ${executor}${blockers}`);
|
|
477
|
+
}
|
|
478
|
+
log('');
|
|
479
|
+
log(t('선택 명령:', 'Selection commands:'));
|
|
480
|
+
log(` leerness agents fallback provider ${JSON.stringify(task)} --role ${r.role} --to <provider> [--model <model>]`);
|
|
481
|
+
log(` leerness agents fallback session ${JSON.stringify(task)} --role ${r.role}`);
|
|
482
|
+
log(` leerness agents fallback direct ${JSON.stringify(task)} --role ${r.role}`);
|
|
483
|
+
log(` leerness agents fallback hold ${JSON.stringify(task)} --role ${r.role}`);
|
|
484
|
+
if (r.tier === 'high-risk') log(t(' 고위험 대체는 --approved-by "<승인자>"가 필요합니다.', ' High-risk substitution requires --approved-by "<approver>".'));
|
|
485
|
+
};
|
|
486
|
+
|
|
62
487
|
if (!sub || sub === 'list') {
|
|
63
488
|
// 1.9.157: Provider Registry 통합 — 빌트인 5종 + 사용자 정의 provider 포함
|
|
64
489
|
const providers = _allProviders(root);
|
|
65
|
-
const userIds = new Set(_readUserProviders(root).map(u => u.id));
|
|
490
|
+
const userIds = new Set(_readUserProviders(root).map(u => canonicalLegacyProviderIdentity(u && u.id)).filter(Boolean));
|
|
66
491
|
const checks = providers.map(a => ({ ...(_checkAgent(a)), source: userIds.has(a.id) ? 'user' : 'builtin' }));
|
|
67
492
|
if (has('--json')) { log(JSON.stringify({ agents: checks }, null, 2)); return; }
|
|
68
493
|
log(t(`# 외부 AI CLI 오케스트레이션 (1.9.30)`, `# External AI CLI orchestration (1.9.30)`));
|
|
@@ -129,6 +554,314 @@ function agentsCmd(root, sub, args = [], deps = {}) {
|
|
|
129
554
|
return;
|
|
130
555
|
}
|
|
131
556
|
|
|
557
|
+
if (sub === 'availability') {
|
|
558
|
+
const found = _findAgentAction(args, new Set(['list', 'mark', 'set', 'clear', 'remove']));
|
|
559
|
+
const action = found.action || 'list';
|
|
560
|
+
if (action === 'list') {
|
|
561
|
+
const state = readAvailabilityObservations(root, parseInt(arg('--limit', '200'), 10));
|
|
562
|
+
if (has('--json')) {
|
|
563
|
+
log(JSON.stringify(state, null, 2));
|
|
564
|
+
if (state.ok === false || state.partial) process.exitCode = 1;
|
|
565
|
+
return state;
|
|
566
|
+
}
|
|
567
|
+
log(t(`# agents availability — active ${state.activeCount}`, `# agents availability — active ${state.activeCount}`));
|
|
568
|
+
for (const item of state.observations) {
|
|
569
|
+
const axes = [
|
|
570
|
+
item.authenticated && `auth=${item.authenticated}`,
|
|
571
|
+
item.modelEntitled && `entitlement=${item.modelEntitled}`,
|
|
572
|
+
item.quota && `quota=${item.quota}`,
|
|
573
|
+
item.policyAllowed && `policy=${item.policyAllowed}`,
|
|
574
|
+
item.reachable && `reachable=${item.reachable}`,
|
|
575
|
+
item.rateLimited && 'rate-limited=true',
|
|
576
|
+
].filter(Boolean).join(' · ');
|
|
577
|
+
log(` ${item.provider}${item.model ? '/' + item.model : ''} — ${item.reason || 'manual-observation'}${axes ? ' · ' + axes : ''}${item.expiresAt ? ' · expires=' + item.expiresAt : ''}`);
|
|
578
|
+
}
|
|
579
|
+
if (state.ok === false || state.partial) { warn(state.error || state.code || 'availability history partial'); process.exitCode = 1; }
|
|
580
|
+
return state;
|
|
581
|
+
}
|
|
582
|
+
const positional = found.index >= 0 ? _parseAgentPositional(args, found.index + 1) : '';
|
|
583
|
+
const provider = (positional.split(/\s+/).filter(Boolean)[0] || arg('--to', null) || arg('--provider', null));
|
|
584
|
+
if (!provider) { failJson(has('--json'), 'availability_provider_required', 'agents availability mark|clear <provider> [--model <id>]'); return; }
|
|
585
|
+
try {
|
|
586
|
+
const written = (action === 'clear' || action === 'remove')
|
|
587
|
+
? appendAvailabilityClear(root, { provider, model: arg('--model', null), reason: arg('--reason', 'manual-clear'), source: 'user-declared' }, { withLock: _withLock })
|
|
588
|
+
: appendAvailabilityObservation(root, {
|
|
589
|
+
provider,
|
|
590
|
+
model: arg('--model', null),
|
|
591
|
+
modelFamily: arg('--model-family', null),
|
|
592
|
+
reason: arg('--reason', null),
|
|
593
|
+
authenticated: arg('--auth-state', null),
|
|
594
|
+
modelEntitled: arg('--entitlement', null),
|
|
595
|
+
quota: arg('--quota-state', null),
|
|
596
|
+
policyAllowed: arg('--policy-state', null),
|
|
597
|
+
reachable: arg('--reachability', null),
|
|
598
|
+
rateLimited: has('--rate-limited') ? true : has('--not-rate-limited') ? false : null,
|
|
599
|
+
retryAfter: arg('--retry-after', null),
|
|
600
|
+
expiresAt: arg('--expires-at', null),
|
|
601
|
+
ttlMin: arg('--ttl-min', null),
|
|
602
|
+
source: 'user-declared',
|
|
603
|
+
}, { withLock: _withLock });
|
|
604
|
+
const state = readAvailabilityObservations(root, 2000);
|
|
605
|
+
if (state.ok === false || state.partial) {
|
|
606
|
+
const error = new Error(state.error || 'availability observation history is incomplete after append');
|
|
607
|
+
error.code = state.code || 'availability_history_partial';
|
|
608
|
+
throw error;
|
|
609
|
+
}
|
|
610
|
+
const out = { ok: true, action: action === 'clear' || action === 'remove' ? 'cleared' : 'marked', event: written.event, active: state.observations, file: written.file };
|
|
611
|
+
if (has('--json')) { log(JSON.stringify(out, null, 2)); return out; }
|
|
612
|
+
ok(t(`availability ${out.action}: ${provider}${arg('--model', null) ? '/' + arg('--model', null) : ''}`, `availability ${out.action}: ${provider}${arg('--model', null) ? '/' + arg('--model', null) : ''}`));
|
|
613
|
+
return out;
|
|
614
|
+
} catch (error) {
|
|
615
|
+
failJson(has('--json'), (error && error.code) || 'availability_write_failed', (error && error.message) || String(error));
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
if (sub === 'resolve') {
|
|
621
|
+
const task = _taskArg();
|
|
622
|
+
const roleArg = arg('--role', null);
|
|
623
|
+
if (!task || !roleArg) { failJson(has('--json'), 'role_and_task_required', 'agents resolve "<task>" --role <role>'); return; }
|
|
624
|
+
const result = _roleResolution(roleArg, task);
|
|
625
|
+
if (!result.ok) { _failRoleResolution(result); return; }
|
|
626
|
+
const taskId = _generatedTaskId('task');
|
|
627
|
+
const audit = _commitRoleResolutionEvent(result, {
|
|
628
|
+
event: 'routing.proposed',
|
|
629
|
+
taskId,
|
|
630
|
+
task: task.slice(0, 300),
|
|
631
|
+
role: result.role,
|
|
632
|
+
tier: result.cls.tier,
|
|
633
|
+
policy: result.resolution.policy,
|
|
634
|
+
requestedExecutor: result.resolution.primary ? { provider: result.resolution.primary.provider, model: result.resolution.primary.model } : null,
|
|
635
|
+
decision: result.resolution.decision,
|
|
636
|
+
options: result.resolution.options.map(o => ({ id: o.id, kind: o.kind, executor: o.executor || null, selectable: o.selectable, requiresConfirmation: o.requiresConfirmation })),
|
|
637
|
+
executed: false,
|
|
638
|
+
});
|
|
639
|
+
if (!audit.ok) { failJson(has('--json'), audit.code || 'resolution_commit_failed', audit.error || 'role resolution could not be committed'); return; }
|
|
640
|
+
const out = { ok: true, task, taskId, role: result.role, classification: result.cls, resolution: result.resolution, auditWritten: audit.ok === true, auditEventId: audit.event.eventId, auditError: null };
|
|
641
|
+
if (has('--json')) { log(JSON.stringify(out, null, 2)); return out; }
|
|
642
|
+
_renderResolution(result, task);
|
|
643
|
+
log(` provenance: task=${taskId} · target=${audit.event.eventId}`);
|
|
644
|
+
if (!audit.ok) warn(`실행 이력 기록 실패: ${audit.error}`);
|
|
645
|
+
return out;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
if (sub === 'fallback') {
|
|
649
|
+
const action = _findAgentAction(args, new Set(['primary', 'provider', 'model', 'session', 'session-takeover', 'takeover', 'direct', 'worker', 'session-direct', 'hold', 'defer', 'pause']));
|
|
650
|
+
const choice = action.action || '';
|
|
651
|
+
const task = action.index >= 0 ? (_parseAgentPositional(args, action.index + 1) || arg('--task', null)) : null;
|
|
652
|
+
const roleArg = arg('--role', null);
|
|
653
|
+
if (!choice || !task || !roleArg) { failJson(has('--json'), 'fallback_arguments_required', 'agents fallback provider|session|direct|hold "<task>" --role <role>'); return; }
|
|
654
|
+
const result = _roleResolution(roleArg, task);
|
|
655
|
+
if (!result.ok) { _failRoleResolution(result); return; }
|
|
656
|
+
const taskId = arg('--id', null) || _generatedTaskId('task');
|
|
657
|
+
const selection = selectFallbackOption(result.resolution, {
|
|
658
|
+
choice,
|
|
659
|
+
provider: arg('--to', null) || arg('--provider', null),
|
|
660
|
+
model: arg('--model', null),
|
|
661
|
+
approvedBy: arg('--approved-by', null),
|
|
662
|
+
});
|
|
663
|
+
if (!selection.ok) { failJson(has('--json'), selection.code, selection.error); return; }
|
|
664
|
+
const option = selection.option;
|
|
665
|
+
if (option.executor && option.executor.provider && ['primary', 'compatible-model', 'compatible-provider'].includes(option.kind)) {
|
|
666
|
+
const delegatedTask = (has('--raw') || typeof _harnessBrief !== 'function') ? task : (_harnessBrief() + task);
|
|
667
|
+
const optionDefinition = _allProviders(root).find(definition => definition.id === option.executor.provider) || null;
|
|
668
|
+
try { option.command = _dispatchCommand(option.executor.provider, delegatedTask, has('--write'), option.executor.model, optionDefinition); }
|
|
669
|
+
catch (error) { failJson(has('--json'), (error && error.code) || 'dispatch_command_invalid', (error && error.message) || 'dispatch command could not be prepared'); return; }
|
|
670
|
+
}
|
|
671
|
+
const eventName = option.kind === 'hold' ? 'task.held' : 'fallback.selected';
|
|
672
|
+
const audit = _commitRoleResolutionEvent(result, {
|
|
673
|
+
event: eventName,
|
|
674
|
+
task: task.slice(0, 300),
|
|
675
|
+
taskId,
|
|
676
|
+
role: result.role,
|
|
677
|
+
tier: result.cls.tier,
|
|
678
|
+
policy: result.resolution.policy,
|
|
679
|
+
requestedExecutor: result.resolution.primary ? { provider: result.resolution.primary.provider, model: result.resolution.primary.model } : null,
|
|
680
|
+
actualExecutor: option.executor || null,
|
|
681
|
+
substitution: {
|
|
682
|
+
occurred: option.kind !== 'primary' && option.kind !== 'hold',
|
|
683
|
+
kind: option.kind,
|
|
684
|
+
reason: result.resolution.primary && result.resolution.primary.availability
|
|
685
|
+
? result.resolution.primary.availability.blockingReasons : ['primary-unconfigured'],
|
|
686
|
+
approvedBy: selection.approvedBy,
|
|
687
|
+
},
|
|
688
|
+
contract: option.contract || null,
|
|
689
|
+
executed: false,
|
|
690
|
+
});
|
|
691
|
+
if (!audit.ok) { failJson(has('--json'), audit.code, `fallback 선택을 기록하지 못해 적용하지 않았습니다: ${audit.error}`); return; }
|
|
692
|
+
const out = {
|
|
693
|
+
ok: true,
|
|
694
|
+
action: option.kind,
|
|
695
|
+
role: result.role,
|
|
696
|
+
tier: result.cls.tier,
|
|
697
|
+
selected: option,
|
|
698
|
+
command: option.command || null,
|
|
699
|
+
contract: option.contract ? { ...option.contract, task: task.slice(0, 300), session: option.executor || null } : null,
|
|
700
|
+
executed: false,
|
|
701
|
+
auditEventId: audit.event.eventId,
|
|
702
|
+
taskId,
|
|
703
|
+
};
|
|
704
|
+
if (has('--json')) { log(JSON.stringify(out, null, 2)); return out; }
|
|
705
|
+
if (option.command) {
|
|
706
|
+
log(`# provenance: task=${taskId} · target=${audit.event.eventId}`);
|
|
707
|
+
log(t(`# fallback 선택: ${option.kind} → ${option.executor.provider}${option.executor.model ? ' / ' + option.executor.model : ''}`, `# Fallback selected: ${option.kind} → ${option.executor.provider}${option.executor.model ? ' / ' + option.executor.model : ''}`));
|
|
708
|
+
log(option.command);
|
|
709
|
+
log(t(' ⓘ 명령만 준비했으며 아직 모델을 호출하지 않았습니다.', ' ⓘ Command prepared; no model has been called yet.'));
|
|
710
|
+
} else if (option.kind === 'hold') {
|
|
711
|
+
ok(t(`작업 보류 기록: 역할 ${result.role}`, `Work hold recorded: role ${result.role}`));
|
|
712
|
+
} else {
|
|
713
|
+
log(`# ${option.kind}`);
|
|
714
|
+
log(JSON.stringify(out.contract, null, 2));
|
|
715
|
+
log(t(' ⓘ 현재 채팅 Host가 이 계약을 받아 역할을 수행해야 합니다.', ' ⓘ The current chat host must consume this contract and perform the role.'));
|
|
716
|
+
}
|
|
717
|
+
return out;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
if (sub === 'record') {
|
|
721
|
+
const action = _findAgentAction(args, new Set(['start', 'started', 'partial', 'complete', 'completed', 'fail', 'failed', 'review', 'reviewed', 'validate', 'validated']));
|
|
722
|
+
const rawEvent = action.action || '';
|
|
723
|
+
const eventMap = {
|
|
724
|
+
start: 'execution.started', started: 'execution.started',
|
|
725
|
+
partial: 'execution.partial', complete: 'execution.completed', completed: 'execution.completed',
|
|
726
|
+
fail: 'execution.failed', failed: 'execution.failed',
|
|
727
|
+
review: 'review.completed', reviewed: 'review.completed',
|
|
728
|
+
validate: 'validation.completed', validated: 'validation.completed',
|
|
729
|
+
};
|
|
730
|
+
const eventName = eventMap[rawEvent];
|
|
731
|
+
const summary = (action.index >= 0 ? _parseAgentPositional(args, action.index + 1) : '') || arg('--result', null) || arg('--summary', null);
|
|
732
|
+
if (!eventName || !summary) { failJson(has('--json'), 'record_arguments_required', 'agents record started|partial|completed|failed|reviewed|validated "<summary>" [--task T-ID]'); return; }
|
|
733
|
+
const session = sessionIdentityFromEnv(process.env);
|
|
734
|
+
const explicitProvider = arg('--to', null) || arg('--provider', null);
|
|
735
|
+
const explicitModel = arg('--model', null);
|
|
736
|
+
const explicitFamily = arg('--model-family', null);
|
|
737
|
+
const explicitSessionId = arg('--session-id', null);
|
|
738
|
+
const explicitIdentity = !!(explicitProvider || explicitModel || explicitFamily || explicitSessionId);
|
|
739
|
+
const explicitProviderIdentity = explicitProvider ? canonicalLegacyProviderIdentity(explicitProvider) : null;
|
|
740
|
+
const ambientProviderIdentity = session.provider ? canonicalLegacyProviderIdentity(session.provider) : null;
|
|
741
|
+
// MCP supplies both its host provider and the ambient session key. Preserve
|
|
742
|
+
// that known identity when the explicitly named provider is the same host;
|
|
743
|
+
// only a genuinely different executor authority loses ambient provenance.
|
|
744
|
+
const sameAmbientProvider = !explicitProvider
|
|
745
|
+
|| !!(explicitProviderIdentity && ambientProviderIdentity
|
|
746
|
+
&& explicitProviderIdentity === ambientProviderIdentity);
|
|
747
|
+
const sessionBoundCall = ['mcp-session-key', 'mcp-connection'].includes(session.identitySource);
|
|
748
|
+
const actualExecutor = normalizeExecutorIdentity({
|
|
749
|
+
provider: explicitProvider || session.provider,
|
|
750
|
+
model: explicitModel || (sameAmbientProvider ? session.model : null),
|
|
751
|
+
modelFamily: explicitFamily || (sameAmbientProvider ? session.modelFamily : null),
|
|
752
|
+
// An explicit MCP session key identifies the caller independently of the
|
|
753
|
+
// provider it reports. Dropping it lets one MCP caller manufacture two
|
|
754
|
+
// provider identities and certify its own work as independent.
|
|
755
|
+
sessionId: explicitSessionId || ((sameAmbientProvider || sessionBoundCall) ? session.sessionId : null),
|
|
756
|
+
identitySource: explicitIdentity ? 'user-declared' : session.identitySource,
|
|
757
|
+
}, { allowLegacyProviderIds: true });
|
|
758
|
+
const implementer = normalizeExecutorIdentity({
|
|
759
|
+
provider: arg('--agent', null),
|
|
760
|
+
model: arg('--agent-model', null),
|
|
761
|
+
modelFamily: arg('--agent-model-family', null),
|
|
762
|
+
sessionId: arg('--agent-session', null),
|
|
763
|
+
identitySource: 'user-declared',
|
|
764
|
+
}, { allowLegacyProviderIds: true });
|
|
765
|
+
const evidence = arg('--evidence', null);
|
|
766
|
+
const requestedRoleRaw = arg('--role', null);
|
|
767
|
+
const requestedRole = requestedRoleRaw == null ? null : _normalizeRole(requestedRoleRaw);
|
|
768
|
+
const reviewOfAttemptId = arg('--target', null);
|
|
769
|
+
const taskId = arg('--task', null);
|
|
770
|
+
const attemptId = arg('--id', null);
|
|
771
|
+
const terminal = ['execution.completed', 'execution.failed', 'review.completed', 'validation.completed'].includes(eventName);
|
|
772
|
+
const event = {
|
|
773
|
+
event: eventName,
|
|
774
|
+
taskId,
|
|
775
|
+
sessionId: actualExecutor.sessionId,
|
|
776
|
+
attemptId,
|
|
777
|
+
parentAttemptId: reviewOfAttemptId,
|
|
778
|
+
requestedRole,
|
|
779
|
+
role: requestedRole,
|
|
780
|
+
actualExecutor,
|
|
781
|
+
status: arg('--status', rawEvent),
|
|
782
|
+
result: { summary: String(summary).slice(0, 1000), evidence },
|
|
783
|
+
evidenceRefs: evidence ? [evidence] : [],
|
|
784
|
+
reviewOfAttemptId: eventName === 'review.completed' ? reviewOfAttemptId : null,
|
|
785
|
+
reviewerIndependent: null,
|
|
786
|
+
review: eventName === 'review.completed' ? {
|
|
787
|
+
reviewOfAttemptId,
|
|
788
|
+
// This is only a caller assertion. _commitTerminalEvent binds it to the
|
|
789
|
+
// referenced implementation event under the ledger lock, rejects a
|
|
790
|
+
// contradiction, and derives the authoritative identity/independence.
|
|
791
|
+
reviewOf: implementer,
|
|
792
|
+
reviewerIndependent: null,
|
|
793
|
+
reviewerIndependence: 'unverified',
|
|
794
|
+
independenceBasis: 'review-target-unresolved',
|
|
795
|
+
} : null,
|
|
796
|
+
executed: true,
|
|
797
|
+
};
|
|
798
|
+
let canonicalEvent;
|
|
799
|
+
try { canonicalEvent = normalizeExecutionEvent(event); }
|
|
800
|
+
catch (error) { failJson(has('--json'), (error && error.code) || 'provenance_event_invalid', (error && error.message) || String(error)); return; }
|
|
801
|
+
if (terminal) {
|
|
802
|
+
// Reject identifiers whose persisted value would differ from the caller's
|
|
803
|
+
// value (whitespace/control cleanup, secret redaction, or truncation). The
|
|
804
|
+
// same canonical bytes are then used for required-field and CAS checks.
|
|
805
|
+
const changed = [
|
|
806
|
+
['--task', taskId, canonicalEvent.taskId],
|
|
807
|
+
['--id', attemptId, canonicalEvent.attemptId],
|
|
808
|
+
['--target', reviewOfAttemptId, canonicalEvent.parentAttemptId],
|
|
809
|
+
['--role', requestedRoleRaw, canonicalEvent.requestedRole],
|
|
810
|
+
['--provider/--to', explicitProvider, canonicalEvent.actualExecutor && canonicalEvent.actualExecutor.provider],
|
|
811
|
+
['--model', explicitModel, canonicalEvent.actualExecutor && canonicalEvent.actualExecutor.model],
|
|
812
|
+
['--model-family', explicitFamily, canonicalEvent.actualExecutor && canonicalEvent.actualExecutor.modelFamily],
|
|
813
|
+
['--session-id', explicitSessionId, canonicalEvent.actualExecutor && canonicalEvent.actualExecutor.sessionId],
|
|
814
|
+
].find(([, raw, normalized]) => raw != null && String(raw) !== normalized);
|
|
815
|
+
if (changed) {
|
|
816
|
+
failJson(has('--json'), 'invalid_provenance_identifier', `${changed[0]} must already be canonical, non-empty, and within 200 characters`);
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
const missing = [];
|
|
820
|
+
if (!canonicalEvent.taskId) missing.push('--task');
|
|
821
|
+
if (!canonicalEvent.attemptId) missing.push('--id');
|
|
822
|
+
if (!canonicalEvent.actualExecutor || !canonicalEvent.actualExecutor.provider) missing.push('--provider/--to (or trusted session provider)');
|
|
823
|
+
if (!canonicalEvent.evidenceRefs.length) missing.push('--evidence');
|
|
824
|
+
if (['review.completed', 'validation.completed'].includes(eventName) && !canonicalEvent.parentAttemptId) missing.push('--target');
|
|
825
|
+
if (canonicalEvent.requestedRole && !canonicalEvent.parentAttemptId) missing.push('--target (role selection/attempt link)');
|
|
826
|
+
if (missing.length) {
|
|
827
|
+
failJson(has('--json'), 'provenance_fields_required', `terminal provenance requires ${missing.join(', ')}`);
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
const audit = terminal ? _commitTerminalEvent(canonicalEvent) : _ledgerEvent(canonicalEvent);
|
|
832
|
+
if (!audit.ok) { failJson(has('--json'), audit.code, audit.error); return; }
|
|
833
|
+
const out = { ok: true, event: audit.event, file: audit.file };
|
|
834
|
+
if (has('--json')) { log(JSON.stringify(out, null, 2)); return out; }
|
|
835
|
+
ok(t(`execution provenance 기록: ${audit.event.event} · ${audit.event.eventId}`, `execution provenance recorded: ${audit.event.event} · ${audit.event.eventId}`));
|
|
836
|
+
log(` ${audit.file}`);
|
|
837
|
+
return out;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
if (sub === 'history') {
|
|
841
|
+
const history = readExecutionEvents(root, parseInt(arg('--limit', '20'), 10));
|
|
842
|
+
if (history.ok === false && history.code !== 'ledger_invalid_lines') {
|
|
843
|
+
failJson(has('--json'), history.code || 'ledger_read_failed', history.error || 'execution ledger를 읽을 수 없습니다');
|
|
844
|
+
return history;
|
|
845
|
+
}
|
|
846
|
+
if (has('--json')) {
|
|
847
|
+
log(JSON.stringify(history, null, 2));
|
|
848
|
+
if (history.invalidLines) process.exitCode = 1;
|
|
849
|
+
return history;
|
|
850
|
+
}
|
|
851
|
+
log(t(`# agents history — 최근 ${history.events.length}건`, `# agents history — latest ${history.events.length} event(s)`));
|
|
852
|
+
for (const event of history.events) {
|
|
853
|
+
const who = event.actualExecutor && event.actualExecutor.provider
|
|
854
|
+
? `${event.actualExecutor.provider}${event.actualExecutor.model ? '/' + event.actualExecutor.model : ''}` : '-';
|
|
855
|
+
log(` ${String(event.at || '').slice(0, 19)} ${String(event.event || '').padEnd(22)} ${String(event.role || '-').padEnd(11)} ${who}`);
|
|
856
|
+
}
|
|
857
|
+
if (history.invalidLines) {
|
|
858
|
+
warn(t(`해석하지 못한 ledger line ${history.invalidLines}건 (유효 이벤트는 보존)`, `${history.invalidLines} execution-ledger line(s) could not be parsed (valid events were preserved)`));
|
|
859
|
+
process.exitCode = 1;
|
|
860
|
+
}
|
|
861
|
+
if (history.truncated) log(t(' ⓘ 최근 4MB 범위만 읽었습니다.', ' ⓘ Only the latest 4 MB was read.'));
|
|
862
|
+
return history;
|
|
863
|
+
}
|
|
864
|
+
|
|
132
865
|
// 1.36.78 (10차 헌트 F5): review-request 가 광고하는 `agents recommend <task>` 가 핸들러 부재로 usage(exit 1)만 뱉던 것.
|
|
133
866
|
// 내부 _recommendAgent 로직을 정식 서브명령으로 노출 — 작업 텍스트/유형 → 적합 CLI + 이유.
|
|
134
867
|
if (sub === 'recommend') {
|
|
@@ -148,7 +881,7 @@ function agentsCmd(root, sub, args = [], deps = {}) {
|
|
|
148
881
|
// 1.36.91: `check` 는 **명시적 재확인** 명령이므로 인증 확인까지 수행한다(`list` 는 지연 때문에 하지 않는다).
|
|
149
882
|
// 확인 가능한 provider 만 ok/no 로 판정하고 나머지는 unknown 으로 남긴다 — 설치됨을 사용가능이라 부르지 않는다.
|
|
150
883
|
const providers = _allProviders(root);
|
|
151
|
-
const userIds = new Set(_readUserProviders(root).map(u => u.id));
|
|
884
|
+
const userIds = new Set(_readUserProviders(root).map(u => canonicalLegacyProviderIdentity(u && u.id)).filter(Boolean));
|
|
152
885
|
const checks = providers.map(a => ({ ...(_checkAgent(a, { auth: true })), source: userIds.has(a.id) ? 'user' : 'builtin' }));
|
|
153
886
|
const authNote = '설치·활성은 확인했습니다. 인증은 무료·비대화형 확인 명령이 있는 provider 만 판정했고(auth: ok|no), 나머지는 unknown 입니다 — 쿼터·과금 권한은 어느 provider 도 확인하지 않았습니다.';
|
|
154
887
|
if (has('--json')) {
|
|
@@ -179,13 +912,17 @@ function agentsCmd(root, sub, args = [], deps = {}) {
|
|
|
179
912
|
if (sub === 'multi') {
|
|
180
913
|
const task = _taskArg();
|
|
181
914
|
if (!task) { fail('multi "<task>" 또는 --task 필요'); return process.exit(1); }
|
|
915
|
+
if (arg('--role', null)) {
|
|
916
|
+
failJson(has('--json'), 'role_multi_execution_unsupported', 'agents multi cannot enforce one role/model contract across multiple provider defaults; use agents resolve then dispatch/fallback');
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
182
919
|
// 1.36.29 (사용자 보고): dispatch 만 위임 브리프를 접두하고 multi(--execute 실 spawn 포함)는 원문 그대로 위임돼
|
|
183
920
|
// 백그라운드 AI 가 leerness 프로토콜(handoff 적재/task 등록/evidence/session close)을 받지 못했다. dispatch 와 동일 규칙: --raw 로 옵트아웃.
|
|
184
921
|
const briefTask = (has('--raw') || typeof _harnessBrief !== 'function') ? task : (_harnessBrief() + task);
|
|
185
922
|
const onlyArg = arg('--only', null); // 'claude,codex' 처럼 콤마 구분 — 활성 중에서 추가 필터
|
|
186
923
|
const writeMode = has('--write');
|
|
187
924
|
const execute = has('--execute'); // 1.9.156: 명령 출력 → 실제 spawn + consensus 합의
|
|
188
|
-
const checks =
|
|
925
|
+
const checks = _allProviders(root).map(a => ({ def: a, status: _checkAgent(a) }));
|
|
189
926
|
let ready = checks.filter(x => x.status.status === 'ready');
|
|
190
927
|
if (onlyArg) {
|
|
191
928
|
const wanted = new Set(onlyArg.split(/[,\s]+/).filter(Boolean));
|
|
@@ -205,6 +942,23 @@ function agentsCmd(root, sub, args = [], deps = {}) {
|
|
|
205
942
|
if (execute) {
|
|
206
943
|
return (async () => {
|
|
207
944
|
const timeout = parseInt(arg('--timeout', '60'), 10) * 1000;
|
|
945
|
+
const executionTaskId = arg('--id', null) || _generatedTaskId('multi');
|
|
946
|
+
const batchAttemptId = `attempt-${crypto.randomUUID ? crypto.randomUUID() : crypto.randomBytes(16).toString('hex')}`;
|
|
947
|
+
const batchAudit = _ledgerEvent({
|
|
948
|
+
event: 'execution.batch.started',
|
|
949
|
+
taskId: executionTaskId,
|
|
950
|
+
task: task.slice(0, 300),
|
|
951
|
+
attemptId: batchAttemptId,
|
|
952
|
+
role: arg('--role', null) ? _normalizeRole(arg('--role', null)) : null,
|
|
953
|
+
mode: writeMode ? 'write' : 'read-only',
|
|
954
|
+
result: { summary: 'agents multi --execute prepared', targets: ready.map(x => x.def.id) },
|
|
955
|
+
executed: false,
|
|
956
|
+
});
|
|
957
|
+
if (!batchAudit.ok) {
|
|
958
|
+
failJson(has('--json'), batchAudit.code || 'ledger_write_failed', `외부 모델 호출 전 실행 원장을 기록하지 못해 중단했습니다: ${batchAudit.error}`);
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
const batchEventId = batchAudit.event.eventId;
|
|
208
962
|
if (!has('--json')) {
|
|
209
963
|
log(`# leerness agents multi --execute (1.9.156) — ${ready.length}개 활성 에이전트 병렬 호출`);
|
|
210
964
|
log(`task: ${task.slice(0, 120)}${task.length > 120 ? '…' : ''}`);
|
|
@@ -217,19 +971,45 @@ function agentsCmd(root, sub, args = [], deps = {}) {
|
|
|
217
971
|
// 병렬 _cliChat 호출 (sandbox 자동: runCommandSafe + env scrub + observability)
|
|
218
972
|
const results = await Promise.all(ready.map(async ({ def }) => {
|
|
219
973
|
const start = Date.now();
|
|
220
|
-
|
|
221
|
-
|
|
974
|
+
let r;
|
|
975
|
+
try { r = await _cliChat(root, def.id, briefTask, { timeout }); }
|
|
976
|
+
catch (error) { r = { ok: false, response: '', error: error && error.message ? error.message : String(error), executed: false }; }
|
|
977
|
+
const result = {
|
|
222
978
|
agent: def.id,
|
|
979
|
+
model: null,
|
|
980
|
+
identitySource: 'cli-default-unverified',
|
|
981
|
+
executed: r.executed !== false,
|
|
223
982
|
elapsed: Date.now() - start,
|
|
224
|
-
ok: r.ok,
|
|
983
|
+
ok: !!r.ok,
|
|
225
984
|
response: r.response || '',
|
|
226
985
|
error: r.error || null,
|
|
227
|
-
responseTokens: Math.ceil((r.response || '').length / 4)
|
|
986
|
+
responseTokens: Math.ceil((r.response || '').length / 4),
|
|
228
987
|
};
|
|
988
|
+
const resultAudit = _ledgerEvent({
|
|
989
|
+
event: result.ok ? 'execution.completed' : 'execution.failed',
|
|
990
|
+
taskId: executionTaskId,
|
|
991
|
+
parentAttemptId: batchEventId,
|
|
992
|
+
attemptId: `${batchAttemptId}:${def.id}`,
|
|
993
|
+
task: task.slice(0, 300),
|
|
994
|
+
role: arg('--role', null) ? _normalizeRole(arg('--role', null)) : null,
|
|
995
|
+
actualExecutor: normalizeExecutorIdentity({ provider: def.id, model: null, identitySource: result.identitySource }),
|
|
996
|
+
mode: writeMode ? 'write' : 'read-only',
|
|
997
|
+
status: result.ok ? 'completed' : 'failed',
|
|
998
|
+
result: {
|
|
999
|
+
summary: result.ok ? 'agents multi --execute completed' : `agents multi --execute failed: ${result.error || 'unknown error'}`,
|
|
1000
|
+
evidence: `elapsedMs=${result.elapsed}; responseChars=${result.response.length}`,
|
|
1001
|
+
},
|
|
1002
|
+
executed: result.executed,
|
|
1003
|
+
});
|
|
1004
|
+
result.auditWritten = resultAudit.ok === true;
|
|
1005
|
+
result.auditEventId = resultAudit.ok === true ? resultAudit.event.eventId : null;
|
|
1006
|
+
result.auditError = resultAudit.ok === true ? null : resultAudit.error;
|
|
1007
|
+
return result;
|
|
229
1008
|
}));
|
|
230
1009
|
const totalElapsed = Date.now() - t0;
|
|
231
1010
|
const ok = results.filter(r => r.ok);
|
|
232
1011
|
const failures = results.filter(r => !r.ok);
|
|
1012
|
+
const auditFailures = results.filter(r => !r.auditWritten);
|
|
233
1013
|
_recordRun(root, { kind: 'agents_multi_execute', count: ready.length, success: ok.length, durationMs: totalElapsed, task: task.slice(0, 200) });
|
|
234
1014
|
// 1.9.155 consensus 로직 재사용 — multi-signal scoring (tokens + overlap + lengthFit)
|
|
235
1015
|
let best = null, scored = [];
|
|
@@ -262,8 +1042,10 @@ function agentsCmd(root, sub, args = [], deps = {}) {
|
|
|
262
1042
|
task, count: ready.length, success: ok.length, totalElapsedMs: totalElapsed,
|
|
263
1043
|
results: scored.length ? scored : results,
|
|
264
1044
|
best: best ? { agent: best.agent, score: best.score, response: best.response } : null,
|
|
265
|
-
failures
|
|
1045
|
+
failures,
|
|
1046
|
+
provenance: { taskId: executionTaskId, batchEventId, complete: !!executionTaskId && auditFailures.length === 0, failedWrites: auditFailures.map(r => ({ agent: r.agent, error: r.auditError })) },
|
|
266
1047
|
}, null, 2));
|
|
1048
|
+
if (auditFailures.length) process.exitCode = 1;
|
|
267
1049
|
return;
|
|
268
1050
|
}
|
|
269
1051
|
log(`\n## 결과: ${ok.length}/${ready.length} 성공 · 총 ${totalElapsed}ms (병렬)`);
|
|
@@ -302,6 +1084,10 @@ function agentsCmd(root, sub, args = [], deps = {}) {
|
|
|
302
1084
|
} catch {}
|
|
303
1085
|
}
|
|
304
1086
|
}
|
|
1087
|
+
if (auditFailures.length) {
|
|
1088
|
+
warn(`실행 provenance 기록 실패 ${auditFailures.length}건 — 결과를 완료 증거로 사용하지 마세요.`);
|
|
1089
|
+
process.exitCode = 1;
|
|
1090
|
+
}
|
|
305
1091
|
if (failures.length && !best) {
|
|
306
1092
|
process.exitCode = 1;
|
|
307
1093
|
}
|
|
@@ -311,7 +1097,7 @@ function agentsCmd(root, sub, args = [], deps = {}) {
|
|
|
311
1097
|
log(JSON.stringify({
|
|
312
1098
|
task, count: ready.length,
|
|
313
1099
|
agents: ready.map(x => ({ id: x.def.id, version: x.status.version })),
|
|
314
|
-
commands: ready.map(x => _dispatchCommand(x.def.id, briefTask, writeMode)),
|
|
1100
|
+
commands: ready.map(x => _dispatchCommand(x.def.id, briefTask, writeMode, null, x.def)),
|
|
315
1101
|
// 1.9.266 (UR-0021 2단계): 각 에이전트 슬래시 명령 힌트 — sub-agent 가 알맞은 슬래시 사용
|
|
316
1102
|
slashCommands: ready.reduce((acc, x) => { const h = _agentSlashHint(root, x.def.id); if (h) acc[x.def.id] = { invoke: h.invoke, commands: h.commands.map(c => c.cmd) }; return acc; }, {})
|
|
317
1103
|
}, null, 2));
|
|
@@ -327,7 +1113,7 @@ function agentsCmd(root, sub, args = [], deps = {}) {
|
|
|
327
1113
|
for (const { def, status } of ready) {
|
|
328
1114
|
log(`### [${def.id}] (v${status.version || '?'})`);
|
|
329
1115
|
log('```sh');
|
|
330
|
-
log(_dispatchCommand(def.id, briefTask, writeMode));
|
|
1116
|
+
log(_dispatchCommand(def.id, briefTask, writeMode, null, def));
|
|
331
1117
|
log('```');
|
|
332
1118
|
// 1.9.266 (UR-0021 2단계): 에이전트별 슬래시 명령 힌트
|
|
333
1119
|
try {
|
|
@@ -354,48 +1140,172 @@ function agentsCmd(root, sub, args = [], deps = {}) {
|
|
|
354
1140
|
}
|
|
355
1141
|
// 1.9.270: --role <role> — 설정된 역할 → provider+model 라우팅 (--to 없을 때)
|
|
356
1142
|
const roleArg = arg('--role', null);
|
|
1143
|
+
const roleTaskId = roleArg ? _generatedTaskId('task') : null;
|
|
357
1144
|
let roleModel = arg('--model', null);
|
|
358
1145
|
let rolePersona = '';
|
|
1146
|
+
let resolvedRole = null;
|
|
1147
|
+
let roleResolutionResult = null;
|
|
1148
|
+
if (roleArg) {
|
|
1149
|
+
roleResolutionResult = _roleResolution(roleArg, task);
|
|
1150
|
+
if (!roleResolutionResult.ok) {
|
|
1151
|
+
_failRoleResolution(roleResolutionResult);
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
resolvedRole = roleResolutionResult.resolved;
|
|
1155
|
+
}
|
|
359
1156
|
if (roleArg && !target) {
|
|
360
|
-
const resolved =
|
|
1157
|
+
const resolved = resolvedRole;
|
|
361
1158
|
if (!resolved) { fail(`역할 미설정: ${_normalizeRole(roleArg)} — leerness roles set ${_normalizeRole(roleArg)} --provider <id> 또는 roles suggest --apply`); return process.exit(1); }
|
|
362
1159
|
target = resolved.provider;
|
|
363
1160
|
if (!roleModel) roleModel = resolved.model;
|
|
364
1161
|
rolePersona = resolved.persona || '';
|
|
365
|
-
|
|
366
|
-
|
|
1162
|
+
if (!has('--json')) {
|
|
1163
|
+
log(`🎭 역할 ${_normalizeRole(roleArg)} → ${target}${roleModel ? ' / ' + roleModel : ''}`);
|
|
1164
|
+
if (rolePersona) log(` persona: ${rolePersona}`);
|
|
1165
|
+
}
|
|
367
1166
|
}
|
|
368
1167
|
if (!target) { fail('--to <agent_id> 또는 --role <role> 필요 (claude/codex/agy/grok/copilot) — 활성 전체 일괄은 `leerness agents multi`'); return process.exit(1); }
|
|
369
|
-
const agentDef =
|
|
1168
|
+
const agentDef = _allProviders(root).find(a => a.id === target);
|
|
370
1169
|
if (!agentDef) { fail(`알 수 없는 agent: ${target}`); return process.exit(1); }
|
|
371
1170
|
// 1.9.36: 작업 유형 키워드 분석 → 최적 CLI 추천 (ready 체크 전에 출력 — 비활성이어도 추천)
|
|
372
1171
|
const recommendation = _recommendAgent(task);
|
|
373
1172
|
const recommended = recommendation.target;
|
|
374
1173
|
if (recommended && recommended !== target) {
|
|
375
|
-
log(`💡 추천: 이 작업은 ${recommended}가 더 적합 (${recommendation.reason})`);
|
|
1174
|
+
if (!has('--json')) log(`💡 추천: 이 작업은 ${recommended}가 더 적합 (${recommendation.reason})`);
|
|
376
1175
|
}
|
|
377
1176
|
const status = _checkAgent(agentDef);
|
|
378
1177
|
if (status.status !== 'ready') {
|
|
379
|
-
|
|
380
|
-
|
|
1178
|
+
if (roleArg && resolvedRole) {
|
|
1179
|
+
const result = roleResolutionResult || _roleResolution(roleArg, task);
|
|
1180
|
+
const audit = result.ok ? _commitRoleResolutionEvent(result, {
|
|
1181
|
+
event: 'routing.blocked', task: task.slice(0, 300), role: result.role, tier: result.cls.tier,
|
|
1182
|
+
taskId: roleTaskId,
|
|
1183
|
+
requestedExecutor: { provider: target, model: roleModel || null },
|
|
1184
|
+
reason: status.status, decision: result.resolution.decision, executed: false,
|
|
1185
|
+
}) : { ok: false, error: result.error };
|
|
1186
|
+
if (has('--json')) {
|
|
1187
|
+
log(JSON.stringify({ ok: false, code: 'role_executor_unavailable', taskId: roleTaskId, role: _normalizeRole(roleArg), target, status, resolution: result.ok ? result.resolution : null, auditWritten: audit.ok === true, auditEventId: audit.ok === true ? audit.event.eventId : null, auditError: audit.ok === true ? null : audit.error }, null, 2));
|
|
1188
|
+
process.exitCode = 1;
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
fail(`${target} 비활성 (${status.status}). 역할을 유지한 대체 선택지를 표시합니다.`);
|
|
1192
|
+
if (result.ok) _renderResolution(result, task);
|
|
1193
|
+
if (!audit.ok) warn(`실행 이력 기록 실패: ${audit.error}`);
|
|
1194
|
+
process.exitCode = 1;
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
failJson(has('--json'), 'agent_unavailable', `${target} 비활성 (${status.status}). 환경변수 ${agentDef.envFlag}=1 + CLI 설치 필요.`);
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
let selectedRoleOption = null;
|
|
1201
|
+
if (roleArg && roleResolutionResult) {
|
|
1202
|
+
const primary = roleResolutionResult.resolution.primary;
|
|
1203
|
+
const primarySelected = !!(primary
|
|
1204
|
+
&& target === primary.provider
|
|
1205
|
+
&& (!roleModel || roleModel === primary.model));
|
|
1206
|
+
const selection = selectFallbackOption(roleResolutionResult.resolution, {
|
|
1207
|
+
choice: primarySelected ? 'primary' : 'provider',
|
|
1208
|
+
provider: target,
|
|
1209
|
+
model: roleModel,
|
|
1210
|
+
approvedBy: arg('--approved-by', null),
|
|
1211
|
+
});
|
|
1212
|
+
if (!selection.ok) {
|
|
1213
|
+
const blocked = {
|
|
1214
|
+
ok: false,
|
|
1215
|
+
code: selection.code,
|
|
1216
|
+
error: selection.error,
|
|
1217
|
+
role: roleResolutionResult.role,
|
|
1218
|
+
target,
|
|
1219
|
+
model: roleModel || null,
|
|
1220
|
+
resolution: roleResolutionResult.resolution,
|
|
1221
|
+
};
|
|
1222
|
+
if (has('--json')) log(JSON.stringify(blocked, null, 2));
|
|
1223
|
+
else {
|
|
1224
|
+
fail(selection.error);
|
|
1225
|
+
_renderResolution(roleResolutionResult, task);
|
|
1226
|
+
}
|
|
1227
|
+
process.exitCode = 1;
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
1230
|
+
selectedRoleOption = selection.option;
|
|
1231
|
+
if (selectedRoleOption.executor) {
|
|
1232
|
+
target = selectedRoleOption.executor.provider;
|
|
1233
|
+
roleModel = selectedRoleOption.executor.model || null;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
if (roleModel && !isValidModelIdentifier(roleModel)) {
|
|
1237
|
+
failJson(has('--json'), 'invalid_model_identifier', 'model is not a safe model identifier');
|
|
1238
|
+
return;
|
|
381
1239
|
}
|
|
382
1240
|
// 1.9.36: --write 시 파일 수정 가능 권장 플래그 자동 첨부, 미명시 시 read-only 안전 모드
|
|
383
1241
|
const writeMode = has('--write');
|
|
384
1242
|
const readOnly = has('--readonly') || !writeMode;
|
|
385
1243
|
// 실제 호출은 안 함 — 프롬프트만 생성 (사용자가 명시적으로 실행)
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
1244
|
+
if (!has('--json')) {
|
|
1245
|
+
log(`# leerness agents dispatch (1.9.36)`);
|
|
1246
|
+
log(`대상: ${target} (${agentDef.bin})`);
|
|
1247
|
+
log(`상태: 🟢 ready, 버전 ${status.version || '?'}`);
|
|
1248
|
+
log(`모드: ${writeMode ? '✏ write (파일 수정 가능)' : '🔒 read-only (분석 전용, 안전)'}`);
|
|
1249
|
+
log('');
|
|
1250
|
+
log(`## 실행 명령 (사용자가 복사해서 실행)`);
|
|
1251
|
+
if (roleModel) log(`# 🎭 모델: ${roleModel} (역할 기반 라우팅, 1.9.270)`);
|
|
1252
|
+
log('');
|
|
1253
|
+
}
|
|
394
1254
|
// 1.9.270: _dispatchCommand 로 통일 (roleModel 주입) — 명령 빌더 단일화
|
|
395
1255
|
// 1.35.6 (18th 위임실증): harness 계약 브리프 자동 접두 — codex 실측(0.141)에서 AGENTS.md 로드 시 준수는 확인됐으나,
|
|
396
1256
|
// cwd 가 프로젝트 루트가 아니거나 AGENTS.md 미지원 CLI(aider/qwen 등)면 계약이 전달되지 않음 → 프롬프트 접두가 안전망. --raw 로 원문 위임.
|
|
397
1257
|
const dispatchTask = (has('--raw') || typeof _harnessBrief !== 'function') ? task : (_harnessBrief() + task);
|
|
398
|
-
|
|
1258
|
+
let preparedCommand;
|
|
1259
|
+
const dispatchDefinition = target === agentDef.id
|
|
1260
|
+
? agentDef
|
|
1261
|
+
: (_allProviders(root).find(definition => definition.id === target) || null);
|
|
1262
|
+
try { preparedCommand = _dispatchCommand(target, dispatchTask, writeMode, roleModel, dispatchDefinition); }
|
|
1263
|
+
catch (error) { failJson(has('--json'), (error && error.code) || 'dispatch_command_invalid', (error && error.message) || 'dispatch command could not be prepared'); return; }
|
|
1264
|
+
const requestedExecutor = resolvedRole ? normalizeExecutorIdentity({
|
|
1265
|
+
provider: resolvedRole.provider,
|
|
1266
|
+
model: resolvedRole.model || null,
|
|
1267
|
+
modelFamily: resolvedRole.modelFamily || null,
|
|
1268
|
+
identitySource: 'role-config',
|
|
1269
|
+
}, { allowLegacyProviderIds: true }) : null;
|
|
1270
|
+
const actualExecutor = selectedRoleOption && selectedRoleOption.executor
|
|
1271
|
+
? normalizeExecutorIdentity({ ...selectedRoleOption.executor, identitySource: 'role-resolution' }, { allowLegacyProviderIds: true })
|
|
1272
|
+
: normalizeExecutorIdentity({ provider: target, model: roleModel || null, identitySource: roleArg ? 'role-config' : 'user-declared' }, { allowLegacyProviderIds: true });
|
|
1273
|
+
const substituted = !!(requestedExecutor && (requestedExecutor.provider !== actualExecutor.provider || (requestedExecutor.model || null) !== (actualExecutor.model || null)));
|
|
1274
|
+
const audit = roleArg && roleResolutionResult
|
|
1275
|
+
? _commitRoleResolutionEvent(roleResolutionResult, {
|
|
1276
|
+
event: 'dispatch.prepared',
|
|
1277
|
+
taskId: roleTaskId,
|
|
1278
|
+
task: task.slice(0, 300),
|
|
1279
|
+
role: roleArg ? _normalizeRole(roleArg) : null,
|
|
1280
|
+
requestedExecutor,
|
|
1281
|
+
actualExecutor,
|
|
1282
|
+
substitution: { occurred: substituted, kind: substituted ? 'explicit-provider-or-model' : 'none' },
|
|
1283
|
+
mode: writeMode ? 'write' : 'read-only',
|
|
1284
|
+
commandPrepared: true,
|
|
1285
|
+
executed: false,
|
|
1286
|
+
})
|
|
1287
|
+
: _ledgerEvent({
|
|
1288
|
+
event: 'dispatch.prepared',
|
|
1289
|
+
task: task.slice(0, 300),
|
|
1290
|
+
role: null,
|
|
1291
|
+
requestedExecutor: null,
|
|
1292
|
+
actualExecutor,
|
|
1293
|
+
substitution: { occurred: false, kind: 'none' },
|
|
1294
|
+
mode: writeMode ? 'write' : 'read-only',
|
|
1295
|
+
commandPrepared: true,
|
|
1296
|
+
executed: false,
|
|
1297
|
+
});
|
|
1298
|
+
if (roleArg && !audit.ok) {
|
|
1299
|
+
failJson(has('--json'), audit.code || 'ledger_write_failed', `역할 실행 명령을 기록하지 못해 준비를 중단했습니다: ${audit.error}`);
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
if (has('--json')) {
|
|
1303
|
+
log(JSON.stringify({ ok: true, action: 'dispatch-prepared', taskId: roleTaskId, role: roleArg ? _normalizeRole(roleArg) : null, target, model: roleModel || null, status, mode: writeMode ? 'write' : 'read-only', command: preparedCommand, executed: false, auditWritten: audit.ok === true, auditEventId: audit.ok === true ? audit.event.eventId : null, auditError: audit.ok === true ? null : audit.error }, null, 2));
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
log(preparedCommand);
|
|
1307
|
+
if (roleArg && audit.ok) log(`# provenance: task=${roleTaskId} · target=${audit.event.eventId}`);
|
|
1308
|
+
if (!audit.ok) warn(`실행 이력 기록 실패: ${audit.error}`);
|
|
399
1309
|
if (!has('--raw') && typeof _harnessBrief === 'function') log(`# ℹ harness 위임 브리프 자동 접두 (1.35.6) — 원문만 위임하려면 --raw`);
|
|
400
1310
|
if (target === 'claude' && writeMode) log(`# ⚠ --dangerously-skip-permissions: 도구 권한 자동 승인 (파일 수정 가능)`);
|
|
401
1311
|
if (target === 'codex') { log(`# ℹ codex는 PowerShell 경유 — POSIX /tmp 경로는 C:\\tmp\\로 해석됨`); log(`# ⚠ 비대화형 spawn 시 stdin 을 닫고 실행 — 열린 파이프면 codex 가 'Reading additional input from stdin...' EOF 대기로 hang (1.35.6 실측)`); if (writeMode) log(`# ⚠ --dangerously-bypass-approvals-and-sandbox: sandbox 우회`); }
|
|
@@ -431,18 +1341,58 @@ function agentsCmd(root, sub, args = [], deps = {}) {
|
|
|
431
1341
|
// 1.9.36: 같은 prompt를 ready CLI 모두에 동시 호출 + 시간/응답 길이/exit code 비교
|
|
432
1342
|
const task = _taskArg();
|
|
433
1343
|
if (!task) { fail('bench "<task>" 필요'); return process.exit(1); }
|
|
1344
|
+
if (arg('--role', null)) {
|
|
1345
|
+
failJson(has('--json'), 'role_benchmark_execution_unsupported', 'agents bench uses provider-default models and cannot prove a role/model contract; use agents resolve then dispatch/fallback');
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
434
1348
|
const timeoutS = parseInt(arg('--timeout', '60'), 10);
|
|
435
1349
|
const writeMode = has('--write');
|
|
1350
|
+
const benchTaskId = arg('--id', null) || _generatedTaskId('bench');
|
|
436
1351
|
// Disabled providers are not bench candidates. Filter before _checkAgent so a
|
|
437
1352
|
// bench that has no opted-in provider cannot execute installed third-party
|
|
438
1353
|
// CLIs merely to discover that they are disabled.
|
|
439
|
-
const
|
|
440
|
-
|
|
441
|
-
|
|
1354
|
+
const benchProviders = typeof _allProviders === 'function' ? _allProviders(root) : EXTERNAL_AGENTS;
|
|
1355
|
+
const ready = benchProviders.filter(a => process.env[a.envFlag] === '1')
|
|
1356
|
+
.map(a => ({ agent: a, status: _checkAgent(a) }))
|
|
1357
|
+
.filter(x => x.status.status === 'ready');
|
|
442
1358
|
if (!ready.length) {
|
|
443
1359
|
fail('ready CLI 없음 — leerness setup-agents 또는 .env에 LEERNESS_ENABLE_X=1 설정 필요');
|
|
444
1360
|
return process.exit(1);
|
|
445
1361
|
}
|
|
1362
|
+
const benchBatchAudit = _ledgerEvent({
|
|
1363
|
+
event: 'benchmark.batch.started',
|
|
1364
|
+
taskId: benchTaskId,
|
|
1365
|
+
task: task.slice(0, 300),
|
|
1366
|
+
attemptId: `attempt-${crypto.randomUUID ? crypto.randomUUID() : crypto.randomBytes(16).toString('hex')}`,
|
|
1367
|
+
role: arg('--role', null) ? _normalizeRole(arg('--role', null)) : null,
|
|
1368
|
+
mode: writeMode ? 'write' : 'read-only',
|
|
1369
|
+
result: { summary: 'agents bench prepared', targets: ready.map(x => x.agent.id) },
|
|
1370
|
+
executed: false,
|
|
1371
|
+
});
|
|
1372
|
+
if (!benchBatchAudit.ok) {
|
|
1373
|
+
failJson(has('--json'), benchBatchAudit.code || 'ledger_write_failed', `외부 모델 호출 전 benchmark 원장을 기록하지 못해 중단했습니다: ${benchBatchAudit.error}`);
|
|
1374
|
+
return;
|
|
1375
|
+
}
|
|
1376
|
+
const benchBatchEventId = benchBatchAudit.event.eventId;
|
|
1377
|
+
const _recordBenchResult = (agent, entry, executed) => {
|
|
1378
|
+
const audit = _ledgerEvent({
|
|
1379
|
+
event: entry.ok ? 'benchmark.completed' : 'benchmark.failed',
|
|
1380
|
+
taskId: benchTaskId,
|
|
1381
|
+
parentAttemptId: benchBatchEventId,
|
|
1382
|
+
attemptId: `${benchBatchAudit.event.attemptId || benchBatchEventId}:${agent.id}`,
|
|
1383
|
+
task: task.slice(0, 300),
|
|
1384
|
+
role: arg('--role', null) ? _normalizeRole(arg('--role', null)) : null,
|
|
1385
|
+
actualExecutor: normalizeExecutorIdentity({ provider: agent.id, model: null, identitySource: 'cli-default-unverified' }),
|
|
1386
|
+
mode: writeMode ? 'write' : 'read-only',
|
|
1387
|
+
status: entry.ok ? 'completed' : 'failed',
|
|
1388
|
+
result: {
|
|
1389
|
+
summary: entry.ok ? 'agents bench completed' : `agents bench failed: ${entry.error || `exit ${entry.exit}`}`,
|
|
1390
|
+
evidence: `elapsedMs=${entry.elapsed}; responseChars=${String(entry.stdout || '').length}`,
|
|
1391
|
+
},
|
|
1392
|
+
executed: !!executed,
|
|
1393
|
+
});
|
|
1394
|
+
return { ...entry, model: null, identitySource: 'cli-default-unverified', auditWritten: audit.ok === true, auditEventId: audit.ok === true ? audit.event.eventId : null, auditError: audit.ok === true ? null : audit.error };
|
|
1395
|
+
};
|
|
446
1396
|
log(`# leerness agents bench (1.9.36)`);
|
|
447
1397
|
log(`task: ${task.slice(0, 80)}${task.length > 80 ? '…' : ''}`);
|
|
448
1398
|
log(`참여 CLI: ${ready.map(r => r.agent.id).join(', ')} (${ready.length}개)`);
|
|
@@ -455,7 +1405,7 @@ function agentsCmd(root, sub, args = [], deps = {}) {
|
|
|
455
1405
|
const t0 = Date.now();
|
|
456
1406
|
const spec = _benchLaunchSpec(agent, task, writeMode);
|
|
457
1407
|
if (spec.unsupported) {
|
|
458
|
-
results.push({ id: agent.id, exit: null, elapsed: 0, stdout: '', stderrLen: 0, error: spec.unsupported, ok: false });
|
|
1408
|
+
results.push(_recordBenchResult(agent, { id: agent.id, exit: null, elapsed: 0, stdout: '', stderrLen: 0, error: spec.unsupported, ok: false }, false));
|
|
459
1409
|
resolve();
|
|
460
1410
|
return;
|
|
461
1411
|
}
|
|
@@ -465,7 +1415,7 @@ function agentsCmd(root, sub, args = [], deps = {}) {
|
|
|
465
1415
|
try {
|
|
466
1416
|
r = _spawnPortable(spec.file, spec.args, { stdio: [spec.stdin, 'pipe', 'pipe'] });
|
|
467
1417
|
} catch (err) {
|
|
468
|
-
results.push({ id: agent.id, exit: -1, elapsed: Date.now() - t0, stdout: '', stderrLen: 0, error: err.message, ok: false });
|
|
1418
|
+
results.push(_recordBenchResult(agent, { id: agent.id, exit: -1, elapsed: Date.now() - t0, stdout: '', stderrLen: 0, error: err.message, ok: false }, false));
|
|
469
1419
|
resolve();
|
|
470
1420
|
return;
|
|
471
1421
|
}
|
|
@@ -474,11 +1424,14 @@ function agentsCmd(root, sub, args = [], deps = {}) {
|
|
|
474
1424
|
r.stderr.on('data', d => { stderr += d; });
|
|
475
1425
|
const timer = setTimeout(() => { r.kill(); }, timeoutS * 1000);
|
|
476
1426
|
let settled = false;
|
|
477
|
-
const settle = (entry) => {
|
|
1427
|
+
const settle = (entry, executed) => {
|
|
478
1428
|
if (settled) return;
|
|
479
1429
|
settled = true;
|
|
480
1430
|
clearTimeout(timer);
|
|
481
|
-
|
|
1431
|
+
// ChildProcess commonly emits `error` and then `close`. Create the
|
|
1432
|
+
// provenance event only after winning this settle boundary so one
|
|
1433
|
+
// invocation can never append two terminal events.
|
|
1434
|
+
results.push(_recordBenchResult(agent, entry, executed));
|
|
482
1435
|
resolve();
|
|
483
1436
|
};
|
|
484
1437
|
r.on('close', (code) => {
|
|
@@ -487,15 +1440,20 @@ function agentsCmd(root, sub, args = [], deps = {}) {
|
|
|
487
1440
|
id: agent.id, exit: code, elapsed,
|
|
488
1441
|
stdout: stdout.trim().split('\n').slice(-3).join('\n'),
|
|
489
1442
|
stderrLen: stderr.length,
|
|
490
|
-
ok: code === 0 && stdout.trim().length > 0
|
|
491
|
-
});
|
|
1443
|
+
ok: code === 0 && stdout.trim().length > 0,
|
|
1444
|
+
}, true);
|
|
492
1445
|
});
|
|
493
1446
|
r.on('error', (err) => {
|
|
494
|
-
settle({ id: agent.id, exit: -1, elapsed: Date.now() - t0, stdout: '', stderrLen: 0, error: err.message, ok: false });
|
|
1447
|
+
settle({ id: agent.id, exit: -1, elapsed: Date.now() - t0, stdout: '', stderrLen: 0, error: err.message, ok: false }, false);
|
|
495
1448
|
});
|
|
496
1449
|
}));
|
|
497
1450
|
return Promise.all(promises).then(() => {
|
|
498
|
-
|
|
1451
|
+
const auditFailures = results.filter(r => !r.auditWritten);
|
|
1452
|
+
if (has('--json')) {
|
|
1453
|
+
log(JSON.stringify({ task, results, provenance: { taskId: benchTaskId, batchEventId: benchBatchEventId, complete: !!benchTaskId && auditFailures.length === 0, failedWrites: auditFailures.map(r => ({ agent: r.id, error: r.auditError })) } }, null, 2));
|
|
1454
|
+
if (auditFailures.length) process.exitCode = 1;
|
|
1455
|
+
return results;
|
|
1456
|
+
}
|
|
499
1457
|
log(`| CLI | 시간 | exit | 응답 길이 | 마지막 라인 |`);
|
|
500
1458
|
log(`|---|---:|---:|---:|---|`);
|
|
501
1459
|
// sort by elapsed
|
|
@@ -510,74 +1468,252 @@ function agentsCmd(root, sub, args = [], deps = {}) {
|
|
|
510
1468
|
log(`결과: ${okCount}/${results.length} 성공`);
|
|
511
1469
|
const fastest = results.filter(r => r.ok).sort((a, b) => a.elapsed - b.elapsed)[0];
|
|
512
1470
|
if (fastest) log(`🏆 가장 빠름: ${fastest.id} (${fastest.elapsed}ms)`);
|
|
1471
|
+
if (auditFailures.length) {
|
|
1472
|
+
warn(`benchmark provenance 기록 실패 ${auditFailures.length}건 — 결과를 완료 증거로 사용하지 마세요.`);
|
|
1473
|
+
process.exitCode = 1;
|
|
1474
|
+
}
|
|
513
1475
|
return results;
|
|
514
1476
|
});
|
|
515
1477
|
}
|
|
516
1478
|
|
|
517
1479
|
if (sub === 'quota') {
|
|
518
|
-
//
|
|
519
|
-
|
|
520
|
-
for
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
1480
|
+
// Provider capacity observation: installation, enablement, authentication, routing eligibility,
|
|
1481
|
+
// model callability, and remaining capacity are separate facts. Unknown values stay unknown.
|
|
1482
|
+
// This command may load the project .env for Leerness enable flags, but it does not emit, persist,
|
|
1483
|
+
// or use credential values to infer quota. Only registered, safe, non-interactive auth checks run,
|
|
1484
|
+
// and only for providers that are both enabled and installed.
|
|
1485
|
+
const availabilityState = readAvailabilityObservations(root, 2000);
|
|
1486
|
+
if (availabilityState.ok === false || availabilityState.partial) {
|
|
1487
|
+
failJson(has('--json'), availabilityState.code || 'availability_history_partial', availabilityState.error || 'availability observation history is incomplete');
|
|
1488
|
+
return;
|
|
1489
|
+
}
|
|
1490
|
+
const providers = _allProviders(root);
|
|
1491
|
+
const userIds = new Set(_readUserProviders(root).map(u => canonicalLegacyProviderIdentity(u && u.id)).filter(Boolean));
|
|
1492
|
+
const attemptedAuthChecks = [];
|
|
1493
|
+
const attemptedVersionChecks = [];
|
|
1494
|
+
const presenceOnlyChecks = [];
|
|
1495
|
+
const limitationCodes = [
|
|
1496
|
+
'local_prerequisites_only',
|
|
1497
|
+
'authentication_is_not_model_entitlement',
|
|
1498
|
+
'exact_capacity_requires_verified_official_adapter',
|
|
1499
|
+
'recorded_availability_is_not_official_capacity',
|
|
1500
|
+
'disabled_providers_presence_only',
|
|
1501
|
+
'credential_values_not_passed_to_probe_commands',
|
|
1502
|
+
];
|
|
1503
|
+
const humanLimitations = [
|
|
1504
|
+
t(
|
|
1505
|
+
'CLI 설치·활성 상태와 등록된 안전한 인증 확인만 관측합니다.',
|
|
1506
|
+
'Only CLI installation, enablement, and registered safe authentication checks are observed.',
|
|
1507
|
+
),
|
|
1508
|
+
t(
|
|
1509
|
+
'인증 성공은 특정 모델 호출 권한이나 잔여량을 증명하지 않습니다.',
|
|
1510
|
+
'Successful authentication does not prove model entitlement or remaining capacity.',
|
|
1511
|
+
),
|
|
1512
|
+
t(
|
|
1513
|
+
'정확한 잔여량은 공급자가 공식 비대화형 계약을 제공하고 별도 어댑터가 검증된 경우에만 수치화할 수 있습니다.',
|
|
1514
|
+
'Exact remaining capacity can be reported only when a provider exposes an official non-interactive contract and a dedicated adapter is verified.',
|
|
1515
|
+
),
|
|
1516
|
+
t(
|
|
1517
|
+
'availability 명령으로 기록한 상태는 실행 이력이며 공급자가 검증한 잔여량이 아닙니다.',
|
|
1518
|
+
'States recorded through the availability command are execution history, not provider-verified remaining capacity.',
|
|
1519
|
+
),
|
|
1520
|
+
];
|
|
1521
|
+
const results = providers.map((agent) => {
|
|
1522
|
+
const providerEnabled = !!(agent && agent.envFlag && process.env[agent.envFlag] === '1');
|
|
1523
|
+
const base = _checkAgent(agent, {
|
|
1524
|
+
auth: providerEnabled,
|
|
1525
|
+
executeVersion: providerEnabled,
|
|
1526
|
+
cwd: root,
|
|
1527
|
+
});
|
|
1528
|
+
const source = userIds.has(agent.id) ? 'user' : 'builtin';
|
|
1529
|
+
const authCheckAttempted = !!base.authSource;
|
|
1530
|
+
if (authCheckAttempted) attemptedAuthChecks.push(agent.id);
|
|
1531
|
+
if (base.versionCheckAttempted) attemptedVersionChecks.push(agent.id);
|
|
1532
|
+
if (base.presenceCheckOnly) presenceOnlyChecks.push(agent.id);
|
|
1533
|
+
const providerIdentity = canonicalProviderIdentity(agent.id);
|
|
1534
|
+
const providerObservations = availabilityState.observations
|
|
1535
|
+
.filter(observation => canonicalProviderIdentity(observation.provider) === providerIdentity)
|
|
1536
|
+
.slice()
|
|
1537
|
+
.sort((a, b) => Number(a.ledgerOrdinal || 0) - Number(b.ledgerOrdinal || 0));
|
|
1538
|
+
const availabilityObservations = providerObservations
|
|
1539
|
+
.map(observation => ({
|
|
1540
|
+
model: observation.model || null,
|
|
1541
|
+
quota: observation.quota || null,
|
|
1542
|
+
rateLimited: observation.rateLimited == null ? null : observation.rateLimited === true,
|
|
1543
|
+
retryAfter: observation.retryAfter || null,
|
|
1544
|
+
reason: observation.reason || null,
|
|
1545
|
+
observedAt: observation.observedAt || observation.at || null,
|
|
1546
|
+
expiresAt: observation.expiresAt || null,
|
|
1547
|
+
}));
|
|
1548
|
+
// Summaries must use the same "newest applicable axis wins" semantics as
|
|
1549
|
+
// routing. Keeping provider-wide and model scopes independent leaves an
|
|
1550
|
+
// older model denial visible after a newer provider-wide recovery.
|
|
1551
|
+
const observedScopes = [...new Set(providerObservations.map(observation => observation.model || null))];
|
|
1552
|
+
const effectiveScopes = observedScopes.map(model => {
|
|
1553
|
+
const effective = availabilityExtrasForCandidate(availabilityState, { provider: agent.id, model });
|
|
1554
|
+
return {
|
|
1555
|
+
model,
|
|
1556
|
+
quota: effective.quota == null ? null : effective.quota,
|
|
1557
|
+
rateLimited: effective.rateLimited == null ? null : effective.rateLimited === true,
|
|
1558
|
+
};
|
|
1559
|
+
});
|
|
1560
|
+
const observedQuotaStates = new Set(effectiveScopes.map(observation => observation.quota).filter(Boolean));
|
|
1561
|
+
const quotaState = observedQuotaStates.size > 1
|
|
1562
|
+
? 'observed-mixed'
|
|
1563
|
+
: observedQuotaStates.has('exhausted')
|
|
1564
|
+
? 'observed-exhausted'
|
|
1565
|
+
: observedQuotaStates.has('available')
|
|
1566
|
+
? 'observed-available'
|
|
1567
|
+
: 'not-observed';
|
|
1568
|
+
|
|
1569
|
+
const effectiveAvailability = normalizeAvailability(
|
|
1570
|
+
agent,
|
|
1571
|
+
base,
|
|
1572
|
+
availabilityExtrasForCandidate(availabilityState, { provider: agent.id, model: null }),
|
|
1573
|
+
);
|
|
1574
|
+
let routingEligibility;
|
|
1575
|
+
let routingReason;
|
|
1576
|
+
let hintCode;
|
|
1577
|
+
if (!effectiveAvailability.installed) {
|
|
1578
|
+
routingEligibility = 'blocked';
|
|
1579
|
+
routingReason = 'cli_unavailable';
|
|
1580
|
+
hintCode = 'install_cli';
|
|
1581
|
+
} else if (!effectiveAvailability.enabled) {
|
|
1582
|
+
routingEligibility = 'blocked';
|
|
1583
|
+
routingReason = 'provider_disabled';
|
|
1584
|
+
hintCode = 'enable_provider';
|
|
1585
|
+
} else if (!effectiveAvailability.eligible) {
|
|
1586
|
+
routingEligibility = 'blocked';
|
|
1587
|
+
routingReason = (effectiveAvailability.blockingReasons[0] || 'availability_blocked').replace(/-/g, '_');
|
|
1588
|
+
hintCode = routingReason === 'not_authenticated' ? 'restore_authentication' : 'restore_provider_availability';
|
|
1589
|
+
} else if (effectiveAvailability.authenticated === 'ok') {
|
|
1590
|
+
routingEligibility = 'eligible';
|
|
1591
|
+
routingReason = 'local_prerequisites_met';
|
|
1592
|
+
hintCode = 'authentication_only';
|
|
1593
|
+
} else {
|
|
1594
|
+
routingEligibility = 'unverified';
|
|
1595
|
+
routingReason = 'authentication_not_observed';
|
|
1596
|
+
hintCode = 'official_adapter_unavailable';
|
|
559
1597
|
}
|
|
560
|
-
|
|
1598
|
+
|
|
1599
|
+
return {
|
|
1600
|
+
id: agent.id,
|
|
1601
|
+
source,
|
|
1602
|
+
bin: agent.bin,
|
|
1603
|
+
status: base.status,
|
|
1604
|
+
installed: !!base.installed,
|
|
1605
|
+
enabled: !!base.enabled,
|
|
1606
|
+
version: base.version || null,
|
|
1607
|
+
versionCheckAttempted: !!base.versionCheckAttempted,
|
|
1608
|
+
versionCheckState: base.versionCheckAttempted
|
|
1609
|
+
? (base.versionCheckSucceeded ? 'ok' : 'failed')
|
|
1610
|
+
: 'not-attempted',
|
|
1611
|
+
presenceCheckOnly: !!base.presenceCheckOnly,
|
|
1612
|
+
auth: base.auth || 'unknown',
|
|
1613
|
+
authCheckAttempted,
|
|
1614
|
+
authEvidencePresent: !!base.authEvidence,
|
|
1615
|
+
authSource: base.authSource || null,
|
|
1616
|
+
routingEligibility,
|
|
1617
|
+
routingReason,
|
|
1618
|
+
modelCallability: 'not-observed',
|
|
1619
|
+
// Legacy field retained for callers that already read `callability`.
|
|
1620
|
+
callability: 'unknown',
|
|
1621
|
+
callabilityReason: 'live_model_call_not_performed',
|
|
1622
|
+
// Legacy `quota` keeps its string status for existing consumers. Numeric
|
|
1623
|
+
// capacity fields remain null until a verified official adapter exists.
|
|
1624
|
+
quota: quotaState === 'not-observed' ? 'unknown' : quotaState,
|
|
1625
|
+
quotaState,
|
|
1626
|
+
verifiedRemainingAmount: null,
|
|
1627
|
+
remaining: null,
|
|
1628
|
+
unit: null,
|
|
1629
|
+
resetAt: null,
|
|
1630
|
+
quotaSource: null,
|
|
1631
|
+
availabilityObservations,
|
|
1632
|
+
// Legacy alias retained for existing CLI/MCP consumers.
|
|
1633
|
+
observed: availabilityObservations,
|
|
1634
|
+
// Legacy field retained for callers that already display `hint`. Canonical guidance uses hintCode.
|
|
1635
|
+
hint: base.status === 'not-installed' ? `${agent.bin} CLI 미설치`
|
|
1636
|
+
: base.status === 'disabled' ? `${agent.envFlag}=1 필요`
|
|
1637
|
+
: hintCode,
|
|
1638
|
+
hintCode,
|
|
1639
|
+
raw: null,
|
|
1640
|
+
};
|
|
1641
|
+
});
|
|
1642
|
+
const policy = {
|
|
1643
|
+
projectEnvironmentMayBeLoaded: true,
|
|
1644
|
+
credentialValuesIncludedInOutput: false,
|
|
1645
|
+
credentialValuesPersistedByCommand: false,
|
|
1646
|
+
credentialValuesInspectedForCapacity: false,
|
|
1647
|
+
credentialValuesPassedToProbeCommands: false,
|
|
1648
|
+
providerCredentialStoresReadDirectly: false,
|
|
1649
|
+
providerCliMayReadOwnCredentialStore: attemptedAuthChecks.length > 0 || attemptedVersionChecks.length > 0,
|
|
1650
|
+
providerCliMayReadEnvironmentCredentials: false,
|
|
1651
|
+
disabledProviderCommandsExecuted: false,
|
|
1652
|
+
registeredSafeAuthChecksAttempted: attemptedAuthChecks,
|
|
1653
|
+
versionChecksAttempted: attemptedVersionChecks,
|
|
1654
|
+
presenceOnlyChecks,
|
|
1655
|
+
browserSessionReuse: false,
|
|
1656
|
+
guiScraping: false,
|
|
1657
|
+
liveModelCalls: false,
|
|
1658
|
+
capacityValuesRequireOfficialContract: true,
|
|
1659
|
+
recordedAvailabilityTreatedAsOfficialCapacity: false,
|
|
1660
|
+
speculativeCapacityClaims: false,
|
|
1661
|
+
};
|
|
1662
|
+
if (has('--json')) {
|
|
1663
|
+
const note = '설치·활성·일부 인증은 확인할 수 있지만, 모델 권한·남은 토큰/크레딧·과금 권한은 공통적으로 검증하지 않습니다. observed-*는 provider 수치 조회가 아니라 append-only 관측 기록입니다.';
|
|
1664
|
+
log(JSON.stringify({
|
|
1665
|
+
schemaVersion: 2,
|
|
1666
|
+
observation: 'provider-capacity',
|
|
1667
|
+
quota: results,
|
|
1668
|
+
availabilityObservationState: {
|
|
1669
|
+
activeCount: availabilityState.activeCount,
|
|
1670
|
+
partial: availabilityState.partial,
|
|
1671
|
+
truncated: availabilityState.truncated,
|
|
1672
|
+
},
|
|
1673
|
+
policy,
|
|
1674
|
+
limitations: limitationCodes,
|
|
1675
|
+
note,
|
|
1676
|
+
}, null, 2));
|
|
1677
|
+
return;
|
|
561
1678
|
}
|
|
562
|
-
|
|
563
|
-
log(`# 외부 AI CLI quota 추정 (1.9.31)`);
|
|
1679
|
+
log(t(`# 외부 AI 공급자 상태 관측 (Leerness v${VERSION})`, `# External AI provider status observation (Leerness v${VERSION})`));
|
|
564
1680
|
log('');
|
|
565
|
-
log(
|
|
566
|
-
|
|
1681
|
+
log(t(
|
|
1682
|
+
'| Provider | 출처 | 설치/활성 | 인증 | 로컬 라우팅 | 모델 호출 | 잔여량 |',
|
|
1683
|
+
'| Provider | source | installed/enabled | auth | local routing | model call | remaining |',
|
|
1684
|
+
));
|
|
1685
|
+
log('|---|---|---|---|---|---|---|');
|
|
567
1686
|
for (const q of results) {
|
|
568
|
-
const
|
|
569
|
-
log(`| ${q.id} | ${
|
|
1687
|
+
const installation = `${q.installed ? 'yes' : 'no'}/${q.enabled ? 'yes' : 'no'}`;
|
|
1688
|
+
log(`| ${q.id} | ${q.source} | ${installation} | ${q.auth} | ${q.routingEligibility} | ${q.modelCallability} | ${q.quotaState} |`);
|
|
570
1689
|
}
|
|
571
1690
|
log('');
|
|
572
|
-
log(
|
|
573
|
-
|
|
574
|
-
log(
|
|
575
|
-
|
|
1691
|
+
log(t('## 관측 경계', '## Observation boundary'));
|
|
1692
|
+
for (const line of humanLimitations) log(` - ${line}`);
|
|
1693
|
+
log(t(
|
|
1694
|
+
' - 프로젝트 .env는 활성화 설정을 적용하기 위해 로드될 수 있지만, 자격증명 값은 출력·저장하거나 잔여량 계산에 사용하지 않습니다.',
|
|
1695
|
+
' - The project .env may be loaded to apply enablement settings, but credential values are not emitted, persisted, or used to calculate capacity.',
|
|
1696
|
+
));
|
|
1697
|
+
log(t(
|
|
1698
|
+
attemptedAuthChecks.length
|
|
1699
|
+
? ` - 등록된 안전한 인증 확인을 실행한 공급자 CLI(${attemptedAuthChecks.join(', ')})는 자체 자격증명 저장소를 읽을 수 있습니다. Leerness는 해당 저장소를 직접 읽지 않습니다.`
|
|
1700
|
+
: ' - 이번 실행에서는 공급자 인증 확인 명령을 실행하지 않았습니다.',
|
|
1701
|
+
attemptedAuthChecks.length
|
|
1702
|
+
? ` - Provider CLIs used for registered safe authentication checks (${attemptedAuthChecks.join(', ')}) may read their own credential stores. Leerness does not read those stores directly.`
|
|
1703
|
+
: ' - No provider authentication check command was executed in this run.',
|
|
1704
|
+
));
|
|
1705
|
+
log(t(
|
|
1706
|
+
' - 브라우저 세션·GUI를 읽지 않았고 실제 모델 호출도 하지 않았습니다.',
|
|
1707
|
+
' - Browser sessions and GUIs were not read, and no live model call was made.',
|
|
1708
|
+
));
|
|
1709
|
+
log(t(
|
|
1710
|
+
' - 비활성 공급자는 실행하지 않고 PATH 상 존재 여부만 확인하며, 모든 공급자 프로브에서 프로젝트 자격증명 환경변수를 제거합니다.',
|
|
1711
|
+
' - Disabled providers are not executed; only PATH presence is checked, and project credential environment variables are removed from every provider probe.',
|
|
1712
|
+
));
|
|
576
1713
|
return;
|
|
577
1714
|
}
|
|
578
|
-
|
|
579
|
-
fail('사용법: leerness agents list|check|quota|dispatch|multi|bench|recommend [--write] "<task>" [--to <id>]');
|
|
1715
|
+
fail('사용법: leerness agents list|check|quota|availability|resolve|fallback|record|history|dispatch|multi|bench|recommend [--write] "<task>" [--to <id>]');
|
|
580
1716
|
return process.exit(1);
|
|
581
1717
|
}
|
|
582
1718
|
|
|
583
|
-
module.exports = { agentsCmd, _benchLaunchSpec };
|
|
1719
|
+
module.exports = { agentsCmd, _benchLaunchSpec, _parseAgentPositional, _findAgentAction };
|