atris 3.12.1 → 3.13.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.
package/README.md CHANGED
@@ -27,7 +27,7 @@ Then read the workspace's `atris/atris.md` and follow it exactly. `atris.md` is
27
27
 
28
28
  | File | Purpose |
29
29
  |------|---------|
30
- | `atris/atris.md` | God file. Protocol and source of truth |
30
+ | `atris/atris.md` | Main instructions for agents working in this repo |
31
31
  | `atris/MAP.md` | Navigation index with file:line refs |
32
32
  | `atris/TODO.md` | Shared task queue |
33
33
  | `atris/logs/YYYY/YYYY-MM-DD.md` | Daily log, inbox, notes, completions |
@@ -67,7 +67,7 @@ atris
67
67
 
68
68
  `atris init` scaffolds the workspace, including `atris/wiki/`. `atris` loads context and hands the workflow off to `atris/atris.md`.
69
69
 
70
- If you're still shaping the idea, use `atris brainstorm`. If you want Atris to keep cycling, use `atris run` or `atris autopilot`. If you want the repo brain kept honest, use `atris loop`. `atris activate` now surfaces wiki state from `atris/wiki/STATUS.md` when it exists.
70
+ If you're still shaping the idea, use `atris brainstorm`. If you want Atris to keep cycling, use `atris run` or `atris autopilot`. If you want project memory checked for stale pages and missing context, use `atris loop`. `atris activate` surfaces wiki state from `atris/wiki/STATUS.md` when it exists.
71
71
 
72
72
  Core loop: `plan` -> `do` -> `review`
73
73
 
@@ -84,9 +84,9 @@ atris business onboard --website https://blondish.world --contact "Joel Zimmerma
84
84
  atris align --fix
