@yemi33/minions 0.1.1954 → 0.1.1956
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/dashboard.js +10 -1
- package/engine/copilot-models.json +5 -0
- package/engine/dispatch.js +2 -0
- package/engine/keep-process-sweep.js +13 -0
- package/engine/pre-dispatch-eval.js +48 -5
- package/engine/shared.js +1 -0
- package/engine.js +145 -55
- package/package.json +1 -1
- package/prompts/cc-system.md +2 -0
- package/knowledge/agents/dallas.md +0 -9
- package/knowledge/agents/lambert.md +0 -9
- package/knowledge/agents/ralph.md +0 -9
- package/knowledge/agents/rebecca.md +0 -9
- package/knowledge/agents/ripley.md +0 -9
package/dashboard.js
CHANGED
|
@@ -4258,6 +4258,15 @@ const server = http.createServer(async (req, res) => {
|
|
|
4258
4258
|
}
|
|
4259
4259
|
if (body.skipPr) item.skipPr = true;
|
|
4260
4260
|
if (body.oneShot) item.oneShot = true;
|
|
4261
|
+
// body.meta passthrough (W-mp7hypmw000jf329) — engine reads
|
|
4262
|
+
// item.meta.keep_processes and meta.keep_processes_skip_workdir_check
|
|
4263
|
+
// (engine.js:3891, engine.js:1584). Without this copy, the
|
|
4264
|
+
// meta.keep_processes documentation in prompts/cc-system.md and
|
|
4265
|
+
// the documented `meta?` /api/routes parameter would silently no-op.
|
|
4266
|
+
// Shallow copy of plain objects only — arrays/null/primitives are dropped.
|
|
4267
|
+
if (body.meta && typeof body.meta === 'object' && !Array.isArray(body.meta)) {
|
|
4268
|
+
item.meta = { ...body.meta };
|
|
4269
|
+
}
|
|
4261
4270
|
copyWorkItemPrFields(item, body);
|
|
4262
4271
|
const createResult = createWorkItemWithDedup(wiPath, item);
|
|
4263
4272
|
if (!createResult.created) {
|
|
@@ -7956,7 +7965,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
7956
7965
|
}},
|
|
7957
7966
|
|
|
7958
7967
|
// Work items
|
|
7959
|
-
{ method: 'POST', path: '/api/work-items', desc: 'Create a new work item', params: 'title, type?, description?, priority?, project?, agent?, agents?, scope?, references?, acceptanceCriteria?', handler: handleWorkItemsCreate },
|
|
7968
|
+
{ method: 'POST', path: '/api/work-items', desc: 'Create a new work item', params: 'title, type?, description?, priority?, project?, agent?, agents?, scope?, references?, acceptanceCriteria?, skipPr?, oneShot?, meta?', handler: handleWorkItemsCreate },
|
|
7960
7969
|
{ method: 'POST', path: '/api/work-items/update', desc: 'Edit a pending/failed work item', params: 'id, source?, title?, description?, type?, priority?, agent?, references?, acceptanceCriteria?', handler: handleWorkItemsUpdate },
|
|
7961
7970
|
{ method: 'POST', path: '/api/work-items/retry', desc: 'Reset a failed/dispatched item to pending', params: 'id, source?', handler: handleWorkItemsRetry },
|
|
7962
7971
|
{ method: 'POST', path: '/api/work-items/delete', desc: 'Remove a work item, kill agent, clear dispatch', params: 'id, source?', handler: handleWorkItemsDelete },
|
package/engine/dispatch.js
CHANGED
|
@@ -345,6 +345,7 @@ function isRetryableFailureReason(reason = '', failureClass = '') {
|
|
|
345
345
|
FAILURE_CLASS.PERMISSION_BLOCKED,
|
|
346
346
|
FAILURE_CLASS.WORKTREE_PREFLIGHT, // pre-spawn worktree validation — recompute will produce the same failure
|
|
347
347
|
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
|
|
348
|
+
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
|
|
348
349
|
]);
|
|
349
350
|
if (neverRetry.has(failureClass)) return false;
|
|
350
351
|
}
|
|
@@ -600,6 +601,7 @@ function completeDispatch(id, result = DISPATCH_RESULT.SUCCESS, reason = '', res
|
|
|
600
601
|
[FAILURE_CLASS.PERMISSION_BLOCKED]: 'permission or auth failure',
|
|
601
602
|
[FAILURE_CLASS.WORKTREE_PREFLIGHT]: 'worktree preflight rejected (nested in project root or rootDir collapsed to drive root)',
|
|
602
603
|
[FAILURE_CLASS.INVALID_KEEP_PROCESSES_WORKDIR]: 'keep_processes cwd is not a real git worktree (rerun in a `git worktree add` directory)',
|
|
604
|
+
[FAILURE_CLASS.INVALID_KEEP_PROCESSES_SCHEMA]: 'keep-pids.json failed shape validation (wrong keys/types/values — see inbox alert for the canonical shape)',
|
|
603
605
|
[FAILURE_CLASS.UNKNOWN]: 'unknown error',
|
|
604
606
|
};
|
|
605
607
|
const classLabel = failureClass ? (CLASS_LABELS[failureClass] || failureClass) : '';
|
|
@@ -344,6 +344,11 @@ function buildKeepProcessesHint(opts) {
|
|
|
344
344
|
const agentId = opts.agentId || '<your-agent-id>';
|
|
345
345
|
const wiId = opts.workItemId || '<this-work-item-id>';
|
|
346
346
|
const minionsDir = opts.minionsDir || '<minions-dir>';
|
|
347
|
+
// W-mp7i902u000l991f — dashboard port for the self-check curl. Caller can
|
|
348
|
+
// pass an explicit `dashboardPort`; falls back to 7331 (the documented
|
|
349
|
+
// default for `dashboard.js`).
|
|
350
|
+
const portIn = Number(opts.dashboardPort);
|
|
351
|
+
const dashboardPort = Number.isFinite(portIn) && portIn > 0 ? portIn : 7331;
|
|
347
352
|
const lines = [
|
|
348
353
|
'',
|
|
349
354
|
'',
|
|
@@ -377,6 +382,14 @@ function buildKeepProcessesHint(opts) {
|
|
|
377
382
|
'',
|
|
378
383
|
'After you write the file, the engine\'s sweep removes it automatically when its TTL fires (it kills the kept PIDs at that point) or when all declared PIDs are already dead. Humans can also kill any kept PID early from the dashboard.',
|
|
379
384
|
'',
|
|
385
|
+
'## Verify before exit (REQUIRED)',
|
|
386
|
+
'',
|
|
387
|
+
'Before you write your completion report, verify the engine accepted your file:',
|
|
388
|
+
'',
|
|
389
|
+
' curl -s http://localhost:' + dashboardPort + '/api/keep-processes',
|
|
390
|
+
'',
|
|
391
|
+
'Find your entry (`agentId: "' + agentId + '"`) and confirm `valid: true`. If `valid: false`, the `reason` field tells you what to fix (e.g. `pids-missing` means you used a wrong top-level key like `processes` instead of `pids`; `ttl-too-long` means your `expires_at` is too far in the future; `expires_at-missing` means the field is absent or non-string). Rewrite the file with the exact shape above and re-check. If you exit with `valid: false`, the engine will mark the dispatch as ERROR (non-retryable) and your kept PIDs will be reaped.',
|
|
392
|
+
'',
|
|
380
393
|
];
|
|
381
394
|
return lines.join('\n');
|
|
382
395
|
}
|
|
@@ -34,7 +34,12 @@ const { callLLM } = require('./llm');
|
|
|
34
34
|
|
|
35
35
|
const SYSTEM_PROMPT = 'Output only JSON.';
|
|
36
36
|
const DEFAULT_TIMEOUT_MS = 60000;
|
|
37
|
-
|
|
37
|
+
// NOTE: no hardcoded default model. Hardcoding `'haiku'` (Claude shorthand)
|
|
38
|
+
// broke the validator on Copilot fleets — copilot.js has
|
|
39
|
+
// `modelShorthands: false`, so it shelled out `copilot --model haiku ...`,
|
|
40
|
+
// the CLI rejected the unknown id, and every dispatch hit `LLM exit 1` and
|
|
41
|
+
// fell open. Resolve the model from the caller's engine config instead
|
|
42
|
+
// (W-mp7ig18j000o7996).
|
|
38
43
|
// Minimum trimmed description length that justifies a description-only LLM
|
|
39
44
|
// validation. Below this we fail-open without burning an LLM call — short
|
|
40
45
|
// descriptions don't carry enough signal for the gate to add value.
|
|
@@ -89,6 +94,37 @@ function _parseResponse(text) {
|
|
|
89
94
|
try { return JSON.parse(body); } catch { return null; }
|
|
90
95
|
}
|
|
91
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Resolve the model for the validator's `callLLM` call.
|
|
99
|
+
*
|
|
100
|
+
* Priority:
|
|
101
|
+
* 1. `opts.model` — explicit caller override (preserves back-compat for
|
|
102
|
+
* unit tests that pin a fixture model).
|
|
103
|
+
* 2. `shared.resolveCcModel(<engine sub-config>)` — defer to whatever the
|
|
104
|
+
* engine has configured for its CC/direct LLM path. Tolerates both the
|
|
105
|
+
* flat shape (`{ ccModel, defaultCli, ... }`, which is what
|
|
106
|
+
* `engine/dispatch.js addToDispatchWithValidation` actually passes today)
|
|
107
|
+
* and a nested shape (`{ engine: { ccModel, ... } }`) for callers that
|
|
108
|
+
* hand in the whole config blob.
|
|
109
|
+
* 3. `undefined` — let the runtime adapter pick its own default. The Claude
|
|
110
|
+
* and Copilot adapters skip `--model` entirely when the value is falsy
|
|
111
|
+
* (engine/runtimes/claude.js:231, engine/runtimes/copilot.js:456), so
|
|
112
|
+
* Copilot ends up reading the user's `~/.copilot/settings.json` model.
|
|
113
|
+
*
|
|
114
|
+
* Returning `undefined` is intentional and must be preserved through to the
|
|
115
|
+
* `callLLM` call — passing `''` or `null` instead would cause the adapter to
|
|
116
|
+
* still skip the flag (truthy check), but it muddies the contract.
|
|
117
|
+
*/
|
|
118
|
+
function _resolveModel(opts) {
|
|
119
|
+
if (opts && opts.model) return opts.model;
|
|
120
|
+
const engineCfg = opts && opts.engineConfig
|
|
121
|
+
? (opts.engineConfig.engine && typeof opts.engineConfig.engine === 'object'
|
|
122
|
+
? opts.engineConfig.engine
|
|
123
|
+
: opts.engineConfig)
|
|
124
|
+
: undefined;
|
|
125
|
+
return shared.resolveCcModel(engineCfg);
|
|
126
|
+
}
|
|
127
|
+
|
|
92
128
|
/**
|
|
93
129
|
* Validate a work item's acceptance criteria with a fast/cheap LLM call.
|
|
94
130
|
*
|
|
@@ -101,9 +137,15 @@ function _parseResponse(text) {
|
|
|
101
137
|
* @param {object} workItem - work item with `acceptance_criteria` (or
|
|
102
138
|
* `acceptanceCriteria`) plus title/description for context.
|
|
103
139
|
* @param {object} [opts]
|
|
104
|
-
* @param {object} [opts.engineConfig] -
|
|
105
|
-
*
|
|
106
|
-
*
|
|
140
|
+
* @param {object} [opts.engineConfig] - engine sub-config (flat
|
|
141
|
+
* `{ ccModel, defaultCli, ... }` as passed by
|
|
142
|
+
* `engine/dispatch.js addToDispatchWithValidation`, or the nested
|
|
143
|
+
* `{ engine: { ... } }` whole-config shape). Used both for
|
|
144
|
+
* runtime/model resolution in `callLLM` and for resolving the validator's
|
|
145
|
+
* own model via `shared.resolveCcModel`.
|
|
146
|
+
* @param {string} [opts.model] - explicit model override; otherwise the
|
|
147
|
+
* engine's CC model resolution applies, falling back to `undefined`
|
|
148
|
+
* (adapter default) when nothing is configured.
|
|
107
149
|
* @param {number} [opts.timeout] - LLM timeout in ms.
|
|
108
150
|
* @returns {Promise<{valid: boolean, reason: string}>}
|
|
109
151
|
*/
|
|
@@ -123,7 +165,7 @@ async function validateAcceptanceCriteria(workItem, opts = {}) {
|
|
|
123
165
|
result = await callLLM(prompt, SYSTEM_PROMPT, {
|
|
124
166
|
timeout: Number(opts.timeout) > 0 ? Number(opts.timeout) : DEFAULT_TIMEOUT_MS,
|
|
125
167
|
label: 'pre-dispatch-eval',
|
|
126
|
-
model: opts
|
|
168
|
+
model: _resolveModel(opts),
|
|
127
169
|
maxTurns: 1,
|
|
128
170
|
direct: true,
|
|
129
171
|
engineConfig: opts.engineConfig,
|
|
@@ -166,5 +208,6 @@ module.exports = {
|
|
|
166
208
|
_extractDescription,
|
|
167
209
|
_buildPrompt,
|
|
168
210
|
_parseResponse,
|
|
211
|
+
_resolveModel,
|
|
169
212
|
DESCRIPTION_MIN_CHARS,
|
|
170
213
|
};
|
package/engine/shared.js
CHANGED
|
@@ -2080,6 +2080,7 @@ const FAILURE_CLASS = {
|
|
|
2080
2080
|
COMPLETION_NONCE_MISMATCH: 'completion-nonce-mismatch', // P-d2a8f6c1: completion JSON nonce did not match the per-spawn value injected via MINIONS_COMPLETION_NONCE — treat as forged/untrusted; ignore PR/noop/status fields from the report
|
|
2081
2081
|
WORKTREE_PREFLIGHT: 'worktree-preflight', // Pre-spawn worktree validation rejected (nested-in-project, drive-root collapse) — never retryable
|
|
2082
2082
|
INVALID_KEEP_PROCESSES_WORKDIR: 'invalid-keep-processes-workdir', // W-mp6k7ywi000fa33c: keep-pids.json declared a cwd that is not a real git worktree (likely a selective copy of the repo) — never retryable; agent must rerun in a real worktree
|
|
2083
|
+
INVALID_KEEP_PROCESSES_SCHEMA: 'invalid-keep-processes-schema', // W-mp7i902u000l991f: keep-pids.json failed validation for a reason other than workdir (pids-missing, ttl-too-long, expires_at-missing, pids-too-many, port-invalid, etc.) — agent wrote the wrong shape; never retryable until they fix the file
|
|
2083
2084
|
UNKNOWN: 'unknown', // Unclassified failure
|
|
2084
2085
|
};
|
|
2085
2086
|
const ESCALATION_POLICY = {
|
package/engine.js
CHANGED
|
@@ -2092,69 +2092,145 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
2092
2092
|
|
|
2093
2093
|
// W-mp6k7ywi000fa33c — keep_processes acceptance gate. When the work
|
|
2094
2094
|
// item carried `meta.keep_processes: true` and produced a keep-pids.json
|
|
2095
|
-
// sidecar
|
|
2096
|
-
//
|
|
2097
|
-
//
|
|
2098
|
-
//
|
|
2099
|
-
//
|
|
2100
|
-
//
|
|
2101
|
-
//
|
|
2102
|
-
//
|
|
2095
|
+
// sidecar that fails validation, reject the file and force the dispatch
|
|
2096
|
+
// to fail with a dedicated failure class. spawn-agent's close handler
|
|
2097
|
+
// has already reaped the kept PIDs (the same validation runs there via
|
|
2098
|
+
// computeReapPlan), so the engine's job here is just (a) flip the
|
|
2099
|
+
// dispatch outcome to ERROR, (b) emit an inbox alert that the
|
|
2100
|
+
// responsible agent will see on its next dispatch, and (c) delete the
|
|
2101
|
+
// now-rejected sidecar so it does not accumulate.
|
|
2102
|
+
//
|
|
2103
|
+
// W-mp7i902u000l991f — symmetric gate. The original W-mp6k7ywi000fa33c
|
|
2104
|
+
// ship only flipped ERROR for `invalid-workdir:*` rejections. Other
|
|
2105
|
+
// schema failures (pids-missing, ttl-too-long, expires_at-missing,
|
|
2106
|
+
// pids-too-many, port-invalid, etc.) merely logged a warning while the
|
|
2107
|
+
// dispatch finished green — visible failure: a green WI with dead
|
|
2108
|
+
// processes (Ralph's W-mp7g7byh000h4b4a). Now ANY rejection is a hard
|
|
2109
|
+
// non-retryable failure when keep_processes was opted in. Workdir
|
|
2110
|
+
// rejections still get their own failure_class
|
|
2111
|
+
// (INVALID_KEEP_PROCESSES_WORKDIR) and inbox-slug to preserve audit
|
|
2112
|
+
// history; everything else routes to INVALID_KEEP_PROCESSES_SCHEMA with
|
|
2113
|
+
// a slug of `keep-processes-schema-<agentId>`.
|
|
2103
2114
|
//
|
|
2104
2115
|
// Per-WI override: `meta.keep_processes_skip_workdir_check: true` skips
|
|
2105
|
-
//
|
|
2106
|
-
|
|
2116
|
+
// workdir validation only — schema validation always runs because pure
|
|
2117
|
+
// shape failures are independent of the workdir feature.
|
|
2118
|
+
let keepProcessesAcceptanceFailure = null;
|
|
2107
2119
|
{
|
|
2108
2120
|
const _wiMeta = dispatchItem.meta?.item?.meta || {};
|
|
2109
2121
|
const _kpEnabled = !!_wiMeta.keep_processes
|
|
2110
2122
|
|| !!dispatchItem.meta?.keep_processes;
|
|
2111
2123
|
const _kpSkipWorkdir = !!_wiMeta.keep_processes_skip_workdir_check
|
|
2112
2124
|
|| !!dispatchItem.meta?.keep_processes_skip_workdir_check;
|
|
2113
|
-
if (_kpEnabled
|
|
2125
|
+
if (_kpEnabled) {
|
|
2114
2126
|
try {
|
|
2115
2127
|
const keepProcessSweep = require('./engine/keep-process-sweep');
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2128
|
+
// First evaluate with workdir validation OFF so we always catch
|
|
2129
|
+
// schema failures (pids-missing, ttl-too-long, etc.) regardless
|
|
2130
|
+
// of whether requireGitWorkdir is enabled or skipped per-WI.
|
|
2131
|
+
const schemaResult = keepProcessSweep.evaluateKeepPidsAcceptance(agentId, { requireGitWorkdir: false });
|
|
2132
|
+
// Then, only when the schema passes AND workdir validation is
|
|
2133
|
+
// gated on, run the workdir check separately. This keeps schema
|
|
2134
|
+
// and workdir failure classes cleanly distinguished.
|
|
2135
|
+
let evalResult = schemaResult;
|
|
2136
|
+
let isWorkdirRejection = false;
|
|
2137
|
+
const workdirGateOn = !_kpSkipWorkdir && ENGINE_DEFAULTS.keepProcesses?.requireGitWorkdir !== false;
|
|
2138
|
+
if (schemaResult.exists && schemaResult.accepted && workdirGateOn) {
|
|
2139
|
+
const workdirResult = keepProcessSweep.evaluateKeepPidsAcceptance(agentId, { requireGitWorkdir: true });
|
|
2140
|
+
if (workdirResult.exists && !workdirResult.accepted && workdirResult.isWorkdirRejection) {
|
|
2141
|
+
evalResult = workdirResult;
|
|
2142
|
+
isWorkdirRejection = true;
|
|
2143
|
+
}
|
|
2144
|
+
} else if (schemaResult.exists && !schemaResult.accepted) {
|
|
2145
|
+
// Schema failure — never workdir even if reason string happens
|
|
2146
|
+
// to start with `invalid-workdir:` (it can't, given we set
|
|
2147
|
+
// requireGitWorkdir:false above).
|
|
2148
|
+
isWorkdirRejection = false;
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
if (evalResult.exists && !evalResult.accepted) {
|
|
2152
|
+
keepProcessesAcceptanceFailure = {
|
|
2119
2153
|
reason: evalResult.reason,
|
|
2120
2154
|
cwd: evalResult.recordedCwd || '',
|
|
2121
2155
|
filePath: evalResult.filePath,
|
|
2156
|
+
isWorkdirRejection,
|
|
2157
|
+
parsedRaw: evalResult.parsedRaw || null,
|
|
2122
2158
|
};
|
|
2123
2159
|
// Delete the sidecar so it does not anchor stale PIDs on later
|
|
2124
2160
|
// sweeps and does not show up as "malformed" forever.
|
|
2125
2161
|
try { fs.unlinkSync(evalResult.filePath); } catch (_e) { /* gone or busy */ }
|
|
2126
|
-
|
|
2162
|
+
const failKind = isWorkdirRejection ? 'workdir' : 'schema';
|
|
2163
|
+
log('warn', `keep-processes acceptance: REJECTED ${agentId} (${id}) — ${failKind}: ${evalResult.reason}; PIDs reaped by spawn-agent, sidecar deleted`);
|
|
2127
2164
|
// Emit inbox alert so the agent sees this on its next turn.
|
|
2128
2165
|
try {
|
|
2129
2166
|
const wiId = dispatchItem.meta?.item?.id || '';
|
|
2130
|
-
const slug =
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2167
|
+
const slug = isWorkdirRejection
|
|
2168
|
+
? `keep-processes-workdir-${agentId}`
|
|
2169
|
+
: `keep-processes-schema-${agentId}`;
|
|
2170
|
+
const ttlIn = Number(_wiMeta.keep_processes_ttl_minutes
|
|
2171
|
+
|| dispatchItem.meta?.keep_processes_ttl_minutes);
|
|
2172
|
+
const canonicalHint = (() => {
|
|
2173
|
+
try {
|
|
2174
|
+
return keepProcessSweep.buildKeepProcessesHint({
|
|
2175
|
+
agentId,
|
|
2176
|
+
workItemId: wiId,
|
|
2177
|
+
ttlMinutes: Number.isFinite(ttlIn) && ttlIn > 0 ? ttlIn : undefined,
|
|
2178
|
+
minionsDir: shared.MINIONS_DIR,
|
|
2179
|
+
});
|
|
2180
|
+
} catch (_hintErr) { return ''; }
|
|
2181
|
+
})();
|
|
2182
|
+
let alertBody;
|
|
2183
|
+
if (isWorkdirRejection) {
|
|
2184
|
+
alertBody = [
|
|
2185
|
+
`# keep_processes setup REJECTED for ${agentId}`,
|
|
2186
|
+
'',
|
|
2187
|
+
`Your kept-PIDs setup at \`${evalResult.recordedCwd || '<unknown>'}\` failed validation: ${evalResult.reason}.`,
|
|
2188
|
+
'The directory is not a git worktree. PIDs were NOT protected and will be reaped.',
|
|
2189
|
+
'',
|
|
2190
|
+
wiId ? `Work item: ${wiId}` : '',
|
|
2191
|
+
`Agent: ${agentId}`,
|
|
2192
|
+
`Dispatch: ${id}`,
|
|
2193
|
+
'',
|
|
2194
|
+
'Why this matters: a keep_processes work item that runs in a non-git directory',
|
|
2195
|
+
'is almost always a partial copy of a repo (a selective `cp -r`). The Minions',
|
|
2196
|
+
'cleanup sweep cannot reason about such directories safely; later sweeps may',
|
|
2197
|
+
'rmSync subdirs treating them as separate worktrees. Re-run the work item',
|
|
2198
|
+
'inside a real `git worktree add` directory, or set',
|
|
2199
|
+
'`meta.keep_processes_skip_workdir_check: true` on the work item if you',
|
|
2200
|
+
'genuinely intend to keep PIDs alive in a non-git directory.',
|
|
2201
|
+
'',
|
|
2202
|
+
].join('\n');
|
|
2203
|
+
} else {
|
|
2204
|
+
let parsedSnippet = '';
|
|
2205
|
+
if (evalResult.parsedRaw) {
|
|
2206
|
+
try {
|
|
2207
|
+
parsedSnippet = JSON.stringify(evalResult.parsedRaw, null, 2);
|
|
2208
|
+
} catch (_jsonErr) {
|
|
2209
|
+
parsedSnippet = String(evalResult.parsedRaw);
|
|
2210
|
+
}
|
|
2211
|
+
if (parsedSnippet.length > 500) parsedSnippet = parsedSnippet.slice(0, 500) + '\n... (truncated)';
|
|
2212
|
+
}
|
|
2213
|
+
alertBody = [
|
|
2214
|
+
`# keep_processes setup REJECTED for ${agentId} (schema)`,
|
|
2215
|
+
'',
|
|
2216
|
+
`Your \`agents/${agentId}/keep-pids.json\` failed shape validation: \`${evalResult.reason}\`.`,
|
|
2217
|
+
'PIDs were NOT protected and were reaped by spawn-agent. The dispatch was marked ERROR (non-retryable).',
|
|
2218
|
+
'',
|
|
2219
|
+
wiId ? `Work item: ${wiId}` : '',
|
|
2220
|
+
`Agent: ${agentId}`,
|
|
2221
|
+
`Dispatch: ${id}`,
|
|
2222
|
+
'',
|
|
2223
|
+
parsedSnippet ? '## What you wrote\n\n```json\n' + parsedSnippet + '\n```\n' : '',
|
|
2224
|
+
'## Canonical shape',
|
|
2225
|
+
'',
|
|
2226
|
+
canonicalHint || '(see `engine/keep-process-sweep.js` `buildKeepProcessesHint` for the canonical shape.)',
|
|
2227
|
+
'',
|
|
2228
|
+
].filter(Boolean).join('\n');
|
|
2229
|
+
}
|
|
2150
2230
|
writeInboxAlert(slug, alertBody);
|
|
2151
2231
|
} catch (alertErr) {
|
|
2152
2232
|
log('warn', `keep-processes acceptance: failed to emit inbox alert for ${agentId}: ${alertErr.message}`);
|
|
2153
2233
|
}
|
|
2154
|
-
} else if (evalResult.exists && !evalResult.accepted) {
|
|
2155
|
-
// Non-workdir validation failure (oversize pids, bad TTL, etc.) —
|
|
2156
|
-
// already handled by validateKeepPidsRecord; just log for audit.
|
|
2157
|
-
log('warn', `keep-processes acceptance: ${agentId} (${id}) sidecar rejected — ${evalResult.reason} (not a workdir failure)`);
|
|
2158
2234
|
}
|
|
2159
2235
|
} catch (e) {
|
|
2160
2236
|
log('warn', `keep-processes acceptance check failed for ${agentId} (${id}): ${e.message}`);
|
|
@@ -2171,12 +2247,13 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
2171
2247
|
// We mark the work item as failed (processWorkItemFailure NOT suppressed)
|
|
2172
2248
|
// so the dispatch is not silently retried by the auto-recovery path.
|
|
2173
2249
|
const nonceFail = nonceMismatch && nonceMismatch.severity === 'hard';
|
|
2174
|
-
// W-mp6k7ywi000fa33c — keep_processes
|
|
2175
|
-
// failure: the agent's claim that "everything was set
|
|
2176
|
-
// structurally false. Force ERROR so the dispatch is
|
|
2177
|
-
// as success even when exit code is 0.
|
|
2178
|
-
|
|
2179
|
-
const
|
|
2250
|
+
// W-mp6k7ywi000fa33c / W-mp7i902u000l991f — keep_processes acceptance
|
|
2251
|
+
// failure is a hard failure: the agent's claim that "everything was set
|
|
2252
|
+
// up correctly" is structurally false. Force ERROR so the dispatch is
|
|
2253
|
+
// not silently treated as success even when exit code is 0. Both
|
|
2254
|
+
// workdir and schema rejections route here; the failure_class differs.
|
|
2255
|
+
const keepProcessesAcceptanceFail = !!keepProcessesAcceptanceFailure;
|
|
2256
|
+
const effectiveResult = (hardContractFail || nonceFail || keepProcessesAcceptanceFail)
|
|
2180
2257
|
? DISPATCH_RESULT.ERROR
|
|
2181
2258
|
: (((code === 0 && !agentReportedFailure) || autoRecovered) ? DISPATCH_RESULT.SUCCESS : DISPATCH_RESULT.ERROR);
|
|
2182
2259
|
const finalCompletionReportPath = structuredCompletion?._path || dispatchItem.meta?.completionReportPath || shared.dispatchCompletionReportPath(id);
|
|
@@ -2184,8 +2261,13 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
2184
2261
|
...(finalCompletionReportPath ? { completionReportPath: finalCompletionReportPath } : {}),
|
|
2185
2262
|
...(structuredCompletion ? { structuredCompletion } : {}),
|
|
2186
2263
|
};
|
|
2187
|
-
const
|
|
2188
|
-
?
|
|
2264
|
+
const _kpFailureClass = keepProcessesAcceptanceFail
|
|
2265
|
+
? (keepProcessesAcceptanceFailure.isWorkdirRejection
|
|
2266
|
+
? FAILURE_CLASS.INVALID_KEEP_PROCESSES_WORKDIR
|
|
2267
|
+
: FAILURE_CLASS.INVALID_KEEP_PROCESSES_SCHEMA)
|
|
2268
|
+
: null;
|
|
2269
|
+
const completeOpts = keepProcessesAcceptanceFail
|
|
2270
|
+
? { ...completionOpts, failureClass: _kpFailureClass, agentRetryable: false }
|
|
2189
2271
|
: (nonceFail
|
|
2190
2272
|
? { ...completionOpts, failureClass: nonceMismatch.failureClass, agentRetryable: false }
|
|
2191
2273
|
: (hardContractFail
|
|
@@ -2198,8 +2280,12 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
2198
2280
|
} : completionOpts)));
|
|
2199
2281
|
// Extract last 5 non-empty stderr lines as error context when exit code is non-zero
|
|
2200
2282
|
let errorReason = '';
|
|
2201
|
-
if (
|
|
2202
|
-
|
|
2283
|
+
if (keepProcessesAcceptanceFail) {
|
|
2284
|
+
if (keepProcessesAcceptanceFailure.isWorkdirRejection) {
|
|
2285
|
+
errorReason = `invalid_keep_processes_workdir: ${keepProcessesAcceptanceFailure.reason} (cwd=${keepProcessesAcceptanceFailure.cwd || '<unknown>'})`.slice(0, 300);
|
|
2286
|
+
} else {
|
|
2287
|
+
errorReason = `invalid_keep_processes_schema: ${keepProcessesAcceptanceFailure.reason}`.slice(0, 300);
|
|
2288
|
+
}
|
|
2203
2289
|
} else if (nonceFail) {
|
|
2204
2290
|
errorReason = nonceMismatch.reason || 'completion nonce mismatch';
|
|
2205
2291
|
} else if (hardContractFail) {
|
|
@@ -2273,11 +2359,12 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
2273
2359
|
|
|
2274
2360
|
completeDispatch(id, effectiveResult, errorReason, resultSummary, completeOpts);
|
|
2275
2361
|
|
|
2276
|
-
// W-mp6k7ywi000fa33c — surface the
|
|
2277
|
-
// dashboard pending-reason area shows the
|
|
2278
|
-
// a bare failure_class label. _pendingReason
|
|
2279
|
-
// by the dashboard as "additional context"
|
|
2280
|
-
|
|
2362
|
+
// W-mp6k7ywi000fa33c / W-mp7i902u000l991f — surface the keep_processes
|
|
2363
|
+
// rejection on the WI so the dashboard pending-reason area shows the
|
|
2364
|
+
// missing structure instead of a bare failure_class label. _pendingReason
|
|
2365
|
+
// on a failed item is treated by the dashboard as "additional context"
|
|
2366
|
+
// rather than a queue gate.
|
|
2367
|
+
if (keepProcessesAcceptanceFailure && dispatchItem.meta?.item?.id) {
|
|
2281
2368
|
try {
|
|
2282
2369
|
const wiPath = resolveWorkItemPath(dispatchItem.meta);
|
|
2283
2370
|
if (wiPath) {
|
|
@@ -2285,7 +2372,10 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
2285
2372
|
if (!Array.isArray(data)) return data;
|
|
2286
2373
|
const wi = data.find(i => i.id === dispatchItem.meta.item.id);
|
|
2287
2374
|
if (!wi) return data;
|
|
2288
|
-
|
|
2375
|
+
const _pfx = keepProcessesAcceptanceFailure.isWorkdirRejection
|
|
2376
|
+
? 'invalid_keep_processes_workdir'
|
|
2377
|
+
: 'invalid_keep_processes_schema';
|
|
2378
|
+
wi._pendingReason = `${_pfx}: ${keepProcessesAcceptanceFailure.reason}`.slice(0, 500);
|
|
2289
2379
|
return data;
|
|
2290
2380
|
});
|
|
2291
2381
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1956",
|
|
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"
|
package/prompts/cc-system.md
CHANGED
|
@@ -148,6 +148,8 @@ curl -s http://localhost:{{dashboard_port}}/api/status
|
|
|
148
148
|
- `POST /api/work-items`: `title` REQUIRED. `description` recommended. `project` REQUIRED when multiple projects are configured (server returns the list of known names if you guess wrong). `type` defaults to `implement`; valid values: `fix`, `implement`, `implement:large`, `explore`, `ask`, `review`, `test`, `verify`. Agent hint via `agent` (string) or `agents` (array).
|
|
149
149
|
- Exempt from the `project` requirement (these run rootless or via central paths): `ask`, `explore`, `plan`, `plan-to-prd`, `meeting`, `docs`. Every other type needs a project worktree, so the server rejects project-less creates with `400 { error, knownProjects }` when ≠1 project is configured.
|
|
150
150
|
- **`meta.keep_processes: true`** — opt-in flag that lets the agent leave specific descendant PIDs running after it exits (default: engine reaps EVERYTHING the agent spawned). **Set this whenever the user's intent is to leave a process alive after the agent finishes** — e.g. "spin up the dev server and exit", "start the watcher and leave it running", "set up my dev env", "keep the emulator open", "launch the daemon for me", "boot the constellation host and disconnect". Don't set it for normal build/test/run-once tasks (`npm test`, `npm run build`, one-shot scripts) — those should be reaped. Also accepts optional `meta.keep_processes_ttl_minutes` (default 60, hard-cap 1440 = 24h). When you set this flag, also make the WI title/description say something like "leave the dev server running" so the agent knows to write `agents/<id>/keep-pids.json` before exiting (the playbook injects the contract automatically when the flag is on). Example: `-d '{"title":"Spin up Constellation dev env and leave server running","type":"implement","project":"constellation","description":"Run bun install + bun run dev. Leave the dev server (port 5173) and Constellation host (port 3001) running after you exit so the user can iterate.","meta":{"keep_processes":true,"keep_processes_ttl_minutes":240}}'`. Inspect / kill kept PIDs anytime via `GET /api/keep-processes` and `POST /api/keep-processes/kill`.
|
|
151
|
+
- **`skipPr: true`** — opt-in flag that tells the engine NOT to enforce the PR-attachment contract for this work item, so the WI can complete `done` without the missing-PR hard-fail. **Set this when the dispatch mutates state OUTSIDE any tracked git repo and therefore cannot produce a PR** — e.g. cleaning `~/.claude/skills/`, editing runtime config under `~/.config/`, resetting the dashboard cache, mutating engine JSON state files (`engine/*.json`) the engine itself owns, or local tooling installs. **Do NOT set it for any task that touches a tracked repo's source** — even one-line diffs in a real repo should produce a PR. Type-selection rule of thumb: prefer `type: "explore"` for genuinely read-only tasks (rootless, no worktree, no PR contract); use `skipPr: true` when the task is write-side mutation but the writes don't land in a git repo. Example: `-d '{"title":"Clean up duplicate skills in ~/.claude/skills","type":"implement","description":"Audit ~/.claude/skills/ and delete the 3 obsolete entries identified in NOTE-mp7gt4iw0004b879. Pure user-machine state outside any git repo, so no PR will be produced.","skipPr":true}'`.
|
|
152
|
+
- **`oneShot: true`** — opt-in flag for one-off human-initiated dispatches that should NOT enroll the discovered PR into the engine's automatic review/fix loop. The PR is still tracked (status + comments are polled normally) but `discoverFromPrs` skips it for review/fix dispatch. **Set this when the user's intent is "do this single action against an existing PR, then stop"** — e.g. "review PR #2533 once", "rebase PR #2540 once and exit", "post a fix-summary comment on PR #2519". Don't set it for normal feature/fix work where the PR should keep cycling through review/fix until merged. Example: `-d '{"title":"One-off review of PR #2533","type":"review","project":"minions","description":"Single review pass on github:yemi33/minions#2533. Do not re-dispatch on subsequent comments.","oneShot":true}'`.
|
|
151
153
|
- `POST /api/notes`: `title`, `what` REQUIRED.
|
|
152
154
|
- `POST /api/knowledge`: `category`, `title`, `content` REQUIRED. Categories: `architecture`, `conventions`, `project-notes`, `build-reports`, `reviews`.
|
|
153
155
|
- `POST /api/plan`: `title` REQUIRED. `description`, `priority`, `project`, `agent`, `branchStrategy` optional.
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
# Dallas — Personal Memory
|
|
2
|
-
|
|
3
|
-
This is Dallas's personal memory file, curated by the consolidation pipeline
|
|
4
|
-
from inbox findings tagged `agent: dallas`. The engine injects this file into
|
|
5
|
-
Dallas's prompt context on every dispatch (alongside `notes.md`).
|
|
6
|
-
|
|
7
|
-
See `knowledge/agents/README.md` for the convention.
|
|
8
|
-
|
|
9
|
-
<!-- Append-only; consolidation engine writes new sections below. -->
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
# Lambert — Personal Memory
|
|
2
|
-
|
|
3
|
-
This is Lambert's personal memory file, curated by the consolidation pipeline
|
|
4
|
-
from inbox findings tagged `agent: lambert`. The engine injects this file into
|
|
5
|
-
Lambert's prompt context on every dispatch (alongside `notes.md`).
|
|
6
|
-
|
|
7
|
-
See `knowledge/agents/README.md` for the convention.
|
|
8
|
-
|
|
9
|
-
<!-- Append-only; consolidation engine writes new sections below. -->
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
# Ralph — Personal Memory
|
|
2
|
-
|
|
3
|
-
This is Ralph's personal memory file, curated by the consolidation pipeline
|
|
4
|
-
from inbox findings tagged `agent: ralph`. The engine injects this file into
|
|
5
|
-
Ralph's prompt context on every dispatch (alongside `notes.md`).
|
|
6
|
-
|
|
7
|
-
See `knowledge/agents/README.md` for the convention.
|
|
8
|
-
|
|
9
|
-
<!-- Append-only; consolidation engine writes new sections below. -->
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
# Rebecca — Personal Memory
|
|
2
|
-
|
|
3
|
-
This is Rebecca's personal memory file, curated by the consolidation pipeline
|
|
4
|
-
from inbox findings tagged `agent: rebecca`. The engine injects this file into
|
|
5
|
-
Rebecca's prompt context on every dispatch (alongside `notes.md`).
|
|
6
|
-
|
|
7
|
-
See `knowledge/agents/README.md` for the convention.
|
|
8
|
-
|
|
9
|
-
<!-- Append-only; consolidation engine writes new sections below. -->
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
# Ripley — Personal Memory
|
|
2
|
-
|
|
3
|
-
This is Ripley's personal memory file, curated by the consolidation pipeline
|
|
4
|
-
from inbox findings tagged `agent: ripley`. The engine injects this file into
|
|
5
|
-
Ripley's prompt context on every dispatch (alongside `notes.md`).
|
|
6
|
-
|
|
7
|
-
See `knowledge/agents/README.md` for the convention.
|
|
8
|
-
|
|
9
|
-
<!-- Append-only; consolidation engine writes new sections below. -->
|