atris 3.69.0 → 3.71.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 +40 -1
- package/atris/skills/README.md +1 -0
- package/atris/skills/campaigns/SKILL.md +81 -0
- package/atris/skills/meta/SKILL.md +52 -8
- package/ax +226 -42
- package/bin/atris.js +26 -6
- package/commands/autoland.js +176 -7
- package/commands/autopilot.js +71 -11
- package/commands/campaign.js +759 -0
- package/commands/compile.js +19 -8
- package/commands/doctor.js +58 -0
- package/commands/dream.js +18 -3
- package/commands/engine.js +147 -5
- package/commands/improve.js +119 -9
- package/commands/init.js +18 -1
- package/commands/interview.js +15 -3
- package/commands/land.js +22 -6
- package/commands/link.js +1 -0
- package/commands/member.js +433 -99
- package/commands/mission.js +332 -41
- package/commands/report.js +42 -8
- package/commands/run.js +9 -2
- package/commands/scout.js +9 -1
- package/commands/task.js +132 -16
- package/commands/team.js +212 -4
- package/commands/unknowns.js +15 -1
- package/commands/verify.js +46 -0
- package/commands/worktree.js +148 -18
- package/commands/write.js +12 -1
- package/commands/youtube.js +17 -2
- package/lib/accept-verify-gate.js +1 -0
- package/lib/auto-accept-certified.js +1 -0
- package/lib/autoland.js +13 -4
- package/lib/campaign-cadence.js +131 -0
- package/lib/campaign-core.js +330 -0
- package/lib/campaign-holdout.js +65 -0
- package/lib/campaign-loop.js +242 -0
- package/lib/campaign-missions.js +237 -0
- package/lib/campaign-new.js +251 -0
- package/lib/campaign-protect.js +75 -0
- package/lib/campaign-reading.js +147 -0
- package/lib/campaign-schema.js +196 -0
- package/lib/campaign-store.js +380 -0
- package/lib/campaign-sync.js +302 -0
- package/lib/campaign-trace.js +300 -0
- package/lib/conductor-artifacts.js +15 -1
- package/lib/daily-log.js +11 -1
- package/lib/dispatch-scout.js +18 -0
- package/lib/engine-ask.js +35 -4
- package/lib/engine-registry.js +2 -0
- package/lib/engine-resource.js +518 -0
- package/lib/engine-validate.js +189 -9
- package/lib/fleet.js +125 -16
- package/lib/init-commit.js +80 -0
- package/lib/intents.js +8 -0
- package/lib/known-commands.js +1 -1
- package/lib/land-green.js +58 -7
- package/lib/landing-proof.js +352 -0
- package/lib/member-autoland.js +479 -10
- package/lib/member-board.js +86 -10
- package/lib/member-map-fold.js +126 -0
- package/lib/member-map-inbox.js +80 -0
- package/lib/member-progress.js +48 -1
- package/lib/member-report.js +433 -0
- package/lib/member-scaffold.js +1 -2
- package/lib/member-self-next-job.js +97 -0
- package/lib/member-switches.js +7 -0
- package/lib/member-task-pick.js +41 -14
- package/lib/member-up.js +249 -16
- package/lib/roster-prep.js +25 -2
- package/lib/runner-command.js +5 -0
- package/lib/task-db.js +63 -11
- package/lib/task-explanation.js +7 -0
- package/lib/task-list-keeper.js +8 -1
- package/lib/task-receipt.js +6 -1
- package/mcp/atris-mcp/index.mjs +212 -10
- package/package.json +2 -1
- package/scripts/det/engine-reading.js +37 -0
- package/scripts/det/ytnotes +29 -5
package/commands/mission.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
5
6
|
const crypto = require('crypto');
|
|
6
7
|
const readline = require('readline');
|
|
7
8
|
const { spawn, spawnSync } = require('child_process');
|
|
@@ -115,6 +116,8 @@ const {
|
|
|
115
116
|
|
|
116
117
|
const VALID_STATUSES = new Set(['planning', 'running', 'ready', 'paused', 'blocked', 'stopped', 'complete']);
|
|
117
118
|
const TERMINAL_STATUSES = new Set(['stopped', 'complete']);
|
|
119
|
+
// Campaign hooks load on first use, so missions without a campaign never open its store.
|
|
120
|
+
const campaignMissions = () => require('../lib/campaign-missions');
|
|
118
121
|
const GOAL_LOOP_STATUSES = new Set(['planning', 'running', 'ready']);
|
|
119
122
|
const STATUS_ALIASES = new Set(['active']);
|
|
120
123
|
const CODEX_NATIVE_GOAL_SLOT_STATUSES = new Set(['active', 'paused', 'usage_limited']);
|
|
@@ -425,16 +428,41 @@ async function missionTickPrep(root, { mission, runtimeMission, job, prompt, ask
|
|
|
425
428
|
|
|
426
429
|
// A tick's time limit: the time left on the mission, capped by the roster
|
|
427
430
|
// line's "max 20 min" when it sets one, else by the default tick cap.
|
|
428
|
-
function
|
|
429
|
-
|
|
431
|
+
function missionTickCapMs(mission) {
|
|
432
|
+
return Number(mission && mission.roster_max_seconds) > 0
|
|
430
433
|
? Number(mission.roster_max_seconds) * 1000
|
|
431
434
|
: MISSION_RUN_DEFAULTS.claudeTimeoutMs;
|
|
432
|
-
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function missionTickTimeoutMs(mission, remainingMs) {
|
|
438
|
+
return clampTimeoutMs(remainingMs, missionTickCapMs(mission));
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// True when the time left on the mission, not the tick's own cap, set the
|
|
442
|
+
// tick's limit, so a kill there means the mission clock ran out.
|
|
443
|
+
function missionTickWallBound(mission, remainingMs) {
|
|
444
|
+
return remainingMs <= missionTickCapMs(mission);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// The reason a member log shows for a tick. A failed engine tick names its
|
|
448
|
+
// engine beside the reason, so a codex run that timed out stops counting as
|
|
449
|
+
// a claude timeout in the repeat-problem scan. The reason itself is left as
|
|
450
|
+
// is because engine health and backoff match on it.
|
|
451
|
+
function memberLogTickReason(result) {
|
|
452
|
+
const reason = result && result.reason;
|
|
453
|
+
if (!reason || !result.engine_id || result.status !== 'errored') return reason;
|
|
454
|
+
return `${reason} (engine: ${result.engine_id})`;
|
|
433
455
|
}
|
|
434
456
|
|
|
435
457
|
function resolveMissionTickRunner(mission, root = process.cwd(), options = {}) {
|
|
436
458
|
if (String(mission && mission.runner || '').trim().toLowerCase() !== MISSION_AUTO_RUNNER) {
|
|
437
|
-
|
|
459
|
+
// A named engine still gets the roster's time limit for that engine.
|
|
460
|
+
const runner = String(mission && mission.runner || '').trim().toLowerCase();
|
|
461
|
+
const pin = runner && !(Number(mission && mission.roster_max_seconds) > 0)
|
|
462
|
+
? require('../lib/engine-registry').rosterPinFor('executor', runner, root, options)
|
|
463
|
+
: {};
|
|
464
|
+
const limited = pin.roster_max_seconds ? { ...mission, roster_max_seconds: pin.roster_max_seconds } : mission;
|
|
465
|
+
return { mission: limited, engine_id: null, requested_engine: null, engine_fallback_reason: null };
|
|
438
466
|
}
|
|
439
467
|
// A mission owned by a team member runs on that member's roster pick. An
|
|
440
468
|
// explicit preferred engine (or a run's --engine) still wins, and with no
|
|
@@ -736,6 +764,8 @@ const MISSION_RUN_VALUE_FLAGS = [
|
|
|
736
764
|
'--engine',
|
|
737
765
|
'--referee-engine',
|
|
738
766
|
'--budget',
|
|
767
|
+
'--campaign',
|
|
768
|
+
'--reserve',
|
|
739
769
|
'--repo',
|
|
740
770
|
'--native-goal-status',
|
|
741
771
|
'--native-goal-objective',
|
|
@@ -1472,14 +1502,24 @@ function nearestMissionCandidates(missions, ref, limit = 5) {
|
|
|
1472
1502
|
.map((entry) => entry.mission);
|
|
1473
1503
|
}
|
|
1474
1504
|
|
|
1475
|
-
|
|
1505
|
+
// Sibling folders looked at for a hint. A missing-id error waited 26 seconds
|
|
1506
|
+
// on a temp folder holding 276,000 entries, so the scan is capped and never
|
|
1507
|
+
// walks the system temp folder.
|
|
1508
|
+
const SIBLING_HINT_SCAN_LIMIT = 2000;
|
|
1509
|
+
|
|
1510
|
+
function listSiblingWorkspaceMissionHints(ref, root = process.cwd(), limit = 5, { scanLimit = SIBLING_HINT_SCAN_LIMIT, tmpRoot = os.tmpdir() } = {}) {
|
|
1476
1511
|
if (!ref) return [];
|
|
1477
1512
|
const baseRoot = path.resolve(root);
|
|
1478
1513
|
const parent = path.dirname(baseRoot);
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1514
|
+
const real = (target) => {
|
|
1515
|
+
try {
|
|
1516
|
+
return fs.realpathSync(target);
|
|
1517
|
+
} catch {
|
|
1518
|
+
return target;
|
|
1519
|
+
}
|
|
1520
|
+
};
|
|
1521
|
+
const here = real(baseRoot);
|
|
1522
|
+
if (real(parent) === real(tmpRoot)) return [];
|
|
1483
1523
|
let entries = [];
|
|
1484
1524
|
try {
|
|
1485
1525
|
entries = fs.readdirSync(parent, { withFileTypes: true });
|
|
@@ -1487,17 +1527,11 @@ function listSiblingWorkspaceMissionHints(ref, root = process.cwd(), limit = 5)
|
|
|
1487
1527
|
return [];
|
|
1488
1528
|
}
|
|
1489
1529
|
const hints = [];
|
|
1490
|
-
for (const entry of entries) {
|
|
1530
|
+
for (const entry of entries.slice(0, scanLimit)) {
|
|
1491
1531
|
if (!entry.isDirectory() || entry.name.startsWith('.')) continue;
|
|
1492
1532
|
const candidateRoot = path.join(parent, entry.name);
|
|
1493
|
-
let candidateReal = candidateRoot;
|
|
1494
|
-
try {
|
|
1495
|
-
candidateReal = fs.realpathSync(candidateRoot);
|
|
1496
|
-
} catch {
|
|
1497
|
-
continue;
|
|
1498
|
-
}
|
|
1499
|
-
if (candidateReal === here) continue;
|
|
1500
1533
|
if (!fs.existsSync(path.join(candidateRoot, '.atris', 'state', 'missions.jsonl'))) continue;
|
|
1534
|
+
if (real(candidateRoot) === here) continue;
|
|
1501
1535
|
const mission = listMissions(candidateRoot).find((row) => missionMatchesRef(row, ref));
|
|
1502
1536
|
if (!mission) continue;
|
|
1503
1537
|
hints.push({
|
|
@@ -1690,6 +1724,7 @@ function saveMission(mission, root = process.cwd(), eventType = 'mission_updated
|
|
|
1690
1724
|
// this file several times, so unbounded snapshot history taxes everything.
|
|
1691
1725
|
try { compactMissionLedger(paths.missionsJsonl); } catch {}
|
|
1692
1726
|
const event = appendEvent(eventType, next, payload, root);
|
|
1727
|
+
if (next.campaign_id) campaignMissions().recordCampaignMissionEnded(root, existing, next, TERMINAL_STATUSES);
|
|
1693
1728
|
renderMissionStatus(root);
|
|
1694
1729
|
renderMemberMissionState(next.owner, root);
|
|
1695
1730
|
return { mission: next, event };
|
|
@@ -1723,7 +1758,18 @@ function appendMemberLog(owner, title, fields = {}, root = process.cwd()) {
|
|
|
1723
1758
|
.map(([key, value]) => `- ${key}: ${String(value).replace(/\n/g, ' ')}`),
|
|
1724
1759
|
'',
|
|
1725
1760
|
];
|
|
1726
|
-
|
|
1761
|
+
let lead = '';
|
|
1762
|
+
try {
|
|
1763
|
+
const size = fs.statSync(logPath).size;
|
|
1764
|
+
if (size > 0) {
|
|
1765
|
+
const fd = fs.openSync(logPath, 'r');
|
|
1766
|
+
const last = Buffer.alloc(1);
|
|
1767
|
+
fs.readSync(fd, last, 0, 1, size - 1);
|
|
1768
|
+
fs.closeSync(fd);
|
|
1769
|
+
if (last[0] !== 0x0a) lead = '\n';
|
|
1770
|
+
}
|
|
1771
|
+
} catch {}
|
|
1772
|
+
fs.appendFileSync(logPath, lead + rows.join('\n'), 'utf8');
|
|
1727
1773
|
return logPath;
|
|
1728
1774
|
}
|
|
1729
1775
|
|
|
@@ -3152,6 +3198,9 @@ function missionFromArgs(args) {
|
|
|
3152
3198
|
'--base',
|
|
3153
3199
|
'--task',
|
|
3154
3200
|
'--ask',
|
|
3201
|
+
'--campaign',
|
|
3202
|
+
'--reserve',
|
|
3203
|
+
'--mission-id',
|
|
3155
3204
|
'--model',
|
|
3156
3205
|
'--destination',
|
|
3157
3206
|
'--native-goal-status',
|
|
@@ -3168,6 +3217,7 @@ function missionFromArgs(args) {
|
|
|
3168
3217
|
const budgetContract = inferRunObjectiveBudgetContract(objective, args);
|
|
3169
3218
|
const maxTicksOverride = readPositiveIntegerFlag(args, '--max-ticks', null, { json: wantsJson(args) });
|
|
3170
3219
|
const requestedOwner = readFlag(args, '--owner', process.env.ATRIS_AGENT_ID || 'mission-lead');
|
|
3220
|
+
const campaignId = readFlag(args, '--campaign', '');
|
|
3171
3221
|
const cadence = readFlag(args, '--cadence', readFlag(args, '--loop', 'manual')) || 'manual';
|
|
3172
3222
|
const engineFlag = readFlag(args, '--engine', '');
|
|
3173
3223
|
const runnerSelection = engineFlag
|
|
@@ -3195,7 +3245,13 @@ function missionFromArgs(args) {
|
|
|
3195
3245
|
const alwaysOn = hasFlag(args, '--always-on');
|
|
3196
3246
|
const xpTaskEnabled = hasFlag(args, '--xp-task') || hasFlag(args, '--agent-xp');
|
|
3197
3247
|
const businessBinding = readBusinessBinding(process.cwd());
|
|
3198
|
-
|
|
3248
|
+
// A campaign proposes a mission under an id before it runs; --mission-id
|
|
3249
|
+
// keeps that id so the campaign and the mission name the same thing.
|
|
3250
|
+
const givenId = readFlag(args, '--mission-id', '');
|
|
3251
|
+
if (givenId && !/^[a-z0-9][a-z0-9-]{2,120}$/.test(givenId)) {
|
|
3252
|
+
exitMissionError('--mission-id takes lowercase letters, digits, and dashes', 2, wantsJson(args));
|
|
3253
|
+
}
|
|
3254
|
+
const id = givenId || missionId(objective);
|
|
3199
3255
|
const mission = {
|
|
3200
3256
|
schema: 'atris.mission.v1',
|
|
3201
3257
|
id,
|
|
@@ -3221,6 +3277,7 @@ function missionFromArgs(args) {
|
|
|
3221
3277
|
stop_condition: stopCondition,
|
|
3222
3278
|
task_ids: taskIds,
|
|
3223
3279
|
human_asks: humanAsks,
|
|
3280
|
+
...(campaignId ? { campaign_id: campaignId } : {}),
|
|
3224
3281
|
...(drive ? { drive } : {}),
|
|
3225
3282
|
next_action: verifier ? 'run verifier with `atris mission tick <id> --verify`' : 'define verifier or run next task',
|
|
3226
3283
|
receipt_path: null,
|
|
@@ -3910,6 +3967,30 @@ function readTasteReviewHistory(root, limit = 4) {
|
|
|
3910
3967
|
return { accepted, revised };
|
|
3911
3968
|
}
|
|
3912
3969
|
|
|
3970
|
+
// Why the mission's own task was last sent back, or '' when it was not. Ticks
|
|
3971
|
+
// run in a worktree, so fall back to the main checkout's task projection.
|
|
3972
|
+
function readBoundTaskTurnDown(mission, root) {
|
|
3973
|
+
const bound = mission?.xp_task || mission?.selected_target_task || null;
|
|
3974
|
+
if (!bound) return '';
|
|
3975
|
+
const keys = new Set([bound.task_id, bound.id, bound.ref, bound.task_ref].filter(Boolean).map(String));
|
|
3976
|
+
if (!keys.size) return '';
|
|
3977
|
+
const roots = [root];
|
|
3978
|
+
const common = spawnSync('git', ['rev-parse', '--path-format=absolute', '--git-common-dir'], { cwd: root, encoding: 'utf8' });
|
|
3979
|
+
if (common.status === 0 && String(common.stdout || '').trim()) roots.push(path.dirname(String(common.stdout).trim()));
|
|
3980
|
+
for (const dir of roots) {
|
|
3981
|
+
const projection = readJson(path.join(dir, '.atris', 'state', 'tasks.projection.json'));
|
|
3982
|
+
const tasks = Array.isArray(projection?.tasks) ? projection.tasks : [];
|
|
3983
|
+
const task = tasks.find((row) => row && [row.id, row.display_id, row.legacy_ref].some((key) => key && keys.has(String(key))));
|
|
3984
|
+
if (!task) continue;
|
|
3985
|
+
const metadata = task.metadata && typeof task.metadata === 'object' ? task.metadata : {};
|
|
3986
|
+
if (metadata.approval_status && metadata.approval_status !== 'revise') return '';
|
|
3987
|
+
const events = (Array.isArray(task.events) ? task.events : []).filter((event) => event?.event_type === 'revision_requested');
|
|
3988
|
+
const note = metadata.human_revision_note || events[events.length - 1]?.payload?.note || '';
|
|
3989
|
+
return missionTasteSnippet(String(note), 600);
|
|
3990
|
+
}
|
|
3991
|
+
return '';
|
|
3992
|
+
}
|
|
3993
|
+
|
|
3913
3994
|
function missionTasteMemorySignals(text) {
|
|
3914
3995
|
const definitions = [
|
|
3915
3996
|
['plain_language', 'the operator asks for plain English', /\b(plain|jargon|feynman|understand|simple|clarify|clarity)\b/i],
|
|
@@ -4310,6 +4391,7 @@ function seedMissionRunContinuation(parent, root = process.cwd(), proof = '') {
|
|
|
4310
4391
|
...mission,
|
|
4311
4392
|
started_from: 'mission_run_continuation',
|
|
4312
4393
|
parent_mission_id: parent.id,
|
|
4394
|
+
...(parent.campaign_id && campaignMissions().checkCampaignStart(root, parent.campaign_id, { reserveUsd: 0 }).ok ? { campaign_id: parent.campaign_id } : {}),
|
|
4313
4395
|
parent_objective: parent.objective,
|
|
4314
4396
|
continue_on_complete: false,
|
|
4315
4397
|
continuation_policy: 'choose_next_mission',
|
|
@@ -4326,6 +4408,7 @@ function seedMissionRunContinuation(parent, root = process.cwd(), proof = '') {
|
|
|
4326
4408
|
parent_objective: parent.objective,
|
|
4327
4409
|
proof: proof || null,
|
|
4328
4410
|
});
|
|
4411
|
+
if (saved.campaign_id) campaignMissions().recordCampaignMissionStarted(root, saved, 0);
|
|
4329
4412
|
const worktreeBaseline = captureMissionWorktreeBaseline(saved, root);
|
|
4330
4413
|
const seededAt = stampIso();
|
|
4331
4414
|
const { mission: updatedParent } = saveMission({
|
|
@@ -4411,7 +4494,7 @@ function startMission(args, options = {}) {
|
|
|
4411
4494
|
const asJson = wantsJson(args);
|
|
4412
4495
|
const firstArg = String(args[0] || '').trim().toLowerCase();
|
|
4413
4496
|
if (args.length === 1 && (firstArg === '--help' || firstArg === '-h' || firstArg === 'help')) {
|
|
4414
|
-
console.log('Usage: atris mission start "<objective>" --owner <member> --verify "..." [--destination "..."] [--no-verify] [--always-on] [--budget quick|long|deep] [--runner manual|claude|atris2|codex_goal|auto]');
|
|
4497
|
+
console.log('Usage: atris mission start "<objective>" --owner <member> --verify "..." [--destination "..."] [--no-verify] [--always-on] [--budget quick|long|deep] [--runner manual|claude|atris2|codex_goal|auto] [--campaign <id> [--reserve <dollars>]]');
|
|
4415
4498
|
console.log('Run `atris mission --help` for the full option list.');
|
|
4416
4499
|
process.exit(0);
|
|
4417
4500
|
}
|
|
@@ -4498,6 +4581,18 @@ function startMission(args, options = {}) {
|
|
|
4498
4581
|
asJson,
|
|
4499
4582
|
);
|
|
4500
4583
|
}
|
|
4584
|
+
// --campaign: the campaign must be open and able to hold this mission's
|
|
4585
|
+
// reserve. Checked before any state exists, so a refusal leaves nothing behind.
|
|
4586
|
+
let campaignReserve = 0;
|
|
4587
|
+
if (mission.campaign_id) {
|
|
4588
|
+
const check = campaignMissions().checkCampaignStart(process.cwd(), mission.campaign_id, { missionId: mission.id, reserveUsd: readFlag(args, '--reserve', '') });
|
|
4589
|
+
if (!check.ok) exitMissionError(`mission start refused: ${check.error}`, 2, asJson);
|
|
4590
|
+
try {
|
|
4591
|
+
campaignMissions().reserveCampaignMission(process.cwd(), mission.campaign_id, mission.id, check.reserve, mission.owner || 'mission-lead');
|
|
4592
|
+
} catch (error) {
|
|
4593
|
+
exitMissionError(`mission start refused: ${error.message}`, 2, asJson);
|
|
4594
|
+
}
|
|
4595
|
+
}
|
|
4501
4596
|
// --worktree: bind the mission to its own isolated checkout. We chdir before
|
|
4502
4597
|
// any state writes so the mission record, baseline sidecar, receipts, and
|
|
4503
4598
|
// member files all land inside the worktree, ticks run there, and the main
|
|
@@ -4589,6 +4684,7 @@ function startMission(args, options = {}) {
|
|
|
4589
4684
|
const warnings = [missingVerifierWarning(mission), missingOwnerMemberWarning(mission.owner)].filter(Boolean);
|
|
4590
4685
|
ensureMemberMissionFile(mission.owner, process.cwd(), mission.objective);
|
|
4591
4686
|
const { mission: saved } = saveMission(assignMissionNumber(mission, process.cwd()), process.cwd(), 'mission_started', { objective: mission.objective });
|
|
4687
|
+
if (saved.campaign_id) campaignMissions().recordCampaignMissionStarted(process.cwd(), saved, campaignReserve);
|
|
4592
4688
|
const goalSlotHandoff = hasFlag(args, '--take-goal-slot') && isCodexGoalMission(saved)
|
|
4593
4689
|
? takeCodexGoalSlotForMission(saved, process.cwd())
|
|
4594
4690
|
: null;
|
|
@@ -4709,6 +4805,8 @@ async function startMissionFromRunObjective(objective, args) {
|
|
|
4709
4805
|
];
|
|
4710
4806
|
if (verifier) startArgs.push('--verify', verifier);
|
|
4711
4807
|
if (budgetTier) startArgs.push('--budget', budgetTier.name);
|
|
4808
|
+
const campaignFlag = readFlag(args, '--campaign', '');
|
|
4809
|
+
if (campaignFlag) startArgs.push('--campaign', campaignFlag);
|
|
4712
4810
|
if (maxTicksOverride) startArgs.push('--max-ticks', String(maxTicksOverride));
|
|
4713
4811
|
const model = readFlag(args, '--model', '');
|
|
4714
4812
|
if (model) startArgs.push('--model', model);
|
|
@@ -4716,6 +4814,16 @@ async function startMissionFromRunObjective(objective, args) {
|
|
|
4716
4814
|
if (hasFlag(args, '--xp-task') || hasFlag(args, '--agent-xp') || missionRunPreflight?.task_spine_required) startArgs.push('--xp-task');
|
|
4717
4815
|
|
|
4718
4816
|
const mission = markMissionRunContinuation(missionFromArgs(startArgs));
|
|
4817
|
+
let campaignReserve = 0;
|
|
4818
|
+
if (mission.campaign_id) {
|
|
4819
|
+
const check = campaignMissions().checkCampaignStart(process.cwd(), mission.campaign_id, { missionId: mission.id, reserveUsd: readFlag(args, '--reserve', '') });
|
|
4820
|
+
if (!check.ok) exitMissionError(`mission start refused: ${check.error}`, 2, wantsJson(args));
|
|
4821
|
+
try {
|
|
4822
|
+
campaignMissions().reserveCampaignMission(process.cwd(), mission.campaign_id, mission.id, check.reserve, mission.owner || 'mission-lead');
|
|
4823
|
+
} catch (error) {
|
|
4824
|
+
exitMissionError(`mission start refused: ${error.message}`, 2, wantsJson(args));
|
|
4825
|
+
}
|
|
4826
|
+
}
|
|
4719
4827
|
if (landOwnerResolution) {
|
|
4720
4828
|
mission.owner = runOwner;
|
|
4721
4829
|
mission.owner_resolution = landOwnerResolution.reason || mission.owner_resolution;
|
|
@@ -4799,6 +4907,7 @@ async function startMissionFromRunObjective(objective, args) {
|
|
|
4799
4907
|
const warnings = [missingVerifierWarning(mission), missingOwnerMemberWarning(mission.owner)].filter(Boolean);
|
|
4800
4908
|
ensureMemberMissionFile(mission.owner, process.cwd(), mission.objective);
|
|
4801
4909
|
const { mission: saved } = saveMission(assignMissionNumber(mission, process.cwd()), process.cwd(), 'mission_started', { objective: mission.objective, source: 'mission_run_objective' });
|
|
4910
|
+
if (saved.campaign_id) campaignMissions().recordCampaignMissionStarted(process.cwd(), saved, campaignReserve);
|
|
4802
4911
|
if (landRun) {
|
|
4803
4912
|
const memberState = renderMemberMissionState(saved.owner);
|
|
4804
4913
|
const logPath = appendMemberLog(saved.owner, 'Mission landing flight dispatched', {
|
|
@@ -6556,7 +6665,7 @@ function runVerifier(command, root = process.cwd()) {
|
|
|
6556
6665
|
const resolvedCommand = resolveVerifierCommand(command);
|
|
6557
6666
|
const { envWithNodeDir } = require('../lib/spawn-env');
|
|
6558
6667
|
const timeoutMs = missionVerifierTimeoutMs();
|
|
6559
|
-
const startedAt =
|
|
6668
|
+
const startedAt = process.hrtime.bigint();
|
|
6560
6669
|
const result = spawnSync(resolvedCommand, {
|
|
6561
6670
|
cwd: root,
|
|
6562
6671
|
shell: true,
|
|
@@ -6564,11 +6673,15 @@ function runVerifier(command, root = process.cwd()) {
|
|
|
6564
6673
|
timeout: timeoutMs,
|
|
6565
6674
|
env: envWithNodeDir(process.env),
|
|
6566
6675
|
});
|
|
6567
|
-
const elapsedMs = Date.now() - startedAt;
|
|
6568
6676
|
// A verifier that blows its window is not a failing check: the code may be
|
|
6569
6677
|
// fine and the suite merely slow (or starved by a concurrent flight). Report
|
|
6570
6678
|
// it as a distinct timed-out state so operators do not read slow as broken.
|
|
6571
6679
|
const timedOut = result.error ? result.error.code === 'ETIMEDOUT' : false;
|
|
6680
|
+
// Steady clock, not wall time: the wall clock can slew, and a kill at the
|
|
6681
|
+
// window once read as 927ms of a 1000ms window. A timed-out run used the
|
|
6682
|
+
// whole window by definition.
|
|
6683
|
+
const measuredMs = Number((process.hrtime.bigint() - startedAt) / 1000000n);
|
|
6684
|
+
const elapsedMs = timedOut ? Math.max(measuredMs, timeoutMs) : measuredMs;
|
|
6572
6685
|
return {
|
|
6573
6686
|
command,
|
|
6574
6687
|
resolved_command: resolvedCommand === command ? null : resolvedCommand,
|
|
@@ -6588,17 +6701,29 @@ const CHECK_FEEDBACK_TRIM_MARKER = 'output trimmed';
|
|
|
6588
6701
|
|
|
6589
6702
|
function extractCheckFeedback(verifierResult) {
|
|
6590
6703
|
if (!verifierResult || verifierResult.passed !== false) return '';
|
|
6591
|
-
|
|
6704
|
+
// A reviewer turn-down goes first: when the tests passed, it is the only
|
|
6705
|
+
// real reason the try failed, and passing test names must not crowd it out.
|
|
6706
|
+
const referee = verifierResult.referee && verifierResult.referee.passed === false
|
|
6707
|
+
? verifierResult.referee
|
|
6708
|
+
: null;
|
|
6709
|
+
const refereeLines = referee
|
|
6710
|
+
? [referee.reason, referee.output].filter((value) => String(value || '').trim())
|
|
6711
|
+
: [];
|
|
6712
|
+
const lines = [...new Set([...refereeLines, verifierResult.output, verifierResult.stdout, verifierResult.stderr]
|
|
6592
6713
|
.filter((value) => String(value || '').trim())
|
|
6593
6714
|
.flatMap((value) => String(value).split(/\r?\n/))
|
|
6594
6715
|
.map((line) => line.trim())
|
|
6595
|
-
.filter(Boolean);
|
|
6716
|
+
.filter(Boolean))];
|
|
6596
6717
|
if (!lines.length) return '';
|
|
6597
6718
|
|
|
6719
|
+
const refereeSet = new Set(refereeLines.flatMap((value) => String(value).split(/\r?\n/)).map((line) => line.trim()).filter(Boolean));
|
|
6720
|
+
const passingLine = /^(?:✔|ℹ )/;
|
|
6598
6721
|
const usefulPattern = /\b(?:error|fail|expected|assert)\w*\b|(?:^|\s|\()[^:\s()]+:\d+(?::\d+)?(?:\b|$)/i;
|
|
6599
|
-
const
|
|
6600
|
-
const
|
|
6601
|
-
const
|
|
6722
|
+
const rest = lines.filter((line) => !refereeSet.has(line));
|
|
6723
|
+
const useful = rest.filter((line) => !passingLine.test(line) && usefulPattern.test(line));
|
|
6724
|
+
const context = rest.filter((line) => !passingLine.test(line) && !usefulPattern.test(line));
|
|
6725
|
+
const passing = rest.filter((line) => passingLine.test(line));
|
|
6726
|
+
const feedback = [...lines.filter((line) => refereeSet.has(line)), ...useful, ...context, ...passing].join('\n');
|
|
6602
6727
|
if (feedback.length <= CHECK_FEEDBACK_LIMIT) return feedback;
|
|
6603
6728
|
|
|
6604
6729
|
const contentLimit = CHECK_FEEDBACK_LIMIT - CHECK_FEEDBACK_TRIM_MARKER.length - 1;
|
|
@@ -6837,7 +6962,10 @@ function worktreeReceipt(before, after, { verifier = '', baseline = null } = {})
|
|
|
6837
6962
|
const MISSION_RUN_DEFAULTS = {
|
|
6838
6963
|
maxTicks: 4,
|
|
6839
6964
|
maxWallSeconds: 3600,
|
|
6840
|
-
|
|
6965
|
+
// Sized from real runs: claude ticks that landed 10/08-10/14 ran a median
|
|
6966
|
+
// 4 min, but 15 of 92 needed more than 10 and the slowest 5% took 17+.
|
|
6967
|
+
// A 10 min cap killed 12 runs at exactly 10:00; 20 min fits 89 of 92.
|
|
6968
|
+
claudeTimeoutMs: 20 * 60 * 1000,
|
|
6841
6969
|
backoff: { initialMs: 30_000, maxMs: 10 * 60_000, factor: 2, jitter: 0.3 },
|
|
6842
6970
|
};
|
|
6843
6971
|
|
|
@@ -8824,6 +8952,45 @@ function consumeMissionPings(mission, cwd) {
|
|
|
8824
8952
|
return { mission: saved, pings: pending };
|
|
8825
8953
|
}
|
|
8826
8954
|
|
|
8955
|
+
// A one-tick job has no next prompt. Deliver notes that arrived while the
|
|
8956
|
+
// worker ran before checking its work, within the same wall budget. Only a
|
|
8957
|
+
// successful worker response acknowledges a batch; failed delivery can retry.
|
|
8958
|
+
async function deliverLateMissionPings(mission, cwd, firstResult, { run, remainingMs, prompt }) {
|
|
8959
|
+
let result = firstResult;
|
|
8960
|
+
let delivered = 0;
|
|
8961
|
+
while (result.ok) {
|
|
8962
|
+
const latest = resolveMission(mission.id, cwd) || mission;
|
|
8963
|
+
const pending = pendingMissionPings(latest);
|
|
8964
|
+
if (!pending.length || TERMINAL_STATUSES.has(latest.status)) break;
|
|
8965
|
+
if (remainingMs() <= 0) {
|
|
8966
|
+
return { ...result, ok: false, timedOut: true, error: 'unread notes at the wall limit' };
|
|
8967
|
+
}
|
|
8968
|
+
const next = await run([
|
|
8969
|
+
prompt,
|
|
8970
|
+
'',
|
|
8971
|
+
'## Notes received before this job finished',
|
|
8972
|
+
'This is the same work step. Read and apply these notes before returning the final receipt. Do not start a second slice.',
|
|
8973
|
+
...pending.map((p) => `- [${p.at}] ${p.from || 'operator'}: ${p.text}`),
|
|
8974
|
+
'',
|
|
8975
|
+
'Previous work receipt:',
|
|
8976
|
+
result.receipt_text || result.result || '',
|
|
8977
|
+
].join('\n'));
|
|
8978
|
+
if (!next.ok) return next;
|
|
8979
|
+
// Re-read so notes sent during delivery survive the acknowledgement.
|
|
8980
|
+
const fresh = resolveMission(mission.id, cwd) || latest;
|
|
8981
|
+
const keys = new Set(pending.map((p) => JSON.stringify([p.at, p.from, p.text])));
|
|
8982
|
+
const consumedAt = stampIso();
|
|
8983
|
+
saveMission({
|
|
8984
|
+
...fresh,
|
|
8985
|
+
pings: (fresh.pings || []).map((p) => p && !p.consumed_at && keys.has(JSON.stringify([p.at, p.from, p.text]))
|
|
8986
|
+
? { ...p, consumed_at: consumedAt } : p),
|
|
8987
|
+
}, cwd, 'mission_pings_consumed', { count: pending.length });
|
|
8988
|
+
delivered += pending.length;
|
|
8989
|
+
result = { ...next, operator_notes_read: delivered };
|
|
8990
|
+
}
|
|
8991
|
+
return result;
|
|
8992
|
+
}
|
|
8993
|
+
|
|
8827
8994
|
function readBoundedContextFile(filePath, maxChars = 1600) {
|
|
8828
8995
|
try {
|
|
8829
8996
|
if (!fs.existsSync(filePath)) return '';
|
|
@@ -8938,6 +9105,16 @@ function buildTickPrompt(mission, tickIndex, maxTicks, frozen, pings = [], cwd =
|
|
|
8938
9105
|
checkFeedback,
|
|
8939
9106
|
]
|
|
8940
9107
|
: [];
|
|
9108
|
+
let turnDown = '';
|
|
9109
|
+
try { turnDown = readBoundTaskTurnDown(mission, mission.worktree_root || mission.worktree?.path || cwd); } catch {}
|
|
9110
|
+
const turnDownLines = turnDown
|
|
9111
|
+
? [
|
|
9112
|
+
``,
|
|
9113
|
+
`## why the last try was turned down`,
|
|
9114
|
+
`the reviewer sent this task back. fix this before anything else.`,
|
|
9115
|
+
turnDown,
|
|
9116
|
+
]
|
|
9117
|
+
: [];
|
|
8941
9118
|
const preloadedContextLines = buildPreloadedMemberContextLines(mission, cwd);
|
|
8942
9119
|
const lines = [
|
|
8943
9120
|
`# Mission Tick ${tickIndex}/${maxTicks}`,
|
|
@@ -8953,6 +9130,7 @@ function buildTickPrompt(mission, tickIndex, maxTicks, frozen, pings = [], cwd =
|
|
|
8953
9130
|
`**Last tick:** ${mission.last_tick_at || 'never'}`,
|
|
8954
9131
|
...missionBudgetPromptLines(mission),
|
|
8955
9132
|
...checkFeedbackLines,
|
|
9133
|
+
...turnDownLines,
|
|
8956
9134
|
...(sharedProcess ? ['', sharedProcess] : []),
|
|
8957
9135
|
...preloadedContextLines,
|
|
8958
9136
|
``,
|
|
@@ -9183,6 +9361,22 @@ function tooDeepRunnerResult(depth) {
|
|
|
9183
9361
|
};
|
|
9184
9362
|
}
|
|
9185
9363
|
|
|
9364
|
+
// Machine memory and load at both ends of a tick, like a dispatch, so a
|
|
9365
|
+
// killed tick still says whether the machine was short on memory. The
|
|
9366
|
+
// returned end writes once, whichever of close or error comes first.
|
|
9367
|
+
function startTickResource(cwd, engine, mission) {
|
|
9368
|
+
const { recordEngineResource } = require('../lib/engine-resource');
|
|
9369
|
+
const resource = { engine: engine || 'runner', task: mission.task_id || mission.id || '', run_id: crypto.randomUUID() };
|
|
9370
|
+
recordEngineResource(cwd, { phase: 'start', ...resource });
|
|
9371
|
+
const startedAt = process.hrtime.bigint();
|
|
9372
|
+
let ended = false;
|
|
9373
|
+
return ({ signal = null, timedOut = false, error = '' } = {}) => {
|
|
9374
|
+
if (ended) return;
|
|
9375
|
+
ended = true;
|
|
9376
|
+
recordEngineResource(cwd, { phase: 'end', ...resource, signal, timed_out: timedOut, error, duration_ms: Math.round(Number(process.hrtime.bigint() - startedAt) / 1e6) });
|
|
9377
|
+
};
|
|
9378
|
+
}
|
|
9379
|
+
|
|
9186
9380
|
function spawnGenericRunnerTick(mission, opts) {
|
|
9187
9381
|
const { sessionId, cwd, signal, timeoutMs, prompt, model, missionLock } = opts;
|
|
9188
9382
|
const dispatchDepth = missionDispatchDepth();
|
|
@@ -9241,7 +9435,17 @@ function spawnGenericRunnerTick(mission, opts) {
|
|
|
9241
9435
|
}
|
|
9242
9436
|
|
|
9243
9437
|
const startedAt = Date.now();
|
|
9244
|
-
const
|
|
9438
|
+
const endResource = startTickResource(cwd, canonicalEngineName(mission.runner), mission);
|
|
9439
|
+
const progressMember = mission.owner || 'mission';
|
|
9440
|
+
// A codex run opens its shell script with the run marker as a no-op, so
|
|
9441
|
+
// member stop can prove the shell leading this group is this run. Only
|
|
9442
|
+
// names member stop accepts go into the shell line.
|
|
9443
|
+
const runToken = lastMessageFile && /^[A-Za-z0-9._-]+$/.test(progressMember) ? newRunToken() : '';
|
|
9444
|
+
const markerPrefix = runToken ? `: ${runMarker(progressMember, runToken)}; ` : '';
|
|
9445
|
+
// The trailing exit keeps a shell like dash from replacing itself with
|
|
9446
|
+
// codex, which would take the marker out of the process list.
|
|
9447
|
+
const markerSuffix = runToken ? '; exit $?' : '';
|
|
9448
|
+
const guardedCommand = `${markerPrefix}PATH=${shellQuote(gitGuard.pathPrefix)}:$PATH; export PATH; ${cmd}${markerSuffix}`;
|
|
9245
9449
|
const proc = spawn('sh', ['-lc', guardedCommand], {
|
|
9246
9450
|
cwd,
|
|
9247
9451
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
@@ -9249,9 +9453,11 @@ function spawnGenericRunnerTick(mission, opts) {
|
|
|
9249
9453
|
env: { ...gitGuard.env, ATRIS_DISPATCH_DEPTH: String(dispatchDepth.depth + 1) },
|
|
9250
9454
|
});
|
|
9251
9455
|
updateMissionLockOwner(missionLock, proc.pid, { phase: 'tick' });
|
|
9252
|
-
const progressMember = mission.owner || 'mission';
|
|
9253
9456
|
const seenItemIds = new Set();
|
|
9254
|
-
if (lastMessageFile)
|
|
9457
|
+
if (lastMessageFile) {
|
|
9458
|
+
const tag = runToken ? { pid: proc.pid, engine: 'codex', run: runToken } : {};
|
|
9459
|
+
appendProgress(progressMember, startedLine(mission.objective, tag), mission.id);
|
|
9460
|
+
}
|
|
9255
9461
|
let stdout = '';
|
|
9256
9462
|
let eventBuf = '';
|
|
9257
9463
|
let lastAgentMessage = '';
|
|
@@ -9303,8 +9509,10 @@ function spawnGenericRunnerTick(mission, opts) {
|
|
|
9303
9509
|
});
|
|
9304
9510
|
proc.stderr.on('data', (chunk) => { stderr += chunk.toString(); });
|
|
9305
9511
|
|
|
9306
|
-
proc.on('close', (code) => {
|
|
9512
|
+
proc.on('close', (code, closeSignal) => {
|
|
9307
9513
|
clearTimeout(timer);
|
|
9514
|
+
// The shell returns 128 + 9 when its engine child was force-killed.
|
|
9515
|
+
endResource({ signal: closeSignal || (code === 137 ? 'SIGKILL' : null), timedOut, error: code === 0 ? '' : stderr.slice(-2000) });
|
|
9308
9516
|
if (lastMessageFile && eventBuf.trim()) readCodexEvent(eventBuf.trim());
|
|
9309
9517
|
let finalText = String(stdout || '').trim();
|
|
9310
9518
|
if (lastMessageFile) {
|
|
@@ -9342,6 +9550,7 @@ function spawnGenericRunnerTick(mission, opts) {
|
|
|
9342
9550
|
|
|
9343
9551
|
proc.on('error', (e) => {
|
|
9344
9552
|
clearTimeout(timer);
|
|
9553
|
+
endResource({ timedOut, error: e.message });
|
|
9345
9554
|
cleanupPrompt();
|
|
9346
9555
|
gitGuard.cleanup();
|
|
9347
9556
|
if (signal) signal.removeEventListener?.('abort', onAbort);
|
|
@@ -9426,6 +9635,7 @@ function spawnClaudeTick(mission, opts) {
|
|
|
9426
9635
|
// detached: the runner spawns its own children; killing only the direct
|
|
9427
9636
|
// child on timeout leaves them holding the session lock, and the next
|
|
9428
9637
|
// tick's resume fails with "Session ID ... is already in use".
|
|
9638
|
+
const endResource = startTickResource(cwd, 'claude', mission);
|
|
9429
9639
|
const proc = spawn(resolveClaudeRunnerBin(), args, {
|
|
9430
9640
|
cwd,
|
|
9431
9641
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
@@ -9504,8 +9714,9 @@ function spawnClaudeTick(mission, opts) {
|
|
|
9504
9714
|
|
|
9505
9715
|
proc.stderr.on('data', (c) => { stderr += c.toString(); });
|
|
9506
9716
|
|
|
9507
|
-
proc.on('close', (code) => {
|
|
9717
|
+
proc.on('close', (code, closeSignal) => {
|
|
9508
9718
|
clearTimeout(timer);
|
|
9719
|
+
endResource({ signal: closeSignal, timedOut, error: code === 0 ? '' : stderr.slice(-2000) });
|
|
9509
9720
|
gitGuard.cleanup();
|
|
9510
9721
|
if (signal) signal.removeEventListener?.('abort', onAbort);
|
|
9511
9722
|
updateMissionLockOwner(missionLock, missionLock?.driverPid);
|
|
@@ -9538,6 +9749,7 @@ function spawnClaudeTick(mission, opts) {
|
|
|
9538
9749
|
|
|
9539
9750
|
proc.on('error', (e) => {
|
|
9540
9751
|
clearTimeout(timer);
|
|
9752
|
+
endResource({ timedOut, error: e.message });
|
|
9541
9753
|
gitGuard.cleanup();
|
|
9542
9754
|
updateMissionLockOwner(missionLock, missionLock?.driverPid);
|
|
9543
9755
|
resolve({ ok: false, error: e.message, sessionIds: [], aborted, timedOut, authExpired: false });
|
|
@@ -9548,14 +9760,42 @@ function spawnClaudeTick(mission, opts) {
|
|
|
9548
9760
|
const ENGINE_VERIFY_TIMEOUT_MS = 5 * 60 * 1000;
|
|
9549
9761
|
const ENGINE_VERIFY_OUTPUT_LIMIT = 4000;
|
|
9550
9762
|
|
|
9551
|
-
|
|
9763
|
+
// The job's own done-looks-like line, so the reviewer judges the work against
|
|
9764
|
+
// what the job promised instead of the mission headline alone. Only a line the
|
|
9765
|
+
// job wrote down counts: a derived fallback ("it clears review") hands the
|
|
9766
|
+
// reviewer a target that says nothing. Any lookup failure means no line.
|
|
9767
|
+
function missionDoneLooksLike(mission, root = process.cwd()) {
|
|
9768
|
+
try {
|
|
9769
|
+
const task = findOpenTaskMatchingMissionObjective(mission, root, true);
|
|
9770
|
+
if (!task) return '';
|
|
9771
|
+
const explanation = require('../lib/task-explanation').taskExplanation(task);
|
|
9772
|
+
return explanation.sources.done_looks_like === 'explicit' ? explanation.done_looks_like : '';
|
|
9773
|
+
} catch {
|
|
9774
|
+
return '';
|
|
9775
|
+
}
|
|
9776
|
+
}
|
|
9777
|
+
|
|
9778
|
+
function buildEngineVerifyPrompt(mission, tickIndex, doneLooksLike = '', { timeoutMs = ENGINE_VERIFY_TIMEOUT_MS, checkPassed = '' } = {}) {
|
|
9779
|
+
const doneLine = String(doneLooksLike || '').replace(/\s+/g, ' ').trim();
|
|
9780
|
+
const minutes = Math.max(1, Math.round(timeoutMs / 60000));
|
|
9552
9781
|
return [
|
|
9553
9782
|
`# Verify Mission Tick ${tickIndex}`,
|
|
9554
9783
|
``,
|
|
9555
9784
|
`This is a bounded verify-only pass for mission: ${mission.objective}`,
|
|
9785
|
+
...(doneLine ? [
|
|
9786
|
+
`The job says done looks like: ${doneLine}`,
|
|
9787
|
+
`Compare the work against that line. If the work does not show it, the verdict is FAIL even when the checks pass.`,
|
|
9788
|
+
`Split that line into each separate thing it promises and check every one. List each part as met or missing.`,
|
|
9789
|
+
`Work that does only some of the parts is not done. If any part is missing, end with 'VERDICT: FAIL; missing: <that part>'.`,
|
|
9790
|
+
] : []),
|
|
9556
9791
|
`Read the diff this tick produced. Do not edit files and do not start another work slice.`,
|
|
9557
9792
|
`ACTUALLY RUN the changed surface on this computer using real commands and real exit codes.`,
|
|
9558
9793
|
`Paste the commands and their real output. Do not accept a receipt claim or static inspection as execution proof.`,
|
|
9794
|
+
// Reviews that outran the clock were 11 of 25 turn-downs (10/11 to 10/14).
|
|
9795
|
+
`You have ${minutes} minutes. Leave time to write the verdict; running out of time counts as a fail.`,
|
|
9796
|
+
...(checkPassed ? [`The mission check \`${checkPassed}\` already passed on this exact code. Do not run it again.`] : []),
|
|
9797
|
+
`Prove the change with the narrowest real run: the changed command, or one test file or one named test.`,
|
|
9798
|
+
`Do not run whole test suites or large test files; on a busy machine they outlast your time.`,
|
|
9559
9799
|
`End your reply with a final line exactly 'VERDICT: PASS' or 'VERDICT: FAIL'.`,
|
|
9560
9800
|
].join('\n');
|
|
9561
9801
|
}
|
|
@@ -9617,7 +9857,10 @@ async function runEngineVerifier(mission, options = {}) {
|
|
|
9617
9857
|
cwd,
|
|
9618
9858
|
signal: options.signal,
|
|
9619
9859
|
timeoutMs: ENGINE_VERIFY_TIMEOUT_MS,
|
|
9620
|
-
prompt: buildEngineVerifyPrompt(mission, options.tickIndex || 1),
|
|
9860
|
+
prompt: buildEngineVerifyPrompt(mission, options.tickIndex || 1, missionDoneLooksLike(mission, cwd), {
|
|
9861
|
+
timeoutMs: ENGINE_VERIFY_TIMEOUT_MS,
|
|
9862
|
+
checkPassed: options.checkPassed || '',
|
|
9863
|
+
}),
|
|
9621
9864
|
model: resolveMissionTickRunnerModel(verifyMission),
|
|
9622
9865
|
});
|
|
9623
9866
|
} catch (error) {
|
|
@@ -9671,6 +9914,20 @@ function cappedClaudeReceiptText(text, limit = 4000) {
|
|
|
9671
9914
|
return clean.slice(0, limit - 16).trimEnd() + '\n...[truncated]';
|
|
9672
9915
|
}
|
|
9673
9916
|
|
|
9917
|
+
// The receipt keeps a capped copy of the runner's report. When the report is
|
|
9918
|
+
// longer than that, the whole text goes to its own file beside the receipts
|
|
9919
|
+
// so nothing the member wrote is lost.
|
|
9920
|
+
function saveFullRunnerReport(mission, text, root = process.cwd()) {
|
|
9921
|
+
const clean = String(text || '').trim();
|
|
9922
|
+
if (!clean || cappedClaudeReceiptText(clean) === clean) return null;
|
|
9923
|
+
const runsDir = statePaths(root).runsDir;
|
|
9924
|
+
fs.mkdirSync(runsDir, { recursive: true });
|
|
9925
|
+
const safeTime = stampIso().replace(/[:.]/g, '-');
|
|
9926
|
+
const reportPath = path.join(runsDir, `mission-${mission.id}-${safeTime}-report.md`);
|
|
9927
|
+
fs.writeFileSync(reportPath, clean + '\n', 'utf8');
|
|
9928
|
+
return path.relative(root, reportPath);
|
|
9929
|
+
}
|
|
9930
|
+
|
|
9674
9931
|
async function parseAndValidateMissionRunPhase(args) {
|
|
9675
9932
|
const asJson = wantsJson(args);
|
|
9676
9933
|
const context = {
|
|
@@ -10062,6 +10319,9 @@ async function executeMissionRunTicksPhase(context) {
|
|
|
10062
10319
|
let cycleTickLimit = effectiveMaxTicks;
|
|
10063
10320
|
while (true) {
|
|
10064
10321
|
while (ticks.length < cycleTickLimit) {
|
|
10322
|
+
// A campaign that is done, paused, or out of money stops its missions.
|
|
10323
|
+
const campaignStop = mission.campaign_id ? campaignMissions().campaignGate(cwd, mission) : null;
|
|
10324
|
+
if (campaignStop) { pauseReason = campaignStop; break; }
|
|
10065
10325
|
// Re-read before measuring the wall. Detached full-budget drivers use
|
|
10066
10326
|
// the mission contract as their wall so process startup drift cannot
|
|
10067
10327
|
// shorten the promised work window.
|
|
@@ -10201,8 +10461,8 @@ async function executeMissionRunTicksPhase(context) {
|
|
|
10201
10461
|
}, Math.max(1, Math.floor(remainingWall * 1000)));
|
|
10202
10462
|
let turn;
|
|
10203
10463
|
try {
|
|
10204
|
-
|
|
10205
|
-
prompt,
|
|
10464
|
+
const runTurn = (direction) => runAtris2Turn({
|
|
10465
|
+
prompt: direction,
|
|
10206
10466
|
model: tickRuntimeMission.model || 'atris:fast',
|
|
10207
10467
|
business: businessId,
|
|
10208
10468
|
maxTurns: 16,
|
|
@@ -10213,6 +10473,16 @@ async function executeMissionRunTicksPhase(context) {
|
|
|
10213
10473
|
localEnv: atris2GitGuard.env,
|
|
10214
10474
|
localPathPrefix: atris2GitGuard.pathPrefix,
|
|
10215
10475
|
});
|
|
10476
|
+
turn = await runTurn(prompt);
|
|
10477
|
+
turn = await deliverLateMissionPings(mission, cwd, { ...turn, receipt_text: turn.text }, {
|
|
10478
|
+
prompt,
|
|
10479
|
+
remainingMs: () => (maxWallSeconds * 1000) - (Date.now() - startedAt),
|
|
10480
|
+
run: async (direction) => {
|
|
10481
|
+
const next = await runTurn(direction);
|
|
10482
|
+
return { ...next, receipt_text: next.text };
|
|
10483
|
+
},
|
|
10484
|
+
});
|
|
10485
|
+
mission = resolveMission(mission.id, cwd) || mission;
|
|
10216
10486
|
} finally {
|
|
10217
10487
|
atris2GitGuard.cleanup();
|
|
10218
10488
|
clearTimeout(tickTimer);
|
|
@@ -10228,6 +10498,7 @@ async function executeMissionRunTicksPhase(context) {
|
|
|
10228
10498
|
error: turn.error,
|
|
10229
10499
|
backend_unavailable: isTransientAtris2BackendError(turn.error) || undefined,
|
|
10230
10500
|
receipt_text: String(turn.text || '').slice(0, 4000),
|
|
10501
|
+
operator_notes_read: turn.operator_notes_read,
|
|
10231
10502
|
};
|
|
10232
10503
|
tickWorkerRun = { endedMs: Date.now(), usage: {} };
|
|
10233
10504
|
if (controller.signal.aborted) { pauseReason = 'aborted-during-atris2'; break; }
|
|
@@ -10261,14 +10532,14 @@ async function executeMissionRunTicksPhase(context) {
|
|
|
10261
10532
|
const restoreTickRunnerProfile = tickEngineId ? applyMissionRunnerProfile(tickEngineId) : () => {};
|
|
10262
10533
|
let claudeResult;
|
|
10263
10534
|
let sessionBusyRetried = false;
|
|
10264
|
-
const runClaudeSession = () => spawnClaudeTick(tickRuntimeMission, {
|
|
10535
|
+
const runClaudeSession = (direction = workerPrompt) => spawnClaudeTick(tickRuntimeMission, {
|
|
10265
10536
|
sessionMode, sessionId: useId, cwd, signal: controller.signal,
|
|
10266
10537
|
missionLock: lock,
|
|
10267
10538
|
timeoutMs: missionTickTimeoutMs(
|
|
10268
10539
|
tickRuntimeMission,
|
|
10269
10540
|
(maxWallSeconds - ((Date.now() - startedAt) / 1000)) * 1000,
|
|
10270
10541
|
),
|
|
10271
|
-
prompt:
|
|
10542
|
+
prompt: direction,
|
|
10272
10543
|
model: resolveMissionTickRunnerModel(tickRuntimeMission),
|
|
10273
10544
|
});
|
|
10274
10545
|
try {
|
|
@@ -10285,17 +10556,30 @@ async function executeMissionRunTicksPhase(context) {
|
|
|
10285
10556
|
useId = pendingSessionId;
|
|
10286
10557
|
claudeResult = await runClaudeSession();
|
|
10287
10558
|
}
|
|
10559
|
+
claudeResult = await deliverLateMissionPings(mission, cwd, claudeResult, {
|
|
10560
|
+
prompt: workerPrompt,
|
|
10561
|
+
remainingMs: () => (maxWallSeconds * 1000) - (Date.now() - startedAt),
|
|
10562
|
+
run: (direction) => {
|
|
10563
|
+
// Claude needs to resume the session it just confirmed. Generic
|
|
10564
|
+
// runners receive the earlier receipt in the delivery prompt.
|
|
10565
|
+
if (claudeResult.sessionIds?.includes(useId)) sessionMode = 'resume';
|
|
10566
|
+
return runClaudeSession(direction);
|
|
10567
|
+
},
|
|
10568
|
+
});
|
|
10569
|
+
mission = resolveMission(mission.id, cwd) || mission;
|
|
10288
10570
|
} finally {
|
|
10289
10571
|
restoreTickRunnerProfile();
|
|
10290
10572
|
}
|
|
10291
10573
|
tickWorkerRun = { endedMs: Date.now(), usage: (claudeResult && claudeResult.run_usage) || {}, prep: tickPrep, startedAt: workerStartedAt };
|
|
10292
10574
|
result.claude = {
|
|
10293
10575
|
ok: claudeResult.ok,
|
|
10576
|
+
error: claudeResult.error,
|
|
10294
10577
|
reason: claudeResult.reason,
|
|
10295
10578
|
protected_lane_guard: claudeResult.protected_lane_guard || null,
|
|
10296
10579
|
brief_id: claudeResult.brief_id || null,
|
|
10297
10580
|
summary: claudeResult.summary,
|
|
10298
10581
|
receipt_text: claudeResult.receipt_text,
|
|
10582
|
+
report_path: saveFullRunnerReport(mission, claudeResult.result, cwd),
|
|
10299
10583
|
stop_reason: claudeResult.stop_reason,
|
|
10300
10584
|
api_equivalent_estimate: claudeResult.api_equivalent_estimate,
|
|
10301
10585
|
duration_total_ms: claudeResult.duration_total_ms,
|
|
@@ -10306,6 +10590,7 @@ async function executeMissionRunTicksPhase(context) {
|
|
|
10306
10590
|
timed_out: claudeResult.timedOut,
|
|
10307
10591
|
aborted: claudeResult.aborted,
|
|
10308
10592
|
session_busy_retry: sessionBusyRetried || undefined,
|
|
10593
|
+
operator_notes_read: claudeResult.operator_notes_read,
|
|
10309
10594
|
};
|
|
10310
10595
|
if (claudeResult.rate_limit_info) {
|
|
10311
10596
|
// Claude reports the five-hour window's resetsAt on every turn, even
|
|
@@ -10320,7 +10605,7 @@ async function executeMissionRunTicksPhase(context) {
|
|
|
10320
10605
|
|
|
10321
10606
|
if (!claudeResult.ok) {
|
|
10322
10607
|
const deadModel = detectUnavailableModel(claudeResult.summary || claudeResult.receipt_text);
|
|
10323
|
-
const wallBoundTimeout = remainingWall * 1000
|
|
10608
|
+
const wallBoundTimeout = missionTickWallBound(tickRuntimeMission, remainingWall * 1000);
|
|
10324
10609
|
let reason = claudeResult.reason === 'too-deep'
|
|
10325
10610
|
? 'too-deep'
|
|
10326
10611
|
: claudeResult.timedOut
|
|
@@ -10410,6 +10695,7 @@ async function executeMissionRunTicksPhase(context) {
|
|
|
10410
10695
|
prep: tickWorkerRun.prep || null,
|
|
10411
10696
|
});
|
|
10412
10697
|
}
|
|
10698
|
+
if (mission.campaign_id && !cachedStep) campaignMissions().recordCampaignTick(cwd, mission, { ...((tickWorkerRun && tickWorkerRun.usage) || {}), ...(tickEngineId ? { engine: tickEngineId } : {}) });
|
|
10413
10699
|
let receiptPath = cachedStep ? cachedStep.receipt_path : null;
|
|
10414
10700
|
|
|
10415
10701
|
// Review-lane drain: always-on loops sweep the agent-safe review actions
|
|
@@ -10557,7 +10843,7 @@ async function executeMissionRunTicksPhase(context) {
|
|
|
10557
10843
|
mission: mission.objective,
|
|
10558
10844
|
state: mission.status,
|
|
10559
10845
|
tick_status: result.status,
|
|
10560
|
-
reason: result
|
|
10846
|
+
reason: memberLogTickReason(result),
|
|
10561
10847
|
layer: result.layer || undefined,
|
|
10562
10848
|
verifier: verifierResult ? (verifierResult.passed ? 'passed' : 'failed') : 'not_run',
|
|
10563
10849
|
summary: tickSummaryText ? tickSummaryText.slice(0, 240) : undefined,
|
|
@@ -10771,6 +11057,7 @@ async function verifyMissionRunTickPhase(context) {
|
|
|
10771
11057
|
signal: controller.signal,
|
|
10772
11058
|
tickIndex: currentTick.tickIdx,
|
|
10773
11059
|
refereeEngine: context.refereeEngine,
|
|
11060
|
+
checkPassed: verifierResult?.passed ? currentTick.frozen.verifier : '',
|
|
10774
11061
|
});
|
|
10775
11062
|
const afterReview = gitCodeState(cwd);
|
|
10776
11063
|
const codeMoved = beforeReview.tree !== afterReview.tree
|
|
@@ -12415,6 +12702,7 @@ function missionCommand(args) {
|
|
|
12415
12702
|
}
|
|
12416
12703
|
|
|
12417
12704
|
module.exports = {
|
|
12705
|
+
listSiblingWorkspaceMissionHints,
|
|
12418
12706
|
missionProtectedLaneHold,
|
|
12419
12707
|
missionCommand,
|
|
12420
12708
|
startMission,
|
|
@@ -12467,6 +12755,7 @@ module.exports = {
|
|
|
12467
12755
|
missionVerifierCheckedText,
|
|
12468
12756
|
missionVerifierHighLevelTestText,
|
|
12469
12757
|
buildEngineVerifyPrompt,
|
|
12758
|
+
missionDoneLooksLike,
|
|
12470
12759
|
engineVerifierResultFromRun,
|
|
12471
12760
|
pickCrossEngineVerifier,
|
|
12472
12761
|
runVerifier,
|
|
@@ -12477,6 +12766,8 @@ module.exports = {
|
|
|
12477
12766
|
resolveMissionTickRunner,
|
|
12478
12767
|
resolveMissionTickRunnerModel,
|
|
12479
12768
|
missionTickTimeoutMs,
|
|
12769
|
+
missionTickWallBound,
|
|
12770
|
+
memberLogTickReason,
|
|
12480
12771
|
engineFailureHealthStatus,
|
|
12481
12772
|
recordMissionEngineTickOutcome,
|
|
12482
12773
|
recordMissionTickRosterRun,
|