atris 3.38.0 → 3.41.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 (78) hide show
  1. package/AGENTS.md +25 -6
  2. package/atris/PERSONA.md +8 -4
  3. package/atris.md +7 -0
  4. package/ax +2 -1
  5. package/bin/atris.js +31 -6
  6. package/commands/agent-spawn.js +13 -11
  7. package/commands/autoland.js +28 -77
  8. package/commands/bench.js +10 -12
  9. package/commands/business.js +345 -0
  10. package/commands/chat-scan.js +5 -7
  11. package/commands/codex-goal.js +8 -10
  12. package/commands/computer.js +20 -0
  13. package/commands/console.js +19 -3
  14. package/commands/decide.js +166 -0
  15. package/commands/deck.js +1 -4
  16. package/commands/drill.js +14 -24
  17. package/commands/engine.js +196 -8
  18. package/commands/gm.js +8 -6
  19. package/commands/harvest.js +1 -4
  20. package/commands/init.js +23 -3
  21. package/commands/land.js +8 -14
  22. package/commands/launchpad.js +1 -14
  23. package/commands/lifecycle.js +5 -5
  24. package/commands/log.js +55 -5
  25. package/commands/member.js +558 -574
  26. package/commands/mission.js +456 -237
  27. package/commands/pack.js +2746 -164
  28. package/commands/play.js +6 -4
  29. package/commands/probe.js +2 -2
  30. package/commands/pulse.js +15 -16
  31. package/commands/release.js +10 -9
  32. package/commands/router.js +5 -4
  33. package/commands/site-deploy.js +885 -0
  34. package/commands/site.js +11 -2
  35. package/commands/slop.js +14 -2
  36. package/commands/stream.js +4 -18
  37. package/commands/task.js +880 -558
  38. package/commands/taste.js +101 -0
  39. package/commands/team.js +176 -3
  40. package/commands/vercel.js +4 -2
  41. package/commands/voice.js +195 -0
  42. package/commands/watch.js +1 -22
  43. package/commands/wiki.js +1 -4
  44. package/commands/workflow.js +2 -2
  45. package/commands/worktree.js +1 -14
  46. package/commands/xp.js +27 -24
  47. package/lib/accept-verify-gate.js +5 -1
  48. package/lib/arg-parser.js +41 -0
  49. package/lib/auto-accept-certified.js +116 -1
  50. package/lib/autoland.js +66 -0
  51. package/lib/bench/runner.js +19 -1
  52. package/lib/context-gatherer.js +7 -1
  53. package/lib/engine-registry.js +141 -20
  54. package/lib/falsifier-probe.js +84 -0
  55. package/lib/fleet.js +65 -15
  56. package/lib/git-spawn.js +15 -0
  57. package/lib/json-file.js +37 -0
  58. package/lib/known-commands.js +2 -2
  59. package/lib/lesson-preflight.js +146 -0
  60. package/lib/loop-doctor.js +0 -2
  61. package/lib/mission-human-asks.js +28 -0
  62. package/lib/mission-protected-lane.js +4 -1
  63. package/lib/official-cli-integration.js +47 -2
  64. package/lib/orb-context.js +8 -1
  65. package/lib/pack-capabilities.js +685 -0
  66. package/lib/router-brain.js +51 -1
  67. package/lib/runner-command.js +0 -6
  68. package/lib/self-drive.js +44 -13
  69. package/lib/task-db.js +137 -3
  70. package/lib/task-decision.js +50 -0
  71. package/lib/taste-lessons.js +153 -0
  72. package/lib/tool-result-encode.js +17 -1
  73. package/lib/voice-gate.js +66 -0
  74. package/lib/wish-audit.js +1 -1
  75. package/lib/wish-delegate.js +1 -1
  76. package/lib/zip.js +95 -7
  77. package/package.json +2 -1
  78. package/templates/business-starter/persona.md +9 -0
