atris 3.15.56 → 3.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +2 -2
- package/GETTING_STARTED.md +1 -1
- package/PERSONA.md +4 -4
- package/README.md +11 -11
- package/atris/skills/copy-editor/SKILL.md +30 -4
- package/atris/skills/improve/SKILL.md +18 -20
- package/atris/wiki/concepts/agent-activation-contract.md +5 -3
- package/atris/wiki/concepts/workspace-initialization-contract.md +4 -4
- package/atris/wiki/index.md +1 -0
- package/ax +522 -73
- package/bin/atris.js +32 -31
- package/commands/align.js +0 -14
- package/commands/apps.js +102 -1
- package/commands/autopilot.js +197 -22
- package/commands/brain.js +219 -34
- package/commands/brainstorm.js +0 -829
- package/commands/computer.js +45 -83
- package/commands/improve.js +501 -0
- package/commands/integrations.js +228 -0
- package/commands/lesson.js +44 -0
- package/commands/member.js +4498 -226
- package/commands/mission.js +302 -27
- package/commands/now.js +89 -1
- package/commands/radar.js +181 -56
- package/commands/skill.js +37 -6
- package/commands/soul.js +0 -4
- package/commands/task.js +5582 -517
- package/commands/terminal.js +14 -10
- package/commands/wiki.js +87 -1
- package/commands/workflow.js +288 -73
- package/commands/worktree.js +52 -15
- package/commands/xp.js +41 -65
- package/lib/auto-accept-certified.js +294 -0
- package/lib/file-ops.js +0 -184
- package/lib/member-alive.js +232 -0
- package/lib/policy-lessons.js +280 -0
- package/lib/receipt-evidence.js +64 -0
- package/lib/state-detection.js +34 -0
- package/lib/task-db.js +568 -16
- package/lib/task-proof.js +43 -0
- package/package.json +1 -1
- package/utils/auth.js +13 -4
- package/commands/research.js +0 -52
- package/lib/section-merge.js +0 -196
package/commands/mission.js
CHANGED
|
@@ -191,7 +191,7 @@ function createMissionXpTask(mission, root = process.cwd(), asJson = false) {
|
|
|
191
191
|
taskDb.noteTask(db, {
|
|
192
192
|
id: task.id,
|
|
193
193
|
actor: process.env.ATRIS_AGENT_ID || mission.owner || 'mission-lead',
|
|
194
|
-
content: `Mission goal loop XP bridge for ${mission.id}. Proof goes through task
|
|
194
|
+
content: `Mission goal loop XP bridge for ${mission.id}. Proof goes through task current-step; AgentXP lands only after human accept.`,
|
|
195
195
|
});
|
|
196
196
|
}
|
|
197
197
|
const { outPath } = writeMissionTaskProjection(taskDb, db, workspaceRoot);
|
|
@@ -262,6 +262,36 @@ function listMissions(root = process.cwd()) {
|
|
|
262
262
|
.sort((a, b) => String(b.updated_at || b.created_at || '').localeCompare(String(a.updated_at || a.created_at || '')));
|
|
263
263
|
}
|
|
264
264
|
|
|
265
|
+
// Cross-worktree rollup: missions started with --worktree keep their state inside
|
|
266
|
+
// that worktree, so a plain `mission status` from any single checkout is blind to
|
|
267
|
+
// them. Enumerate sibling git worktrees and surface their missions read-only.
|
|
268
|
+
function listWorktreeRollupMissions(root = process.cwd()) {
|
|
269
|
+
let entries = [];
|
|
270
|
+
try {
|
|
271
|
+
entries = require('./worktree').listWorktrees(root);
|
|
272
|
+
} catch {
|
|
273
|
+
return []; // not a git repo (or git unavailable): nothing to roll up
|
|
274
|
+
}
|
|
275
|
+
let here = root;
|
|
276
|
+
try {
|
|
277
|
+
here = fs.realpathSync(root);
|
|
278
|
+
} catch { /* keep raw path */ }
|
|
279
|
+
const rolled = [];
|
|
280
|
+
for (const entry of entries) {
|
|
281
|
+
let wtRoot;
|
|
282
|
+
try {
|
|
283
|
+
wtRoot = fs.realpathSync(entry.path);
|
|
284
|
+
} catch {
|
|
285
|
+
continue; // stale worktree record
|
|
286
|
+
}
|
|
287
|
+
if (wtRoot === here) continue;
|
|
288
|
+
for (const mission of listMissions(wtRoot)) {
|
|
289
|
+
rolled.push({ ...mission, worktree_root: entry.path, worktree_branch: entry.branch });
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return rolled;
|
|
293
|
+
}
|
|
294
|
+
|
|
265
295
|
function resolveMission(ref, root = process.cwd()) {
|
|
266
296
|
const missions = listMissions(root);
|
|
267
297
|
if (!ref) return missions.find((mission) => !TERMINAL_STATUSES.has(mission.status)) || missions[0] || null;
|
|
@@ -422,6 +452,13 @@ function renderMemberMissionState(owner, root = process.cwd()) {
|
|
|
422
452
|
return { missionPath, nowPath };
|
|
423
453
|
}
|
|
424
454
|
|
|
455
|
+
// One glanceable label for how a mission earned its completion: evidence
|
|
456
|
+
// source when the gate passed, an explicit marker when an operator forced it.
|
|
457
|
+
function completionGateLabel(gate) {
|
|
458
|
+
if (!gate) return null;
|
|
459
|
+
return gate.forced ? `forced override (${gate.source})` : gate.source;
|
|
460
|
+
}
|
|
461
|
+
|
|
425
462
|
function renderMissionStatus(root = process.cwd()) {
|
|
426
463
|
const paths = statePaths(root);
|
|
427
464
|
const missions = listMissions(root);
|
|
@@ -443,6 +480,8 @@ function renderMissionStatus(root = process.cwd()) {
|
|
|
443
480
|
lines.push(` - next: ${mission.next_action || 'tick or verify'}`);
|
|
444
481
|
if (mission.xp_task?.ref) lines.push(` - AgentXP task: ${mission.xp_task.ref}`);
|
|
445
482
|
if (mission.receipt_path) lines.push(` - proof: ${mission.receipt_path}`);
|
|
483
|
+
const gateLabel = completionGateLabel(mission.completion_gate);
|
|
484
|
+
if (gateLabel) lines.push(` - gate: ${gateLabel}`);
|
|
446
485
|
}
|
|
447
486
|
lines.push('');
|
|
448
487
|
}
|
|
@@ -461,7 +500,8 @@ function missionXpTaskRefFromMission(mission) {
|
|
|
461
500
|
function missionXpReadyAction(mission, receiptPath) {
|
|
462
501
|
const ref = missionXpTaskRefFromMission(mission);
|
|
463
502
|
if (!ref || !receiptPath) return null;
|
|
464
|
-
|
|
503
|
+
const owner = mission.owner || process.env.ATRIS_AGENT_ID || 'mission-lead';
|
|
504
|
+
return `queue AgentXP review: atris task current-step --goal-id ${mission.id} --as ${owner} --proof "${receiptPath}" --json`;
|
|
465
505
|
}
|
|
466
506
|
|
|
467
507
|
function missionFromArgs(args) {
|
|
@@ -475,9 +515,9 @@ function missionFromArgs(args) {
|
|
|
475
515
|
'--stop',
|
|
476
516
|
'--task',
|
|
477
517
|
'--ask',
|
|
478
|
-
], ['--json', '--always-on', '--xp-task', '--agent-xp']).join(' ').trim();
|
|
518
|
+
], ['--json', '--always-on', '--xp-task', '--agent-xp', '--worktree']).join(' ').trim();
|
|
479
519
|
if (!objective) {
|
|
480
|
-
exitMissionError('Usage: atris mission start "<objective>" --owner <member> [--verify "..."] [--cadence manual]', 1, wantsJson(args));
|
|
520
|
+
exitMissionError('Usage: atris mission start "<objective>" --owner <member> [--verify "..."] [--cadence manual] [--worktree]', 1, wantsJson(args));
|
|
481
521
|
}
|
|
482
522
|
const owner = readFlag(args, '--owner', process.env.ATRIS_AGENT_ID || 'mission-lead');
|
|
483
523
|
const cadence = readFlag(args, '--cadence', readFlag(args, '--loop', 'manual')) || 'manual';
|
|
@@ -527,12 +567,27 @@ function missingVerifierWarning(mission) {
|
|
|
527
567
|
function startMission(args) {
|
|
528
568
|
const asJson = wantsJson(args);
|
|
529
569
|
const mission = missionFromArgs(args);
|
|
570
|
+
// --worktree: bind the mission to its own isolated checkout. We chdir before
|
|
571
|
+
// any state writes so the mission record, baseline sidecar, receipts, and
|
|
572
|
+
// member files all land inside the worktree — ticks run there, and the main
|
|
573
|
+
// checkout's dirt never reaches the mission baseline.
|
|
574
|
+
if (hasFlag(args, '--worktree')) {
|
|
575
|
+
let created;
|
|
576
|
+
try {
|
|
577
|
+
const { createAgentWorktree } = require('./worktree');
|
|
578
|
+
created = createAgentWorktree({ member: mission.owner, task: mission.objective });
|
|
579
|
+
} catch (e) {
|
|
580
|
+
exitMissionError(`[mission start] worktree creation failed: ${e.message}`, 2, asJson);
|
|
581
|
+
}
|
|
582
|
+
mission.worktree = { path: created.path, branch: created.branch, base: created.base };
|
|
583
|
+
process.chdir(created.path);
|
|
584
|
+
}
|
|
530
585
|
if (mission.xp_task_enabled) {
|
|
531
586
|
const xpTask = createMissionXpTask(mission, process.cwd(), asJson);
|
|
532
587
|
mission.xp_task = xpTask;
|
|
533
588
|
mission.task_ids = Array.from(new Set([...(mission.task_ids || []), xpTask.task_id]));
|
|
534
589
|
if (!mission.verifier && !mission.always_on) {
|
|
535
|
-
mission.next_action = `work task then run: atris task
|
|
590
|
+
mission.next_action = `work task then run: atris task current-step --goal-id ${mission.id} --as ${mission.owner} --proof "<proof>" --json`;
|
|
536
591
|
}
|
|
537
592
|
}
|
|
538
593
|
const warnings = [missingVerifierWarning(mission)].filter(Boolean);
|
|
@@ -546,15 +601,17 @@ function startMission(args) {
|
|
|
546
601
|
lane: saved.lane,
|
|
547
602
|
verifier: saved.verifier,
|
|
548
603
|
});
|
|
604
|
+
const worktreeBaseline = captureMissionWorktreeBaseline(saved, process.cwd());
|
|
549
605
|
printJsonOrText(
|
|
550
|
-
{ ok: true, action: 'mission_started', mission: saved, warnings, state_path: statePaths().missionsJsonl, member_state: memberState, log_path: logPath },
|
|
606
|
+
{ 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 },
|
|
551
607
|
[
|
|
552
608
|
`Started mission: ${saved.objective}`,
|
|
553
609
|
`Owner: ${saved.owner}`,
|
|
554
610
|
`State: ${saved.status}`,
|
|
611
|
+
...(saved.worktree ? [`Worktree: ${saved.worktree.path}`, `Branch: ${saved.worktree.branch}`] : []),
|
|
555
612
|
...warnings.map((warning) => `Warning: ${warning.message}`),
|
|
556
613
|
...(saved.xp_task ? [`AgentXP task: ${saved.xp_task.ref}`] : []),
|
|
557
|
-
`Next: atris mission tick ${saved.id}
|
|
614
|
+
...(saved.worktree ? [`Next: cd ${saved.worktree.path} && atris mission tick ${saved.id}`] : [`Next: atris mission tick ${saved.id}`]),
|
|
558
615
|
],
|
|
559
616
|
asJson,
|
|
560
617
|
);
|
|
@@ -562,19 +619,30 @@ function startMission(args) {
|
|
|
562
619
|
|
|
563
620
|
function statusMission(args) {
|
|
564
621
|
const asJson = wantsJson(args);
|
|
565
|
-
const
|
|
622
|
+
const localOnly = hasFlag(args, '--local');
|
|
623
|
+
const ref = stripKnownFlags(args, ['--status', '--limit'], ['--json', '--local'])[0] || '';
|
|
566
624
|
const statusFilter = readFlag(args, '--status', '');
|
|
567
625
|
if (statusFilter && !VALID_STATUSES.has(statusFilter) && !STATUS_ALIASES.has(statusFilter)) {
|
|
568
626
|
exitMissionError(`Invalid --status: ${statusFilter}`, 2, asJson);
|
|
569
627
|
}
|
|
570
628
|
const limit = readPositiveIntegerFlag(args, '--limit', null, { json: asJson });
|
|
571
629
|
let missions = ref ? [resolveMission(ref)].filter(Boolean) : listMissions();
|
|
630
|
+
if (!ref && !localOnly) {
|
|
631
|
+
const seen = new Set(missions.map((mission) => mission.id));
|
|
632
|
+
for (const rolled of listWorktreeRollupMissions()) {
|
|
633
|
+
if (seen.has(rolled.id)) continue;
|
|
634
|
+
seen.add(rolled.id);
|
|
635
|
+
missions.push(rolled);
|
|
636
|
+
}
|
|
637
|
+
missions.sort((a, b) => String(b.updated_at || b.created_at || '').localeCompare(String(a.updated_at || a.created_at || '')));
|
|
638
|
+
}
|
|
572
639
|
if (!ref && statusFilter) missions = missions.filter((mission) => missionMatchesStatusFilter(mission, statusFilter));
|
|
573
640
|
if (!ref && limit) missions = missions.slice(0, limit);
|
|
574
641
|
if (ref && !missions.length) {
|
|
575
642
|
exitMissionError(`Mission "${ref}" not found.`, 1, asJson);
|
|
576
643
|
}
|
|
577
|
-
|
|
644
|
+
// Member state renders are cwd-local writes; rolled-up missions stay read-only.
|
|
645
|
+
for (const owner of new Set(missions.filter((mission) => !mission.worktree_root).map((mission) => mission.owner).filter(Boolean))) {
|
|
578
646
|
renderMemberMissionState(owner);
|
|
579
647
|
}
|
|
580
648
|
const payload = {
|
|
@@ -593,8 +661,10 @@ function statusMission(args) {
|
|
|
593
661
|
` id: ${mission.id}`,
|
|
594
662
|
` owner: ${mission.owner}`,
|
|
595
663
|
` state: ${mission.status}`,
|
|
664
|
+
...(mission.worktree_root ? [` worktree: ${mission.worktree_root}`] : []),
|
|
596
665
|
` next: ${mission.next_action || 'tick or verify'}`,
|
|
597
666
|
...(mission.receipt_path ? [` proof: ${mission.receipt_path}`] : []),
|
|
667
|
+
...(completionGateLabel(mission.completion_gate) ? [` gate: ${completionGateLabel(mission.completion_gate)}`] : []),
|
|
598
668
|
])
|
|
599
669
|
: ['No missions yet. Run: atris mission start "..." --owner <member>'],
|
|
600
670
|
asJson,
|
|
@@ -657,6 +727,38 @@ function runVerifier(command, root = process.cwd()) {
|
|
|
657
727
|
};
|
|
658
728
|
}
|
|
659
729
|
|
|
730
|
+
const REVIEW_LANE_DRAIN_TIMEOUT_MS = 120000;
|
|
731
|
+
|
|
732
|
+
// Bounded agent-side review sweep, recorded on the tick. Failures never break
|
|
733
|
+
// the mission loop; they surface in the tick record instead.
|
|
734
|
+
function runReviewLaneDrain(root = process.cwd()) {
|
|
735
|
+
const cliPath = path.resolve(__dirname, '..', 'bin', 'atris.js');
|
|
736
|
+
const res = spawnSync(process.execPath, [cliPath, 'task', 'review-lane-run', '--json'], {
|
|
737
|
+
cwd: root,
|
|
738
|
+
encoding: 'utf8',
|
|
739
|
+
timeout: REVIEW_LANE_DRAIN_TIMEOUT_MS,
|
|
740
|
+
});
|
|
741
|
+
let receipt = null;
|
|
742
|
+
try {
|
|
743
|
+
receipt = JSON.parse(res.stdout);
|
|
744
|
+
} catch { /* fall through to error shape */ }
|
|
745
|
+
if (!receipt) {
|
|
746
|
+
return {
|
|
747
|
+
ok: false,
|
|
748
|
+
error: 'review_lane_run_unparseable',
|
|
749
|
+
status: res.status ?? null,
|
|
750
|
+
stderr: String(res.stderr || '').slice(-400),
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
return {
|
|
754
|
+
ok: receipt.ok === true,
|
|
755
|
+
run_count: receipt.run_count ?? null,
|
|
756
|
+
total_acted_count: receipt.total_acted_count ?? 0,
|
|
757
|
+
stopped_reason: receipt.stopped_reason || null,
|
|
758
|
+
receipt_path: receipt.receipt_path || null,
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
|
|
660
762
|
function gitWorktreeSnapshot(root = process.cwd()) {
|
|
661
763
|
const inside = spawnSync('git', ['rev-parse', '--is-inside-work-tree'], {
|
|
662
764
|
cwd: root,
|
|
@@ -689,7 +791,77 @@ function gitWorktreeSnapshot(root = process.cwd()) {
|
|
|
689
791
|
};
|
|
690
792
|
}
|
|
691
793
|
|
|
692
|
-
|
|
794
|
+
// Porcelain v1 entries are "XY path" or "XY old -> new"; baselines compare by
|
|
795
|
+
// post-rename path so a status-letter change alone never counts as new dirt.
|
|
796
|
+
function porcelainEntryPath(line) {
|
|
797
|
+
const trimmed = String(line || '').slice(3);
|
|
798
|
+
const arrow = trimmed.indexOf(' -> ');
|
|
799
|
+
return arrow >= 0 ? trimmed.slice(arrow + 4) : trimmed;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
const LOOP_EXHAUST_PREFIXES = ['.atris/', 'atris/runs/', 'atris/status/'];
|
|
803
|
+
|
|
804
|
+
function isLoopExhaustPath(entryPath) {
|
|
805
|
+
return LOOP_EXHAUST_PREFIXES.some((prefix) => String(entryPath).startsWith(prefix));
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
function missionBaselinePath(missionId, root = process.cwd()) {
|
|
809
|
+
return path.join(root, '.atris', 'state', 'mission-baselines', `${missionId}.json`);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// Write-once sidecar capturing the dirt the mission inherited. Receipts subtract
|
|
813
|
+
// these paths so pre-existing workspace noise stops flagging unverified_dirty.
|
|
814
|
+
// Stored outside missions.jsonl because that log re-appends the full mission
|
|
815
|
+
// record on every save. Captured after start bookkeeping so the mission's own
|
|
816
|
+
// state writes land inside the baseline, not in new_since_baseline.
|
|
817
|
+
function captureMissionWorktreeBaseline(mission, root = process.cwd()) {
|
|
818
|
+
const snapshot = gitWorktreeSnapshot(root);
|
|
819
|
+
if (!snapshot.available) return null;
|
|
820
|
+
const baselineFile = missionBaselinePath(mission.id, root);
|
|
821
|
+
const paths = new Set((snapshot.entries || []).map(porcelainEntryPath));
|
|
822
|
+
// The sidecar itself and the per-tick lock are mission bookkeeping; without
|
|
823
|
+
// these the mission would flag its own state files as unverified dirt.
|
|
824
|
+
paths.add(path.relative(root, baselineFile));
|
|
825
|
+
paths.add(path.relative(root, path.join(root, '.atris', 'state', `mission-${mission.id}.lock`)));
|
|
826
|
+
const baseline = {
|
|
827
|
+
schema: 'atris.mission_worktree_baseline.v1',
|
|
828
|
+
mission_id: mission.id,
|
|
829
|
+
captured_at: stampIso(),
|
|
830
|
+
dirty_count: snapshot.dirty_count,
|
|
831
|
+
dirty_hash: snapshot.dirty_hash,
|
|
832
|
+
paths: Array.from(paths).sort(),
|
|
833
|
+
};
|
|
834
|
+
fs.mkdirSync(path.dirname(baselineFile), { recursive: true });
|
|
835
|
+
fs.writeFileSync(baselineFile, JSON.stringify(baseline, null, 2) + '\n', 'utf8');
|
|
836
|
+
return baseline;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function loadMissionWorktreeBaseline(missionId, root = process.cwd()) {
|
|
840
|
+
try {
|
|
841
|
+
const baseline = JSON.parse(fs.readFileSync(missionBaselinePath(missionId, root), 'utf8'));
|
|
842
|
+
return Array.isArray(baseline?.paths) ? baseline : null;
|
|
843
|
+
} catch {
|
|
844
|
+
return null;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// Closed missions no longer tick, so the sidecar is dead weight; prune it and
|
|
849
|
+
// fold a compact audit summary into the mission record (full path lists stay
|
|
850
|
+
// out of missions.jsonl, which re-appends the whole record on every save).
|
|
851
|
+
// Paused missions keep their sidecar — resume ticks still subtract it.
|
|
852
|
+
function pruneMissionWorktreeBaseline(mission, root = process.cwd()) {
|
|
853
|
+
const baseline = loadMissionWorktreeBaseline(mission.id, root);
|
|
854
|
+
try { fs.rmSync(missionBaselinePath(mission.id, root), { force: true }); } catch {}
|
|
855
|
+
if (!baseline) return null;
|
|
856
|
+
return {
|
|
857
|
+
captured_at: baseline.captured_at,
|
|
858
|
+
dirty_count: baseline.dirty_count,
|
|
859
|
+
dirty_hash: baseline.dirty_hash,
|
|
860
|
+
path_count: baseline.paths.length,
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function worktreeReceipt(before, after, { verifier = '', baseline = null } = {}) {
|
|
693
865
|
if (!before?.available || !after?.available) {
|
|
694
866
|
return {
|
|
695
867
|
available: false,
|
|
@@ -703,12 +875,27 @@ function worktreeReceipt(before, after, { verifier = '' } = {}) {
|
|
|
703
875
|
const clearedDirty = (before.entries || []).filter((entry) => !afterSet.has(entry));
|
|
704
876
|
const changed = before.dirty_hash !== after.dirty_hash;
|
|
705
877
|
const hasVerifier = !!String(verifier || '').trim();
|
|
878
|
+
// Baseline = dirt the mission inherited (mission-start sidecar when present,
|
|
879
|
+
// tick-start snapshot for legacy missions). Only paths dirtied beyond that
|
|
880
|
+
// baseline count toward the unverified signal. Loop exhaust the mission
|
|
881
|
+
// writes about itself (state plane, receipts, rendered status) is not work
|
|
882
|
+
// product, so it never counts — otherwise every multi-tick mission in a repo
|
|
883
|
+
// that doesn't gitignore those dirs would flag its own bookkeeping.
|
|
884
|
+
const baselinePaths = baseline
|
|
885
|
+
? new Set(baseline.paths)
|
|
886
|
+
: new Set((before.entries || []).map(porcelainEntryPath));
|
|
887
|
+
const newSinceBaseline = Array.from(new Set((after.entries || []).map(porcelainEntryPath)))
|
|
888
|
+
.filter((entryPath) => !baselinePaths.has(entryPath) && !isLoopExhaustPath(entryPath));
|
|
706
889
|
return {
|
|
707
890
|
available: true,
|
|
708
891
|
before_dirty_count: before.dirty_count,
|
|
709
892
|
after_dirty_count: after.dirty_count,
|
|
710
893
|
changed,
|
|
711
|
-
|
|
894
|
+
baseline_source: baseline ? 'mission_start' : 'tick_start',
|
|
895
|
+
baseline_dirty_count: baseline ? baseline.dirty_count : before.dirty_count,
|
|
896
|
+
new_since_baseline_count: newSinceBaseline.length,
|
|
897
|
+
new_since_baseline_sample: newSinceBaseline.slice(0, 25),
|
|
898
|
+
unverified_dirty: !hasVerifier && newSinceBaseline.length > 0,
|
|
712
899
|
unverified_change: !hasVerifier && changed,
|
|
713
900
|
new_dirty_count: newDirty.length,
|
|
714
901
|
cleared_dirty_count: clearedDirty.length,
|
|
@@ -739,7 +926,8 @@ function runnerUsesCallerSession(runner) {
|
|
|
739
926
|
}
|
|
740
927
|
|
|
741
928
|
function nextCandidateTickAction(mission) {
|
|
742
|
-
|
|
929
|
+
const completeFlag = mission.always_on ? '' : ' --complete-on-pass';
|
|
930
|
+
return `next move: run atris mission run ${mission.id}${completeFlag}`;
|
|
743
931
|
}
|
|
744
932
|
|
|
745
933
|
function missionVerifierPassed(mission) {
|
|
@@ -803,9 +991,17 @@ function selectDueMission(root = process.cwd(), now = new Date()) {
|
|
|
803
991
|
return candidates[0] || null;
|
|
804
992
|
}
|
|
805
993
|
|
|
994
|
+
function missionSelectableForCodexGoal(mission, now = new Date()) {
|
|
995
|
+
if (!missionIsRunnable(mission)) return false;
|
|
996
|
+
if (mission.always_on && missionVerifierPassed(mission) && !missionDueAt(mission, now)) {
|
|
997
|
+
return parseCadenceSeconds(mission.cadence) > 0;
|
|
998
|
+
}
|
|
999
|
+
return true;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
806
1002
|
function selectCodexGoalMission(root = process.cwd(), now = new Date()) {
|
|
807
1003
|
const candidates = listMissions(root)
|
|
808
|
-
.filter((mission) =>
|
|
1004
|
+
.filter((mission) => missionSelectableForCodexGoal(mission, now));
|
|
809
1005
|
|
|
810
1006
|
candidates.sort((a, b) => {
|
|
811
1007
|
const aCaller = runnerUsesCallerSession(a.runner) ? 1 : 0;
|
|
@@ -837,7 +1033,8 @@ function codexGoalNextCommand(mission) {
|
|
|
837
1033
|
if (xpAction) return xpAction.replace(/^queue AgentXP review: /, '');
|
|
838
1034
|
}
|
|
839
1035
|
if (mission.verifier && missionDueAt(mission)) {
|
|
840
|
-
|
|
1036
|
+
const completeFlag = mission.always_on ? '' : ' --complete-on-pass';
|
|
1037
|
+
return `atris mission run --due --max-ticks 1${completeFlag}`;
|
|
841
1038
|
}
|
|
842
1039
|
if (mission.verifier) {
|
|
843
1040
|
return `atris mission tick ${mission.id} --verify --summary "<what changed>"`;
|
|
@@ -1243,11 +1440,12 @@ async function runMission(args) {
|
|
|
1243
1440
|
const skipClaude = hasFlag(args, '--no-claude');
|
|
1244
1441
|
const verifyEach = !hasFlag(args, '--no-verify');
|
|
1245
1442
|
const completeOnPass = hasFlag(args, '--complete-on-pass');
|
|
1443
|
+
const skipDrain = hasFlag(args, '--no-drain');
|
|
1246
1444
|
const maxTicksFlag = readFlag(args, '--max-ticks', '');
|
|
1247
1445
|
const maxTicks = Math.max(1, Number(maxTicksFlag) || MISSION_RUN_DEFAULTS.maxTicks);
|
|
1248
1446
|
const maxWallSeconds = Math.max(60, Number(readFlag(args, '--max-wall', '')) || MISSION_RUN_DEFAULTS.maxWallSeconds);
|
|
1249
1447
|
const cadenceOverride = readFlag(args, '--cadence', '');
|
|
1250
|
-
const ref = stripKnownFlags(args, ['--max-ticks', '--max-wall', '--cadence'], ['--json', '--due', '--no-claude', '--no-verify', '--complete-on-pass'])[0] || '';
|
|
1448
|
+
const ref = stripKnownFlags(args, ['--max-ticks', '--max-wall', '--cadence'], ['--json', '--due', '--no-claude', '--no-verify', '--complete-on-pass', '--no-drain'])[0] || '';
|
|
1251
1449
|
|
|
1252
1450
|
let mission = dueMode && !ref ? selectDueMission() : resolveMission(ref);
|
|
1253
1451
|
if (!mission && dueMode && !ref) {
|
|
@@ -1334,6 +1532,7 @@ async function runMission(args) {
|
|
|
1334
1532
|
started_at: stampIso(),
|
|
1335
1533
|
};
|
|
1336
1534
|
const runWorktreeBefore = gitWorktreeSnapshot(cwd);
|
|
1535
|
+
const runWorktreeBaseline = loadMissionWorktreeBaseline(mission.id, cwd);
|
|
1337
1536
|
const cadence = cadenceOverride || mission.cadence || 'manual';
|
|
1338
1537
|
let cadenceSeconds = parseCadenceSeconds(cadence);
|
|
1339
1538
|
// cadence=manual|once: exactly 1 tick unless user explicitly raised --max-ticks
|
|
@@ -1450,7 +1649,16 @@ async function runMission(args) {
|
|
|
1450
1649
|
verifierResult = runVerifier(frozen.verifier);
|
|
1451
1650
|
result.verifier_passed = verifierResult.passed;
|
|
1452
1651
|
}
|
|
1453
|
-
|
|
1652
|
+
|
|
1653
|
+
// Review-lane drain: always-on loops sweep the agent-safe review actions
|
|
1654
|
+
// each tick so proof-backed work reaches certified on cadence with zero
|
|
1655
|
+
// human turns. Human accept stays the only path to Done; --no-drain opts out.
|
|
1656
|
+
if (mission.always_on && result.status === 'ran') {
|
|
1657
|
+
result.review_lane = skipDrain
|
|
1658
|
+
? { skipped: true, reason: 'no-drain-flag' }
|
|
1659
|
+
: runReviewLaneDrain(cwd);
|
|
1660
|
+
}
|
|
1661
|
+
const tickWorktree = worktreeReceipt(tickWorktreeBefore, gitWorktreeSnapshot(cwd), { verifier: frozen.verifier, baseline: runWorktreeBaseline });
|
|
1454
1662
|
|
|
1455
1663
|
// Persist tick to mission state + write structured receipt
|
|
1456
1664
|
const finishedAt = stampIso();
|
|
@@ -1553,7 +1761,7 @@ async function runMission(args) {
|
|
|
1553
1761
|
}, cwd, 'mission_run_paused', { reason: pauseReason }).mission;
|
|
1554
1762
|
}
|
|
1555
1763
|
|
|
1556
|
-
const summaryWorktree = worktreeReceipt(runWorktreeBefore, gitWorktreeSnapshot(cwd), { verifier: frozen.verifier });
|
|
1764
|
+
const summaryWorktree = worktreeReceipt(runWorktreeBefore, gitWorktreeSnapshot(cwd), { verifier: frozen.verifier, baseline: runWorktreeBaseline });
|
|
1557
1765
|
const finalReceipt = writeReceipt(mission, {
|
|
1558
1766
|
kind: 'mission_run_summary',
|
|
1559
1767
|
frozen,
|
|
@@ -1628,12 +1836,13 @@ function tickMission(args) {
|
|
|
1628
1836
|
const lastTickIndex = Number(mission.last_tick_index || 0);
|
|
1629
1837
|
const tickIdx = lastTickIndex + 1;
|
|
1630
1838
|
const tickWorktreeBefore = gitWorktreeSnapshot(cwd);
|
|
1839
|
+
const worktreeBaseline = loadMissionWorktreeBaseline(mission.id, cwd);
|
|
1631
1840
|
|
|
1632
1841
|
let verifierResult = null;
|
|
1633
1842
|
if (verify && mission.verifier) {
|
|
1634
1843
|
verifierResult = runVerifier(mission.verifier);
|
|
1635
1844
|
}
|
|
1636
|
-
const tickWorktree = worktreeReceipt(tickWorktreeBefore, gitWorktreeSnapshot(cwd), { verifier: mission.verifier });
|
|
1845
|
+
const tickWorktree = worktreeReceipt(tickWorktreeBefore, gitWorktreeSnapshot(cwd), { verifier: mission.verifier, baseline: worktreeBaseline });
|
|
1637
1846
|
|
|
1638
1847
|
const tickRecord = {
|
|
1639
1848
|
status: 'ran',
|
|
@@ -1710,10 +1919,49 @@ function tickMission(args) {
|
|
|
1710
1919
|
}
|
|
1711
1920
|
}
|
|
1712
1921
|
|
|
1922
|
+
// Proof receipts are JSON files written by writeReceipt(); anything else
|
|
1923
|
+
// (free text, command strings, missing paths) reads as null and falls back
|
|
1924
|
+
// to durable mission state.
|
|
1925
|
+
function readReceiptProof(proof, root = process.cwd()) {
|
|
1926
|
+
try {
|
|
1927
|
+
const parsed = JSON.parse(fs.readFileSync(path.resolve(root, String(proof || '')), 'utf8'));
|
|
1928
|
+
return parsed?.schema === 'atris.mission_receipt.v1' ? parsed : null;
|
|
1929
|
+
} catch {
|
|
1930
|
+
return null;
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
function receiptShowsPass(receipt) {
|
|
1935
|
+
const result = receipt?.result;
|
|
1936
|
+
return result?.passed === true
|
|
1937
|
+
|| result?.verifier_result?.passed === true
|
|
1938
|
+
|| result?.tick?.verifier_passed === true;
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
// Terminal gate: a verifier mission may only complete on real evidence — a
|
|
1942
|
+
// passing receipt belonging to this mission, or durable state showing the
|
|
1943
|
+
// verifier passed. Mirrors the task plane's proof-only accept guard so the
|
|
1944
|
+
// final transition consumes the receipts instead of trusting free text.
|
|
1945
|
+
function missionCompletionGate(mission, proof, root = process.cwd()) {
|
|
1946
|
+
if (!String(mission.verifier || '').trim()) return { ok: true, source: 'no_verifier' };
|
|
1947
|
+
const receipt = readReceiptProof(proof, root);
|
|
1948
|
+
if (receipt) {
|
|
1949
|
+
if (receipt.mission_id !== mission.id) {
|
|
1950
|
+
return { ok: false, source: 'receipt', reason: `proof receipt belongs to mission ${receipt.mission_id}, not ${mission.id}` };
|
|
1951
|
+
}
|
|
1952
|
+
return receiptShowsPass(receipt)
|
|
1953
|
+
? { ok: true, source: 'receipt', receipt_path: String(proof) }
|
|
1954
|
+
: { ok: false, source: 'receipt', reason: 'proof receipt does not show a passing verifier' };
|
|
1955
|
+
}
|
|
1956
|
+
if (mission.verifier_result?.passed === true) return { ok: true, source: 'mission_state' };
|
|
1957
|
+
return { ok: false, source: 'mission_state', reason: 'verifier has not passed for this mission and proof is not a passing receipt' };
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1713
1960
|
function completeMission(args) {
|
|
1714
1961
|
const asJson = wantsJson(args);
|
|
1962
|
+
const force = hasFlag(args, '--force');
|
|
1715
1963
|
const proof = readFlag(args, '--proof', '');
|
|
1716
|
-
const ref = stripKnownFlags(args, ['--proof'], ['--json'])[0] || '';
|
|
1964
|
+
const ref = stripKnownFlags(args, ['--proof'], ['--json', '--force'])[0] || '';
|
|
1717
1965
|
if (!ref || !proof) {
|
|
1718
1966
|
exitMissionError('Usage: atris mission complete <id> --proof "..."', 1, asJson);
|
|
1719
1967
|
}
|
|
@@ -1721,14 +1969,21 @@ function completeMission(args) {
|
|
|
1721
1969
|
if (!mission) {
|
|
1722
1970
|
exitMissionError(`Mission "${ref}" not found.`, 1, asJson);
|
|
1723
1971
|
}
|
|
1972
|
+
const gate = missionCompletionGate(mission, proof, process.cwd());
|
|
1973
|
+
if (!gate.ok && !force) {
|
|
1974
|
+
exitMissionError(`[mission complete] ${gate.reason}. Run: atris mission tick ${mission.id} --verify (or override as operator with --force)`, 2, asJson);
|
|
1975
|
+
}
|
|
1976
|
+
const baselineSummary = pruneMissionWorktreeBaseline(mission, process.cwd());
|
|
1724
1977
|
const next = {
|
|
1725
1978
|
...mission,
|
|
1726
1979
|
status: 'complete',
|
|
1727
1980
|
completed_at: stampIso(),
|
|
1728
1981
|
proof,
|
|
1982
|
+
completion_gate: { ...gate, forced: force && !gate.ok },
|
|
1983
|
+
worktree_baseline: baselineSummary || mission.worktree_baseline || null,
|
|
1729
1984
|
next_action: 'mission complete',
|
|
1730
1985
|
};
|
|
1731
|
-
const { mission: saved } = saveMission(next, process.cwd(), 'mission_completed', { proof });
|
|
1986
|
+
const { mission: saved } = saveMission(next, process.cwd(), 'mission_completed', { proof, completion_gate: next.completion_gate });
|
|
1732
1987
|
const logPath = appendMemberLog(saved.owner, 'Mission completed', { mission: saved.objective, proof });
|
|
1733
1988
|
const codexGoalState = refreshCodexGoalController(process.cwd());
|
|
1734
1989
|
const xpNextCommand = missionXpReadyAction(saved, proof);
|
|
@@ -1752,19 +2007,37 @@ function stopMission(args) {
|
|
|
1752
2007
|
exitMissionError(`Mission "${ref}" not found.`, 1, asJson);
|
|
1753
2008
|
}
|
|
1754
2009
|
const status = pause ? 'paused' : 'stopped';
|
|
2010
|
+
// Full stops abandon work, so leave evidence: snapshot the worktree against
|
|
2011
|
+
// the mission baseline (what did this mission leave dirty?) before pruning it.
|
|
2012
|
+
let receiptPath = null;
|
|
2013
|
+
if (!pause) {
|
|
2014
|
+
const snapshot = gitWorktreeSnapshot(process.cwd());
|
|
2015
|
+
const worktree = worktreeReceipt(snapshot, snapshot, {
|
|
2016
|
+
verifier: mission.verifier,
|
|
2017
|
+
baseline: loadMissionWorktreeBaseline(mission.id, process.cwd()),
|
|
2018
|
+
});
|
|
2019
|
+
receiptPath = writeReceipt(mission, { kind: 'mission_stop', reason, worktree });
|
|
2020
|
+
}
|
|
2021
|
+
const baselineSummary = pause ? null : pruneMissionWorktreeBaseline(mission, process.cwd());
|
|
1755
2022
|
const next = {
|
|
1756
2023
|
...mission,
|
|
1757
2024
|
status,
|
|
1758
2025
|
stopped_at: status === 'stopped' ? stampIso() : mission.stopped_at || null,
|
|
1759
2026
|
paused_at: status === 'paused' ? stampIso() : mission.paused_at || null,
|
|
1760
2027
|
stop_reason: reason,
|
|
2028
|
+
receipt_path: receiptPath || mission.receipt_path || null,
|
|
2029
|
+
worktree_baseline: baselineSummary || mission.worktree_baseline || null,
|
|
1761
2030
|
next_action: status === 'paused' ? `resume with: atris mission tick ${mission.id}` : 'mission stopped',
|
|
1762
2031
|
};
|
|
1763
|
-
const { mission: saved } = saveMission(next, process.cwd(), pause ? 'mission_paused' : 'mission_stopped', { reason });
|
|
2032
|
+
const { mission: saved } = saveMission(next, process.cwd(), pause ? 'mission_paused' : 'mission_stopped', { reason, receipt_path: receiptPath });
|
|
1764
2033
|
const logPath = appendMemberLog(saved.owner, pause ? 'Mission paused' : 'Mission stopped', { mission: saved.objective, reason });
|
|
1765
2034
|
printJsonOrText(
|
|
1766
|
-
{ ok: true, action: pause ? 'mission_paused' : 'mission_stopped', mission: saved, log_path: logPath },
|
|
1767
|
-
[
|
|
2035
|
+
{ ok: true, action: pause ? 'mission_paused' : 'mission_stopped', mission: saved, receipt_path: receiptPath, log_path: logPath },
|
|
2036
|
+
[
|
|
2037
|
+
`${pause ? 'Paused' : 'Stopped'} mission: ${saved.objective}`,
|
|
2038
|
+
`Reason: ${reason}`,
|
|
2039
|
+
...(receiptPath ? [`Receipt: ${receiptPath}`] : []),
|
|
2040
|
+
],
|
|
1768
2041
|
asJson,
|
|
1769
2042
|
);
|
|
1770
2043
|
}
|
|
@@ -1866,13 +2139,15 @@ function help() {
|
|
|
1866
2139
|
console.log(`
|
|
1867
2140
|
atris mission - durable goal + loop + owner + proof state
|
|
1868
2141
|
|
|
1869
|
-
atris mission start "<objective>" --owner <member> [--verify "..."] [--always-on] [--xp-task]
|
|
1870
|
-
atris mission status [id] [--status <state>] [--limit <n>] [--json]
|
|
2142
|
+
atris mission start "<objective>" --owner <member> [--verify "..."] [--always-on] [--xp-task] [--worktree]
|
|
2143
|
+
atris mission status [id] [--status <state>] [--limit <n>] [--local] [--json]
|
|
2144
|
+
(rolls up sibling git-worktree missions; --local scopes to this checkout)
|
|
1871
2145
|
atris mission goal [--heartbeat] [--json]
|
|
1872
2146
|
atris mission goal-loop [--max-wall 28800] [--max-iterations 32] [--no-claude] [--json]
|
|
1873
2147
|
atris mission tick <id> [--verify] [--complete-on-pass] [--summary "..."] [--json]
|
|
1874
2148
|
atris mission run <id|--due> [--max-ticks 4] [--max-wall 3600] [--cadence "15m"]
|
|
1875
|
-
[--no-claude] [--no-verify] [--complete-on-pass] [--json]
|
|
2149
|
+
[--no-claude] [--no-verify] [--complete-on-pass] [--no-drain] [--json]
|
|
2150
|
+
(always-on runs drain the review lane each tick; --no-drain skips)
|
|
1876
2151
|
atris mission complete <id> --proof "..."
|
|
1877
2152
|
atris mission stop <id> [--pause] [--reason "..."]
|
|
1878
2153
|
|
|
@@ -1886,7 +2161,7 @@ Autonomy recipe:
|
|
|
1886
2161
|
4. Do one bounded step, then record it:
|
|
1887
2162
|
atris mission tick <id> --verify --summary "what changed"
|
|
1888
2163
|
5. Close or continue from the receipt:
|
|
1889
|
-
atris task
|
|
2164
|
+
atris task current-step --goal-id <mission_id> --as <owner> --proof "<receipt_path>" --json (if --xp-task)
|
|
1890
2165
|
atris task accept <xp_task_ref> --reward <n> (human accept mints AgentXP)
|
|
1891
2166
|
atris mission complete <id> --proof "<receipt_path>"
|
|
1892
2167
|
repeat status -> step -> tick for current-agent work
|