@yemi33/minions 0.1.2424 → 0.1.2426

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.
Files changed (185) hide show
  1. package/bin/install-internal-minions.js +1209 -0
  2. package/bin/minions.js +75 -9
  3. package/dashboard/docs/typography.md +27 -12
  4. package/dashboard/js/command-center.js +13 -11
  5. package/dashboard/js/live-stream.js +1 -1
  6. package/dashboard/js/memory-search.js +388 -43
  7. package/dashboard/js/modal-qa.js +5 -5
  8. package/dashboard/js/qa.js +39 -26
  9. package/dashboard/js/refresh.js +37 -18
  10. package/dashboard/js/render-agents.js +14 -18
  11. package/dashboard/js/render-dispatch.js +14 -74
  12. package/dashboard/js/render-inbox.js +3 -3
  13. package/dashboard/js/render-meetings.js +8 -8
  14. package/dashboard/js/render-other.js +45 -20
  15. package/dashboard/js/render-pipelines.js +6 -6
  16. package/dashboard/js/render-plans.js +14 -14
  17. package/dashboard/js/render-prd.js +46 -46
  18. package/dashboard/js/render-prs.js +9 -65
  19. package/dashboard/js/render-schedules.js +5 -5
  20. package/dashboard/js/render-skills.js +6 -6
  21. package/dashboard/js/render-utils.js +4 -4
  22. package/dashboard/js/render-watches.js +4 -16
  23. package/dashboard/js/render-work-items.js +95 -107
  24. package/dashboard/js/settings.js +175 -74
  25. package/dashboard/layout.html +3 -2
  26. package/dashboard/pages/inbox.html +1 -1
  27. package/dashboard/pages/work.html +1 -1
  28. package/dashboard/shared/model-display.js +9 -0
  29. package/dashboard/shared/watches-source.js +43 -0
  30. package/dashboard/shared/welcome-popup.js +259 -0
  31. package/dashboard/slim/body.html +6 -6
  32. package/dashboard/slim/js/chat.js +4 -2
  33. package/dashboard/slim/js/history.js +10 -3
  34. package/dashboard/slim/js/modals-tiles.js +85 -16
  35. package/dashboard/slim/js/status.js +12 -16
  36. package/dashboard/slim/styles.css +24 -4
  37. package/dashboard/styles.css +62 -27
  38. package/dashboard-build.js +8 -2
  39. package/dashboard.js +3854 -2514
  40. package/docs/README.md +7 -5
  41. package/docs/api-errors.md +144 -0
  42. package/docs/auto-discovery.md +83 -61
  43. package/docs/capture-demos.js +173 -30
  44. package/docs/command-center.md +5 -1
  45. package/docs/completion-reports.md +127 -11
  46. package/docs/constants.md +15 -3
  47. package/docs/constellation-style-telemetry.md +2 -2
  48. package/docs/copilot-cli-schema.md +32 -22
  49. package/docs/cross-repo-plans.md +30 -30
  50. package/docs/demo/memory-system.html +1 -1
  51. package/docs/deprecated.json +132 -18
  52. package/docs/diagnostics-memory.md +8 -4
  53. package/docs/human-vs-automated.md +2 -2
  54. package/docs/index.html +9 -2
  55. package/docs/internal-install.md +212 -0
  56. package/docs/kb-pr3223-cascade-archiving.md +16 -0
  57. package/docs/kb-pr696-merge-conflict-docs.md +23 -0
  58. package/docs/kb-sweep.md +2 -2
  59. package/docs/keep-processes.md +8 -1
  60. package/docs/live-checkout-mode.md +1 -1
  61. package/docs/managed-spawn.md +21 -3
  62. package/docs/named-agents.md +3 -2
  63. package/docs/onboarding.md +26 -0
  64. package/docs/plan-lifecycle.md +32 -32
  65. package/docs/pr-screenshots/pr-899/worker-pool-worktrees-AFTER.png +0 -0
  66. package/docs/pr-screenshots/pr-899/worker-pool-worktrees-BEFORE.png +0 -0
  67. package/docs/pr-screenshots/pr-979/auto-fix-pane-AFTER.png +0 -0
  68. package/docs/pr-screenshots/pr-979/auto-fix-pane-BEFORE.png +0 -0
  69. package/docs/pr-screenshots/pr-985/pr-column-em-dash-AFTER.png +0 -0
  70. package/docs/pr-screenshots/pr-985/pr-column-em-dash-BEFORE.png +0 -0
  71. package/docs/qa-runbook-lifecycle.md +67 -51
  72. package/docs/qa-runbooks.md +2 -2
  73. package/docs/runtime-adapters.md +101 -50
  74. package/docs/security.md +28 -0
  75. package/docs/self-improvement.md +47 -13
  76. package/docs/skills.md +29 -0
  77. package/docs/slim-ux/concepts.md +2 -2
  78. package/docs/specs/agent-configurability.md +43 -41
  79. package/docs/specs/agent-rename.md +13 -13
  80. package/docs/team-memory.md +283 -14
  81. package/docs/tutorials/01-install-and-connect.md +23 -0
  82. package/docs/tutorials/03-plan-driven-feature.md +3 -3
  83. package/docs/tutorials/04-runtimes-and-harness.md +18 -21
  84. package/docs/tutorials/08-operations-and-recovery.md +3 -2
  85. package/docs/tutorials/README.md +4 -3
  86. package/docs/visual-evidence-ci.md +103 -0
  87. package/docs/watches.md +35 -30
  88. package/docs/worktree-lifecycle.md +151 -13
  89. package/engine/acp-transport.js +1 -0
  90. package/engine/ado.js +80 -33
  91. package/engine/agent-api-validation.js +571 -0
  92. package/engine/agent-worker-pool.js +7 -6
  93. package/engine/api-contracts/agent-content.js +761 -0
  94. package/engine/api-contracts/cc-ops.js +219 -0
  95. package/engine/api-contracts/config-runtime.js +419 -0
  96. package/engine/api-contracts/core.js +138 -0
  97. package/engine/api-contracts/index.js +506 -0
  98. package/engine/api-contracts/orchestration.js +602 -0
  99. package/engine/api-contracts/pull-requests.js +323 -0
  100. package/engine/api-contracts/qa-process.js +764 -0
  101. package/engine/api-contracts/work-plan-prd.js +494 -0
  102. package/engine/api-validation.js +543 -0
  103. package/engine/cc-api-validation.js +586 -0
  104. package/engine/cc-worker-pool.js +9 -1
  105. package/engine/cleanup.js +393 -271
  106. package/engine/cli.js +65 -7
  107. package/engine/comment-classifier.js +70 -5
  108. package/engine/consolidation.js +123 -5
  109. package/engine/content-api-validation.js +606 -0
  110. package/engine/create-pr-worktree.js +22 -1
  111. package/engine/db/migrations/027-review-learning-lifecycle.js +162 -0
  112. package/engine/dispatch.js +33 -0
  113. package/engine/distribution.js +189 -0
  114. package/engine/execution-model.js +59 -0
  115. package/engine/features.js +12 -0
  116. package/engine/github.js +67 -25
  117. package/engine/harness-context.js +313 -0
  118. package/engine/inbox-store.js +1 -1
  119. package/engine/kb-sweep.js +161 -38
  120. package/engine/keep-process-sweep.js +43 -3
  121. package/engine/lifecycle.js +1066 -165
  122. package/engine/llm.js +4 -0
  123. package/engine/managed-spawn.js +463 -48
  124. package/engine/meeting.js +197 -34
  125. package/engine/memory-retrieval.js +280 -0
  126. package/engine/memory-store.js +821 -38
  127. package/engine/model-discovery.js +17 -3
  128. package/engine/pipeline.js +802 -14
  129. package/engine/plan-prd-validation.js +745 -0
  130. package/engine/playbook.js +206 -17
  131. package/engine/pooled-agent-process.js +6 -1
  132. package/engine/pr-action.js +1 -1
  133. package/engine/pr-issue-validation.js +653 -0
  134. package/engine/pr-resolve.js +75 -13
  135. package/engine/prd-store.js +39 -6
  136. package/engine/preflight.js +7 -2
  137. package/engine/process-utils.js +79 -31
  138. package/engine/projects.js +198 -12
  139. package/engine/promotion.js +371 -0
  140. package/engine/qa-from-prd.js +3 -0
  141. package/engine/qa-process-validation.js +572 -0
  142. package/engine/qa-runbooks.js +41 -37
  143. package/engine/qa-runners/maestro.js +11 -7
  144. package/engine/qa-runners/playwright.js +37 -14
  145. package/engine/qa-runners.js +9 -10
  146. package/engine/qa-runs.js +124 -11
  147. package/engine/qa-sessions.js +139 -40
  148. package/engine/quarantine-refs.js +81 -11
  149. package/engine/queries.js +163 -9
  150. package/engine/review-learning-backfill.js +482 -0
  151. package/engine/review-learning.js +1236 -0
  152. package/engine/runtimes/claude.js +60 -2
  153. package/engine/runtimes/codex.js +57 -16
  154. package/engine/runtimes/contract.js +21 -0
  155. package/engine/runtimes/copilot.js +51 -3
  156. package/engine/runtimes/index.js +1 -0
  157. package/engine/scheduler.js +201 -32
  158. package/engine/settings-validation.js +909 -0
  159. package/engine/shared.js +705 -208
  160. package/engine/spawn-agent.js +48 -21
  161. package/engine/steering-constraints.js +31 -0
  162. package/engine/steering-store.js +10 -2
  163. package/engine/steering.js +14 -2
  164. package/engine/timeout.js +404 -111
  165. package/engine/untrusted-fence.js +24 -10
  166. package/engine/watch-actions.js +7 -2
  167. package/engine/watches.js +353 -51
  168. package/engine/work-item-validation.js +561 -0
  169. package/engine/work-items-store.js +61 -1
  170. package/engine/worktree-gc.js +656 -37
  171. package/engine/worktree-preflight.js +154 -0
  172. package/engine.js +2068 -342
  173. package/package.json +1 -1
  174. package/playbooks/_pr-description-audit.md +88 -41
  175. package/playbooks/build-fix-complex.md +3 -3
  176. package/playbooks/docs.md +2 -2
  177. package/playbooks/fix.md +18 -18
  178. package/playbooks/implement.md +18 -18
  179. package/playbooks/qa-session-draft.md +10 -12
  180. package/playbooks/qa-session-execute.md +16 -12
  181. package/playbooks/qa-validate.md +8 -7
  182. package/playbooks/shared-rules.md +45 -0
  183. package/playbooks/test.md +4 -0
  184. package/prompts/cc-system.md +1 -1
  185. package/skills/check-self-authored-review-comment/SKILL.md +2 -2
