atris 3.31.0 → 3.33.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -4
- package/atris/CLAUDE.md +8 -0
- package/atris.md +8 -0
- package/ax +458 -15
- package/bin/atris.js +200 -76
- package/commands/autoland.js +78 -12
- package/commands/autopilot-front.js +273 -0
- package/commands/engine.js +299 -0
- package/commands/init.js +21 -0
- package/commands/integrations.js +147 -0
- package/commands/member.js +85 -0
- package/commands/mission.js +500 -43
- package/commands/run-front.js +144 -0
- package/commands/run.js +9 -6
- package/commands/sign.js +90 -0
- package/commands/task.js +380 -20
- package/commands/truth.js +72 -11
- package/lib/autoland.js +133 -25
- package/lib/fleet.js +354 -0
- package/lib/inspect-fields.js +174 -0
- package/lib/operator-next.js +7 -0
- package/lib/pulse.js +4 -3
- package/lib/runner-command.js +54 -11
- package/lib/task-db.js +57 -2
- package/package.json +3 -2
- package/scripts/agent_worktree.py +72 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// atris run: thin front door over the mission runtime loop.
|
|
2
|
+
// One bounded pursuit: start a mission from the objective (or resume the most
|
|
3
|
+
// logical runnable mission), tick it, complete on pass, then exit.
|
|
4
|
+
// The old plan→do→review loop lives on behind `atris run --legacy`.
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { spawnSync } = require('child_process');
|
|
7
|
+
|
|
8
|
+
const CLI_PATH = path.join(__dirname, '..', 'bin', 'atris.js');
|
|
9
|
+
|
|
10
|
+
const RUN_VALUE_FLAGS = new Set([
|
|
11
|
+
'--owner',
|
|
12
|
+
'--cadence',
|
|
13
|
+
'--minutes',
|
|
14
|
+
'--hours',
|
|
15
|
+
'--max-ticks',
|
|
16
|
+
'--max-wall',
|
|
17
|
+
'--runner',
|
|
18
|
+
'--runner-bin',
|
|
19
|
+
'--runner-template',
|
|
20
|
+
'--runner-model',
|
|
21
|
+
'--runner-profile',
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
function readValueFlag(args, name) {
|
|
25
|
+
const index = args.indexOf(name);
|
|
26
|
+
return index >= 0 && index + 1 < args.length ? args[index + 1] : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function positiveNumber(value) {
|
|
30
|
+
const parsed = Number(value);
|
|
31
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function runBudgetSeconds(args = []) {
|
|
35
|
+
const hours = positiveNumber(readValueFlag(args, '--hours'));
|
|
36
|
+
if (hours) return Math.round(hours * 3600);
|
|
37
|
+
const minutes = positiveNumber(readValueFlag(args, '--minutes'));
|
|
38
|
+
if (minutes) return Math.round(minutes * 60);
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function runObjective(args = []) {
|
|
43
|
+
const words = [];
|
|
44
|
+
for (let i = 0; i < args.length; i++) {
|
|
45
|
+
const arg = args[i];
|
|
46
|
+
if (RUN_VALUE_FLAGS.has(arg)) { i++; continue; }
|
|
47
|
+
if (arg.startsWith('-')) continue;
|
|
48
|
+
words.push(arg);
|
|
49
|
+
}
|
|
50
|
+
return words.join(' ').trim();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Runners a spawned `mission run` can drive unattended.
|
|
54
|
+
const HEADLESS_DRIVABLE_RUNNERS = new Set(['claude', 'atris2']);
|
|
55
|
+
const CALLER_SESSION_RUNNERS = new Set(['codex_goal', 'caller_session', 'current_agent']);
|
|
56
|
+
|
|
57
|
+
function liveCodexSession(env = process.env) {
|
|
58
|
+
return Boolean(String(env.CODEX_THREAD_ID || '').trim());
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function runnerDrivableForFrontDoor(runner, options = {}) {
|
|
62
|
+
const normalized = String(runner || '').trim().toLowerCase();
|
|
63
|
+
if (HEADLESS_DRIVABLE_RUNNERS.has(normalized)) return true;
|
|
64
|
+
if (options.allowCallerSessionRunners && CALLER_SESSION_RUNNERS.has(normalized)) return true;
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Most logical mission: a moving one beats a planned one, newer beats older.
|
|
69
|
+
// ready is excluded — it waits for review, not for another worker pass.
|
|
70
|
+
function pickRunnableMission(root = process.cwd(), missionMap = null, options = {}) {
|
|
71
|
+
let map = missionMap;
|
|
72
|
+
if (!map) {
|
|
73
|
+
try { map = require('./mission').loadMissionMap(root); } catch { return null; }
|
|
74
|
+
}
|
|
75
|
+
const candidates = Array.from(map.values())
|
|
76
|
+
.filter((mission) => mission && (mission.status === 'running' || mission.status === 'planning'))
|
|
77
|
+
.filter((mission) => runnerDrivableForFrontDoor(mission?.runner, options))
|
|
78
|
+
.filter((mission) => !/^decide and start the next useful mission after:/i.test(String(mission?.objective || '')))
|
|
79
|
+
.sort((a, b) => {
|
|
80
|
+
if (a.status !== b.status) return a.status === 'running' ? -1 : 1;
|
|
81
|
+
return String(b.updated_at || b.created_at || '').localeCompare(String(a.updated_at || a.created_at || ''));
|
|
82
|
+
});
|
|
83
|
+
return candidates[0] || null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function runTickBudget(args, budgetSeconds) {
|
|
87
|
+
const explicit = positiveNumber(readValueFlag(args, '--max-ticks'));
|
|
88
|
+
if (explicit) return Math.floor(explicit);
|
|
89
|
+
// A timed run is a loop contract: keep picking the next useful move until
|
|
90
|
+
// the wall clock spends the budget (same rule as member run).
|
|
91
|
+
return budgetSeconds ? Math.max(4, Math.ceil(budgetSeconds / 300)) : 4;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function runMissionFront(args = []) {
|
|
95
|
+
const objective = runObjective(args);
|
|
96
|
+
const budgetSeconds = runBudgetSeconds(args);
|
|
97
|
+
const maxTicks = runTickBudget(args, budgetSeconds);
|
|
98
|
+
const maxWallSeconds = positiveNumber(readValueFlag(args, '--max-wall')) || budgetSeconds || 1200;
|
|
99
|
+
|
|
100
|
+
if (objective) {
|
|
101
|
+
const { runRuntimeMissionLoop } = require('../lib/mission-runtime-loop');
|
|
102
|
+
const result = await runRuntimeMissionLoop({
|
|
103
|
+
objective,
|
|
104
|
+
owner: readValueFlag(args, '--owner') || 'mission-lead',
|
|
105
|
+
cadence: readValueFlag(args, '--cadence') || '',
|
|
106
|
+
maxTicks,
|
|
107
|
+
maxWallSeconds,
|
|
108
|
+
noComplete: args.includes('--no-complete'),
|
|
109
|
+
}, {
|
|
110
|
+
cliPath: CLI_PATH,
|
|
111
|
+
onProgress: (line) => console.log(` ${line}`),
|
|
112
|
+
});
|
|
113
|
+
console.log(result.text || '');
|
|
114
|
+
return result.ok ? 0 : 1;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const mission = pickRunnableMission(process.cwd(), null, {
|
|
118
|
+
allowCallerSessionRunners: liveCodexSession(),
|
|
119
|
+
});
|
|
120
|
+
if (!mission) {
|
|
121
|
+
console.log('No objective given and no runnable mission found.');
|
|
122
|
+
console.log('Start one: atris run "<objective>" [--minutes N] [--owner <member>]');
|
|
123
|
+
console.log('Or keep going indefinitely: atris autopilot');
|
|
124
|
+
return 1;
|
|
125
|
+
}
|
|
126
|
+
console.log(`Resuming mission ${mission.id} (${mission.status}): ${mission.objective}`);
|
|
127
|
+
const result = spawnSync(process.execPath, [
|
|
128
|
+
CLI_PATH, 'mission', 'run', mission.id,
|
|
129
|
+
'--max-ticks', String(maxTicks),
|
|
130
|
+
'--max-wall', String(maxWallSeconds),
|
|
131
|
+
'--complete-on-pass',
|
|
132
|
+
], { cwd: process.cwd(), stdio: 'inherit' });
|
|
133
|
+
return result.status || 0;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
module.exports = {
|
|
137
|
+
runMissionFront,
|
|
138
|
+
pickRunnableMission,
|
|
139
|
+
liveCodexSession,
|
|
140
|
+
runnerDrivableForFrontDoor,
|
|
141
|
+
runObjective,
|
|
142
|
+
runBudgetSeconds,
|
|
143
|
+
runTickBudget,
|
|
144
|
+
};
|
package/commands/run.js
CHANGED
|
@@ -381,12 +381,15 @@ async function runAtris(options = {}) {
|
|
|
381
381
|
process.exit(1);
|
|
382
382
|
}
|
|
383
383
|
|
|
384
|
-
// Check configured runner CLI is available.
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
384
|
+
// Check configured runner CLI is available. Dry runs preview context only —
|
|
385
|
+
// they must work on machines (and CI) without the runner installed.
|
|
386
|
+
if (!dryRun) {
|
|
387
|
+
try {
|
|
388
|
+
execSync(buildRunnerAvailabilityCommand(), { stdio: 'pipe' });
|
|
389
|
+
} catch (err) {
|
|
390
|
+
console.error(runnerAvailabilityFailureMessage(err));
|
|
391
|
+
process.exit(1);
|
|
392
|
+
}
|
|
390
393
|
}
|
|
391
394
|
|
|
392
395
|
console.log('');
|
package/commands/sign.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
// atris sign: co-author trailer on every commit in an atris workspace.
|
|
5
|
+
// Installs a prepare-commit-msg hook that appends "Co-authored-by: Atris"
|
|
6
|
+
// when the repo has an atris/ folder — same credit line Claude and Cursor use.
|
|
7
|
+
// Idempotent and non-destructive: appends a marked block, removes only its own block.
|
|
8
|
+
|
|
9
|
+
const MARKER = '# atris co-author';
|
|
10
|
+
const TRAILER = 'Co-authored-by: Atris <299057014+atris-builder[bot]@users.noreply.github.com>';
|
|
11
|
+
|
|
12
|
+
const HOOK_BLOCK = `
|
|
13
|
+
${MARKER}
|
|
14
|
+
if [ -d "$(git rev-parse --show-toplevel 2>/dev/null)/atris" ] && [ "$2" != "merge" ] && [ "$2" != "squash" ]; then
|
|
15
|
+
if ! grep -qi "^co-authored-by: atris" "$1"; then
|
|
16
|
+
printf '\\nCo-authored-by: Atris <299057014+atris-builder[bot]@users.noreply.github.com>\\n' >> "$1"
|
|
17
|
+
fi
|
|
18
|
+
fi
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
function hookPathFor(root) {
|
|
22
|
+
return path.join(root, '.git', 'hooks', 'prepare-commit-msg');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Install the marked block into .git/hooks/prepare-commit-msg. Returns { hookPath, already }.
|
|
26
|
+
function installSignHook(root = process.cwd()) {
|
|
27
|
+
if (!fs.existsSync(path.join(root, '.git'))) throw new Error('not a git repo (no .git here)');
|
|
28
|
+
const hookPath = hookPathFor(root);
|
|
29
|
+
fs.mkdirSync(path.dirname(hookPath), { recursive: true });
|
|
30
|
+
let content = '';
|
|
31
|
+
try { content = fs.readFileSync(hookPath, 'utf8'); } catch {}
|
|
32
|
+
if (content.includes(MARKER)) return { hookPath, already: true };
|
|
33
|
+
if (!content) content = '#!/bin/sh\n';
|
|
34
|
+
if (!content.endsWith('\n')) content += '\n';
|
|
35
|
+
content += HOOK_BLOCK;
|
|
36
|
+
fs.writeFileSync(hookPath, content);
|
|
37
|
+
fs.chmodSync(hookPath, 0o755);
|
|
38
|
+
return { hookPath, already: false };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Remove only our marked block; leave the rest of the hook untouched.
|
|
42
|
+
function removeSignHook(root = process.cwd()) {
|
|
43
|
+
const hookPath = hookPathFor(root);
|
|
44
|
+
let content = '';
|
|
45
|
+
try { content = fs.readFileSync(hookPath, 'utf8'); } catch { return { hookPath, removed: false }; }
|
|
46
|
+
if (!content.includes(MARKER)) return { hookPath, removed: false };
|
|
47
|
+
const cleaned = content.replace(HOOK_BLOCK, '');
|
|
48
|
+
if (cleaned.trim() === '#!/bin/sh') {
|
|
49
|
+
fs.unlinkSync(hookPath);
|
|
50
|
+
} else {
|
|
51
|
+
fs.writeFileSync(hookPath, cleaned);
|
|
52
|
+
}
|
|
53
|
+
return { hookPath, removed: true };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function signStatus(root = process.cwd()) {
|
|
57
|
+
const hookPath = hookPathFor(root);
|
|
58
|
+
let content = '';
|
|
59
|
+
try { content = fs.readFileSync(hookPath, 'utf8'); } catch {}
|
|
60
|
+
return { hookPath, installed: content.includes(MARKER) };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function signCommand(sub) {
|
|
64
|
+
const rel = (p) => path.relative(process.cwd(), p);
|
|
65
|
+
if (sub === 'off' || sub === 'remove') {
|
|
66
|
+
const { hookPath, removed } = removeSignHook();
|
|
67
|
+
console.log(removed
|
|
68
|
+
? `\n ✓ atris co-author removed: ${rel(hookPath)}\n`
|
|
69
|
+
: `\n nothing to remove — atris co-author is not installed\n`);
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
if (sub === 'status') {
|
|
73
|
+
const { hookPath, installed } = signStatus();
|
|
74
|
+
console.log(installed
|
|
75
|
+
? `\n ✓ atris co-author is on: ${rel(hookPath)}\n every commit gets: ${TRAILER}\n`
|
|
76
|
+
: `\n atris co-author is off — run 'atris sign' to turn it on\n`);
|
|
77
|
+
return installed ? 0 : 1;
|
|
78
|
+
}
|
|
79
|
+
if (sub && sub !== 'on' && sub !== 'install') {
|
|
80
|
+
console.log(`\n atris sign — co-author trailer on every commit\n\n atris sign install the prepare-commit-msg hook\n atris sign off remove it\n atris sign status check whether it's installed\n\n While installed, any commit in a repo with an atris/ folder gets:\n ${TRAILER}\n`);
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
const { hookPath, already } = installSignHook();
|
|
84
|
+
console.log(already
|
|
85
|
+
? `\n already installed: ${rel(hookPath)}\n`
|
|
86
|
+
: `\n ✓ atris co-author installed: ${rel(hookPath)}\n every commit here now gets: ${TRAILER}\n`);
|
|
87
|
+
return 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = { signCommand, installSignHook, removeSignHook, signStatus, TRAILER };
|