tlc-claude-code 2.4.2 → 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.
@@ -1,4 +1,4 @@
1
- import { describe, it, expect, vi } from 'vitest';
1
+ import { describe, it, expect, vi, afterEach } from 'vitest';
2
2
  import {
3
3
  showRouting,
4
4
  showProviders,
@@ -8,6 +8,10 @@ import {
8
8
  } from './routing-command.js';
9
9
 
10
10
  describe('routing-command', () => {
11
+ afterEach(() => {
12
+ vi.restoreAllMocks();
13
+ });
14
+
11
15
  // ── showRouting ───────────────────────────────────────────────────
12
16
  describe('showRouting', () => {
13
17
  it('returns routing for all routable commands', () => {
@@ -204,6 +208,40 @@ describe('routing-command', () => {
204
208
  const parsed = JSON.parse(writtenData);
205
209
  expect(parsed.task_routing).toEqual(routing);
206
210
  });
211
+
212
+ it('warns when existing personal config JSON is invalid', () => {
213
+ const routing = { build: { models: ['codex'], strategy: 'single' } };
214
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
215
+ const fs = {
216
+ existsSync: vi.fn(() => true),
217
+ readFileSync: vi.fn(() => '{invalid json'),
218
+ mkdirSync: vi.fn(),
219
+ writeFileSync: vi.fn(),
220
+ };
221
+
222
+ savePersonalRouting({ routing, homeDir: '/home/user', fs });
223
+
224
+ expect(warnSpy).toHaveBeenCalledWith(
225
+ '[TLC WARNING] Failed to parse existing personal routing config; overwriting task_routing'
226
+ );
227
+ });
228
+
229
+ it('stays silent when personal config file is missing', () => {
230
+ const routing = { build: { models: ['codex'], strategy: 'single' } };
231
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
232
+ const error = new Error('ENOENT');
233
+ error.code = 'ENOENT';
234
+ const fs = {
235
+ existsSync: vi.fn(() => false),
236
+ readFileSync: vi.fn(() => { throw error; }),
237
+ mkdirSync: vi.fn(),
238
+ writeFileSync: vi.fn(),
239
+ };
240
+
241
+ savePersonalRouting({ routing, homeDir: '/home/user', fs });
242
+
243
+ expect(warnSpy).not.toHaveBeenCalled();
244
+ });
207
245
  });
208
246
 
209
247
  // ── formatRoutingTable ────────────────────────────────────────────
@@ -0,0 +1,319 @@
1
+ import { describe, it, expect, afterEach } from 'vitest';
2
+ import crypto from 'crypto';
3
+ import fs from 'fs';
4
+ import os from 'os';
5
+ import path from 'path';
6
+ import { spawnSync } from 'child_process';
7
+ import routingPreamble from './routing-preamble.js';
8
+
9
+ const { generatePreamble } = routingPreamble;
10
+
11
+ const createdDirs = [];
12
+
13
+ function makeTempDir(name) {
14
+ const dir = path.join(os.tmpdir(), `tlc-routing-${name}-${crypto.randomUUID()}`);
15
+ fs.mkdirSync(dir, { recursive: true });
16
+ createdDirs.push(dir);
17
+ return dir;
18
+ }
19
+
20
+ function writeJson(filePath, value) {
21
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
22
+ fs.writeFileSync(filePath, JSON.stringify(value, null, 2));
23
+ }
24
+
25
+ function writeFile(filePath, contents) {
26
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
27
+ fs.writeFileSync(filePath, contents);
28
+ }
29
+
30
+ function runPreamble({
31
+ commandName,
32
+ flagModel,
33
+ personalConfig,
34
+ personalConfigRaw,
35
+ projectConfig,
36
+ projectConfigRaw,
37
+ projectDirName = 'project',
38
+ } = {}) {
39
+ const homeDir = makeTempDir('home');
40
+ const cwd = makeTempDir(projectDirName);
41
+
42
+ if (personalConfig !== undefined) {
43
+ writeJson(path.join(homeDir, '.tlc', 'config.json'), personalConfig);
44
+ }
45
+
46
+ if (personalConfigRaw !== undefined) {
47
+ writeFile(path.join(homeDir, '.tlc', 'config.json'), personalConfigRaw);
48
+ }
49
+
50
+ if (projectConfig !== undefined) {
51
+ writeJson(path.join(cwd, '.tlc.json'), projectConfig);
52
+ }
53
+
54
+ if (projectConfigRaw !== undefined) {
55
+ writeFile(path.join(cwd, '.tlc.json'), projectConfigRaw);
56
+ }
57
+
58
+ const command = flagModel
59
+ ? `${generatePreamble(commandName)} -- ${JSON.stringify(flagModel)}`
60
+ : generatePreamble(commandName);
61
+
62
+ const completed = spawnSync('bash', ['-lc', command], {
63
+ cwd,
64
+ env: {
65
+ ...process.env,
66
+ HOME: homeDir,
67
+ },
68
+ encoding: 'utf8',
69
+ });
70
+
71
+ if (completed.status !== 0) {
72
+ throw new Error(completed.stderr || `Preamble command failed with exit code ${completed.status}`);
73
+ }
74
+
75
+ return {
76
+ result: JSON.parse(completed.stdout),
77
+ stderr: completed.stderr,
78
+ homeDir,
79
+ cwd,
80
+ };
81
+ }
82
+
83
+ afterEach(() => {
84
+ while (createdDirs.length > 0) {
85
+ const dir = createdDirs.pop();
86
+ fs.rmSync(dir, { recursive: true, force: true });
87
+ }
88
+ });
89
+
90
+ describe('routing-preamble integration', () => {
91
+ it('uses defaults when no config files exist', () => {
92
+ expect(runPreamble({ commandName: 'build' }).result).toEqual({
93
+ models: ['claude'],
94
+ strategy: 'single',
95
+ source: 'shipped-defaults',
96
+ warnings: [],
97
+ });
98
+ });
99
+
100
+ it('uses a personal config model override', () => {
101
+ expect(
102
+ runPreamble({
103
+ commandName: 'build',
104
+ personalConfig: {
105
+ task_routing: {
106
+ build: { model: 'codex' },
107
+ },
108
+ },
109
+ }).result,
110
+ ).toEqual({
111
+ models: ['codex'],
112
+ strategy: 'single',
113
+ source: 'personal-config',
114
+ warnings: [expect.stringMatching(/models/i)],
115
+ });
116
+ });
117
+
118
+ it('uses a personal config models array as-is', () => {
119
+ expect(
120
+ runPreamble({
121
+ commandName: 'review',
122
+ personalConfig: {
123
+ task_routing: {
124
+ review: { models: ['codex', 'claude'] },
125
+ },
126
+ },
127
+ }).result,
128
+ ).toEqual({
129
+ models: ['codex', 'claude'],
130
+ strategy: 'single',
131
+ source: 'personal-config',
132
+ warnings: [],
133
+ });
134
+ });
135
+
136
+ it('lets a project override win over personal routing', () => {
137
+ expect(
138
+ runPreamble({
139
+ commandName: 'docs',
140
+ personalConfig: {
141
+ task_routing: {
142
+ docs: { model: 'codex', strategy: 'parallel' },
143
+ },
144
+ },
145
+ projectConfig: {
146
+ task_routing_override: {
147
+ docs: { model: 'claude' },
148
+ },
149
+ },
150
+ }).result,
151
+ ).toEqual({
152
+ models: ['claude'],
153
+ strategy: 'parallel',
154
+ source: 'project-override',
155
+ warnings: [expect.stringMatching(/models/i), expect.stringMatching(/models/i)],
156
+ });
157
+ });
158
+
159
+ it('uses the project override when both config files are present', () => {
160
+ expect(
161
+ runPreamble({
162
+ commandName: 'test',
163
+ personalConfig: {
164
+ task_routing: {
165
+ test: { models: ['codex', 'claude'], strategy: 'parallel' },
166
+ },
167
+ },
168
+ projectConfig: {
169
+ task_routing_override: {
170
+ test: { models: ['claude'], strategy: 'single' },
171
+ },
172
+ },
173
+ }).result,
174
+ ).toEqual({
175
+ models: ['claude'],
176
+ strategy: 'single',
177
+ source: 'project-override',
178
+ warnings: [],
179
+ });
180
+ });
181
+
182
+ it('lets a flag override win over project and personal config', () => {
183
+ expect(
184
+ runPreamble({
185
+ commandName: 'build',
186
+ flagModel: 'local-model',
187
+ personalConfig: {
188
+ task_routing: {
189
+ build: { model: 'codex', strategy: 'parallel' },
190
+ },
191
+ },
192
+ projectConfig: {
193
+ task_routing_override: {
194
+ build: { model: 'claude', strategy: 'parallel' },
195
+ },
196
+ },
197
+ }).result,
198
+ ).toEqual({
199
+ models: ['local-model'],
200
+ strategy: 'single',
201
+ source: 'flag-override',
202
+ warnings: [expect.stringMatching(/models/i), expect.stringMatching(/models/i)],
203
+ });
204
+ });
205
+
206
+ it('passes through model providers from personal config', () => {
207
+ const providers = {
208
+ claude: { type: 'cli', command: 'claude' },
209
+ codex: { type: 'cli', command: 'codex' },
210
+ };
211
+
212
+ expect(
213
+ runPreamble({
214
+ commandName: 'build',
215
+ personalConfig: {
216
+ task_routing: {
217
+ build: { model: 'codex' },
218
+ },
219
+ model_providers: providers,
220
+ },
221
+ }).result,
222
+ ).toEqual({
223
+ models: ['codex'],
224
+ strategy: 'single',
225
+ source: 'personal-config',
226
+ providers,
227
+ warnings: [expect.stringMatching(/models/i)],
228
+ });
229
+ });
230
+
231
+ it('falls back to defaults for malformed personal config JSON', () => {
232
+ const { result, stderr, homeDir } = runPreamble({
233
+ commandName: 'build',
234
+ personalConfigRaw: '{not-valid-json',
235
+ });
236
+
237
+ expect(result).toEqual({
238
+ models: ['claude'],
239
+ strategy: 'single',
240
+ source: 'shipped-defaults',
241
+ warnings: [expect.stringContaining(path.join(homeDir, '.tlc', 'config.json'))],
242
+ });
243
+ expect(stderr).toContain(path.join(homeDir, '.tlc', 'config.json'));
244
+ });
245
+
246
+ it('falls back to defaults for malformed project config JSON', () => {
247
+ const { result, stderr, cwd } = runPreamble({
248
+ commandName: 'build',
249
+ projectConfigRaw: '{broken-json',
250
+ });
251
+
252
+ expect(result).toEqual({
253
+ models: ['claude'],
254
+ strategy: 'single',
255
+ source: 'shipped-defaults',
256
+ warnings: [expect.stringContaining(path.join(cwd, '.tlc.json'))],
257
+ });
258
+ expect(stderr).toContain(path.join(cwd, '.tlc.json'));
259
+ });
260
+
261
+ it('still uses personal config when .tlc.json is missing in cwd', () => {
262
+ expect(
263
+ runPreamble({
264
+ commandName: 'quick',
265
+ personalConfig: {
266
+ task_routing: {
267
+ quick: { model: 'codex' },
268
+ },
269
+ },
270
+ projectDirName: 'cwd-without-project-config',
271
+ }).result,
272
+ ).toEqual({
273
+ models: ['codex'],
274
+ strategy: 'single',
275
+ source: 'personal-config',
276
+ warnings: [expect.stringMatching(/models/i)],
277
+ });
278
+ });
279
+
280
+ it('preserves a parallel strategy from config', () => {
281
+ expect(
282
+ runPreamble({
283
+ commandName: 'plan',
284
+ personalConfig: {
285
+ task_routing: {
286
+ plan: { model: 'codex', strategy: 'parallel' },
287
+ },
288
+ },
289
+ }).result,
290
+ ).toEqual({
291
+ models: ['codex'],
292
+ strategy: 'parallel',
293
+ source: 'personal-config',
294
+ warnings: [expect.stringMatching(/models/i)],
295
+ });
296
+ });
297
+
298
+ it('prefers models over model in the same entry', () => {
299
+ expect(
300
+ runPreamble({
301
+ commandName: 'build',
302
+ personalConfig: {
303
+ task_routing: {
304
+ build: {
305
+ model: 'claude',
306
+ models: ['codex', 'claude'],
307
+ strategy: 'parallel',
308
+ },
309
+ },
310
+ },
311
+ }).result,
312
+ ).toEqual({
313
+ models: ['codex', 'claude'],
314
+ strategy: 'parallel',
315
+ source: 'personal-config',
316
+ warnings: [],
317
+ });
318
+ });
319
+ });
@@ -13,28 +13,40 @@ function buildProgram(commandName) {
13
13
  "const fs = require('fs');",
14
14
  "const path = require('path');",
15
15
  "const os = require('os');",
16
- "function readJson(filePath, fileSystem) {",
16
+ "function readJson(filePath, fileSystem, label) {",
17
17
  " try {",
18
- " return JSON.parse(fileSystem.readFileSync(filePath, 'utf8'));",
19
- " } catch {",
20
- " return null;",
18
+ " return { data: JSON.parse(fileSystem.readFileSync(filePath, 'utf8')), warning: null };",
19
+ " } catch (error) {",
20
+ " if (error && error.code === 'ENOENT') {",
21
+ " return { data: null, warning: null };",
22
+ " }",
23
+ " return { data: null, warning: 'Failed to parse ' + label + ' routing config at ' + filePath + ': ' + error.message };",
21
24
  " }",
22
25
  "}",
23
26
  "function loadPersonalConfig(options) {",
24
27
  " const configPath = path.join(options.homeDir, '.tlc', 'config.json');",
25
- " return readJson(configPath, options.fs);",
28
+ " const result = readJson(configPath, options.fs, 'personal');",
29
+ " return { config: result.data, warning: result.warning };",
26
30
  "}",
27
31
  "function loadProjectOverride(options) {",
28
32
  " 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;",
33
+ " const result = readJson(configPath, options.fs, 'project');",
34
+ " return {",
35
+ " override: result.data && result.data.task_routing_override ? result.data.task_routing_override : null,",
36
+ " warning: result.warning,",
37
+ " };",
31
38
  "}",
32
39
  "function resolveRouting(options) {",
33
40
  " let models = ['claude'];",
34
41
  " let strategy = 'single';",
35
42
  " let source = 'shipped-defaults';",
36
43
  " let providers;",
37
- " const personal = loadPersonalConfig({ homeDir: options.homeDir, fs: options.fs });",
44
+ " const warnings = [];",
45
+ " const personalResult = loadPersonalConfig({ homeDir: options.homeDir, fs: options.fs });",
46
+ " const personal = personalResult.config;",
47
+ " if (personalResult.warning) {",
48
+ " warnings.push(personalResult.warning);",
49
+ " }",
38
50
  " if (personal) {",
39
51
  " if (personal.model_providers) {",
40
52
  " providers = personal.model_providers;",
@@ -45,6 +57,7 @@ function buildProgram(commandName) {
45
57
  " models = personalRouting.models.slice();",
46
58
  " } else if (typeof personalRouting.model === 'string') {",
47
59
  " models = [personalRouting.model];",
60
+ " warnings.push('Deprecated routing config for command \"' + options.command + '\" in personal config: use \"models\" (array) instead of \"model\" (string).');",
48
61
  " }",
49
62
  " if (personalRouting.strategy) {",
50
63
  " strategy = personalRouting.strategy;",
@@ -52,7 +65,11 @@ function buildProgram(commandName) {
52
65
  " source = 'personal-config';",
53
66
  " }",
54
67
  " }",
55
- " const projectOverride = loadProjectOverride({ projectDir: options.projectDir, fs: options.fs });",
68
+ " const projectResult = loadProjectOverride({ projectDir: options.projectDir, fs: options.fs });",
69
+ " const projectOverride = projectResult.override;",
70
+ " if (projectResult.warning) {",
71
+ " warnings.push(projectResult.warning);",
72
+ " }",
56
73
  " if (projectOverride) {",
57
74
  " const overrideEntry = projectOverride[options.command];",
58
75
  " if (overrideEntry) {",
@@ -60,11 +77,12 @@ function buildProgram(commandName) {
60
77
  " models = overrideEntry.models.slice();",
61
78
  " } else if (typeof overrideEntry.model === 'string') {",
62
79
  " models = [overrideEntry.model];",
80
+ " warnings.push('Deprecated routing config for command \"' + options.command + '\" in project override: use \"models\" (array) instead of \"model\" (string).');",
63
81
  " }",
64
82
  " if (overrideEntry.strategy) {",
65
83
  " strategy = overrideEntry.strategy;",
66
84
  " }",
67
- " source = 'project-override';",
85
+ " source = 'project-override';",
68
86
  " }",
69
87
  " }",
70
88
  " if (options.flagModel) {",
@@ -72,13 +90,18 @@ function buildProgram(commandName) {
72
90
  " strategy = 'single';",
73
91
  " source = 'flag-override';",
74
92
  " }",
75
- " const result = { models, strategy, source };",
93
+ " const result = { models, strategy, source, warnings };",
76
94
  " if (providers) {",
77
95
  " result.providers = providers;",
78
96
  " }",
79
97
  " return result;",
80
98
  "}",
81
99
  `const result = resolveRouting({ command: ${serializedCommand}, flagModel: process.argv[1], projectDir: process.cwd(), homeDir: process.env.HOME || os.homedir(), fs });`,
100
+ "if (Array.isArray(result.warnings) && result.warnings.length > 0) {",
101
+ " for (const warning of result.warnings) {",
102
+ " process.stderr.write(warning + '\\n');",
103
+ " }",
104
+ "}",
82
105
  'process.stdout.write(JSON.stringify(result));',
83
106
  ].join('\n');
84
107
  }
@@ -50,6 +50,7 @@ describe('routing-preamble', () => {
50
50
  models: ['claude'],
51
51
  strategy: 'single',
52
52
  source: 'shipped-defaults',
53
+ warnings: [],
53
54
  });
54
55
  });
55
56
 
@@ -67,6 +68,7 @@ describe('routing-preamble', () => {
67
68
  models: ['codex', 'claude'],
68
69
  strategy: 'parallel',
69
70
  source: 'personal-config',
71
+ warnings: [],
70
72
  });
71
73
  });
72
74
 
@@ -89,6 +91,7 @@ describe('routing-preamble', () => {
89
91
  models: ['gemini'],
90
92
  strategy: 'single',
91
93
  source: 'project-override',
94
+ warnings: [],
92
95
  });
93
96
  });
94
97
 
@@ -106,6 +109,7 @@ describe('routing-preamble', () => {
106
109
  models: ['codex'],
107
110
  strategy: 'single',
108
111
  source: 'personal-config',
112
+ warnings: [expect.stringMatching(/models/i)],
109
113
  });
110
114
  });
