atris 3.68.2 → 3.68.4
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/atris/skills/engines/SKILL.md +1 -1
- package/atris/team/customer-lead/MEMBER.md +1 -1
- package/atris/team/executor/MEMBER.md +19 -18
- package/atris/team/executor/skills/isolated-slice-builder/SKILL.md +21 -0
- package/atris/team/navigator/MEMBER.md +3 -1
- package/atris/team/navigator/skills/loophole-proof-planner/SKILL.md +20 -0
- package/atris/team/researcher/MEMBER.md +2 -1
- package/atris/team/researcher/skills/grounded-evidence-brief/SKILL.md +19 -0
- package/atris/team/validator/MEMBER.md +2 -7
- package/atris/team/validator/skills/cross-model-referee/SKILL.md +20 -0
- package/commands/autoland.js +2 -1
- package/commands/engine.js +2 -2
- package/commands/goal.js +61 -2
- package/commands/land.js +34 -7
- package/commands/member.js +1547 -41
- package/commands/mission.js +513 -18
- package/commands/task.js +26 -3
- package/commands/worktree.js +144 -34
- package/lib/auto-accept-certified.js +7 -6
- package/lib/codex-flight.js +6 -12
- package/lib/conductor-artifacts.js +3 -1
- package/lib/default-runner.js +42 -7
- package/lib/engine-ask.js +8 -1
- package/lib/engine-registry.js +17 -0
- package/lib/fleet.js +163 -88
- package/lib/member-alive.js +1 -1
- package/lib/member-approval.js +153 -0
- package/lib/mission-task-gate.js +170 -0
- package/lib/one-lap-validator.js +2 -1
- package/lib/roster-runs.js +28 -0
- package/lib/runner-command.js +32 -8
- package/lib/task-db.js +25 -1
- package/package.json +5 -1
- package/scripts/det/ytnotes +49 -10
package/commands/mission.js
CHANGED
|
@@ -103,6 +103,8 @@ const {
|
|
|
103
103
|
normalizeHumanAsks,
|
|
104
104
|
openHumanAsks,
|
|
105
105
|
} = require('../lib/mission-human-asks');
|
|
106
|
+
const { missionLinkedTaskGate } = require('../lib/mission-task-gate');
|
|
107
|
+
const { memberApprovalAsks, runBlockingAsks } = require('../lib/member-approval');
|
|
106
108
|
const {
|
|
107
109
|
inspectMissionProtectedDiff,
|
|
108
110
|
prepareMissionGitGuard,
|
|
@@ -730,6 +732,7 @@ const MISSION_RUN_VALUE_FLAGS = [
|
|
|
730
732
|
'--stop',
|
|
731
733
|
'--model',
|
|
732
734
|
'--engine',
|
|
735
|
+
'--referee-engine',
|
|
733
736
|
'--budget',
|
|
734
737
|
'--repo',
|
|
735
738
|
'--native-goal-status',
|
|
@@ -750,6 +753,8 @@ const MISSION_RUN_BOOLEAN_FLAGS = [
|
|
|
750
753
|
'--land',
|
|
751
754
|
'--no-claude',
|
|
752
755
|
'--no-verify',
|
|
756
|
+
'--referee',
|
|
757
|
+
'--no-referee',
|
|
753
758
|
'--complete-on-pass',
|
|
754
759
|
'--no-drain',
|
|
755
760
|
'--create-next',
|
|
@@ -1621,6 +1626,28 @@ function appendJsonLine(file, payload) {
|
|
|
1621
1626
|
fs.appendFileSync(file, JSON.stringify(payload) + '\n', 'utf8');
|
|
1622
1627
|
}
|
|
1623
1628
|
|
|
1629
|
+
function resolveMemberWorkspaceRoot(root = process.cwd()) {
|
|
1630
|
+
const resolved = path.resolve(root || process.cwd());
|
|
1631
|
+
const agentWorktreesMarker = `${path.sep}.agent-worktrees${path.sep}`;
|
|
1632
|
+
const agentIdx = resolved.indexOf(agentWorktreesMarker);
|
|
1633
|
+
if (agentIdx > 0) {
|
|
1634
|
+
const parentDir = resolved.slice(0, agentIdx);
|
|
1635
|
+
const after = resolved.slice(agentIdx + agentWorktreesMarker.length);
|
|
1636
|
+
const repoName = after.split(path.sep)[0];
|
|
1637
|
+
if (repoName) {
|
|
1638
|
+
const primaryRoot = path.join(parentDir, repoName);
|
|
1639
|
+
if (fs.existsSync(path.join(primaryRoot, 'atris'))) return primaryRoot;
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
const marker = `${path.sep}.atris${path.sep}worktrees${path.sep}`;
|
|
1643
|
+
const idx = resolved.indexOf(marker);
|
|
1644
|
+
if (idx > 0) {
|
|
1645
|
+
const parentRoot = resolved.slice(0, idx);
|
|
1646
|
+
if (fs.existsSync(path.join(parentRoot, 'atris'))) return parentRoot;
|
|
1647
|
+
}
|
|
1648
|
+
return resolved;
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1624
1651
|
function appendEvent(type, mission, payload = {}, root = process.cwd()) {
|
|
1625
1652
|
const paths = statePaths(root);
|
|
1626
1653
|
const event = {
|
|
@@ -1632,6 +1659,12 @@ function appendEvent(type, mission, payload = {}, root = process.cwd()) {
|
|
|
1632
1659
|
payload,
|
|
1633
1660
|
};
|
|
1634
1661
|
appendJsonLine(paths.eventsJsonl, event);
|
|
1662
|
+
const parentRoot = resolveMemberWorkspaceRoot(root);
|
|
1663
|
+
if (type === 'mission_tick' && parentRoot !== path.resolve(root || process.cwd())) {
|
|
1664
|
+
try {
|
|
1665
|
+
appendJsonLine(statePaths(parentRoot).eventsJsonl, event);
|
|
1666
|
+
} catch {}
|
|
1667
|
+
}
|
|
1635
1668
|
return event;
|
|
1636
1669
|
}
|
|
1637
1670
|
|
|
@@ -1662,9 +1695,15 @@ function saveMission(mission, root = process.cwd(), eventType = 'mission_updated
|
|
|
1662
1695
|
|
|
1663
1696
|
function memberDir(owner, root = process.cwd()) {
|
|
1664
1697
|
if (!owner || !/^[a-zA-Z0-9._-]+$/.test(owner)) return null;
|
|
1665
|
-
const
|
|
1666
|
-
|
|
1667
|
-
|
|
1698
|
+
const parentRoot = resolveMemberWorkspaceRoot(root);
|
|
1699
|
+
const candidates = parentRoot !== path.resolve(root || process.cwd())
|
|
1700
|
+
? [parentRoot, root]
|
|
1701
|
+
: [root];
|
|
1702
|
+
for (const base of candidates) {
|
|
1703
|
+
const dir = path.join(base, 'atris', 'team', owner);
|
|
1704
|
+
if (fs.existsSync(path.join(dir, 'MEMBER.md'))) return dir;
|
|
1705
|
+
}
|
|
1706
|
+
return null;
|
|
1668
1707
|
}
|
|
1669
1708
|
|
|
1670
1709
|
function appendMemberLog(owner, title, fields = {}, root = process.cwd()) {
|
|
@@ -1686,6 +1725,158 @@ function appendMemberLog(owner, title, fields = {}, root = process.cwd()) {
|
|
|
1686
1725
|
return logPath;
|
|
1687
1726
|
}
|
|
1688
1727
|
|
|
1728
|
+
function crystallizeMemberSkillLesson(owner, entry = {}, root = process.cwd()) {
|
|
1729
|
+
const dir = memberDir(owner, root);
|
|
1730
|
+
if (!dir) return null;
|
|
1731
|
+
const summary = String(entry.summary || entry.objective || '').replace(/\s+/g, ' ').trim();
|
|
1732
|
+
if (!summary) return null;
|
|
1733
|
+
const skillDir = path.join(dir, 'skills', 'auto-learned');
|
|
1734
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
1735
|
+
const skillPath = path.join(skillDir, 'SKILL.md');
|
|
1736
|
+
const existingBullets = [];
|
|
1737
|
+
if (fs.existsSync(skillPath)) {
|
|
1738
|
+
try {
|
|
1739
|
+
const raw = fs.readFileSync(skillPath, 'utf8');
|
|
1740
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
1741
|
+
if (line.trim().startsWith('- **')) existingBullets.push(line.trim());
|
|
1742
|
+
}
|
|
1743
|
+
} catch {}
|
|
1744
|
+
}
|
|
1745
|
+
const verifierText = String(entry.verifier || '').replace(/\s+/g, ' ').trim();
|
|
1746
|
+
const nextPushText = String(entry.next_push || '').replace(/\s+/g, ' ').trim();
|
|
1747
|
+
const layerLabel = String(entry.layer || 'capabilities').trim();
|
|
1748
|
+
const parts = [`- **${todayName()} (${layerLabel})**: ${summary.slice(0, 140)}`];
|
|
1749
|
+
if (verifierText) parts.push(`Verify: \`${verifierText.slice(0, 80)}\``);
|
|
1750
|
+
if (nextPushText) parts.push(`Next push: ${nextPushText.slice(0, 120)}`);
|
|
1751
|
+
const newBullet = parts.join(' -> ');
|
|
1752
|
+
const deduped = [
|
|
1753
|
+
newBullet,
|
|
1754
|
+
...existingBullets.filter((b) => !b.includes(summary.slice(0, 80))),
|
|
1755
|
+
].slice(0, 5);
|
|
1756
|
+
|
|
1757
|
+
const content = [
|
|
1758
|
+
'---',
|
|
1759
|
+
'name: auto-learned',
|
|
1760
|
+
`description: Verified patterns crystallized from passing runs for ${owner}. Bounded to 5 newest patterns so context never bloats.`,
|
|
1761
|
+
'---',
|
|
1762
|
+
'',
|
|
1763
|
+
'# Auto-Learned Verified Patterns',
|
|
1764
|
+
'',
|
|
1765
|
+
...deduped,
|
|
1766
|
+
'',
|
|
1767
|
+
].join('\n');
|
|
1768
|
+
fs.writeFileSync(skillPath, content, 'utf8');
|
|
1769
|
+
return skillPath;
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
function autoQueueMemberNextPush(owner, nextPush, parentObjective = '', root = process.cwd(), { promote = false } = {}) {
|
|
1773
|
+
const cleanPush = String(nextPush || '').replace(/\s+/g, ' ').trim();
|
|
1774
|
+
if (!cleanPush) return null;
|
|
1775
|
+
const dir = memberDir(owner, root);
|
|
1776
|
+
if (!dir) return null;
|
|
1777
|
+
const goalsPath = path.join(dir, 'goals.json');
|
|
1778
|
+
let store = { schema_version: 1, member: owner, updated_at: stampIso(), goals: [] };
|
|
1779
|
+
if (fs.existsSync(goalsPath)) {
|
|
1780
|
+
try {
|
|
1781
|
+
const parsed = JSON.parse(fs.readFileSync(goalsPath, 'utf8'));
|
|
1782
|
+
if (parsed && Array.isArray(parsed.goals)) store = parsed;
|
|
1783
|
+
} catch {}
|
|
1784
|
+
}
|
|
1785
|
+
const now = stampIso();
|
|
1786
|
+
const activeIdx = store.goals.findIndex((g) => g && g.status === 'active');
|
|
1787
|
+
if (activeIdx >= 0) {
|
|
1788
|
+
const prev = store.goals[activeIdx];
|
|
1789
|
+
store.goals[activeIdx] = {
|
|
1790
|
+
...prev,
|
|
1791
|
+
...(promote ? { previous_title: prev.title, title: cleanPush } : {}),
|
|
1792
|
+
next_push: cleanPush,
|
|
1793
|
+
updated_at: now,
|
|
1794
|
+
};
|
|
1795
|
+
} else {
|
|
1796
|
+
store.goals.push({
|
|
1797
|
+
id: `goal_${Date.now().toString(36)}`,
|
|
1798
|
+
member: owner,
|
|
1799
|
+
title: cleanPush,
|
|
1800
|
+
why: parentObjective ? `Compounds after verified win on: ${parentObjective}` : 'Self-proposed next optimization from verified run.',
|
|
1801
|
+
metric: 'verified_slices_shipped',
|
|
1802
|
+
target: '1',
|
|
1803
|
+
horizon: '7d',
|
|
1804
|
+
acceptance: ['Passes test check and cross-model referee'],
|
|
1805
|
+
next_push: cleanPush,
|
|
1806
|
+
status: 'active',
|
|
1807
|
+
created_at: now,
|
|
1808
|
+
updated_at: now,
|
|
1809
|
+
});
|
|
1810
|
+
}
|
|
1811
|
+
store.updated_at = now;
|
|
1812
|
+
fs.writeFileSync(goalsPath, `${JSON.stringify(store, null, 2)}\n`, 'utf8');
|
|
1813
|
+
return goalsPath;
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
const DEFAULT_MEMBER_BATON_RELAY = {
|
|
1817
|
+
researcher: 'navigator',
|
|
1818
|
+
'customer-lead': 'navigator',
|
|
1819
|
+
navigator: 'executor',
|
|
1820
|
+
architect: 'executor',
|
|
1821
|
+
executor: 'validator',
|
|
1822
|
+
'auto-improver': 'validator',
|
|
1823
|
+
};
|
|
1824
|
+
|
|
1825
|
+
function recordMemberHandoff(fromMember, toMember, sliceSummary, root = process.cwd(), extra = {}) {
|
|
1826
|
+
const from = String(fromMember || '').trim().toLowerCase();
|
|
1827
|
+
const to = String(toMember || DEFAULT_MEMBER_BATON_RELAY[from] || '').trim().toLowerCase();
|
|
1828
|
+
const cleanSummary = String(sliceSummary || '').replace(/\s+/g, ' ').trim();
|
|
1829
|
+
if (!from || !to || !cleanSummary) return null;
|
|
1830
|
+
const parentRoot = resolveMemberWorkspaceRoot(root);
|
|
1831
|
+
if (!memberDir(to, parentRoot)) return null;
|
|
1832
|
+
const now = stampIso();
|
|
1833
|
+
const handoff = {
|
|
1834
|
+
id: `handoff_${Date.now().toString(36)}`,
|
|
1835
|
+
from,
|
|
1836
|
+
to,
|
|
1837
|
+
summary: cleanSummary.slice(0, 180),
|
|
1838
|
+
status: extra.status || 'open',
|
|
1839
|
+
next_command: `atris member go ${to}`,
|
|
1840
|
+
created_at: now,
|
|
1841
|
+
};
|
|
1842
|
+
try {
|
|
1843
|
+
const handoffsFile = path.join(parentRoot, '.atris', 'state', 'member-handoffs.jsonl');
|
|
1844
|
+
fs.mkdirSync(path.dirname(handoffsFile), { recursive: true });
|
|
1845
|
+
fs.appendFileSync(handoffsFile, `${JSON.stringify(handoff)}\n`, 'utf8');
|
|
1846
|
+
autoQueueMemberNextPush(to, `${from} handoff: ${cleanSummary}`, `Baton pass from ${from}`, parentRoot);
|
|
1847
|
+
} catch {}
|
|
1848
|
+
return handoff;
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
function closeMemberHandoffsForReceiver(receiverMember, root = process.cwd()) {
|
|
1852
|
+
const receiver = String(receiverMember || '').trim().toLowerCase();
|
|
1853
|
+
if (!receiver) return 0;
|
|
1854
|
+
const parentRoot = resolveMemberWorkspaceRoot(root);
|
|
1855
|
+
const handoffsFile = path.join(parentRoot, '.atris', 'state', 'member-handoffs.jsonl');
|
|
1856
|
+
if (!fs.existsSync(handoffsFile)) return 0;
|
|
1857
|
+
try {
|
|
1858
|
+
const rawLines = fs.readFileSync(handoffsFile, 'utf8').split(/\r?\n/).filter((l) => l.trim());
|
|
1859
|
+
let closedCount = 0;
|
|
1860
|
+
const now = stampIso();
|
|
1861
|
+
const updatedLines = rawLines.map((line) => {
|
|
1862
|
+
try {
|
|
1863
|
+
const row = JSON.parse(line);
|
|
1864
|
+
if (row && String(row.to || '').toLowerCase() === receiver && (row.status || 'open') === 'open') {
|
|
1865
|
+
closedCount++;
|
|
1866
|
+
return JSON.stringify({ ...row, status: 'closed', closed_at: now });
|
|
1867
|
+
}
|
|
1868
|
+
} catch {}
|
|
1869
|
+
return line;
|
|
1870
|
+
});
|
|
1871
|
+
if (closedCount > 0) {
|
|
1872
|
+
fs.writeFileSync(handoffsFile, `${updatedLines.join('\n')}\n`, 'utf8');
|
|
1873
|
+
}
|
|
1874
|
+
return closedCount;
|
|
1875
|
+
} catch {
|
|
1876
|
+
return 0;
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1689
1880
|
function memberMissionFile(owner, root = process.cwd()) {
|
|
1690
1881
|
const dir = memberDir(owner, root);
|
|
1691
1882
|
if (!dir) return null;
|
|
@@ -2017,6 +2208,10 @@ function missionVerifierCheckedText(verifierResult, mission, guard = null) {
|
|
|
2017
2208
|
? 'Engine verify passed after running the changed surface on this computer.'
|
|
2018
2209
|
: 'VERIFY FAILED: the engine verify pass did not end with VERDICT: PASS.';
|
|
2019
2210
|
}
|
|
2211
|
+
// A passing command plus a failing referee must name the referee, not the command.
|
|
2212
|
+
if (verifierResult.referee?.passed === false && verifierResult.status === 0) {
|
|
2213
|
+
return missionVerifierCheckedText(verifierResult.referee, mission, guard);
|
|
2214
|
+
}
|
|
2020
2215
|
const command = verifierResult.command || mission.verifier || 'configured verifier';
|
|
2021
2216
|
if (!verifierResult.passed && verifierResult.timed_out) {
|
|
2022
2217
|
return `VERIFY TIMED OUT: the check ran past its ${verifierTimeoutSeconds(verifierResult)}s window (took ${verifierElapsedSeconds(verifierResult)}s); the code is not failing, the suite is slow. Rerun with more room.`;
|
|
@@ -2051,6 +2246,9 @@ function missionVerifierHighLevelTestText(verifierResult, mission, guard = null)
|
|
|
2051
2246
|
? 'The verify engine inspected the tick diff and reported real changed-surface command output.'
|
|
2052
2247
|
: 'The verify engine did not provide a passing changed-surface verdict.';
|
|
2053
2248
|
}
|
|
2249
|
+
if (verifierResult.referee?.passed === false && verifierResult.status === 0) {
|
|
2250
|
+
return missionVerifierHighLevelTestText(verifierResult.referee, mission, guard);
|
|
2251
|
+
}
|
|
2054
2252
|
const command = verifierResult.command || mission.verifier || 'configured verifier';
|
|
2055
2253
|
if (!verifierResult.passed && verifierResult.timed_out) {
|
|
2056
2254
|
return `Verifier command timed out after ${verifierElapsedSeconds(verifierResult)}s against a ${verifierTimeoutSeconds(verifierResult)}s window: ${command}. No pass/fail verdict was reached; the suite outran the window rather than the code failing.`;
|
|
@@ -2940,6 +3138,7 @@ function missionFromArgs(args) {
|
|
|
2940
3138
|
'--cadence',
|
|
2941
3139
|
'--loop',
|
|
2942
3140
|
'--runner',
|
|
3141
|
+
'--engine',
|
|
2943
3142
|
'--lane',
|
|
2944
3143
|
'--verify',
|
|
2945
3144
|
'--stop',
|
|
@@ -2968,7 +3167,10 @@ function missionFromArgs(args) {
|
|
|
2968
3167
|
const maxTicksOverride = readPositiveIntegerFlag(args, '--max-ticks', null, { json: wantsJson(args) });
|
|
2969
3168
|
const requestedOwner = readFlag(args, '--owner', process.env.ATRIS_AGENT_ID || 'mission-lead');
|
|
2970
3169
|
const cadence = readFlag(args, '--cadence', readFlag(args, '--loop', 'manual')) || 'manual';
|
|
2971
|
-
const
|
|
3170
|
+
const engineFlag = readFlag(args, '--engine', '');
|
|
3171
|
+
const runnerSelection = engineFlag
|
|
3172
|
+
? resolveMissionRunnerSelection(engineFlag, { asJson: wantsJson(args), engineOnly: true, label: 'engine' })
|
|
3173
|
+
: resolveMissionRunnerSelection(readFlag(args, '--runner', 'manual'), { asJson: wantsJson(args), label: 'runner' });
|
|
2972
3174
|
const runner = runnerSelection.runner;
|
|
2973
3175
|
const model = readFlag(args, '--model', '') || (String(runner).toLowerCase() === 'atris2' ? 'atris:fast' : '');
|
|
2974
3176
|
const lane = readFlag(args, '--lane', 'workspace');
|
|
@@ -2985,7 +3187,8 @@ function missionFromArgs(args) {
|
|
|
2985
3187
|
assertMissionVerifier(verifier, wantsJson(args));
|
|
2986
3188
|
const stopCondition = readFlag(args, '--stop', budgetStopCondition(budgetContract) || (verifier ? 'verifier passes and no human asks remain' : 'human marks complete with proof'));
|
|
2987
3189
|
const taskIds = readRepeatedFlag(args, '--task');
|
|
2988
|
-
|
|
3190
|
+
// MEMBER.md approval-required becomes an open human ask the owner must clear.
|
|
3191
|
+
const humanAsks = [...readRepeatedFlag(args, '--ask'), ...memberApprovalAsks(owner, process.cwd(), objective)];
|
|
2989
3192
|
const drive = missionDrive(readFlag(args, '--destination', ''));
|
|
2990
3193
|
const alwaysOn = hasFlag(args, '--always-on');
|
|
2991
3194
|
const xpTaskEnabled = hasFlag(args, '--xp-task') || hasFlag(args, '--agent-xp');
|
|
@@ -8612,6 +8815,101 @@ function consumeMissionPings(mission, cwd) {
|
|
|
8612
8815
|
return { mission: saved, pings: pending };
|
|
8613
8816
|
}
|
|
8614
8817
|
|
|
8818
|
+
function readBoundedContextFile(filePath, maxChars = 1600) {
|
|
8819
|
+
try {
|
|
8820
|
+
if (!fs.existsSync(filePath)) return '';
|
|
8821
|
+
// Frontmatter is machine config (skills, permissions); the prompt only
|
|
8822
|
+
// needs the body.
|
|
8823
|
+
const raw = fs.readFileSync(filePath, 'utf8')
|
|
8824
|
+
.replace(/^---\r?\n[\s\S]*?\r?\n---[ \t]*(?:\r?\n|$)/, '')
|
|
8825
|
+
.replace(/<!--[\s\S]*?-->/g, '')
|
|
8826
|
+
.trim();
|
|
8827
|
+
if (!raw) return '';
|
|
8828
|
+
return raw.length > maxChars ? `${raw.slice(0, maxChars).trimEnd()}\n...[truncated]` : raw;
|
|
8829
|
+
} catch {
|
|
8830
|
+
return '';
|
|
8831
|
+
}
|
|
8832
|
+
}
|
|
8833
|
+
|
|
8834
|
+
function buildPreloadedMemberContextLines(mission, root = process.cwd()) {
|
|
8835
|
+
const owner = String(mission?.owner || '').trim();
|
|
8836
|
+
const baseRoot = mission?.worktree_root || mission?.worktree?.path || root;
|
|
8837
|
+
const sections = [];
|
|
8838
|
+
if (owner && /^[a-zA-Z0-9._-]+$/.test(owner)) {
|
|
8839
|
+
const teamMemberDir = memberDir(owner, baseRoot) || path.join(baseRoot, 'atris', 'team', owner);
|
|
8840
|
+
const memberMd = readBoundedContextFile(path.join(teamMemberDir, 'MEMBER.md'), 1800);
|
|
8841
|
+
const soulMd = readBoundedContextFile(path.join(teamMemberDir, 'SOUL.md'), 1200);
|
|
8842
|
+
const missionMd = readBoundedContextFile(path.join(teamMemberDir, 'MISSION.md'), 800);
|
|
8843
|
+
if (memberMd) {
|
|
8844
|
+
sections.push(`### Member Identity (atris/team/${owner}/MEMBER.md)`, memberMd);
|
|
8845
|
+
}
|
|
8846
|
+
if (soulMd) {
|
|
8847
|
+
sections.push(`### Member Soul (atris/team/${owner}/SOUL.md)`, soulMd);
|
|
8848
|
+
}
|
|
8849
|
+
if (missionMd) {
|
|
8850
|
+
sections.push(`### Member Standing Mission (atris/team/${owner}/MISSION.md)`, missionMd);
|
|
8851
|
+
}
|
|
8852
|
+
try {
|
|
8853
|
+
const skillsDir = path.join(teamMemberDir, 'skills');
|
|
8854
|
+
if (fs.existsSync(skillsDir)) {
|
|
8855
|
+
const skillSnippets = fs.readdirSync(skillsDir)
|
|
8856
|
+
.sort()
|
|
8857
|
+
.slice(0, 3)
|
|
8858
|
+
.map((slug) => {
|
|
8859
|
+
const skillText = readBoundedContextFile(path.join(skillsDir, slug, 'SKILL.md'), 500);
|
|
8860
|
+
return skillText ? `#### Skill: ${slug}\n${skillText}` : '';
|
|
8861
|
+
})
|
|
8862
|
+
.filter(Boolean);
|
|
8863
|
+
if (skillSnippets.length) {
|
|
8864
|
+
sections.push(`### Unlocked Member Skills (atris/team/${owner}/skills/)`, skillSnippets.join('\n\n'));
|
|
8865
|
+
}
|
|
8866
|
+
}
|
|
8867
|
+
} catch {}
|
|
8868
|
+
try {
|
|
8869
|
+
const logsDir = path.join(teamMemberDir, 'logs');
|
|
8870
|
+
if (fs.existsSync(logsDir)) {
|
|
8871
|
+
const latestLog = fs.readdirSync(logsDir)
|
|
8872
|
+
.filter((name) => name.endsWith('.md'))
|
|
8873
|
+
.sort()
|
|
8874
|
+
.pop();
|
|
8875
|
+
if (latestLog) {
|
|
8876
|
+
const rawLog = fs.readFileSync(path.join(logsDir, latestLog), 'utf8').trim();
|
|
8877
|
+
if (rawLog) {
|
|
8878
|
+
const tail = rawLog.length > 500 ? `...[earlier entries]\n${rawLog.slice(-500).trimStart()}` : rawLog;
|
|
8879
|
+
sections.push(`### Recent Member Memory (atris/team/${owner}/logs/${latestLog})`, tail);
|
|
8880
|
+
}
|
|
8881
|
+
}
|
|
8882
|
+
}
|
|
8883
|
+
} catch {}
|
|
8884
|
+
}
|
|
8885
|
+
const boundTask = mission?.xp_task || mission?.selected_target_task || null;
|
|
8886
|
+
if (boundTask && (boundTask.title || boundTask.ref || boundTask.task_id)) {
|
|
8887
|
+
const taskRef = boundTask.ref || boundTask.task_ref || boundTask.task_id;
|
|
8888
|
+
const taskTitle = boundTask.title ? `: ${boundTask.title}` : '';
|
|
8889
|
+
const taskStatus = boundTask.status ? ` (${boundTask.status})` : '';
|
|
8890
|
+
sections.push(`### Bound Task Context`, `- Active task: ${taskRef}${taskTitle}${taskStatus}`);
|
|
8891
|
+
}
|
|
8892
|
+
try {
|
|
8893
|
+
const tasteHistory = readTasteReviewHistory(baseRoot);
|
|
8894
|
+
const tasteLines = [];
|
|
8895
|
+
if (tasteHistory?.accepted?.length) {
|
|
8896
|
+
tasteLines.push(...tasteHistory.accepted.slice(0, 2).map((line) => `- Accepted: ${line}`));
|
|
8897
|
+
}
|
|
8898
|
+
if (tasteHistory?.revised?.length) {
|
|
8899
|
+
tasteLines.push(...tasteHistory.revised.slice(0, 2).map((line) => `- Sent back: ${line}`));
|
|
8900
|
+
}
|
|
8901
|
+
if (tasteLines.length) {
|
|
8902
|
+
sections.push(`### Operator Taste & Review Notes`, tasteLines.join('\n'));
|
|
8903
|
+
}
|
|
8904
|
+
} catch {}
|
|
8905
|
+
if (!sections.length) return [];
|
|
8906
|
+
return [
|
|
8907
|
+
``,
|
|
8908
|
+
`## Preloaded Member & Task Context`,
|
|
8909
|
+
...sections,
|
|
8910
|
+
];
|
|
8911
|
+
}
|
|
8912
|
+
|
|
8615
8913
|
function buildTickPrompt(mission, tickIndex, maxTicks, frozen, pings = [], cwd = process.cwd()) {
|
|
8616
8914
|
const sharedProcess = memberProcessPrompt(cwd);
|
|
8617
8915
|
const pingLines = pings.length
|
|
@@ -8631,6 +8929,7 @@ function buildTickPrompt(mission, tickIndex, maxTicks, frozen, pings = [], cwd =
|
|
|
8631
8929
|
checkFeedback,
|
|
8632
8930
|
]
|
|
8633
8931
|
: [];
|
|
8932
|
+
const preloadedContextLines = buildPreloadedMemberContextLines(mission, cwd);
|
|
8634
8933
|
const lines = [
|
|
8635
8934
|
`# Mission Tick ${tickIndex}/${maxTicks}`,
|
|
8636
8935
|
...pingLines,
|
|
@@ -8646,6 +8945,7 @@ function buildTickPrompt(mission, tickIndex, maxTicks, frozen, pings = [], cwd =
|
|
|
8646
8945
|
...missionBudgetPromptLines(mission),
|
|
8647
8946
|
...checkFeedbackLines,
|
|
8648
8947
|
...(sharedProcess ? ['', sharedProcess] : []),
|
|
8948
|
+
...preloadedContextLines,
|
|
8649
8949
|
``,
|
|
8650
8950
|
`## Your task`,
|
|
8651
8951
|
`Do ONE increment of work toward the stop condition. ONE. No more.`,
|
|
@@ -8657,6 +8957,7 @@ function buildTickPrompt(mission, tickIndex, maxTicks, frozen, pings = [], cwd =
|
|
|
8657
8957
|
`- The tick is not done until the verify step passes. If a frozen verifier exists, the harness runs it, but that does not replace the real-surface proof below. If no frozen verifier exists, finish by actually running the surface you changed on this computer and paste the real command output into your receipt.`,
|
|
8658
8958
|
`- After that one verify step, STOP. Never start a second slice in one tick.`,
|
|
8659
8959
|
`- 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.`,
|
|
8960
|
+
`- Propose the next needle-mover: include a \`next_push: <one concrete feature or optimization to build next>\` line in your receipt and member log so the team compounds exponentially.`,
|
|
8660
8961
|
``,
|
|
8661
8962
|
`## No claimable work? Work the backstop ladder`,
|
|
8662
8963
|
`If the mission and task state offer no claimable work, inspect these rungs in order. Pick the FIRST rung with real fuel and do ONE bounded slice:`,
|
|
@@ -9217,10 +9518,25 @@ function engineVerifierResultFromRun(engineResult, engine = 'codex') {
|
|
|
9217
9518
|
};
|
|
9218
9519
|
}
|
|
9219
9520
|
|
|
9521
|
+
function pickCrossEngineVerifier(mission, cwd = process.cwd(), preferredEngine = '') {
|
|
9522
|
+
const explicit = canonicalEngineName(preferredEngine || mission?.referee_engine || '');
|
|
9523
|
+
if (explicit) return explicit;
|
|
9524
|
+
const workerEngine = canonicalEngineName(mission?.runner) || 'claude';
|
|
9525
|
+
const candidates = workerEngine === 'codex' ? ['claude', 'fable', 'codex'] : ['codex', 'claude'];
|
|
9526
|
+
try {
|
|
9527
|
+
const engines = engineRegistryView(cwd);
|
|
9528
|
+
for (const candidate of candidates) {
|
|
9529
|
+
const entry = engines.find((e) => e.id === candidate);
|
|
9530
|
+
if (entry && entry.installed && entry.health?.status === 'ready') return candidate;
|
|
9531
|
+
}
|
|
9532
|
+
} catch {}
|
|
9533
|
+
return workerEngine === 'codex' ? 'claude' : 'codex';
|
|
9534
|
+
}
|
|
9535
|
+
|
|
9220
9536
|
async function runEngineVerifier(mission, options = {}) {
|
|
9221
9537
|
const cwd = options.cwd || process.cwd();
|
|
9222
|
-
const engine =
|
|
9223
|
-
const verifyMission = { ...mission, runner: engine, runner_kind: 'engine' };
|
|
9538
|
+
const engine = pickCrossEngineVerifier(mission, cwd, options.refereeEngine);
|
|
9539
|
+
const verifyMission = { ...mission, runner: engine, runner_kind: 'engine', model: '' };
|
|
9224
9540
|
const restoreRunnerProfile = applyMissionRunnerProfile(engine);
|
|
9225
9541
|
let engineResult;
|
|
9226
9542
|
try {
|
|
@@ -9261,6 +9577,22 @@ function usefulClaudeReceiptSummary(text, fallback = 'no-text') {
|
|
|
9261
9577
|
return fallback;
|
|
9262
9578
|
}
|
|
9263
9579
|
|
|
9580
|
+
function extractNextPushFromReceiptText(text) {
|
|
9581
|
+
const raw = String(text || '');
|
|
9582
|
+
if (!raw.trim()) return null;
|
|
9583
|
+
const explicitMatch = raw.match(/^\s*(?:[-*]\s*)?(?:next_push|next push|next needle[- ]mover)\s*:\s*(.+)$/im);
|
|
9584
|
+
if (explicitMatch && explicitMatch[1]) {
|
|
9585
|
+
const cleaned = explicitMatch[1].replace(/`/g, '').replace(/\s+/g, ' ').trim();
|
|
9586
|
+
if (cleaned && !/^<one concrete/i.test(cleaned)) return cleaned.slice(0, 180);
|
|
9587
|
+
}
|
|
9588
|
+
const numberedMatch = raw.match(/^\s*(?:\(3\)|3[.)])\s*(?:next(?:\s+tick)?\s*:\s*)?(.+)$/im);
|
|
9589
|
+
if (numberedMatch && numberedMatch[1]) {
|
|
9590
|
+
const cleaned = numberedMatch[1].replace(/`/g, '').replace(/\s+/g, ' ').trim();
|
|
9591
|
+
if (cleaned && !/^layer\s*:/i.test(cleaned)) return cleaned.slice(0, 180);
|
|
9592
|
+
}
|
|
9593
|
+
return null;
|
|
9594
|
+
}
|
|
9595
|
+
|
|
9264
9596
|
function cappedClaudeReceiptText(text, limit = 4000) {
|
|
9265
9597
|
const clean = String(text || '').trim();
|
|
9266
9598
|
if (!clean) return '';
|
|
@@ -9316,6 +9648,8 @@ async function parseAndValidateMissionRunPhase(args) {
|
|
|
9316
9648
|
const selfDrive = dueMode || headlessOnly || hasFlag(args, '--self-drive');
|
|
9317
9649
|
const skipClaude = hasFlag(args, '--no-claude');
|
|
9318
9650
|
const verifyEach = !hasFlag(args, '--no-verify');
|
|
9651
|
+
const refereeEach = hasFlag(args, '--referee') && !hasFlag(args, '--no-referee');
|
|
9652
|
+
const refereeEngine = readFlag(args, '--referee-engine', '');
|
|
9319
9653
|
const completeOnPass = hasFlag(args, '--complete-on-pass');
|
|
9320
9654
|
const skipDrain = hasFlag(args, '--no-drain');
|
|
9321
9655
|
const createNext = hasFlag(args, '--create-next');
|
|
@@ -9351,6 +9685,8 @@ async function parseAndValidateMissionRunPhase(args) {
|
|
|
9351
9685
|
selfDrive,
|
|
9352
9686
|
skipClaude,
|
|
9353
9687
|
verifyEach,
|
|
9688
|
+
refereeEach,
|
|
9689
|
+
refereeEngine,
|
|
9354
9690
|
completeOnPass,
|
|
9355
9691
|
skipDrain,
|
|
9356
9692
|
createNext,
|
|
@@ -9376,7 +9712,6 @@ async function resolveMissionRunPhase(context) {
|
|
|
9376
9712
|
const {
|
|
9377
9713
|
args,
|
|
9378
9714
|
asJson,
|
|
9379
|
-
cwd,
|
|
9380
9715
|
dueMode,
|
|
9381
9716
|
headlessOnly,
|
|
9382
9717
|
budgetTier,
|
|
@@ -9389,7 +9724,7 @@ async function resolveMissionRunPhase(context) {
|
|
|
9389
9724
|
ref,
|
|
9390
9725
|
runArgs,
|
|
9391
9726
|
} = context;
|
|
9392
|
-
let { maxTicks, maxWallSeconds } = context;
|
|
9727
|
+
let { cwd, maxTicks, maxWallSeconds } = context;
|
|
9393
9728
|
|
|
9394
9729
|
if (!dueMode && !ref) {
|
|
9395
9730
|
if (asJson || !process.stdin.isTTY || !process.stderr.isTTY) {
|
|
@@ -9428,6 +9763,13 @@ async function resolveMissionRunPhase(context) {
|
|
|
9428
9763
|
if (!mission) {
|
|
9429
9764
|
exitMissionError('Usage: atris mission run <id|objective> [--max-ticks 4] [--max-wall 3600]', 1, asJson);
|
|
9430
9765
|
}
|
|
9766
|
+
const worktreeRoot = mission.worktree_root || mission.worktree?.path || '';
|
|
9767
|
+
if (worktreeRoot && fs.existsSync(worktreeRoot) && path.resolve(worktreeRoot) !== path.resolve(cwd)) {
|
|
9768
|
+
cwd = path.resolve(worktreeRoot);
|
|
9769
|
+
context.cwd = cwd;
|
|
9770
|
+
process.chdir(cwd);
|
|
9771
|
+
mission = resolveMission(mission.id, cwd) || mission;
|
|
9772
|
+
}
|
|
9431
9773
|
if (!maxTicksFlag && !budgetTier && Number(mission.max_ticks) > 0) {
|
|
9432
9774
|
maxTicks = Math.max(1, Number(mission.max_ticks));
|
|
9433
9775
|
}
|
|
@@ -9452,6 +9794,30 @@ async function resolveMissionRunPhase(context) {
|
|
|
9452
9794
|
console.error(`Mission ${mission.id} is ${mission.status}; nothing to run.`);
|
|
9453
9795
|
process.exit(0);
|
|
9454
9796
|
}
|
|
9797
|
+
// An explicit run must respect open human asks the same way auto-selection
|
|
9798
|
+
// does (missionIsRunnable), instead of ticking straight past them.
|
|
9799
|
+
const blockingAsks = runBlockingAsks(mission.human_asks);
|
|
9800
|
+
if (blockingAsks.length) {
|
|
9801
|
+
detachedDriverLifecycle?.setExitReason('human_asks_open');
|
|
9802
|
+
const lines = [
|
|
9803
|
+
`Mission ${mission.id} is waiting on ${blockingAsks.length} human ask${blockingAsks.length === 1 ? '' : 's'}; not ticking.`,
|
|
9804
|
+
...blockingAsks.map((ask) => ` - ${ask.text}${ask.answered_at ? ' (answered no)' : ''}`),
|
|
9805
|
+
'Answer with: atris decide (then atris decide <n> y), and re-run.',
|
|
9806
|
+
];
|
|
9807
|
+
if (asJson) {
|
|
9808
|
+
printJsonOrText({
|
|
9809
|
+
ok: false,
|
|
9810
|
+
action: 'run_blocked',
|
|
9811
|
+
reason: 'human_asks_open',
|
|
9812
|
+
mission_id: mission.id,
|
|
9813
|
+
human_asks: blockingAsks.map((ask) => ask.text),
|
|
9814
|
+
next_command: 'atris decide',
|
|
9815
|
+
}, [], true);
|
|
9816
|
+
} else {
|
|
9817
|
+
for (const line of lines) console.error(line);
|
|
9818
|
+
}
|
|
9819
|
+
process.exit(2);
|
|
9820
|
+
}
|
|
9455
9821
|
if (hasFlag(args, '--detach')) {
|
|
9456
9822
|
detachMissionRun(args, mission, cwd, asJson);
|
|
9457
9823
|
context.handled = true;
|
|
@@ -9671,6 +10037,7 @@ async function executeMissionRunTicksPhase(context) {
|
|
|
9671
10037
|
const tickEngineId = engineBackedTick ? (tickSelection.engine_id || inferredEngineId) : null;
|
|
9672
10038
|
if (engineBackedTick) {
|
|
9673
10039
|
result.engine_id = tickEngineId;
|
|
10040
|
+
if (tickRuntimeMission.model) result.model = tickRuntimeMission.model;
|
|
9674
10041
|
result.requested_engine = tickSelection.requested_engine;
|
|
9675
10042
|
result.engine_fallback_reason = tickSelection.engine_fallback_reason;
|
|
9676
10043
|
}
|
|
@@ -9945,6 +10312,7 @@ async function executeMissionRunTicksPhase(context) {
|
|
|
9945
10312
|
tickSkipWorker,
|
|
9946
10313
|
tickIdx,
|
|
9947
10314
|
frozen,
|
|
10315
|
+
storedVerifier,
|
|
9948
10316
|
};
|
|
9949
10317
|
let verifierResult = null;
|
|
9950
10318
|
if (cachedStep) {
|
|
@@ -9986,12 +10354,15 @@ async function executeMissionRunTicksPhase(context) {
|
|
|
9986
10354
|
|
|
9987
10355
|
// Layer classification needs the receipt text AND the worktree receipt, so it
|
|
9988
10356
|
// runs here, after both exist, covering the claude and atris2 branches alike.
|
|
10357
|
+
const tickReceiptText = result.atris2?.receipt_text || result.claude?.receipt_text || result.drill?.receipt_text || '';
|
|
9989
10358
|
if (!(cachedStep && cachedStep.tick && cachedStep.tick.layer)) {
|
|
9990
|
-
const tickReceiptText = result.atris2?.receipt_text || result.claude?.receipt_text || result.drill?.receipt_text || '';
|
|
9991
10359
|
const layerInfo = extractLayerFromReceiptText(tickReceiptText, tickWorktree?.new_since_baseline_sample);
|
|
9992
10360
|
result.layer = layerInfo.layer;
|
|
9993
10361
|
result.layer_source = layerInfo.source;
|
|
9994
10362
|
}
|
|
10363
|
+
if (!result.next_push) {
|
|
10364
|
+
result.next_push = extractNextPushFromReceiptText(tickReceiptText);
|
|
10365
|
+
}
|
|
9995
10366
|
|
|
9996
10367
|
// Persist tick to mission state + write structured receipt
|
|
9997
10368
|
const finishedAt = stampIso();
|
|
@@ -10017,9 +10388,14 @@ async function executeMissionRunTicksPhase(context) {
|
|
|
10017
10388
|
// reopen closed rooms, mission 32 complete@19:23:33 → ready@19:23:48).
|
|
10018
10389
|
const latestOnDisk = loadMissionMap(cwd).get(mission.id) || mission;
|
|
10019
10390
|
const alreadyTerminal = TERMINAL_STATUSES.has(String(latestOnDisk.status || ''));
|
|
10391
|
+
// A passing verifier alone never completes a mission whose linked task
|
|
10392
|
+
// is not accepted: park it ready and name the accept command instead.
|
|
10393
|
+
const acceptGate = (verifierResult?.passed && completeOnPass && !fullBudgetMode && !mission.always_on && !xpReadyAction)
|
|
10394
|
+
? missionLinkedTaskGate(mission, cwd)
|
|
10395
|
+
: { ok: true };
|
|
10020
10396
|
let newStatus = (verifierResult?.passed && mission.always_on) ? 'ready' :
|
|
10021
10397
|
(verifierResult?.passed && xpReadyAction) ? 'ready' :
|
|
10022
|
-
(verifierResult?.passed && completeOnPass && !fullBudgetMode) ? 'complete' :
|
|
10398
|
+
(verifierResult?.passed && completeOnPass && !fullBudgetMode && acceptGate.ok) ? 'complete' :
|
|
10023
10399
|
(verifierResult?.passed && fullBudgetMode) ? 'running' :
|
|
10024
10400
|
(verifierResult?.passed ? 'ready' :
|
|
10025
10401
|
(verifierResult ? 'blocked' :
|
|
@@ -10033,6 +10409,8 @@ async function executeMissionRunTicksPhase(context) {
|
|
|
10033
10409
|
nextAction = nextCandidateTickAction(mission);
|
|
10034
10410
|
} else if (verifierResult?.passed && xpReadyAction) {
|
|
10035
10411
|
nextAction = xpReadyAction;
|
|
10412
|
+
} else if (verifierResult?.passed && completeOnPass && !fullBudgetMode && !acceptGate.ok) {
|
|
10413
|
+
nextAction = missionAcceptNextAction(mission, acceptGate);
|
|
10036
10414
|
} else if (verifierResult?.passed && completeOnPass && !fullBudgetMode) {
|
|
10037
10415
|
nextAction = 'mission complete';
|
|
10038
10416
|
} else if (verifierResult?.passed && fullBudgetMode) {
|
|
@@ -10070,6 +10448,7 @@ async function executeMissionRunTicksPhase(context) {
|
|
|
10070
10448
|
last_tick_layer: result.layer,
|
|
10071
10449
|
last_tick_layer_source: result.layer_source,
|
|
10072
10450
|
error_streak_count: errorStreakCount,
|
|
10451
|
+
last_next_push: result.next_push || latestOnDisk.last_next_push || null,
|
|
10073
10452
|
verifier_result: verifierResult || (verifyEach && result.protected_lane_guard && result.protected_lane_guard.allowed === false ? null : latestOnDisk.verifier_result) || null,
|
|
10074
10453
|
last_check_feedback: verifierResult
|
|
10075
10454
|
? extractCheckFeedback(verifierResult)
|
|
@@ -10078,7 +10457,16 @@ async function executeMissionRunTicksPhase(context) {
|
|
|
10078
10457
|
next_action: nextAction,
|
|
10079
10458
|
claude_session_ticks: rotateSessionForContext ? 0 : claudeSessionTicks,
|
|
10080
10459
|
}, cwd, 'mission_tick', {
|
|
10081
|
-
tick_index: tickIdx,
|
|
10460
|
+
tick_index: tickIdx,
|
|
10461
|
+
status: result.status,
|
|
10462
|
+
reason: result.reason,
|
|
10463
|
+
receipt_path: receiptPath,
|
|
10464
|
+
layer: result.layer,
|
|
10465
|
+
next_push: result.next_push || undefined,
|
|
10466
|
+
engine: result.engine_id || runtimeMission.runner || mission.runner || null,
|
|
10467
|
+
task_type: 'executor',
|
|
10468
|
+
verified_passed: verifierResult && typeof verifierResult.passed === 'boolean' ? verifierResult.passed : undefined,
|
|
10469
|
+
duration_ms: Math.max(0, (Date.parse(finishedAt) || 0) - (Date.parse(tickStart) || 0)),
|
|
10082
10470
|
}).mission;
|
|
10083
10471
|
if (rotateSessionForContext) {
|
|
10084
10472
|
// Same shape as the stale-lock rotation above: mint the fresh pending
|
|
@@ -10093,6 +10481,7 @@ async function executeMissionRunTicksPhase(context) {
|
|
|
10093
10481
|
},
|
|
10094
10482
|
).mission;
|
|
10095
10483
|
}
|
|
10484
|
+
const tickSummaryText = String(result?.claude?.summary || '').replace(/\s+/g, ' ').trim();
|
|
10096
10485
|
appendMemberLog(mission.owner, `Mission run tick ${tickIdx}`, {
|
|
10097
10486
|
mission: mission.objective,
|
|
10098
10487
|
state: mission.status,
|
|
@@ -10100,8 +10489,39 @@ async function executeMissionRunTicksPhase(context) {
|
|
|
10100
10489
|
reason: result.reason,
|
|
10101
10490
|
layer: result.layer || undefined,
|
|
10102
10491
|
verifier: verifierResult ? (verifierResult.passed ? 'passed' : 'failed') : 'not_run',
|
|
10492
|
+
summary: tickSummaryText ? tickSummaryText.slice(0, 240) : undefined,
|
|
10493
|
+
next_push: result.next_push ? String(result.next_push).slice(0, 180) : undefined,
|
|
10494
|
+
feedback: verifierResult && !verifierResult.passed ? String(mission.last_check_feedback || '').slice(0, 240) : undefined,
|
|
10103
10495
|
receipt: receiptPath,
|
|
10104
|
-
});
|
|
10496
|
+
}, cwd);
|
|
10497
|
+
if (verifierResult && verifierResult.passed === true && result.status === 'ran') {
|
|
10498
|
+
try {
|
|
10499
|
+
crystallizeMemberSkillLesson(mission.owner, {
|
|
10500
|
+
objective: mission.objective,
|
|
10501
|
+
summary: tickSummaryText || mission.objective,
|
|
10502
|
+
verifier: frozen.verifier || mission.verifier || '',
|
|
10503
|
+
next_push: result.next_push || '',
|
|
10504
|
+
layer: result.layer || 'capabilities',
|
|
10505
|
+
}, cwd);
|
|
10506
|
+
} catch {}
|
|
10507
|
+
if (result.next_push) {
|
|
10508
|
+
try {
|
|
10509
|
+
autoQueueMemberNextPush(mission.owner, result.next_push, mission.objective, cwd, {
|
|
10510
|
+
promote: newStatus === 'complete',
|
|
10511
|
+
});
|
|
10512
|
+
} catch {}
|
|
10513
|
+
}
|
|
10514
|
+
if (mission.owner) {
|
|
10515
|
+
try {
|
|
10516
|
+
closeMemberHandoffsForReceiver(mission.owner, cwd);
|
|
10517
|
+
} catch {}
|
|
10518
|
+
}
|
|
10519
|
+
if (newStatus === 'complete' && mission.owner) {
|
|
10520
|
+
try {
|
|
10521
|
+
recordMemberHandoff(mission.owner, '', tickSummaryText || mission.objective, cwd);
|
|
10522
|
+
} catch {}
|
|
10523
|
+
}
|
|
10524
|
+
}
|
|
10105
10525
|
if (newStatus === 'complete') {
|
|
10106
10526
|
continuationGoal = seedMissionRunContinuation(mission, cwd, receiptPath);
|
|
10107
10527
|
if (continuationGoal?.parent) mission = continuationGoal.parent;
|
|
@@ -10262,12 +10682,37 @@ async function verifyMissionRunTickPhase(context) {
|
|
|
10262
10682
|
if (result.status === 'ran' && verifyEach) {
|
|
10263
10683
|
if (currentTick.frozen.verifier) {
|
|
10264
10684
|
verifierResult = runVerifier(currentTick.frozen.verifier);
|
|
10265
|
-
}
|
|
10266
|
-
|
|
10685
|
+
}
|
|
10686
|
+
const shouldRunReferee = !currentTick.tickSkipWorker
|
|
10687
|
+
&& (!verifierResult || verifierResult.passed)
|
|
10688
|
+
&& (
|
|
10689
|
+
!currentTick.frozen.verifier
|
|
10690
|
+
|| context.refereeEach
|
|
10691
|
+
|| (!currentTick.storedVerifier && !process.env.NODE_TEST_CONTEXT)
|
|
10692
|
+
);
|
|
10693
|
+
if (shouldRunReferee) {
|
|
10694
|
+
const refereeResult = await runEngineVerifier(currentTick.tickRuntimeMission, {
|
|
10267
10695
|
cwd,
|
|
10268
10696
|
signal: controller.signal,
|
|
10269
10697
|
tickIndex: currentTick.tickIdx,
|
|
10698
|
+
refereeEngine: context.refereeEngine,
|
|
10270
10699
|
});
|
|
10700
|
+
result.referee_result = refereeResult;
|
|
10701
|
+
if (!verifierResult || !refereeResult.passed) {
|
|
10702
|
+
verifierResult = verifierResult
|
|
10703
|
+
? {
|
|
10704
|
+
...verifierResult,
|
|
10705
|
+
passed: Boolean(verifierResult.passed && refereeResult.passed),
|
|
10706
|
+
referee: refereeResult,
|
|
10707
|
+
output: [verifierResult.stdout, verifierResult.stderr, refereeResult.output].filter(Boolean).join('\n'),
|
|
10708
|
+
}
|
|
10709
|
+
: refereeResult;
|
|
10710
|
+
} else {
|
|
10711
|
+
verifierResult = {
|
|
10712
|
+
...verifierResult,
|
|
10713
|
+
referee: refereeResult,
|
|
10714
|
+
};
|
|
10715
|
+
}
|
|
10271
10716
|
}
|
|
10272
10717
|
if (verifierResult) result.verifier_passed = verifierResult.passed;
|
|
10273
10718
|
}
|
|
@@ -10601,10 +11046,14 @@ function tickMission(args) {
|
|
|
10601
11046
|
const xpReadyAction = missionXpReadyAction(mission, receiptPath);
|
|
10602
11047
|
const budgetRemainingSeconds = missionFullBudgetRemainingSeconds(mission);
|
|
10603
11048
|
const budgetOpen = budgetRemainingSeconds > 0;
|
|
10604
|
-
|
|
11049
|
+
const acceptGate = (completeOnPass && !mission.always_on && !xpReadyAction && !budgetOpen)
|
|
11050
|
+
? missionLinkedTaskGate(mission, process.cwd())
|
|
11051
|
+
: { ok: true };
|
|
11052
|
+
status = (completeOnPass && !mission.always_on && !xpReadyAction && !budgetOpen && acceptGate.ok) ? 'complete'
|
|
10605
11053
|
: (budgetOpen && !mission.always_on && !xpReadyAction ? 'running' : 'ready');
|
|
10606
11054
|
nextAction = mission.always_on ? nextCandidateTickAction(mission) :
|
|
10607
11055
|
(xpReadyAction
|
|
11056
|
+
|| (!acceptGate.ok ? missionAcceptNextAction(mission, acceptGate) : null)
|
|
10608
11057
|
|| (budgetOpen
|
|
10609
11058
|
? `proof passed with ${durationLabel(budgetRemainingSeconds)} left on the budget; pick the next useful move, then: atris mission tick ${mission.id} --verify --summary "<what changed>"`
|
|
10610
11059
|
: (completeOnPass ? 'mission complete' : `review proof then run: atris mission complete ${mission.id} --proof "${receiptPath}"`)));
|
|
@@ -10747,6 +11196,38 @@ function receiptShowsPass(receipt) {
|
|
|
10747
11196
|
// verifier passed. Mirrors the task plane's proof-only accept guard so the
|
|
10748
11197
|
// final transition consumes the receipts instead of trusting free text.
|
|
10749
11198
|
function missionCompletionGate(mission, proof, root = process.cwd()) {
|
|
11199
|
+
const base = missionVerifierCompletionGate(mission, proof, root);
|
|
11200
|
+
if (!base.ok) {
|
|
11201
|
+
// The check not passing is the first thing to fix, but a linked task that
|
|
11202
|
+
// is not accepted would refuse the next try too: say so now.
|
|
11203
|
+
const pending = missionLinkedTaskGate(mission, root);
|
|
11204
|
+
if (pending.ok) return base;
|
|
11205
|
+
const how = pending.accept_command ? ` (${pending.accept_command})` : '';
|
|
11206
|
+
return { ...base, linked_task_note: `Also, ${pending.reason}${how}` };
|
|
11207
|
+
}
|
|
11208
|
+
// Done means an accepted task plus a receipt: a passing verifier is not
|
|
11209
|
+
// enough when the mission is linked to task(s) a human has not accepted.
|
|
11210
|
+
const linked = missionLinkedTaskGate(mission, root);
|
|
11211
|
+
if (!linked.ok) {
|
|
11212
|
+
return {
|
|
11213
|
+
ok: false,
|
|
11214
|
+
source: 'linked_task',
|
|
11215
|
+
reason: linked.reason,
|
|
11216
|
+
task_id: linked.task_id,
|
|
11217
|
+
task_ref: linked.task_ref,
|
|
11218
|
+
accept_command: linked.accept_command,
|
|
11219
|
+
archived_only: linked.archived_only === true,
|
|
11220
|
+
};
|
|
11221
|
+
}
|
|
11222
|
+
return linked.tasks.length ? { ...base, linked_tasks: linked.tasks } : base;
|
|
11223
|
+
}
|
|
11224
|
+
|
|
11225
|
+
function missionAcceptNextAction(mission, gate) {
|
|
11226
|
+
const how = gate.accept_command || (gate.archived_only ? 'link a live task and accept it' : 'attach a receipt to it');
|
|
11227
|
+
return `verifier passed but ${gate.reason}; accept it (${how}), then: atris mission complete ${mission.id} --proof "<receipt>"`;
|
|
11228
|
+
}
|
|
11229
|
+
|
|
11230
|
+
function missionVerifierCompletionGate(mission, proof, root = process.cwd()) {
|
|
10750
11231
|
const remainingSeconds = missionFullBudgetRemainingSeconds(mission);
|
|
10751
11232
|
if (remainingSeconds > 0) {
|
|
10752
11233
|
return {
|
|
@@ -10789,10 +11270,19 @@ function completeMission(args) {
|
|
|
10789
11270
|
}
|
|
10790
11271
|
const gate = missionCompletionGate(mission, proof, root);
|
|
10791
11272
|
if (!gate.ok && !force) {
|
|
10792
|
-
|
|
11273
|
+
const fix = gate.source === 'linked_task'
|
|
11274
|
+
? (gate.accept_command
|
|
11275
|
+
? `Accept it first: ${gate.accept_command}, then re-run mission complete`
|
|
11276
|
+
: (gate.archived_only ? 'Link a live task to the mission and accept it first' : 'Attach a receipt to it first'))
|
|
11277
|
+
: `Run: atris mission tick ${mission.id} --verify`;
|
|
11278
|
+
const also = gate.linked_task_note ? ` ${gate.linked_task_note}.` : '';
|
|
11279
|
+
exitMissionError(`[mission complete] ${gate.reason}. ${fix}.${also} (or override as operator with --force)`, 2, asJson);
|
|
11280
|
+
}
|
|
11281
|
+
if (!gate.ok && force && gate.source === 'linked_task') {
|
|
11282
|
+
console.error(`[mission complete] WARNING --force overrides the accepted-task gate: ${gate.reason}. Recorded as completion_gate.forced.`);
|
|
10793
11283
|
}
|
|
10794
11284
|
const baselineSummary = pruneMissionWorktreeBaseline(mission, root);
|
|
10795
|
-
const completionGate = { ...gate, forced: force && !gate.ok };
|
|
11285
|
+
const completionGate = { ...gate, forced: force && !gate.ok, ...(force && !gate.ok ? { forced_reason: gate.reason } : {}) };
|
|
10796
11286
|
const baseNext = {
|
|
10797
11287
|
...mission,
|
|
10798
11288
|
status: 'complete',
|
|
@@ -11849,6 +12339,7 @@ module.exports = {
|
|
|
11849
12339
|
startMission,
|
|
11850
12340
|
resolveMission,
|
|
11851
12341
|
spawnMissionDriver,
|
|
12342
|
+
missionDriverHealth,
|
|
11852
12343
|
missionRunnerNeedsLiveSession,
|
|
11853
12344
|
completeMission,
|
|
11854
12345
|
inspectMission,
|
|
@@ -11874,6 +12365,9 @@ module.exports = {
|
|
|
11874
12365
|
selectCodexGoalMission,
|
|
11875
12366
|
codexGoalNextCommand,
|
|
11876
12367
|
usefulClaudeReceiptSummary,
|
|
12368
|
+
autoQueueMemberNextPush,
|
|
12369
|
+
recordMemberHandoff,
|
|
12370
|
+
closeMemberHandoffsForReceiver,
|
|
11877
12371
|
cappedClaudeReceiptText,
|
|
11878
12372
|
extractLayerFromReceiptText,
|
|
11879
12373
|
classifyPathsByLayer,
|
|
@@ -11893,6 +12387,7 @@ module.exports = {
|
|
|
11893
12387
|
missionVerifierHighLevelTestText,
|
|
11894
12388
|
buildEngineVerifyPrompt,
|
|
11895
12389
|
engineVerifierResultFromRun,
|
|
12390
|
+
pickCrossEngineVerifier,
|
|
11896
12391
|
runVerifier,
|
|
11897
12392
|
missionFullBudgetRemainingSeconds,
|
|
11898
12393
|
missionBudgetContinuationText,
|