@yemi33/minions 0.1.2250 → 0.1.2251
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/minions.js +5 -3
- package/dashboard/js/command-center.js +1 -1
- package/dashboard/js/refresh.js +9 -3
- package/dashboard/js/render-prs.js +1 -1
- package/dashboard/js/utils.js +2 -1
- package/dashboard/slim/js/link-pr.js +1 -1
- package/dashboard.js +41 -1
- package/docs/deprecated.json +10 -35
- package/docs/pr-auto-fix-dispatch.md +2 -0
- package/engine/check-status.js +11 -3
- package/engine/cli.js +0 -15
- package/engine/dispatch.js +19 -3
- package/engine/lifecycle.js +45 -4
- package/engine/pipeline.js +5 -1
- package/engine/queries.js +44 -26
- package/engine/scheduler.js +8 -8
- package/engine/shared.js +26 -179
- package/engine/timeout.js +2 -0
- package/engine/watch-actions.js +2 -2
- package/engine/worktree-gc.js +45 -31
- package/engine.js +2 -2
- package/package.json +1 -1
package/bin/minions.js
CHANGED
|
@@ -734,9 +734,11 @@ if (devMode) process.env.MINIONS_DASHBOARD_PORT = String(devPort);
|
|
|
734
734
|
const DASHBOARD_PORT = resolveDashboardPort(rest).port;
|
|
735
735
|
// Propagate the port to every child (engine, dashboard, delegated subcommands).
|
|
736
736
|
// engine/cli.js status reads MINIONS_PORT; dashboard.js reads PORT. We export
|
|
737
|
-
// MINIONS_PORT
|
|
738
|
-
//
|
|
739
|
-
//
|
|
737
|
+
// MINIONS_PORT as the *requested* port for legacy consumers, but loopback callers
|
|
738
|
+
// (watch-actions / pipeline / managed-spawn) must read the *actual* bound port
|
|
739
|
+
// from the dashboard-port.json beacon via shared.readDashboardPortFile() — the
|
|
740
|
+
// dashboard may bind to a fallback port (7332+) on EADDRINUSE, and MINIONS_PORT
|
|
741
|
+
// stays frozen at the originally-requested value in that case.
|
|
740
742
|
process.env.MINIONS_PORT = String(DASHBOARD_PORT);
|
|
741
743
|
const POST_UPDATE_INIT_TIMEOUT_MS = 120000;
|
|
742
744
|
// W-mqpx0tpi — the post-update `minions restart` runs as a child of `minions
|
|
@@ -2050,7 +2050,7 @@ async function ccExecuteAction(action, targetTabId, opts) {
|
|
|
2050
2050
|
break;
|
|
2051
2051
|
}
|
|
2052
2052
|
case 'link-pr': {
|
|
2053
|
-
var prLinkRes = await _ccFetch('/api/pull-requests/link', { url: action.url, title: action.title || '', project: action.project || '',
|
|
2053
|
+
var prLinkRes = await _ccFetch('/api/pull-requests/link', { url: action.url, title: action.title || '', project: action.project || '', contextOnly: action.autoObserve === false });
|
|
2054
2054
|
var prLinkData = await prLinkRes.json().catch(function() { return {}; });
|
|
2055
2055
|
// eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; PR URL and server message wrapped in escHtml() (fields: action.url, prLinkData.message)
|
|
2056
2056
|
status.innerHTML = '✓ PR linked: <strong>' + escHtml(action.url) + '</strong>' +
|
package/dashboard/js/refresh.js
CHANGED
|
@@ -869,10 +869,16 @@ function _processStatusUpdate(data, opts) {
|
|
|
869
869
|
_safeRender('commands', function() { renderCommands(data.commands || []); });
|
|
870
870
|
_changed('mcpServers', data.mcpServers);
|
|
871
871
|
_safeRender('mcpServers', function() { renderMcpServers(data.mcpServers || []); });
|
|
872
|
-
// Harness propagation diagnostic comes from /api/harness/diagnostics
|
|
873
|
-
//
|
|
874
|
-
//
|
|
872
|
+
// Harness propagation diagnostic comes from /api/harness/diagnostics.
|
|
873
|
+
// HEAVY ENDPOINT: the handler runs a synchronous `git status` per linked
|
|
874
|
+
// project (execFileSync, 10s timeout) which hard-blocks the dashboard event
|
|
875
|
+
// loop — a single WSL/UNC-path project freezes the whole dashboard for up to
|
|
876
|
+
// 10s on every poll. It is also "rare-change" and its output is ONLY rendered
|
|
877
|
+
// on the Tools page (`#harness-diag`), with no cross-page consumer of
|
|
878
|
+
// window._lastHarnessDiag. So only poll it while the Tools page is open;
|
|
879
|
+
// every other page skips the fetch entirely. (Load-reduction audit 2026-06-24.)
|
|
875
880
|
_safeRender('harnessDiag', function() {
|
|
881
|
+
if (typeof currentPage !== 'undefined' && currentPage !== 'tools') return;
|
|
876
882
|
fetch('/api/harness/diagnostics')
|
|
877
883
|
.then(function (r) { return r.ok ? r.json() : Promise.reject(); })
|
|
878
884
|
.then(function (fresh) {
|
|
@@ -548,7 +548,7 @@ async function _submitLinkPr(e) {
|
|
|
548
548
|
try {
|
|
549
549
|
const res = await fetch('/api/pull-requests/link', {
|
|
550
550
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
551
|
-
body: JSON.stringify({ url, title, project, context, autoObserve })
|
|
551
|
+
body: JSON.stringify({ url, title, project, context, contextOnly: !autoObserve })
|
|
552
552
|
});
|
|
553
553
|
const data = await res.json();
|
|
554
554
|
if (res.ok) {
|
package/dashboard/js/utils.js
CHANGED
|
@@ -537,7 +537,8 @@ function _renderMdCore(s) {
|
|
|
537
537
|
html = html.replace(/~~(.+?)~~/g, '<s>$1</s>');
|
|
538
538
|
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, function(_, text, href) {
|
|
539
539
|
if (/^(javascript|data|vbscript):/i.test(href)) return text;
|
|
540
|
-
|
|
540
|
+
var safeHref = href.replace(/"/g, '%22');
|
|
541
|
+
return '<a href="' + safeHref + '" target="_blank" rel="noopener" style="color:var(--blue)">' + text + '</a>';
|
|
541
542
|
});
|
|
542
543
|
|
|
543
544
|
// 3. Block-level processing (line by line)
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
var res = await fetch('/api/pull-requests/link', {
|
|
47
47
|
method: 'POST',
|
|
48
48
|
headers: { 'Content-Type': 'application/json' },
|
|
49
|
-
body: JSON.stringify({ url: url, title: title, project: project, context: context,
|
|
49
|
+
body: JSON.stringify({ url: url, title: title, project: project, context: context, contextOnly: !autoObserve }),
|
|
50
50
|
});
|
|
51
51
|
var data = await res.json().catch(function() { return {}; });
|
|
52
52
|
if (res.ok) {
|
package/dashboard.js
CHANGED
|
@@ -5,6 +5,16 @@
|
|
|
5
5
|
* Opens: http://localhost:7331
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
// Enlarge the libuv thread pool (default 4) BEFORE any async fs/crypto op
|
|
9
|
+
// initializes it. The dashboard fans out many concurrent filesystem reads —
|
|
10
|
+
// the worst being the knowledge-base scan (~thousands of files) — and with
|
|
11
|
+
// only 4 pool threads a single fs-heavy burst monopolizes the pool and starves
|
|
12
|
+
// every other fs-dependent request (including the /api/status poll that drives
|
|
13
|
+
// the "Dashboard unreachable — stale" banner). 16 gives headroom without
|
|
14
|
+
// meaningful memory cost. Respect an operator override if already set.
|
|
15
|
+
// (Load-reduction audit 2026-06-24.)
|
|
16
|
+
if (!process.env.UV_THREADPOOL_SIZE) process.env.UV_THREADPOOL_SIZE = '16';
|
|
17
|
+
|
|
8
18
|
// Install ISO timestamp prefixes on console.{log,info,warn,error} so the
|
|
9
19
|
// post-mortem in engine/dashboard-stdio.log is diagnosable to the second.
|
|
10
20
|
// Must run BEFORE any other require that might log during module init.
|
|
@@ -1335,6 +1345,15 @@ function _scanProjectLocalHarnessFootgun(project) {
|
|
|
1335
1345
|
footgunWarning: _PROJECT_LOCAL_FOOTGUN_WARNING,
|
|
1336
1346
|
};
|
|
1337
1347
|
if (!project || !project.localPath) return out;
|
|
1348
|
+
// Skip the synchronous `git status` for UNC / WSL-from-Windows paths
|
|
1349
|
+
// (\\wsl.localhost\..., \\wsl$\..., or any \\server\share). git/stat over a
|
|
1350
|
+
// WSL UNC mount can hang for minutes; even capped at the 10s timeout below it
|
|
1351
|
+
// hard-blocks the dashboard event loop on every poll, freezing the whole
|
|
1352
|
+
// dashboard. Such projects can't carry uncommitted project-local harness
|
|
1353
|
+
// assets the engine would propagate anyway, so returning the empty shell here
|
|
1354
|
+
// is correct, not lossy. (Load-reduction audit 2026-06-24.)
|
|
1355
|
+
const _lp = String(project.localPath);
|
|
1356
|
+
if (_lp.startsWith('\\\\') || _lp.startsWith('//')) return out;
|
|
1338
1357
|
let raw = '';
|
|
1339
1358
|
try {
|
|
1340
1359
|
const { execFileSync } = require('child_process');
|
|
@@ -1397,7 +1416,28 @@ function _scanProjectLocalHarnessFootgun(project) {
|
|
|
1397
1416
|
return out;
|
|
1398
1417
|
}
|
|
1399
1418
|
|
|
1419
|
+
// TTL cache for the harness diagnostic. The build runs a synchronous `git
|
|
1420
|
+
// status` + directory walks per project; the Tools page polls it while open,
|
|
1421
|
+
// so without a cache it re-runs the whole sweep every 4s and blocks the event
|
|
1422
|
+
// loop each time. The diagnostic is rare-change, so a short TTL is ample.
|
|
1423
|
+
// Bypassed whenever explicit opts are supplied (unit tests pass homeDir/
|
|
1424
|
+
// projects/engineConfig) so test determinism is unaffected.
|
|
1425
|
+
// (Load-reduction audit 2026-06-24.)
|
|
1426
|
+
let _harnessDiagCache = null;
|
|
1427
|
+
let _harnessDiagCacheTs = 0;
|
|
1428
|
+
const _HARNESS_DIAG_TTL_MS = 20000;
|
|
1429
|
+
|
|
1400
1430
|
function _buildHarnessDiagnostics(opts = {}) {
|
|
1431
|
+
const _useCache = !opts || Object.keys(opts).length === 0;
|
|
1432
|
+
if (_useCache && _harnessDiagCache && (Date.now() - _harnessDiagCacheTs) < _HARNESS_DIAG_TTL_MS) {
|
|
1433
|
+
return _harnessDiagCache;
|
|
1434
|
+
}
|
|
1435
|
+
const out = _buildHarnessDiagnosticsUncached(opts);
|
|
1436
|
+
if (_useCache) { _harnessDiagCache = out; _harnessDiagCacheTs = Date.now(); }
|
|
1437
|
+
return out;
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
function _buildHarnessDiagnosticsUncached(opts = {}) {
|
|
1401
1441
|
const homeDir = opts.homeDir || os.homedir();
|
|
1402
1442
|
const engineConfig = opts.engineConfig || (CONFIG && CONFIG.engine) || {};
|
|
1403
1443
|
const projects = (opts.projects || PROJECTS || []).filter(
|
|
@@ -7502,7 +7542,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
7502
7542
|
if (isJson) {
|
|
7503
7543
|
try {
|
|
7504
7544
|
const plan = JSON.parse(content);
|
|
7505
|
-
const status = plan.status || 'active';
|
|
7545
|
+
const status = plan.status || (plan.requires_approval && !plan.approvedAt ? 'awaiting-approval' : 'active');
|
|
7506
7546
|
// W-mqacrzis0003df4a Bug 2 — fresh staleness from plans/*.md mtime.
|
|
7507
7547
|
// plan.planStale lags by an engine tick; stat the source plan here
|
|
7508
7548
|
// so the Plans tab does not silently mirror a stale-but-cached false.
|
package/docs/deprecated.json
CHANGED
|
@@ -110,41 +110,6 @@
|
|
|
110
110
|
"targetRemovalDate": "2026-08-03",
|
|
111
111
|
"notes": "Introduced by W-mq03l6zh0006f0a1 (Per-org ADO throttle isolation). 60-day window (2 release cycles + buffer) gives the in-flight per-org migration time to land + observe per-org keys on the diagnostics endpoint. Observable live at GET /api/diagnostics/ado-throttle — the endpoint reports a single `global` key while the arg-less shim is still load-bearing, and per-org `<orgBase>` keys once isolation is complete; that key shape is the human-readable signal for whether this shim can retire."
|
|
112
112
|
},
|
|
113
|
-
{
|
|
114
|
-
"id": "pr-record-contextOnly-underscore-alias",
|
|
115
|
-
"description": "Legacy `pr._contextOnly` field on PR records (engine/<scope>/pull-requests.json + SQL). Renamed to canonical `pr.contextOnly` by the W-mq5s5ttx000j7ab8 PR-gate consolidation. The write side is already gone: normalizePrRecord DELETES `_contextOnly` once canonical `contextOnly` is present, and dashboard.js no longer references the alias at all. What survives is a READ-BRIDGE ONLY — code still reads `_contextOnly === true` as a fallback for pre-migration / raw on-disk records, and a one-shot boot migration copies the alias onto canonical `contextOnly` and deletes it.",
|
|
116
|
-
"code": [
|
|
117
|
-
{ "file": "engine/shared.js", "note": "normalizePrRecord deletes `_contextOnly` when canonical `contextOnly` is present (engine/shared.js:6502-6507); the linkPr path deletes it after reading (:6716); the boot migration migratePrGateFlags deletes it (:6868-6870). The only surviving reads are legacy fallbacks: `pr._contextOnly === true` at :6555, :6713, :6759." },
|
|
118
|
-
{ "file": "dashboard.js", "note": "Zero `_contextOnly` references — neither GET /api/pull-requests nor any observe/link handler writes or shapes the alias." },
|
|
119
|
-
{ "file": "engine.js", "note": "discoverFromPrs reads canonical `pr.contextOnly` only." }
|
|
120
|
-
],
|
|
121
|
-
"deprecated": "2026-06-08",
|
|
122
|
-
"targetRemovalDate": "2026-06-25",
|
|
123
|
-
"notes": "The write side already shipped out — there is no dual-write to retire. What remains is the read-fallback (`_contextOnly === true`) plus the one-shot boot migration migratePrGateFlags (engine/shared.js:6833) that copies + deletes the alias from any pre-migration on-disk record. Pair this entry's removal with `pr-record-autoObserve-underscore-alias`, `pr-record-manual-underscore-alias`, and `pr-link-autoObserve-body-param` — they share the same boot migration. Removal scope (DEFERRED — gated on a positive signal that every live pull-requests.json has been swept of underscore keys, not on the expired calendar date): delete the `_contextOnly` read-fallback in engine/shared.js and the migratePrGateFlags handling of it. On-disk sweep confirmed CLEAN on 2026-06-18: grep of every live projects/*/pull-requests.json (budget-test-project, constellation, minions-opg, office-bohemia) for the `_contextOnly`/`_autoObserve`/`_manual` record keys returned zero matches, so the deferral gate is now cleared and a deliberate, human-reviewed code removal can be scheduled (this calendar bump 2026-06-16 → 2026-06-25 is the docs-only consistency half; the MEDIUM/>3-file code removal stays a separate PR)."
|
|
124
|
-
},
|
|
125
|
-
{
|
|
126
|
-
"id": "pr-record-autoObserve-underscore-alias",
|
|
127
|
-
"description": "Legacy `pr._autoObserve` boolean on PR records. Was one of the clauses in the old `isAutoManagedPrRecord` check; replaced by the canonical `pr.contextOnly` field (foreign-author PRs linked with `autoObserve: true` now land with `contextOnly: false` directly). The write side is already gone — normalizePrRecord and the boot migration DELETE `_autoObserve`, and dashboard.js never writes it. What survives is a READ-BRIDGE ONLY: `_prRecordIsLegacyManaged` still reads `record._autoObserve === true` as a fallback when canonical `contextOnly` is absent on a pre-migration record.",
|
|
128
|
-
"code": [
|
|
129
|
-
{ "file": "engine/shared.js", "note": "normalizePrRecord deletes `_autoObserve` (engine/shared.js:6502-6507); the linkPr path deletes it (:6764); migratePrGateFlags deletes it (:6868). The only surviving read is the legacy-managed fallback `record._autoObserve === true` in _prRecordIsLegacyManaged (:6823)." },
|
|
130
|
-
{ "file": "dashboard.js", "note": "Zero `_autoObserve` references — link/observe handlers accept the canonical `contextOnly` body param and write no underscore alias onto the record." }
|
|
131
|
-
],
|
|
132
|
-
"deprecated": "2026-06-08",
|
|
133
|
-
"targetRemovalDate": "2026-06-25",
|
|
134
|
-
"notes": "The write side already shipped out — there is no dual-write to retire. Removal is paired with `pr-record-contextOnly-underscore-alias` — same boot migration, same read-bridge. Removal scope (DEFERRED — gated on a sweep confirming no live record retains `_autoObserve`, not on the expired calendar date): delete the `_autoObserve === true` read in _prRecordIsLegacyManaged (engine/shared.js:6823) and the migratePrGateFlags handling of it. On-disk sweep confirmed CLEAN on 2026-06-18: grep of every live projects/*/pull-requests.json (budget-test-project, constellation, minions-opg, office-bohemia) for the `_contextOnly`/`_autoObserve`/`_manual` record keys returned zero matches, so the deferral gate is now cleared and a deliberate, human-reviewed code removal can be scheduled (this calendar bump 2026-06-16 → 2026-06-25 is the docs-only consistency half; the MEDIUM/>3-file code removal stays a separate PR)."
|
|
135
|
-
},
|
|
136
|
-
{
|
|
137
|
-
"id": "pr-record-manual-underscore-alias",
|
|
138
|
-
"description": "Legacy `pr._manual` boolean on PR records (set by older `POST /api/pull-requests/link` writes to mark manually-linked PRs). Was the third clause in the old `isAutoManagedPrRecord` check (`pr._manual && !pr._contextOnly` → auto-managed). Post-consolidation, the dispatch gate reads only `pr.contextOnly`. The write side is already gone — normalizePrRecord and the boot migration DELETE `_manual`, and dashboard.js never writes it. The alias survives only as data on pre-migration on-disk records that the boot migration sweeps away.",
|
|
139
|
-
"code": [
|
|
140
|
-
{ "file": "engine.js", "note": "discoverFromPrs reads canonical `pr.contextOnly` only — `_manual` is no longer read." },
|
|
141
|
-
{ "file": "engine/shared.js", "note": "normalizePrRecord deletes `_manual` (engine/shared.js:6502-6507); the linkPr path deletes it (:6765); migratePrGateFlags deletes it (:6869). No code reads `_manual` for dispatch decisions." },
|
|
142
|
-
{ "file": "dashboard.js", "note": "Zero `_manual` references — the link handler no longer writes the provenance alias onto the record." }
|
|
143
|
-
],
|
|
144
|
-
"deprecated": "2026-06-08",
|
|
145
|
-
"targetRemovalDate": "2026-06-25",
|
|
146
|
-
"notes": "Disambiguation: this entry covers ONLY the PR-record `_manual` flag (engine/<scope>/pull-requests.json). Other `_manual`-named flags elsewhere in the engine (work items, dispatch records, etc.) are NOT covered by this entry and remain in active use. The write side already shipped out — there is no write site or dashboard badge left to remove. Removal scope (DEFERRED — gated on a sweep confirming no live record retains `_manual`, not on the expired calendar date): drop the `_manual` handling in normalizePrRecord and migratePrGateFlags (engine/shared.js) once the on-disk sweep is confirmed. On-disk sweep confirmed CLEAN on 2026-06-18: grep of every live projects/*/pull-requests.json (budget-test-project, constellation, minions-opg, office-bohemia) for the `_contextOnly`/`_autoObserve`/`_manual` record keys returned zero matches, so the deferral gate is now cleared and a deliberate, human-reviewed code removal can be scheduled (this calendar bump 2026-06-16 → 2026-06-25 is the docs-only consistency half; the MEDIUM/>3-file code removal stays a separate PR)."
|
|
147
|
-
},
|
|
148
113
|
{
|
|
149
114
|
"id": "pr-link-autoObserve-body-param",
|
|
150
115
|
"description": "Legacy `autoObserve` body parameter on `POST /api/pull-requests/link`. Replaced by canonical `contextOnly` body param (inverse boolean: `autoObserve: false` ⇔ `contextOnly: true`). This is a READ-BRIDGE on the input side only — the handler reads `body.contextOnly` first and falls back to `!body.autoObserve` for callers not yet migrated. No underscore alias is written onto the record (the record-field aliases already shipped out — see the three paired entries); the only thing kept alive is the input fallback.",
|
|
@@ -176,5 +141,15 @@
|
|
|
176
141
|
"deprecated": "2026-06-08",
|
|
177
142
|
"targetRemovalDate": null,
|
|
178
143
|
"notes": "targetRemovalDate intentionally null — unlike the record-field aliases (`_contextOnly`, `_autoObserve`, `_manual`) which carry a 7-day clock, the `observe` body param is documented as a longer-lived back-compat alias. Set targetRemovalDate to a concrete future date once the dashboard UI + any client scripts are confirmed to POST `contextOnly` exclusively. Removal scope when the date is set: drop the `body.observe` fallback in dashboard.js, drop `observe` from the route registry params, and update any client still POSTing `observe`."
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
"id": "discover-review-skills-shim",
|
|
147
|
+
"description": "engine/discover-review-skills.js is a 25-line re-export shim for engine/discover-project-skills.js. Zero production callers; only test files (discover-review-skills.test.js, discover-project-skills.test.js:706) reference it.",
|
|
148
|
+
"addedDate": "2026-06-24",
|
|
149
|
+
"targetRemovalDate": "2026-09-01",
|
|
150
|
+
"removalGate": "No external callers import discover-review-skills.js; test files updated to import from discover-project-skills.js directly.",
|
|
151
|
+
"removalScope": "Delete engine/discover-review-skills.js, delete test/unit/discover-review-skills.test.js, update test/unit/discover-project-skills.test.js:706 to import from discover-project-skills.js.",
|
|
152
|
+
"autoRemoveSafe": false,
|
|
153
|
+
"notes": "gate: zero test references to discover-review-skills; must update tests to point to discover-project-skills.js before file can be deleted"
|
|
179
154
|
}
|
|
180
155
|
]
|
|
@@ -24,6 +24,8 @@ Each PR carries a `repoHost` (`github` / `ado`). Before evaluating any of the si
|
|
|
24
24
|
- GitHub PRs honor `ghPollEnabled` (legacy macro) and the new granular `ghPrStatusPollEnabled` / `ghPrCommentsPollEnabled` / `ghPrReconcileEnabled`.
|
|
25
25
|
- ADO PRs honor `adoPollEnabled` and `adoPrStatusPollEnabled` / `adoPrCommentsPollEnabled` / `adoPrReconcileEnabled`.
|
|
26
26
|
|
|
27
|
+
> **UI-saved configs always carry the granular keys.** The dashboard Settings save (`dashboard/js/settings.js`) persists all seven granular poll keys on every save, so any `config.json` written through the UI always includes them. The `resolvePollFlag` legacy-macro fallback (which falls back to `{ado,gh}PollEnabled` when a granular key is absent) therefore applies only to hand-edited or legacy configs that omit the granular keys.
|
|
28
|
+
|
|
27
29
|
When the provider's status poll is off, the cached `buildStatus` / `reviewStatus` / `_mergeConflict` won't refresh — but `discoverFromPrs` still runs against the **last known cache** unless `pollingPaused` is also set, which forces `pollEnabled=false` and makes every per-PR auto-dispatch gate inert.
|
|
28
30
|
|
|
29
31
|
See [`docs/auto-discovery.md`](auto-discovery.md) → "Granular per-poller flags" for the full polling-side table and the resolution order.
|
package/engine/check-status.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const dir = path.resolve(__dirname, '..');
|
|
4
|
+
const { safeJson, MINIONS_DIR } = require('./shared');
|
|
4
5
|
|
|
5
6
|
console.log('=== Work Items (non-done) ===');
|
|
6
7
|
let items = [];
|
|
@@ -11,9 +12,16 @@ items.filter(i => i.status !== 'done').forEach(i => {
|
|
|
11
12
|
|
|
12
13
|
console.log('\n=== Agent Status (derived from dispatch) ===');
|
|
13
14
|
const { getAgentStatus } = require('./queries');
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
let config = {};
|
|
16
|
+
try { config = safeJson(path.join(MINIONS_DIR, 'config.json')) || {}; } catch {}
|
|
17
|
+
const agents = Object.keys(config.agents || {});
|
|
18
|
+
if (agents.length === 0) {
|
|
19
|
+
console.log('(no agents configured)');
|
|
20
|
+
} else {
|
|
21
|
+
for (const a of agents) {
|
|
22
|
+
const s = getAgentStatus(a);
|
|
23
|
+
console.log(a.padEnd(10), s.status.padEnd(10), (s.task || '-').slice(0, 60));
|
|
24
|
+
}
|
|
17
25
|
}
|
|
18
26
|
|
|
19
27
|
console.log('\n=== Inbox ===');
|
package/engine/cli.js
CHANGED
|
@@ -609,21 +609,6 @@ const commands = {
|
|
|
609
609
|
try { shared.applyLegacyCcModelMigration(config, { logger: e.log }); }
|
|
610
610
|
catch (err) { e.log('warn', `legacy ccModel migration failed: ${err.message}`); }
|
|
611
611
|
|
|
612
|
-
// W-mq5s5ttx000j7ab8-a — One-shot canonical-gate migration. Project the
|
|
613
|
-
// legacy `_contextOnly` / `_autoObserve` / `_manual` keys onto the
|
|
614
|
-
// canonical `contextOnly` field on every `projects/<name>/pull-
|
|
615
|
-
// requests.json` record BEFORE the first tick fires. `isAutoManagedPrRecord`
|
|
616
|
-
// now reads `contextOnly` only, so any record the migration doesn't reach
|
|
617
|
-
// before tick #1 would flip its auto-managed verdict. Idempotent.
|
|
618
|
-
try {
|
|
619
|
-
const projectsRoot = path.join(shared.MINIONS_DIR, 'projects');
|
|
620
|
-
// W-mqil3jtw — pass the configured agent roster so the migration
|
|
621
|
-
// cross-checks `agent` against real agent ids instead of a shape regex;
|
|
622
|
-
// a GitHub author login (e.g. `calebt_microsoft`) must not be mistaken
|
|
623
|
-
// for a managed persona and flip a context-only PR's observe toggle.
|
|
624
|
-
shared.migratePrGateFlags(projectsRoot, { config });
|
|
625
|
-
} catch (err) { e.log('warn', `pr-gate-migration failed: ${err.message}`); }
|
|
626
|
-
|
|
627
612
|
// One-time force-on of the CC worker pool. The pool has been the resolved
|
|
628
613
|
// default for copilot CC since PR #2492, but configs still carrying an
|
|
629
614
|
// explicit `ccUseWorkerPool: false` (set before the default flipped) stay
|
package/engine/dispatch.js
CHANGED
|
@@ -19,10 +19,10 @@ const { getConfig, INBOX_DIR } = queries;
|
|
|
19
19
|
const MINIONS_DIR = shared.MINIONS_DIR;
|
|
20
20
|
|
|
21
21
|
// Dispatch types that do not push code to a PR branch. These remain dispatchable
|
|
22
|
-
// against
|
|
22
|
+
// against context-only PRs because casting a review vote, posting a
|
|
23
23
|
// comment, asking a question, or running a read-only explore never mutates the
|
|
24
24
|
// PR's source branch. Auto-discovery (engine.js#discoverFromPrs) is still
|
|
25
|
-
// gated on `
|
|
25
|
+
// gated on `contextOnly` separately — only explicit WIs (dashboard, watches,
|
|
26
26
|
// CC) can target a context-only PR, and only via these types.
|
|
27
27
|
const NON_MUTATING_DISPATCH_TYPES = new Set([
|
|
28
28
|
WORK_TYPE.REVIEW,
|
|
@@ -30,6 +30,19 @@ const NON_MUTATING_DISPATCH_TYPES = new Set([
|
|
|
30
30
|
WORK_TYPE.EXPLORE,
|
|
31
31
|
]);
|
|
32
32
|
|
|
33
|
+
// Dispatch types that push code directly onto the PR's source branch. Only for
|
|
34
|
+
// these does a branch mismatch (dispatch queued against old branch; PR since
|
|
35
|
+
// rebased) signal a genuinely stale entry.
|
|
36
|
+
//
|
|
37
|
+
// implement / implement_large attach a targetPr for read-only context while
|
|
38
|
+
// working on their own new branch. Comparing that work branch to the tracked
|
|
39
|
+
// PR source branch always differs and causes phantom auto-cancellation (#369).
|
|
40
|
+
const PR_BRANCH_TARGETED_TYPES = new Set([
|
|
41
|
+
WORK_TYPE.FIX,
|
|
42
|
+
WORK_TYPE.VERIFY,
|
|
43
|
+
WORK_TYPE.DECOMPOSE,
|
|
44
|
+
]);
|
|
45
|
+
|
|
33
46
|
// Lazy require to break circular dependency with engine.js
|
|
34
47
|
let _lifecycle = null;
|
|
35
48
|
function lifecycle() { if (!_lifecycle) _lifecycle = require('./lifecycle'); return _lifecycle; }
|
|
@@ -438,7 +451,9 @@ function getStalePrDispatchReason(entry, config) {
|
|
|
438
451
|
|
|
439
452
|
const queuedBranch = entry.meta.branch || entry.meta.pr?.branch || '';
|
|
440
453
|
const trackedBranch = tracked.branch || '';
|
|
441
|
-
if (
|
|
454
|
+
if (PR_BRANCH_TARGETED_TYPES.has(entry.type)
|
|
455
|
+
&& queuedBranch && trackedBranch
|
|
456
|
+
&& shared.sanitizeBranch(queuedBranch) !== shared.sanitizeBranch(trackedBranch)) {
|
|
442
457
|
return `PR ${tracked.id || prLabel} branch changed from ${queuedBranch} to ${trackedBranch}`;
|
|
443
458
|
}
|
|
444
459
|
|
|
@@ -1167,5 +1182,6 @@ module.exports = {
|
|
|
1167
1182
|
isCompletedWorkItemForFailure,
|
|
1168
1183
|
_isPrdSourcedAndVetted,
|
|
1169
1184
|
NON_MUTATING_DISPATCH_TYPES,
|
|
1185
|
+
PR_BRANCH_TARGETED_TYPES,
|
|
1170
1186
|
writeFailedAgentReport,
|
|
1171
1187
|
};
|
package/engine/lifecycle.js
CHANGED
|
@@ -1123,7 +1123,7 @@ function syncPrsFromOutput(output, agentId, meta, config, opts = {}) {
|
|
|
1123
1123
|
//
|
|
1124
1124
|
// `enrollPrFromCanonicalId` is the write-side fix: given a canonical PR id and
|
|
1125
1125
|
// the owning project, fetch live state from GitHub (or ADO, best-effort) and
|
|
1126
|
-
// insert a `
|
|
1126
|
+
// insert a `contextOnly: true` row into the project's `pull-requests.json`.
|
|
1127
1127
|
// Idempotent — no-op if a record already exists. Always marks the record as
|
|
1128
1128
|
// context-only so the engine doesn't try to re-dispatch fix/review loops on
|
|
1129
1129
|
// an already-merged PR (matches the #1772 enrollment-flag semantics).
|
|
@@ -1705,12 +1705,47 @@ function markMissingPrAttachment(meta, agentId, reason, resultSummary, severity,
|
|
|
1705
1705
|
let phantomRetryDeferred = false;
|
|
1706
1706
|
let phantomRetryExhausted = false;
|
|
1707
1707
|
let phantomRetryCount = 0;
|
|
1708
|
+
let systematicPhantomFired = false;
|
|
1708
1709
|
if (isHard && isPhantom && noPrWiPath) {
|
|
1709
1710
|
mutateJsonFileLocked(noPrWiPath, data => {
|
|
1710
1711
|
if (!Array.isArray(data)) return data;
|
|
1711
1712
|
const w = data.find(i => i.id === meta.item.id);
|
|
1712
1713
|
if (!w) return data;
|
|
1713
1714
|
const phantomRetries = w._phantomRetryCount || 0;
|
|
1715
|
+
|
|
1716
|
+
// W-mqsh4d95000fb753 — mirror the _phantomAgents tracking that
|
|
1717
|
+
// _deferRetryWithCounter performs. Without this, the systematic phantom
|
|
1718
|
+
// detector (≥2 distinct agents all phantom-complete the same item) never
|
|
1719
|
+
// fires through this path because _phantomAgents stays empty {}.
|
|
1720
|
+
if (!w._phantomAgents || typeof w._phantomAgents !== 'object' || Array.isArray(w._phantomAgents)) {
|
|
1721
|
+
w._phantomAgents = {};
|
|
1722
|
+
}
|
|
1723
|
+
if (agentId) {
|
|
1724
|
+
w._phantomAgents[agentId] = (w._phantomAgents[agentId] || 0) + 1;
|
|
1725
|
+
}
|
|
1726
|
+
const distinctPhantomAgents = Object.keys(w._phantomAgents);
|
|
1727
|
+
if (distinctPhantomAgents.length >= 2 && phantomRetries >= 1) {
|
|
1728
|
+
w.status = WI_STATUS.FAILED;
|
|
1729
|
+
w.failReason = `Systematic phantom completion across ${distinctPhantomAgents.length} agents ` +
|
|
1730
|
+
`(${distinctPhantomAgents.join(', ')}) — likely a content filter triggered by the task description ` +
|
|
1731
|
+
`or a malformed prompt. Manual intervention required.`;
|
|
1732
|
+
w._failureClass = 'phantom-all-agents';
|
|
1733
|
+
w.failedAt = ts();
|
|
1734
|
+
delete w.completedAt;
|
|
1735
|
+
delete w.dispatched_at;
|
|
1736
|
+
delete w.dispatched_to;
|
|
1737
|
+
delete w._pendingReason;
|
|
1738
|
+
delete w._phantomCompletion;
|
|
1739
|
+
delete w._phantomBranch;
|
|
1740
|
+
delete w._missingPrAttachment;
|
|
1741
|
+
phantomRetryExhausted = true;
|
|
1742
|
+
phantomRetryCount = phantomRetries;
|
|
1743
|
+
systematicPhantomFired = true;
|
|
1744
|
+
log('warn', `Work item ${meta.item.id} systematically phantoms across ${distinctPhantomAgents.length} agents ` +
|
|
1745
|
+
`(${distinctPhantomAgents.join(', ')}) — marking non-retryable (phantom-all-agents)`);
|
|
1746
|
+
return data;
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1714
1749
|
if (phantomRetries < ENGINE_DEFAULTS.maxPhantomRetries) {
|
|
1715
1750
|
w.status = WI_STATUS.PENDING;
|
|
1716
1751
|
w._phantomRetryCount = phantomRetries + 1;
|
|
@@ -1759,6 +1794,14 @@ function markMissingPrAttachment(meta, agentId, reason, resultSummary, severity,
|
|
|
1759
1794
|
}
|
|
1760
1795
|
return;
|
|
1761
1796
|
}
|
|
1797
|
+
if (systematicPhantomFired) {
|
|
1798
|
+
// Already hard-failed with phantom-all-agents inside mutateJsonFileLocked;
|
|
1799
|
+
// skip the generic hard-fail path below to preserve the correct failReason.
|
|
1800
|
+
if (meta.item?.sourcePlan) {
|
|
1801
|
+
try { syncPrdItemStatus(meta.item.id, WI_STATUS.FAILED, meta.item.sourcePlan); } catch (e) { log('warn', 'phantom-all-agents PRD sync: ' + e.message); }
|
|
1802
|
+
}
|
|
1803
|
+
return;
|
|
1804
|
+
}
|
|
1762
1805
|
if (phantomRetryExhausted) {
|
|
1763
1806
|
// Fall through to the regular hard-fail path with augmented reason so
|
|
1764
1807
|
// operators see "phantom retries exhausted" instead of the generic msg.
|
|
@@ -3402,9 +3445,7 @@ function createReviewFeedbackForAuthor(reviewerAgentId, pr, config, opts = {}) {
|
|
|
3402
3445
|
function updateMetrics(agentId, dispatchItem, result, taskUsage, prsCreatedCount, model) {
|
|
3403
3446
|
if (!agentId || agentId.startsWith('temp-') || agentId === 'agent1' || agentId === 'reviewer' || agentId.startsWith('_test')) return;
|
|
3404
3447
|
|
|
3405
|
-
|
|
3406
|
-
mutateJsonFileLocked(metricsPath, metrics => {
|
|
3407
|
-
metrics = metrics || {};
|
|
3448
|
+
shared.mutateMetrics(metrics => {
|
|
3408
3449
|
if (!metrics[agentId]) {
|
|
3409
3450
|
metrics[agentId] = { ...DEFAULT_AGENT_METRICS };
|
|
3410
3451
|
}
|
package/engine/pipeline.js
CHANGED
|
@@ -714,12 +714,16 @@ async function executePlanStage(stage, stageState, run, config, pipeline = {}) {
|
|
|
714
714
|
// `<endpoint>: <reason>` so updateRunStage records a terminal failure.
|
|
715
715
|
async function executeApiStage(stage, stageState, run) {
|
|
716
716
|
const calls = stage.calls || [{ endpoint: stage.endpoint, method: stage.method || 'POST', body: stage.body }];
|
|
717
|
+
if (!calls.length) {
|
|
718
|
+
return { status: PIPELINE_STATUS.FAILED, error: 'calls array is empty', completedAt: ts() };
|
|
719
|
+
}
|
|
717
720
|
const maxAttempts = ENGINE_DEFAULTS.pipelineApiRetries;
|
|
718
721
|
const retryDelay = ENGINE_DEFAULTS.pipelineApiRetryDelay;
|
|
719
722
|
const timeoutMs = ENGINE_DEFAULTS.pipelineApiTimeoutMs;
|
|
720
723
|
|
|
721
724
|
for (const call of calls) {
|
|
722
|
-
const
|
|
725
|
+
const port = shared.readDashboardPortFile(MINIONS_DIR)?.port || (process.env.MINIONS_PORT && parseInt(process.env.MINIONS_PORT, 10)) || 7331;
|
|
726
|
+
const url = `http://localhost:${port}${call.endpoint}`;
|
|
723
727
|
const body = typeof call.body === 'string' ? call.body : JSON.stringify(call.body || {});
|
|
724
728
|
|
|
725
729
|
const attemptOnce = (attempt) => new Promise((resolve) => {
|
package/engine/queries.js
CHANGED
|
@@ -1684,35 +1684,53 @@ async function _scanKnowledgeBase() {
|
|
|
1684
1684
|
// ~30 large KB entries. `_flat()` materialises a fresh flat string via
|
|
1685
1685
|
// Buffer round-trip so the cached entry no longer pins the parent file.
|
|
1686
1686
|
const _flat = (s) => Buffer.from(String(s || ''), 'utf8').toString('utf8');
|
|
1687
|
-
|
|
1687
|
+
// Build the full (category, file) work list first, THEN process it with a
|
|
1688
|
+
// bounded concurrency window. The KB tree holds thousands of files; the old
|
|
1689
|
+
// unbounded `Promise.all(files.map(...))` per category fired thousands of
|
|
1690
|
+
// concurrent readFile+stat ops at once, flooding the (default-4, now 16)
|
|
1691
|
+
// libuv thread pool and starving every other fs-dependent dashboard request
|
|
1692
|
+
// — including the /api/status poll — for tens of seconds, which is what
|
|
1693
|
+
// tripped the "stale / unreachable" banner. Capping in-flight reads keeps
|
|
1694
|
+
// the scan from monopolizing the pool. (Load-reduction audit 2026-06-24.)
|
|
1695
|
+
const work = [];
|
|
1688
1696
|
for (const cat of KB_CATEGORIES) {
|
|
1689
1697
|
const catDir = path.join(KNOWLEDGE_DIR, cat);
|
|
1690
1698
|
const files = (await fsp.readdir(catDir).catch(() => [])).filter(f => f.endsWith('.md'));
|
|
1691
|
-
const
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
}
|
|
1699
|
+
for (const f of files) work.push({ cat, catDir, f });
|
|
1700
|
+
}
|
|
1701
|
+
const scanOne = async ({ cat, catDir, f }) => {
|
|
1702
|
+
const filePath = path.join(catDir, f);
|
|
1703
|
+
const [content, stat] = await Promise.all([
|
|
1704
|
+
fsp.readFile(filePath, 'utf8').catch(() => ''),
|
|
1705
|
+
fsp.stat(filePath).catch(() => null),
|
|
1706
|
+
]);
|
|
1707
|
+
const titleMatch = content.match(/^#\s+(.+)/m);
|
|
1708
|
+
const title = _flat(titleMatch ? titleMatch[1].trim() : f.replace(/\.md$/, ''));
|
|
1709
|
+
const agentMatch = f.match(/^\d{4}-\d{2}-\d{2}-(\w+)-/);
|
|
1710
|
+
const dateMatch = f.match(/^(\d{4}-\d{2}-\d{2})/) || content.match(/^date:\s*(\d{4}-\d{2}-\d{2})$/m);
|
|
1711
|
+
const sourceMatch = content.match(/^source:\s*(.+)/m);
|
|
1712
|
+
const sortTs = (stat && stat.mtimeMs) || 0;
|
|
1713
|
+
const displayDate = dateMatch ? _flat(dateMatch[1]) : (sortTs ? new Date(sortTs).toISOString().slice(0, 10) : '');
|
|
1714
|
+
return {
|
|
1715
|
+
cat, file: f, title,
|
|
1716
|
+
agent: agentMatch ? agentMatch[1] : '',
|
|
1717
|
+
date: displayDate,
|
|
1718
|
+
sortTs,
|
|
1719
|
+
source: _flat(sourceMatch ? sourceMatch[1].trim() : ''),
|
|
1720
|
+
preview: _flat(content.slice(0, 200)),
|
|
1721
|
+
size: content.length,
|
|
1722
|
+
};
|
|
1723
|
+
};
|
|
1724
|
+
const entries = [];
|
|
1725
|
+
const KB_SCAN_CONCURRENCY = 16;
|
|
1726
|
+
let _next = 0;
|
|
1727
|
+
const worker = async () => {
|
|
1728
|
+
while (_next < work.length) {
|
|
1729
|
+
const idx = _next++;
|
|
1730
|
+
entries.push(await scanOne(work[idx]));
|
|
1731
|
+
}
|
|
1732
|
+
};
|
|
1733
|
+
await Promise.all(Array.from({ length: Math.min(KB_SCAN_CONCURRENCY, work.length) }, worker));
|
|
1716
1734
|
entries.sort((a, b) =>
|
|
1717
1735
|
(b.sortTs || 0) - (a.sortTs || 0) ||
|
|
1718
1736
|
(b.date || '').localeCompare(a.date || '') ||
|
package/engine/scheduler.js
CHANGED
|
@@ -259,9 +259,9 @@ function parseCronExpr(expr) {
|
|
|
259
259
|
|
|
260
260
|
return {
|
|
261
261
|
matches(date) {
|
|
262
|
-
return minuteMatcher(date.
|
|
263
|
-
hourMatcher(date.
|
|
264
|
-
dowMatcher(date.
|
|
262
|
+
return minuteMatcher(date.getUTCMinutes()) &&
|
|
263
|
+
hourMatcher(date.getUTCHours()) &&
|
|
264
|
+
dowMatcher(date.getUTCDay());
|
|
265
265
|
}
|
|
266
266
|
};
|
|
267
267
|
}
|
|
@@ -298,11 +298,11 @@ function shouldRunNow(schedule, lastRunAt) {
|
|
|
298
298
|
if (lastRunAt) {
|
|
299
299
|
const last = new Date(lastRunAt);
|
|
300
300
|
if (!isNaN(last.getTime()) &&
|
|
301
|
-
last.
|
|
302
|
-
last.
|
|
303
|
-
last.
|
|
304
|
-
last.
|
|
305
|
-
last.
|
|
301
|
+
last.getUTCFullYear() === now.getUTCFullYear() &&
|
|
302
|
+
last.getUTCMonth() === now.getUTCMonth() &&
|
|
303
|
+
last.getUTCDate() === now.getUTCDate() &&
|
|
304
|
+
last.getUTCHours() === now.getUTCHours() &&
|
|
305
|
+
last.getUTCMinutes() === now.getUTCMinutes()) {
|
|
306
306
|
return false;
|
|
307
307
|
}
|
|
308
308
|
}
|
package/engine/shared.js
CHANGED
|
@@ -3264,6 +3264,12 @@ function resolvePollFlag(engineCfg, granularKey, legacyMacroKey) {
|
|
|
3264
3264
|
if (engineCfg && Object.prototype.hasOwnProperty.call(engineCfg, granularKey)) {
|
|
3265
3265
|
return engineCfg[granularKey] !== false;
|
|
3266
3266
|
}
|
|
3267
|
+
// Legacy-macro path: reachable ONLY for hand-edited configs or pre-P-c4d8e1a3
|
|
3268
|
+
// configs that set the provider-level macro (adoPollEnabled / ghPollEnabled)
|
|
3269
|
+
// to false WITHOUT setting the granular key. The dashboard Settings save
|
|
3270
|
+
// (dashboard/js/settings.js) always persists all seven granular keys, so
|
|
3271
|
+
// UI-saved configs never reach this branch — but operator-edited config.json
|
|
3272
|
+
// files that use the legacy macro still must work. Do NOT delete this branch.
|
|
3267
3273
|
if (legacyMacroKey && engineCfg && engineCfg[legacyMacroKey] === false) {
|
|
3268
3274
|
return false;
|
|
3269
3275
|
}
|
|
@@ -6760,15 +6766,6 @@ function applyPrFieldDelta(target, before, after) {
|
|
|
6760
6766
|
function normalizePrRecord(pr, project = null) {
|
|
6761
6767
|
if (!pr || typeof pr !== 'object') return false;
|
|
6762
6768
|
let changed = false;
|
|
6763
|
-
const hasCanonicalContextOnly = Object.prototype.hasOwnProperty.call(pr, 'contextOnly');
|
|
6764
|
-
if (hasCanonicalContextOnly) {
|
|
6765
|
-
for (const legacyKey of ['_contextOnly', '_autoObserve', '_manual']) {
|
|
6766
|
-
if (Object.prototype.hasOwnProperty.call(pr, legacyKey)) {
|
|
6767
|
-
delete pr[legacyKey];
|
|
6768
|
-
changed = true;
|
|
6769
|
-
}
|
|
6770
|
-
}
|
|
6771
|
-
}
|
|
6772
6769
|
const prNumber = getPrNumber(pr.prNumber ?? pr.id ?? pr.url);
|
|
6773
6770
|
if (prNumber != null && pr.prNumber !== prNumber) {
|
|
6774
6771
|
pr.prNumber = prNumber;
|
|
@@ -6807,15 +6804,13 @@ function normalizePrLinkItems(value) {
|
|
|
6807
6804
|
return [...new Set(items.filter(item => typeof item === 'string' && item))];
|
|
6808
6805
|
}
|
|
6809
6806
|
|
|
6810
|
-
//
|
|
6811
|
-
//
|
|
6812
|
-
//
|
|
6813
|
-
//
|
|
6814
|
-
// contamination while still treating old context-only rows as reference-only.
|
|
6807
|
+
// Canonical `contextOnly` gate — reads only the canonical field.
|
|
6808
|
+
// All records are expected to carry `contextOnly` after the 2026-06-24
|
|
6809
|
+
// on-disk sweep confirmed no live `_contextOnly`/`_autoObserve`/`_manual`
|
|
6810
|
+
// keys remain (and the boot migration has been removed).
|
|
6815
6811
|
function isContextOnlyPrRecord(pr) {
|
|
6816
6812
|
if (!pr || typeof pr !== 'object') return false;
|
|
6817
|
-
|
|
6818
|
-
return pr._contextOnly === true;
|
|
6813
|
+
return pr.contextOnly === true;
|
|
6819
6814
|
}
|
|
6820
6815
|
|
|
6821
6816
|
function isAutoManagedPrRecord(pr) {
|
|
@@ -6971,12 +6966,17 @@ function upsertPullRequestRecord(prPath, entry, { project = null, itemId = null,
|
|
|
6971
6966
|
prNumber: prNumber ?? entry.prNumber ?? null,
|
|
6972
6967
|
prdItems: linkedItemIds,
|
|
6973
6968
|
};
|
|
6974
|
-
|
|
6975
|
-
|
|
6976
|
-
|
|
6977
|
-
|
|
6978
|
-
|
|
6979
|
-
|
|
6969
|
+
// Issue #348: backfill `project` from the project option when the entry
|
|
6970
|
+
// doesn't carry one. The file path already encodes the project name (e.g.
|
|
6971
|
+
// projects/<name>/pull-requests.json), so it is always deterministic.
|
|
6972
|
+
// Without this, PRs linked via /api/pull-requests/link had project:null on
|
|
6973
|
+
// the record, which broke any downstream path that reads pr.project to
|
|
6974
|
+
// resolve the owning project.
|
|
6975
|
+
if (project?.name != null && normalizedEntry.project == null) {
|
|
6976
|
+
normalizedEntry.project = project.name;
|
|
6977
|
+
}
|
|
6978
|
+
if (normalizedEntry.contextOnly != null) {
|
|
6979
|
+
normalizedEntry.contextOnly = normalizedEntry.contextOnly === true;
|
|
6980
6980
|
}
|
|
6981
6981
|
|
|
6982
6982
|
let created = false;
|
|
@@ -6998,34 +6998,18 @@ function upsertPullRequestRecord(prPath, entry, { project = null, itemId = null,
|
|
|
6998
6998
|
} else {
|
|
6999
6999
|
target.id = canonicalId;
|
|
7000
7000
|
if (prNumber != null) target.prNumber = prNumber;
|
|
7001
|
-
const targetWasAutoManaged = isAutoManagedPrRecord(target)
|
|
7002
|
-
|| (target.contextOnly == null && target._contextOnly === true && _prRecordIsLegacyManaged(target));
|
|
7001
|
+
const targetWasAutoManaged = isAutoManagedPrRecord(target);
|
|
7003
7002
|
for (const key of ['url', 'title', 'description', 'agent', 'branch', 'reviewStatus', 'status', 'created', 'sourcePlan', 'itemType']) {
|
|
7004
7003
|
if (normalizedEntry[key] != null && normalizedEntry[key] !== '' && (target[key] == null || target[key] === '')) {
|
|
7005
7004
|
target[key] = normalizedEntry[key];
|
|
7006
7005
|
}
|
|
7007
7006
|
}
|
|
7008
|
-
// W-mq5s5ttx000j7ab8-a — `_manual` and `_autoObserve` are no longer
|
|
7009
|
-
// copied through; the engine reads gate state from the canonical
|
|
7010
|
-
// `contextOnly` field (see isAutoManagedPrRecord above + the boot
|
|
7011
|
-
// migration migratePrGateFlags). `_context`/`_projectResolution` are
|
|
7012
|
-
// unrelated breadcrumbs and stay.
|
|
7013
7007
|
for (const key of ['_context', '_projectResolution']) {
|
|
7014
7008
|
if (normalizedEntry[key] != null) target[key] = normalizedEntry[key];
|
|
7015
7009
|
}
|
|
7016
|
-
|
|
7017
|
-
|
|
7018
|
-
|
|
7019
|
-
// (b)/(c) of the decomposition). Persist as canonical `contextOnly`.
|
|
7020
|
-
const incomingContextOnly = normalizedEntry.contextOnly != null
|
|
7021
|
-
? normalizedEntry.contextOnly
|
|
7022
|
-
: normalizedEntry._contextOnly;
|
|
7023
|
-
if (incomingContextOnly != null) {
|
|
7024
|
-
const wouldDemoteManagedPr = incomingContextOnly === true && targetWasAutoManaged;
|
|
7025
|
-
target.contextOnly = wouldDemoteManagedPr ? false : incomingContextOnly === true;
|
|
7026
|
-
delete target._contextOnly;
|
|
7027
|
-
delete target._autoObserve;
|
|
7028
|
-
delete target._manual;
|
|
7010
|
+
if (normalizedEntry.contextOnly != null) {
|
|
7011
|
+
const wouldDemoteManagedPr = normalizedEntry.contextOnly === true && targetWasAutoManaged;
|
|
7012
|
+
target.contextOnly = wouldDemoteManagedPr ? false : normalizedEntry.contextOnly === true;
|
|
7029
7013
|
}
|
|
7030
7014
|
}
|
|
7031
7015
|
target.prdItems = normalizePrLinkItems(target.prdItems || []);
|
|
@@ -7048,142 +7032,6 @@ function upsertPullRequestRecord(prPath, entry, { project = null, itemId = null,
|
|
|
7048
7032
|
return { id: canonicalId, prNumber, created, linked, skipped, record };
|
|
7049
7033
|
}
|
|
7050
7034
|
|
|
7051
|
-
// ─── PR Gate Migration (W-mq5s5ttx000j7ab8-a) ───────────────────────────────
|
|
7052
|
-
//
|
|
7053
|
-
// One-shot boot migration that projects the legacy gate signals
|
|
7054
|
-
// (`_contextOnly`, `_autoObserve`, `_manual`) onto the canonical
|
|
7055
|
-
// `contextOnly` field on every `projects/<name>/pull-requests.json` record.
|
|
7056
|
-
// Wired from `engine/cli.js#start()` before the first tick fires so the new
|
|
7057
|
-
// `isAutoManagedPrRecord` (which only reads `contextOnly`) returns the same
|
|
7058
|
-
// verdict as the legacy 6-clause helper for pre-existing data.
|
|
7059
|
-
//
|
|
7060
|
-
// Per-record decision:
|
|
7061
|
-
// 1. contextOnly := (_contextOnly === true)
|
|
7062
|
-
// 2. legacyManaged := _autoObserve === true || sourcePlan || itemType ||
|
|
7063
|
-
// (agent is a *configured* Minions persona)
|
|
7064
|
-
// "Configured Minions persona" = `agent` matches an id in
|
|
7065
|
-
// `config.agents` (case-insensitive). The github poller writes the PR
|
|
7066
|
-
// author's login into `agent` (from `prData.user.login`); human logins
|
|
7067
|
-
// like `calebt_microsoft` are kebab/snake-shaped and previously passed a
|
|
7068
|
-
// naive shape regex, mis-classifying a human PR as managed and flipping
|
|
7069
|
-
// its observe toggle ON on the next boot (W-mqil3jtw). Cross-checking the
|
|
7070
|
-
// real roster instead of a shape heuristic fixes that. A login that is
|
|
7071
|
-
// not a configured agent id is treated as a human author.
|
|
7072
|
-
// NOTE: `prdItems.length > 0` is intentionally NOT a managed signal in
|
|
7073
|
-
// this migration. A watch can link a PR context-only AND spin off a
|
|
7074
|
-
// courtesy-review WI whose id is stamped into `prdItems`; that stamp must
|
|
7075
|
-
// not override the explicit context-only intent. Genuine managed PRs
|
|
7076
|
-
// always carry a real signal above (configured agent / sourcePlan /
|
|
7077
|
-
// itemType / _autoObserve), so dropping prdItems-alone is safe here.
|
|
7078
|
-
// 3. If !contextOnly && !legacyManaged, set contextOnly = true and stamp
|
|
7079
|
-
// `_migrationNote: 'auto-set-contextOnly-by-pr-gate-simplification'`.
|
|
7080
|
-
// 4. Persist `contextOnly`; delete `_autoObserve`, `_manual`, `_contextOnly`.
|
|
7081
|
-
//
|
|
7082
|
-
// Idempotent: records that already have `contextOnly` and none of the legacy
|
|
7083
|
-
// keys are skipped (no rewrite, no log line, JSON mtime unchanged).
|
|
7084
|
-
const _PR_GATE_MIGRATION_NOTE = 'auto-set-contextOnly-by-pr-gate-simplification';
|
|
7085
|
-
const _AGENT_PERSONA_RE = /^[a-z0-9_-]+$/;
|
|
7086
|
-
|
|
7087
|
-
function _prRecordHasLegacyGateKey(record) {
|
|
7088
|
-
return Object.prototype.hasOwnProperty.call(record, '_contextOnly')
|
|
7089
|
-
|| Object.prototype.hasOwnProperty.call(record, '_autoObserve')
|
|
7090
|
-
|| Object.prototype.hasOwnProperty.call(record, '_manual');
|
|
7091
|
-
}
|
|
7092
|
-
|
|
7093
|
-
// Resolve the configured agent roster (lower-cased Set) once per migration.
|
|
7094
|
-
// Accepts either an explicit `agentIds` (Set / array) or a `config` object
|
|
7095
|
-
// whose `agents` keys are the roster. Returns an empty Set when nothing is
|
|
7096
|
-
// supplied (no record can match a managed persona).
|
|
7097
|
-
// (Deliberately NOT queries.getAgents(config): that lives in queries.js — which
|
|
7098
|
-
// requires shared.js, so importing it here is circular — and it also reads
|
|
7099
|
-
// dispatch/inbox state. We only need the bare id set.)
|
|
7100
|
-
function _resolveConfiguredAgentIds(opts = {}) {
|
|
7101
|
-
let ids = opts.agentIds;
|
|
7102
|
-
if (ids == null && opts.config && opts.config.agents && typeof opts.config.agents === 'object') {
|
|
7103
|
-
ids = Object.keys(opts.config.agents);
|
|
7104
|
-
}
|
|
7105
|
-
const source = ids instanceof Set ? [...ids] : (Array.isArray(ids) ? ids : []);
|
|
7106
|
-
return new Set(source.map(id => String(id || '').trim().toLowerCase()).filter(Boolean));
|
|
7107
|
-
}
|
|
7108
|
-
|
|
7109
|
-
// `agentIds` (a Set, supplied by the boot migration) switches this helper into
|
|
7110
|
-
// "roster mode": the `agent` field only counts as a managed signal when it is
|
|
7111
|
-
// an actually-configured agent id, and prdItems-alone is dropped (see the
|
|
7112
|
-
// migration header). Callers that can't cheaply resolve the roster (the
|
|
7113
|
-
// runtime upsert/link demotion-guard at the `_prRecordIsLegacyManaged(target)`
|
|
7114
|
-
// call site) pass no agentIds and keep the legacy shape-heuristic behavior.
|
|
7115
|
-
function _prRecordIsLegacyManaged(record, agentIds) {
|
|
7116
|
-
const rosterMode = agentIds instanceof Set;
|
|
7117
|
-
// prdItems-alone is a managed signal only in the legacy runtime path, not the
|
|
7118
|
-
// boot migration (see header: courtesy-review watch contamination).
|
|
7119
|
-
if (!rosterMode && Array.isArray(record.prdItems) && record.prdItems.length > 0) return true;
|
|
7120
|
-
if (record._autoObserve === true) return true;
|
|
7121
|
-
if (record.sourcePlan) return true;
|
|
7122
|
-
if (record.itemType) return true;
|
|
7123
|
-
if (typeof record.agent === 'string') {
|
|
7124
|
-
const agent = record.agent.trim().toLowerCase();
|
|
7125
|
-
if (agent && agent !== 'human') {
|
|
7126
|
-
if (rosterMode ? agentIds.has(agent) : _AGENT_PERSONA_RE.test(agent)) return true;
|
|
7127
|
-
}
|
|
7128
|
-
}
|
|
7129
|
-
return false;
|
|
7130
|
-
}
|
|
7131
|
-
|
|
7132
|
-
function migratePrGateFlags(projectsRoot, opts = {}) {
|
|
7133
|
-
const summary = { projectsScanned: 0, projectsMigrated: 0, totalRecords: 0, totalMigrated: 0 };
|
|
7134
|
-
if (!projectsRoot || typeof projectsRoot !== 'string') return summary;
|
|
7135
|
-
const agentIds = _resolveConfiguredAgentIds(opts);
|
|
7136
|
-
let entries;
|
|
7137
|
-
try {
|
|
7138
|
-
entries = fs.readdirSync(projectsRoot, { withFileTypes: true });
|
|
7139
|
-
} catch {
|
|
7140
|
-
return summary;
|
|
7141
|
-
}
|
|
7142
|
-
for (const entry of entries) {
|
|
7143
|
-
if (!entry.isDirectory()) continue;
|
|
7144
|
-
const projectName = entry.name;
|
|
7145
|
-
const prPath = path.join(projectsRoot, projectName, 'pull-requests.json');
|
|
7146
|
-
if (!fs.existsSync(prPath)) continue;
|
|
7147
|
-
summary.projectsScanned++;
|
|
7148
|
-
|
|
7149
|
-
let migrated = 0;
|
|
7150
|
-
let stamped = 0;
|
|
7151
|
-
let alreadyMigrated = 0;
|
|
7152
|
-
|
|
7153
|
-
mutatePullRequests(prPath, (prs) => {
|
|
7154
|
-
if (!Array.isArray(prs)) return prs;
|
|
7155
|
-
for (const record of prs) {
|
|
7156
|
-
if (!record || typeof record !== 'object') continue;
|
|
7157
|
-
const hasLegacy = _prRecordHasLegacyGateKey(record);
|
|
7158
|
-
const hasCanonical = Object.prototype.hasOwnProperty.call(record, 'contextOnly');
|
|
7159
|
-
if (hasCanonical && !hasLegacy) { alreadyMigrated++; continue; }
|
|
7160
|
-
|
|
7161
|
-
let contextOnly = (record._contextOnly === true);
|
|
7162
|
-
if (!contextOnly && !_prRecordIsLegacyManaged(record, agentIds)) {
|
|
7163
|
-
contextOnly = true;
|
|
7164
|
-
record._migrationNote = _PR_GATE_MIGRATION_NOTE;
|
|
7165
|
-
stamped++;
|
|
7166
|
-
}
|
|
7167
|
-
record.contextOnly = contextOnly;
|
|
7168
|
-
delete record._autoObserve;
|
|
7169
|
-
delete record._manual;
|
|
7170
|
-
delete record._contextOnly;
|
|
7171
|
-
migrated++;
|
|
7172
|
-
}
|
|
7173
|
-
return prs;
|
|
7174
|
-
});
|
|
7175
|
-
|
|
7176
|
-
summary.totalRecords += (migrated + alreadyMigrated);
|
|
7177
|
-
summary.totalMigrated += migrated;
|
|
7178
|
-
if (migrated > 0) {
|
|
7179
|
-
summary.projectsMigrated++;
|
|
7180
|
-
// One line per project that actually moved. Keep idempotent runs silent.
|
|
7181
|
-
console.log(`[pr-gate-migration] ${projectName}: migrated ${migrated} records (${stamped} stamped contextOnly, ${alreadyMigrated} already-migrated)`);
|
|
7182
|
-
}
|
|
7183
|
-
}
|
|
7184
|
-
return summary;
|
|
7185
|
-
}
|
|
7186
|
-
|
|
7187
7035
|
// ─── PR Reference → URL Derivation ───────────────────────────────────────────
|
|
7188
7036
|
//
|
|
7189
7037
|
// W-mq5wfh1v000e0da9 — Given a PR ref (URL, canonical `host:scope#N` id, or
|
|
@@ -9040,7 +8888,6 @@ module.exports = {
|
|
|
9040
8888
|
isContextOnlyPrRecord,
|
|
9041
8889
|
upsertPullRequestRecord,
|
|
9042
8890
|
isAutoManagedPrRecord, // W-mq5s5ttx000j7ab8-a — exported for engine + watch-plugin gate consolidation
|
|
9043
|
-
migratePrGateFlags, // W-mq5s5ttx000j7ab8-a — boot migration wired from engine/cli.js
|
|
9044
8891
|
autoEnrollPrFromFixWorkItem,
|
|
9045
8892
|
deriveUrlForPrRef, // exported for testing
|
|
9046
8893
|
classifyPrRefForVerification, // issue #246 — host routing for loose PR-ref verification
|
package/engine/timeout.js
CHANGED
|
@@ -970,4 +970,6 @@ module.exports = {
|
|
|
970
970
|
// exported for testing
|
|
971
971
|
rememberDeferredSteering, checkDeferredStranded, _runSteeringKillLadder, _collectDescendantPids,
|
|
972
972
|
_resetSteeringStoreCacheForTest, _setSteeringStoreForTest,
|
|
973
|
+
// exported for testing — steering clamp helpers and deferred checkpoint
|
|
974
|
+
_clampSteeringDeferredMaxMs, _clampSteeringMaxKillRetries, _appendLiveOutputLine, deferSteeringUntilCheckpoint,
|
|
973
975
|
};
|
package/engine/watch-actions.js
CHANGED
|
@@ -604,7 +604,7 @@ registerActionType(WATCH_ACTION_TYPE.MINIONS_API, {
|
|
|
604
604
|
if (!_ALLOWED_API_METHODS.has(method)) {
|
|
605
605
|
return { ok: false, summary: `minions-api: unsupported method ${method} (allowed: ${[..._ALLOWED_API_METHODS].join(', ')})` };
|
|
606
606
|
}
|
|
607
|
-
const port =
|
|
607
|
+
const port = shared.readDashboardPortFile(shared.MINIONS_DIR)?.port || 7331;
|
|
608
608
|
const headers = {
|
|
609
609
|
'User-Agent': 'minions-watch/1.0',
|
|
610
610
|
'X-Minions-Internal': '1',
|
|
@@ -933,7 +933,7 @@ registerActionType(WATCH_ACTION_TYPE.CC_TRIAGE, {
|
|
|
933
933
|
|
|
934
934
|
const watchId = String(watch?.id || 'unknown');
|
|
935
935
|
const triggerCount = Number(watch?.triggerCount || 0);
|
|
936
|
-
const port =
|
|
936
|
+
const port = shared.readDashboardPortFile(shared.MINIONS_DIR)?.port || 7331;
|
|
937
937
|
const timeoutMs = Number.isFinite(p.timeoutMs) && p.timeoutMs > 0
|
|
938
938
|
? Math.min(Number(p.timeoutMs), CC_TRIAGE_MAX_TIMEOUT_MS)
|
|
939
939
|
: CC_TRIAGE_DEFAULT_TIMEOUT_MS;
|
package/engine/worktree-gc.js
CHANGED
|
@@ -309,42 +309,56 @@ function _resetStuckPathsForTesting() { _stuckPaths.clear(); }
|
|
|
309
309
|
// `_markStuckSuccess` with the reaped-pid attribution.
|
|
310
310
|
function _postReapRetry(wtPath, gitRoot, parentDir, resolvedPath, _removeWorktree, opts, projStats, result, log, reapedPids) {
|
|
311
311
|
try { shared.clearWorktreeFailureCache(resolvedPath); } catch { /* optional */ }
|
|
312
|
-
// Brief settle window for OS to release file handles from killed processes.
|
|
313
|
-
try {
|
|
314
|
-
const sleepFn = typeof opts.sleepSyncFn === 'function'
|
|
315
|
-
? opts.sleepSyncFn
|
|
316
|
-
: (ms) => {
|
|
317
|
-
try {
|
|
318
|
-
require('child_process').execFileSync(process.execPath, ['-e', `setTimeout(()=>process.exit(0), ${Number(ms) || 0})`], { stdio: 'ignore' });
|
|
319
|
-
} catch { /* timeout / spawn fail — proceed anyway */ }
|
|
320
|
-
};
|
|
321
|
-
sleepFn(2000);
|
|
322
|
-
} catch { /* sleep failures should not block the retry */ }
|
|
323
312
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
313
|
+
// The actual removal attempt, run after the settle window.
|
|
314
|
+
function _doRetry() {
|
|
315
|
+
try {
|
|
316
|
+
const removed = _removeWorktree(wtPath, gitRoot, parentDir);
|
|
317
|
+
if (removed) {
|
|
318
|
+
// Down-count the failure we recorded just before; this dispatch
|
|
319
|
+
// ultimately succeeded after the auto-reap.
|
|
320
|
+
if (projStats) { projStats.failed = Math.max(0, projStats.failed - 1); projStats.evicted++; }
|
|
321
|
+
if (result) { result.failed = Math.max(0, result.failed - 1); result.evicted++; }
|
|
322
|
+
_markStuckSuccess(resolvedPath, {
|
|
323
|
+
writeToInbox: opts.writeToInbox,
|
|
324
|
+
recoveryReason: 'holder-reap',
|
|
325
|
+
reapedPids,
|
|
326
|
+
});
|
|
327
|
+
try { shared.bumpWorktreeGcMetric('recoveredViaHolderReap'); } catch { /* optional */ }
|
|
328
|
+
if (typeof log === 'function') {
|
|
329
|
+
log('info', `worktree-gc: removed ${wtPath} after auto-reaping holder(s) ${reapedPids.join(', ')}`);
|
|
330
|
+
}
|
|
331
|
+
return true;
|
|
332
|
+
}
|
|
333
|
+
} catch (retryErr) {
|
|
337
334
|
if (typeof log === 'function') {
|
|
338
|
-
log('
|
|
335
|
+
log('warn', `worktree-gc: post-reap retry threw for ${wtPath}: ${retryErr && retryErr.message}`);
|
|
339
336
|
}
|
|
340
|
-
return true;
|
|
341
|
-
}
|
|
342
|
-
} catch (retryErr) {
|
|
343
|
-
if (typeof log === 'function') {
|
|
344
|
-
log('warn', `worktree-gc: post-reap retry threw for ${wtPath}: ${retryErr && retryErr.message}`);
|
|
345
337
|
}
|
|
338
|
+
return false;
|
|
346
339
|
}
|
|
347
|
-
|
|
340
|
+
|
|
341
|
+
// Brief settle window for OS to release file handles from killed processes.
|
|
342
|
+
// When sleepSyncFn returns a Promise (or is absent — the production default
|
|
343
|
+
// uses Promise-based setTimeout), the retry fires asynchronously so the
|
|
344
|
+
// event loop is never blocked. When sleepSyncFn is a sync no-op (test
|
|
345
|
+
// injection), the retry fires immediately so test assertions remain sync.
|
|
346
|
+
// The production timer is .unref()'d so it never keeps a test process alive.
|
|
347
|
+
const sleepFn = typeof opts.sleepSyncFn === 'function'
|
|
348
|
+
? opts.sleepSyncFn
|
|
349
|
+
: (ms) => new Promise(r => { const t = setTimeout(r, ms); if (t && typeof t.unref === 'function') t.unref(); });
|
|
350
|
+
|
|
351
|
+
let settled;
|
|
352
|
+
try { settled = sleepFn(2000); } catch { /* sleep init failure — fire retry anyway */ }
|
|
353
|
+
|
|
354
|
+
if (settled && typeof settled.then === 'function') {
|
|
355
|
+
// Async settle window — fire-and-forget; never blocks the event loop.
|
|
356
|
+
settled.then(() => { try { _doRetry(); } catch { /* ignore */ } }).catch(() => {});
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Sync settle window (test injection) — fire retry immediately.
|
|
361
|
+
try { _doRetry(); } catch { /* ignore */ }
|
|
348
362
|
}
|
|
349
363
|
|
|
350
364
|
/**
|
package/engine.js
CHANGED
|
@@ -8615,7 +8615,7 @@ function discoverCentralWorkItems(config) {
|
|
|
8615
8615
|
// Ensure plans directory exists before agent tries to write
|
|
8616
8616
|
if (!fs.existsSync(PLANS_DIR)) fs.mkdirSync(PLANS_DIR, { recursive: true });
|
|
8617
8617
|
const planFileName = `plan-${item.id.toLowerCase()}-${dateStamp()}.md`;
|
|
8618
|
-
vars.plan_content = item.title + (item.description ? '\n\n' + item.description : '');
|
|
8618
|
+
vars.plan_content = shared.redactPromptDescription(item.title + (item.description ? '\n\n' + item.description : ''));
|
|
8619
8619
|
vars.plan_title = item.title;
|
|
8620
8620
|
vars.plan_file = planFileName;
|
|
8621
8621
|
vars.task_description = item.title;
|
|
@@ -8642,7 +8642,7 @@ function discoverCentralWorkItems(config) {
|
|
|
8642
8642
|
vars.plan_content = planFileContent;
|
|
8643
8643
|
} else {
|
|
8644
8644
|
if (planReadError) log('warn', `plan-to-prd: could not read plan file ${item.planFile} for ${item.id}: ${planReadError.message}`);
|
|
8645
|
-
vars.plan_content = item.description || '';
|
|
8645
|
+
vars.plan_content = shared.redactPromptDescription(item.description || '');
|
|
8646
8646
|
}
|
|
8647
8647
|
vars.plan_summary = (item.title || item.planFile).substring(0, 80);
|
|
8648
8648
|
vars.plan_file = item.planFile || '';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2251",
|
|
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"
|