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
@@ -2,7 +2,7 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const crypto = require('crypto');
4
4
  const os = require('os');
5
- const { execFileSync } = require('child_process');
5
+ const { execFileSync, spawnSync } = require('child_process');
6
6
  const { loadCredentials } = require('../utils/auth');
7
7
  const { apiRequestJson } = require('../utils/api');
8
8
  const { runAliveTick } = require('../lib/member-alive');
@@ -406,39 +406,275 @@ function missionPurpose(paths) {
406
406
  };
407
407
  }
408
408
 
409
+ const MEMBER_RUN_RUNNABLE_STATUSES = new Set(['planning', 'running', 'ready']);
410
+
411
+ function memberRunMissionMap() {
412
+ try {
413
+ return require('./mission').loadMissionMap(process.cwd());
414
+ } catch {
415
+ return new Map();
416
+ }
417
+ }
418
+
419
+ function memberRunRunnableMissionId(candidateId, missionMap) {
420
+ const id = String(candidateId || '').trim();
421
+ if (!id) return '';
422
+ const mission = missionMap.get(id);
423
+ // Unknown here can mean a worktree-held mission; let mission run decide.
424
+ if (!mission) return id;
425
+ return MEMBER_RUN_RUNNABLE_STATUSES.has(mission.status) ? id : '';
426
+ }
427
+
428
+ function memberRunOwnedActiveMissionId(name, missionMap) {
429
+ // Discovery fallback when the member's own pointers are stale: resume the
430
+ // member's newest moving mission. ready is excluded — it waits for review.
431
+ const owner = String(name || '').trim().toLowerCase();
432
+ const candidates = Array.from(missionMap.values())
433
+ .filter((mission) => String(mission?.owner || '').trim().toLowerCase() === owner)
434
+ .filter((mission) => mission.status === 'running' || mission.status === 'planning')
435
+ .filter((mission) => !/^decide and start the next useful mission after:/i.test(String(mission?.objective || '')))
436
+ .sort((a, b) => {
437
+ if (a.status !== b.status) return a.status === 'running' ? -1 : 1;
438
+ return String(b.updated_at || b.created_at || '').localeCompare(String(a.updated_at || a.created_at || ''));
439
+ });
440
+ return candidates[0]?.id || '';
441
+ }
442
+
409
443
  function resolveMemberRunMissionId(name, args = []) {
410
444
  const override = readFlag(args, '--mission', '') || readFlag(args, '--mission-id', '');
411
445
  if (override) return override;
412
446
 
413
447
  const paths = requireMemberDir(name);
448
+ const missionMap = memberRunMissionMap();
414
449
  const purpose = missionPurpose(paths);
415
- if (purpose.runtimeMission?.id) return purpose.runtimeMission.id;
450
+ const runtimeId = memberRunRunnableMissionId(purpose.runtimeMission?.id, missionMap);
451
+ if (runtimeId) return runtimeId;
416
452
 
417
453
  const goals = loadMemberGoals(name, paths);
418
454
  const goal = activeGoal(goals);
419
- return goal?.mission_id || '';
455
+ const goalId = memberRunRunnableMissionId(goal?.mission_id, missionMap);
456
+ if (goalId) return goalId;
457
+
458
+ return memberRunOwnedActiveMissionId(paths.storageName || name, missionMap);
459
+ }
460
+
461
+ const MEMBER_RUN_START_VALUE_FLAGS = [
462
+ '--mission',
463
+ '--mission-id',
464
+ '--owner',
465
+ '--runner',
466
+ '--lane',
467
+ '--cadence',
468
+ '--verify',
469
+ '--stop',
470
+ '--model',
471
+ '--max-ticks',
472
+ '--max-wall',
473
+ '--minutes',
474
+ '--hours',
475
+ '--industry',
476
+ '--domain',
477
+ '--value',
478
+ '--outcome',
479
+ '--truth',
480
+ '--proof',
481
+ ];
482
+
483
+ const MEMBER_RUN_START_BOOLEAN_FLAGS = [
484
+ '--json',
485
+ '--worktree',
486
+ '--shared-checkout',
487
+ '--no-worktree',
488
+ '--always-on',
489
+ '--xp-task',
490
+ '--agent-xp',
491
+ '--spend-full-budget',
492
+ '--use-whole-budget',
493
+ '--stop-when-done',
494
+ '--choose-work',
495
+ '--auto',
496
+ ];
497
+
498
+ function memberRunMissionText(args = []) {
499
+ return stripKnownFlags(args, MEMBER_RUN_START_VALUE_FLAGS, MEMBER_RUN_START_BOOLEAN_FLAGS).join(' ').trim();
500
+ }
501
+
502
+ function cleanMemberRunPhrase(value) {
503
+ return String(value || '').replace(/\s+/g, ' ').trim();
504
+ }
505
+
506
+ function memberRunDomain(args = []) {
507
+ return cleanMemberRunPhrase(readFlag(args, '--industry', '') || readFlag(args, '--domain', '')) || 'this workspace and its users';
508
+ }
509
+
510
+ function memberRunOutcome(args = []) {
511
+ return cleanMemberRunPhrase(readFlag(args, '--outcome', '') || readFlag(args, '--value', ''))
512
+ || 'revenue, reliability, speed, security, workflow clarity, or customer trust';
513
+ }
514
+
515
+ function memberRunTruthRule(args = []) {
516
+ return cleanMemberRunPhrase(readFlag(args, '--truth', '') || readFlag(args, '--proof', ''))
517
+ || 'say what changed, what was checked, and what is still unproven';
518
+ }
519
+
520
+ function memberRunUsefulTarget() {
521
+ try {
522
+ const { nextMoves: pickNextMoves, isGenericInboxPlaceholder } = require('../lib/next-moves');
523
+ const moves = pickNextMoves(process.cwd(), 5);
524
+ const candidate = moves.find((move) => {
525
+ const title = cleanMemberRunPhrase(move?.title);
526
+ if (!title) return false;
527
+ if (isGenericInboxPlaceholder(title)) return false;
528
+ if (/^mission xp\s*:/i.test(title)) return false;
529
+ if (/^decide and start the next useful mission after:/i.test(title)) return false;
530
+ return true;
531
+ });
532
+ return candidate?.title ? cleanMemberRunPhrase(candidate.title) : '';
533
+ } catch {
534
+ return '';
535
+ }
536
+ }
537
+
538
+ function memberRunAutoMissionText(name, args = []) {
539
+ const domain = memberRunDomain(args);
540
+ const outcome = memberRunOutcome(args);
541
+ const truth = memberRunTruthRule(args);
542
+ const target = memberRunUsefulTarget();
543
+ const work = target
544
+ ? `Start with this concrete candidate: ${target}.`
545
+ : 'First inspect current Atris state, then choose one bounded useful task.';
546
+ return [
547
+ `Run ${name} on meaningful work for ${domain}.`,
548
+ work,
549
+ `Useful means it improves ${outcome}.`,
550
+ `Truth rule: ${truth}.`,
551
+ 'Explain the choice in plain language, prove the result before Review, and stop instead of doing fake busywork when no useful task exists.',
552
+ ].join(' ');
553
+ }
554
+
555
+ function memberRunBudgetSeconds(args = []) {
556
+ const hours = readNumberFlag(args, '--hours', null);
557
+ if (Number.isFinite(hours) && hours > 0) return Math.round(hours * 3600);
558
+ const minutes = readNumberFlag(args, '--minutes', null);
559
+ if (Number.isFinite(minutes) && minutes > 0) return Math.round(minutes * 60);
560
+ return null;
561
+ }
562
+
563
+ function pushFlagValue(out, args, name) {
564
+ const value = readFlag(args, name, '');
565
+ if (value) out.push(name, String(value));
566
+ }
567
+
568
+ function pushFlagWhenPresent(out, args, name) {
569
+ if (hasFlag(args, name)) out.push(name);
570
+ }
571
+
572
+ function startMemberRunMission(name, missionText, args = []) {
573
+ const paths = requireMemberDir(name);
574
+ const owner = paths.storageName || name;
575
+ const startArgs = [
576
+ 'mission',
577
+ 'start',
578
+ missionText,
579
+ '--owner',
580
+ owner,
581
+ '--runner',
582
+ readFlag(args, '--runner', 'codex_goal'),
583
+ '--lane',
584
+ readFlag(args, '--lane', 'workspace'),
585
+ ];
586
+ pushFlagValue(startArgs, args, '--cadence');
587
+ pushFlagValue(startArgs, args, '--verify');
588
+ pushFlagValue(startArgs, args, '--stop');
589
+ pushFlagValue(startArgs, args, '--model');
590
+ pushFlagValue(startArgs, args, '--max-wall');
591
+ pushFlagValue(startArgs, args, '--minutes');
592
+ pushFlagValue(startArgs, args, '--hours');
593
+ pushFlagValue(startArgs, args, '--base');
594
+ pushFlagWhenPresent(startArgs, args, '--always-on');
595
+ pushFlagWhenPresent(startArgs, args, '--xp-task');
596
+ pushFlagWhenPresent(startArgs, args, '--agent-xp');
597
+ pushFlagWhenPresent(startArgs, args, '--spend-full-budget');
598
+ pushFlagWhenPresent(startArgs, args, '--use-whole-budget');
599
+ pushFlagWhenPresent(startArgs, args, '--stop-when-done');
600
+ if (!hasFlag(args, '--shared-checkout') && !hasFlag(args, '--no-worktree')) startArgs.push('--worktree');
601
+ if (hasFlag(args, '--json')) startArgs.push('--json');
602
+
603
+ const cliPath = path.join(__dirname, '..', 'bin', 'atris.js');
604
+ try {
605
+ execFileSync(process.execPath, [cliPath, ...startArgs], {
606
+ cwd: process.cwd(),
607
+ stdio: 'inherit',
608
+ });
609
+ } catch (error) {
610
+ process.exitCode = Number(error?.status) || 1;
611
+ }
612
+ }
613
+
614
+ // atris member ping <name> "<message>" — talk to an always-on member mid-run.
615
+ // Finds the member's most recently touched live mission (including --worktree
616
+ // missions in sibling checkouts) and leaves the note its next tick consumes.
617
+ function memberPing(name, ...args) {
618
+ if (!name || name === '--help' || name === '-h') {
619
+ console.log('Usage: atris member ping <name> "<message>" [--json]');
620
+ console.log('Leaves a note on the member\'s active mission; the next tick reads it as operator direction.');
621
+ return;
622
+ }
623
+ const text = args.filter((a) => !a.startsWith('--')).join(' ').trim();
624
+ if (!text) {
625
+ console.error('atris member ping: message required');
626
+ process.exit(2);
627
+ }
628
+ const missionMod = require('./mission');
629
+ const terminal = new Set(['complete', 'stopped', 'failed']);
630
+ const candidates = [
631
+ ...missionMod.listMissions(process.cwd()),
632
+ ...missionMod.listWorktreeRollupMissions(process.cwd()),
633
+ ]
634
+ .filter((m) => m && m.owner === name && !terminal.has(m.status))
635
+ .sort((a, b) => String(b.updated_at || b.created_at || '').localeCompare(String(a.updated_at || a.created_at || '')));
636
+ if (!candidates.length) {
637
+ console.error(`atris member ping: no live mission owned by "${name}". Start one: atris member run ${name} "<objective>"`);
638
+ process.exit(1);
639
+ }
640
+ const passthrough = args.filter((a) => a.startsWith('--'));
641
+ return missionMod.pingMission([candidates[0].id, text, ...passthrough]);
420
642
  }
