@yemi33/minions 0.1.2220 → 0.1.2222

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.
@@ -136,7 +136,7 @@ const RENDER_VERSIONS = {
136
136
  inbox: 2,
137
137
  projects: 3,
138
138
  notes: 1,
139
- prd: 2,
139
+ prd: 3,
140
140
  prs: 3,
141
141
  archivedPrds: 1,
142
142
  engine: 4,
@@ -143,9 +143,19 @@ function renderPrd(prd, prog) {
143
143
  section.innerHTML = '';
144
144
  }
145
145
 
146
+ // Color a PR status for the PRD page pills/glyphs: merged -> green,
147
+ // abandoned/closed (any terminal-non-merged) -> red, active/other -> neutral
148
+ // default text color (no accent highlight).
149
+ function _prStatusColor(status) {
150
+ if (status === 'merged') return 'var(--green)';
151
+ if (!status || status === 'active') return 'var(--text)';
152
+ return 'var(--red)';
153
+ }
154
+
146
155
  function _renderPrLink(pr, opts) {
147
156
  var size = (opts && opts.size) || '10px';
148
157
  var statusIcon = pr.status === 'merged' ? '✓' : pr.status === 'abandoned' ? '✗' : '○';
158
+ var iconColor = _prStatusColor(pr.status);
149
159
  // P-79b47b0c — render as an in-stack modal chip via renderArtifactLink
150
160
  // (was a raw <a target="_blank">). The status icon stays as a non-link
151
161
  // prefix so the chip body remains the canonical id, matching the WI/PRD
@@ -154,7 +164,7 @@ function _renderPrLink(pr, opts) {
154
164
  ? renderArtifactLink({ type: 'pr', id: pr.id, label: pr.id, title: (pr.title || '') + ' (' + (pr.status || 'active') + ')' })
155
165
  : '<code>' + escHtml(pr.id) + '</code>';
156
166
  return '<span style="font-size:' + size + ';margin-left:4px;display:inline-flex;align-items:center;gap:3px">' +
157
- '<span title="' + escHtml(pr.status || 'active') + '">' + statusIcon + '</span>' + chip + '</span>';
167
+ '<span style="color:' + iconColor + '" title="' + escHtml(pr.status || 'active') + '">' + statusIcon + '</span>' + chip + '</span>';
158
168
  }
159
169
 
160
170
  function _renderPrdWorkItemIdBadge(workItemId, prdItemId, size, padding) {
@@ -621,7 +631,7 @@ function renderPrdProgress(prog) {
621
631
  if (prs.length > 0) {
622
632
  html += '<div style="font-size:var(--text-sm);font-weight:600;color:var(--blue);margin-bottom:4px">E2E Aggregate PRs</div>';
623
633
  html += prs.map(pr => {
624
- const statusColor = pr.status === 'active' ? 'var(--green)' : pr.status === 'merged' ? 'var(--purple)' : 'var(--muted)';
634
+ const statusColor = _prStatusColor(pr.status);
625
635
  // P-79b47b0c — render PR id as in-stack chip (was raw <a target="_blank">).
626
636
  const prChip = (typeof renderArtifactLink === 'function')
627
637
  ? renderArtifactLink({ type: 'pr', id: pr.id, label: pr.id, title: pr.title || pr.id })
@@ -70,7 +70,7 @@ async function openSettings() {
70
70
  return '<tr>' +
71
71
  '<td style="font-weight:600">' + escHtml(a.emoji || '') + ' ' + escHtml(a.name || id) + '</td>' +
72
72
  '<td><input data-agent="' + escHtml(id) + '" data-field="role" value="' + escHtml(a.role || '') + '" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:var(--text-base)"></td>' +
73
- '<td><input data-agent="' + escHtml(id) + '" data-field="skills" value="' + escHtml((a.skills || []).join(', ')) + '" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:var(--text-base)"></td>' +
73
+ '<td><input data-agent="' + escHtml(id) + '" data-field="expertise" value="' + escHtml((a.expertise || []).join(', ')) + '" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:var(--text-base)"></td>' +
74
74
  '<td data-runtime-cli="' + escHtml(id) + '" style="min-width:110px">' +
75
75
  // Initial loading placeholder — initRuntimeFleetUI() replaces this with a
76
76
  // <select> populated from /api/runtimes once the registry resolves.
@@ -379,6 +379,7 @@ async function openSettings() {
379
379
  settingsField('Worktree Root', 'set-worktreeRoot', e.worktreeRoot || '../worktrees', '', 'Relative or absolute path for git worktrees; on Windows prefer a short path like C:\\wt') +
380
380
  settingsField('Assert-Clean Status Probe Timeout', 'set-assertCleanStatusTimeoutMs', e.assertCleanStatusTimeoutMs || 10000, 'ms', 'Timeout for the `git status --porcelain` preflight probe in assertCleanSharedWorktree. Raise this (e.g. 60000) on GVFS/Plastic-backed monorepos where sparse hydration runs longer than the 10s default. On timeout the engine quarantines the bad worktree and emits a RETRYABLE failure so the next dispatch starts fresh. Clamped 1000–120000ms.') +
381
381
  settingsField('Orphan-holder scan timeout', 'set-orphanHolderScanTimeoutMs', e.orphanHolderScanTimeoutMs || 5000, 'ms', 'Cap on the cross-platform scan (PowerShell / /proc walk / lsof) that identifies the OS process holding a stuck worktree\'s cwd. Bumps up only matter on heavily-loaded hosts where the holder scan races the sweep tick. Clamped 1000–30000ms.') +
382
+ settingsField('Worktree-holder reap probe timeout', 'set-statusProbeKillTimeoutMs', e.statusProbeKillTimeoutMs || 12000, 'ms', 'Windows-only. Timeout for the pre-quarantine worktree-holder reap probe (legacy git.exe-cmdline sweep + PEB-CWD scan that kills an orphaned agent process tree whose CWD pins the worktree dir, causing EBUSY on rename AND `git worktree remove --force`). The old hardcoded 2000ms was the proximate cause of the reaper being a no-op under concurrent-agent load (PowerShell cold-start + process enumeration exceeded 2s → ETIMEDOUT → reaped nothing). ETIMEDOUT is always swallowed (reap nothing; quarantine still proceeds), so raising this only widens the success window. Clamped 2000–60000ms.') +
382
383
  '</div>';
383
384
 
384
385
  const paneCopilot =
@@ -998,6 +999,7 @@ async function saveSettings() {
998
999
  ccUseWorkerPool: !!document.getElementById('set-ccUseWorkerPool')?.checked,
999
1000
  autoReapOrphanWorktreeHolders: !!document.getElementById('set-autoReapOrphanWorktreeHolders')?.checked,
1000
1001
  orphanHolderScanTimeoutMs: document.getElementById('set-orphanHolderScanTimeoutMs')?.value,
1002
+ statusProbeKillTimeoutMs: document.getElementById('set-statusProbeKillTimeoutMs')?.value,
1001
1003
  adoPollEnabled: document.getElementById('set-adoPollEnabled').checked,
1002
1004
  ghPollEnabled: document.getElementById('set-ghPollEnabled').checked,
1003
1005
  pollingPaused: document.getElementById('set-pollingPaused').checked,
package/dashboard.js CHANGED
@@ -10437,6 +10437,12 @@ What would you like to discuss or change? When you're happy, say "approve" and I
10437
10437
  // 30s ceiling (the scan runs once per orphan-sweep tick and a slow
10438
10438
  // scan must not block the rest of the sweep for minutes).
10439
10439
  orphanHolderScanTimeoutMs: [1000, 30000],
10440
+ // W-mqila0t5 — Windows worktree-holder reap probe timeout. 2s floor
10441
+ // (PowerShell cold-start + PEB enumeration needs headroom; the old
10442
+ // 2000ms hardcode was the no-op bug); 60s ceiling (the reap runs once
10443
+ // on the quarantine path and ETIMEDOUT is swallowed, so a long probe
10444
+ // never wedges dispatch — it just falls through to force-remove).
10445
+ statusProbeKillTimeoutMs: [2000, 60000],
10440
10446
  idleAlertMinutes: [1], shutdownTimeout: [30000], restartGracePeriod: [60000],
10441
10447
  meetingRoundTimeout: [60000],
10442
10448
  // W-mq066js7000fff1f-c (Gap B/C): steering safety-net knobs.
@@ -10678,7 +10684,15 @@ What would you like to discuss or change? When you're happy, say "approve" and I
10678
10684
  for (const [id, updates] of Object.entries(body.agents)) {
10679
10685
  if (!config.agents[id]) continue;
10680
10686
  if (updates.role !== undefined) config.agents[id].role = String(updates.role);
10681
- if (updates.skills !== undefined) config.agents[id].skills = Array.isArray(updates.skills) ? updates.skills : String(updates.skills).split(',').map(s => s.trim()).filter(Boolean);
10687
+ // Descriptive capability tags. Field renamed `skills` -> `expertise`
10688
+ // (issue: name collision with executable runtime skills). Accept the
10689
+ // legacy `skills` key from older clients and persist as `expertise`.
10690
+ const expertiseUpdate = updates.expertise !== undefined ? updates.expertise
10691
+ : (updates.skills !== undefined ? updates.skills : undefined);
10692
+ if (expertiseUpdate !== undefined) {
10693
+ config.agents[id].expertise = Array.isArray(expertiseUpdate) ? expertiseUpdate : String(expertiseUpdate).split(',').map(s => s.trim()).filter(Boolean);
10694
+ delete config.agents[id].skills;
10695
+ }
10682
10696
  if (updates.monthlyBudgetUsd !== undefined) {
10683
10697
  const val = updates.monthlyBudgetUsd === '' || updates.monthlyBudgetUsd === null ? undefined : Number(updates.monthlyBudgetUsd);
10684
10698
  if (val === undefined || isNaN(val)) delete config.agents[id].monthlyBudgetUsd;
package/docs/README.md CHANGED
@@ -15,7 +15,8 @@ Architecture, design proposals, and lifecycle references for people working on t
15
15
 
16
16
  - [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.
17
17
  - [command-center.md](command-center.md) — Command Center (CC) chat panel: persistent Sonnet sessions, `--resume` semantics, system-prompt invalidation, and per-tab session storage.
18
- - [specs/agent-configurability.md](specs/agent-configurability.md) — Design spec for the user-configurable Role / Agent Library: Phase 1 exposes & edits each role's definition + skills; Phase 2 adds role/agent CRUD. Covers the additive `config.roles` model, resolver tiering, migration from per-agent `charter.md`, and API/UI surface.
18
+ - [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.
19
+ - [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.
19
20
  - [completion-reports.md](completion-reports.md) — Canonical schema for the per-spawn completion JSON: trust nonce, `failure_class` enum, `noop` semantics, `retryable` / `needs_rerun` shape, and the artifacts array.
20
21
  - [constants.md](constants.md) — Cross-cutting status / type / condition constants (`WI_STATUS`, `WORK_TYPE`, `PR_STATUS`, `WATCH_CONDITION`, …) and the no-magic-strings invariant.
21
22
  - [constellation-bridge.md](constellation-bridge.md) — Read-only cross-repo bridge: `engine.constellationBridge.enabled` flag, marker-file contract, and the `minions bridge` subcommand for local debugging.
@@ -33,6 +34,7 @@ Architecture, design proposals, and lifecycle references for people working on t
33
34
  - [keep-processes.md](keep-processes.md) — `meta.keep_processes` sidecar contract: when to use it vs managed-spawn, sidecar schema, caps, and the [`engine/keep-process-sweep.js`](../engine/keep-process-sweep.js) lifecycle.
34
35
  - [live-checkout-mode.md](live-checkout-mode.md) — Per-project opt-in `checkoutMode: 'live'`: skips `git worktree add` and dispatches in-place inside `project.localPath` for `repo`-managed trees, submodule-heavy repos, deep Windows paths, and native build state. Includes the refuse-on-dirty contract and the per-project mutating-concurrency cap of 1.
35
36
  - [managed-spawn.md](managed-spawn.md) — Engine-owned long-running services (managed-spawn primitive): sidecar schema, healthcheck examples, lifecycle, dashboard API, and the WI 1 (build) → WI 2 (test) chained-validation pattern.
37
+ - [named-agents.md](named-agents.md) — The named-agent roster (Ripley / Dallas / Lambert / Rebecca / Ralph): per-agent `config.json` shape, the per-agent → `engine.*` → runtime resolution chain, how `routing.md` (not the `role`/`skills` metadata) drives dispatch, descriptive vs runtime skills, and per-agent memory files.
36
38
  - [plan-lifecycle.md](plan-lifecycle.md) — Full plan pipeline from `/plan` through PRD materialization, dispatch with dependency gating, verify task, and human archive.
37
39
  - [pr-auto-fix-dispatch.md](pr-auto-fix-dispatch.md) — Short reference table mapping each PR auto-fix / review dispatch site in `engine.js#discoverFromPrs` to its gate flag, plus the `pollingPaused` / `autoFixPaused` master kill-switches and the per-provider polling gates.
38
40
  - [pr-comment-followup.md](pr-comment-followup.md) — PR-comment follow-up dispatch contract: fix/review agents may spin off a new WI via `POST /api/work-items` with `meta.pr_followup` instead of broadening the current PR or rebutting the comment.
@@ -19,7 +19,7 @@ tick()
19
19
  2.5 runCleanup() Periodic cleanup (every 60 ticks ≈ 10min)
20
20
  2.52 sweepKeepProcesses() keep_processes TTL/dead-PID sweep (every 180 ticks)
21
21
  2.53 sweepManagedSpawn() managed_spawn TTL/dead-PID/log-rotate sweep (every 180 ticks)
22
- 2.54 pruneWorktreesPeriodic() Periodic worktree GC: in-root + out-of-root git registry sweep (every worktreePruneIntervalTicks ≈ 30 ticks; catches Windows EPERM/EBUSY stragglers and `git worktree list` entries outside worktreeRoot)
22
+ 2.54 pruneWorktreesPeriodic() Periodic worktree GC: in-root + out-of-root git registry sweep + locked-`initializing` missing-dir reclaim (every worktreePruneIntervalTicks ≈ 30 ticks; catches Windows EPERM/EBUSY stragglers, `git worktree list` entries outside worktreeRoot, and branch-bricking locked missing-dir entries plain prune can't reap)
23
23
  2.55 checkWatches() Persistent watch jobs (every 18 tick-equivalents)
24
24
  2.6 pollPrStatus() Poll ADO + GitHub for build, review, merge status (wall-clock cadence from prPollStatusEvery × tickInterval, default ≈ 12min)
25
25
  processPendingRebases() Run any rebase work queued from the previous tick
@@ -1,4 +1,17 @@
1
1
  [
2
+ {
3
+ "id": "agent-config-skills-field",
4
+ "description": "Legacy per-agent descriptive-metadata array `agents.<id>.skills` in config.json, renamed to `agents.<id>.expertise` to remove the name collision with executable runtime/harness skills (SKILL.md). The field is metadata only (capability tags like `architecture`, `bug-fixes`); nothing in the dispatch path reads it for behavior. A read-compat shim honors the old key so operator configs still carrying `skills` (and no `expertise`) keep working.",
5
+ "code": [
6
+ { "file": "engine/playbook.js", "lines": "950", "note": "buildSystemPrompt reads `agent.expertise ?? agent.skills ?? []` for the `Expertise:` identity line" },
7
+ { "file": "engine/lifecycle.js", "lines": "4620-4621", "note": "pickReReviewAgentHints reads `agent.expertise` with an `agent.skills` array fallback" },
8
+ { "file": "engine/queries.js", "lines": "731", "note": "getAgents normalizes `expertise: a.expertise ?? a.skills ?? []` so the dashboard/settings UI always receives `expertise`" },
9
+ { "file": "dashboard.js", "lines": "10708-10716", "note": "settings POST accepts a legacy `updates.skills` key, persists as `config.agents[id].expertise`, and deletes the old `skills` key" }
10
+ ],
11
+ "removalGate": "Telemetry / a config sweep across all known engines must show no persisted `config.agents.<id>.skills` key (only `expertise`) for >=30 consecutive days, confirming every operator config has been re-saved through the dashboard (which drops the legacy key) or hand-migrated.",
12
+ "targetRemovalDate": "2026-09-17",
13
+ "notes": "Safe to remove on or after 2026-09-17 (~3 release windows) once the removal gate clears. Removal scope: drop the `?? agent.skills` / `?? a.skills` fallbacks in engine/playbook.js:950, engine/lifecycle.js:4620-4621, and engine/queries.js:731; drop the `updates.skills` legacy branch + `delete config.agents[id].skills` in dashboard.js:10708-10716; and update docs/named-agents.md to stop mentioning the legacy key. Does NOT touch the unrelated executable-skills system (queries.js harnesses, runtime adapters, dashboard renderSkills, SKILL.md tooling)."
14
+ },
2
15
  {
3
16
  "id": "qa-json-sidecars",
4
17
  "location": "engine/small-state-store.js _mirrorQaRunsJson + _mirrorQaSessionsJson; engine/shared.js _qaMutator (mirror call); dashboard/js/settings.js set-qaDualWriteJson toggle",
@@ -0,0 +1,216 @@
1
+ # Named Agents — configuration, roles, and skills
2
+
3
+ Minions dispatches a small roster of **named agents** — personas like Ripley or Dallas
4
+ that own work items, open PRs, and review each other's changes. This doc explains who
5
+ they are, the full per-agent config shape, how work actually routes to one of them, what
6
+ the descriptive `expertise` tags and the real executable `skills` are (and why they used to
7
+ share a name), and how each agent accrues personal memory across dispatches.
8
+
9
+ > Orientation only — the load-bearing contracts live in code. Cross-references:
10
+ > [`CLAUDE.md`](../CLAUDE.md), [`.github/copilot-instructions.md`](../.github/copilot-instructions.md),
11
+ > [`docs/runtime-adapters.md`](runtime-adapters.md), [`docs/workspace-manifests.md`](workspace-manifests.md),
12
+ > [`docs/team-memory.md`](team-memory.md), and [`docs/skills.md`](skills.md).
13
+
14
+ ## 1. The roster
15
+
16
+ The five named agents are defined under `agents` in `config.json` (gitignored; the
17
+ shipped `config.template.json` carries no agents — operators populate the roster locally).
18
+ Each entry is keyed by a lowercase agent id (`ripley`, `dallas`, …).
19
+
20
+ | Id | Name | Emoji | Role | `expertise` (descriptive) | `cli` |
21
+ |----|------|-------|------|------------------------|-------|
22
+ | `ripley` | Ripley | 🏗️ | Lead / Explorer | `architecture`, `codebase-exploration`, `design-review` | `copilot` |
23
+ | `dallas` | Dallas | 🔧 | Engineer | `implementation`, `typescript`, `docker`, `testing` | `copilot` |
24
+ | `lambert` | Lambert | 📊 | Analyst | `gap-analysis`, `requirements`, `documentation` | `copilot` |
25
+ | `rebecca` | Rebecca | 🧠 | Architect | `system-design`, `api-design`, `scalability`, `implementation` | `copilot` |
26
+ | `ralph` | Ralph | ⚙️ | Engineer | `implementation`, `bug-fixes`, `testing`, `scaffolding` | `copilot` |
27
+
28
+ The `name`, `emoji`, `role`, and `expertise` fields are **human-facing metadata** — they
29
+ label the agent in the dashboard and document intent. They do **not** drive dispatch (see
30
+ §3) and the `expertise` array is not executable (see §4).
31
+
32
+ ## 2. How an agent is configured
33
+
34
+ The full per-agent config shape supported under `config.agents.<id>`:
35
+
36
+ | Field | Type | Meaning |
37
+ |-------|------|---------|
38
+ | `name` | string | Display name (e.g. `"Ripley"`). |
39
+ | `role` | string | Human-facing role label (e.g. `"Lead / Explorer"`). Metadata only. |
40
+ | `emoji` | string | Dashboard glyph. |
41
+ | `expertise` | string[] | Descriptive capability tags. Metadata only — see §4. Legacy configs that still use the old key name `skills` are read transparently. |
42
+ | `cli` | string | Runtime CLI for this agent's spawns: `copilot`, `claude`, or `codex`. |
43
+ | `model` | string | Per-agent model override (omitted → runtime picks its own default). |
44
+ | `maxBudgetUsd` | number | Per-spawn USD cap. **`0` is a valid cap** (read-only / dry-run agents), not "unlimited". |
45
+ | `bareMode` | boolean | Run Claude in `--bare` mode (Claude-runtime knob). |
46
+ | `hermeticHarness` | boolean | Strip user-scope skill/command/MCP roots from the worktree; `--add-dir` resolves to `[minionsDir]` only. |
47
+ | `workspace_manifest` | object | Optional permission scoping — see below. |
48
+
49
+ ### Three-tier resolution chain
50
+
51
+ Most knobs resolve **per-agent override → `engine.*` fleet default → runtime default**.
52
+ The resolver helpers live in [`engine/shared.js`](../engine/shared.js):
53
+
54
+ | Helper | Chain |
55
+ |--------|-------|
56
+ | `resolveAgentCli(agent, engine)` | `agent.cli` → `engine.defaultCli` → `ENGINE_DEFAULTS.defaultCli` (`'copilot'`). |
57
+ | `resolveAgentModel(agent, engine)` | `agent.model` → `engine.defaultModel` → `undefined` (adapter omits `--model`, CLI uses its own default). |
58
+ | `resolveAgentMaxBudget(agent, engine)` | `agent.maxBudgetUsd` → `engine.maxBudgetUsd` → `undefined` (no cap). Nullish-coalesced so `0` is honored. |
59
+ | `resolveAgentBareMode(agent, engine)` | `agent.bareMode` → `engine.claudeBareMode` → `false`. Strict null check so a per-agent `false` overrides an engine `true`. |
60
+ | `resolveAgentHermeticHarness(agent, engine)` | `agent.hermeticHarness` → `engine.hermeticHarness` → `false`. Same strict-override semantics. |
61
+
62
+ Empty string / `null` / `undefined` all count as "unset" for the string knobs, so an agent
63
+ inherits the fleet default rather than blanking the value.
64
+
65
+ ### Agent vs Command Center independence
66
+
67
+ The Command Center (CC) / doc-chat path is a fleet-wide singleton with **no notion of
68
+ "which agent"**, so it resolves through separate helpers: `resolveCcCli(engine)`
69
+ (`engine.ccCli` → `engine.defaultCli` → fallback) and `resolveCcModel(engine)`
70
+ (`engine.ccModel` → `engine.defaultModel` → `undefined`). These **do not cross over**:
71
+ `ccCli` / `ccModel` never affect a named-agent spawn, and `agent.cli` / `agent.model` never
72
+ affect CC. Both paths share the `engine.defaultCli` / `engine.defaultModel` fleet defaults,
73
+ but only as the middle tier.
74
+
75
+ ### Workspace manifest (optional)
76
+
77
+ `agents.<id>.workspace_manifest` declares declarative permission scoping. Shape and
78
+ defaults (`WORKSPACE_MANIFEST_DEFAULTS` in `engine/shared.js`):
79
+
80
+ | Key | Default | Effect |
81
+ |-----|---------|--------|
82
+ | `allowed_tools` | `null` (permissive) | Whitelist of tool names; merged into the runtime `--allowedTools` flag. `[]` = deny-all. |
83
+ | `allowed_repos` | `null` (permissive) | Repo allow-list; enforced at dispatch time. `[]` = deny-all. |
84
+ | `allowed_external_urls` | `null` (permissive) | URL allow-list (supports `*.example.com` wildcards). |
85
+ | `memory_scope` | `'shared'` | One of `private`, `shared`, `read-only-shared`. |
86
+
87
+ Enforcement helpers (also in `engine/shared.js`): `validateWorkspaceManifest`,
88
+ `resolveAgentManifest`, `agentCanUseRepo`, `agentCanUseTool`, `agentCanFetchUrl`,
89
+ `agentMemoryScope`, `mergeManifestAllowedTools`. Today the engine actively enforces (a) the
90
+ **repo gate** at dispatch time in `engine.js spawnAgent`
91
+ (`FAILURE_CLASS.WORKSPACE_MANIFEST_REPO`, non-retryable) and (b) the **tool merge** into the
92
+ runtime `--allowedTools` flag at spawn time. Defaults are permissive — an agent with no
93
+ manifest is unaffected. Full schema and rollout guidance:
94
+ [`docs/workspace-manifests.md`](workspace-manifests.md).
95
+
96
+ ## 3. How roles are defined / how work routes to an agent
97
+
98
+ The `role` and `expertise` config fields are **descriptive metadata**. The actual dispatch
99
+ decision is **data-driven by [`routing.md`](../routing.md)**, which `engine.js` parses — so
100
+ keep that file's table format exact.
101
+
102
+ Routing maps a `work_type` to a `preferred` agent and a `fallback`:
103
+
104
+ | Work Type | Preferred | Fallback |
105
+ |-----------|-----------|----------|
106
+ | implement | dallas | ralph |
107
+ | implement:large | rebecca | dallas |
108
+ | review | ripley | lambert |
109
+ | fix | `_author_` | `_any_` |
110
+ | plan | ripley | rebecca |
111
+ | plan-to-prd | lambert | rebecca |
112
+ | explore | ripley | rebecca |
113
+ | test | dallas | ralph |
114
+ | ask | ripley | rebecca |
115
+ | verify | dallas | ralph |
116
+ | decompose | ripley | rebecca |
117
+ | meeting | ripley | lambert |
118
+ | docs | lambert | `_any_` |
119
+ | setup | dallas | `_any_` |
120
+ | qa-validate / qa-session-* | dallas | ralph |
121
+
122
+ **Sentinels.** `_author_` routes a `fix` (review-feedback) item back to the PR author for
123
+ context; `_any_` routes to any available idle agent (lowest error rate first).
124
+ `implement:large` is selected for items with `estimated_complexity: "large"`. If both
125
+ preferred and fallback are busy, the engine falls back to any idle agent.
126
+
127
+ **Dispatch rules** (from `routing.md`):
128
+
129
+ - **Eager by default** — the engine spawns every agent that can start work, not one at a time.
130
+ - **No self-review** — auto review dispatch always picks a non-author. When no non-author
131
+ reviewer is idle, the dispatch waits in `dispatch.pending` with
132
+ `_pendingReason: 'no_non_author_reviewer'` and promotes automatically once one frees up.
133
+ An explicit operator dispatch (`agent: <author>`) is honored as an override but logged as
134
+ a warning.
135
+ - **Routing selects an owner; it does not narrow the task.** The assigned agent behaves as
136
+ if the user typed the same task into a CLI directly — Minions adds only safety, status,
137
+ and review guardrails.
138
+
139
+ ### Per-agent retry reassignment
140
+
141
+ Total retries are capped by `ENGINE_DEFAULTS.maxRetries` (default 3). Separately, each work
142
+ item tracks per-agent attempts in `_retriesByAgent: { agentId: count }`. When the **same**
143
+ agent fails the **same** item `maxRetriesPerAgent` times (default 2; overridable via
144
+ `engine.maxRetriesPerAgent` and the dashboard Settings field), discovery force-reassigns to
145
+ a different eligible agent via `resolveAgent(workType, config, { excludeAgent })`. If no
146
+ alternate is available it stays with the same agent and dedups an engine inbox note. Setting
147
+ `agentLock: true` (or `hardAgent: true`) hard-pins the agent and bypasses reassignment —
148
+ operator intent wins. The counter is cleared on successful completion alongside
149
+ `_retryCount`. Helpers: `bumpAgentRetryCount`, `getAgentRetryCount`,
150
+ `resolveMaxRetriesPerAgent` in `engine/shared.js`.
151
+
152
+ ## 4. How skills are defined
153
+
154
+ Historically "skills" meant two unrelated things in Minions. The descriptive per-agent
155
+ metadata array was renamed `skills` → `expertise` to remove that collision, so the only
156
+ "skills" left are the real executable ones. The two concepts to keep separate:
157
+
158
+ **(a) The descriptive `expertise` array in config** (`agents.<id>.expertise`). A list of
159
+ capability tags like `architecture` or `bug-fixes`. This is **metadata only** — labels for
160
+ humans, not executable behavior. Nothing in the dispatch path reads it. (This field was
161
+ formerly named `skills`; legacy configs that still use that key are read transparently via
162
+ an `expertise ?? skills` fallback — see [`docs/deprecated.json`](deprecated.json).)
163
+
164
+ **(b) Real runtime skills** — `SKILL.md` files the runtime CLI actually loads. These are
165
+ auto-extracted from agent output: when an agent emits a fenced ```` ```skill ```` block
166
+ (with YAML frontmatter carrying at least a `name`), `extractSkillsFromOutput` in
167
+ [`engine/lifecycle.js`](../engine/lifecycle.js) writes it out. The engine also scans
168
+ `notes/inbox/` notes for skill blocks, since agents often write them there instead of stdout.
169
+
170
+ - **`scope: minions`** (the default when `scope` is omitted) → written to the runtime's
171
+ **personal** skill directory as `<name>/SKILL.md`, so it becomes a user-level skill
172
+ available in normal runtime windows too.
173
+ - **`scope: project`** (with a `project:` field) → the engine queues a low-priority
174
+ `implement` work item to **PR the skill into the project repo's** skill directory rather
175
+ than writing it directly.
176
+
177
+ Near-duplicate names are flagged to `engine/skills-flagged.json` and skipped (no existing
178
+ skill is deleted or renamed). See [`docs/skills.md`](skills.md) for the block format.
179
+
180
+ **Runtime differences.** The destination directory is runtime-specific
181
+ (`getSkillRoots` / `getSkillWriteTargets` in the runtime adapters under
182
+ [`engine/runtimes/`](../engine/runtimes/)):
183
+
184
+ | Runtime | Personal write target | Project write target | Also reads |
185
+ |---------|-----------------------|----------------------|------------|
186
+ | Claude (`claude.js`) | `~/.claude/skills/` | `<repo>/.claude/skills/` | `~/.agents/skills/` |
187
+ | Copilot (`copilot.js`) | `~/.copilot/skills/` | `<repo>/.github/skills/` | `~/.copilot`, `~/.agents/skills/` |
188
+
189
+ The capability matrix for each runtime is in [`docs/runtime-adapters.md`](runtime-adapters.md).
190
+
191
+ ## 5. Per-agent memory & charters
192
+
193
+ Each named agent has a personal memory file at
194
+ `knowledge/agents/<agentId>.md` (e.g. `knowledge/agents/dallas.md`) — a personal notebook
195
+ injected into that agent's prompt on every dispatch. The format convention is documented in
196
+ [`knowledge/agents/README.md`](../knowledge/agents/README.md).
197
+
198
+ **Written exclusively by the consolidation pipeline.** `appendToAgentMemory` in
199
+ [`engine/consolidation.js`](../engine/consolidation.js) routes each `notes/inbox/` finding
200
+ to its author's file. Authorship comes from the inbox note's YAML frontmatter `agent:`
201
+ field, with the **filename prefix** (`<agent>-…md`) as fallback (`extractInboxAgent`). Key
202
+ properties:
203
+
204
+ - **Append-only** — agents must not edit their own memory files directly; write to
205
+ `notes/inbox/` and let the sweep route it.
206
+ - **~25 KB cap** (`AGENT_MEMORY_BUDGET_BYTES`) — oldest sections pruned at section
207
+ boundaries to stay under budget.
208
+ - **Only configured `config.agents` keys get a file** — the agent must be in the known-team
209
+ set; `temp-*` ids are skipped. This is a strict superset of broadcast consolidation: the
210
+ shared `notes.md` digest still happens; per-agent routing is *in addition*.
211
+
212
+ **Prompt injection order.** [`engine/playbook.js`](../engine/playbook.js) injects, in this
213
+ order, `pinned.md` → `notes.md` → `knowledge/agents/<agentId>.md` (when present). Missing
214
+ files are silently skipped. The per-agent file is fenced as `<UNTRUSTED-INPUT>` and capped
215
+ to the notes prompt budget before injection. The companion read-side reference is
216
+ [`docs/team-memory.md`](team-memory.md).