tlc-claude-code 2.5.0 → 2.6.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.
Files changed (76) hide show
  1. package/.claude/commands/tlc/autofix.md +34 -1
  2. package/.claude/commands/tlc/build.md +89 -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 +9 -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,225 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import path from 'path';
3
+
4
+ const fixedDate = new Date('2026-03-30T08:15:00.000Z');
5
+ const mockedFs = vi.hoisted(() => ({
6
+ copyFileSync: vi.fn(),
7
+ }));
8
+
9
+ vi.mock('fs', () => mockedFs);
10
+
11
+ describe('snapshot-manager', () => {
12
+ let exec;
13
+ let fsMock;
14
+ let takeSnapshot;
15
+ let restoreSnapshot;
16
+ let listSnapshots;
17
+ let enforceRetention;
18
+
19
+ beforeEach(async () => {
20
+ vi.resetModules();
21
+ vi.useFakeTimers();
22
+ vi.setSystemTime(fixedDate);
23
+
24
+ exec = vi.fn().mockResolvedValue({ stdout: '', stderr: '' });
25
+ mockedFs.copyFileSync.mockReset();
26
+ fsMock = {
27
+ ...mockedFs,
28
+ readdirSync: vi.fn(),
29
+ statSync: vi.fn(),
30
+ unlinkSync: vi.fn(),
31
+ };
32
+
33
+ ({
34
+ takeSnapshot,
35
+ restoreSnapshot,
36
+ listSnapshots,
37
+ enforceRetention,
38
+ } = await import('./snapshot-manager.js'));
39
+ });
40
+
41
+ afterEach(() => {
42
+ vi.useRealTimers();
43
+ });
44
+
45
+ it('PostgreSQL snapshot runs pg_dump with correct args', async () => {
46
+ const result = await takeSnapshot({
47
+ dbType: 'postgresql',
48
+ connectionString: 'postgres://user:secret@db.example.com:5432/app_db',
49
+ snapshotDir: '/tmp/snaps',
50
+ gitRef: 'abc1234',
51
+ exec,
52
+ });
53
+
54
+ expect(exec).toHaveBeenCalledWith(
55
+ 'pg_dump --format=custom -f "/tmp/snaps/2026-03-30-abc1234-app_db.dump" "postgres://user:secret@db.example.com:5432/app_db"'
56
+ );
57
+ expect(result).toEqual({
58
+ success: true,
59
+ snapshotPath: '/tmp/snaps/2026-03-30-abc1234-app_db.dump',
60
+ });
61
+ });
62
+
63
+ it('MySQL snapshot runs mysqldump with correct args', async () => {
64
+ const result = await takeSnapshot({
65
+ dbType: 'mysql',
66
+ connectionString: 'mysql://root:secret@mysql.example.com:3307/shop',
67
+ snapshotDir: '/tmp/snaps',
68
+ gitRef: 'feature-x',
69
+ exec,
70
+ });
71
+
72
+ expect(exec).toHaveBeenCalledWith(
73
+ 'mysqldump --host=mysql.example.com --port=3307 --user=root --password=secret shop > "/tmp/snaps/2026-03-30-feature-x-shop.dump"'
74
+ );
75
+ expect(result.snapshotPath).toBe('/tmp/snaps/2026-03-30-feature-x-shop.dump');
76
+ });
77
+
78
+ it('SQLite snapshot copies file', async () => {
79
+ const result = await takeSnapshot({
80
+ dbType: 'sqlite',
81
+ connectionString: '/data/app.sqlite',
82
+ snapshotDir: '/tmp/snaps',
83
+ gitRef: 'main',
84
+ exec,
85
+ fs: fsMock,
86
+ });
87
+
88
+ expect(fsMock.copyFileSync).toHaveBeenCalledWith(
89
+ '/data/app.sqlite',
90
+ '/tmp/snaps/2026-03-30-main-app.dump'
91
+ );
92
+ expect(exec).not.toHaveBeenCalled();
93
+ expect(result.snapshotPath).toBe('/tmp/snaps/2026-03-30-main-app.dump');
94
+ });
95
+
96
+ it('Snapshot filename has correct format', async () => {
97
+ const result = await takeSnapshot({
98
+ dbType: 'postgresql',
99
+ connectionString: 'postgres://user:secret@localhost:5432/reporting',
100
+ snapshotDir: '/tmp/snaps',
101
+ gitRef: 'release-1',
102
+ exec,
103
+ });
104
+
105
+ expect(path.basename(result.snapshotPath)).toBe('2026-03-30-release-1-reporting.dump');
106
+ });
107
+
108
+ it('Restore runs correct command per dbType', async () => {
109
+ await restoreSnapshot({
110
+ snapshotPath: '/tmp/snaps/sample.dump',
111
+ dbType: 'postgresql',
112
+ connectionString: 'postgres://user:secret@db.example.com:5432/app_db',
113
+ exec,
114
+ });
115
+
116
+ expect(exec).toHaveBeenNthCalledWith(
117
+ 1,
118
+ 'pg_restore --clean --if-exists --dbname="postgres://user:secret@db.example.com:5432/app_db" "/tmp/snaps/sample.dump"'
119
+ );
120
+
121
+ await restoreSnapshot({
122
+ snapshotPath: '/tmp/snaps/sample.dump',
123
+ dbType: 'mysql',
124
+ connectionString: 'mysql://root:secret@mysql.example.com:3307/shop',
125
+ exec,
126
+ });
127
+
128
+ expect(exec).toHaveBeenNthCalledWith(
129
+ 2,
130
+ 'mysql --host=mysql.example.com --port=3307 --user=root --password=secret shop < "/tmp/snaps/sample.dump"'
131
+ );
132
+
133
+ await restoreSnapshot({
134
+ snapshotPath: '/tmp/snaps/sample.dump',
135
+ dbType: 'sqlite',
136
+ connectionString: '/data/app.sqlite',
137
+ exec,
138
+ fs: fsMock,
139
+ });
140
+
141
+ expect(fsMock.copyFileSync).toHaveBeenCalledWith('/tmp/snaps/sample.dump', '/data/app.sqlite');
142
+ });
143
+
144
+ it('List returns snapshots sorted newest first', () => {
145
+ fsMock.readdirSync.mockReturnValue([
146
+ '2026-03-28-main-app.dump',
147
+ '2026-03-30-feature-app.dump',
148
+ '2026-03-29-fix-auth.dump',
149
+ ]);
150
+ fsMock.statSync.mockImplementation((filePath) => ({
151
+ size: filePath.includes('feature') ? 300 : filePath.includes('fix') ? 200 : 100,
152
+ }));
153
+
154
+ const result = listSnapshots({
155
+ snapshotDir: '/tmp/snaps',
156
+ fs: fsMock,
157
+ });
158
+
159
+ expect(result).toEqual([
160
+ {
161
+ path: '/tmp/snaps/2026-03-30-feature-app.dump',
162
+ date: '2026-03-30',
163
+ gitRef: 'feature',
164
+ dbName: 'app',
165
+ size: 300,
166
+ },
167
+ {
168
+ path: '/tmp/snaps/2026-03-29-fix-auth.dump',
169
+ date: '2026-03-29',
170
+ gitRef: 'fix',
171
+ dbName: 'auth',
172
+ size: 200,
173
+ },
174
+ {
175
+ path: '/tmp/snaps/2026-03-28-main-app.dump',
176
+ date: '2026-03-28',
177
+ gitRef: 'main',
178
+ dbName: 'app',
179
+ size: 100,
180
+ },
181
+ ]);
182
+ });
183
+
184
+ it('Retention deletes oldest when over limit', () => {
185
+ fsMock.readdirSync.mockReturnValue([
186
+ '2026-03-27-main-app.dump',
187
+ '2026-03-28-main-app.dump',
188
+ '2026-03-29-main-app.dump',
189
+ '2026-03-30-main-app.dump',
190
+ ]);
191
+ fsMock.statSync.mockImplementation(() => ({ size: 100 }));
192
+
193
+ const deleted = enforceRetention({
194
+ snapshotDir: '/tmp/snaps',
195
+ maxSnapshots: 2,
196
+ fs: fsMock,
197
+ });
198
+
199
+ expect(fsMock.unlinkSync).toHaveBeenCalledTimes(2);
200
+ expect(fsMock.unlinkSync).toHaveBeenNthCalledWith(1, '/tmp/snaps/2026-03-27-main-app.dump');
201
+ expect(fsMock.unlinkSync).toHaveBeenNthCalledWith(2, '/tmp/snaps/2026-03-28-main-app.dump');
202
+ expect(deleted).toEqual([
203
+ '/tmp/snaps/2026-03-27-main-app.dump',
204
+ '/tmp/snaps/2026-03-28-main-app.dump',
205
+ ]);
206
+ });
207
+
208
+ it('DB unreachable returns failure with reason', async () => {
209
+ exec.mockRejectedValueOnce(new Error('connect ECONNREFUSED'));
210
+
211
+ const result = await takeSnapshot({
212
+ dbType: 'postgresql',
213
+ connectionString: 'postgres://user:secret@localhost:5432/app_db',
214
+ snapshotDir: '/tmp/snaps',
215
+ gitRef: 'abc1234',
216
+ exec,
217
+ });
218
+
219
+ expect(result).toEqual({
220
+ success: false,
221
+ reason: 'connect ECONNREFUSED',
222
+ error: expect.any(Error),
223
+ });
224
+ });
225
+ });
@@ -23,28 +23,50 @@ const ROUTABLE_COMMANDS = [
23
23
  ];
