sidecar-cli 0.1.0-beta.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.
@@ -0,0 +1,16 @@
1
+ import Database from 'better-sqlite3';
2
+ import { findSidecarRoot, getSidecarPaths } from '../lib/paths.js';
3
+ import { SidecarError } from '../lib/errors.js';
4
+ export function requireInitialized() {
5
+ const rootPath = findSidecarRoot();
6
+ if (!rootPath) {
7
+ throw new SidecarError('Sidecar is not initialized in this directory or any parent directory', 'NOT_INITIALIZED', 2);
8
+ }
9
+ const db = new Database(getSidecarPaths(rootPath).dbPath);
10
+ const row = db.prepare('SELECT id FROM projects ORDER BY id LIMIT 1').get();
11
+ if (!row) {
12
+ db.close();
13
+ throw new SidecarError('Sidecar database exists but project row is missing. Re-run `sidecar init --force`.', 'PROJECT_MISSING', 2);
14
+ }
15
+ return { rootPath, db, projectId: row.id };
16
+ }
@@ -0,0 +1,69 @@
1
+ export function initializeSchema(db) {
2
+ db.pragma('journal_mode = WAL');
3
+ db.exec(`
4
+ CREATE TABLE IF NOT EXISTS projects (
5
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
6
+ name TEXT NOT NULL,
7
+ root_path TEXT NOT NULL,
8
+ created_at TEXT NOT NULL,
9
+ updated_at TEXT NOT NULL
10
+ );
11
+
12
+ CREATE TABLE IF NOT EXISTS events (
13
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
14
+ project_id INTEGER NOT NULL,
15
+ type TEXT NOT NULL,
16
+ title TEXT NOT NULL,
17
+ summary TEXT NOT NULL,
18
+ details_json TEXT NOT NULL DEFAULT '{}',
19
+ created_at TEXT NOT NULL,
20
+ created_by TEXT NOT NULL,
21
+ source TEXT NOT NULL,
22
+ session_id INTEGER,
23
+ FOREIGN KEY (project_id) REFERENCES projects(id),
24
+ FOREIGN KEY (session_id) REFERENCES sessions(id)
25
+ );
26
+
27
+ CREATE TABLE IF NOT EXISTS tasks (
28
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
29
+ project_id INTEGER NOT NULL,
30
+ title TEXT NOT NULL,
31
+ description TEXT,
32
+ status TEXT NOT NULL,
33
+ priority TEXT,
34
+ created_at TEXT NOT NULL,
35
+ updated_at TEXT NOT NULL,
36
+ closed_at TEXT,
37
+ origin_event_id INTEGER,
38
+ FOREIGN KEY (project_id) REFERENCES projects(id),
39
+ FOREIGN KEY (origin_event_id) REFERENCES events(id)
40
+ );
41
+
42
+ CREATE TABLE IF NOT EXISTS sessions (
43
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
44
+ project_id INTEGER NOT NULL,
45
+ started_at TEXT NOT NULL,
46
+ ended_at TEXT,
47
+ actor_type TEXT NOT NULL,
48
+ actor_name TEXT,
49
+ summary TEXT,
50
+ FOREIGN KEY (project_id) REFERENCES projects(id)
51
+ );
52
+
53
+ CREATE TABLE IF NOT EXISTS artifacts (
54
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
55
+ project_id INTEGER NOT NULL,
56
+ path TEXT NOT NULL,
57
+ kind TEXT NOT NULL,
58
+ note TEXT,
59
+ created_at TEXT NOT NULL,
60
+ FOREIGN KEY (project_id) REFERENCES projects(id)
61
+ );
62
+
63
+ CREATE INDEX IF NOT EXISTS idx_events_project_time ON events(project_id, created_at DESC);
64
+ CREATE INDEX IF NOT EXISTS idx_events_type ON events(type);
65
+ CREATE INDEX IF NOT EXISTS idx_tasks_project_status ON tasks(project_id, status);
66
+ CREATE INDEX IF NOT EXISTS idx_sessions_project_active ON sessions(project_id, ended_at);
67
+ CREATE INDEX IF NOT EXISTS idx_artifacts_project_time ON artifacts(project_id, created_at DESC);
68
+ `);
69
+ }
@@ -0,0 +1,12 @@
1
+ export const SIDECAR_MARK = '[■]─[▪] sidecar';
2
+ export const SIDECAR_TAGLINE = 'project memory for your work';
3
+ export function bannerDisabled(argv = process.argv) {
4
+ return process.env.SIDECAR_NO_BANNER === '1' || argv.includes('--no-banner');
5
+ }
6
+ export function renderBanner(includeTagline = true) {
7
+ const lines = [SIDECAR_MARK, ''];
8
+ if (includeTagline) {
9
+ lines.push(SIDECAR_TAGLINE);
10
+ }
11
+ return lines.join('\n');
12
+ }
package/dist/lib/db.js ADDED
@@ -0,0 +1,36 @@
1
+ import fs from 'node:fs';
2
+ import Database from 'better-sqlite3';
3
+ export function openDb(dbPath) {
4
+ return new Database(dbPath);
5
+ }
6
+ export function migrate(db) {
7
+ db.pragma('journal_mode = WAL');
8
+ db.exec(`
9
+ CREATE TABLE IF NOT EXISTS events (
10
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
11
+ ts TEXT NOT NULL,
12
+ type TEXT NOT NULL,
13
+ title TEXT NOT NULL,
14
+ body TEXT NOT NULL,
15
+ tags_json TEXT NOT NULL DEFAULT '[]'
16
+ );
17
+
18
+ CREATE TABLE IF NOT EXISTS tasks (
19
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
20
+ title TEXT NOT NULL,
21
+ status TEXT NOT NULL DEFAULT 'open',
22
+ priority TEXT NOT NULL DEFAULT 'medium',
23
+ created_at TEXT NOT NULL,
24
+ updated_at TEXT NOT NULL
25
+ );
26
+
27
+ CREATE INDEX IF NOT EXISTS idx_events_ts ON events(ts DESC);
28
+ CREATE INDEX IF NOT EXISTS idx_events_type ON events(type);
29
+ CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
30
+ `);
31
+ }
32
+ export function ensureSidecarDir(sidecarDir) {
33
+ if (!fs.existsSync(sidecarDir)) {
34
+ fs.mkdirSync(sidecarDir, { recursive: true });
35
+ }
36
+ }
@@ -0,0 +1,10 @@
1
+ export class SidecarError extends Error {
2
+ code;
3
+ exitCode;
4
+ constructor(message, code = 'SIDE_CAR_ERROR', exitCode = 1) {
5
+ super(message);
6
+ this.code = code;
7
+ this.exitCode = exitCode;
8
+ this.name = 'SidecarError';
9
+ }
10
+ }
@@ -0,0 +1,14 @@
1
+ export function nowIso() {
2
+ return new Date().toISOString();
3
+ }
4
+ export function humanTime(iso) {
5
+ return new Date(iso).toLocaleString();
6
+ }
7
+ export function splitCsv(input) {
8
+ if (!input)
9
+ return [];
10
+ return input.split(',').map((x) => x.trim()).filter(Boolean);
11
+ }
12
+ export function stringifyJson(value) {
13
+ return JSON.stringify(value, null, 2);
14
+ }
@@ -0,0 +1,20 @@
1
+ import { stringifyJson } from './format.js';
2
+ export function jsonSuccess(command, data) {
3
+ return {
4
+ ok: true,
5
+ command,
6
+ data,
7
+ errors: [],
8
+ };
9
+ }
10
+ export function jsonFailure(command, message) {
11
+ return {
12
+ ok: false,
13
+ command,
14
+ data: null,
15
+ errors: [message],
16
+ };
17
+ }
18
+ export function printJsonEnvelope(payload) {
19
+ console.log(stringifyJson(payload));
20
+ }
@@ -0,0 +1,27 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ export const SIDECAR_DIR = '.sidecar';
4
+ export function getSidecarPaths(rootPath) {
5
+ const sidecarPath = path.join(rootPath, SIDECAR_DIR);
6
+ return {
7
+ rootPath,
8
+ sidecarPath,
9
+ dbPath: path.join(sidecarPath, 'sidecar.db'),
10
+ configPath: path.join(sidecarPath, 'config.json'),
11
+ preferencesPath: path.join(sidecarPath, 'preferences.json'),
12
+ agentsPath: path.join(sidecarPath, 'AGENTS.md'),
13
+ summaryPath: path.join(sidecarPath, 'summary.md'),
14
+ };
15
+ }
16
+ export function findSidecarRoot(startDir = process.cwd()) {
17
+ let current = path.resolve(startDir);
18
+ while (true) {
19
+ if (fs.existsSync(path.join(current, SIDECAR_DIR))) {
20
+ return current;
21
+ }
22
+ const parent = path.dirname(current);
23
+ if (parent === current)
24
+ return null;
25
+ current = parent;
26
+ }
27
+ }
@@ -0,0 +1,31 @@
1
+ import { z } from 'zod';
2
+ export const eventTypeSchema = z.enum([
3
+ 'note',
4
+ 'worklog',
5
+ 'decision',
6
+ 'task_update',
7
+ 'summary',
8
+ 'context',
9
+ ]);
10
+ export const taskPrioritySchema = z.enum(['low', 'medium', 'high']);
11
+ export const addEventSchema = z.object({
12
+ type: eventTypeSchema,
13
+ title: z.string().trim().min(1).max(140),
14
+ body: z.string().trim().min(1),
15
+ tags: z.array(z.string().trim().min(1)).default([]),
16
+ });
17
+ export const addTaskSchema = z.object({
18
+ title: z.string().trim().min(1).max(240),
19
+ priority: taskPrioritySchema.default('medium'),
20
+ });
21
+ export const completeTaskSchema = z.object({
22
+ id: z.number().int().positive(),
23
+ });
24
+ export function parseTags(input) {
25
+ if (!input)
26
+ return [];
27
+ return input
28
+ .split(',')
29
+ .map((t) => t.trim())
30
+ .filter(Boolean);
31
+ }
@@ -0,0 +1,12 @@
1
+ import { nowIso } from '../lib/format.js';
2
+ export function addArtifact(db, input) {
3
+ const info = db
4
+ .prepare(`INSERT INTO artifacts (project_id, path, kind, note, created_at) VALUES (?, ?, ?, ?, ?)`)
5
+ .run(input.projectId, input.path, input.kind, input.note ?? null, nowIso());
6
+ return Number(info.lastInsertRowid);
7
+ }
8
+ export function listArtifacts(db, projectId) {
9
+ return db
10
+ .prepare(`SELECT id, path, kind, note, created_at FROM artifacts WHERE project_id = ? ORDER BY created_at DESC LIMIT 50`)
11
+ .all(projectId);
12
+ }
@@ -0,0 +1,198 @@
1
+ export function getCapabilitiesManifest(version) {
2
+ return {
3
+ schema_version: 1,
4
+ cli: {
5
+ name: 'sidecar',
6
+ version,
7
+ },
8
+ commands: [
9
+ {
10
+ name: 'init',
11
+ description: 'Initialize Sidecar in the current directory',
12
+ json_output: true,
13
+ arguments: [],
14
+ options: ['--force', '--name <project-name>', '--json'],
15
+ },
16
+ {
17
+ name: 'status',
18
+ description: 'Show Sidecar initialization status and activity counts',
19
+ json_output: true,
20
+ arguments: [],
21
+ options: ['--json'],
22
+ },
23
+ {
24
+ name: 'capabilities',
25
+ description: 'Show machine-readable CLI manifest',
26
+ json_output: true,
27
+ arguments: [],
28
+ options: ['--json'],
29
+ },
30
+ {
31
+ name: 'context',
32
+ description: 'Generate project context snapshot',
33
+ json_output: true,
34
+ arguments: [],
35
+ options: ['--limit <n>', '--format text|markdown|json', '--json'],
36
+ },
37
+ {
38
+ name: 'summary',
39
+ description: 'Summary operations',
40
+ json_output: true,
41
+ arguments: [],
42
+ options: [],
43
+ subcommands: [
44
+ {
45
+ name: 'refresh',
46
+ description: 'Regenerate .sidecar/summary.md',
47
+ json_output: true,
48
+ arguments: [],
49
+ options: ['--limit <n>', '--json'],
50
+ },
51
+ ],
52
+ },
53
+ {
54
+ name: 'recent',
55
+ description: 'Show recent event timeline',
56
+ json_output: true,
57
+ arguments: [],
58
+ options: ['--type <event-type>', '--limit <n>', '--json'],
59
+ },
60
+ {
61
+ name: 'note',
62
+ description: 'Record a note event',
63
+ json_output: true,
64
+ arguments: ['<text>'],
65
+ options: ['--title <title>', '--by human|agent', '--session <id>', '--json'],
66
+ },
67
+ {
68
+ name: 'decision',
69
+ description: 'Decision operations',
70
+ json_output: true,
71
+ arguments: [],
72
+ options: [],
73
+ subcommands: [
74
+ {
75
+ name: 'record',
76
+ description: 'Record a decision',
77
+ json_output: true,
78
+ arguments: [],
79
+ options: ['--title <title>', '--summary <summary>', '--details <details>', '--by human|agent', '--session <id>', '--json'],
80
+ },
81
+ ],
82
+ },
83
+ {
84
+ name: 'worklog',
85
+ description: 'Worklog operations',
86
+ json_output: true,
87
+ arguments: [],
88
+ options: [],
89
+ subcommands: [
90
+ {
91
+ name: 'record',
92
+ description: 'Record a worklog',
93
+ json_output: true,
94
+ arguments: [],
95
+ options: ['--goal <goal>', '--done <summary>', '--files <paths>', '--risks <text>', '--next <text>', '--by human|agent', '--session <id>', '--json'],
96
+ },
97
+ ],
98
+ },
99
+ {
100
+ name: 'task',
101
+ description: 'Task operations',
102
+ json_output: true,
103
+ arguments: [],
104
+ options: [],
105
+ subcommands: [
106
+ {
107
+ name: 'add',
108
+ description: 'Create an open task',
109
+ json_output: true,
110
+ arguments: ['<title>'],
111
+ options: ['--description <text>', '--priority low|medium|high', '--by human|agent', '--json'],
112
+ },
113
+ {
114
+ name: 'done',
115
+ description: 'Mark a task as done',
116
+ json_output: true,
117
+ arguments: ['<task-id>'],
118
+ options: ['--by human|agent', '--json'],
119
+ },
120
+ {
121
+ name: 'list',
122
+ description: 'List tasks',
123
+ json_output: true,
124
+ arguments: [],
125
+ options: ['--status open|done|all', '--format table|json', '--json'],
126
+ },
127
+ ],
128
+ },
129
+ {
130
+ name: 'session',
131
+ description: 'Session operations',
132
+ json_output: true,
133
+ arguments: [],
134
+ options: [],
135
+ subcommands: [
136
+ {
137
+ name: 'start',
138
+ description: 'Start a session',
139
+ json_output: true,
140
+ arguments: [],
141
+ options: ['--actor human|agent', '--name <actor-name>', '--json'],
142
+ },
143
+ {
144
+ name: 'end',
145
+ description: 'End the active session',
146
+ json_output: true,
147
+ arguments: [],
148
+ options: ['--summary <text>', '--json'],
149
+ },
150
+ {
151
+ name: 'current',
152
+ description: 'Show active session',
153
+ json_output: true,
154
+ arguments: [],
155
+ options: ['--json'],
156
+ },
157
+ {
158
+ name: 'verify',
159
+ description: 'Run lightweight session hygiene checks',
160
+ json_output: true,
161
+ arguments: [],
162
+ options: ['--json'],
163
+ },
164
+ ],
165
+ },
166
+ {
167
+ name: 'doctor',
168
+ description: 'Alias for session verify',
169
+ json_output: true,
170
+ arguments: [],
171
+ options: ['--json'],
172
+ },
173
+ {
174
+ name: 'artifact',
175
+ description: 'Artifact operations',
176
+ json_output: true,
177
+ arguments: [],
178
+ options: [],
179
+ subcommands: [
180
+ {
181
+ name: 'add',
182
+ description: 'Add an artifact reference',
183
+ json_output: true,
184
+ arguments: ['<path>'],
185
+ options: ['--kind file|doc|screenshot|other', '--note <text>', '--json'],
186
+ },
187
+ {
188
+ name: 'list',
189
+ description: 'List recent artifacts',
190
+ json_output: true,
191
+ arguments: [],
192
+ options: ['--json'],
193
+ },
194
+ ],
195
+ },
196
+ ],
197
+ };
198
+ }
@@ -0,0 +1,33 @@
1
+ import { nowIso } from '../lib/format.js';
2
+ export function buildContext(db, input) {
3
+ const project = db.prepare(`SELECT name, root_path FROM projects WHERE id = ?`).get(input.projectId);
4
+ const activeSession = db
5
+ .prepare(`SELECT id, started_at, actor_type, actor_name FROM sessions WHERE project_id = ? AND ended_at IS NULL ORDER BY started_at DESC LIMIT 1`)
6
+ .get(input.projectId);
7
+ const decisions = db
8
+ .prepare(`SELECT created_at, title, summary FROM events WHERE project_id = ? AND type = 'decision' ORDER BY created_at DESC LIMIT ?`)
9
+ .all(input.projectId, input.limit);
10
+ const worklogs = db
11
+ .prepare(`SELECT created_at, title, summary FROM events WHERE project_id = ? AND type = 'worklog' ORDER BY created_at DESC LIMIT ?`)
12
+ .all(input.projectId, input.limit);
13
+ const notes = db
14
+ .prepare(`SELECT created_at, title, summary FROM events WHERE project_id = ? AND type = 'note' ORDER BY created_at DESC LIMIT ?`)
15
+ .all(input.projectId, input.limit);
16
+ const openTasks = db
17
+ .prepare(`SELECT id, title, priority, updated_at FROM tasks WHERE project_id = ? AND status = 'open' ORDER BY updated_at DESC LIMIT ?`)
18
+ .all(input.projectId, input.limit);
19
+ const artifacts = db
20
+ .prepare(`SELECT path, kind, note, created_at FROM artifacts WHERE project_id = ? ORDER BY created_at DESC LIMIT ?`)
21
+ .all(input.projectId, input.limit);
22
+ return {
23
+ generatedAt: nowIso(),
24
+ projectName: project.name,
25
+ projectPath: project.root_path,
26
+ activeSession: activeSession ?? null,
27
+ recentDecisions: decisions,
28
+ recentWorklogs: worklogs,
29
+ notableNotes: notes,
30
+ openTasks,
31
+ recentArtifacts: artifacts,
32
+ };
33
+ }
@@ -0,0 +1,82 @@
1
+ import path from 'node:path';
2
+ import { z } from 'zod';
3
+ import { nowIso, splitCsv } from '../lib/format.js';
4
+ const createdBySchema = z.enum(['human', 'agent', 'system']);
5
+ function normalizeArtifactPath(inputPath) {
6
+ const normalized = path.normalize(inputPath.trim()).replaceAll('\\', '/');
7
+ return normalized.startsWith('./') ? normalized.slice(2) : normalized;
8
+ }
9
+ export function getActiveSessionId(db, projectId) {
10
+ const row = db
11
+ .prepare(`SELECT id FROM sessions WHERE project_id = ? AND ended_at IS NULL ORDER BY started_at DESC LIMIT 1`)
12
+ .get(projectId);
13
+ return row?.id ?? null;
14
+ }
15
+ export function createEvent(db, input) {
16
+ const createdBy = createdBySchema.parse(input.createdBy ?? 'human');
17
+ const source = input.source ?? 'cli';
18
+ const info = db
19
+ .prepare(`
20
+ INSERT INTO events (project_id, type, title, summary, details_json, created_at, created_by, source, session_id)
21
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
22
+ `)
23
+ .run(input.projectId, input.type, input.title, input.summary, JSON.stringify(input.details ?? {}), nowIso(), createdBy, source, input.sessionId ?? null);
24
+ return Number(info.lastInsertRowid);
25
+ }
26
+ export function addNote(db, input) {
27
+ const title = input.title?.trim() || 'Note';
28
+ return createEvent(db, {
29
+ projectId: input.projectId,
30
+ type: 'note',
31
+ title,
32
+ summary: input.text,
33
+ details: { text: input.text },
34
+ createdBy: input.by,
35
+ sessionId: input.sessionId,
36
+ });
37
+ }
38
+ export function addDecision(db, input) {
39
+ return createEvent(db, {
40
+ projectId: input.projectId,
41
+ type: 'decision',
42
+ title: input.title,
43
+ summary: input.summary,
44
+ details: { details: input.details ?? null },
45
+ createdBy: input.by,
46
+ sessionId: input.sessionId,
47
+ });
48
+ }
49
+ export function addWorklog(db, input) {
50
+ const files = Array.from(new Set(splitCsv(input.files).map(normalizeArtifactPath).filter(Boolean)));
51
+ const goal = input.goal?.trim();
52
+ const done = input.done.trim();
53
+ const risks = input.risks?.trim() || null;
54
+ const next = input.next?.trim() || null;
55
+ const title = goal ? `Worklog: ${goal}` : 'Worklog entry';
56
+ const eventId = createEvent(db, {
57
+ projectId: input.projectId,
58
+ type: 'worklog',
59
+ title,
60
+ summary: done,
61
+ details: {
62
+ goal: goal ?? null,
63
+ done,
64
+ files,
65
+ risks,
66
+ next,
67
+ },
68
+ createdBy: input.by,
69
+ sessionId: input.sessionId,
70
+ });
71
+ return { eventId, files };
72
+ }
73
+ export function listRecentEvents(db, input) {
74
+ if (input.type) {
75
+ return db
76
+ .prepare(`SELECT id, type, title, summary, created_by, created_at FROM events WHERE project_id = ? AND type = ? ORDER BY created_at DESC LIMIT ?`)
77
+ .all(input.projectId, input.type, input.limit);
78
+ }
79
+ return db
80
+ .prepare(`SELECT id, type, title, summary, created_by, created_at FROM events WHERE project_id = ? ORDER BY created_at DESC LIMIT ?`)
81
+ .all(input.projectId, input.limit);
82
+ }
@@ -0,0 +1,47 @@
1
+ import { nowIso } from '../lib/format.js';
2
+ export function currentSession(db, projectId) {
3
+ return db
4
+ .prepare(`SELECT * FROM sessions WHERE project_id = ? AND ended_at IS NULL ORDER BY started_at DESC LIMIT 1`)
5
+ .get(projectId);
6
+ }
7
+ export function startSession(db, input) {
8
+ const active = currentSession(db, input.projectId);
9
+ if (active)
10
+ return { ok: false, reason: 'A session is already active' };
11
+ const info = db
12
+ .prepare(`INSERT INTO sessions (project_id, started_at, ended_at, actor_type, actor_name, summary) VALUES (?, ?, NULL, ?, ?, NULL)`)
13
+ .run(input.projectId, nowIso(), input.actor, input.name ?? null);
14
+ return { ok: true, sessionId: Number(info.lastInsertRowid) };
15
+ }
16
+ export function endSession(db, input) {
17
+ const active = currentSession(db, input.projectId);
18
+ if (!active)
19
+ return { ok: false, reason: 'No active session found' };
20
+ db.prepare(`UPDATE sessions SET ended_at = ?, summary = ? WHERE id = ?`).run(nowIso(), input.summary ?? null, active.id);
21
+ return { ok: true, sessionId: active.id };
22
+ }
23
+ export function verifySessionHygiene(db, projectId, summaryRecentlyRefreshed) {
24
+ const warnings = [];
25
+ const active = currentSession(db, projectId);
26
+ if (active) {
27
+ const worklog = db
28
+ .prepare(`SELECT id FROM events WHERE project_id = ? AND type = 'worklog' AND session_id = ? LIMIT 1`)
29
+ .get(projectId, active.id);
30
+ if (!worklog)
31
+ warnings.push('Active session has no worklog yet.');
32
+ }
33
+ const openTasks = db.prepare(`SELECT COUNT(*) as count FROM tasks WHERE project_id = ? AND status = 'open'`).get(projectId);
34
+ if (openTasks.count > 0 && !summaryRecentlyRefreshed) {
35
+ warnings.push('Open tasks exist and summary was not refreshed recently.');
36
+ }
37
+ const recentWork = db
38
+ .prepare(`SELECT COUNT(*) as count FROM events WHERE project_id = ? AND type = 'worklog' AND created_at >= datetime('now', '-3 day')`)
39
+ .get(projectId);
40
+ const recentDecisions = db
41
+ .prepare(`SELECT COUNT(*) as count FROM events WHERE project_id = ? AND type = 'decision' AND created_at >= datetime('now', '-7 day')`)
42
+ .get(projectId);
43
+ if (recentWork.count > 0 && recentDecisions.count === 0) {
44
+ warnings.push('Recent worklogs exist but no recent decision was recorded.');
45
+ }
46
+ return warnings;
47
+ }