atris 3.58.6 → 3.58.8
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/GETTING_STARTED.md +2 -2
- package/PERSONA.md +11 -11
- package/README.md +9 -1
- package/atris/GEMINI.md +7 -6
- package/atris/PERSONA.md +4 -4
- package/atris/features/README.md +77 -6
- package/atris/policies/engineering-principles.md +129 -0
- package/atris/policies/genesis.md +112 -0
- package/atris/policies/product-design-principles.md +100 -0
- package/atris/skills/README.md +44 -15
- package/atris/skills/aeo/SKILL.md +10 -10
- package/atris/skills/atris/SKILL.md +1 -1
- package/atris/skills/design/SKILL.md +3 -1
- package/atris/skills/engines/SKILL.md +42 -50
- package/atris/skills/meta/SKILL.md +4 -4
- package/atris/skills/youtube/SKILL.md +11 -11
- package/bin/atris.js +60 -8
- package/commands/aeo.js +9 -12
- package/commands/analytics.js +8 -11
- package/commands/auth.js +8 -2
- package/commands/autopilot.js +4 -3
- package/commands/avail.js +1 -1
- package/commands/brain.js +4 -1
- package/commands/business.js +13 -8
- package/commands/close.js +2 -1
- package/commands/computer.js +15 -10
- package/commands/context-sync.js +3 -3
- package/commands/design.js +362 -0
- package/commands/doc-health.js +377 -0
- package/commands/drive.js +32 -0
- package/commands/experiments.js +13 -4
- package/commands/improve.js +68 -2
- package/commands/integrations.js +2 -5
- package/commands/land.js +144 -4
- package/commands/learn.js +7 -2
- package/commands/member.js +239 -58
- package/commands/mission.js +94 -18
- package/commands/pack.js +2 -2
- package/commands/probe.js +11 -5
- package/commands/pull.js +1 -1
- package/commands/pulse.js +38 -0
- package/commands/push.js +1 -1
- package/commands/rainmaker.js +3 -16
- package/commands/rsi.js +156 -0
- package/commands/spaceship.js +1 -1
- package/commands/stream.js +27 -2
- package/commands/study.js +15 -9
- package/commands/sync.js +1 -1
- package/commands/task.js +54 -3
- package/commands/workflow.js +17 -7
- package/commands/youtube.js +76 -8
- package/lib/apply-gate.js +22 -4
- package/lib/codex-flight.js +28 -5
- package/lib/daily-log.js +88 -0
- package/lib/design-api.js +130 -0
- package/lib/engine-ask.js +1 -1
- package/lib/first-minute.js +1 -6
- package/lib/fleet.js +4 -1
- package/lib/improve-vitals-html.js +3 -2
- package/lib/known-commands.js +3 -3
- package/lib/lesson-contradiction.js +2 -2
- package/lib/map-refs.js +183 -0
- package/lib/member-context.js +42 -0
- package/lib/mission-room.js +4 -4
- package/lib/noninteractive.js +15 -4
- package/lib/policy-lessons.js +1 -1
- package/lib/receipt-block.js +137 -28
- package/lib/rsi-record.js +335 -0
- package/lib/runner-command.js +1 -1
- package/lib/scorecard.js +4 -4
- package/lib/sleep-sync.js +11 -0
- package/lib/state-detection.js +36 -17
- package/lib/task-db.js +74 -53
- package/lib/task-list-keeper.js +192 -0
- package/lib/todo-fallback.js +10 -4
- package/lib/todo.js +22 -10
- package/lib/wish-delegate.js +19 -5
- package/mcp/atris-mcp/index.mjs +227 -0
- package/package.json +4 -2
- package/scripts/det/det.js +2 -2
- package/scripts/det/pr-description.js +2 -2
- package/scripts/det/test.js +2 -2
- package/scripts/det/ytnotes +43 -8
- package/utils/api.js +1 -1
- package/utils/auth.js +79 -14
- package/utils/backend-root.js +31 -0
- package/utils/config.js +5 -4
- package/utils/owner-identity.js +61 -0
- package/atris/policies/LESSONS.md +0 -13
- package/atris/skills/autopilot/atris-autopilot.md +0 -71
package/lib/state-detection.js
CHANGED
|
@@ -121,11 +121,7 @@ function getTasksFromTodoSection(content, sectionName) {
|
|
|
121
121
|
|
|
122
122
|
if (tasks.length > 0) return tasks;
|
|
123
123
|
|
|
124
|
-
return
|
|
125
|
-
.split('\n')
|
|
126
|
-
.map((line) => line.trim())
|
|
127
|
-
.filter((line) => /^-\s+/.test(line) && !/\(empty/i.test(line))
|
|
128
|
-
.map((line) => line.replace(/^-+\s*/, '').trim());
|
|
124
|
+
return require('./todo-fallback').parseSection(content, sectionName).map(task => task.title);
|
|
129
125
|
}
|
|
130
126
|
|
|
131
127
|
function getTasksFromDbBucket(atrisDir, bucketName) {
|
|
@@ -149,7 +145,7 @@ function getTasksFromDbBucket(atrisDir, bucketName) {
|
|
|
149
145
|
|
|
150
146
|
// Single-read task lane counts for boot/status surfaces.
|
|
151
147
|
// Prefers the task DB (source of truth); falls back to TODO.md parsing.
|
|
152
|
-
//
|
|
148
|
+
// Generated markdown carries the Review lane, but cannot certify its proof.
|
|
153
149
|
function getTaskCounts(atrisDir) {
|
|
154
150
|
try {
|
|
155
151
|
const taskDb = require('./task-db');
|
|
@@ -179,7 +175,7 @@ function getTaskCounts(atrisDir) {
|
|
|
179
175
|
return {
|
|
180
176
|
backlog: getBacklogTasks(atrisDir).length,
|
|
181
177
|
active: getInProgressTasks(atrisDir).length,
|
|
182
|
-
review:
|
|
178
|
+
review: getTasksFromTodoSection(readTodoText(atrisDir), 'Review').length,
|
|
183
179
|
reviewCertified: 0,
|
|
184
180
|
source: 'todo'
|
|
185
181
|
};
|
|
@@ -230,7 +226,7 @@ function getTaskGlance(atrisDir, sampleSize = 3) {
|
|
|
230
226
|
return {
|
|
231
227
|
backlog: backlogTitles.length,
|
|
232
228
|
active: activeTitles.length,
|
|
233
|
-
review:
|
|
229
|
+
review: getTasksFromTodoSection(readTodoText(atrisDir), 'Review').length,
|
|
234
230
|
reviewCertified: 0,
|
|
235
231
|
activeTitles: activeTitles.slice(0, sampleSize),
|
|
236
232
|
backlogTitles: backlogTitles.slice(0, sampleSize),
|
|
@@ -239,6 +235,10 @@ function getTaskGlance(atrisDir, sampleSize = 3) {
|
|
|
239
235
|
};
|
|
240
236
|
}
|
|
241
237
|
|
|
238
|
+
function readTodoText(atrisDir) {
|
|
239
|
+
try { return fs.readFileSync(path.join(atrisDir, 'TODO.md'), 'utf8'); } catch { return ''; }
|
|
240
|
+
}
|
|
241
|
+
|
|
242
242
|
function getTodayInboxItems(workspaceDir) {
|
|
243
243
|
const atrisDir = path.join(workspaceDir, 'atris');
|
|
244
244
|
const logsDir = path.join(atrisDir, 'logs');
|
|
@@ -271,6 +271,32 @@ function getTodayInboxItems(workspaceDir) {
|
|
|
271
271
|
/**
|
|
272
272
|
* Detect workspace state: fresh install, has inbox items, in-progress work, or blocked
|
|
273
273
|
*/
|
|
274
|
+
// Blocked means a task whose status is failed: the DB has no separate
|
|
275
|
+
// blocked status, doneTask logs a failed task as a "blocked" event, and the
|
|
276
|
+
// board renders failed tasks under "## Blocked". Once this workspace has rows
|
|
277
|
+
// in the task DB, the DB is the only source. The DB file is shared by every
|
|
278
|
+
// workspace on the machine, so a workspace with no rows there falls back to
|
|
279
|
+
// the Blocked section of its own TODO.md, never a word elsewhere in a title.
|
|
280
|
+
function hasBlockedTasks(atrisDir, todoFile, legacyTaskContextFile) {
|
|
281
|
+
try {
|
|
282
|
+
const taskDb = require('./task-db');
|
|
283
|
+
if (fs.existsSync(taskDb.getDbPath())) {
|
|
284
|
+
const db = taskDb.open();
|
|
285
|
+
const workspaceRoot = taskDb.workspaceRoot(path.dirname(path.resolve(atrisDir)));
|
|
286
|
+
if (taskDb.listTasks(db, { workspaceRoot, limit: 1 }).length) {
|
|
287
|
+
return taskDb.listTasks(db, { workspaceRoot, status: 'failed', limit: 1 }).length > 0;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
} catch (e) {
|
|
291
|
+
if (process.env.ATRIS_DEBUG) console.error('[state-detection] db blocked read failed:', e.message);
|
|
292
|
+
}
|
|
293
|
+
const taskFilePath = fs.existsSync(todoFile)
|
|
294
|
+
? todoFile
|
|
295
|
+
: (fs.existsSync(legacyTaskContextFile) ? legacyTaskContextFile : null);
|
|
296
|
+
if (!taskFilePath) return false;
|
|
297
|
+
return getTasksFromTodoSection(fs.readFileSync(taskFilePath, 'utf8'), 'Blocked').length > 0;
|
|
298
|
+
}
|
|
299
|
+
|
|
274
300
|
function detectWorkspaceState(workspaceDir) {
|
|
275
301
|
const atrisDir = path.join(workspaceDir, 'atris');
|
|
276
302
|
const featuresDir = path.join(atrisDir, 'features');
|
|
@@ -317,15 +343,8 @@ function detectWorkspaceState(workspaceDir) {
|
|
|
317
343
|
}
|
|
318
344
|
}
|
|
319
345
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
? todoFile
|
|
323
|
-
: (fs.existsSync(legacyTaskContextFile) ? legacyTaskContextFile : null);
|
|
324
|
-
if (taskFilePath) {
|
|
325
|
-
const content = fs.readFileSync(taskFilePath, 'utf8');
|
|
326
|
-
if (content.includes('Risk Level: High') || /\bblocked\b/i.test(content)) {
|
|
327
|
-
return { state: 'blocked', reason: 'Tasks marked as blocked or high risk' };
|
|
328
|
-
}
|
|
346
|
+
if (hasBlockedTasks(atrisDir, todoFile, legacyTaskContextFile)) {
|
|
347
|
+
return { state: 'blocked', reason: 'Tasks marked as blocked' };
|
|
329
348
|
}
|
|
330
349
|
|
|
331
350
|
// Default: setup complete but no active work
|
package/lib/task-db.js
CHANGED
|
@@ -28,11 +28,13 @@ const os = require('os');
|
|
|
28
28
|
const crypto = require('crypto');
|
|
29
29
|
const { DatabaseSync } = require('node:sqlite');
|
|
30
30
|
const reviewIntegrity = require('./review-integrity');
|
|
31
|
+
const dailyLog = require('./daily-log');
|
|
31
32
|
const { isGenericScratchRoot } = require('./scratch-root');
|
|
32
33
|
const { isDecisionTask } = require('./task-decision');
|
|
33
34
|
const { parseVerifyCommand } = require('./auto-accept-certified');
|
|
34
35
|
const { taskExplanation } = require('./task-explanation');
|
|
35
36
|
const { treeHashFor } = require('./tree-hash');
|
|
37
|
+
const { sleepSync } = require('./sleep-sync');
|
|
36
38
|
|
|
37
39
|
const DEFAULT_DB_PATH = path.join(os.homedir(), '.atris', 'tasks.db');
|
|
38
40
|
const TASK_EPISODES_FILE = path.join('.atris', 'state', 'task_episodes.jsonl');
|
|
@@ -67,23 +69,6 @@ const TASK_PLAN_TAGS = new Set([
|
|
|
67
69
|
'ux',
|
|
68
70
|
]);
|
|
69
71
|
|
|
70
|
-
function todayLogName() {
|
|
71
|
-
const now = new Date();
|
|
72
|
-
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}.md`;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function compactLogText(value, max = 240) {
|
|
76
|
-
const text = String(value || '').replace(/\s+/g, ' ').trim();
|
|
77
|
-
if (!text) return '';
|
|
78
|
-
return text.length > max ? `${text.slice(0, Math.max(0, max - 3)).trim()}...` : text;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function logFieldRows(fields) {
|
|
82
|
-
return Object.entries(fields)
|
|
83
|
-
.filter(([, value]) => value !== undefined && value !== null && value !== '')
|
|
84
|
-
.map(([key, value]) => `- ${key}: ${compactLogText(value, 500)}`);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
72
|
function taskMemberCandidates(row, actor) {
|
|
88
73
|
const metadata = row && row.metadata && typeof row.metadata === 'object' ? row.metadata : {};
|
|
89
74
|
return [
|
|
@@ -100,21 +85,22 @@ function taskMemberCandidates(row, actor) {
|
|
|
100
85
|
|
|
101
86
|
function existingMemberSlug(workspaceRoot, row, actor) {
|
|
102
87
|
for (const candidate of taskMemberCandidates(row, actor)) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (fs.existsSync(memberFile)) return candidate;
|
|
88
|
+
const slug = dailyLog.memberSlug(workspaceRoot, candidate);
|
|
89
|
+
if (slug) return slug;
|
|
106
90
|
}
|
|
107
91
|
return null;
|
|
108
92
|
}
|
|
109
93
|
|
|
94
|
+
function taskLogRef(db, row) {
|
|
95
|
+
const allRows = listTasks(db, { workspaceRoot: row.workspace_root });
|
|
96
|
+
return taskDisplayRefMap(allRows).get(row.id) || shortestUniqueTaskRef(row.id, allRows.map(task => task.id), 8) || row.id;
|
|
97
|
+
}
|
|
98
|
+
|
|
110
99
|
function appendTaskCompletionLogs(db, row, { status, actor, action, proof } = {}) {
|
|
111
100
|
if (!row || !row.workspace_root || !fs.existsSync(path.join(row.workspace_root, 'atris'))) return {};
|
|
112
|
-
const
|
|
113
|
-
const stamp = new Date().toTimeString().slice(0, 5);
|
|
114
|
-
const allRows = listTasks(db, { workspaceRoot: row.workspace_root });
|
|
115
|
-
const ref = taskDisplayRefMap(allRows).get(row.id) || shortestUniqueTaskRef(row.id, allRows.map(task => task.id), 8) || row.id;
|
|
101
|
+
const ref = taskLogRef(db, row);
|
|
116
102
|
const metadata = row.metadata && typeof row.metadata === 'object' ? row.metadata : {};
|
|
117
|
-
const proofText = compactLogText(proof || metadata.latest_agent_proof || metadata.verify || '', 500);
|
|
103
|
+
const proofText = dailyLog.compactLogText(proof || metadata.latest_agent_proof || metadata.verify || '', 500);
|
|
118
104
|
const member = existingMemberSlug(row.workspace_root, row, actor);
|
|
119
105
|
const title = status === 'archived' ? 'Task archived'
|
|
120
106
|
: status === 'failed' ? 'Task failed'
|
|
@@ -131,26 +117,10 @@ function appendTaskCompletionLogs(db, row, { status, actor, action, proof } = {}
|
|
|
131
117
|
proof: proofText,
|
|
132
118
|
};
|
|
133
119
|
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
`## ${stamp} · ${title}`,
|
|
139
|
-
...logFieldRows(fields),
|
|
140
|
-
'',
|
|
141
|
-
].join('\n'), 'utf8');
|
|
142
|
-
|
|
143
|
-
let memberLogPath = null;
|
|
144
|
-
if (member) {
|
|
145
|
-
const memberLogsDir = path.join(row.workspace_root, 'atris', 'team', member, 'logs');
|
|
146
|
-
fs.mkdirSync(memberLogsDir, { recursive: true });
|
|
147
|
-
memberLogPath = path.join(memberLogsDir, logName);
|
|
148
|
-
fs.appendFileSync(memberLogPath, [
|
|
149
|
-
`## ${stamp} · ${title}`,
|
|
150
|
-
...logFieldRows({ ...fields, member }),
|
|
151
|
-
'',
|
|
152
|
-
].join('\n'), 'utf8');
|
|
153
|
-
}
|
|
120
|
+
const projectLogPath = dailyLog.appendMasterDailyEntry(row.workspace_root, title, fields);
|
|
121
|
+
const memberLogPath = member
|
|
122
|
+
? dailyLog.appendMemberDailyEntry(row.workspace_root, member, title, fields)
|
|
123
|
+
: null;
|
|
154
124
|
|
|
155
125
|
return {
|
|
156
126
|
project_log_path: projectLogPath,
|
|
@@ -159,6 +129,25 @@ function appendTaskCompletionLogs(db, row, { status, actor, action, proof } = {}
|
|
|
159
129
|
};
|
|
160
130
|
}
|
|
161
131
|
|
|
132
|
+
function appendTaskWorkLog(db, row, { actor, title, note } = {}) {
|
|
133
|
+
if (!row || !row.workspace_root || !fs.existsSync(path.join(row.workspace_root, 'atris'))) return {};
|
|
134
|
+
const member = existingMemberSlug(row.workspace_root, row, actor);
|
|
135
|
+
const fields = {
|
|
136
|
+
task: taskLogRef(db, row),
|
|
137
|
+
title: row.title,
|
|
138
|
+
tag: row.tag,
|
|
139
|
+
actor,
|
|
140
|
+
note: dailyLog.compactLogText(note, 500),
|
|
141
|
+
};
|
|
142
|
+
const memberLogPath = member
|
|
143
|
+
? dailyLog.appendMemberDailyEntry(row.workspace_root, member, title, fields)
|
|
144
|
+
: null;
|
|
145
|
+
const projectLogPath = dailyLog.MASTER_NOTE.test(String(note || ''))
|
|
146
|
+
? dailyLog.appendMasterDailyEntry(row.workspace_root, dailyLog.noteTitle(note), { ...fields, member })
|
|
147
|
+
: null;
|
|
148
|
+
return { project_log_path: projectLogPath, member_log_path: memberLogPath, member };
|
|
149
|
+
}
|
|
150
|
+
|
|
162
151
|
const SCHEMA = `
|
|
163
152
|
CREATE TABLE IF NOT EXISTS tasks (
|
|
164
153
|
id TEXT PRIMARY KEY,
|
|
@@ -186,6 +175,9 @@ CREATE TABLE IF NOT EXISTS task_events (
|
|
|
186
175
|
);
|
|
187
176
|
CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
|
|
188
177
|
CREATE INDEX IF NOT EXISTS idx_tasks_workspace ON tasks(workspace_root);
|
|
178
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_workspace_status_updated ON tasks(workspace_root, status, updated_at);
|
|
179
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_workspace_blocker ON tasks(workspace_root, tag)
|
|
180
|
+
WHERE tag = 'mission-blocker';
|
|
189
181
|
CREATE INDEX IF NOT EXISTS idx_tasks_claimed_by ON tasks(claimed_by);
|
|
190
182
|
CREATE INDEX IF NOT EXISTS idx_task_events_task ON task_events(task_id, version);
|
|
191
183
|
CREATE INDEX IF NOT EXISTS idx_task_events_ws ON task_events(workspace_root, created_at);
|
|
@@ -515,7 +507,9 @@ function claimTask(db, { id, claimedBy }) {
|
|
|
515
507
|
eventType: 'claimed',
|
|
516
508
|
payload: { claimed_by: claimedBy },
|
|
517
509
|
});
|
|
518
|
-
|
|
510
|
+
const enriched = { ...row, metadata: row.metadata ? safeJSON(row.metadata) : null };
|
|
511
|
+
appendTaskWorkLog(db, enriched, { actor: claimedBy, title: 'Task claimed' });
|
|
512
|
+
return { claimed: true, row: enriched };
|
|
519
513
|
}
|
|
520
514
|
// Either id doesn't exist or status != 'open'. Tell the caller which.
|
|
521
515
|
const row = db.prepare('SELECT id, status, claimed_by FROM tasks WHERE id = ?').get(id);
|
|
@@ -665,11 +659,12 @@ function doneTask(db, { id, status, actor, allowReview = false, action, proof, a
|
|
|
665
659
|
return { updated: false };
|
|
666
660
|
}
|
|
667
661
|
|
|
668
|
-
function reapMissionBlockerTasks(db, { workspaceRoot: ws, missions = [], actor } = {}) {
|
|
662
|
+
function reapMissionBlockerTasks(db, { workspaceRoot: ws, missions = [], actor, rows = null, skipLogs = false } = {}) {
|
|
669
663
|
const missionById = new Map((Array.isArray(missions) ? missions : [])
|
|
670
664
|
.filter(mission => mission && mission.id)
|
|
671
665
|
.map(mission => [String(mission.id), mission]));
|
|
672
|
-
const
|
|
666
|
+
const source = Array.isArray(rows) ? rows : listTasks(db, { workspaceRoot: ws || null, limit: null });
|
|
667
|
+
const candidates = source
|
|
673
668
|
.filter(row => OPEN_TASK_STATUSES.has(row.status))
|
|
674
669
|
.filter(row => row.tag === 'mission-blocker')
|
|
675
670
|
.filter(row => row.metadata && row.metadata.mission_id && row.metadata.mission_blocker_class)
|
|
@@ -683,6 +678,7 @@ function reapMissionBlockerTasks(db, { workspaceRoot: ws, missions = [], actor }
|
|
|
683
678
|
id: row.id,
|
|
684
679
|
actor,
|
|
685
680
|
reason,
|
|
681
|
+
skipLogs,
|
|
686
682
|
});
|
|
687
683
|
if (!result.archived) continue;
|
|
688
684
|
closed.push({
|
|
@@ -710,7 +706,7 @@ function reapMissionBlockerTasks(db, { workspaceRoot: ws, missions = [], actor }
|
|
|
710
706
|
// metadata.archived_from. Individual archive commands never archive 'done'
|
|
711
707
|
// rows. The clear-done sweep opts in through fromDone after selecting only the
|
|
712
708
|
// same 'done' rows counted by `atris status`.
|
|
713
|
-
function archiveTask(db, { id, actor, reason, fromFailed = false, fromDone = false } = {}) {
|
|
709
|
+
function archiveTask(db, { id, actor, reason, fromFailed = false, fromDone = false, skipLogs = false } = {}) {
|
|
714
710
|
if (!id) throw new Error('id required');
|
|
715
711
|
const reasonText = String(reason || '').trim();
|
|
716
712
|
if (!reasonText) throw new Error('reason required');
|
|
@@ -752,7 +748,7 @@ function archiveTask(db, { id, actor, reason, fromFailed = false, fromDone = fal
|
|
|
752
748
|
...(['failed', 'done'].includes(row.status) ? { previous_status: row.status } : {}),
|
|
753
749
|
},
|
|
754
750
|
});
|
|
755
|
-
const logs = appendTaskCompletionLogs(db, updated, {
|
|
751
|
+
const logs = skipLogs ? null : appendTaskCompletionLogs(db, updated, {
|
|
756
752
|
status: 'archived',
|
|
757
753
|
actor: metadata.archived_by,
|
|
758
754
|
action: 'archived',
|
|
@@ -1519,6 +1515,11 @@ function noteTask(db, { id, actor, content }) {
|
|
|
1519
1515
|
eventType: 'message',
|
|
1520
1516
|
payload: { content: text },
|
|
1521
1517
|
});
|
|
1518
|
+
// Structured trace notes already land on the member log through
|
|
1519
|
+
// task result/ready; logging them here would double the receipt.
|
|
1520
|
+
if (!/^TASK_[A-Z_]+_TRACE\s/.test(text)) {
|
|
1521
|
+
appendTaskWorkLog(db, row, { actor, title: 'Task note', note: text });
|
|
1522
|
+
}
|
|
1522
1523
|
return { noted: true, event };
|
|
1523
1524
|
}
|
|
1524
1525
|
|
|
@@ -2126,7 +2127,8 @@ function renderTodoMarkdown(rows, { title = 'TODO.md', doneLimit = TODO_RENDER_D
|
|
|
2126
2127
|
failed: displayRows.filter(r => r.status === 'failed'),
|
|
2127
2128
|
done: displayRows.filter(r => r.status === 'done'),
|
|
2128
2129
|
};
|
|
2129
|
-
const lines = [`# ${title}`, '', '> Regenerated from durable Atris task state. Do not treat this file as truth.', ''];
|
|
2130
|
+
const lines = [`# ${title}`, '', 'Details for any task: atris task show <ID>', '', '> Regenerated from durable Atris task state. Do not treat this file as truth.', ''];
|
|
2131
|
+
if (process.env.ATRIS_TODO_RENDER !== 'full') lines.push('<!-- ATRIS_TODO_COMPACT:2 -->', '');
|
|
2130
2132
|
for (const section of preservedSections) {
|
|
2131
2133
|
const text = String(section || '').trim();
|
|
2132
2134
|
if (!text) continue;
|
|
@@ -2170,6 +2172,25 @@ function appendSection(lines, name, rows) {
|
|
|
2170
2172
|
const decision = isDecisionTask(row) ? ' [decision]' : '';
|
|
2171
2173
|
const displayRef = meta.todo_id || row.display_id || row.id;
|
|
2172
2174
|
const explanation = taskExplanation(row);
|
|
2175
|
+
if (process.env.ATRIS_TODO_RENDER !== 'full') {
|
|
2176
|
+
const singleLine = value => String(value || '').replace(/\s+/g, ' ').trim();
|
|
2177
|
+
const clip = (value, limit) => {
|
|
2178
|
+
const text = singleLine(value);
|
|
2179
|
+
return text.length > limit ? text.slice(0, limit - 1) + '…' : text;
|
|
2180
|
+
};
|
|
2181
|
+
const owner = singleLine(row.claimed_by || meta.assigned_to || meta.owner) || 'unassigned';
|
|
2182
|
+
const title = clip(row.title, name === 'Completed' ? 100 : 140);
|
|
2183
|
+
const labels = name === 'Backlog' || name === 'Blocked' ? tag + decision : '';
|
|
2184
|
+
const assignment = name === 'Backlog' || name === 'Completed' ? '' : ` · ${owner}`;
|
|
2185
|
+
const previewLimit = name === 'Blocked' ? 60 : 40;
|
|
2186
|
+
const verify = (name === 'In Progress' || name === 'Blocked') && meta.verify
|
|
2187
|
+
? ` · verify: ${singleLine(meta.verify).slice(0, previewLimit)}` : '';
|
|
2188
|
+
lines.push(`- **[${displayRef}]** ${title}${labels}${assignment}${verify}`);
|
|
2189
|
+
if (name === 'Review') {
|
|
2190
|
+
lines.push(` **Done looks like:** ${clip(explanation.done_looks_like, 160)}`);
|
|
2191
|
+
}
|
|
2192
|
+
continue;
|
|
2193
|
+
}
|
|
2173
2194
|
// The plain face leads. The exact original title remains immediately below
|
|
2174
2195
|
// it so old TODO-only projects and deep inspection keep full fidelity.
|
|
2175
2196
|
lines.push(`- **[${displayRef}]** ${explanation.what_changes}${tag}${decision}`);
|
|
@@ -2204,8 +2225,7 @@ function withBusyRetry(fn, attempts = 8) {
|
|
|
2204
2225
|
const busy = /SQLITE_BUSY|database is locked/i.test(msg) || code === 'SQLITE_BUSY' || code === 5;
|
|
2205
2226
|
if (!busy) throw e;
|
|
2206
2227
|
// Sleep synchronously, node:sqlite is sync; matches the rest of the API
|
|
2207
|
-
|
|
2208
|
-
while (Date.now() < end) {} // tight loop is fine, delay is small
|
|
2228
|
+
sleepSync(delay + Math.floor(Math.random() * delay));
|
|
2209
2229
|
delay = Math.min(delay * 2, 500);
|
|
2210
2230
|
}
|
|
2211
2231
|
}
|
|
@@ -2214,6 +2234,7 @@ function withBusyRetry(fn, attempts = 8) {
|
|
|
2214
2234
|
|
|
2215
2235
|
module.exports = {
|
|
2216
2236
|
open,
|
|
2237
|
+
withBusyRetry,
|
|
2217
2238
|
close,
|
|
2218
2239
|
getDbPath,
|
|
2219
2240
|
workspaceRoot,
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Puts finished and untouched tasks off the visible list. History stays
|
|
4
|
+
// archived. A current list costs one indexed lookup and no writes.
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const taskDb = require('./task-db');
|
|
9
|
+
|
|
10
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
11
|
+
const OPEN_STATUSES = new Set(['open', 'claimed', 'review']);
|
|
12
|
+
|
|
13
|
+
// Failed rows already leave the today screen after 7 days. They stay
|
|
14
|
+
// findable until 30 days, then they leave the list too.
|
|
15
|
+
const IDLE_LIMITS_MS = {
|
|
16
|
+
claimed: 3 * DAY_MS,
|
|
17
|
+
open: 14 * DAY_MS,
|
|
18
|
+
review: 14 * DAY_MS,
|
|
19
|
+
failed: 30 * DAY_MS,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const REASONS = {
|
|
23
|
+
done: 'finished work left the list',
|
|
24
|
+
claimed: 'claimed work sat untouched for 3 days',
|
|
25
|
+
open: 'waiting work sat untouched for 14 days',
|
|
26
|
+
review: 'review sat untouched for 14 days',
|
|
27
|
+
failed: 'failed work sat untouched for 30 days',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const CANDIDATE_SQL = `
|
|
31
|
+
SELECT id, title, status, tag, updated_at, created_at, done_at, metadata
|
|
32
|
+
FROM tasks
|
|
33
|
+
WHERE workspace_root = ? AND status = 'done'
|
|
34
|
+
UNION ALL
|
|
35
|
+
SELECT id, title, status, tag, updated_at, created_at, done_at, metadata
|
|
36
|
+
FROM tasks
|
|
37
|
+
WHERE workspace_root = ? AND status = 'claimed' AND updated_at < ?
|
|
38
|
+
UNION ALL
|
|
39
|
+
SELECT id, title, status, tag, updated_at, created_at, done_at, metadata
|
|
40
|
+
FROM tasks
|
|
41
|
+
WHERE workspace_root = ? AND status = 'open' AND updated_at < ?
|
|
42
|
+
UNION ALL
|
|
43
|
+
SELECT id, title, status, tag, updated_at, created_at, done_at, metadata
|
|
44
|
+
FROM tasks
|
|
45
|
+
WHERE workspace_root = ? AND status = 'review' AND updated_at < ?
|
|
46
|
+
UNION ALL
|
|
47
|
+
SELECT id, title, status, tag, updated_at, created_at, done_at, metadata
|
|
48
|
+
FROM tasks
|
|
49
|
+
WHERE workspace_root = ? AND status = 'failed' AND updated_at < ?
|
|
50
|
+
UNION ALL
|
|
51
|
+
SELECT id, title, status, tag, updated_at, created_at, done_at, metadata
|
|
52
|
+
FROM tasks
|
|
53
|
+
WHERE workspace_root = ? AND tag = 'mission-blocker' AND status IN ('open', 'claimed', 'review')
|
|
54
|
+
`;
|
|
55
|
+
|
|
56
|
+
function actorName(actor) {
|
|
57
|
+
return actor || process.env.ATRIS_AGENT_ID || process.env.USER || 'task-list';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function decisionFor(row, now) {
|
|
61
|
+
if (!row || row.status === 'archived') return null;
|
|
62
|
+
if (row.status === 'done') {
|
|
63
|
+
return { reason: REASONS.done, fromDone: true, fromFailed: false };
|
|
64
|
+
}
|
|
65
|
+
if (!Object.prototype.hasOwnProperty.call(IDLE_LIMITS_MS, row.status)) return null;
|
|
66
|
+
const at = Number(row.updated_at || row.created_at || 0);
|
|
67
|
+
if (!((now - at) > IDLE_LIMITS_MS[row.status])) return null;
|
|
68
|
+
return { reason: REASONS[row.status], fromDone: false, fromFailed: row.status === 'failed' };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function selectCandidates(db, workspaceRoot, now) {
|
|
72
|
+
return db.prepare(CANDIDATE_SQL).all(
|
|
73
|
+
workspaceRoot,
|
|
74
|
+
workspaceRoot, now - IDLE_LIMITS_MS.claimed,
|
|
75
|
+
workspaceRoot, now - IDLE_LIMITS_MS.open,
|
|
76
|
+
workspaceRoot, now - IDLE_LIMITS_MS.review,
|
|
77
|
+
workspaceRoot, now - IDLE_LIMITS_MS.failed,
|
|
78
|
+
workspaceRoot,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function blockerFrom(row) {
|
|
83
|
+
if (!row || row.tag !== 'mission-blocker' || !OPEN_STATUSES.has(row.status)) return null;
|
|
84
|
+
let metadata = row.metadata;
|
|
85
|
+
if (typeof metadata === 'string') {
|
|
86
|
+
try { metadata = JSON.parse(metadata); } catch { metadata = null; }
|
|
87
|
+
}
|
|
88
|
+
if (!metadata || !metadata.mission_id || !metadata.mission_blocker_class) return null;
|
|
89
|
+
return { ...row, metadata };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function loadMissions(workspaceRoot) {
|
|
93
|
+
try {
|
|
94
|
+
return require('../commands/mission').listMissions(workspaceRoot) || [];
|
|
95
|
+
} catch {
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function noteKeep(workspaceRoot, actor, count) {
|
|
101
|
+
if (!count || !workspaceRoot || !fs.existsSync(path.join(workspaceRoot, 'atris'))) return;
|
|
102
|
+
try {
|
|
103
|
+
const now = new Date();
|
|
104
|
+
const year = String(now.getFullYear());
|
|
105
|
+
const dir = path.join(workspaceRoot, 'atris', 'logs', year);
|
|
106
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
107
|
+
const name = `${year}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}.md`;
|
|
108
|
+
const stamp = now.toTimeString().slice(0, 5);
|
|
109
|
+
fs.appendFileSync(path.join(dir, name), `## ${stamp} · Task list kept\n- count: ${count}\n- actor: ${actor}\n\n`);
|
|
110
|
+
} catch {
|
|
111
|
+
// The rows are already off the list. A missed note should not put them back.
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function keepTaskList(db, { workspaceRoot, actor, now = Date.now(), missions } = {}) {
|
|
116
|
+
const who = actorName(actor);
|
|
117
|
+
const seen = new Set();
|
|
118
|
+
const toArchive = [];
|
|
119
|
+
const blockers = [];
|
|
120
|
+
for (const row of selectCandidates(db, workspaceRoot, now)) {
|
|
121
|
+
if (!row || seen.has(row.id)) continue;
|
|
122
|
+
seen.add(row.id);
|
|
123
|
+
const decision = decisionFor(row, now);
|
|
124
|
+
if (decision) {
|
|
125
|
+
toArchive.push({ id: row.id, previous_status: row.status, ...decision });
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const blocker = blockerFrom(row);
|
|
129
|
+
if (blocker) blockers.push(blocker);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const missionList = Array.isArray(missions)
|
|
133
|
+
? missions
|
|
134
|
+
: (blockers.length ? loadMissions(workspaceRoot) : []);
|
|
135
|
+
if (!toArchive.length && !(blockers.length && missionList.length)) {
|
|
136
|
+
return { put_away: [], reaped: [] };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const putAway = [];
|
|
140
|
+
let reaped = [];
|
|
141
|
+
db.exec('BEGIN IMMEDIATE');
|
|
142
|
+
try {
|
|
143
|
+
for (const item of toArchive) {
|
|
144
|
+
const result = taskDb.archiveTask(db, {
|
|
145
|
+
id: item.id,
|
|
146
|
+
actor: who,
|
|
147
|
+
reason: item.reason,
|
|
148
|
+
fromDone: item.fromDone,
|
|
149
|
+
fromFailed: item.fromFailed,
|
|
150
|
+
skipLogs: true,
|
|
151
|
+
});
|
|
152
|
+
if (result.archived) {
|
|
153
|
+
putAway.push({ id: item.id, previous_status: item.previous_status, reason: item.reason });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (blockers.length && missionList.length) {
|
|
157
|
+
reaped = taskDb.reapMissionBlockerTasks(db, {
|
|
158
|
+
workspaceRoot,
|
|
159
|
+
missions: missionList,
|
|
160
|
+
actor: who,
|
|
161
|
+
rows: blockers,
|
|
162
|
+
skipLogs: true,
|
|
163
|
+
}).closed || [];
|
|
164
|
+
}
|
|
165
|
+
db.exec('COMMIT');
|
|
166
|
+
} catch (error) {
|
|
167
|
+
try { db.exec('ROLLBACK'); } catch { /* the original error is the one to surface */ }
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
noteKeep(workspaceRoot, who, putAway.length + reaped.length);
|
|
171
|
+
return { put_away: putAway, reaped };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function keepWorkspaceTaskList(cwd = process.cwd(), { actor } = {}) {
|
|
175
|
+
const dbPath = taskDb.getDbPath();
|
|
176
|
+
if (!dbPath || !fs.existsSync(dbPath)) {
|
|
177
|
+
return { put_away: [], reaped: [], skipped: 'no_db' };
|
|
178
|
+
}
|
|
179
|
+
const db = taskDb.open();
|
|
180
|
+
return keepTaskList(db, {
|
|
181
|
+
workspaceRoot: taskDb.workspaceRoot(cwd),
|
|
182
|
+
actor,
|
|
183
|
+
now: Date.now(),
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
module.exports = {
|
|
188
|
+
DAY_MS,
|
|
189
|
+
IDLE_LIMITS_MS,
|
|
190
|
+
keepTaskList,
|
|
191
|
+
keepWorkspaceTaskList,
|
|
192
|
+
};
|
package/lib/todo-fallback.js
CHANGED
|
@@ -34,7 +34,7 @@ function cleanTaskTitle(text) {
|
|
|
34
34
|
const withoutTags = raw.replace(/\s*\[[\w-]+\]/g, '').trim();
|
|
35
35
|
const bold = withoutTags.match(/^\*\*(.+?)\*\*\s*(?:[\u2014-]\s*)?(.*)$/);
|
|
36
36
|
if (!bold) return withoutTags;
|
|
37
|
-
return [bold[1], bold[2]].filter(Boolean).join(' \
|
|
37
|
+
return [bold[1], bold[2]].filter(Boolean).join(' - ').replace(/\s*\u2014\s*/g, ' - ').trim();
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
function parseSection(content, sectionName) {
|
|
@@ -80,15 +80,21 @@ function parseSection(content, sectionName) {
|
|
|
80
80
|
const bracketTaskMatch = line.match(/^- \*\*\[([^\]\n]+)\]\*\*\s+(.+)$/);
|
|
81
81
|
if (bracketTaskMatch) {
|
|
82
82
|
if (current) tasks.push(current);
|
|
83
|
-
|
|
83
|
+
// Compact rows carry display-only verification previews. Never execute
|
|
84
|
+
// a clipped command; the full command lives in task show or the DB.
|
|
85
|
+
const titleOnly = content.includes('<!-- ATRIS_TODO_COMPACT:2 -->') && /^(Backlog|Completed)$/i.test(sectionName);
|
|
86
|
+
const compact = !titleOnly && bracketTaskMatch[2].match(/^(.*?) · ([^·]+?)(?: · verify: (.*))?$/);
|
|
87
|
+
const title = compact ? compact[1] : bracketTaskMatch[2];
|
|
88
|
+
const { allTags, tag } = tagsFromText(title);
|
|
84
89
|
current = {
|
|
85
90
|
id: bracketTaskMatch[1],
|
|
86
|
-
title: cleanTaskTitle(
|
|
91
|
+
title: cleanTaskTitle(title),
|
|
87
92
|
tag,
|
|
88
93
|
tags: allTags,
|
|
89
|
-
claimed: null,
|
|
94
|
+
claimed: compact && compact[2] !== 'unassigned' ? compact[2].trim() : null,
|
|
90
95
|
stage: null,
|
|
91
96
|
verify: null,
|
|
97
|
+
...(compact || titleOnly ? { verify_preview: compact ? compact[3] || null : null } : {}),
|
|
92
98
|
};
|
|
93
99
|
continue;
|
|
94
100
|
}
|
package/lib/todo.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// SHIM. Same export surface as the legacy markdown parser, but reads from the
|
|
2
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.
|
|
3
|
+
// pure markdown parser at lib/todo-fallback.js. Compact generated boards also
|
|
4
|
+
// consult an existing store so verification previews never replace commands.
|
|
4
5
|
//
|
|
5
6
|
// All 3 callers (commands/autopilot.js, commands/status.js, commands/run.js)
|
|
6
7
|
// inherit the strangler without changing their import path.
|
|
@@ -22,12 +23,14 @@ function dbToShimRow(row) {
|
|
|
22
23
|
? metadata.verify.trim()
|
|
23
24
|
: null;
|
|
24
25
|
const claimed = row.claimed_by || metadata.claimed || null;
|
|
26
|
+
const tags = [...new Set([row.tag, ...(Array.isArray(metadata.todo_tags) ? metadata.todo_tags : []),
|
|
27
|
+
...(Array.isArray(metadata.tags) ? metadata.tags : [])].filter(Boolean))];
|
|
25
28
|
// Map DB row → the shape the existing consumers expect from parseTodo().
|
|
26
29
|
return {
|
|
27
30
|
id: row.id,
|
|
28
31
|
title: row.title,
|
|
29
|
-
tag: row.tag || null,
|
|
30
|
-
tags
|
|
32
|
+
tag: tags.includes('endgame') ? 'endgame' : (row.tag || tags[0] || null),
|
|
33
|
+
tags,
|
|
31
34
|
claimed,
|
|
32
35
|
stage: row.status === 'claimed' ? 'in_progress' : (metadata.stage || null),
|
|
33
36
|
verify,
|
|
@@ -38,25 +41,29 @@ function dbToShimRow(row) {
|
|
|
38
41
|
function dbBuckets(workspaceRoot) {
|
|
39
42
|
const taskDb = require('./task-db');
|
|
40
43
|
const db = taskDb.open();
|
|
41
|
-
const rows = taskDb.listTasks(db, { workspaceRoot, limit: 500 });
|
|
44
|
+
const rows = taskDb.withTaskDisplayRefs(taskDb.listTasks(db, { workspaceRoot, limit: 500 }));
|
|
42
45
|
// Need raw source_key for merge dedup, plus the shim shape for callers.
|
|
43
46
|
const backlog = [];
|
|
44
47
|
const inProgress = [];
|
|
45
48
|
const review = [];
|
|
46
49
|
const completed = [];
|
|
47
50
|
const sourceKeys = new Set();
|
|
51
|
+
const sourceRefs = new Set();
|
|
48
52
|
// Query directly so the shim can dedup against markdown by the strong key
|
|
49
53
|
// even if future list filters hide rows.
|
|
50
54
|
const stmt = db.prepare('SELECT id, source_key FROM tasks WHERE workspace_root = ? AND source_key IS NOT NULL');
|
|
51
55
|
for (const r of stmt.all(workspaceRoot)) sourceKeys.add(r.source_key);
|
|
52
56
|
for (const r of rows) {
|
|
57
|
+
for (const ref of [r.id, r.display_id, r.legacy_ref, r.metadata?.todo_id]) {
|
|
58
|
+
if (ref) sourceRefs.add(String(ref).toLowerCase());
|
|
59
|
+
}
|
|
53
60
|
const shaped = dbToShimRow(r);
|
|
54
61
|
if (r.status === 'open') backlog.push(shaped);
|
|
55
62
|
else if (r.status === 'claimed') inProgress.push(shaped);
|
|
56
63
|
else if (r.status === 'review') review.push(shaped);
|
|
57
64
|
else if (r.status === 'done' || r.status === 'failed') completed.push(shaped);
|
|
58
65
|
}
|
|
59
|
-
return { backlog, inProgress, review, completed, sourceKeys };
|
|
66
|
+
return { backlog, inProgress, review, completed, sourceKeys, sourceRefs };
|
|
60
67
|
}
|
|
61
68
|
|
|
62
69
|
function mergeBuckets(dbBuck, mdBuck, todoPath) {
|
|
@@ -71,6 +78,7 @@ function mergeBuckets(dbBuck, mdBuck, todoPath) {
|
|
|
71
78
|
for (const k of ['backlog', 'inProgress', 'review', 'completed']) {
|
|
72
79
|
for (const r of dbBuck[k]) { out[k].push(r); seenTitles.add(norm(r.title)); }
|
|
73
80
|
for (const r of mdBuck[k]) {
|
|
81
|
+
if (r.id && dbBuck.sourceRefs?.has(String(r.id).toLowerCase())) continue;
|
|
74
82
|
const sk = todoPath ? taskDb.sourceKey(todoPath, r.title) : null;
|
|
75
83
|
if (sk && dbBuck.sourceKeys && dbBuck.sourceKeys.has(sk)) continue;
|
|
76
84
|
if (seenTitles.has(norm(r.title))) continue;
|
|
@@ -81,14 +89,19 @@ function mergeBuckets(dbBuck, mdBuck, todoPath) {
|
|
|
81
89
|
}
|
|
82
90
|
|
|
83
91
|
function parseTodo(todoPath) {
|
|
84
|
-
|
|
85
|
-
|
|
92
|
+
const mdBuck = fallback.parseTodoFile(todoPath);
|
|
93
|
+
const compact = Object.values(mdBuck).some(rows => rows.some(row => Object.hasOwn(row, 'verify_preview')));
|
|
94
|
+
// Compact generated boards omit full commands. Resolve them from existing
|
|
95
|
+
// durable state even without the legacy opt-in, without minting a database.
|
|
96
|
+
if (!TASK_DB_ENABLED && !compact) return mdBuck;
|
|
86
97
|
|
|
87
98
|
// DB-first merged view. Workspace scope = directory containing todoPath
|
|
88
99
|
// (or its parent), so the DB stays per-repo.
|
|
89
100
|
const path = require('path');
|
|
90
101
|
const fs = require('fs');
|
|
91
|
-
|
|
102
|
+
let taskDb;
|
|
103
|
+
try { taskDb = require('./task-db'); } catch { return mdBuck; }
|
|
104
|
+
if (!TASK_DB_ENABLED && !fs.existsSync(taskDb.getDbPath())) return mdBuck;
|
|
92
105
|
let workspaceRoot;
|
|
93
106
|
try {
|
|
94
107
|
const todoAbs = path.resolve(todoPath);
|
|
@@ -106,9 +119,8 @@ function parseTodo(todoPath) {
|
|
|
106
119
|
} catch (e) {
|
|
107
120
|
// If sqlite blew up (missing in node, perms), don't break the legacy path.
|
|
108
121
|
if (process.env.ATRIS_DEBUG) console.error('[todo shim] db read failed:', e.message);
|
|
109
|
-
return
|
|
122
|
+
return mdBuck;
|
|
110
123
|
}
|
|
111
|
-
const mdBuck = fallback.parseTodoFile(todoPath);
|
|
112
124
|
return mergeBuckets(dbBuck, mdBuck, todoPath);
|
|
113
125
|
}
|
|
114
126
|
|