24
24
 
25
25
  /**
26
- * Default routing: coding commands → codex, thinking commands → claude.
26
+ * Dispatch modes for provider execution.
27
27
  *
28
- * Coding: build, quick, autofix (single) + edge-cases, review (parallel)
29
- * Thinking: plan, discuss, docs, coverage, test, design, vision (single)
28
+ * - 'interactive': Provider needs write access (build, fix, implement).
29
+ * Dispatched via tmux session with --dangerously-bypass-approvals-and-sandbox.
30
+ * Codex exec sandbox blocks disk writes — interactive mode is required for coding.
30
31
  *
31
- * @type {Record<string, {models: string[], strategy: string}>}
32
+ * - 'exec': Provider only reads (review, analysis, coverage).
33
+ * Dispatched via `codex exec --full-auto` or `gemini -p`. Fast, no tmux needed.
34
+ *
35
+ * - 'inline': Runs in the current Claude session (discuss, plan, docs).
36
+ * No external dispatch.
37
+ */
38
+ const DISPATCH_MODES = {
39
+ interactive: 'interactive',
40
+ exec: 'exec',
41
+ inline: 'inline',
42
+ };
43
+
44
+ /**
45
+ * Default routing: coding commands → codex interactive, review → codex exec,
46
+ * thinking commands → claude inline.
47
+ *
48
+ * Coding (write): build, quick, autofix → codex interactive (tmux)
49
+ * Analysis (read): review, edge-cases → codex exec
50
+ * Thinking: plan, discuss, docs, coverage → claude inline
51
+ *
52
+ * @type {Record<string, {models: string[], strategy: string, mode: string}>}
32
53
  */
