atris 3.30.12 → 3.32.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.
Files changed (64) hide show
  1. package/AGENTS.md +16 -3
  2. package/FOR_AGENTS.md +81 -0
  3. package/README.md +6 -4
  4. package/atris/CLAUDE.md +8 -0
  5. package/atris/atris.md +51 -19
  6. package/atris/skills/README.md +1 -0
  7. package/atris/skills/blocks/SKILL.md +134 -0
  8. package/atris/skills/clawhub/philosophy-of-work/SKILL.md +56 -0
  9. package/atris/skills/youtube/SKILL.md +31 -11
  10. package/atris.md +15 -0
  11. package/ax +189 -7
  12. package/bin/atris.js +273 -225
  13. package/commands/autoland.js +379 -0
  14. package/commands/autopilot-front.js +273 -0
  15. package/commands/autopilot.js +94 -4
  16. package/commands/business.js +1 -1
  17. package/commands/clean.js +72 -9
  18. package/commands/codex-goal.js +72 -22
  19. package/commands/computer.js +48 -3
  20. package/commands/gm.js +1 -1
  21. package/commands/harvest.js +179 -0
  22. package/commands/init.js +22 -1
  23. package/commands/land.js +442 -0
  24. package/commands/loop-front.js +122 -4
  25. package/commands/member.js +551 -19
  26. package/commands/mission.js +3674 -278
  27. package/commands/play.js +1 -1
  28. package/commands/pulse.js +65 -7
  29. package/commands/run-front.js +144 -0
  30. package/commands/run.js +10 -7
  31. package/commands/sign.js +90 -0
  32. package/commands/strings.js +301 -0
  33. package/commands/task.js +782 -108
  34. package/commands/truth.js +171 -0
  35. package/commands/xp.js +32 -8
  36. package/commands/youtube.js +72 -5
  37. package/decks/README.md +6 -12
  38. package/lib/auto-accept-certified.js +10 -0
  39. package/lib/autoland.js +391 -0
  40. package/lib/context-gatherer.js +0 -8
  41. package/lib/mission-artifact.js +504 -0
  42. package/lib/mission-room.js +846 -0
  43. package/lib/next-moves.js +212 -6
  44. package/lib/operator-next.js +7 -0
  45. package/lib/pulse.js +78 -4
  46. package/lib/runner-command.js +51 -20
  47. package/lib/runs-prune.js +242 -0
  48. package/lib/task-db.js +19 -2
  49. package/lib/task-proof.js +1 -1
  50. package/package.json +4 -4
  51. package/decks/atris-seed-pitch-v3.json +0 -118
  52. package/decks/atris-seed-pitch-v4-skeleton.json +0 -106
  53. package/decks/atris-seed-pitch-v5.json +0 -109
  54. package/decks/atris-seed-pitch-v6.json +0 -137
  55. package/decks/atris-seed-pitch-v7.json +0 -133
  56. package/decks/mark-pincus-narrative.json +0 -102
  57. package/decks/mark-pincus-sourcery.json +0 -94
  58. package/decks/yash-applied-compute-detailed.json +0 -150
  59. package/decks/yash-applied-compute-generalist.json +0 -82
  60. package/decks/yash-applied-compute-narrative.json +0 -54
  61. package/lib/ax-chat-input.js +0 -164
  62. package/lib/ax-goal.js +0 -307
  63. package/lib/ax-prefs.js +0 -63
  64. package/lib/ax-shimmer.js +0 -63
package/commands/play.js CHANGED
@@ -417,7 +417,7 @@ function modeState(args = []) {
417
417
  prompt: latestMessage(events),
418
418
  } : null,
419
419
  proof_recipe: proofRecipe(),
420
- xp_rule: 'AgentXP lands only after a useful artifact has verifier proof and review accepts it.',
420
+ xp_rule: 'AgentXP is awarded only after a useful artifact has verifier proof and accepted review.',
421
421
  global_sync_rule: AGENTXP_GLOBAL_SYNC_RULE,
422
422
  leaderboard_url: AGENTXP_LEADERBOARD_URL,
423
423
  next_commands: commandList,
package/commands/pulse.js CHANGED
@@ -18,6 +18,7 @@ const os = require('os');
18
18
  const path = require('path');
