@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.
Files changed (48) hide show
  1. package/bin/install-internal-minions.js +136 -6
  2. package/bin/minions.js +27 -13
  3. package/dashboard/js/agent-identity.js +121 -0
  4. package/dashboard/js/modal.js +4 -0
  5. package/dashboard/js/refresh.js +38 -3
  6. package/dashboard/js/render-agents.js +14 -2
  7. package/dashboard/js/render-dispatch.js +2 -2
  8. package/dashboard/js/render-other.js +147 -13
  9. package/dashboard/js/render-prd.js +6 -5
  10. package/dashboard/js/render-prs.js +44 -25
  11. package/dashboard/js/render-work-items.js +442 -22
  12. package/dashboard/js/settings.js +8 -0
  13. package/dashboard/js/utils.js +40 -0
  14. package/dashboard/pages/tools.html +1 -0
  15. package/dashboard/pages/work.html +13 -0
  16. package/dashboard/shared/pr-author.js +75 -0
  17. package/dashboard/shared/pr-filters.js +28 -5
  18. package/dashboard/styles.css +85 -0
  19. package/dashboard-build.js +3 -3
  20. package/dashboard.js +93 -7
  21. package/docs/README.md +1 -0
  22. package/docs/copilot-cli-schema.md +1 -0
  23. package/docs/engine-restart.md +4 -2
  24. package/docs/internal-install.md +44 -5
  25. package/docs/named-agents.md +48 -0
  26. package/docs/pr-author-identity.md +63 -10
  27. package/docs/runtime-adapters.md +39 -0
  28. package/docs/temporary-agents.md +172 -0
  29. package/engine/ado/comment.js +261 -4
  30. package/engine/agents/llm.js +26 -0
  31. package/engine/agents/playbook.js +2 -1
  32. package/engine/api/settings-validation.js +25 -0
  33. package/engine/core/operator-identity.js +23 -1
  34. package/engine/core/queries.js +53 -2
  35. package/engine/core/shared.js +286 -9
  36. package/engine/db/migrations/032-review-enrolled-pr-context-only.js +95 -0
  37. package/engine/operations/cli.js +102 -1
  38. package/engine/orchestration/lifecycle.js +7 -0
  39. package/engine/orchestration/routing.js +4 -1
  40. package/engine/providers/gh-comment.js +159 -0
  41. package/engine/recovery/stop-stack.js +16 -2
  42. package/engine/runtimes/claude.js +3 -0
  43. package/engine/runtimes/codex.js +4 -0
  44. package/engine/runtimes/copilot.js +23 -2
  45. package/engine.js +2 -2
  46. package/package.json +1 -1
  47. package/playbooks/fix.md +23 -2
  48. package/playbooks/shared-rules.md +24 -2
@@ -108,6 +108,35 @@ function _runGh(execFileSync, args, timeoutMs, env) {
108
108
  return execFileSync('gh', args, opts);
109
109
  }
110
110
 
