atris 3.61.0 → 3.63.0
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/README.md +37 -5
- package/atris/features/README.md +8 -0
- package/atris/skills/README.md +1 -0
- package/atris/skills/commerce/SKILL.md +96 -0
- package/atris/skills/engines/SKILL.md +9 -9
- package/bin/atris.js +12 -0
- package/commands/autoland.js +36 -5
- package/commands/autopilot.js +105 -6
- package/commands/buy.js +242 -0
- package/commands/engine.js +96 -21
- package/commands/host.js +56 -4
- package/commands/land.js +129 -8
- package/commands/member.js +68 -14
- package/commands/mission.js +116 -4
- package/commands/one-lap.js +14 -10
- package/commands/stream.js +56 -22
- package/commands/task.js +7 -0
- package/commands/team.js +82 -10
- package/commands/transactions.js +115 -0
- package/commands/worktree.js +39 -0
- package/commands/youtube.js +58 -26
- package/lib/commerce.js +75 -0
- package/lib/engine-ask.js +112 -9
- package/lib/engine-registry.js +300 -39
- package/lib/fleet.js +444 -26
- package/lib/functional-owner.js +15 -6
- package/lib/host.js +296 -170
- package/lib/known-commands.js +1 -1
- package/lib/member-engine.js +25 -1
- package/lib/member-park.js +138 -0
- package/lib/member-scaffold.js +39 -0
- package/lib/member-type.js +27 -0
- package/lib/roster-markdown.js +1 -1
- package/lib/roster-prep.js +289 -0
- package/lib/roster-runs.js +389 -0
- package/lib/roster-suggest.js +145 -0
- package/lib/router-brain.js +111 -3
- package/lib/runner-command.js +37 -2
- package/lib/self-drive.js +26 -2
- package/lib/state-detection.js +3 -3
- package/lib/task-db.js +38 -3
- package/lib/team-lineup.js +7 -2
- package/lib/todo.js +1 -1
- package/lib/wish-audit.js +1 -1
- package/package.json +1 -1
- package/scripts/det/cpu-hotspots.js +216 -0
- package/scripts/det/ytnotes +169 -23
- package/scripts/det/ytquote-repair.js +18 -4
- package/scripts/det/ytrail-bench.js +214 -0
- package/scripts/det/ytrail-eval.js +128 -56
- package/templates/members/host/MEMBER.md +5 -3
- package/templates/members/host/MISSION.md +1 -1
- package/templates/members/host/skills/host/SKILL.md +64 -16
package/commands/land.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
-
const
|
|
3
|
+
const gitSpawn = require('../lib/git-spawn');
|
|
4
4
|
const { compactErrorPayload, printCliJson } = require('../lib/cli-json');
|
|
5
5
|
const { listWorktrees, statusCounts } = require('./worktree');
|
|
6
6
|
|
|
@@ -9,6 +9,14 @@ const DEFAULT_STALE_HOURS = 48;
|
|
|
9
9
|
const WORKTREE_REAP_GRACE_MS = 60 * 60 * 1000;
|
|
10
10
|
const PROTECTED_BRANCHES = new Set(['main', 'master']);
|
|
11
11
|
|
|
12
|
+
// Through the module object so tests can count git spawns with a spy.
|
|
13
|
+
function runGit(args, opts) {
|
|
14
|
+
return gitSpawn.runGit(args, opts);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const CHERRY_CACHE_FILE = path.join('.atris', 'state', 'cherry-cache.json');
|
|
18
|
+
const CHERRY_CACHE_MAX = 2000;
|
|
19
|
+
|
|
12
20
|
// Pluralize a count + noun ("1 change", "2 changes").
|
|
13
21
|
function countLabel(n, word) {
|
|
14
22
|
return `${n} ${word}${n === 1 ? '' : 's'}`;
|
|
@@ -157,10 +165,11 @@ function checkoutKeepLine(name, bound, now) {
|
|
|
157
165
|
function listBranches(root, base = '') {
|
|
158
166
|
// With a base, ask git for ahead counts in the same single spawn
|
|
159
167
|
// (%(ahead-behind:) needs git >= 2.41; on failure we retry without it and
|
|
160
|
-
// collectBoard falls back to per-branch aheadCount).
|
|
168
|
+
// collectBoard falls back to per-branch aheadCount). The branch sha comes
|
|
169
|
+
// along in the same spawn so the cherry cache needs no per-branch lookup.
|
|
161
170
|
const format = base
|
|
162
|
-
? `%(refname:short)%09%(committerdate:unix)%09%(ahead-behind:${base})`
|
|
163
|
-
: '%(refname:short)%09%(committerdate:unix)';
|
|
171
|
+
? `%(refname:short)%09%(committerdate:unix)%09%(objectname)%09%(ahead-behind:${base})`
|
|
172
|
+
: '%(refname:short)%09%(committerdate:unix)%09%(objectname)';
|
|
164
173
|
const result = runGit(['for-each-ref', 'refs/heads', `--format=${format}`], { cwd: root, check: false });
|
|
165
174
|
if (result.status !== 0) {
|
|
166
175
|
if (base) return listBranches(root);
|
|
@@ -170,8 +179,9 @@ function listBranches(root, base = '') {
|
|
|
170
179
|
.split(/\r?\n/)
|
|
171
180
|
.filter(Boolean)
|
|
172
181
|
.map((line) => {
|
|
173
|
-
const [name, date, aheadBehind] = line.split('\t');
|
|
182
|
+
const [name, date, sha, aheadBehind] = line.split('\t');
|
|
174
183
|
const entry = { name, lastCommitUnix: Number(date) };
|
|
184
|
+
if (sha) entry.sha = sha;
|
|
175
185
|
if (aheadBehind !== undefined) {
|
|
176
186
|
const ahead = Number(aheadBehind.split(' ')[0]);
|
|
177
187
|
if (Number.isFinite(ahead)) entry.ahead = ahead;
|
|
@@ -206,6 +216,82 @@ function cherryStats(root, base, ref) {
|
|
|
206
216
|
return { landedElsewhere, unique };
|
|
207
217
|
}
|
|
208
218
|
|
|
219
|
+
// `git cherry <base> <ref>` depends only on the two commit shas, so its
|
|
220
|
+
// counts are cached per (base sha, ref sha) under .atris/state. The cache is
|
|
221
|
+
// only written where .atris already exists; a missing or corrupt file means
|
|
222
|
+
// recompute. Entries for refs not seen this run are dropped on save.
|
|
223
|
+
function cherryCachePath(root) {
|
|
224
|
+
return path.join(root, CHERRY_CACHE_FILE);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function loadCherryCache(root) {
|
|
228
|
+
const cache = { file: cherryCachePath(root), entries: {}, used: {}, dirty: false, writable: false };
|
|
229
|
+
try {
|
|
230
|
+
cache.writable = fs.statSync(path.join(root, '.atris')).isDirectory();
|
|
231
|
+
} catch {
|
|
232
|
+
cache.writable = false;
|
|
233
|
+
}
|
|
234
|
+
if (!cache.writable) return cache;
|
|
235
|
+
try {
|
|
236
|
+
const parsed = JSON.parse(fs.readFileSync(cache.file, 'utf8'));
|
|
237
|
+
if (parsed && parsed.version === 1 && parsed.entries && typeof parsed.entries === 'object') {
|
|
238
|
+
cache.entries = parsed.entries;
|
|
239
|
+
}
|
|
240
|
+
} catch {
|
|
241
|
+
cache.entries = {};
|
|
242
|
+
}
|
|
243
|
+
return cache;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function validCherryEntry(entry) {
|
|
247
|
+
return entry && Number.isInteger(entry.landedElsewhere) && Number.isInteger(entry.unique)
|
|
248
|
+
&& entry.landedElsewhere >= 0 && entry.unique >= 0;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function cachedCherryStats(cache, cwd, base, baseSha, ref, refSha) {
|
|
252
|
+
if (!cache || !baseSha || !refSha) return cherryStats(cwd, base, ref);
|
|
253
|
+
const key = `${baseSha}:${refSha}`;
|
|
254
|
+
const hit = cache.entries[key];
|
|
255
|
+
if (validCherryEntry(hit)) {
|
|
256
|
+
cache.used[key] = hit;
|
|
257
|
+
return { landedElsewhere: hit.landedElsewhere, unique: hit.unique };
|
|
258
|
+
}
|
|
259
|
+
// Cherry the exact commits the key names, so a ref that moves mid-run
|
|
260
|
+
// can never file one pair's result under another pair's key.
|
|
261
|
+
const result = runGit(['cherry', baseSha, refSha], { cwd, check: false });
|
|
262
|
+
if (result.status !== 0) return { landedElsewhere: 0, unique: 0 };
|
|
263
|
+
const stats = { landedElsewhere: 0, unique: 0 };
|
|
264
|
+
for (const line of result.stdout.split(/\r?\n/).filter(Boolean)) {
|
|
265
|
+
if (line.startsWith('- ')) stats.landedElsewhere += 1;
|
|
266
|
+
else if (line.startsWith('+ ')) stats.unique += 1;
|
|
267
|
+
}
|
|
268
|
+
cache.used[key] = { ...stats };
|
|
269
|
+
cache.dirty = true;
|
|
270
|
+
return stats;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function saveCherryCache(cache) {
|
|
274
|
+
if (!cache || !cache.writable) return;
|
|
275
|
+
const usedKeys = Object.keys(cache.used);
|
|
276
|
+
// Rewrite when something new was computed or something stale can go.
|
|
277
|
+
if (!cache.dirty && usedKeys.length === Object.keys(cache.entries).length) return;
|
|
278
|
+
const entries = {};
|
|
279
|
+
for (const key of usedKeys.slice(-CHERRY_CACHE_MAX)) entries[key] = cache.used[key];
|
|
280
|
+
try {
|
|
281
|
+
fs.mkdirSync(path.dirname(cache.file), { recursive: true });
|
|
282
|
+
const tmp = `${cache.file}.${process.pid}.tmp`;
|
|
283
|
+
fs.writeFileSync(tmp, `${JSON.stringify({ version: 1, entries })}\n`);
|
|
284
|
+
fs.renameSync(tmp, cache.file);
|
|
285
|
+
} catch {
|
|
286
|
+
// The cache is only a speedup; a failed write costs a recompute next time.
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function resolveSha(root, ref) {
|
|
291
|
+
const result = runGit(['rev-parse', '--verify', '--quiet', `${ref}^{commit}`], { cwd: root, check: false });
|
|
292
|
+
return result.status === 0 ? result.stdout.trim() : '';
|
|
293
|
+
}
|
|
294
|
+
|
|
209
295
|
// Board: every branch and worktree with unlanded state, classified.
|
|
210
296
|
// landed, no commits ahead of base; the branch pointer is residue
|
|
211
297
|
// active, has unlanded commits, younger than TTL
|
|
@@ -220,6 +306,8 @@ function collectBoard(root, { ttlDays = DEFAULT_TTL_DAYS, staleHours = DEFAULT_S
|
|
|
220
306
|
|
|
221
307
|
const branches = [];
|
|
222
308
|
const lastCommitMsByBranch = new Map();
|
|
309
|
+
const cherryCache = loadCherryCache(root);
|
|
310
|
+
const baseSha = resolveSha(root, base);
|
|
223
311
|
for (const branch of listBranches(root, base)) {
|
|
224
312
|
if (PROTECTED_BRANCHES.has(branch.name)) continue;
|
|
225
313
|
const lastCommitMs = Number(branch.lastCommitUnix) * 1000;
|
|
@@ -227,7 +315,9 @@ function collectBoard(root, { ttlDays = DEFAULT_TTL_DAYS, staleHours = DEFAULT_S
|
|
|
227
315
|
const ahead = branch.ahead !== undefined ? branch.ahead : aheadCount(root, base, branch.name);
|
|
228
316
|
const age = ageDays(branch.lastCommitUnix, now);
|
|
229
317
|
const hours = ageHours(branch.lastCommitUnix, now);
|
|
230
|
-
const cherry = ahead > 0
|
|
318
|
+
const cherry = ahead > 0
|
|
319
|
+
? cachedCherryStats(cherryCache, root, base, baseSha, branch.name, branch.sha)
|
|
320
|
+
: { landedElsewhere: 0, unique: 0 };
|
|
231
321
|
let state = 'active';
|
|
232
322
|
if (ahead === 0 || (cherry.landedElsewhere > 0 && cherry.unique === 0)) state = 'landed';
|
|
233
323
|
else if (age > ttlDays) state = 'due';
|
|
@@ -271,7 +361,7 @@ function collectBoard(root, { ttlDays = DEFAULT_TTL_DAYS, staleHours = DEFAULT_S
|
|
|
271
361
|
const cnt = runGit(['rev-list', '--count', `${base}..HEAD`], { cwd: wt.path, check: false });
|
|
272
362
|
ahead = cnt.status === 0 ? Number(cnt.stdout.trim()) || 0 : 0;
|
|
273
363
|
if (ahead > 0) {
|
|
274
|
-
unlandedCommits =
|
|
364
|
+
unlandedCommits = cachedCherryStats(cherryCache, wt.path, base, baseSha, 'HEAD', wt.head).unique;
|
|
275
365
|
const ts = runGit(['log', '-1', '--format=%ct', 'HEAD'], { cwd: wt.path, check: false });
|
|
276
366
|
if (ts.status === 0) ageDaysVal = ageDays(Number(ts.stdout.trim()), now);
|
|
277
367
|
if (!light) subjects = commitSubjects(wt.path, base, 'HEAD');
|
|
@@ -309,6 +399,8 @@ function collectBoard(root, { ttlDays = DEFAULT_TTL_DAYS, staleHours = DEFAULT_S
|
|
|
309
399
|
branch.stale = branch.state === 'active' && branch.activityHours >= staleHours;
|
|
310
400
|
}
|
|
311
401
|
|
|
402
|
+
saveCherryCache(cherryCache);
|
|
403
|
+
|
|
312
404
|
const due = branches.filter((b) => b.state === 'due');
|
|
313
405
|
const landed = branches.filter((b) => b.state === 'landed');
|
|
314
406
|
const active = branches.filter((b) => b.state === 'active');
|
|
@@ -333,6 +425,33 @@ function collectBoard(root, { ttlDays = DEFAULT_TTL_DAYS, staleHours = DEFAULT_S
|
|
|
333
425
|
};
|
|
334
426
|
}
|
|
335
427
|
|
|
428
|
+
// One landing board per process per repo. Boot, stream, and the team view
|
|
429
|
+
// each ask for the board more than once in a single command; the first call
|
|
430
|
+
// pays for git, later calls within BOARD_MEMO_MAX_AGE_MS share the result.
|
|
431
|
+
// A full board also answers light requests (light is a subset). Long-lived
|
|
432
|
+
// callers (stream follow) clear the memo before each poll.
|
|
433
|
+
const BOARD_MEMO_MAX_AGE_MS = 10000;
|
|
434
|
+
const boardMemo = new Map();
|
|
435
|
+
|
|
436
|
+
function boardMemoKey(root, { ttlDays = DEFAULT_TTL_DAYS, staleHours = DEFAULT_STALE_HOURS, base = '' } = {}) {
|
|
437
|
+
return JSON.stringify([canonicalPath(root), ttlDays, staleHours, base]);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function sharedBoard(root, opts = {}) {
|
|
441
|
+
const key = boardMemoKey(root, opts);
|
|
442
|
+
const light = Boolean(opts.light);
|
|
443
|
+
const nowMs = Date.now();
|
|
444
|
+
const hit = boardMemo.get(key);
|
|
445
|
+
if (hit && nowMs - hit.at < BOARD_MEMO_MAX_AGE_MS && (light || !hit.light)) return hit.board;
|
|
446
|
+
const board = collectBoard(root, { ...opts, light });
|
|
447
|
+
boardMemo.set(key, { board, light, at: Date.now() });
|
|
448
|
+
return board;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function clearBoardMemo() {
|
|
452
|
+
boardMemo.clear();
|
|
453
|
+
}
|
|
454
|
+
|
|
336
455
|
// Counts for the boot banner and digest. This intentionally pays the same
|
|
337
456
|
// classification cost as the landing board so "in the air" never includes
|
|
338
457
|
// merged branch residue.
|
|
@@ -340,7 +459,7 @@ function landSummary(cwd = process.cwd(), ttlDays = DEFAULT_TTL_DAYS) {
|
|
|
340
459
|
const root = repoRoot(cwd);
|
|
341
460
|
if (!root || !hasCommits(root)) return null;
|
|
342
461
|
try {
|
|
343
|
-
const board =
|
|
462
|
+
const board = sharedBoard(root, { ttlDays, light: true });
|
|
344
463
|
return {
|
|
345
464
|
branches: board.summary.active + board.summary.due,
|
|
346
465
|
due: board.summary.due,
|
|
@@ -918,9 +1037,11 @@ function landCommand(args = []) {
|
|
|
918
1037
|
}
|
|
919
1038
|
|
|
920
1039
|
module.exports = {
|
|
1040
|
+
clearBoardMemo,
|
|
921
1041
|
collectBoard,
|
|
922
1042
|
countLabel,
|
|
923
1043
|
landCommand,
|
|
924
1044
|
landSummary,
|
|
925
1045
|
reap,
|
|
1046
|
+
sharedBoard,
|
|
926
1047
|
};
|
package/commands/member.js
CHANGED
|
@@ -7,11 +7,12 @@ const { loadCredentials } = require('../utils/auth');
|
|
|
7
7
|
const { resolveBackendRoot } = require('../utils/backend-root');
|
|
8
8
|
const { apiRequestJson } = require('../utils/api');
|
|
9
9
|
const { runAliveTick } = require('../lib/member-alive');
|
|
10
|
+
const { memberRefusalReason } = require('../lib/member-type');
|
|
10
11
|
const { defaultObjectiveRunner } = require('../lib/default-runner');
|
|
11
12
|
const { readJson, writeJson } = require('../lib/json-file');
|
|
12
13
|
const { sleepSync } = require('../lib/sleep-sync');
|
|
13
14
|
const { hasFlag, readFlag, readNumberFlag } = require('../lib/arg-parser');
|
|
14
|
-
const { ensureMemberBundle, memberBundlePresent } = require('../lib/member-scaffold');
|
|
15
|
+
const { ensureMemberBundle, memberBundlePresent, updateMemberSkills } = require('../lib/member-scaffold');
|
|
15
16
|
const { memberProcessPrompt, MEMBER_PROCESS_PATH } = require('../lib/member-context');
|
|
16
17
|
|
|
17
18
|
function findWorkspaceBusinessId(startDir = process.cwd()) {
|
|
@@ -306,6 +307,28 @@ function nearestMemberSlug(name, slugs) {
|
|
|
306
307
|
return bestScore <= Math.max(2, Math.floor(needle.length / 3)) ? best : null;
|
|
307
308
|
}
|
|
308
309
|
|
|
310
|
+
// A person is never run as an agent. Reads the member's own type and returns
|
|
311
|
+
// the plain refusal sentence, or '' when the member can run.
|
|
312
|
+
function memberRefusalFor(name, memberFile) {
|
|
313
|
+
try {
|
|
314
|
+
return memberRefusalReason(name, parseFrontmatter(fs.readFileSync(memberFile, 'utf8')) || {});
|
|
315
|
+
} catch {
|
|
316
|
+
return '';
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Same as requireMemberDir, then stops with a non-zero exit for a human or
|
|
321
|
+
// odd-typed member. Every path that runs a member as an agent starts here.
|
|
322
|
+
function requireRunnableMember(name) {
|
|
323
|
+
const paths = requireMemberDir(name);
|
|
324
|
+
const reason = memberRefusalFor(name, paths.memberFile);
|
|
325
|
+
if (reason) {
|
|
326
|
+
console.error(reason);
|
|
327
|
+
process.exit(1);
|
|
328
|
+
}
|
|
329
|
+
return paths;
|
|
330
|
+
}
|
|
331
|
+
|
|
309
332
|
function requireMemberDir(name) {
|
|
310
333
|
if (!name) {
|
|
311
334
|
console.error('Usage: atris member <goal|tick|review> <name> ...');
|
|
@@ -1077,7 +1100,14 @@ function memberRun(name, ...args) {
|
|
|
1077
1100
|
}
|
|
1078
1101
|
|
|
1079
1102
|
const asJson = hasFlag(args, '--json');
|
|
1080
|
-
const paths =
|
|
1103
|
+
const paths = requireRunnableMember(name);
|
|
1104
|
+
// Parking only hides a member from the team views; a run by name still goes.
|
|
1105
|
+
const { isParkedText } = require('../lib/member-park');
|
|
1106
|
+
try {
|
|
1107
|
+
if (isParkedText(fs.readFileSync(paths.memberFile, 'utf8'))) {
|
|
1108
|
+
console.error(`${name} is parked; running anyway`);
|
|
1109
|
+
}
|
|
1110
|
+
} catch { /* an unreadable card is requireMemberDir's problem, not this notice's */ }
|
|
1081
1111
|
const requestedVerifier = readFlag(args, '--verify', '') || memberRunConfiguredVerifier(paths);
|
|
1082
1112
|
const missionText = memberRunMissionText(args);
|
|
1083
1113
|
const hasMissionOverride = Boolean(readFlag(args, '--mission', '') || readFlag(args, '--mission-id', ''));
|
|
@@ -4095,6 +4125,8 @@ function memberList(...flags) {
|
|
|
4095
4125
|
const asJson = flags.includes('--json') || process.argv.includes('--json');
|
|
4096
4126
|
const teamDir = path.join(process.cwd(), 'atris', 'team');
|
|
4097
4127
|
const members = findAllMembers(teamDir);
|
|
4128
|
+
const { isMemberParked } = require('../lib/member-park');
|
|
4129
|
+
const parked = new Set(members.filter((m) => isMemberParked(process.cwd(), m.name)).map((m) => m.name));
|
|
4098
4130
|
|
|
4099
4131
|
if (asJson) {
|
|
4100
4132
|
console.log(JSON.stringify({
|
|
@@ -4102,6 +4134,7 @@ function memberList(...flags) {
|
|
|
4102
4134
|
count: members.length,
|
|
4103
4135
|
members: members.map((m) => ({
|
|
4104
4136
|
name: m.name,
|
|
4137
|
+
...(parked.has(m.name) ? { parked: true } : {}),
|
|
4105
4138
|
role: m.role,
|
|
4106
4139
|
format: m.format,
|
|
4107
4140
|
skills: m.format === 'directory' ? m.skillCount : null,
|
|
@@ -4149,12 +4182,14 @@ function memberList(...flags) {
|
|
|
4149
4182
|
clipCell(m.format === 'directory' ? 'dir' : 'flat', fmtW) +
|
|
4150
4183
|
clipCell(skills, skillW) +
|
|
4151
4184
|
clipCell(context, ctxW) +
|
|
4152
|
-
(m.version || '-')
|
|
4185
|
+
(m.version || '-') +
|
|
4186
|
+
(parked.has(m.name) ? ' parked' : '')
|
|
4153
4187
|
);
|
|
4154
4188
|
}
|
|
4155
4189
|
|
|
4156
4190
|
console.log('');
|
|
4157
|
-
|
|
4191
|
+
const parkedNote = parked.size ? ` (${parked.size} parked; unpark with atris team unpark <name>)` : '';
|
|
4192
|
+
console.log(`${members.length} ${members.length === 1 ? 'member' : 'members'} found.${parkedNote}`);
|
|
4158
4193
|
}
|
|
4159
4194
|
|
|
4160
4195
|
// --- CREATE subcommand ---
|
|
@@ -4209,6 +4244,8 @@ name: ${name}
|
|
|
4209
4244
|
role: ${role}
|
|
4210
4245
|
description: ${description || `Handles ${role.toLowerCase()} tasks`}
|
|
4211
4246
|
version: 1.0.0
|
|
4247
|
+
# ai or human; a human member is a person and Atris never acts as them
|
|
4248
|
+
type: ai
|
|
4212
4249
|
|
|
4213
4250
|
skills: []
|
|
4214
4251
|
|
|
@@ -4296,10 +4333,10 @@ tools: []
|
|
|
4296
4333
|
}
|
|
4297
4334
|
|
|
4298
4335
|
function printMemberInstallUsage(stream = console.log) {
|
|
4299
|
-
stream('Usage: atris member install <slug>');
|
|
4336
|
+
stream('Usage: atris member install <slug> [--update]');
|
|
4300
4337
|
}
|
|
4301
4338
|
|
|
4302
|
-
function memberInstall(slug) {
|
|
4339
|
+
function memberInstall(slug, update = false) {
|
|
4303
4340
|
if (slug === '--help' || slug === '-h') {
|
|
4304
4341
|
printMemberInstallUsage();
|
|
4305
4342
|
return;
|
|
@@ -4314,9 +4351,22 @@ function memberInstall(slug) {
|
|
|
4314
4351
|
}
|
|
4315
4352
|
|
|
4316
4353
|
const memberDir = path.join(process.cwd(), 'atris', 'team', slug);
|
|
4354
|
+
const templateDir = path.join(__dirname, '..', 'templates', 'members', slug);
|
|
4355
|
+
if (update) {
|
|
4356
|
+
if (!fs.existsSync(templateDir)) {
|
|
4357
|
+
console.error(`No member template for ${slug}.`);
|
|
4358
|
+
process.exit(1);
|
|
4359
|
+
}
|
|
4360
|
+
const updated = updateMemberSkills(templateDir, memberDir);
|
|
4361
|
+
if (updated.length === 0) {
|
|
4362
|
+
console.log(`MEMBER ${slug} already up to date`);
|
|
4363
|
+
} else {
|
|
4364
|
+
for (const rel of updated) console.log(`atris/team/${slug}/${rel.split(path.sep).join('/')}`);
|
|
4365
|
+
}
|
|
4366
|
+
return;
|
|
4367
|
+
}
|
|
4317
4368
|
const already = memberBundlePresent(memberDir);
|
|
4318
4369
|
fs.mkdirSync(path.join(memberDir, 'logs'), { recursive: true });
|
|
4319
|
-
const templateDir = path.join(__dirname, '..', 'templates', 'members', slug);
|
|
4320
4370
|
const copied = [];
|
|
4321
4371
|
if (fs.existsSync(templateDir)) {
|
|
4322
4372
|
const copyMissing = (source, relative = '') => {
|
|
@@ -7838,6 +7888,8 @@ function wakeMemberEngine(name, root = process.cwd()) {
|
|
|
7838
7888
|
|
|
7839
7889
|
async function runMemberWake(name, { execute = false, confirmed = false, force = false, domainInput = {} } = {}) {
|
|
7840
7890
|
const paths = requireMemberDir(name);
|
|
7891
|
+
const refusal = memberRefusalFor(name, paths.memberFile);
|
|
7892
|
+
if (refusal) throw new Error(refusal);
|
|
7841
7893
|
const runtimeKind = paths.runtimeKind || memberRuntimeKind(name);
|
|
7842
7894
|
if (runtimeKind === 'auto-improver') {
|
|
7843
7895
|
return runAutoImproverWake(name, paths, { execute, confirmed });
|
|
@@ -8151,6 +8203,7 @@ function clipText(value, max = 70) {
|
|
|
8151
8203
|
}
|
|
8152
8204
|
|
|
8153
8205
|
async function memberWake(name, ...args) {
|
|
8206
|
+
if (name) requireRunnableMember(name);
|
|
8154
8207
|
const asJson = hasFlag(args, '--json');
|
|
8155
8208
|
const execute = hasFlag(args, '--execute') && !hasFlag(args, '--dry-run');
|
|
8156
8209
|
const confirmed = hasFlag(args, '--confirm-autonomy-policy');
|
|
@@ -8198,14 +8251,15 @@ function collectFleetActivationRows(root = process.cwd()) {
|
|
|
8198
8251
|
const goalPlane = memberGoalPlaneStatus(slug, paths);
|
|
8199
8252
|
const activeMission = memberLiveMission(slug, missionMap);
|
|
8200
8253
|
const latestMission = memberLatestMission(slug, missionMap);
|
|
8201
|
-
const
|
|
8254
|
+
const refusal = memberRefusalFor(slug, paths.memberFile);
|
|
8255
|
+
const skipped = Boolean(refusal) || (!goalPlane.goal && !activeMission);
|
|
8202
8256
|
return {
|
|
8203
8257
|
member: slug,
|
|
8204
8258
|
goal: goalPlane.goal || null,
|
|
8205
8259
|
active_mission: activeMission || null,
|
|
8206
8260
|
latest_mission: latestMission || null,
|
|
8207
8261
|
skipped,
|
|
8208
|
-
skip_reason: skipped ? 'no_goal_and_no_active_mission' : null,
|
|
8262
|
+
skip_reason: refusal ? 'not_ai' : skipped ? 'no_goal_and_no_active_mission' : null,
|
|
8209
8263
|
};
|
|
8210
8264
|
})
|
|
8211
8265
|
.sort((a, b) => a.member.localeCompare(b.member));
|
|
@@ -8238,7 +8292,7 @@ async function memberLoopAll(kind, ...args) {
|
|
|
8238
8292
|
for (const row of rows) {
|
|
8239
8293
|
if (row.skipped) {
|
|
8240
8294
|
skipped.push(row);
|
|
8241
|
-
if (!asJson) console.log(`Skip ${row.member}: no goal and no active mission
|
|
8295
|
+
if (!asJson) console.log(`Skip ${row.member}: ${row.skip_reason === 'not_ai' ? 'not an AI member.' : 'no goal and no active mission.'}`);
|
|
8242
8296
|
continue;
|
|
8243
8297
|
}
|
|
8244
8298
|
if (!asJson) console.log(`Activate ${row.member}: ${row.goal ? 'goal' : `mission ${row.active_mission?.id || 'active'}`}`);
|
|
@@ -8650,7 +8704,7 @@ function memberLoopRecordResult(name, paths, lease, run) {
|
|
|
8650
8704
|
// Driver: the phases above do the work; this reads as setup -> gate -> run -> record.
|
|
8651
8705
|
async function memberLoop(name, ...args) {
|
|
8652
8706
|
if (name === '--all' || name === 'all') return memberLoopAll('loop', ...args);
|
|
8653
|
-
|
|
8707
|
+
requireRunnableMember(name);
|
|
8654
8708
|
const asJson = hasFlag(args, '--json');
|
|
8655
8709
|
const aliveMode = hasFlag(args, '--alive');
|
|
8656
8710
|
const execute = hasFlag(args, '--execute');
|
|
@@ -8714,7 +8768,7 @@ async function memberLoop(name, ...args) {
|
|
|
8714
8768
|
}
|
|
8715
8769
|
|
|
8716
8770
|
async function memberTick(name, ...args) {
|
|
8717
|
-
const paths =
|
|
8771
|
+
const paths = requireRunnableMember(name);
|
|
8718
8772
|
const asJson = hasFlag(args, '--json');
|
|
8719
8773
|
const force = hasFlag(args, '--force');
|
|
8720
8774
|
const goalId = readFlag(args, '--goal', '');
|
|
@@ -9332,7 +9386,7 @@ async function memberCommand(subcommand, ...args) {
|
|
|
9332
9386
|
case 'new':
|
|
9333
9387
|
return memberCreate(args[0], ...args.slice(1));
|
|
9334
9388
|
case 'install':
|
|
9335
|
-
return memberInstall(args[0]);
|
|
9389
|
+
return memberInstall(args[0], args.includes('--update'));
|
|
9336
9390
|
case 'activate':
|
|
9337
9391
|
return memberActivate(args[0]);
|
|
9338
9392
|
case 'upgrade':
|
|
@@ -9394,7 +9448,7 @@ async function memberCommand(subcommand, ...args) {
|
|
|
9394
9448
|
console.log('');
|
|
9395
9449
|
console.log('Subcommands:');
|
|
9396
9450
|
console.log(' create <name> Scaffold a new team member (MEMBER.md + dirs) [--push]');
|
|
9397
|
-
console.log(' install <slug>
|
|
9451
|
+
console.log(' install <slug> [--update] Install missing files, or refresh template skills');
|
|
9398
9452
|
console.log(' chat <name> "..." Send a message to this member\'s desk in Atris Desktop');
|
|
9399
9453
|
console.log(' list [--json] Show all team members');
|
|
9400
9454
|
console.log(' activate <name> Symlink member skills, show context and permissions');
|
package/commands/mission.js
CHANGED
|
@@ -22,6 +22,7 @@ const {
|
|
|
22
22
|
const {
|
|
23
23
|
resolveClaudeRunnerModel,
|
|
24
24
|
resolvePinnedRunnerModel,
|
|
25
|
+
runnerModelFor,
|
|
25
26
|
resolveClaudeRunnerBin,
|
|
26
27
|
resolveClaudeRunnerCommandTemplate,
|
|
27
28
|
buildRunnerCommand,
|
|
@@ -32,8 +33,11 @@ const {
|
|
|
32
33
|
const {
|
|
33
34
|
resolveEngineForRoleWithPreference,
|
|
34
35
|
engineFailureHealthStatus,
|
|
36
|
+
engineStallReason,
|
|
37
|
+
recordEngineRunHealth,
|
|
35
38
|
setEngineHealth,
|
|
36
39
|
} = require('../lib/engine-registry');
|
|
40
|
+
const { appendRosterRun, parseRunUsage } = require('../lib/roster-runs');
|
|
37
41
|
const {
|
|
38
42
|
FUNCTIONAL_MEMBER_TOPICS,
|
|
39
43
|
listWorkspaceMemberSlugs,
|
|
@@ -391,9 +395,29 @@ function rosterTickPins(engine) {
|
|
|
391
395
|
return {
|
|
392
396
|
...(engine && engine.roster_effort ? { roster_effort: engine.roster_effort } : {}),
|
|
393
397
|
...(engine && engine.roster_max_seconds ? { roster_max_seconds: engine.roster_max_seconds } : {}),
|
|
398
|
+
...(engine && engine.roster_prep ? { roster_prep: engine.roster_prep } : {}),
|
|
394
399
|
};
|
|
395
400
|
}
|
|
396
401
|
|
|
402
|
+
// A tick whose roster worker asks for prep ("prep: search") runs the same
|
|
403
|
+
// prep pass fleet runs before the worker starts, and the worker's prompt
|
|
404
|
+
// gets the brief. No prep line means the prompt goes out as it was.
|
|
405
|
+
async function missionTickPrep(root, { mission, runtimeMission, job, prompt, ask } = {}) {
|
|
406
|
+
const prepJob = String(runtimeMission && runtimeMission.roster_prep || '').trim();
|
|
407
|
+
if (!prepJob) return { prompt, prep: null };
|
|
408
|
+
const { runPrepPass, withPrepBrief } = require('../lib/roster-prep');
|
|
409
|
+
const prep = await runPrepPass({
|
|
410
|
+
prepJob,
|
|
411
|
+
forJob: job || 'build',
|
|
412
|
+
task: { display_id: (mission && mission.id) || '', title: (mission && (mission.objective || mission.slug)) || '' },
|
|
413
|
+
prompt,
|
|
414
|
+
root,
|
|
415
|
+
record: { member: (mission && mission.owner) || '' },
|
|
416
|
+
...(ask ? { ask } : {}),
|
|
417
|
+
});
|
|
418
|
+
return { prompt: withPrepBrief(prompt, prep), prep };
|
|
419
|
+
}
|
|
420
|
+
|
|
397
421
|
// A tick's time limit: the time left on the mission, capped by the roster
|
|
398
422
|
// line's "max 20 min" when it sets one, else by the default tick cap.
|
|
399
423
|
function missionTickTimeoutMs(mission, remainingMs) {
|
|
@@ -427,6 +451,7 @@ function resolveMissionTickRunner(mission, root = process.cwd(), options = {}) {
|
|
|
427
451
|
requested_engine: null,
|
|
428
452
|
engine_fallback_reason: null,
|
|
429
453
|
member_engine_reason: member.reason,
|
|
454
|
+
roster_job: member.job || 'build',
|
|
430
455
|
};
|
|
431
456
|
}
|
|
432
457
|
}
|
|
@@ -445,10 +470,59 @@ function resolveMissionTickRunner(mission, root = process.cwd(), options = {}) {
|
|
|
445
470
|
};
|
|
446
471
|
}
|
|
447
472
|
|
|
473
|
+
// A stalled tick benches its engine for the cooldown, so the next tick's
|
|
474
|
+
// roster pick is the backup; no retry inside the tick is needed.
|
|
448
475
|
function recordMissionEngineTickOutcome(engineId, result, root = process.cwd()) {
|
|
449
476
|
if (!engineId) return null;
|
|
450
|
-
|
|
451
|
-
|
|
477
|
+
if (result && result.status === 'ran') return setEngineHealth(engineId, 'ready', root);
|
|
478
|
+
if (!engineFailureHealthStatus(result)) return null;
|
|
479
|
+
return recordEngineRunHealth(engineId, result, root);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// One run-record line per engine tick: how long the worker ran, and how it
|
|
483
|
+
// ended. A tick that ran and passed (or had no verifier) landed; a failing
|
|
484
|
+
// verifier or a real error failed; a time cap or dropped connection stalled;
|
|
485
|
+
// a closed credit window is credit out. Never throws into the tick.
|
|
486
|
+
function missionTickRunOutcome(result, verifierResult, maxSeconds) {
|
|
487
|
+
if (result && result.status === 'ran') {
|
|
488
|
+
return verifierResult && verifierResult.passed === false
|
|
489
|
+
? { outcome: 'failed', detail: 'the verifier failed' }
|
|
490
|
+
: { outcome: 'landed' };
|
|
491
|
+
}
|
|
492
|
+
if (engineFailureHealthStatus(result) === 'credit_out') return { outcome: 'credit out', detail: 'the engine hit its usage limit' };
|
|
493
|
+
const stall = engineStallReason({ ...result, ...(maxSeconds ? { max_seconds: maxSeconds } : {}) });
|
|
494
|
+
if (stall) return { outcome: 'stalled', detail: stall.text };
|
|
495
|
+
if (/wall-exceeded/.test(String(result && result.reason || ''))) return { outcome: 'failed', detail: 'the mission ran out of time' };
|
|
496
|
+
const summary = String(result && result.claude && result.claude.summary || '').split('\n')[0].trim();
|
|
497
|
+
return { outcome: 'failed', detail: summary && summary !== 'error' ? summary : String(result && result.reason || 'the tick failed').replace(/-/g, ' ') };
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function recordMissionTickRosterRun(root, { mission, runtimeMission, engineId, job, result, verifierResult, startedAt, endedMs, usage, prep = null } = {}) {
|
|
501
|
+
if (!engineId) return null;
|
|
502
|
+
try {
|
|
503
|
+
const { engineRunsView } = require('../lib/roster-models');
|
|
504
|
+
const { prepRecordFields } = require('../lib/roster-prep');
|
|
505
|
+
const run = runtimeMission || mission || {};
|
|
506
|
+
const startedMs = Date.parse(startedAt);
|
|
507
|
+
const maxSeconds = Number(run.roster_max_seconds) || 0;
|
|
508
|
+
return appendRosterRun(root, {
|
|
509
|
+
at: startedAt,
|
|
510
|
+
job: job || 'build',
|
|
511
|
+
member: (mission && mission.owner) || '',
|
|
512
|
+
engine: engineId,
|
|
513
|
+
model: run.model || engineRunsView(engineId).model || '',
|
|
514
|
+
effort: run.roster_effort || '',
|
|
515
|
+
max_seconds: maxSeconds,
|
|
516
|
+
seconds: Number.isFinite(startedMs) ? Math.max(0, (endedMs - startedMs) / 1000) : undefined,
|
|
517
|
+
task: (mission && mission.id) || '',
|
|
518
|
+
source: 'mission',
|
|
519
|
+
...prepRecordFields(prep),
|
|
520
|
+
...(usage || {}),
|
|
521
|
+
...missionTickRunOutcome(result, verifierResult, maxSeconds),
|
|
522
|
+
});
|
|
523
|
+
} catch {
|
|
524
|
+
return null;
|
|
525
|
+
}
|
|
452
526
|
}
|
|
453
527
|
|
|
454
528
|
function runnerModelPatch(runner, model) {
|
|
@@ -495,6 +569,8 @@ function applyMissionRunnerProfile(runner) {
|
|
|
495
569
|
// default model pin. Every other engine's CLI rejects a claude model name, so
|
|
496
570
|
// it only ever takes an explicit pin (the mission's model or the roster
|
|
497
571
|
// line's); '' lets the engine ride its own default.
|
|
572
|
+
// A mission often still carries the claude model it was created with; an
|
|
573
|
+
// engine that runs only its own models (codex) drops it.
|
|
498
574
|
const CLAUDE_FAMILY_RUNNER_ENGINES = new Set(['claude', 'fable', 'haiku']);
|
|
499
575
|
|
|
500
576
|
function resolveMissionTickRunnerModel(mission) {
|
|
@@ -502,7 +578,7 @@ function resolveMissionTickRunnerModel(mission) {
|
|
|
502
578
|
if (!engine || CLAUDE_FAMILY_RUNNER_ENGINES.has(engine)) {
|
|
503
579
|
return resolveClaudeRunnerModel(mission);
|
|
504
580
|
}
|
|
505
|
-
return resolvePinnedRunnerModel(mission);
|
|
581
|
+
return runnerModelFor(engine, resolvePinnedRunnerModel(mission));
|
|
506
582
|
}
|
|
507
583
|
|
|
508
584
|
function exitMissionError(message, code = 1, asJson = false) {
|
|
@@ -8904,6 +8980,7 @@ function spawnGenericRunnerTick(mission, opts) {
|
|
|
8904
8980
|
stop_reason: null,
|
|
8905
8981
|
stderr: errStr,
|
|
8906
8982
|
parse_errors: 0,
|
|
8983
|
+
run_usage: parseRunUsage({ stdout, stderr }),
|
|
8907
8984
|
});
|
|
8908
8985
|
});
|
|
8909
8986
|
|
|
@@ -9008,6 +9085,7 @@ function spawnClaudeTick(mission, opts) {
|
|
|
9008
9085
|
let numTurns = null;
|
|
9009
9086
|
let rateLimitInfo = null;
|
|
9010
9087
|
let stopReason = null;
|
|
9088
|
+
let usage = null;
|
|
9011
9089
|
let parseErrors = 0;
|
|
9012
9090
|
let stderr = '';
|
|
9013
9091
|
let timedOut = false;
|
|
@@ -9047,6 +9125,7 @@ function spawnClaudeTick(mission, opts) {
|
|
|
9047
9125
|
if (typeof ev.duration_api_ms === 'number') durationApiMs = ev.duration_api_ms;
|
|
9048
9126
|
if (typeof ev.num_turns === 'number') numTurns = ev.num_turns;
|
|
9049
9127
|
if (ev.stop_reason) stopReason = ev.stop_reason;
|
|
9128
|
+
if (ev.usage && typeof ev.usage === 'object') usage = ev.usage;
|
|
9050
9129
|
}
|
|
9051
9130
|
} catch {
|
|
9052
9131
|
parseErrors++;
|
|
@@ -9083,6 +9162,7 @@ function spawnClaudeTick(mission, opts) {
|
|
|
9083
9162
|
rate_limit_info: rateLimitInfo,
|
|
9084
9163
|
stderr: errStr,
|
|
9085
9164
|
parse_errors: parseErrors,
|
|
9165
|
+
run_usage: parseRunUsage({ usage, cost_usd: costEstimate }),
|
|
9086
9166
|
});
|
|
9087
9167
|
});
|
|
9088
9168
|
|
|
@@ -9571,6 +9651,9 @@ async function executeMissionRunTicksPhase(context) {
|
|
|
9571
9651
|
const tickStart = stampIso();
|
|
9572
9652
|
const tickWorktreeBefore = gitWorktreeSnapshot(cwd);
|
|
9573
9653
|
let result = { status: 'skipped', reason: 'unknown', tick_index: tickIdx, ran: false, started_at: tickStart };
|
|
9654
|
+
// Set when a roster engine really ran this tick: when it stopped and the
|
|
9655
|
+
// usage it printed. The run record is written after the verifier.
|
|
9656
|
+
let tickWorkerRun = null;
|
|
9574
9657
|
let cachedStep = findCachedMissionStepReceipt(cwd, { missionId: mission.id, tickIndex: tickIdx });
|
|
9575
9658
|
let cachedVerifierResult = cachedStep ? cachedStep.verifier_result : null;
|
|
9576
9659
|
const tickSelection = resolveMissionTickRunner(runtimeMission, cwd);
|
|
@@ -9707,6 +9790,7 @@ async function executeMissionRunTicksPhase(context) {
|
|
|
9707
9790
|
backend_unavailable: isTransientAtris2BackendError(turn.error) || undefined,
|
|
9708
9791
|
receipt_text: String(turn.text || '').slice(0, 4000),
|
|
9709
9792
|
};
|
|
9793
|
+
tickWorkerRun = { endedMs: Date.now(), usage: {} };
|
|
9710
9794
|
if (controller.signal.aborted) { pauseReason = 'aborted-during-atris2'; break; }
|
|
9711
9795
|
if (turn.error === 'not-logged-in') { pauseReason = 'auth-required'; break; }
|
|
9712
9796
|
if (wallExpired) {
|
|
@@ -9724,6 +9808,17 @@ async function executeMissionRunTicksPhase(context) {
|
|
|
9724
9808
|
const pingDrain = consumeMissionPings(mission, cwd);
|
|
9725
9809
|
mission = pingDrain.mission;
|
|
9726
9810
|
runtimeMission = runtimeView(mission);
|
|
9811
|
+
let tickPrep = null;
|
|
9812
|
+
let workerPrompt = prompt;
|
|
9813
|
+
if (tickEngineId) {
|
|
9814
|
+
({ prompt: workerPrompt, prep: tickPrep } = await missionTickPrep(cwd, {
|
|
9815
|
+
mission,
|
|
9816
|
+
runtimeMission: tickRuntimeMission,
|
|
9817
|
+
job: tickSelection.roster_job || 'build',
|
|
9818
|
+
prompt,
|
|
9819
|
+
}));
|
|
9820
|
+
}
|
|
9821
|
+
const workerStartedAt = tickPrep ? stampIso() : tickStart;
|
|
9727
9822
|
const restoreTickRunnerProfile = tickEngineId ? applyMissionRunnerProfile(tickEngineId) : () => {};
|
|
9728
9823
|
let claudeResult;
|
|
9729
9824
|
let sessionBusyRetried = false;
|
|
@@ -9734,7 +9829,7 @@ async function executeMissionRunTicksPhase(context) {
|
|
|
9734
9829
|
tickRuntimeMission,
|
|
9735
9830
|
(maxWallSeconds - ((Date.now() - startedAt) / 1000)) * 1000,
|
|
9736
9831
|
),
|
|
9737
|
-
prompt,
|
|
9832
|
+
prompt: workerPrompt,
|
|
9738
9833
|
model: resolveMissionTickRunnerModel(tickRuntimeMission),
|
|
9739
9834
|
});
|
|
9740
9835
|
try {
|
|
@@ -9754,6 +9849,7 @@ async function executeMissionRunTicksPhase(context) {
|
|
|
9754
9849
|
} finally {
|
|
9755
9850
|
restoreTickRunnerProfile();
|
|
9756
9851
|
}
|
|
9852
|
+
tickWorkerRun = { endedMs: Date.now(), usage: (claudeResult && claudeResult.run_usage) || {}, prep: tickPrep, startedAt: workerStartedAt };
|
|
9757
9853
|
result.claude = {
|
|
9758
9854
|
ok: claudeResult.ok,
|
|
9759
9855
|
reason: claudeResult.reason,
|
|
@@ -9860,6 +9956,20 @@ async function executeMissionRunTicksPhase(context) {
|
|
|
9860
9956
|
}
|
|
9861
9957
|
pauseReason = context.pauseReason;
|
|
9862
9958
|
context.currentTick = null;
|
|
9959
|
+
if (tickEngineId && !cachedStep && tickWorkerRun) {
|
|
9960
|
+
recordMissionTickRosterRun(cwd, {
|
|
9961
|
+
mission,
|
|
9962
|
+
runtimeMission: tickRuntimeMission,
|
|
9963
|
+
engineId: tickEngineId,
|
|
9964
|
+
job: tickSelection.roster_job || 'build',
|
|
9965
|
+
result,
|
|
9966
|
+
verifierResult,
|
|
9967
|
+
startedAt: tickWorkerRun.startedAt || tickStart,
|
|
9968
|
+
endedMs: tickWorkerRun.endedMs,
|
|
9969
|
+
usage: tickWorkerRun.usage,
|
|
9970
|
+
prep: tickWorkerRun.prep || null,
|
|
9971
|
+
});
|
|
9972
|
+
}
|
|
9863
9973
|
let receiptPath = cachedStep ? cachedStep.receipt_path : null;
|
|
9864
9974
|
|
|
9865
9975
|
// Review-lane drain: always-on loops sweep the agent-safe review actions
|
|
@@ -11792,6 +11902,8 @@ module.exports = {
|
|
|
11792
11902
|
missionTickTimeoutMs,
|
|
11793
11903
|
engineFailureHealthStatus,
|
|
11794
11904
|
recordMissionEngineTickOutcome,
|
|
11905
|
+
recordMissionTickRosterRun,
|
|
11906
|
+
missionTickPrep,
|
|
11795
11907
|
tickMadeProgress,
|
|
11796
11908
|
consecutiveNoProgressTicks,
|
|
11797
11909
|
consecutiveIdenticalSummaryTicks,
|