atris 3.53.0 → 3.55.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 (119) hide show
  1. package/FOR_AGENTS.md +7 -0
  2. package/README.md +1 -1
  3. package/atris/policies/ANTISLOP.md +2 -1
  4. package/atris/skills/copy-editor/SKILL.md +2 -1
  5. package/atris/skills/design/SKILL.md +1 -1
  6. package/atris/skills/x-search/SKILL.md +24 -22
  7. package/atris/skills/youtube/SKILL.md +82 -33
  8. package/atris/team/_template/SOUL.md +40 -0
  9. package/atris/team/validator/MEMBER.md +1 -2
  10. package/bin/atris +21 -0
  11. package/bin/atris.js +563 -373
  12. package/commands/api-key.js +170 -0
  13. package/commands/auth.js +453 -43
  14. package/commands/autoland.js +3 -1
  15. package/commands/autopilot-front.js +58 -11
  16. package/commands/avail.js +5 -4
  17. package/commands/balance.js +55 -0
  18. package/commands/bench.js +29 -1
  19. package/commands/brain.js +63 -12
  20. package/commands/brainstorm.js +18 -0
  21. package/commands/business.js +149 -1
  22. package/commands/chat-scan.js +17 -4
  23. package/commands/close.js +5 -1
  24. package/commands/doctor.js +174 -0
  25. package/commands/dream.js +7 -0
  26. package/commands/drill.js +34 -2
  27. package/commands/engine.js +47 -35
  28. package/commands/errors.js +58 -8
  29. package/commands/experiments.js +14 -0
  30. package/commands/feedback.js +61 -2
  31. package/commands/fleet-report.js +27 -7
  32. package/commands/fleet.js +98 -11
  33. package/commands/founder.js +60 -21
  34. package/commands/human-missions.js +79 -4
  35. package/commands/improve.js +54 -4
  36. package/commands/init.js +68 -92
  37. package/commands/install.js +40 -0
  38. package/commands/integrations.js +399 -30
  39. package/commands/interview.js +8 -0
  40. package/commands/land.js +48 -17
  41. package/commands/learn.js +29 -4
  42. package/commands/log.js +51 -0
  43. package/commands/member.js +68 -10
  44. package/commands/mission.js +287 -50
  45. package/commands/now.js +15 -3
  46. package/commands/orb.js +97 -2
  47. package/commands/pack.js +83 -9
  48. package/commands/playbook.js +381 -0
  49. package/commands/plugin.js +16 -0
  50. package/commands/radar.js +31 -7
  51. package/commands/recap.js +128 -21
  52. package/commands/release.js +102 -2
  53. package/commands/review.js +5 -10
  54. package/commands/revisions.js +55 -0
  55. package/commands/run-front.js +12 -4
  56. package/commands/scout.js +12 -1
  57. package/commands/search.js +123 -28
  58. package/commands/sign.js +2 -2
  59. package/commands/signup.js +12 -3
  60. package/commands/site-deploy.js +17 -4
  61. package/commands/site.js +2 -2
  62. package/commands/skill-eval.js +289 -0
  63. package/commands/skill.js +56 -6
  64. package/commands/social.js +603 -0
  65. package/commands/spaceship.js +69 -6
  66. package/commands/stream.js +8 -1
  67. package/commands/study.js +32 -19
  68. package/commands/sync.js +28 -22
  69. package/commands/task.js +418 -60
  70. package/commands/teach.js +72 -63
  71. package/commands/terminal.js +25 -9
  72. package/commands/topup.js +88 -0
  73. package/commands/truth.js +52 -6
  74. package/commands/usage.js +91 -0
  75. package/commands/verify.js +3 -2
  76. package/commands/version.js +3 -1
  77. package/commands/voice.js +5 -1
  78. package/commands/who.js +69 -11
  79. package/commands/wiki.js +7 -4
  80. package/commands/wish.js +76 -5
  81. package/commands/workflow.js +327 -145
  82. package/commands/worktree.js +1 -0
  83. package/commands/write.js +5 -0
  84. package/commands/x-search.js +340 -0
  85. package/commands/xp.js +5 -1
  86. package/commands/youtube.js +551 -25
  87. package/lib/account-bound.js +47 -0
  88. package/lib/auto-accept-certified.js +8 -1
  89. package/lib/auto-lessons.js +186 -0
  90. package/lib/autoland.js +4 -2
  91. package/lib/chat-log-scan.js +7 -4
  92. package/lib/cli-json.js +77 -0
  93. package/lib/cli-scope.js +26 -0
  94. package/lib/codex-flight.js +1 -0
  95. package/lib/conductor-artifacts.js +1 -1
  96. package/lib/context-gatherer.js +11 -0
  97. package/lib/developer-api.js +116 -0
  98. package/lib/feedback-triage.js +274 -0
  99. package/lib/first-minute.js +398 -0
  100. package/lib/fleet.js +115 -34
  101. package/lib/functional-owner.js +22 -0
  102. package/lib/known-commands.js +31 -5
  103. package/lib/member-scaffold.js +197 -0
  104. package/lib/mission-root.js +4 -2
  105. package/lib/noninteractive.js +88 -0
  106. package/lib/policy-lessons.js +4 -1
  107. package/lib/revision-metric.js +279 -0
  108. package/lib/scratch-root.js +48 -0
  109. package/lib/skill-eval-gate.js +249 -0
  110. package/lib/task-db.js +4 -0
  111. package/lib/task-proof.js +69 -8
  112. package/lib/verifier-quality.js +69 -0
  113. package/lib/wish-audit.js +2 -2
  114. package/lib/wish-delegate.js +10 -1
  115. package/lib/workspace-scaffold.js +270 -0
  116. package/package.json +3 -2
  117. package/scripts/det/checklist-score.js +191 -0
  118. package/scripts/det/ytsearch +31 -0
  119. package/utils/update-check.js +15 -0
