@yemi33/minions 0.1.333 → 0.1.335

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,6 +1,14 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.333 (2026-04-03)
3
+ ## 0.1.335 (2026-04-03)
4
+
5
+ ### Fixes
6
+ - clear stale buildStatus when PRs are merged or abandoned
7
+
8
+ ## 0.1.334 (2026-04-03)
9
+
10
+ ### Features
11
+ - Fix PR write races in ado.js and github.js
4
12
 
5
13
  ### Fixes
6
14
  - guarantee test cleanup via finally block in test harness
package/engine/ado.js CHANGED
@@ -7,6 +7,7 @@ const path = require('path');
7
7
  const shared = require('./shared');
8
8
  const { exec, getAdoOrgBase, addPrLink, log, ts, dateStamp, PR_STATUS } = shared;
9
9
  const { getPrs } = require('./queries');
10
+ const { mutateJsonFileLocked } = shared;
10
11
 
11
12
  // Lazy require to avoid circular dependency — only needed for engine().handlePostMerge
12
13
  let _engine = null;
@@ -99,7 +100,15 @@ async function forEachActivePr(config, token, callback) {
99
100
  }
100
101
 
101
102
  if (projectUpdated > 0) {
102
- shared.safeWrite(shared.projectPrPath(project), prs);
103
+ mutateJsonFileLocked(shared.projectPrPath(project), (currentPrs) => {
104
+ // Merge updated PRs into the locked copy by ID
105
+ for (const updatedPr of prs) {
106
+ const idx = currentPrs.findIndex(p => p.id === updatedPr.id);
107
+ if (idx >= 0) currentPrs[idx] = updatedPr;
108
+ else currentPrs.push(updatedPr);
109
+ }
110
+ return currentPrs;
111
+ }, { defaultValue: [] });
103
112
  totalUpdated += projectUpdated;
104
113
  }
105
114
  }
@@ -137,6 +146,12 @@ async function pollPrStatus(config) {
137
146
  pr.reviewStatus = newStatus === PR_STATUS.MERGED ? 'approved' : 'pending';
138
147
  log('info', `PR ${pr.id} reviewStatus: waiting → ${pr.reviewStatus} (${newStatus})`);
139
148
  }
149
+ // Clear stale build status — checks won't be polled after close
150
+ if (pr.buildStatus && pr.buildStatus !== 'none') {
151
+ delete pr.buildStatus;
152
+ delete pr.buildFailReason;
153
+ delete pr._buildFailNotified;
154
+ }
140
155
  await engine().handlePostMerge(pr, project, config, newStatus);
141
156
  }
142
157
  }
@@ -174,11 +189,11 @@ async function pollPrStatus(config) {
174
189
  if (authorId) {
175
190
  try {
176
191
  const metricsPath = path.join(__dirname, 'metrics.json');
177
- const metrics = shared.safeJson(metricsPath) || {};
178
- if (!metrics[authorId]) metrics[authorId] = {};
179
- if (newReviewStatus === 'approved') metrics[authorId].prsApproved = (metrics[authorId].prsApproved || 0) + 1;
180
- else metrics[authorId].prsRejected = (metrics[authorId].prsRejected || 0) + 1;
181
- shared.safeWrite(metricsPath, metrics);
192
+ mutateJsonFileLocked(metricsPath, (metrics) => {
193
+ if (!metrics[authorId]) metrics[authorId] = {};
194
+ if (newReviewStatus === 'approved') metrics[authorId].prsApproved = (metrics[authorId].prsApproved || 0) + 1;
195
+ else metrics[authorId].prsRejected = (metrics[authorId].prsRejected || 0) + 1;
196
+ });
182
197
  } catch (err) { log('warn', `Metrics update: ${err.message}`); }
183
198
  }
184
199
  }
