atris 3.70.0 → 3.71.1

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 (79) hide show
  1. package/README.md +18 -0
  2. package/atris/skills/README.md +1 -0
  3. package/atris/skills/campaigns/SKILL.md +81 -0
  4. package/atris/skills/meta/SKILL.md +52 -8
  5. package/ax +226 -42
  6. package/bin/atris.js +38 -8
  7. package/commands/autoland.js +176 -7
  8. package/commands/autopilot.js +71 -11
  9. package/commands/campaign.js +779 -0
  10. package/commands/compile.js +19 -8
  11. package/commands/doctor.js +58 -0
  12. package/commands/dream.js +18 -3
  13. package/commands/engine.js +147 -5
  14. package/commands/improve.js +119 -9
  15. package/commands/init.js +18 -1
  16. package/commands/integrations.js +17 -5
  17. package/commands/interview.js +15 -3
  18. package/commands/land.js +22 -6
  19. package/commands/link.js +1 -0
  20. package/commands/member.js +359 -94
  21. package/commands/mission.js +249 -36
  22. package/commands/report.js +42 -8
  23. package/commands/run.js +9 -2
  24. package/commands/scout.js +9 -1
  25. package/commands/task.js +71 -17
  26. package/commands/team.js +212 -4
  27. package/commands/unknowns.js +15 -1
  28. package/commands/verify.js +46 -0
  29. package/commands/worktree.js +108 -15
  30. package/commands/write.js +12 -1
  31. package/commands/youtube.js +17 -2
  32. package/lib/accept-verify-gate.js +1 -0
  33. package/lib/account-bound.js +13 -0
  34. package/lib/auto-accept-certified.js +1 -0
  35. package/lib/autoland.js +13 -4
  36. package/lib/campaign-cadence.js +131 -0
  37. package/lib/campaign-core.js +330 -0
  38. package/lib/campaign-holdout.js +65 -0
  39. package/lib/campaign-loop.js +246 -0
  40. package/lib/campaign-missions.js +237 -0
  41. package/lib/campaign-new.js +251 -0
  42. package/lib/campaign-protect.js +75 -0
  43. package/lib/campaign-reading.js +147 -0
  44. package/lib/campaign-run-lock.js +59 -0
  45. package/lib/campaign-schema.js +196 -0
  46. package/lib/campaign-store.js +380 -0
  47. package/lib/campaign-sync.js +302 -0
  48. package/lib/campaign-trace.js +300 -0
  49. package/lib/conductor-artifacts.js +15 -1
  50. package/lib/daily-log.js +11 -1
  51. package/lib/dispatch-scout.js +18 -0
  52. package/lib/engine-ask.js +33 -3
  53. package/lib/engine-registry.js +2 -0
  54. package/lib/engine-resource.js +518 -0
  55. package/lib/fleet.js +125 -16
  56. package/lib/init-commit.js +80 -0
  57. package/lib/intents.js +8 -0
  58. package/lib/known-commands.js +1 -1
  59. package/lib/land-green.js +58 -7
  60. package/lib/landing-proof.js +352 -0
  61. package/lib/member-autoland.js +395 -18
  62. package/lib/member-board.js +7 -1
  63. package/lib/member-map-fold.js +126 -0
  64. package/lib/member-map-inbox.js +80 -0
  65. package/lib/member-progress.js +48 -1
  66. package/lib/member-report.js +127 -8
  67. package/lib/member-self-next-job.js +97 -0
  68. package/lib/member-task-pick.js +41 -14
  69. package/lib/member-up.js +249 -16
  70. package/lib/roster-prep.js +25 -2
  71. package/lib/runner-command.js +5 -0
  72. package/lib/task-db.js +47 -11
  73. package/lib/task-explanation.js +7 -0
  74. package/lib/task-list-keeper.js +8 -1
  75. package/lib/task-receipt.js +6 -1
  76. package/mcp/atris-mcp/index.mjs +212 -10
  77. package/package.json +2 -1
  78. package/scripts/det/engine-reading.js +37 -0
  79. package/scripts/det/ytnotes +29 -5
package/commands/init.js CHANGED
@@ -5,6 +5,7 @@ const { ensureExperimentsFramework } = require('./experiments');
5
5
  const { ensureWorkspaceBrain } = require('../lib/workspace-scaffold');
6
6
  const { upsertAtrisClaudeBootBlock } = require('../lib/claude-boot-block');
7
7
  const { ensureRunIgnore } = require('../lib/gitignore');
8
+ const { snapshotBeforeInit, commitInitFiles } = require('../lib/init-commit');
8
9
  const { INTENTS } = require('../lib/intents');
