tlc-claude-code 2.4.1 → 2.4.3
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/autofix.md +70 -6
- package/.claude/commands/tlc/build.md +138 -6
- package/.claude/commands/tlc/coverage.md +70 -6
- package/.claude/commands/tlc/discuss.md +244 -129
- package/.claude/commands/tlc/docs.md +70 -6
- package/.claude/commands/tlc/e2e-verify.md +1 -1
- package/.claude/commands/tlc/edge-cases.md +70 -6
- package/.claude/commands/tlc/plan.md +147 -8
- package/.claude/commands/tlc/quick.md +70 -6
- package/.claude/commands/tlc/review.md +70 -6
- package/.claude/commands/tlc/tlc.md +204 -473
- package/CLAUDE.md +6 -5
- package/package.json +4 -1
- package/scripts/dev-link.sh +29 -0
- package/scripts/test-package.sh +54 -0
- package/scripts/version-sync.js +42 -0
- package/scripts/version-sync.test.js +100 -0
- package/server/lib/model-router.js +11 -2
- package/server/lib/model-router.test.js +27 -1
- package/server/lib/orchestration/codex-orchestrator.js +185 -0
- package/server/lib/orchestration/codex-orchestrator.test.js +221 -0
- package/server/lib/orchestration/dep-linker.js +61 -0
- package/server/lib/orchestration/dep-linker.test.js +174 -0
- package/server/lib/router-config.js +18 -3
- package/server/lib/router-config.test.js +57 -1
- package/server/lib/routing/index.js +34 -0
- package/server/lib/routing/index.test.js +33 -0
- package/server/lib/routing-command.js +11 -2
- package/server/lib/routing-command.test.js +39 -1
- package/server/lib/routing-preamble.integration.test.js +319 -0
- package/server/lib/routing-preamble.js +116 -0
- package/server/lib/routing-preamble.test.js +266 -0
- package/server/lib/task-router-config.js +35 -14
- package/server/lib/task-router-config.test.js +77 -13
|
@@ -10,12 +10,76 @@ This command supports multi-model routing via `~/.tlc/config.json`.
|
|
|
10
10
|
|
|
11
11
|
1. Read routing config:
|
|
12
12
|
```bash
|
|
13
|
-
node -e "
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
node -e "const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const os = require('os');
|
|
16
|
+
function readJson(filePath, fileSystem) {
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(fileSystem.readFileSync(filePath, 'utf8'));
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function loadPersonalConfig(options) {
|
|
24
|
+
const configPath = path.join(options.homeDir, '.tlc', 'config.json');
|
|
25
|
+
return readJson(configPath, options.fs);
|
|
26
|
+
}
|
|
27
|
+
function loadProjectOverride(options) {
|
|
28
|
+
const configPath = path.join(options.projectDir, '.tlc.json');
|
|
29
|
+
const data = readJson(configPath, options.fs);
|
|
30
|
+
return data && data.task_routing_override ? data.task_routing_override : null;
|
|
31
|
+
}
|
|
32
|
+
function resolveRouting(options) {
|
|
33
|
+
let models = ['claude'];
|
|
34
|
+
let strategy = 'single';
|
|
35
|
+
let source = 'shipped-defaults';
|
|
36
|
+
let providers;
|
|
37
|
+
const personal = loadPersonalConfig({ homeDir: options.homeDir, fs: options.fs });
|
|
38
|
+
if (personal) {
|
|
39
|
+
if (personal.model_providers) {
|
|
40
|
+
providers = personal.model_providers;
|
|
41
|
+
}
|
|
42
|
+
const personalRouting = personal.task_routing && personal.task_routing[options.command];
|
|
43
|
+
if (personalRouting) {
|
|
44
|
+
if (Array.isArray(personalRouting.models)) {
|
|
45
|
+
models = personalRouting.models.slice();
|
|
46
|
+
} else if (typeof personalRouting.model === 'string') {
|
|
47
|
+
models = [personalRouting.model];
|
|
48
|
+
}
|
|
49
|
+
if (personalRouting.strategy) {
|
|
50
|
+
strategy = personalRouting.strategy;
|
|
51
|
+
}
|
|
52
|
+
source = 'personal-config';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const projectOverride = loadProjectOverride({ projectDir: options.projectDir, fs: options.fs });
|
|
56
|
+
if (projectOverride) {
|
|
57
|
+
const overrideEntry = projectOverride[options.command];
|
|
58
|
+
if (overrideEntry) {
|
|
59
|
+
if (Array.isArray(overrideEntry.models)) {
|
|
60
|
+
models = overrideEntry.models.slice();
|
|
61
|
+
} else if (typeof overrideEntry.model === 'string') {
|
|
62
|
+
models = [overrideEntry.model];
|
|
63
|
+
}
|
|
64
|
+
if (overrideEntry.strategy) {
|
|
65
|
+
strategy = overrideEntry.strategy;
|
|
66
|
+
}
|
|
67
|
+
source = 'project-override';
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (options.flagModel) {
|
|
71
|
+
models = [options.flagModel];
|
|
72
|
+
strategy = 'single';
|
|
73
|
+
source = 'flag-override';
|
|
74
|
+
}
|
|
75
|
+
const result = { models, strategy, source };
|
|
76
|
+
if (providers) {
|
|
77
|
+
result.providers = providers;
|
|
78
|
+
}
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
const result = resolveRouting({ command: \"autofix\", flagModel: process.argv[1], projectDir: process.cwd(), homeDir: process.env.HOME || os.homedir(), fs });
|
|
82
|
+
process.stdout.write(JSON.stringify(result));" 2>/dev/null
|
|
19
83
|
```
|
|
20
84
|
|
|
21
85
|
2. If `models[0]` is NOT `claude` (i.e., routed to external model):
|
|
@@ -10,12 +10,76 @@ This command supports multi-model routing via `~/.tlc/config.json`.
|
|
|
10
10
|
|
|
11
11
|
1. Read routing config:
|
|
12
12
|
```bash
|
|
13
|
-
node -e "
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
node -e "const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const os = require('os');
|
|
16
|
+
function readJson(filePath, fileSystem) {
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(fileSystem.readFileSync(filePath, 'utf8'));
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function loadPersonalConfig(options) {
|
|
24
|
+
const configPath = path.join(options.homeDir, '.tlc', 'config.json');
|
|
25
|
+
return readJson(configPath, options.fs);
|
|
26
|
+
}
|
|
27
|
+
function loadProjectOverride(options) {
|
|
28
|
+
const configPath = path.join(options.projectDir, '.tlc.json');
|
|
29
|
+
const data = readJson(configPath, options.fs);
|
|
30
|
+
return data && data.task_routing_override ? data.task_routing_override : null;
|
|
31
|
+
}
|
|
32
|
+
function resolveRouting(options) {
|
|
33
|
+
let models = ['claude'];
|
|
34
|
+
let strategy = 'single';
|
|
35
|
+
let source = 'shipped-defaults';
|
|
36
|
+
let providers;
|
|
37
|
+
const personal = loadPersonalConfig({ homeDir: options.homeDir, fs: options.fs });
|
|
38
|
+
if (personal) {
|
|
39
|
+
if (personal.model_providers) {
|
|
40
|
+
providers = personal.model_providers;
|
|
41
|
+
}
|
|
42
|
+
const personalRouting = personal.task_routing && personal.task_routing[options.command];
|
|
43
|
+
if (personalRouting) {
|
|
44
|
+
if (Array.isArray(personalRouting.models)) {
|
|
45
|
+
models = personalRouting.models.slice();
|
|
46
|
+
} else if (typeof personalRouting.model === 'string') {
|
|
47
|
+
models = [personalRouting.model];
|
|
48
|
+
}
|
|
49
|
+
if (personalRouting.strategy) {
|
|
50
|
+
strategy = personalRouting.strategy;
|
|
51
|
+
}
|
|
52
|
+
source = 'personal-config';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const projectOverride = loadProjectOverride({ projectDir: options.projectDir, fs: options.fs });
|
|
56
|
+
if (projectOverride) {
|
|
57
|
+
const overrideEntry = projectOverride[options.command];
|
|
58
|
+
if (overrideEntry) {
|
|
59
|
+
if (Array.isArray(overrideEntry.models)) {
|
|
60
|
+
models = overrideEntry.models.slice();
|
|
61
|
+
} else if (typeof overrideEntry.model === 'string') {
|
|
62
|
+
models = [overrideEntry.model];
|
|
63
|
+
}
|
|
64
|
+
if (overrideEntry.strategy) {
|
|
65
|
+
strategy = overrideEntry.strategy;
|
|
66
|
+
}
|
|
67
|
+
source = 'project-override';
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (options.flagModel) {
|
|
71
|
+
models = [options.flagModel];
|
|
72
|
+
strategy = 'single';
|
|
73
|
+
source = 'flag-override';
|
|
74
|
+
}
|
|
75
|
+
const result = { models, strategy, source };
|
|
76
|
+
if (providers) {
|
|
77
|
+
result.providers = providers;
|
|
78
|
+
}
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
const result = resolveRouting({ command: \"build\", flagModel: process.argv[1], projectDir: process.cwd(), homeDir: process.env.HOME || os.homedir(), fs });
|
|
82
|
+
process.stdout.write(JSON.stringify(result));" 2>/dev/null
|
|
19
83
|
```
|
|
20
84
|
|
|
21
85
|
2. If `models[0]` is NOT `claude` (i.e., routed to external model):
|
|
@@ -212,6 +276,34 @@ This is the core TLC command. Tests before code, one task at a time.
|
|
|
212
276
|
|
|
213
277
|
## Process
|
|
214
278
|
|
|
279
|
+
### Step 0: Create Phase Branch (Mandatory)
|
|
280
|
+
|
|
281
|
+
**Never commit directly to the main branch.** Before ANY work begins:
|
|
282
|
+
|
|
283
|
+
```bash
|
|
284
|
+
# Get main branch name from .tlc.json or default to "main"
|
|
285
|
+
mainBranch=$(node -e "try{console.log(require('./.tlc.json').git?.mainBranch||'main')}catch{console.log('main')}" 2>/dev/null)
|
|
286
|
+
|
|
287
|
+
# Check if already on a phase branch
|
|
288
|
+
currentBranch=$(git branch --show-current)
|
|
289
|
+
if [[ "$currentBranch" == phase/* ]]; then
|
|
290
|
+
echo "Already on phase branch: $currentBranch"
|
|
291
|
+
else
|
|
292
|
+
# Create phase branch
|
|
293
|
+
branchName="phase/${phase_number}"
|
|
294
|
+
git checkout -b "$branchName" "$mainBranch" 2>/dev/null || git checkout "$branchName"
|
|
295
|
+
echo "Working on branch: $branchName"
|
|
296
|
+
fi
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
**This is not optional.** All commits go to the phase branch. When the phase is complete:
|
|
300
|
+
1. Push the branch: `git push -u origin phase/{N}`
|
|
301
|
+
2. Create PR: `gh pr create --base main --head phase/{N}`
|
|
302
|
+
3. CI validates on the PR
|
|
303
|
+
4. Merge when green
|
|
304
|
+
|
|
305
|
+
**Why:** TLC projects use integration branches. Committing to main bypasses CI, review, and the PR workflow we built. Eat your own soup.
|
|
306
|
+
|
|
215
307
|
### Step 1: Load Plans
|
|
216
308
|
|
|
217
309
|
Read all `.planning/phases/{phase}-*-PLAN.md` files for this phase.
|
|
@@ -852,6 +944,46 @@ Verdict: ❌ CHANGES REQUESTED
|
|
|
852
944
|
|
|
853
945
|
**CRITICAL: Phase is NOT complete until review passes.**
|
|
854
946
|
|
|
947
|
+
### Step 11: Push Branch and Create PR (Mandatory)
|
|
948
|
+
|
|
949
|
+
**After review passes, push the phase branch and create a PR. Ask the user before pushing.**
|
|
950
|
+
|
|
951
|
+
```bash
|
|
952
|
+
# Get current branch
|
|
953
|
+
branchName=$(git branch --show-current)
|
|
954
|
+
mainBranch=$(node -e "try{console.log(require('./.tlc.json').git?.mainBranch||'main')}catch{console.log('main')}" 2>/dev/null)
|
|
955
|
+
|
|
956
|
+
# Confirm with user
|
|
957
|
+
echo "Phase complete. Ready to push $branchName and create PR to $mainBranch?"
|
|
958
|
+
```
|
|
959
|
+
|
|
960
|
+
After user approval:
|
|
961
|
+
|
|
962
|
+
```bash
|
|
963
|
+
# Push branch
|
|
964
|
+
git push -u origin "$branchName"
|
|
965
|
+
|
|
966
|
+
# Create PR with phase summary
|
|
967
|
+
gh pr create \
|
|
968
|
+
--base "$mainBranch" \
|
|
969
|
+
--head "$branchName" \
|
|
970
|
+
--title "Phase {N}: {Phase Name}" \
|
|
971
|
+
--body "## Summary
|
|
972
|
+
- {task count} tasks completed
|
|
973
|
+
- {test count} tests passing
|
|
974
|
+
- Review: APPROVED
|
|
975
|
+
|
|
976
|
+
## Tasks
|
|
977
|
+
{list of completed tasks from PLAN.md}
|
|
978
|
+
|
|
979
|
+
## Test Results
|
|
980
|
+
{test runner summary}"
|
|
981
|
+
```
|
|
982
|
+
|
|
983
|
+
**The PR is the deliverable, not the commits.** CI runs on the PR. Merge when green.
|
|
984
|
+
|
|
985
|
+
**If the user has already been working on main** (e.g., first time using branching): push main directly but note that future phases MUST use branches.
|
|
986
|
+
|
|
855
987
|
## Framework Defaults
|
|
856
988
|
|
|
857
989
|
### TLC Default: Mocha Stack
|
|
@@ -10,12 +10,76 @@ This command supports multi-model routing via `~/.tlc/config.json`.
|
|
|
10
10
|
|
|
11
11
|
1. Read routing config:
|
|
12
12
|
```bash
|
|
13
|
-
node -e "
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
node -e "const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const os = require('os');
|
|
16
|
+
function readJson(filePath, fileSystem) {
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(fileSystem.readFileSync(filePath, 'utf8'));
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function loadPersonalConfig(options) {
|
|
24
|
+
const configPath = path.join(options.homeDir, '.tlc', 'config.json');
|
|
25
|
+
return readJson(configPath, options.fs);
|
|
26
|
+
}
|
|
27
|
+
function loadProjectOverride(options) {
|
|
28
|
+
const configPath = path.join(options.projectDir, '.tlc.json');
|
|
29
|
+
const data = readJson(configPath, options.fs);
|
|
30
|
+
return data && data.task_routing_override ? data.task_routing_override : null;
|
|
31
|
+
}
|
|
32
|
+
function resolveRouting(options) {
|
|
33
|
+
let models = ['claude'];
|
|
34
|
+
let strategy = 'single';
|
|
35
|
+
let source = 'shipped-defaults';
|
|
36
|
+
let providers;
|
|
37
|
+
const personal = loadPersonalConfig({ homeDir: options.homeDir, fs: options.fs });
|
|
38
|
+
if (personal) {
|
|
39
|
+
if (personal.model_providers) {
|
|
40
|
+
providers = personal.model_providers;
|
|
41
|
+
}
|
|
42
|
+
const personalRouting = personal.task_routing && personal.task_routing[options.command];
|
|
43
|
+
if (personalRouting) {
|
|
44
|
+
if (Array.isArray(personalRouting.models)) {
|
|
45
|
+
models = personalRouting.models.slice();
|
|
46
|
+
} else if (typeof personalRouting.model === 'string') {
|
|
47
|
+
models = [personalRouting.model];
|
|
48
|
+
}
|
|
49
|
+
if (personalRouting.strategy) {
|
|
50
|
+
strategy = personalRouting.strategy;
|
|
51
|
+
}
|
|
52
|
+
source = 'personal-config';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const projectOverride = loadProjectOverride({ projectDir: options.projectDir, fs: options.fs });
|
|
56
|
+
if (projectOverride) {
|
|
57
|
+
const overrideEntry = projectOverride[options.command];
|
|
58
|
+
if (overrideEntry) {
|
|
59
|
+
if (Array.isArray(overrideEntry.models)) {
|
|
60
|
+
models = overrideEntry.models.slice();
|
|
61
|
+
} else if (typeof overrideEntry.model === 'string') {
|
|
62
|
+
models = [overrideEntry.model];
|
|
63
|
+
}
|
|
64
|
+
if (overrideEntry.strategy) {
|
|
65
|
+
strategy = overrideEntry.strategy;
|
|
66
|
+
}
|
|
67
|
+
source = 'project-override';
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (options.flagModel) {
|
|
71
|
+
models = [options.flagModel];
|
|
72
|
+
strategy = 'single';
|
|
73
|
+
source = 'flag-override';
|
|
74
|
+
}
|
|
75
|
+
const result = { models, strategy, source };
|
|
76
|
+
if (providers) {
|
|
77
|
+
result.providers = providers;
|
|
78
|
+
}
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
const result = resolveRouting({ command: \"coverage\", flagModel: process.argv[1], projectDir: process.cwd(), homeDir: process.env.HOME || os.homedir(), fs });
|
|
82
|
+
process.stdout.write(JSON.stringify(result));" 2>/dev/null
|
|
19
83
|
```
|
|
20
84
|
|
|
21
85
|
2. If `models[0]` is NOT `claude` (i.e., routed to external model):
|