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.
Files changed (32) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +240 -0
  3. package/THIRD_PARTY_NOTICES.md +49 -0
  4. package/bin/cli.js +80 -24
  5. package/bin/config.js +164 -0
  6. package/bin/entry.js +11 -1
  7. package/bin/native-skills.js +183 -0
  8. package/bin/spec-doctor.js +251 -0
  9. package/bin/spec-link.js +97 -0
  10. package/bin/spec-recipe.js +74 -0
  11. package/bin/spec-state.js +415 -0
  12. package/bin/spec.js +859 -0
  13. package/bin/upgrade.js +303 -298
  14. package/package.json +5 -3
  15. package/skills/finishing-a-development-branch/SKILL.md +240 -0
  16. package/skills/requesting-code-review/code-reviewer.md +198 -0
  17. package/skills/subagent-driven-development/SKILL.md +574 -0
  18. package/skills/subagent-driven-development/implementer-prompt.md +154 -0
  19. package/skills/subagent-driven-development/re-review-prompt.md +115 -0
  20. package/skills/subagent-driven-development/scripts/review-package +53 -0
  21. package/skills/subagent-driven-development/scripts/review-package.js +52 -0
  22. package/skills/subagent-driven-development/scripts/sdd-workspace +82 -0
  23. package/skills/subagent-driven-development/scripts/sdd-workspace-lib.js +62 -0
  24. package/skills/subagent-driven-development/scripts/sdd-workspace.js +15 -0
  25. package/skills/subagent-driven-development/scripts/task-brief +43 -0
  26. package/skills/subagent-driven-development/scripts/task-brief.js +46 -0
  27. package/skills/subagent-driven-development/task-reviewer-prompt.md +207 -0
  28. package/skills/system-discovery/SKILL.md +140 -0
  29. package/skills/system-reverse-engineer/SKILL.md +208 -0
  30. package/skills/system-spec-review/SKILL.md +177 -0
  31. package/skills/upstream.json +30 -0
  32. 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 'global' are handled and returned above before this
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
+ };