atris 3.30.12 → 3.31.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 (58) hide show
  1. package/AGENTS.md +16 -3
  2. package/FOR_AGENTS.md +81 -0
  3. package/README.md +3 -1
  4. package/atris/atris.md +51 -19
  5. package/atris/skills/README.md +1 -0
  6. package/atris/skills/blocks/SKILL.md +134 -0
  7. package/atris/skills/clawhub/philosophy-of-work/SKILL.md +56 -0
  8. package/atris/skills/youtube/SKILL.md +31 -11
  9. package/atris.md +7 -0
  10. package/ax +95 -3
  11. package/bin/atris.js +126 -153
  12. package/commands/autoland.js +319 -0
  13. package/commands/autopilot.js +94 -4
  14. package/commands/business.js +1 -1
  15. package/commands/clean.js +72 -9
  16. package/commands/codex-goal.js +72 -22
  17. package/commands/computer.js +48 -3
  18. package/commands/gm.js +1 -1
  19. package/commands/harvest.js +179 -0
  20. package/commands/init.js +1 -1
  21. package/commands/land.js +442 -0
  22. package/commands/loop-front.js +122 -4
  23. package/commands/member.js +519 -19
  24. package/commands/mission.js +3330 -290
  25. package/commands/play.js +1 -1
  26. package/commands/pulse.js +65 -7
  27. package/commands/run.js +3 -3
  28. package/commands/strings.js +301 -0
  29. package/commands/task.js +575 -102
  30. package/commands/truth.js +170 -0
  31. package/commands/xp.js +32 -8
  32. package/commands/youtube.js +72 -5
  33. package/decks/README.md +6 -12
  34. package/lib/auto-accept-certified.js +10 -0
  35. package/lib/autoland.js +283 -0
  36. package/lib/context-gatherer.js +0 -8
  37. package/lib/mission-artifact.js +504 -0
  38. package/lib/mission-room.js +846 -0
  39. package/lib/next-moves.js +212 -6
  40. package/lib/pulse.js +74 -1
  41. package/lib/runner-command.js +20 -8
  42. package/lib/runs-prune.js +242 -0
  43. package/lib/task-proof.js +1 -1
  44. package/package.json +3 -3
  45. package/decks/atris-seed-pitch-v3.json +0 -118
  46. package/decks/atris-seed-pitch-v4-skeleton.json +0 -106
  47. package/decks/atris-seed-pitch-v5.json +0 -109
  48. package/decks/atris-seed-pitch-v6.json +0 -137
  49. package/decks/atris-seed-pitch-v7.json +0 -133
  50. package/decks/mark-pincus-narrative.json +0 -102
  51. package/decks/mark-pincus-sourcery.json +0 -94
  52. package/decks/yash-applied-compute-detailed.json +0 -150
  53. package/decks/yash-applied-compute-generalist.json +0 -82
  54. package/decks/yash-applied-compute-narrative.json +0 -54
  55. package/lib/ax-chat-input.js +0 -164
  56. package/lib/ax-goal.js +0 -307
  57. package/lib/ax-prefs.js +0 -63
  58. 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,245 @@ 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
+ }
420
612
  }
421
613
 
422
614
  function memberRun(name, ...args) {
423
615
  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');
616
+ console.log('Usage: atris member run <name> ["mission text"] [--minutes N|--hours N] [--json]');
617
+ console.log('New mission: atris member run growth "Improve onboarding proof" --minutes 30 --json');
618
+ console.log('Choose work: atris member run growth --industry logistics --value "reduce support time" --minutes 30 --json');
619
+ console.log('Existing mission: atris member run growth --mission <mission-id> --max-ticks 1 --json');
620
+ console.log('Meaning: if mission text is omitted, the member chooses one useful bounded task from Atris state.');
621
+ console.log('Truth: useful work must improve revenue, reliability, speed, security, clarity, or trust, then show plain proof.');
622
+ console.log('Isolation: new missions use --worktree by default; add --shared-checkout to stay here.');
623
+ return;
624
+ }
625
+
626
+ const missionText = memberRunMissionText(args);
627
+ const hasMissionOverride = Boolean(readFlag(args, '--mission', '') || readFlag(args, '--mission-id', ''));
628
+ if (missionText && !hasMissionOverride) {
629
+ startMemberRunMission(name, missionText, args);
427
630
  return;
428
631
  }
