atris 3.16.1 → 3.22.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 +32 -7
- package/atris/skills/atris/SKILL.md +15 -2
- package/atris/skills/atris-feedback/SKILL.md +7 -0
- package/atris/skills/design/SKILL.md +29 -2
- package/atris/skills/engines/SKILL.md +44 -0
- package/atris/skills/flow/SKILL.md +1 -1
- package/atris/skills/wake/SKILL.md +37 -0
- package/atris/skills/youtube/SKILL.md +13 -39
- package/atris/team/validator/MEMBER.md +1 -0
- package/atris/wiki/concepts/agent-activation-contract.md +3 -3
- package/atris/wiki/concepts/workspace-initialization-contract.md +3 -3
- package/atris/wiki/index.md +1 -0
- package/atris.md +43 -19
- package/bin/atris.js +413 -31
- package/commands/agent-spawn.js +480 -0
- package/commands/analytics.js +6 -3
- package/commands/apps.js +11 -0
- package/commands/autopilot.js +42 -18
- package/commands/brain.js +74 -7
- package/commands/brainstorm.js +9 -58
- package/commands/clean.js +1 -4
- package/commands/compile.js +9 -4
- package/commands/console.js +8 -3
- package/commands/deck.js +184 -0
- package/commands/init.js +22 -11
- package/commands/lesson.js +76 -0
- package/commands/member.js +252 -48
- package/commands/mission.js +405 -13
- package/commands/now.js +4 -2
- package/commands/probe.js +105 -27
- package/commands/pulse.js +504 -0
- package/commands/radar.js +1 -0
- package/commands/recap.js +71 -25
- package/commands/run.js +615 -22
- package/commands/site.js +48 -0
- package/commands/slop.js +307 -0
- package/commands/spaceship.js +39 -0
- package/commands/sync.js +0 -2
- package/commands/task.js +429 -37
- package/commands/theme.js +217 -0
- package/commands/verify.js +7 -3
- package/lib/activity-stream.js +166 -0
- package/lib/auto-accept-certified.js +23 -1
- package/lib/context-gatherer.js +170 -0
- package/lib/deck-from-md.js +110 -0
- package/lib/escape-regexp.js +13 -0
- package/lib/file-ops.js +6 -3
- package/lib/html-render.js +257 -0
- package/lib/journal.js +1 -1
- package/lib/lesson-contradiction.js +113 -0
- package/lib/memory-view.js +95 -0
- package/lib/policy-lessons.js +3 -2
- package/lib/pulse.js +401 -0
- package/lib/runner-command.js +156 -0
- package/lib/site.js +114 -0
- package/lib/slides-deck.js +237 -0
- package/lib/state-detection.js +1 -4
- package/lib/task-db.js +101 -4
- package/lib/task-proof.js +1 -1
- package/lib/theme.js +264 -0
- package/lib/todo-fallback.js +2 -1
- package/lib/todo-sections.js +33 -0
- package/package.json +1 -2
- package/utils/api.js +14 -2
- package/atris/atrisDev.md +0 -717
package/commands/mission.js
CHANGED
|
@@ -4,6 +4,10 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const crypto = require('crypto');
|
|
6
6
|
const { spawn, spawnSync } = require('child_process');
|
|
7
|
+
const {
|
|
8
|
+
resolveClaudeRunnerModel,
|
|
9
|
+
resolveClaudeRunnerBin,
|
|
10
|
+
} = require('../lib/runner-command');
|
|
7
11
|
|
|
8
12
|
const VALID_STATUSES = new Set(['planning', 'running', 'ready', 'paused', 'blocked', 'stopped', 'complete']);
|
|
9
13
|
const TERMINAL_STATUSES = new Set(['stopped', 'complete']);
|
|
@@ -425,7 +429,7 @@ function renderMemberNowMarkdown(owner, missions) {
|
|
|
425
429
|
lines.push(`- id: ${mission.id}`);
|
|
426
430
|
lines.push(`- status: ${mission.status}`);
|
|
427
431
|
lines.push(`- cadence: ${mission.cadence}`);
|
|
428
|
-
lines.push(`- runner: ${mission.runner}`);
|
|
432
|
+
lines.push(`- runner: ${mission.runner}${mission.model ? ` (${mission.model})` : ''}`);
|
|
429
433
|
lines.push(`- lane: ${mission.lane}`);
|
|
430
434
|
if (mission.xp_task?.ref) lines.push(`- AgentXP task: ${mission.xp_task.ref}`);
|
|
431
435
|
if (mission.verifier) lines.push(`- verifier: ${mission.verifier}`);
|
|
@@ -515,6 +519,7 @@ function missionFromArgs(args) {
|
|
|
515
519
|
'--stop',
|
|
516
520
|
'--task',
|
|
517
521
|
'--ask',
|
|
522
|
+
'--model',
|
|
518
523
|
], ['--json', '--always-on', '--xp-task', '--agent-xp', '--worktree']).join(' ').trim();
|
|
519
524
|
if (!objective) {
|
|
520
525
|
exitMissionError('Usage: atris mission start "<objective>" --owner <member> [--verify "..."] [--cadence manual] [--worktree]', 1, wantsJson(args));
|
|
@@ -522,6 +527,7 @@ function missionFromArgs(args) {
|
|
|
522
527
|
const owner = readFlag(args, '--owner', process.env.ATRIS_AGENT_ID || 'mission-lead');
|
|
523
528
|
const cadence = readFlag(args, '--cadence', readFlag(args, '--loop', 'manual')) || 'manual';
|
|
524
529
|
const runner = readFlag(args, '--runner', 'manual');
|
|
530
|
+
const model = readFlag(args, '--model', '') || (String(runner).toLowerCase() === 'atris2' ? 'atris:fast' : '');
|
|
525
531
|
const lane = readFlag(args, '--lane', 'workspace');
|
|
526
532
|
const verifier = readFlag(args, '--verify', '');
|
|
527
533
|
assertMissionVerifier(verifier, wantsJson(args));
|
|
@@ -540,6 +546,7 @@ function missionFromArgs(args) {
|
|
|
540
546
|
status: 'planning',
|
|
541
547
|
cadence,
|
|
542
548
|
runner,
|
|
549
|
+
...(model ? { model } : {}),
|
|
543
550
|
lane,
|
|
544
551
|
verifier,
|
|
545
552
|
always_on: alwaysOn,
|
|
@@ -661,6 +668,7 @@ function statusMission(args) {
|
|
|
661
668
|
` id: ${mission.id}`,
|
|
662
669
|
` owner: ${mission.owner}`,
|
|
663
670
|
` state: ${mission.status}`,
|
|
671
|
+
...missionHeartbeatLines(mission),
|
|
664
672
|
...(mission.worktree_root ? [` worktree: ${mission.worktree_root}`] : []),
|
|
665
673
|
` next: ${mission.next_action || 'tick or verify'}`,
|
|
666
674
|
...(mission.receipt_path ? [` proof: ${mission.receipt_path}`] : []),
|
|
@@ -671,6 +679,68 @@ function statusMission(args) {
|
|
|
671
679
|
);
|
|
672
680
|
}
|
|
673
681
|
|
|
682
|
+
// `atris mission watch [id]` — read-only live heartbeat. Prints a line per tick as it
|
|
683
|
+
// lands so a human (or any terminal) can see the loop is alive without rerunning status.
|
|
684
|
+
function watchMission(args) {
|
|
685
|
+
const ref = stripKnownFlags(args, ['--interval', '--idle-every'], [])[0] || '';
|
|
686
|
+
const intervalSeconds = Math.max(1, parseInt(readFlag(args, '--interval', '2'), 10) || 2);
|
|
687
|
+
const idleEverySeconds = Math.max(1, parseInt(readFlag(args, '--idle-every', '30'), 10) || 30);
|
|
688
|
+
const loadTargets = () => {
|
|
689
|
+
if (ref) {
|
|
690
|
+
const mission = resolveMission(ref);
|
|
691
|
+
return mission ? [mission] : [];
|
|
692
|
+
}
|
|
693
|
+
return listMissions().filter((mission) => !HEARTBEAT_TERMINAL_STATUSES.has(mission.status));
|
|
694
|
+
};
|
|
695
|
+
if (ref && !loadTargets().length) {
|
|
696
|
+
exitMissionError(`Mission "${ref}" not found.`, 1, false);
|
|
697
|
+
}
|
|
698
|
+
const stamp = () => new Date().toTimeString().slice(0, 8);
|
|
699
|
+
const shortId = (mission) => mission.id.length > 20 ? `…${mission.id.slice(-8)}` : mission.id;
|
|
700
|
+
const emit = (mission, note) => console.log(`[${stamp()}] ${mission.owner} ${shortId(mission)} — ${note}`);
|
|
701
|
+
const fingerprint = (mission) => [mission.status, mission.last_tick_at, mission.last_tick_index, mission.receipt_path].join('|');
|
|
702
|
+
const tickNote = (mission) => {
|
|
703
|
+
const heartbeat = missionHeartbeatLines(mission).map((line) => line.trim()).join(', ');
|
|
704
|
+
return `${heartbeat || `state: ${mission.status}`}${mission.receipt_path ? ` — proof: ${mission.receipt_path}` : ''}`;
|
|
705
|
+
};
|
|
706
|
+
const seen = new Map();
|
|
707
|
+
let lastIdleAt = Date.now();
|
|
708
|
+
console.log(`watching ${ref || 'active missions'} every ${intervalSeconds}s — ctrl+c to stop`);
|
|
709
|
+
const poll = () => {
|
|
710
|
+
const targets = loadTargets();
|
|
711
|
+
if (!targets.length && !seen.size) {
|
|
712
|
+
emitOnce('no active missions yet — waiting');
|
|
713
|
+
}
|
|
714
|
+
let changed = false;
|
|
715
|
+
for (const mission of targets) {
|
|
716
|
+
const fp = fingerprint(mission);
|
|
717
|
+
if (seen.get(mission.id) !== fp) {
|
|
718
|
+
seen.set(mission.id, fp);
|
|
719
|
+
emit(mission, tickNote(mission));
|
|
720
|
+
changed = true;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
if (changed) {
|
|
724
|
+
lastIdleAt = Date.now();
|
|
725
|
+
} else if (Date.now() - lastIdleAt >= idleEverySeconds * 1000) {
|
|
726
|
+
lastIdleAt = Date.now();
|
|
727
|
+
for (const mission of targets) {
|
|
728
|
+
emit(mission, `alive, ${missionHeartbeatLines(mission).map((line) => line.trim()).join(', ') || `state: ${mission.status}`}`);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
let warnedEmpty = false;
|
|
733
|
+
const emitOnce = (message) => {
|
|
734
|
+
if (warnedEmpty) return;
|
|
735
|
+
warnedEmpty = true;
|
|
736
|
+
console.log(`[${stamp()}] ${message}`);
|
|
737
|
+
};
|
|
738
|
+
poll();
|
|
739
|
+
setInterval(poll, intervalSeconds * 1000);
|
|
740
|
+
// The bin router exits when the command's promise settles; watch runs until ctrl+c.
|
|
741
|
+
return new Promise(() => {});
|
|
742
|
+
}
|
|
743
|
+
|
|
674
744
|
function writeReceipt(mission, result, root = process.cwd()) {
|
|
675
745
|
const paths = statePaths(root);
|
|
676
746
|
fs.mkdirSync(paths.runsDir, { recursive: true });
|
|
@@ -957,6 +1027,37 @@ function secondsUntilMissionDue(mission, now = new Date()) {
|
|
|
957
1027
|
return Math.max(0, Math.ceil((dueAt - now.getTime()) / 1000));
|
|
958
1028
|
}
|
|
959
1029
|
|
|
1030
|
+
function formatDurationShort(seconds) {
|
|
1031
|
+
if (!Number.isFinite(seconds) || seconds < 0) return null;
|
|
1032
|
+
if (seconds < 60) return `${Math.round(seconds)}s`;
|
|
1033
|
+
if (seconds < 3600) return `${Math.round(seconds / 60)}m`;
|
|
1034
|
+
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ${Math.round((seconds % 3600) / 60)}m`;
|
|
1035
|
+
return `${Math.floor(seconds / 86400)}d`;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
const HEARTBEAT_TERMINAL_STATUSES = new Set(['complete', 'stopped', 'paused']);
|
|
1039
|
+
|
|
1040
|
+
function missionHeartbeatLines(mission, now = new Date()) {
|
|
1041
|
+
const lines = [];
|
|
1042
|
+
const lastTickAt = mission.last_tick_at ? Date.parse(mission.last_tick_at) : NaN;
|
|
1043
|
+
if (Number.isFinite(lastTickAt)) {
|
|
1044
|
+
const age = formatDurationShort((now.getTime() - lastTickAt) / 1000);
|
|
1045
|
+
const verifier = mission.verifier
|
|
1046
|
+
? (mission.verifier_result ? (mission.verifier_result.passed ? 'verifier passed' : 'verifier failed') : 'verifier not run')
|
|
1047
|
+
: 'no verifier';
|
|
1048
|
+
const tickIdx = mission.last_tick_index != null ? `#${mission.last_tick_index}, ` : '';
|
|
1049
|
+
const layerSuffix = mission.last_tick_layer ? `, layer: ${mission.last_tick_layer}` : '';
|
|
1050
|
+
lines.push(` last tick: ${age} ago (${tickIdx}${mission.last_tick_status || 'unknown'}, ${verifier}${layerSuffix})`);
|
|
1051
|
+
} else if (!HEARTBEAT_TERMINAL_STATUSES.has(mission.status)) {
|
|
1052
|
+
lines.push(' last tick: never');
|
|
1053
|
+
}
|
|
1054
|
+
if (parseCadenceSeconds(mission.cadence) > 0 && !HEARTBEAT_TERMINAL_STATUSES.has(mission.status)) {
|
|
1055
|
+
const dueIn = secondsUntilMissionDue(mission, now);
|
|
1056
|
+
lines.push(dueIn === 0 ? ' due: now' : ` due: in ${formatDurationShort(dueIn)}`);
|
|
1057
|
+
}
|
|
1058
|
+
return lines;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
960
1061
|
function missionHasHumanAsks(mission) {
|
|
961
1062
|
return Array.isArray(mission?.human_asks)
|
|
962
1063
|
&& mission.human_asks.some((ask) => String(ask || '').trim());
|
|
@@ -1214,6 +1315,22 @@ function consecutiveVerifierFails(ticks) {
|
|
|
1214
1315
|
return n;
|
|
1215
1316
|
}
|
|
1216
1317
|
|
|
1318
|
+
// Count the trailing run of errored ticks that all share the most-recent error's
|
|
1319
|
+
// reason. Backoff caps at 10min, so an error that recurs identically (claude-timeout,
|
|
1320
|
+
// atris2-error, a dead model) otherwise retries forever until max-ticks/max-wall.
|
|
1321
|
+
// Two identical failures in a row is the MEMBER.md "same approach failed twice" signal.
|
|
1322
|
+
function consecutiveSameReasonErrors(ticks) {
|
|
1323
|
+
const last = ticks[ticks.length - 1];
|
|
1324
|
+
if (!last || last.status !== 'errored' || !last.reason) return { reason: null, count: 0 };
|
|
1325
|
+
let count = 0;
|
|
1326
|
+
for (let i = ticks.length - 1; i >= 0; i--) {
|
|
1327
|
+
const t = ticks[i];
|
|
1328
|
+
if (t.status === 'errored' && t.reason === last.reason) count++;
|
|
1329
|
+
else break;
|
|
1330
|
+
}
|
|
1331
|
+
return { reason: last.reason, count };
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1217
1334
|
function isWithinActiveHours(activeHours, now = new Date()) {
|
|
1218
1335
|
if (!activeHours || !activeHours.start || !activeHours.end) return true;
|
|
1219
1336
|
const tz = activeHours.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
|
|
@@ -1255,8 +1372,9 @@ function releaseMissionLock(lock) {
|
|
|
1255
1372
|
}
|
|
1256
1373
|
|
|
1257
1374
|
function probeClaudeBinary() {
|
|
1258
|
-
const
|
|
1259
|
-
|
|
1375
|
+
const runnerBin = resolveClaudeRunnerBin();
|
|
1376
|
+
const help = spawnSync(runnerBin, ['--help'], { encoding: 'utf8', timeout: 8000 });
|
|
1377
|
+
if (help.status !== 0) return { ok: false, error: `${runnerBin} --help failed` };
|
|
1260
1378
|
const text = String(help.stdout || '');
|
|
1261
1379
|
const required = ['--output-format', '--permission-mode', '--resume', '--session-id', '--include-partial-messages'];
|
|
1262
1380
|
const missing = required.filter((flag) => !text.includes(flag));
|
|
@@ -1279,6 +1397,7 @@ function buildTickPrompt(mission, tickIndex, maxTicks, frozen) {
|
|
|
1279
1397
|
``,
|
|
1280
1398
|
`## Your task`,
|
|
1281
1399
|
`Do ONE increment of work toward the stop condition. ONE. No more.`,
|
|
1400
|
+
`- You are the member "${mission.owner}". Read atris/team/${mission.owner}/MEMBER.md (and SOUL.md if present) before acting — work in that identity, inside its scope and stop rules. After your work, append what you did and what you learned to atris/team/${mission.owner}/logs/<today's date>.md.`,
|
|
1282
1401
|
`- FIRST: inspect current mission/task state before acting. Read the relevant files, run \`atris mission status ${mission.id}\`, \`git status\`, or \`atris task list\` as needed so you know what's already done.`,
|
|
1283
1402
|
`- Pick the smallest concrete action that moves the mission forward.`,
|
|
1284
1403
|
`- Edit / run / research as needed for the lane.`,
|
|
@@ -1291,7 +1410,7 @@ function buildTickPrompt(mission, tickIndex, maxTicks, frozen) {
|
|
|
1291
1410
|
`- Do NOT start new missions, modify other missions, or expand scope.`,
|
|
1292
1411
|
`- Do NOT run destructive commands without strong evidence they're correct.`,
|
|
1293
1412
|
``,
|
|
1294
|
-
`When done, output a short receipt: (1) the exact files edited / commands run / artifacts produced — name them, (2) the metric of progress, (3) what the next tick should pick up.`,
|
|
1413
|
+
`When done, output a short receipt: (1) the exact files edited / commands run / artifacts produced — name them, (2) the metric of progress, (3) what the next tick should pick up. End the receipt with one line naming the layer this tick touched: \`layer: identity|beliefs|capabilities|behaviors|environment\` (final line — the harness parses it).`,
|
|
1295
1414
|
];
|
|
1296
1415
|
if (mission.task_ids?.length) {
|
|
1297
1416
|
lines.push('', `## Task ids`, mission.task_ids.map((t) => `- ${t}`).join('\n'));
|
|
@@ -1302,8 +1421,43 @@ function buildTickPrompt(mission, tickIndex, maxTicks, frozen) {
|
|
|
1302
1421
|
return lines.join('\n');
|
|
1303
1422
|
}
|
|
1304
1423
|
|
|
1424
|
+
// resolveClaudeRunnerModel / resolveClaudeRunnerBin + the runner-resolution rationale now live in
|
|
1425
|
+
// lib/runner-command.js so missions, autopilot, and run share one resolver
|
|
1426
|
+
// (imported at the top, re-exported below for test/mission-model-resolution.test.js).
|
|
1427
|
+
|
|
1428
|
+
// The claude CLI prints "...issue with the selected model (<id>). It may not exist
|
|
1429
|
+
// or you may not have access to it." when a model id is retired or inaccessible.
|
|
1430
|
+
// Detect it so the tick reason becomes the actionable 'model-unavailable' (+the id)
|
|
1431
|
+
// instead of a generic 'claude-error' that buries the root cause.
|
|
1432
|
+
function detectUnavailableModel(text) {
|
|
1433
|
+
const s = String(text || '');
|
|
1434
|
+
const m = s.match(/issue with the selected model \(([^)]+)\)/i);
|
|
1435
|
+
if (m) return m[1].trim();
|
|
1436
|
+
if (/selected model/i.test(s) && /may not (?:exist|have access)/i.test(s)) return 'unknown';
|
|
1437
|
+
return null;
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
// Human-facing guidance written to a paused mission's next_action. Most pauses just
|
|
1441
|
+
// need a resume; a model-unavailable pause is a config error a bare resume won't fix,
|
|
1442
|
+
// so name the dead id and the two knobs that change it.
|
|
1443
|
+
function missionPauseNextAction(pauseReason, missionId, deadModel = null, lastErrorReason = null) {
|
|
1444
|
+
if (pauseReason === 'model-unavailable' && deadModel) {
|
|
1445
|
+
return `model "${deadModel}" is unavailable — set a live model (mission.model, ATRIS_RUNNER_MODEL, or legacy ATRIS_CLAUDE_MODEL), then: atris mission run ${missionId}`;
|
|
1446
|
+
}
|
|
1447
|
+
if (typeof pauseReason === 'string' && pauseReason.startsWith('repeated-error:')) {
|
|
1448
|
+
const reason = pauseReason.slice('repeated-error:'.length);
|
|
1449
|
+
return `tick kept failing with "${reason}" — inspect the last receipt, fix the cause, then: atris mission run ${missionId}`;
|
|
1450
|
+
}
|
|
1451
|
+
// Single-tick cron runs pause via max-ticks-reached on the very first errored tick.
|
|
1452
|
+
// A bare "resume" there just re-errors; point the operator at the cause instead.
|
|
1453
|
+
if (pauseReason === 'max-ticks-reached' && lastErrorReason) {
|
|
1454
|
+
return `hit the tick budget while erroring ("${lastErrorReason}") — inspect the last receipt before resuming: atris mission run ${missionId}`;
|
|
1455
|
+
}
|
|
1456
|
+
return `resume with: atris mission run ${missionId}`;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1305
1459
|
function spawnClaudeTick(mission, opts) {
|
|
1306
|
-
const { sessionMode, sessionId, cwd, signal, timeoutMs, prompt } = opts;
|
|
1460
|
+
const { sessionMode, sessionId, cwd, signal, timeoutMs, prompt, model } = opts;
|
|
1307
1461
|
return new Promise((resolve) => {
|
|
1308
1462
|
const args = [
|
|
1309
1463
|
'-p', prompt,
|
|
@@ -1312,11 +1466,12 @@ function spawnClaudeTick(mission, opts) {
|
|
|
1312
1466
|
'--permission-mode', 'bypassPermissions',
|
|
1313
1467
|
'--include-partial-messages',
|
|
1314
1468
|
];
|
|
1469
|
+
if (model) args.push('--model', model);
|
|
1315
1470
|
if (sessionMode === 'set') args.push('--session-id', sessionId);
|
|
1316
1471
|
else if (sessionMode === 'resume') args.push('--resume', sessionId);
|
|
1317
1472
|
|
|
1318
1473
|
const startedAt = Date.now();
|
|
1319
|
-
const proc = spawn(
|
|
1474
|
+
const proc = spawn(resolveClaudeRunnerBin(), args, { cwd, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
1320
1475
|
|
|
1321
1476
|
let stdoutBuf = '';
|
|
1322
1477
|
let observedSessionIds = new Set();
|
|
@@ -1523,6 +1678,7 @@ async function runMission(args) {
|
|
|
1523
1678
|
sessionId = mission.claude_session_id || null;
|
|
1524
1679
|
pendingSessionId = mission.pending_session_id || null;
|
|
1525
1680
|
const callerSessionRunner = runnerUsesCallerSession(mission.runner);
|
|
1681
|
+
const atris2Runner = String(mission.runner || '').trim().toLowerCase() === 'atris2';
|
|
1526
1682
|
const skipWorker = skipClaude || callerSessionRunner;
|
|
1527
1683
|
|
|
1528
1684
|
// Freeze run-start contract (verifier, lane). Stored on receipts, not the mission record.
|
|
@@ -1539,7 +1695,8 @@ async function runMission(args) {
|
|
|
1539
1695
|
const effectiveMaxTicks = (cadenceSeconds === 0 && !maxTicksFlag) ? 1 : maxTicks;
|
|
1540
1696
|
|
|
1541
1697
|
// Session setup: only Claude-backed workers need a persisted session id.
|
|
1542
|
-
|
|
1698
|
+
// atris2 turns are stateless per tick — continuity lives on disk (logs, receipts, now.md).
|
|
1699
|
+
if (!skipWorker && !atris2Runner && !sessionId && !pendingSessionId) {
|
|
1543
1700
|
pendingSessionId = crypto.randomUUID();
|
|
1544
1701
|
mission = saveMission({ ...mission, pending_session_id: pendingSessionId }, cwd, 'mission_session_pending', { session_id: pendingSessionId }).mission;
|
|
1545
1702
|
}
|
|
@@ -1548,7 +1705,11 @@ async function runMission(args) {
|
|
|
1548
1705
|
let backoffAttempt = 0;
|
|
1549
1706
|
let lastRateLimit = null;
|
|
1550
1707
|
|
|
1551
|
-
const sessionLabel = skipWorker
|
|
1708
|
+
const sessionLabel = skipWorker
|
|
1709
|
+
? 'caller-session'
|
|
1710
|
+
: atris2Runner
|
|
1711
|
+
? `atris2 (${mission.model || 'atris:fast'})`
|
|
1712
|
+
: (sessionId || `pending=${pendingSessionId}`);
|
|
1552
1713
|
console.error(`[mission run] ${mission.id}\n objective: ${mission.objective}\n lane: ${frozen.lane}\n cadence: ${cadence} (${cadenceSeconds}s)\n max_ticks: ${effectiveMaxTicks}, max_wall: ${maxWallSeconds}s\n session: ${sessionLabel}`);
|
|
1553
1714
|
|
|
1554
1715
|
while (ticks.length < effectiveMaxTicks) {
|
|
@@ -1587,6 +1748,32 @@ async function runMission(args) {
|
|
|
1587
1748
|
ran: true,
|
|
1588
1749
|
claude: { skipped: true, reason: callerSessionRunner ? 'runner-uses-caller-session' : 'no-claude-mode' },
|
|
1589
1750
|
};
|
|
1751
|
+
} else if (atris2Runner) {
|
|
1752
|
+
const prompt = buildTickPrompt(mission, tickIdx, effectiveMaxTicks, frozen);
|
|
1753
|
+
const { runAtris2Turn } = require('./probe');
|
|
1754
|
+
const turn = await runAtris2Turn({
|
|
1755
|
+
prompt,
|
|
1756
|
+
model: mission.model || 'atris:fast',
|
|
1757
|
+
maxTurns: 16,
|
|
1758
|
+
signal: controller.signal,
|
|
1759
|
+
});
|
|
1760
|
+
result.atris2 = {
|
|
1761
|
+
ok: turn.ok,
|
|
1762
|
+
engine: turn.engine,
|
|
1763
|
+
model: mission.model || 'atris:fast',
|
|
1764
|
+
tools_run: turn.tools_run,
|
|
1765
|
+
unsupported: turn.unsupported,
|
|
1766
|
+
duration_ms: turn.duration_ms,
|
|
1767
|
+
error: turn.error,
|
|
1768
|
+
receipt_text: String(turn.text || '').slice(0, 4000),
|
|
1769
|
+
};
|
|
1770
|
+
if (controller.signal.aborted) { pauseReason = 'aborted-during-atris2'; break; }
|
|
1771
|
+
if (turn.error === 'not-logged-in') { pauseReason = 'auth-required'; break; }
|
|
1772
|
+
if (!turn.ok || !String(turn.text || '').trim()) {
|
|
1773
|
+
result = { ...result, status: 'errored', reason: 'atris2-error' };
|
|
1774
|
+
} else {
|
|
1775
|
+
result = { ...result, status: 'ran', reason: 'tick-ok', ran: true };
|
|
1776
|
+
}
|
|
1590
1777
|
} else {
|
|
1591
1778
|
const sessionMode = sessionId ? 'resume' : 'set';
|
|
1592
1779
|
const useId = sessionId || pendingSessionId;
|
|
@@ -1594,6 +1781,7 @@ async function runMission(args) {
|
|
|
1594
1781
|
const claudeResult = await spawnClaudeTick(mission, {
|
|
1595
1782
|
sessionMode, sessionId: useId, cwd, signal: controller.signal,
|
|
1596
1783
|
timeoutMs: MISSION_RUN_DEFAULTS.claudeTimeoutMs, prompt,
|
|
1784
|
+
model: resolveClaudeRunnerModel(mission),
|
|
1597
1785
|
});
|
|
1598
1786
|
result.claude = {
|
|
1599
1787
|
ok: claudeResult.ok,
|
|
@@ -1619,7 +1807,10 @@ async function runMission(args) {
|
|
|
1619
1807
|
if (claudeResult.authExpired) { pauseReason = 'auth-required'; break; }
|
|
1620
1808
|
|
|
1621
1809
|
if (!claudeResult.ok) {
|
|
1622
|
-
|
|
1810
|
+
const deadModel = detectUnavailableModel(claudeResult.summary || claudeResult.receipt_text);
|
|
1811
|
+
let reason = claudeResult.timedOut ? 'claude-timeout' : 'claude-error';
|
|
1812
|
+
if (deadModel) { reason = 'model-unavailable'; result.model_unavailable = deadModel; }
|
|
1813
|
+
result = { ...result, status: 'errored', reason };
|
|
1623
1814
|
} else {
|
|
1624
1815
|
// Promote pending session id ONLY if claude confirmed the exact UUID we requested.
|
|
1625
1816
|
// Mismatch is an invariant failure (we sent --session-id X, got Y) → pause, don't rotate.
|
|
@@ -1660,6 +1851,13 @@ async function runMission(args) {
|
|
|
1660
1851
|
}
|
|
1661
1852
|
const tickWorktree = worktreeReceipt(tickWorktreeBefore, gitWorktreeSnapshot(cwd), { verifier: frozen.verifier, baseline: runWorktreeBaseline });
|
|
1662
1853
|
|
|
1854
|
+
// Layer classification needs the receipt text AND the worktree receipt, so it
|
|
1855
|
+
// runs here — after both exist — covering the claude and atris2 branches alike.
|
|
1856
|
+
const tickReceiptText = result.atris2?.receipt_text || result.claude?.receipt_text || '';
|
|
1857
|
+
const layerInfo = extractLayerFromReceiptText(tickReceiptText, tickWorktree?.new_since_baseline_sample);
|
|
1858
|
+
result.layer = layerInfo.layer;
|
|
1859
|
+
result.layer_source = layerInfo.source;
|
|
1860
|
+
|
|
1663
1861
|
// Persist tick to mission state + write structured receipt
|
|
1664
1862
|
const finishedAt = stampIso();
|
|
1665
1863
|
const tickRecord = { ...result, started_at: tickStart, finished_at: finishedAt, worktree: tickWorktree };
|
|
@@ -1699,17 +1897,20 @@ async function runMission(args) {
|
|
|
1699
1897
|
last_tick_status: result.status,
|
|
1700
1898
|
last_tick_reason: result.reason,
|
|
1701
1899
|
last_tick_index: tickIdx,
|
|
1900
|
+
last_tick_layer: result.layer,
|
|
1901
|
+
last_tick_layer_source: result.layer_source,
|
|
1702
1902
|
verifier_result: verifierResult || mission.verifier_result || null,
|
|
1703
1903
|
receipt_path: receiptPath,
|
|
1704
1904
|
next_action: nextAction,
|
|
1705
1905
|
}, cwd, 'mission_tick', {
|
|
1706
|
-
tick_index: tickIdx, status: result.status, reason: result.reason, receipt_path: receiptPath,
|
|
1906
|
+
tick_index: tickIdx, status: result.status, reason: result.reason, receipt_path: receiptPath, layer: result.layer,
|
|
1707
1907
|
}).mission;
|
|
1708
1908
|
appendMemberLog(mission.owner, `Mission run tick ${tickIdx}`, {
|
|
1709
1909
|
mission: mission.objective,
|
|
1710
1910
|
state: mission.status,
|
|
1711
1911
|
tick_status: result.status,
|
|
1712
1912
|
reason: result.reason,
|
|
1913
|
+
layer: result.layer || undefined,
|
|
1713
1914
|
verifier: verifierResult ? (verifierResult.passed ? 'passed' : 'failed') : 'not_run',
|
|
1714
1915
|
receipt: receiptPath,
|
|
1715
1916
|
});
|
|
@@ -1726,6 +1927,16 @@ async function runMission(args) {
|
|
|
1726
1927
|
|
|
1727
1928
|
if (newStatus === 'complete' || (newStatus === 'ready' && !mission.always_on)) break;
|
|
1728
1929
|
if (consecutiveVerifierFails(ticks) >= 2) { pauseReason = 'consecutive-verifier-fails'; break; }
|
|
1930
|
+
// A retired/inaccessible model is deterministic: the id is fixed for the run, so
|
|
1931
|
+
// every remaining tick (and every future cron firing) fails identically. Backoff
|
|
1932
|
+
// only slows the bleeding. Stop on first detection and surface the dead id —
|
|
1933
|
+
// CLI-245 named this failure; this stops the loop from grinding on it forever.
|
|
1934
|
+
if (result.status === 'errored' && result.reason === 'model-unavailable') { pauseReason = 'model-unavailable'; break; }
|
|
1935
|
+
// Any OTHER error that recurs identically (claude-timeout, atris2-error, claude-error)
|
|
1936
|
+
// is the same trap one step less deterministic: keep retrying and the loop burns every
|
|
1937
|
+
// tick + cron firing on it. Halt at two-in-a-row and surface the reason for a human.
|
|
1938
|
+
const errStreak = consecutiveSameReasonErrors(ticks);
|
|
1939
|
+
if (errStreak.count >= 2) { pauseReason = `repeated-error:${errStreak.reason}`; break; }
|
|
1729
1940
|
|
|
1730
1941
|
// Sleep until next tick
|
|
1731
1942
|
let sleepMs = 0;
|
|
@@ -1752,13 +1963,16 @@ async function runMission(args) {
|
|
|
1752
1963
|
}
|
|
1753
1964
|
|
|
1754
1965
|
if (pauseReason && !['complete', 'ready', 'max-wall-reached'].includes(pauseReason)) {
|
|
1966
|
+
const lastTick = ticks[ticks.length - 1];
|
|
1967
|
+
const deadModel = pauseReason === 'model-unavailable' ? (lastTick && lastTick.model_unavailable) || null : null;
|
|
1968
|
+
const lastErrorReason = lastTick && lastTick.status === 'errored' ? lastTick.reason : null;
|
|
1755
1969
|
mission = saveMission({
|
|
1756
1970
|
...mission,
|
|
1757
1971
|
status: 'paused',
|
|
1758
1972
|
paused_at: stampIso(),
|
|
1759
1973
|
stop_reason: pauseReason,
|
|
1760
|
-
next_action:
|
|
1761
|
-
}, cwd, 'mission_run_paused', { reason: pauseReason }).mission;
|
|
1974
|
+
next_action: missionPauseNextAction(pauseReason, mission.id, deadModel, lastErrorReason),
|
|
1975
|
+
}, cwd, 'mission_run_paused', { reason: pauseReason, ...(deadModel ? { model_unavailable: deadModel } : {}) }).mission;
|
|
1762
1976
|
}
|
|
1763
1977
|
|
|
1764
1978
|
const summaryWorktree = worktreeReceipt(runWorktreeBefore, gitWorktreeSnapshot(cwd), { verifier: frozen.verifier, baseline: runWorktreeBaseline });
|
|
@@ -1844,6 +2058,9 @@ function tickMission(args) {
|
|
|
1844
2058
|
}
|
|
1845
2059
|
const tickWorktree = worktreeReceipt(tickWorktreeBefore, gitWorktreeSnapshot(cwd), { verifier: mission.verifier, baseline: worktreeBaseline });
|
|
1846
2060
|
|
|
2061
|
+
// Same layer classification as the run-tick path; manual ticks carry their
|
|
2062
|
+
// receipt text in --summary.
|
|
2063
|
+
const layerInfo = extractLayerFromReceiptText(summary || '', tickWorktree?.new_since_baseline_sample);
|
|
1847
2064
|
const tickRecord = {
|
|
1848
2065
|
status: 'ran',
|
|
1849
2066
|
reason: 'tick-recorded',
|
|
@@ -1852,6 +2069,8 @@ function tickMission(args) {
|
|
|
1852
2069
|
started_at: tickStart,
|
|
1853
2070
|
claude: { skipped: true, reason: 'orchestrator-is-caller-session' },
|
|
1854
2071
|
summary: summary || null,
|
|
2072
|
+
layer: layerInfo.layer,
|
|
2073
|
+
layer_source: layerInfo.source,
|
|
1855
2074
|
verifier_passed: verifierResult ? !!verifierResult.passed : null,
|
|
1856
2075
|
finished_at: stampIso(),
|
|
1857
2076
|
worktree: tickWorktree,
|
|
@@ -1880,24 +2099,31 @@ function tickMission(args) {
|
|
|
1880
2099
|
status = 'blocked';
|
|
1881
2100
|
nextAction = 'fix verifier failure or revise mission';
|
|
1882
2101
|
}
|
|
2102
|
+
const clearsPauseState = !['paused', 'stopped'].includes(status);
|
|
1883
2103
|
const nextMission = {
|
|
1884
2104
|
...mission,
|
|
1885
2105
|
status,
|
|
2106
|
+
paused_at: clearsPauseState ? null : mission.paused_at || null,
|
|
2107
|
+
stop_reason: clearsPauseState ? null : mission.stop_reason || null,
|
|
2108
|
+
resumed_at: clearsPauseState && mission.status === 'paused' ? tickRecord.finished_at : mission.resumed_at || null,
|
|
1886
2109
|
receipt_path: receiptPath,
|
|
1887
2110
|
last_tick_at: tickRecord.finished_at,
|
|
1888
2111
|
last_tick_status: tickRecord.status,
|
|
1889
2112
|
last_tick_reason: tickRecord.reason,
|
|
1890
2113
|
last_tick_index: tickIdx,
|
|
2114
|
+
last_tick_layer: tickRecord.layer,
|
|
2115
|
+
last_tick_layer_source: tickRecord.layer_source,
|
|
1891
2116
|
verifier_result: verifierResult || mission.verifier_result || null,
|
|
1892
2117
|
next_action: nextAction,
|
|
1893
2118
|
};
|
|
1894
2119
|
const { mission: saved } = saveMission(nextMission, cwd, 'mission_tick', {
|
|
1895
|
-
tick_index: tickIdx, verify, verifier_result: verifierResult, receipt_path: receiptPath,
|
|
2120
|
+
tick_index: tickIdx, verify, verifier_result: verifierResult, receipt_path: receiptPath, layer: tickRecord.layer,
|
|
1896
2121
|
});
|
|
1897
2122
|
const logPath = appendMemberLog(saved.owner, 'Mission tick', {
|
|
1898
2123
|
mission: saved.objective,
|
|
1899
2124
|
state: saved.status,
|
|
1900
2125
|
tick_index: tickIdx,
|
|
2126
|
+
layer: tickRecord.layer || undefined,
|
|
1901
2127
|
verifier: verifierResult ? (verifierResult.passed ? 'passed' : 'failed') : 'not_run',
|
|
1902
2128
|
receipt: receiptPath,
|
|
1903
2129
|
summary: summary || undefined,
|
|
@@ -2140,7 +2366,14 @@ function help() {
|
|
|
2140
2366
|
atris mission - durable goal + loop + owner + proof state
|
|
2141
2367
|
|
|
2142
2368
|
atris mission start "<objective>" --owner <member> [--verify "..."] [--always-on] [--xp-task] [--worktree]
|
|
2369
|
+
[--runner manual|claude|atris2|codex_goal] [--model <id>]
|
|
2370
|
+
(runner claude spawns local claude -p per tick, --model passes through;
|
|
2371
|
+
runner atris2 runs each tick as one /atris2/turn on the AtrisOS backend,
|
|
2372
|
+
default model atris:fast; runner codex_goal publishes the goal for a live
|
|
2373
|
+
Codex session to pull via atris mission goal)
|
|
2143
2374
|
atris mission status [id] [--status <state>] [--limit <n>] [--local] [--json]
|
|
2375
|
+
atris mission watch [id] [--interval <s>] [--idle-every <s>] Live heartbeat: prints a line per tick as it lands
|
|
2376
|
+
atris mission layers [--mission <id-substr>] [--since <date>] [--json] Per-layer growth curve across tick receipts
|
|
2144
2377
|
(rolls up sibling git-worktree missions; --local scopes to this checkout)
|
|
2145
2378
|
atris mission goal [--heartbeat] [--json]
|
|
2146
2379
|
atris mission goal-loop [--max-wall 28800] [--max-iterations 32] [--no-claude] [--json]
|
|
@@ -2189,6 +2422,153 @@ State:
|
|
|
2189
2422
|
`.trim());
|
|
2190
2423
|
}
|
|
2191
2424
|
|
|
2425
|
+
// Extract layer classification from receipt text.
|
|
2426
|
+
// Priority: layer tag on the last non-empty line (source: explicit) >
|
|
2427
|
+
// last strictly-matching layer line anywhere in the text (source: explicit-inline) >
|
|
2428
|
+
// changed-path classification (source: fallback). The single-token regex keeps the
|
|
2429
|
+
// enum docs line ("layer: identity|beliefs|...") from ever matching.
|
|
2430
|
+
function extractLayerFromReceiptText(text, fallbackPaths = []) {
|
|
2431
|
+
const text_str = String(text || '').trim();
|
|
2432
|
+
|
|
2433
|
+
if (text_str) {
|
|
2434
|
+
// Matches a standalone "layer: x" line, or a one-line summary ending in
|
|
2435
|
+
// "...; layer: x". Quoted bullets ("- layer: x") and the enum-doc line stay inert.
|
|
2436
|
+
const layerPattern = /^(?:.*;\s*)?layer:\s*(identity|beliefs|capabilities|behaviors|environment)\s*$/i;
|
|
2437
|
+
const lines = text_str.split(/\r?\n/).reverse();
|
|
2438
|
+
let isLastNonEmpty = true;
|
|
2439
|
+
for (const line of lines) {
|
|
2440
|
+
const trimmed = line.trim();
|
|
2441
|
+
if (!trimmed) continue;
|
|
2442
|
+
const match = trimmed.match(layerPattern);
|
|
2443
|
+
if (match) {
|
|
2444
|
+
return { layer: match[1].toLowerCase(), source: isLastNonEmpty ? 'explicit' : 'explicit-inline' };
|
|
2445
|
+
}
|
|
2446
|
+
isLastNonEmpty = false;
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
// Fallback to path classification
|
|
2451
|
+
if (Array.isArray(fallbackPaths) && fallbackPaths.length > 0) {
|
|
2452
|
+
const classified = classifyPathsByLayer(fallbackPaths);
|
|
2453
|
+
if (classified && classified.layer) {
|
|
2454
|
+
return { layer: classified.layer, source: classified.source };
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2458
|
+
return { layer: null, source: 'unknown' };
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
// Classify paths into layers. Returns { layer: string, source: string, confidence: number }
|
|
2462
|
+
// Rules: atris/team/ => identity, atris/lessons.md|atris/wiki/ => beliefs,
|
|
2463
|
+
// test/|skills/ => capabilities, commands/|bin/ => behaviors, else environment
|
|
2464
|
+
function classifyPathsByLayer(paths) {
|
|
2465
|
+
if (!Array.isArray(paths) || paths.length === 0) {
|
|
2466
|
+
return { layer: null, source: 'unknown' };
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
const counts = {
|
|
2470
|
+
identity: 0,
|
|
2471
|
+
beliefs: 0,
|
|
2472
|
+
capabilities: 0,
|
|
2473
|
+
behaviors: 0,
|
|
2474
|
+
environment: 0,
|
|
2475
|
+
};
|
|
2476
|
+
|
|
2477
|
+
for (const pathStr of paths) {
|
|
2478
|
+
const p = String(pathStr || '');
|
|
2479
|
+
if (p.includes('atris/team/')) {
|
|
2480
|
+
counts.identity++;
|
|
2481
|
+
} else if (p.includes('atris/lessons.md') || p.includes('atris/wiki/')) {
|
|
2482
|
+
counts.beliefs++;
|
|
2483
|
+
} else if (p.includes('test/') || p.includes('skills/')) {
|
|
2484
|
+
counts.capabilities++;
|
|
2485
|
+
} else if (p.includes('commands/') || p.includes('bin/')) {
|
|
2486
|
+
counts.behaviors++;
|
|
2487
|
+
} else {
|
|
2488
|
+
counts.environment++;
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
// Find max with tie-break: identity > beliefs > capabilities > behaviors > environment
|
|
2493
|
+
const tieBreakOrder = ['identity', 'beliefs', 'capabilities', 'behaviors', 'environment'];
|
|
2494
|
+
let maxCount = 0;
|
|
2495
|
+
let winnerLayer = null;
|
|
2496
|
+
for (const layer of tieBreakOrder) {
|
|
2497
|
+
if (counts[layer] > maxCount) {
|
|
2498
|
+
maxCount = counts[layer];
|
|
2499
|
+
winnerLayer = layer;
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
return winnerLayer ? { layer: winnerLayer, source: 'fallback' } : { layer: null, source: 'unknown' };
|
|
2504
|
+
}
|
|
2505
|
+
|
|
2506
|
+
// `atris mission layers` — per-layer growth curve across tick receipts. The member
|
|
2507
|
+
// proof standard says: if every tick is one layer and none touch the others, the
|
|
2508
|
+
// loop is doing work but not getting smarter. This makes that check one command.
|
|
2509
|
+
function layersMission(args) {
|
|
2510
|
+
const asJson = args.includes('--json');
|
|
2511
|
+
const missionFilter = readFlag(args, '--mission', '');
|
|
2512
|
+
const sinceRaw = readFlag(args, '--since', '');
|
|
2513
|
+
// --since accepts a date (2026-06-13) or full ISO; filters on the receipt's `at`
|
|
2514
|
+
// stamp so the curve can be read over a window (e.g. "what did today's ticks touch?").
|
|
2515
|
+
const sinceMs = sinceRaw ? Date.parse(sinceRaw) : null;
|
|
2516
|
+
if (sinceRaw && Number.isNaN(sinceMs)) {
|
|
2517
|
+
exitMissionError(`--since "${sinceRaw}" is not a parseable date (try 2026-06-13 or an ISO timestamp).`, 1, asJson);
|
|
2518
|
+
}
|
|
2519
|
+
const paths = statePaths(process.cwd());
|
|
2520
|
+
const LAYERS = ['identity', 'beliefs', 'capabilities', 'behaviors', 'environment'];
|
|
2521
|
+
const byLayer = Object.fromEntries(LAYERS.map((l) => [l, 0]));
|
|
2522
|
+
const bySource = { explicit: 0, 'explicit-inline': 0, fallback: 0, unknown: 0 };
|
|
2523
|
+
let total = 0;
|
|
2524
|
+
let untagged = 0;
|
|
2525
|
+
let files = [];
|
|
2526
|
+
try {
|
|
2527
|
+
files = fs.readdirSync(paths.runsDir).filter((f) => f.startsWith('mission-') && f.endsWith('.json'));
|
|
2528
|
+
} catch {
|
|
2529
|
+
files = [];
|
|
2530
|
+
}
|
|
2531
|
+
for (const file of files) {
|
|
2532
|
+
if (missionFilter && !file.includes(missionFilter)) continue;
|
|
2533
|
+
let receipt;
|
|
2534
|
+
try {
|
|
2535
|
+
receipt = JSON.parse(fs.readFileSync(path.join(paths.runsDir, file), 'utf8'));
|
|
2536
|
+
} catch {
|
|
2537
|
+
continue;
|
|
2538
|
+
}
|
|
2539
|
+
if (sinceMs != null) {
|
|
2540
|
+
const atMs = Date.parse(receipt && receipt.at);
|
|
2541
|
+
if (Number.isNaN(atMs) || atMs < sinceMs) continue;
|
|
2542
|
+
}
|
|
2543
|
+
const tick = receipt?.result?.tick;
|
|
2544
|
+
if (!tick) continue; // summaries, stop receipts, legacy shapes
|
|
2545
|
+
total++;
|
|
2546
|
+
const layer = String(tick.layer || '').toLowerCase();
|
|
2547
|
+
const source = String(tick.layer_source || 'unknown');
|
|
2548
|
+
if (LAYERS.includes(layer)) {
|
|
2549
|
+
byLayer[layer]++;
|
|
2550
|
+
bySource[source in bySource ? source : 'unknown']++;
|
|
2551
|
+
} else {
|
|
2552
|
+
untagged++;
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
const tagged = total - untagged;
|
|
2556
|
+
const dominant = LAYERS.reduce((a, b) => (byLayer[b] > byLayer[a] ? b : a), LAYERS[0]);
|
|
2557
|
+
const skewed = tagged >= 5 && byLayer[dominant] / tagged >= 0.8;
|
|
2558
|
+
const scopeBits = [
|
|
2559
|
+
missionFilter ? `mission filter: ${missionFilter}` : null,
|
|
2560
|
+
sinceRaw ? `since: ${sinceRaw}` : null,
|
|
2561
|
+
].filter(Boolean);
|
|
2562
|
+
const lines = [
|
|
2563
|
+
`Layer growth curve${scopeBits.length ? ` (${scopeBits.join(', ')})` : ''}: ${tagged} tagged / ${total} tick receipts`,
|
|
2564
|
+
...LAYERS.map((l) => ` ${l.padEnd(12)} ${String(byLayer[l]).padStart(3)}${byLayer[l] ? ' ' + '█'.repeat(Math.min(byLayer[l], 40)) : ''}`),
|
|
2565
|
+
...(untagged ? [` untagged ${String(untagged).padStart(3)} (pre-layer receipts or missing tag)`] : []),
|
|
2566
|
+
` provenance: explicit ${bySource.explicit}, explicit-inline ${bySource['explicit-inline']}, fallback ${bySource.fallback}`,
|
|
2567
|
+
...(skewed ? [` rebalance: ${Math.round((byLayer[dominant] / tagged) * 100)}% of tagged ticks are "${dominant}" — the proof standard wants the other layers moving too`] : []),
|
|
2568
|
+
];
|
|
2569
|
+
printJsonOrText({ ok: true, since: sinceRaw || null, total, tagged, untagged, by_layer: byLayer, by_source: bySource, dominant: tagged ? dominant : null, skewed }, lines, asJson);
|
|
2570
|
+
}
|
|
2571
|
+
|
|
2192
2572
|
function missionCommand(args) {
|
|
2193
2573
|
const subcommand = args[0] || 'status';
|
|
2194
2574
|
const rest = args.slice(1);
|
|
@@ -2201,6 +2581,10 @@ function missionCommand(args) {
|
|
|
2201
2581
|
case 'list':
|
|
2202
2582
|
case 'ls':
|
|
2203
2583
|
return statusMission(rest);
|
|
2584
|
+
case 'watch':
|
|
2585
|
+
return watchMission(rest);
|
|
2586
|
+
case 'layers':
|
|
2587
|
+
return layersMission(rest);
|
|
2204
2588
|
case 'goal':
|
|
2205
2589
|
case 'codex-goal':
|
|
2206
2590
|
return goalMission(rest);
|
|
@@ -2228,6 +2612,7 @@ function missionCommand(args) {
|
|
|
2228
2612
|
|
|
2229
2613
|
module.exports = {
|
|
2230
2614
|
missionCommand,
|
|
2615
|
+
missionHeartbeatLines,
|
|
2231
2616
|
listMissions,
|
|
2232
2617
|
loadMissionMap,
|
|
2233
2618
|
renderMissionStatus,
|
|
@@ -2235,4 +2620,11 @@ module.exports = {
|
|
|
2235
2620
|
selectCodexGoalMission,
|
|
2236
2621
|
usefulClaudeReceiptSummary,
|
|
2237
2622
|
cappedClaudeReceiptText,
|
|
2623
|
+
extractLayerFromReceiptText,
|
|
2624
|
+
classifyPathsByLayer,
|
|
2625
|
+
resolveClaudeRunnerModel,
|
|
2626
|
+
resolveClaudeRunnerBin,
|
|
2627
|
+
detectUnavailableModel,
|
|
2628
|
+
missionPauseNextAction,
|
|
2629
|
+
consecutiveSameReasonErrors,
|
|
2238
2630
|
};
|