atris 3.26.0 → 3.28.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/AGENTS.md +1 -1
- package/README.md +7 -5
- package/atris/features/company-brain-sync/build.md +4 -4
- package/atris/features/company-brain-sync/idea.md +2 -2
- package/atris/features/company-brain-sync/validate.md +19 -19
- package/atris/skills/aeo/SKILL.md +1 -1
- package/atris/skills/autopilot/SKILL.md +2 -2
- package/atris/skills/slides/SKILL.md +2 -2
- package/bin/atris.js +32 -14
- package/commands/activate.js +24 -0
- package/commands/aeo.js +1 -1
- package/commands/autopilot.js +1 -1
- package/commands/clarity.js +125 -0
- package/commands/computer.js +9 -9
- package/commands/feedback.js +1 -1
- package/commands/init.js +2 -2
- package/commands/live.js +3 -3
- package/commands/moves.js +156 -0
- package/commands/plugin.js +1 -1
- package/commands/pull.js +4 -4
- package/commands/security-review.js +132 -0
- package/commands/signup.js +101 -0
- package/commands/task.js +0 -1
- package/lib/clarity.js +97 -0
- package/lib/next-moves.js +362 -0
- package/lib/security-scan.js +188 -0
- package/package.json +1 -8
- package/utils/update-check.js +77 -24
- package/atris/wiki/concepts/agent-activation-contract.md +0 -81
- package/atris/wiki/concepts/workspace-initialization-contract.md +0 -73
- package/atris/wiki/index.md +0 -31
- package/atris/wiki/sources/atris-labs-2026-05-10.txt +0 -14
- package/atris/wiki/sources/atris-labs-goals-2026-05-10.txt +0 -14
- package/atris/wiki/sources/atrisos-generative-ui-product-surface-2026-05-10.txt +0 -10
- package/atris/wiki/sources/jack-dorsey-2026-05-10.txt +0 -12
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// `atris moves`: alive onboarding. Shows the 3 highest-leverage next moves and
|
|
4
|
+
// lets you approve (seed it into the loop), kill (stop suggesting it), or skip.
|
|
5
|
+
// This is the seed of proactiveness: the workspace proposes, you steer.
|
|
6
|
+
|
|
7
|
+
const readline = require('readline');
|
|
8
|
+
const {
|
|
9
|
+
nextMoves,
|
|
10
|
+
recordDecision,
|
|
11
|
+
seedInboxFromMove,
|
|
12
|
+
} = require('../lib/next-moves');
|
|
13
|
+
|
|
14
|
+
function currentMoves(root, limit) {
|
|
15
|
+
return nextMoves(root, limit);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function renderMoves(moves) {
|
|
19
|
+
if (!moves.length) {
|
|
20
|
+
return [
|
|
21
|
+
'',
|
|
22
|
+
'your next moves',
|
|
23
|
+
'',
|
|
24
|
+
' nothing queued. add an item to ROADMAP.md under "## Open loop items",',
|
|
25
|
+
' or jot an idea with `atris log`.',
|
|
26
|
+
'',
|
|
27
|
+
].join('\n');
|
|
28
|
+
}
|
|
29
|
+
const lines = ['', 'your next moves', ''];
|
|
30
|
+
moves.forEach((m, i) => {
|
|
31
|
+
lines.push(` ${i + 1}. ${m.title}`);
|
|
32
|
+
lines.push(` why: ${m.why} id: ${m.id}`);
|
|
33
|
+
lines.push('');
|
|
34
|
+
});
|
|
35
|
+
lines.push(' approve: atris moves --approve <id|N> seed it into the loop');
|
|
36
|
+
lines.push(' kill: atris moves --kill <id|N> stop suggesting it');
|
|
37
|
+
lines.push(' (use the id when you act later; the numbered order can shift)');
|
|
38
|
+
lines.push('');
|
|
39
|
+
return lines.join('\n');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseIndexes(value) {
|
|
43
|
+
return String(value || '')
|
|
44
|
+
.split(/[\s,]+/)
|
|
45
|
+
.map((s) => parseInt(s, 10))
|
|
46
|
+
.filter((n) => Number.isInteger(n) && n >= 1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Resolve --approve/--kill tokens to move objects. A token is either a stable
|
|
50
|
+
// move id (m_...) or a 1-based position. The id is preferred because the list
|
|
51
|
+
// can re-rank between viewing and acting, so a bare index can hit a different
|
|
52
|
+
// move than the one you saw.
|
|
53
|
+
function resolveSelection(moves, value) {
|
|
54
|
+
const tokens = String(value || '').split(/[\s,]+/).filter(Boolean);
|
|
55
|
+
const byId = new Map(moves.map((m) => [m.id, m]));
|
|
56
|
+
const seen = new Set();
|
|
57
|
+
const out = [];
|
|
58
|
+
for (const tok of tokens) {
|
|
59
|
+
let move = null;
|
|
60
|
+
if (byId.has(tok)) move = byId.get(tok);
|
|
61
|
+
else if (/^\d+$/.test(tok)) move = moves[parseInt(tok, 10) - 1];
|
|
62
|
+
if (move && !seen.has(move.id)) { seen.add(move.id); out.push(move); }
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function applyDecision(root, selectedMoves, decision, stamp) {
|
|
68
|
+
const { claimRoadmapItem } = require('../lib/next-moves');
|
|
69
|
+
const acted = [];
|
|
70
|
+
for (const move of selectedMoves) {
|
|
71
|
+
recordDecision(root, move, decision, stamp);
|
|
72
|
+
if (decision === 'approve') {
|
|
73
|
+
// Seed then claim, mirroring the loop. For a roadmap-sourced move, mark it
|
|
74
|
+
// claimed in ROADMAP so the loop and the moves list agree it is handled.
|
|
75
|
+
const seeded = seedInboxFromMove(root, move);
|
|
76
|
+
if (move.source === 'roadmap') claimRoadmapItem(root, move.title);
|
|
77
|
+
acted.push({ move, seeded });
|
|
78
|
+
} else {
|
|
79
|
+
acted.push({ move });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return acted;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function readArgValue(args, name) {
|
|
86
|
+
const i = args.indexOf(name);
|
|
87
|
+
if (i === -1) return null;
|
|
88
|
+
return args[i + 1] || '';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function movesCommand(args = [], root = process.cwd()) {
|
|
92
|
+
if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
|
|
93
|
+
console.log('');
|
|
94
|
+
console.log('Usage: atris moves [--approve <id|N>] [--kill <id|N>] [--json] [--limit N]');
|
|
95
|
+
console.log('');
|
|
96
|
+
console.log('Show the next moves and steer them. Each move has a stable id; prefer it');
|
|
97
|
+
console.log('over the position number, which can shift between viewing and acting.');
|
|
98
|
+
console.log('');
|
|
99
|
+
console.log(' atris moves Show the 3 next moves (prompts on a terminal)');
|
|
100
|
+
console.log(' atris moves --approve <id|N> Seed that move into the loop (writes the inbox)');
|
|
101
|
+
console.log(' atris moves --kill <id|N> Stop suggesting that move');
|
|
102
|
+
console.log(' atris moves --json Print the moves (with ids) as JSON, no prompt');
|
|
103
|
+
console.log(' atris moves --limit N Show N moves (default 3)');
|
|
104
|
+
console.log('');
|
|
105
|
+
return 0;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const limitArg = readArgValue(args, '--limit');
|
|
109
|
+
const limit = limitArg && !Number.isNaN(parseInt(limitArg, 10)) ? Math.max(1, parseInt(limitArg, 10)) : 3;
|
|
110
|
+
const stamp = new Date().toISOString();
|
|
111
|
+
|
|
112
|
+
const approveVal = readArgValue(args, '--approve');
|
|
113
|
+
const killVal = readArgValue(args, '--kill');
|
|
114
|
+
|
|
115
|
+
if (approveVal !== null || killVal !== null) {
|
|
116
|
+
const moves = currentMoves(root, limit);
|
|
117
|
+
const killed = applyDecision(root, resolveSelection(moves, killVal), 'kill', stamp);
|
|
118
|
+
const approved = applyDecision(root, resolveSelection(moves, approveVal), 'approve', stamp);
|
|
119
|
+
for (const a of approved) {
|
|
120
|
+
const note = a.seeded && a.seeded.alreadyPresent ? 'already in the inbox' : 'seeded into the loop';
|
|
121
|
+
console.log(`approved: ${a.move.title} -> ${note}`);
|
|
122
|
+
}
|
|
123
|
+
for (const k of killed) console.log(`killed: ${k.move.title} -> will not suggest again`);
|
|
124
|
+
if (!approved.length && !killed.length) console.log('no matching move for that id or number.');
|
|
125
|
+
return 0;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const moves = currentMoves(root, limit);
|
|
129
|
+
|
|
130
|
+
if (args.includes('--json')) {
|
|
131
|
+
console.log(JSON.stringify({ moves }, null, 2));
|
|
132
|
+
return 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
console.log(renderMoves(moves));
|
|
136
|
+
|
|
137
|
+
// Only prompt on a real terminal, so spawned/non-interactive runs never hang.
|
|
138
|
+
if (!process.stdin.isTTY || !moves.length) return 0;
|
|
139
|
+
|
|
140
|
+
const answer = await new Promise((resolve) => {
|
|
141
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
142
|
+
rl.question('approve N, kill kN, or enter to skip: ', (a) => { rl.close(); resolve(a.trim()); });
|
|
143
|
+
});
|
|
144
|
+
if (!answer) return 0;
|
|
145
|
+
|
|
146
|
+
const killTokens = (answer.match(/k\s*\d+/gi) || []).map((t) => t.replace(/k/i, '').trim());
|
|
147
|
+
const approveTokens = answer.replace(/k\s*\d+/gi, '').trim();
|
|
148
|
+
|
|
149
|
+
const killed = applyDecision(root, resolveSelection(moves, killTokens.join(' ')), 'kill', stamp);
|
|
150
|
+
const approved = applyDecision(root, resolveSelection(moves, approveTokens), 'approve', stamp);
|
|
151
|
+
for (const a of approved) console.log(`approved: ${a.move.title} -> seeded into the loop`);
|
|
152
|
+
for (const k of killed) console.log(`killed: ${k.move.title}`);
|
|
153
|
+
return 0;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
module.exports = { movesCommand, renderMoves, currentMoves, parseIndexes, resolveSelection };
|
package/commands/plugin.js
CHANGED
|
@@ -46,7 +46,7 @@ function generateManifest(skills, projectDir) {
|
|
|
46
46
|
version: pkg.version || '1.0.0',
|
|
47
47
|
description: `Atris workspace skills for Cowork — ${skills.length} integrations (email, calendar, slack, drive, notion, and more)`,
|
|
48
48
|
author: {
|
|
49
|
-
name: typeof pkg.author === 'string' ? pkg.author : (pkg.author?.name || 'Atris
|
|
49
|
+
name: typeof pkg.author === 'string' ? pkg.author : (pkg.author?.name || 'Atris')
|
|
50
50
|
},
|
|
51
51
|
homepage: pkg.homepage || 'https://github.com/atrislabs/atris',
|
|
52
52
|
keywords: ['atris', 'workspace', 'integrations', 'email', 'calendar', 'slack', 'notion', 'drive']
|
package/commands/pull.js
CHANGED
|
@@ -110,9 +110,9 @@ async function pullAtris() {
|
|
|
110
110
|
console.log(' Local files that conflict with cloud are replaced by the cloud version.');
|
|
111
111
|
console.log('');
|
|
112
112
|
console.log(' atris pull Pull into current business workspace');
|
|
113
|
-
console.log(' atris pull
|
|
114
|
-
console.log(' atris pull
|
|
115
|
-
console.log(' atris pull
|
|
113
|
+
console.log(' atris pull <business> Pull a business into ./<business> or --into <path>');
|
|
114
|
+
console.log(' atris pull <business> --into /tmp/<business>');
|
|
115
|
+
console.log(' atris pull <business> --only atris/wiki/');
|
|
116
116
|
console.log(' atris pull --keep-local Preserve conflicting local edits as .remote files (legacy)');
|
|
117
117
|
console.log(' atris pull --dry-run Preview pull changes without writing local files');
|
|
118
118
|
console.log(' atris pull --no-manifest Pull for inspection without changing this machine\'s sync anchor');
|
|
@@ -249,7 +249,7 @@ async function pullBusiness(slug) {
|
|
|
249
249
|
// correct workspace for THIS business — i.e. it has a `.atris/business.json`
|
|
250
250
|
// whose slug matches `slug`. Any other signal (a stray `atris/` folder, a
|
|
251
251
|
// business.json for a different business, etc.) is NOT enough: pulling
|
|
252
|
-
//
|
|
252
|
+
// one business on top of another business workspace would mix two businesses into
|
|
253
253
|
// one directory and write one manifest over the other (or vice
|
|
254
254
|
// versa), causing the next sync to do strange things.
|
|
255
255
|
//
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// atris security-review — deterministic secrets / PII / privacy scan (no LLM).
|
|
2
|
+
//
|
|
3
|
+
// "Is this workspace safe to commit, publish, or hand to an autonomous loop?"
|
|
4
|
+
// Answers it with facts: file:line + rule + severity. Exit 1 on a HIGH finding,
|
|
5
|
+
// so it drops into the autopilot/mission/CI verification gate. `--json` emits a
|
|
6
|
+
// SOC 2 evidence artifact. Scans git-tracked files by default (what is exposed
|
|
7
|
+
// is what is committed), or a path you pass.
|
|
8
|
+
//
|
|
9
|
+
// Usage:
|
|
10
|
+
// atris security-review scan tracked files (default)
|
|
11
|
+
// atris security-review src/ scan a path
|
|
12
|
+
// atris security-review --staged scan staged changes (pre-commit gate)
|
|
13
|
+
// atris security-review --json machine output for CI / the loop
|
|
14
|
+
// atris security-review --strict also fail on MEDIUM (PII/paths)
|
|
15
|
+
// atris security-review hook install a pre-commit gate
|
|
16
|
+
//
|
|
17
|
+
// Exit code: 0 = clean, 1 = findings at/over the fail threshold, 2 = bad usage.
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
const { runScan, RULES } = require('../lib/security-scan');
|
|
22
|
+
|
|
23
|
+
const ICON = { high: '✗', medium: '!', low: '·', privacy: '✗', secret: '✗', pii: '!' };
|
|
24
|
+
const SEV_ORDER = { high: 0, medium: 1, low: 2 };
|
|
25
|
+
|
|
26
|
+
function securityReviewCommand(argv = []) {
|
|
27
|
+
const sub = argv[0];
|
|
28
|
+
if (sub === 'help' || argv.includes('-h') || argv.includes('--help')) return printHelp();
|
|
29
|
+
if (sub === 'rules') return printRules();
|
|
30
|
+
if (sub === 'hook' || sub === 'install-hook') return installHook();
|
|
31
|
+
|
|
32
|
+
const json = argv.includes('--json');
|
|
33
|
+
const quiet = argv.includes('--quiet');
|
|
34
|
+
const strict = argv.includes('--strict');
|
|
35
|
+
const staged = argv.includes('--staged');
|
|
36
|
+
const paths = argv.filter((a) => !a.startsWith('-') && a !== 'scan');
|
|
37
|
+
const root = process.cwd();
|
|
38
|
+
|
|
39
|
+
let result;
|
|
40
|
+
try {
|
|
41
|
+
result = runScan({ root, paths, staged });
|
|
42
|
+
} catch (e) {
|
|
43
|
+
console.error(`security-review: ${e.message}`);
|
|
44
|
+
return 2;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const { findings, counts, scanned } = result;
|
|
48
|
+
findings.sort((a, b) => (SEV_ORDER[a.sev] - SEV_ORDER[b.sev]) || a.file.localeCompare(b.file) || (a.line - b.line));
|
|
49
|
+
const failThreshold = strict ? ['high', 'medium'] : ['high'];
|
|
50
|
+
const failing = findings.filter((f) => failThreshold.includes(f.sev)).length;
|
|
51
|
+
|
|
52
|
+
if (json) {
|
|
53
|
+
console.log(JSON.stringify({
|
|
54
|
+
ok: failing === 0,
|
|
55
|
+
scanned,
|
|
56
|
+
counts,
|
|
57
|
+
fail_threshold: strict ? 'medium' : 'high',
|
|
58
|
+
findings,
|
|
59
|
+
generated_for: 'soc2-evidence',
|
|
60
|
+
}, null, 2));
|
|
61
|
+
return failing ? 1 : 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!quiet) {
|
|
65
|
+
console.log('\n ◉ atris security review');
|
|
66
|
+
if (!findings.length) {
|
|
67
|
+
console.log(`\n ✓ clean — no secrets, PII, or sensitive files in ${scanned} tracked file${scanned === 1 ? '' : 's'}\n`);
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
console.log('');
|
|
71
|
+
const w = Math.max(...findings.map((f) => `${f.file}${f.line ? ':' + f.line : ''}`.length));
|
|
72
|
+
for (const f of findings) {
|
|
73
|
+
const loc = `${f.file}${f.line ? ':' + f.line : ''}`.padEnd(w);
|
|
74
|
+
console.log(` ${ICON[f.sev] || '·'} ${f.sev.toUpperCase().padEnd(6)} ${loc} ${f.rule.padEnd(22)} ${f.why}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
console.log(`\n ${counts.high || 0} high · ${counts.medium || 0} medium · ${counts.low || 0} low across ${scanned} file${scanned === 1 ? '' : 's'}`);
|
|
78
|
+
if (failing) {
|
|
79
|
+
console.log(` ${failing} finding${failing === 1 ? '' : 's'} at/over the ${strict ? 'MEDIUM' : 'HIGH'} threshold · exit 1`);
|
|
80
|
+
console.log(' fix or suppress (trailing `atris-allow-secret`), then re-run.\n');
|
|
81
|
+
} else {
|
|
82
|
+
console.log(` no findings at the ${strict ? 'MEDIUM' : 'HIGH'} threshold · exit 0\n`);
|
|
83
|
+
}
|
|
84
|
+
return failing ? 1 : 0;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function printRules() {
|
|
88
|
+
console.log('\n atris security-review — deterministic rules:\n');
|
|
89
|
+
for (const r of RULES) console.log(` ${r.sev.toUpperCase().padEnd(6)} ${r.cat.padEnd(8)} ${r.id.padEnd(22)} ${r.why}`);
|
|
90
|
+
console.log(`\n ${RULES.length} rules + tracked-sensitive-file check. Suppress a line with a trailing \`atris-allow-secret\`.\n`);
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function installHook() {
|
|
95
|
+
const root = process.cwd();
|
|
96
|
+
const hookDir = path.join(root, '.git', 'hooks');
|
|
97
|
+
try {
|
|
98
|
+
fs.mkdirSync(hookDir, { recursive: true });
|
|
99
|
+
const hookPath = path.join(hookDir, 'pre-commit');
|
|
100
|
+
const marker = '# atris security gate';
|
|
101
|
+
let content = '';
|
|
102
|
+
try { content = fs.readFileSync(hookPath, 'utf8'); } catch {}
|
|
103
|
+
if (content.includes(marker)) { console.log(`\n already installed: ${path.relative(root, hookPath)}\n`); return 0; }
|
|
104
|
+
if (!content) content = '#!/bin/sh\n';
|
|
105
|
+
if (!content.endsWith('\n')) content += '\n';
|
|
106
|
+
content += `\n${marker}\nif command -v atris >/dev/null 2>&1; then atris security-review --staged --quiet || exit 1; fi\n`;
|
|
107
|
+
fs.writeFileSync(hookPath, content);
|
|
108
|
+
fs.chmodSync(hookPath, 0o755);
|
|
109
|
+
console.log(`\n ✓ security pre-commit gate installed: ${path.relative(root, hookPath)}\n every commit now runs: atris security-review --staged\n`);
|
|
110
|
+
return 0;
|
|
111
|
+
} catch (e) { console.error(` ${e.message}`); return 2; }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function printHelp() {
|
|
115
|
+
console.log(`
|
|
116
|
+
atris security-review — deterministic secrets / PII / privacy scan (no LLM)
|
|
117
|
+
|
|
118
|
+
atris security-review scan git-tracked files (default)
|
|
119
|
+
atris security-review <path> scan a file or dir
|
|
120
|
+
atris security-review --staged scan staged changes (pre-commit gate)
|
|
121
|
+
atris security-review --strict also fail on MEDIUM (PII / personal paths)
|
|
122
|
+
atris security-review --json machine output / SOC 2 evidence artifact
|
|
123
|
+
atris security-review rules list the active detectors
|
|
124
|
+
atris security-review hook install a pre-commit gate
|
|
125
|
+
|
|
126
|
+
Scans for hardcoded secrets, API keys, personal data, and tracked sensitive
|
|
127
|
+
files. exit 0 = clean, 1 = found. Wire into the autopilot/mission gate and CI.
|
|
128
|
+
`);
|
|
129
|
+
return 0;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
module.exports = { securityReviewCommand };
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// atris signup <handle>
|
|
2
|
+
//
|
|
3
|
+
// One-call seedless agent signup. A brand-new agent with no account hits
|
|
4
|
+
// POST /api/auth/agent/signup (unauthenticated) and gets back a real, INERT
|
|
5
|
+
// Atris identity + token, then we write the active profile so `atris play`
|
|
6
|
+
// works on the very next command. This closes the install -> signup -> play
|
|
7
|
+
// seam: `npm i -g atris && atris signup x && atris play`.
|
|
8
|
+
//
|
|
9
|
+
// The account is born inert by design (0 credits, no executing agent, external
|
|
10
|
+
// mail paid-gated) — identity is free, capability is earned via AgentXP.
|
|
11
|
+
// Backend: backend/routers/agent_auth_router.py (POST /auth/agent/signup).
|
|
12
|
+
|
|
13
|
+
const crypto = require('crypto');
|
|
14
|
+
const { apiRequestJson, getApiBaseUrl } = require('../utils/api');
|
|
15
|
+
const { saveCredentials } = require('../utils/auth');
|
|
16
|
+
|
|
17
|
+
// Mirror the server rule exactly (^[a-z0-9]{3,30}$) so we fail fast and friendly
|
|
18
|
+
// before spending a network round trip / a rate-limit slot.
|
|
19
|
+
const HANDLE_RE = /^[a-z0-9]{3,30}$/;
|
|
20
|
+
|
|
21
|
+
// Proof-of-work — mirror the server (agent_auth_router.py). The signup endpoint
|
|
22
|
+
// requires a nonce whose sha256(prefix:handle:bucket:nonce) has DIFFICULTY_BITS
|
|
23
|
+
// leading zero bits. Bucket = current 5-min window. Solved locally (~1s) so
|
|
24
|
+
// signup stays a single call; this is the cost that makes mass-minting expensive.
|
|
25
|
+
const POW_PREFIX = 'atris-signup-v1';
|
|
26
|
+
const POW_WINDOW_S = 300;
|
|
27
|
+
const POW_DIFFICULTY_BITS = 20;
|
|
28
|
+
|
|
29
|
+
function solvePow(handle) {
|
|
30
|
+
const bucket = Math.floor(Date.now() / 1000 / POW_WINDOW_S);
|
|
31
|
+
const fullBytes = Math.floor(POW_DIFFICULTY_BITS / 8);
|
|
32
|
+
const remBits = POW_DIFFICULTY_BITS % 8;
|
|
33
|
+
for (let n = 0; ; n++) {
|
|
34
|
+
const d = crypto.createHash('sha256').update(`${POW_PREFIX}:${handle}:${bucket}:${n}`).digest();
|
|
35
|
+
let ok = true;
|
|
36
|
+
for (let i = 0; i < fullBytes; i++) { if (d[i] !== 0) { ok = false; break; } }
|
|
37
|
+
if (ok && remBits && (d[fullBytes] >> (8 - remBits)) !== 0) ok = false;
|
|
38
|
+
if (ok) return String(n);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseHandle(args = []) {
|
|
43
|
+
const positional = args.find((a) => a && !a.startsWith('-'));
|
|
44
|
+
return (positional || '').trim().toLowerCase();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function signupCommand(args = []) {
|
|
48
|
+
const handle = parseHandle(args);
|
|
49
|
+
|
|
50
|
+
if (!handle) {
|
|
51
|
+
console.error('Usage: atris signup <handle>');
|
|
52
|
+
console.error(' handle: 3–30 characters, lowercase letters and digits only.');
|
|
53
|
+
return 1;
|
|
54
|
+
}
|
|
55
|
+
if (!HANDLE_RE.test(handle)) {
|
|
56
|
+
console.error(`✗ Invalid handle "${handle}".`);
|
|
57
|
+
console.error(' Must be 3–30 characters, lowercase letters and digits (a–z, 0–9). No spaces or symbols.');
|
|
58
|
+
return 1;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
console.log(`Claiming @${handle} … (solving proof-of-work)`);
|
|
62
|
+
const pow = solvePow(handle);
|
|
63
|
+
|
|
64
|
+
const res = await apiRequestJson('/auth/agent/signup', {
|
|
65
|
+
method: 'POST',
|
|
66
|
+
body: { handle, pow },
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
if (res.ok && res.data && res.data.token) {
|
|
70
|
+
const { token, email, user_id: userId } = res.data;
|
|
71
|
+
const identity = email || `${handle}@atrismail.com`;
|
|
72
|
+
saveCredentials(token, null, identity, userId || null, 'atrisos');
|
|
73
|
+
console.log(`\n✓ You're in — ${identity}`);
|
|
74
|
+
console.log(' Inert starter account (0 credits): identity is free, capability is earned.');
|
|
75
|
+
console.log(' Saved to your active profile.');
|
|
76
|
+
console.log('\nNext:');
|
|
77
|
+
console.log(' atris play # claim a starter mission and earn your first proof-backed rep');
|
|
78
|
+
console.log(' atris xp # see where you stand on the board');
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Friendly, actionable errors — no stack traces for expected cases.
|
|
83
|
+
if (res.status === 409) {
|
|
84
|
+
console.error(`✗ "${handle}" is already taken or reserved. Try a different handle.`);
|
|
85
|
+
return 1;
|
|
86
|
+
}
|
|
87
|
+
if (res.status === 429) {
|
|
88
|
+
console.error('✗ Too many signups right now. Wait a minute and try again.');
|
|
89
|
+
return 1;
|
|
90
|
+
}
|
|
91
|
+
if (res.status === 404) {
|
|
92
|
+
console.error('✗ Seedless signup isn’t available on this backend yet.');
|
|
93
|
+
console.error(' Use `atris login` for now, or try again after the next deploy.');
|
|
94
|
+
return 1;
|
|
95
|
+
}
|
|
96
|
+
console.error(`✗ Signup failed: ${res.error || 'unknown error'} (status ${res.status}).`);
|
|
97
|
+
console.error(` API: ${getApiBaseUrl()}`);
|
|
98
|
+
return 1;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = { signupCommand, parseHandle, HANDLE_RE };
|
package/commands/task.js
CHANGED
|
@@ -616,7 +616,6 @@ function readGoalSources(root = process.cwd()) {
|
|
|
616
616
|
const candidates = [
|
|
617
617
|
path.join(root, 'atris', 'goals.md'),
|
|
618
618
|
path.join(root, 'goals.md'),
|
|
619
|
-
path.join(root, 'atris', 'wiki', 'concepts', 'atris-labs-goals.md'),
|
|
620
619
|
];
|
|
621
620
|
for (const file of candidates) {
|
|
622
621
|
if (!fs.existsSync(file)) continue;
|
package/lib/clarity.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// The clarity interview. The front door of the product: draw out how the human
|
|
4
|
+
// works, write it down once, and let every agent read it so they prompt
|
|
5
|
+
// themselves well. A small, high-signal profile, not a survey.
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
// One or two ideas at a time, plain English. Each answer is free text; the
|
|
11
|
+
// parenthetical is a nudge, not a fixed menu.
|
|
12
|
+
const QUESTIONS = [
|
|
13
|
+
{ key: 'focus', q: 'what are you building, and who is it for?' },
|
|
14
|
+
{ key: 'voice', q: 'how should output sound? (plain, terse, warm, formal)' },
|
|
15
|
+
{ key: 'cadence', q: 'how do you like to work? (one idea at a time, batch, overnight autonomous)' },
|
|
16
|
+
{ key: 'done', q: 'what does "done" mean to you? (tests green, shipped, you reviewed it)' },
|
|
17
|
+
{ key: 'leash', q: 'how much should agents do without asking? (ask first, proceed and report, full auto)' },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const KEYS = QUESTIONS.map((x) => x.key);
|
|
21
|
+
|
|
22
|
+
function isEmptyProfile(profile) {
|
|
23
|
+
return !profile || !KEYS.some((k) => profile[k]);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function renderClarityMd(profile = {}) {
|
|
27
|
+
const labels = {
|
|
28
|
+
focus: 'Focus',
|
|
29
|
+
voice: 'Voice',
|
|
30
|
+
cadence: 'Cadence',
|
|
31
|
+
done: 'Done means',
|
|
32
|
+
leash: 'Leash',
|
|
33
|
+
};
|
|
34
|
+
const lines = [
|
|
35
|
+
'# Clarity profile',
|
|
36
|
+
'',
|
|
37
|
+
'How the operator works. Agents read this to prompt themselves well,',
|
|
38
|
+
'so the human does not have to repeat themselves.',
|
|
39
|
+
'',
|
|
40
|
+
];
|
|
41
|
+
for (const key of KEYS) {
|
|
42
|
+
if (profile[key]) lines.push(`- ${labels[key]}: ${profile[key]}`);
|
|
43
|
+
}
|
|
44
|
+
if (isEmptyProfile(profile)) {
|
|
45
|
+
lines.push('- (not set yet, run `atris clarity` to fill this in)');
|
|
46
|
+
}
|
|
47
|
+
lines.push('');
|
|
48
|
+
if (profile.updated_at) lines.push(`Updated: ${profile.updated_at}`);
|
|
49
|
+
lines.push('');
|
|
50
|
+
return lines.join('\n');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function profilePaths(root = process.cwd()) {
|
|
54
|
+
return {
|
|
55
|
+
json: path.join(root, '.atris', 'clarity.json'),
|
|
56
|
+
md: path.join(root, 'atris', 'CLARITY.md'),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function readProfile(root = process.cwd()) {
|
|
61
|
+
const { json } = profilePaths(root);
|
|
62
|
+
try {
|
|
63
|
+
return JSON.parse(fs.readFileSync(json, 'utf8'));
|
|
64
|
+
} catch {
|
|
65
|
+
return {};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function writeProfile(root, profile) {
|
|
70
|
+
const { json, md } = profilePaths(root);
|
|
71
|
+
fs.mkdirSync(path.dirname(json), { recursive: true });
|
|
72
|
+
fs.mkdirSync(path.dirname(md), { recursive: true });
|
|
73
|
+
fs.writeFileSync(json, `${JSON.stringify(profile, null, 2)}\n`, 'utf8');
|
|
74
|
+
fs.writeFileSync(md, renderClarityMd(profile), 'utf8');
|
|
75
|
+
return { json, md };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Merge new answers over an existing profile (so --set is incremental).
|
|
79
|
+
function mergeProfile(existing, answers, stamp) {
|
|
80
|
+
const merged = { ...existing };
|
|
81
|
+
for (const key of KEYS) {
|
|
82
|
+
if (typeof answers[key] === 'string' && answers[key].trim()) merged[key] = answers[key].trim();
|
|
83
|
+
}
|
|
84
|
+
merged.updated_at = stamp || merged.updated_at || null;
|
|
85
|
+
return merged;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = {
|
|
89
|
+
QUESTIONS,
|
|
90
|
+
KEYS,
|
|
91
|
+
mergeProfile,
|
|
92
|
+
isEmptyProfile,
|
|
93
|
+
renderClarityMd,
|
|
94
|
+
profilePaths,
|
|
95
|
+
readProfile,
|
|
96
|
+
writeProfile,
|
|
97
|
+
};
|