@yemi33/minions 0.1.2360 → 0.1.2362

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 CHANGED
@@ -1373,6 +1373,11 @@ function linkPullRequestForTracking({ url, title, project: projectName, contextO
1373
1373
  }, {
1374
1374
  project: targetProject,
1375
1375
  itemId: linkedWorkItemId,
1376
+ // W-mrdutzdr000t22f4 — a POST /api/pull-requests/link call is deliberate
1377
+ // user intent to (re)track this PR. Unlike passive re-enrollment paths
1378
+ // (pollers, enrollPrFromCanonicalId), this call site should clear a
1379
+ // tombstone left by a prior explicit delete rather than silently no-op.
1380
+ allowResurrect: true,
1376
1381
  });
1377
1382
  return { ...result, prPath, targetProject, projectResolution, prNum };
1378
1383
  }
@@ -13844,16 +13849,25 @@ What would you like to discuss or change? When you're happy, say "approve" and I
13844
13849
  } catch (e) {
13845
13850
  return jsonReply(res, e.statusCode || 400, { error: e.message }, req);
13846
13851
  }
13847
- const { id: prId, prPath, prNum, created, linked, targetProject, projectResolution } = linkResult;
13852
+ const { id: prId, prPath, prNum, created, linked, targetProject, projectResolution, skipped, resurrected } = linkResult;
13848
13853
  invalidateStatusCache();
13854
+ // W-mrdutzdr000t22f4 — upsertPullRequestRecord can leave the record
13855
+ // untouched (`skipped: true`) for reasons other than a resurrect, e.g.
13856
+ // a legitimate cross-scope tombstone match it still refuses to touch.
13857
+ // Surface that instead of always claiming success so a caller relying
13858
+ // on "PR linked." isn't misled into thinking the record changed.
13849
13859
  jsonReply(res, 200, {
13850
13860
  ok: true,
13851
13861
  id: prId,
13852
13862
  created,
13853
13863
  linked,
13864
+ skipped: skipped === true,
13865
+ resurrected: resurrected === true,
13854
13866
  project: targetProject?.name || 'central',
13855
13867
  projectResolution,
13856
- message: projectResolution?.message || 'PR linked.',
13868
+ message: skipped
13869
+ ? 'PR was not linked: an existing tracked record blocked the update.'
13870
+ : (projectResolution?.message || 'PR linked.'),
13857
13871
  });
13858
13872
 
13859
13873
  // Async-enrich: fetch title, description, branch, author from GitHub/ADO API
@@ -1163,18 +1163,33 @@ async function discoverPipelineWork(config) {
1163
1163
  }).join('\n\n');
1164
1164
  }
1165
1165
 
