@yemi33/minions 0.1.2392 → 0.1.2394

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.
@@ -554,6 +554,8 @@ const QA_SESSION_TERMINAL_STATES = new Set(['done', 'failed', 'killed']);
554
554
  let _qaSessionsPollInterval = null;
555
555
  let _qaSessionsCache = [];
556
556
  let _qaRunnersCache = [];
557
+ // Polling replaces the cards every three seconds, so disclosure state lives outside the DOM.
558
+ const _qaExpandedFlows = new Set();
557
559
 
558
560
  function _stopQaSessionsPoll() {
559
561
  if (_qaSessionsPollInterval) {
@@ -706,6 +708,17 @@ function _qaAfterSessionsRender() {
706
708
  if (allTerminal) _stopQaSessionsPoll();
707
709
  }
708
710
 
711
+ function qaSessionFlowsToggle(details) {
712
+ if (!details) return;
713
+ const isOpen = details.open;
714
+ const summary = details.querySelector('.qa-session-flows-summary');
715
+ if (summary) summary.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
716
+ const id = details.getAttribute('data-session-id');
717
+ if (!id) return;
718
+ if (isOpen) _qaExpandedFlows.add(id);
719
+ else _qaExpandedFlows.delete(id);
720
+ }
721
+
709
722
  function _qaRenderSessionCard(s) {
710
723
  const id = s && s.id || '';
711
724
  const state = (s && s.state) || 'unknown';
@@ -749,7 +762,19 @@ function _qaRenderSessionCard(s) {
749
762
 
750
763
  let body = '';
751
764
  if (flowsShort) {
752
- body += '<div class="qa-session-flows"><strong>Flows:</strong> ' + escHtml(flowsShort) + '</div>';
765
+ const flowsOpen = _qaExpandedFlows.has(id);
766
+ body += '<details class="qa-session-flows"' + (flowsOpen ? ' open' : '') +
767
+ ' data-session-id="' + escHtml(id) + '" ontoggle="qaSessionFlowsToggle(this)">' +
768
+ '<summary class="qa-session-flows-summary" aria-expanded="' + (flowsOpen ? 'true' : 'false') + '">' +
769
+ '<strong class="qa-session-flows-label">Flows:</strong>' +
770
+ '<span class="qa-session-flows-preview">' + escHtml(flowsShort) + '</span>' +
771
+ '<span class="qa-session-flows-affordance">' +
772
+ '<span class="qa-session-flows-expand-label">Show full prompt</span>' +
773
+ '<span class="qa-session-flows-collapse-label">Hide full prompt</span>' +
774
+ '</span>' +
775
+ '</summary>' +
776
+ '<div class="qa-session-flows-full">' + escHtml(flowsRaw) + '</div>' +
777
+ '</details>';
753
778
  }
754
779
  if (state === 'failed' && (failureClass || error)) {
755
780
  body += '<div class="qa-session-error"><strong>' + escHtml(failureClass || 'failed') + ':</strong> ' + escHtml(error || summary || 'no details') + '</div>';
@@ -1597,6 +1597,31 @@
1597
1597
  background: var(--surface); padding: var(--space-3);
1598
1598
  border-radius: var(--radius-sm); border: 1px solid var(--border);
1599
1599
  }
1600
+ .qa-session-flows-summary {
1601
+ display: grid; grid-template-columns: auto minmax(0, 1fr) auto;
1602
+ align-items: start; gap: var(--space-2); cursor: pointer; list-style: none;
1603
+ }
1604
+ .qa-session-flows-summary::-webkit-details-marker { display: none; }
1605
+ .qa-session-flows-summary::marker { content: ''; }
1606
+ .qa-session-flows-summary:focus-visible {
1607
+ outline: 2px solid var(--blue); outline-offset: 3px;
1608
+ border-radius: var(--radius-sm);
1609
+ }
1610
+ .qa-session-flows-preview,
1611
+ .qa-session-flows-full { overflow-wrap: anywhere; }
1612
+ .qa-session-flows-affordance {
1613
+ color: var(--blue); font-size: var(--text-xs); white-space: nowrap;
1614
+ }
1615
+ .qa-session-flows-affordance::before { content: '+ '; }
1616
+ .qa-session-flows-collapse-label { display: none; }
1617
+ .qa-session-flows[open] .qa-session-flows-preview { display: none; }
1618
+ .qa-session-flows[open] .qa-session-flows-affordance::before { content: '- '; }
1619
+ .qa-session-flows[open] .qa-session-flows-expand-label { display: none; }
1620
+ .qa-session-flows[open] .qa-session-flows-collapse-label { display: inline; }
1621
+ .qa-session-flows-full {
1622
+ margin-top: var(--space-3); padding-top: var(--space-3);
1623
+ border-top: 1px solid var(--border); white-space: pre-wrap;
1624
+ }
1600
1625
  .qa-session-error {
1601
1626
  font-size: var(--text-base); color: var(--red);
1602
1627
  background: rgba(248, 81, 73, 0.08); padding: var(--space-3);
@@ -51,7 +51,7 @@ Independent of the master kill-switches, each PR tracks repeated no-op fix outco
51
51
  2. Click the red chip → `POST /api/pull-requests/clear-paused-cause` with `{ prId, cause }`.
52
52
  3. Direct API call to the same endpoint.
53
53
 
54
- This per-PR per-cause pause is unrelated to `autoFixPaused`. Cause keys are validated against `shared.PR_FIX_CAUSE` (e.g. `BUILD_FAILURE`, `REVIEW_FEEDBACK`, `HUMAN_FEEDBACK`, `MERGE_CONFLICT`, `PR_FIX`). Full mechanics in [`docs/pr-review-fix-loop.md`](pr-review-fix-loop.md) §4E.
54
+ This per-PR per-cause pause is unrelated to `autoFixPaused`. Cause values are validated against `Object.values(shared.PR_FIX_CAUSE)` (`human-feedback`, `review-feedback`, `build-failure`, `merge-conflict`, or `pr-fix`). Full mechanics in [`docs/pr-review-fix-loop.md`](pr-review-fix-loop.md) §4E.
55
55
 
56
56
  ## Key files
57
57
 
@@ -2629,7 +2629,7 @@ function normalizePrFixBranchName(branch) {
2629
2629
  }
2630
2630
 
2631
2631
  function getPrFixBaselineHead(pr) {
2632
- return String(pr?.headSha || pr?._adoSourceCommit || pr?._adoHeadCommit || '').trim();
2632
+ return String(pr?.headRefOid || pr?.headSha || pr?._adoSourceCommit || pr?._adoHeadCommit || '').trim();
2633
2633
  }
2634
2634
 
2635
2635
  function findPrFixWorktree(meta, project, config) {
@@ -5962,7 +5962,14 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
5962
5962
  }
5963
5963
  if (effectiveSuccess && !skipDoneStatus) {
5964
5964
  // Coding WIs and direct PR-sourced fix dispatches share this follow-up path.
5965
- try { autoDispatchLiveValidationWi(meta, config, dispatchItem); } catch (err) { log('warn', `autoDispatchLiveValidationWi: ${err.message}`); }
5965
+ const verifiedHeadSha = prFixBranchChange?.evidence === 'remote-head'
5966
+ ? prFixBranchChange.afterHead
5967
+ : '';
5968
+ try {
5969
+ autoDispatchLiveValidationWi(meta, config, dispatchItem, { verifiedHeadSha });
5970
+ } catch (err) {
5971
+ log('warn', `autoDispatchLiveValidationWi: ${err.message}`);
5972
+ }
5966
5973
  }
5967
5974
  // Failure retry is handled by completeDispatch in dispatch.js — not duplicated here.
5968
5975
  // Only clear _decomposing flag on failure so decompose items don't get permanently stuck.
@@ -6943,9 +6950,9 @@ function collapseAllDuplicatePrRecords(config) {
6943
6950
  // always a canonical `test` WI with the PR-focused `build-and-test` playbook;
6944
6951
  // liveValidation.type controls routing, not the semantic type of the task.
6945
6952
  //
6946
- // Deduplicates per codingWiId: any non-terminal WI with
6947
- // meta.liveValidationFor === codingWiId blocks a second dispatch, so only one
6948
- // validation task is ever in flight per coding completion.
6953
+ // Deduplicates both per codingWiId and per canonical PR source revision. This
6954
+ // keeps concurrent/repeated fixes for one unchanged PR head from queueing
6955
+ // duplicate validations while allowing a fresh validation after the head moves.
6949
6956
  //
6950
6957
  // W-mr9u39az000db5c2 — guard against a self-referential recursive cascade.
6951
6958
  // When liveValidation.type overlaps with a coding WI's own type set (e.g.
@@ -6970,7 +6977,7 @@ const ELIGIBLE_LIVE_VALIDATION_TYPES = new Set([
6970
6977
  WORK_TYPE.BUILD_FIX_COMPLEX,
6971
6978
  ]);
6972
6979
 
6973
- function autoDispatchLiveValidationWi(meta, config, dispatchItem = null) {
6980
+ function autoDispatchLiveValidationWi(meta, config, dispatchItem = null, options = {}) {
6974
6981
  const item = meta?.item || null;
6975
6982
  const sourceType = item?.type || dispatchItem?.type || null;
6976
6983
  const sourceKey = item?.id || dispatchItem?.id || null;
@@ -7033,27 +7040,48 @@ function autoDispatchLiveValidationWi(meta, config, dispatchItem = null) {
7033
7040
  const prTarget = prUrl || (prId != null ? String(prId) : null);
7034
7041
  if (!prTarget) return;
7035
7042
 
7043
+ const prIdentityRef = {
7044
+ id: prId != null ? String(prId) : prTarget,
7045
+ url: prUrl || '',
7046
+ };
7047
+ let currentPr = typeof prRef === 'object' ? prRef : null;
7048
+ try {
7049
+ currentPr = shared.findPrRecord(shared.readPullRequests(project), prIdentityRef, project) || currentPr;
7050
+ } catch (err) {
7051
+ log('warn', `liveValidation: could not resolve tracked PR ${prTarget}: ${err.message}`);
7052
+ }
7053
+ const liveValidationPrId = shared.getPrIdentityKey(currentPr || prIdentityRef, project);
7054
+ if (!liveValidationPrId) return;
7055
+ const liveValidationHeadSha = String(options?.verifiedHeadSha || '').trim()
7056
+ || getPrFixBaselineHead(currentPr);
7057
+
7036
7058
  const codingWiId = sourceKey;
7037
7059
  const wiScope = item ? resolveWorkItemScope(meta) : projectName;
7038
7060
  if (!wiScope) return;
7039
7061
 
7040
7062
  try {
7041
- mutateWorkItems(wiScope, items => {
7063
+ mutateWorkItems(wiScope, (items, comparisonItems) => {
7042
7064
  if (!Array.isArray(items)) return items;
7043
7065
 
7044
- // Dedup: only one validation WI in flight per coding WI. Any non-terminal
7045
- // WI already tracking this coding WI (meta.liveValidationFor) blocks a
7046
- // second dispatch, regardless of its validation type. (Back-compat: WIs
7047
- // created before meta.liveValidationType existed are matched too, since
7048
- // we key solely on liveValidationFor.)
7049
- const existing = items.find(i =>
7050
- i &&
7051
- i.meta &&
7052
- i.meta.liveValidationFor === codingWiId &&
7053
- !PLAN_TERMINAL_STATUSES.has(i.status)
7054
- );
7066
+ // Keep the source-WI guard and revision guard in this same mutation as the
7067
+ // insert, and compare both supported scopes while the SQLite write
7068
+ // transaction is held. Legacy live-validation WIs predate the persisted
7069
+ // revision fields, so derive their identity from structured PR fields and
7070
+ // conservatively treat a missing head as matching until terminal.
7071
+ const existing = comparisonItems.find(i => {
7072
+ if (!i?.meta?.liveValidationFor || PLAN_TERMINAL_STATUSES.has(i.status)) return false;
7073
+ if (i.meta.liveValidationFor === codingWiId) return true;
7074
+
7075
+ const existingPrRef = i.meta.liveValidationPrId || shared.extractStructuredWorkItemPrRef(i);
7076
+ if (!existingPrRef) return false;
7077
+ const existingPrId = shared.getPrIdentityKey(existingPrRef, project);
7078
+ if (existingPrId !== liveValidationPrId) return false;
7079
+
7080
+ const existingHeadSha = String(i.meta.liveValidationHeadSha || '').trim();
7081
+ return !existingHeadSha || !liveValidationHeadSha || existingHeadSha === liveValidationHeadSha;
7082
+ });
7055
7083
  if (existing) {
7056
- log('info', `liveValidation: dedup — non-terminal validation WI ${existing.id} already exists for ${codingWiId}`);
7084
+ log('info', `liveValidation: dedup — non-terminal validation WI ${existing.id} blocks another validation for ${codingWiId} (${liveValidationPrId}@${liveValidationHeadSha || 'unknown'})`);
7057
7085
  return items;
7058
7086
  }
7059
7087
 
@@ -7081,6 +7109,8 @@ function autoDispatchLiveValidationWi(meta, config, dispatchItem = null) {
7081
7109
  meta: {
7082
7110
  liveValidationFor: codingWiId,
7083
7111
  liveValidationType: validationType,
7112
+ liveValidationPrId,
7113
+ ...(liveValidationHeadSha ? { liveValidationHeadSha } : {}),
7084
7114
  playbook: 'build-and-test',
7085
7115
  ...(dispatchItem?.id ? { sourceDispatchId: dispatchItem.id } : {}),
7086
7116
  },
@@ -7093,7 +7123,7 @@ function autoDispatchLiveValidationWi(meta, config, dispatchItem = null) {
7093
7123
  items.push(validationWi);
7094
7124
  log('info', `liveValidation: auto-dispatched validation WI ${validationWi.id} (type=${validationType}) for coding WI ${codingWiId}`);
7095
7125
  return items;
7096
- });
7126
+ }, { readScopes: ['central', projectName] });
7097
7127
  } catch (err) {
7098
7128
  log('warn', `autoDispatchLiveValidationWi for ${codingWiId}: ${err.message}`);
7099
7129
  }
package/engine/shared.js CHANGED
@@ -7702,12 +7702,17 @@ function readWorkItems(source = null) {
7702
7702
  return require('./work-items-store').readWorkItemsForScope(stateScope(source));
7703
7703
  }
7704
7704
 
7705
- function mutateWorkItems(source, mutator) {
7705
+ function mutateWorkItems(source, mutator, opts = {}) {
7706
7706
  const store = require('./work-items-store');
7707
- const { wrote, result } = store.applyWorkItemsMutation(stateScope(source), (items) => {
7707
+ const scope = stateScope(source);
7708
+ const normalizedOpts = { ...opts };
7709
+ if (Array.isArray(opts.readScopes)) {
7710
+ normalizedOpts.readScopes = opts.readScopes.map(stateScope);
7711
+ }
7712
+ const { wrote, result } = store.applyWorkItemsMutation(scope, (items, comparisonItems) => {
7708
7713
  if (!Array.isArray(items)) items = [];
7709
- return mutator(items) || items;
7710
- });
7714
+ return mutator(items, comparisonItems) || items;
7715
+ }, normalizedOpts);
7711
7716
  if (wrote) {
7712
7717
  try { require('./db-events').emitStateEvent('work_items'); } catch { /* optional */ }
7713
7718
  try { require('./queries').invalidateWorkItemsCache(); } catch { /* queries not loaded */ }
@@ -128,13 +128,14 @@ function _applyWorkItemsDiff(db, diff) {
128
128
 
129
129
  // Apply a mutation to the work-items array for a given scope. Wrapped in
130
130
  // a single transaction so the diff and apply can't race against a
131
- // concurrent reader/writer.
131
+ // concurrent reader/writer. Optional readScopes are exposed to the mutator
132
+ // as a combined comparison set; only target-scope mutations are persisted.
132
133
  //
133
134
  // Returns { wrote, result }:
134
135
  // wrote — true iff at least one INSERT/UPDATE/DELETE landed
135
136
  // result — the post-mutation array (legacy return shape of
136
137
  // mutateJsonFileLocked)
137
- function applyWorkItemsMutation(scope, mutator, opts) {
138
+ function applyWorkItemsMutation(scope, mutator, opts = {}) {
138
139
  const { getDb, withTransaction } = require('./db');
139
140
  const db = getDb();
140
141
 
@@ -144,8 +145,14 @@ function applyWorkItemsMutation(scope, mutator, opts) {
144
145
  // added by readAllWorkItems only and would round-trip as garbage into
145
146
  // the data column otherwise.
146
147
  for (const wi of before) { if (wi && wi._source) delete wi._source; }
148
+ const comparisonItems = [...before];
149
+ const readScopes = Array.isArray(opts.readScopes) ? opts.readScopes : [];
150
+ for (const readScope of new Set(readScopes)) {
151
+ if (readScope === scope) continue;
152
+ comparisonItems.push(...readWorkItemsForScope(readScope));
153
+ }
147
154
  const beforeSnapshot = JSON.parse(JSON.stringify(before));
148
- const next = mutator(before);
155
+ const next = mutator(before, comparisonItems);
149
156
  const after = (next === undefined || next === null)
150
157
  ? before
151
158
  : (Array.isArray(next) ? next : before);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2392",
3
+ "version": "0.1.2394",
4
4
  "description": "Multi-agent AI dev team that runs from ~/.minions/ — five autonomous agents share a single engine, dashboard, and knowledge base",
5
5
  "bin": {
6
6
  "minions": "bin/minions.js"
@@ -135,7 +135,7 @@ Rules for items:
135
135
  { "id": "P-bbb2", "name": "UI client", "project": "web-dashboard", ... }
136
136
  ]
137
137
  ```
138
- The engine routes each item to that project's SQL work-item scope. The verify task at plan completion picks the project with the most completed items and runs cross-repo verification from there. See the **Cross-repo plans** section below for the full output contract when `{{target_projects}}` is set.
138
+ The engine routes each item to that project's SQL work-item scope. At plan completion, the engine creates one project-scoped verify work item per touched project (each project with an active PR linked to a completed plan item). If no linked PRs surface, it falls back to one verify work item on the primary project. See the **Cross-repo plans** section below for the full output contract when `{{target_projects}}` is set.
139
139
  - `depends_on` lists IDs of items that must be done first
140
140
  - Keep descriptions actionable — name the files, functions, patterns, or integration points the implementing agent should touch whenever the plan makes them clear
141
141
  - Include `acceptance_criteria` so reviewers know when it's done
@@ -161,7 +161,7 @@ Apply these rules on top of (and where they conflict, instead of) the single-pro
161
161
  - `parallel`: omit `feature_branch` entirely (the engine derives per-item branches as `user/<loginname>/<wi-id>-<slug>` in each project).
162
162
  - `shared-branch` (only with explicit justification): use the canonical `user/<loginname>/PL-<short-kebab-slug>` form. The engine will pre-create a matching branch in every project listed in `{{target_projects}}` (P-1c0f5e84) so each item can push to the same branch name in its own repo.
163
163
 
164
- The verify task at plan completion picks the project with the most completed items and runs cross-repo verification from there; items with no PRs (e.g. docs-only changes that landed via direct commit) are still counted via their `done` status.
164
+ At plan completion, the engine creates one verify work item per touched project, where a touched project has at least one active PR linked to a completed plan item. Those project-scoped verify tasks run independently in parallel. If no linked PRs surface (for example, items completed without PR records or legacy state), the fan-out collapses to one verify work item on the primary project — the project with the most completed items — as a fallback.
165
165
 
166
166
  {{/target_projects}}
167
167
  ## Reusing an Existing PRD