429
632
 
430
633
  const missionId = resolveMemberRunMissionId(name, args);
431
634
  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;
635
+ startMemberRunMission(name, memberRunAutoMissionText(name, args), args);
436
636
  return;
437
637
  }
438
638
 
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');
639
+ const runArgs = stripKnownFlags(args, ['--mission', '--mission-id', '--minutes', '--hours'], ['--worktree', '--shared-checkout', '--no-worktree']);
640
+ const budgetSeconds = memberRunBudgetSeconds(args);
641
+ // A timed run is a loop contract, not one tick: keep picking the next useful
642
+ // move until the wall clock (--max-wall) spends the budget. Only untimed runs
643
+ // default to a single tick.
644
+ if (!readFlag(runArgs, '--max-ticks', '')) {
645
+ runArgs.push('--max-ticks', budgetSeconds ? String(Math.max(4, Math.ceil(budgetSeconds / 300))) : '1');
646
+ }
647
+ if (!readFlag(runArgs, '--max-wall', '')) runArgs.push('--max-wall', String(budgetSeconds || 900));
442
648
 
443
649
  const cliPath = path.join(__dirname, '..', 'bin', 'atris.js');
444
650
  try {
@@ -2023,6 +2229,247 @@ function memberLoopPaths(name) {
2023
2229
  lockPath: path.join(stateDir, `${name}.lock.json`),
2024
2230
  stopPath: path.join(stateDir, `${name}.stop.json`),
2025
2231
  latestPath: path.join(stateDir, `${name}.latest.json`),
2232
+ cronScriptPath: path.join(stateDir, `${name}.hourly-alive.sh`),
2233
+ };
2234
+ }
2235
+
2236
+ function memberAliveCronMarker(name) {
2237
+ return `ATRIS_MEMBER_ALIVE_${String(name || '').replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}`;
2238
+ }
2239
+
2240
+ function shellSingleQuote(value) {
2241
+ return `'${String(value || '').replace(/'/g, "'\\''")}'`;
2242
+ }
2243
+
2244
+ function resolveAtrisCommand() {
2245
+ return path.join(__dirname, '..', 'bin', 'atris.js');
2246
+ }
2247
+
2248
+ function buildMemberAliveCronScript(name, args = [], paths = memberLoopPaths(name)) {
2249
+ const root = process.cwd();
2250
+ const atrisBin = resolveAtrisCommand();
2251
+ const logDir = path.join(paths.stateDir, 'logs');
2252
+ const command = [
2253
+ process.execPath,
2254
+ atrisBin,
2255
+ 'member',
2256
+ 'alive',
2257
+ name,
2258
+ '--hourly',
2259
+ '--forever',
2260
+ '--ticks',
2261
+ '1',
2262
+ '--json',
2263
+ ...(hasFlag(args, '--execute') ? ['--execute'] : []),
2264
+ ...(hasFlag(args, '--confirm-autonomy-policy') ? ['--confirm-autonomy-policy'] : []),
2265
+ ];
2266
+ const agent = String(readFlag(args, '--agent', '') || '').trim();
2267
+ const model = String(readFlag(args, '--model', '') || '').trim();
2268
+ const maxWall = String(readFlag(args, '--operate-max-wall', readFlag(args, '--max-wall', '')) || '').trim();
2269
+ const autoAcceptLimit = String(readFlag(args, '--auto-accept-limit', '') || '').trim();
2270
+ if (agent) command.push('--agent', agent);
2271
+ if (model) command.push('--model', model);
2272
+ if (maxWall) command.push('--operate-max-wall', maxWall);
2273
+ if (autoAcceptLimit) command.push('--auto-accept-limit', autoAcceptLimit);
2274
+ const renderedCommand = command.map(shellSingleQuote).join(' ');
2275
+ return {
2276
+ command,
2277
+ rendered_command: renderedCommand,
2278
+ script: [
2279
+ '#!/bin/zsh',
2280
+ 'set -u',
2281
+ `ROOT=${shellSingleQuote(root)}`,
2282
+ `LOG_DIR=${shellSingleQuote(logDir)}`,
2283
+ 'mkdir -p "$LOG_DIR"',
2284
+ 'stamp="$(date +"%Y%m%d-%H%M%S")"',
2285
+ 'cd "$ROOT" || exit 1',
2286
+ 'export ATRIS_SKIP_UPDATE_CHECK=1',
2287
+ `${renderedCommand} >> "$LOG_DIR/$stamp.log" 2>&1`,
2288
+ 'echo "done: $(date -Iseconds) exit=$?" >> "$LOG_DIR/$stamp.log"',
2289
+ '',
2290
+ ].join('\n'),
2291
+ };
2292
+ }
2293
+
2294
+ function memberAliveCronCadence(args = []) {
2295
+ const pulse = require('../lib/pulse');
2296
+ const raw = hasFlag(args, '--hourly') || hasFlag(args, '--every-hour')
2297
+ ? 'hourly'
2298
+ : readFlag(args, '--cadence', readFlag(args, '--cron', readFlag(args, '--interval', 'hourly')));
2299
+ return pulse.normalizeCronCadence(raw);
2300
+ }
2301
+
2302
+ function buildMemberAliveCrontabLine(name, args = [], paths = memberLoopPaths(name)) {
2303
+ return `${memberAliveCronCadence(args)} ${paths.cronScriptPath} # ${memberAliveCronMarker(name)}`;
2304
+ }
2305
+
2306
+ function dirtyGitStatus(root = process.cwd()) {
2307
+ const result = spawnSync('git', ['status', '--porcelain'], { cwd: root, encoding: 'utf8', timeout: 10000 });
2308
+ if (result.status !== 0) {
2309
+ return { inside_git: false, dirty: false, dirty_count: 0, dirty_files: [] };
2310
+ }
2311
+ const dirtyFiles = String(result.stdout || '')
2312
+ .split(/\r?\n/)
2313
+ .map((line) => line.trim())
2314
+ .filter(Boolean);
2315
+ return {
2316
+ inside_git: true,
2317
+ dirty: dirtyFiles.length > 0,
2318
+ dirty_count: dirtyFiles.length,
2319
+ dirty_files: dirtyFiles.slice(0, 12),
2320
+ };
2321
+ }
2322
+
2323
+ function memberAliveInstallPreview(name, args = [], paths = memberLoopPaths(name), script = buildMemberAliveCronScript(name, args, paths)) {
2324
+ return {
2325
+ schema: 'atris.member_alive_install_preview.v1',
2326
+ member: name,
2327
+ cadence: memberAliveCronCadence(args),
2328
+ mission_text: memberRunAutoMissionText(name, args),
2329
+ run: script.rendered_command,
2330
+ verify: `atris member alive ${name} --status --json`,
2331
+ stop_when: `Run atris member alive ${name} --uninstall, or create the stop marker at ${paths.stopPath}.`,
2332
+ receipts: path.join(paths.stateDir, 'logs'),
2333
+ };
2334
+ }
2335
+
2336
+ function memberAliveInstallPreviewLines(preview) {
2337
+ return [
2338
+ 'Every hour this will:',
2339
+ ` run: ${preview.run}`,
2340
+ ` verify: ${preview.verify}`,
2341
+ ` stop when: ${preview.stop_when}`,
2342
+ ` receipts: ${preview.receipts}`,
2343
+ ];
2344
+ }
2345
+
2346
+ function installMemberAliveCron(name, args = []) {
2347
+ const asJson = hasFlag(args, '--json');
2348
+ const dryRun = hasFlag(args, '--dry-run');
2349
+ if (hasFlag(args, '--execute') && !hasFlag(args, '--confirm-autonomy-policy')) {
2350
+ const payload = {
2351
+ ok: false,
2352
+ action: 'alive_install',
2353
+ member: name,
2354
+ status: 'blocked',
2355
+ reason: 'execute_requires_confirm_autonomy_policy',
2356
+ };
2357
+ printJsonOrText(payload, [
2358
+ `Install blocked for ${name}: execute requires --confirm-autonomy-policy.`,
2359
+ ], asJson);
2360
+ process.exitCode = 1;
2361
+ return payload;
2362
+ }
2363
+ const paths = memberLoopPaths(name);
2364
+ fs.mkdirSync(paths.stateDir, { recursive: true });
2365
+ const script = buildMemberAliveCronScript(name, args, paths);
2366
+ const crontabLine = buildMemberAliveCrontabLine(name, args, paths);
2367
+ const preview = memberAliveInstallPreview(name, args, paths, script);
2368
+ if (hasFlag(args, '--execute') && !dryRun) {
2369
+ const gitStatus = dirtyGitStatus();
2370
+ if (gitStatus.dirty) {
2371
+ const payload = {
2372
+ ok: false,
2373
+ action: 'alive_install',
2374
+ member: name,
2375
+ status: 'blocked',
2376
+ reason: 'install_requires_clean_git',
2377
+ git: gitStatus,
2378
+ preview,
2379
+ };
2380
+ printJsonOrText(payload, [
2381
+ `Install blocked for ${name}: commit or clean local changes before installing an execute loop.`,
2382
+ ...memberAliveInstallPreviewLines(preview),
2383
+ ], asJson);
2384
+ process.exitCode = 1;
2385
+ return payload;
2386
+ }
2387
+ }
2388
+ fs.writeFileSync(paths.cronScriptPath, script.script, { mode: 0o755 });
2389
+ const marker = memberAliveCronMarker(name);
2390
+ let installed = false;
2391
+ let error = null;
2392
+ if (!dryRun) {
2393
+ const existing = spawnSync('crontab', ['-l'], { encoding: 'utf8', timeout: 10000 });
2394
+ const current = existing.status === 0 ? existing.stdout : '';
2395
+ const kept = String(current || '').split(/\r?\n/).filter((line) => line.trim() && !line.includes(marker));
2396
+ const next = `${kept.join('\n')}${kept.length ? '\n' : ''}${crontabLine}\n`;
2397
+ const apply = spawnSync('crontab', ['-'], { input: next, encoding: 'utf8', timeout: 10000 });
2398
+ installed = apply.status === 0;
2399
+ if (!installed) error = apply.stderr || `crontab exited ${apply.status}`;
2400
+ }
2401
+ const payload = {
2402
+ ok: dryRun || installed,
2403
+ action: 'alive_install',
2404
+ member: name,
2405
+ cadence: 'hourly',
2406
+ forever: true,
2407
+ installed,
2408
+ dry_run: dryRun,
2409
+ script_path: paths.cronScriptPath,
2410
+ crontab_line: crontabLine,
2411
+ command: script.command,
2412
+ preview,
2413
+ stop_command: `atris member alive ${name} --uninstall`,
2414
+ error,
2415
+ };
2416
+ writeJsonFile(paths.latestPath, payload);
2417
+ printJsonOrText(payload, [
2418
+ `${dryRun ? 'Would install' : installed ? 'Installed' : 'Install failed'} hourly alive loop: ${name}`,
2419
+ `Cron: ${crontabLine}`,
2420
+ ...memberAliveInstallPreviewLines(preview),
2421
+ `Stop: ${payload.stop_command}`,
2422
+ ], asJson);
2423
+ if (!payload.ok) process.exitCode = 1;
2424
+ return payload;
2425
+ }
2426
+
2427
+ function uninstallMemberAliveCron(name, args = []) {
2428
+ const asJson = hasFlag(args, '--json');
2429
+ const paths = memberLoopPaths(name);
2430
+ const marker = memberAliveCronMarker(name);
2431
+ const existing = spawnSync('crontab', ['-l'], { encoding: 'utf8', timeout: 10000 });
2432
+ if (existing.status !== 0) {
2433
+ const payload = { ok: true, action: 'alive_uninstall', member: name, removed: false, reason: 'no_crontab' };
2434
+ printJsonOrText(payload, [`No hourly alive crontab found for ${name}.`], asJson);
2435
+ return payload;
2436
+ }
2437
+ const before = String(existing.stdout || '').split(/\r?\n/);
2438
+ const after = before.filter((line) => !line.includes(marker));
2439
+ const removed = after.length !== before.length;
2440
+ const apply = spawnSync('crontab', ['-'], { input: after.filter(Boolean).join('\n') + (after.filter(Boolean).length ? '\n' : ''), encoding: 'utf8', timeout: 10000 });
2441
+ const payload = {
2442
+ ok: apply.status === 0,
2443
+ action: 'alive_uninstall',
2444
+ member: name,
2445
+ removed,
2446
+ marker,
2447
+ script_path: paths.cronScriptPath,
2448
+ error: apply.status === 0 ? null : apply.stderr || `crontab exited ${apply.status}`,
2449
+ };
2450
+ printJsonOrText(payload, [
2451
+ removed ? `Removed hourly alive loop: ${name}` : `No hourly alive line found for ${name}.`,
2452
+ ], asJson);
2453
+ if (!payload.ok) process.exitCode = 1;
2454
+ return payload;
2455
+ }
2456
+
2457
+ function memberAliveCronStatus(name, paths = memberLoopPaths(name)) {
2458
+ const marker = memberAliveCronMarker(name);
2459
+ let crontabLine = null;
2460
+ const crontab = spawnSync('crontab', ['-l'], { encoding: 'utf8', timeout: 10000 });
2461
+ if (crontab.status === 0) {
2462
+ crontabLine = String(crontab.stdout || '').split(/\r?\n/).find((line) => line.includes(marker)) || null;
2463
+ }
2464
+ return {
2465
+ schema: 'atris.member_alive_cron_status.v1',
2466
+ marker,
2467
+ installed: Boolean(crontabLine),
2468
+ crontab_line: crontabLine,
2469
+ script_path: paths.cronScriptPath,
2470
+ script_exists: fs.existsSync(paths.cronScriptPath),
2471
+ install_command: `atris member alive ${name} --install --hourly --forever --execute --confirm-autonomy-policy --json`,
2472
+ uninstall_command: `atris member alive ${name} --uninstall`,
2026
2473
  };
2027
2474
  }
