@yemi33/minions 0.1.2147 → 0.1.2149
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/minions.js +8 -0
- package/dashboard/js/settings.js +9 -0
- package/dashboard/slim/body.html +2 -1
- package/dashboard/slim/js/command-send.js +3 -2
- package/dashboard/slim/js/history.js +9 -2
- package/dashboard/slim/styles.css +88 -10
- package/dashboard.js +5 -0
- package/docs/README.md +1 -0
- package/docs/completion-reports.md +44 -12
- package/docs/project-skills.md +133 -0
- package/engine/ado.js +19 -0
- package/engine/discover-project-skills.js +490 -0
- package/engine/discover-review-skills.js +15 -269
- package/engine/dispatch.js +2 -0
- package/engine/github.js +14 -0
- package/engine/lifecycle.js +17 -0
- package/engine/playbook-intents.js +76 -0
- package/engine/playbook.js +59 -20
- package/engine/shared.js +288 -0
- package/engine/worktree-gc.js +241 -21
- package/engine.js +294 -32
- package/package.json +1 -1
- package/playbooks/fix.md +4 -0
- package/playbooks/implement-shared.md +4 -0
- package/playbooks/implement.md +4 -0
- package/playbooks/plan-to-prd.md +4 -0
- package/playbooks/plan.md +4 -0
- package/playbooks/review.md +3 -3
- package/bin/minions.js.rej +0 -16
- package/dashboard/slim/body.html.rej +0 -11
- package/dashboard/slim/js/command-send.js.rej +0 -12
- package/dashboard/slim/js/history.js.rej +0 -26
- package/dashboard/slim/styles.css.rej +0 -124
- package/docs/README.md.rej +0 -9
- package/docs/onboarding.md.rej +0 -10
|
@@ -1,279 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* engine/discover-review-skills.js —
|
|
2
|
+
* engine/discover-review-skills.js — backward-compat shim for PR #82
|
|
3
|
+
* (W-mq16xtdx001a347e).
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* PR #82 introduced `discover-review-skills.js` as a review-only helper. The
|
|
6
|
+
* follow-on W-mq1cczi90006b21f generalized it into
|
|
7
|
+
* `engine/discover-project-skills.js`, which discovers every project skill
|
|
8
|
+
* and classifies them into an intent vocabulary.
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* Returns a deduplicated array of:
|
|
16
|
-
* { kind: 'skill'|'command'|'slash-command', name, path, oneLineDescription }
|
|
17
|
-
*
|
|
18
|
-
* Contract:
|
|
19
|
-
* - Worktree missing / unreadable → returns [] (never throws)
|
|
20
|
-
* - Single bounded readdir per surface (no recursive grep)
|
|
21
|
-
* - File-count + walltime budget caps the walk in pathological repos
|
|
22
|
-
* - Pure: no engine state mutations, no logging beyond debug
|
|
10
|
+
* This file re-exports the two public PR-82 names so existing imports keep
|
|
11
|
+
* resolving without forcing every consumer to migrate in lockstep. The shim
|
|
12
|
+
* filters to the `review` intent and renders the original "## Project review
|
|
13
|
+
* skills" header copy. New code should import directly from
|
|
14
|
+
* engine/discover-project-skills.js and call `filterByIntents` itself.
|
|
23
15
|
*/
|
|
24
16
|
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
// Word-bounded so we don't match arbitrary substrings like "previewer" or
|
|
30
|
-
// "swarmm" inside unrelated names.
|
|
31
|
-
const KEYWORD_RE = /\b(review|review-swarm|swarm|code-review|pr-review|constellation-review)\b/i;
|
|
32
|
-
|
|
33
|
-
// Slash-command extractor — pulls `/foo-review`, `/review-swarm`, etc. out of
|
|
34
|
-
// markdown prose. Anchored so `/path/to/file` doesn't match (must look like a
|
|
35
|
-
// slash-command token: leading `/` immediately followed by an id with no `/`).
|
|
36
|
-
const SLASH_COMMAND_RE = /(?<![A-Za-z0-9/_-])\/([a-z][a-z0-9-]*)(?![A-Za-z0-9/])/g;
|
|
37
|
-
|
|
38
|
-
const DEFAULTS = {
|
|
39
|
-
maxFilesPerSurface: 50, // hard cap on skill packs / command files we'll read
|
|
40
|
-
maxBytesPerFile: 8 * 1024, // only need frontmatter + first heading
|
|
41
|
-
docsScanMaxBytes: 32 * 1024, // CLAUDE.md / copilot-instructions.md top window
|
|
42
|
-
walltimeMs: 250, // bail rather than block dispatch on pathological FS
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
function _now() { return Date.now(); }
|
|
46
|
-
|
|
47
|
-
function _safeReadHead(filePath, maxBytes) {
|
|
48
|
-
let fd;
|
|
49
|
-
try {
|
|
50
|
-
fd = fs.openSync(filePath, 'r');
|
|
51
|
-
const buf = Buffer.alloc(maxBytes);
|
|
52
|
-
const n = fs.readSync(fd, buf, 0, maxBytes, 0);
|
|
53
|
-
return buf.slice(0, n).toString('utf8');
|
|
54
|
-
} catch { return ''; }
|
|
55
|
-
finally { if (fd !== undefined) { try { fs.closeSync(fd); } catch { /* ignore */ } } }
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function _parseFrontmatter(content) {
|
|
59
|
-
const m = String(content || '').match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
60
|
-
if (!m) return {};
|
|
61
|
-
const out = {};
|
|
62
|
-
for (const line of m[1].split(/\r?\n/)) {
|
|
63
|
-
const lm = line.match(/^([\w-]+):\s*(.*)$/);
|
|
64
|
-
if (!lm) continue;
|
|
65
|
-
out[lm[1].toLowerCase()] = lm[2].trim().replace(/^["']|["']$/g, '');
|
|
66
|
-
}
|
|
67
|
-
return out;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function _firstHeading(content) {
|
|
71
|
-
const lines = String(content || '').split(/\r?\n/);
|
|
72
|
-
for (const line of lines) {
|
|
73
|
-
const m = line.match(/^#+\s+(.+?)\s*$/);
|
|
74
|
-
if (m) return m[1].trim();
|
|
75
|
-
}
|
|
76
|
-
return '';
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function _firstNonEmptyLine(content) {
|
|
80
|
-
const lines = String(content || '').split(/\r?\n/);
|
|
81
|
-
for (const line of lines) {
|
|
82
|
-
const t = line.trim();
|
|
83
|
-
if (t && !t.startsWith('---') && !t.startsWith('#')) return t;
|
|
84
|
-
}
|
|
85
|
-
return '';
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function _truncate(s, max) {
|
|
89
|
-
const text = String(s || '').trim();
|
|
90
|
-
if (text.length <= max) return text;
|
|
91
|
-
return text.slice(0, max - 1).trim() + '…';
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function _discoverSkills(projectPath, opts, deadline) {
|
|
95
|
-
const out = [];
|
|
96
|
-
const skillsDir = path.join(projectPath, '.claude', 'skills');
|
|
97
|
-
let entries;
|
|
98
|
-
try { entries = fs.readdirSync(skillsDir, { withFileTypes: true }); } catch { return out; }
|
|
99
|
-
let scanned = 0;
|
|
100
|
-
for (const ent of entries) {
|
|
101
|
-
if (scanned >= opts.maxFilesPerSurface) break;
|
|
102
|
-
if (_now() > deadline) break;
|
|
103
|
-
if (!ent.isDirectory()) continue;
|
|
104
|
-
const skillPath = path.join(skillsDir, ent.name, 'SKILL.md');
|
|
105
|
-
let stat;
|
|
106
|
-
try { stat = fs.statSync(skillPath); } catch { continue; }
|
|
107
|
-
if (!stat.isFile()) continue;
|
|
108
|
-
scanned += 1;
|
|
109
|
-
const head = _safeReadHead(skillPath, opts.maxBytesPerFile);
|
|
110
|
-
if (!head) continue;
|
|
111
|
-
const fm = _parseFrontmatter(head);
|
|
112
|
-
const name = fm.name || ent.name;
|
|
113
|
-
const desc = fm.description || '';
|
|
114
|
-
const hay = `${name} ${desc}`;
|
|
115
|
-
if (!KEYWORD_RE.test(hay)) continue;
|
|
116
|
-
out.push({
|
|
117
|
-
kind: 'skill',
|
|
118
|
-
name: String(name),
|
|
119
|
-
path: path.relative(projectPath, skillPath).split(path.sep).join('/'),
|
|
120
|
-
oneLineDescription: _truncate(desc || _firstHeading(head) || name, 200),
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
return out;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function _discoverCommands(projectPath, opts, deadline) {
|
|
127
|
-
const out = [];
|
|
128
|
-
const cmdDir = path.join(projectPath, '.claude', 'commands');
|
|
129
|
-
let entries;
|
|
130
|
-
try { entries = fs.readdirSync(cmdDir, { withFileTypes: true }); } catch { return out; }
|
|
131
|
-
let scanned = 0;
|
|
132
|
-
for (const ent of entries) {
|
|
133
|
-
if (scanned >= opts.maxFilesPerSurface) break;
|
|
134
|
-
if (_now() > deadline) break;
|
|
135
|
-
if (!ent.isFile()) continue;
|
|
136
|
-
if (!/\.md$/i.test(ent.name)) continue;
|
|
137
|
-
scanned += 1;
|
|
138
|
-
const base = ent.name.replace(/\.md$/i, '');
|
|
139
|
-
const cmdPath = path.join(cmdDir, ent.name);
|
|
140
|
-
const head = _safeReadHead(cmdPath, opts.maxBytesPerFile);
|
|
141
|
-
const heading = _firstHeading(head);
|
|
142
|
-
const hay = `${base} ${heading}`;
|
|
143
|
-
if (!KEYWORD_RE.test(hay)) continue;
|
|
144
|
-
out.push({
|
|
145
|
-
kind: 'command',
|
|
146
|
-
name: `/${base}`,
|
|
147
|
-
path: path.relative(projectPath, cmdPath).split(path.sep).join('/'),
|
|
148
|
-
oneLineDescription: _truncate(heading || _firstNonEmptyLine(head) || base, 200),
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
return out;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function _extractSlashCommandsFromDoc(content) {
|
|
155
|
-
const found = new Map();
|
|
156
|
-
const text = String(content || '');
|
|
157
|
-
let m;
|
|
158
|
-
SLASH_COMMAND_RE.lastIndex = 0;
|
|
159
|
-
while ((m = SLASH_COMMAND_RE.exec(text)) !== null) {
|
|
160
|
-
const id = m[1];
|
|
161
|
-
if (!KEYWORD_RE.test(id)) continue;
|
|
162
|
-
if (!found.has(id)) found.set(id, m.index);
|
|
163
|
-
}
|
|
164
|
-
return [...found.keys()];
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function _discoverDocSlashCommands(projectPath, opts, deadline, alreadySeen) {
|
|
168
|
-
const out = [];
|
|
169
|
-
const docPaths = [
|
|
170
|
-
path.join(projectPath, '.github', 'copilot-instructions.md'),
|
|
171
|
-
path.join(projectPath, 'CLAUDE.md'),
|
|
172
|
-
];
|
|
173
|
-
for (const docPath of docPaths) {
|
|
174
|
-
if (_now() > deadline) break;
|
|
175
|
-
let stat;
|
|
176
|
-
try { stat = fs.statSync(docPath); } catch { continue; }
|
|
177
|
-
if (!stat.isFile()) continue;
|
|
178
|
-
const head = _safeReadHead(docPath, opts.docsScanMaxBytes);
|
|
179
|
-
if (!head) continue;
|
|
180
|
-
const ids = _extractSlashCommandsFromDoc(head);
|
|
181
|
-
const relDoc = path.relative(projectPath, docPath).split(path.sep).join('/');
|
|
182
|
-
for (const id of ids) {
|
|
183
|
-
const fullName = `/${id}`;
|
|
184
|
-
if (alreadySeen.has(fullName)) continue;
|
|
185
|
-
alreadySeen.add(fullName);
|
|
186
|
-
out.push({
|
|
187
|
-
kind: 'slash-command',
|
|
188
|
-
name: fullName,
|
|
189
|
-
path: relDoc,
|
|
190
|
-
oneLineDescription: `Documented review entrypoint in ${relDoc}`,
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
return out;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* @param {object} args
|
|
199
|
-
* @param {string} args.projectPath — absolute path to the project worktree / checkout
|
|
200
|
-
* @param {object} [args.opts] — override defaults (mostly for tests)
|
|
201
|
-
* @returns {Array<{kind:string,name:string,path:string,oneLineDescription:string}>}
|
|
202
|
-
*/
|
|
203
|
-
function discoverReviewSkills(args) {
|
|
204
|
-
const projectPath = args && args.projectPath;
|
|
205
|
-
if (!projectPath || typeof projectPath !== 'string') return [];
|
|
206
|
-
try { if (!fs.statSync(projectPath).isDirectory()) return []; } catch { return []; }
|
|
207
|
-
|
|
208
|
-
const opts = Object.assign({}, DEFAULTS, args.opts || {});
|
|
209
|
-
const deadline = _now() + Math.max(1, opts.walltimeMs);
|
|
210
|
-
|
|
211
|
-
const seenKey = new Set(); // dedupe across surfaces by `${kind}:${name}`
|
|
212
|
-
const seenSlash = new Set();
|
|
213
|
-
const all = [];
|
|
214
|
-
|
|
215
|
-
for (const entry of _discoverSkills(projectPath, opts, deadline)) {
|
|
216
|
-
const key = `skill:${entry.name}`;
|
|
217
|
-
if (seenKey.has(key)) continue;
|
|
218
|
-
seenKey.add(key);
|
|
219
|
-
all.push(entry);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
for (const entry of _discoverCommands(projectPath, opts, deadline)) {
|
|
223
|
-
const key = `command:${entry.name}`;
|
|
224
|
-
if (seenKey.has(key)) continue;
|
|
225
|
-
seenKey.add(key);
|
|
226
|
-
seenSlash.add(entry.name); // /foo from .claude/commands subsumes doc-mention
|
|
227
|
-
all.push(entry);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
for (const entry of _discoverDocSlashCommands(projectPath, opts, deadline, seenSlash)) {
|
|
231
|
-
const key = `slash-command:${entry.name}`;
|
|
232
|
-
if (seenKey.has(key)) continue;
|
|
233
|
-
seenKey.add(key);
|
|
234
|
-
all.push(entry);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
return all;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* Format a discovery result list as a Markdown block ready to splice into the
|
|
242
|
-
* review playbook. Returns empty string when the list is empty so the caller
|
|
243
|
-
* can no-op cleanly (no stray header, no blank padding lines).
|
|
244
|
-
*
|
|
245
|
-
* @param {Array} entries — output of discoverReviewSkills()
|
|
246
|
-
* @returns {string}
|
|
247
|
-
*/
|
|
248
|
-
function renderReviewSkillsBlock(entries) {
|
|
249
|
-
if (!Array.isArray(entries) || entries.length === 0) return '';
|
|
250
|
-
const lines = [];
|
|
251
|
-
lines.push('## Project review skills (prefer these when applicable)');
|
|
252
|
-
lines.push('');
|
|
253
|
-
lines.push("This project ships purpose-built review tooling. When the diff under review is within scope of one of these skills, INVOKE IT FIRST and use its findings as the primary signal — your verdict can then be anchored to what the skill returned plus any gaps you spot on top.");
|
|
254
|
-
lines.push('');
|
|
255
|
-
for (const e of entries) {
|
|
256
|
-
const kindHint = e.kind === 'skill' ? `skill: \`${e.name}\``
|
|
257
|
-
: e.kind === 'command' ? `\`${e.name}\``
|
|
258
|
-
: `\`${e.name}\``;
|
|
259
|
-
const pathHint = e.path ? ` (\`${e.path}\`)` : '';
|
|
260
|
-
const desc = e.oneLineDescription ? ` — ${e.oneLineDescription}` : '';
|
|
261
|
-
lines.push(`- ${kindHint}${pathHint}${desc}`);
|
|
262
|
-
}
|
|
263
|
-
lines.push('');
|
|
264
|
-
lines.push("Record the skill outcome in your completion report's `meta.review` block (`skillInvoked` or `skillSkipped` — see `docs/completion-reports.md`) so the engine can later measure skill-vs-first-principles signal.");
|
|
265
|
-
return lines.join('\n');
|
|
266
|
-
}
|
|
17
|
+
const {
|
|
18
|
+
discoverReviewSkills,
|
|
19
|
+
renderReviewSkillsBlock,
|
|
20
|
+
} = require('./discover-project-skills');
|
|
267
21
|
|
|
268
22
|
module.exports = {
|
|
269
23
|
discoverReviewSkills,
|
|
270
24
|
renderReviewSkillsBlock,
|
|
271
|
-
// exported for tests
|
|
272
|
-
_internal: {
|
|
273
|
-
KEYWORD_RE,
|
|
274
|
-
SLASH_COMMAND_RE,
|
|
275
|
-
DEFAULTS,
|
|
276
|
-
_parseFrontmatter,
|
|
277
|
-
_extractSlashCommandsFromDoc,
|
|
278
|
-
},
|
|
279
25
|
};
|
package/engine/dispatch.js
CHANGED
|
@@ -400,6 +400,7 @@ function isRetryableFailureReason(reason = '', failureClass = '') {
|
|
|
400
400
|
FAILURE_CLASS.WORKTREE_PREFLIGHT, // pre-spawn worktree validation — recompute will produce the same failure
|
|
401
401
|
FAILURE_CLASS.WORKTREE_DIRTY, // #2996: reused worktree was dirty and could not be auto-healed — non-retryable for this dispatch attempt; the engine quarantined the worktree so the next discovery cycle creates a fresh one
|
|
402
402
|
FAILURE_CLASS.WORKTREE_DIVERGENT, // #2996: reused worktree's local branch had unpushed commits — engine quarantined + backed up the local ref; non-retryable for this dispatch (next discovery creates fresh worktree on origin/<branch>)
|
|
403
|
+
FAILURE_CLASS.WORKTREE_QUARANTINE_ENV_BLOCKED, // W-mq5n1zx5: quarantine rename couldn't release the worktree dir even after retry + force-remove fallback. Non-retryable at the dispatch level so the per-agent retry counter isn't bumped (environmental, not the agent's fault); the WI auto-recovery loop in engine.js#discoverFromWorkItems re-queues without touching _retriesByAgent.
|
|
403
404
|
FAILURE_CLASS.INVALID_KEEP_PROCESSES_WORKDIR, // W-mp6k7ywi000fa33c — keep-pids cwd is not a real git worktree; re-running won't fix the structural issue
|
|
404
405
|
FAILURE_CLASS.INVALID_KEEP_PROCESSES_SCHEMA, // W-mp7i902u000l991f — keep-pids.json failed shape validation; re-running with the same wrong file won't fix it
|
|
405
406
|
FAILURE_CLASS.INVALID_MANAGED_SPAWN, // W-mpbhxg3b000u8411 — managed-spawn.json failed validation; re-running with the same wrong file won't fix it
|
|
@@ -770,6 +771,7 @@ function completeDispatch(id, result = DISPATCH_RESULT.SUCCESS, reason = '', res
|
|
|
770
771
|
[FAILURE_CLASS.WORKTREE_PREFLIGHT]: 'worktree preflight rejected (nested in project root or rootDir collapsed to drive root)',
|
|
771
772
|
[FAILURE_CLASS.WORKTREE_DIRTY]: 'reused worktree had uncommitted edits and could not be auto-healed (#2996) — engine quarantined the dir so the next dispatch creates a fresh worktree',
|
|
772
773
|
[FAILURE_CLASS.WORKTREE_DIVERGENT]: 'reused worktree had unpushed local commits ahead of origin (#2996) — engine backed up the local ref and quarantined the dir so the next dispatch starts from origin/<branch>',
|
|
774
|
+
[FAILURE_CLASS.WORKTREE_QUARANTINE_ENV_BLOCKED]: 'quarantine rename was blocked by the OS (Windows EBUSY/EPERM) even after retry + force-remove fallback — environmental, not the agent\'s fault; auto-recovery loop will re-queue without bumping per-agent retries',
|
|
773
775
|
[FAILURE_CLASS.INVALID_KEEP_PROCESSES_WORKDIR]: 'keep_processes cwd is not a real git worktree (rerun in a `git worktree add` directory)',
|
|
774
776
|
[FAILURE_CLASS.INVALID_KEEP_PROCESSES_SCHEMA]: 'keep-pids.json failed shape validation (wrong keys/types/values — see inbox alert for the canonical shape)',
|
|
775
777
|
[FAILURE_CLASS.INVALID_MANAGED_SPAWN]: 'managed-spawn.json failed validation (bad schema, workdir, or allowlist — see inbox alert)',
|
package/engine/github.js
CHANGED
|
@@ -748,6 +748,20 @@ async function pollPrStatus(config) {
|
|
|
748
748
|
updated = true;
|
|
749
749
|
}
|
|
750
750
|
|
|
751
|
+
// #3079 — Track GitHub target branch name + clear stale merge-conflict
|
|
752
|
+
// dispatch state when the PR is retargeted (e.g. `gh pr edit --base
|
|
753
|
+
// master` after parent PR merges). Mirrors ado.js applyAdoPrMetadata.
|
|
754
|
+
// First-poll seeding (no prior baseRefName) is a no-op so we don't
|
|
755
|
+
// strip state immediately after upsert.
|
|
756
|
+
const nextBaseRefName = String(prData.base?.ref || '').trim();
|
|
757
|
+
if (nextBaseRefName && pr.baseRefName !== nextBaseRefName) {
|
|
758
|
+
if (shared.resetMergeConflictStateOnRetarget(pr, nextBaseRefName)) {
|
|
759
|
+
log('info', `GitHub: PR ${pr.id} retargeted ${pr.baseRefName} → ${nextBaseRefName}, clearing stale MERGE_CONFLICT dispatch records`);
|
|
760
|
+
}
|
|
761
|
+
pr.baseRefName = nextBaseRefName;
|
|
762
|
+
updated = true;
|
|
763
|
+
}
|
|
764
|
+
|
|
751
765
|
// P-w1a3f9b2 — Phase 1.1: plumb mergeable / isDraft / mergeStateStatus /
|
|
752
766
|
// headRefOid onto the PR object so watches captureState (engine/watches.js)
|
|
753
767
|
// and future predicates (Phase 2.1: head-commit-change, mergeable-flipped,
|
package/engine/lifecycle.js
CHANGED
|
@@ -2291,6 +2291,15 @@ function recordPrNoOpFixAttempt(target, cause, source, dispatchItem, branchChang
|
|
|
2291
2291
|
return out;
|
|
2292
2292
|
})()
|
|
2293
2293
|
: {}),
|
|
2294
|
+
// #3079 — MERGE_CONFLICT noops record the composite guard key (source
|
|
2295
|
+
// head + base SHA + target ref name). The same-head guard at
|
|
2296
|
+
// engine.js:5676 reads this back and compares against the current
|
|
2297
|
+
// PR's prMergeConflictGuardKey, so a retarget (target ref change) or
|
|
2298
|
+
// a parent-merge (base SHA change) naturally releases the pause even
|
|
2299
|
+
// when the source head didn't move.
|
|
2300
|
+
...(cause === shared.PR_FIX_CAUSE.MERGE_CONFLICT
|
|
2301
|
+
? { mergeConflictKey: shared.prMergeConflictGuardKey(target) }
|
|
2302
|
+
: {}),
|
|
2294
2303
|
};
|
|
2295
2304
|
target.lastDispatchedAt = now;
|
|
2296
2305
|
target.lastDispatchOutcome = 'noop';
|
|
@@ -2664,6 +2673,14 @@ function updatePrAfterFixError(pr, project, source, options = {}) {
|
|
|
2664
2673
|
|| '');
|
|
2665
2674
|
if (commentKey) next.lastProcessedCommentKey = commentKey;
|
|
2666
2675
|
}
|
|
2676
|
+
// #3079 — Refresh mergeConflictKey from live target state so a
|
|
2677
|
+
// mid-flight retarget reflects in the agent-error record too. The
|
|
2678
|
+
// engine.js skipConflictFix guard prefers mergeConflictKey over
|
|
2679
|
+
// legacy headSha; without this refresh, the prior record's stale
|
|
2680
|
+
// key could re-fire the suppression even after a retarget.
|
|
2681
|
+
if (cause === shared.PR_FIX_CAUSE.MERGE_CONFLICT) {
|
|
2682
|
+
next.mergeConflictKey = shared.prMergeConflictGuardKey(target);
|
|
2683
|
+
}
|
|
2667
2684
|
target._lastDispatchByCause[cause] = next;
|
|
2668
2685
|
result = { cause, indeterminate: true, errorClass };
|
|
2669
2686
|
log('warn', `Updated ${pr.id} → recorded ${cause} agent-error fix attempt (indeterminate=true) — same-head guard relaxed for next tick${errorMessage ? ` (${errorMessage.slice(0, 80)})` : ''}`);
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* engine/playbook-intents.js — W-mq1cczi90006b21f
|
|
3
|
+
*
|
|
4
|
+
* Maps Minions playbook names (work-item types) to the intent buckets each
|
|
5
|
+
* playbook should surface from project-local skill discovery. The mapping is
|
|
6
|
+
* the single source of truth for "which playbook gets which slice of the
|
|
7
|
+
* skills block" — engine/playbook.js consults this at render time.
|
|
8
|
+
*
|
|
9
|
+
* Intent vocabulary (defined in engine/discover-project-skills.js):
|
|
10
|
+
* review | build | fix | test | plan | research | deploy | observability | meta
|
|
11
|
+
*
|
|
12
|
+
* Adding a new playbook? Add an entry here. Playbooks not in this map fall
|
|
13
|
+
* through to NO skills block (the engine treats undefined as the empty set,
|
|
14
|
+
* which is the safe default — agents see no header rather than a noisy one).
|
|
15
|
+
*
|
|
16
|
+
* Adding a new intent? Add the keyword regex to INTENT_KEYWORDS in
|
|
17
|
+
* engine/discover-project-skills.js AND a row to this map (or update an
|
|
18
|
+
* existing playbook to include it). Then document the verb in
|
|
19
|
+
* docs/project-skills.md.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
// Canonical mapping. Lower-cased playbook names; intent values must be
|
|
23
|
+
// members of INTENT_VOCABULARY in engine/discover-project-skills.js.
|
|
24
|
+
const PLAYBOOK_INTENTS = Object.freeze({
|
|
25
|
+
// Review-flavored playbooks — preserves PR-82 behavior verbatim.
|
|
26
|
+
review: ['review'],
|
|
27
|
+
|
|
28
|
+
// Fix dispatches often need to re-run review tooling after the fix lands,
|
|
29
|
+
// and may also touch tests; surface review+test alongside debug/triage.
|
|
30
|
+
fix: ['fix', 'test', 'review'],
|
|
31
|
+
|
|
32
|
+
// Implementation surfaces build/scaffold/codemod skills plus the matching
|
|
33
|
+
// test skills, and research for codebase investigation.
|
|
34
|
+
implement: ['build', 'test', 'research'],
|
|
35
|
+
'implement-shared': ['build', 'test', 'research'],
|
|
36
|
+
|
|
37
|
+
// Planning surfaces planning + research (codebase exploration is part of
|
|
38
|
+
// good planning).
|
|
39
|
+
plan: ['plan', 'research'],
|
|
40
|
+
'plan-to-prd': ['plan', 'research'],
|
|
41
|
+
decompose: ['plan', 'research'],
|
|
42
|
+
|
|
43
|
+
// Doc / verify / build-and-test playbooks — keep tight intent sets so
|
|
44
|
+
// we don't dump build-bumping skills into a docs prompt.
|
|
45
|
+
docs: ['research'],
|
|
46
|
+
verify: ['test', 'review'],
|
|
47
|
+
'build-and-test': ['build', 'test'],
|
|
48
|
+
test: ['test'],
|
|
49
|
+
|
|
50
|
+
// Exploration / asks: research-heavy.
|
|
51
|
+
explore: ['research'],
|
|
52
|
+
|
|
53
|
+
// qa-validate: targeted at test/qa skills.
|
|
54
|
+
'qa-validate': ['test'],
|
|
55
|
+
|
|
56
|
+
// Followup-dispatch is a template (not a top-level playbook); the engine
|
|
57
|
+
// inherits the parent dispatch's intent set when rendering follow-ups.
|
|
58
|
+
// See engine/playbook.js → renderPlaybook for the inheritance plumb.
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Look up the intent set for a playbook name. Returns [] for unknown
|
|
63
|
+
* playbooks (caller treats empty as "render no block").
|
|
64
|
+
*
|
|
65
|
+
* @param {string} playbookName
|
|
66
|
+
* @returns {string[]}
|
|
67
|
+
*/
|
|
68
|
+
function intentsForPlaybook(playbookName) {
|
|
69
|
+
if (!playbookName || typeof playbookName !== 'string') return [];
|
|
70
|
+
return PLAYBOOK_INTENTS[playbookName.toLowerCase()] || [];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = {
|
|
74
|
+
PLAYBOOK_INTENTS,
|
|
75
|
+
intentsForPlaybook,
|
|
76
|
+
};
|
package/engine/playbook.js
CHANGED
|
@@ -339,6 +339,17 @@ const PLAYBOOK_OPTIONAL_VARS = new Set([
|
|
|
339
339
|
'role', // 'primary' | 'co-service'
|
|
340
340
|
'primary_project', // canonical primary project name; empty on single-project sessions
|
|
341
341
|
'co_services_json', // JSON-encoded co-service project names (e.g. ["api","worker"]); empty when none
|
|
342
|
+
// W-mq16xtdx001a347e (PR-82) — project-local review-skill discovery vars.
|
|
343
|
+
// Always rendered (empty string when no discoveries); listed here so the
|
|
344
|
+
// unresolved-var check doesn't complain when discovery returns nothing or
|
|
345
|
+
// projects legitimately resolve it to ''.
|
|
346
|
+
'project_review_skills_block',
|
|
347
|
+
'skip_project_review_skills',
|
|
348
|
+
// W-mq1cczi90006b21f — generic, intent-filtered project skills block.
|
|
349
|
+
// Spliced by review/fix/implement/plan/etc. when discovery + intent map
|
|
350
|
+
// produce matches. Optional for the same reason as the review-only alias.
|
|
351
|
+
'project_skills_block',
|
|
352
|
+
'skip_project_skills',
|
|
342
353
|
]);
|
|
343
354
|
|
|
344
355
|
const PLAYBOOK_REQUIRED_VARS = {
|
|
@@ -695,30 +706,57 @@ function renderPlaybook(type, vars) {
|
|
|
695
706
|
} catch (e) { log('warn', `managed-spawn live-processes inject failed: ${e.message}`); }
|
|
696
707
|
}
|
|
697
708
|
|
|
698
|
-
// W-mq16xtdx001a347e
|
|
699
|
-
//
|
|
700
|
-
//
|
|
701
|
-
//
|
|
702
|
-
//
|
|
703
|
-
//
|
|
704
|
-
//
|
|
705
|
-
|
|
709
|
+
// W-mq16xtdx001a347e (PR #82) + W-mq1cczi90006b21f (generalization) —
|
|
710
|
+
// surface project-local skills relevant to the playbook being dispatched.
|
|
711
|
+
//
|
|
712
|
+
// PR-82 hard-scoped this to type === 'review'. This wiring generalizes it:
|
|
713
|
+
// 1. Discovery walks every project skill / command / documented slash-
|
|
714
|
+
// command (engine/discover-project-skills.js).
|
|
715
|
+
// 2. engine/playbook-intents.js maps playbook → intent set.
|
|
716
|
+
// 3. Discovered entries are filtered to the playbook's intent set and
|
|
717
|
+
// rendered into `project_skills_block`. Empty intent set or zero
|
|
718
|
+
// matches → empty string (no stray header).
|
|
719
|
+
//
|
|
720
|
+
// Header copy: review dispatches keep PR-82's "## Project review skills"
|
|
721
|
+
// header (existing agents/tests grep for it). All other playbooks use the
|
|
722
|
+
// generalized "## Project skills" header.
|
|
723
|
+
//
|
|
724
|
+
// Backward-compat: `project_review_skills_block` and
|
|
725
|
+
// `vars.skip_project_review_skills` remain wired so external consumers of
|
|
726
|
+
// the PR-82 names keep working. They alias to the generic var/flag.
|
|
727
|
+
const skipProjectSkills = !!(vars.skip_project_skills || vars.skip_project_review_skills);
|
|
728
|
+
let projectSkillsBlock = '';
|
|
729
|
+
if (!skipProjectSkills) {
|
|
706
730
|
try {
|
|
707
|
-
const discover = require('./discover-
|
|
731
|
+
const discover = require('./discover-project-skills');
|
|
732
|
+
const { intentsForPlaybook } = require('./playbook-intents');
|
|
708
733
|
const projectPath = matchedProject?.localPath || '';
|
|
709
734
|
if (projectPath) {
|
|
710
|
-
|
|
711
|
-
vars.
|
|
712
|
-
|
|
713
|
-
|
|
735
|
+
// Inherit intent set from parent dispatch type when the engine
|
|
736
|
+
// passes vars.parent_dispatch_type (followup-dispatch flow).
|
|
737
|
+
const effectiveType = vars.parent_dispatch_type || type;
|
|
738
|
+
const intents = intentsForPlaybook(effectiveType);
|
|
739
|
+
if (intents.length > 0) {
|
|
740
|
+
const entries = discover.discoverProjectSkills({ projectPath });
|
|
741
|
+
const filtered = discover.filterByIntents(entries, intents);
|
|
742
|
+
// type === 'review' keeps PR-82 header copy + meta.review.* outcome
|
|
743
|
+
// guidance; everything else uses the generic header that points at
|
|
744
|
+
// meta.skill.* in the completion report.
|
|
745
|
+
projectSkillsBlock = (type === 'review')
|
|
746
|
+
? discover.renderReviewSkillsBlock(filtered)
|
|
747
|
+
: discover.renderProjectSkillsBlock(filtered);
|
|
748
|
+
}
|
|
714
749
|
}
|
|
715
750
|
} catch (e) {
|
|
716
|
-
log('warn', `discover-
|
|
717
|
-
vars.project_review_skills_block = '';
|
|
751
|
+
log('warn', `discover-project-skills failed: ${e.message}`);
|
|
718
752
|
}
|
|
719
|
-
} else {
|
|
720
|
-
vars.project_review_skills_block = '';
|
|
721
753
|
}
|
|
754
|
+
vars.project_skills_block = projectSkillsBlock;
|
|
755
|
+
// Backward-compat alias for any consumer that still references
|
|
756
|
+
// {{project_review_skills_block}} directly. Same content; cheaper than
|
|
757
|
+
// re-rendering. Non-review playbooks that don't reference the alias
|
|
758
|
+
// ignore it (it's in PLAYBOOK_OPTIONAL_VARS).
|
|
759
|
+
vars.project_review_skills_block = projectSkillsBlock;
|
|
722
760
|
|
|
723
761
|
// W-mpeiwz6k0005bf34-c — opt-in qa-validate context block. Injected only
|
|
724
762
|
// when the dispatcher set vars.qa_run_id (truthy) from the work item's
|
|
@@ -1129,10 +1167,11 @@ function buildPrDispatch(agentId, config, project, pr, type, extraVars, taskLabe
|
|
|
1129
1167
|
const vars = {
|
|
1130
1168
|
...buildBaseVars(agentId, config, project),
|
|
1131
1169
|
branch_name: extraVars?.pr_branch || '',
|
|
1132
|
-
// W-mq16xtdx001a347e — honor meta
|
|
1133
|
-
//
|
|
1134
|
-
//
|
|
1170
|
+
// W-mq16xtdx001a347e + W-mq1cczi90006b21f — honor both meta flag names
|
|
1171
|
+
// (skipProjectReviewSkills is the PR-82 alias). Default OFF: review/fix
|
|
1172
|
+
// dispatches surface the project-local skills block.
|
|
1135
1173
|
skip_project_review_skills: !!(meta && meta.skipProjectReviewSkills),
|
|
1174
|
+
skip_project_skills: !!(meta && (meta.skipProjectSkills || meta.skipProjectReviewSkills)),
|
|
1136
1175
|
...extraVars,
|
|
1137
1176
|
task_id: dispatchId,
|
|
1138
1177
|
};
|