tlc-claude-code 2.0.1 → 2.2.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/.claude/agents/builder.md +144 -0
- package/.claude/agents/planner.md +143 -0
- package/.claude/agents/reviewer.md +160 -0
- package/.claude/commands/tlc/build.md +4 -0
- package/.claude/commands/tlc/deploy.md +194 -2
- package/.claude/commands/tlc/e2e-verify.md +214 -0
- package/.claude/commands/tlc/guard.md +191 -0
- package/.claude/commands/tlc/help.md +32 -0
- package/.claude/commands/tlc/init.md +73 -37
- package/.claude/commands/tlc/llm.md +19 -4
- package/.claude/commands/tlc/preflight.md +134 -0
- package/.claude/commands/tlc/review-plan.md +363 -0
- package/.claude/commands/tlc/review.md +172 -57
- package/.claude/commands/tlc/watchci.md +159 -0
- package/.claude/hooks/tlc-block-tools.sh +41 -0
- package/.claude/hooks/tlc-capture-exchange.sh +50 -0
- package/.claude/hooks/tlc-post-build.sh +38 -0
- package/.claude/hooks/tlc-post-push.sh +22 -0
- package/.claude/hooks/tlc-prompt-guard.sh +69 -0
- package/.claude/hooks/tlc-session-init.sh +123 -0
- package/CLAUDE.md +13 -0
- package/bin/install.js +268 -2
- package/bin/postinstall.js +102 -24
- package/bin/setup-autoupdate.js +206 -0
- package/bin/setup-autoupdate.test.js +124 -0
- package/bin/tlc.js +0 -0
- package/dashboard-web/dist/assets/index-CdS5CHqu.css +1 -0
- package/dashboard-web/dist/assets/index-CwNPPVpg.js +483 -0
- package/dashboard-web/dist/assets/index-CwNPPVpg.js.map +1 -0
- package/dashboard-web/dist/index.html +2 -2
- package/docker-compose.dev.yml +18 -12
- package/package.json +4 -2
- package/scripts/project-docs.js +1 -1
- package/server/index.js +228 -2
- package/server/lib/capture-bridge.js +242 -0
- package/server/lib/capture-bridge.test.js +363 -0
- package/server/lib/capture-guard.js +140 -0
- package/server/lib/capture-guard.test.js +182 -0
- package/server/lib/command-runner.js +159 -0
- package/server/lib/command-runner.test.js +92 -0
- package/server/lib/cost-tracker.test.js +49 -12
- package/server/lib/deploy/runners/dependency-runner.js +106 -0
- package/server/lib/deploy/runners/dependency-runner.test.js +148 -0
- package/server/lib/deploy/runners/secrets-runner.js +174 -0
- package/server/lib/deploy/runners/secrets-runner.test.js +127 -0
- package/server/lib/deploy/security-gates.js +11 -24
- package/server/lib/deploy/security-gates.test.js +9 -2
- package/server/lib/deploy-engine.js +182 -0
- package/server/lib/deploy-engine.test.js +147 -0
- package/server/lib/docker-api.js +137 -0
- package/server/lib/docker-api.test.js +202 -0
- package/server/lib/docker-client.js +297 -0
- package/server/lib/docker-client.test.js +308 -0
- package/server/lib/input-sanitizer.js +86 -0
- package/server/lib/input-sanitizer.test.js +117 -0
- package/server/lib/launchd-agent.js +225 -0
- package/server/lib/launchd-agent.test.js +185 -0
- package/server/lib/memory-api.js +3 -1
- package/server/lib/memory-api.test.js +3 -5
- package/server/lib/memory-bridge-e2e.test.js +160 -0
- package/server/lib/memory-committer.js +18 -4
- package/server/lib/memory-committer.test.js +21 -0
- package/server/lib/memory-hooks-capture.test.js +69 -4
- package/server/lib/memory-hooks-integration.test.js +98 -0
- package/server/lib/memory-hooks.js +42 -4
- package/server/lib/memory-store-adapter.js +105 -0
- package/server/lib/memory-store-adapter.test.js +141 -0
- package/server/lib/memory-wiring-e2e.test.js +93 -0
- package/server/lib/nginx-config.js +114 -0
- package/server/lib/nginx-config.test.js +82 -0
- package/server/lib/ollama-health.js +91 -0
- package/server/lib/ollama-health.test.js +74 -0
- package/server/lib/orchestration/agent-dispatcher.js +114 -0
- package/server/lib/orchestration/agent-dispatcher.test.js +110 -0
- package/server/lib/orchestration/orchestrator.js +130 -0
- package/server/lib/orchestration/orchestrator.test.js +192 -0
- package/server/lib/orchestration/tmux-manager.js +101 -0
- package/server/lib/orchestration/tmux-manager.test.js +109 -0
- package/server/lib/orchestration/worktree-manager.js +132 -0
- package/server/lib/orchestration/worktree-manager.test.js +129 -0
- package/server/lib/port-guard.js +44 -0
- package/server/lib/port-guard.test.js +65 -0
- package/server/lib/project-scanner.js +37 -2
- package/server/lib/project-scanner.test.js +152 -0
- package/server/lib/remember-command.js +2 -0
- package/server/lib/remember-command.test.js +23 -0
- package/server/lib/review/plan-reviewer.js +260 -0
- package/server/lib/review/plan-reviewer.test.js +269 -0
- package/server/lib/review/review-schemas.js +173 -0
- package/server/lib/review/review-schemas.test.js +152 -0
- package/server/lib/security/crypto-utils.test.js +2 -2
- package/server/lib/semantic-recall.js +1 -1
- package/server/lib/semantic-recall.test.js +17 -0
- package/server/lib/ssh-client.js +184 -0
- package/server/lib/ssh-client.test.js +127 -0
- package/server/lib/vps-api.js +184 -0
- package/server/lib/vps-api.test.js +208 -0
- package/server/lib/vps-bootstrap.js +124 -0
- package/server/lib/vps-bootstrap.test.js +79 -0
- package/server/lib/vps-monitor.js +126 -0
- package/server/lib/vps-monitor.test.js +98 -0
- package/server/lib/workspace-api.js +182 -1
- package/server/lib/workspace-api.test.js +474 -0
- package/server/package-lock.json +737 -0
- package/server/package.json +3 -0
- package/server/setup.sh +271 -271
- package/dashboard-web/dist/assets/index-Uhc49PE-.css +0 -1
- package/dashboard-web/dist/assets/index-W36XHPC5.js +0 -431
- package/dashboard-web/dist/assets/index-W36XHPC5.js.map +0 -1
|
@@ -386,4 +386,156 @@ describe('ProjectScanner', () => {
|
|
|
386
386
|
// The last reported count should match total found
|
|
387
387
|
expect(progressCounts[progressCounts.length - 1]).toBe(2);
|
|
388
388
|
});
|
|
389
|
+
|
|
390
|
+
// =========================================================================
|
|
391
|
+
// Phase 79 — Task 1: Stop recursion at project boundaries
|
|
392
|
+
// =========================================================================
|
|
393
|
+
|
|
394
|
+
// Test 20: Does NOT recurse into subdirectories of a detected project
|
|
395
|
+
it('does not recurse into subdirectories of a detected project', () => {
|
|
396
|
+
// Create a TLC project with a nested sub-package that also looks like a project
|
|
397
|
+
const projectDir = createTlcProject(tempDir, 'monorepo-project');
|
|
398
|
+
const subPkgDir = path.join(projectDir, 'packages', 'sub-package');
|
|
399
|
+
fs.mkdirSync(subPkgDir, { recursive: true });
|
|
400
|
+
fs.writeFileSync(path.join(subPkgDir, 'package.json'), JSON.stringify({ name: 'sub-package' }));
|
|
401
|
+
fs.mkdirSync(path.join(subPkgDir, '.git'), { recursive: true });
|
|
402
|
+
|
|
403
|
+
const results = scanner.scan([tempDir]);
|
|
404
|
+
|
|
405
|
+
// Should only find the parent project, not the sub-package
|
|
406
|
+
expect(results).toHaveLength(1);
|
|
407
|
+
expect(results[0].name).toBe('monorepo-project');
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// Test 21: TLC project's server/ subdirectory not listed separately
|
|
411
|
+
it('does not list subdirectories of a TLC project as separate projects', () => {
|
|
412
|
+
const projectDir = createTlcProject(tempDir, 'tlc-project');
|
|
413
|
+
// Create a server/ subdirectory with its own package.json + .git
|
|
414
|
+
const serverDir = path.join(projectDir, 'server');
|
|
415
|
+
fs.mkdirSync(serverDir, { recursive: true });
|
|
416
|
+
fs.writeFileSync(path.join(serverDir, 'package.json'), JSON.stringify({ name: 'tlc-server' }));
|
|
417
|
+
fs.mkdirSync(path.join(serverDir, '.git'), { recursive: true });
|
|
418
|
+
|
|
419
|
+
const results = scanner.scan([tempDir]);
|
|
420
|
+
|
|
421
|
+
expect(results).toHaveLength(1);
|
|
422
|
+
expect(results[0].name).toBe('tlc-project');
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
// Test 22: Top-level non-project directories are still traversed
|
|
426
|
+
it('still traverses non-project directories to find nested projects', () => {
|
|
427
|
+
// Create a plain directory (not a project) with a project nested inside
|
|
428
|
+
const groupDir = path.join(tempDir, 'my-workspace');
|
|
429
|
+
fs.mkdirSync(groupDir, { recursive: true });
|
|
430
|
+
// No .tlc.json, no .planning, no package.json+.git — just a folder
|
|
431
|
+
createTlcProject(groupDir, 'nested-real-project');
|
|
432
|
+
|
|
433
|
+
const results = scanner.scan([tempDir]);
|
|
434
|
+
|
|
435
|
+
expect(results).toHaveLength(1);
|
|
436
|
+
expect(results[0].name).toBe('nested-real-project');
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
// Test 23: Multiple projects at same level, none recurse into children
|
|
440
|
+
it('finds sibling projects but does not recurse into either', () => {
|
|
441
|
+
const projA = createTlcProject(tempDir, 'project-a');
|
|
442
|
+
const projB = createTlcProject(tempDir, 'project-b');
|
|
443
|
+
|
|
444
|
+
// Add nested sub-projects inside each
|
|
445
|
+
const nestedA = path.join(projA, 'nested');
|
|
446
|
+
fs.mkdirSync(nestedA, { recursive: true });
|
|
447
|
+
fs.writeFileSync(path.join(nestedA, '.tlc.json'), '{}');
|
|
448
|
+
|
|
449
|
+
const nestedB = path.join(projB, 'apps', 'frontend');
|
|
450
|
+
fs.mkdirSync(nestedB, { recursive: true });
|
|
451
|
+
fs.writeFileSync(path.join(nestedB, 'package.json'), JSON.stringify({ name: 'frontend' }));
|
|
452
|
+
fs.mkdirSync(path.join(nestedB, '.git'), { recursive: true });
|
|
453
|
+
|
|
454
|
+
const results = scanner.scan([tempDir]);
|
|
455
|
+
|
|
456
|
+
expect(results).toHaveLength(2);
|
|
457
|
+
const names = results.map(r => r.name);
|
|
458
|
+
expect(names).toContain('project-a');
|
|
459
|
+
expect(names).toContain('project-b');
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
// =========================================================================
|
|
463
|
+
// Phase 79 — Task 2: Monorepo sub-package metadata
|
|
464
|
+
// =========================================================================
|
|
465
|
+
|
|
466
|
+
// Test 24: Detects npm workspaces array format
|
|
467
|
+
it('detects npm workspaces and returns isMonorepo: true', () => {
|
|
468
|
+
createTlcProject(tempDir, 'npm-monorepo', {
|
|
469
|
+
packageJson: {
|
|
470
|
+
name: 'npm-monorepo',
|
|
471
|
+
version: '1.0.0',
|
|
472
|
+
workspaces: ['packages/*'],
|
|
473
|
+
},
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
// Create a matching sub-package directory
|
|
477
|
+
const pkgDir = path.join(tempDir, 'npm-monorepo', 'packages', 'core');
|
|
478
|
+
fs.mkdirSync(pkgDir, { recursive: true });
|
|
479
|
+
fs.writeFileSync(path.join(pkgDir, 'package.json'), JSON.stringify({ name: '@mono/core' }));
|
|
480
|
+
|
|
481
|
+
const results = scanner.scan([tempDir]);
|
|
482
|
+
|
|
483
|
+
expect(results).toHaveLength(1);
|
|
484
|
+
expect(results[0].isMonorepo).toBe(true);
|
|
485
|
+
expect(results[0].workspaces).toBeInstanceOf(Array);
|
|
486
|
+
expect(results[0].workspaces.length).toBeGreaterThanOrEqual(1);
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
// Test 25: Detects yarn workspaces object format
|
|
490
|
+
it('detects yarn workspaces object format', () => {
|
|
491
|
+
createTlcProject(tempDir, 'yarn-monorepo', {
|
|
492
|
+
packageJson: {
|
|
493
|
+
name: 'yarn-monorepo',
|
|
494
|
+
version: '1.0.0',
|
|
495
|
+
workspaces: { packages: ['packages/*'] },
|
|
496
|
+
},
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
const pkgDir = path.join(tempDir, 'yarn-monorepo', 'packages', 'utils');
|
|
500
|
+
fs.mkdirSync(pkgDir, { recursive: true });
|
|
501
|
+
fs.writeFileSync(path.join(pkgDir, 'package.json'), JSON.stringify({ name: '@mono/utils' }));
|
|
502
|
+
|
|
503
|
+
const results = scanner.scan([tempDir]);
|
|
504
|
+
|
|
505
|
+
expect(results).toHaveLength(1);
|
|
506
|
+
expect(results[0].isMonorepo).toBe(true);
|
|
507
|
+
expect(results[0].workspaces).toBeInstanceOf(Array);
|
|
508
|
+
expect(results[0].workspaces.length).toBeGreaterThanOrEqual(1);
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
// Test 26: Non-monorepo returns isMonorepo: false and empty workspaces
|
|
512
|
+
it('returns isMonorepo false and empty workspaces for regular project', () => {
|
|
513
|
+
createTlcProject(tempDir, 'regular-project', {
|
|
514
|
+
packageJson: { name: 'regular-project', version: '1.0.0' },
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
const results = scanner.scan([tempDir]);
|
|
518
|
+
|
|
519
|
+
expect(results).toHaveLength(1);
|
|
520
|
+
expect(results[0].isMonorepo).toBe(false);
|
|
521
|
+
expect(results[0].workspaces).toEqual([]);
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
// Test 27: Monorepo with no matching workspace directories
|
|
525
|
+
it('returns empty workspaces when glob pattern matches nothing', () => {
|
|
526
|
+
createTlcProject(tempDir, 'empty-mono', {
|
|
527
|
+
packageJson: {
|
|
528
|
+
name: 'empty-mono',
|
|
529
|
+
version: '1.0.0',
|
|
530
|
+
workspaces: ['packages/*'],
|
|
531
|
+
},
|
|
532
|
+
});
|
|
533
|
+
// Don't create the packages/ directory at all
|
|
534
|
+
|
|
535
|
+
const results = scanner.scan([tempDir]);
|
|
536
|
+
|
|
537
|
+
expect(results).toHaveLength(1);
|
|
538
|
+
expect(results[0].isMonorepo).toBe(true);
|
|
539
|
+
expect(results[0].workspaces).toEqual([]);
|
|
540
|
+
});
|
|
389
541
|
});
|
|
@@ -55,6 +55,7 @@ export function createRememberCommand({ richCapture, vectorIndexer, embeddingCli
|
|
|
55
55
|
chunk.title = `[PERMANENT] ${text}`;
|
|
56
56
|
chunk.topic = text;
|
|
57
57
|
chunk.content = text;
|
|
58
|
+
chunk.text = text;
|
|
58
59
|
} else if (exchanges && exchanges.length > 0) {
|
|
59
60
|
// Exchange capture mode
|
|
60
61
|
const summary = exchanges
|
|
@@ -64,6 +65,7 @@ export function createRememberCommand({ richCapture, vectorIndexer, embeddingCli
|
|
|
64
65
|
chunk.title = `[PERMANENT] ${summary}`;
|
|
65
66
|
chunk.topic = summary;
|
|
66
67
|
chunk.exchanges = exchanges;
|
|
68
|
+
chunk.text = summary;
|
|
67
69
|
} else {
|
|
68
70
|
return {
|
|
69
71
|
success: false,
|
|
@@ -262,4 +262,27 @@ describe('remember-command', () => {
|
|
|
262
262
|
// Should provide guidance about what to provide
|
|
263
263
|
expect(result.message.length).toBeGreaterThan(0);
|
|
264
264
|
});
|
|
265
|
+
|
|
266
|
+
// -------------------------------------------------------------------------
|
|
267
|
+
// 10. Phase 81: chunk.text must be set for vector indexing
|
|
268
|
+
// -------------------------------------------------------------------------
|
|
269
|
+
it('explicit text remember sets chunk.text on indexChunk call', async () => {
|
|
270
|
+
await remember.execute(testDir, { text: 'Always use UTC timestamps' });
|
|
271
|
+
|
|
272
|
+
expect(mockVectorIndexer.indexChunk).toHaveBeenCalledTimes(1);
|
|
273
|
+
const indexedChunk = mockVectorIndexer.indexChunk.mock.calls[0][1];
|
|
274
|
+
// chunk.text MUST be set — vectorIndexer.indexChunk only reads chunk.text
|
|
275
|
+
expect(indexedChunk.text).toBe('Always use UTC timestamps');
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('exchange capture sets chunk.text to exchange summary', async () => {
|
|
279
|
+
await remember.execute(testDir, { exchanges: sampleExchanges });
|
|
280
|
+
|
|
281
|
+
expect(mockVectorIndexer.indexChunk).toHaveBeenCalledTimes(1);
|
|
282
|
+
const indexedChunk = mockVectorIndexer.indexChunk.mock.calls[0][1];
|
|
283
|
+
// chunk.text MUST be non-empty for vector indexing to work
|
|
284
|
+
expect(indexedChunk.text).toBeDefined();
|
|
285
|
+
expect(typeof indexedChunk.text).toBe('string');
|
|
286
|
+
expect(indexedChunk.text.length).toBeGreaterThan(0);
|
|
287
|
+
});
|
|
265
288
|
});
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan Reviewer
|
|
3
|
+
* Validates .planning/phases/{N}-PLAN.md files for structure, scope, and completeness.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Parse a TLC plan markdown file into a structured object.
|
|
8
|
+
*
|
|
9
|
+
* @param {string} content - Raw markdown content of the plan file.
|
|
10
|
+
* @returns {{ tasks: Array, prerequisites: string[], dependencies: string, rawContent: string }}
|
|
11
|
+
*/
|
|
12
|
+
export function parsePlan(content) {
|
|
13
|
+
const rawContent = content;
|
|
14
|
+
const tasks = [];
|
|
15
|
+
const prerequisites = [];
|
|
16
|
+
let dependencies = '';
|
|
17
|
+
|
|
18
|
+
// --- Prerequisites section (also accept "Context" with "Depends on" lines) ---
|
|
19
|
+
const prereqMatch = content.match(/^##\s+(?:Prerequisites|Context)\s*\n([\s\S]*?)(?=^##\s|\Z)/m);
|
|
20
|
+
if (prereqMatch) {
|
|
21
|
+
const block = prereqMatch[1];
|
|
22
|
+
const lines = block.split('\n');
|
|
23
|
+
for (const line of lines) {
|
|
24
|
+
const trimmed = line.trim();
|
|
25
|
+
if (trimmed.startsWith('-') || trimmed.startsWith('*')) {
|
|
26
|
+
const item = trimmed.replace(/^[-*]\s*/, '').trim();
|
|
27
|
+
if (item) prerequisites.push(item);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// --- Dependencies section ---
|
|
33
|
+
const depsMatch = content.match(/^##\s+Dependencies\s*\n([\s\S]*?)(?=^##\s|$)/m);
|
|
34
|
+
if (depsMatch) {
|
|
35
|
+
dependencies = depsMatch[1].trim();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// --- Tasks section ---
|
|
39
|
+
// Split on ### Task N: headings
|
|
40
|
+
const taskHeadingRegex = /^###\s+Task\s+\d+:\s+(.+?)(?:\s+\[.*?\])?\s*$/m;
|
|
41
|
+
// Use a global split to get all task blocks
|
|
42
|
+
const taskBlocks = [];
|
|
43
|
+
const taskHeadingGlobal = /^###\s+Task\s+[\d.]+:\s+(.*?)(?:\s+\[.*?\])?\s*$/gm;
|
|
44
|
+
let match;
|
|
45
|
+
const headingPositions = [];
|
|
46
|
+
|
|
47
|
+
while ((match = taskHeadingGlobal.exec(content)) !== null) {
|
|
48
|
+
headingPositions.push({ index: match.index, title: match[1].trim(), end: match.index + match[0].length });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (let i = 0; i < headingPositions.length; i++) {
|
|
52
|
+
const start = headingPositions[i].end;
|
|
53
|
+
const end = i + 1 < headingPositions.length ? headingPositions[i + 1].index : content.length;
|
|
54
|
+
taskBlocks.push({ title: headingPositions[i].title, body: content.slice(start, end) });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
for (const { title, body } of taskBlocks) {
|
|
58
|
+
// Goal
|
|
59
|
+
const goalMatch = body.match(/^\*\*Goal:\*\*\s*(.+)$/m);
|
|
60
|
+
const goal = goalMatch ? goalMatch[1].trim() : '';
|
|
61
|
+
|
|
62
|
+
// Helper: extract a named section from the task body.
|
|
63
|
+
// Stops at the next **SectionName:** heading or end of body.
|
|
64
|
+
const extractSection = (sectionName) => {
|
|
65
|
+
const pattern = new RegExp(`\\*\\*${sectionName}:\\*\\*\\s*\\n([\\s\\S]*?)(?=\\*\\*[A-Za-z]|$)`);
|
|
66
|
+
const m = body.match(pattern);
|
|
67
|
+
return m ? m[1] : null;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Files
|
|
71
|
+
const files = [];
|
|
72
|
+
const filesBlock = extractSection('Files');
|
|
73
|
+
if (filesBlock) {
|
|
74
|
+
for (const line of filesBlock.split('\n')) {
|
|
75
|
+
const trimmed = line.trim();
|
|
76
|
+
if (trimmed.startsWith('-') || trimmed.startsWith('*')) {
|
|
77
|
+
const file = trimmed.replace(/^[-*]\s*/, '').trim();
|
|
78
|
+
if (file) files.push(file);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Acceptance Criteria
|
|
84
|
+
const criteria = [];
|
|
85
|
+
const criteriaBlock = extractSection('Acceptance Criteria');
|
|
86
|
+
if (criteriaBlock) {
|
|
87
|
+
for (const line of criteriaBlock.split('\n')) {
|
|
88
|
+
const trimmed = line.trim();
|
|
89
|
+
// Match "- [ ] text" or "- [x] text" checkboxes
|
|
90
|
+
const cbMatch = trimmed.match(/^[-*]\s+\[[ xX]\]\s+(.+)$/);
|
|
91
|
+
if (cbMatch) {
|
|
92
|
+
criteria.push(cbMatch[1].trim());
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Test Cases
|
|
98
|
+
const testCases = [];
|
|
99
|
+
const testCasesBlock = extractSection('Test Cases');
|
|
100
|
+
if (testCasesBlock) {
|
|
101
|
+
for (const line of testCasesBlock.split('\n')) {
|
|
102
|
+
const trimmed = line.trim();
|
|
103
|
+
if (trimmed.startsWith('-') || trimmed.startsWith('*')) {
|
|
104
|
+
const tc = trimmed.replace(/^[-*]\s*/, '').trim();
|
|
105
|
+
if (tc) testCases.push(tc);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
tasks.push({ title, goal, files, criteria, testCases });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return { tasks, prerequisites, dependencies, rawContent };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Validate the structural completeness of a parsed plan.
|
|
118
|
+
* Flags tasks missing acceptance criteria or test cases.
|
|
119
|
+
*
|
|
120
|
+
* @param {{ tasks: Array }} plan
|
|
121
|
+
* @returns {Array<{ task: string, message: string }>}
|
|
122
|
+
*/
|
|
123
|
+
export function validateStructure(plan) {
|
|
124
|
+
const issues = [];
|
|
125
|
+
for (const task of plan.tasks) {
|
|
126
|
+
if (!task.criteria || task.criteria.length === 0) {
|
|
127
|
+
issues.push({ task: task.title, message: `Task "${task.title}" is missing acceptance criteria` });
|
|
128
|
+
}
|
|
129
|
+
if (!task.testCases || task.testCases.length === 0) {
|
|
130
|
+
issues.push({ task: task.title, message: `Task "${task.title}" is missing test cases` });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return issues;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Words that indicate a vague/broad task title
|
|
137
|
+
const VAGUE_WORDS = ['system', 'entire', 'all', 'everything', 'complete', 'whole', 'full'];
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Validate the scope of tasks in a parsed plan.
|
|
141
|
+
* Flags vague task titles and tasks with no files listed.
|
|
142
|
+
*
|
|
143
|
+
* @param {{ tasks: Array }} plan
|
|
144
|
+
* @returns {Array<{ task: string, message: string }>}
|
|
145
|
+
*/
|
|
146
|
+
export function validateScope(plan) {
|
|
147
|
+
const issues = [];
|
|
148
|
+
for (const task of plan.tasks) {
|
|
149
|
+
// Check for vague/overly-broad titles
|
|
150
|
+
const titleLower = task.title.toLowerCase();
|
|
151
|
+
const vagueFound = VAGUE_WORDS.some(w => titleLower.includes(w));
|
|
152
|
+
if (vagueFound) {
|
|
153
|
+
issues.push({
|
|
154
|
+
task: task.title,
|
|
155
|
+
message: `Task "${task.title}" title is too vague or broad — consider narrowing the scope`,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Check for missing files
|
|
160
|
+
if (!task.files || task.files.length === 0) {
|
|
161
|
+
issues.push({
|
|
162
|
+
task: task.title,
|
|
163
|
+
message: `Task "${task.title}" has no files listed`,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return issues;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Validate the architecture of a parsed plan.
|
|
172
|
+
* Flags files with line-count estimates exceeding 1000 lines.
|
|
173
|
+
*
|
|
174
|
+
* @param {{ tasks: Array }} plan
|
|
175
|
+
* @returns {Array<{ task: string, message: string }>}
|
|
176
|
+
*/
|
|
177
|
+
export function validateArchitecture(plan) {
|
|
178
|
+
const issues = [];
|
|
179
|
+
// Match patterns like "(estimated: 1500 lines)" or "(estimated ~1500 lines)"
|
|
180
|
+
const overSizedPattern = /estimated[^)]*?(\d{4,})\s*lines/i;
|
|
181
|
+
|
|
182
|
+
for (const task of plan.tasks) {
|
|
183
|
+
for (const file of task.files) {
|
|
184
|
+
const match = file.match(overSizedPattern);
|
|
185
|
+
if (match) {
|
|
186
|
+
const lineCount = parseInt(match[1], 10);
|
|
187
|
+
if (lineCount > 1000) {
|
|
188
|
+
issues.push({
|
|
189
|
+
task: task.title,
|
|
190
|
+
message: `Task "${task.title}" plans a file larger than 1000 lines (${lineCount} lines estimated): ${file}`,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// Also check task goal for large estimates
|
|
196
|
+
if (task.goal) {
|
|
197
|
+
const goalMatch = task.goal.match(overSizedPattern);
|
|
198
|
+
if (goalMatch) {
|
|
199
|
+
const lineCount = parseInt(goalMatch[1], 10);
|
|
200
|
+
if (lineCount > 1000) {
|
|
201
|
+
issues.push({
|
|
202
|
+
task: task.title,
|
|
203
|
+
message: `Task "${task.title}" goal describes a file larger than 1000 lines (${lineCount} lines estimated)`,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return issues;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Validate the completeness of a parsed plan.
|
|
214
|
+
* Flags plans with no prerequisites section.
|
|
215
|
+
*
|
|
216
|
+
* @param {{ prerequisites: string[] }} plan
|
|
217
|
+
* @returns {Array<{ task: string, message: string }>}
|
|
218
|
+
*/
|
|
219
|
+
export function validateCompleteness(plan) {
|
|
220
|
+
const issues = [];
|
|
221
|
+
if (!plan.prerequisites || plan.prerequisites.length === 0) {
|
|
222
|
+
issues.push({
|
|
223
|
+
task: null,
|
|
224
|
+
message: 'Plan is missing a prerequisites section — add a ## Prerequisites block listing prior phases',
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
return issues;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Generate a review prompt string for LLM-based plan review.
|
|
232
|
+
*
|
|
233
|
+
* @param {{ tasks: Array, rawContent: string }} plan
|
|
234
|
+
* @param {{ projectName?: string, techStack?: string }} context
|
|
235
|
+
* @param {{ maxLines?: number }} [options]
|
|
236
|
+
* @returns {string}
|
|
237
|
+
*/
|
|
238
|
+
export function generateReviewPrompt(plan, context = {}, options = {}) {
|
|
239
|
+
const projectName = context.projectName || 'Unknown Project';
|
|
240
|
+
const techStack = context.techStack ? `\nTech stack: ${context.techStack}` : '';
|
|
241
|
+
|
|
242
|
+
const taskList = plan.tasks.map((t, i) => ` ${i + 1}. ${t.title}`).join('\n');
|
|
243
|
+
|
|
244
|
+
let planContent = plan.rawContent || '';
|
|
245
|
+
if (options.maxLines && typeof options.maxLines === 'number') {
|
|
246
|
+
const lines = planContent.split('\n');
|
|
247
|
+
if (lines.length > options.maxLines) {
|
|
248
|
+
planContent = lines.slice(0, options.maxLines).join('\n') + '\n[... truncated ...]';
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return `You are reviewing a TLC phase plan for the project: ${projectName}.${techStack}
|
|
253
|
+
|
|
254
|
+
Tasks in this plan:
|
|
255
|
+
${taskList}
|
|
256
|
+
|
|
257
|
+
Please review the following plan for structure, scope, architecture, and completeness:
|
|
258
|
+
|
|
259
|
+
${planContent}`;
|
|
260
|
+
}
|