@yemi33/minions 0.1.2177 → 0.1.2179
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/bin/minions.js +24 -11
- package/dashboard/js/command-parser.js +1 -1
- package/dashboard/js/memory-panel.js +262 -0
- package/dashboard/js/qa.js +2 -2
- package/dashboard/js/refresh.js +9 -1
- package/dashboard/js/render-dispatch.js +92 -0
- package/dashboard/js/render-other.js +1 -1
- package/dashboard/js/render-plans.js +82 -13
- 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/settings.js +100 -11
- package/dashboard/layout.html +6 -0
- package/dashboard/pages/engine-memory-panel.html +49 -0
- package/dashboard/pages/engine.html +1 -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/styles.css +39 -0
- package/dashboard-build.js +17 -2
- package/dashboard.js +469 -21
- package/docs/README.md +8 -1
- package/docs/auto-discovery.md +40 -0
- package/docs/branch-derivation.md +13 -1
- package/docs/cross-repo-plans.md +292 -0
- package/docs/deprecated.json +4 -4
- package/docs/pr-auto-fix-dispatch.md +64 -0
- package/docs/pr-review-fix-loop.md +1 -1
- package/docs/watches.md +1 -0
- package/engine/ado.js +1 -10
- package/engine/diagnostics-memory.js +190 -0
- package/engine/dispatch.js +53 -0
- package/engine/lifecycle.js +155 -191
- package/engine/meeting.js +30 -0
- package/engine/playbook.js +15 -0
- package/engine/queries.js +165 -5
- package/engine/runtimes/copilot.js +19 -0
- package/engine/shared.js +303 -3
- package/engine/watchdog.js +6 -0
- package/engine.js +576 -113
- package/package.json +2 -2
- package/playbooks/plan-to-prd.md +25 -2
- package/playbooks/plan.md +4 -2
package/docs/README.md
CHANGED
|
@@ -7,12 +7,13 @@ A navigable index of every Markdown file under `docs/`. Entries are grouped by a
|
|
|
7
7
|
Hands-on stories and distribution guides for people running or evaluating Minions.
|
|
8
8
|
|
|
9
9
|
- [blog-first-successful-dispatch.md](blog-first-successful-dispatch.md) — Narrative walkthrough of the first end-to-end agent dispatch and the seven failed spawn attempts that preceded it.
|
|
10
|
-
- [distribution.md](distribution.md) — How Minions is published
|
|
10
|
+
- [distribution.md](distribution.md) — How Minions is published (this repo: `@opg-microsoft/minions` to GitHub Packages; paired peer `yemi33/minions` to npm) and the bidirectional sync contract — automated opg → yemi33 backport workflow + manual yemi33 → opg sync PRs.
|
|
11
11
|
|
|
12
12
|
## Contributor-facing
|
|
13
13
|
|
|
14
14
|
Architecture, design proposals, and lifecycle references for people working on the engine, dashboard, or playbooks.
|
|
15
15
|
|
|
16
|
+
- [branch-derivation.md](branch-derivation.md) — Engine-side branch fallback (`work/<wi-id>`) vs. agent-authored long form, the structured-vs-loose PR-pointer extractors, and the canonical PR-fix duplication incident.
|
|
16
17
|
- [command-center.md](command-center.md) — Command Center (CC) chat panel: persistent Sonnet sessions, `--resume` semantics, system-prompt invalidation, and per-tab session storage.
|
|
17
18
|
- [completion-reports.md](completion-reports.md) — Canonical schema for the per-spawn completion JSON: trust nonce, `failure_class` enum, `noop` semantics, `retryable` / `needs_rerun` shape, and the artifacts array.
|
|
18
19
|
- [constants.md](constants.md) — Cross-cutting status / type / condition constants (`WI_STATUS`, `WORK_TYPE`, `PR_STATUS`, `WATCH_CONDITION`, …) and the no-magic-strings invariant.
|
|
@@ -21,14 +22,17 @@ Architecture, design proposals, and lifecycle references for people working on t
|
|
|
21
22
|
- [cooldown-merge-semantics.md](cooldown-merge-semantics.md) — Scoping deliverable defining merge semantics for `saveCooldowns` (longer-of TTL merge, key-level upserts, gitignored on-disk format).
|
|
22
23
|
- [copilot-cli-schema.md](copilot-cli-schema.md) — Behavior and schema reference for the GitHub Copilot CLI adapter (capability flags, stdin vs `-p`, model discovery, effort levels).
|
|
23
24
|
- [dead-code-audit-retractions.md](dead-code-audit-retractions.md) — Retracted dead-code-audit findings (false positives) that future audits MUST read before re-citing.
|
|
25
|
+
- [deprecated-process.md](deprecated-process.md) — Schema for `docs/deprecated.json` and the weekly `cleanup-deprecated` audit walk that retires entries past their removal signal.
|
|
24
26
|
- [design-state-storage.md](design-state-storage.md) — Design proposal evaluating five database options for replacing Minions' file-based JSON state; recommends `node:sqlite` as the medium-term target (accepted; implementation tracked in CHANGELOG.md Phases 0–9).
|
|
25
27
|
- [harness-mode.md](harness-mode.md) — Tri-Agent Harness Mode (`harness_mode: "tri_agent"` on scheduled tasks): Planner → Generator → Evaluator loop that iterates a shared on-disk artifact until a rubric passes or the iteration cap fires.
|
|
26
28
|
- [kb-sweep.md](kb-sweep.md) — Knowledge-base consolidation sweep (hash dedup → LLM batch dedup/reclassify → per-entry compress) and the detached runner that keeps it alive across `minions restart`.
|
|
27
29
|
- [keep-processes.md](keep-processes.md) — `meta.keep_processes` sidecar contract: when to use it vs managed-spawn, sidecar schema, caps, and the [`engine/keep-process-sweep.js`](../engine/keep-process-sweep.js) lifecycle.
|
|
30
|
+
- [live-checkout-mode.md](live-checkout-mode.md) — Per-project opt-in `worktreeMode: 'live'`: skips `git worktree add` and dispatches in-place inside `project.localPath` for `repo`-managed trees, submodule-heavy repos, deep Windows paths, and native build state. Includes the refuse-on-dirty contract and the per-project mutating-concurrency cap of 1.
|
|
28
31
|
- [managed-spawn.md](managed-spawn.md) — Engine-owned long-running services (managed-spawn primitive): sidecar schema, healthcheck examples, lifecycle, dashboard API, and the WI 1 (build) → WI 2 (test) chained-validation pattern.
|
|
29
32
|
- [plan-lifecycle.md](plan-lifecycle.md) — Full plan pipeline from `/plan` through PRD materialization, dispatch with dependency gating, verify task, and human archive.
|
|
30
33
|
- [pr-comment-followup.md](pr-comment-followup.md) — PR-comment follow-up dispatch contract: fix/review agents may spin off a new WI via `POST /api/work-items` with `meta.pr_followup` instead of broadening the current PR or rebutting the comment.
|
|
31
34
|
- [pr-review-fix-loop.md](pr-review-fix-loop.md) — How the engine moves a PR from creation through review, fix dispatch, and re-review, including stale-status guards.
|
|
35
|
+
- [project-skills.md](project-skills.md) — Project-local skill discovery (`.claude/skills/`, `.claude/commands/`, `CLAUDE.md` / `.github/copilot-instructions.md` slash-command mentions): how dispatched agents see and steer toward purpose-built tooling the project ships, plus the intent-vocabulary contract.
|
|
32
36
|
- [qa-runbook-lifecycle.md](qa-runbook-lifecycle.md) — End-to-end QA runbook lifecycle (W-mpeiwz6k0005bf34): runbook + run-record storage, `POST /api/qa/runbooks/run` dispatch into the `qa-validate` playbook, artifact contract, and how the `/qa` page mirrors managed-spawn observability.
|
|
33
37
|
- [qa-runbooks.md](qa-runbooks.md) — Per-project QA runbook schema, storage layout (`projects/<name>/runbooks/<id>.json`), CRUD endpoints, run-record lifecycle, and the `qa-validate` agent sidecar contract.
|
|
34
38
|
- [rfc-completion-json.md](rfc-completion-json.md) — RFC for replacing stdout regex-scraping with a structured `completion.json` control-plane protocol.
|
|
@@ -38,8 +42,10 @@ Architecture, design proposals, and lifecycle references for people working on t
|
|
|
38
42
|
- [slim-ux/concepts.md](slim-ux/concepts.md) — Slim-UX design notes: simplified surface concepts driving the project picker, inline project link, and decoupled folder picker.
|
|
39
43
|
- [slim-ux/architecture-suggestions.md](slim-ux/architecture-suggestions.md) — Slim-UX follow-up architecture suggestions paired with `concepts.md`.
|
|
40
44
|
- [team-memory.md](team-memory.md) — Per-agent memory layer (`knowledge/agents/<id>.md`) and the consolidation/routing rules that populate it from `notes/inbox/`.
|
|
45
|
+
- [timeouts-and-liveness.md](timeouts-and-liveness.md) — What kills (or doesn't kill) a live tracked agent: the wall-clock vs steering kill invariants, spawn-phase watchdog gates, steering safety nets, and stale-orphan detection ladder.
|
|
41
46
|
- [watches.md](watches.md) — Persistent monitoring jobs (`engine/watches.json`): target-type registry, conditions, follow-up actions, and the `watches.d/` plugin folder.
|
|
42
47
|
- [workspace-manifests.md](workspace-manifests.md) — Declarative per-agent permission scoping: `allowed_tools` / `allowed_repos` / `allowed_external_urls` / `memory_scope`, dispatch-time repo gate, and runtime `--allowedTools` narrowing.
|
|
48
|
+
- [worktree-lifecycle.md](worktree-lifecycle.md) — Worktree pool recycling, the live-dispatch guard that prevents wiping an agent's unpushed work, the dirty/divergent quarantine path, and the Windows EPERM/EBUSY file-lock retry footgun.
|
|
43
49
|
|
|
44
50
|
## Operations
|
|
45
51
|
|
|
@@ -50,6 +56,7 @@ Operational runbooks for engine operators and fleet maintainers.
|
|
|
50
56
|
- [human-vs-automated.md](human-vs-automated.md) — Quick reference table of which features humans start, run, decide, and recover, and the two human approval gates.
|
|
51
57
|
- [kb-sweep.md](kb-sweep.md) — Knowledge-base sweep runbook: how `engine/kb-sweep.js` consolidates `notes/inbox/` into `knowledge/` and survives `minions restart`.
|
|
52
58
|
- [onboarding.md](onboarding.md) — First-30-minutes walkthrough for a new operator: install, init, dispatch a first work item, watch it land.
|
|
59
|
+
- [preflight.md](preflight.md) — `minions doctor` and the lighter per-CLI `minions preflight` checks: what each non-self-explanatory row (permission bypass, runtime detection, drive-root, etc.) is asserting.
|
|
53
60
|
- [security.md](security.md) — Threat model: single-user/loopback deployment assumptions, dashboard Origin gate, data-flow trust boundaries, secret handling, and known residual risks (CSRF sweep, prompt injection, log-redactor audit).
|
|
54
61
|
|
|
55
62
|
---
|
package/docs/auto-discovery.md
CHANGED
|
@@ -57,6 +57,46 @@ Skips PRs where `status !== "active"`.
|
|
|
57
57
|
|
|
58
58
|
Inside `discoverFromPrs()`, ADO and GitHub projects first resolve their own provider poll gate (`adoPollEnabled` or `ghPollEnabled`). PR-derived automation is inert when that provider's polling is off, so cached build, vote, conflict, and comment state cannot trigger new dispatches. The shared dispatch toggles (`autoReviewPrs`, `autoReReviewPrs`, `autoFixReviewFeedback`, `autoFixHumanComments`, `autoFixBuilds`, and `autoFixConflicts`) apply to both providers. `evalLoop` gates the minion review loop: initial minion reviews, minion re-reviews, and minion review-feedback fixes. Human-feedback fixes are evaluated outside `evalLoop`. Conflict fixes are additionally gated by `!fixDispatched`, so an earlier successful human/review/build fix dispatch in the same PR discovery pass suppresses the conflict fix until a later pass.
|
|
59
59
|
|
|
60
|
+
**`autoReReviewPrs` gates both re-review paths (`P-e8b1c4d2`).** Re-reviews fire from two places: (1) the open-loop **discovery** path in `engine.js:discoverFromPrs` (PR shows `reviewStatus=waiting` after a fix push), and (2) the **closure-loop** in `engine/lifecycle.js:dispatchReReviewForFix` (a fix WI completes whose meta carries `addresses_review_wi`, queuing the next review against the same PR). The `engine.autoReReviewPrs` toggle is read at BOTH sites so flipping it OFF mutes the entire re-review cycle — not just the discovery sweep. Default ON; flip via Dashboard → Settings → Auto-fix & Review Loop, or `engine.autoReReviewPrs: false` in `config.json`.
|
|
61
|
+
|
|
62
|
+
**Hard-stop kill-switch (`pollingPaused`).** `engine.pollingPaused: true` is a master override that wins over both `adoPollEnabled` and `ghPollEnabled`. When ON, section 2.6/2.7 of the tick cycle skips `pollPrStatus` and `pollPrHumanComments` for both providers, and `discoverFromPrs` forces `pollEnabled=false` for every project so every per-PR auto-dispatch gate (`autoReviewPrs` / `autoFixBuilds` / `autoFixConflicts` / `autoFixReviewFeedback` / `autoFixHumanComments`) becomes inert. Reconciliation (the recovery sweep that follows section 2.7) is intentionally not gated. The engine logs `[engine] PR polling paused — …` once on the transition from unpaused → paused; routine PR poll log lines resume when cleared. Flip via Dashboard → Settings → Polling, or set `engine.pollingPaused: true` in `config.json`. Default OFF — fresh installs behave identically to before this knob existed.
|
|
63
|
+
|
|
64
|
+
**Hard-stop kill-switch (`autoFixPaused`).** `engine.autoFixPaused: true` is a narrower master override that wins over every auto-fix dispatch gate. When ON, `discoverFromPrs` forces `autoFixBuilds` / `autoFixConflicts` / `autoFixReviewFeedback` / `autoFixHumanComments` to false for every project, so no fix agent is auto-dispatched against any PR. Review dispatch (`autoReviewPrs` / `autoReReviewPrs`), PR status / human-comment polling, and reconciliation are intentionally not gated — operators can pause a fix-storm during an incident while still seeing fresh review verdicts and build status. The engine logs `[engine] auto-fix paused — …` once on the transition from unpaused → paused; routine discovery resumes when cleared. Flip via Dashboard → Settings → Auto-fix & Review Loop, or set `engine.autoFixPaused: true` in `config.json`. Default OFF — fresh installs behave identically to before this knob existed.
|
|
65
|
+
|
|
66
|
+
**Granular per-poller flags (`P-c4d8e1a3`).** The legacy `adoPollEnabled` / `ghPollEnabled` macros are bundle toggles that silence three axes at once (status, comments, reconcile) when set to `false`. To turn off only one axis, set the matching granular flag in `config.engine`:
|
|
67
|
+
|
|
68
|
+
| Flag | Default | Phase | Composes with `pollingPaused`? |
|
|
69
|
+
|------|---------|-------|--------------------------------|
|
|
70
|
+
| `adoPrStatusPollEnabled` | `true` | Section 2.6 (ADO `pollPrStatus`) | Yes — status polls honor the master killswitch |
|
|
71
|
+
| `adoPrCommentsPollEnabled` | `true` | Section 2.7 (ADO `pollPrHumanComments`) | Yes |
|
|
72
|
+
| `adoPrReconcileEnabled` | `true` | Section 2.7 tail (ADO `reconcilePrs`) | No — reconcile is a recovery sweep |
|
|
73
|
+
| `ghPrStatusPollEnabled` | `true` | Section 2.6 (GitHub `ghPollPrStatus`) | Yes |
|
|
74
|
+
| `ghPrCommentsPollEnabled` | `true` | Section 2.7 (GitHub `ghPollPrHumanComments`) | Yes |
|
|
75
|
+
| `ghPrReconcileEnabled` | `true` | Section 2.7 tail (GitHub `ghReconcilePrs`) | No — reconcile is a recovery sweep |
|
|
76
|
+
| `processPendingRebasesEnabled` | `true` | Section 2.6 tail (`processPendingRebases`) | No — rebase processor has no legacy macro |
|
|
77
|
+
|
|
78
|
+
Resolution order (`shared.resolvePollFlag(engineCfg, granularKey, legacyMacroKey)`): (1) granular flag explicitly set → wins; (2) legacy macro is `false` → propagates `false` to all three axes of that provider (including reconcile); (3) otherwise → `ENGINE_DEFAULTS[granularKey]` (`true`).
|
|
79
|
+
|
|
80
|
+
**Breaking change vs. legacy semantics:** Before P-c4d8e1a3, the legacy `adoPollEnabled: false` / `ghPollEnabled: false` silenced only status + comments polls, leaving reconcile running. The new `resolvePollFlag` contract propagates the legacy `false` to reconcile too. Operators who want the old behavior (status + comments OFF, reconcile ON) must now set `adoPrReconcileEnabled: true` / `ghPrReconcileEnabled: true` explicitly alongside the legacy macro. Default-config installs (no overrides) and installs that flip only the new granular flags are unaffected.
|
|
81
|
+
|
|
82
|
+
Flip via Dashboard → Settings → Polling → "Granular per-poller controls" collapsible, or set the keys directly in `config.engine`.
|
|
83
|
+
|
|
84
|
+
**Granular work-discovery flags (`P-d6f0a2b5`).** Each phase inside `engine.discoverWork()` is independently gateable, so operators can silence a single discovery source without disabling the other phases or touching the per-project `project.workSources.*.enabled` toggles. The two layers compose: a `false` at either the global flag or the per-project `workSources.*.enabled` level skips the matching call.
|
|
85
|
+
|
|
86
|
+
| Flag | Default | Phase inside `discoverWork()` |
|
|
87
|
+
|------|---------|--------------------------------|
|
|
88
|
+
| `prDiscoveryEnabled` | `true` | Per-project `discoverFromPrs(config, project)` — PR-derived fix / review / build-test work |
|
|
89
|
+
| `workItemsDiscoveryEnabled` | `true` | Per-project `discoverFromWorkItems(config, project)` — project-local `work-items.json` scan |
|
|
90
|
+
| `centralWorkDiscoveryEnabled` | `true` | `discoverCentralWorkItems(config)` — top-level project-agnostic `work-items.json` scan |
|
|
91
|
+
| `scheduledWorkDiscoveryEnabled` | `true` | `discoverScheduledWork(config)` — cron-style scheduled tasks + scheduled meetings |
|
|
92
|
+
| `planMaterializationEnabled` | `true` | `reconcilePrdStatuses(config)` + `materializePlansAsWorkItems(config)` (paired side-effect passes) |
|
|
93
|
+
|
|
94
|
+
Resolution is intentionally simpler than the granular per-poller flags above: each call site uses the inline check `if (config.engine?.<flag> !== false) { ... }`, so a missing or `true` value runs the phase and an explicit `false` skips it. There is no legacy macro to fall back to, and `shared.resolvePollFlag` is **not** used here. Default-config installs (no overrides) behave identically to before this knob existed.
|
|
95
|
+
|
|
96
|
+
Composition with per-project gates: setting `workItemsDiscoveryEnabled: false` skips `discoverFromWorkItems` even for projects whose own `workSources.workItems.enabled` is `true`; setting it back to `true` restores the per-project gate that `discoverFromWorkItems` already honors internally. Same pattern for `prDiscoveryEnabled` vs. `workSources.pullRequests.enabled`. `scheduledWorkDiscoveryEnabled: false` stops cron-style scheduled tasks from firing without touching the scheduler config; `planMaterializationEnabled: false` suppresses the `reconcilePrdStatuses` + `materializePlansAsWorkItems` pair atomically (useful during a long migration that must not auto-create work items).
|
|
97
|
+
|
|
98
|
+
Flip via Dashboard → Settings → Polling → "Granular work-discovery controls (P-d6f0a2b5)" collapsible, or set the keys directly in `config.engine`.
|
|
99
|
+
|
|
60
100
|
### Source 2: PRD Gap Analysis (via `materializePlansAsWorkItems`)
|
|
61
101
|
|
|
62
102
|
PRD items flow through `materializePlansAsWorkItems()`, which scans `~/.minions/prd/*.json` for PRD files with `missing` / `updated` / `planned` items and creates work items in the target project's queue.
|
|
@@ -44,6 +44,18 @@ When a ref is detected, `copyWorkItemPrFields` stamps
|
|
|
44
44
|
`targetPr` / `pr_id` / `prNumber`, `item.branch` is unset, and
|
|
45
45
|
`discoverFromWorkItems` reuses the PR's source branch.
|
|
46
46
|
|
|
47
|
+
**Type gate (W-mqbaby2a000pa8ee).** The loose description/title scan
|
|
48
|
+
(step 4 above) only stamps on `type: "fix"` WIs. Non-fix WIs
|
|
49
|
+
(implement / explore / test / review / …) only get stamped from
|
|
50
|
+
structured fields (steps 1-3). Without this gate, an implement WI
|
|
51
|
+
whose description merely mentions an existing PR in prose
|
|
52
|
+
("Class bug surfaced today on pull request 130") would silently
|
|
53
|
+
get `targetPr` / `pr_id` / `prNumber` stamped, route through the
|
|
54
|
+
PR-fix dispatch path, fail the PR-branch lookup, and stick in
|
|
55
|
+
`_pendingReason: null` forever. Structured intent
|
|
56
|
+
(`targetPr` / `prUrl` / `references[].url` / etc.) still stamps on
|
|
57
|
+
every type — only the loose regex scan is gated.
|
|
58
|
+
|
|
47
59
|
## Structured-vs-loose split (W-mq18ec6h000p7b87)
|
|
48
60
|
|
|
49
61
|
The PR-ref extractor has **two** variants — pick the right one for the
|
|
@@ -52,7 +64,7 @@ call site:
|
|
|
52
64
|
| Helper | What it walks | Used by | Why |
|
|
53
65
|
|--------|---------------|---------|-----|
|
|
54
66
|
| `shared.extractStructuredWorkItemPrRef(item)` | Structured fields + `references[*].url` + `meta.pr_followup.parent_pr_url`. **No** description / title scan. | `engine.js#getStructuredWorkItemPrRef` → `pr_not_found` dispatch gate. | Gating blocks dispatch and MUST require explicit operator intent. A description like "see PR #3015 for context" must NOT trip the gate. |
|
|
55
|
-
| `shared.extractWorkItemPrRef(item)` | Structured walk + last-resort description / title scan. | `engine.js#getWorkItemPrRef` (branch derivation, prompt PR context, `resolveWorkItemPrRecord`); `dashboard.js#getWorkItemPrRef` (POST `/api/work-items` create-time `targetPr` stamping). | Callers downgrade gracefully when no PR record matches; stamp path preserves the operator UX of pasting a PR URL into description prose and getting `targetPr` auto-stamped. |
|
|
67
|
+
| `shared.extractWorkItemPrRef(item)` | Structured walk + last-resort description / title scan. | `engine.js#getWorkItemPrRef` (branch derivation, prompt PR context, `resolveWorkItemPrRecord`); `dashboard.js#getWorkItemPrRef` (POST `/api/work-items` create-time `targetPr` stamping, **type-gated on `fix` — W-mqbaby2a000pa8ee**). | Callers downgrade gracefully when no PR record matches; stamp path preserves the operator UX of pasting a PR URL into description prose and getting `targetPr` auto-stamped on `type: "fix"` WIs. |
|
|
56
68
|
|
|
57
69
|
**Rule of thumb: gate uses structured-only; stamp uses loose.**
|
|
58
70
|
Stamping is best-effort and reversible; gating blocks dispatch and should
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
# Cross-repo plans
|
|
2
|
+
|
|
3
|
+
> A cross-repo plan is a single Minions plan whose work items ship into **two or more configured projects**. The plan has no owning project; each `missing_features` item declares its own `project`, the materializer fans the items into per-project `work-items.json` files, and verify completion fans out into one verify work item per touched repo.
|
|
4
|
+
>
|
|
5
|
+
> Shipped by `PL-cross-repo-coordination` (`prd/minions-opg-2026-06-10-3.json`, items `P-7a3f1c08` … `P-8b5d3e07`). Default plan shape is unchanged for single-project plans — everything in this doc only activates when the plan markdown declares more than one project.
|
|
6
|
+
|
|
7
|
+
## When to use a cross-repo plan
|
|
8
|
+
|
|
9
|
+
Use a cross-repo plan when **one coherent feature touches code in multiple repos and the work should be tracked together**. Common examples:
|
|
10
|
+
|
|
11
|
+
- An engine change in `minions` + a matching opg compliance file in `minions-opg`.
|
|
12
|
+
- A protocol/contract change in repo A whose downstream consumer in repo B must land in lockstep.
|
|
13
|
+
- A docs/runbook sweep that updates the same convention across all configured repos.
|
|
14
|
+
|
|
15
|
+
Use **separate single-project plans** when the repos are evolving independently and you only happen to be working on both at once — there's no win in coupling unrelated PRs onto a single PRD lifecycle.
|
|
16
|
+
|
|
17
|
+
A cross-repo plan does NOT mean "shared git history". Repos are still separate Git origins and each item gets its own PR in its own repo. The "cross-repo" part is purely a coordination layer in the engine: one PRD, per-item project routing, one rollup card on the dashboard, fan-out verify at the end.
|
|
18
|
+
|
|
19
|
+
## Authoring (plan markdown → PRD)
|
|
20
|
+
|
|
21
|
+
### 1. Plan markdown declares projects
|
|
22
|
+
|
|
23
|
+
A cross-repo plan is detected by a single signal at parse time: `shared.extractPlanTargetProjects(planContent)` returns ≥ 2 entries AND `shared.extractPlanDeclaredProject(planContent)` returns `''` (no singular `Project:` line). The parser accepts two forms, both anchored in the first 80 lines:
|
|
24
|
+
|
|
25
|
+
**Primary signal — structured HTML comment** (preferred, written by the engine's plan template):
|
|
26
|
+
|
|
27
|
+
```markdown
|
|
28
|
+
<!-- minions:targetProjects=minions,minions-opg -->
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Fallback — human-readable plural `Projects:` line** (works when an operator hand-edits a plan):
|
|
32
|
+
|
|
33
|
+
```markdown
|
|
34
|
+
**Projects:** minions, minions-opg
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Both forms accept `,`, `|`, or `;` as separators with surrounding whitespace, and strip surrounding quotes. See `engine/shared.js#extractPlanTargetProjects` (line ~3713) for the exact regex.
|
|
38
|
+
|
|
39
|
+
The dashboard's Create-Plan modal (P-b51c08af) renders a multi-select project picker. Submitting two or more projects normalizes to a deduped array and `POST /api/plans/create` writes the plan with the plural `**Projects:**` header + HTML comment marker, no singular `**Project:**` line. Selecting zero or one project keeps the legacy singular header (no behavioural change for single-project plans).
|
|
40
|
+
|
|
41
|
+
### 2. Plan-to-prd dispatch
|
|
42
|
+
|
|
43
|
+
When the central planner picks up a plan-to-prd work item, `engine.js#discoverCentralWorkItems` (~line 7470) parses both project signals from the plan content and sets `vars.target_projects` on the dispatch when the plan is cross-repo. The `plan-to-prd.md` playbook (P-3b8c40d9) injects a `{{target_projects}}` section that:
|
|
44
|
+
|
|
45
|
+
- Tells the agent to **omit the top-level `"project"` field** on the PRD JSON (or set it to `""`).
|
|
46
|
+
- Requires **every `missing_features` item to carry an explicit `project`** chosen from the target list.
|
|
47
|
+
- Defaults `branch_strategy` to `"parallel"` (a literal shared git history across separate repos is impossible — `shared-branch` is only correct as a naming convention when the plan author explicitly asked to keep matching branch names across repos for a single launch).
|
|
48
|
+
- Reminds the agent that `depends_on` across projects is **advisory** (see below) and cross-repo cycles are still cycles.
|
|
49
|
+
|
|
50
|
+
The plan-to-prd dispatch itself runs in the first listed project's read-only worktree so `vars.project_path` resolves and the playbook's per-project carve-outs behave; the dispatch never mutates that project.
|
|
51
|
+
|
|
52
|
+
### Authored PRD shape
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"plan": "minions-opg-2026-06-10-3",
|
|
57
|
+
"project": "",
|
|
58
|
+
"branch_strategy": "parallel",
|
|
59
|
+
"missing_features": [
|
|
60
|
+
{
|
|
61
|
+
"id": "P-aaa1",
|
|
62
|
+
"name": "Engine plumbing in minions",
|
|
63
|
+
"project": "minions",
|
|
64
|
+
"depends_on": []
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"id": "P-bbb2",
|
|
68
|
+
"name": "Compliance file in minions-opg",
|
|
69
|
+
"project": "minions-opg",
|
|
70
|
+
"depends_on": ["P-aaa1"]
|
|
71
|
+
}
|
|
72
|
+
]
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The top-level `project` is empty. Each item names its own repo. `depends_on` may freely cross project boundaries.
|
|
77
|
+
|
|
78
|
+
## Per-item project routing
|
|
79
|
+
|
|
80
|
+
`engine.js#materializePlansAsWorkItems` (~line 5151) groups PRD `missing_features` by `item.project` into a per-project map, then writes each group to `shared.projectWorkItemsPath(project)` — i.e. `<MINIONS_DIR>/projects/<name>/work-items.json`. The routing rules:
|
|
81
|
+
|
|
82
|
+
| `item.project` value | Where it lands |
|
|
83
|
+
| --- | --- |
|
|
84
|
+
| Names a configured project | That project's `projects/<name>/work-items.json` |
|
|
85
|
+
| Names an unknown project | Skipped; PRD item stamped with `_invalidProject: <error>` (see `formatUnknownProjectError`) |
|
|
86
|
+
| Unset, and central WIs disabled | Falls back to the plan-level `defaultProject` (or skipped if neither is resolvable) |
|
|
87
|
+
| Unset, and central WIs enabled | Lands in central `work-items.json` (legacy single-project plans) |
|
|
88
|
+
|
|
89
|
+
The materializer enforces a single per-project lock per write (`mutateWorkItems(wiPath, …)`), so per-project writes never contend. Cross-project re-opens (e.g. a `missing` PRD item whose previous WI lives in a sibling project) are queued as `deferredReopens` and executed outside the current project's lock (see `engine.js:5297` for the pattern).
|
|
90
|
+
|
|
91
|
+
**Reconciliation, PRD-removal sync, and `i.projects[]` rollup** all happen per-project under the same loop. Per-item state is owned by exactly one project's file at a time, so reading `projectWorkItemsPath(project)` always gives a consistent slice for that repo.
|
|
92
|
+
|
|
93
|
+
## Cycle detection (runs ONCE over the whole plan)
|
|
94
|
+
|
|
95
|
+
`detectDependencyCycles(plan.missing_features)` (`engine.js:4638`) runs once over the full PRD `missing_features` set **before** the per-project grouping loop, so a cyclic chain that crosses repos (e.g. `P-aaa1` in `minions` → `P-bbb2` in `minions-opg` → `P-aaa1`) is detected and every cyclic item is skipped at materialization time — neither project's `work-items.json` receives the cyclic items.
|
|
96
|
+
|
|
97
|
+
Caveat: `detectDependencyCycles` is a partial DFS — 2-cycles (`A → B → A`) are fully detected, but N-cycles ≥ 3 with distinct nodes only flag the back-edge target and recursing parent. Both legs of a 2-cycle and the canonical back-edge of a longer cycle reliably get skipped. Fixing the partial detector is tracked separately; cross-repo plans inherit the same behaviour as single-project plans.
|
|
98
|
+
|
|
99
|
+
## Cross-repo `depends_on` (advisory merge)
|
|
100
|
+
|
|
101
|
+
`item.depends_on` may freely reference a work-item id that lives in another project. The dispatcher in `engine.js#spawnAgent` (~line 2498) partitions resolved dep branches into two buckets via `dep.isCrossRepo`:
|
|
102
|
+
|
|
103
|
+
1. **Same-project deps** — fetched into the dispatcher's `rootDir` and merged into the worktree using the historical loop (with ancestor pruning, preflight conflict detection, ADO bearer-token retry, etc.). A merge failure here is a real failure (`FAILURE_CLASS.MERGE_CONFLICT` after preflight, retryable).
|
|
104
|
+
|
|
105
|
+
2. **Cross-repo deps** — **advisory only**. The engine:
|
|
106
|
+
- Fetches the dep branch into the **dep's** project rootDir (the current project's origin doesn't carry the ref).
|
|
107
|
+
- Transfer-fetches the tip into the current worktree under the namespaced ref `refs/cross-repo-deps/<branch>` via a filesystem-path remote.
|
|
108
|
+
- Computes a best-effort tip SHA + changed-file list (vs the dep project's main).
|
|
109
|
+
- Appends a `## Cross-repo dependencies` section to the prompt (`engine.js#buildCrossRepoDepsSection`, ~line 671) listing the repo, branch, short SHA, ref, and changed files.
|
|
110
|
+
- **Never merges** the dep branch into the worktree (cross-repo merges would commingle unrelated repo content).
|
|
111
|
+
- **Never escalates** on fetch failure — the dispatch proceeds with an advisory entry that records the attempt with `files: []`.
|
|
112
|
+
- Cross-repo deps with no resolvable `projectRoot` (missing `localPath` on the dep project) are dropped with a warn and never reach the agent.
|
|
113
|
+
|
|
114
|
+
The agent's contract with cross-repo deps is read-only inspection (`git show refs/cross-repo-deps/<branch>`, `git log refs/cross-repo-deps/<branch> -- <files>`) followed by code changes in **its** repo that honor the contract surfaced in the dep branch. This matches the natural semantics of independent repos: there's no single git history to merge into.
|
|
115
|
+
|
|
116
|
+
## Shared-branch pre-creation (per project, idempotent)
|
|
117
|
+
|
|
118
|
+
When a cross-repo plan declares `branch_strategy: "shared-branch"` with an explicit `feature_branch`, the materializer's tail block in `engine.js#materializePlansAsWorkItems` (~line 5318) pre-creates that branch in **every project the plan touched** before the first dispatcher pass:
|
|
119
|
+
|
|
120
|
+
```text
|
|
121
|
+
for each project P in itemsByProject (excluding the `_central` bucket):
|
|
122
|
+
root = path.resolve(P.localPath)
|
|
123
|
+
main = shared.resolveMainBranch(root, P.mainBranch)
|
|
124
|
+
if not git rev-parse --verify --quiet refs/heads/<branch>:
|
|
125
|
+
git branch <branch> <main>
|
|
126
|
+
git push -u origin <branch> # no-op if origin already has the tip
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The block is fully idempotent — `rev-parse --verify` guards the local `git branch` create, and `git push -u origin <branch>` is a no-op when origin already has the tip. Per-project failure isolation is explicit: a broken `localPath` or git config in one project logs a warn and continues to the next. The `_central` pseudo-project bucket (legacy central work-items.json) is skipped because it has no project root.
|
|
130
|
+
|
|
131
|
+
This is what makes the per-project dispatcher's later `git worktree add ... origin/<branch>` work — each project's origin already has the branch pointing at its own main, and parallel dispatches across repos can each commit + push to the same branch name in their own repo independently.
|
|
132
|
+
|
|
133
|
+
`shared-branch` is **only a naming convention** across cross-repo plans, not a literal shared git history. Two PRs ship — one per repo — and each repo's branch evolves independently. The dashboard groups them by `plan.feature_branch` for display.
|
|
134
|
+
|
|
135
|
+
## Per-project dispatch (read-side)
|
|
136
|
+
|
|
137
|
+
`engine.js#discoverFromWorkItems(config, project)` (~line 6671) reads exactly one project's `projectWorkItemsPath(project)` per call, routes pending items via `routing.md`, and returns `newWork[]` entries for the dispatcher. Cross-repo plans don't change this loop — the per-project file is the source of truth for what each project's dispatcher sees, and the engine iterates all configured projects per tick.
|
|
138
|
+
|
|
139
|
+
Module-level `_claimedAgents` in `engine/routing.js` persists across in-process calls but is cleared per tick (`routing.resetClaims()`), so two items in different projects routed to the same agent in the same tick contend correctly for the single agent slot.
|
|
140
|
+
|
|
141
|
+
## Per-project verify fan-out
|
|
142
|
+
|
|
143
|
+
`engine/lifecycle.js#checkPlanCompletion` (line 24, fan-out at lines 217–274) gates on two conditions:
|
|
144
|
+
|
|
145
|
+
1. Every PRD `missing_features` item has been materialized into a WI.
|
|
146
|
+
2. Every materialized WI is in a terminal state (`DONE_STATUSES` or `cancelled`).
|
|
147
|
+
|
|
148
|
+
When both gates pass, the function groups **active PRs** by project — reading each project's `pull-requests.json` via `shared.projectPrPath(p)`, joining to the plan's done items via `engine/pr-links.json` (`getPrLinks()`). The result is a `touchedProjects` array: every project that has at least one active PR linked to one of the plan's done items.
|
|
149
|
+
|
|
150
|
+
For each touched project the fan-out (lines 245–415):
|
|
151
|
+
|
|
152
|
+
- Looks up any existing verify WI keyed on `(sourcePlan, itemType:'verify', project)`. Legacy single-WI plans match against the primary project name when `existingVerify.project` is unset.
|
|
153
|
+
- Skips if the existing verify is active; re-opens if it's eligible (`isReopenableVerify`).
|
|
154
|
+
- Otherwise builds a per-project setup block:
|
|
155
|
+
- **Shared-branch plans** reuse `feature_branch` directly — the verify agent checks out the branch in-place in the project's localPath.
|
|
156
|
+
- **Parallel-branch plans** merge each linked PR branch into a per-project worktree at `<localPath>/../worktrees/verify-<projName>-<planSlug>`.
|
|
157
|
+
- Validates every branch ref through `shared.validateGitRef` before splicing into the bash setup commands (P-f3-verify-prompt — defense in depth against shell injection from a crafted `feature_branch` or PR `branch` field).
|
|
158
|
+
- Writes a new verify WI into the touched project's `projectWorkItemsPath(p)` with `itemType: 'verify'`, `type: 'verify'`, `priority: 'high'`, `status: 'pending'`, and `project: <projName>`. The title is project-prefixed (`Verify plan (minions-opg): …`) when `touchedProjects.length > 1` to disambiguate the dashboard listing.
|
|
159
|
+
|
|
160
|
+
**Fallback:** when no PRs were ever linked (e.g. items completed without PR records, or legacy state), the fan-out collapses to a single verify WI on the **primary project** (the project with the most completed items) so the plan still gets verified.
|
|
161
|
+
|
|
162
|
+
The verify agents then run in parallel — one per touched project, each in its own worktree, each running its own build + test cycle — and each writes a per-project verify guide.
|
|
163
|
+
|
|
164
|
+
## Dashboard surfaces
|
|
165
|
+
|
|
166
|
+
Plan card and detail views surface the cross-repo shape:
|
|
167
|
+
|
|
168
|
+
- **`i.projects[]` per PRD item + plan `_projects` rollup** (P-e8d49105, `engine/queries.js`) — populated when at least one PRD item carries a `project` field. Plan card uses `_projects.length >= 2` as the gate for cross-repo UI variations.
|
|
169
|
+
- **Per-project status rollup pills** (P-66b1faec, `dashboard.js#handlePlansList`) — when `Object.keys(_perProjectProgress).length >= 2`, the plan card meta line renders one pill per project with a per-project completion count (e.g. `minions 3/4 · minions-opg 1/2`). Single-project plans render the legacy aggregate progress bar unchanged.
|
|
170
|
+
- **Per-project verify badges in plan detail** (P-d7e592b1, `dashboard/js/render-plans.js`) — when `verifyWis.length >= 2`, the detail view renders one labelled verify badge per project; single-project plans render the legacy single badge unchanged.
|
|
171
|
+
|
|
172
|
+
## Back-compat contract
|
|
173
|
+
|
|
174
|
+
The single-project path is the unchanged default. Specifically:
|
|
175
|
+
|
|
176
|
+
| Surface | Single-project plan | Cross-repo plan |
|
|
177
|
+
| --- | --- | --- |
|
|
178
|
+
| Plan markdown header | `**Project:** <name>` | `**Projects:** a, b` + `<!-- minions:targetProjects=a,b -->` |
|
|
179
|
+
| `extractPlanDeclaredProject` | Returns `<name>` | Returns `''` |
|
|
180
|
+
| `extractPlanTargetProjects` | Returns `[]` | Returns `[a, b]` (≥ 2 entries) |
|
|
181
|
+
| PRD `project` field | `<name>` | `""` (or omitted) |
|
|
182
|
+
| PRD `missing_features[i].project` | Usually unset | **Always set**, must be in target list |
|
|
183
|
+
| `materializePlansAsWorkItems` grouping | One project (or central) | One project bucket per distinct `item.project` |
|
|
184
|
+
| Shared-branch pre-create | Only the plan's project | Every touched project (per-project, idempotent) |
|
|
185
|
+
| `checkPlanCompletion` verify | One verify WI on primary project | One verify WI per touched project |
|
|
186
|
+
| Dashboard plan card | Aggregate progress bar | Per-project rollup pills (when `_projects.length >= 2`) |
|
|
187
|
+
| Dashboard plan detail verify | Single badge | One labelled badge per project (when `verifyWis.length >= 2`) |
|
|
188
|
+
|
|
189
|
+
In every row, the cross-repo path is gated on the cross-repo signal (`target_projects` parsed from plan markdown, or per-item `project` field populated). If the signal is absent, the surface behaves exactly as it did before `PL-cross-repo-coordination`.
|
|
190
|
+
|
|
191
|
+
## Failure modes
|
|
192
|
+
|
|
193
|
+
| Signal | Outcome |
|
|
194
|
+
| --- | --- |
|
|
195
|
+
| PRD item `project` names an unknown project | Item skipped at materialization; PRD item stamped with `_invalidProject: <error>` via `mutatePrdLocked`. Surfaces in dashboard PRD view; fixable by editing the PRD or registering the project. |
|
|
196
|
+
| Cross-repo `depends_on` references unknown project | Dep dropped with a warn (`Skipping cross-repo dep <branch> — dep project "<name>" has no resolvable rootDir`); dispatch proceeds. |
|
|
197
|
+
| Cross-repo dep branch fetch fails | Advisory entry emitted with `files: []`; dispatch proceeds, agent sees a prompt section noting the attempt. No retry, no escalation. |
|
|
198
|
+
| Cross-repo cycle in `depends_on` | Detected at materialization; every cyclic item skipped, no WIs written. (2-cycles fully detected; N≥3-cycles partially detected — back-edge target + recursing parent.) |
|
|
199
|
+
| Shared-branch pre-create fails in one project | Logged at `warn`, per-project loop continues to the next project. The dispatcher will fail later when it can't `git worktree add ... origin/<branch>` in that project; the inbox alert from that failure points back to the missing branch. |
|
|
200
|
+
| Verify completion finds no active PRs linked | Falls back to a single verify WI on the primary project (the project with the most completed items). |
|
|
201
|
+
|
|
202
|
+
## Worked example
|
|
203
|
+
|
|
204
|
+
Plan markdown `plans/cross-repo-launch.md`:
|
|
205
|
+
|
|
206
|
+
```markdown
|
|
207
|
+
# Cross-repo launch coordination
|
|
208
|
+
|
|
209
|
+
**Projects:** minions, minions-opg
|
|
210
|
+
<!-- minions:targetProjects=minions,minions-opg -->
|
|
211
|
+
|
|
212
|
+
## Context
|
|
213
|
+
|
|
214
|
+
Add a new compliance check + matching engine plumbing.
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Operator approves the plan. Plan-to-prd dispatches against the `minions` project (read-only worktree, first in the target list). The plan-to-prd agent writes PRD `prd/cross-repo-launch.json`:
|
|
218
|
+
|
|
219
|
+
```json
|
|
220
|
+
{
|
|
221
|
+
"plan": "cross-repo-launch",
|
|
222
|
+
"project": "",
|
|
223
|
+
"branch_strategy": "parallel",
|
|
224
|
+
"missing_features": [
|
|
225
|
+
{
|
|
226
|
+
"id": "P-001",
|
|
227
|
+
"name": "Add compliance-check helper in engine",
|
|
228
|
+
"project": "minions",
|
|
229
|
+
"depends_on": []
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
"id": "P-002",
|
|
233
|
+
"name": "Wire compliance helper into opg dashboard",
|
|
234
|
+
"project": "minions-opg",
|
|
235
|
+
"depends_on": ["P-001"]
|
|
236
|
+
}
|
|
237
|
+
]
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Engine ticks once:
|
|
242
|
+
|
|
243
|
+
1. `materializePlansAsWorkItems` groups by project: `{ minions: [P-001], minions-opg: [P-002] }`. Cycle scan runs once over both items, finds nothing. Writes `projects/minions/work-items.json` with P-001 and `projects/minions-opg/work-items.json` with P-002. Branch-strategy is `parallel`, so no shared-branch pre-create runs.
|
|
244
|
+
2. `discoverFromWorkItems(config, minions)` picks up P-001, routes it via `routing.md`, dispatches `implement` agent. Worktree created on `work/P-001` off `minions`' main.
|
|
245
|
+
3. `discoverFromWorkItems(config, minions-opg)` picks up P-002. Its `depends_on: ["P-001"]` resolves to a same-project dep? **No** — P-001 lives in the `minions` project, so it's a **cross-repo dep**. The dispatcher fetches P-001's branch into the `minions` rootDir, transfer-fetches it into the `minions-opg` worktree under `refs/cross-repo-deps/work/P-001`, computes a short SHA + changed-file list, and appends a `## Cross-repo dependencies` section to the prompt. The agent inspects via `git show` / `git log` and ships matching code in `minions-opg`.
|
|
246
|
+
4. Both items complete with PRs. `engine/pr-links.json` links each PR to its WI.
|
|
247
|
+
5. Next tick `lifecycle.checkPlanCompletion` runs. Gates pass (every item materialized + done). Groups active PRs by project: `{ minions: [PR for P-001], minions-opg: [PR for P-002] }`. Creates two verify WIs:
|
|
248
|
+
- `(minions)` verify WI in `projects/minions/work-items.json` with `project: 'minions'`.
|
|
249
|
+
- `(minions-opg)` verify WI in `projects/minions-opg/work-items.json` with `project: 'minions-opg'`.
|
|
250
|
+
6. Both verify agents dispatch in parallel. Each builds + tests its own repo. Each writes `prd/guides/verify-cross-repo-launch.md` in its own repo's worktree.
|
|
251
|
+
7. Dashboard plan card shows `minions 2/2 · minions-opg 2/2` rollup pills and two labelled verify badges. Operator archives the plan via `POST /api/plans/archive` when both verifies complete.
|
|
252
|
+
|
|
253
|
+
Two PRs ship — one per repo — coordinated by a single plan that lived in the central plans tree.
|
|
254
|
+
|
|
255
|
+
## Code-site index
|
|
256
|
+
|
|
257
|
+
When you need to change the cross-repo path, the load-bearing seams are:
|
|
258
|
+
|
|
259
|
+
| Concern | File:line | Notes |
|
|
260
|
+
| --- | --- | --- |
|
|
261
|
+
| Parse plan markdown projects | `engine/shared.js:3690` (`extractPlanDeclaredProject`), `engine/shared.js:3713` (`extractPlanTargetProjects`) | Both scan first 80 lines; HTML comment marker preferred over `**Projects:**` fallback. |
|
|
262
|
+
| `POST /api/plans/create` array normalization | `dashboard.js` (P-2e9b54d1) | Dedup + per-name validation via `shared.findProjectByName`; 0–1 → singular header, 2+ → plural header + marker. |
|
|
263
|
+
| Plan-to-prd dispatch wiring | `engine.js:7470+` (`discoverCentralWorkItems`) | Sets `vars.target_projects` when plan is cross-repo; picks first listed project for the read-only worktree. |
|
|
264
|
+
| Plan-to-prd playbook contract | `playbooks/plan-to-prd.md` (P-3b8c40d9) | Tells the agent to omit top-level `project`, set per-item `project`, default to `parallel`. |
|
|
265
|
+
| Per-project work-item grouping | `engine.js:5151–5181` (`materializePlansAsWorkItems`) | `itemsByProject` map; unknown-project items stamped `_invalidProject`. |
|
|
266
|
+
| Cross-repo cycle detection | `engine.js:5183–5192` | One global scan over `plan.missing_features` BEFORE per-project loop. |
|
|
267
|
+
| Shared-branch pre-create per project | `engine.js:5318–5356` | Idempotent `rev-parse` → `branch` → `push -u`; per-project failure isolation. |
|
|
268
|
+
| Cross-repo dep partitioning + advisory fetch | `engine.js:2495–2682` (`spawnAgent` dep loop) | `isCrossRepo` split; transfer-fetch via filesystem-path remote into `refs/cross-repo-deps/<branch>`. |
|
|
269
|
+
| Cross-repo dep prompt section | `engine.js:662–710` (`buildCrossRepoDepsSection`) | Renders one section per dep with repo, branch, SHA, files, local ref. |
|
|
270
|
+
| Per-project verify fan-out | `engine/lifecycle.js:217–274` (`checkPlanCompletion`) | Group active PRs by project via `engine/pr-links.json`; one verify WI per touched project. |
|
|
271
|
+
| Verify WI creation | `engine/lifecycle.js:387–411` | `itemType:'verify'`, `project: projName`, project-prefixed title when `touchedProjects.length > 1`. |
|
|
272
|
+
| Per-project rollup pills | `dashboard.js#handlePlansList` (P-66b1faec) | Gate `_perProjectProgress.length >= 2`. |
|
|
273
|
+
| Per-project verify badges | `dashboard/js/render-plans.js` (P-d7e592b1) | Gate `verifyWis.length >= 2`. |
|
|
274
|
+
| `i.projects[]` + plan `_projects` rollup | `engine/queries.js` (P-e8d49105) | Populated when at least one PRD item has a `project` field. |
|
|
275
|
+
|
|
276
|
+
## Executable spec
|
|
277
|
+
|
|
278
|
+
`test/integration/cross-repo-plan-e2e.test.js` (P-8b5d3e07) drives the four load-bearing seams against the real engine code in an isolated `MINIONS_TEST_DIR` (no live state touched, no agents spawned):
|
|
279
|
+
|
|
280
|
+
1. PRD with per-item `project` fans into per-project `work-items.json` files (alpha vs beta).
|
|
281
|
+
2. `discoverFromWorkItems(config, project)` returns only the queried project's items.
|
|
282
|
+
3. `checkPlanCompletion` creates one verify WI per touched project when each project has at least one active PR linked to a done WI.
|
|
283
|
+
4. A 3-feature cyclic chain that crosses project boundaries (alpha → beta → alpha) is detected and none of the cyclic items are written.
|
|
284
|
+
|
|
285
|
+
Run via `node test/integration/cross-repo-plan-e2e.test.js` (or as part of `npm run test:integration`).
|
|
286
|
+
|
|
287
|
+
## Pointers
|
|
288
|
+
|
|
289
|
+
- Authoring entry points: [`playbooks/plan.md`](../playbooks/plan.md) (P-c1f87a92), [`playbooks/plan-to-prd.md`](../playbooks/plan-to-prd.md) (P-3b8c40d9).
|
|
290
|
+
- Single-project plan flow (the default): [`CLAUDE.md` → Plan → PRD → Work Items → Verify](../CLAUDE.md#plan--prd--work-items--verify).
|
|
291
|
+
- Worktree behaviour for parallel-branch verify worktrees: [`docs/worktree-lifecycle.md`](worktree-lifecycle.md).
|
|
292
|
+
- Live-checkout opt-out (rare; opt-in per project, not per plan): [`docs/live-checkout-mode.md`](live-checkout-mode.md).
|
package/docs/deprecated.json
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"id": "completion-fallback-parsers",
|
|
28
28
|
"description": "parseStructuredCompletion and parseCompletionFieldSummary in engine/lifecycle.js",
|
|
29
29
|
"file": "engine/lifecycle.js",
|
|
30
|
-
"lines": "
|
|
30
|
+
"lines": "3432, 3630",
|
|
31
31
|
"telemetryGate": "_engine.completionFallbacks must read 0 (both fenced and summary counters) across sweepWindowDays starting from sweepStartDate",
|
|
32
32
|
"sweepWindowDays": 14,
|
|
33
33
|
"sweepStartDate": "2026-06-11",
|
|
@@ -36,13 +36,13 @@
|
|
|
36
36
|
"enforcingSweepWindowTest": "test/unit/completion-fallback-sweep-window.test.js",
|
|
37
37
|
"symbols": ["parseStructuredCompletion", "parseCompletionFieldSummary"],
|
|
38
38
|
"removalSites": [
|
|
39
|
-
{ "file": "engine/lifecycle.js", "line":
|
|
40
|
-
{ "file": "engine/lifecycle.js", "line":
|
|
39
|
+
{ "file": "engine/lifecycle.js", "line": 4697, "symbol": "parseStructuredCompletion", "reason": "Gated fallback at engine/lifecycle.js:4697-4722 — removed in the same follow-up PR that drops the parser definitions." },
|
|
40
|
+
{ "file": "engine/lifecycle.js", "line": 4698, "symbol": "parseCompletionFieldSummary", "reason": "Gated fallback at engine/lifecycle.js:4697-4722 — removed in the same follow-up PR that drops the parser definitions." },
|
|
41
41
|
{ "file": "engine/timeout.js", "line": 646, "symbol": "parseStructuredCompletion", "reason": "Gated fallback inside detectNonTerminalResultSummary on the timeout/process-exit path. Already removed on yemi33/master in P-a7b2c1d9 (commit eb544aa80dbd64c87b78ad08a17ca3560e627094, merged 2026-06-10); tracked here so the static call-site audit still passes on opg-microsoft/minions where the P-a7b2c1d9 backport is pending. Stale entry once the opg sync lands and can be dropped together with the parser definitions." }
|
|
42
42
|
],
|
|
43
43
|
"allowedCallers": [],
|
|
44
44
|
"schemaRef": "docs/deprecated-process.md (see 'Static call-site audit fields')",
|
|
45
|
-
"notes": "Do NOT set removedAt until telemetry confirms zero usage across the sweepWindowDays from sweepStartDate. The follow-up code-removal PR (dropping parseStructuredCompletion at engine/lifecycle.js:
|
|
45
|
+
"notes": "Do NOT set removedAt until telemetry confirms zero usage across the sweepWindowDays from sweepStartDate. The follow-up code-removal PR (dropping parseStructuredCompletion at engine/lifecycle.js:3432, parseCompletionFieldSummary at :3630, the gated fallback at :4697-4722, and the opg-only timeout.js:646 caller after the P-a7b2c1d9 backport lands) is dispatched separately once the window is observed clean. The symbols/removalSites/allowedCallers triple is enforced by test/unit/deprecated-call-site-audit.test.js — see docs/deprecated-process.md for the schema. Line numbers in `lines` and `removalSites` were last refreshed against engine/lifecycle.js as of PR #118 (P-h14-repohost, commit e7f9181a); any subsequent edit that shifts these symbols must rebump both sets in the same PR or this audit fails."
|
|
46
46
|
},
|
|
47
47
|
{
|
|
48
48
|
"id": "config-claude-binary-override",
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# PR Auto-Fix Dispatch — site / cause / gate reference
|
|
2
|
+
|
|
3
|
+
How the engine decides whether to auto-dispatch a fix or review agent in response to PR state. This document is the short reference table for "which flag gates which site"; for the full lifecycle narrative, read [`docs/pr-review-fix-loop.md`](pr-review-fix-loop.md), and for the polling side, read [`docs/auto-discovery.md`](auto-discovery.md).
|
|
4
|
+
|
|
5
|
+
All dispatch decisions happen inside `engine.js#discoverFromPrs` once per discovery tick. The evaluator walks PRs in the order documented in `pr-review-fix-loop.md` §4 and consults the per-cause gate flag together with the master kill-switch and per-provider polling gate.
|
|
6
|
+
|
|
7
|
+
## Dispatch sites
|
|
8
|
+
|
|
9
|
+
| # | Cause | Trigger (PR state) | Per-cause gate flag | Hard-stop kill-switch | Other gates |
|
|
10
|
+
|---|------------------------|---------------------------------------------------|----------------------------|-----------------------|-----------------------------------------------------------|
|
|
11
|
+
| 1 | Initial minion review | `reviewStatus === 'pending'` | `autoReviewPrs` | `autoFixPaused`* | `evalLoop`; provider polling enabled |
|
|
12
|
+
| 2 | Human-feedback fix | `humanFeedback.pendingFix` (or coalesced) | `autoFixHumanComments` | `autoFixPaused` | provider polling enabled; `!awaitingReReview` |
|
|
13
|
+
| 3 | Minion re-review | `reviewStatus === 'waiting'` after fix push | `autoReReviewPrs` | `autoFixPaused`* | `evalLoop`; provider polling enabled |
|
|
14
|
+
| 4 | Minion review-feedback fix | `reviewStatus === 'changes-requested'` | `autoFixReviewFeedback` | `autoFixPaused` | `evalLoop`; provider polling enabled; `!awaitingReReview` |
|
|
15
|
+
| 5 | Build-failure fix | `buildStatus === 'failing'` + grace expired | `autoFixBuilds` | `autoFixPaused` | provider polling enabled; `_buildFixPushedAt` grace |
|
|
16
|
+
| 6 | Merge-conflict fix | `_mergeConflict === true` + `status === 'active'` | `autoFixConflicts` | `autoFixPaused` | provider polling enabled; `!fixDispatched` in same pass |
|
|
17
|
+
|
|
18
|
+
\* `autoFixPaused` is the auto-**fix** kill-switch. It forces sites 2, 4, 5, and 6 off, but is **not** wired to suppress review dispatch (sites 1 and 3) — review verdicts stay fresh during a fix-storm incident. To halt review dispatch, clear `autoReviewPrs` / `autoReReviewPrs` directly, or set `pollingPaused: true` (which forces every per-PR auto-dispatch gate inert by zeroing `pollEnabled`).
|
|
19
|
+
|
|
20
|
+
### Provider polling gate
|
|
21
|
+
|
|
22
|
+
Each PR carries a `repoHost` (`github` / `ado`). Before evaluating any of the six sites, `discoverFromPrs` resolves a `pollEnabled` boolean from that provider's poll gate:
|
|
23
|
+
|
|
24
|
+
- GitHub PRs honor `ghPollEnabled` (legacy macro) and the new granular `ghPrStatusPollEnabled` / `ghPrCommentsPollEnabled` / `ghPrReconcileEnabled`.
|
|
25
|
+
- ADO PRs honor `adoPollEnabled` and `adoPrStatusPollEnabled` / `adoPrCommentsPollEnabled` / `adoPrReconcileEnabled`.
|
|
26
|
+
|
|
27
|
+
When the provider's status poll is off, the cached `buildStatus` / `reviewStatus` / `_mergeConflict` won't refresh — but `discoverFromPrs` still runs against the **last known cache** unless `pollingPaused` is also set, which forces `pollEnabled=false` and makes every per-PR auto-dispatch gate inert.
|
|
28
|
+
|
|
29
|
+
See [`docs/auto-discovery.md`](auto-discovery.md) → "Granular per-poller flags" for the full polling-side table and the resolution order.
|
|
30
|
+
|
|
31
|
+
## Kill-switch hierarchy
|
|
32
|
+
|
|
33
|
+
There are two independent operator-facing master flags:
|
|
34
|
+
|
|
35
|
+
| Flag | Default | Effect |
|
|
36
|
+
|------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
37
|
+
| `pollingPaused` | `false` | Skips `pollPrStatus` + `pollPrHumanComments` for both providers. Forces `pollEnabled=false` in `discoverFromPrs` so every per-PR auto-dispatch gate (`autoReviewPrs` / `autoFixBuilds` / …) becomes inert. Reconciliation (recovery sweep) intentionally still runs. Default OFF — fresh installs behave identically. |
|
|
38
|
+
| `autoFixPaused` | `false` | Narrower: forces `autoFixBuilds` / `autoFixConflicts` / `autoFixReviewFeedback` / `autoFixHumanComments` to `false` for every project. Review dispatch, polling, and reconciliation are intentionally not gated, so operators can stop a fix-storm while still seeing fresh verdicts and build status. Default OFF. |
|
|
39
|
+
|
|
40
|
+
These compose: setting `pollingPaused: true` plus `autoFixPaused: true` halts *all* automated PR work (review + fix + polling). Setting only `autoFixPaused: true` halts only auto-fix dispatch. Setting only `pollingPaused: true` halts polling and effectively halts dispatch (because the per-PR gates rely on cached poll state), but leaves reconciliation running and lets manual `/api/pull-requests/observe` or operator-driven dispatch still proceed.
|
|
41
|
+
|
|
42
|
+
Flip via Dashboard → Settings → Polling (`pollingPaused`) / Auto-fix & Review Loop (`autoFixPaused`), or set `engine.<flag>: true` in `config.json`.
|
|
43
|
+
|
|
44
|
+
## Per-PR per-cause pause (different mechanism)
|
|
45
|
+
|
|
46
|
+
Independent of the master kill-switches, each PR tracks repeated no-op fix outcomes per cause in `pr._noOpFixes[<cause>]`. When the same fix dispatch returns "nothing to fix" multiple times against the same evidence fingerprint, `recordPrNoOpFixAttempt` flips `_noOpFixes[<cause>].paused = true` for that **single PR + cause** pair. The dashboard renders a red `_pausedCauses` chip; the recovery paths are:
|
|
47
|
+
|
|
48
|
+
1. Push a new SHA to the PR branch (fingerprint shifts → `clearPrNoOpFixAttempt` on next non-noop completion, or proactive GC in `pollPrStatus`).
|
|
49
|
+
2. Click the red chip → `POST /api/pull-requests/clear-paused-cause` with `{ prId, cause }`.
|
|
50
|
+
3. Direct API call to the same endpoint.
|
|
51
|
+
|
|
52
|
+
This per-PR per-cause pause is unrelated to `autoFixPaused`. Cause keys are validated against `shared.PR_FIX_CAUSE` (e.g. `BUILD_FAILURE`, `REVIEW_FEEDBACK`, `HUMAN_FEEDBACK`, `MERGE_CONFLICT`, `PR_FIX`). Full mechanics in [`docs/pr-review-fix-loop.md`](pr-review-fix-loop.md) §4E.
|
|
53
|
+
|
|
54
|
+
## Key files
|
|
55
|
+
|
|
56
|
+
| File | Role |
|
|
57
|
+
|---------------------------|---------------------------------------------------------------------------------------------------|
|
|
58
|
+
| `engine.js#discoverFromPrs` | Single dispatch site walker — evaluates the six causes per tick in fixed order |
|
|
59
|
+
| `engine/shared.js` (`ENGINE_DEFAULTS`) | Default values for every per-cause gate + both kill-switches |
|
|
60
|
+
| `engine/lifecycle.js#runPostCompletionHooks` | Wires `autoReReviewPrs` into the closure-loop re-review path (P-e8b1c4d2) |
|
|
61
|
+
| `dashboard/js/settings.js` | Auto-fix & Review Loop pane (gates + `autoFixPaused`); Polling pane (`pollingPaused` + granular polls) |
|
|
62
|
+
| `dashboard/js/render-dispatch.js` | `renderPausedBanner(engine)` — sticky cross-page banner showing both pause states |
|
|
63
|
+
| `POST /api/pull-requests/clear-paused-cause` | Clears a per-PR per-cause pause record |
|
|
64
|
+
| `POST /api/engine/polling/{pause,resume}`, `POST /api/engine/auto-fix/{pause,resume}` | Convenience endpoints for the two master flags (P-f3c9d0e7) |
|
|
@@ -34,7 +34,7 @@ How the engine manages the lifecycle of a PR from creation through review, fix,
|
|
|
34
34
|
|
|
35
35
|
When multiple problems coexist, earlier triggers get the first chance to enqueue work. The local `fixDispatched` flag is declared before the initial review trigger and set after first-review, human-feedback, review-feedback, and build-failure dispatches. Conflict fixes run last and explicitly require `!fixDispatched`, so any earlier successful review/fix dispatch suppresses the conflict fix for that PR in the same discovery pass. Build fixes are evaluated after human and minion review feedback, but the build-fix condition itself is not gated by `!fixDispatched`.
|
|
36
36
|
|
|
37
|
-
The engine does not cap review→fix cycles or build-fix attempts. Each trigger evaluates its own gates on every discovery pass; loops stop only when the underlying condition clears (reviewer approves, build passes, conflict resolves, human feedback handled). Operators who need to halt automation on a runaway PR
|
|
37
|
+
The engine does not cap review→fix cycles or build-fix attempts. Each trigger evaluates its own gates on every discovery pass; loops stop only when the underlying condition clears (reviewer approves, build passes, conflict resolves, human feedback handled). Operators who need to halt automation on a runaway PR have three escalating tools: (1) clear the relevant per-cause flag (`evalLoop`, `autoFixBuilds`, `autoFixConflicts`, `autoFixHumanComments`, `autoFixReviewFeedback`, `autoReviewPrs`, `autoReReviewPrs`); (2) flip the `autoFixPaused` master kill-switch to halt every auto-**fix** dispatch (sites B, C, D, and human-comment fix) at once while leaving review verdicts and polling fresh — Dashboard → Settings → Auto-fix & Review Loop or `POST /api/engine/auto-fix/pause`; (3) flip `pollingPaused` to halt PR polling entirely, which also forces every per-PR auto-dispatch gate inert. Per-PR per-cause noop pauses (`_noOpFixes[cause].paused`) are an orthogonal recovery surface — see §4E and `POST /api/pull-requests/clear-paused-cause`. See [docs/pr-auto-fix-dispatch.md](pr-auto-fix-dispatch.md) for the full site × gate × kill-switch reference table.
|
|
38
38
|
|
|
39
39
|
### A. Human comments (`humanFeedback.pendingFix`)
|
|
40
40
|
|
package/docs/watches.md
CHANGED
|
@@ -78,6 +78,7 @@ Target-type behavior in `engine/watches.js` is **data-driven via a registry** *(
|
|
|
78
78
|
- `fetchEntity(target, state)` — entity-or-null lookup
|
|
79
79
|
- `captureState(entity)` — snapshot used for change-detection diffs
|
|
80
80
|
- `evaluate(condition, entity, prevState, target)` — returns `{ triggered, message }`
|
|
81
|
+
- `isTerminalForCondition(condition, entity, prevState)` — *(optional)* W-mqa63opd000ha836. When a watch's `condition` cannot possibly fire again because the target reached a permanent terminal state (e.g. a PR was merged/closed/abandoned and the watch is on `build-fail`/`build-pass`/`vote-change`/`new-comments`/`head-commit-change`/`mergeable-flipped`/`behind-master`/`ready-for-merge`/`draft-flipped`), return `true` and the engine auto-expires the watch instead of polling forever. Default returns `false` so existing target types keep current behavior. The main check-loop guard only invokes this when `triggerCount > 0` **or** the prev-state status already matched the current entity status — so a watch armed on an already-terminal PR with `condition: merged` still gets its one shot via the absolute-condition fire-once path *(source: `engine/watches.js:157-160,542-549`)*.
|
|
81
82
|
|
|
82
83
|
The registry IS the allowlist for `createWatch` and `/api/watches/target-types`; the old hardcoded "pr or work-item" check is gone. Add a new target type at runtime with `registerTargetType(type, spec)` and look one up with `getTargetType(type)`. `listTargetTypes()` returns the serializable form used by the dashboard *(source: `engine/watches.js:124-174`)*.
|
|
83
84
|
|
package/engine/ado.js
CHANGED
|
@@ -993,15 +993,6 @@ async function forEachActivePr(config, token, callback) {
|
|
|
993
993
|
continue;
|
|
994
994
|
}
|
|
995
995
|
|
|
996
|
-
// Per-project throttle skip — emit one log line per skipped project, then continue.
|
|
997
|
-
// Sub-item W-mq03l6zh0006f0a1-b will replace the global isAdoThrottled() probe with
|
|
998
|
-
// a per-org `isOrgBaseThrottled(orgBase)` check so a 429 on one org no longer pauses
|
|
999
|
-
// polling for healthy orgs.
|
|
1000
|
-
if (isAdoThrottled()) {
|
|
1001
|
-
log('info', `[ado] PR poll skipped for ${project.name || project.repoName || 'unknown project'} — org ${orgBase} throttled`);
|
|
1002
|
-
continue;
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
996
|
// Parallelize PR polling within each project (max 5 concurrent to avoid rate limits)
|
|
1006
997
|
const CONCURRENCY = 5;
|
|
1007
998
|
for (let i = 0; i < activePrs.length; i += CONCURRENCY) {
|
|
@@ -2552,7 +2543,7 @@ function _resetAdoThrottle() {
|
|
|
2552
2543
|
|
|
2553
2544
|
/** Set throttle state directly — exported for testing only.
|
|
2554
2545
|
* Default orgBase keeps back-compat with arg-less callers that just want
|
|
2555
|
-
* "some org is throttled" semantics through
|
|
2546
|
+
* "some org is throttled" semantics through the arg-less throttle queries. */
|
|
2556
2547
|
function _setAdoThrottleForTest(state, orgBase = 'dev.azure.com/__test__') {
|
|
2557
2548
|
const tracker = getAdoThrottleForOrg(orgBase);
|
|
2558
2549
|
tracker._setForTest(state);
|