atris 3.37.0 → 3.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/atris/PERSONA.md +3 -7
- package/atris.md +10 -2
- package/ax +158 -33
- package/bin/atris.js +49 -2
- package/commands/activate.js +16 -3
- package/commands/agent-spawn.js +0 -7
- package/commands/autoland.js +92 -14
- package/commands/autopilot-front.js +0 -4
- package/commands/autopilot.js +0 -12
- package/commands/brain.js +0 -7
- package/commands/brief.js +0 -2
- package/commands/business-sync.js +0 -1
- package/commands/business.js +107 -12
- package/commands/clean.js +31 -2
- package/commands/close.js +46 -11
- package/commands/cloud.js +0 -1
- package/commands/codex-goal.js +0 -2
- package/commands/computer.js +6 -5
- package/commands/console.js +46 -9
- package/commands/deck.js +29 -8
- package/commands/dream.js +0 -7
- package/commands/drill.js +0 -1
- package/commands/engine.js +10 -18
- package/commands/experiments.js +0 -4
- package/commands/fleet-report.js +12 -4
- package/commands/game.js +261 -0
- package/commands/gm.js +10 -2
- package/commands/goal.js +0 -6
- package/commands/harvest.js +0 -1
- package/commands/improve.js +29 -8
- package/commands/init.js +18 -34
- package/commands/integrations.js +0 -7
- package/commands/land.js +71 -8
- package/commands/launchpad.js +0 -2
- package/commands/lesson.js +17 -7
- package/commands/loop-front.js +0 -2
- package/commands/loops.js +6 -5
- package/commands/meet.js +0 -2
- package/commands/member.js +68 -3
- package/commands/mission.js +359 -102
- 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 +670 -77
- package/commands/play.js +0 -2
- package/commands/probe.js +10 -4
- package/commands/pulse.js +73 -48
- package/commands/push.js +0 -4
- package/commands/radar.js +14 -11
- package/commands/recover.js +340 -0
- package/commands/report.js +0 -1
- package/commands/router.js +319 -0
- package/commands/run-front.js +0 -2
- package/commands/search.js +0 -3
- package/commands/skill.js +7 -2
- package/commands/slop.js +225 -7
- package/commands/status.js +6 -0
- package/commands/stream.js +0 -2
- package/commands/study.js +0 -3
- package/commands/sync-checkout.js +51 -0
- package/commands/sync.js +11 -40
- package/commands/task.js +125 -39
- package/commands/teach.js +86 -0
- package/commands/unknowns.js +0 -10
- package/commands/update.js +0 -1
- package/commands/visualize.js +0 -5
- package/commands/watch.js +0 -6
- package/commands/wiki.js +41 -11
- package/commands/wish.js +0 -1
- package/commands/worktree.js +87 -14
- package/commands/xp.js +1 -6
- package/commands/youtube.js +0 -2
- package/lib/accept-verify-gate.js +122 -0
- package/lib/activity-stream.js +0 -2
- package/lib/auto-accept-certified.js +145 -2
- package/lib/autoland.js +0 -3
- package/lib/ax-auto-lane.js +94 -10
- package/lib/bench/context.js +0 -2
- package/lib/bench/report.js +0 -1
- package/lib/bench/runner.js +0 -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 +0 -1
- 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 +16 -8
- package/lib/experiments/daily.js +0 -6
- package/lib/fleet.js +210 -27
- package/lib/functional-owner.js +0 -3
- package/lib/inspect-fields.js +0 -1
- package/lib/known-commands.js +2 -2
- package/lib/learnings.js +0 -2
- package/lib/loop-doctor.js +11 -17
- 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-protected-lane.js +293 -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 +0 -4
- package/lib/orb-context.js +6 -3
- package/lib/orb-scorecard.js +0 -1
- package/lib/outbound-send-gate.js +0 -6
- 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 +0 -2
- package/lib/runner-command.js +0 -4
- package/lib/runtime-bootstrap.js +0 -1
- package/lib/scorecard.js +0 -1
- package/lib/self-drive.js +1 -1
- package/lib/state-detection.js +0 -6
- package/lib/task-db.js +0 -4
- package/lib/team-presence.js +0 -1
- package/lib/voice-gate.js +63 -2
- package/lib/wiki.js +152 -9
- package/lib/wish-audit.js +20 -6
- package/lib/wish-delegate.js +26 -20
- 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 +0 -1
- package/package.json +1 -1
package/commands/mission.js
CHANGED
|
@@ -78,6 +78,11 @@ const {
|
|
|
78
78
|
resolveDefaultVerifier,
|
|
79
79
|
} = require('../lib/default-verifier');
|
|
80
80
|
const { redirectToWorkspaceRoot } = require('../lib/mission-root');
|
|
81
|
+
const {
|
|
82
|
+
inspectMissionProtectedDiff,
|
|
83
|
+
prepareMissionGitGuard,
|
|
84
|
+
unreadableMissionGuard,
|
|
85
|
+
} = require('../lib/mission-protected-lane');
|
|
81
86
|
|
|
82
87
|
const VALID_STATUSES = new Set(['planning', 'running', 'ready', 'paused', 'blocked', 'stopped', 'complete']);
|
|
83
88
|
const TERMINAL_STATUSES = new Set(['stopped', 'complete']);
|
|
@@ -1697,6 +1702,16 @@ function missionLastStepSummary(ticks = []) {
|
|
|
1697
1702
|
return '';
|
|
1698
1703
|
}
|
|
1699
1704
|
|
|
1705
|
+
function verifierElapsedSeconds(verifierResult) {
|
|
1706
|
+
const ms = Number(verifierResult && verifierResult.elapsed_ms);
|
|
1707
|
+
return Number.isFinite(ms) ? Math.round(ms / 1000) : 0;
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
function verifierTimeoutSeconds(verifierResult) {
|
|
1711
|
+
const ms = Number(verifierResult && verifierResult.timeout_ms);
|
|
1712
|
+
return Number.isFinite(ms) ? Math.round(ms / 1000) : 0;
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1700
1715
|
function missionVerifierCheckedText(verifierResult, mission) {
|
|
1701
1716
|
if (!verifierResult) return 'UNVERIFIED: tick recorded but nothing was checked; treat this increment as unproven.';
|
|
1702
1717
|
if (verifierResult.mode === 'engine-unavailable') {
|
|
@@ -1708,6 +1723,9 @@ function missionVerifierCheckedText(verifierResult, mission) {
|
|
|
1708
1723
|
: 'VERIFY FAILED: the engine verify pass did not end with VERDICT: PASS.';
|
|
1709
1724
|
}
|
|
1710
1725
|
const command = verifierResult.command || mission.verifier || 'configured verifier';
|
|
1726
|
+
if (!verifierResult.passed && verifierResult.timed_out) {
|
|
1727
|
+
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.`;
|
|
1728
|
+
}
|
|
1711
1729
|
if (verifierResult.passed) {
|
|
1712
1730
|
if (/^git\s+diff\s+--check\b/i.test(command)) return 'I ran the diff cleanliness check.';
|
|
1713
1731
|
if (/\bnode\s+--test\b/i.test(command)) return 'I ran the behavior checks.';
|
|
@@ -1732,6 +1750,9 @@ function missionVerifierHighLevelTestText(verifierResult, mission) {
|
|
|
1732
1750
|
: 'The verify engine did not provide a passing changed-surface verdict.';
|
|
1733
1751
|
}
|
|
1734
1752
|
const command = verifierResult.command || mission.verifier || 'configured verifier';
|
|
1753
|
+
if (!verifierResult.passed && verifierResult.timed_out) {
|
|
1754
|
+
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.`;
|
|
1755
|
+
}
|
|
1735
1756
|
const outcome = verifierResult.passed ? 'passed' : 'failed';
|
|
1736
1757
|
if (/^git\s+diff\s+--check\b/i.test(command)) {
|
|
1737
1758
|
return `Diff cleanliness check ${outcome}: no whitespace or patch-format issues in the changed files.`;
|
|
@@ -1964,12 +1985,22 @@ function shouldMissionRunTrustedRoom(rawObjective, args = []) {
|
|
|
1964
1985
|
return missionRunTrustedRoomSignals(rawObjective);
|
|
1965
1986
|
}
|
|
1966
1987
|
|
|
1988
|
+
const MISSION_BARE_COMMAND_WORDS = new Set(['go', 'keep', 'going', 'do', 'it', 'start', 'run', 'continue']);
|
|
1989
|
+
|
|
1990
|
+
function missionObjectiveIsJunk(value) {
|
|
1991
|
+
const objective = String(value || '').trim();
|
|
1992
|
+
if (!objective || objective.startsWith('-')) return true;
|
|
1993
|
+
if (/[\u0000-\u001f\u007f-\u009f\ufffd]/u.test(objective)) return true;
|
|
1994
|
+
if (objective.replace(/\s/g, '').length < 8) return true;
|
|
1995
|
+
const words = objective.toLowerCase().match(/[a-z]+/g) || [];
|
|
1996
|
+
return words.length > 0
|
|
1997
|
+
&& objective.replace(/[^a-z]+/gi, '').length > 0
|
|
1998
|
+
&& words.every((word) => MISSION_BARE_COMMAND_WORDS.has(word));
|
|
1999
|
+
}
|
|
2000
|
+
|
|
1967
2001
|
function missionRunConcreteTitle(value) {
|
|
1968
2002
|
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;
|
|
2003
|
+
return missionObjectiveIsJunk(title) ? '' : title;
|
|
1973
2004
|
}
|
|
1974
2005
|
|
|
1975
2006
|
function selectMissionRunUsefulTarget(rawObjective, root = process.cwd()) {
|
|
@@ -2207,6 +2238,9 @@ function missionFullBudgetRemainingSeconds(mission, nowMs = Date.now()) {
|
|
|
2207
2238
|
}
|
|
2208
2239
|
|
|
2209
2240
|
function missionBudgetContinuationText(mission, nowMs = Date.now()) {
|
|
2241
|
+
// Paused/stopped/blocked missions are not running: never sell remaining time
|
|
2242
|
+
// as a live commitment (CLI-1192: paused overnight loops read as healthy).
|
|
2243
|
+
if (!GOAL_LOOP_STATUSES.has(String(mission?.status || ''))) return null;
|
|
2210
2244
|
const remaining = missionFullBudgetRemainingSeconds(mission, nowMs);
|
|
2211
2245
|
if (remaining <= 0) return null;
|
|
2212
2246
|
const budget = String(mission?.budget_contract?.budget_label || 'promised time').trim();
|
|
@@ -2218,10 +2252,15 @@ function missionBudgetContinuationText(mission, nowMs = Date.now()) {
|
|
|
2218
2252
|
}
|
|
2219
2253
|
|
|
2220
2254
|
function missionHumanStatusText(mission, nowMs = Date.now()) {
|
|
2255
|
+
const rawStatus = String(mission?.status || 'unknown');
|
|
2256
|
+
if (rawStatus === 'paused') {
|
|
2257
|
+
const reason = String(mission?.stop_reason || mission?.pause_reason || '').trim();
|
|
2258
|
+
return gateForHuman(reason ? `paused: ${reason}` : 'paused').text;
|
|
2259
|
+
}
|
|
2221
2260
|
const continuation = missionBudgetContinuationText(mission, nowMs);
|
|
2222
2261
|
const status = continuation
|
|
2223
2262
|
? `working for the full ${String(mission?.budget_contract?.budget_label || 'promised time').trim()}`
|
|
2224
|
-
:
|
|
2263
|
+
: rawStatus;
|
|
2225
2264
|
return gateForHuman(status).text;
|
|
2226
2265
|
}
|
|
2227
2266
|
|
|
@@ -3852,16 +3891,15 @@ function findActiveTwinMission(objective, owner, root = process.cwd()) {
|
|
|
3852
3891
|
function startMission(args, options = {}) {
|
|
3853
3892
|
const asJson = wantsJson(args);
|
|
3854
3893
|
const firstArg = String(args[0] || '').trim().toLowerCase();
|
|
3855
|
-
if (
|
|
3894
|
+
if (args.length === 1 && (firstArg === '--help' || firstArg === '-h' || firstArg === 'help')) {
|
|
3856
3895
|
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
3896
|
console.log('Run `atris mission --help` for the full option list.');
|
|
3858
3897
|
process.exit(0);
|
|
3859
3898
|
}
|
|
3860
3899
|
let mission = applyMissionStartPatch(missionFromArgs(args), options.missionPatch);
|
|
3861
|
-
// A flag-looking or empty objective is a typo, not a mission.
|
|
3862
3900
|
const rawObjective = String(mission.objective || '').trim();
|
|
3863
|
-
if (
|
|
3864
|
-
exitMissionError(`mission start
|
|
3901
|
+
if (missionObjectiveIsJunk(rawObjective)) {
|
|
3902
|
+
exitMissionError(`mission start refused junk objective: ${rawObjective ? `"${rawObjective}"` : 'nothing'}`, 1, asJson);
|
|
3865
3903
|
}
|
|
3866
3904
|
// Pasting a mission id where an objective goes is an id mismatch, not a new
|
|
3867
3905
|
// mission: recover the existing record or explain where it actually lives.
|
|
@@ -4251,6 +4289,11 @@ async function startMissionFromRunObjective(objective, args) {
|
|
|
4251
4289
|
|
|
4252
4290
|
function attachMissionTask(args) {
|
|
4253
4291
|
const asJson = wantsJson(args);
|
|
4292
|
+
if (hasFlag(args, '--help') || hasFlag(args, '-h') || String(args[0] || '').trim() === 'help') {
|
|
4293
|
+
console.log('Usage: atris mission attach-task <id> [--json]');
|
|
4294
|
+
console.log('Run `atris mission --help` for the full option list.');
|
|
4295
|
+
process.exit(0);
|
|
4296
|
+
}
|
|
4254
4297
|
const ref = stripKnownFlags(args, [], ['--json'])[0] || '';
|
|
4255
4298
|
if (!ref) {
|
|
4256
4299
|
exitMissionError('Usage: atris mission attach-task <id> [--json]', 1, asJson);
|
|
@@ -5148,12 +5191,24 @@ function missionReportFor(mission, root = process.cwd()) {
|
|
|
5148
5191
|
const timeline = missionReportTimeline(mission, root);
|
|
5149
5192
|
const verificationDebt = missionVerificationDebt(mission, root);
|
|
5150
5193
|
const workerCheckedIn = Boolean(explicitWorkerReceipt) || timeline.length > 0;
|
|
5194
|
+
const driver = missionDriverHealth(mission, root);
|
|
5195
|
+
const driverAlive = Boolean(driver && driver.alive);
|
|
5151
5196
|
const workerReceiptPath = mission.worker_receipt_path || (receipt && verifierReceiptPath) || null;
|
|
5152
5197
|
const verifierPassed = mission.verifier_result && mission.verifier_result.passed === true;
|
|
5153
5198
|
const pendingBudgetContinuation = missionBudgetContinuationText(mission);
|
|
5154
5199
|
const budgetContinuation = workerCheckedIn ? pendingBudgetContinuation : null;
|
|
5155
|
-
const
|
|
5200
|
+
const awaitingFirstWorker = !workerCheckedIn && Boolean(pendingBudgetContinuation);
|
|
5201
|
+
// An active status implies a worker is running. With no receipt AND no live
|
|
5202
|
+
// driver, that claim is a lie: report that no worker is running and name the
|
|
5203
|
+
// command that starts one instead of saying the mission is in progress.
|
|
5204
|
+
const noLiveWorker = !workerCheckedIn
|
|
5205
|
+
&& !driverAlive
|
|
5206
|
+
&& !pendingBudgetContinuation
|
|
5207
|
+
&& GOAL_LOOP_STATUSES.has(String(mission.status || ''));
|
|
5208
|
+
const operatorOutcome = awaitingFirstWorker
|
|
5156
5209
|
? 'No worker has checked in yet.'
|
|
5210
|
+
: noLiveWorker
|
|
5211
|
+
? 'No worker is running; nothing has produced a receipt yet.'
|
|
5157
5212
|
: budgetContinuation
|
|
5158
5213
|
? 'The last verifier passed; full-budget work is continuing.'
|
|
5159
5214
|
: mission.operator_outcome
|
|
@@ -5162,8 +5217,10 @@ function missionReportFor(mission, root = process.cwd()) {
|
|
|
5162
5217
|
id: mission.id,
|
|
5163
5218
|
objective: mission.objective,
|
|
5164
5219
|
status: mission.status,
|
|
5165
|
-
human_status:
|
|
5220
|
+
human_status: awaitingFirstWorker
|
|
5166
5221
|
? 'waiting for the first worker check-in'
|
|
5222
|
+
: noLiveWorker
|
|
5223
|
+
? 'no live worker (nothing is running)'
|
|
5167
5224
|
: missionHumanStatusText(mission),
|
|
5168
5225
|
budget_continuation: budgetContinuation,
|
|
5169
5226
|
operator_outcome: operatorOutcome,
|
|
@@ -5175,9 +5232,10 @@ function missionReportFor(mission, root = process.cwd()) {
|
|
|
5175
5232
|
verifier_receipt_path: verifierReceiptPath,
|
|
5176
5233
|
proof_text: mission.receipt_path ? missionStatusProofText(mission) : null,
|
|
5177
5234
|
operator_next: budgetContinuation
|
|
5178
|
-
|| (
|
|
5235
|
+
|| (awaitingFirstWorker ? 'Wait for the first worker check-in.' : null)
|
|
5179
5236
|
|| mission.operator_next
|
|
5180
5237
|
|| mission.next_action
|
|
5238
|
+
|| (noLiveWorker ? `Start a worker: atris mission run ${mission.id}` : null)
|
|
5181
5239
|
|| 'Review the mission state.',
|
|
5182
5240
|
};
|
|
5183
5241
|
}
|
|
@@ -5857,19 +5915,29 @@ function runVerifier(command, root = process.cwd()) {
|
|
|
5857
5915
|
if (!command) return null;
|
|
5858
5916
|
const resolvedCommand = resolveVerifierCommand(command);
|
|
5859
5917
|
const { envWithNodeDir } = require('../lib/spawn-env');
|
|
5918
|
+
const timeoutMs = missionVerifierTimeoutMs();
|
|
5919
|
+
const startedAt = Date.now();
|
|
5860
5920
|
const result = spawnSync(resolvedCommand, {
|
|
5861
5921
|
cwd: root,
|
|
5862
5922
|
shell: true,
|
|
5863
5923
|
encoding: 'utf8',
|
|
5864
|
-
timeout:
|
|
5924
|
+
timeout: timeoutMs,
|
|
5865
5925
|
env: envWithNodeDir(process.env),
|
|
5866
5926
|
});
|
|
5927
|
+
const elapsedMs = Date.now() - startedAt;
|
|
5928
|
+
// A verifier that blows its window is not a failing check: the code may be
|
|
5929
|
+
// fine and the suite merely slow (or starved by a concurrent flight). Report
|
|
5930
|
+
// it as a distinct timed-out state so operators do not read slow as broken.
|
|
5931
|
+
const timedOut = result.error ? result.error.code === 'ETIMEDOUT' : false;
|
|
5867
5932
|
return {
|
|
5868
5933
|
command,
|
|
5869
5934
|
resolved_command: resolvedCommand === command ? null : resolvedCommand,
|
|
5870
5935
|
status: result.status,
|
|
5871
5936
|
signal: result.signal || null,
|
|
5872
5937
|
passed: result.status === 0,
|
|
5938
|
+
timed_out: timedOut,
|
|
5939
|
+
elapsed_ms: elapsedMs,
|
|
5940
|
+
timeout_ms: timeoutMs,
|
|
5873
5941
|
stdout: String(result.stdout || '').slice(-4000),
|
|
5874
5942
|
stderr: String(result.stderr || '').slice(-4000),
|
|
5875
5943
|
};
|
|
@@ -5952,8 +6020,14 @@ function gitWorktreeSnapshot(root = process.cwd()) {
|
|
|
5952
6020
|
}
|
|
5953
6021
|
const entries = String(status.stdout || '').split(/\r?\n/).filter(Boolean).sort();
|
|
5954
6022
|
const digest = crypto.createHash('sha1').update(entries.join('\n')).digest('hex');
|
|
6023
|
+
const head = spawnSync('git', ['rev-parse', '--verify', 'HEAD'], {
|
|
6024
|
+
cwd: root,
|
|
6025
|
+
encoding: 'utf8',
|
|
6026
|
+
timeout: 5000,
|
|
6027
|
+
});
|
|
5955
6028
|
return {
|
|
5956
6029
|
available: true,
|
|
6030
|
+
head: head.status === 0 ? String(head.stdout || '').trim() : null,
|
|
5957
6031
|
dirty_count: entries.length,
|
|
5958
6032
|
dirty_hash: digest,
|
|
5959
6033
|
dirty_sample: entries.slice(0, 25),
|
|
@@ -5961,6 +6035,49 @@ function gitWorktreeSnapshot(root = process.cwd()) {
|
|
|
5961
6035
|
};
|
|
5962
6036
|
}
|
|
5963
6037
|
|
|
6038
|
+
function missionProtectedTags(mission, root = process.cwd()) {
|
|
6039
|
+
const tags = [
|
|
6040
|
+
mission?.tag,
|
|
6041
|
+
mission?.lane,
|
|
6042
|
+
...(Array.isArray(mission?.tags) ? mission.tags : []),
|
|
6043
|
+
].filter(Boolean);
|
|
6044
|
+
const taskRefs = new Set([
|
|
6045
|
+
mission?.task_id,
|
|
6046
|
+
mission?.task_ref,
|
|
6047
|
+
...(Array.isArray(mission?.task_ids) ? mission.task_ids : []),
|
|
6048
|
+
].filter(Boolean).map(String));
|
|
6049
|
+
if (!taskRefs.size) return tags;
|
|
6050
|
+
try {
|
|
6051
|
+
const projection = JSON.parse(fs.readFileSync(path.join(root, '.atris', 'state', 'tasks.projection.json'), 'utf8'));
|
|
6052
|
+
for (const task of Array.isArray(projection?.tasks) ? projection.tasks : []) {
|
|
6053
|
+
if (!task || ![task.id, task.display_id, task.legacy_ref].some((ref) => taskRefs.has(String(ref || '')))) continue;
|
|
6054
|
+
if (task.tag) tags.push(task.tag);
|
|
6055
|
+
if (Array.isArray(task.tags)) tags.push(...task.tags);
|
|
6056
|
+
}
|
|
6057
|
+
} catch {}
|
|
6058
|
+
return tags;
|
|
6059
|
+
}
|
|
6060
|
+
|
|
6061
|
+
function inspectMissionTickProtectedDiff(mission, before, root = process.cwd()) {
|
|
6062
|
+
const after = gitWorktreeSnapshot(root);
|
|
6063
|
+
if (before?.reason === 'not-git-worktree' && after?.reason === 'not-git-worktree') {
|
|
6064
|
+
return {
|
|
6065
|
+
ok: true,
|
|
6066
|
+
allowed: true,
|
|
6067
|
+
status: 'clear',
|
|
6068
|
+
reason: 'no git worktree exists, so this tick cannot commit or land',
|
|
6069
|
+
unreadable: false,
|
|
6070
|
+
surfaces: [],
|
|
6071
|
+
matches: [],
|
|
6072
|
+
};
|
|
6073
|
+
}
|
|
6074
|
+
return inspectMissionProtectedDiff({
|
|
6075
|
+
root,
|
|
6076
|
+
baseRef: before?.head || '',
|
|
6077
|
+
tags: missionProtectedTags(mission, root),
|
|
6078
|
+
});
|
|
6079
|
+
}
|
|
6080
|
+
|
|
5964
6081
|
// Porcelain v1 entries are "XY path" or "XY old -> new"; baselines compare by
|
|
5965
6082
|
// post-rename path so a status-letter change alone never counts as new dirt.
|
|
5966
6083
|
function porcelainEntryPath(line) {
|
|
@@ -6264,7 +6381,14 @@ function missionSortTime(mission) {
|
|
|
6264
6381
|
}
|
|
6265
6382
|
|
|
6266
6383
|
function selectDueMission(root = process.cwd(), now = new Date(), options = {}) {
|
|
6267
|
-
const
|
|
6384
|
+
const missions = listMissions(root);
|
|
6385
|
+
for (const mission of missions) {
|
|
6386
|
+
if (!missionIsRunnable(mission) || !missionObjectiveIsJunk(mission.objective)) continue;
|
|
6387
|
+
stopMissionRecord(mission, 'junk_objective_gate', root, { writeStopReceipt: false });
|
|
6388
|
+
console.error(`auto-stopped junk mission: "${String(mission.objective || '')}"`);
|
|
6389
|
+
}
|
|
6390
|
+
const candidates = missions
|
|
6391
|
+
.filter((mission) => !missionObjectiveIsJunk(mission.objective))
|
|
6268
6392
|
.filter((mission) => missionSelectableForLoop(mission, now))
|
|
6269
6393
|
.filter((mission) => !options.headlessOnly || !runnerUsesCallerSession(mission.runner))
|
|
6270
6394
|
.filter((mission) => !missionTaskHumanAcceptWaiting(mission))
|
|
@@ -7876,6 +8000,14 @@ function spawnMissionDriver(mission, root = process.cwd()) {
|
|
|
7876
8000
|
log_path: logPath,
|
|
7877
8001
|
}, null, 2) + '\n', 'utf8');
|
|
7878
8002
|
child.unref();
|
|
8003
|
+
// A returned pid is not proof the engine is running: spawn can hand back a
|
|
8004
|
+
// pid for a child that never execs or exits immediately. Verify the process
|
|
8005
|
+
// is actually alive before claiming a live worker, and drop the misleading
|
|
8006
|
+
// driver-state file when it is not.
|
|
8007
|
+
if (!child.pid || !missionLockOwnerIsAlive(child.pid)) {
|
|
8008
|
+
try { fs.rmSync(paths.stateFile, { force: true }); } catch {}
|
|
8009
|
+
return { ok: false, error: 'mission driver exited before it could start', pid: child.pid || null };
|
|
8010
|
+
}
|
|
7879
8011
|
return {
|
|
7880
8012
|
ok: true,
|
|
7881
8013
|
pid: child.pid,
|
|
@@ -7949,7 +8081,13 @@ function detachMissionRun(args, mission, root = process.cwd(), asJson = false) {
|
|
|
7949
8081
|
|
|
7950
8082
|
function probeClaudeBinary() {
|
|
7951
8083
|
const runnerBin = resolveClaudeRunnerBin();
|
|
7952
|
-
|
|
8084
|
+
const commandTemplate = resolveClaudeRunnerCommandTemplate();
|
|
8085
|
+
if (commandTemplate) {
|
|
8086
|
+
// A command template fully replaces the claude invocation (GLM/OpenAI/
|
|
8087
|
+
// local runners). The runner bin only matters when the template actually
|
|
8088
|
+
// references {bin}; requiring `claude` on PATH otherwise defeats the
|
|
8089
|
+
// template's purpose and breaks generic runners on claude-less machines.
|
|
8090
|
+
if (!commandTemplate.includes('{bin}')) return { ok: true };
|
|
7953
8091
|
const probe = spawnSync('sh', ['-c', `command -v ${shellQuote(runnerBin)}`], { encoding: 'utf8', timeout: 8000 });
|
|
7954
8092
|
if (probe.status !== 0) return { ok: false, error: `${runnerBin} CLI not found` };
|
|
7955
8093
|
return { ok: true };
|
|
@@ -8072,6 +8210,9 @@ function detectUnavailableModel(text) {
|
|
|
8072
8210
|
// need a resume; a model-unavailable pause is a config error a bare resume won't fix,
|
|
8073
8211
|
// so name the dead id and the two knobs that change it.
|
|
8074
8212
|
function missionPauseNextAction(pauseReason, missionId, deadModel = null, lastErrorReason = null) {
|
|
8213
|
+
if (pauseReason === 'protected-lane-review') {
|
|
8214
|
+
return `review the protected diff and its receipt before resuming: atris mission run ${missionId}`;
|
|
8215
|
+
}
|
|
8075
8216
|
if (pauseReason === 'too-deep') {
|
|
8076
8217
|
return 'stopped: helpers were spawning helpers too many levels deep';
|
|
8077
8218
|
}
|
|
@@ -8260,13 +8401,31 @@ function spawnGenericRunnerTick(mission, opts) {
|
|
|
8260
8401
|
resolve({ ok: false, error: e.message, sessionIds: [], aborted: false, timedOut: false, authExpired: false, brief_id: briefId });
|
|
8261
8402
|
return;
|
|
8262
8403
|
}
|
|
8404
|
+
let gitGuard;
|
|
8405
|
+
try {
|
|
8406
|
+
gitGuard = prepareMissionGitGuard({ root: cwd, tags: missionProtectedTags(mission, cwd) });
|
|
8407
|
+
} catch (error) {
|
|
8408
|
+
resolve({
|
|
8409
|
+
ok: false,
|
|
8410
|
+
reason: 'protected-diff-unreadable',
|
|
8411
|
+
error: error.message || String(error),
|
|
8412
|
+
protected_lane_guard: unreadableMissionGuard(error),
|
|
8413
|
+
sessionIds: [],
|
|
8414
|
+
aborted: false,
|
|
8415
|
+
timedOut: false,
|
|
8416
|
+
authExpired: false,
|
|
8417
|
+
brief_id: briefId,
|
|
8418
|
+
});
|
|
8419
|
+
return;
|
|
8420
|
+
}
|
|
8263
8421
|
|
|
8264
8422
|
const startedAt = Date.now();
|
|
8265
|
-
const
|
|
8423
|
+
const guardedCommand = `PATH=${shellQuote(gitGuard.pathPrefix)}:$PATH; export PATH; ${cmd}`;
|
|
8424
|
+
const proc = spawn('sh', ['-lc', guardedCommand], {
|
|
8266
8425
|
cwd,
|
|
8267
8426
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
8268
8427
|
detached: true,
|
|
8269
|
-
env: { ...
|
|
8428
|
+
env: { ...gitGuard.env, ATRIS_DISPATCH_DEPTH: String(dispatchDepth.depth + 1) },
|
|
8270
8429
|
});
|
|
8271
8430
|
updateMissionLockOwner(missionLock, proc.pid, { phase: 'tick' });
|
|
8272
8431
|
let stdout = '';
|
|
@@ -8297,6 +8456,7 @@ function spawnGenericRunnerTick(mission, opts) {
|
|
|
8297
8456
|
proc.on('close', (code) => {
|
|
8298
8457
|
clearTimeout(timer);
|
|
8299
8458
|
cleanupPrompt();
|
|
8459
|
+
gitGuard.cleanup();
|
|
8300
8460
|
if (signal) signal.removeEventListener?.('abort', onAbort);
|
|
8301
8461
|
updateMissionLockOwner(missionLock, missionLock?.driverPid);
|
|
8302
8462
|
const finalText = String(stdout || '').trim();
|
|
@@ -8325,6 +8485,7 @@ function spawnGenericRunnerTick(mission, opts) {
|
|
|
8325
8485
|
proc.on('error', (e) => {
|
|
8326
8486
|
clearTimeout(timer);
|
|
8327
8487
|
cleanupPrompt();
|
|
8488
|
+
gitGuard.cleanup();
|
|
8328
8489
|
if (signal) signal.removeEventListener?.('abort', onAbort);
|
|
8329
8490
|
updateMissionLockOwner(missionLock, missionLock?.driverPid);
|
|
8330
8491
|
resolve({ ok: false, error: e.message, sessionIds: [], aborted, timedOut, authExpired: false, brief_id: briefId });
|
|
@@ -8373,6 +8534,22 @@ function spawnClaudeTick(mission, opts) {
|
|
|
8373
8534
|
const dispatchDepth = missionDispatchDepth();
|
|
8374
8535
|
if (dispatchDepth.tooDeep) return Promise.resolve(tooDeepRunnerResult(dispatchDepth));
|
|
8375
8536
|
return new Promise((resolve) => {
|
|
8537
|
+
let gitGuard;
|
|
8538
|
+
try {
|
|
8539
|
+
gitGuard = prepareMissionGitGuard({ root: cwd, tags: missionProtectedTags(mission, cwd) });
|
|
8540
|
+
} catch (error) {
|
|
8541
|
+
resolve({
|
|
8542
|
+
ok: false,
|
|
8543
|
+
reason: 'protected-diff-unreadable',
|
|
8544
|
+
error: error.message || String(error),
|
|
8545
|
+
protected_lane_guard: unreadableMissionGuard(error),
|
|
8546
|
+
sessionIds: [],
|
|
8547
|
+
aborted: false,
|
|
8548
|
+
timedOut: false,
|
|
8549
|
+
authExpired: false,
|
|
8550
|
+
});
|
|
8551
|
+
return;
|
|
8552
|
+
}
|
|
8376
8553
|
const args = [
|
|
8377
8554
|
'-p', prompt,
|
|
8378
8555
|
'--output-format', 'stream-json',
|
|
@@ -8392,7 +8569,7 @@ function spawnClaudeTick(mission, opts) {
|
|
|
8392
8569
|
cwd,
|
|
8393
8570
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
8394
8571
|
detached: true,
|
|
8395
|
-
env: { ...
|
|
8572
|
+
env: { ...gitGuard.env, ATRIS_DISPATCH_DEPTH: String(dispatchDepth.depth + 1) },
|
|
8396
8573
|
});
|
|
8397
8574
|
updateMissionLockOwner(missionLock, proc.pid, { phase: 'tick' });
|
|
8398
8575
|
|
|
@@ -8455,6 +8632,7 @@ function spawnClaudeTick(mission, opts) {
|
|
|
8455
8632
|
|
|
8456
8633
|
proc.on('close', (code) => {
|
|
8457
8634
|
clearTimeout(timer);
|
|
8635
|
+
gitGuard.cleanup();
|
|
8458
8636
|
if (signal) signal.removeEventListener?.('abort', onAbort);
|
|
8459
8637
|
updateMissionLockOwner(missionLock, missionLock?.driverPid);
|
|
8460
8638
|
const ok = code === 0 && !isError && !timedOut && !aborted;
|
|
@@ -8484,6 +8662,7 @@ function spawnClaudeTick(mission, opts) {
|
|
|
8484
8662
|
|
|
8485
8663
|
proc.on('error', (e) => {
|
|
8486
8664
|
clearTimeout(timer);
|
|
8665
|
+
gitGuard.cleanup();
|
|
8487
8666
|
updateMissionLockOwner(missionLock, missionLock?.driverPid);
|
|
8488
8667
|
resolve({ ok: false, error: e.message, sessionIds: [], aborted, timedOut, authExpired: false });
|
|
8489
8668
|
});
|
|
@@ -8826,6 +9005,10 @@ async function runMission(args) {
|
|
|
8826
9005
|
const startedAt = Date.now();
|
|
8827
9006
|
let backoffAttempt = 0;
|
|
8828
9007
|
let lastRateLimit = null;
|
|
9008
|
+
// A usage-wall cooldown belongs to the engine that hit it, not the whole run.
|
|
9009
|
+
// Tracking which engine set it lets an auto-runner swap to another ready engine
|
|
9010
|
+
// and keep working instead of waiting the wall out (or pausing for a human).
|
|
9011
|
+
let rateLimitedEngineId = null;
|
|
8829
9012
|
let continuationGoal = null;
|
|
8830
9013
|
|
|
8831
9014
|
const sessionLabel = skipWorker
|
|
@@ -8885,6 +9068,18 @@ async function runMission(args) {
|
|
|
8885
9068
|
result.engine_fallback_reason = tickSelection.engine_fallback_reason;
|
|
8886
9069
|
}
|
|
8887
9070
|
|
|
9071
|
+
// Usage-wall engine swap: when a prior tick's engine hit a rate-limit wall and
|
|
9072
|
+
// the auto runner has already resolved this tick to a *different* ready engine
|
|
9073
|
+
// (the walled one is now credit_out and excluded from resolution), the run-wide
|
|
9074
|
+
// cooldown no longer applies. Clear it so the fresh engine runs now instead of
|
|
9075
|
+
// the loop sleeping through — or pausing on `rate-limit-exceeded-wall` for — a
|
|
9076
|
+
// wall this engine never hit.
|
|
9077
|
+
if (lastRateLimit && rateLimitedEngineId && tickEngineId && tickEngineId !== rateLimitedEngineId) {
|
|
9078
|
+
result.engine_swapped_from = rateLimitedEngineId;
|
|
9079
|
+
lastRateLimit = null;
|
|
9080
|
+
rateLimitedEngineId = null;
|
|
9081
|
+
}
|
|
9082
|
+
|
|
8888
9083
|
// Active-hours gate
|
|
8889
9084
|
if (engineBackedTick && !tickEngineId) {
|
|
8890
9085
|
result = { ...result, status: 'errored', reason: 'no-ready-engine' };
|
|
@@ -8916,55 +9111,76 @@ async function runMission(args) {
|
|
|
8916
9111
|
drill: drillResult,
|
|
8917
9112
|
};
|
|
8918
9113
|
} 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;
|
|
9114
|
+
let atris2GitGuard = null;
|
|
8934
9115
|
try {
|
|
8935
|
-
|
|
8936
|
-
|
|
8937
|
-
|
|
8938
|
-
|
|
8939
|
-
|
|
8940
|
-
|
|
8941
|
-
|
|
8942
|
-
|
|
8943
|
-
|
|
8944
|
-
|
|
8945
|
-
|
|
8946
|
-
|
|
8947
|
-
|
|
9116
|
+
atris2GitGuard = prepareMissionGitGuard({ root: cwd, tags: missionProtectedTags(mission, cwd) });
|
|
9117
|
+
} catch (error) {
|
|
9118
|
+
result = {
|
|
9119
|
+
...result,
|
|
9120
|
+
status: 'ran',
|
|
9121
|
+
reason: 'protected-diff-unreadable',
|
|
9122
|
+
ran: true,
|
|
9123
|
+
atris2: {
|
|
9124
|
+
ok: false,
|
|
9125
|
+
error: error.message || String(error),
|
|
9126
|
+
protected_lane_guard: unreadableMissionGuard(error),
|
|
9127
|
+
},
|
|
9128
|
+
};
|
|
8948
9129
|
}
|
|
8949
|
-
|
|
8950
|
-
|
|
8951
|
-
|
|
8952
|
-
|
|
8953
|
-
|
|
8954
|
-
|
|
8955
|
-
|
|
8956
|
-
|
|
8957
|
-
|
|
8958
|
-
|
|
8959
|
-
|
|
8960
|
-
|
|
8961
|
-
|
|
8962
|
-
|
|
8963
|
-
|
|
8964
|
-
|
|
8965
|
-
|
|
8966
|
-
|
|
8967
|
-
|
|
9130
|
+
if (atris2GitGuard) {
|
|
9131
|
+
const pingDrain = consumeMissionPings(mission, cwd);
|
|
9132
|
+
mission = pingDrain.mission;
|
|
9133
|
+
runtimeMission = runtimeView(mission);
|
|
9134
|
+
const prompt = buildTickPrompt(tickRuntimeMission, tickIdx, effectiveMaxTicks, frozen, pingDrain.pings);
|
|
9135
|
+
const { runAtris2Turn } = require('./probe');
|
|
9136
|
+
const businessId = businessIdForAtris2Mission(tickRuntimeMission, cwd);
|
|
9137
|
+
const tickController = new AbortController();
|
|
9138
|
+
let wallExpired = false;
|
|
9139
|
+
const abortTick = () => tickController.abort();
|
|
9140
|
+
controller.signal.addEventListener('abort', abortTick, { once: true });
|
|
9141
|
+
const tickTimer = setTimeout(() => {
|
|
9142
|
+
wallExpired = true;
|
|
9143
|
+
tickController.abort();
|
|
9144
|
+
}, Math.max(1, Math.floor(remainingWall * 1000)));
|
|
9145
|
+
let turn;
|
|
9146
|
+
try {
|
|
9147
|
+
turn = await runAtris2Turn({
|
|
9148
|
+
prompt,
|
|
9149
|
+
model: tickRuntimeMission.model || 'atris:fast',
|
|
9150
|
+
business: businessId,
|
|
9151
|
+
maxTurns: 16,
|
|
9152
|
+
signal: tickController.signal,
|
|
9153
|
+
// CLI-231: mission ticks execute relayed ops in the mission's own
|
|
9154
|
+
// workspace, not the hosted ai-computer filesystem.
|
|
9155
|
+
localCwd: cwd,
|
|
9156
|
+
localEnv: atris2GitGuard.env,
|
|
9157
|
+
localPathPrefix: atris2GitGuard.pathPrefix,
|
|
9158
|
+
});
|
|
9159
|
+
} finally {
|
|
9160
|
+
atris2GitGuard.cleanup();
|
|
9161
|
+
clearTimeout(tickTimer);
|
|
9162
|
+
controller.signal.removeEventListener('abort', abortTick);
|
|
9163
|
+
}
|
|
9164
|
+
result.atris2 = {
|
|
9165
|
+
ok: turn.ok,
|
|
9166
|
+
engine: turn.engine,
|
|
9167
|
+
model: tickRuntimeMission.model || 'atris:fast',
|
|
9168
|
+
tools_run: turn.tools_run,
|
|
9169
|
+
unsupported: turn.unsupported,
|
|
9170
|
+
duration_ms: turn.duration_ms,
|
|
9171
|
+
error: turn.error,
|
|
9172
|
+
backend_unavailable: isTransientAtris2BackendError(turn.error) || undefined,
|
|
9173
|
+
receipt_text: String(turn.text || '').slice(0, 4000),
|
|
9174
|
+
};
|
|
9175
|
+
if (controller.signal.aborted) { pauseReason = 'aborted-during-atris2'; break; }
|
|
9176
|
+
if (turn.error === 'not-logged-in') { pauseReason = 'auth-required'; break; }
|
|
9177
|
+
if (wallExpired) {
|
|
9178
|
+
result = { ...result, status: 'errored', reason: 'wall-exceeded-during-tick' };
|
|
9179
|
+
} else if (!turn.ok || !String(turn.text || '').trim()) {
|
|
9180
|
+
result = { ...result, status: 'errored', reason: atris2TurnErrorReason(turn.error) };
|
|
9181
|
+
} else {
|
|
9182
|
+
result = { ...result, status: 'ran', reason: 'tick-ok', ran: true };
|
|
9183
|
+
}
|
|
8968
9184
|
}
|
|
8969
9185
|
} else {
|
|
8970
9186
|
let sessionMode = sessionId ? 'resume' : 'set';
|
|
@@ -9006,6 +9222,7 @@ async function runMission(args) {
|
|
|
9006
9222
|
result.claude = {
|
|
9007
9223
|
ok: claudeResult.ok,
|
|
9008
9224
|
reason: claudeResult.reason,
|
|
9225
|
+
protected_lane_guard: claudeResult.protected_lane_guard || null,
|
|
9009
9226
|
brief_id: claudeResult.brief_id || null,
|
|
9010
9227
|
summary: claudeResult.summary,
|
|
9011
9228
|
receipt_text: claudeResult.receipt_text,
|
|
@@ -9026,6 +9243,7 @@ async function runMission(args) {
|
|
|
9026
9243
|
// treating an allowed resetsAt as one pauses every timed run after
|
|
9027
9244
|
// tick 1 with rate-limit-exceeded-wall.
|
|
9028
9245
|
lastRateLimit = claudeResult.rate_limit_info.status === 'allowed' ? null : claudeResult.rate_limit_info;
|
|
9246
|
+
rateLimitedEngineId = lastRateLimit ? tickEngineId : null;
|
|
9029
9247
|
}
|
|
9030
9248
|
if (claudeResult.aborted) { pauseReason = 'aborted-during-claude'; break; }
|
|
9031
9249
|
if (claudeResult.authExpired) { pauseReason = 'auth-required'; break; }
|
|
@@ -9091,6 +9309,19 @@ async function runMission(args) {
|
|
|
9091
9309
|
// and caller-session ticks must not launch it behind the operator's
|
|
9092
9310
|
// back. Leaving verifier_passed unset also lets the idle-stop breaker
|
|
9093
9311
|
// judge these ticks from the worktree signal instead of hanging here.
|
|
9312
|
+
const runnerGuard = result.claude?.protected_lane_guard || result.atris2?.protected_lane_guard || null;
|
|
9313
|
+
if (result.status === 'ran' || runnerGuard) {
|
|
9314
|
+
const protectedLaneGuard = runnerGuard?.allowed === false
|
|
9315
|
+
? runnerGuard
|
|
9316
|
+
: inspectMissionTickProtectedDiff(mission, tickWorktreeBefore, cwd);
|
|
9317
|
+
result.protected_lane_guard = protectedLaneGuard;
|
|
9318
|
+
if (!protectedLaneGuard.allowed) {
|
|
9319
|
+
result.status = 'paused-for-review';
|
|
9320
|
+
result.reason = 'protected-lane-review';
|
|
9321
|
+
result.ran = false;
|
|
9322
|
+
pauseReason = 'protected-lane-review';
|
|
9323
|
+
}
|
|
9324
|
+
}
|
|
9094
9325
|
let verifierResult = null;
|
|
9095
9326
|
let receiptPath = null;
|
|
9096
9327
|
if (result.status === 'ran' && verifyEach) {
|
|
@@ -9246,6 +9477,7 @@ async function runMission(args) {
|
|
|
9246
9477
|
break;
|
|
9247
9478
|
}
|
|
9248
9479
|
|
|
9480
|
+
if (pauseReason === 'protected-lane-review') break;
|
|
9249
9481
|
if (callerSessionRunner && result.status === 'ran') break;
|
|
9250
9482
|
if (newStatus === 'complete' || (newStatus === 'ready' && !mission.always_on && !fullBudgetMode)) break;
|
|
9251
9483
|
// BCK-1324: two consecutive ticks that each self-report "ran" but leave no
|
|
@@ -9516,6 +9748,7 @@ function tickMission(args) {
|
|
|
9516
9748
|
const tickIdx = lastTickIndex + 1;
|
|
9517
9749
|
const tickWorktreeBefore = gitWorktreeSnapshot(cwd);
|
|
9518
9750
|
const worktreeBaseline = loadMissionWorktreeBaseline(mission.id, cwd);
|
|
9751
|
+
const protectedLaneGuard = inspectMissionTickProtectedDiff(mission, tickWorktreeBefore, cwd);
|
|
9519
9752
|
|
|
9520
9753
|
const effectiveVerifier = effectiveMissionVerifier(mission);
|
|
9521
9754
|
const verifierCommand = verify
|
|
@@ -9524,7 +9757,7 @@ function tickMission(args) {
|
|
|
9524
9757
|
if (verifierCommand) assertMissionVerifier(verifierCommand, asJson);
|
|
9525
9758
|
|
|
9526
9759
|
let verifierResult = null;
|
|
9527
|
-
if (verify && verifierCommand) {
|
|
9760
|
+
if (protectedLaneGuard.allowed && verify && verifierCommand) {
|
|
9528
9761
|
verifierResult = runVerifier(verifierCommand);
|
|
9529
9762
|
}
|
|
9530
9763
|
const tickWorktree = worktreeReceipt(tickWorktreeBefore, gitWorktreeSnapshot(cwd), { verifier: verifierCommand || effectiveVerifier, baseline: worktreeBaseline });
|
|
@@ -9533,16 +9766,17 @@ function tickMission(args) {
|
|
|
9533
9766
|
// receipt text in --summary.
|
|
9534
9767
|
const layerInfo = extractLayerFromReceiptText(summary || '', tickWorktree?.new_since_baseline_sample);
|
|
9535
9768
|
const tickRecord = {
|
|
9536
|
-
status: 'ran',
|
|
9537
|
-
reason: 'tick-recorded',
|
|
9769
|
+
status: protectedLaneGuard.allowed ? 'ran' : 'paused-for-review',
|
|
9770
|
+
reason: protectedLaneGuard.allowed ? 'tick-recorded' : 'protected-lane-review',
|
|
9538
9771
|
tick_index: tickIdx,
|
|
9539
|
-
ran:
|
|
9772
|
+
ran: protectedLaneGuard.allowed,
|
|
9540
9773
|
started_at: tickStart,
|
|
9541
9774
|
claude: { skipped: true, reason: 'orchestrator-is-caller-session' },
|
|
9542
9775
|
summary: summary || null,
|
|
9543
9776
|
layer: layerInfo.layer,
|
|
9544
9777
|
layer_source: layerInfo.source,
|
|
9545
9778
|
verifier_passed: verifierResult ? !!verifierResult.passed : null,
|
|
9779
|
+
protected_lane_guard: protectedLaneGuard,
|
|
9546
9780
|
finished_at: stampIso(),
|
|
9547
9781
|
worktree: tickWorktree,
|
|
9548
9782
|
};
|
|
@@ -9555,6 +9789,7 @@ function tickMission(args) {
|
|
|
9555
9789
|
started_at: tickStart,
|
|
9556
9790
|
},
|
|
9557
9791
|
verifier_result: verifierResult,
|
|
9792
|
+
protected_lane_guard: protectedLaneGuard,
|
|
9558
9793
|
rate_limit_info: null,
|
|
9559
9794
|
worktree: tickWorktree,
|
|
9560
9795
|
});
|
|
@@ -9566,7 +9801,10 @@ function tickMission(args) {
|
|
|
9566
9801
|
? nextCandidateTickAction(mission)
|
|
9567
9802
|
: 'attach task, verifier, or proof');
|
|
9568
9803
|
const nextGoalChain = advanceMissionGoalChain(mission.goal_chain, summary, verifierResult);
|
|
9569
|
-
if (
|
|
9804
|
+
if (!protectedLaneGuard.allowed) {
|
|
9805
|
+
status = 'paused';
|
|
9806
|
+
nextAction = `review the protected diff and receipt before resuming: atris mission run ${mission.id}`;
|
|
9807
|
+
} else if (verifierResult?.passed && nextGoalChain && !nextGoalChain.pause_ready) {
|
|
9570
9808
|
status = 'running';
|
|
9571
9809
|
nextAction = missionGoalChainNextAction(nextGoalChain);
|
|
9572
9810
|
} else if (verifierResult?.passed) {
|
|
@@ -9591,10 +9829,10 @@ function tickMission(args) {
|
|
|
9591
9829
|
}
|
|
9592
9830
|
const clearsPauseState = !['paused', 'stopped'].includes(status);
|
|
9593
9831
|
const nextMission = {
|
|
9594
|
-
|
|
9595
|
-
|
|
9596
|
-
paused_at: clearsPauseState ? null :
|
|
9597
|
-
stop_reason: clearsPauseState ? null :
|
|
9832
|
+
...mission,
|
|
9833
|
+
status,
|
|
9834
|
+
paused_at: clearsPauseState ? null : tickRecord.finished_at,
|
|
9835
|
+
stop_reason: clearsPauseState ? null : tickRecord.reason,
|
|
9598
9836
|
resumed_at: clearsPauseState && mission.status === 'paused' ? tickRecord.finished_at : mission.resumed_at || null,
|
|
9599
9837
|
receipt_path: receiptPath,
|
|
9600
9838
|
last_tick_at: tickRecord.finished_at,
|
|
@@ -9698,6 +9936,11 @@ function missionCompletionGate(mission, proof, root = process.cwd()) {
|
|
|
9698
9936
|
|
|
9699
9937
|
function completeMission(args) {
|
|
9700
9938
|
const asJson = wantsJson(args);
|
|
9939
|
+
if (hasFlag(args, '--help') || hasFlag(args, '-h') || String(args[0] || '').trim() === 'help') {
|
|
9940
|
+
console.log('Usage: atris mission complete <id> --proof "..."');
|
|
9941
|
+
console.log('Run `atris mission --help` for the full option list.');
|
|
9942
|
+
process.exit(0);
|
|
9943
|
+
}
|
|
9701
9944
|
const force = hasFlag(args, '--force');
|
|
9702
9945
|
const proof = readFlag(args, '--proof', '');
|
|
9703
9946
|
const ref = stripKnownFlags(args, ['--proof'], ['--json', '--force'])[0] || '';
|
|
@@ -9773,31 +10016,21 @@ function completeMission(args) {
|
|
|
9773
10016
|
);
|
|
9774
10017
|
}
|
|
9775
10018
|
|
|
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
|
-
}
|
|
10019
|
+
function stopMissionRecord(mission, reason, root = process.cwd(), options = {}) {
|
|
10020
|
+
const pause = options.pause === true;
|
|
9788
10021
|
const status = pause ? 'paused' : 'stopped';
|
|
9789
10022
|
// Full stops abandon work, so leave evidence: snapshot the worktree against
|
|
9790
10023
|
// the mission baseline (what did this mission leave dirty?) before pruning it.
|
|
9791
10024
|
let receiptPath = null;
|
|
9792
|
-
if (!pause) {
|
|
9793
|
-
const snapshot = gitWorktreeSnapshot(
|
|
10025
|
+
if (!pause && options.writeStopReceipt !== false) {
|
|
10026
|
+
const snapshot = gitWorktreeSnapshot(root);
|
|
9794
10027
|
const worktree = worktreeReceipt(snapshot, snapshot, {
|
|
9795
10028
|
verifier: mission.verifier,
|
|
9796
|
-
baseline: loadMissionWorktreeBaseline(mission.id,
|
|
10029
|
+
baseline: loadMissionWorktreeBaseline(mission.id, root),
|
|
9797
10030
|
});
|
|
9798
|
-
receiptPath = writeReceipt(mission, { kind: 'mission_stop', reason, worktree });
|
|
10031
|
+
receiptPath = writeReceipt(mission, { kind: 'mission_stop', reason, worktree }, root);
|
|
9799
10032
|
}
|
|
9800
|
-
const baselineSummary = pause ? null : pruneMissionWorktreeBaseline(mission,
|
|
10033
|
+
const baselineSummary = pause ? null : pruneMissionWorktreeBaseline(mission, root);
|
|
9801
10034
|
const next = {
|
|
9802
10035
|
...mission,
|
|
9803
10036
|
status,
|
|
@@ -9808,9 +10041,32 @@ function stopMission(args) {
|
|
|
9808
10041
|
worktree_baseline: baselineSummary || mission.worktree_baseline || null,
|
|
9809
10042
|
next_action: status === 'paused' ? `resume with: atris mission tick ${mission.id}` : 'mission stopped',
|
|
9810
10043
|
};
|
|
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 });
|
|
10044
|
+
const { mission: saved } = saveMission(next, root, pause ? 'mission_paused' : 'mission_stopped', { reason, receipt_path: receiptPath });
|
|
10045
|
+
const directGoalRequestCleared = pause ? false : clearDirectRunCodexGoalRequestForMission(saved.id, root);
|
|
10046
|
+
const logPath = appendMemberLog(saved.owner, pause ? 'Mission paused' : 'Mission stopped', { mission: saved.objective, reason }, root);
|
|
10047
|
+
return { saved, receiptPath, directGoalRequestCleared, logPath };
|
|
10048
|
+
}
|
|
10049
|
+
|
|
10050
|
+
function stopMission(args) {
|
|
10051
|
+
const asJson = wantsJson(args);
|
|
10052
|
+
// Help is read-only: `mission stop <id> --help` must never resolve the id and
|
|
10053
|
+
// stop the mission just to be asked for usage.
|
|
10054
|
+
if (hasFlag(args, '--help') || hasFlag(args, '-h') || String(args[0] || '').trim() === 'help') {
|
|
10055
|
+
console.log('Usage: atris mission stop <id> [--pause] [--reason "..."]');
|
|
10056
|
+
console.log('Run `atris mission --help` for the full option list.');
|
|
10057
|
+
process.exit(0);
|
|
10058
|
+
}
|
|
10059
|
+
const reason = readFlag(args, '--reason', 'stopped by operator');
|
|
10060
|
+
const pause = hasFlag(args, '--pause');
|
|
10061
|
+
const ref = stripKnownFlags(args, ['--reason'], ['--json', '--pause'])[0] || '';
|
|
10062
|
+
if (!ref) {
|
|
10063
|
+
exitMissionError('Usage: atris mission stop <id> [--pause] [--reason "..."]', 1, asJson);
|
|
10064
|
+
}
|
|
10065
|
+
const mission = resolveMission(ref);
|
|
10066
|
+
if (!mission) {
|
|
10067
|
+
exitMissingMission(ref, 1, asJson);
|
|
10068
|
+
}
|
|
10069
|
+
const { saved, receiptPath, directGoalRequestCleared, logPath } = stopMissionRecord(mission, reason, process.cwd(), { pause });
|
|
9814
10070
|
printJsonOrText(
|
|
9815
10071
|
{ ok: true, action: pause ? 'mission_paused' : 'mission_stopped', mission: saved, receipt_path: receiptPath, log_path: logPath, direct_goal_request_cleared: directGoalRequestCleared },
|
|
9816
10072
|
[
|
|
@@ -10506,6 +10762,11 @@ function inspectMission(args) {
|
|
|
10506
10762
|
// output so callers (member ping) can compose their own multi-lane report.
|
|
10507
10763
|
function pingMission(args, opts = {}) {
|
|
10508
10764
|
const asJson = args.includes('--json');
|
|
10765
|
+
if (!opts.silent && (hasFlag(args, '--help') || hasFlag(args, '-h') || String(args[0] || '').trim() === 'help')) {
|
|
10766
|
+
console.log('Usage: atris mission ping <id> "<note>" [--from <who>] [--json]');
|
|
10767
|
+
console.log('Run `atris mission --help` for the full option list.');
|
|
10768
|
+
process.exit(0);
|
|
10769
|
+
}
|
|
10509
10770
|
const rest = args.filter((a) => a !== '--json');
|
|
10510
10771
|
let from = process.env.USER || 'operator';
|
|
10511
10772
|
const fromIdx = rest.indexOf('--from');
|
|
@@ -10674,6 +10935,7 @@ module.exports = {
|
|
|
10674
10935
|
extractCheckFeedback,
|
|
10675
10936
|
loadMissionMap,
|
|
10676
10937
|
renderMissionStatus,
|
|
10938
|
+
missionObjectiveIsJunk,
|
|
10677
10939
|
selectDueMission,
|
|
10678
10940
|
selectAtrisGoalMission,
|
|
10679
10941
|
selectCodexGoalMission,
|
|
@@ -10682,29 +10944,22 @@ module.exports = {
|
|
|
10682
10944
|
cappedClaudeReceiptText,
|
|
10683
10945
|
extractLayerFromReceiptText,
|
|
10684
10946
|
classifyPathsByLayer,
|
|
10685
|
-
collectMissionDoctorFindings,
|
|
10686
10947
|
resolveClaudeRunnerModel,
|
|
10687
10948
|
resolveClaudeRunnerBin,
|
|
10688
10949
|
businessIdForAtris2Mission,
|
|
10689
10950
|
detectUnavailableModel,
|
|
10690
10951
|
missionPauseNextAction,
|
|
10691
|
-
HUMAN_BLOCKING_PAUSE_REASONS,
|
|
10692
10952
|
composeHumanBlockingPauseMessage,
|
|
10693
10953
|
escalateHumanBlockingPause,
|
|
10694
|
-
humanBlockingPauseReason,
|
|
10695
10954
|
consecutiveSameReasonErrors,
|
|
10696
10955
|
missionVerifierTimeoutMs,
|
|
10697
10956
|
missionLandingStepSummary,
|
|
10698
10957
|
missionLandingLines,
|
|
10699
10958
|
missionVerifierCheckedText,
|
|
10700
10959
|
missionVerifierHighLevelTestText,
|
|
10701
|
-
missionTickVerification,
|
|
10702
|
-
missionVerificationDebt,
|
|
10703
|
-
missionVerificationDebtLine,
|
|
10704
|
-
missionReportFor,
|
|
10705
|
-
renderMemberNowMarkdown,
|
|
10706
10960
|
buildEngineVerifyPrompt,
|
|
10707
10961
|
engineVerifierResultFromRun,
|
|
10962
|
+
runVerifier,
|
|
10708
10963
|
missionFullBudgetRemainingSeconds,
|
|
10709
10964
|
missionBudgetContinuationText,
|
|
10710
10965
|
missionHumanStatusText,
|
|
@@ -10718,4 +10973,6 @@ module.exports = {
|
|
|
10718
10973
|
missionJudgmentCard,
|
|
10719
10974
|
missionShippedCard,
|
|
10720
10975
|
appendMissionJudgmentCard,
|
|
10976
|
+
acquireMissionLock,
|
|
10977
|
+
releaseMissionLock,
|
|
10721
10978
|
};
|