@yemi33/minions 0.1.2321 → 0.1.2323
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/dashboard/docs/typography.md +21 -2
- package/dashboard/js/render-other.js +17 -1
- package/dashboard/js/render-prs.js +74 -18
- package/dashboard/slim/body.html +6 -1
- package/dashboard/slim/js/modals-tiles.js +23 -116
- package/dashboard/slim/styles.css +11 -28
- package/dashboard/styles.css +18 -8
- package/dashboard.js +58 -0
- package/docs/harness-transparency.md +17 -1
- package/docs/project-skills.md +61 -0
- package/engine/consolidation.js +69 -7
- package/engine/discover-project-skills.js +190 -15
- package/engine/lifecycle.js +15 -0
- package/engine/live-checkout.js +38 -0
- package/engine/playbook.js +31 -1
- package/engine/queries.js +15 -0
- package/engine/shared.js +83 -1
- package/engine.js +132 -16
- package/package.json +1 -1
- package/prompts/cc-system.md +15 -4
package/engine/live-checkout.js
CHANGED
|
@@ -220,6 +220,25 @@ function _collectAutoCleanablePaths(dirtyFiles) {
|
|
|
220
220
|
return cleanable.length > 0 ? cleanable : null;
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
+
// W-mr3lokxs — Cap a porcelain dirty-file list before it is embedded verbatim
|
|
224
|
+
// into a live-checkout-dirty inbox alert body. On GVFS/virtual repos (see the
|
|
225
|
+
// "~120k virtual dirty files" note in prepareLiveCheckout below) a single
|
|
226
|
+
// refusal can report tens of thousands of "modified" paths; embedding the full
|
|
227
|
+
// list produced multi-MB / tens-of-MB alert bodies that then bloated the
|
|
228
|
+
// knowledge base (240 dirty-worktree dumps ≈ 113 MB). Keep the first `maxLines`
|
|
229
|
+
// entries and replace the remainder with a single truncation-notice line so the
|
|
230
|
+
// alert stays human-scannable and bounded. Pure/deterministic — exported for
|
|
231
|
+
// unit testing.
|
|
232
|
+
const DIRTY_ALERT_MAX_LINES = 200;
|
|
233
|
+
function capDirtyFileLines(dirtyFiles, maxLines = DIRTY_ALERT_MAX_LINES) {
|
|
234
|
+
const lines = Array.isArray(dirtyFiles) ? dirtyFiles : [];
|
|
235
|
+
const cap = Number.isInteger(maxLines) && maxLines > 0 ? maxLines : DIRTY_ALERT_MAX_LINES;
|
|
236
|
+
if (lines.length <= cap) return lines.slice();
|
|
237
|
+
const kept = lines.slice(0, cap);
|
|
238
|
+
kept.push(`... and ${lines.length - cap} more (run \`git status --porcelain=v1 -b\` locally for the full list)`);
|
|
239
|
+
return kept;
|
|
240
|
+
}
|
|
241
|
+
|
|
223
242
|
// issue #608 — detects a branch name that the ENGINE created for a live-mode
|
|
224
243
|
// dispatch (never the operator's own branch). Matches ONLY the literal
|
|
225
244
|
// `work/<wi-id>` convention produced by `shared.deriveWorkItemBranchName` —
|
|
@@ -362,6 +381,23 @@ async function prepareLiveCheckout(opts = {}) {
|
|
|
362
381
|
// gitOpts.maxBuffer still wins (spread after the default).
|
|
363
382
|
const baseOpts = { cwd: localPath, maxBuffer: LIVE_CHECKOUT_GIT_MAX_BUFFER, ...(gitOpts || {}) };
|
|
364
383
|
|
|
384
|
+
// ── Step 0 (W-mr3tayu4): clear a stale .git/index.lock BEFORE any preflight
|
|
385
|
+
// git command runs. A leftover index.lock (from a crashed/killed git
|
|
386
|
+
// process) makes every subsequent git invocation fail with "Unable to
|
|
387
|
+
// create '<path>/.git/index.lock': File exists." In live-checkout mode the
|
|
388
|
+
// engine never git-reset/cleans the operator tree, so nothing else clears
|
|
389
|
+
// it — and the LIVE_CHECKOUT_FAILED classification is retryable, so every
|
|
390
|
+
// automatic retry hit the SAME stale lock and failed identically forever
|
|
391
|
+
// until an operator manually deleted it (observed incident: C:/office/src).
|
|
392
|
+
// Mirror the worktree-mode self-heal (engine.js `git worktree add` retry
|
|
393
|
+
// paths) by calling the SHARED age-gated (>300000ms) remover — see
|
|
394
|
+
// engine.js#removeStaleIndexLock, which now also delegates here. Because
|
|
395
|
+
// each retry re-enters spawnAgent → prepareLiveCheckout, running this at the
|
|
396
|
+
// top also clears a lock that appears mid-retry, not just at first preflight.
|
|
397
|
+
// The 5-min age gate is intentionally conservative so a lock held by a
|
|
398
|
+
// currently-running git process is never removed out from under it.
|
|
399
|
+
shared.removeStaleIndexLock(localPath, { log: (level, msg) => logFn(msg, level) });
|
|
400
|
+
|
|
365
401
|
// ── Step 1: git status --porcelain=v1 -b. Bail early on dirty tree. ─────
|
|
366
402
|
// Skipped when `skipDirtyCheck:true` (issue #522): GVFS/VFS-for-Git repos
|
|
367
403
|
// report all un-hydrated virtual files as modified — this is normal GVFS
|
|
@@ -1234,5 +1270,7 @@ module.exports = {
|
|
|
1234
1270
|
_looksLikeAgentBranch,
|
|
1235
1271
|
_extractBranchFromPorcelainHeader,
|
|
1236
1272
|
_commitAgentWipWithRetry,
|
|
1273
|
+
capDirtyFileLines,
|
|
1274
|
+
DIRTY_ALERT_MAX_LINES,
|
|
1237
1275
|
LIVE_CHECKOUT_AUTO_CLEAN_PATTERNS,
|
|
1238
1276
|
};
|
package/engine/playbook.js
CHANGED
|
@@ -386,6 +386,15 @@ const PLAYBOOK_OPTIONAL_VARS = new Set([
|
|
|
386
386
|
// produce matches. Optional for the same reason as the review-only alias.
|
|
387
387
|
'project_skills_block',
|
|
388
388
|
'skip_project_skills',
|
|
389
|
+
// P-f52d81ba — monorepo-aware harness scoping. The engine derives a dominant
|
|
390
|
+
// sub-project for the dispatch (explicit meta.workdir first segment, or
|
|
391
|
+
// detectDominantSubproject over the current dispatch's changed paths) and threads
|
|
392
|
+
// it here so playbook.js can pass it as `scopeSubproject` into project-skill
|
|
393
|
+
// discovery. Optional with an empty-string default: single-root projects and
|
|
394
|
+
// mixed/root diffs legitimately resolve it to '' (flat discovery), and no
|
|
395
|
+
// playbook references {{dominant_subproject}} directly, so the unresolved-var check
|
|
396
|
+
// must stay quiet when it is unset.
|
|
397
|
+
'dominant_subproject',
|
|
389
398
|
// M006 — typed handoff envelope fields. Set on a follow-up dispatch meta
|
|
390
399
|
// when lifecycle.js queues the item after a completing agent so the
|
|
391
400
|
// receiving agent has explicit context about who handed off and why.
|
|
@@ -401,6 +410,7 @@ const PLAYBOOK_OPTIONAL_VARS = new Set([
|
|
|
401
410
|
'prior_explore_context',
|
|
402
411
|
]);
|
|
403
412
|
|
|
413
|
+
|
|
404
414
|
const PLAYBOOK_REQUIRED_VARS = {
|
|
405
415
|
'implement': ['item_id', 'item_name', 'branch_name', 'project_path'],
|
|
406
416
|
'implement-shared': ['item_id', 'item_name', 'branch_name', 'worktree_path'],
|
|
@@ -814,7 +824,27 @@ function renderPlaybook(type, vars) {
|
|
|
814
824
|
const effectiveType = vars.parent_dispatch_type || type;
|
|
815
825
|
const intents = intentsForPlaybook(effectiveType);
|
|
816
826
|
if (intents.length > 0) {
|
|
817
|
-
|
|
827
|
+
// P-f52d81ba — monorepo-aware scoping. When the engine derived a
|
|
828
|
+
// dominant sub-project for this dispatch (git diff / PR files /
|
|
829
|
+
// references → detectDominantSubproject, threaded in via vars.dominant_subproject),
|
|
830
|
+
// surface that sub-project's skills FIRST. Empty/unset ⇒ today's flat
|
|
831
|
+
// global name-sort. Use the diagnostics variant so per-surface
|
|
832
|
+
// overflow (entries dropped past the maxFilesPerSurface cap) is
|
|
833
|
+
// observable instead of silent.
|
|
834
|
+
const scopeSubproject = typeof vars.dominant_subproject === 'string' ? vars.dominant_subproject : '';
|
|
835
|
+
const { entries, truncations } = discover.discoverProjectSkillsWithDiagnostics({
|
|
836
|
+
projectPath,
|
|
837
|
+
scopeSubproject,
|
|
838
|
+
opts: { maxFilesPerSurface: ENGINE_DEFAULTS.projectSkillMaxFilesPerSurface },
|
|
839
|
+
});
|
|
840
|
+
if (Array.isArray(truncations) && truncations.length > 0) {
|
|
841
|
+
log('warn', `discover-project-skills: ${truncations.length} surface(s) exceeded maxFilesPerSurface (entries dropped) for project ${matchedProject?.name || projectPath}${scopeSubproject ? ', subproject=' + scopeSubproject : ''}: ${truncations.map(t => `${t.dir} (${t.scanned}/${t.total})`).join('; ')}`);
|
|
842
|
+
}
|
|
843
|
+
// Stash discovery diagnostics on the vars object so the engine
|
|
844
|
+
// caller (spawnAgent) can record detected sub-project + truncations onto
|
|
845
|
+
// the dispatch record for later grounding/observability. Best-effort:
|
|
846
|
+
// callers that don't read it simply ignore the field.
|
|
847
|
+
vars._discoveryDiagnostics = { subproject: scopeSubproject, truncations: Array.isArray(truncations) ? truncations : [] };
|
|
818
848
|
const filtered = discover.filterByIntents(entries, intents);
|
|
819
849
|
// type === 'review' keeps PR-82 header copy + meta.review.* outcome
|
|
820
850
|
// guidance; everything else uses the generic header that points at
|
package/engine/queries.js
CHANGED
|
@@ -949,6 +949,15 @@ function _pickPrDedupeWinner(group, projects) {
|
|
|
949
949
|
return { winner, dropped };
|
|
950
950
|
}
|
|
951
951
|
|
|
952
|
+
// W-mr3mvgfe — terminal PR statuses (done being polled/reviewed) get pushed
|
|
953
|
+
// to the bottom of the PR tab's default sort. Includes `CLOSED` (a PR closed
|
|
954
|
+
// without merging, distinct from `ABANDONED` but equally inactive — see
|
|
955
|
+
// engine/github.js `newStatus = PR_STATUS.CLOSED` on `prData.state === 'closed'`
|
|
956
|
+
// without `merged`) alongside `MERGED`/`ABANDONED`.
|
|
957
|
+
function _isTerminalPrStatus(status) {
|
|
958
|
+
return status === PR_STATUS.MERGED || status === PR_STATUS.ABANDONED || status === PR_STATUS.CLOSED;
|
|
959
|
+
}
|
|
960
|
+
|
|
952
961
|
function getPullRequests(config) {
|
|
953
962
|
const now = Date.now();
|
|
954
963
|
if (_prsCache && (now - _prsCacheAt) < 1000) return _prsCache;
|
|
@@ -1018,6 +1027,12 @@ function getPullRequests(config) {
|
|
|
1018
1027
|
allPrs.push(pr);
|
|
1019
1028
|
}
|
|
1020
1029
|
allPrs.sort((a, b) => {
|
|
1030
|
+
// W-mr3mvgfe: group terminal PRs (merged/abandoned/closed) after active
|
|
1031
|
+
// ones so a stale, already-resolved PR doesn't crowd out active work at
|
|
1032
|
+
// the top of the list regardless of its `created` timestamp.
|
|
1033
|
+
const aTerminal = _isTerminalPrStatus(a.status) ? 1 : 0;
|
|
1034
|
+
const bTerminal = _isTerminalPrStatus(b.status) ? 1 : 0;
|
|
1035
|
+
if (aTerminal !== bTerminal) return aTerminal - bTerminal;
|
|
1021
1036
|
// W-mpej044m00076d63: sort by the full ISO `created` timestamp DESC so
|
|
1022
1037
|
// same-day PRs preserve creation order (previously the slice-to-date
|
|
1023
1038
|
// collapsed every PR opened on the same day into one bucket, then tied
|
package/engine/shared.js
CHANGED
|
@@ -2583,6 +2583,37 @@ function shellSafeGh(args, opts = {}) {
|
|
|
2583
2583
|
}).then(({ stdout }) => stdout);
|
|
2584
2584
|
}
|
|
2585
2585
|
|
|
2586
|
+
// W-mr3tayu4 — shared, age-gated stale `.git/index.lock` remover. Previously
|
|
2587
|
+
// lived only in engine.js and was called from the worktree-mode `git worktree
|
|
2588
|
+
// add` retry paths; it is now shared so engine/live-checkout.js#prepareLiveCheckout
|
|
2589
|
+
// (checkoutMode:'live' projects) can self-heal the same failure without a
|
|
2590
|
+
// circular import. Removes `<rootDir>/.git/index.lock` ONLY when it is older
|
|
2591
|
+
// than STALE_INDEX_LOCK_MAX_AGE_MS (5 min) — deliberately conservative so a lock
|
|
2592
|
+
// held by a currently-running git process is never deleted out from under it.
|
|
2593
|
+
// All fs/log seams are injectable for unit tests; production callers pass none.
|
|
2594
|
+
const STALE_INDEX_LOCK_MAX_AGE_MS = 300000;
|
|
2595
|
+
|
|
2596
|
+
function removeStaleIndexLock(rootDir, opts = {}) {
|
|
2597
|
+
const {
|
|
2598
|
+
log: logOverride, // (level, msg) => void — defaults to the shared `log`
|
|
2599
|
+
_exists = fs.existsSync,
|
|
2600
|
+
_stat = fs.statSync,
|
|
2601
|
+
_unlink = fs.unlinkSync,
|
|
2602
|
+
_now = Date.now,
|
|
2603
|
+
} = opts;
|
|
2604
|
+
const emit = typeof logOverride === 'function' ? logOverride : (level, msg) => log(level, msg);
|
|
2605
|
+
const lockFile = path.join(rootDir, '.git', 'index.lock');
|
|
2606
|
+
try {
|
|
2607
|
+
if (_exists(lockFile)) {
|
|
2608
|
+
const age = _now() - _stat(lockFile).mtimeMs;
|
|
2609
|
+
if (age > STALE_INDEX_LOCK_MAX_AGE_MS) {
|
|
2610
|
+
_unlink(lockFile);
|
|
2611
|
+
emit('warn', `Removed stale index.lock (${Math.round(age / 1000)}s old) in ${rootDir}`);
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
} catch (e) { emit('warn', 'git: ' + e.message); }
|
|
2615
|
+
}
|
|
2616
|
+
|
|
2586
2617
|
function shellSafeGit(args, opts = {}) {
|
|
2587
2618
|
if (!Array.isArray(args)) {
|
|
2588
2619
|
return Promise.reject(new TypeError('shellSafeGit: args must be an array'));
|
|
@@ -2796,11 +2827,44 @@ const KB_READABLE_CATEGORIES = Object.freeze([
|
|
|
2796
2827
|
'consolidated', 'consolidation', 'consolidations',
|
|
2797
2828
|
'team-memory', 'general', 'patterns',
|
|
2798
2829
|
]);
|
|
2830
|
+
/**
|
|
2831
|
+
* Detect engine-authored system alerts (dirty-tree refusals, worktree-skip-live
|
|
2832
|
+
* guard notices, blocked/conflicted live-checkout dispatches, managed-spawn
|
|
2833
|
+
* failures, ADO-auth alerts, …). These are operational noise, NOT durable
|
|
2834
|
+
* reusable knowledge, and the same underlying incident recurs verbatim across
|
|
2835
|
+
* many different inbox filenames (one per worktree/date), so consolidation
|
|
2836
|
+
* must never copy their full body into knowledge/ as a distinct "new" entry
|
|
2837
|
+
* per recurrence — see W-mr3pi9de (worktree-skip-live note titles were
|
|
2838
|
+
* identical across every firing regardless of worktree, producing 30+
|
|
2839
|
+
* duplicate-titled knowledge/project-notes/*.md entries on a single day) and
|
|
2840
|
+
* the sibling W-mr3lokxs fix for live-checkout-dirty alerts.
|
|
2841
|
+
*
|
|
2842
|
+
* `engine/dispatch.js#writeInboxAlert` names every file `engine-alert-<slug>-
|
|
2843
|
+
* <date>.md`, so the filename prefix is one signal. `shared.js#_writeWorktree
|
|
2844
|
+
* SkipLiveInboxNote` (and any other engine-authored note) stamps frontmatter
|
|
2845
|
+
* `agent: engine`, accepted as a second signal so alerts written under a
|
|
2846
|
+
* different filename convention are still recognized.
|
|
2847
|
+
*/
|
|
2848
|
+
function isEngineSystemAlert(name, content) {
|
|
2849
|
+
const nameLower = (name || '').toLowerCase();
|
|
2850
|
+
if (nameLower.includes('engine-alert-') || nameLower.includes('engine-worktree-skip-live-')) return true;
|
|
2851
|
+
const fmMatch = String(content || '').match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
2852
|
+
if (fmMatch && /^agent:\s*engine\s*$/im.test(fmMatch[1])) return true;
|
|
2853
|
+
return false;
|
|
2854
|
+
}
|
|
2855
|
+
|
|
2799
2856
|
/**
|
|
2800
2857
|
* Classify an inbox item into a knowledge base category.
|
|
2801
2858
|
* Single source of truth — used by consolidation.js (both LLM and regex paths).
|
|
2802
2859
|
*/
|
|
2803
2860
|
function classifyInboxItem(name, content) {
|
|
2861
|
+
// W-mr3pi9de / W-mr3lokxs — engine-authored system alerts are operational
|
|
2862
|
+
// noise. Route them to 'project-notes' BEFORE the content-substring
|
|
2863
|
+
// heuristics below, so a note whose body incidentally contains substrings
|
|
2864
|
+
// like 'lint', 'review', or 'pr-' can't be misclassified into
|
|
2865
|
+
// build-reports/reviews. classifyToKnowledgeBase additionally forces these
|
|
2866
|
+
// non-reusable and content-hash-dedups them regardless of category.
|
|
2867
|
+
if (isEngineSystemAlert(name, content)) return 'project-notes';
|
|
2804
2868
|
const nameLower = (name || '').toLowerCase();
|
|
2805
2869
|
const contentLower = (content || '').toLowerCase();
|
|
2806
2870
|
if (nameLower.includes('review') || nameLower.includes('pr-') || nameLower.includes('pr4') || nameLower.includes('feedback')) return 'reviews';
|
|
@@ -3372,6 +3436,15 @@ const ENGINE_DEFAULTS = {
|
|
|
3372
3436
|
// closed by default. Set to false to fall back to the legacy
|
|
3373
3437
|
// "only committed assets are visible" behavior.
|
|
3374
3438
|
harnessPropagateProjectLocal: true,
|
|
3439
|
+
// P-7c1a9e42 — cap on how many skill packs / command files
|
|
3440
|
+
// engine/discover-project-skills.js reads per surface (root `.claude/skills`,
|
|
3441
|
+
// each `<area>/.claude/skills`, etc.). Raised well above the module-level
|
|
3442
|
+
// DEFAULTS.maxFilesPerSurface fallback (50) so large monorepo sub-projects (e.g.
|
|
3443
|
+
// office/src/ocm/.claude/skills with ~80 packages) don't silently drop the
|
|
3444
|
+
// most-relevant skills past the cap. engine/playbook.js threads this into the
|
|
3445
|
+
// discovery `opts`; when no override is supplied the module falls back to its
|
|
3446
|
+
// own DEFAULTS.maxFilesPerSurface. See docs/project-skills.md.
|
|
3447
|
+
projectSkillMaxFilesPerSurface: 200,
|
|
3375
3448
|
// P-49e1c8b7 — hermetic harness opt-out. When TRUE the spawned agent runs
|
|
3376
3449
|
// with a known-empty harness surface around the worktree:
|
|
3377
3450
|
// - `--add-dir` is exactly `[minionsDir]` (user-scope skill/command/MCP
|
|
@@ -8853,7 +8926,13 @@ function _writeWorktreeSkipLiveInboxNote(worktreePath, callerTag, blockingInfo)
|
|
|
8853
8926
|
`date: ${date}`,
|
|
8854
8927
|
'---',
|
|
8855
8928
|
'',
|
|
8856
|
-
|
|
8929
|
+
// W-mr3pi9de — embed the actual worktree basename, not a hardcoded
|
|
8930
|
+
// literal. The previous fixed string made every note (and therefore
|
|
8931
|
+
// every knowledge/project-notes/*.md entry consolidation wrote from
|
|
8932
|
+
// it) share byte-identical titles regardless of which worktree fired
|
|
8933
|
+
// the guard, defeating uniquePath's -2/-3/… suffixing and flooding the
|
|
8934
|
+
// KB with near-duplicate-content entries under different numbers.
|
|
8935
|
+
`# Engine skipped worktree wipe — live dispatch guard fired (${base})`,
|
|
8857
8936
|
'',
|
|
8858
8937
|
`- caller: ${callerTag || 'unknown'}`,
|
|
8859
8938
|
`- worktree: ${worktreePath}`,
|
|
@@ -9374,6 +9453,8 @@ module.exports = {
|
|
|
9374
9453
|
execSilent,
|
|
9375
9454
|
shellSafeGh,
|
|
9376
9455
|
shellSafeGit,
|
|
9456
|
+
removeStaleIndexLock,
|
|
9457
|
+
STALE_INDEX_LOCK_MAX_AGE_MS,
|
|
9377
9458
|
shellSafeGitSync,
|
|
9378
9459
|
validateGitRef,
|
|
9379
9460
|
validateGhSlug,
|
|
@@ -9392,6 +9473,7 @@ module.exports = {
|
|
|
9392
9473
|
KB_CATEGORIES,
|
|
9393
9474
|
KB_READABLE_CATEGORIES,
|
|
9394
9475
|
classifyInboxItem,
|
|
9476
|
+
isEngineSystemAlert,
|
|
9395
9477
|
ENGINE_DEFAULTS,
|
|
9396
9478
|
resolvePollFlag, // P-c4d8e1a3 — granular per-poller flag resolution
|
|
9397
9479
|
resolveAgentCli, resolveCcCli, resolveCcUseWorkerPool, resolveAgentModel, resolveCcModel,
|
package/engine.js
CHANGED
|
@@ -933,17 +933,13 @@ async function _fetchWithTransientRetry(args, opts, label) {
|
|
|
933
933
|
}
|
|
934
934
|
}
|
|
935
935
|
|
|
936
|
+
// W-mr3tayu4 — thin wrapper around shared.removeStaleIndexLock. The age-gated
|
|
937
|
+
// (>300000ms) stale-lock removal now lives in engine/shared.js so both the
|
|
938
|
+
// worktree-add retry paths here and engine/live-checkout.js#prepareLiveCheckout
|
|
939
|
+
// share one implementation (no circular import). Kept as a named local because
|
|
940
|
+
// three internal call sites and the test harness reference `removeStaleIndexLock`.
|
|
936
941
|
function removeStaleIndexLock(rootDir) {
|
|
937
|
-
|
|
938
|
-
try {
|
|
939
|
-
if (fs.existsSync(lockFile)) {
|
|
940
|
-
const age = Date.now() - fs.statSync(lockFile).mtimeMs;
|
|
941
|
-
if (age > 300000) {
|
|
942
|
-
fs.unlinkSync(lockFile);
|
|
943
|
-
log('warn', `Removed stale index.lock (${Math.round(age / 1000)}s old) in ${rootDir}`);
|
|
944
|
-
}
|
|
945
|
-
}
|
|
946
|
-
} catch (e) { log('warn', 'git: ' + e.message); }
|
|
942
|
+
return shared.removeStaleIndexLock(rootDir);
|
|
947
943
|
}
|
|
948
944
|
|
|
949
945
|
async function runWorktreeAdd(rootDir, worktreePath, addArgs, gitOpts, worktreeCreateRetries) {
|
|
@@ -1927,6 +1923,38 @@ async function recoverPartialWorktree(rootDir, worktreePath, branchName, gitOpts
|
|
|
1927
1923
|
}
|
|
1928
1924
|
}
|
|
1929
1925
|
|
|
1926
|
+
// P-f52d81ba — persist the dispatch's detected monorepo sub-project (+ any per-surface
|
|
1927
|
+
// skill-discovery truncations the render surfaced) onto the dispatch record so
|
|
1928
|
+
// later grounding/observability can see which sub-project scoped discovery + harness
|
|
1929
|
+
// propagation. Best-effort: mutation failures must never block the spawn. Only
|
|
1930
|
+
// writes fields that carry a value so unset sub-projects leave the record untouched.
|
|
1931
|
+
function _recordDispatchDiscoveryDiagnostics(id, dispatchItem, subproject, diagnostics) {
|
|
1932
|
+
try {
|
|
1933
|
+
const dominantSubproject = (typeof subproject === 'string' && subproject) ? subproject : undefined;
|
|
1934
|
+
const truncations = diagnostics && Array.isArray(diagnostics.truncations) && diagnostics.truncations.length
|
|
1935
|
+
? diagnostics.truncations
|
|
1936
|
+
: undefined;
|
|
1937
|
+
if (dominantSubproject === undefined && truncations === undefined) return;
|
|
1938
|
+
mutateDispatch((dispatch) => {
|
|
1939
|
+
for (const queue of ['pending', 'active', 'completed']) {
|
|
1940
|
+
const arr = Array.isArray(dispatch?.[queue]) ? dispatch[queue] : null;
|
|
1941
|
+
if (!arr) continue;
|
|
1942
|
+
const found = arr.find(d => d && d.id === id);
|
|
1943
|
+
if (!found) continue;
|
|
1944
|
+
if (dominantSubproject !== undefined) found._dominantSubproject = dominantSubproject;
|
|
1945
|
+
if (truncations !== undefined) found._skillDiscoveryTruncations = truncations;
|
|
1946
|
+
}
|
|
1947
|
+
return dispatch;
|
|
1948
|
+
});
|
|
1949
|
+
if (dispatchItem) {
|
|
1950
|
+
if (dominantSubproject !== undefined) dispatchItem._dominantSubproject = dominantSubproject;
|
|
1951
|
+
if (truncations !== undefined) dispatchItem._skillDiscoveryTruncations = truncations;
|
|
1952
|
+
}
|
|
1953
|
+
} catch (e) {
|
|
1954
|
+
log('warn', `record discovery diagnostics failed for ${id} (non-fatal): ${e.message}`);
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1930
1958
|
// Seed a spawned agent's COPILOT_HOME mcp-config as a copy of the operator's
|
|
1931
1959
|
// `~/.copilot/mcp-config.json` MINUS the servers in
|
|
1932
1960
|
// `engine.copilotAgentDisabledMcpServers`, then point COPILOT_HOME at it.
|
|
@@ -2555,7 +2583,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
2555
2583
|
'## Dirty files (`git status --porcelain=v1 -b`)',
|
|
2556
2584
|
'',
|
|
2557
2585
|
'```',
|
|
2558
|
-
...(_dirtyFiles.length > 0 ? _dirtyFiles : ['(none reported)']),
|
|
2586
|
+
...(_dirtyFiles.length > 0 ? _liveCheckout.capDirtyFileLines(_dirtyFiles) : ['(none reported)']),
|
|
2559
2587
|
'```',
|
|
2560
2588
|
'',
|
|
2561
2589
|
_autoCleanupEnabled
|
|
@@ -3894,6 +3922,45 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
3894
3922
|
|
|
3895
3923
|
updateAgentStatus(id, AGENT_STATUS.READY, 'Worktree ready, preparing to spawn process');
|
|
3896
3924
|
|
|
3925
|
+
// ── P-f52d81ba — derive the effective monorepo sub-project for this dispatch ──────
|
|
3926
|
+
// Feeds BOTH project-skill discovery (scopeSubproject → sub-project's skills lead the
|
|
3927
|
+
// rendered project_skills_block) and project-local harness propagation
|
|
3928
|
+
// (workdir clip → only the sub-project's --project-harness-dir roots are surfaced).
|
|
3929
|
+
// Precedence:
|
|
3930
|
+
// 1. Explicit meta.workdir wins — effective sub-project = its first segment. No
|
|
3931
|
+
// diff work (the operator already declared the target sub-package).
|
|
3932
|
+
// 2. Else best-effort from the CURRENT dispatch's diff: `git diff
|
|
3933
|
+
// --name-only <main>...HEAD` in this worktree → detectDominantSubproject.
|
|
3934
|
+
// 3. Else '' (flat discovery / unclipped harness — today's behavior).
|
|
3935
|
+
// Contract: never blocks dispatch, never changes agent cwd (only an explicit
|
|
3936
|
+
// meta.workdir moves cwd, applied separately below), and degrades to '' on
|
|
3937
|
+
// any failure. Derived from THIS worktree's HEAD so a follow-up/parent's
|
|
3938
|
+
// stale diff can't leak in (AC: current-dispatch diff only).
|
|
3939
|
+
let _effectiveSubproject = '';
|
|
3940
|
+
try {
|
|
3941
|
+
if (validatedWorkdir) {
|
|
3942
|
+
_effectiveSubproject = String(validatedWorkdir).replace(/\\/g, '/').replace(/^\/+/, '').split('/')[0] || '';
|
|
3943
|
+
} else if (worktreePath && fs.existsSync(worktreePath) && project?.localPath) {
|
|
3944
|
+
let _subprojectChangedPaths = [];
|
|
3945
|
+
try {
|
|
3946
|
+
const _subprojectBase = sanitizeBranch(shared.resolveMainBranch(rootDir, project.mainBranch));
|
|
3947
|
+
const _subprojectDiff = await execAsync(`git diff --name-only ${_subprojectBase}...HEAD`, { ..._gitOpts, cwd: worktreePath, timeout: 10000 });
|
|
3948
|
+
_subprojectChangedPaths = (_subprojectDiff.stdout || '').split('\n').map(s => s.trim()).filter(Boolean);
|
|
3949
|
+
} catch (e) {
|
|
3950
|
+
log('debug', `subproject-detect: git diff failed for ${id} (flat discovery): ${e.message}`);
|
|
3951
|
+
}
|
|
3952
|
+
if (_subprojectChangedPaths.length) {
|
|
3953
|
+
const { detectDominantSubproject } = require('./engine/discover-project-skills')._internal;
|
|
3954
|
+
const _det = detectDominantSubproject({ projectPath: project.localPath, changedPaths: _subprojectChangedPaths });
|
|
3955
|
+
if (_det && _det.confident && _det.subproject) _effectiveSubproject = _det.subproject;
|
|
3956
|
+
}
|
|
3957
|
+
}
|
|
3958
|
+
} catch (e) {
|
|
3959
|
+
log('warn', `subproject-detect failed for ${id} (non-fatal, flat discovery): ${e.message}`);
|
|
3960
|
+
_effectiveSubproject = '';
|
|
3961
|
+
}
|
|
3962
|
+
|
|
3963
|
+
let _refreshedDiscoveryDiagnostics = null;
|
|
3897
3964
|
if (worktreePath && meta?.source === 'work-item' && meta?.item?.branchStrategy === 'shared-branch') {
|
|
3898
3965
|
const refreshed = renderProjectWorkItemPromptForAgent(
|
|
3899
3966
|
meta.item,
|
|
@@ -3903,15 +3970,21 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
3903
3970
|
project,
|
|
3904
3971
|
rootDir,
|
|
3905
3972
|
branchName,
|
|
3906
|
-
{ worktreePath }
|
|
3973
|
+
{ worktreePath, dominantSubproject: _effectiveSubproject }
|
|
3907
3974
|
);
|
|
3908
3975
|
if (refreshed.prompt) {
|
|
3909
3976
|
taskPrompt = refreshed.prompt;
|
|
3910
3977
|
fullTaskPrompt = buildFullTaskPrompt(taskPrompt);
|
|
3911
3978
|
safeWrite(promptPath, fullTaskPrompt);
|
|
3912
|
-
log('info', `Refreshed shared-branch prompt for ${id} with worktree ${worktreePath}`);
|
|
3979
|
+
log('info', `Refreshed shared-branch prompt for ${id} with worktree ${worktreePath}${_effectiveSubproject ? ` (subproject=${_effectiveSubproject})` : ''}`);
|
|
3913
3980
|
}
|
|
3981
|
+
_refreshedDiscoveryDiagnostics = refreshed.discoveryDiagnostics || null;
|
|
3914
3982
|
}
|
|
3983
|
+
// Record the detected sub-project (+ any per-surface skill-discovery truncations the
|
|
3984
|
+
// shared-branch re-render surfaced) onto the dispatch record for later
|
|
3985
|
+
// grounding/observability. Sub-project-only for non-re-render dispatches (their
|
|
3986
|
+
// prompt was rendered at dispatch-build time; truncations are logged there).
|
|
3987
|
+
_recordDispatchDiscoveryDiagnostics(id, dispatchItem, _effectiveSubproject, _refreshedDiscoveryDiagnostics);
|
|
3915
3988
|
|
|
3916
3989
|
// Inject dirty file list when worktree has uncommitted changes (e.g., max_turns retry)
|
|
3917
3990
|
// This signals to the respawned agent that prior work exists in the worktree (#960)
|
|
@@ -4223,7 +4296,12 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
4223
4296
|
const filteredDirs = shared.filterProjectHarnessDirsForWorkdir(candidateDirs, {
|
|
4224
4297
|
projectLocalPath: project.localPath,
|
|
4225
4298
|
worktreePath,
|
|
4226
|
-
workdir
|
|
4299
|
+
// P-f52d81ba — clip to the effective sub-project. Explicit meta.workdir wins
|
|
4300
|
+
// (validatedWorkdir, possibly multi-segment); otherwise use the
|
|
4301
|
+
// auto-derived dominant sub-project (single first-level segment) so an
|
|
4302
|
+
// ocm/-dominant diff surfaces only ocm/'s --project-harness-dir roots.
|
|
4303
|
+
// Empty ⇒ no clip (today's whole-project harness surface).
|
|
4304
|
+
workdir: validatedWorkdir || _effectiveSubproject || null,
|
|
4227
4305
|
});
|
|
4228
4306
|
for (const abs of filteredDirs) {
|
|
4229
4307
|
if (!fs.existsSync(abs)) continue;
|
|
@@ -4231,7 +4309,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
4231
4309
|
propagatedProjectHarnessDirs.push(abs);
|
|
4232
4310
|
}
|
|
4233
4311
|
if (projectHarnessArgs.length) {
|
|
4234
|
-
log('debug', `Project-local harness propagation: ${projectHarnessArgs.length / 2} dir(s) for ${id} (${worktreePath}${validatedWorkdir ? ', workdir=' + validatedWorkdir : ''})`);
|
|
4312
|
+
log('debug', `Project-local harness propagation: ${projectHarnessArgs.length / 2} dir(s) for ${id} (${worktreePath}${validatedWorkdir ? ', workdir=' + validatedWorkdir : (_effectiveSubproject ? ', subproject=' + _effectiveSubproject : '')})`);
|
|
4235
4313
|
}
|
|
4236
4314
|
} catch (err) {
|
|
4237
4315
|
log('warn', `Project-local harness propagation failed for ${id} (non-fatal): ${err.message}`);
|
|
@@ -7699,6 +7777,30 @@ function _buildRunnerBriefVars(item, project) {
|
|
|
7699
7777
|
}
|
|
7700
7778
|
}
|
|
7701
7779
|
|
|
7780
|
+
// P-f52d81ba — sync effective-sub-project derivation for the build-time prompt render
|
|
7781
|
+
// (renderProjectWorkItemPromptForAgent runs synchronously, so no git diff).
|
|
7782
|
+
// Explicit meta.workdir wins (first segment). Otherwise best-effort from the
|
|
7783
|
+
// work item's references[*].path via detectDominantSubproject. Returns '' (flat
|
|
7784
|
+
// discovery) on anything unresolved. spawnAgent's async re-render overrides
|
|
7785
|
+
// this with a git-diff-derived sub-project via options.dominantSubproject.
|
|
7786
|
+
function _deriveDominantSubprojectSync(item, project) {
|
|
7787
|
+
try {
|
|
7788
|
+
const wd = item && item.meta && item.meta.workdir;
|
|
7789
|
+
if (typeof wd === 'string' && wd.trim()) {
|
|
7790
|
+
return wd.replace(/\\/g, '/').replace(/^\/+/, '').split('/')[0] || '';
|
|
7791
|
+
}
|
|
7792
|
+
const projectPath = project && project.localPath;
|
|
7793
|
+
if (!projectPath) return '';
|
|
7794
|
+
const paths = (item && Array.isArray(item.references))
|
|
7795
|
+
? item.references.map(r => (r && typeof r.path === 'string') ? r.path : '').filter(Boolean)
|
|
7796
|
+
: [];
|
|
7797
|
+
if (paths.length === 0) return '';
|
|
7798
|
+
const { detectDominantSubproject } = require('./engine/discover-project-skills')._internal;
|
|
7799
|
+
const det = detectDominantSubproject({ projectPath, changedPaths: paths });
|
|
7800
|
+
return (det && det.confident && det.subproject) ? det.subproject : '';
|
|
7801
|
+
} catch { return ''; }
|
|
7802
|
+
}
|
|
7803
|
+
|
|
7702
7804
|
/**
|
|
7703
7805
|
* Scan work-items.json for manually queued tasks
|
|
7704
7806
|
*/
|
|
@@ -7873,6 +7975,14 @@ function renderProjectWorkItemPromptForAgent(item, workType, agentId, config, pr
|
|
|
7873
7975
|
// Either flag suppresses both blocks on the dispatch.
|
|
7874
7976
|
skip_project_review_skills: !!(item.meta && item.meta.skipProjectReviewSkills),
|
|
7875
7977
|
skip_project_skills: !!(item.meta && (item.meta.skipProjectSkills || item.meta.skipProjectReviewSkills)),
|
|
7978
|
+
// P-f52d81ba — effective monorepo sub-project for scoped skill discovery. When the
|
|
7979
|
+
// async caller (spawnAgent) supplies a git-diff-derived sub-project it wins (even
|
|
7980
|
+
// ''); otherwise derive best-effort from explicit meta.workdir /
|
|
7981
|
+
// references[*].path. Empty ⇒ flat discovery. playbook.js reads this as the
|
|
7982
|
+
// discovery scopeSubproject.
|
|
7983
|
+
dominant_subproject: (typeof options.dominantSubproject === 'string')
|
|
7984
|
+
? options.dominantSubproject
|
|
7985
|
+
: _deriveDominantSubprojectSync(item, project),
|
|
7876
7986
|
// M006 — typed handoff envelope. Set on follow-up dispatch metas by
|
|
7877
7987
|
// lifecycle.js when queuing an item after a completing agent. The
|
|
7878
7988
|
// renderPlaybook path injects a "Handoff Context" section when truthy
|
|
@@ -7892,10 +8002,16 @@ function renderProjectWorkItemPromptForAgent(item, workType, agentId, config, pr
|
|
|
7892
8002
|
if (playbookName === 'work-item' && workType === WORK_TYPE.REVIEW) {
|
|
7893
8003
|
log('info', `Work item ${item.id} is type "review" but has no PR — using work-item playbook`);
|
|
7894
8004
|
}
|
|
8005
|
+
const prompt = item.prompt || renderPlaybook(playbookName, vars) || renderPlaybook('work-item', vars) || item.description;
|
|
7895
8006
|
return {
|
|
7896
8007
|
needsReview: false,
|
|
7897
8008
|
checkpointCount: cpResult.checkpointCount,
|
|
7898
|
-
prompt
|
|
8009
|
+
prompt,
|
|
8010
|
+
// P-f52d81ba — surface the discovery diagnostics renderPlaybook stashed on
|
|
8011
|
+
// the vars object (detected sub-project + per-surface truncations) so spawnAgent
|
|
8012
|
+
// can record them onto the dispatch record. null when no playbook render
|
|
8013
|
+
// ran (item.prompt short-circuit) or discovery was skipped.
|
|
8014
|
+
discoveryDiagnostics: vars._discoveryDiagnostics || null,
|
|
7899
8015
|
};
|
|
7900
8016
|
}
|
|
7901
8017
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2323",
|
|
4
4
|
"description": "Multi-agent AI dev team that runs from ~/.minions/ — five autonomous agents share a single engine, dashboard, and knowledge base",
|
|
5
5
|
"bin": {
|
|
6
6
|
"minions": "bin/minions.js"
|
package/prompts/cc-system.md
CHANGED
|
@@ -89,16 +89,25 @@ If you start a small task and discover it's actually Medium (3+ files, more tool
|
|
|
89
89
|
|
|
90
90
|
When genuinely in doubt about the size, delegate — agents have isolated worktrees, full tool access, durable work-item tracking, and no turn limits.
|
|
91
91
|
|
|
92
|
+
### HARD STOP — never edit files yourself in a live/hybrid project
|
|
93
|
+
This overrides everything above, **including the direct-handling override**. For any project whose effective checkout mode is `live` (or `hybrid` for a code-authoring work-item type — `implement`/`fix`/`docs`/`decompose`), you MUST NOT use `Edit`/`Write`/`Bash` (or any tool) to mutate files inside that project's `localPath`, **regardless of task size** — not even a Small 1-line change, and not even when the human explicitly says to do it yourself. The Step-3 "do it yourself" allowance and the Step-1 direct-handling override do **not** apply to writes inside a live/hybrid checkout.
|
|
94
|
+
|
|
95
|
+
Why: live-mode projects have no worktree isolation — the engine runs agents in-place inside `localPath`, caps dispatch to one mutating operation at a time, and refuses to dispatch on a dirty tree (`live-checkout-dirty`, non-retryable). A stray CC edit leaves the checkout dirty in a way the dispatch bookkeeping does not track, and that dirty state then **blocks every subsequent live-mode dispatch** on the project until a human manually cleans it up. It is NOT covered by the engine's dispatch-time auto-stash/auto-reset recovery — those only run as part of an actual dispatch preflight, never for stray CC edits.
|
|
96
|
+
|
|
97
|
+
Instead, when a change is needed in a live/hybrid project, **dispatch a normal work item** (`POST /api/work-items`, `type: "implement"` or `"fix"` as usual) and let the engine own the live-checkout dispatch path (dirty-tree refusal, single-mutating-dispatch cap, auto-stash/auto-reset). Do not try to shortcut it with your own edit.
|
|
98
|
+
|
|
99
|
+
Read-only inspection is still fine to do yourself in a live/hybrid checkout: viewing files, `git status`/`git log`/`git diff`, and non-mutating build/test commands are all fair game. The restriction is specifically about **writes**. See "Project checkout modes (worktree / live / hybrid)" below for the mode contract this rule protects.
|
|
100
|
+
|
|
92
101
|
### After you make a local code edit yourself — offer to open a PR
|
|
93
|
-
|
|
102
|
+
This flow is **worktree-mode only** — it applies solely to projects whose effective checkout mode is `worktree`. For `live`/`hybrid` projects you never edit files yourself (see the HARD STOP above), so there is nothing to PR from a CC edit. If you ever find uncommitted changes sitting in a live-mode project's checkout anyway, do **not** commit or push them yourself — just report the dirty state to the user and let them decide how to clean it up.
|
|
103
|
+
|
|
104
|
+
When you edit files **yourself** in a configured worktree-mode project's working tree (the Step-3 "do it yourself" path — NOT a change a dispatched agent made in its own worktree), don't leave the diff sitting uncommitted. After you finish editing, call:
|
|
94
105
|
```
|
|
95
106
|
curl -s -X POST http://localhost:{{dashboard_port}}/api/pr-action/offer-create-pr \
|
|
96
107
|
-H 'Content-Type: application/json' -H 'X-CC-Turn-Id: {{cc_turn_id}}' \
|
|
97
108
|
-d '{"project":"<project name>"}'
|
|
98
109
|
```
|
|
99
|
-
This checks the project's working tree (`git status --porcelain`) and, when there are uncommitted changes, returns a **`[Create PR]`** follow-up chip to the user (same chip mechanism as the `pr-action` `[Comment]`/`[Fix once]`/`[Track for auto-fix]` chips). When the user clicks it, you'll receive a turn with **explicit step-by-step instructions — follow them exactly**.
|
|
100
|
-
- **Worktree-mode projects** (the default): the turn tells you to first `POST /api/pr-action/prepare-create-pr-worktree {"project":"…"}`. The server moves your uncommitted changes into an **isolated worktree** and restores the live checkout clean, returning `{worktreePath, branch, baseBranch}`. You then run **all git from inside `worktreePath`** (`git -C <worktreePath> …`) to commit/push/open the PR, then `POST /api/pr-action/cleanup-create-pr-worktree` to remove it. **Never commit, branch, or push in the live checkout** — that leaks commits onto the operator's `main`.
|
|
101
|
-
- **Live-mode projects**: the turn tells you to commit on a new branch off main in the live checkout and switch back to the original branch when done.
|
|
110
|
+
This checks the project's working tree (`git status --porcelain`) and, when there are uncommitted changes, returns a **`[Create PR]`** follow-up chip to the user (same chip mechanism as the `pr-action` `[Comment]`/`[Fix once]`/`[Track for auto-fix]` chips). When the user clicks it, you'll receive a turn with **explicit step-by-step instructions — follow them exactly**. Because you only ever edit files yourself in worktree-mode projects (see the HARD STOP above), this is always the worktree-mode flow: the turn tells you to first `POST /api/pr-action/prepare-create-pr-worktree {"project":"…"}`. The server moves your uncommitted changes into an **isolated worktree** and restores the live checkout clean, returning `{worktreePath, branch, baseBranch}`. You then run **all git from inside `worktreePath`** (`git -C <worktreePath> …`) to commit/push/open the PR, then `POST /api/pr-action/cleanup-create-pr-worktree` to remove it. **Never commit, branch, or push in the live checkout** — that leaks commits onto the operator's `main`.
|
|
102
111
|
|
|
103
112
|
Pass `"contextOnly":true` if the PR should be tracked-but-not-auto-reviewed; omit it to have the engine auto-manage the PR (review → fix → re-review → auto-merge). If `hasChanges` is `false`, there's nothing to PR — skip the offer. Don't commit/push on your own initiative; surface the chip and let the user decide.
|
|
104
113
|
|
|
@@ -327,6 +336,8 @@ Every configured project has an effective **checkout mode** — surfaced in your
|
|
|
327
336
|
- **`live`** — agents run **in-place** inside the project's `localPath` (no worktree). The engine caps this to **one mutating dispatch at a time** per project and **refuses on a dirty tree**. Use only when worktrees are unworkable (e.g. Android `repo`, submodules, deep Windows paths, emulators that bind the real checkout).
|
|
328
337
|
- **`hybrid`** — `live` **plus** a `liveValidation: { type, autoDispatch }` block. Coding work items (`implement`/`fix`/`docs`/`decompose`) author in **isolated worktrees** (full parallelism), while **only** work items whose type matches `liveValidation.type` (e.g. `build-and-test`) run **in-place on the live checkout**. This is the best of both: parallel code authoring + a real on-disk build/validation that can't run in a worktree. `type` accepts a single string (e.g. `"build-and-test"`) or an array of strings (e.g. `["implement","fix"]`) to route multiple work-item types to the live checkout at once.
|
|
329
338
|
|
|
339
|
+
**CC never writes into a live/hybrid checkout itself.** Because `live` (and the code-authoring surface of `hybrid`) runs in-place with no worktree isolation, a one-off CC edit inside such a project's `localPath` leaves the tree dirty and blocks every future live-mode dispatch (the engine refuses on a dirty tree, `live-checkout-dirty`, non-retryable — and it does not track or auto-recover stray CC edits). So for any needed change in a `live`/`hybrid` project you **dispatch a work item** and let the engine own the live-checkout path; you never `Edit`/`Write`/`Bash`-mutate files there yourself, regardless of task size or a direct-handling override. Read-only inspection remains fine. This is the same rule stated up top under "HARD STOP — never edit files yourself in a live/hybrid project"; the two sections agree by design.
|
|
340
|
+
|
|
330
341
|
**When to recommend hybrid:** the project's build/test/validation genuinely cannot run in an isolated worktree (it needs the real checkout — submodules, a `repo`-managed tree, an emulator/dev-server bound to `localPath`, deep-path tooling), **but** you still want coding agents to work in parallel rather than serialize through the single live checkout. If the *whole* workflow must run on the real tree, use plain `live`. If nothing needs the real tree, stay on `worktree`.
|
|
331
342
|
|
|
332
343
|
**Configuring it** (via the projects array on `POST /api/settings` — never hand-edit `config.json`):
|