atris 3.30.0 → 3.30.2
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 +6 -0
- package/atris/AGENTS.md +11 -0
- package/atris/CLAUDE.md +5 -0
- package/atris/atris.md +19 -4
- package/atris/policies/atris-design.md +71 -0
- package/atris/policies/design-seed.md +187 -0
- package/atris/skills/atris/SKILL.md +26 -3
- package/atris/skills/design/SKILL.md +3 -2
- package/atris/skills/loop/SKILL.md +5 -3
- package/atris/team/_template/MEMBER.md +19 -0
- package/atris.md +63 -23
- package/ax +1434 -1698
- package/bin/atris.js +5 -1
- package/commands/agent-spawn.js +2 -2
- package/commands/brain.js +92 -7
- package/commands/brainstorm.js +62 -22
- package/commands/business-sync.js +92 -8
- package/commands/business.js +13 -7
- package/commands/chat-scan.js +102 -0
- package/commands/computer.js +758 -15
- package/commands/deck.js +505 -105
- package/commands/init.js +6 -1
- package/commands/launchpad.js +638 -0
- package/commands/log-sync.js +44 -13
- package/commands/loop-front.js +290 -0
- package/commands/member.js +124 -3
- package/commands/mission.js +653 -30
- package/commands/pull.js +37 -28
- package/commands/pulse.js +11 -8
- package/commands/run.js +79 -39
- package/commands/sync.js +36 -17
- package/commands/task.js +1072 -89
- package/commands/workflow.js +6 -6
- package/commands/xp.js +13 -1
- package/commands/youtube.js +221 -7
- package/decks/README.md +89 -0
- package/decks/archetype-catalog.json +180 -0
- package/decks/atris-antislop-pitch.json +48 -0
- package/decks/atris-archetypes-v2.json +83 -0
- package/decks/atris-one-loop-pitch.json +80 -0
- package/decks/atris-seed-pitch-v3.json +118 -0
- package/decks/atris-seed-pitch-v4-skeleton.json +106 -0
- package/decks/atris-seed-pitch-v5.json +109 -0
- package/decks/atris-seed-pitch-v6.json +137 -0
- package/decks/atris-seed-pitch-v7.json +133 -0
- package/decks/atris-single-shot-proof.json +74 -0
- package/decks/mark-pincus-narrative.json +102 -0
- package/decks/mark-pincus-sourcery.json +94 -0
- package/decks/yash-applied-compute-detailed.json +150 -0
- package/decks/yash-applied-compute-generalist.json +82 -0
- package/decks/yash-applied-compute-narrative.json +54 -0
- package/lib/ax-prefs.js +5 -12
- package/lib/chat-log-scan.js +377 -0
- package/lib/context-gatherer.js +35 -1
- package/lib/deck-compose.js +145 -0
- package/lib/deck-history.js +64 -0
- package/lib/deck-layout.js +169 -0
- package/lib/deck-review.js +431 -0
- package/lib/deck-schema.js +154 -0
- package/lib/file-ops.js +3 -3
- package/lib/functional-owner.js +189 -0
- package/lib/journal.js +7 -73
- package/lib/slides-deck.js +512 -58
- package/lib/task-db.js +128 -11
- package/package.json +2 -1
- package/templates/business-starter/team/START_HERE.md +12 -8
- package/utils/auth.js +4 -0
- package/utils/config.js +4 -0
- package/atris/atrisDev.md +0 -717
package/commands/mission.js
CHANGED
|
@@ -8,6 +8,10 @@ const {
|
|
|
8
8
|
resolveClaudeRunnerModel,
|
|
9
9
|
resolveClaudeRunnerBin,
|
|
10
10
|
} = require('../lib/runner-command');
|
|
11
|
+
const {
|
|
12
|
+
normalizeOwnerSlug,
|
|
13
|
+
resolveFunctionalOwner,
|
|
14
|
+
} = require('../lib/functional-owner');
|
|
11
15
|
|
|
12
16
|
const VALID_STATUSES = new Set(['planning', 'running', 'ready', 'paused', 'blocked', 'stopped', 'complete']);
|
|
13
17
|
const TERMINAL_STATUSES = new Set(['stopped', 'complete']);
|
|
@@ -166,27 +170,33 @@ function createMissionXpTask(mission, root = process.cwd(), asJson = false) {
|
|
|
166
170
|
const db = taskDb.open();
|
|
167
171
|
const workspaceRoot = taskDb.workspaceRoot(root);
|
|
168
172
|
const title = `Mission XP: ${mission.objective}`;
|
|
173
|
+
const ownerResolution = resolveMissionOwner(mission, workspaceRoot);
|
|
174
|
+
const owner = ownerResolution.owner;
|
|
169
175
|
const metadata = {
|
|
170
|
-
assigned_to:
|
|
176
|
+
assigned_to: owner,
|
|
171
177
|
delegate_via: 'mission_goal_loop',
|
|
172
178
|
created_for_day: todayName(),
|
|
173
179
|
goal_id: mission.id,
|
|
174
180
|
goal_objective: mission.objective,
|
|
175
181
|
mission_id: mission.id,
|
|
176
182
|
mission_objective: mission.objective,
|
|
177
|
-
mission_owner:
|
|
183
|
+
mission_owner: owner,
|
|
184
|
+
owner_resolution: ownerResolution.reason,
|
|
178
185
|
mission_lane: mission.lane,
|
|
179
186
|
mission_runner: mission.runner,
|
|
180
187
|
verify: mission.verifier || null,
|
|
181
188
|
stop_condition: mission.stop_condition || null,
|
|
182
189
|
};
|
|
190
|
+
if (ownerResolution.requested_owner && ownerResolution.requested_owner !== owner) metadata.requested_owner = ownerResolution.requested_owner;
|
|
191
|
+
if (ownerResolution.executed_by) metadata.executed_by = normalizeOwnerSlug(ownerResolution.executed_by);
|
|
192
|
+
if (ownerResolution.proposed_member) metadata.proposed_member = ownerResolution.proposed_member;
|
|
183
193
|
const result = taskDb.addTask(db, {
|
|
184
194
|
title,
|
|
185
195
|
tag: 'agent-xp',
|
|
186
196
|
workspaceRoot,
|
|
187
197
|
sourceKey: `mission-xp:${mission.id}`,
|
|
188
198
|
status: 'claimed',
|
|
189
|
-
claimedBy:
|
|
199
|
+
claimedBy: owner,
|
|
190
200
|
metadata,
|
|
191
201
|
});
|
|
192
202
|
const rows = taskDb.withTaskDisplayRefs(taskDb.listTasks(db, { workspaceRoot }));
|
|
@@ -194,7 +204,7 @@ function createMissionXpTask(mission, root = process.cwd(), asJson = false) {
|
|
|
194
204
|
if (task) {
|
|
195
205
|
taskDb.noteTask(db, {
|
|
196
206
|
id: task.id,
|
|
197
|
-
actor: process.env.ATRIS_AGENT_ID ||
|
|
207
|
+
actor: process.env.ATRIS_AGENT_ID || owner,
|
|
198
208
|
content: `Mission goal loop XP bridge for ${mission.id}. Proof goes through task current-step; AgentXP lands only after human accept.`,
|
|
199
209
|
});
|
|
200
210
|
}
|
|
@@ -204,7 +214,9 @@ function createMissionXpTask(mission, root = process.cwd(), asJson = false) {
|
|
|
204
214
|
ref: missionTaskRef(task) || result.id,
|
|
205
215
|
title,
|
|
206
216
|
status: task?.status || 'claimed',
|
|
207
|
-
assigned_to:
|
|
217
|
+
assigned_to: owner,
|
|
218
|
+
owner_resolution: ownerResolution.reason,
|
|
219
|
+
executed_by: ownerResolution.executed_by ? normalizeOwnerSlug(ownerResolution.executed_by) : null,
|
|
208
220
|
inserted: result.inserted !== false,
|
|
209
221
|
projection_path: outPath,
|
|
210
222
|
};
|
|
@@ -223,6 +235,24 @@ function statePaths(root = process.cwd()) {
|
|
|
223
235
|
};
|
|
224
236
|
}
|
|
225
237
|
|
|
238
|
+
function readBusinessBinding(root = process.cwd()) {
|
|
239
|
+
const file = path.join(root, '.atris', 'business.json');
|
|
240
|
+
try {
|
|
241
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
242
|
+
return {
|
|
243
|
+
business_id: parsed.business_id || '',
|
|
244
|
+
workspace_id: parsed.workspace_id || '',
|
|
245
|
+
slug: parsed.slug || '',
|
|
246
|
+
};
|
|
247
|
+
} catch {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function businessIdForAtris2Mission(mission, cwd = process.cwd()) {
|
|
253
|
+
return mission?.business_id || readBusinessBinding(cwd)?.business_id || null;
|
|
254
|
+
}
|
|
255
|
+
|
|
226
256
|
function readJsonLines(file) {
|
|
227
257
|
if (!fs.existsSync(file)) return [];
|
|
228
258
|
return fs.readFileSync(file, 'utf8')
|
|
@@ -424,6 +454,7 @@ function renderMemberNowMarkdown(owner, missions) {
|
|
|
424
454
|
return lines.join('\n');
|
|
425
455
|
}
|
|
426
456
|
for (const mission of missions) {
|
|
457
|
+
const taskSpine = missionTaskSpine(mission);
|
|
427
458
|
lines.push(`## ${mission.objective}`);
|
|
428
459
|
lines.push('');
|
|
429
460
|
lines.push(`- id: ${mission.id}`);
|
|
@@ -431,6 +462,9 @@ function renderMemberNowMarkdown(owner, missions) {
|
|
|
431
462
|
lines.push(`- cadence: ${mission.cadence}`);
|
|
432
463
|
lines.push(`- runner: ${mission.runner}${mission.model ? ` (${mission.model})` : ''}`);
|
|
433
464
|
lines.push(`- lane: ${mission.lane}`);
|
|
465
|
+
if (taskSpine.task_ref) lines.push(`- task: ${taskSpine.task_ref}`);
|
|
466
|
+
if (taskSpine.current_step_command) lines.push(`- task next: ${taskSpine.current_step_command}`);
|
|
467
|
+
if (!taskSpine.has_task && taskSpine.ensure_task_command) lines.push(`- task setup: ${taskSpine.ensure_task_command}`);
|
|
434
468
|
if (mission.xp_task?.ref) lines.push(`- AgentXP task: ${mission.xp_task.ref}`);
|
|
435
469
|
if (mission.verifier) lines.push(`- verifier: ${mission.verifier}`);
|
|
436
470
|
if (mission.stop_condition) lines.push(`- stop: ${mission.stop_condition}`);
|
|
@@ -463,6 +497,52 @@ function completionGateLabel(gate) {
|
|
|
463
497
|
return gate.forced ? `forced override (${gate.source})` : gate.source;
|
|
464
498
|
}
|
|
465
499
|
|
|
500
|
+
function missionCompletionReceipt(mission, proof, xpNextCommand = null) {
|
|
501
|
+
const gate = mission.completion_gate || {};
|
|
502
|
+
const gateLabel = completionGateLabel(gate) || 'completion gate';
|
|
503
|
+
const checked = gate.source === 'receipt' && gate.receipt_path
|
|
504
|
+
? `I checked the passing verifier receipt ${gate.receipt_path}.`
|
|
505
|
+
: gate.source === 'mission_state'
|
|
506
|
+
? 'I checked mission state showing the verifier passed.'
|
|
507
|
+
: gate.source === 'no_verifier'
|
|
508
|
+
? 'No verifier was configured, so completion used the no-verifier gate.'
|
|
509
|
+
: `I checked the ${gateLabel} completion gate.`;
|
|
510
|
+
const tested = mission.verifier_result?.passed
|
|
511
|
+
? `Verifier passed: ${mission.verifier_result.command || mission.verifier || 'configured verifier'}.`
|
|
512
|
+
: mission.verifier
|
|
513
|
+
? `Completion proof is attached for verifier: ${mission.verifier}.`
|
|
514
|
+
: 'No verifier command was recorded for this mission.';
|
|
515
|
+
const landing = {
|
|
516
|
+
happened: `Mission completed: ${mission.objective}.`,
|
|
517
|
+
checked,
|
|
518
|
+
tested,
|
|
519
|
+
saved: `Saved complete mission ${mission.id}${proof ? ` with proof ${proof}` : ''}.`,
|
|
520
|
+
decision: xpNextCommand
|
|
521
|
+
? `Queue AgentXP next: ${xpNextCommand}`
|
|
522
|
+
: 'Mission is complete; start or resume the next mission if more work remains.',
|
|
523
|
+
};
|
|
524
|
+
const result = {
|
|
525
|
+
changed: landing.happened,
|
|
526
|
+
checked: landing.checked,
|
|
527
|
+
tested: landing.tested,
|
|
528
|
+
saved: landing.saved,
|
|
529
|
+
accept: landing.decision,
|
|
530
|
+
};
|
|
531
|
+
return { landing, result };
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function missionResultLines(completion) {
|
|
535
|
+
const landing = completion?.landing || {};
|
|
536
|
+
const result = completion?.result || {};
|
|
537
|
+
const lines = ['Result:'];
|
|
538
|
+
if (landing.happened) lines.push(` What happened: ${landing.happened}`);
|
|
539
|
+
if (landing.checked) lines.push(` How I checked: ${landing.checked}`);
|
|
540
|
+
if (landing.tested) lines.push(` What I tested: ${landing.tested}`);
|
|
541
|
+
if (result.saved) lines.push(` Saved: ${result.saved}`);
|
|
542
|
+
if (landing.decision) lines.push(` Decision: ${landing.decision}`);
|
|
543
|
+
return lines;
|
|
544
|
+
}
|
|
545
|
+
|
|
466
546
|
function renderMissionStatus(root = process.cwd()) {
|
|
467
547
|
const paths = statePaths(root);
|
|
468
548
|
const missions = listMissions(root);
|
|
@@ -478,10 +558,14 @@ function renderMissionStatus(root = process.cwd()) {
|
|
|
478
558
|
lines.push('No missions yet.', '');
|
|
479
559
|
} else {
|
|
480
560
|
for (const mission of missions.slice(0, 12)) {
|
|
561
|
+
const taskSpine = missionTaskSpine(mission);
|
|
481
562
|
lines.push(`- **${mission.id}** ${mission.objective}`);
|
|
482
563
|
lines.push(` - owner: ${mission.owner}`);
|
|
483
564
|
lines.push(` - state: ${mission.status}`);
|
|
484
565
|
lines.push(` - next: ${mission.next_action || 'tick or verify'}`);
|
|
566
|
+
if (taskSpine?.task_ref) lines.push(` - task: ${taskSpine.task_ref}`);
|
|
567
|
+
if (taskSpine?.current_step_command) lines.push(` - task next: ${taskSpine.current_step_command}`);
|
|
568
|
+
if (taskSpine && !taskSpine.has_task && taskSpine.ensure_task_command) lines.push(` - task setup: ${taskSpine.ensure_task_command}`);
|
|
485
569
|
if (mission.xp_task?.ref) lines.push(` - AgentXP task: ${mission.xp_task.ref}`);
|
|
486
570
|
if (mission.receipt_path) lines.push(` - proof: ${mission.receipt_path}`);
|
|
487
571
|
const gateLabel = completionGateLabel(mission.completion_gate);
|
|
@@ -501,13 +585,110 @@ function missionXpTaskRefFromMission(mission) {
|
|
|
501
585
|
return '';
|
|
502
586
|
}
|
|
503
587
|
|
|
588
|
+
function resolveMissionOwner(mission, root = process.cwd()) {
|
|
589
|
+
const requestedOwner = mission?.owner || process.env.ATRIS_AGENT_ID || 'mission-lead';
|
|
590
|
+
const resolved = resolveFunctionalOwner({
|
|
591
|
+
requestedOwner,
|
|
592
|
+
title: mission?.objective || '',
|
|
593
|
+
tag: mission?.lane || 'mission',
|
|
594
|
+
note: mission?.next_action || '',
|
|
595
|
+
goal: mission?.objective || '',
|
|
596
|
+
root,
|
|
597
|
+
fallbackOwners: ['mission-lead', 'task-planner', 'architect', 'validator'],
|
|
598
|
+
});
|
|
599
|
+
const requested = mission?.requested_owner || (
|
|
600
|
+
resolved.requested_owner && resolved.requested_owner !== resolved.owner ? resolved.requested_owner : null
|
|
601
|
+
);
|
|
602
|
+
return {
|
|
603
|
+
...resolved,
|
|
604
|
+
reason: mission?.owner_resolution || resolved.reason,
|
|
605
|
+
requested_owner: requested,
|
|
606
|
+
executed_by: mission?.executed_by || resolved.executed_by || null,
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function applyMissionOwnerResolution(mission, root = process.cwd()) {
|
|
611
|
+
const ownerResolution = resolveMissionOwner(mission, root);
|
|
612
|
+
const next = {
|
|
613
|
+
...mission,
|
|
614
|
+
owner: ownerResolution.owner,
|
|
615
|
+
owner_resolution: ownerResolution.reason,
|
|
616
|
+
};
|
|
617
|
+
if (ownerResolution.requested_owner && ownerResolution.requested_owner !== ownerResolution.owner) {
|
|
618
|
+
next.requested_owner = ownerResolution.requested_owner;
|
|
619
|
+
}
|
|
620
|
+
if (ownerResolution.executed_by) {
|
|
621
|
+
next.executed_by = normalizeOwnerSlug(ownerResolution.executed_by);
|
|
622
|
+
}
|
|
623
|
+
if (ownerResolution.proposed_member) {
|
|
624
|
+
next.proposed_member = ownerResolution.proposed_member;
|
|
625
|
+
}
|
|
626
|
+
return { mission: next, ownerResolution };
|
|
627
|
+
}
|
|
628
|
+
|
|
504
629
|
function missionXpReadyAction(mission, receiptPath) {
|
|
505
630
|
const ref = missionXpTaskRefFromMission(mission);
|
|
506
631
|
if (!ref || !receiptPath) return null;
|
|
507
|
-
const owner = mission.owner
|
|
632
|
+
const owner = resolveMissionOwner(mission).owner;
|
|
508
633
|
return `queue AgentXP review: atris task current-step --goal-id ${mission.id} --as ${owner} --proof "${receiptPath}" --json`;
|
|
509
634
|
}
|
|
510
635
|
|
|
636
|
+
function missionTaskSpine(mission) {
|
|
637
|
+
if (!mission || !mission.id) return null;
|
|
638
|
+
const ownerResolution = resolveMissionOwner(mission);
|
|
639
|
+
const taskIds = Array.isArray(mission.task_ids) ? mission.task_ids.filter(Boolean) : [];
|
|
640
|
+
const taskId = mission.xp_task?.task_id
|
|
641
|
+
|| mission.current_task_id
|
|
642
|
+
|| mission.task_id
|
|
643
|
+
|| taskIds[0]
|
|
644
|
+
|| null;
|
|
645
|
+
const taskRef = mission.xp_task?.ref
|
|
646
|
+
|| mission.task_ref
|
|
647
|
+
|| (taskId ? String(taskId) : null);
|
|
648
|
+
const owner = ownerResolution.owner;
|
|
649
|
+
return {
|
|
650
|
+
schema: 'atris.mission_task_spine.v1',
|
|
651
|
+
goal_id: mission.id,
|
|
652
|
+
owner,
|
|
653
|
+
requested_owner: ownerResolution.requested_owner || null,
|
|
654
|
+
owner_resolution: ownerResolution.reason,
|
|
655
|
+
executed_by: ownerResolution.executed_by ? normalizeOwnerSlug(ownerResolution.executed_by) : null,
|
|
656
|
+
lane: mission.lane || 'workspace',
|
|
657
|
+
runner: mission.runner || 'manual',
|
|
658
|
+
task_id: taskId,
|
|
659
|
+
current_task_id: taskId,
|
|
660
|
+
task_ref: taskRef,
|
|
661
|
+
has_task: Boolean(taskId || taskRef),
|
|
662
|
+
current_step_command: taskId || taskRef
|
|
663
|
+
? `atris task current-step --goal-id ${mission.id} --as ${owner} --proof "<proof>" --json`
|
|
664
|
+
: null,
|
|
665
|
+
ensure_task_command: taskId || taskRef
|
|
666
|
+
? null
|
|
667
|
+
: `atris mission attach-task ${mission.id} --json`,
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function missionStatusView(mission) {
|
|
672
|
+
const taskSpine = missionTaskSpine(mission);
|
|
673
|
+
if (!taskSpine) return mission;
|
|
674
|
+
const requestedOwner = taskSpine.requested_owner
|
|
675
|
+
|| mission.requested_owner
|
|
676
|
+
|| (mission.owner && mission.owner !== taskSpine.owner ? mission.owner : null);
|
|
677
|
+
return {
|
|
678
|
+
...mission,
|
|
679
|
+
owner: taskSpine.owner,
|
|
680
|
+
functional_owner: taskSpine.owner,
|
|
681
|
+
requested_owner: requestedOwner,
|
|
682
|
+
owner_resolution: taskSpine.owner_resolution,
|
|
683
|
+
executed_by: taskSpine.executed_by || mission.executed_by || null,
|
|
684
|
+
goal_id: taskSpine.goal_id,
|
|
685
|
+
task_id: taskSpine.task_id,
|
|
686
|
+
current_task_id: taskSpine.current_task_id,
|
|
687
|
+
task_ref: taskSpine.task_ref,
|
|
688
|
+
task_spine: taskSpine,
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
|
|
511
692
|
function missionFromArgs(args) {
|
|
512
693
|
const objective = stripKnownFlags(args, [
|
|
513
694
|
'--owner',
|
|
@@ -524,11 +705,20 @@ function missionFromArgs(args) {
|
|
|
524
705
|
if (!objective) {
|
|
525
706
|
exitMissionError('Usage: atris mission start "<objective>" --owner <member> [--verify "..."] [--cadence manual] [--worktree]', 1, wantsJson(args));
|
|
526
707
|
}
|
|
527
|
-
const
|
|
708
|
+
const requestedOwner = readFlag(args, '--owner', process.env.ATRIS_AGENT_ID || 'mission-lead');
|
|
528
709
|
const cadence = readFlag(args, '--cadence', readFlag(args, '--loop', 'manual')) || 'manual';
|
|
529
710
|
const runner = readFlag(args, '--runner', 'manual');
|
|
530
711
|
const model = readFlag(args, '--model', '') || (String(runner).toLowerCase() === 'atris2' ? 'atris:fast' : '');
|
|
531
712
|
const lane = readFlag(args, '--lane', 'workspace');
|
|
713
|
+
const ownerResolution = resolveFunctionalOwner({
|
|
714
|
+
requestedOwner,
|
|
715
|
+
title: objective,
|
|
716
|
+
tag: lane,
|
|
717
|
+
goal: objective,
|
|
718
|
+
root: process.cwd(),
|
|
719
|
+
fallbackOwners: ['mission-lead', 'task-planner', 'architect', 'validator'],
|
|
720
|
+
});
|
|
721
|
+
const owner = ownerResolution.owner;
|
|
532
722
|
const verifier = readFlag(args, '--verify', '');
|
|
533
723
|
assertMissionVerifier(verifier, wantsJson(args));
|
|
534
724
|
const stopCondition = readFlag(args, '--stop', verifier ? 'verifier passes and no human asks remain' : 'human marks complete with proof');
|
|
@@ -536,6 +726,7 @@ function missionFromArgs(args) {
|
|
|
536
726
|
const humanAsks = readRepeatedFlag(args, '--ask');
|
|
537
727
|
const alwaysOn = hasFlag(args, '--always-on');
|
|
538
728
|
const xpTaskEnabled = hasFlag(args, '--xp-task') || hasFlag(args, '--agent-xp');
|
|
729
|
+
const businessBinding = readBusinessBinding(process.cwd());
|
|
539
730
|
const id = missionId(objective);
|
|
540
731
|
const mission = {
|
|
541
732
|
schema: 'atris.mission.v1',
|
|
@@ -543,10 +734,16 @@ function missionFromArgs(args) {
|
|
|
543
734
|
slug: slugify(objective),
|
|
544
735
|
objective,
|
|
545
736
|
owner,
|
|
737
|
+
owner_resolution: ownerResolution.reason,
|
|
738
|
+
...(ownerResolution.requested_owner && ownerResolution.requested_owner !== owner ? { requested_owner: ownerResolution.requested_owner } : {}),
|
|
739
|
+
...(ownerResolution.executed_by ? { executed_by: normalizeOwnerSlug(ownerResolution.executed_by) } : {}),
|
|
740
|
+
...(ownerResolution.proposed_member ? { proposed_member: ownerResolution.proposed_member } : {}),
|
|
546
741
|
status: 'planning',
|
|
547
742
|
cadence,
|
|
548
743
|
runner,
|
|
549
744
|
...(model ? { model } : {}),
|
|
745
|
+
...(businessBinding?.business_id ? { business_id: businessBinding.business_id } : {}),
|
|
746
|
+
...(businessBinding?.workspace_id ? { workspace_id: businessBinding.workspace_id } : {}),
|
|
550
747
|
lane,
|
|
551
748
|
verifier,
|
|
552
749
|
always_on: alwaysOn,
|
|
@@ -571,6 +768,36 @@ function missingVerifierWarning(mission) {
|
|
|
571
768
|
};
|
|
572
769
|
}
|
|
573
770
|
|
|
771
|
+
function missionRunSmokeVerifier() {
|
|
772
|
+
const script = [
|
|
773
|
+
"const fs=require('fs')",
|
|
774
|
+
"const os=require('os')",
|
|
775
|
+
"const path=require('path')",
|
|
776
|
+
"const {spawnSync}=require('child_process')",
|
|
777
|
+
"const root=fs.mkdtempSync(path.join(os.tmpdir(),'atris-mission-run-verifier-'))",
|
|
778
|
+
"process.on('exit',()=>fs.rmSync(root,{recursive:true,force:true}))",
|
|
779
|
+
"fs.mkdirSync(path.join(root,'atris'),{recursive:true})",
|
|
780
|
+
"const r=spawnSync('atris',['mission','run','verifier smoke objective','--json'],{cwd:root,encoding:'utf8',env:{...process.env,ATRIS_SKIP_UPDATE_CHECK:'1'}})",
|
|
781
|
+
"if(r.status!==0){process.stderr.write(r.stderr||r.stdout);process.exit(1)}",
|
|
782
|
+
"const p=JSON.parse(r.stdout)",
|
|
783
|
+
"if(p.action!=='mission_run_started')process.exit(2)",
|
|
784
|
+
"if(p.mission?.runner!=='codex_goal')process.exit(3)",
|
|
785
|
+
"if(p.codex_goal_state?.goal?.visible_goal?.schema!=='atris.visible_chat_goal_bridge.v1')process.exit(4)",
|
|
786
|
+
].join(';');
|
|
787
|
+
return `node -e ${JSON.stringify(script)}`;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function inferRunObjectiveVerifier(objective, root = process.cwd()) {
|
|
791
|
+
const text = String(objective || '').toLowerCase();
|
|
792
|
+
if (!/\bmission\s+run\b/.test(text)) return '';
|
|
793
|
+
const sourceTest = path.join(root, 'test', 'mission-status.test.js');
|
|
794
|
+
const sourceCommand = path.join(root, 'commands', 'mission.js');
|
|
795
|
+
if (fs.existsSync(sourceTest) && fs.existsSync(sourceCommand)) {
|
|
796
|
+
return 'node --test test/mission-status.test.js';
|
|
797
|
+
}
|
|
798
|
+
return missionRunSmokeVerifier();
|
|
799
|
+
}
|
|
800
|
+
|
|
574
801
|
function startMission(args) {
|
|
575
802
|
const asJson = wantsJson(args);
|
|
576
803
|
const mission = missionFromArgs(args);
|
|
@@ -624,6 +851,137 @@ function startMission(args) {
|
|
|
624
851
|
);
|
|
625
852
|
}
|
|
626
853
|
|
|
854
|
+
function startMissionFromRunObjective(objective, args) {
|
|
855
|
+
const asJson = wantsJson(args);
|
|
856
|
+
const verifier = readFlag(args, '--verify', inferRunObjectiveVerifier(objective));
|
|
857
|
+
const stopCondition = readFlag(
|
|
858
|
+
args,
|
|
859
|
+
'--stop',
|
|
860
|
+
verifier ? 'verifier passes and visible goal lands' : 'visible goal lands and proof is ready',
|
|
861
|
+
);
|
|
862
|
+
const startArgs = [
|
|
863
|
+
objective,
|
|
864
|
+
'--owner',
|
|
865
|
+
readFlag(args, '--owner', process.env.ATRIS_AGENT_ID || 'mission-lead'),
|
|
866
|
+
'--runner',
|
|
867
|
+
readFlag(args, '--runner', 'codex_goal'),
|
|
868
|
+
'--lane',
|
|
869
|
+
readFlag(args, '--lane', 'workspace'),
|
|
870
|
+
'--cadence',
|
|
871
|
+
readFlag(args, '--cadence', 'manual'),
|
|
872
|
+
'--stop',
|
|
873
|
+
stopCondition,
|
|
874
|
+
];
|
|
875
|
+
if (verifier) startArgs.push('--verify', verifier);
|
|
876
|
+
const model = readFlag(args, '--model', '');
|
|
877
|
+
if (model) startArgs.push('--model', model);
|
|
878
|
+
if (hasFlag(args, '--always-on')) startArgs.push('--always-on');
|
|
879
|
+
if (hasFlag(args, '--xp-task') || hasFlag(args, '--agent-xp')) startArgs.push('--xp-task');
|
|
880
|
+
|
|
881
|
+
const mission = missionFromArgs(startArgs);
|
|
882
|
+
const warnings = [missingVerifierWarning(mission)].filter(Boolean);
|
|
883
|
+
ensureMemberMissionFile(mission.owner, process.cwd(), mission.objective);
|
|
884
|
+
const { mission: saved } = saveMission(mission, process.cwd(), 'mission_started', { objective: mission.objective, source: 'mission_run_objective' });
|
|
885
|
+
const memberState = renderMemberMissionState(saved.owner);
|
|
886
|
+
const logPath = appendMemberLog(saved.owner, 'Mission started from run', {
|
|
887
|
+
mission: saved.objective,
|
|
888
|
+
cadence: saved.cadence,
|
|
889
|
+
runner: saved.runner,
|
|
890
|
+
lane: saved.lane,
|
|
891
|
+
verifier: saved.verifier,
|
|
892
|
+
});
|
|
893
|
+
const worktreeBaseline = captureMissionWorktreeBaseline(saved, process.cwd());
|
|
894
|
+
const codexGoalState = refreshCodexGoalController(process.cwd());
|
|
895
|
+
printJsonOrText(
|
|
896
|
+
{
|
|
897
|
+
ok: true,
|
|
898
|
+
action: 'mission_run_started',
|
|
899
|
+
mission: saved,
|
|
900
|
+
warnings,
|
|
901
|
+
state_path: statePaths().missionsJsonl,
|
|
902
|
+
member_state: memberState,
|
|
903
|
+
log_path: logPath,
|
|
904
|
+
worktree_baseline: worktreeBaseline ? {
|
|
905
|
+
path: path.relative(process.cwd(), missionBaselinePath(saved.id)),
|
|
906
|
+
dirty_count: worktreeBaseline.dirty_count,
|
|
907
|
+
dirty_hash: worktreeBaseline.dirty_hash,
|
|
908
|
+
} : null,
|
|
909
|
+
codex_goal_state: codexGoalState,
|
|
910
|
+
next_command: codexGoalState.goal?.next_command || `atris mission tick ${saved.id} --verify`,
|
|
911
|
+
},
|
|
912
|
+
[
|
|
913
|
+
`Started mission: ${saved.objective}`,
|
|
914
|
+
`Owner: ${saved.owner}`,
|
|
915
|
+
`Runner: ${saved.runner}`,
|
|
916
|
+
...(codexGoalState.goal ? [`Visible goal: ${codexGoalState.goal.objective}`] : []),
|
|
917
|
+
...warnings.map((warning) => `Warning: ${warning.message}`),
|
|
918
|
+
],
|
|
919
|
+
asJson,
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
function attachMissionTask(args) {
|
|
924
|
+
const asJson = wantsJson(args);
|
|
925
|
+
const ref = stripKnownFlags(args, [], ['--json'])[0] || '';
|
|
926
|
+
if (!ref) {
|
|
927
|
+
exitMissionError('Usage: atris mission attach-task <id> [--json]', 1, asJson);
|
|
928
|
+
}
|
|
929
|
+
const mission = resolveMission(ref);
|
|
930
|
+
if (!mission) {
|
|
931
|
+
exitMissionError(`Mission "${ref}" not found.`, 1, asJson);
|
|
932
|
+
}
|
|
933
|
+
if (TERMINAL_STATUSES.has(mission.status)) {
|
|
934
|
+
exitMissionError(`Mission "${ref}" is ${mission.status}; task spines attach only to active missions.`, 2, asJson);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
const existingSpine = missionTaskSpine(mission);
|
|
938
|
+
if (existingSpine?.has_task) {
|
|
939
|
+
const view = missionStatusView(mission);
|
|
940
|
+
printJsonOrText(
|
|
941
|
+
{ ok: true, action: 'mission_task_spine_exists', mission: view, task_spine: view.task_spine },
|
|
942
|
+
[
|
|
943
|
+
`Mission task spine already exists: ${mission.objective}`,
|
|
944
|
+
`Task: ${view.task_spine.task_ref}`,
|
|
945
|
+
`Next: ${view.task_spine.current_step_command}`,
|
|
946
|
+
],
|
|
947
|
+
asJson,
|
|
948
|
+
);
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
const ownership = applyMissionOwnerResolution(mission, process.cwd());
|
|
953
|
+
const baseMission = ownership.mission;
|
|
954
|
+
const xpTask = createMissionXpTask(baseMission, process.cwd(), asJson);
|
|
955
|
+
const nextMission = {
|
|
956
|
+
...baseMission,
|
|
957
|
+
xp_task_enabled: true,
|
|
958
|
+
xp_task: xpTask,
|
|
959
|
+
task_ids: Array.from(new Set([...(baseMission.task_ids || []), xpTask.task_id])),
|
|
960
|
+
};
|
|
961
|
+
if (nextMission.status === 'ready' && nextMission.receipt_path) {
|
|
962
|
+
nextMission.next_action = missionXpReadyAction(nextMission, nextMission.receipt_path) || nextMission.next_action;
|
|
963
|
+
} else if (!nextMission.verifier && !nextMission.always_on) {
|
|
964
|
+
nextMission.next_action = `work task then run: atris task current-step --goal-id ${nextMission.id} --as ${nextMission.owner} --proof "<proof>" --json`;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
const { mission: saved } = saveMission(nextMission, process.cwd(), 'mission_task_spine_attached', { task_id: xpTask.task_id, task_ref: xpTask.ref });
|
|
968
|
+
const memberState = renderMemberMissionState(saved.owner);
|
|
969
|
+
const logPath = appendMemberLog(saved.owner, 'Mission task spine attached', {
|
|
970
|
+
mission: saved.objective,
|
|
971
|
+
task: xpTask.ref,
|
|
972
|
+
});
|
|
973
|
+
const view = missionStatusView(saved);
|
|
974
|
+
printJsonOrText(
|
|
975
|
+
{ ok: true, action: 'mission_task_spine_attached', mission: view, task: xpTask, task_spine: view.task_spine, member_state: memberState, log_path: logPath },
|
|
976
|
+
[
|
|
977
|
+
`Attached task spine: ${saved.objective}`,
|
|
978
|
+
`Task: ${xpTask.ref}`,
|
|
979
|
+
`Next: ${view.task_spine.current_step_command}`,
|
|
980
|
+
],
|
|
981
|
+
asJson,
|
|
982
|
+
);
|
|
983
|
+
}
|
|
984
|
+
|
|
627
985
|
function statusMission(args) {
|
|
628
986
|
const asJson = wantsJson(args);
|
|
629
987
|
const localOnly = hasFlag(args, '--local');
|
|
@@ -648,14 +1006,15 @@ function statusMission(args) {
|
|
|
648
1006
|
if (ref && !missions.length) {
|
|
649
1007
|
exitMissionError(`Mission "${ref}" not found.`, 1, asJson);
|
|
650
1008
|
}
|
|
1009
|
+
const missionViews = missions.map(missionStatusView);
|
|
651
1010
|
// Member state renders are cwd-local writes; rolled-up missions stay read-only.
|
|
652
|
-
for (const owner of new Set(
|
|
1011
|
+
for (const owner of new Set(missionViews.filter((mission) => !mission.worktree_root).map((mission) => mission.owner).filter(Boolean))) {
|
|
653
1012
|
renderMemberMissionState(owner);
|
|
654
1013
|
}
|
|
655
1014
|
const payload = {
|
|
656
1015
|
ok: true,
|
|
657
1016
|
action: 'mission_status',
|
|
658
|
-
missions,
|
|
1017
|
+
missions: missionViews,
|
|
659
1018
|
state_path: statePaths().missionsJsonl,
|
|
660
1019
|
events_path: statePaths().eventsJsonl,
|
|
661
1020
|
status_path: renderMissionStatus(),
|
|
@@ -663,14 +1022,18 @@ function statusMission(args) {
|
|
|
663
1022
|
printJsonOrText(
|
|
664
1023
|
payload,
|
|
665
1024
|
missions.length
|
|
666
|
-
?
|
|
1025
|
+
? missionViews.flatMap((mission) => [
|
|
667
1026
|
`Mission: ${mission.objective}`,
|
|
668
1027
|
` id: ${mission.id}`,
|
|
669
1028
|
` owner: ${mission.owner}`,
|
|
1029
|
+
...(mission.executed_by ? [` executed_by: ${mission.executed_by}`] : []),
|
|
670
1030
|
` state: ${mission.status}`,
|
|
671
1031
|
...missionHeartbeatLines(mission),
|
|
672
1032
|
...(mission.worktree_root ? [` worktree: ${mission.worktree_root}`] : []),
|
|
673
1033
|
` next: ${mission.next_action || 'tick or verify'}`,
|
|
1034
|
+
...(mission.task_spine?.task_ref ? [` task: ${mission.task_spine.task_ref}`] : []),
|
|
1035
|
+
...(mission.task_spine?.current_step_command ? [` task next: ${mission.task_spine.current_step_command}`] : []),
|
|
1036
|
+
...(!mission.task_spine?.has_task && mission.task_spine?.ensure_task_command ? [` task setup: ${mission.task_spine.ensure_task_command}`] : []),
|
|
674
1037
|
...(mission.receipt_path ? [` proof: ${mission.receipt_path}`] : []),
|
|
675
1038
|
...(completionGateLabel(mission.completion_gate) ? [` gate: ${completionGateLabel(mission.completion_gate)}`] : []),
|
|
676
1039
|
])
|
|
@@ -679,6 +1042,108 @@ function statusMission(args) {
|
|
|
679
1042
|
);
|
|
680
1043
|
}
|
|
681
1044
|
|
|
1045
|
+
function readMissionReceipt(receiptPath, root = process.cwd()) {
|
|
1046
|
+
if (!receiptPath) return null;
|
|
1047
|
+
const file = path.isAbsolute(receiptPath) ? receiptPath : path.join(root, receiptPath);
|
|
1048
|
+
try {
|
|
1049
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
1050
|
+
} catch {
|
|
1051
|
+
return null;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
function firstUsefulLine(text, fallback = '') {
|
|
1056
|
+
return String(text || '')
|
|
1057
|
+
.split(/\r?\n/)
|
|
1058
|
+
.map((line) => line.replace(/^[-*\s#]+/, '').trim())
|
|
1059
|
+
.filter(Boolean)
|
|
1060
|
+
.find((line) => !/^(receipt|summary|final|result)$/i.test(line))
|
|
1061
|
+
|| fallback;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
function missionWorkerLabel(mission) {
|
|
1065
|
+
const runner = String(mission && mission.runner || 'manual').toLowerCase();
|
|
1066
|
+
if (runner === 'atris2') return `Remote Atris2 computer${mission.model ? ` using ${mission.model}` : ''}`;
|
|
1067
|
+
if (runner === 'claude') return `Claude worker${mission.model ? ` using ${mission.model}` : ''}`;
|
|
1068
|
+
if (runner === 'codex_goal') return 'Codex goal handoff';
|
|
1069
|
+
return 'Local mission tick';
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
function missionWorkerSummary(mission, receipt) {
|
|
1073
|
+
if (mission && mission.worker_summary) return mission.worker_summary;
|
|
1074
|
+
const tick = receipt && receipt.result && (receipt.result.tick || (Array.isArray(receipt.result.ticks) ? receipt.result.ticks[receipt.result.ticks.length - 1] : null));
|
|
1075
|
+
if (!tick) return mission && mission.last_tick_reason ? `Last tick: ${mission.last_tick_reason}` : 'No worker receipt yet.';
|
|
1076
|
+
if (tick.atris2) {
|
|
1077
|
+
return firstUsefulLine(tick.atris2.receipt_text, tick.atris2.ok ? 'Remote worker ran and returned a response.' : 'Remote worker failed.');
|
|
1078
|
+
}
|
|
1079
|
+
if (tick.claude) {
|
|
1080
|
+
if (tick.claude.skipped) {
|
|
1081
|
+
return tick.summary || `Worker step skipped: ${tick.claude.reason || tick.reason || 'not needed'}.`;
|
|
1082
|
+
}
|
|
1083
|
+
return tick.claude.summary || firstUsefulLine(tick.claude.receipt_text, tick.claude.ok ? 'Worker ran and returned a response.' : 'Worker failed.');
|
|
1084
|
+
}
|
|
1085
|
+
if (tick.summary) return tick.summary;
|
|
1086
|
+
return tick.reason ? `Worker tick: ${tick.reason}` : 'Worker tick recorded.';
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
function missionReportFor(mission, root = process.cwd()) {
|
|
1090
|
+
const verifierReceiptPath = mission.receipt_path || null;
|
|
1091
|
+
const receipt = readMissionReceipt(verifierReceiptPath, root);
|
|
1092
|
+
const workerReceiptPath = mission.worker_receipt_path || (receipt && verifierReceiptPath) || null;
|
|
1093
|
+
const verifierPassed = mission.verifier_result && mission.verifier_result.passed === true;
|
|
1094
|
+
const operatorOutcome = mission.operator_outcome
|
|
1095
|
+
|| (verifierPassed ? 'Verifier passed.' : mission.status === 'complete' ? 'Mission is complete.' : mission.status === 'blocked' ? 'Mission is blocked.' : 'Mission is still in progress.');
|
|
1096
|
+
return {
|
|
1097
|
+
id: mission.id,
|
|
1098
|
+
objective: mission.objective,
|
|
1099
|
+
status: mission.status,
|
|
1100
|
+
operator_outcome: operatorOutcome,
|
|
1101
|
+
worker: mission.worker || missionWorkerLabel(mission),
|
|
1102
|
+
worker_summary: missionWorkerSummary(mission, receipt),
|
|
1103
|
+
worker_receipt_path: workerReceiptPath,
|
|
1104
|
+
verifier_receipt_path: verifierReceiptPath,
|
|
1105
|
+
operator_next: mission.operator_next || mission.next_action || 'Review the mission state.',
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
function reportMission(args) {
|
|
1110
|
+
const asJson = wantsJson(args);
|
|
1111
|
+
const localOnly = hasFlag(args, '--local');
|
|
1112
|
+
const ref = stripKnownFlags(args, ['--limit'], ['--json', '--local'])[0] || '';
|
|
1113
|
+
const limit = readPositiveIntegerFlag(args, '--limit', ref ? 1 : 3, { json: asJson });
|
|
1114
|
+
let missions = ref ? [resolveMission(ref)].filter(Boolean) : listMissions();
|
|
1115
|
+
if (!ref && !localOnly) {
|
|
1116
|
+
const seen = new Set(missions.map((mission) => mission.id));
|
|
1117
|
+
for (const rolled of listWorktreeRollupMissions()) {
|
|
1118
|
+
if (seen.has(rolled.id)) continue;
|
|
1119
|
+
seen.add(rolled.id);
|
|
1120
|
+
missions.push(rolled);
|
|
1121
|
+
}
|
|
1122
|
+
missions.sort((a, b) => String(b.updated_at || b.created_at || '').localeCompare(String(a.updated_at || a.created_at || '')));
|
|
1123
|
+
}
|
|
1124
|
+
if (ref && !missions.length) {
|
|
1125
|
+
exitMissionError(`Mission "${ref}" not found.`, 1, asJson);
|
|
1126
|
+
}
|
|
1127
|
+
missions = missions.slice(0, limit);
|
|
1128
|
+
const reports = missions.map((mission) => missionReportFor(mission, mission.worktree_root || process.cwd()));
|
|
1129
|
+
printJsonOrText(
|
|
1130
|
+
{ ok: true, action: 'mission_report', reports },
|
|
1131
|
+
reports.length
|
|
1132
|
+
? reports.flatMap((report) => [
|
|
1133
|
+
`Mission: ${report.objective}`,
|
|
1134
|
+
` state: ${report.status}`,
|
|
1135
|
+
` What happened: ${report.operator_outcome}`,
|
|
1136
|
+
` Worker: ${report.worker}`,
|
|
1137
|
+
` Worker summary: ${report.worker_summary}`,
|
|
1138
|
+
...(report.worker_receipt_path ? [` Worker receipt: ${report.worker_receipt_path}`] : []),
|
|
1139
|
+
...(report.verifier_receipt_path ? [` Verifier receipt: ${report.verifier_receipt_path}`] : []),
|
|
1140
|
+
` Next: ${report.operator_next}`,
|
|
1141
|
+
])
|
|
1142
|
+
: ['No missions yet. Run: atris mission start "..." --owner <member>'],
|
|
1143
|
+
asJson,
|
|
1144
|
+
);
|
|
1145
|
+
}
|
|
1146
|
+
|
|
682
1147
|
// `atris mission watch [id]` — read-only live heartbeat. Prints a line per tick as it
|
|
683
1148
|
// lands so a human (or any terminal) can see the loop is alive without rerunning status.
|
|
684
1149
|
function watchMission(args) {
|
|
@@ -1125,10 +1590,14 @@ function selectCodexGoalMission(root = process.cwd(), now = new Date()) {
|
|
|
1125
1590
|
}
|
|
1126
1591
|
|
|
1127
1592
|
function codexGoalObjective(mission) {
|
|
1128
|
-
return
|
|
1593
|
+
return mission.objective;
|
|
1129
1594
|
}
|
|
1130
1595
|
|
|
1131
1596
|
function codexGoalNextCommand(mission) {
|
|
1597
|
+
const taskSpine = missionTaskSpine(mission);
|
|
1598
|
+
if (taskSpine && !taskSpine.has_task && taskSpine.ensure_task_command) {
|
|
1599
|
+
return taskSpine.ensure_task_command;
|
|
1600
|
+
}
|
|
1132
1601
|
if (mission.status === 'ready') {
|
|
1133
1602
|
const xpAction = missionXpReadyAction(mission, mission.receipt_path);
|
|
1134
1603
|
if (xpAction) return xpAction.replace(/^queue AgentXP review: /, '');
|
|
@@ -1143,14 +1612,40 @@ function codexGoalNextCommand(mission) {
|
|
|
1143
1612
|
return `atris mission tick ${mission.id} --summary "<what changed>"`;
|
|
1144
1613
|
}
|
|
1145
1614
|
|
|
1615
|
+
function codexVisibleGoalBridge(mission, goalObjective) {
|
|
1616
|
+
return {
|
|
1617
|
+
schema: 'atris.visible_chat_goal_bridge.v1',
|
|
1618
|
+
runtime: 'codex',
|
|
1619
|
+
source: 'atris_mission',
|
|
1620
|
+
mission_id: mission.id,
|
|
1621
|
+
desired_objective: goalObjective,
|
|
1622
|
+
status: 'needs_runtime_write',
|
|
1623
|
+
state_file: '.atris/state/codex_goal.json',
|
|
1624
|
+
status_file: 'atris/status/codex-goal.md',
|
|
1625
|
+
operations: {
|
|
1626
|
+
read_current_goal: 'get_goal',
|
|
1627
|
+
keep_if_matching: 'if current goal objective equals goal.objective, continue the mission',
|
|
1628
|
+
create_when_empty_or_completed: 'create_goal({ objective: goal.objective })',
|
|
1629
|
+
complete_after_proof: 'update_goal({ status: "complete" })',
|
|
1630
|
+
refresh_next_candidate: 'atris mission goal --json',
|
|
1631
|
+
},
|
|
1632
|
+
guardrails: [
|
|
1633
|
+
'Do not complete a human-set active goal unless it matches this mission goal or the mission receipt proves handoff.',
|
|
1634
|
+
'If create_goal fails because another goal is active, keep this bridge waiting for the visible goal slot.',
|
|
1635
|
+
],
|
|
1636
|
+
};
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1146
1639
|
function codexGoalToolContract(mission) {
|
|
1147
1640
|
return {
|
|
1148
1641
|
current_policy: 'keep one visible Codex /goal active for the selected Atris mission',
|
|
1149
1642
|
read_current_goal: 'get_goal',
|
|
1150
1643
|
complete_current_goal: 'update_goal({ status: "complete" })',
|
|
1151
1644
|
select_next_goal: 'atris mission goal --json',
|
|
1152
|
-
set_next_goal: '
|
|
1153
|
-
|
|
1645
|
+
set_next_goal: 'use goal.visible_goal: create_goal({ objective: goal.objective }) when no active goal blocks the slot',
|
|
1646
|
+
visible_goal_bridge: 'goal.visible_goal',
|
|
1647
|
+
platform_requirement: 'Codex runtime must expose replace_goal/set_goal, or allow update_goal({ status: "complete" }) followed by create_goal({ objective }).',
|
|
1648
|
+
runtime_tool_sequence: 'get_goal -> update_goal({ status: "complete" }) after proof -> atris mission goal --json -> create_goal({ objective: goal.objective })',
|
|
1154
1649
|
blocked_without_platform_goal_write: true,
|
|
1155
1650
|
mission_id: mission.id,
|
|
1156
1651
|
};
|
|
@@ -1191,6 +1686,12 @@ function writeCodexGoalState(payload, root = process.cwd()) {
|
|
|
1191
1686
|
lines.push(`- reason: ${state.goal.reason}`);
|
|
1192
1687
|
lines.push(`- objective: ${state.goal.objective}`);
|
|
1193
1688
|
lines.push(`- next: ${state.goal.next_command}`);
|
|
1689
|
+
if (state.goal.visible_goal) {
|
|
1690
|
+
lines.push(`- visible goal: ${state.goal.visible_goal.status}`);
|
|
1691
|
+
lines.push(`- visible goal desired: ${state.goal.visible_goal.desired_objective}`);
|
|
1692
|
+
lines.push(`- visible goal create: ${state.goal.visible_goal.operations.create_when_empty_or_completed}`);
|
|
1693
|
+
lines.push(`- visible goal complete: ${state.goal.visible_goal.operations.complete_after_proof}`);
|
|
1694
|
+
}
|
|
1194
1695
|
lines.push(`- platform write blocked: ${state.goal.codex_tool_contract.blocked_without_platform_goal_write}`);
|
|
1195
1696
|
} else {
|
|
1196
1697
|
lines.push('- mission: none');
|
|
@@ -1219,14 +1720,21 @@ function buildCodexGoalPayload(root = process.cwd(), options = {}) {
|
|
|
1219
1720
|
}
|
|
1220
1721
|
|
|
1221
1722
|
const { mission, reason } = selected;
|
|
1723
|
+
const taskSpine = missionTaskSpine(mission);
|
|
1724
|
+
const missionView = missionStatusView(mission);
|
|
1725
|
+
const objective = codexGoalObjective(mission);
|
|
1222
1726
|
const goal = {
|
|
1223
|
-
objective
|
|
1727
|
+
objective,
|
|
1224
1728
|
mission_id: mission.id,
|
|
1225
1729
|
mission_objective: mission.objective,
|
|
1226
1730
|
mission_status: mission.status,
|
|
1731
|
+
owner: taskSpine?.owner || mission.owner,
|
|
1732
|
+
executed_by: taskSpine?.executed_by || mission.executed_by || null,
|
|
1733
|
+
task_spine: taskSpine,
|
|
1227
1734
|
reason,
|
|
1228
1735
|
next_command: codexGoalNextCommand(mission),
|
|
1229
1736
|
replace_after: 'After proof or verifier pass, run atris mission goal --json again and replace the Codex /goal with the returned objective.',
|
|
1737
|
+
visible_goal: codexVisibleGoalBridge(mission, objective),
|
|
1230
1738
|
codex_tool_contract: codexGoalToolContract(mission),
|
|
1231
1739
|
};
|
|
1232
1740
|
const heartbeat = heartbeatMode ? codexGoalHeartbeat(goal, mission) : undefined;
|
|
@@ -1234,7 +1742,7 @@ function buildCodexGoalPayload(root = process.cwd(), options = {}) {
|
|
|
1234
1742
|
ok: true,
|
|
1235
1743
|
action: heartbeatMode ? 'codex_goal_heartbeat' : 'codex_goal_candidate',
|
|
1236
1744
|
goal,
|
|
1237
|
-
mission,
|
|
1745
|
+
mission: missionView,
|
|
1238
1746
|
heartbeat,
|
|
1239
1747
|
};
|
|
1240
1748
|
}
|
|
@@ -1249,10 +1757,8 @@ function refreshCodexGoalController(root = process.cwd(), options = {}) {
|
|
|
1249
1757
|
};
|
|
1250
1758
|
}
|
|
1251
1759
|
|
|
1252
|
-
function
|
|
1760
|
+
function runAtrisMissionJsonCommand(root, args, options = {}) {
|
|
1253
1761
|
const cliPath = path.join(__dirname, '..', 'bin', 'atris.js');
|
|
1254
|
-
const args = ['mission', 'run', '--due', '--max-ticks', '1', '--complete-on-pass', '--json'];
|
|
1255
|
-
if (options.noClaude) args.push('--no-claude');
|
|
1256
1762
|
const result = spawnSync(process.execPath, [cliPath, ...args], {
|
|
1257
1763
|
cwd: root,
|
|
1258
1764
|
encoding: 'utf8',
|
|
@@ -1264,15 +1770,79 @@ function runMissionRunDueOnce(root = process.cwd(), options = {}) {
|
|
|
1264
1770
|
payload = JSON.parse(result.stdout || '{}');
|
|
1265
1771
|
} catch {}
|
|
1266
1772
|
return {
|
|
1773
|
+
action: options.action || null,
|
|
1267
1774
|
command: `atris ${args.join(' ')}`,
|
|
1268
1775
|
status: result.status,
|
|
1269
1776
|
ok: result.status === 0,
|
|
1777
|
+
heavy_work: options.heavyWork === true,
|
|
1778
|
+
setup_work: options.setupWork === true,
|
|
1270
1779
|
stdout: String(result.stdout || '').slice(-4000),
|
|
1271
1780
|
stderr: String(result.stderr || '').slice(-4000),
|
|
1272
1781
|
payload,
|
|
1273
1782
|
};
|
|
1274
1783
|
}
|
|
1275
1784
|
|
|
1785
|
+
function runMissionRunDueOnce(root = process.cwd(), options = {}) {
|
|
1786
|
+
const args = ['mission', 'run', '--due', '--max-ticks', '1', '--complete-on-pass', '--json'];
|
|
1787
|
+
if (options.noClaude) args.push('--no-claude');
|
|
1788
|
+
return runAtrisMissionJsonCommand(root, args, {
|
|
1789
|
+
action: 'mission_run_due',
|
|
1790
|
+
heavyWork: true,
|
|
1791
|
+
});
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
function goalLoopNextCommandPlan(goal) {
|
|
1795
|
+
const command = String(goal?.next_command || '').trim();
|
|
1796
|
+
const attach = command.match(/^atris\s+mission\s+(attach-task|ensure-task|task-spine)\s+([A-Za-z0-9_.:-]+)\s+--json$/);
|
|
1797
|
+
if (attach) {
|
|
1798
|
+
return {
|
|
1799
|
+
action: 'mission_attach_task',
|
|
1800
|
+
command,
|
|
1801
|
+
args: ['mission', 'attach-task', attach[2], '--json'],
|
|
1802
|
+
heavy_work: false,
|
|
1803
|
+
setup_work: true,
|
|
1804
|
+
run_when_due_only: false,
|
|
1805
|
+
};
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
const dueRun = command.match(/^atris\s+mission\s+run\s+--due\s+--max-ticks\s+([0-9]+)(\s+--complete-on-pass)?(?:\s+--json)?$/);
|
|
1809
|
+
if (dueRun) {
|
|
1810
|
+
const args = ['mission', 'run', '--due', '--max-ticks', dueRun[1]];
|
|
1811
|
+
if (dueRun[2]) args.push('--complete-on-pass');
|
|
1812
|
+
args.push('--json');
|
|
1813
|
+
return {
|
|
1814
|
+
action: 'mission_run_due',
|
|
1815
|
+
command: `atris ${args.join(' ')}`,
|
|
1816
|
+
args,
|
|
1817
|
+
heavy_work: true,
|
|
1818
|
+
setup_work: false,
|
|
1819
|
+
run_when_due_only: true,
|
|
1820
|
+
};
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
return null;
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
function shouldRunGoalLoopCommand(heartbeat, plan) {
|
|
1827
|
+
if (!heartbeat?.goal) return false;
|
|
1828
|
+
if (plan && plan.run_when_due_only === false) return true;
|
|
1829
|
+
return heartbeat.heartbeat?.due === true;
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
function runMissionGoalNextCommand(root = process.cwd(), heartbeat, options = {}) {
|
|
1833
|
+
const plan = goalLoopNextCommandPlan(heartbeat?.goal);
|
|
1834
|
+
if (plan) {
|
|
1835
|
+
const args = [...plan.args];
|
|
1836
|
+
if (options.noClaude && plan.action === 'mission_run_due') args.push('--no-claude');
|
|
1837
|
+
return runAtrisMissionJsonCommand(root, args, {
|
|
1838
|
+
action: plan.action,
|
|
1839
|
+
heavyWork: plan.heavy_work,
|
|
1840
|
+
setupWork: plan.setup_work,
|
|
1841
|
+
});
|
|
1842
|
+
}
|
|
1843
|
+
return runMissionRunDueOnce(root, options);
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1276
1846
|
function sleep(ms, signal) {
|
|
1277
1847
|
return new Promise((resolve, reject) => {
|
|
1278
1848
|
if (ms <= 0) return resolve();
|
|
@@ -1600,7 +2170,11 @@ async function runMission(args) {
|
|
|
1600
2170
|
const maxTicks = Math.max(1, Number(maxTicksFlag) || MISSION_RUN_DEFAULTS.maxTicks);
|
|
1601
2171
|
const maxWallSeconds = Math.max(60, Number(readFlag(args, '--max-wall', '')) || MISSION_RUN_DEFAULTS.maxWallSeconds);
|
|
1602
2172
|
const cadenceOverride = readFlag(args, '--cadence', '');
|
|
1603
|
-
const ref = stripKnownFlags(
|
|
2173
|
+
const ref = stripKnownFlags(
|
|
2174
|
+
args,
|
|
2175
|
+
['--max-ticks', '--max-wall', '--cadence', '--owner', '--runner', '--lane', '--verify', '--stop', '--model'],
|
|
2176
|
+
['--json', '--due', '--no-claude', '--no-verify', '--complete-on-pass', '--no-drain', '--always-on', '--xp-task', '--agent-xp'],
|
|
2177
|
+
).join(' ').trim();
|
|
1604
2178
|
|
|
1605
2179
|
let mission = dueMode && !ref ? selectDueMission() : resolveMission(ref);
|
|
1606
2180
|
if (!mission && dueMode && !ref) {
|
|
@@ -1611,8 +2185,12 @@ async function runMission(args) {
|
|
|
1611
2185
|
);
|
|
1612
2186
|
return;
|
|
1613
2187
|
}
|
|
2188
|
+
if (!mission && ref && /\s/.test(ref) && !String(ref).startsWith('mission-')) {
|
|
2189
|
+
startMissionFromRunObjective(ref, args);
|
|
2190
|
+
return;
|
|
2191
|
+
}
|
|
1614
2192
|
if (!mission) {
|
|
1615
|
-
exitMissionError(ref ? `Mission "${ref}" not found.` : 'Usage: atris mission run <id> [--max-ticks 4] [--max-wall 3600]', 1, asJson);
|
|
2193
|
+
exitMissionError(ref ? `Mission "${ref}" not found.` : 'Usage: atris mission run <id|objective> [--max-ticks 4] [--max-wall 3600]', 1, asJson);
|
|
1616
2194
|
}
|
|
1617
2195
|
if (['complete', 'stopped'].includes(mission.status)) {
|
|
1618
2196
|
if (asJson) {
|
|
@@ -1752,9 +2330,11 @@ async function runMission(args) {
|
|
|
1752
2330
|
} else if (atris2Runner) {
|
|
1753
2331
|
const prompt = buildTickPrompt(mission, tickIdx, effectiveMaxTicks, frozen);
|
|
1754
2332
|
const { runAtris2Turn } = require('./probe');
|
|
2333
|
+
const businessId = businessIdForAtris2Mission(mission, cwd);
|
|
1755
2334
|
const turn = await runAtris2Turn({
|
|
1756
2335
|
prompt,
|
|
1757
2336
|
model: mission.model || 'atris:fast',
|
|
2337
|
+
business: businessId,
|
|
1758
2338
|
maxTurns: 16,
|
|
1759
2339
|
signal: controller.signal,
|
|
1760
2340
|
});
|
|
@@ -2201,22 +2781,43 @@ function completeMission(args) {
|
|
|
2201
2781
|
exitMissionError(`[mission complete] ${gate.reason}. Run: atris mission tick ${mission.id} --verify (or override as operator with --force)`, 2, asJson);
|
|
2202
2782
|
}
|
|
2203
2783
|
const baselineSummary = pruneMissionWorktreeBaseline(mission, process.cwd());
|
|
2204
|
-
const
|
|
2784
|
+
const completionGate = { ...gate, forced: force && !gate.ok };
|
|
2785
|
+
const baseNext = {
|
|
2205
2786
|
...mission,
|
|
2206
2787
|
status: 'complete',
|
|
2207
2788
|
completed_at: stampIso(),
|
|
2208
2789
|
proof,
|
|
2209
|
-
completion_gate:
|
|
2790
|
+
completion_gate: completionGate,
|
|
2210
2791
|
worktree_baseline: baselineSummary || mission.worktree_baseline || null,
|
|
2211
2792
|
next_action: 'mission complete',
|
|
2212
2793
|
};
|
|
2794
|
+
const xpNextCommand = missionXpReadyAction(baseNext, proof);
|
|
2795
|
+
const completion = missionCompletionReceipt(baseNext, proof, xpNextCommand);
|
|
2796
|
+
const next = {
|
|
2797
|
+
...baseNext,
|
|
2798
|
+
landing: completion.landing,
|
|
2799
|
+
result: completion.result,
|
|
2800
|
+
};
|
|
2213
2801
|
const { mission: saved } = saveMission(next, process.cwd(), 'mission_completed', { proof, completion_gate: next.completion_gate });
|
|
2214
2802
|
const logPath = appendMemberLog(saved.owner, 'Mission completed', { mission: saved.objective, proof });
|
|
2215
2803
|
const codexGoalState = refreshCodexGoalController(process.cwd());
|
|
2216
|
-
const xpNextCommand = missionXpReadyAction(saved, proof);
|
|
2217
2804
|
printJsonOrText(
|
|
2218
|
-
{
|
|
2219
|
-
|
|
2805
|
+
{
|
|
2806
|
+
ok: true,
|
|
2807
|
+
action: 'mission_completed',
|
|
2808
|
+
mission: saved,
|
|
2809
|
+
landing: completion.landing,
|
|
2810
|
+
result: completion.result,
|
|
2811
|
+
log_path: logPath,
|
|
2812
|
+
codex_goal_state: codexGoalState,
|
|
2813
|
+
xp_next_command: xpNextCommand,
|
|
2814
|
+
},
|
|
2815
|
+
[
|
|
2816
|
+
`Completed mission: ${saved.objective}`,
|
|
2817
|
+
...missionResultLines(completion),
|
|
2818
|
+
`Proof: ${proof}`,
|
|
2819
|
+
...(xpNextCommand ? [`AgentXP: ${xpNextCommand}`] : []),
|
|
2820
|
+
],
|
|
2220
2821
|
asJson,
|
|
2221
2822
|
);
|
|
2222
2823
|
}
|
|
@@ -2311,16 +2912,27 @@ async function goalLoopMission(args) {
|
|
|
2311
2912
|
iteration: index + 1,
|
|
2312
2913
|
heartbeat,
|
|
2313
2914
|
ran_heavy_work: false,
|
|
2915
|
+
ran_setup_work: false,
|
|
2314
2916
|
dry_run: dryRun,
|
|
2315
2917
|
};
|
|
2316
2918
|
|
|
2317
|
-
|
|
2919
|
+
const commandPlan = goalLoopNextCommandPlan(heartbeat.goal);
|
|
2920
|
+
if (shouldRunGoalLoopCommand(heartbeat, commandPlan)) {
|
|
2318
2921
|
if (dryRun) {
|
|
2319
2922
|
event.ran_heavy_work = false;
|
|
2320
|
-
event.
|
|
2923
|
+
event.ran_setup_work = false;
|
|
2924
|
+
event.run = {
|
|
2925
|
+
skipped: true,
|
|
2926
|
+
reason: 'dry-run',
|
|
2927
|
+
action: commandPlan?.action || 'mission_run_due',
|
|
2928
|
+
command: commandPlan?.command || 'atris mission run --due --max-ticks 1 --complete-on-pass --json',
|
|
2929
|
+
heavy_work: commandPlan?.heavy_work === true,
|
|
2930
|
+
setup_work: commandPlan?.setup_work === true,
|
|
2931
|
+
};
|
|
2321
2932
|
} else {
|
|
2322
|
-
event.run =
|
|
2323
|
-
event.ran_heavy_work = event.run.ok === true;
|
|
2933
|
+
event.run = runMissionGoalNextCommand(root, heartbeat, { noClaude });
|
|
2934
|
+
event.ran_heavy_work = event.run.ok === true && event.run.heavy_work === true;
|
|
2935
|
+
event.ran_setup_work = event.run.ok === true && event.run.setup_work === true;
|
|
2324
2936
|
event.after_run = refreshCodexGoalController(root, { heartbeat: true });
|
|
2325
2937
|
}
|
|
2326
2938
|
}
|
|
@@ -2347,6 +2959,7 @@ async function goalLoopMission(args) {
|
|
|
2347
2959
|
max_iterations: maxIterations,
|
|
2348
2960
|
max_wall_seconds: maxWallSeconds,
|
|
2349
2961
|
heavy_runs: events.filter((event) => event.ran_heavy_work).length,
|
|
2962
|
+
setup_runs: events.filter((event) => event.ran_setup_work).length,
|
|
2350
2963
|
events,
|
|
2351
2964
|
final_state: finalState,
|
|
2352
2965
|
};
|
|
@@ -2373,6 +2986,8 @@ atris mission - durable goal + loop + owner + proof state
|
|
|
2373
2986
|
default model atris:fast; runner codex_goal publishes the goal for a live
|
|
2374
2987
|
Codex session to pull via atris mission goal)
|
|
2375
2988
|
atris mission status [id] [--status <state>] [--limit <n>] [--local] [--json]
|
|
2989
|
+
atris mission attach-task <id> [--json] Create the missing task spine for an existing active mission
|
|
2990
|
+
atris mission report [id] [--limit <n>] [--local] [--json] Plain outcome, worker receipt, verifier receipt, and next move
|
|
2376
2991
|
atris mission watch [id] [--interval <s>] [--idle-every <s>] Live heartbeat: prints a line per tick as it lands
|
|
2377
2992
|
atris mission layers [--mission <id-substr>] [--since <date>] [--json] Per-layer growth curve across tick receipts
|
|
2378
2993
|
(rolls up sibling git-worktree missions; --local scopes to this checkout)
|
|
@@ -2389,7 +3004,7 @@ Autonomy recipe:
|
|
|
2389
3004
|
1. Pick an owner member: atris member create <member> (if missing)
|
|
2390
3005
|
2. Start a current-agent mission with a verifier:
|
|
2391
3006
|
atris mission start "ship one proof" --owner <member> --runner codex_goal --lane code --verify "npm test" --stop "verifier passes" --xp-task
|
|
2392
|
-
3. Codex sessions: atris mission goal --json, then
|
|
3007
|
+
3. Codex sessions: atris mission goal --json, then mirror goal.visible_goal into the native chat goal
|
|
2393
3008
|
Overnight controller: atris mission goal --heartbeat --json
|
|
2394
3009
|
Bounded overnight runner: atris mission goal-loop --max-wall 28800 --no-claude --json
|
|
2395
3010
|
4. Do one bounded step, then record it:
|
|
@@ -2582,6 +3197,13 @@ function missionCommand(args) {
|
|
|
2582
3197
|
case 'list':
|
|
2583
3198
|
case 'ls':
|
|
2584
3199
|
return statusMission(rest);
|
|
3200
|
+
case 'attach-task':
|
|
3201
|
+
case 'ensure-task':
|
|
3202
|
+
case 'task-spine':
|
|
3203
|
+
return attachMissionTask(rest);
|
|
3204
|
+
case 'report':
|
|
3205
|
+
case 'debrief':
|
|
3206
|
+
return reportMission(rest);
|
|
2585
3207
|
case 'watch':
|
|
2586
3208
|
return watchMission(rest);
|
|
2587
3209
|
case 'layers':
|
|
@@ -2625,6 +3247,7 @@ module.exports = {
|
|
|
2625
3247
|
classifyPathsByLayer,
|
|
2626
3248
|
resolveClaudeRunnerModel,
|
|
2627
3249
|
resolveClaudeRunnerBin,
|
|
3250
|
+
businessIdForAtris2Mission,
|
|
2628
3251
|
detectUnavailableModel,
|
|
2629
3252
|
missionPauseNextAction,
|
|
2630
3253
|
consecutiveSameReasonErrors,
|