@@ -0,0 +1,146 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+
6
+ const ACTIVE_STATUSES = new Set(['observed', 'open', 'attempted']);
7
+ const KEYWORD_STOP_WORDS = new Set([
8
+ 'and', 'are', 'for', 'from', 'into', 'not', 'of', 'on', 'or', 'the', 'this', 'to', 'with', 'without',
9
+ ]);
10
+
11
+ function readJson(filePath) {
12
+ try {
13
+ const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
14
+ return parsed && typeof parsed === 'object' ? parsed : {};
15
+ } catch {
16
+ return {};
17
+ }
18
+ }
19
+
20
+ function readLessonText(filePath) {
21
+ let content = '';
22
+ try {
23
+ content = fs.readFileSync(filePath, 'utf8');
24
+ } catch {
25
+ return new Map();
26
+ }
27
+
28
+ const bySlug = new Map();
29
+ for (const line of content.split(/\r?\n/)) {
30
+ const match = line.match(/^\s*-\s*\*\*\[([^\]]+)\]\s*([a-z0-9][a-z0-9-]*)\*\*\s*[—:-]+\s*(?:pass|fail)?\s*[—:-]*\s*(.*)$/i);
31
+ if (!match || bySlug.has(match[2])) continue;
32
+ const text = match[3].replace(/^\[resolved\]\s*/i, '').trim();
33
+ if (text) bySlug.set(match[2], text);
34
+ }
35
+ return bySlug;
36
+ }
37
+
38
+ function isPromptRule(mechanism) {
39
+ if (mechanism && typeof mechanism === 'object') {
40
+ const kind = String(mechanism.type || mechanism.kind || '').toLowerCase();
41
+ return kind === 'prompt-rule' || kind === 'prompt_rule' || mechanism.prompt_rule === true;
42
+ }
43
+ return /^\s*prompt[-_\s]?rule\b/i.test(String(mechanism || ''));
44
+ }
45
+
46
+ function lessonIsEligible(lesson) {
47
+ const status = String(lesson && lesson.status || '').toLowerCase();
48
+ if (ACTIVE_STATUSES.has(status)) return true;
49
+ return status === 'resolved' && isPromptRule(lesson.mechanism);
50
+ }
51
+
52
+ function normalizeFile(file) {
53
+ return String(file || '')
54
+ .trim()
55
+ .replace(/\\/g, '/')
56
+ .replace(/^\.\//, '')
57
+ .replace(/\/{2,}/g, '/')
58
+ .replace(/\/$/, '');
59
+ }
60
+
61
+ function pathSpecificity(file) {
62
+ const normalized = normalizeFile(file);
63
+ const segments = normalized.split('/').filter(Boolean).length;
64
+ return (segments * 100) + normalized.length;
65
+ }
66
+
67
+ function overlapStrength(appliesTo, file) {
68
+ const lessonPath = normalizeFile(appliesTo).toLowerCase();
69
+ const taskPath = normalizeFile(file).toLowerCase();
70
+ if (!lessonPath || !taskPath) return 0;
71
+ if (lessonPath === taskPath) return 3;
72
+ if (taskPath.endsWith(`/${lessonPath}`) || lessonPath.endsWith(`/${taskPath}`)) return 3;
73
+ if (taskPath.startsWith(`${lessonPath}/`) || lessonPath.startsWith(`${taskPath}/`)) return 2;
74
+ return 0;
75
+ }
76
+
77
+ function slugKeywords(slug) {
78
+ return [...new Set(String(slug || '').toLowerCase().split(/[^a-z0-9]+/)
79
+ .filter((word) => word.length >= 3 && !KEYWORD_STOP_WORDS.has(word)))];
80
+ }
81
+
82
+ function keywordAppears(text, keyword) {
83
+ const escaped = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
84
+ return new RegExp(`(^|[^a-z0-9])${escaped}([^a-z0-9]|$)`, 'i').test(text);
85
+ }
86
+
87
+ function bestMatch({ slug, lesson, briefText, files }) {
88
+ const brief = String(briefText || '');
89
+ const briefLower = brief.toLowerCase().replace(/\\/g, '/');
90
+ const appliesTo = Array.isArray(lesson.applies_to) ? lesson.applies_to : [];
91
+ let best = null;
92
+
93
+ for (const lessonFile of appliesTo) {
94
+ const normalizedLessonFile = normalizeFile(lessonFile);
95
+ if (!normalizedLessonFile) continue;
96
+ for (const taskFile of files) {
97
+ const strength = overlapStrength(normalizedLessonFile, taskFile);
98
+ if (!strength) continue;
99
+ const score = 300000 + (strength * 10000) + pathSpecificity(normalizedLessonFile) - appliesTo.length;
100
+ if (!best || score > best.score) {
101
+ best = { score, why: `file overlaps ${normalizedLessonFile}` };
102
+ }
103
+ }
104
+ if (briefLower.includes(normalizedLessonFile.toLowerCase())) {
105
+ const score = 200000 + pathSpecificity(normalizedLessonFile) - appliesTo.length;
106
+ if (!best || score > best.score) {
107
+ best = { score, why: `brief mentions ${normalizedLessonFile}` };
108
+ }
109
+ }
110
+ }
111
+
112
+ const matchedKeywords = slugKeywords(slug).filter((keyword) => keywordAppears(brief, keyword));
113
+ if (matchedKeywords.length) {
114
+ const keywordLength = matchedKeywords.reduce((total, keyword) => total + keyword.length, 0);
115
+ const score = 100000 + (matchedKeywords.length * 1000) + keywordLength;
116
+ if (!best || score > best.score) {
117
+ best = { score, why: `brief mentions ${matchedKeywords.join(', ')}` };
118
+ }
119
+ }
120
+
121
+ return best;
122
+ }
123
+
124
+ function matchLessons({ briefText = '', files = [], root = process.cwd() } = {}) {
125
+ const metadata = readJson(path.join(root, 'atris', 'lessons.json'));
126
+ const lessonText = readLessonText(path.join(root, 'atris', 'lessons.md'));
127
+ const normalizedFiles = (Array.isArray(files) ? files : [files]).map(normalizeFile).filter(Boolean);
128
+ const matches = [];
129
+
130
+ for (const [slug, lesson] of Object.entries(metadata)) {
131
+ if (slug === '_schema' || !lesson || typeof lesson !== 'object') continue;
132
+ if (!lessonIsEligible(lesson)) continue;
133
+ const text = lessonText.get(slug);
134
+ if (!text) continue;
135
+ const matched = bestMatch({ slug, lesson, briefText, files: normalizedFiles });
136
+ if (!matched) continue;
137
+ matches.push({ slug, text, why_matched: matched.why, score: matched.score });
138
+ }
139
+
140
+ return matches
141
+ .sort((left, right) => right.score - left.score || left.slug.localeCompare(right.slug))
142
+ .slice(0, 5)
143
+ .map(({ slug, text, why_matched }) => ({ slug, text, why_matched }));
144
+ }
145
+
146
+ module.exports = { matchLessons };
@@ -20,8 +20,6 @@ const HEADLESS_CAPABLE_RUNNERS = new Set([
20
20
  'codex',
21
21
  'cursor',
22
22
  'devin',
23
- 'hermes',
24
- 'hermes-agent',
25
23
  ]);
26
24
  const FINDING_PRIORITY = {
27
25
  repeated_auth_failure: 500,
@@ -0,0 +1,28 @@
1
+ 'use strict';
2
+
3
+ function normalizeHumanAsk(ask) {
4
+ if (typeof ask === 'string') {
5
+ return { text: ask, answered_at: null };
6
+ }
7
+ if (!ask || typeof ask !== 'object' || Array.isArray(ask)) {
8
+ return { text: String(ask || ''), answered_at: null };
9
+ }
10
+ return {
11
+ ...ask,
12
+ text: String(ask.text || ''),
13
+ answered_at: ask.answered_at || null,
14
+ };
15
+ }
16
+
17
+ function normalizeHumanAsks(asks) {
18
+ return Array.isArray(asks) ? asks.map(normalizeHumanAsk) : [];
19
+ }
20
+
21
+ function openHumanAsks(asks) {
22
+ return normalizeHumanAsks(asks).filter((ask) => ask.text.trim() && !ask.answered_at);
23
+ }
24
+
25
+ module.exports = {
26
+ normalizeHumanAsks,
27
+ openHumanAsks,
28
+ };
@@ -127,6 +127,7 @@ function defaultGit(gitBin = 'git') {
127
127
  cwd: root,
128
128
  encoding: 'utf8',
129
129
  timeout: 10000,
130
+ maxBuffer: 64 * 1024 * 1024,
130
131
  });
131
132
  }
