atris 3.13.0 → 3.14.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/lib/todo.js CHANGED
@@ -1,204 +1,119 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
-
4
- /**
5
- * Parse TODO.md into structured task objects.
6
- * Supports format: - **T1:** Description
7
- * with optional **Claimed by:** and **Stage:** lines
8
- */
9
- function parseTodo(todoPath) {
10
- if (!fs.existsSync(todoPath)) return { backlog: [], inProgress: [], completed: [] };
11
-
12
- const content = fs.readFileSync(todoPath, 'utf8');
1
+ // SHIM. Same export surface as the legacy markdown parser, but reads from the
2
+ // SQLite task store first when ATRIS_TASK_DB=1 is set, falling back to the
3
+ // pure markdown parser at lib/todo-fallback.js.
4
+ //
5
+ // All 3 callers (commands/autopilot.js, commands/status.js, commands/run.js)
6
+ // inherit the strangler without changing their import path.
7
+ //
8
+ // When ATRIS_TASK_DB=1:
9
+ // parseTodo({path}) returns DB-derived tasks first; markdown rows whose
10
+ // title doesn't already exist in DB are appended (so import is gradual).
11
+ // Otherwise: behaves exactly like the old parser.
12
+
13
+ 'use strict';
14
+
15
+ const fallback = require('./todo-fallback');
16
+
17
+ const TASK_DB_ENABLED = process.env.ATRIS_TASK_DB === '1';
18
+
19
+ function dbToShimRow(row) {
20
+ const metadata = row.metadata && typeof row.metadata === 'object' ? row.metadata : {};
21
+ const verify = typeof metadata.verify === 'string' && metadata.verify.trim()
22
+ ? metadata.verify.trim()
23
+ : null;
24
+ const claimed = row.claimed_by || metadata.claimed || null;
25
+ // Map DB row → the shape the existing consumers expect from parseTodo().
13
26
  return {
14
- backlog: parseSection(content, 'Backlog'),
15
- inProgress: parseSection(content, 'In Progress'),
16
- completed: parseSection(content, 'Completed'),
27
+ id: row.id,
28
+ title: row.title,
29
+ tag: row.tag || null,
30
+ tags: row.tag ? [row.tag] : [],
31
+ claimed,
32
+ stage: row.status === 'claimed' ? 'in_progress' : (metadata.stage || null),
33
+ verify,
34
+ _source: 'db',
17
35
  };
18
36
  }
19
37
 
20
- /**
21
- * Parse a specific section from TODO.md content into task objects.
22
- * @param {string} content - Full TODO.md content
23
- * @param {string} sectionName - Section name to extract (e.g., 'Backlog')
24
- * @returns {Array} Array of parsed task objects
25
- */
26
- function parseSection(content, sectionName) {
27
- const escaped = sectionName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
28
- const match = content.match(new RegExp(`##\\s+${escaped}\\n([\\s\\S]*?)(?=\\n##|$)`, 'i'));
29
- if (!match) return [];
30
-
31
- const body = (match[1] || '').trim();
32
- if (!body || /^\(clean\)/i.test(body) || /^\(empty/i.test(body) || /^\(see /i.test(body)) return [];
33
-
34
- const tasks = [];
35
- const lines = body.split('\n');
36
- let current = null;
37
-
38
- for (const rawLine of lines) {
39
- const line = rawLine.trimEnd();
40
-
41
- // New task line: - **T1:** Description or - **T1a:** Description [tag] [tag]
42
- // Accepts task IDs like T1, W3b, M12c, R1, T#1 — letter(s), optional symbols, digits, optional trailing letter.
43
- const taskMatch = line.match(/^- \*\*([A-Za-z][A-Za-z0-9#]*\d[a-z]?):\*\*\s*(.+)$/);
44
- if (taskMatch) {
45
- if (current) tasks.push(current);
46
- // Capture ALL bracketed tags in the line, not just the last one. Endgame is priority.
47
- const allTags = [...taskMatch[2].matchAll(/\[(\w+)\]/g)].map(m => m[1]);
48
- const tag = allTags.includes('endgame') ? 'endgame' : (allTags[0] || null);
49
- current = {
50
- id: taskMatch[1],
51
- title: taskMatch[2].replace(/\s*\[\w+\]/g, '').trim(),
52
- tag,
53
- tags: allTags,
54
- claimed: null,
55
- stage: null,
56
- verify: null,
57
- };
58
- continue;
59
- }
60
-
61
- // Also support checkbox format: - [x] Description
62
- const checkMatch = line.match(/^- \[[ x]\]\s+(.+)$/);
63
- if (checkMatch && !current) {
64
- tasks.push({
65
- id: null,
66
- title: checkMatch[1].trim(),
67
- tag: null,
68
- claimed: null,
69
- stage: null,
70
- verify: null,
71
- });
72
- continue;
73
- }
74
-
75
- // Plain bullet without ID: - Description
76
- const plainMatch = line.match(/^- (.+)$/);
77
- if (plainMatch && !current && !plainMatch[1].startsWith('**')) {
78
- tasks.push({
79
- id: null,
80
- title: plainMatch[1].trim(),
81
- tag: null,
82
- claimed: null,
83
- stage: null,
84
- verify: null,
85
- });
86
- continue;
87
- }
88
-
89
- if (!current) continue;
90
-
91
- // Claimed by line
92
- const claimMatch = line.match(/\*\*Claimed by:\*\*\s*(.+)$/) || line.match(/Claimed by:\s*(.+)$/);
93
- if (claimMatch) {
94
- current.claimed = claimMatch[1].trim();
95
- continue;
96
- }
97
-
98
- // Stage line
99
- const stageMatch = line.match(/\*\*Stage:\*\*\s*(.+)$/) || line.match(/Stage:\s*(.+)$/);
100
- if (stageMatch) {
101
- current.stage = stageMatch[1].trim();
102
- continue;
103
- }
38
+ function dbBuckets(workspaceRoot) {
39
+ const taskDb = require('./task-db');
40
+ const db = taskDb.open();
41
+ const rows = taskDb.listTasks(db, { workspaceRoot, limit: 500 });
42
+ // Need raw source_key for merge dedup, plus the shim shape for callers.
43
+ const backlog = [];
44
+ const inProgress = [];
45
+ const completed = [];
46
+ const sourceKeys = new Set();
47
+ // Query directly so the shim can dedup against markdown by the strong key
48
+ // even if future list filters hide rows.
49
+ const stmt = db.prepare('SELECT id, source_key FROM tasks WHERE workspace_root = ? AND source_key IS NOT NULL');
50
+ for (const r of stmt.all(workspaceRoot)) sourceKeys.add(r.source_key);
51
+ for (const r of rows) {
52
+ const shaped = dbToShimRow(r);
53
+ if (r.status === 'open') backlog.push(shaped);
54
+ else if (r.status === 'claimed') inProgress.push(shaped);
55
+ else if (r.status === 'done' || r.status === 'failed') completed.push(shaped);
56
+ }
57
+ return { backlog, inProgress, completed, sourceKeys };
58
+ }
104
59
 
105
- // Verify line
106
- const verifyMatch = line.match(/\*\*Verify:\*\*\s*(.+)$/) || line.match(/Verify:\s*(.+)$/);
107
- if (verifyMatch) {
108
- current.verify = verifyMatch[1].trim();
109
- continue;
60
+ function mergeBuckets(dbBuck, mdBuck, todoPath) {
61
+ // Append markdown rows that aren't already in the DB. Dedup by source_key
62
+ // (strong: same source_file + normalized_title hash) first, and by
63
+ // normalized_title as a compatibility fallback for legacy markdown rows
64
+ // that pre-date the import path.
65
+ const taskDb = require('./task-db');
66
+ const norm = (t) => String(t || '').toLowerCase().trim().replace(/\s+/g, ' ');
67
+ const seenTitles = new Set();
68
+ const out = { backlog: [], inProgress: [], completed: [] };
69
+ for (const k of ['backlog', 'inProgress', 'completed']) {
70
+ for (const r of dbBuck[k]) { out[k].push(r); seenTitles.add(norm(r.title)); }
71
+ for (const r of mdBuck[k]) {
72
+ const sk = todoPath ? taskDb.sourceKey(todoPath, r.title) : null;
73
+ if (sk && dbBuck.sourceKeys && dbBuck.sourceKeys.has(sk)) continue;
74
+ if (seenTitles.has(norm(r.title))) continue;
75
+ out[k].push({ ...r, _source: 'md' });
110
76
  }
111
77
  }
112
-
113
- if (current) tasks.push(current);
114
- return tasks;
78
+ return out;
115
79
  }
116
80
 
117
- /**
118
- * Get latest journal entry for a team member.
119
- * Reads the most recent file in atris/team/[member]/journal/
120
- * Returns { date, entries[] } or null
121
- */
122
- function getTeamMemberJournal(atrisDir, memberName) {
123
- const journalDir = path.join(atrisDir, 'team', memberName, 'journal');
124
- if (!fs.existsSync(journalDir)) return null;
125
-
126
- let files;
81
+ function parseTodo(todoPath) {
82
+ // Legacy pure-markdown path.
83
+ if (!TASK_DB_ENABLED) return fallback.parseTodoFile(todoPath);
84
+
85
+ // DB-first merged view. Workspace scope = directory containing todoPath
86
+ // (or its parent), so the DB stays per-repo.
87
+ const path = require('path');
88
+ const fs = require('fs');
89
+ const taskDb = require('./task-db');
90
+ let workspaceRoot;
127
91
  try {
128
- files = fs.readdirSync(journalDir)
129
- .filter(f => f.endsWith('.md'))
130
- .sort()
131
- .reverse();
92
+ const todoAbs = path.resolve(todoPath);
93
+ // typical layout: <repo>/atris/TODO.md → workspace = <repo>
94
+ const guess = path.dirname(path.dirname(todoAbs));
95
+ // Normalize via taskDb so it matches what `task add` writes.
96
+ workspaceRoot = fs.existsSync(guess) ? taskDb.workspaceRoot(guess) : taskDb.workspaceRoot();
132
97
  } catch {
133
- return null;
98
+ workspaceRoot = taskDb.workspaceRoot();
134
99
  }
135
100
 
136
- if (files.length === 0) return null;
137
-
138
- const latestFile = files[0];
139
- let content;
101
+ let dbBuck;
140
102
  try {
141
- content = fs.readFileSync(path.join(journalDir, latestFile), 'utf8');
142
- } catch {
143
- return null;
144
- }
145
- const date = latestFile.replace('.md', '');
146
-
147
- // Extract key fields from journal entry
148
- const taskMatch = content.match(/\*\*Task:\*\*\s*(.+)/);
149
- const deliveredMatch = content.match(/\*\*Delivered:\*\*\s*(.+)/);
150
- const patternMatch = content.match(/\*\*Pattern:\*\*\s*(.+)/);
151
- const learnedMatch = content.match(/\*\*Learned:\*\*\s*(.+)/);
152
-
153
- return {
154
- date,
155
- file: path.join(journalDir, latestFile),
156
- task: taskMatch ? taskMatch[1].trim() : null,
157
- delivered: deliveredMatch ? deliveredMatch[1].trim() : null,
158
- pattern: patternMatch ? patternMatch[1].trim() : null,
159
- learned: learnedMatch ? learnedMatch[1].trim() : null,
160
- raw: content,
161
- };
162
- }
163
-
164
- /**
165
- * Get all team members that have directories in atris/team/
166
- */
167
- function listTeamMembers(atrisDir) {
168
- const teamDir = path.join(atrisDir, 'team');
169
- if (!fs.existsSync(teamDir)) return [];
170
-
171
- return fs.readdirSync(teamDir)
172
- .filter(name => {
173
- if (name.startsWith('_') || name.startsWith('.')) return false;
174
- const full = path.join(teamDir, name);
175
- try { return fs.statSync(full).isDirectory(); } catch { return false; }
176
- });
177
- }
178
-
179
- /**
180
- * Get team activity: latest journal entry per member
181
- */
182
- function getTeamActivity(atrisDir) {
183
- const members = listTeamMembers(atrisDir);
184
- const activity = [];
185
-
186
- for (const member of members) {
187
- const journal = getTeamMemberJournal(atrisDir, member);
188
- if (journal) {
189
- activity.push({ member, ...journal });
190
- }
103
+ dbBuck = dbBuckets(workspaceRoot);
104
+ } catch (e) {
105
+ // If sqlite blew up (missing in node, perms), don't break the legacy path.
106
+ if (process.env.ATRIS_DEBUG) console.error('[todo shim] db read failed:', e.message);
107
+ return fallback.parseTodoFile(todoPath);
191
108
  }
192
-
193
- // Sort by date descending (most recent first)
194
- activity.sort((a, b) => b.date.localeCompare(a.date));
195
- return activity;
109
+ const mdBuck = fallback.parseTodoFile(todoPath);
110
+ return mergeBuckets(dbBuck, mdBuck, todoPath);
196
111
  }
197
112
 
198
113
  module.exports = {
199
114
  parseTodo,
200
- parseSection,
201
- getTeamMemberJournal,
202
- listTeamMembers,
203
- getTeamActivity,
115
+ parseSection: fallback.parseSection,
116
+ getTeamMemberJournal: fallback.getTeamMemberJournal,
117
+ listTeamMembers: fallback.listTeamMembers,
118
+ getTeamActivity: fallback.getTeamActivity,
204
119
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atris",
3
- "version": "3.13.0",
3
+ "version": "3.14.0",
4
4
  "description": "Atris — an operating system for intelligence. Integrates with any agent.",
5
5
  "main": "bin/atris.js",
6
6
  "bin": {
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "files": [
10
10
  "bin/",
11
- "cli/",
11
+ "cli/*.py",
12
12
  "commands/",
13
13
  "utils/",
14
14
  "lib/",