atris 3.52.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 (131) 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/engines/SKILL.md +8 -7
  7. package/atris/skills/x-search/SKILL.md +24 -22
  8. package/atris/skills/youtube/SKILL.md +89 -40
  9. package/atris/team/_template/SOUL.md +40 -0
  10. package/atris/team/validator/MEMBER.md +1 -2
  11. package/ax +592 -91
  12. package/bin/atris +21 -0
  13. package/bin/atris.js +580 -378
  14. package/commands/api-key.js +170 -0
  15. package/commands/auth.js +453 -43
  16. package/commands/autoland.js +3 -1
  17. package/commands/autopilot-front.js +58 -11
  18. package/commands/avail.js +5 -4
  19. package/commands/balance.js +55 -0
  20. package/commands/bench.js +29 -1
  21. package/commands/brain.js +63 -12
  22. package/commands/brainstorm.js +18 -0
  23. package/commands/business.js +149 -1
  24. package/commands/chat-scan.js +17 -4
  25. package/commands/close.js +5 -1
  26. package/commands/computer.js +2 -2
  27. package/commands/doctor.js +174 -0
  28. package/commands/dream.js +7 -0
  29. package/commands/drill.js +34 -2
  30. package/commands/engine.js +47 -35
  31. package/commands/errors.js +58 -8
  32. package/commands/experiments.js +14 -0
  33. package/commands/feedback.js +61 -2
  34. package/commands/fleet-report.js +27 -7
  35. package/commands/fleet.js +98 -11
  36. package/commands/founder.js +60 -21
  37. package/commands/human-missions.js +79 -4
  38. package/commands/improve.js +54 -4
  39. package/commands/init.js +68 -92
  40. package/commands/install.js +40 -0
  41. package/commands/integrations.js +399 -30
  42. package/commands/interview.js +8 -0
  43. package/commands/land.js +48 -17
  44. package/commands/learn.js +29 -4
  45. package/commands/log.js +51 -0
  46. package/commands/member.js +68 -10
  47. package/commands/mission.js +339 -74
  48. package/commands/now.js +25 -6
  49. package/commands/orb.js +97 -2
  50. package/commands/pack.js +83 -9
  51. package/commands/playbook.js +381 -0
  52. package/commands/plugin.js +16 -0
  53. package/commands/radar.js +96 -40
  54. package/commands/recap.js +128 -21
  55. package/commands/release.js +102 -2
  56. package/commands/review.js +5 -10
  57. package/commands/revisions.js +55 -0
  58. package/commands/run-front.js +12 -4
  59. package/commands/scout.js +12 -1
  60. package/commands/search.js +123 -28
  61. package/commands/sign.js +2 -2
  62. package/commands/signup.js +12 -3
  63. package/commands/site-deploy.js +17 -4
  64. package/commands/site.js +2 -2
  65. package/commands/skill-eval.js +289 -0
  66. package/commands/skill.js +56 -6
  67. package/commands/social.js +603 -0
  68. package/commands/spaceship.js +69 -6
  69. package/commands/stream.js +8 -1
  70. package/commands/study.js +32 -19
  71. package/commands/sync.js +28 -22
  72. package/commands/task.js +418 -60
  73. package/commands/teach.js +72 -63
  74. package/commands/terminal.js +25 -9
  75. package/commands/topup.js +88 -0
  76. package/commands/truth.js +52 -6
  77. package/commands/usage.js +91 -0
  78. package/commands/verify.js +3 -2
  79. package/commands/version.js +3 -1
  80. package/commands/voice.js +5 -1
  81. package/commands/who.js +69 -11
  82. package/commands/wiki.js +7 -4
  83. package/commands/wish.js +76 -5
  84. package/commands/workflow.js +327 -145
  85. package/commands/worktree.js +1 -0
  86. package/commands/write.js +5 -0
  87. package/commands/x-search.js +340 -0
  88. package/commands/xp.js +5 -1
  89. package/commands/youtube.js +551 -33
  90. package/lib/account-bound.js +47 -0
  91. package/lib/auto-accept-certified.js +10 -2
  92. package/lib/auto-lessons.js +186 -0
  93. package/lib/autoland.js +4 -2
  94. package/lib/chat-log-scan.js +7 -4
  95. package/lib/cli-json.js +77 -0
  96. package/lib/cli-scope.js +26 -0
  97. package/lib/codex-flight.js +1 -0
  98. package/lib/conductor-artifacts.js +1 -1
  99. package/lib/context-gatherer.js +11 -0
  100. package/lib/developer-api.js +116 -0
  101. package/lib/engine-ask.js +11 -3
  102. package/lib/engine-registry.js +32 -7
  103. package/lib/feedback-triage.js +274 -0
  104. package/lib/first-minute.js +398 -0
  105. package/lib/fleet.js +115 -34
  106. package/lib/functional-owner.js +22 -0
  107. package/lib/known-commands.js +31 -5
  108. package/lib/member-scaffold.js +197 -0
  109. package/lib/mission-ledger-compact.js +127 -0
  110. package/lib/mission-root.js +4 -2
  111. package/lib/mission-runtime-loop.js +30 -2
  112. package/lib/next-moves.js +34 -34
  113. package/lib/noninteractive.js +88 -0
  114. package/lib/permission-grants.js +10 -1
  115. package/lib/policy-lessons.js +4 -1
  116. package/lib/revision-metric.js +279 -0
  117. package/lib/runner-command.js +5 -4
  118. package/lib/scratch-root.js +48 -0
  119. package/lib/skill-eval-gate.js +249 -0
  120. package/lib/task-db.js +22 -2
  121. package/lib/task-proof.js +75 -9
  122. package/lib/task-receipt.js +5 -1
  123. package/lib/verifier-quality.js +69 -0
  124. package/lib/wish-audit.js +2 -2
  125. package/lib/wish-delegate.js +10 -1
  126. package/lib/workspace-scaffold.js +270 -0
  127. package/package.json +4 -2
  128. package/scripts/det/checklist-score.js +191 -0
  129. package/scripts/det/ytsearch +31 -0
  130. package/scripts/outbound-artifact-gate.js +227 -0
  131. package/utils/update-check.js +15 -0
