atris 3.38.0 → 3.41.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/AGENTS.md +25 -6
- package/atris/PERSONA.md +8 -4
- package/atris.md +7 -0
- package/ax +2 -1
- package/bin/atris.js +31 -6
- package/commands/agent-spawn.js +13 -11
- package/commands/autoland.js +28 -77
- package/commands/bench.js +10 -12
- package/commands/business.js +345 -0
- package/commands/chat-scan.js +5 -7
- package/commands/codex-goal.js +8 -10
- package/commands/computer.js +20 -0
- package/commands/console.js +19 -3
- package/commands/decide.js +166 -0
- package/commands/deck.js +1 -4
- package/commands/drill.js +14 -24
- package/commands/engine.js +196 -8
- package/commands/gm.js +8 -6
- package/commands/harvest.js +1 -4
- package/commands/init.js +23 -3
- package/commands/land.js +8 -14
- package/commands/launchpad.js +1 -14
- package/commands/lifecycle.js +5 -5
- package/commands/log.js +55 -5
- package/commands/member.js +558 -574
- package/commands/mission.js +456 -237
- package/commands/pack.js +2746 -164
- package/commands/play.js +6 -4
- package/commands/probe.js +2 -2
- package/commands/pulse.js +15 -16
- package/commands/release.js +10 -9
- package/commands/router.js +5 -4
- package/commands/site-deploy.js +885 -0
- package/commands/site.js +11 -2
- package/commands/slop.js +14 -2
- package/commands/stream.js +4 -18
- package/commands/task.js +880 -558
- package/commands/taste.js +101 -0
- package/commands/team.js +176 -3
- package/commands/vercel.js +4 -2
- package/commands/voice.js +195 -0
- package/commands/watch.js +1 -22
- package/commands/wiki.js +1 -4
- package/commands/workflow.js +2 -2
- package/commands/worktree.js +1 -14
- package/commands/xp.js +27 -24
- package/lib/accept-verify-gate.js +5 -1
- package/lib/arg-parser.js +41 -0
- package/lib/auto-accept-certified.js +116 -1
- package/lib/autoland.js +66 -0
- package/lib/bench/runner.js +19 -1
- package/lib/context-gatherer.js +7 -1
- package/lib/engine-registry.js +141 -20
- package/lib/falsifier-probe.js +84 -0
- package/lib/fleet.js +65 -15
- package/lib/git-spawn.js +15 -0
- package/lib/json-file.js +37 -0
- package/lib/known-commands.js +2 -2
- package/lib/lesson-preflight.js +146 -0
- package/lib/loop-doctor.js +0 -2
- package/lib/mission-human-asks.js +28 -0
- package/lib/mission-protected-lane.js +4 -1
- package/lib/official-cli-integration.js +47 -2
- package/lib/orb-context.js +8 -1
- package/lib/pack-capabilities.js +685 -0
- package/lib/router-brain.js +51 -1
- package/lib/runner-command.js +0 -6
- package/lib/self-drive.js +44 -13
- package/lib/task-db.js +137 -3
- package/lib/task-decision.js +50 -0
- package/lib/taste-lessons.js +153 -0
- package/lib/tool-result-encode.js +17 -1
- package/lib/voice-gate.js +66 -0
- package/lib/wish-audit.js +1 -1
- package/lib/wish-delegate.js +1 -1
- package/lib/zip.js +95 -7
- package/package.json +2 -1
- package/templates/business-starter/persona.md +9 -0
package/commands/mission.js
CHANGED
|
@@ -5,6 +5,7 @@ const path = require('path');
|
|
|
5
5
|
const crypto = require('crypto');
|
|
6
6
|
const readline = require('readline');
|
|
7
7
|
const { spawn, spawnSync } = require('child_process');
|
|
8
|
+
const { hasFlag, readFlag, readIntFlag } = require('../lib/arg-parser');
|
|
8
9
|
const {
|
|
9
10
|
appendBriefRecord,
|
|
10
11
|
stampBriefOutcome,
|
|
@@ -78,6 +79,11 @@ const {
|
|
|
78
79
|
resolveDefaultVerifier,
|
|
79
80
|
} = require('../lib/default-verifier');
|
|
80
81
|
const { redirectToWorkspaceRoot } = require('../lib/mission-root');
|
|
82
|
+
const { readJson, writeJson } = require('../lib/json-file');
|
|
83
|
+
const {
|
|
84
|
+
normalizeHumanAsks,
|
|
85
|
+
openHumanAsks,
|
|
86
|
+
} = require('../lib/mission-human-asks');
|
|
81
87
|
const {
|
|
82
88
|
inspectMissionProtectedDiff,
|
|
83
89
|
prepareMissionGitGuard,
|
|
@@ -318,28 +324,6 @@ function readMissionRouteProposal(args, asJson) {
|
|
|
318
324
|
};
|
|
319
325
|
}
|
|
320
326
|
|
|
321
|
-
function hasFlag(args, name) {
|
|
322
|
-
return args.includes(name);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
function unquote(value) {
|
|
326
|
-
const text = String(value);
|
|
327
|
-
if ((text.startsWith('"') && text.endsWith('"')) || (text.startsWith("'") && text.endsWith("'"))) {
|
|
328
|
-
return text.slice(1, -1);
|
|
329
|
-
}
|
|
330
|
-
return text;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
function readFlag(args, name, fallback = '') {
|
|
334
|
-
const prefix = `${name}=`;
|
|
335
|
-
for (let i = 0; i < args.length; i += 1) {
|
|
336
|
-
const arg = String(args[i]);
|
|
337
|
-
if (arg === name && args[i + 1] && !String(args[i + 1]).startsWith('--')) return unquote(args[i + 1]);
|
|
338
|
-
if (arg.startsWith(prefix)) return unquote(arg.slice(prefix.length));
|
|
339
|
-
}
|
|
340
|
-
return fallback;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
327
|
const MISSION_NATIVE_RUNNER_NAMES = Object.freeze(['manual', 'claude', 'atris2', 'codex_goal', 'caller_session', 'current_agent', 'drill']);
|
|
344
328
|
const MISSION_NATIVE_RUNNER_SET = new Set(MISSION_NATIVE_RUNNER_NAMES);
|
|
345
329
|
const MISSION_AUTO_RUNNER = 'auto';
|
|
@@ -513,11 +497,11 @@ function readRepeatedFlag(args, name) {
|
|
|
513
497
|
for (let i = 0; i < args.length; i += 1) {
|
|
514
498
|
const arg = String(args[i]);
|
|
515
499
|
if (arg === name && args[i + 1] && !String(args[i + 1]).startsWith('--')) {
|
|
516
|
-
values.push(
|
|
500
|
+
values.push(readFlag([name, args[i + 1]], name, ''));
|
|
517
501
|
i += 1;
|
|
518
502
|
continue;
|
|
519
503
|
}
|
|
520
|
-
if (arg.startsWith(prefix)) values.push(
|
|
504
|
+
if (arg.startsWith(prefix)) values.push(readFlag([arg], name, ''));
|
|
521
505
|
}
|
|
522
506
|
return values.filter(Boolean);
|
|
523
507
|
}
|
|
@@ -739,8 +723,7 @@ function loadTaskDb(asJson = false) {
|
|
|
739
723
|
function writeMissionTaskProjection(taskDb, db, workspaceRoot) {
|
|
740
724
|
const projection = taskDb.taskProjection(db, { workspaceRoot, limit: 500 });
|
|
741
725
|
const outPath = path.join(workspaceRoot, '.atris', 'state', 'tasks.projection.json');
|
|
742
|
-
|
|
743
|
-
fs.writeFileSync(outPath, JSON.stringify(projection, null, 2) + '\n', 'utf8');
|
|
726
|
+
writeJson(outPath, projection);
|
|
744
727
|
return { projection, outPath };
|
|
745
728
|
}
|
|
746
729
|
|
|
@@ -839,16 +822,13 @@ function statePaths(root = process.cwd()) {
|
|
|
839
822
|
|
|
840
823
|
function readBusinessBinding(root = process.cwd()) {
|
|
841
824
|
const file = path.join(root, '.atris', 'business.json');
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
} catch {
|
|
850
|
-
return null;
|
|
851
|
-
}
|
|
825
|
+
const parsed = readJson(file);
|
|
826
|
+
if (parsed == null) return null;
|
|
827
|
+
return {
|
|
828
|
+
business_id: parsed.business_id || '',
|
|
829
|
+
workspace_id: parsed.workspace_id || '',
|
|
830
|
+
slug: parsed.slug || '',
|
|
831
|
+
};
|
|
852
832
|
}
|
|
853
833
|
|
|
854
834
|
function businessIdForAtris2Mission(mission, cwd = process.cwd()) {
|
|
@@ -935,9 +915,11 @@ function terminalNextAction(status) {
|
|
|
935
915
|
|
|
936
916
|
function normalizeMissionState(mission) {
|
|
937
917
|
if (!mission) return mission;
|
|
938
|
-
let normalized = mission
|
|
939
|
-
|
|
940
|
-
|
|
918
|
+
let normalized = Array.isArray(mission.human_asks)
|
|
919
|
+
? { ...mission, human_asks: normalizeHumanAsks(mission.human_asks) }
|
|
920
|
+
: mission;
|
|
921
|
+
const nextAction = terminalNextAction(normalized.status);
|
|
922
|
+
if (nextAction && normalized.next_action !== nextAction) {
|
|
941
923
|
normalized = { ...normalized, next_action: nextAction };
|
|
942
924
|
}
|
|
943
925
|
const effectiveVerifier = effectiveMissionVerifier(normalized);
|
|
@@ -1524,9 +1506,10 @@ function renderMemberNowMarkdown(owner, missions, root = process.cwd()) {
|
|
|
1524
1506
|
if (mission.stop_condition) lines.push(`- stop: ${mission.stop_condition}`);
|
|
1525
1507
|
if (budgetContinuation || mission.next_action) lines.push(`- next: ${budgetContinuation || mission.next_action}`);
|
|
1526
1508
|
if (mission.receipt_path) lines.push(`- proof: ${missionStatusProofText(mission)}`);
|
|
1527
|
-
|
|
1509
|
+
const humanAsks = openHumanAsks(mission.human_asks);
|
|
1510
|
+
if (humanAsks.length) {
|
|
1528
1511
|
lines.push('- human asks:');
|
|
1529
|
-
for (const ask of
|
|
1512
|
+
for (const ask of humanAsks) lines.push(` - ${ask.text}`);
|
|
1530
1513
|
}
|
|
1531
1514
|
lines.push('');
|
|
1532
1515
|
}
|
|
@@ -1734,7 +1717,7 @@ function missionVerifierCheckedText(verifierResult, mission) {
|
|
|
1734
1717
|
if (/(?:node\s+\S*atris\.js|\batris)\s+drill\b/i.test(command)) return 'I ran the no-model end-to-end workflow drill.';
|
|
1735
1718
|
return `Verifier passed: ${command}.`;
|
|
1736
1719
|
}
|
|
1737
|
-
if (/^git\s+diff\s+--check\b/i.test(command)) return 'VERIFY FAILED: diff cleanliness check failed.';
|
|
1720
|
+
if (/^git\s+diff\s+--check\b/i.test(command)) return 'VERIFY FAILED: diff cleanliness check failed. Trailing whitespace in markdown is auto-fixable: npm run audit:markdown-whitespace -- --fix';
|
|
1738
1721
|
if (/\bnode\s+--test\b/i.test(command)) return 'VERIFY FAILED: behavior checks failed.';
|
|
1739
1722
|
return `VERIFY FAILED: ${command}.`;
|
|
1740
1723
|
}
|
|
@@ -1755,7 +1738,8 @@ function missionVerifierHighLevelTestText(verifierResult, mission) {
|
|
|
1755
1738
|
}
|
|
1756
1739
|
const outcome = verifierResult.passed ? 'passed' : 'failed';
|
|
1757
1740
|
if (/^git\s+diff\s+--check\b/i.test(command)) {
|
|
1758
|
-
return `Diff cleanliness check
|
|
1741
|
+
if (verifierResult.passed) return `Diff cleanliness check passed: no whitespace or patch-format issues in the changed files.`;
|
|
1742
|
+
return `Diff cleanliness check ${outcome}: whitespace or patch-format issues in the changed files. Trailing whitespace in markdown is auto-fixable: npm run audit:markdown-whitespace -- --fix`;
|
|
1759
1743
|
}
|
|
1760
1744
|
if (/\bnode\s+--test\b/i.test(command) && /\btest\/mission-status\.test\.js\b/i.test(command)) {
|
|
1761
1745
|
return `Mission behavior checks ${outcome}: mission start, tick, completion, timeline landing, goal-chain, next-mission, and human-accept boundaries were exercised.`;
|
|
@@ -2980,12 +2964,8 @@ function handledContinuationTargetKeys(root, moves) {
|
|
|
2980
2964
|
|
|
2981
2965
|
function readTaskProjectionForMission(root = process.cwd()) {
|
|
2982
2966
|
const file = path.join(root, '.atris', 'state', 'tasks.projection.json');
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
return Array.isArray(parsed?.tasks) ? parsed.tasks : [];
|
|
2986
|
-
} catch {
|
|
2987
|
-
return [];
|
|
2988
|
-
}
|
|
2967
|
+
const parsed = readJson(file);
|
|
2968
|
+
return Array.isArray(parsed?.tasks) ? parsed.tasks : [];
|
|
2989
2969
|
}
|
|
2990
2970
|
|
|
2991
2971
|
function taskTags(task) {
|
|
@@ -3367,8 +3347,7 @@ function readRecentTasteLogs(root, owner, limit = 3) {
|
|
|
3367
3347
|
|
|
3368
3348
|
function readTasteReviewHistory(root, limit = 4) {
|
|
3369
3349
|
const file = path.join(root, '.atris', 'state', 'tasks.projection.json');
|
|
3370
|
-
|
|
3371
|
-
try { projection = JSON.parse(fs.readFileSync(file, 'utf8')); } catch { projection = null; }
|
|
3350
|
+
const projection = readJson(file);
|
|
3372
3351
|
const tasks = Array.isArray(projection?.tasks) ? projection.tasks : [];
|
|
3373
3352
|
const accepted = [];
|
|
3374
3353
|
const revised = [];
|
|
@@ -4738,11 +4717,7 @@ function statusMission(args) {
|
|
|
4738
4717
|
function readMissionReceipt(receiptPath, root = process.cwd()) {
|
|
4739
4718
|
if (!receiptPath) return null;
|
|
4740
4719
|
const file = path.isAbsolute(receiptPath) ? receiptPath : path.join(root, receiptPath);
|
|
4741
|
-
|
|
4742
|
-
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
4743
|
-
} catch {
|
|
4744
|
-
return null;
|
|
4745
|
-
}
|
|
4720
|
+
return readJson(file);
|
|
4746
4721
|
}
|
|
4747
4722
|
|
|
4748
4723
|
function firstUsefulLine(text, fallback = '') {
|
|
@@ -4867,12 +4842,7 @@ function missionVerificationDebt(mission, root = process.cwd()) {
|
|
|
4867
4842
|
let unchecked = 0;
|
|
4868
4843
|
const seen = new Set();
|
|
4869
4844
|
for (const file of files) {
|
|
4870
|
-
|
|
4871
|
-
try {
|
|
4872
|
-
receipt = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
4873
|
-
} catch {
|
|
4874
|
-
continue;
|
|
4875
|
-
}
|
|
4845
|
+
const receipt = readJson(file);
|
|
4876
4846
|
if (!receipt || receipt.mission_id !== mission.id) continue;
|
|
4877
4847
|
for (const tick of missionReceiptTicks(receipt)) {
|
|
4878
4848
|
if (!tick || tick.status !== 'ran') continue;
|
|
@@ -4909,12 +4879,7 @@ function missionReportTimeline(mission, root = process.cwd(), limit = 6) {
|
|
|
4909
4879
|
const items = [];
|
|
4910
4880
|
const seen = new Set();
|
|
4911
4881
|
for (const file of files) {
|
|
4912
|
-
|
|
4913
|
-
try {
|
|
4914
|
-
receipt = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
4915
|
-
} catch {
|
|
4916
|
-
continue;
|
|
4917
|
-
}
|
|
4882
|
+
const receipt = readJson(file);
|
|
4918
4883
|
if (!receipt || receipt.mission_id !== mission.id) continue;
|
|
4919
4884
|
const receiptPath = path.relative(root, file);
|
|
4920
4885
|
for (const tick of missionReceiptTicks(receipt)) {
|
|
@@ -4957,12 +4922,7 @@ function missionLandingTimeline(mission, root = process.cwd(), limit = 12, { kin
|
|
|
4957
4922
|
const items = [];
|
|
4958
4923
|
const seen = new Set();
|
|
4959
4924
|
for (const file of files) {
|
|
4960
|
-
|
|
4961
|
-
try {
|
|
4962
|
-
receipt = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
4963
|
-
} catch {
|
|
4964
|
-
continue;
|
|
4965
|
-
}
|
|
4925
|
+
const receipt = readJson(file);
|
|
4966
4926
|
if (!receipt || receipt.mission_id !== mission.id) continue;
|
|
4967
4927
|
const receiptKind = receipt.result && receipt.result.kind ? String(receipt.result.kind) : (receipt.kind ? String(receipt.kind) : '');
|
|
4968
4928
|
if (kindFilter && receiptKind !== kindFilter) continue;
|
|
@@ -5312,14 +5272,11 @@ function timelineMission(args) {
|
|
|
5312
5272
|
const sinceFilter = readFlag(args, '--since', '') || null;
|
|
5313
5273
|
const ref = stripKnownFlags(args, ['--limit', '--output', '--out', '--kind', '--since'], ['--json', '--write', '--all', '--prune-preview'])[0] || '';
|
|
5314
5274
|
const limit = all ? Number.MAX_SAFE_INTEGER : readPositiveIntegerFlag(args, '--limit', 12, { json: asJson });
|
|
5315
|
-
const
|
|
5316
|
-
|
|
5317
|
-
? resolveMission(ref)
|
|
5318
|
-
: (missions.find((row) => !TERMINAL_STATUSES.has(row.status)) || missions[0] || null);
|
|
5319
|
-
if (ref && !mission) {
|
|
5275
|
+
const data = loadTimelineData(ref, { limit, kindFilter, sinceFilter, write, prunePreviewRequested, outputPath });
|
|
5276
|
+
if (ref && !data.mission) {
|
|
5320
5277
|
exitMissingMission(ref, 1, asJson);
|
|
5321
5278
|
}
|
|
5322
|
-
if (!mission) {
|
|
5279
|
+
if (!data.mission) {
|
|
5323
5280
|
printJsonOrText(
|
|
5324
5281
|
{
|
|
5325
5282
|
ok: true,
|
|
@@ -5333,8 +5290,40 @@ function timelineMission(args) {
|
|
|
5333
5290
|
);
|
|
5334
5291
|
return;
|
|
5335
5292
|
}
|
|
5293
|
+
const { payload, lines } = groupTimeline(data, { all, kindFilter, sinceFilter });
|
|
5294
|
+
renderTimeline(payload, lines, asJson);
|
|
5295
|
+
}
|
|
5296
|
+
|
|
5297
|
+
function loadTimelineData(ref, { limit, kindFilter, sinceFilter, write, prunePreviewRequested, outputPath }) {
|
|
5298
|
+
const missions = listMissions();
|
|
5299
|
+
const mission = ref
|
|
5300
|
+
? resolveMission(ref)
|
|
5301
|
+
: (missions.find((row) => !TERMINAL_STATUSES.has(row.status)) || missions[0] || null);
|
|
5302
|
+
if (!mission) return { mission: null };
|
|
5336
5303
|
const root = mission.worktree_root || process.cwd();
|
|
5337
5304
|
const timelineResult = missionLandingTimeline(mission, root, limit, { kind: kindFilter, since: sinceFilter });
|
|
5305
|
+
const generatedAt = stampIso();
|
|
5306
|
+
let artifactPath = null;
|
|
5307
|
+
let prunePreview = null;
|
|
5308
|
+
if (write || prunePreviewRequested) {
|
|
5309
|
+
try {
|
|
5310
|
+
prunePreview = pruneRuns(root, { keepNewest: 200, keepDays: 14 });
|
|
5311
|
+
} catch (error) {
|
|
5312
|
+
prunePreview = { error: error.message || String(error) };
|
|
5313
|
+
}
|
|
5314
|
+
}
|
|
5315
|
+
if (write) {
|
|
5316
|
+
const outPath = outputPath
|
|
5317
|
+
? path.resolve(root, outputPath)
|
|
5318
|
+
: defaultMissionTimelinePath(root, mission);
|
|
5319
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
5320
|
+
fs.writeFileSync(outPath, missionTimelineMarkdown(mission, timelineResult.items, { prunePreview, generatedAt, timelineMeta: timelineResult.meta }), 'utf8');
|
|
5321
|
+
artifactPath = path.relative(root, outPath);
|
|
5322
|
+
}
|
|
5323
|
+
return { mission, timelineResult, generatedAt, prunePreview, artifactPath };
|
|
5324
|
+
}
|
|
5325
|
+
|
|
5326
|
+
function groupTimeline({ mission, timelineResult, generatedAt, prunePreview, artifactPath }, { all, kindFilter, sinceFilter }) {
|
|
5338
5327
|
const timeline = timelineResult.items;
|
|
5339
5328
|
const currentLanding = missionTimelineCurrentLanding(timeline);
|
|
5340
5329
|
const timelineItemDisplay = (item) => ({
|
|
@@ -5363,24 +5352,6 @@ function timelineMission(args) {
|
|
|
5363
5352
|
...timelineItemDisplay(item),
|
|
5364
5353
|
}));
|
|
5365
5354
|
const nextMove = missionTimelineNextMove(mission, currentLanding);
|
|
5366
|
-
const generatedAt = stampIso();
|
|
5367
|
-
let artifactPath = null;
|
|
5368
|
-
let prunePreview = null;
|
|
5369
|
-
if (write || prunePreviewRequested) {
|
|
5370
|
-
try {
|
|
5371
|
-
prunePreview = pruneRuns(root, { keepNewest: 200, keepDays: 14 });
|
|
5372
|
-
} catch (error) {
|
|
5373
|
-
prunePreview = { error: error.message || String(error) };
|
|
5374
|
-
}
|
|
5375
|
-
}
|
|
5376
|
-
if (write) {
|
|
5377
|
-
const outPath = outputPath
|
|
5378
|
-
? path.resolve(root, outputPath)
|
|
5379
|
-
: defaultMissionTimelinePath(root, mission);
|
|
5380
|
-
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
5381
|
-
fs.writeFileSync(outPath, missionTimelineMarkdown(mission, timeline, { prunePreview, generatedAt, timelineMeta: timelineResult.meta }), 'utf8');
|
|
5382
|
-
artifactPath = path.relative(root, outPath);
|
|
5383
|
-
}
|
|
5384
5355
|
const pruneSummary = missionTimelinePruneSummaryObject(prunePreview);
|
|
5385
5356
|
const pruneDisplay = {
|
|
5386
5357
|
label: 'Prune preview',
|
|
@@ -5645,6 +5616,10 @@ function timelineMission(args) {
|
|
|
5645
5616
|
if (artifactPath) lines.push(`Saved: ${artifactPath}`);
|
|
5646
5617
|
const pruneLine = missionTimelinePruneSummaryLine(prunePreview);
|
|
5647
5618
|
if (pruneLine) lines.push(pruneLine);
|
|
5619
|
+
return { payload, lines };
|
|
5620
|
+
}
|
|
5621
|
+
|
|
5622
|
+
function renderTimeline(payload, lines, asJson) {
|
|
5648
5623
|
printJsonOrText(payload, lines, asJson);
|
|
5649
5624
|
}
|
|
5650
5625
|
|
|
@@ -5814,8 +5789,8 @@ function watchMission(args) {
|
|
|
5814
5789
|
return;
|
|
5815
5790
|
}
|
|
5816
5791
|
const ref = stripKnownFlags(args, ['--interval', '--idle-every'], [])[0] || '';
|
|
5817
|
-
const intervalSeconds = Math.max(1,
|
|
5818
|
-
const idleEverySeconds = Math.max(1,
|
|
5792
|
+
const intervalSeconds = Math.max(1, readIntFlag(args, '--interval', 2) || 2);
|
|
5793
|
+
const idleEverySeconds = Math.max(1, readIntFlag(args, '--idle-every', 30) || 30);
|
|
5819
5794
|
const loadTargets = () => {
|
|
5820
5795
|
if (ref) {
|
|
5821
5796
|
const mission = resolveMission(ref);
|
|
@@ -6047,14 +6022,12 @@ function missionProtectedTags(mission, root = process.cwd()) {
|
|
|
6047
6022
|
...(Array.isArray(mission?.task_ids) ? mission.task_ids : []),
|
|
6048
6023
|
].filter(Boolean).map(String));
|
|
6049
6024
|
if (!taskRefs.size) return tags;
|
|
6050
|
-
|
|
6051
|
-
|
|
6052
|
-
|
|
6053
|
-
|
|
6054
|
-
|
|
6055
|
-
|
|
6056
|
-
}
|
|
6057
|
-
} catch {}
|
|
6025
|
+
const projection = readJson(path.join(root, '.atris', 'state', 'tasks.projection.json'));
|
|
6026
|
+
for (const task of Array.isArray(projection?.tasks) ? projection.tasks : []) {
|
|
6027
|
+
if (!task || ![task.id, task.display_id, task.legacy_ref].some((ref) => taskRefs.has(String(ref || '')))) continue;
|
|
6028
|
+
if (task.tag) tags.push(task.tag);
|
|
6029
|
+
if (Array.isArray(task.tags)) tags.push(...task.tags);
|
|
6030
|
+
}
|
|
6058
6031
|
return tags;
|
|
6059
6032
|
}
|
|
6060
6033
|
|
|
@@ -6118,18 +6091,13 @@ function captureMissionWorktreeBaseline(mission, root = process.cwd()) {
|
|
|
6118
6091
|
dirty_hash: snapshot.dirty_hash,
|
|
6119
6092
|
paths: Array.from(paths).sort(),
|
|
6120
6093
|
};
|
|
6121
|
-
|
|
6122
|
-
fs.writeFileSync(baselineFile, JSON.stringify(baseline, null, 2) + '\n', 'utf8');
|
|
6094
|
+
writeJson(baselineFile, baseline);
|
|
6123
6095
|
return baseline;
|
|
6124
6096
|
}
|
|
6125
6097
|
|
|
6126
6098
|
function loadMissionWorktreeBaseline(missionId, root = process.cwd()) {
|
|
6127
|
-
|
|
6128
|
-
|
|
6129
|
-
return Array.isArray(baseline?.paths) ? baseline : null;
|
|
6130
|
-
} catch {
|
|
6131
|
-
return null;
|
|
6132
|
-
}
|
|
6099
|
+
const baseline = readJson(missionBaselinePath(missionId, root));
|
|
6100
|
+
return Array.isArray(baseline?.paths) ? baseline : null;
|
|
6133
6101
|
}
|
|
6134
6102
|
|
|
6135
6103
|
// Closed missions no longer tick, so the sidecar is dead weight; prune it and
|
|
@@ -6208,6 +6176,12 @@ const MISSION_RUN_DEFAULTS = {
|
|
|
6208
6176
|
backoff: { initialMs: 30_000, maxMs: 10 * 60_000, factor: 2, jitter: 0.3 },
|
|
6209
6177
|
};
|
|
6210
6178
|
|
|
6179
|
+
// Clamp a remaining-wall timeout to [1, capMs]. Used by spawn plumbing so a
|
|
6180
|
+
// nearly-expired wall cannot schedule a zero or multi-hour child wait.
|
|
6181
|
+
function clampTimeoutMs(remainingMs, capMs = MISSION_RUN_DEFAULTS.claudeTimeoutMs) {
|
|
6182
|
+
return Math.min(capMs, Math.max(1, Math.floor(remainingMs)));
|
|
6183
|
+
}
|
|
6184
|
+
|
|
6211
6185
|
// Claude sessions accumulate context across resumed ticks; an always-on
|
|
6212
6186
|
// mission would grow without bound. Continuity lives on disk (receipts, logs,
|
|
6213
6187
|
// now.md), so a healthy session is disposable: rotate to a fresh one every N
|
|
@@ -6217,6 +6191,25 @@ const CLAUDE_SESSION_CONTEXT_ROTATE_TICKS = Math.max(
|
|
|
6217
6191
|
Number(process.env.ATRIS_CLAUDE_SESSION_ROTATE_TICKS) || 8,
|
|
6218
6192
|
);
|
|
6219
6193
|
|
|
6194
|
+
// A mission that sits in a protected lane must not tick unattended: the
|
|
6195
|
+
// worker lands its own diffs, so the only safe moment to stop a wrong one is
|
|
6196
|
+
// before the tick fires. Same tag-plus-text routing the task lane uses
|
|
6197
|
+
// (CLI-1189). An operator override is explicit: protected_lane_ack in the
|
|
6198
|
+
// mission metadata, set by a human, or ATRIS_ALLOW_PROTECTED_MISSION=1.
|
|
6199
|
+
function missionProtectedLaneHold(mission) {
|
|
6200
|
+
if (process.env.ATRIS_ALLOW_PROTECTED_MISSION === '1') return null;
|
|
6201
|
+
if (mission?.metadata?.protected_lane_ack) return null;
|
|
6202
|
+
const { DENIED_TAGS } = require('../lib/fleet');
|
|
6203
|
+
const { declaredProtectedLane, sniffedProtectedLane } = require('../lib/auto-accept-certified');
|
|
6204
|
+
const lane = String(mission?.lane || '').toLowerCase();
|
|
6205
|
+
if (DENIED_TAGS.includes(lane)) return { pause_reason: `protected-lane-${lane}` };
|
|
6206
|
+
const probe = { title: mission?.objective, objective: mission?.stop_condition, metadata: mission?.metadata || {} };
|
|
6207
|
+
if (declaredProtectedLane(probe)) return { pause_reason: 'protected-lane-declared' };
|
|
6208
|
+
const sniffed = sniffedProtectedLane(probe);
|
|
6209
|
+
if (sniffed) return { pause_reason: `protected-lane-${sniffed.lane}` };
|
|
6210
|
+
return null;
|
|
6211
|
+
}
|
|
6212
|
+
|
|
6220
6213
|
function runnerUsesCallerSession(runner) {
|
|
6221
6214
|
return new Set(['codex_goal', 'caller_session', 'current_agent']).has(String(runner || '').trim().toLowerCase());
|
|
6222
6215
|
}
|
|
@@ -6339,8 +6332,7 @@ function missionHeartbeatLines(mission, now = new Date()) {
|
|
|
6339
6332
|
}
|
|
6340
6333
|
|
|
6341
6334
|
function missionHasHumanAsks(mission) {
|
|
6342
|
-
return
|
|
6343
|
-
&& mission.human_asks.some((ask) => String(ask || '').trim());
|
|
6335
|
+
return openHumanAsks(mission?.human_asks).length > 0;
|
|
6344
6336
|
}
|
|
6345
6337
|
|
|
6346
6338
|
function missionTaskHumanAcceptWaiting(mission) {
|
|
@@ -6427,7 +6419,10 @@ function missionSelectableForCodexGoal(mission, now = new Date()) {
|
|
|
6427
6419
|
function selectCodexGoalMission(root = process.cwd(), options = {}, now = new Date()) {
|
|
6428
6420
|
const requestedId = String(options.missionId || '').trim();
|
|
6429
6421
|
const candidates = listMissions(root)
|
|
6430
|
-
|
|
6422
|
+
// caller_session/current_agent missions run in this process, but they do
|
|
6423
|
+
// not own the native Codex goal handshake. Keep selection aligned with
|
|
6424
|
+
// ackMissionGoal(), which intentionally accepts only codex_goal runners.
|
|
6425
|
+
.filter((mission) => isCodexGoalMission(mission))
|
|
6431
6426
|
.filter((mission) => missionSelectableForCodexGoal(mission, now));
|
|
6432
6427
|
if (requestedId) {
|
|
6433
6428
|
const exact = candidates.find((mission) => missionMatchesRef(mission, requestedId));
|
|
@@ -6442,10 +6437,6 @@ function selectCodexGoalMission(root = process.cwd(), options = {}, now = new Da
|
|
|
6442
6437
|
const bRank = missionGoalSelectionRank(b);
|
|
6443
6438
|
if (aRank !== bRank) return aRank - bRank;
|
|
6444
6439
|
|
|
6445
|
-
const aCaller = runnerUsesCallerSession(a.runner) ? 1 : 0;
|
|
6446
|
-
const bCaller = runnerUsesCallerSession(b.runner) ? 1 : 0;
|
|
6447
|
-
if (aCaller !== bCaller) return bCaller - aCaller;
|
|
6448
|
-
|
|
6449
6440
|
const aVerifier = effectiveMissionVerifier(a) ? 1 : 0;
|
|
6450
6441
|
const bVerifier = effectiveMissionVerifier(b) ? 1 : 0;
|
|
6451
6442
|
if (aVerifier !== bVerifier) return bVerifier - aVerifier;
|
|
@@ -6738,19 +6729,13 @@ function writeDirectRunCodexGoalRequest(mission, root = process.cwd()) {
|
|
|
6738
6729
|
requested_at: stampIso(),
|
|
6739
6730
|
};
|
|
6740
6731
|
const paths = statePaths(root);
|
|
6741
|
-
|
|
6742
|
-
fs.writeFileSync(paths.codexGoalRequestJson, JSON.stringify(request, null, 2) + '\n', 'utf8');
|
|
6732
|
+
writeJson(paths.codexGoalRequestJson, request);
|
|
6743
6733
|
return request;
|
|
6744
6734
|
}
|
|
6745
6735
|
|
|
6746
6736
|
function readDirectRunCodexGoalRequest(root = process.cwd(), now = new Date()) {
|
|
6747
6737
|
const file = statePaths(root).codexGoalRequestJson;
|
|
6748
|
-
|
|
6749
|
-
try {
|
|
6750
|
-
request = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
6751
|
-
} catch {
|
|
6752
|
-
return null;
|
|
6753
|
-
}
|
|
6738
|
+
const request = readJson(file);
|
|
6754
6739
|
const missionId = String(request?.mission_id || '').trim();
|
|
6755
6740
|
if (!missionId) return null;
|
|
6756
6741
|
const mission = resolveMission(missionId, root);
|
|
@@ -6761,13 +6746,9 @@ function readDirectRunCodexGoalRequest(root = process.cwd(), now = new Date()) {
|
|
|
6761
6746
|
|
|
6762
6747
|
function clearDirectRunCodexGoalRequestForMission(missionId, root = process.cwd()) {
|
|
6763
6748
|
const file = statePaths(root).codexGoalRequestJson;
|
|
6764
|
-
|
|
6765
|
-
|
|
6766
|
-
|
|
6767
|
-
} catch {
|
|
6768
|
-
return false;
|
|
6769
|
-
}
|
|
6770
|
-
if (String(request?.mission_id || '') !== String(missionId || '')) return false;
|
|
6749
|
+
const request = readJson(file);
|
|
6750
|
+
if (!request) return false;
|
|
6751
|
+
if (String(request.mission_id || '') !== String(missionId || '')) return false;
|
|
6771
6752
|
try {
|
|
6772
6753
|
fs.rmSync(file, { force: true });
|
|
6773
6754
|
return true;
|
|
@@ -7085,8 +7066,7 @@ function writeCodexGoalState(payload, root = process.cwd()) {
|
|
|
7085
7066
|
updated_at: stampIso(),
|
|
7086
7067
|
...payload,
|
|
7087
7068
|
};
|
|
7088
|
-
|
|
7089
|
-
fs.writeFileSync(paths.codexGoalJson, JSON.stringify(state, null, 2) + '\n', 'utf8');
|
|
7069
|
+
writeJson(paths.codexGoalJson, state);
|
|
7090
7070
|
|
|
7091
7071
|
const lines = [
|
|
7092
7072
|
'# Codex Goal Controller',
|
|
@@ -7357,8 +7337,7 @@ function writeAtrisGoalState(payload, root = process.cwd()) {
|
|
|
7357
7337
|
updated_at: stampIso(),
|
|
7358
7338
|
...payload,
|
|
7359
7339
|
};
|
|
7360
|
-
|
|
7361
|
-
fs.writeFileSync(paths.atrisGoalJson, JSON.stringify(state, null, 2) + '\n', 'utf8');
|
|
7340
|
+
writeJson(paths.atrisGoalJson, state);
|
|
7362
7341
|
|
|
7363
7342
|
const lines = [
|
|
7364
7343
|
'# Atris Goal Controller',
|
|
@@ -7600,8 +7579,37 @@ function missionJudgmentTopic(mission) {
|
|
|
7600
7579
|
return plain.split(/\s+/).filter(Boolean).slice(0, 14).join(' ');
|
|
7601
7580
|
}
|
|
7602
7581
|
|
|
7582
|
+
// Words that leave a title hanging mid-thought when they land at the end:
|
|
7583
|
+
// articles, prepositions, conjunctions, subordinators, and auxiliary/linking
|
|
7584
|
+
// verbs. A hard 8-word slice of an objective often stops on one of these
|
|
7585
|
+
// ("...runner away from", "...say work is"), which reads as a truncation bug.
|
|
7586
|
+
const DANGLING_TAIL_WORDS = new Set([
|
|
7587
|
+
'a', 'an', 'the',
|
|
7588
|
+
'of', 'to', 'in', 'on', 'at', 'for', 'from', 'with', 'by', 'into', 'onto',
|
|
7589
|
+
'over', 'under', 'about', 'after', 'before', 'between', 'through', 'during',
|
|
7590
|
+
'without', 'within', 'away', 'up', 'off', 'out',
|
|
7591
|
+
'and', 'or', 'but', 'nor', 'so', 'yet', 'when', 'while', 'if', 'that',
|
|
7592
|
+
'which', 'because', 'although', 'though', 'since', 'whether', 'as',
|
|
7593
|
+
'is', 'are', 'was', 'were', 'be', 'been', 'being', 'am', 'has', 'have',
|
|
7594
|
+
'had', 'do', 'does', 'did', 'will', 'would', 'shall', 'should', 'can',
|
|
7595
|
+
'could', 'may', 'might', 'must',
|
|
7596
|
+
]);
|
|
7597
|
+
|
|
7598
|
+
// Drop trailing function words so the title ends where a thought ends, never
|
|
7599
|
+
// on a preposition or auxiliary. Falls back to the raw slice if trimming would
|
|
7600
|
+
// empty the title.
|
|
7601
|
+
function trimDanglingTail(words) {
|
|
7602
|
+
const trimmed = [...words];
|
|
7603
|
+
while (trimmed.length > 1 && DANGLING_TAIL_WORDS.has(trimmed[trimmed.length - 1].toLowerCase())) {
|
|
7604
|
+
trimmed.pop();
|
|
7605
|
+
}
|
|
7606
|
+
return trimmed.length ? trimmed : words;
|
|
7607
|
+
}
|
|
7608
|
+
|
|
7603
7609
|
function missionJudgmentTitle(mission) {
|
|
7604
|
-
const words =
|
|
7610
|
+
const words = trimDanglingTail(
|
|
7611
|
+
missionJudgmentTopic(mission).split(/\s+/).filter(Boolean).slice(0, 8),
|
|
7612
|
+
);
|
|
7605
7613
|
const title = words.join(' ') || 'Mission needs a new direction';
|
|
7606
7614
|
return title.charAt(0).toUpperCase() + title.slice(1);
|
|
7607
7615
|
}
|
|
@@ -7766,12 +7774,11 @@ function missionDriverPaths(mission, root = process.cwd()) {
|
|
|
7766
7774
|
}
|
|
7767
7775
|
|
|
7768
7776
|
function readMissionDriverState(stateFile) {
|
|
7769
|
-
|
|
7777
|
+
return readJson(stateFile, {}) || {};
|
|
7770
7778
|
}
|
|
7771
7779
|
|
|
7772
7780
|
function writeMissionDriverState(stateFile, state) {
|
|
7773
|
-
|
|
7774
|
-
fs.writeFileSync(stateFile, JSON.stringify(state, null, 2) + '\n', 'utf8');
|
|
7781
|
+
writeJson(stateFile, state);
|
|
7775
7782
|
}
|
|
7776
7783
|
|
|
7777
7784
|
function missionDriverError(error) {
|
|
@@ -7864,10 +7871,8 @@ function installDetachedMissionDriverLifecycle(mission, root = process.cwd()) {
|
|
|
7864
7871
|
|
|
7865
7872
|
function missionDriverHealth(mission, root = process.cwd()) {
|
|
7866
7873
|
const paths = missionDriverPaths(mission, root);
|
|
7867
|
-
|
|
7868
|
-
|
|
7869
|
-
try { driverState = JSON.parse(fs.readFileSync(paths.stateFile, 'utf8')); } catch {}
|
|
7870
|
-
try { lockState = JSON.parse(fs.readFileSync(paths.lockFile, 'utf8')); } catch {}
|
|
7874
|
+
const driverState = readJson(paths.stateFile);
|
|
7875
|
+
const lockState = readJson(paths.lockFile);
|
|
7871
7876
|
const candidates = [
|
|
7872
7877
|
driverState ? { ...driverState, source: 'driver_state' } : null,
|
|
7873
7878
|
lockState ? { ...lockState, source: 'mission_lock' } : null,
|
|
@@ -7902,8 +7907,7 @@ function acquireMissionLock(missionId, root = process.cwd(), options = {}) {
|
|
|
7902
7907
|
return { ok: true, lockFile, fd, missionId, driverPid: process.pid, startedAt, recordLength: record.length };
|
|
7903
7908
|
} catch (e) {
|
|
7904
7909
|
if (e.code === 'EEXIST') {
|
|
7905
|
-
|
|
7906
|
-
try { info = JSON.parse(fs.readFileSync(lockFile, 'utf8') || '{}'); } catch {}
|
|
7910
|
+
const info = readJson(lockFile, {}) || {};
|
|
7907
7911
|
const holderPid = Number(info.pid);
|
|
7908
7912
|
const holderKnown = Number.isInteger(holderPid) && holderPid > 0;
|
|
7909
7913
|
// A lock is created empty (openSync 'wx') and its pid record is written a
|
|
@@ -8184,8 +8188,9 @@ function buildTickPrompt(mission, tickIndex, maxTicks, frozen, pings = []) {
|
|
|
8184
8188
|
if (mission.task_ids?.length) {
|
|
8185
8189
|
lines.push('', `## Task ids`, mission.task_ids.map((t) => `- ${t}`).join('\n'));
|
|
8186
8190
|
}
|
|
8187
|
-
|
|
8188
|
-
|
|
8191
|
+
const humanAsks = openHumanAsks(mission.human_asks);
|
|
8192
|
+
if (humanAsks.length) {
|
|
8193
|
+
lines.push('', `## Human asks (don't act on these — surface them)`, humanAsks.map((ask) => `- ${ask.text}`).join('\n'));
|
|
8189
8194
|
}
|
|
8190
8195
|
return lines.join('\n');
|
|
8191
8196
|
}
|
|
@@ -8761,16 +8766,28 @@ function cappedClaudeReceiptText(text, limit = 4000) {
|
|
|
8761
8766
|
return clean.slice(0, limit - 16).trimEnd() + '\n...[truncated]';
|
|
8762
8767
|
}
|
|
8763
8768
|
|
|
8764
|
-
async function
|
|
8769
|
+
async function parseAndValidateMissionRunPhase(args) {
|
|
8765
8770
|
const asJson = wantsJson(args);
|
|
8771
|
+
const context = {
|
|
8772
|
+
args,
|
|
8773
|
+
asJson,
|
|
8774
|
+
cwd: process.cwd(),
|
|
8775
|
+
handled: false,
|
|
8776
|
+
returnValue: undefined,
|
|
8777
|
+
lock: null,
|
|
8778
|
+
error: null,
|
|
8779
|
+
};
|
|
8766
8780
|
if (hasFlag(args, '--help') || hasFlag(args, '-h')) {
|
|
8767
8781
|
help();
|
|
8768
|
-
|
|
8782
|
+
context.handled = true;
|
|
8783
|
+
return context;
|
|
8769
8784
|
}
|
|
8770
8785
|
if (hasFlag(args, '--cloud')) {
|
|
8771
8786
|
const result = await runCloudMissionCommand(args);
|
|
8772
8787
|
process.exitCode = result.exitCode;
|
|
8773
|
-
|
|
8788
|
+
context.handled = true;
|
|
8789
|
+
context.returnValue = result;
|
|
8790
|
+
return context;
|
|
8774
8791
|
}
|
|
8775
8792
|
// --fleet: staff every idle capable engine on the board's claimable
|
|
8776
8793
|
// safe-lane tasks, build in parallel worktrees, land serially. Humble flag,
|
|
@@ -8789,7 +8806,8 @@ async function runMission(args) {
|
|
|
8789
8806
|
});
|
|
8790
8807
|
if (asJson) console.log(JSON.stringify(flight, null, 2));
|
|
8791
8808
|
process.exitCode = flight.paused && flight.paused.length > 0 ? 1 : 0;
|
|
8792
|
-
|
|
8809
|
+
context.handled = true;
|
|
8810
|
+
return context;
|
|
8793
8811
|
}
|
|
8794
8812
|
const dueMode = hasFlag(args, '--due');
|
|
8795
8813
|
const headlessOnly = hasFlag(args, '--headless');
|
|
@@ -8825,22 +8843,70 @@ async function runMission(args) {
|
|
|
8825
8843
|
const ref = input.ref;
|
|
8826
8844
|
const runArgs = input.args;
|
|
8827
8845
|
|
|
8846
|
+
Object.assign(context, {
|
|
8847
|
+
dueMode,
|
|
8848
|
+
headlessOnly,
|
|
8849
|
+
selfDrive,
|
|
8850
|
+
skipClaude,
|
|
8851
|
+
verifyEach,
|
|
8852
|
+
completeOnPass,
|
|
8853
|
+
skipDrain,
|
|
8854
|
+
createNext,
|
|
8855
|
+
budgetTier,
|
|
8856
|
+
maxTicksFlag,
|
|
8857
|
+
maxTicks,
|
|
8858
|
+
maxWallFlag,
|
|
8859
|
+
explicitMaxWallSeconds,
|
|
8860
|
+
runBudgetContract,
|
|
8861
|
+
maxWallSeconds,
|
|
8862
|
+
cadenceOverride,
|
|
8863
|
+
runnerOverride,
|
|
8864
|
+
modelOverride,
|
|
8865
|
+
runtimeView,
|
|
8866
|
+
input,
|
|
8867
|
+
ref,
|
|
8868
|
+
runArgs,
|
|
8869
|
+
});
|
|
8870
|
+
return context;
|
|
8871
|
+
}
|
|
8872
|
+
|
|
8873
|
+
async function resolveMissionRunPhase(context) {
|
|
8874
|
+
const {
|
|
8875
|
+
args,
|
|
8876
|
+
asJson,
|
|
8877
|
+
cwd,
|
|
8878
|
+
dueMode,
|
|
8879
|
+
headlessOnly,
|
|
8880
|
+
budgetTier,
|
|
8881
|
+
maxTicksFlag,
|
|
8882
|
+
maxWallFlag,
|
|
8883
|
+
explicitMaxWallSeconds,
|
|
8884
|
+
runBudgetContract,
|
|
8885
|
+
runtimeView,
|
|
8886
|
+
input,
|
|
8887
|
+
ref,
|
|
8888
|
+
runArgs,
|
|
8889
|
+
} = context;
|
|
8890
|
+
let { maxTicks, maxWallSeconds } = context;
|
|
8891
|
+
|
|
8828
8892
|
if (!dueMode && !ref) {
|
|
8829
8893
|
if (asJson || !process.stdin.isTTY || !process.stderr.isTTY) {
|
|
8830
8894
|
missionRunInputRequired(asJson, input.owner);
|
|
8831
8895
|
}
|
|
8832
8896
|
const prompted = await promptMissionRunInput(runArgs);
|
|
8833
8897
|
await startMissionFromRunObjective(prompted.objective, prompted.args);
|
|
8898
|
+
context.handled = true;
|
|
8834
8899
|
return;
|
|
8835
8900
|
}
|
|
8836
8901
|
|
|
8837
|
-
let mission = dueMode && !ref ? selectDueMission(
|
|
8902
|
+
let mission = dueMode && !ref ? selectDueMission(cwd, new Date(), { headlessOnly }) : resolveMission(ref, cwd);
|
|
8838
8903
|
if (!mission && dueMode && !ref) {
|
|
8839
8904
|
printJsonOrText(
|
|
8840
8905
|
{ ok: true, action: 'run_skipped', reason: 'no_due_mission', mission: null },
|
|
8841
8906
|
['No due mission found.'],
|
|
8842
8907
|
asJson,
|
|
8843
8908
|
);
|
|
8909
|
+
context.handled = true;
|
|
8844
8910
|
return;
|
|
8845
8911
|
}
|
|
8846
8912
|
// BCK-1319: a bare single token that looks like an id/suffix/number (no
|
|
@@ -8854,6 +8920,7 @@ async function runMission(args) {
|
|
|
8854
8920
|
}
|
|
8855
8921
|
if (!mission && ref) {
|
|
8856
8922
|
await startMissionFromRunObjective(ref, runArgs);
|
|
8923
|
+
context.handled = true;
|
|
8857
8924
|
return;
|
|
8858
8925
|
}
|
|
8859
8926
|
if (!mission) {
|
|
@@ -8867,7 +8934,8 @@ async function runMission(args) {
|
|
|
8867
8934
|
} else if (!maxWallFlag && Number(mission.budget_contract?.requested_seconds) > 0) {
|
|
8868
8935
|
maxWallSeconds = Math.max(60, Number(mission.budget_contract.requested_seconds));
|
|
8869
8936
|
}
|
|
8870
|
-
const detachedDriverLifecycle = installDetachedMissionDriverLifecycle(mission,
|
|
8937
|
+
const detachedDriverLifecycle = installDetachedMissionDriverLifecycle(mission, cwd);
|
|
8938
|
+
context.detachedDriverLifecycle = detachedDriverLifecycle;
|
|
8871
8939
|
if (['complete', 'stopped'].includes(mission.status)) {
|
|
8872
8940
|
detachedDriverLifecycle?.setExitReason(mission.status);
|
|
8873
8941
|
if (asJson) {
|
|
@@ -8876,18 +8944,20 @@ async function runMission(args) {
|
|
|
8876
8944
|
[],
|
|
8877
8945
|
true,
|
|
8878
8946
|
);
|
|
8947
|
+
context.handled = true;
|
|
8879
8948
|
return;
|
|
8880
8949
|
}
|
|
8881
8950
|
console.error(`Mission ${mission.id} is ${mission.status}; nothing to run.`);
|
|
8882
8951
|
process.exit(0);
|
|
8883
8952
|
}
|
|
8884
8953
|
if (hasFlag(args, '--detach')) {
|
|
8885
|
-
detachMissionRun(args, mission,
|
|
8954
|
+
detachMissionRun(args, mission, cwd, asJson);
|
|
8955
|
+
context.handled = true;
|
|
8886
8956
|
return;
|
|
8887
8957
|
}
|
|
8888
8958
|
|
|
8889
8959
|
const nativeGoalRunOptions = codexNativeGoalOptionsFromArgs(args);
|
|
8890
|
-
const autoAck = maybeAutoAckCodexNativeGoal(mission,
|
|
8960
|
+
const autoAck = maybeAutoAckCodexNativeGoal(mission, cwd, nativeGoalRunOptions);
|
|
8891
8961
|
if (autoAck) mission = autoAck.saved;
|
|
8892
8962
|
maybeBlockUntilCodexNativeGoalStarted(runtimeView(mission), asJson, nativeGoalRunOptions);
|
|
8893
8963
|
|
|
@@ -8896,37 +8966,63 @@ async function runMission(args) {
|
|
|
8896
8966
|
// (e.g. --engine cursor) never invokes claude. The in-lock probe below runs
|
|
8897
8967
|
// after applyMissionRunnerProfile and checks the right binary.
|
|
8898
8968
|
|
|
8899
|
-
const lock = acquireMissionLock(mission.id);
|
|
8969
|
+
const lock = acquireMissionLock(mission.id, cwd);
|
|
8900
8970
|
if (!lock.ok) {
|
|
8901
8971
|
exitMissionError(`another driver is already running mission ${mission.id} (pid ${lock.holder?.pid || '?'})`, 3, asJson);
|
|
8902
8972
|
}
|
|
8903
8973
|
|
|
8904
|
-
|
|
8905
|
-
|
|
8906
|
-
|
|
8907
|
-
|
|
8908
|
-
|
|
8909
|
-
|
|
8910
|
-
|
|
8911
|
-
|
|
8912
|
-
|
|
8913
|
-
|
|
8974
|
+
Object.assign(context, {
|
|
8975
|
+
mission,
|
|
8976
|
+
maxTicks,
|
|
8977
|
+
maxWallSeconds,
|
|
8978
|
+
nativeGoalRunOptions,
|
|
8979
|
+
lock,
|
|
8980
|
+
pauseReason: null,
|
|
8981
|
+
sessionId: null,
|
|
8982
|
+
pendingSessionId: null,
|
|
8983
|
+
ranTicks: 0,
|
|
8984
|
+
ticks: [],
|
|
8985
|
+
onSig: null,
|
|
8986
|
+
restoreRunnerProfile: null,
|
|
8987
|
+
blocker: null,
|
|
8988
|
+
});
|
|
8989
|
+
}
|
|
8914
8990
|
|
|
8915
|
-
|
|
8916
|
-
const
|
|
8991
|
+
async function executeMissionRunTicksPhase(context) {
|
|
8992
|
+
const {
|
|
8993
|
+
asJson,
|
|
8994
|
+
cwd,
|
|
8995
|
+
explicitMaxWallSeconds,
|
|
8996
|
+
runBudgetContract,
|
|
8997
|
+
runtimeView,
|
|
8998
|
+
runnerOverride,
|
|
8999
|
+
skipClaude,
|
|
9000
|
+
verifyEach,
|
|
9001
|
+
completeOnPass,
|
|
9002
|
+
skipDrain,
|
|
9003
|
+
maxTicksFlag,
|
|
9004
|
+
budgetTier,
|
|
9005
|
+
cadenceOverride,
|
|
9006
|
+
detachedDriverLifecycle,
|
|
9007
|
+
lock,
|
|
9008
|
+
nativeGoalRunOptions,
|
|
9009
|
+
} = context;
|
|
9010
|
+
let { mission, maxTicks, maxWallSeconds, pauseReason, sessionId, pendingSessionId, ranTicks } = context;
|
|
9011
|
+
const ticks = context.ticks;
|
|
8917
9012
|
const controller = new AbortController();
|
|
8918
|
-
|
|
9013
|
+
context.controller = controller;
|
|
9014
|
+
context.onSig = (signal) => {
|
|
8919
9015
|
detachedDriverLifecycle?.setExitReason(`signal-${String(signal || 'abort').toLowerCase()}`);
|
|
8920
9016
|
controller.abort();
|
|
8921
9017
|
};
|
|
8922
|
-
process.on('SIGINT', onSig);
|
|
8923
|
-
process.on('SIGTERM', onSig);
|
|
9018
|
+
process.on('SIGINT', context.onSig);
|
|
9019
|
+
process.on('SIGTERM', context.onSig);
|
|
8924
9020
|
|
|
8925
9021
|
// Re-read inside the lock. The initial resolveMission ran pre-lock, so a concurrent
|
|
8926
9022
|
// `mission tick` could have written between resolveMission and acquireMissionLock.
|
|
8927
9023
|
// Derive sessionId, pendingSessionId, and the frozen contract from the fresh record
|
|
8928
9024
|
// so a fast tick's writes can't be silently overwritten by this run loop.
|
|
8929
|
-
mission = resolveMission(mission.id) || mission;
|
|
9025
|
+
mission = resolveMission(mission.id, cwd) || mission;
|
|
8930
9026
|
if (explicitMaxWallSeconds && runBudgetContract) {
|
|
8931
9027
|
mission = saveMission({
|
|
8932
9028
|
...mission,
|
|
@@ -8941,10 +9037,12 @@ async function runMission(args) {
|
|
|
8941
9037
|
if (['complete', 'stopped'].includes(mission.status)) {
|
|
8942
9038
|
detachedDriverLifecycle?.setExitReason(mission.status);
|
|
8943
9039
|
console.error(`Mission ${mission.id} is ${mission.status}; nothing to run.`);
|
|
9040
|
+
context.handled = true;
|
|
8944
9041
|
return;
|
|
8945
9042
|
}
|
|
8946
9043
|
if (returnIfCodexNativeGoalNotStarted(runtimeMission, asJson, nativeGoalRunOptions)) {
|
|
8947
9044
|
detachedDriverLifecycle?.setExitReason('native-goal-not-started');
|
|
9045
|
+
context.handled = true;
|
|
8948
9046
|
return;
|
|
8949
9047
|
}
|
|
8950
9048
|
if (mission.status === 'paused') {
|
|
@@ -8961,7 +9059,7 @@ async function runMission(args) {
|
|
|
8961
9059
|
sessionId = mission.claude_session_id || null;
|
|
8962
9060
|
pendingSessionId = mission.pending_session_id || null;
|
|
8963
9061
|
const autoRunner = String(runtimeMission.runner || '').trim().toLowerCase() === MISSION_AUTO_RUNNER;
|
|
8964
|
-
restoreRunnerProfile = autoRunner ? () => {} : applyMissionRunnerProfile(runtimeMission.runner);
|
|
9062
|
+
context.restoreRunnerProfile = autoRunner ? () => {} : applyMissionRunnerProfile(runtimeMission.runner);
|
|
8965
9063
|
const callerSessionRunner = runnerUsesCallerSession(runtimeMission.runner);
|
|
8966
9064
|
const runnerName = String(runtimeMission.runner || '').trim().toLowerCase();
|
|
8967
9065
|
const atris2Runner = runnerName === 'atris2';
|
|
@@ -9028,7 +9126,7 @@ async function runMission(args) {
|
|
|
9028
9126
|
// Re-read before measuring the wall. Detached full-budget drivers use
|
|
9029
9127
|
// the mission contract as their wall so process startup drift cannot
|
|
9030
9128
|
// shorten the promised work window.
|
|
9031
|
-
mission = resolveMission(mission.id) || mission;
|
|
9129
|
+
mission = resolveMission(mission.id, cwd) || mission;
|
|
9032
9130
|
runtimeMission = runtimeView(mission);
|
|
9033
9131
|
const contractualRemaining = detachedDriverLifecycle && mission.always_on && missionSpendsFullBudget(mission)
|
|
9034
9132
|
? missionFullBudgetRemainingSeconds(mission)
|
|
@@ -9045,6 +9143,8 @@ async function runMission(args) {
|
|
|
9045
9143
|
if (['complete', 'stopped', 'paused'].includes(mission.status)) { pauseReason = mission.status; break; }
|
|
9046
9144
|
if (effectiveMissionVerifier(mission) !== storedVerifier) { pauseReason = 'verifier-mutated'; break; }
|
|
9047
9145
|
if ((mission.lane || 'workspace') !== frozen.lane) { pauseReason = 'lane-mutated'; break; }
|
|
9146
|
+
const protectedHold = missionProtectedLaneHold(mission);
|
|
9147
|
+
if (protectedHold) { pauseReason = protectedHold.pause_reason; break; }
|
|
9048
9148
|
|
|
9049
9149
|
const tickIdx = Number(mission.last_tick_index || 0) + 1;
|
|
9050
9150
|
const tickStart = stampIso();
|
|
@@ -9195,9 +9295,9 @@ async function runMission(args) {
|
|
|
9195
9295
|
const runClaudeSession = () => spawnClaudeTick(tickRuntimeMission, {
|
|
9196
9296
|
sessionMode, sessionId: useId, cwd, signal: controller.signal,
|
|
9197
9297
|
missionLock: lock,
|
|
9198
|
-
timeoutMs:
|
|
9298
|
+
timeoutMs: clampTimeoutMs(
|
|
9299
|
+
(maxWallSeconds - ((Date.now() - startedAt) / 1000)) * 1000,
|
|
9199
9300
|
MISSION_RUN_DEFAULTS.claudeTimeoutMs,
|
|
9200
|
-
Math.max(1, Math.floor((maxWallSeconds - ((Date.now() - startedAt) / 1000)) * 1000)),
|
|
9201
9301
|
),
|
|
9202
9302
|
prompt,
|
|
9203
9303
|
model: resolveClaudeRunnerModel(tickRuntimeMission),
|
|
@@ -9304,39 +9404,22 @@ async function runMission(args) {
|
|
|
9304
9404
|
if (engineHealth) result.engine_health = engineHealth.health;
|
|
9305
9405
|
}
|
|
9306
9406
|
|
|
9307
|
-
|
|
9308
|
-
|
|
9309
|
-
|
|
9310
|
-
|
|
9311
|
-
|
|
9312
|
-
|
|
9313
|
-
|
|
9314
|
-
|
|
9315
|
-
|
|
9316
|
-
|
|
9317
|
-
|
|
9318
|
-
|
|
9319
|
-
|
|
9320
|
-
|
|
9321
|
-
|
|
9322
|
-
pauseReason = 'protected-lane-review';
|
|
9323
|
-
}
|
|
9324
|
-
}
|
|
9325
|
-
let verifierResult = null;
|
|
9407
|
+
context.pauseReason = pauseReason;
|
|
9408
|
+
context.currentTick = {
|
|
9409
|
+
mission,
|
|
9410
|
+
result,
|
|
9411
|
+
tickWorktreeBefore,
|
|
9412
|
+
tickRuntimeMission,
|
|
9413
|
+
tickSkipWorker,
|
|
9414
|
+
tickIdx,
|
|
9415
|
+
frozen,
|
|
9416
|
+
};
|
|
9417
|
+
await verifyMissionRunTickPhase(context);
|
|
9418
|
+
result = context.currentTick.result;
|
|
9419
|
+
const verifierResult = context.currentTick.verifierResult;
|
|
9420
|
+
pauseReason = context.pauseReason;
|
|
9421
|
+
context.currentTick = null;
|
|
9326
9422
|
let receiptPath = null;
|
|
9327
|
-
if (result.status === 'ran' && verifyEach) {
|
|
9328
|
-
if (frozen.verifier) {
|
|
9329
|
-
verifierResult = runVerifier(frozen.verifier);
|
|
9330
|
-
} else if (!tickSkipWorker) {
|
|
9331
|
-
verifierResult = await runEngineVerifier(tickRuntimeMission, {
|
|
9332
|
-
cwd,
|
|
9333
|
-
signal: controller.signal,
|
|
9334
|
-
tickIndex: tickIdx,
|
|
9335
|
-
});
|
|
9336
|
-
}
|
|
9337
|
-
if (verifierResult) result.verifier_passed = verifierResult.passed;
|
|
9338
|
-
}
|
|
9339
|
-
stampMissionRunnerBrief(cwd, result.claude?.brief_id, result, verifierResult);
|
|
9340
9423
|
|
|
9341
9424
|
// Review-lane drain: always-on loops sweep the agent-safe review actions
|
|
9342
9425
|
// each tick so proof-backed work reaches certified on cadence with zero
|
|
@@ -9477,7 +9560,7 @@ async function runMission(args) {
|
|
|
9477
9560
|
break;
|
|
9478
9561
|
}
|
|
9479
9562
|
|
|
9480
|
-
if (
|
|
9563
|
+
if (['mission-diff-unreadable', 'protected-lane-review'].includes(pauseReason)) break;
|
|
9481
9564
|
if (callerSessionRunner && result.status === 'ran') break;
|
|
9482
9565
|
if (newStatus === 'complete' || (newStatus === 'ready' && !mission.always_on && !fullBudgetMode)) break;
|
|
9483
9566
|
// BCK-1324: two consecutive ticks that each self-report "ran" but leave no
|
|
@@ -9525,7 +9608,7 @@ async function runMission(args) {
|
|
|
9525
9608
|
if (lastTick && lastTick.status !== 'ran' && !missionRunKeepsRetryingError(lastTick.reason)) pauseReason = 'max-ticks-reached';
|
|
9526
9609
|
}
|
|
9527
9610
|
|
|
9528
|
-
mission = resolveMission(mission.id) || mission;
|
|
9611
|
+
mission = resolveMission(mission.id, cwd) || mission;
|
|
9529
9612
|
const remainingBudgetSeconds = missionFullBudgetRemainingSeconds(mission);
|
|
9530
9613
|
const explicitExit = ['complete', 'stopped', 'paused'].includes(String(mission.status || ''));
|
|
9531
9614
|
const healthyCycleBoundary = !pauseReason || pauseReason === 'max-ticks-reached';
|
|
@@ -9557,6 +9640,97 @@ async function runMission(args) {
|
|
|
9557
9640
|
cycleTickLimit = ticks.length + effectiveMaxTicks;
|
|
9558
9641
|
}
|
|
9559
9642
|
|
|
9643
|
+
Object.assign(context, {
|
|
9644
|
+
mission,
|
|
9645
|
+
pauseReason,
|
|
9646
|
+
sessionId,
|
|
9647
|
+
pendingSessionId,
|
|
9648
|
+
ranTicks,
|
|
9649
|
+
frozen,
|
|
9650
|
+
runWorktreeBefore,
|
|
9651
|
+
runWorktreeBaseline,
|
|
9652
|
+
effectiveMaxTicks,
|
|
9653
|
+
startedAt,
|
|
9654
|
+
continuationGoal,
|
|
9655
|
+
runtimeMission,
|
|
9656
|
+
});
|
|
9657
|
+
}
|
|
9658
|
+
|
|
9659
|
+
async function verifyMissionRunTickPhase(context) {
|
|
9660
|
+
const { cwd, verifyEach, controller, currentTick } = context;
|
|
9661
|
+
let { result } = currentTick;
|
|
9662
|
+
|
|
9663
|
+
// An explicit verifier is safe and useful in no-worker mode. The fallback
|
|
9664
|
+
// engine verifier is worker activity, so no-worker ticks must not launch it.
|
|
9665
|
+
const runnerGuard = result.claude?.protected_lane_guard || result.atris2?.protected_lane_guard || null;
|
|
9666
|
+
if (result.status === 'ran' || runnerGuard) {
|
|
9667
|
+
const protectedLaneGuard = runnerGuard?.allowed === false
|
|
9668
|
+
? runnerGuard
|
|
9669
|
+
: inspectMissionTickProtectedDiff(currentTick.mission, currentTick.tickWorktreeBefore, cwd);
|
|
9670
|
+
result.protected_lane_guard = protectedLaneGuard;
|
|
9671
|
+
if (!protectedLaneGuard.allowed) {
|
|
9672
|
+
const guardPauseReason = protectedLaneGuard.unreadable
|
|
9673
|
+
? 'mission-diff-unreadable'
|
|
9674
|
+
: 'protected-lane-review';
|
|
9675
|
+
result.status = protectedLaneGuard.status;
|
|
9676
|
+
result.reason = guardPauseReason;
|
|
9677
|
+
result.ran = false;
|
|
9678
|
+
context.pauseReason = guardPauseReason;
|
|
9679
|
+
}
|
|
9680
|
+
}
|
|
9681
|
+
|
|
9682
|
+
let verifierResult = null;
|
|
9683
|
+
if (result.status === 'ran' && verifyEach) {
|
|
9684
|
+
if (currentTick.frozen.verifier) {
|
|
9685
|
+
verifierResult = runVerifier(currentTick.frozen.verifier);
|
|
9686
|
+
} else if (!currentTick.tickSkipWorker) {
|
|
9687
|
+
verifierResult = await runEngineVerifier(currentTick.tickRuntimeMission, {
|
|
9688
|
+
cwd,
|
|
9689
|
+
signal: controller.signal,
|
|
9690
|
+
tickIndex: currentTick.tickIdx,
|
|
9691
|
+
});
|
|
9692
|
+
}
|
|
9693
|
+
if (verifierResult) result.verifier_passed = verifierResult.passed;
|
|
9694
|
+
}
|
|
9695
|
+
stampMissionRunnerBrief(cwd, result.claude?.brief_id, result, verifierResult);
|
|
9696
|
+
currentTick.result = result;
|
|
9697
|
+
currentTick.verifierResult = verifierResult;
|
|
9698
|
+
}
|
|
9699
|
+
|
|
9700
|
+
function completeMissionRunPhase(context) {
|
|
9701
|
+
const {
|
|
9702
|
+
asJson,
|
|
9703
|
+
cwd,
|
|
9704
|
+
selfDrive,
|
|
9705
|
+
createNext,
|
|
9706
|
+
runtimeView,
|
|
9707
|
+
runnerOverride,
|
|
9708
|
+
runBudgetContract,
|
|
9709
|
+
detachedDriverLifecycle,
|
|
9710
|
+
lock,
|
|
9711
|
+
frozen,
|
|
9712
|
+
runWorktreeBefore,
|
|
9713
|
+
runWorktreeBaseline,
|
|
9714
|
+
effectiveMaxTicks,
|
|
9715
|
+
startedAt,
|
|
9716
|
+
} = context;
|
|
9717
|
+
const ticks = context.ticks || [];
|
|
9718
|
+
let {
|
|
9719
|
+
mission,
|
|
9720
|
+
pauseReason,
|
|
9721
|
+
ranTicks,
|
|
9722
|
+
sessionId,
|
|
9723
|
+
blocker,
|
|
9724
|
+
continuationGoal,
|
|
9725
|
+
} = context;
|
|
9726
|
+
|
|
9727
|
+
try {
|
|
9728
|
+
if (context.error) {
|
|
9729
|
+
detachedDriverLifecycle?.finish('run-error', context.error);
|
|
9730
|
+
return;
|
|
9731
|
+
}
|
|
9732
|
+
if (context.handled) return;
|
|
9733
|
+
|
|
9560
9734
|
// BCK-1324: no-progress is a clean, honest stop — the run did what it
|
|
9561
9735
|
// could and correctly recognized there was nothing left to do. It is NOT
|
|
9562
9736
|
// a failure/blocker: pausing it (resumable, retried by cron/self-drive)
|
|
@@ -9670,18 +9844,35 @@ async function runMission(args) {
|
|
|
9670
9844
|
detachedDriverLifecycle?.finish('run-error', error);
|
|
9671
9845
|
throw error;
|
|
9672
9846
|
} finally {
|
|
9673
|
-
if (onSig) {
|
|
9674
|
-
try { process.removeListener('SIGINT', onSig); } catch {}
|
|
9675
|
-
try { process.removeListener('SIGTERM', onSig); } catch {}
|
|
9847
|
+
if (context.onSig) {
|
|
9848
|
+
try { process.removeListener('SIGINT', context.onSig); } catch {}
|
|
9849
|
+
try { process.removeListener('SIGTERM', context.onSig); } catch {}
|
|
9676
9850
|
}
|
|
9677
|
-
if (restoreRunnerProfile) {
|
|
9678
|
-
try { restoreRunnerProfile(); } catch {}
|
|
9851
|
+
if (context.restoreRunnerProfile) {
|
|
9852
|
+
try { context.restoreRunnerProfile(); } catch {}
|
|
9679
9853
|
}
|
|
9680
9854
|
releaseMissionLock(lock);
|
|
9681
9855
|
detachedDriverLifecycle?.finish();
|
|
9682
9856
|
}
|
|
9683
9857
|
}
|
|
9684
9858
|
|
|
9859
|
+
async function runMission(args) {
|
|
9860
|
+
const context = await parseAndValidateMissionRunPhase(args);
|
|
9861
|
+
if (context.handled) return context.returnValue;
|
|
9862
|
+
|
|
9863
|
+
try {
|
|
9864
|
+
await resolveMissionRunPhase(context);
|
|
9865
|
+
if (!context.handled) await executeMissionRunTicksPhase(context);
|
|
9866
|
+
} catch (error) {
|
|
9867
|
+
context.error = error;
|
|
9868
|
+
throw error;
|
|
9869
|
+
} finally {
|
|
9870
|
+
if (context.lock) completeMissionRunPhase(context);
|
|
9871
|
+
}
|
|
9872
|
+
|
|
9873
|
+
return context.returnValue;
|
|
9874
|
+
}
|
|
9875
|
+
|
|
9685
9876
|
function tickMission(args) {
|
|
9686
9877
|
const asJson = wantsJson(args);
|
|
9687
9878
|
if (hasFlag(args, '--help') || hasFlag(args, '-h') || String(args[0] || '').trim() === 'help') {
|
|
@@ -9765,9 +9956,12 @@ function tickMission(args) {
|
|
|
9765
9956
|
// Same layer classification as the run-tick path; manual ticks carry their
|
|
9766
9957
|
// receipt text in --summary.
|
|
9767
9958
|
const layerInfo = extractLayerFromReceiptText(summary || '', tickWorktree?.new_since_baseline_sample);
|
|
9959
|
+
const guardPauseReason = protectedLaneGuard.unreadable
|
|
9960
|
+
? 'mission-diff-unreadable'
|
|
9961
|
+
: 'protected-lane-review';
|
|
9768
9962
|
const tickRecord = {
|
|
9769
|
-
status: protectedLaneGuard.allowed ? 'ran' :
|
|
9770
|
-
reason: protectedLaneGuard.allowed ? 'tick-recorded' :
|
|
9963
|
+
status: protectedLaneGuard.allowed ? 'ran' : protectedLaneGuard.status,
|
|
9964
|
+
reason: protectedLaneGuard.allowed ? 'tick-recorded' : guardPauseReason,
|
|
9771
9965
|
tick_index: tickIdx,
|
|
9772
9966
|
ran: protectedLaneGuard.allowed,
|
|
9773
9967
|
started_at: tickStart,
|
|
@@ -9803,7 +9997,9 @@ function tickMission(args) {
|
|
|
9803
9997
|
const nextGoalChain = advanceMissionGoalChain(mission.goal_chain, summary, verifierResult);
|
|
9804
9998
|
if (!protectedLaneGuard.allowed) {
|
|
9805
9999
|
status = 'paused';
|
|
9806
|
-
nextAction =
|
|
10000
|
+
nextAction = protectedLaneGuard.unreadable
|
|
10001
|
+
? `repair the mission diff tooling failure, then rerun: atris mission run ${mission.id}`
|
|
10002
|
+
: `review the protected diff and receipt before resuming: atris mission run ${mission.id}`;
|
|
9807
10003
|
} else if (verifierResult?.passed && nextGoalChain && !nextGoalChain.pause_ready) {
|
|
9808
10004
|
status = 'running';
|
|
9809
10005
|
nextAction = missionGoalChainNextAction(nextGoalChain);
|
|
@@ -9892,12 +10088,8 @@ function tickMission(args) {
|
|
|
9892
10088
|
// (free text, command strings, missing paths) reads as null and falls back
|
|
9893
10089
|
// to durable mission state.
|
|
9894
10090
|
function readReceiptProof(proof, root = process.cwd()) {
|
|
9895
|
-
|
|
9896
|
-
|
|
9897
|
-
return parsed?.schema === 'atris.mission_receipt.v1' ? parsed : null;
|
|
9898
|
-
} catch {
|
|
9899
|
-
return null;
|
|
9900
|
-
}
|
|
10091
|
+
const parsed = readJson(path.resolve(root, String(proof || '')));
|
|
10092
|
+
return parsed?.schema === 'atris.mission_receipt.v1' ? parsed : null;
|
|
9901
10093
|
}
|
|
9902
10094
|
|
|
9903
10095
|
function receiptShowsPass(receipt) {
|
|
@@ -10599,12 +10791,7 @@ function layersMission(args) {
|
|
|
10599
10791
|
}
|
|
10600
10792
|
for (const file of files) {
|
|
10601
10793
|
if (missionFilter && !file.includes(missionFilter)) continue;
|
|
10602
|
-
|
|
10603
|
-
try {
|
|
10604
|
-
receipt = JSON.parse(fs.readFileSync(path.join(paths.runsDir, file), 'utf8'));
|
|
10605
|
-
} catch {
|
|
10606
|
-
continue;
|
|
10607
|
-
}
|
|
10794
|
+
const receipt = readJson(path.join(paths.runsDir, file));
|
|
10608
10795
|
if (sinceMs != null) {
|
|
10609
10796
|
const atMs = Date.parse(receipt && receipt.at);
|
|
10610
10797
|
if (Number.isNaN(atMs) || atMs < sinceMs) continue;
|
|
@@ -10807,6 +10994,36 @@ function pingMission(args, opts = {}) {
|
|
|
10807
10994
|
return saved;
|
|
10808
10995
|
}
|
|
10809
10996
|
|
|
10997
|
+
function answerMissionHumanAsk(ref, askIndex, answer, note = '') {
|
|
10998
|
+
const found = findMissionAcrossWorktrees(ref);
|
|
10999
|
+
if (!found) throw new Error(`mission not found: ${ref}`);
|
|
11000
|
+
const { mission, root } = found;
|
|
11001
|
+
const humanAsks = normalizeHumanAsks(mission.human_asks);
|
|
11002
|
+
const ask = humanAsks[askIndex];
|
|
11003
|
+
if (!ask || !ask.text.trim()) throw new Error(`human ask not found at index ${askIndex}`);
|
|
11004
|
+
if (ask.answered_at) throw new Error('human ask is already answered');
|
|
11005
|
+
const normalizedAnswer = String(answer || '').toLowerCase();
|
|
11006
|
+
if (!['yes', 'no'].includes(normalizedAnswer)) throw new Error('human ask answer must be yes or no');
|
|
11007
|
+
const answeredAt = stampIso();
|
|
11008
|
+
humanAsks[askIndex] = {
|
|
11009
|
+
...ask,
|
|
11010
|
+
answered_at: answeredAt,
|
|
11011
|
+
answer: normalizedAnswer,
|
|
11012
|
+
note: String(note || '').trim(),
|
|
11013
|
+
};
|
|
11014
|
+
return saveMission(
|
|
11015
|
+
{ ...mission, human_asks: humanAsks },
|
|
11016
|
+
root,
|
|
11017
|
+
'mission_human_ask_answered',
|
|
11018
|
+
{
|
|
11019
|
+
ask_index: askIndex,
|
|
11020
|
+
text: ask.text.slice(0, 200),
|
|
11021
|
+
answer: normalizedAnswer,
|
|
11022
|
+
note: String(note || '').trim(),
|
|
11023
|
+
},
|
|
11024
|
+
).mission;
|
|
11025
|
+
}
|
|
11026
|
+
|
|
10810
11027
|
function missionCommand(args) {
|
|
10811
11028
|
const subcommand = args[0] || 'status';
|
|
10812
11029
|
const rest = args.slice(1);
|
|
@@ -10915,6 +11132,7 @@ function missionCommand(args) {
|
|
|
10915
11132
|
}
|
|
10916
11133
|
|
|
10917
11134
|
module.exports = {
|
|
11135
|
+
missionProtectedLaneHold,
|
|
10918
11136
|
missionCommand,
|
|
10919
11137
|
startMission,
|
|
10920
11138
|
spawnMissionDriver,
|
|
@@ -10931,6 +11149,7 @@ module.exports = {
|
|
|
10931
11149
|
findActiveTwinMission,
|
|
10932
11150
|
TWIN_ACTIVE_STATUSES,
|
|
10933
11151
|
pingMission,
|
|
11152
|
+
answerMissionHumanAsk,
|
|
10934
11153
|
buildTickPrompt,
|
|
10935
11154
|
extractCheckFeedback,
|
|
10936
11155
|
loadMissionMap,
|