@yemi33/minions 0.1.2178 → 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 +39 -17
- package/dashboard/js/command-parser.js +1 -1
- package/dashboard/js/memory-panel.js +324 -0
- package/dashboard/js/qa.js +2 -2
- package/dashboard/js/refresh.js +19 -1
- package/dashboard/js/render-other.js +143 -2
- package/dashboard/js/render-prs.js +2 -1
- package/dashboard/js/render-schedules.js +1 -1
- package/dashboard/js/render-watches.js +1 -1
- package/dashboard/js/render-work-items.js +18 -1
- package/dashboard/js/settings.js +23 -0
- package/dashboard/pages/engine-memory-panel.html +56 -0
- package/dashboard/pages/engine.html +1 -0
- package/dashboard/pages/tools.html +8 -0
- package/dashboard/slim/js/link-pr.js +5 -5
- package/dashboard/slim/js/modals-tiles.js +44 -3
- package/dashboard/slim/js/projects.js +8 -6
- package/dashboard/slim/styles.css +20 -0
- package/dashboard-build.js +17 -2
- package/dashboard.js +693 -19
- package/docs/branch-derivation.md +13 -1
- 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/diagnostics-memory.js +190 -0
- package/engine/lifecycle.js +111 -1
- package/engine/preflight.js +265 -0
- package/engine/queries.js +331 -19
- 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 +390 -15
- package/engine/spawn-agent.js +178 -12
- package/engine/watchdog.js +6 -0
- package/engine.js +277 -4
- package/package.json +2 -2
|
@@ -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
|
});
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* engine/diagnostics-memory.js — In-process memory + event-loop + GC sampler.
|
|
3
|
+
*
|
|
4
|
+
* P-a1b2c3d4 (memory + perf audit plan). Importable from both engine.js and
|
|
5
|
+
* dashboard.js — each Node process gets its own module-scope state (event-loop
|
|
6
|
+
* histogram, GC counters, ring buffer). No I/O; persistence is the caller's
|
|
7
|
+
* responsibility (see P-b2c3d4e5).
|
|
8
|
+
*
|
|
9
|
+
* Exports:
|
|
10
|
+
* - sampleSelf({ label }) → snapshot object with RSS / heap / event-loop /
|
|
11
|
+
* GC counters / pid / uptime / capturedAt.
|
|
12
|
+
* - getHistory({ limit }) → up-to-`limit` samples from the ring buffer
|
|
13
|
+
* (oldest first), defaults to all 1440.
|
|
14
|
+
* - recordSample(sample) → append to the ring buffer; rotates at the cap.
|
|
15
|
+
* - startPeriodicSampling({ intervalMs, onSample }) → setInterval-driven
|
|
16
|
+
* sampler; returns a stop() function. Dashboard.js uses this on boot;
|
|
17
|
+
* engine.js drives sampling from its own tick instead.
|
|
18
|
+
*
|
|
19
|
+
* Zero deps — all Node built-ins. The event-loop histogram is a
|
|
20
|
+
* `monitorEventLoopDelay({ resolution: 20 })` singleton lazily enabled on the
|
|
21
|
+
* first `sampleSelf()` call. The PerformanceObserver subscribes to
|
|
22
|
+
* `entryTypes: ['gc']` and accumulates pause times into module-scope counters.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const v8 = require('v8');
|
|
26
|
+
const { monitorEventLoopDelay, PerformanceObserver, constants } = require('perf_hooks');
|
|
27
|
+
|
|
28
|
+
const RING_BUFFER_CAP = 1440;
|
|
29
|
+
const HISTOGRAM_RESOLUTION_MS = 20;
|
|
30
|
+
const NS_PER_MS = 1e6;
|
|
31
|
+
|
|
32
|
+
const ringBuffer = [];
|
|
33
|
+
|
|
34
|
+
let eventLoopHistogram = null;
|
|
35
|
+
let gcObserver = null;
|
|
36
|
+
let gcPausesTotalMs = 0;
|
|
37
|
+
let gcCount = 0;
|
|
38
|
+
let lastGcPauseMs = 0;
|
|
39
|
+
let lastGcKind = null;
|
|
40
|
+
|
|
41
|
+
const GC_KIND_NAMES = {
|
|
42
|
+
[constants.NODE_PERFORMANCE_GC_MAJOR]: 'major',
|
|
43
|
+
[constants.NODE_PERFORMANCE_GC_MINOR]: 'minor',
|
|
44
|
+
[constants.NODE_PERFORMANCE_GC_INCREMENTAL]: 'incremental',
|
|
45
|
+
[constants.NODE_PERFORMANCE_GC_WEAKCB]: 'weakcb',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
function _ensureHistogramStarted() {
|
|
49
|
+
if (eventLoopHistogram) return eventLoopHistogram;
|
|
50
|
+
eventLoopHistogram = monitorEventLoopDelay({ resolution: HISTOGRAM_RESOLUTION_MS });
|
|
51
|
+
eventLoopHistogram.enable();
|
|
52
|
+
return eventLoopHistogram;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function _ensureGcObserverStarted() {
|
|
56
|
+
if (gcObserver) return gcObserver;
|
|
57
|
+
gcObserver = new PerformanceObserver((list) => {
|
|
58
|
+
for (const entry of list.getEntries()) {
|
|
59
|
+
// entry.duration is in ms (fractional). entry.kind / entry.detail.kind
|
|
60
|
+
// is the numeric NODE_PERFORMANCE_GC_* constant.
|
|
61
|
+
const pauseMs = Number(entry.duration) || 0;
|
|
62
|
+
gcPausesTotalMs += pauseMs;
|
|
63
|
+
gcCount += 1;
|
|
64
|
+
lastGcPauseMs = pauseMs;
|
|
65
|
+
const kindNum = (entry.detail && entry.detail.kind) || entry.kind || null;
|
|
66
|
+
lastGcKind = (kindNum != null && GC_KIND_NAMES[kindNum]) || (kindNum != null ? String(kindNum) : null);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
gcObserver.observe({ entryTypes: ['gc'], buffered: false });
|
|
70
|
+
// Don't keep the event loop alive just to observe GC.
|
|
71
|
+
if (typeof gcObserver.unref === 'function') gcObserver.unref();
|
|
72
|
+
return gcObserver;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function _nsToMs(ns) {
|
|
76
|
+
if (!Number.isFinite(ns) || ns <= 0) return 0;
|
|
77
|
+
return ns / NS_PER_MS;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function _readEventLoopLag(hist) {
|
|
81
|
+
// hist.percentile(p) throws when the histogram is empty (no recorded
|
|
82
|
+
// samples yet). Guard so the first sampleSelf() call after process start
|
|
83
|
+
// returns zeros instead of throwing.
|
|
84
|
+
let p50 = 0, p99 = 0, max = 0;
|
|
85
|
+
try {
|
|
86
|
+
if (hist && typeof hist.percentile === 'function') {
|
|
87
|
+
const c = typeof hist.count === 'number' ? hist.count : (hist.totalCount || 0);
|
|
88
|
+
if (c > 0) {
|
|
89
|
+
p50 = _nsToMs(hist.percentile(50));
|
|
90
|
+
p99 = _nsToMs(hist.percentile(99));
|
|
91
|
+
max = _nsToMs(hist.max);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
// Empty / not-yet-populated histogram. Leave zeros.
|
|
96
|
+
}
|
|
97
|
+
return { p50, p99, max };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function sampleSelf({ label = null } = {}) {
|
|
101
|
+
const hist = _ensureHistogramStarted();
|
|
102
|
+
_ensureGcObserverStarted();
|
|
103
|
+
|
|
104
|
+
const mem = process.memoryUsage();
|
|
105
|
+
const heap = v8.getHeapStatistics();
|
|
106
|
+
const lag = _readEventLoopLag(hist);
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
rss: mem.rss,
|
|
110
|
+
heapUsed: mem.heapUsed,
|
|
111
|
+
heapTotal: mem.heapTotal,
|
|
112
|
+
external: mem.external,
|
|
113
|
+
arrayBuffers: mem.arrayBuffers || 0,
|
|
114
|
+
heapSizeLimit: heap.heap_size_limit,
|
|
115
|
+
eventLoopLagP50: lag.p50,
|
|
116
|
+
eventLoopLagP99: lag.p99,
|
|
117
|
+
eventLoopLagMax: lag.max,
|
|
118
|
+
lastGcPauseMs,
|
|
119
|
+
lastGcKind,
|
|
120
|
+
gcPausesTotalMs,
|
|
121
|
+
gcCount,
|
|
122
|
+
uptime: process.uptime(),
|
|
123
|
+
pid: process.pid,
|
|
124
|
+
label,
|
|
125
|
+
capturedAt: Date.now(),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function recordSample(sample) {
|
|
130
|
+
if (!sample || typeof sample !== 'object') return;
|
|
131
|
+
ringBuffer.push(sample);
|
|
132
|
+
while (ringBuffer.length > RING_BUFFER_CAP) ringBuffer.shift();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function getHistory({ limit } = {}) {
|
|
136
|
+
const n = ringBuffer.length;
|
|
137
|
+
if (!Number.isInteger(limit) || limit <= 0 || limit >= n) {
|
|
138
|
+
return ringBuffer.slice();
|
|
139
|
+
}
|
|
140
|
+
return ringBuffer.slice(n - limit);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function startPeriodicSampling({ intervalMs = 60000, onSample = null } = {}) {
|
|
144
|
+
if (!Number.isFinite(intervalMs) || intervalMs <= 0) {
|
|
145
|
+
throw new Error('startPeriodicSampling: intervalMs must be a positive number');
|
|
146
|
+
}
|
|
147
|
+
const handle = setInterval(() => {
|
|
148
|
+
let sample;
|
|
149
|
+
try {
|
|
150
|
+
sample = sampleSelf({ label: 'periodic' });
|
|
151
|
+
} catch {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
recordSample(sample);
|
|
155
|
+
if (typeof onSample === 'function') {
|
|
156
|
+
try { onSample(sample); } catch { /* swallow — sampler must not crash on callback errors */ }
|
|
157
|
+
}
|
|
158
|
+
}, intervalMs);
|
|
159
|
+
if (typeof handle.unref === 'function') handle.unref();
|
|
160
|
+
return function stop() {
|
|
161
|
+
clearInterval(handle);
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function _resetForTest() {
|
|
166
|
+
ringBuffer.length = 0;
|
|
167
|
+
if (eventLoopHistogram && typeof eventLoopHistogram.disable === 'function') {
|
|
168
|
+
try { eventLoopHistogram.disable(); } catch {}
|
|
169
|
+
}
|
|
170
|
+
eventLoopHistogram = null;
|
|
171
|
+
if (gcObserver && typeof gcObserver.disconnect === 'function') {
|
|
172
|
+
try { gcObserver.disconnect(); } catch {}
|
|
173
|
+
}
|
|
174
|
+
gcObserver = null;
|
|
175
|
+
gcPausesTotalMs = 0;
|
|
176
|
+
gcCount = 0;
|
|
177
|
+
lastGcPauseMs = 0;
|
|
178
|
+
lastGcKind = null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
module.exports = {
|
|
182
|
+
sampleSelf,
|
|
183
|
+
getHistory,
|
|
184
|
+
recordSample,
|
|
185
|
+
startPeriodicSampling,
|
|
186
|
+
// Constants for callers that need to know the cap up front.
|
|
187
|
+
RING_BUFFER_CAP,
|
|
188
|
+
// exported for testing
|
|
189
|
+
_resetForTest,
|
|
190
|
+
};
|
package/engine/lifecycle.js
CHANGED
|
@@ -915,9 +915,30 @@ function syncPrsFromOutput(output, agentId, meta, config, opts = {}) {
|
|
|
915
915
|
(projects.length === 1 ? projects[0] : null);
|
|
916
916
|
const useCentral = !defaultProject;
|
|
917
917
|
|
|
918
|
-
// Match each PR to its correct project
|
|
918
|
+
// Match each PR to its correct project. W-mqba5ulq000nd255 — primary
|
|
919
|
+
// strategy is canonical scope: parse the evidence URL into a host scope
|
|
920
|
+
// (e.g. `github:opg-microsoft/minions`) and find the configured project
|
|
921
|
+
// whose `getProjectPrScope` matches. This routes the record into the
|
|
922
|
+
// correctly-scoped project file even when the dispatching agent ran
|
|
923
|
+
// against a different project (the cross-project PR case that previously
|
|
924
|
+
// produced stale `_invalidProjectScope` stubs in the wrong file). Falls
|
|
925
|
+
// back to the legacy substring match (handles legacy non-canonical
|
|
926
|
+
// configs), then to a repoName-by-_git fallback, then to the dispatching
|
|
927
|
+
// project (which will get the `_invalidProjectScope` stamp via
|
|
928
|
+
// normalizePrRecord — preserving today's tracking behavior for orphan /
|
|
929
|
+
// unknown-owner URLs).
|
|
919
930
|
function resolveProjectForPr(prId) {
|
|
920
931
|
const evidenceUrl = prEvidence.get(prId) || '';
|
|
932
|
+
// Scope match wins. Use the most-specific URL available: prefer the
|
|
933
|
+
// direct evidence URL captured alongside the PR id; fall back to the
|
|
934
|
+
// generic stdout scan if that's empty.
|
|
935
|
+
const urlScope = shared.getPrScopeInfo(null, evidenceUrl)?.scope || '';
|
|
936
|
+
if (urlScope) {
|
|
937
|
+
for (const p of projects) {
|
|
938
|
+
const projScope = shared.getProjectPrScope(p);
|
|
939
|
+
if (projScope && projScope === urlScope) return p;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
921
942
|
const evidenceText = `${outputText}\n${evidenceUrl}`;
|
|
922
943
|
for (const p of projects) {
|
|
923
944
|
if (!p.prUrlBase) continue;
|
|
@@ -5474,6 +5495,94 @@ function diagnoseEmptyOutput(failureClass, code, elapsedMs) {
|
|
|
5474
5495
|
return `[empty-output: process exited in ${elapsedMs}ms \u2014 possible causes: machine sleep, network unavailability, auth failure]`;
|
|
5475
5496
|
}
|
|
5476
5497
|
|
|
5498
|
+
// W-mqba5ulq000nd255 — Reconciliation sweep: delete PR records flagged with
|
|
5499
|
+
// `_invalidProjectScope: { reason: "pr_scope_mismatch" }` IFF a sibling
|
|
5500
|
+
// record for the same canonical pr.id exists in another project whose scope
|
|
5501
|
+
// matches the PR URL (i.e. the correctly-scoped project owns the canonical
|
|
5502
|
+
// record). Sibling-less mismatches are preserved as tracking. Runs once per
|
|
5503
|
+
// tick from engine.js after the ADO/GitHub reconcile polls finish.
|
|
5504
|
+
//
|
|
5505
|
+
// Returns { pruned, scanned } so the engine tick can log a summary line.
|
|
5506
|
+
function pruneScopeMismatchDuplicatePrs(config) {
|
|
5507
|
+
config = config || getConfig();
|
|
5508
|
+
const projects = shared.getProjects(config) || [];
|
|
5509
|
+
if (projects.length === 0) return { pruned: 0, scanned: 0 };
|
|
5510
|
+
|
|
5511
|
+
// Map project name -> canonical scope so we can find which project IS the
|
|
5512
|
+
// correctly-scoped owner for a given mismatch record.
|
|
5513
|
+
const scopeByProject = new Map();
|
|
5514
|
+
const projectByScope = new Map();
|
|
5515
|
+
for (const p of projects) {
|
|
5516
|
+
if (!p || !p.name) continue;
|
|
5517
|
+
const sc = shared.getProjectPrScope(p);
|
|
5518
|
+
if (!sc) continue;
|
|
5519
|
+
scopeByProject.set(p.name, sc);
|
|
5520
|
+
projectByScope.set(sc, p);
|
|
5521
|
+
}
|
|
5522
|
+
|
|
5523
|
+
// Index all PRs by canonical id across all projects (post-_scope decoration).
|
|
5524
|
+
const store = require('./pull-requests-store');
|
|
5525
|
+
const allPrs = store.readAllPullRequests() || [];
|
|
5526
|
+
const byId = new Map();
|
|
5527
|
+
for (const pr of allPrs) {
|
|
5528
|
+
if (!pr || !pr.id) continue;
|
|
5529
|
+
if (!byId.has(pr.id)) byId.set(pr.id, []);
|
|
5530
|
+
byId.get(pr.id).push(pr);
|
|
5531
|
+
}
|
|
5532
|
+
|
|
5533
|
+
// Build per-project delete sets keyed by id, so we batch one mutation per
|
|
5534
|
+
// affected project file.
|
|
5535
|
+
const deletesByProject = new Map(); // projectName -> Set(prId)
|
|
5536
|
+
let scanned = 0;
|
|
5537
|
+
let pruned = 0;
|
|
5538
|
+
|
|
5539
|
+
for (const records of byId.values()) {
|
|
5540
|
+
if (records.length < 2) continue;
|
|
5541
|
+
for (const rec of records) {
|
|
5542
|
+
scanned++;
|
|
5543
|
+
if (!rec._invalidProjectScope || rec._invalidProjectScope.reason !== 'pr_scope_mismatch') continue;
|
|
5544
|
+
const correctScope = rec._invalidProjectScope.prScope;
|
|
5545
|
+
if (!correctScope) continue;
|
|
5546
|
+
const correctProject = projectByScope.get(correctScope);
|
|
5547
|
+
if (!correctProject) continue; // no configured project owns the URL — keep as tracking
|
|
5548
|
+
// Sibling check: is there a record under the correct scope for the same id?
|
|
5549
|
+
const sibling = records.find(r => r !== rec && r._scope === correctProject.name);
|
|
5550
|
+
if (!sibling) continue;
|
|
5551
|
+
// Safe to prune.
|
|
5552
|
+
const owningScope = rec._scope;
|
|
5553
|
+
if (!owningScope || owningScope === 'central') continue;
|
|
5554
|
+
if (!deletesByProject.has(owningScope)) deletesByProject.set(owningScope, new Set());
|
|
5555
|
+
deletesByProject.get(owningScope).add(rec.id);
|
|
5556
|
+
}
|
|
5557
|
+
}
|
|
5558
|
+
|
|
5559
|
+
if (deletesByProject.size === 0) return { pruned: 0, scanned };
|
|
5560
|
+
|
|
5561
|
+
for (const [projectName, idsToDelete] of deletesByProject) {
|
|
5562
|
+
const project = projects.find(p => p.name === projectName);
|
|
5563
|
+
if (!project) continue;
|
|
5564
|
+
const prPath = projectPrPath(project);
|
|
5565
|
+
try {
|
|
5566
|
+
shared.mutatePullRequests(prPath, (prs) => {
|
|
5567
|
+
const before = prs.length;
|
|
5568
|
+
const next = prs.filter(p => !idsToDelete.has(p?.id));
|
|
5569
|
+
const deleted = before - next.length;
|
|
5570
|
+
if (deleted > 0) {
|
|
5571
|
+
pruned += deleted;
|
|
5572
|
+
for (const id of idsToDelete) {
|
|
5573
|
+
log('info', `[pull-requests] pruned scope-mismatch duplicate ${id} from project=${projectName} (sibling exists in correctly-scoped project)`);
|
|
5574
|
+
}
|
|
5575
|
+
}
|
|
5576
|
+
return next;
|
|
5577
|
+
});
|
|
5578
|
+
} catch (err) {
|
|
5579
|
+
log('warn', `pruneScopeMismatchDuplicatePrs: failed to mutate ${projectName}: ${err?.message || err}`);
|
|
5580
|
+
}
|
|
5581
|
+
}
|
|
5582
|
+
|
|
5583
|
+
return { pruned, scanned };
|
|
5584
|
+
}
|
|
5585
|
+
|
|
5477
5586
|
module.exports = {
|
|
5478
5587
|
checkPlanCompletion,
|
|
5479
5588
|
archivePlan,
|
|
@@ -5482,6 +5591,7 @@ module.exports = {
|
|
|
5482
5591
|
syncPrdItemStatus,
|
|
5483
5592
|
reconcilePrdStatuses,
|
|
5484
5593
|
syncPrsFromOutput,
|
|
5594
|
+
pruneScopeMismatchDuplicatePrs,
|
|
5485
5595
|
updatePrAfterReview,
|
|
5486
5596
|
updatePrAfterFix,
|
|
5487
5597
|
updatePrAfterFixError,
|