@@ -6,6 +6,7 @@
6
6
  * atris feedback submit "message here" Submit feedback
7
7
  * atris feedback List feedback
8
8
  * atris feedback list List feedback
9
+ * atris feedback triage <id> [--dispatch] Build or dispatch a reproduction brief
9
10
  * atris feedback resolve <id> "<resolution>" Mark resolved (admin)
10
11
  * atris feedback close <id> Close as wontfix (admin)
11
12
  * atris feedback delete <id> Delete feedback (admin)
@@ -18,10 +19,17 @@ const fs = require('fs');
18
19
  const path = require('path');
19
20
  const { loadCredentials } = require('../utils/auth');
20
21
  const { apiRequestJson } = require('../utils/api');
22
+ const {
23
+ planFeedbackTriage,
24
+ renderFeedbackTriageBrief,
25
+ dispatchFeedbackTriage,
26
+ recordFeedbackTriageVerdict,
27
+ } = require('../lib/feedback-triage');
21
28
 
22
29
  const KNOWN_FEEDBACK_COMMANDS = new Set([
23
30
  'list',
24
31
  'submit',
32
+ 'triage',
25
33
  'resolve',
26
34
  'close',
27
35
  'delete',
@@ -151,6 +159,43 @@ async function resolveIdPrefix(prefix, { token, businessId }) {
151
159
  return { id: matches[0].id, item: matches[0] };
152
160
  }
153
161
 
162
+ function findFeedbackItem(prefix, items) {
163
+ const requested = String(prefix || '').trim();
164
+ if (!requested) return { error: 'feedback id is required' };
165
+ const matches = (items || []).filter((item) => String(item && item.id || '').startsWith(requested));
166
+ if (matches.length === 0) return { error: `No feedback matches id prefix "${requested}"` };
167
+ if (matches.length > 1) return { error: `Ambiguous id "${requested}"; matches ${matches.length} items. Use a longer prefix.` };
168
+ return { item: matches[0] };
169
+ }
170
+
171
+ async function triageFeedback(idPrefix, options = {}) {
172
+ const workspace = path.resolve(options.workspace || process.cwd());
173
+ const token = options.token || getAuth().token;
174
+ const fetchItems = options.fetchFeedbackItems || fetchFeedbackItems;
175
+ const items = await fetchItems({ token, businessId: options.businessId, limit: 200 });
176
+ const lookup = findFeedbackItem(idPrefix, items);
177
+ if (lookup.error) throw new Error(lookup.error);
178
+
179
+ const brief = planFeedbackTriage(lookup.item, workspace);
180
+ const write = options.write || console.log;
181
+ write(renderFeedbackTriageBrief(brief));
182
+ if (!options.dispatch) return { brief, dispatch: null, verdict: null };
183
+
184
+ const dispatchBrief = options.dispatchFeedbackTriage || dispatchFeedbackTriage;
185
+ const dispatched = await dispatchBrief(brief, workspace, options.dispatchDeps || {});
186
+ const recordVerdict = options.recordFeedbackTriageVerdict || recordFeedbackTriageVerdict;
187
+ const recorded = recordVerdict(lookup.item, workspace, {
188
+ verdict: dispatched.verdict,
189
+ evidence: dispatched.evidence,
190
+ verify_command: brief.verify_command,
191
+ engine: dispatched.engine,
192
+ });
193
+ write('');
194
+ write(`verdict: ${recorded.record.verdict}`);
195
+ write(`${recorded.appended ? 'recorded' : 'already recorded'}: ${path.relative(workspace, recorded.path)}`);
196
+ return { brief, dispatch: dispatched, verdict: recorded };
197
+ }
198
+
154
199
  async function resolveFeedback(idPrefix, resolution) {
155
200
  if (!idPrefix || !resolution) {
156
201
  console.error('Usage: atris feedback resolve <id> "<resolution>"');
@@ -236,6 +281,8 @@ function printHelp(write = console.log) {
236
281
  write(' atris feedback submit "msg" --business <slug>');
237
282
  write(' atris feedback List feedback');
238
283
  write(' atris feedback list List feedback');
284
+ write(' atris feedback triage <id> Print a reproduction brief');
285
+ write(' atris feedback triage <id> --dispatch Run triage and record its verdict');
239
286
  write(' atris feedback resolve <id> "<note>" Mark resolved (admin)');
240
287
  write(' atris feedback close <id> Close as wontfix (admin)');
241
288
  write(' atris feedback delete <id> Delete feedback (admin)');
@@ -296,7 +343,8 @@ function rejectUnknownFeedbackCommand(subcommand) {
296
343
 
297
344
  async function feedbackCommand() {
298
345
  const rawArgs = process.argv.slice(3);
299
- const [businessArg, args] = extractFlag(rawArgs, '--business', '-b');
346
+ const dispatchRequested = rawArgs.includes('--dispatch');
347
+ const [businessArg, args] = extractFlag(rawArgs.filter((arg) => arg !== '--dispatch'), '--business', '-b');
300
348
  const businessId = resolveBusinessArg(businessArg);
301
349
  if (businessArg && !businessId) {
302
350
  console.error(`Error: unknown business "${businessArg}". Check ~/.atris/businesses.json`);
@@ -329,6 +377,14 @@ async function feedbackCommand() {
329
377
  return;
330
378
  }
331
379
 
380
+ if (subcommand === 'triage') {
381
+ if (!args[1] || args.length !== 2) {
382
+ throw new Error('usage: atris feedback triage <id> [--dispatch]');
383
+ }
384
+ await triageFeedback(args[1], { businessId, dispatch: dispatchRequested });
385
+ return;
386
+ }
387
+
332
388
  if (subcommand === 'resolve') {
333
389
  const id = args[1];
334
390
  const resolution = args.slice(2).join(' ');
@@ -359,4 +415,7 @@ async function feedbackCommand() {
359
415
  rejectUnknownFeedbackCommand(subcommand);
360
416
  }
361
417
 
362
- module.exports = { feedbackCommand };
418
+ module.exports = {
419
+ feedbackCommand,
420
+ triageFeedback,
421
+ };
@@ -15,6 +15,8 @@
15
15
 
16
16
  const { loadCredentials } = require('../utils/auth');
17
17
  const { apiRequestJson } = require('../utils/api');
18
+ const { requireAccountBound, refuseAccountGlobal } = require('../lib/account-bound');
19
+ const { argsWantHelp, wantsJson } = require('../lib/noninteractive');
18
20
 
19
21
  function sleep(ms) {
20
22
  return new Promise((r) => setTimeout(r, ms));
@@ -157,15 +159,33 @@ async function leaderboard(token, businesses) {
157
159
 
158
160
  async function fleetReport() {
159
161
  const args = process.argv.slice(3);
160
- const allAlive = args.includes('--all-alive');
161
- const wake = args.includes('--wake');
162
- const dryRun = args.includes('--dry-run');
163
- const board = args.includes('--leaderboard');
164
- const slug = args.find((a) => !a.startsWith('--'));
162
+ const usage = 'Usage: atris fleet-report <business> [--wake] | --all-alive [--dry-run] | --leaderboard';
163
+ if (argsWantHelp(args) || wantsJson(args)) {
164
+ if (wantsJson(args)) {
165
+ console.log(JSON.stringify({
166
+ ok: false,
167
+ command: 'fleet-report',
168
+ error: 'usage',
169
+ usage,
170
+ }, null, 2));
171
+ } else {
172
+ console.log(usage);
173
+ }
174
+ process.exit(2);
175
+ }
176
+
177
+ const gate = requireAccountBound(args);
178
+ if (!gate.ok) process.exit(refuseAccountGlobal());
179
+
180
+ const allAlive = gate.args.includes('--all-alive');
181
+ const wake = gate.args.includes('--wake');
182
+ const dryRun = gate.args.includes('--dry-run');
183
+ const board = gate.args.includes('--leaderboard');
184
+ const slug = gate.args.find((a) => !a.startsWith('--'));
165
185
 
166
186
  if (!allAlive && !slug && !board) {
167
- console.log('Usage: atris fleet-report <business> [--wake] | --all-alive [--dry-run] | --leaderboard');
168
- process.exit(1);
187
+ console.log(usage);
188
+ process.exit(2);
169
189
  }
170
190
 
171
191
  const creds = loadCredentials();
package/commands/fleet.js CHANGED
@@ -19,6 +19,7 @@ const fs = require('fs');
19
19
  const path = require('path');
20
20
  const os = require('os');
21
21
  const crypto = require('crypto');
22
+ const { parseScopeFlag } = require('../lib/cli-scope');
22
23
 
23
24
  // ── Config ──────────────────────────────────────────────
24
25
 
@@ -327,21 +328,74 @@ async function fleetWatch(state, intervalSec = 10) {
327
328
  });
328
329
  }
329
330
 
331
+ async function fleetWorkspaceStatus(root = process.cwd(), { json = false } = {}) {
332
+ const teamDir = path.join(root, 'atris', 'team');
333
+ const members = [];
334
+ if (fs.existsSync(teamDir)) {
335
+ for (const name of fs.readdirSync(teamDir).filter((n) => !n.startsWith('_') && !n.startsWith('.'))) {
336
+ const memberFile = path.join(teamDir, name, 'MEMBER.md');
337
+ if (!fs.existsSync(memberFile)) continue;
338
+ members.push({ member_id: name, member_name: name, member_type: 'agent', source: 'workspace' });
339
+ }
340
+ }
341
+ const missionsPath = path.join(root, '.atris', 'state', 'missions.jsonl');
342
+ let activeMissions = 0;
343
+ if (fs.existsSync(missionsPath)) {
344
+ const seen = new Map();
345
+ for (const line of fs.readFileSync(missionsPath, 'utf8').split('\n')) {
346
+ if (!line.trim()) continue;
347
+ try {
348
+ const rec = JSON.parse(line);
349
+ const id = rec.id || rec.mission_id;
350
+ if (id) seen.set(id, rec);
351
+ } catch {}
352
+ }
353
+ activeMissions = [...seen.values()].filter((m) => ['ready', 'running', 'planning', 'paused'].includes(m.status)).length;
354
+ }
355
+ const payload = {
356
+ scope: 'workspace',
357
+ hub: null,
358
+ members: { count: members.length, members },
359
+ claims: { count: 0, claims: [] },
360
+ missions_active: activeMissions,
361
+ note: 'workspace scope only. pass --global to query the machine Swarlo hub.',
362
+ };
363
+ if (json) {
364
+ console.log(JSON.stringify(payload, null, 2));
365
+ return;
366
+ }
367
+ console.log(`\n Fleet (workspace)`);
368
+ console.log(` ─────────────────────────────────`);
369
+ console.log(` Scope: workspace`);
370
+ console.log(` Members: ${members.length} (from atris/team)`);
371
+ for (const mem of members.slice(0, 24)) {
372
+ console.log(` ${mem.member_name}`);
373
+ }
374
+ console.log(` Missions active: ${activeMissions}`);
375
+ console.log(` Hub: not queried (use --global for localhost Swarlo)`);
376
+ console.log();
377
+ }
378
+
330
379
  // ── Main ────────────────────────────────────────────────
331
380
 
332
381
  async function fleet(args = []) {
333
- const subcommand = (args[0] || 'status').toLowerCase();
334
- const rest = args.slice(1).join(' ');
382
+ const scope = parseScopeFlag(args);
383
+ const argv = scope.args.filter((a) => !String(a).startsWith('-'));
384
+ const flags = scope.args.filter((a) => String(a).startsWith('-'));
385
+ const subcommand = (argv[0] || 'status').toLowerCase();
386
+ const rest = argv.slice(1).join(' ');
387
+ const json = flags.includes('--json') || args.includes('--json');
335
388
 
336
389
  if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
337
390
  console.log('');
338
391
  console.log(' atris fleet — coordinate agent swarm via Swarlo');
339
392
  console.log('');
340
- console.log(' fleet show board status');
341
- console.log(' fleet post <msg> post message to board');
342
- console.log(' fleet task <prompt> create a task for agents');
343
- console.log(' fleet claim <key> claim a task');
344
- console.log(' fleet done <key> report task complete');
393
+ console.log(' fleet show workspace fleet (default)');
394
+ console.log(' fleet --global query the machine Swarlo hub');
395
+ console.log(' fleet post <msg> post message to board (global)');
396
+ console.log(' fleet task <prompt> create a task for agents (global)');
397
+ console.log(' fleet claim <key> claim a task (global)');
398
+ console.log(' fleet done <key> report task complete (global)');
345
399
  console.log(' fleet members who is online');
346
400
  console.log(' fleet prune [min] remove stale members (default 60m)');
347
401
  console.log(' fleet watch [sec] live-tail the board (default 10s)');
@@ -349,12 +403,42 @@ async function fleet(args = []) {
349
403
  return;
350
404
  }
351
405
 
406
+ if (!scope.global && (subcommand === 'status' || subcommand === 'st' || subcommand === 'members' || subcommand === 'who')) {
407
+ await fleetWorkspaceStatus(process.cwd(), { json });
408
+ return;
409
+ }
410
+
411
+ if (!scope.global && ['post', 'say', 'task', 'claim', 'done', 'report', 'prune', 'watch', 'tail'].includes(subcommand)) {
412
+ console.error('fleet mutations need the machine hub. re-run with --global.');
413
+ process.exitCode = 1;
414
+ return;
415
+ }
416
+
352
417
  const state = await ensureRegistered();
353
418
 
354
419
  switch (subcommand) {
355
420
  case 'status':
356
421
  case 'st':
357
- await fleetStatus(state);
422
+ if (json) {
423
+ const hubUrl = getHubUrl();
424
+ const hubId = getHubId();
425
+ const h = authHeaders(state);
426
+ const [members, posts, claims] = await Promise.all([
427
+ request('GET', `${hubUrl}/api/${hubId}/members`, null, h),
428
+ request('GET', `${hubUrl}/api/${hubId}/channels/general/posts?limit=5`, null, h),
429
+ request('GET', `${hubUrl}/api/${hubId}/claims`, null, h),
430
+ ]);
431
+ console.log(JSON.stringify({
432
+ scope: 'global',
433
+ hub: { id: hubId, url: hubUrl },
434
+ you: state.member_id,
435
+ members: members.data,
436
+ claims: claims.data,
437
+ recent: posts.data,
438
+ }, null, 2));
439
+ } else {
440
+ await fleetStatus(state);
441
+ }
358
442
  break;
359
443
  case 'post':
360
444
  case 'say':
@@ -387,9 +471,12 @@ async function fleet(args = []) {
387
471
  await fleetWatch(state, parseInt(rest) || 10);
388
472
  break;
389
473
  default:
390
- // If no subcommand match, treat the whole thing as status
391
- await fleetStatus(state);
474
+ if (!scope.global) {
475
+ await fleetWorkspaceStatus(process.cwd(), { json });
476
+ } else {
477
+ await fleetStatus(state);
478
+ }
392
479
  }
393
480
  }
394
481
 
395
- module.exports = { fleet };
482
+ module.exports = { fleet, fleetWorkspaceStatus };
@@ -41,11 +41,23 @@ function parseArgs(args = []) {
41
41
  let index = args[0] === 'score' ? 1 : 0;
42
42
  let root = null;
43
43
  let days = DEFAULT_DAYS;
44
+ let json = false;
45
+ let global = false;
44
46
 
45
47
  while (index < args.length) {
46
48
  const arg = String(args[index]);
47
49
  if (arg === '--help' || arg === '-h' || arg === 'help') {
48
- return { help: true, root, days };
50
+ return { help: true, root, days, json, global };
51
+ }
52
+ if (arg === '--json') {
53
+ json = true;
54
+ index += 1;
55
+ continue;
56
+ }
57
+ if (arg === '--global' || arg === '-g') {
58
+ global = true;
59
+ index += 1;
60
+ continue;
49
61
  }
50
62
  if (arg === '--root' || arg.startsWith('--root=')) {
51
63
  const value = arg.startsWith('--root=') ? arg.slice('--root='.length) : args[++index];
@@ -65,7 +77,7 @@ function parseArgs(args = []) {
65
77
  throw new Error(`unknown founder option: ${arg}`);
66
78
  }
67
79
 
68
- return { help: false, root, days };
80
+ return { help: false, root, days, json, global };
69
81
  }
70
82
 
71
83
  function founderNow(env = process.env) {
@@ -156,17 +168,26 @@ function discoverRepos(root) {
156
168
  .sort((left, right) => path.basename(left).localeCompare(path.basename(right)));
157
169
  }
158
170
 
159
- function collectRepoScorecards(root, bounds) {
160
- return discoverRepos(root).map((repoRoot) => {
161
- const branch = defaultBranch(repoRoot);
162
- const history = commitHistory(repoRoot, branch, bounds);
163
- return {
164
- repo: path.basename(repoRoot),
165
- commitsThisWeek: totalInRange(history.timestamps, bounds.currentWindowStart, bounds.until, true),
166
- commitsLastWeek: totalInRange(history.timestamps, bounds.priorWindowStart, bounds.currentWindowStart),
167
- perDay: history.perDay,
168
- };
169
- }).sort((left, right) => (
171
+ function scoreOneRepo(repoRoot, bounds) {
172
+ const branch = defaultBranch(repoRoot);
173
+ const history = commitHistory(repoRoot, branch, bounds);
174
+ return {
175
+ repo: path.basename(repoRoot),
176
+ commitsThisWeek: totalInRange(history.timestamps, bounds.currentWindowStart, bounds.until, true),
177
+ commitsLastWeek: totalInRange(history.timestamps, bounds.priorWindowStart, bounds.currentWindowStart),
178
+ perDay: history.perDay,
179
+ };
180
+ }
181
+
182
+ function collectRepoScorecards(root, bounds, { singleRepo = false, workspace = null } = {}) {
183
+ if (singleRepo) {
184
+ const target = workspace || root;
185
+ if (fs.existsSync(path.join(target, '.git')) || runGit(target, ['rev-parse', '--is-inside-work-tree']).ok) {
186
+ return [scoreOneRepo(target, bounds)];
187
+ }
188
+ return [];
189
+ }
190
+ return discoverRepos(root).map((repoRoot) => scoreOneRepo(repoRoot, bounds)).sort((left, right) => (
170
191
  right.commitsThisWeek - left.commitsThisWeek
171
192
  || right.commitsLastWeek - left.commitsLastWeek
172
193
  || left.repo.localeCompare(right.repo)
@@ -220,12 +241,18 @@ function percentChange(current, previous) {
220
241
  return Math.round(((current - previous) / previous) * 100);
221
242
  }
222
243
 
223
- function buildFounderScorecard(root, { days = DEFAULT_DAYS, now = new Date() } = {}) {
244
+ function buildFounderScorecard(root, {
245
+ days = DEFAULT_DAYS,
246
+ now = new Date(),
247
+ singleRepo = false,
248
+ workspace = null,
249
+ } = {}) {
224
250
  const bounds = historyBounds(now, days);
225
- const perRepo = collectRepoScorecards(root, bounds);
251
+ const perRepo = collectRepoScorecards(root, bounds, { singleRepo, workspace });
226
252
  const commitsThisWeek = perRepo.reduce((total, repo) => total + repo.commitsThisWeek, 0);
227
253
  const commitsLastWeek = perRepo.reduce((total, repo) => total + repo.commitsLastWeek, 0);
228
- const tasks = readTaskScorecard(root, bounds);
254
+ const taskRoot = singleRepo ? (workspace || root) : root;
255
+ const tasks = readTaskScorecard(taskRoot, bounds);
229
256
 
230
257
  return {
231
258
  ts: now.toISOString(),
@@ -272,8 +299,10 @@ function renderFounderScorecard(scorecard) {
272
299
  }
273
300
 
274
301
  function showFounderHelp() {
275
- console.log('usage: atris founder [score] [--days n] [--root dir]');
302
+ console.log('usage: atris founder [score] [--days n] [--root dir] [--json] [--global]');
276
303
  console.log('shows the last 7 days against the prior 7 days from git and task receipts.');
304
+ console.log('default scope is this workspace. pass --global to scan sibling repos.');
305
+ console.log('supported flags: --days, --root, --json, --global, --help, -h');
277
306
  }
278
307
 
279
308
  function founderCommand(args = [], options = {}) {
@@ -282,7 +311,8 @@ function founderCommand(args = [], options = {}) {
282
311
  parsed = parseArgs(args);
283
312
  } catch (error) {
284
313
  console.error(`error: ${error.message}`);
285
- return 1;
314
+ console.error('supported flags: --days, --root, --json, --global, --help, -h');
315
+ return 2;
286
316
  }
287
317
  if (parsed.help) {
288
318
  showFounderHelp();
@@ -293,11 +323,20 @@ function founderCommand(args = [], options = {}) {
293
323
  const currentWorkspace = workspaceRoot(cwd);
294
324
  const root = parsed.root
295
325
  ? path.resolve(cwd, parsed.root)
296
- : defaultScanRoot(cwd, currentWorkspace);
326
+ : (parsed.global ? defaultScanRoot(cwd, currentWorkspace) : currentWorkspace);
297
327
  const now = founderNow(options.env || process.env);
298
- const scorecard = buildFounderScorecard(root, { days: parsed.days, now });
328
+ const scorecard = buildFounderScorecard(root, {
329
+ days: parsed.days,
330
+ now,
331
+ singleRepo: !parsed.global && !parsed.root,
332
+ workspace: currentWorkspace,
333
+ });
299
334
  appendFounderScorecard(currentWorkspace, scorecard);
300
- console.log(renderFounderScorecard(scorecard));
335
+ if (parsed.json) {
336
+ console.log(JSON.stringify(scorecard, null, 2));
337
+ } else {
338
+ console.log(renderFounderScorecard(scorecard));
339
+ }
301
340
  return 0;
302
341
  }
303
342
 
@@ -13,6 +13,8 @@ const {
13
13
  } = require('../lib/cloud-mission');
14
14
  const { apiRequestJson } = require('../utils/api');
15
15
  const { decodeJwtClaims, loadCredentials } = require('../utils/auth');
16
+ const { isHelpToken } = require('../lib/noninteractive');
17
+ const { isFreshWorkspace, speakFirstMinute } = require('../lib/first-minute');
16
18
 
17
19
  const PACKAGE_PATH = path.join(__dirname, '..', 'package.json');
18
20
  const HUMAN_STATES = Object.freeze({
@@ -138,6 +140,50 @@ function readBusinessBinding(root) {
138
140
  }
139
141
  }
140
142
 
143
+ function isBoundCloudComputer(root = process.cwd()) {
144
+ return Boolean(readBusinessBinding(root));
145
+ }
146
+
147
+ function parseMissionId(args = []) {
148
+ const list = Array.isArray(args) ? args : [];
149
+ for (let i = 0; i < list.length; i += 1) {
150
+ const value = String(list[i] || '');
151
+ if (value === '--mission' || value === '--mission-id' || value === '--id') {
152
+ const next = list[i + 1];
153
+ if (next && !String(next).startsWith('-') && !isHelpToken(next)) return String(next).trim();
154
+ return '';
155
+ }
156
+ if (value.startsWith('--mission=') || value.startsWith('--mission-id=') || value.startsWith('--id=')) {
157
+ return value.slice(value.indexOf('=') + 1).trim();
158
+ }
159
+ }
160
+ // Bare positional id for stop/ready/approve (never treat free-sentence ask text as an id).
161
+ const flags = new Set(['--json', '--help', '-h', 'help']);
162
+ const positional = list.find((value) => value && !String(value).startsWith('-') && !flags.has(value));
163
+ if (!positional) return '';
164
+ // Mission ids are compact tokens, not multi-word wants.
165
+ if (list.filter((value) => value && !String(value).startsWith('-') && !flags.has(value)).length > 1) return '';
166
+ return String(positional).trim();
167
+ }
168
+
169
+ /**
170
+ * Scratch / unbound folders must not drive account-current cloud work.
171
+ * Require an explicit mission/task id: atris stop --mission <id>
172
+ */
173
+ function refuseUnboundCloudComputer(args = [], options = {}, commandName = 'stop') {
174
+ const root = options.root || process.cwd();
175
+ if (options.businessId || isBoundCloudComputer(root)) return null;
176
+ if (commandName !== 'ask') {
177
+ const missionId = parseMissionId(args);
178
+ if (missionId) return null;
179
+ }
180
+ return new HumanCommandError(
181
+ 'This folder is not a bound cloud-computer (missing .atris/business.json).',
182
+ `Pass --mission <id>: atris ${commandName} --mission <id>`,
183
+ 2,
184
+ );
185
+ }
186
+
141
187
  function credentialUserId(credentials) {
142
188
  const claims = decodeJwtClaims(credentials && credentials.token);
143
189
  return String(
@@ -488,12 +534,30 @@ function credentialsOrThrow(options = {}) {
488
534
  return credentials;
489
535
  }
490
536
 
537
+ function isBareAsk(args = []) {
538
+ return (Array.isArray(args) ? args : []).every((value) => value === '--json');
539
+ }
540
+
491
541
  async function askCommand(args, options = {}) {
492
542
  const asJson = args.includes('--json');
493
543
  if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
494
544
  (options.log || console.log)('Usage: atris ask "what you want" [--budget <usd>] [--json]');
495
545
  return 0;
496
546
  }
547
+ const root = options.root || process.cwd();
548
+ const fresh = isFreshWorkspace(root);
549
+ // Empty folder talks like bare atris. Do not start a mission or hit the
550
+ // network. After init, bare ask stays in the room.
551
+ if (!options.businessId && (fresh || isBareAsk(args))) {
552
+ return speakFirstMinute({
553
+ root,
554
+ fresh,
555
+ asJson,
556
+ log: options.log || console.log,
557
+ });
558
+ }
559
+ const unbound = refuseUnboundCloudComputer(args, options, 'ask');
560
+ if (unbound) return reportError(unbound, 'start that mission', asJson, options);
497
561
  try {
498
562
  const parsed = parseAskArgs(args);
499
563
  const credentials = credentialsOrThrow(options);
@@ -620,17 +684,26 @@ async function proofCommand(args = [], options = {}) {
620
684
 
621
685
  async function changeCurrentMission(action, body, args, options = {}) {
622
686
  const asJson = args.includes('--json');
687
+ const commandName = action === 'approve' ? 'approve' : action === 'answer' ? 'mission answer' : 'stop';
688
+ const unbound = refuseUnboundCloudComputer(args, options, commandName === 'mission answer' ? 'stop' : commandName);
689
+ if (unbound) return reportError(unbound, action === 'approve' ? 'approve that step' : action === 'answer' ? 'send that answer' : 'stop that mission', asJson, options);
623
690
  try {
624
691
  credentialsOrThrow(options);
625
- const current = await loadCurrentMission(options);
626
- const currentCard = missionCard(current, {}, options.now ? options.now() : Date.now());
692
+ const explicitId = parseMissionId(args);
693
+ let currentCard;
694
+ if (explicitId) {
695
+ currentCard = missionCard({ mission_id: explicitId }, {}, options.now ? options.now() : Date.now());
696
+ } else {
697
+ const current = await loadCurrentMission(options);
698
+ currentCard = missionCard(current, {}, options.now ? options.now() : Date.now());
699
+ }
627
700
  if (!currentCard.mission_id) {
628
701
  throw new HumanCommandError(
629
702
  'Atris could not identify the current mission.',
630
703
  'Run: atris mission',
631
704
  );
632
705
  }
633
- if (['approve', 'answer'].includes(action) && currentCard.state !== 'your_turn') {
706
+ if (['approve', 'answer'].includes(action) && currentCard.state !== 'your_turn' && !explicitId) {
634
707
  throw new HumanCommandError(
635
708
  action === 'approve'
636
709
  ? 'Nothing is waiting for your approval.'
@@ -658,7 +731,7 @@ async function approveCommand(args, options = {}) {
658
731
 
659
732
  async function stopCommand(args, options = {}) {
660
733
  if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
661
- (options.log || console.log)('Usage: atris stop [--json]');
734
+ (options.log || console.log)('Usage: atris stop [--mission <id>] [--json]');
662
735
  return 0;
663
736
  }
664
737
  return changeCurrentMission('stop', {}, args, options);
@@ -776,6 +849,8 @@ async function readyCommand(args, options = {}) {
776
849
  (options.log || console.log)('Usage: atris ready --json');
777
850
  return 0;
778
851
  }
852
+ const unbound = refuseUnboundCloudComputer(args, options, 'ready');
853
+ if (unbound) return reportError(unbound, 'check readiness', asJson, options);
779
854
  const request = options.apiRequestJson || apiRequestJson;
780
855
  const load = options.loadCredentials || loadCredentials;
781
856
  const credentials = load() || {};
@@ -433,10 +433,19 @@ function collectImproveVitals(options = {}, deps = {}) {
433
433
  const cronFn = deps.cronInstalled || cronInstalled;
434
434
  // Per-repo slots (pr 310): the crontab marker is derived from the root, so
435
435
  // the check must ask about THIS repo's markers, not the legacy default.
436
- const slotMarkers = (() => {
437
- try { return pulse.resolvePulseSlot(root).markers; } catch { return undefined; }
436
+ const slot = (() => {
437
+ try { return pulse.resolvePulseSlot(root); } catch { return null; }
438
438
  })();
439
- const installed = Boolean(slotMarkers ? cronFn(slotMarkers) : cronFn());
439
+ const slotMarkers = slot ? slot.markers : undefined;
440
+ const legacyCronInstalled = Boolean(slotMarkers ? cronFn(slotMarkers) : cronFn());
441
+ const homeDir = deps.homeDir || os.homedir();
442
+ const launchdInstalled = (() => {
443
+ if (!slot) return false;
444
+ const label = `com.atris.pulse.${slot.marker.toLowerCase().replace(/^atris_pulse_/, '').replace(/[^a-z0-9]+/g, '-')}`;
445
+ const plistPath = path.join(homeDir, 'Library', 'LaunchAgents', `${label}.plist`);
446
+ return fs.existsSync(plistPath);
447
+ })();
448
+ const installed = launchdInstalled || legacyCronInstalled;
440
449
  const installNudge = installed ? null : 'the scheduled improve loop is off. turn it on: atris pulse install --model claude-sonnet-5';
441
450
 
442
451
  const experimentsPath = path.join(root, '.atris', 'state', 'experiments-daily.json');
@@ -803,6 +812,29 @@ async function runImprove(opts = {}, deps = {}) {
803
812
  };
804
813
  };
805
814
 
815
+ // Dry-run is a local plan only. Never call the paid /api/improve endpoint.
816
+ if (opts.dryRun) {
817
+ const finishedAt = now();
818
+ return {
819
+ ok: true,
820
+ source: 'local',
821
+ reason: 'dry_run',
822
+ summary: {
823
+ shipped: '(dry-run) no paid improve call',
824
+ verify: null,
825
+ reward: null,
826
+ credits: null,
827
+ files: [],
828
+ dry_run: true,
829
+ },
830
+ scorecardPath: null,
831
+ journalPath: null,
832
+ receipt: 'skipped',
833
+ startedAt,
834
+ finishedAt,
835
+ };
836
+ }
837
+
806
838
  // No auth → local fallback (or report if fallback disabled).
807
839
  if (!creds || !creds.token) {
808
840
  if (!localFallbackEligible) {
@@ -934,6 +966,24 @@ function formatImproveReport(result = {}) {
934
966
 
935
967
  const REVISIONS_SCHEMA = 'atris.improve_revisions.v1';
936
968
  const REVISION_WINDOW_HOURS = 72;
969
+ // Shared journal and state files are touched by agents and humans alike as a
970
+ // matter of routine (daily logs, now.md, run receipts), so overlap there says
971
+ // nothing about the quality of a landing. On 2026-08-21 half the "revisions"
972
+ // in a live reading were journal commits following journal commits. Only
973
+ // product-file overlap counts.
974
+ const REVISION_IGNORED_FILES = [
975
+ /^atris\/logs\//,
976
+ /^atris\/runs\//,
977
+ /^atris\/team\/[^/]+\/(?:logs|now\.md)/,
978
+ /^atris\/(?:now|thinking|TODO)\.md$/,
979
+ /^atris\/brain\//,
980
+ /^atris\/status\//,
981
+ /^\.atris\//,
982
+ ];
983
+
984
+ function isRevisionSignalFile(file) {
985
+ return !REVISION_IGNORED_FILES.some((re) => re.test(String(file || '')));
986
+ }
937
987
  const REVISION_WINDOW_MS = REVISION_WINDOW_HOURS * 60 * 60 * 1000;
938
988
  const AGENT_TRAILER_MARKERS = [
939
989
  'atris-builder[bot]',
@@ -1041,7 +1091,7 @@ function collectRevisionSignals(root, options = {}) {
1041
1091
 
1042
1092
  const revisions = [];
1043
1093
  for (const landing of landings) {
1044
- const landedFiles = new Set(filesOf(landing));
1094
+ const landedFiles = new Set(filesOf(landing).filter(isRevisionSignalFile));
1045
1095
  if (!landedFiles.size) continue;
1046
1096
  const revisedBy = [];
1047
1097
  const touched = new Set();