@yemi33/minions 0.1.2107 → 0.1.2109

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.
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2107",
3
+ "version": "0.1.2109",
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"