19
19
  const { spawnSync } = require('child_process');
20
20
  const pulse = require('../lib/pulse');
21
+ const { DEFAULT_CLAUDE_RUNNER_MODEL } = require('../lib/runner-command');
21
22
 
22
23
  function hasFlag(args, name) {
23
24
  return args.includes(name);
@@ -51,6 +52,7 @@ Options:
51
52
  --verify "<cmd>" Verifier for changed-work ticks (default: npm test)
52
53
  --cadence "<cron>" Cron cadence for install
53
54
  --days <n> Auto-expire installed heartbeat after n days
55
+ --hours <n> Auto-expire installed heartbeat after n hours
54
56
  --model <id> Runner model alias/id for installed heartbeat
55
57
  --runner-profile <n> Runner profile for installed heartbeat (e.g. atris-fast)
56
58
  --runner-bin <path> Runner binary for installed heartbeat
@@ -197,6 +199,26 @@ function tickCommand(args, root = process.cwd()) {
197
199
  }
198
200
 
199
201
  const tickIndex = pulse.nextTickIndex(root);
202
+ const signalExitCode = { SIGINT: 130, SIGTERM: 143 };
203
+ let interrupted = false;
204
+ const finishInterrupted = (signal) => {
205
+ if (interrupted) return;
206
+ interrupted = true;
207
+ try {
208
+ pulse.appendPulseReceipt(root, pulse.buildInterruptedPulseReceipt({
209
+ tickIndex,
210
+ signal,
211
+ startedAt,
212
+ prevTickStale: priorStale.stale,
213
+ }));
214
+ } catch {}
215
+ try {
216
+ pulse.releaseLock(root);
217
+ } catch {}
218
+ process.exit(signalExitCode[signal] || 1);
219
+ };
220
+ process.once('SIGINT', finishInterrupted);
221
+ process.once('SIGTERM', finishInterrupted);
200
222
 
201
223
  // 'started' receipt - if the tick dies after this, the orphan surfaces as a ghost.
202
224
  pulse.appendPulseReceipt(root, pulse.buildPulseReceipt({
@@ -260,7 +282,7 @@ function tickCommand(args, root = process.cwd()) {
260
282
  what,
261
283
  changedFiles,
262
284
  elapsedMs,
263
- model: process.env.ATRIS_RUNNER_MODEL || process.env.ATRIS_CLAUDE_MODEL || 'opus',
285
+ model: process.env.ATRIS_RUNNER_MODEL || process.env.ATRIS_CLAUDE_MODEL || DEFAULT_CLAUDE_RUNNER_MODEL,
264
286
  }));
265
287
  scorecardWritten = true;
266
288
  }
@@ -301,6 +323,8 @@ function tickCommand(args, root = process.cwd()) {
301
323
  if (!asJson) process.stdout.write(`pulse tick #${tickIndex} crashed: ${out.error}\n`);
302
324
  return emit(out, asJson);
303
325
  } finally {
326
+ process.removeListener('SIGINT', finishInterrupted);
327
+ process.removeListener('SIGTERM', finishInterrupted);
304
328
  pulse.releaseLock(root);
305
329
  }
306
330
  }
