@yemi33/minions 0.1.2154 → 0.1.2156
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/engine/cli.js
CHANGED
|
@@ -580,6 +580,21 @@ const commands = {
|
|
|
580
580
|
}
|
|
581
581
|
} catch (err) { e.log('warn', `cc worker pool force-on migration failed: ${err.message}`); }
|
|
582
582
|
|
|
583
|
+
// Repair work-item note links that broke before the rewriter covered
|
|
584
|
+
// completion-report artifact links (W-mq1j85cj00055a8f). Notes the engine
|
|
585
|
+
// auto-archived back then left the WI pill pointing at the gone
|
|
586
|
+
// notes/inbox/<name> path; this repoints them to the archived/KB location.
|
|
587
|
+
// Idempotent (only touches provably-broken + resolvable links) and routed
|
|
588
|
+
// through mutateWorkItems so SQL + JSON mirror stay consistent — a no-op
|
|
589
|
+
// once healed, so it's safe to run on every boot.
|
|
590
|
+
try {
|
|
591
|
+
const res = require('./note-link-backfill').runNoteLinkBackfill({ config });
|
|
592
|
+
if (res.fixedLinks > 0) {
|
|
593
|
+
e.log('info', `Repointed ${res.fixedLinks} broken note link(s) across ${res.fixedItems} work item(s)`);
|
|
594
|
+
console.log(` Repaired ${res.fixedLinks} broken note link(s) in ${res.fixedItems} work item(s).`);
|
|
595
|
+
}
|
|
596
|
+
} catch (err) { e.log('warn', `note-link backfill failed: ${err.message}`); }
|
|
597
|
+
|
|
583
598
|
// Auto-heal projects missing workSources (cloned-repo / hand-rolled-config
|
|
584
599
|
// footgun): without this block, discoverFromWorkItems / discoverFromPrs
|
|
585
600
|
// bail silently and the engine looks healthy but never dispatches. The
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// engine/db/migrations/013-backfill-broken-note-links.js
|
|
2
|
+
//
|
|
3
|
+
// No-op marker. The one-time repair of broken inbox note links was moved to an
|
|
4
|
+
// idempotent engine-boot sweep (engine/cli.js → note-link-backfill.runNoteLink
|
|
5
|
+
// Backfill) because it must go through shared.mutateWorkItems to keep the SQL
|
|
6
|
+
// store and its JSON mirror consistent — a raw in-migration UPDATE left the
|
|
7
|
+
// mirror diverged and could be reverted by the store's resync. This migration
|
|
8
|
+
// stays so schema_version progression is identical across installs that already
|
|
9
|
+
// recorded v13.
|
|
10
|
+
|
|
11
|
+
module.exports = {
|
|
12
|
+
version: 13,
|
|
13
|
+
description: 'reserved (note-link backfill moved to an idempotent engine-boot sweep)',
|
|
14
|
+
up() { /* intentionally empty — see header */ },
|
|
15
|
+
};
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// engine/note-link-backfill.js
|
|
2
|
+
//
|
|
3
|
+
// One-time repair for work-item note links that broke BEFORE the live
|
|
4
|
+
// rewrite landed (W-mq1j85cj00055a8f extended to _artifacts.notes / artifacts).
|
|
5
|
+
//
|
|
6
|
+
// Before the rewriter covered completion-report artifact links, any inbox note
|
|
7
|
+
// that the engine auto-archived (or that was promoted to KB) left the work
|
|
8
|
+
// item's link pointing at the now-gone `notes/inbox/<name>` path — a dangling
|
|
9
|
+
// pill in the dashboard. This module scans existing work items, finds links
|
|
10
|
+
// whose inbox file is GONE, discovers where the note actually went (the flat
|
|
11
|
+
// `notes/archive/` dir or a `knowledge/<cat>/` dir), and repoints the link —
|
|
12
|
+
// re-encoding the `_artifacts.notes[].file` token to the renderer's
|
|
13
|
+
// `archive:<base>` / `kb:<cat>/<file>` form so the pill resolves again.
|
|
14
|
+
//
|
|
15
|
+
// It only ever touches links that are PROVABLY broken (inbox file missing) AND
|
|
16
|
+
// resolvable (a matching archive/KB file exists). Valid links, already-relocated
|
|
17
|
+
// tokens, and notes with no discoverable destination (e.g. merged into the flat
|
|
18
|
+
// notes.md) are left untouched. Idempotent: a second pass finds nothing because
|
|
19
|
+
// the repaired links no longer reference `notes/inbox/`.
|
|
20
|
+
//
|
|
21
|
+
// Wired as an idempotent engine-boot sweep (engine/cli.js, alongside the other
|
|
22
|
+
// one-shot boot migrations). It runs through shared.mutateWorkItems — the same
|
|
23
|
+
// write path the live rewriter uses — so the canonical SQL store and its JSON
|
|
24
|
+
// mirror stay consistent, and it's a no-op once every link is healed.
|
|
25
|
+
|
|
26
|
+
'use strict';
|
|
27
|
+
|
|
28
|
+
const fs = require('fs');
|
|
29
|
+
const path = require('path');
|
|
30
|
+
|
|
31
|
+
const INBOX_REF_RE = /(?:^|\/)notes\/inbox\/([^/?#]+)/;
|
|
32
|
+
|
|
33
|
+
// Extract the inbox basename from a `…/notes/inbox/<name>` string (tolerating a
|
|
34
|
+
// trailing ?query / #fragment), or null if the string isn't an inbox ref.
|
|
35
|
+
function inboxNameFromRef(str) {
|
|
36
|
+
if (typeof str !== 'string') return null;
|
|
37
|
+
const m = str.match(INBOX_REF_RE);
|
|
38
|
+
return m ? m[1] : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Build a resolver: inbox basename → { newLocation, fileToken } | null.
|
|
42
|
+
// - null when the note is STILL in the inbox (link isn't broken), or when no
|
|
43
|
+
// destination can be discovered (note merged into notes.md, or deleted).
|
|
44
|
+
// - newLocation is the MINIONS_DIR-relative path; fileToken is the prefixed
|
|
45
|
+
// basename the work-item renderer resolves for note pills.
|
|
46
|
+
// The archive dir is flat: consolidation writes `${YYYY-MM-DD}-${name}` (with an
|
|
47
|
+
// optional uniquePath `-N` collision suffix). We index each archived file by the
|
|
48
|
+
// basename it derived from (date prefix stripped) and by its literal name.
|
|
49
|
+
function buildNoteResolver(dirs) {
|
|
50
|
+
const inboxDir = dirs.inboxDir;
|
|
51
|
+
const archiveDir = dirs.archiveDir;
|
|
52
|
+
const knowledgeDir = dirs.knowledgeDir;
|
|
53
|
+
const kbCategories = Array.isArray(dirs.kbCategories) ? dirs.kbCategories : [];
|
|
54
|
+
// Reuse the live rewriter's encoder so the backfill and the live path emit
|
|
55
|
+
// byte-identical pill tokens (one source for the archive:/kb: grammar).
|
|
56
|
+
const tokenFor = require('./shared')._artifactNoteFileToken;
|
|
57
|
+
|
|
58
|
+
const archiveByBase = new Map();
|
|
59
|
+
let archiveFiles = [];
|
|
60
|
+
try { archiveFiles = fs.readdirSync(archiveDir); } catch { archiveFiles = []; }
|
|
61
|
+
for (const f of archiveFiles) {
|
|
62
|
+
const origin = f.replace(/^\d{4}-\d{2}-\d{2}-/, '');
|
|
63
|
+
for (const key of new Set([origin, f])) {
|
|
64
|
+
const prev = archiveByBase.get(key);
|
|
65
|
+
// Lexicographic max keeps the newest date-prefixed file for a given note.
|
|
66
|
+
if (!prev || f > prev) archiveByBase.set(key, f);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const kbByBase = new Map();
|
|
71
|
+
for (const cat of kbCategories) {
|
|
72
|
+
let files = [];
|
|
73
|
+
try { files = fs.readdirSync(path.join(knowledgeDir, cat)); } catch { files = []; }
|
|
74
|
+
for (const f of files) if (!kbByBase.has(f)) kbByBase.set(f, cat + '/' + f);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return function resolve(baseName) {
|
|
78
|
+
if (!baseName) return null;
|
|
79
|
+
try { if (fs.existsSync(path.join(inboxDir, baseName))) return null; } catch { /* treat as gone */ }
|
|
80
|
+
const arch = archiveByBase.get(baseName);
|
|
81
|
+
if (arch) { const nl = 'notes/archive/' + arch; return { newLocation: nl, fileToken: tokenFor(nl) }; }
|
|
82
|
+
const kb = kbByBase.get(baseName);
|
|
83
|
+
if (kb) { const nl = 'knowledge/' + kb; return { newLocation: nl, fileToken: tokenFor(nl) }; }
|
|
84
|
+
return null;
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Repoint every broken inbox note link on one work item in place. `resolve` is a
|
|
89
|
+
// buildNoteResolver() result (returns null for non-broken / unresolvable). Mirrors
|
|
90
|
+
// the link shapes shared.rewriteInboxRefsAcrossProjects handles: references[],
|
|
91
|
+
// _artifacts.notes[] (string + object), and artifacts[].path. Returns the count
|
|
92
|
+
// of links repointed.
|
|
93
|
+
function repointItemNoteLinks(item, resolve) {
|
|
94
|
+
if (!item || typeof item !== 'object') return 0;
|
|
95
|
+
let fixed = 0;
|
|
96
|
+
|
|
97
|
+
if (Array.isArray(item.references)) {
|
|
98
|
+
for (let i = 0; i < item.references.length; i++) {
|
|
99
|
+
const ref = item.references[i];
|
|
100
|
+
if (typeof ref === 'string') {
|
|
101
|
+
const name = inboxNameFromRef(ref);
|
|
102
|
+
if (name) { const d = resolve(name); if (d) { item.references[i] = d.newLocation; fixed++; } }
|
|
103
|
+
} else if (ref && typeof ref === 'object') {
|
|
104
|
+
for (const key of ['url', 'path', 'href']) {
|
|
105
|
+
const name = inboxNameFromRef(ref[key]);
|
|
106
|
+
if (name) { const d = resolve(name); if (d) { ref[key] = d.newLocation; fixed++; break; } }
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const notes = item._artifacts && item._artifacts.notes;
|
|
113
|
+
if (Array.isArray(notes)) {
|
|
114
|
+
for (let i = 0; i < notes.length; i++) {
|
|
115
|
+
const n = notes[i];
|
|
116
|
+
if (typeof n === 'string') {
|
|
117
|
+
// Bare basename = an inbox pill. Skip tokens already relocated.
|
|
118
|
+
if (n.startsWith('archive:') || n.startsWith('kb:')) continue;
|
|
119
|
+
const d = resolve(n);
|
|
120
|
+
if (d && d.fileToken) { notes[i] = d.fileToken; fixed++; }
|
|
121
|
+
} else if (n && typeof n === 'object') {
|
|
122
|
+
let name = inboxNameFromRef(n.path);
|
|
123
|
+
if (!name && typeof n.file === 'string'
|
|
124
|
+
&& !n.file.startsWith('archive:') && !n.file.startsWith('kb:')) {
|
|
125
|
+
name = n.file;
|
|
126
|
+
}
|
|
127
|
+
if (name) {
|
|
128
|
+
const d = resolve(name);
|
|
129
|
+
if (d) {
|
|
130
|
+
if (typeof n.path === 'string') n.path = d.newLocation;
|
|
131
|
+
if (d.fileToken && typeof n.file === 'string') n.file = d.fileToken;
|
|
132
|
+
fixed++;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (Array.isArray(item.artifacts)) {
|
|
140
|
+
for (const a of item.artifacts) {
|
|
141
|
+
if (a && typeof a === 'object') {
|
|
142
|
+
const name = inboxNameFromRef(a.path);
|
|
143
|
+
if (name) { const d = resolve(name); if (d) { a.path = d.newLocation; fixed++; } }
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return fixed;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Repair broken note links across every work-items file (each project's plus
|
|
152
|
+
// the central one), routed through shared.mutateWorkItems so the canonical SQL
|
|
153
|
+
// store AND its JSON mirror stay consistent — the same write path the live
|
|
154
|
+
// rewriter uses. Idempotent: only touches links that are provably broken AND
|
|
155
|
+
// resolvable, so once healed a re-run writes nothing (skipWriteIfUnchanged).
|
|
156
|
+
// Safe to call on every engine boot. Per-file try/catch isolates a bad scope.
|
|
157
|
+
function runNoteLinkBackfill(opts = {}) {
|
|
158
|
+
const shared = require('./shared');
|
|
159
|
+
const minionsDir = opts.minionsDir || shared.MINIONS_DIR;
|
|
160
|
+
const resolve = buildNoteResolver({
|
|
161
|
+
inboxDir: path.join(minionsDir, 'notes', 'inbox'),
|
|
162
|
+
archiveDir: path.join(minionsDir, 'notes', 'archive'),
|
|
163
|
+
knowledgeDir: path.join(minionsDir, 'knowledge'),
|
|
164
|
+
kbCategories: shared.KB_CATEGORIES,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
let config = opts.config;
|
|
168
|
+
if (!config) {
|
|
169
|
+
try { config = JSON.parse(fs.readFileSync(path.join(minionsDir, 'config.json'), 'utf8')); }
|
|
170
|
+
catch { config = {}; }
|
|
171
|
+
}
|
|
172
|
+
const projects = Array.isArray(config.projects) ? config.projects : [];
|
|
173
|
+
const candidates = [];
|
|
174
|
+
for (const p of projects) {
|
|
175
|
+
if (p && p.name) candidates.push(path.join(minionsDir, 'projects', p.name, 'work-items.json'));
|
|
176
|
+
}
|
|
177
|
+
candidates.push(path.join(minionsDir, 'work-items.json'));
|
|
178
|
+
|
|
179
|
+
let scanned = 0, fixedItems = 0, fixedLinks = 0;
|
|
180
|
+
for (const wiPath of candidates) {
|
|
181
|
+
try {
|
|
182
|
+
shared.mutateWorkItems(wiPath, (items) => {
|
|
183
|
+
if (!Array.isArray(items)) return items;
|
|
184
|
+
for (const item of items) {
|
|
185
|
+
if (!item) continue;
|
|
186
|
+
scanned++;
|
|
187
|
+
try {
|
|
188
|
+
const n = repointItemNoteLinks(item, resolve);
|
|
189
|
+
if (n > 0) { fixedItems++; fixedLinks += n; }
|
|
190
|
+
} catch { /* skip unfixable item */ }
|
|
191
|
+
}
|
|
192
|
+
return items;
|
|
193
|
+
});
|
|
194
|
+
} catch { /* per-file isolation — one bad scope can't block the others */ }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return { scanned, fixedItems, fixedLinks };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
module.exports = {
|
|
201
|
+
inboxNameFromRef,
|
|
202
|
+
buildNoteResolver,
|
|
203
|
+
repointItemNoteLinks,
|
|
204
|
+
runNoteLinkBackfill,
|
|
205
|
+
};
|
package/engine/shared.js
CHANGED
|
@@ -7503,6 +7503,7 @@ module.exports = {
|
|
|
7503
7503
|
extractWorkItemPrRef,
|
|
7504
7504
|
extractStructuredWorkItemPrRef,
|
|
7505
7505
|
rewriteInboxRefsAcrossProjects,
|
|
7506
|
+
_artifactNoteFileToken, // exported so the one-time note-link backfill shares the token grammar
|
|
7506
7507
|
getProjectPrScope,
|
|
7507
7508
|
getPrNumber,
|
|
7508
7509
|
getPrDisplayId,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2156",
|
|
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"
|