atris 2.5.2 → 2.5.4

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 (50) hide show
  1. package/README.md +10 -0
  2. package/atris/experiments/README.md +118 -0
  3. package/atris/experiments/_examples/smoke-keep-revert/README.md +45 -0
  4. package/atris/experiments/_examples/smoke-keep-revert/candidate.py +8 -0
  5. package/atris/experiments/_examples/smoke-keep-revert/loop.py +129 -0
  6. package/atris/experiments/_examples/smoke-keep-revert/measure.py +47 -0
  7. package/atris/experiments/_examples/smoke-keep-revert/program.md +3 -0
  8. package/atris/experiments/_examples/smoke-keep-revert/proposals/bad_patch.py +19 -0
  9. package/atris/experiments/_examples/smoke-keep-revert/proposals/fix_patch.py +22 -0
  10. package/atris/experiments/_examples/smoke-keep-revert/reset.py +21 -0
  11. package/atris/experiments/_examples/smoke-keep-revert/results.tsv +5 -0
  12. package/atris/experiments/_examples/smoke-keep-revert/visual.svg +52 -0
  13. package/atris/experiments/_fixtures/invalid/BadName/loop.py +1 -0
  14. package/atris/experiments/_fixtures/invalid/BadName/program.md +3 -0
  15. package/atris/experiments/_fixtures/invalid/BadName/results.tsv +1 -0
  16. package/atris/experiments/_fixtures/invalid/bloated-context/loop.py +1 -0
  17. package/atris/experiments/_fixtures/invalid/bloated-context/measure.py +1 -0
  18. package/atris/experiments/_fixtures/invalid/bloated-context/program.md +6 -0
  19. package/atris/experiments/_fixtures/invalid/bloated-context/results.tsv +1 -0
  20. package/atris/experiments/_fixtures/valid/good-experiment/loop.py +1 -0
  21. package/atris/experiments/_fixtures/valid/good-experiment/measure.py +1 -0
  22. package/atris/experiments/_fixtures/valid/good-experiment/program.md +3 -0
  23. package/atris/experiments/_fixtures/valid/good-experiment/results.tsv +1 -0
  24. package/atris/experiments/_template/pack/loop.py +3 -0
  25. package/atris/experiments/_template/pack/measure.py +13 -0
  26. package/atris/experiments/_template/pack/program.md +3 -0
  27. package/atris/experiments/_template/pack/reset.py +3 -0
  28. package/atris/experiments/_template/pack/results.tsv +1 -0
  29. package/atris/experiments/benchmark_runtime.py +81 -0
  30. package/atris/experiments/benchmark_validate.py +70 -0
  31. package/atris/experiments/validate.py +92 -0
  32. package/atris/policies/atris-design.md +66 -0
  33. package/atris/skills/README.md +1 -0
  34. package/atris/skills/apps/SKILL.md +243 -0
  35. package/atris/skills/autoresearch/SKILL.md +63 -0
  36. package/atris/skills/create-app/SKILL.md +6 -0
  37. package/atris/skills/design/SKILL.md +15 -1
  38. package/atris/skills/drive/SKILL.md +335 -20
  39. package/atris/skills/ramp/SKILL.md +295 -0
  40. package/bin/atris.js +76 -5
  41. package/commands/business.js +132 -0
  42. package/commands/clean.js +113 -70
  43. package/commands/console.js +397 -0
  44. package/commands/experiments.js +216 -0
  45. package/commands/init.js +4 -0
  46. package/commands/pull.js +311 -0
  47. package/commands/push.js +170 -0
  48. package/commands/run.js +366 -0
  49. package/commands/status.js +21 -1
  50. package/package.json +2 -1
