tlc-claude-code 2.7.0 → 2.9.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 (30) hide show
  1. package/.claude/commands/tlc/audit.md +12 -0
  2. package/.claude/commands/tlc/autofix.md +12 -0
  3. package/.claude/commands/tlc/build.md +35 -4
  4. package/.claude/commands/tlc/cleanup.md +13 -1
  5. package/.claude/commands/tlc/coverage.md +12 -0
  6. package/.claude/commands/tlc/discuss.md +12 -0
  7. package/.claude/commands/tlc/docs.md +13 -1
  8. package/.claude/commands/tlc/edge-cases.md +13 -1
  9. package/.claude/commands/tlc/plan.md +32 -6
  10. package/.claude/commands/tlc/preflight.md +12 -0
  11. package/.claude/commands/tlc/progress.md +41 -15
  12. package/.claude/commands/tlc/refactor.md +12 -0
  13. package/.claude/commands/tlc/review-pr.md +32 -11
  14. package/.claude/commands/tlc/review.md +12 -0
  15. package/.claude/commands/tlc/security.md +13 -1
  16. package/.claude/commands/tlc/status.md +42 -3
  17. package/.claude/commands/tlc/tlc.md +32 -16
  18. package/.claude/commands/tlc/verify.md +12 -0
  19. package/.claude/commands/tlc/watchci.md +12 -0
  20. package/package.json +1 -1
  21. package/scripts/renumber-phases.js +283 -0
  22. package/scripts/renumber-phases.test.js +305 -0
  23. package/server/lib/orchestration/completion-checker.js +52 -2
  24. package/server/lib/orchestration/completion-checker.test.js +64 -0
  25. package/server/lib/orchestration/session-status.js +28 -4
  26. package/server/lib/orchestration/session-status.test.js +44 -1
  27. package/server/lib/orchestration/skill-dispatcher.js +270 -0
  28. package/server/lib/orchestration/skill-dispatcher.test.js +449 -0
  29. package/server/lib/workspace-manifest.js +138 -0
  30. package/server/lib/workspace-manifest.test.js +179 -0
@@ -198,6 +198,14 @@ If no roadmap exists:
198
198
 
199
199
  ### Step 2: Find The Active Phase
200
200
 
201
+ Before matching phase artifacts, determine the current repo prefix:
202
+
203
+ 1. Try workspace-aware discovery first:
204
+ - Load `/workspace/Tools/.tlc-workspace.json` when available, or discover the nearest manifest via `server/lib/workspace-manifest.js`
205
+ - Use the current repo path plus the manifest to resolve the repo prefix (`TLC`, `CORE`, `SA`, etc.)
206
+ 2. If no manifest is available, scan `.planning/phases/` for existing prefixed artifacts such as `TLC-108-PLAN.md` and infer the current repo prefix from those filenames
207
+ 3. If neither manifest nor prefixed files are available, fall back to legacy unprefixed detection
208
+
201
209
  From the roadmap, identify:
202
210
 
203
211
  - The current phase marked as in progress, current, or active
@@ -206,18 +214,22 @@ From the roadmap, identify:
206
214
 
207
215
  Extract:
208
216
 
209
- - Phase number
217
+ - Phase number / phase ID
210
218
  - Phase name
211
219
  - Task count if the roadmap or plan exposes it
212
220
  - Completed task count if available
213
221
 
214
222
  ### Step 3: Check Artifacts In The Same Pass
215
223
 
216
- For the active phase `N`, check:
224
+ For the active phase, build the preferred prefixed ID `{PREFIX}-{N}` when a prefix is known. Check artifacts in this order:
217
225
 
