@yemi33/minions 0.1.2280 → 0.1.2282

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.
@@ -39,6 +39,12 @@ Orphan declaration requires four checks (all must pass):
39
39
  3. `isOsPidAliveForDispatch` returns false.
40
40
  4. Full-log re-scan still shows no completion.
41
41
 
42
+ **Minimum-alive guard (M004).** `ENGINE_DEFAULTS.minAliveBeforeOrphanMs` (default 30s, configurable via
43
+ `config.engine.minAliveBeforeOrphanMs`) is checked before the four-check ladder. A dispatch that
44
+ hasn't been alive for that duration is skipped — prevents false-positive orphan declarations for
45
+ short-duration agents (e.g. verify agents that complete within ~20s before process tracking catches up).
46
+ Bypassed when `canReapDeadProcess` is true (confirmed-dead at restart).
47
+
42
48
  After engine restart, gated on `engineRestartGraceUntil` (default
43
49
  20 min) — see [engine-restart.md](engine-restart.md).
44
50
 
@@ -56,7 +56,7 @@ A config that never mentions `workspace_manifest` produces the exact same dispat
56
56
  | **Dispatch — repo gate** | `engine.js spawnAgent()` (right after project resolution) | Calls `shared.agentCanUseRepo(agent, project)`. Mismatch → `completeDispatch(... FAILURE_CLASS.WORKSPACE_MANIFEST_REPO, agentRetryable: false)` and `cleanupTempAgent` runs. Non-retryable because the structural answer is "widen the manifest or pick a different agent". |
57
57
  | **Spawn — tool merge** | `engine.js spawnAgent()` → `_buildAgentSpawnFlags(..., allowedTools)` | `shared.mergeManifestAllowedTools(claudeConfig.allowedTools, manifest.allowed_tools)` produces the intersection of the runtime baseline and the manifest list. Result is passed as `--allowedTools <csv>` to Claude / Copilot / Codex, so the CLI itself enforces the narrowed surface. Empty manifest list (`[]`) = deny-all. Same merge runs on the steering-resume codepath. |
58
58
  | **Agent context** | Future work (playbook.js) | Manifest can be surfaced into the agent prompt so the agent sees its declared scope. Today, `shared.resolveAgentManifest(agent)` returns the resolved struct any caller can read. |
59
- | **URL fetch** | Advisory today | `shared.agentCanFetchUrl(agent, url)` is available for runtime intercepts. Minions itself does not currently intercept `web_fetch` inside the spawned CLI subprocess; a future runtime adapter hook can wire this. |
59
+ | **URL fetch** | Advisory today | `allowed_external_urls` is validated at config load. No runtime helper function a future runtime adapter hook can wire URL-fetch intercepts against the manifest. |
60
60
  | **Memory scope** | Advisory today | `shared.agentMemoryScope(agent)` is available for consolidation / playbook / inbox callers. Semantics: `private` = agent only sees its own `knowledge/agents/<id>.md`; `shared` = full team knowledge (current default); `read-only-shared` = reads shared memory but should not write inbox/notes. |
61
61
 
62
62
  ## Helpers (in `engine/shared.js`)
@@ -64,15 +64,13 @@ A config that never mentions `workspace_manifest` produces the exact same dispat
64
64
  ```js
65
65
  const { MEMORY_SCOPES, WORKSPACE_MANIFEST_DEFAULTS,
66
66
  validateWorkspaceManifest, resolveAgentManifest,
67
- agentCanUseRepo, agentCanUseTool, agentCanFetchUrl, agentMemoryScope,
67
+ agentCanUseRepo, agentMemoryScope,
68
68
  mergeManifestAllowedTools, formatManifestRejection } = require('./engine/shared');
69
69
  ```
70
70
 
71
71
  - `validateWorkspaceManifest(manifest)` → `{ ok, errors }`. `null`/`undefined` is valid (uses defaults).
72
72
  - `resolveAgentManifest(agent, config?)` → fresh copy of `WORKSPACE_MANIFEST_DEFAULTS` overlaid with the agent's `workspace_manifest`. Malformed manifest silently falls back to defaults; surface the error via `validateWorkspaceManifest` at config-load time.
73
73
  - `agentCanUseRepo(agent, projectOrString)` → `bool`. Accepts a project object or a string identifier (`github:owner/repo`, `ado:org/proj/repo`, or bare `owner/repo`). Case-insensitive.
74
- - `agentCanUseTool(agent, toolName)` → `bool`. Case-sensitive.
75
- - `agentCanFetchUrl(agent, url)` → `bool`. Wildcard `*.example.com` matches subdomains **and** apex.
76
74
  - `agentMemoryScope(agent)` → one of `MEMORY_SCOPES`. Unknown values fall back to `'shared'`.
77
75
  - `mergeManifestAllowedTools(baselineCsv, manifestArray)` → merged CSV. Intersection semantics: `null` manifest = baseline unchanged; empty array = deny-all; empty baseline + manifest = manifest as ceiling.
