@yemi33/minions 0.1.1981 → 0.1.1982
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/docs/managed-spawn.md +3 -2
- package/engine/managed-spawn.js +34 -3
- package/engine/shared.js +108 -17
- package/package.json +1 -1
package/docs/managed-spawn.md
CHANGED
|
@@ -35,7 +35,7 @@ The sidecar lives at `<MINIONS_DIR>/agents/<agentId>/managed-spawn.json` and is
|
|
|
35
35
|
"name": "constellation-host", // kebab-case, ≤64 chars, unique within file
|
|
36
36
|
"cmd": "bun", // must be on engine.managedSpawn.executableAllowlist
|
|
37
37
|
"args": ["run", "dev"], // ≤64 entries
|
|
38
|
-
"cwd": "D:/repos/constellation", // must be a real git worktree (requireGitWorkdir: true)
|
|
38
|
+
"cwd": "D:/repos/constellation", // must be inside a real git worktree (requireGitWorkdir: true) — monorepo subdirs ok, ancestor walked up to gitWorktreeMaxParentDepth parents
|
|
39
39
|
"env": { "CONSTELLATION_SERVER": "http://localhost:3000" }, // ≤32 keys; POSIX-shape + denylist enforced
|
|
40
40
|
"ports": [3001], // 1024-65535; ≤20 per spec; advisory only (engine doesn't bind)
|
|
41
41
|
"ttl_minutes": 240, // ≤1440 (24h hard cap); defaults to 240 (4h)
|
|
@@ -217,7 +217,8 @@ All knobs live under `engine.managedSpawn` in `engine/shared.js:1500` (`ENGINE_D
|
|
|
217
217
|
| `logRotateBytes` | `10485760` | Rotation threshold for `<name>.log`. |
|
|
218
218
|
| `bootReconcileMaxMs` | `2000` | Boot-time reconcile hard timeout. |
|
|
219
219
|
| `promptContextMaxBytes` | `2048` | Auto-injected `## Live managed processes` block cap. |
|
|
220
|
-
| `requireGitWorkdir` | `true` | Reject specs whose `cwd` isn't a git worktree. |
|
|
220
|
+
| `requireGitWorkdir` | `true` | Reject specs whose `cwd` isn't inside a git worktree (root or any ancestor up to `gitWorktreeMaxParentDepth`). |
|
|
221
|
+
| `gitWorktreeMaxParentDepth` | `6` | How many parent directories `shared.isValidGitWorktree` walks when probing for `.git`. Lets monorepo specs pin a per-package `cwd` (`<root>/packages/<pkg>/...`); set to `0` to disable the walk. |
|
|
221
222
|
| `executableAllowlist` | `[node, bun, npm, …]` | Single global. Applies to `spec.cmd` AND `command` healthcheck `cmd`. |
|
|
222
223
|
| `envKeyDenyPatterns` | `[^AWS_, ^AZURE_, _SECRET, _TOKEN, _API_KEY, …]` | Regex source strings, matched case-insensitively. Keys matching ANY pattern are rejected unless exact-listed in `envKeyDenyOverrides`. Threat model: credential leakage, not env-key enumeration — plain project vars (`CONSTELLATION_SERVER`, `DATABASE_URL`, …) pass with no config. |
|
|
223
224
|
| `envKeyDenyOverrides` | `[AWS_REGION, AWS_DEFAULT_REGION, AZURE_REGION, GCP_REGION, AWS_PROFILE]` | Exact-match exemptions for known-safe keys that would otherwise be caught by a broad prefix pattern. Case-sensitive. |
|
package/engine/managed-spawn.js
CHANGED
|
@@ -286,9 +286,20 @@ function _validateSpec(spec, index, limits, opts) {
|
|
|
286
286
|
return { ok: false, reason: 'cwd-too-long' };
|
|
287
287
|
}
|
|
288
288
|
if (_resolveRequireGitWorkdir(opts) && typeof spec.cwd === 'string' && spec.cwd.length > 0) {
|
|
289
|
-
const wt = shared.isValidGitWorktree(spec.cwd);
|
|
289
|
+
const wt = shared.isValidGitWorktree(spec.cwd, { memo: opts._gitWorktreeMemo });
|
|
290
290
|
if (!wt.ok) {
|
|
291
|
-
|
|
291
|
+
// W-mpbpa01y000qcdc2 — enrich the reject reason so the dispatcher prompt
|
|
292
|
+
// can distinguish a stale `cwd` (typoed / belonged to a torn-down
|
|
293
|
+
// worktree) from a real subdir whose `.git` lives at an ancestor. The
|
|
294
|
+
// legacy substring `invalid-workdir:` is preserved so the engine close-
|
|
295
|
+
// handler gate still matches.
|
|
296
|
+
const detail = [
|
|
297
|
+
wt.reason,
|
|
298
|
+
'cwd-exists:' + (wt.exists ? 'true' : 'false'),
|
|
299
|
+
'nearest-ancestor:' + (wt.nearestGitAncestor || 'null'),
|
|
300
|
+
'worktree-root:' + (wt.worktreeRoot || 'null'),
|
|
301
|
+
].join(' | ');
|
|
302
|
+
return { ok: false, reason: INVALID_WORKDIR_REASON_PREFIX + detail };
|
|
292
303
|
}
|
|
293
304
|
}
|
|
294
305
|
|
|
@@ -420,10 +431,14 @@ function validateManagedSpawnRecord(parsed, opts) {
|
|
|
420
431
|
try { projects = shared.getProjects(); }
|
|
421
432
|
catch (_e) { projects = []; }
|
|
422
433
|
}
|
|
423
|
-
const specOpts = Object.assign({}, opts, { projects: projects });
|
|
434
|
+
const specOpts = Object.assign({}, opts, { projects: projects, _gitWorktreeMemo: new Map() });
|
|
424
435
|
|
|
425
436
|
const seen = new Set();
|
|
426
437
|
const out = [];
|
|
438
|
+
// W-mpbpa01y000qcdc2 — share a memo across all specs in this file so
|
|
439
|
+
// adjacent `cwd`s under the same worktree root don't re-stat the same
|
|
440
|
+
// ancestors. Threaded through `opts._gitWorktreeMemo` into `_validateSpec`,
|
|
441
|
+
// then into `shared.isValidGitWorktree`.
|
|
427
442
|
for (let i = 0; i < parsed.specs.length; i++) {
|
|
428
443
|
const v = _validateSpec(parsed.specs[i], i, limits, specOpts);
|
|
429
444
|
if (!v.ok) {
|
|
@@ -645,6 +660,22 @@ function buildManagedSpawnHint(opts) {
|
|
|
645
660
|
'',
|
|
646
661
|
'A passing smoke-test is the entry gate to writing the sidecar — not a nice-to-have. If you skip it, you are betting the WI completion against a command you never ran.',
|
|
647
662
|
'',
|
|
663
|
+
'### Monorepo `cwd` tip (W-mpbpa01y000qcdc2)',
|
|
664
|
+
'',
|
|
665
|
+
'**Only worktree ROOTS have a `.git` entry.** In a multi-package repo (yarn workspaces, pnpm, bun workspaces, lerna, lage, turborepo), `packages/<pkg>/` does NOT contain its own `.git` — the engine\'s workdir validator walks up to ' + (limits.gitWorktreeMaxParentDepth || 6) + ' parent directories looking for one, so a per-package `cwd` IS accepted, but the **canonical recipe** is to keep `cwd` at the worktree root and target the package via the runtime\'s workspace flag:',
|
|
666
|
+
'',
|
|
667
|
+
'```jsonc',
|
|
668
|
+
'{',
|
|
669
|
+
' "name": "constellation-host",',
|
|
670
|
+
' "cmd": "bun",',
|
|
671
|
+
' "args": ["-F", "@scope/server", "run", "dev"], // bun workspace filter',
|
|
672
|
+
' "cwd": "<worktree-root>", // NOT <worktree-root>/packages/server',
|
|
673
|
+
' "healthcheck": { "type": "http", "url": "http://localhost:3001/health", "expect_status": 200, "interval_s": 1, "timeout_s": 60 }',
|
|
674
|
+
'}',
|
|
675
|
+
'```',
|
|
676
|
+
'',
|
|
677
|
+
'Equivalent flags for other runtimes: `pnpm --filter <pkg>`, `yarn workspace <pkg>`, `npm run -w <pkg>`, `lage run <task> --to <pkg>`. Keeping `cwd` at the root makes the spec portable across machines (no hard-coded package path) and avoids per-package `node_modules` resolution surprises.',
|
|
678
|
+
'',
|
|
648
679
|
'### Verify before exit',
|
|
649
680
|
'',
|
|
650
681
|
'After you write the file, query the engine to confirm acceptance:',
|
package/engine/shared.js
CHANGED
|
@@ -1089,11 +1089,20 @@ function validateGhSlug(slug) {
|
|
|
1089
1089
|
return slug;
|
|
1090
1090
|
}
|
|
1091
1091
|
|
|
1092
|
-
// W-mp6k7ywi000fa33c — pure helper. Returns
|
|
1093
|
-
//
|
|
1094
|
-
//
|
|
1095
|
-
//
|
|
1096
|
-
//
|
|
1092
|
+
// W-mp6k7ywi000fa33c — pure helper. Returns:
|
|
1093
|
+
// { ok: boolean,
|
|
1094
|
+
// reason?: string,
|
|
1095
|
+
// exists: boolean, // whether dirPath itself exists on disk
|
|
1096
|
+
// nearestGitAncestor: string|null, // path to nearest ancestor .git found
|
|
1097
|
+
// worktreeRoot: string|null, // dir that owns the nearest .git
|
|
1098
|
+
// depth: number // 0 = dirPath itself; >0 = parents walked; -1 = not found
|
|
1099
|
+
// }
|
|
1100
|
+
// `ok: true` when `dirPath` (or one of its ancestors up to a configurable
|
|
1101
|
+
// depth) contains either a `.git` directory OR a `.git` worktree pointer
|
|
1102
|
+
// file (a real file whose first line starts with `gitdir:`). The ancestor
|
|
1103
|
+
// walk lets monorepo agents pin a per-package `cwd` (e.g.
|
|
1104
|
+
// `<worktree-root>/packages/server`) — only the worktree root carries `.git`
|
|
1105
|
+
// but the package subdir is still inside a real worktree (W-mpbpa01y000qcdc2).
|
|
1097
1106
|
//
|
|
1098
1107
|
// No shelling out (no `git rev-parse`); just `fs.existsSync`/`fs.statSync`
|
|
1099
1108
|
// and a tiny content sniff for the worktree pointer case. This catches the
|
|
@@ -1101,32 +1110,108 @@ function validateGhSlug(slug) {
|
|
|
1101
1110
|
// `cp -r`) instead of `git worktree add`, which produced a directory that
|
|
1102
1111
|
// looks file-by-file like a worktree but has no git linkage. See
|
|
1103
1112
|
// W-mp6ha6q9000d58a5 for the real-world incident this prevents.
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1113
|
+
//
|
|
1114
|
+
// Options:
|
|
1115
|
+
// maxParentDepth — number of parent directories to walk after the direct
|
|
1116
|
+
// probe; defaults to ENGINE_DEFAULTS.managedSpawn.gitWorktreeMaxParentDepth
|
|
1117
|
+
// (6). Pass 0 to disable the walk and restore the legacy behavior.
|
|
1118
|
+
// memo — optional Map shared across multiple calls (e.g. validating N
|
|
1119
|
+
// specs in one sidecar file) so adjacent paths don't re-stat the same
|
|
1120
|
+
// ancestors. Keys are absolute path strings; values are result objects.
|
|
1121
|
+
function _probeGitAtDir(dirPath) {
|
|
1108
1122
|
let dirStat;
|
|
1109
1123
|
try { dirStat = fs.statSync(dirPath); }
|
|
1110
|
-
catch (_e) { return { ok: false, reason: 'directory does not exist: ' + dirPath }; }
|
|
1124
|
+
catch (_e) { return { ok: false, exists: false, reason: 'directory does not exist: ' + dirPath }; }
|
|
1111
1125
|
if (!dirStat.isDirectory()) {
|
|
1112
|
-
return { ok: false, reason: 'path is not a directory: ' + dirPath };
|
|
1126
|
+
return { ok: false, exists: true, reason: 'path is not a directory: ' + dirPath };
|
|
1113
1127
|
}
|
|
1114
1128
|
const gitPath = path.join(dirPath, '.git');
|
|
1115
1129
|
let gitStat;
|
|
1116
1130
|
try { gitStat = fs.statSync(gitPath); }
|
|
1117
|
-
catch (_e) { return { ok: false, reason: 'no .git directory or worktree pointer at ' + dirPath }; }
|
|
1118
|
-
if (gitStat.isDirectory()) return { ok: true };
|
|
1131
|
+
catch (_e) { return { ok: false, exists: true, reason: 'no .git directory or worktree pointer at ' + dirPath }; }
|
|
1132
|
+
if (gitStat.isDirectory()) return { ok: true, exists: true, gitPath: gitPath };
|
|
1119
1133
|
if (gitStat.isFile()) {
|
|
1120
1134
|
// Worktree pointer files contain "gitdir: <abs path>" on the first line.
|
|
1121
1135
|
// A `.git` file that doesn't match this shape is a normal file, not a
|
|
1122
1136
|
// valid worktree linkage — reject it.
|
|
1123
1137
|
let head = '';
|
|
1124
1138
|
try { head = fs.readFileSync(gitPath, { encoding: 'utf8', flag: 'r' }).slice(0, 256); }
|
|
1125
|
-
catch (e) { return { ok: false, reason: '.git file unreadable: ' + e.message }; }
|
|
1126
|
-
if (/^gitdir:\s*\S/.test(head)) return { ok: true };
|
|
1127
|
-
return { ok: false, reason: '.git file present but not a worktree pointer (no "gitdir:" prefix): ' + dirPath };
|
|
1139
|
+
catch (e) { return { ok: false, exists: true, reason: '.git file unreadable: ' + e.message }; }
|
|
1140
|
+
if (/^gitdir:\s*\S/.test(head)) return { ok: true, exists: true, gitPath: gitPath };
|
|
1141
|
+
return { ok: false, exists: true, reason: '.git file present but not a worktree pointer (no "gitdir:" prefix): ' + dirPath };
|
|
1142
|
+
}
|
|
1143
|
+
return { ok: false, exists: true, reason: '.git entry is neither a file nor a directory: ' + gitPath };
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
function isValidGitWorktree(dirPath, opts) {
|
|
1147
|
+
if (typeof dirPath !== 'string' || dirPath.length === 0) {
|
|
1148
|
+
return { ok: false, exists: false, nearestGitAncestor: null, worktreeRoot: null, depth: -1, reason: 'cwd missing or not a string' };
|
|
1149
|
+
}
|
|
1150
|
+
opts = opts || {};
|
|
1151
|
+
const limits = (ENGINE_DEFAULTS && ENGINE_DEFAULTS.managedSpawn) || {};
|
|
1152
|
+
const defaultDepth = Number.isFinite(limits.gitWorktreeMaxParentDepth) ? limits.gitWorktreeMaxParentDepth : 6;
|
|
1153
|
+
const maxDepth = Math.max(0, Number.isFinite(opts.maxParentDepth) ? opts.maxParentDepth : defaultDepth);
|
|
1154
|
+
const memo = opts.memo instanceof Map ? opts.memo : null;
|
|
1155
|
+
|
|
1156
|
+
if (memo && memo.has(dirPath)) return memo.get(dirPath);
|
|
1157
|
+
|
|
1158
|
+
const direct = _probeGitAtDir(dirPath);
|
|
1159
|
+
if (direct.ok) {
|
|
1160
|
+
const out = { ok: true, exists: true, nearestGitAncestor: direct.gitPath, worktreeRoot: dirPath, depth: 0 };
|
|
1161
|
+
if (memo) memo.set(dirPath, out);
|
|
1162
|
+
return out;
|
|
1128
1163
|
}
|
|
1129
|
-
|
|
1164
|
+
|
|
1165
|
+
// Don't walk ancestors when the path itself doesn't exist — that's a stale
|
|
1166
|
+
// / typoed cwd, not a monorepo subdir, and walking would mask the real
|
|
1167
|
+
// problem. The legacy "directory does not exist" reason is preserved.
|
|
1168
|
+
if (!direct.exists) {
|
|
1169
|
+
const out = { ok: false, exists: false, nearestGitAncestor: null, worktreeRoot: null, depth: -1, reason: direct.reason };
|
|
1170
|
+
if (memo) memo.set(dirPath, out);
|
|
1171
|
+
return out;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// Walk parents up to maxDepth, looking for any ancestor whose direct probe
|
|
1175
|
+
// succeeds. Reuse the memo for previously-seen ancestors (matters when
|
|
1176
|
+
// validating several specs that share a common worktree-root prefix).
|
|
1177
|
+
let cur = dirPath;
|
|
1178
|
+
let walked = 0;
|
|
1179
|
+
for (let i = 1; i <= maxDepth; i++) {
|
|
1180
|
+
const parent = path.dirname(cur);
|
|
1181
|
+
if (parent === cur) break; // hit the filesystem root
|
|
1182
|
+
walked = i;
|
|
1183
|
+
if (memo && memo.has(parent)) {
|
|
1184
|
+
const cached = memo.get(parent);
|
|
1185
|
+
if (cached.ok) {
|
|
1186
|
+
const out = { ok: true, exists: true, nearestGitAncestor: cached.nearestGitAncestor, worktreeRoot: cached.worktreeRoot, depth: i };
|
|
1187
|
+
memo.set(dirPath, out);
|
|
1188
|
+
return out;
|
|
1189
|
+
}
|
|
1190
|
+
cur = parent;
|
|
1191
|
+
continue;
|
|
1192
|
+
}
|
|
1193
|
+
const probe = _probeGitAtDir(parent);
|
|
1194
|
+
if (probe.ok) {
|
|
1195
|
+
const ancestorResult = { ok: true, exists: true, nearestGitAncestor: probe.gitPath, worktreeRoot: parent, depth: 0 };
|
|
1196
|
+
if (memo) memo.set(parent, ancestorResult);
|
|
1197
|
+
const out = { ok: true, exists: true, nearestGitAncestor: probe.gitPath, worktreeRoot: parent, depth: i };
|
|
1198
|
+
if (memo) memo.set(dirPath, out);
|
|
1199
|
+
return out;
|
|
1200
|
+
}
|
|
1201
|
+
cur = parent;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
// No ancestor satisfied the .git probe. Synthesize a reason that keeps the
|
|
1205
|
+
// direct-probe detail (so legacy substring assertions like "no .git" and
|
|
1206
|
+
// "not a worktree pointer" still match) AND adds the ancestor-walk context
|
|
1207
|
+
// the dispatcher prompt needs to distinguish "stale path" from
|
|
1208
|
+
// "real subdir of an unrelated dir".
|
|
1209
|
+
const reason = direct.reason
|
|
1210
|
+
+ ' (and no .git directory or worktree pointer in any of '
|
|
1211
|
+
+ walked + ' parent directories up to depth ' + maxDepth + ')';
|
|
1212
|
+
const out = { ok: false, exists: true, nearestGitAncestor: null, worktreeRoot: null, depth: -1, reason: reason };
|
|
1213
|
+
if (memo) memo.set(dirPath, out);
|
|
1214
|
+
return out;
|
|
1130
1215
|
}
|
|
1131
1216
|
|
|
1132
1217
|
function shellSafeGh(args, opts = {}) {
|
|
@@ -1513,6 +1598,12 @@ const ENGINE_DEFAULTS = {
|
|
|
1513
1598
|
bootReconcileMaxMs: 2000, // boot-time reconcile timeout (don't block engine boot)
|
|
1514
1599
|
promptContextMaxBytes: 2048, // cap on auto-injected `## Live managed processes` block
|
|
1515
1600
|
requireGitWorkdir: true, // reject specs whose `cwd` isn't a real git worktree
|
|
1601
|
+
// W-mpbpa01y000qcdc2 — how many parent directories `isValidGitWorktree`
|
|
1602
|
+
// walks before giving up. In monorepos only the worktree ROOT has `.git`;
|
|
1603
|
+
// a per-package `cwd` like `<root>/packages/server` needs the walk to
|
|
1604
|
+
// succeed. 6 covers `packages/<pkg>/src/<sub>/<sub>/<sub>` without
|
|
1605
|
+
// walking out of any realistic project; set to 0 to disable.
|
|
1606
|
+
gitWorktreeMaxParentDepth: 6,
|
|
1516
1607
|
// Single global executable allowlist. Applies to both `spec.cmd` and any
|
|
1517
1608
|
// `command` healthcheck's `cmd`. Keep narrow — adding a binary here lets
|
|
1518
1609
|
// any agent's sidecar invoke it under engine ownership.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1982",
|
|
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"
|