9
10
  const {
10
11
  upsertAgentVoiceCard,
@@ -307,7 +308,7 @@ function initAtris() {
307
308
  // shouldn't have file-system side effects.
308
309
  const args = process.argv.slice(3);
309
310
  if (args[0] === '-h' || args[0] === '--help' || args[0] === 'help') {
310
- console.log('Usage: atris init [--force] [--yes|-y] [--verbose] [--minimal] [--full] [--with-adapters] [--with-skills]');
311
+ console.log('Usage: atris init [--force] [--yes|-y] [--verbose] [--minimal] [--full] [--with-adapters] [--with-skills] [--no-commit]');
311
312
  console.log('');
312
313
  console.log(' Scaffold the atris/ workspace in the current directory.');
313
314
  console.log(' Refuses to run inside an existing atris/ folder unless --force is passed.');
@@ -315,6 +316,8 @@ function initAtris() {
315
316
  console.log(' Use --verbose to list every file installed.');
316
317
  console.log(' Use --minimal for atris.md + MAP + TODO + today journal + .atris/state (no IDE farm).');
317
318
  console.log(' Use --with-adapters / --with-skills with --minimal, or --full for the default full scaffold.');
319
+ console.log(' In a git repo with no commits yet, init commits the files it creates so every member copy starts with them.');
320
+ console.log(' Use --no-commit to leave them uncommitted.');
318
321
  return;
319
322
  }
320
323
  const verbose = args.includes('--verbose');
@@ -392,6 +395,9 @@ function initAtris() {
392
395
  walker = parent;
393
396
  }
394
397
 
398
+ // Snapshot what was already uncommitted so the setup commit only carries init's own files.
399
+ const gitSnapshot = args.includes('--no-commit') ? null : snapshotBeforeInit(cwd);
400
+
395
401
  const targetDir = path.join(cwd, 'atris');
396
402
  const teamDir = path.join(targetDir, 'team');
397
403
  const legacyAgentTeamDir = path.join(targetDir, 'agent_team');
@@ -1122,6 +1128,17 @@ Read atris/MAP.md. Begin iteration 1.`;
1122
1128
  fs.copyFileSync(sourceFile, targetFile);
1123
1129
  markReady('workspace', 'atris.md', '✓ Copied atris.md to atris/ folder');
1124
1130
  printReadyCounts();
1131
+ if (gitSnapshot) {
1132
+ const saved = commitInitFiles(gitSnapshot);
1133
+ if (saved.committed) {
1134
+ console.log('setup saved to git, so every member starts with it');
1135
+ } else if (saved.reason === 'has_history') {
1136
+ console.log('commit the new atris files so members start with them');
1137
+ } else if (saved.reason === 'add_failed' || saved.reason === 'commit_failed') {
1138
+ console.log('setup files are not committed yet. members only see them after you commit.');
1139
+ if (saved.detail) console.log(` git said: ${saved.detail.split('\n')[0]}`);
1140
+ }
1141
+ }
1125
1142
  console.log('\natris initialized.');
1126
1143
  console.log('no commands to learn. tell your agent what you want in plain words.');
1127
1144
  } else {
@@ -7,7 +7,7 @@
7
7
  * atris gmail archive <id> [...] [--account <id>] - Archive messages
8
8
  * atris gmail triage [--account <id>] [--limit N] - Record keep or archive verdicts
9
9
  * atris gmail verdicts [--summary] [--account <id>] [--limit N] - List recent Gmail verdicts
10
- * atris gmail send <to> <subject> <body...> [--body-file <path>] [--account <id>] - Send an email
10
+ * atris gmail send <to> <subject> <body...> --approved [--body-file <path>] [--account <id>] - Send an email
11
11
  * atris gmail voice [account] [--clear] - Edit an account's writing voice
12
12
  * atris gmail connect [name] - Connect or reconnect a Gmail account
13
13
  * atris gmail accounts - List connected Gmail accounts and the active account
@@ -41,7 +41,7 @@ const { sleepSync } = require('../lib/sleep-sync');
41
41
  const CALENDAR_CACHE_PATH = path.join(stateHome(), '.atris', 'calendar-events-cache.json');
42
42
  const GMAIL_CONNECT_POLL_MS = 3000;
43
43
  const GMAIL_CONNECT_TIMEOUT_MS = 3 * 60 * 1000;
44
- const GMAIL_SEND_USAGE = 'usage: atris gmail send <to> <subject> <body...> [--body-file <path>] [--account <id>]';
44
+ const GMAIL_SEND_USAGE = 'usage: atris gmail send <to> <subject> <body...> --approved [--body-file <path>] [--account <id>]';
45
45
  const GMAIL_VOICE_USAGE = 'usage: atris gmail voice [account] [--clear]';
46
46
  const GMAIL_TRIAGE_USAGE = 'usage: atris gmail triage [--account <id>] [--limit N]';
47
47
  const GMAIL_VERDICTS_USAGE = 'usage: atris gmail verdicts [--summary] [--account <id>] [--limit N]';
@@ -560,10 +560,15 @@ async function gmailArchive(messageIds, options = {}) {
560
560
  function parseGmailSendArgs(args = []) {
561
561
  let accountId = null;
562
562
  let bodyFile = null;
563
+ let approved = false;
563
564
  const positional = [];
564
565
 
565
566
  for (let i = 0; i < args.length; i += 1) {
566
567
  const arg = args[i];
568
+ if (arg === '--approved' || arg === '--confirm-approved') {
569
+ approved = true;
570
+ continue;
571
+ }
567
572
  if (arg === '--account' || arg === '--body-file') {
568
573
  const value = String(args[i + 1] || '').trim();
569
574
  if (!value || value.startsWith('--')) {
@@ -596,10 +601,17 @@ function parseGmailSendArgs(args = []) {
596
601
  }
597
602
  }
598
603
 
599
- return { to, subject, body, accountId };
604
+ return { to, subject, body, accountId, approved };
600
605
  }
601
606
 
602
607
  async function gmailSend(to, subject, body, options = {}) {
608
+ // Approval is checked before any token lookup or network call so an
609
+ // unapproved send can never reach the server.
610
+ if (!options.approved) {
611
+ console.error('Refusing to send without --approved after the exact recipient, subject, and body are confirmed.');
612
+ console.error(GMAIL_SEND_USAGE);
613
+ process.exit(2);
614
+ }
603
615
  const token = await getAuthToken();
604
616
  const accountId = resolveGmailAccountId(options.accountId);
605
617
  const accounts = await fetchGmailAccounts(token);
@@ -864,7 +876,7 @@ async function gmailCommand(subcommand, ...args) {
864
876
  }
865
877
  case 'send': {
866
878
  const parsed = parseGmailSendArgs(args);
867
- await gmailSend(parsed.to, parsed.subject, parsed.body, { accountId: parsed.accountId || undefined });
879
+ await gmailSend(parsed.to, parsed.subject, parsed.body, { accountId: parsed.accountId || undefined, approved: parsed.approved });
868
880
  break;
869
881
  }
870
882
  case 'voice': {
@@ -888,7 +900,7 @@ async function gmailCommand(subcommand, ...args) {
888
900
  console.log(' atris gmail archive <id> [...] [--account <id>] - archive messages (reversible, all mail keeps them)');
889
901
  console.log(' atris gmail triage [--account <id>] [--limit N] - record keep or archive verdicts without changing mail');
890
902
  console.log(' atris gmail verdicts [--summary] [--account <id>] [--limit N] - list recent gmail verdicts');
891
- console.log(' atris gmail send <to> <subject> <body...> [--body-file <path>] [--account <id>] - send an email');
903
+ console.log(' atris gmail send <to> <subject> <body...> --approved [--body-file <path>] [--account <id>] - send an email');
892
904
  console.log(' atris gmail voice [account] [--clear] - edit or clear an account writing voice');
893
905
  console.log(' atris gmail connect [name] - connect or reconnect a gmail account');
894
906
  console.log(' atris gmail accounts - list connected gmail accounts and the active account');
@@ -135,6 +135,7 @@ Full skill (read if present): .claude/skills/interview/SKILL.md · spec: atris/f
135
135
  const modes = {
136
136
  operator: `Mode: OPERATOR WARM START. Interview the human at the keyboard. Open with the sharpest observed behavior in their recent journal/logs, especially repeated manual actions (unextracted members) and policies their own behavior contradicts. Output: a new member, an amendment, or a policy change.`,
137
137
  member: `Mode: MEMBER RE-INTERVIEW (performance review). Subject: the team member "${subject}". Read its folder (MEMBER.md, logs/, now.md, proofs) and interview the human ABOUT it: where it failed, what it should defend, what taste is missing from its file. Honest failure → document the lesson, no penalty. A false "done" is the one unforgivable act. Output: amend atris/team/${subject}/MEMBER.md.`,
138
+ one_on_one: `Mode: ONE-ON-ONE. You ARE the team member "${subject}". Speak in the first person as ${subject}, in the voice of your own SOUL.md and MEMBER.md, to your operator at the keyboard. This is a conversation with them, not an interview about you. Open with one honest observation from your own logs: what you shipped, where you stalled, what you are unsure of. One point or question per turn, and let their answer steer the next. Own failures plainly; a false "done" is the one unforgivable act. End by agreeing ONE concrete next step with them, then record it yourself by running: atris member goal ${subject} "<the next step>". Read the step back in one line, then say goodbye.`,
138
139
  expert: `Mode: COLD START. Subject: a new expert called "${subject}". No system data. Opener: "when someone is stuck, what's the problem where they say 'just ask you'?" (civilian variant: "what are you the designated one for?"). Then last real instance → live method demo → quality bar → verify question. Output: create atris/team/<slug>/MEMBER.md.`,
139
140
  };
140
141
 
@@ -147,7 +148,7 @@ Full skill (read if present): .claude/skills/interview/SKILL.md · spec: atris/f
147
148
  ].join('\n\n');
148
149
  }
149
150
 
150
- async function interviewCommand(args = [], root = process.cwd()) {
151
+ async function interviewCommand(args = [], root = process.cwd(), { mode: forcedMode = '' } = {}) {
151
152
  if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
152
153
  showHelp();
153
154
  return 0;
@@ -182,6 +183,16 @@ async function interviewCommand(args = [], root = process.cwd()) {
182
183
  path.join(ws, 'atris', 'now.md'),
183
184
  path.join(ws, 'atris', 'TODO.md')
184
185
  );
186
+ } else if (forcedMode === 'one_on_one' && fs.existsSync(memberDir)) {
187
+ mode = 'one_on_one';
188
+ contextPaths = existing(
189
+ path.join(memberDir, 'MEMBER.md'),
190
+ path.join(memberDir, 'SOUL.md'),
191
+ path.join(memberDir, 'MISSION.md'),
192
+ path.join(memberDir, 'now.md'),
193
+ path.join(memberDir, 'goals.md'),
194
+ path.join(memberDir, 'logs')
195
+ );
185
196
  } else if (fs.existsSync(memberDir)) {
186
197
  mode = 'member';
187
198
  contextPaths = existing(
@@ -195,10 +206,11 @@ async function interviewCommand(args = [], root = process.cwd()) {
195
206
  mode = 'expert';
196
207
  }
197
208
 
198
- const prompt = composePrompt(mode, mode === 'member' ? slug : subjectRaw, contextPaths, ws);
209
+ const memberMode = mode === 'member' || mode === 'one_on_one';
210
+ const prompt = composePrompt(mode, memberMode ? slug : subjectRaw, contextPaths, ws);
199
211
 
200
212
  console.log('');
201
- console.log(`◈ interview: ${mode === 'operator' ? 'you, from your logs' : mode === 'member' ? `1-on-1 about ${slug}` : `cold start: ${subjectRaw}`}`);
213
+ console.log(`◈ interview: ${mode === 'operator' ? 'you, from your logs' : mode === 'one_on_one' ? `1-on-1 with ${slug}` : mode === 'member' ? `1-on-1 about ${slug}` : `cold start: ${subjectRaw}`}`);
202
214
  if (contextPaths.length) console.log(` context: ${contextPaths.length} source${contextPaths.length > 1 ? 's' : ''} loaded`);
203
215
  console.log('');
204
216
 
package/commands/land.js CHANGED
@@ -2,7 +2,7 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const gitSpawn = require('../lib/git-spawn');
4
4
  const { compactErrorPayload, printCliJson } = require('../lib/cli-json');
5
- const { listWorktrees, statusCounts, defaultMainlineBase, listActiveProcessCwds, worktreeHasActiveProcess, recordWorktreeRemoval } = require('./worktree');
5
+ const { listWorktrees, statusCounts, defaultMainlineBase, listActiveProcessPaths, worktreeHasActiveProcess, recordWorktreeRemoval } = require('./worktree');
6
6
 
7
7
  const DEFAULT_TTL_DAYS = 7;
8
8
  const DEFAULT_STALE_HOURS = 48;
@@ -545,8 +545,8 @@ function reapReason(w, board, ttlDays, force) {
545
545
  // bundle cannot cover their commits, unattended reaps (autoland) pass false
546
546
  // and leave them for a human reap.
547
547
  function reap(root, { ttlDays = DEFAULT_TTL_DAYS, staleHours = DEFAULT_STALE_HOURS, base: baseOverride = '', dryRun = false, remote = true, includeDetached = true, protectCurrent = true, force = false, now = Date.now(), activeCwds: activeCwdsOverride, by = 'atris land --reap' } = {}) {
548
- const readActiveCwds = () => (Array.isArray(activeCwdsOverride) ? activeCwdsOverride : listActiveProcessCwds());
549
- let activeCwds = null;
548
+ const readActiveCwds = () => (Array.isArray(activeCwdsOverride) ? activeCwdsOverride : listActiveProcessPaths());
549
+ let activeCwds;
550
550
  const board = collectBoard(root, { ttlDays, staleHours, base: baseOverride, now });
551
551
  const targets = board.branches.filter((b) => b.state === 'landed' || b.state === 'due');
552
552
  const targetNames = new Set(targets.map((b) => b.name));
@@ -568,9 +568,9 @@ function reap(root, { ttlDays = DEFAULT_TTL_DAYS, staleHours = DEFAULT_STALE_HOU
568
568
  }
569
569
  // A run working inside the folder outranks --force: removing it pulls
570
570
  // the floor out from under a live engine.
571
- if (!activeCwds) activeCwds = readActiveCwds();
571
+ if (activeCwds === undefined) activeCwds = readActiveCwds();
572
572
  if (worktreeHasActiveProcess(w.path, activeCwds)) {
573
- protectedWorktrees.push({ ...w, reason: 'active_process' });
573
+ protectedWorktrees.push({ ...w, reason: activeCwds ? 'active_process' : 'process_scan_failed' });
574
574
  continue;
575
575
  }
576
576
  if (worktreeWithinReapGrace(w.path, now)) {
@@ -665,7 +665,7 @@ function reap(root, { ttlDays = DEFAULT_TTL_DAYS, staleHours = DEFAULT_STALE_HOU
665
665
  for (const w of survivingWorktreeTargets) {
666
666
  // A run can start in the folder while the bundle is built; look again.
667
667
  if (worktreeHasActiveProcess(w.path, activeAtRemoval)) {
668
- receipt.keptWorktrees.push(`${w.path} (active_process)`);
668
+ receipt.keptWorktrees.push(`${w.path} (${activeAtRemoval ? 'active_process' : 'process_scan_failed'})`);
669
669
  if (w.branch) targetNames.delete(w.branch);
670
670
  continue;
671
671
  }
@@ -984,6 +984,8 @@ function showHelp() {
984
984
  console.log(' atris land --auto --dry-run show which branch would be tried and why');
985
985
  console.log(' atris land --auto --verify "<cmd>" --limit <n>');
986
986
  console.log(' change the check (default: npm test) or the cap');
987
+ console.log(' atris land --requeue put member changes github dropped from its');
988
+ console.log(' merge line back in, caught up with master');
987
989
  console.log(' atris land --ttl <days> change the 7-day overdue line');
988
990
  console.log(' atris land --json machine-readable output');
989
991
  console.log('');
@@ -1025,6 +1027,20 @@ function landCommand(args = []) {
1025
1027
  }
1026
1028
  }
1027
1029
 
1030
+ if (args.includes('--requeue')) {
1031
+ try {
1032
+ const result = require('../lib/member-autoland').requeueDroppedAutolands({ cwd: root });
1033
+ if (json) console.log(JSON.stringify(result, null, 2));
1034
+ else {
1035
+ console.log(`merge line: ${result.line}`);
1036
+ for (const r of result.results) console.log(` ${r.line}`);
1037
+ }
1038
+ return 0;
1039
+ } catch (err) {
1040
+ return finishLandError(err, json);
1041
+ }
1042
+ }
1043
+
1028
1044
  if (args.includes('--reap')) {
1029
1045
  try {
1030
1046
  const receipt = reap(root, { ttlDays, base, dryRun: args.includes('--dry-run'), remote: !args.includes('--no-remote'), force: args.includes('--force') });
package/commands/link.js CHANGED
@@ -246,6 +246,7 @@ function normalizeLinkAnswer(parsed, members) {
246
246
  function compactFailure(answer) {
247
247
  if (!answer) return 'no answer';
248
248
  if (answer.reason === 'timeout') return `timed out after ${Math.round((answer.duration_ms || 0) / 1000)}s`;
249
+ if (answer.kill_cause) return `stopped by ${answer.signal || 'SIGKILL'}; ${answer.kill_cause}`;
249
250
  const detail = String(answer.stderr || '').trim().split(/\r?\n/).find(Boolean);
250
251
  return detail || String(answer.reason || 'failed').replace(/_/g, ' ');
251
252
  }