@yemi33/minions 0.1.2145 → 0.1.2147
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 +6 -0
- package/dashboard/js/render-pinned.js +14 -1
- package/dashboard.js +11 -0
- package/docs/architecture.excalidraw +3456 -0
- package/docs/completion-reports.md +33 -0
- package/engine/cleanup.js +9 -0
- package/engine/cli.js +23 -11
- package/engine/discover-review-skills.js +279 -0
- package/engine/playbook.js +35 -0
- package/engine/shared.js +210 -1
- package/engine/worktree-gc.js +9 -1
- package/engine.js +75 -8
- package/package.json +1 -1
- package/playbooks/review.md +4 -0
|
@@ -84,6 +84,39 @@ Do **not** invent, regenerate, or share the nonce across dispatches — each spa
|
|
|
84
84
|
| `tests` | string | `pass`, `fail`, `skipped`, `N/A`, or a free-form note like `skipped — relying on PR pipeline`. |
|
|
85
85
|
| `pending` | string | Any remaining work, or `none`. |
|
|
86
86
|
| `followups` | array | Optional. PR-comment follow-up work items the agent dispatched via `POST /api/work-items` with `meta.pr_followup` set. Each entry: `{wi_id, title, reason, parent_comment_id}`. See [PR-comment follow-ups](#pr-comment-follow-ups). |
|
|
87
|
+
| `meta.review` | object | Optional, review tasks only. Records project-local review-skill outcome — see [Review skill outcomes](#review-skill-outcomes). |
|
|
88
|
+
|
|
89
|
+
## Review skill outcomes
|
|
90
|
+
|
|
91
|
+
W-mq16xtdx001a347e. Review playbook renders a `## Project review skills` block at dispatch time when the target project ships `.claude/skills/*` or `.claude/commands/*` with `review` / `swarm` in the name or description (or when `.github/copilot-instructions.md` / `CLAUDE.md` mention a canonical `/…review…` slash-command). When the agent acts on that block, it records the outcome under `meta.review` so later evaluation can compare skill-driven reviews vs. first-principles reviews.
|
|
92
|
+
|
|
93
|
+
All `meta.review` fields are optional and backward-compatible — older agents that never set them stay valid.
|
|
94
|
+
|
|
95
|
+
```json
|
|
96
|
+
{
|
|
97
|
+
"status": "success",
|
|
98
|
+
"verdict": "approved",
|
|
99
|
+
"pr": "https://github.com/owner/repo/pull/123",
|
|
100
|
+
"meta": {
|
|
101
|
+
"review": {
|
|
102
|
+
"skillInvoked": {
|
|
103
|
+
"name": "/review-swarm",
|
|
104
|
+
"path": ".claude/commands/review-swarm.md",
|
|
105
|
+
"kind": "command"
|
|
106
|
+
},
|
|
107
|
+
"skillFindings": 4
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
| Field | Type | Notes |
|
|
114
|
+
|---|---|---|
|
|
115
|
+
| `meta.review.skillInvoked` | object | The project review skill the agent actually ran. Shape: `{name, path, kind}` where `kind` is one of `skill`, `command`, `slash-command` (mirrors the discovery layer in `engine/discover-review-skills.js`). Omit when no skill was invoked. |
|
|
116
|
+
| `meta.review.skillFindings` | number | Count of findings the invoked skill returned. Used later to measure skill quality and the value-add of first-principles review on top. Omit when no skill was invoked. |
|
|
117
|
+
| `meta.review.skillSkipped` | object | Set when a project review skill was available but the agent intentionally chose not to run it (trivial diff, out-of-scope diff, meta-review of the skill itself, etc.). Shape: `{name, reason}`. Mirror the skip rationale into the `Automated checks` section of the PR comment so a human reviewer sees the reasoning. |
|
|
118
|
+
|
|
119
|
+
Dispatchers can suppress the block entirely by setting `meta.skipProjectReviewSkills: true` on the review work item — the playbook then renders identically to the pre-W-mq16xtdx 8-step contract. Use this for meta-reviews of the review skill itself; the engine still accepts `meta.review.skillSkipped` in the report regardless.
|
|
87
120
|
|
|
88
121
|
## `failure_class` enum
|
|
89
122
|
|
package/engine/cleanup.js
CHANGED
|
@@ -634,6 +634,15 @@ async function runCleanup(config, verbose = false) {
|
|
|
634
634
|
if (registered.has(full)) continue;
|
|
635
635
|
let stat; try { stat = fs.statSync(full); } catch { continue; }
|
|
636
636
|
if (stat.mtimeMs >= _twoHoursAgo) continue;
|
|
637
|
+
// W-mq5rwwss000f30a7 — even an "orphan" dir (one git doesn't know
|
|
638
|
+
// about) may still host a live agent if dispatch persistence ran
|
|
639
|
+
// before `git worktree add` finished. Skip when a live dispatch
|
|
640
|
+
// claims it.
|
|
641
|
+
if (shared.isWorktreePathLive(full)) {
|
|
642
|
+
log('info', `Cleanup: skip orphan worktree dir ${full} — live dispatch claims it`);
|
|
643
|
+
shared._writeWorktreeSkipLiveInboxNote(full, 'cleanup.orphanWorktreeDirSweep');
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
637
646
|
try {
|
|
638
647
|
fs.rmSync(full, { recursive: true, force: true });
|
|
639
648
|
cleaned.orphanWorktreeDirs++;
|
package/engine/cli.js
CHANGED
|
@@ -163,18 +163,30 @@ function handleCommand(cmd, args) {
|
|
|
163
163
|
//
|
|
164
164
|
// `minions work --help` used to create ghost work items with title='--help'
|
|
165
165
|
// because the bare-string `title` was truthy and bypassed the `!title`
|
|
166
|
-
// usage check.
|
|
167
|
-
//
|
|
168
|
-
// command-specific `Usage:` output. `pr` and `bridge` handle help inline.
|
|
166
|
+
// usage check. Same class of bug exists in `spawn`/`plan`/`complete` —
|
|
167
|
+
// every command that takes a positional arg and tests it with `if (!arg)`.
|
|
169
168
|
//
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
169
|
+
// Intercept here so a single guard covers the whole command set. `pr` and
|
|
170
|
+
// `bridge` already handle `help`/`--help`/`-h` inline (see their own
|
|
171
|
+
// first-arg branches), so let them route through unchanged.
|
|
172
|
+
//
|
|
173
|
+
// W-mq60aco9: print the *per-command* usage (matching the convention from
|
|
174
|
+
// PR #3054 which added per-positional `Usage:` for work/spawn/plan/complete).
|
|
175
|
+
// Previously this branch printed the global `Commands:` list and short-
|
|
176
|
+
// circuited the per-command guards — so `minions work --help` advertised
|
|
177
|
+
// the wrong help text. Fall back to the global list only for commands
|
|
178
|
+
// missing from CLI_COMMAND_DOCS. The per-command `_isHelpArg` guards in
|
|
179
|
+
// work/spawn/plan/complete remain as defense-in-depth for any caller that
|
|
180
|
+
// bypasses handleCommand.
|
|
181
|
+
if (cmd !== 'pr' && cmd !== 'bridge' && isHelpToken(args && args[0])) {
|
|
182
|
+
const doc = CLI_COMMAND_DOCS[cmd];
|
|
183
|
+
if (doc) {
|
|
184
|
+
console.log(`Usage: minions ${cmd}${doc.args ? ' ' + doc.args : ''}`);
|
|
185
|
+
if (doc.summary) console.log(` ${doc.summary}`);
|
|
186
|
+
} else {
|
|
187
|
+
console.log('Commands:');
|
|
188
|
+
for (const line of formatCliCommandHelpLines()) console.log(line);
|
|
189
|
+
}
|
|
178
190
|
return;
|
|
179
191
|
}
|
|
180
192
|
return commands[cmd](...args);
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* engine/discover-review-skills.js — W-mq16xtdx001a347e
|
|
3
|
+
*
|
|
4
|
+
* Cheap, bounded filesystem walk that surfaces project-local review tooling
|
|
5
|
+
* (skills, slash-commands, copilot-instructions slash-command mentions) so
|
|
6
|
+
* Minions review dispatches reliably steer agents toward the right skill
|
|
7
|
+
* instead of reviewing every PR from first principles.
|
|
8
|
+
*
|
|
9
|
+
* Walk surfaces:
|
|
10
|
+
* - .claude/skills/*\/SKILL.md — name/description frontmatter
|
|
11
|
+
* - .claude/commands/*.md — filename + first heading
|
|
12
|
+
* - .github/copilot-instructions.md — slash-command mentions (top N bytes)
|
|
13
|
+
* - CLAUDE.md — slash-command mentions (top N bytes)
|
|
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
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const fs = require('fs');
|
|
26
|
+
const path = require('path');
|
|
27
|
+
|
|
28
|
+
// Heuristic match — "review", "swarm", or the explicit constellation-review id.
|
|
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
|
+
}
|
|
267
|
+
|
|
268
|
+
module.exports = {
|
|
269
|
+
discoverReviewSkills,
|
|
270
|
+
renderReviewSkillsBlock,
|
|
271
|
+
// exported for tests
|
|
272
|
+
_internal: {
|
|
273
|
+
KEYWORD_RE,
|
|
274
|
+
SLASH_COMMAND_RE,
|
|
275
|
+
DEFAULTS,
|
|
276
|
+
_parseFrontmatter,
|
|
277
|
+
_extractSlashCommandsFromDoc,
|
|
278
|
+
},
|
|
279
|
+
};
|
package/engine/playbook.js
CHANGED
|
@@ -301,6 +301,12 @@ const PLAYBOOK_OPTIONAL_VARS = new Set([
|
|
|
301
301
|
// PR-context vars on non-PR tasks (implement/explore/etc.)
|
|
302
302
|
'pr_id', 'pr_number', 'pr_title', 'pr_branch', 'pr_author', 'pr_url',
|
|
303
303
|
'reviewer',
|
|
304
|
+
// W-mq16xtdx001a347e — review dispatches inject this block (or empty string
|
|
305
|
+
// when discovery finds no project-local review skills). Optional because
|
|
306
|
+
// non-review playbooks never reference it and review-against-skill-less
|
|
307
|
+
// projects legitimately resolve it to ''.
|
|
308
|
+
'project_review_skills_block',
|
|
309
|
+
'skip_project_review_skills',
|
|
304
310
|
// P-e6b3c2d8 — QA Session template vars. session_id / target_kind /
|
|
305
311
|
// flows_raw / managed_spawn_name are required (declared in
|
|
306
312
|
// PLAYBOOK_REQUIRED_VARS['qa-session-setup']); these target_* sub-fields
|
|
@@ -689,6 +695,31 @@ function renderPlaybook(type, vars) {
|
|
|
689
695
|
} catch (e) { log('warn', `managed-spawn live-processes inject failed: ${e.message}`); }
|
|
690
696
|
}
|
|
691
697
|
|
|
698
|
+
// W-mq16xtdx001a347e — review dispatches: discover project-local review
|
|
699
|
+
// tooling (.claude/skills, .claude/commands, copilot-instructions slash-
|
|
700
|
+
// command mentions) and render a "Project review skills" block near the top
|
|
701
|
+
// of the playbook. Default-ON for `review` type; suppressed when the
|
|
702
|
+
// dispatcher passes `meta.skipProjectReviewSkills` (threaded as
|
|
703
|
+
// vars.skip_project_review_skills). Renders empty string when discovery
|
|
704
|
+
// returns no matches so first-principles reviewers see no stray header.
|
|
705
|
+
if (type === 'review' && !vars.skip_project_review_skills) {
|
|
706
|
+
try {
|
|
707
|
+
const discover = require('./discover-review-skills');
|
|
708
|
+
const projectPath = matchedProject?.localPath || '';
|
|
709
|
+
if (projectPath) {
|
|
710
|
+
const entries = discover.discoverReviewSkills({ projectPath });
|
|
711
|
+
vars.project_review_skills_block = discover.renderReviewSkillsBlock(entries);
|
|
712
|
+
} else {
|
|
713
|
+
vars.project_review_skills_block = '';
|
|
714
|
+
}
|
|
715
|
+
} catch (e) {
|
|
716
|
+
log('warn', `discover-review-skills failed: ${e.message}`);
|
|
717
|
+
vars.project_review_skills_block = '';
|
|
718
|
+
}
|
|
719
|
+
} else {
|
|
720
|
+
vars.project_review_skills_block = '';
|
|
721
|
+
}
|
|
722
|
+
|
|
692
723
|
// W-mpeiwz6k0005bf34-c — opt-in qa-validate context block. Injected only
|
|
693
724
|
// when the dispatcher set vars.qa_run_id (truthy) from the work item's
|
|
694
725
|
// `meta.qaRunId`. Mirrors the managed_spawn hint pattern: the playbook is
|
|
@@ -1098,6 +1129,10 @@ function buildPrDispatch(agentId, config, project, pr, type, extraVars, taskLabe
|
|
|
1098
1129
|
const vars = {
|
|
1099
1130
|
...buildBaseVars(agentId, config, project),
|
|
1100
1131
|
branch_name: extraVars?.pr_branch || '',
|
|
1132
|
+
// W-mq16xtdx001a347e — honor meta.skipProjectReviewSkills on PR-built
|
|
1133
|
+
// dispatches (auto re-reviews, conflict-fix follow-ups, etc.). Default
|
|
1134
|
+
// OFF: review dispatches surface the project-local review skills block.
|
|
1135
|
+
skip_project_review_skills: !!(meta && meta.skipProjectReviewSkills),
|
|
1101
1136
|
...extraVars,
|
|
1102
1137
|
task_id: dispatchId,
|
|
1103
1138
|
};
|