@yemi33/minions 0.1.2292 → 0.1.2294
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/settings.js +0 -45
- package/dashboard.js +3 -32
- package/docs/deprecated.json +6 -0
- package/docs/live-checkout-mode.md +0 -54
- package/docs/named-agents.md +1 -1
- package/docs/workspace-manifests.md +3 -4
- package/engine/cli.js +13 -0
- package/engine/features.js +14 -11
- package/engine/github.js +3 -0
- package/engine/lifecycle.js +86 -2
- package/engine/playbook.js +9 -4
- package/engine/prd-store.js +113 -7
- package/engine/queries.js +93 -72
- package/engine/shared.js +152 -30
- package/engine/work-items-store.js +21 -0
- package/engine.js +116 -151
- package/package.json +1 -1
- package/playbooks/fix.md +9 -0
package/dashboard/js/settings.js
CHANGED
|
@@ -289,32 +289,6 @@ async function openSettings() {
|
|
|
289
289
|
'⚠ Live mode: dispatches run directly in this repo\'s checkout. Only one mutating dispatch runs at a time. Dirty working trees block dispatch — commit or stash before running.' +
|
|
290
290
|
'</div>' +
|
|
291
291
|
'</div>';
|
|
292
|
-
// M006 — liveValidation section (per-project hybrid mode config).
|
|
293
|
-
// Controls: type (work item type that stays serialized in live checkout)
|
|
294
|
-
// and autoDispatch (auto-create validation WI after coding WI completes).
|
|
295
|
-
// Section is rendered with reduced opacity + pointer-events:none when
|
|
296
|
-
// checkoutMode is not 'live' — it only applies in hybrid mode.
|
|
297
|
-
var lvType = (p.liveValidation && p.liveValidation.type) ? p.liveValidation.type : '';
|
|
298
|
-
var lvAutoDispatch = !!(p.liveValidation && p.liveValidation.autoDispatch);
|
|
299
|
-
var lvDisabled = (currentWtMode !== 'live');
|
|
300
|
-
var lvSectionStyle = lvDisabled
|
|
301
|
-
? 'opacity:0.45;pointer-events:none;margin-bottom:6px'
|
|
302
|
-
: 'margin-bottom:6px';
|
|
303
|
-
var liveValidationBlock =
|
|
304
|
-
'<div data-live-validation-section="' + escHtml(p.name) + '" data-search="live validation deferred build test auto dispatch worktree" style="' + lvSectionStyle + '">' +
|
|
305
|
-
'<label style="font-size:var(--text-sm);color:var(--muted);display:block;margin-bottom:2px">Live validation (deferred build/test)' +
|
|
306
|
-
(lvDisabled ? ' <span style="font-size:var(--text-xs);opacity:0.7">(requires Live checkout)</span>' : '') +
|
|
307
|
-
'</label>' +
|
|
308
|
-
'<div style="font-size:var(--text-xs);color:var(--muted);margin-bottom:4px;line-height:1.4">' +
|
|
309
|
-
'For checkoutMode: live projects — coding agents run in worktrees; a separate dispatch validates in live checkout.' +
|
|
310
|
-
'</div>' +
|
|
311
|
-
'<input id="set-liveValidationType-' + escHtml(p.name) + '" value="' + escHtml(lvType) + '" placeholder="e.g. build-and-test" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:var(--text-md);margin-bottom:4px">' +
|
|
312
|
-
'<div style="font-size:var(--text-xs);color:var(--muted);margin-bottom:4px">Validation work item type (e.g. <code>build-and-test</code>, <code>test</code>, <code>verify</code>). Leave blank to disable.</div>' +
|
|
313
|
-
'<div style="display:flex;align-items:center;gap:8px;padding:2px 0">' +
|
|
314
|
-
'<input type="checkbox" id="set-liveValidationAutoDispatch-' + escHtml(p.name) + '"' + (lvAutoDispatch ? ' checked' : '') + ' style="accent-color:var(--blue);width:16px;height:16px;cursor:pointer">' +
|
|
315
|
-
'<label for="set-liveValidationAutoDispatch-' + escHtml(p.name) + '" style="font-size:var(--text-md);color:var(--text);cursor:pointer">Auto-dispatch validation WI after coding WI completes</label>' +
|
|
316
|
-
'</div>' +
|
|
317
|
-
'</div>';
|
|
318
292
|
return '<div data-settings-project="' + escHtml(p.name) + '" data-search="project ' + escHtml(p.name.toLowerCase()) + '" style="border:1px solid var(--border);border-radius:6px;padding:10px 12px;margin-bottom:12px">' +
|
|
319
293
|
'<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px">' +
|
|
320
294
|
'<div style="font-size:var(--text-md);font-weight:600">' + escHtml(p.name) + '</div>' +
|
|
@@ -323,7 +297,6 @@ async function openSettings() {
|
|
|
323
297
|
pathRow +
|
|
324
298
|
branchGrid +
|
|
325
299
|
worktreeModeBlock +
|
|
326
|
-
liveValidationBlock +
|
|
327
300
|
driftNote +
|
|
328
301
|
'<div style="display:flex;flex-direction:column;gap:6px;margin-top:8px">' +
|
|
329
302
|
settingsToggle('Discover from PRs', 'set-ws-prs-' + p.name, p.workSources.pullRequests.enabled, 'Discovery gate: scan repo for open PRs and surface them as review tasks. Independent of ADO/GitHub polling — does not affect already-tracked PRs.') +
|
|
@@ -729,14 +702,6 @@ async function openSettings() {
|
|
|
729
702
|
const chip = document.querySelector('[data-checkout-mode-chip="' + (window.CSS && CSS.escape ? CSS.escape(projName) : projName) + '"]');
|
|
730
703
|
if (!chip) return;
|
|
731
704
|
chip.style.display = (sel.value === 'live') ? '' : 'none';
|
|
732
|
-
// M006 — also toggle the liveValidation section opacity/pointer-events
|
|
733
|
-
// reactively: disabled when checkoutMode is not 'live'.
|
|
734
|
-
const lvSection = document.querySelector('[data-live-validation-section="' + (window.CSS && CSS.escape ? CSS.escape(projName) : projName) + '"]');
|
|
735
|
-
if (lvSection) {
|
|
736
|
-
const isLive = (sel.value === 'live');
|
|
737
|
-
lvSection.style.opacity = isLive ? '' : '0.45';
|
|
738
|
-
lvSection.style.pointerEvents = isLive ? '' : 'none';
|
|
739
|
-
}
|
|
740
705
|
});
|
|
741
706
|
});
|
|
742
707
|
}
|
|
@@ -1181,20 +1146,10 @@ async function saveSettings() {
|
|
|
1181
1146
|
// values.
|
|
1182
1147
|
const wtModeInput = document.getElementById('set-checkoutMode-' + p.name);
|
|
1183
1148
|
const wtModeValue = (wtModeInput && wtModeInput.value === 'live') ? 'live' : 'worktree';
|
|
1184
|
-
// M006 — liveValidation: read type text input and autoDispatch checkbox.
|
|
1185
|
-
// Empty type → send null so the server clears the field.
|
|
1186
|
-
const lvTypeInput = document.getElementById('set-liveValidationType-' + p.name);
|
|
1187
|
-
const lvAutoDispatchInput = document.getElementById('set-liveValidationAutoDispatch-' + p.name);
|
|
1188
|
-
const lvTypeValue = lvTypeInput ? lvTypeInput.value.trim() : '';
|
|
1189
|
-
const lvAutoDispatchValue = lvAutoDispatchInput ? !!lvAutoDispatchInput.checked : false;
|
|
1190
|
-
const liveValidationValue = lvTypeValue
|
|
1191
|
-
? { type: lvTypeValue, autoDispatch: lvAutoDispatchValue }
|
|
1192
|
-
: null;
|
|
1193
1149
|
return {
|
|
1194
1150
|
name: p.name,
|
|
1195
1151
|
mainBranch: mainBranchValue || null,
|
|
1196
1152
|
checkoutMode: wtModeValue,
|
|
1197
|
-
liveValidation: liveValidationValue,
|
|
1198
1153
|
workSources: {
|
|
1199
1154
|
pullRequests: { enabled: document.getElementById('set-ws-prs-' + p.name)?.checked ?? true },
|
|
1200
1155
|
workItems: { enabled: document.getElementById('set-ws-wi-' + p.name)?.checked ?? true }
|
package/dashboard.js
CHANGED
|
@@ -430,21 +430,6 @@ function mergeSettingsConfigUpdate(current, candidate, body, patch = {}) {
|
|
|
430
430
|
// `worktreeMode`. Drop any stale legacy key on every settings save so a
|
|
431
431
|
// migrated project never carries both fields.
|
|
432
432
|
delete currentProject.worktreeMode;
|
|
433
|
-
// M006 — mirror liveValidation: check both candidateProject (modified
|
|
434
|
-
// in-memory config) and the original body update. When handleSettingsUpdate
|
|
435
|
-
// deletes the field (null or empty type sent), candidateProject won't have
|
|
436
|
-
// it but the body update will — mirror the deletion to disk.
|
|
437
|
-
if (Object.prototype.hasOwnProperty.call(candidateProject, 'liveValidation')) {
|
|
438
|
-
if (candidateProject.liveValidation && typeof candidateProject.liveValidation === 'object' && candidateProject.liveValidation.type) {
|
|
439
|
-
currentProject.liveValidation = candidateProject.liveValidation;
|
|
440
|
-
} else {
|
|
441
|
-
delete currentProject.liveValidation;
|
|
442
|
-
}
|
|
443
|
-
} else if (Object.prototype.hasOwnProperty.call(update, 'liveValidation')) {
|
|
444
|
-
// Body sent liveValidation key but handleSettingsUpdate deleted it from
|
|
445
|
-
// the in-memory config (null / empty type) — propagate to disk.
|
|
446
|
-
delete currentProject.liveValidation;
|
|
447
|
-
}
|
|
448
433
|
}
|
|
449
434
|
}
|
|
450
435
|
shared.pruneDefaultClaudeConfig(current);
|
|
@@ -10921,11 +10906,6 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
10921
10906
|
// the per-project dropdown. resolveCheckoutMode honors the legacy
|
|
10922
10907
|
// worktreeMode field; 'worktree' (default) or 'live'.
|
|
10923
10908
|
checkoutMode: shared.resolveCheckoutMode(p),
|
|
10924
|
-
// M006 — surface liveValidation so the Settings UI can pre-fill
|
|
10925
|
-
// the type input and autoDispatch toggle. Null when not configured.
|
|
10926
|
-
liveValidation: (p.liveValidation && typeof p.liveValidation === 'object' && p.liveValidation.type)
|
|
10927
|
-
? { type: p.liveValidation.type, autoDispatch: !!p.liveValidation.autoDispatch }
|
|
10928
|
-
: null,
|
|
10929
10909
|
workSources: {
|
|
10930
10910
|
pullRequests: { enabled: p.workSources?.pullRequests?.enabled !== false, cooldownMinutes: p.workSources?.pullRequests?.cooldownMinutes ?? 30 },
|
|
10931
10911
|
workItems: { enabled: p.workSources?.workItems?.enabled !== false, cooldownMinutes: p.workSources?.workItems?.cooldownMinutes ?? 0 }
|
|
@@ -11350,19 +11330,10 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
11350
11330
|
// Drop the legacy field so a migrated project never carries both.
|
|
11351
11331
|
delete proj.worktreeMode;
|
|
11352
11332
|
}
|
|
11353
|
-
// M006 — per-project liveValidation: { type, autoDispatch }.
|
|
11354
|
-
// Only meaningful when checkoutMode is 'live'. Null / missing type
|
|
11355
|
-
// clears the field; explicit object with a non-empty type persists it.
|
|
11356
|
-
if (Object.prototype.hasOwnProperty.call(update, 'liveValidation')) {
|
|
11357
|
-
const lv = update.liveValidation;
|
|
11358
|
-
if (!lv || typeof lv !== 'object' || !lv.type || typeof lv.type !== 'string' || !lv.type.trim()) {
|
|
11359
|
-
delete proj.liveValidation;
|
|
11360
|
-
} else {
|
|
11361
|
-
proj.liveValidation = { type: lv.type.trim(), autoDispatch: !!lv.autoDispatch };
|
|
11362
|
-
}
|
|
11363
|
-
}
|
|
11364
11333
|
}
|
|
11365
11334
|
}
|
|
11335
|
+
|
|
11336
|
+
shared.pruneDefaultClaudeConfig(config);
|
|
11366
11337
|
mutateDashboardConfig(current => mergeSettingsConfigUpdate(current, config, body, _configPatch));
|
|
11367
11338
|
// Refresh in-memory CONFIG so subsequent reads see the update
|
|
11368
11339
|
reloadConfig();
|
|
@@ -13427,7 +13398,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
13427
13398
|
// /api/prd/regenerate removed — use /api/plans/approve which does diff-aware update
|
|
13428
13399
|
|
|
13429
13400
|
// Agents
|
|
13430
|
-
{ method: 'POST', path: '/api/pull-requests/link', desc: 'Manually link an external PR for tracking', params: 'url, title?, project?, contextOnly?, context?, workItemId?', handler: async (req, res) => {
|
|
13401
|
+
{ method: 'POST', path: '/api/pull-requests/link', desc: 'Manually link an external PR for tracking', params: 'url, title?, project?, contextOnly?, autoObserve? (deprecated alias for !contextOnly), context?, workItemId?', handler: async (req, res) => {
|
|
13431
13402
|
const body = await readBody(req);
|
|
13432
13403
|
const { url } = body;
|
|
13433
13404
|
if (!url) return jsonReply(res, 400, { error: 'url required' });
|
package/docs/deprecated.json
CHANGED
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
"LEGACY_NEEDS_REVIEW_STATUS"
|
|
53
53
|
],
|
|
54
54
|
"reason": "Read-side tolerance: cleanup sweep auto-migrates four obsolete work-item / PRD status strings ('in-pr', 'implemented', 'complete', 'needs-human-review') to the canonical 'done' / 'failed' values. The aliases are no longer written anywhere in the engine; the constants exist only to repair stale on-disk values from old engine versions.",
|
|
55
|
+
"status": "active",
|
|
55
56
|
"targetRemovalDate": null,
|
|
56
57
|
"notes": "Keep indefinitely until telemetry / a sweep log shows zero migrations performed for 30 consecutive days across all known projects (work-items.json + prd/*.json). At that point the constants and both _migrateLegacyItem branches in engine/cleanup.js (definitions at :1165-1166; usage at :1168-1183 for work items and :1269-1272 for PRD missing_features) can be deleted. Total cost on disk today: 4 strings."
|
|
57
58
|
},
|
|
@@ -76,6 +77,7 @@
|
|
|
76
77
|
}
|
|
77
78
|
],
|
|
78
79
|
"removalGate": "Telemetry: the `deprecated-config-claude` warning emitted at engine/shared.js:2492-2495 must report zero hits across all known engines for >=30 consecutive days, AND a sweep of every persisted config.json must show no `config.claude.binary` value that diverges from DEFAULT_CLAUDE.binary. Only then is the override branch in resolveBinary() (engine/runtimes/claude.js:82-86) removable, along with the `_deprecatedConfigClaudeFields` membership for `binary` and the warning emitter at engine/shared.js:2482-2496.",
|
|
80
|
+
"status": "active",
|
|
79
81
|
"targetRemovalDate": null,
|
|
80
82
|
"notes": "Do NOT set targetRemovalDate — removal must be signal-gated, not calendar-gated. This entry is paired with `prune-default-claude-config`: the prune strips DEFAULT-matching values but intentionally preserves user overrides, which is precisely why the override branch in claude.js stays reachable. Removing the override before the prune entry's gate clears would silently break installs that still rely on a custom binary path."
|
|
81
83
|
},
|
|
@@ -120,6 +122,7 @@
|
|
|
120
122
|
}
|
|
121
123
|
],
|
|
122
124
|
"removalGate": "Telemetry: the once-per-boot deprecation log line emitted by applyLegacyCcModelMigration (via the injected logger at engine/shared.js:2407) must show zero promotion events across all known engines for >=30 consecutive days, AND a sweep of every persisted config.json must confirm no `engine.ccModel` field remains. Once both conditions hold, removal deletes the function + _resetLegacyCcModelMigrationFlag export at engine/shared.js:4977, the boot call at engine/cli.js:477, the CLAUDE.md:316 paragraph and docs/slim-ux/concepts.md:671 reference, and the tests at runtime-fleet-helpers.test.js:209-254 + :500-505 + unit.test.js:19801.",
|
|
125
|
+
"status": "active",
|
|
123
126
|
"targetRemovalDate": null,
|
|
124
127
|
"notes": "Do NOT set targetRemovalDate — gating is signal-based. The function is silent on no-op (returns false without logging), so the meaningful telemetry signal is the absence of the promotion log line over the sweep window, NOT the absence of function invocations (cli.js calls it every boot regardless)."
|
|
125
128
|
},
|
|
@@ -162,6 +165,7 @@
|
|
|
162
165
|
}
|
|
163
166
|
],
|
|
164
167
|
"removalGate": "All direct-readers of the mirror JSON files must be confirmed routed through their respective SQL store's read helper. Specifically: (a) grep the codebase for `safeJson`, `safeJsonArr`, `safeJsonObj`, `readFileSync(...work-items.json|pull-requests.json|metrics.json|watches.json|schedule-runs.json|pipeline-runs.json|managed-processes.json|worktree-pool.json|log.json|dispatch.json...)` and confirm every hit is either (i) a test fixture that can move to the SQL helper, or (ii) intentionally documented as bypassing SQL. (b) Run the full test suite with each store's _mirrorJsonFromSql temporarily neutered (returning early before safeWrite) and confirm 0 failures — that proves no production code path depends on the mirror. Once both conditions hold, removal deletes each store's _mirrorJsonFromSql call site in shared.js (mutateWorkItems/mutatePullRequests/etc.), the corresponding _readJsonArrayFallback paths, and the JSON file gitignore entries. CLAUDE.md update can ship independently as soon as someone has bandwidth.",
|
|
168
|
+
"status": "active",
|
|
165
169
|
"targetRemovalDate": null,
|
|
166
170
|
"notes": "Do NOT set targetRemovalDate — gating is signal-based, not calendar-based. The mirror writes are cheap (a few KB per write, sub-ms) so there is no production cost to keeping them indefinitely; the only reason to remove them is to simplify the codebase and lock in SQL-as-the-single-source-of-truth. Order matters: when retiring a specific store's mirror, retire the corresponding CLAUDE.md mention in the same PR so the docs never claim SQL-only while a mirror still writes."
|
|
167
171
|
},
|
|
@@ -216,6 +220,7 @@
|
|
|
216
220
|
}
|
|
217
221
|
],
|
|
218
222
|
"removalGate": "Telemetry: pruneDefaultClaudeConfig must return false (no mutation) for every call across all known engines for >=30 consecutive days (add an `_engine.pruneDefaultClaudeConfigStrips` counter if needed to observe this), AND the parent `config-claude-binary-override` entry must have already cleared its own gate. The dependency is strict: removing the prune while users still rely on the override branch would surface the `deprecated-config-claude` warning on every stale generated default. Once both conditions hold, removal is the function definition (engine/shared.js:3126), the export at :5673, all 5 call sites (dashboard.js:202, :9116, :9331, :9450; minions.js:385), and the tests at unit.test.js:2260-2303 + runtime-fleet-helpers.test.js:546.",
|
|
223
|
+
"status": "active",
|
|
219
224
|
"targetRemovalDate": null,
|
|
220
225
|
"notes": "Do NOT set targetRemovalDate — gating is signal-based AND ordered. This entry MUST NOT be removed before `config-claude-binary-override` clears its gate, otherwise installs with stale defaults will flood the deprecation channel until their next config save. The 5 call sites form a complete coverage net: load (dashboard.js:202 + minions.js:385) + save (dashboard.js:9116/9331/9450), so any code path that touches config.json runs the sanitizer."
|
|
221
226
|
},
|
|
@@ -281,6 +286,7 @@
|
|
|
281
286
|
}
|
|
282
287
|
],
|
|
283
288
|
"deprecated": "2026-06-08",
|
|
289
|
+
"status": "active",
|
|
284
290
|
"targetRemovalDate": null,
|
|
285
291
|
"notes": "targetRemovalDate intentionally null — unlike the record-field aliases (`_contextOnly`, `_autoObserve`, `_manual`) which carry a 7-day clock, the `observe` body param is documented as a longer-lived back-compat alias. Set targetRemovalDate to a concrete future date once the dashboard UI + any client scripts are confirmed to POST `contextOnly` exclusively. Removal scope when the date is set: drop the `body.observe` fallback in dashboard.js, drop `observe` from the route registry params, and update any client still POSTing `observe`."
|
|
286
292
|
},
|
|
@@ -173,60 +173,6 @@ git for-each-ref --format '%(refname:short) %(upstream:track)' refs/heads \
|
|
|
173
173
|
|
|
174
174
|
The engine has no opinion about local branches; this hygiene is the operator's responsibility in live mode.
|
|
175
175
|
|
|
176
|
-
## Hybrid mode / deferred validation
|
|
177
|
-
|
|
178
|
-
> **Requires `checkoutMode: 'live'`** — `liveValidation` is ignored when `checkoutMode` is `'worktree'` (the default).
|
|
179
|
-
|
|
180
|
-
Hybrid mode lets you run *coding* work items (implement, fix, docs, …) in isolated worktrees while keeping *validation* work items (build-and-test, test, verify, …) serialized in live checkout — the setup where build caches, native toolchains, and test infrastructure only exist in one canonical checkout.
|
|
181
|
-
|
|
182
|
-
### Full flow
|
|
183
|
-
|
|
184
|
-
1. **Coding WI dispatched** — because `liveValidation.type` is set and this WI's type is *not* the validation type, `resolveCheckoutMode(project, workItem.type)` returns `'worktree'`. The engine creates an isolated worktree as normal, runs the agent, and pushes a PR branch.
|
|
185
|
-
2. **Coding WI completes** — if `liveValidation.autoDispatch: true`, the lifecycle hook (`engine/lifecycle.js`) auto-creates a validation WI of type `liveValidation.type` targeting the same PR branch.
|
|
186
|
-
3. **Validation WI dispatched** — `resolveCheckoutMode(project, 'build-and-test')` returns `'live'` (matches `liveValidation.type`). The engine runs `prepareLiveCheckout` in the operator's canonical checkout, which checks out the coding WI's branch in-place and runs the validation agent.
|
|
187
|
-
4. **Validation WI completes** — `restoreLiveCheckoutAtDispatchEnd` checks the operator's tree back to the original ref.
|
|
188
|
-
|
|
189
|
-
### Config snippet
|
|
190
|
-
|
|
191
|
-
```jsonc
|
|
192
|
-
{
|
|
193
|
-
"projects": [{
|
|
194
|
-
"name": "android-aosp",
|
|
195
|
-
"localPath": "/home/yemi/aosp",
|
|
196
|
-
"checkoutMode": "live",
|
|
197
|
-
"liveValidation": {
|
|
198
|
-
"type": "build-and-test",
|
|
199
|
-
"autoDispatch": true
|
|
200
|
-
}
|
|
201
|
-
}]
|
|
202
|
-
}
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
Configure via Dashboard → Settings → Projects → **Live validation (deferred build/test)** section. The section is greyed-out when `checkoutMode` is not `'live'`.
|
|
206
|
-
|
|
207
|
-
### Agent behavior contract
|
|
208
|
-
|
|
209
|
-
When a coding agent is dispatched on a project with `liveValidation.autoDispatch: true`, the agent's playbook includes a note that inline build/test runs are not required:
|
|
210
|
-
|
|
211
|
-
> Skip inline build and test verification steps — a separate validation dispatch will run these in the live checkout after this coding WI completes. Push the branch and report success; the validation WI handles build/test confirmation.
|
|
212
|
-
|
|
213
|
-
This prevents coding agents (running in isolated worktrees) from attempting builds that may fail due to missing native toolchains, caches, or environment variables only present in the canonical checkout.
|
|
214
|
-
|
|
215
|
-
### Migration guidance
|
|
216
|
-
|
|
217
|
-
If your project is on `checkoutMode: live` and you want parallel coding WIs, add:
|
|
218
|
-
|
|
219
|
-
```jsonc
|
|
220
|
-
"liveValidation": { "type": "build-and-test", "autoDispatch": true }
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
No other config changes needed. The engine automatically:
|
|
224
|
-
- Dispatches coding WIs into isolated worktrees (escaping the live-checkout cap of 1)
|
|
225
|
-
- Dispatches validation WIs serially in live checkout (preserving your build environment)
|
|
226
|
-
- Auto-creates validation WIs after each coding WI's PR is pushed
|
|
227
|
-
|
|
228
|
-
To opt back out, clear `liveValidation` from the project config (or set to `null` via Dashboard → Settings).
|
|
229
|
-
|
|
230
176
|
## Non-goals
|
|
231
177
|
|
|
232
178
|
Live-checkout mode is deliberately small. These are NOT supported and will not be added:
|
package/docs/named-agents.md
CHANGED
|
@@ -86,7 +86,7 @@ defaults (`WORKSPACE_MANIFEST_DEFAULTS` in `engine/shared.js`):
|
|
|
86
86
|
|
|
87
87
|
Enforcement helpers (also in `engine/shared.js`): `validateWorkspaceManifest`,
|
|
88
88
|
`resolveAgentManifest`, `agentCanUseRepo`, `agentCanUseTool`, `agentCanFetchUrl`,
|
|
89
|
-
`
|
|
89
|
+
`mergeManifestAllowedTools`. Today the engine actively enforces (a) the
|
|
90
90
|
**repo gate** at dispatch time in `engine.js spawnAgent`
|
|
91
91
|
(`FAILURE_CLASS.WORKSPACE_MANIFEST_REPO`, non-retryable) and (b) the **tool merge** into the
|
|
92
92
|
runtime `--allowedTools` flag at spawn time. Defaults are permissive — an agent with no
|
|
@@ -31,7 +31,7 @@ A manifest lives on each agent definition under `workspace_manifest`. All fields
|
|
|
31
31
|
| `allowed_tools` | `string[]` \| `null` | `null` (permissive) | Canonical tool-name whitelist (e.g. `Edit`, `Read`, `Bash`). Empty array = deny all. Case-sensitive. Merged into the runtime adapter's `--allowedTools` flag at spawn time. |
|
|
32
32
|
| `allowed_repos` | `string[]` \| `null` | `null` (permissive) | Canonical PR-scope (`github:owner/repo`, `ado:org/proj/repo`) or bare `owner/repo`. Empty array = deny all. Case-insensitive. Enforced at dispatch time — out-of-scope dispatch fails with `failure_class: 'workspace-manifest-repo-forbidden'`. |
|
|
33
33
|
| `allowed_external_urls` | `string[]` \| `null` | `null` (permissive) | Host allow-list for `web_fetch` / `web_search`. Bare host (`github.com`) = exact match; `*.example.com` = wildcard subdomain **and** apex. Empty array = deny all. Currently advisory — see "Enforcement points" below. |
|
|
34
|
-
| `memory_scope` | `'private'` \| `'shared'` \| `'read-only-shared'` | `'shared'` | What slice of team knowledge the agent can read/write.
|
|
34
|
+
| `memory_scope` | `'private'` \| `'shared'` \| `'read-only-shared'` | `'shared'` | What slice of team knowledge the agent can read/write. Full enforcement at the consolidation layer is future work. |
|
|
35
35
|
|
|
36
36
|
## Default behaviour & backward compatibility
|
|
37
37
|
|
|
@@ -57,21 +57,20 @@ A config that never mentions `workspace_manifest` produces the exact same dispat
|
|
|
57
57
|
| **Spawn — tool merge** | `engine.js spawnAgent()` → `_buildAgentSpawnFlags(..., allowedTools)` | `shared.mergeManifestAllowedTools(claudeConfig.allowedTools, manifest.allowed_tools)` produces the intersection of the runtime baseline and the manifest list. Result is passed as `--allowedTools <csv>` to Claude / Copilot / Codex, so the CLI itself enforces the narrowed surface. Empty manifest list (`[]`) = deny-all. Same merge runs on the steering-resume codepath. |
|
|
58
58
|
| **Agent context** | Future work (playbook.js) | Manifest can be surfaced into the agent prompt so the agent sees its declared scope. Today, `shared.resolveAgentManifest(agent)` returns the resolved struct any caller can read. |
|
|
59
59
|
| **URL fetch** | Advisory today | `allowed_external_urls` is validated at config load. No runtime helper function — a future runtime adapter hook can wire URL-fetch intercepts against the manifest. |
|
|
60
|
-
| **Memory scope** | Advisory today | `
|
|
60
|
+
| **Memory scope** | Advisory today | `memory_scope` field is stored in the manifest (`private` = agent only sees its own `knowledge/agents/<id>.md`; `shared` = full team knowledge (current default); `read-only-shared` = reads shared memory but should not write inbox/notes). No runtime enforcement helper — full enforcement at the consolidation layer is future work. |
|
|
61
61
|
|
|
62
62
|
## Helpers (in `engine/shared.js`)
|
|
63
63
|
|
|
64
64
|
```js
|
|
65
65
|
const { MEMORY_SCOPES, WORKSPACE_MANIFEST_DEFAULTS,
|
|
66
66
|
validateWorkspaceManifest, resolveAgentManifest,
|
|
67
|
-
agentCanUseRepo,
|
|
67
|
+
agentCanUseRepo,
|
|
68
68
|
mergeManifestAllowedTools, formatManifestRejection } = require('./engine/shared');
|
|
69
69
|
```
|
|
70
70
|
|
|
71
71
|
- `validateWorkspaceManifest(manifest)` → `{ ok, errors }`. `null`/`undefined` is valid (uses defaults).
|
|
72
72
|
- `resolveAgentManifest(agent, config?)` → fresh copy of `WORKSPACE_MANIFEST_DEFAULTS` overlaid with the agent's `workspace_manifest`. Malformed manifest silently falls back to defaults; surface the error via `validateWorkspaceManifest` at config-load time.
|
|
73
73
|
- `agentCanUseRepo(agent, projectOrString)` → `bool`. Accepts a project object or a string identifier (`github:owner/repo`, `ado:org/proj/repo`, or bare `owner/repo`). Case-insensitive.
|
|
74
|
-
- `agentMemoryScope(agent)` → one of `MEMORY_SCOPES`. Unknown values fall back to `'shared'`.
|
|
75
74
|
- `mergeManifestAllowedTools(baselineCsv, manifestArray)` → merged CSV. Intersection semantics: `null` manifest = baseline unchanged; empty array = deny-all; empty baseline + manifest = manifest as ceiling.
|
|
76
75
|
- `formatManifestRejection({ agentId, kind, target, allowed })` → structured human-readable rejection string used by the dispatch repo gate.
|
|
77
76
|
|
package/engine/cli.js
CHANGED
|
@@ -624,6 +624,19 @@ const commands = {
|
|
|
624
624
|
}
|
|
625
625
|
} catch (err) { e.log('warn', `note-link backfill failed: ${err.message}`); }
|
|
626
626
|
|
|
627
|
+
// Backfill work_items.prd_item_id (the WI↔PRD-item FK) for rows that predate
|
|
628
|
+
// the dual-write stamp (#546) or that a JSON re-hydrate transiently zeroed.
|
|
629
|
+
// Set-based, only touches NULL FKs, no-op once everything is linked — safe on
|
|
630
|
+
// every boot. Lets the Phase-10 render join move off the feature-id string.
|
|
631
|
+
try {
|
|
632
|
+
const fk = require('./prd-store').backfillWorkItemPrdItemIds();
|
|
633
|
+
const filled = (fk.byWorkItemId || 0) + (fk.bySourcePlan || 0);
|
|
634
|
+
if (fk.ok && filled > 0) {
|
|
635
|
+
e.log('info', `Backfilled prd_item_id on ${filled} work item(s) (${fk.byWorkItemId} by workItemId, ${fk.bySourcePlan} by sourcePlan)`);
|
|
636
|
+
console.log(` Linked ${filled} work item(s) to their PRD item.`);
|
|
637
|
+
}
|
|
638
|
+
} catch (err) { e.log('warn', `prd_item_id backfill failed: ${err.message}`); }
|
|
639
|
+
|
|
627
640
|
// Auto-heal projects missing workSources (cloned-repo / hand-rolled-config
|
|
628
641
|
// footgun): without this block, discoverFromWorkItems / discoverFromPrs
|
|
629
642
|
// bail silently and the engine looks healthy but never dispatches. The
|
package/engine/features.js
CHANGED
|
@@ -77,19 +77,22 @@ const FEATURES = {
|
|
|
77
77
|
default: true,
|
|
78
78
|
addedIn: '0.1.1916',
|
|
79
79
|
requiredCcRuntime: 'copilot',
|
|
80
|
+
expires: '2026-09-01',
|
|
80
81
|
},
|
|
81
|
-
//
|
|
82
|
-
// PRD
|
|
83
|
-
// (
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
82
|
+
// prdJoinFromFk — Phase 10 step 4.3 render-join flip. When ON, getPrdInfo
|
|
83
|
+
// resolves each PRD item to its work item via the stable SQL FK
|
|
84
|
+
// (work_items.prd_item_id → prd_items.id) instead of the fragile feature-id
|
|
85
|
+
// string match (work_item.id === feature.id), which bleeds when a live and an
|
|
86
|
+
// archived PRD share a feature id (footgun #7). PURELY ADDITIVE: the FK lookup
|
|
87
|
+
// is tried first and falls back to the exact legacy id match for any row not
|
|
88
|
+
// yet stamped, when the flag is OFF, or when SQL is unavailable — so flipping
|
|
89
|
+
// it off (or a NULL FK) reproduces current behavior byte-for-byte. The FK
|
|
90
|
+
// lives only in SQL (unconditional read source since Phase 10 step 3 was
|
|
91
|
+
// completed). Temporary migration gate.
|
|
92
|
+
'prdJoinFromFk': {
|
|
93
|
+
description: 'Resolve the dashboard WI↔PRD-item join via the SQL FK (work_items.prd_item_id) instead of the feature-id string match. Additive with a string-match fallback; reversible (Phase 10 step 4.3).',
|
|
91
94
|
default: true,
|
|
92
|
-
addedIn: '0.1.
|
|
95
|
+
addedIn: '0.1.2232',
|
|
93
96
|
expires: '2026-12-01',
|
|
94
97
|
},
|
|
95
98
|
};
|
package/engine/github.js
CHANGED
|
@@ -1935,6 +1935,9 @@ module.exports = {
|
|
|
1935
1935
|
// Exported for testing
|
|
1936
1936
|
isGitHub,
|
|
1937
1937
|
getRepoSlug,
|
|
1938
|
+
getConfiguredGitHubAuthorLogins, // exported for testing (W-mqyn73pa000333c4)
|
|
1939
|
+
_commentKey, // exported for testing (W-mqyn73pa000333c4)
|
|
1940
|
+
_entryCommentKey, // exported for testing (W-mqyn73pa000333c4)
|
|
1938
1941
|
isSlugInBackoff,
|
|
1939
1942
|
recordSlugFailure,
|
|
1940
1943
|
resetSlugBackoff,
|
package/engine/lifecycle.js
CHANGED
|
@@ -74,9 +74,15 @@ function checkPlanCompletion(meta, config) {
|
|
|
74
74
|
|
|
75
75
|
// Check 2: every feature must be in a terminal state (done, failed, or cancelled).
|
|
76
76
|
// Failed/cancelled items are unrecoverable — waiting on them blocks the plan indefinitely.
|
|
77
|
+
// P-a2b4c6d8: when a work item EXISTS, use ONLY the work item's status — do NOT fall
|
|
78
|
+
// through to the PRD item's own status field as a shortcut. If syncPrdItemStatus stamped
|
|
79
|
+
// the PRD item 'done' ahead of the actual WI completing (race / premature write), the old
|
|
80
|
+
// fallback incorrectly passed the gate and triggered early verify WI creation. The PRD item
|
|
81
|
+
// status is only a valid fallback when NO work item has been materialized for that feature
|
|
82
|
+
// (externally-resolved items that were never dispatched).
|
|
77
83
|
const notTerminal = [...planFeatureIds].filter(id => {
|
|
78
84
|
const w = workItemById[id];
|
|
79
|
-
if (w
|
|
85
|
+
if (w) return !PLAN_TERMINAL_STATUSES.has(w.status); // WI exists: trust WI status only
|
|
80
86
|
const prdItem = (plan.missing_features || []).find(f => f.id === id);
|
|
81
87
|
return !(prdItem && PLAN_TERMINAL_STATUSES.has(prdItem.status));
|
|
82
88
|
});
|
|
@@ -815,6 +821,22 @@ function stampPrdItemWorkItemId(itemId, sourcePlan) {
|
|
|
815
821
|
} catch (err) { log('warn', `stampPrdItemWorkItemId: ${err.message}`); }
|
|
816
822
|
}
|
|
817
823
|
|
|
824
|
+
function stampPrdItemPrUrl(itemId, sourcePlan, prUrl) {
|
|
825
|
+
if (!itemId || !sourcePlan || !prUrl) return;
|
|
826
|
+
try {
|
|
827
|
+
const fpath = path.join(PRD_DIR, sourcePlan);
|
|
828
|
+
if (!fs.existsSync(fpath)) return;
|
|
829
|
+
const plan = safeJsonNoRestore(fpath);
|
|
830
|
+
const feature = plan?.missing_features?.find(f => f.id === itemId);
|
|
831
|
+
if (!feature || feature.pr_url === prUrl) return;
|
|
832
|
+
mutateJsonFileLocked(fpath, (fresh) => {
|
|
833
|
+
const f = fresh?.missing_features?.find(x => x.id === itemId);
|
|
834
|
+
if (f && f.pr_url !== prUrl) f.pr_url = prUrl;
|
|
835
|
+
return fresh;
|
|
836
|
+
}, { skipWriteIfUnchanged: true });
|
|
837
|
+
} catch (err) { log('warn', `stampPrdItemPrUrl: ${err?.message || err}`); }
|
|
838
|
+
}
|
|
839
|
+
|
|
818
840
|
// ─── PRD Backward-Scan Reconciliation (#929, #984) ─────────────────────────
|
|
819
841
|
// Proactive counterpart to syncPrdItemStatus. Scans all active PRDs and:
|
|
820
842
|
// 1. Promotes "missing" items to "updated" when a done work item already exists (#929)
|
|
@@ -1088,6 +1110,7 @@ function syncPrsFromOutput(output, agentId, meta, config, opts = {}) {
|
|
|
1088
1110
|
url: prUrl,
|
|
1089
1111
|
prdItems: meta?.item?.id ? [meta.item.id] : [],
|
|
1090
1112
|
sourcePlan: meta?.item?.sourcePlan || '',
|
|
1113
|
+
prdItemId: meta?.item?.id || '',
|
|
1091
1114
|
itemType: meta?.item?.itemType || '',
|
|
1092
1115
|
contextOnly: shouldSyncPrAsContextOnly(meta),
|
|
1093
1116
|
};
|
|
@@ -2496,7 +2519,7 @@ function fixCompletionChangedBranch(structuredCompletion) {
|
|
|
2496
2519
|
}
|
|
2497
2520
|
|
|
2498
2521
|
function normalizePrFixBranchName(branch) {
|
|
2499
|
-
return String(branch || '').trim().replace(/^refs\/heads
|
|
2522
|
+
return String(branch || '').trim().replace(/^refs\/heads\//i, '');
|
|
2500
2523
|
}
|
|
2501
2524
|
|
|
2502
2525
|
function getPrFixBaselineHead(pr) {
|
|
@@ -5834,6 +5857,25 @@ function syncPrdFromPrs(config) {
|
|
|
5834
5857
|
if (totalReconciled > 0) {
|
|
5835
5858
|
log('info', `PR sync: reconciled ${totalReconciled} work item(s) to done`);
|
|
5836
5859
|
}
|
|
5860
|
+
|
|
5861
|
+
// Stamp pr_url onto PRD items for every PR that has prdItems[] + a URL (P-b3c2d4e5).
|
|
5862
|
+
// This is separate from the done-reconcile loop above — we stamp pr_url as soon as
|
|
5863
|
+
// we know the PR exists, regardless of merged/open status.
|
|
5864
|
+
for (const pr of allPrs) {
|
|
5865
|
+
if (!pr?.url || !Array.isArray(pr.prdItems) || pr.prdItems.length === 0) continue;
|
|
5866
|
+
for (const itemId of pr.prdItems) {
|
|
5867
|
+
if (!itemId) continue;
|
|
5868
|
+
let sourcePlan = pr.sourcePlan;
|
|
5869
|
+
if (!sourcePlan) {
|
|
5870
|
+
for (const project of allProjects) {
|
|
5871
|
+
const items = safeJsonArr(shared.projectWorkItemsPath(project));
|
|
5872
|
+
const wi = items.find(w => w.id === itemId);
|
|
5873
|
+
if (wi?.sourcePlan) { sourcePlan = wi.sourcePlan; break; }
|
|
5874
|
+
}
|
|
5875
|
+
}
|
|
5876
|
+
if (sourcePlan) stampPrdItemPrUrl(itemId, sourcePlan, pr.url);
|
|
5877
|
+
}
|
|
5878
|
+
}
|
|
5837
5879
|
} catch (err) {
|
|
5838
5880
|
// Non-fatal — log and continue
|
|
5839
5881
|
try { log('warn', `syncPrdFromPrs error: ${err?.message || err}`); } catch { /* engine not available */ }
|
|
@@ -6156,6 +6198,44 @@ function pruneScopeMismatchDuplicatePrs(config) {
|
|
|
6156
6198
|
return { pruned, scanned };
|
|
6157
6199
|
}
|
|
6158
6200
|
|
|
6201
|
+
// Repair helper: collapse prNumber duplicates across all project-scoped and
|
|
6202
|
+
// the central pull-requests file. Called once per reconcile cycle alongside
|
|
6203
|
+
// pruneScopeMismatchDuplicatePrs so accumulated duplicates are cleaned up
|
|
6204
|
+
// without requiring a one-off migration.
|
|
6205
|
+
function collapseAllDuplicatePrRecords(config) {
|
|
6206
|
+
config = config || getConfig();
|
|
6207
|
+
const projects = shared.getProjects(config) || [];
|
|
6208
|
+
let collapsed = 0;
|
|
6209
|
+
let scanned = 0;
|
|
6210
|
+
|
|
6211
|
+
for (const project of projects) {
|
|
6212
|
+
if (!project || !project.name) continue;
|
|
6213
|
+
const prPath = projectPrPath(project);
|
|
6214
|
+
try {
|
|
6215
|
+
const result = shared.collapseDuplicatePrRecords(prPath, { project });
|
|
6216
|
+
collapsed += result.collapsed || 0;
|
|
6217
|
+
scanned++;
|
|
6218
|
+
} catch (err) {
|
|
6219
|
+
log('warn', `collapseAllDuplicatePrRecords: failed for project=${project.name}: ${err?.message || err}`);
|
|
6220
|
+
}
|
|
6221
|
+
}
|
|
6222
|
+
|
|
6223
|
+
// Central file (no project context — normalization uses URL-only scope).
|
|
6224
|
+
const centralPath = path.join(MINIONS_DIR, 'pull-requests.json');
|
|
6225
|
+
try {
|
|
6226
|
+
const result = shared.collapseDuplicatePrRecords(centralPath, {});
|
|
6227
|
+
collapsed += result.collapsed || 0;
|
|
6228
|
+
scanned++;
|
|
6229
|
+
} catch (err) {
|
|
6230
|
+
log('warn', `collapseAllDuplicatePrRecords: failed for central file: ${err?.message || err}`);
|
|
6231
|
+
}
|
|
6232
|
+
|
|
6233
|
+
if (collapsed > 0) {
|
|
6234
|
+
log('info', `[pull-requests] collapseAllDuplicatePrRecords: collapsed ${collapsed} duplicate(s) across ${scanned} scope(s)`);
|
|
6235
|
+
}
|
|
6236
|
+
return { collapsed, scanned };
|
|
6237
|
+
}
|
|
6238
|
+
|
|
6159
6239
|
// M003 — After a coding WI completes successfully, auto-dispatch a live-validation
|
|
6160
6240
|
// WI when project.liveValidation.autoDispatch === true and the completed item is
|
|
6161
6241
|
// a coding WI (not the validation type itself). Skips if the coding WI has no PR.
|
|
@@ -6237,6 +6317,7 @@ module.exports = {
|
|
|
6237
6317
|
reconcilePrdStatuses,
|
|
6238
6318
|
syncPrsFromOutput,
|
|
6239
6319
|
pruneScopeMismatchDuplicatePrs,
|
|
6320
|
+
collapseAllDuplicatePrRecords,
|
|
6240
6321
|
updatePrAfterReview,
|
|
6241
6322
|
updatePrAfterFix,
|
|
6242
6323
|
updatePrAfterFixError,
|
|
@@ -6300,8 +6381,11 @@ module.exports = {
|
|
|
6300
6381
|
enrollPrFromCanonicalId,
|
|
6301
6382
|
_setEnrollmentGhRunnerForTest,
|
|
6302
6383
|
// W-mqtplpk6001oe6d5 — stamp PR ref + workItemId onto WI and PRD item at completion time.
|
|
6384
|
+
// W-mqvwzgi500259361 — exported for unit testing case-insensitive refs/heads/ stripping.
|
|
6385
|
+
normalizePrFixBranchName,
|
|
6303
6386
|
stampWiPrRef,
|
|
6304
6387
|
stampPrdItemWorkItemId,
|
|
6388
|
+
stampPrdItemPrUrl,
|
|
6305
6389
|
// M003 — auto-dispatch live-validation WI after coding WI completion.
|
|
6306
6390
|
autoDispatchLiveValidationWi,
|
|
6307
6391
|
};
|
package/engine/playbook.js
CHANGED
|
@@ -394,6 +394,11 @@ const PLAYBOOK_OPTIONAL_VARS = new Set([
|
|
|
394
394
|
// Both are optional; playbooks that don't reference them receive empty strings.
|
|
395
395
|
'from_agent',
|
|
396
396
|
'from_conclusion',
|
|
397
|
+
// P-mqyp0008v022w3x4 (SHERLOC) — prior explore WI resultSummary injected into
|
|
398
|
+
// fix dispatches when a completed explore WI references the fix WI. Empty string
|
|
399
|
+
// when no prior explore WI exists; the {{#prior_explore_context}} conditional
|
|
400
|
+
// in fix.md renders the section only when non-empty.
|
|
401
|
+
'prior_explore_context',
|
|
397
402
|
]);
|
|
398
403
|
|
|
399
404
|
const PLAYBOOK_REQUIRED_VARS = {
|
|
@@ -1011,11 +1016,11 @@ function renderPlaybook(type, vars) {
|
|
|
1011
1016
|
// ─── Playbook Section Validator ──────────────────────────────────────────────
|
|
1012
1017
|
|
|
1013
1018
|
// Required structural section patterns — warn (do not throw) when absent.
|
|
1014
|
-
//
|
|
1015
|
-
// production playbooks use those headers. Only '## Your Task' is universally
|
|
1016
|
-
// present — it is injected by renderPlaybook via the playbook template.
|
|
1019
|
+
// Pluralisation: /^## Tools?\b/m matches both '## Tool' and '## Tools'.
|
|
1017
1020
|
const _REQUIRED_PROMPT_SECTIONS = [
|
|
1018
|
-
{ pattern: /^## Your Task\b/m,
|
|
1021
|
+
{ pattern: /^## Your Task\b/m, label: '## Your Task' },
|
|
1022
|
+
{ pattern: /^## Tools?\b/m, label: '## Tools' },
|
|
1023
|
+
{ pattern: /^## Constraints\b/m, label: '## Constraints' },
|
|
1019
1024
|
];
|
|
1020
1025
|
|
|
1021
1026
|
/**
|