111
115
 
@@ -123,6 +127,7 @@ describe('routing-preamble', () => {
123
127
  models: ['codex', 'claude'],
124
128
  strategy: 'parallel',
125
129
  source: 'personal-config',
130
+ warnings: [],
126
131
  });
127
132
  });
128
133
 
@@ -147,6 +152,7 @@ describe('routing-preamble', () => {
147
152
  strategy: 'single',
148
153
  source: 'personal-config',
149
154
  providers,
155
+ warnings: [],
150
156
  });
151
157
  });
152
158
 
@@ -170,6 +176,7 @@ describe('routing-preamble', () => {
170
176
  models: ['local-model'],
171
177
  strategy: 'single',
172
178
  source: 'flag-override',
179
+ warnings: [],
173
180
  });
174
181
  });
175
182
 
@@ -192,6 +199,7 @@ describe('routing-preamble', () => {
192
199
  models: ['claude'],
193
200
  strategy: 'single',
194
201
  source: 'shipped-defaults',
202
+ warnings: [expect.stringContaining(path.join(homeDir, '.tlc', 'config.json'))],
195
203
  });
196
204
  });
197
205
 
@@ -213,6 +221,7 @@ describe('routing-preamble', () => {
213
221
  models: ['claude'],
214
222
  strategy: 'single',
215
223
  source: 'shipped-defaults',
224
+ warnings: [expect.stringContaining(path.join(cwd, '.tlc.json'))],
216
225
  });