33
54
  const SHIPPED_DEFAULTS = {
34
- // Coding — codex
35
- 'build': { models: ['codex'], strategy: 'single' },
36
- 'quick': { models: ['codex'], strategy: 'single' },
37
- 'autofix': { models: ['codex'], strategy: 'single' },
38
- 'edge-cases': { models: ['codex'], strategy: 'parallel' },
39
- 'review': { models: ['codex', 'claude'], strategy: 'parallel' },
40
- // Thinking claude
41
- 'plan': { models: ['codex', 'claude'], strategy: 'parallel' },
42
- 'discuss': { models: ['claude'], strategy: 'single' },
43
- 'docs': { models: ['claude'], strategy: 'single' },
44
- 'coverage': { models: ['claude'], strategy: 'single' },
45
- 'test': { models: ['claude'], strategy: 'single' },
46
- 'design': { models: ['claude'], strategy: 'single' },
47
- 'vision': { models: ['claude'], strategy: 'single' },
55
+ // Coding — codex interactive (needs disk writes, tmux session)
56
+ 'build': { models: ['codex'], strategy: 'single', mode: 'interactive' },
57
+ 'quick': { models: ['codex'], strategy: 'single', mode: 'interactive' },
58
+ 'autofix': { models: ['codex'], strategy: 'single', mode: 'interactive' },
59
+ // Analysis codex exec (read-only, fast)
60
+ 'edge-cases': { models: ['codex'], strategy: 'parallel', mode: 'exec' },
61
+ 'review': { models: ['codex', 'claude'], strategy: 'parallel', mode: 'exec' },
62
+ // Thinking claude inline
63
+ 'plan': { models: ['codex', 'claude'], strategy: 'parallel', mode: 'inline' },
64
+ 'discuss': { models: ['claude'], strategy: 'single', mode: 'inline' },
65
+ 'docs': { models: ['claude'], strategy: 'single', mode: 'inline' },
66
+ 'coverage': { models: ['claude'], strategy: 'single', mode: 'exec' },
67
+ 'test': { models: ['claude'], strategy: 'single', mode: 'inline' },
68
+ 'design': { models: ['claude'], strategy: 'single', mode: 'inline' },
69
+ 'vision': { models: ['claude'], strategy: 'single', mode: 'inline' },
48
70
  };