85
85
  ```
86
86
 
87
- That creates the cloud business, writes `.atris/business.json`, initializes `.atris/state/` for events, episodes, and scorecards, and scaffolds the local `atris/` workspace under `~/arena/atris-business/<slug>/` with starter team lanes, a default recap artifact, and a first-loop starter queue in `atris/TODO.md`.
87
+ That creates the cloud business, writes `.atris/business.json`, initializes `.atris/state/` for events and run history, and scaffolds the local `atris/` workspace under `~/arena/atris-business/<slug>/` with starter roles, a default recap template, and an initial task queue in `atris/TODO.md`.
88
88
 
89
- If you do not have a neat source pack yet, `atris business onboard` is the low-friction intake step: give it a website, a named human, a few notes, or even just run it in a folder with loose files, and it seeds raw intake, a starter brief, a first loop, a safe next action, and an operator one-pager for you.
89
+ If you do not have a neat source pack yet, `atris business onboard` is the easiest intake step: give it a website, a named human, a few notes, or run it in a folder with loose files. Atris turns that into raw intake, a starter brief, a first workflow, a safe next action, and a short operator brief.
90
90
 
91
91
  You can also use bare input:
92
92
 
@@ -120,26 +120,27 @@ atris business record atris/reports/2026-04-12-operator-recap.md --outcome mixed
120
120
  | `atris ingest` | Stage raw evidence into `atris/context/` and compile into `atris/wiki/` |
121
121
  | `atris loop` | Refresh wiki health, stale/orphan signals, and next ingest candidates |
122
122
  | `atris wiki` | Full wiki namespace: ingest, query, lint, search, log, and loop |
123
- | `atris experiments` | Run Karpathy-style keep/revert packs |
123
+ | `atris receipt` | Save evidence from an agent run |
124
+ | `atris experiments` | Run small experiments and compare results |
124
125
 
125
126
  ## Built-In Systems
126
127
 
127
128
  - `atris learn` stores structured project memory in `atris/learnings.jsonl`
128
129
  - `atris wiki` keeps repo memory in `atris/wiki/` by default, with `--cloud` when you want the remote workspace path
129
130
  - `atris ingest` now stages local source packs under `atris/context/_ingest/`, writes a manifest receipt, and refreshes `atris/wiki/STATUS.md` plus `log.md`
130
- - `atris wiki --private` uses `.atris/presidio/` for local-only sensitive notes and operating memory
131
+ - `atris wiki --private` stores local-only sensitive notes under `.atris/presidio/`
131
132
  - `atris loop` refreshes `atris/wiki/STATUS.md` and `atris/wiki/log.md`, flags stale/orphan pages, and suggests the next ingest
132
133
  - `atris activate` loads the current wiki status so the next session starts with project memory, not just tasks
133
- - `atris experiments` runs Karpathy-style keep/revert loops in `atris/experiments/`
134
+ - `atris experiments` runs small test packs in `atris/experiments/`
134
135
  - `atris pull` and `atris push` sync cloud workspaces and journals
135
136
 
136
137
  ## Verifiable Feedback Loop
137
138
 
138
139
  Under the hood, Atris can keep score on real repo work.
139
140
 
140
- - Endgame tasks can carry a `Verify:` command, so work can end on a deterministic check instead of pure prose.
141
- - `atris autopilot` can run that check after review, record a reward in the journal, and append a local scorecard when a horizon closes.
142
- - Future horizon picks can weight against recent scorecards, so the loop learns from repo-local history without claiming model retraining.
141
+ - Tasks can carry a `Verify:` command, so work can end on a deterministic check instead of pure prose.
142
+ - `atris autopilot` can run that check after review and record the result in the journal.
143
+ - Future task picks can use recent results, so Atris learns from repo-local history without claiming model retraining.
143
144
 
144
145
  ## Benchmark Harness
145
146
 
@@ -167,7 +168,7 @@ What to inspect:
167
168
  - receipts land in `atris/experiments/endstate-baseline/artifacts/` and
168
169
  `atris/experiments/endstate-stack/artifacts/`
169
170
  - scores append to each pack's `results.tsv`
170
- - `atris experiments compare endstate` prints the latest side-by-side scorecard
171
+ - `atris experiments compare endstate` prints the latest side-by-side comparison
171
172
  - `atris experiments replay endstate` runs the full public dry-run rehearsal
172
173
  - the benchmark contract lives at `atris/features/endstate/contract.md`
173
174
  - the verification log lives at `atris/features/endstate/validate.md`
@@ -198,7 +199,7 @@ For Codex, copy any skill folder into `~/.codex/skills/`.
198
199
  ## v3.2.0
199
200
 
200
201
  - **Staleness gate** — tasks tagged `[unverified]` are skipped at the moment of use, not pruned eagerly. Three-state model: actionable / unverified / deleted.
201
- - **Lesson gate** — `isLessonResolved` checks whether a lesson already shipped before proposing new horizons from it. Prevents the loop from re-solving solved problems.
202
+ - **Lesson gate** — `isLessonResolved` checks whether a lesson already shipped before proposing new work from it. Prevents the loop from re-solving solved problems.
202
203
  - **`atris release`** — new command: tags the version, bumps package.json, creates a GitHub release, and drafts a `/launch` post in one shot.
203
204
  - **Shell injection fix** — `checkStaleness` switched from `execSync` string interpolation to `execFileSync` with args arrays. Markdown-derived content (task titles, inbox items) no longer reaches a shell.
204
205
  - **Codex hardening** — `atris activate` and `atris` entry point detect Codex environments and write `AGENTS.md` so Codex sessions start with workspace context.
package/bin/atris.js CHANGED
@@ -247,7 +247,7 @@ function showHelp() {
247
247
  console.log('Optional helpers:');
248
248
  console.log(' brainstorm - Explore ideas conversationally before planning');
249
249
  console.log(' autopilot - Guided loop that can clarify TODOs and run plan → do → review');
250
- console.log(' visualize - Legacy visualization helper (prefer "atris plan")');
250
+ console.log(' visualize - Generate a Slack/deck-ready visual from a prompt');
251
251
  console.log('');
252
252
  console.log('Experiments:');
253
253
  console.log(' experiments init [slug] - Prepare atris/experiments/ or scaffold a pack');
@@ -272,7 +272,7 @@ function showHelp() {
272
272
  console.log(' wake [business] - Resume workspace (agents restart)');
273
273
  console.log('');
274
274
  console.log('Business:');
275
- console.log(' business init <name> - Create canonical business workspace (cloud + local)');
275
+ console.log(' business init <name> - RECOMMENDED: create business environment (cloud + local)');
276
276
  console.log(' business onboard - Onboard from sparse input (--name, --website, --contact)');
277
277
  console.log(' business add <slug> - Connect a business');
278
278
  console.log(' business list - Show connected businesses');
@@ -280,7 +280,7 @@ function showHelp() {
280
280
  console.log(' business team [slug] - Show members, roles, and admin access');
281
281
  console.log(' business health <slug> - Health report (members, workspace, issues)');
282
282
  console.log(' business audit - One-line health summary of all businesses');
283
- console.log(' business create <name> - Create new business; add --workspace for canonical local scaffold');
283
+ console.log(' business create <name> - Cloud-only business record; add --workspace to also scaffold local');
284
284
  console.log(' business connect <svc> - Wire a skill/integration');
285
285
  console.log(' business notify <mode> - Set notification mode (digest/silent/push)');
286
286
  console.log(' business deploy <slug> - Push local business to cloud');
@@ -291,6 +291,7 @@ function showHelp() {
291
291
  console.log('');
292
292
  console.log('Cloud & agents:');
293
293
  console.log(' computer - Talk directly to the AI computer (bash or agent exec)');
294
+ console.log(' receipt - Save evidence from an agent run');
294
295
  console.log(' console - Start/attach always-on coding console (tmux daemon)');
295
296
  console.log(' agent - Select which Atris agent to use');
296
297
  console.log(' chat - Chat with the selected Atris agent');
@@ -432,10 +433,10 @@ const { planAtris: planCmd, doAtris: doCmd, reviewAtris: reviewCmd } = require('
432
433
  // Check if this is a known command or natural language input
433
434
  const knownCommands = ['init', 'log', 'status', 'analytics', 'visualize', 'brainstorm', 'autopilot', 'run', 'plan', 'do', 'review', 'release',
434
435
  'activate', '_activate', 'agent', 'chat', 'console', 'login', 'logout', 'whoami', 'switch', 'use', 'accounts', '_resolve', '_profile-email', '_switch-session', 'shell-init', 'update', 'upgrade', 'version', 'help', 'next', 'atris',
435
- 'clean', 'verify', 'search', 'skill', 'member', 'app', 'learn', 'plugin', 'experiments', 'pull', 'push', 'align', 'terminal', 'computer', 'diff', 'business', 'sync',
436
+ 'clean', 'verify', 'search', 'skill', 'member', 'app', 'learn', 'plugin', 'experiments', 'receipt', 'proof', 'openclaw', 'pull', 'push', 'align', 'terminal', 'computer', 'diff', 'business', 'sync',
436
437
  'ingest', 'query', 'lint', 'loop',
437
438
  'gmail', 'calendar', 'twitter', 'slack', 'integrations', 'setup', 'clean-workspace', 'cw',
438
- 'fork', 'browse', 'publish', 'sleep', 'wake', 'feedback', 'wiki', 'code-review', 'cr', 'soul', 'fleet'];
439
+ 'fork', 'browse', 'publish', 'sleep', 'wake', 'feedback', 'errors', 'wiki', 'code-review', 'cr', 'soul', 'fleet'];
439
440
 
440
441
  // Check if command is an atris.md spec file - triggers welcome visualization
441
442
  function isSpecFile(cmd) {
@@ -868,10 +869,9 @@ if (command === 'init') {
868
869
  } else if (command === 'shell-init') {
869
870
  require('../commands/auth').shellInit();
870
871
  } else if (command === 'visualize') {
871
- console.log('ℹ️ "atris visualize" is a legacy helper. Visualization is now built into "atris plan".');
872
- console.log(' Prefer: atris plan');
873
- console.log('');
874
- require('../commands/visualize').visualizeAtris();
872
+ require('../commands/visualize').visualizeAtris(process.argv.slice(3))
873
+ .then(() => process.exit(0))
874
+ .catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
875
875
  } else if (command === 'run') {
876
876
  const args = process.argv.slice(3);
877
877
  if (args.includes('--help') || args.includes('-h')) {
@@ -1160,6 +1160,10 @@ if (command === 'init') {
1160
1160
  const subcommand = process.argv[3];
1161
1161
  const args = process.argv.slice(4);
1162
1162
  require('../commands/experiments').experimentsCommand(subcommand, ...args);
1163
+ } else if (command === 'receipt' || command === 'proof' || command === 'openclaw') {
1164
+ const subcommand = process.argv[3];
1165
+ const args = process.argv.slice(4);
1166
+ require('../commands/proof').proofCommand(subcommand, ...args);
1163
1167
  } else if (command === 'setup') {
1164
1168
  require('../commands/setup').setupAtris()
1165
1169
  .then(() => process.exit(0))
@@ -1188,6 +1192,10 @@ if (command === 'init') {
1188
1192
  require('../commands/feedback').feedbackCommand()
1189
1193
  .then(() => process.exit(0))
1190
1194
  .catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
1195
+ } else if (command === 'errors') {
1196
+ require('../commands/errors').errorsCommand()
1197
+ .then(() => process.exit(0))
1198
+ .catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
1191
1199
  } else {
1192
1200
  console.log(`Unknown command: ${command}`);
1193
1201
  console.log('Run "atris help" to see available commands');
@@ -13,6 +13,20 @@ function getBusinessConfigPath() {
13
13
  return path.join(dir, 'businesses.json');
14
14
  }
15
15
 
16
+ function slugify(name) {
17
+ return String(name || '')
18
+ .toLowerCase()
19
+ .trim()
20
+ .replace(/[^a-z0-9\s-]/g, '')
21
+ .replace(/\s+/g, '-')
22
+ .replace(/-+/g, '-')
23
+ .replace(/^-+|-+$/g, '');
24
+ }
25
+
26
+ function isHelpToken(arg) {
27
+ return arg === '--help' || arg === '-h' || arg === 'help' || arg === '-?';
28
+ }
29
+
16
30
  function loadBusinesses() {
17
31
  const p = getBusinessConfigPath();
18
32
  if (!fs.existsSync(p)) return {};
@@ -774,7 +788,45 @@ function detectBusinessSlug(explicitSlug) {
774
788
  }
775
789
  }
776
790
 
791
+ async function findExistingBusinessBySlug(slug, token) {
792
+ if (!slug) return null;
793
+
794
+ // Local cache first — no network round-trip needed.
795
+ const local = loadBusinesses();
796
+ if (local[slug]) {
797
+ return { id: local[slug].business_id, name: local[slug].name, slug, source: 'local' };
798
+ }
799
+ for (const v of Object.values(local)) {
800
+ if (v && v.slug === slug) {
801
+ return { id: v.business_id, name: v.name, slug, source: 'local' };
802
+ }
803
+ }
804
+
805
+ if (!token) return null;
806
+
807
+ // Cloud lookup — covers businesses the user is a member of but hasn't added.
808
+ const direct = await apiRequestJson(`/business/by-slug/${encodeURIComponent(slug)}`, {
809
+ method: 'GET',
810
+ token,
811
+ });
812
+ if (direct.ok && direct.data && direct.data.id) {
813
+ return { id: direct.data.id, name: direct.data.name, slug: direct.data.slug || slug, source: 'cloud' };
814
+ }
815
+
816
+ const list = await apiRequestJson('/business/', { method: 'GET', token });
817
+ if (list.ok && Array.isArray(list.data)) {
818
+ const match = list.data.find(b => b && b.slug === slug);
819
+ if (match) return { id: match.id, name: match.name, slug: match.slug, source: 'cloud' };
820
+ }
821
+
822
+ return null;
823
+ }
824
+
777
825
  async function addBusiness(slug) {
826
+ if (!slug || isHelpToken(slug)) {
827
+ console.error('Usage: atris business add <slug>');
828
+ process.exit(1);
829
+ }
778
830
  if (!slug) {
779
831
  console.error('Usage: atris business add <slug>');
780
832
  process.exit(1);
@@ -1008,7 +1060,7 @@ function listBusinessesLocal(opts = {}) {
1008
1060
  }
1009
1061
 
1010
1062
  async function removeBusiness(slug) {
1011
- if (!slug) {
1063
+ if (!slug || isHelpToken(slug)) {
1012
1064
  console.error('Usage: atris business remove <slug>');
1013
1065
  process.exit(1);
1014
1066
  }
@@ -1262,8 +1314,11 @@ async function businessAudit() {
1262
1314
  }
1263
1315
 
1264
1316
  async function createBusinessInternal(name, flags = [], mode = 'auto') {
1265
- if (!name) {
1317
+ if (!name || isHelpToken(name) || String(name).startsWith('-')) {
1266
1318
  console.error('Usage: atris business create <name> [--description "..."] [--workspace] [--here|--root <dir>]');
1319
+ if (name && String(name).startsWith('-') && !isHelpToken(name)) {
1320
+ console.error(`\n Refusing to create a business named "${name}" — looks like a flag, not a name.`);
1321
+ }
1267
1322
  process.exit(1);
1268
1323
  }
1269
1324
 
@@ -1275,6 +1330,29 @@ async function createBusinessInternal(name, flags = [], mode = 'auto') {
1275
1330
 
1276
1331
  const options = parseCreateBusinessFlags(flags);
1277
1332
  const description = options.description;
1333
+ const force = flags.includes('--force') || flags.includes('--allow-duplicate');
1334
+
1335
+ // Pre-flight: refuse to create a duplicate by slug. The backend will silently
1336
+ // suffix `-1`, `-2`, etc., which produces ghost businesses when users actually
1337
+ // wanted to attach to an existing one. Guide them to `atris pull` instead.
1338
+ if (!force) {
1339
+ const desiredSlug = slugify(name);
1340
+ if (desiredSlug) {
1341
+ const existing = await findExistingBusinessBySlug(desiredSlug, creds.token);
1342
+ if (existing) {
1343
+ console.error(`\nA business with slug "${desiredSlug}" already exists.`);
1344
+ console.error(` Name: ${existing.name || desiredSlug}`);
1345
+ if (existing.id) console.error(` ID: ${existing.id}`);
1346
+ console.error('');
1347
+ console.error('To set up a local workspace for it, run:');
1348
+ console.error(` atris pull ${desiredSlug} # into ./${desiredSlug}`);
1349
+ console.error(` atris pull ${desiredSlug} --into <path> # into a custom path`);
1350
+ console.error('');
1351
+ console.error(`To create a NEW business anyway (will be slugged "${desiredSlug}-1"), pass --force.`);
1352
+ process.exit(1);
1353
+ }
1354
+ }
1355
+ }
1278
1356
 