421
643
 
422
644
  function memberRun(name, ...args) {
423
645
  if (!name || name === '--help' || name === '-h' || hasFlag(args, '--help') || hasFlag(args, '-h')) {
424
- console.log('Usage: atris member run <name> [mission run flags]');
425
- console.log('Example: atris member run block-builder --max-ticks 1 --max-wall 900 --json');
426
- console.log('Override: atris member run block-builder --mission <mission-id> --json');
646
+ console.log('Usage: atris member run <name> ["mission text"] [--minutes N|--hours N] [--json]');
647
+ console.log('New mission: atris member run growth "Improve onboarding proof" --minutes 30 --json');
648
+ console.log('Choose work: atris member run growth --industry logistics --value "reduce support time" --minutes 30 --json');
649
+ console.log('Existing mission: atris member run growth --mission <mission-id> --max-ticks 1 --json');
650
+ console.log('Meaning: if mission text is omitted, the member chooses one useful bounded task from Atris state.');
651
+ console.log('Truth: useful work must improve revenue, reliability, speed, security, clarity, or trust, then show plain proof.');
652
+ console.log('Isolation: new missions use --worktree by default; add --shared-checkout to stay here.');
653
+ return;
654
+ }
655
+
656
+ const missionText = memberRunMissionText(args);
657
+ const hasMissionOverride = Boolean(readFlag(args, '--mission', '') || readFlag(args, '--mission-id', ''));
658
+ if (missionText && !hasMissionOverride) {
659
+ startMemberRunMission(name, missionText, args);
427
660
  return;
428
661
  }
429
662
 
430
663
  const missionId = resolveMemberRunMissionId(name, args);
431
664
  if (!missionId) {
432
- console.error(`No active Mission Runtime found for member "${name}".`);
433
- console.error(`Try: atris member goal-from-mission ${name} --json`);
434
- console.error(`Or: atris mission start "..." --owner ${name}`);
435
- process.exitCode = 1;
665
+ startMemberRunMission(name, memberRunAutoMissionText(name, args), args);
436
666
  return;
437
667
  }
438
668
 
439
- const runArgs = stripKnownFlags(args, ['--mission', '--mission-id']);
440
- if (!readFlag(runArgs, '--max-ticks', '')) runArgs.push('--max-ticks', '1');
441
- if (!readFlag(runArgs, '--max-wall', '')) runArgs.push('--max-wall', '900');
669
+ const runArgs = stripKnownFlags(args, ['--mission', '--mission-id', '--minutes', '--hours'], ['--worktree', '--shared-checkout', '--no-worktree']);
670
+ const budgetSeconds = memberRunBudgetSeconds(args);
671
+ // A timed run is a loop contract, not one tick: keep picking the next useful
672
+ // move until the wall clock (--max-wall) spends the budget. Only untimed runs
673
+ // default to a single tick.
674
+ if (!readFlag(runArgs, '--max-ticks', '')) {
675
+ runArgs.push('--max-ticks', budgetSeconds ? String(Math.max(4, Math.ceil(budgetSeconds / 300))) : '1');
676
+ }
677
+ if (!readFlag(runArgs, '--max-wall', '')) runArgs.push('--max-wall', String(budgetSeconds || 900));
442
678
 
443
679
  const cliPath = path.join(__dirname, '..', 'bin', 'atris.js');
444
680
  try {
@@ -2023,6 +2259,247 @@ function memberLoopPaths(name) {
2023
2259
  lockPath: path.join(stateDir, `${name}.lock.json`),
2024
2260
  stopPath: path.join(stateDir, `${name}.stop.json`),
2025
2261
  latestPath: path.join(stateDir, `${name}.latest.json`),
2262
+ cronScriptPath: path.join(stateDir, `${name}.hourly-alive.sh`),
2263
+ };
2264
+ }
2265
+
2266
+ function memberAliveCronMarker(name) {
2267
+ return `ATRIS_MEMBER_ALIVE_${String(name || '').replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}`;
2268
+ }
2269
+
2270
+ function shellSingleQuote(value) {
2271
+ return `'${String(value || '').replace(/'/g, "'\\''")}'`;
2272
+ }
2273
+
2274
+ function resolveAtrisCommand() {
2275
+ return path.join(__dirname, '..', 'bin', 'atris.js');
2276
+ }
2277
+
2278
+ function buildMemberAliveCronScript(name, args = [], paths = memberLoopPaths(name)) {
2279
+ const root = process.cwd();
2280
+ const atrisBin = resolveAtrisCommand();
2281
+ const logDir = path.join(paths.stateDir, 'logs');
2282
+ const command = [
2283
+ process.execPath,
2284
+ atrisBin,
2285
+ 'member',
2286
+ 'alive',
2287
+ name,
2288
+ '--hourly',
2289
+ '--forever',
2290
+ '--ticks',
2291
+ '1',
2292
+ '--json',
2293
+ ...(hasFlag(args, '--execute') ? ['--execute'] : []),
2294
+ ...(hasFlag(args, '--confirm-autonomy-policy') ? ['--confirm-autonomy-policy'] : []),
2295
+ ];
2296
+ const agent = String(readFlag(args, '--agent', '') || '').trim();
2297
+ const model = String(readFlag(args, '--model', '') || '').trim();
2298
+ const maxWall = String(readFlag(args, '--operate-max-wall', readFlag(args, '--max-wall', '')) || '').trim();
2299
+ const autoAcceptLimit = String(readFlag(args, '--auto-accept-limit', '') || '').trim();
2300
+ if (agent) command.push('--agent', agent);
2301
+ if (model) command.push('--model', model);
2302
+ if (maxWall) command.push('--operate-max-wall', maxWall);
2303
+ if (autoAcceptLimit) command.push('--auto-accept-limit', autoAcceptLimit);
2304
+ const renderedCommand = command.map(shellSingleQuote).join(' ');
2305
+ return {
2306
+ command,
2307
+ rendered_command: renderedCommand,
2308
+ script: [
2309
+ '#!/bin/zsh',
2310
+ 'set -u',
2311
+ `ROOT=${shellSingleQuote(root)}`,
2312
+ `LOG_DIR=${shellSingleQuote(logDir)}`,
2313
+ 'mkdir -p "$LOG_DIR"',
2314
+ 'stamp="$(date +"%Y%m%d-%H%M%S")"',
2315
+ 'cd "$ROOT" || exit 1',
2316
+ 'export ATRIS_SKIP_UPDATE_CHECK=1',
2317
+ `${renderedCommand} >> "$LOG_DIR/$stamp.log" 2>&1`,
2318
+ 'echo "done: $(date -Iseconds) exit=$?" >> "$LOG_DIR/$stamp.log"',
2319
+ '',
2320
+ ].join('\n'),
2321
+ };
2322
+ }
2323
+
2324
+ function memberAliveCronCadence(args = []) {
2325
+ const pulse = require('../lib/pulse');
2326
+ const raw = hasFlag(args, '--hourly') || hasFlag(args, '--every-hour')
2327
+ ? 'hourly'
2328
+ : readFlag(args, '--cadence', readFlag(args, '--cron', readFlag(args, '--interval', 'hourly')));
2329
+ return pulse.normalizeCronCadence(raw);
2330
+ }
2331
+
2332
+ function buildMemberAliveCrontabLine(name, args = [], paths = memberLoopPaths(name)) {
2333
+ return `${memberAliveCronCadence(args)} ${paths.cronScriptPath} # ${memberAliveCronMarker(name)}`;
2334
+ }
2335
+
2336
+ function dirtyGitStatus(root = process.cwd()) {
2337
+ const result = spawnSync('git', ['status', '--porcelain'], { cwd: root, encoding: 'utf8', timeout: 10000 });
2338
+ if (result.status !== 0) {
2339
+ return { inside_git: false, dirty: false, dirty_count: 0, dirty_files: [] };
2340
+ }
2341
+ const dirtyFiles = String(result.stdout || '')
2342
+ .split(/\r?\n/)
2343
+ .map((line) => line.trim())
2344
+ .filter(Boolean);
2345
+ return {
2346
+ inside_git: true,
2347
+ dirty: dirtyFiles.length > 0,
2348
+ dirty_count: dirtyFiles.length,
2349
+ dirty_files: dirtyFiles.slice(0, 12),
2350
+ };
2351
+ }
2352
+
2353
+ function memberAliveInstallPreview(name, args = [], paths = memberLoopPaths(name), script = buildMemberAliveCronScript(name, args, paths)) {
2354
+ return {
2355
+ schema: 'atris.member_alive_install_preview.v1',
2356
+ member: name,
2357
+ cadence: memberAliveCronCadence(args),
2358
+ mission_text: memberRunAutoMissionText(name, args),
2359
+ run: script.rendered_command,
2360
+ verify: `atris member alive ${name} --status --json`,
2361
+ stop_when: `Run atris member alive ${name} --uninstall, or create the stop marker at ${paths.stopPath}.`,
2362
+ receipts: path.join(paths.stateDir, 'logs'),
2363
+ };
2364
+ }
2365
+
2366
+ function memberAliveInstallPreviewLines(preview) {
2367
+ return [
2368
+ 'Every hour this will:',
2369
+ ` run: ${preview.run}`,
2370
+ ` verify: ${preview.verify}`,
2371
+ ` stop when: ${preview.stop_when}`,
2372
+ ` receipts: ${preview.receipts}`,
2373
+ ];
2374
+ }
2375
+
2376
+ function installMemberAliveCron(name, args = []) {
2377
+ const asJson = hasFlag(args, '--json');
2378
+ const dryRun = hasFlag(args, '--dry-run');
2379
+ if (hasFlag(args, '--execute') && !hasFlag(args, '--confirm-autonomy-policy')) {
2380
+ const payload = {
2381
+ ok: false,
2382
+ action: 'alive_install',
2383
+ member: name,
2384
+ status: 'blocked',
2385
+ reason: 'execute_requires_confirm_autonomy_policy',
2386
+ };
2387
+ printJsonOrText(payload, [
2388
+ `Install blocked for ${name}: execute requires --confirm-autonomy-policy.`,
2389
+ ], asJson);
2390
+ process.exitCode = 1;
2391
+ return payload;
2392
+ }
2393
+ const paths = memberLoopPaths(name);
2394
+ fs.mkdirSync(paths.stateDir, { recursive: true });
2395
+ const script = buildMemberAliveCronScript(name, args, paths);
2396
+ const crontabLine = buildMemberAliveCrontabLine(name, args, paths);
2397
+ const preview = memberAliveInstallPreview(name, args, paths, script);
2398
+ if (hasFlag(args, '--execute') && !dryRun) {
2399
+ const gitStatus = dirtyGitStatus();
2400
+ if (gitStatus.dirty) {
2401
+ const payload = {
2402
+ ok: false,
2403
+ action: 'alive_install',
2404
+ member: name,
2405
+ status: 'blocked',
2406
+ reason: 'install_requires_clean_git',
2407
+ git: gitStatus,
2408
+ preview,
2409
+ };
2410
+ printJsonOrText(payload, [
2411
+ `Install blocked for ${name}: commit or clean local changes before installing an execute loop.`,
2412
+ ...memberAliveInstallPreviewLines(preview),
2413
+ ], asJson);
2414
+ process.exitCode = 1;
2415
+ return payload;
2416
+ }
2417
+ }
2418
+ fs.writeFileSync(paths.cronScriptPath, script.script, { mode: 0o755 });
2419
+ const marker = memberAliveCronMarker(name);
2420
+ let installed = false;
2421
+ let error = null;
2422
+ if (!dryRun) {
2423
+ const existing = spawnSync('crontab', ['-l'], { encoding: 'utf8', timeout: 10000 });
2424
+ const current = existing.status === 0 ? existing.stdout : '';
2425
+ const kept = String(current || '').split(/\r?\n/).filter((line) => line.trim() && !line.includes(marker));
2426
+ const next = `${kept.join('\n')}${kept.length ? '\n' : ''}${crontabLine}\n`;
2427
+ const apply = spawnSync('crontab', ['-'], { input: next, encoding: 'utf8', timeout: 10000 });
2428
+ installed = apply.status === 0;
2429
+ if (!installed) error = apply.stderr || `crontab exited ${apply.status}`;
2430
+ }
2431
+ const payload = {
2432
+ ok: dryRun || installed,
2433
+ action: 'alive_install',
2434
+ member: name,
2435
+ cadence: 'hourly',
2436
+ forever: true,
2437
+ installed,
2438
+ dry_run: dryRun,
2439
+ script_path: paths.cronScriptPath,
2440
+ crontab_line: crontabLine,
2441
+ command: script.command,
2442
+ preview,
2443
+ stop_command: `atris member alive ${name} --uninstall`,
2444
+ error,
2445
+ };
2446
+ writeJsonFile(paths.latestPath, payload);
2447
+ printJsonOrText(payload, [
2448
+ `${dryRun ? 'Would install' : installed ? 'Installed' : 'Install failed'} hourly alive loop: ${name}`,
2449
+ `Cron: ${crontabLine}`,
2450
+ ...memberAliveInstallPreviewLines(preview),
2451
+ `Stop: ${payload.stop_command}`,
2452
+ ], asJson);
2453
+ if (!payload.ok) process.exitCode = 1;
2454
+ return payload;
2455
+ }
2456
+
2457
+ function uninstallMemberAliveCron(name, args = []) {
2458
+ const asJson = hasFlag(args, '--json');
2459
+ const paths = memberLoopPaths(name);
2460
+ const marker = memberAliveCronMarker(name);
2461
+ const existing = spawnSync('crontab', ['-l'], { encoding: 'utf8', timeout: 10000 });
2462
+ if (existing.status !== 0) {
2463
+ const payload = { ok: true, action: 'alive_uninstall', member: name, removed: false, reason: 'no_crontab' };
2464
+ printJsonOrText(payload, [`No hourly alive crontab found for ${name}.`], asJson);
2465
+ return payload;
2466
+ }
2467
+ const before = String(existing.stdout || '').split(/\r?\n/);
2468
+ const after = before.filter((line) => !line.includes(marker));
2469
+ const removed = after.length !== before.length;
2470
+ const apply = spawnSync('crontab', ['-'], { input: after.filter(Boolean).join('\n') + (after.filter(Boolean).length ? '\n' : ''), encoding: 'utf8', timeout: 10000 });
2471
+ const payload = {
2472
+ ok: apply.status === 0,
2473
+ action: 'alive_uninstall',
2474
+ member: name,
2475
+ removed,
2476
+ marker,
2477
+ script_path: paths.cronScriptPath,
2478
+ error: apply.status === 0 ? null : apply.stderr || `crontab exited ${apply.status}`,
2479
+ };
2480
+ printJsonOrText(payload, [
2481
+ removed ? `Removed hourly alive loop: ${name}` : `No hourly alive line found for ${name}.`,
2482
+ ], asJson);
2483
+ if (!payload.ok) process.exitCode = 1;
2484
+ return payload;
2485
+ }
2486
+
2487
+ function memberAliveCronStatus(name, paths = memberLoopPaths(name)) {
2488
+ const marker = memberAliveCronMarker(name);
2489
+ let crontabLine = null;
2490
+ const crontab = spawnSync('crontab', ['-l'], { encoding: 'utf8', timeout: 10000 });
2491
+ if (crontab.status === 0) {
2492
+ crontabLine = String(crontab.stdout || '').split(/\r?\n/).find((line) => line.includes(marker)) || null;
2493
+ }
2494
+ return {
2495
+ schema: 'atris.member_alive_cron_status.v1',
2496
+ marker,
2497
+ installed: Boolean(crontabLine),
2498
+ crontab_line: crontabLine,
2499
+ script_path: paths.cronScriptPath,
2500
+ script_exists: fs.existsSync(paths.cronScriptPath),
2501
+ install_command: `atris member alive ${name} --install --hourly --forever --execute --confirm-autonomy-policy --json`,
2502
+ uninstall_command: `atris member alive ${name} --uninstall`,
2026
2503
  };
2027
2504
  }
2028
2505
 
@@ -2145,6 +2622,11 @@ function stripAutoImproverTitleNoise(text) {
2145
2622
  function isAutoImproverGeneratedLogLine(line, relativePath = '') {
2146
2623
  const text = String(line || '').trim();
2147
2624
  if (!text) return false;
2625
+ if (/^[-*]?\s*Next tick will stop until a human looks at the error\.?$/i.test(text)) return true;
2626
+ if (/^[-*]?\s*Next tick will verify failed, halting\.?$/i.test(text)) return true;
2627
+ if (/^##\s+\d{1,2}:\d{2}\s+·\s+Task failed$/i.test(text)) return true;
2628
+ if (/^[-*]?\s*(status|action|state|verifier)\s*:\s*(failed|blocked)\s*$/i.test(text)) return true;
2629
+ if (/^[-*]?\s*(title|proof):\s*/i.test(text)) return true;
2148
2630
  if (String(relativePath || '').startsWith('atris/team/auto-improver/logs/') && /^-\s*summary:\s*/i.test(text)) return true;
2149
2631
  if (/\bauto[- ]improver\b/i.test(text) && /\b(dogfood|receipt|prevented|pain_)\b/i.test(text)) return true;
2150
2632
  if (/\bauto_improver\b/i.test(text)) return true;
@@ -2162,12 +2644,28 @@ function normalizeFailurePattern(line) {
2162
2644
  .trim());
2163
2645
  }
2164
2646
 
2647
+ function failureCoveredByPassLesson(line, passLessonText = '') {
2648
+ const text = String(line || '');
2649
+ if (!text || !passLessonText) return false;
2650
+ if (/spawnSync\s+\/bin\/sh\s+ETIMEDOUT/i.test(text)) {
2651
+ const target = text.match(/"([^"]+)"/)?.[1] || '';
2652
+ const targetTail = target ? target.split('/').slice(-2).join('/') : '';
2653
+ return /spawnSync\s+\/bin\/sh\s+ETIMEDOUT/i.test(passLessonText)
2654
+ && (!target || passLessonText.includes(target) || (targetTail && passLessonText.includes(targetTail)));
2655
+ }
2656
+ return false;
2657
+ }
2658
+
2165
2659
  function collectAutoImproverLogSignals(root) {
2166
2660
  const roots = [
2167
2661
  path.join(root, 'atris', 'logs'),
2168
2662
  path.join(root, 'atris', 'team'),
2169
2663
  path.join(root, 'atris', 'wiki'),
2170
2664
  ].filter((candidate) => fs.existsSync(candidate));
2665
+ const passLessonText = safeReadText(path.join(root, 'atris', 'lessons.md'), 500000)
2666
+ .split(/\r?\n/)
2667
+ .filter((line) => /\s+pass\s+—/.test(line))
2668
+ .join('\n');
2171
2669
  const failureRegex = /\b(error|failed|failure|blocked|timeout|regression|crash|missing proof|naraka|suffering)\b/i;
2172
2670
  const unclearRegex = /\b(tbd|unclear|unknown|needs user|needs owner|needs proof|no next|blocked)\b/i;
2173
2671
  const counts = new Map();
@@ -2203,6 +2701,7 @@ function collectAutoImproverLogSignals(root) {
2203
2701
  if (/^\s*[-*]?\s*check:\s/i.test(line)) continue;
2204
2702
  if (/\b(errors?|fail(?:ed|ures?)|blocked|timeouts?)\s*:\s*0\b/i.test(line)) continue;
2205
2703
  if (/\bblocked\s*(?:->|→|to)\s*ready\b/i.test(line)) continue;
2704
+ if (failureCoveredByPassLesson(line, passLessonText)) continue;
2206
2705
  if (unclearRegex.test(line)) {
2207
2706
  unclearNextActions += 1;
2208
2707
  unclearActions.push({
@@ -6900,6 +7399,14 @@ async function memberLoop(name, ...args) {
6900
7399
  const paths = memberLoopPaths(name);
6901
7400
  fs.mkdirSync(paths.stateDir, { recursive: true });
6902
7401
 
7402
+ if (hasFlag(args, '--install')) {
7403
+ return installMemberAliveCron(name, args);
7404
+ }
7405
+
7406
+ if (hasFlag(args, '--uninstall')) {
7407
+ return uninstallMemberAliveCron(name, args);
7408
+ }
7409
+
6903
7410
  if (status) {
6904
7411
  const active = readJsonIfExists(paths.lockPath);
6905
7412
  const latest = readJsonIfExists(paths.latestPath);
@@ -6910,12 +7417,14 @@ async function memberLoop(name, ...args) {
6910
7417
  active: Boolean(active && Number(active.expires_at_ms || 0) > Date.now() && isPidAlive(active.pid)),
6911
7418
  lease: active || null,
6912
7419
  latest: latest || null,
7420
+ hourly_alive: memberAliveCronStatus(name, paths),
6913
7421
  lock_path: paths.lockPath,
6914
7422
  latest_path: paths.latestPath,
6915
7423
  };
6916
7424
  printJsonOrText(payload, [
6917
7425
  `Loop status: ${name}`,
6918
7426
  `Active: ${payload.active ? 'yes' : 'no'}`,
7427
+ `Hourly: ${payload.hourly_alive.installed ? 'installed' : payload.hourly_alive.script_exists ? 'script ready' : 'not installed'}`,
6919
7428
  `Latest: ${latest?.receipt_path ? path.relative(process.cwd(), latest.receipt_path) : 'none'}`,
6920
7429
  ], asJson);
6921
7430
  return;
@@ -6953,11 +7462,21 @@ async function memberLoop(name, ...args) {
6953
7462
  const ticksFlag = readNumberFlag(args, '--ticks', null);
6954
7463
  const minutes = readNumberFlag(args, '--minutes', null);
6955
7464
  const durationSeconds = readNumberFlag(args, '--duration-seconds', readNumberFlag(args, '--seconds', null));
6956
- const intervalSeconds = readNumberFlag(args, '--interval', readNumberFlag(args, '--interval-seconds', 60));
7465
+ const hourly = hasFlag(args, '--hourly') || hasFlag(args, '--every-hour');
7466
+ const forever = hasFlag(args, '--forever') || hasFlag(args, '--until-stopped');
7467
+ const intervalSeconds = hourly
7468
+ ? 3600
7469
+ : readNumberFlag(args, '--interval', readNumberFlag(args, '--interval-seconds', 60));
6957
7470
  const intervalMs = Math.max(0, Math.floor(Number(intervalSeconds == null ? 60 : intervalSeconds) * 1000));
6958
- const durationMs = Math.max(0, Math.floor(durationSeconds != null ? Number(durationSeconds) * 1000 : Number(minutes == null ? 10 : minutes) * 60 * 1000));
7471
+ const durationMs = forever && ticksFlag == null
7472
+ ? 0
7473
+ : Math.max(0, Math.floor(durationSeconds != null ? Number(durationSeconds) * 1000 : Number(minutes == null ? 10 : minutes) * 60 * 1000));
6959
7474
  const ticks = ticksFlag != null
6960
7475
  ? Math.max(1, Math.floor(Number(ticksFlag)))
7476
+ : forever
7477
+ ? execute && confirmed
7478
+ ? Number.MAX_SAFE_INTEGER
7479
+ : 1
6961
7480
  : intervalMs > 0
6962
7481
  ? Math.max(1, Math.floor(durationMs / intervalMs))
6963
7482
  : 1;
@@ -6967,7 +7486,7 @@ async function memberLoop(name, ...args) {
6967
7486
  const ttlMs = Math.max(
6968
7487
  30000,
6969
7488
  Math.floor(Number(ttlSeconds == null ? 0 : ttlSeconds) * 1000),
6970
- durationMs + 60000,
7489
+ forever ? intervalMs + (2 * 60 * 60 * 1000) : durationMs + 60000,
6971
7490
  intervalMs + 60000,
6972
7491
  );
6973
7492
 
@@ -7162,6 +7681,12 @@ async function memberLoop(name, ...args) {
7162
7681
  alive: aliveMode,
7163
7682
  status: failed ? 'failed' : stopped ? 'stopped' : earlyExit ? (earlyExit.blocked_on_human ? 'blocked_on_human' : 'idle') : 'completed',
7164
7683
  mode: execute ? 'execute' : 'dry_run',
7684
+ cadence: hourly ? 'hourly' : 'custom',
7685
+ forever,
7686
+ stop_command: `atris member ${aliveMode ? 'alive' : 'loop'} ${name} --stop`,
7687
+ operating_contract: forever
7688
+ ? 'Run on this cadence until stopped; each tick must choose useful work, prove it plainly, or stop/ask instead of faking activity.'
7689
+ : 'Run for the requested bounded window; each tick must choose useful work, prove it plainly, or stop/ask instead of faking activity.',
7165
7690
  run_id: runId,
7166
7691
  ticks_requested: ticks,
7167
7692
  ticks: tickResults.length,
@@ -7191,8 +7716,10 @@ async function memberLoop(name, ...args) {
7191
7716
  printJsonOrText(payload, [
7192
7717
  `${aliveMode ? 'Alive' : 'Loop'}: ${name}`,
7193
7718
  `Status: ${payload.status}`,
7719
+ `Cadence: ${payload.cadence}${payload.forever ? ' forever' : ''}`,
7194
7720
  `Ticks: ${payload.ticks}/${payload.ticks_requested}`,
7195
7721
  `Decisions: ${Object.entries(decisions).map(([key, count]) => `${key} x${count}`).join(', ') || 'none'}`,
7722
+ `Stop: ${payload.stop_command}`,
7196
7723
  `Receipt: ${path.relative(process.cwd(), receiptPath)}`,
7197
7724
  ], asJson);
7198
7725
  }
@@ -7533,7 +8060,7 @@ async function memberCommand(subcommand, ...args) {
7533
8060
  // Subcommands that take a member name as args[0] otherwise treat `--help` as
7534
8061
  // a name and error with "Member '--help' not found". `create`/`new` handle
7535
8062
  // help themselves (with subcommand-specific usage) — leave those alone.
7536
- const HELP_AWARE_SUBCOMMANDS = new Set(['create', 'new']);
8063
+ const HELP_AWARE_SUBCOMMANDS = new Set(['create', 'new', 'run']);
7537
8064
  if (!HELP_AWARE_SUBCOMMANDS.has(subcommand) && (args[0] === '-h' || args[0] === '--help')) {
7538
8065
  subcommand = undefined;
7539
8066
  }
@@ -7566,6 +8093,8 @@ async function memberCommand(subcommand, ...args) {
7566
8093
  return memberWake(args[0], ...args.slice(1));
7567
8094
  case 'run':
7568
8095
  return memberRun(args[0], ...args.slice(1));
8096
+ case 'ping':
8097
+ return memberPing(args[0], ...args.slice(1));
7569
8098
  case 'loop':
7570
8099
  return memberLoop(args[0], ...args.slice(1));
7571
8100
  case 'alive':
@@ -7603,7 +8132,7 @@ async function memberCommand(subcommand, ...args) {
7603
8132
  console.log(' goal-from-mission <name> Create/reuse a goal from MISSION.md and now.md');
7604
8133
  console.log(' goal-from-score <name> Create/reuse an active goal from Team score evidence');
7605
8134
  console.log(' wake <name> Read Mission state and decide tick/wait/ask/stop');
7606
- console.log(" run <name> Run the member's active Mission Runtime");
8135
+ console.log(' run <name> ["..."] Start a budgeted member mission, or run its active Mission Runtime');
7607
8136
  console.log(' loop <name> Repeat wake on a bounded cadence with a no-overlap lease');
7608
8137
  console.log(' tick <name> Propose the next bounded experiment');
7609
8138
  console.log(' review <name> <id> Accept/discard an experiment with proof');
@@ -7633,13 +8162,16 @@ async function memberCommand(subcommand, ...args) {
7633
8162
  console.log(' atris member goal-from-mission growth --json');
7634
8163
  console.log(' atris member goal-from-score growth --score-json team-score.json --json');
7635
8164
  console.log(' atris member wake growth --json');
7636
- console.log(' atris member run growth --max-ticks 1 --max-wall 900 --json');
8165
+ console.log(' atris member run growth "Improve onboarding proof" --minutes 30 --json');
8166
+ console.log(' atris member run growth --mission <mission-id> --max-ticks 1 --json');
7637
8167
  console.log(' atris member wake growth --execute --confirm-autonomy-policy');
7638
8168
  console.log(' atris member supervisor recommendations --json');
7639
8169
  console.log(' atris member objective-generator proposals --json');
7640
8170
  console.log(' atris member generalist proof --json');
7641
8171
  console.log(' atris member generalist patterns --json');
7642
8172
  console.log(' atris member loop growth --minutes 10 --interval 60 --json');
8173
+ console.log(' atris member alive growth --hourly --forever --execute --confirm-autonomy-policy --json');
8174
+ console.log(' atris member alive growth --install --hourly --forever --execute --confirm-autonomy-policy --json');
7643
8175
  console.log(' atris member alive growth --minutes 480 --interval 900 --execute --confirm-autonomy-policy --json');
7644
8176
  console.log(' atris member loop growth --ticks 2 --interval 0 --json');
7645
8177
  console.log(' atris member tick growth --json');