@@ -0,0 +1,162 @@
1
+ // Review-learning lifecycle fields extend migration 017 in place. Existing
2
+ // memory rows and FTS content stay intact; only exact generated feedback
3
+ // provenance is moved out of the recallable `active` state.
4
+
5
+ function _hasColumn(db, table, column) {
6
+ return db.prepare(`PRAGMA table_info(${table})`).all().some(row => row.name === column);
7
+ }
8
+
9
+ function _addColumn(db, table, definition) {
10
+ const column = definition.trim().split(/\s+/, 1)[0];
11
+ if (!_hasColumn(db, table, column)) db.exec(`ALTER TABLE ${table} ADD COLUMN ${definition}`);
12
+ }
13
+
14
+ function _parseObject(value) {
15
+ try {
16
+ const parsed = JSON.parse(value);
17
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
18
+ } catch {
19
+ return {};
20
+ }
21
+ }
22
+
23
+ // Mirror engine/review-learning.js#normalizeCanonicalPrId's space-compatible
24
+ // contract so the quarantine accepts valid canonical IDs whose ADO
25
+ // org/project/repo segments contain spaces (or %20) — createReviewFeedbackForAuthor
26
+ // writes the raw canonical `pr.id`, and valid ADO scopes may contain spaces.
27
+ // Kept self-contained: migrations must not import runtime engine modules.
28
+ function _isCanonicalPrId(value) {
29
+ const match = String(value == null ? '' : value).trim().match(/^(github|ado):(.+?)#([1-9]\d*)$/i);
30
+ if (!match) return false;
31
+ const host = match[1].toLowerCase();
32
+ const expectedSegments = host === 'github' ? 2 : 3;
33
+ const segments = match[2]
34
+ .split('/')
35
+ .map(segment => segment.replace(/%20/gi, ' ').trim().toLowerCase());
36
+ if (segments.length !== expectedSegments) return false;
37
+ return segments.every(segment => (
38
+ segment
39
+ && !/[|#\u0000-\u001f\u007f]/.test(segment)
40
+ && (host !== 'github' || /^[a-z0-9][a-z0-9._-]*$/.test(segment))
41
+ ));
42
+ }
43
+
44
+ function _frontmatterBlocks(body) {
45
+ const blocks = [];
46
+ const re = /^---\r?\n([\s\S]*?)\r?\n---/gm;
47
+ let match;
48
+ while ((match = re.exec(String(body || ''))) !== null) {
49
+ const fields = {};
50
+ for (const line of match[1].split(/\r?\n/)) {
51
+ const field = line.match(/^([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$/);
52
+ if (field) fields[field[1].toLowerCase()] = field[2].trim();
53
+ }
54
+ blocks.push(fields);
55
+ }
56
+ return blocks;
57
+ }
58
+
59
+ function _generatedFeedbackProvenance(row) {
60
+ if (row.source_type !== 'knowledge-base' || row.status !== 'active') return null;
61
+ if (!/^knowledge\/.+\.md$/i.test(String(row.source_path || ''))) return null;
62
+
63
+ const metadata = _parseObject(row.metadata);
64
+ const blocks = _frontmatterBlocks(row.body);
65
+ const sourceBlock = blocks.find(block => block.source && block.agent);
66
+ const detailBlock = blocks.find(block => block.sourcepr && block.reviewer && block.author);
67
+ if (!sourceBlock) return null;
68
+ if (String(sourceBlock.agent).toLowerCase() !== 'feedback') return null;
69
+ if (metadata.agent != null && String(metadata.agent).toLowerCase() !== 'feedback') return null;
70
+
71
+ const sourceRef = String(sourceBlock.source || '').trim();
72
+ if (!/^feedback-[a-z][a-z0-9-]{1,63}-from-[a-z][a-z0-9-]{1,63}-.+-\d{4}-\d{2}-\d{2}\.md$/.test(sourceRef)) {
73
+ return null;
74
+ }
75
+ if (detailBlock) {
76
+ const author = String(detailBlock.author || '').trim().toLowerCase();
77
+ const reviewer = String(detailBlock.reviewer || '').trim().toLowerCase();
78
+ if (String(detailBlock.agent || 'feedback').toLowerCase() !== 'feedback') return null;
79
+ if (!/^[a-z][a-z0-9-]{1,63}$/.test(author) || !/^[a-z][a-z0-9-]{1,63}$/.test(reviewer)) return null;
80
+ if (!sourceRef.startsWith(`feedback-${author}-from-${reviewer}-`)) return null;
81
+ if (!_isCanonicalPrId(detailBlock.sourcepr)) return null;
82
+ }
83
+ if (metadata.inbox != null && String(metadata.inbox) !== sourceRef) return null;
84
+ if (row.source_ref !== sourceRef && row.source_ref !== row.source_path) return null;
85
+ if (!/^# Review Feedback for .+/m.test(String(row.body || ''))) return null;
86
+ return { metadata, sourceRef };
87
+ }
88
+
89
+ function _quarantineGeneratedFeedback(db) {
90
+ const rows = db.prepare(`
91
+ SELECT id, source_type, source_path, source_ref, status, body, metadata
92
+ FROM memory_records
93
+ WHERE status='active' AND source_type='knowledge-base'
94
+ `).all();
95
+ const update = db.prepare(`
96
+ UPDATE memory_records
97
+ SET status='candidate', metadata=?, updated_at=?
98
+ WHERE id=? AND status='active'
99
+ `);
100
+ const now = Date.now();
101
+ for (const row of rows) {
102
+ const provenance = _generatedFeedbackProvenance(row);
103
+ if (!provenance) continue;
104
+ update.run(JSON.stringify({
105
+ ...provenance.metadata,
106
+ legacyUnverified: true,
107
+ candidateState: 'legacy-unverified',
108
+ legacySourceType: row.source_type,
109
+ legacySourceRef: provenance.sourceRef,
110
+ quarantinedBy: 'migration-027',
111
+ }), now, row.id);
112
+ }
113
+ }
114
+
115
+ module.exports = {
116
+ version: 27,
117
+ description: 'review-learning candidates, retrieval traces, and lifecycle events',
118
+ up(db) {
119
+ _addColumn(db, 'memory_retrieval_runs', 'dispatch_id TEXT');
120
+ _addColumn(db, 'memory_retrieval_runs', "retrieval_kind TEXT NOT NULL DEFAULT 'general'");
121
+ _addColumn(db, 'memory_retrieval_runs', "selected_ids TEXT NOT NULL DEFAULT '[]'");
122
+ _addColumn(db, 'memory_retrieval_runs', "decision_trace TEXT NOT NULL DEFAULT '[]'");
123
+
124
+ db.exec(`
125
+ CREATE INDEX IF NOT EXISTS idx_memory_retrieval_dispatch_kind
126
+ ON memory_retrieval_runs(dispatch_id, retrieval_kind, created_at DESC);
127
+
128
+ CREATE TABLE IF NOT EXISTS memory_lifecycle_events (
129
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
130
+ event_key TEXT NOT NULL UNIQUE CHECK(length(event_key) <= 64),
131
+ event_type TEXT NOT NULL CHECK(length(event_type) <= 64),
132
+ memory_id TEXT,
133
+ finding_key TEXT,
134
+ project TEXT NOT NULL CHECK(length(project) <= 120),
135
+ agent TEXT,
136
+ area TEXT NOT NULL DEFAULT 'general' CHECK(length(area) <= 360),
137
+ disposition TEXT,
138
+ work_item_id TEXT,
139
+ dispatch_id TEXT,
140
+ metadata TEXT NOT NULL DEFAULT '{}' CHECK(length(metadata) <= 8192),
141
+ created_at INTEGER NOT NULL
142
+ );
143
+ CREATE INDEX IF NOT EXISTS idx_memory_lifecycle_project_area_created
144
+ ON memory_lifecycle_events(project, area, created_at DESC);
145
+ CREATE INDEX IF NOT EXISTS idx_memory_lifecycle_finding
146
+ ON memory_lifecycle_events(finding_key, event_type, created_at DESC);
147
+ CREATE INDEX IF NOT EXISTS idx_memory_lifecycle_memory
148
+ ON memory_lifecycle_events(memory_id, created_at DESC);
149
+ CREATE INDEX IF NOT EXISTS idx_memory_lifecycle_dispatch
150
+ ON memory_lifecycle_events(dispatch_id, created_at DESC);
151
+
152
+ CREATE INDEX IF NOT EXISTS idx_memory_review_learning_active
153
+ ON memory_records(scope_key, updated_at DESC)
154
+ WHERE source_type='review-learning'
155
+ AND memory_type='procedural'
156
+ AND scope_type='project'
157
+ AND status='active';
158
+ `);
159
+
160
+ _quarantineGeneratedFeedback(db);
161
+ },
162
+ };
@@ -977,6 +977,35 @@ function markCompletedSourceWorkItem(item) {
977
977
 
978
978
  // ─── Complete Dispatch ───────────────────────────────────────────────────────
979
979
 
980
+ const WORKTREE_PROTECTION_RETRY_MS = 60 * 1000;
981
+
982
+ function deferDispatchForWorktreeProtection(id, protection = {}, terminal = {}) {
983
+ let deferred = false;
984
+ const now = Date.now();
985
+ mutateDispatch((dispatch) => {
986
+ const item = (dispatch.active || []).find(entry => entry.id === id);
987
+ if (!item) return dispatch;
988
+ const previous = item._worktreeProtectionDeferred || {};
989
+ const previousTerminal = previous.terminal || {};
990
+ item._worktreeProtectionDeferred = {
991
+ deferredAt: previous.deferredAt || new Date(now).toISOString(),
992
+ retryAfter: new Date(now + WORKTREE_PROTECTION_RETRY_MS).toISOString(),
993
+ protectionReason: protection.reason || previous.protectionReason || 'preservation-failed',
994
+ ...(protection.error ? { error: String(protection.error) } : {}),
995
+ terminal: {
996
+ reason: terminal.reason || previousTerminal.reason || '',
997
+ resultSummary: terminal.resultSummary || previousTerminal.resultSummary || '',
998
+ completeOpts: terminal.completeOpts && typeof terminal.completeOpts === 'object'
999
+ ? terminal.completeOpts
1000
+ : (previousTerminal.completeOpts || {}),
1001
+ },
1002
+ };
1003
+ deferred = true;
1004
+ return dispatch;
1005
+ });
1006
+ return deferred;
1007
+ }
1008
+
980
1009
  function completeDispatch(id, result = DISPATCH_RESULT.SUCCESS, reason = '', resultSummary = '', opts = {}) {
981
1010
  const {
982
1011
  processWorkItemFailure = true,
@@ -1033,6 +1062,7 @@ function completeDispatch(id, result = DISPATCH_RESULT.SUCCESS, reason = '', res
1033
1062
  delete item.prompt;
1034
1063
  delete item._promptFile;
1035
1064
  delete item._promptBytes;
1065
+ delete item._worktreeProtectionDeferred;
1036
1066
  if (dispatch.completed.length >= 100) {
1037
1067
  dispatch.completed = dispatch.completed.slice(-100);
1038
1068
  }
@@ -1208,6 +1238,8 @@ function completeDispatch(id, result = DISPATCH_RESULT.SUCCESS, reason = '', res
1208
1238
  // Human-readable labels for each failure class — used as fallback when reason is empty
1209
1239
  const CLASS_LABELS = {
1210
1240
  [FAILURE_CLASS.EMPTY_OUTPUT]: 'agent produced no output \u2014 likely crashed on startup',
1241
+ [FAILURE_CLASS.RUNTIME_TRUNCATED_EXIT]: 'runtime process ended mid-turn (exit 0 with an unfinished tool call) \u2014 no terminal result and no completion report; retry re-runs the truncated turn',
1242
+ [FAILURE_CLASS.RUNTIME_SESSION_STALE]: 'resumed runtime session was unusable (conversation purged, or its working directory no longer exists) \u2014 cached session cleared so the retry starts fresh',
1211
1243
  [FAILURE_CLASS.BUILD_FAILURE]: 'build/test/lint failure in output',
1212
1244
  [FAILURE_CLASS.MERGE_CONFLICT]: 'merge conflict',
1213
1245
  [FAILURE_CLASS.DEPENDENCY_MERGE_SETUP]: 'dependency merge setup failed',
@@ -1525,6 +1557,7 @@ module.exports = {
1525
1557
  addToDispatchWithValidation,
1526
1558
  getPrDispatchDedupeKey,
1527
1559
  isRetryableFailureReason,
1560
+ deferDispatchForWorktreeProtection,
1528
1561
  completeDispatch,
1529
1562
  writeInboxAlert,
1530
1563
  updateAgentStatus,
@@ -0,0 +1,189 @@
1
+ // engine/distribution.js — identity of the *installed* Minions distribution.
2
+ //
3
+ // Minions ships from one source tree as two npm packages (tools/prepare-package.js):
4
+ // • public → `@yemi33/minions` (npmjs, yemi33/minions)
5
+ // • internal → `@yemishin_microsoft/minions` (GitHub Packages; overridable
6
+ // at publish time via INTERNAL_PACKAGE_NAME)
7
+ //
8
+ // Distribution identity is derived ONLY from the installed package manifest —
9
+ // the `name` field npm resolved this runtime from. Repository remotes, branch
10
+ // names, checkout paths and config values are all operator-mutable and are
11
+ // deliberately NOT consulted: a public install must never be mistaken for an
12
+ // internal one because someone added an opg remote.
13
+ //
14
+ // Also owns the *installation identity* (`installationId`) that internal-only
15
+ // first-run surfaces key their acknowledgement on. It lives in the SQL
16
+ // small-state `engine_state` blob, so it survives dashboard/engine restarts and
17
+ // package upgrades, while a genuinely new runtime installation (fresh
18
+ // engine/state.db) gets a fresh id. The internal install/state-migration path
19
+ // consumes the same helpers so both agree on what "this installation" means.
20
+
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+
24
+ const PUBLIC_PACKAGE_NAME = '@yemi33/minions';
25
+
26
+ const DISTRIBUTION_CHANNEL = {
27
+ PUBLIC: 'public',
28
+ INTERNAL: 'internal',
29
+ };
30
+
31
+ // Exact copy required by W-ms1jgr95000ke749 — the popup's primary message.
32
+ const INTERNAL_WELCOME_MESSAGE = 'Welcome to the internal Minions!';
33
+
34
+ const INSTALLATION_ID_KEY = 'installationId';
35
+ const INTERNAL_WELCOME_ACK_KEY = 'internalWelcome';
36
+
37
+ // The installed package root: engine/ lives one level under it in both the npm
38
+ // tarball and the git checkout.
39
+ const PACKAGE_ROOT = path.join(__dirname, '..');
40
+
41
+ function _packageJsonPath(opts) {
42
+ if (opts && opts.packageJsonPath) return opts.packageJsonPath;
43
+ return path.join((opts && opts.packageRoot) || PACKAGE_ROOT, 'package.json');
44
+ }
45
+
46
+ // Read `{ name, version }` off the installed manifest. Returns null when the
47
+ // manifest is missing or unparseable — callers treat that as "not internal".
48
+ function readRuntimePackageMeta(opts) {
49
+ try {
50
+ const raw = fs.readFileSync(_packageJsonPath(opts), 'utf8');
51
+ const pkg = JSON.parse(raw);
52
+ if (!pkg || typeof pkg !== 'object' || Array.isArray(pkg)) return null;
53
+ const name = typeof pkg.name === 'string' ? pkg.name.trim() : '';
54
+ const version = typeof pkg.version === 'string' ? pkg.version.trim() : '';
55
+ if (!name) return null;
56
+ return { name, version: version || null };
57
+ } catch {
58
+ return null;
59
+ }
60
+ }
61
+
62
+ // A Minions package is `minions` or `@scope/minions`. Anything else (a vendored
63
+ // fork under a different product name, a corrupt manifest) is not classified as
64
+ // an internal distribution — the internal-only surfaces stay hidden.
65
+ function isMinionsPackageName(name) {
66
+ return typeof name === 'string' && /^(?:@[^/\s]+\/)?minions$/.test(name.trim());
67
+ }
68
+
69
+ // Internal == a recognised Minions package that is NOT the public one.
70
+ // Fail closed: unknown / unreadable / non-Minions names are never internal.
71
+ function isInternalPackageName(name) {
72
+ if (!isMinionsPackageName(name)) return false;
73
+ return name.trim() !== PUBLIC_PACKAGE_NAME;
74
+ }
75
+
76
+ // { packageName, version, channel, isInternal }. `packageName` is null when the
77
+ // manifest could not be read.
78
+ function resolveDistribution(opts) {
79
+ const meta = readRuntimePackageMeta(opts);
80
+ const packageName = meta ? meta.name : null;
81
+ const isInternal = isInternalPackageName(packageName);
82
+ return {
83
+ packageName,
84
+ version: meta ? meta.version : null,
85
+ channel: isInternal ? DISTRIBUTION_CHANNEL.INTERNAL : DISTRIBUTION_CHANNEL.PUBLIC,
86
+ isInternal,
87
+ };
88
+ }
89
+
90
+ function _smallState() {
91
+ return require('./small-state-store');
92
+ }
93
+
94
+ function _newInstallationId() {
95
+ return require('crypto').randomUUID();
96
+ }
97
+
98
+ // Stable per-installation id, created on first read and persisted in SQL.
99
+ // Deliberately NOT derived from a path or hostname: copying ~/.minions to a new
100
+ // machine is the same installation, and two installations on one machine must
101
+ // not collide.
102
+ function resolveInstallationId() {
103
+ const store = _smallState();
104
+ const existing = store.readEngineState()[INSTALLATION_ID_KEY];
105
+ if (typeof existing === 'string' && existing) return existing;
106
+ const generated = _newInstallationId();
107
+ const { result } = store.applyEngineStateMutation((state) => {
108
+ if (typeof state[INSTALLATION_ID_KEY] === 'string' && state[INSTALLATION_ID_KEY]) return state;
109
+ state[INSTALLATION_ID_KEY] = generated;
110
+ return state;
111
+ });
112
+ return result[INSTALLATION_ID_KEY] || generated;
113
+ }
114
+
115
+ // { ackedAt, installationId, packageName } | null
116
+ function readInternalWelcomeAck() {
117
+ const ack = _smallState().readEngineState()[INTERNAL_WELCOME_ACK_KEY];
118
+ if (!ack || typeof ack !== 'object' || Array.isArray(ack)) return null;
119
+ if (typeof ack.ackedAt !== 'string' || !ack.ackedAt) return null;
120
+ return ack;
121
+ }
122
+
123
+ // Record the acknowledgement against the CURRENT installation id, so a package
124
+ // upgrade (same state.db) stays acknowledged while a fresh installation does not
125
+ // inherit it. Idempotent: re-acking keeps the original timestamp.
126
+ function acknowledgeInternalWelcome(opts) {
127
+ const installationId = resolveInstallationId();
128
+ const distribution = resolveDistribution(opts);
129
+ const store = _smallState();
130
+ const { result } = store.applyEngineStateMutation((state) => {
131
+ const prev = state[INTERNAL_WELCOME_ACK_KEY];
132
+ const alreadyAcked = prev && typeof prev === 'object' && !Array.isArray(prev)
133
+ && typeof prev.ackedAt === 'string' && prev.ackedAt
134
+ && prev.installationId === installationId;
135
+ if (alreadyAcked) return state;
136
+ state[INTERNAL_WELCOME_ACK_KEY] = {
137
+ ackedAt: new Date().toISOString(),
138
+ installationId,
139
+ packageName: distribution.packageName,
140
+ };
141
+ return state;
142
+ });
143
+ return result[INTERNAL_WELCOME_ACK_KEY];
144
+ }
145
+
146
+ // The full payload the dashboard serves from GET /api/welcome.
147
+ //
148
+ // `show` is true only for an internal distribution whose acknowledgement is
149
+ // missing or belongs to a different installation. SQL being unavailable resolves
150
+ // to `show: false` rather than throwing — a broken state DB must not turn into a
151
+ // popup on every refresh.
152
+ function resolveInternalWelcomeState(opts) {
153
+ const distribution = resolveDistribution(opts);
154
+ const payload = {
155
+ show: false,
156
+ message: INTERNAL_WELCOME_MESSAGE,
157
+ distribution,
158
+ installationId: null,
159
+ ackedAt: null,
160
+ };
161
+ if (!distribution.isInternal) return payload;
162
+ try {
163
+ const installationId = resolveInstallationId();
164
+ const ack = readInternalWelcomeAck();
165
+ payload.installationId = installationId;
166
+ payload.ackedAt = ack && ack.installationId === installationId ? ack.ackedAt : null;
167
+ payload.show = !payload.ackedAt;
168
+ } catch {
169
+ payload.show = false;
170
+ }
171
+ return payload;
172
+ }
173
+
174
+ module.exports = {
175
+ PUBLIC_PACKAGE_NAME,
176
+ DISTRIBUTION_CHANNEL,
177
+ INTERNAL_WELCOME_MESSAGE,
178
+ INSTALLATION_ID_KEY,
179
+ INTERNAL_WELCOME_ACK_KEY,
180
+ PACKAGE_ROOT,
181
+ readRuntimePackageMeta,
182
+ isMinionsPackageName,
183
+ isInternalPackageName,
184
+ resolveDistribution,
185
+ resolveInstallationId,
186
+ readInternalWelcomeAck,
187
+ acknowledgeInternalWelcome,
188
+ resolveInternalWelcomeState,
189
+ };
@@ -59,10 +59,69 @@ function resolveCommentExecutionModel({
59
59
  });
60
60
  }
61
61
 
62
+ /**
63
+ * Honest, stable label for "no concrete model id is known — the runtime picked
64
+ * its own default". Never invent a model name and never render blank.
65
+ */
66
+ function runtimeDefaultModelLabel(runtime) {
67
+ const name = typeof runtime === 'string' ? runtime.trim() : '';
68
+ return name ? `${name} default` : UNKNOWN_MODEL;
69
+ }
70
+
71
+ /**
72
+ * A model id we can show verbatim. `auto` is a request for the runtime to pick,
73
+ * not a concrete model — mirrors buildRuntimeModelEvidence's treatment in
74
+ * engine/queries.js.
75
+ */
76
+ function concreteExecutionModel(value) {
77
+ const model = normalizeExecutionModel(value);
78
+ if (!model) return null;
79
+ return model.toLowerCase() === 'auto' ? null : model;
80
+ }
81
+
82
+ /**
83
+ * W-ms0ytfdd000d5a26 — Resolve the effective model for a work item.
84
+ *
85
+ * Persisted dispatch evidence always wins so later config edits cannot rewrite
86
+ * history:
87
+ * 1. `dispatchEntry.executionModel` — what the runtime reported it ran.
88
+ * 2. `dispatchEntry.requestedModel` — what Minions sent via `--model`.
89
+ * 3. `<runtime> default` — it ran, but nothing concrete was pinned
90
+ * or reported.
91
+ * Only a work item with no dispatch record at all falls through to the caller's
92
+ * current-config resolution (`configuredRuntime` / `configuredModel`), which is
93
+ * flagged with `persisted: false` so the UI can say so.
94
+ *
95
+ * Runtime-agnostic by construction: the runtime is data carried on the record
96
+ * (or supplied by the caller), never branched on by name.
97
+ */
98
+ function resolveWorkItemModel({ dispatchEntry, configuredRuntime, configuredModel } = {}) {
99
+ const fallbackRuntime = typeof configuredRuntime === 'string' ? configuredRuntime.trim() : '';
100
+ const build = (model, source, runtime, persisted) => ({
101
+ model: model || null,
102
+ label: model || runtimeDefaultModelLabel(runtime),
103
+ source: model ? source : (runtime ? 'runtime-default' : 'unknown'),
104
+ runtime: runtime || null,
105
+ persisted,
106
+ });
107
+
108
+ if (dispatchEntry) {
109
+ const entryRuntime = typeof dispatchEntry.runtimeName === 'string' ? dispatchEntry.runtimeName.trim() : '';
110
+ const runtime = entryRuntime || fallbackRuntime;
111
+ const reported = concreteExecutionModel(dispatchEntry.executionModel);
112
+ if (reported) return build(reported, 'runtime-reported', runtime, true);
113
+ return build(concreteExecutionModel(dispatchEntry.requestedModel), 'requested', runtime, true);
114
+ }
115
+ return build(concreteExecutionModel(configuredModel), 'configured', fallbackRuntime, false);
116
+ }
117
+
62
118
  module.exports = {
63
119
  UNKNOWN_MODEL,
64
120
  MODEL_ID_RE,
65
121
  normalizeExecutionModel,
122
+ concreteExecutionModel,
66
123
  resolveExecutionModel,
67
124
  resolveCommentExecutionModel,
125
+ runtimeDefaultModelLabel,
126
+ resolveWorkItemModel,
68
127
  };
@@ -102,6 +102,18 @@ const FEATURES = {
102
102
  addedIn: '0.1.2233',
103
103
  expires: '2027-01-31',
104
104
  },
105
+ 'review-learning-capture': {
106
+ description: 'Capture verified code-review findings as bounded, project-scoped review-learning candidates and procedural lessons.',
107
+ default: false,
108
+ addedIn: '0.1.2203',
109
+ expires: '2027-01-31',
110
+ },
111
+ 'review-learning-recall': {
112
+ description: 'Recall bounded applicable review lessons through the dedicated project-scoped review-learning path.',
113
+ default: false,
114
+ addedIn: '0.1.2203',
115
+ expires: '2027-01-31',
116
+ },
105
117
  };
106
118
 
107
119
  const ENV_TRUTHY = new Set(['1', 'true', 'on', 'yes']);