thachvd-kit 1.0.36 → 1.0.38
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/LICENSE +21 -21
- package/README.md +240 -0
- package/THIRD_PARTY_NOTICES.md +49 -0
- package/bin/cli.js +80 -24
- package/bin/config.js +164 -0
- package/bin/entry.js +11 -1
- package/bin/native-skills.js +183 -0
- package/bin/spec-doctor.js +251 -0
- package/bin/spec-link.js +97 -0
- package/bin/spec-recipe.js +74 -0
- package/bin/spec-state.js +415 -0
- package/bin/spec.js +859 -0
- package/bin/upgrade.js +303 -298
- package/package.json +5 -3
- package/skills/finishing-a-development-branch/SKILL.md +240 -0
- package/skills/requesting-code-review/code-reviewer.md +198 -0
- package/skills/subagent-driven-development/SKILL.md +574 -0
- package/skills/subagent-driven-development/implementer-prompt.md +154 -0
- package/skills/subagent-driven-development/re-review-prompt.md +115 -0
- package/skills/subagent-driven-development/scripts/review-package +53 -0
- package/skills/subagent-driven-development/scripts/review-package.js +52 -0
- package/skills/subagent-driven-development/scripts/sdd-workspace +82 -0
- package/skills/subagent-driven-development/scripts/sdd-workspace-lib.js +62 -0
- package/skills/subagent-driven-development/scripts/sdd-workspace.js +15 -0
- package/skills/subagent-driven-development/scripts/task-brief +43 -0
- package/skills/subagent-driven-development/scripts/task-brief.js +46 -0
- package/skills/subagent-driven-development/task-reviewer-prompt.md +207 -0
- package/skills/system-discovery/SKILL.md +140 -0
- package/skills/system-reverse-engineer/SKILL.md +208 -0
- package/skills/system-spec-review/SKILL.md +177 -0
- package/skills/upstream.json +30 -0
- package/skills/using-git-worktrees/SKILL.md +175 -0
package/bin/config.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const CONFIG_RELATIVE_PATH = path.join('.thachvd', 'config.json');
|
|
5
|
+
const SUPPORTED_SPEC_LANGUAGES = new Set(['en', 'vi']);
|
|
6
|
+
const SUPPORTED_SPEC_PROFILES = new Set(['auto', 'backend', 'frontend', 'fullstack', 'mobile', 'infra']);
|
|
7
|
+
|
|
8
|
+
function resolveHome(env = process.env) {
|
|
9
|
+
return env.USERPROFILE || env.HOME || null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function configPath(homeDir) {
|
|
13
|
+
return path.join(homeDir, CONFIG_RELATIVE_PATH);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function readConfig(homeDir) {
|
|
17
|
+
if (!homeDir) return {};
|
|
18
|
+
const filePath = configPath(homeDir);
|
|
19
|
+
if (!fs.existsSync(filePath)) return {};
|
|
20
|
+
try {
|
|
21
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
22
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
|
23
|
+
} catch (error) {
|
|
24
|
+
throw new Error(`Cannot parse ${filePath}: ${error.message}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function writeConfig(homeDir, config) {
|
|
29
|
+
if (!homeDir) throw new Error('Cannot determine home directory from USERPROFILE or HOME.');
|
|
30
|
+
const filePath = configPath(homeDir);
|
|
31
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
32
|
+
fs.writeFileSync(filePath, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
33
|
+
return filePath;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function normalizeLanguage(value) {
|
|
37
|
+
const language = String(value || '').trim().toLowerCase();
|
|
38
|
+
if (!SUPPORTED_SPEC_LANGUAGES.has(language)) {
|
|
39
|
+
throw new Error(`Unsupported spec language: ${value}. Supported values: en, vi`);
|
|
40
|
+
}
|
|
41
|
+
return language;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function normalizeProfile(value) {
|
|
45
|
+
const profile = String(value || '').trim().toLowerCase();
|
|
46
|
+
if (!SUPPORTED_SPEC_PROFILES.has(profile)) {
|
|
47
|
+
throw new Error(`Unsupported spec profile: ${value}. Supported values: auto, backend, frontend, fullstack, mobile, infra`);
|
|
48
|
+
}
|
|
49
|
+
return profile;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getDefaultSpecProfile(env = process.env) {
|
|
53
|
+
const homeDir = resolveHome(env);
|
|
54
|
+
if (!homeDir) return null;
|
|
55
|
+
const config = readConfig(homeDir);
|
|
56
|
+
return config.spec_profile ? normalizeProfile(config.spec_profile) : null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getDefaultSpecLanguage(env = process.env) {
|
|
60
|
+
const homeDir = resolveHome(env);
|
|
61
|
+
if (!homeDir) return null;
|
|
62
|
+
const config = readConfig(homeDir);
|
|
63
|
+
return config.spec_language ? normalizeLanguage(config.spec_language) : null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function printHelp() {
|
|
67
|
+
console.log(`thachvd-kit config
|
|
68
|
+
|
|
69
|
+
Usage:
|
|
70
|
+
thachvd-kit config show
|
|
71
|
+
thachvd-kit config set spec-language en|vi
|
|
72
|
+
thachvd-kit config unset spec-language
|
|
73
|
+
thachvd-kit config set spec-profile auto|backend|frontend|fullstack|mobile|infra
|
|
74
|
+
thachvd-kit config unset spec-profile
|
|
75
|
+
|
|
76
|
+
The user-level config is stored at ~/.thachvd/config.json (or the platform-equivalent home directory).
|
|
77
|
+
Project-level --language and --profile always override their user defaults.
|
|
78
|
+
`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function runConfigCommand(args, options = {}) {
|
|
82
|
+
const env = options.env || process.env;
|
|
83
|
+
const homeDir = options.homeDir || resolveHome(env);
|
|
84
|
+
const command = args[0] || 'show';
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
if (command === '--help' || command === '-h' || command === 'help') {
|
|
88
|
+
printHelp();
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (command === 'show') {
|
|
93
|
+
const config = readConfig(homeDir);
|
|
94
|
+
console.log(`Config: ${homeDir ? configPath(homeDir) : '(home unavailable)'}`);
|
|
95
|
+
console.log(`spec-language: ${config.spec_language || '(default: en)'}`);
|
|
96
|
+
console.log(`spec-profile: ${config.spec_profile || '(default: auto)'}`);
|
|
97
|
+
return 0;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (command === 'set') {
|
|
101
|
+
if (!args[1] || !args[2]) {
|
|
102
|
+
throw new Error('Usage: thachvd-kit config set spec-language en|vi OR spec-profile auto|backend|frontend|fullstack|mobile|infra');
|
|
103
|
+
}
|
|
104
|
+
const config = readConfig(homeDir);
|
|
105
|
+
if (args[1] === 'spec-language') {
|
|
106
|
+
const language = normalizeLanguage(args[2]);
|
|
107
|
+
config.spec_language = language;
|
|
108
|
+
const filePath = writeConfig(homeDir, config);
|
|
109
|
+
console.log(`Updated ${filePath}`);
|
|
110
|
+
console.log(`spec-language: ${language}`);
|
|
111
|
+
return 0;
|
|
112
|
+
}
|
|
113
|
+
if (args[1] === 'spec-profile') {
|
|
114
|
+
const profile = normalizeProfile(args[2]);
|
|
115
|
+
config.spec_profile = profile;
|
|
116
|
+
const filePath = writeConfig(homeDir, config);
|
|
117
|
+
console.log(`Updated ${filePath}`);
|
|
118
|
+
console.log(`spec-profile: ${profile}`);
|
|
119
|
+
return 0;
|
|
120
|
+
}
|
|
121
|
+
throw new Error(`Unknown config key: ${args[1]}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (command === 'unset') {
|
|
125
|
+
const config = readConfig(homeDir);
|
|
126
|
+
if (args[1] === 'spec-language') {
|
|
127
|
+
delete config.spec_language;
|
|
128
|
+
const filePath = writeConfig(homeDir, config);
|
|
129
|
+
console.log(`Updated ${filePath}`);
|
|
130
|
+
console.log('spec-language: (default: en)');
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
if (args[1] === 'spec-profile') {
|
|
134
|
+
delete config.spec_profile;
|
|
135
|
+
const filePath = writeConfig(homeDir, config);
|
|
136
|
+
console.log(`Updated ${filePath}`);
|
|
137
|
+
console.log('spec-profile: (default: auto)');
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
throw new Error('Usage: thachvd-kit config unset spec-language|spec-profile');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
throw new Error(`Unknown config command: ${command}`);
|
|
144
|
+
} catch (error) {
|
|
145
|
+
console.error(`config ${command} failed: ${error.message}`);
|
|
146
|
+
process.exitCode = 1;
|
|
147
|
+
return 1;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
module.exports = {
|
|
152
|
+
CONFIG_RELATIVE_PATH,
|
|
153
|
+
SUPPORTED_SPEC_LANGUAGES,
|
|
154
|
+
SUPPORTED_SPEC_PROFILES,
|
|
155
|
+
resolveHome,
|
|
156
|
+
configPath,
|
|
157
|
+
readConfig,
|
|
158
|
+
writeConfig,
|
|
159
|
+
normalizeLanguage,
|
|
160
|
+
normalizeProfile,
|
|
161
|
+
getDefaultSpecLanguage,
|
|
162
|
+
getDefaultSpecProfile,
|
|
163
|
+
runConfigCommand
|
|
164
|
+
};
|
package/bin/entry.js
CHANGED
|
@@ -11,7 +11,7 @@ function isInitInvocation(args) {
|
|
|
11
11
|
if (args.includes('--help') || args.includes('-h') || args.includes('--version') || args.includes('-v')) {
|
|
12
12
|
return false;
|
|
13
13
|
}
|
|
14
|
-
// Note: 'upgrade' and '
|
|
14
|
+
// Note: 'upgrade', 'global', 'config', and 'spec' are handled and returned above before this
|
|
15
15
|
// is ever called, so they never reach here.
|
|
16
16
|
const first = args[0];
|
|
17
17
|
return !first || first === 'init' || first.startsWith('-');
|
|
@@ -74,6 +74,16 @@ if (command === 'global') {
|
|
|
74
74
|
return;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
if (command === 'config') {
|
|
78
|
+
require('./config').runConfigCommand(args.slice(1));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (command === 'spec') {
|
|
83
|
+
require('./spec').runSpecCommand(args.slice(1));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
77
87
|
const shouldUpgradeGeneratedPolicy = isInitInvocation(args);
|
|
78
88
|
const beforeSnapshot = shouldUpgradeGeneratedPolicy ? snapshotPolicyFiles(process.cwd()) : null;
|
|
79
89
|
const force = args.includes('--yes') || args.includes('-y');
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const NATIVE_SKILLS = [
|
|
5
|
+
'using-git-worktrees',
|
|
6
|
+
'subagent-driven-development',
|
|
7
|
+
'finishing-a-development-branch',
|
|
8
|
+
'system-discovery',
|
|
9
|
+
'system-reverse-engineer',
|
|
10
|
+
'system-spec-review'
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
const SYSTEM_SPEC_SKILLS = [
|
|
14
|
+
'system-discovery',
|
|
15
|
+
'system-reverse-engineer',
|
|
16
|
+
'system-spec-review'
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const NATIVE_SUPPORT_FILES = [
|
|
20
|
+
path.join('requesting-code-review', 'code-reviewer.md')
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const NATIVE_SKILL_SURFACES = [
|
|
24
|
+
{ id: 'agents', relativeDir: path.join('.agents', 'skills') },
|
|
25
|
+
{ id: 'claude', relativeDir: path.join('.claude', 'skills') }
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const nativeSkillsSourceDir = path.resolve(__dirname, '..', 'skills');
|
|
29
|
+
|
|
30
|
+
function sourcePath(relativePath) {
|
|
31
|
+
return path.join(nativeSkillsSourceDir, relativePath);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function skillSourceDir(name) {
|
|
35
|
+
return sourcePath(name);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function collectFiles(directory, relative = '') {
|
|
39
|
+
const files = [];
|
|
40
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
41
|
+
const entryRelative = path.join(relative, entry.name);
|
|
42
|
+
const entryPath = path.join(directory, entry.name);
|
|
43
|
+
if (entry.isDirectory()) files.push(...collectFiles(entryPath, entryRelative));
|
|
44
|
+
else if (entry.isFile()) files.push(entryRelative);
|
|
45
|
+
}
|
|
46
|
+
return files;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function ownedFiles() {
|
|
50
|
+
return [
|
|
51
|
+
...NATIVE_SKILLS.flatMap(name => collectFiles(skillSourceDir(name), name)),
|
|
52
|
+
...NATIVE_SUPPORT_FILES
|
|
53
|
+
];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function ownedSkillFiles(name) {
|
|
57
|
+
return collectFiles(skillSourceDir(name), name);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function targetPath(targetDir, surface, relativePath) {
|
|
61
|
+
return path.join(targetDir, surface.relativeDir, relativePath);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function checkNativeSkills(targetDir) {
|
|
65
|
+
const surfaces = {};
|
|
66
|
+
const skills = {};
|
|
67
|
+
|
|
68
|
+
for (const surface of NATIVE_SKILL_SURFACES) {
|
|
69
|
+
const missingFiles = ownedFiles().filter(relativePath => !fs.existsSync(targetPath(targetDir, surface, relativePath)));
|
|
70
|
+
const installed = [];
|
|
71
|
+
const missing = [];
|
|
72
|
+
for (const name of NATIVE_SKILLS) {
|
|
73
|
+
(ownedSkillFiles(name).every(relativePath => fs.existsSync(targetPath(targetDir, surface, relativePath)))
|
|
74
|
+
? installed
|
|
75
|
+
: missing).push(name);
|
|
76
|
+
}
|
|
77
|
+
const supportInstalled = NATIVE_SUPPORT_FILES.filter(relativePath => !missingFiles.includes(relativePath));
|
|
78
|
+
const supportMissing = NATIVE_SUPPORT_FILES.filter(relativePath => missingFiles.includes(relativePath));
|
|
79
|
+
surfaces[surface.id] = {
|
|
80
|
+
installed,
|
|
81
|
+
missing,
|
|
82
|
+
total: NATIVE_SKILLS.length,
|
|
83
|
+
missingFiles,
|
|
84
|
+
support: {
|
|
85
|
+
installed: supportInstalled,
|
|
86
|
+
missing: supportMissing,
|
|
87
|
+
total: NATIVE_SUPPORT_FILES.length
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
for (const name of NATIVE_SKILLS) {
|
|
93
|
+
skills[name] = {};
|
|
94
|
+
for (const surface of NATIVE_SKILL_SURFACES) {
|
|
95
|
+
skills[name][surface.id] = ownedSkillFiles(name).every(relativePath => fs.existsSync(targetPath(targetDir, surface, relativePath)));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const installed = NATIVE_SKILLS.filter(name => NATIVE_SKILL_SURFACES.every(surface => skills[name][surface.id]));
|
|
100
|
+
const missing = NATIVE_SKILLS.filter(name => !installed.includes(name));
|
|
101
|
+
const missingFiles = Object.fromEntries(NATIVE_SKILL_SURFACES.map(surface => [surface.id, surfaces[surface.id].missingFiles]));
|
|
102
|
+
return {
|
|
103
|
+
installed,
|
|
104
|
+
missing,
|
|
105
|
+
total: NATIVE_SKILLS.length,
|
|
106
|
+
allInstalled: missingFiles.agents.length === 0 && missingFiles.claude.length === 0,
|
|
107
|
+
surfaces,
|
|
108
|
+
skills,
|
|
109
|
+
missingFiles
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function copyFile(source, destination) {
|
|
114
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
115
|
+
fs.copyFileSync(source, destination);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function installNativeSkills(targetDir, options = {}) {
|
|
119
|
+
const dryRun = options.dryRun === true;
|
|
120
|
+
const operations = [];
|
|
121
|
+
for (const surface of NATIVE_SKILL_SURFACES) {
|
|
122
|
+
for (const relativePath of ownedFiles()) {
|
|
123
|
+
const source = sourcePath(relativePath);
|
|
124
|
+
const destination = targetPath(targetDir, surface, relativePath);
|
|
125
|
+
operations.push({ surface: surface.id, relativePath, source, destination });
|
|
126
|
+
if (!dryRun) copyFile(source, destination);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
ok: true,
|
|
131
|
+
dryRun,
|
|
132
|
+
operations,
|
|
133
|
+
results: NATIVE_SKILLS.map(name => ({ name, ok: true, dryRun })),
|
|
134
|
+
message: dryRun
|
|
135
|
+
? `Dry run: would copy ${NATIVE_SKILLS.length} native skills to .agents/skills and .claude/skills`
|
|
136
|
+
: `installed ${NATIVE_SKILLS.length} thachvd-kit native skills to .agents/skills and .claude/skills`
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function installSystemSpecSkills(targetDir, options = {}) {
|
|
141
|
+
const dryRun = options.dryRun === true;
|
|
142
|
+
const operations = [];
|
|
143
|
+
for (const surface of NATIVE_SKILL_SURFACES) {
|
|
144
|
+
for (const name of SYSTEM_SPEC_SKILLS) {
|
|
145
|
+
for (const relativePath of ownedSkillFiles(name)) {
|
|
146
|
+
const source = sourcePath(relativePath);
|
|
147
|
+
const destination = targetPath(targetDir, surface, relativePath);
|
|
148
|
+
operations.push({ surface: surface.id, relativePath, source, destination });
|
|
149
|
+
if (!dryRun) copyFile(source, destination);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
ok: true,
|
|
155
|
+
dryRun,
|
|
156
|
+
operations,
|
|
157
|
+
message: dryRun
|
|
158
|
+
? `Dry run: would copy ${SYSTEM_SPEC_SKILLS.length} system-spec skills to workspace agent surfaces`
|
|
159
|
+
: `installed ${SYSTEM_SPEC_SKILLS.length} system-spec skills to workspace agent surfaces`
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function updateNativeSkills(targetDir, options = {}) {
|
|
164
|
+
const result = installNativeSkills(targetDir, options);
|
|
165
|
+
return {
|
|
166
|
+
...result,
|
|
167
|
+
message: result.dryRun
|
|
168
|
+
? `Dry run: would refresh ${NATIVE_SKILLS.length} native skills from bundled copies`
|
|
169
|
+
: `updated ${NATIVE_SKILLS.length} thachvd-kit native skills from bundled copies`
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
module.exports = {
|
|
174
|
+
NATIVE_SKILLS,
|
|
175
|
+
SYSTEM_SPEC_SKILLS,
|
|
176
|
+
NATIVE_SUPPORT_FILES,
|
|
177
|
+
NATIVE_SKILL_SURFACES,
|
|
178
|
+
nativeSkillsSourceDir,
|
|
179
|
+
checkNativeSkills,
|
|
180
|
+
installNativeSkills,
|
|
181
|
+
installSystemSpecSkills,
|
|
182
|
+
updateNativeSkills
|
|
183
|
+
};
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { spawnSync } = require('child_process');
|
|
4
|
+
const state = require('./spec-state');
|
|
5
|
+
const links = require('./spec-link');
|
|
6
|
+
|
|
7
|
+
const SYSTEM_SKILLS = ['system-discovery', 'system-reverse-engineer', 'system-spec-review'];
|
|
8
|
+
|
|
9
|
+
function commandExists(command) {
|
|
10
|
+
const checker = process.platform === 'win32' ? 'where' : 'command';
|
|
11
|
+
const args = process.platform === 'win32' ? [command] : ['-v', command];
|
|
12
|
+
const result = spawnSync(checker, args, {
|
|
13
|
+
encoding: 'utf8',
|
|
14
|
+
shell: process.platform !== 'win32'
|
|
15
|
+
});
|
|
16
|
+
return result.status === 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function add(checks, level, name, message) {
|
|
20
|
+
checks.push({ level, name, message });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function runGit(cwd, args) {
|
|
24
|
+
const result = spawnSync('git', args, {
|
|
25
|
+
cwd,
|
|
26
|
+
encoding: 'utf8'
|
|
27
|
+
});
|
|
28
|
+
if (result.status !== 0) {
|
|
29
|
+
return {
|
|
30
|
+
ok: false,
|
|
31
|
+
error: (result.stderr || '').trim() || `git ${args.join(' ')} failed`
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return { ok: true, stdout: (result.stdout || '').trim() };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function canonicalPath(value) {
|
|
38
|
+
const resolved = path.resolve(value);
|
|
39
|
+
try {
|
|
40
|
+
return fs.realpathSync.native ? fs.realpathSync.native(resolved) : fs.realpathSync(resolved);
|
|
41
|
+
} catch {
|
|
42
|
+
return resolved;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function inspectSpecStorage(workspaceRoot, manifest) {
|
|
47
|
+
const specRoot = canonicalPath(path.resolve(workspaceRoot, manifest.spec_root));
|
|
48
|
+
if (!fs.existsSync(specRoot)) {
|
|
49
|
+
return { ok: false, message: `spec root is missing: ${manifest.spec_root}` };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const top = runGit(specRoot, ['rev-parse', '--show-toplevel']);
|
|
53
|
+
if (!top.ok) {
|
|
54
|
+
return {
|
|
55
|
+
ok: false,
|
|
56
|
+
message: 'spec root is not inside a Git worktree; generated specs/provenance are not version-controlled'
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const gitRoot = canonicalPath(top.stdout);
|
|
61
|
+
const required = [
|
|
62
|
+
path.join(specRoot, 'README.md'),
|
|
63
|
+
path.join(specRoot, '_meta', 'workspace.json'),
|
|
64
|
+
path.join(specRoot, 'architecture', 'capability-map.md'),
|
|
65
|
+
state.verificationPath(workspaceRoot, manifest),
|
|
66
|
+
state.reviewPath(workspaceRoot, manifest)
|
|
67
|
+
].filter(filePath => fs.existsSync(filePath));
|
|
68
|
+
|
|
69
|
+
const untracked = [];
|
|
70
|
+
for (const filePath of required) {
|
|
71
|
+
const relative = path.relative(gitRoot, canonicalPath(filePath)).split(path.sep).join('/');
|
|
72
|
+
const tracked = runGit(gitRoot, ['ls-files', '--error-unmatch', '--', relative]);
|
|
73
|
+
if (!tracked.ok) untracked.push(relative);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const specRelative = path.relative(gitRoot, specRoot).split(path.sep).join('/') || '.';
|
|
77
|
+
const status = runGit(gitRoot, ['status', '--porcelain', '--', specRelative]);
|
|
78
|
+
if (!status.ok) return { ok: false, message: status.error };
|
|
79
|
+
|
|
80
|
+
if (untracked.length > 0 || status.stdout) {
|
|
81
|
+
const parts = [];
|
|
82
|
+
if (untracked.length > 0) parts.push(`${untracked.length} key spec file(s) are not tracked`);
|
|
83
|
+
if (status.stdout) parts.push('spec root has uncommitted changes');
|
|
84
|
+
return {
|
|
85
|
+
ok: false,
|
|
86
|
+
message: `${parts.join('; ')}. Commit reviewed specs/provenance before treating them as durable.`
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return { ok: true, message: `spec root is clean and version-controlled in ${gitRoot}` };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function runDoctor(workspaceRoot, manifest, options = {}) {
|
|
94
|
+
const checks = [];
|
|
95
|
+
const binaryExists = options.commandExists || commandExists;
|
|
96
|
+
|
|
97
|
+
add(checks, 'ok', 'manifest', `.thachvd/system.json valid for ${manifest.name}; profile=${manifest.profile || 'auto'}; language=${manifest.language || 'en'}`);
|
|
98
|
+
|
|
99
|
+
if (binaryExists('codebase-memory-mcp')) {
|
|
100
|
+
add(checks, 'ok', 'codebase-memory', 'codebase-memory-mcp is installed');
|
|
101
|
+
} else {
|
|
102
|
+
add(checks, 'warn', 'codebase-memory', 'codebase-memory-mcp is not available on PATH');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const heads = state.captureRepoHeads(workspaceRoot, manifest, options);
|
|
106
|
+
for (const repo of manifest.repos) {
|
|
107
|
+
const repoRoot = path.resolve(workspaceRoot, repo.path);
|
|
108
|
+
if (!fs.existsSync(repoRoot)) {
|
|
109
|
+
add(checks, 'error', `repo:${repo.name}`, `repository path is missing: ${repo.path}`);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const head = heads[repo.name];
|
|
113
|
+
if (!head || !head.ok) {
|
|
114
|
+
add(checks, 'error', `repo:${repo.name}`, head ? head.error : 'Git HEAD unavailable');
|
|
115
|
+
} else {
|
|
116
|
+
add(checks, 'ok', `repo:${repo.name}`, `${repo.path} @ ${head.commit.slice(0, 12)}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
for (const skill of SYSTEM_SKILLS) {
|
|
121
|
+
const missing = [];
|
|
122
|
+
for (const surface of ['.agents', '.claude']) {
|
|
123
|
+
const filePath = path.join(workspaceRoot, surface, 'skills', skill, 'SKILL.md');
|
|
124
|
+
if (!fs.existsSync(filePath)) missing.push(surface);
|
|
125
|
+
}
|
|
126
|
+
if (missing.length === 0) add(checks, 'ok', `skill:${skill}`, 'installed on agent + Claude surfaces');
|
|
127
|
+
else add(checks, 'warn', `skill:${skill}`, `missing from: ${missing.join(', ')}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const storage = inspectSpecStorage(workspaceRoot, manifest);
|
|
131
|
+
add(checks, storage.ok ? 'ok' : 'warn', 'spec-storage', storage.message);
|
|
132
|
+
|
|
133
|
+
const workspaceSnapshot = path.join(workspaceRoot, manifest.spec_root, '_meta', 'workspace.json');
|
|
134
|
+
if (!fs.existsSync(workspaceSnapshot)) {
|
|
135
|
+
add(checks, 'warn', 'workspace-snapshot', 'missing; rerun spec init --yes to create a durable topology snapshot');
|
|
136
|
+
} else {
|
|
137
|
+
try {
|
|
138
|
+
const snapshot = JSON.parse(fs.readFileSync(workspaceSnapshot, 'utf8'));
|
|
139
|
+
const comparable = value => JSON.stringify({
|
|
140
|
+
schema_version: value.schema_version,
|
|
141
|
+
name: value.name,
|
|
142
|
+
spec_root: value.spec_root,
|
|
143
|
+
language: value.language,
|
|
144
|
+
profile: value.profile || 'auto',
|
|
145
|
+
repos: value.repos
|
|
146
|
+
});
|
|
147
|
+
if (comparable(snapshot) === comparable(manifest)) {
|
|
148
|
+
add(checks, 'ok', 'workspace-snapshot', 'durable topology snapshot matches the active manifest');
|
|
149
|
+
} else {
|
|
150
|
+
add(checks, 'warn', 'workspace-snapshot', 'snapshot differs from .thachvd/system.json; refresh with spec init --yes');
|
|
151
|
+
}
|
|
152
|
+
} catch (error) {
|
|
153
|
+
add(checks, 'error', 'workspace-snapshot', `cannot parse workspace snapshot: ${error.message}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const capabilityMap = path.join(workspaceRoot, manifest.spec_root, 'architecture', 'capability-map.md');
|
|
158
|
+
if (fs.existsSync(capabilityMap)) add(checks, 'ok', 'capability-map', 'capability map exists');
|
|
159
|
+
else add(checks, 'warn', 'capability-map', 'missing; run spec discover + /system-discovery');
|
|
160
|
+
|
|
161
|
+
const progress = state.readProgress(workspaceRoot, manifest);
|
|
162
|
+
if (!progress) {
|
|
163
|
+
add(checks, 'warn', 'reverse-progress', 'not started');
|
|
164
|
+
} else if (progress.status === 'completed') {
|
|
165
|
+
const capabilityStates = Object.values(progress.capabilities || {});
|
|
166
|
+
const blocked = capabilityStates.filter(item => item.status === 'blocked').length;
|
|
167
|
+
add(checks, blocked > 0 ? 'warn' : 'ok', 'reverse-progress',
|
|
168
|
+
`completed; ${capabilityStates.length} capabilities, ${blocked} blocked`);
|
|
169
|
+
} else {
|
|
170
|
+
const capabilityStates = Object.values(progress.capabilities || {});
|
|
171
|
+
const completed = capabilityStates.filter(item => item.status === 'completed').length;
|
|
172
|
+
add(checks, 'warn', 'reverse-progress',
|
|
173
|
+
`${progress.status}; ${completed}/${capabilityStates.length || '?'} capabilities completed`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
let verification = null;
|
|
177
|
+
let verificationFailed = false;
|
|
178
|
+
try {
|
|
179
|
+
verification = state.readVerification(workspaceRoot, manifest);
|
|
180
|
+
} catch (error) {
|
|
181
|
+
verificationFailed = true;
|
|
182
|
+
add(checks, 'error', 'verification', error.message);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (!verification && !verificationFailed) {
|
|
186
|
+
add(checks, 'warn', 'verification', 'not independently verified');
|
|
187
|
+
} else if (verification) {
|
|
188
|
+
try {
|
|
189
|
+
state.validateVerification(manifest, verification);
|
|
190
|
+
const statuses = Object.values(verification.capabilities || {});
|
|
191
|
+
const verified = statuses.filter(item => item.status === 'verified').length;
|
|
192
|
+
const unresolved = statuses.length - verified;
|
|
193
|
+
add(checks, unresolved === 0 ? 'ok' : 'warn', 'verification',
|
|
194
|
+
`${verified}/${statuses.length} capabilities verified; ${unresolved} unresolved`);
|
|
195
|
+
|
|
196
|
+
const reviewFile = state.reviewPath(workspaceRoot, manifest);
|
|
197
|
+
if (fs.existsSync(reviewFile)) add(checks, 'ok', 'review-report', 'review.md exists');
|
|
198
|
+
else add(checks, 'warn', 'review-report', 'verification exists but review.md is missing');
|
|
199
|
+
|
|
200
|
+
const drift = state.checkSpecDrift(workspaceRoot, manifest, verification, options);
|
|
201
|
+
add(checks, drift.stale_count === 0 ? 'ok' : 'warn', 'drift',
|
|
202
|
+
drift.stale_count === 0
|
|
203
|
+
? 'no stale/unknown verified capabilities detected'
|
|
204
|
+
: `${drift.stale_count} stale/unknown capabilities detected; run spec check`);
|
|
205
|
+
} catch (error) {
|
|
206
|
+
add(checks, 'error', 'verification', error.message);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
for (const repo of manifest.repos) {
|
|
211
|
+
const agentsPath = path.join(workspaceRoot, repo.path, 'AGENTS.md');
|
|
212
|
+
if (!fs.existsSync(agentsPath)) {
|
|
213
|
+
add(checks, 'warn', `agent-link:${repo.name}`, 'AGENTS.md missing or not linked');
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
const content = fs.readFileSync(agentsPath, 'utf8');
|
|
217
|
+
if (content.includes(links.START) && content.includes(links.END)) {
|
|
218
|
+
add(checks, 'ok', `agent-link:${repo.name}`, 'system spec managed block present');
|
|
219
|
+
} else {
|
|
220
|
+
add(checks, 'warn', `agent-link:${repo.name}`, 'system spec managed block missing; run spec link after verification');
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const counts = {
|
|
225
|
+
ok: checks.filter(item => item.level === 'ok').length,
|
|
226
|
+
warn: checks.filter(item => item.level === 'warn').length,
|
|
227
|
+
error: checks.filter(item => item.level === 'error').length
|
|
228
|
+
};
|
|
229
|
+
return { checks, counts };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function formatDoctorReport(report) {
|
|
233
|
+
const label = { ok: 'OK', warn: 'WARN', error: 'ERROR' };
|
|
234
|
+
const lines = ['System spec doctor', ''];
|
|
235
|
+
for (const check of report.checks) {
|
|
236
|
+
lines.push(`${label[check.level]} ${check.name}: ${check.message}`);
|
|
237
|
+
}
|
|
238
|
+
lines.push('');
|
|
239
|
+
lines.push(`Summary: ${report.counts.ok} OK, ${report.counts.warn} WARN, ${report.counts.error} ERROR`);
|
|
240
|
+
return lines.join('\n');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
module.exports = {
|
|
244
|
+
SYSTEM_SKILLS,
|
|
245
|
+
canonicalPath,
|
|
246
|
+
commandExists,
|
|
247
|
+
runGit,
|
|
248
|
+
inspectSpecStorage,
|
|
249
|
+
runDoctor,
|
|
250
|
+
formatDoctorReport
|
|
251
|
+
};
|