tlc-claude-code 1.4.8 → 1.4.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/package.json +1 -1
  2. package/server/index.js +229 -14
  3. package/server/lib/compliance/control-mapper.js +401 -0
  4. package/server/lib/compliance/control-mapper.test.js +117 -0
  5. package/server/lib/compliance/evidence-linker.js +296 -0
  6. package/server/lib/compliance/evidence-linker.test.js +121 -0
  7. package/server/lib/compliance/gdpr-checklist.js +416 -0
  8. package/server/lib/compliance/gdpr-checklist.test.js +131 -0
  9. package/server/lib/compliance/hipaa-checklist.js +277 -0
  10. package/server/lib/compliance/hipaa-checklist.test.js +101 -0
  11. package/server/lib/compliance/iso27001-checklist.js +287 -0
  12. package/server/lib/compliance/iso27001-checklist.test.js +99 -0
  13. package/server/lib/compliance/multi-framework-reporter.js +284 -0
  14. package/server/lib/compliance/multi-framework-reporter.test.js +127 -0
  15. package/server/lib/compliance/pci-dss-checklist.js +214 -0
  16. package/server/lib/compliance/pci-dss-checklist.test.js +95 -0
  17. package/server/lib/compliance/trust-centre.js +187 -0
  18. package/server/lib/compliance/trust-centre.test.js +93 -0
  19. package/server/lib/dashboard/api-server.js +155 -0
  20. package/server/lib/dashboard/api-server.test.js +155 -0
  21. package/server/lib/dashboard/health-api.js +199 -0
  22. package/server/lib/dashboard/health-api.test.js +122 -0
  23. package/server/lib/dashboard/notes-api.js +234 -0
  24. package/server/lib/dashboard/notes-api.test.js +134 -0
  25. package/server/lib/dashboard/router-api.js +176 -0
  26. package/server/lib/dashboard/router-api.test.js +132 -0
  27. package/server/lib/dashboard/tasks-api.js +289 -0
  28. package/server/lib/dashboard/tasks-api.test.js +161 -0
  29. package/server/lib/dashboard/tlc-introspection.js +197 -0
  30. package/server/lib/dashboard/tlc-introspection.test.js +138 -0
  31. package/server/lib/dashboard/version-api.js +222 -0
  32. package/server/lib/dashboard/version-api.test.js +112 -0
  33. package/server/lib/dashboard/websocket-server.js +104 -0
  34. package/server/lib/dashboard/websocket-server.test.js +118 -0
  35. package/server/lib/deploy/branch-classifier.js +163 -0
  36. package/server/lib/deploy/branch-classifier.test.js +164 -0
  37. package/server/lib/deploy/deployment-approval.js +299 -0
  38. package/server/lib/deploy/deployment-approval.test.js +296 -0
  39. package/server/lib/deploy/deployment-audit.js +374 -0
  40. package/server/lib/deploy/deployment-audit.test.js +307 -0
  41. package/server/lib/deploy/deployment-executor.js +335 -0
  42. package/server/lib/deploy/deployment-executor.test.js +329 -0
  43. package/server/lib/deploy/deployment-rules.js +163 -0
  44. package/server/lib/deploy/deployment-rules.test.js +188 -0
  45. package/server/lib/deploy/rollback-manager.js +379 -0
  46. package/server/lib/deploy/rollback-manager.test.js +321 -0
  47. package/server/lib/deploy/security-gates.js +236 -0
  48. package/server/lib/deploy/security-gates.test.js +222 -0
  49. package/server/lib/k8s/gitops-config.js +188 -0
  50. package/server/lib/k8s/gitops-config.test.js +59 -0
  51. package/server/lib/k8s/helm-generator.js +196 -0
  52. package/server/lib/k8s/helm-generator.test.js +59 -0
  53. package/server/lib/k8s/kustomize-generator.js +176 -0
  54. package/server/lib/k8s/kustomize-generator.test.js +58 -0
  55. package/server/lib/k8s/network-policy.js +114 -0
  56. package/server/lib/k8s/network-policy.test.js +53 -0
  57. package/server/lib/k8s/pod-security.js +114 -0
  58. package/server/lib/k8s/pod-security.test.js +55 -0
  59. package/server/lib/k8s/rbac-generator.js +132 -0
  60. package/server/lib/k8s/rbac-generator.test.js +57 -0
  61. package/server/lib/k8s/resource-manager.js +172 -0
  62. package/server/lib/k8s/resource-manager.test.js +60 -0
  63. package/server/lib/k8s/secrets-encryption.js +168 -0
  64. package/server/lib/k8s/secrets-encryption.test.js +49 -0
  65. package/server/lib/monitoring/alert-manager.js +238 -0
  66. package/server/lib/monitoring/alert-manager.test.js +106 -0
  67. package/server/lib/monitoring/health-check.js +226 -0
  68. package/server/lib/monitoring/health-check.test.js +176 -0
  69. package/server/lib/monitoring/incident-manager.js +230 -0
  70. package/server/lib/monitoring/incident-manager.test.js +98 -0
  71. package/server/lib/monitoring/log-aggregator.js +147 -0
  72. package/server/lib/monitoring/log-aggregator.test.js +89 -0
  73. package/server/lib/monitoring/metrics-collector.js +337 -0
  74. package/server/lib/monitoring/metrics-collector.test.js +172 -0
  75. package/server/lib/monitoring/status-page.js +214 -0
  76. package/server/lib/monitoring/status-page.test.js +105 -0
  77. package/server/lib/monitoring/uptime-monitor.js +194 -0
  78. package/server/lib/monitoring/uptime-monitor.test.js +109 -0
  79. package/server/lib/network/fail2ban-config.js +294 -0
  80. package/server/lib/network/fail2ban-config.test.js +275 -0
  81. package/server/lib/network/firewall-manager.js +252 -0
  82. package/server/lib/network/firewall-manager.test.js +254 -0
  83. package/server/lib/network/geoip-filter.js +282 -0
  84. package/server/lib/network/geoip-filter.test.js +264 -0
  85. package/server/lib/network/rate-limiter.js +229 -0
  86. package/server/lib/network/rate-limiter.test.js +293 -0
  87. package/server/lib/network/request-validator.js +351 -0
  88. package/server/lib/network/request-validator.test.js +345 -0
  89. package/server/lib/network/security-headers.js +251 -0
  90. package/server/lib/network/security-headers.test.js +283 -0
  91. package/server/lib/network/tls-config.js +210 -0
  92. package/server/lib/network/tls-config.test.js +248 -0
  93. package/server/lib/security/auth-security.js +369 -0
  94. package/server/lib/security/auth-security.test.js +448 -0
  95. package/server/lib/security/cis-benchmark.js +152 -0
  96. package/server/lib/security/cis-benchmark.test.js +137 -0
  97. package/server/lib/security/compose-templates.js +312 -0
  98. package/server/lib/security/compose-templates.test.js +229 -0
  99. package/server/lib/security/container-runtime.js +456 -0
  100. package/server/lib/security/container-runtime.test.js +503 -0
  101. package/server/lib/security/cors-validator.js +278 -0
  102. package/server/lib/security/cors-validator.test.js +310 -0
  103. package/server/lib/security/crypto-utils.js +253 -0
  104. package/server/lib/security/crypto-utils.test.js +409 -0
  105. package/server/lib/security/dockerfile-linter.js +459 -0
  106. package/server/lib/security/dockerfile-linter.test.js +483 -0
  107. package/server/lib/security/dockerfile-templates.js +278 -0
  108. package/server/lib/security/dockerfile-templates.test.js +164 -0
  109. package/server/lib/security/error-sanitizer.js +426 -0
  110. package/server/lib/security/error-sanitizer.test.js +331 -0
  111. package/server/lib/security/headers-generator.js +368 -0
  112. package/server/lib/security/headers-generator.test.js +398 -0
  113. package/server/lib/security/image-scanner.js +83 -0
  114. package/server/lib/security/image-scanner.test.js +106 -0
  115. package/server/lib/security/input-validator.js +352 -0
  116. package/server/lib/security/input-validator.test.js +330 -0
  117. package/server/lib/security/network-policy.js +174 -0
  118. package/server/lib/security/network-policy.test.js +164 -0
  119. package/server/lib/security/output-encoder.js +237 -0
  120. package/server/lib/security/output-encoder.test.js +276 -0
  121. package/server/lib/security/path-validator.js +359 -0
  122. package/server/lib/security/path-validator.test.js +293 -0
  123. package/server/lib/security/query-builder.js +421 -0
  124. package/server/lib/security/query-builder.test.js +318 -0
  125. package/server/lib/security/secret-detector.js +290 -0
  126. package/server/lib/security/secret-detector.test.js +354 -0
  127. package/server/lib/security/secrets-validator.js +137 -0
  128. package/server/lib/security/secrets-validator.test.js +120 -0
  129. package/server/lib/security-testing/dast-runner.js +154 -0
  130. package/server/lib/security-testing/dast-runner.test.js +62 -0
  131. package/server/lib/security-testing/dependency-scanner.js +172 -0
  132. package/server/lib/security-testing/dependency-scanner.test.js +64 -0
  133. package/server/lib/security-testing/pentest-runner.js +230 -0
  134. package/server/lib/security-testing/pentest-runner.test.js +60 -0
  135. package/server/lib/security-testing/sast-runner.js +136 -0
  136. package/server/lib/security-testing/sast-runner.test.js +62 -0
  137. package/server/lib/security-testing/secret-scanner.js +153 -0
  138. package/server/lib/security-testing/secret-scanner.test.js +66 -0
  139. package/server/lib/security-testing/security-gate.js +216 -0
  140. package/server/lib/security-testing/security-gate.test.js +115 -0
  141. package/server/lib/security-testing/security-reporter.js +303 -0
  142. package/server/lib/security-testing/security-reporter.test.js +114 -0
  143. package/server/lib/standards/audit-checker.js +546 -0
  144. package/server/lib/standards/audit-checker.test.js +415 -0
  145. package/server/lib/standards/cleanup-executor.js +452 -0
  146. package/server/lib/standards/cleanup-executor.test.js +293 -0
  147. package/server/lib/standards/refactor-stepper.js +425 -0
  148. package/server/lib/standards/refactor-stepper.test.js +298 -0
  149. package/server/lib/standards/standards-injector.js +167 -0
  150. package/server/lib/standards/standards-injector.test.js +232 -0
  151. package/server/lib/user-management.test.js +284 -0
  152. package/server/lib/vps/backup-manager.js +157 -0
  153. package/server/lib/vps/backup-manager.test.js +59 -0
  154. package/server/lib/vps/caddy-config.js +159 -0
  155. package/server/lib/vps/caddy-config.test.js +48 -0
  156. package/server/lib/vps/compose-orchestrator.js +219 -0
  157. package/server/lib/vps/compose-orchestrator.test.js +50 -0
  158. package/server/lib/vps/database-config.js +208 -0
  159. package/server/lib/vps/database-config.test.js +47 -0
  160. package/server/lib/vps/deploy-script.js +211 -0
  161. package/server/lib/vps/deploy-script.test.js +53 -0
  162. package/server/lib/vps/secrets-manager.js +148 -0
  163. package/server/lib/vps/secrets-manager.test.js +58 -0
  164. package/server/lib/vps/server-hardening.js +174 -0
  165. package/server/lib/vps/server-hardening.test.js +70 -0
  166. package/server/package-lock.json +19 -0
  167. package/server/package.json +1 -0
  168. package/server/templates/CLAUDE.md +37 -0
  169. package/server/templates/CODING-STANDARDS.md +408 -0
