atris 2.6.3 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +124 -34
  2. package/atris/CLAUDE.md +5 -1
  3. package/atris/atris.md +4 -0
  4. package/atris/features/README.md +24 -0
  5. package/atris/skills/autopilot/SKILL.md +74 -75
  6. package/atris/skills/endgame/SKILL.md +179 -0
  7. package/atris/skills/flow/SKILL.md +121 -0
  8. package/atris/skills/improve/SKILL.md +84 -0
  9. package/atris/skills/loop/SKILL.md +72 -0
  10. package/atris/skills/wiki/SKILL.md +61 -0
  11. package/atris/team/executor/MEMBER.md +10 -4
  12. package/atris/team/navigator/MEMBER.md +2 -0
  13. package/atris/team/validator/MEMBER.md +8 -5
  14. package/atris.md +33 -0
  15. package/bin/atris.js +210 -41
  16. package/commands/activate.js +28 -2
  17. package/commands/align.js +720 -0
  18. package/commands/auth.js +75 -2
  19. package/commands/autopilot.js +1213 -270
  20. package/commands/browse.js +100 -0
  21. package/commands/business.js +785 -12
  22. package/commands/clean.js +107 -2
  23. package/commands/computer.js +429 -0
  24. package/commands/context-sync.js +78 -8
  25. package/commands/experiments.js +351 -0
  26. package/commands/feedback.js +150 -0
  27. package/commands/fleet.js +395 -0
  28. package/commands/fork.js +127 -0
  29. package/commands/init.js +50 -1
  30. package/commands/learn.js +407 -0
  31. package/commands/lifecycle.js +94 -0
  32. package/commands/loop.js +114 -0
  33. package/commands/publish.js +129 -0
  34. package/commands/pull.js +369 -38
  35. package/commands/push.js +283 -246
  36. package/commands/review.js +149 -0
  37. package/commands/run.js +76 -43
  38. package/commands/serve.js +360 -0
  39. package/commands/setup.js +1 -1
  40. package/commands/soul.js +381 -0
  41. package/commands/status.js +119 -1
  42. package/commands/sync.js +147 -1
  43. package/commands/terminal.js +201 -0
  44. package/commands/wiki.js +376 -0
  45. package/commands/workflow.js +191 -74
  46. package/commands/workspace-clean.js +3 -3
  47. package/lib/endstate.js +259 -0
  48. package/lib/learnings.js +235 -0
  49. package/lib/manifest.js +1 -0
  50. package/lib/todo.js +9 -5
  51. package/lib/wiki.js +578 -0
  52. package/package.json +2 -2
  53. package/utils/api.js +40 -35
  54. package/utils/auth.js +1 -0
