@yemi33/minions 0.1.2305 → 0.1.2307
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 +1 -0
- package/dashboard/js/refresh.js +17 -13
- package/dashboard/js/render-other.js +251 -3
- package/dashboard/js/render-plans.js +1 -1
- package/dashboard/js/render-prs.js +4 -1
- package/dashboard/js/render-utils.js +13 -0
- package/dashboard/js/render-work-items.js +11 -13
- package/dashboard/js/settings.js +30 -3
- package/dashboard/slim/body.html +6 -22
- package/dashboard/slim/js/modals-tiles.js +32 -22
- package/dashboard/slim/js/plans.js +24 -516
- package/dashboard/slim/styles.css +10 -7
- package/dashboard/styles.css +58 -0
- package/dashboard.js +66 -9
- package/docs/completion-reports.md +27 -2
- package/docs/copilot-cli-schema.md +31 -1
- package/docs/design-state-storage.md +1 -1
- package/docs/harness-transparency.md +18 -0
- package/docs/live-checkout-mode.md +57 -8
- package/engine/ado-comment.js +5 -2
- package/engine/ado.js +37 -0
- package/engine/cli.js +58 -3
- package/engine/comment-format.js +82 -2
- package/engine/consolidation.js +59 -2
- package/engine/discover-project-skills.js +4 -0
- package/engine/dispatch.js +36 -1
- package/engine/gh-comment.js +8 -3
- package/engine/lifecycle.js +241 -25
- package/engine/live-checkout.js +335 -7
- package/engine/playbook.js +2 -2
- package/engine/pre-dispatch-eval.js +54 -5
- package/engine/queries.js +11 -7
- package/engine/shared.js +96 -6
- package/engine/supervisor.js +144 -19
- package/engine/watchdog.js +10 -0
- package/engine.js +210 -5
- package/package.json +1 -1
- package/playbooks/review.md +2 -0
- package/playbooks/shared-rules.md +9 -0
package/engine/ado.js
CHANGED
|
@@ -42,6 +42,23 @@ function isGitHubProject(project) {
|
|
|
42
42
|
return String(project?.repoHost || '').toLowerCase() === 'github';
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
// #3758 — True when at least one configured project is NOT a GitHub project
|
|
46
|
+
// (i.e., could plausibly need ADO). Tick-loop entry points
|
|
47
|
+
// (pollPrStatus/pollPrHumanComments/reconcilePrs) use this to stay fully
|
|
48
|
+
// inert on a GitHub-only install: without it, a config with adoPollEnabled
|
|
49
|
+
// left at its default attempts az/azureauth token acquisition every poll for
|
|
50
|
+
// no reason, producing a permanent, noisy — and, per the failure this
|
|
51
|
+
// closes, potentially fatal — failure loop.
|
|
52
|
+
//
|
|
53
|
+
// Deliberately does NOT require adoOrg/adoProject to already be populated:
|
|
54
|
+
// several legitimate ADO projects start with those fields blank and get them
|
|
55
|
+
// repaired in-place (from a tracked PR URL, canonical PR id, or `git remote`)
|
|
56
|
+
// by repairAdoProjectConfig() inside forEachActivePr/reconcilePrs itself —
|
|
57
|
+
// gating on adoOrg/adoProject here would skip that repair before it ever ran.
|
|
58
|
+
function hasAdoProjectConfigured(config) {
|
|
59
|
+
return shared.getProjects(config).some(p => !isGitHubProject(p));
|
|
60
|
+
}
|
|
61
|
+
|
|
45
62
|
function isAdoGuid(value) {
|
|
46
63
|
return ADO_GUID_RE.test(String(value || '').trim());
|
|
47
64
|
}
|
|
@@ -1165,6 +1182,13 @@ async function forEachActivePr(config, token, callback) {
|
|
|
1165
1182
|
if (currentPrs[idx].reviewStatus === REVIEW_STATUS.APPROVED && after.reviewStatus !== REVIEW_STATUS.APPROVED) {
|
|
1166
1183
|
after.reviewStatus = REVIEW_STATUS.APPROVED;
|
|
1167
1184
|
}
|
|
1185
|
+
// Never downgrade status from 'merged' — permanent terminal state. A
|
|
1186
|
+
// stale/out-of-order poll response could otherwise overwrite a concurrent
|
|
1187
|
+
// success/merge write-back (mirrors github.js:564-570 and the central
|
|
1188
|
+
// ADO write-back guard at ado.js:1327-1330).
|
|
1189
|
+
if (currentPrs[idx].status === PR_STATUS.MERGED && after.status !== PR_STATUS.MERGED) {
|
|
1190
|
+
after.status = PR_STATUS.MERGED;
|
|
1191
|
+
}
|
|
1168
1192
|
shared.applyPrFieldDelta(currentPrs[idx], before, after);
|
|
1169
1193
|
}
|
|
1170
1194
|
// Don't push if not found — it was deleted by another writer, respect that
|
|
@@ -1328,6 +1352,10 @@ async function forEachActivePr(config, token, callback) {
|
|
|
1328
1352
|
async function pollPrStatus(config) {
|
|
1329
1353
|
_adoPollHadAuthFailure = false; // reset before polling — set again if errors recur
|
|
1330
1354
|
|
|
1355
|
+
// #3758 — inert when no project actually uses ADO. Skip token acquisition
|
|
1356
|
+
// entirely rather than spawning az/azureauth for a GitHub-only install.
|
|
1357
|
+
if (!hasAdoProjectConfigured(config)) return;
|
|
1358
|
+
|
|
1331
1359
|
const token = await getAdoToken();
|
|
1332
1360
|
if (!token) {
|
|
1333
1361
|
const w = consumeNoAdoTokenWarning();
|
|
@@ -1882,6 +1910,9 @@ async function pollPrStatus(config) {
|
|
|
1882
1910
|
// ─── Poll Human Comments on PRs ──────────────────────────────────────────────
|
|
1883
1911
|
|
|
1884
1912
|
async function pollPrHumanComments(config) {
|
|
1913
|
+
// #3758 — inert when no project actually uses ADO.
|
|
1914
|
+
if (!hasAdoProjectConfigured(config)) return;
|
|
1915
|
+
|
|
1885
1916
|
const token = await getAdoToken();
|
|
1886
1917
|
if (!token) return;
|
|
1887
1918
|
|
|
@@ -2100,6 +2131,9 @@ async function pollPrHumanComments(config) {
|
|
|
2100
2131
|
* in pull-requests.json, and add them. Matches PRs to work items by branch name.
|
|
2101
2132
|
*/
|
|
2102
2133
|
async function reconcilePrs(config) {
|
|
2134
|
+
// #3758 — inert when no project actually uses ADO.
|
|
2135
|
+
if (!hasAdoProjectConfigured(config)) return;
|
|
2136
|
+
|
|
2103
2137
|
const token = await getAdoToken();
|
|
2104
2138
|
if (!token) {
|
|
2105
2139
|
const w = consumeNoAdoTokenWarning();
|
|
@@ -2984,4 +3018,7 @@ module.exports = {
|
|
|
2984
3018
|
isPrValidationBuild,
|
|
2985
3019
|
shouldRequeueStaleBuild,
|
|
2986
3020
|
PR_BUILD_REQUEUE_INTERVAL_MS,
|
|
3021
|
+
// #3758 — exported for unit tests of GitHub-only-config inertness.
|
|
3022
|
+
isGitHubProject,
|
|
3023
|
+
hasAdoProjectConfigured,
|
|
2987
3024
|
};
|
package/engine/cli.js
CHANGED
|
@@ -250,7 +250,7 @@ const CLI_COMMAND_DOCS = Object.freeze({
|
|
|
250
250
|
'mcp-sync': { args: '', summary: 'Print harness propagation diagnostic (same source as `minions doctor --harness`; read-only, no writes)' },
|
|
251
251
|
doctor: { args: '[--harness]', summary: 'Check prerequisites and runtime health (--harness: print harness propagation diagnostic)' },
|
|
252
252
|
config: { args: 'set-cli <R> [--model M]', summary: 'Persist defaultCli/defaultModel without starting' },
|
|
253
|
-
pr: { args: 'comment <repo> <prNumber> --agent <id> --kind <k> [--wi <id>] [--resolved] [--harness-file <f>|--harness-json <j>] [--body-file <f>|--body <text>]', summary: 'Post a marker-prepended PR comment via gh (ADO: --resolved posts pre-closed)' },
|
|
253
|
+
pr: { args: 'comment <repo> <prNumber> --agent <id> --kind <k> [--wi <id>] [--resolved] [--harness-file <f>|--harness-json <j>] [--skill-skipped-file <f>|--skill-skipped-json <j>] [--body-file <f>|--body <text>]', summary: 'Post a marker-prepended PR comment via gh (ADO: --resolved posts pre-closed)' },
|
|
254
254
|
bridge: { args: 'status|health|enable|disable', summary: 'Constellation bridge: toggle and inspect the read-only cross-repo feed' },
|
|
255
255
|
});
|
|
256
256
|
|
|
@@ -475,6 +475,49 @@ function resolveHarnessUsedForComment(flags) {
|
|
|
475
475
|
return shared.groundHarnessUsed(parsed, propagated);
|
|
476
476
|
}
|
|
477
477
|
|
|
478
|
+
// resolveSkillSkippedForComment(flags) — W-mr287sc0000uc214. Turn the
|
|
479
|
+
// `minions pr comment` skip flags into a canonical `{name, reason}` record for
|
|
480
|
+
// the posters' `skillSkipped` param (engine/gh-comment.js / engine/ado-comment.js),
|
|
481
|
+
// which fold it via buildMinionsCommentBody -> buildSkippedSkillSection into a
|
|
482
|
+
// VISIBLE callout in the same comment as the verdict.
|
|
483
|
+
//
|
|
484
|
+
// Source precedence mirrors the harness resolver: `--skill-skipped-file <path>`
|
|
485
|
+
// (JSON on disk) wins over the inline `--skill-skipped-json <json>`; neither
|
|
486
|
+
// supplied -> returns undefined so the comment body is byte-identical to today
|
|
487
|
+
// (no callout). Read/parse failures are HARD errors (process.exit(2)). Accepts
|
|
488
|
+
// the raw `{name, reason}` shape or a `meta.skill` / `meta.review` wrapper; a
|
|
489
|
+
// record with no non-empty `name` yields undefined (nothing to surface).
|
|
490
|
+
function resolveSkillSkippedForComment(flags) {
|
|
491
|
+
const fileArg = flags['skill-skipped-file'];
|
|
492
|
+
const jsonArg = flags['skill-skipped-json'];
|
|
493
|
+
|
|
494
|
+
let raw;
|
|
495
|
+
if (fileArg !== undefined) {
|
|
496
|
+
try {
|
|
497
|
+
raw = fs.readFileSync(fileArg, 'utf8');
|
|
498
|
+
} catch (e) {
|
|
499
|
+
console.error(`error: could not read --skill-skipped-file ${fileArg}: ${e.message}`);
|
|
500
|
+
process.exit(2);
|
|
501
|
+
}
|
|
502
|
+
} else if (jsonArg !== undefined) {
|
|
503
|
+
raw = String(jsonArg);
|
|
504
|
+
} else {
|
|
505
|
+
return undefined;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
let parsed;
|
|
509
|
+
try {
|
|
510
|
+
parsed = JSON.parse(raw);
|
|
511
|
+
} catch (e) {
|
|
512
|
+
const src = fileArg !== undefined ? `--skill-skipped-file ${fileArg}` : '--skill-skipped-json';
|
|
513
|
+
console.error(`error: invalid JSON in ${src}: ${e.message}`);
|
|
514
|
+
process.exit(2);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const { _extractSkillSkipped } = require('./comment-format');
|
|
518
|
+
return _extractSkillSkipped(parsed) || undefined;
|
|
519
|
+
}
|
|
520
|
+
|
|
478
521
|
const commands = {
|
|
479
522
|
start(...startArgs) {
|
|
480
523
|
// Apply --cli / --model fleet flags before any engine wiring touches
|
|
@@ -2044,6 +2087,10 @@ const commands = {
|
|
|
2044
2087
|
console.log(' --harness-file <path> JSON file with the { skills, mcpServers, commands, docs } record (wins over --harness-json)');
|
|
2045
2088
|
console.log(' --harness-json <json> same record inline as a JSON string');
|
|
2046
2089
|
console.log(' --dispatch-id <id> grounding manifest override (else basename of $MINIONS_COMPLETION_REPORT)');
|
|
2090
|
+
console.log('');
|
|
2091
|
+
console.log('Skipped in-scope project skill (folded in as a VISIBLE "⚠️ Skipped project skill" callout):');
|
|
2092
|
+
console.log(' --skill-skipped-file <path> JSON file with the { name, reason } skip record (wins over --skill-skipped-json)');
|
|
2093
|
+
console.log(' --skill-skipped-json <json> same record inline as a JSON string (also accepts a meta.skill/meta.review wrapper)');
|
|
2047
2094
|
console.log(' --resolved (ADO only) post the thread pre-resolved/closed — for non-actionable FYI/praise notes');
|
|
2048
2095
|
console.log('');
|
|
2049
2096
|
console.log('Posts a PR comment with the hidden minions marker, the collapsible');
|
|
@@ -2117,6 +2164,12 @@ const commands = {
|
|
|
2117
2164
|
// -> undefined -> body byte-identical to today.
|
|
2118
2165
|
const harnessUsed = resolveHarnessUsedForComment(flags);
|
|
2119
2166
|
|
|
2167
|
+
// W-mr287sc0000uc214 — thread the skipped-in-scope-project-skill self-report
|
|
2168
|
+
// into both posters so it's surfaced as a VISIBLE callout in the same comment
|
|
2169
|
+
// as the verdict (not buried in the completion-report JSON). Absent flags
|
|
2170
|
+
// -> undefined -> body byte-identical to today.
|
|
2171
|
+
const skillSkipped = resolveSkillSkippedForComment(flags);
|
|
2172
|
+
|
|
2120
2173
|
// ── Azure DevOps: <prNumber> --host ado --ado-org --ado-project --repo-id ──
|
|
2121
2174
|
if (isAdo) {
|
|
2122
2175
|
const [prNumberRaw] = positional;
|
|
@@ -2136,7 +2189,7 @@ const commands = {
|
|
|
2136
2189
|
const orgBase = flags['org-base'] || `https://dev.azure.com/${adoOrg}`;
|
|
2137
2190
|
const adoComment = require('./ado-comment');
|
|
2138
2191
|
adoComment.postAdoPrComment({
|
|
2139
|
-
orgBase, project, repositoryId, prNumber, body, agentId, kind, workItemId, harnessUsed,
|
|
2192
|
+
orgBase, project, repositoryId, prNumber, body, agentId, kind, workItemId, harnessUsed, skillSkipped,
|
|
2140
2193
|
resolved: flags.resolved === true,
|
|
2141
2194
|
}).then((result) => {
|
|
2142
2195
|
if (result && result.threadId) {
|
|
@@ -2175,7 +2228,7 @@ const commands = {
|
|
|
2175
2228
|
|
|
2176
2229
|
try {
|
|
2177
2230
|
const result = ghComment.postPrComment({
|
|
2178
|
-
repo, prNumber, body, agentId, kind, workItemId, harnessUsed,
|
|
2231
|
+
repo, prNumber, body, agentId, kind, workItemId, harnessUsed, skillSkipped,
|
|
2179
2232
|
});
|
|
2180
2233
|
if (result.output) console.log(result.output);
|
|
2181
2234
|
} catch (e) {
|
|
@@ -2316,6 +2369,8 @@ module.exports = {
|
|
|
2316
2369
|
_dispatchSessionBranch: dispatchSessionBranch,
|
|
2317
2370
|
// P-7a3c9e21 — harness-flag resolver exported for CLI-threading tests.
|
|
2318
2371
|
_resolveHarnessUsedForComment: resolveHarnessUsedForComment,
|
|
2372
|
+
// W-mr287sc0000uc214 — skipped-project-skill flag resolver exported for tests.
|
|
2373
|
+
_resolveSkillSkippedForComment: resolveSkillSkippedForComment,
|
|
2319
2374
|
// W-mpcyvff6000pf828 (#2653) — heartbeat writer + factory exported for tests
|
|
2320
2375
|
_writeHeartbeatNow: writeHeartbeatNow,
|
|
2321
2376
|
_createHeartbeatInterval: createHeartbeatInterval,
|
package/engine/comment-format.js
CHANGED
|
@@ -36,6 +36,13 @@ const HARNESS_KINDS = [
|
|
|
36
36
|
const HARNESS_SUMMARY_ICON = '\u{1F9F0}'; // 🧰
|
|
37
37
|
const HARNESS_WARN_ICON = '⚠️'; // ⚠️
|
|
38
38
|
|
|
39
|
+
// W-mr287sc0000uc214 — the visible "skipped project skill" callout. The label is
|
|
40
|
+
// a stable literal so buildMinionsCommentBody can dedupe (never stack two
|
|
41
|
+
// callouts) and tests can assert on it. Clamps mirror the harness field caps.
|
|
42
|
+
const SKILL_SKIPPED_LABEL = '**Skipped project skill:**';
|
|
43
|
+
const SKILL_SKIPPED_MAX_NAME_LEN = 200;
|
|
44
|
+
const SKILL_SKIPPED_MAX_REASON_LEN = 1000;
|
|
45
|
+
|
|
39
46
|
// Canonical Minions site URL. shared-rules.md ("Brand link (MANDATORY)") requires
|
|
40
47
|
// the human-readable "Minions" mention in a PR/ADO comment to render as a markdown
|
|
41
48
|
// hyperlink to this destination. Keep this in sync with the literal in
|
|
@@ -119,6 +126,64 @@ function buildHarnessUsedSection(harnessUsed) {
|
|
|
119
126
|
+ `${lines.join('\n')}${legend}\n\n</details>`;
|
|
120
127
|
}
|
|
121
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Extract a canonical `{ name, reason }` skip record from a self-reported skip
|
|
131
|
+
* value. Accepts the raw `{ name, reason }` shape directly, or a `meta` wrapper
|
|
132
|
+
* that carries it — `meta.skill` (`{ skipped: { name, reason } }`) or
|
|
133
|
+
* `meta.review` (`{ skillSkipped: { name, reason } }`) — so callers can pass the
|
|
134
|
+
* completion-report block verbatim. Returns null when no non-empty `name` is
|
|
135
|
+
* present so callers treat "no skip" as a single falsy case.
|
|
136
|
+
* @param {*} input
|
|
137
|
+
* @returns {{name:string, reason:string}|null}
|
|
138
|
+
*/
|
|
139
|
+
function _extractSkillSkipped(input) {
|
|
140
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) return null;
|
|
141
|
+
let node = input;
|
|
142
|
+
if (input.skipped && typeof input.skipped === 'object' && !Array.isArray(input.skipped)) {
|
|
143
|
+
node = input.skipped; // meta.skill shape
|
|
144
|
+
} else if (input.skillSkipped && typeof input.skillSkipped === 'object' && !Array.isArray(input.skillSkipped)) {
|
|
145
|
+
node = input.skillSkipped; // meta.review shape
|
|
146
|
+
}
|
|
147
|
+
const name = typeof node.name === 'string' ? node.name.trim() : '';
|
|
148
|
+
if (!name) return null;
|
|
149
|
+
const reason = typeof node.reason === 'string' ? node.reason.trim() : '';
|
|
150
|
+
return { name, reason };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Render the "skipped project skill" self-report as a VISIBLE Markdown
|
|
155
|
+
* blockquote callout (W-mr287sc0000uc214), e.g.:
|
|
156
|
+
* > ⚠️ **Skipped project skill:** `FMF-review-context` — small 2-file diff…
|
|
157
|
+
*
|
|
158
|
+
* A review/fix agent may self-report that it intentionally skipped an available,
|
|
159
|
+
* in-scope project (review) skill via `meta.review.skillSkipped` /
|
|
160
|
+
* `meta.skill.skipped` (shape `{name, reason}`). That skip used to live ONLY in
|
|
161
|
+
* the completion-report JSON — invisible to a human reading the PR before merge.
|
|
162
|
+
* This is a sibling of buildHarnessUsedSection (same neutral chokepoint, same
|
|
163
|
+
* `_clean` hygiene) but a VISIBLE blockquote rather than a collapsed <details>:
|
|
164
|
+
* the whole point is that a human reviewing the PR sees the skip rationale in the
|
|
165
|
+
* same comment as the APPROVE / REQUEST_CHANGES verdict, before merge.
|
|
166
|
+
*
|
|
167
|
+
* Deliberately platform-NEUTRAL Markdown so the GitHub and ADO posters fold in a
|
|
168
|
+
* byte-identical callout — there is no second renderer to drift. Accepts either
|
|
169
|
+
* the raw `{name, reason}` record or a `meta.skill` / `meta.review` wrapper.
|
|
170
|
+
*
|
|
171
|
+
* Returns '' when the record is absent/malformed (no skip name), so callers can
|
|
172
|
+
* append it unconditionally without emitting a stray callout — reports without
|
|
173
|
+
* meta.review.skillSkipped / meta.skill.skipped render identically to today.
|
|
174
|
+
*
|
|
175
|
+
* @param {object|null} skillSkipped `{name, reason}` or a meta.skill/meta.review wrapper
|
|
176
|
+
* @returns {string} '' when empty, else a one-line `> ⚠️ …` blockquote
|
|
177
|
+
*/
|
|
178
|
+
function buildSkippedSkillSection(skillSkipped) {
|
|
179
|
+
const parsed = _extractSkillSkipped(skillSkipped);
|
|
180
|
+
if (!parsed) return '';
|
|
181
|
+
const name = _clean(parsed.name).slice(0, SKILL_SKIPPED_MAX_NAME_LEN);
|
|
182
|
+
const reasonClean = parsed.reason ? _clean(parsed.reason).slice(0, SKILL_SKIPPED_MAX_REASON_LEN) : '';
|
|
183
|
+
const tail = reasonClean ? ` — ${reasonClean}` : ''; // em dash
|
|
184
|
+
return `> ${HARNESS_WARN_ICON} ${SKILL_SKIPPED_LABEL} \`${name}\`${tail}`;
|
|
185
|
+
}
|
|
186
|
+
|
|
122
187
|
// ── Minions marker + neutral comment-body builder ───────────────────────────
|
|
123
188
|
//
|
|
124
189
|
// The hidden HTML-comment marker and the body composition (marker + harness
|
|
@@ -188,15 +253,26 @@ function _buildMarker({ agentId, kind, workItemId }) {
|
|
|
188
253
|
* Idempotency: if `body` already starts with a minions marker it is returned
|
|
189
254
|
* with the harness/brand transforms applied but no second marker prepended.
|
|
190
255
|
*
|
|
191
|
-
* @param {{agentId:string, kind:string, workItemId?:string, body?:string, harnessUsed?:object}} args
|
|
256
|
+
* @param {{agentId:string, kind:string, workItemId?:string, body?:string, harnessUsed?:object, skillSkipped?:object}} args
|
|
192
257
|
* @returns {string} the final comment body
|
|
193
258
|
*/
|
|
194
|
-
function buildMinionsCommentBody({ agentId, kind, workItemId, body, harnessUsed }) {
|
|
259
|
+
function buildMinionsCommentBody({ agentId, kind, workItemId, body, harnessUsed, skillSkipped }) {
|
|
195
260
|
// Validate the inputs even when the body is pre-marked, so callers can't
|
|
196
261
|
// silently bypass validation by pre-marking their body.
|
|
197
262
|
_validateMarkerInputs({ agentId, kind, workItemId });
|
|
198
263
|
let safeBody = body == null ? '' : String(body);
|
|
199
264
|
|
|
265
|
+
// Fold in the VISIBLE "skipped project skill" callout (W-mr287sc0000uc214)
|
|
266
|
+
// BEFORE the collapsed harness section so a human sees the skip rationale in
|
|
267
|
+
// the same comment as the verdict — not buried in a <details> or reachable
|
|
268
|
+
// only via a separate API call. Dedup on the stable label so a re-marked body
|
|
269
|
+
// already carrying the callout doesn't stack a second one. Absent/malformed
|
|
270
|
+
// skip → '' → no-op (byte-identical to today).
|
|
271
|
+
const skipSection = buildSkippedSkillSection(skillSkipped);
|
|
272
|
+
if (skipSection && !safeBody.includes(SKILL_SKIPPED_LABEL)) {
|
|
273
|
+
safeBody = safeBody ? `${safeBody}\n\n${skipSection}` : skipSection;
|
|
274
|
+
}
|
|
275
|
+
|
|
200
276
|
// Fold the grounded harness-usage digest in as a collapsible <details>
|
|
201
277
|
// section. Append at the END so it survives the marker-idempotency path;
|
|
202
278
|
// dedup on the summary line so a re-marked body that already carries the
|
|
@@ -232,6 +308,7 @@ function parseMinionsMarker(body) {
|
|
|
232
308
|
|
|
233
309
|
module.exports = {
|
|
234
310
|
buildHarnessUsedSection,
|
|
311
|
+
buildSkippedSkillSection,
|
|
235
312
|
linkifyBrandTrailer,
|
|
236
313
|
MINIONS_BRAND_URL,
|
|
237
314
|
// Neutral marker + body builder (consumed by gh-comment.js and ado-comment.js).
|
|
@@ -247,4 +324,7 @@ module.exports = {
|
|
|
247
324
|
_validateMarkerInputs,
|
|
248
325
|
// Exported for tests / advanced callers that want to mirror the kind mapping.
|
|
249
326
|
HARNESS_KINDS,
|
|
327
|
+
// W-mr287sc0000uc214 — skipped-project-skill callout helpers.
|
|
328
|
+
_extractSkillSkipped,
|
|
329
|
+
SKILL_SKIPPED_LABEL,
|
|
250
330
|
};
|
package/engine/consolidation.js
CHANGED
|
@@ -1136,6 +1136,52 @@ function consolidateWithRegex(items, files, config) {
|
|
|
1136
1136
|
|
|
1137
1137
|
// ─── Knowledge Base Classification ───────────────────────────────────────────
|
|
1138
1138
|
|
|
1139
|
+
// Matches the reusable-knowledge section headers agents conventionally use in
|
|
1140
|
+
// their inbox findings notes ("## Patterns / Conventions", "## Bugs & Gotchas",
|
|
1141
|
+
// "## Dependencies", etc. — see notes.md consolidation history). Presence
|
|
1142
|
+
// signals the note carries generalizable knowledge worth a full-body KB copy;
|
|
1143
|
+
// absence means the note is most likely a narrow, one-off deliverable (a
|
|
1144
|
+
// single PR review, a task status update, a one-time plan) that should get a
|
|
1145
|
+
// condensed stub instead of a full-body duplicate (issue #604).
|
|
1146
|
+
// The keyword doesn't have to open the heading — "#### Bugs & Gotchas" (the
|
|
1147
|
+
// exact header this module itself generates for regex-fallback consolidation
|
|
1148
|
+
// entries, see catLabels above) puts "Bugs" before "Gotchas". Allow up to a
|
|
1149
|
+
// few leading heading words (bounded to avoid ReDoS) before the keyword.
|
|
1150
|
+
const REUSABLE_SIGNAL_RE = /^#{1,4}\s*(?:[\w&/-]+\s+){0,4}(patterns?(\s*(&|and|\/)\s*conventions?)?|conventions?|gotchas?|dependencies|best practices?)\b/im;
|
|
1151
|
+
|
|
1152
|
+
function hasReusableSignal(content) {
|
|
1153
|
+
return REUSABLE_SIGNAL_RE.test(content || '');
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
/**
|
|
1157
|
+
* Build a condensed KB stub for narrowly-scoped, one-off notes instead of
|
|
1158
|
+
* duplicating the full body (issue #604 — classifyToKnowledgeBase was copying
|
|
1159
|
+
* every consolidated inbox note's ENTIRE body into knowledge/<category>/,
|
|
1160
|
+
* doubling disk usage and paying a recurring context/token cost on every
|
|
1161
|
+
* future dispatch that primes from knowledge/ before notes/inbox/).
|
|
1162
|
+
*
|
|
1163
|
+
* Deterministic and LLM-free: keeps the note's H1 title (if any) plus its
|
|
1164
|
+
* first substantive paragraph as a short abstract, then links back to the
|
|
1165
|
+
* archived note for anyone who needs the full detail. Existing full-body KB
|
|
1166
|
+
* files already on disk are untouched — this only changes how NEW entries
|
|
1167
|
+
* are written.
|
|
1168
|
+
*/
|
|
1169
|
+
function buildCondensedKbBody(content, titleLine, archiveRelPath) {
|
|
1170
|
+
const body = String(content || '').trim();
|
|
1171
|
+
const withoutTitle = titleLine ? body.replace(/^#\s+.+(\r?\n)+/, '') : body;
|
|
1172
|
+
const firstBlock = (withoutTitle.split(/\r?\n\s*\r?\n/).find(b => b.trim().length > 0) || '').trim();
|
|
1173
|
+
const MAX_LEN = 500;
|
|
1174
|
+
let abstract = firstBlock;
|
|
1175
|
+
if (abstract.length > MAX_LEN) {
|
|
1176
|
+
abstract = abstract.slice(0, MAX_LEN).replace(/\s+\S*$/, '').trim() + '\u2026';
|
|
1177
|
+
}
|
|
1178
|
+
const parts = [];
|
|
1179
|
+
if (titleLine) parts.push(titleLine.trim());
|
|
1180
|
+
parts.push(abstract || '_(no summary available)_');
|
|
1181
|
+
parts.push(`_Full note: \`${archiveRelPath}\`._`);
|
|
1182
|
+
return parts.join('\n\n') + '\n';
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1139
1185
|
async function classifyToKnowledgeBase(items, config) {
|
|
1140
1186
|
|
|
1141
1187
|
if (!fs.existsSync(KNOWLEDGE_DIR)) fs.mkdirSync(KNOWLEDGE_DIR, { recursive: true });
|
|
@@ -1174,9 +1220,18 @@ async function classifyToKnowledgeBase(items, config) {
|
|
|
1174
1220
|
const kbFilename = `${dateStamp()}-${agent}-${titleSlug}.md`;
|
|
1175
1221
|
const kbPath = shared.uniquePath(path.join(categoryDirs[category], kbFilename));
|
|
1176
1222
|
|
|
1177
|
-
|
|
1223
|
+
// Reserve full-body KB copies for content that's actually general/reusable
|
|
1224
|
+
// (patterns, conventions, gotchas); narrowly-scoped one-off notes get a
|
|
1225
|
+
// condensed stub + link back to the archived note instead (issue #604).
|
|
1226
|
+
const reusable = category === 'conventions' || hasReusableSignal(content);
|
|
1227
|
+
const archiveRelPath = `notes/archive/${dateStamp()}-${item.name}`;
|
|
1228
|
+
const kbBody = reusable
|
|
1229
|
+
? content
|
|
1230
|
+
: buildCondensedKbBody(content, titleMatch ? titleMatch[0] : null, archiveRelPath);
|
|
1231
|
+
|
|
1232
|
+
const frontmatter = `---\nsource: ${item.name}\nagent: ${agent}\ncategory: ${category}\ndate: ${dateStamp()}\nreusable: ${reusable}\n---\n\n`;
|
|
1178
1233
|
try {
|
|
1179
|
-
safeWrite(kbPath, frontmatter +
|
|
1234
|
+
safeWrite(kbPath, frontmatter + kbBody);
|
|
1180
1235
|
classified++;
|
|
1181
1236
|
} catch (err) {
|
|
1182
1237
|
log('warn', `Failed to classify ${item.name} to knowledge base: ${err.message}`);
|
|
@@ -1340,4 +1395,6 @@ module.exports = {
|
|
|
1340
1395
|
archiveInboxFiles,
|
|
1341
1396
|
_capNotesPreservingOverflow,
|
|
1342
1397
|
NOTES_MAX_BYTES,
|
|
1398
|
+
hasReusableSignal,
|
|
1399
|
+
buildCondensedKbBody,
|
|
1343
1400
|
};
|
|
@@ -437,6 +437,8 @@ function renderProjectSkillsBlock(entries) {
|
|
|
437
437
|
}
|
|
438
438
|
lines.push('');
|
|
439
439
|
lines.push("Record the skill outcome in your completion report's `meta.skill` block (`invoked` or `skipped` — see `docs/completion-reports.md`) so the engine can later measure skill-vs-first-principles signal.");
|
|
440
|
+
lines.push('');
|
|
441
|
+
lines.push("**Before you record a `skipped` outcome for an in-scope skill, this is a hard constraint, not a suggestion:** re-read that skill's own SKILL.md review criteria / checklist (the content injected above) and confirm your skip reason does not contradict any explicit \"Flag if…\" / \"Verify that…\" / \"Requirements\" item it documents. If the diff trips ANY explicit checklist item the skill covers, skipping is NOT permitted — apply the skill (or at minimum that specific checklist item). A skip reason that waves away a gate the skill explicitly documents (e.g. \"no CHANGELOG needed here\" when the skill's CHANGELOG Requirements section says otherwise) is invalid. When you do skip, the SAME `{name, reason}` you record in `meta.skill.skipped` must be the reason surfaced in the PR comment — no drift between the two.");
|
|
440
442
|
return lines.join('\n');
|
|
441
443
|
}
|
|
442
444
|
|
|
@@ -469,6 +471,8 @@ function renderReviewSkillsBlock(entries) {
|
|
|
469
471
|
}
|
|
470
472
|
lines.push('');
|
|
471
473
|
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.");
|
|
474
|
+
lines.push('');
|
|
475
|
+
lines.push("**Before you record a `skillSkipped` outcome for an in-scope review skill, this is a hard constraint, not a suggestion:** re-read that skill's own SKILL.md review criteria / checklist (the content injected above) and confirm your skip reason does not contradict any explicit \"Flag if…\" / \"Verify that…\" / \"Requirements\" item it documents. If the diff under review trips ANY explicit checklist item the skill covers, skipping is NOT permitted — apply the skill (or at minimum that specific checklist item) and let it inform your verdict. A skip reason that waves away a gate the skill explicitly documents (e.g. \"no CHANGELOG needed for this diff\" when the skill's CHANGELOG Requirements section says otherwise) is invalid. When you do skip, the SAME `{name, reason}` you record in `meta.review.skillSkipped` must be the reason surfaced in the PR comment — no drift between the two.");
|
|
472
476
|
return lines.join('\n');
|
|
473
477
|
}
|
|
474
478
|
|
package/engine/dispatch.js
CHANGED
|
@@ -444,7 +444,10 @@ async function addToDispatchWithValidation(item, opts = {}) {
|
|
|
444
444
|
|
|
445
445
|
let evaluation;
|
|
446
446
|
try {
|
|
447
|
-
evaluation = await validate(wi, {
|
|
447
|
+
evaluation = await validate(wi, {
|
|
448
|
+
engineConfig: config?.engine,
|
|
449
|
+
project: _resolveEvalProjectContext(item, config),
|
|
450
|
+
});
|
|
448
451
|
} catch (e) {
|
|
449
452
|
log('warn', `pre-dispatch-eval: validator threw — failing open: ${e.message}`);
|
|
450
453
|
return addToDispatch(item);
|
|
@@ -458,6 +461,37 @@ async function addToDispatchWithValidation(item, opts = {}) {
|
|
|
458
461
|
return null;
|
|
459
462
|
}
|
|
460
463
|
|
|
464
|
+
/**
|
|
465
|
+
* Build the target-project identity passed to the pre-dispatch validator so the
|
|
466
|
+
* LLM knows which repo the work item targets. Without this the model has no way
|
|
467
|
+
* to know which project it is reasoning about and defaults to describing the
|
|
468
|
+
* Minions engine checkout it executes inside (cwd is hardcoded to MINIONS_DIR),
|
|
469
|
+
* producing false-invalid verdicts backed by fabricated file-existence claims
|
|
470
|
+
* about the wrong repo (W-mr25eg2400078d4b; repro WI W-mr2401ga0007830d
|
|
471
|
+
* targeting constellation). Best-effort — resolves the full project config to
|
|
472
|
+
* derive a canonical repo slug, but degrades gracefully to name/localPath only.
|
|
473
|
+
* @returns {{name?: string, slug?: string, localPath?: string}|null}
|
|
474
|
+
*/
|
|
475
|
+
function _resolveEvalProjectContext(item, config) {
|
|
476
|
+
const metaProject = item?.meta?.project;
|
|
477
|
+
if (!metaProject) return null;
|
|
478
|
+
const name = typeof metaProject === 'string' ? metaProject : metaProject.name;
|
|
479
|
+
const ctx = {};
|
|
480
|
+
if (name) ctx.name = String(name);
|
|
481
|
+
if (metaProject && typeof metaProject === 'object' && metaProject.localPath) {
|
|
482
|
+
ctx.localPath = String(metaProject.localPath);
|
|
483
|
+
}
|
|
484
|
+
try {
|
|
485
|
+
const full = _resolveDispatchProject(name || metaProject, config);
|
|
486
|
+
if (full && typeof full === 'object') {
|
|
487
|
+
if (!ctx.localPath && full.localPath) ctx.localPath = String(full.localPath);
|
|
488
|
+
const slug = shared.getProjectPrScope(full);
|
|
489
|
+
if (slug) ctx.slug = slug;
|
|
490
|
+
}
|
|
491
|
+
} catch { /* best-effort — name/localPath alone still help */ }
|
|
492
|
+
return Object.keys(ctx).length > 0 ? ctx : null;
|
|
493
|
+
}
|
|
494
|
+
|
|
461
495
|
|
|
462
496
|
function _resolveDispatchProject(projectRef, config) {
|
|
463
497
|
if (!projectRef) return null;
|
|
@@ -580,6 +614,7 @@ function isRetryableFailureReason(reason = '', failureClass = '') {
|
|
|
580
614
|
FAILURE_CLASS.LIVE_CHECKOUT_DIRTY, // P-a3f9b204 — live-checkout refused to spawn because operator localPath is dirty; mechanical retry won't fix it (operator must commit/stash/discard). NOTE (W-mqzmkoqt000hbca2): engine.js#spawnAgent ALWAYS passes an explicit `agentRetryable` for this class, so this Set entry is never the actual gate — when auto-cleanup (liveCheckoutAutoReset/AutoStash) is enabled the engine overrides to retryable. Kept as a defensive safety net for any caller that omits the explicit override.
|
|
581
615
|
FAILURE_CLASS.LIVE_CHECKOUT_MID_OPERATION, // P-a7f3c1d9 — live-checkout refused to spawn because the operator tree is mid-operation (in-progress merge/rebase/cherry-pick/bisect or detached HEAD); mechanical retry won't fix it (operator must finish/abort the op or checkout a branch)
|
|
582
616
|
FAILURE_CLASS.LIVE_CHECKOUT_BLOB_FETCH, // PL-live-checkout-reliability-hardening — live-checkout `git checkout <existing-branch>` failed hydrating the tree through the auth-less GVFS cache server (blobless partial clone, headless); deterministic, so mechanical retry just reproduces it (operator must hydrate the branch with their own creds, then re-dispatch)
|
|
617
|
+
FAILURE_CLASS.LIVE_CHECKOUT_WORKTREE_CONFLICT, // W-mr28h2j2000y0de1 — live-checkout `git checkout <existing-branch>` failed because that branch is already checked out in another worktree; structural conflict, so mechanical retry just reproduces it (operator must `git worktree remove` the other tree or finish its WIP, then re-dispatch)
|
|
583
618
|
FAILURE_CLASS.OUTPUT_TRUNCATED, // P-8e4c2a17 — agent stdout exceeded the hard capture cap before the terminal result event; mechanical retry just reproduces the overflow (agent must reduce output volume or the task must be split)
|
|
584
619
|
]);
|
|
585
620
|
if (neverRetry.has(failureClass)) return false;
|
package/engine/gh-comment.js
CHANGED
|
@@ -43,6 +43,7 @@ const { execFileSync: _execFileSync } = require('child_process');
|
|
|
43
43
|
const { resolveTokenForSlug: _defaultResolveTokenForSlug } = require('./gh-token');
|
|
44
44
|
const {
|
|
45
45
|
buildHarnessUsedSection,
|
|
46
|
+
buildSkippedSkillSection,
|
|
46
47
|
linkifyBrandTrailer,
|
|
47
48
|
MINIONS_BRAND_URL,
|
|
48
49
|
// Neutral marker + body builder now lives in comment-format.js so the GitHub
|
|
@@ -155,13 +156,14 @@ function postPrComment({
|
|
|
155
156
|
kind,
|
|
156
157
|
workItemId,
|
|
157
158
|
harnessUsed,
|
|
159
|
+
skillSkipped,
|
|
158
160
|
timeoutMs = 30000,
|
|
159
161
|
execFileSync = _execFileSync,
|
|
160
162
|
resolveTokenForSlug,
|
|
161
163
|
} = {}) {
|
|
162
164
|
_validateRepo(repo);
|
|
163
165
|
_validatePrNumber(prNumber);
|
|
164
|
-
const finalBody = buildMinionsCommentBody({ agentId, kind, workItemId, body, harnessUsed });
|
|
166
|
+
const finalBody = buildMinionsCommentBody({ agentId, kind, workItemId, body, harnessUsed, skillSkipped });
|
|
165
167
|
const file = _writeTempBodyFile(finalBody);
|
|
166
168
|
const env = _resolveTokenEnvForRepo(repo, resolveTokenForSlug);
|
|
167
169
|
try {
|
|
@@ -185,13 +187,14 @@ function postPrReviewComment({
|
|
|
185
187
|
kind,
|
|
186
188
|
workItemId,
|
|
187
189
|
harnessUsed,
|
|
190
|
+
skillSkipped,
|
|
188
191
|
timeoutMs = 30000,
|
|
189
192
|
execFileSync = _execFileSync,
|
|
190
193
|
resolveTokenForSlug,
|
|
191
194
|
} = {}) {
|
|
192
195
|
_validateRepo(repo);
|
|
193
196
|
_validatePrNumber(prNumber);
|
|
194
|
-
const finalBody = buildMinionsCommentBody({ agentId, kind, workItemId, body, harnessUsed });
|
|
197
|
+
const finalBody = buildMinionsCommentBody({ agentId, kind, workItemId, body, harnessUsed, skillSkipped });
|
|
195
198
|
const file = _writeTempBodyFile(finalBody);
|
|
196
199
|
const env = _resolveTokenEnvForRepo(repo, resolveTokenForSlug);
|
|
197
200
|
try {
|
|
@@ -222,6 +225,7 @@ function postPrReview({
|
|
|
222
225
|
kind,
|
|
223
226
|
workItemId,
|
|
224
227
|
harnessUsed,
|
|
228
|
+
skillSkipped,
|
|
225
229
|
timeoutMs = 30000,
|
|
226
230
|
execFileSync = _execFileSync,
|
|
227
231
|
resolveTokenForSlug,
|
|
@@ -234,7 +238,7 @@ function postPrReview({
|
|
|
234
238
|
}
|
|
235
239
|
_validateRepo(repo);
|
|
236
240
|
_validatePrNumber(prNumber);
|
|
237
|
-
const finalBody = buildMinionsCommentBody({ agentId, kind, workItemId, body, harnessUsed });
|
|
241
|
+
const finalBody = buildMinionsCommentBody({ agentId, kind, workItemId, body, harnessUsed, skillSkipped });
|
|
238
242
|
const file = _writeTempBodyFile(finalBody);
|
|
239
243
|
const env = _resolveTokenEnvForRepo(repo, resolveTokenForSlug);
|
|
240
244
|
try {
|
|
@@ -254,6 +258,7 @@ module.exports = {
|
|
|
254
258
|
// Builders / parsers (pure functions — usable from anywhere)
|
|
255
259
|
buildMinionsCommentBody,
|
|
256
260
|
buildHarnessUsedSection, // re-export of engine/comment-format.js for comment callers
|
|
261
|
+
buildSkippedSkillSection, // re-export of engine/comment-format.js for comment callers
|
|
257
262
|
linkifyBrandTrailer, // re-export of engine/comment-format.js for comment callers
|
|
258
263
|
MINIONS_BRAND_URL, // re-export of engine/comment-format.js for comment callers
|
|
259
264
|
parseMinionsMarker,
|