atris 3.35.0 → 3.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/AGENTS.md +37 -0
  2. package/README.md +5 -3
  3. package/atris/GETTING_STARTED.md +1 -1
  4. package/atris/atris.md +3 -0
  5. package/atris/policies/day-loop-voice.md +102 -0
  6. package/atris/policies/outbound-artifact-gate.md +2 -0
  7. package/atris/skills/design/SKILL.md +56 -32
  8. package/atris/skills/endgame/SKILL.md +12 -6
  9. package/atris/skills/engines/SKILL.md +22 -4
  10. package/atris/skills/fable-method/SKILL.md +66 -0
  11. package/atris/skills/improve/SKILL.md +65 -45
  12. package/atris/skills/youtube/SKILL.md +10 -1
  13. package/atris.md +2 -0
  14. package/ax +147 -19
  15. package/bin/atris.js +576 -267
  16. package/commands/activate.js +194 -88
  17. package/commands/agents.js +166 -0
  18. package/commands/autoland.js +459 -107
  19. package/commands/autopilot-front.js +20 -2
  20. package/commands/autopilot.js +118 -2
  21. package/commands/avail.js +407 -0
  22. package/commands/bench.js +188 -0
  23. package/commands/brain.js +3 -0
  24. package/commands/brief.js +651 -0
  25. package/commands/business-sync.js +192 -6
  26. package/commands/clean.js +50 -24
  27. package/commands/close.js +1083 -0
  28. package/commands/cloud.js +245 -0
  29. package/commands/compile.js +292 -1
  30. package/commands/computer.js +150 -3
  31. package/commands/dream.js +365 -0
  32. package/commands/drill.js +371 -0
  33. package/commands/engine.js +993 -32
  34. package/commands/experiments.js +28 -0
  35. package/commands/feedback.js +34 -12
  36. package/commands/fleet-report.js +206 -0
  37. package/commands/gm.js +23 -0
  38. package/commands/goal.js +247 -0
  39. package/commands/improve.js +642 -26
  40. package/commands/init.js +72 -44
  41. package/commands/interview.js +67 -1
  42. package/commands/land.js +152 -52
  43. package/commands/lifecycle.js +39 -3
  44. package/commands/log.js +84 -1
  45. package/commands/loops.js +220 -16
  46. package/commands/meet.js +220 -0
  47. package/commands/member.js +511 -34
  48. package/commands/mission.js +3144 -338
  49. package/commands/next.js +137 -0
  50. package/commands/now.js +220 -25
  51. package/commands/one-lap.js +776 -0
  52. package/commands/orb.js +314 -0
  53. package/commands/pack-craft.js +179 -0
  54. package/commands/pack.js +823 -0
  55. package/commands/play.js +3 -2
  56. package/commands/probe.js +30 -3
  57. package/commands/pulse.js +241 -46
  58. package/commands/push.js +260 -82
  59. package/commands/rainmaker.js +49 -0
  60. package/commands/report.js +415 -0
  61. package/commands/scout.js +147 -0
  62. package/commands/search.js +363 -0
  63. package/commands/skill.js +47 -3
  64. package/commands/slop.js +50 -2
  65. package/commands/soul.js +1 -1
  66. package/commands/stream.js +861 -0
  67. package/commands/study.js +693 -0
  68. package/commands/sync.js +67 -54
  69. package/commands/task.js +1346 -117
  70. package/commands/team.js +73 -0
  71. package/commands/verify.js +96 -0
  72. package/commands/watch.js +303 -0
  73. package/commands/wish.js +500 -0
  74. package/commands/workflow.js +11 -5
  75. package/commands/worktree.js +234 -13
  76. package/commands/xp.js +29 -11
  77. package/lib/auto-accept-certified.js +331 -34
  78. package/lib/autoland.js +319 -54
  79. package/lib/ax-auto-lane.js +96 -0
  80. package/lib/bench/context.js +147 -0
  81. package/lib/bench/engines.js +141 -0
  82. package/lib/bench/report.js +140 -0
  83. package/lib/bench/runner.js +512 -0
  84. package/lib/brief-ledger.js +350 -0
  85. package/lib/cloud-mission.js +259 -0
  86. package/lib/codex-flight.js +154 -0
  87. package/lib/default-runner.js +45 -0
  88. package/lib/default-verifier.js +70 -0
  89. package/lib/engine-registry.js +232 -0
  90. package/lib/experiments/daily.js +640 -0
  91. package/lib/fleet.js +2219 -67
  92. package/lib/improve-vitals-html.js +171 -0
  93. package/lib/known-commands.js +58 -0
  94. package/lib/loop-doctor.js +416 -0
  95. package/lib/member-switches.js +144 -0
  96. package/lib/mission-room.js +1 -0
  97. package/lib/mission-root.js +52 -0
  98. package/lib/next-moves.js +327 -10
  99. package/lib/one-lap-validator.js +60 -0
  100. package/lib/orb-context.js +477 -0
  101. package/lib/orb-scorecard.js +224 -0
  102. package/lib/policy-lessons.js +52 -1
  103. package/lib/pulse.js +277 -3
  104. package/lib/receipt-block.js +168 -0
  105. package/lib/receipt-evidence.js +65 -4
  106. package/lib/router-brain.js +352 -0
  107. package/lib/runner-command.js +10 -0
  108. package/lib/self-drive.js +258 -0
  109. package/lib/short-name.js +103 -0
  110. package/lib/spawn-env.js +18 -0
  111. package/lib/state-detection.js +56 -1
  112. package/lib/sync-status.js +59 -0
  113. package/lib/task-db.js +108 -29
  114. package/lib/task-proof.js +23 -1
  115. package/lib/team-presence.js +260 -0
  116. package/lib/tool-result-encode.js +7 -0
  117. package/lib/trust-tiers.js +90 -0
  118. package/lib/usage.js +107 -0
  119. package/lib/voice-gate.js +163 -0
  120. package/lib/wish-audit.js +1368 -0
  121. package/lib/wish-delegate.js +1840 -0
  122. package/lib/wish-design.js +110 -0
  123. package/lib/wish-stats.js +183 -0
  124. package/lib/wish-store.js +354 -0
  125. package/lib/zip.js +221 -0
  126. package/package.json +3 -1
  127. package/templates/loops/atris/loops/LOOPS.md +55 -0
  128. package/templates/loops/atris/loops/TICK.md +24 -0
  129. package/templates/loops/atris/loops/feedback.md +22 -0
  130. package/templates/loops/atris/loops/quality.md +22 -0
  131. package/templates/loops/atris/wiki/systems/loops.md +41 -0
  132. package/utils/api.js +5 -1
  133. package/utils/auth.js +57 -21
