tlc-claude-code 1.8.5 → 2.0.1
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/.claude/commands/tlc/bootstrap.md +77 -0
- package/.claude/commands/tlc/build.md +20 -6
- package/.claude/commands/tlc/recall.md +87 -0
- package/.claude/commands/tlc/remember.md +71 -0
- package/CLAUDE.md +84 -201
- package/dashboard-web/dist/assets/index-Uhc49PE-.css +1 -0
- package/dashboard-web/dist/assets/index-W36XHPC5.js +431 -0
- package/dashboard-web/dist/assets/index-W36XHPC5.js.map +1 -0
- package/dashboard-web/dist/index.html +2 -2
- package/package.json +1 -1
- package/server/index.js +13 -0
- package/server/lib/bug-writer.js +204 -0
- package/server/lib/bug-writer.test.js +279 -0
- package/server/lib/claude-cascade.js +247 -0
- package/server/lib/claude-cascade.test.js +245 -0
- package/server/lib/context-injection.js +121 -0
- package/server/lib/context-injection.test.js +340 -0
- package/server/lib/conversation-chunker.js +320 -0
- package/server/lib/conversation-chunker.test.js +573 -0
- package/server/lib/embedding-client.js +160 -0
- package/server/lib/embedding-client.test.js +243 -0
- package/server/lib/global-config.js +198 -0
- package/server/lib/global-config.test.js +288 -0
- package/server/lib/inherited-search.js +184 -0
- package/server/lib/inherited-search.test.js +343 -0
- package/server/lib/memory-api.js +180 -0
- package/server/lib/memory-api.test.js +322 -0
- package/server/lib/memory-hooks-capture.test.js +350 -0
- package/server/lib/memory-hooks.js +101 -0
- package/server/lib/memory-inheritance.js +179 -0
- package/server/lib/memory-inheritance.test.js +360 -0
- package/server/lib/plan-writer.js +196 -0
- package/server/lib/plan-writer.test.js +298 -0
- package/server/lib/project-scanner.js +267 -0
- package/server/lib/project-scanner.test.js +389 -0
- package/server/lib/project-status.js +302 -0
- package/server/lib/project-status.test.js +470 -0
- package/server/lib/projects-registry.js +237 -0
- package/server/lib/projects-registry.test.js +275 -0
- package/server/lib/recall-command.js +207 -0
- package/server/lib/recall-command.test.js +306 -0
- package/server/lib/remember-command.js +96 -0
- package/server/lib/remember-command.test.js +265 -0
- package/server/lib/rich-capture.js +221 -0
- package/server/lib/rich-capture.test.js +312 -0
- package/server/lib/roadmap-api.js +200 -0
- package/server/lib/roadmap-api.test.js +318 -0
- package/server/lib/semantic-recall.js +242 -0
- package/server/lib/semantic-recall.test.js +446 -0
- package/server/lib/setup-generator.js +315 -0
- package/server/lib/setup-generator.test.js +303 -0
- package/server/lib/test-inventory.js +112 -0
- package/server/lib/test-inventory.test.js +360 -0
- package/server/lib/vector-indexer.js +246 -0
- package/server/lib/vector-indexer.test.js +459 -0
- package/server/lib/vector-store.js +260 -0
- package/server/lib/vector-store.test.js +706 -0
- package/server/lib/workspace-api.js +811 -0
- package/server/lib/workspace-api.test.js +743 -0
- package/server/lib/workspace-bootstrap.js +164 -0
- package/server/lib/workspace-bootstrap.test.js +503 -0
- package/server/lib/workspace-context.js +129 -0
- package/server/lib/workspace-context.test.js +214 -0
- package/server/lib/workspace-detector.js +162 -0
- package/server/lib/workspace-detector.test.js +193 -0
- package/server/lib/workspace-init.js +307 -0
- package/server/lib/workspace-init.test.js +244 -0
- package/server/lib/workspace-snapshot.js +236 -0
- package/server/lib/workspace-snapshot.test.js +444 -0
- package/server/lib/workspace-watcher.js +162 -0
- package/server/lib/workspace-watcher.test.js +257 -0
- package/server/package-lock.json +552 -0
- package/server/package.json +4 -0
- package/dashboard-web/dist/assets/index-B1I_joSL.js +0 -393
- package/dashboard-web/dist/assets/index-B1I_joSL.js.map +0 -1
- package/dashboard-web/dist/assets/index-Trhg1C1Y.css +0 -1
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file project-status.js
|
|
3
|
+
* @description Project Status module (Phase 75, Task 1).
|
|
4
|
+
*
|
|
5
|
+
* Factory function `createProjectStatus(deps)` accepts injected dependencies
|
|
6
|
+
* (fs, execSync) and returns `{ getFullStatus(projectPath) }`.
|
|
7
|
+
*
|
|
8
|
+
* Parses `.planning/ROADMAP.md` for milestones and phases, reads per-phase
|
|
9
|
+
* PLAN.md, TESTS.md, and VERIFIED.md files, extracts git log and project info.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Parse phase status marker from heading text.
|
|
14
|
+
* @param {string} text - Heading text like "Phase 1: Core Infrastructure [x]"
|
|
15
|
+
* @returns {'done'|'in_progress'|'pending'} Phase status
|
|
16
|
+
*/
|
|
17
|
+
function parseStatus(text) {
|
|
18
|
+
if (/\[x\]/.test(text)) return 'done';
|
|
19
|
+
if (/\[>\]/.test(text)) return 'in_progress';
|
|
20
|
+
return 'pending';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Parse ROADMAP.md content into milestones with phases.
|
|
25
|
+
* @param {string} content - Raw ROADMAP.md content
|
|
26
|
+
* @returns {{ milestones: Array, totalPhases: number, completedPhases: number }}
|
|
27
|
+
*/
|
|
28
|
+
function parseRoadmap(content) {
|
|
29
|
+
const lines = content.replace(/\r/g, '').split('\n');
|
|
30
|
+
const milestones = [];
|
|
31
|
+
let currentMilestone = null;
|
|
32
|
+
let currentPhase = null;
|
|
33
|
+
let inDeliverables = false;
|
|
34
|
+
|
|
35
|
+
for (const line of lines) {
|
|
36
|
+
// Milestone heading: ## Milestone: Name
|
|
37
|
+
const milestoneMatch = line.match(/^## Milestone:\s*(.+)$/);
|
|
38
|
+
if (milestoneMatch) {
|
|
39
|
+
currentMilestone = { name: milestoneMatch[1].trim(), phases: [] };
|
|
40
|
+
milestones.push(currentMilestone);
|
|
41
|
+
currentPhase = null;
|
|
42
|
+
inDeliverables = false;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Phase heading: ### Phase N: Name [status] optional suffix
|
|
47
|
+
const phaseMatch = line.match(/^### Phase (\d+):\s*(.+)$/);
|
|
48
|
+
if (phaseMatch) {
|
|
49
|
+
const number = parseInt(phaseMatch[1], 10);
|
|
50
|
+
const rest = phaseMatch[2];
|
|
51
|
+
// Extract name: everything before the status marker
|
|
52
|
+
const nameMatch = rest.match(/^(.+?)\s*\[/);
|
|
53
|
+
const name = nameMatch ? nameMatch[1].trim() : rest.trim();
|
|
54
|
+
const status = parseStatus(rest);
|
|
55
|
+
|
|
56
|
+
currentPhase = {
|
|
57
|
+
number,
|
|
58
|
+
name,
|
|
59
|
+
status,
|
|
60
|
+
goal: '',
|
|
61
|
+
deliverables: [],
|
|
62
|
+
taskCount: 0,
|
|
63
|
+
completedTaskCount: 0,
|
|
64
|
+
hasTests: false,
|
|
65
|
+
testFileCount: 0,
|
|
66
|
+
testCount: 0,
|
|
67
|
+
verified: false,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
if (currentMilestone) {
|
|
71
|
+
currentMilestone.phases.push(currentPhase);
|
|
72
|
+
}
|
|
73
|
+
inDeliverables = false;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!currentPhase) continue;
|
|
78
|
+
|
|
79
|
+
// Goal line: **Goal:** text
|
|
80
|
+
const goalMatch = line.match(/^\*\*Goal:\*\*\s*(.+)$/);
|
|
81
|
+
if (goalMatch) {
|
|
82
|
+
currentPhase.goal = goalMatch[1].trim();
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Deliverables header
|
|
87
|
+
if (line.match(/^\*\*Deliverables:\*\*/)) {
|
|
88
|
+
inDeliverables = true;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Deliverable/checklist item: - [x] text or - [ ] text
|
|
93
|
+
// Match both inside **Deliverables:** sections and standalone checklist items
|
|
94
|
+
const deliverableMatch = line.match(/^- \[(x| )\]\s*(.+)$/);
|
|
95
|
+
if (deliverableMatch) {
|
|
96
|
+
currentPhase.deliverables.push({
|
|
97
|
+
text: deliverableMatch[2].trim(),
|
|
98
|
+
done: deliverableMatch[1] === 'x',
|
|
99
|
+
});
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
// End deliverables section on non-deliverable, non-empty, non-blank line
|
|
103
|
+
if (inDeliverables && line.trim() !== '' && !line.match(/^- \[/)) {
|
|
104
|
+
inDeliverables = false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const allPhases = milestones.flatMap((m) => m.phases);
|
|
109
|
+
const totalPhases = allPhases.length;
|
|
110
|
+
const completedPhases = allPhases.filter((p) => p.status === 'done').length;
|
|
111
|
+
|
|
112
|
+
return { milestones, totalPhases, completedPhases };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Parse PLAN.md to count tasks and completed tasks.
|
|
117
|
+
* @param {string} content - Raw PLAN.md content
|
|
118
|
+
* @returns {{ taskCount: number, completedTaskCount: number }}
|
|
119
|
+
*/
|
|
120
|
+
function parsePlan(content) {
|
|
121
|
+
const lines = content.replace(/\r/g, '').split('\n');
|
|
122
|
+
let taskCount = 0;
|
|
123
|
+
let completedTaskCount = 0;
|
|
124
|
+
|
|
125
|
+
for (const line of lines) {
|
|
126
|
+
const taskMatch = line.match(/^### Task \d+:.+\[(x| )\]/);
|
|
127
|
+
if (taskMatch) {
|
|
128
|
+
taskCount++;
|
|
129
|
+
if (taskMatch[1] === 'x') completedTaskCount++;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return { taskCount, completedTaskCount };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Parse TESTS.md to count test files and total tests.
|
|
138
|
+
* @param {string} content - Raw TESTS.md content
|
|
139
|
+
* @returns {{ testFileCount: number, testCount: number }}
|
|
140
|
+
*/
|
|
141
|
+
function parseTests(content) {
|
|
142
|
+
const lines = content.replace(/\r/g, '').split('\n');
|
|
143
|
+
let testFileCount = 0;
|
|
144
|
+
let testCount = 0;
|
|
145
|
+
|
|
146
|
+
for (const line of lines) {
|
|
147
|
+
// Match table rows like: | lib/core.test.js | 10 | Passing |
|
|
148
|
+
// Skip header row, separator row, and total row
|
|
149
|
+
const rowMatch = line.match(/^\|\s*([^|]+)\s*\|\s*(\d+)\s*\|\s*([^|]+)\s*\|$/);
|
|
150
|
+
if (rowMatch) {
|
|
151
|
+
const file = rowMatch[1].trim();
|
|
152
|
+
const count = parseInt(rowMatch[2].trim(), 10);
|
|
153
|
+
// Skip header, separator, and total rows
|
|
154
|
+
if (file === 'File' || file.startsWith('--') || file.startsWith('**')) continue;
|
|
155
|
+
testFileCount++;
|
|
156
|
+
testCount += count;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return { testFileCount, testCount };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Parse git log output into structured commit objects.
|
|
165
|
+
* @param {string} output - Raw git log output (pipe-delimited)
|
|
166
|
+
* @returns {Array<{hash: string, message: string, date: string, author: string}>}
|
|
167
|
+
*/
|
|
168
|
+
function parseGitLog(output) {
|
|
169
|
+
if (!output || !output.trim()) return [];
|
|
170
|
+
return output.trim().split('\n').map((line) => {
|
|
171
|
+
const [hash, message, date, author] = line.split('|');
|
|
172
|
+
return { hash, message, date, author };
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Creates a project status reader with injected dependencies.
|
|
178
|
+
* @param {Object} deps - Dependencies
|
|
179
|
+
* @param {Object} deps.fs - File system module (existsSync, readFileSync, readdirSync)
|
|
180
|
+
* @param {Function} deps.execSync - Child process execSync function
|
|
181
|
+
* @returns {{ getFullStatus: (projectPath: string) => Object }}
|
|
182
|
+
*/
|
|
183
|
+
function createProjectStatus({ fs, execSync }) {
|
|
184
|
+
/**
|
|
185
|
+
* Read a file safely, returning null if it doesn't exist.
|
|
186
|
+
* @param {string} filePath - Absolute path
|
|
187
|
+
* @returns {string|null}
|
|
188
|
+
*/
|
|
189
|
+
function readFile(filePath) {
|
|
190
|
+
if (fs.existsSync(filePath)) {
|
|
191
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
192
|
+
}
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Get full project status for a given project path.
|
|
198
|
+
* @param {string} projectPath - Absolute path to the project root
|
|
199
|
+
* @returns {Object} Full status object
|
|
200
|
+
*/
|
|
201
|
+
function getFullStatus(projectPath) {
|
|
202
|
+
// Parse project info
|
|
203
|
+
const projectInfo = {};
|
|
204
|
+
const pkgContent = readFile(`${projectPath}/package.json`);
|
|
205
|
+
if (pkgContent) {
|
|
206
|
+
const pkg = JSON.parse(pkgContent);
|
|
207
|
+
projectInfo.name = pkg.name;
|
|
208
|
+
projectInfo.version = pkg.version;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const projectMd = readFile(`${projectPath}/PROJECT.md`);
|
|
212
|
+
if (projectMd) {
|
|
213
|
+
// Extract first non-heading, non-empty paragraph
|
|
214
|
+
const mdLines = projectMd.split('\n');
|
|
215
|
+
for (const mdLine of mdLines) {
|
|
216
|
+
if (mdLine.trim() && !mdLine.startsWith('#')) {
|
|
217
|
+
projectInfo.description = mdLine.trim();
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Parse roadmap
|
|
224
|
+
const roadmapContent = readFile(`${projectPath}/.planning/ROADMAP.md`);
|
|
225
|
+
let milestones = [];
|
|
226
|
+
let totalPhases = 0;
|
|
227
|
+
let completedPhases = 0;
|
|
228
|
+
|
|
229
|
+
if (roadmapContent) {
|
|
230
|
+
const roadmap = parseRoadmap(roadmapContent);
|
|
231
|
+
milestones = roadmap.milestones;
|
|
232
|
+
totalPhases = roadmap.totalPhases;
|
|
233
|
+
completedPhases = roadmap.completedPhases;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Enrich phases with per-phase file data
|
|
237
|
+
let totalTestFiles = 0;
|
|
238
|
+
let totalTests = 0;
|
|
239
|
+
|
|
240
|
+
for (const milestone of milestones) {
|
|
241
|
+
for (const phase of milestone.phases) {
|
|
242
|
+
const n = phase.number;
|
|
243
|
+
|
|
244
|
+
// PLAN.md
|
|
245
|
+
const planContent = readFile(`${projectPath}/.planning/phases/${n}-PLAN.md`);
|
|
246
|
+
if (planContent) {
|
|
247
|
+
const plan = parsePlan(planContent);
|
|
248
|
+
phase.taskCount = plan.taskCount;
|
|
249
|
+
phase.completedTaskCount = plan.completedTaskCount;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// TESTS.md
|
|
253
|
+
const testsContent = readFile(`${projectPath}/.planning/phases/${n}-TESTS.md`);
|
|
254
|
+
if (testsContent) {
|
|
255
|
+
const tests = parseTests(testsContent);
|
|
256
|
+
phase.hasTests = true;
|
|
257
|
+
phase.testFileCount = tests.testFileCount;
|
|
258
|
+
phase.testCount = tests.testCount;
|
|
259
|
+
totalTestFiles += tests.testFileCount;
|
|
260
|
+
totalTests += tests.testCount;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Fall back to deliverable counts when no PLAN.md exists
|
|
264
|
+
if (phase.taskCount === 0 && phase.deliverables.length > 0) {
|
|
265
|
+
phase.taskCount = phase.deliverables.length;
|
|
266
|
+
phase.completedTaskCount = phase.deliverables.filter((d) => d.done).length;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// VERIFIED.md
|
|
270
|
+
phase.verified = fs.existsSync(`${projectPath}/.planning/phases/${n}-VERIFIED.md`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Git log
|
|
275
|
+
let recentCommits = [];
|
|
276
|
+
try {
|
|
277
|
+
const gitOutput = execSync(
|
|
278
|
+
`git log --format="%h|%s|%ad|%an" --date=short -10`,
|
|
279
|
+
{ cwd: projectPath, encoding: 'utf8' }
|
|
280
|
+
);
|
|
281
|
+
recentCommits = parseGitLog(gitOutput);
|
|
282
|
+
} catch {
|
|
283
|
+
// Git not available or not a git repo
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
milestones,
|
|
288
|
+
totalPhases,
|
|
289
|
+
completedPhases,
|
|
290
|
+
testSummary: {
|
|
291
|
+
totalFiles: totalTestFiles,
|
|
292
|
+
totalTests: totalTests,
|
|
293
|
+
},
|
|
294
|
+
recentCommits,
|
|
295
|
+
projectInfo,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return { getFullStatus };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
module.exports = { createProjectStatus };
|