@yemi33/minions 0.1.2380 → 0.1.2381
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 +1 -1
- package/dashboard/js/render-other.js +43 -23
- package/dashboard/js/settings.js +40 -17
- package/dashboard.js +42 -47
- package/docs/live-checkout-mode.md +45 -26
- package/engine/dispatch.js +5 -3
- package/engine/lifecycle.js +59 -72
- package/engine/live-checkout.js +193 -149
- package/engine/playbook.js +12 -15
- package/engine/routing.js +1 -1
- package/engine/shared.js +136 -62
- package/engine.js +50 -15
- package/package.json +1 -1
- package/prompts/cc-system.md +7 -7
|
@@ -20,34 +20,34 @@ For these repos, the operator usually already has one canonical checkout that bu
|
|
|
20
20
|
|
|
21
21
|
## Contract (six guarantees)
|
|
22
22
|
|
|
23
|
-
Live mode is opinionated about what the engine will and will not do to the operator's tree. The contract has six guarantees; the engine enforces
|
|
23
|
+
Live mode is opinionated about what the engine will and will not do to the operator's tree. The contract has six guarantees; the engine enforces them by recovering when explicitly configured, otherwise leaving blocked work pending or failing closed.
|
|
24
24
|
|
|
25
25
|
### 1. Per-project mutating-concurrency cap of 1
|
|
26
26
|
|
|
27
|
-
Only one
|
|
27
|
+
Only one worktree-requiring dispatch (fix / build-fix-complex / implement / review / test / verify / decompose / setup / docs) runs per live-mode project at a time. Read-only types (meeting / ask / explore / plan / plan-to-prd) are excluded from the cap because they never write to disk.
|
|
28
28
|
|
|
29
29
|
Implementation: `engine.js` builds a `liveProjectsInUse` set from the active dispatch list at every allocation pass and per-tick re-annotation; pending items whose project is already in the set are skipped with a `skipReason` and re-tried on the next tick. (`engine.js:7198`, `engine.js:7460`.)
|
|
30
30
|
|
|
31
|
-
### 2.
|
|
31
|
+
### 2. Dirty-tree policy
|
|
32
32
|
|
|
33
|
-
**W-mrcifup2000c218a — detected at ALLOCATION time first.**
|
|
33
|
+
**W-mrcifup2000c218a — detected at ALLOCATION time first.** The allocator runs a cheap read-only dirty probe. When both auto-stash and auto-reset are disabled, a dirty hit stamps `_pendingReason: 'live_checkout_dirty'`, writes a deduped inbox alert, and leaves the WI pending without consuming a retry. When either recovery policy is enabled, dirt alone does not block allocation so `spawnAgent` can perform the configured recovery; the independent stale-base precheck still blocks committed base drift.
|
|
34
34
|
|
|
35
35
|
The in-spawn check below remains as a **defense-in-depth fallback** for the race window between the allocation-time probe and the actual git operations inside `spawnAgent` (e.g. the tree goes dirty in the few seconds between the pre-check and the spawn) — it is now the *exception* path, not the common path.
|
|
36
36
|
|
|
37
|
-
Before spawning, `engine/live-checkout.js#prepareLiveCheckout` runs `git status --porcelain` from `project.localPath`. Any output (staged, unstaged, untracked) **returns** an explicit `{ ok:false, reason:'dirty', dirtyFiles:[…] }` result
|
|
37
|
+
Before spawning, `engine/live-checkout.js#prepareLiveCheckout` first refuses an in-progress repository operation or detached HEAD, captures the original ref, then runs `git status --porcelain` from `project.localPath`. Any dirty output (staged, unstaged, untracked) **returns** an explicit `{ ok:false, reason:'dirty', dirtyFiles:[…] }` result unless explicit reset recovery is requested. `spawnAgent` uses that result to run auto-stash first and optional auto-reset second. Only when configured recovery is unavailable or fails does the confirmed-dirty result become a dispatch refusal:
|
|
38
38
|
|
|
39
39
|
- Non-retryable `FAILURE_CLASS.LIVE_CHECKOUT_DIRTY` (added to `engine/dispatch.js`'s `neverRetry` set so the dispatcher never re-spawns mechanically). **Reserved for this confirmed-dirty result only** — a thrown helper/git error is `LIVE_CHECKOUT_FAILED`, see below (#305). **Auto-cleanup exception (W-mqzmkoqt000hbca2, #582):** when `liveCheckoutAutoReset` or `liveCheckoutAutoStash` is enabled (per-project or fleet-wide), `spawnAgent` treats the dirty refusal as retryable instead — the engine re-cleans the tree on every attempt, so the two-strike cap below is skipped and the dispatch keeps retrying up to the normal `maxRetries` cap. The `dispatch.js` `neverRetry` entry is kept only as a defensive fallback for callers that omit the explicit `agentRetryable` override.
|
|
40
40
|
- Inbox alert written via `dispatch.writeInboxAlert('live-checkout-dirty-<wi-id>', body)`. The body lists the dirty files verbatim from `git status --porcelain`.
|
|
41
41
|
- Work item stamped with `_pendingReason: 'live_checkout_dirty'` so the dashboard surfaces the block.
|
|
42
42
|
- Completion summary: `live-checkout refused: N dirty file(s) in <localPath>`.
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
Outside the explicit auto-stash and auto-reset policies, the engine never resets, cleans, or stashes the operator checkout. Dispatch-scoped cleanup paths are no-ops because `worktreePath` stays null, and periodic worktree GC filters live-checkout projects out entirely.
|
|
45
45
|
|
|
46
|
-
#### 2b.
|
|
46
|
+
#### 2b. Auto-stash on dirty (W-mqtvnnj1000357fa)
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
Auto-stash is **ON by default**. The engine stashes dirty changes so dispatch can proceed without manual intervention; operators can opt out per project or fleet-wide:
|
|
49
49
|
|
|
50
|
-
-
|
|
50
|
+
- Configure via per-project `project.liveCheckoutAutoStash` (takes priority) or fleet-wide `engine.liveCheckoutAutoStash` (default `true`). Both are surfaced in Settings.
|
|
51
51
|
- When enabled and the tree is dirty, `spawnAgent` delegates the whole flow to `engine/live-checkout.js#applyLiveCheckoutAutoStash`, which calls `performLiveCheckoutAutoStash` to run `git stash push --include-untracked -m "minions-auto-stash-<dispatchId>-<timestamp>"` in `project.localPath` (`--include-untracked` so the `??` files that `git status --porcelain` counts as dirty are parked too). The stash git command runs **outside any file lock**.
|
|
52
52
|
- On stash **success** the helper re-runs `prepareLiveCheckout` (the tree is now clean), clears any stale `_pendingReason: 'live_checkout_dirty'` stamp (via the injected `clearDirtyStamp` callback), writes a `live-checkout-autostash-<wi-id>` inbox note with the stash name + manual-pop guidance, and returns `{ outcome:'stashed', liveResult }` so dispatch proceeds. If the re-preflight throws, it returns `{ outcome:'threw', error }` and `spawnAgent` fails the dispatch as `LIVE_CHECKOUT_FAILED`.
|
|
53
53
|
- On stash **failure** the error is surfaced (logged, never swallowed), the helper returns `{ outcome:'unchanged', liveResult }`, and the dispatch falls through to the normal retry-once-then-fail dirty path above.
|
|
@@ -61,25 +61,26 @@ A thrown error from `prepareLiveCheckout` — a required-arg/ref-validation guar
|
|
|
61
61
|
|
|
62
62
|
#### 2b. Opt-in auto-reset on dirty (`liveCheckoutAutoReset`, W-mqvejug6000eeb20)
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
Auto-stash is the default non-destructive recovery. `liveCheckoutAutoReset` is an additional **opt-in, destructive fallback** used only when auto-stash is disabled, skipped, or fails and the tree remains dirty. It removes uncommitted WIP without changing the current branch or any commit:
|
|
65
65
|
|
|
66
66
|
1. `prepareLiveCheckout` detects the dirty tree (same porcelain preflight as Guarantee 2).
|
|
67
67
|
2. It resolves the effective flag via `shared.resolveLiveCheckoutAutoReset(project, engine)` — **per-project `project.liveCheckoutAutoReset` (boolean) wins**, else the fleet-wide `engine.liveCheckoutAutoReset`, else `false`. (engine.js does not thread project/engine config into the helper, so the production path self-resolves by loading `config.json` and matching the project by `localPath`; it **fails closed** to `false` on any config read error so a glitch can never silently trigger a destructive reset.)
|
|
68
|
-
3.
|
|
69
|
-
4.
|
|
68
|
+
3. Before mutation, `prepareLiveCheckout` refuses an in-progress merge/rebase/cherry-pick/revert/bisect or detached HEAD.
|
|
69
|
+
4. If ON: `git reset --hard HEAD` discards tracked index/worktree changes and `git clean -fd` removes ordinary untracked files. The helper then **always re-runs the porcelain preflight once**, even when either command reports an error, because a failed command may have partially changed the tree. If the tree is verified clean, dispatch proceeds normally. Otherwise it returns the safe `{ ok:false, reason:'dirty' }` refusal with the actual remaining paths when available.
|
|
70
|
+
5. Every destructive attempt writes a uniquely keyed `live-checkout-autoreset-<attempt>-<dispatch-or-WI-id>` inbox note recording each command outcome plus the before/after dirty paths, including partial failures.
|
|
70
71
|
|
|
71
|
-
This is **DESTRUCTIVE** —
|
|
72
|
+
This is **DESTRUCTIVE** — tracked WIP and untracked files can be permanently unrecoverable. It does **not** fetch, switch branches, reset to a remote ref, or erase local commits; the stale-base guard still refuses a local base with unpushed committed drift. As of W-mqzbbhn2 it was **ON by default**; as of **W-mrawgw4q000a6bff it is OFF by default again** (`ENGINE_DEFAULTS.liveCheckoutAutoReset: false`). Set `liveCheckoutAutoReset: true` (per-project or fleet-wide) to opt back in.
|
|
72
73
|
|
|
73
74
|
- **Fleet-wide:** Dashboard → Settings → `Live-checkout auto-reset (fleet-wide)` (`engine.liveCheckoutAutoReset`).
|
|
74
75
|
- **Per-project override:** set `liveCheckoutAutoReset: true|false` on the project object in `config.json` (see the config snippet under *Enabling live mode*). A per-project boolean overrides the fleet-wide default for that project. *(Per-project Settings-UI persistence is deferred — the per-project override is config.json-only for now.)*
|
|
75
76
|
|
|
76
|
-
**Precedence when both `liveCheckoutAutoStash` and `liveCheckoutAutoReset` are enabled (W-mrawgw4q000a6bff).** `engine.js spawnAgent`
|
|
77
|
+
**Precedence when both `liveCheckoutAutoStash` and `liveCheckoutAutoReset` are enabled (W-mrawgw4q000a6bff).** `engine.js spawnAgent` tries the **non-destructive** auto-stash first: it resolves `liveCheckoutAutoStash` up front and, if enabled, calls the initial `prepareLiveCheckout` with `autoReset` forced `false`. `applyLiveCheckoutAutoStash` attempts the stash and its own internal re-preflight also forces `autoReset: false`. Only if the stash fails or is skipped (`outcome:'unchanged'`), the tree remains dirty, and `resolveLiveCheckoutAutoReset` resolves `true` does `spawnAgent` invoke `prepareLiveCheckout` again with `autoReset: true`. Neither recovery policy syncs the branch to origin.
|
|
77
78
|
|
|
78
79
|
#### 2c. Stale `.git/index.lock` self-heal (W-mr3tayu4)
|
|
79
80
|
|
|
80
81
|
Before any preflight git command runs, `prepareLiveCheckout` calls the shared, age-gated `shared.removeStaleIndexLock(localPath)` to clear a leftover `.git/index.lock` from a crashed/killed git process. Live mode never resets/cleans the operator tree, so nothing else would clear it, and — since `LIVE_CHECKOUT_FAILED` is retryable — every automatic retry used to hit the same stale lock and fail identically forever until an operator deleted it by hand. The remover only touches locks older than 5 minutes, so a lock held by a currently-running git process is never removed out from under it. `engine.js`'s own worktree-mode lock cleanup now delegates to the same shared helper.
|
|
81
82
|
|
|
82
|
-
### 3. No
|
|
83
|
+
### 3. No implicit remote sync or force checkout
|
|
83
84
|
|
|
84
85
|
After the clean-tree check, `prepareLiveCheckout`:
|
|
85
86
|
|
|
@@ -247,7 +248,7 @@ This mirrors, in CC-instruction form, what `prepareLiveCheckout` enforces engine
|
|
|
247
248
|
|
|
248
249
|
1. Open the Minions dashboard → **Settings** → **Projects** → expand the target project.
|
|
249
250
|
2. Set **Checkout mode** → **Live checkout**. A yellow warning chip appears immediately:
|
|
250
|
-
> ⚠ Live mode: dispatches run directly in this repo's checkout. Only one mutating dispatch runs at a time. Dirty
|
|
251
|
+
> ⚠ Live mode: dispatches run directly in this repo's checkout. Only one mutating dispatch runs at a time. Dirty-tree behavior follows the auto-stash/reset settings.
|
|
251
252
|
3. Click **Save**. The dashboard POSTs the change through `mergeSettingsConfigUpdate`; `shared.validateCheckoutMode` rejects anything other than `'worktree'` or `'live'` with HTTP 400 (the legacy `'isolated'` value is silently coerced to `'worktree'`).
|
|
252
253
|
|
|
253
254
|
### Enabling live mode (config.json)
|
|
@@ -258,10 +259,9 @@ This mirrors, in CC-instruction form, what `prepareLiveCheckout` enforces engine
|
|
|
258
259
|
"name": "android-aosp",
|
|
259
260
|
"localPath": "/home/yemi/aosp",
|
|
260
261
|
"checkoutMode": "live",
|
|
261
|
-
// "liveCheckoutAutoReset": true, // OPT-IN, DESTRUCTIVE —
|
|
262
|
-
//
|
|
263
|
-
// (
|
|
264
|
-
// inbox note). Overrides the fleet-wide engine.liveCheckoutAutoReset.
|
|
262
|
+
// "liveCheckoutAutoReset": true, // OPT-IN, DESTRUCTIVE fallback — if
|
|
263
|
+
// auto-stash is disabled/fails, reset tracked WIP to HEAD and clean
|
|
264
|
+
// untracked files (preserves branch + commits; logs an inbox note).
|
|
265
265
|
// …
|
|
266
266
|
}]
|
|
267
267
|
}
|
|
@@ -269,11 +269,30 @@ This mirrors, in CC-instruction form, what `prepareLiveCheckout` enforces engine
|
|
|
269
269
|
|
|
270
270
|
Absent / `null` / `''` reads as `'worktree'` (the default) — explicit is preferred. A legacy `"worktreeMode": "live"` is still honored (and `"worktreeMode": "isolated"` reads as `'worktree'`), but new configs should use `checkoutMode`.
|
|
271
271
|
|
|
272
|
+
### Enabling hybrid validation
|
|
273
|
+
|
|
274
|
+
Hybrid mode routes only selected canonical work-item types on the live checkout; every unselected type uses an isolated worktree. The normal configuration selects `test` and optionally creates one PR-targeted validation WI automatically:
|
|
275
|
+
|
|
276
|
+
```jsonc
|
|
277
|
+
{
|
|
278
|
+
"projects": [{
|
|
279
|
+
"name": "android-aosp",
|
|
280
|
+
"checkoutMode": "live",
|
|
281
|
+
"liveValidation": {
|
|
282
|
+
"type": "test",
|
|
283
|
+
"autoDispatch": true
|
|
284
|
+
}
|
|
285
|
+
}]
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
`type` may be a string or array, but every entry must be a live-capable canonical `WORK_TYPE`. `build-and-test` is a playbook alias and is normalized to `test` only for backward compatibility. `autoDispatch` must be boolean and requires `test` in the configured types; the generated WI has type `test`, carries structured `targetPr` / `pr_id` / URL-reference fields, targets the source PR branch, and renders the `build-and-test` playbook. Successful direct PR-backed `fix` and `build-fix-complex` dispatches, including human-feedback fixes, use the dispatch ID as their dedup key and do not add a nonexistent WI dependency.
|
|
290
|
+
|
|
272
291
|
### Enabling auto-stash on dirty (config.json)
|
|
273
292
|
|
|
274
293
|
```jsonc
|
|
275
294
|
{
|
|
276
|
-
// Fleet-wide fallback (default
|
|
295
|
+
// Fleet-wide fallback (default true) — applies to every live project that
|
|
277
296
|
// doesn't set its own override.
|
|
278
297
|
"engine": { "liveCheckoutAutoStash": true },
|
|
279
298
|
"projects": [{
|
|
@@ -287,11 +306,11 @@ Absent / `null` / `''` reads as `'worktree'` (the default) — explicit is prefe
|
|
|
287
306
|
}
|
|
288
307
|
```
|
|
289
308
|
|
|
290
|
-
With auto-stash on, a dirty tree is `git stash push --include-untracked`'d before dispatch instead of refusing (see [2b](#2b-
|
|
309
|
+
With auto-stash on, a dirty tree is `git stash push --include-untracked`'d before dispatch instead of refusing (see [2b](#2b-auto-stash-on-dirty-w-mqtvnnj1000357fa)). The engine never pops the stash — recover with `git stash pop` manually.
|
|
291
310
|
|
|
292
311
|
### Recovering from `live_checkout_dirty` refusal
|
|
293
312
|
|
|
294
|
-
When dispatch is blocked by a dirty tree, the dashboard shows the work item as pending with `_pendingReason: 'live_checkout_dirty'` and an inbox alert lists the dirty files.
|
|
313
|
+
When dispatch is blocked by a dirty tree, the dashboard shows the work item as pending with `_pendingReason: 'live_checkout_dirty'` and an inbox alert lists the dirty files. Enable auto-stash for non-destructive automatic recovery; enable [auto-reset](#2b-opt-in-auto-reset-on-dirty-livecheckoutautoreset-w-mqvejug6000eeb20) only as a destructive fallback. To recover manually from the project checkout:
|
|
295
314
|
|
|
296
315
|
```bash
|
|
297
316
|
# Option A — preserve work for later
|
|
@@ -305,7 +324,7 @@ git checkout -- .
|
|
|
305
324
|
git clean -fd
|
|
306
325
|
```
|
|
307
326
|
|
|
308
|
-
|
|
327
|
+
The next tick automatically re-picks the pending work item; nothing needs clearing on the engine side.
|
|
309
328
|
|
|
310
329
|
### Branch-leak mitigation
|
|
311
330
|
|
|
@@ -344,9 +363,9 @@ Live-checkout mode is deliberately small. These are NOT supported and will not b
|
|
|
344
363
|
| `engine/shared.js` — `CHECKOUT_MODES`, `validateCheckoutMode`, `resolveCheckoutMode`, `isLiveCheckoutProject` | Enum + validator + back-compat resolver (P-a3f9b201; consolidated W-mqiaw974). |
|
|
345
364
|
| `engine/shared.js` — `resolveLiveCheckoutAutoReset` + `ENGINE_DEFAULTS.liveCheckoutAutoReset` | Pure precedence resolver (per-project boolean > fleet-wide engine default > false) + the fleet-wide default (OFF as of W-mrawgw4q000a6bff — was ON as of W-mqzbbhn2). Gates the dirty-tree auto-reset in `prepareLiveCheckout` (W-mqvejug6000eeb20); in `engine.js spawnAgent` only fires as a fallback after auto-stash fails/is disabled (W-mrawgw4q000a6bff). |
|
|
346
365
|
| `engine/shared.js` — `resolveSpawnPaths` | Returns `{ cwd: localPath, worktreeRootDir: null, liveMode: true }` for live projects (P-a3f9b202). |
|
|
347
|
-
| `engine/live-checkout.js` — `prepareLiveCheckout` | Pure helper:
|
|
366
|
+
| `engine/live-checkout.js` — `prepareLiveCheckout` | Pure helper by default: pre-mutation mid-operation / detached-HEAD preflight, original-ref capture, dirty check, **already-on-branch fast path**, `refs/heads/<branch>` existence check, branch resolution from HEAD (no fetch — issue #226), **no-half-switch + `blob-fetch`/`worktree-conflict` classification**, and opt-in destructive WIP cleanup (`reset --hard HEAD` + `clean -fd` + re-check + audit note) that preserves the branch and committed history. Git probes use a **50 MB maxBuffer**. |
|
|
348
367
|
| `engine/live-checkout.js` — `restoreLiveCheckoutAtDispatchEnd` | Dispatch-end auto-restore (plain `git checkout <originalRef>`, never `--force`/reset/clean/stash, best-effort) + **self-healing dirty recovery** (auto-commit agent WIP onto the agent branch) + `live-checkout-failed-<dispatchId>` terminal-failure alert + `live-checkout-branch-<dispatchId>` fallback notify (now also on unexpected restore errors) (P-d9e6b2c4; self-heal PL-live-checkout-reliability-hardening). |
|
|
349
|
-
| `engine/live-checkout.js` — `resolveLiveCheckoutAutoStash`, `performLiveCheckoutAutoStash`, `applyLiveCheckoutAutoStash` |
|
|
368
|
+
| `engine/live-checkout.js` — `resolveLiveCheckoutAutoStash`, `performLiveCheckoutAutoStash`, `applyLiveCheckoutAutoStash` | Auto-stash resolver (per-project boolean, then fleet setting, then default true) + `git stash push --include-untracked` runner + stash/re-preflight orchestration. Engine never auto-pops (W-mqtvnnj1000357fa). |
|
|
350
369
|
| `engine/live-checkout.js` — `maybeRestoreLiveCheckoutFromRecord` | Shared wrapper that fires the dispatch-end restore from a persisted dispatch record; used by `cli.js` + both `timeout.js` reaping paths so a restart-spanning live dispatch is never stranded (PL-live-checkout-reliability-hardening). |
|
|
351
370
|
| `engine.js` — `spawnAgent` live-mode block | Calls `prepareLiveCheckout`, delegates opt-in auto-stash to `applyLiveCheckoutAutoStash` on a dirty tree (W-mqtvnnj1000357fa), handles dirty / throw branches, gates `git worktree add` on `!liveMode` (P-a3f9b204). |
|
|
352
371
|
| `engine.js` — `spawnAgent` mid-op / detached-HEAD refusal block | Emits `LIVE_CHECKOUT_MID_OPERATION`, writes `live-checkout-blocked-<wi-id>` alert, stamps `_pendingReason: 'live_checkout_mid_operation'` / `'live_checkout_detached_head'` (P-c5a1f3b8). |
|
package/engine/dispatch.js
CHANGED
|
@@ -53,6 +53,8 @@ const NON_MUTATING_DISPATCH_TYPES = new Set([
|
|
|
53
53
|
// PR source branch always differs and causes phantom auto-cancellation (#369).
|
|
54
54
|
const PR_BRANCH_TARGETED_TYPES = new Set([
|
|
55
55
|
WORK_TYPE.FIX,
|
|
56
|
+
WORK_TYPE.BUILD_FIX_COMPLEX,
|
|
57
|
+
WORK_TYPE.TEST,
|
|
56
58
|
WORK_TYPE.VERIFY,
|
|
57
59
|
WORK_TYPE.DECOMPOSE,
|
|
58
60
|
]);
|
|
@@ -148,7 +150,7 @@ function getPrDispatchDedupeKey(entry) {
|
|
|
148
150
|
if (!entry?.type) return null;
|
|
149
151
|
const targetKey = getPrDispatchTargetKey(entry);
|
|
150
152
|
if (!targetKey) return null;
|
|
151
|
-
const type = entry.type
|
|
153
|
+
const type = shared.isFixLikeWorkType(entry.type) ? WORK_TYPE.FIX : entry.type;
|
|
152
154
|
return `${targetKey}:${type}`;
|
|
153
155
|
}
|
|
154
156
|
|
|
@@ -197,7 +199,7 @@ function findActivePrOrBranchLock(dispatch, item) {
|
|
|
197
199
|
// PR-target dedup is FIX-only: a FIX shouldn't stack on top of another FIX
|
|
198
200
|
// for the same PR, but a REVIEW + FIX pair targeting the same PR is the
|
|
199
201
|
// normal review-then-fix flow and must not be dedup'd here.
|
|
200
|
-
if (item?.type
|
|
202
|
+
if (shared.isFixLikeWorkType(item?.type)) {
|
|
201
203
|
const prTargetKey = getPrDispatchTargetKey(item);
|
|
202
204
|
if (prTargetKey) {
|
|
203
205
|
const existing = active.find(d => getPrDispatchTargetKey(d) === prTargetKey);
|
|
@@ -703,7 +705,7 @@ async function pruneStalePrDispatchesAsync(config = queries.getConfig()) {
|
|
|
703
705
|
for (const entry of pending) {
|
|
704
706
|
const reason = reasonsById.get(entry.id);
|
|
705
707
|
if (!reason) continue;
|
|
706
|
-
if (entry.type
|
|
708
|
+
if (!shared.isFixLikeWorkType(entry.type) || !_MERGED_OR_ABANDONED_REASON_RE.test(reason)) continue;
|
|
707
709
|
try {
|
|
708
710
|
const wi = _resolveWorkItemForPrunedEntry(entry);
|
|
709
711
|
const scopePaths = shared.extractScopeFilePathsFromWorkItem(wi);
|
package/engine/lifecycle.js
CHANGED
|
@@ -1559,7 +1559,7 @@ function isPrAttachmentRequired(type, item, meta = {}) {
|
|
|
1559
1559
|
// PR — the agent updates meta.pr in place. The meta.pr short-circuit beats
|
|
1560
1560
|
// the explicit-flag fallthrough so a legacy requiresPr:true fix doesn't
|
|
1561
1561
|
// trigger the contract when there's already a PR attached.
|
|
1562
|
-
if ((type
|
|
1562
|
+
if ((shared.isFixLikeWorkType(type) || type === WORK_TYPE.TEST) && meta?.pr) return false;
|
|
1563
1563
|
|
|
1564
1564
|
// Standalone test work is usually pure build/run/verify. It should only be
|
|
1565
1565
|
// PR-required when the caller explicitly marks it as file-changing work.
|
|
@@ -1568,7 +1568,7 @@ function isPrAttachmentRequired(type, item, meta = {}) {
|
|
|
1568
1568
|
return explicit
|
|
1569
1569
|
|| type === WORK_TYPE.IMPLEMENT
|
|
1570
1570
|
|| type === WORK_TYPE.IMPLEMENT_LARGE
|
|
1571
|
-
|| type
|
|
1571
|
+
|| shared.isFixLikeWorkType(type);
|
|
1572
1572
|
}
|
|
1573
1573
|
|
|
1574
1574
|
function readOptionalJsonStrict(filePath, label, validate) {
|
|
@@ -5494,7 +5494,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5494
5494
|
// Auto-recover: if a failed implement/fix/test agent created PRs, it likely succeeded before the failure surfaced.
|
|
5495
5495
|
// P-d2a8f6c1: skip auto-recovery for nonce-mismatched dispatches — a forged
|
|
5496
5496
|
// report shouldn't be able to ride PRs into a "done" status.
|
|
5497
|
-
const prCreatingType = type === WORK_TYPE.IMPLEMENT || type === WORK_TYPE.IMPLEMENT_LARGE || type
|
|
5497
|
+
const prCreatingType = type === WORK_TYPE.IMPLEMENT || type === WORK_TYPE.IMPLEMENT_LARGE || shared.isFixLikeWorkType(type) || type === WORK_TYPE.TEST;
|
|
5498
5498
|
const autoRecovered = !nonceMismatch && !agentReportedFailure && !isSuccess && prsCreatedCount > 0 && prCreatingType && !!meta?.item?.id;
|
|
5499
5499
|
if (autoRecovered) {
|
|
5500
5500
|
log('info', `Auto-recovery: agent failed but created ${prsCreatedCount} PR(s) — upgrading ${meta.item.id} to done`);
|
|
@@ -5792,14 +5792,16 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5792
5792
|
try { stampPrdItemWorkItemId(meta.item.id, meta.item.sourcePlan); } catch (err) { log('warn', `stampPrdItemWorkItemId: ${err.message}`); }
|
|
5793
5793
|
}
|
|
5794
5794
|
promoteCompletionArtifacts(meta, agentId, dispatchItem.id, structuredCompletion, { resultSummary });
|
|
5795
|
-
// M003 — auto-dispatch a live-validation WI when a coding WI completes.
|
|
5796
|
-
try { autoDispatchLiveValidationWi(meta, config); } catch (err) { log('warn', `autoDispatchLiveValidationWi: ${err.message}`); }
|
|
5797
5795
|
// P-mqyp0009y025z6a7 — goal invalidation: cancel any pending/queued WIs
|
|
5798
5796
|
// listed in the completion report's optional `invalidates[]` field.
|
|
5799
5797
|
if (structuredCompletion && Array.isArray(structuredCompletion.invalidates) && structuredCompletion.invalidates.length > 0) {
|
|
5800
5798
|
try { applyGoalInvalidation(meta.item.id, structuredCompletion.invalidates, config); } catch (err) { log('warn', `applyGoalInvalidation: ${err.message}`); }
|
|
5801
5799
|
}
|
|
5802
5800
|
}
|
|
5801
|
+
if (effectiveSuccess && !skipDoneStatus) {
|
|
5802
|
+
// Coding WIs and direct PR-sourced fix dispatches share this follow-up path.
|
|
5803
|
+
try { autoDispatchLiveValidationWi(meta, config, dispatchItem); } catch (err) { log('warn', `autoDispatchLiveValidationWi: ${err.message}`); }
|
|
5804
|
+
}
|
|
5803
5805
|
// Failure retry is handled by completeDispatch in dispatch.js — not duplicated here.
|
|
5804
5806
|
// Only clear _decomposing flag on failure so decompose items don't get permanently stuck.
|
|
5805
5807
|
if (!effectiveSuccess && meta?.item?.id && type === WORK_TYPE.DECOMPOSE) {
|
|
@@ -5994,7 +5996,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5994
5996
|
|
|
5995
5997
|
let prFixBranchChange = null;
|
|
5996
5998
|
let prFixBuildStillFailing = null;
|
|
5997
|
-
if (type
|
|
5999
|
+
if (shared.isFixLikeWorkType(type) && effectiveSuccess && meta?.pr?.id) {
|
|
5998
6000
|
try {
|
|
5999
6001
|
prFixBranchChange = await detectPrFixBranchChange(meta, config);
|
|
6000
6002
|
} catch (err) {
|
|
@@ -6136,7 +6138,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
6136
6138
|
} else if (type === WORK_TYPE.REVIEW) {
|
|
6137
6139
|
log('warn', `Skipping PR review metadata update for ${meta?.pr?.id || meta?.pr?.url || '(unknown PR)'} because review dispatch ${dispatchItem.id} did not complete cleanly`);
|
|
6138
6140
|
}
|
|
6139
|
-
if (type
|
|
6141
|
+
if (shared.isFixLikeWorkType(type) && effectiveSuccess) {
|
|
6140
6142
|
updatePrAfterFix(meta?.pr, meta?.project, meta?.source, {
|
|
6141
6143
|
branchChanged: fixCompletionChangedBranch(structuredCompletion),
|
|
6142
6144
|
automationCauseKey: meta?.automationCauseKey,
|
|
@@ -6173,7 +6175,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
6173
6175
|
}
|
|
6174
6176
|
} catch (err) { log('warn', `PRD sync after fix: ${err.message}`); }
|
|
6175
6177
|
}
|
|
6176
|
-
} else if (type
|
|
6178
|
+
} else if (shared.isFixLikeWorkType(type) && meta?.pr?.id) {
|
|
6177
6179
|
// W-mph6br6a0006a2b9 (F9): the fix dispatch did not complete cleanly
|
|
6178
6180
|
// (agent crash, non-zero exit, hard contract failure, or nonce mismatch),
|
|
6179
6181
|
// so updatePrAfterFix above was skipped. Without a write here the PR's
|
|
@@ -6800,37 +6802,9 @@ function collapseAllDuplicatePrRecords(config) {
|
|
|
6800
6802
|
// a coding WI (not one of the validation types itself). Skips if the coding WI
|
|
6801
6803
|
// has no PR.
|
|
6802
6804
|
//
|
|
6803
|
-
//
|
|
6804
|
-
//
|
|
6805
|
-
//
|
|
6806
|
-
// explore, ask, docs, decompose, ...) is excluded even when it resolves a PR
|
|
6807
|
-
// ref and autoDispatch is on, because those completions don't author a code
|
|
6808
|
-
// diff worth re-validating — e.g. a `review` dispatch that merely comments on
|
|
6809
|
-
// someone else's PR previously spawned a spurious "Validate: Review opg PR
|
|
6810
|
-
// #792 ..." follow-up. See the ELIGIBLE_TYPES check below.
|
|
6811
|
-
//
|
|
6812
|
-
// Files exactly ONE validation WI per coding-WI completion (W-mrhybval0001).
|
|
6813
|
-
// `liveValidation.type` still governs CHECKOUT ROUTING — the set of work-item
|
|
6814
|
-
// types that run in-place on the live checkout (see resolveCheckoutMode's
|
|
6815
|
-
// membership check) — but auto-validation no longer fans that array out into
|
|
6816
|
-
// one validation WI per type. That earlier behavior (W-mr2m1ute000a9c01)
|
|
6817
|
-
// conflated "which types run live" with "how many validation tasks to file";
|
|
6818
|
-
// a multi-select routing config would silently spawn N validation WIs after
|
|
6819
|
-
// every coding completion. Validation is a single downstream step, so we
|
|
6820
|
-
// collapse to one WI.
|
|
6821
|
-
//
|
|
6822
|
-
// liveValidation.type may be a single string (legacy / back-compat) or an
|
|
6823
|
-
// array of strings (multi-select hybrid types). We choose ONE validation type
|
|
6824
|
-
// for the single WI. It MUST be a member of lvTypes, otherwise resolveCheckoutMode
|
|
6825
|
-
// would route the validation WI to a worktree instead of the live checkout
|
|
6826
|
-
// (defeating hybrid validation). Preference order: the canonical `test` type
|
|
6827
|
-
// when configured, else the first configured live type. The completing item's
|
|
6828
|
-
// own type is excluded so a validation-type WI can't recursively validate
|
|
6829
|
-
// itself (anti-loop).
|
|
6830
|
-
//
|
|
6831
|
-
// Only these item.type values ever trigger this hook (see ELIGIBLE_TYPES
|
|
6832
|
-
// below) — real code-authoring completions, not review/test/verify/setup/
|
|
6833
|
-
// explore/ask/docs/decompose completions.
|
|
6805
|
+
// Only real code-authoring completions trigger the hook. The follow-up is
|
|
6806
|
+
// always a canonical `test` WI with the PR-focused `build-and-test` playbook;
|
|
6807
|
+
// liveValidation.type controls routing, not the semantic type of the task.
|
|
6834
6808
|
//
|
|
6835
6809
|
// Deduplicates per codingWiId: any non-terminal WI with
|
|
6836
6810
|
// meta.liveValidationFor === codingWiId blocks a second dispatch, so only one
|
|
@@ -6852,22 +6826,30 @@ function collapseAllDuplicatePrRecords(config) {
|
|
|
6852
6826
|
// verify, setup, explore, ask, docs, decompose, ...) is excluded even when it
|
|
6853
6827
|
// resolves a PR ref, because those completions don't author a code diff
|
|
6854
6828
|
// worth re-validating.
|
|
6855
|
-
const ELIGIBLE_LIVE_VALIDATION_TYPES = new Set([
|
|
6829
|
+
const ELIGIBLE_LIVE_VALIDATION_TYPES = new Set([
|
|
6830
|
+
WORK_TYPE.IMPLEMENT,
|
|
6831
|
+
WORK_TYPE.IMPLEMENT_LARGE,
|
|
6832
|
+
WORK_TYPE.FIX,
|
|
6833
|
+
WORK_TYPE.BUILD_FIX_COMPLEX,
|
|
6834
|
+
]);
|
|
6856
6835
|
|
|
6857
|
-
function autoDispatchLiveValidationWi(meta, config) {
|
|
6858
|
-
const item = meta?.item;
|
|
6859
|
-
|
|
6836
|
+
function autoDispatchLiveValidationWi(meta, config, dispatchItem = null) {
|
|
6837
|
+
const item = meta?.item || null;
|
|
6838
|
+
const sourceType = item?.type || dispatchItem?.type || null;
|
|
6839
|
+
const sourceKey = item?.id || dispatchItem?.id || null;
|
|
6840
|
+
if (!sourceKey || !sourceType) return;
|
|
6841
|
+
if (!item && !meta?.pr) return;
|
|
6860
6842
|
|
|
6861
6843
|
// Never re-trigger off the completion of a WI that was itself auto-created
|
|
6862
6844
|
// as a live-validation follow-up — that's what causes the infinite
|
|
6863
6845
|
// "Validate: Validate: ..." cascade (W-mr9u39az000db5c2 / issue #708).
|
|
6864
|
-
if (item
|
|
6846
|
+
if (item?.meta && item.meta.liveValidationFor) return;
|
|
6865
6847
|
|
|
6866
|
-
//
|
|
6848
|
+
// Only implement/implement:large/fix/build-fix-complex completions ever
|
|
6867
6849
|
// author a real code diff worth validating. Review, test, verify, setup,
|
|
6868
6850
|
// explore, ask, docs, decompose, etc. completions must never spawn a
|
|
6869
6851
|
// "Validate: ..." follow-up, even when they resolve a PR reference.
|
|
6870
|
-
if (!ELIGIBLE_LIVE_VALIDATION_TYPES.has(
|
|
6852
|
+
if (!ELIGIBLE_LIVE_VALIDATION_TYPES.has(sourceType)) return;
|
|
6871
6853
|
|
|
6872
6854
|
// Issue #716 — QA Session infrastructure WIs (SETUP/DRAFT/EXECUTE, tagged
|
|
6873
6855
|
// via meta.sessionId and always dispatched with skipPr:true) never own a
|
|
@@ -6877,7 +6859,7 @@ function autoDispatchLiveValidationWi(meta, config) {
|
|
|
6877
6859
|
// for all three phases) spawns spurious "Validate: QA Session ..." WIs the
|
|
6878
6860
|
// moment any PR reference resolves against it (see the skipPr guard below
|
|
6879
6861
|
// resolveWorkItemPrRecord in engine.js for the matching pre-dispatch fix).
|
|
6880
|
-
if (item
|
|
6862
|
+
if (item?.skipPr || (item?.meta && item.meta.sessionId)) return;
|
|
6881
6863
|
|
|
6882
6864
|
const projectName = meta?.project?.name;
|
|
6883
6865
|
if (!projectName) return;
|
|
@@ -6887,23 +6869,8 @@ function autoDispatchLiveValidationWi(meta, config) {
|
|
|
6887
6869
|
const project = projects.find(p => p && p.name === projectName);
|
|
6888
6870
|
if (!project) return;
|
|
6889
6871
|
|
|
6890
|
-
const
|
|
6891
|
-
if (!
|
|
6892
|
-
|
|
6893
|
-
const lvTypes = Array.isArray(lv.type) ? lv.type : [lv.type];
|
|
6894
|
-
|
|
6895
|
-
// Candidate validation types = configured live types minus the completing
|
|
6896
|
-
// item's own type (avoid an infinite validate-the-validation loop).
|
|
6897
|
-
const candidates = lvTypes.filter(t => t && t !== item.type);
|
|
6898
|
-
if (candidates.length === 0) return;
|
|
6899
|
-
|
|
6900
|
-
// Collapse to a SINGLE validation type: prefer the canonical `test` type when
|
|
6901
|
-
// it is a configured live type, else the first configured live type. Either
|
|
6902
|
-
// way the chosen type is a member of lvTypes, so the validation WI routes to
|
|
6903
|
-
// the live checkout (not a worktree).
|
|
6904
|
-
const validationType = candidates.includes(WORK_TYPE.TEST)
|
|
6905
|
-
? WORK_TYPE.TEST
|
|
6906
|
-
: candidates[0];
|
|
6872
|
+
const validationType = shared.resolveLiveValidationAutoDispatchType(project);
|
|
6873
|
+
if (!validationType) return;
|
|
6907
6874
|
|
|
6908
6875
|
// Resolve PR reference: prefer the canonical stamped _pr field (set by
|
|
6909
6876
|
// stampWiPrRef which runs earlier in runPostCompletionHooks), then fall
|
|
@@ -6915,11 +6882,22 @@ function autoDispatchLiveValidationWi(meta, config) {
|
|
|
6915
6882
|
// (W-mq18ec6h000p7b87) and would otherwise let a non-fix WI (e.g.
|
|
6916
6883
|
// 'implement'/'docs') that merely *mentions* an unrelated PR number in
|
|
6917
6884
|
// prose trigger a live-validation dispatch against the wrong PR.
|
|
6918
|
-
const prRef = item
|
|
6885
|
+
const prRef = item
|
|
6886
|
+
? (item._pr || item._prUrl || shared.extractStructuredWorkItemPrRef(item) || null)
|
|
6887
|
+
: (meta?.pr || null);
|
|
6919
6888
|
if (!prRef) return;
|
|
6920
6889
|
|
|
6921
|
-
const
|
|
6922
|
-
|
|
6890
|
+
const prUrl = item?._prUrl || (typeof prRef === 'object'
|
|
6891
|
+
? (prRef.url || prRef.webUrl || prRef.remoteUrl || null)
|
|
6892
|
+
: (/^https?:\/\//i.test(String(prRef)) ? String(prRef) : null));
|
|
6893
|
+
const prId = typeof prRef === 'object'
|
|
6894
|
+
? (prRef.id || prRef.pr_id || prRef.prId || prRef.pullRequestId || shared.getPrNumber(prRef))
|
|
6895
|
+
: String(prRef);
|
|
6896
|
+
const prTarget = prUrl || (prId != null ? String(prId) : null);
|
|
6897
|
+
if (!prTarget) return;
|
|
6898
|
+
|
|
6899
|
+
const codingWiId = sourceKey;
|
|
6900
|
+
const wiScope = item ? resolveWorkItemScope(meta) : projectName;
|
|
6923
6901
|
if (!wiScope) return;
|
|
6924
6902
|
|
|
6925
6903
|
try {
|
|
@@ -6942,7 +6920,7 @@ function autoDispatchLiveValidationWi(meta, config) {
|
|
|
6942
6920
|
return items;
|
|
6943
6921
|
}
|
|
6944
6922
|
|
|
6945
|
-
const proposedTitle = 'Validate: ' + (item
|
|
6923
|
+
const proposedTitle = 'Validate: ' + (item?.title || meta?.pr?.title || codingWiId);
|
|
6946
6924
|
// Defense-in-depth (belt-and-suspenders alongside the meta.liveValidationFor
|
|
6947
6925
|
// guard above): never create a doubly-nested "Validate: Validate: ..."
|
|
6948
6926
|
// title even if some future caller manages to invoke this function
|
|
@@ -6957,11 +6935,20 @@ function autoDispatchLiveValidationWi(meta, config) {
|
|
|
6957
6935
|
title: proposedTitle,
|
|
6958
6936
|
type: validationType,
|
|
6959
6937
|
status: WI_STATUS.PENDING,
|
|
6960
|
-
depends_on: [codingWiId],
|
|
6961
|
-
|
|
6962
|
-
|
|
6938
|
+
depends_on: item?.id ? [codingWiId] : [],
|
|
6939
|
+
targetPr: prTarget,
|
|
6940
|
+
pr_id: prId != null ? String(prId) : prTarget,
|
|
6941
|
+
references: prUrl
|
|
6942
|
+
? [{ url: prUrl, label: 'Source PR' }]
|
|
6943
|
+
: [prTarget],
|
|
6944
|
+
meta: {
|
|
6945
|
+
liveValidationFor: codingWiId,
|
|
6946
|
+
liveValidationType: validationType,
|
|
6947
|
+
playbook: 'build-and-test',
|
|
6948
|
+
...(dispatchItem?.id ? { sourceDispatchId: dispatchItem.id } : {}),
|
|
6949
|
+
},
|
|
6963
6950
|
project: projectName,
|
|
6964
|
-
priority: item
|
|
6951
|
+
priority: item?.priority || 'medium',
|
|
6965
6952
|
created: ts(),
|
|
6966
6953
|
createdBy: 'lifecycle:live-validation-auto-dispatch',
|
|
6967
6954
|
};
|