package/lib/zip.js ADDED
@@ -0,0 +1,221 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const zlib = require('zlib');
6
+
7
+ const LOCAL_FILE_HEADER = 0x04034b50;
8
+ const CENTRAL_DIRECTORY_HEADER = 0x02014b50;
9
+ const END_OF_CENTRAL_DIRECTORY = 0x06054b50;
10
+ const UTF8_FLAG = 0x0800;
11
+ const DEFLATE = 8;
12
+ const STORE = 0;
13
+
14
+ let crcTable = null;
15
+
16
+ function getCrcTable() {
17
+ if (crcTable) return crcTable;
18
+ crcTable = new Uint32Array(256);
19
+ for (let i = 0; i < 256; i += 1) {
20
+ let c = i;
21
+ for (let j = 0; j < 8; j += 1) {
22
+ c = (c & 1) ? (0xedb88320 ^ (c >>> 1)) : (c >>> 1);
23
+ }
24
+ crcTable[i] = c >>> 0;
25
+ }
26
+ return crcTable;
27
+ }
28
+
29
+ function crc32(data) {
30
+ const table = getCrcTable();
31
+ const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data || '');
32
+ let crc = 0xffffffff;
33
+ for (const byte of buffer) {
34
+ crc = table[(crc ^ byte) & 0xff] ^ (crc >>> 8);
35
+ }
36
+ return (crc ^ 0xffffffff) >>> 0;
37
+ }
38
+
39
+ function dosDateTime(dateValue) {
40
+ const date = dateValue instanceof Date && Number.isFinite(dateValue.getTime())
41
+ ? dateValue
42
+ : new Date();
43
+ const year = Math.max(1980, Math.min(2107, date.getFullYear()));
44
+ const month = date.getMonth() + 1;
45
+ const day = date.getDate();
46
+ const hours = date.getHours();
47
+ const minutes = date.getMinutes();
48
+ const seconds = Math.floor(date.getSeconds() / 2);
49
+ return {
50
+ date: ((year - 1980) << 9) | (month << 5) | day,
51
+ time: (hours << 11) | (minutes << 5) | seconds,
52
+ };
53
+ }
54
+
55
+ function normalizeEntryName(name) {
56
+ return String(name || '').replace(/\\/g, '/');
57
+ }
58
+
59
+ function assertUInt32(value, label) {
60
+ if (!Number.isFinite(value) || value < 0 || value > 0xffffffff) {
61
+ throw new Error(`zip ${label} is too large`);
62
+ }
63
+ }
64
+
65
+ function createZipBuffer(entries) {
66
+ const localParts = [];
67
+ const centralParts = [];
68
+ let offset = 0;
69
+
70
+ for (const entry of entries) {
71
+ const name = normalizeEntryName(entry.name);
72
+ if (!name) throw new Error('zip entry name is required');
73
+ const nameBuffer = Buffer.from(name, 'utf8');
74
+ const data = Buffer.isBuffer(entry.data) ? entry.data : Buffer.from(entry.data || '');
75
+ const compressed = zlib.deflateRawSync(data);
76
+ const crc = crc32(data);
77
+ const { date, time } = dosDateTime(entry.mtime);
78
+
79
+ assertUInt32(data.length, 'entry');
80
+ assertUInt32(compressed.length, 'compressed entry');
81
+ assertUInt32(offset, 'offset');
82
+
83
+ const localHeader = Buffer.alloc(30);
84
+ localHeader.writeUInt32LE(LOCAL_FILE_HEADER, 0);
85
+ localHeader.writeUInt16LE(20, 4);
86
+ localHeader.writeUInt16LE(UTF8_FLAG, 6);
87
+ localHeader.writeUInt16LE(DEFLATE, 8);
88
+ localHeader.writeUInt16LE(time, 10);
89
+ localHeader.writeUInt16LE(date, 12);
90
+ localHeader.writeUInt32LE(crc, 14);
91
+ localHeader.writeUInt32LE(compressed.length, 18);
92
+ localHeader.writeUInt32LE(data.length, 22);
93
+ localHeader.writeUInt16LE(nameBuffer.length, 26);
94
+ localHeader.writeUInt16LE(0, 28);
95
+ localParts.push(localHeader, nameBuffer, compressed);
96
+
97
+ const centralHeader = Buffer.alloc(46);
98
+ centralHeader.writeUInt32LE(CENTRAL_DIRECTORY_HEADER, 0);
99
+ centralHeader.writeUInt16LE(20, 4);
100
+ centralHeader.writeUInt16LE(20, 6);
101
+ centralHeader.writeUInt16LE(UTF8_FLAG, 8);
102
+ centralHeader.writeUInt16LE(DEFLATE, 10);
103
+ centralHeader.writeUInt16LE(time, 12);
104
+ centralHeader.writeUInt16LE(date, 14);
105
+ centralHeader.writeUInt32LE(crc, 16);
106
+ centralHeader.writeUInt32LE(compressed.length, 20);
107
+ centralHeader.writeUInt32LE(data.length, 24);
108
+ centralHeader.writeUInt16LE(nameBuffer.length, 28);
109
+ centralHeader.writeUInt16LE(0, 30);
110
+ centralHeader.writeUInt16LE(0, 32);
111
+ centralHeader.writeUInt16LE(0, 34);
112
+ centralHeader.writeUInt16LE(0, 36);
113
+ centralHeader.writeUInt32LE(0, 38);
114
+ centralHeader.writeUInt32LE(offset, 42);
115
+ centralParts.push(centralHeader, nameBuffer);
116
+
117
+ offset += localHeader.length + nameBuffer.length + compressed.length;
118
+ }
119
+
120
+ const centralOffset = offset;
121
+ const centralSize = centralParts.reduce((sum, part) => sum + part.length, 0);
122
+ assertUInt32(centralOffset, 'central directory offset');
123
+ assertUInt32(centralSize, 'central directory');
124
+ if (entries.length > 0xffff) throw new Error('zip has too many entries');
125
+
126
+ const end = Buffer.alloc(22);
127
+ end.writeUInt32LE(END_OF_CENTRAL_DIRECTORY, 0);
128
+ end.writeUInt16LE(0, 4);
129
+ end.writeUInt16LE(0, 6);
130
+ end.writeUInt16LE(entries.length, 8);
131
+ end.writeUInt16LE(entries.length, 10);
132
+ end.writeUInt32LE(centralSize, 12);
133
+ end.writeUInt32LE(centralOffset, 16);
134
+ end.writeUInt16LE(0, 20);
135
+
136
+ return Buffer.concat([...localParts, ...centralParts, end]);
137
+ }
138
+
139
+ function writeZipFile(filePath, entries) {
140
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
141
+ fs.writeFileSync(filePath, createZipBuffer(entries));
142
+ }
143
+
144
+ function findEndOfCentralDirectory(buffer) {
145
+ const start = Math.max(0, buffer.length - 22 - 0xffff);
146
+ for (let i = buffer.length - 22; i >= start; i -= 1) {
147
+ if (buffer.readUInt32LE(i) === END_OF_CENTRAL_DIRECTORY) return i;
148
+ }
149
+ throw new Error('invalid zip: missing central directory');
150
+ }
151
+
152
+ function assertReadable(buffer, offset, length, label) {
153
+ if (offset < 0 || length < 0 || offset + length > buffer.length) {
154
+ throw new Error(`invalid zip: truncated ${label}`);
155
+ }
156
+ }
157
+
158
+ function readZipBuffer(input) {
159
+ const buffer = Buffer.isBuffer(input) ? input : Buffer.from(input);
160
+ const endOffset = findEndOfCentralDirectory(buffer);
161
+ const entryCount = buffer.readUInt16LE(endOffset + 10);
162
+ const centralSize = buffer.readUInt32LE(endOffset + 12);
163
+ const centralOffset = buffer.readUInt32LE(endOffset + 16);
164
+ assertReadable(buffer, centralOffset, centralSize, 'central directory');
165
+
166
+ const entries = [];
167
+ let cursor = centralOffset;
168
+ for (let i = 0; i < entryCount; i += 1) {
169
+ assertReadable(buffer, cursor, 46, 'central directory entry');
170
+ if (buffer.readUInt32LE(cursor) !== CENTRAL_DIRECTORY_HEADER) {
171
+ throw new Error('invalid zip: malformed central directory');
172
+ }
173
+
174
+ const flags = buffer.readUInt16LE(cursor + 8);
175
+ const method = buffer.readUInt16LE(cursor + 10);
176
+ const compressedSize = buffer.readUInt32LE(cursor + 20);
177
+ const uncompressedSize = buffer.readUInt32LE(cursor + 24);
178
+ const nameLength = buffer.readUInt16LE(cursor + 28);
179
+ const extraLength = buffer.readUInt16LE(cursor + 30);
180
+ const commentLength = buffer.readUInt16LE(cursor + 32);
181
+ const localOffset = buffer.readUInt32LE(cursor + 42);
182
+ const nameStart = cursor + 46;
183
+ assertReadable(buffer, nameStart, nameLength + extraLength + commentLength, 'central directory name');
184
+ const name = buffer.slice(nameStart, nameStart + nameLength).toString('utf8');
185
+ cursor = nameStart + nameLength + extraLength + commentLength;
186
+
187
+ if (flags & 1) throw new Error(`invalid zip: encrypted entry ${name} is not supported`);
188
+ if (![STORE, DEFLATE].includes(method)) {
189
+ throw new Error(`invalid zip: unsupported compression method ${method} for ${name}`);
190
+ }
191
+
192
+ assertReadable(buffer, localOffset, 30, 'local file header');
193
+ if (buffer.readUInt32LE(localOffset) !== LOCAL_FILE_HEADER) {
194
+ throw new Error(`invalid zip: missing local header for ${name}`);
195
+ }
196
+ const localNameLength = buffer.readUInt16LE(localOffset + 26);
197
+ const localExtraLength = buffer.readUInt16LE(localOffset + 28);
198
+ const dataStart = localOffset + 30 + localNameLength + localExtraLength;
199
+ assertReadable(buffer, dataStart, compressedSize, `file data for ${name}`);
200
+ const compressed = buffer.slice(dataStart, dataStart + compressedSize);
201
+ const data = method === STORE ? Buffer.from(compressed) : zlib.inflateRawSync(compressed);
202
+ if (data.length !== uncompressedSize) {
203
+ throw new Error(`invalid zip: size mismatch for ${name}`);
204
+ }
205
+ entries.push({ name, data });
206
+ }
207
+
208
+ return entries;
209
+ }
210
+
211
+ function readZipFile(filePath) {
212
+ return readZipBuffer(fs.readFileSync(filePath));
213
+ }
214
+
215
+ module.exports = {
216
+ createZipBuffer,
217
+ writeZipFile,
218
+ readZipBuffer,
219
+ readZipFile,
220
+ crc32,
221
+ };
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "atris",
3
- "version": "3.35.0",
3
+ "version": "3.37.0",
4
+ "description": "you say what you want in plain words. atris builds it, checks it, and shows you proof.",
4
5
  "main": "bin/atris.js",
