@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.
- package/bin/install-internal-minions.js +1209 -0
- package/bin/minions.js +75 -9
- package/dashboard/docs/typography.md +27 -12
- package/dashboard/js/command-center.js +13 -11
- package/dashboard/js/live-stream.js +1 -1
- package/dashboard/js/memory-search.js +388 -43
- package/dashboard/js/modal-qa.js +5 -5
- package/dashboard/js/qa.js +39 -26
- package/dashboard/js/refresh.js +37 -18
- package/dashboard/js/render-agents.js +14 -18
- package/dashboard/js/render-dispatch.js +14 -74
- package/dashboard/js/render-inbox.js +3 -3
- package/dashboard/js/render-meetings.js +8 -8
- package/dashboard/js/render-other.js +45 -20
- package/dashboard/js/render-pipelines.js +6 -6
- package/dashboard/js/render-plans.js +14 -14
- package/dashboard/js/render-prd.js +46 -46
- package/dashboard/js/render-prs.js +9 -65
- package/dashboard/js/render-schedules.js +5 -5
- package/dashboard/js/render-skills.js +6 -6
- package/dashboard/js/render-utils.js +4 -4
- package/dashboard/js/render-watches.js +4 -16
- package/dashboard/js/render-work-items.js +95 -107
- package/dashboard/js/settings.js +175 -74
- package/dashboard/layout.html +3 -2
- package/dashboard/pages/inbox.html +1 -1
- package/dashboard/pages/work.html +1 -1
- package/dashboard/shared/model-display.js +9 -0
- package/dashboard/shared/watches-source.js +43 -0
- package/dashboard/shared/welcome-popup.js +259 -0
- package/dashboard/slim/body.html +6 -6
- package/dashboard/slim/js/chat.js +4 -2
- package/dashboard/slim/js/history.js +10 -3
- package/dashboard/slim/js/modals-tiles.js +85 -16
- package/dashboard/slim/js/status.js +12 -16
- package/dashboard/slim/styles.css +24 -4
- package/dashboard/styles.css +62 -27
- package/dashboard-build.js +8 -2
- package/dashboard.js +3854 -2514
- package/docs/README.md +7 -5
- package/docs/api-errors.md +144 -0
- package/docs/auto-discovery.md +83 -61
- package/docs/capture-demos.js +173 -30
- package/docs/command-center.md +5 -1
- package/docs/completion-reports.md +127 -11
- package/docs/constants.md +15 -3
- package/docs/constellation-style-telemetry.md +2 -2
- package/docs/copilot-cli-schema.md +32 -22
- package/docs/cross-repo-plans.md +30 -30
- package/docs/demo/memory-system.html +1 -1
- package/docs/deprecated.json +132 -18
- package/docs/diagnostics-memory.md +8 -4
- package/docs/human-vs-automated.md +2 -2
- package/docs/index.html +9 -2
- package/docs/internal-install.md +212 -0
- package/docs/kb-pr3223-cascade-archiving.md +16 -0
- package/docs/kb-pr696-merge-conflict-docs.md +23 -0
- package/docs/kb-sweep.md +2 -2
- package/docs/keep-processes.md +8 -1
- package/docs/live-checkout-mode.md +1 -1
- package/docs/managed-spawn.md +21 -3
- package/docs/named-agents.md +3 -2
- package/docs/onboarding.md +26 -0
- package/docs/plan-lifecycle.md +32 -32
- package/docs/pr-screenshots/pr-899/worker-pool-worktrees-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-899/worker-pool-worktrees-BEFORE.png +0 -0
- package/docs/pr-screenshots/pr-979/auto-fix-pane-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-979/auto-fix-pane-BEFORE.png +0 -0
- package/docs/pr-screenshots/pr-985/pr-column-em-dash-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-985/pr-column-em-dash-BEFORE.png +0 -0
- package/docs/qa-runbook-lifecycle.md +67 -51
- package/docs/qa-runbooks.md +2 -2
- package/docs/runtime-adapters.md +101 -50
- package/docs/security.md +28 -0
- package/docs/self-improvement.md +47 -13
- package/docs/skills.md +29 -0
- package/docs/slim-ux/concepts.md +2 -2
- package/docs/specs/agent-configurability.md +43 -41
- package/docs/specs/agent-rename.md +13 -13
- package/docs/team-memory.md +283 -14
- package/docs/tutorials/01-install-and-connect.md +23 -0
- package/docs/tutorials/03-plan-driven-feature.md +3 -3
- package/docs/tutorials/04-runtimes-and-harness.md +18 -21
- package/docs/tutorials/08-operations-and-recovery.md +3 -2
- package/docs/tutorials/README.md +4 -3
- package/docs/visual-evidence-ci.md +103 -0
- package/docs/watches.md +35 -30
- package/docs/worktree-lifecycle.md +151 -13
- package/engine/acp-transport.js +1 -0
- package/engine/ado.js +80 -33
- package/engine/agent-api-validation.js +571 -0
- package/engine/agent-worker-pool.js +7 -6
- package/engine/api-contracts/agent-content.js +761 -0
- package/engine/api-contracts/cc-ops.js +219 -0
- package/engine/api-contracts/config-runtime.js +419 -0
- package/engine/api-contracts/core.js +138 -0
- package/engine/api-contracts/index.js +506 -0
- package/engine/api-contracts/orchestration.js +602 -0
- package/engine/api-contracts/pull-requests.js +323 -0
- package/engine/api-contracts/qa-process.js +764 -0
- package/engine/api-contracts/work-plan-prd.js +494 -0
- package/engine/api-validation.js +543 -0
- package/engine/cc-api-validation.js +586 -0
- package/engine/cc-worker-pool.js +9 -1
- package/engine/cleanup.js +393 -271
- package/engine/cli.js +65 -7
- package/engine/comment-classifier.js +70 -5
- package/engine/consolidation.js +123 -5
- package/engine/content-api-validation.js +606 -0
- package/engine/create-pr-worktree.js +22 -1
- package/engine/db/migrations/027-review-learning-lifecycle.js +162 -0
- package/engine/dispatch.js +33 -0
- package/engine/distribution.js +189 -0
- package/engine/execution-model.js +59 -0
- package/engine/features.js +12 -0
- package/engine/github.js +67 -25
- package/engine/harness-context.js +313 -0
- package/engine/inbox-store.js +1 -1
- package/engine/kb-sweep.js +161 -38
- package/engine/keep-process-sweep.js +43 -3
- package/engine/lifecycle.js +1066 -165
- package/engine/llm.js +4 -0
- package/engine/managed-spawn.js +463 -48
- package/engine/meeting.js +197 -34
- package/engine/memory-retrieval.js +280 -0
- package/engine/memory-store.js +821 -38
- package/engine/model-discovery.js +17 -3
- package/engine/pipeline.js +802 -14
- package/engine/plan-prd-validation.js +745 -0
- package/engine/playbook.js +206 -17
- package/engine/pooled-agent-process.js +6 -1
- package/engine/pr-action.js +1 -1
- package/engine/pr-issue-validation.js +653 -0
- package/engine/pr-resolve.js +75 -13
- package/engine/prd-store.js +39 -6
- package/engine/preflight.js +7 -2
- package/engine/process-utils.js +79 -31
- package/engine/projects.js +198 -12
- package/engine/promotion.js +371 -0
- package/engine/qa-from-prd.js +3 -0
- package/engine/qa-process-validation.js +572 -0
- package/engine/qa-runbooks.js +41 -37
- package/engine/qa-runners/maestro.js +11 -7
- package/engine/qa-runners/playwright.js +37 -14
- package/engine/qa-runners.js +9 -10
- package/engine/qa-runs.js +124 -11
- package/engine/qa-sessions.js +139 -40
- package/engine/quarantine-refs.js +81 -11
- package/engine/queries.js +163 -9
- package/engine/review-learning-backfill.js +482 -0
- package/engine/review-learning.js +1236 -0
- package/engine/runtimes/claude.js +60 -2
- package/engine/runtimes/codex.js +57 -16
- package/engine/runtimes/contract.js +21 -0
- package/engine/runtimes/copilot.js +51 -3
- package/engine/runtimes/index.js +1 -0
- package/engine/scheduler.js +201 -32
- package/engine/settings-validation.js +909 -0
- package/engine/shared.js +705 -208
- package/engine/spawn-agent.js +48 -21
- package/engine/steering-constraints.js +31 -0
- package/engine/steering-store.js +10 -2
- package/engine/steering.js +14 -2
- package/engine/timeout.js +404 -111
- package/engine/untrusted-fence.js +24 -10
- package/engine/watch-actions.js +7 -2
- package/engine/watches.js +353 -51
- package/engine/work-item-validation.js +561 -0
- package/engine/work-items-store.js +61 -1
- package/engine/worktree-gc.js +656 -37
- package/engine/worktree-preflight.js +154 -0
- package/engine.js +2068 -342
- package/package.json +1 -1
- package/playbooks/_pr-description-audit.md +88 -41
- package/playbooks/build-fix-complex.md +3 -3
- package/playbooks/docs.md +2 -2
- package/playbooks/fix.md +18 -18
- package/playbooks/implement.md +18 -18
- package/playbooks/qa-session-draft.md +10 -12
- package/playbooks/qa-session-execute.md +16 -12
- package/playbooks/qa-validate.md +8 -7
- package/playbooks/shared-rules.md +45 -0
- package/playbooks/test.md +4 -0
- package/prompts/cc-system.md +1 -1
- package/skills/check-self-authored-review-comment/SKILL.md +2 -2
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
// engine/review-learning-backfill.js — strict, offline backfill of durable
|
|
2
|
+
// review-learning lessons from legacy artifacts.
|
|
3
|
+
//
|
|
4
|
+
// The backfill scans deterministic, bounded slices of legacy sources and imports
|
|
5
|
+
// a lesson ONLY when the source already carries everything the live capture path
|
|
6
|
+
// requires: project, canonical PR, author agent, at least one discrete generalized
|
|
7
|
+
// rule, successful current-head resolution evidence, source consistency, and clean
|
|
8
|
+
// security provenance. It never asks an LLM to infer historical rules — records
|
|
9
|
+
// that lack any qualification signal are left non-recallable with an explicit skip
|
|
10
|
+
// reason.
|
|
11
|
+
//
|
|
12
|
+
// Imports go through the exact same stable-key + upsert path as live capture
|
|
13
|
+
// (reviewLearning.buildFindingKey + memoryStore.upsertReviewLearningLesson), so a
|
|
14
|
+
// dry-run classifies identically to a real run and a second real run imports zero
|
|
15
|
+
// duplicates.
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const shared = require('./shared');
|
|
20
|
+
const reviewLearning = require('./review-learning');
|
|
21
|
+
|
|
22
|
+
const REASON_BUCKETS = Object.freeze([
|
|
23
|
+
'already-present',
|
|
24
|
+
'ambiguous-raw-comment',
|
|
25
|
+
'missing-generalized-rule',
|
|
26
|
+
'missing-resolution-evidence',
|
|
27
|
+
'source-mismatch',
|
|
28
|
+
'ambiguous-project',
|
|
29
|
+
'missing-pr',
|
|
30
|
+
'missing-author',
|
|
31
|
+
'injection-flagged',
|
|
32
|
+
'not-qualified',
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
const RAW_KINDS = Object.freeze([
|
|
36
|
+
'completion-learning',
|
|
37
|
+
'completed-dispatch-metadata',
|
|
38
|
+
'pr-record',
|
|
39
|
+
'legacy-feedback',
|
|
40
|
+
'quarantined-candidate',
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
const { POSITIVE_DISPOSITIONS } = reviewLearning;
|
|
44
|
+
|
|
45
|
+
function _skip(reason, extra = {}) {
|
|
46
|
+
const bucket = REASON_BUCKETS.includes(reason) ? reason : 'not-qualified';
|
|
47
|
+
return { status: 'skipped', reason: bucket, ...extra };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Deterministic classification of a normalization error into a skip bucket. The
|
|
51
|
+
// live normalizer throws typed messages; we map them so per-record reason counts
|
|
52
|
+
// are stable across dry-run and real runs.
|
|
53
|
+
function bucketForError(err) {
|
|
54
|
+
const msg = String(err && err.message || err || '').toLowerCase();
|
|
55
|
+
if (/prohibited|not a safe field|injection|chain[- ]?of[- ]?thought|reasoning|transcript/.test(msg)) {
|
|
56
|
+
return 'injection-flagged';
|
|
57
|
+
}
|
|
58
|
+
if (/project does not match|project scope|finding key project/.test(msg)) return 'source-mismatch';
|
|
59
|
+
if (/project/.test(msg)) return 'ambiguous-project';
|
|
60
|
+
if (/source does not match|not an allowed source|source key|conflicting disposition|conflicting audience|author agent does not match/.test(msg)) {
|
|
61
|
+
return 'source-mismatch';
|
|
62
|
+
}
|
|
63
|
+
if (/author/.test(msg)) return 'missing-author';
|
|
64
|
+
if (/canonical pr|pr id|source pr|\bpr\b/.test(msg)) return 'missing-pr';
|
|
65
|
+
if (/rule/.test(msg)) return 'missing-generalized-rule';
|
|
66
|
+
if (/evidence/.test(msg)) return 'missing-resolution-evidence';
|
|
67
|
+
return 'not-qualified';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function _normalizeContext(context = {}) {
|
|
71
|
+
const project = reviewLearning.normalizeProjectKey(context.project);
|
|
72
|
+
const prId = reviewLearning.normalizeCanonicalPrId(context.prId || context.pr);
|
|
73
|
+
const rawAllowed = context.allowedSourceKeys instanceof Set
|
|
74
|
+
? [...context.allowedSourceKeys]
|
|
75
|
+
: (Array.isArray(context.allowedSourceKeys) ? context.allowedSourceKeys : []);
|
|
76
|
+
const allowedSourceKeys = new Set(rawAllowed
|
|
77
|
+
.map(value => {
|
|
78
|
+
try { return reviewLearning.normalizeFindingSourceKey(value); } catch { return null; }
|
|
79
|
+
})
|
|
80
|
+
.filter(Boolean));
|
|
81
|
+
const authorAgent = context.authorAgent == null ? '' : String(context.authorAgent).trim();
|
|
82
|
+
return { project, prId, authorAgent, allowedSourceKeys };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Mirrors engine/lifecycle.js#_groupReviewLearningEntries: collapse normalized
|
|
86
|
+
// completion entries into one lesson group per finding key, flagging conflicting
|
|
87
|
+
// dispositions/audiences so they are rejected rather than silently merged.
|
|
88
|
+
function _groupEntries(entries) {
|
|
89
|
+
const groups = new Map();
|
|
90
|
+
for (const entry of entries) {
|
|
91
|
+
const findingKey = reviewLearning.buildFindingKey({
|
|
92
|
+
project: entry.project,
|
|
93
|
+
prId: entry.source.pr,
|
|
94
|
+
sourceKey: entry.source.sourceKey,
|
|
95
|
+
});
|
|
96
|
+
let group = groups.get(findingKey);
|
|
97
|
+
if (!group) {
|
|
98
|
+
group = {
|
|
99
|
+
findingKey,
|
|
100
|
+
project: entry.project,
|
|
101
|
+
prId: entry.source.pr,
|
|
102
|
+
sourceKey: entry.source.sourceKey,
|
|
103
|
+
authorAgent: entry.authorAgent,
|
|
104
|
+
disposition: entry.disposition,
|
|
105
|
+
audience: entry.applicability,
|
|
106
|
+
rules: [],
|
|
107
|
+
files: [],
|
|
108
|
+
symbols: [],
|
|
109
|
+
tags: [],
|
|
110
|
+
evidence: [],
|
|
111
|
+
confidences: [],
|
|
112
|
+
};
|
|
113
|
+
groups.set(findingKey, group);
|
|
114
|
+
}
|
|
115
|
+
if (group.disposition !== entry.disposition) {
|
|
116
|
+
group.error = `review learning source ${entry.source.sourceKey} has conflicting dispositions`;
|
|
117
|
+
}
|
|
118
|
+
if (group.audience !== entry.applicability) {
|
|
119
|
+
group.error = `review learning source ${entry.source.sourceKey} has conflicting audiences`;
|
|
120
|
+
}
|
|
121
|
+
group.rules.push(entry.rule);
|
|
122
|
+
group.files.push(...entry.files);
|
|
123
|
+
group.symbols.push(...entry.symbols);
|
|
124
|
+
group.tags.push(...entry.tags);
|
|
125
|
+
group.evidence.push(...entry.evidence);
|
|
126
|
+
group.confidences.push(entry.confidence);
|
|
127
|
+
}
|
|
128
|
+
return [...groups.values()].map((group) => {
|
|
129
|
+
const facets = reviewLearning.normalizeReviewFacets(group);
|
|
130
|
+
return {
|
|
131
|
+
...group,
|
|
132
|
+
rules: [...new Set(group.rules)].sort((a, b) => a.localeCompare(b)),
|
|
133
|
+
files: facets.files,
|
|
134
|
+
symbols: facets.symbols,
|
|
135
|
+
tags: facets.tags,
|
|
136
|
+
area: facets.area,
|
|
137
|
+
evidence: [...new Set(group.evidence)].sort((a, b) => a.localeCompare(b)),
|
|
138
|
+
confidence: group.confidences.length ? Math.min(...group.confidences) : 0.8,
|
|
139
|
+
};
|
|
140
|
+
}).sort((a, b) => a.findingKey.localeCompare(b.findingKey));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Build the deterministic lesson input consumed by the live upsert path.
|
|
144
|
+
function _lessonInputForGroup(group, rec) {
|
|
145
|
+
return {
|
|
146
|
+
findingKey: group.findingKey,
|
|
147
|
+
project: group.project,
|
|
148
|
+
prId: group.prId,
|
|
149
|
+
authorAgent: group.authorAgent,
|
|
150
|
+
audience: group.audience,
|
|
151
|
+
disposition: group.disposition,
|
|
152
|
+
rules: group.rules,
|
|
153
|
+
files: group.files,
|
|
154
|
+
symbols: group.symbols,
|
|
155
|
+
tags: group.tags,
|
|
156
|
+
evidence: group.evidence,
|
|
157
|
+
confidence: group.confidence,
|
|
158
|
+
metadata: {
|
|
159
|
+
backfill: true,
|
|
160
|
+
backfillSource: rec.kind,
|
|
161
|
+
backfillRef: rec.orderKey || null,
|
|
162
|
+
sourceKey: group.sourceKey,
|
|
163
|
+
verifiedBy: `exact-${reviewLearning.classifyExactEvidence(group.evidence).join('+') || 'evidence'}`,
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Import (or, in dry-run, classify) a single qualified lesson group. Idempotency
|
|
169
|
+
// uses the same stable finding key + body content that live capture writes, so a
|
|
170
|
+
// second real run reports the record as already-present instead of duplicating it.
|
|
171
|
+
function _importGroup(group, rec, deps) {
|
|
172
|
+
const { memoryStore, dryRun } = deps;
|
|
173
|
+
if (group.error) return _skip('source-mismatch', { findingKey: group.findingKey });
|
|
174
|
+
if (!POSITIVE_DISPOSITIONS.has(group.disposition)) {
|
|
175
|
+
return _skip('missing-resolution-evidence', { findingKey: group.findingKey });
|
|
176
|
+
}
|
|
177
|
+
if (reviewLearning.classifyExactEvidence(group.evidence).length === 0) {
|
|
178
|
+
return _skip('missing-resolution-evidence', { findingKey: group.findingKey });
|
|
179
|
+
}
|
|
180
|
+
if (!Array.isArray(group.rules) || group.rules.length === 0) {
|
|
181
|
+
return _skip('missing-generalized-rule', { findingKey: group.findingKey });
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const lessonInput = _lessonInputForGroup(group, rec);
|
|
185
|
+
let record;
|
|
186
|
+
try {
|
|
187
|
+
record = reviewLearning.buildLessonMemoryRecord(lessonInput);
|
|
188
|
+
} catch (err) {
|
|
189
|
+
return _skip(bucketForError(err), { findingKey: group.findingKey });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
let existing = [];
|
|
193
|
+
try {
|
|
194
|
+
existing = memoryStore.listActiveReviewLearnings({
|
|
195
|
+
project: group.project,
|
|
196
|
+
findingKey: group.findingKey,
|
|
197
|
+
limit: 50,
|
|
198
|
+
});
|
|
199
|
+
} catch {
|
|
200
|
+
existing = [];
|
|
201
|
+
}
|
|
202
|
+
if (existing.some(row => row.body === record.body)) {
|
|
203
|
+
return { status: 'already-present', findingKey: group.findingKey, memoryId: existing.find(row => row.body === record.body)?.id || null };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (dryRun) {
|
|
207
|
+
return { status: 'imported', findingKey: group.findingKey, dryRun: true };
|
|
208
|
+
}
|
|
209
|
+
const lesson = memoryStore.upsertReviewLearningLesson(lessonInput);
|
|
210
|
+
return { status: 'imported', findingKey: group.findingKey, memoryId: lesson?.id || null };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Process one raw source record into an array of outcomes. A completion-learning
|
|
214
|
+
// record can yield several findings; every other legacy kind currently lacks a
|
|
215
|
+
// discrete generalized rule and/or verified resolution evidence, so it resolves
|
|
216
|
+
// to a single explicit skip.
|
|
217
|
+
function processRecord(rec, deps) {
|
|
218
|
+
if (!rec || typeof rec !== 'object') return [{ status: 'error' }];
|
|
219
|
+
|
|
220
|
+
if (rec.injectionFlagged === true) {
|
|
221
|
+
return [_skip('injection-flagged', { ref: rec.orderKey || null })];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
switch (rec.kind) {
|
|
225
|
+
case 'completion-learning':
|
|
226
|
+
case 'completed-dispatch-metadata': {
|
|
227
|
+
const ctxInput = rec.context;
|
|
228
|
+
if (!ctxInput || typeof ctxInput !== 'object' || Object.keys(ctxInput).length === 0) {
|
|
229
|
+
// No trusted dispatch context — the source binding cannot be verified.
|
|
230
|
+
return [_skip('source-mismatch', { ref: rec.orderKey || null })];
|
|
231
|
+
}
|
|
232
|
+
let context;
|
|
233
|
+
try {
|
|
234
|
+
context = _normalizeContext(ctxInput);
|
|
235
|
+
} catch (err) {
|
|
236
|
+
return [_skip(bucketForError(err), { ref: rec.orderKey || null })];
|
|
237
|
+
}
|
|
238
|
+
if (!context.authorAgent) return [_skip('missing-author', { ref: rec.orderKey || null })];
|
|
239
|
+
const learnings = Array.isArray(rec.learnings) ? rec.learnings : [];
|
|
240
|
+
if (learnings.length === 0) {
|
|
241
|
+
return [_skip('missing-generalized-rule', { ref: rec.orderKey || null })];
|
|
242
|
+
}
|
|
243
|
+
let normalized;
|
|
244
|
+
try {
|
|
245
|
+
normalized = reviewLearning.normalizeCompletionReviewLearnings(learnings, {
|
|
246
|
+
project: context.project,
|
|
247
|
+
prId: context.prId,
|
|
248
|
+
authorAgent: context.authorAgent,
|
|
249
|
+
allowedSourceKeys: context.allowedSourceKeys,
|
|
250
|
+
});
|
|
251
|
+
} catch {
|
|
252
|
+
// Re-normalize per entry so each malformed entry is bucketed individually.
|
|
253
|
+
return learnings.map((entry) => {
|
|
254
|
+
try {
|
|
255
|
+
reviewLearning.normalizeCompletionReviewLearnings([entry], {
|
|
256
|
+
project: context.project,
|
|
257
|
+
prId: context.prId,
|
|
258
|
+
authorAgent: context.authorAgent,
|
|
259
|
+
allowedSourceKeys: context.allowedSourceKeys,
|
|
260
|
+
});
|
|
261
|
+
return _skip('not-qualified', { ref: rec.orderKey || null });
|
|
262
|
+
} catch (err) {
|
|
263
|
+
return _skip(bucketForError(err), { ref: rec.orderKey || null });
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
const groups = _groupEntries(normalized);
|
|
268
|
+
return groups.map(group => _importGroup(group, rec, deps));
|
|
269
|
+
}
|
|
270
|
+
case 'pr-record':
|
|
271
|
+
// Tracked PR records carry project/PR/author provenance but no discrete
|
|
272
|
+
// generalized rule; importing would require inferring the lesson.
|
|
273
|
+
return [_skip('missing-generalized-rule', { ref: rec.orderKey || null })];
|
|
274
|
+
case 'legacy-feedback':
|
|
275
|
+
// Raw feedback inbox/archive prose — ambiguous without a generalized rule.
|
|
276
|
+
return [_skip('ambiguous-raw-comment', { ref: rec.orderKey || null })];
|
|
277
|
+
case 'quarantined-candidate':
|
|
278
|
+
// Migration-027 quarantined candidates stay non-recallable: they hold raw
|
|
279
|
+
// generated feedback, not a discrete rule, so they are reconciled (counted,
|
|
280
|
+
// left as candidates) rather than promoted.
|
|
281
|
+
return [_skip('missing-generalized-rule', { ref: rec.orderKey || null, reconciled: true })];
|
|
282
|
+
default:
|
|
283
|
+
return [_skip('not-qualified', { ref: rec.orderKey || null })];
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ─── Default source scanners (offline; read FS + SQL stores) ─────────────────
|
|
288
|
+
|
|
289
|
+
function _readdirSafe(dir) {
|
|
290
|
+
try { return fs.readdirSync(dir); } catch { return []; }
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function _readJsonSafe(file) {
|
|
294
|
+
try {
|
|
295
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
296
|
+
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
297
|
+
} catch { return null; }
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function scanCompletionArtifacts(dir, { project } = {}) {
|
|
301
|
+
const completionsDir = path.join(dir, 'engine', 'completions');
|
|
302
|
+
const files = _readdirSafe(completionsDir).filter(f => f.endsWith('.json')).sort();
|
|
303
|
+
if (files.length === 0) return [];
|
|
304
|
+
|
|
305
|
+
let dispatch = { pending: [], active: [], completed: [] };
|
|
306
|
+
try { dispatch = require('./dispatch-store').readDispatchSectioned() || dispatch; } catch {}
|
|
307
|
+
const entriesById = new Map();
|
|
308
|
+
for (const list of ['completed', 'active', 'pending']) {
|
|
309
|
+
for (const entry of (dispatch[list] || [])) {
|
|
310
|
+
if (entry && entry.id && !entriesById.has(entry.id)) entriesById.set(entry.id, entry);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
let buildContext = null;
|
|
315
|
+
try { buildContext = require('./lifecycle').buildReviewLearningCompletionContext; } catch {}
|
|
316
|
+
|
|
317
|
+
const records = [];
|
|
318
|
+
for (const file of files) {
|
|
319
|
+
const dispatchId = file.replace(/\.json$/, '');
|
|
320
|
+
const report = _readJsonSafe(path.join(completionsDir, file));
|
|
321
|
+
if (!report) continue;
|
|
322
|
+
const learnings = report.reviewLearnings || report.meta?.reviewLearnings;
|
|
323
|
+
if (!Array.isArray(learnings) || learnings.length === 0) continue;
|
|
324
|
+
|
|
325
|
+
const injectionFlagged = report.securityFlags?.injectionAttempt === true
|
|
326
|
+
|| report.meta?.securityFlags?.injectionAttempt === true;
|
|
327
|
+
const entry = entriesById.get(dispatchId);
|
|
328
|
+
let context = null;
|
|
329
|
+
if (entry && typeof buildContext === 'function') {
|
|
330
|
+
try {
|
|
331
|
+
const ctx = buildContext(entry, entry.agent);
|
|
332
|
+
context = {
|
|
333
|
+
project: ctx.project,
|
|
334
|
+
prId: ctx.prId,
|
|
335
|
+
authorAgent: ctx.authorAgent,
|
|
336
|
+
allowedSourceKeys: ctx.allowedSourceKeys,
|
|
337
|
+
};
|
|
338
|
+
} catch { context = null; }
|
|
339
|
+
}
|
|
340
|
+
if (!context) {
|
|
341
|
+
// No trusted dispatch context — cannot bind sources; leave non-recallable.
|
|
342
|
+
records.push({ kind: 'completion-learning', orderKey: dispatchId, learnings, context: {}, injectionFlagged });
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
if (project && context.project !== reviewLearning.normalizeProjectKey(project)) continue;
|
|
346
|
+
records.push({ kind: 'completion-learning', orderKey: dispatchId, learnings, context, injectionFlagged });
|
|
347
|
+
}
|
|
348
|
+
return records;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function scanQuarantinedCandidates(deps, { project } = {}) {
|
|
352
|
+
const { memoryStore } = deps;
|
|
353
|
+
let rows = [];
|
|
354
|
+
try {
|
|
355
|
+
rows = memoryStore.listReviewLearningCandidates({ project: project || undefined, limit: 500 }) || [];
|
|
356
|
+
} catch { rows = []; }
|
|
357
|
+
return rows
|
|
358
|
+
.filter(row => row?.metadata?.legacyUnverified === true || row?.metadata?.quarantinedBy)
|
|
359
|
+
.map(row => ({
|
|
360
|
+
kind: 'quarantined-candidate',
|
|
361
|
+
orderKey: `qc:${row.id}`,
|
|
362
|
+
project: row.scopeKey || null,
|
|
363
|
+
memoryId: row.id,
|
|
364
|
+
}))
|
|
365
|
+
.sort((a, b) => a.orderKey.localeCompare(b.orderKey));
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function scanLegacyFeedbackFiles(dir) {
|
|
369
|
+
const roots = [
|
|
370
|
+
path.join(dir, 'notes', 'inbox'),
|
|
371
|
+
path.join(dir, 'notes', 'inbox', 'archive'),
|
|
372
|
+
path.join(dir, 'notes', 'archive'),
|
|
373
|
+
];
|
|
374
|
+
const records = [];
|
|
375
|
+
for (const root of roots) {
|
|
376
|
+
for (const name of _readdirSafe(root)) {
|
|
377
|
+
if (!/^feedback-.+\.md$/i.test(name)) continue;
|
|
378
|
+
records.push({ kind: 'legacy-feedback', orderKey: `fb:${path.relative(dir, path.join(root, name)).replace(/\\/g, '/')}` });
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return records.sort((a, b) => a.orderKey.localeCompare(b.orderKey));
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function scanTrackedPrRecords(dir) {
|
|
385
|
+
let prs = [];
|
|
386
|
+
try {
|
|
387
|
+
const prStore = require('./pull-requests-store');
|
|
388
|
+
prs = (prStore.readAllPullRequests ? prStore.readAllPullRequests() : prStore.readPullRequests?.()) || [];
|
|
389
|
+
} catch { prs = []; }
|
|
390
|
+
return prs
|
|
391
|
+
.filter(pr => pr && (pr.reviewStatus || pr.minionsReview))
|
|
392
|
+
.map(pr => ({ kind: 'pr-record', orderKey: `pr:${pr.project || ''}:${pr.id || pr.prNumber || ''}` }))
|
|
393
|
+
.sort((a, b) => a.orderKey.localeCompare(b.orderKey));
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function gatherDefaultSources(dir, deps, { project } = {}) {
|
|
397
|
+
return [
|
|
398
|
+
...scanCompletionArtifacts(dir, { project }),
|
|
399
|
+
...scanTrackedPrRecords(dir),
|
|
400
|
+
...scanLegacyFeedbackFiles(dir),
|
|
401
|
+
...scanQuarantinedCandidates(deps, { project }),
|
|
402
|
+
];
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// ─── Orchestrator ────────────────────────────────────────────────────────────
|
|
406
|
+
|
|
407
|
+
function _emptyReasonCounts() {
|
|
408
|
+
const reasons = {};
|
|
409
|
+
for (const bucket of REASON_BUCKETS) reasons[bucket] = 0;
|
|
410
|
+
return reasons;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function backfillReviewLearnings(options = {}) {
|
|
414
|
+
const {
|
|
415
|
+
dryRun = false,
|
|
416
|
+
project = null,
|
|
417
|
+
dir = shared.MINIONS_DIR,
|
|
418
|
+
sources = null,
|
|
419
|
+
} = options;
|
|
420
|
+
const limit = Number.isFinite(Number(options.limit)) && Number(options.limit) > 0
|
|
421
|
+
? Math.floor(Number(options.limit))
|
|
422
|
+
: 100;
|
|
423
|
+
const memoryStore = options.memoryStore || require('./memory-store');
|
|
424
|
+
const deps = { memoryStore, dryRun, project };
|
|
425
|
+
|
|
426
|
+
let raw = Array.isArray(sources) ? sources.slice() : gatherDefaultSources(dir, deps, { project });
|
|
427
|
+
// Deterministic ordering + bounded slice honor the requested limit before any work.
|
|
428
|
+
raw.sort((a, b) => String((a && a.orderKey) || '').localeCompare(String((b && b.orderKey) || '')));
|
|
429
|
+
const bounded = raw.slice(0, limit);
|
|
430
|
+
|
|
431
|
+
const counts = {
|
|
432
|
+
dryRun,
|
|
433
|
+
limit,
|
|
434
|
+
scannedSources: bounded.length,
|
|
435
|
+
processed: 0,
|
|
436
|
+
imported: 0,
|
|
437
|
+
alreadyPresent: 0,
|
|
438
|
+
skipped: 0,
|
|
439
|
+
errors: 0,
|
|
440
|
+
reasons: _emptyReasonCounts(),
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
for (const rec of bounded) {
|
|
444
|
+
let outcomes;
|
|
445
|
+
try {
|
|
446
|
+
outcomes = processRecord(rec, deps);
|
|
447
|
+
} catch {
|
|
448
|
+
// Non-integrity error for this record — count and continue.
|
|
449
|
+
counts.errors += 1;
|
|
450
|
+
counts.processed += 1;
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
for (const outcome of outcomes) {
|
|
454
|
+
counts.processed += 1;
|
|
455
|
+
if (outcome.status === 'imported') counts.imported += 1;
|
|
456
|
+
else if (outcome.status === 'already-present') counts.alreadyPresent += 1;
|
|
457
|
+
else if (outcome.status === 'error') counts.errors += 1;
|
|
458
|
+
else {
|
|
459
|
+
counts.skipped += 1;
|
|
460
|
+
const reason = REASON_BUCKETS.includes(outcome.reason) ? outcome.reason : 'not-qualified';
|
|
461
|
+
counts.reasons[reason] += 1;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return counts;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
module.exports = {
|
|
469
|
+
REASON_BUCKETS,
|
|
470
|
+
RAW_KINDS,
|
|
471
|
+
backfillReviewLearnings,
|
|
472
|
+
processRecord,
|
|
473
|
+
bucketForError,
|
|
474
|
+
gatherDefaultSources,
|
|
475
|
+
scanCompletionArtifacts,
|
|
476
|
+
scanQuarantinedCandidates,
|
|
477
|
+
scanLegacyFeedbackFiles,
|
|
478
|
+
scanTrackedPrRecords,
|
|
479
|
+
// exported for testing
|
|
480
|
+
_groupEntries,
|
|
481
|
+
_normalizeContext,
|
|
482
|
+
};
|