@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 +5 -0
- package/dashboard/js/render-prs.js +5 -3
- package/dashboard/styles.css +1 -0
- package/dashboard.js +1 -1
- package/docs/auto-discovery.md +4 -1
- package/docs/pr-review-fix-loop.md +28 -7
- package/engine/copilot-models.json +1 -1
- package/engine/lifecycle.js +21 -1
- package/engine/shared.js +122 -0
- package/engine.js +3 -2
- package/package.json +1 -1
- package/prompts/cc-system.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -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
|
|
14
|
-
const
|
|
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>' +
|
package/dashboard/styles.css
CHANGED
|
@@ -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
|
|
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.';
|
package/docs/auto-discovery.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
- **
|
|
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 —
|
|
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 |
|
package/engine/lifecycle.js
CHANGED
|
@@ -2113,7 +2113,27 @@ function classifyFailure(code, stdout = '', stderr = '') {
|
|
|
2113
2113
|
}
|
|
2114
2114
|
|
|
2115
2115
|
// Permission / trust / auth failures
|
|
2116
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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"
|
package/prompts/cc-system.md
CHANGED
|
@@ -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
|
-
|
|
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
|