@yemi33/minions 0.1.2380 → 0.1.2382
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 +41 -2
- package/dashboard/js/refresh.js +9 -3
- package/dashboard/js/render-other.js +81 -23
- package/dashboard/js/render-work-items.js +61 -20
- package/dashboard/js/settings.js +47 -25
- package/dashboard/pages/engine.html +6 -0
- package/dashboard.js +126 -62
- package/docs/README.md +1 -0
- package/docs/claude-md-propagation.md +118 -0
- package/docs/engine-restart.md +10 -0
- package/docs/live-checkout-mode.md +45 -26
- package/docs/worktree-lifecycle.md +9 -0
- package/engine/claude-md-context.js +195 -0
- package/engine/consolidation.js +47 -3
- package/engine/db/migrations/026-pre-dispatch-rejections.js +55 -0
- package/engine/dispatch.js +78 -24
- package/engine/lifecycle.js +129 -84
- package/engine/live-checkout.js +193 -149
- package/engine/pipeline.js +147 -20
- package/engine/playbook.js +47 -15
- package/engine/prd-store.js +14 -6
- package/engine/quarantine-refs.js +33 -0
- package/engine/queries.js +8 -6
- package/engine/restart-health.js +69 -4
- package/engine/routing.js +1 -1
- package/engine/runtimes/claude.js +6 -0
- package/engine/runtimes/codex.js +18 -0
- package/engine/runtimes/copilot.js +7 -0
- package/engine/shared.js +199 -106
- package/engine/supervisor.js +72 -16
- package/engine/timeout.js +87 -16
- package/engine/untrusted-fence.js +2 -1
- package/engine.js +301 -31
- package/package.json +1 -1
- package/prompts/cc-system.md +7 -7
package/dashboard.js
CHANGED
|
@@ -421,8 +421,14 @@ function mergeSettingsConfigUpdate(current, candidate, body, patch = {}) {
|
|
|
421
421
|
// `candidate` in handleSettingsUpdate) is silently dropped on the way
|
|
422
422
|
// through mergeSettingsConfigUpdate → mutateDashboardConfig and never
|
|
423
423
|
// reaches disk — the endpoint would return 200 but persist nothing.
|
|
424
|
+
const modeWasRequested = Object.prototype.hasOwnProperty.call(update, 'checkoutMode')
|
|
425
|
+
|| Object.prototype.hasOwnProperty.call(update, 'worktreeMode');
|
|
424
426
|
if (Object.prototype.hasOwnProperty.call(candidateProject, 'checkoutMode')) {
|
|
425
427
|
currentProject.checkoutMode = candidateProject.checkoutMode;
|
|
428
|
+
} else if (Object.prototype.hasOwnProperty.call(candidateProject, 'worktreeMode')) {
|
|
429
|
+
currentProject.checkoutMode = shared.resolveCheckoutMode(candidateProject);
|
|
430
|
+
} else if (!modeWasRequested && Object.prototype.hasOwnProperty.call(currentProject, 'worktreeMode')) {
|
|
431
|
+
currentProject.checkoutMode = shared.resolveCheckoutMode(currentProject);
|
|
426
432
|
} else {
|
|
427
433
|
delete currentProject.checkoutMode;
|
|
428
434
|
}
|
|
@@ -832,7 +838,7 @@ async function copyWorkItemPrFields(item, input, pr = null, opts = {}) {
|
|
|
832
838
|
if (structuredRef) {
|
|
833
839
|
prRef = structuredRef; // explicit operator intent — trusted, no verification
|
|
834
840
|
} else {
|
|
835
|
-
if (String(item?.type || '').toLowerCase()
|
|
841
|
+
if (!shared.isFixLikeWorkType(String(item?.type || '').toLowerCase())) return;
|
|
836
842
|
const looseRef = getWorkItemPrRef(input);
|
|
837
843
|
if (!looseRef) return;
|
|
838
844
|
// issue #246 — confirm the loose ref is a PR (not an issue) before stamping.
|
|
@@ -1649,13 +1655,23 @@ const PLANS_DIR = path.join(MINIONS_DIR, 'plans');
|
|
|
1649
1655
|
// W-mpetru8a000s123a — /api/plans cache. Dashboard auto-polls every 4s; the
|
|
1650
1656
|
// pre-cache cold path walked 4 directories (plans/, prd/, archive variants),
|
|
1651
1657
|
// sync-stat'd every file, and parsed/regex-scanned each .md → 2-3s blocking.
|
|
1652
|
-
//
|
|
1653
|
-
// call invalidatePlansCache() for immediate visibility
|
|
1658
|
+
// 30s TTL keeps the full markdown/SQL rollup off the 4s browser poll cadence.
|
|
1659
|
+
// Mutation handlers call invalidatePlansCache() for immediate visibility;
|
|
1660
|
+
// out-of-process edits surface within 30s.
|
|
1654
1661
|
let _plansCache = null;
|
|
1655
1662
|
let _plansCacheTs = 0;
|
|
1656
|
-
const PLANS_CACHE_TTL_MS =
|
|
1663
|
+
const PLANS_CACHE_TTL_MS = 30000;
|
|
1657
1664
|
function invalidatePlansCache() { _plansCache = null; _plansCacheTs = 0; }
|
|
1658
1665
|
|
|
1666
|
+
async function _mapInBatches(items, batchSize, mapper) {
|
|
1667
|
+
const result = [];
|
|
1668
|
+
for (let start = 0; start < items.length; start += batchSize) {
|
|
1669
|
+
result.push(...await Promise.all(items.slice(start, start + batchSize).map(mapper)));
|
|
1670
|
+
if (start + batchSize < items.length) await _yieldEventLoop();
|
|
1671
|
+
}
|
|
1672
|
+
return result;
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1659
1675
|
function statePathExists(filePath) {
|
|
1660
1676
|
return fs.existsSync(filePath);
|
|
1661
1677
|
}
|
|
@@ -2538,26 +2554,7 @@ function _buildStatusSlowState() {
|
|
|
2538
2554
|
...status,
|
|
2539
2555
|
mainBranch,
|
|
2540
2556
|
branchMismatch,
|
|
2541
|
-
|
|
2542
|
-
// mode so the Projects view can render a "Worktrees" vs "Live checkout"
|
|
2543
|
-
// pill. resolveCheckoutMode honors the legacy worktreeMode field and
|
|
2544
|
-
// defaults to 'worktree' when unset (matches engine spawn behavior).
|
|
2545
|
-
checkoutMode: shared.resolveCheckoutMode(p),
|
|
2546
|
-
// Surface the hybrid live-validation type (when configured) so the
|
|
2547
|
-
// Projects view can render a distinct "Hybrid" pill: coding WIs author
|
|
2548
|
-
// in isolated worktrees while only the named validation type runs
|
|
2549
|
-
// in-place on the live checkout. Only meaningful when checkoutMode is
|
|
2550
|
-
// 'live'; null otherwise (full-live or worktree projects).
|
|
2551
|
-
liveValidationType: (shared.resolveCheckoutMode(p) === 'live' && p.liveValidation && p.liveValidation.type)
|
|
2552
|
-
? p.liveValidation.type
|
|
2553
|
-
: null,
|
|
2554
|
-
// W-mr2q361a00097e5c — surface liveValidation.autoDispatch alongside
|
|
2555
|
-
// .type so the hybrid picker can preselect the "auto-validate"
|
|
2556
|
-
// checkbox and the pill can visibly distinguish manual vs
|
|
2557
|
-
// auto-validating hybrid projects. Only meaningful when checkoutMode
|
|
2558
|
-
// is 'live'; false otherwise (matches the implicit today-default of
|
|
2559
|
-
// autoDispatch being absent/false).
|
|
2560
|
-
liveValidationAutoDispatch: !!(shared.resolveCheckoutMode(p) === 'live' && p.liveValidation && p.liveValidation.autoDispatch === true),
|
|
2557
|
+
..._projectCheckoutConfigView(p),
|
|
2561
2558
|
};
|
|
2562
2559
|
}),
|
|
2563
2560
|
autoMode: {
|
|
@@ -2974,15 +2971,35 @@ async function _freshnessEntryMtimeMs(fp, depth) {
|
|
|
2974
2971
|
return max;
|
|
2975
2972
|
}
|
|
2976
2973
|
|
|
2974
|
+
const FRESHNESS_MTIME_CACHE_TTL_MS = 1000;
|
|
2975
|
+
const _freshnessMtimeCache = new Map();
|
|
2976
|
+
|
|
2977
2977
|
async function _maxInputMtimeMs(inputs) {
|
|
2978
|
+
const key = inputs.map(fp => path.resolve(fp)).sort().join('\0');
|
|
2979
|
+
const now = Date.now();
|
|
2980
|
+
const cached = _freshnessMtimeCache.get(key);
|
|
2981
|
+
if (cached && now - cached.createdAt < FRESHNESS_MTIME_CACHE_TTL_MS) {
|
|
2982
|
+
return cached.promise;
|
|
2983
|
+
}
|
|
2978
2984
|
// Directory mtime only advances on add/remove, not in-place edits. Walk two
|
|
2979
2985
|
// levels so prd/<plan>.json and agents/<id>/live-output.log both bust ETags.
|
|
2980
2986
|
// Deeper steering edits still rely on add/remove cadence, matching the prior
|
|
2981
2987
|
// bounded walk.
|
|
2982
|
-
const
|
|
2988
|
+
const promise = Promise.all(
|
|
2983
2989
|
inputs.map(fp => _freshnessEntryMtimeMs(fp, 2)),
|
|
2984
|
-
);
|
|
2985
|
-
|
|
2990
|
+
).then(mtimes => Math.floor(mtimes.reduce((max, mtime) => Math.max(max, mtime), 0)));
|
|
2991
|
+
_freshnessMtimeCache.set(key, { createdAt: now, promise });
|
|
2992
|
+
if (_freshnessMtimeCache.size > 100) {
|
|
2993
|
+
for (const [cacheKey, entry] of _freshnessMtimeCache) {
|
|
2994
|
+
if (now - entry.createdAt >= FRESHNESS_MTIME_CACHE_TTL_MS) _freshnessMtimeCache.delete(cacheKey);
|
|
2995
|
+
}
|
|
2996
|
+
}
|
|
2997
|
+
try {
|
|
2998
|
+
return await promise;
|
|
2999
|
+
} catch (err) {
|
|
3000
|
+
if (_freshnessMtimeCache.get(key)?.promise === promise) _freshnessMtimeCache.delete(key);
|
|
3001
|
+
throw err;
|
|
3002
|
+
}
|
|
2986
3003
|
}
|
|
2987
3004
|
async function serveFreshJson(req, res, opts) {
|
|
2988
3005
|
const inputs = Array.isArray(opts && opts.inputs) ? opts.inputs : [];
|
|
@@ -3174,7 +3191,20 @@ function handleStateRead(req, res) {
|
|
|
3174
3191
|
res.setHeader('Content-Type', _stateReadContentType(path.extname(resolved)));
|
|
3175
3192
|
try {
|
|
3176
3193
|
const stream = fs.createReadStream(resolved);
|
|
3194
|
+
// HANDLE-LEAK GUARD (#858): destroy the read stream if the client aborts or
|
|
3195
|
+
// the connection closes before the pipe drains. Without this, an aborted
|
|
3196
|
+
// GET /state/notes.md leaks the open file handle indefinitely (confirmed
|
|
3197
|
+
// via handle64: dashboard held a handle on notes.md for 2+ hours). On
|
|
3198
|
+
// Windows that lingering handle makes the engine's consolidation
|
|
3199
|
+
// renameSync throw EPERM, which used to crash-loop the whole daemon.
|
|
3200
|
+
const destroyStream = () => { if (!stream.destroyed) stream.destroy(); };
|
|
3201
|
+
if (typeof res.on === 'function') res.on('close', destroyStream);
|
|
3202
|
+
if (typeof req.on === 'function') {
|
|
3203
|
+
req.on('close', destroyStream);
|
|
3204
|
+
req.on('aborted', destroyStream);
|
|
3205
|
+
}
|
|
3177
3206
|
stream.on('error', (err) => {
|
|
3207
|
+
destroyStream();
|
|
3178
3208
|
if (!res.headersSent) {
|
|
3179
3209
|
res.statusCode = 500;
|
|
3180
3210
|
res.setHeader('Content-Type', 'application/json');
|
|
@@ -4113,20 +4143,40 @@ function _resetPreambleCache() {
|
|
|
4113
4143
|
_preambleCacheTs = 0;
|
|
4114
4144
|
}
|
|
4115
4145
|
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4146
|
+
function _projectLiveValidationTypeValue(p) {
|
|
4147
|
+
const types = shared.resolveLiveValidationTypes(p);
|
|
4148
|
+
if (types.length === 0) return null;
|
|
4149
|
+
return Array.isArray(p?.liveValidation?.type) ? types : types[0];
|
|
4150
|
+
}
|
|
4151
|
+
|
|
4152
|
+
function _projectCheckoutConfigView(p) {
|
|
4153
|
+
const checkoutMode = shared.resolveCheckoutMode(p);
|
|
4154
|
+
return {
|
|
4155
|
+
checkoutMode,
|
|
4156
|
+
liveValidationType: checkoutMode === 'live' ? _projectLiveValidationTypeValue(p) : null,
|
|
4157
|
+
liveValidationInvalid: checkoutMode === 'live' && shared.isLiveValidationConfigInvalid(p),
|
|
4158
|
+
liveValidationAutoDispatch: !!shared.resolveLiveValidationAutoDispatchType(p),
|
|
4159
|
+
};
|
|
4160
|
+
}
|
|
4161
|
+
|
|
4162
|
+
// Compact checkout-mode label for Command Center state surfaces.
|
|
4121
4163
|
function _projectCheckoutModeLabel(p) {
|
|
4122
|
-
const
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4164
|
+
const {
|
|
4165
|
+
checkoutMode,
|
|
4166
|
+
liveValidationType,
|
|
4167
|
+
liveValidationInvalid,
|
|
4168
|
+
} = _projectCheckoutConfigView(p);
|
|
4169
|
+
if (liveValidationInvalid) {
|
|
4170
|
+
const effectiveTypes = liveValidationType
|
|
4171
|
+
? (Array.isArray(liveValidationType) ? liveValidationType : [liveValidationType])
|
|
4172
|
+
: [];
|
|
4173
|
+
return `hybrid (invalid liveValidation; effective live types: ${effectiveTypes.join(', ') || 'none'})`;
|
|
4174
|
+
}
|
|
4175
|
+
if (checkoutMode === 'live' && liveValidationType) {
|
|
4176
|
+
const types = Array.isArray(liveValidationType) ? liveValidationType : [liveValidationType];
|
|
4127
4177
|
return `hybrid (live-validation: ${types.join(', ')})`;
|
|
4128
4178
|
}
|
|
4129
|
-
return
|
|
4179
|
+
return checkoutMode;
|
|
4130
4180
|
}
|
|
4131
4181
|
|
|
4132
4182
|
function buildCCStatePreamble() {
|
|
@@ -4299,13 +4349,13 @@ function getWorkItemPrRef(input) {
|
|
|
4299
4349
|
// afford to be loose because gating would still require explicit
|
|
4300
4350
|
// operator intent.
|
|
4301
4351
|
//
|
|
4302
|
-
// FIX-
|
|
4352
|
+
// FIX-LIKE GATE on the stamp path (W-mqbaby2a000pa8ee): in practice
|
|
4303
4353
|
// the asymmetry leaked — the loose stamp writes to `item.pr_id` (a
|
|
4304
4354
|
// canonical structured field), and the strict gate later reads
|
|
4305
4355
|
// `item.pr_id` and sees the loose stamp AS IF it were structured
|
|
4306
4356
|
// intent. To stop that leak without changing the loose detector
|
|
4307
4357
|
// (which has other legitimate callers), `copyWorkItemPrFields` now
|
|
4308
|
-
// gates the loose result
|
|
4358
|
+
// gates the loose result through `isFixLikeWorkType`. Other WIs only
|
|
4309
4359
|
// get stamped from structured fields (incl. references/follow-up).
|
|
4310
4360
|
// See the comment block on `copyWorkItemPrFields` above for details.
|
|
4311
4361
|
return shared.extractWorkItemPrRef(input);
|
|
@@ -7681,7 +7731,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
7681
7731
|
{ dir: path.join(PLANS_DIR, 'archive'), archived: true },
|
|
7682
7732
|
]) {
|
|
7683
7733
|
const allFiles = (await fsp.readdir(dir).catch(() => [])).filter(f => f.endsWith('.md'));
|
|
7684
|
-
const dirResults = await
|
|
7734
|
+
const dirResults = await _mapInBatches(allFiles, 25, async f => {
|
|
7685
7735
|
const filePath = path.join(dir, f);
|
|
7686
7736
|
const [content, stat] = await Promise.all([
|
|
7687
7737
|
fsp.readFile(filePath, 'utf8').catch(() => ''),
|
|
@@ -7718,10 +7768,20 @@ const server = http.createServer(async (req, res) => {
|
|
|
7718
7768
|
revisionFeedback: null,
|
|
7719
7769
|
version: versionMatch ? parseInt(versionMatch[1]) : null,
|
|
7720
7770
|
};
|
|
7721
|
-
})
|
|
7771
|
+
});
|
|
7722
7772
|
for (const r of dirResults) if (r) plans.push(r);
|
|
7723
7773
|
}
|
|
7724
7774
|
|
|
7775
|
+
const sourcePlanMtimes = new Map();
|
|
7776
|
+
const sourcePlanNames = [...new Set(prdRows
|
|
7777
|
+
.filter(row => !row.archived && row.plan?.source_plan)
|
|
7778
|
+
.map(row => row.plan.source_plan))];
|
|
7779
|
+
const sourceStats = await _mapInBatches(sourcePlanNames, 25, async sourcePlan => {
|
|
7780
|
+
const stat = await fsp.stat(path.join(PLANS_DIR, sourcePlan)).catch(() => null);
|
|
7781
|
+
return [sourcePlan, stat ? Math.floor(stat.mtimeMs) : null];
|
|
7782
|
+
});
|
|
7783
|
+
for (const [sourcePlan, mtimeMs] of sourceStats) sourcePlanMtimes.set(sourcePlan, mtimeMs);
|
|
7784
|
+
|
|
7725
7785
|
// --- PRD JSON records (prd/*.json + prd/archive/*.json, SQL-backed) ---
|
|
7726
7786
|
for (const row of prdRows) {
|
|
7727
7787
|
const plan = row.plan;
|
|
@@ -7735,11 +7795,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
7735
7795
|
// so the Plans tab does not silently mirror a stale-but-cached false.
|
|
7736
7796
|
let freshStale = false;
|
|
7737
7797
|
if (!archived && plan.source_plan) {
|
|
7738
|
-
|
|
7739
|
-
|
|
7740
|
-
|
|
7741
|
-
if (recorded && sourceMtime > recorded) freshStale = true;
|
|
7742
|
-
} catch { /* source plan may have been deleted/renamed */ }
|
|
7798
|
+
const sourceMtime = sourcePlanMtimes.get(plan.source_plan);
|
|
7799
|
+
const recorded = plan.sourcePlanModifiedAt ? new Date(plan.sourcePlanModifiedAt).getTime() : null;
|
|
7800
|
+
if (recorded && sourceMtime != null && sourceMtime > recorded) freshStale = true;
|
|
7743
7801
|
}
|
|
7744
7802
|
// P-e8d49105 — _projects rollup feeds the plan-card multi-badge
|
|
7745
7803
|
// path in dashboard/js/render-plans.js. For PRD JSONs, walk
|
|
@@ -10835,19 +10893,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
10835
10893
|
name: p.name,
|
|
10836
10894
|
localPath: p.localPath || null,
|
|
10837
10895
|
mainBranch: p.mainBranch || null,
|
|
10838
|
-
|
|
10839
|
-
// the per-project dropdown. resolveCheckoutMode honors the legacy
|
|
10840
|
-
// worktreeMode field; 'worktree' (default) or 'live'.
|
|
10841
|
-
checkoutMode: shared.resolveCheckoutMode(p),
|
|
10842
|
-
// W-mr9mx93d000x1dda — surface the hybrid liveValidation config
|
|
10843
|
-
// (type(s) + autoDispatch) so the Settings → Projects checkout-mode
|
|
10844
|
-
// control can pre-fill its Hybrid sub-panel authoritatively (mirrors
|
|
10845
|
-
// the /api/status project mapping the Projects-bar picker reads).
|
|
10846
|
-
// Only meaningful under checkoutMode:'live'; null/false otherwise.
|
|
10847
|
-
liveValidationType: (shared.resolveCheckoutMode(p) === 'live' && p.liveValidation && p.liveValidation.type)
|
|
10848
|
-
? p.liveValidation.type
|
|
10849
|
-
: null,
|
|
10850
|
-
liveValidationAutoDispatch: !!(shared.resolveCheckoutMode(p) === 'live' && p.liveValidation && p.liveValidation.autoDispatch === true),
|
|
10896
|
+
..._projectCheckoutConfigView(p),
|
|
10851
10897
|
// W-mqtvnnj1000357fa — surface the RAW per-project auto-stash override
|
|
10852
10898
|
// (true / false / undefined) so the Settings UI can pre-select the
|
|
10853
10899
|
// tri-state dropdown. Undefined means "use fleet default"
|
|
@@ -11767,6 +11813,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
11767
11813
|
age_minutes: ageMinutes,
|
|
11768
11814
|
};
|
|
11769
11815
|
}
|
|
11816
|
+
|
|
11770
11817
|
return {
|
|
11771
11818
|
agentId: rec.agentId,
|
|
11772
11819
|
valid: true,
|
|
@@ -11790,6 +11837,18 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
11790
11837
|
}
|
|
11791
11838
|
}
|
|
11792
11839
|
|
|
11840
|
+
async function handleWorktreeQuarantineRefs(req, res) {
|
|
11841
|
+
try {
|
|
11842
|
+
const { listQuarantineRefs } = require('./engine/quarantine-refs');
|
|
11843
|
+
return jsonReply(res, 200, {
|
|
11844
|
+
items: await listQuarantineRefs(PROJECTS),
|
|
11845
|
+
generatedAt: new Date().toISOString(),
|
|
11846
|
+
}, req);
|
|
11847
|
+
} catch (e) {
|
|
11848
|
+
return jsonReply(res, 500, { error: e.message }, req);
|
|
11849
|
+
}
|
|
11850
|
+
}
|
|
11851
|
+
|
|
11793
11852
|
async function handleKeepProcessesKill(req, res) {
|
|
11794
11853
|
try {
|
|
11795
11854
|
const body = await readBody(req);
|
|
@@ -13184,6 +13243,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
13184
13243
|
}},
|
|
13185
13244
|
{ method: 'GET', path: '/api/health', desc: 'Lightweight health check for monitoring', handler: handleHealth },
|
|
13186
13245
|
{ method: 'GET', path: '/api/keep-processes', desc: 'List all active agents/<id>/keep-pids.json entries (W-mp68q6ke0010de68)', handler: handleKeepProcessesList },
|
|
13246
|
+
{ method: 'GET', path: '/api/worktree-quarantine-refs', desc: 'List recoverable refs/minions/quarantine/* and quarantine-wip/* refs across configured projects', handler: handleWorktreeQuarantineRefs },
|
|
13187
13247
|
{ method: 'POST', path: '/api/keep-processes/kill', desc: 'Kill a single kept PID and remove it from the agent\'s keep-pids.json', params: 'agentId, pid', handler: handleKeepProcessesKill },
|
|
13188
13248
|
// P-4b8d2e57 (managed-spawn item 4): engine-managed long-running services.
|
|
13189
13249
|
// List/by-name return ETag + honor If-None-Match → 304. Kill removes from
|
|
@@ -14772,7 +14832,8 @@ if (require.main === module) {
|
|
|
14772
14832
|
// supervisor) discovers it instead of guessing 7331. The original
|
|
14773
14833
|
// server.on('error') one-shot exit is replaced by the retry loop — only
|
|
14774
14834
|
// non-EADDRINUSE errors abort.
|
|
14775
|
-
const
|
|
14835
|
+
const requireRequestedPort = process.env.MINIONS_REQUIRE_DASHBOARD_PORT === '1';
|
|
14836
|
+
const maxScan = requireRequestedPort ? 1 : (shared.DASHBOARD_PORT_SCAN_MAX || 100);
|
|
14776
14837
|
let listenErr = null;
|
|
14777
14838
|
for (let attempt = 0; attempt < maxScan; attempt++) {
|
|
14778
14839
|
const tryPort = REQUESTED_PORT + attempt;
|
|
@@ -14798,7 +14859,10 @@ if (require.main === module) {
|
|
|
14798
14859
|
}
|
|
14799
14860
|
if (listenErr) {
|
|
14800
14861
|
if (listenErr.code === 'EADDRINUSE') {
|
|
14801
|
-
|
|
14862
|
+
const range = requireRequestedPort
|
|
14863
|
+
? String(REQUESTED_PORT)
|
|
14864
|
+
: `[${REQUESTED_PORT}..${REQUESTED_PORT + maxScan - 1}]`;
|
|
14865
|
+
console.error(`\n No free dashboard port at ${range}. Free it and retry.\n`);
|
|
14802
14866
|
} else {
|
|
14803
14867
|
console.error(listenErr);
|
|
14804
14868
|
}
|
package/docs/README.md
CHANGED
|
@@ -29,6 +29,7 @@ Hands-on stories and distribution guides for people running or evaluating Minion
|
|
|
29
29
|
Architecture, design proposals, and lifecycle references for people working on the engine, dashboard, or playbooks.
|
|
30
30
|
|
|
31
31
|
- [branch-derivation.md](branch-derivation.md) — Engine-side branch fallback (`work/<wi-id>`) vs. agent-authored long form, the structured-vs-loose PR-pointer extractors, and the canonical PR-fix duplication incident.
|
|
32
|
+
- [claude-md-propagation.md](claude-md-propagation.md) — Propagating repo-authored `CLAUDE.md` instructions to runtimes that don't auto-load it (Copilot/Codex): the `claudeMdNativeDiscovery` capability flag, the bounded nearest-applicable walk-up discovery, the `propagateClaudeMdForNonClaudeRuntimes` config knob, and why it's orthogonal to each runtime's native `AGENTS.md` discovery.
|
|
32
33
|
- [command-center.md](command-center.md) — Command Center (CC) chat panel: configured-runtime sessions, resume semantics, system-prompt invalidation, and per-tab session storage.
|
|
33
34
|
- [specs/agent-rename.md](specs/agent-rename.md) — Design spec for **agent rename & name decoupling** — the prerequisite for the Role/Agent Library. Formalizes the stable opaque agent id, makes charters name-agnostic, and adds `POST /api/agents/rename` (display name + emoji). Feature-flagged (`agent-rename`) and fully backward-compatible.
|
|
34
35
|
- [specs/agent-configurability.md](specs/agent-configurability.md) — Design spec for the user-configurable Role / Agent Library, feature-flagged behind `agent-library` and fully backward-compatible. Builds on `agent-rename.md`. Role = the reusable durable charter; agent = a thin instance (identity + variable expertise + role pointer). Phase 0 renames `skills`→`expertise`; Phase 1 exposes/edits role charters + per-agent expertise; Phase 2 adds role/agent CRUD with builtin delete-protection.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# CLAUDE.md propagation to non-Claude runtimes
|
|
2
|
+
|
|
3
|
+
**Work item:** W-mrdon0pe000l045a
|
|
4
|
+
**Config flag:** `engine.propagateClaudeMdForNonClaudeRuntimes` (default `true`)
|
|
5
|
+
**Per-project override:** `project.propagateClaudeMdForNonClaudeRuntimes`
|
|
6
|
+
**Module:** `engine/claude-md-context.js` · **Injection:** `engine/playbook.js`
|
|
7
|
+
|
|
8
|
+
## Problem
|
|
9
|
+
|
|
10
|
+
`CLAUDE.md` auto-discovery is a **Claude-Code-CLI-only** convention — the `claude`
|
|
11
|
+
binary reads repo-authored `CLAUDE.md` files itself at startup (unless `--bare`).
|
|
12
|
+
The **Copilot** and **Codex** runtimes have **no equivalent auto-load** for
|
|
13
|
+
`CLAUDE.md` at all. So repo-authored `CLAUDE.md` files — which frequently carry
|
|
14
|
+
load-bearing "must be kept in sync" instructions and name blessed tooling — are
|
|
15
|
+
structurally invisible to a Copilot/Codex-based fleet.
|
|
16
|
+
|
|
17
|
+
### Motivating incident
|
|
18
|
+
|
|
19
|
+
`W-mrdkv6zc00071fef` (a `fluidclientframework.android` dependency bump in
|
|
20
|
+
`office/src`) missed a **third** sync-point file (`settings.gradle.kts:116`) that
|
|
21
|
+
`loop/app/android/android/CLAUDE.md` explicitly warns about, and that
|
|
22
|
+
`updateDeps.bat` (named in that same `CLAUDE.md`) would have caught via its build
|
|
23
|
+
verification step. Neither the implementer nor the reviewer ever saw that file's
|
|
24
|
+
content because Copilot has no `CLAUDE.md` auto-load mechanism. Propagating
|
|
25
|
+
`CLAUDE.md` content directly addresses recurrence of this bug class.
|
|
26
|
+
|
|
27
|
+
## Relationship to Copilot's native `AGENTS.md` discovery (NOT the same gap)
|
|
28
|
+
|
|
29
|
+
Copilot has its **own native `AGENTS.md` auto-load**, which (post-#817, "Delegate
|
|
30
|
+
repository harness discovery to native runtimes") Minions leaves to the runtime
|
|
31
|
+
CLI itself rather than suppressing or synthesizing.
|
|
32
|
+
|
|
33
|
+
`CLAUDE.md` propagation is **independent and additive**:
|
|
34
|
+
|
|
35
|
+
- `CLAUDE.md` carries **zero split-brain risk** for Copilot/Codex, because those
|
|
36
|
+
runtimes **never read it themselves** — there is nothing to conflict with.
|
|
37
|
+
- Therefore propagation is **orthogonal** to Copilot's native `AGENTS.md`
|
|
38
|
+
discovery: whether or not the runtime loads its own `AGENTS.md` has **no
|
|
39
|
+
effect** on `CLAUDE.md` propagation.
|
|
40
|
+
|
|
41
|
+
| Concern | Owner | What governs it |
|
|
42
|
+
|---------|-------|-----------------|
|
|
43
|
+
| Copilot's OWN `AGENTS.md` auto-load | the Copilot CLI (native) | delegated to the runtime; Minions does not suppress or synthesize it |
|
|
44
|
+
| Repo `CLAUDE.md` invisible to non-Claude runtimes | `propagateClaudeMdForNonClaudeRuntimes` | injects a "Project instructions (CLAUDE.md)" prompt layer |
|
|
45
|
+
|
|
46
|
+
## Capability flag
|
|
47
|
+
|
|
48
|
+
Each runtime adapter (`engine/runtimes/<name>.js`) declares
|
|
49
|
+
`capabilities.claudeMdNativeDiscovery`:
|
|
50
|
+
|
|
51
|
+
| Runtime | `claudeMdNativeDiscovery` | Propagation |
|
|
52
|
+
|---------|---------------------------|-------------|
|
|
53
|
+
| `claude` | `true` | **skipped** — the Claude CLI already reads CLAUDE.md; re-injecting would double it |
|
|
54
|
+
| `copilot` | `false` | **injected** |
|
|
55
|
+
| `codex` | `false` | **injected** |
|
|
56
|
+
|
|
57
|
+
Unknown runtimes are treated as **not** auto-discovering (propagate), since an
|
|
58
|
+
unrecognized runtime is far more likely to lack native CLAUDE.md loading than to
|
|
59
|
+
have it. Engine code gates on the capability flag, never on `runtime.name`.
|
|
60
|
+
|
|
61
|
+
## Bounded discovery algorithm
|
|
62
|
+
|
|
63
|
+
`office/src` is a huge monorepo — a full tree walk collecting every `CLAUDE.md`
|
|
64
|
+
is not viable. Instead the module mirrors how a human/tool resolves "nearest
|
|
65
|
+
applicable instructions":
|
|
66
|
+
|
|
67
|
+
1. **Collect path hints** for the dispatch (`engine.js#_deriveClaudeMdPathHints`):
|
|
68
|
+
`item.meta.workdir` and `item.references[*].path`.
|
|
69
|
+
2. **Walk UP** from each hint directory toward the **project root**, `stat`-ing
|
|
70
|
+
`CLAUDE.md` at each ancestor level (`discoverClaudeMdFiles`).
|
|
71
|
+
3. **Always include the project-root `CLAUDE.md`** as a fallback — if there are
|
|
72
|
+
no path hints, at minimum the project root is checked (discovery is never
|
|
73
|
+
skipped entirely).
|
|
74
|
+
4. **Nearest-wins ordering:** deepest directory first, project root last, so the
|
|
75
|
+
most specific instructions lead.
|
|
76
|
+
5. **Bounds (cheap by construction):**
|
|
77
|
+
- `MAX_WALK_DEPTH = 24` ancestor levels per hint (deep-path guard),
|
|
78
|
+
- `MAX_FILES = 8` total collected files,
|
|
79
|
+
- path-traversal guard — hints that escape the project root are dropped,
|
|
80
|
+
- the walk **never ascends above the project root**.
|
|
81
|
+
|
|
82
|
+
This only `stat`s `CLAUDE.md` at a small number of ancestor directories, so it
|
|
83
|
+
does not meaningfully slow dispatch even in a monorepo.
|
|
84
|
+
|
|
85
|
+
## Injection & byte cap
|
|
86
|
+
|
|
87
|
+
`engine/playbook.js#renderPlaybook` adds `CLAUDE.md` propagation as a context
|
|
88
|
+
layer alongside the existing `pinned.md` → `notes.md` →
|
|
89
|
+
`knowledge/agents/<agentId>.md` layers:
|
|
90
|
+
|
|
91
|
+
- Header: `## Project instructions (CLAUDE.md)`, with a short provenance note so
|
|
92
|
+
the agent understands why it appears and that "keep in sync" rules are
|
|
93
|
+
authoritative.
|
|
94
|
+
- Content is wrapped in an `<UNTRUSTED-INPUT>` fence (repo-authored, semi-trusted).
|
|
95
|
+
- Each file is labeled by its repo-relative path (`### <relPath>`).
|
|
96
|
+
- The whole block is capped at `ENGINE_DEFAULTS.maxNotesPromptBytes` (the same
|
|
97
|
+
budget the sibling notes / agent-memory layers use), so it can never balloon a
|
|
98
|
+
prompt on a large monorepo. Individual files are truncated with a
|
|
99
|
+
"read the full file if needed" marker when they exceed the remaining budget.
|
|
100
|
+
|
|
101
|
+
Injection only happens when **all** hold:
|
|
102
|
+
|
|
103
|
+
1. the resolved runtime's `claudeMdNativeDiscovery` is falsy (Copilot/Codex),
|
|
104
|
+
2. `resolvePropagateClaudeMdForNonClaudeRuntimes(project, engine)` returns true,
|
|
105
|
+
3. the project has a `localPath`, and
|
|
106
|
+
4. at least one non-empty `CLAUDE.md` was discovered.
|
|
107
|
+
|
|
108
|
+
## Config resolution
|
|
109
|
+
|
|
110
|
+
`shared.resolvePropagateClaudeMdForNonClaudeRuntimes(project, engine)`:
|
|
111
|
+
|
|
112
|
+
1. per-project boolean `project.propagateClaudeMdForNonClaudeRuntimes`, else
|
|
113
|
+
2. fleet-wide boolean `engine.propagateClaudeMdForNonClaudeRuntimes`, else
|
|
114
|
+
3. hardcoded default `true`.
|
|
115
|
+
|
|
116
|
+
Only an explicit boolean counts as "set" at either level (undefined/null/string
|
|
117
|
+
fall through). Editable from **Settings → Copilot Tuning → "Propagate CLAUDE.md
|
|
118
|
+
to non-Claude runtimes"** or via `POST /api/settings`.
|
package/docs/engine-restart.md
CHANGED
|
@@ -70,6 +70,16 @@ If an agent is killed as an orphan and the work item retries, cooldowns use expo
|
|
|
70
70
|
|
|
71
71
|
The engine.js *process itself* (not an agent) can crash and get auto-respawned by three independent mechanisms: `engine/supervisor.js#checkEngine` (dead-PID), `engine/supervisor.js#checkEngineHung` (stale-heartbeat), and `engine/watchdog.js` (OS-scheduled external recovery); `dashboard.js`'s in-process 30s watchdog also auto-restarts on a dead PID (the user-triggered Restart Engine button is excluded). Each successful respawn calls `shared.recordEngineRespawn(source)`, which appends `{ts, source}` to `control.json`'s `engineRespawns[]` (rolling window, default `CRASH_LOOP_WINDOW_MS` = 10 min). Once respawns within the window reach `CRASH_LOOP_THRESHOLD` (default 3), `control.crashLoopAlert` is set (dashboard-visible via `/api/status`) and a deduped (one-per-day) `notes/inbox/` alert is written; the alert clears automatically once the window rolls past the incident. Override the window/threshold via `MINIONS_CRASH_LOOP_WINDOW_MS` / `MINIONS_CRASH_LOOP_THRESHOLD` env vars.
|
|
72
72
|
|
|
73
|
+
### 7. Exclusive Restart Topology
|
|
74
|
+
|
|
75
|
+
Managed dashboard spawns must bind the requested port; automatic fallback remains available only for standalone `node dashboard.js` use. Before a supervisor respawn, every matching stale daemon is killed and confirmed dead. If any PID survives, the supervisor skips the replacement and retries on its next tick instead of creating a second engine or a dashboard on another port.
|
|
76
|
+
|
|
77
|
+
`minions restart` verifies ownership of the exact engine, dashboard, and supervisor PIDs it spawned and rejects duplicate daemon processes. A failed verification tears down the partial stack and leaves `stop-intent.json` set so watchdogs cannot race another replacement into the failed topology.
|
|
78
|
+
|
|
79
|
+
PID-bearing file locks are reaped immediately when their recorded owner is dead. The five-minute stale threshold remains for legacy locks without owner metadata and the longer safety window remains for live holders.
|
|
80
|
+
|
|
81
|
+
Dashboard hot polling avoids recurring event-loop stalls: freshness-directory walks are coalesced briefly, plan scans are cached for 30 seconds and processed in cooperative batches, and quarantine-ref Git reads run asynchronously in parallel.
|
|
82
|
+
|
|
73
83
|
## Safe Restart Pattern
|
|
74
84
|
|
|
75
85
|
```bash
|