78
76
  - `formatManifestRejection({ agentId, kind, target, allowed })` → structured human-readable rejection string used by the dispatch repo gate.
package/engine/ado.js CHANGED
@@ -942,7 +942,13 @@ async function getAdoToken(opts = {}) {
942
942
  }
943
943
  }
944
944
 
945
+ // Test seam — overrides adoFetch for unit tests only. Must be null in production.
946
+ let _adoFetchOverride = null;
947
+ function _setAdoFetchForTest(fn) { _adoFetchOverride = fn; }
948
+ function _resetAdoFetchForTest() { _adoFetchOverride = null; }
949
+
945
950
  async function adoFetch(url, token, opts = {}) {
951
+ if (_adoFetchOverride) return _adoFetchOverride(url, token, opts);
946
952
  const _retryCount = typeof opts === 'number' ? opts : (opts._retryCount || 0); // backward compat
947
953
  const method = (typeof opts === 'object' && opts.method) || 'GET';
948
954
  const body = (typeof opts === 'object' && opts.body) || undefined;
@@ -1642,13 +1648,16 @@ async function pollPrStatus(config) {
1642
1648
  if (prBuilds.length > 0) {
1643
1649
  buildStatus = classifyBuildStatus(prBuilds);
1644
1650
  if (buildStatus === BUILD_STATUS.FAILING) {
1645
- const failed = prBuilds.find(b => b.result === 'failed');
1651
+ // M005: count all failed builds for build-fix-complex routing
1652
+ const failedBuilds = prBuilds.filter(b => b.result === 'failed');
1653
+ const failed = failedBuilds[0];
1646
1654
  buildFailReason = failed?.definition?.name || 'Build failed';
1647
1655
  buildFailureSignature = shared.safeSlugComponent([
1648
1656
  failed?.definition?.name,
1649
1657
  failed?.result,
1650
1658
  failed?.status,
1651
1659
  ].filter(Boolean).join('\n') || buildFailReason, 80);
1660
+ pr._failedCheckCount = failedBuilds.length;
1652
1661
  }
1653
1662
  } else if (allBuilds.length > 0) {
1654
1663
  // Stale merge-commit classification (Issue #2747). ADO returned
@@ -1738,6 +1747,7 @@ async function pollPrStatus(config) {
1738
1747
  if (buildStatus === BUILD_STATUS.PASSING) {
1739
1748
  delete pr.buildErrorLog;
1740
1749
  delete pr.buildFailureSignature;
1750
+ delete pr._failedCheckCount; // M005: clear on recovery
1741
1751
  // Reset build fix retry counter on recovery — allows fresh auto-fix cycles if build breaks again
1742
1752
  if (pr.buildFixAttempts) { delete pr.buildFixAttempts; }
1743
1753
  }
@@ -2945,6 +2955,8 @@ module.exports = {
2945
2955
  _resetAdoThrottle, // exported for testing
2946
2956
  _setAdoThrottleForTest, // exported for testing
2947
2957
  _setAdoTokenForTest, // exported for testing
2958
+ _setAdoFetchForTest, // exported for testing
2959
+ _resetAdoFetchForTest, // exported for testing
2948
2960
  // Exported for unit tests — engine code MUST go through pollPrStatus / reconcilePrs.
2949
2961
  decodeUrlSegment,
2950
2962
  stripGitSuffix,
package/engine/cleanup.js CHANGED
@@ -1643,7 +1643,7 @@ function runPeriodicWorktreeSweep(config) {
1643
1643
  // null" claim is true for the dispatch-end GC but NOT for this registry-derived
1644
1644
  // periodic sweep.)
1645
1645
  const projects = allProjects.filter(p => {
1646
- try { return !shared.isLiveCheckoutProject(p); } catch { return true; }
1646
+ try { return !shared.isLiveCheckoutProject(p); } catch { return false; } // W-mquoe8fu000w9383: fail-CLOSED — exclude from GC when checkout mode cannot be determined
1647
1647
  });
1648
1648
  const liveSkipped = allProjects.length - projects.length;
1649
1649
  if (projects.length === 0) {
@@ -1,6 +1,20 @@
1
1
  /**
2
2
  * engine/dispatch.js — Dispatch queue management: add, complete, mutate, alerts.
3
3
  * Extracted from engine.js for modularity. No logic changes.
4
+ *
5
+ * ## Dispatch meta schema — optional fields (M006)
6
+ *
7
+ * Dispatch items carry a free-form `meta` object. The following optional fields
8
+ * are part of the typed handoff envelope contract (added in M006):
9
+ *
10
+ * meta.from_agent {string} — agent id of the completing agent that queued
11
+ * this follow-up (e.g. 'lambert', 'temp-abc123').
12
+ * meta.from_conclusion {string} — 1–3 sentence structured summary of what the
13
+ * completing agent concluded before handing off.
14
+ *
15
+ * These fields are optional and additive. Dispatches without them are unaffected.
16
+ * When present, engine/playbook.js injects a "Handoff Context" section into the
17
+ * rendered prompt so the receiving agent has explicit orientation.
4
18
  */
5
19
 
6
20
  const fs = require('fs');
package/engine/github.js CHANGED
@@ -1037,7 +1037,9 @@ async function pollPrStatus(config) {
1037
1037
 
1038
1038
  if (hasFailed) {
1039
1039
  buildStatus = BUILD_STATUS.FAILING;
1040
- const failed = runs.find(r => r.conclusion === 'failure' || r.conclusion === 'timed_out' || r.conclusion === 'startup_failure');
1040
+ // M005: count all failing runs for build-fix-complex routing
1041
+ const failedRuns = runs.filter(r => r.conclusion === 'failure' || r.conclusion === 'timed_out' || r.conclusion === 'startup_failure');
1042
+ const failed = failedRuns[0];
1041
1043
  buildFailReason = failed?.name || 'Check failed';
1042
1044
  buildFailureSignature = shared.safeSlugComponent([
1043
1045
  failed?.name,
@@ -1046,6 +1048,7 @@ async function pollPrStatus(config) {
1046
1048
  failed?.output?.summary,
1047
1049
  failed?.output?.text,
1048
1050
  ].filter(Boolean).join('\n') || buildFailReason, 80);
1051
+ pr._failedCheckCount = failedRuns.length;
1049
1052
  } else if (allDone && allPassed) {
1050
1053
  buildStatus = BUILD_STATUS.PASSING;
1051
1054
  } else if (allDone) {
@@ -1079,6 +1082,7 @@ async function pollPrStatus(config) {
1079
1082
  if (buildStatus === BUILD_STATUS.PASSING) {
1080
1083
  delete pr.buildErrorLog;
1081
1084
  delete pr.buildFailureSignature;
1085
+ delete pr._failedCheckCount; // M005: clear on recovery
1082
1086
  // Reset build fix retry counter on recovery — allows fresh auto-fix cycles if build breaks again
1083
1087
  if (pr.buildFixAttempts) { delete pr.buildFixAttempts; }
1084
1088
  }
@@ -4765,7 +4765,7 @@ function handleHarnessIterationResult(stdout, structuredCompletion, meta, config
4765
4765
  * - All RMW on dispatch.json goes through addToDispatch (mutateDispatch), per
4766
4766
  * the concurrency rules in CLAUDE.md.
4767
4767
  */
4768
- function dispatchReReviewForFix(fixDispatchItem, meta, config) {
4768
+ function dispatchReReviewForFix(fixDispatchItem, meta, config, opts = {}) {
4769
4769
  const addressesReviewWi = meta?.addresses_review_wi || meta?.item?.meta?.addresses_review_wi || null;
4770
4770
  if (!addressesReviewWi) return null;
4771
4771
  const pr = meta?.pr;
@@ -4814,6 +4814,15 @@ function dispatchReReviewForFix(fixDispatchItem, meta, config) {
4814
4814
  const projMeta = project;
4815
4815
  const headSha = String(livePr.headSha || livePr._adoSourceCommit || livePr._adoHeadCommit || pr.headSha || '').trim();
4816
4816
  const reReviewKey = `rereview-${project?.name || 'default'}-${shared.getPrDisplayId ? shared.getPrDisplayId(livePr) : pr.id}-${headSha ? headSha.slice(0, 8) : 'nohead'}`;
4817
+ // M006 — typed handoff envelope: stamp from_agent / from_conclusion on the
4818
+ // re-review meta so the reviewer agent sees who fixed the PR and what they
4819
+ // concluded. from_agent is the fix agent id; from_conclusion is a 1-sentence
4820
+ // summary derived from the fix resultSummary (capped to avoid bloating the
4821
+ // dispatch queue). Both are optional — the reviewer playbook degrades to
4822
+ // standard orientation when they are absent.
4823
+ const fromAgent = opts.agentId || fixDispatchItem?.agent || '';
4824
+ const rawConclusion = opts.resultSummary || fixDispatchItem?.resultSummary || '';
4825
+ const fromConclusion = rawConclusion ? String(rawConclusion).slice(0, 300) : '';
4817
4826
  const rereviewMeta = {
4818
4827
  dispatchKey: reReviewKey,
4819
4828
  source: 'pr',
@@ -4822,6 +4831,8 @@ function dispatchReReviewForFix(fixDispatchItem, meta, config) {
4822
4831
  project: projMeta,
4823
4832
  rereview_of: fixDispatchItem?.id || null,
4824
4833
  addresses_review_wi: addressesReviewWi,
4834
+ ...(fromAgent ? { from_agent: fromAgent } : {}),
4835
+ ...(fromConclusion ? { from_conclusion: fromConclusion } : {}),
4825
4836
  };
4826
4837
  const extraVars = {
4827
4838
  pr_id: livePr.id, pr_number: prNumber, pr_title: livePr.title || '', pr_branch: prBranch,
@@ -5686,7 +5697,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
5686
5697
  // reviewer. See engine/ado.js:resetReviewerNegativeVote for the verdict-
5687
5698
  // flip reset that fires on the resulting APPROVE.
5688
5699
  try {
5689
- dispatchReReviewForFix(dispatchItem, meta, config);
5700
+ dispatchReReviewForFix(dispatchItem, meta, config, { agentId, resultSummary });
5690
5701
  } catch (err) {
5691
5702
  log('warn', `Re-review auto-dispatch for ${meta?.pr?.id || 'unknown PR'}: ${err.message}`);
5692
5703
  }
@@ -530,7 +530,7 @@ function _findExistingPrdForPlan(planFile, prdDir) {
530
530
  // safeJsonNoRestore: PRDs are terminal artifacts — never restore archived
531
531
  // PRDs from a stale .backup sidecar (W-mouptdh1000h9f39).
532
532
  const prd = safeJsonNoRestore(path.join(prdDir, pf));
533
- if (prd?.source_plan === planFile) return pf;
533
+ if (prd?.source_plan && path.basename(String(prd.source_plan)) === planFile) return pf;
534
534
  }
535
535
  return null;
536
536
  }
@@ -928,7 +928,7 @@ function isStageComplete(stage, stageState, run, config) {
928
928
  for (const pf of prdFiles) {
929
929
  // safeJsonNoRestore — see _findExistingPrdForPlan above (W-mouptdh1000h9f39).
930
930
  const prd = safeJsonNoRestore(path.join(prdDir, pf));
931
- if (prd?.source_plan === planFile && !(artifacts.prds || []).includes(pf) && !discoveredPrds.includes(pf)) {
931
+ if (prd?.source_plan && path.basename(String(prd.source_plan)) === planFile && !(artifacts.prds || []).includes(pf) && !discoveredPrds.includes(pf)) {
932
932
  discoveredPrds.push(pf);
933
933
  }
934
934
  }
@@ -386,12 +386,21 @@ const PLAYBOOK_OPTIONAL_VARS = new Set([
386
386
  // produce matches. Optional for the same reason as the review-only alias.
387
387
  'project_skills_block',
388
388
  'skip_project_skills',
389
+ // M006 — typed handoff envelope fields. Set on a follow-up dispatch meta
390
+ // when lifecycle.js queues the item after a completing agent so the
391
+ // receiving agent has explicit context about who handed off and why.
392
+ // from_agent: agent id of the completing agent (e.g. 'lambert')
393
+ // from_conclusion: 1–3 sentence structured summary of what that agent concluded
394
+ // Both are optional; playbooks that don't reference them receive empty strings.
395
+ 'from_agent',
396
+ 'from_conclusion',
389
397
  ]);
390
398
 
391
399
  const PLAYBOOK_REQUIRED_VARS = {
392
400
  'implement': ['item_id', 'item_name', 'branch_name', 'project_path'],
393
401
  'implement-shared': ['item_id', 'item_name', 'branch_name', 'worktree_path'],
394
402
  'fix': ['pr_id', 'pr_branch'],
403
+ 'build-fix-complex': ['pr_id', 'pr_branch'],
395
404
  'review': ['pr_id', 'pr_branch'],
396
405
  'build-and-test': ['pr_id', 'pr_branch', 'project_path'],
397
406
  'explore': ['task_description'],
@@ -570,6 +579,25 @@ function buildQaValidateContextBlock({ runId, runbook, target, artifactsDir }) {
570
579
  }
571
580
 
572
581
 
582
+ // ─── M006 — Handoff Context Block ────────────────────────────────────────────
583
+
584
+ /**
585
+ * Build a "Handoff Context" appendix section for follow-up dispatches.
586
+ * Called when vars.from_agent or vars.from_conclusion is truthy so the
587
+ * receiving agent knows who handed off and what they concluded.
588
+ *
589
+ * @param {{ fromAgent?: string, fromConclusion?: string }} opts
590
+ * @returns {string} markdown section, or '' when both fields are falsy
591
+ */
592
+ function buildHandoffContextBlock({ fromAgent, fromConclusion } = {}) {
593
+ if (!fromAgent && !fromConclusion) return '';
594
+ const lines = ['\n\n---\n\n## Handoff Context (from previous agent)\n'];
595
+ if (fromAgent) lines.push(`**Previous agent:** \`${fromAgent}\``);
596
+ if (fromConclusion) lines.push(`\n**Conclusion:** ${fromConclusion}`);
597
+ return lines.join('\n');
598
+ }
599
+
600
+
573
601
  // ─── Playbook Renderer ──────────────────────────────────────────────────────
574
602
 
575
603
  /**
@@ -823,7 +851,7 @@ function renderPlaybook(type, vars) {
823
851
  // (of liveValidation.type) runs the full build on the live environment after
824
852
  // the PR lands. When autoDispatch is false the agent is responsible for its own
825
853
  // inline validation (may fail in isolated worktrees — documented limitation).
826
- const LIVE_VALIDATION_PLAYBOOKS = new Set(['implement', 'fix', 'docs', 'decompose']);
854
+ const LIVE_VALIDATION_PLAYBOOKS = new Set(['implement', 'fix', 'build-fix-complex', 'docs', 'decompose']);
827
855
  if (LIVE_VALIDATION_PLAYBOOKS.has(type)) {
828
856
  const lv = matchedProject && matchedProject.liveValidation;
829
857
  if (lv && lv.autoDispatch === true) {
@@ -838,6 +866,18 @@ function renderPlaybook(type, vars) {
838
866
  }
839
867
  }
840
868
 
869
+ // M006 — typed handoff envelope: inject from_agent/from_conclusion context
870
+ // when the dispatch was queued as a follow-up from a completing agent. Only
871
+ // fires when at least one field is truthy so unrelated dispatches stay clean.
872
+ if (vars.from_agent || vars.from_conclusion) {
873
+ try {
874
+ const block = buildHandoffContextBlock({
875
+ fromAgent: vars.from_agent,
876
+ fromConclusion: vars.from_conclusion,
877
+ });
878
+ if (block) inertAppendices.push(block);
879
+ } catch (e) { log('warn', `handoff-context inject failed: ${e.message}`); }
880
+ }
841
881
  // Inject KB guardrail
842
882
  content += `\n\n---\n\n## Knowledge Base Rules\n\n`;
843
883
  content += `**Never delete, move, or overwrite files in \`knowledge/\`.** The sweep (consolidation engine) is the only process that writes to \`knowledge/\`. If you think a KB file is wrong, note it in your learnings file — do not touch \`knowledge/\` directly.\n`;
@@ -1266,6 +1306,9 @@ function selectPlaybook(workType, item) {
1266
1306
  if (workType === WORK_TYPE.FIX && hasPrContext) {
1267
1307
  return 'fix';
1268
1308
  }
1309
+ if (workType === WORK_TYPE.BUILD_FIX_COMPLEX && hasPrContext) {
1310
+ return 'build-fix-complex';
1311
+ }
1269
1312
  const typeSpecificPlaybooks = ['explore', 'review', 'test', 'plan-to-prd', 'plan', 'ask', 'verify', 'decompose', 'docs', 'setup', 'meeting-investigate', 'meeting-debate', 'meeting-conclude'];
1270
1313
  return typeSpecificPlaybooks.includes(workType) ? workType : 'work-item';
1271
1314
  }
@@ -1295,7 +1338,10 @@ function buildPrDispatch(agentId, config, project, pr, type, extraVars, taskLabe
1295
1338
  ...extraVars,
1296
1339
  task_id: dispatchId,
1297
1340
  };
1298
- const playbookName = type === WORK_TYPE.TEST ? 'build-and-test' : (type === WORK_TYPE.REVIEW ? 'review' : 'fix');
1341
+ // M005: use selectPlaybook for routing rather than a hardcoded ternary so
1342
+ // new work types (e.g. BUILD_FIX_COMPLEX → 'build-fix-complex') are picked
1343
+ // up automatically. TEST keeps its legacy alias ('build-and-test' ≠ 'test').
1344
+ const playbookName = type === WORK_TYPE.TEST ? 'build-and-test' : selectPlaybook(type, extraVars || {});
1299
1345
  const prompt = renderPlaybook(playbookName, vars);
1300
1346
  if (!prompt) return null;
1301
1347
  return {
package/engine/shared.js CHANGED
@@ -3795,6 +3795,14 @@ const WORK_TYPE = {
3795
3795
  // so callers don't need to also set skipPr:true. Still worktree-requiring
3796
3796
  // and still project-required (mirrors implement/fix HTTP-validate path).
3797
3797
  SETUP: 'setup',
3798
+ // BUILD_FIX_COMPLEX (M005 / SHERLOC): engine-dispatched two-phase build-fix
3799
+ // variant for multi-file or multi-test failures. Phase 1 (Localization)
3800
+ // requires the agent to document fault files, root-cause hypothesis, and
3801
+ // proposed repair before making any edits. Phase 2 (Implementation) executes
3802
+ // the plan. Dispatched automatically by the engine when a PR's build failure
3803
+ // spans more than one file/test. Always operates on a PR branch (PR context
3804
+ // required at dispatch time, same as FIX).
3805
+ BUILD_FIX_COMPLEX: 'build-fix-complex',
3798
3806
  };
3799
3807
 
3800
3808
  // Work types whose dispatch path requires a per-project git worktree. The
@@ -3828,6 +3836,10 @@ const WORKTREE_REQUIRING_TYPES = new Set([
3828
3836
  // worktree resolver falls back to MINIONS_DIR's parent and collapses to a
3829
3837
  // drive root on installs where MINIONS_DIR sits one level below the root.
3830
3838
  WORK_TYPE.SETUP,
3839
+ // BUILD_FIX_COMPLEX (M005): runs on the PR's source branch (like FIX) so a
3840
+ // real project worktree is required. Without a project the worktree resolver
3841
+ // falls back to MINIONS_DIR's parent and can collapse to a drive root.
3842
+ WORK_TYPE.BUILD_FIX_COMPLEX,
3831
3843
  WORK_TYPE.DOCS,
3832
3844
  ]);
3833
3845
 
@@ -3848,6 +3860,7 @@ const VALID_WORK_TYPES = new Set([
3848
3860
  WORK_TYPE.TEST,
3849
3861
  WORK_TYPE.DOCS,
3850
3862
  WORK_TYPE.SETUP,
3863
+ WORK_TYPE.BUILD_FIX_COMPLEX,
3851
3864
  ]);
3852
3865
 
3853
3866
  // Match a leading "Type: <word>" line in a description (handles both bare
package/engine.js CHANGED
@@ -7206,7 +7206,11 @@ async function discoverFromPrs(config, project) {
7206
7206
  }
7207
7207
  } catch (e) { log('warn', `Pre-dispatch build check for ${pr.id}: ${e.message} — skipping dispatch`); continue; }
7208
7208
 
7209
- const agentId = resolveAgent('fix', config, { authorAgent: pr.agent });
7209
+ // M005 (SHERLOC): route to build-fix-complex when multiple checks failed;
7210
+ // single-check failures stay on the fast 'fix' path.
7211
+ const isComplexBuildFailure = (pr._failedCheckCount != null ? pr._failedCheckCount : 1) > 1;
7212
+ const buildFixWorkType = isComplexBuildFailure ? WORK_TYPE.BUILD_FIX_COMPLEX : WORK_TYPE.FIX;
7213
+ const agentId = resolveAgent(buildFixWorkType, config, { authorAgent: pr.agent });
7210
7214
  if (!agentId) continue;
7211
7215
  const prBranch = ensurePrBranchForDispatch(project, pr, 'build-fix');
7212
7216
  if (!prBranch) continue;
@@ -7225,7 +7229,7 @@ async function discoverFromPrs(config, project) {
7225
7229
  pr.url ? `PR URL: ${pr.url}` : '',
7226
7230
  ].filter(Boolean).join('\n');
7227
7231
 
7228
- const item = buildPrDispatch(agentId, config, project, pr, 'fix', {
7232
+ const item = buildPrDispatch(agentId, config, project, pr, buildFixWorkType, {
7229
7233
  pr_id: pr.id, pr_branch: prBranch,
7230
7234
  review_note: reviewNote,
7231
7235
  }, `Fix build failure on ${pr.id}: ${pr.title || ''}`, { dispatchKey: key, cooldownKey: key, automationCauseKey: buildCauseKey, source: 'pr', pr, branch: prBranch, project: projMeta });
@@ -7614,6 +7618,12 @@ function renderProjectWorkItemPromptForAgent(item, workType, agentId, config, pr
7614
7618
  // Either flag suppresses both blocks on the dispatch.
7615
7619
  skip_project_review_skills: !!(item.meta && item.meta.skipProjectReviewSkills),
7616
7620
  skip_project_skills: !!(item.meta && (item.meta.skipProjectSkills || item.meta.skipProjectReviewSkills)),
7621
+ // M006 — typed handoff envelope. Set on follow-up dispatch metas by
7622
+ // lifecycle.js when queuing an item after a completing agent. The
7623
+ // renderPlaybook path injects a "Handoff Context" section when truthy
7624
+ // so the receiving agent sees who handed off and what they concluded.
7625
+ from_agent: (item.meta && item.meta.from_agent) ? String(item.meta.from_agent) : '',
7626
+ from_conclusion: (item.meta && item.meta.from_conclusion) ? String(item.meta.from_conclusion) : '',
7617
7627
  };
7618
7628
  const cpResult = buildWorkItemDispatchVars(item, vars, config, {
7619
7629
  worktreePath: vars.worktree_path || root,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2280",
3
+ "version": "0.1.2282",
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"
@@ -0,0 +1,177 @@
1
+ ---
2
+ requiresProjectContext: true
3
+ ---
4
+
5
+ # Playbook: Fix Complex Build Failure (SHERLOC two-phase)
6
+
7
+ You are {{agent_name}}, the {{agent_role}} on the {{project_name}} project.
8
+ TEAM ROOT: {{team_root}}
9
+
10
+ Repository ID comes from `.minions/config.json` under `project.repositoryId`.
11
+ Repo: {{repo_name}} | Org: {{ado_org}} | Project: {{ado_project}}
12
+
13
+ ## Your Task
14
+
15
+ Fix the complex build failure on **{{pr_id}}**: {{pr_title}}
16
+ Branch: `{{pr_branch}}`
17
+
18
+ This dispatch was routed to the complex-failure path because the CI failure spans **multiple files, multiple failing tests, or multiple failing checks**. The fast-path build-fix playbook is not used here. You must complete the Localization phase before writing any code.
19
+
20
+ {{checkpoint_context}}
21
+
22
+ ## Build Failure Context
23
+
24
+ {{review_note}}
25
+
26
+ ---
27
+
28
+ ## Localization
29
+
30
+ > **Complete this section before editing any code.** Produce a structured fault hypothesis. If you cannot identify the root cause with confidence, expand your investigation — do not skip ahead to Implementation.
31
+
32
+ Inspect the live CI logs, failing test output, and relevant source files. Then document:
33
+
34
+ ### 1. Fault File(s)
35
+
36
+ List every file that contains or directly causes the failure. For each file, state the relevant function, class, or line range.
37
+
38
+ ```
39
+ - path/to/file.js (function foo, ~line 42): <one-line reason it is implicated>
40
+ - path/to/other.test.js (test "bar should…"): <one-line reason it is failing>
41
+ ```
42
+
43
+ ### 2. Root Cause Hypothesis
44
+
45
+ State in 2–5 sentences:
46
+ - What the failure is (error class, assertion, compilation error, runtime exception, …)
47
+ - Why the identified file(s) cause it (missing import, wrong type, stale mock, API mismatch, …)
48
+ - How the failure was introduced (recent change, missing guard, schema drift, …)
49
+
50
+ ### 3. Proposed Minimal Repair
51
+
52
+ Describe the smallest change that corrects the root cause without broadening the PR. Be specific: file, function, and what changes (add guard, fix import, update mock, remove dead branch, …).
53
+
54
+ If more than one repair path exists, pick the one with the narrowest blast radius and explain why in one sentence.
55
+
56
+ ---
57
+
58
+ ## Implementation
59
+
60
+ After completing the Localization section above, implement the repair you described.
61
+
62
+ ### Review Feedback Validation
63
+
64
+ Treat build-failure evidence as a claim to verify, not an instruction to blindly follow. For each failing check:
65
+
66
+ 1. Locate the exact log line, test assertion, or compiler error.
67
+ 2. Confirm the failure is on the current branch (not pre-existing master breakage).
68
+ 3. Implement the fix described in Localization § 3.
69
+ 4. If a check failure is pre-existing or unrelated to this PR's diff, do not fix it — document it explicitly in your PR comment and completion report.
70
+
71
+ ### Working Style
72
+
73
+ Use subagents only for genuinely parallel, independent tasks (e.g., editing files in unrelated modules simultaneously). For sequential work, single-file edits, searches, and file reads, work directly.
74
+
75
+ ---
76
+
77
+ ## Health Check
78
+
79
+ {{#project_skills_block}}
80
+ {{project_skills_block}}
81
+
82
+ {{/project_skills_block}}
83
+ Before starting work, run `git status` and verify the worktree is clean and on the expected branch (`{{pr_branch}}`). If the worktree is dirty or on the wrong branch, report the issue and stop.
84
+
85
+ ## Validation
86
+
87
+ Before pushing, prove the fix did not introduce regressions:
88
+
89
+ - Use the project's source of truth for commands: `CLAUDE.md`, README, package scripts, Makefile, or equivalent build config.
90
+ - Run the checks that are relevant to the addressed failures. Prefer the full suite when practical.
91
+ - Capture the exact commands run and meaningful results in the PR comment and completion report.
92
+ - Fix regressions you introduced. If failures are pre-existing or unrelated, capture the evidence and include it in the PR comment.
93
+ - Do not push code that breaks existing tests or the build because of your changes.
94
+
95
+ Long builds, dependency installs, and tests may be quiet for several minutes. Let the normal CLI command run naturally; do not add artificial heartbeat output or split commands just to show progress.
96
+
97
+ ## Publish & Comment on PR
98
+
99
+ After the fix is validated or any unavoidable limitation is clearly documented, commit only relevant files and push:
100
+
101
+ ```bash
102
+ git add <specific files>
103
+ git commit -m "fix: resolve complex build failure on {{pr_id}}"
104
+ git push
105
+ ```
106
+
107
+ Do NOT remove the worktree — the engine handles cleanup automatically.
108
+
109
+ {{pr_comment_instructions}}
110
+ - pullRequestId: `{{pr_number}}`
111
+ - content: Include the completed Localization hypothesis (fault files, root cause, repair), what was changed (file:line), and build/test validation results.
112
+ - Sign: `Fixed by [Minions](https://icy-water-0224cc51e.2.azurestaticapps.net/minions) ({{agent_name}} — {{agent_role}} · {{agent_model}})`
113
+
114
+ ## PR description audit (mandatory unless `meta.skipDescriptionAudit`)
115
+
116
+ <!-- Inlined from playbooks/_pr-description-audit.md — keep in sync (enforced by test/unit/config-and-playbooks.test.js). -->
117
+
118
+ {{#skip_description_audit}}
119
+ > **Audit suppressed.** This dispatch was invoked with `meta.skipDescriptionAudit: true`.
120
+ > Record `meta.descriptionAudit = { ran: false, result: "skipped:meta-flag" }` in your completion report
121
+ > and skip the audit steps below entirely.
122
+ {{/skip_description_audit}}
123
+
124
+ After you push commits to the PR's source branch and BEFORE you mark the work item done, audit the PR description and refresh it to match the new diff. Skip the audit when you pushed no commits this dispatch or when the dispatch is a no-op completion; record the skip in `meta.descriptionAudit`.
125
+
126
+ **Audit steps**
127
+
128
+ 1. Fetch the current PR description.
129
+ - GitHub: `gh pr view <num> --repo <owner>/<repo> --json body --jq .body`
130
+ - Azure DevOps: `az repos pr show --id <num> --query description -o tsv`
131
+ 2. Compute the diff for the commits you just pushed: `git log --stat <baseline>..HEAD` where `<baseline>` is `origin/<branch>@{1}` for follow-up pushes (or `origin/main` for the very first push).
132
+ 3. For each section of the description, evaluate whether the new diff invalidates anything it claims. Focus on: feature lists / "what changed" bullets, config/option tables, file or path lists, line counts, before/after metrics, ASCII diagrams, and embedded markdown image refs.
133
+ 4. **No drift** → record `meta.descriptionAudit = { ran: true, result: "no-changes-needed" }` and stop.
134
+ 5. **Drift detected** → build a patched body. **Preserve prose voice.** Edit only the stale parts. Do NOT rewrite. Do NOT add new sections. Do NOT change the PR title. Do NOT toggle draft state. Do NOT post a separate PR comment narrating the description change.
135
+ 6. Push the patched description.
136
+ - GitHub: write the new body to a temp file, then `gh pr edit <num> --repo <owner>/<repo> --body-file <file>`.
137
+ - Azure DevOps: `az repos pr update --id <num> --description "$(Get-Content -Raw <file>)"`. If the body exceeds ~4 KB, fall back to `az rest --method patch` against `…/pullRequests/<id>?api-version=7.1` with `{ "description": "<body>" }`.
138
+ 7. GET-verify the description post-patch by re-running step 1 and confirming the body matches what you sent.
139
+
140
+ **Out-of-scope guardrails**
141
+
142
+ - Don't add screenshots that weren't already there. Refresh only.
143
+ - Don't rewrite description prose beyond the targeted stale edits.
144
+ - Don't modify the PR title; don't toggle draft state, close/reopen the PR.
145
+ - Don't post a separate PR comment summarizing the description change.
146
+ - Don't fail the work item on a screenshot or attachment failure — degrade gracefully and record the skip.
147
+ - Screenshots NEVER land in the repo. `agents/<id>/screenshots/` only; never `git add` a PNG.
148
+
149
+ **Completion-report shape**
150
+
151
+ When the audit ran (whether it edited the description or no-op'd), include this in the completion JSON (full schema: `docs/completion-reports.md` → "PR description audit"):
152
+
153
+ ```json
154
+ {
155
+ "meta": {
156
+ "descriptionAudit": {
157
+ "ran": true,
158
+ "result": "description-patched",
159
+ "oldScreenshots": [],
160
+ "newScreenshots": [],
161
+ "patchedSections": ["feature-bullets", "config-table"]
162
+ }
163
+ }
164
+ }
165
+ ```
166
+
167
+ Omit `meta.descriptionAudit` entirely when the audit did not run (no commits pushed, no-op completion). When `meta.skipDescriptionAudit` suppressed the audit, set `ran: false` and `result: "skipped:meta-flag"`.
168
+
169
+ ## When to Stop
170
+
171
+ Your task is complete when: the Localization hypothesis is documented, the fix is implemented and validated, the branch is pushed, and the PR is commented. Do NOT continue into unrelated improvements.
172
+
173
+ **NEVER run `gh pr merge` or any merge command on this PR.** The engine handles merging after review approval.
174
+
175
+ ## Completion
176
+
177
+ After finishing, write the JSON completion report described in the shared rules (schema: `docs/completion-reports.md`). Include the completed Localization section (fault files, root cause, repair) in your `summary` field. A fenced ` ```completion ` block in stdout is accepted only as a compatibility fallback — do not duplicate the schema here.
package/routing.md CHANGED
@@ -11,6 +11,7 @@ How the engine decides who handles what. Parsed by engine.js — keep the table
11
11
  | implement:large | rebecca | dallas |
12
12
  | review | ripley | lambert |
13
13
  | fix | _author_ | _any_ |
14
+ | build-fix-complex | _author_ | dallas |
14
15
  | plan | ripley | rebecca |
15
16
  | plan-to-prd | lambert | rebecca |
16
17
  | explore | ripley | rebecca |
@@ -30,6 +31,7 @@ Notes:
30
31
  - `_author_` means route to the PR author
31
32
  - `_any_` means route to any available idle agent (lowest error rate first)
32
33
  - `implement:large` is for items with `estimated_complexity: "large"`
34
+ - `build-fix-complex` is for multi-file failures, more than one failing test, or multiple failing checks; simple 1-file-1-test failures use the `fix` work type (fast path)
33
35
  - Engine falls back to any idle agent if both preferred and fallback are busy
34
36
  - Routing selects an owner; it should not narrow the user's task contract. The assigned agent should behave like the user typed the same task directly into a CLI, with Minions adding only safety, status, and review guardrails.
35
37
  - `fix` review-feedback routing sends work to the PR author for context, but the author must still validate review comments as claims before editing and push back with evidence when the comment is invalid, stale, already addressed, out of scope, or harmful.