49
71
 
50
72
  /**
@@ -107,9 +129,10 @@ function loadProjectOverride({ projectDir, fs }) {
107
129
  */
108
130
  function resolveRouting({ command, flagModel, projectDir = process.cwd(), homeDir = require('os').homedir(), fs = require('fs') }) {
109
131
  // Start with shipped defaults
110
- const defaultEntry = SHIPPED_DEFAULTS[command] || { models: ['claude'], strategy: 'single' };
132
+ const defaultEntry = SHIPPED_DEFAULTS[command] || { models: ['claude'], strategy: 'single', mode: 'inline' };
111
133
  let models = [...defaultEntry.models];
112
134
  let strategy = defaultEntry.strategy;
135
+ let mode = defaultEntry.mode || 'inline';
113
136
  let source = 'shipped-defaults';
114
137
  let providers;
115
138
  const warnings = [];
@@ -136,6 +159,9 @@ function resolveRouting({ command, flagModel, projectDir = process.cwd(), homeDi
136
159
  if (personalRouting.strategy) {
137
160
  strategy = personalRouting.strategy;
138
161
  }
162
+ if (personalRouting.mode) {
163
+ mode = personalRouting.mode;
164
+ }
139
165
  source = 'personal-config';
140
166
  }
141
167
  }
@@ -157,6 +183,9 @@ function resolveRouting({ command, flagModel, projectDir = process.cwd(), homeDi
157
183
  if (overrideEntry.strategy) {
158
184
  strategy = overrideEntry.strategy;
159
185
  }
186
+ if (overrideEntry.mode) {
187
+ mode = overrideEntry.mode;
188
+ }
160
189
  source = 'project-override';
161
190
  }
162
191
  }
@@ -168,7 +197,7 @@ function resolveRouting({ command, flagModel, projectDir = process.cwd(), homeDi
168
197
  source = 'flag-override';
169
198
  }
170
199
 
171
- const result = { models, strategy, source, warnings };
200
+ const result = { models, strategy, mode, source, warnings };
172
201
  if (providers) {
173
202
  result.providers = providers;
174
203
  }
@@ -181,4 +210,5 @@ module.exports = {
181
210
  loadProjectOverride,
182
211
  SHIPPED_DEFAULTS,
183
212
  ROUTABLE_COMMANDS,
213
+ DISPATCH_MODES,
184
214
  };
@@ -27,22 +27,25 @@ function mockFs(fileMap = {}) {
27
27
  describe('task-router-config', () => {
28
28
  // ── SHIPPED_DEFAULTS ──────────────────────────────────────────────
29
29
  describe('SHIPPED_DEFAULTS', () => {
30
- it('routes coding commands to codex', () => {
31
- expect(SHIPPED_DEFAULTS['build']).toEqual({ models: ['codex'], strategy: 'single' });
32
- expect(SHIPPED_DEFAULTS['quick']).toEqual({ models: ['codex'], strategy: 'single' });
33
- expect(SHIPPED_DEFAULTS['autofix']).toEqual({ models: ['codex'], strategy: 'single' });
34
- expect(SHIPPED_DEFAULTS['edge-cases']).toEqual({ models: ['codex'], strategy: 'parallel' });
35
- expect(SHIPPED_DEFAULTS['review']).toEqual({ models: ['codex', 'claude'], strategy: 'parallel' });
30
+ it('routes coding commands to codex interactive', () => {
31
+ expect(SHIPPED_DEFAULTS['build']).toEqual({ models: ['codex'], strategy: 'single', mode: 'interactive' });
32
+ expect(SHIPPED_DEFAULTS['quick']).toEqual({ models: ['codex'], strategy: 'single', mode: 'interactive' });
33
+ expect(SHIPPED_DEFAULTS['autofix']).toEqual({ models: ['codex'], strategy: 'single', mode: 'interactive' });
36
34
  });
37
35
 
38
- it('routes thinking commands to claude', () => {
39
- expect(SHIPPED_DEFAULTS['plan']).toEqual({ models: ['codex', 'claude'], strategy: 'parallel' });
40
- expect(SHIPPED_DEFAULTS['discuss']).toEqual({ models: ['claude'], strategy: 'single' });
41
- expect(SHIPPED_DEFAULTS['docs']).toEqual({ models: ['claude'], strategy: 'single' });
42
- expect(SHIPPED_DEFAULTS['coverage']).toEqual({ models: ['claude'], strategy: 'single' });
43
- expect(SHIPPED_DEFAULTS['test']).toEqual({ models: ['claude'], strategy: 'single' });
44
- expect(SHIPPED_DEFAULTS['design']).toEqual({ models: ['claude'], strategy: 'single' });
45
- expect(SHIPPED_DEFAULTS['vision']).toEqual({ models: ['claude'], strategy: 'single' });
36
+ it('routes analysis commands to exec mode', () => {
37
+ expect(SHIPPED_DEFAULTS['edge-cases']).toEqual({ models: ['codex'], strategy: 'parallel', mode: 'exec' });
38
+ expect(SHIPPED_DEFAULTS['review']).toEqual({ models: ['codex', 'claude'], strategy: 'parallel', mode: 'exec' });
39
+ expect(SHIPPED_DEFAULTS['coverage']).toEqual({ models: ['claude'], strategy: 'single', mode: 'exec' });
40
+ });
41
+
42
+ it('routes thinking commands to claude inline', () => {
43
+ expect(SHIPPED_DEFAULTS['plan']).toEqual({ models: ['codex', 'claude'], strategy: 'parallel', mode: 'inline' });
44
+ expect(SHIPPED_DEFAULTS['discuss']).toEqual({ models: ['claude'], strategy: 'single', mode: 'inline' });
45
+ expect(SHIPPED_DEFAULTS['docs']).toEqual({ models: ['claude'], strategy: 'single', mode: 'inline' });
46
+ expect(SHIPPED_DEFAULTS['test']).toEqual({ models: ['claude'], strategy: 'single', mode: 'inline' });
47
+ expect(SHIPPED_DEFAULTS['design']).toEqual({ models: ['claude'], strategy: 'single', mode: 'inline' });
48
+ expect(SHIPPED_DEFAULTS['vision']).toEqual({ models: ['claude'], strategy: 'single', mode: 'inline' });
46
49
  });
47
50
 
48
51
  it('has an entry for every routable command', () => {
@@ -156,6 +159,7 @@ describe('task-router-config', () => {
156
159
  expect(result).toEqual({
157
160
  models: ['codex'],
158
161
  strategy: 'single',
162
+ mode: 'interactive',
159
163
  source: 'shipped-defaults',
160
164
  warnings: [],
161
165
  });
@@ -173,6 +177,7 @@ describe('task-router-config', () => {
173
177
  expect(result).toEqual({
174
178
  models: ['claude'],
175
179
  strategy: 'single',
180
+ mode: 'inline',
176
181
  source: 'shipped-defaults',
177
182
  warnings: [],
178
183
  });
@@ -198,6 +203,7 @@ describe('task-router-config', () => {
198
203
  expect(result).toEqual({
199
204
  models: ['codex'],
200
205
  strategy: 'single',
206
+ mode: 'interactive',
201
207
  source: 'personal-config',
202
208
  warnings: [],
203
209
  });
@@ -229,6 +235,7 @@ describe('task-router-config', () => {
229
235
  expect(result).toEqual({
230
236
  models: ['local'],
231
237
  strategy: 'single',
238
+ mode: 'interactive',
232
239
  source: 'project-override',
233
240
  warnings: [],
234
241
  });
@@ -261,6 +268,7 @@ describe('task-router-config', () => {
261
268
  expect(result).toEqual({
262
269
  models: ['gemini'],
263
270
  strategy: 'single',
271
+ mode: 'interactive',
264
272
  source: 'flag-override',
265
273
  warnings: [],
266
274
  });
@@ -278,6 +286,7 @@ describe('task-router-config', () => {
278
286
  expect(result).toEqual({
279
287
  models: ['claude'],
280
288
  strategy: 'single',
289
+ mode: 'inline',
281
290
  source: 'shipped-defaults',
282
291
  warnings: [],
283
292
  });
@@ -303,11 +312,12 @@ describe('task-router-config', () => {
303
312
  expect(reviewResult).toEqual({
304
313
  models: ['claude', 'codex'],
305
314
  strategy: 'parallel',
315
+ mode: 'exec',
306
316
  source: 'personal-config',
307
317
  warnings: [],
308
318
  });
309
319
 
310
- // 'build' should still use its shipped default (codex/single)
320
+ // 'build' should still use its shipped default (codex/single/interactive)
311
321
  const buildResult = resolveRouting({
312
322
  command: 'build',
313
323
  projectDir: '/project',
@@ -317,6 +327,7 @@ describe('task-router-config', () => {
317
327
  expect(buildResult).toEqual({
318
328
  models: ['codex'],
319
329
  strategy: 'single',
330
+ mode: 'interactive',
320
331
  source: 'shipped-defaults',
321
332
  warnings: [],
322
333
  });
@@ -388,6 +399,7 @@ describe('task-router-config', () => {
388
399
  expect(result).toEqual({
389
400
  models: ['local'],
390
401
  strategy: 'single',
402
+ mode: 'interactive',
391
403
  source: 'project-override',
392
404
  warnings: [],
393
405
  });
@@ -428,6 +440,7 @@ describe('task-router-config', () => {
428
440
  expect(result).toEqual({
429
441
  models: ['codex'],
430
442
  strategy: 'single',
443
+ mode: 'interactive',
431
444
  source: 'shipped-defaults',
432
445
  warnings: [expect.stringContaining('/home/user/.tlc/config.json')],
433
446
  });
@@ -448,6 +461,7 @@ describe('task-router-config', () => {
448
461
  expect(result).toEqual({
449
462
  models: ['codex'],
450
463
  strategy: 'single',
464
+ mode: 'interactive',
451
465
  source: 'shipped-defaults',
452
466
  warnings: [expect.stringContaining('/project/.tlc.json')],
453
467
  });