atris 3.37.1 → 3.40.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/README.md +2 -0
- package/atris/PERSONA.md +11 -11
- package/atris.md +17 -2
- package/ax +160 -34
- package/bin/atris.js +78 -6
- package/commands/activate.js +16 -3
- package/commands/agent-spawn.js +13 -18
- package/commands/autoland.js +76 -47
- package/commands/autopilot-front.js +0 -4
- package/commands/autopilot.js +0 -12
- package/commands/bench.js +10 -12
- package/commands/brain.js +0 -7
- package/commands/brief.js +0 -2
- package/commands/business-sync.js +0 -1
- package/commands/business.js +452 -12
- package/commands/chat-scan.js +5 -7
- package/commands/clean.js +31 -2
- package/commands/close.js +46 -11
- package/commands/cloud.js +0 -1
- package/commands/codex-goal.js +8 -12
- package/commands/computer.js +6 -5
- package/commands/console.js +64 -11
- package/commands/decide.js +166 -0
- package/commands/deck.js +30 -12
- package/commands/dream.js +0 -7
- package/commands/drill.js +14 -25
- package/commands/engine.js +206 -26
- package/commands/experiments.js +0 -4
- package/commands/fleet-report.js +12 -4
- package/commands/game.js +261 -0
- package/commands/gm.js +17 -7
- package/commands/goal.js +0 -6
- package/commands/harvest.js +1 -5
- package/commands/improve.js +29 -8
- package/commands/init.js +41 -37
- package/commands/integrations.js +0 -7
- package/commands/land.js +77 -20
- package/commands/launchpad.js +1 -16
- package/commands/lesson.js +17 -7
- package/commands/lifecycle.js +5 -5
- package/commands/log.js +55 -5
- package/commands/loop-front.js +0 -2
- package/commands/loops.js +6 -5
- package/commands/meet.js +0 -2
- package/commands/member.js +622 -573
- package/commands/mission.js +790 -314
- package/commands/next.js +0 -1
- package/commands/now.js +0 -12
- package/commands/one-lap.js +8 -8
- package/commands/orb.js +0 -4
- package/commands/pack-craft.js +0 -1
- package/commands/pack.js +3599 -424
- package/commands/play.js +6 -6
- package/commands/probe.js +12 -6
- package/commands/pulse.js +88 -64
- package/commands/push.js +0 -4
- package/commands/radar.js +14 -11
- package/commands/recover.js +340 -0
- package/commands/release.js +10 -9
- package/commands/report.js +0 -1
- package/commands/router.js +320 -0
- package/commands/run-front.js +0 -2
- package/commands/search.js +0 -3
- package/commands/site-deploy.js +870 -0
- package/commands/site.js +11 -2
- package/commands/skill.js +7 -2
- package/commands/slop.js +237 -7
- package/commands/status.js +6 -0
- package/commands/stream.js +4 -20
- package/commands/study.js +0 -3
- package/commands/sync-checkout.js +51 -0
- package/commands/sync.js +11 -40
- package/commands/task.js +999 -595
- package/commands/taste.js +101 -0
- package/commands/teach.js +86 -0
- package/commands/team.js +83 -3
- package/commands/unknowns.js +0 -10
- package/commands/update.js +0 -1
- package/commands/vercel.js +4 -2
- package/commands/visualize.js +0 -5
- package/commands/voice.js +195 -0
- package/commands/watch.js +1 -28
- package/commands/wiki.js +42 -15
- package/commands/wish.js +0 -1
- package/commands/workflow.js +2 -2
- package/commands/worktree.js +88 -28
- package/commands/xp.js +28 -30
- package/commands/youtube.js +0 -2
- package/lib/accept-verify-gate.js +126 -0
- package/lib/activity-stream.js +0 -2
- package/lib/arg-parser.js +41 -0
- package/lib/auto-accept-certified.js +261 -3
- package/lib/autoland.js +66 -3
- package/lib/ax-auto-lane.js +68 -7
- package/lib/bench/context.js +0 -2
- package/lib/bench/report.js +0 -1
- package/lib/bench/runner.js +18 -11
- package/lib/brief-ledger.js +0 -6
- package/lib/business-simulate.js +382 -0
- package/lib/chat-log-scan.js +0 -5
- package/lib/checkout-sync.js +52 -0
- package/lib/claude-boot-block.js +48 -0
- package/lib/cloud-mission.js +0 -6
- package/lib/codex-flight.js +0 -1
- package/lib/conductor-artifacts.js +25 -0
- package/lib/context-gatherer.js +7 -2
- package/lib/deck-compose.js +0 -1
- package/lib/deck-history.js +0 -1
- package/lib/deck-layout.js +0 -5
- package/lib/deck-review.js +0 -7
- package/lib/deck-schema.js +0 -1
- package/lib/default-verifier.js +0 -2
- package/lib/engine-registry.js +157 -28
- package/lib/experiments/daily.js +0 -6
- package/lib/falsifier-probe.js +84 -0
- package/lib/fleet.js +275 -42
- package/lib/functional-owner.js +0 -3
- package/lib/git-spawn.js +15 -0
- package/lib/inspect-fields.js +0 -1
- package/lib/json-file.js +37 -0
- package/lib/known-commands.js +2 -2
- package/lib/learnings.js +0 -2
- package/lib/lesson-preflight.js +146 -0
- package/lib/loop-doctor.js +11 -19
- package/lib/manifest.js +0 -2
- package/lib/member-alive.js +0 -1
- package/lib/member-switches.js +0 -5
- package/lib/mission-artifact.js +0 -1
- package/lib/mission-human-asks.js +28 -0
- package/lib/mission-protected-lane.js +296 -0
- package/lib/mission-room.js +0 -2
- package/lib/mission-runtime-loop.js +0 -1
- package/lib/next-moves.js +0 -18
- package/lib/official-cli-integration.js +47 -6
- package/lib/orb-context.js +14 -4
- package/lib/orb-scorecard.js +0 -1
- package/lib/outbound-send-gate.js +0 -6
- package/lib/pack-capabilities.js +685 -0
- package/lib/permission-grants.js +0 -6
- package/lib/policy-lessons.js +0 -2
- package/lib/pulse.js +68 -15
- package/lib/review-integrity.js +0 -2
- package/lib/router-brain.js +51 -3
- package/lib/runner-command.js +0 -10
- package/lib/runtime-bootstrap.js +0 -1
- package/lib/scorecard.js +0 -1
- package/lib/self-drive.js +45 -14
- package/lib/state-detection.js +0 -6
- package/lib/task-db.js +137 -7
- package/lib/task-decision.js +50 -0
- package/lib/taste-lessons.js +153 -0
- package/lib/team-presence.js +0 -1
- package/lib/tool-result-encode.js +17 -1
- package/lib/voice-gate.js +129 -2
- package/lib/wiki.js +152 -9
- package/lib/wish-audit.js +21 -7
- package/lib/wish-delegate.js +27 -21
- package/lib/wish-stats.js +0 -5
- package/lib/wish-store.js +0 -1
- package/lib/workspace-safety.js +0 -2
- package/lib/zip.js +95 -8
- 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,16 @@ 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');
|
|
87
|
+
const {
|
|
88
|
+
inspectMissionProtectedDiff,
|
|
89
|
+
prepareMissionGitGuard,
|
|
90
|
+
unreadableMissionGuard,
|
|
91
|
+
} = require('../lib/mission-protected-lane');
|
|
81
92
|
|
|
82
93
|
const VALID_STATUSES = new Set(['planning', 'running', 'ready', 'paused', 'blocked', 'stopped', 'complete']);
|
|
83
94
|
const TERMINAL_STATUSES = new Set(['stopped', 'complete']);
|
|
@@ -313,28 +324,6 @@ function readMissionRouteProposal(args, asJson) {
|
|
|
313
324
|
};
|
|
314
325
|
}
|
|
315
326
|
|
|
316
|
-
function hasFlag(args, name) {
|
|
317
|
-
return args.includes(name);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
function unquote(value) {
|
|
321
|
-
const text = String(value);
|
|
322
|
-
if ((text.startsWith('"') && text.endsWith('"')) || (text.startsWith("'") && text.endsWith("'"))) {
|
|
323
|
-
return text.slice(1, -1);
|
|
324
|
-
}
|
|
325
|
-
return text;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
function readFlag(args, name, fallback = '') {
|
|
329
|
-
const prefix = `${name}=`;
|
|
330
|
-
for (let i = 0; i < args.length; i += 1) {
|
|
331
|
-
const arg = String(args[i]);
|
|
332
|
-
if (arg === name && args[i + 1] && !String(args[i + 1]).startsWith('--')) return unquote(args[i + 1]);
|
|
333
|
-
if (arg.startsWith(prefix)) return unquote(arg.slice(prefix.length));
|
|
334
|
-
}
|
|
335
|
-
return fallback;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
327
|
const MISSION_NATIVE_RUNNER_NAMES = Object.freeze(['manual', 'claude', 'atris2', 'codex_goal', 'caller_session', 'current_agent', 'drill']);
|
|
339
328
|
const MISSION_NATIVE_RUNNER_SET = new Set(MISSION_NATIVE_RUNNER_NAMES);
|
|
340
329
|
const MISSION_AUTO_RUNNER = 'auto';
|
|
@@ -508,11 +497,11 @@ function readRepeatedFlag(args, name) {
|
|
|
508
497
|
for (let i = 0; i < args.length; i += 1) {
|
|
509
498
|
const arg = String(args[i]);
|
|
510
499
|
if (arg === name && args[i + 1] && !String(args[i + 1]).startsWith('--')) {
|
|
511
|
-
values.push(
|
|
500
|
+
values.push(readFlag([name, args[i + 1]], name, ''));
|
|
512
501
|
i += 1;
|
|
513
502
|
continue;
|
|
514
503
|
}
|
|
515
|
-
if (arg.startsWith(prefix)) values.push(
|
|
504
|
+
if (arg.startsWith(prefix)) values.push(readFlag([arg], name, ''));
|
|
516
505
|
}
|
|
517
506
|
return values.filter(Boolean);
|
|
518
507
|
}
|
|
@@ -734,8 +723,7 @@ function loadTaskDb(asJson = false) {
|
|
|
734
723
|
function writeMissionTaskProjection(taskDb, db, workspaceRoot) {
|
|
735
724
|
const projection = taskDb.taskProjection(db, { workspaceRoot, limit: 500 });
|
|
736
725
|
const outPath = path.join(workspaceRoot, '.atris', 'state', 'tasks.projection.json');
|
|
737
|
-
|
|
738
|
-
fs.writeFileSync(outPath, JSON.stringify(projection, null, 2) + '\n', 'utf8');
|
|
726
|
+
writeJson(outPath, projection);
|
|
739
727
|
return { projection, outPath };
|
|
740
728
|
}
|
|
741
729
|
|
|
@@ -834,16 +822,13 @@ function statePaths(root = process.cwd()) {
|
|
|
834
822
|
|
|
835
823
|
function readBusinessBinding(root = process.cwd()) {
|
|
836
824
|
const file = path.join(root, '.atris', 'business.json');
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
} catch {
|
|
845
|
-
return null;
|
|
846
|
-
}
|
|
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
|
+
};
|
|
847
832
|
}
|
|
848
833
|
|
|
849
834
|
function businessIdForAtris2Mission(mission, cwd = process.cwd()) {
|
|
@@ -930,9 +915,11 @@ function terminalNextAction(status) {
|
|
|
930
915
|
|
|
931
916
|
function normalizeMissionState(mission) {
|
|
932
917
|
if (!mission) return mission;
|
|
933
|
-
let normalized = mission
|
|
934
|
-
|
|
935
|
-
|
|
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) {
|
|
936
923
|
normalized = { ...normalized, next_action: nextAction };
|
|
937
924
|
}
|
|
938
925
|
const effectiveVerifier = effectiveMissionVerifier(normalized);
|
|
@@ -1519,9 +1506,10 @@ function renderMemberNowMarkdown(owner, missions, root = process.cwd()) {
|
|
|
1519
1506
|
if (mission.stop_condition) lines.push(`- stop: ${mission.stop_condition}`);
|
|
1520
1507
|
if (budgetContinuation || mission.next_action) lines.push(`- next: ${budgetContinuation || mission.next_action}`);
|
|
1521
1508
|
if (mission.receipt_path) lines.push(`- proof: ${missionStatusProofText(mission)}`);
|
|
1522
|
-
|
|
1509
|
+
const humanAsks = openHumanAsks(mission.human_asks);
|
|
1510
|
+
if (humanAsks.length) {
|
|
1523
1511
|
lines.push('- human asks:');
|
|
1524
|
-
for (const ask of
|
|
1512
|
+
for (const ask of humanAsks) lines.push(` - ${ask.text}`);
|
|
1525
1513
|
}
|
|
1526
1514
|
lines.push('');
|
|
1527
1515
|
}
|
|
@@ -1697,6 +1685,16 @@ function missionLastStepSummary(ticks = []) {
|
|
|
1697
1685
|
return '';
|
|
1698
1686
|
}
|
|
1699
1687
|
|
|
1688
|
+
function verifierElapsedSeconds(verifierResult) {
|
|
1689
|
+
const ms = Number(verifierResult && verifierResult.elapsed_ms);
|
|
1690
|
+
return Number.isFinite(ms) ? Math.round(ms / 1000) : 0;
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
function verifierTimeoutSeconds(verifierResult) {
|
|
1694
|
+
const ms = Number(verifierResult && verifierResult.timeout_ms);
|
|
1695
|
+
return Number.isFinite(ms) ? Math.round(ms / 1000) : 0;
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1700
1698
|
function missionVerifierCheckedText(verifierResult, mission) {
|
|
1701
1699
|
if (!verifierResult) return 'UNVERIFIED: tick recorded but nothing was checked; treat this increment as unproven.';
|
|
1702
1700
|
if (verifierResult.mode === 'engine-unavailable') {
|
|
@@ -1708,6 +1706,9 @@ function missionVerifierCheckedText(verifierResult, mission) {
|
|
|
1708
1706
|
: 'VERIFY FAILED: the engine verify pass did not end with VERDICT: PASS.';
|
|
1709
1707
|
}
|
|
1710
1708
|
const command = verifierResult.command || mission.verifier || 'configured verifier';
|
|
1709
|
+
if (!verifierResult.passed && verifierResult.timed_out) {
|
|
1710
|
+
return `VERIFY TIMED OUT: the check ran past its ${verifierTimeoutSeconds(verifierResult)}s window (took ${verifierElapsedSeconds(verifierResult)}s); the code is not failing, the suite is slow. Rerun with more room.`;
|
|
1711
|
+
}
|
|
1711
1712
|
if (verifierResult.passed) {
|
|
1712
1713
|
if (/^git\s+diff\s+--check\b/i.test(command)) return 'I ran the diff cleanliness check.';
|
|
1713
1714
|
if (/\bnode\s+--test\b/i.test(command)) return 'I ran the behavior checks.';
|
|
@@ -1716,7 +1717,7 @@ function missionVerifierCheckedText(verifierResult, mission) {
|
|
|
1716
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.';
|
|
1717
1718
|
return `Verifier passed: ${command}.`;
|
|
1718
1719
|
}
|
|
1719
|
-
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';
|
|
1720
1721
|
if (/\bnode\s+--test\b/i.test(command)) return 'VERIFY FAILED: behavior checks failed.';
|
|
1721
1722
|
return `VERIFY FAILED: ${command}.`;
|
|
1722
1723
|
}
|
|
@@ -1732,9 +1733,13 @@ function missionVerifierHighLevelTestText(verifierResult, mission) {
|
|
|
1732
1733
|
: 'The verify engine did not provide a passing changed-surface verdict.';
|
|
1733
1734
|
}
|
|
1734
1735
|
const command = verifierResult.command || mission.verifier || 'configured verifier';
|
|
1736
|
+
if (!verifierResult.passed && verifierResult.timed_out) {
|
|
1737
|
+
return `Verifier command timed out after ${verifierElapsedSeconds(verifierResult)}s against a ${verifierTimeoutSeconds(verifierResult)}s window: ${command}. No pass/fail verdict was reached; the suite outran the window rather than the code failing.`;
|
|
1738
|
+
}
|
|
1735
1739
|
const outcome = verifierResult.passed ? 'passed' : 'failed';
|
|
1736
1740
|
if (/^git\s+diff\s+--check\b/i.test(command)) {
|
|
1737
|
-
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`;
|
|
1738
1743
|
}
|
|
1739
1744
|
if (/\bnode\s+--test\b/i.test(command) && /\btest\/mission-status\.test\.js\b/i.test(command)) {
|
|
1740
1745
|
return `Mission behavior checks ${outcome}: mission start, tick, completion, timeline landing, goal-chain, next-mission, and human-accept boundaries were exercised.`;
|
|
@@ -1964,12 +1969,22 @@ function shouldMissionRunTrustedRoom(rawObjective, args = []) {
|
|
|
1964
1969
|
return missionRunTrustedRoomSignals(rawObjective);
|
|
1965
1970
|
}
|
|
1966
1971
|
|
|
1972
|
+
const MISSION_BARE_COMMAND_WORDS = new Set(['go', 'keep', 'going', 'do', 'it', 'start', 'run', 'continue']);
|
|
1973
|
+
|
|
1974
|
+
function missionObjectiveIsJunk(value) {
|
|
1975
|
+
const objective = String(value || '').trim();
|
|
1976
|
+
if (!objective || objective.startsWith('-')) return true;
|
|
1977
|
+
if (/[\u0000-\u001f\u007f-\u009f\ufffd]/u.test(objective)) return true;
|
|
1978
|
+
if (objective.replace(/\s/g, '').length < 8) return true;
|
|
1979
|
+
const words = objective.toLowerCase().match(/[a-z]+/g) || [];
|
|
1980
|
+
return words.length > 0
|
|
1981
|
+
&& objective.replace(/[^a-z]+/gi, '').length > 0
|
|
1982
|
+
&& words.every((word) => MISSION_BARE_COMMAND_WORDS.has(word));
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1967
1985
|
function missionRunConcreteTitle(value) {
|
|
1968
1986
|
const title = String(value || '').replace(/\s+/g, ' ').trim();
|
|
1969
|
-
|
|
1970
|
-
if (/^(go|go go|go go go|keep going|do it|start|run|continue)$/i.test(title)) return '';
|
|
1971
|
-
if (title.length < 8) return '';
|
|
1972
|
-
return title;
|
|
1987
|
+
return missionObjectiveIsJunk(title) ? '' : title;
|
|
1973
1988
|
}
|
|
1974
1989
|
|
|
1975
1990
|
function selectMissionRunUsefulTarget(rawObjective, root = process.cwd()) {
|
|
@@ -2207,6 +2222,9 @@ function missionFullBudgetRemainingSeconds(mission, nowMs = Date.now()) {
|
|
|
2207
2222
|
}
|
|
2208
2223
|
|
|
2209
2224
|
function missionBudgetContinuationText(mission, nowMs = Date.now()) {
|
|
2225
|
+
// Paused/stopped/blocked missions are not running: never sell remaining time
|
|
2226
|
+
// as a live commitment (CLI-1192: paused overnight loops read as healthy).
|
|
2227
|
+
if (!GOAL_LOOP_STATUSES.has(String(mission?.status || ''))) return null;
|
|
2210
2228
|
const remaining = missionFullBudgetRemainingSeconds(mission, nowMs);
|
|
2211
2229
|
if (remaining <= 0) return null;
|
|
2212
2230
|
const budget = String(mission?.budget_contract?.budget_label || 'promised time').trim();
|
|
@@ -2218,10 +2236,15 @@ function missionBudgetContinuationText(mission, nowMs = Date.now()) {
|
|
|
2218
2236
|
}
|
|
2219
2237
|
|
|
2220
2238
|
function missionHumanStatusText(mission, nowMs = Date.now()) {
|
|
2239
|
+
const rawStatus = String(mission?.status || 'unknown');
|
|
2240
|
+
if (rawStatus === 'paused') {
|
|
2241
|
+
const reason = String(mission?.stop_reason || mission?.pause_reason || '').trim();
|
|
2242
|
+
return gateForHuman(reason ? `paused: ${reason}` : 'paused').text;
|
|
2243
|
+
}
|
|
2221
2244
|
const continuation = missionBudgetContinuationText(mission, nowMs);
|
|
2222
2245
|
const status = continuation
|
|
2223
2246
|
? `working for the full ${String(mission?.budget_contract?.budget_label || 'promised time').trim()}`
|
|
2224
|
-
:
|
|
2247
|
+
: rawStatus;
|
|
2225
2248
|
return gateForHuman(status).text;
|
|
2226
2249
|
}
|
|
2227
2250
|
|
|
@@ -2941,12 +2964,8 @@ function handledContinuationTargetKeys(root, moves) {
|
|
|
2941
2964
|
|
|
2942
2965
|
function readTaskProjectionForMission(root = process.cwd()) {
|
|
2943
2966
|
const file = path.join(root, '.atris', 'state', 'tasks.projection.json');
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
return Array.isArray(parsed?.tasks) ? parsed.tasks : [];
|
|
2947
|
-
} catch {
|
|
2948
|
-
return [];
|
|
2949
|
-
}
|
|
2967
|
+
const parsed = readJson(file);
|
|
2968
|
+
return Array.isArray(parsed?.tasks) ? parsed.tasks : [];
|
|
2950
2969
|
}
|
|
2951
2970
|
|
|
2952
2971
|
function taskTags(task) {
|
|
@@ -3328,8 +3347,7 @@ function readRecentTasteLogs(root, owner, limit = 3) {
|
|
|
3328
3347
|
|
|
3329
3348
|
function readTasteReviewHistory(root, limit = 4) {
|
|
3330
3349
|
const file = path.join(root, '.atris', 'state', 'tasks.projection.json');
|
|
3331
|
-
|
|
3332
|
-
try { projection = JSON.parse(fs.readFileSync(file, 'utf8')); } catch { projection = null; }
|
|
3350
|
+
const projection = readJson(file);
|
|
3333
3351
|
const tasks = Array.isArray(projection?.tasks) ? projection.tasks : [];
|
|
3334
3352
|
const accepted = [];
|
|
3335
3353
|
const revised = [];
|
|
@@ -3852,16 +3870,15 @@ function findActiveTwinMission(objective, owner, root = process.cwd()) {
|
|
|
3852
3870
|
function startMission(args, options = {}) {
|
|
3853
3871
|
const asJson = wantsJson(args);
|
|
3854
3872
|
const firstArg = String(args[0] || '').trim().toLowerCase();
|
|
3855
|
-
if (
|
|
3873
|
+
if (args.length === 1 && (firstArg === '--help' || firstArg === '-h' || firstArg === 'help')) {
|
|
3856
3874
|
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]');
|
|
3857
3875
|
console.log('Run `atris mission --help` for the full option list.');
|
|
3858
3876
|
process.exit(0);
|
|
3859
3877
|
}
|
|
3860
3878
|
let mission = applyMissionStartPatch(missionFromArgs(args), options.missionPatch);
|
|
3861
|
-
// A flag-looking or empty objective is a typo, not a mission.
|
|
3862
3879
|
const rawObjective = String(mission.objective || '').trim();
|
|
3863
|
-
if (
|
|
3864
|
-
exitMissionError(`mission start
|
|
3880
|
+
if (missionObjectiveIsJunk(rawObjective)) {
|
|
3881
|
+
exitMissionError(`mission start refused junk objective: ${rawObjective ? `"${rawObjective}"` : 'nothing'}`, 1, asJson);
|
|
3865
3882
|
}
|
|
3866
3883
|
// Pasting a mission id where an objective goes is an id mismatch, not a new
|
|
3867
3884
|
// mission: recover the existing record or explain where it actually lives.
|
|
@@ -4251,6 +4268,11 @@ async function startMissionFromRunObjective(objective, args) {
|
|
|
4251
4268
|
|
|
4252
4269
|
function attachMissionTask(args) {
|
|
4253
4270
|
const asJson = wantsJson(args);
|
|
4271
|
+
if (hasFlag(args, '--help') || hasFlag(args, '-h') || String(args[0] || '').trim() === 'help') {
|
|
4272
|
+
console.log('Usage: atris mission attach-task <id> [--json]');
|
|
4273
|
+
console.log('Run `atris mission --help` for the full option list.');
|
|
4274
|
+
process.exit(0);
|
|
4275
|
+
}
|
|
4254
4276
|
const ref = stripKnownFlags(args, [], ['--json'])[0] || '';
|
|
4255
4277
|
if (!ref) {
|
|
4256
4278
|
exitMissionError('Usage: atris mission attach-task <id> [--json]', 1, asJson);
|
|
@@ -4695,11 +4717,7 @@ function statusMission(args) {
|
|
|
4695
4717
|
function readMissionReceipt(receiptPath, root = process.cwd()) {
|
|
4696
4718
|
if (!receiptPath) return null;
|
|
4697
4719
|
const file = path.isAbsolute(receiptPath) ? receiptPath : path.join(root, receiptPath);
|
|
4698
|
-
|
|
4699
|
-
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
4700
|
-
} catch {
|
|
4701
|
-
return null;
|
|
4702
|
-
}
|
|
4720
|
+
return readJson(file);
|
|
4703
4721
|
}
|
|
4704
4722
|
|
|
4705
4723
|
function firstUsefulLine(text, fallback = '') {
|
|
@@ -4824,12 +4842,7 @@ function missionVerificationDebt(mission, root = process.cwd()) {
|
|
|
4824
4842
|
let unchecked = 0;
|
|
4825
4843
|
const seen = new Set();
|
|
4826
4844
|
for (const file of files) {
|
|
4827
|
-
|
|
4828
|
-
try {
|
|
4829
|
-
receipt = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
4830
|
-
} catch {
|
|
4831
|
-
continue;
|
|
4832
|
-
}
|
|
4845
|
+
const receipt = readJson(file);
|
|
4833
4846
|
if (!receipt || receipt.mission_id !== mission.id) continue;
|
|
4834
4847
|
for (const tick of missionReceiptTicks(receipt)) {
|
|
4835
4848
|
if (!tick || tick.status !== 'ran') continue;
|
|
@@ -4866,12 +4879,7 @@ function missionReportTimeline(mission, root = process.cwd(), limit = 6) {
|
|
|
4866
4879
|
const items = [];
|
|
4867
4880
|
const seen = new Set();
|
|
4868
4881
|
for (const file of files) {
|
|
4869
|
-
|
|
4870
|
-
try {
|
|
4871
|
-
receipt = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
4872
|
-
} catch {
|
|
4873
|
-
continue;
|
|
4874
|
-
}
|
|
4882
|
+
const receipt = readJson(file);
|
|
4875
4883
|
if (!receipt || receipt.mission_id !== mission.id) continue;
|
|
4876
4884
|
const receiptPath = path.relative(root, file);
|
|
4877
4885
|
for (const tick of missionReceiptTicks(receipt)) {
|
|
@@ -4914,12 +4922,7 @@ function missionLandingTimeline(mission, root = process.cwd(), limit = 12, { kin
|
|
|
4914
4922
|
const items = [];
|
|
4915
4923
|
const seen = new Set();
|
|
4916
4924
|
for (const file of files) {
|
|
4917
|
-
|
|
4918
|
-
try {
|
|
4919
|
-
receipt = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
4920
|
-
} catch {
|
|
4921
|
-
continue;
|
|
4922
|
-
}
|
|
4925
|
+
const receipt = readJson(file);
|
|
4923
4926
|
if (!receipt || receipt.mission_id !== mission.id) continue;
|
|
4924
4927
|
const receiptKind = receipt.result && receipt.result.kind ? String(receipt.result.kind) : (receipt.kind ? String(receipt.kind) : '');
|
|
4925
4928
|
if (kindFilter && receiptKind !== kindFilter) continue;
|
|
@@ -5148,12 +5151,24 @@ function missionReportFor(mission, root = process.cwd()) {
|
|
|
5148
5151
|
const timeline = missionReportTimeline(mission, root);
|
|
5149
5152
|
const verificationDebt = missionVerificationDebt(mission, root);
|
|
5150
5153
|
const workerCheckedIn = Boolean(explicitWorkerReceipt) || timeline.length > 0;
|
|
5154
|
+
const driver = missionDriverHealth(mission, root);
|
|
5155
|
+
const driverAlive = Boolean(driver && driver.alive);
|
|
5151
5156
|
const workerReceiptPath = mission.worker_receipt_path || (receipt && verifierReceiptPath) || null;
|
|
5152
5157
|
const verifierPassed = mission.verifier_result && mission.verifier_result.passed === true;
|
|
5153
5158
|
const pendingBudgetContinuation = missionBudgetContinuationText(mission);
|
|
5154
5159
|
const budgetContinuation = workerCheckedIn ? pendingBudgetContinuation : null;
|
|
5155
|
-
const
|
|
5160
|
+
const awaitingFirstWorker = !workerCheckedIn && Boolean(pendingBudgetContinuation);
|
|
5161
|
+
// An active status implies a worker is running. With no receipt AND no live
|
|
5162
|
+
// driver, that claim is a lie: report that no worker is running and name the
|
|
5163
|
+
// command that starts one instead of saying the mission is in progress.
|
|
5164
|
+
const noLiveWorker = !workerCheckedIn
|
|
5165
|
+
&& !driverAlive
|
|
5166
|
+
&& !pendingBudgetContinuation
|
|
5167
|
+
&& GOAL_LOOP_STATUSES.has(String(mission.status || ''));
|
|
5168
|
+
const operatorOutcome = awaitingFirstWorker
|
|
5156
5169
|
? 'No worker has checked in yet.'
|
|
5170
|
+
: noLiveWorker
|
|
5171
|
+
? 'No worker is running; nothing has produced a receipt yet.'
|
|
5157
5172
|
: budgetContinuation
|
|
5158
5173
|
? 'The last verifier passed; full-budget work is continuing.'
|
|
5159
5174
|
: mission.operator_outcome
|
|
@@ -5162,8 +5177,10 @@ function missionReportFor(mission, root = process.cwd()) {
|
|
|
5162
5177
|
id: mission.id,
|
|
5163
5178
|
objective: mission.objective,
|
|
5164
5179
|
status: mission.status,
|
|
5165
|
-
human_status:
|
|
5180
|
+
human_status: awaitingFirstWorker
|
|
5166
5181
|
? 'waiting for the first worker check-in'
|
|
5182
|
+
: noLiveWorker
|
|
5183
|
+
? 'no live worker (nothing is running)'
|
|
5167
5184
|
: missionHumanStatusText(mission),
|
|
5168
5185
|
budget_continuation: budgetContinuation,
|
|
5169
5186
|
operator_outcome: operatorOutcome,
|
|
@@ -5175,9 +5192,10 @@ function missionReportFor(mission, root = process.cwd()) {
|
|
|
5175
5192
|
verifier_receipt_path: verifierReceiptPath,
|
|
5176
5193
|
proof_text: mission.receipt_path ? missionStatusProofText(mission) : null,
|
|
5177
5194
|
operator_next: budgetContinuation
|
|
5178
|
-
|| (
|
|
5195
|
+
|| (awaitingFirstWorker ? 'Wait for the first worker check-in.' : null)
|
|
5179
5196
|
|| mission.operator_next
|
|
5180
5197
|
|| mission.next_action
|
|
5198
|
+
|| (noLiveWorker ? `Start a worker: atris mission run ${mission.id}` : null)
|
|
5181
5199
|
|| 'Review the mission state.',
|
|
5182
5200
|
};
|
|
5183
5201
|
}
|
|
@@ -5254,14 +5272,11 @@ function timelineMission(args) {
|
|
|
5254
5272
|
const sinceFilter = readFlag(args, '--since', '') || null;
|
|
5255
5273
|
const ref = stripKnownFlags(args, ['--limit', '--output', '--out', '--kind', '--since'], ['--json', '--write', '--all', '--prune-preview'])[0] || '';
|
|
5256
5274
|
const limit = all ? Number.MAX_SAFE_INTEGER : readPositiveIntegerFlag(args, '--limit', 12, { json: asJson });
|
|
5257
|
-
const
|
|
5258
|
-
|
|
5259
|
-
? resolveMission(ref)
|
|
5260
|
-
: (missions.find((row) => !TERMINAL_STATUSES.has(row.status)) || missions[0] || null);
|
|
5261
|
-
if (ref && !mission) {
|
|
5275
|
+
const data = loadTimelineData(ref, { limit, kindFilter, sinceFilter, write, prunePreviewRequested, outputPath });
|
|
5276
|
+
if (ref && !data.mission) {
|
|
5262
5277
|
exitMissingMission(ref, 1, asJson);
|
|
5263
5278
|
}
|
|
5264
|
-
if (!mission) {
|
|
5279
|
+
if (!data.mission) {
|
|
5265
5280
|
printJsonOrText(
|
|
5266
5281
|
{
|
|
5267
5282
|
ok: true,
|
|
@@ -5275,8 +5290,40 @@ function timelineMission(args) {
|
|
|
5275
5290
|
);
|
|
5276
5291
|
return;
|
|
5277
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 };
|
|
5278
5303
|
const root = mission.worktree_root || process.cwd();
|
|
5279
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 }) {
|
|
5280
5327
|
const timeline = timelineResult.items;
|
|
5281
5328
|
const currentLanding = missionTimelineCurrentLanding(timeline);
|
|
5282
5329
|
const timelineItemDisplay = (item) => ({
|
|
@@ -5305,24 +5352,6 @@ function timelineMission(args) {
|
|
|
5305
5352
|
...timelineItemDisplay(item),
|
|
5306
5353
|
}));
|
|
5307
5354
|
const nextMove = missionTimelineNextMove(mission, currentLanding);
|
|
5308
|
-
const generatedAt = stampIso();
|
|
5309
|
-
let artifactPath = null;
|
|
5310
|
-
let prunePreview = null;
|
|
5311
|
-
if (write || prunePreviewRequested) {
|
|
5312
|
-
try {
|
|
5313
|
-
prunePreview = pruneRuns(root, { keepNewest: 200, keepDays: 14 });
|
|
5314
|
-
} catch (error) {
|
|
5315
|
-
prunePreview = { error: error.message || String(error) };
|
|
5316
|
-
}
|
|
5317
|
-
}
|
|
5318
|
-
if (write) {
|
|
5319
|
-
const outPath = outputPath
|
|
5320
|
-
? path.resolve(root, outputPath)
|
|
5321
|
-
: defaultMissionTimelinePath(root, mission);
|
|
5322
|
-
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
5323
|
-
fs.writeFileSync(outPath, missionTimelineMarkdown(mission, timeline, { prunePreview, generatedAt, timelineMeta: timelineResult.meta }), 'utf8');
|
|
5324
|
-
artifactPath = path.relative(root, outPath);
|
|
5325
|
-
}
|
|
5326
5355
|
const pruneSummary = missionTimelinePruneSummaryObject(prunePreview);
|
|
5327
5356
|
const pruneDisplay = {
|
|
5328
5357
|
label: 'Prune preview',
|
|
@@ -5587,6 +5616,10 @@ function timelineMission(args) {
|
|
|
5587
5616
|
if (artifactPath) lines.push(`Saved: ${artifactPath}`);
|
|
5588
5617
|
const pruneLine = missionTimelinePruneSummaryLine(prunePreview);
|
|
5589
5618
|
if (pruneLine) lines.push(pruneLine);
|
|
5619
|
+
return { payload, lines };
|
|
5620
|
+
}
|
|
5621
|
+
|
|
5622
|
+
function renderTimeline(payload, lines, asJson) {
|
|
5590
5623
|
printJsonOrText(payload, lines, asJson);
|
|
5591
5624
|
}
|
|
5592
5625
|
|
|
@@ -5756,8 +5789,8 @@ function watchMission(args) {
|
|
|
5756
5789
|
return;
|
|
5757
5790
|
}
|
|
5758
5791
|
const ref = stripKnownFlags(args, ['--interval', '--idle-every'], [])[0] || '';
|
|
5759
|
-
const intervalSeconds = Math.max(1,
|
|
5760
|
-
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);
|
|
5761
5794
|
const loadTargets = () => {
|
|
5762
5795
|
if (ref) {
|
|
5763
5796
|
const mission = resolveMission(ref);
|
|
@@ -5857,19 +5890,29 @@ function runVerifier(command, root = process.cwd()) {
|
|
|
5857
5890
|
if (!command) return null;
|
|
5858
5891
|
const resolvedCommand = resolveVerifierCommand(command);
|
|
5859
5892
|
const { envWithNodeDir } = require('../lib/spawn-env');
|
|
5893
|
+
const timeoutMs = missionVerifierTimeoutMs();
|
|
5894
|
+
const startedAt = Date.now();
|
|
5860
5895
|
const result = spawnSync(resolvedCommand, {
|
|
5861
5896
|
cwd: root,
|
|
5862
5897
|
shell: true,
|
|
5863
5898
|
encoding: 'utf8',
|
|
5864
|
-
timeout:
|
|
5899
|
+
timeout: timeoutMs,
|
|
5865
5900
|
env: envWithNodeDir(process.env),
|
|
5866
5901
|
});
|
|
5902
|
+
const elapsedMs = Date.now() - startedAt;
|
|
5903
|
+
// A verifier that blows its window is not a failing check: the code may be
|
|
5904
|
+
// fine and the suite merely slow (or starved by a concurrent flight). Report
|
|
5905
|
+
// it as a distinct timed-out state so operators do not read slow as broken.
|
|
5906
|
+
const timedOut = result.error ? result.error.code === 'ETIMEDOUT' : false;
|
|
5867
5907
|
return {
|
|
5868
5908
|
command,
|
|
5869
5909
|
resolved_command: resolvedCommand === command ? null : resolvedCommand,
|
|
5870
5910
|
status: result.status,
|
|
5871
5911
|
signal: result.signal || null,
|
|
5872
5912
|
passed: result.status === 0,
|
|
5913
|
+
timed_out: timedOut,
|
|
5914
|
+
elapsed_ms: elapsedMs,
|
|
5915
|
+
timeout_ms: timeoutMs,
|
|
5873
5916
|
stdout: String(result.stdout || '').slice(-4000),
|
|
5874
5917
|
stderr: String(result.stderr || '').slice(-4000),
|
|
5875
5918
|
};
|
|
@@ -5952,8 +5995,14 @@ function gitWorktreeSnapshot(root = process.cwd()) {
|
|
|
5952
5995
|
}
|
|
5953
5996
|
const entries = String(status.stdout || '').split(/\r?\n/).filter(Boolean).sort();
|
|
5954
5997
|
const digest = crypto.createHash('sha1').update(entries.join('\n')).digest('hex');
|
|
5998
|
+
const head = spawnSync('git', ['rev-parse', '--verify', 'HEAD'], {
|
|
5999
|
+
cwd: root,
|
|
6000
|
+
encoding: 'utf8',
|
|
6001
|
+
timeout: 5000,
|
|
6002
|
+
});
|
|
5955
6003
|
return {
|
|
5956
6004
|
available: true,
|
|
6005
|
+
head: head.status === 0 ? String(head.stdout || '').trim() : null,
|
|
5957
6006
|
dirty_count: entries.length,
|
|
5958
6007
|
dirty_hash: digest,
|
|
5959
6008
|
dirty_sample: entries.slice(0, 25),
|
|
@@ -5961,6 +6010,47 @@ function gitWorktreeSnapshot(root = process.cwd()) {
|
|
|
5961
6010
|
};
|
|
5962
6011
|
}
|
|
5963
6012
|
|
|
6013
|
+
function missionProtectedTags(mission, root = process.cwd()) {
|
|
6014
|
+
const tags = [
|
|
6015
|
+
mission?.tag,
|
|
6016
|
+
mission?.lane,
|
|
6017
|
+
...(Array.isArray(mission?.tags) ? mission.tags : []),
|
|
6018
|
+
].filter(Boolean);
|
|
6019
|
+
const taskRefs = new Set([
|
|
6020
|
+
mission?.task_id,
|
|
6021
|
+
mission?.task_ref,
|
|
6022
|
+
...(Array.isArray(mission?.task_ids) ? mission.task_ids : []),
|
|
6023
|
+
].filter(Boolean).map(String));
|
|
6024
|
+
if (!taskRefs.size) return tags;
|
|
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
|
+
}
|
|
6031
|
+
return tags;
|
|
6032
|
+
}
|
|
6033
|
+
|
|
6034
|
+
function inspectMissionTickProtectedDiff(mission, before, root = process.cwd()) {
|
|
6035
|
+
const after = gitWorktreeSnapshot(root);
|
|
6036
|
+
if (before?.reason === 'not-git-worktree' && after?.reason === 'not-git-worktree') {
|
|
6037
|
+
return {
|
|
6038
|
+
ok: true,
|
|
6039
|
+
allowed: true,
|
|
6040
|
+
status: 'clear',
|
|
6041
|
+
reason: 'no git worktree exists, so this tick cannot commit or land',
|
|
6042
|
+
unreadable: false,
|
|
6043
|
+
surfaces: [],
|
|
6044
|
+
matches: [],
|
|
6045
|
+
};
|
|
6046
|
+
}
|
|
6047
|
+
return inspectMissionProtectedDiff({
|
|
6048
|
+
root,
|
|
6049
|
+
baseRef: before?.head || '',
|
|
6050
|
+
tags: missionProtectedTags(mission, root),
|
|
6051
|
+
});
|
|
6052
|
+
}
|
|
6053
|
+
|
|
5964
6054
|
// Porcelain v1 entries are "XY path" or "XY old -> new"; baselines compare by
|
|
5965
6055
|
// post-rename path so a status-letter change alone never counts as new dirt.
|
|
5966
6056
|
function porcelainEntryPath(line) {
|
|
@@ -6001,18 +6091,13 @@ function captureMissionWorktreeBaseline(mission, root = process.cwd()) {
|
|
|
6001
6091
|
dirty_hash: snapshot.dirty_hash,
|
|
6002
6092
|
paths: Array.from(paths).sort(),
|
|
6003
6093
|
};
|
|
6004
|
-
|
|
6005
|
-
fs.writeFileSync(baselineFile, JSON.stringify(baseline, null, 2) + '\n', 'utf8');
|
|
6094
|
+
writeJson(baselineFile, baseline);
|
|
6006
6095
|
return baseline;
|
|
6007
6096
|
}
|
|
6008
6097
|
|
|
6009
6098
|
function loadMissionWorktreeBaseline(missionId, root = process.cwd()) {
|
|
6010
|
-
|
|
6011
|
-
|
|
6012
|
-
return Array.isArray(baseline?.paths) ? baseline : null;
|
|
6013
|
-
} catch {
|
|
6014
|
-
return null;
|
|
6015
|
-
}
|
|
6099
|
+
const baseline = readJson(missionBaselinePath(missionId, root));
|
|
6100
|
+
return Array.isArray(baseline?.paths) ? baseline : null;
|
|
6016
6101
|
}
|
|
6017
6102
|
|
|
6018
6103
|
// Closed missions no longer tick, so the sidecar is dead weight; prune it and
|
|
@@ -6091,6 +6176,12 @@ const MISSION_RUN_DEFAULTS = {
|
|
|
6091
6176
|
backoff: { initialMs: 30_000, maxMs: 10 * 60_000, factor: 2, jitter: 0.3 },
|
|
6092
6177
|
};
|
|
6093
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
|
+
|
|
6094
6185
|
// Claude sessions accumulate context across resumed ticks; an always-on
|
|
6095
6186
|
// mission would grow without bound. Continuity lives on disk (receipts, logs,
|
|
6096
6187
|
// now.md), so a healthy session is disposable: rotate to a fresh one every N
|
|
@@ -6100,6 +6191,25 @@ const CLAUDE_SESSION_CONTEXT_ROTATE_TICKS = Math.max(
|
|
|
6100
6191
|
Number(process.env.ATRIS_CLAUDE_SESSION_ROTATE_TICKS) || 8,
|
|
6101
6192
|
);
|
|
6102
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
|
+
|
|
6103
6213
|
function runnerUsesCallerSession(runner) {
|
|
6104
6214
|
return new Set(['codex_goal', 'caller_session', 'current_agent']).has(String(runner || '').trim().toLowerCase());
|
|
6105
6215
|
}
|
|
@@ -6222,8 +6332,7 @@ function missionHeartbeatLines(mission, now = new Date()) {
|
|
|
6222
6332
|
}
|
|
6223
6333
|
|
|
6224
6334
|
function missionHasHumanAsks(mission) {
|
|
6225
|
-
return
|
|
6226
|
-
&& mission.human_asks.some((ask) => String(ask || '').trim());
|
|
6335
|
+
return openHumanAsks(mission?.human_asks).length > 0;
|
|
6227
6336
|
}
|
|
6228
6337
|
|
|
6229
6338
|
function missionTaskHumanAcceptWaiting(mission) {
|
|
@@ -6264,7 +6373,14 @@ function missionSortTime(mission) {
|
|
|
6264
6373
|
}
|
|
6265
6374
|
|
|
6266
6375
|
function selectDueMission(root = process.cwd(), now = new Date(), options = {}) {
|
|
6267
|
-
const
|
|
6376
|
+
const missions = listMissions(root);
|
|
6377
|
+
for (const mission of missions) {
|
|
6378
|
+
if (!missionIsRunnable(mission) || !missionObjectiveIsJunk(mission.objective)) continue;
|
|
6379
|
+
stopMissionRecord(mission, 'junk_objective_gate', root, { writeStopReceipt: false });
|
|
6380
|
+
console.error(`auto-stopped junk mission: "${String(mission.objective || '')}"`);
|
|
6381
|
+
}
|
|
6382
|
+
const candidates = missions
|
|
6383
|
+
.filter((mission) => !missionObjectiveIsJunk(mission.objective))
|
|
6268
6384
|
.filter((mission) => missionSelectableForLoop(mission, now))
|
|
6269
6385
|
.filter((mission) => !options.headlessOnly || !runnerUsesCallerSession(mission.runner))
|
|
6270
6386
|
.filter((mission) => !missionTaskHumanAcceptWaiting(mission))
|
|
@@ -6303,7 +6419,10 @@ function missionSelectableForCodexGoal(mission, now = new Date()) {
|
|
|
6303
6419
|
function selectCodexGoalMission(root = process.cwd(), options = {}, now = new Date()) {
|
|
6304
6420
|
const requestedId = String(options.missionId || '').trim();
|
|
6305
6421
|
const candidates = listMissions(root)
|
|
6306
|
-
|
|
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))
|
|
6307
6426
|
.filter((mission) => missionSelectableForCodexGoal(mission, now));
|
|
6308
6427
|
if (requestedId) {
|
|
6309
6428
|
const exact = candidates.find((mission) => missionMatchesRef(mission, requestedId));
|
|
@@ -6318,10 +6437,6 @@ function selectCodexGoalMission(root = process.cwd(), options = {}, now = new Da
|
|
|
6318
6437
|
const bRank = missionGoalSelectionRank(b);
|
|
6319
6438
|
if (aRank !== bRank) return aRank - bRank;
|
|
6320
6439
|
|
|
6321
|
-
const aCaller = runnerUsesCallerSession(a.runner) ? 1 : 0;
|
|
6322
|
-
const bCaller = runnerUsesCallerSession(b.runner) ? 1 : 0;
|
|
6323
|
-
if (aCaller !== bCaller) return bCaller - aCaller;
|
|
6324
|
-
|
|
6325
6440
|
const aVerifier = effectiveMissionVerifier(a) ? 1 : 0;
|
|
6326
6441
|
const bVerifier = effectiveMissionVerifier(b) ? 1 : 0;
|
|
6327
6442
|
if (aVerifier !== bVerifier) return bVerifier - aVerifier;
|
|
@@ -6614,19 +6729,13 @@ function writeDirectRunCodexGoalRequest(mission, root = process.cwd()) {
|
|
|
6614
6729
|
requested_at: stampIso(),
|
|
6615
6730
|
};
|
|
6616
6731
|
const paths = statePaths(root);
|
|
6617
|
-
|
|
6618
|
-
fs.writeFileSync(paths.codexGoalRequestJson, JSON.stringify(request, null, 2) + '\n', 'utf8');
|
|
6732
|
+
writeJson(paths.codexGoalRequestJson, request);
|
|
6619
6733
|
return request;
|
|
6620
6734
|
}
|
|
6621
6735
|
|
|
6622
6736
|
function readDirectRunCodexGoalRequest(root = process.cwd(), now = new Date()) {
|
|
6623
6737
|
const file = statePaths(root).codexGoalRequestJson;
|
|
6624
|
-
|
|
6625
|
-
try {
|
|
6626
|
-
request = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
6627
|
-
} catch {
|
|
6628
|
-
return null;
|
|
6629
|
-
}
|
|
6738
|
+
const request = readJson(file);
|
|
6630
6739
|
const missionId = String(request?.mission_id || '').trim();
|
|
6631
6740
|
if (!missionId) return null;
|
|
6632
6741
|
const mission = resolveMission(missionId, root);
|
|
@@ -6637,13 +6746,9 @@ function readDirectRunCodexGoalRequest(root = process.cwd(), now = new Date()) {
|
|
|
6637
6746
|
|
|
6638
6747
|
function clearDirectRunCodexGoalRequestForMission(missionId, root = process.cwd()) {
|
|
6639
6748
|
const file = statePaths(root).codexGoalRequestJson;
|
|
6640
|
-
|
|
6641
|
-
|
|
6642
|
-
|
|
6643
|
-
} catch {
|
|
6644
|
-
return false;
|
|
6645
|
-
}
|
|
6646
|
-
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;
|
|
6647
6752
|
try {
|
|
6648
6753
|
fs.rmSync(file, { force: true });
|
|
6649
6754
|
return true;
|
|
@@ -6961,8 +7066,7 @@ function writeCodexGoalState(payload, root = process.cwd()) {
|
|
|
6961
7066
|
updated_at: stampIso(),
|
|
6962
7067
|
...payload,
|
|
6963
7068
|
};
|
|
6964
|
-
|
|
6965
|
-
fs.writeFileSync(paths.codexGoalJson, JSON.stringify(state, null, 2) + '\n', 'utf8');
|
|
7069
|
+
writeJson(paths.codexGoalJson, state);
|
|
6966
7070
|
|
|
6967
7071
|
const lines = [
|
|
6968
7072
|
'# Codex Goal Controller',
|
|
@@ -7233,8 +7337,7 @@ function writeAtrisGoalState(payload, root = process.cwd()) {
|
|
|
7233
7337
|
updated_at: stampIso(),
|
|
7234
7338
|
...payload,
|
|
7235
7339
|
};
|
|
7236
|
-
|
|
7237
|
-
fs.writeFileSync(paths.atrisGoalJson, JSON.stringify(state, null, 2) + '\n', 'utf8');
|
|
7340
|
+
writeJson(paths.atrisGoalJson, state);
|
|
7238
7341
|
|
|
7239
7342
|
const lines = [
|
|
7240
7343
|
'# Atris Goal Controller',
|
|
@@ -7476,8 +7579,37 @@ function missionJudgmentTopic(mission) {
|
|
|
7476
7579
|
return plain.split(/\s+/).filter(Boolean).slice(0, 14).join(' ');
|
|
7477
7580
|
}
|
|
7478
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
|
+
|
|
7479
7609
|
function missionJudgmentTitle(mission) {
|
|
7480
|
-
const words =
|
|
7610
|
+
const words = trimDanglingTail(
|
|
7611
|
+
missionJudgmentTopic(mission).split(/\s+/).filter(Boolean).slice(0, 8),
|
|
7612
|
+
);
|
|
7481
7613
|
const title = words.join(' ') || 'Mission needs a new direction';
|
|
7482
7614
|
return title.charAt(0).toUpperCase() + title.slice(1);
|
|
7483
7615
|
}
|
|
@@ -7642,12 +7774,11 @@ function missionDriverPaths(mission, root = process.cwd()) {
|
|
|
7642
7774
|
}
|
|
7643
7775
|
|
|
7644
7776
|
function readMissionDriverState(stateFile) {
|
|
7645
|
-
|
|
7777
|
+
return readJson(stateFile, {}) || {};
|
|
7646
7778
|
}
|
|
7647
7779
|
|
|
7648
7780
|
function writeMissionDriverState(stateFile, state) {
|
|
7649
|
-
|
|
7650
|
-
fs.writeFileSync(stateFile, JSON.stringify(state, null, 2) + '\n', 'utf8');
|
|
7781
|
+
writeJson(stateFile, state);
|
|
7651
7782
|
}
|
|
7652
7783
|
|
|
7653
7784
|
function missionDriverError(error) {
|
|
@@ -7740,10 +7871,8 @@ function installDetachedMissionDriverLifecycle(mission, root = process.cwd()) {
|
|
|
7740
7871
|
|
|
7741
7872
|
function missionDriverHealth(mission, root = process.cwd()) {
|
|
7742
7873
|
const paths = missionDriverPaths(mission, root);
|
|
7743
|
-
|
|
7744
|
-
|
|
7745
|
-
try { driverState = JSON.parse(fs.readFileSync(paths.stateFile, 'utf8')); } catch {}
|
|
7746
|
-
try { lockState = JSON.parse(fs.readFileSync(paths.lockFile, 'utf8')); } catch {}
|
|
7874
|
+
const driverState = readJson(paths.stateFile);
|
|
7875
|
+
const lockState = readJson(paths.lockFile);
|
|
7747
7876
|
const candidates = [
|
|
7748
7877
|
driverState ? { ...driverState, source: 'driver_state' } : null,
|
|
7749
7878
|
lockState ? { ...lockState, source: 'mission_lock' } : null,
|
|
@@ -7778,8 +7907,7 @@ function acquireMissionLock(missionId, root = process.cwd(), options = {}) {
|
|
|
7778
7907
|
return { ok: true, lockFile, fd, missionId, driverPid: process.pid, startedAt, recordLength: record.length };
|
|
7779
7908
|
} catch (e) {
|
|
7780
7909
|
if (e.code === 'EEXIST') {
|
|
7781
|
-
|
|
7782
|
-
try { info = JSON.parse(fs.readFileSync(lockFile, 'utf8') || '{}'); } catch {}
|
|
7910
|
+
const info = readJson(lockFile, {}) || {};
|
|
7783
7911
|
const holderPid = Number(info.pid);
|
|
7784
7912
|
const holderKnown = Number.isInteger(holderPid) && holderPid > 0;
|
|
7785
7913
|
// A lock is created empty (openSync 'wx') and its pid record is written a
|
|
@@ -7876,6 +8004,14 @@ function spawnMissionDriver(mission, root = process.cwd()) {
|
|
|
7876
8004
|
log_path: logPath,
|
|
7877
8005
|
}, null, 2) + '\n', 'utf8');
|
|
7878
8006
|
child.unref();
|
|
8007
|
+
// A returned pid is not proof the engine is running: spawn can hand back a
|
|
8008
|
+
// pid for a child that never execs or exits immediately. Verify the process
|
|
8009
|
+
// is actually alive before claiming a live worker, and drop the misleading
|
|
8010
|
+
// driver-state file when it is not.
|
|
8011
|
+
if (!child.pid || !missionLockOwnerIsAlive(child.pid)) {
|
|
8012
|
+
try { fs.rmSync(paths.stateFile, { force: true }); } catch {}
|
|
8013
|
+
return { ok: false, error: 'mission driver exited before it could start', pid: child.pid || null };
|
|
8014
|
+
}
|
|
7879
8015
|
return {
|
|
7880
8016
|
ok: true,
|
|
7881
8017
|
pid: child.pid,
|
|
@@ -7949,7 +8085,13 @@ function detachMissionRun(args, mission, root = process.cwd(), asJson = false) {
|
|
|
7949
8085
|
|
|
7950
8086
|
function probeClaudeBinary() {
|
|
7951
8087
|
const runnerBin = resolveClaudeRunnerBin();
|
|
7952
|
-
|
|
8088
|
+
const commandTemplate = resolveClaudeRunnerCommandTemplate();
|
|
8089
|
+
if (commandTemplate) {
|
|
8090
|
+
// A command template fully replaces the claude invocation (GLM/OpenAI/
|
|
8091
|
+
// local runners). The runner bin only matters when the template actually
|
|
8092
|
+
// references {bin}; requiring `claude` on PATH otherwise defeats the
|
|
8093
|
+
// template's purpose and breaks generic runners on claude-less machines.
|
|
8094
|
+
if (!commandTemplate.includes('{bin}')) return { ok: true };
|
|
7953
8095
|
const probe = spawnSync('sh', ['-c', `command -v ${shellQuote(runnerBin)}`], { encoding: 'utf8', timeout: 8000 });
|
|
7954
8096
|
if (probe.status !== 0) return { ok: false, error: `${runnerBin} CLI not found` };
|
|
7955
8097
|
return { ok: true };
|
|
@@ -8046,8 +8188,9 @@ function buildTickPrompt(mission, tickIndex, maxTicks, frozen, pings = []) {
|
|
|
8046
8188
|
if (mission.task_ids?.length) {
|
|
8047
8189
|
lines.push('', `## Task ids`, mission.task_ids.map((t) => `- ${t}`).join('\n'));
|
|
8048
8190
|
}
|
|
8049
|
-
|
|
8050
|
-
|
|
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'));
|
|
8051
8194
|
}
|
|
8052
8195
|
return lines.join('\n');
|
|
8053
8196
|
}
|
|
@@ -8072,6 +8215,9 @@ function detectUnavailableModel(text) {
|
|
|
8072
8215
|
// need a resume; a model-unavailable pause is a config error a bare resume won't fix,
|
|
8073
8216
|
// so name the dead id and the two knobs that change it.
|
|
8074
8217
|
function missionPauseNextAction(pauseReason, missionId, deadModel = null, lastErrorReason = null) {
|
|
8218
|
+
if (pauseReason === 'protected-lane-review') {
|
|
8219
|
+
return `review the protected diff and its receipt before resuming: atris mission run ${missionId}`;
|
|
8220
|
+
}
|
|
8075
8221
|
if (pauseReason === 'too-deep') {
|
|
8076
8222
|
return 'stopped: helpers were spawning helpers too many levels deep';
|
|
8077
8223
|
}
|
|
@@ -8260,13 +8406,31 @@ function spawnGenericRunnerTick(mission, opts) {
|
|
|
8260
8406
|
resolve({ ok: false, error: e.message, sessionIds: [], aborted: false, timedOut: false, authExpired: false, brief_id: briefId });
|
|
8261
8407
|
return;
|
|
8262
8408
|
}
|
|
8409
|
+
let gitGuard;
|
|
8410
|
+
try {
|
|
8411
|
+
gitGuard = prepareMissionGitGuard({ root: cwd, tags: missionProtectedTags(mission, cwd) });
|
|
8412
|
+
} catch (error) {
|
|
8413
|
+
resolve({
|
|
8414
|
+
ok: false,
|
|
8415
|
+
reason: 'protected-diff-unreadable',
|
|
8416
|
+
error: error.message || String(error),
|
|
8417
|
+
protected_lane_guard: unreadableMissionGuard(error),
|
|
8418
|
+
sessionIds: [],
|
|
8419
|
+
aborted: false,
|
|
8420
|
+
timedOut: false,
|
|
8421
|
+
authExpired: false,
|
|
8422
|
+
brief_id: briefId,
|
|
8423
|
+
});
|
|
8424
|
+
return;
|
|
8425
|
+
}
|
|
8263
8426
|
|
|
8264
8427
|
const startedAt = Date.now();
|
|
8265
|
-
const
|
|
8428
|
+
const guardedCommand = `PATH=${shellQuote(gitGuard.pathPrefix)}:$PATH; export PATH; ${cmd}`;
|
|
8429
|
+
const proc = spawn('sh', ['-lc', guardedCommand], {
|
|
8266
8430
|
cwd,
|
|
8267
8431
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
8268
8432
|
detached: true,
|
|
8269
|
-
env: { ...
|
|
8433
|
+
env: { ...gitGuard.env, ATRIS_DISPATCH_DEPTH: String(dispatchDepth.depth + 1) },
|
|
8270
8434
|
});
|
|
8271
8435
|
updateMissionLockOwner(missionLock, proc.pid, { phase: 'tick' });
|
|
8272
8436
|
let stdout = '';
|
|
@@ -8297,6 +8461,7 @@ function spawnGenericRunnerTick(mission, opts) {
|
|
|
8297
8461
|
proc.on('close', (code) => {
|
|
8298
8462
|
clearTimeout(timer);
|
|
8299
8463
|
cleanupPrompt();
|
|
8464
|
+
gitGuard.cleanup();
|
|
8300
8465
|
if (signal) signal.removeEventListener?.('abort', onAbort);
|
|
8301
8466
|
updateMissionLockOwner(missionLock, missionLock?.driverPid);
|
|
8302
8467
|
const finalText = String(stdout || '').trim();
|
|
@@ -8325,6 +8490,7 @@ function spawnGenericRunnerTick(mission, opts) {
|
|
|
8325
8490
|
proc.on('error', (e) => {
|
|
8326
8491
|
clearTimeout(timer);
|
|
8327
8492
|
cleanupPrompt();
|
|
8493
|
+
gitGuard.cleanup();
|
|
8328
8494
|
if (signal) signal.removeEventListener?.('abort', onAbort);
|
|
8329
8495
|
updateMissionLockOwner(missionLock, missionLock?.driverPid);
|
|
8330
8496
|
resolve({ ok: false, error: e.message, sessionIds: [], aborted, timedOut, authExpired: false, brief_id: briefId });
|
|
@@ -8373,6 +8539,22 @@ function spawnClaudeTick(mission, opts) {
|
|
|
8373
8539
|
const dispatchDepth = missionDispatchDepth();
|
|
8374
8540
|
if (dispatchDepth.tooDeep) return Promise.resolve(tooDeepRunnerResult(dispatchDepth));
|
|
8375
8541
|
return new Promise((resolve) => {
|
|
8542
|
+
let gitGuard;
|
|
8543
|
+
try {
|
|
8544
|
+
gitGuard = prepareMissionGitGuard({ root: cwd, tags: missionProtectedTags(mission, cwd) });
|
|
8545
|
+
} catch (error) {
|
|
8546
|
+
resolve({
|
|
8547
|
+
ok: false,
|
|
8548
|
+
reason: 'protected-diff-unreadable',
|
|
8549
|
+
error: error.message || String(error),
|
|
8550
|
+
protected_lane_guard: unreadableMissionGuard(error),
|
|
8551
|
+
sessionIds: [],
|
|
8552
|
+
aborted: false,
|
|
8553
|
+
timedOut: false,
|
|
8554
|
+
authExpired: false,
|
|
8555
|
+
});
|
|
8556
|
+
return;
|
|
8557
|
+
}
|
|
8376
8558
|
const args = [
|
|
8377
8559
|
'-p', prompt,
|
|
8378
8560
|
'--output-format', 'stream-json',
|
|
@@ -8392,7 +8574,7 @@ function spawnClaudeTick(mission, opts) {
|
|
|
8392
8574
|
cwd,
|
|
8393
8575
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
8394
8576
|
detached: true,
|
|
8395
|
-
env: { ...
|
|
8577
|
+
env: { ...gitGuard.env, ATRIS_DISPATCH_DEPTH: String(dispatchDepth.depth + 1) },
|
|
8396
8578
|
});
|
|
8397
8579
|
updateMissionLockOwner(missionLock, proc.pid, { phase: 'tick' });
|
|
8398
8580
|
|
|
@@ -8455,6 +8637,7 @@ function spawnClaudeTick(mission, opts) {
|
|
|
8455
8637
|
|
|
8456
8638
|
proc.on('close', (code) => {
|
|
8457
8639
|
clearTimeout(timer);
|
|
8640
|
+
gitGuard.cleanup();
|
|
8458
8641
|
if (signal) signal.removeEventListener?.('abort', onAbort);
|
|
8459
8642
|
updateMissionLockOwner(missionLock, missionLock?.driverPid);
|
|
8460
8643
|
const ok = code === 0 && !isError && !timedOut && !aborted;
|
|
@@ -8484,6 +8667,7 @@ function spawnClaudeTick(mission, opts) {
|
|
|
8484
8667
|
|
|
8485
8668
|
proc.on('error', (e) => {
|
|
8486
8669
|
clearTimeout(timer);
|
|
8670
|
+
gitGuard.cleanup();
|
|
8487
8671
|
updateMissionLockOwner(missionLock, missionLock?.driverPid);
|
|
8488
8672
|
resolve({ ok: false, error: e.message, sessionIds: [], aborted, timedOut, authExpired: false });
|
|
8489
8673
|
});
|
|
@@ -8582,16 +8766,28 @@ function cappedClaudeReceiptText(text, limit = 4000) {
|
|
|
8582
8766
|
return clean.slice(0, limit - 16).trimEnd() + '\n...[truncated]';
|
|
8583
8767
|
}
|
|
8584
8768
|
|
|
8585
|
-
async function
|
|
8769
|
+
async function parseAndValidateMissionRunPhase(args) {
|
|
8586
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
|
+
};
|
|
8587
8780
|
if (hasFlag(args, '--help') || hasFlag(args, '-h')) {
|
|
8588
8781
|
help();
|
|
8589
|
-
|
|
8782
|
+
context.handled = true;
|
|
8783
|
+
return context;
|
|
8590
8784
|
}
|
|
8591
8785
|
if (hasFlag(args, '--cloud')) {
|
|
8592
8786
|
const result = await runCloudMissionCommand(args);
|
|
8593
8787
|
process.exitCode = result.exitCode;
|
|
8594
|
-
|
|
8788
|
+
context.handled = true;
|
|
8789
|
+
context.returnValue = result;
|
|
8790
|
+
return context;
|
|
8595
8791
|
}
|
|
8596
8792
|
// --fleet: staff every idle capable engine on the board's claimable
|
|
8597
8793
|
// safe-lane tasks, build in parallel worktrees, land serially. Humble flag,
|
|
@@ -8610,7 +8806,8 @@ async function runMission(args) {
|
|
|
8610
8806
|
});
|
|
8611
8807
|
if (asJson) console.log(JSON.stringify(flight, null, 2));
|
|
8612
8808
|
process.exitCode = flight.paused && flight.paused.length > 0 ? 1 : 0;
|
|
8613
|
-
|
|
8809
|
+
context.handled = true;
|
|
8810
|
+
return context;
|
|
8614
8811
|
}
|
|
8615
8812
|
const dueMode = hasFlag(args, '--due');
|
|
8616
8813
|
const headlessOnly = hasFlag(args, '--headless');
|
|
@@ -8646,22 +8843,70 @@ async function runMission(args) {
|
|
|
8646
8843
|
const ref = input.ref;
|
|
8647
8844
|
const runArgs = input.args;
|
|
8648
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
|
+
|
|
8649
8892
|
if (!dueMode && !ref) {
|
|
8650
8893
|
if (asJson || !process.stdin.isTTY || !process.stderr.isTTY) {
|
|
8651
8894
|
missionRunInputRequired(asJson, input.owner);
|
|
8652
8895
|
}
|
|
8653
8896
|
const prompted = await promptMissionRunInput(runArgs);
|
|
8654
8897
|
await startMissionFromRunObjective(prompted.objective, prompted.args);
|
|
8898
|
+
context.handled = true;
|
|
8655
8899
|
return;
|
|
8656
8900
|
}
|
|
8657
8901
|
|
|
8658
|
-
let mission = dueMode && !ref ? selectDueMission(
|
|
8902
|
+
let mission = dueMode && !ref ? selectDueMission(cwd, new Date(), { headlessOnly }) : resolveMission(ref, cwd);
|
|
8659
8903
|
if (!mission && dueMode && !ref) {
|
|
8660
8904
|
printJsonOrText(
|
|
8661
8905
|
{ ok: true, action: 'run_skipped', reason: 'no_due_mission', mission: null },
|
|
8662
8906
|
['No due mission found.'],
|
|
8663
8907
|
asJson,
|
|
8664
8908
|
);
|
|
8909
|
+
context.handled = true;
|
|
8665
8910
|
return;
|
|
8666
8911
|
}
|
|
8667
8912
|
// BCK-1319: a bare single token that looks like an id/suffix/number (no
|
|
@@ -8675,6 +8920,7 @@ async function runMission(args) {
|
|
|
8675
8920
|
}
|
|
8676
8921
|
if (!mission && ref) {
|
|
8677
8922
|
await startMissionFromRunObjective(ref, runArgs);
|
|
8923
|
+
context.handled = true;
|
|
8678
8924
|
return;
|
|
8679
8925
|
}
|
|
8680
8926
|
if (!mission) {
|
|
@@ -8688,7 +8934,8 @@ async function runMission(args) {
|
|
|
8688
8934
|
} else if (!maxWallFlag && Number(mission.budget_contract?.requested_seconds) > 0) {
|
|
8689
8935
|
maxWallSeconds = Math.max(60, Number(mission.budget_contract.requested_seconds));
|
|
8690
8936
|
}
|
|
8691
|
-
const detachedDriverLifecycle = installDetachedMissionDriverLifecycle(mission,
|
|
8937
|
+
const detachedDriverLifecycle = installDetachedMissionDriverLifecycle(mission, cwd);
|
|
8938
|
+
context.detachedDriverLifecycle = detachedDriverLifecycle;
|
|
8692
8939
|
if (['complete', 'stopped'].includes(mission.status)) {
|
|
8693
8940
|
detachedDriverLifecycle?.setExitReason(mission.status);
|
|
8694
8941
|
if (asJson) {
|
|
@@ -8697,18 +8944,20 @@ async function runMission(args) {
|
|
|
8697
8944
|
[],
|
|
8698
8945
|
true,
|
|
8699
8946
|
);
|
|
8947
|
+
context.handled = true;
|
|
8700
8948
|
return;
|
|
8701
8949
|
}
|
|
8702
8950
|
console.error(`Mission ${mission.id} is ${mission.status}; nothing to run.`);
|
|
8703
8951
|
process.exit(0);
|
|
8704
8952
|
}
|
|
8705
8953
|
if (hasFlag(args, '--detach')) {
|
|
8706
|
-
detachMissionRun(args, mission,
|
|
8954
|
+
detachMissionRun(args, mission, cwd, asJson);
|
|
8955
|
+
context.handled = true;
|
|
8707
8956
|
return;
|
|
8708
8957
|
}
|
|
8709
8958
|
|
|
8710
8959
|
const nativeGoalRunOptions = codexNativeGoalOptionsFromArgs(args);
|
|
8711
|
-
const autoAck = maybeAutoAckCodexNativeGoal(mission,
|
|
8960
|
+
const autoAck = maybeAutoAckCodexNativeGoal(mission, cwd, nativeGoalRunOptions);
|
|
8712
8961
|
if (autoAck) mission = autoAck.saved;
|
|
8713
8962
|
maybeBlockUntilCodexNativeGoalStarted(runtimeView(mission), asJson, nativeGoalRunOptions);
|
|
8714
8963
|
|
|
@@ -8717,37 +8966,63 @@ async function runMission(args) {
|
|
|
8717
8966
|
// (e.g. --engine cursor) never invokes claude. The in-lock probe below runs
|
|
8718
8967
|
// after applyMissionRunnerProfile and checks the right binary.
|
|
8719
8968
|
|
|
8720
|
-
const lock = acquireMissionLock(mission.id);
|
|
8969
|
+
const lock = acquireMissionLock(mission.id, cwd);
|
|
8721
8970
|
if (!lock.ok) {
|
|
8722
8971
|
exitMissionError(`another driver is already running mission ${mission.id} (pid ${lock.holder?.pid || '?'})`, 3, asJson);
|
|
8723
8972
|
}
|
|
8724
8973
|
|
|
8725
|
-
|
|
8726
|
-
|
|
8727
|
-
|
|
8728
|
-
|
|
8729
|
-
|
|
8730
|
-
|
|
8731
|
-
|
|
8732
|
-
|
|
8733
|
-
|
|
8734
|
-
|
|
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
|
+
}
|
|
8735
8990
|
|
|
8736
|
-
|
|
8737
|
-
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;
|
|
8738
9012
|
const controller = new AbortController();
|
|
8739
|
-
|
|
9013
|
+
context.controller = controller;
|
|
9014
|
+
context.onSig = (signal) => {
|
|
8740
9015
|
detachedDriverLifecycle?.setExitReason(`signal-${String(signal || 'abort').toLowerCase()}`);
|
|
8741
9016
|
controller.abort();
|
|
8742
9017
|
};
|
|
8743
|
-
process.on('SIGINT', onSig);
|
|
8744
|
-
process.on('SIGTERM', onSig);
|
|
9018
|
+
process.on('SIGINT', context.onSig);
|
|
9019
|
+
process.on('SIGTERM', context.onSig);
|
|
8745
9020
|
|
|
8746
9021
|
// Re-read inside the lock. The initial resolveMission ran pre-lock, so a concurrent
|
|
8747
9022
|
// `mission tick` could have written between resolveMission and acquireMissionLock.
|
|
8748
9023
|
// Derive sessionId, pendingSessionId, and the frozen contract from the fresh record
|
|
8749
9024
|
// so a fast tick's writes can't be silently overwritten by this run loop.
|
|
8750
|
-
mission = resolveMission(mission.id) || mission;
|
|
9025
|
+
mission = resolveMission(mission.id, cwd) || mission;
|
|
8751
9026
|
if (explicitMaxWallSeconds && runBudgetContract) {
|
|
8752
9027
|
mission = saveMission({
|
|
8753
9028
|
...mission,
|
|
@@ -8762,10 +9037,12 @@ async function runMission(args) {
|
|
|
8762
9037
|
if (['complete', 'stopped'].includes(mission.status)) {
|
|
8763
9038
|
detachedDriverLifecycle?.setExitReason(mission.status);
|
|
8764
9039
|
console.error(`Mission ${mission.id} is ${mission.status}; nothing to run.`);
|
|
9040
|
+
context.handled = true;
|
|
8765
9041
|
return;
|
|
8766
9042
|
}
|
|
8767
9043
|
if (returnIfCodexNativeGoalNotStarted(runtimeMission, asJson, nativeGoalRunOptions)) {
|
|
8768
9044
|
detachedDriverLifecycle?.setExitReason('native-goal-not-started');
|
|
9045
|
+
context.handled = true;
|
|
8769
9046
|
return;
|
|
8770
9047
|
}
|
|
8771
9048
|
if (mission.status === 'paused') {
|
|
@@ -8782,7 +9059,7 @@ async function runMission(args) {
|
|
|
8782
9059
|
sessionId = mission.claude_session_id || null;
|
|
8783
9060
|
pendingSessionId = mission.pending_session_id || null;
|
|
8784
9061
|
const autoRunner = String(runtimeMission.runner || '').trim().toLowerCase() === MISSION_AUTO_RUNNER;
|
|
8785
|
-
restoreRunnerProfile = autoRunner ? () => {} : applyMissionRunnerProfile(runtimeMission.runner);
|
|
9062
|
+
context.restoreRunnerProfile = autoRunner ? () => {} : applyMissionRunnerProfile(runtimeMission.runner);
|
|
8786
9063
|
const callerSessionRunner = runnerUsesCallerSession(runtimeMission.runner);
|
|
8787
9064
|
const runnerName = String(runtimeMission.runner || '').trim().toLowerCase();
|
|
8788
9065
|
const atris2Runner = runnerName === 'atris2';
|
|
@@ -8826,6 +9103,10 @@ async function runMission(args) {
|
|
|
8826
9103
|
const startedAt = Date.now();
|
|
8827
9104
|
let backoffAttempt = 0;
|
|
8828
9105
|
let lastRateLimit = null;
|
|
9106
|
+
// A usage-wall cooldown belongs to the engine that hit it, not the whole run.
|
|
9107
|
+
// Tracking which engine set it lets an auto-runner swap to another ready engine
|
|
9108
|
+
// and keep working instead of waiting the wall out (or pausing for a human).
|
|
9109
|
+
let rateLimitedEngineId = null;
|
|
8829
9110
|
let continuationGoal = null;
|
|
8830
9111
|
|
|
8831
9112
|
const sessionLabel = skipWorker
|
|
@@ -8845,7 +9126,7 @@ async function runMission(args) {
|
|
|
8845
9126
|
// Re-read before measuring the wall. Detached full-budget drivers use
|
|
8846
9127
|
// the mission contract as their wall so process startup drift cannot
|
|
8847
9128
|
// shorten the promised work window.
|
|
8848
|
-
mission = resolveMission(mission.id) || mission;
|
|
9129
|
+
mission = resolveMission(mission.id, cwd) || mission;
|
|
8849
9130
|
runtimeMission = runtimeView(mission);
|
|
8850
9131
|
const contractualRemaining = detachedDriverLifecycle && mission.always_on && missionSpendsFullBudget(mission)
|
|
8851
9132
|
? missionFullBudgetRemainingSeconds(mission)
|
|
@@ -8862,6 +9143,8 @@ async function runMission(args) {
|
|
|
8862
9143
|
if (['complete', 'stopped', 'paused'].includes(mission.status)) { pauseReason = mission.status; break; }
|
|
8863
9144
|
if (effectiveMissionVerifier(mission) !== storedVerifier) { pauseReason = 'verifier-mutated'; break; }
|
|
8864
9145
|
if ((mission.lane || 'workspace') !== frozen.lane) { pauseReason = 'lane-mutated'; break; }
|
|
9146
|
+
const protectedHold = missionProtectedLaneHold(mission);
|
|
9147
|
+
if (protectedHold) { pauseReason = protectedHold.pause_reason; break; }
|
|
8865
9148
|
|
|
8866
9149
|
const tickIdx = Number(mission.last_tick_index || 0) + 1;
|
|
8867
9150
|
const tickStart = stampIso();
|
|
@@ -8885,6 +9168,18 @@ async function runMission(args) {
|
|
|
8885
9168
|
result.engine_fallback_reason = tickSelection.engine_fallback_reason;
|
|
8886
9169
|
}
|
|
8887
9170
|
|
|
9171
|
+
// Usage-wall engine swap: when a prior tick's engine hit a rate-limit wall and
|
|
9172
|
+
// the auto runner has already resolved this tick to a *different* ready engine
|
|
9173
|
+
// (the walled one is now credit_out and excluded from resolution), the run-wide
|
|
9174
|
+
// cooldown no longer applies. Clear it so the fresh engine runs now instead of
|
|
9175
|
+
// the loop sleeping through — or pausing on `rate-limit-exceeded-wall` for — a
|
|
9176
|
+
// wall this engine never hit.
|
|
9177
|
+
if (lastRateLimit && rateLimitedEngineId && tickEngineId && tickEngineId !== rateLimitedEngineId) {
|
|
9178
|
+
result.engine_swapped_from = rateLimitedEngineId;
|
|
9179
|
+
lastRateLimit = null;
|
|
9180
|
+
rateLimitedEngineId = null;
|
|
9181
|
+
}
|
|
9182
|
+
|
|
8888
9183
|
// Active-hours gate
|
|
8889
9184
|
if (engineBackedTick && !tickEngineId) {
|
|
8890
9185
|
result = { ...result, status: 'errored', reason: 'no-ready-engine' };
|
|
@@ -8916,55 +9211,76 @@ async function runMission(args) {
|
|
|
8916
9211
|
drill: drillResult,
|
|
8917
9212
|
};
|
|
8918
9213
|
} else if (tickAtris2Runner) {
|
|
8919
|
-
|
|
8920
|
-
mission = pingDrain.mission;
|
|
8921
|
-
runtimeMission = runtimeView(mission);
|
|
8922
|
-
const prompt = buildTickPrompt(tickRuntimeMission, tickIdx, effectiveMaxTicks, frozen, pingDrain.pings);
|
|
8923
|
-
const { runAtris2Turn } = require('./probe');
|
|
8924
|
-
const businessId = businessIdForAtris2Mission(tickRuntimeMission, cwd);
|
|
8925
|
-
const tickController = new AbortController();
|
|
8926
|
-
let wallExpired = false;
|
|
8927
|
-
const abortTick = () => tickController.abort();
|
|
8928
|
-
controller.signal.addEventListener('abort', abortTick, { once: true });
|
|
8929
|
-
const tickTimer = setTimeout(() => {
|
|
8930
|
-
wallExpired = true;
|
|
8931
|
-
tickController.abort();
|
|
8932
|
-
}, Math.max(1, Math.floor(remainingWall * 1000)));
|
|
8933
|
-
let turn;
|
|
9214
|
+
let atris2GitGuard = null;
|
|
8934
9215
|
try {
|
|
8935
|
-
|
|
8936
|
-
|
|
8937
|
-
|
|
8938
|
-
|
|
8939
|
-
|
|
8940
|
-
|
|
8941
|
-
|
|
8942
|
-
|
|
8943
|
-
|
|
8944
|
-
|
|
8945
|
-
|
|
8946
|
-
|
|
8947
|
-
|
|
9216
|
+
atris2GitGuard = prepareMissionGitGuard({ root: cwd, tags: missionProtectedTags(mission, cwd) });
|
|
9217
|
+
} catch (error) {
|
|
9218
|
+
result = {
|
|
9219
|
+
...result,
|
|
9220
|
+
status: 'ran',
|
|
9221
|
+
reason: 'protected-diff-unreadable',
|
|
9222
|
+
ran: true,
|
|
9223
|
+
atris2: {
|
|
9224
|
+
ok: false,
|
|
9225
|
+
error: error.message || String(error),
|
|
9226
|
+
protected_lane_guard: unreadableMissionGuard(error),
|
|
9227
|
+
},
|
|
9228
|
+
};
|
|
8948
9229
|
}
|
|
8949
|
-
|
|
8950
|
-
|
|
8951
|
-
|
|
8952
|
-
|
|
8953
|
-
|
|
8954
|
-
|
|
8955
|
-
|
|
8956
|
-
|
|
8957
|
-
|
|
8958
|
-
|
|
8959
|
-
|
|
8960
|
-
|
|
8961
|
-
|
|
8962
|
-
|
|
8963
|
-
|
|
8964
|
-
|
|
8965
|
-
|
|
8966
|
-
|
|
8967
|
-
|
|
9230
|
+
if (atris2GitGuard) {
|
|
9231
|
+
const pingDrain = consumeMissionPings(mission, cwd);
|
|
9232
|
+
mission = pingDrain.mission;
|
|
9233
|
+
runtimeMission = runtimeView(mission);
|
|
9234
|
+
const prompt = buildTickPrompt(tickRuntimeMission, tickIdx, effectiveMaxTicks, frozen, pingDrain.pings);
|
|
9235
|
+
const { runAtris2Turn } = require('./probe');
|
|
9236
|
+
const businessId = businessIdForAtris2Mission(tickRuntimeMission, cwd);
|
|
9237
|
+
const tickController = new AbortController();
|
|
9238
|
+
let wallExpired = false;
|
|
9239
|
+
const abortTick = () => tickController.abort();
|
|
9240
|
+
controller.signal.addEventListener('abort', abortTick, { once: true });
|
|
9241
|
+
const tickTimer = setTimeout(() => {
|
|
9242
|
+
wallExpired = true;
|
|
9243
|
+
tickController.abort();
|
|
9244
|
+
}, Math.max(1, Math.floor(remainingWall * 1000)));
|
|
9245
|
+
let turn;
|
|
9246
|
+
try {
|
|
9247
|
+
turn = await runAtris2Turn({
|
|
9248
|
+
prompt,
|
|
9249
|
+
model: tickRuntimeMission.model || 'atris:fast',
|
|
9250
|
+
business: businessId,
|
|
9251
|
+
maxTurns: 16,
|
|
9252
|
+
signal: tickController.signal,
|
|
9253
|
+
// CLI-231: mission ticks execute relayed ops in the mission's own
|
|
9254
|
+
// workspace, not the hosted ai-computer filesystem.
|
|
9255
|
+
localCwd: cwd,
|
|
9256
|
+
localEnv: atris2GitGuard.env,
|
|
9257
|
+
localPathPrefix: atris2GitGuard.pathPrefix,
|
|
9258
|
+
});
|
|
9259
|
+
} finally {
|
|
9260
|
+
atris2GitGuard.cleanup();
|
|
9261
|
+
clearTimeout(tickTimer);
|
|
9262
|
+
controller.signal.removeEventListener('abort', abortTick);
|
|
9263
|
+
}
|
|
9264
|
+
result.atris2 = {
|
|
9265
|
+
ok: turn.ok,
|
|
9266
|
+
engine: turn.engine,
|
|
9267
|
+
model: tickRuntimeMission.model || 'atris:fast',
|
|
9268
|
+
tools_run: turn.tools_run,
|
|
9269
|
+
unsupported: turn.unsupported,
|
|
9270
|
+
duration_ms: turn.duration_ms,
|
|
9271
|
+
error: turn.error,
|
|
9272
|
+
backend_unavailable: isTransientAtris2BackendError(turn.error) || undefined,
|
|
9273
|
+
receipt_text: String(turn.text || '').slice(0, 4000),
|
|
9274
|
+
};
|
|
9275
|
+
if (controller.signal.aborted) { pauseReason = 'aborted-during-atris2'; break; }
|
|
9276
|
+
if (turn.error === 'not-logged-in') { pauseReason = 'auth-required'; break; }
|
|
9277
|
+
if (wallExpired) {
|
|
9278
|
+
result = { ...result, status: 'errored', reason: 'wall-exceeded-during-tick' };
|
|
9279
|
+
} else if (!turn.ok || !String(turn.text || '').trim()) {
|
|
9280
|
+
result = { ...result, status: 'errored', reason: atris2TurnErrorReason(turn.error) };
|
|
9281
|
+
} else {
|
|
9282
|
+
result = { ...result, status: 'ran', reason: 'tick-ok', ran: true };
|
|
9283
|
+
}
|
|
8968
9284
|
}
|
|
8969
9285
|
} else {
|
|
8970
9286
|
let sessionMode = sessionId ? 'resume' : 'set';
|
|
@@ -8979,9 +9295,9 @@ async function runMission(args) {
|
|
|
8979
9295
|
const runClaudeSession = () => spawnClaudeTick(tickRuntimeMission, {
|
|
8980
9296
|
sessionMode, sessionId: useId, cwd, signal: controller.signal,
|
|
8981
9297
|
missionLock: lock,
|
|
8982
|
-
timeoutMs:
|
|
9298
|
+
timeoutMs: clampTimeoutMs(
|
|
9299
|
+
(maxWallSeconds - ((Date.now() - startedAt) / 1000)) * 1000,
|
|
8983
9300
|
MISSION_RUN_DEFAULTS.claudeTimeoutMs,
|
|
8984
|
-
Math.max(1, Math.floor((maxWallSeconds - ((Date.now() - startedAt) / 1000)) * 1000)),
|
|
8985
9301
|
),
|
|
8986
9302
|
prompt,
|
|
8987
9303
|
model: resolveClaudeRunnerModel(tickRuntimeMission),
|
|
@@ -9006,6 +9322,7 @@ async function runMission(args) {
|
|
|
9006
9322
|
result.claude = {
|
|
9007
9323
|
ok: claudeResult.ok,
|
|
9008
9324
|
reason: claudeResult.reason,
|
|
9325
|
+
protected_lane_guard: claudeResult.protected_lane_guard || null,
|
|
9009
9326
|
brief_id: claudeResult.brief_id || null,
|
|
9010
9327
|
summary: claudeResult.summary,
|
|
9011
9328
|
receipt_text: claudeResult.receipt_text,
|
|
@@ -9026,6 +9343,7 @@ async function runMission(args) {
|
|
|
9026
9343
|
// treating an allowed resetsAt as one pauses every timed run after
|
|
9027
9344
|
// tick 1 with rate-limit-exceeded-wall.
|
|
9028
9345
|
lastRateLimit = claudeResult.rate_limit_info.status === 'allowed' ? null : claudeResult.rate_limit_info;
|
|
9346
|
+
rateLimitedEngineId = lastRateLimit ? tickEngineId : null;
|
|
9029
9347
|
}
|
|
9030
9348
|
if (claudeResult.aborted) { pauseReason = 'aborted-during-claude'; break; }
|
|
9031
9349
|
if (claudeResult.authExpired) { pauseReason = 'auth-required'; break; }
|
|
@@ -9086,26 +9404,22 @@ async function runMission(args) {
|
|
|
9086
9404
|
if (engineHealth) result.engine_health = engineHealth.health;
|
|
9087
9405
|
}
|
|
9088
9406
|
|
|
9089
|
-
|
|
9090
|
-
|
|
9091
|
-
|
|
9092
|
-
|
|
9093
|
-
|
|
9094
|
-
|
|
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;
|
|
9095
9422
|
let receiptPath = null;
|
|
9096
|
-
if (result.status === 'ran' && verifyEach) {
|
|
9097
|
-
if (frozen.verifier) {
|
|
9098
|
-
verifierResult = runVerifier(frozen.verifier);
|
|
9099
|
-
} else if (!tickSkipWorker) {
|
|
9100
|
-
verifierResult = await runEngineVerifier(tickRuntimeMission, {
|
|
9101
|
-
cwd,
|
|
9102
|
-
signal: controller.signal,
|
|
9103
|
-
tickIndex: tickIdx,
|
|
9104
|
-
});
|
|
9105
|
-
}
|
|
9106
|
-
if (verifierResult) result.verifier_passed = verifierResult.passed;
|
|
9107
|
-
}
|
|
9108
|
-
stampMissionRunnerBrief(cwd, result.claude?.brief_id, result, verifierResult);
|
|
9109
9423
|
|
|
9110
9424
|
// Review-lane drain: always-on loops sweep the agent-safe review actions
|
|
9111
9425
|
// each tick so proof-backed work reaches certified on cadence with zero
|
|
@@ -9246,6 +9560,7 @@ async function runMission(args) {
|
|
|
9246
9560
|
break;
|
|
9247
9561
|
}
|
|
9248
9562
|
|
|
9563
|
+
if (['mission-diff-unreadable', 'protected-lane-review'].includes(pauseReason)) break;
|
|
9249
9564
|
if (callerSessionRunner && result.status === 'ran') break;
|
|
9250
9565
|
if (newStatus === 'complete' || (newStatus === 'ready' && !mission.always_on && !fullBudgetMode)) break;
|
|
9251
9566
|
// BCK-1324: two consecutive ticks that each self-report "ran" but leave no
|
|
@@ -9293,7 +9608,7 @@ async function runMission(args) {
|
|
|
9293
9608
|
if (lastTick && lastTick.status !== 'ran' && !missionRunKeepsRetryingError(lastTick.reason)) pauseReason = 'max-ticks-reached';
|
|
9294
9609
|
}
|
|
9295
9610
|
|
|
9296
|
-
mission = resolveMission(mission.id) || mission;
|
|
9611
|
+
mission = resolveMission(mission.id, cwd) || mission;
|
|
9297
9612
|
const remainingBudgetSeconds = missionFullBudgetRemainingSeconds(mission);
|
|
9298
9613
|
const explicitExit = ['complete', 'stopped', 'paused'].includes(String(mission.status || ''));
|
|
9299
9614
|
const healthyCycleBoundary = !pauseReason || pauseReason === 'max-ticks-reached';
|
|
@@ -9325,6 +9640,97 @@ async function runMission(args) {
|
|
|
9325
9640
|
cycleTickLimit = ticks.length + effectiveMaxTicks;
|
|
9326
9641
|
}
|
|
9327
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
|
+
|
|
9328
9734
|
// BCK-1324: no-progress is a clean, honest stop — the run did what it
|
|
9329
9735
|
// could and correctly recognized there was nothing left to do. It is NOT
|
|
9330
9736
|
// a failure/blocker: pausing it (resumable, retried by cron/self-drive)
|
|
@@ -9438,18 +9844,35 @@ async function runMission(args) {
|
|
|
9438
9844
|
detachedDriverLifecycle?.finish('run-error', error);
|
|
9439
9845
|
throw error;
|
|
9440
9846
|
} finally {
|
|
9441
|
-
if (onSig) {
|
|
9442
|
-
try { process.removeListener('SIGINT', onSig); } catch {}
|
|
9443
|
-
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 {}
|
|
9444
9850
|
}
|
|
9445
|
-
if (restoreRunnerProfile) {
|
|
9446
|
-
try { restoreRunnerProfile(); } catch {}
|
|
9851
|
+
if (context.restoreRunnerProfile) {
|
|
9852
|
+
try { context.restoreRunnerProfile(); } catch {}
|
|
9447
9853
|
}
|
|
9448
9854
|
releaseMissionLock(lock);
|
|
9449
9855
|
detachedDriverLifecycle?.finish();
|
|
9450
9856
|
}
|
|
9451
9857
|
}
|
|
9452
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
|
+
|
|
9453
9876
|
function tickMission(args) {
|
|
9454
9877
|
const asJson = wantsJson(args);
|
|
9455
9878
|
if (hasFlag(args, '--help') || hasFlag(args, '-h') || String(args[0] || '').trim() === 'help') {
|
|
@@ -9516,6 +9939,7 @@ function tickMission(args) {
|
|
|
9516
9939
|
const tickIdx = lastTickIndex + 1;
|
|
9517
9940
|
const tickWorktreeBefore = gitWorktreeSnapshot(cwd);
|
|
9518
9941
|
const worktreeBaseline = loadMissionWorktreeBaseline(mission.id, cwd);
|
|
9942
|
+
const protectedLaneGuard = inspectMissionTickProtectedDiff(mission, tickWorktreeBefore, cwd);
|
|
9519
9943
|
|
|
9520
9944
|
const effectiveVerifier = effectiveMissionVerifier(mission);
|
|
9521
9945
|
const verifierCommand = verify
|
|
@@ -9524,7 +9948,7 @@ function tickMission(args) {
|
|
|
9524
9948
|
if (verifierCommand) assertMissionVerifier(verifierCommand, asJson);
|
|
9525
9949
|
|
|
9526
9950
|
let verifierResult = null;
|
|
9527
|
-
if (verify && verifierCommand) {
|
|
9951
|
+
if (protectedLaneGuard.allowed && verify && verifierCommand) {
|
|
9528
9952
|
verifierResult = runVerifier(verifierCommand);
|
|
9529
9953
|
}
|
|
9530
9954
|
const tickWorktree = worktreeReceipt(tickWorktreeBefore, gitWorktreeSnapshot(cwd), { verifier: verifierCommand || effectiveVerifier, baseline: worktreeBaseline });
|
|
@@ -9532,17 +9956,21 @@ function tickMission(args) {
|
|
|
9532
9956
|
// Same layer classification as the run-tick path; manual ticks carry their
|
|
9533
9957
|
// receipt text in --summary.
|
|
9534
9958
|
const layerInfo = extractLayerFromReceiptText(summary || '', tickWorktree?.new_since_baseline_sample);
|
|
9959
|
+
const guardPauseReason = protectedLaneGuard.unreadable
|
|
9960
|
+
? 'mission-diff-unreadable'
|
|
9961
|
+
: 'protected-lane-review';
|
|
9535
9962
|
const tickRecord = {
|
|
9536
|
-
status: 'ran',
|
|
9537
|
-
reason: 'tick-recorded',
|
|
9963
|
+
status: protectedLaneGuard.allowed ? 'ran' : protectedLaneGuard.status,
|
|
9964
|
+
reason: protectedLaneGuard.allowed ? 'tick-recorded' : guardPauseReason,
|
|
9538
9965
|
tick_index: tickIdx,
|
|
9539
|
-
ran:
|
|
9966
|
+
ran: protectedLaneGuard.allowed,
|
|
9540
9967
|
started_at: tickStart,
|
|
9541
9968
|
claude: { skipped: true, reason: 'orchestrator-is-caller-session' },
|
|
9542
9969
|
summary: summary || null,
|
|
9543
9970
|
layer: layerInfo.layer,
|
|
9544
9971
|
layer_source: layerInfo.source,
|
|
9545
9972
|
verifier_passed: verifierResult ? !!verifierResult.passed : null,
|
|
9973
|
+
protected_lane_guard: protectedLaneGuard,
|
|
9546
9974
|
finished_at: stampIso(),
|
|
9547
9975
|
worktree: tickWorktree,
|
|
9548
9976
|
};
|
|
@@ -9555,6 +9983,7 @@ function tickMission(args) {
|
|
|
9555
9983
|
started_at: tickStart,
|
|
9556
9984
|
},
|
|
9557
9985
|
verifier_result: verifierResult,
|
|
9986
|
+
protected_lane_guard: protectedLaneGuard,
|
|
9558
9987
|
rate_limit_info: null,
|
|
9559
9988
|
worktree: tickWorktree,
|
|
9560
9989
|
});
|
|
@@ -9566,7 +9995,12 @@ function tickMission(args) {
|
|
|
9566
9995
|
? nextCandidateTickAction(mission)
|
|
9567
9996
|
: 'attach task, verifier, or proof');
|
|
9568
9997
|
const nextGoalChain = advanceMissionGoalChain(mission.goal_chain, summary, verifierResult);
|
|
9569
|
-
if (
|
|
9998
|
+
if (!protectedLaneGuard.allowed) {
|
|
9999
|
+
status = 'paused';
|
|
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}`;
|
|
10003
|
+
} else if (verifierResult?.passed && nextGoalChain && !nextGoalChain.pause_ready) {
|
|
9570
10004
|
status = 'running';
|
|
9571
10005
|
nextAction = missionGoalChainNextAction(nextGoalChain);
|
|
9572
10006
|
} else if (verifierResult?.passed) {
|
|
@@ -9591,10 +10025,10 @@ function tickMission(args) {
|
|
|
9591
10025
|
}
|
|
9592
10026
|
const clearsPauseState = !['paused', 'stopped'].includes(status);
|
|
9593
10027
|
const nextMission = {
|
|
9594
|
-
|
|
9595
|
-
|
|
9596
|
-
paused_at: clearsPauseState ? null :
|
|
9597
|
-
stop_reason: clearsPauseState ? null :
|
|
10028
|
+
...mission,
|
|
10029
|
+
status,
|
|
10030
|
+
paused_at: clearsPauseState ? null : tickRecord.finished_at,
|
|
10031
|
+
stop_reason: clearsPauseState ? null : tickRecord.reason,
|
|
9598
10032
|
resumed_at: clearsPauseState && mission.status === 'paused' ? tickRecord.finished_at : mission.resumed_at || null,
|
|
9599
10033
|
receipt_path: receiptPath,
|
|
9600
10034
|
last_tick_at: tickRecord.finished_at,
|
|
@@ -9654,12 +10088,8 @@ function tickMission(args) {
|
|
|
9654
10088
|
// (free text, command strings, missing paths) reads as null and falls back
|
|
9655
10089
|
// to durable mission state.
|
|
9656
10090
|
function readReceiptProof(proof, root = process.cwd()) {
|
|
9657
|
-
|
|
9658
|
-
|
|
9659
|
-
return parsed?.schema === 'atris.mission_receipt.v1' ? parsed : null;
|
|
9660
|
-
} catch {
|
|
9661
|
-
return null;
|
|
9662
|
-
}
|
|
10091
|
+
const parsed = readJson(path.resolve(root, String(proof || '')));
|
|
10092
|
+
return parsed?.schema === 'atris.mission_receipt.v1' ? parsed : null;
|
|
9663
10093
|
}
|
|
9664
10094
|
|
|
9665
10095
|
function receiptShowsPass(receipt) {
|
|
@@ -9698,6 +10128,11 @@ function missionCompletionGate(mission, proof, root = process.cwd()) {
|
|
|
9698
10128
|
|
|
9699
10129
|
function completeMission(args) {
|
|
9700
10130
|
const asJson = wantsJson(args);
|
|
10131
|
+
if (hasFlag(args, '--help') || hasFlag(args, '-h') || String(args[0] || '').trim() === 'help') {
|
|
10132
|
+
console.log('Usage: atris mission complete <id> --proof "..."');
|
|
10133
|
+
console.log('Run `atris mission --help` for the full option list.');
|
|
10134
|
+
process.exit(0);
|
|
10135
|
+
}
|
|
9701
10136
|
const force = hasFlag(args, '--force');
|
|
9702
10137
|
const proof = readFlag(args, '--proof', '');
|
|
9703
10138
|
const ref = stripKnownFlags(args, ['--proof'], ['--json', '--force'])[0] || '';
|
|
@@ -9773,31 +10208,21 @@ function completeMission(args) {
|
|
|
9773
10208
|
);
|
|
9774
10209
|
}
|
|
9775
10210
|
|
|
9776
|
-
function
|
|
9777
|
-
const
|
|
9778
|
-
const reason = readFlag(args, '--reason', 'stopped by operator');
|
|
9779
|
-
const pause = hasFlag(args, '--pause');
|
|
9780
|
-
const ref = stripKnownFlags(args, ['--reason'], ['--json', '--pause'])[0] || '';
|
|
9781
|
-
if (!ref) {
|
|
9782
|
-
exitMissionError('Usage: atris mission stop <id> [--pause] [--reason "..."]', 1, asJson);
|
|
9783
|
-
}
|
|
9784
|
-
const mission = resolveMission(ref);
|
|
9785
|
-
if (!mission) {
|
|
9786
|
-
exitMissingMission(ref, 1, asJson);
|
|
9787
|
-
}
|
|
10211
|
+
function stopMissionRecord(mission, reason, root = process.cwd(), options = {}) {
|
|
10212
|
+
const pause = options.pause === true;
|
|
9788
10213
|
const status = pause ? 'paused' : 'stopped';
|
|
9789
10214
|
// Full stops abandon work, so leave evidence: snapshot the worktree against
|
|
9790
10215
|
// the mission baseline (what did this mission leave dirty?) before pruning it.
|
|
9791
10216
|
let receiptPath = null;
|
|
9792
|
-
if (!pause) {
|
|
9793
|
-
const snapshot = gitWorktreeSnapshot(
|
|
10217
|
+
if (!pause && options.writeStopReceipt !== false) {
|
|
10218
|
+
const snapshot = gitWorktreeSnapshot(root);
|
|
9794
10219
|
const worktree = worktreeReceipt(snapshot, snapshot, {
|
|
9795
10220
|
verifier: mission.verifier,
|
|
9796
|
-
baseline: loadMissionWorktreeBaseline(mission.id,
|
|
10221
|
+
baseline: loadMissionWorktreeBaseline(mission.id, root),
|
|
9797
10222
|
});
|
|
9798
|
-
receiptPath = writeReceipt(mission, { kind: 'mission_stop', reason, worktree });
|
|
10223
|
+
receiptPath = writeReceipt(mission, { kind: 'mission_stop', reason, worktree }, root);
|
|
9799
10224
|
}
|
|
9800
|
-
const baselineSummary = pause ? null : pruneMissionWorktreeBaseline(mission,
|
|
10225
|
+
const baselineSummary = pause ? null : pruneMissionWorktreeBaseline(mission, root);
|
|
9801
10226
|
const next = {
|
|
9802
10227
|
...mission,
|
|
9803
10228
|
status,
|
|
@@ -9808,9 +10233,32 @@ function stopMission(args) {
|
|
|
9808
10233
|
worktree_baseline: baselineSummary || mission.worktree_baseline || null,
|
|
9809
10234
|
next_action: status === 'paused' ? `resume with: atris mission tick ${mission.id}` : 'mission stopped',
|
|
9810
10235
|
};
|
|
9811
|
-
const { mission: saved } = saveMission(next,
|
|
9812
|
-
const directGoalRequestCleared = pause ? false : clearDirectRunCodexGoalRequestForMission(saved.id,
|
|
9813
|
-
const logPath = appendMemberLog(saved.owner, pause ? 'Mission paused' : 'Mission stopped', { mission: saved.objective, reason });
|
|
10236
|
+
const { mission: saved } = saveMission(next, root, pause ? 'mission_paused' : 'mission_stopped', { reason, receipt_path: receiptPath });
|
|
10237
|
+
const directGoalRequestCleared = pause ? false : clearDirectRunCodexGoalRequestForMission(saved.id, root);
|
|
10238
|
+
const logPath = appendMemberLog(saved.owner, pause ? 'Mission paused' : 'Mission stopped', { mission: saved.objective, reason }, root);
|
|
10239
|
+
return { saved, receiptPath, directGoalRequestCleared, logPath };
|
|
10240
|
+
}
|
|
10241
|
+
|
|
10242
|
+
function stopMission(args) {
|
|
10243
|
+
const asJson = wantsJson(args);
|
|
10244
|
+
// Help is read-only: `mission stop <id> --help` must never resolve the id and
|
|
10245
|
+
// stop the mission just to be asked for usage.
|
|
10246
|
+
if (hasFlag(args, '--help') || hasFlag(args, '-h') || String(args[0] || '').trim() === 'help') {
|
|
10247
|
+
console.log('Usage: atris mission stop <id> [--pause] [--reason "..."]');
|
|
10248
|
+
console.log('Run `atris mission --help` for the full option list.');
|
|
10249
|
+
process.exit(0);
|
|
10250
|
+
}
|
|
10251
|
+
const reason = readFlag(args, '--reason', 'stopped by operator');
|
|
10252
|
+
const pause = hasFlag(args, '--pause');
|
|
10253
|
+
const ref = stripKnownFlags(args, ['--reason'], ['--json', '--pause'])[0] || '';
|
|
10254
|
+
if (!ref) {
|
|
10255
|
+
exitMissionError('Usage: atris mission stop <id> [--pause] [--reason "..."]', 1, asJson);
|
|
10256
|
+
}
|
|
10257
|
+
const mission = resolveMission(ref);
|
|
10258
|
+
if (!mission) {
|
|
10259
|
+
exitMissingMission(ref, 1, asJson);
|
|
10260
|
+
}
|
|
10261
|
+
const { saved, receiptPath, directGoalRequestCleared, logPath } = stopMissionRecord(mission, reason, process.cwd(), { pause });
|
|
9814
10262
|
printJsonOrText(
|
|
9815
10263
|
{ ok: true, action: pause ? 'mission_paused' : 'mission_stopped', mission: saved, receipt_path: receiptPath, log_path: logPath, direct_goal_request_cleared: directGoalRequestCleared },
|
|
9816
10264
|
[
|
|
@@ -10343,12 +10791,7 @@ function layersMission(args) {
|
|
|
10343
10791
|
}
|
|
10344
10792
|
for (const file of files) {
|
|
10345
10793
|
if (missionFilter && !file.includes(missionFilter)) continue;
|
|
10346
|
-
|
|
10347
|
-
try {
|
|
10348
|
-
receipt = JSON.parse(fs.readFileSync(path.join(paths.runsDir, file), 'utf8'));
|
|
10349
|
-
} catch {
|
|
10350
|
-
continue;
|
|
10351
|
-
}
|
|
10794
|
+
const receipt = readJson(path.join(paths.runsDir, file));
|
|
10352
10795
|
if (sinceMs != null) {
|
|
10353
10796
|
const atMs = Date.parse(receipt && receipt.at);
|
|
10354
10797
|
if (Number.isNaN(atMs) || atMs < sinceMs) continue;
|
|
@@ -10506,6 +10949,11 @@ function inspectMission(args) {
|
|
|
10506
10949
|
// output so callers (member ping) can compose their own multi-lane report.
|
|
10507
10950
|
function pingMission(args, opts = {}) {
|
|
10508
10951
|
const asJson = args.includes('--json');
|
|
10952
|
+
if (!opts.silent && (hasFlag(args, '--help') || hasFlag(args, '-h') || String(args[0] || '').trim() === 'help')) {
|
|
10953
|
+
console.log('Usage: atris mission ping <id> "<note>" [--from <who>] [--json]');
|
|
10954
|
+
console.log('Run `atris mission --help` for the full option list.');
|
|
10955
|
+
process.exit(0);
|
|
10956
|
+
}
|
|
10509
10957
|
const rest = args.filter((a) => a !== '--json');
|
|
10510
10958
|
let from = process.env.USER || 'operator';
|
|
10511
10959
|
const fromIdx = rest.indexOf('--from');
|
|
@@ -10546,6 +10994,36 @@ function pingMission(args, opts = {}) {
|
|
|
10546
10994
|
return saved;
|
|
10547
10995
|
}
|
|
10548
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
|
+
|
|
10549
11027
|
function missionCommand(args) {
|
|
10550
11028
|
const subcommand = args[0] || 'status';
|
|
10551
11029
|
const rest = args.slice(1);
|
|
@@ -10654,6 +11132,7 @@ function missionCommand(args) {
|
|
|
10654
11132
|
}
|
|
10655
11133
|
|
|
10656
11134
|
module.exports = {
|
|
11135
|
+
missionProtectedLaneHold,
|
|
10657
11136
|
missionCommand,
|
|
10658
11137
|
startMission,
|
|
10659
11138
|
spawnMissionDriver,
|
|
@@ -10670,10 +11149,12 @@ module.exports = {
|
|
|
10670
11149
|
findActiveTwinMission,
|
|
10671
11150
|
TWIN_ACTIVE_STATUSES,
|
|
10672
11151
|
pingMission,
|
|
11152
|
+
answerMissionHumanAsk,
|
|
10673
11153
|
buildTickPrompt,
|
|
10674
11154
|
extractCheckFeedback,
|
|
10675
11155
|
loadMissionMap,
|
|
10676
11156
|
renderMissionStatus,
|
|
11157
|
+
missionObjectiveIsJunk,
|
|
10677
11158
|
selectDueMission,
|
|
10678
11159
|
selectAtrisGoalMission,
|
|
10679
11160
|
selectCodexGoalMission,
|
|
@@ -10682,29 +11163,22 @@ module.exports = {
|
|
|
10682
11163
|
cappedClaudeReceiptText,
|
|
10683
11164
|
extractLayerFromReceiptText,
|
|
10684
11165
|
classifyPathsByLayer,
|
|
10685
|
-
collectMissionDoctorFindings,
|
|
10686
11166
|
resolveClaudeRunnerModel,
|
|
10687
11167
|
resolveClaudeRunnerBin,
|
|
10688
11168
|
businessIdForAtris2Mission,
|
|
10689
11169
|
detectUnavailableModel,
|
|
10690
11170
|
missionPauseNextAction,
|
|
10691
|
-
HUMAN_BLOCKING_PAUSE_REASONS,
|
|
10692
11171
|
composeHumanBlockingPauseMessage,
|
|
10693
11172
|
escalateHumanBlockingPause,
|
|
10694
|
-
humanBlockingPauseReason,
|
|
10695
11173
|
consecutiveSameReasonErrors,
|
|
10696
11174
|
missionVerifierTimeoutMs,
|
|
10697
11175
|
missionLandingStepSummary,
|
|
10698
11176
|
missionLandingLines,
|
|
10699
11177
|
missionVerifierCheckedText,
|
|
10700
11178
|
missionVerifierHighLevelTestText,
|
|
10701
|
-
missionTickVerification,
|
|
10702
|
-
missionVerificationDebt,
|
|
10703
|
-
missionVerificationDebtLine,
|
|
10704
|
-
missionReportFor,
|
|
10705
|
-
renderMemberNowMarkdown,
|
|
10706
11179
|
buildEngineVerifyPrompt,
|
|
10707
11180
|
engineVerifierResultFromRun,
|
|
11181
|
+
runVerifier,
|
|
10708
11182
|
missionFullBudgetRemainingSeconds,
|
|
10709
11183
|
missionBudgetContinuationText,
|
|
10710
11184
|
missionHumanStatusText,
|
|
@@ -10718,4 +11192,6 @@ module.exports = {
|
|
|
10718
11192
|
missionJudgmentCard,
|
|
10719
11193
|
missionShippedCard,
|
|
10720
11194
|
appendMissionJudgmentCard,
|
|
11195
|
+
acquireMissionLock,
|
|
11196
|
+
releaseMissionLock,
|
|
10721
11197
|
};
|