5
6
  "bin": {
6
7
  "atris": "bin/atris.js",
@@ -54,6 +55,7 @@
54
55
  ],
55
56
  "scripts": {
56
57
  "test": "node --test",
58
+ "test:fast": "xargs node --test < test/fast-tests.txt",
57
59
  "lint:decks": "node scripts/lint-all-decks.js",
58
60
  "publish:release": "node scripts/publish-atris-release.js",
59
61
  "prepare": "cp scripts/pre-commit .git/hooks/pre-commit 2>/dev/null && cp scripts/commit-msg .git/hooks/commit-msg 2>/dev/null && cp scripts/prepare-commit-msg .git/hooks/prepare-commit-msg 2>/dev/null && chmod +x .git/hooks/pre-commit .git/hooks/commit-msg .git/hooks/prepare-commit-msg || true"
@@ -0,0 +1,55 @@
1
+ # The Loops
2
+
3
+ **Boot phrase:** "atris loops" or "run the loops" - audits every loop file live,
4
+ routes every open loop back to an owner, and keeps the product improving from proof
5
+ instead of memory.
6
+
7
+ Missions are point-in-time. Loops are forever. A loop owns one thing that must always
8
+ work and always improve, names the signal that proves it, and feeds the loops
9
+ downstream of it. Every mission dispatched in this project should name the loop it
10
+ serves; a mission that serves no loop is probably make-work.
11
+
12
+ Each loop file follows the same contract, enforced by `atris loops audit`:
13
+
14
+ - **Owner** - the `team/<member>` accountable for the loop.
15
+ - **Wiki** - the knowledge page that keeps the loop context alive across sessions.
16
+ - **Runner** - the named thing that executes the signal: script, CI, mission, tick.
17
+ - **Protects** - the user-felt thing that must never break.
18
+ - **Signal** - the receipt that proves it. Green is an exit code or a number, not vibes.
19
+ - **Check** - optional shell command run by `atris loops audit`.
20
+ - **Cadence** - when the loop runs: per-commit, per-tick, nightly, weekly.
21
+ - **Feeds / Fed by** - the loop graph. No loop is an island.
22
+
23
+ ## The Starter Loops
24
+
25
+ | # | Loop | Protects | Signal |
26
+ |---|------|----------|--------|
27
+ | 1 | [feedback](feedback.md) | user pain becomes tracked work | TODO: intake count, queue check, or triage command |
28
+ | 2 | [quality](quality.md) | changes do not regress trust | TODO: test, lint, audit, or review gate |
29
+
30
+ ## The Graph
31
+
32
+ ```text
33
+ users
34
+ |
35
+ v
36
+ [feedback] ---- pain becomes tasks ----+
37
+ | |
38
+ v v
39
+ [quality] <--------- fixes, reviews, receipts
40
+ ```
41
+
42
+ Reading the graph: feedback is the intake. Quality is the ratchet. Feedback discovers
43
+ breaks and turns them into work; quality proves the work made the project tighter.
44
+
45
+ ## Learning Rule
46
+
47
+ Every error that reaches a loop exits as a dated line in that loop's **Log** before the
48
+ work closes. The loop file is the owner member's training data: a class of error is
49
+ allowed to happen once per lesson.
50
+
51
+ ## Operating Rule
52
+
53
+ One tick = run `atris loops audit`, pick the first open loop, run or fix its signal,
54
+ re-run the audit, and append a dated Log line with the receipt. When every loop is
55
+ green, raise one bar instead of idling.
@@ -0,0 +1,24 @@
1
+ # The Tick - loop protocol for ANY model
2
+
3
+ This is the minimum viable evolutionary state of the project. Any agent can run this
4
+ exactly. No step is optional, and no step requires guessing from memory.
5
+
6
+ ## The Tick
7
+
8
+ 1. `atris loops audit` - this is the source of truth. Never assess loop health from
9
+ prose, memory, or vibes.
10
+ 2. Pick the first open loop. If several are open, take the first listed.
11
+ 3. Do exactly what the loop's signal or Check says. Stay inside that loop's surface;
12
+ do not refactor unrelated files.
13
+ 4. `atris loops audit` again. The selected loop must improve or the failure must be
14
+ made explicit.
15
+ 5. Append one dated line to `## Log` in `atris/loops/<loop>.md`: what was open, what
16
+ changed, and the receipt command plus exit code.
17
+ 6. Move the owning task to proof-ready, or land according to this repo's policy.
18
+
19
+ ## Truth Rules
20
+
21
+ - Claim green only by pasting the re-run audit output.
22
+ - Never edit loop files, baselines, or checks to hide a failing signal.
23
+ - If you cannot fix the first open loop, write why as a dated Log line and stop.
24
+ - If all loops are green, tighten one signal so the system keeps improving.
@@ -0,0 +1,22 @@
1
+ # Loop - Feedback
2
+
3
+ **Owner:** `team/TODO-feedback-owner`
4
+ **Wiki:** [systems/loops.md](../wiki/systems/loops.md) TODO: replace or extend with the owner context page.
5
+ **Runner:** TODO: name the intake command, CI job, mission, or wake tick that owns this loop.
6
+
7
+ **Protects:** TODO: every user pain, bug report, or operator note becomes tracked work
8
+ or an explicit no-op decision.
9
+
10
+ **Signal (green =):** TODO: define a numeric or exit-code signal, for example open
11
+ feedback count, stale inbox count, or a triage command that exits 0.
12
+
13
+ **Check:** `true`
14
+
15
+ **Cadence:** TODO: per tick, daily, weekly, or per release.
16
+
17
+ **Feeds:** quality.
18
+ **Fed by:** users, support, operators, telemetry.
19
+
20
+ ## Log
21
+
22
+ - {{DATE}}: TODO: first loop scaffold created; replace this line with the first real receipt.
@@ -0,0 +1,22 @@
1
+ # Loop - Quality
2
+
3
+ **Owner:** `team/TODO-quality-owner`
4
+ **Wiki:** [systems/loops.md](../wiki/systems/loops.md) TODO: replace or extend with the owner context page.
5
+ **Runner:** TODO: name the test, audit, review lane, CI job, or mission that owns this loop.
6
+
7
+ **Protects:** TODO: shipped changes do not make the project harder to trust, operate,
8
+ or understand.
9
+
10
+ **Signal (green =):** TODO: define the receipt, for example a focused test, lint,
11
+ review gate, smoke check, or quality score with a threshold.
12
+
13
+ **Check:** `true`
14
+
15
+ **Cadence:** TODO: per commit, per PR, daily, or before release.
16
+
17
+ **Feeds:** feedback, release confidence, owner trust.
18
+ **Fed by:** feedback, reviews, incidents, failed checks.
19
+
20
+ ## Log
21
+
22
+ - {{DATE}}: TODO: first loop scaffold created; replace this line with the first real receipt.
@@ -0,0 +1,41 @@
1
+ # The Loop System - how this project improves itself
2
+
3
+ A loop owns one user-felt thing that must never break, names the receipt that proves it,
4
+ has a team member accountable for it, and feeds the loops downstream. Missions are
5
+ point-in-time; loops are forever. Source of truth: `atris/loops/LOOPS.md`.
6
+
7
+ ## The Closure Contract
8
+
9
+ A loop is **closed** when all five hold; a project where every loop is closed is
10
+ **self-improving**: breaks discover themselves, route to an owner, and land as fixes
11
+ without waiting for a human to notice first.
12
+
13
+ 1. **Owner** - a `team/<member>/` directory exists. Someone wakes for it.
14
+ 2. **Wiki** - a knowledge page resolves, so context survives sessions.
15
+ 3. **Runner** - the named thing executes the signal: script, CI job, mission, or tick.
16
+ 4. **Signal** - green is an exit code or number, never vibes.
17
+ 5. **Fresh log** - a dated receipt in the loop file is newer than 14 days.
18
+
19
+ `atris loops audit` enforces this contract and prints the self-improving verdict. If a
20
+ loop file has `**Check:** \`<command>\``, the audit runs that command from the project
21
+ root and reports its exit code.
22
+
23
+ ## Starter Owner Map
24
+
25
+ | # | Loop | Owner | Runner class |
26
+ |---|------|-------|--------------|
27
+ | 1 | feedback | TODO-feedback-owner | TODO intake command, mission, or wake tick |
28
+ | 2 | quality | TODO-quality-owner | TODO test, audit, review lane, or CI job |
29
+
30
+ ## Known Open Edges
31
+
32
+ - Replace TODO owners with real `team/<member>/` directories.
33
+ - Replace TODO runners with real commands or missions.
34
+ - Replace `true` starter checks with project-native proof commands.
35
+ - Add loops only when they protect a real user-felt thing.
36
+
37
+ ## Learning Rule
38
+
39
+ An error closes only by becoming a dated Log line in its owning loop, with a receipt.
40
+ Members wake with their loop file, this wiki, and recent receipts in context. Each
41
+ lesson removes a class of error; the loop audit makes the next tick cheap.
package/utils/api.js CHANGED
@@ -25,7 +25,11 @@ const DEFAULT_USER_AGENT = `${DEFAULT_CLIENT_ID} (node ${process.version}; ${os.
25
25
  * @returns {string} API base URL
26
26
  */
27
27
  function getApiBaseUrl() {
28
- const raw = process.env.ATRIS_API_URL || 'https://api.atris.ai/api';
28
+ const backend = process.env.ATRIS_BACKEND_URL
29
+ ? process.env.ATRIS_BACKEND_URL.replace(/\/+$/, '')
30
+ : '';
31
+ const raw = process.env.ATRIS_API_URL
32
+ || (backend ? (backend.endsWith('/api') ? backend : `${backend}/api`) : 'https://api.atris.ai/api');
29
33
  return raw.replace(/\/$/, '');
30
34
  }
31
35
 
package/utils/auth.js CHANGED
@@ -139,13 +139,25 @@ function shouldRefreshToken(token, bufferSeconds = TOKEN_REFRESH_BUFFER_SECONDS)
139
139
  }
140
140
 
141
141
  // Credentials management
142
+
143
+ // Create a directory owner-only (0o700). Under the default umask (0o022) a bare
144
+ // recursive mkdir yields 0o755, leaving ~/.atris and its profiles/sessions
145
+ // world-traversable so any local user can enumerate tokens and workspace ids.
146
+ function mkPrivateDir(dir) {
147
+ if (!fs.existsSync(dir)) {
148
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
149
+ }
150
+ try {
151
+ fs.chmodSync(dir, 0o700);
152
+ } catch {
153
+ // Best effort: permissions may be unsupported on this platform.
154
+ }
155
+ return dir;
156
+ }
157
+
142
158
  function getAtrisDir() {
143
159
  const homeDir = os.homedir();
144
- const atrisDir = path.join(homeDir, '.atris');
145
- if (!fs.existsSync(atrisDir)) {
146
- fs.mkdirSync(atrisDir, { recursive: true });
147
- }
148
- return atrisDir;
160
+ return mkPrivateDir(path.join(homeDir, '.atris'));
149
161
  }
150
162
 
151
163
  function getCredentialsPath() {
@@ -153,11 +165,7 @@ function getCredentialsPath() {
153
165
  }
154
166
 
155
167
  function getSessionsDir() {
156
- const dir = path.join(getAtrisDir(), 'sessions');
157
- if (!fs.existsSync(dir)) {
158
- fs.mkdirSync(dir, { recursive: true });
159
- }
160
- return dir;
168
+ return mkPrivateDir(path.join(getAtrisDir(), 'sessions'));
161
169
  }
162
170
 
163
171
  function getTerminalSessionId() {
@@ -248,11 +256,7 @@ function cleanStaleSessions() {
248
256
  }
249
257
 
250
258
  function getProfilesDir() {
251
- const dir = path.join(getAtrisDir(), 'profiles');
252
- if (!fs.existsSync(dir)) {
253
- fs.mkdirSync(dir, { recursive: true });
254
- }
255
- return dir;
259
+ return mkPrivateDir(path.join(getAtrisDir(), 'profiles'));
256
260
  }
257
261
 
258
262
  function profileNameFromEmail(email) {
@@ -342,7 +346,7 @@ function loadCredentials() {
342
346
  const profileOverride = process.env.ATRIS_PROFILE;
343
347
  if (profileOverride) {
344
348
  const profile = loadProfile(profileOverride);
345
- if (profile) return profile;
349
+ if (profile) return { ...profile, source_profile: profileOverride };
346
350
  const profiles = listProfiles();
347
351
  const q = profileOverride.toLowerCase();
348
352
  const match = profiles.find(p => p.toLowerCase() === q)
@@ -350,7 +354,7 @@ function loadCredentials() {
350
354
  || profiles.find(p => p.toLowerCase().includes(q));
351
355
  if (match) {
352
356
  const matched = loadProfile(match);
353
- if (matched) return matched;
357
+ if (matched) return { ...matched, source_profile: match };
354
358
  }
355
359
  }
356
360
 
@@ -358,7 +362,7 @@ function loadCredentials() {
358
362
  const sessionProfile = getSessionProfile();
359
363
  if (sessionProfile) {
360
364
  const profile = loadProfile(sessionProfile);
361
- if (profile) return profile;
365
+ if (profile) return { ...profile, source_profile: sessionProfile };
362
366
  }
363
367
 
364
368
  // 3. Global credentials.json
@@ -438,7 +442,27 @@ async function performTokenRefresh(credentials, apiRequestJson) {
438
442
  const email = refreshUser?.email || credentials.email;
439
443
  const userId = refreshUser?.id || credentials.user_id;
440
444
 
441
- saveCredentials(accessToken, newRefreshToken, email, userId, provider);
445
+ // Refreshed tokens must land in the file they were loaded from. Writing a
446
+ // profile's refresh to the global credentials.json leaves the profile token
447
+ // to expire permanently (every ATRIS_PROFILE session then 401s forever).
448
+ const persistRefreshed = () => {
449
+ if (credentials.source_profile) {
450
+ const { source_profile, source, ...rest } = { ...credentials };
451
+ saveProfile(credentials.source_profile, {
452
+ ...rest,
453
+ token: accessToken,
454
+ refresh_token: newRefreshToken,
455
+ email,
456
+ user_id: userId,
457
+ provider,
458
+ saved_at: new Date().toISOString(),
459
+ });
460
+ } else {
461
+ saveCredentials(accessToken, newRefreshToken, email, userId, provider);
462
+ }
463
+ };
464
+
465
+ persistRefreshed();
442
466
  let latestCreds = loadCredentials();
443
467
 
444
468
  const validation = await validateAccessToken(accessToken, apiRequestJson);
@@ -456,7 +480,20 @@ async function performTokenRefresh(credentials, apiRequestJson) {
456
480
  updatedProvider !== latestCreds.provider ||
457
481
  updatedUserId !== latestCreds.user_id
458
482
  ) {
459
- saveCredentials(accessToken, newRefreshToken, updatedEmail, updatedUserId, updatedProvider);
483
+ if (credentials.source_profile) {
484
+ const { source_profile, source, ...rest } = { ...credentials };
485
+ saveProfile(credentials.source_profile, {
486
+ ...rest,
487
+ token: accessToken,
488
+ refresh_token: newRefreshToken,
489
+ email: updatedEmail,
490
+ user_id: updatedUserId,
491
+ provider: updatedProvider,
492
+ saved_at: new Date().toISOString(),
493
+ });
494
+ } else {
495
+ saveCredentials(accessToken, newRefreshToken, updatedEmail, updatedUserId, updatedProvider);
496
+ }
460
497
  latestCreds = loadCredentials();
461
498
  }
462
499
  }
@@ -568,7 +605,6 @@ async function displayAccountSummary(apiRequestJson) {
568
605
  console.log(`User ID: ${userId}`);
569
606
  console.log(`Provider: ${provider}`);
570
607
  console.log(`Credentials saved: ${savedAt}`);
571
- console.log(`Credential file: ${getCredentialsPath()}`);
572
608
 
573
609
  try {
574
610
  const agentsResponse = await fetchMyAgents(credentials.token, apiRequestJson);