tlc-claude-code 2.6.1 → 2.8.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.
- package/.claude/commands/tlc/audit.md +18 -1
- package/.claude/commands/tlc/autofix.md +17 -1
- package/.claude/commands/tlc/build.md +37 -2
- package/.claude/commands/tlc/coverage.md +16 -0
- package/.claude/commands/tlc/discuss.md +15 -0
- package/.claude/commands/tlc/init.md +19 -0
- package/.claude/commands/tlc/plan.md +35 -6
- package/.claude/commands/tlc/preflight.md +16 -0
- package/.claude/commands/tlc/progress.md +41 -15
- package/.claude/commands/tlc/refactor.md +17 -1
- package/.claude/commands/tlc/review-pr.md +19 -10
- package/.claude/commands/tlc/review.md +16 -0
- package/.claude/commands/tlc/status.md +23 -3
- package/.claude/commands/tlc/tlc.md +32 -16
- package/.claude/hooks/tlc-session-init.sh +24 -0
- package/CLAUDE.md +14 -0
- package/bin/install.js +66 -0
- package/package.json +1 -1
- package/scripts/renumber-phases.js +283 -0
- package/scripts/renumber-phases.test.js +305 -0
- package/server/lib/workspace-manifest.js +138 -0
- package/server/lib/workspace-manifest.test.js +179 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const MANIFEST_FILENAME = '.tlc-workspace.json';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Validate the parsed workspace manifest structure.
|
|
8
|
+
*
|
|
9
|
+
* @param {object} manifest - Parsed manifest JSON.
|
|
10
|
+
* @returns {object} The validated manifest object.
|
|
11
|
+
*/
|
|
12
|
+
function validateManifest(manifest) {
|
|
13
|
+
if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) {
|
|
14
|
+
throw new Error(`Invalid workspace manifest: expected object in ${MANIFEST_FILENAME}`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (!manifest.repos || typeof manifest.repos !== 'object' || Array.isArray(manifest.repos)) {
|
|
18
|
+
throw new Error(`Invalid workspace manifest: missing repos key in ${MANIFEST_FILENAME}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return manifest;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Discover and parse the nearest workspace manifest by walking up the tree.
|
|
26
|
+
*
|
|
27
|
+
* @param {string} startDir - Directory to start searching from.
|
|
28
|
+
* @returns {object|null} Parsed workspace manifest, or null when not found.
|
|
29
|
+
*/
|
|
30
|
+
function discoverWorkspace(startDir) {
|
|
31
|
+
let currentDir = path.resolve(startDir);
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
if (!fs.statSync(currentDir).isDirectory()) {
|
|
35
|
+
currentDir = path.dirname(currentDir);
|
|
36
|
+
}
|
|
37
|
+
} catch {
|
|
38
|
+
currentDir = path.dirname(currentDir);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
while (true) {
|
|
42
|
+
const manifestPath = path.join(currentDir, MANIFEST_FILENAME);
|
|
43
|
+
|
|
44
|
+
if (fs.existsSync(manifestPath)) {
|
|
45
|
+
let parsed;
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
parsed = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
49
|
+
} catch (error) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`Failed to parse workspace manifest at ${manifestPath}: ${error.message}`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return validateManifest(parsed);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const parentDir = path.dirname(currentDir);
|
|
59
|
+
if (parentDir === currentDir) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
currentDir = parentDir;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Resolve a repository definition by prefix.
|
|
68
|
+
*
|
|
69
|
+
* @param {object} manifest - Parsed workspace manifest.
|
|
70
|
+
* @param {string} prefix - Repo prefix to look up.
|
|
71
|
+
* @returns {{ path: string, prefix: string, role: string }|null} Repo metadata or null.
|
|
72
|
+
*/
|
|
73
|
+
function resolveRepo(manifest, prefix) {
|
|
74
|
+
if (!manifest || !manifest.repos || !prefix) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
for (const repoConfig of Object.values(manifest.repos)) {
|
|
79
|
+
if (repoConfig && repoConfig.prefix === prefix) {
|
|
80
|
+
return {
|
|
81
|
+
path: repoConfig.path,
|
|
82
|
+
prefix: repoConfig.prefix,
|
|
83
|
+
role: repoConfig.role,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Resolve a phase identifier into repo, prefix, and phase components.
|
|
93
|
+
*
|
|
94
|
+
* @param {object} manifest - Parsed workspace manifest.
|
|
95
|
+
* @param {string} phaseId - Phase identifier such as "CORE-2" or "108".
|
|
96
|
+
* @param {string} currentRepoPrefix - Prefix of the current repo for unprefixed phase IDs.
|
|
97
|
+
* @returns {{ repo: string, prefix: string, phase: string }|null} Resolved phase metadata or null.
|
|
98
|
+
*/
|
|
99
|
+
function resolvePhase(manifest, phaseId, currentRepoPrefix) {
|
|
100
|
+
if (!manifest || !manifest.repos || typeof phaseId !== 'string' || phaseId.length === 0) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const prefixedMatch = /^([A-Z]+)-(.+)$/.exec(phaseId);
|
|
105
|
+
if (prefixedMatch) {
|
|
106
|
+
const [, prefix, phase] = prefixedMatch;
|
|
107
|
+
|
|
108
|
+
for (const [repo, repoConfig] of Object.entries(manifest.repos)) {
|
|
109
|
+
if (repoConfig && repoConfig.prefix === prefix) {
|
|
110
|
+
return { repo, prefix, phase };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!currentRepoPrefix) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for (const [repo, repoConfig] of Object.entries(manifest.repos)) {
|
|
122
|
+
if (repoConfig && repoConfig.prefix === currentRepoPrefix) {
|
|
123
|
+
return {
|
|
124
|
+
repo,
|
|
125
|
+
prefix: currentRepoPrefix,
|
|
126
|
+
phase: phaseId,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = {
|
|
135
|
+
discoverWorkspace,
|
|
136
|
+
resolveRepo,
|
|
137
|
+
resolvePhase,
|
|
138
|
+
};
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
discoverWorkspace,
|
|
8
|
+
resolveRepo,
|
|
9
|
+
resolvePhase,
|
|
10
|
+
} = await import('./workspace-manifest.js');
|
|
11
|
+
|
|
12
|
+
function makeTmpDir() {
|
|
13
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'workspace-manifest-'));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function writeJson(filePath, value) {
|
|
17
|
+
fs.writeFileSync(filePath, JSON.stringify(value, null, 2));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function removeDir(dirPath) {
|
|
21
|
+
fs.rmSync(dirPath, { recursive: true, force: true });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function createManifest(overrides = {}) {
|
|
25
|
+
return {
|
|
26
|
+
workspace: 'tlc-platform',
|
|
27
|
+
repos: {
|
|
28
|
+
TLC: { path: 'TLC', prefix: 'TLC', role: 'Runtime library' },
|
|
29
|
+
'tlc-core': { path: 'tlc-core', prefix: 'CORE', role: 'Execution substrate' },
|
|
30
|
+
'tlc-standalone': {
|
|
31
|
+
path: 'tlc-standalone',
|
|
32
|
+
prefix: 'SA',
|
|
33
|
+
role: 'Provider-agnostic CLI',
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
orchestrator: { url: 'http://localhost:3100', provider: 'tlc-core' },
|
|
37
|
+
...overrides,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe('workspace-manifest', () => {
|
|
42
|
+
const tempDirs = [];
|
|
43
|
+
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
while (tempDirs.length > 0) {
|
|
46
|
+
removeDir(tempDirs.pop());
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('discoverWorkspace from subdirectory finds manifest at parent', () => {
|
|
51
|
+
const rootDir = makeTmpDir();
|
|
52
|
+
tempDirs.push(rootDir);
|
|
53
|
+
|
|
54
|
+
const nestedDir = path.join(rootDir, 'TLC', 'server', 'lib');
|
|
55
|
+
fs.mkdirSync(nestedDir, { recursive: true });
|
|
56
|
+
writeJson(path.join(rootDir, '.tlc-workspace.json'), createManifest());
|
|
57
|
+
|
|
58
|
+
const manifest = discoverWorkspace(nestedDir);
|
|
59
|
+
|
|
60
|
+
expect(manifest).not.toBeNull();
|
|
61
|
+
expect(manifest.workspace).toBe('tlc-platform');
|
|
62
|
+
expect(manifest.repos.TLC.path).toBe('TLC');
|
|
63
|
+
expect(manifest.repos['tlc-core'].prefix).toBe('CORE');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('resolveRepo("CORE") returns tlc-core metadata', () => {
|
|
67
|
+
const manifest = createManifest();
|
|
68
|
+
|
|
69
|
+
expect(resolveRepo(manifest, 'CORE')).toEqual({
|
|
70
|
+
path: 'tlc-core',
|
|
71
|
+
prefix: 'CORE',
|
|
72
|
+
role: 'Execution substrate',
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('resolvePhase("CORE-2") parses correctly', () => {
|
|
77
|
+
const manifest = createManifest();
|
|
78
|
+
|
|
79
|
+
expect(resolvePhase(manifest, 'CORE-2', 'TLC')).toEqual({
|
|
80
|
+
repo: 'tlc-core',
|
|
81
|
+
prefix: 'CORE',
|
|
82
|
+
phase: '2',
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('resolvePhase("108", "TLC") returns TLC-108', () => {
|
|
87
|
+
const manifest = createManifest();
|
|
88
|
+
|
|
89
|
+
expect(resolvePhase(manifest, '108', 'TLC')).toEqual({
|
|
90
|
+
repo: 'TLC',
|
|
91
|
+
prefix: 'TLC',
|
|
92
|
+
phase: '108',
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('discoverWorkspace from /tmp returns null when no manifest exists in tree', () => {
|
|
97
|
+
const rootDir = makeTmpDir();
|
|
98
|
+
tempDirs.push(rootDir);
|
|
99
|
+
|
|
100
|
+
const nestedDir = path.join(rootDir, 'lonely', 'child');
|
|
101
|
+
fs.mkdirSync(nestedDir, { recursive: true });
|
|
102
|
+
|
|
103
|
+
expect(discoverWorkspace(nestedDir)).toBeNull();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('resolveRepo("UNKNOWN") returns null', () => {
|
|
107
|
+
const manifest = createManifest();
|
|
108
|
+
|
|
109
|
+
expect(resolveRepo(manifest, 'UNKNOWN')).toBeNull();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('resolvePhase("NOPE-1") returns null', () => {
|
|
113
|
+
const manifest = createManifest();
|
|
114
|
+
|
|
115
|
+
expect(resolvePhase(manifest, 'NOPE-1', 'TLC')).toBeNull();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('invalid JSON throws descriptive error', () => {
|
|
119
|
+
const rootDir = makeTmpDir();
|
|
120
|
+
tempDirs.push(rootDir);
|
|
121
|
+
|
|
122
|
+
fs.writeFileSync(
|
|
123
|
+
path.join(rootDir, '.tlc-workspace.json'),
|
|
124
|
+
'{"workspace":"tlc-platform","repos":'
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
expect(() => discoverWorkspace(rootDir)).toThrow(/parse workspace manifest/i);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('manifest missing repos key throws descriptive error', () => {
|
|
131
|
+
const rootDir = makeTmpDir();
|
|
132
|
+
tempDirs.push(rootDir);
|
|
133
|
+
|
|
134
|
+
writeJson(path.join(rootDir, '.tlc-workspace.json'), {
|
|
135
|
+
workspace: 'tlc-platform',
|
|
136
|
+
orchestrator: { url: 'http://localhost:3100', provider: 'tlc-core' },
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
expect(() => discoverWorkspace(rootDir)).toThrow(/missing repos key/i);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('discoverWorkspace accepts a file path and searches from its parent directory', () => {
|
|
143
|
+
const rootDir = makeTmpDir();
|
|
144
|
+
tempDirs.push(rootDir);
|
|
145
|
+
|
|
146
|
+
const repoDir = path.join(rootDir, 'TLC');
|
|
147
|
+
const filePath = path.join(repoDir, 'server', 'lib', 'workspace-manifest.js');
|
|
148
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
149
|
+
fs.writeFileSync(filePath, '// fixture');
|
|
150
|
+
writeJson(path.join(rootDir, '.tlc-workspace.json'), createManifest());
|
|
151
|
+
|
|
152
|
+
const manifest = discoverWorkspace(filePath);
|
|
153
|
+
|
|
154
|
+
expect(manifest.repos['tlc-standalone']).toEqual({
|
|
155
|
+
path: 'tlc-standalone',
|
|
156
|
+
prefix: 'SA',
|
|
157
|
+
role: 'Provider-agnostic CLI',
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('resolvePhase returns null for unprefixed phase when current repo prefix is unknown', () => {
|
|
162
|
+
const manifest = createManifest();
|
|
163
|
+
|
|
164
|
+
expect(resolvePhase(manifest, '108', 'NOPE')).toBeNull();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('resolvePhase returns null when current repo prefix is not provided for unprefixed phase', () => {
|
|
168
|
+
const manifest = createManifest();
|
|
169
|
+
|
|
170
|
+
expect(resolvePhase(manifest, '108')).toBeNull();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('resolveRepo returns null when prefix is missing', () => {
|
|
174
|
+
const manifest = createManifest();
|
|
175
|
+
|
|
176
|
+
expect(resolveRepo(manifest)).toBeNull();
|
|
177
|
+
expect(resolveRepo(manifest, '')).toBeNull();
|
|
178
|
+
});
|
|
179
|
+
});
|