@yemi33/minions 0.1.1950 → 0.1.1951
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/command-center.js +9 -0
- package/dashboard/js/modal-qa.js +10 -0
- package/dashboard/js/refresh.js +4 -0
- package/dashboard/js/render-dispatch.js +25 -0
- package/dashboard/js/render-other.js +109 -2
- package/dashboard/js/settings.js +1 -1
- package/dashboard/layout.html +2 -2
- package/dashboard/pages/engine.html +6 -0
- package/dashboard/slim.html +1987 -0
- package/dashboard/styles.css +8 -0
- package/dashboard.js +450 -40
- package/docs/completion-reports.md +25 -0
- package/docs/design-state-storage.md +1 -1
- package/docs/slim-ux/architecture-suggestions.md +467 -0
- package/docs/slim-ux/concepts.md +824 -0
- package/engine/ado-mcp-wrapper.js +33 -7
- package/engine/ado.js +123 -15
- package/engine/cc-worker-pool.js +41 -0
- package/engine/cleanup.js +71 -34
- package/engine/cli.js +37 -0
- package/engine/dispatch.js +32 -9
- package/engine/features.js +6 -0
- package/engine/gh-token.js +137 -0
- package/engine/github.js +166 -29
- package/engine/issues.js +29 -0
- package/engine/keep-process-sweep.js +397 -0
- package/engine/lifecycle.js +150 -33
- package/engine/playbook.js +17 -0
- package/engine/queries.js +71 -0
- package/engine/recovery.js +6 -0
- package/engine/shared.js +446 -14
- package/engine/spawn-agent.js +44 -2
- package/engine/timeout.js +34 -11
- package/engine/worktree-pool.js +410 -0
- package/engine.js +643 -119
- package/package.json +6 -3
- package/playbooks/review.md +2 -0
- package/playbooks/shared-rules.md +3 -1
- package/prompts/cc-system.md +24 -0
- package/engine/copilot-models.json +0 -5
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* engine/keep-process-sweep.js -- opt-in `meta.keep_processes` reaper.
|
|
3
|
+
*
|
|
4
|
+
* Walks agents/<id>/keep-pids.json files and, for each:
|
|
5
|
+
* - Validates the file (bounded JSON shape, see validateKeepPidsRecord).
|
|
6
|
+
* Malformed files are left alone (the user/agent can clean them up by
|
|
7
|
+
* hand) but they are never honored as anchors.
|
|
8
|
+
* - When expires_at < now: kills each declared PID via shared.killByPidImmediate
|
|
9
|
+
* and unlinks the file. Audit-logs {agentId, pid, reason: 'ttl_expired'}.
|
|
10
|
+
* - When all declared PIDs are dead but expires_at has not fired: silently
|
|
11
|
+
* unlinks the file (nothing to anchor).
|
|
12
|
+
*
|
|
13
|
+
* Run once at engine boot AND from the tick cycle every
|
|
14
|
+
* ENGINE_DEFAULTS.keepProcesses.sweepEvery ticks. Idempotent -- a no-op when
|
|
15
|
+
* no agents declared keep_processes.
|
|
16
|
+
*
|
|
17
|
+
* See W-mp68q6ke0010de68 for the file shape and acceptance tests.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const shared = require('./shared');
|
|
23
|
+
const queries = require('./queries');
|
|
24
|
+
|
|
25
|
+
const { log, ENGINE_DEFAULTS } = shared;
|
|
26
|
+
|
|
27
|
+
const KEEP_PIDS_FILENAME = 'keep-pids.json';
|
|
28
|
+
const INVALID_WORKDIR_REASON_PREFIX = 'invalid-workdir: ';
|
|
29
|
+
|
|
30
|
+
function _agentsDir() {
|
|
31
|
+
return queries.AGENTS_DIR || path.join(shared.MINIONS_DIR, 'agents');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function _resolveRequireGitWorkdir(opts) {
|
|
35
|
+
const limits = ENGINE_DEFAULTS.keepProcesses || {};
|
|
36
|
+
if (opts && Object.prototype.hasOwnProperty.call(opts, 'requireGitWorkdir')) {
|
|
37
|
+
return !!opts.requireGitWorkdir;
|
|
38
|
+
}
|
|
39
|
+
// Treat undefined default as ON to fail closed.
|
|
40
|
+
if (limits.requireGitWorkdir === false) return false;
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function validateKeepPidsRecord(parsed, opts) {
|
|
45
|
+
opts = opts || {};
|
|
46
|
+
const limits = ENGINE_DEFAULTS.keepProcesses || {};
|
|
47
|
+
const maxPids = Math.max(1, Number(limits.maxPerAgent) || 5);
|
|
48
|
+
const maxTtlMs = Math.max(60000, (Number(limits.maxTtlMinutes) || 1440) * 60 * 1000);
|
|
49
|
+
const now = Number.isFinite(opts.now) ? opts.now : Date.now();
|
|
50
|
+
const requireGitWorkdir = _resolveRequireGitWorkdir(opts);
|
|
51
|
+
|
|
52
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
53
|
+
return { ok: false, reason: 'not-an-object' };
|
|
54
|
+
}
|
|
55
|
+
if (!Array.isArray(parsed.pids)) return { ok: false, reason: 'pids-missing' };
|
|
56
|
+
if (parsed.pids.length === 0) return { ok: false, reason: 'pids-empty' };
|
|
57
|
+
if (parsed.pids.length > maxPids) return { ok: false, reason: 'pids-too-many (>' + maxPids + ')' };
|
|
58
|
+
const pids = [];
|
|
59
|
+
for (const raw of parsed.pids) {
|
|
60
|
+
const n = Number(raw);
|
|
61
|
+
if (!Number.isInteger(n) || n <= 0) return { ok: false, reason: 'pid-invalid (' + raw + ')' };
|
|
62
|
+
pids.push(n);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (typeof parsed.expires_at !== 'string') return { ok: false, reason: 'expires_at-missing' };
|
|
66
|
+
const expiresAtMs = Date.parse(parsed.expires_at);
|
|
67
|
+
if (!Number.isFinite(expiresAtMs)) return { ok: false, reason: 'expires_at-unparsable' };
|
|
68
|
+
if (expiresAtMs - now > maxTtlMs) {
|
|
69
|
+
return { ok: false, reason: 'ttl-too-long (>' + Math.round(maxTtlMs / 60000) + 'min)' };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (parsed.purpose != null && typeof parsed.purpose !== 'string') {
|
|
73
|
+
return { ok: false, reason: 'purpose-not-string' };
|
|
74
|
+
}
|
|
75
|
+
if (typeof parsed.purpose === 'string' && parsed.purpose.length > 500) {
|
|
76
|
+
return { ok: false, reason: 'purpose-too-long' };
|
|
77
|
+
}
|
|
78
|
+
if (parsed.cwd != null && typeof parsed.cwd !== 'string') {
|
|
79
|
+
return { ok: false, reason: 'cwd-not-string' };
|
|
80
|
+
}
|
|
81
|
+
if (typeof parsed.cwd === 'string' && parsed.cwd.length > 500) {
|
|
82
|
+
return { ok: false, reason: 'cwd-too-long' };
|
|
83
|
+
}
|
|
84
|
+
// W-mp6k7ywi000fa33c — when the engine requires a real git workdir
|
|
85
|
+
// (default true; per-WI override via `meta.keep_processes_skip_workdir_check`),
|
|
86
|
+
// verify the recorded `cwd` looks like a real worktree. Empty/missing
|
|
87
|
+
// `cwd` is not validated for back-compat with files that pre-date this
|
|
88
|
+
// check; agents authored after this change SHOULD include `cwd`. The
|
|
89
|
+
// engine acceptance hook in engine.js keys off the `invalid-workdir:`
|
|
90
|
+
// prefix to mark the WI failed and emit an inbox alert.
|
|
91
|
+
if (requireGitWorkdir && typeof parsed.cwd === 'string' && parsed.cwd.length > 0) {
|
|
92
|
+
const wt = shared.isValidGitWorktree(parsed.cwd);
|
|
93
|
+
if (!wt.ok) {
|
|
94
|
+
return { ok: false, reason: INVALID_WORKDIR_REASON_PREFIX + wt.reason };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (parsed.ports != null) {
|
|
98
|
+
if (!Array.isArray(parsed.ports)) return { ok: false, reason: 'ports-not-array' };
|
|
99
|
+
if (parsed.ports.length > 20) return { ok: false, reason: 'ports-too-many (>20)' };
|
|
100
|
+
for (const p of parsed.ports) {
|
|
101
|
+
const n = Number(p);
|
|
102
|
+
if (!Number.isInteger(n) || n <= 0 || n > 65535) {
|
|
103
|
+
return { ok: false, reason: 'port-invalid (' + p + ')' };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (parsed.written_by != null && typeof parsed.written_by !== 'string') {
|
|
108
|
+
return { ok: false, reason: 'written_by-not-string' };
|
|
109
|
+
}
|
|
110
|
+
if (parsed.wi_id != null && typeof parsed.wi_id !== 'string') {
|
|
111
|
+
return { ok: false, reason: 'wi_id-not-string' };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
ok: true,
|
|
116
|
+
value: {
|
|
117
|
+
pids: pids,
|
|
118
|
+
purpose: typeof parsed.purpose === 'string' ? parsed.purpose : '',
|
|
119
|
+
cwd: typeof parsed.cwd === 'string' ? parsed.cwd : '',
|
|
120
|
+
ports: Array.isArray(parsed.ports) ? parsed.ports.map(Number) : [],
|
|
121
|
+
expires_at: parsed.expires_at,
|
|
122
|
+
expiresAtMs: expiresAtMs,
|
|
123
|
+
written_by: typeof parsed.written_by === 'string' ? parsed.written_by : '',
|
|
124
|
+
wi_id: typeof parsed.wi_id === 'string' ? parsed.wi_id : '',
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function readKeepPidsFile(agentId, opts) {
|
|
130
|
+
opts = opts || {};
|
|
131
|
+
const filePath = path.join(_agentsDir(), agentId, KEEP_PIDS_FILENAME);
|
|
132
|
+
let raw;
|
|
133
|
+
try { raw = fs.readFileSync(filePath, 'utf8'); }
|
|
134
|
+
catch (_e) { return null; }
|
|
135
|
+
let parsed = null;
|
|
136
|
+
try { parsed = JSON.parse(raw); }
|
|
137
|
+
catch (e) {
|
|
138
|
+
return { agentId: agentId, filePath: filePath, valid: false, reason: 'json-parse: ' + e.message, parsed: null };
|
|
139
|
+
}
|
|
140
|
+
const v = validateKeepPidsRecord(parsed, opts);
|
|
141
|
+
if (!v.ok) return { agentId: agentId, filePath: filePath, valid: false, reason: v.reason, parsed: parsed };
|
|
142
|
+
return { agentId: agentId, filePath: filePath, valid: true, value: v.value, parsed: parsed };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function listAllKeepPidsFiles(opts) {
|
|
146
|
+
opts = opts || {};
|
|
147
|
+
const dir = _agentsDir();
|
|
148
|
+
let entries = [];
|
|
149
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
|
|
150
|
+
catch (_e) { return []; }
|
|
151
|
+
const out = [];
|
|
152
|
+
for (const ent of entries) {
|
|
153
|
+
if (!ent.isDirectory()) continue;
|
|
154
|
+
if (ent.name.startsWith('_') || ent.name.startsWith('.')) continue;
|
|
155
|
+
// W-mp6k7ywi000fa33c — forward opts (notably `requireGitWorkdir`) so
|
|
156
|
+
// sweep callers can disable workdir validation for cleanup of files
|
|
157
|
+
// pointing at directories that no longer exist.
|
|
158
|
+
const rec = readKeepPidsFile(ent.name, opts);
|
|
159
|
+
if (rec) out.push(rec);
|
|
160
|
+
}
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function alivePids(pids, opts) {
|
|
165
|
+
opts = opts || {};
|
|
166
|
+
const isAlive = typeof opts.isAlive === 'function'
|
|
167
|
+
? opts.isAlive
|
|
168
|
+
: function (pid) { try { process.kill(pid, 0); return true; } catch (_e) { return false; } };
|
|
169
|
+
return pids.filter(isAlive);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function _audit(eventType, payload) {
|
|
173
|
+
try {
|
|
174
|
+
log('info', 'keep-processes ' + eventType + ': ' + JSON.stringify(payload));
|
|
175
|
+
} catch (_e) { /* logging best-effort */ }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function sweepKeepProcesses(opts) {
|
|
179
|
+
opts = opts || {};
|
|
180
|
+
const now = Number.isFinite(opts.now) ? opts.now : Date.now();
|
|
181
|
+
const killOne = typeof opts.killOne === 'function'
|
|
182
|
+
? opts.killOne
|
|
183
|
+
: function (pid) { return shared.killByPidImmediate(pid); };
|
|
184
|
+
const stats = { scanned: 0, expiredFiles: 0, killedPids: 0, deadFiles: 0, malformed: 0 };
|
|
185
|
+
// W-mp6k7ywi000fa33c — sweep ignores workdir validation so TTL/dead-PID
|
|
186
|
+
// cleanup still drains files whose cwd no longer exists (e.g., the agent
|
|
187
|
+
// worktree was already torn down). Anchor reads (computeReapPlan,
|
|
188
|
+
// getActiveAnchorPids*) keep the default-on validation so invalid records
|
|
189
|
+
// are not honored as anchors.
|
|
190
|
+
const records = listAllKeepPidsFiles({ now: now, requireGitWorkdir: false });
|
|
191
|
+
for (const rec of records) {
|
|
192
|
+
stats.scanned++;
|
|
193
|
+
if (!rec.valid) {
|
|
194
|
+
stats.malformed++;
|
|
195
|
+
log('warn', 'keep-processes: malformed ' + rec.filePath + ' -- ' + rec.reason + '; leaving in place');
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
const value = rec.value;
|
|
199
|
+
const filePath = rec.filePath;
|
|
200
|
+
const agentId = rec.agentId;
|
|
201
|
+
if (value.expiresAtMs < now) {
|
|
202
|
+
let killed = 0;
|
|
203
|
+
for (const pid of value.pids) {
|
|
204
|
+
const ok = killOne(pid);
|
|
205
|
+
if (ok) killed++;
|
|
206
|
+
_audit('ttl-expired-kill', { agentId: agentId, pid: pid, reason: 'ttl_expired', wi_id: value.wi_id });
|
|
207
|
+
}
|
|
208
|
+
stats.killedPids += killed;
|
|
209
|
+
stats.expiredFiles++;
|
|
210
|
+
try { fs.unlinkSync(filePath); } catch (_e) { /* gone already */ }
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
const stillAlive = alivePids(value.pids, opts);
|
|
214
|
+
if (stillAlive.length === 0) {
|
|
215
|
+
stats.deadFiles++;
|
|
216
|
+
try { fs.unlinkSync(filePath); } catch (_e) { /* gone already */ }
|
|
217
|
+
_audit('dead-pids-cleanup', { agentId: agentId, pids: value.pids, wi_id: value.wi_id });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return stats;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function getActiveAnchorPids(opts) {
|
|
224
|
+
opts = opts || {};
|
|
225
|
+
const now = Number.isFinite(opts.now) ? opts.now : Date.now();
|
|
226
|
+
const out = new Set();
|
|
227
|
+
for (const rec of listAllKeepPidsFiles({ now: now })) {
|
|
228
|
+
if (!rec.valid) continue;
|
|
229
|
+
if (rec.value.expiresAtMs < now) continue;
|
|
230
|
+
for (const pid of rec.value.pids) out.add(pid);
|
|
231
|
+
}
|
|
232
|
+
return out;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function getActiveAnchorPidsForAgent(agentId, opts) {
|
|
236
|
+
opts = opts || {};
|
|
237
|
+
const now = Number.isFinite(opts.now) ? opts.now : Date.now();
|
|
238
|
+
const out = new Set();
|
|
239
|
+
if (!agentId) return { pids: out, record: null };
|
|
240
|
+
// W-mp6k7ywi000fa33c — forward `requireGitWorkdir` (and any future opts)
|
|
241
|
+
// so the per-WI override path can disable workdir validation. Earlier
|
|
242
|
+
// implementation only forwarded `now`, which silently ignored opts.
|
|
243
|
+
const readOpts = { now: now };
|
|
244
|
+
if (Object.prototype.hasOwnProperty.call(opts, 'requireGitWorkdir')) {
|
|
245
|
+
readOpts.requireGitWorkdir = opts.requireGitWorkdir;
|
|
246
|
+
}
|
|
247
|
+
const rec = readKeepPidsFile(String(agentId), readOpts);
|
|
248
|
+
if (!rec) return { pids: out, record: null };
|
|
249
|
+
if (!rec.valid) {
|
|
250
|
+
log('warn', 'keep-processes: ignoring ' + rec.filePath + ' for ' + agentId + ' -- ' + rec.reason);
|
|
251
|
+
return { pids: out, record: null, reason: rec.reason };
|
|
252
|
+
}
|
|
253
|
+
if (rec.value.expiresAtMs < now) return { pids: out, record: rec.value, reason: 'expired' };
|
|
254
|
+
for (const pid of rec.value.pids) out.add(pid);
|
|
255
|
+
return { pids: out, record: rec.value };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function computeReapPlan(descendants, agentId, opts) {
|
|
259
|
+
opts = opts || {};
|
|
260
|
+
const list = (Array.isArray(descendants) ? descendants : []).map(Number)
|
|
261
|
+
.filter(function (n) { return Number.isInteger(n) && n > 0; });
|
|
262
|
+
const loader = typeof opts.loadAnchors === 'function'
|
|
263
|
+
? opts.loadAnchors
|
|
264
|
+
: function (id, o) { return getActiveAnchorPidsForAgent(id, o); };
|
|
265
|
+
const result = loader(agentId, opts);
|
|
266
|
+
const keptSet = result && result.pids;
|
|
267
|
+
const record = result && result.record;
|
|
268
|
+
const reason = result && result.reason;
|
|
269
|
+
if (!keptSet || keptSet.size === 0) {
|
|
270
|
+
return { toKill: list, kept: [], record: record || null, reason: reason || null };
|
|
271
|
+
}
|
|
272
|
+
const kept = [];
|
|
273
|
+
const toKill = [];
|
|
274
|
+
for (const pid of list) {
|
|
275
|
+
if (keptSet.has(pid)) kept.push(pid);
|
|
276
|
+
else toKill.push(pid);
|
|
277
|
+
}
|
|
278
|
+
return { toKill: toKill, kept: kept, record: record || null, reason: null };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// W-mp6k7ywi000fa33c — engine-side acceptance helper. Returns a structured
|
|
282
|
+
// summary the engine.js close handler uses to decide whether to:
|
|
283
|
+
// (a) accept the file silently (no keep-pids.json present, or valid),
|
|
284
|
+
// (b) reject it as workdir-invalid → mark WI failed + emit inbox alert
|
|
285
|
+
// + delete the file so it does not anchor anything stale,
|
|
286
|
+
// (c) note a non-workdir validation failure (logged but not WI-failing,
|
|
287
|
+
// since spawn-agent already reaped the PIDs).
|
|
288
|
+
//
|
|
289
|
+
// Pure: no side effects. Caller is responsible for the WI/inbox writes.
|
|
290
|
+
//
|
|
291
|
+
// opts.requireGitWorkdir defaults to ENGINE_DEFAULTS.keepProcesses.requireGitWorkdir.
|
|
292
|
+
// Pass `false` to bypass workdir validation (per-WI override path).
|
|
293
|
+
function evaluateKeepPidsAcceptance(agentId, opts) {
|
|
294
|
+
opts = opts || {};
|
|
295
|
+
const filePath = path.join(_agentsDir(), String(agentId || ''), KEEP_PIDS_FILENAME);
|
|
296
|
+
if (!fs.existsSync(filePath)) {
|
|
297
|
+
return { exists: false, accepted: false, isWorkdirRejection: false, reason: null, record: null, recordedCwd: null, filePath: filePath };
|
|
298
|
+
}
|
|
299
|
+
const rec = readKeepPidsFile(agentId, opts);
|
|
300
|
+
if (!rec) {
|
|
301
|
+
// Race: file existed in `existsSync` check above but was unlinked
|
|
302
|
+
// before readFileSync ran. Treat as no-file.
|
|
303
|
+
return { exists: false, accepted: false, isWorkdirRejection: false, reason: null, record: null, recordedCwd: null, filePath: filePath };
|
|
304
|
+
}
|
|
305
|
+
if (rec.valid) {
|
|
306
|
+
return {
|
|
307
|
+
exists: true,
|
|
308
|
+
accepted: true,
|
|
309
|
+
isWorkdirRejection: false,
|
|
310
|
+
reason: null,
|
|
311
|
+
record: rec.value,
|
|
312
|
+
recordedCwd: rec.value.cwd || null,
|
|
313
|
+
filePath: rec.filePath,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
const reason = rec.reason || 'unknown';
|
|
317
|
+
const isWorkdirRejection = typeof reason === 'string' && reason.indexOf(INVALID_WORKDIR_REASON_PREFIX) === 0;
|
|
318
|
+
let recordedCwd = null;
|
|
319
|
+
if (rec.parsed && typeof rec.parsed === 'object' && typeof rec.parsed.cwd === 'string') {
|
|
320
|
+
recordedCwd = rec.parsed.cwd;
|
|
321
|
+
}
|
|
322
|
+
return {
|
|
323
|
+
exists: true,
|
|
324
|
+
accepted: false,
|
|
325
|
+
isWorkdirRejection: isWorkdirRejection,
|
|
326
|
+
reason: reason,
|
|
327
|
+
record: null,
|
|
328
|
+
recordedCwd: recordedCwd,
|
|
329
|
+
filePath: rec.filePath,
|
|
330
|
+
parsedRaw: rec.parsed || null,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function buildKeepProcessesHint(opts) {
|
|
335
|
+
opts = opts || {};
|
|
336
|
+
const limits = ENGINE_DEFAULTS.keepProcesses || {};
|
|
337
|
+
const maxTtl = Math.max(1, Number(limits.maxTtlMinutes) || 1440);
|
|
338
|
+
const defaultTtl = Math.max(1, Number(limits.defaultTtlMinutes) || 60);
|
|
339
|
+
const maxPids = Math.max(1, Number(limits.maxPerAgent) || 5);
|
|
340
|
+
const ttlIn = Number(opts.ttlMinutes);
|
|
341
|
+
const ttl = Number.isFinite(ttlIn) && ttlIn > 0
|
|
342
|
+
? Math.min(maxTtl, Math.floor(ttlIn))
|
|
343
|
+
: defaultTtl;
|
|
344
|
+
const agentId = opts.agentId || '<your-agent-id>';
|
|
345
|
+
const wiId = opts.workItemId || '<this-work-item-id>';
|
|
346
|
+
const minionsDir = opts.minionsDir || '<minions-dir>';
|
|
347
|
+
const lines = [
|
|
348
|
+
'',
|
|
349
|
+
'',
|
|
350
|
+
'---',
|
|
351
|
+
'',
|
|
352
|
+
'## Long-running processes (keep_processes flag)',
|
|
353
|
+
'',
|
|
354
|
+
'This work item permits you to leave specific descendant processes running after you exit (TTL: ' + ttl + ' minutes, hard-cap ' + maxTtl + ' minutes).',
|
|
355
|
+
'',
|
|
356
|
+
'If -- and ONLY if -- you start a long-running process (dev server, watcher, daemon, emulator, etc.) that the user explicitly asked to remain alive, then BEFORE you write your completion report you MUST write its PID(s) to:',
|
|
357
|
+
'',
|
|
358
|
+
' `' + minionsDir + '/agents/' + agentId + '/keep-pids.json`',
|
|
359
|
+
'',
|
|
360
|
+
'with this exact JSON shape:',
|
|
361
|
+
'',
|
|
362
|
+
'```json',
|
|
363
|
+
'{',
|
|
364
|
+
' "pids": [12345, 12346],',
|
|
365
|
+
' "purpose": "bun run dev (Constellation server + dashboard)",',
|
|
366
|
+
' "cwd": "D:/repos/constellation",',
|
|
367
|
+
' "ports": [3001, 5173],',
|
|
368
|
+
' "expires_at": "<ISO-8601 timestamp <= ' + ttl + ' minutes from now>",',
|
|
369
|
+
' "written_by": "' + agentId + '",',
|
|
370
|
+
' "wi_id": "' + wiId + '"',
|
|
371
|
+
'}',
|
|
372
|
+
'```',
|
|
373
|
+
'',
|
|
374
|
+
'Caps the engine enforces: max ' + maxPids + ' PIDs, TTL <= ' + maxTtl + ' minutes, `purpose`/`cwd` <= 500 chars, <= 20 ports. Files outside the cap are dropped (engine reaps as normal) and a warning is logged.',
|
|
375
|
+
'',
|
|
376
|
+
'If you do NOT write the file, the engine will kill ALL of your descendant processes when you exit (today\'s default). Do not write the file unless you are intentionally leaving a process behind for the human or follow-up work.',
|
|
377
|
+
'',
|
|
378
|
+
'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
|
+
'',
|
|
380
|
+
];
|
|
381
|
+
return lines.join('\n');
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
module.exports = {
|
|
385
|
+
KEEP_PIDS_FILENAME: KEEP_PIDS_FILENAME,
|
|
386
|
+
INVALID_WORKDIR_REASON_PREFIX: INVALID_WORKDIR_REASON_PREFIX,
|
|
387
|
+
validateKeepPidsRecord: validateKeepPidsRecord,
|
|
388
|
+
readKeepPidsFile: readKeepPidsFile,
|
|
389
|
+
listAllKeepPidsFiles: listAllKeepPidsFiles,
|
|
390
|
+
alivePids: alivePids,
|
|
391
|
+
sweepKeepProcesses: sweepKeepProcesses,
|
|
392
|
+
getActiveAnchorPids: getActiveAnchorPids,
|
|
393
|
+
getActiveAnchorPidsForAgent: getActiveAnchorPidsForAgent,
|
|
394
|
+
computeReapPlan: computeReapPlan,
|
|
395
|
+
buildKeepProcessesHint: buildKeepProcessesHint,
|
|
396
|
+
evaluateKeepPidsAcceptance: evaluateKeepPidsAcceptance,
|
|
397
|
+
};
|