@@ -0,0 +1,289 @@
1
+ /**
2
+ * Tasks API Module
3
+ * REST API for task CRUD operations
4
+ */
5
+ import { promises as defaultFs } from 'fs';
6
+ import path from 'path';
7
+ import { createTlcIntrospection } from './tlc-introspection.js';
8
+
9
+ /**
10
+ * Parse tasks from PLAN.md content
11
+ * @param {string} content - Plan file content
12
+ * @returns {Array} Parsed tasks
13
+ */
14
+ export function parseTasksFromPlan(content) {
15
+ const tasks = [];
16
+ const lines = content.split('\n');
17
+
18
+ for (let i = 0; i < lines.length; i++) {
19
+ const line = lines[i];
20
+
21
+ // Match: ### Task N: Name [status@owner] or ### Task N: Name [status]
22
+ const taskMatch = line.match(/^###\s+Task\s+(\d+):\s*(.+?)\s*\[(x|>|\s*)(?:@(\w+))?\]/);
23
+ if (taskMatch) {
24
+ const number = parseInt(taskMatch[1], 10);
25
+ const subject = taskMatch[2].trim();
26
+ const marker = taskMatch[3];
27
+ const owner = taskMatch[4] || null;
28
+
29
+ let status;
30
+ if (marker === 'x') {
31
+ status = 'completed';
32
+ } else if (marker === '>') {
33
+ status = 'in_progress';
34
+ } else {
35
+ status = 'pending';
36
+ }
37
+
38
+ // Look for goal in next lines
39
+ let goal = null;
40
+ for (let j = i + 1; j < lines.length && j < i + 5; j++) {
41
+ const goalMatch = lines[j].match(/^\*\*Goal:\*\*\s*(.+)$/);
42
+ if (goalMatch) {
43
+ goal = goalMatch[1].trim();
44
+ break;
45
+ }
46
+ // Stop if we hit another task
47
+ if (lines[j].match(/^###\s+Task/)) break;
48
+ }
49
+
50
+ tasks.push({
51
+ number,
52
+ subject,
53
+ status,
54
+ owner,
55
+ goal
56
+ });
57
+ }
58
+ }
59
+
60
+ return tasks;
61
+ }
62
+
63
+ /**
64
+ * Format task for API response
65
+ * @param {Object} task - Raw task object
66
+ * @param {number} phase - Phase number
67
+ * @returns {Object} Formatted task
68
+ */
69
+ export function formatTaskForApi(task, phase) {
70
+ return {
71
+ id: `phase-${phase}-task-${task.number}`,
72
+ phase,
73
+ number: task.number,
74
+ subject: task.subject,
75
+ status: task.status,
76
+ owner: task.owner,
77
+ goal: task.goal
78
+ };
79
+ }
80
+
81
+ /**
82
+ * Get tasks from current phase
83
+ * @param {Object} options - Options
84
+ * @returns {Promise<Array>} Tasks array
85
+ */
86
+ export async function getTasks(options = {}) {
87
+ const { introspection, fs: fileSystem = defaultFs, basePath = process.cwd() } = options;
88
+
89
+ const currentPhase = introspection.getCurrentPhase();
90
+ const phaseNumber = currentPhase?.number || 1;
91
+
92
+ // Try to read the phase plan file
93
+ const planPath = path.join(basePath, '.planning', 'phases', `${phaseNumber}-PLAN.md`);
94
+
95
+ let content = '';
96
+ try {
97
+ content = await fileSystem.readFile(planPath, 'utf-8');
98
+ } catch {
99
+ content = '';
100
+ }
101
+
102
+ const tasks = parseTasksFromPlan(content);
103
+ return tasks.map(task => formatTaskForApi(task, phaseNumber));
104
+ }
105
+
106
+ /**
107
+ * Create a new task
108
+ * @param {Object} taskData - Task data
109
+ * @param {Object} options - Options
110
+ * @returns {Promise<Object>} Created task
111
+ */
112
+ export async function createTask(taskData, options = {}) {
113
+ if (!taskData.subject) {
114
+ throw new Error('Subject is required');
115
+ }
116
+
117
+ const { fs: fileSystem = defaultFs, basePath = process.cwd() } = options;
118
+ const phase = taskData.phase || 1;
119
+
120
+ const planPath = path.join(basePath, '.planning', 'phases', `${phase}-PLAN.md`);
121
+
122
+ let content = '';
123
+ try {
124
+ content = await fileSystem.readFile(planPath, 'utf-8');
125
+ } catch {
126
+ content = '## Tasks\n';
127
+ }
128
+
129
+ // Find highest task number
130
+ const existingTasks = parseTasksFromPlan(content);
131
+ const maxNumber = existingTasks.reduce((max, t) => Math.max(max, t.number), 0);
132
+ const newNumber = maxNumber + 1;
133
+
134
+ // Create task markdown
135
+ const taskMd = `\n### Task ${newNumber}: ${taskData.subject} [ ]\n**Goal:** ${taskData.description || 'TBD'}\n`;
136
+
137
+ // Append to content
138
+ const newContent = content + taskMd;
139
+ await fileSystem.writeFile(planPath, newContent);
140
+
141
+ return {
142
+ id: `phase-${phase}-task-${newNumber}`,
143
+ phase,
144
+ number: newNumber,
145
+ subject: taskData.subject,
146
+ status: 'pending',
147
+ owner: null,
148
+ goal: taskData.description || 'TBD'
149
+ };
150
+ }
151
+
152
+ /**
153
+ * Update an existing task
154
+ * @param {string} taskId - Task ID
155
+ * @param {Object} updates - Updates to apply
156
+ * @param {Object} options - Options
157
+ * @returns {Promise<Object>} Updated task
158
+ */
159
+ export async function updateTask(taskId, updates, options = {}) {
160
+ const { fs: fileSystem = defaultFs, basePath = process.cwd() } = options;
161
+
162
+ // Parse task ID
163
+ const idMatch = taskId.match(/^(?:phase-(\d+)-)?task-(\d+)$/);
164
+ const phase = idMatch?.[1] ? parseInt(idMatch[1], 10) : 1;
165
+ const taskNumber = idMatch?.[2] ? parseInt(idMatch[2], 10) : parseInt(taskId.replace('task-', ''), 10);
166
+
167
+ const planPath = path.join(basePath, '.planning', 'phases', `${phase}-PLAN.md`);
168
+
169
+ let content = '';
170
+ try {
171
+ content = await fileSystem.readFile(planPath, 'utf-8');
172
+ } catch {
173
+ throw new Error(`Task ${taskId} not found`);
174
+ }
175
+
176
+ const tasks = parseTasksFromPlan(content);
177
+ const task = tasks.find(t => t.number === taskNumber);
178
+
179
+ if (!task) {
180
+ throw new Error(`Task ${taskId} not found`);
181
+ }
182
+
183
+ // Apply updates
184
+ const updatedTask = { ...task };
185
+ if (updates.subject !== undefined) {
186
+ updatedTask.subject = updates.subject;
187
+ }
188
+ if (updates.status !== undefined) {
189
+ updatedTask.status = updates.status;
190
+ }
191
+ if (updates.owner !== undefined) {
192
+ updatedTask.owner = updates.owner;
193
+ }
194
+
195
+ // Build new marker
196
+ let marker = ' ';
197
+ if (updatedTask.status === 'completed') marker = 'x';
198
+ else if (updatedTask.status === 'in_progress') marker = '>';
199
+
200
+ const ownerPart = updatedTask.owner ? `@${updatedTask.owner}` : '';
201
+ const newMarker = `[${marker}${ownerPart}]`;
202
+
203
+ // Replace in content
204
+ const oldPattern = new RegExp(
205
+ `(###\\s+Task\\s+${taskNumber}:\\s*)${task.subject.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(\\s*\\[)[^\\]]*\\]`,
206
+ 'g'
207
+ );
208
+ const newContent = content.replace(oldPattern, `$1${updatedTask.subject}$2${marker}${ownerPart}]`);
209
+
210
+ await fileSystem.writeFile(planPath, newContent);
211
+
212
+ return formatTaskForApi(updatedTask, phase);
213
+ }
214
+
215
+ /**
216
+ * Delete a task
217
+ * @param {string} taskId - Task ID
218
+ * @param {Object} options - Options
219
+ * @returns {Promise<void>}
220
+ */
221
+ export async function deleteTask(taskId, options = {}) {
222
+ const { fs: fileSystem = defaultFs, basePath = process.cwd() } = options;
223
+
224
+ // Parse task ID
225
+ const idMatch = taskId.match(/^(?:phase-(\d+)-)?task-(\d+)$/);
226
+ const phase = idMatch?.[1] ? parseInt(idMatch[1], 10) : 1;
227
+ const taskNumber = idMatch?.[2] ? parseInt(idMatch[2], 10) : parseInt(taskId.replace('task-', ''), 10);
228
+
229
+ const planPath = path.join(basePath, '.planning', 'phases', `${phase}-PLAN.md`);
230
+
231
+ let content = '';
232
+ try {
233
+ content = await fileSystem.readFile(planPath, 'utf-8');
234
+ } catch {
235
+ throw new Error(`Task ${taskId} not found`);
236
+ }
237
+
238
+ // Remove the task section (from ### Task N to next ### or end)
239
+ const lines = content.split('\n');
240
+ const newLines = [];
241
+ let skipping = false;
242
+
243
+ for (const line of lines) {
244
+ const taskMatch = line.match(/^###\s+Task\s+(\d+):/);
245
+ if (taskMatch) {
246
+ const num = parseInt(taskMatch[1], 10);
247
+ if (num === taskNumber) {
248
+ skipping = true;
249
+ continue;
250
+ } else {
251
+ skipping = false;
252
+ }
253
+ }
254
+
255
+ if (!skipping) {
256
+ newLines.push(line);
257
+ }
258
+ }
259
+
260
+ await fileSystem.writeFile(planPath, newLines.join('\n'));
261
+ }
262
+
263
+ /**
264
+ * Create Tasks API handlers
265
+ * @param {Object} options - Options
266
+ * @returns {Object} API handlers
267
+ */
268
+ export function createTasksApi(options = {}) {
269
+ const { basePath = process.cwd(), fs: fileSystem = defaultFs } = options;
270
+ const introspection = options.introspection || createTlcIntrospection({ basePath, fs: fileSystem });
271
+
272
+ return {
273
+ async get(query = {}) {
274
+ return getTasks({ introspection, fs: fileSystem, basePath });
275
+ },
276
+
277
+ async post(taskData) {
278
+ return createTask(taskData, { fs: fileSystem, basePath });
279
+ },
280
+
281
+ async patch(taskId, updates) {
282
+ return updateTask(taskId, updates, { fs: fileSystem, basePath });
283
+ },
284
+
285
+ async delete(taskId) {
286
+ return deleteTask(taskId, { fs: fileSystem, basePath });
287
+ }
288
+ };
289
+ }
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Tasks API Module Tests
3
+ */
4
+ import { describe, it, expect, vi } from 'vitest';
5
+ import { getTasks, createTask, updateTask, deleteTask, parseTasksFromPlan, formatTaskForApi, createTasksApi } from './tasks-api.js';
6
+
7
+ describe('tasks-api', () => {
8
+ describe('getTasks', () => {
9
+ it('returns tasks from current phase', async () => {
10
+ const mockIntrospection = {
11
+ getCurrentPhase: vi.fn().mockReturnValue({ number: 1 })
12
+ };
13
+ const mockFs = {
14
+ readFile: vi.fn().mockResolvedValue(`
15
+ ### Task 1: Test task [ ]
16
+ **Goal:** Do something
17
+ ### Task 2: Done task [x]
18
+ **Goal:** Did something
19
+ `)
20
+ };
21
+ const tasks = await getTasks({ introspection: mockIntrospection, fs: mockFs });
22
+ expect(tasks.length).toBe(2);
23
+ expect(tasks[0].subject).toBe('Test task');
24
+ expect(tasks[0].status).toBe('pending');
25
+ expect(tasks[1].status).toBe('completed');
26
+ });
27
+
28
+ it('returns flat array format', async () => {
29
+ const mockIntrospection = {
30
+ getCurrentPhase: vi.fn().mockReturnValue({ number: 1 })
31
+ };
32
+ const mockFs = {
33
+ readFile: vi.fn().mockResolvedValue('### Task 1: Test [ ]')
34
+ };
35
+ const tasks = await getTasks({ introspection: mockIntrospection, fs: mockFs });
36
+ expect(Array.isArray(tasks)).toBe(true);
37
+ expect(tasks[0].id).toBeDefined();
38
+ });
39
+ });
40
+
41
+ describe('createTask', () => {
42
+ it('creates task with required fields', async () => {
43
+ const mockFs = {
44
+ readFile: vi.fn().mockResolvedValue('## Tasks\n'),
45
+ writeFile: vi.fn().mockResolvedValue(undefined)
46
+ };
47
+ const task = await createTask({
48
+ subject: 'New task',
49
+ description: 'Task description',
50
+ phase: 1
51
+ }, { fs: mockFs });
52
+ expect(task.id).toBeDefined();
53
+ expect(task.subject).toBe('New task');
54
+ expect(task.status).toBe('pending');
55
+ });
56
+
57
+ it('validates required fields', async () => {
58
+ await expect(createTask({ description: 'No subject' }, {}))
59
+ .rejects.toThrow(/subject.*required/i);
60
+ });
61
+
62
+ it('appends to PLAN.md', async () => {
63
+ const mockFs = {
64
+ readFile: vi.fn().mockResolvedValue('## Tasks\n'),
65
+ writeFile: vi.fn().mockResolvedValue(undefined)
66
+ };
67
+ await createTask({ subject: 'Test', phase: 1 }, { fs: mockFs });
68
+ expect(mockFs.writeFile).toHaveBeenCalled();
69
+ const content = mockFs.writeFile.mock.calls[0][1];
70
+ expect(content).toContain('Test');
71
+ });
72
+ });
73
+
74
+ describe('updateTask', () => {
75
+ it('updates task status', async () => {
76
+ const mockFs = {
77
+ readFile: vi.fn().mockResolvedValue('### Task 1: Test [ ]'),
78
+ writeFile: vi.fn().mockResolvedValue(undefined)
79
+ };
80
+ const task = await updateTask('task-1', { status: 'completed' }, { fs: mockFs });
81
+ expect(task.status).toBe('completed');
82
+ });
83
+
84
+ it('updates task subject', async () => {
85
+ const mockFs = {
86
+ readFile: vi.fn().mockResolvedValue('### Task 1: Test [ ]'),
87
+ writeFile: vi.fn().mockResolvedValue(undefined)
88
+ };
89
+ const task = await updateTask('task-1', { subject: 'Updated' }, { fs: mockFs });
90
+ expect(task.subject).toBe('Updated');
91
+ });
92
+
93
+ it('throws for non-existent task', async () => {
94
+ const mockFs = {
95
+ readFile: vi.fn().mockResolvedValue('### Task 1: Test [ ]')
96
+ };
97
+ await expect(updateTask('task-99', {}, { fs: mockFs }))
98
+ .rejects.toThrow(/not found/i);
99
+ });
100
+ });
101
+
102
+ describe('deleteTask', () => {
103
+ it('removes task from plan', async () => {
104
+ const mockFs = {
105
+ readFile: vi.fn().mockResolvedValue('### Task 1: Test [ ]\n### Task 2: Keep [ ]'),
106
+ writeFile: vi.fn().mockResolvedValue(undefined)
107
+ };
108
+ await deleteTask('task-1', { fs: mockFs });
109
+ const content = mockFs.writeFile.mock.calls[0][1];
110
+ expect(content).not.toContain('Task 1');
111
+ expect(content).toContain('Task 2');
112
+ });
113
+ });
114
+
115
+ describe('parseTasksFromPlan', () => {
116
+ it('parses task format', () => {
117
+ const content = `
118
+ ### Task 1: First task [ ]
119
+ **Goal:** Do first thing
120
+ ### Task 2: Second task [x@alice]
121
+ **Goal:** Do second thing
122
+ `;
123
+ const tasks = parseTasksFromPlan(content);
124
+ expect(tasks.length).toBe(2);
125
+ expect(tasks[0].subject).toBe('First task');
126
+ expect(tasks[1].owner).toBe('alice');
127
+ });
128
+
129
+ it('handles in-progress marker', () => {
130
+ const content = '### Task 1: Working [>@bob]';
131
+ const tasks = parseTasksFromPlan(content);
132
+ expect(tasks[0].status).toBe('in_progress');
133
+ expect(tasks[0].owner).toBe('bob');
134
+ });
135
+ });
136
+
137
+ describe('formatTaskForApi', () => {
138
+ it('formats task with all fields', () => {
139
+ const task = {
140
+ number: 1,
141
+ subject: 'Test',
142
+ status: 'pending',
143
+ goal: 'Do something',
144
+ owner: null
145
+ };
146
+ const formatted = formatTaskForApi(task, 5);
147
+ expect(formatted.id).toBe('phase-5-task-1');
148
+ expect(formatted.phase).toBe(5);
149
+ });
150
+ });
151
+
152
+ describe('createTasksApi', () => {
153
+ it('creates API handlers', () => {
154
+ const api = createTasksApi({ basePath: '/test' });
155
+ expect(api.get).toBeDefined();
156
+ expect(api.post).toBeDefined();
157
+ expect(api.patch).toBeDefined();
158
+ expect(api.delete).toBeDefined();
159
+ });
160
+ });
161
+ });
@@ -0,0 +1,197 @@
1
+ /**
2
+ * TLC Introspection Module
3
+ * Reads TLC project state (ROADMAP.md, PROJECT.md, .tlc.json)
4
+ */
5
+ import { promises as defaultFs } from 'fs';
6
+ import path from 'path';
7
+
8
+ /**
9
+ * Parse phases from ROADMAP.md content
10
+ * @param {string} content - ROADMAP.md content
11
+ * @returns {Array} Parsed phases
12
+ */
13
+ export function parseRoadmap(content) {
14
+ const phases = [];
15
+ let currentMilestone = null;
16
+
17
+ const lines = content.split('\n');
18
+
19
+ for (const line of lines) {
20
+ // Check for milestone
21
+ const milestoneMatch = line.match(/^##\s+Milestone:\s*(.+)$/);
22
+ if (milestoneMatch) {
23
+ currentMilestone = milestoneMatch[1].trim();
24
+ continue;
25
+ }
26
+
27
+ // Check for phase
28
+ const phaseMatch = line.match(/^###\s+Phase\s+(\d+):\s*(.+?)\s*\[(x|>|\s*)\]/);
29
+ if (phaseMatch) {
30
+ const number = parseInt(phaseMatch[1], 10);
31
+ const name = phaseMatch[2].trim();
32
+ const marker = phaseMatch[3];
33
+
34
+ let status;
35
+ if (marker === 'x') {
36
+ status = 'complete';
37
+ } else if (marker === '>') {
38
+ status = 'current';
39
+ } else {
40
+ status = 'pending';
41
+ }
42
+
43
+ phases.push({
44
+ number,
45
+ name,
46
+ status,
47
+ milestone: currentMilestone
48
+ });
49
+ }
50
+ }
51
+
52
+ return phases;
53
+ }
54
+
55
+ /**
56
+ * Parse PROJECT.md content
57
+ * @param {string} content - PROJECT.md content
58
+ * @returns {Object} Parsed project info
59
+ */
60
+ export function parseProjectMd(content) {
61
+ if (!content || !content.trim()) {
62
+ return { name: 'Untitled', description: '' };
63
+ }
64
+
65
+ const lines = content.split('\n');
66
+ let name = 'Untitled';
67
+ let description = '';
68
+
69
+ // Extract name from first H1
70
+ const nameMatch = content.match(/^#\s+(.+)$/m);
71
+ if (nameMatch) {
72
+ name = nameMatch[1].trim();
73
+ }
74
+
75
+ // Extract description (content between first H1 and next heading)
76
+ let foundName = false;
77
+ let descLines = [];
78
+
79
+ for (const line of lines) {
80
+ if (line.match(/^#\s+/)) {
81
+ if (foundName) break;
82
+ foundName = true;
83
+ continue;
84
+ }
85
+ if (foundName && line.match(/^##/)) {
86
+ break;
87
+ }
88
+ if (foundName && line.trim()) {
89
+ descLines.push(line);
90
+ }
91
+ }
92
+
93
+ description = descLines.join('\n').trim();
94
+
95
+ return { name, description };
96
+ }
97
+
98
+ /**
99
+ * Parse .tlc.json config
100
+ * @param {string} content - JSON string
101
+ * @returns {Object} Parsed config
102
+ */
103
+ export function parseTlcConfig(content) {
104
+ try {
105
+ return JSON.parse(content);
106
+ } catch {
107
+ return { project: 'unknown' };
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Get complete project state
113
+ * @param {Object} options - Options with fs and basePath
114
+ * @returns {Promise<Object>} Project state
115
+ */
116
+ export async function getProjectState(options = {}) {
117
+ const fs = options.fs || defaultFs;
118
+ const basePath = options.basePath || process.cwd();
119
+
120
+ let projectContent = '';
121
+ let roadmapContent = '';
122
+ let configContent = '';
123
+
124
+ try {
125
+ projectContent = await fs.readFile(path.join(basePath, 'PROJECT.md'), 'utf-8');
126
+ } catch {
127
+ projectContent = '';
128
+ }
129
+
130
+ try {
131
+ roadmapContent = await fs.readFile(path.join(basePath, '.planning', 'ROADMAP.md'), 'utf-8');
132
+ } catch {
133
+ roadmapContent = '';
134
+ }
135
+
136
+ try {
137
+ configContent = await fs.readFile(path.join(basePath, '.tlc.json'), 'utf-8');
138
+ } catch {
139
+ configContent = '';
140
+ }
141
+
142
+ return {
143
+ project: parseProjectMd(projectContent),
144
+ phases: parseRoadmap(roadmapContent),
145
+ config: parseTlcConfig(configContent)
146
+ };
147
+ }
148
+
149
+ /**
150
+ * Get current phase from phases array
151
+ * @param {Array} phases - Array of phases
152
+ * @returns {Object|null} Current phase
153
+ */
154
+ export function getCurrentPhase(phases) {
155
+ // First look for explicitly current phase
156
+ const current = phases.find(p => p.status === 'current');
157
+ if (current) return current;
158
+
159
+ // Otherwise return first pending phase
160
+ const pending = phases.find(p => p.status === 'pending');
161
+ return pending || null;
162
+ }
163
+
164
+ /**
165
+ * Create TLC introspection instance
166
+ * @param {Object} options - Options
167
+ * @returns {Object} Introspection API
168
+ */
169
+ export function createTlcIntrospection(options = {}) {
170
+ const fs = options.fs || defaultFs;
171
+ const basePath = options.basePath || process.cwd();
172
+
173
+ let cachedState = null;
174
+
175
+ return {
176
+ async getState() {
177
+ if (!cachedState) {
178
+ cachedState = await getProjectState({ fs, basePath });
179
+ }
180
+ return cachedState;
181
+ },
182
+
183
+ async getPhases() {
184
+ const state = await this.getState();
185
+ return state.phases;
186
+ },
187
+
188
+ async getCurrentPhase() {
189
+ const phases = await this.getPhases();
190
+ return getCurrentPhase(phases);
191
+ },
192
+
193
+ invalidateCache() {
194
+ cachedState = null;
195
+ }
196
+ };
197
+ }