package/commands/recap.js CHANGED
@@ -1,5 +1,7 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
+ const { isCertifiedReview, personName } = require('../lib/first-minute');
4
+ const { isRealTestRunnerProof, quoteVerifierCommand } = require('../lib/verifier-quality');
3
5
 
4
6
  const DAY_MS = 24 * 60 * 60 * 1000;
5
7
  const DEFAULT_DAYS = 7;
@@ -30,8 +32,10 @@ function loadTasks(root) {
30
32
  const db = taskDb.open();
31
33
  const ws = taskDb.workspaceRoot(root);
32
34
  const rows = taskDb.listTasks(db, { workspaceRoot: ws });
33
- const refs = taskDb.taskDisplayRefMap(rows);
34
- return rows.map(row => ({ ...row, display_id: refs.get(row.id) || row.id.slice(-6) }));
35
+ if (Array.isArray(rows) && rows.length) {
36
+ const refs = taskDb.taskDisplayRefMap(rows);
37
+ return rows.map(row => ({ ...row, display_id: refs.get(row.id) || row.id.slice(-6) }));
38
+ }
35
39
  } catch (e) {
36
40
  // fall through to projection
37
41
  }
@@ -59,14 +63,22 @@ function plainCheck(proof, width = 70) {
59
63
  const checks = [];
60
64
  const add = label => { if (!checks.includes(label)) checks.push(label); };
61
65
 
66
+ const quoted = quoteVerifierCommand(flat);
67
+ if (quoted) add(`ran \`${quoted}\``);
68
+
62
69
  if (/\b(PR|pull request)\b.*\b(merged|MERGED)\b|\bmerged\b.*\b(PR|pull request)\b/i.test(flat)) add('merged');
63
- if (/\b(node --test|npm test|npm run test|pytest|go test|cargo test|test\/|tests?)\b/i.test(flat)
64
- && /\b(pass|passed|green|ok|0 failures?|9\/9|12\/12)\b/i.test(flat)) add('tests passed');
65
- if (/\b(node --check|git diff --check|git diff --exit-code|git diff --quiet|rg\b|grep\b|diff --brief|cmp -s)\b/i.test(flat)) add('code check passed');
70
+ // Only say "tests passed" when a real test runner appears in the proof.
71
+ // A bare `test -f` / file-exists check must not claim tests ran.
72
+ if (isRealTestRunnerProof(flat)) add('tests passed');
73
+ if (/\b(node --check|git diff --check|git diff --exit-code|git diff --quiet)\b/i.test(flat)) add('code check passed');
74
+ if (/\b(?:\brg\b|\bgrep\b)\b/i.test(flat) && !/\btest\s+-[efsd]\b/i.test(flat)) add('content check passed');
66
75
  if (/\b(bench|benchmark|measured|latency|speed)\b/i.test(flat)) add('measured improvement');
67
76
  if (/\brepeated agent review\b|\bagent review\b/i.test(flat)) add('reviewed repeatedly');
68
77
  if (/\batris\/runs\/[^\s]+\.json\b|receipt\b/i.test(flat)) add('record saved');
69
78
  if (/\b(human approved|accepted by|accepted_at|reward)\b/i.test(flat)) add('human accepted');
79
+ if (/\btest\s+-[efsd]\b|\b\[\s+-[efsd]\b/i.test(flat) && !isRealTestRunnerProof(flat)) {
80
+ add(quoted ? 'file check' : 'file check only');
81
+ }
70
82
 
71
83
  if (checks.length) return checks.join(', ');
72
84
  return shortProof(proof, width);
@@ -99,28 +111,38 @@ function buildRecapData(root = process.cwd(), { days = DEFAULT_DAYS } = {}) {
99
111
  done_at: t.done_at || null,
100
112
  });
101
113
 
114
+ const newestFirst = (a, b) => (
115
+ Number(b.updated_at || b.created_at || 0) - Number(a.updated_at || a.created_at || 0)
116
+ );
102
117
  const shipped = tasks
103
118
  .filter(t => t.status === 'done' && Number(t.done_at || 0) >= cutoff)
104
119
  .sort((a, b) => Number(b.done_at || 0) - Number(a.done_at || 0))
105
120
  .map(pick);
106
- const waiting = tasks
107
- .filter(t => t.status === 'review')
108
- .sort((a, b) => Number(b.updated_at || 0) - Number(a.updated_at || 0))
121
+ const reviewTasks = tasks.filter(t => t.status === 'review');
122
+ const waiting = reviewTasks
123
+ .filter(isCertifiedReview)
124
+ .sort(newestFirst)
125
+ .map(pick);
126
+ const checking = reviewTasks
127
+ .filter(t => !isCertifiedReview(t))
128
+ .sort(newestFirst)
109
129
  .map(pick);
110
130
  const inProgress = tasks
111
131
  .filter(t => t.status === 'open' || t.status === 'claimed')
112
132
  .map(pick);
113
133
 
114
- const withProof = [...shipped, ...waiting].filter(t => t.proof).length;
134
+ const withProof = [...shipped, ...waiting, ...checking].filter(t => t.proof).length;
115
135
  return {
116
136
  empty: false,
117
137
  days: windowDays,
118
138
  workspace: path.basename(root),
119
139
  shipped,
120
140
  waiting,
141
+ checking,
121
142
  inProgress,
143
+ next: waiting.length ? `atris task accept ${waiting[0].id}` : null,
122
144
  proof_attached: withProof,
123
- proof_total: shipped.length + waiting.length,
145
+ proof_total: shipped.length + waiting.length + checking.length,
124
146
  };
125
147
  }