218
- - Discussion: `.planning/phases/{N}-DISCUSSION.md`
219
- - Plan: `.planning/phases/{N}-PLAN.md`
220
- - Verification: `.planning/phases/{N}-VERIFIED.md`
226
+ - Discussion: `.planning/phases/{PREFIX}-{N}-DISCUSSION.md`, then legacy `.planning/phases/{N}-DISCUSSION.md`
227
+ - Plan: `.planning/phases/{PREFIX}-{N}-PLAN.md`, then legacy `.planning/phases/{N}-PLAN.md`
228
+ - Verification: `.planning/phases/{PREFIX}-{N}-VERIFIED.md`, then legacy `.planning/phases/{N}-VERIFIED.md`
229
+
230
+ Phase file pattern matching must accept both:
231
+ - Prefixed: `{PREFIX}-{N}-PLAN.md`, `{PREFIX}-{N}-DISCUSSION.md`, `{PREFIX}-{N}-VERIFIED.md`
232
+ - Legacy: `{N}-PLAN.md`, `{N}-DISCUSSION.md`, `{N}-VERIFIED.md`
221
233
 
222
234
  If the plan exists, inspect it to determine build state:
223
235
 
@@ -228,16 +240,20 @@ If the plan exists, inspect it to determine build state:
228
240
  Use these state rules:
229
241
 
230
242
  1. No discussion file:
231
- Run `/tlc:discuss {N}`
243
+ Run `/tlc:discuss {PHASE_ID}`
232
244
  2. Discussion exists, no plan:
233
- Run `/tlc:plan {N}`
245
+ Run `/tlc:plan {PHASE_ID}`
234
246
  3. Plan exists and phase is not fully built:
235
- Run `/tlc:build {N}`
247
+ Run `/tlc:build {PHASE_ID}`
236
248
  4. Build complete, no verification file:
237
- Run `/tlc:verify {N}`
249
+ Run `/tlc:verify {PHASE_ID}`
238
250
  5. Verification exists or all phases are complete:
239
251
  Suggest release, but do not auto-run it
240
252
 
253
+ Backward compatibility:
254
+ - If prefixed artifacts exist, prefer them everywhere in status output and auto-run commands
255
+ - If no prefixed artifacts exist and no workspace manifest is found, continue using legacy unprefixed phase IDs
256
+
241
257
  ## Output Format
242
258
 
243
259
  Normal output must stay within 3-5 lines.
@@ -245,9 +261,9 @@ Normal output must stay within 3-5 lines.
245
261
  Use this pattern:
246
262
 
247
263
  ```text
248
- TLC v{version} | Phase {N}: {Name} | {done}/{total} tasks done
264
+ TLC v{version} | Phase {PHASE_ID}: {Name} | {done}/{total} tasks done
249
265
  Next: {action summary}
250
- Running /tlc:{command} {N}...
266
+ Running /tlc:{command} {PHASE_ID}...
251
267
  ```
252
268
 
253
269
  If there is nothing safe to auto-run:
@@ -288,7 +304,7 @@ Auto-run:
288
304
  Auto-run:
289
305
 
290
306
  ```text
291
- /tlc:plan {N}
307
+ /tlc:plan {PHASE_ID}
292
308
  ```
293
309
 
294
310
  ### Planned, Not Built
@@ -296,7 +312,7 @@ Auto-run:
296
312
  Auto-run:
297
313
 
298
314
  ```text
299
- /tlc:build {N}
315
+ /tlc:build {PHASE_ID}
300
316
  ```
301
317
 
302
318
  Use the next incomplete task from the plan in the status line when available.
@@ -306,7 +322,7 @@ Use the next incomplete task from the plan in the status line when available.
306
322
  Auto-run:
307
323
 
308
324
  ```text
309
- /tlc:verify {N}
325
+ /tlc:verify {PHASE_ID}
310
326
  ```
311
327
 
312
328
  ### All Complete
@@ -322,9 +338,9 @@ Run /tlc:complete or /tlc:new-milestone
322
338
  ## Example
323
339
 
324
340
  ```text
325
- TLC v2.4.2 | Phase 8: Auth System | 3/5 tasks done
341
+ TLC v2.4.2 | Phase TLC-8: Auth System | 3/5 tasks done
326
342
  Next: build task 4 (JWT middleware)
327
- Running /tlc:build 8...
343
+ Running /tlc:build TLC-8...
328
344
  ```
329
345
 