1166
- // Scan for inbox/archive notes created by this stage's agents
1166
+ // Scan for inbox/archive notes created by this stage's agents.
1167
+ // stageWiIds are scoped to this run (work-item ids embed the unique
1168
+ // run id), so matching on them alone is safe. stage.id/pipeline.id
1169
+ // are STATIC across every run of a recurring pipeline, so a bare
1170
+ // substring match on them would match every historical note ever
1171
+ // written for that stage/pipeline name (W-mrcr6do0001404e6). Keep
1172
+ // that fallback only as a safety net for notes that don't embed a
1173
+ // WI id, and gate it on mtime >= this run's startedAt so it can
1174
+ // never pick up notes older than the current run.
1167
1175
  try {
1168
1176
  const notesDirs = [
1169
1177
  NOTES_INBOX_DIR,
1170
1178
  NOTES_ARCHIVE_DIR,
1171
1179
  ];
1172
1180
  const stageWiIds = stageState.artifacts?.workItems || [];
1181
+ const runStartedAt = activeRun.startedAt ? new Date(activeRun.startedAt).getTime() : 0;
1173
1182
  const notes = [];
1174
1183
  for (const dir of notesDirs) {
1175
1184
  for (const f of safeReadDir(dir).filter(n => n.endsWith('.md'))) {
1176
- if (stageWiIds.some(id => f.includes(id)) || f.includes(stage.id) || f.includes(pipeline.id)) {
1185
+ if (stageWiIds.some(id => f.includes(id))) {
1177
1186
  notes.push(f);
1187
+ continue;
1188
+ }
1189
+ if (f.includes(stage.id) || f.includes(pipeline.id)) {
1190
+ let mtime = 0;
1191
+ try { mtime = fs.statSync(path.join(dir, f)).mtimeMs; } catch { /* file may have vanished */ }
1192
+ if (mtime >= runStartedAt) notes.push(f);
1178
1193
  }
1179
1194
  }
1180
1195
  }
package/engine/shared.js CHANGED
@@ -7873,7 +7873,7 @@ function collapseDuplicatePrRecords(prPath, { project = null } = {}) {
7873
7873
  * manually records a PR for a work item must use this helper so the PR record
7874
7874
  * and the canonical work-item attachment are created together and idempotently.
7875
7875
  */
7876
- function upsertPullRequestRecord(prPath, entry, { project = null, itemId = null, itemIds = null, beforeInsert = null } = {}) {
7876
+ function upsertPullRequestRecord(prPath, entry, { project = null, itemId = null, itemIds = null, beforeInsert = null, allowResurrect = false } = {}) {
7877
7877
  if (!prPath) throw new Error('prPath required');
7878
7878
  if (!entry || typeof entry !== 'object') throw new Error('entry required');
7879
7879
 
@@ -7906,6 +7906,7 @@ function upsertPullRequestRecord(prPath, entry, { project = null, itemId = null,
7906
7906
  let created = false;
7907
7907
  let linked = false;
7908
7908
  let skipped = false;
7909
+ let resurrected = false;
7909
7910
  let record = null;
7910
7911
 
7911
7912
  mutatePullRequests(prPath, (prs) => {
@@ -7914,9 +7915,20 @@ function upsertPullRequestRecord(prPath, entry, { project = null, itemId = null,
7914
7915
  // Issue #384: tombstoned records (userDeleted:true) must not be re-activated
7915
7916
  // by any poller path. Return skipped so reconcilePrs / shared-branch-reconcile
7916
7917
  // cannot resurrect a PR the user explicitly removed.
7918
+ //
7919
+ // W-mrdutzdr000t22f4 — passive re-enrollment (pollers, enrollPrFromCanonicalId)
7920
+ // must keep respecting the tombstone, but a deliberate user-initiated relink
7921
+ // via POST /api/pull-requests/link is explicit intent to bring the PR back.
7922
+ // Callers that want that behavior opt in with `allowResurrect: true`, which
7923
+ // clears the tombstone here and lets the normal field-merge logic below run.
7917
7924
  if (target && target.userDeleted === true) {
7918
- skipped = true;
7919
- return prs;
7925
+ if (!allowResurrect) {
7926
+ skipped = true;
7927
+ return prs;
7928
+ }
7929
+ delete target.userDeleted;
7930
+ delete target.userDeletedAt;
7931
+ resurrected = true;
7920
7932
  }
7921
7933
  // P-e9f0a2b4 — When findPrRecord returns null but multiple records share the
7922
7934
  // same prNumber (e.g. cross-scope contamination or a project-config change
@@ -7980,7 +7992,7 @@ function upsertPullRequestRecord(prPath, entry, { project = null, itemId = null,
7980
7992
  }
7981
7993
  }
7982
7994
 
7983
- return { id: canonicalId, prNumber, created, linked, skipped, record };
7995
+ return { id: canonicalId, prNumber, created, linked, skipped, resurrected, record };
7984
7996
  }
7985
7997
 
7986
7998
  // ─── PR Reference → URL Derivation ───────────────────────────────────────────
package/engine.js CHANGED
@@ -6569,23 +6569,33 @@ function restoreHijackedOperatorCheckouts(config) {
6569
6569
  if (!localPath || !fs.existsSync(localPath)) continue;
6570
6570
  if (shared.resolveCheckoutMode(project) === shared.CHECKOUT_MODES.LIVE) continue; // live-mode: feature branch is intentional
6571
6571
  const main = project.mainBranch || 'main';
6572
- if (String(execSilent('git rev-parse --is-inside-work-tree', { cwd: localPath }) || '').trim() !== 'true') continue;
6573
- const cur = String(execSilent('git rev-parse --abbrev-ref HEAD', { cwd: localPath }) || '').trim();
6572
+ const opts = { cwd: localPath };
6573
+ // W-mrcvh5hw000o6cf1: use the argv-form (shell: false) git helper instead of
6574
+ // execSilent(). execSilent() shells out via cmd.exe, which cannot chdir into
6575
+ // a UNC path (`//wsl.localhost/...`, `\\server\share\...`) — it silently
6576
+ // falls back to the Windows directory and every git call then fails with
6577
+ // "fatal: not a git repository", every tick, forever, for WSL/UNC-hosted
6578
+ // projects. shellSafeGitSync spawns git.exe directly (execFileSync,
6579
+ // shell:false) so `cwd` is honored even for UNC paths.
6580
+ let isRepo;
6581
+ try { isRepo = String(shared.shellSafeGitSync(['rev-parse', '--is-inside-work-tree'], opts) || '').trim(); }
6582
+ catch { continue; } // not a git repo (or transiently unreachable) — nothing to restore, skip quietly
6583
+ if (isRepo !== 'true') continue;
6584
+ const cur = String(shared.shellSafeGitSync(['rev-parse', '--abbrev-ref', 'HEAD'], opts) || '').trim();
6574
6585
  if (!cur || cur === 'HEAD' || cur === main) continue; // on main or detached — fine
6575
- const gitDir = String(execSilent('git rev-parse --git-dir', { cwd: localPath }) || '').trim();
6586
+ const gitDir = String(shared.shellSafeGitSync(['rev-parse', '--git-dir'], opts) || '').trim();
6576
6587
  const absGitDir = gitDir ? (path.isAbsolute(gitDir) ? gitDir : path.join(localPath, gitDir)) : path.join(localPath, '.git');
6577
6588
  const mergeStuck = fs.existsSync(path.join(absGitDir, 'MERGE_HEAD'));
6578
6589
  const rebaseStuck = fs.existsSync(path.join(absGitDir, 'rebase-merge')) || fs.existsSync(path.join(absGitDir, 'rebase-apply'));
6579
6590
  if (!mergeStuck && !rebaseStuck && !_FLEET_BRANCH_RE.test(cur)) continue; // likely a human's deliberate checkout — leave it
6580
6591
  const why = mergeStuck ? ' (stuck merge)' : rebaseStuck ? ' (stuck rebase)' : ' (fleet branch in live checkout)';
6581
6592
  log('warn', `Operator checkout for ${project.name} is on '${cur}'${why}; worktree-mode requires ${main} — restoring`);
6582
- const opts = { cwd: localPath };
6583
- if (mergeStuck) execSilent('git merge --abort', opts);
6584
- if (rebaseStuck) execSilent('git rebase --abort', opts);
6585
- execSilent('git reset --hard HEAD', opts); // drop uncommitted hijack state; branch commits stay on the ref
6593
+ if (mergeStuck) shared.shellSafeGitSync(['merge', '--abort'], opts);
6594
+ if (rebaseStuck) shared.shellSafeGitSync(['rebase', '--abort'], opts);
6595
+ shared.shellSafeGitSync(['reset', '--hard', 'HEAD'], opts); // drop uncommitted hijack state; branch commits stay on the ref
6586
6596
  try { shared.validateGitRef(main); } catch (ve) { throw new Error(`restoreHijackedOperatorCheckouts: invalid mainBranch ${JSON.stringify(main)}: ${ve.message}`); }
6587
6597
  shared.shellSafeGitSync(['checkout', main], opts);
6588
- const now = String(execSilent('git rev-parse --abbrev-ref HEAD', { cwd: localPath }) || '').trim();
6598
+ const now = String(shared.shellSafeGitSync(['rev-parse', '--abbrev-ref', 'HEAD'], opts) || '').trim();
6589
6599
  if (now === main) {
6590
6600
  try {
6591
6601
  writeInboxAlert(`operator-checkout-restored-${project.name}`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2360",
3
+ "version": "0.1.2362",
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"