atris 3.33.3 → 3.35.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 (54) hide show
  1. package/AGENTS.md +0 -2
  2. package/FOR_AGENTS.md +5 -3
  3. package/atris/skills/engines/SKILL.md +16 -7
  4. package/atris/skills/render-cli/SKILL.md +88 -0
  5. package/atris.md +19 -0
  6. package/ax +475 -17
  7. package/bin/atris.js +206 -44
  8. package/commands/aeo.js +52 -0
  9. package/commands/autoland.js +313 -19
  10. package/commands/business.js +91 -8
  11. package/commands/codex-goal.js +26 -2
  12. package/commands/drive.js +187 -0
  13. package/commands/engine.js +73 -2
  14. package/commands/feed.js +202 -0
  15. package/commands/github.js +38 -0
  16. package/commands/gm.js +262 -3
  17. package/commands/init.js +13 -1
  18. package/commands/integrations.js +39 -11
  19. package/commands/interview.js +143 -0
  20. package/commands/land.js +114 -13
  21. package/commands/lesson.js +112 -1
  22. package/commands/linear.js +38 -0
  23. package/commands/loops.js +212 -0
  24. package/commands/member.js +398 -42
  25. package/commands/mission.js +598 -64
  26. package/commands/now.js +25 -1
  27. package/commands/radar.js +259 -14
  28. package/commands/serve.js +54 -0
  29. package/commands/status.js +50 -5
  30. package/commands/stripe.js +38 -0
  31. package/commands/supabase.js +39 -0
  32. package/commands/task.js +935 -71
  33. package/commands/truth.js +29 -3
  34. package/commands/unknowns.js +627 -0
  35. package/commands/update.js +44 -0
  36. package/commands/vercel.js +38 -0
  37. package/commands/worktree.js +68 -10
  38. package/commands/write.js +399 -0
  39. package/lib/auto-accept-certified.js +70 -19
  40. package/lib/autoland.js +39 -3
  41. package/lib/fleet.js +256 -13
  42. package/lib/memory-view.js +14 -5
  43. package/lib/mission-runtime-loop.js +7 -0
  44. package/lib/official-cli-integration.js +174 -0
  45. package/lib/outbound-send-gate.js +165 -0
  46. package/lib/permission-grants.js +293 -0
  47. package/lib/review-integrity.js +147 -0
  48. package/lib/runner-command.js +23 -0
  49. package/lib/task-db.js +220 -7
  50. package/lib/task-proof.js +20 -0
  51. package/lib/task-receipt.js +93 -0
  52. package/package.json +1 -1
  53. package/utils/update-check.js +27 -6
  54. package/atris/learnings.jsonl +0 -1
@@ -9,13 +9,18 @@
9
9
  const fs = require('fs');
10
10
  const path = require('path');
11
11
 
12
- // "- **[2026-06-18] some-id** — pass — text..." -> { date, id, status, text }
12
+ // "- **[2026-06-18] some-id** — pass — text..." -> { date, id, status, text, resolved }
13
+ // A leading "[resolved]" marker (written when a lesson's detector passes) is
14
+ // stripped from the text and surfaced as `resolved` so the view can retire it.
13
15
  function parseLessons(text) {
14
16
  const out = [];
15
17
  for (const line of String(text || '').split('\n')) {
16
18
  const m = line.match(/^-\s*\*\*\[([^\]]+)\]\s*([^*]+?)\*\*\s*[—:-]*\s*(pass|fail)?\s*[—:-]*\s*(.*)$/i);
17
19
  if (!m) continue;
18
- out.push({ date: m[1].trim(), id: m[2].trim(), status: (m[3] || '').toLowerCase(), text: m[4].trim() });
20
+ let body = m[4].trim();
21
+ const resolved = /\[resolved\]/i.test(body);
22
+ body = body.replace(/\[resolved\]\s*/i, '').trim();
23
+ out.push({ date: m[1].trim(), id: m[2].trim(), status: (m[3] || '').toLowerCase(), text: body, resolved });
19
24
  }
20
25
  return out; // append-only file: oldest first
21
26
  }