330
346
  ## Summary
@@ -17,6 +17,18 @@ Verify the phase automatically from its plan acceptance criteria, then leave onl
17
17
 
18
18
  If no phase number, auto-detect current phase.
19
19
 
20
+ ## Background Dispatch
21
+
22
+ When the orchestrator is available (`curl -sf http://localhost:3100/health`), dispatch this command to a background agent session instead of running inline.
23
+
24
+ 1. Check orchestrator health
25
+ 2. If available: package the full skill prompt with project context (branch, changed files, current phase) and dispatch via `skill-dispatcher.dispatch({ skill: 'verify', prompt, project })`
26
+ 3. Report to user: "Dispatched verify to background session ses_XXXX. Results will appear when complete."
27
+ 4. When session completes, capture result via `skill-dispatcher.captureResult()` and display summary
28
+ 5. If orchestrator unavailable, fall back to inline execution (existing behavior below)
29
+
30
+ Override: `--inline` flag forces inline execution even when orchestrator is available.
31
+
20
32
  ## Process
21
33
 
22
34
  ### Step 1: Load the Phase Plan
@@ -19,6 +19,18 @@ This is the "I pushed, now babysit CI for me" command.
19
19
  /tlc:watchci 3 # max 3 fix attempts (default: 5)
20
20
  ```
21
21
 
22
+ ## Background Dispatch
23
+
24
+ When the orchestrator is available (`curl -sf http://localhost:3100/health`), dispatch this command to a background agent session instead of running inline.
25
+
26
+ 1. Check orchestrator health
27
+ 2. If available: package the full skill prompt with project context (branch, changed files, current phase) and dispatch via `skill-dispatcher.dispatch({ skill: 'watchci', prompt, project })`
28
+ 3. Report to user: "Dispatched watchci to background session ses_XXXX. Results will appear when complete."
29
+ 4. When session completes, capture result via `skill-dispatcher.captureResult()` and display summary
30
+ 5. If orchestrator unavailable, fall back to inline execution (existing behavior below)
31
+
32
+ Override: `--inline` flag forces inline execution even when orchestrator is available.
33
+
22
34
  ## Process
23
35
 
24
36
  ### Step 1: Find the Active Run
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tlc-claude-code",
3
- "version": "2.7.0",
3
+ "version": "2.9.0",
4
4
  "description": "TLC - Test Led Coding for Claude Code",
