@yemi33/minions 0.1.1865 → 0.1.1867
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 +11 -0
- package/engine/cooldown.js +2 -0
- package/engine/lifecycle.js +60 -2
- package/engine/meeting.js +13 -0
- package/engine/scheduler.js +1 -0
- package/engine.js +26 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.1867 (2026-05-11)
|
|
4
|
+
|
|
5
|
+
### Fixes
|
|
6
|
+
- harden PRD filename uniqueness against TOCTOU collisions (#2346)
|
|
7
|
+
|
|
8
|
+
### Other
|
|
9
|
+
- test(consolidation): add unit tests for archiveInboxFiles, buildConsolidationPrompt, consolidateInbox state-flag handling (#2348)
|
|
10
|
+
- test(cooldown): add unit tests for normalizePrHeadForCooldown, getPrReviewHead, getPrReviewCooldownBase, clearLegacyPrReviewCooldown, clearCooldown (#2347)
|
|
11
|
+
- test(meeting): add unit tests for meeting pure helpers (round/content/artifact) (#2343)
|
|
12
|
+
- test(scheduler): add unit tests for writeScheduleRunEntry, createScheduledWorkItem, recordScheduleRun (#2341)
|
|
13
|
+
|
|
3
14
|
## 0.1.1865 (2026-05-11)
|
|
4
15
|
|
|
5
16
|
### Fixes
|
package/engine/cooldown.js
CHANGED
|
@@ -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,
|
package/engine/lifecycle.js
CHANGED
|
@@ -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
|
-
|
|
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) {
|
|
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/scheduler.js
CHANGED
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
|
|
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.
|
|
3
|
+
"version": "0.1.1867",
|
|
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"
|