tlc-claude-code 2.5.0 → 2.6.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.
Files changed (76) hide show
  1. package/.claude/commands/tlc/autofix.md +34 -1
  2. package/.claude/commands/tlc/build.md +164 -6
  3. package/.claude/commands/tlc/ci.md +178 -414
  4. package/.claude/commands/tlc/coverage.md +34 -0
  5. package/.claude/commands/tlc/deploy.md +19 -6
  6. package/.claude/commands/tlc/discuss.md +34 -0
  7. package/.claude/commands/tlc/docs.md +35 -1
  8. package/.claude/commands/tlc/e2e.md +300 -0
  9. package/.claude/commands/tlc/edge-cases.md +35 -1
  10. package/.claude/commands/tlc/init.md +38 -8
  11. package/.claude/commands/tlc/new-project.md +46 -4
  12. package/.claude/commands/tlc/plan.md +33 -0
  13. package/.claude/commands/tlc/quick.md +33 -0
  14. package/.claude/commands/tlc/release.md +85 -135
  15. package/.claude/commands/tlc/restore.md +14 -0
  16. package/.claude/commands/tlc/review.md +76 -1
  17. package/.claude/commands/tlc/tlc.md +134 -0
  18. package/.claude/commands/tlc/verify.md +64 -65
  19. package/.claude/commands/tlc/watchci.md +10 -0
  20. package/.claude/hooks/tlc-block-tools.sh +13 -0
  21. package/.claude/hooks/tlc-session-init.sh +29 -0
  22. package/CODING-STANDARDS.md +35 -10
  23. package/package.json +1 -1
  24. package/server/lib/block-tools-hook.js +23 -0
  25. package/server/lib/e2e/acceptance-parser.js +132 -0
  26. package/server/lib/e2e/acceptance-parser.test.js +110 -0
  27. package/server/lib/e2e/framework-detector.js +47 -0
  28. package/server/lib/e2e/framework-detector.test.js +94 -0
  29. package/server/lib/e2e/log-assertions.js +107 -0
  30. package/server/lib/e2e/log-assertions.test.js +68 -0
  31. package/server/lib/e2e/test-generator.js +159 -0
  32. package/server/lib/e2e/test-generator.test.js +121 -0
  33. package/server/lib/e2e/verify-runner.js +191 -0
  34. package/server/lib/e2e/verify-runner.test.js +167 -0
  35. package/server/lib/hooks/block-tools-hook.test.js +54 -0
  36. package/server/lib/orchestration/cli-dispatch.js +16 -1
  37. package/server/lib/orchestration/cli-dispatch.test.js +94 -8
  38. package/server/lib/orchestration/completion-checker.js +101 -0
  39. package/server/lib/orchestration/completion-checker.test.js +177 -0
  40. package/server/lib/orchestration/result-verifier.js +143 -0
  41. package/server/lib/orchestration/result-verifier.test.js +291 -0
  42. package/server/lib/orchestration/session-dispatcher.js +99 -0
  43. package/server/lib/orchestration/session-dispatcher.test.js +215 -0
  44. package/server/lib/orchestration/session-status.js +147 -0
  45. package/server/lib/orchestration/session-status.test.js +130 -0
  46. package/server/lib/release/agent-runner-updates.js +24 -0
  47. package/server/lib/release/agent-runner-updates.test.js +22 -0
  48. package/server/lib/release/changelog-generator.js +142 -0
  49. package/server/lib/release/changelog-generator.test.js +113 -0
  50. package/server/lib/release/ci-watcher.js +83 -0
  51. package/server/lib/release/ci-watcher.test.js +81 -0
  52. package/server/lib/release/health-checker.js +111 -0
  53. package/server/lib/release/health-checker.test.js +121 -0
  54. package/server/lib/release/release-pipeline.js +187 -0
  55. package/server/lib/release/release-pipeline.test.js +262 -0
  56. package/server/lib/release/version-bumper.js +183 -0
  57. package/server/lib/release/version-bumper.test.js +142 -0
  58. package/server/lib/routing-preamble.integration.test.js +12 -0
  59. package/server/lib/routing-preamble.js +13 -2
  60. package/server/lib/routing-preamble.test.js +49 -0
  61. package/server/lib/scaffolding/ci-detector.js +139 -0
  62. package/server/lib/scaffolding/ci-detector.test.js +198 -0
  63. package/server/lib/scaffolding/ci-scaffolder.js +347 -0
  64. package/server/lib/scaffolding/ci-scaffolder.test.js +157 -0
  65. package/server/lib/scaffolding/deploy-detector.js +135 -0
  66. package/server/lib/scaffolding/deploy-detector.test.js +106 -0
  67. package/server/lib/scaffolding/health-scaffold.js +374 -0
  68. package/server/lib/scaffolding/health-scaffold.test.js +99 -0
  69. package/server/lib/scaffolding/logger-scaffold.js +196 -0
  70. package/server/lib/scaffolding/logger-scaffold.test.js +146 -0
  71. package/server/lib/scaffolding/migration-detector.js +78 -0
  72. package/server/lib/scaffolding/migration-detector.test.js +127 -0
  73. package/server/lib/scaffolding/snapshot-manager.js +142 -0
  74. package/server/lib/scaffolding/snapshot-manager.test.js +225 -0
  75. package/server/lib/task-router-config.js +50 -20
  76. package/server/lib/task-router-config.test.js +29 -15