126
148
 
@@ -140,8 +162,9 @@ function renderRecap(data) {
140
162
  const headline = [];
141
163
  if (data.shipped.length) headline.push(`${data.shipped.length} done`);
142
164
  if (data.waiting.length) headline.push(`${data.waiting.length} needs you`);
165
+ if (data.checking.length) headline.push(`${data.checking.length} still being checked`);
143
166
  if (data.inProgress.length) headline.push(`${data.inProgress.length} still working`);
144
- lines.push(headline.length ? headline.join(' · ') : 'Quiet window no movement in this period.');
167
+ lines.push(headline.length ? headline.join(' · ') : 'Quiet window. nothing moved in this period.');
145
168
 
146
169
  if (data.shipped.length) {
147
170
  lines.push('');
@@ -163,7 +186,16 @@ function renderRecap(data) {
163
186
  if (check) lines.push(` checked: ${check}`);
164
187
  }
165
188
  if (data.waiting.length > 10) lines.push(` … and ${data.waiting.length - 10} more`);
166
- lines.push(' next: run atris task reviews');
189
+ if (data.next) lines.push(` next: ${data.next}`);
190
+ }
191
+
192
+ if (data.checking.length) {
193
+ lines.push('');
194
+ lines.push(`STILL BEING CHECKED — ${data.checking.length}`);
195
+ for (const t of data.checking.slice(0, 10)) {
196
+ lines.push(` ${t.id} ${shortTitle(t.title)}`);
197
+ }
198
+ if (data.checking.length > 10) lines.push(` … and ${data.checking.length - 10} more`);
167
199
  }
168
200
 
169
201
  if (data.inProgress.length) {
@@ -187,6 +219,7 @@ function renderShare(data) {
187
219
  lines.push('');
188
220
  if (data.shipped.length) lines.push(`- ${data.shipped.length} done and accepted`);
189
221
  if (data.waiting.length) lines.push(`- ${data.waiting.length} ready for you to approve or send back`);
222
+ if (data.checking.length) lines.push(`- ${data.checking.length} still being checked`);
190
223
  if (data.inProgress.length) lines.push(`- ${data.inProgress.length} still being worked on`);
191
224
  const highlights = [...data.shipped, ...data.waiting].filter(t => t.proof).slice(0, 5);
192
225
  if (highlights.length) {
@@ -201,17 +234,83 @@ function renderShare(data) {
201
234
  return lines.join('\n');
202
235
  }
203
236
 
237
+ function recapSoftTitle(title, maxWords = 5) {
238
+ const words = String(title || '').replace(/\s+/g, ' ').trim().split(' ').filter(Boolean);
239
+ if (!words.length) return '';
240
+ const text = words.slice(0, maxWords).join(' ').replace(/[.,;:!?]+$/g, '');
241
+ return `"${text.toLowerCase()}"`;
242
+ }
243
+
244
+ function renderRecapMinute(data, { person } = {}) {
245
+ const who = person != null ? person : personName();
246
+ const greet = who ? `hey ${who}, ` : '';
247
+ if (data.empty) {
248
+ return [
249
+ `${greet}no task history yet.`,
250
+ '',
251
+ 'next: atris init --minimal',
252
+ ].join('\n');
253
+ }
254
+
255
+ const waiting = Array.isArray(data.waiting) ? data.waiting : [];
256
+ const checking = Array.isArray(data.checking) ? data.checking : [];
257
+ const shipped = Array.isArray(data.shipped) ? data.shipped : [];
258
+ const inProgress = Array.isArray(data.inProgress) ? data.inProgress : [];
259
+ const certified = waiting[0] || null;
260
+ const title = certified ? recapSoftTitle(certified.title) : '';
261
+
262
+ if (certified) {
263
+ const win = title
264
+ ? `${greet}${title} is waiting for your ok.`
265
+ : `${greet}one finished thing is waiting for your ok.`;
266
+ const lines = [win];
267
+ if (checking.length === 1) lines.push('1 still being checked.');
268
+ if (checking.length > 1) lines.push(`${checking.length} still being checked.`);
269
+ if (data.next) {
270
+ lines.push('');
271
+ lines.push(`next: ${data.next}`);
272
+ }
273
+ return lines.join('\n');
274
+ }
275
+
276
+ if (checking.length === 1) {
277
+ const named = recapSoftTitle(checking[0] && checking[0].title);
278
+ if (named) return `${greet}${named} is still being checked.`;
279
+ return `${greet}1 finished thing is still being checked.`;
280
+ }
281
+ if (checking.length > 1) {
282
+ return `${greet}${checking.length} finished things are still being checked.`;
283
+ }
284
+
285
+ if (shipped.length) {
286
+ const named = recapSoftTitle(shipped[0].title);
287
+ return named
288
+ ? `${greet}you already shipped ${named}.`
289
+ : `${greet}you already shipped the last finished thing.`;
290
+ }
291
+
292
+ if (inProgress.length) {
293
+ const item = inProgress[0];
294
+ const named = recapSoftTitle(item && item.title);
295
+ if (item && item.owner && named) return `${greet}${named} is already yours.`;
296
+ if (named) return `${greet}${named} is ready to claim.`;
297
+ }
298
+
299
+ return `${greet}quiet window. nothing moved in this period.`;
300
+ }
301
+
204
302
  function printRecapHelp() {
205
303
  console.log(`
206
- atris recap - what got done, in plain English
304
+ atris recap - what got done, in spoken lines
207
305
 
208
- atris recap Last 7 days: done, needs you, still working
209
- atris recap --days 30 Widen the window
210
- atris recap --share Paste-ready summary for Slack, email, or a customer
211
- atris recap --json Structured output for agents and dashboards
306
+ atris recap a few spoken lines: waiting for your ok, still being checked
307
+ atris recap --verbose the old report
308
+ atris recap --days 30 widen the window
309
+ atris recap --share paste-ready summary for Slack, email, or a customer
310
+ atris recap --json structured output for agents and dashboards
212
311
 
213
- Looks at Atris' saved work and explains it without internal jargon:
214
- what changed, how it was checked, and what still needs you.
312
+ certified work names atris task accept. uncertified stays still being checked.
313
+ use --verbose or --share for the old report.
215
314
  `);
216
315
  }
217
316
 
@@ -227,7 +326,15 @@ function recapAtris(args = []) {
227
326
  console.log(JSON.stringify(data, null, 2));
228
327
  return;
229
328
  }
230
- console.log(args.includes('--share') ? renderShare(data) : renderRecap(data));
329
+ if (args.includes('--share')) {
330
+ console.log(renderShare(data));
331
+ return;
332
+ }
333
+ if (args.includes('--verbose') || args.includes('--full')) {
334
+ console.log(renderRecap(data));
335
+ return;
336
+ }
337
+ console.log(renderRecapMinute(data));
231
338
  }
232
339
 
233
- module.exports = { recapAtris, buildRecapData, renderRecap, renderShare };
340
+ module.exports = { recapAtris, buildRecapData, renderRecap, renderRecapMinute, renderShare };
@@ -1,6 +1,7 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- const { execFileSync } = require('child_process');
3
+ const { execFileSync, spawnSync } = require('child_process');
4
+ const { runGit } = require('../lib/checkout-sync');
4
5
 
5
6
  /**
6
7
  * atris release [--dry-run] - Tag a release, create GitHub release, draft /launch post
@@ -181,4 +182,103 @@ function printLaunchPost(version, commitLines) {
181
182
  console.log('--- end launch post ---');
182
183
  }
183
184
 
184
- module.exports = { releaseAtris };
185
+ function readPackageVersion(cwd) {
186
+ try {
187
+ const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
188
+ return typeof pkg.version === 'string' && pkg.version.trim() ? pkg.version.trim() : null;
189
+ } catch {
190
+ return null;
191
+ }
192
+ }
193
+
194
+ function runFullTestSuite(cwd) {
195
+ return spawnSync('npm', ['test'], {
196
+ cwd,
197
+ env: { ...process.env, CI: 'true' },
198
+ stdio: 'inherit',
199
+ });
200
+ }
201
+
202
+ function gitStdout(result) {
203
+ return result && result.status === 0 ? String(result.stdout || '').trim() : '';
204
+ }
205
+
206
+ /**
207
+ * atris release preflight
208
+ *
209
+ * Enforced checks before pushing a v* tag from master. Prints pass/fail for
210
+ * each check and exits nonzero if any fail. Skips the test suite when earlier
211
+ * git checks already failed so a bad checkout does not burn a full run.
212
+ */
213
+ function releasePreflight({ cwd = process.cwd(), runTests = runFullTestSuite } = {}) {
214
+ let failed = false;
215
+
216
+ const branchResult = runGit(['rev-parse', '--abbrev-ref', 'HEAD'], cwd);
217
+ const branch = gitStdout(branchResult);
218
+ if (branch === 'master') {
219
+ console.log('pass: current branch is master');
220
+ } else {
221
+ failed = true;
222
+ console.log(`fail: current branch is ${branch || 'unknown'}, need master`);
223
+ }
224
+
225
+ const statusResult = runGit(['status', '--porcelain'], cwd);
226
+ if (statusResult.status === 0 && String(statusResult.stdout || '').trim() === '') {
227
+ console.log('pass: working tree is clean');
228
+ } else {
229
+ failed = true;
230
+ console.log('fail: working tree is not clean');
231
+ }
232
+
233
+ const localMaster = gitStdout(runGit(['rev-parse', 'master'], cwd));
234
+ const originMaster = gitStdout(runGit(['rev-parse', 'origin/master'], cwd));
235
+ if (!localMaster) {
236
+ failed = true;
237
+ console.log('fail: local master is missing');
238
+ } else if (!originMaster) {
239
+ failed = true;
240
+ console.log('fail: origin/master is missing');
241
+ } else if (localMaster !== originMaster) {
242
+ failed = true;
243
+ console.log('fail: local master does not match origin/master');
244
+ } else {
245
+ console.log('pass: local master matches origin/master');
246
+ }
247
+
248
+ const version = readPackageVersion(cwd);
249
+ if (!version) {
250
+ failed = true;
251
+ console.log('fail: package.json version is missing');
252
+ } else {
253
+ const tagName = `v${version}`;
254
+ const tagList = gitStdout(runGit(['tag', '--list', tagName], cwd));
255
+ if (tagList) {
256
+ failed = true;
257
+ console.log(`fail: tag ${tagName} already exists`);
258
+ } else {
259
+ console.log(`pass: no tag ${tagName}`);
260
+ }
261
+ }
262
+
263
+ if (failed) {
264
+ console.log('skip: test suite (earlier checks failed)');
265
+ } else {
266
+ const testResult = runTests(cwd);
267
+ if (testResult && testResult.error) {
268
+ failed = true;
269
+ console.log(`fail: test suite could not run (${testResult.error.message})`);
270
+ } else if (testResult && testResult.status === 0) {
271
+ console.log('pass: test suite');
272
+ } else {
273
+ const code = testResult && typeof testResult.status === 'number' ? testResult.status : 1;
274
+ failed = true;
275
+ console.log(`fail: test suite exited ${code}`);
276
+ }
277
+ }
278
+
279
+ console.log('');
280
+ console.log(failed ? 'release preflight failed' : 'release preflight passed');
281
+ return failed ? 1 : 0;
282
+ }
283
+
284
+ module.exports = { releaseAtris, releasePreflight, runFullTestSuite };
@@ -30,24 +30,19 @@ function findReviewEngine() {
30
30
  }
31
31
 
32
32
  function printMissingReviewEngine(jsonMode) {
33
+ const install = 'atris skill install code-review';
33
34
  if (jsonMode) {
34
35
  console.log(JSON.stringify({
35
36
  ok: false,
36
- error: 'Review engine not found.',
37
+ error: 'not installed',
37
38
  expected: 'atris/business/claude-code-review/workspace/review_engine.py',
38
39
  specialists: ['Security', 'Testing', 'Performance', 'Maintainability', 'Database', 'Async'],
39
- install: 'copy review_engine.py to your project',
40
+ install,
40
41
  }, null, 2));
41
42
  } else {
42
- console.error('Review engine not found.');
43
- console.error('Expected at: atris/business/claude-code-review/workspace/review_engine.py');
44
- console.error('');
45
- console.error('The review engine runs 6 specialists:');
46
- console.error(' Security, Testing, Performance, Maintainability, Database, Async');
47
- console.error('');
48
- console.error('Install: copy review_engine.py to your project');
43
+ console.error(`not installed; ${install}`);
49
44
  }
50
- process.exit(1);
45
+ process.exit(2);
51
46
  }
52
47
 
53
48
  function exitReviewError(message, jsonMode, extra = {}) {
@@ -0,0 +1,55 @@
1
+ 'use strict';
2
+
3
+ const {
4
+ appendDailySummary,
5
+ computeRevisionMetric,
6
+ } = require('../lib/revision-metric');
7
+
8
+ function plural(count, word) {
9
+ return `${count} ${word}${count === 1 ? '' : 's'}`;
10
+ }
11
+
12
+ function plainSubject(subject) {
13
+ return String(subject || 'untitled landing')
14
+ .replace(/—/g, ',')
15
+ .replace(/\s+/g, ' ')
16
+ .trim()
17
+ .toLowerCase();
18
+ }
19
+
20
+ function formatRevisionMetric(metric, saved) {
21
+ const summary = metric.rolling_7_days;
22
+ const lines = [
23
+ `revision signals in the last seven days: ${summary.revision_count} across ${plural(summary.landed_changes, 'atris-assisted landing')}.`,
24
+ `clean so far: ${summary.clean_so_far}. still observing: ${summary.still_observing}. the target is zero revisions.`,
25
+ 'this is a same-file git signal, not proof of automatic landing or why a human changed the file.',
26
+ ];
27
+ for (const change of (metric.changes || []).filter((item) => item.revision_count > 0)) {
28
+ lines.push(`"${plainSubject(change.subject)}": ${plural(change.revision_count, 'possible revision')}.`);
29
+ }
30
+ lines.push(saved.appended
31
+ ? `daily summary saved for ${saved.row.date}.`
32
+ : `daily summary for ${saved.row.date} was already saved.`);
33
+ return lines.join('\n');
34
+ }
35
+
36
+ function revisionsCommand(args = [], deps = {}) {
37
+ const root = deps.root || process.cwd();
38
+ const json = args.includes('--json');
39
+ let metric;
40
+ try {
41
+ metric = (deps.computeRevisionMetric || computeRevisionMetric)(root, {
42
+ now: deps.now,
43
+ ref: deps.ref,
44
+ });
45
+ } catch {
46
+ console.error('this folder has no readable git history, so revisions cannot be measured.');
47
+ return 1;
48
+ }
49
+ const saved = (deps.appendDailySummary || appendDailySummary)(root, metric, deps.stateOptions);
50
+ if (json) console.log(JSON.stringify({ ...metric, daily_summary: saved }));
51
+ else console.log(formatRevisionMetric(metric, saved));
52
+ return 0;
53
+ }
54
+
55
+ module.exports = { formatRevisionMetric, plainSubject, revisionsCommand };
@@ -65,19 +65,27 @@ function runnerDrivableForFrontDoor(runner, options = {}) {
65
65
  return false;
66
66
  }
67
67
 
68
- // Most logical mission: a moving one beats a planned one, newer beats older.
69
- // ready is excluded it waits for review, not for another worker pass.
68
+ // Most logical mission: a moving one beats a planned one, ready waits for a
69
+ // resume when nothing else is moving, and newer beats older.
70
70
  function pickRunnableMission(root = process.cwd(), missionMap = null, options = {}) {
71
71
  let map = missionMap;
72
72
  if (!map) {
73
73
  try { map = require('./mission').loadMissionMap(root); } catch { return null; }
74
74
  }
75
+ const rank = (status) => {
76
+ if (status === 'running') return 0;
77
+ if (status === 'planning') return 1;
78
+ if (status === 'ready') return 2;
79
+ return 9;
80
+ };
75
81
  const candidates = Array.from(map.values())
76
- .filter((mission) => mission && (mission.status === 'running' || mission.status === 'planning'))
82
+ .filter((mission) => mission && (mission.status === 'running' || mission.status === 'planning' || mission.status === 'ready'))
77
83
  .filter((mission) => runnerDrivableForFrontDoor(mission?.runner, options))
78
84
  .filter((mission) => !/^decide and start the next useful mission after:/i.test(String(mission?.objective || '')))
79
85
  .sort((a, b) => {
80
- if (a.status !== b.status) return a.status === 'running' ? -1 : 1;
86
+ const ra = rank(a.status);
87
+ const rb = rank(b.status);
88
+ if (ra !== rb) return ra - rb;
81
89
  return String(b.updated_at || b.created_at || '').localeCompare(String(a.updated_at || a.created_at || ''));
82
90
  });
83
91
  return candidates[0] || null;
package/commands/scout.js CHANGED
@@ -90,6 +90,17 @@ function runScout(question, { dir, model, engine, timeoutMs }) {
90
90
 
91
91
  async function scoutCommand(argv) {
92
92
  const args = [...argv];
93
+ const { refuseHeadlessUnless } = require('../lib/noninteractive');
94
+ if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
95
+ showHelp();
96
+ return 0;
97
+ }
98
+ if (refuseHeadlessUnless(args, {
99
+ allowYes: true,
100
+ usage: 'Usage: atris scout [--yes] "<question>"\nHeadless scout needs --yes.',
101
+ })) {
102
+ return 2;
103
+ }
93
104
  const questions = [];
94
105
  let dir = process.cwd();
95
106
  let model = MODELS.haiku;
@@ -105,7 +116,7 @@ async function scoutCommand(argv) {
105
116
  if (!ENGINES[engine]) { console.error(`✗ unknown engine "${engine}" (claude|cursor|devin|fast)`); return 1; }
106
117
  }
107
118
  else if (a === '--timeout') timeoutMs = (parseInt(args.shift(), 10) || 180) * 1000;
108
- else if (a === '--help' || a === '-h') { showHelp(); return 0; }
119
+ else if (a === '--help' || a === '-h' || a === '--yes' || a === '-y') { /* handled above / proceed flag */ }
109
120
  else questions.push(a);
110
121
  }
111
122