@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.
@@ -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 (`~/.claude.json`) | Inherited by agents | — | — |
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
 
@@ -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: 'Sync MCP servers from ~/.claude.json' },
251
- doctor: { args: '', summary: 'Check prerequisites and runtime health' },
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
- console.log('MCP servers are read directly from ~/.claude.json — no sync needed.');
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
  });
@@ -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
  };