@@ -196,8 +211,8 @@ async function pollPrStatus(config) {
196
211
 
197
212
  const buildStatuses = [...latest.values()].filter(s => {
198
213
  const ctx = ((s.context?.genre || '') + '/' + (s.context?.name || '')).toLowerCase();
199
- return ctx.includes('codecoverage') || ctx.includes('build') ||
200
- ctx.includes('deploy') || ctx.includes('ci/');
214
+ return /\bcodecoverage\b/.test(ctx) || /\bbuild\b/.test(ctx) ||
215
+ /\bdeploy\b/.test(ctx) || /(?:^|\/)ci(?:\/|$)/.test(ctx);
201
216
  });
202
217
 
203
218
  let buildStatus = 'none';
@@ -248,7 +263,8 @@ async function pollPrHumanComments(config) {
248
263
  const threadsData = await adoFetch(threadsUrl, token);
249
264
  const threads = threadsData.value || [];
250
265
 
251
- const cutoff = pr.humanFeedback?.lastProcessedCommentDate || pr.created || '1970-01-01';
266
+ const cutoffStr = pr.humanFeedback?.lastProcessedCommentDate || pr.created || '1970-01-01';
267
+ const cutoffMs = new Date(cutoffStr).getTime() || 0;
252
268
 
253
269
  // Collect ALL human comments on the PR for full context
254
270
  const allHumanComments = [];
@@ -269,7 +285,8 @@ async function pollPrHumanComments(config) {
269
285
  allHumanComments.push(entry);
270
286
 
271
287
  // Track which comments are new (for triggering — any new comment triggers a fix)
272
- if (comment.publishedDate && comment.publishedDate > cutoff) {
288
+ const commentMs = comment.publishedDate ? new Date(comment.publishedDate).getTime() : 0;
289
+ if (commentMs && commentMs > cutoffMs) {
273
290
  newHumanComments.push(entry);
274
291
  }
275
292
  }
@@ -285,7 +302,7 @@ async function pollPrHumanComments(config) {
285
302
  // Provide ALL comments as context — the agent needs full thread context to fix properly
286
303
  const feedbackContent = allHumanComments
287
304
  .map(c => {
288
- const isNew = c.date > cutoff;
305
+ const isNew = (new Date(c.date).getTime() || 0) > cutoffMs;
289
306
  return `${isNew ? '**[NEW]** ' : ''}**${c.author}** (${c.date}):\n${c.content.replace(/@minions\s*/gi, '').trim()}`;
290
307
  })
291
308
  .join('\n\n---\n\n');
@@ -412,7 +429,15 @@ async function reconcilePrs(config) {
412
429
  }
413
430
 
414
431
  if (projectAdded > 0 || projectUpdated > 0 || backfilled > 0) {
415
- shared.safeWrite(prPath, existingPrs);
432
+ mutateJsonFileLocked(prPath, (currentPrs) => {
433
+ // Merge reconciled PRs into the locked copy by ID
434
+ for (const pr of existingPrs) {
435
+ const idx = currentPrs.findIndex(p => p.id === pr.id);
436
+ if (idx >= 0) currentPrs[idx] = pr;
437
+ else currentPrs.push(pr);
438
+ }
439
+ return currentPrs;
440
+ }, { defaultValue: [] });
416
441
  totalAdded += projectAdded;
417
442
  if (projectUpdated > 0) log('info', `PR reconciliation: linked ${projectUpdated} existing PR(s) to PRD items in ${project.name}`);
418
443
  }
package/engine/github.js CHANGED
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  const shared = require('./shared');
8
- const { exec, getProjects, projectPrPath, projectWorkItemsPath, safeJson, safeWrite, MINIONS_DIR, addPrLink, getPrLinks, log, ts, dateStamp, PR_STATUS } = shared;
8
+ const { exec, getProjects, projectPrPath, projectWorkItemsPath, safeJson, safeWrite, mutateJsonFileLocked, MINIONS_DIR, addPrLink, getPrLinks, log, ts, dateStamp, PR_STATUS } = shared;
9
9
  const { getPrs } = require('./queries');
10
10
  const path = require('path');
11
11
 
@@ -71,7 +71,14 @@ async function forEachActiveGhPr(config, callback) {
71
71
  }
72
72
 
73
73
  if (projectUpdated > 0) {
74
- safeWrite(projectPrPath(project), prs);
74
+ mutateJsonFileLocked(projectPrPath(project), (currentPrs) => {
75
+ for (const updatedPr of prs) {
76
+ const idx = currentPrs.findIndex(p => p.id === updatedPr.id);
77
+ if (idx >= 0) currentPrs[idx] = updatedPr;
78
+ else currentPrs.push(updatedPr);
79
+ }
80
+ return currentPrs;
81
+ }, { defaultValue: [] });
75
82
  totalUpdated += projectUpdated;
76
83
  }
77
84
  }
@@ -105,7 +112,14 @@ async function forEachActiveGhPr(config, callback) {
105
112
  }
106
113
  }
107
114
  if (centralUpdated > 0) {
108
- safeWrite(centralPath, centralPrs);
115
+ mutateJsonFileLocked(centralPath, (currentPrs) => {
116
+ for (const updatedPr of centralPrs) {
117
+ const idx = currentPrs.findIndex(p => p.id === updatedPr.id);
118
+ if (idx >= 0) currentPrs[idx] = updatedPr;
119
+ else currentPrs.push(updatedPr);
120
+ }
121
+ return currentPrs;
122
+ }, { defaultValue: [] });
109
123
  totalUpdated += centralUpdated;
110
124
  }
111
125
 
@@ -138,6 +152,12 @@ async function pollPrStatus(config) {
138
152
  pr.reviewStatus = newStatus === PR_STATUS.MERGED ? 'approved' : 'pending';
139
153
  log('info', `PR ${pr.id} reviewStatus: waiting → ${pr.reviewStatus} (${newStatus})`);
140
154
  }
155
+ // Clear stale build status — checks won't be polled after close
156
+ if (pr.buildStatus && pr.buildStatus !== 'none') {
157
+ delete pr.buildStatus;
158
+ delete pr.buildFailReason;
159
+ delete pr._buildFailNotified;
160
+ }
141
161
  await engine().handlePostMerge(pr, project, config, newStatus);
142
162
  }
143
163
  }
