@yemi33/minions 0.1.2193 → 0.1.2195
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/command-center.js +2 -0
- package/dashboard/js/fre.js +125 -1
- package/dashboard/js/refresh.js +6 -0
- package/dashboard.js +12 -11
- package/engine/queries.js +5 -4
- package/engine/shared.js +100 -0
- package/engine.js +47 -9
- package/package.json +1 -1
|
@@ -476,6 +476,8 @@ function ccAbort() {
|
|
|
476
476
|
|
|
477
477
|
function toggleCommandCenter() {
|
|
478
478
|
_ccOpen = !_ccOpen;
|
|
479
|
+
// Opening CC means the operator found it — retire the first-use coachmark.
|
|
480
|
+
if (_ccOpen && typeof dismissCcHint === 'function') dismissCcHint();
|
|
479
481
|
var drawer = document.getElementById('cc-drawer');
|
|
480
482
|
var overlay = document.getElementById('cc-overlay');
|
|
481
483
|
if (_ccOpen) ccApplySavedWidth();
|
package/dashboard/js/fre.js
CHANGED
|
@@ -188,4 +188,128 @@ function renderFre(statusOrProjects) {
|
|
|
188
188
|
'</div>';
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
-
|
|
191
|
+
// ── Command Center first-use coachmark ──────────────────────────────────────
|
|
192
|
+
// A small popup anchored under the Command Center header button, shown to
|
|
193
|
+
// brand-new operators who have never used CC. Teaches the primary entry point:
|
|
194
|
+
// "prompt the Command Center to ask it to dispatch work." Self-contained and
|
|
195
|
+
// idempotent — safe to call renderCcHint() every refresh tick.
|
|
196
|
+
//
|
|
197
|
+
// Trigger: NOT dismissed AND the operator has never sent a CC message (no
|
|
198
|
+
// 'cc-tabs' entry carries any messages). Auto-suppressed while the CC drawer
|
|
199
|
+
// is open. Dismissed permanently when the user opens CC (see
|
|
200
|
+
// toggleCommandCenter in command-center.js) or clicks "Got it".
|
|
201
|
+
|
|
202
|
+
const CC_HINT_DISMISS_KEY = 'minions_cc_hint_dismissed';
|
|
203
|
+
const CC_HINT_MOUNT_ID = 'cc-hint-coachmark';
|
|
204
|
+
|
|
205
|
+
function _ccHintIsDismissed() {
|
|
206
|
+
try { return localStorage.getItem(CC_HINT_DISMISS_KEY) === '1'; } catch { return false; }
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// "Brand new" = the operator has never sent a Command Center message. Reads the
|
|
210
|
+
// same 'cc-tabs' localStorage key command-center.js persists; any tab carrying
|
|
211
|
+
// at least one message means CC has been used and the hint has served its
|
|
212
|
+
// purpose. Absent/empty/parse-error all read as new (fail toward showing).
|
|
213
|
+
function _ccHintUserIsNew() {
|
|
214
|
+
try {
|
|
215
|
+
const raw = localStorage.getItem('cc-tabs');
|
|
216
|
+
if (!raw) return true;
|
|
217
|
+
const tabs = JSON.parse(raw);
|
|
218
|
+
if (!Array.isArray(tabs) || tabs.length === 0) return true;
|
|
219
|
+
return !tabs.some(function(t) { return t && Array.isArray(t.messages) && t.messages.length > 0; });
|
|
220
|
+
} catch { return true; }
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function _ccHintRemove() {
|
|
224
|
+
const mount = document.getElementById(CC_HINT_MOUNT_ID);
|
|
225
|
+
if (mount && mount.parentNode) mount.parentNode.removeChild(mount);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function dismissCcHint() {
|
|
229
|
+
try { localStorage.setItem(CC_HINT_DISMISS_KEY, '1'); } catch { /* expected */ }
|
|
230
|
+
_ccHintRemove();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Reset helper (exposed for tests / debugging only). Not wired to UI.
|
|
234
|
+
function _ccHintReset() {
|
|
235
|
+
try { localStorage.removeItem(CC_HINT_DISMISS_KEY); } catch { /* expected */ }
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Open the Command Center from the coachmark CTA, then dismiss the hint.
|
|
239
|
+
function openCcFromHint() {
|
|
240
|
+
dismissCcHint();
|
|
241
|
+
if (typeof toggleCommandCenter === 'function' && !window._ccOpen) {
|
|
242
|
+
try { toggleCommandCenter(); } catch { /* drawer not mounted yet */ }
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Re-anchor the coachmark under the CC button. Called on render and on resize.
|
|
247
|
+
function _ccHintPosition() {
|
|
248
|
+
const mount = document.getElementById(CC_HINT_MOUNT_ID);
|
|
249
|
+
const btn = document.getElementById('cc-toggle-btn');
|
|
250
|
+
if (!mount || !btn) return;
|
|
251
|
+
const rect = btn.getBoundingClientRect();
|
|
252
|
+
mount.style.top = (rect.bottom + 10) + 'px';
|
|
253
|
+
// Right-align the card to the button's right edge.
|
|
254
|
+
mount.style.right = Math.max(8, window.innerWidth - rect.right) + 'px';
|
|
255
|
+
// Point the caret at the button's horizontal centre.
|
|
256
|
+
const arrow = document.getElementById('cc-hint-arrow');
|
|
257
|
+
if (arrow) arrow.style.right = Math.max(12, (rect.width / 2) - 6) + 'px';
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
let _ccHintResizeBound = false;
|
|
261
|
+
|
|
262
|
+
// Render the coachmark into document.body. Idempotent — bails out (and removes
|
|
263
|
+
// any stale mount) when dismissed, when CC has been used, when the header
|
|
264
|
+
// button isn't mounted yet, or while the drawer is open.
|
|
265
|
+
function renderCcHint() {
|
|
266
|
+
if (_ccHintIsDismissed() || !_ccHintUserIsNew()) {
|
|
267
|
+
_ccHintRemove();
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const btn = document.getElementById('cc-toggle-btn');
|
|
271
|
+
if (!btn) return; // header not assembled yet
|
|
272
|
+
const drawer = document.getElementById('cc-drawer');
|
|
273
|
+
if (drawer && drawer.style.display === 'flex') {
|
|
274
|
+
// User already found CC — don't hover the hint over the open drawer.
|
|
275
|
+
_ccHintRemove();
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
let mount = document.getElementById(CC_HINT_MOUNT_ID);
|
|
280
|
+
if (!mount) {
|
|
281
|
+
mount = document.createElement('div');
|
|
282
|
+
mount.id = CC_HINT_MOUNT_ID;
|
|
283
|
+
mount.style.cssText = [
|
|
284
|
+
'position:fixed',
|
|
285
|
+
'z-index:330',
|
|
286
|
+
'width:260px',
|
|
287
|
+
'padding:14px 16px',
|
|
288
|
+
'background:var(--surface)',
|
|
289
|
+
'border:1px solid var(--blue)',
|
|
290
|
+
'border-radius:var(--radius-lg)',
|
|
291
|
+
'box-shadow:var(--shadow-md)',
|
|
292
|
+
'color:var(--text)',
|
|
293
|
+
'font-size:var(--text-md)',
|
|
294
|
+
'line-height:1.5',
|
|
295
|
+
].join(';');
|
|
296
|
+
document.body.appendChild(mount);
|
|
297
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: static string literal — no user-controlled data is interpolated
|
|
298
|
+
mount.innerHTML =
|
|
299
|
+
'<div id="cc-hint-arrow" style="position:absolute;top:-7px;width:12px;height:12px;background:var(--surface);border-left:1px solid var(--blue);border-top:1px solid var(--blue);transform:rotate(45deg)"></div>' +
|
|
300
|
+
'<div style="font-weight:700;color:var(--blue);margin-bottom:4px">👋 Start here</div>' +
|
|
301
|
+
'<div style="color:var(--text)">Prompt your <strong>Command Center</strong> here to ask it to dispatch work.</div>' +
|
|
302
|
+
'<div style="display:flex;gap:8px;margin-top:12px">' +
|
|
303
|
+
'<button onclick="openCcFromHint()" class="btn-primary-solid" style="padding:5px 12px;font-size:var(--text-sm)">Open Command Center</button>' +
|
|
304
|
+
'<button onclick="dismissCcHint()" style="padding:5px 12px;font-size:var(--text-sm);background:transparent;color:var(--muted);border:1px solid var(--border);border-radius:var(--radius-sm);cursor:pointer">Got it</button>' +
|
|
305
|
+
'</div>';
|
|
306
|
+
}
|
|
307
|
+
_ccHintPosition();
|
|
308
|
+
if (!_ccHintResizeBound) {
|
|
309
|
+
_ccHintResizeBound = true;
|
|
310
|
+
window.addEventListener('resize', _ccHintPosition);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
window.MinionsFre = { renderFre, dismissFre, openSettingsToDefaultCli, _freReset, FRE_DISMISS_KEY,
|
|
315
|
+
renderCcHint, dismissCcHint, openCcFromHint, _ccHintReset, _ccHintUserIsNew, CC_HINT_DISMISS_KEY };
|
package/dashboard/js/refresh.js
CHANGED
|
@@ -612,6 +612,12 @@ function _processStatusUpdate(data, opts) {
|
|
|
612
612
|
if (typeof renderFre === 'function') {
|
|
613
613
|
_safeRender('fre', function() { renderFre(data); });
|
|
614
614
|
}
|
|
615
|
+
// Command Center first-use coachmark — anchored under the CC header button
|
|
616
|
+
// for brand-new operators who have never sent a CC message. Idempotent +
|
|
617
|
+
// cheap; self-suppresses once CC is used or dismissed.
|
|
618
|
+
if (typeof renderCcHint === 'function') {
|
|
619
|
+
_safeRender('ccHint', function() { renderCcHint(); });
|
|
620
|
+
}
|
|
615
621
|
// Notes file (notes.md) is now sourced from /state/notes.md. Same shape
|
|
616
622
|
// as the legacy /api/status notes slice ({ content, updatedAt }); the
|
|
617
623
|
// ETag mtime is parsed out of the response header for updatedAt.
|
package/dashboard.js
CHANGED
|
@@ -7363,19 +7363,20 @@ const server = http.createServer(async (req, res) => {
|
|
|
7363
7363
|
return data;
|
|
7364
7364
|
}, { defaultValue: {} });
|
|
7365
7365
|
|
|
7366
|
-
// W-mqacrzis0003df4a — Fresh source-plan
|
|
7367
|
-
//
|
|
7368
|
-
//
|
|
7369
|
-
//
|
|
7370
|
-
//
|
|
7371
|
-
//
|
|
7372
|
-
//
|
|
7373
|
-
//
|
|
7366
|
+
// W-mqacrzis0003df4a + W-mqfevwr60018bd09 — Fresh source-plan
|
|
7367
|
+
// staleness check (content-hash gated). Approve is the last gate
|
|
7368
|
+
// before materialization, and the diff-aware regen block below gates
|
|
7369
|
+
// on `wasStale`. The persisted `data.planStale` flag lags by an
|
|
7370
|
+
// engine tick (~10s); without this fresh stat a fast user can
|
|
7371
|
+
// Approve within the tick window, `wasStale` stays false, the
|
|
7372
|
+
// diff-aware regen is silently skipped, and items materialize from
|
|
7373
|
+
// the OLD PRD. The content-hash gate (W-mqfevwr60018bd09) also
|
|
7374
|
+
// ensures path-only repoints don't fire the destructive diff-aware
|
|
7375
|
+
// regen — Mirrors the staleness logic in engine/queries.js#getPrdInfo
|
|
7376
|
+
// and the /api/plans handler above so all three readers agree.
|
|
7374
7377
|
if (!wasStale && plan && plan.source_plan && plan.sourcePlanModifiedAt) {
|
|
7375
7378
|
try {
|
|
7376
|
-
|
|
7377
|
-
const recorded = new Date(plan.sourcePlanModifiedAt).getTime();
|
|
7378
|
-
if (recorded && sourceMtime > recorded) wasStale = true;
|
|
7379
|
+
if (shared.isSourcePlanContentStale(PLANS_DIR, plan).stale) wasStale = true;
|
|
7379
7380
|
} catch { /* source plan may have been deleted/renamed — fall through with wasStale=false */ }
|
|
7380
7381
|
}
|
|
7381
7382
|
|
package/engine/queries.js
CHANGED
|
@@ -1892,13 +1892,14 @@ function getPrdInfo(config) {
|
|
|
1892
1892
|
}
|
|
1893
1893
|
if (!plan || !plan.missing_features) continue;
|
|
1894
1894
|
|
|
1895
|
-
// Staleness:
|
|
1895
|
+
// Staleness: gate on actual source-plan content change rather
|
|
1896
|
+
// than mtime alone (W-mqfevwr60018bd09). Pure path / pure mtime
|
|
1897
|
+
// drifts return stale=false here so a repointed source_plan
|
|
1898
|
+
// doesn't masquerade as a real revision in the cached PRD info.
|
|
1896
1899
|
let planStale = false;
|
|
1897
1900
|
if (!archived && plan.source_plan) {
|
|
1898
1901
|
try {
|
|
1899
|
-
|
|
1900
|
-
const recorded = plan.sourcePlanModifiedAt ? new Date(plan.sourcePlanModifiedAt).getTime() : null;
|
|
1901
|
-
if (recorded && sourceMtime > recorded) planStale = true;
|
|
1902
|
+
planStale = shared.isSourcePlanContentStale(PLANS_DIR, plan).stale;
|
|
1902
1903
|
} catch { /* optional */ }
|
|
1903
1904
|
}
|
|
1904
1905
|
existingPrds.push({
|
package/engine/shared.js
CHANGED
|
@@ -657,6 +657,103 @@ function safeReadDir(dir) {
|
|
|
657
657
|
try { return fs.readdirSync(dir); } catch { return []; }
|
|
658
658
|
}
|
|
659
659
|
|
|
660
|
+
// ── PRD source-plan resolution & content hashing (W-mqfevwr60018bd09) ────────
|
|
661
|
+
//
|
|
662
|
+
// Repointing a PRD's `source_plan` field (e.g. plans/foo.md →
|
|
663
|
+
// plans/archive/foo.md, or any operator edit that touches mtime without
|
|
664
|
+
// changing the markdown body) used to trigger the diff-aware PRD resync as
|
|
665
|
+
// if the plan content had changed — destroying / re-materializing work
|
|
666
|
+
// items even though only the POINTER moved. These helpers gate the
|
|
667
|
+
// destructive resync on actual *content* change via a sha256 hash, and
|
|
668
|
+
// also fall back to plans/archive/<basename> so a silently-relocated
|
|
669
|
+
// source plan no longer spams ENOENT warnings on every tick.
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Resolve a PRD's `source_plan` field to an absolute file path under
|
|
673
|
+
* `plansDir`. Tries the direct join first; falls back to
|
|
674
|
+
* plansDir/archive/<basename(source_plan)> when the direct path is
|
|
675
|
+
* missing. Returns null when neither location resolves to a regular file.
|
|
676
|
+
*/
|
|
677
|
+
function resolveSourcePlanPath(plansDir, sourcePlan) {
|
|
678
|
+
if (!plansDir || !sourcePlan || typeof sourcePlan !== 'string') return null;
|
|
679
|
+
const direct = path.join(plansDir, sourcePlan);
|
|
680
|
+
try { if (fs.statSync(direct).isFile()) return direct; } catch { /* miss */ }
|
|
681
|
+
const archived = path.join(plansDir, 'archive', path.basename(sourcePlan));
|
|
682
|
+
try { if (fs.statSync(archived).isFile()) return archived; } catch { /* miss */ }
|
|
683
|
+
return null;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Compute a deterministic sha256 hex digest of a source plan markdown
|
|
688
|
+
* file. Returns null when the file cannot be read. Used to gate the
|
|
689
|
+
* destructive PRD resync on actual content change rather than mtime
|
|
690
|
+
* or path drift.
|
|
691
|
+
*/
|
|
692
|
+
function computeSourcePlanContentHash(absPath) {
|
|
693
|
+
if (!absPath || typeof absPath !== 'string') return null;
|
|
694
|
+
try {
|
|
695
|
+
const buf = fs.readFileSync(absPath);
|
|
696
|
+
return crypto.createHash('sha256').update(buf).digest('hex');
|
|
697
|
+
} catch { return null; }
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* Decide whether a PRD's source plan has *actually* changed since the
|
|
702
|
+
* last sync, gating the destructive diff-aware resync on content change
|
|
703
|
+
* rather than mtime or path drift.
|
|
704
|
+
*
|
|
705
|
+
* Returns:
|
|
706
|
+
* {
|
|
707
|
+
* stale: boolean, // true → callers should treat the PRD as stale
|
|
708
|
+
* contentChanged: boolean, // true iff we have BOTH hashes AND they differ
|
|
709
|
+
* currentHash: string|null, // computed only when mtime advanced
|
|
710
|
+
* resolvedPath: string|null,// absolute path of the resolved markdown
|
|
711
|
+
* sourceMtime: number|null, // floor(ms) mtime of the resolved file
|
|
712
|
+
* }
|
|
713
|
+
*
|
|
714
|
+
* `stale` is true only when:
|
|
715
|
+
* - the resolved file exists, AND
|
|
716
|
+
* - mtime advanced past plan.sourcePlanModifiedAt, AND
|
|
717
|
+
* - either no _sourcePlanContentHash was recorded on the PRD (legacy
|
|
718
|
+
* fallback to mtime — preserves pre-fix behavior on first encounter),
|
|
719
|
+
* OR the recorded hash differs from the current file's hash.
|
|
720
|
+
*
|
|
721
|
+
* `contentChanged` is true only when both the recorded hash and the
|
|
722
|
+
* current hash exist AND differ. Pure-pointer / pure-mtime drifts leave
|
|
723
|
+
* `contentChanged` false so callers can silently re-baseline
|
|
724
|
+
* `sourcePlanModifiedAt` (and `_sourcePlanContentHash` for legacy PRDs)
|
|
725
|
+
* without triggering the destructive resync pipeline.
|
|
726
|
+
*/
|
|
727
|
+
function isSourcePlanContentStale(plansDir, plan) {
|
|
728
|
+
const out = {
|
|
729
|
+
stale: false, contentChanged: false,
|
|
730
|
+
currentHash: null, resolvedPath: null, sourceMtime: null,
|
|
731
|
+
};
|
|
732
|
+
if (!plan || typeof plan !== 'object' || !plan.source_plan) return out;
|
|
733
|
+
const resolved = resolveSourcePlanPath(plansDir, plan.source_plan);
|
|
734
|
+
out.resolvedPath = resolved;
|
|
735
|
+
if (!resolved) return out;
|
|
736
|
+
let mtime;
|
|
737
|
+
try { mtime = Math.floor(fs.statSync(resolved).mtimeMs); } catch { return out; }
|
|
738
|
+
out.sourceMtime = mtime;
|
|
739
|
+
const recorded = plan.sourcePlanModifiedAt ? new Date(plan.sourcePlanModifiedAt).getTime() : null;
|
|
740
|
+
if (!recorded || mtime <= recorded) return out;
|
|
741
|
+
// mtime advanced — verify the content actually differs before flagging.
|
|
742
|
+
out.currentHash = computeSourcePlanContentHash(resolved);
|
|
743
|
+
const recordedHash = plan._sourcePlanContentHash || null;
|
|
744
|
+
if (recordedHash && out.currentHash) {
|
|
745
|
+
out.contentChanged = recordedHash !== out.currentHash;
|
|
746
|
+
out.stale = out.contentChanged;
|
|
747
|
+
} else {
|
|
748
|
+
// Legacy PRD with no recorded hash — fall back to mtime semantics so
|
|
749
|
+
// we don't silently ignore real revisions made before the hash field
|
|
750
|
+
// existed. The engine sweep records the fresh hash on this pass so
|
|
751
|
+
// subsequent mtime-only drifts no longer trigger resync.
|
|
752
|
+
out.stale = true;
|
|
753
|
+
}
|
|
754
|
+
return out;
|
|
755
|
+
}
|
|
756
|
+
|
|
660
757
|
// ── SQL-routing shim for migrated state files ──────────────────────────────
|
|
661
758
|
//
|
|
662
759
|
// Phase 9 (post-Phase 8 cleanup): every state file that has a SQL backing
|
|
@@ -8211,6 +8308,9 @@ module.exports = {
|
|
|
8211
8308
|
getProjectOrg,
|
|
8212
8309
|
getAdoOrgBase,
|
|
8213
8310
|
sanitizePath,
|
|
8311
|
+
resolveSourcePlanPath, // W-mqfevwr60018bd09 — PRD staleness: resolve source_plan to absolute path (archive-aware)
|
|
8312
|
+
computeSourcePlanContentHash, // W-mqfevwr60018bd09 — sha256 of source plan markdown body
|
|
8313
|
+
isSourcePlanContentStale, // W-mqfevwr60018bd09 — gate destructive PRD resync on actual content change
|
|
8214
8314
|
sanitizeBranch,
|
|
8215
8315
|
getOperatorLogin,
|
|
8216
8316
|
deriveWorkItemBranchName,
|
package/engine.js
CHANGED
|
@@ -5348,34 +5348,72 @@ function materializePlansAsWorkItems(config) {
|
|
|
5348
5348
|
}
|
|
5349
5349
|
} catch (e) { log('warn', `Sequential ID remapping failed for ${file}: ${e.message}`); }
|
|
5350
5350
|
|
|
5351
|
-
// Plan staleness: if source_plan
|
|
5351
|
+
// Plan staleness: if source_plan markdown content was modified since
|
|
5352
|
+
// last sync, auto-clean and re-sync. W-mqfevwr60018bd09 — gate the
|
|
5353
|
+
// destructive resync on a sha256 content hash rather than mtime alone
|
|
5354
|
+
// so a path-only repoint (e.g. operator changes `source_plan` from
|
|
5355
|
+
// `plans/foo.md` to `plans/archive/foo.md` to silence ENOENT warnings)
|
|
5356
|
+
// no longer destroys work items when the markdown body is identical.
|
|
5352
5357
|
if (plan.source_plan) {
|
|
5353
|
-
const
|
|
5358
|
+
const staleness = shared.isSourcePlanContentStale(PLANS_DIR, plan);
|
|
5359
|
+
const resolved = staleness.resolvedPath;
|
|
5360
|
+
const sourceMtime = staleness.sourceMtime;
|
|
5361
|
+
const recorded = plan.sourcePlanModifiedAt ? new Date(plan.sourcePlanModifiedAt).getTime() : null;
|
|
5354
5362
|
try {
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
//
|
|
5363
|
+
if (!resolved) {
|
|
5364
|
+
// Neither plans/<source_plan> nor plans/archive/<basename> exists.
|
|
5365
|
+
// Stay quiet — emitting an ENOENT warn every tick was the original
|
|
5366
|
+
// noise that prompted operators to repoint source_plan and trigger
|
|
5367
|
+
// the destructive resync (see W-mqfevwr60018bd09 repro).
|
|
5368
|
+
} else if (!recorded) {
|
|
5369
|
+
// First time seeing this plan — baseline both the mtime and the
|
|
5370
|
+
// content hash so future ticks can distinguish path-only drift
|
|
5371
|
+
// from a real revision.
|
|
5372
|
+
const baselineHash = staleness.currentHash || shared.computeSourcePlanContentHash(resolved);
|
|
5359
5373
|
plan = mutatePrdLocked(file, plan, (current) => {
|
|
5360
5374
|
if (!current.sourcePlanModifiedAt) current.sourcePlanModifiedAt = new Date(sourceMtime).toISOString();
|
|
5375
|
+
if (!current._sourcePlanContentHash && baselineHash) current._sourcePlanContentHash = baselineHash;
|
|
5361
5376
|
return current;
|
|
5362
5377
|
}, { skipWriteIfUnchanged: true });
|
|
5363
|
-
} else if (
|
|
5364
|
-
//
|
|
5378
|
+
} else if (staleness.stale) {
|
|
5379
|
+
// mtime advanced AND content actually changed (or this is a
|
|
5380
|
+
// legacy PRD with no recorded hash — see isSourcePlanContentStale).
|
|
5381
|
+
// Run the destructive resync as before.
|
|
5365
5382
|
log('info', `Source plan ${plan.source_plan} updated — re-syncing PRD ${file}`);
|
|
5366
5383
|
autoCleanPrdWorkItems(file, config);
|
|
5367
5384
|
|
|
5368
|
-
// Handle PRD based on current status
|
|
5369
5385
|
const prdStatus = plan.status || (plan.requires_approval ? 'awaiting-approval' : null);
|
|
5386
|
+
const refreshedHash = staleness.currentHash || shared.computeSourcePlanContentHash(resolved);
|
|
5370
5387
|
|
|
5371
5388
|
plan = mutatePrdLocked(file, plan, (current) => {
|
|
5372
5389
|
current.sourcePlanModifiedAt = new Date(sourceMtime).toISOString();
|
|
5390
|
+
if (refreshedHash) current._sourcePlanContentHash = refreshedHash;
|
|
5373
5391
|
current.lastSyncedFromPlan = ts();
|
|
5374
5392
|
const currentPrdStatus = current.status || (current.requires_approval ? 'awaiting-approval' : null);
|
|
5375
5393
|
if (currentPrdStatus) current.planStale = true;
|
|
5376
5394
|
return current;
|
|
5377
5395
|
});
|
|
5378
5396
|
if (prdStatus) log('info', `PRD ${file} flagged as stale (plan revised while ${prdStatus}) — user can regenerate from dashboard`);
|
|
5397
|
+
} else if (sourceMtime > recorded) {
|
|
5398
|
+
// mtime drifted forward but content hash matches — silently
|
|
5399
|
+
// re-baseline the tracking fields so we don't keep recomputing
|
|
5400
|
+
// the hash on every tick. No resync, no planStale flip.
|
|
5401
|
+
const refreshedHash = staleness.currentHash || shared.computeSourcePlanContentHash(resolved);
|
|
5402
|
+
plan = mutatePrdLocked(file, plan, (current) => {
|
|
5403
|
+
current.sourcePlanModifiedAt = new Date(sourceMtime).toISOString();
|
|
5404
|
+
if (refreshedHash && !current._sourcePlanContentHash) current._sourcePlanContentHash = refreshedHash;
|
|
5405
|
+
return current;
|
|
5406
|
+
}, { skipWriteIfUnchanged: true });
|
|
5407
|
+
} else if (!plan._sourcePlanContentHash) {
|
|
5408
|
+
// Steady state but no hash recorded yet (PRD pre-dates this fix).
|
|
5409
|
+
// Backfill silently so the very next path-only repoint is a no-op.
|
|
5410
|
+
const baselineHash = shared.computeSourcePlanContentHash(resolved);
|
|
5411
|
+
if (baselineHash) {
|
|
5412
|
+
plan = mutatePrdLocked(file, plan, (current) => {
|
|
5413
|
+
if (!current._sourcePlanContentHash) current._sourcePlanContentHash = baselineHash;
|
|
5414
|
+
return current;
|
|
5415
|
+
}, { skipWriteIfUnchanged: true });
|
|
5416
|
+
}
|
|
5379
5417
|
}
|
|
5380
5418
|
} catch (e) { log('warn', 'plan staleness check: ' + e.message); }
|
|
5381
5419
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2195",
|
|
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"
|