1279
1357
  console.log(`Creating business: ${name}...`);
1280
1358
 
@@ -1832,12 +1910,52 @@ async function quickstart() {
1832
1910
  (get 1 email/day instead of every notification)
1833
1911
 
1834
1912
  Templates: saas, agency, ecommerce, content, restaurant
1913
+
1914
+ Rule of thumb:
1915
+ atris business init "<name>" = cloud + local business computer workspace
1916
+ atris business create "<name>" = cloud-only unless you pass --workspace
1835
1917
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1836
1918
  `);
1837
1919
  }
1838
1920
 
1839
1921
 
1922
+ function printBusinessHelp() {
1923
+ console.log('Usage: atris business <command> [args]');
1924
+ console.log('');
1925
+ console.log(' quickstart ← Start here! 3-command guide');
1926
+ console.log('');
1927
+ console.log(' init <name> RECOMMENDED: create a business environment (cloud + local)');
1928
+ console.log(' workspace <name> Alias for init');
1929
+ console.log(' create <name> Cloud-only business record; add --workspace to also scaffold local');
1930
+ console.log(' add <slug> Register an existing cloud business');
1931
+ console.log(' list Show registered businesses');
1932
+ console.log(' team [slug] Show members, roles, and admin access');
1933
+ console.log(' status <slug> Quick status check');
1934
+ console.log(' health [slug] Full health dashboard');
1935
+ console.log(' audit Audit all businesses');
1936
+ console.log(' connect <service> Connect a skill/integration');
1937
+ console.log(' notify <mode> Set notification mode (digest/silent/push)');
1938
+ console.log(' deploy <slug> Push local business to cloud');
1939
+ console.log(' onboard Seed brief, person, first loop, safe next action, and one-pager from sparse input');
1940
+ console.log(' record <report> Append recap state into events, episodes, and scorecards');
1941
+ console.log(' remove <slug> Unregister locally');
1942
+ console.log('');
1943
+ console.log(' Already-attached business? Run `atris pull <slug>` to scaffold a local workspace.');
1944
+ }
1945
+
1840
1946
  async function businessCommand(subcommand, ...args) {
1947
+ // Help intercept — without this, `atris business init --help` would treat
1948
+ // `--help` as a business name and create one. Same for any subcommand that
1949
+ // takes a positional name/slug.
1950
+ if (!subcommand || isHelpToken(subcommand)) {
1951
+ printBusinessHelp();
1952
+ return;
1953
+ }
1954
+ if (args.length > 0 && isHelpToken(args[0])) {
1955
+ printBusinessHelp();
1956
+ return;
1957
+ }
1958
+
1841
1959
  switch (subcommand) {
1842
1960
  case 'add':
1843
1961
  await addBusiness(args[0]);
@@ -1907,25 +2025,10 @@ async function businessCommand(subcommand, ...args) {
1907
2025
  await quickstart();
1908
2026
  break;
1909
2027
  default:
1910
- console.log('Usage: atris business <command> [args]');
1911
- console.log('');
1912
- console.log(' quickstart ← Start here! 3-command guide');
1913
- console.log('');
1914
- console.log(' init <name> Create a business environment (cloud + local)');
1915
- console.log(' workspace <name> Alias for init');
1916
- console.log(' create <name> Create the cloud business; add --workspace for a local business environment');
1917
- console.log(' add <slug> Register an existing cloud business');
1918
- console.log(' list Show registered businesses');
1919
- console.log(' team [slug] Show members, roles, and admin access');
1920
- console.log(' status <slug> Quick status check');
1921
- console.log(' health [slug] Full health dashboard');
1922
- console.log(' audit Audit all businesses');
1923
- console.log(' connect <service> Connect a skill/integration');
1924
- console.log(' notify <mode> Set notification mode (digest/silent/push)');
1925
- console.log(' deploy <slug> Push local business to cloud');
1926
- console.log(' onboard Seed brief, person, first loop, safe next action, and one-pager from sparse input');
1927
- console.log(' record <report> Append recap state into events, episodes, and scorecards');
1928
- console.log(' remove <slug> Unregister locally');
2028
+ console.error(`Unknown subcommand: ${subcommand}`);
2029
+ console.error('');
2030
+ printBusinessHelp();
2031
+ process.exitCode = 1;
1929
2032
  }
1930
2033
  }
1931
2034
 
@@ -52,8 +52,40 @@ const KNOWN_CHAT_COMMANDS = new Set([
52
52
  '/start',
53
53
  '/status',
54
54
  '/worker',
55
+ '/workflow',
55
56
  ]);
56
57
 
58
+ const CODEOPS_WORKFLOW_PROMPT = `
59
+ ## Atris CodeOps Workflow
60
+
61
+ You are running inside Atris CodeOps with full computer permissions (permission_mode=bypassPermissions).
62
+ Use those permissions to inspect, edit, test, commit, push, and open PRs when the task calls for it.
63
+
64
+ Do not behave like an open-ended chat.
65
+ Every coding or repo operation must follow the scientific workflow:
66
+ OBSERVE -> HYPOTHESIS -> PLAN -> ACTION -> VALIDATION -> EVIDENCE -> NEXT STATE.
67
+
68
+ For a new coding request, first show a concise PLAN with Files, Checks, Risk, and Merge policy.
69
+ If the user has not clearly approved execution, ask for approval before editing.
70
+ If the user explicitly says to execute, proceed after the concise plan.
71
+
72
+ After work, always report:
73
+ - edited_files
74
+ - commands_run
75
+ - validation_result
76
+ - evidence
77
+ - pr_url if any
78
+ - pr_state
79
+ - merge_state
80
+ - next_task
81
+
82
+ Use one of these next states:
83
+ planned, executing, validated, pr_opened, merge_ready, merge_blocked_checks, merge_blocked_policy, merged, failed, needs_human.
84
+
85
+ Never hide failures.
86
+ A blocked check or missing permission is evidence, not success.
87
+ `.trim();
88
+
57
89
  function color(code, value) {
58
90
  if (process.env.NO_COLOR || !process.stdout.isTTY) return String(value);
59
91
  return `\x1b[${code}m${value}\x1b[0m`;
@@ -165,6 +197,7 @@ function printCloudHelp() {
165
197
  console.log(' /start Show the beginner flow again');
166
198
  console.log(' /help Show this menu');
167
199
  console.log(' /status Show cloud computer status');
200
+ console.log(' /workflow Show the CodeOps workflow contract');
168
201
  console.log(' /files [path] List files in the workspace');
169
202
  console.log(' /run <cmd> Run shell without the model');
170
203
  console.log(' /audit [n] Show recent runs, output, and charges');
@@ -201,6 +234,48 @@ function printCloudStartPanel(ctx, worker, model, billingLabel, authSummary = nu
201
234
  console.log(ui.dim('Plain English goes to Atris. Slash commands control the computer.'));
202
235
  }
203
236
 
237
+ function appendSystemPrompt(basePrompt, extraPrompt) {
238
+ if (!extraPrompt) return basePrompt || null;
239
+ if (basePrompt && basePrompt.includes('## Atris CodeOps Workflow')) return basePrompt;
240
+ if (!basePrompt) return extraPrompt;
241
+ return `${String(basePrompt).trim()}\n\n${extraPrompt}`;
242
+ }
243
+
244
+ function codeOpsCloudOptions(options = {}) {
245
+ return {
246
+ ...options,
247
+ worker: options.worker || 'claude',
248
+ mode: 'codeops',
249
+ systemPrompt: appendSystemPrompt(options.systemPrompt, CODEOPS_WORKFLOW_PROMPT),
250
+ };
251
+ }
252
+
253
+ function printCodeOpsStartPanel(ctx, worker, model, billingLabel, authSummary = null) {
254
+ console.log('');
255
+ console.log(ui.bold('Atris CodeOps Computer'));
256
+ console.log(`${ctx.businessName} ${ui.dim('/workspace persists, full permissions enabled')}`);
257
+ console.log(`Lane: ${ui.bold(formatWorkerName(worker))} ${ui.dim(formatCloudSelection({ worker, model }))}`);
258
+ console.log(`Billing: ${billingLabel}`);
259
+ if (authSummary) console.log(`${authSummary.label} ${ui.dim(authSummary.detail)}`);
260
+ console.log(`${ui.green('Workflow locked')} ${ui.dim('observe -> plan -> act -> validate -> evidence -> next')}`);
261
+ console.log('');
262
+ console.log(ui.bold('Start here'));
263
+ console.log(' Type a coding goal in plain English.');
264
+ console.log(' CodeOps will plan first, then execute after approval or explicit proceed language.');
265
+ console.log(' Use /workflow to see the contract, /run for shell, /audit for run history, /exit to leave.');
266
+ console.log('');
267
+ }
268
+
269
+ function printCodeOpsWorkflowContract() {
270
+ console.log('');
271
+ console.log(ui.bold('CodeOps workflow'));
272
+ console.log(' observe -> hypothesis -> plan -> action -> validation -> evidence -> next state');
273
+ console.log('');
274
+ console.log(' Required final evidence: edited_files, commands_run, validation_result, pr_url, pr_state, merge_state, next_task.');
275
+ console.log(' Allowed states: planned, executing, validated, pr_opened, merge_ready, merge_blocked_checks, merge_blocked_policy, merged, failed, needs_human.');
276
+ console.log(' Full permissions stay on; the workflow contract controls how the computer uses them.');
277
+ }
278
+
204
279
  function buildLocalBridgeSystemPrompt(sessionId, localRoot, allowBash) {
205
280
  const endpoint = `/api/cli/sessions/${sessionId}/file-op`;
206
281
  const bashLine = allowBash
@@ -1386,6 +1461,8 @@ async function computerExec(token, prompt, ctx = null, options = {}) {
1386
1461
  workspace_id: ctx.workspaceId,
1387
1462
  ...(options.worker ? { worker: options.worker } : {}),
1388
1463
  ...(options.model ? { model: options.model } : {}),
1464
+ ...(options.systemPrompt ? { system_prompt: options.systemPrompt } : {}),
1465
+ ...(options.allowedTools ? { allowed_tools: options.allowedTools } : {}),
1389
1466
  },
1390
1467
  timeoutMs: 40000,
1391
1468
  });
@@ -1742,6 +1819,10 @@ async function computerChat(token, ctx, initialOptions = {}) {
1742
1819
  return;
1743
1820
  }
1744
1821
 
1822
+ const isCodeOps = initialOptions.mode === 'codeops' || ctx.slug === 'atris-codeops';
1823
+ const chatSystemPrompt = isCodeOps
1824
+ ? appendSystemPrompt(initialOptions.systemPrompt, CODEOPS_WORKFLOW_PROMPT)
1825
+ : initialOptions.systemPrompt;
1745
1826
  let sessionId = `biz-${ctx.businessId.slice(0, 8)}-${Date.now().toString(36)}`;
1746
1827
  printCloudWordmark();
1747
1828
  const selection = await chooseCloudLane(token, ctx, initialOptions);
@@ -1758,12 +1839,16 @@ async function computerChat(token, ctx, initialOptions = {}) {
1758
1839
  return;
1759
1840
  }
1760
1841
 
1761
- printCloudStartPanel(ctx, worker, model, billingLabel, authSummary);
1842
+ if (isCodeOps) {
1843
+ printCodeOpsStartPanel(ctx, worker, model, billingLabel, authSummary);
1844
+ } else {
1845
+ printCloudStartPanel(ctx, worker, model, billingLabel, authSummary);
1846
+ }
1762
1847
 
1763
1848
  const rl = readline.createInterface({
1764
1849
  input: process.stdin,
1765
1850
  output: process.stdout,
1766
- prompt: 'cloud> ',
1851
+ prompt: isCodeOps ? 'codeops> ' : 'cloud> ',
1767
1852
  });
1768
1853
 
1769
1854
  rl.prompt();
@@ -1782,7 +1867,11 @@ async function computerChat(token, ctx, initialOptions = {}) {
1782
1867
  if (line === '/start') {
1783
1868
  billingLabel = await describeBillingMode(token, ctx, worker);
1784
1869
  authSummary = activeWorker(worker) === 'claude' ? await describeClaudeAuth(token, ctx) : null;
1785
- printCloudStartPanel(ctx, worker, model, billingLabel, authSummary);
1870
+ if (isCodeOps) {
1871
+ printCodeOpsStartPanel(ctx, worker, model, billingLabel, authSummary);
1872
+ } else {
1873
+ printCloudStartPanel(ctx, worker, model, billingLabel, authSummary);
1874
+ }
1786
1875
  rl.prompt();
1787
1876
  continue;
1788
1877
  }
@@ -1796,6 +1885,11 @@ async function computerChat(token, ctx, initialOptions = {}) {
1796
1885
  rl.prompt();
1797
1886
  continue;
1798
1887
  }
1888
+ if (line === '/workflow') {
1889
+ printCodeOpsWorkflowContract();
1890
+ rl.prompt();
1891
+ continue;
1892
+ }
1799
1893
  if (line === '/pwd') {
1800
1894
  await computerRun(token, 'pwd', ctx);
1801
1895
  rl.prompt();
@@ -1889,7 +1983,12 @@ async function computerChat(token, ctx, initialOptions = {}) {
1889
1983
  }
1890
1984
  }
1891
1985
 
1892
- sessionId = await sendBusinessChat(token, ctx, line, sessionId, false, rl, { worker, model });
1986
+ sessionId = await sendBusinessChat(token, ctx, line, sessionId, false, rl, {
1987
+ worker,
1988
+ model,
1989
+ systemPrompt: chatSystemPrompt,
1990
+ allowedTools: initialOptions.allowedTools,
1991
+ });
1893
1992
  rl.prompt();
1894
1993
  }
1895
1994
  } catch (error) {
@@ -2318,7 +2417,7 @@ async function runComputer() {
2318
2417
  console.log(' local-byo Open LOCAL BYO Claude mode; Anthropic tokens, no cloud audit');
2319
2418
  console.log(' --cloud Open CLOUD workspace mode in the bound business workspace');
2320
2419
  console.log(' cloud Open CLOUD workspace mode in the bound business workspace');
2321
- console.log(' codeops Open Atris CodeOps cloud workspace if your account has access');
2420
+ console.log(' codeops Open Atris CodeOps workflow computer if your account has access');
2322
2421
  console.log(' --worker Cloud worker override: claude | openai');
2323
2422
  console.log(' --model Cloud model override');
2324
2423
  console.log(' claude|codex Legacy local console backends');
@@ -2351,6 +2450,9 @@ async function runComputer() {
2351
2450
  console.log(' atris computer --cloud --worker openai --model gpt-5.4');
2352
2451
  console.log(' atris computer cloud');
2353
2452
  console.log(' atris computer codeops');
2453
+ console.log(' atris computer codeops status');
2454
+ console.log(' atris computer codeops run "pwd"');
2455
+ console.log(' atris computer codeops exec "Plan a safe repo fix"');
2354
2456
  console.log(' atris computer status');
2355
2457
  console.log(' atris computer wake');
2356
2458
  console.log(' atris computer run "ls -la /workspace"');
@@ -2370,7 +2472,44 @@ async function runComputer() {
2370
2472
  console.error('Ask an Atris CodeOps admin to add you to the atris-codeops business.');
2371
2473
  return;
2372
2474
  }
2373
- await computerChat(token, codeopsCtx, { worker: 'claude', ...cloudOptions });
2475
+ const codeopsOptions = codeOpsCloudOptions(cloudOptions);
2476
+ const codeopsSub = args[1];
2477
+ const codeopsRest = args.slice(2).join(' ');
2478
+ if (!codeopsSub || codeopsSub === 'chat') {
2479
+ await computerChat(token, codeopsCtx, codeopsOptions);
2480
+ return;
2481
+ }
2482
+ switch (codeopsSub) {
2483
+ case '--help':
2484
+ case 'help':
2485
+ console.log('Usage: atris computer codeops [chat|status|wake|sleep|run|grep|ls|cat|exec|audit|workflow]');
2486
+ console.log('');
2487
+ console.log('Examples:');
2488
+ console.log(' atris computer codeops');
2489
+ console.log(' atris computer codeops status');
2490
+ console.log(' atris computer codeops run "pwd && git status --short"');
2491
+ console.log(' atris computer codeops exec "Plan the smallest safe fix, then wait"');
2492
+ return;
2493
+ case 'status': return computerStatus(token, codeopsCtx);
2494
+ case 'wake': return computerWake(token, codeopsCtx);
2495
+ case 'sleep': return computerSleep(token, codeopsCtx);
2496
+ case 'run': return computerRun(token, codeopsRest, codeopsCtx);
2497
+ case 'grep': return computerGrep(token, codeopsRest, codeopsCtx);
2498
+ case 'ls': return computerLs(token, codeopsRest || undefined, codeopsCtx);
2499
+ case 'cat': return computerCat(token, codeopsRest, codeopsCtx);
2500
+ case 'exec': return computerExec(token, codeopsRest, codeopsCtx, codeopsOptions);
2501
+ case 'audit': {
2502
+ const limit = codeopsRest ? Number.parseInt(codeopsRest, 10) : 10;
2503
+ return computerAudit(token, codeopsCtx, Number.isFinite(limit) ? limit : 10);
2504
+ }
2505
+ case 'workflow':
2506
+ printCodeOpsWorkflowContract();
2507
+ return;
2508
+ default:
2509
+ console.error(`Unknown CodeOps subcommand: ${codeopsSub}`);
2510
+ console.log('Run: atris computer codeops help');
2511
+ return;
2512
+ }
2374
2513
  return;
2375
2514
  }
2376
2515
 
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Errors command for Atris CLI — admin dashboard over atris_error_events.
3
+ *
4
+ * Usage:
5
+ * atris errors List errors from last 24h, grouped by signature
6
+ * atris errors --hours 72 Widen the window (max 720h / 30d)
7
+ * atris errors --limit 1000 Raise the raw-event cap for grouping
8
+ * atris errors show <id> Full detail (stack trace, message) for one event
9
+ *
10
+ * Requires admin role on the user row.
11
+ */
12
+
13
+ const { loadCredentials } = require('../utils/auth');
14
+ const { apiRequestJson } = require('../utils/api');
15
+
16
+ function getToken() {
17
+ const creds = loadCredentials();
18
+ if (!creds || !creds.token) {
19
+ console.error('Not logged in. Run: atris login');
20
+ process.exit(1);
21
+ }
22
+ return creds.token;
23
+ }
24
+
25
+ function extractFlag(args, ...names) {
26
+ const remaining = [];
27
+ let value = null;
28
+ for (let i = 0; i < args.length; i++) {
29
+ const a = args[i];
30
+ const eq = a.indexOf('=');
31
+ const key = eq >= 0 ? a.slice(0, eq) : a;
32
+ if (names.includes(key)) {
33
+ value = eq >= 0 ? a.slice(eq + 1) : args[++i];
34
+ } else {
35
+ remaining.push(a);
36
+ }
37
+ }
38
+ return [value, remaining];
39
+ }
40
+
41
+ async function listErrors(args) {
42
+ const [hoursArg, r1] = extractFlag(args, '--hours', '-H');
43
+ const [limitArg, r2] = extractFlag(r1, '--limit', '-L');
44
+ const hours = hoursArg ? parseInt(hoursArg, 10) : 24;
45
+ const limit = limitArg ? parseInt(limitArg, 10) : 500;
46
+
47
+ const token = getToken();
48
+ const result = await apiRequestJson(`/errors?hours=${hours}&limit=${limit}`, {
49
+ method: 'GET',
50
+ token,
51
+ });
52
+
53
+ if (!result.ok) {
54
+ console.error(`Error: ${result.error || 'Failed to fetch errors'}`);
55
+ process.exit(1);
56
+ }
57
+
58
+ const data = result.data || {};
59
+ const groups = data.groups || [];
60
+
61
+ if (groups.length === 0) {
62
+ console.log(`No errors in the last ${hours}h. Clean.`);
63
+ return;
64
+ }
65
+
66
+ console.log(
67
+ `Errors — last ${data.window_hours}h — ` +
68
+ `${data.total_events} events across ${data.unique_signatures} signatures\n`,
69
+ );
70
+
71
+ groups.forEach((g, idx) => {
72
+ const s = g.sample || {};
73
+ const shortId = (s.id || '').substring(0, 8);
74
+ const last = s.created_at ? s.created_at.substring(0, 16).replace('T', ' ') : '';
75
+ const status = s.status_code ? ` [${s.status_code}]` : '';
76
+ const msg = (s.message || '').replace(/\s+/g, ' ').substring(0, 140);
77
+
78
+ console.log(`${idx + 1}. x${g.count} ${g.signature}${status}`);
79
+ if (last) console.log(` last: ${last} UTC latest id: ${shortId}`);
80
+ if (msg) console.log(` "${msg}"`);
81
+ console.log('');
82
+ });
83
+
84
+ console.log(
85
+ `Run \`atris errors show <id>\` for full stack trace of a specific event.`,
86
+ );
87
+ }
88
+
89
+ async function showError(errorId) {
90
+ if (!errorId) {
91
+ console.error('Usage: atris errors show <id>');
92
+ console.error('(id must be a full UUID — get one from `atris errors` output)');
93
+ process.exit(1);
94
+ }
95
+
96
+ const token = getToken();
97
+ const result = await apiRequestJson(`/errors/${encodeURIComponent(errorId)}`, {
98
+ method: 'GET',
99
+ token,
100
+ });
101
+
102
+ if (!result.ok) {
103
+ console.error(`Error: ${result.error || 'Failed to fetch error'}`);
104
+ process.exit(1);
105
+ }
106
+
107
+ const e = result.data || {};
108
+ console.log(`Error ${e.id || errorId}`);
109
+ console.log(` type: ${e.error_type || '?'}`);
110
+ console.log(` method: ${e.request_method || '?'}`);
111
+ console.log(` path: ${e.request_path || '?'}`);
112
+ console.log(` status: ${e.status_code || '?'}`);
113
+ console.log(` when: ${e.created_at || '?'}`);
114
+ console.log(` source: ${e.source || '?'}`);
115
+ if (e.user_id) console.log(` user: ${e.user_id}`);
116
+ console.log('');
117
+ console.log('Message:');
118
+ console.log(' ' + (e.message || '(none)').split('\n').join('\n '));
119
+ console.log('');
120
+ if (e.stack_trace) {
121
+ console.log('Stack trace:');
122
+ console.log(' ' + e.stack_trace.split('\n').join('\n '));
123
+ }
124
+ }
125
+
126
+ function printHelp() {
127
+ console.log('');
128
+ console.log('Usage:');
129
+ console.log(' atris errors List errors from last 24h, grouped');
130
+ console.log(' atris errors --hours 72 Widen the window (max 720h / 30d)');
131
+ console.log(' atris errors --limit 1000 Raise the raw-event cap');
132
+ console.log(' atris errors show <full-uuid> Full detail for one event');
133
+ console.log('');
134
+ console.log('Admin role required.');
135
+ console.log('');
136
+ }
137
+
138
+ async function errorsCommand() {
139
+ const args = process.argv.slice(3);
140
+ const sub = args[0];
141
+
142
+ if (sub === '--help' || sub === '-h' || sub === 'help') {
143
+ printHelp();
144
+ return;
145
+ }
146
+
147
+ if (sub === 'show') {
148
+ await showError(args[1]);
149
+ return;
150
+ }
151
+
152
+ await listErrors(args);
153
+ }
154
+
155
+ module.exports = { errorsCommand };
@@ -0,0 +1,115 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ function readBusinessBinding(cwd = process.cwd()) {
5
+ const bindingPath = path.join(cwd, '.atris', 'business.json');
6
+ if (!fs.existsSync(bindingPath)) return null;
7
+ try {
8
+ return JSON.parse(fs.readFileSync(bindingPath, 'utf8'));
9
+ } catch {
10
+ return null;
11
+ }
12
+ }
13
+
14
+ function slugify(value) {
15
+ return String(value || 'business-workflow')
16
+ .toLowerCase()
17
+ .replace(/[^a-z0-9]+/g, '-')
18
+ .replace(/^-+|-+$/g, '') || 'business-workflow';
19
+ }
20
+
21
+ function ensureDir(dir) {
22
+ fs.mkdirSync(dir, { recursive: true });
23
+ }
24
+
25
+ function writeJson(filePath, value) {
26
+ ensureDir(path.dirname(filePath));
27
+ fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
28
+ }
29
+
30
+ function doctor(cwd = process.cwd()) {
31
+ const binding = readBusinessBinding(cwd);
32
+ console.log('Receipt check');
33
+ console.log(`business binding: ${binding ? `${binding.name || binding.slug || binding.business_id} ready` : 'missing'}`);
34
+ console.log(`receipt folder: ${fs.existsSync(path.join(cwd, '.atris', 'receipts')) ? 'ready' : 'missing'}`);
35
+ console.log('');
36
+ console.log('Next: atris receipt init business-workflow');
37
+ }
38
+
39
+ function init(taskSlug = 'business-workflow', cwd = process.cwd()) {
40
+ const binding = readBusinessBinding(cwd);
41
+ if (!binding) {
42
+ console.error('No business binding found. Run: atris business init <name> --here');
43
+ process.exitCode = 1;
44
+ return;
45
+ }
46
+
47
+ const slug = slugify(taskSlug);
48
+ const root = path.join(cwd, '.atris');
49
+ const taskPath = path.join(root, 'tasks', `${slug}.json`);
50
+ const receiptsDir = path.join(root, 'receipts');
51
+
52
+ ensureDir(receiptsDir);
53
+ fs.writeFileSync(path.join(receiptsDir, '.gitkeep'), '');
54
+ writeJson(taskPath, {
55
+ schema: 'atris.receipt_task.v1',
56
+ slug,
57
+ goal: 'Run one business-computer task and save what happened.',
58
+ workspace: {
59
+ business_id: binding.business_id || binding.id || null,
60
+ workspace_id: binding.workspace_id || null,
61
+ name: binding.name || null,
62
+ slug: binding.slug || null,
63
+ },
64
+ runtime: {
65
+ proof_command: 'atris computer proof',
66
+ replay_command: 'atris experiments replay endstate',
67
+ },
68
+ verify: [
69
+ 'atris computer proof',
70
+ 'atris experiments replay endstate',
71
+ ],
72
+ });
73
+
74
+ console.log(`Receipt task ready: ${slug}`);
75
+ console.log(`Task: ${path.relative(cwd, taskPath)}`);
76
+ console.log(`Receipts: ${path.relative(cwd, receiptsDir)}`);
77
+ }
78
+
79
+ function run(args = []) {
80
+ const dryRun = args.includes('--dry-run');
81
+ console.log('Receipt run');
82
+ console.log('1. atris computer proof');
83
+ console.log('2. atris experiments replay endstate');
84
+ if (dryRun) {
85
+ console.log('Dry run only; no receipts written.');
86
+ return;
87
+ }
88
+ console.log('Run those commands, then save the receipt under .atris/receipts/.');
89
+ }
90
+
91
+ function proofCommand(subcommand = 'doctor', ...args) {
92
+ switch (subcommand || 'doctor') {
93
+ case 'doctor':
94
+ return doctor();
95
+ case 'init':
96
+ return init(args[0] || 'business-workflow');
97
+ case 'proof':
98
+ return run(args);
99
+ case 'help':
100
+ case '--help':
101
+ case '-h':
102
+ console.log('Usage: atris receipt [doctor|init <slug>|run --dry-run]');
103
+ return;
104
+ case 'run':
105
+ return run(args);
106
+ default:
107
+ console.error(`Unknown receipt command: ${subcommand}`);
108
+ console.log('Usage: atris receipt [doctor|init <slug>|run --dry-run]');
109
+ process.exitCode = 1;
110
+ }
111
+ }
112
+
113
+ module.exports = {
114
+ proofCommand,
115
+ };
package/commands/pull.js CHANGED
@@ -703,8 +703,10 @@ async function pullBusiness(slug) {
703
703
  name: businessName,
704
704
  }, null, 2));