@@ -0,0 +1,70 @@
1
+ """Benchmark the validator against fixed good/bad fixtures."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ import sys
7
+
8
+
9
+ ROOT = Path(__file__).resolve().parent
10
+ if str(ROOT) not in sys.path:
11
+ sys.path.insert(0, str(ROOT))
12
+
13
+ from validate import validate_experiment
14
+
15
+
16
+ FIXTURES_DIR = ROOT / "_fixtures"
17
+
18
+ CASES = [
19
+ {
20
+ "path": FIXTURES_DIR / "valid" / "good-experiment",
21
+ "expect_ok": True,
22
+ "must_contain": [],
23
+ },
24
+ {
25
+ "path": FIXTURES_DIR / "invalid" / "BadName",
26
+ "expect_ok": False,
27
+ "must_contain": ["invalid folder name", "missing required file measure.py"],
28
+ },
29
+ {
30
+ "path": FIXTURES_DIR / "invalid" / "bloated-context",
31
+ "expect_ok": False,
32
+ "must_contain": ["program.md too long"],
33
+ },
34
+ ]
35
+
36
+
37
+ def main() -> int:
38
+ passed = 0
39
+ failures = []
40
+
41
+ for case in CASES:
42
+ issues = validate_experiment(case["path"])
43
+ is_ok = not issues
44
+
45
+ if case["expect_ok"] != is_ok:
46
+ failures.append(f"{case['path'].name}: expected ok={case['expect_ok']} got ok={is_ok}")
47
+ continue
48
+
49
+ missing = [needle for needle in case["must_contain"] if not any(needle in issue for issue in issues)]
50
+ if missing:
51
+ failures.append(f"{case['path'].name}: missing expected issue(s): {', '.join(missing)}")
52
+ continue
53
+
54
+ passed += 1
55
+
56
+ total = len(CASES)
57
+ score = passed / total if total else 0.0
58
+ print(f"SCORE {score:.4f} ({passed}/{total})")
59
+
60
+ if failures:
61
+ for failure in failures:
62
+ print(f"FAIL {failure}")
63
+ return 1
64
+
65
+ print("PASS benchmark_validate")
66
+ return 0
67
+
68
+
69
+ if __name__ == "__main__":
70
+ raise SystemExit(main())
@@ -0,0 +1,92 @@
1
+ """Validate experiments for structure and context hygiene."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import re
7
+ from pathlib import Path
8
+
9
+
10
+ REQUIRED_FILES = ("program.md", "measure.py", "loop.py", "results.tsv")
11
+ MAX_PROGRAM_CHARS = 1200
12
+ MAX_RESULTS_BYTES = 64_000
13
+ SLUG_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
14
+
15
+
16
+ def find_experiments(root: Path) -> list[Path]:
17
+ return sorted(
18
+ path
19
+ for path in root.iterdir()
20
+ if path.is_dir() and not path.name.startswith((".", "_"))
21
+ )
22
+
23
+
24
+ def resolve_experiments(root: Path) -> list[Path]:
25
+ if not root.exists() or not root.is_dir():
26
+ return []
27
+
28
+ # Allow validating a single pack directly, not just a parent directory.
29
+ if any((root / filename).exists() for filename in REQUIRED_FILES):
30
+ return [root]
31
+
32
+ return find_experiments(root)
33
+
34
+
35
+ def validate_experiment(path: Path) -> list[str]:
36
+ issues: list[str] = []
37
+
38
+ if not SLUG_RE.match(path.name):
39
+ issues.append(f"{path.name}: invalid folder name, use lowercase-hyphen slug")
40
+
41
+ for filename in REQUIRED_FILES:
42
+ if not (path / filename).exists():
43
+ issues.append(f"{path.name}: missing required file {filename}")
44
+
45
+ program_path = path / "program.md"
46
+ if program_path.exists():
47
+ size = len(program_path.read_text(encoding="utf-8"))
48
+ if size > MAX_PROGRAM_CHARS:
49
+ issues.append(
50
+ f"{path.name}: program.md too long ({size} chars > {MAX_PROGRAM_CHARS})"
51
+ )
52
+
53
+ results_path = path / "results.tsv"
54
+ if results_path.exists():
55
+ size = results_path.stat().st_size
56
+ if size > MAX_RESULTS_BYTES:
57
+ issues.append(
58
+ f"{path.name}: results.tsv too large ({size} bytes > {MAX_RESULTS_BYTES})"
59
+ )
60
+
61
+ return issues
62
+
63
+
64
+ def main() -> int:
65
+ parser = argparse.ArgumentParser(description="Validate experiment packs.")
66
+ parser.add_argument("root", nargs="?", default=".", help="Directory containing experiment packs")
67
+ args = parser.parse_args()
68
+
69
+ root = Path(args.root).resolve()
70
+ experiments = resolve_experiments(root)
71
+ if not experiments:
72
+ print("FAIL: no experiments found")
73
+ return 1
74
+
75
+ all_issues: list[str] = []
76
+ for path in experiments:
77
+ all_issues.extend(validate_experiment(path))
78
+
79
+ if all_issues:
80
+ print("FAIL")
81
+ for issue in all_issues:
82
+ print(f"- {issue}")
83
+ return 1
84
+
85
+ print(f"PASS: {len(experiments)} experiment(s) valid")
86
+ for path in experiments:
87
+ print(f"- {path.name}")
88
+ return 0
89
+
90
+
91
+ if __name__ == "__main__":
92
+ raise SystemExit(main())
@@ -52,12 +52,43 @@ dark backgrounds are easier to make look good. steal from places you like — li
52
52
 
