@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.
@@ -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' });
@@ -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:
@@ -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
- `agentMemoryScope`, `mergeManifestAllowedTools`. Today the engine actively enforces (a) the
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. Currently exposed via `shared.agentMemoryScope(agent)` for callers that want to gate inbox writes; full enforcement at the consolidation layer is future work. |
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 | `shared.agentMemoryScope(agent)` is available for consolidation / playbook / inbox callers. Semantics: `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. |
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, agentMemoryScope,
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
@@ -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
- // prdReadsFromSql — Phase 10 step 3 read-flip. When ON, getPrdInfo sources its
82
- // PRD list (existingPrds / verifyPrsByPlan / allPrdItems) from the SQL mirror
83
- // (prds/prd_items/prd_verify_prs) after reconciling it from disk, instead of
84
- // scanning prd/*.json directly. JSON stays canonical and the dual-write keeps
85
- // SQL in sync; reconciliation catches PRDs written outside the chokepoint. The
86
- // two code paths are proven output-equivalent by db-phase10-read-flip.test.js.
87
- // Reversible: set `features.prdReadsFromSql: false` to fall back to the file
88
- // scan instantly. Temporary migration gate remove once the read-flip soaks.
89
- 'prdReadsFromSql': {
90
- description: 'Source the dashboard PRD/plan read (getPrdInfo) from the SQL mirror instead of scanning prd/*.json. Reversible; the two paths are output-equivalent (Phase 10 step 3).',
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.2090',
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,
@@ -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 && PLAN_TERMINAL_STATUSES.has(w.status)) return false;
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
  };
@@ -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
- // NOTE: '## Tools' / '## Constraints' are NOT included because none of the
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, label: '## Your Task' },
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
  /**