@@ -55,15 +60,18 @@ function buildMemorySpec(root = process.cwd(), opts = {}) {
55
60
  const theme = opts.theme || 'atris';
56
61
  const brand = { name: opts.brand || 'Atris', accent: '.' };
57
62
  const { lessons, learnings, tasks } = gatherMemory(root);
63
+ const openLessons = lessons.filter((l) => !l.resolved);
64
+ const resolvedCount = lessons.length - openLessons.length;
58
65
  const recent = lessons.slice(-8).reverse(); // newest first
59
66
 
67
+ const retired = resolvedCount ? `, ${plural(resolvedCount, 'lesson')} auto-resolved` : '';
60
68
  const slides = [];
61
69
  slides.push({
62
70
  type: 'title',
63
71
  headline: 'What the workspace **learned**',
64
- sub: `${plural(lessons.length, 'lesson')} and ${plural(learnings.length, 'note')} compounded into memory${tasks ? `, ${plural(tasks.done, 'task')} done` : ''}.`,
72
+ sub: `${plural(openLessons.length, 'open lesson')}${retired} and ${plural(learnings.length, 'note')} compounded into memory${tasks ? `, ${plural(tasks.done, 'task')} done` : ''}.`,
65
73
  });
66
- slides.push({ type: 'bignumber', number: String(lessons.length), label: 'lessons compounded into the workspace' });
74
+ slides.push({ type: 'bignumber', number: String(openLessons.length), label: `open lessons${resolvedCount ? ` (${resolvedCount} retired)` : ''}` });
67
75
 
68
76
  if (recent.length) {
69
77
  slides.push({
@@ -76,7 +84,8 @@ function buildMemorySpec(root = process.cwd(), opts = {}) {
76
84
  header: { title: 'Recent updates', meta: `${recent.length} shown` },
77
85
  rows: recent.map((l, i) => ({
78
86
  title: titleize(l.id), sub: l.date,
79
- value: l.status || 'note', sev: l.status === 'fail' ? 0 : (l.status === 'pass' ? 2 : 1),
87
+ value: l.resolved ? 'resolved' : (l.status || 'note'),
88
+ sev: l.resolved ? 2 : (l.status === 'fail' ? 0 : (l.status === 'pass' ? 2 : 1)),
80
89
  active: i === 0,
81
90
  })),
82
91
  },
@@ -57,7 +57,10 @@ function missionRunIntentFromMessage(message) {
57
57
  rest = cadenceFlag.text;
58
58
  const noRun = hasBooleanFlag(rest, '--no-run') || hasBooleanFlag(rest, '--no-loop');
59
59
  const noComplete = hasBooleanFlag(rest, '--no-complete');
60
+ const forceRoomPreflight = hasBooleanFlag(rest, '--preflight') || hasBooleanFlag(rest, '--room-preflight');
61
+ const skipRoomPreflight = hasBooleanFlag(rest, '--no-preflight') || hasBooleanFlag(rest, '--no-room-preflight') || !forceRoomPreflight;
60
62
  rest = stripBooleanFlag(stripBooleanFlag(stripBooleanFlag(rest, '--no-run'), '--no-loop'), '--no-complete');
63
+ rest = stripBooleanFlag(stripBooleanFlag(stripBooleanFlag(stripBooleanFlag(rest, '--preflight'), '--room-preflight'), '--no-preflight'), '--no-room-preflight');
61
64
  rest = rest.replace(/(^|\s)--json(?=\s|$)/g, ' ').replace(/\s+/g, ' ').trim();
62
65
 
63
66
  const wantsLongRun = /\b(24\s*h|24\s*hours?|overnight|forever)\b/i.test(text);
@@ -69,6 +72,8 @@ function missionRunIntentFromMessage(message) {
69
72
  cadence: cadenceFlag.value || (wantsLongRun ? '15m' : ''),
70
73
  noRun,
71
74
  noComplete,
75
+ roomPreflight: forceRoomPreflight,
76
+ noRoomPreflight: skipRoomPreflight,
72
77
  };
73
78
  }
74
79
 
@@ -245,6 +250,8 @@ async function runRuntimeMissionLoop(intent, options = {}) {
245
250
 
246
251
  const startArgs = ['mission', 'run', intent.objective, '--runner', 'atris2', '--owner', intent.owner || 'mission-lead', '--json'];
247
252
  if (intent.cadence) startArgs.push('--cadence', intent.cadence);
253
+ if (intent.roomPreflight) startArgs.push('--room-preflight');
254
+ else if (intent.noRoomPreflight) startArgs.push('--no-room-preflight');
248
255
  const start = runCliJsonSync(cliPath, startArgs, { cwd, timeoutMs: 30000 });
249
256
  const result = { ok: start.ok, status: start.status, start, noRun: intent.noRun === true, achieved: false };
250
257
  if (!start.ok || !start.payload?.mission?.id) {
@@ -0,0 +1,174 @@
1
+ const { spawnSync } = require('node:child_process');
2
+
3
+ function hasHelp(args = []) {
4
+ return args.includes('--help') || args.includes('-h') || args[0] === 'help';
5
+ }
6
+
7
+ function firstLine(value) {
8
+ return String(value || '').split(/\r?\n/).map((line) => line.trim()).find(Boolean) || 'unknown';
9
+ }
10
+
11
+ function formatCommand(parts = []) {
12
+ return parts.join(' ').trim();
13
+ }
14
+
15
+ function runCaptured(binary, args = []) {
16
+ return spawnSync(binary, args, {
17
+ encoding: 'utf8',
18
+ env: process.env,
19
+ });
20
+ }
21
+
22
+ function missingBinary(result) {
23
+ return result && result.error && result.error.code === 'ENOENT';
24
+ }
25
+
26
+ function printMissingBinary(config) {
27
+ console.error(`${config.name} cli not found.`);
28
+ console.error(`install: ${config.installHint}`);
29
+ }
30
+
31
+ function ensureBinary(config) {
32
+ const result = runCaptured(config.binary, config.versionArgs || ['--version']);
33
+ if (missingBinary(result)) {
34
+ printMissingBinary(config);
35
+ return { ok: false, result };
36
+ }
37
+ if (result.error) {
38
+ console.error(`${config.name} cli check failed: ${result.error.message}`);
39
+ return { ok: false, result };
40
+ }
41
+ return { ok: true, result };
42
+ }
43
+
44
+ function runAuthCheck(config) {
45
+ if (!config.authArgs || config.authArgs.length === 0) {
46
+ return { ok: true, skipped: true };
47
+ }
48
+ const result = runCaptured(config.binary, config.authArgs);
49
+ if (missingBinary(result)) {
50
+ printMissingBinary(config);
51
+ return { ok: false, result };
52
+ }
53
+ if (result.error) {
54
+ console.error(`${config.name} auth check failed: ${result.error.message}`);
55
+ return { ok: false, result };
56
+ }
57
+ return { ok: result.status === 0, result };
58
+ }
59
+
60
+ function printHelp(config) {
61
+ console.log('');
62
+ console.log(`usage: atris ${config.name} <command> [args]`);
63
+ console.log('');
64
+ console.log('commands:');
65
+ console.log(' doctor detect the cli binary and auth state');
66
+ console.log(' auth check auth state');
67
+ for (const command of config.commands) {
68
+ console.log(` ${command.usage.padEnd(13)} ${command.description}`);
69
+ }
70
+ console.log('');
71
+ console.log(`binary: ${config.binary}`);
72
+ console.log(`install: ${config.installHint}`);
73
+ console.log('');
74
+ }
75
+
76
+ function printDoctor(config) {
77
+ const binary = ensureBinary(config);
78
+ if (!binary.ok) return 1;
79
+
80
+ console.log(`${config.name} integration`);
81
+ console.log(`binary: found ${config.binary}`);
82
+ console.log(`version: ${firstLine(binary.result.stdout || binary.result.stderr).toLowerCase()}`);
83
+
84
+ const auth = runAuthCheck(config);
85
+ if (auth.skipped) {
86
+ console.log('auth: not checked');
87
+ return 0;
88
+ }
89
+ if (auth.ok) {
90
+ console.log('auth: ok');
91
+ return 0;
92
+ }
93
+ console.error('auth: failed');
94
+ console.error(`login: ${config.loginHint}`);
95
+ return 1;
96
+ }
97
+
98
+ function printAuth(config) {
99
+ const binary = ensureBinary(config);
100
+ if (!binary.ok) return 1;
101
+ const auth = runAuthCheck(config);
102
+ if (auth.skipped || auth.ok) {
103
+ console.log('auth: ok');
104
+ return 0;
105
+ }
106
+ console.error('auth: failed');
107
+ console.error(`login: ${config.loginHint}`);
108
+ return 1;
109
+ }
110
+
111
+ function matchCommand(config, args) {
112
+ return config.commands.find((command) => {
113
+ if (args.length < command.match.length) return false;
114
+ return command.match.every((part, index) => args[index] === part);
115
+ });
116
+ }
117
+
118
+ function runPassthrough(config, command, args) {
119
+ const binary = ensureBinary(config);
120
+ if (!binary.ok) return 1;
121
+
122
+ const rest = args.slice(command.match.length);
123
+ const cliArgs = [...command.forward, ...rest];
124
+ const result = spawnSync(config.binary, cliArgs, {
125
+ stdio: 'inherit',
126
+ env: process.env,
127
+ });
128
+ if (missingBinary(result)) {
129
+ printMissingBinary(config);
130
+ return 1;
131
+ }
132
+ if (result.error) {
133
+ console.error(`${config.name} command failed: ${result.error.message}`);
134
+ return 1;
135
+ }
136
+ return result.status ?? 0;
137
+ }
138
+
139
+ function createOfficialCliCommand(config) {
140
+ return function officialCliCommand(args = []) {
141
+ const normalizedArgs = Array.isArray(args) ? args : Array.from(arguments);
142
+ if (normalizedArgs.length === 0 || hasHelp(normalizedArgs)) {
143
+ printHelp(config);
144
+ return 0;
145
+ }
146
+
147
+ const subcommand = normalizedArgs[0];
148
+ const doctorAliases = config.doctorAliases || ['doctor', 'status'];
149
+ if (doctorAliases.includes(subcommand)) {
150
+ return printDoctor(config);
151
+ }
152
+ if (subcommand === 'auth') {
153
+ return printAuth(config);
154
+ }
155
+
156
+ const command = matchCommand(config, normalizedArgs);
157
+ if (!command) {
158
+ console.error(`unknown ${config.name} command: ${formatCommand(normalizedArgs)}`);
159
+ console.error(`run: atris ${config.name} --help`);
160
+ return 1;
161
+ }
162
+
163
+ return runPassthrough(config, command, normalizedArgs);
164
+ };
165
+ }
166
+
167
+ module.exports = {
168
+ createOfficialCliCommand,
169
+ ensureBinary,
170
+ hasHelp,
171
+ matchCommand,
172
+ printHelp,
173
+ runAuthCheck,
174
+ };
@@ -0,0 +1,165 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+ const {
7
+ HTML_TAG_RE,
8
+ RENDERED_SOURCE_FENCE_RE,
9
+ scanOutboundArtifact,
10
+ } = require('../scripts/outbound-artifact-gate');
11
+
12
+ const DEFAULT_GATE = Object.freeze({
13
+ format: 'plain',
14
+ proofFile: null,
15
+ bodyFile: null,
16
+ visual: false,
17
+ allowSlop: false,
18
+ });
19
+
20
+ class OutboundArtifactGateError extends Error {
21
+ constructor(check, file, errors = []) {
22
+ super(`outbound artifact gate failed: ${check} in ${file}`);
23
+ this.name = 'OutboundArtifactGateError';
24
+ this.check = check;
25
+ this.file = file;
26
+ this.errors = errors;
27
+ }
28
+ }
29
+
30
+ function defaultGateOptions() {
31
+ return { ...DEFAULT_GATE };
32
+ }
33
+
34
+ function readOutboundBodyFile(bodyFile) {
35
+ const file = path.resolve(String(bodyFile || ''));
36
+ try {
37
+ return fs.readFileSync(file, 'utf8');
38
+ } catch {
39
+ throw new OutboundArtifactGateError('body-file-read-error', file);
40
+ }
41
+ }
42
+
43
+ function writeInlineBodyFile(body) {
44
+ const file = path.join(os.tmpdir(), `atris-outbound-gate-${process.pid}-${Date.now()}.txt`);
45
+ fs.writeFileSync(file, String(body || ''), { encoding: 'utf8', flag: 'wx' });
46
+ return file;
47
+ }
48
+
49
+ function normalizeProofFile(proofFile) {
50
+ return proofFile ? path.resolve(String(proofFile)) : null;
51
+ }
52
+
53
+ function extractOutboundGateOptions(args = []) {
54
+ const rest = [];
55
+ const gate = defaultGateOptions();
56
+
57
+ for (let i = 0; i < args.length; i += 1) {
58
+ const arg = String(args[i] || '');
59
+ const eq = arg.indexOf('=');
60
+ const key = eq >= 0 ? arg.slice(0, eq) : arg;
61
+ const inlineValue = eq >= 0 ? arg.slice(eq + 1) : null;
62
+
63
+ if (key === '--visual') {
64
+ gate.visual = true;
65
+ continue;
66
+ }
67
+ if (key === '--allow-slop') {
68
+ gate.allowSlop = true;
69
+ continue;
70
+ }
71
+ if (key === '--format') {
72
+ gate.format = String(inlineValue !== null ? inlineValue : args[i + 1] || 'plain').toLowerCase();
73
+ if (inlineValue === null) i += 1;
74
+ continue;
75
+ }
76
+ if (key === '--proof-file' || key === '--render-proof') {
77
+ gate.proofFile = normalizeProofFile(inlineValue !== null ? inlineValue : args[i + 1]);
78
+ if (inlineValue === null) i += 1;
79
+ continue;
80
+ }
81
+ if (key === '--body-file') {
82
+ gate.bodyFile = path.resolve(String(inlineValue !== null ? inlineValue : args[i + 1] || ''));
83
+ if (inlineValue === null) i += 1;
84
+ continue;
85
+ }
86
+
87
+ rest.push(arg);
88
+ }
89
+
90
+ return { args: rest, gate };
91
+ }
92
+
93
+ function parseOutboundSendArgs(args = []) {
94
+ const parsed = extractOutboundGateOptions(args);
95
+ return {
96
+ text: parsed.gate.bodyFile ? '' : parsed.args.join(' ').trim(),
97
+ gate: parsed.gate,
98
+ };
99
+ }
100
+
101
+ function shouldRunOutboundGate(channel, body, gate = {}) {
102
+ const format = String(gate.format || 'plain').toLowerCase();
103
+ if (format !== 'plain') return true;
104
+ if (gate.visual || gate.proofFile || gate.bodyFile) return true;
105
+ return HTML_TAG_RE.test(body) || RENDERED_SOURCE_FENCE_RE.test(body);
106
+ }
107
+
108
+ function assertOutboundSendAllowed(channel, body, gate = {}) {
109
+ const options = { ...defaultGateOptions(), ...(gate || {}) };
110
+ options.format = String(options.format || 'plain').toLowerCase();
111
+ options.channel = String(channel || 'other').toLowerCase();
112
+ options.proofFile = normalizeProofFile(options.proofFile);
113
+ if (options.bodyFile) options.bodyFile = path.resolve(String(options.bodyFile));
114
+
115
+ const inspectedBody = options.bodyFile ? readOutboundBodyFile(options.bodyFile) : String(body || '');
116
+ if (!shouldRunOutboundGate(options.channel, inspectedBody, options)) {
117
+ return { ok: true, inspected: false, body: inspectedBody, file: null };
118
+ }
119
+
120
+ const inspectedFile = options.bodyFile || writeInlineBodyFile(inspectedBody);
121
+ const errors = scanOutboundArtifact({
122
+ channel: options.channel,
123
+ format: options.format,
124
+ bodyFile: inspectedFile,
125
+ proofFile: options.proofFile,
126
+ visual: options.visual,
127
+ allowSlop: options.allowSlop,
128
+ }, inspectedBody);
129
+
130
+ if (errors.length) {
131
+ throw new OutboundArtifactGateError(errors[0].id, inspectedFile, errors);
132
+ }
133
+
134
+ return { ok: true, inspected: true, body: inspectedBody, file: inspectedFile };
135
+ }
136
+
137
+ function exitOutboundGateFailure(error) {
138
+ console.error(error.message);
139
+ process.exit(1);
140
+ }
141
+
142
+ function assertOutboundSendAllowedOrExit(channel, body, gate = {}, options = {}) {
143
+ try {
144
+ return assertOutboundSendAllowed(channel, body, gate);
145
+ } catch (error) {
146
+ exitOutboundGateFailure(error, options);
147
+ }
148
+ }
149
+
150
+ function prepareOutboundSendTextOrExit(channel, args = [], options = {}) {
151
+ const parsed = parseOutboundSendArgs(args);
152
+ return assertOutboundSendAllowedOrExit(channel, parsed.text, parsed.gate, options);
153
+ }
154
+
155
+ module.exports = {
156
+ OutboundArtifactGateError,
157
+ assertOutboundSendAllowed,
158
+ assertOutboundSendAllowedOrExit,
159
+ defaultGateOptions,
160
+ extractOutboundGateOptions,
161
+ parseOutboundSendArgs,
162
+ prepareOutboundSendTextOrExit,
163
+ readOutboundBodyFile,
164
+ shouldRunOutboundGate,
165
+ };