@yemi33/minions 0.1.2453 → 0.1.2455
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/bin/install-internal-minions.js +136 -6
- package/bin/minions.js +27 -13
- package/dashboard/js/agent-identity.js +121 -0
- package/dashboard/js/modal.js +4 -0
- package/dashboard/js/refresh.js +38 -3
- package/dashboard/js/render-agents.js +14 -2
- package/dashboard/js/render-dispatch.js +2 -2
- package/dashboard/js/render-other.js +147 -13
- package/dashboard/js/render-prd.js +6 -5
- package/dashboard/js/render-prs.js +44 -25
- package/dashboard/js/render-work-items.js +442 -22
- package/dashboard/js/settings.js +8 -0
- package/dashboard/js/utils.js +40 -0
- package/dashboard/pages/tools.html +1 -0
- package/dashboard/pages/work.html +13 -0
- package/dashboard/shared/pr-author.js +75 -0
- package/dashboard/shared/pr-filters.js +28 -5
- package/dashboard/styles.css +85 -0
- package/dashboard-build.js +3 -3
- package/dashboard.js +93 -7
- package/docs/README.md +1 -0
- package/docs/copilot-cli-schema.md +1 -0
- package/docs/engine-restart.md +4 -2
- package/docs/internal-install.md +44 -5
- package/docs/named-agents.md +48 -0
- package/docs/pr-author-identity.md +63 -10
- package/docs/runtime-adapters.md +39 -0
- package/docs/temporary-agents.md +172 -0
- package/engine/ado/comment.js +261 -4
- package/engine/agents/llm.js +26 -0
- package/engine/agents/playbook.js +2 -1
- package/engine/api/settings-validation.js +25 -0
- package/engine/core/operator-identity.js +23 -1
- package/engine/core/queries.js +53 -2
- package/engine/core/shared.js +286 -9
- package/engine/db/migrations/032-review-enrolled-pr-context-only.js +95 -0
- package/engine/operations/cli.js +102 -1
- package/engine/orchestration/lifecycle.js +7 -0
- package/engine/orchestration/routing.js +4 -1
- package/engine/providers/gh-comment.js +159 -0
- package/engine/recovery/stop-stack.js +16 -2
- package/engine/runtimes/claude.js +3 -0
- package/engine/runtimes/codex.js +4 -0
- package/engine/runtimes/copilot.js +23 -2
- package/engine.js +2 -2
- package/package.json +1 -1
- package/playbooks/fix.md +23 -2
- package/playbooks/shared-rules.md +24 -2
package/engine/ado/comment.js
CHANGED
|
@@ -31,18 +31,68 @@
|
|
|
31
31
|
* `active` (1). ADO honors the status field on thread creation, so unlike GitHub
|
|
32
32
|
* this needs no second API round-trip. Actionable comments (bugs, required
|
|
33
33
|
* changes) stay `active` so they block/notify the author.
|
|
34
|
+
*
|
|
35
|
+
* Fix summaries on the ORIGINATING thread (W-msd9os8e00qnd628):
|
|
36
|
+
* `replyToAdoThread` posts a fix-summary reply onto an existing review thread
|
|
37
|
+
* and transitions that thread to `fixed` (2) / `closed` (4), rather than opening
|
|
38
|
+
* a fresh active summary thread. It reads the thread first so retries / restarts
|
|
39
|
+
* / duplicate callbacks are idempotent (marker-keyed reply dedupe; already-
|
|
40
|
+
* resolved threads left as-is) and a reply/PATCH partial failure is surfaced and
|
|
41
|
+
* retryable.
|
|
34
42
|
*/
|
|
35
43
|
|
|
36
|
-
const { buildMinionsCommentBody } = require('../providers/comment-format');
|
|
44
|
+
const { buildMinionsCommentBody, parseMinionsMarker } = require('../providers/comment-format');
|
|
37
45
|
const { acquireAdoToken } = require('./token');
|
|
38
46
|
|
|
39
47
|
const ADO_API_VERSION = '7.1';
|
|
40
48
|
|
|
41
|
-
// ADO CommentThreadStatus enum
|
|
42
|
-
//
|
|
43
|
-
// pre-resolved so they don't clutter
|
|
49
|
+
// ADO CommentThreadStatus enum. `active` (1) is the default for actionable
|
|
50
|
+
// threads; `fixed` (2) records a finding the fix agent addressed; `closed` (4)
|
|
51
|
+
// is used for non-actionable threads posted pre-resolved so they don't clutter
|
|
52
|
+
// the PR with noise the author must triage.
|
|
53
|
+
const ADO_THREAD_STATUS_UNKNOWN = 0;
|
|
44
54
|
const ADO_THREAD_STATUS_ACTIVE = 1;
|
|
55
|
+
const ADO_THREAD_STATUS_FIXED = 2;
|
|
56
|
+
const ADO_THREAD_STATUS_WONT_FIX = 3;
|
|
45
57
|
const ADO_THREAD_STATUS_CLOSED = 4;
|
|
58
|
+
const ADO_THREAD_STATUS_BY_DESIGN = 5;
|
|
59
|
+
const ADO_THREAD_STATUS_PENDING = 6;
|
|
60
|
+
|
|
61
|
+
// Canonical mapping from a caller-friendly status name to the ADO enum value.
|
|
62
|
+
// Used by the reply-and-resolve path so a fix agent can transition the
|
|
63
|
+
// ORIGINATING thread to a resolved state (`fixed`/`closed`) rather than opening
|
|
64
|
+
// a new active summary thread. `active` is the "leave unresolved" sentinel.
|
|
65
|
+
const ADO_THREAD_STATUS_BY_NAME = Object.freeze({
|
|
66
|
+
active: ADO_THREAD_STATUS_ACTIVE,
|
|
67
|
+
fixed: ADO_THREAD_STATUS_FIXED,
|
|
68
|
+
wontfix: ADO_THREAD_STATUS_WONT_FIX,
|
|
69
|
+
'wont-fix': ADO_THREAD_STATUS_WONT_FIX,
|
|
70
|
+
closed: ADO_THREAD_STATUS_CLOSED,
|
|
71
|
+
resolved: ADO_THREAD_STATUS_FIXED,
|
|
72
|
+
bydesign: ADO_THREAD_STATUS_BY_DESIGN,
|
|
73
|
+
'by-design': ADO_THREAD_STATUS_BY_DESIGN,
|
|
74
|
+
pending: ADO_THREAD_STATUS_PENDING,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Thread statuses that mean "resolved" for idempotency purposes — a thread
|
|
78
|
+
// already in one of these needs no further status PATCH.
|
|
79
|
+
const ADO_RESOLVED_THREAD_STATUSES = new Set([
|
|
80
|
+
ADO_THREAD_STATUS_FIXED,
|
|
81
|
+
ADO_THREAD_STATUS_WONT_FIX,
|
|
82
|
+
ADO_THREAD_STATUS_CLOSED,
|
|
83
|
+
ADO_THREAD_STATUS_BY_DESIGN,
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
function resolveAdoThreadStatus(status) {
|
|
87
|
+
if (typeof status === 'number' && Number.isInteger(status)) return status;
|
|
88
|
+
const key = String(status || '').trim().toLowerCase();
|
|
89
|
+
if (key === '') return ADO_THREAD_STATUS_ACTIVE;
|
|
90
|
+
const mapped = ADO_THREAD_STATUS_BY_NAME[key];
|
|
91
|
+
if (mapped === undefined) {
|
|
92
|
+
throw new Error(`invalid thread status: ${JSON.stringify(status)} (expected one of ${Object.keys(ADO_THREAD_STATUS_BY_NAME).join(', ')})`);
|
|
93
|
+
}
|
|
94
|
+
return mapped;
|
|
95
|
+
}
|
|
46
96
|
|
|
47
97
|
// ── Validation (ADO-specific) ────────────────────────────────────────────────
|
|
48
98
|
// Field validation for the marker (agentId / kind / workItemId) is done inside
|
|
@@ -72,6 +122,12 @@ function _validatePrNumber(prNumber) {
|
|
|
72
122
|
}
|
|
73
123
|
}
|
|
74
124
|
|
|
125
|
+
function _validateThreadId(threadId) {
|
|
126
|
+
if (typeof threadId !== 'number' || !Number.isInteger(threadId) || threadId <= 0) {
|
|
127
|
+
throw new Error(`invalid threadId: ${JSON.stringify(threadId)} (expected positive integer)`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
75
131
|
/**
|
|
76
132
|
* Build the ADO threads endpoint for a PR. `orgBase` already includes the org
|
|
77
133
|
* (e.g. https://dev.azure.com/contoso). Project + repositoryId are URL-encoded.
|
|
@@ -82,6 +138,25 @@ function buildThreadsUrl({ orgBase, project, repositoryId, prNumber }) {
|
|
|
82
138
|
+ `/pullRequests/${prNumber}/threads?api-version=${ADO_API_VERSION}`;
|
|
83
139
|
}
|
|
84
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Endpoint for a single existing thread (GET current status/comments, PATCH
|
|
143
|
+
* status). `orgBase` already includes the org.
|
|
144
|
+
*/
|
|
145
|
+
function buildThreadUrl({ orgBase, project, repositoryId, prNumber, threadId }) {
|
|
146
|
+
const base = String(orgBase).replace(/\/+$/, '');
|
|
147
|
+
return `${base}/${encodeURIComponent(project)}/_apis/git/repositories/${encodeURIComponent(repositoryId)}`
|
|
148
|
+
+ `/pullRequests/${prNumber}/threads/${threadId}?api-version=${ADO_API_VERSION}`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Endpoint for the comments collection of an existing thread (POST a reply).
|
|
153
|
+
*/
|
|
154
|
+
function buildThreadCommentsUrl({ orgBase, project, repositoryId, prNumber, threadId }) {
|
|
155
|
+
const base = String(orgBase).replace(/\/+$/, '');
|
|
156
|
+
return `${base}/${encodeURIComponent(project)}/_apis/git/repositories/${encodeURIComponent(repositoryId)}`
|
|
157
|
+
+ `/pullRequests/${prNumber}/threads/${threadId}/comments?api-version=${ADO_API_VERSION}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
85
160
|
/**
|
|
86
161
|
* Default token acquisition — thin wrapper over engine/ado/token.js so callers
|
|
87
162
|
* can inject a stub in tests. Returns the bearer token string or throws.
|
|
@@ -174,12 +249,193 @@ async function postAdoPrComment({
|
|
|
174
249
|
};
|
|
175
250
|
}
|
|
176
251
|
|
|
252
|
+
/**
|
|
253
|
+
* Reply to an EXISTING review thread with a fix summary and transition that
|
|
254
|
+
* thread to a resolved status — instead of opening a new active summary thread.
|
|
255
|
+
*
|
|
256
|
+
* This is the fix-summary path for Azure DevOps (W-msd9os8e00qnd628): when a fix
|
|
257
|
+
* agent has evidence a review finding was addressed, it replies on the
|
|
258
|
+
* originating thread (identified by the structured `threadId` carried on the
|
|
259
|
+
* work item / completion report) and moves that thread to `fixed` (2) / `closed`
|
|
260
|
+
* (4). Contrast with `postAdoPrComment`, which creates a fresh thread.
|
|
261
|
+
*
|
|
262
|
+
* Idempotency (retries / engine restarts / duplicate completion callbacks /
|
|
263
|
+
* provider timeouts): the current thread is read first. A reply whose hidden
|
|
264
|
+
* marker already matches this dispatch's structured identity (agentId + kind +
|
|
265
|
+
* workItemId) is treated as already-posted and the reply POST is skipped; a
|
|
266
|
+
* thread already in a resolved status is left as-is. Resolution is keyed by the
|
|
267
|
+
* explicit `threadId` and the structured marker — never by body/title text — so
|
|
268
|
+
* sibling findings are never bulk-resolved.
|
|
269
|
+
*
|
|
270
|
+
* Partial-failure visibility: the reply POST and the status PATCH are distinct
|
|
271
|
+
* round-trips. If the reply succeeds but the status PATCH fails, we throw so the
|
|
272
|
+
* caller sees the failure and can retry; the retry re-reads the thread, finds
|
|
273
|
+
* the reply already present (skips it), and re-attempts only the PATCH. Local
|
|
274
|
+
* state is never marked resolved when the provider PATCH failed.
|
|
275
|
+
*
|
|
276
|
+
* @param {object} args
|
|
277
|
+
* @param {string} args.orgBase
|
|
278
|
+
* @param {string} args.project
|
|
279
|
+
* @param {string} args.repositoryId
|
|
280
|
+
* @param {number} args.prNumber
|
|
281
|
+
* @param {number} args.threadId existing ADO thread id (positive integer)
|
|
282
|
+
* @param {string} args.body reply body (pre-marker; builder prepends marker)
|
|
283
|
+
* @param {string} args.agentId
|
|
284
|
+
* @param {string} args.kind
|
|
285
|
+
* @param {string} [args.workItemId]
|
|
286
|
+
* @param {string} [args.model]
|
|
287
|
+
* @param {(string|number)} [args.status='fixed'] target status; 'active' leaves the thread unresolved
|
|
288
|
+
* @param {number} [args.timeoutMs=30000]
|
|
289
|
+
* @param {Function} [args.acquireToken]
|
|
290
|
+
* @param {Function} [args.fetchImpl]
|
|
291
|
+
* @returns {Promise<{threadId:number, commentId:(number|undefined), replied:boolean, alreadyReplied:boolean, resolved:boolean, alreadyResolved:boolean, statusApplied:(number|null)}>}
|
|
292
|
+
*/
|
|
293
|
+
async function replyToAdoThread({
|
|
294
|
+
orgBase,
|
|
295
|
+
project,
|
|
296
|
+
repositoryId,
|
|
297
|
+
prNumber,
|
|
298
|
+
threadId,
|
|
299
|
+
body,
|
|
300
|
+
agentId,
|
|
301
|
+
kind,
|
|
302
|
+
workItemId,
|
|
303
|
+
model,
|
|
304
|
+
status = 'fixed',
|
|
305
|
+
timeoutMs = 30000,
|
|
306
|
+
acquireToken = _defaultAcquireToken,
|
|
307
|
+
fetchImpl,
|
|
308
|
+
} = {}) {
|
|
309
|
+
_validateOrgBase(orgBase);
|
|
310
|
+
_validateProject(project);
|
|
311
|
+
_validateRepositoryId(repositoryId);
|
|
312
|
+
_validatePrNumber(prNumber);
|
|
313
|
+
_validateThreadId(threadId);
|
|
314
|
+
|
|
315
|
+
const targetStatus = resolveAdoThreadStatus(status);
|
|
316
|
+
|
|
317
|
+
// buildMinionsCommentBody validates marker fields and throws on bad input.
|
|
318
|
+
const finalBody = buildMinionsCommentBody({ agentId, kind, workItemId, model, body });
|
|
319
|
+
|
|
320
|
+
const token = await acquireToken();
|
|
321
|
+
if (!token || typeof token !== 'string') {
|
|
322
|
+
throw new Error('ADO comment: could not acquire an access token (az account get-access-token failed)');
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const doFetch = fetchImpl || (typeof fetch === 'function' ? fetch : null);
|
|
326
|
+
if (!doFetch) throw new Error('ADO comment: no fetch implementation available');
|
|
327
|
+
|
|
328
|
+
const authHeaders = {
|
|
329
|
+
'Authorization': `Bearer ${token}`,
|
|
330
|
+
'Content-Type': 'application/json',
|
|
331
|
+
};
|
|
332
|
+
const threadUrl = buildThreadUrl({ orgBase, project, repositoryId, prNumber, threadId });
|
|
333
|
+
|
|
334
|
+
// ── 1. Read the current thread (status + existing comments) for idempotency ──
|
|
335
|
+
const getRes = await doFetch(threadUrl, {
|
|
336
|
+
method: 'GET',
|
|
337
|
+
headers: authHeaders,
|
|
338
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
339
|
+
});
|
|
340
|
+
if (!getRes.ok) {
|
|
341
|
+
const detail = await getRes.text().catch(() => '');
|
|
342
|
+
throw new Error(`ADO thread GET ${getRes.status} ${getRes.statusText}: ${String(detail).slice(0, 500)}`);
|
|
343
|
+
}
|
|
344
|
+
const thread = await getRes.json().catch(() => ({}));
|
|
345
|
+
const existingComments = Array.isArray(thread.comments) ? thread.comments : [];
|
|
346
|
+
const currentStatus = typeof thread.status === 'number'
|
|
347
|
+
? thread.status
|
|
348
|
+
: resolveAdoThreadStatus(thread.status || 'active');
|
|
349
|
+
|
|
350
|
+
// Idempotency: has THIS dispatch (structured marker identity) already replied?
|
|
351
|
+
const alreadyReplied = existingComments.some((c) => {
|
|
352
|
+
const marker = parseMinionsMarker(String(c?.content || ''));
|
|
353
|
+
if (!marker) return false;
|
|
354
|
+
if (marker.agentId !== agentId || marker.kind !== kind) return false;
|
|
355
|
+
// When a workItemId is supplied it is the structured finding identity — a
|
|
356
|
+
// match on it (or its absence on both sides) confirms this is our reply.
|
|
357
|
+
if (workItemId) return marker.workItemId === workItemId;
|
|
358
|
+
return marker.workItemId === undefined;
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// The first comment id anchors a reply to the originating thread.
|
|
362
|
+
const parentCommentId = existingComments.length > 0 && Number.isInteger(existingComments[0]?.id)
|
|
363
|
+
? existingComments[0].id
|
|
364
|
+
: 1;
|
|
365
|
+
|
|
366
|
+
let replied = false;
|
|
367
|
+
let commentId;
|
|
368
|
+
if (!alreadyReplied) {
|
|
369
|
+
const commentsUrl = buildThreadCommentsUrl({ orgBase, project, repositoryId, prNumber, threadId });
|
|
370
|
+
const postRes = await doFetch(commentsUrl, {
|
|
371
|
+
method: 'POST',
|
|
372
|
+
headers: authHeaders,
|
|
373
|
+
body: JSON.stringify({ parentCommentId, content: finalBody, commentType: 1 }),
|
|
374
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
375
|
+
});
|
|
376
|
+
if (!postRes.ok) {
|
|
377
|
+
const detail = await postRes.text().catch(() => '');
|
|
378
|
+
throw new Error(`ADO thread reply POST ${postRes.status} ${postRes.statusText}: ${String(detail).slice(0, 500)}`);
|
|
379
|
+
}
|
|
380
|
+
const postJson = await postRes.json().catch(() => ({}));
|
|
381
|
+
commentId = postJson && postJson.id;
|
|
382
|
+
replied = true;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// ── 2. Transition the thread status (skip when unresolved requested or the
|
|
386
|
+
// thread is already in the requested/equivalent resolved state) ──
|
|
387
|
+
const wantsResolve = targetStatus !== ADO_THREAD_STATUS_ACTIVE
|
|
388
|
+
&& targetStatus !== ADO_THREAD_STATUS_UNKNOWN;
|
|
389
|
+
const alreadyResolved = wantsResolve && currentStatus === targetStatus;
|
|
390
|
+
let resolved = ADO_RESOLVED_THREAD_STATUSES.has(currentStatus);
|
|
391
|
+
let statusApplied = null;
|
|
392
|
+
|
|
393
|
+
if (wantsResolve && currentStatus !== targetStatus) {
|
|
394
|
+
// The reply above may already be committed. A PATCH failure here throws so
|
|
395
|
+
// the caller can retry; the retry skips the (now-present) reply and PATCHes
|
|
396
|
+
// again — local state is never marked resolved on a failed PATCH.
|
|
397
|
+
const patchRes = await doFetch(threadUrl, {
|
|
398
|
+
method: 'PATCH',
|
|
399
|
+
headers: authHeaders,
|
|
400
|
+
body: JSON.stringify({ status: targetStatus }),
|
|
401
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
402
|
+
});
|
|
403
|
+
if (!patchRes.ok) {
|
|
404
|
+
const detail = await patchRes.text().catch(() => '');
|
|
405
|
+
throw new Error(`ADO thread status PATCH ${patchRes.status} ${patchRes.statusText}: ${String(detail).slice(0, 500)} (reply ${replied ? 'was' : 'was not'} posted; retry is idempotent)`);
|
|
406
|
+
}
|
|
407
|
+
statusApplied = targetStatus;
|
|
408
|
+
resolved = true;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return {
|
|
412
|
+
threadId,
|
|
413
|
+
commentId,
|
|
414
|
+
replied,
|
|
415
|
+
alreadyReplied,
|
|
416
|
+
resolved,
|
|
417
|
+
alreadyResolved: !!alreadyResolved,
|
|
418
|
+
statusApplied,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
177
422
|
module.exports = {
|
|
178
423
|
postAdoPrComment,
|
|
424
|
+
replyToAdoThread,
|
|
179
425
|
buildThreadsUrl,
|
|
426
|
+
buildThreadUrl,
|
|
427
|
+
buildThreadCommentsUrl,
|
|
428
|
+
resolveAdoThreadStatus,
|
|
180
429
|
ADO_API_VERSION,
|
|
430
|
+
ADO_THREAD_STATUS_UNKNOWN,
|
|
181
431
|
ADO_THREAD_STATUS_ACTIVE,
|
|
432
|
+
ADO_THREAD_STATUS_FIXED,
|
|
433
|
+
ADO_THREAD_STATUS_WONT_FIX,
|
|
182
434
|
ADO_THREAD_STATUS_CLOSED,
|
|
435
|
+
ADO_THREAD_STATUS_BY_DESIGN,
|
|
436
|
+
ADO_THREAD_STATUS_PENDING,
|
|
437
|
+
ADO_THREAD_STATUS_BY_NAME,
|
|
438
|
+
ADO_RESOLVED_THREAD_STATUSES,
|
|
183
439
|
// Re-export the neutral builder so ADO callers have a single import surface,
|
|
184
440
|
// mirroring engine/providers/gh-comment.js.
|
|
185
441
|
buildMinionsCommentBody,
|
|
@@ -188,5 +444,6 @@ module.exports = {
|
|
|
188
444
|
_validateProject,
|
|
189
445
|
_validateRepositoryId,
|
|
190
446
|
_validatePrNumber,
|
|
447
|
+
_validateThreadId,
|
|
191
448
|
_defaultAcquireToken,
|
|
192
449
|
};
|
package/engine/agents/llm.js
CHANGED
|
@@ -55,6 +55,26 @@ function _emptyDailyDelta() {
|
|
|
55
55
|
return { costUsd: 0, inputTokens: 0, outputTokens: 0, cacheRead: 0 };
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
// Merge a source billableUnits map ({unit:{total,reported,unavailable}}) into a
|
|
59
|
+
// target object's billableUnits (W-msdcgitc). Used to fold buffered CC/doc-chat
|
|
60
|
+
// deltas into the persisted metrics and to re-merge on a failed flush. Generic
|
|
61
|
+
// over unit name so no runtime-name branch is needed.
|
|
62
|
+
function _mergeBillableUnits(target, source) {
|
|
63
|
+
if (!source || typeof source !== 'object') return;
|
|
64
|
+
if (!target.billableUnits || typeof target.billableUnits !== 'object') {
|
|
65
|
+
target.billableUnits = {};
|
|
66
|
+
}
|
|
67
|
+
for (const [unit, b] of Object.entries(source)) {
|
|
68
|
+
if (!b || typeof b !== 'object') continue;
|
|
69
|
+
const bucket = target.billableUnits[unit]
|
|
70
|
+
|| { total: 0, reported: 0, unavailable: 0 };
|
|
71
|
+
bucket.total += b.total || 0;
|
|
72
|
+
bucket.reported += b.reported || 0;
|
|
73
|
+
bucket.unavailable += b.unavailable || 0;
|
|
74
|
+
target.billableUnits[unit] = bucket;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
58
78
|
function _ensureFlushTimer() {
|
|
59
79
|
if (_flushTimer) return;
|
|
60
80
|
const interval = shared.ENGINE_DEFAULTS.metricsFlushIntervalMs || 10000;
|
|
@@ -74,6 +94,7 @@ function trackEngineUsage(category, usage) {
|
|
|
74
94
|
cat.outputTokens += usage.outputTokens || 0;
|
|
75
95
|
cat.cacheRead += usage.cacheRead || 0;
|
|
76
96
|
cat.cacheCreation += usage.cacheCreation || 0;
|
|
97
|
+
shared.accumulateBillableUnits(cat, usage.billable);
|
|
77
98
|
if (usage.durationMs) {
|
|
78
99
|
cat.totalDurationMs += usage.durationMs;
|
|
79
100
|
cat.timedCalls += 1;
|
|
@@ -86,6 +107,7 @@ function trackEngineUsage(category, usage) {
|
|
|
86
107
|
daily.inputTokens += usage.inputTokens || 0;
|
|
87
108
|
daily.outputTokens += usage.outputTokens || 0;
|
|
88
109
|
daily.cacheRead += usage.cacheRead || 0;
|
|
110
|
+
shared.accumulateBillableUnits(daily, usage.billable);
|
|
89
111
|
|
|
90
112
|
_ensureFlushTimer();
|
|
91
113
|
}
|
|
@@ -135,6 +157,7 @@ function flushMetricsBuffer() {
|
|
|
135
157
|
cat.errorsByCode[code] = (cat.errorsByCode[code] || 0) + count;
|
|
136
158
|
}
|
|
137
159
|
}
|
|
160
|
+
_mergeBillableUnits(cat, delta.billableUnits);
|
|
138
161
|
}
|
|
139
162
|
if (!metrics._daily) metrics._daily = {};
|
|
140
163
|
for (const [day, delta] of Object.entries(pending.daily)) {
|
|
@@ -146,6 +169,7 @@ function flushMetricsBuffer() {
|
|
|
146
169
|
d.inputTokens += delta.inputTokens;
|
|
147
170
|
d.outputTokens += delta.outputTokens;
|
|
148
171
|
d.cacheRead += delta.cacheRead;
|
|
172
|
+
_mergeBillableUnits(d, delta.billableUnits);
|
|
149
173
|
}
|
|
150
174
|
return metrics;
|
|
151
175
|
});
|
|
@@ -164,12 +188,14 @@ function flushMetricsBuffer() {
|
|
|
164
188
|
c.errorsByCode[code] = (c.errorsByCode[code] || 0) + count;
|
|
165
189
|
}
|
|
166
190
|
}
|
|
191
|
+
_mergeBillableUnits(c, delta.billableUnits);
|
|
167
192
|
}
|
|
168
193
|
for (const [day, delta] of Object.entries(pending.daily)) {
|
|
169
194
|
if (!_pendingMetrics.daily[day]) _pendingMetrics.daily[day] = _emptyDailyDelta();
|
|
170
195
|
const d = _pendingMetrics.daily[day];
|
|
171
196
|
d.costUsd += delta.costUsd; d.inputTokens += delta.inputTokens;
|
|
172
197
|
d.outputTokens += delta.outputTokens; d.cacheRead += delta.cacheRead;
|
|
198
|
+
_mergeBillableUnits(d, delta.billableUnits);
|
|
173
199
|
}
|
|
174
200
|
console.error('metrics flush:', e.message);
|
|
175
201
|
}
|
|
@@ -1373,7 +1373,8 @@ function _checkPlaybookSections(content, context = '') {
|
|
|
1373
1373
|
|
|
1374
1374
|
// Lean system prompt: agent identity + rules only (~2-4KB, never grows)
|
|
1375
1375
|
function buildSystemPrompt(agentId, config, project) {
|
|
1376
|
-
const agent = config.agents[agentId] || tempAgents.get(agentId)
|
|
1376
|
+
const agent = config.agents[agentId] || tempAgents.get(agentId)
|
|
1377
|
+
|| { name: shared.tempAgentDisplayName(agentId), role: 'Temporary Agent', expertise: [] };
|
|
1377
1378
|
const charter = safeRead(path.join(AGENTS_DIR, agentId, 'charter.md')); // returns '' for temp agents (no charter file)
|
|
1378
1379
|
project = project || getProjects(config)[0] || {};
|
|
1379
1380
|
|
|
@@ -108,6 +108,7 @@ const ENGINE_SPECIAL_FIELDS = new Set([
|
|
|
108
108
|
'ccWorkerIdleTimeoutMs',
|
|
109
109
|
'worktreeRoot',
|
|
110
110
|
'operatorLogin',
|
|
111
|
+
'operatorAdoIdentity',
|
|
111
112
|
'defaultCli',
|
|
112
113
|
'defaultModel',
|
|
113
114
|
'ccCli',
|
|
@@ -338,6 +339,30 @@ function applyEngineSettings(candidate, bodyEngine, errors) {
|
|
|
338
339
|
const value = parseClearableString(bodyEngine.operatorLogin, 'engine.operatorLogin', errors);
|
|
339
340
|
setOrDelete(engine, 'operatorLogin', value);
|
|
340
341
|
}
|
|
342
|
+
if (hasOwn(bodyEngine, 'operatorAdoIdentity')) {
|
|
343
|
+
// Structured ADO self-identity used ONLY by the AutoFix foreign-author
|
|
344
|
+
// guardrail (id/descriptor — never email). An empty object clears it.
|
|
345
|
+
const raw = bodyEngine.operatorAdoIdentity;
|
|
346
|
+
if (raw === null || raw === undefined || CLEAR_SENTINELS.has(raw)) {
|
|
347
|
+
delete engine.operatorAdoIdentity;
|
|
348
|
+
} else if (typeof raw === 'object') {
|
|
349
|
+
const next = {};
|
|
350
|
+
if (raw.id !== undefined && raw.id !== null && raw.id !== '') {
|
|
351
|
+
const id = parseString(raw.id, 'engine.operatorAdoIdentity.id', errors);
|
|
352
|
+
if (id) next.id = id;
|
|
353
|
+
}
|
|
354
|
+
if (raw.descriptor !== undefined && raw.descriptor !== null && raw.descriptor !== '') {
|
|
355
|
+
const descriptor = parseString(raw.descriptor, 'engine.operatorAdoIdentity.descriptor', errors);
|
|
356
|
+
if (descriptor) next.descriptor = descriptor;
|
|
357
|
+
}
|
|
358
|
+
if (Object.keys(next).length) engine.operatorAdoIdentity = next;
|
|
359
|
+
else delete engine.operatorAdoIdentity;
|
|
360
|
+
} else {
|
|
361
|
+
errors.add('engine.operatorAdoIdentity', 'invalid-object',
|
|
362
|
+
'engine.operatorAdoIdentity must be an object with id and/or descriptor', raw,
|
|
363
|
+
{ expected: 'object' });
|
|
364
|
+
}
|
|
365
|
+
}
|
|
341
366
|
for (const key of ['defaultCli', 'ccCli']) {
|
|
342
367
|
if (!hasOwn(bodyEngine, key)) continue;
|
|
343
368
|
const value = parseRuntimeName(bodyEngine[key], `engine.${key}`, errors);
|
|
@@ -75,9 +75,31 @@ function resolveOperatorLogin(config, { force = false } = {}) {
|
|
|
75
75
|
|
|
76
76
|
// Clear the process-lifetime cache. Called by the dashboard when the operator
|
|
77
77
|
// login override changes in Settings so the next resolve picks up the new value.
|
|
78
|
-
function _resetOperatorLoginCacheForTest() { _cached = null; }
|
|
78
|
+
function _resetOperatorLoginCacheForTest() { _cached = null; _cachedGithubViewer = null; }
|
|
79
|
+
|
|
80
|
+
// ── Strict GitHub viewer login (W-msd8pr2300ev95e1) ─────────────────────────
|
|
81
|
+
// Unlike resolveOperatorLogin, this NEVER falls back to the git-email localpart
|
|
82
|
+
// or the OS username — those are not GitHub logins and must never be used to
|
|
83
|
+
// decide "did the current GitHub token author this PR?". It accepts ONLY:
|
|
84
|
+
// 1. `config.engine.operatorLogin` — explicit Settings override
|
|
85
|
+
// 2. `gh api user --jq .login` — the token's own canonical login
|
|
86
|
+
// and returns '' otherwise, so the caller fails CLOSED (author ownership
|
|
87
|
+
// "unknown") rather than guessing self-authorship from a weak fallback.
|
|
88
|
+
let _cachedGithubViewer = null;
|
|
89
|
+
function resolveGithubViewerLoginStrict(config, { force = false } = {}) {
|
|
90
|
+
if (!force && _cachedGithubViewer !== null) return _cachedGithubViewer;
|
|
91
|
+
const override = config?.engine?.operatorLogin;
|
|
92
|
+
if (override && typeof override === 'string' && override.trim()) {
|
|
93
|
+
_cachedGithubViewer = override.trim();
|
|
94
|
+
return _cachedGithubViewer;
|
|
95
|
+
}
|
|
96
|
+
const ghLogin = _execImpl('gh api user --jq .login');
|
|
97
|
+
_cachedGithubViewer = ghLogin ? ghLogin : '';
|
|
98
|
+
return _cachedGithubViewer;
|
|
99
|
+
}
|
|
79
100
|
|
|
80
101
|
module.exports = {
|
|
81
102
|
resolveOperatorLogin,
|
|
103
|
+
resolveGithubViewerLoginStrict,
|
|
82
104
|
_resetOperatorLoginCacheForTest,
|
|
83
105
|
};
|
package/engine/core/queries.js
CHANGED
|
@@ -501,9 +501,15 @@ function getMetrics() {
|
|
|
501
501
|
const runtimeName = shared.resolveAgentCli(configuredAgents[agentId], engineConfig);
|
|
502
502
|
m.runtime = runtimeName;
|
|
503
503
|
try {
|
|
504
|
-
|
|
504
|
+
const caps = resolveRuntime(runtimeName).capabilities || {};
|
|
505
|
+
m.costTracking = caps.costTracking === true;
|
|
506
|
+
// Typed native billable unit for the display layer (W-msdcgitc). Copilot
|
|
507
|
+
// reports 'premiumRequests'; adapters without one default to 'usd'. The
|
|
508
|
+
// dashboard reads THIS, never the runtime name, to decide labels/units.
|
|
509
|
+
m.billableUnit = typeof caps.billableUnit === 'string' ? caps.billableUnit : 'usd';
|
|
505
510
|
} catch {
|
|
506
511
|
m.costTracking = false;
|
|
512
|
+
m.billableUnit = 'usd';
|
|
507
513
|
}
|
|
508
514
|
}
|
|
509
515
|
|
|
@@ -895,7 +901,21 @@ function getAgents(config) {
|
|
|
895
901
|
const seen = new Set(roster.map(a => a.id));
|
|
896
902
|
for (const d of (dispatch.active || [])) {
|
|
897
903
|
if (d.agent && d.agent.startsWith('temp-') && !seen.has(d.agent)) {
|
|
898
|
-
|
|
904
|
+
// Additive friendly display identity; the canonical `id` (temp-<uid>)
|
|
905
|
+
// stays the machine identity for routing/SQL/paths/memory. `name` falls
|
|
906
|
+
// back to the deterministic call sign when the dispatch record predates
|
|
907
|
+
// friendly naming (e.g. after a restart with no in-memory tempAgents).
|
|
908
|
+
const identity = shared.resolveAgentIdentity(d.agent, config);
|
|
909
|
+
roster.push({
|
|
910
|
+
id: d.agent,
|
|
911
|
+
name: d.agentName || identity.displayName || d.agent,
|
|
912
|
+
role: d.agentRole || 'Temporary Agent',
|
|
913
|
+
emoji: '\u{1F4A8}',
|
|
914
|
+
expertise: [],
|
|
915
|
+
_temp: true,
|
|
916
|
+
isTemp: true,
|
|
917
|
+
callSign: identity.callSign,
|
|
918
|
+
});
|
|
899
919
|
seen.add(d.agent);
|
|
900
920
|
}
|
|
901
921
|
}
|
|
@@ -949,8 +969,13 @@ function getAgents(config) {
|
|
|
949
969
|
|
|
950
970
|
const chartered = fs.existsSync(path.join(AGENTS_DIR, a.id, 'charter.md'));
|
|
951
971
|
if (lastAction.length > 120) lastAction = lastAction.slice(0, 120) + '...';
|
|
972
|
+
// Structured display identity (canonical id stays authoritative; friendly
|
|
973
|
+
// name + temp flag are additive). Clients render a "Temp" badge from `isTemp`.
|
|
974
|
+
const isTemp = a.isTemp === true || shared.isTempAgentId(a.id);
|
|
952
975
|
return {
|
|
953
976
|
...a,
|
|
977
|
+
isTemp,
|
|
978
|
+
callSign: isTemp ? (a.callSign || shared.tempAgentCallSign(a.id)) : null,
|
|
954
979
|
runtime,
|
|
955
980
|
model,
|
|
956
981
|
displayModel: modelDisplay.model,
|
|
@@ -1151,6 +1176,28 @@ function getPullRequests(config) {
|
|
|
1151
1176
|
const projectByName = new Map(projects.map(p => [p.name, p]));
|
|
1152
1177
|
const allPrs = [];
|
|
1153
1178
|
|
|
1179
|
+
// W-msd8pr2300ev95e1 — resolve the CURRENT authenticated identity per provider
|
|
1180
|
+
// ONCE (cached) so we can stamp each PR's author-ownership tri-state for the
|
|
1181
|
+
// AutoFix foreign-author guardrail. STABLE identities only (GitHub canonical
|
|
1182
|
+
// login / numeric id; ADO id / descriptor) — never display names or emails.
|
|
1183
|
+
// Fails CLOSED: a missing/blank provider entry yields 'unknown'.
|
|
1184
|
+
const selfByProvider = {};
|
|
1185
|
+
try {
|
|
1186
|
+
const ghLogin = require('./operator-identity').resolveGithubViewerLoginStrict(config);
|
|
1187
|
+
if (ghLogin) selfByProvider.github = { login: ghLogin };
|
|
1188
|
+
} catch { /* identity resolution is best-effort; absence → unknown */ }
|
|
1189
|
+
try {
|
|
1190
|
+
const adoSelf = config?.engine?.operatorAdoIdentity;
|
|
1191
|
+
if (adoSelf && typeof adoSelf === 'object') {
|
|
1192
|
+
const entry = {};
|
|
1193
|
+
if (adoSelf.id) entry.id = String(adoSelf.id);
|
|
1194
|
+
if (adoSelf.descriptor) entry.descriptor = String(adoSelf.descriptor);
|
|
1195
|
+
// Deliberately NOT copying uniqueName/login: ADO login is an email, and
|
|
1196
|
+
// the guardrail contract forbids comparing emails.
|
|
1197
|
+
if (Object.keys(entry).length) selfByProvider.ado = entry;
|
|
1198
|
+
}
|
|
1199
|
+
} catch { /* best-effort */ }
|
|
1200
|
+
|
|
1154
1201
|
// SQL is the canonical (and only) PR store after Phase 9.
|
|
1155
1202
|
const store = require('../persistence/pull-requests-store');
|
|
1156
1203
|
const sqlPrs = store.readAllPullRequests() || [];
|
|
@@ -1231,6 +1278,10 @@ function getPullRequests(config) {
|
|
|
1231
1278
|
// dashboards / API consumers can render a chip without re-parsing
|
|
1232
1279
|
// _noOpFixes themselves.
|
|
1233
1280
|
pr._pausedCauses = shared.getPrPausedCauses(pr);
|
|
1281
|
+
// W-msd8pr2300ev95e1 — author-ownership tri-state for the AutoFix
|
|
1282
|
+
// foreign-author warning. 'self' | 'foreign' | 'unknown' (fail closed).
|
|
1283
|
+
try { pr._authorOwnership = shared.resolvePrAuthorOwnership(pr, selfByProvider); }
|
|
1284
|
+
catch { pr._authorOwnership = shared.PR_AUTHOR_OWNERSHIP.UNKNOWN; }
|
|
1234
1285
|
allPrs.push(pr);
|
|
1235
1286
|
}
|
|
1236
1287
|
allPrs.sort((a, b) => {
|