atris 3.30.8 → 3.31.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 +16 -3
- package/FOR_AGENTS.md +81 -0
- package/README.md +4 -2
- package/atris/atris.md +51 -19
- package/atris/skills/README.md +1 -0
- package/atris/skills/blocks/SKILL.md +134 -0
- package/atris/skills/clawhub/philosophy-of-work/SKILL.md +56 -0
- package/atris/skills/youtube/SKILL.md +31 -11
- package/atris.md +7 -0
- package/ax +367 -93
- package/bin/atris.js +317 -155
- package/commands/autoland.js +319 -0
- package/commands/autopilot.js +94 -4
- package/commands/business.js +1 -1
- package/commands/clean.js +72 -9
- package/commands/codex-goal.js +72 -22
- package/commands/computer.js +48 -3
- package/commands/gm.js +1 -1
- package/commands/harvest.js +179 -0
- package/commands/init.js +1 -1
- package/commands/land.js +442 -0
- package/commands/loop-front.js +122 -4
- package/commands/member.js +519 -19
- package/commands/mission.js +3659 -282
- package/commands/play.js +1 -1
- package/commands/pulse.js +65 -7
- package/commands/run.js +3 -3
- package/commands/strings.js +301 -0
- package/commands/task.js +575 -102
- package/commands/truth.js +170 -0
- package/commands/xp.js +32 -8
- package/commands/youtube.js +72 -5
- package/decks/README.md +6 -12
- package/lib/auto-accept-certified.js +10 -0
- package/lib/autoland.js +283 -0
- package/lib/context-gatherer.js +0 -8
- package/lib/mission-artifact.js +504 -0
- package/lib/mission-room.js +846 -0
- package/lib/mission-runtime-loop.js +320 -0
- package/lib/next-moves.js +212 -6
- package/lib/pulse.js +74 -1
- package/lib/runner-command.js +20 -8
- package/lib/runs-prune.js +242 -0
- package/lib/task-proof.js +1 -1
- package/package.json +3 -3
- package/decks/atris-seed-pitch-v3.json +0 -118
- package/decks/atris-seed-pitch-v4-skeleton.json +0 -106
- package/decks/atris-seed-pitch-v5.json +0 -109
- package/decks/atris-seed-pitch-v6.json +0 -137
- package/decks/atris-seed-pitch-v7.json +0 -133
- package/decks/mark-pincus-narrative.json +0 -102
- package/decks/mark-pincus-sourcery.json +0 -94
- package/decks/yash-applied-compute-detailed.json +0 -150
- package/decks/yash-applied-compute-generalist.json +0 -82
- package/decks/yash-applied-compute-narrative.json +0 -54
- package/lib/ax-chat-input.js +0 -164
- package/lib/ax-goal.js +0 -307
- package/lib/ax-prefs.js +0 -63
- package/lib/ax-shimmer.js +0 -63
package/commands/mission.js
CHANGED
|
@@ -15,11 +15,27 @@ const {
|
|
|
15
15
|
normalizeOwnerSlug,
|
|
16
16
|
resolveFunctionalOwner,
|
|
17
17
|
} = require('../lib/functional-owner');
|
|
18
|
+
const {
|
|
19
|
+
buildMissionRoom,
|
|
20
|
+
writeMissionRoomReceipt,
|
|
21
|
+
missionRoomLines,
|
|
22
|
+
} = require('../lib/mission-room');
|
|
23
|
+
const {
|
|
24
|
+
missionArtifactPaths,
|
|
25
|
+
writeMissionArtifact,
|
|
26
|
+
} = require('../lib/mission-artifact');
|
|
27
|
+
const {
|
|
28
|
+
pruneRuns,
|
|
29
|
+
runsPruneLines,
|
|
30
|
+
formatBytes,
|
|
31
|
+
} = require('../lib/runs-prune');
|
|
18
32
|
|
|
19
33
|
const VALID_STATUSES = new Set(['planning', 'running', 'ready', 'paused', 'blocked', 'stopped', 'complete']);
|
|
20
34
|
const TERMINAL_STATUSES = new Set(['stopped', 'complete']);
|
|
21
35
|
const GOAL_LOOP_STATUSES = new Set(['planning', 'running', 'ready']);
|
|
22
36
|
const STATUS_ALIASES = new Set(['active']);
|
|
37
|
+
const DEFAULT_LONG_RUN_VERIFIER = 'git diff --check';
|
|
38
|
+
const SLEEP_LENGTH_BUDGET_SECONDS = 3600;
|
|
23
39
|
|
|
24
40
|
function stampIso() {
|
|
25
41
|
return new Date().toISOString();
|
|
@@ -84,6 +100,16 @@ function readPositiveIntegerFlag(args, name, fallback = null, options = {}) {
|
|
|
84
100
|
return value;
|
|
85
101
|
}
|
|
86
102
|
|
|
103
|
+
function readNonNegativeIntegerFlag(args, name, fallback = null) {
|
|
104
|
+
const raw = readFlag(args, name, '');
|
|
105
|
+
if (!raw) return fallback;
|
|
106
|
+
const value = Number(raw);
|
|
107
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
108
|
+
throw new Error(`${name} must be a non-negative integer.`);
|
|
109
|
+
}
|
|
110
|
+
return value;
|
|
111
|
+
}
|
|
112
|
+
|
|
87
113
|
function readRepeatedFlag(args, name) {
|
|
88
114
|
const values = [];
|
|
89
115
|
const prefix = `${name}=`;
|
|
@@ -147,6 +173,8 @@ function printJsonOrText(payload, lines, asJson) {
|
|
|
147
173
|
const MISSION_RUN_VALUE_FLAGS = [
|
|
148
174
|
'--max-ticks',
|
|
149
175
|
'--max-wall',
|
|
176
|
+
'--minutes',
|
|
177
|
+
'--hours',
|
|
150
178
|
'--cadence',
|
|
151
179
|
'--owner',
|
|
152
180
|
'--runner',
|
|
@@ -154,6 +182,10 @@ const MISSION_RUN_VALUE_FLAGS = [
|
|
|
154
182
|
'--verify',
|
|
155
183
|
'--stop',
|
|
156
184
|
'--model',
|
|
185
|
+
'--native-goal-status',
|
|
186
|
+
'--native-goal-objective',
|
|
187
|
+
'--visible-goal-status',
|
|
188
|
+
'--visible-goal-objective',
|
|
157
189
|
];
|
|
158
190
|
const MISSION_RUN_BOOLEAN_FLAGS = [
|
|
159
191
|
'--json',
|
|
@@ -162,6 +194,16 @@ const MISSION_RUN_BOOLEAN_FLAGS = [
|
|
|
162
194
|
'--no-verify',
|
|
163
195
|
'--complete-on-pass',
|
|
164
196
|
'--no-drain',
|
|
197
|
+
'--create-next',
|
|
198
|
+
'--spend-full-budget',
|
|
199
|
+
'--use-whole-budget',
|
|
200
|
+
'--stop-when-done',
|
|
201
|
+
'--room-preflight',
|
|
202
|
+
'--no-room-preflight',
|
|
203
|
+
'--room-auto-run',
|
|
204
|
+
'--no-room-auto-run',
|
|
205
|
+
'--allow-native-goal-supersede',
|
|
206
|
+
'--supersede-paused-native-goal',
|
|
165
207
|
'--always-on',
|
|
166
208
|
'--xp-task',
|
|
167
209
|
'--agent-xp',
|
|
@@ -321,7 +363,7 @@ function createMissionXpTask(mission, root = process.cwd(), asJson = false) {
|
|
|
321
363
|
taskDb.noteTask(db, {
|
|
322
364
|
id: task.id,
|
|
323
365
|
actor: process.env.ATRIS_AGENT_ID || owner,
|
|
324
|
-
content: `Mission goal loop XP bridge for ${mission.id}. Proof goes through task current-step; AgentXP
|
|
366
|
+
content: `Mission goal loop XP bridge for ${mission.id}. Proof goes through task current-step; AgentXP is awarded only after human approval.`,
|
|
325
367
|
});
|
|
326
368
|
}
|
|
327
369
|
const { outPath } = writeMissionTaskProjection(taskDb, db, workspaceRoot);
|
|
@@ -345,7 +387,10 @@ function statePaths(root = process.cwd()) {
|
|
|
345
387
|
missionsJsonl: path.join(stateDir, 'missions.jsonl'),
|
|
346
388
|
eventsJsonl: path.join(stateDir, 'mission_events.jsonl'),
|
|
347
389
|
codexGoalJson: path.join(stateDir, 'codex_goal.json'),
|
|
390
|
+
codexGoalRequestJson: path.join(stateDir, 'codex_goal_request.json'),
|
|
348
391
|
codexGoalStatus: path.join(root, 'atris', 'status', 'codex-goal.md'),
|
|
392
|
+
atrisGoalJson: path.join(stateDir, 'atris_goal.json'),
|
|
393
|
+
atrisGoalStatus: path.join(root, 'atris', 'status', 'atris-goal.md'),
|
|
349
394
|
statusNow: path.join(root, 'atris', 'status', 'now.md'),
|
|
350
395
|
runsDir: path.join(root, 'atris', 'runs'),
|
|
351
396
|
};
|
|
@@ -402,9 +447,21 @@ function terminalNextAction(status) {
|
|
|
402
447
|
|
|
403
448
|
function normalizeMissionState(mission) {
|
|
404
449
|
if (!mission) return mission;
|
|
450
|
+
let normalized = mission;
|
|
405
451
|
const nextAction = terminalNextAction(mission.status);
|
|
406
|
-
if (
|
|
407
|
-
|
|
452
|
+
if (nextAction && mission.next_action !== nextAction) {
|
|
453
|
+
normalized = { ...normalized, next_action: nextAction };
|
|
454
|
+
}
|
|
455
|
+
const effectiveVerifier = effectiveMissionVerifier(normalized);
|
|
456
|
+
const explicitVerifier = String(normalized.verifier || '').trim();
|
|
457
|
+
if (effectiveVerifier && effectiveVerifier !== explicitVerifier) {
|
|
458
|
+
return { ...normalized, effective_verifier: effectiveVerifier };
|
|
459
|
+
}
|
|
460
|
+
if (Object.prototype.hasOwnProperty.call(normalized, 'effective_verifier')) {
|
|
461
|
+
const { effective_verifier, ...withoutDerivedVerifier } = normalized;
|
|
462
|
+
return withoutDerivedVerifier;
|
|
463
|
+
}
|
|
464
|
+
return normalized;
|
|
408
465
|
}
|
|
409
466
|
|
|
410
467
|
function listMissions(root = process.cwd()) {
|
|
@@ -616,6 +673,7 @@ function completionGateLabel(gate) {
|
|
|
616
673
|
function missionCompletionReceipt(mission, proof, xpNextCommand = null) {
|
|
617
674
|
const gate = mission.completion_gate || {};
|
|
618
675
|
const gateLabel = completionGateLabel(gate) || 'completion gate';
|
|
676
|
+
const happened = `${mission.objective} is complete.`;
|
|
619
677
|
const checked = gate.source === 'receipt' && gate.receipt_path
|
|
620
678
|
? `I checked the passing verifier receipt ${gate.receipt_path}.`
|
|
621
679
|
: gate.source === 'mission_state'
|
|
@@ -624,21 +682,23 @@ function missionCompletionReceipt(mission, proof, xpNextCommand = null) {
|
|
|
624
682
|
? 'No verifier was configured, so completion used the no-verifier gate.'
|
|
625
683
|
: `I checked the ${gateLabel} completion gate.`;
|
|
626
684
|
const tested = mission.verifier_result?.passed
|
|
627
|
-
?
|
|
685
|
+
? missionVerifierHighLevelTestText(mission.verifier_result, mission)
|
|
628
686
|
: mission.verifier
|
|
629
687
|
? `Completion proof is attached for verifier: ${mission.verifier}.`
|
|
630
688
|
: 'No verifier command was recorded for this mission.';
|
|
631
689
|
const landing = {
|
|
632
|
-
happened
|
|
690
|
+
happened,
|
|
691
|
+
reason: missionHumanReasonText(mission, happened),
|
|
633
692
|
checked,
|
|
634
693
|
tested,
|
|
635
|
-
saved:
|
|
694
|
+
saved: proof ? `Proof saved at ${proof}.` : 'Proof saved in mission state.',
|
|
636
695
|
decision: xpNextCommand
|
|
637
|
-
?
|
|
638
|
-
: '
|
|
696
|
+
? 'Ready for human review; accept in Atris if the proof looks right.'
|
|
697
|
+
: 'Pick the next customer-facing move.',
|
|
639
698
|
};
|
|
640
699
|
const result = {
|
|
641
700
|
changed: landing.happened,
|
|
701
|
+
reason: landing.reason,
|
|
642
702
|
checked: landing.checked,
|
|
643
703
|
tested: landing.tested,
|
|
644
704
|
saved: landing.saved,
|
|
@@ -650,12 +710,657 @@ function missionCompletionReceipt(mission, proof, xpNextCommand = null) {
|
|
|
650
710
|
function missionResultLines(completion) {
|
|
651
711
|
const landing = completion?.landing || {};
|
|
652
712
|
const result = completion?.result || {};
|
|
653
|
-
const lines = ['
|
|
654
|
-
if (landing.happened) lines.push(`
|
|
713
|
+
const lines = ['Landing:'];
|
|
714
|
+
if (landing.happened) lines.push(` Changed: ${landing.happened}`);
|
|
715
|
+
if (landing.reason) lines.push(` Why it matters: ${landing.reason}`);
|
|
716
|
+
if (landing.artifact) lines.push(` Artifact: ${landing.artifact}`);
|
|
655
717
|
if (landing.checked) lines.push(` How I checked: ${landing.checked}`);
|
|
656
718
|
if (landing.tested) lines.push(` What I tested: ${landing.tested}`);
|
|
657
|
-
if (result.saved) lines.push(`
|
|
658
|
-
if (landing.decision) lines.push(`
|
|
719
|
+
if (result.saved) lines.push(` Proof: ${result.saved}`);
|
|
720
|
+
if (landing.decision) lines.push(` Next: ${landing.decision}`);
|
|
721
|
+
return lines;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function missionSelfImprovementSeedAction(mission, root = process.cwd()) {
|
|
725
|
+
try {
|
|
726
|
+
const moves = require('../lib/next-moves');
|
|
727
|
+
const seed = moves.nextMoves(root, 3).find((move) => (
|
|
728
|
+
move
|
|
729
|
+
&& move.source === 'mission'
|
|
730
|
+
&& (!mission?.id || !move.ref || move.ref === mission.id)
|
|
731
|
+
&& move.title !== mission.objective
|
|
732
|
+
));
|
|
733
|
+
return seed ? `${seed.title}.` : '';
|
|
734
|
+
} catch {
|
|
735
|
+
return '';
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function missionHumanNextAction(mission, root = process.cwd(), options = {}) {
|
|
740
|
+
if (!mission) return 'Pick the next customer-facing move.';
|
|
741
|
+
if (mission.goal_chain?.pause_ready) return 'Mission feels good; review proof, then complete, revise, or choose the next goal.';
|
|
742
|
+
if (mission.status === 'ready' && /^queue AgentXP review:/i.test(mission.next_action || '')) {
|
|
743
|
+
return 'Ready for human review; accept in Atris if the proof looks right.';
|
|
744
|
+
}
|
|
745
|
+
if (mission.status === 'ready' && mission.always_on) {
|
|
746
|
+
return options.allowSelfImprovementSeed
|
|
747
|
+
? (missionSelfImprovementSeedAction(mission, root) || 'Run the next proof step.')
|
|
748
|
+
: 'Run the next proof step.';
|
|
749
|
+
}
|
|
750
|
+
if (mission.status === 'ready') return 'Review the proof, then complete the mission.';
|
|
751
|
+
if (mission.status === 'complete') return 'Pick the next customer-facing move.';
|
|
752
|
+
if (mission.status === 'blocked') return 'Fix the verifier failure or revise the mission.';
|
|
753
|
+
return 'Keep running the mission.';
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function missionLandingStepSummary(summary) {
|
|
757
|
+
const clean = String(summary || '').replace(/\s+/g, ' ').trim();
|
|
758
|
+
if (!clean) return '';
|
|
759
|
+
const withoutLabel = clean
|
|
760
|
+
.replace(/^(?:landing|changed|summary|result|product proof|proof):\s*/i, '')
|
|
761
|
+
.trim();
|
|
762
|
+
if (!withoutLabel) return '';
|
|
763
|
+
const plainVerified = missionPlainVerifiedSummary(withoutLabel);
|
|
764
|
+
if (plainVerified) return plainVerified;
|
|
765
|
+
const beforeChecks = withoutLabel.split(/\s+(?:checks?|verified|proof):\s+/i)[0] || withoutLabel;
|
|
766
|
+
const clipped = missionLandingSentenceClip(beforeChecks, 220);
|
|
767
|
+
return clipped ? `${clipped}.` : '';
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function missionHumanReasonText(mission, changed = '') {
|
|
771
|
+
const objective = String(mission?.objective || '').trim();
|
|
772
|
+
const text = `${objective} ${changed}`.toLowerCase();
|
|
773
|
+
if (/\b(human|plain|language|landing|proof|receipt|understand|readable)\b/.test(text)) {
|
|
774
|
+
return 'It makes the result understandable before a human accepts or rejects it.';
|
|
775
|
+
}
|
|
776
|
+
if (/\b(update|install|runner|autopilot|mission run|heartbeat)\b/.test(text)) {
|
|
777
|
+
return 'It proves the workflow works in the place people actually use it.';
|
|
778
|
+
}
|
|
779
|
+
return 'It turns the mission into a concrete result a human can accept, reject, or run again.';
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function missionPlainVerifiedSummary(text) {
|
|
783
|
+
const clean = String(text || '').replace(/\s+/g, ' ').trim();
|
|
784
|
+
const shipped = clean.match(/^Verified\s+(.+?)\s+behavior\s+already\s+shipped\s+on\s+master\s*:/i);
|
|
785
|
+
if (!shipped) return '';
|
|
786
|
+
const topic = shipped[1].toLowerCase();
|
|
787
|
+
if (topic.includes('npm auto-update')) {
|
|
788
|
+
return 'Verified npm auto-update works for installed npm packages on master.';
|
|
789
|
+
}
|
|
790
|
+
if (topic.includes('runner-agnostic heartbeat')) {
|
|
791
|
+
return 'Verified autopilot and mission run use the same runner setup on master.';
|
|
792
|
+
}
|
|
793
|
+
return `Verified ${shipped[1]} is already working on master.`;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function missionLandingSentenceClip(text, max) {
|
|
797
|
+
const clean = String(text || '').replace(/\s+/g, ' ').trim();
|
|
798
|
+
if (!clean) return '';
|
|
799
|
+
if (clean.length <= max) return clean.replace(/[.!?:;,]+$/g, '').trim();
|
|
800
|
+
const sentence = clean.match(/^(.{24,220}?[.!?])\s+/);
|
|
801
|
+
const base = sentence ? sentence[1] : clean.slice(0, max);
|
|
802
|
+
return base
|
|
803
|
+
.replace(/\s+\S*$/, '')
|
|
804
|
+
.replace(/[.!?:;,]+$/g, '')
|
|
805
|
+
.replace(/\b(?:and|or|but|with|to|for|from|by|through|via|using|including|include|includes|plus|then|both|the|a|an|of|in|on|at|as)$/i, '')
|
|
806
|
+
.trim();
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function missionLastStepSummary(ticks = []) {
|
|
810
|
+
for (let index = ticks.length - 1; index >= 0; index -= 1) {
|
|
811
|
+
const tick = ticks[index] || {};
|
|
812
|
+
const summary = missionLandingStepSummary(tick.summary || tick.claude?.summary || tick.atris2?.receipt_text || '');
|
|
813
|
+
if (summary) return summary;
|
|
814
|
+
}
|
|
815
|
+
return '';
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function missionVerifierCheckedText(verifierResult, mission) {
|
|
819
|
+
if (!verifierResult) return 'Tick recorded; no verifier was run.';
|
|
820
|
+
const command = verifierResult.command || mission.verifier || 'configured verifier';
|
|
821
|
+
if (verifierResult.passed) {
|
|
822
|
+
if (/^git\s+diff\s+--check\b/i.test(command)) return 'I ran the diff cleanliness check.';
|
|
823
|
+
if (/\bnode\s+--test\b/i.test(command)) return 'I ran the behavior checks.';
|
|
824
|
+
return `Verifier passed: ${command}.`;
|
|
825
|
+
}
|
|
826
|
+
if (/^git\s+diff\s+--check\b/i.test(command)) return 'Diff cleanliness check failed.';
|
|
827
|
+
if (/\bnode\s+--test\b/i.test(command)) return 'Behavior checks failed.';
|
|
828
|
+
return `Verifier failed: ${command}.`;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function missionVerifierHighLevelTestText(verifierResult, mission) {
|
|
832
|
+
if (!verifierResult) return 'No automated verifier ran for this receipt; judge it from the receipt, changed files, and next action.';
|
|
833
|
+
const command = verifierResult.command || mission.verifier || 'configured verifier';
|
|
834
|
+
const outcome = verifierResult.passed ? 'passed' : 'failed';
|
|
835
|
+
if (/^git\s+diff\s+--check\b/i.test(command)) {
|
|
836
|
+
return `Diff cleanliness check ${outcome}: no whitespace or patch-format issues in the changed files.`;
|
|
837
|
+
}
|
|
838
|
+
if (/\bnode\s+--test\b/i.test(command) && /\btest\/mission-status\.test\.js\b/i.test(command)) {
|
|
839
|
+
return `Mission behavior checks ${outcome}: mission start, tick, completion, timeline landing, goal-chain, next-mission, and human-accept boundaries were exercised.`;
|
|
840
|
+
}
|
|
841
|
+
if (/\bnode\s+--test\b/i.test(command)) {
|
|
842
|
+
return `Automated behavior checks ${outcome}: the touched code path was exercised by Node tests.`;
|
|
843
|
+
}
|
|
844
|
+
return `Verifier command ${outcome}: ${command}.`;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
function missionFallbackChangedText(mission, status, tickIndex, { ranTicks = null, effectiveMaxTicks = null } = {}) {
|
|
848
|
+
if (mission?.always_on && (status === 'ready' || status === 'running')) {
|
|
849
|
+
return 'Recorded a proof heartbeat for this always-on mission.';
|
|
850
|
+
}
|
|
851
|
+
if (status === 'ready') return `${mission.objective} is ready for review.`;
|
|
852
|
+
if (status === 'complete') return `${mission.objective} is complete.`;
|
|
853
|
+
if (status === 'blocked') return `${mission.objective} is blocked.`;
|
|
854
|
+
if (ranTicks != null && effectiveMaxTicks != null) {
|
|
855
|
+
return ranTicks > 0
|
|
856
|
+
? `${mission.objective} ran ${ranTicks}/${effectiveMaxTicks} tick(s).`
|
|
857
|
+
: `${mission.objective} did not run a tick.`;
|
|
858
|
+
}
|
|
859
|
+
return `${mission.objective} recorded tick ${tickIndex}.`;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function missionTickResultLines(mission, tickIndex, receiptPath, verifierResult = null, stepSummary = '') {
|
|
863
|
+
return missionLandingLines(missionReceiptLanding(mission, {
|
|
864
|
+
tick: { tick_index: tickIndex },
|
|
865
|
+
summary: stepSummary,
|
|
866
|
+
verifier_result: verifierResult,
|
|
867
|
+
}, receiptPath));
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function missionReceiptSummaryText(result) {
|
|
871
|
+
const tick = result?.tick || {};
|
|
872
|
+
return tick.summary
|
|
873
|
+
|| tick.atris2?.receipt_text
|
|
874
|
+
|| tick.claude?.receipt_text
|
|
875
|
+
|| result?.summary
|
|
876
|
+
|| result?.reason
|
|
877
|
+
|| '';
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
function missionReceiptTickIndex(mission, result) {
|
|
881
|
+
const value = Number(result?.tick?.tick_index || mission?.last_tick_index || 0);
|
|
882
|
+
return Number.isFinite(value) && value > 0 ? value : 1;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
function missionReceiptStatus(mission, result) {
|
|
886
|
+
const tickStatus = String(result?.tick?.status || '').trim();
|
|
887
|
+
if (tickStatus === 'blocked') return 'blocked';
|
|
888
|
+
if (result?.verifier_result?.passed === false) return 'blocked';
|
|
889
|
+
if (result?.verifier_result?.passed === true) return 'proof_ready';
|
|
890
|
+
return String(mission?.status || 'running');
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
function missionReceiptNextText(mission, result) {
|
|
894
|
+
if (result?.next) return String(result.next);
|
|
895
|
+
if (result?.landing?.next) return String(result.landing.next);
|
|
896
|
+
if (result?.verifier_result?.passed === true) {
|
|
897
|
+
return mission?.always_on ? 'Run the next proof step.' : 'Review the proof, then complete the mission.';
|
|
898
|
+
}
|
|
899
|
+
if (result?.verifier_result) return 'Fix the verifier failure or revise the mission.';
|
|
900
|
+
return missionHumanNextAction(mission);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
function missionReceiptLanding(mission, result, receiptPath = '') {
|
|
904
|
+
const verifierResult = result?.verifier_result || null;
|
|
905
|
+
const status = missionReceiptStatus(mission, result);
|
|
906
|
+
const summary = missionReceiptSummaryText(result);
|
|
907
|
+
const changed = missionLandingStepSummary(summary)
|
|
908
|
+
|| missionFallbackChangedText(mission, status, missionReceiptTickIndex(mission, result));
|
|
909
|
+
const checked = missionVerifierCheckedText(verifierResult, mission);
|
|
910
|
+
const tested = missionVerifierHighLevelTestText(verifierResult, mission);
|
|
911
|
+
const reason = missionHumanReasonText(mission, changed);
|
|
912
|
+
return {
|
|
913
|
+
schema: 'atris.result_landing.v1',
|
|
914
|
+
status,
|
|
915
|
+
changed,
|
|
916
|
+
reason,
|
|
917
|
+
checked,
|
|
918
|
+
tested,
|
|
919
|
+
proof: receiptPath ? `Receipt saved at ${receiptPath}.` : 'Receipt saved in mission run history.',
|
|
920
|
+
next: missionReceiptNextText(mission, result),
|
|
921
|
+
timeline_visible: !missionLandingChangedIsGenericTick(mission, changed),
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
function missionLandingLines(landing) {
|
|
926
|
+
if (!landing) return [];
|
|
927
|
+
return [
|
|
928
|
+
'Landing:',
|
|
929
|
+
` Changed: ${landing.changed || landing.happened || 'Landing recorded.'}`,
|
|
930
|
+
` Why it matters: ${landing.reason || landing.why || 'This makes the work easier to judge.'}`,
|
|
931
|
+
` How I checked: ${landing.checked || 'No check recorded.'}`,
|
|
932
|
+
` What I tested: ${landing.tested || 'No test summary recorded.'}`,
|
|
933
|
+
` Proof: ${landing.proof || landing.saved || 'No proof path recorded.'}`,
|
|
934
|
+
` Next: ${landing.next || landing.decision || 'Pick the next useful move.'}`,
|
|
935
|
+
];
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
function missionStatusLandingLines(landing) {
|
|
939
|
+
const lines = missionLandingLines(landing);
|
|
940
|
+
if (!lines.length) return [];
|
|
941
|
+
return [
|
|
942
|
+
' last landing:',
|
|
943
|
+
...lines.slice(1).map((line) => ` ${line.trim()}`),
|
|
944
|
+
];
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
function missionLastLanding(mission, root = process.cwd()) {
|
|
948
|
+
const receipt = readMissionReceipt(mission?.receipt_path, mission?.worktree_root || root);
|
|
949
|
+
const landing = receipt?.result?.landing;
|
|
950
|
+
if (!landing || typeof landing !== 'object') return null;
|
|
951
|
+
return {
|
|
952
|
+
...landing,
|
|
953
|
+
receipt_path: mission.receipt_path || null,
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
function missionLandingChangedIsGenericTick(mission, changed) {
|
|
958
|
+
const text = String(changed || '').trim();
|
|
959
|
+
const objective = String(mission?.objective || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
960
|
+
if (!objective) return false;
|
|
961
|
+
return new RegExp(`^${objective} recorded tick \\d+\\.$`).test(text);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
function normalizeMissionReceiptResult(mission, result, receiptPath = '') {
|
|
965
|
+
const object = result && typeof result === 'object' && !Array.isArray(result)
|
|
966
|
+
? { ...result }
|
|
967
|
+
: { value: result };
|
|
968
|
+
if (!object.landing) {
|
|
969
|
+
object.landing = missionReceiptLanding(mission, object, receiptPath);
|
|
970
|
+
}
|
|
971
|
+
if (object.verifier_result && !('passed' in object)) {
|
|
972
|
+
object.passed = !!object.verifier_result.passed;
|
|
973
|
+
}
|
|
974
|
+
return object;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
function missionRunStartNextLine(mission, nextCommand, warnings = []) {
|
|
978
|
+
if (warnings.length) return 'Add a verifier before completion, then run the first proof tick.';
|
|
979
|
+
if (isCodexGoalMission(mission) && !codexNativeGoalAck(mission)) {
|
|
980
|
+
return 'Start the visible goal, then continue this mission.';
|
|
981
|
+
}
|
|
982
|
+
if (/attach-task/.test(nextCommand || '')) return 'Attach task context, then continue this mission.';
|
|
983
|
+
return 'Run the first proof tick.';
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
function missionRunTakeoffLines(mission, { warnings = [], nextCommand = '' } = {}) {
|
|
987
|
+
const checked = mission.verifier
|
|
988
|
+
? `Verifier configured: ${mission.verifier}.`
|
|
989
|
+
: 'No verifier was recorded for this mission.';
|
|
990
|
+
return [
|
|
991
|
+
'Takeoff:',
|
|
992
|
+
` Goal: ${mission.objective}`,
|
|
993
|
+
` Done when: ${mission.stop_condition || 'the mission has proof or a human decision'}.`,
|
|
994
|
+
...(missionBudgetLine(mission) ? [` Budget: ${missionBudgetLine(mission)}`] : []),
|
|
995
|
+
...missionGoalChainLines(mission),
|
|
996
|
+
' Proof: Mission state saved in .atris/state/missions.jsonl.',
|
|
997
|
+
` Check: ${checked}`,
|
|
998
|
+
` Next: ${missionRunStartNextLine(mission, nextCommand, warnings)}`,
|
|
999
|
+
];
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
function missionRunPreflightSignals(text) {
|
|
1003
|
+
const lower = String(text || '').toLowerCase().replace(/\s+/g, ' ');
|
|
1004
|
+
return /\b(messy|shower|overnight|nonstop|forever|goal\s+after\s+goal|self[-\s]?improve|figure\s+out|think\s+through|thinkwell|what\s+to\s+do\s+next|keep\s+going|tell\s+me|right\s+mission\s+input|finish[-\s]+line)\b/i.test(lower);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
function shouldMissionRunRoomPreflight(objective, args = []) {
|
|
1008
|
+
if (hasFlag(args, '--no-room-preflight')) return false;
|
|
1009
|
+
if (hasFlag(args, '--room-preflight')) return true;
|
|
1010
|
+
return missionRunPreflightSignals(objective) || missionRunTrustedRoomSignals(objective);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
function missionRunTrustedRoomSignals(text) {
|
|
1014
|
+
const lower = String(text || '').toLowerCase().replace(/\s+/g, ' ');
|
|
1015
|
+
return /\b(one[-\s]?message|autonomy|autonomous|self[-\s]?improve|improve\s+(atris|this|it)|keep\s+going|work\s+on\s+this|next\s+useful|goes?\s+off|no\s+junk|junk\s+state)\b/i.test(lower);
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function shouldMissionRunTrustedRoom(rawObjective, args = []) {
|
|
1019
|
+
if (hasFlag(args, '--no-room-auto-run')) return false;
|
|
1020
|
+
if (hasFlag(args, '--room-auto-run')) return true;
|
|
1021
|
+
return missionRunTrustedRoomSignals(rawObjective);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
function missionRunConcreteTitle(value) {
|
|
1025
|
+
const title = String(value || '').replace(/\s+/g, ' ').trim();
|
|
1026
|
+
if (!title) return '';
|
|
1027
|
+
if (/^(go|go go|go go go|keep going|do it|start|run|continue)$/i.test(title)) return '';
|
|
1028
|
+
if (title.length < 8) return '';
|
|
1029
|
+
return title;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
function selectMissionRunUsefulTarget(rawObjective, root = process.cwd()) {
|
|
1033
|
+
try {
|
|
1034
|
+
const moves = require('../lib/next-moves');
|
|
1035
|
+
const rawTitle = missionRunConcreteTitle(rawObjective);
|
|
1036
|
+
return moves.nextMoves(root, 8)
|
|
1037
|
+
.filter((move) => missionRunConcreteTitle(move?.title))
|
|
1038
|
+
.filter((move) => !rawTitle || String(move.title).trim() !== rawTitle)
|
|
1039
|
+
.find((move) => move.source === 'task')
|
|
1040
|
+
|| moves.nextMoves(root, 8).find((move) => missionRunConcreteTitle(move?.title))
|
|
1041
|
+
|| null;
|
|
1042
|
+
} catch {
|
|
1043
|
+
return null;
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
function missionRunPreflightSentence(text, max = 140) {
|
|
1048
|
+
const clean = String(text || '').replace(/\s+/g, ' ').trim();
|
|
1049
|
+
if (clean.length <= max) return clean;
|
|
1050
|
+
return `${clean.slice(0, max - 3).replace(/\s+\S*$/, '').trimEnd()}...`;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
function missionRunPreflightObjective(rawObjective, room, owner) {
|
|
1054
|
+
const name = room?.name || 'Mission Room';
|
|
1055
|
+
const task = room?.task_plan_preview?.task || room?.truth_snapshot || rawObjective;
|
|
1056
|
+
return `${name} with ${owner}: turn "${missionRunPreflightSentence(task)}" into one visible goal, task spine, proof receipt, and next action.`;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
function missionRunTrustedObjective(rawObjective, room, target) {
|
|
1060
|
+
const targetTitle = missionRunConcreteTitle(target?.title);
|
|
1061
|
+
if (targetTitle) return targetTitle;
|
|
1062
|
+
if (missionRunTrustedRoomSignals(rawObjective)) {
|
|
1063
|
+
return 'Improve Atris one-message autonomy without creating junk mission state';
|
|
1064
|
+
}
|
|
1065
|
+
return missionRunPreflightObjective(rawObjective, room, room?.owner || 'mission-lead');
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
function buildMissionRunRoomPreflight(rawObjective, args = [], options = {}) {
|
|
1069
|
+
if (!shouldMissionRunRoomPreflight(rawObjective, args)) return null;
|
|
1070
|
+
const root = options.root || process.cwd();
|
|
1071
|
+
const owner = options.owner || readFlag(args, '--owner', process.env.ATRIS_AGENT_ID || 'mission-lead');
|
|
1072
|
+
const trustedRun = options.allowTrustedRun !== false && shouldMissionRunTrustedRoom(rawObjective, args);
|
|
1073
|
+
const selectedTarget = trustedRun ? selectMissionRunUsefulTarget(rawObjective, root) : null;
|
|
1074
|
+
const ownerResolution = resolveFunctionalOwner({
|
|
1075
|
+
requestedOwner: owner,
|
|
1076
|
+
title: selectedTarget?.title || rawObjective,
|
|
1077
|
+
tag: readFlag(args, '--lane', 'workspace'),
|
|
1078
|
+
goal: selectedTarget?.title || rawObjective,
|
|
1079
|
+
root,
|
|
1080
|
+
fallbackOwners: ['mission-lead', 'task-planner', 'architect', 'validator'],
|
|
1081
|
+
});
|
|
1082
|
+
const room = buildMissionRoom(rawObjective, {
|
|
1083
|
+
root,
|
|
1084
|
+
owner: ownerResolution.owner,
|
|
1085
|
+
ownerResolution,
|
|
1086
|
+
trustedRun,
|
|
1087
|
+
selectedTarget,
|
|
1088
|
+
verifier: trustedRun ? DEFAULT_LONG_RUN_VERIFIER : '',
|
|
1089
|
+
});
|
|
1090
|
+
const written = writeMissionRoomReceipt(room, { root });
|
|
1091
|
+
const shapedObjective = trustedRun
|
|
1092
|
+
? missionRunTrustedObjective(rawObjective, written.room, selectedTarget)
|
|
1093
|
+
: missionRunPreflightObjective(rawObjective, written.room, ownerResolution.owner);
|
|
1094
|
+
return {
|
|
1095
|
+
schema: 'atris.mission_run_preflight.v1',
|
|
1096
|
+
source: 'mission_room',
|
|
1097
|
+
raw_objective: rawObjective,
|
|
1098
|
+
shaped_objective: shapedObjective,
|
|
1099
|
+
visible_goal_objective: shapedObjective,
|
|
1100
|
+
room_name: written.room.name,
|
|
1101
|
+
room_receipt_path: written.relativePath,
|
|
1102
|
+
owner: ownerResolution.owner,
|
|
1103
|
+
owner_resolution: ownerResolution.reason,
|
|
1104
|
+
trusted_run: trustedRun,
|
|
1105
|
+
selected_target: selectedTarget ? {
|
|
1106
|
+
title: selectedTarget.title,
|
|
1107
|
+
source: selectedTarget.source,
|
|
1108
|
+
task_id: selectedTarget.task_id || null,
|
|
1109
|
+
ref: selectedTarget.ref || null,
|
|
1110
|
+
why: selectedTarget.why || '',
|
|
1111
|
+
} : null,
|
|
1112
|
+
task_spine_required: !selectedTarget,
|
|
1113
|
+
next_action: selectedTarget
|
|
1114
|
+
? 'run one proof tick for the selected existing task'
|
|
1115
|
+
: 'attach task spine, then run one proof tick',
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
function missionRunTaskLabel(task) {
|
|
1120
|
+
const ref = task?.ref || task?.display_id || task?.id || 'task';
|
|
1121
|
+
return [ref, task?.title].filter(Boolean).join(' ');
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
function missionRunSelectedTaskTarget(preflight) {
|
|
1125
|
+
const target = preflight?.selected_target;
|
|
1126
|
+
if (!target || target.source !== 'task') return null;
|
|
1127
|
+
const taskId = String(target.task_id || '').trim();
|
|
1128
|
+
const ref = String(target.ref || taskId || '').trim();
|
|
1129
|
+
if (!taskId && !ref) return null;
|
|
1130
|
+
return {
|
|
1131
|
+
task_id: taskId || ref,
|
|
1132
|
+
task_ref: ref || taskId,
|
|
1133
|
+
title: target.title || '',
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
function attachSelectedTargetTaskSpine(mission) {
|
|
1138
|
+
const selected = missionRunSelectedTaskTarget(mission?.mission_run_preflight);
|
|
1139
|
+
if (!selected) return mission;
|
|
1140
|
+
const taskIds = Array.from(new Set([...(mission.task_ids || []), selected.task_id].filter(Boolean)));
|
|
1141
|
+
return {
|
|
1142
|
+
...mission,
|
|
1143
|
+
task_ids: taskIds,
|
|
1144
|
+
current_task_id: selected.task_id,
|
|
1145
|
+
task_ref: selected.task_ref,
|
|
1146
|
+
task_scope_ref: selected.task_ref || selected.task_id,
|
|
1147
|
+
selected_target_task: selected,
|
|
1148
|
+
next_action: `work selected task then run: atris task current-step --goal-id ${selected.task_ref || selected.task_id} --as ${mission.owner} --proof "<proof>" --json`,
|
|
1149
|
+
};
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
function missionRunCreatedNextChangedText(createdNext) {
|
|
1153
|
+
const createdTask = createdNext?.ok ? createdNext.task : null;
|
|
1154
|
+
if (createdTask) {
|
|
1155
|
+
return `Created and claimed next task: ${missionRunTaskLabel(createdTask)}.`;
|
|
1156
|
+
}
|
|
1157
|
+
const activeTask = createdNext?.reason === 'active_task' ? createdNext.move : null;
|
|
1158
|
+
if (activeTask) {
|
|
1159
|
+
return `Kept active task: ${missionRunTaskLabel(activeTask)}. No duplicate was created.`;
|
|
1160
|
+
}
|
|
1161
|
+
return null;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
function missionRunCreatedNextLine(createdNext, continuationGoal, mission) {
|
|
1165
|
+
const createdTask = createdNext?.ok ? createdNext.task : null;
|
|
1166
|
+
if (createdTask) {
|
|
1167
|
+
return `Created next task: ${missionRunTaskLabel(createdTask)}.`;
|
|
1168
|
+
}
|
|
1169
|
+
const activeTask = createdNext?.reason === 'active_task' ? createdNext.move : null;
|
|
1170
|
+
if (activeTask) {
|
|
1171
|
+
return `Continue active task: ${missionRunTaskLabel(activeTask)}.`;
|
|
1172
|
+
}
|
|
1173
|
+
return continuationGoal?.mission
|
|
1174
|
+
? `Next mission: ${continuationGoal.mission.objective}.`
|
|
1175
|
+
: missionHumanNextAction(mission, process.cwd(), { allowSelfImprovementSeed: true });
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
function missionRunTimelineCommand(mission) {
|
|
1179
|
+
return `atris mission timeline ${mission.id} --limit 5`;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
function missionRunExportCommand(mission) {
|
|
1183
|
+
return `atris mission timeline ${mission.id} --all --write`;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
function missionRunPrunePreviewCommand(mission) {
|
|
1187
|
+
return `atris mission timeline ${mission.id} --prune-preview`;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
function missionRunChangedText(mission, ranTicks, effectiveMaxTicks, ticks = [], createdNext = null) {
|
|
1191
|
+
const stepChanged = missionLastStepSummary(ticks);
|
|
1192
|
+
const createdNextChanged = missionRunCreatedNextChangedText(createdNext);
|
|
1193
|
+
return createdNextChanged || stepChanged || missionFallbackChangedText(mission, mission.status, null, { ranTicks, effectiveMaxTicks });
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
function missionBudgetLine(mission) {
|
|
1197
|
+
const contract = mission?.budget_contract;
|
|
1198
|
+
if (!contract) return null;
|
|
1199
|
+
return `${contract.plain_language} Limit: ${contract.budget_label}.`;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
function missionSpendsFullBudget(mission) {
|
|
1203
|
+
return mission?.budget_contract?.policy === 'spend_full_budget';
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
function missionFullBudgetRemainingSeconds(mission, nowMs = Date.now()) {
|
|
1207
|
+
if (!missionSpendsFullBudget(mission)) return 0;
|
|
1208
|
+
const budgetSeconds = Number(mission?.budget_contract?.requested_seconds || mission?.max_wall_seconds || 0);
|
|
1209
|
+
if (!Number.isFinite(budgetSeconds) || budgetSeconds <= 0) return 0;
|
|
1210
|
+
const startedMs = Date.parse(mission.started_at || mission.created_at || mission.updated_at || '');
|
|
1211
|
+
if (!Number.isFinite(startedMs)) return 0;
|
|
1212
|
+
return Math.max(0, Math.ceil((startedMs + budgetSeconds * 1000 - nowMs) / 1000));
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
function missionGoalChainIntent(text) {
|
|
1216
|
+
const lower = String(text || '').toLowerCase().replace(/\s+/g, ' ');
|
|
1217
|
+
return /\b(3\s*(?:or|-)?\s*4|three\s+or\s+four|multiple|child|subgoals?|goal\s+after\s+goal|keeps?\s+goaling|mission\s+feeling\s+good|feels?\s+good|validated\s+and\s+i\s+can\s+understand)\b/.test(lower)
|
|
1218
|
+
|| /\bset\s+a\s+goal\b.{0,160}\b(accomplish|complete|finish|prove)\w*\b.{0,200}\bnext\s+(?:useful\s+)?goal\b/.test(lower);
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
function missionGoalChainTargetCount(text) {
|
|
1222
|
+
const lower = String(text || '').toLowerCase();
|
|
1223
|
+
if (/\b3\s*(?:or|-)?\s*4\b|\bthree\s+or\s+four\b/.test(lower)) return 4;
|
|
1224
|
+
const explicit = lower.match(/\b([3-4])\s+(?:goals?|child\s+goals?|subgoals?)\b/);
|
|
1225
|
+
if (explicit) return Number(explicit[1]);
|
|
1226
|
+
return 4;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
function buildMissionGoalChain(objective, options = {}) {
|
|
1230
|
+
const targetCount = Math.max(3, Math.min(4, Number(options.targetCount || missionGoalChainTargetCount(objective)) || 4));
|
|
1231
|
+
const baseGoals = [
|
|
1232
|
+
{
|
|
1233
|
+
title: 'Find the novel mission',
|
|
1234
|
+
done_when: 'A concrete mission is named with why it matters now.',
|
|
1235
|
+
validation: 'Plain-English reason plus the source signal that made it worth doing.',
|
|
1236
|
+
},
|
|
1237
|
+
{
|
|
1238
|
+
title: 'Split it into child goals',
|
|
1239
|
+
done_when: 'The mission has a 3-4 step path with a clear first move.',
|
|
1240
|
+
validation: 'Each child goal says what proof would make it done.',
|
|
1241
|
+
},
|
|
1242
|
+
{
|
|
1243
|
+
title: 'Run the smallest proof',
|
|
1244
|
+
done_when: 'One real artifact, code change, status output, or receipt exists.',
|
|
1245
|
+
validation: 'A verifier, receipt, or inspectable output proves the work happened.',
|
|
1246
|
+
},
|
|
1247
|
+
{
|
|
1248
|
+
title: 'Explain the pause or next goal',
|
|
1249
|
+
done_when: 'The result says what changed, how it was checked, and whether to accept, revise, or continue.',
|
|
1250
|
+
validation: 'The operator can understand the mission state without reading logs.',
|
|
1251
|
+
},
|
|
1252
|
+
].slice(0, targetCount).map((goal, index) => ({
|
|
1253
|
+
order: index + 1,
|
|
1254
|
+
status: 'pending',
|
|
1255
|
+
...goal,
|
|
1256
|
+
}));
|
|
1257
|
+
|
|
1258
|
+
return {
|
|
1259
|
+
schema: 'atris.mission_goal_chain.v1',
|
|
1260
|
+
mode: 'validated_child_goals',
|
|
1261
|
+
status: 'planned',
|
|
1262
|
+
target_count: targetCount,
|
|
1263
|
+
done_count: 0,
|
|
1264
|
+
current_goal_order: 1,
|
|
1265
|
+
plain_language: 'One mission can be reached by several small goals, each with proof.',
|
|
1266
|
+
pause_rule: 'Pause when the chain has proof strong enough to understand, accept, revise, or choose the next mission.',
|
|
1267
|
+
validation_rule: 'Every child goal must leave a receipt, verifier result, artifact, or explicit stop reason.',
|
|
1268
|
+
goals: baseGoals,
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
function missionGoalChainCounts(goalChain) {
|
|
1273
|
+
const goals = Array.isArray(goalChain?.goals) ? goalChain.goals : [];
|
|
1274
|
+
const done = goals.filter((goal) => goal?.status === 'done').length;
|
|
1275
|
+
return { done, total: goals.length };
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
function missionGoalChainPendingGoal(goalChain) {
|
|
1279
|
+
const goals = Array.isArray(goalChain?.goals) ? goalChain.goals : [];
|
|
1280
|
+
return goals.find((goal) => goal?.status !== 'done' && goal?.status !== 'blocked') || null;
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
function missionGoalChainNextAction(goalChain) {
|
|
1284
|
+
const goal = missionGoalChainPendingGoal(goalChain);
|
|
1285
|
+
if (!goal) return 'continue child-goal chain';
|
|
1286
|
+
return `continue child goal ${goal.order}: ${goal.title}`;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
function advanceMissionGoalChain(goalChain, summary, verifierResult = null) {
|
|
1290
|
+
if (!goalChain || !Array.isArray(goalChain.goals) || !goalChain.goals.length) return goalChain || null;
|
|
1291
|
+
const cleanSummary = String(summary || '').replace(/\s+/g, ' ').trim();
|
|
1292
|
+
if (!cleanSummary && !verifierResult) return goalChain;
|
|
1293
|
+
|
|
1294
|
+
const goals = goalChain.goals.map((goal) => ({ ...goal }));
|
|
1295
|
+
const nextIndex = goals.findIndex((goal) => goal.status !== 'done');
|
|
1296
|
+
if (nextIndex === -1) return goalChain;
|
|
1297
|
+
|
|
1298
|
+
const validationResult = verifierResult
|
|
1299
|
+
? (verifierResult.passed ? 'Verifier passed.' : 'Verifier failed; this goal needs repair.')
|
|
1300
|
+
: 'Summary receipt recorded.';
|
|
1301
|
+
goals[nextIndex] = {
|
|
1302
|
+
...goals[nextIndex],
|
|
1303
|
+
status: verifierResult && !verifierResult.passed ? 'blocked' : 'done',
|
|
1304
|
+
completed_at: stampIso(),
|
|
1305
|
+
result: cleanSummary.slice(0, 240) || validationResult,
|
|
1306
|
+
validation_result: validationResult,
|
|
1307
|
+
};
|
|
1308
|
+
|
|
1309
|
+
const counts = missionGoalChainCounts({ goals });
|
|
1310
|
+
const blocked = goals.some((goal) => goal.status === 'blocked');
|
|
1311
|
+
const nextPending = goals.find((goal) => goal.status !== 'done' && goal.status !== 'blocked');
|
|
1312
|
+
return {
|
|
1313
|
+
...goalChain,
|
|
1314
|
+
goals,
|
|
1315
|
+
done_count: counts.done,
|
|
1316
|
+
current_goal_order: nextPending ? nextPending.order : null,
|
|
1317
|
+
status: blocked ? 'blocked' : counts.done >= counts.total ? 'validated' : 'running',
|
|
1318
|
+
pause_ready: !blocked && counts.done >= counts.total,
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
function missionGoalChainLines(mission) {
|
|
1323
|
+
const chain = mission?.goal_chain;
|
|
1324
|
+
if (!chain || !Array.isArray(chain.goals) || !chain.goals.length) return [];
|
|
1325
|
+
const counts = missionGoalChainCounts(chain);
|
|
1326
|
+
const lines = [
|
|
1327
|
+
` goal chain: ${counts.done}/${counts.total} done; ${chain.pause_rule}`,
|
|
1328
|
+
];
|
|
1329
|
+
for (const goal of chain.goals) {
|
|
1330
|
+
const mark = goal.status === 'done' ? '[x]' : goal.status === 'blocked' ? '[!]' : '[ ]';
|
|
1331
|
+
lines.push(` ${mark} ${goal.order}. ${goal.title} -> ${goal.validation}`);
|
|
1332
|
+
}
|
|
1333
|
+
return lines;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
function missionRunSummaryLines(mission, ranTicks, effectiveMaxTicks, finalReceipt, pauseReason = null, continuationGoal = null, ticks = [], createdNext = null) {
|
|
1337
|
+
const changed = missionRunChangedText(mission, ranTicks, effectiveMaxTicks, ticks, createdNext);
|
|
1338
|
+
const reason = missionHumanReasonText(mission, changed);
|
|
1339
|
+
const verifier = mission.verifier_result;
|
|
1340
|
+
const checked = verifier
|
|
1341
|
+
? missionVerifierCheckedText(verifier, mission)
|
|
1342
|
+
: pauseReason
|
|
1343
|
+
? `Run paused: ${pauseReason}.`
|
|
1344
|
+
: 'Run recorded; no verifier was run.';
|
|
1345
|
+
const tested = verifier
|
|
1346
|
+
? missionVerifierHighLevelTestText(verifier, mission)
|
|
1347
|
+
: mission.verifier
|
|
1348
|
+
? `Verifier was configured but not completed: ${mission.verifier}.`
|
|
1349
|
+
: 'No verifier command was recorded for this mission.';
|
|
1350
|
+
const nextLine = missionRunCreatedNextLine(createdNext, continuationGoal, mission);
|
|
1351
|
+
const lines = [
|
|
1352
|
+
'Landing:',
|
|
1353
|
+
` Changed: ${changed}`,
|
|
1354
|
+
` Why it matters: ${reason}`,
|
|
1355
|
+
...(missionBudgetLine(mission) ? [` Budget: ${missionBudgetLine(mission)}`] : []),
|
|
1356
|
+
` How I checked: ${checked}`,
|
|
1357
|
+
` What I tested: ${tested}`,
|
|
1358
|
+
` Proof: Summary receipt saved at ${finalReceipt}.`,
|
|
1359
|
+
` Timeline: ${missionRunTimelineCommand(mission)}`,
|
|
1360
|
+
` Export: ${missionRunExportCommand(mission)}`,
|
|
1361
|
+
` Prune preview: ${missionRunPrunePreviewCommand(mission)}`,
|
|
1362
|
+
` Next: ${nextLine}`,
|
|
1363
|
+
];
|
|
659
1364
|
return lines;
|
|
660
1365
|
}
|
|
661
1366
|
|
|
@@ -761,6 +1466,7 @@ function missionTaskSpine(mission) {
|
|
|
761
1466
|
const taskRef = mission.xp_task?.ref
|
|
762
1467
|
|| mission.task_ref
|
|
763
1468
|
|| (taskId ? String(taskId) : null);
|
|
1469
|
+
const taskScopeRef = mission.task_scope_ref || mission.id;
|
|
764
1470
|
const owner = ownerResolution.owner;
|
|
765
1471
|
return {
|
|
766
1472
|
schema: 'atris.mission_task_spine.v1',
|
|
@@ -776,7 +1482,7 @@ function missionTaskSpine(mission) {
|
|
|
776
1482
|
task_ref: taskRef,
|
|
777
1483
|
has_task: Boolean(taskId || taskRef),
|
|
778
1484
|
current_step_command: taskId || taskRef
|
|
779
|
-
? `atris task current-step --goal-id ${
|
|
1485
|
+
? `atris task current-step --goal-id ${taskScopeRef} --as ${owner} --proof "<proof>" --json`
|
|
780
1486
|
: null,
|
|
781
1487
|
ensure_task_command: taskId || taskRef
|
|
782
1488
|
? null
|
|
@@ -802,6 +1508,7 @@ function missionStatusView(mission) {
|
|
|
802
1508
|
current_task_id: taskSpine.current_task_id,
|
|
803
1509
|
task_ref: taskSpine.task_ref,
|
|
804
1510
|
task_spine: taskSpine,
|
|
1511
|
+
last_landing: missionLastLanding(mission),
|
|
805
1512
|
};
|
|
806
1513
|
}
|
|
807
1514
|
|
|
@@ -814,13 +1521,22 @@ function missionFromArgs(args) {
|
|
|
814
1521
|
'--lane',
|
|
815
1522
|
'--verify',
|
|
816
1523
|
'--stop',
|
|
1524
|
+
'--max-wall',
|
|
1525
|
+
'--minutes',
|
|
1526
|
+
'--hours',
|
|
1527
|
+
'--base',
|
|
817
1528
|
'--task',
|
|
818
1529
|
'--ask',
|
|
819
1530
|
'--model',
|
|
820
|
-
|
|
1531
|
+
'--native-goal-status',
|
|
1532
|
+
'--native-goal-objective',
|
|
1533
|
+
'--visible-goal-status',
|
|
1534
|
+
'--visible-goal-objective',
|
|
1535
|
+
], ['--json', '--always-on', '--xp-task', '--agent-xp', '--worktree', '--spend-full-budget', '--use-whole-budget', '--stop-when-done', '--allow-native-goal-supersede', '--supersede-paused-native-goal']).join(' ').trim();
|
|
821
1536
|
if (!objective) {
|
|
822
1537
|
exitMissionError('Usage: atris mission start "<objective>" --owner <member> [--verify "..."] [--cadence manual] [--worktree]', 1, wantsJson(args));
|
|
823
1538
|
}
|
|
1539
|
+
const budgetContract = inferRunObjectiveBudgetContract(objective, args);
|
|
824
1540
|
const requestedOwner = readFlag(args, '--owner', process.env.ATRIS_AGENT_ID || 'mission-lead');
|
|
825
1541
|
const cadence = readFlag(args, '--cadence', readFlag(args, '--loop', 'manual')) || 'manual';
|
|
826
1542
|
const runner = readFlag(args, '--runner', 'manual');
|
|
@@ -837,7 +1553,7 @@ function missionFromArgs(args) {
|
|
|
837
1553
|
const owner = ownerResolution.owner;
|
|
838
1554
|
const verifier = readFlag(args, '--verify', '');
|
|
839
1555
|
assertMissionVerifier(verifier, wantsJson(args));
|
|
840
|
-
const stopCondition = readFlag(args, '--stop', verifier ? 'verifier passes and no human asks remain' : 'human marks complete with proof');
|
|
1556
|
+
const stopCondition = readFlag(args, '--stop', budgetStopCondition(budgetContract) || (verifier ? 'verifier passes and no human asks remain' : 'human marks complete with proof'));
|
|
841
1557
|
const taskIds = readRepeatedFlag(args, '--task');
|
|
842
1558
|
const humanAsks = readRepeatedFlag(args, '--ask');
|
|
843
1559
|
const alwaysOn = hasFlag(args, '--always-on');
|
|
@@ -872,12 +1588,16 @@ function missionFromArgs(args) {
|
|
|
872
1588
|
created_at: stampIso(),
|
|
873
1589
|
updated_at: stampIso(),
|
|
874
1590
|
};
|
|
1591
|
+
if (budgetContract) {
|
|
1592
|
+
mission.budget_contract = budgetContract;
|
|
1593
|
+
if (budgetContract.requested_seconds) mission.max_wall_seconds = budgetContract.requested_seconds;
|
|
1594
|
+
}
|
|
875
1595
|
if (alwaysOn) mission.next_action = nextCandidateTickAction(mission);
|
|
876
1596
|
return mission;
|
|
877
1597
|
}
|
|
878
1598
|
|
|
879
1599
|
function missingVerifierWarning(mission) {
|
|
880
|
-
if (
|
|
1600
|
+
if (effectiveMissionVerifier(mission)) return null;
|
|
881
1601
|
return {
|
|
882
1602
|
code: 'missing_verifier',
|
|
883
1603
|
message: 'Mission has no verifier; it cannot complete automatically and future runs will report unverified worktree side effects.',
|
|
@@ -899,6 +1619,9 @@ function missionRunSmokeVerifier() {
|
|
|
899
1619
|
"if(p.action!=='mission_run_started')process.exit(2)",
|
|
900
1620
|
"if(p.mission?.runner!=='codex_goal')process.exit(3)",
|
|
901
1621
|
"if(p.codex_goal_state?.goal?.visible_goal?.schema!=='atris.visible_chat_goal_bridge.v1')process.exit(4)",
|
|
1622
|
+
"if(p.requires_native_goal_start!==true)process.exit(5)",
|
|
1623
|
+
"if(p.native_goal_action?.tool!=='create_goal')process.exit(6)",
|
|
1624
|
+
"if(!/create_goal/.test(p.next_command||''))process.exit(7)",
|
|
902
1625
|
].join(';');
|
|
903
1626
|
return `node -e ${JSON.stringify(script)}`;
|
|
904
1627
|
}
|
|
@@ -914,57 +1637,844 @@ function inferRunObjectiveVerifier(objective, root = process.cwd()) {
|
|
|
914
1637
|
return missionRunSmokeVerifier();
|
|
915
1638
|
}
|
|
916
1639
|
|
|
917
|
-
function
|
|
1640
|
+
function durationSecondsFromText(text) {
|
|
1641
|
+
const match = String(text || '').match(/\b(\d+(?:\.\d+)?)[-\s]*(s|sec|secs|second|seconds|m|min|mins|minute|minutes|h|hr|hrs|hour|hours|d|day|days)\b/i);
|
|
1642
|
+
if (!match) return null;
|
|
1643
|
+
const value = Number(match[1]);
|
|
1644
|
+
if (!Number.isFinite(value) || value <= 0) return null;
|
|
1645
|
+
const unit = match[2].toLowerCase();
|
|
1646
|
+
if (/^d/.test(unit)) return Math.round(value * 86400);
|
|
1647
|
+
if (/^h/.test(unit)) return Math.round(value * 3600);
|
|
1648
|
+
if (/^m(?!s)/.test(unit)) return Math.round(value * 60);
|
|
1649
|
+
return Math.round(value);
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
function durationLabel(seconds, fallback = 'the requested time') {
|
|
1653
|
+
const value = Number(seconds);
|
|
1654
|
+
if (!Number.isFinite(value) || value <= 0) return fallback;
|
|
1655
|
+
if (value % 86400 === 0) {
|
|
1656
|
+
const days = value / 86400;
|
|
1657
|
+
return `${days} day${days === 1 ? '' : 's'}`;
|
|
1658
|
+
}
|
|
1659
|
+
if (value % 3600 === 0) {
|
|
1660
|
+
const hours = value / 3600;
|
|
1661
|
+
return `${hours} hour${hours === 1 ? '' : 's'}`;
|
|
1662
|
+
}
|
|
1663
|
+
if (value % 60 === 0) {
|
|
1664
|
+
const minutes = value / 60;
|
|
1665
|
+
return `${minutes} minute${minutes === 1 ? '' : 's'}`;
|
|
1666
|
+
}
|
|
1667
|
+
return `${value} second${value === 1 ? '' : 's'}`;
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
function wantsFullBudget(text, args = []) {
|
|
1671
|
+
if (hasFlag(args, '--spend-full-budget') || hasFlag(args, '--use-whole-budget')) return true;
|
|
1672
|
+
if (hasFlag(args, '--hours') || hasFlag(args, '--minutes')) return true;
|
|
1673
|
+
const compact = String(text || '').toLowerCase().replace(/\s+/g, ' ');
|
|
1674
|
+
const duration = '\\d+(?:\\.\\d+)?[-\\s]*(?:s|sec|secs|second|seconds|m|min|mins|minute|minutes|h|hr|hrs|hour|hours|d|day|days)';
|
|
1675
|
+
return /\b(use|spend)\s+(the\s+)?(whole|full|entire)\s+(time|budget|window)\b/.test(compact)
|
|
1676
|
+
|| /\b(use|spend)\s+(the\s+)?(whole|full|entire)\s+\d/.test(compact)
|
|
1677
|
+
|| new RegExp(`\\b(?:spend|use)\\s+${duration}\\b`).test(compact)
|
|
1678
|
+
|| new RegExp(`(?:^|\\b)(?:run|work|think|research|investigate|brainstorm|plan|map|audit|explore|study|analyze|build)\\s+(?:[^.!?;]{0,80}\\s+)?for\\s+${duration}\\b`).test(compact)
|
|
1679
|
+
|| new RegExp(`^\\s*for\\s+${duration}\\b`).test(compact)
|
|
1680
|
+
|| /\bfor\s+the\s+(whole|full|entire)\s+/.test(compact)
|
|
1681
|
+
|| /\bkeep\s+going\s+until\s+(time|the\s+time|budget|the\s+budget)\s+(is\s+)?(up|done|spent|used)\b/.test(compact)
|
|
1682
|
+
|| /\b(run|work|stop|continue)\s+(until|till)\s+(the\s+)?(time|budget)\s+(is\s+)?(up|done|spent|used)\b/.test(compact);
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
function sleepLengthBudgetIntent(requestedSeconds, text = '') {
|
|
1686
|
+
const seconds = Number(requestedSeconds);
|
|
1687
|
+
return /\b(overnight|while\s+i\s+(?:sleep|am\s+sleeping|['’]?m\s+sleeping)|sleep(?:ing)?\s+run)\b/i.test(text)
|
|
1688
|
+
|| (Number.isFinite(seconds) && seconds >= SLEEP_LENGTH_BUDGET_SECONDS);
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
function inferRunObjectiveBudgetContract(objective, args = []) {
|
|
1692
|
+
const text = `${objective || ''} ${Array.isArray(args) ? args.join(' ') : ''}`;
|
|
1693
|
+
const explicitHours = Number(readFlag(args, '--hours', ''));
|
|
1694
|
+
const explicitMinutes = Number(readFlag(args, '--minutes', ''));
|
|
1695
|
+
const explicitMaxWall = Number(readFlag(args, '--max-wall', ''));
|
|
1696
|
+
const explicitSeconds = Number.isFinite(explicitHours) && explicitHours > 0
|
|
1697
|
+
? Math.round(explicitHours * 3600)
|
|
1698
|
+
: (Number.isFinite(explicitMinutes) && explicitMinutes > 0
|
|
1699
|
+
? Math.round(explicitMinutes * 60)
|
|
1700
|
+
: (Number.isFinite(explicitMaxWall) && explicitMaxWall > 0 ? Math.round(explicitMaxWall) : null));
|
|
1701
|
+
const requestedSeconds = explicitSeconds || durationSecondsFromText(text);
|
|
1702
|
+
const overnight = /\bovernight\b/i.test(text);
|
|
1703
|
+
if (!requestedSeconds && !overnight) return null;
|
|
1704
|
+
const spendBudget = !hasFlag(args, '--stop-when-done')
|
|
1705
|
+
&& (wantsFullBudget(text, args) || sleepLengthBudgetIntent(requestedSeconds, text));
|
|
1706
|
+
const policy = spendBudget
|
|
1707
|
+
? 'spend_full_budget'
|
|
1708
|
+
: 'stop_when_done';
|
|
1709
|
+
const budgetLabel = requestedSeconds
|
|
1710
|
+
? durationLabel(requestedSeconds)
|
|
1711
|
+
: 'the overnight window';
|
|
1712
|
+
const plainLanguage = policy === 'spend_full_budget'
|
|
1713
|
+
? 'Use the whole time.'
|
|
1714
|
+
: 'Finish early if solved.';
|
|
1715
|
+
const stopRule = policy === 'spend_full_budget'
|
|
1716
|
+
? `Use the whole ${budgetLabel}; keep picking the next useful move until time is up, unless blocked or unsafe.`
|
|
1717
|
+
: `Use up to ${budgetLabel}; stop early when the mission is done, proven, or blocked.`;
|
|
918
1718
|
return {
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
1719
|
+
schema: 'atris.mission_budget_contract.v1',
|
|
1720
|
+
requested_seconds: requestedSeconds,
|
|
1721
|
+
budget_label: budgetLabel,
|
|
1722
|
+
policy,
|
|
1723
|
+
plain_language: plainLanguage,
|
|
1724
|
+
stop_rule: stopRule,
|
|
923
1725
|
};
|
|
924
1726
|
}
|
|
925
1727
|
|
|
926
|
-
function
|
|
927
|
-
|
|
1728
|
+
function budgetStopCondition(contract) {
|
|
1729
|
+
if (!contract) return '';
|
|
1730
|
+
if (contract.policy === 'spend_full_budget') {
|
|
1731
|
+
return `run for ${contract.budget_label}; use the whole time unless blocked or unsafe`;
|
|
1732
|
+
}
|
|
1733
|
+
return `run for ${contract.budget_label}, or stop early when proof is strong enough`;
|
|
928
1734
|
}
|
|
929
1735
|
|
|
930
|
-
function
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
1736
|
+
function inferRunObjectiveLoopOptions(objective, args = []) {
|
|
1737
|
+
const text = `${objective || ''} ${Array.isArray(args) ? args.join(' ') : ''}`;
|
|
1738
|
+
const hoursMatch = text.match(/\b(\d+(?:\.\d+)?)\s*(h|hr|hrs|hour|hours)\b/i);
|
|
1739
|
+
const requestedHours = hoursMatch ? Number(hoursMatch[1]) : null;
|
|
1740
|
+
const wantsLongRun = /\b(overnight|nonstop|forever|goal\s+after\s+goal|self[-\s]?improve)\b/i.test(text)
|
|
1741
|
+
|| (Number.isFinite(requestedHours) && requestedHours > 0);
|
|
1742
|
+
if (!wantsLongRun) return { wantsLongRun: false, requestedHours: null, cadence: '' };
|
|
1743
|
+
return {
|
|
1744
|
+
wantsLongRun: true,
|
|
1745
|
+
requestedHours: Number.isFinite(requestedHours) && requestedHours > 0 ? requestedHours : null,
|
|
1746
|
+
cadence: readFlag(args, '--cadence', '13m'),
|
|
1747
|
+
};
|
|
936
1748
|
}
|
|
937
1749
|
|
|
938
|
-
function
|
|
939
|
-
const
|
|
940
|
-
|
|
941
|
-
.
|
|
942
|
-
mission.id !== excluded
|
|
943
|
-
&& mission.started_from === 'mission_run_continuation'
|
|
944
|
-
&& mission.continuation_policy === 'choose_next_mission'
|
|
945
|
-
&& !TERMINAL_STATUSES.has(mission.status)
|
|
946
|
-
));
|
|
947
|
-
candidates.sort((a, b) => missionSortTime(b) - missionSortTime(a));
|
|
948
|
-
return candidates[0] || null;
|
|
1750
|
+
function missionLongRunIntent(mission) {
|
|
1751
|
+
const text = `${mission?.objective || ''} ${mission?.stop_condition || ''}`;
|
|
1752
|
+
return Boolean(mission?.overnight_loop)
|
|
1753
|
+
|| /\b(overnight|nonstop|forever|goal\s+after\s+goal|self[-\s]?improve)\b/i.test(text);
|
|
949
1754
|
}
|
|
950
1755
|
|
|
951
|
-
function
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1756
|
+
function missionChoosesNextMission(mission) {
|
|
1757
|
+
return mission?.started_from === 'mission_run_continuation'
|
|
1758
|
+
&& mission?.continuation_policy === 'choose_next_mission';
|
|
1759
|
+
}
|
|
955
1760
|
|
|
956
|
-
|
|
957
|
-
const
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
1761
|
+
function isConcreteContinuationTarget(title) {
|
|
1762
|
+
const text = String(title || '').trim();
|
|
1763
|
+
if (!text) return false;
|
|
1764
|
+
if (/^decide and start the next useful mission after:/i.test(text)) return false;
|
|
1765
|
+
if (/<next useful mission>/i.test(text)) return false;
|
|
1766
|
+
if (/^create the next proof-backed self-improvement task$/i.test(text)) return false;
|
|
1767
|
+
return true;
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
function continuationTargetKey(title) {
|
|
1771
|
+
return String(title || '')
|
|
1772
|
+
.replace(/^mission xp:\s*/i, '')
|
|
1773
|
+
.replace(/\s+/g, ' ')
|
|
1774
|
+
.trim()
|
|
1775
|
+
.toLowerCase();
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
function handledContinuationTargetKeys(root, moves) {
|
|
1779
|
+
const keys = new Set();
|
|
1780
|
+
const add = (title) => {
|
|
1781
|
+
const key = continuationTargetKey(title);
|
|
1782
|
+
if (key) keys.add(key);
|
|
1783
|
+
};
|
|
1784
|
+
try {
|
|
1785
|
+
for (const title of moves.readHandledTaskTitles(root)) add(title);
|
|
1786
|
+
} catch {
|
|
1787
|
+
// Best-effort guard; mission state below still catches completed missions.
|
|
1788
|
+
}
|
|
1789
|
+
try {
|
|
1790
|
+
for (const row of listMissions(root)) {
|
|
1791
|
+
add(row.objective);
|
|
1792
|
+
}
|
|
1793
|
+
} catch {
|
|
1794
|
+
// If mission state is unreadable, fall back to the remaining explicit filters.
|
|
1795
|
+
}
|
|
1796
|
+
return keys;
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
function readTaskProjectionForMission(root = process.cwd()) {
|
|
1800
|
+
const file = path.join(root, '.atris', 'state', 'tasks.projection.json');
|
|
1801
|
+
try {
|
|
1802
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
1803
|
+
return Array.isArray(parsed?.tasks) ? parsed.tasks : [];
|
|
1804
|
+
} catch {
|
|
1805
|
+
return [];
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
function taskTags(task) {
|
|
1810
|
+
return [
|
|
1811
|
+
task?.tag,
|
|
1812
|
+
...(Array.isArray(task?.tags) ? task.tags : []),
|
|
1813
|
+
...(Array.isArray(task?.metadata?.tags) ? task.metadata.tags : []),
|
|
1814
|
+
]
|
|
1815
|
+
.filter(Boolean)
|
|
1816
|
+
.map((tag) => String(tag).trim().toLowerCase())
|
|
1817
|
+
.filter(Boolean);
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
function certifiedReviewNextTaskCandidates(root = process.cwd()) {
|
|
1821
|
+
return readTaskProjectionForMission(root)
|
|
1822
|
+
.filter((task) => String(task?.status || '').toLowerCase() === 'review')
|
|
1823
|
+
.filter((task) => task?.review?.agent_certified === true || task?.metadata?.agent_certified === true)
|
|
1824
|
+
.map((task) => ({
|
|
1825
|
+
title: task?.review?.next_task || task?.metadata?.latest_agent_next_task || '',
|
|
1826
|
+
why: `certified review ${task.display_id || task.id || ''} suggested this next task`.trim(),
|
|
1827
|
+
source: 'certified_review_next_task',
|
|
1828
|
+
ref: task.display_id || task.id || null,
|
|
1829
|
+
weight: 95,
|
|
1830
|
+
}))
|
|
1831
|
+
.filter((move) => String(move.title || '').trim());
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
function endgameBacklogCandidates(root = process.cwd()) {
|
|
1835
|
+
return readTaskProjectionForMission(root)
|
|
1836
|
+
.filter((task) => String(task?.status || '').toLowerCase() === 'open')
|
|
1837
|
+
.filter((task) => taskTags(task).includes('endgame'))
|
|
1838
|
+
.map((task) => ({
|
|
1839
|
+
title: String(task.title || '').trim(),
|
|
1840
|
+
why: `open endgame backlog task ${task.display_id || task.id || ''}`.trim(),
|
|
1841
|
+
source: 'endgame_backlog',
|
|
1842
|
+
ref: task.display_id || task.id || null,
|
|
1843
|
+
weight: 85,
|
|
1844
|
+
}))
|
|
1845
|
+
.filter((move) => move.title);
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
function cleanWikiNextIngestTitle(value) {
|
|
1849
|
+
return String(value || '')
|
|
1850
|
+
.replace(/`([^`]+)`/g, '$1')
|
|
1851
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
|
|
1852
|
+
.replace(/^[-*]\s*/, '')
|
|
1853
|
+
.replace(/^next[- ]?ingests?\s*:\s*/i, '')
|
|
1854
|
+
.replace(/^next source\s*:\s*/i, '')
|
|
1855
|
+
.replace(/\s+/g, ' ')
|
|
1856
|
+
.trim()
|
|
1857
|
+
.replace(/[.!?:;,]+$/g, '');
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
function wikiStatusNextIngestCandidates(root = process.cwd()) {
|
|
1861
|
+
const file = path.join(root, 'atris', 'wiki', 'STATUS.md');
|
|
1862
|
+
let text = '';
|
|
1863
|
+
try { text = fs.readFileSync(file, 'utf8'); } catch { return []; }
|
|
1864
|
+
const out = [];
|
|
1865
|
+
let inSection = false;
|
|
1866
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
1867
|
+
const line = rawLine.trim();
|
|
1868
|
+
if (/^#+\s+next[- ]?ingests?\b/i.test(line)) {
|
|
1869
|
+
inSection = true;
|
|
1870
|
+
continue;
|
|
1871
|
+
}
|
|
1872
|
+
if (inSection && /^#+\s+/.test(line)) inSection = false;
|
|
1873
|
+
const direct = line.match(/^[-*]?\s*next[- ]?ingests?\s*:\s*(.+)$/i)
|
|
1874
|
+
|| line.match(/^[-*]?\s*next source\s*:\s*(.+)$/i);
|
|
1875
|
+
if (direct) {
|
|
1876
|
+
const title = cleanWikiNextIngestTitle(direct[1]);
|
|
1877
|
+
if (title) out.push(title);
|
|
1878
|
+
continue;
|
|
1879
|
+
}
|
|
1880
|
+
if (inSection && /^[-*]\s+/.test(line)) {
|
|
1881
|
+
const title = cleanWikiNextIngestTitle(line);
|
|
1882
|
+
if (title) out.push(title);
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
return out.map((title) => ({
|
|
1886
|
+
title: /^ingest\b/i.test(title) ? title : `Ingest ${title}`,
|
|
1887
|
+
why: 'wiki STATUS listed this as a next ingest',
|
|
1888
|
+
source: 'wiki_status_next_ingest',
|
|
1889
|
+
ref: 'atris/wiki/STATUS.md',
|
|
1890
|
+
weight: 70,
|
|
1891
|
+
}));
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
function missionExtraNextCandidates(root = process.cwd()) {
|
|
1895
|
+
return [
|
|
1896
|
+
...certifiedReviewNextTaskCandidates(root),
|
|
1897
|
+
...endgameBacklogCandidates(root),
|
|
1898
|
+
...wikiStatusNextIngestCandidates(root),
|
|
1899
|
+
];
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
function uniqueMissionCandidates(candidates) {
|
|
1903
|
+
const seen = new Set();
|
|
1904
|
+
const out = [];
|
|
1905
|
+
for (const candidate of candidates) {
|
|
1906
|
+
const key = continuationTargetKey(candidate?.title);
|
|
1907
|
+
if (!key || seen.has(key)) continue;
|
|
1908
|
+
seen.add(key);
|
|
1909
|
+
out.push(candidate);
|
|
1910
|
+
}
|
|
1911
|
+
return out;
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
function trustedNextMissionSource(source) {
|
|
1915
|
+
return new Set([
|
|
1916
|
+
'certified_review_next_task',
|
|
1917
|
+
'endgame_backlog',
|
|
1918
|
+
'wiki_status_next_ingest',
|
|
1919
|
+
'mission_report',
|
|
1920
|
+
]).has(String(source || ''));
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
function rejectNextMissionCandidate(move, context) {
|
|
1924
|
+
const title = String(move?.title || '').trim();
|
|
1925
|
+
if (!title) return 'empty title';
|
|
1926
|
+
if (context.moves.isGenericInboxPlaceholder(title)) return 'placeholder';
|
|
1927
|
+
if (!isConcreteContinuationTarget(title)) return 'not a concrete mission';
|
|
1928
|
+
const key = continuationTargetKey(title);
|
|
1929
|
+
if (context.handledTargets.has(key)) return 'already handled';
|
|
1930
|
+
if (context.currentObjective && title === context.currentObjective) return 'same as current objective';
|
|
1931
|
+
if (context.parentObjective && title === context.parentObjective) return 'same as parent objective';
|
|
1932
|
+
if (context.mission?.id && move?.ref === context.mission.id && title === context.currentObjective) return 'same as current mission';
|
|
1933
|
+
const preview = missionValuePreview(move, context.mission, context.root);
|
|
1934
|
+
const score = Number(preview?.score?.total || 0);
|
|
1935
|
+
move.value_preview = preview;
|
|
1936
|
+
if (score <= 0 && !trustedNextMissionSource(move.source)) return 'zero value score';
|
|
1937
|
+
return '';
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
function nearMissPreview(nearMisses) {
|
|
1941
|
+
if (!nearMisses.length) return null;
|
|
1942
|
+
return {
|
|
1943
|
+
schema: 'atris.next_mission_stop_preview.v1',
|
|
1944
|
+
stop_reason: 'no concrete follow-up mission found in Atris state',
|
|
1945
|
+
near_misses: nearMisses.slice(0, 3),
|
|
1946
|
+
feynman: {
|
|
1947
|
+
what: 'Atris found possible next moves, but none passed the mission filter.',
|
|
1948
|
+
why_now: 'Stopping is safer than spending tokens on fake or low-value work.',
|
|
1949
|
+
risk: 'A human may want to promote one near-miss manually.',
|
|
1950
|
+
validation: 'Review the near-miss reasons, then add a concrete next task if one should run.',
|
|
1951
|
+
},
|
|
1952
|
+
};
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
function chooseNextMissionAnalysis(mission, root = process.cwd()) {
|
|
1956
|
+
try {
|
|
1957
|
+
const moves = require('../lib/next-moves');
|
|
1958
|
+
const currentObjective = String(mission?.objective || '').trim();
|
|
1959
|
+
const parentObjective = String(mission?.parent_objective || '').trim();
|
|
1960
|
+
const handledTargets = handledContinuationTargetKeys(root, moves);
|
|
1961
|
+
const latestTarget = moves.latestSuggestedTarget(root);
|
|
1962
|
+
const reportCandidates = latestTarget
|
|
1963
|
+
? [{
|
|
1964
|
+
title: latestTarget,
|
|
1965
|
+
why: 'latest proof timeline suggested this follow-up mission',
|
|
1966
|
+
source: 'mission_report',
|
|
1967
|
+
weight: 75,
|
|
1968
|
+
}]
|
|
1969
|
+
: [];
|
|
1970
|
+
const context = { mission, root, moves, currentObjective, parentObjective, handledTargets };
|
|
1971
|
+
const nearMisses = [];
|
|
1972
|
+
const candidates = uniqueMissionCandidates([
|
|
1973
|
+
...missionExtraNextCandidates(root),
|
|
1974
|
+
...reportCandidates,
|
|
1975
|
+
...moves.nextMoves(root, 8),
|
|
1976
|
+
])
|
|
1977
|
+
.filter((move) => {
|
|
1978
|
+
const reason = rejectNextMissionCandidate(move, context);
|
|
1979
|
+
if (reason) {
|
|
1980
|
+
nearMisses.push({
|
|
1981
|
+
title: String(move?.title || '').trim() || '(empty)',
|
|
1982
|
+
source: move?.source || null,
|
|
1983
|
+
ref: move?.ref || null,
|
|
1984
|
+
reason,
|
|
1985
|
+
});
|
|
1986
|
+
return false;
|
|
1987
|
+
}
|
|
1988
|
+
return true;
|
|
1989
|
+
})
|
|
1990
|
+
.sort((a, b) => {
|
|
1991
|
+
const aScore = Number(a.value_preview?.score?.total || 0);
|
|
1992
|
+
const bScore = Number(b.value_preview?.score?.total || 0);
|
|
1993
|
+
if (aScore !== bScore) return bScore - aScore;
|
|
1994
|
+
return Number(b.weight || 0) - Number(a.weight || 0);
|
|
1995
|
+
});
|
|
1996
|
+
return { target: candidates[0] || null, near_misses: nearMisses.slice(0, 3) };
|
|
1997
|
+
} catch {
|
|
1998
|
+
// Fall through to an explicit stop command; never emit the old placeholder.
|
|
1999
|
+
}
|
|
2000
|
+
return { target: null, near_misses: [] };
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
function chooseNextMissionTarget(mission, root = process.cwd()) {
|
|
2004
|
+
return chooseNextMissionAnalysis(mission, root).target;
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
function normalizeMissionOwner(value) {
|
|
2008
|
+
return String(value || '').trim().toLowerCase();
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
function missionMemberTasteProfile(owner) {
|
|
2012
|
+
const key = normalizeMissionOwner(owner);
|
|
2013
|
+
if (/\b(security|sync-inspector|proof-inspector|validator)\b/.test(key)) {
|
|
2014
|
+
return {
|
|
2015
|
+
id: 'security_guard',
|
|
2016
|
+
name: 'Security / trust',
|
|
2017
|
+
bias: 'Prefer work that prevents unsafe behavior, privacy mistakes, or quiet trust breaks.',
|
|
2018
|
+
};
|
|
2019
|
+
}
|
|
2020
|
+
if (/\b(researcher|architect|improver|auto-improver|problem-solver|objective-generator)\b/.test(key)) {
|
|
2021
|
+
return {
|
|
2022
|
+
id: 'technical_homerun',
|
|
2023
|
+
name: 'Technical homerun',
|
|
2024
|
+
bias: 'Prefer technical leaps, but require a usability or proof gate before spending serious tokens.',
|
|
2025
|
+
};
|
|
2026
|
+
}
|
|
2027
|
+
return {
|
|
2028
|
+
id: 'usability_operator',
|
|
2029
|
+
name: 'Usability / demo',
|
|
2030
|
+
bias: 'Prefer work that makes the product easier, faster, clearer, or more demo-ready.',
|
|
2031
|
+
};
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
function missionValueSignals(title) {
|
|
2035
|
+
const text = String(title || '').toLowerCase();
|
|
2036
|
+
const signals = [];
|
|
2037
|
+
const add = (id, label, pattern) => {
|
|
2038
|
+
if (pattern.test(text)) signals.push({ id, label });
|
|
2039
|
+
};
|
|
2040
|
+
add('usability', 'simplifies use', /\b(simple|simplify|preview|clear|plain|feynman|demo|onboarding|ux|workflow|usable|understand)\b/);
|
|
2041
|
+
add('speed', 'speeds the process', /\b(speed|fast|faster|latency|token|waste|friction|shortcut|automation|auto)\b/);
|
|
2042
|
+
add('users_revenue', 'can help users or revenue', /\b(user|users|customer|revenue|payment|checkout|pricing|conversion|retention|demo)\b/);
|
|
2043
|
+
add('trust', 'protects trust', /\b(security|safe|safety|trust|permission|approval|auth|privacy|gmail|email|connector|leak|isolation)\b/);
|
|
2044
|
+
add('technical', 'technical advancement', /\b(agent|ax|connector|isolation|research|benchmark|model|compiler|runtime|architecture|rl|experiment)\b/);
|
|
2045
|
+
add('freshness', 'keeps workflow current', /\b(up[- ]?to[- ]?date|fresh|sync|stale|current|latest)\b/);
|
|
2046
|
+
return signals;
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
function missionTasteSnippet(value, max = 180) {
|
|
2050
|
+
const text = String(value || '')
|
|
2051
|
+
.replace(/<!--[\s\S]*?-->/g, ' ')
|
|
2052
|
+
.replace(/[`*_#>]+/g, '')
|
|
2053
|
+
.replace(/^[-\d.()[\]\s]+/g, '')
|
|
2054
|
+
.replace(/\s+/g, ' ')
|
|
2055
|
+
.trim();
|
|
2056
|
+
if (!text) return '';
|
|
2057
|
+
return text.length > max ? `${text.slice(0, max - 3).trim()}...` : text;
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
function missionTasteSnippets(text, limit = 4) {
|
|
2061
|
+
const tastePattern = /\b(plain|jargon|feynman|understand|simple|clarify|clarity|proof|receipt|verify|verified|checked|tested|accept|approval|runway|revenue|cash|user|customer|demo|technical|homerun|security|trust|privacy|logs|working memory|recent|speed|token|waste|simplify)\b/i;
|
|
2062
|
+
const snippets = [];
|
|
2063
|
+
for (const line of String(text || '').split(/\r?\n/)) {
|
|
2064
|
+
const snippet = missionTasteSnippet(line);
|
|
2065
|
+
if (!snippet || !tastePattern.test(snippet)) continue;
|
|
2066
|
+
snippets.push(snippet);
|
|
2067
|
+
}
|
|
2068
|
+
return snippets.slice(-limit);
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
function readTextFile(root, relativePath) {
|
|
2072
|
+
const file = path.join(root, ...relativePath.split('/'));
|
|
2073
|
+
try {
|
|
2074
|
+
return { path: relativePath, present: true, text: fs.readFileSync(file, 'utf8') };
|
|
2075
|
+
} catch {
|
|
2076
|
+
return { path: relativePath, present: false, text: '' };
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
function recentMarkdownFiles(dir, limit = 2) {
|
|
2081
|
+
const files = [];
|
|
2082
|
+
try {
|
|
2083
|
+
for (const entry of fs.readdirSync(dir)) {
|
|
2084
|
+
const full = path.join(dir, entry);
|
|
2085
|
+
const stat = fs.statSync(full);
|
|
2086
|
+
if (stat.isDirectory()) {
|
|
2087
|
+
for (const child of recentMarkdownFiles(full, limit * 4)) files.push(child);
|
|
2088
|
+
} else if (/\.md$/i.test(entry)) {
|
|
2089
|
+
files.push(full);
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
} catch {
|
|
2093
|
+
return [];
|
|
2094
|
+
}
|
|
2095
|
+
return files
|
|
2096
|
+
.sort((a, b) => {
|
|
2097
|
+
const aBase = path.basename(a);
|
|
2098
|
+
const bBase = path.basename(b);
|
|
2099
|
+
if (aBase !== bBase) return aBase.localeCompare(bBase);
|
|
2100
|
+
return a.localeCompare(b);
|
|
2101
|
+
})
|
|
2102
|
+
.slice(-limit);
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
function readRecentTasteLogs(root, owner, limit = 3) {
|
|
2106
|
+
const logFiles = [
|
|
2107
|
+
...recentMarkdownFiles(path.join(root, 'atris', 'logs'), 2),
|
|
2108
|
+
...recentMarkdownFiles(path.join(root, 'atris', 'team', owner || '', 'logs'), 2),
|
|
2109
|
+
];
|
|
2110
|
+
const seen = new Set();
|
|
2111
|
+
return logFiles
|
|
2112
|
+
.filter((file) => {
|
|
2113
|
+
const key = path.resolve(file);
|
|
2114
|
+
if (seen.has(key)) return false;
|
|
2115
|
+
seen.add(key);
|
|
2116
|
+
return true;
|
|
2117
|
+
})
|
|
2118
|
+
.slice(-limit)
|
|
2119
|
+
.map((file) => {
|
|
2120
|
+
let text = '';
|
|
2121
|
+
try { text = fs.readFileSync(file, 'utf8'); } catch { text = ''; }
|
|
2122
|
+
return {
|
|
2123
|
+
path: path.relative(root, file),
|
|
2124
|
+
snippets: missionTasteSnippets(text, 3),
|
|
2125
|
+
};
|
|
2126
|
+
})
|
|
2127
|
+
.filter((entry) => entry.snippets.length);
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
function readTasteReviewHistory(root, limit = 4) {
|
|
2131
|
+
const file = path.join(root, '.atris', 'state', 'tasks.projection.json');
|
|
2132
|
+
let projection = null;
|
|
2133
|
+
try { projection = JSON.parse(fs.readFileSync(file, 'utf8')); } catch { projection = null; }
|
|
2134
|
+
const tasks = Array.isArray(projection?.tasks) ? projection.tasks : [];
|
|
2135
|
+
const accepted = [];
|
|
2136
|
+
const revised = [];
|
|
2137
|
+
const sorted = tasks
|
|
2138
|
+
.filter(Boolean)
|
|
2139
|
+
.sort((a, b) => Number(b.updated_at || 0) - Number(a.updated_at || 0));
|
|
2140
|
+
for (const task of sorted) {
|
|
2141
|
+
const title = missionTasteSnippet(task.title, 120);
|
|
2142
|
+
for (const event of Array.isArray(task.events) ? task.events : []) {
|
|
2143
|
+
const type = String(event?.event_type || '');
|
|
2144
|
+
const payload = event?.payload || {};
|
|
2145
|
+
if ((type === 'accepted' || (type === 'reviewed' && Number(payload.reward || 0) > 0)) && accepted.length < limit) {
|
|
2146
|
+
accepted.push(missionTasteSnippet(`${title}: ${payload.proof || payload.note || 'accepted'}`));
|
|
2147
|
+
}
|
|
2148
|
+
if (type === 'revision_requested' && revised.length < limit) {
|
|
2149
|
+
revised.push(missionTasteSnippet(`${title}: ${payload.note || 'revision requested'}`));
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
return { accepted, revised };
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
function missionTasteMemorySignals(text) {
|
|
2157
|
+
const definitions = [
|
|
2158
|
+
['plain_language', 'Keshav asks for plain English', /\b(plain|jargon|feynman|understand|simple|clarify|clarity)\b/i],
|
|
2159
|
+
['proof_gate', 'Keshav wants proof before accept', /\b(proof|receipt|verify|verified|checked|tested|accept|approval)\b/i],
|
|
2160
|
+
['runway_revenue', 'runway pushes user or revenue work', /\b(runway|revenue|cash|user|users|customer|demo|adoption|retention)\b/i],
|
|
2161
|
+
['technical_ambition', 'technical advancement still matters', /\b(technical|homerun|research|runtime|architecture|model|benchmark|agent)\b/i],
|
|
2162
|
+
['trust_boundary', 'trust and approval boundaries matter', /\b(security|safe|safety|trust|privacy|approval|permission|gmail|email|connector)\b/i],
|
|
2163
|
+
['working_memory', 'recent logs are working memory', /\b(logs?|working memory|recent|today|now)\b/i],
|
|
2164
|
+
['speed_token', 'avoid wasted time and tokens', /\b(speed|fast|faster|token|waste|friction|simplify)\b/i],
|
|
2165
|
+
];
|
|
2166
|
+
return definitions
|
|
2167
|
+
.filter(([, , pattern]) => pattern.test(text))
|
|
2168
|
+
.map(([id, label]) => ({ id, label }));
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
function readMissionTasteMemory(root = process.cwd(), owner = '') {
|
|
2172
|
+
const safeOwner = normalizeMissionOwner(owner || process.env.ATRIS_AGENT_ID || 'mission-lead') || 'mission-lead';
|
|
2173
|
+
const thinking = readTextFile(root, 'atris/thinking.md');
|
|
2174
|
+
const memberMission = readTextFile(root, `atris/team/${safeOwner}/MISSION.md`);
|
|
2175
|
+
const recentLogs = readRecentTasteLogs(root, safeOwner);
|
|
2176
|
+
const taskHistory = readTasteReviewHistory(root);
|
|
2177
|
+
const thinkingSnippets = missionTasteSnippets(thinking.text, 5);
|
|
2178
|
+
const memberMissionSnippets = missionTasteSnippets(memberMission.text, 5);
|
|
2179
|
+
const allText = [
|
|
2180
|
+
...thinkingSnippets,
|
|
2181
|
+
...memberMissionSnippets,
|
|
2182
|
+
...recentLogs.flatMap((entry) => entry.snippets),
|
|
2183
|
+
...taskHistory.accepted,
|
|
2184
|
+
...taskHistory.revised,
|
|
2185
|
+
].join('\n');
|
|
2186
|
+
const signals = missionTasteMemorySignals(allText);
|
|
2187
|
+
return {
|
|
2188
|
+
schema: 'atris.mission_taste_memory.v1',
|
|
2189
|
+
owner: safeOwner,
|
|
2190
|
+
sources: {
|
|
2191
|
+
thinking_md: {
|
|
2192
|
+
path: thinking.path,
|
|
2193
|
+
present: thinking.present,
|
|
2194
|
+
snippets: thinkingSnippets,
|
|
2195
|
+
},
|
|
2196
|
+
member_mission: {
|
|
2197
|
+
path: memberMission.path,
|
|
2198
|
+
present: memberMission.present,
|
|
2199
|
+
snippets: memberMissionSnippets,
|
|
2200
|
+
},
|
|
2201
|
+
recent_logs: recentLogs,
|
|
2202
|
+
task_history: taskHistory,
|
|
2203
|
+
},
|
|
2204
|
+
signals,
|
|
2205
|
+
};
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
function missionTasteMemoryBoost(signals, tasteMemory, profile) {
|
|
2209
|
+
const candidateIds = new Set((signals || []).map((signal) => signal.id));
|
|
2210
|
+
const memoryIds = new Set((tasteMemory?.signals || []).map((signal) => signal.id));
|
|
2211
|
+
const hasCandidate = (id) => candidateIds.has(id) ? 1 : 0;
|
|
2212
|
+
const hasMemory = (id) => memoryIds.has(id);
|
|
2213
|
+
let boost = 0;
|
|
2214
|
+
if (hasMemory('plain_language')) boost += hasCandidate('usability') * 2;
|
|
2215
|
+
if (hasMemory('proof_gate')) boost += (hasCandidate('trust') || hasCandidate('usability') || hasCandidate('technical')) ? 1 : 0;
|
|
2216
|
+
if (hasMemory('runway_revenue')) boost += hasCandidate('users_revenue') * 3;
|
|
2217
|
+
if (hasMemory('technical_ambition')) boost += profile.id === 'technical_homerun' ? hasCandidate('technical') * 2 : hasCandidate('technical');
|
|
2218
|
+
if (hasMemory('trust_boundary')) boost += hasCandidate('trust') * 2;
|
|
2219
|
+
if (hasMemory('working_memory')) boost += (hasCandidate('freshness') || hasCandidate('speed')) ? 1 : 0;
|
|
2220
|
+
if (hasMemory('speed_token')) boost += hasCandidate('speed') * 2;
|
|
2221
|
+
return boost;
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
function missionValueScore(signals, profile, tasteMemory = null) {
|
|
2225
|
+
const ids = new Set((signals || []).map((signal) => signal.id));
|
|
2226
|
+
const has = (id) => ids.has(id) ? 1 : 0;
|
|
2227
|
+
const base = has('usability') + has('speed') + has('users_revenue') + has('trust') + has('freshness');
|
|
2228
|
+
let profileBoost = 0;
|
|
2229
|
+
if (profile.id === 'technical_homerun') profileBoost = (has('technical') * 2) + has('trust') + has('speed');
|
|
2230
|
+
else if (profile.id === 'security_guard') profileBoost = (has('trust') * 3) + has('freshness');
|
|
2231
|
+
else profileBoost = (has('usability') * 2) + has('speed') + has('users_revenue');
|
|
2232
|
+
const memoryBoost = missionTasteMemoryBoost(signals, tasteMemory, profile);
|
|
2233
|
+
return {
|
|
2234
|
+
total: base + profileBoost + memoryBoost,
|
|
2235
|
+
signals: Array.from(ids),
|
|
2236
|
+
memory_boost: memoryBoost,
|
|
2237
|
+
};
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
function missionPlainTaskPreview(title) {
|
|
2241
|
+
const text = String(title || '').replace(/\s+/g, ' ').trim();
|
|
2242
|
+
if (!text) return 'Do one concrete useful thing.';
|
|
2243
|
+
if (/ax\b/i.test(text) && /gmail/i.test(text) && /turn isolation/i.test(text)) {
|
|
2244
|
+
return 'Make ax safer with Gmail: keep each chat request separate, and show a clear preview or receipt before Gmail actions.';
|
|
2245
|
+
}
|
|
2246
|
+
const cleaned = text
|
|
2247
|
+
.replace(/^mission xp:\s*/i, '')
|
|
2248
|
+
.replace(/^ship\s+/i, '')
|
|
2249
|
+
.replace(/\bturn isolation\b/ig, 'separate chat requests')
|
|
2250
|
+
.replace(/\breceipt previews?\b/ig, 'clear before/after receipts')
|
|
2251
|
+
.replace(/\bconnector\b/ig, 'connected-app path');
|
|
2252
|
+
return `${cleaned.charAt(0).toUpperCase()}${cleaned.slice(1)}`.replace(/[.!?:;,]+$/g, '') + '.';
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
function missionTasteMemoryReason(tasteMemory, signals) {
|
|
2256
|
+
const candidateIds = new Set((signals || []).map((signal) => signal.id));
|
|
2257
|
+
const memoryIds = new Set((tasteMemory?.signals || []).map((signal) => signal.id));
|
|
2258
|
+
if (memoryIds.has('runway_revenue') && candidateIds.has('users_revenue')) return 'Keshav has been steering toward user/revenue proof because runway is tight.';
|
|
2259
|
+
if (memoryIds.has('plain_language') && candidateIds.has('usability')) return 'Keshav keeps asking for plain-English, understandable work.';
|
|
2260
|
+
if (memoryIds.has('proof_gate') && (candidateIds.has('trust') || candidateIds.has('technical'))) return 'Keshav wants proof and receipts before serious work is accepted.';
|
|
2261
|
+
if (memoryIds.has('working_memory')) return 'Recent logs are being used as working memory for what matters now.';
|
|
2262
|
+
if (memoryIds.has('technical_ambition') && candidateIds.has('technical')) return 'The member memory still values technical advancement when it has proof.';
|
|
2263
|
+
return '';
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
function missionPreviewWhyNow(move, profile, signals, tasteMemory = null) {
|
|
2267
|
+
const ids = new Set((signals || []).map((signal) => signal.id));
|
|
2268
|
+
let base = '';
|
|
2269
|
+
if (profile.id === 'technical_homerun') {
|
|
2270
|
+
base = ids.has('technical')
|
|
2271
|
+
? 'This is a technical bet; run it only because it can make the product stronger without making the user think harder.'
|
|
2272
|
+
: 'This is not a technical homerun on its face, so it needs clear usability proof before it should win.';
|
|
2273
|
+
} else if (profile.id === 'security_guard') {
|
|
2274
|
+
base = ids.has('trust')
|
|
2275
|
+
? 'This matters because trust failures are expensive and should be blocked before users feel them.'
|
|
2276
|
+
: 'This is not mainly security work, so it should only run if the current trust queue is quiet.';
|
|
2277
|
+
} else if (ids.has('users_revenue')) {
|
|
2278
|
+
base = 'This matters now because it can help demos, users, retention, or revenue.';
|
|
2279
|
+
} else if (ids.has('usability') || ids.has('speed')) {
|
|
2280
|
+
base = 'This matters now because it can make the product easier or faster to use.';
|
|
2281
|
+
} else {
|
|
2282
|
+
base = move?.why || 'This needs a clear reason before it should spend serious tokens.';
|
|
2283
|
+
}
|
|
2284
|
+
const memoryReason = missionTasteMemoryReason(tasteMemory, signals);
|
|
2285
|
+
return memoryReason ? `${base} Taste memory says: ${memoryReason}` : base;
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
function missionPreviewRisk(signals) {
|
|
2289
|
+
const ids = new Set((signals || []).map((signal) => signal.id));
|
|
2290
|
+
if (ids.has('technical') && !ids.has('usability')) return 'Risk: it becomes clever infrastructure that does not make the product easier.';
|
|
2291
|
+
if (ids.has('trust')) return 'Risk: changing connected-tool behavior can hide or create approval/privacy mistakes.';
|
|
2292
|
+
return 'Risk: it adds complexity without a visible product win.';
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
function missionPreviewValidation(signals) {
|
|
2296
|
+
const ids = new Set((signals || []).map((signal) => signal.id));
|
|
2297
|
+
if (ids.has('trust')) return 'Validate with a before/after receipt and a test proving unsafe state does not carry across turns.';
|
|
2298
|
+
if (ids.has('users_revenue')) return 'Validate with a customer-visible proof: demo path, signup/payment path, or retention signal.';
|
|
2299
|
+
if (ids.has('technical')) return 'Validate with a small benchmark, regression test, or artifact that proves the technical bet helped.';
|
|
2300
|
+
return 'Validate with a simple before/after check that a human can understand.';
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
function missionValuePreview(move, mission, root = process.cwd()) {
|
|
2304
|
+
const profile = missionMemberTasteProfile(mission?.owner);
|
|
2305
|
+
const signals = missionValueSignals(move?.title);
|
|
2306
|
+
const tasteMemory = readMissionTasteMemory(root, mission?.owner);
|
|
2307
|
+
const score = missionValueScore(signals, profile, tasteMemory);
|
|
2308
|
+
return {
|
|
2309
|
+
schema: 'atris.mission_value_preview.v1',
|
|
2310
|
+
member: mission?.owner || null,
|
|
2311
|
+
profile,
|
|
2312
|
+
candidate: {
|
|
2313
|
+
title: move?.title || '',
|
|
2314
|
+
source: move?.source || null,
|
|
2315
|
+
ref: move?.ref || null,
|
|
2316
|
+
},
|
|
2317
|
+
feynman: {
|
|
2318
|
+
what: missionPlainTaskPreview(move?.title),
|
|
2319
|
+
why_now: missionPreviewWhyNow(move, profile, signals, tasteMemory),
|
|
2320
|
+
risk: missionPreviewRisk(signals),
|
|
2321
|
+
validation: missionPreviewValidation(signals),
|
|
2322
|
+
taste: missionTasteMemoryReason(tasteMemory, signals) || 'No live taste memory matched this candidate yet.',
|
|
2323
|
+
},
|
|
2324
|
+
value_signals: signals,
|
|
2325
|
+
taste_memory: tasteMemory,
|
|
2326
|
+
score,
|
|
2327
|
+
};
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
function resumableActiveMissions(mission, root = process.cwd()) {
|
|
2331
|
+
const selfId = String(mission?.id || '');
|
|
2332
|
+
const parentId = String(mission?.parent_mission_id || '');
|
|
2333
|
+
try {
|
|
2334
|
+
// Only planning missions resume: ready missions wait for human review, and
|
|
2335
|
+
// running missions already have an actor.
|
|
2336
|
+
return listMissions(root).filter((row) => {
|
|
2337
|
+
if (!row || row.status !== 'planning') return false;
|
|
2338
|
+
if (row.id === selfId || row.id === parentId) return false;
|
|
2339
|
+
if (String(row.started_from || '') === 'mission_run_continuation') return false;
|
|
2340
|
+
if (!isConcreteContinuationTarget(row.objective)) return false;
|
|
2341
|
+
return true;
|
|
2342
|
+
});
|
|
2343
|
+
} catch {
|
|
2344
|
+
return [];
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
|
|
2348
|
+
function chooseResumeMissionPlan(mission, nearMisses, root = process.cwd()) {
|
|
2349
|
+
const ranked = resumableActiveMissions(mission, root)
|
|
2350
|
+
.map((row) => ({
|
|
2351
|
+
row,
|
|
2352
|
+
preview: missionValuePreview({ title: row.objective, source: 'resume_active_mission', ref: row.id }, mission, root),
|
|
2353
|
+
}))
|
|
2354
|
+
.sort((a, b) => {
|
|
2355
|
+
const aScore = Number(a.preview?.score?.total || 0);
|
|
2356
|
+
const bScore = Number(b.preview?.score?.total || 0);
|
|
2357
|
+
if (aScore !== bScore) return bScore - aScore;
|
|
2358
|
+
return String(b.row.updated_at || '').localeCompare(String(a.row.updated_at || ''));
|
|
2359
|
+
});
|
|
2360
|
+
const best = ranked[0];
|
|
2361
|
+
if (!best) return null;
|
|
2362
|
+
return {
|
|
2363
|
+
target: {
|
|
2364
|
+
title: best.row.objective,
|
|
2365
|
+
source: 'resume_active_mission',
|
|
2366
|
+
ref: best.row.id,
|
|
2367
|
+
value_preview: best.preview,
|
|
2368
|
+
},
|
|
2369
|
+
command: `atris mission run ${best.row.id} --json`,
|
|
2370
|
+
preview: {
|
|
2371
|
+
schema: 'atris.next_mission_resume_preview.v1',
|
|
2372
|
+
resume_mission_id: best.row.id,
|
|
2373
|
+
resume_objective: best.row.objective,
|
|
2374
|
+
resume_status: best.row.status,
|
|
2375
|
+
resume_owner: best.row.owner || null,
|
|
2376
|
+
near_misses: nearMisses.slice(0, 3),
|
|
2377
|
+
feynman: {
|
|
2378
|
+
what: 'Every new candidate is already tracked, and one active mission is still in planning.',
|
|
2379
|
+
why_now: 'Resuming chosen work beats stopping when real work is already on the board.',
|
|
2380
|
+
risk: 'The resumed mission may deserve a fresher objective; stop it if it no longer matters.',
|
|
2381
|
+
validation: 'Run the resume command, then check the mission landing and receipt.',
|
|
2382
|
+
},
|
|
2383
|
+
},
|
|
2384
|
+
};
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
function chooseNextMissionPlan(mission, root = process.cwd()) {
|
|
2388
|
+
const owner = mission?.owner || process.env.ATRIS_AGENT_ID || 'mission-lead';
|
|
2389
|
+
const analysis = chooseNextMissionAnalysis(mission, root);
|
|
2390
|
+
const target = analysis.target;
|
|
2391
|
+
if (target?.title) {
|
|
2392
|
+
return {
|
|
2393
|
+
target,
|
|
2394
|
+
command: `atris mission run ${shellQuote(target.title)} --owner ${owner}`,
|
|
2395
|
+
preview: target.value_preview || missionValuePreview(target, mission, root),
|
|
2396
|
+
};
|
|
2397
|
+
}
|
|
2398
|
+
const resume = chooseResumeMissionPlan(mission, analysis.near_misses || [], root);
|
|
2399
|
+
if (resume) return resume;
|
|
2400
|
+
const missionId = mission?.id || '<mission-id>';
|
|
2401
|
+
return {
|
|
2402
|
+
target: null,
|
|
2403
|
+
command: `atris mission stop ${missionId} --reason ${shellQuote('no concrete follow-up mission found in Atris state')} --json`,
|
|
2404
|
+
preview: nearMissPreview(analysis.near_misses || []),
|
|
2405
|
+
};
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
function chooseNextMissionCommand(mission, root = process.cwd()) {
|
|
2409
|
+
return chooseNextMissionPlan(mission, root).command;
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
function chooseNextMissionPreview(mission, root = process.cwd()) {
|
|
2413
|
+
return chooseNextMissionPlan(mission, root).preview;
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2416
|
+
function effectiveMissionVerifier(mission) {
|
|
2417
|
+
const explicit = String(mission?.verifier || '').trim();
|
|
2418
|
+
if (explicit) return explicit;
|
|
2419
|
+
if (missionChoosesNextMission(mission)) return '';
|
|
2420
|
+
const runner = String(mission?.runner || '').trim().toLowerCase();
|
|
2421
|
+
if (missionLongRunIntent(mission) && (!runner || runner === 'codex_goal')) {
|
|
2422
|
+
return DEFAULT_LONG_RUN_VERIFIER;
|
|
2423
|
+
}
|
|
2424
|
+
return '';
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
function markMissionRunContinuation(mission) {
|
|
2428
|
+
return {
|
|
2429
|
+
...mission,
|
|
2430
|
+
started_from: 'mission_run_objective',
|
|
2431
|
+
continue_on_complete: true,
|
|
2432
|
+
continuation_policy: 'decide_and_start_next_useful_mission',
|
|
2433
|
+
};
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
function continuationObjective(parent) {
|
|
2437
|
+
return `Decide and start the next useful mission after: ${parent.objective}`;
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
function findActiveContinuationMission(parent, root = process.cwd()) {
|
|
2441
|
+
return listMissions(root).find((mission) => (
|
|
2442
|
+
mission.parent_mission_id === parent.id
|
|
2443
|
+
&& mission.started_from === 'mission_run_continuation'
|
|
2444
|
+
&& !TERMINAL_STATUSES.has(mission.status)
|
|
2445
|
+
)) || null;
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
function findActiveMissionRunContinuation(root = process.cwd(), excludeId = '') {
|
|
2449
|
+
const excluded = String(excludeId || '');
|
|
2450
|
+
const candidates = listMissions(root)
|
|
2451
|
+
.filter((mission) => (
|
|
2452
|
+
mission.id !== excluded
|
|
2453
|
+
&& mission.started_from === 'mission_run_continuation'
|
|
2454
|
+
&& mission.continuation_policy === 'choose_next_mission'
|
|
2455
|
+
&& !TERMINAL_STATUSES.has(mission.status)
|
|
2456
|
+
));
|
|
2457
|
+
candidates.sort((a, b) => missionSortTime(b) - missionSortTime(a));
|
|
2458
|
+
return candidates[0] || null;
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
function completeActiveContinuationForStartedMission(nextMission, root = process.cwd()) {
|
|
2462
|
+
const continuation = findActiveMissionRunContinuation(root, nextMission?.id);
|
|
2463
|
+
if (!continuation || !nextMission) return null;
|
|
2464
|
+
if (continuation.objective === nextMission.objective) return null;
|
|
2465
|
+
|
|
2466
|
+
const proof = `Started next mission ${nextMission.id}: ${nextMission.objective}`;
|
|
2467
|
+
const completionGate = { ok: true, source: 'mission_run_continuation', forced: false };
|
|
2468
|
+
const baseNext = {
|
|
2469
|
+
...continuation,
|
|
2470
|
+
status: 'complete',
|
|
2471
|
+
completed_at: stampIso(),
|
|
2472
|
+
proof,
|
|
2473
|
+
completion_gate: completionGate,
|
|
2474
|
+
continued_by_mission_id: nextMission.id,
|
|
2475
|
+
continued_by_objective: nextMission.objective,
|
|
2476
|
+
next_action: 'mission complete',
|
|
2477
|
+
};
|
|
968
2478
|
const completion = missionCompletionReceipt(baseNext, proof);
|
|
969
2479
|
const { mission: saved } = saveMission({
|
|
970
2480
|
...baseNext,
|
|
@@ -990,26 +2500,47 @@ function completeActiveContinuationForStartedMission(nextMission, root = process
|
|
|
990
2500
|
};
|
|
991
2501
|
}
|
|
992
2502
|
|
|
2503
|
+
function missionCanSeedContinuation(parent) {
|
|
2504
|
+
if (!parent) return false;
|
|
2505
|
+
if (parent.status === 'complete') return true;
|
|
2506
|
+
return GOAL_LOOP_STATUSES.has(String(parent.status || '')) && missionTaskHumanAcceptWaiting(parent);
|
|
2507
|
+
}
|
|
2508
|
+
|
|
993
2509
|
function seedMissionRunContinuation(parent, root = process.cwd(), proof = '') {
|
|
994
|
-
if (!parent
|
|
2510
|
+
if (!missionCanSeedContinuation(parent)) return null;
|
|
995
2511
|
if (parent.continue_on_complete !== true) return null;
|
|
996
|
-
if (parent.continuation_seeded_mission_id)
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
2512
|
+
if (parent.continuation_seeded_mission_id) {
|
|
2513
|
+
const seeded = resolveMission(parent.continuation_seeded_mission_id, root);
|
|
2514
|
+
if (seeded && TERMINAL_STATUSES.has(String(seeded.status || '')) && seeded.continued_by_mission_id) {
|
|
2515
|
+
return {
|
|
2516
|
+
inserted: false,
|
|
2517
|
+
reason: 'already_continued',
|
|
2518
|
+
mission_id: parent.continuation_seeded_mission_id,
|
|
2519
|
+
mission: null,
|
|
2520
|
+
continued_by_mission_id: seeded.continued_by_mission_id,
|
|
2521
|
+
};
|
|
2522
|
+
}
|
|
2523
|
+
return {
|
|
2524
|
+
inserted: false,
|
|
2525
|
+
reason: 'already_seeded',
|
|
2526
|
+
mission_id: parent.continuation_seeded_mission_id,
|
|
2527
|
+
mission: seeded || null,
|
|
2528
|
+
};
|
|
2529
|
+
}
|
|
1001
2530
|
|
|
1002
2531
|
const existing = findActiveContinuationMission(parent, root);
|
|
1003
2532
|
if (existing) return { inserted: false, reason: 'active_continuation_exists', mission: existing };
|
|
1004
2533
|
|
|
1005
2534
|
const owner = parent.owner || process.env.ATRIS_AGENT_ID || 'mission-lead';
|
|
1006
2535
|
const objective = continuationObjective(parent);
|
|
2536
|
+
const parentRunner = String(parent.runner || '').trim().toLowerCase();
|
|
2537
|
+
const continuationRunner = parentRunner === 'atris2' ? 'atris2' : 'codex_goal';
|
|
1007
2538
|
const mission = missionFromArgs([
|
|
1008
2539
|
objective,
|
|
1009
2540
|
'--owner',
|
|
1010
2541
|
owner,
|
|
1011
2542
|
'--runner',
|
|
1012
|
-
|
|
2543
|
+
continuationRunner,
|
|
1013
2544
|
'--lane',
|
|
1014
2545
|
parent.lane || 'workspace',
|
|
1015
2546
|
'--cadence',
|
|
@@ -1025,8 +2556,11 @@ function seedMissionRunContinuation(parent, root = process.cwd(), proof = '') {
|
|
|
1025
2556
|
continue_on_complete: false,
|
|
1026
2557
|
continuation_policy: 'choose_next_mission',
|
|
1027
2558
|
parent_proof: proof || parent.receipt_path || null,
|
|
1028
|
-
next_action:
|
|
2559
|
+
next_action: '',
|
|
1029
2560
|
};
|
|
2561
|
+
const nextPlan = chooseNextMissionPlan(nextMission, root);
|
|
2562
|
+
nextMission.next_action = `decide next mission, then run: ${nextPlan.command}`;
|
|
2563
|
+
nextMission.next_action_preview = nextPlan.preview;
|
|
1030
2564
|
|
|
1031
2565
|
ensureMemberMissionFile(nextMission.owner, root, nextMission.objective);
|
|
1032
2566
|
const { mission: saved } = saveMission(nextMission, root, 'mission_continuation_started', {
|
|
@@ -1057,6 +2591,36 @@ function seedMissionRunContinuation(parent, root = process.cwd(), proof = '') {
|
|
|
1057
2591
|
};
|
|
1058
2592
|
}
|
|
1059
2593
|
|
|
2594
|
+
function seedNextMoveContinuationGoal(root = process.cwd()) {
|
|
2595
|
+
const candidates = listMissions(root)
|
|
2596
|
+
.filter((mission) => runnerUsesCallerSession(mission.runner))
|
|
2597
|
+
.filter((mission) => mission.continue_on_complete === true)
|
|
2598
|
+
.filter((mission) => missionCanSeedContinuation(mission));
|
|
2599
|
+
candidates.sort((a, b) => missionSortTime(b) - missionSortTime(a));
|
|
2600
|
+
for (const parent of candidates) {
|
|
2601
|
+
const seeded = seedMissionRunContinuation(parent, root, parent.receipt_path || 'agent-certified task waiting for human accept');
|
|
2602
|
+
if (seeded?.mission && missionSelectableForCodexGoal(seeded.mission)) return { ...seeded, parent: seeded.parent || parent };
|
|
2603
|
+
}
|
|
2604
|
+
return null;
|
|
2605
|
+
}
|
|
2606
|
+
|
|
2607
|
+
// Continuation runs started from inside an existing agent worktree must build
|
|
2608
|
+
// on that work, not restart clean from origin/master. Returns the current HEAD
|
|
2609
|
+
// sha (a sha survives normalizeTargetRef; an unpushed branch name would be
|
|
2610
|
+
// rewritten to a nonexistent origin/ ref) when cwd is an agent worktree.
|
|
2611
|
+
function inheritedWorktreeBase(cwd) {
|
|
2612
|
+
try {
|
|
2613
|
+
const top = spawnSync('git', ['rev-parse', '--show-toplevel'], { cwd, encoding: 'utf8' });
|
|
2614
|
+
const root = String(top.stdout || '').trim();
|
|
2615
|
+
if (top.status !== 0 || !root) return '';
|
|
2616
|
+
if (!fs.existsSync(path.join(root, '.atris', 'agent-worktree.json'))) return '';
|
|
2617
|
+
const head = spawnSync('git', ['rev-parse', 'HEAD'], { cwd: root, encoding: 'utf8' });
|
|
2618
|
+
return head.status === 0 ? String(head.stdout || '').trim() : '';
|
|
2619
|
+
} catch {
|
|
2620
|
+
return '';
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
|
|
1060
2624
|
function startMission(args) {
|
|
1061
2625
|
const asJson = wantsJson(args);
|
|
1062
2626
|
const mission = missionFromArgs(args);
|
|
@@ -1068,7 +2632,8 @@ function startMission(args) {
|
|
|
1068
2632
|
let created;
|
|
1069
2633
|
try {
|
|
1070
2634
|
const { createAgentWorktree } = require('./worktree');
|
|
1071
|
-
|
|
2635
|
+
const base = readFlag(args, '--base', '') || inheritedWorktreeBase(process.cwd());
|
|
2636
|
+
created = createAgentWorktree({ member: mission.owner, task: mission.objective, ...(base ? { base } : {}) });
|
|
1072
2637
|
} catch (e) {
|
|
1073
2638
|
exitMissionError(`[mission start] worktree creation failed: ${e.message}`, 2, asJson);
|
|
1074
2639
|
}
|
|
@@ -1112,35 +2677,88 @@ function startMission(args) {
|
|
|
1112
2677
|
|
|
1113
2678
|
function startMissionFromRunObjective(objective, args) {
|
|
1114
2679
|
const asJson = wantsJson(args);
|
|
1115
|
-
const
|
|
2680
|
+
const rawObjective = String(objective || '').trim();
|
|
2681
|
+
const inferredLoop = inferRunObjectiveLoopOptions(rawObjective, args);
|
|
2682
|
+
const budgetContract = inferRunObjectiveBudgetContract(rawObjective, args);
|
|
2683
|
+
const inferredOwner = inferredLoop.wantsLongRun && /\bself[-\s]?improve\b/i.test(rawObjective)
|
|
2684
|
+
? 'auto-improver'
|
|
2685
|
+
: (process.env.ATRIS_AGENT_ID || 'mission-lead');
|
|
2686
|
+
const preflightOwner = readFlag(args, '--owner', inferredOwner);
|
|
2687
|
+
const missionRunPreflight = buildMissionRunRoomPreflight(rawObjective, args, {
|
|
2688
|
+
root: process.cwd(),
|
|
2689
|
+
owner: preflightOwner,
|
|
2690
|
+
allowTrustedRun: !inferredLoop.wantsLongRun,
|
|
2691
|
+
});
|
|
2692
|
+
const missionObjective = missionRunPreflight?.shaped_objective || rawObjective;
|
|
2693
|
+
const verifier = readFlag(
|
|
2694
|
+
args,
|
|
2695
|
+
'--verify',
|
|
2696
|
+
inferRunObjectiveVerifier(missionObjective)
|
|
2697
|
+
|| inferRunObjectiveVerifier(rawObjective)
|
|
2698
|
+
|| (missionRunPreflight?.trusted_run ? DEFAULT_LONG_RUN_VERIFIER : '')
|
|
2699
|
+
|| (inferredLoop.wantsLongRun ? DEFAULT_LONG_RUN_VERIFIER : ''),
|
|
2700
|
+
);
|
|
1116
2701
|
const stopCondition = readFlag(
|
|
1117
2702
|
args,
|
|
1118
2703
|
'--stop',
|
|
1119
|
-
|
|
2704
|
+
budgetStopCondition(budgetContract) || (inferredLoop.wantsLongRun
|
|
2705
|
+
? `run for ${inferredLoop.requestedHours || 'the requested overnight window'} hour${inferredLoop.requestedHours === 1 ? '' : 's'}, or stop when proof is ready`
|
|
2706
|
+
: (verifier ? 'verifier passes and visible goal lands' : 'visible goal lands and proof is ready')),
|
|
1120
2707
|
);
|
|
1121
2708
|
const startArgs = [
|
|
1122
|
-
|
|
2709
|
+
missionObjective,
|
|
1123
2710
|
'--owner',
|
|
1124
|
-
readFlag(args, '--owner',
|
|
2711
|
+
readFlag(args, '--owner', inferredOwner),
|
|
1125
2712
|
'--runner',
|
|
1126
2713
|
readFlag(args, '--runner', 'codex_goal'),
|
|
1127
2714
|
'--lane',
|
|
1128
2715
|
readFlag(args, '--lane', 'workspace'),
|
|
1129
2716
|
'--cadence',
|
|
1130
|
-
readFlag(args, '--cadence', 'manual'),
|
|
2717
|
+
readFlag(args, '--cadence', inferredLoop.wantsLongRun ? inferredLoop.cadence : 'manual'),
|
|
1131
2718
|
'--stop',
|
|
1132
2719
|
stopCondition,
|
|
1133
2720
|
];
|
|
1134
2721
|
if (verifier) startArgs.push('--verify', verifier);
|
|
1135
2722
|
const model = readFlag(args, '--model', '');
|
|
1136
2723
|
if (model) startArgs.push('--model', model);
|
|
1137
|
-
if (hasFlag(args, '--always-on')) startArgs.push('--always-on');
|
|
1138
|
-
if (hasFlag(args, '--xp-task') || hasFlag(args, '--agent-xp')) startArgs.push('--xp-task');
|
|
2724
|
+
if (hasFlag(args, '--always-on') || inferredLoop.wantsLongRun) startArgs.push('--always-on');
|
|
2725
|
+
if (hasFlag(args, '--xp-task') || hasFlag(args, '--agent-xp') || missionRunPreflight?.task_spine_required) startArgs.push('--xp-task');
|
|
1139
2726
|
|
|
1140
2727
|
const mission = markMissionRunContinuation(missionFromArgs(startArgs));
|
|
2728
|
+
if (missionGoalChainIntent(rawObjective) || missionGoalChainIntent(missionObjective)) {
|
|
2729
|
+
mission.goal_chain = buildMissionGoalChain(missionObjective || rawObjective);
|
|
2730
|
+
mission.next_action = missionGoalChainNextAction(mission.goal_chain);
|
|
2731
|
+
}
|
|
2732
|
+
if (missionRunPreflight) {
|
|
2733
|
+
mission.mission_run_preflight = missionRunPreflight;
|
|
2734
|
+
mission.raw_objective = rawObjective;
|
|
2735
|
+
}
|
|
2736
|
+
const selectedTargetTask = missionRunSelectedTaskTarget(missionRunPreflight);
|
|
2737
|
+
if (selectedTargetTask) {
|
|
2738
|
+
Object.assign(mission, attachSelectedTargetTaskSpine(mission));
|
|
2739
|
+
}
|
|
2740
|
+
if (budgetContract) {
|
|
2741
|
+
mission.budget_contract = budgetContract;
|
|
2742
|
+
if (budgetContract.requested_seconds) mission.max_wall_seconds = budgetContract.requested_seconds;
|
|
2743
|
+
}
|
|
2744
|
+
if (inferredLoop.wantsLongRun) {
|
|
2745
|
+
mission.overnight_loop = {
|
|
2746
|
+
requested_hours: inferredLoop.requestedHours,
|
|
2747
|
+
cadence: mission.cadence,
|
|
2748
|
+
install_command: inferredLoop.requestedHours
|
|
2749
|
+
? `atris loop start --overnight --cadence ${mission.cadence} --hours ${inferredLoop.requestedHours}`
|
|
2750
|
+
: `atris loop start --overnight --cadence ${mission.cadence}`,
|
|
2751
|
+
};
|
|
2752
|
+
}
|
|
2753
|
+
if (mission.xp_task_enabled && !selectedTargetTask) {
|
|
2754
|
+
const xpTask = createMissionXpTask(mission, process.cwd(), asJson);
|
|
2755
|
+
mission.xp_task = xpTask;
|
|
2756
|
+
mission.task_ids = Array.from(new Set([...(mission.task_ids || []), xpTask.task_id]));
|
|
2757
|
+
}
|
|
1141
2758
|
const warnings = [missingVerifierWarning(mission)].filter(Boolean);
|
|
1142
2759
|
ensureMemberMissionFile(mission.owner, process.cwd(), mission.objective);
|
|
1143
2760
|
const { mission: saved } = saveMission(mission, process.cwd(), 'mission_started', { objective: mission.objective, source: 'mission_run_objective' });
|
|
2761
|
+
const directGoalRequest = writeDirectRunCodexGoalRequest(saved, process.cwd());
|
|
1144
2762
|
const memberState = renderMemberMissionState(saved.owner);
|
|
1145
2763
|
const logPath = appendMemberLog(saved.owner, 'Mission started from run', {
|
|
1146
2764
|
mission: saved.objective,
|
|
@@ -1151,12 +2769,26 @@ function startMissionFromRunObjective(objective, args) {
|
|
|
1151
2769
|
});
|
|
1152
2770
|
const worktreeBaseline = captureMissionWorktreeBaseline(saved, process.cwd());
|
|
1153
2771
|
const completedContinuationGoal = completeActiveContinuationForStartedMission(saved, process.cwd());
|
|
1154
|
-
const
|
|
2772
|
+
const nativeGoalStatus = readFlag(args, '--native-goal-status', readFlag(args, '--visible-goal-status', ''));
|
|
2773
|
+
const nativeGoalObjective = readFlag(args, '--native-goal-objective', readFlag(args, '--visible-goal-objective', ''));
|
|
2774
|
+
const nativeGoalOptions = {
|
|
2775
|
+
...(nativeGoalStatus ? { nativeGoalStatus } : {}),
|
|
2776
|
+
...(nativeGoalObjective ? { nativeGoalObjective } : {}),
|
|
2777
|
+
...(hasFlag(args, '--allow-native-goal-supersede') || hasFlag(args, '--supersede-paused-native-goal') ? { allowNativeGoalSupersede: true } : {}),
|
|
2778
|
+
};
|
|
2779
|
+
const atrisGoalState = refreshAtrisGoalController(process.cwd(), { missionId: saved.id });
|
|
2780
|
+
const codexGoalState = runnerUsesCallerSession(saved.runner)
|
|
2781
|
+
? refreshCodexGoalController(process.cwd(), { missionId: saved.id, ...nativeGoalOptions })
|
|
2782
|
+
: null;
|
|
2783
|
+
const nativeGoal = codexGoalState?.native_goal_action
|
|
2784
|
+
|| (codexGoalState?.goal?.requires_native_goal_start ? codexGoalState.goal.native_goal_action : null);
|
|
2785
|
+
const nextCommand = codexGoalState?.next_command || codexGoalState?.goal?.next_command || atrisGoalState.goal?.next_command || `atris mission tick ${saved.id} --summary "<what changed>"`;
|
|
1155
2786
|
printJsonOrText(
|
|
1156
2787
|
{
|
|
1157
2788
|
ok: true,
|
|
1158
2789
|
action: 'mission_run_started',
|
|
1159
2790
|
mission: saved,
|
|
2791
|
+
budget_contract: saved.budget_contract || null,
|
|
1160
2792
|
warnings,
|
|
1161
2793
|
state_path: statePaths().missionsJsonl,
|
|
1162
2794
|
member_state: memberState,
|
|
@@ -1167,16 +2799,16 @@ function startMissionFromRunObjective(objective, args) {
|
|
|
1167
2799
|
dirty_hash: worktreeBaseline.dirty_hash,
|
|
1168
2800
|
} : null,
|
|
1169
2801
|
completed_continuation_goal: completedContinuationGoal,
|
|
2802
|
+
direct_goal_request: directGoalRequest,
|
|
2803
|
+
atris_goal_state: atrisGoalState,
|
|
1170
2804
|
codex_goal_state: codexGoalState,
|
|
1171
|
-
|
|
2805
|
+
requires_native_goal_start: codexGoalState?.requires_native_goal_start === true || codexGoalState?.goal?.requires_native_goal_start === true,
|
|
2806
|
+
requires_native_goal_replace: codexGoalState?.requires_native_goal_replace === true || codexGoalState?.goal?.requires_native_goal_replace === true,
|
|
2807
|
+
native_goal_action: nativeGoal,
|
|
2808
|
+
native_goal_ack_command: codexGoalState?.goal?.native_goal_ack_command || codexGoalState?.active_goal_conflict?.commands?.ack_new_mission || null,
|
|
2809
|
+
next_command: nextCommand,
|
|
1172
2810
|
},
|
|
1173
|
-
|
|
1174
|
-
`Started mission: ${saved.objective}`,
|
|
1175
|
-
`Owner: ${saved.owner}`,
|
|
1176
|
-
`Runner: ${saved.runner}`,
|
|
1177
|
-
...(codexGoalState.goal ? [`Visible goal: ${codexGoalState.goal.objective}`] : []),
|
|
1178
|
-
...warnings.map((warning) => `Warning: ${warning.message}`),
|
|
1179
|
-
],
|
|
2811
|
+
missionRunTakeoffLines(saved, { warnings, nextCommand }),
|
|
1180
2812
|
asJson,
|
|
1181
2813
|
);
|
|
1182
2814
|
}
|
|
@@ -1187,60 +2819,128 @@ function attachMissionTask(args) {
|
|
|
1187
2819
|
if (!ref) {
|
|
1188
2820
|
exitMissionError('Usage: atris mission attach-task <id> [--json]', 1, asJson);
|
|
1189
2821
|
}
|
|
1190
|
-
|
|
2822
|
+
let mission = resolveMission(ref);
|
|
1191
2823
|
if (!mission) {
|
|
1192
2824
|
exitMissionError(`Mission "${ref}" not found.`, 1, asJson);
|
|
1193
2825
|
}
|
|
1194
|
-
|
|
1195
|
-
|
|
2826
|
+
|
|
2827
|
+
const lock = acquireMissionLock(mission.id, process.cwd(), { waitMs: 2000 });
|
|
2828
|
+
if (!lock.ok) {
|
|
2829
|
+
exitMissionError(`[mission attach-task] lock busy (held by pid ${lock.holder?.pid || '?'} since ${lock.holder?.started_at || '?'}). Exit.`, 3, asJson);
|
|
1196
2830
|
}
|
|
1197
2831
|
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
2832
|
+
try {
|
|
2833
|
+
mission = resolveMission(mission.id) || mission;
|
|
2834
|
+
if (TERMINAL_STATUSES.has(mission.status)) {
|
|
2835
|
+
releaseMissionLock(lock);
|
|
2836
|
+
exitMissionError(`Mission "${ref}" is ${mission.status}; task spines attach only to active missions.`, 2, asJson);
|
|
2837
|
+
}
|
|
2838
|
+
|
|
2839
|
+
const existingSpine = missionTaskSpine(mission);
|
|
2840
|
+
if (existingSpine?.has_task) {
|
|
2841
|
+
let currentMission = mission;
|
|
2842
|
+
if (missionChoosesNextMission(currentMission)) {
|
|
2843
|
+
const nextPlan = chooseNextMissionPlan(currentMission);
|
|
2844
|
+
const nextAction = `decide next mission, then run: ${nextPlan.command}`;
|
|
2845
|
+
const currentPreviewTitle = currentMission.next_action_preview?.candidate?.title || null;
|
|
2846
|
+
const nextPreviewTitle = nextPlan.preview?.candidate?.title || null;
|
|
2847
|
+
const previewChanged = Boolean(currentMission.next_action_preview) !== Boolean(nextPlan.preview)
|
|
2848
|
+
|| currentPreviewTitle !== nextPreviewTitle;
|
|
2849
|
+
if (currentMission.next_action !== nextAction || previewChanged) {
|
|
2850
|
+
currentMission = saveMission(
|
|
2851
|
+
{
|
|
2852
|
+
...currentMission,
|
|
2853
|
+
next_action: nextAction,
|
|
2854
|
+
next_action_preview: nextPlan.preview,
|
|
2855
|
+
},
|
|
2856
|
+
process.cwd(),
|
|
2857
|
+
'mission_next_action_refreshed',
|
|
2858
|
+
{
|
|
2859
|
+
next_command: nextPlan.command,
|
|
2860
|
+
target: nextPreviewTitle,
|
|
2861
|
+
},
|
|
2862
|
+
).mission;
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
const view = missionStatusView(currentMission);
|
|
2866
|
+
printJsonOrText(
|
|
2867
|
+
{ ok: true, action: 'mission_task_spine_exists', mission: view, task_spine: view.task_spine },
|
|
2868
|
+
[
|
|
2869
|
+
`Mission task spine already exists: ${mission.objective}`,
|
|
2870
|
+
`Task: ${view.task_spine.task_ref}`,
|
|
2871
|
+
`Next: ${view.task_spine.current_step_command}`,
|
|
2872
|
+
],
|
|
2873
|
+
asJson,
|
|
2874
|
+
);
|
|
2875
|
+
return;
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2878
|
+
const ownership = applyMissionOwnerResolution(mission, process.cwd());
|
|
2879
|
+
const baseMission = ownership.mission;
|
|
2880
|
+
const selectedTargetMission = attachSelectedTargetTaskSpine(baseMission);
|
|
2881
|
+
if (missionTaskSpine(selectedTargetMission)?.has_task) {
|
|
2882
|
+
const { mission: saved } = saveMission(
|
|
2883
|
+
selectedTargetMission,
|
|
2884
|
+
process.cwd(),
|
|
2885
|
+
'mission_selected_task_spine_attached',
|
|
2886
|
+
{
|
|
2887
|
+
task_id: selectedTargetMission.current_task_id || selectedTargetMission.task_ids?.[0] || null,
|
|
2888
|
+
task_ref: selectedTargetMission.task_ref || null,
|
|
2889
|
+
},
|
|
2890
|
+
);
|
|
2891
|
+
const memberState = renderMemberMissionState(saved.owner);
|
|
2892
|
+
const logPath = appendMemberLog(saved.owner, 'Mission selected task spine attached', {
|
|
2893
|
+
mission: saved.objective,
|
|
2894
|
+
task: saved.task_ref || saved.current_task_id,
|
|
2895
|
+
});
|
|
2896
|
+
const view = missionStatusView(saved);
|
|
2897
|
+
printJsonOrText(
|
|
2898
|
+
{ ok: true, action: 'mission_selected_task_spine_attached', mission: view, task_spine: view.task_spine, member_state: memberState, log_path: logPath },
|
|
2899
|
+
[
|
|
2900
|
+
`Attached selected task spine: ${saved.objective}`,
|
|
2901
|
+
`Task: ${view.task_spine.task_ref}`,
|
|
2902
|
+
`Next: ${view.task_spine.current_step_command}`,
|
|
2903
|
+
],
|
|
2904
|
+
asJson,
|
|
2905
|
+
);
|
|
2906
|
+
return;
|
|
2907
|
+
}
|
|
2908
|
+
const xpTask = createMissionXpTask(baseMission, process.cwd(), asJson);
|
|
2909
|
+
const nextMission = {
|
|
2910
|
+
...baseMission,
|
|
2911
|
+
xp_task_enabled: true,
|
|
2912
|
+
xp_task: xpTask,
|
|
2913
|
+
task_ids: Array.from(new Set([...(baseMission.task_ids || []), xpTask.task_id])),
|
|
2914
|
+
};
|
|
2915
|
+
if (nextMission.status === 'ready' && nextMission.receipt_path) {
|
|
2916
|
+
nextMission.next_action = missionXpReadyAction(nextMission, nextMission.receipt_path) || nextMission.next_action;
|
|
2917
|
+
} else if (missionChoosesNextMission(nextMission)) {
|
|
2918
|
+
const nextPlan = chooseNextMissionPlan(nextMission);
|
|
2919
|
+
nextMission.next_action = `decide next mission, then run: ${nextPlan.command}`;
|
|
2920
|
+
nextMission.next_action_preview = nextPlan.preview;
|
|
2921
|
+
} else if (!nextMission.verifier && !nextMission.always_on) {
|
|
2922
|
+
nextMission.next_action = `work task then run: atris task current-step --goal-id ${nextMission.id} --as ${nextMission.owner} --proof "<proof>" --json`;
|
|
2923
|
+
}
|
|
2924
|
+
|
|
2925
|
+
const { mission: saved } = saveMission(nextMission, process.cwd(), 'mission_task_spine_attached', { task_id: xpTask.task_id, task_ref: xpTask.ref });
|
|
2926
|
+
const memberState = renderMemberMissionState(saved.owner);
|
|
2927
|
+
const logPath = appendMemberLog(saved.owner, 'Mission task spine attached', {
|
|
2928
|
+
mission: saved.objective,
|
|
2929
|
+
task: xpTask.ref,
|
|
2930
|
+
});
|
|
2931
|
+
const view = missionStatusView(saved);
|
|
1201
2932
|
printJsonOrText(
|
|
1202
|
-
{ ok: true, action: '
|
|
2933
|
+
{ ok: true, action: 'mission_task_spine_attached', mission: view, task: xpTask, task_spine: view.task_spine, member_state: memberState, log_path: logPath },
|
|
1203
2934
|
[
|
|
1204
|
-
`
|
|
1205
|
-
`Task: ${
|
|
2935
|
+
`Attached task spine: ${saved.objective}`,
|
|
2936
|
+
`Task: ${xpTask.ref}`,
|
|
1206
2937
|
`Next: ${view.task_spine.current_step_command}`,
|
|
1207
2938
|
],
|
|
1208
2939
|
asJson,
|
|
1209
2940
|
);
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
const ownership = applyMissionOwnerResolution(mission, process.cwd());
|
|
1214
|
-
const baseMission = ownership.mission;
|
|
1215
|
-
const xpTask = createMissionXpTask(baseMission, process.cwd(), asJson);
|
|
1216
|
-
const nextMission = {
|
|
1217
|
-
...baseMission,
|
|
1218
|
-
xp_task_enabled: true,
|
|
1219
|
-
xp_task: xpTask,
|
|
1220
|
-
task_ids: Array.from(new Set([...(baseMission.task_ids || []), xpTask.task_id])),
|
|
1221
|
-
};
|
|
1222
|
-
if (nextMission.status === 'ready' && nextMission.receipt_path) {
|
|
1223
|
-
nextMission.next_action = missionXpReadyAction(nextMission, nextMission.receipt_path) || nextMission.next_action;
|
|
1224
|
-
} else if (!nextMission.verifier && !nextMission.always_on) {
|
|
1225
|
-
nextMission.next_action = `work task then run: atris task current-step --goal-id ${nextMission.id} --as ${nextMission.owner} --proof "<proof>" --json`;
|
|
2941
|
+
} finally {
|
|
2942
|
+
releaseMissionLock(lock);
|
|
1226
2943
|
}
|
|
1227
|
-
|
|
1228
|
-
const { mission: saved } = saveMission(nextMission, process.cwd(), 'mission_task_spine_attached', { task_id: xpTask.task_id, task_ref: xpTask.ref });
|
|
1229
|
-
const memberState = renderMemberMissionState(saved.owner);
|
|
1230
|
-
const logPath = appendMemberLog(saved.owner, 'Mission task spine attached', {
|
|
1231
|
-
mission: saved.objective,
|
|
1232
|
-
task: xpTask.ref,
|
|
1233
|
-
});
|
|
1234
|
-
const view = missionStatusView(saved);
|
|
1235
|
-
printJsonOrText(
|
|
1236
|
-
{ ok: true, action: 'mission_task_spine_attached', mission: view, task: xpTask, task_spine: view.task_spine, member_state: memberState, log_path: logPath },
|
|
1237
|
-
[
|
|
1238
|
-
`Attached task spine: ${saved.objective}`,
|
|
1239
|
-
`Task: ${xpTask.ref}`,
|
|
1240
|
-
`Next: ${view.task_spine.current_step_command}`,
|
|
1241
|
-
],
|
|
1242
|
-
asJson,
|
|
1243
|
-
);
|
|
1244
2944
|
}
|
|
1245
2945
|
|
|
1246
2946
|
function statusMission(args) {
|
|
@@ -1288,14 +2988,17 @@ function statusMission(args) {
|
|
|
1288
2988
|
` id: ${mission.id}`,
|
|
1289
2989
|
` owner: ${mission.owner}`,
|
|
1290
2990
|
...(mission.executed_by ? [` executed_by: ${mission.executed_by}`] : []),
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
2991
|
+
` state: ${mission.status}`,
|
|
2992
|
+
...missionHeartbeatLines(mission),
|
|
2993
|
+
...(mission.worktree_root ? [` worktree: ${mission.worktree_root}`] : []),
|
|
2994
|
+
` next: ${mission.next_action || 'tick or verify'}`,
|
|
2995
|
+
...(mission.next_action_preview?.feynman?.what ? [` preview: ${mission.next_action_preview.feynman.what}`] : []),
|
|
2996
|
+
...missionGoalChainLines(mission),
|
|
2997
|
+
...(mission.task_spine?.task_ref ? [` task: ${mission.task_spine.task_ref}`] : []),
|
|
1296
2998
|
...(mission.task_spine?.current_step_command ? [` task next: ${mission.task_spine.current_step_command}`] : []),
|
|
1297
2999
|
...(!mission.task_spine?.has_task && mission.task_spine?.ensure_task_command ? [` task setup: ${mission.task_spine.ensure_task_command}`] : []),
|
|
1298
3000
|
...(mission.receipt_path ? [` proof: ${mission.receipt_path}`] : []),
|
|
3001
|
+
...missionStatusLandingLines(mission.last_landing),
|
|
1299
3002
|
...(completionGateLabel(mission.completion_gate) ? [` gate: ${completionGateLabel(mission.completion_gate)}`] : []),
|
|
1300
3003
|
])
|
|
1301
3004
|
: ['No missions yet. Run: atris mission start "..." --owner <member>'],
|
|
@@ -1303,48 +3006,381 @@ function statusMission(args) {
|
|
|
1303
3006
|
);
|
|
1304
3007
|
}
|
|
1305
3008
|
|
|
1306
|
-
function readMissionReceipt(receiptPath, root = process.cwd()) {
|
|
1307
|
-
if (!receiptPath) return null;
|
|
1308
|
-
const file = path.isAbsolute(receiptPath) ? receiptPath : path.join(root, receiptPath);
|
|
1309
|
-
try {
|
|
1310
|
-
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
1311
|
-
} catch {
|
|
1312
|
-
return null;
|
|
1313
|
-
}
|
|
3009
|
+
function readMissionReceipt(receiptPath, root = process.cwd()) {
|
|
3010
|
+
if (!receiptPath) return null;
|
|
3011
|
+
const file = path.isAbsolute(receiptPath) ? receiptPath : path.join(root, receiptPath);
|
|
3012
|
+
try {
|
|
3013
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
3014
|
+
} catch {
|
|
3015
|
+
return null;
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
|
|
3019
|
+
function firstUsefulLine(text, fallback = '') {
|
|
3020
|
+
return String(text || '')
|
|
3021
|
+
.split(/\r?\n/)
|
|
3022
|
+
.map((line) => line.replace(/^[-*\s#]+/, '').trim())
|
|
3023
|
+
.filter(Boolean)
|
|
3024
|
+
.find((line) => !/^(receipt|summary|final|result)$/i.test(line))
|
|
3025
|
+
|| fallback;
|
|
3026
|
+
}
|
|
3027
|
+
|
|
3028
|
+
function missionWorkerLabel(mission) {
|
|
3029
|
+
const runner = String(mission && mission.runner || 'manual').toLowerCase();
|
|
3030
|
+
if (runner === 'atris2') return `Remote Atris2 computer${mission.model ? ` using ${mission.model}` : ''}`;
|
|
3031
|
+
if (runner === 'claude') return `Claude worker${mission.model ? ` using ${mission.model}` : ''}`;
|
|
3032
|
+
if (runner === 'codex_goal') return 'Codex goal handoff';
|
|
3033
|
+
return 'Local mission tick';
|
|
3034
|
+
}
|
|
3035
|
+
|
|
3036
|
+
function missionWorkerSummary(mission, receipt) {
|
|
3037
|
+
if (mission && mission.worker_summary) return mission.worker_summary;
|
|
3038
|
+
const tick = receipt && receipt.result && (receipt.result.tick || (Array.isArray(receipt.result.ticks) ? receipt.result.ticks[receipt.result.ticks.length - 1] : null));
|
|
3039
|
+
if (!tick) return mission && mission.last_tick_reason ? `Last tick: ${mission.last_tick_reason}` : 'No worker receipt yet.';
|
|
3040
|
+
return missionTickReportSummary(tick);
|
|
3041
|
+
}
|
|
3042
|
+
|
|
3043
|
+
function missionTickReportSummary(tick) {
|
|
3044
|
+
if (!tick) return 'Worker tick recorded.';
|
|
3045
|
+
if (tick.atris2) {
|
|
3046
|
+
return firstUsefulLine(tick.atris2.receipt_text, tick.atris2.ok ? 'Remote worker ran and returned a response.' : 'Remote worker failed.');
|
|
3047
|
+
}
|
|
3048
|
+
if (tick.claude) {
|
|
3049
|
+
if (tick.claude.skipped) {
|
|
3050
|
+
const skipReason = tick.claude.reason || tick.reason || '';
|
|
3051
|
+
if (!tick.summary && ['runner-uses-caller-session', 'orchestrator-is-caller-session'].includes(skipReason)) {
|
|
3052
|
+
return 'Goal handed to the active Codex session.';
|
|
3053
|
+
}
|
|
3054
|
+
return tick.summary || `Worker step skipped: ${tick.claude.reason || tick.reason || 'not needed'}.`;
|
|
3055
|
+
}
|
|
3056
|
+
return tick.claude.summary || firstUsefulLine(tick.claude.receipt_text, tick.claude.ok ? 'Worker ran and returned a response.' : 'Worker failed.');
|
|
3057
|
+
}
|
|
3058
|
+
if (tick.summary) return tick.summary;
|
|
3059
|
+
return tick.reason ? `Worker tick: ${tick.reason}` : 'Worker tick recorded.';
|
|
3060
|
+
}
|
|
3061
|
+
|
|
3062
|
+
function missionReceiptTicks(receipt) {
|
|
3063
|
+
const result = receipt && receipt.result;
|
|
3064
|
+
if (!result) return [];
|
|
3065
|
+
if (result.tick) return [result.tick];
|
|
3066
|
+
if (Array.isArray(result.ticks)) return result.ticks.filter(Boolean);
|
|
3067
|
+
return [];
|
|
3068
|
+
}
|
|
3069
|
+
|
|
3070
|
+
function missionTickAt(receipt, tick) {
|
|
3071
|
+
return tick.finished_at || tick.started_at || receipt.at || '';
|
|
3072
|
+
}
|
|
3073
|
+
|
|
3074
|
+
function missionTimelineTitle(tick, summary) {
|
|
3075
|
+
const tickIndex = Number(tick && tick.tick_index);
|
|
3076
|
+
const prefix = Number.isInteger(tickIndex) && tickIndex > 0 ? `Goal ${tickIndex}` : 'Goal';
|
|
3077
|
+
const text = String(summary || '').trim();
|
|
3078
|
+
const explicitGoal = text.match(/^(Goal\s+\d+)\s*:\s*(.*)$/i);
|
|
3079
|
+
if (explicitGoal) {
|
|
3080
|
+
return missionTimelineTitleLine(`${explicitGoal[1].replace(/\s+/g, ' ')}: ${explicitGoal[2].trim()}`);
|
|
3081
|
+
}
|
|
3082
|
+
return missionTimelineTitleLine(`${prefix}: ${text.replace(/^Goal\s*:\s*/i, '').trim()}`);
|
|
3083
|
+
}
|
|
3084
|
+
|
|
3085
|
+
function missionTimelineTitleLine(title) {
|
|
3086
|
+
const text = String(title || '').replace(/\s+/g, ' ').trim();
|
|
3087
|
+
const firstSentence = text.match(/^(.+?[.!?])(?:\s+|$)/);
|
|
3088
|
+
const candidate = firstSentence ? firstSentence[1].trim() : text;
|
|
3089
|
+
if (candidate.length <= 180) return candidate;
|
|
3090
|
+
return `${candidate.slice(0, 177).trimEnd()}...`;
|
|
3091
|
+
}
|
|
3092
|
+
|
|
3093
|
+
function missionTimelineSummaryIsUseful(summary) {
|
|
3094
|
+
const text = String(summary || '').trim();
|
|
3095
|
+
if (!text) return false;
|
|
3096
|
+
return ![
|
|
3097
|
+
'Goal handed to the active Codex session.',
|
|
3098
|
+
'Worker tick recorded.',
|
|
3099
|
+
].includes(text) && !/^Worker step skipped:/i.test(text);
|
|
3100
|
+
}
|
|
3101
|
+
|
|
3102
|
+
function missionTimelineTickIndex(tick) {
|
|
3103
|
+
const tickIndex = Number(tick && tick.tick_index);
|
|
3104
|
+
return Number.isInteger(tickIndex) && tickIndex > 0 ? tickIndex : null;
|
|
3105
|
+
}
|
|
3106
|
+
|
|
3107
|
+
function missionReportTimeline(mission, root = process.cwd(), limit = 6) {
|
|
3108
|
+
const paths = statePaths(root);
|
|
3109
|
+
let files = [];
|
|
3110
|
+
try {
|
|
3111
|
+
files = fs.readdirSync(paths.runsDir)
|
|
3112
|
+
.filter((file) => file.startsWith('mission-') && file.endsWith('.json'))
|
|
3113
|
+
.map((file) => path.join(paths.runsDir, file));
|
|
3114
|
+
} catch {
|
|
3115
|
+
files = [];
|
|
3116
|
+
}
|
|
3117
|
+
|
|
3118
|
+
const items = [];
|
|
3119
|
+
const seen = new Set();
|
|
3120
|
+
for (const file of files) {
|
|
3121
|
+
let receipt = null;
|
|
3122
|
+
try {
|
|
3123
|
+
receipt = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
3124
|
+
} catch {
|
|
3125
|
+
continue;
|
|
3126
|
+
}
|
|
3127
|
+
if (!receipt || receipt.mission_id !== mission.id) continue;
|
|
3128
|
+
const receiptPath = path.relative(root, file);
|
|
3129
|
+
for (const tick of missionReceiptTicks(receipt)) {
|
|
3130
|
+
const summary = missionTickReportSummary(tick);
|
|
3131
|
+
if (!missionTimelineSummaryIsUseful(summary)) continue;
|
|
3132
|
+
const at = missionTickAt(receipt, tick);
|
|
3133
|
+
const key = `${tick.tick_index || ''}:${at}:${summary}`;
|
|
3134
|
+
if (seen.has(key)) continue;
|
|
3135
|
+
seen.add(key);
|
|
3136
|
+
items.push({
|
|
3137
|
+
at,
|
|
3138
|
+
tick_index: missionTimelineTickIndex(tick),
|
|
3139
|
+
title: missionTimelineTitle(tick, summary),
|
|
3140
|
+
summary,
|
|
3141
|
+
receipt_path: receiptPath,
|
|
3142
|
+
});
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
3145
|
+
|
|
3146
|
+
items.sort((a, b) => String(a.at || '').localeCompare(String(b.at || '')));
|
|
3147
|
+
return items.slice(Math.max(0, items.length - limit));
|
|
3148
|
+
}
|
|
3149
|
+
|
|
3150
|
+
function missionLandingTimeline(mission, root = process.cwd(), limit = 12) {
|
|
3151
|
+
const paths = statePaths(root);
|
|
3152
|
+
let files = [];
|
|
3153
|
+
try {
|
|
3154
|
+
files = fs.readdirSync(paths.runsDir)
|
|
3155
|
+
.filter((file) => file.startsWith('mission-') && file.endsWith('.json'))
|
|
3156
|
+
.map((file) => path.join(paths.runsDir, file));
|
|
3157
|
+
} catch {
|
|
3158
|
+
files = [];
|
|
3159
|
+
}
|
|
3160
|
+
|
|
3161
|
+
const items = [];
|
|
3162
|
+
const seen = new Set();
|
|
3163
|
+
for (const file of files) {
|
|
3164
|
+
let receipt = null;
|
|
3165
|
+
try {
|
|
3166
|
+
receipt = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
3167
|
+
} catch {
|
|
3168
|
+
continue;
|
|
3169
|
+
}
|
|
3170
|
+
if (!receipt || receipt.mission_id !== mission.id) continue;
|
|
3171
|
+
const landing = receipt.result && receipt.result.landing;
|
|
3172
|
+
if (landing && landing.timeline_visible === false) continue;
|
|
3173
|
+
const changed = String(landing && landing.changed || '').trim();
|
|
3174
|
+
const next = String(landing && landing.next || '').trim();
|
|
3175
|
+
if (!changed && !next) continue;
|
|
3176
|
+
const receiptPath = path.relative(root, file);
|
|
3177
|
+
const key = `${receipt.at || ''}:${changed}:${next}`;
|
|
3178
|
+
if (seen.has(key)) continue;
|
|
3179
|
+
seen.add(key);
|
|
3180
|
+
items.push({
|
|
3181
|
+
at: receipt.at || '',
|
|
3182
|
+
changed,
|
|
3183
|
+
next,
|
|
3184
|
+
receipt_path: receiptPath,
|
|
3185
|
+
created_next: receipt.result.created_next || null,
|
|
3186
|
+
});
|
|
3187
|
+
}
|
|
3188
|
+
items.sort((a, b) => String(a.at || '').localeCompare(String(b.at || '')));
|
|
3189
|
+
const normalizedLimit = Number.isFinite(limit) && limit > 0 ? limit : items.length;
|
|
3190
|
+
const shown = items.slice(Math.max(0, items.length - normalizedLimit));
|
|
3191
|
+
return {
|
|
3192
|
+
items: shown,
|
|
3193
|
+
meta: {
|
|
3194
|
+
shown_count: shown.length,
|
|
3195
|
+
total_count: items.length,
|
|
3196
|
+
hidden_count: Math.max(0, items.length - shown.length),
|
|
3197
|
+
truncated: shown.length < items.length,
|
|
3198
|
+
limit: normalizedLimit === Number.MAX_SAFE_INTEGER ? null : normalizedLimit,
|
|
3199
|
+
},
|
|
3200
|
+
};
|
|
3201
|
+
}
|
|
3202
|
+
|
|
3203
|
+
function formatInteger(value) {
|
|
3204
|
+
return String(Math.max(0, Number(value || 0))).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
3205
|
+
}
|
|
3206
|
+
|
|
3207
|
+
function missionTimelinePruneSummaryMarkdown(prunePreview) {
|
|
3208
|
+
if (!prunePreview) return [];
|
|
3209
|
+
if (prunePreview.error) {
|
|
3210
|
+
return [
|
|
3211
|
+
'',
|
|
3212
|
+
'## Latest prune dry-run',
|
|
3213
|
+
'',
|
|
3214
|
+
`- Not available: ${prunePreview.error}`,
|
|
3215
|
+
];
|
|
3216
|
+
}
|
|
3217
|
+
return [
|
|
3218
|
+
'',
|
|
3219
|
+
'## Latest prune dry-run',
|
|
3220
|
+
'',
|
|
3221
|
+
`- Policy: keep newest ${formatInteger(prunePreview.policy?.keep_newest)}; keep ${formatInteger(prunePreview.policy?.keep_days)} days`,
|
|
3222
|
+
`- Total run files: ${formatInteger(prunePreview.total_files)}`,
|
|
3223
|
+
`- Total run bytes: ${formatInteger(prunePreview.total_bytes)} (${formatBytes(prunePreview.total_bytes)})`,
|
|
3224
|
+
`- Referenced files kept: ${formatInteger(prunePreview.referenced_files)}`,
|
|
3225
|
+
`- Recent files kept: ${formatInteger(prunePreview.recent_files)}`,
|
|
3226
|
+
`- Would prune: ${formatInteger(prunePreview.prune_count)} files / ${formatInteger(prunePreview.prune_bytes)} bytes (${formatBytes(prunePreview.prune_bytes)})`,
|
|
3227
|
+
`- Deleted files: ${formatInteger(prunePreview.deleted_count)}`,
|
|
3228
|
+
];
|
|
3229
|
+
}
|
|
3230
|
+
|
|
3231
|
+
function missionTimelinePruneSummaryLine(prunePreview) {
|
|
3232
|
+
if (!prunePreview) return null;
|
|
3233
|
+
if (prunePreview.error) return `Prune dry-run: not available (${prunePreview.error}).`;
|
|
3234
|
+
return `Prune dry-run: ${formatInteger(prunePreview.prune_count)} files / ${formatBytes(prunePreview.prune_bytes)} would prune; ${formatInteger(prunePreview.deleted_count)} deleted.`;
|
|
3235
|
+
}
|
|
3236
|
+
|
|
3237
|
+
function missionTimelinePruneSummaryObject(prunePreview) {
|
|
3238
|
+
if (!prunePreview) return null;
|
|
3239
|
+
if (prunePreview.error) {
|
|
3240
|
+
return {
|
|
3241
|
+
ok: false,
|
|
3242
|
+
error: prunePreview.error,
|
|
3243
|
+
text: missionTimelinePruneSummaryLine(prunePreview),
|
|
3244
|
+
};
|
|
3245
|
+
}
|
|
3246
|
+
return {
|
|
3247
|
+
ok: true,
|
|
3248
|
+
text: missionTimelinePruneSummaryLine(prunePreview),
|
|
3249
|
+
total_files: prunePreview.total_files,
|
|
3250
|
+
total_bytes: prunePreview.total_bytes,
|
|
3251
|
+
referenced_files: prunePreview.referenced_files,
|
|
3252
|
+
recent_files: prunePreview.recent_files,
|
|
3253
|
+
prune_count: prunePreview.prune_count,
|
|
3254
|
+
prune_bytes: prunePreview.prune_bytes,
|
|
3255
|
+
prune_bytes_text: formatBytes(prunePreview.prune_bytes),
|
|
3256
|
+
deleted_count: prunePreview.deleted_count,
|
|
3257
|
+
};
|
|
3258
|
+
}
|
|
3259
|
+
|
|
3260
|
+
function missionTimelineCurrentLanding(timeline) {
|
|
3261
|
+
if (!timeline.length) return null;
|
|
3262
|
+
const latest = timeline[timeline.length - 1];
|
|
3263
|
+
return {
|
|
3264
|
+
at: latest.at || '',
|
|
3265
|
+
changed: latest.changed || '',
|
|
3266
|
+
next: latest.next || '',
|
|
3267
|
+
receipt_path: latest.receipt_path || '',
|
|
3268
|
+
};
|
|
3269
|
+
}
|
|
3270
|
+
|
|
3271
|
+
function missionTimelineEmptyStateDisplay(mission, timelineLength = 0) {
|
|
3272
|
+
const hasMission = Boolean(mission);
|
|
3273
|
+
const isEmpty = !timelineLength;
|
|
3274
|
+
return {
|
|
3275
|
+
label: 'Empty state',
|
|
3276
|
+
is_empty: isEmpty,
|
|
3277
|
+
has_mission: hasMission,
|
|
3278
|
+
title: isEmpty ? (hasMission ? 'No timeline items yet.' : 'No missions yet.') : null,
|
|
3279
|
+
message: isEmpty
|
|
3280
|
+
? (hasMission ? 'Run the mission once to create the first timeline item.' : 'Start a mission to create the first timeline item.')
|
|
3281
|
+
: null,
|
|
3282
|
+
action_label: isEmpty ? (hasMission ? 'Run mission' : 'Start mission') : null,
|
|
3283
|
+
command: isEmpty ? (hasMission ? `atris mission run ${mission.id} --create-next` : 'atris mission start "..." --owner <member>') : null,
|
|
3284
|
+
};
|
|
3285
|
+
}
|
|
3286
|
+
|
|
3287
|
+
function missionTimelineNextMove(mission, landing) {
|
|
3288
|
+
const latestNext = landing ? String(landing.next || '').trim() : '';
|
|
3289
|
+
return latestNext || `atris mission run ${mission.id} --create-next`;
|
|
1314
3290
|
}
|
|
1315
3291
|
|
|
1316
|
-
function
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
3292
|
+
function missionTimelineCountLine(meta) {
|
|
3293
|
+
if (!meta) return null;
|
|
3294
|
+
const shown = Number(meta.shown_count || 0);
|
|
3295
|
+
const total = Number(meta.total_count || 0);
|
|
3296
|
+
if (meta.truncated) {
|
|
3297
|
+
return `Showing latest ${formatInteger(shown)} of ${formatInteger(total)} ${total === 1 ? 'item' : 'items'}.`;
|
|
3298
|
+
}
|
|
3299
|
+
return `Showing ${formatInteger(shown)} ${shown === 1 ? 'item' : 'items'}.`;
|
|
1323
3300
|
}
|
|
1324
3301
|
|
|
1325
|
-
function
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
3302
|
+
function missionTimelineCurrentLandingLines(landing) {
|
|
3303
|
+
if (!landing) return [];
|
|
3304
|
+
return [
|
|
3305
|
+
'Current landing:',
|
|
3306
|
+
` Changed: ${landing.changed || 'Landing recorded.'}`,
|
|
3307
|
+
...(landing.next ? [` Next: ${landing.next}`] : []),
|
|
3308
|
+
` Proof: ${landing.receipt_path}`,
|
|
3309
|
+
'',
|
|
3310
|
+
];
|
|
1331
3311
|
}
|
|
1332
3312
|
|
|
1333
|
-
function
|
|
1334
|
-
|
|
1335
|
-
const
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
3313
|
+
function missionTimelineMarkdown(mission, timeline, { prunePreview = null, generatedAt = stampIso(), timelineMeta = null } = {}) {
|
|
3314
|
+
const latestLanding = timeline.length ? timeline[timeline.length - 1] : null;
|
|
3315
|
+
const nextMove = missionTimelineNextMove(mission, latestLanding);
|
|
3316
|
+
const lines = [
|
|
3317
|
+
`# Mission timeline: ${mission.objective}`,
|
|
3318
|
+
'',
|
|
3319
|
+
`Mission: ${mission.id}`,
|
|
3320
|
+
`Status: ${mission.status}`,
|
|
3321
|
+
`Generated at: ${generatedAt}`,
|
|
3322
|
+
...(timelineMeta ? [missionTimelineCountLine(timelineMeta)] : []),
|
|
3323
|
+
'',
|
|
3324
|
+
'## Operator commands',
|
|
3325
|
+
'',
|
|
3326
|
+
`- Timeline: \`${missionRunTimelineCommand(mission)}\``,
|
|
3327
|
+
`- Export: \`${missionRunExportCommand(mission)}\``,
|
|
3328
|
+
`- Prune preview: \`${missionRunPrunePreviewCommand(mission)}\``,
|
|
3329
|
+
'',
|
|
3330
|
+
];
|
|
3331
|
+
if (latestLanding) {
|
|
3332
|
+
lines.push(
|
|
3333
|
+
'## Current landing',
|
|
3334
|
+
'',
|
|
3335
|
+
`Changed: ${latestLanding.changed || 'Landing recorded.'}`,
|
|
3336
|
+
...(latestLanding.next ? [`Next: ${latestLanding.next}`] : []),
|
|
3337
|
+
`Proof: ${latestLanding.receipt_path}`,
|
|
3338
|
+
'',
|
|
3339
|
+
);
|
|
1339
3340
|
}
|
|
1340
|
-
if (
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
3341
|
+
if (!timeline.length) {
|
|
3342
|
+
lines.push('No landing receipts yet.');
|
|
3343
|
+
lines.push(
|
|
3344
|
+
'',
|
|
3345
|
+
'## Next move',
|
|
3346
|
+
'',
|
|
3347
|
+
nextMove,
|
|
3348
|
+
);
|
|
3349
|
+
lines.push(
|
|
3350
|
+
'',
|
|
3351
|
+
'## Keep it concise',
|
|
3352
|
+
'',
|
|
3353
|
+
'- Dry run: `atris mission prune-runs --days 14 --keep-newest 200`',
|
|
3354
|
+
'- Apply only after review: add `--apply`.',
|
|
3355
|
+
);
|
|
3356
|
+
lines.push(...missionTimelinePruneSummaryMarkdown(prunePreview));
|
|
3357
|
+
lines.push('');
|
|
3358
|
+
return lines.join('\n');
|
|
1345
3359
|
}
|
|
1346
|
-
|
|
1347
|
-
|
|
3360
|
+
timeline.forEach((item, index) => {
|
|
3361
|
+
if (index === 0) lines.push('## Full history', '');
|
|
3362
|
+
lines.push(`${index + 1}. ${item.changed || 'Landing recorded.'}`);
|
|
3363
|
+
if (item.next) lines.push(` - Next: ${item.next}`);
|
|
3364
|
+
lines.push(` - Proof: ${item.receipt_path}`);
|
|
3365
|
+
});
|
|
3366
|
+
lines.push('', '## Next move', '', nextMove);
|
|
3367
|
+
lines.push(
|
|
3368
|
+
'',
|
|
3369
|
+
'## Keep it concise',
|
|
3370
|
+
'',
|
|
3371
|
+
'- Dry run: `atris mission prune-runs --days 14 --keep-newest 200`',
|
|
3372
|
+
'- Apply only after review: add `--apply`.',
|
|
3373
|
+
);
|
|
3374
|
+
lines.push(...missionTimelinePruneSummaryMarkdown(prunePreview));
|
|
3375
|
+
lines.push('');
|
|
3376
|
+
return lines.join('\n');
|
|
3377
|
+
}
|
|
3378
|
+
|
|
3379
|
+
function defaultMissionTimelinePath(root, mission) {
|
|
3380
|
+
const safeId = String(mission.id || 'mission')
|
|
3381
|
+
.replace(/[^a-zA-Z0-9._-]+/g, '-')
|
|
3382
|
+
.replace(/^-+|-+$/g, '') || 'mission';
|
|
3383
|
+
return path.join(root, 'atris', 'reports', `${safeId}-timeline.md`);
|
|
1348
3384
|
}
|
|
1349
3385
|
|
|
1350
3386
|
function missionReportFor(mission, root = process.cwd()) {
|
|
@@ -1361,12 +3397,19 @@ function missionReportFor(mission, root = process.cwd()) {
|
|
|
1361
3397
|
operator_outcome: operatorOutcome,
|
|
1362
3398
|
worker: mission.worker || missionWorkerLabel(mission),
|
|
1363
3399
|
worker_summary: missionWorkerSummary(mission, receipt),
|
|
3400
|
+
timeline: missionReportTimeline(mission, root),
|
|
1364
3401
|
worker_receipt_path: workerReceiptPath,
|
|
1365
3402
|
verifier_receipt_path: verifierReceiptPath,
|
|
1366
3403
|
operator_next: mission.operator_next || mission.next_action || 'Review the mission state.',
|
|
1367
3404
|
};
|
|
1368
3405
|
}
|
|
1369
3406
|
|
|
3407
|
+
function missionReportNextText(nextAction) {
|
|
3408
|
+
return String(nextAction || 'Review the mission state.')
|
|
3409
|
+
.replace(/^next move:\s*run\s+/i, '')
|
|
3410
|
+
.trim();
|
|
3411
|
+
}
|
|
3412
|
+
|
|
1370
3413
|
function reportMission(args) {
|
|
1371
3414
|
const asJson = wantsJson(args);
|
|
1372
3415
|
const localOnly = hasFlag(args, '--local');
|
|
@@ -1396,15 +3439,483 @@ function reportMission(args) {
|
|
|
1396
3439
|
` What happened: ${report.operator_outcome}`,
|
|
1397
3440
|
` Worker: ${report.worker}`,
|
|
1398
3441
|
` Worker summary: ${report.worker_summary}`,
|
|
3442
|
+
...(report.timeline && report.timeline.length ? [
|
|
3443
|
+
' Timeline:',
|
|
3444
|
+
...report.timeline.map((item) => ` - ${item.title}`),
|
|
3445
|
+
] : []),
|
|
1399
3446
|
...(report.worker_receipt_path ? [` Worker receipt: ${report.worker_receipt_path}`] : []),
|
|
1400
3447
|
...(report.verifier_receipt_path ? [` Verifier receipt: ${report.verifier_receipt_path}`] : []),
|
|
1401
|
-
` Next: ${report.operator_next}`,
|
|
3448
|
+
` Next: ${missionReportNextText(report.operator_next)}`,
|
|
1402
3449
|
])
|
|
1403
3450
|
: ['No missions yet. Run: atris mission start "..." --owner <member>'],
|
|
1404
3451
|
asJson,
|
|
1405
3452
|
);
|
|
1406
3453
|
}
|
|
1407
3454
|
|
|
3455
|
+
function timelineMission(args) {
|
|
3456
|
+
const asJson = wantsJson(args);
|
|
3457
|
+
const write = hasFlag(args, '--write');
|
|
3458
|
+
const all = hasFlag(args, '--all');
|
|
3459
|
+
const prunePreviewRequested = hasFlag(args, '--prune-preview');
|
|
3460
|
+
const outputPath = readFlag(args, '--output', '') || readFlag(args, '--out', '');
|
|
3461
|
+
const ref = stripKnownFlags(args, ['--limit', '--output', '--out'], ['--json', '--write', '--all', '--prune-preview'])[0] || '';
|
|
3462
|
+
const limit = all ? Number.MAX_SAFE_INTEGER : readPositiveIntegerFlag(args, '--limit', 12, { json: asJson });
|
|
3463
|
+
const missions = listMissions();
|
|
3464
|
+
const mission = ref
|
|
3465
|
+
? resolveMission(ref)
|
|
3466
|
+
: (missions.find((row) => !TERMINAL_STATUSES.has(row.status)) || missions[0] || null);
|
|
3467
|
+
if (ref && !mission) {
|
|
3468
|
+
exitMissionError(`Mission "${ref}" not found.`, 1, asJson);
|
|
3469
|
+
}
|
|
3470
|
+
if (!mission) {
|
|
3471
|
+
printJsonOrText(
|
|
3472
|
+
{
|
|
3473
|
+
ok: true,
|
|
3474
|
+
action: 'mission_timeline',
|
|
3475
|
+
mission: null,
|
|
3476
|
+
timeline: [],
|
|
3477
|
+
empty_state_display: missionTimelineEmptyStateDisplay(null, 0),
|
|
3478
|
+
},
|
|
3479
|
+
['No missions yet. Run: atris mission start "..." --owner <member>'],
|
|
3480
|
+
asJson,
|
|
3481
|
+
);
|
|
3482
|
+
return;
|
|
3483
|
+
}
|
|
3484
|
+
const root = mission.worktree_root || process.cwd();
|
|
3485
|
+
const timelineResult = missionLandingTimeline(mission, root, limit);
|
|
3486
|
+
const timeline = timelineResult.items;
|
|
3487
|
+
const currentLanding = missionTimelineCurrentLanding(timeline);
|
|
3488
|
+
const timelineItemDisplay = (item) => ({
|
|
3489
|
+
changed_label: 'Changed',
|
|
3490
|
+
changed: item.changed || 'Landing recorded.',
|
|
3491
|
+
next_label: 'Next',
|
|
3492
|
+
next: item.next || null,
|
|
3493
|
+
proof_label: 'Proof',
|
|
3494
|
+
proof: item.receipt_path || null,
|
|
3495
|
+
});
|
|
3496
|
+
const currentLandingDisplay = currentLanding ? {
|
|
3497
|
+
label: 'Current landing',
|
|
3498
|
+
...timelineItemDisplay(currentLanding),
|
|
3499
|
+
} : null;
|
|
3500
|
+
const historyWithoutCurrent = currentLanding ? timeline.slice(0, -1) : timeline;
|
|
3501
|
+
const historyWithoutCurrentDisplay = historyWithoutCurrent.map((item, index) => ({
|
|
3502
|
+
index: index + 1,
|
|
3503
|
+
label: `History item ${index + 1}`,
|
|
3504
|
+
...timelineItemDisplay(item),
|
|
3505
|
+
}));
|
|
3506
|
+
const timelineDisplay = timeline.map((item, index) => ({
|
|
3507
|
+
index: index + 1,
|
|
3508
|
+
label: `Timeline item ${index + 1}`,
|
|
3509
|
+
at_label: 'When',
|
|
3510
|
+
at: item.at || null,
|
|
3511
|
+
...timelineItemDisplay(item),
|
|
3512
|
+
}));
|
|
3513
|
+
const nextMove = missionTimelineNextMove(mission, currentLanding);
|
|
3514
|
+
const generatedAt = stampIso();
|
|
3515
|
+
let artifactPath = null;
|
|
3516
|
+
let prunePreview = null;
|
|
3517
|
+
if (write || prunePreviewRequested) {
|
|
3518
|
+
try {
|
|
3519
|
+
prunePreview = pruneRuns(root, { keepNewest: 200, keepDays: 14 });
|
|
3520
|
+
} catch (error) {
|
|
3521
|
+
prunePreview = { error: error.message || String(error) };
|
|
3522
|
+
}
|
|
3523
|
+
}
|
|
3524
|
+
if (write) {
|
|
3525
|
+
const outPath = outputPath
|
|
3526
|
+
? path.resolve(root, outputPath)
|
|
3527
|
+
: defaultMissionTimelinePath(root, mission);
|
|
3528
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
3529
|
+
fs.writeFileSync(outPath, missionTimelineMarkdown(mission, timeline, { prunePreview, generatedAt, timelineMeta: timelineResult.meta }), 'utf8');
|
|
3530
|
+
artifactPath = path.relative(root, outPath);
|
|
3531
|
+
}
|
|
3532
|
+
const pruneSummary = missionTimelinePruneSummaryObject(prunePreview);
|
|
3533
|
+
const pruneDisplay = {
|
|
3534
|
+
label: 'Prune preview',
|
|
3535
|
+
command: missionRunPrunePreviewCommand(mission),
|
|
3536
|
+
available: Boolean(pruneSummary),
|
|
3537
|
+
ok: pruneSummary?.ok ?? null,
|
|
3538
|
+
summary: pruneSummary?.text || null,
|
|
3539
|
+
would_prune_label: 'Would prune',
|
|
3540
|
+
prune_count: pruneSummary?.prune_count ?? null,
|
|
3541
|
+
prune_bytes_text: pruneSummary?.prune_bytes_text || null,
|
|
3542
|
+
deleted_label: 'Deleted',
|
|
3543
|
+
deleted_count: pruneSummary?.deleted_count ?? null,
|
|
3544
|
+
};
|
|
3545
|
+
const payload = {
|
|
3546
|
+
ok: true,
|
|
3547
|
+
action: 'mission_timeline',
|
|
3548
|
+
generated_at: generatedAt,
|
|
3549
|
+
generated: {
|
|
3550
|
+
label: 'Generated at',
|
|
3551
|
+
at: generatedAt,
|
|
3552
|
+
},
|
|
3553
|
+
schema_display: {
|
|
3554
|
+
label: 'Schema',
|
|
3555
|
+
name: 'atris.mission_timeline',
|
|
3556
|
+
version: 1,
|
|
3557
|
+
primary_objects: [
|
|
3558
|
+
'display',
|
|
3559
|
+
'summary_display',
|
|
3560
|
+
'navigation_display',
|
|
3561
|
+
'filter_display',
|
|
3562
|
+
'mission_display',
|
|
3563
|
+
'current_landing_display',
|
|
3564
|
+
'history_without_current_display',
|
|
3565
|
+
'timeline_display',
|
|
3566
|
+
'timeline_meta_display',
|
|
3567
|
+
'empty_state_display',
|
|
3568
|
+
'status_display',
|
|
3569
|
+
'actions_display',
|
|
3570
|
+
'proof_display',
|
|
3571
|
+
'receipt_display',
|
|
3572
|
+
'export_display',
|
|
3573
|
+
'prune_display',
|
|
3574
|
+
'artifact_display',
|
|
3575
|
+
],
|
|
3576
|
+
},
|
|
3577
|
+
summary_display: {
|
|
3578
|
+
label: 'Summary',
|
|
3579
|
+
title: `Mission timeline: ${mission.objective}`,
|
|
3580
|
+
count: missionTimelineCountLine(timelineResult.meta),
|
|
3581
|
+
latest_label: 'Latest',
|
|
3582
|
+
latest: currentLanding?.changed || null,
|
|
3583
|
+
proof_label: 'Proof',
|
|
3584
|
+
proof: currentLanding?.receipt_path || null,
|
|
3585
|
+
next_label: 'Next',
|
|
3586
|
+
next: nextMove,
|
|
3587
|
+
},
|
|
3588
|
+
display: {
|
|
3589
|
+
title: `Mission timeline: ${mission.objective}`,
|
|
3590
|
+
generated: `Generated at: ${generatedAt}`,
|
|
3591
|
+
count: missionTimelineCountLine(timelineResult.meta),
|
|
3592
|
+
current_landing_label: 'Current landing',
|
|
3593
|
+
history_label: 'History',
|
|
3594
|
+
next_label: 'Next',
|
|
3595
|
+
},
|
|
3596
|
+
status_display: {
|
|
3597
|
+
label: 'Status',
|
|
3598
|
+
mission_status_label: 'Mission status',
|
|
3599
|
+
mission_status: mission.status,
|
|
3600
|
+
history_status_label: 'History status',
|
|
3601
|
+
history_status: timelineResult.meta.truncated ? 'Compact history' : 'Full history',
|
|
3602
|
+
count: missionTimelineCountLine(timelineResult.meta),
|
|
3603
|
+
truncated: Boolean(timelineResult.meta.truncated),
|
|
3604
|
+
hidden_count: timelineResult.meta.hidden_count,
|
|
3605
|
+
},
|
|
3606
|
+
empty_state_display: missionTimelineEmptyStateDisplay(mission, timeline.length),
|
|
3607
|
+
mission: {
|
|
3608
|
+
id: mission.id,
|
|
3609
|
+
objective: mission.objective,
|
|
3610
|
+
status: mission.status,
|
|
3611
|
+
},
|
|
3612
|
+
mission_labels: {
|
|
3613
|
+
mission: 'Mission',
|
|
3614
|
+
objective: 'Objective',
|
|
3615
|
+
status: 'Status',
|
|
3616
|
+
},
|
|
3617
|
+
mission_display: {
|
|
3618
|
+
label: 'Mission',
|
|
3619
|
+
title: mission.objective,
|
|
3620
|
+
id: mission.id,
|
|
3621
|
+
status: mission.status,
|
|
3622
|
+
},
|
|
3623
|
+
operator_commands: {
|
|
3624
|
+
timeline: missionRunTimelineCommand(mission),
|
|
3625
|
+
export: missionRunExportCommand(mission),
|
|
3626
|
+
prune_preview: missionRunPrunePreviewCommand(mission),
|
|
3627
|
+
},
|
|
3628
|
+
commands: {
|
|
3629
|
+
timeline: missionRunTimelineCommand(mission),
|
|
3630
|
+
export: missionRunExportCommand(mission),
|
|
3631
|
+
prune_preview: missionRunPrunePreviewCommand(mission),
|
|
3632
|
+
},
|
|
3633
|
+
actions_display: {
|
|
3634
|
+
label: 'Actions',
|
|
3635
|
+
items: [
|
|
3636
|
+
{ label: 'Timeline', command: missionRunTimelineCommand(mission) },
|
|
3637
|
+
{ label: 'Export', command: missionRunExportCommand(mission) },
|
|
3638
|
+
{ label: 'Prune preview', command: missionRunPrunePreviewCommand(mission) },
|
|
3639
|
+
],
|
|
3640
|
+
},
|
|
3641
|
+
navigation_display: {
|
|
3642
|
+
label: 'Navigation',
|
|
3643
|
+
current_label: 'Current view',
|
|
3644
|
+
current: 'timeline',
|
|
3645
|
+
items: [
|
|
3646
|
+
{ key: 'timeline', label: 'Timeline', command: missionRunTimelineCommand(mission), active: true },
|
|
3647
|
+
{ key: 'export', label: 'Full history', command: missionRunExportCommand(mission), active: false },
|
|
3648
|
+
{ key: 'prune_preview', label: 'Prune preview', command: missionRunPrunePreviewCommand(mission), active: false },
|
|
3649
|
+
],
|
|
3650
|
+
},
|
|
3651
|
+
filter_display: {
|
|
3652
|
+
label: 'Filters',
|
|
3653
|
+
active_label: 'Active filter',
|
|
3654
|
+
active: all ? 'full_history' : 'latest',
|
|
3655
|
+
limit_label: 'Limit',
|
|
3656
|
+
limit: timelineResult.meta.limit,
|
|
3657
|
+
shown_count: timelineResult.meta.shown_count,
|
|
3658
|
+
total_count: timelineResult.meta.total_count,
|
|
3659
|
+
hidden_count: timelineResult.meta.hidden_count,
|
|
3660
|
+
truncated_label: 'Truncated',
|
|
3661
|
+
truncated: Boolean(timelineResult.meta.truncated),
|
|
3662
|
+
items: [
|
|
3663
|
+
{ key: 'latest', label: 'Latest', command: missionRunTimelineCommand(mission), active: !all },
|
|
3664
|
+
{ key: 'full_history', label: 'Full history', command: `atris mission timeline ${mission.id} --all`, active: Boolean(all) },
|
|
3665
|
+
],
|
|
3666
|
+
},
|
|
3667
|
+
current_landing: currentLanding,
|
|
3668
|
+
current_landing_display: currentLandingDisplay,
|
|
3669
|
+
current_landing_label: 'Current landing',
|
|
3670
|
+
history_without_current: historyWithoutCurrent,
|
|
3671
|
+
history_without_current_display: historyWithoutCurrentDisplay,
|
|
3672
|
+
history_without_current_count: historyWithoutCurrent.length,
|
|
3673
|
+
has_history_without_current: historyWithoutCurrent.length > 0,
|
|
3674
|
+
history_label: 'History',
|
|
3675
|
+
labels: {
|
|
3676
|
+
current_landing: 'Current landing',
|
|
3677
|
+
history: 'History',
|
|
3678
|
+
},
|
|
3679
|
+
counts: {
|
|
3680
|
+
timeline: timeline.length,
|
|
3681
|
+
history_without_current: historyWithoutCurrent.length,
|
|
3682
|
+
total: timelineResult.meta.total_count,
|
|
3683
|
+
hidden: timelineResult.meta.hidden_count,
|
|
3684
|
+
shown: timelineResult.meta.shown_count,
|
|
3685
|
+
},
|
|
3686
|
+
booleans: {
|
|
3687
|
+
has_current_landing: Boolean(currentLanding),
|
|
3688
|
+
has_history_without_current: historyWithoutCurrent.length > 0,
|
|
3689
|
+
truncated: Boolean(timelineResult.meta.truncated),
|
|
3690
|
+
all: Boolean(all),
|
|
3691
|
+
},
|
|
3692
|
+
next_move: nextMove,
|
|
3693
|
+
next: {
|
|
3694
|
+
label: 'Next',
|
|
3695
|
+
move: nextMove,
|
|
3696
|
+
has_move: Boolean(nextMove),
|
|
3697
|
+
},
|
|
3698
|
+
timeline,
|
|
3699
|
+
timeline_display: timelineDisplay,
|
|
3700
|
+
timeline_meta: timelineResult.meta,
|
|
3701
|
+
timeline_meta_display: {
|
|
3702
|
+
label: 'Timeline metadata',
|
|
3703
|
+
shown_label: 'Shown',
|
|
3704
|
+
shown_count: timelineResult.meta.shown_count,
|
|
3705
|
+
total_label: 'Total',
|
|
3706
|
+
total_count: timelineResult.meta.total_count,
|
|
3707
|
+
hidden_label: 'Hidden',
|
|
3708
|
+
hidden_count: timelineResult.meta.hidden_count,
|
|
3709
|
+
limit_label: 'Limit',
|
|
3710
|
+
limit: timelineResult.meta.limit,
|
|
3711
|
+
truncated_label: 'Truncated',
|
|
3712
|
+
truncated: Boolean(timelineResult.meta.truncated),
|
|
3713
|
+
},
|
|
3714
|
+
all,
|
|
3715
|
+
prune_summary: pruneSummary,
|
|
3716
|
+
prune_display: pruneDisplay,
|
|
3717
|
+
prune_preview: prunePreview,
|
|
3718
|
+
artifact: {
|
|
3719
|
+
path: artifactPath,
|
|
3720
|
+
written: Boolean(artifactPath),
|
|
3721
|
+
format: artifactPath ? 'markdown' : null,
|
|
3722
|
+
},
|
|
3723
|
+
artifact_display: {
|
|
3724
|
+
label: 'Artifact',
|
|
3725
|
+
path_label: 'Path',
|
|
3726
|
+
path: artifactPath,
|
|
3727
|
+
written_label: 'Written',
|
|
3728
|
+
written: Boolean(artifactPath),
|
|
3729
|
+
format_label: 'Format',
|
|
3730
|
+
format: artifactPath ? 'markdown' : null,
|
|
3731
|
+
},
|
|
3732
|
+
export_display: {
|
|
3733
|
+
label: 'Export',
|
|
3734
|
+
command: missionRunExportCommand(mission),
|
|
3735
|
+
report_label: 'Saved report',
|
|
3736
|
+
report_path: artifactPath,
|
|
3737
|
+
report_written: Boolean(artifactPath),
|
|
3738
|
+
report_format: artifactPath ? 'markdown' : null,
|
|
3739
|
+
},
|
|
3740
|
+
receipt_display: {
|
|
3741
|
+
label: 'Receipts',
|
|
3742
|
+
latest_label: 'Latest receipt',
|
|
3743
|
+
latest_path: currentLanding?.receipt_path || null,
|
|
3744
|
+
has_latest: Boolean(currentLanding?.receipt_path),
|
|
3745
|
+
count_label: 'Receipts',
|
|
3746
|
+
count: timeline.filter((item) => item.receipt_path).length,
|
|
3747
|
+
items: timeline.map((item, index) => ({
|
|
3748
|
+
index: index + 1,
|
|
3749
|
+
label: `Receipt ${index + 1}`,
|
|
3750
|
+
path: item.receipt_path || null,
|
|
3751
|
+
at: item.at || null,
|
|
3752
|
+
current: Boolean(currentLanding?.receipt_path && item.receipt_path === currentLanding.receipt_path),
|
|
3753
|
+
})),
|
|
3754
|
+
},
|
|
3755
|
+
proof_display: {
|
|
3756
|
+
label: 'Proof',
|
|
3757
|
+
latest_receipt_label: 'Latest receipt',
|
|
3758
|
+
latest_receipt_path: currentLanding?.receipt_path || null,
|
|
3759
|
+
report_label: 'Saved report',
|
|
3760
|
+
report_path: artifactPath,
|
|
3761
|
+
report_written: Boolean(artifactPath),
|
|
3762
|
+
report_format: artifactPath ? 'markdown' : null,
|
|
3763
|
+
},
|
|
3764
|
+
artifact_path: artifactPath,
|
|
3765
|
+
};
|
|
3766
|
+
const lines = timeline.length
|
|
3767
|
+
? [
|
|
3768
|
+
`Mission timeline: ${mission.objective}`,
|
|
3769
|
+
`Generated at: ${generatedAt}`,
|
|
3770
|
+
missionTimelineCountLine(timelineResult.meta),
|
|
3771
|
+
...missionTimelineCurrentLandingLines(currentLanding),
|
|
3772
|
+
...(historyWithoutCurrent.length ? ['History:'] : []),
|
|
3773
|
+
...historyWithoutCurrent.flatMap((item, index) => [
|
|
3774
|
+
` ${index + 1}. ${item.changed || 'Landing recorded.'}`,
|
|
3775
|
+
...(item.next ? [` Next: ${item.next}`] : []),
|
|
3776
|
+
` Proof: ${item.receipt_path}`,
|
|
3777
|
+
]),
|
|
3778
|
+
]
|
|
3779
|
+
: [
|
|
3780
|
+
`Mission timeline: ${mission.objective}`,
|
|
3781
|
+
`Generated at: ${generatedAt}`,
|
|
3782
|
+
missionTimelineCountLine(timelineResult.meta),
|
|
3783
|
+
' No landing receipts yet.',
|
|
3784
|
+
` Next: atris mission run ${mission.id} --create-next`,
|
|
3785
|
+
];
|
|
3786
|
+
if (timelineResult.meta.truncated) lines.push(`Full history: ${missionRunExportCommand(mission)}`);
|
|
3787
|
+
if (artifactPath) lines.push(`Saved: ${artifactPath}`);
|
|
3788
|
+
const pruneLine = missionTimelinePruneSummaryLine(prunePreview);
|
|
3789
|
+
if (pruneLine) lines.push(pruneLine);
|
|
3790
|
+
printJsonOrText(payload, lines, asJson);
|
|
3791
|
+
}
|
|
3792
|
+
|
|
3793
|
+
function missionReceiptHealth(mission, root = process.cwd()) {
|
|
3794
|
+
const receiptPath = String(mission?.receipt_path || '').trim();
|
|
3795
|
+
if (!receiptPath) return { ok: false, reason: 'missing_receipt_path' };
|
|
3796
|
+
const file = path.isAbsolute(receiptPath) ? receiptPath : path.join(root, receiptPath);
|
|
3797
|
+
let receipt = null;
|
|
3798
|
+
try {
|
|
3799
|
+
receipt = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
3800
|
+
} catch {
|
|
3801
|
+
return { ok: false, reason: 'receipt_not_found', receipt_path: receiptPath };
|
|
3802
|
+
}
|
|
3803
|
+
const missionVerifierPassedState = mission?.verifier_result?.passed === true;
|
|
3804
|
+
const receiptPassed = receipt?.result?.passed === true
|
|
3805
|
+
|| receipt?.result?.verifier_result?.passed === true;
|
|
3806
|
+
if (!missionVerifierPassedState || !receiptPassed) {
|
|
3807
|
+
return { ok: false, reason: 'verifier_not_passed', receipt_path: receiptPath };
|
|
3808
|
+
}
|
|
3809
|
+
return { ok: true, reason: 'verifier_passed', receipt_path: receiptPath };
|
|
3810
|
+
}
|
|
3811
|
+
|
|
3812
|
+
function collectMissionDoctorFindings(root = process.cwd(), options = {}) {
|
|
3813
|
+
const localOnly = options.localOnly === true;
|
|
3814
|
+
let missions = listMissions(root);
|
|
3815
|
+
if (!localOnly) {
|
|
3816
|
+
const seen = new Set(missions.map((mission) => mission.id));
|
|
3817
|
+
for (const rolled of listWorktreeRollupMissions(root)) {
|
|
3818
|
+
if (seen.has(rolled.id)) continue;
|
|
3819
|
+
seen.add(rolled.id);
|
|
3820
|
+
missions.push(rolled);
|
|
3821
|
+
}
|
|
3822
|
+
}
|
|
3823
|
+
missions = missions.map(missionStatusView);
|
|
3824
|
+
const findings = [];
|
|
3825
|
+
const add = (mission, code, message, severity = 'high', extra = {}) => {
|
|
3826
|
+
findings.push({
|
|
3827
|
+
severity,
|
|
3828
|
+
code,
|
|
3829
|
+
mission_id: mission.id,
|
|
3830
|
+
owner: mission.owner,
|
|
3831
|
+
status: mission.status,
|
|
3832
|
+
objective: mission.objective,
|
|
3833
|
+
message,
|
|
3834
|
+
next: extra.next || `atris mission status ${mission.id}`,
|
|
3835
|
+
...extra,
|
|
3836
|
+
});
|
|
3837
|
+
};
|
|
3838
|
+
|
|
3839
|
+
for (const mission of missions) {
|
|
3840
|
+
const status = String(mission.status || '');
|
|
3841
|
+
const active = !TERMINAL_STATUSES.has(status);
|
|
3842
|
+
const objective = String(mission.objective || '').trim();
|
|
3843
|
+
if (active && !effectiveMissionVerifier(mission)) {
|
|
3844
|
+
add(
|
|
3845
|
+
mission,
|
|
3846
|
+
'missing_verifier',
|
|
3847
|
+
'Mission has no verifier, so it cannot prove done work.',
|
|
3848
|
+
'high',
|
|
3849
|
+
{ next: `atris mission start "${objective}" --owner ${mission.owner} --verify "<cmd>"` },
|
|
3850
|
+
);
|
|
3851
|
+
}
|
|
3852
|
+
if (active && /^(?:--)?help$/i.test(objective)) {
|
|
3853
|
+
add(
|
|
3854
|
+
mission,
|
|
3855
|
+
'accidental_help_mission',
|
|
3856
|
+
'Looks like a help flag became a mission.',
|
|
3857
|
+
'medium',
|
|
3858
|
+
{ next: `atris mission stop ${mission.id} --reason "accidental help mission"` },
|
|
3859
|
+
);
|
|
3860
|
+
}
|
|
3861
|
+
if (status === 'ready') {
|
|
3862
|
+
const receiptRoot = mission.worktree_root || root;
|
|
3863
|
+
const health = missionReceiptHealth(mission, receiptRoot);
|
|
3864
|
+
if (!health.ok) {
|
|
3865
|
+
add(
|
|
3866
|
+
mission,
|
|
3867
|
+
'stale_ready_receipt',
|
|
3868
|
+
`Ready mission does not have a fresh passing receipt (${health.reason}).`,
|
|
3869
|
+
'high',
|
|
3870
|
+
{
|
|
3871
|
+
receipt_path: health.receipt_path || mission.receipt_path || null,
|
|
3872
|
+
receipt_reason: health.reason,
|
|
3873
|
+
next: `atris mission tick ${mission.id} --verify`,
|
|
3874
|
+
},
|
|
3875
|
+
);
|
|
3876
|
+
}
|
|
3877
|
+
}
|
|
3878
|
+
if (mission.always_on === true && status === 'blocked') {
|
|
3879
|
+
add(
|
|
3880
|
+
mission,
|
|
3881
|
+
'blocked_always_on_loop',
|
|
3882
|
+
'Always-on mission is blocked and will not keep improving.',
|
|
3883
|
+
'high',
|
|
3884
|
+
{ next: `atris mission run ${mission.id} --max-ticks 1` },
|
|
3885
|
+
);
|
|
3886
|
+
}
|
|
3887
|
+
}
|
|
3888
|
+
|
|
3889
|
+
findings.sort((a, b) => {
|
|
3890
|
+
const rank = { high: 0, medium: 1, low: 2 };
|
|
3891
|
+
return (rank[a.severity] ?? 9) - (rank[b.severity] ?? 9)
|
|
3892
|
+
|| String(a.code).localeCompare(String(b.code))
|
|
3893
|
+
|| String(a.objective).localeCompare(String(b.objective));
|
|
3894
|
+
});
|
|
3895
|
+
return { missions, findings };
|
|
3896
|
+
}
|
|
3897
|
+
|
|
3898
|
+
function doctorMission(args) {
|
|
3899
|
+
const asJson = wantsJson(args);
|
|
3900
|
+
const localOnly = hasFlag(args, '--local');
|
|
3901
|
+
const { missions, findings } = collectMissionDoctorFindings(process.cwd(), { localOnly });
|
|
3902
|
+
const payload = {
|
|
3903
|
+
ok: findings.length === 0,
|
|
3904
|
+
action: 'mission_doctor',
|
|
3905
|
+
checked_count: missions.length,
|
|
3906
|
+
finding_count: findings.length,
|
|
3907
|
+
findings,
|
|
3908
|
+
};
|
|
3909
|
+
const lines = findings.length
|
|
3910
|
+
? [
|
|
3911
|
+
`Mission doctor: ${findings.length} problem(s) across ${missions.length} mission(s)`,
|
|
3912
|
+
...findings.map((finding) => `${finding.severity.toUpperCase()} ${finding.code}: ${finding.objective} -> ${finding.next}`),
|
|
3913
|
+
]
|
|
3914
|
+
: [`Mission doctor: clean (${missions.length} mission(s) checked)`];
|
|
3915
|
+
printJsonOrText(payload, lines, asJson);
|
|
3916
|
+
if (findings.length) process.exitCode = 1;
|
|
3917
|
+
}
|
|
3918
|
+
|
|
1408
3919
|
// `atris mission watch [id]` — read-only live heartbeat. Prints a line per tick as it
|
|
1409
3920
|
// lands so a human (or any terminal) can see the loop is alive without rerunning status.
|
|
1410
3921
|
function watchMission(args) {
|
|
@@ -1472,11 +3983,8 @@ function writeReceipt(mission, result, root = process.cwd()) {
|
|
|
1472
3983
|
fs.mkdirSync(paths.runsDir, { recursive: true });
|
|
1473
3984
|
const safeTime = stampIso().replace(/[:.]/g, '-');
|
|
1474
3985
|
const receiptPath = path.join(paths.runsDir, `mission-${mission.id}-${safeTime}.json`);
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
const finalResult = (result && typeof result === 'object' && result.verifier_result && !('passed' in result))
|
|
1478
|
-
? { ...result, passed: !!result.verifier_result.passed }
|
|
1479
|
-
: result;
|
|
3986
|
+
const relativeReceiptPath = path.relative(root, receiptPath);
|
|
3987
|
+
const finalResult = normalizeMissionReceiptResult(mission, result, relativeReceiptPath);
|
|
1480
3988
|
fs.writeFileSync(receiptPath, JSON.stringify({
|
|
1481
3989
|
schema: 'atris.mission_receipt.v1',
|
|
1482
3990
|
mission_id: mission.id,
|
|
@@ -1486,7 +3994,7 @@ function writeReceipt(mission, result, root = process.cwd()) {
|
|
|
1486
3994
|
verifier: mission.verifier || null,
|
|
1487
3995
|
result: finalResult,
|
|
1488
3996
|
}, null, 2) + '\n', 'utf8');
|
|
1489
|
-
return
|
|
3997
|
+
return relativeReceiptPath;
|
|
1490
3998
|
}
|
|
1491
3999
|
|
|
1492
4000
|
function shellQuote(value) {
|
|
@@ -1721,11 +4229,19 @@ function runnerUsesCallerSession(runner) {
|
|
|
1721
4229
|
return new Set(['codex_goal', 'caller_session', 'current_agent']).has(String(runner || '').trim().toLowerCase());
|
|
1722
4230
|
}
|
|
1723
4231
|
|
|
4232
|
+
function isCodexGoalMission(mission) {
|
|
4233
|
+
return String(mission?.runner || '').trim().toLowerCase() === 'codex_goal';
|
|
4234
|
+
}
|
|
4235
|
+
|
|
1724
4236
|
function nextCandidateTickAction(mission) {
|
|
1725
4237
|
const completeFlag = mission.always_on ? '' : ' --complete-on-pass';
|
|
1726
4238
|
return `next move: run atris mission run ${mission.id}${completeFlag}`;
|
|
1727
4239
|
}
|
|
1728
4240
|
|
|
4241
|
+
function nextCandidateRunCommand(mission) {
|
|
4242
|
+
return `atris mission run ${mission.id}`;
|
|
4243
|
+
}
|
|
4244
|
+
|
|
1729
4245
|
function missionVerifierPassed(mission) {
|
|
1730
4246
|
return (mission && mission.verifier_result && mission.verifier_result.passed) === true;
|
|
1731
4247
|
}
|
|
@@ -1768,7 +4284,7 @@ function missionHeartbeatLines(mission, now = new Date()) {
|
|
|
1768
4284
|
const lastTickAt = mission.last_tick_at ? Date.parse(mission.last_tick_at) : NaN;
|
|
1769
4285
|
if (Number.isFinite(lastTickAt)) {
|
|
1770
4286
|
const age = formatDurationShort((now.getTime() - lastTickAt) / 1000);
|
|
1771
|
-
const verifier = mission
|
|
4287
|
+
const verifier = effectiveMissionVerifier(mission)
|
|
1772
4288
|
? (mission.verifier_result ? (mission.verifier_result.passed ? 'verifier passed' : 'verifier failed') : 'verifier not run')
|
|
1773
4289
|
: 'no verifier';
|
|
1774
4290
|
const tickIdx = mission.last_tick_index != null ? `#${mission.last_tick_index}, ` : '';
|
|
@@ -1789,6 +4305,23 @@ function missionHasHumanAsks(mission) {
|
|
|
1789
4305
|
&& mission.human_asks.some((ask) => String(ask || '').trim());
|
|
1790
4306
|
}
|
|
1791
4307
|
|
|
4308
|
+
function missionTaskHumanAcceptWaiting(mission) {
|
|
4309
|
+
const taskId = missionTaskSpine(mission)?.task_id;
|
|
4310
|
+
if (!taskId) return false;
|
|
4311
|
+
try {
|
|
4312
|
+
const taskDb = require('../lib/task-db');
|
|
4313
|
+
const db = taskDb.open();
|
|
4314
|
+
const task = taskDb.getTask(db, taskId);
|
|
4315
|
+
if (!task) return false;
|
|
4316
|
+
const metadata = task.metadata || {};
|
|
4317
|
+
return task.status === 'review'
|
|
4318
|
+
&& metadata.approval_status === 'pending'
|
|
4319
|
+
&& metadata.agent_certified === true;
|
|
4320
|
+
} catch {
|
|
4321
|
+
return false;
|
|
4322
|
+
}
|
|
4323
|
+
}
|
|
4324
|
+
|
|
1792
4325
|
function missionIsRunnable(mission) {
|
|
1793
4326
|
return mission
|
|
1794
4327
|
&& GOAL_LOOP_STATUSES.has(String(mission.status || ''))
|
|
@@ -1802,7 +4335,8 @@ function missionSortTime(mission) {
|
|
|
1802
4335
|
function selectDueMission(root = process.cwd(), now = new Date()) {
|
|
1803
4336
|
const candidates = listMissions(root)
|
|
1804
4337
|
.filter((mission) => missionSelectableForLoop(mission, now))
|
|
1805
|
-
.filter((mission) => mission
|
|
4338
|
+
.filter((mission) => !missionTaskHumanAcceptWaiting(mission))
|
|
4339
|
+
.filter((mission) => effectiveMissionVerifier(mission) || callerSessionMissionReadyForDue(mission))
|
|
1806
4340
|
.filter((mission) => mission.always_on || !missionVerifierPassed(mission))
|
|
1807
4341
|
.filter((mission) => missionDueAt(mission, now));
|
|
1808
4342
|
|
|
@@ -1811,82 +4345,423 @@ function selectDueMission(root = process.cwd(), now = new Date()) {
|
|
|
1811
4345
|
const bCaller = runnerUsesCallerSession(b.runner) ? 1 : 0;
|
|
1812
4346
|
if (aCaller !== bCaller) return bCaller - aCaller;
|
|
1813
4347
|
|
|
1814
|
-
const aTime = missionSortTime(a);
|
|
1815
|
-
const bTime = missionSortTime(b);
|
|
1816
|
-
return bTime - aTime;
|
|
4348
|
+
const aTime = missionSortTime(a);
|
|
4349
|
+
const bTime = missionSortTime(b);
|
|
4350
|
+
return bTime - aTime;
|
|
4351
|
+
});
|
|
4352
|
+
return candidates[0] || null;
|
|
4353
|
+
}
|
|
4354
|
+
|
|
4355
|
+
function callerSessionMissionReadyForDue(mission) {
|
|
4356
|
+
return runnerUsesCallerSession(mission?.runner)
|
|
4357
|
+
&& mission?.always_on === true
|
|
4358
|
+
&& Boolean(codexNativeGoalAck(mission))
|
|
4359
|
+
&& missionTaskSpine(mission)?.has_task === true;
|
|
4360
|
+
}
|
|
4361
|
+
|
|
4362
|
+
function missionSelectableForCodexGoal(mission, now = new Date()) {
|
|
4363
|
+
if (!missionIsRunnable(mission)) return false;
|
|
4364
|
+
if (missionTaskHumanAcceptWaiting(mission)) return false;
|
|
4365
|
+
if (mission.always_on && missionVerifierPassed(mission) && !missionDueAt(mission, now)) {
|
|
4366
|
+
return parseCadenceSeconds(mission.cadence) > 0;
|
|
4367
|
+
}
|
|
4368
|
+
return true;
|
|
4369
|
+
}
|
|
4370
|
+
|
|
4371
|
+
function selectCodexGoalMission(root = process.cwd(), options = {}, now = new Date()) {
|
|
4372
|
+
const requestedId = String(options.missionId || '').trim();
|
|
4373
|
+
const candidates = listMissions(root)
|
|
4374
|
+
.filter((mission) => runnerUsesCallerSession(mission.runner))
|
|
4375
|
+
.filter((mission) => missionSelectableForCodexGoal(mission, now));
|
|
4376
|
+
if (requestedId) {
|
|
4377
|
+
const exact = candidates.find((mission) => mission.id === requestedId || mission.id.startsWith(requestedId));
|
|
4378
|
+
if (exact) {
|
|
4379
|
+
const due = effectiveMissionVerifier(exact) && missionDueAt(exact, now);
|
|
4380
|
+
return { mission: exact, reason: due ? 'due' : 'selected' };
|
|
4381
|
+
}
|
|
4382
|
+
}
|
|
4383
|
+
|
|
4384
|
+
candidates.sort((a, b) => {
|
|
4385
|
+
const aCaller = runnerUsesCallerSession(a.runner) ? 1 : 0;
|
|
4386
|
+
const bCaller = runnerUsesCallerSession(b.runner) ? 1 : 0;
|
|
4387
|
+
if (aCaller !== bCaller) return bCaller - aCaller;
|
|
4388
|
+
|
|
4389
|
+
const aVerifier = effectiveMissionVerifier(a) ? 1 : 0;
|
|
4390
|
+
const bVerifier = effectiveMissionVerifier(b) ? 1 : 0;
|
|
4391
|
+
if (aVerifier !== bVerifier) return bVerifier - aVerifier;
|
|
4392
|
+
|
|
4393
|
+
const aTime = missionSortTime(a);
|
|
4394
|
+
const bTime = missionSortTime(b);
|
|
4395
|
+
return bTime - aTime;
|
|
4396
|
+
});
|
|
4397
|
+
|
|
4398
|
+
const mission = candidates[0] || null;
|
|
4399
|
+
if (!mission) return null;
|
|
4400
|
+
const due = effectiveMissionVerifier(mission) && missionDueAt(mission, now);
|
|
4401
|
+
return { mission, reason: due ? 'due' : 'active' };
|
|
4402
|
+
}
|
|
4403
|
+
|
|
4404
|
+
function selectAtrisGoalMission(root = process.cwd(), options = {}, now = new Date()) {
|
|
4405
|
+
const requestedId = String(options.missionId || '').trim();
|
|
4406
|
+
const runnable = listMissions(root).filter((mission) => missionIsRunnable(mission));
|
|
4407
|
+
if (requestedId) {
|
|
4408
|
+
const exact = runnable.find((mission) => mission.id === requestedId || mission.id.startsWith(requestedId));
|
|
4409
|
+
if (exact) return { mission: exact, reason: 'selected' };
|
|
4410
|
+
}
|
|
4411
|
+
runnable.sort((a, b) => {
|
|
4412
|
+
const aDue = effectiveMissionVerifier(a) && missionDueAt(a, now) ? 1 : 0;
|
|
4413
|
+
const bDue = effectiveMissionVerifier(b) && missionDueAt(b, now) ? 1 : 0;
|
|
4414
|
+
if (aDue !== bDue) return bDue - aDue;
|
|
4415
|
+
|
|
4416
|
+
const aTime = missionSortTime(a);
|
|
4417
|
+
const bTime = missionSortTime(b);
|
|
4418
|
+
return bTime - aTime;
|
|
4419
|
+
});
|
|
4420
|
+
const mission = runnable[0] || null;
|
|
4421
|
+
if (!mission) return null;
|
|
4422
|
+
const due = effectiveMissionVerifier(mission) && missionDueAt(mission, now);
|
|
4423
|
+
return { mission, reason: due ? 'due' : 'active' };
|
|
4424
|
+
}
|
|
4425
|
+
|
|
4426
|
+
function codexGoalObjective(mission) {
|
|
4427
|
+
return mission.objective;
|
|
4428
|
+
}
|
|
4429
|
+
|
|
4430
|
+
function codexNativeGoalAck(mission, objective = codexGoalObjective(mission)) {
|
|
4431
|
+
const ack = mission?.native_goal_ack || null;
|
|
4432
|
+
if (!ack || String(ack.runtime || '').toLowerCase() !== 'codex') return null;
|
|
4433
|
+
if (ack.status !== 'active') return null;
|
|
4434
|
+
if (String(ack.objective || '') !== String(objective || '')) return null;
|
|
4435
|
+
return ack;
|
|
4436
|
+
}
|
|
4437
|
+
|
|
4438
|
+
function codexRuntimeGoalStateFromOptions(options = {}) {
|
|
4439
|
+
const status = String(options.nativeGoalStatus || options.visibleGoalStatus || '').trim().toLowerCase();
|
|
4440
|
+
const objective = String(options.nativeGoalObjective || options.visibleGoalObjective || '').trim();
|
|
4441
|
+
if (!status && !objective) return null;
|
|
4442
|
+
return {
|
|
4443
|
+
status: status || null,
|
|
4444
|
+
objective: objective || null,
|
|
4445
|
+
};
|
|
4446
|
+
}
|
|
4447
|
+
|
|
4448
|
+
function codexRuntimeStateBlocksMissionSlot(runtimeGoalState, mission) {
|
|
4449
|
+
if (!runtimeGoalState) return true;
|
|
4450
|
+
if (runtimeGoalState.status && !new Set(['active', 'paused']).has(runtimeGoalState.status)) return false;
|
|
4451
|
+
if (!runtimeGoalState.objective) return new Set(['active', 'paused']).has(runtimeGoalState.status);
|
|
4452
|
+
return runtimeGoalState.objective === codexGoalObjective(mission);
|
|
4453
|
+
}
|
|
4454
|
+
|
|
4455
|
+
function codexGoalAckCommand(mission, objective = codexGoalObjective(mission)) {
|
|
4456
|
+
return `atris mission goal ack ${mission.id} --runtime codex --status active --objective ${shellQuote(objective)} --json`;
|
|
4457
|
+
}
|
|
4458
|
+
|
|
4459
|
+
function codexNativeGoalReplaceAction(newMission, activeMission, runtimeGoalState = null, commands = {}) {
|
|
4460
|
+
const fromObjective = codexGoalObjective(activeMission);
|
|
4461
|
+
const toObjective = codexGoalObjective(newMission);
|
|
4462
|
+
return {
|
|
4463
|
+
runtime: 'codex',
|
|
4464
|
+
tool: 'replace_goal',
|
|
4465
|
+
available: false,
|
|
4466
|
+
blocked_by: 'codex_runtime_missing_replace_goal_tool',
|
|
4467
|
+
args: {
|
|
4468
|
+
objective: toObjective,
|
|
4469
|
+
from_objective: fromObjective,
|
|
4470
|
+
to_objective: toObjective,
|
|
4471
|
+
from_mission_id: activeMission.id,
|
|
4472
|
+
to_mission_id: newMission.id,
|
|
4473
|
+
current_status: runtimeGoalState?.status || null,
|
|
4474
|
+
},
|
|
4475
|
+
after_success: {
|
|
4476
|
+
ack_new_mission: commands.ack_new_mission || codexGoalAckCommand(newMission, toObjective),
|
|
4477
|
+
},
|
|
4478
|
+
fallback: {
|
|
4479
|
+
reason: 'This Codex runtime exposes get_goal/create_goal/update_goal but not replace_goal or resume_goal.',
|
|
4480
|
+
commands,
|
|
4481
|
+
},
|
|
4482
|
+
};
|
|
4483
|
+
}
|
|
4484
|
+
|
|
4485
|
+
function codexNativeGoalRuntimeReplaceAction(newMission, runtimeGoalState = null, commands = {}) {
|
|
4486
|
+
const toObjective = codexGoalObjective(newMission);
|
|
4487
|
+
const fromObjective = runtimeGoalState?.objective || null;
|
|
4488
|
+
const ackNewMission = commands.ack_new_mission || codexGoalAckCommand(newMission, toObjective);
|
|
4489
|
+
const completeCurrentGoal = 'update_goal({ status: "complete" })';
|
|
4490
|
+
const createNewGoal = `create_goal({ objective: ${JSON.stringify(toObjective)} })`;
|
|
4491
|
+
const supersedeApproved = commands.allow_native_goal_supersede === true;
|
|
4492
|
+
return {
|
|
4493
|
+
runtime: 'codex',
|
|
4494
|
+
tool: 'replace_goal',
|
|
4495
|
+
available: false,
|
|
4496
|
+
blocked_by: 'codex_runtime_missing_replace_goal_tool',
|
|
4497
|
+
args: {
|
|
4498
|
+
objective: toObjective,
|
|
4499
|
+
from_objective: fromObjective,
|
|
4500
|
+
to_objective: toObjective,
|
|
4501
|
+
from_mission_id: null,
|
|
4502
|
+
to_mission_id: newMission.id,
|
|
4503
|
+
current_status: runtimeGoalState?.status || null,
|
|
4504
|
+
},
|
|
4505
|
+
after_success: {
|
|
4506
|
+
ack_new_mission: ackNewMission,
|
|
4507
|
+
},
|
|
4508
|
+
fallback: {
|
|
4509
|
+
reason: 'This Codex runtime exposes get_goal/create_goal/update_goal but not replace_goal or resume_goal.',
|
|
4510
|
+
automatic: supersedeApproved,
|
|
4511
|
+
approved: supersedeApproved,
|
|
4512
|
+
executable_now: supersedeApproved,
|
|
4513
|
+
blocked_by: supersedeApproved ? null : 'native_goal_cancel_or_supersede_tool_missing',
|
|
4514
|
+
safe_when: 'Use only when a mission handoff proves the paused goal is intentionally superseded; update_goal complete otherwise misrepresents abandoned work as finished.',
|
|
4515
|
+
sequence_name: 'complete_paused_goal_then_create_new_goal',
|
|
4516
|
+
sequence: [
|
|
4517
|
+
completeCurrentGoal,
|
|
4518
|
+
createNewGoal,
|
|
4519
|
+
ackNewMission,
|
|
4520
|
+
],
|
|
4521
|
+
commands: {
|
|
4522
|
+
...commands,
|
|
4523
|
+
complete_current_goal: completeCurrentGoal,
|
|
4524
|
+
create_new_goal: createNewGoal,
|
|
4525
|
+
ack_new_mission: ackNewMission,
|
|
4526
|
+
},
|
|
4527
|
+
},
|
|
4528
|
+
};
|
|
4529
|
+
}
|
|
4530
|
+
|
|
4531
|
+
function codexRuntimeGoalNeedsReplace(runtimeGoalState, objective) {
|
|
4532
|
+
if (!runtimeGoalState) return false;
|
|
4533
|
+
if (runtimeGoalState.status !== 'paused') return false;
|
|
4534
|
+
if (!runtimeGoalState.objective) return true;
|
|
4535
|
+
return runtimeGoalState.objective !== objective;
|
|
4536
|
+
}
|
|
4537
|
+
|
|
4538
|
+
function writeDirectRunCodexGoalRequest(mission, root = process.cwd()) {
|
|
4539
|
+
if (!isCodexGoalMission(mission)) return null;
|
|
4540
|
+
const request = {
|
|
4541
|
+
schema: 'atris.codex_goal_direct_run_request.v1',
|
|
4542
|
+
source: 'mission_run_objective',
|
|
4543
|
+
mission_id: mission.id,
|
|
4544
|
+
objective: codexGoalObjective(mission),
|
|
4545
|
+
mission_run_preflight: mission.mission_run_preflight || null,
|
|
4546
|
+
requested_at: stampIso(),
|
|
4547
|
+
};
|
|
4548
|
+
const paths = statePaths(root);
|
|
4549
|
+
fs.mkdirSync(path.dirname(paths.codexGoalRequestJson), { recursive: true });
|
|
4550
|
+
fs.writeFileSync(paths.codexGoalRequestJson, JSON.stringify(request, null, 2) + '\n', 'utf8');
|
|
4551
|
+
return request;
|
|
4552
|
+
}
|
|
4553
|
+
|
|
4554
|
+
function readDirectRunCodexGoalRequest(root = process.cwd(), now = new Date()) {
|
|
4555
|
+
const file = statePaths(root).codexGoalRequestJson;
|
|
4556
|
+
let request = null;
|
|
4557
|
+
try {
|
|
4558
|
+
request = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
4559
|
+
} catch {
|
|
4560
|
+
return null;
|
|
4561
|
+
}
|
|
4562
|
+
const missionId = String(request?.mission_id || '').trim();
|
|
4563
|
+
if (!missionId) return null;
|
|
4564
|
+
const mission = resolveMission(missionId, root);
|
|
4565
|
+
if (!mission || !isCodexGoalMission(mission)) return null;
|
|
4566
|
+
if (!missionSelectableForCodexGoal(mission, now)) return null;
|
|
4567
|
+
return { request, mission };
|
|
4568
|
+
}
|
|
4569
|
+
|
|
4570
|
+
function clearDirectRunCodexGoalRequestForMission(missionId, root = process.cwd()) {
|
|
4571
|
+
const file = statePaths(root).codexGoalRequestJson;
|
|
4572
|
+
let request = null;
|
|
4573
|
+
try {
|
|
4574
|
+
request = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
4575
|
+
} catch {
|
|
4576
|
+
return false;
|
|
4577
|
+
}
|
|
4578
|
+
if (String(request?.mission_id || '') !== String(missionId || '')) return false;
|
|
4579
|
+
try {
|
|
4580
|
+
fs.rmSync(file, { force: true });
|
|
4581
|
+
return true;
|
|
4582
|
+
} catch {
|
|
4583
|
+
return false;
|
|
4584
|
+
}
|
|
4585
|
+
}
|
|
4586
|
+
|
|
4587
|
+
function activeCodexVisibleGoalOwner(root = process.cwd(), excludeId = '', now = new Date(), runtimeGoalState = null) {
|
|
4588
|
+
const excluded = String(excludeId || '');
|
|
4589
|
+
const candidates = listMissions(root)
|
|
4590
|
+
.filter((mission) => mission.id !== excluded)
|
|
4591
|
+
.filter((mission) => isCodexGoalMission(mission))
|
|
4592
|
+
.filter((mission) => missionSelectableForCodexGoal(mission, now))
|
|
4593
|
+
.filter((mission) => Boolean(codexNativeGoalAck(mission)))
|
|
4594
|
+
.filter((mission) => codexRuntimeStateBlocksMissionSlot(runtimeGoalState, mission));
|
|
4595
|
+
candidates.sort((a, b) => {
|
|
4596
|
+
const aAck = Date.parse(a.native_goal_ack?.acknowledged_at || '') || 0;
|
|
4597
|
+
const bAck = Date.parse(b.native_goal_ack?.acknowledged_at || '') || 0;
|
|
4598
|
+
if (aAck !== bAck) return bAck - aAck;
|
|
4599
|
+
return missionSortTime(b) - missionSortTime(a);
|
|
1817
4600
|
});
|
|
1818
4601
|
return candidates[0] || null;
|
|
1819
4602
|
}
|
|
1820
4603
|
|
|
1821
|
-
function
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
4604
|
+
function codexGoalActiveConflictPayload(newMission, activeMission, request = null, heartbeatMode = false, runtimeGoalState = null) {
|
|
4605
|
+
const newObjective = codexGoalObjective(newMission);
|
|
4606
|
+
const pausedConflict = runtimeGoalState?.status === 'paused';
|
|
4607
|
+
const holdOldMission = `atris mission pause ${activeMission.id} --reason ${shellQuote(`visible goal replaced by ${newMission.id}`)} --json`;
|
|
4608
|
+
const ackNewMission = codexGoalAckCommand(newMission, newObjective);
|
|
4609
|
+
const completeOldMission = `atris mission complete ${activeMission.id} --proof "<proof>" --json`;
|
|
4610
|
+
const refreshAfterResume = `atris mission goal --native-goal-status active --native-goal-objective ${shellQuote(codexGoalObjective(activeMission))} --json`;
|
|
4611
|
+
const nextCommand = pausedConflict
|
|
4612
|
+
? `Resume the paused Codex goal, then run ${refreshAfterResume}`
|
|
4613
|
+
: `${holdOldMission} && ${ackNewMission}`;
|
|
4614
|
+
const commands = {
|
|
4615
|
+
hold_old_mission: holdOldMission,
|
|
4616
|
+
complete_old_mission: completeOldMission,
|
|
4617
|
+
refresh_after_resume: refreshAfterResume,
|
|
4618
|
+
ack_new_mission: ackNewMission,
|
|
4619
|
+
};
|
|
4620
|
+
const nativeGoalAction = codexNativeGoalReplaceAction(newMission, activeMission, runtimeGoalState, commands);
|
|
4621
|
+
const nativeGoalResolution = {
|
|
4622
|
+
action: 'replace_visible_goal',
|
|
4623
|
+
automatic_decision: true,
|
|
4624
|
+
executable_now: false,
|
|
4625
|
+
blocked_by: nativeGoalAction.blocked_by,
|
|
4626
|
+
required_tool: nativeGoalAction.tool,
|
|
4627
|
+
from_mission_id: activeMission.id,
|
|
4628
|
+
to_mission_id: newMission.id,
|
|
4629
|
+
};
|
|
4630
|
+
const conflict = {
|
|
4631
|
+
schema: 'atris.codex_active_goal_conflict.v1',
|
|
4632
|
+
message: pausedConflict
|
|
4633
|
+
? `new mission created, but old mission ${activeMission.id} is paused and still occupies the visible slot.`
|
|
4634
|
+
: `new mission created, but old mission ${activeMission.id} still owns the visible slot.`,
|
|
4635
|
+
source: request?.source || 'mission_run_objective',
|
|
4636
|
+
status: pausedConflict ? 'paused_goal_conflict' : 'active_goal_conflict',
|
|
4637
|
+
new_mission_id: newMission.id,
|
|
4638
|
+
new_objective: newObjective,
|
|
4639
|
+
active_mission_id: activeMission.id,
|
|
4640
|
+
active_objective: codexGoalObjective(activeMission),
|
|
4641
|
+
next_command: nextCommand,
|
|
4642
|
+
native_goal_action: nativeGoalAction,
|
|
4643
|
+
native_goal_resolution: nativeGoalResolution,
|
|
4644
|
+
commands,
|
|
4645
|
+
new_mission: missionStatusView(newMission),
|
|
4646
|
+
active_mission: missionStatusView(activeMission),
|
|
4647
|
+
request,
|
|
4648
|
+
runtime_goal_state: runtimeGoalState,
|
|
4649
|
+
};
|
|
4650
|
+
return {
|
|
4651
|
+
ok: true,
|
|
4652
|
+
action: pausedConflict ? 'paused_goal_conflict' : 'active_goal_conflict',
|
|
4653
|
+
active_goal_conflict: conflict,
|
|
4654
|
+
mission: conflict.new_mission,
|
|
4655
|
+
conflicting_mission: conflict.active_mission,
|
|
4656
|
+
goal: null,
|
|
4657
|
+
heartbeat: heartbeatMode ? { heavy_work_performed: false, next_heavy_command: nextCommand } : undefined,
|
|
4658
|
+
requires_native_goal_start: true,
|
|
4659
|
+
requires_native_goal_replace: true,
|
|
4660
|
+
native_goal_action: nativeGoalAction,
|
|
4661
|
+
native_goal_resolution: nativeGoalResolution,
|
|
4662
|
+
next_command: nextCommand,
|
|
4663
|
+
};
|
|
1827
4664
|
}
|
|
1828
4665
|
|
|
1829
|
-
function
|
|
1830
|
-
|
|
1831
|
-
|
|
4666
|
+
function codexNativeGoalAction(mission, objective = codexGoalObjective(mission)) {
|
|
4667
|
+
return {
|
|
4668
|
+
runtime: 'codex',
|
|
4669
|
+
tool: 'create_goal',
|
|
4670
|
+
args: { objective },
|
|
4671
|
+
};
|
|
4672
|
+
}
|
|
1832
4673
|
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
if (aCaller !== bCaller) return bCaller - aCaller;
|
|
4674
|
+
function codexNativeGoalStartInstruction(mission, objective = codexGoalObjective(mission)) {
|
|
4675
|
+
return `Call native Codex create_goal({ objective: ${JSON.stringify(objective)} }), then run ${codexGoalAckCommand(mission, objective)}`;
|
|
4676
|
+
}
|
|
1837
4677
|
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
4678
|
+
function codexNativeGoalReplaceInstruction(mission, runtimeGoalState = null, objective = codexGoalObjective(mission), options = {}) {
|
|
4679
|
+
const fromObjective = runtimeGoalState?.objective
|
|
4680
|
+
? ` from paused objective ${JSON.stringify(runtimeGoalState.objective)}`
|
|
4681
|
+
: '';
|
|
4682
|
+
if (options.allowNativeGoalSupersede === true) {
|
|
4683
|
+
return `Supersede approved: run update_goal({ status: "complete" }), then create_goal({ objective: ${JSON.stringify(objective)} }), then run ${codexGoalAckCommand(mission, objective)}. Atris records the old paused goal as superseded.`;
|
|
4684
|
+
}
|
|
4685
|
+
return `Native Codex replace_goal is required${fromObjective} to ${JSON.stringify(objective)}, then run ${codexGoalAckCommand(mission, objective)}; this runtime currently lacks replace_goal. Fallback is update_goal({ status: "complete" }) -> create_goal({ objective: ${JSON.stringify(objective)} }) -> mission goal ack only after handoff proof says the paused goal is intentionally superseded.`;
|
|
4686
|
+
}
|
|
1841
4687
|
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
4688
|
+
function codexNativeGoalBlockPayload(mission) {
|
|
4689
|
+
const objective = codexGoalObjective(mission);
|
|
4690
|
+
return {
|
|
4691
|
+
ok: false,
|
|
4692
|
+
code: 'native_goal_not_started',
|
|
4693
|
+
mission_id: mission.id,
|
|
4694
|
+
objective,
|
|
4695
|
+
requires_native_goal_start: true,
|
|
4696
|
+
native_goal_action: codexNativeGoalAction(mission, objective),
|
|
4697
|
+
native_goal_ack_command: codexGoalAckCommand(mission, objective),
|
|
4698
|
+
next_action: codexNativeGoalStartInstruction(mission, objective),
|
|
4699
|
+
};
|
|
4700
|
+
}
|
|
1846
4701
|
|
|
1847
|
-
|
|
1848
|
-
if (!mission) return
|
|
1849
|
-
const
|
|
1850
|
-
|
|
4702
|
+
function maybeBlockUntilCodexNativeGoalStarted(mission, asJson) {
|
|
4703
|
+
if (!isCodexGoalMission(mission) || codexNativeGoalAck(mission)) return;
|
|
4704
|
+
const payload = codexNativeGoalBlockPayload(mission);
|
|
4705
|
+
if (asJson) console.log(JSON.stringify(payload, null, 2));
|
|
4706
|
+
else console.error(payload.next_action);
|
|
4707
|
+
process.exit(2);
|
|
1851
4708
|
}
|
|
1852
4709
|
|
|
1853
|
-
function
|
|
1854
|
-
return
|
|
4710
|
+
function returnIfCodexNativeGoalNotStarted(mission, asJson) {
|
|
4711
|
+
if (!isCodexGoalMission(mission) || codexNativeGoalAck(mission)) return false;
|
|
4712
|
+
const payload = codexNativeGoalBlockPayload(mission);
|
|
4713
|
+
if (asJson) console.log(JSON.stringify(payload, null, 2));
|
|
4714
|
+
else console.error(payload.next_action);
|
|
4715
|
+
process.exitCode = 2;
|
|
4716
|
+
return true;
|
|
1855
4717
|
}
|
|
1856
4718
|
|
|
1857
4719
|
function codexGoalNextCommand(mission) {
|
|
4720
|
+
if (isCodexGoalMission(mission) && !codexNativeGoalAck(mission)) {
|
|
4721
|
+
return codexNativeGoalStartInstruction(mission);
|
|
4722
|
+
}
|
|
1858
4723
|
const taskSpine = missionTaskSpine(mission);
|
|
1859
4724
|
if (taskSpine && !taskSpine.has_task && taskSpine.ensure_task_command) {
|
|
1860
4725
|
return taskSpine.ensure_task_command;
|
|
1861
4726
|
}
|
|
4727
|
+
if (missionChoosesNextMission(mission)) {
|
|
4728
|
+
return chooseNextMissionCommand(mission);
|
|
4729
|
+
}
|
|
4730
|
+
if (mission.status === 'ready' && mission.always_on) {
|
|
4731
|
+
return nextCandidateRunCommand(mission);
|
|
4732
|
+
}
|
|
1862
4733
|
if (mission.status === 'ready') {
|
|
1863
4734
|
const xpAction = missionXpReadyAction(mission, mission.receipt_path);
|
|
1864
4735
|
if (xpAction) return xpAction.replace(/^queue AgentXP review: /, '');
|
|
1865
4736
|
}
|
|
1866
|
-
|
|
4737
|
+
const verifier = effectiveMissionVerifier(mission);
|
|
4738
|
+
if (verifier && missionDueAt(mission)) {
|
|
1867
4739
|
const completeFlag = mission.always_on ? '' : ' --complete-on-pass';
|
|
1868
4740
|
return `atris mission run --due --max-ticks 1${completeFlag}`;
|
|
1869
4741
|
}
|
|
1870
|
-
if (
|
|
4742
|
+
if (verifier) {
|
|
1871
4743
|
return `atris mission tick ${mission.id} --verify --summary "<what changed>"`;
|
|
1872
4744
|
}
|
|
1873
4745
|
return `atris mission tick ${mission.id} --summary "<what changed>"`;
|
|
1874
4746
|
}
|
|
1875
4747
|
|
|
1876
4748
|
function codexVisibleGoalBridge(mission, goalObjective) {
|
|
4749
|
+
const ack = codexNativeGoalAck(mission, goalObjective);
|
|
1877
4750
|
return {
|
|
1878
4751
|
schema: 'atris.visible_chat_goal_bridge.v1',
|
|
1879
4752
|
runtime: 'codex',
|
|
1880
4753
|
source: 'atris_mission',
|
|
1881
4754
|
mission_id: mission.id,
|
|
1882
4755
|
desired_objective: goalObjective,
|
|
1883
|
-
status: 'needs_runtime_write',
|
|
4756
|
+
status: ack ? 'active' : 'needs_runtime_write',
|
|
4757
|
+
acknowledged_at: ack?.acknowledged_at || null,
|
|
1884
4758
|
state_file: '.atris/state/codex_goal.json',
|
|
1885
4759
|
status_file: 'atris/status/codex-goal.md',
|
|
1886
4760
|
operations: {
|
|
1887
4761
|
read_current_goal: 'get_goal',
|
|
1888
4762
|
keep_if_matching: 'if current goal objective equals goal.objective, continue the mission',
|
|
1889
4763
|
create_when_empty_or_completed: 'create_goal({ objective: goal.objective })',
|
|
4764
|
+
ack_after_create: codexGoalAckCommand(mission, goalObjective),
|
|
1890
4765
|
complete_after_proof: 'update_goal({ status: "complete" })',
|
|
1891
4766
|
refresh_on_phase_change: 'atris mission goal --json before continuing changed work',
|
|
1892
4767
|
refresh_next_candidate: 'atris mission goal --json',
|
|
@@ -1894,6 +4769,7 @@ function codexVisibleGoalBridge(mission, goalObjective) {
|
|
|
1894
4769
|
guardrails: [
|
|
1895
4770
|
'Do not complete a human-set active goal unless it matches this mission goal or the mission receipt proves handoff.',
|
|
1896
4771
|
'If create_goal fails because another goal is active, keep this bridge waiting for the visible goal slot.',
|
|
4772
|
+
'Do not run mission work for runner=codex_goal until ack_after_create has been recorded.',
|
|
1897
4773
|
'When the work changes phase or objective, refresh the mission goal before continuing.',
|
|
1898
4774
|
],
|
|
1899
4775
|
};
|
|
@@ -1909,7 +4785,7 @@ function codexGoalToolContract(mission) {
|
|
|
1909
4785
|
visible_goal_bridge: 'goal.visible_goal',
|
|
1910
4786
|
platform_requirement: 'Codex runtime must expose replace_goal/set_goal, or allow update_goal({ status: "complete" }) followed by create_goal({ objective }).',
|
|
1911
4787
|
phase_change_refresh: 'before changed follow-up work, run atris mission goal --json and mirror the returned visible goal',
|
|
1912
|
-
runtime_tool_sequence: 'get_goal -> update_goal({ status: "complete" }) after proof or phase change -> atris mission goal --json
|
|
4788
|
+
runtime_tool_sequence: 'get_goal -> create_goal({ objective }) -> atris mission goal ack <mission-id> --runtime codex --status active --objective "<objective>" --json -> do work -> update_goal({ status: "complete" }) after proof or phase change -> atris mission goal --json',
|
|
1913
4789
|
blocked_without_platform_goal_write: true,
|
|
1914
4790
|
mission_id: mission.id,
|
|
1915
4791
|
};
|
|
@@ -1957,6 +4833,15 @@ function writeCodexGoalState(payload, root = process.cwd()) {
|
|
|
1957
4833
|
lines.push(`- visible goal complete: ${state.goal.visible_goal.operations.complete_after_proof}`);
|
|
1958
4834
|
}
|
|
1959
4835
|
lines.push(`- platform write blocked: ${state.goal.codex_tool_contract.blocked_without_platform_goal_write}`);
|
|
4836
|
+
} else if (state.active_goal_conflict) {
|
|
4837
|
+
lines.push(`- conflict: ${state.active_goal_conflict.message}`);
|
|
4838
|
+
lines.push(`- new mission: ${state.active_goal_conflict.new_mission_id}`);
|
|
4839
|
+
lines.push(`- active mission: ${state.active_goal_conflict.active_mission_id}`);
|
|
4840
|
+
if (state.active_goal_conflict.native_goal_action) {
|
|
4841
|
+
lines.push(`- native goal action: ${state.active_goal_conflict.native_goal_action.tool}`);
|
|
4842
|
+
lines.push(`- native goal executable now: ${state.active_goal_conflict.native_goal_action.available !== false}`);
|
|
4843
|
+
}
|
|
4844
|
+
lines.push(`- next: ${state.active_goal_conflict.next_command}`);
|
|
1960
4845
|
} else {
|
|
1961
4846
|
lines.push('- mission: none');
|
|
1962
4847
|
}
|
|
@@ -1972,7 +4857,34 @@ function writeCodexGoalState(payload, root = process.cwd()) {
|
|
|
1972
4857
|
|
|
1973
4858
|
function buildCodexGoalPayload(root = process.cwd(), options = {}) {
|
|
1974
4859
|
const heartbeatMode = options.heartbeat === true;
|
|
1975
|
-
const
|
|
4860
|
+
const runtimeGoalState = codexRuntimeGoalStateFromOptions(options);
|
|
4861
|
+
const directRequest = options.missionId
|
|
4862
|
+
? (() => {
|
|
4863
|
+
const mission = resolveMission(options.missionId, root);
|
|
4864
|
+
return mission && isCodexGoalMission(mission) && missionSelectableForCodexGoal(mission)
|
|
4865
|
+
? { mission, request: null }
|
|
4866
|
+
: null;
|
|
4867
|
+
})()
|
|
4868
|
+
: readDirectRunCodexGoalRequest(root);
|
|
4869
|
+
const activeGoalOwner = directRequest
|
|
4870
|
+
? activeCodexVisibleGoalOwner(root, directRequest.mission.id, new Date(), runtimeGoalState)
|
|
4871
|
+
: null;
|
|
4872
|
+
if (directRequest && activeGoalOwner) {
|
|
4873
|
+
return codexGoalActiveConflictPayload(directRequest.mission, activeGoalOwner, directRequest.request, heartbeatMode, runtimeGoalState);
|
|
4874
|
+
}
|
|
4875
|
+
let selected = directRequest
|
|
4876
|
+
? { mission: directRequest.mission, reason: 'direct_run', direct_goal_request: directRequest.request }
|
|
4877
|
+
: selectCodexGoalMission(root, options);
|
|
4878
|
+
if (!selected && !directRequest) {
|
|
4879
|
+
const continuation = seedNextMoveContinuationGoal(root);
|
|
4880
|
+
if (continuation?.mission) {
|
|
4881
|
+
selected = {
|
|
4882
|
+
mission: continuation.mission,
|
|
4883
|
+
reason: continuation.inserted ? 'next_move_continuation_seeded' : 'next_move_continuation_active',
|
|
4884
|
+
seeded_continuation_goal: continuation,
|
|
4885
|
+
};
|
|
4886
|
+
}
|
|
4887
|
+
}
|
|
1976
4888
|
if (!selected) {
|
|
1977
4889
|
const heartbeat = heartbeatMode ? codexGoalHeartbeat(null, null) : undefined;
|
|
1978
4890
|
return {
|
|
@@ -1983,10 +4895,20 @@ function buildCodexGoalPayload(root = process.cwd(), options = {}) {
|
|
|
1983
4895
|
};
|
|
1984
4896
|
}
|
|
1985
4897
|
|
|
1986
|
-
const { mission, reason } = selected;
|
|
4898
|
+
const { mission, reason, direct_goal_request: directGoalRequest, seeded_continuation_goal: seededContinuationGoal } = selected;
|
|
1987
4899
|
const taskSpine = missionTaskSpine(mission);
|
|
1988
4900
|
const missionView = missionStatusView(mission);
|
|
1989
4901
|
const objective = codexGoalObjective(mission);
|
|
4902
|
+
const ack = codexNativeGoalAck(mission, objective);
|
|
4903
|
+
const runtimeNeedsReplace = !ack && codexRuntimeGoalNeedsReplace(runtimeGoalState, objective);
|
|
4904
|
+
const nativeGoalAction = ack
|
|
4905
|
+
? null
|
|
4906
|
+
: runtimeNeedsReplace
|
|
4907
|
+
? codexNativeGoalRuntimeReplaceAction(mission, runtimeGoalState, {
|
|
4908
|
+
ack_new_mission: codexGoalAckCommand(mission, objective),
|
|
4909
|
+
allow_native_goal_supersede: options.allowNativeGoalSupersede === true,
|
|
4910
|
+
})
|
|
4911
|
+
: codexNativeGoalAction(mission, objective);
|
|
1990
4912
|
const goal = {
|
|
1991
4913
|
objective,
|
|
1992
4914
|
mission_id: mission.id,
|
|
@@ -1996,10 +4918,23 @@ function buildCodexGoalPayload(root = process.cwd(), options = {}) {
|
|
|
1996
4918
|
executed_by: taskSpine?.executed_by || mission.executed_by || null,
|
|
1997
4919
|
task_spine: taskSpine,
|
|
1998
4920
|
reason,
|
|
1999
|
-
next_command:
|
|
4921
|
+
next_command: runtimeNeedsReplace
|
|
4922
|
+
? codexNativeGoalReplaceInstruction(mission, runtimeGoalState, objective, {
|
|
4923
|
+
allowNativeGoalSupersede: options.allowNativeGoalSupersede === true,
|
|
4924
|
+
})
|
|
4925
|
+
: codexGoalNextCommand(mission),
|
|
2000
4926
|
replace_after: 'After proof or verifier pass, run atris mission goal --json again and replace the Codex /goal with the returned objective.',
|
|
2001
4927
|
visible_goal: codexVisibleGoalBridge(mission, objective),
|
|
2002
4928
|
codex_tool_contract: codexGoalToolContract(mission),
|
|
4929
|
+
requires_native_goal_start: !ack,
|
|
4930
|
+
requires_native_goal_replace: runtimeNeedsReplace,
|
|
4931
|
+
native_goal_action: nativeGoalAction,
|
|
4932
|
+
native_goal_ack_command: codexGoalAckCommand(mission, objective),
|
|
4933
|
+
native_goal_ack: ack,
|
|
4934
|
+
direct_goal_request: directGoalRequest || null,
|
|
4935
|
+
seeded_continuation_goal: seededContinuationGoal || null,
|
|
4936
|
+
next_action_preview: missionChoosesNextMission(mission) ? chooseNextMissionPreview(mission, root) : (mission.next_action_preview || null),
|
|
4937
|
+
runtime_goal_state: runtimeGoalState,
|
|
2003
4938
|
};
|
|
2004
4939
|
const heartbeat = heartbeatMode ? codexGoalHeartbeat(goal, mission) : undefined;
|
|
2005
4940
|
return {
|
|
@@ -2008,6 +4943,10 @@ function buildCodexGoalPayload(root = process.cwd(), options = {}) {
|
|
|
2008
4943
|
goal,
|
|
2009
4944
|
mission: missionView,
|
|
2010
4945
|
heartbeat,
|
|
4946
|
+
requires_native_goal_start: goal.requires_native_goal_start,
|
|
4947
|
+
requires_native_goal_replace: goal.requires_native_goal_replace,
|
|
4948
|
+
native_goal_action: goal.native_goal_action,
|
|
4949
|
+
runtime_goal_state: runtimeGoalState,
|
|
2011
4950
|
};
|
|
2012
4951
|
}
|
|
2013
4952
|
|
|
@@ -2021,6 +4960,138 @@ function refreshCodexGoalController(root = process.cwd(), options = {}) {
|
|
|
2021
4960
|
};
|
|
2022
4961
|
}
|
|
2023
4962
|
|
|
4963
|
+
function atrisVisibleGoalBridge(mission, goalObjective) {
|
|
4964
|
+
return {
|
|
4965
|
+
schema: 'atris.visible_goal_bridge.v1',
|
|
4966
|
+
runtime: String(mission.runner || 'manual'),
|
|
4967
|
+
source: 'atris_mission',
|
|
4968
|
+
mission_id: mission.id,
|
|
4969
|
+
desired_objective: goalObjective,
|
|
4970
|
+
status: 'active',
|
|
4971
|
+
state_file: '.atris/state/atris_goal.json',
|
|
4972
|
+
status_file: 'atris/status/atris-goal.md',
|
|
4973
|
+
operations: {
|
|
4974
|
+
read_current_goal: 'atris mission goal --runtime atris --json',
|
|
4975
|
+
update_from_mission: 'atris mission tick <mission-id> --summary "<what changed>" --json',
|
|
4976
|
+
refresh_on_phase_change: 'atris mission goal --runtime atris --json before continuing changed work',
|
|
4977
|
+
complete_after_proof: 'atris mission complete <mission-id> --proof "<receipt_path>" --json',
|
|
4978
|
+
},
|
|
4979
|
+
guardrails: [
|
|
4980
|
+
'Atris owns this goal state; no external native-goal tool ack is required.',
|
|
4981
|
+
'Do not claim completion until the mission has proof or a recorded blocked human ask.',
|
|
4982
|
+
'Use the mission task spine when task_spine.current_step_command is present.',
|
|
4983
|
+
],
|
|
4984
|
+
};
|
|
4985
|
+
}
|
|
4986
|
+
|
|
4987
|
+
function atrisGoalToolContract(mission) {
|
|
4988
|
+
return {
|
|
4989
|
+
current_policy: 'keep one Atris-owned visible goal active for mission/chat/fast runtimes',
|
|
4990
|
+
read_current_goal: 'atris mission goal --runtime atris --json',
|
|
4991
|
+
set_next_goal: 'atris mission run "<objective>" --runner atris2|manual|claude',
|
|
4992
|
+
complete_current_goal: 'atris mission complete <mission-id> --proof "<receipt_path>" --json',
|
|
4993
|
+
visible_goal_bridge: 'goal.visible_goal',
|
|
4994
|
+
platform_requirement: 'No native platform goal tool is required; Atris mission/task state is the source of truth.',
|
|
4995
|
+
runtime_tool_sequence: 'atris mission run -> atris_goal_state active -> ax fast / atris chat shows goal -> mission tick/task proof -> mission complete',
|
|
4996
|
+
blocked_without_platform_goal_write: false,
|
|
4997
|
+
mission_id: mission.id,
|
|
4998
|
+
};
|
|
4999
|
+
}
|
|
5000
|
+
|
|
5001
|
+
function buildAtrisGoalPayload(root = process.cwd(), options = {}) {
|
|
5002
|
+
const heartbeatMode = options.heartbeat === true;
|
|
5003
|
+
const selected = selectAtrisGoalMission(root, options);
|
|
5004
|
+
if (!selected) {
|
|
5005
|
+
return {
|
|
5006
|
+
ok: true,
|
|
5007
|
+
action: heartbeatMode ? 'atris_goal_heartbeat' : 'no_goal_candidate',
|
|
5008
|
+
mission: null,
|
|
5009
|
+
heartbeat: heartbeatMode ? { heavy_work_performed: false, next_heavy_command: null } : undefined,
|
|
5010
|
+
};
|
|
5011
|
+
}
|
|
5012
|
+
|
|
5013
|
+
const { mission, reason } = selected;
|
|
5014
|
+
const taskSpine = missionTaskSpine(mission);
|
|
5015
|
+
const missionView = missionStatusView(mission);
|
|
5016
|
+
const objective = mission.objective;
|
|
5017
|
+
const goal = {
|
|
5018
|
+
objective,
|
|
5019
|
+
mission_id: mission.id,
|
|
5020
|
+
mission_objective: mission.objective,
|
|
5021
|
+
mission_status: mission.status,
|
|
5022
|
+
owner: taskSpine?.owner || mission.owner,
|
|
5023
|
+
executed_by: taskSpine?.executed_by || mission.executed_by || null,
|
|
5024
|
+
runner: mission.runner || 'manual',
|
|
5025
|
+
model: mission.model || null,
|
|
5026
|
+
task_spine: taskSpine,
|
|
5027
|
+
reason,
|
|
5028
|
+
next_command: codexGoalNextCommand(mission),
|
|
5029
|
+
replace_after: 'After proof or phase change, run atris mission goal --runtime atris --json and continue from the returned next_command.',
|
|
5030
|
+
visible_goal: atrisVisibleGoalBridge(mission, objective),
|
|
5031
|
+
atris_tool_contract: atrisGoalToolContract(mission),
|
|
5032
|
+
requires_native_goal_start: false,
|
|
5033
|
+
native_goal_action: null,
|
|
5034
|
+
};
|
|
5035
|
+
return {
|
|
5036
|
+
ok: true,
|
|
5037
|
+
action: heartbeatMode ? 'atris_goal_heartbeat' : 'atris_goal_candidate',
|
|
5038
|
+
goal,
|
|
5039
|
+
mission: missionView,
|
|
5040
|
+
heartbeat: heartbeatMode ? codexGoalHeartbeat(goal, mission) : undefined,
|
|
5041
|
+
requires_native_goal_start: false,
|
|
5042
|
+
native_goal_action: null,
|
|
5043
|
+
};
|
|
5044
|
+
}
|
|
5045
|
+
|
|
5046
|
+
function writeAtrisGoalState(payload, root = process.cwd()) {
|
|
5047
|
+
const paths = statePaths(root);
|
|
5048
|
+
const state = {
|
|
5049
|
+
schema: 'atris.goal_controller.v1',
|
|
5050
|
+
updated_at: stampIso(),
|
|
5051
|
+
...payload,
|
|
5052
|
+
};
|
|
5053
|
+
fs.mkdirSync(path.dirname(paths.atrisGoalJson), { recursive: true });
|
|
5054
|
+
fs.writeFileSync(paths.atrisGoalJson, JSON.stringify(state, null, 2) + '\n', 'utf8');
|
|
5055
|
+
|
|
5056
|
+
const lines = [
|
|
5057
|
+
'# Atris Goal Controller',
|
|
5058
|
+
'',
|
|
5059
|
+
'<!-- Generated by Atris. Do not hand-edit. -->',
|
|
5060
|
+
'',
|
|
5061
|
+
`- updated: ${state.updated_at}`,
|
|
5062
|
+
`- action: ${state.action}`,
|
|
5063
|
+
];
|
|
5064
|
+
if (state.goal) {
|
|
5065
|
+
lines.push(`- mission: ${state.goal.mission_id}`);
|
|
5066
|
+
lines.push(`- runner: ${state.goal.runner}`);
|
|
5067
|
+
lines.push(`- status: ${state.goal.mission_status}`);
|
|
5068
|
+
lines.push(`- reason: ${state.goal.reason}`);
|
|
5069
|
+
lines.push(`- objective: ${state.goal.objective}`);
|
|
5070
|
+
lines.push(`- next: ${state.goal.next_command}`);
|
|
5071
|
+
lines.push(`- visible goal: ${state.goal.visible_goal.status}`);
|
|
5072
|
+
} else {
|
|
5073
|
+
lines.push('- mission: none');
|
|
5074
|
+
}
|
|
5075
|
+
lines.push('');
|
|
5076
|
+
fs.mkdirSync(path.dirname(paths.atrisGoalStatus), { recursive: true });
|
|
5077
|
+
fs.writeFileSync(paths.atrisGoalStatus, lines.join('\n'), 'utf8');
|
|
5078
|
+
return {
|
|
5079
|
+
state_path: paths.atrisGoalJson,
|
|
5080
|
+
status_path: paths.atrisGoalStatus,
|
|
5081
|
+
state,
|
|
5082
|
+
};
|
|
5083
|
+
}
|
|
5084
|
+
|
|
5085
|
+
function refreshAtrisGoalController(root = process.cwd(), options = {}) {
|
|
5086
|
+
const payload = buildAtrisGoalPayload(root, options);
|
|
5087
|
+
const rendered = writeAtrisGoalState(payload, root);
|
|
5088
|
+
return {
|
|
5089
|
+
...payload,
|
|
5090
|
+
state_path: rendered.state_path,
|
|
5091
|
+
status_path: rendered.status_path,
|
|
5092
|
+
};
|
|
5093
|
+
}
|
|
5094
|
+
|
|
2024
5095
|
function runAtrisMissionJsonCommand(root, args, options = {}) {
|
|
2025
5096
|
const cliPath = path.join(__dirname, '..', 'bin', 'atris.js');
|
|
2026
5097
|
const result = spawnSync(process.execPath, [cliPath, ...args], {
|
|
@@ -2089,6 +5160,7 @@ function goalLoopNextCommandPlan(goal) {
|
|
|
2089
5160
|
|
|
2090
5161
|
function shouldRunGoalLoopCommand(heartbeat, plan) {
|
|
2091
5162
|
if (!heartbeat?.goal) return false;
|
|
5163
|
+
if (heartbeat.goal.requires_native_goal_start === true) return false;
|
|
2092
5164
|
if (plan && plan.run_when_due_only === false) return true;
|
|
2093
5165
|
return heartbeat.heartbeat?.due === true;
|
|
2094
5166
|
}
|
|
@@ -2138,6 +5210,19 @@ function computeBackoff(policy, attempt) {
|
|
|
2138
5210
|
return Math.min(policy.maxMs, Math.round(base + jitter));
|
|
2139
5211
|
}
|
|
2140
5212
|
|
|
5213
|
+
function missionBudgetPromptLines(mission) {
|
|
5214
|
+
const contract = mission?.budget_contract;
|
|
5215
|
+
if (!contract) return [];
|
|
5216
|
+
return [
|
|
5217
|
+
``,
|
|
5218
|
+
`## Budget`,
|
|
5219
|
+
`Plain rule: ${contract.plain_language}`,
|
|
5220
|
+
`Limit: ${contract.budget_label}`,
|
|
5221
|
+
`Stop rule: ${contract.stop_rule}`,
|
|
5222
|
+
`Before acting, state the current bottleneck, the next useful move, and what proof or stop reason will make the tick honest.`,
|
|
5223
|
+
];
|
|
5224
|
+
}
|
|
5225
|
+
|
|
2141
5226
|
function consecutiveVerifierFails(ticks) {
|
|
2142
5227
|
let n = 0;
|
|
2143
5228
|
for (let i = ticks.length - 1; i >= 0; i--) {
|
|
@@ -2180,22 +5265,36 @@ function isWithinActiveHours(activeHours, now = new Date()) {
|
|
|
2180
5265
|
return cur >= start || cur < end;
|
|
2181
5266
|
}
|
|
2182
5267
|
|
|
2183
|
-
function
|
|
5268
|
+
function sleepSync(ms) {
|
|
5269
|
+
const waitMs = Math.max(0, Number(ms) || 0);
|
|
5270
|
+
if (!waitMs) return;
|
|
5271
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, waitMs);
|
|
5272
|
+
}
|
|
5273
|
+
|
|
5274
|
+
function acquireMissionLock(missionId, root = process.cwd(), options = {}) {
|
|
2184
5275
|
const dir = path.join(root, '.atris', 'state');
|
|
2185
5276
|
fs.mkdirSync(dir, { recursive: true });
|
|
2186
5277
|
const lockFile = path.join(dir, `mission-${missionId}.lock`);
|
|
5278
|
+
const waitMs = Math.max(0, Number(options.waitMs) || 0);
|
|
5279
|
+
const deadline = waitMs ? Date.now() + waitMs : 0;
|
|
2187
5280
|
let fd;
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
5281
|
+
while (true) {
|
|
5282
|
+
try {
|
|
5283
|
+
fd = fs.openSync(lockFile, 'wx');
|
|
5284
|
+
fs.writeSync(fd, JSON.stringify({ pid: process.pid, started_at: stampIso(), mission_id: missionId }));
|
|
5285
|
+
return { ok: true, lockFile, fd };
|
|
5286
|
+
} catch (e) {
|
|
5287
|
+
if (e.code === 'EEXIST') {
|
|
5288
|
+
let info = {};
|
|
5289
|
+
try { info = JSON.parse(fs.readFileSync(lockFile, 'utf8') || '{}'); } catch {}
|
|
5290
|
+
if (deadline && Date.now() < deadline) {
|
|
5291
|
+
sleepSync(Math.min(25, deadline - Date.now()));
|
|
5292
|
+
continue;
|
|
5293
|
+
}
|
|
5294
|
+
return { ok: false, lockFile, busy: true, holder: info };
|
|
5295
|
+
}
|
|
5296
|
+
return { ok: false, lockFile, error: e.message };
|
|
2197
5297
|
}
|
|
2198
|
-
return { ok: false, lockFile, error: e.message };
|
|
2199
5298
|
}
|
|
2200
5299
|
}
|
|
2201
5300
|
|
|
@@ -2228,6 +5327,7 @@ function buildTickPrompt(mission, tickIndex, maxTicks, frozen) {
|
|
|
2228
5327
|
`**Verifier (frozen):** ${frozen.verifier || '(none — receipt only)'}`,
|
|
2229
5328
|
`**Last status:** ${mission.status}`,
|
|
2230
5329
|
`**Last tick:** ${mission.last_tick_at || 'never'}`,
|
|
5330
|
+
...missionBudgetPromptLines(mission),
|
|
2231
5331
|
``,
|
|
2232
5332
|
`## Your task`,
|
|
2233
5333
|
`Do ONE increment of work toward the stop condition. ONE. No more.`,
|
|
@@ -2434,9 +5534,11 @@ async function runMission(args) {
|
|
|
2434
5534
|
const verifyEach = !hasFlag(args, '--no-verify');
|
|
2435
5535
|
const completeOnPass = hasFlag(args, '--complete-on-pass');
|
|
2436
5536
|
const skipDrain = hasFlag(args, '--no-drain');
|
|
5537
|
+
const createNext = hasFlag(args, '--create-next');
|
|
2437
5538
|
const maxTicksFlag = readFlag(args, '--max-ticks', '');
|
|
2438
5539
|
const maxTicks = Math.max(1, Number(maxTicksFlag) || MISSION_RUN_DEFAULTS.maxTicks);
|
|
2439
|
-
const
|
|
5540
|
+
const maxWallFlag = readFlag(args, '--max-wall', '');
|
|
5541
|
+
let maxWallSeconds = Math.max(60, Number(maxWallFlag) || MISSION_RUN_DEFAULTS.maxWallSeconds);
|
|
2440
5542
|
const cadenceOverride = readFlag(args, '--cadence', '');
|
|
2441
5543
|
const input = missionRunInputFromArgs(args);
|
|
2442
5544
|
const ref = input.ref;
|
|
@@ -2467,6 +5569,9 @@ async function runMission(args) {
|
|
|
2467
5569
|
if (!mission) {
|
|
2468
5570
|
exitMissionError(ref ? `Mission "${ref}" not found.` : 'Usage: atris mission run <id|objective> [--max-ticks 4] [--max-wall 3600]', 1, asJson);
|
|
2469
5571
|
}
|
|
5572
|
+
if (!maxWallFlag && Number(mission.budget_contract?.requested_seconds) > 0) {
|
|
5573
|
+
maxWallSeconds = Math.max(60, Number(mission.budget_contract.requested_seconds));
|
|
5574
|
+
}
|
|
2470
5575
|
if (['complete', 'stopped'].includes(mission.status)) {
|
|
2471
5576
|
if (asJson) {
|
|
2472
5577
|
printJsonOrText(
|
|
@@ -2480,6 +5585,8 @@ async function runMission(args) {
|
|
|
2480
5585
|
process.exit(0);
|
|
2481
5586
|
}
|
|
2482
5587
|
|
|
5588
|
+
maybeBlockUntilCodexNativeGoalStarted(mission, asJson);
|
|
5589
|
+
|
|
2483
5590
|
const preLockRunner = String(mission.runner || '').trim().toLowerCase();
|
|
2484
5591
|
const preLockCallerSession = runnerUsesCallerSession(mission.runner);
|
|
2485
5592
|
if (!skipClaude && !preLockCallerSession && preLockRunner !== 'atris2') {
|
|
@@ -2520,10 +5627,12 @@ async function runMission(args) {
|
|
|
2520
5627
|
console.error(`Mission ${mission.id} is ${mission.status}; nothing to run.`);
|
|
2521
5628
|
return;
|
|
2522
5629
|
}
|
|
5630
|
+
if (returnIfCodexNativeGoalNotStarted(mission, asJson)) return;
|
|
2523
5631
|
if (mission.status === 'paused') {
|
|
2524
5632
|
mission = saveMission({
|
|
2525
5633
|
...mission,
|
|
2526
5634
|
status: 'running',
|
|
5635
|
+
paused_at: null,
|
|
2527
5636
|
resumed_at: stampIso(),
|
|
2528
5637
|
stop_reason: null,
|
|
2529
5638
|
next_action: `running: atris mission run ${mission.id}`,
|
|
@@ -2537,7 +5646,7 @@ async function runMission(args) {
|
|
|
2537
5646
|
|
|
2538
5647
|
// Freeze run-start contract (verifier, lane). Stored on receipts, not the mission record.
|
|
2539
5648
|
const frozen = {
|
|
2540
|
-
verifier: mission
|
|
5649
|
+
verifier: effectiveMissionVerifier(mission),
|
|
2541
5650
|
lane: mission.lane || 'workspace',
|
|
2542
5651
|
started_at: stampIso(),
|
|
2543
5652
|
};
|
|
@@ -2565,7 +5674,9 @@ async function runMission(args) {
|
|
|
2565
5674
|
: atris2Runner
|
|
2566
5675
|
? `atris2 (${mission.model || 'atris:fast'})`
|
|
2567
5676
|
: (sessionId || `pending=${pendingSessionId}`);
|
|
2568
|
-
|
|
5677
|
+
if (!asJson) {
|
|
5678
|
+
console.error(`[mission run] ${mission.id}\n objective: ${mission.objective}\n lane: ${frozen.lane}\n cadence: ${cadence} (${cadenceSeconds}s)\n max_ticks: ${effectiveMaxTicks}, max_wall: ${maxWallSeconds}s\n session: ${sessionLabel}`);
|
|
5679
|
+
}
|
|
2569
5680
|
|
|
2570
5681
|
while (ticks.length < effectiveMaxTicks) {
|
|
2571
5682
|
const elapsedSec = (Date.now() - startedAt) / 1000;
|
|
@@ -2576,7 +5687,7 @@ async function runMission(args) {
|
|
|
2576
5687
|
// Re-read mission, detect mutation of frozen fields
|
|
2577
5688
|
mission = resolveMission(mission.id) || mission;
|
|
2578
5689
|
if (['complete', 'stopped', 'paused'].includes(mission.status)) { pauseReason = mission.status; break; }
|
|
2579
|
-
if (mission
|
|
5690
|
+
if (effectiveMissionVerifier(mission) !== frozen.verifier) { pauseReason = 'verifier-mutated'; break; }
|
|
2580
5691
|
if ((mission.lane || 'workspace') !== frozen.lane) { pauseReason = 'lane-mutated'; break; }
|
|
2581
5692
|
|
|
2582
5693
|
const tickIdx = Number(mission.last_tick_index || 0) + 1;
|
|
@@ -2729,9 +5840,12 @@ async function runMission(args) {
|
|
|
2729
5840
|
});
|
|
2730
5841
|
|
|
2731
5842
|
const xpReadyAction = missionXpReadyAction(mission, receiptPath);
|
|
5843
|
+
const budgetRemainingSeconds = missionFullBudgetRemainingSeconds(mission);
|
|
5844
|
+
const fullBudgetMode = budgetRemainingSeconds > 0;
|
|
2732
5845
|
const newStatus = (verifierResult?.passed && mission.always_on) ? 'ready' :
|
|
2733
5846
|
(verifierResult?.passed && xpReadyAction) ? 'ready' :
|
|
2734
|
-
(verifierResult?.passed && completeOnPass) ? 'complete' :
|
|
5847
|
+
(verifierResult?.passed && completeOnPass && !fullBudgetMode) ? 'complete' :
|
|
5848
|
+
(verifierResult?.passed && fullBudgetMode) ? 'running' :
|
|
2735
5849
|
(verifierResult?.passed ? 'ready' :
|
|
2736
5850
|
(verifierResult ? 'blocked' :
|
|
2737
5851
|
(result.status === 'ran' ? 'running' : mission.status)));
|
|
@@ -2740,16 +5854,22 @@ async function runMission(args) {
|
|
|
2740
5854
|
nextAction = nextCandidateTickAction(mission);
|
|
2741
5855
|
} else if (verifierResult?.passed && xpReadyAction) {
|
|
2742
5856
|
nextAction = xpReadyAction;
|
|
2743
|
-
} else if (verifierResult?.passed && completeOnPass) {
|
|
5857
|
+
} else if (verifierResult?.passed && completeOnPass && !fullBudgetMode) {
|
|
2744
5858
|
nextAction = 'mission complete';
|
|
5859
|
+
} else if (verifierResult?.passed && fullBudgetMode) {
|
|
5860
|
+
nextAction = `proof passed with ${durationLabel(budgetRemainingSeconds)} left on the budget; pick the next useful move, then: atris mission tick ${mission.id} --verify --summary "<what changed>"`;
|
|
2745
5861
|
} else if (verifierResult?.passed) {
|
|
2746
5862
|
nextAction = `review proof then run: atris mission complete ${mission.id} --proof "${receiptPath}"`;
|
|
2747
5863
|
} else if (verifierResult) {
|
|
2748
5864
|
nextAction = 'fix verifier failure or revise mission';
|
|
5865
|
+
} else if (result.status === 'ran' && mission.always_on) {
|
|
5866
|
+
nextAction = nextCandidateTickAction(mission);
|
|
2749
5867
|
}
|
|
2750
5868
|
mission = saveMission({
|
|
2751
5869
|
...mission,
|
|
2752
5870
|
status: newStatus,
|
|
5871
|
+
paused_at: null,
|
|
5872
|
+
stop_reason: null,
|
|
2753
5873
|
last_tick_at: finishedAt,
|
|
2754
5874
|
last_tick_status: result.status,
|
|
2755
5875
|
last_tick_reason: result.reason,
|
|
@@ -2777,7 +5897,9 @@ async function runMission(args) {
|
|
|
2777
5897
|
}
|
|
2778
5898
|
refreshCodexGoalController(cwd);
|
|
2779
5899
|
|
|
2780
|
-
|
|
5900
|
+
if (!asJson) {
|
|
5901
|
+
console.error(`[tick ${tickIdx}] status=${result.status} reason=${result.reason} verifier=${verifierResult ? (verifierResult.passed ? 'pass' : 'fail') : 'skip'} -> ${receiptPath || '-'}`);
|
|
5902
|
+
}
|
|
2781
5903
|
|
|
2782
5904
|
if (result.status === 'ran') {
|
|
2783
5905
|
ranTicks++;
|
|
@@ -2786,7 +5908,8 @@ async function runMission(args) {
|
|
|
2786
5908
|
backoffAttempt++;
|
|
2787
5909
|
}
|
|
2788
5910
|
|
|
2789
|
-
if (
|
|
5911
|
+
if (callerSessionRunner && result.status === 'ran') break;
|
|
5912
|
+
if (newStatus === 'complete' || (newStatus === 'ready' && !mission.always_on && !fullBudgetMode)) break;
|
|
2790
5913
|
if (consecutiveVerifierFails(ticks) >= 2) { pauseReason = 'consecutive-verifier-fails'; break; }
|
|
2791
5914
|
// A retired/inaccessible model is deterministic: the id is fixed for the run, so
|
|
2792
5915
|
// every remaining tick (and every future cron firing) fails identically. Backoff
|
|
@@ -2837,6 +5960,17 @@ async function runMission(args) {
|
|
|
2837
5960
|
}
|
|
2838
5961
|
|
|
2839
5962
|
const summaryWorktree = worktreeReceipt(runWorktreeBefore, gitWorktreeSnapshot(cwd), { verifier: frozen.verifier, baseline: runWorktreeBaseline });
|
|
5963
|
+
const createdNext = createNext
|
|
5964
|
+
? require('./loop-front').createNextLoopTask(['--as', mission.owner || 'auto-improver', '--json'], cwd, { print: false })
|
|
5965
|
+
: null;
|
|
5966
|
+
const landingSummary = {
|
|
5967
|
+
changed: missionRunChangedText(mission, ranTicks, effectiveMaxTicks, ticks, createdNext),
|
|
5968
|
+
timeline_command: missionRunTimelineCommand(mission),
|
|
5969
|
+
export_command: missionRunExportCommand(mission),
|
|
5970
|
+
prune_preview_command: missionRunPrunePreviewCommand(mission),
|
|
5971
|
+
next: missionRunCreatedNextLine(createdNext, continuationGoal, mission),
|
|
5972
|
+
};
|
|
5973
|
+
landingSummary.reason = missionHumanReasonText(mission, landingSummary.changed);
|
|
2840
5974
|
const finalReceipt = writeReceipt(mission, {
|
|
2841
5975
|
kind: 'mission_run_summary',
|
|
2842
5976
|
frozen,
|
|
@@ -2847,22 +5981,17 @@ async function runMission(args) {
|
|
|
2847
5981
|
session_id: sessionId,
|
|
2848
5982
|
pending_session_id: mission.pending_session_id || null,
|
|
2849
5983
|
elapsed_seconds: (Date.now() - startedAt) / 1000,
|
|
5984
|
+
budget_contract: mission.budget_contract || null,
|
|
2850
5985
|
worktree: summaryWorktree,
|
|
5986
|
+
created_next: createdNext,
|
|
5987
|
+
landing: landingSummary,
|
|
2851
5988
|
});
|
|
5989
|
+
const atrisGoalState = refreshAtrisGoalController(cwd, { missionId: mission.id });
|
|
2852
5990
|
const codexGoalState = refreshCodexGoalController(cwd);
|
|
2853
5991
|
|
|
2854
5992
|
printJsonOrText(
|
|
2855
|
-
{ ok: true, action: 'mission_run', mission, ran_ticks: ranTicks, tick_count: ticks.length, ticks, pause_reason: pauseReason, session_id: sessionId, summary_receipt: finalReceipt, worktree: summaryWorktree, codex_goal_state: codexGoalState, continuation_goal: continuationGoal },
|
|
2856
|
-
|
|
2857
|
-
`Ran mission ${mission.id}`,
|
|
2858
|
-
` objective: ${mission.objective}`,
|
|
2859
|
-
` ran_ticks: ${ranTicks}/${effectiveMaxTicks} (skipped/errored: ${ticks.length - ranTicks})`,
|
|
2860
|
-
` final state: ${mission.status}`,
|
|
2861
|
-
pauseReason ? ` pause: ${pauseReason}` : null,
|
|
2862
|
-
` session: ${sessionId || '(none)'}`,
|
|
2863
|
-
` summary receipt: ${finalReceipt}`,
|
|
2864
|
-
continuationGoal?.mission ? ` next goal: ${continuationGoal.mission.objective}` : null,
|
|
2865
|
-
].filter(Boolean),
|
|
5993
|
+
{ ok: true, action: 'mission_run', mission, ran_ticks: ranTicks, tick_count: ticks.length, ticks, pause_reason: pauseReason, session_id: sessionId, summary_receipt: finalReceipt, budget_contract: mission.budget_contract || null, worktree: summaryWorktree, atris_goal_state: atrisGoalState, codex_goal_state: codexGoalState, continuation_goal: continuationGoal, created_next: createdNext },
|
|
5994
|
+
missionRunSummaryLines(mission, ranTicks, effectiveMaxTicks, finalReceipt, pauseReason, continuationGoal, ticks, createdNext),
|
|
2866
5995
|
asJson,
|
|
2867
5996
|
);
|
|
2868
5997
|
} finally {
|
|
@@ -2877,9 +6006,10 @@ async function runMission(args) {
|
|
|
2877
6006
|
function tickMission(args) {
|
|
2878
6007
|
const asJson = wantsJson(args);
|
|
2879
6008
|
const verify = hasFlag(args, '--verify');
|
|
6009
|
+
const verifyOverride = readFlag(args, '--verify', '');
|
|
2880
6010
|
const completeOnPass = hasFlag(args, '--complete-on-pass');
|
|
2881
6011
|
const summary = readFlag(args, '--summary', '');
|
|
2882
|
-
const ref = stripKnownFlags(args, ['--summary'
|
|
6012
|
+
const ref = stripKnownFlags(args, ['--summary', '--verify'], ['--json', '--complete-on-pass'])[0] || '';
|
|
2883
6013
|
let mission = resolveMission(ref);
|
|
2884
6014
|
if (!mission) {
|
|
2885
6015
|
exitMissionError(ref ? `Mission "${ref}" not found.` : 'No mission found. Run: atris mission start "..."', 1, asJson);
|
|
@@ -2902,6 +6032,7 @@ function tickMission(args) {
|
|
|
2902
6032
|
printJsonOrText({ ok: true, action: 'tick_skipped', mission: saved }, [`Skipped ${mission.id}: ${mission.status}`], asJson);
|
|
2903
6033
|
return;
|
|
2904
6034
|
}
|
|
6035
|
+
if (returnIfCodexNativeGoalNotStarted(mission, asJson)) return;
|
|
2905
6036
|
|
|
2906
6037
|
// Per the /mission skill design, the calling Claude session IS the per-tick LLM.
|
|
2907
6038
|
// This CLI subcommand records the tick: writes a structured receipt (matching the
|
|
@@ -2914,11 +6045,17 @@ function tickMission(args) {
|
|
|
2914
6045
|
const tickWorktreeBefore = gitWorktreeSnapshot(cwd);
|
|
2915
6046
|
const worktreeBaseline = loadMissionWorktreeBaseline(mission.id, cwd);
|
|
2916
6047
|
|
|
6048
|
+
const effectiveVerifier = effectiveMissionVerifier(mission);
|
|
6049
|
+
const verifierCommand = verify
|
|
6050
|
+
? String(verifyOverride || effectiveVerifier || '').trim()
|
|
6051
|
+
: '';
|
|
6052
|
+
if (verifierCommand) assertMissionVerifier(verifierCommand, asJson);
|
|
6053
|
+
|
|
2917
6054
|
let verifierResult = null;
|
|
2918
|
-
if (verify &&
|
|
2919
|
-
verifierResult = runVerifier(
|
|
6055
|
+
if (verify && verifierCommand) {
|
|
6056
|
+
verifierResult = runVerifier(verifierCommand);
|
|
2920
6057
|
}
|
|
2921
|
-
const tickWorktree = worktreeReceipt(tickWorktreeBefore, gitWorktreeSnapshot(cwd), { verifier:
|
|
6058
|
+
const tickWorktree = worktreeReceipt(tickWorktreeBefore, gitWorktreeSnapshot(cwd), { verifier: verifierCommand || effectiveVerifier, baseline: worktreeBaseline });
|
|
2922
6059
|
|
|
2923
6060
|
// Same layer classification as the run-tick path; manual ticks carry their
|
|
2924
6061
|
// receipt text in --summary.
|
|
@@ -2941,7 +6078,7 @@ function tickMission(args) {
|
|
|
2941
6078
|
kind: 'mission_tick',
|
|
2942
6079
|
tick: tickRecord,
|
|
2943
6080
|
frozen: {
|
|
2944
|
-
verifier:
|
|
6081
|
+
verifier: verifierCommand || effectiveVerifier || '',
|
|
2945
6082
|
lane: mission.lane || 'workspace',
|
|
2946
6083
|
started_at: tickStart,
|
|
2947
6084
|
},
|
|
@@ -2950,21 +6087,40 @@ function tickMission(args) {
|
|
|
2950
6087
|
worktree: tickWorktree,
|
|
2951
6088
|
});
|
|
2952
6089
|
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
6090
|
+
let status = 'running';
|
|
6091
|
+
let nextAction = (verifierCommand || effectiveVerifier)
|
|
6092
|
+
? `run verifier: ${verifierCommand || effectiveVerifier}`
|
|
6093
|
+
: (mission.always_on && missionTaskSpine(mission)?.has_task
|
|
6094
|
+
? nextCandidateTickAction(mission)
|
|
6095
|
+
: 'attach task, verifier, or proof');
|
|
6096
|
+
const nextGoalChain = advanceMissionGoalChain(mission.goal_chain, summary, verifierResult);
|
|
6097
|
+
if (verifierResult?.passed && nextGoalChain && !nextGoalChain.pause_ready) {
|
|
6098
|
+
status = 'running';
|
|
6099
|
+
nextAction = missionGoalChainNextAction(nextGoalChain);
|
|
6100
|
+
} else if (verifierResult?.passed) {
|
|
2956
6101
|
const xpReadyAction = missionXpReadyAction(mission, receiptPath);
|
|
2957
|
-
|
|
6102
|
+
const budgetRemainingSeconds = missionFullBudgetRemainingSeconds(mission);
|
|
6103
|
+
const budgetOpen = budgetRemainingSeconds > 0;
|
|
6104
|
+
status = (completeOnPass && !mission.always_on && !xpReadyAction && !budgetOpen) ? 'complete'
|
|
6105
|
+
: (budgetOpen && !mission.always_on && !xpReadyAction ? 'running' : 'ready');
|
|
2958
6106
|
nextAction = mission.always_on ? nextCandidateTickAction(mission) :
|
|
2959
|
-
(xpReadyAction
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
6107
|
+
(xpReadyAction
|
|
6108
|
+
|| (budgetOpen
|
|
6109
|
+
? `proof passed with ${durationLabel(budgetRemainingSeconds)} left on the budget; pick the next useful move, then: atris mission tick ${mission.id} --verify --summary "<what changed>"`
|
|
6110
|
+
: (completeOnPass ? 'mission complete' : `review proof then run: atris mission complete ${mission.id} --proof "${receiptPath}"`)));
|
|
6111
|
+
} else if (verifierResult) {
|
|
6112
|
+
status = 'blocked';
|
|
6113
|
+
nextAction = 'fix verifier failure or revise mission';
|
|
6114
|
+
} else if (nextGoalChain?.pause_ready) {
|
|
6115
|
+
status = 'ready';
|
|
6116
|
+
nextAction = `mission feels good; review proof then run: atris mission complete ${mission.id} --proof "${receiptPath}"`;
|
|
6117
|
+
} else if (nextGoalChain) {
|
|
6118
|
+
nextAction = missionGoalChainNextAction(nextGoalChain);
|
|
6119
|
+
}
|
|
6120
|
+
const clearsPauseState = !['paused', 'stopped'].includes(status);
|
|
6121
|
+
const nextMission = {
|
|
6122
|
+
...mission,
|
|
6123
|
+
status,
|
|
2968
6124
|
paused_at: clearsPauseState ? null : mission.paused_at || null,
|
|
2969
6125
|
stop_reason: clearsPauseState ? null : mission.stop_reason || null,
|
|
2970
6126
|
resumed_at: clearsPauseState && mission.status === 'paused' ? tickRecord.finished_at : mission.resumed_at || null,
|
|
@@ -2973,11 +6129,12 @@ function tickMission(args) {
|
|
|
2973
6129
|
last_tick_status: tickRecord.status,
|
|
2974
6130
|
last_tick_reason: tickRecord.reason,
|
|
2975
6131
|
last_tick_index: tickIdx,
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
6132
|
+
last_tick_layer: tickRecord.layer,
|
|
6133
|
+
last_tick_layer_source: tickRecord.layer_source,
|
|
6134
|
+
verifier_result: verifierResult || mission.verifier_result || null,
|
|
6135
|
+
...(nextGoalChain ? { goal_chain: nextGoalChain } : {}),
|
|
6136
|
+
next_action: nextAction,
|
|
6137
|
+
};
|
|
2981
6138
|
const { mission: saved } = saveMission(nextMission, cwd, 'mission_tick', {
|
|
2982
6139
|
tick_index: tickIdx, verify, verifier_result: verifierResult, receipt_path: receiptPath, layer: tickRecord.layer,
|
|
2983
6140
|
});
|
|
@@ -2994,15 +6151,12 @@ function tickMission(args) {
|
|
|
2994
6151
|
? seedMissionRunContinuation(saved, cwd, receiptPath)
|
|
2995
6152
|
: null;
|
|
2996
6153
|
const outputMission = continuationGoal?.parent || saved;
|
|
6154
|
+
const atrisGoalState = refreshAtrisGoalController(process.cwd(), { missionId: outputMission.id });
|
|
2997
6155
|
const codexGoalState = refreshCodexGoalController(process.cwd());
|
|
2998
6156
|
printJsonOrText(
|
|
2999
|
-
{ ok: true, action: 'mission_tick', mission: outputMission, tick: tickRecord, verifier_result: verifierResult, receipt_path: receiptPath, log_path: logPath, codex_goal_state: codexGoalState, continuation_goal: continuationGoal },
|
|
6157
|
+
{ ok: true, action: 'mission_tick', mission: outputMission, tick: tickRecord, verifier_result: verifierResult, receipt_path: receiptPath, log_path: logPath, atris_goal_state: atrisGoalState, codex_goal_state: codexGoalState, continuation_goal: continuationGoal },
|
|
3000
6158
|
[
|
|
3001
|
-
|
|
3002
|
-
`State: ${outputMission.status}`,
|
|
3003
|
-
`Tick: ${tickIdx}`,
|
|
3004
|
-
`Next: ${outputMission.next_action}`,
|
|
3005
|
-
...(receiptPath ? [`Receipt: ${receiptPath}`] : []),
|
|
6159
|
+
...missionTickResultLines(outputMission, tickIdx, receiptPath, verifierResult, summary),
|
|
3006
6160
|
...(continuationGoal?.mission ? [`Next goal: ${continuationGoal.mission.objective}`] : []),
|
|
3007
6161
|
],
|
|
3008
6162
|
asJson,
|
|
@@ -3036,7 +6190,14 @@ function receiptShowsPass(receipt) {
|
|
|
3036
6190
|
// verifier passed. Mirrors the task plane's proof-only accept guard so the
|
|
3037
6191
|
// final transition consumes the receipts instead of trusting free text.
|
|
3038
6192
|
function missionCompletionGate(mission, proof, root = process.cwd()) {
|
|
3039
|
-
|
|
6193
|
+
const remainingSeconds = missionFullBudgetRemainingSeconds(mission);
|
|
6194
|
+
if (remainingSeconds > 0) {
|
|
6195
|
+
return {
|
|
6196
|
+
ok: false,
|
|
6197
|
+
source: 'budget_contract',
|
|
6198
|
+
reason: `full-budget mission still has ${durationLabel(remainingSeconds)} left; keep picking the next useful move or use --force if blocked/unsafe`,
|
|
6199
|
+
};
|
|
6200
|
+
}
|
|
3040
6201
|
const receipt = readReceiptProof(proof, root);
|
|
3041
6202
|
if (receipt) {
|
|
3042
6203
|
if (receipt.mission_id !== mission.id) {
|
|
@@ -3047,6 +6208,7 @@ function missionCompletionGate(mission, proof, root = process.cwd()) {
|
|
|
3047
6208
|
: { ok: false, source: 'receipt', reason: 'proof receipt does not show a passing verifier' };
|
|
3048
6209
|
}
|
|
3049
6210
|
if (mission.verifier_result?.passed === true) return { ok: true, source: 'mission_state' };
|
|
6211
|
+
if (!effectiveMissionVerifier(mission)) return { ok: true, source: 'no_verifier' };
|
|
3050
6212
|
return { ok: false, source: 'mission_state', reason: 'verifier has not passed for this mission and proof is not a passing receipt' };
|
|
3051
6213
|
}
|
|
3052
6214
|
|
|
@@ -3055,6 +6217,7 @@ function completeMission(args) {
|
|
|
3055
6217
|
const force = hasFlag(args, '--force');
|
|
3056
6218
|
const proof = readFlag(args, '--proof', '');
|
|
3057
6219
|
const ref = stripKnownFlags(args, ['--proof'], ['--json', '--force'])[0] || '';
|
|
6220
|
+
const root = process.cwd();
|
|
3058
6221
|
if (!ref || !proof) {
|
|
3059
6222
|
exitMissionError('Usage: atris mission complete <id> --proof "..."', 1, asJson);
|
|
3060
6223
|
}
|
|
@@ -3062,11 +6225,11 @@ function completeMission(args) {
|
|
|
3062
6225
|
if (!mission) {
|
|
3063
6226
|
exitMissionError(`Mission "${ref}" not found.`, 1, asJson);
|
|
3064
6227
|
}
|
|
3065
|
-
const gate = missionCompletionGate(mission, proof,
|
|
6228
|
+
const gate = missionCompletionGate(mission, proof, root);
|
|
3066
6229
|
if (!gate.ok && !force) {
|
|
3067
6230
|
exitMissionError(`[mission complete] ${gate.reason}. Run: atris mission tick ${mission.id} --verify (or override as operator with --force)`, 2, asJson);
|
|
3068
6231
|
}
|
|
3069
|
-
const baselineSummary = pruneMissionWorktreeBaseline(mission,
|
|
6232
|
+
const baselineSummary = pruneMissionWorktreeBaseline(mission, root);
|
|
3070
6233
|
const completionGate = { ...gate, forced: force && !gate.ok };
|
|
3071
6234
|
const baseNext = {
|
|
3072
6235
|
...mission,
|
|
@@ -3079,16 +6242,31 @@ function completeMission(args) {
|
|
|
3079
6242
|
};
|
|
3080
6243
|
const xpNextCommand = missionXpReadyAction(baseNext, proof);
|
|
3081
6244
|
const completion = missionCompletionReceipt(baseNext, proof, xpNextCommand);
|
|
6245
|
+
const artifactPreview = missionArtifactPaths(baseNext, root);
|
|
6246
|
+
completion.landing.artifact = `Open timeline at ${artifactPreview.relativeIndexHtml}.`;
|
|
6247
|
+
completion.result.artifact = artifactPreview.relativeIndexHtml;
|
|
3082
6248
|
const next = {
|
|
3083
6249
|
...baseNext,
|
|
3084
6250
|
landing: completion.landing,
|
|
3085
6251
|
result: completion.result,
|
|
3086
6252
|
};
|
|
3087
|
-
const { mission: saved } = saveMission(next,
|
|
3088
|
-
const continuationGoal = seedMissionRunContinuation(saved,
|
|
6253
|
+
const { mission: saved } = saveMission(next, root, 'mission_completed', { proof, completion_gate: next.completion_gate });
|
|
6254
|
+
const continuationGoal = seedMissionRunContinuation(saved, root, proof);
|
|
3089
6255
|
const outputMission = continuationGoal?.parent || saved;
|
|
3090
|
-
const
|
|
3091
|
-
|
|
6256
|
+
const artifact = writeMissionArtifact(outputMission, {
|
|
6257
|
+
root,
|
|
6258
|
+
proof,
|
|
6259
|
+
completion,
|
|
6260
|
+
xpNextCommand,
|
|
6261
|
+
continuationGoal,
|
|
6262
|
+
});
|
|
6263
|
+
const logPath = appendMemberLog(outputMission.owner, 'Mission completed', {
|
|
6264
|
+
mission: outputMission.objective,
|
|
6265
|
+
proof,
|
|
6266
|
+
artifact: artifact.index_html,
|
|
6267
|
+
}, root);
|
|
6268
|
+
const atrisGoalState = refreshAtrisGoalController(root, { missionId: continuationGoal?.mission?.id || outputMission.id });
|
|
6269
|
+
const codexGoalState = refreshCodexGoalController(root);
|
|
3092
6270
|
printJsonOrText(
|
|
3093
6271
|
{
|
|
3094
6272
|
ok: true,
|
|
@@ -3096,16 +6274,15 @@ function completeMission(args) {
|
|
|
3096
6274
|
mission: outputMission,
|
|
3097
6275
|
landing: completion.landing,
|
|
3098
6276
|
result: completion.result,
|
|
6277
|
+
artifact,
|
|
3099
6278
|
log_path: logPath,
|
|
6279
|
+
atris_goal_state: atrisGoalState,
|
|
3100
6280
|
codex_goal_state: codexGoalState,
|
|
3101
6281
|
xp_next_command: xpNextCommand,
|
|
3102
6282
|
continuation_goal: continuationGoal,
|
|
3103
6283
|
},
|
|
3104
6284
|
[
|
|
3105
|
-
`Completed mission: ${outputMission.objective}`,
|
|
3106
6285
|
...missionResultLines(completion),
|
|
3107
|
-
`Proof: ${proof}`,
|
|
3108
|
-
...(xpNextCommand ? [`AgentXP: ${xpNextCommand}`] : []),
|
|
3109
6286
|
...(continuationGoal?.mission ? [`Next goal: ${continuationGoal.mission.objective}`] : []),
|
|
3110
6287
|
],
|
|
3111
6288
|
asJson,
|
|
@@ -3148,9 +6325,10 @@ function stopMission(args) {
|
|
|
3148
6325
|
next_action: status === 'paused' ? `resume with: atris mission tick ${mission.id}` : 'mission stopped',
|
|
3149
6326
|
};
|
|
3150
6327
|
const { mission: saved } = saveMission(next, process.cwd(), pause ? 'mission_paused' : 'mission_stopped', { reason, receipt_path: receiptPath });
|
|
6328
|
+
const directGoalRequestCleared = pause ? false : clearDirectRunCodexGoalRequestForMission(saved.id, process.cwd());
|
|
3151
6329
|
const logPath = appendMemberLog(saved.owner, pause ? 'Mission paused' : 'Mission stopped', { mission: saved.objective, reason });
|
|
3152
6330
|
printJsonOrText(
|
|
3153
|
-
{ ok: true, action: pause ? 'mission_paused' : 'mission_stopped', mission: saved, receipt_path: receiptPath, log_path: logPath },
|
|
6331
|
+
{ ok: true, action: pause ? 'mission_paused' : 'mission_stopped', mission: saved, receipt_path: receiptPath, log_path: logPath, direct_goal_request_cleared: directGoalRequestCleared },
|
|
3154
6332
|
[
|
|
3155
6333
|
`${pause ? 'Paused' : 'Stopped'} mission: ${saved.objective}`,
|
|
3156
6334
|
`Reason: ${reason}`,
|
|
@@ -3162,8 +6340,50 @@ function stopMission(args) {
|
|
|
3162
6340
|
|
|
3163
6341
|
function goalMission(args) {
|
|
3164
6342
|
const asJson = wantsJson(args);
|
|
6343
|
+
if (args[0] === 'ack') {
|
|
6344
|
+
return ackMissionGoal(args.slice(1));
|
|
6345
|
+
}
|
|
6346
|
+
const runtime = String(readFlag(args, '--runtime', 'codex') || 'codex').trim().toLowerCase();
|
|
6347
|
+
if (runtime === 'atris' || runtime === 'atris2' || runtime === 'ax') {
|
|
6348
|
+
const heartbeatMode = hasFlag(args, '--heartbeat');
|
|
6349
|
+
const payload = refreshAtrisGoalController(process.cwd(), { heartbeat: heartbeatMode });
|
|
6350
|
+
if (!payload.goal) {
|
|
6351
|
+
printJsonOrText(payload, ['No active mission found for Atris goal.'], asJson);
|
|
6352
|
+
return;
|
|
6353
|
+
}
|
|
6354
|
+
printJsonOrText(
|
|
6355
|
+
payload,
|
|
6356
|
+
[
|
|
6357
|
+
`Atris goal: ${payload.goal.objective}`,
|
|
6358
|
+
`Runner: ${payload.goal.runner}`,
|
|
6359
|
+
`Next: ${payload.goal.next_command}`,
|
|
6360
|
+
payload.goal.replace_after,
|
|
6361
|
+
],
|
|
6362
|
+
asJson,
|
|
6363
|
+
);
|
|
6364
|
+
return;
|
|
6365
|
+
}
|
|
3165
6366
|
const heartbeatMode = hasFlag(args, '--heartbeat');
|
|
3166
|
-
const
|
|
6367
|
+
const nativeGoalStatus = readFlag(args, '--native-goal-status', readFlag(args, '--visible-goal-status', ''));
|
|
6368
|
+
const nativeGoalObjective = readFlag(args, '--native-goal-objective', readFlag(args, '--visible-goal-objective', ''));
|
|
6369
|
+
const allowNativeGoalSupersede = hasFlag(args, '--allow-native-goal-supersede') || hasFlag(args, '--supersede-paused-native-goal');
|
|
6370
|
+
const payload = refreshCodexGoalController(process.cwd(), {
|
|
6371
|
+
heartbeat: heartbeatMode,
|
|
6372
|
+
...(nativeGoalStatus ? { nativeGoalStatus } : {}),
|
|
6373
|
+
...(nativeGoalObjective ? { nativeGoalObjective } : {}),
|
|
6374
|
+
...(allowNativeGoalSupersede ? { allowNativeGoalSupersede: true } : {}),
|
|
6375
|
+
});
|
|
6376
|
+
if (payload.active_goal_conflict) {
|
|
6377
|
+
printJsonOrText(
|
|
6378
|
+
payload,
|
|
6379
|
+
[
|
|
6380
|
+
payload.active_goal_conflict.message,
|
|
6381
|
+
`Next: ${payload.active_goal_conflict.next_command}`,
|
|
6382
|
+
],
|
|
6383
|
+
asJson,
|
|
6384
|
+
);
|
|
6385
|
+
return;
|
|
6386
|
+
}
|
|
3167
6387
|
if (!payload.goal) {
|
|
3168
6388
|
printJsonOrText(
|
|
3169
6389
|
payload,
|
|
@@ -3185,6 +6405,72 @@ function goalMission(args) {
|
|
|
3185
6405
|
);
|
|
3186
6406
|
}
|
|
3187
6407
|
|
|
6408
|
+
function ackMissionGoal(args) {
|
|
6409
|
+
const asJson = wantsJson(args);
|
|
6410
|
+
const ref = stripKnownFlags(args, ['--runtime', '--status', '--objective'], ['--json'])[0] || '';
|
|
6411
|
+
if (!ref) {
|
|
6412
|
+
exitMissionError('Usage: atris mission goal ack <mission-id> --runtime codex --status active --objective "<objective>" --json', 1, asJson);
|
|
6413
|
+
}
|
|
6414
|
+
const runtime = String(readFlag(args, '--runtime', 'codex') || 'codex').trim().toLowerCase();
|
|
6415
|
+
const status = String(readFlag(args, '--status', 'active') || 'active').trim().toLowerCase();
|
|
6416
|
+
let mission = resolveMission(ref);
|
|
6417
|
+
if (!mission) {
|
|
6418
|
+
exitMissionError(`Mission "${ref}" not found.`, 1, asJson);
|
|
6419
|
+
}
|
|
6420
|
+
if (runtime !== 'codex' || status !== 'active') {
|
|
6421
|
+
exitMissionError('Native goal ack requires --runtime codex --status active.', 2, asJson);
|
|
6422
|
+
}
|
|
6423
|
+
|
|
6424
|
+
const lock = acquireMissionLock(mission.id, process.cwd(), { waitMs: 2000 });
|
|
6425
|
+
if (!lock.ok) {
|
|
6426
|
+
exitMissionError(`[mission goal ack] lock busy (held by pid ${lock.holder?.pid || '?'} since ${lock.holder?.started_at || '?'}). Exit.`, 3, asJson);
|
|
6427
|
+
}
|
|
6428
|
+
|
|
6429
|
+
try {
|
|
6430
|
+
mission = resolveMission(mission.id) || mission;
|
|
6431
|
+
if (!isCodexGoalMission(mission)) {
|
|
6432
|
+
releaseMissionLock(lock);
|
|
6433
|
+
exitMissionError(`Mission "${ref}" uses runner=${mission.runner || 'manual'}; native Codex goal ack is only for runner=codex_goal.`, 2, asJson);
|
|
6434
|
+
}
|
|
6435
|
+
const expectedObjective = codexGoalObjective(mission);
|
|
6436
|
+
const objective = readFlag(args, '--objective', expectedObjective);
|
|
6437
|
+
if (objective !== expectedObjective) {
|
|
6438
|
+
releaseMissionLock(lock);
|
|
6439
|
+
exitMissionError(`Native goal objective mismatch. Expected: ${expectedObjective}`, 2, asJson);
|
|
6440
|
+
}
|
|
6441
|
+
const ack = {
|
|
6442
|
+
runtime: 'codex',
|
|
6443
|
+
status: 'active',
|
|
6444
|
+
objective,
|
|
6445
|
+
acknowledged_at: stampIso(),
|
|
6446
|
+
};
|
|
6447
|
+
const nextMission = {
|
|
6448
|
+
...mission,
|
|
6449
|
+
native_goal_ack: ack,
|
|
6450
|
+
next_action: codexGoalNextCommand({ ...mission, native_goal_ack: ack }),
|
|
6451
|
+
};
|
|
6452
|
+
const { mission: saved } = saveMission(nextMission, process.cwd(), 'mission_native_goal_ack', { ack });
|
|
6453
|
+
const payload = refreshCodexGoalController(process.cwd());
|
|
6454
|
+
printJsonOrText(
|
|
6455
|
+
{
|
|
6456
|
+
ok: true,
|
|
6457
|
+
action: 'native_goal_acknowledged',
|
|
6458
|
+
mission: saved,
|
|
6459
|
+
native_goal_ack: ack,
|
|
6460
|
+
codex_goal_state: payload,
|
|
6461
|
+
next_command: payload.goal?.next_command || `atris mission tick ${saved.id} --summary "<what changed>"`,
|
|
6462
|
+
},
|
|
6463
|
+
[
|
|
6464
|
+
`Native goal active: ${saved.objective}`,
|
|
6465
|
+
`Next: ${payload.goal?.next_command || saved.next_action}`,
|
|
6466
|
+
],
|
|
6467
|
+
asJson,
|
|
6468
|
+
);
|
|
6469
|
+
} finally {
|
|
6470
|
+
releaseMissionLock(lock);
|
|
6471
|
+
}
|
|
6472
|
+
}
|
|
6473
|
+
|
|
3188
6474
|
async function goalLoopMission(args) {
|
|
3189
6475
|
const asJson = wantsJson(args);
|
|
3190
6476
|
const noClaude = hasFlag(args, '--no-claude');
|
|
@@ -3276,18 +6562,29 @@ atris mission - durable goal + loop + owner + proof state
|
|
|
3276
6562
|
default model atris:fast; runner codex_goal publishes the goal for a live
|
|
3277
6563
|
Codex session to pull via atris mission goal)
|
|
3278
6564
|
atris mission status [id] [--status <state>] [--limit <n>] [--local] [--json]
|
|
6565
|
+
atris mission doctor [--local] [--json] Flag no-verifier missions, help missions, stale ready receipts, and blocked always-on loops
|
|
3279
6566
|
atris mission attach-task <id> [--json] Create the missing task spine for an existing active mission
|
|
3280
6567
|
atris mission report [id] [--limit <n>] [--local] [--json] Plain outcome, worker receipt, verifier receipt, and next move
|
|
6568
|
+
atris mission timeline [id] [--limit <n>] [--all] [--prune-preview] [--write] [--json] Saved landing Changed/Next lines from mission receipts
|
|
3281
6569
|
atris mission watch [id] [--interval <s>] [--idle-every <s>] Live heartbeat: prints a line per tick as it lands
|
|
3282
6570
|
atris mission layers [--mission <id-substr>] [--since <date>] [--json] Per-layer growth curve across tick receipts
|
|
3283
6571
|
(rolls up sibling git-worktree missions; --local scopes to this checkout)
|
|
3284
|
-
atris mission
|
|
6572
|
+
atris mission room "<messy input>" [--owner <member>] [--room-auto-run] [--json] Create a Mission Room card and shareable receipt from messy intent
|
|
6573
|
+
atris mission prune-runs [--apply] [--days <n>] [--keep-newest <n>] [--json] Compress old run receipts into a manifest and prune unreferenced clutter
|
|
6574
|
+
atris mission goal [--runtime codex|atris] [--heartbeat] [--native-goal-status active|paused] [--native-goal-objective "..."] [--allow-native-goal-supersede] [--json]
|
|
6575
|
+
atris mission goal ack <id> --runtime codex --status active --objective "<objective>" --json
|
|
3285
6576
|
atris mission goal-loop [--max-wall 28800] [--max-iterations 32] [--no-claude] [--json]
|
|
3286
|
-
atris mission tick <id> [--verify] [--complete-on-pass] [--summary "..."] [--json]
|
|
6577
|
+
atris mission tick <id> [--verify ["cmd"]] [--complete-on-pass] [--summary "..."] [--json]
|
|
3287
6578
|
atris mission "<objective>" [--owner <member>] Shortcut for: atris mission run "<objective>"
|
|
3288
6579
|
atris mission run ["objective"|<member> ["objective"]|id|--due] [--owner <member>] [--max-ticks 4] [--max-wall 3600] [--cadence "15m"]
|
|
3289
|
-
[--
|
|
6580
|
+
[--native-goal-status active|paused] [--native-goal-objective "..."] [--allow-native-goal-supersede]
|
|
6581
|
+
[--spend-full-budget|--use-whole-budget|--stop-when-done] [--room-preflight|--no-room-preflight]
|
|
6582
|
+
[--room-auto-run|--no-room-auto-run]
|
|
6583
|
+
[--no-claude] [--no-verify] [--complete-on-pass] [--no-drain] [--create-next] [--json]
|
|
3290
6584
|
(bare/member-only run prompts; one-word fuzzy intent starts a new visible-goal mission; --due runs the saved queue)
|
|
6585
|
+
(short time like "20 minutes" means finish early; long/sleep time like "5 hours" keeps using the budget; --stop-when-done overrides)
|
|
6586
|
+
(messy, shower, and overnight requests preflight through Mission Room before the visible goal is written)
|
|
6587
|
+
(--room-auto-run makes trusted self-improvement asks preview through Mission Room, select real work, then start one bounded goal)
|
|
3291
6588
|
(mission-run completions seed the next visible goal: decide and start the next useful mission)
|
|
3292
6589
|
atris mission complete <id> --proof "..."
|
|
3293
6590
|
atris mission stop <id> [--pause] [--reason "..."]
|
|
@@ -3296,7 +6593,7 @@ Autonomy recipe:
|
|
|
3296
6593
|
1. Pick an owner member: atris member create <member> (if missing)
|
|
3297
6594
|
2. Start a current-agent mission with a verifier:
|
|
3298
6595
|
atris mission start "ship one proof" --owner <member> --runner codex_goal --lane code --verify "npm test" --stop "verifier passes" --xp-task
|
|
3299
|
-
3. Codex sessions:
|
|
6596
|
+
3. Codex sessions: read native get_goal, then pass its status into atris mission goal --native-goal-status <status> --native-goal-objective "<objective>" --json
|
|
3300
6597
|
Overnight controller: atris mission goal --heartbeat --json
|
|
3301
6598
|
Bounded overnight runner: atris mission goal-loop --max-wall 28800 --no-claude --json
|
|
3302
6599
|
4. Do one bounded step, then record it:
|
|
@@ -3477,6 +6774,70 @@ function layersMission(args) {
|
|
|
3477
6774
|
printJsonOrText({ ok: true, since: sinceRaw || null, total, tagged, untagged, by_layer: byLayer, by_source: bySource, dominant: tagged ? dominant : null, skewed }, lines, asJson);
|
|
3478
6775
|
}
|
|
3479
6776
|
|
|
6777
|
+
function roomMission(args) {
|
|
6778
|
+
const asJson = wantsJson(args);
|
|
6779
|
+
const explicitOwner = readFlag(args, '--owner', '');
|
|
6780
|
+
const input = stripKnownFlags(args, ['--owner'], ['--json', '--room-auto-run', '--no-room-auto-run']).join(' ').trim();
|
|
6781
|
+
if (!input) {
|
|
6782
|
+
exitMissionError('Usage: atris mission room "<messy input>" [--owner <member>] [--room-auto-run] [--json]', 1, asJson);
|
|
6783
|
+
}
|
|
6784
|
+
const requestedOwner = explicitOwner || process.env.ATRIS_AGENT_ID || '';
|
|
6785
|
+
const ownerResolution = resolveFunctionalOwner({
|
|
6786
|
+
requestedOwner,
|
|
6787
|
+
title: input,
|
|
6788
|
+
note: input,
|
|
6789
|
+
root: process.cwd(),
|
|
6790
|
+
fallbackOwners: ['mission-lead', 'task-planner', 'architect', 'validator'],
|
|
6791
|
+
});
|
|
6792
|
+
const owner = ownerResolution.owner || requestedOwner || 'mission-lead';
|
|
6793
|
+
|
|
6794
|
+
let room;
|
|
6795
|
+
try {
|
|
6796
|
+
room = buildMissionRoom(input, {
|
|
6797
|
+
owner,
|
|
6798
|
+
root: process.cwd(),
|
|
6799
|
+
ownerResolution,
|
|
6800
|
+
trustedRun: hasFlag(args, '--room-auto-run') && !hasFlag(args, '--no-room-auto-run'),
|
|
6801
|
+
verifier: DEFAULT_LONG_RUN_VERIFIER,
|
|
6802
|
+
});
|
|
6803
|
+
} catch (error) {
|
|
6804
|
+
exitMissionError(error.message || String(error), 1, asJson);
|
|
6805
|
+
}
|
|
6806
|
+
const { receipt, relativePath, room: persistedRoom } = writeMissionRoomReceipt(room, { root: process.cwd() });
|
|
6807
|
+
const payload = {
|
|
6808
|
+
ok: true,
|
|
6809
|
+
action: 'mission_room_created',
|
|
6810
|
+
room: persistedRoom,
|
|
6811
|
+
receipt_path: relativePath,
|
|
6812
|
+
receipt,
|
|
6813
|
+
};
|
|
6814
|
+
printJsonOrText(payload, missionRoomLines(persistedRoom, relativePath), asJson);
|
|
6815
|
+
}
|
|
6816
|
+
|
|
6817
|
+
function pruneRunsMission(args) {
|
|
6818
|
+
const asJson = wantsJson(args);
|
|
6819
|
+
let keepNewest;
|
|
6820
|
+
let keepDays;
|
|
6821
|
+
try {
|
|
6822
|
+
keepNewest = readNonNegativeIntegerFlag(args, '--keep-newest', 200);
|
|
6823
|
+
keepDays = readNonNegativeIntegerFlag(args, '--days', 14);
|
|
6824
|
+
} catch (error) {
|
|
6825
|
+
exitMissionError(error.message || String(error), 1, asJson);
|
|
6826
|
+
}
|
|
6827
|
+
const result = pruneRuns(process.cwd(), {
|
|
6828
|
+
apply: hasFlag(args, '--apply'),
|
|
6829
|
+
archive: !hasFlag(args, '--no-archive'),
|
|
6830
|
+
keepNewest,
|
|
6831
|
+
keepDays,
|
|
6832
|
+
});
|
|
6833
|
+
printJsonOrText(
|
|
6834
|
+
{ ok: !result.errors.length, ...result },
|
|
6835
|
+
runsPruneLines(result),
|
|
6836
|
+
asJson,
|
|
6837
|
+
);
|
|
6838
|
+
if (result.errors.length) process.exitCode = 1;
|
|
6839
|
+
}
|
|
6840
|
+
|
|
3480
6841
|
function missionCommand(args) {
|
|
3481
6842
|
const subcommand = args[0] || 'status';
|
|
3482
6843
|
const rest = args.slice(1);
|
|
@@ -3488,7 +6849,13 @@ function missionCommand(args) {
|
|
|
3488
6849
|
case 'status':
|
|
3489
6850
|
case 'list':
|
|
3490
6851
|
case 'ls':
|
|
6852
|
+
case 'show':
|
|
6853
|
+
case 'info':
|
|
6854
|
+
case 'view':
|
|
3491
6855
|
return statusMission(rest);
|
|
6856
|
+
case 'doctor':
|
|
6857
|
+
case 'check':
|
|
6858
|
+
return doctorMission(rest);
|
|
3492
6859
|
case 'attach-task':
|
|
3493
6860
|
case 'ensure-task':
|
|
3494
6861
|
case 'task-spine':
|
|
@@ -3496,10 +6863,19 @@ function missionCommand(args) {
|
|
|
3496
6863
|
case 'report':
|
|
3497
6864
|
case 'debrief':
|
|
3498
6865
|
return reportMission(rest);
|
|
6866
|
+
case 'timeline':
|
|
6867
|
+
case 'landings':
|
|
6868
|
+
return timelineMission(rest);
|
|
3499
6869
|
case 'watch':
|
|
3500
6870
|
return watchMission(rest);
|
|
3501
6871
|
case 'layers':
|
|
3502
6872
|
return layersMission(rest);
|
|
6873
|
+
case 'room':
|
|
6874
|
+
return roomMission(rest);
|
|
6875
|
+
case 'prune-runs':
|
|
6876
|
+
case 'runs-prune':
|
|
6877
|
+
case 'clean-runs':
|
|
6878
|
+
return pruneRunsMission(rest);
|
|
3503
6879
|
case 'goal':
|
|
3504
6880
|
case 'codex-goal':
|
|
3505
6881
|
return goalMission(rest);
|
|
@@ -3538,6 +6914,7 @@ module.exports = {
|
|
|
3538
6914
|
cappedClaudeReceiptText,
|
|
3539
6915
|
extractLayerFromReceiptText,
|
|
3540
6916
|
classifyPathsByLayer,
|
|
6917
|
+
collectMissionDoctorFindings,
|
|
3541
6918
|
resolveClaudeRunnerModel,
|
|
3542
6919
|
resolveClaudeRunnerBin,
|
|
3543
6920
|
businessIdForAtris2Mission,
|