@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/core/shared.js
CHANGED
|
@@ -2044,6 +2044,116 @@ function uid() {
|
|
|
2044
2044
|
return Date.now().toString(36) + _uidCounter.toString(36).padStart(4, '0') + crypto.randomBytes(2).toString('hex');
|
|
2045
2045
|
}
|
|
2046
2046
|
|
|
2047
|
+
// ---------------------------------------------------------------------------
|
|
2048
|
+
// Temporary-agent friendly display identity (W-msd8zdzb00i550a8)
|
|
2049
|
+
//
|
|
2050
|
+
// Dynamically-created temp agents keep their canonical id (`temp-<uid>`) for
|
|
2051
|
+
// ALL machine identity — routing, SQL keys, filesystem paths, logs, APIs,
|
|
2052
|
+
// process ownership, memory rules, reconciliation. Friendly naming here is a
|
|
2053
|
+
// purely ADDITIVE display identity; it never renames the canonical id.
|
|
2054
|
+
//
|
|
2055
|
+
// The friendly call sign is DERIVED DETERMINISTICALLY from the immutable
|
|
2056
|
+
// canonical id, so it is stable for the agent's whole lifetime and across
|
|
2057
|
+
// engine/dashboard restarts and in historical records — with no persisted
|
|
2058
|
+
// state (no SQL, no JSON). The active set of agents never influences the
|
|
2059
|
+
// derivation, which is exactly what "preserve an already assigned name rather
|
|
2060
|
+
// than renaming when the active set changes" requires.
|
|
2061
|
+
//
|
|
2062
|
+
// Collision handling is deterministic: the call sign is an `<adjective> <name>`
|
|
2063
|
+
// pair, each element chosen from a curated, roster-disjoint vocabulary via an
|
|
2064
|
+
// independent FNV-1a hash stream of the canonical id. The combined space keeps
|
|
2065
|
+
// concurrently-visible temp agents distinct, and the curated `name` list is
|
|
2066
|
+
// disjoint from the shipped roster (Ripley/Dallas/Lambert/Rebecca/Ralph) so a
|
|
2067
|
+
// call sign never equals a configured display name. The always-visible "Temp"
|
|
2068
|
+
// badge disambiguates even in the rare full collision, and the canonical id
|
|
2069
|
+
// stays discoverable in tooltips/detail for debugging.
|
|
2070
|
+
//
|
|
2071
|
+
// NOTE: dashboard/js/agent-identity.js mirrors this vocabulary + algorithm for
|
|
2072
|
+
// a client-side fallback. Keep the two in sync (asserted by
|
|
2073
|
+
// test/unit/temp-agent-identity.test.js).
|
|
2074
|
+
const TEMP_AGENT_CALL_SIGN_ADJECTIVES = [
|
|
2075
|
+
'Cosmic', 'Starry', 'Nebular', 'Twinkly', 'Lunar', 'Solar', 'Astral', 'Galactic',
|
|
2076
|
+
'Stellar', 'Orbital', 'Meteoric', 'Cloudy', 'Sunny', 'Radiant', 'Sparkly', 'Dreamy',
|
|
2077
|
+
'Breezy', 'Drifting', 'Glowing', 'Floaty', 'Skybound', 'Wandering', 'Whirling', 'Shimmery',
|
|
2078
|
+
];
|
|
2079
|
+
const TEMP_AGENT_CALL_SIGN_NAMES = [
|
|
2080
|
+
'Comet', 'Nova', 'Luna', 'Sol', 'Astro', 'Orion', 'Nebula', 'Pluto',
|
|
2081
|
+
'Halley', 'Vega', 'Rigel', 'Sirius', 'Polaris', 'Stardust', 'Cosmo', 'Nimbus',
|
|
2082
|
+
'Zephyr', 'Aurora', 'Meteor', 'Galaxy', 'Twinkle', 'Cirrus', 'Cygnus', 'Juno',
|
|
2083
|
+
];
|
|
2084
|
+
|
|
2085
|
+
// Case-insensitive canonical-temp-id test. Legacy mixed-case `Temp-*` ids are
|
|
2086
|
+
// treated as temp too; machine identity is unchanged, only display is affected.
|
|
2087
|
+
function isTempAgentId(id) {
|
|
2088
|
+
return typeof id === 'string' && /^temp-/i.test(id.trim());
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
// FNV-1a 32-bit over a string → non-negative integer. Deterministic and
|
|
2092
|
+
// dependency-free; mirrored verbatim on the client.
|
|
2093
|
+
function _fnv1a32(str) {
|
|
2094
|
+
let h = 0x811c9dc5;
|
|
2095
|
+
for (let i = 0; i < str.length; i++) {
|
|
2096
|
+
h ^= str.charCodeAt(i);
|
|
2097
|
+
h = (h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24))) >>> 0;
|
|
2098
|
+
}
|
|
2099
|
+
return h >>> 0;
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
/**
|
|
2103
|
+
* Deterministic friendly call sign for a temp agent id, e.g. "Cosmic Comet".
|
|
2104
|
+
* Returns null for non-temp / malformed ids so callers can fall back to the
|
|
2105
|
+
* canonical id. Derivation depends ONLY on the immutable canonical id.
|
|
2106
|
+
*/
|
|
2107
|
+
function tempAgentCallSign(id) {
|
|
2108
|
+
if (!isTempAgentId(id)) return null;
|
|
2109
|
+
const key = id.trim().toLowerCase();
|
|
2110
|
+
// Single hash of the canonical id, then slice independent index streams from
|
|
2111
|
+
// its low and high bits. Deriving both indices from ONE hash of the bare id
|
|
2112
|
+
// (rather than hashing `id+':adj'` / `id+':name'`) is deliberate: a trailing
|
|
2113
|
+
// discriminator suffix lets the identical suffix bytes dominate FNV-1a's low
|
|
2114
|
+
// bits, collapsing the joint (adjective,name) distribution for the
|
|
2115
|
+
// long-shared-prefix ids that concurrent temp allocation produces. Slicing
|
|
2116
|
+
// one hash keeps the full id entropy in both indices.
|
|
2117
|
+
const h = _fnv1a32(key);
|
|
2118
|
+
const adj = TEMP_AGENT_CALL_SIGN_ADJECTIVES[h % TEMP_AGENT_CALL_SIGN_ADJECTIVES.length];
|
|
2119
|
+
const name = TEMP_AGENT_CALL_SIGN_NAMES[
|
|
2120
|
+
Math.floor(h / TEMP_AGENT_CALL_SIGN_ADJECTIVES.length) % TEMP_AGENT_CALL_SIGN_NAMES.length
|
|
2121
|
+
];
|
|
2122
|
+
return `${adj} ${name}`;
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
/**
|
|
2126
|
+
* Friendly display name for any agent id. For temp ids this is the deterministic
|
|
2127
|
+
* call sign; for configured/permanent agents it is the configured display name
|
|
2128
|
+
* (or the id when unknown). The canonical id is never mutated.
|
|
2129
|
+
*/
|
|
2130
|
+
function tempAgentDisplayName(id) {
|
|
2131
|
+
return tempAgentCallSign(id) || (typeof id === 'string' ? id : '');
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
/**
|
|
2135
|
+
* Structured display identity for an agent id. Returns canonical id, friendly
|
|
2136
|
+
* display name, the call sign (temp only), the temporary flag, and a role hint.
|
|
2137
|
+
* API payloads should expose these structured fields rather than replacing ids
|
|
2138
|
+
* with formatted strings, so id-based clients stay compatible.
|
|
2139
|
+
*/
|
|
2140
|
+
function resolveAgentIdentity(id, config = null) {
|
|
2141
|
+
const canonicalId = typeof id === 'string' ? id : '';
|
|
2142
|
+
const temp = isTempAgentId(canonicalId);
|
|
2143
|
+
const configured = config && config.agents ? config.agents[canonicalId] : null;
|
|
2144
|
+
if (temp) {
|
|
2145
|
+
const callSign = tempAgentCallSign(canonicalId);
|
|
2146
|
+
return { id: canonicalId, displayName: callSign, callSign, isTemp: true, role: 'Temporary Agent' };
|
|
2147
|
+
}
|
|
2148
|
+
return {
|
|
2149
|
+
id: canonicalId,
|
|
2150
|
+
displayName: (configured && configured.name) || canonicalId,
|
|
2151
|
+
callSign: null,
|
|
2152
|
+
isTemp: false,
|
|
2153
|
+
role: (configured && configured.role) || 'Agent',
|
|
2154
|
+
};
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2047
2157
|
/**
|
|
2048
2158
|
* Return a unique filepath by appending -2, -3, etc. if the file already exists.
|
|
2049
2159
|
* E.g. uniquePath('/plans/foo.json') → '/plans/foo-2.json' if foo.json exists.
|
|
@@ -5937,6 +6047,39 @@ function waitForDispatchFinalization(dispatchId) {
|
|
|
5937
6047
|
// Structured completion protocol — fields agents must produce in ```completion blocks
|
|
5938
6048
|
const COMPLETION_FIELDS = ['status', 'summary', 'files_changed', 'tests', 'pr', 'not_changed', 'failure_class', 'retryable', 'needs_rerun', 'verdict', 'artifacts', 'nonce', 'securityFlags', 'reviewLearnings', 'reviewLearningApplications'];
|
|
5939
6049
|
|
|
6050
|
+
// Generic runtime-reported billable-unit accumulator (W-msdcgitc).
|
|
6051
|
+
//
|
|
6052
|
+
// Adapters may report a NON-USD native billable unit — e.g. the Copilot CLI's
|
|
6053
|
+
// premium-request count — via `usage.billable = { unit, value, reported }`. This
|
|
6054
|
+
// folds such reports into `target.billableUnits[unit] = { total, reported,
|
|
6055
|
+
// unavailable }` so the engine accumulates credits by dispatch/agent/day WITHOUT
|
|
6056
|
+
// ever branching on the runtime name. USD is deliberately excluded: dollar cost
|
|
6057
|
+
// flows through the richer `costUsd` + token path, so routing it here too would
|
|
6058
|
+
// double-count. A report whose `reported` is not true (the CLI did not expose the
|
|
6059
|
+
// field for this run) is counted as an `unavailable` observation and does NOT
|
|
6060
|
+
// advance `total`/`reported`, keeping "unreported" distinct from a genuine 0.
|
|
6061
|
+
// The adapter value is already scoped to the finalized unit of work (per
|
|
6062
|
+
// invocation / per pooled turn), so summing is correct and no delta-vs-cumulative
|
|
6063
|
+
// diffing is required here.
|
|
6064
|
+
function accumulateBillableUnits(target, billable) {
|
|
6065
|
+
if (!target || typeof target !== 'object') return;
|
|
6066
|
+
if (!billable || typeof billable !== 'object') return;
|
|
6067
|
+
const unit = billable.unit;
|
|
6068
|
+
if (typeof unit !== 'string' || !unit || unit === 'usd') return;
|
|
6069
|
+
if (!target.billableUnits || typeof target.billableUnits !== 'object') {
|
|
6070
|
+
target.billableUnits = {};
|
|
6071
|
+
}
|
|
6072
|
+
const bucket = target.billableUnits[unit]
|
|
6073
|
+
|| { total: 0, reported: 0, unavailable: 0 };
|
|
6074
|
+
if (billable.reported === true && Number.isFinite(billable.value)) {
|
|
6075
|
+
bucket.total += billable.value;
|
|
6076
|
+
bucket.reported += 1;
|
|
6077
|
+
} else {
|
|
6078
|
+
bucket.unavailable += 1;
|
|
6079
|
+
}
|
|
6080
|
+
target.billableUnits[unit] = bucket;
|
|
6081
|
+
}
|
|
6082
|
+
|
|
5940
6083
|
const DEFAULT_AGENT_METRICS = {
|
|
5941
6084
|
tasksCompleted: 0, tasksErrored: 0,
|
|
5942
6085
|
prsCreated: 0, prsApproved: 0, prsRejected: 0, prsMerged: 0,
|
|
@@ -8702,11 +8845,113 @@ function mergePrAuthorIdentity(existing, incoming) {
|
|
|
8702
8845
|
return out;
|
|
8703
8846
|
}
|
|
8704
8847
|
|
|
8705
|
-
//
|
|
8706
|
-
|
|
8848
|
+
// A human-readable name from ONE identity fragment that never contains an email
|
|
8849
|
+
// address:
|
|
8850
|
+
// - "Display Name <user@example.com>" → "Display Name"
|
|
8851
|
+
// - "user@example.com" → "user" (local-part, domain dropped)
|
|
8852
|
+
// - "Display Name" → "Display Name"
|
|
8853
|
+
// Any '@' anywhere forces local-part extraction, so a malformed address can
|
|
8854
|
+
// never leak through. Returns '' when nothing usable remains.
|
|
8855
|
+
function _authorFragmentName(raw) {
|
|
8856
|
+
let s = _authorField(raw, 256);
|
|
8857
|
+
if (!s) return '';
|
|
8858
|
+
// "Name <addr>" — keep the display part when it is a real (non-email) name.
|
|
8859
|
+
const m = s.match(/^\s*"?([^"<]*?)"?\s*<[^>]*>\s*$/);
|
|
8860
|
+
if (m && m[1].trim() && m[1].indexOf('@') === -1) return m[1].trim();
|
|
8861
|
+
if (s.indexOf('@') !== -1) {
|
|
8862
|
+
s = s.replace(/<([^>]*)>/, '$1'); // unwrap a "<addr>"
|
|
8863
|
+
return s.split('@')[0].replace(/^["'<\s]+|["'>\s]+$/g, '').trim();
|
|
8864
|
+
}
|
|
8865
|
+
return s;
|
|
8866
|
+
}
|
|
8867
|
+
|
|
8868
|
+
// Human-readable author name that NEVER exposes an email address. Prefers a real
|
|
8869
|
+
// provider display name / login, and only when the sole usable identity is an
|
|
8870
|
+
// email does it fall back to the local-part (no domain). Returns '' (→ renders
|
|
8871
|
+
// "Unknown") when nothing usable is present. The structured identity fields
|
|
8872
|
+
// (login/id/descriptor) are untouched — this is a presentation label only.
|
|
8873
|
+
function prAuthorDisplayName(author) {
|
|
8707
8874
|
const a = normalizePrAuthorIdentity(author);
|
|
8708
8875
|
if (!a) return '';
|
|
8709
|
-
return a.displayName
|
|
8876
|
+
return _authorFragmentName(a.displayName)
|
|
8877
|
+
|| _authorFragmentName(a.login)
|
|
8878
|
+
|| _authorField(a.id, 64)
|
|
8879
|
+
|| '';
|
|
8880
|
+
}
|
|
8881
|
+
|
|
8882
|
+
// Human-readable label for a PR author (typed SQL column + filter/search text).
|
|
8883
|
+
// Routed through prAuthorDisplayName so the projected label is always email-free.
|
|
8884
|
+
function prAuthorLabel(author) {
|
|
8885
|
+
return prAuthorDisplayName(author);
|
|
8886
|
+
}
|
|
8887
|
+
|
|
8888
|
+
// ── Foreign-author ownership for the AutoFix guardrail (W-msd8pr2300ev95e1) ──
|
|
8889
|
+
//
|
|
8890
|
+
// Decide whether the CURRENT authenticated identity for a PR's provider authored
|
|
8891
|
+
// that PR, so the dashboard can warn when auto-fix is enabled on someone else's
|
|
8892
|
+
// PR (Minions would push commits to a PR the operator only asked to read/review).
|
|
8893
|
+
//
|
|
8894
|
+
// STABLE identities only — never display names or emails:
|
|
8895
|
+
// GitHub: canonical login (case-insensitive) or numeric id.
|
|
8896
|
+
// ADO: identity id or descriptor (exact), or uniqueName login (ci).
|
|
8897
|
+
//
|
|
8898
|
+
// Fail CLOSED: returns 'unknown' whenever EITHER the PR author identity or the
|
|
8899
|
+
// current provider identity cannot be established. The caller must render an
|
|
8900
|
+
// "author ownership unknown" warning rather than treating unknown as self.
|
|
8901
|
+
//
|
|
8902
|
+
// `selfByProvider` is `{ github: { login, id }, ado: { id, descriptor, login } }`
|
|
8903
|
+
// resolved by the caller (queries.js) from the per-provider token identity; a
|
|
8904
|
+
// missing provider entry (or empty fields) yields 'unknown' for that provider.
|
|
8905
|
+
const PR_AUTHOR_OWNERSHIP = { SELF: 'self', FOREIGN: 'foreign', UNKNOWN: 'unknown' };
|
|
8906
|
+
|
|
8907
|
+
function _ciEq(a, b) {
|
|
8908
|
+
const x = _authorField(a, 256).toLowerCase();
|
|
8909
|
+
const y = _authorField(b, 256).toLowerCase();
|
|
8910
|
+
return !!x && x === y;
|
|
8911
|
+
}
|
|
8912
|
+
function _exactEq(a, b) {
|
|
8913
|
+
const x = _authorField(a, 256);
|
|
8914
|
+
const y = _authorField(b, 256);
|
|
8915
|
+
return !!x && x === y;
|
|
8916
|
+
}
|
|
8917
|
+
|
|
8918
|
+
function _prProviderFromRecord(pr, author) {
|
|
8919
|
+
const p = _authorField(author && author.provider, 32).toLowerCase();
|
|
8920
|
+
if (p === 'github' || p === 'ado') return p;
|
|
8921
|
+
const id = _authorField(pr && pr.id, 128).toLowerCase();
|
|
8922
|
+
if (id.startsWith('github:') || id.startsWith('gh:')) return 'github';
|
|
8923
|
+
if (id.startsWith('ado:')) return 'ado';
|
|
8924
|
+
const url = _authorField(pr && pr.url, 400).toLowerCase();
|
|
8925
|
+
if (url.includes('github.com')) return 'github';
|
|
8926
|
+
if (url.includes('dev.azure.com') || url.includes('visualstudio.com') || url.includes('/_git/')) return 'ado';
|
|
8927
|
+
return '';
|
|
8928
|
+
}
|
|
8929
|
+
|
|
8930
|
+
function resolvePrAuthorOwnership(pr, selfByProvider) {
|
|
8931
|
+
const author = normalizePrAuthorIdentity(pr && pr.author);
|
|
8932
|
+
if (!author) return PR_AUTHOR_OWNERSHIP.UNKNOWN;
|
|
8933
|
+
const provider = _prProviderFromRecord(pr, author);
|
|
8934
|
+
if (!provider) return PR_AUTHOR_OWNERSHIP.UNKNOWN;
|
|
8935
|
+
const self = selfByProvider && selfByProvider[provider];
|
|
8936
|
+
if (!self) return PR_AUTHOR_OWNERSHIP.UNKNOWN;
|
|
8937
|
+
|
|
8938
|
+
if (provider === 'github') {
|
|
8939
|
+
const haveSelf = _authorField(self.login, 256) || _authorField(self.id, 64);
|
|
8940
|
+
const haveAuthor = _authorField(author.login, 256) || _authorField(author.id, 64);
|
|
8941
|
+
if (!haveSelf || !haveAuthor) return PR_AUTHOR_OWNERSHIP.UNKNOWN;
|
|
8942
|
+
if (_exactEq(self.id, author.id) || _ciEq(self.login, author.login)) return PR_AUTHOR_OWNERSHIP.SELF;
|
|
8943
|
+
return PR_AUTHOR_OWNERSHIP.FOREIGN;
|
|
8944
|
+
}
|
|
8945
|
+
// ADO: compare stable identity id/descriptor ONLY. ADO `login` is the
|
|
8946
|
+
// uniqueName (a sign-in email), and the guardrail contract forbids comparing
|
|
8947
|
+
// emails, so it is deliberately excluded here.
|
|
8948
|
+
const haveSelf = _authorField(self.id, 64) || _authorField(self.descriptor, 256);
|
|
8949
|
+
const haveAuthor = _authorField(author.id, 64) || _authorField(author.descriptor, 256);
|
|
8950
|
+
if (!haveSelf || !haveAuthor) return PR_AUTHOR_OWNERSHIP.UNKNOWN;
|
|
8951
|
+
if (_exactEq(self.id, author.id) || _exactEq(self.descriptor, author.descriptor)) {
|
|
8952
|
+
return PR_AUTHOR_OWNERSHIP.SELF;
|
|
8953
|
+
}
|
|
8954
|
+
return PR_AUTHOR_OWNERSHIP.FOREIGN;
|
|
8710
8955
|
}
|
|
8711
8956
|
|
|
8712
8957
|
function _jsonEqual(a, b) {
|
|
@@ -9396,16 +9641,35 @@ function classifyPrRefForVerification(prRef, project = null) {
|
|
|
9396
9641
|
// .parent_pr_url). Description-only PR mentions are INTENTIONALLY skipped
|
|
9397
9642
|
// per the "structured-vs-loose split" — enrollment must be intentional.
|
|
9398
9643
|
//
|
|
9644
|
+
// Enrollment mode is type-dependent (W-msd8pr2300ev95e1): mutating PR-requiring
|
|
9645
|
+
// types (fix / test / build-fix-complex) enroll AUTO-MANAGED (contextOnly:false)
|
|
9646
|
+
// — the normal coding workflow; a read-only `review` WI enrolls CONTEXT-ONLY so
|
|
9647
|
+
// a "review this PR" request never silently enables auto-fix on the PR.
|
|
9648
|
+
//
|
|
9399
9649
|
// Returns:
|
|
9400
|
-
// { skipped: true, reason }
|
|
9401
|
-
// { alreadyEnrolled: true, id }
|
|
9402
|
-
// { enrolled: true, id, scope }
|
|
9650
|
+
// { skipped: true, reason } — not a pr-requiring type / no ref / no URL / upsert error
|
|
9651
|
+
// { alreadyEnrolled: true, id } — PR already tracked
|
|
9652
|
+
// { enrolled: true, id, scope, contextOnly } — newly enrolled (contextOnly true for review)
|
|
9403
9653
|
const _PR_REQUIRING_TYPES = new Set([
|
|
9404
9654
|
WORK_TYPE.FIX,
|
|
9405
9655
|
WORK_TYPE.BUILD_FIX_COMPLEX,
|
|
9406
9656
|
WORK_TYPE.REVIEW,
|
|
9407
9657
|
WORK_TYPE.TEST,
|
|
9408
9658
|
]);
|
|
9659
|
+
// W-msd8pr2300ev95e1 — READ-ONLY PR-requiring types. A `review` WI enrolls the
|
|
9660
|
+
// PR record so the review dispatch clears the `pr_not_found` gate (the #536
|
|
9661
|
+
// rationale), but a human "review this PR" request is one-shot / read-review
|
|
9662
|
+
// intent by default: it must NOT enable auto-fix, auto-review, auto-merge, or
|
|
9663
|
+
// any future automatic fix / re-review dispatch on someone else's PR. So a
|
|
9664
|
+
// review enrollment is context-only. The explicit review WI still dispatches
|
|
9665
|
+
// (isExplicitlyRequestedPrDispatch keeps work-item-sourced review dispatches
|
|
9666
|
+
// even on a contextOnly PR), and the upsert's non-demote guard means a review
|
|
9667
|
+
// WI that happens to target an ALREADY auto-managed PR (a normal coding
|
|
9668
|
+
// workflow) never demotes it. Mutating PR-requiring types (fix / test /
|
|
9669
|
+
// build-fix-complex) stay auto-managed — that is the normal coding workflow.
|
|
9670
|
+
const _READ_ONLY_PR_ENROLL_TYPES = new Set([
|
|
9671
|
+
WORK_TYPE.REVIEW,
|
|
9672
|
+
]);
|
|
9409
9673
|
function autoEnrollPrFromWorkItem(item, project, minionsDir) {
|
|
9410
9674
|
if (!item || !_PR_REQUIRING_TYPES.has(item.type)) return { skipped: true, reason: 'not-pr-requiring-type' };
|
|
9411
9675
|
const prRef = extractStructuredWorkItemPrRef(item);
|
|
@@ -9420,6 +9684,7 @@ function autoEnrollPrFromWorkItem(item, project, minionsDir) {
|
|
|
9420
9684
|
const parsedUrl = parsePrUrl(url);
|
|
9421
9685
|
const prNum = parsedUrl ? parsedUrl.prNumber : null;
|
|
9422
9686
|
const prId = getCanonicalPrId(project, prRef, url);
|
|
9687
|
+
const readOnlyEnroll = _READ_ONLY_PR_ENROLL_TYPES.has(item.type);
|
|
9423
9688
|
try {
|
|
9424
9689
|
const result = upsertPullRequestRecord(source, {
|
|
9425
9690
|
id: prId,
|
|
@@ -9432,10 +9697,15 @@ function autoEnrollPrFromWorkItem(item, project, minionsDir) {
|
|
|
9432
9697
|
status: 'active',
|
|
9433
9698
|
created: new Date().toISOString(),
|
|
9434
9699
|
url,
|
|
9435
|
-
|
|
9700
|
+
// Read-review intent enrolls context-only with the per-row automation
|
|
9701
|
+
// controls explicitly OFF (belt-and-suspenders over the contextOnly
|
|
9702
|
+
// default derivation), so no later default change can silently re-enable
|
|
9703
|
+
// auto-fix on a review-enrolled PR.
|
|
9704
|
+
contextOnly: readOnlyEnroll,
|
|
9705
|
+
...(readOnlyEnroll ? { autoFixEnabled: false, autoReviewEnabled: false } : {}),
|
|
9436
9706
|
}, { project, itemId: item.id });
|
|
9437
9707
|
return result.created
|
|
9438
|
-
? { enrolled: true, id: result.id, scope: stateScope(source) }
|
|
9708
|
+
? { enrolled: true, id: result.id, scope: stateScope(source), contextOnly: readOnlyEnroll }
|
|
9439
9709
|
: { alreadyEnrolled: true, id: result.id };
|
|
9440
9710
|
} catch (e) {
|
|
9441
9711
|
log('warn', `autoEnrollPrFromWorkItem ${item.id}: ${e.message}`);
|
|
@@ -10952,6 +11222,10 @@ module.exports = {
|
|
|
10952
11222
|
readPullRequests,
|
|
10953
11223
|
mutatePullRequests,
|
|
10954
11224
|
uid,
|
|
11225
|
+
isTempAgentId,
|
|
11226
|
+
tempAgentCallSign,
|
|
11227
|
+
tempAgentDisplayName,
|
|
11228
|
+
resolveAgentIdentity,
|
|
10955
11229
|
uniquePath,
|
|
10956
11230
|
moveFileNoClobber,
|
|
10957
11231
|
isPlainObject,
|
|
@@ -11008,7 +11282,7 @@ module.exports = {
|
|
|
11008
11282
|
projectWorkSourceWarnings,
|
|
11009
11283
|
backfillProjectWorkSourceDefaults,
|
|
11010
11284
|
WORK_ITEM_PRIORITY, WORK_ITEM_PRIORITY_VALUES, WORK_ITEM_PRIORITY_ERROR_CODE, validateWorkItemPriority,
|
|
11011
|
-
WI_STATUS, DONE_STATUSES, PLAN_TERMINAL_STATUSES, isPrdArchived, isDefunctPrd, WORK_TYPE, isFixLikeWorkType, WORKTREE_REQUIRING_TYPES, LIVE_VALIDATION_WORK_TYPES, VALID_WORK_TYPES, resolveWorkItemTypeFromPrdItem, PLAN_STATUS, PRD_ITEM_STATUS, PRD_MATERIALIZABLE, PR_STATUS, PR_POLLABLE_STATUSES, isPrPollable, resolveAbandonedPrRepollWindowMs, getPrAbandonedAtMs, stampPrAbandonedAt, clearPrAbandonedAt, PR_PENDING_REASON, BUILD_STATUS, REVIEW_STATUS, FETCH_TIMEOUT_MS, RETRY_DELAY_MS, ADO_TOKEN_REFRESH_MAX_RETRIES, DISPATCH_RESULT, mutateMetrics, mutateWatches, mutateScheduleRuns, mutatePipelineRuns, mutateManagedProcesses, mutateWorktreePool, mutateQaRuns, mutateQaSessions, trackReviewMetric, queuePlanToPrd, extractPlanDeclaredProject, extractPlanTargetProjects,
|
|
11285
|
+
WI_STATUS, DONE_STATUSES, PLAN_TERMINAL_STATUSES, isPrdArchived, isDefunctPrd, WORK_TYPE, isFixLikeWorkType, WORKTREE_REQUIRING_TYPES, LIVE_VALIDATION_WORK_TYPES, VALID_WORK_TYPES, resolveWorkItemTypeFromPrdItem, PLAN_STATUS, PRD_ITEM_STATUS, PRD_MATERIALIZABLE, PR_STATUS, PR_POLLABLE_STATUSES, isPrPollable, resolveAbandonedPrRepollWindowMs, getPrAbandonedAtMs, stampPrAbandonedAt, clearPrAbandonedAt, PR_PENDING_REASON, BUILD_STATUS, REVIEW_STATUS, FETCH_TIMEOUT_MS, RETRY_DELAY_MS, ADO_TOKEN_REFRESH_MAX_RETRIES, DISPATCH_RESULT, mutateMetrics, accumulateBillableUnits, mutateWatches, mutateScheduleRuns, mutatePipelineRuns, mutateManagedProcesses, mutateWorktreePool, mutateQaRuns, mutateQaSessions, trackReviewMetric, queuePlanToPrd, extractPlanDeclaredProject, extractPlanTargetProjects,
|
|
11012
11286
|
// W-ms5tb6ha016fda10 — explicit, fail-closed control-plane execution surface
|
|
11013
11287
|
EXECUTION_SURFACE, EXECUTION_SURFACE_VALUES, CONTROL_PLANE_ELIGIBLE_TYPES, CLOUD_ELIGIBLE_TYPES,
|
|
11014
11288
|
isControlPlaneEligibleType, validateExecutionSurface, readDeclaredExecutionSurface, isControlPlaneExecution,
|
|
@@ -11087,6 +11361,9 @@ module.exports = {
|
|
|
11087
11361
|
normalizePrAuthorIdentity,
|
|
11088
11362
|
mergePrAuthorIdentity,
|
|
11089
11363
|
prAuthorLabel,
|
|
11364
|
+
prAuthorDisplayName,
|
|
11365
|
+
resolvePrAuthorOwnership,
|
|
11366
|
+
PR_AUTHOR_OWNERSHIP,
|
|
11090
11367
|
applyPrFieldDelta,
|
|
11091
11368
|
normalizePrRecord,
|
|
11092
11369
|
normalizePrRecords,
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// engine/db/migrations/032-review-enrolled-pr-context-only.js
|
|
2
|
+
//
|
|
3
|
+
// W-msd8pr2300ev95e1 — repair PR records that were SILENTLY auto-enrolled by a
|
|
4
|
+
// review request.
|
|
5
|
+
//
|
|
6
|
+
// Regression: PR #536 widened `autoEnrollPrFromWorkItem` from FIX-only to
|
|
7
|
+
// {FIX, REVIEW, TEST} and enrolled every one with `contextOnly:false`. Because
|
|
8
|
+
// `isPrAutoFixEnabled` / `isPrAutoReviewEnabled` derive from `!contextOnly`
|
|
9
|
+
// when no explicit boolean is stored, a human "review this PR" request turned
|
|
10
|
+
// another author's PR into an AUTO-MANAGED record — the engine then dispatched
|
|
11
|
+
// fixes/re-reviews and pushed commits to a PR the operator only asked to read.
|
|
12
|
+
// The code fix makes review enrollment context-only going forward; this
|
|
13
|
+
// migration conservatively repairs records already enrolled that way.
|
|
14
|
+
//
|
|
15
|
+
// CONSERVATIVE, STRUCTURED-PROVENANCE ONLY — never title/description text:
|
|
16
|
+
// 1. status === 'active' (don't touch merged/abandoned)
|
|
17
|
+
// 2. contextOnly !== true (currently auto-managed by default)
|
|
18
|
+
// 3. NO explicit autoFixEnabled/autoReviewEnabled boolean stored
|
|
19
|
+
// → an explicitly TRACKED PR (POST /api/pull-requests/link,
|
|
20
|
+
// /api/pr-action/track, or the per-row automation toggle) always
|
|
21
|
+
// persists these booleans, so this gate alone preserves every
|
|
22
|
+
// intentionally-tracked record.
|
|
23
|
+
// 4. agent === 'human' (the sentinel autoEnroll stamps;
|
|
24
|
+
// excludes agent-authored/synced coding PRs, which stay tracked)
|
|
25
|
+
// 5. prdItems links to >= 1 still-existing work item, and EVERY linked,
|
|
26
|
+
// still-existing work item is type `review` (no fix/test/implement/…):
|
|
27
|
+
// → proves the record's only provenance is a read-review request. If a
|
|
28
|
+
// mutating WI is linked, or no linked WI still exists, we cannot prove
|
|
29
|
+
// review-only provenance and DO NOT touch the record.
|
|
30
|
+
//
|
|
31
|
+
// Repair = set contextOnly:true + autoFixEnabled:false + autoReviewEnabled:false
|
|
32
|
+
// (belt-and-suspenders), matching the new read-review enrollment shape.
|
|
33
|
+
|
|
34
|
+
function _hasTable(db, table) {
|
|
35
|
+
return !!db.prepare(`
|
|
36
|
+
SELECT 1 FROM sqlite_master WHERE type='table' AND name=?
|
|
37
|
+
`).get(table);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = {
|
|
41
|
+
version: 32,
|
|
42
|
+
description: 'repair review-enrolled PRs to context-only (W-msd8pr2300ev95e1)',
|
|
43
|
+
up(db) {
|
|
44
|
+
if (!_hasTable(db, 'pull_requests')) return;
|
|
45
|
+
|
|
46
|
+
// Build id -> type map for every work item. Archived items live in the same
|
|
47
|
+
// `work_items` table (migration 021 added an `archived` column, not a
|
|
48
|
+
// separate table), so this single scan covers both live and archived rows.
|
|
49
|
+
const wiTypeById = new Map();
|
|
50
|
+
if (_hasTable(db, 'work_items')) {
|
|
51
|
+
for (const row of db.prepare('SELECT id, type FROM work_items').all()) {
|
|
52
|
+
if (row && row.id) wiTypeById.set(String(row.id), String(row.type || '').trim().toLowerCase());
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const rows = db.prepare('SELECT scope, id, agent, status, data FROM pull_requests').all();
|
|
57
|
+
const update = db.prepare('UPDATE pull_requests SET data = ? WHERE scope = ? AND id = ?');
|
|
58
|
+
|
|
59
|
+
let repaired = 0;
|
|
60
|
+
|
|
61
|
+
for (const row of rows) {
|
|
62
|
+
let record;
|
|
63
|
+
try { record = JSON.parse(row.data); }
|
|
64
|
+
catch { continue; }
|
|
65
|
+
if (!record || typeof record !== 'object') continue;
|
|
66
|
+
|
|
67
|
+
// (1) active only.
|
|
68
|
+
if (String(record.status || row.status || '').toLowerCase() !== 'active') continue;
|
|
69
|
+
// (2) currently auto-managed by default (not already context-only).
|
|
70
|
+
if (record.contextOnly === true) continue;
|
|
71
|
+
// (3) no explicit per-row automation booleans → excludes intentional tracking.
|
|
72
|
+
if (typeof record.autoFixEnabled === 'boolean' || typeof record.autoReviewEnabled === 'boolean') continue;
|
|
73
|
+
// (4) enrolled under the human sentinel (autoEnroll stamp), not an agent author.
|
|
74
|
+
if (String(record.agent || row.agent || '').trim().toLowerCase() !== 'human') continue;
|
|
75
|
+
|
|
76
|
+
// (5) structured provenance: every linked, still-existing WI is a review.
|
|
77
|
+
const links = Array.isArray(record.prdItems) ? record.prdItems : [];
|
|
78
|
+
const knownTypes = [];
|
|
79
|
+
for (const wiId of links) {
|
|
80
|
+
const t = wiTypeById.get(String(wiId));
|
|
81
|
+
if (t) knownTypes.push(t);
|
|
82
|
+
}
|
|
83
|
+
if (knownTypes.length === 0) continue; // cannot prove provenance → leave alone
|
|
84
|
+
if (!knownTypes.every(t => t === 'review')) continue; // a mutating WI is linked → coding workflow
|
|
85
|
+
|
|
86
|
+
record.contextOnly = true;
|
|
87
|
+
record.autoFixEnabled = false;
|
|
88
|
+
record.autoReviewEnabled = false;
|
|
89
|
+
update.run(JSON.stringify(record), row.scope, row.id);
|
|
90
|
+
repaired += 1;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
console.log(`[db-migrate] v32: repaired ${repaired} review-enrolled PR record(s) to context-only`);
|
|
94
|
+
},
|
|
95
|
+
};
|
package/engine/operations/cli.js
CHANGED
|
@@ -45,6 +45,23 @@ function dispatchSessionBranch(item) {
|
|
|
45
45
|
);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Coerce a CLI `--thread <id>` value to the positive integer thread id that the
|
|
50
|
+
* ADO comment layer requires. `--thread` always arrives as a string, but
|
|
51
|
+
* engine/ado/comment.js#_validateThreadId requires `typeof number`, so passing
|
|
52
|
+
* the raw string through threw `invalid threadId` for every ADO fix-summary call
|
|
53
|
+
* (W-msd9os8e00qnd628). Returns the coerced integer, or null when the value is
|
|
54
|
+
* not a positive integer (the GitHub path keeps the raw string node id and must
|
|
55
|
+
* not use this). Kept as a pure, exported helper so the coercion is unit-tested
|
|
56
|
+
* directly rather than only through source inspection.
|
|
57
|
+
*/
|
|
58
|
+
function coerceAdoThreadId(raw) {
|
|
59
|
+
if (raw === undefined || raw === null || raw === '') return null;
|
|
60
|
+
const n = Number(raw);
|
|
61
|
+
if (!Number.isInteger(n) || n <= 0) return null;
|
|
62
|
+
return n;
|
|
63
|
+
}
|
|
64
|
+
|
|
48
65
|
function isEngineProcessAlive(control) {
|
|
49
66
|
if (!control?.pid) return false;
|
|
50
67
|
if (control.pid === process.pid) return true;
|
|
@@ -2199,6 +2216,13 @@ const commands = {
|
|
|
2199
2216
|
console.log(' GitHub: minions pr comment <repo> <prNumber> --agent <id> --kind <k> [--wi <id>] (--body-file <path> | --body <text>)');
|
|
2200
2217
|
console.log(' ADO: minions pr comment <prNumber> --host ado --ado-org <org> --ado-project <proj> --repo-id <id> --agent <id> --kind <k> [--wi <id>] (--body-file <path> | --body <text>)');
|
|
2201
2218
|
console.log(' --resolved (ADO only) post the thread pre-resolved/closed — for non-actionable FYI/praise notes');
|
|
2219
|
+
console.log(' --thread <id> reply to an EXISTING originating review thread instead of opening a new one');
|
|
2220
|
+
console.log(' (ADO: numeric thread id; GitHub: review-thread GraphQL node id)');
|
|
2221
|
+
console.log(' --resolve with --thread, transition the thread to a resolved/fixed state (fix-summary path)');
|
|
2222
|
+
console.log(' --resolve-status <s> (ADO only) terminal status for --resolve: fixed (default) | closed');
|
|
2223
|
+
console.log('');
|
|
2224
|
+
console.log('Fix summaries for ADDRESSED review findings should reply to the originating thread with');
|
|
2225
|
+
console.log('--thread <id> --resolve (keyed by structured finding/thread identity) — never open a fresh active thread.');
|
|
2202
2226
|
console.log('');
|
|
2203
2227
|
console.log('Posts a PR comment with the hidden minions marker and brand link folded in by the shared');
|
|
2204
2228
|
console.log('builder — so engine classifiers identify agent-authored comments by');
|
|
@@ -2216,7 +2240,7 @@ const commands = {
|
|
|
2216
2240
|
const positional = [];
|
|
2217
2241
|
const flags = {};
|
|
2218
2242
|
// Valueless boolean flags — present means true, no following value consumed.
|
|
2219
|
-
const BOOLEAN_FLAGS = new Set(['resolved']);
|
|
2243
|
+
const BOOLEAN_FLAGS = new Set(['resolved', 'resolve']);
|
|
2220
2244
|
let i = 0;
|
|
2221
2245
|
while (i < rest.length) {
|
|
2222
2246
|
const a = rest[i];
|
|
@@ -2242,6 +2266,17 @@ const commands = {
|
|
|
2242
2266
|
|
|
2243
2267
|
const isAdo = String(flags.host || '').toLowerCase() === 'ado';
|
|
2244
2268
|
|
|
2269
|
+
// `--thread <id>` targets an EXISTING originating review thread: reply on it
|
|
2270
|
+
// and (with `--resolve`) transition it to a resolved/fixed state, instead of
|
|
2271
|
+
// opening a fresh active feedback thread for an addressed finding
|
|
2272
|
+
// (W-msd9os8e00qnd628). `--resolve` is meaningless without a thread to
|
|
2273
|
+
// transition, so require the pair.
|
|
2274
|
+
const threadId = flags.thread;
|
|
2275
|
+
if (flags.resolve === true && (threadId === undefined || threadId === '')) {
|
|
2276
|
+
console.error('error: --resolve requires --thread <id> (the originating review thread to transition)');
|
|
2277
|
+
process.exit(2);
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2245
2280
|
const agentId = flags.agent;
|
|
2246
2281
|
const kind = flags.kind;
|
|
2247
2282
|
const workItemId = flags.wi;
|
|
@@ -2287,6 +2322,41 @@ const commands = {
|
|
|
2287
2322
|
}
|
|
2288
2323
|
const orgBase = flags['org-base'] || `https://dev.azure.com/${adoOrg}`;
|
|
2289
2324
|
const adoComment = require('../ado/comment');
|
|
2325
|
+
|
|
2326
|
+
// ── Reply-to-originating-thread + resolve (fix-summary path) ──
|
|
2327
|
+
// When a thread id is supplied we reply on THAT thread and (with
|
|
2328
|
+
// --resolve) transition it to fixed/closed, rather than creating a new
|
|
2329
|
+
// active thread. --resolve-status picks the terminal status; default
|
|
2330
|
+
// `fixed` for addressed findings, `closed` for non-actionable/deferred.
|
|
2331
|
+
if (threadId !== undefined && threadId !== '') {
|
|
2332
|
+
// ADO thread ids are numeric; `--thread` arrives as a string, and
|
|
2333
|
+
// replyToAdoThread's _validateThreadId requires typeof number. Coerce +
|
|
2334
|
+
// validate here (mirrors the prNumber handling above) so the fix-summary
|
|
2335
|
+
// path doesn't throw `invalid threadId` (W-msd9os8e00qnd628).
|
|
2336
|
+
const adoThreadId = coerceAdoThreadId(threadId);
|
|
2337
|
+
if (adoThreadId === null) {
|
|
2338
|
+
console.error(`error: invalid --thread: ${JSON.stringify(threadId)} (ADO expects a positive integer thread id)`);
|
|
2339
|
+
process.exit(2);
|
|
2340
|
+
}
|
|
2341
|
+
const wantsResolve = flags.resolve === true;
|
|
2342
|
+
const resolveStatus = flags['resolve-status'] || 'fixed';
|
|
2343
|
+
adoComment.replyToAdoThread({
|
|
2344
|
+
orgBase, project, repositoryId, prNumber, threadId: adoThreadId, body, agentId, kind, workItemId,
|
|
2345
|
+
model: executionModel.model,
|
|
2346
|
+
// `active` means "reply only, don't transition" inside replyToAdoThread.
|
|
2347
|
+
status: wantsResolve ? resolveStatus : 'active',
|
|
2348
|
+
}).then((result) => {
|
|
2349
|
+
const parts = [`ADO thread ${result.threadId}`];
|
|
2350
|
+
parts.push(result.replied ? 'replied' : (result.alreadyReplied ? 'reply already present' : 'no reply'));
|
|
2351
|
+
if (wantsResolve) parts.push(result.resolved ? 'resolved' : 'not resolved');
|
|
2352
|
+
console.log(parts.join('; '));
|
|
2353
|
+
}).catch((e) => {
|
|
2354
|
+
console.error(`error: ${e.message}`);
|
|
2355
|
+
process.exit(1);
|
|
2356
|
+
});
|
|
2357
|
+
return;
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2290
2360
|
adoComment.postAdoPrComment({
|
|
2291
2361
|
orgBase, project, repositoryId, prNumber, body, agentId, kind, workItemId,
|
|
2292
2362
|
model: executionModel.model,
|
|
@@ -2316,6 +2386,36 @@ const commands = {
|
|
|
2316
2386
|
process.exit(2);
|
|
2317
2387
|
}
|
|
2318
2388
|
const [repo, prNumberRaw] = positional;
|
|
2389
|
+
|
|
2390
|
+
// ── GitHub review-thread reply + resolve (fix-summary path) ──
|
|
2391
|
+
// A `--thread <nodeId>` on GitHub is an inline review-thread GraphQL node id.
|
|
2392
|
+
// We reply on that thread and resolve it via resolveReviewThread — the only
|
|
2393
|
+
// GitHub surface with a real "resolved" state. Top-level conversation
|
|
2394
|
+
// comments have no such state, so an addressed finding MUST come through
|
|
2395
|
+
// here, not through a fresh `postPrComment` claiming "resolved"
|
|
2396
|
+
// (W-msd9os8e00qnd628). The thread node id is self-contained, so only <repo>
|
|
2397
|
+
// (for per-slug token routing) is required positionally — not <prNumber>.
|
|
2398
|
+
if (threadId !== undefined && threadId !== '') {
|
|
2399
|
+
if (!repo) {
|
|
2400
|
+
console.error('Usage: minions pr comment <repo> --thread <reviewThreadNodeId> --agent <id> --kind <k> [--wi <id>] (--body-file <path> | --body <text>)');
|
|
2401
|
+
process.exit(2);
|
|
2402
|
+
}
|
|
2403
|
+
try {
|
|
2404
|
+
const result = ghComment.replyAndResolveReviewThread({
|
|
2405
|
+
repo, threadId, body, agentId, kind, workItemId, model: executionModel.model,
|
|
2406
|
+
resolve: true,
|
|
2407
|
+
});
|
|
2408
|
+
const parts = [`GitHub review thread ${result.threadId}`];
|
|
2409
|
+
parts.push(result.replied ? 'replied' : (result.alreadyReplied ? 'reply already present' : 'no reply'));
|
|
2410
|
+
parts.push(result.resolved ? 'resolved' : 'not resolved');
|
|
2411
|
+
console.log(parts.join('; '));
|
|
2412
|
+
} catch (e) {
|
|
2413
|
+
console.error(`error: ${e.message}`);
|
|
2414
|
+
process.exit(1);
|
|
2415
|
+
}
|
|
2416
|
+
return;
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2319
2419
|
if (!repo || prNumberRaw === undefined) {
|
|
2320
2420
|
console.error('Usage: minions pr comment <repo> <prNumber> --agent <id> --kind <k> [--wi <id>] (--body-file <path> | --body <text>)');
|
|
2321
2421
|
process.exit(2);
|
|
@@ -2518,6 +2618,7 @@ module.exports = {
|
|
|
2518
2618
|
_normalizeSessionBranch: normalizeSessionBranch,
|
|
2519
2619
|
_dispatchSessionBranch: dispatchSessionBranch,
|
|
2520
2620
|
_resolveCommentExecutionModel: resolveCommentExecutionModel, // exported for testing
|
|
2621
|
+
_coerceAdoThreadId: coerceAdoThreadId, // exported for testing (W-msd9os8e00qnd628)
|
|
2521
2622
|
// W-mpcyvff6000pf828 (#2653) — heartbeat writer + factory exported for tests
|
|
2522
2623
|
_writeHeartbeatNow: writeHeartbeatNow,
|
|
2523
2624
|
_createHeartbeatInterval: createHeartbeatInterval,
|
|
@@ -4037,6 +4037,11 @@ function updateMetrics(agentId, dispatchItem, result, taskUsage, prsCreatedCount
|
|
|
4037
4037
|
m.totalInputTokens = (m.totalInputTokens || 0) + (taskUsage.inputTokens || 0);
|
|
4038
4038
|
m.totalOutputTokens = (m.totalOutputTokens || 0) + (taskUsage.outputTokens || 0);
|
|
4039
4039
|
m.totalCacheRead = (m.totalCacheRead || 0) + (taskUsage.cacheRead || 0);
|
|
4040
|
+
// Non-USD runtime-reported billable units (e.g. Copilot premium requests).
|
|
4041
|
+
// Accumulated generically off typed usage metadata, never a runtime-name
|
|
4042
|
+
// branch (W-msdcgitc). USD is skipped by the helper (it rides the costUsd
|
|
4043
|
+
// path above), so this cannot double-count dollar cost.
|
|
4044
|
+
shared.accumulateBillableUnits(m, taskUsage.billable);
|
|
4040
4045
|
}
|
|
4041
4046
|
// Track agent runs in _engine for LLM performance tile (alongside CC/doc-chat)
|
|
4042
4047
|
if (!metrics._engine) metrics._engine = {};
|
|
@@ -4061,6 +4066,7 @@ function updateMetrics(agentId, dispatchItem, result, taskUsage, prsCreatedCount
|
|
|
4061
4066
|
eng.outputTokens += taskUsage.outputTokens || 0;
|
|
4062
4067
|
eng.cacheRead += taskUsage.cacheRead || 0;
|
|
4063
4068
|
eng.cacheCreation = (eng.cacheCreation || 0) + (taskUsage.cacheCreation || 0);
|
|
4069
|
+
shared.accumulateBillableUnits(eng, taskUsage.billable);
|
|
4064
4070
|
}
|
|
4065
4071
|
|
|
4066
4072
|
const today = dateStamp();
|
|
@@ -4074,6 +4080,7 @@ function updateMetrics(agentId, dispatchItem, result, taskUsage, prsCreatedCount
|
|
|
4074
4080
|
daily.inputTokens += taskUsage.inputTokens || 0;
|
|
4075
4081
|
daily.outputTokens += taskUsage.outputTokens || 0;
|
|
4076
4082
|
daily.cacheRead += taskUsage.cacheRead || 0;
|
|
4083
|
+
shared.accumulateBillableUnits(daily, taskUsage.billable);
|
|
4077
4084
|
}
|
|
4078
4085
|
const cutoff = new Date();
|
|
4079
4086
|
cutoff.setDate(cutoff.getDate() - 30);
|
|
@@ -263,7 +263,10 @@ function resolveAgent(workType, config, opts = {}) {
|
|
|
263
263
|
_tempBudget--;
|
|
264
264
|
const tempId = `temp-${shared.uid()}`;
|
|
265
265
|
_claimedAgents.add(tempId);
|
|
266
|
-
|
|
266
|
+
// Friendly, deterministic call sign derived from the immutable temp id
|
|
267
|
+
// (shared.tempAgentCallSign) — stable across restarts; the canonical
|
|
268
|
+
// `temp-<uid>` id is unchanged and remains the machine identity.
|
|
269
|
+
tempAgents.set(tempId, { name: shared.tempAgentCallSign(tempId), role: 'Temporary Agent', createdAt: ts() });
|
|
267
270
|
log('info', `Spawning temp agent ${tempId} — all permanent agents busy`);
|
|
268
271
|
return tempId;
|
|
269
272
|
}
|