132
133
 
@@ -167,7 +168,8 @@ function protectedPause(reason, detail = {}) {
167
168
  }
168
169
 
169
170
  function unreadableMissionGuard(error) {
170
- return protectedPause('mission diff could not be read; human review is required', {
171
+ return protectedPause('mission diff could not be read; repair the tooling failure and rerun', {
172
+ status: 'paused-for-retry',
171
173
  unreadable: true,
172
174
  detail: error?.message || String(error || 'unknown diff read failure'),
173
175
  surfaces: ['unreadable diff'],
@@ -285,6 +287,7 @@ function prepareMissionGitGuard({
285
287
  }
286
288
 
287
289
  module.exports = {
290
+ defaultGit,
288
291
  matchProtectedMissionDiff,
289
292
  unreadableMissionGuard,
290
293
  inspectMissionProtectedDiff,
@@ -67,6 +67,10 @@ function printHelp(config) {
67
67
  for (const command of config.commands) {
68
68
  console.log(` ${command.usage.padEnd(13)} ${command.description}`);
69
69
  }
70
+ if (config.commands.some((command) => command.summarize)) {
71
+ console.log('');
72
+ console.log(' --raw print the full cli output instead of the grouped summary');
73
+ }
70
74
  console.log('');
71
75
  console.log(`binary: ${config.binary}`);
72
76
  console.log(`install: ${config.installHint}`);
@@ -115,14 +119,54 @@ function matchCommand(config, args) {
115
119
  });
116
120
  }
117
121
 
122
+ const SUMMARY_SAMPLE_LINES = 5;
123
+
124
+ function summaryLines(result) {
125
+ const stdout = String(result.stdout || '').split(/\r?\n/);
126
+ const stderr = String(result.stderr || '').split(/\r?\n/);
127
+ return { stdout, stderr, all: [...stdout, ...stderr].filter((line) => line.trim()) };
128
+ }
129
+
130
+ function printOutputSummary(config, cliArgs, result) {
131
+ const status = result.status ?? 0;
132
+ const { all } = summaryLines(result);
133
+ const errors = all.filter((line) => /\berror\b|\bfailed\b|\bfatal\b/i.test(line));
134
+ const warnings = all.filter((line) => /\bwarn/i.test(line));
135
+ const links = [...new Set(all.flatMap((line) => line.match(/https?:\/\/\S+/g) || []))];
136
+ const label = `${config.name} ${cliArgs.join(' ')}`.trim();
137
+
138
+ console.log(`${label}: ${status === 0 ? 'ok' : `failed (exit ${status})`}, ${all.length} output lines`);
139
+ console.log('');
140
+ if (links.length) {
141
+ console.log(`links (${links.length})`);
142
+ for (const link of links.slice(0, SUMMARY_SAMPLE_LINES)) console.log(` ${link}`);
143
+ console.log('');
144
+ }
145
+ console.log(errors.length ? `errors (${errors.length})` : 'errors: none');
146
+ for (const line of errors.slice(0, SUMMARY_SAMPLE_LINES)) console.log(` ${line.trim()}`);
147
+ if (errors.length) console.log('');
148
+ console.log(warnings.length ? `warnings (${warnings.length})` : 'warnings: none');
149
+ for (const line of warnings.slice(0, SUMMARY_SAMPLE_LINES)) console.log(` ${line.trim()}`);
150
+ if (warnings.length) console.log('');
151
+ const tail = all.slice(-SUMMARY_SAMPLE_LINES);
152
+ if (tail.length) {
153
+ console.log('last lines');
154
+ for (const line of tail) console.log(` ${line.trim()}`);
155
+ console.log('');
156
+ }
157
+ console.log(`full output: atris ${label} --raw`);
158
+ }
159
+
118
160
  function runPassthrough(config, command, args) {
119
161
  const binary = ensureBinary(config);
120
162
  if (!binary.ok) return 1;
121
163
 
122
- const rest = args.slice(command.match.length);
164
+ const raw = args.includes('--raw');
165
+ const rest = args.slice(command.match.length).filter((arg) => arg !== '--raw');
123
166
  const cliArgs = [...command.forward, ...rest];
167
+ const summarize = command.summarize && !raw;
124
168
  const result = spawnSync(config.binary, cliArgs, {
125
- stdio: 'inherit',
169
+ ...(summarize ? { encoding: 'utf8' } : { stdio: 'inherit' }),
126
170
  env: process.env,
127
171
  });
128
172
  if (missingBinary(result)) {
@@ -133,6 +177,7 @@ function runPassthrough(config, command, args) {
133
177
  console.error(`${config.name} command failed: ${result.error.message}`);
134
178
  return 1;
135
179
  }
180
+ if (summarize) printOutputSummary(config, cliArgs, result);
136
181
  return result.status ?? 0;
137
182
  }
138
183
 
@@ -13,7 +13,13 @@ const BACKLOG_STATUSES = new Set(['open', 'backlog', 'inbox', 'planned', 'plan',
13
13
  function cleanTitle(value, max = 64) {
14
14
  const text = String(value || '').replace(/\s+/g, ' ').trim();
15
15
  if (!text) return '';
16
- return text.length > max ? `${text.slice(0, max - 1)}…` : text;
16
+ if (text.length <= max) return text;
17
+ const slice = text.slice(0, max - 1);
18
+ const lastSpace = slice.lastIndexOf(' ');
19
+ // Cut on a word boundary so a title never ends on half a word ("...the owne…").
20
+ // Fall back to the hard slice only when a single word already fills the limit.
21
+ const body = lastSpace > 0 ? slice.slice(0, lastSpace) : slice;
22
+ return `${body.replace(/[\s,;:.!?-]+$/, '')}…`;
17
23
  }
18
24
 
19
25
  function asRecord(value) {
@@ -475,6 +481,7 @@ async function appendOrbChoice(root, label, when = new Date()) {
475
481
  }
476
482
 
477
483
  module.exports = {
484
+ cleanTitle,
478
485
  collectOrbContext,
479
486
  appendOrbChoice,
480
487
  };