2028
2475
 
@@ -2145,6 +2592,11 @@ function stripAutoImproverTitleNoise(text) {
2145
2592
  function isAutoImproverGeneratedLogLine(line, relativePath = '') {
2146
2593
  const text = String(line || '').trim();
2147
2594
  if (!text) return false;
2595
+ if (/^[-*]?\s*Next tick will stop until a human looks at the error\.?$/i.test(text)) return true;
2596
+ if (/^[-*]?\s*Next tick will verify failed, halting\.?$/i.test(text)) return true;
2597
+ if (/^##\s+\d{1,2}:\d{2}\s+·\s+Task failed$/i.test(text)) return true;
2598
+ if (/^[-*]?\s*(status|action|state|verifier)\s*:\s*(failed|blocked)\s*$/i.test(text)) return true;
2599
+ if (/^[-*]?\s*(title|proof):\s*/i.test(text)) return true;
2148
2600
  if (String(relativePath || '').startsWith('atris/team/auto-improver/logs/') && /^-\s*summary:\s*/i.test(text)) return true;
2149
2601
  if (/\bauto[- ]improver\b/i.test(text) && /\b(dogfood|receipt|prevented|pain_)\b/i.test(text)) return true;
2150
2602
  if (/\bauto_improver\b/i.test(text)) return true;
@@ -2162,12 +2614,28 @@ function normalizeFailurePattern(line) {
2162
2614
  .trim());
2163
2615
  }
2164
2616
 
2617
+ function failureCoveredByPassLesson(line, passLessonText = '') {
2618
+ const text = String(line || '');
2619
+ if (!text || !passLessonText) return false;
2620
+ if (/spawnSync\s+\/bin\/sh\s+ETIMEDOUT/i.test(text)) {
2621
+ const target = text.match(/"([^"]+)"/)?.[1] || '';
2622
+ const targetTail = target ? target.split('/').slice(-2).join('/') : '';
2623
+ return /spawnSync\s+\/bin\/sh\s+ETIMEDOUT/i.test(passLessonText)
2624
+ && (!target || passLessonText.includes(target) || (targetTail && passLessonText.includes(targetTail)));
2625
+ }
2626
+ return false;
2627
+ }
2628
+
2165
2629
  function collectAutoImproverLogSignals(root) {
2166
2630
  const roots = [
2167
2631
  path.join(root, 'atris', 'logs'),
2168
2632
  path.join(root, 'atris', 'team'),
2169
2633
  path.join(root, 'atris', 'wiki'),
2170
2634
  ].filter((candidate) => fs.existsSync(candidate));
2635
+ const passLessonText = safeReadText(path.join(root, 'atris', 'lessons.md'), 500000)
2636
+ .split(/\r?\n/)
2637
+ .filter((line) => /\s+pass\s+—/.test(line))
2638
+ .join('\n');
2171
2639
  const failureRegex = /\b(error|failed|failure|blocked|timeout|regression|crash|missing proof|naraka|suffering)\b/i;
2172
2640
  const unclearRegex = /\b(tbd|unclear|unknown|needs user|needs owner|needs proof|no next|blocked)\b/i;
2173
2641
  const counts = new Map();
@@ -2203,6 +2671,7 @@ function collectAutoImproverLogSignals(root) {
2203
2671
  if (/^\s*[-*]?\s*check:\s/i.test(line)) continue;
2204
2672
  if (/\b(errors?|fail(?:ed|ures?)|blocked|timeouts?)\s*:\s*0\b/i.test(line)) continue;
2205
2673
  if (/\bblocked\s*(?:->|→|to)\s*ready\b/i.test(line)) continue;
2674
+ if (failureCoveredByPassLesson(line, passLessonText)) continue;
2206
2675
  if (unclearRegex.test(line)) {
2207
2676
  unclearNextActions += 1;
2208
2677
  unclearActions.push({
@@ -6900,6 +7369,14 @@ async function memberLoop(name, ...args) {
6900
7369
  const paths = memberLoopPaths(name);
6901
7370
  fs.mkdirSync(paths.stateDir, { recursive: true });
6902
7371
 
7372
+ if (hasFlag(args, '--install')) {
7373
+ return installMemberAliveCron(name, args);
7374
+ }
7375
+
7376
+ if (hasFlag(args, '--uninstall')) {
7377
+ return uninstallMemberAliveCron(name, args);
7378
+ }
7379
+
6903
7380
  if (status) {
6904
7381
  const active = readJsonIfExists(paths.lockPath);
6905
7382
  const latest = readJsonIfExists(paths.latestPath);
@@ -6910,12 +7387,14 @@ async function memberLoop(name, ...args) {
6910
7387
  active: Boolean(active && Number(active.expires_at_ms || 0) > Date.now() && isPidAlive(active.pid)),
6911
7388
  lease: active || null,
6912
7389
  latest: latest || null,
7390
+ hourly_alive: memberAliveCronStatus(name, paths),
6913
7391
  lock_path: paths.lockPath,
6914
7392
  latest_path: paths.latestPath,
6915
7393
  };
6916
7394
  printJsonOrText(payload, [
6917
7395
  `Loop status: ${name}`,
6918
7396
  `Active: ${payload.active ? 'yes' : 'no'}`,
7397
+ `Hourly: ${payload.hourly_alive.installed ? 'installed' : payload.hourly_alive.script_exists ? 'script ready' : 'not installed'}`,
6919
7398
  `Latest: ${latest?.receipt_path ? path.relative(process.cwd(), latest.receipt_path) : 'none'}`,
6920
7399
  ], asJson);
6921
7400
  return;
@@ -6953,11 +7432,21 @@ async function memberLoop(name, ...args) {
6953
7432
  const ticksFlag = readNumberFlag(args, '--ticks', null);
6954
7433
  const minutes = readNumberFlag(args, '--minutes', null);
6955
7434
  const durationSeconds = readNumberFlag(args, '--duration-seconds', readNumberFlag(args, '--seconds', null));
6956
- const intervalSeconds = readNumberFlag(args, '--interval', readNumberFlag(args, '--interval-seconds', 60));
7435
+ const hourly = hasFlag(args, '--hourly') || hasFlag(args, '--every-hour');
7436
+ const forever = hasFlag(args, '--forever') || hasFlag(args, '--until-stopped');
7437
+ const intervalSeconds = hourly
7438
+ ? 3600
7439
+ : readNumberFlag(args, '--interval', readNumberFlag(args, '--interval-seconds', 60));
6957
7440
  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));
7441
+ const durationMs = forever && ticksFlag == null
7442
+ ? 0
7443
+ : Math.max(0, Math.floor(durationSeconds != null ? Number(durationSeconds) * 1000 : Number(minutes == null ? 10 : minutes) * 60 * 1000));
6959
7444
  const ticks = ticksFlag != null
6960
7445
  ? Math.max(1, Math.floor(Number(ticksFlag)))
7446
+ : forever
7447
+ ? execute && confirmed
7448
+ ? Number.MAX_SAFE_INTEGER
7449
+ : 1
6961
7450
  : intervalMs > 0
6962
7451
  ? Math.max(1, Math.floor(durationMs / intervalMs))
6963
7452
  : 1;
@@ -6967,7 +7456,7 @@ async function memberLoop(name, ...args) {
6967
7456
  const ttlMs = Math.max(
6968
7457
  30000,
6969
7458
  Math.floor(Number(ttlSeconds == null ? 0 : ttlSeconds) * 1000),
6970
- durationMs + 60000,
7459
+ forever ? intervalMs + (2 * 60 * 60 * 1000) : durationMs + 60000,
6971
7460
  intervalMs + 60000,
6972
7461
  );
6973
7462
 
@@ -7162,6 +7651,12 @@ async function memberLoop(name, ...args) {
7162
7651
  alive: aliveMode,
7163
7652
  status: failed ? 'failed' : stopped ? 'stopped' : earlyExit ? (earlyExit.blocked_on_human ? 'blocked_on_human' : 'idle') : 'completed',
7164
7653
  mode: execute ? 'execute' : 'dry_run',
7654
+ cadence: hourly ? 'hourly' : 'custom',
7655
+ forever,
7656
+ stop_command: `atris member ${aliveMode ? 'alive' : 'loop'} ${name} --stop`,
7657
+ operating_contract: forever
7658
+ ? 'Run on this cadence until stopped; each tick must choose useful work, prove it plainly, or stop/ask instead of faking activity.'
7659
+ : 'Run for the requested bounded window; each tick must choose useful work, prove it plainly, or stop/ask instead of faking activity.',
7165
7660
  run_id: runId,
7166
7661
  ticks_requested: ticks,
7167
7662
  ticks: tickResults.length,
@@ -7191,8 +7686,10 @@ async function memberLoop(name, ...args) {
7191
7686
  printJsonOrText(payload, [
7192
7687
  `${aliveMode ? 'Alive' : 'Loop'}: ${name}`,
7193
7688
  `Status: ${payload.status}`,
7689
+ `Cadence: ${payload.cadence}${payload.forever ? ' forever' : ''}`,
7194
7690
  `Ticks: ${payload.ticks}/${payload.ticks_requested}`,
7195
7691
  `Decisions: ${Object.entries(decisions).map(([key, count]) => `${key} x${count}`).join(', ') || 'none'}`,
7692
+ `Stop: ${payload.stop_command}`,
7196
7693
  `Receipt: ${path.relative(process.cwd(), receiptPath)}`,
7197
7694
  ], asJson);
7198
7695
  }
@@ -7533,7 +8030,7 @@ async function memberCommand(subcommand, ...args) {
7533
8030
  // Subcommands that take a member name as args[0] otherwise treat `--help` as
7534
8031
  // a name and error with "Member '--help' not found". `create`/`new` handle
7535
8032
  // help themselves (with subcommand-specific usage) — leave those alone.
7536
- const HELP_AWARE_SUBCOMMANDS = new Set(['create', 'new']);
8033
+ const HELP_AWARE_SUBCOMMANDS = new Set(['create', 'new', 'run']);
7537
8034
  if (!HELP_AWARE_SUBCOMMANDS.has(subcommand) && (args[0] === '-h' || args[0] === '--help')) {
7538
8035
  subcommand = undefined;
7539
8036
  }
@@ -7603,7 +8100,7 @@ async function memberCommand(subcommand, ...args) {
7603
8100
  console.log(' goal-from-mission <name> Create/reuse a goal from MISSION.md and now.md');
7604
8101
  console.log(' goal-from-score <name> Create/reuse an active goal from Team score evidence');
7605
8102
  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");
8103
+ console.log(' run <name> ["..."] Start a budgeted member mission, or run its active Mission Runtime');
7607
8104
  console.log(' loop <name> Repeat wake on a bounded cadence with a no-overlap lease');
7608
8105
  console.log(' tick <name> Propose the next bounded experiment');
7609
8106
  console.log(' review <name> <id> Accept/discard an experiment with proof');
@@ -7633,13 +8130,16 @@ async function memberCommand(subcommand, ...args) {
7633
8130
  console.log(' atris member goal-from-mission growth --json');
7634
8131
  console.log(' atris member goal-from-score growth --score-json team-score.json --json');
7635
8132
  console.log(' atris member wake growth --json');
7636
- console.log(' atris member run growth --max-ticks 1 --max-wall 900 --json');
8133
+ console.log(' atris member run growth "Improve onboarding proof" --minutes 30 --json');
8134
+ console.log(' atris member run growth --mission <mission-id> --max-ticks 1 --json');
7637
8135
  console.log(' atris member wake growth --execute --confirm-autonomy-policy');
7638
8136
  console.log(' atris member supervisor recommendations --json');
7639
8137
  console.log(' atris member objective-generator proposals --json');
7640
8138
  console.log(' atris member generalist proof --json');
7641
8139
  console.log(' atris member generalist patterns --json');
7642
8140
  console.log(' atris member loop growth --minutes 10 --interval 60 --json');
8141
+ console.log(' atris member alive growth --hourly --forever --execute --confirm-autonomy-policy --json');
8142
+ console.log(' atris member alive growth --install --hourly --forever --execute --confirm-autonomy-policy --json');
7643
8143
  console.log(' atris member alive growth --minutes 480 --interval 900 --execute --confirm-autonomy-policy --json');
7644
8144
  console.log(' atris member loop growth --ticks 2 --interval 0 --json');
7645
8145
  console.log(' atris member tick growth --json');