@@ -182,11 +202,11 @@ async function pollPrStatus(config) {
182
202
  if (authorId) {
183
203
  try {
184
204
  const metricsPath = path.join(__dirname, 'metrics.json');
185
- const metrics = shared.safeJson(metricsPath) || {};
186
- if (!metrics[authorId]) metrics[authorId] = {};
187
- if (newReviewStatus === 'approved') metrics[authorId].prsApproved = (metrics[authorId].prsApproved || 0) + 1;
188
- else metrics[authorId].prsRejected = (metrics[authorId].prsRejected || 0) + 1;
189
- shared.safeWrite(metricsPath, metrics);
205
+ mutateJsonFileLocked(metricsPath, (metrics) => {
206
+ if (!metrics[authorId]) metrics[authorId] = {};
207
+ if (newReviewStatus === 'approved') metrics[authorId].prsApproved = (metrics[authorId].prsApproved || 0) + 1;
208
+ else metrics[authorId].prsRejected = (metrics[authorId].prsRejected || 0) + 1;
209
+ });
190
210
  } catch (err) { log('warn', `Metrics update: ${err.message}`); }
191
211
  }
192
212
  }
@@ -258,7 +278,8 @@ async function pollPrHumanComments(config) {
258
278
  return true;
259
279
  });
260
280
 
261
- const cutoff = pr.humanFeedback?.lastProcessedCommentDate || pr.created || '1970-01-01';
281
+ const cutoffStr = pr.humanFeedback?.lastProcessedCommentDate || pr.created || '1970-01-01';
282
+ const cutoffMs = new Date(cutoffStr).getTime() || 0;
262
283
 
263
284
  // Collect ALL human comments for full context, track new ones for triggering
264
285
  const allCommentEntries = [];
