atris 3.31.0 → 3.33.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -4
- package/atris/CLAUDE.md +8 -0
- package/atris.md +8 -0
- package/ax +458 -15
- package/bin/atris.js +200 -76
- package/commands/autoland.js +78 -12
- package/commands/autopilot-front.js +273 -0
- package/commands/engine.js +299 -0
- package/commands/init.js +21 -0
- package/commands/integrations.js +147 -0
- package/commands/member.js +85 -0
- package/commands/mission.js +500 -43
- package/commands/run-front.js +144 -0
- package/commands/run.js +9 -6
- package/commands/sign.js +90 -0
- package/commands/task.js +380 -20
- package/commands/truth.js +72 -11
- package/lib/autoland.js +133 -25
- package/lib/fleet.js +354 -0
- package/lib/inspect-fields.js +174 -0
- package/lib/operator-next.js +7 -0
- package/lib/pulse.js +4 -3
- package/lib/runner-command.js +54 -11
- package/lib/task-db.js +57 -2
- package/package.json +3 -2
- package/scripts/agent_worktree.py +72 -0
package/commands/mission.js
CHANGED
|
@@ -29,6 +29,16 @@ const {
|
|
|
29
29
|
runsPruneLines,
|
|
30
30
|
formatBytes,
|
|
31
31
|
} = require('../lib/runs-prune');
|
|
32
|
+
const { operatorReady, hasAgentJargon } = require('./autoland');
|
|
33
|
+
const {
|
|
34
|
+
MISSION_INSPECT_FIELDS,
|
|
35
|
+
readFieldsFlag,
|
|
36
|
+
stripInspectArgs,
|
|
37
|
+
validateFields,
|
|
38
|
+
missionInspectFieldValues,
|
|
39
|
+
inspectTextLines,
|
|
40
|
+
buildInspectPayload,
|
|
41
|
+
} = require('../lib/inspect-fields');
|
|
32
42
|
|
|
33
43
|
const VALID_STATUSES = new Set(['planning', 'running', 'ready', 'paused', 'blocked', 'stopped', 'complete']);
|
|
34
44
|
const TERMINAL_STATUSES = new Set(['stopped', 'complete']);
|
|
@@ -90,6 +100,28 @@ function exitMissionError(message, code = 1, asJson = false) {
|
|
|
90
100
|
process.exit(code);
|
|
91
101
|
}
|
|
92
102
|
|
|
103
|
+
// Write-time warnings judge only what a machine can truly judge: identifiers,
|
|
104
|
+
// flags, and task ids. Whether a why is present is a judgment call — the tick
|
|
105
|
+
// prompt demands it, the review pass judges it. A warning that cries wolf on
|
|
106
|
+
// plain sentences teaches agents to ignore it (golden-path papercut). The
|
|
107
|
+
// strict operatorReady bar stays at the digest surface, where under-showing
|
|
108
|
+
// is cheap. Boundary pinned by the marker-free fixture in mission-status tests.
|
|
109
|
+
function warnIfSummaryNeedsOperatorWhy(summary) {
|
|
110
|
+
const text = String(summary || '').trim();
|
|
111
|
+
if (!text || !hasAgentJargon(text)) return null;
|
|
112
|
+
const warning = 'Warning: this tick summary contains flags, ids, or code identifiers. Rewrite it in words the operator can use; identifiers belong in the receipt body.';
|
|
113
|
+
console.error(warning);
|
|
114
|
+
return warning;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function warnIfTaskTitleNeedsOperatorWhy(title) {
|
|
118
|
+
const text = String(title || '').trim();
|
|
119
|
+
if (!text || !hasAgentJargon(text)) return null;
|
|
120
|
+
const warning = 'Warning: this task title contains flags, ids, or code identifiers. Rewrite it in words the operator can use; identifiers belong in the task body.';
|
|
121
|
+
console.error(warning);
|
|
122
|
+
return warning;
|
|
123
|
+
}
|
|
124
|
+
|
|
93
125
|
function readPositiveIntegerFlag(args, name, fallback = null, options = {}) {
|
|
94
126
|
const raw = readFlag(args, name, '');
|
|
95
127
|
if (!raw) return fallback;
|
|
@@ -171,6 +203,7 @@ function printJsonOrText(payload, lines, asJson) {
|
|
|
171
203
|
}
|
|
172
204
|
|
|
173
205
|
const MISSION_RUN_VALUE_FLAGS = [
|
|
206
|
+
'--slots',
|
|
174
207
|
'--max-ticks',
|
|
175
208
|
'--max-wall',
|
|
176
209
|
'--minutes',
|
|
@@ -190,6 +223,8 @@ const MISSION_RUN_VALUE_FLAGS = [
|
|
|
190
223
|
const MISSION_RUN_BOOLEAN_FLAGS = [
|
|
191
224
|
'--json',
|
|
192
225
|
'--due',
|
|
226
|
+
'--fleet',
|
|
227
|
+
'--dry-run',
|
|
193
228
|
'--no-claude',
|
|
194
229
|
'--no-verify',
|
|
195
230
|
'--complete-on-pass',
|
|
@@ -204,6 +239,7 @@ const MISSION_RUN_BOOLEAN_FLAGS = [
|
|
|
204
239
|
'--no-room-auto-run',
|
|
205
240
|
'--allow-native-goal-supersede',
|
|
206
241
|
'--supersede-paused-native-goal',
|
|
242
|
+
'--take-goal-slot',
|
|
207
243
|
'--always-on',
|
|
208
244
|
'--xp-task',
|
|
209
245
|
'--agent-xp',
|
|
@@ -328,6 +364,7 @@ function createMissionXpTask(mission, root = process.cwd(), asJson = false) {
|
|
|
328
364
|
const db = taskDb.open();
|
|
329
365
|
const workspaceRoot = taskDb.workspaceRoot(root);
|
|
330
366
|
const title = `Mission XP: ${mission.objective}`;
|
|
367
|
+
const operatorTitleWarning = warnIfTaskTitleNeedsOperatorWhy(title);
|
|
331
368
|
const ownerResolution = resolveMissionOwner(mission, workspaceRoot);
|
|
332
369
|
const owner = ownerResolution.owner;
|
|
333
370
|
const metadata = {
|
|
@@ -371,6 +408,7 @@ function createMissionXpTask(mission, root = process.cwd(), asJson = false) {
|
|
|
371
408
|
task_id: result.id,
|
|
372
409
|
ref: missionTaskRef(task) || result.id,
|
|
373
410
|
title,
|
|
411
|
+
operator_title_warning: operatorTitleWarning,
|
|
374
412
|
status: task?.status || 'claimed',
|
|
375
413
|
assigned_to: owner,
|
|
376
414
|
owner_resolution: ownerResolution.reason,
|
|
@@ -499,10 +537,72 @@ function listWorktreeRollupMissions(root = process.cwd()) {
|
|
|
499
537
|
return rolled;
|
|
500
538
|
}
|
|
501
539
|
|
|
540
|
+
function missionMatchesRef(mission, ref) {
|
|
541
|
+
return mission && (mission.id === ref || mission.id.startsWith(ref) || mission.slug === ref);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function listSiblingWorkspaceMissionHints(ref, root = process.cwd(), limit = 5) {
|
|
545
|
+
if (!ref) return [];
|
|
546
|
+
const baseRoot = path.resolve(root);
|
|
547
|
+
const parent = path.dirname(baseRoot);
|
|
548
|
+
let here = baseRoot;
|
|
549
|
+
try {
|
|
550
|
+
here = fs.realpathSync(baseRoot);
|
|
551
|
+
} catch { /* keep raw path */ }
|
|
552
|
+
let entries = [];
|
|
553
|
+
try {
|
|
554
|
+
entries = fs.readdirSync(parent, { withFileTypes: true });
|
|
555
|
+
} catch {
|
|
556
|
+
return [];
|
|
557
|
+
}
|
|
558
|
+
const hints = [];
|
|
559
|
+
for (const entry of entries) {
|
|
560
|
+
if (!entry.isDirectory() || entry.name.startsWith('.')) continue;
|
|
561
|
+
const candidateRoot = path.join(parent, entry.name);
|
|
562
|
+
let candidateReal = candidateRoot;
|
|
563
|
+
try {
|
|
564
|
+
candidateReal = fs.realpathSync(candidateRoot);
|
|
565
|
+
} catch {
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
if (candidateReal === here) continue;
|
|
569
|
+
if (!fs.existsSync(path.join(candidateRoot, '.atris', 'state', 'missions.jsonl'))) continue;
|
|
570
|
+
const mission = listMissions(candidateRoot).find((row) => missionMatchesRef(row, ref));
|
|
571
|
+
if (!mission) continue;
|
|
572
|
+
hints.push({
|
|
573
|
+
id: mission.id,
|
|
574
|
+
status: mission.status,
|
|
575
|
+
objective: mission.objective,
|
|
576
|
+
workspace_root: candidateRoot,
|
|
577
|
+
command: `cd ${shellQuote(candidateRoot)} && atris mission status ${mission.id}`,
|
|
578
|
+
});
|
|
579
|
+
if (hints.length >= limit) break;
|
|
580
|
+
}
|
|
581
|
+
return hints;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function exitMissingMission(ref, code = 1, asJson = false) {
|
|
585
|
+
const error = `Mission "${ref}" not found.`;
|
|
586
|
+
const hints = listSiblingWorkspaceMissionHints(ref);
|
|
587
|
+
if (asJson) {
|
|
588
|
+
const payload = { ok: false, error };
|
|
589
|
+
if (hints.length) payload.workspace_hint = hints[0];
|
|
590
|
+
if (hints.length > 1) payload.workspace_hints = hints;
|
|
591
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
592
|
+
} else {
|
|
593
|
+
console.error(error);
|
|
594
|
+
if (hints.length) {
|
|
595
|
+
console.error(`Workspace hint: mission ${hints[0].id} exists in ${hints[0].workspace_root}.`);
|
|
596
|
+
console.error(`Run: ${hints[0].command}`);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
process.exit(code);
|
|
600
|
+
}
|
|
601
|
+
|
|
502
602
|
function resolveMission(ref, root = process.cwd()) {
|
|
503
603
|
const missions = listMissions(root);
|
|
504
604
|
if (!ref) return missions.find((mission) => !TERMINAL_STATUSES.has(mission.status)) || missions[0] || null;
|
|
505
|
-
return missions.find((mission) =>
|
|
605
|
+
return missions.find((mission) => missionMatchesRef(mission, ref)) || null;
|
|
506
606
|
}
|
|
507
607
|
|
|
508
608
|
function missionMatchesStatusFilter(mission, statusFilter) {
|
|
@@ -1532,7 +1632,7 @@ function missionFromArgs(args) {
|
|
|
1532
1632
|
'--native-goal-objective',
|
|
1533
1633
|
'--visible-goal-status',
|
|
1534
1634
|
'--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();
|
|
1635
|
+
], ['--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', '--take-goal-slot']).join(' ').trim();
|
|
1536
1636
|
if (!objective) {
|
|
1537
1637
|
exitMissionError('Usage: atris mission start "<objective>" --owner <member> [--verify "..."] [--cadence manual] [--worktree]', 1, wantsJson(args));
|
|
1538
1638
|
}
|
|
@@ -2008,7 +2108,51 @@ function normalizeMissionOwner(value) {
|
|
|
2008
2108
|
return String(value || '').trim().toLowerCase();
|
|
2009
2109
|
}
|
|
2010
2110
|
|
|
2011
|
-
function
|
|
2111
|
+
function readMissionMemberRole(owner, root = process.cwd()) {
|
|
2112
|
+
const dir = memberDir(owner, root);
|
|
2113
|
+
if (!dir) return '';
|
|
2114
|
+
try {
|
|
2115
|
+
const text = fs.readFileSync(path.join(dir, 'MEMBER.md'), 'utf8');
|
|
2116
|
+
const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
2117
|
+
if (!match) return '';
|
|
2118
|
+
const role = match[1].match(/^role:\s*(.+)$/im);
|
|
2119
|
+
return role ? role[1].replace(/^["']|["']$/g, '').trim() : '';
|
|
2120
|
+
} catch {
|
|
2121
|
+
return '';
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
function missionTasteProfileForRole(role) {
|
|
2126
|
+
const text = String(role || '').toLowerCase();
|
|
2127
|
+
if (!text) return null;
|
|
2128
|
+
if (/\b(security|risk|trust|privacy|compliance|safety|safe|guard|inspector)\b/.test(text)) {
|
|
2129
|
+
return {
|
|
2130
|
+
id: 'security_guard',
|
|
2131
|
+
name: 'Security / trust',
|
|
2132
|
+
bias: 'Prefer work that prevents unsafe behavior, privacy mistakes, or quiet trust breaks.',
|
|
2133
|
+
role_reason: 'Member role says guard risk before spending tokens.',
|
|
2134
|
+
};
|
|
2135
|
+
}
|
|
2136
|
+
if (/\b(demo|reliability|ux|usability|support|operator|launcher|customer|growth|onboarding)\b/.test(text)) {
|
|
2137
|
+
return {
|
|
2138
|
+
id: 'usability_operator',
|
|
2139
|
+
name: 'Usability / demo',
|
|
2140
|
+
bias: 'Prefer work that makes the product easier, faster, clearer, or more demo-ready.',
|
|
2141
|
+
role_reason: 'Member role says optimize usability before spending tokens.',
|
|
2142
|
+
};
|
|
2143
|
+
}
|
|
2144
|
+
if (/\b(technical|engineer|architect|research|improver|runtime|compiler|infra|backend|agent|model)\b/.test(text)) {
|
|
2145
|
+
return {
|
|
2146
|
+
id: 'technical_homerun',
|
|
2147
|
+
name: 'Technical homerun',
|
|
2148
|
+
bias: 'Prefer technical leaps, but require a usability or proof gate before spending serious tokens.',
|
|
2149
|
+
role_reason: 'Member role says chase validated technical homeruns.',
|
|
2150
|
+
};
|
|
2151
|
+
}
|
|
2152
|
+
return null;
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
function missionOwnerTasteProfile(owner) {
|
|
2012
2156
|
const key = normalizeMissionOwner(owner);
|
|
2013
2157
|
if (/\b(security|sync-inspector|proof-inspector|validator)\b/.test(key)) {
|
|
2014
2158
|
return {
|
|
@@ -2031,13 +2175,25 @@ function missionMemberTasteProfile(owner) {
|
|
|
2031
2175
|
};
|
|
2032
2176
|
}
|
|
2033
2177
|
|
|
2178
|
+
function missionMemberTasteProfile(owner, root = process.cwd()) {
|
|
2179
|
+
const role = readMissionMemberRole(owner, root);
|
|
2180
|
+
const roleProfile = missionTasteProfileForRole(role);
|
|
2181
|
+
const profile = roleProfile || missionOwnerTasteProfile(owner);
|
|
2182
|
+
return {
|
|
2183
|
+
...profile,
|
|
2184
|
+
role: role || null,
|
|
2185
|
+
role_source: roleProfile ? `atris/team/${normalizeMissionOwner(owner)}/MEMBER.md` : null,
|
|
2186
|
+
role_reason: roleProfile?.role_reason || null,
|
|
2187
|
+
};
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2034
2190
|
function missionValueSignals(title) {
|
|
2035
2191
|
const text = String(title || '').toLowerCase();
|
|
2036
2192
|
const signals = [];
|
|
2037
2193
|
const add = (id, label, pattern) => {
|
|
2038
2194
|
if (pattern.test(text)) signals.push({ id, label });
|
|
2039
2195
|
};
|
|
2040
|
-
add('usability', 'simplifies use', /\b(simple|simplify|
|
|
2196
|
+
add('usability', 'simplifies use', /\b(simple|simplify|previews?|clear|plain|feynman|demos?|onboarding|ux|workflow|usable|understand)\b/);
|
|
2041
2197
|
add('speed', 'speeds the process', /\b(speed|fast|faster|latency|token|waste|friction|shortcut|automation|auto)\b/);
|
|
2042
2198
|
add('users_revenue', 'can help users or revenue', /\b(user|users|customer|revenue|payment|checkout|pricing|conversion|retention|demo)\b/);
|
|
2043
2199
|
add('trust', 'protects trust', /\b(security|safe|safety|trust|permission|approval|auth|privacy|gmail|email|connector|leak|isolation)\b/);
|
|
@@ -2265,6 +2421,7 @@ function missionTasteMemoryReason(tasteMemory, signals) {
|
|
|
2265
2421
|
|
|
2266
2422
|
function missionPreviewWhyNow(move, profile, signals, tasteMemory = null) {
|
|
2267
2423
|
const ids = new Set((signals || []).map((signal) => signal.id));
|
|
2424
|
+
const roleReason = profile?.role_reason ? `${profile.role_reason} ` : '';
|
|
2268
2425
|
let base = '';
|
|
2269
2426
|
if (profile.id === 'technical_homerun') {
|
|
2270
2427
|
base = ids.has('technical')
|
|
@@ -2282,7 +2439,7 @@ function missionPreviewWhyNow(move, profile, signals, tasteMemory = null) {
|
|
|
2282
2439
|
base = move?.why || 'This needs a clear reason before it should spend serious tokens.';
|
|
2283
2440
|
}
|
|
2284
2441
|
const memoryReason = missionTasteMemoryReason(tasteMemory, signals);
|
|
2285
|
-
return memoryReason ? `${base} Taste memory says: ${memoryReason}` : base
|
|
2442
|
+
return memoryReason ? `${roleReason}${base} Taste memory says: ${memoryReason}` : `${roleReason}${base}`;
|
|
2286
2443
|
}
|
|
2287
2444
|
|
|
2288
2445
|
function missionPreviewRisk(signals) {
|
|
@@ -2301,7 +2458,7 @@ function missionPreviewValidation(signals) {
|
|
|
2301
2458
|
}
|
|
2302
2459
|
|
|
2303
2460
|
function missionValuePreview(move, mission, root = process.cwd()) {
|
|
2304
|
-
const profile = missionMemberTasteProfile(mission?.owner);
|
|
2461
|
+
const profile = missionMemberTasteProfile(mission?.owner, root);
|
|
2305
2462
|
const signals = missionValueSignals(move?.title);
|
|
2306
2463
|
const tasteMemory = readMissionTasteMemory(root, mission?.owner);
|
|
2307
2464
|
const score = missionValueScore(signals, profile, tasteMemory);
|
|
@@ -2651,6 +2808,9 @@ function startMission(args) {
|
|
|
2651
2808
|
const warnings = [missingVerifierWarning(mission)].filter(Boolean);
|
|
2652
2809
|
ensureMemberMissionFile(mission.owner, process.cwd(), mission.objective);
|
|
2653
2810
|
const { mission: saved } = saveMission(mission, process.cwd(), 'mission_started', { objective: mission.objective });
|
|
2811
|
+
const goalSlotHandoff = hasFlag(args, '--take-goal-slot') && isCodexGoalMission(saved)
|
|
2812
|
+
? takeCodexGoalSlotForMission(saved, process.cwd())
|
|
2813
|
+
: null;
|
|
2654
2814
|
const memberState = renderMemberMissionState(saved.owner);
|
|
2655
2815
|
const logPath = appendMemberLog(saved.owner, 'Mission started', {
|
|
2656
2816
|
mission: saved.objective,
|
|
@@ -2661,7 +2821,7 @@ function startMission(args) {
|
|
|
2661
2821
|
});
|
|
2662
2822
|
const worktreeBaseline = captureMissionWorktreeBaseline(saved, process.cwd());
|
|
2663
2823
|
printJsonOrText(
|
|
2664
|
-
{ ok: true, action: 'mission_started', mission: saved, warnings, state_path: statePaths().missionsJsonl, member_state: memberState, log_path: logPath, worktree_baseline: worktreeBaseline ? { path: path.relative(process.cwd(), missionBaselinePath(saved.id)), dirty_count: worktreeBaseline.dirty_count, dirty_hash: worktreeBaseline.dirty_hash } : null },
|
|
2824
|
+
{ ok: true, action: 'mission_started', mission: saved, warnings, goal_slot_handoff: goalSlotHandoff, state_path: statePaths().missionsJsonl, member_state: memberState, log_path: logPath, worktree_baseline: worktreeBaseline ? { path: path.relative(process.cwd(), missionBaselinePath(saved.id)), dirty_count: worktreeBaseline.dirty_count, dirty_hash: worktreeBaseline.dirty_hash } : null },
|
|
2665
2825
|
[
|
|
2666
2826
|
`Started mission: ${saved.objective}`,
|
|
2667
2827
|
`Owner: ${saved.owner}`,
|
|
@@ -2776,6 +2936,9 @@ function startMissionFromRunObjective(objective, args) {
|
|
|
2776
2936
|
...(nativeGoalObjective ? { nativeGoalObjective } : {}),
|
|
2777
2937
|
...(hasFlag(args, '--allow-native-goal-supersede') || hasFlag(args, '--supersede-paused-native-goal') ? { allowNativeGoalSupersede: true } : {}),
|
|
2778
2938
|
};
|
|
2939
|
+
const goalSlotHandoff = hasFlag(args, '--take-goal-slot') && isCodexGoalMission(saved)
|
|
2940
|
+
? takeCodexGoalSlotForMission(saved, process.cwd(), nativeGoalOptions)
|
|
2941
|
+
: null;
|
|
2779
2942
|
const atrisGoalState = refreshAtrisGoalController(process.cwd(), { missionId: saved.id });
|
|
2780
2943
|
const codexGoalState = runnerUsesCallerSession(saved.runner)
|
|
2781
2944
|
? refreshCodexGoalController(process.cwd(), { missionId: saved.id, ...nativeGoalOptions })
|
|
@@ -2800,6 +2963,7 @@ function startMissionFromRunObjective(objective, args) {
|
|
|
2800
2963
|
} : null,
|
|
2801
2964
|
completed_continuation_goal: completedContinuationGoal,
|
|
2802
2965
|
direct_goal_request: directGoalRequest,
|
|
2966
|
+
goal_slot_handoff: goalSlotHandoff,
|
|
2803
2967
|
atris_goal_state: atrisGoalState,
|
|
2804
2968
|
codex_goal_state: codexGoalState,
|
|
2805
2969
|
requires_native_goal_start: codexGoalState?.requires_native_goal_start === true || codexGoalState?.goal?.requires_native_goal_start === true,
|
|
@@ -2821,7 +2985,7 @@ function attachMissionTask(args) {
|
|
|
2821
2985
|
}
|
|
2822
2986
|
let mission = resolveMission(ref);
|
|
2823
2987
|
if (!mission) {
|
|
2824
|
-
|
|
2988
|
+
exitMissingMission(ref, 1, asJson);
|
|
2825
2989
|
}
|
|
2826
2990
|
|
|
2827
2991
|
const lock = acquireMissionLock(mission.id, process.cwd(), { waitMs: 2000 });
|
|
@@ -2965,7 +3129,7 @@ function statusMission(args) {
|
|
|
2965
3129
|
if (!ref && statusFilter) missions = missions.filter((mission) => missionMatchesStatusFilter(mission, statusFilter));
|
|
2966
3130
|
if (!ref && limit) missions = missions.slice(0, limit);
|
|
2967
3131
|
if (ref && !missions.length) {
|
|
2968
|
-
|
|
3132
|
+
exitMissingMission(ref, 1, asJson);
|
|
2969
3133
|
}
|
|
2970
3134
|
const missionViews = missions.map(missionStatusView);
|
|
2971
3135
|
// Member state renders are cwd-local writes; rolled-up missions stay read-only.
|
|
@@ -3147,7 +3311,7 @@ function missionReportTimeline(mission, root = process.cwd(), limit = 6) {
|
|
|
3147
3311
|
return items.slice(Math.max(0, items.length - limit));
|
|
3148
3312
|
}
|
|
3149
3313
|
|
|
3150
|
-
function missionLandingTimeline(mission, root = process.cwd(), limit = 12) {
|
|
3314
|
+
function missionLandingTimeline(mission, root = process.cwd(), limit = 12, { kind = null, since = null } = {}) {
|
|
3151
3315
|
const paths = statePaths(root);
|
|
3152
3316
|
let files = [];
|
|
3153
3317
|
try {
|
|
@@ -3158,6 +3322,8 @@ function missionLandingTimeline(mission, root = process.cwd(), limit = 12) {
|
|
|
3158
3322
|
files = [];
|
|
3159
3323
|
}
|
|
3160
3324
|
|
|
3325
|
+
const kindFilter = kind ? String(kind).trim() : null;
|
|
3326
|
+
const sinceFilter = since ? String(since).trim() : null;
|
|
3161
3327
|
const items = [];
|
|
3162
3328
|
const seen = new Set();
|
|
3163
3329
|
for (const file of files) {
|
|
@@ -3168,6 +3334,9 @@ function missionLandingTimeline(mission, root = process.cwd(), limit = 12) {
|
|
|
3168
3334
|
continue;
|
|
3169
3335
|
}
|
|
3170
3336
|
if (!receipt || receipt.mission_id !== mission.id) continue;
|
|
3337
|
+
const receiptKind = receipt.result && receipt.result.kind ? String(receipt.result.kind) : (receipt.kind ? String(receipt.kind) : '');
|
|
3338
|
+
if (kindFilter && receiptKind !== kindFilter) continue;
|
|
3339
|
+
if (sinceFilter && String(receipt.at || '') < sinceFilter) continue;
|
|
3171
3340
|
const landing = receipt.result && receipt.result.landing;
|
|
3172
3341
|
if (landing && landing.timeline_visible === false) continue;
|
|
3173
3342
|
const changed = String(landing && landing.changed || '').trim();
|
|
@@ -3426,7 +3595,7 @@ function reportMission(args) {
|
|
|
3426
3595
|
missions.sort((a, b) => String(b.updated_at || b.created_at || '').localeCompare(String(a.updated_at || a.created_at || '')));
|
|
3427
3596
|
}
|
|
3428
3597
|
if (ref && !missions.length) {
|
|
3429
|
-
|
|
3598
|
+
exitMissingMission(ref, 1, asJson);
|
|
3430
3599
|
}
|
|
3431
3600
|
missions = missions.slice(0, limit);
|
|
3432
3601
|
const reports = missions.map((mission) => missionReportFor(mission, mission.worktree_root || process.cwd()));
|
|
@@ -3458,14 +3627,16 @@ function timelineMission(args) {
|
|
|
3458
3627
|
const all = hasFlag(args, '--all');
|
|
3459
3628
|
const prunePreviewRequested = hasFlag(args, '--prune-preview');
|
|
3460
3629
|
const outputPath = readFlag(args, '--output', '') || readFlag(args, '--out', '');
|
|
3461
|
-
const
|
|
3630
|
+
const kindFilter = readFlag(args, '--kind', '') || null;
|
|
3631
|
+
const sinceFilter = readFlag(args, '--since', '') || null;
|
|
3632
|
+
const ref = stripKnownFlags(args, ['--limit', '--output', '--out', '--kind', '--since'], ['--json', '--write', '--all', '--prune-preview'])[0] || '';
|
|
3462
3633
|
const limit = all ? Number.MAX_SAFE_INTEGER : readPositiveIntegerFlag(args, '--limit', 12, { json: asJson });
|
|
3463
3634
|
const missions = listMissions();
|
|
3464
3635
|
const mission = ref
|
|
3465
3636
|
? resolveMission(ref)
|
|
3466
3637
|
: (missions.find((row) => !TERMINAL_STATUSES.has(row.status)) || missions[0] || null);
|
|
3467
3638
|
if (ref && !mission) {
|
|
3468
|
-
|
|
3639
|
+
exitMissingMission(ref, 1, asJson);
|
|
3469
3640
|
}
|
|
3470
3641
|
if (!mission) {
|
|
3471
3642
|
printJsonOrText(
|
|
@@ -3482,7 +3653,7 @@ function timelineMission(args) {
|
|
|
3482
3653
|
return;
|
|
3483
3654
|
}
|
|
3484
3655
|
const root = mission.worktree_root || process.cwd();
|
|
3485
|
-
const timelineResult = missionLandingTimeline(mission, root, limit);
|
|
3656
|
+
const timelineResult = missionLandingTimeline(mission, root, limit, { kind: kindFilter, since: sinceFilter });
|
|
3486
3657
|
const timeline = timelineResult.items;
|
|
3487
3658
|
const currentLanding = missionTimelineCurrentLanding(timeline);
|
|
3488
3659
|
const timelineItemDisplay = (item) => ({
|
|
@@ -3652,8 +3823,14 @@ function timelineMission(args) {
|
|
|
3652
3823
|
label: 'Filters',
|
|
3653
3824
|
active_label: 'Active filter',
|
|
3654
3825
|
active: all ? 'full_history' : 'latest',
|
|
3826
|
+
mission_label: 'Mission',
|
|
3827
|
+
mission: mission.id,
|
|
3655
3828
|
limit_label: 'Limit',
|
|
3656
3829
|
limit: timelineResult.meta.limit,
|
|
3830
|
+
kind_label: 'Kind',
|
|
3831
|
+
kind: kindFilter,
|
|
3832
|
+
since_label: 'Since',
|
|
3833
|
+
since: sinceFilter,
|
|
3657
3834
|
shown_count: timelineResult.meta.shown_count,
|
|
3658
3835
|
total_count: timelineResult.meta.total_count,
|
|
3659
3836
|
hidden_count: timelineResult.meta.hidden_count,
|
|
@@ -3930,7 +4107,7 @@ function watchMission(args) {
|
|
|
3930
4107
|
return listMissions().filter((mission) => !HEARTBEAT_TERMINAL_STATUSES.has(mission.status));
|
|
3931
4108
|
};
|
|
3932
4109
|
if (ref && !loadTargets().length) {
|
|
3933
|
-
|
|
4110
|
+
exitMissingMission(ref, 1, false);
|
|
3934
4111
|
}
|
|
3935
4112
|
const stamp = () => new Date().toTimeString().slice(0, 8);
|
|
3936
4113
|
const shortId = (mission) => mission.id.length > 20 ? `…${mission.id.slice(-8)}` : mission.id;
|
|
@@ -4225,6 +4402,15 @@ const MISSION_RUN_DEFAULTS = {
|
|
|
4225
4402
|
backoff: { initialMs: 30_000, maxMs: 10 * 60_000, factor: 2, jitter: 0.3 },
|
|
4226
4403
|
};
|
|
4227
4404
|
|
|
4405
|
+
// Claude sessions accumulate context across resumed ticks; an always-on
|
|
4406
|
+
// mission would grow without bound. Continuity lives on disk (receipts, logs,
|
|
4407
|
+
// now.md), so a healthy session is disposable: rotate to a fresh one every N
|
|
4408
|
+
// ran ticks. Failure-path rotation (stale lock) stays separate below.
|
|
4409
|
+
const CLAUDE_SESSION_CONTEXT_ROTATE_TICKS = Math.max(
|
|
4410
|
+
1,
|
|
4411
|
+
Number(process.env.ATRIS_CLAUDE_SESSION_ROTATE_TICKS) || 8,
|
|
4412
|
+
);
|
|
4413
|
+
|
|
4228
4414
|
function runnerUsesCallerSession(runner) {
|
|
4229
4415
|
return new Set(['codex_goal', 'caller_session', 'current_agent']).has(String(runner || '').trim().toLowerCase());
|
|
4230
4416
|
}
|
|
@@ -4328,6 +4514,16 @@ function missionIsRunnable(mission) {
|
|
|
4328
4514
|
&& !missionHasHumanAsks(mission);
|
|
4329
4515
|
}
|
|
4330
4516
|
|
|
4517
|
+
// Fresh goal selection: moving work outranks review-parked work. A ready
|
|
4518
|
+
// mission stays selectable (its next action is native-goal completion /
|
|
4519
|
+
// review), but it must never beat a running or planning mission on recency.
|
|
4520
|
+
const GOAL_SELECTION_STATUS_RANK = { running: 0, planning: 1, ready: 2 };
|
|
4521
|
+
|
|
4522
|
+
function missionGoalSelectionRank(mission) {
|
|
4523
|
+
const rank = GOAL_SELECTION_STATUS_RANK[String(mission?.status || '')];
|
|
4524
|
+
return Number.isFinite(rank) ? rank : 3;
|
|
4525
|
+
}
|
|
4526
|
+
|
|
4331
4527
|
function missionSortTime(mission) {
|
|
4332
4528
|
return Date.parse(mission?.updated_at || mission?.created_at || '') || 0;
|
|
4333
4529
|
}
|
|
@@ -4382,6 +4578,10 @@ function selectCodexGoalMission(root = process.cwd(), options = {}, now = new Da
|
|
|
4382
4578
|
}
|
|
4383
4579
|
|
|
4384
4580
|
candidates.sort((a, b) => {
|
|
4581
|
+
const aRank = missionGoalSelectionRank(a);
|
|
4582
|
+
const bRank = missionGoalSelectionRank(b);
|
|
4583
|
+
if (aRank !== bRank) return aRank - bRank;
|
|
4584
|
+
|
|
4385
4585
|
const aCaller = runnerUsesCallerSession(a.runner) ? 1 : 0;
|
|
4386
4586
|
const bCaller = runnerUsesCallerSession(b.runner) ? 1 : 0;
|
|
4387
4587
|
if (aCaller !== bCaller) return bCaller - aCaller;
|
|
@@ -4409,6 +4609,10 @@ function selectAtrisGoalMission(root = process.cwd(), options = {}, now = new Da
|
|
|
4409
4609
|
if (exact) return { mission: exact, reason: 'selected' };
|
|
4410
4610
|
}
|
|
4411
4611
|
runnable.sort((a, b) => {
|
|
4612
|
+
const aRank = missionGoalSelectionRank(a);
|
|
4613
|
+
const bRank = missionGoalSelectionRank(b);
|
|
4614
|
+
if (aRank !== bRank) return aRank - bRank;
|
|
4615
|
+
|
|
4412
4616
|
const aDue = effectiveMissionVerifier(a) && missionDueAt(a, now) ? 1 : 0;
|
|
4413
4617
|
const bDue = effectiveMissionVerifier(b) && missionDueAt(b, now) ? 1 : 0;
|
|
4414
4618
|
if (aDue !== bDue) return bDue - aDue;
|
|
@@ -4427,14 +4631,50 @@ function codexGoalObjective(mission) {
|
|
|
4427
4631
|
return mission.objective;
|
|
4428
4632
|
}
|
|
4429
4633
|
|
|
4430
|
-
function codexNativeGoalAck(mission
|
|
4634
|
+
function codexNativeGoalAck(mission) {
|
|
4431
4635
|
const ack = mission?.native_goal_ack || null;
|
|
4432
4636
|
if (!ack || String(ack.runtime || '').toLowerCase() !== 'codex') return null;
|
|
4433
4637
|
if (ack.status !== 'active') return null;
|
|
4434
|
-
if (String(ack.
|
|
4638
|
+
if (ack.mission_id && String(ack.mission_id) !== String(mission?.id || '')) return null;
|
|
4435
4639
|
return ack;
|
|
4436
4640
|
}
|
|
4437
4641
|
|
|
4642
|
+
function supersedeOtherCodexNativeGoalAcks(root, activeMission, activeAck) {
|
|
4643
|
+
const activeMissionId = String(activeMission?.id || '');
|
|
4644
|
+
if (!activeMissionId) return [];
|
|
4645
|
+
const supersededAt = stampIso();
|
|
4646
|
+
const superseded = [];
|
|
4647
|
+
for (const mission of listMissions(root)) {
|
|
4648
|
+
if (String(mission.id || '') === activeMissionId) continue;
|
|
4649
|
+
const priorAck = codexNativeGoalAck(mission);
|
|
4650
|
+
if (!priorAck) continue;
|
|
4651
|
+
const nextAck = {
|
|
4652
|
+
...priorAck,
|
|
4653
|
+
status: 'superseded',
|
|
4654
|
+
superseded_at: supersededAt,
|
|
4655
|
+
superseded_by_mission_id: activeMissionId,
|
|
4656
|
+
superseded_by_objective: activeAck?.objective || activeMission.objective,
|
|
4657
|
+
};
|
|
4658
|
+
const { mission: saved } = saveMission(
|
|
4659
|
+
{ ...mission, native_goal_ack: nextAck },
|
|
4660
|
+
root,
|
|
4661
|
+
'mission_native_goal_ack_superseded',
|
|
4662
|
+
{
|
|
4663
|
+
superseded_by_mission_id: activeMissionId,
|
|
4664
|
+
superseded_by_objective: activeAck?.objective || activeMission.objective,
|
|
4665
|
+
previous_ack: priorAck,
|
|
4666
|
+
},
|
|
4667
|
+
);
|
|
4668
|
+
superseded.push({
|
|
4669
|
+
mission_id: saved.id,
|
|
4670
|
+
objective: saved.objective,
|
|
4671
|
+
previous_ack: priorAck,
|
|
4672
|
+
native_goal_ack: nextAck,
|
|
4673
|
+
});
|
|
4674
|
+
}
|
|
4675
|
+
return superseded;
|
|
4676
|
+
}
|
|
4677
|
+
|
|
4438
4678
|
function codexRuntimeGoalStateFromOptions(options = {}) {
|
|
4439
4679
|
const status = String(options.nativeGoalStatus || options.visibleGoalStatus || '').trim().toLowerCase();
|
|
4440
4680
|
const objective = String(options.nativeGoalObjective || options.visibleGoalObjective || '').trim();
|
|
@@ -4601,6 +4841,33 @@ function activeCodexVisibleGoalOwner(root = process.cwd(), excludeId = '', now =
|
|
|
4601
4841
|
return candidates[0] || null;
|
|
4602
4842
|
}
|
|
4603
4843
|
|
|
4844
|
+
function pauseMissionRecord(mission, reason, root = process.cwd()) {
|
|
4845
|
+
const next = {
|
|
4846
|
+
...mission,
|
|
4847
|
+
status: 'paused',
|
|
4848
|
+
paused_at: stampIso(),
|
|
4849
|
+
stop_reason: reason,
|
|
4850
|
+
next_action: `resume with: atris mission tick ${mission.id}`,
|
|
4851
|
+
};
|
|
4852
|
+
const { mission: saved } = saveMission(next, root, 'mission_paused', { reason, source: 'goal_slot_handoff' });
|
|
4853
|
+
appendMemberLog(saved.owner, 'Mission paused', { mission: saved.objective, reason });
|
|
4854
|
+
return saved;
|
|
4855
|
+
}
|
|
4856
|
+
|
|
4857
|
+
function takeCodexGoalSlotForMission(newMission, root = process.cwd(), options = {}) {
|
|
4858
|
+
if (!newMission || !isCodexGoalMission(newMission)) return null;
|
|
4859
|
+
const runtimeGoalState = codexRuntimeGoalStateFromOptions(options);
|
|
4860
|
+
const activeOwner = activeCodexVisibleGoalOwner(root, newMission.id, new Date(), runtimeGoalState);
|
|
4861
|
+
if (!activeOwner) return null;
|
|
4862
|
+
const reason = `visible goal replaced by ${newMission.id}`;
|
|
4863
|
+
const paused = pauseMissionRecord(activeOwner, reason, root);
|
|
4864
|
+
return {
|
|
4865
|
+
paused_mission_id: paused.id,
|
|
4866
|
+
paused_mission: missionStatusView(paused),
|
|
4867
|
+
reason,
|
|
4868
|
+
};
|
|
4869
|
+
}
|
|
4870
|
+
|
|
4604
4871
|
function codexGoalActiveConflictPayload(newMission, activeMission, request = null, heartbeatMode = false, runtimeGoalState = null) {
|
|
4605
4872
|
const newObjective = codexGoalObjective(newMission);
|
|
4606
4873
|
const pausedConflict = runtimeGoalState?.status === 'paused';
|
|
@@ -4899,7 +5166,7 @@ function buildCodexGoalPayload(root = process.cwd(), options = {}) {
|
|
|
4899
5166
|
const taskSpine = missionTaskSpine(mission);
|
|
4900
5167
|
const missionView = missionStatusView(mission);
|
|
4901
5168
|
const objective = codexGoalObjective(mission);
|
|
4902
|
-
const ack = codexNativeGoalAck(mission
|
|
5169
|
+
const ack = codexNativeGoalAck(mission);
|
|
4903
5170
|
const runtimeNeedsReplace = !ack && codexRuntimeGoalNeedsReplace(runtimeGoalState, objective);
|
|
4904
5171
|
const nativeGoalAction = ack
|
|
4905
5172
|
? null
|
|
@@ -5315,9 +5582,30 @@ function probeClaudeBinary() {
|
|
|
5315
5582
|
return { ok: true };
|
|
5316
5583
|
}
|
|
5317
5584
|
|
|
5318
|
-
|
|
5585
|
+
// Pull unread operator pings off the mission and mark them consumed, so the
|
|
5586
|
+
// next tick's prompt carries them exactly once. Pings are how a human talks to
|
|
5587
|
+
// an always-on member mid-run: atris member ping <name> "<msg>".
|
|
5588
|
+
function consumeMissionPings(mission, cwd) {
|
|
5589
|
+
const pending = (Array.isArray(mission.pings) ? mission.pings : []).filter((p) => p && !p.consumed_at);
|
|
5590
|
+
if (!pending.length) return { mission, pings: [] };
|
|
5591
|
+
const consumedAt = stampIso();
|
|
5592
|
+
const pings = (mission.pings || []).map((p) => (p && !p.consumed_at ? { ...p, consumed_at: consumedAt } : p));
|
|
5593
|
+
const saved = saveMission({ ...mission, pings }, cwd, 'mission_pings_consumed', { count: pending.length }).mission;
|
|
5594
|
+
return { mission: saved, pings: pending };
|
|
5595
|
+
}
|
|
5596
|
+
|
|
5597
|
+
function buildTickPrompt(mission, tickIndex, maxTicks, frozen, pings = []) {
|
|
5598
|
+
const pingLines = pings.length
|
|
5599
|
+
? [
|
|
5600
|
+
``,
|
|
5601
|
+
`## Operator pings (read these first)`,
|
|
5602
|
+
`Your operator sent ${pings.length === 1 ? 'a message' : 'messages'} mid-run. Treat them as fresh direction for this tick (they do not change the frozen verifier or lane):`,
|
|
5603
|
+
...pings.map((p) => `- [${p.at}] ${p.from || 'operator'}: ${p.text}`),
|
|
5604
|
+
]
|
|
5605
|
+
: [];
|
|
5319
5606
|
const lines = [
|
|
5320
5607
|
`# Mission Tick ${tickIndex}/${maxTicks}`,
|
|
5608
|
+
...pingLines,
|
|
5321
5609
|
``,
|
|
5322
5610
|
`**Objective:** ${mission.objective}`,
|
|
5323
5611
|
`**Owner:** ${mission.owner}`,
|
|
@@ -5336,6 +5624,7 @@ function buildTickPrompt(mission, tickIndex, maxTicks, frozen) {
|
|
|
5336
5624
|
`- Pick the smallest concrete action that moves the mission forward.`,
|
|
5337
5625
|
`- Edit / run / research as needed for the lane.`,
|
|
5338
5626
|
`- After your work, the harness runs the frozen verifier — make sure it'll pass.`,
|
|
5627
|
+
`- Write the tick summary in operator language: what changed and what it buys or costs in plain words, with no flags, task ids, or code identifiers.`,
|
|
5339
5628
|
`- If you can't make progress this tick, say why explicitly. Don't fake it.`,
|
|
5340
5629
|
``,
|
|
5341
5630
|
`## Constraints`,
|
|
@@ -5405,7 +5694,10 @@ function spawnClaudeTick(mission, opts) {
|
|
|
5405
5694
|
else if (sessionMode === 'resume') args.push('--resume', sessionId);
|
|
5406
5695
|
|
|
5407
5696
|
const startedAt = Date.now();
|
|
5408
|
-
|
|
5697
|
+
// detached: the runner spawns its own children; killing only the direct
|
|
5698
|
+
// child on timeout leaves them holding the session lock, and the next
|
|
5699
|
+
// tick's resume fails with "Session ID ... is already in use".
|
|
5700
|
+
const proc = spawn(resolveClaudeRunnerBin(), args, { cwd, stdio: ['ignore', 'pipe', 'pipe'], detached: true });
|
|
5409
5701
|
|
|
5410
5702
|
let stdoutBuf = '';
|
|
5411
5703
|
let observedSessionIds = new Set();
|
|
@@ -5422,8 +5714,11 @@ function spawnClaudeTick(mission, opts) {
|
|
|
5422
5714
|
let aborted = false;
|
|
5423
5715
|
|
|
5424
5716
|
const kill = (reason) => {
|
|
5425
|
-
|
|
5426
|
-
|
|
5717
|
+
const killGroup = (sig) => {
|
|
5718
|
+
try { process.kill(-proc.pid, sig); } catch { try { proc.kill(sig); } catch {} }
|
|
5719
|
+
};
|
|
5720
|
+
killGroup('SIGTERM');
|
|
5721
|
+
setTimeout(() => killGroup('SIGKILL'), 3000).unref();
|
|
5427
5722
|
};
|
|
5428
5723
|
const timer = setTimeout(() => { timedOut = true; kill('timeout'); }, timeoutMs);
|
|
5429
5724
|
const onAbort = () => { aborted = true; kill('aborted'); };
|
|
@@ -5529,6 +5824,21 @@ async function runMission(args) {
|
|
|
5529
5824
|
help();
|
|
5530
5825
|
return;
|
|
5531
5826
|
}
|
|
5827
|
+
// --fleet: staff every idle capable engine on the board's claimable
|
|
5828
|
+
// safe-lane tasks, build in parallel worktrees, land serially. Humble flag,
|
|
5829
|
+
// full loop — see lib/fleet.js. --dry-run previews the staffing only.
|
|
5830
|
+
if (hasFlag(args, '--fleet')) {
|
|
5831
|
+
const { runFleetFlight } = require('../lib/fleet');
|
|
5832
|
+
const slots = Math.max(1, Number(readFlag(args, '--slots', '')) || 3);
|
|
5833
|
+
const flight = await runFleetFlight({
|
|
5834
|
+
slots,
|
|
5835
|
+
dryRun: hasFlag(args, '--dry-run'),
|
|
5836
|
+
log: asJson ? () => {} : console.log,
|
|
5837
|
+
});
|
|
5838
|
+
if (asJson) console.log(JSON.stringify(flight, null, 2));
|
|
5839
|
+
process.exitCode = flight.paused && flight.paused.length > 0 ? 1 : 0;
|
|
5840
|
+
return;
|
|
5841
|
+
}
|
|
5532
5842
|
const dueMode = hasFlag(args, '--due');
|
|
5533
5843
|
const skipClaude = hasFlag(args, '--no-claude');
|
|
5534
5844
|
const verifyEach = !hasFlag(args, '--no-verify');
|
|
@@ -5715,7 +6025,9 @@ async function runMission(args) {
|
|
|
5715
6025
|
claude: { skipped: true, reason: callerSessionRunner ? 'runner-uses-caller-session' : 'no-claude-mode' },
|
|
5716
6026
|
};
|
|
5717
6027
|
} else if (atris2Runner) {
|
|
5718
|
-
const
|
|
6028
|
+
const pingDrain = consumeMissionPings(mission, cwd);
|
|
6029
|
+
mission = pingDrain.mission;
|
|
6030
|
+
const prompt = buildTickPrompt(mission, tickIdx, effectiveMaxTicks, frozen, pingDrain.pings);
|
|
5719
6031
|
const { runAtris2Turn } = require('./probe');
|
|
5720
6032
|
const businessId = businessIdForAtris2Mission(mission, cwd);
|
|
5721
6033
|
const turn = await runAtris2Turn({
|
|
@@ -5745,7 +6057,9 @@ async function runMission(args) {
|
|
|
5745
6057
|
} else {
|
|
5746
6058
|
const sessionMode = sessionId ? 'resume' : 'set';
|
|
5747
6059
|
const useId = sessionId || pendingSessionId;
|
|
5748
|
-
const
|
|
6060
|
+
const pingDrain = consumeMissionPings(mission, cwd);
|
|
6061
|
+
mission = pingDrain.mission;
|
|
6062
|
+
const prompt = buildTickPrompt(mission, tickIdx, effectiveMaxTicks, frozen, pingDrain.pings);
|
|
5749
6063
|
const claudeResult = await spawnClaudeTick(mission, {
|
|
5750
6064
|
sessionMode, sessionId: useId, cwd, signal: controller.signal,
|
|
5751
6065
|
timeoutMs: MISSION_RUN_DEFAULTS.claudeTimeoutMs, prompt,
|
|
@@ -5766,10 +6080,11 @@ async function runMission(args) {
|
|
|
5766
6080
|
aborted: claudeResult.aborted,
|
|
5767
6081
|
};
|
|
5768
6082
|
if (claudeResult.rate_limit_info) {
|
|
5769
|
-
|
|
5770
|
-
|
|
5771
|
-
|
|
5772
|
-
|
|
6083
|
+
// Claude reports the five-hour window's resetsAt on every turn, even
|
|
6084
|
+
// when status is "allowed". Only a non-allowed status is a cooldown;
|
|
6085
|
+
// treating an allowed resetsAt as one pauses every timed run after
|
|
6086
|
+
// tick 1 with rate-limit-exceeded-wall.
|
|
6087
|
+
lastRateLimit = claudeResult.rate_limit_info.status === 'allowed' ? null : claudeResult.rate_limit_info;
|
|
5773
6088
|
}
|
|
5774
6089
|
if (claudeResult.aborted) { pauseReason = 'aborted-during-claude'; break; }
|
|
5775
6090
|
if (claudeResult.authExpired) { pauseReason = 'auth-required'; break; }
|
|
@@ -5778,6 +6093,20 @@ async function runMission(args) {
|
|
|
5778
6093
|
const deadModel = detectUnavailableModel(claudeResult.summary || claudeResult.receipt_text);
|
|
5779
6094
|
let reason = claudeResult.timedOut ? 'claude-timeout' : 'claude-error';
|
|
5780
6095
|
if (deadModel) { reason = 'model-unavailable'; result.model_unavailable = deadModel; }
|
|
6096
|
+
// A killed tick (usually claude-timeout) can leave the session lock
|
|
6097
|
+
// held, so the next resume fails with "already in use". Session
|
|
6098
|
+
// continuity is disposable — mission state lives on disk (receipts,
|
|
6099
|
+
// logs, now.md) — so rotate to a fresh id instead of grinding the
|
|
6100
|
+
// repeated-error breaker on a stale lock.
|
|
6101
|
+
if (/session id .* is already in use/i.test(claudeResult.stderr || '')) {
|
|
6102
|
+
reason = 'claude-session-busy';
|
|
6103
|
+
pendingSessionId = crypto.randomUUID();
|
|
6104
|
+
sessionId = null;
|
|
6105
|
+
mission = saveMission(
|
|
6106
|
+
{ ...mission, claude_session_id: null, pending_session_id: pendingSessionId },
|
|
6107
|
+
cwd, 'mission_session_rotated', { reason: 'session-lock-busy', session_id: pendingSessionId },
|
|
6108
|
+
).mission;
|
|
6109
|
+
}
|
|
5781
6110
|
result = { ...result, status: 'errored', reason };
|
|
5782
6111
|
} else {
|
|
5783
6112
|
// Promote pending session id ONLY if claude confirmed the exact UUID we requested.
|
|
@@ -5865,6 +6194,11 @@ async function runMission(args) {
|
|
|
5865
6194
|
} else if (result.status === 'ran' && mission.always_on) {
|
|
5866
6195
|
nextAction = nextCandidateTickAction(mission);
|
|
5867
6196
|
}
|
|
6197
|
+
// Context hygiene: count ran claude ticks on this session; at the
|
|
6198
|
+
// rotation threshold, drop the session so the next tick starts fresh.
|
|
6199
|
+
const claudeRanTick = Boolean(result.claude) && result.status === 'ran';
|
|
6200
|
+
const claudeSessionTicks = claudeRanTick ? Number(mission.claude_session_ticks || 0) + 1 : Number(mission.claude_session_ticks || 0);
|
|
6201
|
+
const rotateSessionForContext = claudeRanTick && claudeSessionTicks >= CLAUDE_SESSION_CONTEXT_ROTATE_TICKS;
|
|
5868
6202
|
mission = saveMission({
|
|
5869
6203
|
...mission,
|
|
5870
6204
|
status: newStatus,
|
|
@@ -5879,9 +6213,23 @@ async function runMission(args) {
|
|
|
5879
6213
|
verifier_result: verifierResult || mission.verifier_result || null,
|
|
5880
6214
|
receipt_path: receiptPath,
|
|
5881
6215
|
next_action: nextAction,
|
|
6216
|
+
claude_session_ticks: rotateSessionForContext ? 0 : claudeSessionTicks,
|
|
5882
6217
|
}, cwd, 'mission_tick', {
|
|
5883
6218
|
tick_index: tickIdx, status: result.status, reason: result.reason, receipt_path: receiptPath, layer: result.layer,
|
|
5884
6219
|
}).mission;
|
|
6220
|
+
if (rotateSessionForContext) {
|
|
6221
|
+
// Same shape as the stale-lock rotation above: mint the fresh pending
|
|
6222
|
+
// id AND reset the loop locals, or the next tick resumes the old
|
|
6223
|
+
// session from its stale local and the rotation is a no-op.
|
|
6224
|
+
pendingSessionId = crypto.randomUUID();
|
|
6225
|
+
sessionId = null;
|
|
6226
|
+
mission = saveMission(
|
|
6227
|
+
{ ...mission, claude_session_id: null, pending_session_id: pendingSessionId },
|
|
6228
|
+
cwd, 'mission_session_rotated', {
|
|
6229
|
+
reason: 'context-refresh', after_ticks: claudeSessionTicks, session_id: pendingSessionId,
|
|
6230
|
+
},
|
|
6231
|
+
).mission;
|
|
6232
|
+
}
|
|
5885
6233
|
appendMemberLog(mission.owner, `Mission run tick ${tickIdx}`, {
|
|
5886
6234
|
mission: mission.objective,
|
|
5887
6235
|
state: mission.status,
|
|
@@ -5904,7 +6252,10 @@ async function runMission(args) {
|
|
|
5904
6252
|
if (result.status === 'ran') {
|
|
5905
6253
|
ranTicks++;
|
|
5906
6254
|
backoffAttempt = 0;
|
|
5907
|
-
} else if (result.status === 'errored') {
|
|
6255
|
+
} else if (result.status === 'errored' && result.reason !== 'claude-session-busy') {
|
|
6256
|
+
// A rotated session is already healed — the next tick starts on a
|
|
6257
|
+
// fresh id, so backing off just burns wall clock. If rotation itself
|
|
6258
|
+
// keeps failing, the repeated-error breaker below still stops the run.
|
|
5908
6259
|
backoffAttempt++;
|
|
5909
6260
|
}
|
|
5910
6261
|
|
|
@@ -5924,7 +6275,7 @@ async function runMission(args) {
|
|
|
5924
6275
|
|
|
5925
6276
|
// Sleep until next tick
|
|
5926
6277
|
let sleepMs = 0;
|
|
5927
|
-
if (result.status === 'errored') {
|
|
6278
|
+
if (result.status === 'errored' && result.reason !== 'claude-session-busy') {
|
|
5928
6279
|
sleepMs = computeBackoff(MISSION_RUN_DEFAULTS.backoff, backoffAttempt);
|
|
5929
6280
|
} else if (cadenceSeconds > 0) {
|
|
5930
6281
|
sleepMs = cadenceSeconds * 1000;
|
|
@@ -6009,10 +6360,12 @@ function tickMission(args) {
|
|
|
6009
6360
|
const verifyOverride = readFlag(args, '--verify', '');
|
|
6010
6361
|
const completeOnPass = hasFlag(args, '--complete-on-pass');
|
|
6011
6362
|
const summary = readFlag(args, '--summary', '');
|
|
6363
|
+
const operatorSummaryWarning = warnIfSummaryNeedsOperatorWhy(summary);
|
|
6012
6364
|
const ref = stripKnownFlags(args, ['--summary', '--verify'], ['--json', '--complete-on-pass'])[0] || '';
|
|
6013
6365
|
let mission = resolveMission(ref);
|
|
6014
6366
|
if (!mission) {
|
|
6015
|
-
|
|
6367
|
+
if (ref) exitMissingMission(ref, 1, asJson);
|
|
6368
|
+
exitMissionError('No mission found. Run: atris mission start "..."', 1, asJson);
|
|
6016
6369
|
}
|
|
6017
6370
|
|
|
6018
6371
|
// Same per-mission flock that `mission run` uses. Without it, a tick could
|
|
@@ -6154,7 +6507,7 @@ function tickMission(args) {
|
|
|
6154
6507
|
const atrisGoalState = refreshAtrisGoalController(process.cwd(), { missionId: outputMission.id });
|
|
6155
6508
|
const codexGoalState = refreshCodexGoalController(process.cwd());
|
|
6156
6509
|
printJsonOrText(
|
|
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 },
|
|
6510
|
+
{ 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, operator_summary_warning: operatorSummaryWarning },
|
|
6158
6511
|
[
|
|
6159
6512
|
...missionTickResultLines(outputMission, tickIdx, receiptPath, verifierResult, summary),
|
|
6160
6513
|
...(continuationGoal?.mission ? [`Next goal: ${continuationGoal.mission.objective}`] : []),
|
|
@@ -6223,7 +6576,7 @@ function completeMission(args) {
|
|
|
6223
6576
|
}
|
|
6224
6577
|
const mission = resolveMission(ref);
|
|
6225
6578
|
if (!mission) {
|
|
6226
|
-
|
|
6579
|
+
exitMissingMission(ref, 1, asJson);
|
|
6227
6580
|
}
|
|
6228
6581
|
const gate = missionCompletionGate(mission, proof, root);
|
|
6229
6582
|
if (!gate.ok && !force) {
|
|
@@ -6299,7 +6652,7 @@ function stopMission(args) {
|
|
|
6299
6652
|
}
|
|
6300
6653
|
const mission = resolveMission(ref);
|
|
6301
6654
|
if (!mission) {
|
|
6302
|
-
|
|
6655
|
+
exitMissingMission(ref, 1, asJson);
|
|
6303
6656
|
}
|
|
6304
6657
|
const status = pause ? 'paused' : 'stopped';
|
|
6305
6658
|
// Full stops abandon work, so leave evidence: snapshot the worktree against
|
|
@@ -6415,7 +6768,7 @@ function ackMissionGoal(args) {
|
|
|
6415
6768
|
const status = String(readFlag(args, '--status', 'active') || 'active').trim().toLowerCase();
|
|
6416
6769
|
let mission = resolveMission(ref);
|
|
6417
6770
|
if (!mission) {
|
|
6418
|
-
|
|
6771
|
+
exitMissingMission(ref, 1, asJson);
|
|
6419
6772
|
}
|
|
6420
6773
|
if (runtime !== 'codex' || status !== 'active') {
|
|
6421
6774
|
exitMissionError('Native goal ack requires --runtime codex --status active.', 2, asJson);
|
|
@@ -6432,16 +6785,15 @@ function ackMissionGoal(args) {
|
|
|
6432
6785
|
releaseMissionLock(lock);
|
|
6433
6786
|
exitMissionError(`Mission "${ref}" uses runner=${mission.runner || 'manual'}; native Codex goal ack is only for runner=codex_goal.`, 2, asJson);
|
|
6434
6787
|
}
|
|
6435
|
-
const
|
|
6436
|
-
const
|
|
6437
|
-
|
|
6438
|
-
releaseMissionLock(lock);
|
|
6439
|
-
exitMissionError(`Native goal objective mismatch. Expected: ${expectedObjective}`, 2, asJson);
|
|
6440
|
-
}
|
|
6788
|
+
const canonicalObjective = codexGoalObjective(mission);
|
|
6789
|
+
const reportedObjective = readFlag(args, '--objective', canonicalObjective);
|
|
6790
|
+
const objective = canonicalObjective;
|
|
6441
6791
|
const ack = {
|
|
6442
6792
|
runtime: 'codex',
|
|
6443
6793
|
status: 'active',
|
|
6794
|
+
mission_id: mission.id,
|
|
6444
6795
|
objective,
|
|
6796
|
+
...(reportedObjective && reportedObjective !== objective ? { reported_objective: reportedObjective } : {}),
|
|
6445
6797
|
acknowledged_at: stampIso(),
|
|
6446
6798
|
};
|
|
6447
6799
|
const nextMission = {
|
|
@@ -6449,6 +6801,7 @@ function ackMissionGoal(args) {
|
|
|
6449
6801
|
native_goal_ack: ack,
|
|
6450
6802
|
next_action: codexGoalNextCommand({ ...mission, native_goal_ack: ack }),
|
|
6451
6803
|
};
|
|
6804
|
+
const supersededNativeGoalAcks = supersedeOtherCodexNativeGoalAcks(process.cwd(), nextMission, ack);
|
|
6452
6805
|
const { mission: saved } = saveMission(nextMission, process.cwd(), 'mission_native_goal_ack', { ack });
|
|
6453
6806
|
const payload = refreshCodexGoalController(process.cwd());
|
|
6454
6807
|
printJsonOrText(
|
|
@@ -6457,6 +6810,7 @@ function ackMissionGoal(args) {
|
|
|
6457
6810
|
action: 'native_goal_acknowledged',
|
|
6458
6811
|
mission: saved,
|
|
6459
6812
|
native_goal_ack: ack,
|
|
6813
|
+
superseded_native_goal_acks: supersededNativeGoalAcks,
|
|
6460
6814
|
codex_goal_state: payload,
|
|
6461
6815
|
next_command: payload.goal?.next_command || `atris mission tick ${saved.id} --summary "<what changed>"`,
|
|
6462
6816
|
},
|
|
@@ -6555,13 +6909,15 @@ function help() {
|
|
|
6555
6909
|
console.log(`
|
|
6556
6910
|
atris mission - durable goal + loop + owner + proof state
|
|
6557
6911
|
|
|
6558
|
-
atris mission start "<objective>" --owner <member> [--verify "..."] [--always-on] [--xp-task] [--worktree]
|
|
6912
|
+
atris mission start "<objective>" --owner <member> [--verify "..."] [--always-on] [--xp-task] [--worktree] [--take-goal-slot]
|
|
6559
6913
|
[--runner manual|claude|atris2|codex_goal] [--model <id>]
|
|
6560
6914
|
(runner claude spawns local claude -p per tick, --model passes through;
|
|
6561
6915
|
runner atris2 runs each tick as one /atris2/turn on the AtrisOS backend,
|
|
6562
6916
|
default model atris:fast; runner codex_goal publishes the goal for a live
|
|
6563
6917
|
Codex session to pull via atris mission goal)
|
|
6564
6918
|
atris mission status [id] [--status <state>] [--limit <n>] [--local] [--json]
|
|
6919
|
+
atris mission inspect <id> --fields status,runner,ack,pings [--json]
|
|
6920
|
+
Field-selectable mission state (status, runner, native goal ack, ping counts)
|
|
6565
6921
|
atris mission doctor [--local] [--json] Flag no-verifier missions, help missions, stale ready receipts, and blocked always-on loops
|
|
6566
6922
|
atris mission attach-task <id> [--json] Create the missing task spine for an existing active mission
|
|
6567
6923
|
atris mission report [id] [--limit <n>] [--local] [--json] Plain outcome, worker receipt, verifier receipt, and next move
|
|
@@ -6576,8 +6932,9 @@ atris mission - durable goal + loop + owner + proof state
|
|
|
6576
6932
|
atris mission goal-loop [--max-wall 28800] [--max-iterations 32] [--no-claude] [--json]
|
|
6577
6933
|
atris mission tick <id> [--verify ["cmd"]] [--complete-on-pass] [--summary "..."] [--json]
|
|
6578
6934
|
atris mission "<objective>" [--owner <member>] Shortcut for: atris mission run "<objective>"
|
|
6935
|
+
atris mission run --fleet [--slots 3] [--dry-run] [--json] Staff every idle capable engine on claimable safe-lane tasks: parallel worktree builds, serial rebase-before-ship landings, receipt in atris/runs/
|
|
6579
6936
|
atris mission run ["objective"|<member> ["objective"]|id|--due] [--owner <member>] [--max-ticks 4] [--max-wall 3600] [--cadence "15m"]
|
|
6580
|
-
[--native-goal-status active|paused] [--native-goal-objective "..."] [--allow-native-goal-supersede]
|
|
6937
|
+
[--native-goal-status active|paused] [--native-goal-objective "..."] [--allow-native-goal-supersede] [--take-goal-slot]
|
|
6581
6938
|
[--spend-full-budget|--use-whole-budget|--stop-when-done] [--room-preflight|--no-room-preflight]
|
|
6582
6939
|
[--room-auto-run|--no-room-auto-run]
|
|
6583
6940
|
[--no-claude] [--no-verify] [--complete-on-pass] [--no-drain] [--create-next] [--json]
|
|
@@ -6838,6 +7195,98 @@ function pruneRunsMission(args) {
|
|
|
6838
7195
|
if (result.errors.length) process.exitCode = 1;
|
|
6839
7196
|
}
|
|
6840
7197
|
|
|
7198
|
+
// Locate a mission whose state may live in a sibling worktree (--worktree
|
|
7199
|
+
// missions keep their jsonl there). Returns the mission plus the root whose
|
|
7200
|
+
// state file owns it, so writes land where the runner reads.
|
|
7201
|
+
function findMissionAcrossWorktrees(ref, root = process.cwd()) {
|
|
7202
|
+
const local = resolveMission(ref, root);
|
|
7203
|
+
if (local) return { mission: local, root };
|
|
7204
|
+
for (const rolled of listWorktreeRollupMissions(root)) {
|
|
7205
|
+
if (rolled.id === ref || rolled.id.startsWith(ref) || rolled.slug === ref) {
|
|
7206
|
+
const { worktree_root, worktree_branch, ...mission } = rolled;
|
|
7207
|
+
return { mission, root: worktree_root };
|
|
7208
|
+
}
|
|
7209
|
+
}
|
|
7210
|
+
return null;
|
|
7211
|
+
}
|
|
7212
|
+
|
|
7213
|
+
// atris mission ping <id> "<message>" — leave a note the mission's next tick
|
|
7214
|
+
// reads (and consumes) as operator direction. This is how you talk to an
|
|
7215
|
+
// always-on member mid-run without stopping it.
|
|
7216
|
+
function inspectMission(args) {
|
|
7217
|
+
const asJson = wantsJson(args);
|
|
7218
|
+
const parsed = readFieldsFlag(args, '--fields');
|
|
7219
|
+
if (!parsed || parsed.error) {
|
|
7220
|
+
exitMissionError(
|
|
7221
|
+
parsed?.error || 'Usage: atris mission inspect <id> --fields status,runner,ack,pings [--json]',
|
|
7222
|
+
2,
|
|
7223
|
+
asJson,
|
|
7224
|
+
);
|
|
7225
|
+
}
|
|
7226
|
+
const fieldError = validateFields(parsed.fields, MISSION_INSPECT_FIELDS, 'mission');
|
|
7227
|
+
if (fieldError) exitMissionError(fieldError, 2, asJson);
|
|
7228
|
+
const ref = stripInspectArgs(args)[0] || '';
|
|
7229
|
+
if (!ref) {
|
|
7230
|
+
exitMissionError('Usage: atris mission inspect <id> --fields status,runner,ack,pings [--json]', 2, asJson);
|
|
7231
|
+
}
|
|
7232
|
+
const found = findMissionAcrossWorktrees(ref);
|
|
7233
|
+
if (!found) exitMissingMission(ref, 1, asJson);
|
|
7234
|
+
const { mission } = found;
|
|
7235
|
+
const values = missionInspectFieldValues(mission, parsed.fields);
|
|
7236
|
+
const payload = buildInspectPayload({
|
|
7237
|
+
action: 'mission_inspect',
|
|
7238
|
+
idKey: 'mission_id',
|
|
7239
|
+
idValue: mission.id,
|
|
7240
|
+
fields: parsed.fields,
|
|
7241
|
+
values,
|
|
7242
|
+
});
|
|
7243
|
+
printJsonOrText(payload, inspectTextLines(parsed.fields, values), asJson);
|
|
7244
|
+
}
|
|
7245
|
+
|
|
7246
|
+
// always-on member mid-run without stopping it. opts.silent skips console
|
|
7247
|
+
// output so callers (member ping) can compose their own multi-lane report.
|
|
7248
|
+
function pingMission(args, opts = {}) {
|
|
7249
|
+
const asJson = args.includes('--json');
|
|
7250
|
+
const rest = args.filter((a) => a !== '--json');
|
|
7251
|
+
let from = process.env.USER || 'operator';
|
|
7252
|
+
const fromIdx = rest.indexOf('--from');
|
|
7253
|
+
if (fromIdx !== -1) {
|
|
7254
|
+
from = rest[fromIdx + 1] || from;
|
|
7255
|
+
rest.splice(fromIdx, 2);
|
|
7256
|
+
}
|
|
7257
|
+
const [ref, ...textParts] = rest;
|
|
7258
|
+
const text = textParts.join(' ').trim();
|
|
7259
|
+
if (!ref || !text) {
|
|
7260
|
+
console.error('usage: atris mission ping <id> "<message>" [--from <name>]');
|
|
7261
|
+
process.exit(2);
|
|
7262
|
+
}
|
|
7263
|
+
const found = findMissionAcrossWorktrees(ref);
|
|
7264
|
+
if (!found) {
|
|
7265
|
+
exitMissingMission(ref, 1, asJson);
|
|
7266
|
+
}
|
|
7267
|
+
const { mission, root } = found;
|
|
7268
|
+
if (TERMINAL_STATUSES.has(mission.status)) {
|
|
7269
|
+
console.error(`Mission ${mission.id} is ${mission.status}; pings only reach live missions.`);
|
|
7270
|
+
process.exit(1);
|
|
7271
|
+
}
|
|
7272
|
+
const ping = { at: stampIso(), from, text };
|
|
7273
|
+
const saved = saveMission(
|
|
7274
|
+
{ ...mission, pings: [...(Array.isArray(mission.pings) ? mission.pings : []), ping] },
|
|
7275
|
+
root,
|
|
7276
|
+
'mission_ping',
|
|
7277
|
+
{ from, text: text.slice(0, 200) },
|
|
7278
|
+
).mission;
|
|
7279
|
+
const pending = (saved.pings || []).filter((p) => p && !p.consumed_at).length;
|
|
7280
|
+
if (!opts.silent) {
|
|
7281
|
+
if (asJson) {
|
|
7282
|
+
console.log(JSON.stringify({ ok: true, action: 'mission_ping', mission_id: saved.id, pending_pings: pending, ping }));
|
|
7283
|
+
} else {
|
|
7284
|
+
console.log(`pinged ${saved.id} — the next tick reads it (${pending} unread).`);
|
|
7285
|
+
}
|
|
7286
|
+
}
|
|
7287
|
+
return saved;
|
|
7288
|
+
}
|
|
7289
|
+
|
|
6841
7290
|
function missionCommand(args) {
|
|
6842
7291
|
const subcommand = args[0] || 'status';
|
|
6843
7292
|
const rest = args.slice(1);
|
|
@@ -6882,6 +7331,10 @@ function missionCommand(args) {
|
|
|
6882
7331
|
case 'goal-loop':
|
|
6883
7332
|
case 'codex-goal-loop':
|
|
6884
7333
|
return goalLoopMission(rest);
|
|
7334
|
+
case 'ping':
|
|
7335
|
+
return pingMission(rest);
|
|
7336
|
+
case 'inspect':
|
|
7337
|
+
return inspectMission(rest);
|
|
6885
7338
|
case 'tick':
|
|
6886
7339
|
return tickMission(rest);
|
|
6887
7340
|
case 'run':
|
|
@@ -6904,11 +7357,15 @@ function missionCommand(args) {
|
|
|
6904
7357
|
|
|
6905
7358
|
module.exports = {
|
|
6906
7359
|
missionCommand,
|
|
7360
|
+
inspectMission,
|
|
6907
7361
|
missionHeartbeatLines,
|
|
6908
7362
|
listMissions,
|
|
7363
|
+
listWorktreeRollupMissions,
|
|
7364
|
+
pingMission,
|
|
6909
7365
|
loadMissionMap,
|
|
6910
7366
|
renderMissionStatus,
|
|
6911
7367
|
selectDueMission,
|
|
7368
|
+
selectAtrisGoalMission,
|
|
6912
7369
|
selectCodexGoalMission,
|
|
6913
7370
|
usefulClaudeReceiptSummary,
|
|
6914
7371
|
cappedClaudeReceiptText,
|