@@ -372,14 +396,45 @@ function resolveEngineBinDirs(extraBins = []) {
372
396
 
373
397
  function installCommand(args, root = process.cwd()) {
374
398
  const asJson = wantsJson(args);
375
- const cron = readFlag(args, '--cadence', pulse.DEFAULT_CADENCE_CRON);
376
- const days = Math.max(1, Number(readFlag(args, '--days', '7')) || 7);
399
+ const cadenceInput = readFlag(args, '--cadence', pulse.DEFAULT_CADENCE_CRON);
400
+ let cron;
401
+ try {
402
+ cron = pulse.normalizeCronCadence(cadenceInput);
403
+ } catch (error) {
404
+ const out = {
405
+ ok: false,
406
+ action: 'pulse_install',
407
+ reason: 'invalid_cadence',
408
+ detail: error && error.message ? error.message : String(error),
409
+ cadence: cadenceInput,
410
+ };
411
+ if (!asJson) process.stdout.write(`pulse install failed: ${out.detail}\n`);
412
+ return emit(out, asJson);
413
+ }
414
+ let expiry;
415
+ try {
416
+ expiry = pulse.normalizeExpiryDuration({
417
+ hours: readFlag(args, '--hours', null),
418
+ days: readFlag(args, '--days', '7'),
419
+ });
420
+ } catch (error) {
421
+ const out = {
422
+ ok: false,
423
+ action: 'pulse_install',
424
+ reason: 'invalid_expiry',
425
+ detail: error && error.message ? error.message : String(error),
426
+ hours: readFlag(args, '--hours', null),
427
+ days: readFlag(args, '--days', '7'),
428
+ };
429
+ if (!asJson) process.stdout.write(`pulse install failed: ${out.detail}\n`);
430
+ return emit(out, asJson);
431
+ }
377
432
  const verifyCmd = readFlag(args, '--verify', 'npm test');
378
- const model = readFlag(args, '--model', process.env.ATRIS_RUNNER_MODEL || process.env.ATRIS_CLAUDE_MODEL || 'opus');
433
+ const model = readFlag(args, '--model', process.env.ATRIS_RUNNER_MODEL || process.env.ATRIS_CLAUDE_MODEL || DEFAULT_CLAUDE_RUNNER_MODEL);
379
434
  const runnerProfile = readFlag(args, '--runner-profile', process.env.ATRIS_RUNNER_PROFILE || '');
380
435
  const runnerBin = readFlag(args, '--runner-bin', process.env.ATRIS_RUNNER_BIN || process.env.ATRIS_CLAUDE_BIN || '');
381
436
  const runnerCommandTemplate = readFlag(args, '--runner-template', process.env.ATRIS_RUNNER_COMMAND_TEMPLATE || process.env.ATRIS_CLAUDE_COMMAND_TEMPLATE || '');
382
- const deadlineEpoch = Math.floor(Date.now() / 1000) + days * 86400;
437
+ const deadlineEpoch = Math.floor(Date.now() / 1000) + expiry.seconds;
383
438
 
384
439
  fs.mkdirSync(STATE_HOME, { recursive: true });
385
440
  const scriptPath = path.join(STATE_HOME, 'tick.sh');
@@ -414,7 +469,10 @@ function installCommand(args, root = process.cwd()) {
414
469
  script_path: scriptPath,
415
470
  crontab_line: line,
416
471
  cadence: cron,
417
- expires_in_days: days,
472
+ cadence_input: cadenceInput,
473
+ expires_in_days: expiry.days,
474
+ expires_in_hours: expiry.hours,
475
+ expires_in_seconds: expiry.seconds,
418
476
  deadline_epoch: deadlineEpoch,
419
477
  runner_profile: runnerProfile || null,
420
478
  runner_bin: runnerBin || null,
@@ -425,7 +483,7 @@ function installCommand(args, root = process.cwd()) {
425
483
  process.stdout.write([
426
484
  `pulse installed. heartbeat fires '${cron}' against ${root}.`,
427
485
  `script: ${scriptPath}`,
428
- `auto-expires in ${days} days. stop early: atris pulse uninstall`,
486
+ `auto-expires in ${expiry.hours ? `${expiry.hours} hours` : `${expiry.days} days`}. stop early: atris pulse uninstall`,
429
487
  ].join('\n') + '\n');
430
488
  } else {
431
489
  process.stdout.write(`pulse install failed to write crontab: ${apply.stderr || apply.status}\nscript written to ${scriptPath}; add this line to your crontab manually:\n${line}\n`);
@@ -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
@@ -15,7 +15,7 @@ const { parseTodo } = require('../lib/todo');
15
15
  const {
16
16
  buildRunnerCommand,
17
17
  buildRunnerAvailabilityCommand,
18
- resolveClaudeRunnerBin,
18
+ runnerAvailabilityFailureMessage,
19
19
  } = require('../lib/runner-command');
20
20
  const { cleanAtris } = require('./clean');
21
21
 
@@ -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
- try {
386
- execSync(buildRunnerAvailabilityCommand(), { stdio: 'pipe' });
387
- } catch {
388
- console.error(`${resolveClaudeRunnerBin()} CLI not found. Set ATRIS_RUNNER_BIN (or legacy ATRIS_CLAUDE_BIN), or install the configured runner first.`);
389
- process.exit(1);
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('');
@@ -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 };