111
+ // GitHub GraphQL node id for a review thread (opaque, e.g. `PRRT_kwDO…`). We
112
+ // don't decode it — only guard against empty / whitespace values before it is
113
+ // interpolated into a `gh api graphql` argv (argv form, no shell, so this is a
114
+ // sanity check, not an injection gate).
115
+ const GH_NODE_ID_RE = /^[A-Za-z0-9_=+/.:-]+$/;
116
+
117
+ function _validateThreadNodeId(threadId) {
118
+ if (typeof threadId !== 'string' || !GH_NODE_ID_RE.test(threadId)) {
119
+ throw new Error(`invalid reviewThreadId: ${JSON.stringify(threadId)} (expected a GitHub review-thread node id)`);
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Run `gh api graphql` and return the parsed JSON `data` object. Throws on a
125
+ * transport failure (execFileSync non-zero — e.g. permission denied) and on a
126
+ * GraphQL `errors` payload (some gh versions exit 0 but embed errors).
127
+ */
128
+ function _runGhGraphql(execFileSync, gqlArgs, timeoutMs, env) {
129
+ const raw = _runGh(execFileSync, ['api', 'graphql', ...gqlArgs], timeoutMs, env);
130
+ let parsed;
131
+ try { parsed = JSON.parse(String(raw || '').trim() || '{}'); }
132
+ catch (e) { throw new Error(`gh api graphql: unparseable response (${e.message})`); }
133
+ if (parsed && Array.isArray(parsed.errors) && parsed.errors.length > 0) {
134
+ const msg = parsed.errors.map((x) => x?.message || String(x)).join('; ');
135
+ throw new Error(`gh api graphql error: ${msg}`);
136
+ }
137
+ return parsed && parsed.data ? parsed.data : {};
138
+ }
139
+
111
140
  /**
112
141
  * Resolve the per-slug PAT for `repo` and return an env override suitable for
113
142
  * passing to `_runGh`. Returns `undefined` when no mapping exists (caller
@@ -207,6 +236,132 @@ function postPrReviewComment({
207
236
  }
208
237
  }
209
238
 
239
+ /**
240
+ * Reply to an existing GitHub PR *review thread* with a fix summary and resolve
241
+ * that thread — instead of opening a fresh top-level comment or a new review
242
+ * thread that the author must triage.
243
+ *
244
+ * This is the fix-summary path for GitHub (W-msd9os8e00qnd628). GitHub has no
245
+ * "resolved" state for conversation (issue) comments — only inline review
246
+ * threads can be resolved — so this is the ONLY way to post an addressed-finding
247
+ * summary that ends in a resolved state. When no review-thread node id is
248
+ * available the caller must fall back to `postPrComment` (an informational
249
+ * top-level summary that MUST NOT claim to be "resolved").
250
+ *
251
+ * Mechanics (via `gh api graphql`, per-slug PAT threaded through GH_TOKEN):
252
+ * 1. Query the thread by node id → `isResolved` + existing comment bodies.
253
+ * 2. `addPullRequestReviewThreadReply` unless a comment already carries THIS
254
+ * dispatch's hidden marker (agentId + kind + workItemId) — marker-keyed so
255
+ * retries / restarts / duplicate callbacks don't double-post.
256
+ * 3. `resolveReviewThread` unless already resolved.
257
+ *
258
+ * Idempotency + partial-failure: the reply and resolve are distinct round-trips.
259
+ * A reply success followed by a resolve failure throws (so the caller sees it
260
+ * and can retry); the retry re-queries, finds the reply present (skips it), and
261
+ * re-attempts only the resolve. Local state is never marked resolved when the
262
+ * `resolveReviewThread` mutation failed.
263
+ *
264
+ * Self-authored / shared-credential semantics: resolution is gated only by the
265
+ * thread node identity and marker, never by `viewerDidAuthor` or reviewer/fixer
266
+ * equality — a shared platform credential must not block a legitimate fix reply.
267
+ *
268
+ * @param {object} args
269
+ * @param {string} args.repo owner/name (for per-slug token routing)
270
+ * @param {string} args.threadId review-thread GraphQL node id
271
+ * @param {string} args.body reply body (pre-marker; builder prepends marker)
272
+ * @param {string} args.agentId
273
+ * @param {string} args.kind
274
+ * @param {string} [args.workItemId]
275
+ * @param {string} [args.model]
276
+ * @param {boolean} [args.resolve=true] resolve the thread after replying
277
+ * @param {number} [args.timeoutMs=30000]
278
+ * @param {Function} [args.execFileSync]
279
+ * @param {Function} [args.resolveTokenForSlug]
280
+ * @returns {{threadId:string, replied:boolean, alreadyReplied:boolean, resolved:boolean, alreadyResolved:boolean}}
281
+ */
282
+ function replyAndResolveReviewThread({
283
+ repo,
284
+ threadId,
285
+ body,
286
+ agentId,
287
+ kind,
288
+ workItemId,
289
+ model,
290
+ resolve = true,
291
+ timeoutMs = 30000,
292
+ execFileSync = _execFileSync,
293
+ resolveTokenForSlug,
294
+ } = {}) {
295
+ _validateRepo(repo);
296
+ _validateThreadNodeId(threadId);
297
+ const finalBody = buildMinionsCommentBody({ agentId, kind, workItemId, model, body });
298
+ const env = _resolveTokenEnvForRepo(repo, resolveTokenForSlug);
299
+
300
+ // ── 1. Read the current thread (isResolved + comment bodies) for idempotency ─
301
+ const queryData = _runGhGraphql(
302
+ execFileSync,
303
+ [
304
+ '-f',
305
+ 'query=query($id:ID!){ node(id:$id){ __typename ... on PullRequestReviewThread { id isResolved comments(first:100){ nodes { body } } } } }',
306
+ '-f', `id=${threadId}`,
307
+ ],
308
+ timeoutMs,
309
+ env,
310
+ );
311
+ const node = queryData && queryData.node;
312
+ if (!node || (node.__typename && node.__typename !== 'PullRequestReviewThread')) {
313
+ throw new Error(`gh review thread ${threadId}: not a PullRequestReviewThread (only inline review threads can be resolved)`);
314
+ }
315
+ const alreadyResolved = node.isResolved === true;
316
+ const existingBodies = Array.isArray(node.comments?.nodes)
317
+ ? node.comments.nodes.map((c) => String(c?.body || ''))
318
+ : [];
319
+ const alreadyReplied = existingBodies.some((b) => {
320
+ const marker = parseMinionsMarker(b);
321
+ if (!marker) return false;
322
+ if (marker.agentId !== agentId || marker.kind !== kind) return false;
323
+ if (workItemId) return marker.workItemId === workItemId;
324
+ return marker.workItemId === undefined;
325
+ });
326
+
327
+ // ── 2. Reply on the originating thread (unless we already did) ──
328
+ let replied = false;
329
+ if (!alreadyReplied) {
330
+ _runGhGraphql(
331
+ execFileSync,
332
+ [
333
+ '-f',
334
+ 'query=mutation($id:ID!,$body:String!){ addPullRequestReviewThreadReply(input:{pullRequestReviewThreadId:$id, body:$body}){ comment { id } } }',
335
+ '-f', `id=${threadId}`,
336
+ '-f', `body=${finalBody}`,
337
+ ],
338
+ timeoutMs,
339
+ env,
340
+ );
341
+ replied = true;
342
+ }
343
+
344
+ // ── 3. Resolve the thread (unless already resolved / not requested) ──
345
+ // A failure here after a successful reply throws; the retry skips the reply
346
+ // (marker present) and re-attempts only the resolve.
347
+ let resolved = alreadyResolved;
348
+ if (resolve && !alreadyResolved) {
349
+ _runGhGraphql(
350
+ execFileSync,
351
+ [
352
+ '-f',
353
+ 'query=mutation($id:ID!){ resolveReviewThread(input:{threadId:$id}){ thread { isResolved } } }',
354
+ '-f', `id=${threadId}`,
355
+ ],
356
+ timeoutMs,
357
+ env,
358
+ );
359
+ resolved = true;
360
+ }
361
+
362
+ return { threadId, replied, alreadyReplied, resolved, alreadyResolved };
363
+ }
364
+
210
365
  module.exports = {
211
366
  // Builders / parsers (pure functions — usable from anywhere)
212
367
  buildMinionsCommentBody,
@@ -222,9 +377,13 @@ module.exports = {
222
377
  // gh wrappers (argv-form, --body-file)
223
378
  postPrComment,
224
379
  postPrReviewComment,
380
+ // GitHub review-thread reply + resolve (fix-summary path, GraphQL)
381
+ replyAndResolveReviewThread,
225
382
  // Internal helpers exported for tests / advanced callers
226
383
  _buildMarker,
227
384
  _writeTempBodyFile,
228
385
  _resolveTokenEnvForRepo,
386
+ _validateThreadNodeId,
387
+ _runGhGraphql,
229
388
  _clearTokenWarnings: () => _warnedUnresolvedSlugs.clear(),
230
389
  };
@@ -104,9 +104,22 @@ const STOP_STACK_EXIT = Object.freeze({
104
104
  REFUSED_UNVERIFIED: 4,
105
105
  });
106
106
 
107
- // Matches the internal installer's quiescence window so a stop driven by the
108
- // migration gate cannot be shorter than the wait that follows it.
107
+ // The FLOOR of the internal installer's quiescence window (see
108
+ // bin/install-internal-minions.js#QUIESCE_TIMEOUT_FLOOR_MS), so a stop driven by
109
+ // the migration gate can never be shorter than the wait that follows it. That
110
+ // window is derived from the target runtime and only ever widens from here.
109
111
  const DEFAULT_STOP_TIMEOUT_MS = 60000;
112
+ /**
113
+ * Grace added on top of the ENGINE'S OWN drain budget (`engine.shutdownTimeout`)
114
+ * when a caller derives a teardown deadline from it, covering the fixed work
115
+ * either side of the drain: writing stop intent, stopping the supervisor and
116
+ * dashboard, the verified reap, and the WAL-index release.
117
+ *
118
+ * Exported because it is a cross-surface number: `minions restart` applies it,
119
+ * and the standalone internal installer — which requires nothing from `engine/`
120
+ * and therefore mirrors the value — is held to it by a drift test.
121
+ */
122
+ const DRAIN_GRACE_MS = 5000;
110
123
  const PID_POLL_MS = 100;
111
124
  const SHM_POLL_MS = 500;
112
125
  /**
@@ -600,6 +613,7 @@ module.exports = {
600
613
  STOP_STACK_HOLDER_REASONS,
601
614
  STOP_STACK_EXIT,
602
615
  DEFAULT_STOP_TIMEOUT_MS,
616
+ DRAIN_GRACE_MS,
603
617
  POST_KILL_GRACE_MS,
604
618
  PID_POLL_MS,
605
619
  SHM_POLL_MS,
@@ -874,6 +874,9 @@ const capabilities = {
874
874
  effortLevels: true,
875
875
  // Emits `total_cost_usd` and detailed token usage in the result event
876
876
  costTracking: true,
877
+ // Native billable unit is USD dollar cost (rendered as `$`). Typed metadata
878
+ // so the display layer never branches on the runtime name (W-msdcgitc).
879
+ billableUnit: 'usd',
877
880
  // Family shorthands (`sonnet` / `opus` / `haiku`) are accepted by the CLI
878
881
  modelShorthands: true,
879
882
  // No public model enumeration mechanism — settings UI uses free-text
@@ -993,6 +993,10 @@ const capabilities = {
993
993
  systemPromptFile: false,
994
994
  effortLevels: true,
995
995
  costTracking: false,
996
+ // Native billable unit is USD (Codex reports no cost telemetry today, so it
997
+ // renders as unavailable). Typed metadata so the display layer never branches
998
+ // on the runtime name (W-msdcgitc).
999
+ billableUnit: 'usd',
996
1000
  modelShorthands: false,
997
1001
  modelDiscovery: true,
998
1002
  // Bundled catalogs can lag staged/custom model availability. Let Codex
@@ -906,8 +906,24 @@ function parseOutput(raw, { maxTextLength = 0 } = {}) {
906
906
  numTurns: turnEndCount,
907
907
  // Copilot-specific extension — preserved alongside the standard shape
908
908
  // so the engine can distinguish "this turn cost N premium requests"
909
- // from token accounting on the Claude path.
910
- premiumRequests: typeof u.premiumRequests === 'number' ? u.premiumRequests : 0,
909
+ // from token accounting on the Claude path. NULL (not 0) when the
910
+ // installed CLI did not report the field for this run, so the engine
911
+ // records it as UNAVAILABLE rather than a real "0 premium requests"
912
+ // (W-msdcgitc). Treated as authoritative — never derived from tokens,
913
+ // model, or elapsed time.
914
+ premiumRequests: typeof u.premiumRequests === 'number' ? u.premiumRequests : null,
915
+ // Generic typed billable-unit descriptor (runtime/usage contract,
916
+ // W-msdcgitc). `value` is the authoritative provider-reported count for
917
+ // THIS result event (per-invocation, already scoped — not a running
918
+ // total the engine must diff), and `reported:false` marks an
919
+ // unavailable observation the engine keeps distinct from a real 0. The
920
+ // engine accumulates this generically (shared.accumulateBillableUnits)
921
+ // without ever branching on the runtime name.
922
+ billable: {
923
+ unit: 'premiumRequests',
924
+ value: typeof u.premiumRequests === 'number' ? u.premiumRequests : null,
925
+ reported: typeof u.premiumRequests === 'number',
926
+ },
911
927
  sessionDurationMs: typeof u.sessionDurationMs === 'number' ? u.sessionDurationMs : 0,
912
928
  };
913
929
  }
@@ -1439,6 +1455,11 @@ const capabilities = {
1439
1455
  effortLevels: true,
1440
1456
  // result.usage carries premiumRequests count, no USD or tokens
1441
1457
  costTracking: false,
1458
+ // Native billable unit is GitHub Copilot premium requests, not USD. The
1459
+ // display layer renders this in the same position Claude shows dollar cost
1460
+ // (W-msdcgitc). Typed metadata so orchestration/dashboard never branch on the
1461
+ // runtime name; adapters without a native credit unit default to 'usd'.
1462
+ billableUnit: 'premiumRequests',
1442
1463
  // No 'sonnet'/'opus'/'haiku' shorthand — Copilot expects full model IDs
1443
1464
  modelShorthands: false,
1444
1465
  // GET https://api.githubcopilot.com/models works (verified during spike)
package/engine.js CHANGED
@@ -11706,7 +11706,7 @@ function discoverFromWorkItems(config, project) {
11706
11706
  newWork.push({
11707
11707
  type: workType,
11708
11708
  agent: agentId,
11709
- agentName: config.agents[agentId]?.name || tempAgents.get(agentId)?.name || agentId,
11709
+ agentName: config.agents[agentId]?.name || tempAgents.get(agentId)?.name || shared.tempAgentCallSign(agentId) || agentId,
11710
11710
  agentRole: config.agents[agentId]?.role || tempAgents.get(agentId)?.role || 'Agent',
11711
11711
  task: `[${project?.name || 'project'}] ${item.title || item.description?.slice(0, 80) || item.id}`,
11712
11712
  prompt,
@@ -12952,7 +12952,7 @@ function resolvePendingDispatchAgent(item, config) {
12952
12952
  function assignPendingDispatchAgent(item, agentId, config) {
12953
12953
  const agents = config.agents || {};
12954
12954
  item.agent = agentId;
12955
- item.agentName = agents[agentId]?.name || tempAgents.get(agentId)?.name || agentId;
12955
+ item.agentName = agents[agentId]?.name || tempAgents.get(agentId)?.name || shared.tempAgentCallSign(agentId) || agentId;
12956
12956
  item.agentRole = agents[agentId]?.role || tempAgents.get(agentId)?.role || 'Agent';
12957
12957
  delete item._agentBusySince;
12958
12958
  delete item.skipReason;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2453",
3
+ "version": "0.1.2455",
4
4
  "description": "Multi-agent AI dev team that runs from ~/.minions/ — five autonomous agents share a single engine, dashboard, and knowledge base",
5
5
  "bin": {
6
6
  "minions": "bin/minions.js"
package/playbooks/fix.md CHANGED
@@ -232,8 +232,29 @@ Pick the disposition that matches the finding and post the matching reply, then
232
232
  - **Won't fix / disagree** (comment is invalid, stale, already addressed, out of scope, or harmful) → Reply with the evidence-backed rationale explaining why no code change was made. Leave the thread **active** for the reviewer to decide; do NOT resolve unilaterally.
233
233
 
234
234
  Host-specific mechanics:
235
- - **GitHub**: Reply to each review comment, then resolve the conversation for the four resolve-eligible dispositions above. Leave Won't-fix threads unresolved.
236
- - **ADO**: Use `az` CLI first to reply to each thread and update status when supported; use ADO MCP only as a fallback when `az` is unavailable or insufficient. Set status to `fixed` for Fixed/Satisfied-elsewhere/Tracked-as-follow-up and `closed` for Deferred; leave `active` for Won't-fix rationale replies.
235
+ - **Preferred (both hosts): reply on the ORIGINATING thread and resolve it in one call** with
236
+ `minions pr comment <repo> --thread <threadId> --resolve --agent <id> --kind fix [--wi <finding-id>] --body-file <path>`
237
+ (ADO: add `--host ado --ado-org … --ado-project … --repo-id …` and, for Deferred, `--resolve-status closed`;
238
+ GitHub: `--thread` is the review-thread GraphQL node id). The `--thread` id is the **structured thread/finding
239
+ identity carried on the work item / your `reviewFindingResolution` evidence** — never resolve by matching title or
240
+ body text, and never resolve sibling threads or all threads in bulk. This path is idempotent: re-running after a
241
+ retry, restart, or a provider timeout re-checks the thread, skips a reply it already posted (matched by the hidden
242
+ minions marker), and re-attempts only the unfinished resolve.
243
+ - **Do NOT open a fresh active thread to summarize an addressed finding.** A new top-level/active thread for a fix
244
+ you already made just adds triage noise the author must re-close. Post the summary as a reply on the finding's own
245
+ thread via `--thread … --resolve` above.
246
+ - **GitHub**: `--thread … --resolve` replies to the review thread and resolves it via `resolveReviewThread` for the
247
+ four resolve-eligible dispositions. Leave Won't-fix threads unresolved. If no review-thread id exists (the finding
248
+ was a top-level conversation comment), a top-level `minions pr comment` reply is informational only — GitHub has no
249
+ resolved state for conversation comments, so do **not** claim it is "resolved."
250
+ - **ADO**: `--thread … --resolve` posts the reply on the originating thread and transitions it (`fixed` for
251
+ Fixed/Satisfied-elsewhere/Tracked-as-follow-up, `--resolve-status closed` for Deferred). Prefer it over raw `az`;
252
+ if you must fall back, use `az` first (ADO MCP only when `az` is insufficient), reply on the existing thread, and
253
+ set its status — do not create a new thread just for the fix summary. Leave `active` for Won't-fix rationale replies.
254
+
255
+ Never auto-resolve a finding the agent did NOT actually address. When the fix report says the finding was not
256
+ addressed, is ambiguous, needs human input, or lacks the required commit/`file:line` resolution evidence, reply with
257
+ the rationale and leave the thread **active**. Human feedback is not auto-resolved merely because a dispatch completed.
237
258
 
238
259
  Hard rule: never resolve, close, or mark-fixed a thread without first posting a reply that explains the disposition. The chunk-5 / Caleb-Tseng / MaiLibraryViewModel thread (ADO 5215549 thread 65692221) is the canonical bad example — closed silently with no audit trail.
239
260
 
@@ -262,6 +262,18 @@ non-actionable thread is posted pre-resolved in a single call. Prefer
262
262
  thread `status` field yourself: `closed` (4) for non-actionable notes (or `fixed`
263
263
  (2) when the comment records something already handled), `active` (1) for
264
264
  actionable findings. The verdict/summary comment itself stays `active`.
265
+
266
+ **Fix summaries reply on the originating thread — never a new active thread.**
267
+ When you are recording that an existing review finding was *addressed*, reply on
268
+ that finding's own thread and transition it, rather than opening a fresh thread:
269
+ `minions pr comment <pr> --host ado --ado-org … --ado-project … --repo-id … --thread <threadId> --resolve [--resolve-status closed] --agent <id> --kind fix [--wi <finding-id>] --body-file <path>`.
270
+ The `--thread` id is **structured thread/finding identity** from the work item or
271
+ your `reviewFindingResolution` evidence — resolve only that thread, keyed by id,
272
+ never by matching comment title/body text and never in bulk across sibling
273
+ threads. The reply+status transition is idempotent across retries/restarts: it
274
+ re-reads the thread, skips a reply already carrying your minions marker, and only
275
+ re-applies the status if the earlier PATCH failed — so a partial failure stays
276
+ visible and retryable instead of being silently marked resolved locally.
265
277
  {{/ado_shared_rules}}
266
278
  {{#github_shared_rules}}
267
279
  **GitHub mechanism.** GitHub has no "resolved" state for PR *conversation*
@@ -273,9 +285,19 @@ single API call that creates a thread pre-resolved. So on GitHub:
273
285
  `<details><summary>Non-blocking observations</summary>…</details>` block inside
274
286
  the verdict comment so they are visible but de-emphasized and create no separate
275
287
  unresolved threads. `--resolved` is rejected on the GitHub path for this reason.
288
+ - **To record that an existing review finding was addressed**, reply on that
289
+ finding's own review thread and resolve it in one idempotent call:
290
+ `minions pr comment <repo> --thread <reviewThreadNodeId> --resolve --agent <id> --kind fix [--wi <finding-id>] --body-file <path>`.
291
+ The `--thread` node id is **structured thread identity** from the work item or
292
+ your `reviewFindingResolution` evidence — resolve only that thread, never by
293
+ matching comment text and never in bulk. Do **not** open a fresh active thread
294
+ to summarize a fix you already made. If the finding was a *top-level*
295
+ conversation comment (no review-thread node id), a top-level `minions pr comment`
296
+ reply is informational only — never call it "resolved."
276
297
  - If you DO post a non-actionable note as its own **inline review thread**, resolve
277
- it immediately after posting via the GraphQL `resolveReviewThread` mutation
278
- (issue comments cannot be resolved this way; only review threads can):
298
+ it immediately after posting (equivalent to `--thread … --resolve`, or the raw
299
+ GraphQL `resolveReviewThread` mutation below; issue comments cannot be resolved
300
+ this way, only review threads can):
279
301
  ```bash
280
302
  # 1. find the thread node id for the comment you just posted
281
303
  gh api graphql -f query='query($owner:String!,$repo:String!,$pr:Int!){