@@ -275,7 +296,8 @@ async function pollPrHumanComments(config) {
275
296
  allCommentEntries.push(entry);
276
297
 
277
298
  // Any new comment triggers a fix — no @minions filter needed
278
- if (date > cutoff) {
299
+ const dateMs = date ? new Date(date).getTime() : 0;
300
+ if (dateMs && dateMs > cutoffMs) {
279
301
  newComments.push(entry);
280
302
  }
281
303
  }
@@ -290,7 +312,7 @@ async function pollPrHumanComments(config) {
290
312
  // Provide ALL comments as context — the agent needs full thread context to fix properly
291
313
  const feedbackContent = allCommentEntries
292
314
  .map(c => {
293
- const isNew = c.date > cutoff;
315
+ const isNew = (new Date(c.date).getTime() || 0) > cutoffMs;
294
316
  return `${isNew ? '**[NEW]** ' : ''}**${c.author}** (${c.date}):\n${c.content.replace(/@minions\s*/gi, '').trim()}`;
295
317
  })
296
318
  .join('\n\n---\n\n');
@@ -397,7 +419,14 @@ async function reconcilePrs(config) {
397
419
  }
398
420
 
399
421
  if (projectAdded > 0 || backfilled > 0) {
400
- safeWrite(prPath, existingPrs);
422
+ mutateJsonFileLocked(prPath, (currentPrs) => {
423
+ for (const pr of existingPrs) {
424
+ const idx = currentPrs.findIndex(p => p.id === pr.id);
425
+ if (idx >= 0) currentPrs[idx] = pr;
426
+ else currentPrs.push(pr);
427
+ }
428
+ return currentPrs;
429
+ }, { defaultValue: [] });
401
430
  totalAdded += projectAdded;
402
431
  }
403
432
  }
@@ -293,6 +293,14 @@ function renderPlaybook(type, vars) {
293
293
  log('warn', `Playbook "${type}": substituted values contain unresolved {{...}} patterns (potential self-reference): ${selfRefVars.join(', ')}`);
294
294
  }
295
295
 
296
+ // Warn when a substituted value itself contains {{...}} patterns (potential self-reference)
297
+ const selfRefVars = Object.entries(allVars)
298
+ .filter(([, val]) => /\{\{\w+\}\}/.test(String(val)))
299
+ .map(([key]) => key);
300
+ if (selfRefVars.length > 0) {
301
+ log('warn', `Playbook "${type}": substituted values contain unresolved {{...}} patterns (potential self-reference): ${selfRefVars.join(', ')}`);
302
+ }
303
+
296
304
  // Warn on variables that resolved to empty string
297
305
  const emptyVars = Object.entries(allVars)
298
306
  .filter(([, val]) => String(val) === '')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.333",
3
+ "version": "0.1.335",
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"
@@ -102,7 +102,7 @@ Structure your report exactly like this:
102
102
 
103
103
  ## Auto-file Work Items on Failure
104
104
 
105
- If the build OR tests fail, you MUST create a work item so another agent can fix it. Write a JSON entry to the project's work queue:
105
+ If the build or tests fail, create a work item so another agent can fix it. Write a JSON entry to the project's work queue:
106
106
 
107
107
  ```bash
108
108
  # Read existing items, append new one, write back
@@ -17,7 +17,7 @@ Implement PRD item **{{item_id}}: {{item_name}}**
17
17
 
18
18
  This is part of a **shared-branch plan**. Other agents may have already committed work to this branch before you. Your job is to build on top of their work.
19
19
 
20
- ## Git Workflow (SHARED BRANCH — CRITICAL)
20
+ ## Git Workflow (Shared Branch)
21
21
 
22
22
  Your worktree is already set up. Pull latest before starting:
23
23
 
@@ -54,7 +54,7 @@ git push origin {{branch_name}}
54
54
 
55
55
  ## Build and Verify
56
56
 
57
- After implementation, you MUST:
57
+ After implementation:
58
58
  1. Build the project using the repo's build system (check CLAUDE.md, package.json, README)
59
59
  2. Verify the build succeeds with your changes AND all prior commits on this branch
60
60
  3. If the build fails:
@@ -58,7 +58,7 @@ Do NOT remove the worktree — the engine handles cleanup automatically.
58
58
 
59
59
  ## Build and Demo Rule
60
60
 
61
- After implementation, you MUST:
61
+ After implementation:
62
62
  1. Build the project using the repo's build system (check CLAUDE.md, package.json, README)
63
63
  2. Start if applicable
64
64
  3. Include the browser URL and run instructions in the PR description
@@ -71,7 +71,7 @@ After building, verify the build succeeded. If the build fails:
71
71
 
72
72
  ## Test Validation (MANDATORY before PR)
73
73
 
74
- Before creating a PR, you MUST run the project's test suite and ensure all existing tests pass:
74
+ Before creating a PR, run the project's test suite and ensure all existing tests pass:
75
75
 
76
76
  1. Find the test command by reading the project's own documentation — check CLAUDE.md, agent.md, README, or package.json scripts in the project root. Every project defines its own conventions.
77
77
  2. Run the full test suite using whatever command the project specifies
@@ -18,7 +18,7 @@ A user has provided a plan. Analyze it against the codebase and produce a struct
18
18
  3. **Break the plan into discrete, implementable items** — each should be a single PR's worth of work
19
19
  4. **Estimate complexity** — `small` (< 1 file), `medium` (2-5 files), `large` (6+ files or cross-cutting)
20
20
  5. **Order by dependency** — items that others depend on come first
21
- 6. **Use unique item IDs** — generate a short uuid for each item (e.g. `P-a3f9b2c1`). NEVER use sequential `P001`/`P002` — IDs must be globally unique across all PRDs to avoid collisions
21
+ 6. **Use unique item IDs** — generate a short uuid for each item (e.g. `P-a3f9b2c1`). Do not use sequential `P001`/`P002` — IDs must be globally unique across all PRDs to avoid collisions
22
22
  7. **Identify open questions** — flag anything ambiguous in the plan that needs user input
23
23
 
24
24
  ## Output
@@ -83,7 +83,7 @@ When using `parallel`:
83
83
 
84
84
  Rules for items:
85
85
  - IDs must be `P-<uuid>` format (e.g. `P-a3f9b2c1`) — globally unique, never sequential
86
- - **`status` MUST always be `"missing"`no exceptions.** Do NOT set `done`, `complete`, `implemented`, or any other value, even if you observe active PRs or completed work in the codebase. Status is exclusively engine-managed after the PRD is written. Pre-setting any other status causes items to be silently skipped by the engine and breaks dependency resolution for all downstream items.
86
+ - **`status` is always `"missing"`**do not set `done`, `complete`, `implemented`, or any other value, even if you observe active PRs or completed work in the codebase. Status is exclusively engine-managed after the PRD is written. Pre-setting any other status causes items to be silently skipped by the engine and breaks dependency resolution for all downstream items.
87
87
  - **`project` field is REQUIRED** — set it to the project name where the code changes go (e.g., `"OfficeAgent"`, `"office-bohemia"`). Cross-repo plans must route each item to the correct project. The engine materializes items into that project's work queue.
88
88
  - `depends_on` lists IDs of items that must be done first
89
89
  - Keep descriptions actionable — an implementing agent should know exactly what to build
@@ -38,7 +38,7 @@ Branch: `{{pr_branch}}`
38
38
 
39
39
  ## Post Review — Comment AND Vote on PR
40
40
 
41
- You MUST do both of the following:
41
+ Do both of the following:
42
42
 
43
43
  ### Step 1: Leave a detailed review comment
44
44
 
@@ -61,7 +61,7 @@ This vote is visible to human reviewers in the PR UI and helps them understand t
61
61
  If you encounter merge conflicts (e.g., the PR shows conflicts):
62
62
  1. Note the conflict in your review comment. Do NOT attempt to resolve — flag it for the author.
63
63
 
64
- ## CRITICAL: Do NOT run git checkout on the main working tree. Use `git diff` and `git show` only.
64
+ ## Do not run git checkout on the main working tree. Use `git diff` and `git show` only.
65
65
 
66
66
  ## Signal Completion
67
67
 
package/playbooks/test.md CHANGED
@@ -37,7 +37,7 @@ This is a **test/build/run task**. Your goal is to build, run, test, or verify s
37
37
 
38
38
  ## Run Command (IMPORTANT)
39
39
 
40
- When the build succeeds and the task involves running a server or app, you MUST output a ready-to-paste run command using **absolute paths** so the user can launch it from any terminal. Format it exactly like this:
40
+ When the build succeeds and the task involves running a server or app, output a ready-to-paste run command using **absolute paths** so the user can launch it from any terminal. Format it exactly like this:
41
41
 
42
42
  ```
43
43
  ## Run Command
@@ -218,7 +218,7 @@ For each project worktree:
218
218
  - If a project doesn't build, still document what SHOULD be testable once fixed
219
219
  - Do NOT fix code — only report issues
220
220
  - Leave all worktrees in place for the user to inspect
221
- - The application MUST be started **detached** so it keeps running after your process exits
221
+ - Start the application **detached** so it keeps running after your process exits
222
222
  - Use absolute paths everywhere so the user can copy-paste commands
223
223
  - E2E PRs are for review only — do NOT auto-complete or merge them
224
224
 
@@ -53,7 +53,7 @@ Write your findings to: `{{team_root}}/notes/inbox/{{agent_id}}-{{item_id}}-{{da
53
53
  **Note:** Do NOT write to `agents/*/status.json` — the engine manages your status automatically.
54
54
 
55
55
  ## Rules
56
- - NEVER checkout branches in the main working tree — use worktrees
56
+ - Do not checkout branches in the main working tree — use worktrees
57
57
  - Use the repo host's MCP tools for PR creation — check available MCP tools before starting
58
58
  - Use PowerShell for build commands on Windows if applicable
59
59
  - If you discover a repeatable workflow, output it as a ```skill block (the engine auto-extracts it to ~/.claude/skills/)