@yemi33/minions 0.1.2179 → 0.1.2180
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/README.md +7 -5
- package/bin/minions.js +15 -6
- package/dashboard/js/memory-panel.js +62 -0
- package/dashboard/js/refresh.js +18 -0
- package/dashboard/js/render-other.js +142 -1
- package/dashboard/js/render-work-items.js +18 -1
- package/dashboard/js/settings.js +23 -0
- package/dashboard/pages/engine-memory-panel.html +7 -0
- package/dashboard/pages/tools.html +8 -0
- package/dashboard.js +466 -3
- package/docs/diagnostics-memory.md +446 -0
- package/docs/harness-propagation.md +273 -0
- package/docs/human-vs-automated.md +1 -1
- package/docs/runtime-adapters.md +5 -0
- package/engine/cli.js +24 -5
- package/engine/preflight.js +265 -0
- package/engine/queries.js +192 -15
- package/engine/runtimes/claude.js +36 -0
- package/engine/runtimes/codex.js +19 -0
- package/engine/runtimes/copilot.js +27 -36
- package/engine/shared.js +277 -13
- package/engine/spawn-agent.js +178 -12
- package/engine.js +232 -3
- package/package.json +1 -1
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# Harness Propagation
|
|
2
|
+
|
|
3
|
+
> Status: contract doc for the **seamless user/repo harness invocation** plan
|
|
4
|
+
> (`plans/seamless-user-repo-harness-invocation.md`, PRD
|
|
5
|
+
> `prd/minions-opg-2026-06-10-4.json`). This is the foundation item — it nails
|
|
6
|
+
> down the *current* propagation surfaces and the worktree footgun so the
|
|
7
|
+
> rest of the plan can extend them without reinventing the contract. Read this
|
|
8
|
+
> before adding a new adapter method, a new asset kind, or a new opt-out flag.
|
|
9
|
+
|
|
10
|
+
## What is a "harness"
|
|
11
|
+
|
|
12
|
+
In this doc, "harness" means any user-installed or repo-local asset that the
|
|
13
|
+
runtime CLI auto-discovers when it starts:
|
|
14
|
+
|
|
15
|
+
- **Skills** — `SKILL.md` files the CLI loads as reusable workflows.
|
|
16
|
+
- **Slash commands** — `*.md` files the CLI exposes as `/command-name`.
|
|
17
|
+
- **MCP servers** — JSON config entries that point at a stdio/HTTP server.
|
|
18
|
+
|
|
19
|
+
A harness is "propagated" when it reaches a dispatched agent without the user
|
|
20
|
+
having to register it again in Minions. The single-line goal: *if it works
|
|
21
|
+
when you type the same task into `claude` / `copilot` / `codex` in this repo
|
|
22
|
+
on this machine, it works when Minions dispatches the same task here.*
|
|
23
|
+
|
|
24
|
+
## The four propagation mechanisms
|
|
25
|
+
|
|
26
|
+
Every asset reaches the agent through exactly one of these mechanisms today.
|
|
27
|
+
No engine code branches on `runtime.name` for any of them — the runtime
|
|
28
|
+
adapter answers the contract questions and the engine fans out.
|
|
29
|
+
|
|
30
|
+
### 1. Runtime native discovery (no engine code involved)
|
|
31
|
+
|
|
32
|
+
The CLI reads its own config and indexes its own asset dirs on every spawn.
|
|
33
|
+
Minions does not copy, symlink, or aggregate user-level assets — it just
|
|
34
|
+
makes sure the relevant dirs are *readable* from the agent's cwd (see
|
|
35
|
+
mechanism #3 below). The actual loading is the CLI's job.
|
|
36
|
+
|
|
37
|
+
| Runtime | User skills | User commands | User MCP servers |
|
|
38
|
+
|---------|-------------|---------------|------------------|
|
|
39
|
+
| Claude (`engine/runtimes/claude.js`) | `~/.claude/skills`, `~/.agents/skills`, plus `~/.claude/plugins/<…>/skills` | `~/.claude/commands`, plugin `commands/` | `~/.claude.json → mcpServers` |
|
|
40
|
+
| Copilot (`engine/runtimes/copilot.js`) | `~/.copilot/skills`, `~/.agents/skills`, `~/.copilot/installed-plugins/<…>/skills` | `~/.copilot/commands` *(probe)* | `~/.copilot/mcp-config.json → mcpServers` |
|
|
41
|
+
| Codex (`engine/runtimes/codex.js`) | `~/.agents/skills`, `/etc/codex/skills` | not yet supported by CLI | not yet a stable contract |
|
|
42
|
+
|
|
43
|
+
The first column is the source of truth for "where does the CLI look on
|
|
44
|
+
this machine?". The dashboard's tooling page mirrors this via
|
|
45
|
+
`engine/queries.js → collectSkillFiles / collectCommandFiles`.
|
|
46
|
+
|
|
47
|
+
### 2. Cwd routing (`shared.resolveSpawnPaths`)
|
|
48
|
+
|
|
49
|
+
`engine/shared.js → resolveSpawnPaths(project, type, MINIONS_DIR)` decides
|
|
50
|
+
what the agent's `cwd` is:
|
|
51
|
+
|
|
52
|
+
- **Read-only types** (`meeting`, `ask`, `explore`, `plan-to-prd`, `plan`)
|
|
53
|
+
→ `cwd = project.localPath` (the operator's main checkout — sees
|
|
54
|
+
uncommitted files).
|
|
55
|
+
- **Code-mutating types** (`implement`, `fix`, `review`, `test`, `verify`,
|
|
56
|
+
`decompose`, `docs`) → `cwd = <worktree>` (a fresh `git worktree add`
|
|
57
|
+
checked out at branch tip — only sees committed files).
|
|
58
|
+
|
|
59
|
+
Cwd routing matters because every CLI's native discovery is **rooted at
|
|
60
|
+
cwd**. Project-scope skills like `<repo>/.claude/skills/foo/SKILL.md` are
|
|
61
|
+
loaded by the CLI only if `<cwd>/.claude/skills/foo/SKILL.md` exists at
|
|
62
|
+
spawn time. See *The worktree-uncommitted footgun* below.
|
|
63
|
+
|
|
64
|
+
Live-checkout mode (`project.worktreeMode: 'live'`) collapses both branches
|
|
65
|
+
to `cwd = project.localPath` for every dispatch type, so the agent sees
|
|
66
|
+
the operator's working tree as-is (including uncommitted assets). The
|
|
67
|
+
tradeoff is single-mutating-dispatch concurrency per project — see
|
|
68
|
+
`docs/live-checkout-mode.md`.
|
|
69
|
+
|
|
70
|
+
### 3. `--add-dir` (`engine/spawn-agent.js → computeAddDirs`)
|
|
71
|
+
|
|
72
|
+
`computeAddDirs({ runtime, minionsDir, homeDir })` builds the list of
|
|
73
|
+
absolute dirs the CLI is allowed to read **outside** its cwd. The current
|
|
74
|
+
list:
|
|
75
|
+
|
|
76
|
+
1. `minionsDir` — always first, so playbooks, system prompt, and skill
|
|
77
|
+
index are reachable from any worktree.
|
|
78
|
+
2. Every existing dir returned by `runtime.getUserAssetDirs({ homeDir })`
|
|
79
|
+
for the resolved runtime — e.g. `~/.claude` + `~/.agents` for Claude,
|
|
80
|
+
`~/.copilot` + `~/.agents` for Copilot, `~/.codex` + `~/.agents` for
|
|
81
|
+
Codex.
|
|
82
|
+
|
|
83
|
+
Non-existent dirs are dropped (Claude CLI rejects unknown `--add-dir`
|
|
84
|
+
entries) and the list is deduped by resolved path.
|
|
85
|
+
|
|
86
|
+
`--add-dir` is the **only** mechanism today that crosses the worktree
|
|
87
|
+
boundary. It does not (yet) cover project-local-but-uncommitted assets —
|
|
88
|
+
that's plan item #5 (`harnessPropagateProjectLocal`).
|
|
89
|
+
|
|
90
|
+
### 4. MCP suppression flags
|
|
91
|
+
|
|
92
|
+
Two flags strip otherwise-inherited MCP / instruction surfaces; everything
|
|
93
|
+
else flows through implicitly because the CLI reads its native config on
|
|
94
|
+
every spawn:
|
|
95
|
+
|
|
96
|
+
- `engine.copilotDisableBuiltinMcps` (default `true`) → `--disable-builtin-mcps`
|
|
97
|
+
on Copilot. Strips Copilot's built-in `github-mcp-server` so dispatched
|
|
98
|
+
agents do not try to open a parallel PR through the bundled MCP while
|
|
99
|
+
Minions is already managing the PR via the host integration.
|
|
100
|
+
- `engine.copilotSuppressAgentsMd` (default `true`) → `--no-custom-instructions`
|
|
101
|
+
on Copilot. Strips project `AGENTS.md` auto-load so Minions playbook
|
|
102
|
+
prompts are not silently overridden by repo-local instructions.
|
|
103
|
+
|
|
104
|
+
There is no equivalent flag for stripping user-level skills, user-level
|
|
105
|
+
slash commands, or user-level MCPs. The current posture is "always inherit"
|
|
106
|
+
— `engine.hermeticHarness` (P-49e1c8b7, default `false`) is the per-fleet /
|
|
107
|
+
per-agent opt-out. When TRUE:
|
|
108
|
+
|
|
109
|
+
- `--add-dir` collapses to exactly `[minionsDir]` — every dir from
|
|
110
|
+
`runtime.getUserAssetDirs({ homeDir })` is dropped.
|
|
111
|
+
- `harnessPropagateProjectLocal` is skipped — no `--project-harness-dir`
|
|
112
|
+
flags emitted.
|
|
113
|
+
- `claudePreApproveWorkspaceMcps` is skipped — workspace `.mcp.json`
|
|
114
|
+
servers are not pre-approved in `~/.claude.json`.
|
|
115
|
+
|
|
116
|
+
The flag is **independent** of `copilotDisableBuiltinMcps` and
|
|
117
|
+
`copilotSuppressAgentsMd` — those keep their existing semantics so an
|
|
118
|
+
operator can run hermetic Claude *and* keep Copilot's built-in MCPs off
|
|
119
|
+
or AGENTS.md auto-load suppressed without re-thinking each lever. Per-agent
|
|
120
|
+
override at `agent.hermeticHarness` (resolved via
|
|
121
|
+
`shared.resolveAgentHermeticHarness(agent, engine)`, mirrors
|
|
122
|
+
`shared.resolveAgentBareMode`).
|
|
123
|
+
|
|
124
|
+
## The worktree-uncommitted footgun
|
|
125
|
+
|
|
126
|
+
The single biggest behavioral wrinkle for new contributors:
|
|
127
|
+
|
|
128
|
+
1. The operator drops an experimental skill at
|
|
129
|
+
`<repo>/.claude/skills/bar/SKILL.md` on their main checkout.
|
|
130
|
+
2. The skill works fine when invoked by hand:
|
|
131
|
+
`cd <repo>; claude "use the bar skill"` — Claude's auto-discovery
|
|
132
|
+
sees it because cwd is the main checkout.
|
|
133
|
+
3. The operator dispatches an `implement` work item against the same repo.
|
|
134
|
+
4. The engine runs `git worktree add <wt> origin/<branch>` (uncommitted
|
|
135
|
+
files do not appear in a fresh worktree), then spawns the agent with
|
|
136
|
+
`cwd = <wt>`.
|
|
137
|
+
5. Claude's discovery now happens at `<wt>` — `.claude/skills/bar/` is
|
|
138
|
+
absent. The agent silently underperforms with no error message.
|
|
139
|
+
|
|
140
|
+
Read-only dispatch types (`meeting`, `ask`, `explore`, `plan-to-prd`,
|
|
141
|
+
`plan`) do **not** hit this footgun, because their cwd is
|
|
142
|
+
`project.localPath`. Mutating types do.
|
|
143
|
+
|
|
144
|
+
`--add-dir` is not currently extended to cover this case (plan item #5),
|
|
145
|
+
so the only workarounds today are:
|
|
146
|
+
|
|
147
|
+
- **Commit it.** The skill becomes visible to every dispatch and every
|
|
148
|
+
teammate, with the usual reviewability tradeoff.
|
|
149
|
+
- **Move it user-scope.** Drop it under `~/.claude/skills/bar/` instead —
|
|
150
|
+
the CLI's user-skill discovery still works inside a worktree because
|
|
151
|
+
`--add-dir` attaches `~/.claude`.
|
|
152
|
+
- **Flip to live-checkout mode.** Set `project.worktreeMode = 'live'` on
|
|
153
|
+
the project so every dispatch runs in `project.localPath`. Caveats in
|
|
154
|
+
`docs/live-checkout-mode.md`.
|
|
155
|
+
|
|
156
|
+
The dashboard tooling page does not yet surface this gap; that's plan
|
|
157
|
+
item #8 ("Harness diagnostics" panel). Until then, run
|
|
158
|
+
`minions doctor --harness` to get the per-runtime view of which dirs the
|
|
159
|
+
engine actually surfaces, and check that flagged "missing" entries
|
|
160
|
+
actually match the assets you expected to inherit.
|
|
161
|
+
|
|
162
|
+
## `minions doctor --harness`
|
|
163
|
+
|
|
164
|
+
One-shot CLI diagnostic that prints, per registered runtime, every dir
|
|
165
|
+
or file the engine would surface to a spawned agent under its scope label.
|
|
166
|
+
Implemented in `engine/preflight.js → runHarnessDoctor(minionsHome)` and
|
|
167
|
+
wired into `bin/minions.js` and `engine/cli.js` as the `--harness` mode
|
|
168
|
+
of the existing `minions doctor` command.
|
|
169
|
+
|
|
170
|
+
### What it prints
|
|
171
|
+
|
|
172
|
+
```
|
|
173
|
+
Minions Harness Propagation
|
|
174
|
+
Runtime: claude
|
|
175
|
+
User asset dirs (--add-dir to agents):
|
|
176
|
+
✓ /home/you/.claude [user]
|
|
177
|
+
✓ /home/you/.agents [user]
|
|
178
|
+
Skill roots (CLI native discovery):
|
|
179
|
+
✓ /home/you/.claude/skills [user]
|
|
180
|
+
✓ /home/you/.agents/skills [user]
|
|
181
|
+
⚠ /repo/.claude/skills [project:myrepo] (missing on disk)
|
|
182
|
+
✓ /repo/.agents/skills [project:myrepo]
|
|
183
|
+
Skill write targets (auto-extract destinations):
|
|
184
|
+
✓ /home/you/.claude/skills [personal]
|
|
185
|
+
✓ /repo/.claude/skills [project:myrepo]
|
|
186
|
+
Runtime: copilot
|
|
187
|
+
User asset dirs (--add-dir to agents):
|
|
188
|
+
✓ /home/you/.copilot [user]
|
|
189
|
+
✓ /home/you/.agents [user]
|
|
190
|
+
…
|
|
191
|
+
Worktree --add-dir snapshot (engine fleet default: copilot):
|
|
192
|
+
✓ /home/you/.minions [minions]
|
|
193
|
+
✓ /home/you/.copilot [user]
|
|
194
|
+
✓ /home/you/.agents [user]
|
|
195
|
+
All harness paths surveyed. Missing on-disk paths are warnings, not failures —
|
|
196
|
+
they're listed so you can decide whether to create the dir, populate it, or
|
|
197
|
+
ignore it for this host.
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Exit behavior
|
|
201
|
+
|
|
202
|
+
The command exits **0 on a healthy host** even if some scoped paths do
|
|
203
|
+
not exist on disk. The whole point is to surface "what would the engine
|
|
204
|
+
attach today?" — a user that does not run Codex may legitimately have
|
|
205
|
+
no `~/.codex` dir, and that is not a failure. Exits non-zero only on a
|
|
206
|
+
true runtime/config error (e.g. registry refuses to resolve a known
|
|
207
|
+
runtime, or `config.json` is unparseable).
|
|
208
|
+
|
|
209
|
+
### When to run it
|
|
210
|
+
|
|
211
|
+
- After installing a new skill / command / MCP and "it didn't take" on
|
|
212
|
+
the next dispatched agent — confirm Minions actually surfaces that dir
|
|
213
|
+
for the runtime you're dispatching with.
|
|
214
|
+
- After changing `engine.defaultCli` — different runtimes have different
|
|
215
|
+
asset dirs; the `Worktree --add-dir snapshot` block reflects the fleet
|
|
216
|
+
default.
|
|
217
|
+
- After re-cloning a repo with `<repo>/.claude/skills/` — the diagnostic
|
|
218
|
+
shows whether the project-scope path is on disk.
|
|
219
|
+
- Before reporting "agent ignored my skill" — half the time the answer is
|
|
220
|
+
visible in the diagnostic output.
|
|
221
|
+
|
|
222
|
+
It does **not** check whether the assets *load successfully* inside the
|
|
223
|
+
CLI (no parse of `SKILL.md`, no MCP handshake). For loadability,
|
|
224
|
+
inspect the dashboard tooling page or run an `ask` dispatch and watch
|
|
225
|
+
the live output.
|
|
226
|
+
|
|
227
|
+
## Adapter contract summary
|
|
228
|
+
|
|
229
|
+
The runtime adapters answer these questions. The engine never special-cases
|
|
230
|
+
a runtime by name — it only calls these methods on the resolved adapter.
|
|
231
|
+
|
|
232
|
+
| Method | Signature | Used by |
|
|
233
|
+
|--------|-----------|---------|
|
|
234
|
+
| `getUserAssetDirs({ homeDir })` | → `string[]` | `engine/spawn-agent.js → computeAddDirs`; `runHarnessDoctor` |
|
|
235
|
+
| `getSkillRoots({ homeDir, project? })` | → `[{ scope, dir, projectName? }]` | `engine/queries.js → collectSkillFiles`; `runHarnessDoctor` |
|
|
236
|
+
| `getSkillWriteTargets({ homeDir, project? })` | → `{ personal, project? }` | `engine/lifecycle.js → extractSkillsFromOutput`; `runHarnessDoctor` |
|
|
237
|
+
| `getCommandRoots({ homeDir, project? })` | → `[{ scope, dir, projectName? }]` | `engine/queries.js → collectCommandFiles` (via `getProjectHarnesses` for project scope); `runHarnessDoctor` |
|
|
238
|
+
| `getMcpConfigPaths({ homeDir, project? })` | → `[{ scope, file, projectName? }]` | `runHarnessDoctor`; planned: `engine/queries.js → getStatusSlowStateMtimePaths` |
|
|
239
|
+
|
|
240
|
+
`runHarnessDoctor` renders all five surfaces as "Slash commands" and "MCP
|
|
241
|
+
config files" sections alongside the existing User asset dirs / Skill
|
|
242
|
+
roots / Skill write targets blocks. Adapters that don't yet have a stable
|
|
243
|
+
CLI contract for a surface (today: Codex for both commands and MCP,
|
|
244
|
+
Copilot for project-scope commands) return `[]` so consumers can iterate
|
|
245
|
+
runtimes generically without per-runtime branching.
|
|
246
|
+
|
|
247
|
+
`engine/queries.js` exposes two shared helpers — `getUserHarnesses(homeDir)`
|
|
248
|
+
and `getProjectHarnesses(project)` — that iterate every registered runtime
|
|
249
|
+
via `engine/runtimes.listRuntimes()` + `resolveRuntime()` and union the
|
|
250
|
+
`getSkillRoots` / `getCommandRoots` / `getMcpConfigPaths` contributions,
|
|
251
|
+
deduped by absolute path. The same physical dir contributed by multiple
|
|
252
|
+
adapters (e.g. `~/.agents/skills` exposed by Claude, Copilot, and Codex)
|
|
253
|
+
surfaces once with a `runtimes: [name, ...]` provenance array. Use them
|
|
254
|
+
from dashboard / diagnostics consumers instead of re-walking adapters by
|
|
255
|
+
hand; `collectSkillFiles` / `collectCommandFiles` already do.
|
|
256
|
+
|
|
257
|
+
> **Follow-up.** `getStatusSlowStateMtimePaths` still hardcodes the
|
|
258
|
+
> per-runtime MCP file paths; refactoring it to consume
|
|
259
|
+
> `getUserHarnesses(...).mcps` + `getProjectHarnesses(...).mcps` is
|
|
260
|
+
> tracked separately so the slow-state mtime tracker automatically picks
|
|
261
|
+
> up new adapters.
|
|
262
|
+
|
|
263
|
+
## Related docs
|
|
264
|
+
|
|
265
|
+
- `docs/runtime-adapters.md` — full adapter interface table.
|
|
266
|
+
- `docs/live-checkout-mode.md` — `project.worktreeMode: 'live'`
|
|
267
|
+
contract (alternative to the worktree-add-dir story for repos where
|
|
268
|
+
worktrees are unworkable).
|
|
269
|
+
- `docs/skills.md` — skill block format and auto-extraction targets.
|
|
270
|
+
- `plans/seamless-user-repo-harness-invocation.md` — full multi-item plan
|
|
271
|
+
this doc is the foundation of.
|
|
272
|
+
- `CLAUDE.md` → "Agent Spawn" + "Adapter Contract" — load-bearing
|
|
273
|
+
invariants the propagation contract piggy-backs on.
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
| Metrics | Engine (auto-collect) | Engine | You (view) | — |
|
|
16
16
|
| Error recovery | Engine (detect) | — | You (retry/delete) | You |
|
|
17
17
|
| Project linking | You (`minions add/scan`) | — | — | — |
|
|
18
|
-
| MCP servers | You (
|
|
18
|
+
| MCP servers / Skills / Commands | You (native CLI config) | Inherited by agents | — | — (opt-out via `hermeticHarness`) |
|
|
19
19
|
|
|
20
20
|
## The Two Human Gates
|
|
21
21
|
|
package/docs/runtime-adapters.md
CHANGED
|
@@ -174,6 +174,11 @@ user sees outside Minions. The cross-runtime portable location
|
|
|
174
174
|
`~/.agents/skills` is included by every adapter that opts into it (Copilot today;
|
|
175
175
|
add it to new adapters when the runtime can read directly from there).
|
|
176
176
|
|
|
177
|
+
For a full per-runtime survey of which dirs the engine actually surfaces today
|
|
178
|
+
— grouped by scope label and with on-disk-missing flags — run
|
|
179
|
+
`minions doctor --harness`; the diagnostic and the propagation contract it
|
|
180
|
+
codifies are documented in [`docs/harness-propagation.md`](./harness-propagation.md).
|
|
181
|
+
|
|
177
182
|
## The "No Runtime-Name Branching" Rule
|
|
178
183
|
|
|
179
184
|
The whole point of this layer: **engine code MUST gate behavior on
|
package/engine/cli.js
CHANGED
|
@@ -247,8 +247,8 @@ const CLI_COMMAND_DOCS = Object.freeze({
|
|
|
247
247
|
kill: { args: '', summary: 'Kill all active agents, reset to pending' },
|
|
248
248
|
complete: { args: '<dispatch-id>', summary: 'Mark a dispatch as done' },
|
|
249
249
|
cleanup: { args: '', summary: 'Clean temp files, worktrees, zombies' },
|
|
250
|
-
'mcp-sync': { args: '', summary: '
|
|
251
|
-
doctor: { args: '',
|
|
250
|
+
'mcp-sync': { args: '', summary: 'Print harness propagation diagnostic (same source as `minions doctor --harness`; read-only, no writes)' },
|
|
251
|
+
doctor: { args: '[--harness]', summary: 'Check prerequisites and runtime health (--harness: print harness propagation diagnostic)' },
|
|
252
252
|
config: { args: 'set-cli <R> [--model M]', summary: 'Persist defaultCli/defaultModel without starting' },
|
|
253
253
|
pr: { args: 'comment <repo> <prNumber> --agent <id> --kind <k> [--wi <id>] [--body-file <f>|--body <text>]', summary: 'Post a marker-prepended PR comment via gh' },
|
|
254
254
|
bridge: { args: 'status|health|enable|disable', summary: 'Constellation bridge: toggle and inspect the read-only cross-repo feed' },
|
|
@@ -1356,6 +1356,7 @@ const commands = {
|
|
|
1356
1356
|
const flagFields = [
|
|
1357
1357
|
'claudeBareMode', 'claudeFallbackModel',
|
|
1358
1358
|
'copilotDisableBuiltinMcps', 'copilotSuppressAgentsMd', 'copilotStreamMode', 'copilotReasoningSummaries',
|
|
1359
|
+
'harnessPropagateProjectLocal', 'claudePreApproveWorkspaceMcps', 'hermeticHarness',
|
|
1359
1360
|
'maxBudgetUsd', 'disableModelDiscovery',
|
|
1360
1361
|
'ccUseWorkerPool',
|
|
1361
1362
|
];
|
|
@@ -1854,11 +1855,29 @@ const commands = {
|
|
|
1854
1855
|
},
|
|
1855
1856
|
|
|
1856
1857
|
'mcp-sync'() {
|
|
1857
|
-
|
|
1858
|
+
// P-90a37c52 — repurposed as a harness diagnostic printer. Same source
|
|
1859
|
+
// as `minions doctor --harness` (engine/preflight.js → runHarnessDoctor)
|
|
1860
|
+
// but stays exit 0 regardless of the diagnostic's hardError signal, so
|
|
1861
|
+
// callers that scripted around the legacy `mcp-sync` (which was a pure
|
|
1862
|
+
// print + exit 0) don't have their automation flipped. For an exit
|
|
1863
|
+
// code that reflects harness health, use `minions doctor --harness`.
|
|
1864
|
+
//
|
|
1865
|
+
// Stays a no-op: no writes to ~/.claude.json, no state mutation. The
|
|
1866
|
+
// legacy "Sync MCP servers from ~/.claude.json — no sync needed."
|
|
1867
|
+
// message is replaced by the structured per-runtime propagation survey.
|
|
1868
|
+
const { runHarnessDoctor } = require('./preflight');
|
|
1869
|
+
runHarnessDoctor(MINIONS_DIR);
|
|
1858
1870
|
},
|
|
1859
1871
|
|
|
1860
|
-
doctor() {
|
|
1861
|
-
const { doctor } = require('./preflight');
|
|
1872
|
+
doctor(...doctorArgs) {
|
|
1873
|
+
const { doctor, runHarnessDoctor } = require('./preflight');
|
|
1874
|
+
if (doctorArgs.includes('--harness')) {
|
|
1875
|
+
// P-a3f9b2c1 — harness propagation diagnostic. See
|
|
1876
|
+
// docs/harness-propagation.md and the parallel branch in bin/minions.js.
|
|
1877
|
+
const ok = runHarnessDoctor(MINIONS_DIR);
|
|
1878
|
+
if (!ok) process.exit(1);
|
|
1879
|
+
return;
|
|
1880
|
+
}
|
|
1862
1881
|
return doctor(MINIONS_DIR).then(ok => {
|
|
1863
1882
|
if (!ok) process.exit(1);
|
|
1864
1883
|
});
|
package/engine/preflight.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
const fs = require('fs');
|
|
12
|
+
const os = require('os');
|
|
12
13
|
const path = require('path');
|
|
13
14
|
const { execSync, execFileSync } = require('child_process');
|
|
14
15
|
|
|
@@ -666,12 +667,272 @@ function doctor(minionsHome) {
|
|
|
666
667
|
});
|
|
667
668
|
}
|
|
668
669
|
|
|
670
|
+
/**
|
|
671
|
+
* Harness propagation diagnostic for `minions doctor --harness` (plan item #1
|
|
672
|
+
* of `seamless-user-repo-harness-invocation`, PRD `P-a3f9b2c1`). Iterates
|
|
673
|
+
* every registered runtime and prints every dir / file the engine would
|
|
674
|
+
* surface to a spawned agent today, grouped by scope label
|
|
675
|
+
* (`user` / `project:<name>` / `personal` / `minions`). Missing on-disk paths
|
|
676
|
+
* are flagged with `⚠` but do not fail the command — they're listed so the
|
|
677
|
+
* operator can decide whether to create / populate the dir or leave it as-is
|
|
678
|
+
* for this host.
|
|
679
|
+
*
|
|
680
|
+
* The diagnostic is intentionally read-only and side-effect-free: no mutation
|
|
681
|
+
* of config, cache files, or worktrees. Exits non-zero only on a true
|
|
682
|
+
* runtime/config error (e.g. registry refuses to resolve a registered
|
|
683
|
+
* runtime). See `docs/harness-propagation.md` for the full contract.
|
|
684
|
+
*
|
|
685
|
+
* Pure-ish: `homeDir` and `existsFn` are injectable for tests so a temp
|
|
686
|
+
* sandbox can assert the "missing path" branch without touching the real
|
|
687
|
+
* `~/.claude` / `~/.copilot` dirs.
|
|
688
|
+
*/
|
|
689
|
+
function _resolveProjectsForHarness(minionsHome) {
|
|
690
|
+
const configPath = path.join(minionsHome, 'config.json');
|
|
691
|
+
try {
|
|
692
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
693
|
+
const projects = Array.isArray(config.projects) ? config.projects : [];
|
|
694
|
+
return {
|
|
695
|
+
config,
|
|
696
|
+
projects: projects.filter(p => p && p.name && p.localPath && !String(p.name).startsWith('YOUR_')),
|
|
697
|
+
};
|
|
698
|
+
} catch {
|
|
699
|
+
return { config: null, projects: [] };
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function _padRight(s, len) {
|
|
704
|
+
s = String(s);
|
|
705
|
+
if (s.length >= len) return s;
|
|
706
|
+
return s + ' '.repeat(len - s.length);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function _formatHarnessLine(absPath, scopeLabel, exists) {
|
|
710
|
+
const marker = exists ? '✓' : '⚠';
|
|
711
|
+
const pathCol = _padRight(absPath, 56);
|
|
712
|
+
const scopeCol = `[${scopeLabel}]`;
|
|
713
|
+
const suffix = exists ? '' : ' (missing on disk)';
|
|
714
|
+
return ` ${marker} ${pathCol} ${scopeCol}${suffix}`;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function _runtimeHarnessRows(runtime, { homeDir, projects, existsFn }) {
|
|
718
|
+
const rows = {
|
|
719
|
+
userAssetDirs: [],
|
|
720
|
+
skillRootsUser: [],
|
|
721
|
+
skillRootsProject: [],
|
|
722
|
+
skillWriteTargets: [],
|
|
723
|
+
commandRootsUser: [],
|
|
724
|
+
commandRootsProject: [],
|
|
725
|
+
mcpConfigUser: [],
|
|
726
|
+
mcpConfigProject: [],
|
|
727
|
+
};
|
|
728
|
+
if (typeof runtime.getUserAssetDirs === 'function') {
|
|
729
|
+
try {
|
|
730
|
+
for (const d of runtime.getUserAssetDirs({ homeDir }) || []) {
|
|
731
|
+
if (!d) continue;
|
|
732
|
+
rows.userAssetDirs.push({ path: d, scope: 'user', exists: existsFn(d) });
|
|
733
|
+
}
|
|
734
|
+
} catch { /* adapter optional fields */ }
|
|
735
|
+
}
|
|
736
|
+
if (typeof runtime.getSkillRoots === 'function') {
|
|
737
|
+
try {
|
|
738
|
+
for (const root of runtime.getSkillRoots({ homeDir }) || []) {
|
|
739
|
+
if (!root || !root.dir) continue;
|
|
740
|
+
if (root.scope === 'project') continue; // user call shouldn't return project rows, but be defensive
|
|
741
|
+
rows.skillRootsUser.push({ path: root.dir, scope: root.scope || 'user', exists: existsFn(root.dir) });
|
|
742
|
+
}
|
|
743
|
+
} catch { /* */ }
|
|
744
|
+
for (const project of projects) {
|
|
745
|
+
try {
|
|
746
|
+
for (const root of runtime.getSkillRoots({ homeDir, project }) || []) {
|
|
747
|
+
if (!root || !root.dir || root.scope !== 'project') continue;
|
|
748
|
+
rows.skillRootsProject.push({
|
|
749
|
+
path: root.dir,
|
|
750
|
+
scope: `project:${root.projectName || project.name}`,
|
|
751
|
+
exists: existsFn(root.dir),
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
} catch { /* */ }
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
if (typeof runtime.getSkillWriteTargets === 'function') {
|
|
758
|
+
try {
|
|
759
|
+
const targets = runtime.getSkillWriteTargets({ homeDir }) || {};
|
|
760
|
+
if (targets.personal) {
|
|
761
|
+
rows.skillWriteTargets.push({ path: targets.personal, scope: 'personal', exists: existsFn(targets.personal) });
|
|
762
|
+
}
|
|
763
|
+
} catch { /* */ }
|
|
764
|
+
for (const project of projects) {
|
|
765
|
+
try {
|
|
766
|
+
const targets = runtime.getSkillWriteTargets({ homeDir, project }) || {};
|
|
767
|
+
if (targets.project) {
|
|
768
|
+
rows.skillWriteTargets.push({
|
|
769
|
+
path: targets.project,
|
|
770
|
+
scope: `project:${project.name}`,
|
|
771
|
+
exists: existsFn(targets.project),
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
} catch { /* */ }
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
if (typeof runtime.getCommandRoots === 'function') {
|
|
778
|
+
try {
|
|
779
|
+
for (const root of runtime.getCommandRoots({ homeDir }) || []) {
|
|
780
|
+
if (!root || !root.dir) continue;
|
|
781
|
+
if (root.scope === 'project') continue;
|
|
782
|
+
rows.commandRootsUser.push({ path: root.dir, scope: root.scope || 'user', exists: existsFn(root.dir) });
|
|
783
|
+
}
|
|
784
|
+
} catch { /* */ }
|
|
785
|
+
for (const project of projects) {
|
|
786
|
+
try {
|
|
787
|
+
for (const root of runtime.getCommandRoots({ homeDir, project }) || []) {
|
|
788
|
+
if (!root || !root.dir || root.scope !== 'project') continue;
|
|
789
|
+
rows.commandRootsProject.push({
|
|
790
|
+
path: root.dir,
|
|
791
|
+
scope: `project:${root.projectName || project.name}`,
|
|
792
|
+
exists: existsFn(root.dir),
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
} catch { /* */ }
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
if (typeof runtime.getMcpConfigPaths === 'function') {
|
|
799
|
+
try {
|
|
800
|
+
for (const entry of runtime.getMcpConfigPaths({ homeDir }) || []) {
|
|
801
|
+
if (!entry || !entry.file) continue;
|
|
802
|
+
if (entry.scope === 'project') continue;
|
|
803
|
+
rows.mcpConfigUser.push({ path: entry.file, scope: entry.scope || 'user', exists: existsFn(entry.file) });
|
|
804
|
+
}
|
|
805
|
+
} catch { /* */ }
|
|
806
|
+
for (const project of projects) {
|
|
807
|
+
try {
|
|
808
|
+
for (const entry of runtime.getMcpConfigPaths({ homeDir, project }) || []) {
|
|
809
|
+
if (!entry || !entry.file || entry.scope !== 'project') continue;
|
|
810
|
+
rows.mcpConfigProject.push({
|
|
811
|
+
path: entry.file,
|
|
812
|
+
scope: `project:${entry.projectName || project.name}`,
|
|
813
|
+
exists: existsFn(entry.file),
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
} catch { /* */ }
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
return rows;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function _computeAddDirSnapshot(runtime, { minionsHome, homeDir, existsFn }) {
|
|
823
|
+
let computeAddDirs;
|
|
824
|
+
try { ({ computeAddDirs } = require('./spawn-agent')); }
|
|
825
|
+
catch { return null; }
|
|
826
|
+
if (typeof computeAddDirs !== 'function') return null;
|
|
827
|
+
let dirs = [];
|
|
828
|
+
try { dirs = computeAddDirs({ runtime, minionsDir: minionsHome, homeDir, exists: existsFn }) || []; }
|
|
829
|
+
catch { return null; }
|
|
830
|
+
return dirs.map(d => {
|
|
831
|
+
let scope = 'user';
|
|
832
|
+
if (path.resolve(d) === path.resolve(minionsHome)) scope = 'minions';
|
|
833
|
+
return { path: d, scope, exists: existsFn(d) };
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function runHarnessDoctor(minionsHome, opts = {}) {
|
|
838
|
+
const homeDir = opts.homeDir || os.homedir();
|
|
839
|
+
const existsFn = opts.existsFn || fs.existsSync;
|
|
840
|
+
const out = opts.out || (line => console.log(line));
|
|
841
|
+
|
|
842
|
+
let registry;
|
|
843
|
+
try { registry = require('./runtimes'); }
|
|
844
|
+
catch (e) {
|
|
845
|
+
out(`Minions Harness Propagation`);
|
|
846
|
+
out(` Could not load runtime registry: ${e.message}`);
|
|
847
|
+
return false;
|
|
848
|
+
}
|
|
849
|
+
const runtimeNames = registry.listRuntimes();
|
|
850
|
+
const { config, projects } = _resolveProjectsForHarness(minionsHome);
|
|
851
|
+
const defaultCli = (config && config.engine && config.engine.defaultCli) || 'copilot';
|
|
852
|
+
|
|
853
|
+
out('Minions Harness Propagation');
|
|
854
|
+
if (projects.length === 0) {
|
|
855
|
+
out(' (no real projects configured — project-scope rows will be empty)');
|
|
856
|
+
} else {
|
|
857
|
+
out(` Projects: ${projects.map(p => p.name).join(', ')}`);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
let hardError = false;
|
|
861
|
+
for (const runtimeName of runtimeNames) {
|
|
862
|
+
out('');
|
|
863
|
+
out(` Runtime: ${runtimeName}`);
|
|
864
|
+
let runtime;
|
|
865
|
+
try { runtime = registry.resolveRuntime(runtimeName); }
|
|
866
|
+
catch (e) {
|
|
867
|
+
out(` Could not resolve adapter: ${e.message}`);
|
|
868
|
+
hardError = true;
|
|
869
|
+
continue;
|
|
870
|
+
}
|
|
871
|
+
const rows = _runtimeHarnessRows(runtime, { homeDir, projects, existsFn });
|
|
872
|
+
|
|
873
|
+
out(' User asset dirs (--add-dir to agents):');
|
|
874
|
+
if (rows.userAssetDirs.length === 0) out(' (adapter does not expose getUserAssetDirs)');
|
|
875
|
+
for (const r of rows.userAssetDirs) out(_formatHarnessLine(r.path, r.scope, r.exists));
|
|
876
|
+
|
|
877
|
+
out(' Skill roots (CLI native discovery):');
|
|
878
|
+
if (rows.skillRootsUser.length === 0 && rows.skillRootsProject.length === 0) {
|
|
879
|
+
out(' (adapter does not expose getSkillRoots)');
|
|
880
|
+
}
|
|
881
|
+
for (const r of rows.skillRootsUser) out(_formatHarnessLine(r.path, r.scope, r.exists));
|
|
882
|
+
for (const r of rows.skillRootsProject) out(_formatHarnessLine(r.path, r.scope, r.exists));
|
|
883
|
+
|
|
884
|
+
out(' Skill write targets (auto-extract destinations):');
|
|
885
|
+
if (rows.skillWriteTargets.length === 0) {
|
|
886
|
+
out(' (adapter does not expose getSkillWriteTargets)');
|
|
887
|
+
}
|
|
888
|
+
for (const r of rows.skillWriteTargets) out(_formatHarnessLine(r.path, r.scope, r.exists));
|
|
889
|
+
|
|
890
|
+
out(' Slash commands (CLI native discovery):');
|
|
891
|
+
if (rows.commandRootsUser.length === 0 && rows.commandRootsProject.length === 0) {
|
|
892
|
+
out(' (adapter does not expose getCommandRoots)');
|
|
893
|
+
}
|
|
894
|
+
for (const r of rows.commandRootsUser) out(_formatHarnessLine(r.path, r.scope, r.exists));
|
|
895
|
+
for (const r of rows.commandRootsProject) out(_formatHarnessLine(r.path, r.scope, r.exists));
|
|
896
|
+
|
|
897
|
+
out(' MCP config files:');
|
|
898
|
+
if (rows.mcpConfigUser.length === 0 && rows.mcpConfigProject.length === 0) {
|
|
899
|
+
out(' (adapter does not expose getMcpConfigPaths)');
|
|
900
|
+
}
|
|
901
|
+
for (const r of rows.mcpConfigUser) out(_formatHarnessLine(r.path, r.scope, r.exists));
|
|
902
|
+
for (const r of rows.mcpConfigProject) out(_formatHarnessLine(r.path, r.scope, r.exists));
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
out('');
|
|
906
|
+
out(` Worktree --add-dir snapshot (engine fleet default: ${defaultCli}):`);
|
|
907
|
+
let defaultRuntime = null;
|
|
908
|
+
try { defaultRuntime = registry.resolveRuntime(defaultCli); }
|
|
909
|
+
catch { /* preflight handles unknown-runtime errors; here we just skip */ }
|
|
910
|
+
if (!defaultRuntime) {
|
|
911
|
+
out(' (cannot resolve fleet-default runtime — see `minions doctor` for details)');
|
|
912
|
+
} else {
|
|
913
|
+
const snapshot = _computeAddDirSnapshot(defaultRuntime, { minionsHome, homeDir, existsFn });
|
|
914
|
+
if (!snapshot || snapshot.length === 0) {
|
|
915
|
+
out(' (no dirs attached — engine/spawn-agent.js unavailable or adapter has no asset dirs)');
|
|
916
|
+
} else {
|
|
917
|
+
for (const r of snapshot) out(_formatHarnessLine(r.path, r.scope, r.exists));
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
out('');
|
|
922
|
+
out(' All harness paths surveyed. Missing on-disk paths are warnings, not failures —');
|
|
923
|
+
out(' they\'re listed so you can decide whether to create the dir, populate it, or');
|
|
924
|
+
out(' ignore it for this host.');
|
|
925
|
+
out('');
|
|
926
|
+
return !hardError;
|
|
927
|
+
}
|
|
928
|
+
|
|
669
929
|
module.exports = {
|
|
670
930
|
findClaudeBinary,
|
|
671
931
|
runPreflight,
|
|
672
932
|
printPreflight,
|
|
673
933
|
checkOrExit,
|
|
674
934
|
doctor,
|
|
935
|
+
runHarnessDoctor,
|
|
675
936
|
// Exposed for unit tests (P-9e8a3f1d) — engine code MUST go through
|
|
676
937
|
// runPreflight/doctor, never these helpers directly.
|
|
677
938
|
_distinctRuntimes,
|
|
@@ -682,4 +943,8 @@ module.exports = {
|
|
|
682
943
|
_fetchCliHelpText,
|
|
683
944
|
_checkBypassFlagSupported,
|
|
684
945
|
_bypassFlagResults,
|
|
946
|
+
// Exposed for harness-propagation doctor unit tests (P-a3f9b2c1).
|
|
947
|
+
_runtimeHarnessRows,
|
|
948
|
+
_computeAddDirSnapshot,
|
|
949
|
+
_formatHarnessLine,
|
|
685
950
|
};
|