@yemi33/minions 0.1.2176 → 0.1.2178
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/dashboard/js/refresh.js +8 -0
- package/dashboard/js/render-dispatch.js +92 -0
- package/dashboard/js/render-plans.js +82 -13
- package/dashboard/js/settings.js +100 -11
- package/dashboard/js/utils.js +8 -5
- package/dashboard/layout.html +6 -0
- package/dashboard/slim/body.html +12 -9
- package/dashboard/slim/js/command-send.js +5 -1
- package/dashboard/slim/js/modals-tiles.js +89 -7
- package/dashboard/slim/js/projects.js +36 -28
- package/dashboard/slim/js/status.js +52 -4
- package/dashboard/slim/styles.css +165 -20
- package/dashboard/styles.css +39 -0
- package/dashboard.js +250 -6
- package/docs/README.md +8 -1
- package/docs/auto-discovery.md +40 -0
- 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/dispatch.js +53 -0
- package/engine/lifecycle.js +44 -190
- package/engine/meeting.js +30 -0
- package/engine/playbook.js +15 -0
- package/engine/queries.js +26 -1
- package/engine/runtimes/copilot.js +19 -0
- package/engine/shared.js +190 -1
- package/engine.js +531 -112
- package/package.json +1 -1
- package/playbooks/plan-to-prd.md +25 -2
- package/playbooks/plan.md +4 -2
|
@@ -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);
|
package/engine/dispatch.js
CHANGED
|
@@ -295,6 +295,33 @@ function _routeToReviewQueue(item, evaluation) {
|
|
|
295
295
|
});
|
|
296
296
|
}
|
|
297
297
|
|
|
298
|
+
/**
|
|
299
|
+
* Resolve whether the work item's parent PRD is in a "vetted" status —
|
|
300
|
+
* `approved` or `active` — for the PRD-sourced bypass (W-mq9acoo800177bcb).
|
|
301
|
+
* Returns true when the bypass should fire: PRD exists on disk and its
|
|
302
|
+
* status is one of the vetted set. Returns false on missing sourcePlan,
|
|
303
|
+
* missing/unreadable PRD file, or non-vetted status (rejected, paused,
|
|
304
|
+
* revision-requested, completed, awaiting-approval). Errors fail closed —
|
|
305
|
+
* if we can't confirm the PRD is approved/active, we let the normal
|
|
306
|
+
* validator run.
|
|
307
|
+
*
|
|
308
|
+
* @param {object} wi - the work item (item.meta.item)
|
|
309
|
+
* @returns {boolean}
|
|
310
|
+
*/
|
|
311
|
+
function _isPrdSourcedAndVetted(wi) {
|
|
312
|
+
const sourcePlan = wi && wi.sourcePlan;
|
|
313
|
+
if (!sourcePlan || typeof sourcePlan !== 'string') return false;
|
|
314
|
+
try {
|
|
315
|
+
const prdPath = path.join(queries.PRD_DIR, sourcePlan);
|
|
316
|
+
const plan = shared.safeJsonNoRestore(prdPath);
|
|
317
|
+
if (!plan || !plan.missing_features) return false;
|
|
318
|
+
const status = plan.status || shared.PLAN_STATUS.ACTIVE;
|
|
319
|
+
return status === shared.PLAN_STATUS.APPROVED || status === shared.PLAN_STATUS.ACTIVE;
|
|
320
|
+
} catch {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
298
325
|
/**
|
|
299
326
|
* Async wrapper around addToDispatch that consults the pre-dispatch validator
|
|
300
327
|
* when ENGINE_DEFAULTS.enablePreDispatchEval is true. Validator failures are
|
|
@@ -305,6 +332,8 @@ function _routeToReviewQueue(item, evaluation) {
|
|
|
305
332
|
* @param {object} [opts.config] - engine config; defaults to queries.getConfig().
|
|
306
333
|
* @param {Function} [opts.validate] - injection point for tests; defaults to
|
|
307
334
|
* require('./pre-dispatch-eval').validateAcceptanceCriteria.
|
|
335
|
+
* @param {Function} [opts.isPrdVetted] - injection point for tests; defaults
|
|
336
|
+
* to the on-disk PRD status read via _isPrdSourcedAndVetted.
|
|
308
337
|
* @returns {Promise<string|null>} dispatch id when queued; null when routed to review.
|
|
309
338
|
*/
|
|
310
339
|
async function addToDispatchWithValidation(item, opts = {}) {
|
|
@@ -328,6 +357,29 @@ async function addToDispatchWithValidation(item, opts = {}) {
|
|
|
328
357
|
return addToDispatch(item);
|
|
329
358
|
}
|
|
330
359
|
|
|
360
|
+
// W-mq9acoo800177bcb — PRD-sourced bypass. plan-to-prd already LLM-vets
|
|
361
|
+
// every item it lands in a PRD, so re-validating each materialized item
|
|
362
|
+
// per discovery tick is the dominant cost of the workflow this
|
|
363
|
+
// optimization targets (user-observed: 2 PRDs / 11 items → ~5 min
|
|
364
|
+
// queueing). Gate behind preDispatchEvalSkipPrdSourced (default true) and
|
|
365
|
+
// only fire when the parent PRD is approved/active — rejected / paused /
|
|
366
|
+
// revision-requested / completed PRDs still flow through the normal
|
|
367
|
+
// validator path so a stale PRD reopen can't smuggle in unvetted items.
|
|
368
|
+
const skipPrdSourced = config?.engine?.preDispatchEvalSkipPrdSourced
|
|
369
|
+
?? ENGINE_DEFAULTS.preDispatchEvalSkipPrdSourced;
|
|
370
|
+
if (skipPrdSourced) {
|
|
371
|
+
const isPrdVetted = typeof opts.isPrdVetted === 'function'
|
|
372
|
+
? opts.isPrdVetted
|
|
373
|
+
: _isPrdSourcedAndVetted;
|
|
374
|
+
if (isPrdVetted(wi)) {
|
|
375
|
+
// Stamp the skip marker on the dispatch item so the entry that lands
|
|
376
|
+
// in dispatch.json carries observability — operators can spot which
|
|
377
|
+
// items the validator skipped vs which it ran.
|
|
378
|
+
item._preDispatchEvalSkipped = 'prd-sourced';
|
|
379
|
+
return addToDispatch(item);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
331
383
|
const validate = typeof opts.validate === 'function'
|
|
332
384
|
? opts.validate
|
|
333
385
|
: require('./pre-dispatch-eval').validateAcceptanceCriteria;
|
|
@@ -1073,5 +1125,6 @@ module.exports = {
|
|
|
1073
1125
|
findActivePrOrBranchLock,
|
|
1074
1126
|
normalizeRetryableDecision,
|
|
1075
1127
|
isCompletedWorkItemForFailure,
|
|
1128
|
+
_isPrdSourcedAndVetted,
|
|
1076
1129
|
NON_MUTATING_DISPATCH_TYPES,
|
|
1077
1130
|
};
|