5
5
  "bin": {
6
6
  "tlc-claude-code": "./bin/install.js",
@@ -0,0 +1,283 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const ALLOWED_SUFFIXES = new Set([
5
+ 'PLAN',
6
+ 'DISCUSSION',
7
+ 'TESTS',
8
+ 'VERIFIED',
9
+ 'TEST-PLAN',
10
+ 'ARCHITECTURE',
11
+ 'COMPLETION-PLAN',
12
+ 'NOTE',
13
+ 'AUDIT-REPORT',
14
+ 'RESEARCH',
15
+ 'DISCUSSION-addendum',
16
+ ]);
17
+
18
+ const PHASE_FILE_PATTERN = /^([A-Za-z0-9]+)-([A-Za-z-]+)\.md(\.superseded)?$/;
19
+
20
+ function printUsage(stream = process.stderr) {
21
+ stream.write(
22
+ 'Usage: node scripts/renumber-phases.js --repo-path <path> --prefix <PREFIX> [--dry-run]\n'
23
+ );
24
+ }
25
+
26
+ function parseArgs(argv) {
27
+ const options = {
28
+ dryRun: false,
29
+ };
30
+
31
+ for (let index = 0; index < argv.length; index += 1) {
32
+ const arg = argv[index];
33
+
34
+ if (arg === '--dry-run') {
35
+ options.dryRun = true;
36
+ continue;
37
+ }
38
+
39
+ if (arg === '--repo-path' || arg === '--prefix') {
40
+ const value = argv[index + 1];
41
+ if (!value || value.startsWith('--')) {
42
+ throw new Error(`Missing value for ${arg}`);
43
+ }
44
+
45
+ if (arg === '--repo-path') {
46
+ options.repoPath = value;
47
+ } else {
48
+ options.prefix = value;
49
+ }
50
+
51
+ index += 1;
52
+ continue;
53
+ }
54
+
55
+ throw new Error(`Unknown argument: ${arg}`);
56
+ }
57
+
58
+ if (!options.repoPath || !options.prefix) {
59
+ throw new Error('Missing required arguments');
60
+ }
61
+
62
+ return options;
63
+ }
64
+
65
+ function escapeRegExp(value) {
66
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
67
+ }
68
+
69
+ function createPhaseReferenceRegex(prefix) {
70
+ return new RegExp(`\\bPhase\\s+(${escapeRegExp(prefix)}-)?(\\d+[A-Za-z]?)\\b`, 'g');
71
+ }
72
+
73
+ function updatePhaseReferences(content, prefix) {
74
+ const regex = createPhaseReferenceRegex(prefix);
75
+ let replacements = 0;
76
+
77
+ const updated = content.replace(regex, (match, existingPrefix, phaseId) => {
78
+ if (existingPrefix) {
79
+ return match;
80
+ }
81
+
82
+ replacements += 1;
83
+ return `Phase ${prefix}-${phaseId}`;
84
+ });
85
+
86
+ return {
87
+ content: updated,
88
+ replacements,
89
+ };
90
+ }
91
+
92
+ function isEligiblePhaseFile(name, prefix) {
93
+ const match = name.match(PHASE_FILE_PATTERN);
94
+ if (!match) {
95
+ return null;
96
+ }
97
+
98
+ const [, phaseId, suffix, supersededExtension = ''] = match;
99
+
100
+ if (phaseId.startsWith(`${prefix}-`)) {
101
+ return null;
102
+ }
103
+
104
+ if (!/^\d+[A-Za-z]?$/.test(phaseId)) {
105
+ return null;
106
+ }
107
+
108
+ if (!ALLOWED_SUFFIXES.has(suffix)) {
109
+ return null;
110
+ }
111
+
112
+ return {
113
+ phaseId,
114
+ suffix,
115
+ supersededExtension,
116
+ };
117
+ }
118
+
119
+ function collectOperations(repoPath, prefix) {
120
+ const phasesDir = path.join(repoPath, '.planning', 'phases');
121
+ if (!fs.existsSync(phasesDir) || !fs.statSync(phasesDir).isDirectory()) {
122
+ throw new Error(`Missing phases directory: ${phasesDir}`);
123
+ }
124
+
125
+ const roadmapPath = path.join(repoPath, '.planning', 'ROADMAP.md');
126
+ const operations = [];
127
+ const errors = [];
128
+
129
+ for (const entry of fs.readdirSync(phasesDir, { withFileTypes: true })) {
130
+ if (!entry.isFile()) {
131
+ continue;
132
+ }
133
+
134
+ const fileInfo = isEligiblePhaseFile(entry.name, prefix);
135
+ if (!fileInfo) {
136
+ continue;
137
+ }
138
+
139
+ const nextName = `${prefix}-${fileInfo.phaseId}-${fileInfo.suffix}.md${fileInfo.supersededExtension}`;
140
+ operations.push({
141
+ type: 'rename-file',
142
+ from: path.join(phasesDir, entry.name),
143
+ to: path.join(phasesDir, nextName),
144
+ phaseId: fileInfo.phaseId,
145
+ });
146
+ }
147
+
148
+ if (fs.existsSync(roadmapPath)) {
149
+ const roadmapContent = fs.readFileSync(roadmapPath, 'utf8');
150
+ const updatedRoadmap = updatePhaseReferences(roadmapContent, prefix);
151
+
152
+ if (updatedRoadmap.replacements > 0) {
153
+ operations.push({
154
+ type: 'update-file',
155
+ target: roadmapPath,
156
+ nextContent: updatedRoadmap.content,
157
+ replacements: updatedRoadmap.replacements,
158
+ });
159
+ }
160
+ }
161
+
162
+ for (const renameOperation of operations.filter((operation) => operation.type === 'rename-file')) {
163
+ const content = fs.readFileSync(renameOperation.from, 'utf8');
164
+ const updated = updatePhaseReferences(content, prefix);
165
+
166
+ if (updated.replacements > 0) {
167
+ operations.push({
168
+ type: 'update-renamed-file',
169
+ target: renameOperation.to,
170
+ sourcePath: renameOperation.from,
171
+ nextContent: updated.content,
172
+ replacements: updated.replacements,
173
+ });
174
+ }
175
+ }
176
+
177
+ for (const renameOperation of operations.filter((operation) => operation.type === 'rename-file')) {
178
+ if (!fs.existsSync(renameOperation.to)) {
179
+ continue;
180
+ }
181
+
182
+ errors.push(
183
+ `Cannot rename ${path.basename(renameOperation.from)} because ${path.basename(renameOperation.to)} already exists`
184
+ );
185
+ }
186
+
187
+ return {
188
+ operations,
189
+ errors,
190
+ };
191
+ }
192
+
193
+ function applyOperations(operations, dryRun) {
194
+ const summary = {
195
+ renamed: 0,
196
+ updated: 0,
197
+ };
198
+
199
+ for (const operation of operations) {
200
+ if (operation.type === 'rename-file') {
201
+ if (dryRun) {
202
+ console.log(`DRY-RUN rename ${operation.from} -> ${operation.to}`);
203
+ } else {
204
+ fs.renameSync(operation.from, operation.to);
205
+ }
206
+
207
+ summary.renamed += 1;
208
+ continue;
209
+ }
210
+
211
+ if (operation.type === 'update-file' || operation.type === 'update-renamed-file') {
212
+ if (dryRun) {
213
+ console.log(`DRY-RUN update ${operation.target} (${operation.replacements} references)`);
214
+ } else {
215
+ fs.writeFileSync(operation.target, operation.nextContent);
216
+ }
217
+
218
+ summary.updated += operation.replacements;
219
+ }
220
+ }
221
+
222
+ return summary;
223
+ }
224
+
225
+ function renumberPhases({ repoPath, prefix, dryRun = false }) {
226
+ const resolvedRepoPath = path.resolve(repoPath);
227
+ const trimmedPrefix = prefix.trim();
228
+
229
+ if (!trimmedPrefix) {
230
+ throw new Error('Prefix must not be empty');
231
+ }
232
+
233
+ const { operations, errors } = collectOperations(resolvedRepoPath, trimmedPrefix);
234
+ const summary = applyOperations(operations, dryRun);
235
+
236
+ for (const error of errors) {
237
+ console.error(error);
238
+ }
239
+
240
+ const finalSummary = {
241
+ renamed: summary.renamed,
242
+ updated: summary.updated,
243
+ errors: errors.length,
244
+ };
245
+
246
+ console.log(
247
+ `Renamed: ${finalSummary.renamed} files, Updated: ${finalSummary.updated} references, Errors: ${finalSummary.errors}`
248
+ );
249
+
250
+ return finalSummary;
251
+ }
252
+
253
+ function main(argv = process.argv.slice(2)) {
254
+ try {
255
+ const options = parseArgs(argv);
256
+ renumberPhases(options);
257
+ return 0;
258
+ } catch (error) {
259
+ const message = error instanceof Error ? error.message : String(error);
260
+
261
+ if (message.startsWith('Missing required arguments') || message.startsWith('Missing value for')) {
262
+ printUsage(process.stderr);
263
+ }
264
+
265
+ console.error(message);
266
+ return 1;
267
+ }
268
+ }
269
+
270
+ if (require.main === module) {
271
+ process.exit(main());
272
+ }
273
+
274
+ module.exports = {
275
+ ALLOWED_SUFFIXES,
276
+ PHASE_FILE_PATTERN,
277
+ collectOperations,
278
+ main,
279
+ parseArgs,
280
+ printUsage,
281
+ renumberPhases,
282
+ updatePhaseReferences,
283
+ };