@@ -0,0 +1,235 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const TYPES = ['pattern', 'pitfall', 'preference', 'architecture', 'tool'];
5
+ const SOURCES = ['observed', 'user-stated', 'inferred', 'review'];
6
+
7
+ function getLearningsPath() {
8
+ return path.join(process.cwd(), 'atris', 'learnings.jsonl');
9
+ }
10
+
11
+ function ensureLearningsFile() {
12
+ const filePath = getLearningsPath();
13
+ const dir = path.dirname(filePath);
14
+ if (!fs.existsSync(dir)) {
15
+ fs.mkdirSync(dir, { recursive: true });
16
+ }
17
+ if (!fs.existsSync(filePath)) {
18
+ fs.writeFileSync(filePath, '');
19
+ }
20
+ return filePath;
21
+ }
22
+
23
+ /**
24
+ * Confidence decay: observed/inferred lose 1 point per 30 days.
25
+ * User-stated preferences never decay.
26
+ */
27
+ function effectiveConfidence(entry) {
28
+ let conf = entry.confidence || 5;
29
+ if (entry.source === 'observed' || entry.source === 'inferred') {
30
+ const days = Math.floor((Date.now() - new Date(entry.ts).getTime()) / 86400000);
31
+ conf = Math.max(0, conf - Math.floor(days / 30));
32
+ }
33
+ return conf;
34
+ }
35
+
36
+ /**
37
+ * Read all learnings, dedup (latest per key+type wins), apply decay.
38
+ */
39
+ function loadLearnings() {
40
+ const filePath = getLearningsPath();
41
+ if (!fs.existsSync(filePath)) return [];
42
+
43
+ const lines = fs.readFileSync(filePath, 'utf8').trim().split('\n').filter(Boolean);
44
+ const entries = [];
45
+
46
+ for (const line of lines) {
47
+ try {
48
+ const entry = JSON.parse(line);
49
+ if (!entry.key || !entry.type) continue;
50
+ entry._effectiveConfidence = effectiveConfidence(entry);
51
+ entries.push(entry);
52
+ } catch {
53
+ // skip corrupted lines
54
+ }
55
+ }
56
+
57
+ // Dedup: last occurrence per key+type wins (append-only means last line is newest)
58
+ const seen = new Map();
59
+ for (const entry of entries) {
60
+ const dk = `${entry.key}|${entry.type}`;
61
+ seen.set(dk, entry);
62
+ }
63
+
64
+ return Array.from(seen.values())
65
+ .sort((a, b) => {
66
+ if (b._effectiveConfidence !== a._effectiveConfidence) {
67
+ return b._effectiveConfidence - a._effectiveConfidence;
68
+ }
69
+ return new Date(b.ts).getTime() - new Date(a.ts).getTime();
70
+ });
71
+ }
72
+
73
+ /**
74
+ * Add a learning entry. Append-only.
75
+ */
76
+ function addLearning({ type, key, insight, confidence, source, files }) {
77
+ if (!TYPES.includes(type)) {
78
+ throw new Error(`Invalid type: ${type}. Must be one of: ${TYPES.join(', ')}`);
79
+ }
80
+ if (!SOURCES.includes(source)) {
81
+ throw new Error(`Invalid source: ${source}. Must be one of: ${SOURCES.join(', ')}`);
82
+ }
83
+ if (confidence < 1 || confidence > 10) {
84
+ throw new Error('Confidence must be 1-10');
85
+ }
86
+ if (!key || !insight) {
87
+ throw new Error('Key and insight are required');
88
+ }
89
+
90
+ const filePath = ensureLearningsFile();
91
+ const entry = {
92
+ ts: new Date().toISOString(),
93
+ type,
94
+ key: key.toLowerCase().replace(/\s+/g, '-'),
95
+ insight,
96
+ confidence,
97
+ source,
98
+ files: files || [],
99
+ };
100
+
101
+ fs.appendFileSync(filePath, JSON.stringify(entry) + '\n');
102
+ return entry;
103
+ }
104
+
105
+ /**
106
+ * Search learnings by keyword.
107
+ */
108
+ function searchLearnings(query, limit = 20) {
109
+ const all = loadLearnings();
110
+ if (!query) return all.slice(0, limit);
111
+
112
+ const q = query.toLowerCase();
113
+ return all
114
+ .filter(e =>
115
+ (e.key || '').toLowerCase().includes(q) ||
116
+ (e.insight || '').toLowerCase().includes(q) ||
117
+ (e.files || []).some(f => f.toLowerCase().includes(q))
118
+ )
119
+ .slice(0, limit);
120
+ }
121
+
122
+ /**
123
+ * Prune: find stale (deleted files) and contradictions (same key, different insight).
124
+ */
125
+ function findPruneTargets() {
126
+ const all = loadLearnings();
127
+ const stale = [];
128
+ const contradictions = [];
129
+
130
+ // Stale: referenced files no longer exist
131
+ for (const entry of all) {
132
+ if (entry.files && entry.files.length > 0) {
133
+ const missing = entry.files.filter(f => !fs.existsSync(path.join(process.cwd(), f)));
134
+ if (missing.length > 0) {
135
+ stale.push({ entry, missingFiles: missing });
136
+ }
137
+ }
138
+ }
139
+
140
+ // Contradictions: same key, different type entries with conflicting insights
141
+ const byKey = new Map();
142
+ for (const entry of all) {
143
+ const existing = byKey.get(entry.key);
144
+ if (existing && existing.type === entry.type && existing.insight !== entry.insight) {
145
+ contradictions.push({ a: existing, b: entry });
146
+ }
147
+ byKey.set(entry.key, entry);
148
+ }
149
+
150
+ return { stale, contradictions };
151
+ }
152
+
153
+ /**
154
+ * Remove a learning by key+type (appends a tombstone with confidence 0).
155
+ */
156
+ function removeLearning(key, type) {
157
+ const filePath = ensureLearningsFile();
158
+ const entry = {
159
+ ts: new Date().toISOString(),
160
+ type,
161
+ key,
162
+ insight: '[REMOVED]',
163
+ confidence: 0,
164
+ source: 'user-stated',
165
+ files: [],
166
+ };
167
+ fs.appendFileSync(filePath, JSON.stringify(entry) + '\n');
168
+ }
169
+
170
+ /**
171
+ * Get stats about learnings.
172
+ */
173
+ function getStats() {
174
+ const all = loadLearnings();
175
+ const active = all.filter(e => e._effectiveConfidence > 0 && e.insight !== '[REMOVED]');
176
+ const byType = {};
177
+ const bySource = {};
178
+ let totalConf = 0;
179
+
180
+ for (const e of active) {
181
+ byType[e.type] = (byType[e.type] || 0) + 1;
182
+ bySource[e.source] = (bySource[e.source] || 0) + 1;
183
+ totalConf += e._effectiveConfidence;
184
+ }
185
+
186
+ return {
187
+ total: active.length,
188
+ byType,
189
+ bySource,
190
+ avgConfidence: active.length > 0 ? (totalConf / active.length).toFixed(1) : 0,
191
+ high: active.filter(e => e._effectiveConfidence >= 7).length,
192
+ medium: active.filter(e => e._effectiveConfidence >= 4 && e._effectiveConfidence < 7).length,
193
+ low: active.filter(e => e._effectiveConfidence > 0 && e._effectiveConfidence < 4).length,
194
+ };
195
+ }
196
+
197
+ /**
198
+ * Export learnings as markdown.
199
+ */
200
+ function exportMarkdown() {
201
+ const all = loadLearnings().filter(e => e._effectiveConfidence > 0 && e.insight !== '[REMOVED]');
202
+ const byType = {};
203
+
204
+ for (const e of all) {
205
+ if (!byType[e.type]) byType[e.type] = [];
206
+ byType[e.type].push(e);
207
+ }
208
+
209
+ const lines = ['## Project Learnings', ''];
210
+ for (const [type, entries] of Object.entries(byType)) {
211
+ lines.push(`### ${type.charAt(0).toUpperCase() + type.slice(1)}s`);
212
+ for (const e of entries) {
213
+ const files = e.files?.length ? ` (${e.files.join(', ')})` : '';
214
+ lines.push(`- **${e.key}** [${e._effectiveConfidence}/10]: ${e.insight}${files}`);
215
+ }
216
+ lines.push('');
217
+ }
218
+
219
+ return lines.join('\n');
220
+ }
221
+
222
+ module.exports = {
223
+ TYPES,
224
+ SOURCES,
225
+ getLearningsPath,
226
+ ensureLearningsFile,
227
+ effectiveConfidence,
228
+ loadLearnings,
229
+ addLearning,
230
+ searchLearnings,
231
+ findPruneTargets,
232
+ removeLearning,
233
+ getStats,
234
+ exportMarkdown,
235
+ };
package/lib/manifest.js CHANGED
@@ -77,6 +77,7 @@ function computeLocalHashes(localDir) {
77
77
  for (const entry of entries) {
78
78
  if (entry.name.startsWith('.')) continue;
79
79
  const fullPath = path.join(dir, entry.name);
80
+ const relFromRoot = path.relative(localDir, fullPath);
80
81
  if (entry.isDirectory()) {
81
82
  if (SKIP_DIRS.has(entry.name)) continue;
82
83
  walk(fullPath);
package/lib/todo.js CHANGED
@@ -32,15 +32,19 @@ function parseSection(content, sectionName) {
32
32
  for (const rawLine of lines) {
33
33
  const line = rawLine.trimEnd();
34
34
 
35
- // New task line: - **T1:** Description or - **T1:** Description [tag]
36
- const taskMatch = line.match(/^- \*\*([A-Z]\d+):\*\*\s*(.+)$/);
35
+ // New task line: - **T1:** Description or - **T1a:** Description [tag] [tag]
36
+ // Accepts task IDs like T1, W3b, M12c — single uppercase letter, digits, optional trailing lowercase letter.
37
+ const taskMatch = line.match(/^- \*\*([A-Z]\d+[a-z]?):\*\*\s*(.+)$/);
37
38
  if (taskMatch) {
38
39
  if (current) tasks.push(current);
39
- const tagMatch = taskMatch[2].match(/\[(\w+)\]\s*$/);
40
+ // Capture ALL bracketed tags in the line, not just the last one. Endgame is priority.
41
+ const allTags = [...taskMatch[2].matchAll(/\[(\w+)\]/g)].map(m => m[1]);
42
+ const tag = allTags.includes('endgame') ? 'endgame' : (allTags[0] || null);
40
43
  current = {
41
44
  id: taskMatch[1],
42
- title: tagMatch ? taskMatch[2].replace(/\s*\[\w+\]\s*$/, '').trim() : taskMatch[2].trim(),
43
- tag: tagMatch ? tagMatch[1] : null,
45
+ title: taskMatch[2].replace(/\s*\[\w+\]/g, '').trim(),
46
+ tag,
47
+ tags: allTags,
44
48
  claimed: null,
45
49
  stage: null,
46
50
  };