@yemi33/minions 0.1.2423 → 0.1.2425
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/install-internal-minions.js +1209 -0
- package/bin/minions.js +75 -9
- package/dashboard/docs/typography.md +27 -12
- package/dashboard/js/command-center.js +13 -11
- package/dashboard/js/live-stream.js +1 -1
- package/dashboard/js/memory-search.js +388 -43
- package/dashboard/js/modal-qa.js +5 -5
- package/dashboard/js/qa.js +39 -26
- package/dashboard/js/refresh.js +37 -18
- package/dashboard/js/render-agents.js +14 -18
- package/dashboard/js/render-dispatch.js +14 -74
- package/dashboard/js/render-inbox.js +3 -3
- package/dashboard/js/render-meetings.js +8 -8
- package/dashboard/js/render-other.js +45 -20
- package/dashboard/js/render-pipelines.js +6 -6
- package/dashboard/js/render-plans.js +14 -14
- package/dashboard/js/render-prd.js +46 -46
- package/dashboard/js/render-prs.js +9 -65
- package/dashboard/js/render-schedules.js +5 -5
- package/dashboard/js/render-skills.js +6 -6
- package/dashboard/js/render-utils.js +4 -4
- package/dashboard/js/render-watches.js +4 -16
- package/dashboard/js/render-work-items.js +95 -107
- package/dashboard/js/settings.js +175 -74
- package/dashboard/layout.html +3 -2
- package/dashboard/pages/inbox.html +1 -1
- package/dashboard/pages/work.html +1 -1
- package/dashboard/shared/model-display.js +9 -0
- package/dashboard/shared/watches-source.js +43 -0
- package/dashboard/shared/welcome-popup.js +259 -0
- package/dashboard/slim/body.html +6 -6
- package/dashboard/slim/js/chat.js +4 -2
- package/dashboard/slim/js/history.js +10 -3
- package/dashboard/slim/js/modals-tiles.js +85 -16
- package/dashboard/slim/js/status.js +12 -16
- package/dashboard/slim/styles.css +24 -4
- package/dashboard/styles.css +62 -27
- package/dashboard-build.js +8 -2
- package/dashboard.js +3854 -2514
- package/docs/README.md +6 -4
- package/docs/api-errors.md +144 -0
- package/docs/auto-discovery.md +83 -61
- package/docs/capture-demos.js +173 -30
- package/docs/command-center.md +5 -1
- package/docs/completion-reports.md +127 -11
- package/docs/constants.md +15 -3
- package/docs/constellation-style-telemetry.md +2 -2
- package/docs/copilot-cli-schema.md +32 -22
- package/docs/cross-repo-plans.md +30 -30
- package/docs/demo/memory-system.html +1 -1
- package/docs/deprecated.json +132 -18
- package/docs/diagnostics-memory.md +8 -4
- package/docs/human-vs-automated.md +2 -2
- package/docs/internal-install.md +190 -0
- package/docs/kb-pr3223-cascade-archiving.md +16 -0
- package/docs/kb-pr696-merge-conflict-docs.md +23 -0
- package/docs/kb-sweep.md +2 -2
- package/docs/keep-processes.md +8 -1
- package/docs/live-checkout-mode.md +12 -7
- package/docs/managed-spawn.md +21 -3
- package/docs/named-agents.md +3 -2
- package/docs/plan-lifecycle.md +32 -32
- package/docs/pr-auto-fix-dispatch.md +3 -3
- package/docs/pr-review-fix-loop.md +6 -6
- package/docs/pr-screenshots/pr-899/worker-pool-worktrees-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-899/worker-pool-worktrees-BEFORE.png +0 -0
- package/docs/pr-screenshots/pr-979/auto-fix-pane-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-979/auto-fix-pane-BEFORE.png +0 -0
- package/docs/pr-screenshots/pr-985/pr-column-em-dash-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-985/pr-column-em-dash-BEFORE.png +0 -0
- package/docs/qa-runbook-lifecycle.md +67 -51
- package/docs/qa-runbooks.md +2 -2
- package/docs/runtime-adapters.md +101 -50
- package/docs/security.md +28 -0
- package/docs/self-improvement.md +47 -13
- package/docs/skills.md +29 -0
- package/docs/slim-ux/concepts.md +2 -2
- package/docs/specs/agent-configurability.md +43 -41
- package/docs/specs/agent-rename.md +13 -13
- package/docs/team-memory.md +283 -14
- package/docs/tutorials/03-plan-driven-feature.md +3 -3
- package/docs/tutorials/04-runtimes-and-harness.md +18 -21
- package/docs/tutorials/08-operations-and-recovery.md +3 -2
- package/docs/tutorials/README.md +4 -3
- package/docs/visual-evidence-ci.md +103 -0
- package/docs/watches.md +35 -30
- package/docs/worktree-lifecycle.md +151 -13
- package/engine/acp-transport.js +1 -0
- package/engine/ado.js +80 -33
- package/engine/agent-api-validation.js +571 -0
- package/engine/agent-worker-pool.js +7 -6
- package/engine/api-contracts/agent-content.js +761 -0
- package/engine/api-contracts/cc-ops.js +219 -0
- package/engine/api-contracts/config-runtime.js +419 -0
- package/engine/api-contracts/core.js +138 -0
- package/engine/api-contracts/index.js +506 -0
- package/engine/api-contracts/orchestration.js +602 -0
- package/engine/api-contracts/pull-requests.js +323 -0
- package/engine/api-contracts/qa-process.js +764 -0
- package/engine/api-contracts/work-plan-prd.js +494 -0
- package/engine/api-validation.js +543 -0
- package/engine/cc-api-validation.js +586 -0
- package/engine/cc-worker-pool.js +9 -1
- package/engine/cleanup.js +393 -271
- package/engine/cli.js +65 -7
- package/engine/comment-classifier.js +70 -5
- package/engine/consolidation.js +123 -5
- package/engine/content-api-validation.js +606 -0
- package/engine/create-pr-worktree.js +22 -1
- package/engine/db/migrations/027-review-learning-lifecycle.js +162 -0
- package/engine/dispatch.js +33 -0
- package/engine/distribution.js +189 -0
- package/engine/execution-model.js +59 -0
- package/engine/features.js +12 -0
- package/engine/github.js +67 -25
- package/engine/harness-context.js +313 -0
- package/engine/inbox-store.js +1 -1
- package/engine/kb-sweep.js +161 -38
- package/engine/keep-process-sweep.js +43 -3
- package/engine/lifecycle.js +1066 -165
- package/engine/llm.js +4 -0
- package/engine/managed-spawn.js +463 -48
- package/engine/meeting.js +197 -34
- package/engine/memory-retrieval.js +280 -0
- package/engine/memory-store.js +821 -38
- package/engine/model-discovery.js +17 -3
- package/engine/pipeline.js +802 -14
- package/engine/plan-prd-validation.js +745 -0
- package/engine/playbook.js +206 -17
- package/engine/pooled-agent-process.js +6 -1
- package/engine/pr-action.js +1 -1
- package/engine/pr-issue-validation.js +653 -0
- package/engine/pr-resolve.js +75 -13
- package/engine/prd-store.js +39 -6
- package/engine/preflight.js +7 -2
- package/engine/process-utils.js +79 -31
- package/engine/projects.js +198 -12
- package/engine/promotion.js +371 -0
- package/engine/qa-from-prd.js +3 -0
- package/engine/qa-process-validation.js +572 -0
- package/engine/qa-runbooks.js +41 -37
- package/engine/qa-runners/maestro.js +11 -7
- package/engine/qa-runners/playwright.js +37 -14
- package/engine/qa-runners.js +9 -10
- package/engine/qa-runs.js +124 -11
- package/engine/qa-sessions.js +139 -40
- package/engine/quarantine-refs.js +81 -11
- package/engine/queries.js +163 -9
- package/engine/review-learning-backfill.js +482 -0
- package/engine/review-learning.js +1236 -0
- package/engine/runtimes/claude.js +60 -2
- package/engine/runtimes/codex.js +57 -16
- package/engine/runtimes/contract.js +21 -0
- package/engine/runtimes/copilot.js +51 -3
- package/engine/runtimes/index.js +1 -0
- package/engine/scheduler.js +201 -32
- package/engine/settings-validation.js +909 -0
- package/engine/shared.js +705 -208
- package/engine/spawn-agent.js +48 -21
- package/engine/steering-constraints.js +31 -0
- package/engine/steering-store.js +10 -2
- package/engine/steering.js +14 -2
- package/engine/timeout.js +404 -111
- package/engine/untrusted-fence.js +24 -10
- package/engine/watch-actions.js +7 -2
- package/engine/watches.js +353 -51
- package/engine/work-item-validation.js +561 -0
- package/engine/work-items-store.js +61 -1
- package/engine/worktree-gc.js +656 -37
- package/engine/worktree-preflight.js +154 -0
- package/engine.js +2068 -342
- package/package.json +1 -1
- package/playbooks/_pr-description-audit.md +88 -41
- package/playbooks/build-fix-complex.md +3 -3
- package/playbooks/docs.md +2 -2
- package/playbooks/fix.md +18 -18
- package/playbooks/implement.md +18 -18
- package/playbooks/qa-session-draft.md +10 -12
- package/playbooks/qa-session-execute.md +16 -12
- package/playbooks/qa-validate.md +8 -7
- package/playbooks/shared-rules.md +45 -0
- package/playbooks/test.md +4 -0
- package/prompts/cc-system.md +1 -1
- package/skills/check-self-authored-review-comment/SKILL.md +2 -2
package/docs/watches.md
CHANGED
|
@@ -31,7 +31,7 @@ Defined in `engine/shared.js` (`WATCH_STATUS`):
|
|
|
31
31
|
| `active` | Eligible for evaluation each tick |
|
|
32
32
|
| `paused` | Skipped by `checkWatches`; persists indefinitely until resumed/deleted |
|
|
33
33
|
| `triggered` | Reserved status (set on demand by callers; not auto-applied) |
|
|
34
|
-
| `expired` | Auto-set when `stopAfter` is reached, or on first trigger for absolute conditions when `stopAfter === 0`. The watch is left on disk for audit but no longer evaluated *(source: `engine/watches.js:
|
|
34
|
+
| `expired` | Auto-set when `stopAfter` is reached, or on first trigger for absolute conditions when `stopAfter === 0`. The watch is left on disk for audit but no longer evaluated *(source: `engine/watches.js:499-513`)* |
|
|
35
35
|
|
|
36
36
|
Pause/resume flips the `status` field via `POST /api/watches/update` *(source: `engine/watches.js` `updateWatch`, `dashboard.js` `handleWatchesUpdate`)*.
|
|
37
37
|
|
|
@@ -44,14 +44,14 @@ Defined in `engine/shared.js` (`WATCH_CONDITION`). Conditions split into two fam
|
|
|
44
44
|
|
|
45
45
|
`merged`, `build-fail`, `build-pass`, `completed`, `failed`, `concluded`, `approved`, `rejected`, `ready-for-merge`, `retry-limit-reached`, `all-items-done`, `item-failed-n-times`.
|
|
46
46
|
|
|
47
|
-
When `stopAfter === 0`, these are **fire-once** — the engine flips the watch to `expired` after the first trigger so a permanently-merged PR (or a permanently-true compound state assertion like `ready-for-merge`) doesn't keep notifying *(source: `engine/watches.js:
|
|
47
|
+
When `stopAfter === 0`, these are **fire-once** — the engine flips the watch to `expired` after the first trigger so a permanently-merged PR (or a permanently-true compound state assertion like `ready-for-merge`) doesn't keep notifying *(source: `engine/watches.js:499-513`)*.
|
|
48
48
|
|
|
49
49
|
> **Per-target override (W-mp7hg58e000b5212):** the global `WATCH_ABSOLUTE_CONDITIONS` set is the legacy fallback. Each target type now declares its own `absoluteConditions: [...]` array in its spec; `registerTargetType` normalizes that into a `Set` that takes precedence at evaluation time. The plugin contract (see below) uses this to keep absolute-vs-change semantics local to each target type. Plugins that omit `absoluteConditions` get an empty set (all change-based).
|
|
50
50
|
|
|
51
51
|
### Change-based conditions
|
|
52
52
|
`status-change`, `any`, `new-comments`, `vote-change`, `stage-complete`, `ran`, `enabled`, `disabled`, `activity-change`, plus the predicate conditions added under P-w4e2f6a1 / P-w5b8d2c9 for the `pr`, `work-item`, `plan`, and `pipeline` target types (`head-commit-change`, `mergeable-flipped`, `behind-master`, `draft-flipped`, `stalled`, `dependency-met`, `stage-advanced`, `stuck-in-stage`). See `engine/shared.js` `WATCH_CONDITION` for the canonical enum.
|
|
53
53
|
|
|
54
|
-
These compare the live entity against the watch's `_lastState` snapshot and run forever when `stopAfter === 0`. Baseline `_lastState` is captured on the first check so the very next change triggers the watch *(source: `engine/watches.js:
|
|
54
|
+
These compare the live entity against the watch's `_lastState` snapshot and run forever when `stopAfter === 0`. Baseline `_lastState` is captured on the first check so the very next change triggers the watch *(source: `engine/watches.js:436-440, 558-559`)*.
|
|
55
55
|
|
|
56
56
|
### Tick-counted conditions
|
|
57
57
|
`stalled`, `stuck-in-stage` — require N consecutive unchanged captures (default `WATCH_STALLED_DEFAULT_TICKS = 12`, `WATCH_STUCK_STAGE_DEFAULT_TICKS = 12`, both in `engine/shared.js`). Counters (`_unchangedTicks`, `_stuckStageTicks`) are recomputed inside `_captureState` by comparing the fresh snapshot against `prevState`.
|
|
@@ -69,24 +69,27 @@ Compound state-assertion predicates (`ready-for-merge`, `retry-limit-reached`, `
|
|
|
69
69
|
|
|
70
70
|
## Target Types — `TARGET_TYPES` Registry
|
|
71
71
|
|
|
72
|
-
Target-type behavior in `engine/watches.js` is **data-driven via a registry** *(source: `engine/watches.js:
|
|
72
|
+
Target-type behavior in `engine/watches.js` is **data-driven via a registry** *(source: `engine/watches.js:122-174`)*. Each spec uses the following fields; entries are required unless marked optional:
|
|
73
73
|
|
|
74
74
|
- `label` — human name shown in dashboard pickers
|
|
75
|
-
- `description` — short help text
|
|
75
|
+
- `description` — **(optional)** short help text; defaults to `''` in `listTargetTypes()`
|
|
76
76
|
- `conditions` — non-empty array of accepted condition keys (also acts as the per-type allowlist)
|
|
77
|
-
- `absoluteConditions` — *(optional)*
|
|
78
|
-
- `fetchEntity(target, state)` — entity-or-null lookup
|
|
79
|
-
- `captureState(entity)` — snapshot used for change-detection diffs
|
|
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
|
|
77
|
+
- `absoluteConditions` — *(optional)* `Set` of condition keys that fire-once when `stopAfter === 0`; defaults to empty set (all conditions treated as change-based)
|
|
78
|
+
- `fetchEntity(target, state)` — entity-or-null lookup; must return the fetched resource or null
|
|
79
|
+
- `captureState(entity, prevState)` — snapshot object used for change-detection diffs; `prevState` parameter lets counters and custom fields carry across checks
|
|
80
|
+
- `evaluate(condition, entity, prevState, target)` — returns `{ triggered: boolean, message: string }`; core condition-check logic
|
|
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`, or `draft-flipped`), return `true` and the engine auto-expires the watch. Default returns `false` so existing target types keep polling forever. The main check loop invokes this only when `triggerCount > 0` **or** the prev-state status already matched current entity — so a watch armed on an already-terminal PR with `condition: merged` still fires once via the absolute-condition fire-once path *(source: `engine/watches.js:150-158, 537-550`)*.
|
|
82
|
+
- `contextVars(entity, prevState, newState)` — *(optional)* returns object with extra action-template variables contributed by the target type (e.g., `{ httpStatus: 200, prevExtracted: 'foo' }`)
|
|
82
83
|
|
|
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:
|
|
84
|
+
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:122-174`)*.
|
|
84
85
|
|
|
85
86
|
### User-extensible via `watches.d/` (W-mp7hg58e000b5212)
|
|
86
87
|
|
|
87
|
-
At engine boot, every `*.js` file in `<MINIONS_DIR>/watches.d/` is auto-loaded **after** the built-in registrations *(source: `engine/watches.js:
|
|
88
|
+
At engine boot, every `*.js` file in `<MINIONS_DIR>/watches.d/` is auto-loaded **after** the built-in registrations *(source: `engine/watches.js:1401-1436`)*, so plugins can both add new target types and override built-ins. A plugin file exports either `{ name, spec }` or an array of such objects. Failures are logged-and-skipped — one bad plugin must not break boot or block other plugins. Reloads require an engine restart.
|
|
88
89
|
|
|
89
|
-
Canonical example: `watches.d/http.js` (W-mp7i22mu00191b07) — a generic HTTP poller covering the full plugin contract including `
|
|
90
|
+
Canonical example: `watches.d/http.js` (W-mp7i22mu00191b07) — a generic HTTP poller covering the full plugin contract including `captureState` (custom snapshot fields) and `contextVars` (custom action-template vars like `{{httpStatus}}`, `{{prevExtracted}}`).
|
|
91
|
+
|
|
92
|
+
This source checkout also includes `ado-author-prs`, `gh-author-prs`, and `teams-channel` plugins under `watches.d/`. They are dynamically loaded plugins, not members of the eight built-in target types; `GET /api/watches/target-types` is the authoritative list for the running installation.
|
|
90
93
|
|
|
91
94
|
### Built-in target types
|
|
92
95
|
|
|
@@ -103,11 +106,11 @@ The eight built-ins are registered at module load *(source: `engine/watches.js`
|
|
|
103
106
|
| `dispatch` | Dispatch entry id | `completed`, `failed`, `status-change`, `any` | Looks across `pending` / `active` / `completed` lists |
|
|
104
107
|
| `agent` | Agent id | `activity-change`, `status-change`, `any` | `activity-change` fires only on transitions in/out of `'working'` |
|
|
105
108
|
|
|
106
|
-
`evaluateWatch` dispatches to `tt.evaluate(...)`; unknown target types return `"Unknown target type: ..."` and unknown conditions return `"Unknown condition: ..."` — both are non-triggering *(source: `engine/watches.js:
|
|
109
|
+
`evaluateWatch` dispatches to `tt.evaluate(...)`; unknown target types return `"Unknown target type: ..."` and unknown conditions return `"Unknown condition: ..."` — both are non-triggering *(source: `engine/watches.js:320-365`)*.
|
|
107
110
|
|
|
108
111
|
### Plugin folder (`watches.d/`) — user-extensible target types
|
|
109
112
|
|
|
110
|
-
W-mp7hg58e000b5212 added a **plugin folder** so operators can register new target types without editing engine source. At engine boot, `engine/watches.js` scans `<MINIONS_DIR>/watches.d/*.js` *after* the eight built-ins are registered (so plugins can override a built-in by re-using its key — last-write-wins) and calls `registerTargetType()` for each export *(source: `engine/watches.js:
|
|
113
|
+
W-mp7hg58e000b5212 added a **plugin folder** so operators can register new target types without editing engine source. At engine boot, `engine/watches.js` scans `<MINIONS_DIR>/watches.d/*.js` *after* the eight built-ins are registered (so plugins can override a built-in by re-using its key — last-write-wins) and calls `registerTargetType()` for each export *(source: `engine/watches.js:1401-1436`)*.
|
|
111
114
|
|
|
112
115
|
Each `watches.d/<name>.js` file must export `{ name, spec }` (or an array of those):
|
|
113
116
|
|
|
@@ -120,10 +123,11 @@ module.exports = {
|
|
|
120
123
|
conditions: ['status-change', 'value-equals'],
|
|
121
124
|
absoluteConditions: ['value-equals'], // optional, defaults to []
|
|
122
125
|
fetchEntity(target, state) { /* sync entity lookup */ },
|
|
123
|
-
captureState(entity) { /* snapshot for diffs */ },
|
|
126
|
+
captureState(entity, prevState) { /* snapshot for diffs */ },
|
|
124
127
|
evaluate(condition, entity, prevState, target) {
|
|
125
128
|
return { triggered: <bool>, message: '...' };
|
|
126
129
|
},
|
|
130
|
+
contextVars(entity, prevState, newState) { return {}; }, // optional
|
|
127
131
|
},
|
|
128
132
|
};
|
|
129
133
|
```
|
|
@@ -134,7 +138,7 @@ Resolution is `path.join(shared.MINIONS_DIR, 'watches.d')` so it works in both d
|
|
|
134
138
|
|
|
135
139
|
## Tick Integration
|
|
136
140
|
|
|
137
|
-
`engine.js` calls `checkWatches(config, state)` every `ENGINE_DEFAULTS.watchPollEvery` ticks (default 18 ⇒ ~3 min at the default 10s tick) inside its own `safe('checkWatches', ...)` block *(source: `engine.js:
|
|
141
|
+
`engine.js` calls `checkWatches(config, state)` every `ENGINE_DEFAULTS.watchPollEvery` ticks (default 18 ⇒ ~3 min at the default 10s tick) inside its own `safe('checkWatches', ...)` block *(source: `engine.js:11286-11328`)*. The engine builds the state object from cached project files + module reads:
|
|
138
142
|
|
|
139
143
|
```
|
|
140
144
|
{
|
|
@@ -150,16 +154,16 @@ Resolution is `path.join(shared.MINIONS_DIR, 'watches.d')` so it works in both d
|
|
|
150
154
|
1. Skips paused/expired watches and any watch checked within its `interval`.
|
|
151
155
|
2. Captures a baseline `_lastState` on first check (so change conditions have something to diff).
|
|
152
156
|
3. Calls `evaluateWatch(watch, state)`.
|
|
153
|
-
4. On trigger: increments `triggerCount`, sets `last_triggered`,
|
|
157
|
+
4. On trigger: increments `triggerCount`, sets `last_triggered`, and snapshots an effective action when notification or follow-up work is configured. When `notify === 'inbox'` and the watch has an owner, an implicit `{ type: 'notify' }` action step is prepended before any configured `watch.action`; without that implicit step, a single explicit action keeps its object shape.
|
|
154
158
|
5. Applies fire-once / `stopAfter` expiration.
|
|
155
159
|
6. On non-trigger: writes a per-poll inbox note when `onNotMet === 'notify'`.
|
|
156
160
|
7. Refreshes `_lastState` for the next check.
|
|
157
161
|
|
|
158
|
-
I/O happens **outside the lock
|
|
162
|
+
I/O happens **outside the lock**. Trigger actions, including the implicit inbox notification, run through `_runActionTask`. Only `onNotMet === 'notify'` polling notes use the direct notification queue and `writeToInbox`. Each action result is persisted back onto the watch as `_lastActionResult` in a follow-up locked write. An implicit notify makes the effective action array-shaped, so its result uses the `type: 'chain'` / `steps[]` shape; an explicit action without an implicit notify preserves the legacy single-action result shape *(source: `engine/watches.js:459-495,515-521,601-684`)*.
|
|
159
163
|
|
|
160
164
|
## Follow-Up Actions on Trigger
|
|
161
165
|
|
|
162
|
-
`watch.action` is an optional structured action
|
|
166
|
+
`watch.action` is an optional structured action (or action chain). When the implicit inbox notification is enabled, configured actions follow it in the same effective chain. Action types live in a sibling registry in `engine/watch-actions.js` and are validated at create/update time *(source: `engine/watches.js:189-245` `createWatch`, `engine/watch-actions.js:95-105` `registerActionType`)*. `GET /api/watches/action-types` returns the live list for dashboard pickers.
|
|
163
167
|
|
|
164
168
|
### Built-in actions
|
|
165
169
|
|
|
@@ -167,10 +171,10 @@ I/O happens **outside the lock**: notifications via `writeToInbox`, follow-up ac
|
|
|
167
171
|
|------------------------|-----------------------------------------------------------------------------------------------|
|
|
168
172
|
| `notify` | Explicit inbox write; lets you customize `owner`/`body` instead of the default trigger string |
|
|
169
173
|
| `dispatch-work-item` | Add a new project or central work item with `createdBy: "watch:<id>"` |
|
|
170
|
-
| `run-skill` | Wrapper around `dispatch-work-item` that asks the agent to run a
|
|
174
|
+
| `run-skill` | Wrapper around `dispatch-work-item` that asks the agent to run a named runtime-native skill |
|
|
171
175
|
| `webhook` | `http`/`https` request to an arbitrary URL (10s safety timeout, JSON or string body) |
|
|
172
176
|
| `minions-api` | Loopback call to the in-process dashboard at `http://127.0.0.1:${shared.readDashboardPortFile(MINIONS_DIR)?.port \|\| 7331}` (reads the bound-port beacon file, not the `MINIONS_PORT` env, so it still resolves after an `EADDRINUSE` port scan) — `endpoint` must start with `/api/`; sets `X-Minions-Internal: 1`; returns `{ok, status, summary, response}` for chain-step templating |
|
|
173
|
-
| `cancel-work-item` | Flip a WI to `WI_STATUS.CANCELLED` across
|
|
177
|
+
| `cancel-work-item` | Flip a WI to `WI_STATUS.CANCELLED` across central and configured-project SQL scopes |
|
|
174
178
|
| `trigger-pipeline` | Start a new pipeline run (skipped if the pipeline already has an active run) |
|
|
175
179
|
| `archive-plan` | Set PRD `status="archived"` + `archivedAt` |
|
|
176
180
|
| `resume-plan` | Set PRD `status=PLAN_STATUS.ACTIVE` and clear `planStale` |
|
|
@@ -180,7 +184,7 @@ Constants live in `WATCH_ACTION_TYPE` (`engine/shared.js`); handlers in `engine/
|
|
|
180
184
|
|
|
181
185
|
### Templating
|
|
182
186
|
|
|
183
|
-
Action params support `{{var}}` substitution from the trigger context *(source: `engine/watch-actions.js:
|
|
187
|
+
Action params support `{{var}}` substitution from the trigger context *(source: `engine/watch-actions.js:133-230`)*. Built-in vars from `buildTriggerContext`:
|
|
184
188
|
|
|
185
189
|
- Always: `target`, `targetType`, `condition`, `watchId`, `triggerCount`, `message`
|
|
186
190
|
- All scalar fields from `newState` (e.g. `status`, `buildStatus`, `reviewStatus`, `lastRun`)
|
|
@@ -190,13 +194,13 @@ Unknown vars are left as `{{var}}` so callers can detect them downstream.
|
|
|
190
194
|
|
|
191
195
|
### Failure isolation
|
|
192
196
|
|
|
193
|
-
Action handlers must return `{ ok: false, summary }` rather than throw — `runWatchAction` wraps them in try/catch as a backstop *(source: `engine/watch-actions.js:
|
|
197
|
+
Action handlers must return `{ ok: false, summary }` rather than throw — `runWatchAction` wraps them in try/catch as a backstop *(source: `engine/watch-actions.js:262-293`)*. A bad action never blocks other watches in the same tick, and the failed result is persisted onto the watch as `_lastActionResult` for debugging. Multi-step action chains run via `runWatchActionChain` *(source: `engine/watch-actions.js:310-355`)* — each step's `{ok, summary, result}` is templated into the next step's params as `{{steps.N.result.<field>}}`.
|
|
194
198
|
|
|
195
199
|
## Dashboard Surface
|
|
196
200
|
|
|
197
201
|
| Route | Method | Handler | Purpose |
|
|
198
202
|
|--------------------------------|--------|-------------------------------|-------------------------------------------------------------------------|
|
|
199
|
-
| `/watches` | GET | `dashboard/pages/watches.html` | Watches page (sidebar link in `dashboard/layout.html:
|
|
203
|
+
| `/watches` | GET | `dashboard/pages/watches.html` | Watches page (sidebar link in `dashboard/layout.html:111`) |
|
|
200
204
|
| `/api/watches` | GET | `handleWatchesList` | List all watches |
|
|
201
205
|
| `/api/watches/target-types` | GET | `handleWatchesTargetTypes` | Live registry from `listTargetTypes()` — used by the create-watch modal |
|
|
202
206
|
| `/api/watches/action-types` | GET | `handleWatchesActionTypes` | Live registry from `listActionTypes()` |
|
|
@@ -212,7 +216,7 @@ The CC state preamble injects a `Watches: <count>` line via `getWatches()` so th
|
|
|
212
216
|
|
|
213
217
|
## Command Center Integration
|
|
214
218
|
|
|
215
|
-
CC creates, pauses, resumes, and deletes watches by calling the REST API directly via its `Bash` tool — the older `===ACTIONS===` `create-watch`/`delete-watch`/`pause-watch`/`resume-watch` actions have been retired *(source: `test/unit.test.js:
|
|
219
|
+
CC creates, pauses, resumes, and deletes watches by calling the REST API directly via its `Bash` tool — the older `===ACTIONS===` `create-watch`/`delete-watch`/`pause-watch`/`resume-watch` actions have been retired *(source: `test/unit.test.js:21048-21052`, `test/unit/watches-module.test.js:1699`)*. To use a watch from CC chat, ask it something like:
|
|
216
220
|
|
|
217
221
|
> "Watch PR 1234 for build-pass and dispatch a verify work-item when it fires."
|
|
218
222
|
|
|
@@ -223,9 +227,10 @@ CC will discover the endpoints from `GET /api/routes` and `POST /api/watches` wi
|
|
|
223
227
|
When a watch fires:
|
|
224
228
|
|
|
225
229
|
- The SQL watch record gets `triggerCount++`, `last_triggered=<ISO>`, `_lastTriggerMessage=<message>`, and a refreshed `_lastState`.
|
|
226
|
-
-
|
|
227
|
-
-
|
|
228
|
-
-
|
|
230
|
+
- When `notify === 'inbox'`, the engine prepends an implicit `{type: 'notify'}` action to the action chain (if present) or runs notify standalone, so the inbox alert participates in action-chain execution and `_lastActionResult` *(source: `engine/watches.js:459-496`)*. This is the **trigger-time notify**, paired with an optional explicit `action`.
|
|
231
|
+
- When `onNotMet === 'notify'` (and the condition has NOT yet fired), the engine instead queues a direct poll notification outside the action chain — independent of any `action` field *(source: `engine/watches.js:515-521`)*. This is a separate **onNotMet-notify**, useful for "still watching, condition not met yet" progress updates.
|
|
232
|
+
- If an action runs, `_lastActionResult` stores either the single-action shape (`{ type, ok, summary, dispatchedItemId, at }`) or the chain shape (`{ type: "chain", ok, summary, aborted, steps, at }`).
|
|
233
|
+
- The SQL engine log gets `Watch triggered: <id> — <message>` and `Watch <id> action <type-or-chain[N]>: <summary>`.
|
|
229
234
|
|
|
230
235
|
Absolute conditions firing under `stopAfter === 0` flip `status` to `expired`; `stopAfter > 0` flips it to `expired` once `triggerCount >= stopAfter`.
|
|
231
236
|
|
|
@@ -242,7 +247,7 @@ Absolute conditions firing under `stopAfter === 0` flip `status` to `expired`; `
|
|
|
242
247
|
| `_lastActionResult.ok === false` with `"unknown action type"` | The `action.type` isn't registered. List with `listActionTypes()` / `GET /api/watches/action-types` |
|
|
243
248
|
| Webhook action returns `"only http/https allowed"` | URLs must use `http://` or `https://` schemes; other protocols are rejected by design *(source: `engine/watch-actions.js` `WEBHOOK` handler)* |
|
|
244
249
|
| Trigger fires but follow-up `dispatch-work-item` is missing | Check the engine log for `Watch <id> action <type>: <summary>`. Common reasons: missing `title`, a project-scope write failed, or the WI landed in the central scope because no project was specified |
|
|
245
|
-
| Watch `_lastActionResult` shows `"timeout"` for webhook | Webhooks have a 10s safety timeout to keep the watches tick fast *(source: `engine/watch-actions.js:
|
|
250
|
+
| Watch `_lastActionResult` shows `"timeout"` for webhook | Webhooks have a 10s safety timeout to keep the watches tick fast *(source: `engine/watch-actions.js:550-553`)* |
|
|
246
251
|
| `checkWatches` block crashes silently | Wrapped in `safe('checkWatches', ...)` so one failure doesn't abort the tick. Inspect the engine log for `Watch check error (<id>)` lines. Regression #1088: the block must use `getProjects(config)`, never the long-removed `PROJECTS` constant |
|
|
247
252
|
|
|
248
253
|
## See Also
|
|
@@ -9,7 +9,7 @@ Lifecycle keeps the cross-cutting invariants; the detail lives here.
|
|
|
9
9
|
> Source of truth: `engine/worktree-pool.js`, `engine/shared.js#removeWorktree`
|
|
10
10
|
> + `_retryFsOp`, `engine.js` (`_quarantineDirtyWorktree`,
|
|
11
11
|
> `_killGitDescendantsForWorktree`, `pruneOrphanWorktrees*`, `gcDispatchWorktreeIfOrphan`),
|
|
12
|
-
> `engine/cleanup.js`. Last verified: 2026-
|
|
12
|
+
> `engine/cleanup.js`. Last verified: 2026-07-23.
|
|
13
13
|
|
|
14
14
|
## Operator-checkout boundary
|
|
15
15
|
|
|
@@ -30,6 +30,17 @@ legacy processes are terminated and retried. Worktree setup may update shared Gi
|
|
|
30
30
|
but it never checks out, resets, cleans, writes files, or removes
|
|
31
31
|
`.git/index.lock` in the operator checkout.
|
|
32
32
|
|
|
33
|
+
Before allocating a branch-bearing worktree-mode dispatch, the engine compares
|
|
34
|
+
the target branch's `git worktree list --porcelain` holder against every
|
|
35
|
+
configured `localPath`, including overlapping nested-project layouts. A match
|
|
36
|
+
keeps the dispatch and source work item pending with
|
|
37
|
+
`_pendingReason: "worktree_branch_held"` and `_worktreeBranchHeld`
|
|
38
|
+
branch/path diagnostics; it consumes no retry and is re-checked each tick.
|
|
39
|
+
Releasing the branch clears the diagnostic and dispatches automatically.
|
|
40
|
+
`spawnAgent` repeats the probe, and the `git worktree add` error handler applies
|
|
41
|
+
the same deferral if the checkout acquires the branch in the remaining race
|
|
42
|
+
window. Neither path runs an agent in or changes the operator checkout.
|
|
43
|
+
|
|
33
44
|
Worktrees that host live `managed_spawn` or `keep_processes` cwd records are
|
|
34
45
|
anchored across dispatch-end, periodic, boot, in-root, and out-of-root GC.
|
|
35
46
|
Anchor comparisons use canonical filesystem paths so junction/case aliases do
|
|
@@ -38,17 +49,28 @@ not make a live service's worktree look orphaned.
|
|
|
38
49
|
## Live-worktree guard (W-mq5rwwss000f30a7)
|
|
39
50
|
|
|
40
51
|
**Invariant:** every code path that wipes, renames, or recycles a worktree
|
|
41
|
-
MUST call `shared.isWorktreePathLive(path, { db?, excludeDispatchId? })`
|
|
52
|
+
MUST call `shared.isWorktreePathLive(path, { db?, excludeDispatchId?, excludeSpawnLeaseId? })`
|
|
42
53
|
first and skip on `true`. Originally added after W-mq5n1zx5000hcfb5 — an
|
|
43
54
|
agent's worktree was wiped 4× consecutively by the reaper while the
|
|
44
55
|
dispatch was still active.
|
|
45
56
|
|
|
46
57
|
- **Backed by SQL** against `dispatches` (status IN ('pending','active')),
|
|
47
58
|
reading `json_extract(data, '$.worktreePath')` with a `data.meta.worktreePath`
|
|
48
|
-
fallback.
|
|
49
|
-
|
|
59
|
+
fallback. `_claimSpawnAttemptLease` first persists `_spawnLeaseId`, owner PID,
|
|
60
|
+
and claim time on the pending row; `_persistSpawnWorktreePath` then records
|
|
61
|
+
the path before runtime activation. Setup is protected while still pending,
|
|
62
|
+
and leased pending rows count against dispatch capacity.
|
|
50
63
|
- **Fails OPEN** (returns `true`) when SQLite is unreachable or the query
|
|
51
64
|
throws — better to leak a worktree than nuke an agent's unpushed work.
|
|
65
|
+
- **Scoped exclusions:** normal owner cleanup may pass `excludeDispatchId`.
|
|
66
|
+
Spawn-time quarantine also passes `excludeSpawnLeaseId`; the dispatch row is
|
|
67
|
+
excluded only when its persisted lease matches. A stale tick carrying an old
|
|
68
|
+
lease therefore sees a successor lease as live and cannot rename its
|
|
69
|
+
worktree.
|
|
70
|
+
- **Generation check:** `_isSpawnAttemptCurrent` revalidates both
|
|
71
|
+
`tickGeneration` and the persisted lease throughout worktree setup.
|
|
72
|
+
`_quarantineDirtyWorktree` repeats that check before destructive stages and
|
|
73
|
+
returns `{ skipped: true, staleLease: true }` after ownership changes.
|
|
52
74
|
- **Wired sites:** `shared.removeWorktree` (accepts `{ excludeDispatchId }`
|
|
53
75
|
forwarded to the guard); pool-return chain in `engine.js`
|
|
54
76
|
(`excludeDispatchId: id` so the dispatch can clean up its own worktree);
|
|
@@ -85,6 +107,32 @@ that intentionally preserve an existing branch continue using that branch
|
|
|
85
107
|
unchanged. Warm-pool borrowing keeps its existing fetch-and-checkout of
|
|
86
108
|
`origin/<mainBranch>`.
|
|
87
109
|
|
|
110
|
+
## Creation integrity gate (#905)
|
|
111
|
+
|
|
112
|
+
`engine.js#runWorktreeAdd` treats every nonzero `git worktree add` result as a
|
|
113
|
+
potentially corrupt partial checkout. Before retrying, it checks the live-path
|
|
114
|
+
guard, removes locked/initializing Git metadata with double force, removes any
|
|
115
|
+
remaining directory through the shared Windows file-lock retry helper, prunes,
|
|
116
|
+
and proves that neither the path nor its worktree registration remains. A retry
|
|
117
|
+
uses a generation-suffixed candidate path rather than the failed path.
|
|
118
|
+
An unregistered candidate directory is cleaned before fresh creation only when
|
|
119
|
+
it carries the engine ownership marker. An unmarked pre-existing directory is
|
|
120
|
+
treated as foreign, preserved, and bypassed with a generation-suffixed candidate
|
|
121
|
+
path. Periodic GC treats generation-suffixed candidates as owned by the live
|
|
122
|
+
dispatch until the exact path is persisted.
|
|
123
|
+
|
|
124
|
+
A zero exit code is necessary but not sufficient. Before the candidate is
|
|
125
|
+
persisted as the dispatch worktree or an agent can start, it must:
|
|
126
|
+
|
|
127
|
+
- appear at the intended canonical path in `git worktree list --porcelain`;
|
|
128
|
+
- resolve its own top level and HEAD consistently with that registration;
|
|
129
|
+
- match the requested branch (or detached state) and pre-resolved target commit;
|
|
130
|
+
- return a clean `git --no-optional-locks status --porcelain`.
|
|
131
|
+
|
|
132
|
+
Validation failures follow the same cleanup-and-fresh-retry path. The former
|
|
133
|
+
`recoverPartialWorktree` fallback was removed: a command that returned nonzero
|
|
134
|
+
is never accepted merely because the leftover directory answers `rev-parse`.
|
|
135
|
+
|
|
88
136
|
## Ownership marker is never "dirty" (#284)
|
|
89
137
|
|
|
90
138
|
The reused-worktree preflight (`assertCleanSharedWorktree`) builds its
|
|
@@ -123,10 +171,32 @@ quarantine fallback; transient probe, fetch, auth, and push failures leave clean
|
|
|
123
171
|
ahead commits in place for retry. Recoverable
|
|
124
172
|
`refs/minions/quarantine/*` and `refs/minions/quarantine-wip/*` refs are listed
|
|
125
173
|
on Dashboard → Engine under **Quarantined Work**.
|
|
174
|
+
Periodic cleanup deletes these recovery refs after 10 days. This is intentionally
|
|
175
|
+
longer than the 24-hour quarantine-directory retention window: the refs preserve
|
|
176
|
+
committed HEAD and WIP snapshots after the directory itself is removed, while
|
|
177
|
+
still preventing recovery refs from growing without bound.
|
|
178
|
+
|
|
179
|
+
At dispatch close, an error result is inspected before orphan GC. A clean branch
|
|
180
|
+
that is aligned with origin (or successfully pushed there) remains removable.
|
|
181
|
+
Dirty files, unpushed commits that cannot be pushed, and indeterminate branch
|
|
182
|
+
state go through `_quarantineDirtyWorktree` first; dispatch-end GC is skipped
|
|
183
|
+
unless that inspection proves removal safe. The quarantine live guard receives
|
|
184
|
+
`excludeDispatchId` so the ending dispatch may preserve its own worktree while
|
|
185
|
+
any sibling live claimant still blocks rename/removal. This also covers exit
|
|
186
|
+
code `0` that becomes an error only after the required completion report or PR
|
|
187
|
+
contract is validated.
|
|
188
|
+
|
|
189
|
+
Process-anchored failed worktrees are snapshotted without moving the live
|
|
190
|
+
directory. If both that WIP snapshot and the protection-marker write fail, the
|
|
191
|
+
dispatch stays active with `_worktreeProtectionDeferred` instead of becoming
|
|
192
|
+
terminal. The active SQL dispatch row remains a fail-open GC claim after the
|
|
193
|
+
process anchor expires. Timeout recovery then retries preservation; only a
|
|
194
|
+
durable recovery ref or protected quarantine allows terminalization and later
|
|
195
|
+
periodic cleanup.
|
|
126
196
|
|
|
127
197
|
When `discoverFromWorkItems` finds a worktree in a `WORKTREE_DIRTY`,
|
|
128
198
|
`WORKTREE_DIVERGENT`, or post-stuck state, `_quarantineDirtyWorktree`
|
|
129
|
-
moves it to `<
|
|
199
|
+
moves it to `<path>-quarantine-<epoch-ms>` and lets the next tick
|
|
130
200
|
recreate a clean worktree. Six layered defenses cover the Windows EBUSY
|
|
131
201
|
race when git status descendants still hold packfile handles
|
|
132
202
|
(W-mq5n1zx5000hcfb5; PR #3156).
|
|
@@ -172,6 +242,12 @@ command when `preserved`, a `⚠️` warning when `failed`, or an explicit "noth
|
|
|
172
242
|
to preserve" note when `clean` — so a human never has to guess which bucket a
|
|
173
243
|
given quarantine landed in.
|
|
174
244
|
|
|
245
|
+
If either the committed backup ref or an uncommitted WIP ref cannot be created,
|
|
246
|
+
the quarantined directory is the only recovery artifact. The engine stamps
|
|
247
|
+
`.minions-quarantine-protected` into that directory, and the central
|
|
248
|
+
`shared.removeWorktree` primitive refuses to delete it. An operator must inspect
|
|
249
|
+
and remove that quarantine manually after recovering the files.
|
|
250
|
+
|
|
175
251
|
### Layer 1b — `--no-optional-locks` on every status probe (load-bearing)
|
|
176
252
|
|
|
177
253
|
`_statusPorcelainCmd()` emits `git --no-optional-locks status …`. Skips
|
|
@@ -289,7 +365,10 @@ layers.
|
|
|
289
365
|
|
|
290
366
|
## Periodic prune (W-mq5o6bvy000x7191)
|
|
291
367
|
|
|
292
|
-
`pruneWorktreesPeriodic` runs `
|
|
368
|
+
`pruneWorktreesPeriodic` runs `reconcileStaleWorktreeDirectories`
|
|
369
|
+
(physical directories absent from Git's registry),
|
|
370
|
+
`reclaimMissingDirWorktrees` (registry entries with a missing backing
|
|
371
|
+
directory), `pruneOrphanWorktrees` (in-root), and
|
|
293
372
|
`pruneOrphanWorktreesFromGitRegistry` (out-of-root via
|
|
294
373
|
`git worktree list --porcelain`) per project at
|
|
295
374
|
`ENGINE_DEFAULTS.worktreePruneIntervalTicks` cadence — catches Windows
|
|
@@ -297,6 +376,36 @@ EPERM/EBUSY stragglers that the dispatch-end GC couldn't reap and sweeps
|
|
|
297
376
|
the `git worktree list` registry for OUT-of-root entries the in-root
|
|
298
377
|
scanner is blind to.
|
|
299
378
|
|
|
379
|
+
The filesystem reconciliation pass is deliberately fail-closed and distinct
|
|
380
|
+
from dispatch-oriented GC. It lists every repo sharing a configured worktree
|
|
381
|
+
root before deleting anything; if any list fails, the whole shared root is
|
|
382
|
+
left untouched and the failure is logged/reported in sweep diagnostics. This
|
|
383
|
+
pass never deletes registered paths, but only `locked` / `initializing`
|
|
384
|
+
registrations are protected from the downstream dispatch-oriented pruner.
|
|
385
|
+
Unlocked registrations can therefore be removed there when unclaimed, while
|
|
386
|
+
non-terminal SQL claims, pool/managed-process/keep-process anchors, detected
|
|
387
|
+
CWD holders, protected quarantines, and unrelated infrastructure directories
|
|
388
|
+
remain protected. Deletion requires either `.minions-worktree` or the narrow
|
|
389
|
+
legacy proof used for old Windows residue: a `W-*` basename plus a `.git` file
|
|
390
|
+
pointing inside one of the project's common `.git/worktrees` directories.
|
|
391
|
+
Ordinary unregistered residue is eligible after 2h; unprotected
|
|
392
|
+
`-quarantine-<epoch-ms>` residue after 24h. Protected quarantine directories
|
|
393
|
+
remain until manual recovery because they are the only durable copy of work
|
|
394
|
+
whose backup ref could not be created.
|
|
395
|
+
|
|
396
|
+
`runCleanup` delegates its configured-root orphan pass to the same reconciler;
|
|
397
|
+
the former fail-open duplicate scan and generic age/count-cap deletion are
|
|
398
|
+
retired. Its remaining PR-terminal cleanup is narrower: only a currently
|
|
399
|
+
registered, unlocked, marker-owned worktree whose PR is still merged or
|
|
400
|
+
abandoned can be removed, after fresh SQL-live, process-anchor/CWD-holder, and
|
|
401
|
+
protected-quarantine checks.
|
|
402
|
+
|
|
403
|
+
Both removal pruners check exact persisted path ownership through
|
|
404
|
+
`shared.isWorktreePathLive` before calling `removeWorktree`. A live claim is
|
|
405
|
+
an expected `skipped` outcome, not a failed removal, and is retried on the next
|
|
406
|
+
sweep after the dispatch becomes terminal. Aggregate logs report mutually
|
|
407
|
+
meaningful `removed`, `skipped`, `preserved`, and `failed` counts.
|
|
408
|
+
|
|
300
409
|
### Scratch-dir skip and bounded reaper (PR #388)
|
|
301
410
|
|
|
302
411
|
The worktree root (`<localPath>/../worktrees/`) is a **shared namespace**: it
|
|
@@ -331,7 +440,7 @@ When adding new code that enumerates the worktree root directory, apply
|
|
|
331
440
|
`shared.isWorktreeRootInfraEntry(name)` to skip dot-prefixed entries before
|
|
332
441
|
calling any `removeWorktree` path.
|
|
333
442
|
|
|
334
|
-
### Ownership marker —
|
|
443
|
+
### Ownership marker — periodic GC only touches engine worktrees (W-mqecdoot)
|
|
335
444
|
|
|
336
445
|
A project repo's `git worktree list` includes EVERY worktree registered
|
|
337
446
|
against it — including ones a developer created by hand
|
|
@@ -341,14 +450,35 @@ live dispatch / pool / managed-spawn, **deleting human worktrees
|
|
|
341
450
|
mid-edit**. The fix: the engine stamps a `.minions-worktree` ownership
|
|
342
451
|
marker (`shared.WORKTREE_OWNER_MARKER`) into every worktree it creates
|
|
343
452
|
(`shared.writeWorktreeOwnerMarker`, called right after `git worktree add`
|
|
344
|
-
in `engine.js#runWorktreeAdd`).
|
|
345
|
-
|
|
346
|
-
|
|
453
|
+
in `engine.js#runWorktreeAdd`). Both in-root and out-of-root removal pruners
|
|
454
|
+
require `shared.hasWorktreeOwnerMarker(path)` before eviction —
|
|
455
|
+
**no marker → foreign → kept and never
|
|
347
456
|
escalated** (fail-open: leak a dir rather than nuke unpushed work). The
|
|
348
457
|
trailing `git worktree prune --expire=now` (drops registry entries whose
|
|
349
458
|
dirs are already gone) still runs regardless of ownership. The marker is
|
|
350
459
|
gitignored so it never shows up in any worktree's `git status`.
|
|
351
460
|
|
|
461
|
+
`shared.removeWorktree` remembers whether the marker existed before removal.
|
|
462
|
+
If git and filesystem fallbacks partially delete an owned worktree but leave a
|
|
463
|
+
locked husk, it restores the marker so the next periodic sweep can retry.
|
|
464
|
+
Previously unmarked paths are never marked by this recovery.
|
|
465
|
+
|
|
466
|
+
### Checkout free-space preflight
|
|
467
|
+
|
|
468
|
+
`engine.worktreeMinFreeSpaceBytes` defaults to 10 GiB (`0` disables) and is
|
|
469
|
+
editable in Dashboard -> Settings -> Worker Pool & Worktrees. Allocation and a
|
|
470
|
+
second spawn-time race check call `engine/worktree-preflight.js` before any
|
|
471
|
+
`git worktree add`. The probe uses the nearest existing ancestor when the
|
|
472
|
+
configured worktree root has not been created yet.
|
|
473
|
+
|
|
474
|
+
Below the threshold, the existing dispatch remains pending with
|
|
475
|
+
`_pendingReason: "worktree_low_disk_space"` and structured
|
|
476
|
+
`_worktreeDiskPreflight` fields (`requiredBytes`, `availableBytes`,
|
|
477
|
+
`worktreeRoot`, `probePath`, `checkedAt`, `message`). A statfs/probe error uses
|
|
478
|
+
`worktree_disk_probe_failed` and also fails closed. Both paths write a deduped
|
|
479
|
+
inbox alert, start no process or checkout, consume no retry, and are re-checked
|
|
480
|
+
on every allocation tick.
|
|
481
|
+
|
|
352
482
|
### Locked-`initializing` missing-dir reclaim (W-mqifblkf00149df5)
|
|
353
483
|
|
|
354
484
|
A third pruner, `worktree-gc.js#reclaimMissingDirWorktrees`, closes a
|
|
@@ -378,6 +508,11 @@ single `-f` errors `cannot remove a locked working tree`) followed by
|
|
|
378
508
|
(`shared.isWorktreePathLive`, fail-open: skip/leak rather than nuke); and
|
|
379
509
|
3. it is not the main checkout (`path === project root`).
|
|
380
510
|
|
|
511
|
+
The missing-dir pass runs before either removal pruner mutates the shared
|
|
512
|
+
porcelain snapshot. If a concurrent cleanup wins the race anyway, a failed
|
|
513
|
+
remove is re-listed and treated as success only when the registration is
|
|
514
|
+
already absent.
|
|
515
|
+
|
|
381
516
|
The same `remove -f -f` reaper (`engine.js#pruneStaleWorktreeForBranch`) is
|
|
382
517
|
also wired into **both** spawn-time worktree-creation recovery paths —
|
|
383
518
|
`_branchOnRemote === true` AND the fresh-create `else` branch — so a
|
|
@@ -434,9 +569,12 @@ Windows-only; three layers, each fail-open:
|
|
|
434
569
|
|
|
435
570
|
1. POSIX is a no-op (the `EBUSY` race is Windows-specific).
|
|
436
571
|
2. Master switch `engine.statusProbeKillDescendantsWin32` (default ON).
|
|
437
|
-
3. `shared.isWorktreePathLive(path, { excludeDispatchId })`
|
|
438
|
-
|
|
439
|
-
|
|
572
|
+
3. `shared.isWorktreePathLive(path, { excludeDispatchId, excludeSpawnLeaseId? })`
|
|
573
|
+
must be false. Spawn-attempt-owned cleanup passes both exclusions, so the
|
|
574
|
+
dispatch row is excluded only when its persisted lease matches; a stale
|
|
575
|
+
attempt cannot exclude a successor row with the same dispatch id. Fail-open:
|
|
576
|
+
on a live claim OR on a SQL error / throw the WHOLE reap is SKIPPED (leak a
|
|
577
|
+
worktree rather than nuke a live agent's unpushed work).
|
|
440
578
|
This check is **re-run at kill time** before Layer 2 (W-mqinlicl): the PEB
|
|
441
579
|
probe can take several seconds, and liveness flips on the engine tick — a
|
|
442
580
|
"not live" snapshot from the top of the function is unsafe to kill on.
|
package/engine/acp-transport.js
CHANGED