@yemi33/minions 0.1.2108 → 0.1.2110
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/qa.js +64 -2
- package/dashboard/js/settings.js +2 -0
- package/dashboard.js +41 -0
- package/docs/deprecated.json +8 -0
- package/docs/design-state-storage.md +1 -1
- package/docs/onboarding.md +2 -2
- package/docs/qa-runbook-lifecycle.md +36 -0
- package/engine/db/migrations/009-qa.js +140 -0
- package/engine/qa-runs.js +118 -32
- package/engine/qa-sessions.js +30 -14
- package/engine/shared.js +79 -1
- package/engine/small-state-store.js +320 -0
- package/engine.js +52 -6
- package/package.json +1 -1
|
@@ -521,6 +521,314 @@ function _mirrorWorktreePoolJson(filePath) {
|
|
|
521
521
|
} catch { /* mirror best-effort */ }
|
|
522
522
|
}
|
|
523
523
|
|
|
524
|
+
// ─── qa_runs ───────────────────────────────────────────────────────────────
|
|
525
|
+
// Shape: [ {id, runbookId, targetName, project, workItemId, status, startedAt,
|
|
526
|
+
// completedAt, createdAt, artifacts, summary, ...}, ... ]
|
|
527
|
+
// SQL: row per id, extracted query columns + JSON blob `data`.
|
|
528
|
+
// Pattern: mirrors watches-store (top-level array, id-keyed diff).
|
|
529
|
+
|
|
530
|
+
let _qaRunsHash = null;
|
|
531
|
+
|
|
532
|
+
function _hydrateQaRuns(db) {
|
|
533
|
+
const fp = _resolveFilePath('qa-runs.json');
|
|
534
|
+
const raw = _readJson(fp) || [];
|
|
535
|
+
if (!Array.isArray(raw)) return;
|
|
536
|
+
db.prepare('DELETE FROM qa_runs').run();
|
|
537
|
+
const now = Date.now();
|
|
538
|
+
const ins = db.prepare(`
|
|
539
|
+
INSERT INTO qa_runs (id, runbook_id, target_name, project, work_item_id, status, started_at, completed_at, created_at, data)
|
|
540
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
541
|
+
ON CONFLICT(id) DO NOTHING
|
|
542
|
+
`);
|
|
543
|
+
for (const run of raw) {
|
|
544
|
+
if (!run || !run.id) continue;
|
|
545
|
+
ins.run(
|
|
546
|
+
String(run.id),
|
|
547
|
+
String(run.runbookId || ''),
|
|
548
|
+
String(run.targetName || ''),
|
|
549
|
+
run.project || null,
|
|
550
|
+
run.workItemId || null,
|
|
551
|
+
String(run.status || 'pending'),
|
|
552
|
+
_toMs(run.startedAt),
|
|
553
|
+
_toMs(run.completedAt),
|
|
554
|
+
_toMs(run.createdAt) || now,
|
|
555
|
+
JSON.stringify(run),
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function _resyncQaRunsIfDiverged(db) {
|
|
561
|
+
const fp = _resolveFilePath('qa-runs.json');
|
|
562
|
+
const currentHash = _fileContentHash(fp);
|
|
563
|
+
if (currentHash == null) return;
|
|
564
|
+
if (_qaRunsHash != null && currentHash === _qaRunsHash) return;
|
|
565
|
+
if (_qaRunsHash == null) {
|
|
566
|
+
const sqlHas = db.prepare('SELECT 1 FROM qa_runs LIMIT 1').get();
|
|
567
|
+
if (sqlHas) { _qaRunsHash = currentHash; return; }
|
|
568
|
+
}
|
|
569
|
+
_hydrateQaRuns(db);
|
|
570
|
+
_qaRunsHash = currentHash;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function _readQaRunsFromSqlOnly(db) {
|
|
574
|
+
const rows = db.prepare('SELECT data FROM qa_runs ORDER BY created_at, rowid').all();
|
|
575
|
+
const out = [];
|
|
576
|
+
for (const row of rows) {
|
|
577
|
+
try { out.push(JSON.parse(row.data)); } catch { /* skip malformed */ }
|
|
578
|
+
}
|
|
579
|
+
return out;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function readQaRuns() {
|
|
583
|
+
const { getDb } = require('./db');
|
|
584
|
+
let db;
|
|
585
|
+
try { db = getDb(); }
|
|
586
|
+
catch { return _readJson(_resolveFilePath('qa-runs.json')) || []; }
|
|
587
|
+
_resyncQaRunsIfDiverged(db);
|
|
588
|
+
const out = _readQaRunsFromSqlOnly(db);
|
|
589
|
+
if (out.length === 0) {
|
|
590
|
+
const fallback = _readJson(_resolveFilePath('qa-runs.json'));
|
|
591
|
+
if (Array.isArray(fallback) && fallback.length > 0) return fallback;
|
|
592
|
+
return [];
|
|
593
|
+
}
|
|
594
|
+
return out;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function applyQaRunsMutation(mutator) {
|
|
598
|
+
const { getDb, withTransaction } = require('./db');
|
|
599
|
+
let db;
|
|
600
|
+
try { db = getDb(); }
|
|
601
|
+
catch (e) { throw new Error(`small-state-store: SQLite unavailable (${e.message})`); }
|
|
602
|
+
|
|
603
|
+
return withTransaction(db, () => {
|
|
604
|
+
_resyncQaRunsIfDiverged(db);
|
|
605
|
+
const before = _readQaRunsFromSqlOnly(db);
|
|
606
|
+
const beforeSnap = JSON.parse(JSON.stringify(before));
|
|
607
|
+
const next = mutator(before);
|
|
608
|
+
const after = (next === undefined || next === null)
|
|
609
|
+
? before
|
|
610
|
+
: (Array.isArray(next) ? next : before);
|
|
611
|
+
|
|
612
|
+
const indexById = (arr) => {
|
|
613
|
+
const out = new Map();
|
|
614
|
+
for (const r of arr) {
|
|
615
|
+
if (r && r.id) out.set(String(r.id), r);
|
|
616
|
+
}
|
|
617
|
+
return out;
|
|
618
|
+
};
|
|
619
|
+
const beforeMap = indexById(beforeSnap);
|
|
620
|
+
const afterMap = indexById(after);
|
|
621
|
+
|
|
622
|
+
const now = Date.now();
|
|
623
|
+
const upsert = db.prepare(`
|
|
624
|
+
INSERT INTO qa_runs (id, runbook_id, target_name, project, work_item_id, status, started_at, completed_at, created_at, data)
|
|
625
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
626
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
627
|
+
runbook_id = excluded.runbook_id,
|
|
628
|
+
target_name = excluded.target_name,
|
|
629
|
+
project = excluded.project,
|
|
630
|
+
work_item_id = excluded.work_item_id,
|
|
631
|
+
status = excluded.status,
|
|
632
|
+
started_at = excluded.started_at,
|
|
633
|
+
completed_at = excluded.completed_at,
|
|
634
|
+
created_at = excluded.created_at,
|
|
635
|
+
data = excluded.data
|
|
636
|
+
`);
|
|
637
|
+
const del = db.prepare('DELETE FROM qa_runs WHERE id = ?');
|
|
638
|
+
let wrote = false;
|
|
639
|
+
for (const [id, run] of afterMap) {
|
|
640
|
+
const prev = beforeMap.get(id);
|
|
641
|
+
if (!prev || JSON.stringify(prev) !== JSON.stringify(run)) {
|
|
642
|
+
upsert.run(
|
|
643
|
+
id,
|
|
644
|
+
String(run.runbookId || ''),
|
|
645
|
+
String(run.targetName || ''),
|
|
646
|
+
run.project || null,
|
|
647
|
+
run.workItemId || null,
|
|
648
|
+
String(run.status || 'pending'),
|
|
649
|
+
_toMs(run.startedAt),
|
|
650
|
+
_toMs(run.completedAt),
|
|
651
|
+
_toMs(run.createdAt) || now,
|
|
652
|
+
JSON.stringify(run),
|
|
653
|
+
);
|
|
654
|
+
wrote = true;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
for (const [id] of beforeMap) {
|
|
658
|
+
if (!afterMap.has(id)) { del.run(id); wrote = true; }
|
|
659
|
+
}
|
|
660
|
+
return { wrote, result: after };
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function _mirrorQaRunsJson(filePath) {
|
|
665
|
+
try {
|
|
666
|
+
const shared = require('./shared');
|
|
667
|
+
const { getDb } = require('./db');
|
|
668
|
+
const arr = _readQaRunsFromSqlOnly(getDb());
|
|
669
|
+
const target = filePath || _resolveFilePath('qa-runs.json');
|
|
670
|
+
shared.safeWrite(target, arr);
|
|
671
|
+
const h = _fileContentHash(target);
|
|
672
|
+
if (h != null) _qaRunsHash = h;
|
|
673
|
+
} catch { /* mirror best-effort */ }
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// ─── qa_sessions ───────────────────────────────────────────────────────────
|
|
677
|
+
// Shape: [ {id, state, spec:{...}, primaryProject, coServices, setupStatus,
|
|
678
|
+
// workItems, managedSpawnName, qaRunId, testFile, summary,
|
|
679
|
+
// failureClass, error, createdAt, updatedAt, completedAt, ...}, ... ]
|
|
680
|
+
// SQL: row per id, extracted query columns + JSON blob.
|
|
681
|
+
|
|
682
|
+
let _qaSessionsHash = null;
|
|
683
|
+
|
|
684
|
+
function _qaSessionPrimaryProject(session) {
|
|
685
|
+
if (!session || typeof session !== 'object') return null;
|
|
686
|
+
if (session.primaryProject) return String(session.primaryProject);
|
|
687
|
+
if (session.spec && session.spec.project) return String(session.spec.project);
|
|
688
|
+
if (session.spec && Array.isArray(session.spec.projects) && session.spec.projects[0]) {
|
|
689
|
+
return String(session.spec.projects[0]);
|
|
690
|
+
}
|
|
691
|
+
return null;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function _hydrateQaSessions(db) {
|
|
695
|
+
const fp = _resolveFilePath('qa-sessions.json');
|
|
696
|
+
const raw = _readJson(fp) || [];
|
|
697
|
+
if (!Array.isArray(raw)) return;
|
|
698
|
+
db.prepare('DELETE FROM qa_sessions').run();
|
|
699
|
+
const now = Date.now();
|
|
700
|
+
const ins = db.prepare(`
|
|
701
|
+
INSERT INTO qa_sessions (id, state, primary_project, qa_run_id, created_at, updated_at, completed_at, data)
|
|
702
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
703
|
+
ON CONFLICT(id) DO NOTHING
|
|
704
|
+
`);
|
|
705
|
+
for (const session of raw) {
|
|
706
|
+
if (!session || !session.id) continue;
|
|
707
|
+
ins.run(
|
|
708
|
+
String(session.id),
|
|
709
|
+
String(session.state || 'pending'),
|
|
710
|
+
_qaSessionPrimaryProject(session),
|
|
711
|
+
session.qaRunId || null,
|
|
712
|
+
_toMs(session.createdAt) || now,
|
|
713
|
+
_toMs(session.updatedAt) || _toMs(session.createdAt) || now,
|
|
714
|
+
_toMs(session.completedAt),
|
|
715
|
+
JSON.stringify(session),
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function _resyncQaSessionsIfDiverged(db) {
|
|
721
|
+
const fp = _resolveFilePath('qa-sessions.json');
|
|
722
|
+
const currentHash = _fileContentHash(fp);
|
|
723
|
+
if (currentHash == null) return;
|
|
724
|
+
if (_qaSessionsHash != null && currentHash === _qaSessionsHash) return;
|
|
725
|
+
if (_qaSessionsHash == null) {
|
|
726
|
+
const sqlHas = db.prepare('SELECT 1 FROM qa_sessions LIMIT 1').get();
|
|
727
|
+
if (sqlHas) { _qaSessionsHash = currentHash; return; }
|
|
728
|
+
}
|
|
729
|
+
_hydrateQaSessions(db);
|
|
730
|
+
_qaSessionsHash = currentHash;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function _readQaSessionsFromSqlOnly(db) {
|
|
734
|
+
const rows = db.prepare('SELECT data FROM qa_sessions ORDER BY created_at, rowid').all();
|
|
735
|
+
const out = [];
|
|
736
|
+
for (const row of rows) {
|
|
737
|
+
try { out.push(JSON.parse(row.data)); } catch { /* skip */ }
|
|
738
|
+
}
|
|
739
|
+
return out;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function readQaSessions() {
|
|
743
|
+
const { getDb } = require('./db');
|
|
744
|
+
let db;
|
|
745
|
+
try { db = getDb(); }
|
|
746
|
+
catch { return _readJson(_resolveFilePath('qa-sessions.json')) || []; }
|
|
747
|
+
_resyncQaSessionsIfDiverged(db);
|
|
748
|
+
const out = _readQaSessionsFromSqlOnly(db);
|
|
749
|
+
if (out.length === 0) {
|
|
750
|
+
const fallback = _readJson(_resolveFilePath('qa-sessions.json'));
|
|
751
|
+
if (Array.isArray(fallback) && fallback.length > 0) return fallback;
|
|
752
|
+
return [];
|
|
753
|
+
}
|
|
754
|
+
return out;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function applyQaSessionsMutation(mutator) {
|
|
758
|
+
const { getDb, withTransaction } = require('./db');
|
|
759
|
+
let db;
|
|
760
|
+
try { db = getDb(); }
|
|
761
|
+
catch (e) { throw new Error(`small-state-store: SQLite unavailable (${e.message})`); }
|
|
762
|
+
|
|
763
|
+
return withTransaction(db, () => {
|
|
764
|
+
_resyncQaSessionsIfDiverged(db);
|
|
765
|
+
const before = _readQaSessionsFromSqlOnly(db);
|
|
766
|
+
const beforeSnap = JSON.parse(JSON.stringify(before));
|
|
767
|
+
const next = mutator(before);
|
|
768
|
+
const after = (next === undefined || next === null)
|
|
769
|
+
? before
|
|
770
|
+
: (Array.isArray(next) ? next : before);
|
|
771
|
+
|
|
772
|
+
const indexById = (arr) => {
|
|
773
|
+
const out = new Map();
|
|
774
|
+
for (const s of arr) {
|
|
775
|
+
if (s && s.id) out.set(String(s.id), s);
|
|
776
|
+
}
|
|
777
|
+
return out;
|
|
778
|
+
};
|
|
779
|
+
const beforeMap = indexById(beforeSnap);
|
|
780
|
+
const afterMap = indexById(after);
|
|
781
|
+
|
|
782
|
+
const now = Date.now();
|
|
783
|
+
const upsert = db.prepare(`
|
|
784
|
+
INSERT INTO qa_sessions (id, state, primary_project, qa_run_id, created_at, updated_at, completed_at, data)
|
|
785
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
786
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
787
|
+
state = excluded.state,
|
|
788
|
+
primary_project = excluded.primary_project,
|
|
789
|
+
qa_run_id = excluded.qa_run_id,
|
|
790
|
+
created_at = excluded.created_at,
|
|
791
|
+
updated_at = excluded.updated_at,
|
|
792
|
+
completed_at = excluded.completed_at,
|
|
793
|
+
data = excluded.data
|
|
794
|
+
`);
|
|
795
|
+
const del = db.prepare('DELETE FROM qa_sessions WHERE id = ?');
|
|
796
|
+
let wrote = false;
|
|
797
|
+
for (const [id, session] of afterMap) {
|
|
798
|
+
const prev = beforeMap.get(id);
|
|
799
|
+
if (!prev || JSON.stringify(prev) !== JSON.stringify(session)) {
|
|
800
|
+
upsert.run(
|
|
801
|
+
id,
|
|
802
|
+
String(session.state || 'pending'),
|
|
803
|
+
_qaSessionPrimaryProject(session),
|
|
804
|
+
session.qaRunId || null,
|
|
805
|
+
_toMs(session.createdAt) || now,
|
|
806
|
+
_toMs(session.updatedAt) || _toMs(session.createdAt) || now,
|
|
807
|
+
_toMs(session.completedAt),
|
|
808
|
+
JSON.stringify(session),
|
|
809
|
+
);
|
|
810
|
+
wrote = true;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
for (const [id] of beforeMap) {
|
|
814
|
+
if (!afterMap.has(id)) { del.run(id); wrote = true; }
|
|
815
|
+
}
|
|
816
|
+
return { wrote, result: after };
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function _mirrorQaSessionsJson(filePath) {
|
|
821
|
+
try {
|
|
822
|
+
const shared = require('./shared');
|
|
823
|
+
const { getDb } = require('./db');
|
|
824
|
+
const arr = _readQaSessionsFromSqlOnly(getDb());
|
|
825
|
+
const target = filePath || _resolveFilePath('qa-sessions.json');
|
|
826
|
+
shared.safeWrite(target, arr);
|
|
827
|
+
const h = _fileContentHash(target);
|
|
828
|
+
if (h != null) _qaSessionsHash = h;
|
|
829
|
+
} catch { /* mirror best-effort */ }
|
|
830
|
+
}
|
|
831
|
+
|
|
524
832
|
// ─── Test seam ─────────────────────────────────────────────────────────────
|
|
525
833
|
|
|
526
834
|
function _resetAllForTest() {
|
|
@@ -531,11 +839,15 @@ function _resetAllForTest() {
|
|
|
531
839
|
db.exec('DELETE FROM pipeline_runs');
|
|
532
840
|
db.exec('DELETE FROM managed_processes');
|
|
533
841
|
db.exec('DELETE FROM worktree_pool');
|
|
842
|
+
try { db.exec('DELETE FROM qa_runs'); } catch { /* migration not applied */ }
|
|
843
|
+
try { db.exec('DELETE FROM qa_sessions'); } catch { /* migration not applied */ }
|
|
534
844
|
} catch { /* not initialized */ }
|
|
535
845
|
_scheduleRunsHash = null;
|
|
536
846
|
_pipelineRunsHash = null;
|
|
537
847
|
_managedProcessesHash = null;
|
|
538
848
|
_worktreePoolHash = null;
|
|
849
|
+
_qaRunsHash = null;
|
|
850
|
+
_qaSessionsHash = null;
|
|
539
851
|
}
|
|
540
852
|
|
|
541
853
|
module.exports = {
|
|
@@ -555,6 +867,14 @@ module.exports = {
|
|
|
555
867
|
readWorktreePool,
|
|
556
868
|
applyWorktreePoolMutation,
|
|
557
869
|
_mirrorWorktreePoolJson,
|
|
870
|
+
// qa_runs
|
|
871
|
+
readQaRuns,
|
|
872
|
+
applyQaRunsMutation,
|
|
873
|
+
_mirrorQaRunsJson,
|
|
874
|
+
// qa_sessions
|
|
875
|
+
readQaSessions,
|
|
876
|
+
applyQaSessionsMutation,
|
|
877
|
+
_mirrorQaSessionsJson,
|
|
558
878
|
// test seam
|
|
559
879
|
_resetAllForTest,
|
|
560
880
|
};
|
package/engine.js
CHANGED
|
@@ -877,7 +877,15 @@ async function pruneStaleWorktreeForBranch(rootDir, branchName, gitOpts) {
|
|
|
877
877
|
// can recover any unpushed work.
|
|
878
878
|
// - Otherwise (filesystem-only dirty) reset --hard + clean -fd, then
|
|
879
879
|
// re-verify.
|
|
880
|
-
// 4.
|
|
880
|
+
// 4. W-mpykcky7: when the branch has no upstream AND the worktree is
|
|
881
|
+
// filesystem-clean AND HEAD is at the project main branch tip (caller
|
|
882
|
+
// passes opts.mainBranch), treat as safe-to-reuse — there are no
|
|
883
|
+
// local-only commits to lose. Without this, every reused worktree
|
|
884
|
+
// whose previous agent exited without making any commits gets
|
|
885
|
+
// quarantined in a false-positive loop. Any uncertainty (rev-list
|
|
886
|
+
// error, main missing, commitsAheadOfMain > 0) falls through to the
|
|
887
|
+
// conservative quarantine path.
|
|
888
|
+
// 5. Return { clean, healed, reason, dirtyFiles, ahead, behind,
|
|
881
889
|
// quarantined, quarantinedPath, backupRef } so the caller can fail
|
|
882
890
|
// fast with a first-class DIRTY_WORKTREE / WORKTREE_DIRTY /
|
|
883
891
|
// WORKTREE_DIVERGENT reason and retry semantics.
|
|
@@ -955,7 +963,42 @@ async function assertCleanSharedWorktree(rootDir, worktreePath, branchName, disp
|
|
|
955
963
|
return result;
|
|
956
964
|
}
|
|
957
965
|
|
|
958
|
-
// 4.
|
|
966
|
+
// 4. No-upstream-but-at-main safe-path (W-mpykcky7). When the worktree's
|
|
967
|
+
// branch has no upstream (never pushed), the original #2996 gate
|
|
968
|
+
// conservatively assumed unpushed work and quarantined. But if the
|
|
969
|
+
// worktree is filesystem-clean AND HEAD is at the project main branch
|
|
970
|
+
// tip (no local-only commits beyond main), there is nothing to lose —
|
|
971
|
+
// reuse is safe. Common case: a previous agent exited without making
|
|
972
|
+
// any commits, leaving HEAD == origin/<mainBranch>. Falls through to
|
|
973
|
+
// the conservative quarantine path on any uncertainty (rev-list error,
|
|
974
|
+
// main branch missing, commitsAheadOfMain > 0).
|
|
975
|
+
// Invariants preserved from #2996:
|
|
976
|
+
// - filesystemDirty still quarantines (we're past that gate only if
|
|
977
|
+
// statusOut was empty).
|
|
978
|
+
// - upstreamKnown && ahead > 0 still quarantines below (untouched).
|
|
979
|
+
// - other-dispatch-active (above) still returns without quarantine.
|
|
980
|
+
if (!filesystemDirty && !upstreamKnown && opts.mainBranch) {
|
|
981
|
+
let commitsAheadOfMain = null;
|
|
982
|
+
try {
|
|
983
|
+
const r = await execAsync(
|
|
984
|
+
`git rev-list ${opts.mainBranch}..HEAD --count`,
|
|
985
|
+
{ ...gitOpts, cwd: worktreePath, timeout: 10000 },
|
|
986
|
+
);
|
|
987
|
+
const parsed = parseInt(String(r || '').trim(), 10);
|
|
988
|
+
if (Number.isFinite(parsed)) commitsAheadOfMain = parsed;
|
|
989
|
+
} catch {
|
|
990
|
+
// rev-list error (main missing locally, fetch failed, etc.) — fall
|
|
991
|
+
// through to the conservative quarantine path. Don't loosen on
|
|
992
|
+
// uncertainty.
|
|
993
|
+
}
|
|
994
|
+
if (commitsAheadOfMain === 0) {
|
|
995
|
+
result.clean = true;
|
|
996
|
+
result.reason = 'no-upstream-but-at-main';
|
|
997
|
+
return result;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// 5. Unpushed-commit check — refuse to reset when local work would be lost.
|
|
959
1002
|
// Use the ahead count we already computed (cheaper + correct vs.
|
|
960
1003
|
// `git log @{u}..HEAD` which needs an upstream config and silently
|
|
961
1004
|
// returns empty when missing).
|
|
@@ -985,7 +1028,7 @@ async function assertCleanSharedWorktree(rootDir, worktreePath, branchName, disp
|
|
|
985
1028
|
return result;
|
|
986
1029
|
}
|
|
987
1030
|
|
|
988
|
-
//
|
|
1031
|
+
// 6. Safe to self-heal (filesystem-dirty only, no unpushed commits, no
|
|
989
1032
|
// other active dispatch): reset + clean.
|
|
990
1033
|
try {
|
|
991
1034
|
await execAsync('git reset --hard HEAD', { ...gitOpts, cwd: worktreePath, timeout: 30000 });
|
|
@@ -996,7 +1039,7 @@ async function assertCleanSharedWorktree(rootDir, worktreePath, branchName, disp
|
|
|
996
1039
|
return result;
|
|
997
1040
|
}
|
|
998
1041
|
|
|
999
|
-
//
|
|
1042
|
+
// 7. Re-verify
|
|
1000
1043
|
try {
|
|
1001
1044
|
const r2 = await execAsync('git status --porcelain', { ...gitOpts, cwd: worktreePath, timeout: 10000 });
|
|
1002
1045
|
const after = (r2 || '').toString().trim();
|
|
@@ -1759,7 +1802,10 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1759
1802
|
// implementation failures and cascading dependent items.
|
|
1760
1803
|
if (worktreePath && fs.existsSync(worktreePath) && meta?.branchStrategy === 'shared-branch' && branchName) {
|
|
1761
1804
|
_phaseT.cleanCheckStart = Date.now();
|
|
1762
|
-
const cleanResult = await assertCleanSharedWorktree(
|
|
1805
|
+
const cleanResult = await assertCleanSharedWorktree(
|
|
1806
|
+
rootDir, worktreePath, branchName, id, _gitOpts,
|
|
1807
|
+
{ mainBranch: shared.resolveMainBranch(rootDir, project.mainBranch) },
|
|
1808
|
+
);
|
|
1763
1809
|
_phaseT.cleanCheckEnd = Date.now();
|
|
1764
1810
|
if (!cleanResult.clean) {
|
|
1765
1811
|
const previewFiles = (cleanResult.dirtyFiles || []).slice(0, 5).join(', ');
|
|
@@ -1817,7 +1863,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1817
1863
|
_phaseT.dirtyReusedCheckStart = Date.now();
|
|
1818
1864
|
const cleanResult = await assertCleanSharedWorktree(
|
|
1819
1865
|
rootDir, worktreePath, branchName, id, _gitOpts,
|
|
1820
|
-
{ quarantineOnUnsafe: true },
|
|
1866
|
+
{ quarantineOnUnsafe: true, mainBranch: shared.resolveMainBranch(rootDir, project.mainBranch) },
|
|
1821
1867
|
);
|
|
1822
1868
|
_phaseT.dirtyReusedCheckEnd = Date.now();
|
|
1823
1869
|
if (!cleanResult.clean) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2110",
|
|
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"
|