@yemi33/minions 0.1.1866 → 0.1.1868

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/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1868 (2026-05-11)
4
+
5
+ ### Other
6
+ - docs(shared): JSDoc + tests for getCanonicalPrId (#2339)
7
+
3
8
  ## 0.1.1866 (2026-05-11)
4
9
 
5
10
  ### Other
@@ -236,7 +236,9 @@ module.exports = {
236
236
  loadCooldowns,
237
237
  saveCooldowns,
238
238
  isOnCooldown,
239
+ normalizePrHeadForCooldown, // exported for testing
239
240
  getPrReviewHead,
241
+ getPrReviewCooldownBase, // exported for testing
240
242
  getPrReviewCooldownKey,
241
243
  clearLegacyPrReviewCooldown,
242
244
  setCooldown,
@@ -2929,9 +2929,40 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
2929
2929
  // Must run BEFORE updateWorkItemStatus(DONE) — otherwise _retryCount is deleted and retries never advance
2930
2930
  if (effectiveSuccess && type === WORK_TYPE.PLAN_TO_PRD && meta?.item?.id) {
2931
2931
  let prdFound = false;
2932
+ let resolvedPrdFilename = null;
2932
2933
  const expectedFile = meta.item._prdFilename;
2933
2934
  if (expectedFile) {
2934
- prdFound = fs.existsSync(path.join(PRD_DIR, expectedFile));
2935
+ // Inline `fs.existsSync(path.join(PRD_DIR, expectedFile))` is the literal
2936
+ // substring pinned by source-text regression test (test/unit.test.js#893).
2937
+ // Do not refactor into a temp variable without updating that assertion.
2938
+ if (fs.existsSync(path.join(PRD_DIR, expectedFile))) {
2939
+ // Defense-in-depth (W-mozkn1bb001j66fd): a near-simultaneous plan-to-prd
2940
+ // dispatch for a different plan may have clobbered our pinned filename.
2941
+ // Verify the file's source_plan matches our planFile before accepting it.
2942
+ // Without this check we'd mark the WI done against ANOTHER plan's PRD.
2943
+ if (meta.item.planFile) {
2944
+ try {
2945
+ const prd = safeJson(path.join(PRD_DIR, expectedFile));
2946
+ if (prd && prd.source_plan === meta.item.planFile) {
2947
+ prdFound = true;
2948
+ resolvedPrdFilename = expectedFile;
2949
+ } else {
2950
+ log('warn', `plan-to-prd ${meta.item.id}: file "${expectedFile}" exists but source_plan="${prd?.source_plan ?? '(unknown)'}" doesn't match expected planFile="${meta.item.planFile}" — possible collision, scanning by source_plan`);
2951
+ }
2952
+ } catch (e) {
2953
+ // Unparseable PRD file — log and accept (legacy behavior). A real
2954
+ // collision could hide behind a parse error, so the warn keeps the
2955
+ // anomaly visible for follow-up rather than failing silently.
2956
+ log('warn', `plan-to-prd ${meta.item.id}: file "${expectedFile}" exists but failed to parse (${e.message}) — accepting as legacy behavior; manual inspection recommended if this recurs`);
2957
+ prdFound = true;
2958
+ resolvedPrdFilename = expectedFile;
2959
+ }
2960
+ } else {
2961
+ // No planFile to verify against — fall back to legacy existence check.
2962
+ prdFound = true;
2963
+ resolvedPrdFilename = expectedFile;
2964
+ }
2965
+ }
2935
2966
  }
2936
2967
  if (!prdFound && meta.item.planFile) {
2937
2968
  try {
@@ -2939,11 +2970,38 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
2939
2970
  if (!f.endsWith('.json')) continue;
2940
2971
  try {
2941
2972
  const prd = safeJson(path.join(PRD_DIR, f));
2942
- if (prd && prd.source_plan === meta.item.planFile) { prdFound = true; break; }
2973
+ if (prd && prd.source_plan === meta.item.planFile) {
2974
+ prdFound = true;
2975
+ resolvedPrdFilename = f;
2976
+ break;
2977
+ }
2943
2978
  } catch {}
2944
2979
  }
2945
2980
  } catch {}
2946
2981
  }
2982
+ // Migrate _prdFilename when the source_plan scan found our PRD under a
2983
+ // different on-disk filename (e.g. agent bumped past a clobbered name, or
2984
+ // a previous tick pinned a stale name). Future readers (artifacts, dashboard
2985
+ // links) need to point at the actual file.
2986
+ if (prdFound && resolvedPrdFilename && resolvedPrdFilename !== expectedFile) {
2987
+ const wiPath = resolveWorkItemPath(meta);
2988
+ if (wiPath) {
2989
+ try {
2990
+ mutateJsonFileLocked(wiPath, data => {
2991
+ if (!Array.isArray(data)) return data;
2992
+ const w = data.find(i => i.id === meta.item.id);
2993
+ if (!w) return data;
2994
+ w._prdFilename = resolvedPrdFilename;
2995
+ if (w._artifacts && typeof w._artifacts === 'object') {
2996
+ w._artifacts.prd = resolvedPrdFilename;
2997
+ }
2998
+ return data;
2999
+ }, { skipWriteIfUnchanged: true });
3000
+ meta.item._prdFilename = resolvedPrdFilename;
3001
+ log('info', `plan-to-prd ${meta.item.id}: migrated _prdFilename "${expectedFile}" → "${resolvedPrdFilename}" (matched by source_plan, likely collision)`);
3002
+ } catch (err) { log('warn', `plan-to-prd _prdFilename migration: ${err.message}`); }
3003
+ }
3004
+ }
2947
3005
  if (!prdFound) {
2948
3006
  skipDoneStatus = true;
2949
3007
  const wiPath = resolveWorkItemPath(meta);
package/engine/meeting.js CHANGED
@@ -910,6 +910,19 @@ module.exports = {
910
910
  // getMeetings/discoverMeetingWork/collectMeetingFindings/checkMeetingTimeouts,
911
911
  // never these helpers directly.
912
912
  resolveMeetingNoteArtifactPath,
913
+ readMeetingNoteArtifact,
914
+ isTerminalMeetingStatus,
915
+ expectedMeetingStatusForRound,
916
+ roundKeyFor,
917
+ getRoundFailures,
918
+ hasRoundFailure,
919
+ hasRoundSuccess,
920
+ hasRoundTerminalOutcome,
921
+ isEmptyMeetingContent,
922
+ isSuccessfulStructuredCompletion,
923
+ getStructuredNoteArtifacts,
924
+ formatMeetingContributions,
925
+ stripMeetingSummaryMarkdown,
913
926
  cleanMeetingSummaryText,
914
927
  splitMeetingSummaryFragments,
915
928
  truncateMeetingSummary,
package/engine/shared.js CHANGED
@@ -2751,6 +2751,34 @@ function isPrCompatibleWithProject(project, prRef, url = '') {
2751
2751
  return !getPrProjectScopeMismatch(project, prRef, url);
2752
2752
  }
2753
2753
 
2754
+ /**
2755
+ * Build a canonical, repository-scoped PR identifier.
2756
+ *
2757
+ * Disambiguates PR numbers across hosts/repos by prefixing with the host scope
2758
+ * derived from `url` (if it parses as a known host PR URL) or otherwise from
2759
+ * the `project` config. The `url` always wins when present so that a PR
2760
+ * record carrying a foreign-host URL is not mis-scoped to its current
2761
+ * project.
2762
+ *
2763
+ * Output shapes:
2764
+ * - GitHub project: `github:<owner>/<repo>#<prNumber>` (e.g. `github:yemi33/minions#1234`)
2765
+ * - ADO project: `ado:<org>/<adoProject>/<repo>#<prNumber>` (e.g. `ado:office/office/Office#5134010`)
2766
+ * - No usable scope but numeric prRef: `PR-<prNumber>` (legacy fallback)
2767
+ * - Unparseable: returns the raw id string (may be empty)
2768
+ *
2769
+ * @param {object|null} project - Project config; uses `repoHost`, `adoOrg`,
2770
+ * `adoProject`, `repoName`, and `prUrlBase` to derive the scope. May be null
2771
+ * when only `url` provides scope.
2772
+ * .
2773
+ * @param {number|string|object} prRef - PR reference. Accepts a numeric id,
2774
+ * a numeric string, an existing canonical id (`github:owner/repo#42`),
2775
+ * a host PR URL, or a PR-record object with `id`/`url`/`prNumber` fields.
2776
+ * @param {string} [url=''] - Full PR URL used for host disambiguation
2777
+ * (primary signal for ADO vs GitHub when `prRef` is bare-numeric and the
2778
+ * project config disagrees with the URL host).
2779
+ * @returns {string} Canonical PR id string (see shapes above), or the raw id
2780
+ * when no scope and no number can be derived.
2781
+ */
2754
2782
  function getCanonicalPrId(project, prRef, url = '') {
2755
2783
  const isObjectRef = !!prRef && typeof prRef === 'object';
2756
2784
  const rawId = isObjectRef ? (prRef.id || '') : String(prRef || '');
package/engine.js CHANGED
@@ -3985,7 +3985,16 @@ function discoverCentralWorkItems(config) {
3985
3985
  }
3986
3986
  }
3987
3987
  if (!prdFilename) {
3988
- // Generate unique PRD filename — check prd/ and prd/archive/ for collisions
3988
+ // Generate unique PRD filename — check prd/, prd/archive/, AND any
3989
+ // _prdFilename already pinned by sibling work items (W-mozkn1bb001j66fd):
3990
+ // - on-disk PRDs (prd/ + prd/archive/)
3991
+ // - other items in the central work-items list whose _prdFilename was
3992
+ // pinned by a previous tick (their PRD may not be on disk yet if
3993
+ // the agent hasn't completed)
3994
+ // - other items being prepared this same tick (mutations Map)
3995
+ // Without these in-memory scopes two near-simultaneous plan-to-prd
3996
+ // dispatches both pinned the same filename and the second agent
3997
+ // silently overwrote the first PRD on disk.
3989
3998
  const prdBase = vars.project_filename_slug + '-' + dateStamp();
3990
3999
  prdFilename = prdBase + '.json';
3991
4000
  shared.sanitizePath(prdFilename, PRD_DIR);
@@ -3993,6 +4002,22 @@ function discoverCentralWorkItems(config) {
3993
4002
  ...prdFiles,
3994
4003
  ...safeReadDir(path.join(PRD_DIR, 'archive')).filter(f => f.endsWith('.json')),
3995
4004
  ]);
4005
+ for (const otherItem of items) {
4006
+ if (!otherItem || otherItem.id === item.id) continue;
4007
+ if (otherItem._prdFilename && otherItem.planFile !== item.planFile) {
4008
+ prdExisting.add(otherItem._prdFilename);
4009
+ }
4010
+ }
4011
+ for (const [otherId, m] of mutations) {
4012
+ if (otherId === item.id) continue;
4013
+ // ORDERING NOTE: this loop reads m._prdFilename from the mutations
4014
+ // Map; it works only because each item writes its pinned filename
4015
+ // into mutations BEFORE the next iteration of the outer item loop
4016
+ // reaches this code (see the mutations.set call ~5 lines below).
4017
+ // A future refactor that batches Map writes after the per-item
4018
+ // loop would silently break Layer 1 of the W-mozkn1bb001j66fd fix.
4019
+ if (m && m._prdFilename) prdExisting.add(m._prdFilename);
4020
+ }
3996
4021
  let prdCounter = 2;
3997
4022
  while (prdExisting.has(prdFilename)) { prdFilename = prdBase + '-' + prdCounter + '.json'; prdCounter++; }
3998
4023
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1866",
3
+ "version": "0.1.1868",
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"