705
705
 
706
- // Wire skills → .claude/skills/ so they work as slash commands
707
- const skillsDir = path.join(outputDir, 'skills');
706
+ // Wire skills → .claude/skills/ so they work as slash commands.
707
+ // Source of truth is atris/skills/ (vendor-neutral, syncs to cloud).
708
+ // .claude/skills/ is a locally-generated adapter Claude Code reads from.
709
+ const skillsDir = path.join(outputDir, 'atris', 'skills');
708
710
  const claudeSkillsDir = path.join(outputDir, '.claude', 'skills');
709
711
 
710
712
  if (fs.existsSync(skillsDir)) {
package/commands/push.js CHANGED
@@ -367,6 +367,12 @@ async function pushAtris() {
367
367
 
368
368
  if (!result.ok) {
369
369
  if (result.status === 403) {
370
+ const detail = result.errorMessage || result.error || (result.data && result.data.detail) || '';
371
+ if (detail && /plan required|business, max, or enterprise/i.test(detail)) {
372
+ console.error(`\n Access denied: ${detail}`);
373
+ await emit('access_denied', { error_detail: detail });
374
+ process.exit(1);
375
+ }
370
376
  // Permission denied — retry with only team/ and journal/ files
371
377
  const allowed = filesToPush.filter(f => f.path.startsWith('/team/') || f.path.startsWith('/journal/'));
372
378
  skipped = filesToPush.filter(f => !f.path.startsWith('/team/') && !f.path.startsWith('/journal/'));
@@ -1,20 +1,24 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
+ const { spawnSync } = require('child_process');
3
4
  const { getLogPath } = require('../lib/journal');
5
+ const { ensureValidCredentials } = require('../utils/auth');
6
+ const { apiRequestJson } = require('../utils/api');
7
+ const { loadConfig } = require('../utils/config');
4
8
 
5
- function visualizeAtris() {
6
- const { logFile, dateFormatted } = getLogPath();
9
+ const DEFAULT_MODEL = 'gpt-image-2';
10
+ const DEFAULT_SIZE = '1536x1024';
11
+ const DEFAULT_QUALITY = 'high';
12
+
13
+ function legacyVisualizeInbox() {
14
+ const { logFile } = getLogPath();
7
15
 
8
- // Check if log exists
9
16
  if (!fs.existsSync(logFile)) {
10
17
  console.log('✗ No journal entry for today. Run "atris log" to create one.');
11
18
  process.exit(1);
12
19
  }
13
20
 
14
- // Read the log file
15
21
  const logContent = fs.readFileSync(logFile, 'utf8');
16
-
17
- // Extract Inbox section
18
22
  const inboxMatch = logContent.match(/## Inbox\n([\s\S]*?)(?=\n##|$)/);
19
23
  if (!inboxMatch || !inboxMatch[1].trim()) {
20
24
  console.log('✗ No items in Inbox. Add ideas to your journal first.');
@@ -35,7 +39,6 @@ function visualizeAtris() {
35
39
  process.exit(1);
36
40
  }
37
41
 
38
- // Display visualization template
39
42
  console.log('');
40
43
  console.log('┌─────────────────────────────────────────────────────────────┐');
41
44
  console.log('│ Atris Visualize — Break Down & Approval Gate │');
@@ -68,7 +71,320 @@ function visualizeAtris() {
68
71
  console.log('');
69
72
  }
70
73
 
74
+ function parseVisualizeArgs(args = []) {
75
+ const options = {
76
+ model: DEFAULT_MODEL,
77
+ size: DEFAULT_SIZE,
78
+ quality: DEFAULT_QUALITY,
79
+ outputFormat: 'png',
80
+ dryRun: false,
81
+ open: false,
82
+ timeoutMs: 180000,
83
+ };
84
+ const promptParts = [];
85
+
86
+ for (let i = 0; i < args.length; i++) {
87
+ const arg = args[i];
88
+ if (arg === '--') {
89
+ promptParts.push(...args.slice(i + 1));
90
+ break;
91
+ }
92
+ if (arg === '--help' || arg === '-h') options.help = true;
93
+ else if (arg === '--dry-run') options.dryRun = true;
94
+ else if (arg === '--open') options.open = true;
95
+ else if (arg === '--no-open') options.open = false;
96
+ else if (arg === '--raw') options.raw = true;
97
+ else if (arg === '--agent' && args[i + 1]) options.agentId = args[++i];
98
+ else if (arg.startsWith('--agent=')) options.agentId = arg.slice('--agent='.length);
99
+ else if (arg === '--model' && args[i + 1]) options.model = args[++i];
100
+ else if (arg.startsWith('--model=')) options.model = arg.slice('--model='.length);
101
+ else if (arg === '--size' && args[i + 1]) options.size = args[++i];
102
+ else if (arg.startsWith('--size=')) options.size = arg.slice('--size='.length);
103
+ else if (arg === '--quality' && args[i + 1]) options.quality = args[++i];
104
+ else if (arg.startsWith('--quality=')) options.quality = arg.slice('--quality='.length);
105
+ else if (arg === '--out' && args[i + 1]) options.out = args[++i];
106
+ else if (arg.startsWith('--out=')) options.out = arg.slice('--out='.length);
107
+ else if (arg === '--timeout' && args[i + 1]) options.timeoutMs = Number(args[++i]) * 1000;
108
+ else if (arg.startsWith('--timeout=')) options.timeoutMs = Number(arg.slice('--timeout='.length)) * 1000;
109
+ else if (arg === '--format' && args[i + 1]) options.outputFormat = args[++i];
110
+ else if (arg.startsWith('--format=')) options.outputFormat = arg.slice('--format='.length);
111
+ else promptParts.push(arg);
112
+ }
113
+
114
+ return { prompt: promptParts.join(' ').trim(), options };
115
+ }
116
+
117
+ function showVisualizeHelp() {
118
+ console.log('');
119
+ console.log('Usage: atris visualize <prompt> [options]');
120
+ console.log('');
121
+ console.log('Generate a Slack/deck-ready business visual from workspace context.');
122
+ console.log('');
123
+ console.log('Options:');
124
+ console.log(' --model <name> Image model (default: gpt-image-2)');
125
+ console.log(' --size <WxH> Output size (default: 1536x1024)');
126
+ console.log(' --quality <level> Quality (default: high)');
127
+ console.log(' --out <path> Save path (default: atris/reports/visuals/<slug>.png)');
128
+ console.log(' --agent <id> Agent id for backend image endpoint');
129
+ console.log(' --dry-run Print generated prompt without calling the backend');
130
+ console.log(' --open Open the saved PNG after generation');
131
+ console.log(' --raw Send your prompt as-is, without workspace prompt shaping');
132
+ console.log('');
133
+ console.log('No prompt keeps the legacy inbox visualization helper.');
134
+ console.log('');
135
+ }
136
+
137
+ function readTextIfExists(filePath, maxChars) {
138
+ try {
139
+ if (!fs.existsSync(filePath)) return '';
140
+ return fs.readFileSync(filePath, 'utf8').slice(0, maxChars);
141
+ } catch {
142
+ return '';
143
+ }
144
+ }
145
+
146
+ function readBusinessMeta(cwd = process.cwd()) {
147
+ const businessPath = path.join(cwd, '.atris', 'business.json');
148
+ try {
149
+ if (!fs.existsSync(businessPath)) return {};
150
+ return JSON.parse(fs.readFileSync(businessPath, 'utf8'));
151
+ } catch {
152
+ return {};
153
+ }
154
+ }
155
+
156
+ function findRelevantContextFiles(cwd, prompt) {
157
+ const roots = [
158
+ path.join(cwd, 'atris', 'context'),
159
+ path.join(cwd, 'atris', 'wiki'),
160
+ ];
161
+ const words = new Set(
162
+ prompt.toLowerCase().split(/[^a-z0-9]+/).filter(w => w.length >= 4)
163
+ );
164
+ const files = [];
165
+
166
+ function walk(dir) {
167
+ if (!fs.existsSync(dir)) return;
168
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
169
+ const full = path.join(dir, entry.name);
170
+ if (entry.isDirectory()) walk(full);
171
+ else if (entry.isFile() && entry.name.endsWith('.md')) files.push(full);
172
+ }
173
+ }
174
+
175
+ roots.forEach(walk);
176
+ return files
177
+ .map(file => {
178
+ const rel = path.relative(cwd, file);
179
+ const haystack = rel.toLowerCase();
180
+ let score = 0;
181
+ for (const word of words) {
182
+ if (haystack.includes(word)) score += 3;
183
+ }
184
+ const body = readTextIfExists(file, 1200).toLowerCase();
185
+ for (const word of words) {
186
+ if (body.includes(word)) score += 1;
187
+ }
188
+ return { file, rel, score };
189
+ })
190
+ .filter(item => item.score > 0)
191
+ .sort((a, b) => b.score - a.score)
192
+ .slice(0, 4);
193
+ }
194
+
195
+ function collectWorkspaceContext(prompt, cwd = process.cwd()) {
196
+ const business = readBusinessMeta(cwd);
197
+ const chunks = [];
198
+ if (business.name || business.slug) {
199
+ chunks.push(`Workspace: ${business.name || business.slug} (${business.slug || 'no-slug'})`);
200
+ }
201
+
202
+ const mapSnippet = readTextIfExists(path.join(cwd, 'atris', 'MAP.md'), 1400);
203
+ if (mapSnippet) chunks.push(`MAP excerpt:\n${mapSnippet}`);
204
+
205
+ const todoSnippet = readTextIfExists(path.join(cwd, 'atris', 'TODO.md'), 1000);
206
+ if (todoSnippet) chunks.push(`TODO excerpt:\n${todoSnippet}`);
207
+
208
+ const relevant = findRelevantContextFiles(cwd, prompt);
209
+ for (const item of relevant) {
210
+ chunks.push(`${item.rel}:\n${readTextIfExists(item.file, 900)}`);
211
+ }
212
+
213
+ return chunks.join('\n\n---\n\n').slice(0, 6000);
214
+ }
215
+
216
+ function classifyArtifact(prompt) {
217
+ const p = prompt.toLowerCase();
218
+ if (/security|compliance|soc2|soc 2|questionnaire|risk|posture/.test(p)) return 'security posture';
219
+ if (/wbr|weekly|metric|metrics|revenue|p&l|pnl|forecast|dashboard/.test(p)) return 'metric story';
220
+ if (/onboard|setup|connect|workflow|process|flow|steps|how to/.test(p)) return 'workflow';
221
+ if (/architecture|system|infra|stack|api|database|service/.test(p)) return 'architecture';
222
+ if (/compare|comparison|versus|\bvs\b|tradeoff/.test(p)) return 'comparison';
223
+ if (/status|update|recap|progress|roadmap/.test(p)) return 'status update';
224
+ return 'business explainer';
225
+ }
226
+
227
+ function slugify(input) {
228
+ return String(input || 'visual')
229
+ .toLowerCase()
230
+ .replace(/[^a-z0-9]+/g, '-')
231
+ .replace(/^-+|-+$/g, '')
232
+ .slice(0, 54) || 'visual';
233
+ }
234
+
235
+ function defaultOutputPath(prompt, cwd = process.cwd()) {
236
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
237
+ const visualsDir = fs.existsSync(path.join(cwd, 'atris'))
238
+ ? path.join(cwd, 'atris', 'reports', 'visuals')
239
+ : path.join(cwd, 'visuals');
240
+ return path.join(visualsDir, `${slugify(prompt)}-${stamp}.png`);
241
+ }
242
+
243
+ function resolveOutputPath(out, prompt, cwd = process.cwd()) {
244
+ if (!out) return defaultOutputPath(prompt, cwd);
245
+ return path.isAbsolute(out) ? out : path.join(cwd, out);
246
+ }
247
+
248
+ function buildImagePrompt(userPrompt, options = {}, cwd = process.cwd()) {
249
+ if (options.raw) return userPrompt;
250
+
251
+ const artifactType = classifyArtifact(userPrompt);
252
+ const context = collectWorkspaceContext(userPrompt, cwd);
253
+ const contextBlock = context ? `\nWorkspace context to respect:\n${context}\n` : '';
254
+
255
+ return `Use case: productivity-visual
256
+ Asset type: Slack-shareable and deck-ready business artifact
257
+ Artifact type: ${artifactType}
258
+ Primary request: ${userPrompt}
259
+ ${contextBlock}
260
+ Design requirements:
261
+ - Create a polished, modern SaaS-style visual on a clean light background.
262
+ - Use business-appropriate typography, generous spacing, and a restrained palette.
263
+ - Make the visual useful at Slack preview size: large labels, short text, no tiny paragraphs.
264
+ - Prefer a clear structure: flow, comparison, architecture diagram, metric story, or status map depending on the request.
265
+ - If rendering text, keep it concise and accurate; do not invent unsupported names, numbers, claims, or logos.
266
+ - Do not use real third-party logos unless the user explicitly asks.
267
+ - Avoid decorative stock-art scenes. The output should feel like a usable work artifact.
268
+ - Include enough visual hierarchy that a busy operator can understand it in 5 seconds.
269
+ `;
270
+ }
271
+
272
+ async function resolveAgentId(token, explicitAgentId) {
273
+ if (explicitAgentId) return { id: explicitAgentId, label: explicitAgentId };
274
+
275
+ const agentsResult = await apiRequestJson('/agent/my-agents', { method: 'GET', token });
276
+ const agents = agentsResult.data?.my_agents || agentsResult.data?.agents || [];
277
+ const activeAgents = agents.filter(agent => agent.status !== 'inactive' && agent.id);
278
+ const agentById = new Map(activeAgents.map(agent => [agent.id, agent]));
279
+ const fromAccessible = (agentId, fallbackLabel) => {
280
+ if (!agentId || !agentById.has(agentId)) return null;
281
+ const agent = agentById.get(agentId);
282
+ return { id: agent.id, label: agent.name || fallbackLabel || agent.id };
283
+ };
284
+
285
+ const config = loadConfig();
286
+ const configAgent = fromAccessible(config.agent_id, config.agent_name);
287
+ if (configAgent) return configAgent;
288
+
289
+ const business = readBusinessMeta();
290
+ const localBusinessAgent = fromAccessible(business.agent_id, business.agent_name);
291
+ if (localBusinessAgent) return localBusinessAgent;
292
+
293
+ if (business.slug) {
294
+ const list = await apiRequestJson('/business/', { method: 'GET', token });
295
+ const businesses = Array.isArray(list.data) ? list.data : [];
296
+ const match = businesses.find(b => b.slug === business.slug || b.name === business.name);
297
+ const agentId = match?.agent_id || match?.default_agent_id || match?.agent?.id;
298
+ const businessAgent = fromAccessible(agentId, match?.agent_name || match?.agent?.name);
299
+ if (businessAgent) return businessAgent;
300
+ }
301
+
302
+ if (activeAgents.length === 1) return { id: activeAgents[0].id, label: activeAgents[0].name || activeAgents[0].id };
303
+ if (activeAgents.length > 1) return { id: activeAgents[0].id, label: activeAgents[0].name || activeAgents[0].id };
304
+
305
+ throw new Error('No agent found. Run "atris agent" or pass --agent <agent_id>.');
306
+ }
307
+
308
+ function writeImageFile(base64Image, outputPath) {
309
+ const clean = String(base64Image || '').replace(/^data:image\/[a-zA-Z0-9.+-]+;base64,/, '');
310
+ if (!clean) throw new Error('Backend returned no image data.');
311
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
312
+ fs.writeFileSync(outputPath, Buffer.from(clean, 'base64'));
313
+ }
314
+
315
+ function maybeOpenImage(outputPath) {
316
+ if (process.platform === 'darwin') spawnSync('open', [outputPath], { stdio: 'ignore' });
317
+ else if (process.platform === 'win32') spawnSync('cmd', ['/c', 'start', '', outputPath], { stdio: 'ignore' });
318
+ else spawnSync('xdg-open', [outputPath], { stdio: 'ignore' });
319
+ }
320
+
321
+ async function generateVisual(prompt, options = {}) {
322
+ const outputPath = resolveOutputPath(options.out, prompt);
323
+ const imagePrompt = buildImagePrompt(prompt, options);
324
+
325
+ if (options.dryRun) {
326
+ console.log('Atris Visualize dry run');
327
+ console.log(`Model: ${options.model}`);
328
+ console.log(`Size: ${options.size}`);
329
+ console.log(`Output: ${outputPath}`);
330
+ console.log('');
331
+ console.log(imagePrompt.trim());
332
+ return { outputPath, imagePrompt, dryRun: true };
333
+ }
334
+
335
+ const ensured = await ensureValidCredentials(apiRequestJson);
336
+ const creds = ensured.error ? null : ensured.credentials;
337
+ if (!creds?.token) {
338
+ const detail = ensured.detail || ensured.error;
339
+ throw new Error(detail ? `Authentication failed: ${detail}. Run "atris login".` : 'Not logged in. Run "atris login".');
340
+ }
341
+
342
+ const agent = await resolveAgentId(creds.token, options.agentId);
343
+ console.log(`Generating visual with ${options.model} via agent ${agent.label}...`);
344
+
345
+ const result = await apiRequestJson(`/agent/${agent.id}/image/generate`, {
346
+ method: 'POST',
347
+ token: creds.token,
348
+ timeoutMs: options.timeoutMs,
349
+ body: {
350
+ prompt: imagePrompt,
351
+ n: 1,
352
+ size: options.size,
353
+ model: options.model,
354
+ quality: options.quality,
355
+ output_format: options.outputFormat,
356
+ },
357
+ });
358
+
359
+ if (!result.ok) {
360
+ throw new Error(`Image generation failed (${result.status}): ${result.error || result.text || 'unknown error'}`);
361
+ }
362
+
363
+ const image = result.data?.images?.[0];
364
+ writeImageFile(image, outputPath);
365
+ console.log(`Saved: ${outputPath}`);
366
+ if (options.open) maybeOpenImage(outputPath);
367
+ return { outputPath, imagePrompt, model: result.data?.model_used || options.model };
368
+ }
369
+
370
+ async function visualizeAtris(args = process.argv.slice(3)) {
371
+ const { prompt, options } = parseVisualizeArgs(args);
372
+ if (options.help) {
373
+ showVisualizeHelp();
374
+ return;
375
+ }
376
+ if (!prompt) {
377
+ legacyVisualizeInbox();
378
+ return;
379
+ }
380
+ await generateVisual(prompt, options);
381
+ }
71
382
 
72
383
  module.exports = {
73
- visualizeAtris
384
+ visualizeAtris,
385
+ parseVisualizeArgs,
386
+ buildImagePrompt,
387
+ classifyArtifact,
388
+ resolveOutputPath,
389
+ generateVisual,
74
390
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atris",
3
- "version": "3.12.1",
3
+ "version": "3.13.0",
4
4
  "description": "Atris — an operating system for intelligence. Integrates with any agent.",
5
5
  "main": "bin/atris.js",
6
6
  "bin": {