@@ -0,0 +1,94 @@
1
+ import { afterEach, describe, it } from 'vitest';
2
+ const assert = require('node:assert');
3
+ const fs = require('node:fs');
4
+ const os = require('node:os');
5
+ const path = require('node:path');
6
+
7
+ const { detectE2eFramework } = require('./framework-detector.js');
8
+
9
+ const tempDirs = [];
10
+
11
+ function createProjectDir() {
12
+ const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'framework-detector-'));
13
+ tempDirs.push(projectDir);
14
+ return projectDir;
15
+ }
16
+
17
+ function writePackageJson(projectDir, packageJson) {
18
+ fs.writeFileSync(
19
+ path.join(projectDir, 'package.json'),
20
+ JSON.stringify(packageJson, null, 2)
21
+ );
22
+ }
23
+
24
+ afterEach(() => {
25
+ while (tempDirs.length > 0) {
26
+ fs.rmSync(tempDirs.pop(), { recursive: true, force: true });
27
+ }
28
+ });
29
+
30
+ describe('detectE2eFramework', () => {
31
+ it('returns playwright when a Playwright config is present', () => {
32
+ const projectDir = createProjectDir();
33
+ writePackageJson(projectDir, {
34
+ name: 'playwright-project',
35
+ devDependencies: {},
36
+ });
37
+ fs.writeFileSync(path.join(projectDir, 'playwright.config.ts'), 'export default {};');
38
+
39
+ const result = detectE2eFramework({ projectDir, fs });
40
+
41
+ assert.deepStrictEqual(result, { framework: 'playwright' });
42
+ });
43
+
44
+ it('returns supertest when package.json declares supertest', () => {
45
+ const projectDir = createProjectDir();
46
+ writePackageJson(projectDir, {
47
+ name: 'supertest-project',
48
+ devDependencies: {
49
+ supertest: '^7.0.0',
50
+ },
51
+ });
52
+
53
+ const result = detectE2eFramework({ projectDir, fs });
54
+
55
+ assert.deepStrictEqual(result, { framework: 'supertest' });
56
+ });
57
+
58
+ it('prefers playwright when both signals are present', () => {
59
+ const projectDir = createProjectDir();
60
+ writePackageJson(projectDir, {
61
+ name: 'mixed-project',
62
+ dependencies: {
63
+ supertest: '^7.0.0',
64
+ },
65
+ });
66
+ fs.writeFileSync(path.join(projectDir, 'playwright.config.ts'), 'export default {};');
67
+
68
+ const result = detectE2eFramework({ projectDir, fs });
69
+
70
+ assert.deepStrictEqual(result, { framework: 'playwright' });
71
+ });
72
+
73
+ it('returns none when neither signal is present', () => {
74
+ const projectDir = createProjectDir();
75
+ writePackageJson(projectDir, {
76
+ name: 'plain-project',
77
+ dependencies: {
78
+ express: '^4.0.0',
79
+ },
80
+ });
81
+
82
+ const result = detectE2eFramework({ projectDir, fs });
83
+
84
+ assert.deepStrictEqual(result, { framework: 'none' });
85
+ });
86
+
87
+ it('returns none when package.json is missing', () => {
88
+ const projectDir = createProjectDir();
89
+
90
+ const result = detectE2eFramework({ projectDir, fs });
91
+
92
+ assert.deepStrictEqual(result, { framework: 'none' });
93
+ });
94
+ });
@@ -0,0 +1,107 @@
1
+ 'use strict';
2
+
3
+ function normalizeEntry(entry) {
4
+ if (typeof entry === 'string') {
5
+ const trimmed = entry.trim();
6
+ const upper = trimmed.toUpperCase();
7
+
8
+ if (upper.includes('ERROR')) {
9
+ return { level: 'ERROR', message: trimmed };
10
+ }
11
+
12
+ if (upper.includes('WARN')) {
13
+ return { level: 'WARN', message: trimmed };
14
+ }
15
+
16
+ return null;
17
+ }
18
+
19
+ if (!entry || typeof entry !== 'object') {
20
+ return null;
21
+ }
22
+
23
+ const rawLevel = typeof entry.level === 'string'
24
+ ? entry.level
25
+ : typeof entry.severity === 'string'
26
+ ? entry.severity
27
+ : null;
28
+
29
+ const rawMessage = typeof entry.message === 'string'
30
+ ? entry.message
31
+ : typeof entry.msg === 'string'
32
+ ? entry.msg
33
+ : typeof entry.content === 'string'
34
+ ? entry.content
35
+ : null;
36
+
37
+ if (!rawLevel || !rawMessage) {
38
+ return null;
39
+ }
40
+
41
+ const normalizedLevel = rawLevel.toUpperCase();
42
+
43
+ if (normalizedLevel === 'WARNING') {
44
+ return { level: 'WARN', message: rawMessage };
45
+ }
46
+
47
+ if (normalizedLevel === 'WARN' || normalizedLevel === 'ERROR') {
48
+ return { level: normalizedLevel, message: rawMessage };
49
+ }
50
+
51
+ return null;
52
+ }
53
+
54
+ function createMemoryCollector(buffer) {
55
+ const startIndex = buffer.length;
56
+ let stopIndex = null;
57
+
58
+ function getEntries(level) {
59
+ const endIndex = stopIndex === null ? buffer.length : stopIndex;
60
+ const entries = buffer.slice(startIndex, endIndex);
61
+
62
+ return entries
63
+ .map(normalizeEntry)
64
+ .filter((entry) => entry && entry.level === level)
65
+ .map((entry) => entry.message);
66
+ }
67
+
68
+ return {
69
+ getErrors() {
70
+ return getEntries('ERROR');
71
+ },
72
+
73
+ getWarnings() {
74
+ return getEntries('WARN');
75
+ },
76
+
77
+ assertClean() {
78
+ const errors = this.getErrors();
79
+
80
+ if (errors.length > 0) {
81
+ throw new Error(`Expected no ERROR logs, but found:\n${errors.join('\n')}`);
82
+ }
83
+ },
84
+
85
+ stop() {
86
+ if (stopIndex === null) {
87
+ stopIndex = buffer.length;
88
+ }
89
+ },
90
+ };
91
+ }
92
+
93
+ function createLogCollector(options) {
94
+ const { logSource } = options || {};
95
+
96
+ if (logSource !== 'memory' && !Array.isArray(logSource)) {
97
+ throw new TypeError('createLogCollector requires logSource "memory" or an array buffer');
98
+ }
99
+
100
+ const buffer = logSource === 'memory' ? [] : logSource;
101
+
102
+ return createMemoryCollector(buffer);
103
+ }
104
+
105
+ module.exports = {
106
+ createLogCollector,
107
+ };
@@ -0,0 +1,68 @@
1
+ 'use strict';
2
+
3
+ import { describe, it, expect } from 'vitest';
4
+
5
+ const { createLogCollector } = require('./log-assertions.js');
6
+
7
+ describe('createLogCollector', () => {
8
+ it('collects warning and error messages from an in-memory buffer', () => {
9
+ const buffer = [];
10
+ const collector = createLogCollector({ logSource: buffer });
11
+
12
+ buffer.push({ level: 'info', message: 'startup complete' });
13
+ buffer.push({ level: 'warn', message: 'slow request' });
14
+ buffer.push({ severity: 'warning', message: 'deprecated config' });
15
+ buffer.push({ level: 'error', message: 'database unavailable' });
16
+ buffer.push('plain ERROR line');
17
+
18
+ expect(collector.getWarnings()).toEqual([
19
+ 'slow request',
20
+ 'deprecated config',
21
+ ]);
22
+ expect(collector.getErrors()).toEqual([
23
+ 'database unavailable',
24
+ 'plain ERROR line',
25
+ ]);
26
+ });
27
+
28
+ it('assertClean passes when only warnings are present', () => {
29
+ const buffer = [];
30
+ const collector = createLogCollector({ logSource: buffer });
31
+
32
+ buffer.push({ level: 'warn', message: 'retrying request' });
33
+
34
+ expect(() => collector.assertClean()).not.toThrow();
35
+ });
36
+
37
+ it('assertClean throws with the error contents in the message', () => {
38
+ const buffer = [];
39
+ const collector = createLogCollector({ logSource: buffer });
40
+
41
+ buffer.push({ level: 'error', message: 'first failure' });
42
+ buffer.push('ERROR second failure');
43
+
44
+ expect(() => collector.assertClean()).toThrowError(
45
+ /first failure[\s\S]*ERROR second failure/
46
+ );
47
+ });
48
+
49
+ it('stop freezes the observed range', () => {
50
+ const buffer = [];
51
+ const collector = createLogCollector({ logSource: buffer });
52
+
53
+ buffer.push({ level: 'warn', message: 'before stop' });
54
+ collector.stop();
55
+ buffer.push({ level: 'error', message: 'after stop' });
56
+
57
+ expect(collector.getWarnings()).toEqual(['before stop']);
58
+ expect(collector.getErrors()).toEqual([]);
59
+ });
60
+
61
+ it('supports a standalone memory source for tests', () => {
62
+ const collector = createLogCollector({ logSource: 'memory' });
63
+
64
+ expect(collector.getWarnings()).toEqual([]);
65
+ expect(collector.getErrors()).toEqual([]);
66
+ expect(() => collector.assertClean()).not.toThrow();
67
+ });
68
+ });
@@ -0,0 +1,159 @@
1
+ 'use strict';
2
+
3
+ const path = require('node:path');
4
+
5
+ const { parseAcceptanceCriteria } = require('./acceptance-parser.js');
6
+
7
+ function slugify(value) {
8
+ const slug = String(value || '')
9
+ .toLowerCase()
10
+ .replace(/[^a-z0-9\s-]/g, '')
11
+ .replace(/\s+/g, '-')
12
+ .replace(/-+/g, '-')
13
+ .trim();
14
+
15
+ return slug || 'scenario';
16
+ }
17
+
18
+ function escapeSingleQuotes(value) {
19
+ return String(value).replace(/\\/g, '\\\\').replace(/'/g, "\\'");
20
+ }
21
+
22
+ function resolveScenarioName(scenario, index) {
23
+ if (typeof scenario === 'string') {
24
+ return `Scenario ${index + 1}`;
25
+ }
26
+
27
+ return scenario.title
28
+ || scenario.name
29
+ || scenario.slug
30
+ || scenario.id
31
+ || `Scenario ${index + 1}`;
32
+ }
33
+
34
+ function resolveScenarioSlug(scenario, index) {
35
+ if (scenario && typeof scenario === 'object' && typeof scenario.slug === 'string' && scenario.slug.trim()) {
36
+ return slugify(scenario.slug);
37
+ }
38
+
39
+ return slugify(resolveScenarioName(scenario, index));
40
+ }
41
+
42
+ function resolvePlanContent(scenario) {
43
+ if (typeof scenario === 'string') {
44
+ return scenario;
45
+ }
46
+
47
+ if (!scenario || typeof scenario !== 'object') {
48
+ return '';
49
+ }
50
+
51
+ return scenario.planContent
52
+ || scenario.acceptanceCriteria
53
+ || scenario.content
54
+ || scenario.description
55
+ || '';
56
+ }
57
+
58
+ function buildPlaywrightContent(scenarioName, criteria) {
59
+ const tests = criteria.map(({ criterion }) => ` test('${escapeSingleQuotes(criterion)}', async ({ page }) => {
60
+ // TODO: Implement the browser flow for this acceptance criterion.
61
+ // TODO: Assert application logs remain clean for this flow.
62
+ });
63
+ `).join('\n');
64
+
65
+ return `'use strict';
66
+
67
+ const { test } = require('@playwright/test');
68
+
69
+ test.describe('${escapeSingleQuotes(scenarioName)}', () => {
70
+ test.afterEach(async ({ page }, testInfo) => {
71
+ await page.screenshot({
72
+ path: testInfo.outputPath('screenshot.png'),
73
+ fullPage: true,
74
+ });
75
+ });
76
+
77
+ ${tests}});
78
+ `;
79
+ }
80
+
81
+ function buildSupertestContent(scenarioName, criteria) {
82
+ const tests = criteria.map(({ criterion }) => ` it('${escapeSingleQuotes(criterion)}', async () => {
83
+ // TODO: Implement the API flow with supertest.
84
+ // TODO: Assert application logs remain clean for this flow.
85
+ });
86
+ `).join('\n');
87
+
88
+ return `'use strict';
89
+
90
+ const { afterEach, describe, it } = require('vitest');
91
+ const request = require('supertest');
92
+
93
+ describe('${escapeSingleQuotes(scenarioName)}', () => {
94
+ afterEach(async () => {
95
+ // TODO: Capture a screenshot or response artifact after each test.
96
+ });
97
+
98
+ ${tests}});
99
+ `;
100
+ }
101
+
102
+ function buildFileContent(scenarioName, criteria, framework) {
103
+ if (framework === 'playwright') {
104
+ return buildPlaywrightContent(scenarioName, criteria);
105
+ }
106
+
107
+ if (framework === 'supertest') {
108
+ return buildSupertestContent(scenarioName, criteria);
109
+ }
110
+
111
+ throw new Error(`Unsupported E2E framework: ${framework}`);
112
+ }
113
+
114
+ function generateE2eTests(options) {
115
+ const {
116
+ scenarios = [],
117
+ framework,
118
+ outputDir = '',
119
+ } = options || {};
120
+
121
+ const files = [];
122
+ const skipped = [];
123
+
124
+ for (const [index, scenario] of scenarios.entries()) {
125
+ const scenarioName = resolveScenarioName(scenario, index);
126
+ const slug = resolveScenarioSlug(scenario, index);
127
+ const planContent = resolvePlanContent(scenario);
128
+ const parsedCriteria = parseAcceptanceCriteria({ planContent });
129
+ const actionableCriteria = [];
130
+
131
+ for (const item of parsedCriteria) {
132
+ if (item.type === 'manual') {
133
+ skipped.push({
134
+ slug,
135
+ criterion: item.criterion,
136
+ type: item.type,
137
+ });
138
+ continue;
139
+ }
140
+
141
+ actionableCriteria.push(item);
142
+ }
143
+
144
+ if (actionableCriteria.length === 0) {
145
+ continue;
146
+ }
147
+
148
+ files.push({
149
+ path: path.join(outputDir, 'e2e', `${slug}.test.ts`),
150
+ content: buildFileContent(scenarioName, actionableCriteria, framework),
151
+ });
152
+ }
153
+
154
+ return { files, skipped };
155
+ }
156
+
157
+ module.exports = {
158
+ generateE2eTests,
159
+ };
@@ -0,0 +1,121 @@
1
+ 'use strict';
2
+
3
+ import { describe, it } from 'vitest';
4
+ const assert = require('node:assert');
5
+
6
+ const { generateE2eTests } = require('./test-generator.js');
7
+
8
+ describe('generateE2eTests', () => {
9
+ it('generates playwright test files from parsed acceptance criteria', () => {
10
+ const result = generateE2eTests({
11
+ scenarios: [
12
+ {
13
+ title: 'User Login',
14
+ planContent: `
15
+ - [ ] User can click the sign in button
16
+ Given the user is on the login page
17
+ When they click the sign in button
18
+ Then the dashboard page should load
19
+ `,
20
+ },
21
+ ],
22
+ framework: 'playwright',
23
+ outputDir: 'generated',
24
+ });
25
+
26
+ assert.deepStrictEqual(result.skipped, []);
27
+ assert.strictEqual(result.files.length, 1);
28
+ assert.strictEqual(result.files[0].path, 'generated/e2e/user-login.test.ts');
29
+ assert.match(result.files[0].content, /const \{ test \} = require\('@playwright\/test'\);/);
30
+ assert.match(result.files[0].content, /test\.afterEach\(async \(\{ page \}, testInfo\) =>/);
31
+ assert.match(result.files[0].content, /path: testInfo\.outputPath\('screenshot\.png'\)/);
32
+ assert.match(result.files[0].content, /test\('User can click the sign in button', async \(\{ page \}\) =>/);
33
+ assert.match(result.files[0].content, /test\('the dashboard page should load', async \(\{ page \}\) =>/);
34
+ assert.match(result.files[0].content, /Assert application logs remain clean for this flow\./);
35
+ });
36
+
37
+ it('generates supertest test files with an afterEach artifact hook', () => {
38
+ const result = generateE2eTests({
39
+ scenarios: [
40
+ {
41
+ slug: 'health-check',
42
+ acceptanceCriteria: `
43
+ - [ ] GET /health endpoint returns 200
44
+ `,
45
+ },
46
+ ],
47
+ framework: 'supertest',
48
+ outputDir: '',
49
+ });
50
+
51
+ assert.deepStrictEqual(result.skipped, []);
52
+ assert.strictEqual(result.files.length, 1);
53
+ assert.strictEqual(result.files[0].path, 'e2e/health-check.test.ts');
54
+ assert.match(result.files[0].content, /const \{ afterEach, describe, it \} = require\('vitest'\);/);
55
+ assert.match(result.files[0].content, /const request = require\('supertest'\);/);
56
+ assert.match(result.files[0].content, /afterEach\(async \(\) =>/);
57
+ assert.match(result.files[0].content, /Capture a screenshot or response artifact after each test\./);
58
+ assert.match(result.files[0].content, /it\('GET \/health endpoint returns 200', async \(\) =>/);
59
+ });
60
+
61
+ it('skips manual criteria while still generating files for automatable tests', () => {
62
+ const result = generateE2eTests({
63
+ scenarios: [
64
+ {
65
+ title: 'Mixed Coverage',
66
+ planContent: `
67
+ Data is retained for 30 days.
68
+ - [ ] User can click the export button
69
+ `,
70
+ },
71
+ ],
72
+ framework: 'playwright',
73
+ outputDir: 'artifacts',
74
+ });
75
+
76
+ assert.deepStrictEqual(result.skipped, [
77
+ {
78
+ slug: 'mixed-coverage',
79
+ criterion: 'Data is retained for 30 days.',
80
+ type: 'manual',
81
+ },
82
+ ]);
83
+ assert.strictEqual(result.files.length, 1);
84
+ assert.strictEqual(result.files[0].path, 'artifacts/e2e/mixed-coverage.test.ts');
85
+ assert.match(result.files[0].content, /test\('User can click the export button'/);
86
+ assert.doesNotMatch(result.files[0].content, /Data is retained for 30 days\./);
87
+ });
88
+
89
+ it('omits files for scenarios that only produce manual criteria', () => {
90
+ const result = generateE2eTests({
91
+ scenarios: [
92
+ {
93
+ title: 'Manual Review',
94
+ planContent: 'Data is retained for 30 days.',
95
+ },
96
+ ],
97
+ framework: 'supertest',
98
+ outputDir: 'tmp',
99
+ });
100
+
101
+ assert.deepStrictEqual(result.files, []);
102
+ assert.deepStrictEqual(result.skipped, [
103
+ {
104
+ slug: 'manual-review',
105
+ criterion: 'Data is retained for 30 days.',
106
+ type: 'manual',
107
+ },
108
+ ]);
109
+ });
110
+
111
+ it('throws for unsupported frameworks', () => {
112
+ assert.throws(
113
+ () => generateE2eTests({
114
+ scenarios: [{ title: 'User Login', planContent: '- [ ] User can click the sign in button' }],
115
+ framework: 'cypress',
116
+ outputDir: 'generated',
117
+ }),
118
+ /Unsupported E2E framework: cypress/
119
+ );
120
+ });
121
+ });