@gzmagyari/kanbanboard 1.0.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/db.mjs ADDED
@@ -0,0 +1,378 @@
1
+ import Database from 'better-sqlite3';
2
+ import { getDbPath, ensureDirectories } from './lib/paths.mjs';
3
+
4
+ ensureDirectories();
5
+
6
+ const dbPath = getDbPath();
7
+ export const db = new Database(dbPath);
8
+
9
+ db.pragma('journal_mode = WAL');
10
+ db.pragma('foreign_keys = ON');
11
+
12
+ // Schema versioning (SQLite PRAGMA user_version)
13
+ export const SCHEMA_VERSION = 12;
14
+ export const DEFAULT_PROJECT_ID = 'default';
15
+
16
+ export const TASK_COLUMNS = [
17
+ { key: 'ideas', name: 'Ideas' },
18
+ { key: 'todo', name: 'To do' },
19
+ { key: 'in_progress', name: 'In Progress' },
20
+ { key: 'review', name: 'Review' },
21
+ { key: 'testing', name: 'Testing' },
22
+ { key: 'done', name: 'Done' }
23
+ ];
24
+
25
+ export const AUTO_COLUMNS = [
26
+ { key: 'ideas', name: 'Ideas' },
27
+ { key: 'active', name: 'Active' }
28
+ ];
29
+
30
+ export const ENTITY_TYPES = {
31
+ task: 'task',
32
+ recurring: 'recurring',
33
+ scheduled: 'scheduled',
34
+ milestone: 'milestone'
35
+ };
36
+
37
+ export function assertTaskStatus(status) {
38
+ if (!TASK_COLUMNS.some(c => c.key === status)) {
39
+ const err = new Error(`Invalid status: ${status}`);
40
+ err.statusCode = 400;
41
+ throw err;
42
+ }
43
+ }
44
+
45
+ export function assertAutoStatus(status) {
46
+ if (!AUTO_COLUMNS.some(c => c.key === status)) {
47
+ const err = new Error(`Invalid status: ${status}`);
48
+ err.statusCode = 400;
49
+ throw err;
50
+ }
51
+ }
52
+
53
+ export const MILESTONE_COLUMNS = [
54
+ { key: 'ideas', name: 'Ideas' },
55
+ { key: 'todo', name: 'To Do' },
56
+ { key: 'in_progress', name: 'In Progress' },
57
+ { key: 'done', name: 'Done' }
58
+ ];
59
+
60
+ export function assertMilestoneStatus(status) {
61
+ if (!MILESTONE_COLUMNS.some(c => c.key === status)) {
62
+ const err = new Error(`Invalid milestone status: ${status}`);
63
+ err.statusCode = 400;
64
+ throw err;
65
+ }
66
+ }
67
+
68
+ export const AGENT_STATUSES = ['active', 'inactive'];
69
+
70
+ export function assertAgentStatus(status) {
71
+ if (!AGENT_STATUSES.includes(status)) {
72
+ const err = new Error(`Invalid agent status: ${status}`);
73
+ err.statusCode = 400;
74
+ throw err;
75
+ }
76
+ }
77
+
78
+ function tableColumns(tableName) {
79
+ try {
80
+ return db.prepare(`PRAGMA table_info(${tableName})`).all().map(r => r.name);
81
+ } catch {
82
+ return [];
83
+ }
84
+ }
85
+
86
+ function hasColumn(tableName, colName) {
87
+ return tableColumns(tableName).includes(colName);
88
+ }
89
+
90
+ function addColumnIfMissing(tableName, colName, ddl) {
91
+ if (hasColumn(tableName, colName)) return;
92
+ db.exec(`ALTER TABLE ${tableName} ADD COLUMN ${ddl};`);
93
+ }
94
+
95
+ export function migrate() {
96
+ const currentVersion = db.pragma('user_version', { simple: true });
97
+
98
+ // Base tables (create with latest shape; older DBs will be ALTERed below)
99
+ // IMPORTANT: do not create indexes referencing new columns until after ALTER TABLE.
100
+ db.exec(`
101
+ CREATE TABLE IF NOT EXISTS tasks (
102
+ id TEXT PRIMARY KEY,
103
+ title TEXT NOT NULL,
104
+ description TEXT NOT NULL DEFAULT '',
105
+ status TEXT NOT NULL,
106
+ detail_text TEXT NOT NULL DEFAULT '',
107
+ project_id TEXT NOT NULL DEFAULT '${DEFAULT_PROJECT_ID}',
108
+ parent_id TEXT,
109
+ deleted_at INTEGER,
110
+ created_at INTEGER NOT NULL,
111
+ updated_at INTEGER NOT NULL
112
+ );
113
+ CREATE INDEX IF NOT EXISTS idx_tasks_status_updated ON tasks(status, updated_at);
114
+
115
+ CREATE TABLE IF NOT EXISTS automation_tasks (
116
+ id TEXT PRIMARY KEY,
117
+ type TEXT NOT NULL, -- 'recurring' | 'scheduled'
118
+ title TEXT NOT NULL,
119
+ description TEXT NOT NULL DEFAULT '',
120
+ status TEXT NOT NULL DEFAULT 'ideas', -- 'ideas' | 'active'
121
+ detail_text TEXT NOT NULL DEFAULT '',
122
+ project_id TEXT NOT NULL DEFAULT '${DEFAULT_PROJECT_ID}',
123
+
124
+ schedule_kind TEXT NOT NULL, -- 'cron' | 'at'
125
+ expr TEXT,
126
+ tz TEXT,
127
+ at_iso TEXT,
128
+
129
+ cron_job_id TEXT,
130
+ cron_enabled INTEGER NOT NULL DEFAULT 1,
131
+ last_synced_at INTEGER,
132
+ last_sync_error TEXT,
133
+
134
+ created_at INTEGER NOT NULL,
135
+ updated_at INTEGER NOT NULL
136
+ );
137
+ CREATE INDEX IF NOT EXISTS idx_auto_type_status_updated ON automation_tasks(type, status, updated_at);
138
+ CREATE INDEX IF NOT EXISTS idx_auto_status_updated ON automation_tasks(status, updated_at);
139
+ `);
140
+
141
+ // Detect old comments schema (task_id) and migrate before we create new indexes.
142
+ const commentsCols = tableColumns('comments');
143
+
144
+ const hasOld = commentsCols.includes('task_id');
145
+
146
+ if (hasOld) {
147
+ // Old schema existed; rename and rebuild.
148
+ db.exec(`ALTER TABLE comments RENAME TO comments_old;`);
149
+
150
+ const hasProgress = tableColumns('progress_updates').length > 0;
151
+ if (hasProgress) {
152
+ db.exec(`ALTER TABLE progress_updates RENAME TO progress_updates_old;`);
153
+ }
154
+
155
+ db.exec(`
156
+ CREATE TABLE comments (
157
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
158
+ entity_type TEXT NOT NULL DEFAULT 'task',
159
+ entity_id TEXT NOT NULL,
160
+ author TEXT NOT NULL DEFAULT 'Gabor',
161
+ text TEXT NOT NULL,
162
+ created_at INTEGER NOT NULL
163
+ );
164
+
165
+ CREATE TABLE progress_updates (
166
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
167
+ entity_type TEXT NOT NULL DEFAULT 'task',
168
+ entity_id TEXT NOT NULL,
169
+ author TEXT NOT NULL DEFAULT 'Jarvis',
170
+ text TEXT NOT NULL,
171
+ created_at INTEGER NOT NULL
172
+ );
173
+ `);
174
+
175
+ db.exec(`
176
+ INSERT INTO comments(id, entity_type, entity_id, author, text, created_at)
177
+ SELECT id, 'task', task_id, author, text, created_at FROM comments_old;
178
+ `);
179
+
180
+ if (hasProgress) {
181
+ db.exec(`
182
+ INSERT INTO progress_updates(id, entity_type, entity_id, author, text, created_at)
183
+ SELECT id, 'task', task_id, author, text, created_at FROM progress_updates_old;
184
+ `);
185
+ }
186
+
187
+ db.exec(`
188
+ DROP TABLE comments_old;
189
+ DROP TABLE IF EXISTS progress_updates_old;
190
+ `);
191
+ }
192
+
193
+ // Create shared tables if they still don't exist (fresh DB)
194
+ db.exec(`
195
+ CREATE TABLE IF NOT EXISTS comments (
196
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
197
+ entity_type TEXT NOT NULL DEFAULT 'task',
198
+ entity_id TEXT NOT NULL,
199
+ author TEXT NOT NULL DEFAULT 'Gabor',
200
+ text TEXT NOT NULL,
201
+ created_at INTEGER NOT NULL
202
+ );
203
+
204
+ CREATE TABLE IF NOT EXISTS progress_updates (
205
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
206
+ entity_type TEXT NOT NULL DEFAULT 'task',
207
+ entity_id TEXT NOT NULL,
208
+ author TEXT NOT NULL DEFAULT 'Jarvis',
209
+ text TEXT NOT NULL,
210
+ created_at INTEGER NOT NULL
211
+ );
212
+
213
+ CREATE INDEX IF NOT EXISTS idx_comments_entity_created ON comments(entity_type, entity_id, created_at);
214
+ CREATE INDEX IF NOT EXISTS idx_updates_entity_created ON progress_updates(entity_type, entity_id, created_at);
215
+ `);
216
+
217
+ // Projects
218
+ db.exec(`
219
+ CREATE TABLE IF NOT EXISTS projects (
220
+ id TEXT PRIMARY KEY,
221
+ name TEXT NOT NULL,
222
+ scope_text TEXT NOT NULL DEFAULT '',
223
+ repo_path TEXT,
224
+ created_at INTEGER NOT NULL,
225
+ updated_at INTEGER NOT NULL
226
+ );
227
+ CREATE INDEX IF NOT EXISTS idx_projects_updated ON projects(updated_at);
228
+ `);
229
+
230
+ // Ensure new columns exist for older DBs
231
+ addColumnIfMissing('tasks', 'project_id', `project_id TEXT NOT NULL DEFAULT '${DEFAULT_PROJECT_ID}'`);
232
+ addColumnIfMissing('tasks', 'parent_id', `parent_id TEXT`);
233
+ addColumnIfMissing('tasks', 'deleted_at', `deleted_at INTEGER`);
234
+ addColumnIfMissing('automation_tasks', 'project_id', `project_id TEXT NOT NULL DEFAULT '${DEFAULT_PROJECT_ID}'`);
235
+ addColumnIfMissing('projects', 'repo_path', `repo_path TEXT`);
236
+ addColumnIfMissing('projects', 'status_text', `status_text TEXT NOT NULL DEFAULT ''`);
237
+ addColumnIfMissing('projects', 'guidelines_text', `guidelines_text TEXT NOT NULL DEFAULT ''`);
238
+ addColumnIfMissing('tasks', 'milestone_id', `milestone_id TEXT`);
239
+ addColumnIfMissing('tasks', 'assigned_agent_id', `assigned_agent_id TEXT`);
240
+ addColumnIfMissing('agents', 'role', `role TEXT`);
241
+
242
+ // Ensure a default project always exists
243
+ const now = Date.now();
244
+ db.prepare(`
245
+ INSERT OR IGNORE INTO projects (id, name, scope_text, created_at, updated_at)
246
+ VALUES (?, ?, ?, ?, ?)
247
+ `).run(DEFAULT_PROJECT_ID, 'General', '', now, now);
248
+
249
+ // Backfill any NULL/empty project_id values
250
+ db.prepare(`UPDATE tasks SET project_id = ? WHERE project_id IS NULL OR project_id = ''`).run(DEFAULT_PROJECT_ID);
251
+ db.prepare(`UPDATE automation_tasks SET project_id = ? WHERE project_id IS NULL OR project_id = ''`).run(DEFAULT_PROJECT_ID);
252
+
253
+ // Indexes that rely on new columns (create after ALTER TABLE)
254
+ db.exec(`
255
+ CREATE INDEX IF NOT EXISTS idx_tasks_project_status_updated ON tasks(project_id, status, updated_at);
256
+ CREATE INDEX IF NOT EXISTS idx_tasks_project_parent_updated ON tasks(project_id, parent_id, updated_at);
257
+ CREATE INDEX IF NOT EXISTS idx_tasks_project_deleted_updated ON tasks(project_id, deleted_at, updated_at);
258
+ CREATE INDEX IF NOT EXISTS idx_tasks_parent_deleted ON tasks(parent_id, deleted_at);
259
+
260
+ CREATE INDEX IF NOT EXISTS idx_auto_project_type_status_updated ON automation_tasks(project_id, type, status, updated_at);
261
+ CREATE INDEX IF NOT EXISTS idx_auto_project_status_updated ON automation_tasks(project_id, status, updated_at);
262
+ `);
263
+
264
+ // Agent role uniqueness (partial unique index — only where role IS NOT NULL)
265
+ db.exec(`
266
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_agents_project_role ON agents(project_id, role) WHERE role IS NOT NULL;
267
+ CREATE INDEX IF NOT EXISTS idx_tasks_assigned_agent ON tasks(assigned_agent_id);
268
+ `);
269
+
270
+ // LLM run audit log (Phase 3)
271
+ db.exec(`
272
+ CREATE TABLE IF NOT EXISTS llm_runs (
273
+ id TEXT PRIMARY KEY,
274
+ op TEXT NOT NULL,
275
+ project_id TEXT,
276
+ entity_type TEXT,
277
+ entity_id TEXT,
278
+ model TEXT NOT NULL,
279
+ request_json TEXT NOT NULL,
280
+ response_json TEXT,
281
+ error TEXT,
282
+ created_at INTEGER NOT NULL
283
+ );
284
+ CREATE INDEX IF NOT EXISTS idx_llm_runs_created ON llm_runs(created_at);
285
+ CREATE INDEX IF NOT EXISTS idx_llm_runs_project_created ON llm_runs(project_id, created_at);
286
+ `);
287
+
288
+ // LLM plans (Phase 4: plan -> apply)
289
+ db.exec(`
290
+ CREATE TABLE IF NOT EXISTS llm_plans (
291
+ id TEXT PRIMARY KEY,
292
+ op TEXT NOT NULL,
293
+ project_id TEXT NOT NULL,
294
+ input_text TEXT NOT NULL,
295
+ plan_json TEXT NOT NULL,
296
+ llm_run_id TEXT,
297
+ applied INTEGER NOT NULL DEFAULT 0,
298
+ applied_at INTEGER,
299
+ created_at INTEGER NOT NULL
300
+ );
301
+ CREATE INDEX IF NOT EXISTS idx_llm_plans_project_created ON llm_plans(project_id, created_at);
302
+ CREATE INDEX IF NOT EXISTS idx_llm_plans_op_created ON llm_plans(op, created_at);
303
+ `);
304
+
305
+ // Milestones
306
+ db.exec(`
307
+ CREATE TABLE IF NOT EXISTS milestones (
308
+ id TEXT PRIMARY KEY,
309
+ project_id TEXT NOT NULL,
310
+ title TEXT NOT NULL,
311
+ description TEXT NOT NULL DEFAULT '',
312
+ status TEXT NOT NULL DEFAULT 'ideas',
313
+ target_date TEXT,
314
+ created_at INTEGER NOT NULL,
315
+ updated_at INTEGER NOT NULL
316
+ );
317
+ CREATE INDEX IF NOT EXISTS idx_milestones_project_status ON milestones(project_id, status, updated_at);
318
+ CREATE INDEX IF NOT EXISTS idx_tasks_milestone_id ON tasks(milestone_id);
319
+ `);
320
+
321
+ // Chat messages (per-project LLM chat)
322
+ db.exec(`
323
+ CREATE TABLE IF NOT EXISTS chat_messages (
324
+ id TEXT PRIMARY KEY,
325
+ project_id TEXT NOT NULL,
326
+ role TEXT NOT NULL,
327
+ content TEXT,
328
+ tool_calls_json TEXT,
329
+ tool_call_id TEXT,
330
+ tool_name TEXT,
331
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
332
+ FOREIGN KEY (project_id) REFERENCES projects(id)
333
+ );
334
+ CREATE INDEX IF NOT EXISTS idx_chat_messages_project ON chat_messages(project_id, created_at);
335
+ `);
336
+
337
+ // Agents (configurable Claude Code agents with session persistence)
338
+ db.exec(`
339
+ CREATE TABLE IF NOT EXISTS agents (
340
+ id TEXT PRIMARY KEY,
341
+ project_id TEXT NOT NULL,
342
+ name TEXT NOT NULL,
343
+ description TEXT NOT NULL DEFAULT '',
344
+ system_prompt TEXT NOT NULL DEFAULT '',
345
+ status TEXT NOT NULL DEFAULT 'active',
346
+ claude_session_id TEXT,
347
+ repo_path TEXT,
348
+ allowed_tools TEXT NOT NULL DEFAULT '',
349
+ max_turns INTEGER DEFAULT 25,
350
+ dangerously_skip_permissions INTEGER NOT NULL DEFAULT 0,
351
+ created_at INTEGER NOT NULL,
352
+ updated_at INTEGER NOT NULL
353
+ );
354
+ CREATE INDEX IF NOT EXISTS idx_agents_project ON agents(project_id, updated_at);
355
+ `);
356
+
357
+ // Agent run audit log
358
+ db.exec(`
359
+ CREATE TABLE IF NOT EXISTS agent_runs (
360
+ id TEXT PRIMARY KEY,
361
+ agent_id TEXT NOT NULL,
362
+ prompt TEXT NOT NULL,
363
+ result_text TEXT,
364
+ session_id_used TEXT,
365
+ session_id_after TEXT,
366
+ status TEXT NOT NULL DEFAULT 'running',
367
+ error TEXT,
368
+ duration_ms INTEGER,
369
+ created_at INTEGER NOT NULL,
370
+ completed_at INTEGER
371
+ );
372
+ CREATE INDEX IF NOT EXISTS idx_agent_runs_agent ON agent_runs(agent_id, created_at);
373
+ `);
374
+
375
+ if (currentVersion < SCHEMA_VERSION) {
376
+ db.pragma(`user_version = ${SCHEMA_VERSION}`);
377
+ }
378
+ }
@@ -0,0 +1,202 @@
1
+ # Project Manager Chat
2
+
3
+ A per-project conversational AI assistant that can answer questions about the project and create, update, edit, and delete tasks and milestones via natural language.
4
+
5
+ ## Overview
6
+
7
+ The Chat feature adds a conversational interface to each project. The LLM receives full project context (scope, status, all tasks, all milestones) and can both answer questions and perform actions using OpenAI-compatible tool calls (function calling).
8
+
9
+ Messages are stored per-project. The LLM sees the last 50 messages as conversation history, maintaining continuity across requests.
10
+
11
+ ## Architecture
12
+
13
+ ### Database
14
+
15
+ **Table: `chat_messages`** (schema v9)
16
+
17
+ | Column | Type | Description |
18
+ |--------|------|-------------|
19
+ | `id` | TEXT PK | nanoid |
20
+ | `project_id` | TEXT FK | Links to projects table |
21
+ | `role` | TEXT | `user`, `assistant`, or `tool` |
22
+ | `content` | TEXT | Message text (NULL for tool-call-only assistant messages) |
23
+ | `tool_calls_json` | TEXT | JSON array of tool calls (assistant messages only) |
24
+ | `tool_call_id` | TEXT | References the tool call being responded to (tool messages only) |
25
+ | `tool_name` | TEXT | Which tool was called (tool messages only) |
26
+ | `created_at` | TEXT | ISO 8601 timestamp |
27
+
28
+ ### LLM Integration
29
+
30
+ Uses the new `llmChatWithTools()` function in `llm.mjs`, which differs from `llmChat()`:
31
+ - Does **not** force `response_format: json_object`
32
+ - Accepts `tools` and `tool_choice` parameters
33
+ - Returns `{ run_id, url, data, content, tool_calls }`
34
+
35
+ ### Tool-Call Loop
36
+
37
+ When the user sends a message:
38
+
39
+ 1. User message is saved to `chat_messages`
40
+ 2. Last 50 messages are loaded and converted to OpenAI message format
41
+ 3. System prompt is built with full project context
42
+ 4. LLM is called with 8 tool definitions
43
+ 5. If the LLM returns tool calls:
44
+ - Each tool call is executed against the database
45
+ - Results are saved as `role='tool'` messages
46
+ - The LLM is called again with the updated conversation
47
+ 6. This loops up to **5 iterations** until the LLM returns a text-only response
48
+ 7. The final assistant message is saved and returned
49
+
50
+ ## API Endpoints
51
+
52
+ All endpoints require the `X-API-Key` header if `AI_API_KEY` is configured.
53
+
54
+ ### Send Message
55
+
56
+ ```
57
+ POST /api/projects/:id/chat
58
+ ```
59
+
60
+ **Request:**
61
+ ```json
62
+ {
63
+ "message": "What should I work on next?"
64
+ }
65
+ ```
66
+
67
+ **Response:**
68
+ ```json
69
+ {
70
+ "message": {
71
+ "id": "abc123",
72
+ "role": "assistant",
73
+ "content": "Based on the current status, I'd suggest...",
74
+ "created_at": "2026-02-11T20:00:00.000Z"
75
+ },
76
+ "actions_performed": [
77
+ {
78
+ "tool": "create_task",
79
+ "args": { "title": "Fix login bug", "status": "todo" },
80
+ "result": { "ok": true, "action": "created_task", "task_id": "xyz789", "title": "Fix login bug" }
81
+ }
82
+ ]
83
+ }
84
+ ```
85
+
86
+ ### Get Conversation History
87
+
88
+ ```
89
+ GET /api/projects/:id/chat?limit=50
90
+ ```
91
+
92
+ Returns only `user` and `assistant` messages with content (tool-call plumbing messages are filtered out). Messages are ordered oldest-first.
93
+
94
+ **Response:**
95
+ ```json
96
+ {
97
+ "messages": [
98
+ {
99
+ "id": "msg1",
100
+ "role": "user",
101
+ "content": "Create a task for fixing the login bug",
102
+ "tool_calls_json": null,
103
+ "created_at": "2026-02-11T20:00:00.000Z"
104
+ },
105
+ {
106
+ "id": "msg2",
107
+ "role": "assistant",
108
+ "content": "Done! I've created the task 'Fix login bug' in the todo column.",
109
+ "tool_calls_json": "[{\"id\":\"call_1\",\"function\":{\"name\":\"create_task\",...}}]",
110
+ "created_at": "2026-02-11T20:00:01.000Z"
111
+ }
112
+ ]
113
+ }
114
+ ```
115
+
116
+ ### Clear Conversation
117
+
118
+ ```
119
+ DELETE /api/projects/:id/chat
120
+ ```
121
+
122
+ Deletes all messages for the project (including tool-call messages).
123
+
124
+ **Response:**
125
+ ```json
126
+ {
127
+ "cleared": 42
128
+ }
129
+ ```
130
+
131
+ ## Available Tools
132
+
133
+ The LLM has access to 8 tools for managing tasks and milestones:
134
+
135
+ ### Task Tools
136
+
137
+ | Tool | Description |
138
+ |------|-------------|
139
+ | `create_task` | Create a new task. Params: `title` (required), `description`, `detail_text`, `status`, `parent_id`, `milestone_id` |
140
+ | `update_task` | Update task fields. Params: `task_id` (required), plus any field to change |
141
+ | `edit_task_field` | Surgical find-and-replace on a text field. Params: `task_id`, `field` (title/description/detail_text), `old_text`, `new_text` |
142
+ | `delete_task` | Soft-delete a task and all its descendants. Params: `task_id` |
143
+
144
+ ### Milestone Tools
145
+
146
+ | Tool | Description |
147
+ |------|-------------|
148
+ | `create_milestone` | Create a new milestone. Params: `title` (required), `description`, `status`, `target_date` |
149
+ | `update_milestone` | Update milestone fields. Params: `milestone_id` (required), plus any field to change |
150
+ | `edit_milestone_field` | Surgical find-and-replace on a text field. Params: `milestone_id`, `field` (title/description), `old_text`, `new_text` |
151
+ | `delete_milestone` | Delete a milestone and unlink its tasks. Params: `milestone_id` |
152
+
153
+ ### Surgical Edit Tools
154
+
155
+ The `edit_task_field` and `edit_milestone_field` tools allow the LLM to make precise text edits without rewriting entire fields. The system prompt instructs the LLM to prefer these for small changes.
156
+
157
+ Example: To change "Implment" to "Implement" in a task description, the LLM calls:
158
+ ```json
159
+ {
160
+ "name": "edit_task_field",
161
+ "arguments": {
162
+ "task_id": "abc123",
163
+ "field": "description",
164
+ "old_text": "Implment",
165
+ "new_text": "Implement"
166
+ }
167
+ }
168
+ ```
169
+
170
+ ## System Prompt
171
+
172
+ The LLM receives a system message containing:
173
+
174
+ 1. **Role instruction** — "You are a Project Manager assistant for {project name}"
175
+ 2. **Project scope** — The full `scope_text` (PRD/requirements)
176
+ 3. **Current status** — The `status_text` summary
177
+ 4. **All milestones** — Status, title, ID, task progress (e.g., "3/7 tasks done")
178
+ 5. **All tasks** — ID, status, title, parent, milestone, description snippet, detail snippet (up to 500 tasks, 20K chars)
179
+
180
+ ## Frontend
181
+
182
+ The Chat tab (tab index 4) appears after Milestones in the tab bar.
183
+
184
+ **Features:**
185
+ - Message bubbles: user messages right-aligned (blue), assistant messages left-aligned (dark)
186
+ - Tool call summary chip on assistant messages that performed actions (shows tool names)
187
+ - Loading spinner with "Thinking..." text while waiting for LLM response
188
+ - Auto-scroll to bottom on new messages
189
+ - Enter key sends message, Shift+Enter for newline
190
+ - Clear button with confirmation dialog
191
+ - Placeholder message when no project is selected
192
+ - Chat auto-loads when switching to the tab or changing projects
193
+ - Task/milestone boards auto-refresh when the LLM performs actions
194
+
195
+ ## Files Changed
196
+
197
+ | File | Changes |
198
+ |------|---------|
199
+ | `db.mjs` | Schema v9, `chat_messages` table + index |
200
+ | `llm.mjs` | `llmChatWithTools()` function |
201
+ | `server.mjs` | 8 tool definitions, `buildChatSystemMessage()`, `executeChatToolCall()`, `chatMessagesToLLMMessages()`, 3 API endpoints |
202
+ | `public/index.html` | Chat tab UI, data properties, methods, tab/project watchers |