217
226
  });
218
227
 
@@ -240,6 +249,7 @@ describe('routing-preamble', () => {
240
249
  models: ['codex'],
241
250
  strategy: 'single',
242
251
  source: 'project-override',
252
+ warnings: [expect.stringMatching(/models/i)],
243
253
  });
244
254
  });
245
255
 
@@ -250,6 +260,7 @@ describe('routing-preamble', () => {
250
260
  models: ['claude'],
251
261
  strategy: 'single',
252
262
  source: 'shipped-defaults',
263
+ warnings: [],
253
264
  });
254
265
  });
255
266
  });
@@ -35,15 +35,21 @@ for (const cmd of ROUTABLE_COMMANDS) {
35
35
  * @param {object} options
36
36
  * @param {string} options.homeDir - Home directory path
37
37
  * @param {object} options.fs - Filesystem module (must have readFileSync)
38
- * @returns {object|null} Parsed config or null on failure
38
+ * @returns {{ config: object|null, warning: string|null }} Parsed config and optional warning
39
39
  */
40
40
  function loadPersonalConfig({ homeDir, fs }) {
41
+ const configPath = path.join(homeDir, '.tlc', 'config.json');
41
42
  try {
42
- const configPath = path.join(homeDir, '.tlc', 'config.json');
43
43
  const content = fs.readFileSync(configPath, 'utf-8');
44
- return JSON.parse(content);
45
- } catch {
46
- return null;
44
+ return { config: JSON.parse(content), warning: null };
45
+ } catch (error) {
46
+ if (error && error.code === 'ENOENT') {
47
+ return { config: null, warning: null };
48
+ }
49
+ return {
50
+ config: null,
51
+ warning: `Failed to parse personal routing config at ${configPath}: ${error.message}`,
52
+ };
47
53
  }
48
54
  }
49
55
 
@@ -52,16 +58,22 @@ function loadPersonalConfig({ homeDir, fs }) {
52
58
  * @param {object} options
53
59
  * @param {string} options.projectDir - Project directory path
54
60
  * @param {object} options.fs - Filesystem module (must have readFileSync)
55
- * @returns {object|null} The task_routing_override section or null
61
+ * @returns {{ override: object|null, warning: string|null }} The task_routing_override section and optional warning
56
62
  */
57
63
  function loadProjectOverride({ projectDir, fs }) {
64
+ const configPath = path.join(projectDir, '.tlc.json');
58
65
  try {
59
- const configPath = path.join(projectDir, '.tlc.json');
60
66
  const content = fs.readFileSync(configPath, 'utf-8');
61
67
  const data = JSON.parse(content);
62
- return data.task_routing_override || null;
63
- } catch {
64
- return null;
68
+ return { override: data.task_routing_override || null, warning: null };
69
+ } catch (error) {
70
+ if (error && error.code === 'ENOENT') {
71
+ return { override: null, warning: null };
72
+ }
73
+ return {
74
+ override: null,
75
+ warning: `Failed to parse project routing config at ${configPath}: ${error.message}`,
76
+ };
65
77
  }
66
78
  }
67
79
 
@@ -74,7 +86,7 @@ function loadProjectOverride({ projectDir, fs }) {
74
86
  * @param {string} options.projectDir - Project directory path
75
87
  * @param {string} options.homeDir - Home directory path
76
88
  * @param {object} options.fs - Filesystem module (must have readFileSync)
77
- * @returns {{ models: string[], strategy: string, source: string, providers?: object }}
89
+ * @returns {{ models: string[], strategy: string, source: string, providers?: object, warnings: string[] }}
78
90
  */
79
91
  function resolveRouting({ command, flagModel, projectDir = process.cwd(), homeDir = require('os').homedir(), fs = require('fs') }) {
80
92
  // Start with shipped defaults
@@ -83,9 +95,13 @@ function resolveRouting({ command, flagModel, projectDir = process.cwd(), homeDi
83
95
  let strategy = defaultEntry.strategy;
84
96
  let source = 'shipped-defaults';
85
97
  let providers;
98
+ const warnings = [];
86
99
 
87
100
  // Layer 1: personal config (~/.tlc/config.json)
88
- const personal = loadPersonalConfig({ homeDir, fs });
101
+ const { config: personal, warning: personalWarning } = loadPersonalConfig({ homeDir, fs });
102
+ if (personalWarning) {
103
+ warnings.push(personalWarning);
104
+ }
89
105
  if (personal) {
90
106
  // Capture model_providers if present
91
107
  if (personal.model_providers) {
@@ -98,6 +114,7 @@ function resolveRouting({ command, flagModel, projectDir = process.cwd(), homeDi
98
114
  models = [...personalRouting.models];
99
115
  } else if (personalRouting.model) {
100
116
  models = [personalRouting.model];
117
+ warnings.push(`Deprecated routing config for command "${command}" in personal config: use "models" (array) instead of "model" (string).`);
101
118
  }
102
119
  if (personalRouting.strategy) {
103
120
  strategy = personalRouting.strategy;
@@ -107,7 +124,10 @@ function resolveRouting({ command, flagModel, projectDir = process.cwd(), homeDi
107
124
  }
108
125
 
109
126
  // Layer 2: project override (.tlc.json -> task_routing_override)
110
- const projectOverride = loadProjectOverride({ projectDir, fs });
127
+ const { override: projectOverride, warning: projectWarning } = loadProjectOverride({ projectDir, fs });
128
+ if (projectWarning) {
129
+ warnings.push(projectWarning);
130
+ }
111
131
  if (projectOverride) {
112
132
  const overrideEntry = projectOverride[command];
113
133
  if (overrideEntry) {
@@ -115,6 +135,7 @@ function resolveRouting({ command, flagModel, projectDir = process.cwd(), homeDi
115
135
  models = [...overrideEntry.models];
116
136
  } else if (overrideEntry.model) {
117
137
  models = [overrideEntry.model];
138
+ warnings.push(`Deprecated routing config for command "${command}" in project override: use "models" (array) instead of "model" (string).`);
118
139
  }
119
140
  if (overrideEntry.strategy) {
120
141
  strategy = overrideEntry.strategy;
@@ -130,7 +151,7 @@ function resolveRouting({ command, flagModel, projectDir = process.cwd(), homeDi
130
151
  source = 'flag-override';
131
152
  }
132
153
 
133
- const result = { models, strategy, source };
154
+ const result = { models, strategy, source, warnings };
134
155
  if (providers) {
135
156
  result.providers = providers;
136
157
  }