@yemi33/minions 0.1.1617 → 0.1.1619

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1619 (2026-04-29)
4
+
5
+ ### Features
6
+ - surface partial PR escalation state (#1856)
7
+
3
8
  ## 0.1.1617 (2026-04-29)
4
9
 
5
10
  ### Fixes
@@ -10,8 +10,10 @@ function prRow(pr) {
10
10
  // If PR is merged/abandoned, treat 'waiting' review as resolved
11
11
  const effectiveReviewStatus = (pr.status === 'merged' || pr.status === 'abandoned') && pr.reviewStatus === 'waiting' ? (pr.status === 'merged' ? 'approved' : 'pending') : pr.reviewStatus;
12
12
  const reviewSource = sq.status || effectiveReviewStatus || 'pending';
13
- const reviewClass = reviewSource === 'approved' ? 'approved' : (reviewSource === 'changes-requested' || reviewSource === 'rejected') ? 'rejected' : reviewSource === 'waiting' ? 'building' : 'draft';
14
- const reviewLabel = sq.status === 'waiting' ? 'reviewing (minions)' : sq.status ? sq.status + ' (minions)' : (effectiveReviewStatus || 'pending');
13
+ const reviewEscalated = !!pr._evalEscalated;
14
+ const reviewClass = reviewEscalated ? 'review-escalated' : reviewSource === 'approved' ? 'approved' : (reviewSource === 'changes-requested' || reviewSource === 'rejected') ? 'rejected' : reviewSource === 'waiting' ? 'building' : 'draft';
15
+ const reviewLabel = reviewEscalated ? 'review loop escalated (build/conflict may still run)' : sq.status === 'waiting' ? 'reviewing (minions)' : sq.status ? sq.status + ' (minions)' : (effectiveReviewStatus || 'pending');
16
+ const reviewTitle = reviewEscalated ? 'Review/re-review and review-fix automation stopped after evalMaxIterations; build-fix and conflict-fix automation may still run.' : '';
15
17
  const buildClass = pr.buildFixEscalated ? 'build-escalated' : pr._buildStatusStale ? 'build-stale' : pr.buildStatus === 'passing' ? 'build-pass' : pr.buildStatus === 'failing' ? 'build-fail' : pr.buildStatus === 'running' ? 'building' : 'no-build';
16
18
  const buildLabel = pr.buildFixEscalated ? 'escalated (' + (pr.buildFixAttempts || '?') + ' fixes)' : (pr.buildStatus || 'none') + (pr._buildStatusStale ? ' (stale)' : '');
17
19
  const statusClass = pr.status === 'merged' ? 'merged' : pr.status === 'abandoned' ? 'rejected' : pr.status === 'active' ? 'active' : 'draft';
@@ -23,7 +25,7 @@ function prRow(pr) {
23
25
  '<td><a class="pr-title" href="' + escapeHtml(safeUrl(url)) + '" target="_blank" rel="noopener">' + escapeHtml(pr.title || 'Untitled') + '</a>' + (pr.description ? '<div class="pr-desc">' + escapeHtml(pr.description.length > 120 ? pr.description.slice(0, 120) + '...' : pr.description) + '</div>' : '') + '</td>' +
24
26
  '<td><span class="pr-agent">' + escapeHtml(pr.agent || '—') + '</span></td>' +
25
27
  '<td><span class="pr-branch">' + escapeHtml(pr.branch || '—') + '</span></td>' +
26
- '<td><span class="pr-badge ' + reviewClass + '">' + escapeHtml(reviewLabel) + '</span></td>' +
28
+ '<td><span class="pr-badge ' + reviewClass + '"' + (reviewTitle ? ' title="' + escapeHtml(reviewTitle) + '"' : '') + '>' + escapeHtml(reviewLabel) + '</span></td>' +
27
29
  '<td>' + (sq.reviewer && sq.status !== 'waiting' ? '<span class="pr-agent" title="' + escapeHtml(sq.note || '') + '">' + escapeHtml(sq.reviewer) + '</span>' : sq.reviewer && sq.status === 'waiting' ? '<span class="pr-agent" style="color:var(--muted)" title="Vote pending confirmation">' + escapeHtml(sq.reviewer) + '…</span>' : pr.reviewedBy && pr.reviewedBy.length ? '<span class="pr-agent">' + escapeHtml(pr.reviewedBy.join(', ')) + '</span>' : '<span style="color:var(--muted);font-size:11px">—</span>') + '</td>' +
28
30
  '<td><span class="pr-badge ' + buildClass + '">' + escapeHtml(buildLabel) + '</span></td>' +
29
31
  '<td><span class="pr-badge ' + statusClass + '">' + escapeHtml(statusLabel) + '</span></td>' +
@@ -268,6 +268,7 @@
268
268
  .pr-badge.approved { background: rgba(63,185,80,0.15); color: var(--green); border: 1px solid var(--green); }
269
269
  .pr-badge.rejected { background: rgba(248,81,73,0.15); color: var(--red); border: 1px solid var(--red); }
270
270
  .pr-badge.needs-review { background: rgba(227,179,65,0.15); color: var(--orange); border: 1px solid var(--orange); }
271
+ .pr-badge.review-escalated { background: rgba(227,179,65,0.22); color: var(--orange); border: 1px dashed var(--orange); font-weight: 700; }
271
272
  .pr-badge.merged { background: rgba(188,140,255,0.15); color: var(--purple); border: 1px solid var(--purple); }
272
273
  .pr-badge.building { background: rgba(210,153,34,0.15); color: var(--yellow); border: 1px solid var(--yellow); animation: pulse 1.5s infinite; }
273
274
  .pr-badge.build-pass { background: rgba(63,185,80,0.15); color: var(--green); border: 1px solid var(--green); }
package/dashboard.js CHANGED
@@ -667,7 +667,7 @@ function ccSessionValid() {
667
667
  const CC_STATIC_SYSTEM_PROMPT = (() => {
668
668
  try {
669
669
  const raw = fs.readFileSync(path.join(MINIONS_DIR, 'prompts', 'cc-system.md'), 'utf8');
670
- return raw.replace(/\{\{minions_dir\}\}/g, MINIONS_DIR);
670
+ return shared.renderCcSystemPrompt(raw, { liveRoot: MINIONS_DIR });
671
671
  } catch (e) {
672
672
  console.error('Failed to load prompts/cc-system.md:', e.message);
673
673
  return 'You are the Command Center AI for Minions. Delegate work to agents.';
@@ -32,9 +32,13 @@ Before scanning, the engine materializes plans and specs into project work items
32
32
  |----------|--------|---------------|
33
33
  | Minions review pending/waiting | Queue a code review | `review` |
34
34
  | Minions review `changes-requested` | Route back to author for fixes | `fix` |
35
+ | Human feedback pending | Route back to author for fixes | `fix` |
35
36
  | `buildStatus: "failing"` | Route to any agent for build fix | `fix` |
37
+ | `_mergeConflict: true` | Route to author for conflict resolution | `fix` |
36
38
  Skips PRs where `status !== "active"`.
37
39
 
40
+ PR fix triggers are evaluated in this source order inside `discoverFromPrs()`: review feedback first (`engine.js:2166-2180`), human feedback second (`engine.js:2191-2226`), build failure third (`engine.js:2229-2271`), and merge conflict fourth (`engine.js:2299-2317`). Conflict fixes are additionally gated by `!fixDispatched` (`engine.js:2301`), so any earlier successful fix dispatch in the same PR discovery pass suppresses the conflict fix until a later pass.
41
+
38
42
  ### Source 2: PRD Gap Analysis (via `materializePlansAsWorkItems`)
39
43
 
40
44
  PRD items flow through `materializePlansAsWorkItems()`, which scans `~/.minions/prd/*.json` for PRD files with `missing` / `updated` / `planned` items and creates work items in the target project's queue.
@@ -413,4 +417,3 @@ All discovery behavior is controlled via `config.json`:
413
417
  ```
414
418
 
415
419
  To disable a work source for a project, set `"enabled": false`. To change where the engine looks for PRD or PR files, change the `path` field (resolved relative to `localPath`).
416
-
@@ -21,14 +21,24 @@ How the engine manages the lifecycle of a PR from creation through review, fix,
21
21
  - Stores `minionsReview: { reviewer, reviewedAt, note }`
22
22
  - Creates feedback file for author agent
23
23
 
24
- ## 4. Fix dispatch (3 independent triggers, at most one per tick)
24
+ ## 4. Fix dispatch trigger order
25
+
26
+ `discoverFromPrs()` evaluates PR auto-fix triggers in a fixed order during each discovery pass:
27
+
28
+ 1. Review feedback (`changes-requested`) — `engine.js:2166-2180`
29
+ 2. Human feedback (`humanFeedback.pendingFix` or coalesced feedback) — `engine.js:2191-2226`
30
+ 3. Build failure (`buildStatus === 'failing'`) — `engine.js:2229-2271`
31
+ 4. Merge conflict (`_mergeConflict`) — `engine.js:2299-2317`
32
+
33
+ When multiple problems coexist, earlier triggers get the first chance to enqueue work. The local `fixDispatched` flag is declared before the fix triggers (`engine.js:2168`) and set after review-feedback, human-feedback, and build-failure dispatches (`engine.js:2180`, `engine.js:2226`, `engine.js:2271`). Conflict fixes run last and explicitly require `!fixDispatched` (`engine.js:2301`), so any earlier successful fix dispatch suppresses the conflict fix for that PR in the same discovery pass. Build fixes are evaluated after review and human feedback, but the build-fix condition itself is not gated by `!fixDispatched` (`engine.js:2238`).
25
34
 
26
35
  ### A. Review feedback (`changes-requested`)
27
36
 
28
- - Gate: `reviewStatus === 'changes-requested'` + `!awaitingReReview` + not dispatched + not on cooldown
37
+ - Gate: `reviewStatus === 'changes-requested'` + `!awaitingReReview` + `!evalEscalated` + not dispatched + not on cooldown
29
38
  - Routes to PR author via `_author_` routing token
30
39
  - `review_note` = reviewer's feedback
31
- - Sets `fixDispatched = true` — prevents trigger B from also firing this tick
40
+ - Sets `fixDispatched = true` — prevents human-feedback and conflict fixes from also firing this pass
41
+ - **Review-loop escalation**: after `evalMaxIterations` review→fix cycles (default 3), `_evalEscalated` is set on the PR and *only this trigger plus review/re-review* stop. Triggers B (human comments), C (build failures), and the merge-conflict fix path keep running. The dashboard PR row distinguishes the two states with separate badges (review badge `review-escalated` vs. build badge `build-escalated`).
32
42
 
33
43
  ### B. Human comments (`humanFeedback.pendingFix`)
34
44
 
@@ -36,13 +46,22 @@ How the engine manages the lifecycle of a PR from creation through review, fix,
36
46
  - Agent comments filtered out via `/\bMinions\s*\(/i` regex on comment body
37
47
  - Coalesces multiple comments arriving during cooldown into single fix
38
48
  - Routes to author
49
+ - Not gated by `_evalEscalated` — humans can always force more fixes via PR comments even after the review loop escalates.
39
50
 
40
51
  ### C. Build failures (`buildStatus === 'failing'`)
41
52
 
42
53
  - Gate: `buildFixAttempts < maxBuildFixAttempts` (default 3) + grace period expired
43
54
  - **Grace period** (`_buildFixPushedAt`): after fix dispatches, waits `buildFixGracePeriod` (default 10min, configurable in `ENGINE_DEFAULTS`) for CI to run before re-dispatching. Cleared when poller detects build status transition (CI actually ran).
44
55
  - **Error logs**: GitHub fetches annotations (failures only, not warnings) + Actions job log (always). ADO queries builds API directly (not status checks), fetches build timeline → failed task logs (up to 10 per build, up to 10 failing pipelines).
45
- - **Escalation**: after 3 failed attempts, writes inbox alert, sets `buildFixEscalated = true`, stops auto-dispatch. Counter resets when build recovers.
56
+ - **Build-fix escalation**: after 3 failed attempts, writes an inbox alert, sets `buildFixEscalated = true`, and stops *only this trigger* (auto-dispatch for build fixes). The counter resets when the build recovers. Independent of `_evalEscalated`.
57
+ - Not gated by `_evalEscalated` — build-fix is mechanical and runs even if the review loop has escalated.
58
+ - Sets `fixDispatched = true` after dispatch so the later conflict trigger is suppressed in the same pass.
59
+
60
+ ### D. Merge conflicts (`_mergeConflict`)
61
+
62
+ - Gate: `autoFixConflicts` + `status === 'active'` + `_mergeConflict` + `!fixDispatched`
63
+ - Routes to the PR author to resolve target-branch conflicts
64
+ - Runs after review, human, and build triggers; if any earlier trigger enqueued a fix for this PR, the conflict fix waits for a later discovery pass
46
65
 
47
66
  ## 5. Fix completes
48
67
 
@@ -71,7 +90,7 @@ How the engine manages the lifecycle of a PR from creation through review, fix,
71
90
  | Scenario | Guard |
72
91
  |---|---|
73
92
  | Simultaneous review + fix | `activePrIds` — skip PR if any dispatch in-flight |
74
- | Duplicate fix (review + human) | `fixDispatched` flag — only one fix per PR per tick |
93
+ | Duplicate fix (review + human + conflict) | `fixDispatched` flag — later human/conflict triggers skip after earlier fix dispatches in the same PR pass |
75
94
  | Branch write conflict | `isBranchActive()` mutex |
76
95
  | Fix while awaiting re-review | `awaitingReReview` (waiting + fixedAt) |
77
96
  | Build fix before CI runs | `_buildFixPushedAt` grace period (10min) |
@@ -100,8 +119,10 @@ How the engine manages the lifecycle of a PR from creation through review, fix,
100
119
  | `reviewStatus` | Poller + post-completion | `pending` / `approved` / `changes-requested` / `waiting` |
101
120
  | `buildStatus` | Poller | `none` / `passing` / `failing` / `running` |
102
121
  | `buildErrorLog` | Poller | Actual CI error output for fix agents |
103
- | `buildFixAttempts` | Discovery (on dispatch) | Counter for escalation cap |
104
- | `buildFixEscalated` | Discovery (on cap) | Stops auto-dispatch |
122
+ | `buildFixAttempts` | Discovery (on dispatch) | Counter for build-fix escalation cap |
123
+ | `buildFixEscalated` | Discovery (on cap) | Stops *build-fix* auto-dispatch only (review/re-review and other fix triggers continue) |
124
+ | `_reviewFixCycles` | Discovery (on dispatch) | Counter for review→fix cycle cap (`evalMaxIterations`) |
125
+ | `_evalEscalated` | Discovery (on cap) | Stops *review/re-review and review-feedback fix* auto-dispatch only (build-fix, conflict-fix, and human-feedback fix continue). Cleared when reviewer eventually approves the PR. |
105
126
  | `_buildFixPushedAt` | Discovery (on dispatch) | Grace period timestamp |
106
127
  | `_buildFailNotified` | Discovery | Dedup for inbox alert |
107
128
  | `lastPushedAt` | Poller (new commit) | Tracks latest push for re-review logic |
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "runtime": "copilot",
3
3
  "models": null,
4
- "cachedAt": "2026-04-29T01:10:14.567Z"
4
+ "cachedAt": "2026-04-29T01:13:06.848Z"
5
5
  }
@@ -2113,7 +2113,27 @@ function classifyFailure(code, stdout = '', stderr = '') {
2113
2113
  }
2114
2114
 
2115
2115
  // Permission / trust / auth failures
2116
- if (/permission denied|access denied|unauthorized|403 forbidden|trust.*blocked|auth.*fail/i.test(combined)) {
2116
+ //
2117
+ // History (W-moja4a5qp9pj): the previous patterns `trust.*blocked` and
2118
+ // `auth.*fail` used unbounded greedy `.*`. JSONL agent init events that
2119
+ // emit the entire skill / slash-command catalogue on a single line
2120
+ // happen to contain words like `check-self-authored-...` and
2121
+ // `diagnose-build-fail-...`, which made the greedy regex match across
2122
+ // thousands of unrelated characters and silently flag healthy agents
2123
+ // as PERMISSION_BLOCKED on any non-zero exit. Use anchored phrases that
2124
+ // only match real auth/trust failure messages.
2125
+ const _PERM_PHRASES = [
2126
+ /\bpermission denied\b/i,
2127
+ /\baccess denied\b/i,
2128
+ /\bunauthorized\b/i,
2129
+ /\b403 forbidden\b/i,
2130
+ /\bauthentication (?:failed|error|failure)\b/i,
2131
+ /\bauth(?:entication)? (?:fail(?:ed|ure|s)?|denied|rejected)\b/i,
2132
+ /\btrust (?:gate|domain|zone|policy)? ?(?:is |was |has been )?(?:blocked|denied|rejected)\b/i,
2133
+ /\bcredentials? (?:rejected|invalid|expired)\b/i,
2134
+ /\btoken (?:rejected|invalid|expired|revoked)\b/i,
2135
+ ];
2136
+ if (_PERM_PHRASES.some(re => re.test(combined))) {
2117
2137
  return FAILURE_CLASS.PERMISSION_BLOCKED;
2118
2138
  }
2119
2139
 
package/engine/shared.js CHANGED
@@ -1272,6 +1272,122 @@ function getAdoOrgBase(project) {
1272
1272
 
1273
1273
  // ── Path Sanitization ───────────────────────────────────────────────────────
1274
1274
 
1275
+ /**
1276
+ * Files in the LIVE Minions checkout (MINIONS_DIR) that the Command Center
1277
+ * must never edit directly. Three flavours:
1278
+ *
1279
+ * - "basenames": exact relative paths under the live root (engine.js, dashboard.js,
1280
+ * minions.js, config.json — and the runtime state files engine/control.json
1281
+ * and engine/dispatch.json).
1282
+ * - "globs": direct-child JS files under protected live directories
1283
+ * (engine/*.js, bin/*.js).
1284
+ * - "prefixes": relative directory prefixes whose entire subtree is read-only
1285
+ * when it lives in the live root (dashboard/**).
1286
+ *
1287
+ * The list is intentionally small and explicit. It mirrors the textual rule in
1288
+ * `prompts/cc-system.md`. Source of truth lives here; the system prompt renders
1289
+ * `{{cc_protected_paths}}` from this list at startup so the two cannot drift.
1290
+ *
1291
+ * The guard is ROOT-AWARE: a path only counts as protected when its absolute
1292
+ * resolution sits inside MINIONS_DIR. The same basename inside an isolated
1293
+ * task worktree (e.g. `D:/worktrees/minions-work/W-xxx/dashboard.js`) is NOT
1294
+ * protected — agents working in those copies are free to edit them, since
1295
+ * git keeps changes inside the worktree until the agent pushes a branch.
1296
+ */
1297
+ const _CC_PROTECTED_BASENAMES = Object.freeze([
1298
+ 'engine.js',
1299
+ 'dashboard.js',
1300
+ 'minions.js',
1301
+ 'config.json',
1302
+ 'engine/control.json',
1303
+ 'engine/dispatch.json',
1304
+ ]);
1305
+ const _CC_PROTECTED_FILE_GLOBS = Object.freeze([
1306
+ 'engine/*.js',
1307
+ 'bin/*.js',
1308
+ ]);
1309
+ const _CC_PROTECTED_PREFIXES = Object.freeze([
1310
+ 'dashboard/',
1311
+ ]);
1312
+
1313
+ /**
1314
+ * Returns the literal text used by the CC system prompt for the protected-file
1315
+ * rule. Combines the basenames + prefixes above into a single sentence so the
1316
+ * authored rule and the helper that enforces it can never disagree.
1317
+ *
1318
+ * The result is anchored to a specific live root so the LLM can't conflate
1319
+ * "edits to dashboard.js" with "edits to a worktree copy of dashboard.js".
1320
+ */
1321
+ function describeCcProtectedPaths(liveRoot) {
1322
+ const root = (liveRoot && typeof liveRoot === 'string') ? liveRoot : MINIONS_DIR;
1323
+ const norm = root.replace(/\\/g, '/');
1324
+ const basenames = _CC_PROTECTED_BASENAMES.map(b => '`' + b + '`').join(', ');
1325
+ const globs = _CC_PROTECTED_FILE_GLOBS.map(g => '`' + g + '`').join(', ');
1326
+ const prefixes = _CC_PROTECTED_PREFIXES.map(p => '`' + p + '**`').join(', ');
1327
+ return `READ ONLY in the live checkout at \`${norm}\` — never write/edit: ${basenames}, ${globs}, ${prefixes}. This rule is path-scoped, not basename-scoped. Files with the same basename inside an isolated agent worktree (e.g. \`{worktreeRoot}/W-<id>/dashboard.js\`) are NOT protected — agents working in their own worktrees may edit any repository source the work item requires.`;
1328
+ }
1329
+
1330
+ function renderCcSystemPrompt(raw, opts) {
1331
+ const liveRoot = (opts && typeof opts.liveRoot === 'string') ? opts.liveRoot : MINIONS_DIR;
1332
+ return String(raw || '')
1333
+ .replace(/\{\{minions_dir\}\}/g, liveRoot)
1334
+ .replace(/\{\{cc_protected_paths\}\}/g, describeCcProtectedPaths(liveRoot));
1335
+ }
1336
+
1337
+ /**
1338
+ * Is this absolute path a CC-protected file in the LIVE Minions checkout?
1339
+ *
1340
+ * Returns true ONLY if all three hold:
1341
+ * 1. `absPath` resolves to something inside `liveRoot` (default: MINIONS_DIR).
1342
+ * 2. Its relative path matches a protected basename (e.g. `dashboard.js`)
1343
+ * OR matches a protected direct-child glob (`engine/*.js`, `bin/*.js`)
1344
+ * OR sits under a protected directory prefix (`dashboard/`).
1345
+ * 3. The input is a real string (no nullish, no non-string values).
1346
+ *
1347
+ * Returns false for:
1348
+ * - Paths outside `liveRoot` (worktrees, sibling repos, scratch dirs, etc.)
1349
+ * - Non-protected files inside `liveRoot` (notes.md, knowledge/foo.md, …)
1350
+ * - Invalid inputs (null/undefined/empty/non-string)
1351
+ *
1352
+ * Why this exists: PR W-moja4a5qp9pj. The CC system prompt previously named
1353
+ * protected files by basename only ("never write/edit dashboard.js"). Agents
1354
+ * dispatched into isolated worktrees inherited the same prose verbatim and
1355
+ * occasionally interpreted it as banning their own worktree copy of those
1356
+ * files, blocking otherwise legitimate fixes. The guard now distinguishes
1357
+ * "same path, live tree" from "same basename, worktree copy".
1358
+ */
1359
+ function isLiveCommandCenterPath(absPath, opts) {
1360
+ if (typeof absPath !== 'string' || absPath.length === 0) return false;
1361
+ if (absPath.includes('\0')) return false;
1362
+ const liveRoot = (opts && typeof opts.liveRoot === 'string') ? opts.liveRoot : MINIONS_DIR;
1363
+ const pathApi = /^[a-zA-Z]:[\\/]/.test(absPath) || /^[a-zA-Z]:[\\/]/.test(liveRoot) ? path.win32 : path;
1364
+ let resolved;
1365
+ let resolvedRoot;
1366
+ try {
1367
+ resolved = pathApi.resolve(absPath);
1368
+ resolvedRoot = pathApi.resolve(liveRoot);
1369
+ } catch { return false; }
1370
+ // Must be inside liveRoot. Compare with trailing separator to avoid the
1371
+ // sibling-prefix bug ("D:/squad-old" startsWith "D:/squad").
1372
+ const rootWithSep = resolvedRoot.endsWith(pathApi.sep) ? resolvedRoot : (resolvedRoot + pathApi.sep);
1373
+ const caseInsensitive = pathApi === path.win32 || process.platform === 'win32';
1374
+ const cmpResolved = caseInsensitive ? resolved.toLowerCase() : resolved;
1375
+ const cmpResolvedRoot = caseInsensitive ? resolvedRoot.toLowerCase() : resolvedRoot;
1376
+ const cmpRootWithSep = caseInsensitive ? rootWithSep.toLowerCase() : rootWithSep;
1377
+ if (cmpResolved !== cmpResolvedRoot && !cmpResolved.startsWith(cmpRootWithSep)) return false;
1378
+ // Compute the path relative to the live root and normalize separators so
1379
+ // the basename / prefix checks are platform-independent.
1380
+ const rel = pathApi.relative(resolvedRoot, resolved).replace(/\\/g, '/');
1381
+ if (rel === '' || rel === '.') return false; // root itself is not a "file"
1382
+ const relForMatch = rel.toLowerCase();
1383
+ if (_CC_PROTECTED_BASENAMES.includes(relForMatch)) return true;
1384
+ if (/^(?:engine|bin)\/[^/]+\.js$/.test(relForMatch)) return true;
1385
+ for (const prefix of _CC_PROTECTED_PREFIXES) {
1386
+ if (relForMatch === prefix.slice(0, -1) /* exact dir */ || relForMatch.startsWith(prefix)) return true;
1387
+ }
1388
+ return false;
1389
+ }
1390
+
1275
1391
  /**
1276
1392
  * Validate that a user-supplied filename stays within the given base directory.
1277
1393
  * Rejects path traversal (../, encoded variants), null bytes, and absolute paths.
@@ -2099,6 +2215,12 @@ module.exports = {
2099
2215
  getAdoOrgBase,
2100
2216
  sanitizePath,
2101
2217
  sanitizeBranch,
2218
+ isLiveCommandCenterPath,
2219
+ describeCcProtectedPaths,
2220
+ renderCcSystemPrompt,
2221
+ _CC_PROTECTED_BASENAMES, // exported for testing
2222
+ _CC_PROTECTED_FILE_GLOBS, // exported for testing
2223
+ _CC_PROTECTED_PREFIXES, // exported for testing
2102
2224
  isAllowedOrigin,
2103
2225
  buildSecurityHeaders,
2104
2226
  hasDangerousKey,
package/engine.js CHANGED
@@ -2049,7 +2049,8 @@ async function discoverFromPrs(config, project) {
2049
2049
  for (const pr of prs) {
2050
2050
  if (pr.status !== PR_STATUS.ACTIVE || pr._contextOnly) continue;
2051
2051
  const prDisplayId = shared.getPrDisplayId(pr);
2052
- if (activePrIds.has(pr.id)) continue; // Skip PRs with active dispatch (prevent race)
2052
+ const prCanonicalId = shared.getCanonicalPrId(project, pr, pr.url || '');
2053
+ if (activePrIds.has(prCanonicalId)) continue; // Skip PRs with active dispatch (prevent race)
2053
2054
  // Branch mutex: skip if PR branch is locked by any active dispatch (cross-type collision)
2054
2055
  if (pr.branch && isBranchActive(pr.branch)) {
2055
2056
  log('info', `Branch mutex: skipping PR ${pr.id} dispatch — branch ${pr.branch} locked by another agent`);
@@ -2080,7 +2081,7 @@ async function discoverFromPrs(config, project) {
2080
2081
  if (target) target._evalEscalated = true;
2081
2082
  });
2082
2083
  } catch (e) { log('warn', 'mark eval escalated: ' + e.message); }
2083
- log('warn', `PR ${pr.id}: review→fix escalated after ${evalCycles} cycles — suspending auto-dispatch`);
2084
+ log('warn', `PR ${pr.id}: review→fix escalated after ${evalCycles} cycles — suspending review/re-review and review-fix dispatch; build/conflict fixes may continue`);
2084
2085
  }
2085
2086
 
2086
2087
  // PRs needing review: evalLoop gates the entire review+fix cycle; pollEnabled ensures reviewStatus is fresh
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1617",
3
+ "version": "0.1.1619",
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"
@@ -17,7 +17,7 @@ Codex will review your changes — make sure your implementation is thorough and
17
17
  - Leave no stone unturned when implementing or explaining. Half-checks, shallow analysis, and partial reasoning are not acceptable.
18
18
 
19
19
  ## Guardrails
20
- READ ONLY — never write/edit: `engine.js`, `engine/*.js`, `dashboard.js`, `dashboard/**`, `minions.js`, `bin/*.js`, `engine/control.json`, `engine/dispatch.json`, `config.json`.
20
+ {{cc_protected_paths}}
21
21
  CAN modify: notes, plans, knowledge, work items, pull-requests.json, routing.md, charters, skills, playbooks, project repos.
22
22
 
23
23
  ## Filesystem