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
@@ -37,8 +37,13 @@ function buildProgram(commandName) {
37
37
  " };",
38
38
  "}",
39
39
  "function resolveRouting(options) {",
40
+ " const MODES = {",
41
+ " 'build': 'interactive', 'quick': 'interactive', 'autofix': 'interactive',",
42
+ " 'edge-cases': 'exec', 'review': 'exec', 'coverage': 'exec',",
43
+ " };",
40
44
  " let models = ['claude'];",
41
45
  " let strategy = 'single';",
46
+ " let mode = MODES[options.command] || 'inline';",
42
47
  " let source = 'shipped-defaults';",
43
48
  " let providers;",
44
49
  " const warnings = [];",
@@ -62,6 +67,9 @@ function buildProgram(commandName) {
62
67
  " if (personalRouting.strategy) {",
63
68
  " strategy = personalRouting.strategy;",
64
69
  " }",
70
+ " if (personalRouting.mode) {",
71
+ " mode = personalRouting.mode;",
72
+ " }",
65
73
  " source = 'personal-config';",
66
74
  " }",
67
75
  " }",
@@ -82,7 +90,10 @@ function buildProgram(commandName) {
82
90
  " if (overrideEntry.strategy) {",
83
91
  " strategy = overrideEntry.strategy;",
84
92
  " }",
85
- " source = 'project-override';",
93
+ " if (overrideEntry.mode) {",
94
+ " mode = overrideEntry.mode;",
95
+ " }",
96
+ " source = 'project-override';",
86
97
  " }",
87
98
  " }",
88
99
  " if (options.flagModel) {",
@@ -90,7 +101,7 @@ function buildProgram(commandName) {
90
101
  " strategy = 'single';",
91
102
  " source = 'flag-override';",
92
103
  " }",
93
- " const result = { models, strategy, source, warnings };",
104
+ " const result = { models, strategy, mode, source, warnings };",
94
105
  " if (providers) {",
95
106
  " result.providers = providers;",
96
107
  " }",
@@ -49,6 +49,7 @@ describe('routing-preamble', () => {
49
49
  expect(result).toEqual({
50
50
  models: ['claude'],
51
51
  strategy: 'single',
52
+ mode: 'interactive',
52
53
  source: 'shipped-defaults',
53
54
  warnings: [],
54
55
  });
@@ -67,6 +68,7 @@ describe('routing-preamble', () => {
67
68
  expect(result).toEqual({
68
69
  models: ['codex', 'claude'],
69
70
  strategy: 'parallel',
71
+ mode: 'exec',
70
72
  source: 'personal-config',
71
73
  warnings: [],
72
74
  });
@@ -90,6 +92,7 @@ describe('routing-preamble', () => {
90
92
  expect(result).toEqual({
91
93
  models: ['gemini'],
92
94
  strategy: 'single',
95
+ mode: 'interactive',
93
96
  source: 'project-override',
94
97
  warnings: [],
95
98
  });
@@ -108,6 +111,7 @@ describe('routing-preamble', () => {
108
111
  expect(result).toEqual({
109
112
  models: ['codex'],
110
113
  strategy: 'single',
114
+ mode: 'interactive',
111
115
  source: 'personal-config',
112
116
  warnings: [expect.stringMatching(/models/i)],
113
117
  });
@@ -126,6 +130,7 @@ describe('routing-preamble', () => {
126
130
  expect(result).toEqual({
127
131
  models: ['codex', 'claude'],
128
132
  strategy: 'parallel',
133
+ mode: 'interactive',
129
134
  source: 'personal-config',
130
135
  warnings: [],
131
136
  });
@@ -150,6 +155,7 @@ describe('routing-preamble', () => {
150
155
  expect(result).toEqual({
151
156
  models: ['codex'],
152
157
  strategy: 'single',
158
+ mode: 'interactive',
153
159
  source: 'personal-config',
154
160
  providers,
155
161
  warnings: [],
@@ -175,6 +181,7 @@ describe('routing-preamble', () => {
175
181
  expect(result).toEqual({
176
182
  models: ['local-model'],
177
183
  strategy: 'single',
184
+ mode: 'interactive',
178
185
  source: 'flag-override',
179
186
  warnings: [],
180
187
  });
@@ -198,6 +205,7 @@ describe('routing-preamble', () => {
198
205
  expect(JSON.parse(output)).toEqual({
199
206
  models: ['claude'],
200
207
  strategy: 'single',
208
+ mode: 'interactive',
201
209
  source: 'shipped-defaults',
202
210
  warnings: [expect.stringContaining(path.join(homeDir, '.tlc', 'config.json'))],
203
211
  });
@@ -220,6 +228,7 @@ describe('routing-preamble', () => {
220
228
  expect(JSON.parse(output)).toEqual({
221
229
  models: ['claude'],
222
230
  strategy: 'single',
231
+ mode: 'interactive',
223
232
  source: 'shipped-defaults',
224
233
  warnings: [expect.stringContaining(path.join(cwd, '.tlc.json'))],
225
234
  });
@@ -248,6 +257,7 @@ describe('routing-preamble', () => {
248
257
  expect(JSON.parse(output)).toEqual({
249
258
  models: ['codex'],
250
259
  strategy: 'single',
260
+ mode: 'inline',
251
261
  source: 'project-override',
252
262
  warnings: [expect.stringMatching(/models/i)],
253
263
  });
@@ -259,8 +269,47 @@ describe('routing-preamble', () => {
259
269
  expect(result).toEqual({
260
270
  models: ['claude'],
261
271
  strategy: 'single',
272
+ mode: 'inline',
262
273
  source: 'shipped-defaults',
263
274
  warnings: [],
264
275
  });
265
276
  });
277
+
278
+ it('personal config can override mode', () => {
279
+ const result = runGeneratedScript({
280
+ commandName: 'build',
281
+ homeConfig: {
282
+ task_routing: {
283
+ build: { models: ['codex'], strategy: 'single', mode: 'exec' },
284
+ },
285
+ },
286
+ });
287
+
288
+ expect(result).toEqual({
289
+ models: ['codex'],
290
+ strategy: 'single',
291
+ mode: 'exec',
292
+ source: 'personal-config',
293
+ warnings: [],
294
+ });
295
+ });
296
+
297
+ it('project override can override mode', () => {
298
+ const result = runGeneratedScript({
299
+ commandName: 'review',
300
+ projectConfig: {
301
+ task_routing_override: {
302
+ review: { models: ['claude'], strategy: 'single', mode: 'interactive' },
303
+ },
304
+ },
305
+ });
306
+
307
+ expect(result).toEqual({
308
+ models: ['claude'],
309
+ strategy: 'single',
310
+ mode: 'interactive',
311
+ source: 'project-override',
312
+ warnings: [],
313
+ });
314
+ });
266
315
  });
@@ -0,0 +1,139 @@
1
+ const path = require('path');
2
+
3
+ function isFile(fsImpl, targetPath) {
4
+ try {
5
+ return fsImpl.statSync(targetPath).isFile();
6
+ } catch {
7
+ return false;
8
+ }
9
+ }
10
+
11
+ function isDirectory(fsImpl, targetPath) {
12
+ try {
13
+ return fsImpl.statSync(targetPath).isDirectory();
14
+ } catch {
15
+ return false;
16
+ }
17
+ }
18
+
19
+ function listFiles(fsImpl, targetDir) {
20
+ try {
21
+ return fsImpl.readdirSync(targetDir, { withFileTypes: true });
22
+ } catch {
23
+ return [];
24
+ }
25
+ }
26
+
27
+ function readFile(fsImpl, filePath) {
28
+ try {
29
+ return fsImpl.readFileSync(filePath, 'utf8');
30
+ } catch {
31
+ return '';
32
+ }
33
+ }
34
+
35
+ function makeWorkflow(projectDir, workflowPath, contents) {
36
+ const normalized = String(contents || '').toLowerCase();
37
+
38
+ return {
39
+ path: path.relative(projectDir, workflowPath).replace(/\\/g, '/'),
40
+ hasTest: /\b(test|tests|jest|vitest|mocha|pytest|rspec|go test|cargo test|npm test|pnpm test|yarn test)\b/.test(normalized),
41
+ hasDeploy: /\b(deploy|deployment|release|publish|ship|helm upgrade|kubectl apply|terraform apply)\b/.test(normalized),
42
+ hasCoverage: /\b(coverage|codecov|coveralls|lcov|nyc|cobertura)\b/.test(normalized),
43
+ };
44
+ }
45
+
46
+ function collectGithubWorkflows(projectDir, fsImpl) {
47
+ const workflowsDir = path.join(projectDir, '.github', 'workflows');
48
+ const entries = listFiles(fsImpl, workflowsDir);
49
+
50
+ return entries
51
+ .filter((entry) => entry.isFile() && /\.ya?ml$/i.test(entry.name))
52
+ .map((entry) => {
53
+ const workflowPath = path.join(workflowsDir, entry.name);
54
+ return makeWorkflow(projectDir, workflowPath, readFile(fsImpl, workflowPath));
55
+ })
56
+ .sort((a, b) => a.path.localeCompare(b.path));
57
+ }
58
+
59
+ function collectCircleciWorkflows(projectDir, fsImpl) {
60
+ const circleDir = path.join(projectDir, '.circleci');
61
+ if (!isDirectory(fsImpl, circleDir)) {
62
+ return [];
63
+ }
64
+
65
+ const entries = listFiles(fsImpl, circleDir).filter((entry) => entry.isFile());
66
+ if (entries.length === 0) {
67
+ return [
68
+ {
69
+ path: '.circleci/',
70
+ hasTest: false,
71
+ hasDeploy: false,
72
+ hasCoverage: false,
73
+ },
74
+ ];
75
+ }
76
+
77
+ return entries
78
+ .map((entry) => {
79
+ const workflowPath = path.join(circleDir, entry.name);
80
+ return makeWorkflow(projectDir, workflowPath, readFile(fsImpl, workflowPath));
81
+ })
82
+ .sort((a, b) => a.path.localeCompare(b.path));
83
+ }
84
+
85
+ function detectCI({ projectDir, fs: fsImpl }) {
86
+ const githubWorkflows = collectGithubWorkflows(projectDir, fsImpl);
87
+ const jenkinsPath = path.join(projectDir, 'Jenkinsfile');
88
+ const gitlabPath = path.join(projectDir, '.gitlab-ci.yml');
89
+ const circleciWorkflows = collectCircleciWorkflows(projectDir, fsImpl);
90
+
91
+ let platform = 'none';
92
+ let workflows = [];
93
+
94
+ if (githubWorkflows.length > 0) {
95
+ platform = 'github-actions';
96
+ workflows = githubWorkflows;
97
+ } else if (isFile(fsImpl, jenkinsPath)) {
98
+ platform = 'jenkins';
99
+ workflows = [makeWorkflow(projectDir, jenkinsPath, readFile(fsImpl, jenkinsPath))];
100
+ } else if (isFile(fsImpl, gitlabPath)) {
101
+ platform = 'gitlab';
102
+ workflows = [makeWorkflow(projectDir, gitlabPath, readFile(fsImpl, gitlabPath))];
103
+ } else if (circleciWorkflows.length > 0) {
104
+ platform = 'circleci';
105
+ workflows = circleciWorkflows;
106
+ }
107
+
108
+ if (platform === 'none') {
109
+ return {
110
+ platform,
111
+ workflows: [],
112
+ gaps: ['no-ci'],
113
+ };
114
+ }
115
+
116
+ const gaps = [];
117
+
118
+ if (!workflows.some((workflow) => workflow.hasTest)) {
119
+ gaps.push('missing-test-step');
120
+ }
121
+
122
+ if (!workflows.some((workflow) => workflow.hasDeploy)) {
123
+ gaps.push('missing-deploy-step');
124
+ }
125
+
126
+ if (!workflows.some((workflow) => workflow.hasCoverage)) {
127
+ gaps.push('missing-coverage-upload');
128
+ }
129
+
130
+ return {
131
+ platform,
132
+ workflows,
133
+ gaps,
134
+ };
135
+ }
136
+
137
+ module.exports = {
138
+ detectCI,
139
+ };
@@ -0,0 +1,198 @@
1
+ import { afterEach, describe, expect, it } from 'vitest';
2
+
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+
7
+ const { detectCI } = require('./ci-detector');
8
+
9
+ function makeTempProject() {
10
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'ci-detector-test-'));
11
+ }
12
+
13
+ function writeFile(projectDir, relativePath, contents = '') {
14
+ const filePath = path.join(projectDir, relativePath);
15
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
16
+ fs.writeFileSync(filePath, contents);
17
+ }
18
+
19
+ function makeDir(projectDir, relativePath) {
20
+ fs.mkdirSync(path.join(projectDir, relativePath), { recursive: true });
21
+ }
22
+
23
+ const tempDirs = [];
24
+
25
+ function createProject() {
26
+ const projectDir = makeTempProject();
27
+ tempDirs.push(projectDir);
28
+ return projectDir;
29
+ }
30
+
31
+ afterEach(() => {
32
+ while (tempDirs.length > 0) {
33
+ fs.rmSync(tempDirs.pop(), { recursive: true, force: true });
34
+ }
35
+ });
36
+
37
+ describe('detectCI', () => {
38
+ it('returns no-ci when no CI config exists', () => {
39
+ const projectDir = createProject();
40
+
41
+ expect(detectCI({ projectDir, fs })).toEqual({
42
+ platform: 'none',
43
+ workflows: [],
44
+ gaps: ['no-ci'],
45
+ });
46
+ });
47
+
48
+ it('detects GitHub Actions workflows and aggregates missing gaps across files', () => {
49
+ const projectDir = createProject();
50
+ writeFile(
51
+ projectDir,
52
+ '.github/workflows/test.yml',
53
+ [
54
+ 'name: Test',
55
+ 'jobs:',
56
+ ' test:',
57
+ ' steps:',
58
+ ' - run: npm test',
59
+ ' - run: npx vitest --coverage',
60
+ ].join('\n')
61
+ );
62
+ writeFile(
63
+ projectDir,
64
+ '.github/workflows/deploy.yml',
65
+ [
66
+ 'name: Deploy',
67
+ 'jobs:',
68
+ ' deploy:',
69
+ ' steps:',
70
+ ' - run: npm run deploy',
71
+ ' - uses: codecov/codecov-action@v4',
72
+ ].join('\n')
73
+ );
74
+
75
+ expect(detectCI({ projectDir, fs })).toEqual({
76
+ platform: 'github-actions',
77
+ workflows: [
78
+ {
79
+ path: '.github/workflows/deploy.yml',
80
+ hasTest: false,
81
+ hasDeploy: true,
82
+ hasCoverage: true,
83
+ },
84
+ {
85
+ path: '.github/workflows/test.yml',
86
+ hasTest: true,
87
+ hasDeploy: false,
88
+ hasCoverage: true,
89
+ },
90
+ ],
91
+ gaps: [],
92
+ });
93
+ });
94
+
95
+ it('detects Jenkinsfile and reports missing coverage upload', () => {
96
+ const projectDir = createProject();
97
+ writeFile(
98
+ projectDir,
99
+ 'Jenkinsfile',
100
+ [
101
+ 'pipeline {',
102
+ ' stages {',
103
+ ' stage("Test") { steps { sh "npm test" } }',
104
+ ' stage("Deploy") { steps { sh "npm run deploy" } }',
105
+ ' }',
106
+ '}',
107
+ ].join('\n')
108
+ );
109
+
110
+ expect(detectCI({ projectDir, fs })).toEqual({
111
+ platform: 'jenkins',
112
+ workflows: [
113
+ {
114
+ path: 'Jenkinsfile',
115
+ hasTest: true,
116
+ hasDeploy: true,
117
+ hasCoverage: false,
118
+ },
119
+ ],
120
+ gaps: ['missing-coverage-upload'],
121
+ });
122
+ });
123
+
124
+ it('detects GitLab CI and reports missing deploy and coverage steps', () => {
125
+ const projectDir = createProject();
126
+ writeFile(
127
+ projectDir,
128
+ '.gitlab-ci.yml',
129
+ [
130
+ 'stages:',
131
+ ' - test',
132
+ 'test:',
133
+ ' script:',
134
+ ' - pytest',
135
+ ].join('\n')
136
+ );
137
+
138
+ expect(detectCI({ projectDir, fs })).toEqual({
139
+ platform: 'gitlab',
140
+ workflows: [
141
+ {
142
+ path: '.gitlab-ci.yml',
143
+ hasTest: true,
144
+ hasDeploy: false,
145
+ hasCoverage: false,
146
+ },
147
+ ],
148
+ gaps: ['missing-deploy-step', 'missing-coverage-upload'],
149
+ });
150
+ });
151
+
152
+ it('detects CircleCI from the directory and workflow file', () => {
153
+ const projectDir = createProject();
154
+ writeFile(
155
+ projectDir,
156
+ '.circleci/config.yml',
157
+ [
158
+ 'version: 2.1',
159
+ 'jobs:',
160
+ ' build:',
161
+ ' steps:',
162
+ ' - run: yarn test',
163
+ ' - run: yarn coverage',
164
+ ].join('\n')
165
+ );
166
+
167
+ expect(detectCI({ projectDir, fs })).toEqual({
168
+ platform: 'circleci',
169
+ workflows: [
170
+ {
171
+ path: '.circleci/config.yml',
172
+ hasTest: true,
173
+ hasDeploy: false,
174
+ hasCoverage: true,
175
+ },
176
+ ],
177
+ gaps: ['missing-deploy-step'],
178
+ });
179
+ });
180
+
181
+ it('treats an empty .circleci directory as CI with all gaps missing', () => {
182
+ const projectDir = createProject();
183
+ makeDir(projectDir, '.circleci');
184
+
185
+ expect(detectCI({ projectDir, fs })).toEqual({
186
+ platform: 'circleci',
187
+ workflows: [
188
+ {
189
+ path: '.circleci/',
190
+ hasTest: false,
191
+ hasDeploy: false,
192
+ hasCoverage: false,
193
+ },
194
+ ],
195
+ gaps: ['missing-test-step', 'missing-deploy-step', 'missing-coverage-upload'],
196
+ });
197
+ });
198
+ });