53
53
  **avoid:** static pages with nothing moving, or the opposite — bouncing everything
54
54
 
55
+ **specific anti-patterns:**
56
+ - cursor-following lines or elements
57
+ - meteor/particle effects shooting across screen
58
+ - buttons that follow the cursor (harder to click, not clever)
59
+ - FAQ/content that breaks if you scroll past before the fade-in finishes
60
+ - animations that swap styles endlessly without purpose (rotating shapes, morphing buttons)
61
+
55
62
  **the move:** one well-timed animation beats ten scattered ones. page load with staggered reveals (animation-delay) creates more impact than hover effects on every button.
56
63
 
57
64
  css transitions: 200-300ms, ease-out. that's it.
58
65
 
59
66
  ---
60
67
 
68
+ ## hover states
69
+
70
+ **avoid:**
71
+ - elements that fade out or disappear on hover
72
+ - nav items that shift position or slide horizontally on hover
73
+ - arrows/icons that move backwards or vertically on hover
74
+ - hiding critical info or functionality behind hover (hover doesn't exist on mobile)
75
+
76
+ **the move:** hover should make elements feel "lickable" — inviting to click. slightly brighten, scale up (1.02-1.05), or add a subtle glow. the user should feel pulled toward clicking, not confused about what happened.
77
+
78
+ test every hover on mobile. if something only works on hover, it's broken for half your users.
79
+
80
+ ---
81
+
82
+ ## scroll behavior
83
+
84
+ **avoid:** scrolljacking — never override native browser scroll with custom scroll logic. it feels like "moving through molasses" and users hate it.
85
+
86
+ **the move:** let the browser handle scrolling. if you want scroll-triggered effects, use intersection observer to trigger animations as sections enter the viewport — but don't mess with scroll speed or direction.
87
+
88
+ use the "peeking" technique: let a few pixels of the next section peek above the fold instead of full-screen heroes with "scroll down" arrows. this naturally signals more content below.
89
+
90
+ ---
91
+
61
92
  ## backgrounds
62
93
 
63
94
  **avoid:** solid white, solid light gray, flat nothing
@@ -82,12 +113,47 @@ vary your choices. alternate themes. try different directions between projects.
82
113
 
83
114
  ---
84
115
 
116
+ ## information hierarchy
117
+
118
+ **avoid:** mixing 4-5 competing text styles on one page. labels, headers, subheaders, badges, and body text all fighting for attention.
119
+
120
+ **the move:** pick 2-3 levels max. one dominant style, one supporting, one accent. if you add a new style, ask: does this earn its place or is it clutter?
121
+
122
+ ---
123
+
124
+ ## hero section (the H1 test)
125
+
126
+ your hero must answer four questions in seconds:
127
+ 1. **what is it?** — clear product description
128
+ 2. **who is it for?** — the target user
129
+ 3. **to what end?** — why should they care
130
+ 4. **what's the CTA?** — one clear next step
131
+
132
+ if a stranger can't answer all four in 5 seconds of looking at your hero, rewrite it.
133
+
134
+ ---
135
+
136
+ ## assets
137
+
138
+ **avoid:**
139
+ - blurry or low-res screenshots
140
+ - "fake dashboard" mockups with Fisher-Price primary colors (red/yellow/green/blue)
141
+ - non-system emojis used as decoration (lazy AI tell)
142
+
143
+ **the move:** real product screenshots at high resolution. if you don't have a product yet, use a well-designed mockup — but make it sharp and believable.
144
+
145
+ ---
146
+
85
147
  ## before shipping
86
148
 
87
149
  - can you name the aesthetic in 2-3 words?
88
150
  - did you pick a real font, not a default?
89
151
  - is there at least one intentional animation?
90
152
  - does the background have depth?
153
+ - do hover states feel inviting, not confusing?
154
+ - does scrolling feel native?
155
+ - does the hero pass the H1 test (what/who/why/CTA)?
156
+ - are all screenshots/assets crisp?
91
157
  - would a designer immediately clock this as ai-generated?
92
158
 
93
159
  if the last answer is yes, you're not done.
@@ -27,6 +27,7 @@ cp -r atris/skills/[name] ~/.codex/skills/
27
27
  |-------|-------------|--------|
28
28
  | atris | Workflow enforcement + plan/do/review | `policies/ANTISLOP.md` |
29
29
  | autopilot | PRD-driven autonomous execution | — |
30
+ | autoresearch | Bounded keep/revert experiment loops via `atris/experiments/` | — |
30
31
  | backend | Backend architecture anti-patterns | `policies/atris-backend.md` |
31
32
  | design | Frontend aesthetics policy | `policies/atris-design.md` |
32
33
  | calendar | Google Calendar integration via AtrisOS | — |
@@ -0,0 +1,243 @@
1
+ ---
2
+ name: apps
3
+ description: View, manage, and trigger Atris apps. Use when user asks about their apps, app status, runs, data, or wants to trigger an app.
4
+ version: 1.0.0
5
+ tags:
6
+ - apps
7
+ - atris
8
+ - management
9
+ ---
10
+
11
+ # Apps
12
+
13
+ View and manage your Atris apps — status, runs, data, secrets, members.
14
+
15
+ ## Bootstrap
16
+
17
+ ```bash
18
+ TOKEN=$(node -e "console.log(require('$HOME/.atris/credentials.json').token)" 2>/dev/null \
19
+ || python3 -c "import json,os; print(json.load(open(os.path.expanduser('~/.atris/credentials.json')))['token'])" 2>/dev/null)
20
+ if [ -z "$TOKEN" ]; then echo "Not logged in. Run: atris login"; exit 1; fi
21
+ echo "Ready."
22
+ ```
23
+
24
+ Base URL: `https://api.atris.ai/api/apps`
25
+
26
+ Auth: `-H "Authorization: Bearer $TOKEN"`
27
+
28
+ ---
29
+
30
+ ## List My Apps
31
+
32
+ ```bash
33
+ curl -s "https://api.atris.ai/api/apps" \
34
+ -H "Authorization: Bearer $TOKEN"
35
+ ```
36
+
37
+ Returns all apps you own with id, name, slug, description, template, status.
38
+
39
+ ### Filter Apps
40
+
41
+ ```bash
42
+ # Template apps only
43
+ curl -s "https://api.atris.ai/api/apps?filter=template" \
44
+ -H "Authorization: Bearer $TOKEN"
45
+
46
+ # Paid apps
47
+ curl -s "https://api.atris.ai/api/apps?filter=paid" \
48
+ -H "Authorization: Bearer $TOKEN"
49
+
50
+ # Free apps
51
+ curl -s "https://api.atris.ai/api/apps?filter=free" \
52
+ -H "Authorization: Bearer $TOKEN"
53
+ ```
54
+
55
+ ---
56
+
57
+ ## App Details
58
+
59
+ ### Get App Status
60
+ ```bash
61
+ curl -s "https://api.atris.ai/api/apps/{slug}/status" \
62
+ -H "Authorization: Bearer $TOKEN"
63
+ ```
64
+
65
+ Returns: last run, next run, health, active members.
66
+
67
+ ### Get App Runs
68
+ ```bash
69
+ curl -s "https://api.atris.ai/api/apps/{slug}/runs?limit=10" \
70
+ -H "Authorization: Bearer $TOKEN"
71
+ ```
72
+
73
+ ### Get Single Run
74
+ ```bash
75
+ curl -s "https://api.atris.ai/api/apps/{slug}/runs/{run_id}" \
76
+ -H "Authorization: Bearer $TOKEN"
77
+ ```
78
+
79
+ ---
80
+
81
+ ## App Data
82
+
83
+ ### Read All Data
84
+ ```bash
85
+ curl -s "https://api.atris.ai/api/apps/{slug}/data" \
86
+ -H "Authorization: Bearer $TOKEN"
87
+ ```
88
+
89
+ ### Read Specific Collection
90
+ ```bash
91
+ curl -s "https://api.atris.ai/api/apps/{slug}/data/{collection}" \
92
+ -H "Authorization: Bearer $TOKEN"
93
+ ```
94
+
95
+ ### Push Data In
96
+ ```bash
97
+ curl -s -X POST "https://api.atris.ai/api/apps/{slug}/ingest" \
98
+ -H "Authorization: Bearer $TOKEN" \
99
+ -H "Content-Type: application/json" \
100
+ -d '{"collection": "leads", "data": {"name": "Acme", "score": 85}}'
101
+ ```
102
+
103
+ ---
104
+
105
+ ## Secrets
106
+
107
+ ### List Secret Keys (names + storage tier)
108
+ ```bash
109
+ curl -s "https://api.atris.ai/api/apps/{slug}/secrets" \
110
+ -H "Authorization: Bearer $TOKEN"
111
+ ```
112
+
113
+ Returns key names and where they're stored:
114
+ - `"storage_tier": "cloud"` — encrypted in Atris vault
115
+ - `"storage_tier": "local"` — on your machine at `~/.atris/secrets/{slug}/`
116
+
117
+ ### Store Secret (cloud)
118
+ ```bash
119
+ curl -s -X PUT "https://api.atris.ai/api/apps/{slug}/secrets/{key}" \
120
+ -H "Authorization: Bearer $TOKEN" \
121
+ -H "Content-Type: application/json" \
122
+ -d '{"value": "sk-secret-value"}'
123
+ ```
124
+
125
+ ### Register Local Secret (manifest only)
126
+ ```bash
127
+ curl -s -X POST "https://api.atris.ai/api/apps/{slug}/secrets/{key}/register-local" \
128
+ -H "Authorization: Bearer $TOKEN"
129
+ ```
130
+
131
+ No value sent. Just tells the web UI "this key exists on my machine."
132
+
133
+ ### Delete Secret
134
+ ```bash
135
+ curl -s -X DELETE "https://api.atris.ai/api/apps/{slug}/secrets/{key}" \
136
+ -H "Authorization: Bearer $TOKEN"
137
+ ```
138
+
139
+ ---
140
+
141
+ ## Members
142
+
143
+ ### List App Members
144
+ ```bash
145
+ curl -s "https://api.atris.ai/api/apps/{slug}/members" \
146
+ -H "Authorization: Bearer $TOKEN"
147
+ ```
148
+
149
+ ### Add Member (agent operator)
150
+ ```bash
151
+ curl -s -X POST "https://api.atris.ai/api/apps/{slug}/members" \
152
+ -H "Authorization: Bearer $TOKEN" \
153
+ -H "Content-Type: application/json" \
154
+ -d '{"agent_id": "AGENT_ID", "role": "operator"}'
155
+ ```
156
+
157
+ ### Remove Member
158
+ ```bash
159
+ curl -s -X DELETE "https://api.atris.ai/api/apps/{slug}/members/{agent_id}" \
160
+ -H "Authorization: Bearer $TOKEN"
161
+ ```
162
+
163
+ ---
164
+
165
+ ## Trigger
166
+
167
+ ### Run App Now
168
+ ```bash
169
+ curl -s -X POST "https://api.atris.ai/api/apps/{slug}/trigger" \
170
+ -H "Authorization: Bearer $TOKEN" \
171
+ -H "Content-Type: application/json" \
172
+ -d '{"trigger_type": "manual"}'
173
+ ```
174
+
175
+ ---
176
+
177
+ ## App Manifest (for published apps)
178
+
179
+ ```bash
180
+ curl -s "https://api.atris.ai/api/apps/{slug}/manifest"
181
+ ```
182
+
183
+ No auth needed. Returns name, description, required secrets, schedule.
184
+
185
+ ### Install a Published App
186
+
187
+ ```bash
188
+ curl -s -X POST "https://api.atris.ai/api/apps/{slug}/install" \
189
+ -H "Authorization: Bearer $TOKEN" \
190
+ -H "Content-Type: application/json" \
191
+ -d '{}'
192
+ ```
193
+
194
+ ---
195
+
196
+ ## Workflows
197
+
198
+ ### "What apps do I have?"
199
+ 1. List apps: `GET /api/apps`
200
+ 2. Display: name, slug, template, last run status
201
+
202
+ ### "How is my app doing?"
203
+ 1. Get status: `GET /api/apps/{slug}/status`
204
+ 2. Get recent runs: `GET /api/apps/{slug}/runs?limit=5`
205
+ 3. Show: health, last run time, success/failure, output
206
+
207
+ ### "Check my app's secrets"
208
+ 1. List secrets: `GET /api/apps/{slug}/secrets`
209
+ 2. Show each key with storage tier (cloud/local)
210
+ 3. If required secrets are missing, tell the user how to add them
211
+
212
+ ### "Run my app"
213
+ 1. Trigger: `POST /api/apps/{slug}/trigger`
214
+ 2. Poll status: `GET /api/apps/{slug}/status` (wait for completion)
215
+ 3. Show run result: `GET /api/apps/{slug}/runs?limit=1`
216
+
217
+ ---
218
+
219
+ ## Quick Reference
220
+
221
+ ```bash
222
+ TOKEN=$(node -e "console.log(require('$HOME/.atris/credentials.json').token)")
223
+
224
+ # List all my apps
225
+ curl -s "https://api.atris.ai/api/apps" -H "Authorization: Bearer $TOKEN"
226
+
227
+ # App status
228
+ curl -s "https://api.atris.ai/api/apps/SLUG/status" -H "Authorization: Bearer $TOKEN"
229
+
230
+ # Recent runs
231
+ curl -s "https://api.atris.ai/api/apps/SLUG/runs?limit=5" -H "Authorization: Bearer $TOKEN"
232
+
233
+ # Trigger a run
234
+ curl -s -X POST "https://api.atris.ai/api/apps/SLUG/trigger" \
235
+ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
236
+ -d '{"trigger_type":"manual"}'
237
+
238
+ # Read app data
239
+ curl -s "https://api.atris.ai/api/apps/SLUG/data" -H "Authorization: Bearer $TOKEN"
240
+
241
+ # List secrets (with storage tier)
242
+ curl -s "https://api.atris.ai/api/apps/SLUG/secrets" -H "Authorization: Bearer $TOKEN"
243
+ ```
@@ -0,0 +1,63 @@
1
+ ---
2
+ name: autoresearch
3
+ description: Karpathy-style keep/revert experiment loop for Atris experiment packs. Use when improving prompts, tools, workers, or bounded repo targets.
4
+ version: 1.0.0
5
+ tags:
6
+ - experiments
7
+ - keep-revert
8
+ - optimization
9
+ - metrics
10
+ ---
11
+
12
+ # Autoresearch Skill
13
+
14
+ Autoresearch means one bounded target, one external metric, one keep/revert loop, one append-only log.
15
+
16
+ ## When to use
17
+
18
+ - prompt optimization
19
+ - worker routing
20
+ - tool behavior
21
+ - evaluation harnesses
22
+ - any repo-local target that can be measured honestly
23
+
24
+ ## Process
25
+
26
+ 1. Read `atris/experiments/<slug>/program.md`
27
+ 2. Confirm the target is bounded
28
+ 3. Run the baseline with `measure.py`
29
+ 4. Apply one candidate change
30
+ 5. Rerun the metric
31
+ 6. Keep only if the score improves
32
+ 7. Write the outcome to `results.tsv`
33
+ 8. Revert losses
34
+
35
+ ## Rules
36
+
37
+ - external metric only
38
+ - no unlogged keeps
39
+ - no broad refactors inside an experiment
40
+ - one experiment pack = one target
41
+ - if variance exists, define the keep margin first
42
+
43
+ ## Commands
44
+
45
+ ```bash
46
+ atris experiments init <slug>
47
+ atris experiments validate
48
+ atris experiments benchmark
49
+ ```
50
+
51
+ ## Good output
52
+
53
+ - short `program.md`
54
+ - honest `measure.py`
55
+ - deterministic `loop.py`
56
+ - append-only `results.tsv`
57
+
58
+ ## Bad output
59
+
60
+ - "felt better"
61
+ - changed three things at once
62
+ - kept a patch without a measured win
63
+ - no reset/revert path
@@ -104,6 +104,12 @@ unset secret_val
104
104
  echo "Saved locally."
105
105
  ```
106
106
 
107
+ Register the key in the web UI (manifest only — no value sent):
108
+ ```bash
109
+ curl -s -X POST "https://api.atris.ai/api/apps/SLUG/secrets/KEY_NAME/register-local" \
110
+ -H "Authorization: Bearer $TOKEN"
111
+ ```
112
+
107
113
  Verify (key names only, never values):
108
114
  ```bash
109
115
  ls ~/.atris/secrets/SLUG/
@@ -27,10 +27,20 @@ This skill uses the Atris workflow:
27
27
 
28
28
  **Layout:** break the hero + 3 cards + footer template. asymmetry is interesting. dramatic whitespace.
29
29
 
30
- **Motion:** one well-timed animation beats ten scattered ones. 200-300ms ease-out.
30
+ **Motion:** one well-timed animation beats ten scattered ones. 200-300ms ease-out. no cursor-following lines, no meteor effects, no buttons that chase the cursor.
31
+
32
+ **Hover:** make elements feel inviting on hover (brighten, subtle scale). never fade out, shift, or hide content behind hover. hover doesn't exist on mobile.
33
+
34
+ **Scroll:** never override native scroll. use "peeking" (show a few px of next section) instead of full-screen hero + scroll arrow.
35
+
36
+ **Hero (H1 test):** must answer in 5 seconds — what is it, who is it for, why care, what's the CTA.
37
+
38
+ **Assets:** high-res screenshots only. no fake dashboards with primary colors. no decorative non-system emojis.
31
39
 
32
40
  **Backgrounds:** add depth. gradients, patterns, mesh effects. flat = boring.
33
41
 
42
+ **Hierarchy:** 2-3 text levels max. don't mix 5 competing styles.
43
+
34
44
  ## Before Shipping Checklist
35
45
 
36
46
  Run through `atris/policies/atris-design.md` "before shipping" section:
@@ -38,6 +48,10 @@ Run through `atris/policies/atris-design.md` "before shipping" section:
38
48
  - distinctive font, not default?
39
49
  - at least one intentional animation?
40
50
  - background has depth?
51
+ - hover states feel inviting, not confusing?
52
+ - scrolling feels native?
53
+ - hero passes H1 test (what/who/why/CTA)?
54
+ - all assets crisp?
41
55
  - would a designer clock this as ai-generated?
42
56
 
43
57
  ## Atris Commands