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/spec.js ADDED
@@ -0,0 +1,859 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { spawnSync } = require('child_process');
4
+ const recipe = require('./spec-recipe');
5
+ const links = require('./spec-link');
6
+ const nativeSkills = require('./native-skills');
7
+ const specState = require('./spec-state');
8
+ const userConfig = require('./config');
9
+ const specDoctor = require('./spec-doctor');
10
+
11
+ const MANIFEST_RELATIVE_PATH = path.join('.thachvd', 'system.json');
12
+ const DEFAULT_SPEC_ROOT = 'system-specs';
13
+ const CODEBASE_MEMORY_BINARY = 'codebase-memory-mcp';
14
+ const MANIFEST_SCHEMA_VERSION = 1;
15
+ const DEFAULT_SPEC_LANGUAGE = 'en';
16
+ const SUPPORTED_SPEC_LANGUAGES = new Set(['en', 'vi']);
17
+ const DEFAULT_SPEC_PROFILE = 'auto';
18
+ const SUPPORTED_SPEC_PROFILES = new Set(['auto', 'backend', 'frontend', 'fullstack', 'mobile', 'infra']);
19
+ const DISCOVERY_PROMPT_RELATIVE_PATH = path.join('.agent', 'docs', 'system-discovery-prompt.md');
20
+ const SYSTEM_REVERSE_PROMPT_RELATIVE_PATH = path.join('.agent', 'docs', 'system-reverse-prompt.md');
21
+ const SYSTEM_REVIEW_PROMPT_RELATIVE_PATH = path.join('.agent', 'docs', 'system-spec-review-prompt.md');
22
+ const WORKSPACE_SCAN_IGNORES = new Set([
23
+ '.git', '.thachvd', '.agent', '.agents', '.claude', 'node_modules',
24
+ 'vendor', 'dist', 'build', 'coverage', 'system-specs'
25
+ ]);
26
+
27
+ function toPortablePath(value) {
28
+ return value.split(path.sep).join('/');
29
+ }
30
+
31
+ function parseOptionValues(args, name) {
32
+ const values = [];
33
+ for (let i = 0; i < args.length; i++) {
34
+ const arg = args[i];
35
+ if (arg === name) {
36
+ if (i + 1 >= args.length || args[i + 1].startsWith('--')) {
37
+ throw new Error(`${name} requires a value`);
38
+ }
39
+ values.push(args[++i]);
40
+ } else if (arg.startsWith(`${name}=`)) {
41
+ values.push(arg.slice(name.length + 1));
42
+ }
43
+ }
44
+ return values;
45
+ }
46
+
47
+ function parseSingleOption(args, name, fallback = null) {
48
+ const values = parseOptionValues(args, name);
49
+ if (values.length > 1) throw new Error(`${name} may only be provided once`);
50
+ return values[0] ?? fallback;
51
+ }
52
+
53
+ function hasFlag(args, name) {
54
+ return args.includes(name);
55
+ }
56
+
57
+ function commandExists(command) {
58
+ const checker = process.platform === 'win32' ? 'where' : 'command';
59
+ const checkerArgs = process.platform === 'win32' ? [command] : ['-v', command];
60
+ const result = spawnSync(checker, checkerArgs, {
61
+ encoding: 'utf8',
62
+ shell: process.platform !== 'win32'
63
+ });
64
+ return result.status === 0;
65
+ }
66
+
67
+ function assertDirectory(directory, label) {
68
+ let stat;
69
+ try {
70
+ stat = fs.statSync(directory);
71
+ } catch {
72
+ throw new Error(`${label} does not exist: ${directory}`);
73
+ }
74
+ if (!stat.isDirectory()) throw new Error(`${label} is not a directory: ${directory}`);
75
+ }
76
+
77
+ function normalizeSpecLanguage(value) {
78
+ const language = (value || DEFAULT_SPEC_LANGUAGE).trim().toLowerCase();
79
+ if (!SUPPORTED_SPEC_LANGUAGES.has(language)) {
80
+ throw new Error(`Unsupported spec language: ${value}. Supported values: en, vi`);
81
+ }
82
+ return language;
83
+ }
84
+
85
+ function normalizeSpecProfile(value) {
86
+ const profile = (value || DEFAULT_SPEC_PROFILE).trim().toLowerCase();
87
+ if (!SUPPORTED_SPEC_PROFILES.has(profile)) {
88
+ throw new Error(`Unsupported spec profile: ${value}. Supported values: auto, backend, frontend, fullstack, mobile, infra`);
89
+ }
90
+ return profile;
91
+ }
92
+
93
+ function profileInstruction(profile) {
94
+ const common = [
95
+ 'Profile guidance is a review checklist, not a source of truth. Do not omit behavior that does not fit the selected profile.',
96
+ 'Use implementation/tests as the source of truth and mark unclear behavior explicitly.'
97
+ ];
98
+ const specific = {
99
+ auto: [
100
+ 'Infer a practical profile for each repository from code/config evidence; a multi-repo system may contain different repo profiles.',
101
+ 'Record the inferred repo profile(s) in system-overview/repo-map with supporting evidence.',
102
+ 'Apply the relevant specialized checklist below only after inferring it.'
103
+ ],
104
+ backend: [
105
+ 'Pay special attention to HTTP/RPC routes, middleware, authentication/authorization, services/domain logic, persistence/migrations/transactions, caches, queues/events/jobs/schedulers, external clients, feature flags, retries/idempotency, concurrency and failure/rollback behavior.'
106
+ ],
107
+ frontend: [
108
+ 'Pay special attention to routes/navigation, page/component composition, state management, API/data clients, forms/validation, auth/session handling, feature flags, analytics, accessibility, loading/error/empty states, browser storage and build/runtime configuration.'
109
+ ],
110
+ fullstack: [
111
+ 'Apply both backend and frontend checklists, and explicitly trace client/server boundaries, shared schemas/types, authentication/session propagation, BFF/SSR/server actions where present, and end-to-end failure handling.'
112
+ ],
113
+ mobile: [
114
+ 'Pay special attention to navigation, app lifecycle/background work, local storage/cache, offline/sync behavior, permissions, push notifications, deep links, networking/auth refresh, platform-specific code, device integrations and release/runtime configuration.'
115
+ ],
116
+ infra: [
117
+ 'Pay special attention to IaC modules/stacks, environments/workspaces, CI/CD, secrets, IAM, networking, state backends, observability, autoscaling/capacity, deployment ordering, rollback/recovery and destructive-change safeguards.'
118
+ ]
119
+ };
120
+ return [...common, ...(specific[profile] || specific.auto)].join('\n- ');
121
+ }
122
+
123
+ function languageInstruction(language) {
124
+ if (language === 'vi') {
125
+ return [
126
+ 'Write documentation prose, headings, explanations, and summaries in natural Vietnamese.',
127
+ 'Keep code identifiers, class/function names, API routes, event/queue names, database/schema names, file paths, commands, and source anchors exactly as they appear in code.',
128
+ 'Prefer established Vietnamese technical wording; retain the original English term in parentheses when translating it would make the concept less precise.'
129
+ ].join('\n- ');
130
+ }
131
+ return [
132
+ 'Write documentation prose, headings, explanations, and summaries in English.',
133
+ 'Keep code identifiers, class/function names, API routes, event/queue names, database/schema names, file paths, commands, and source anchors exactly as they appear in code.'
134
+ ].join('\n- ');
135
+ }
136
+
137
+ function resolveSpecRoot(workspaceRoot, specRoot) {
138
+ if (!specRoot || typeof specRoot !== 'string') throw new Error('spec_root must be a non-empty string');
139
+ if (path.isAbsolute(specRoot)) throw new Error('spec_root must be relative to the workspace root');
140
+ const resolved = path.resolve(workspaceRoot, specRoot);
141
+ const relative = path.relative(workspaceRoot, resolved);
142
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
143
+ throw new Error('spec_root must stay inside the workspace root');
144
+ }
145
+ return { resolved, relative: toPortablePath(relative || '.') };
146
+ }
147
+
148
+ function isGitRepositoryRoot(directory) {
149
+ return fs.existsSync(path.join(directory, '.git'));
150
+ }
151
+
152
+ function discoverRepoPaths(workspaceRoot, options = {}) {
153
+ const root = path.resolve(workspaceRoot);
154
+ if (isGitRepositoryRoot(root)) return ['.'];
155
+ const excluded = new Set((options.excludePaths || []).map(value => path.resolve(root, value)));
156
+
157
+ let entries = [];
158
+ try {
159
+ entries = fs.readdirSync(root, { withFileTypes: true });
160
+ } catch {
161
+ return ['.'];
162
+ }
163
+
164
+ const children = entries
165
+ .filter(entry => entry.isDirectory() && !WORKSPACE_SCAN_IGNORES.has(entry.name))
166
+ .map(entry => path.join(root, entry.name))
167
+ .filter(directory => !excluded.has(path.resolve(directory)))
168
+ .filter(isGitRepositoryRoot)
169
+ .sort((a, b) => a.localeCompare(b));
170
+
171
+ if (children.length === 0) return ['.'];
172
+ return children.map(directory => toPortablePath(path.relative(root, directory)));
173
+ }
174
+
175
+ function normalizeRepo(workspaceRoot, repoPath, usedNames) {
176
+ const absolutePath = path.resolve(workspaceRoot, repoPath);
177
+ assertDirectory(absolutePath, 'Repository path');
178
+ const baseName = path.basename(absolutePath) || 'repo';
179
+ let name = baseName;
180
+ let suffix = 2;
181
+ while (usedNames.has(name)) name = `${baseName}-${suffix++}`;
182
+ usedNames.add(name);
183
+ return {
184
+ name,
185
+ path: toPortablePath(path.relative(workspaceRoot, absolutePath) || '.')
186
+ };
187
+ }
188
+
189
+ function createManifest(workspaceRoot, options = {}) {
190
+ const name = options.name || path.basename(path.resolve(workspaceRoot));
191
+ const specRoot = resolveSpecRoot(workspaceRoot, options.specRoot || DEFAULT_SPEC_ROOT);
192
+ const repoPaths = options.repos && options.repos.length > 0
193
+ ? options.repos
194
+ : discoverRepoPaths(workspaceRoot, { excludePaths: [specRoot.resolved] });
195
+ const language = normalizeSpecLanguage(options.language);
196
+ const profile = normalizeSpecProfile(options.profile);
197
+ const usedNames = new Set();
198
+ const repos = repoPaths.map(repoPath => normalizeRepo(workspaceRoot, repoPath, usedNames));
199
+ const seenPaths = new Set();
200
+ for (const repo of repos) {
201
+ if (seenPaths.has(repo.path)) throw new Error(`Duplicate repository path: ${repo.path}`);
202
+ seenPaths.add(repo.path);
203
+ }
204
+ return {
205
+ schema_version: MANIFEST_SCHEMA_VERSION,
206
+ name,
207
+ spec_root: specRoot.relative,
208
+ language,
209
+ profile,
210
+ repos
211
+ };
212
+ }
213
+
214
+ function validateManifest(workspaceRoot, manifest) {
215
+ if (!manifest || typeof manifest !== 'object') throw new Error('System manifest must be a JSON object');
216
+ if (manifest.schema_version !== MANIFEST_SCHEMA_VERSION) {
217
+ throw new Error(`Unsupported system manifest schema_version: ${manifest.schema_version}`);
218
+ }
219
+ if (!manifest.name || typeof manifest.name !== 'string') throw new Error('System manifest requires name');
220
+ manifest.language = normalizeSpecLanguage(manifest.language || DEFAULT_SPEC_LANGUAGE);
221
+ manifest.profile = normalizeSpecProfile(manifest.profile || DEFAULT_SPEC_PROFILE);
222
+ resolveSpecRoot(workspaceRoot, manifest.spec_root);
223
+ if (!Array.isArray(manifest.repos) || manifest.repos.length === 0) {
224
+ throw new Error('System manifest requires at least one repository');
225
+ }
226
+ const names = new Set();
227
+ const paths = new Set();
228
+ for (const repo of manifest.repos) {
229
+ if (!repo || typeof repo.name !== 'string' || !repo.name.trim()) throw new Error('Each repository requires a name');
230
+ if (!repo.path || typeof repo.path !== 'string') throw new Error(`Repository ${repo.name || '(unknown)'} requires a path`);
231
+ if (names.has(repo.name)) throw new Error(`Duplicate repository name: ${repo.name}`);
232
+ if (paths.has(repo.path)) throw new Error(`Duplicate repository path: ${repo.path}`);
233
+ names.add(repo.name);
234
+ paths.add(repo.path);
235
+ }
236
+ return manifest;
237
+ }
238
+
239
+ function manifestPath(workspaceRoot) {
240
+ return path.join(workspaceRoot, MANIFEST_RELATIVE_PATH);
241
+ }
242
+
243
+ function readManifest(workspaceRoot) {
244
+ const filePath = manifestPath(workspaceRoot);
245
+ if (!fs.existsSync(filePath)) {
246
+ throw new Error(`System manifest not found at ${MANIFEST_RELATIVE_PATH}. Run: thachvd-kit spec init`);
247
+ }
248
+ let parsed;
249
+ try {
250
+ parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
251
+ } catch (error) {
252
+ throw new Error(`Cannot parse ${MANIFEST_RELATIVE_PATH}: ${error.message}`);
253
+ }
254
+ return validateManifest(workspaceRoot, parsed);
255
+ }
256
+
257
+ function initialReadme(manifest) {
258
+ const tick = '`';
259
+ const repoLines = manifest.repos.map(repo => `- ${tick}${repo.name}${tick}: ${tick}${repo.path}${tick}`).join('\n');
260
+ if (manifest.language === 'vi') {
261
+ return `# Đặc tả hệ thống ${manifest.name}
262
+
263
+ Thư mục này chứa bộ đặc tả AS-IS đã được review cho hệ thống brownfield hiện tại.
264
+
265
+ ## Repository
266
+
267
+ ${repoLines}
268
+
269
+ ## Luồng thực hiện
270
+
271
+ 1. Index các repository bằng ${tick}thachvd-kit spec index${tick}.
272
+ 2. Chạy ${tick}thachvd-kit spec discover${tick}, sau đó chạy ${tick}/system-discovery${tick}.
273
+ 3. Review thủ công repo map, capability map và integration map.
274
+ 4. Chạy ${tick}thachvd-kit spec reverse${tick}, sau đó chạy ${tick}/system-reverse-engineer${tick} để tạo tài liệu cho toàn bộ hệ thống đã được duyệt.
275
+ 5. Chạy ${tick}thachvd-kit spec verify${tick}, sau đó chạy ${tick}/system-spec-review${tick} để kiểm chứng độc lập với code/tests.
276
+ 6. Chạy ${tick}thachvd-kit spec check${tick} để phát hiện spec có thể stale so với code.
277
+ 7. Chạy ${tick}thachvd-kit spec link${tick} để các agent sau này tự tìm thấy bộ spec.
278
+ 8. Luôn tách biệt hành vi quan sát được từ code/tests với business rationale chưa được xác nhận.
279
+
280
+ Ngôn ngữ tài liệu: Tiếng Việt. Tên code/API/event/schema/file path giữ nguyên như trong source.
281
+ Profile phân tích: ${manifest.profile}.
282
+ `;
283
+ }
284
+ return `# ${manifest.name} System Specs
285
+
286
+ This directory is the durable, reviewed specification layer for the current brownfield system.
287
+
288
+ ## Repositories
289
+
290
+ ${repoLines}
291
+
292
+ ## Workflow
293
+
294
+ 1. Index repositories with ${tick}thachvd-kit spec index${tick}.
295
+ 2. Run ${tick}thachvd-kit spec discover${tick}, then ${tick}/system-discovery${tick}.
296
+ 3. Human-review the repo/capability/integration maps.
297
+ 4. Run ${tick}thachvd-kit spec reverse${tick}, then ${tick}/system-reverse-engineer${tick} to document the whole approved system.
298
+ 5. Run ${tick}thachvd-kit spec verify${tick}, then ${tick}/system-spec-review${tick} for independent evidence and coverage verification.
299
+ 6. Run ${tick}thachvd-kit spec check${tick} to detect potential drift from verified commits.
300
+ 7. Run ${tick}thachvd-kit spec link${tick} so future agents discover these specs.
301
+ 8. Keep observed behavior distinct from business rationale that requires owner confirmation.
302
+
303
+ Documentation language: English. Code/API/event/schema/file-path identifiers stay unchanged.
304
+ Analysis profile: ${manifest.profile}.
305
+ `;
306
+ }
307
+
308
+ function initializeWorkspace(workspaceRoot, options = {}) {
309
+ const filePath = manifestPath(workspaceRoot);
310
+ if (fs.existsSync(filePath) && !options.force) {
311
+ throw new Error(`${MANIFEST_RELATIVE_PATH} already exists. Re-run with --yes to replace it.`);
312
+ }
313
+ const manifest = createManifest(workspaceRoot, options);
314
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
315
+ fs.writeFileSync(filePath, JSON.stringify(manifest, null, 2) + '\n', 'utf8');
316
+
317
+ const { resolved: specRoot } = resolveSpecRoot(workspaceRoot, manifest.spec_root);
318
+ for (const directory of ['architecture', 'capabilities', 'flows', 'contracts', '_meta']) {
319
+ fs.mkdirSync(path.join(specRoot, directory), { recursive: true });
320
+ }
321
+ fs.writeFileSync(
322
+ path.join(specRoot, '_meta', 'workspace.json'),
323
+ JSON.stringify(manifest, null, 2) + '\n',
324
+ 'utf8'
325
+ );
326
+ const readmePath = path.join(specRoot, 'README.md');
327
+ if (!fs.existsSync(readmePath)) {
328
+ fs.writeFileSync(readmePath, initialReadme(manifest), 'utf8');
329
+ }
330
+
331
+ nativeSkills.installSystemSpecSkills(workspaceRoot);
332
+ return manifest;
333
+ }
334
+
335
+ function selectRepos(manifest, requestedNames) {
336
+ if (!requestedNames || requestedNames.length === 0) return manifest.repos;
337
+ const requested = new Set(requestedNames);
338
+ const selected = manifest.repos.filter(repo => requested.has(repo.name));
339
+ const missing = [...requested].filter(name => !manifest.repos.some(repo => repo.name === name));
340
+ if (missing.length > 0) throw new Error(`Unknown repository name(s): ${missing.join(', ')}`);
341
+ return selected;
342
+ }
343
+
344
+ function indexWorkspace(workspaceRoot, manifest, options = {}) {
345
+ const binary = options.binary || CODEBASE_MEMORY_BINARY;
346
+ const spawn = options.spawn || spawnSync;
347
+ const dryRun = options.dryRun === true;
348
+ const repos = selectRepos(manifest, options.repoNames || []);
349
+ if (!dryRun && options.skipBinaryCheck !== true && !commandExists(binary)) {
350
+ throw new Error(`${binary} is not installed. Run thachvd-kit setup or install codebase-memory-mcp first.`);
351
+ }
352
+
353
+ const results = [];
354
+ for (const repo of repos) {
355
+ const absolutePath = path.resolve(workspaceRoot, repo.path);
356
+ try {
357
+ assertDirectory(absolutePath, `Repository ${repo.name}`);
358
+ } catch (error) {
359
+ results.push({ name: repo.name, path: repo.path, ok: false, error: error.message });
360
+ continue;
361
+ }
362
+ const args = ['cli', 'index_repository', '--repo-path', absolutePath];
363
+ if (dryRun) {
364
+ results.push({ name: repo.name, path: repo.path, ok: true, dryRun: true, command: [binary, ...args] });
365
+ continue;
366
+ }
367
+ const result = spawn(binary, args, {
368
+ cwd: workspaceRoot,
369
+ encoding: 'utf8',
370
+ stdio: 'inherit',
371
+ shell: process.platform === 'win32'
372
+ });
373
+ results.push({
374
+ name: repo.name,
375
+ path: repo.path,
376
+ ok: result.status === 0,
377
+ status: result.status
378
+ });
379
+ }
380
+ return results;
381
+ }
382
+
383
+ function generateDiscoveryPrompt(manifest) {
384
+ const repos = manifest.repos.map(repo => `- ${repo.name}: ${repo.path}`).join('\n');
385
+ return `# System Discovery Prompt
386
+
387
+ Use the installed /system-discovery skill.
388
+
389
+ System: ${manifest.name}
390
+ Spec root: ${manifest.spec_root}
391
+ Documentation language: ${manifest.language}
392
+ Analysis profile: ${manifest.profile}
393
+
394
+ Language rules:
395
+ - ${languageInstruction(manifest.language)}
396
+
397
+ Profile rules:
398
+ - ${profileInstruction(manifest.profile)}
399
+
400
+ Repositories:
401
+ ${repos}
402
+
403
+ Requirements:
404
+ - Treat code and tests as the source of truth.
405
+ - Use Codebase Memory MCP as the primary structural discovery source.
406
+ - Inspect every configured repository and cross-repository flow.
407
+ - Write AS-IS system docs only; do not modify product code.
408
+ - Produce system-overview, repo-map, capability-map, integrations, and glossary under the configured spec root.
409
+ - Attach repository/file/symbol evidence to important claims.
410
+ - Mark inferred or unknown behavior explicitly; never invent business rationale.
411
+ - Stop after the capability map and related system docs are written.
412
+ - Require human review of capability boundaries before detailed reverse-engineering work.
413
+ `;
414
+ }
415
+
416
+ function writeDiscoveryPrompt(workspaceRoot, manifest) {
417
+ const outputPath = path.join(workspaceRoot, DISCOVERY_PROMPT_RELATIVE_PATH);
418
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
419
+ fs.writeFileSync(outputPath, generateDiscoveryPrompt(manifest), 'utf8');
420
+ return outputPath;
421
+ }
422
+
423
+ function generateSystemReversePrompt(manifest, capability = null) {
424
+ const repos = manifest.repos.map(repo => `- ${repo.name}: ${repo.path}`).join('\n');
425
+ const target = capability
426
+ ? `Only refresh the approved capability: ${capability}`
427
+ : 'Process every approved capability in the reviewed capability map.';
428
+ const progressRef = `${manifest.spec_root}/_meta/${specState.progressFileName(capability)}`;
429
+ return `# System Reverse Engineering Prompt
430
+
431
+ Use the installed /system-reverse-engineer skill.
432
+
433
+ System: ${manifest.name}
434
+ Spec root: ${manifest.spec_root}
435
+ Documentation language: ${manifest.language}
436
+ Analysis profile: ${manifest.profile}
437
+
438
+ Language rules:
439
+ - ${languageInstruction(manifest.language)}
440
+
441
+ Profile rules:
442
+ - ${profileInstruction(manifest.profile)}
443
+
444
+ Repositories:
445
+ ${repos}
446
+
447
+ Scope:
448
+ - ${target}
449
+
450
+ Requirements:
451
+ - Read the human-reviewed system discovery documents first.
452
+ - Use Codebase Memory MCP as the primary structural source and verify important claims against code/tests.
453
+ - Reverse-engineer AS-IS behavior only; do not modify product code.
454
+ - For each approved capability, produce PRD + design documentation under the configured spec root.
455
+ - Update real cross-repository flows and contracts as evidence is confirmed.
456
+ - Distinguish implementation facts, test-confirmed behavior, inference, and unknown business rationale.
457
+ - One invocation should cover the full approved system unless a capability was explicitly supplied for a targeted refresh.
458
+ - Use ${progressRef} as the durable checkpoint. Create/populate the approved capability worklist if needed, mark a capability in_progress before working on it, completed only after both PRD/design are written and checked, and blocked with a reason when necessary.
459
+ - Resume unfinished work from that checkpoint on later invocations; do not restart completed capabilities unless this is a targeted refresh.
460
+ - Finish with a coverage summary and unresolved ambiguities.
461
+ `;
462
+ }
463
+
464
+ function generateSystemReviewPrompt(manifest, repoHeads, capability = null) {
465
+ const repos = manifest.repos.map(repo => {
466
+ const head = repoHeads[repo.name];
467
+ return `- ${repo.name}: ${repo.path} @ ${head && head.ok ? head.commit : 'UNAVAILABLE'}`;
468
+ }).join('\n');
469
+ const scope = capability
470
+ ? `Targeted re-review: only capability "${capability}". Preserve unrelated verification entries and their per-capability commit baselines.`
471
+ : 'Full-system independent review: verify every approved capability.';
472
+ return `# Independent System Spec Review Prompt
473
+
474
+ Use the installed /system-spec-review skill.
475
+
476
+ System: ${manifest.name}
477
+ Spec root: ${manifest.spec_root}
478
+ Documentation language: ${manifest.language}
479
+ Analysis profile: ${manifest.profile}
480
+ Review scope: ${scope}
481
+
482
+ Language rules:
483
+ - ${languageInstruction(manifest.language)}
484
+
485
+ Profile rules:
486
+ - ${profileInstruction(manifest.profile)}
487
+
488
+ Repositories at review handoff:
489
+ ${repos}
490
+
491
+ Requirements:
492
+ - Independently reconstruct implementation coverage instead of trusting the generated specs.
493
+ - Verify important claims and a risk-weighted sample of source anchors against code/tests.
494
+ - Check omitted entry points, jobs, schedulers, listeners, observers/hooks, policies, integrations, state transitions, failure paths, and tests.
495
+ - Check PRD/design consistency and both sides of cross-repository contracts.
496
+ - Do not silently rewrite source specs during the first review pass.
497
+ - Write/update ${manifest.spec_root}/_meta/review.md.
498
+ - Write valid ${manifest.spec_root}/_meta/verification.json with source_paths and capability-specific verified_commits.
499
+ - For a full review, record current top-level repo commit SHAs and every approved capability status.
500
+ - For a targeted re-review, update only the requested capability entry and its verified_commits; preserve all unrelated capability baselines.
501
+ - Mark material gaps as needs_review or blocked; do not call them verified.
502
+ `;
503
+ }
504
+
505
+ function writeSystemReviewPrompt(workspaceRoot, manifest, repoHeads, capability = null) {
506
+ const relativePath = capability
507
+ ? path.join('.agent', 'docs', `system-spec-review-${recipe.normalizeCapability(capability)}-prompt.md`)
508
+ : SYSTEM_REVIEW_PROMPT_RELATIVE_PATH;
509
+ const outputPath = path.join(workspaceRoot, relativePath);
510
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
511
+ fs.writeFileSync(outputPath, generateSystemReviewPrompt(manifest, repoHeads, capability), 'utf8');
512
+ return outputPath;
513
+ }
514
+
515
+ function writeSystemReversePrompt(workspaceRoot, manifest, capability = null) {
516
+ const relativePath = capability
517
+ ? path.join('.agent', 'docs', `reverse-${recipe.normalizeCapability(capability)}-prompt.md`)
518
+ : SYSTEM_REVERSE_PROMPT_RELATIVE_PATH;
519
+ const outputPath = path.join(workspaceRoot, relativePath);
520
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
521
+ fs.writeFileSync(outputPath, generateSystemReversePrompt(manifest, capability), 'utf8');
522
+ return outputPath;
523
+ }
524
+
525
+ function printHelp() {
526
+ console.log(`thachvd-kit spec
527
+
528
+ Document an existing brownfield codebase as reviewed AS-IS system specs.
529
+ The workflow is opt-in and does not change normal thachvd-kit development unless you run spec init.
530
+
531
+ Quick start (same commands for one repo or many):
532
+ cd your-repo-or-workspace
533
+ thachvd-kit spec init
534
+ thachvd-kit spec index
535
+ thachvd-kit spec discover
536
+ # In your AI client: run /system-discovery
537
+ # Human-review system-specs/architecture/capability-map.md
538
+ thachvd-kit spec reverse
539
+ # In your AI client: run /system-reverse-engineer
540
+ thachvd-kit spec verify
541
+ # In your AI client: run /system-spec-review
542
+ thachvd-kit spec check
543
+ thachvd-kit spec link
544
+
545
+ Documentation language:
546
+ thachvd-kit spec init --language vi
547
+ # Prose/headings are Vietnamese; code/API/event/schema/file identifiers remain unchanged.
548
+
549
+ Analysis profile:
550
+ thachvd-kit spec init --profile backend
551
+ # Default is auto. Profiles adjust the discovery/review checklist, not the source of truth.
552
+
553
+ Repository auto-detection:
554
+ - If the current directory is a Git repository, it is treated as a single-repo system.
555
+ - Otherwise, direct child Git repositories are detected as a multi-repo system.
556
+ - --repo remains available only as an override for unusual layouts.
557
+
558
+ Usage:
559
+ thachvd-kit spec init [--name NAME] [--language en|vi] [--profile auto|backend|frontend|fullstack|mobile|infra] [--repo PATH ...] [--spec-root PATH] [--yes]
560
+ thachvd-kit spec index [--repo NAME ...] [--dry-run]
561
+ thachvd-kit spec discover
562
+ thachvd-kit spec reverse [CAPABILITY] [--reset]
563
+ thachvd-kit spec verify [CAPABILITY]
564
+ thachvd-kit spec check [--strict]
565
+ thachvd-kit spec doctor [--strict]
566
+ thachvd-kit spec link [--dry-run] [--allow-unverified]
567
+ thachvd-kit spec status
568
+
569
+ Advanced optional integration (Claude Code):
570
+ thachvd-kit spec recipe-setup [--fullstack] [--dry-run]
571
+ # Installs Shinpr's /recipe-reverse-engineer helper. The primary workflow
572
+ # does not require this plugin.
573
+
574
+ Commands:
575
+ init Auto-detect one or many repositories and create the system manifest/spec skeleton.
576
+ index Index every configured repository with codebase-memory-mcp.
577
+ discover Write the handoff for /system-discovery.
578
+ reverse Write the handoff for /system-reverse-engineer and create/resume its checkpoint.
579
+ No argument = whole approved system; a capability is a targeted refresh.
580
+ verify Write the handoff for independent /system-spec-review.
581
+ A capability argument re-verifies only that refreshed capability and preserves other baselines.
582
+ check Deterministically compare verified commits/source paths with current Git changes.
583
+ --strict exits non-zero when stale/unknown capabilities exist.
584
+ doctor Read-only health report for repos, tools, skills, progress, verification, drift, and links.
585
+ --strict exits non-zero when any WARN/ERROR remains.
586
+ link Add/update the managed system-spec block in each repository's AGENTS.md.
587
+ Requires all capabilities independently verified unless --allow-unverified is explicit.
588
+ status Show configured repositories, reverse progress, and verification state.
589
+ recipe-setup Optional: install Shinpr's reverse-engineering helper in each configured repo.
590
+
591
+ Recommended order:
592
+ init -> index -> discover -> /system-discovery -> human review
593
+ -> reverse -> /system-reverse-engineer -> verify -> /system-spec-review
594
+ -> check -> link
595
+ `);
596
+ }
597
+
598
+ function printStatus(workspaceRoot, manifest) {
599
+ console.log(`System: ${manifest.name}`);
600
+ console.log(`Manifest: ${MANIFEST_RELATIVE_PATH}`);
601
+ console.log(`Spec root: ${manifest.spec_root}`);
602
+ console.log(`Documentation language: ${manifest.language}`);
603
+ console.log(`Analysis profile: ${manifest.profile}`);
604
+ console.log('Repositories:');
605
+ for (const repo of manifest.repos) {
606
+ const exists = fs.existsSync(path.resolve(workspaceRoot, repo.path));
607
+ console.log(` ${exists ? 'OK' : 'MISSING'} ${repo.name} -> ${repo.path}`);
608
+ }
609
+ }
610
+
611
+ function runSpecCommand(args, options = {}) {
612
+ const workspaceRoot = options.workspaceRoot || process.cwd();
613
+ const command = args[0];
614
+ if (!command || command === '--help' || command === '-h' || command === 'help') {
615
+ printHelp();
616
+ return 0;
617
+ }
618
+
619
+ try {
620
+ if (command === 'init') {
621
+ const commandArgs = args.slice(1);
622
+ const explicitLanguage = parseSingleOption(commandArgs, '--language');
623
+ const explicitProfile = parseSingleOption(commandArgs, '--profile');
624
+ const manifest = initializeWorkspace(workspaceRoot, {
625
+ name: parseSingleOption(commandArgs, '--name'),
626
+ language: explicitLanguage || userConfig.getDefaultSpecLanguage() || DEFAULT_SPEC_LANGUAGE,
627
+ profile: explicitProfile || userConfig.getDefaultSpecProfile() || DEFAULT_SPEC_PROFILE,
628
+ repos: parseOptionValues(commandArgs, '--repo'),
629
+ specRoot: parseSingleOption(commandArgs, '--spec-root', DEFAULT_SPEC_ROOT),
630
+ force: hasFlag(commandArgs, '--yes') || hasFlag(commandArgs, '-y')
631
+ });
632
+ console.log(`Created ${MANIFEST_RELATIVE_PATH} for ${manifest.name}`);
633
+ console.log(`Configured ${manifest.repos.length} repository(s); specs live in ${manifest.spec_root}`);
634
+ console.log(`Documentation language: ${manifest.language}`);
635
+ console.log(`Analysis profile: ${manifest.profile}`);
636
+ for (const repo of manifest.repos) console.log(` - ${repo.name}: ${repo.path}`);
637
+ console.log('Installed /system-discovery, /system-reverse-engineer, and /system-spec-review in the workspace.');
638
+ console.log('Next: run thachvd-kit spec index');
639
+ return 0;
640
+ }
641
+
642
+ if (command === 'index') {
643
+ const commandArgs = args.slice(1);
644
+ const manifest = readManifest(workspaceRoot);
645
+ const results = indexWorkspace(workspaceRoot, manifest, {
646
+ repoNames: parseOptionValues(commandArgs, '--repo'),
647
+ dryRun: hasFlag(commandArgs, '--dry-run')
648
+ });
649
+ for (const result of results) {
650
+ if (result.dryRun) console.log(`WOULD INDEX ${result.name}: ${result.command.join(' ')}`);
651
+ else console.log(`${result.ok ? 'OK' : 'FAIL'} ${result.name} -> ${result.path}`);
652
+ }
653
+ const failures = results.filter(result => !result.ok);
654
+ if (failures.length > 0) {
655
+ process.exitCode = 1;
656
+ return 1;
657
+ }
658
+ return 0;
659
+ }
660
+
661
+ if (command === 'discover') {
662
+ const manifest = readManifest(workspaceRoot);
663
+ const outputPath = writeDiscoveryPrompt(workspaceRoot, manifest);
664
+ console.log(`Wrote ${toPortablePath(path.relative(workspaceRoot, outputPath))}`);
665
+ console.log('Next: run /system-discovery in your AI client, then review the generated capability map before continuing.');
666
+ return 0;
667
+ }
668
+
669
+ if (command === 'recipe-setup') {
670
+ const commandArgs = args.slice(1);
671
+ const manifest = readManifest(workspaceRoot);
672
+ const result = recipe.setupRecipePlugin(workspaceRoot, manifest, {
673
+ fullstack: hasFlag(commandArgs, '--fullstack'),
674
+ dryRun: hasFlag(commandArgs, '--dry-run')
675
+ });
676
+ for (const repoResult of result.results || []) {
677
+ const status = repoResult.dryRun ? 'WOULD INSTALL' : (repoResult.ok ? 'OK' : 'FAIL');
678
+ console.log(`${status} ${repoResult.name}`);
679
+ }
680
+ if (!result.ok) {
681
+ throw new Error(result.error || 'recipe plugin setup failed in one or more repositories');
682
+ }
683
+ console.log(`${result.dryRun ? 'Would install' : 'Installed'} ${result.plugin}@claude-code-workflows at project scope for all configured repositories`);
684
+ return 0;
685
+ }
686
+
687
+ if (command === 'reverse') {
688
+ const commandArgs = args.slice(1);
689
+ const capability = commandArgs.find(arg => !arg.startsWith('--')) || null;
690
+ const manifest = readManifest(workspaceRoot);
691
+ const progress = specState.ensureProgress(workspaceRoot, manifest, capability, {
692
+ reset: hasFlag(commandArgs, '--reset')
693
+ });
694
+ const outputPath = writeSystemReversePrompt(workspaceRoot, manifest, capability);
695
+ console.log(`Wrote ${toPortablePath(path.relative(workspaceRoot, outputPath))}`);
696
+ console.log(`${progress.resumed ? 'Resuming' : (progress.completed ? 'Existing completed' : 'Initialized')} checkpoint: ${toPortablePath(path.relative(workspaceRoot, progress.path))}`);
697
+ console.log(capability
698
+ ? `Next: run /system-reverse-engineer to refresh only ${capability}.`
699
+ : 'Next: run /system-reverse-engineer to document/resume every approved capability in the system.');
700
+ if (progress.completed && !hasFlag(commandArgs, '--reset')) {
701
+ console.log('Use --reset if you intentionally want to rerun this completed reverse scope.');
702
+ }
703
+ return 0;
704
+ }
705
+
706
+ if (command === 'verify') {
707
+ const commandArgs = args.slice(1);
708
+ const capability = commandArgs.find(arg => !arg.startsWith('--')) || null;
709
+ const manifest = readManifest(workspaceRoot);
710
+ const progress = specState.readProgress(workspaceRoot, manifest, capability);
711
+ if (!progress || progress.status !== 'completed') {
712
+ throw new Error(capability
713
+ ? `Targeted reverse engineering for ${capability} is not completed. Finish /system-reverse-engineer first.`
714
+ : 'Full-system reverse engineering is not completed. Finish /system-reverse-engineer first.');
715
+ }
716
+ if (capability) {
717
+ const existingVerification = specState.readVerification(workspaceRoot, manifest);
718
+ if (!existingVerification) {
719
+ throw new Error('Targeted verification requires an existing full-system verification baseline.');
720
+ }
721
+ specState.validateVerification(manifest, existingVerification);
722
+ const slug = recipe.normalizeCapability(capability);
723
+ if (!existingVerification.capabilities[slug]) {
724
+ throw new Error(`Targeted verification capability is not in the existing baseline: ${slug}. Run a full-system review instead.`);
725
+ }
726
+ }
727
+ const repoHeads = specState.captureRepoHeads(workspaceRoot, manifest);
728
+ const unavailable = Object.entries(repoHeads).filter(([, value]) => !value.ok);
729
+ if (unavailable.length > 0) {
730
+ throw new Error(`Cannot capture Git HEAD for: ${unavailable.map(([name]) => name).join(', ')}`);
731
+ }
732
+ const outputPath = writeSystemReviewPrompt(workspaceRoot, manifest, repoHeads, capability);
733
+ console.log(`Wrote ${toPortablePath(path.relative(workspaceRoot, outputPath))}`);
734
+ for (const [name, head] of Object.entries(repoHeads)) console.log(` ${name}: ${head.commit}`);
735
+ console.log(capability
736
+ ? `Next: run /system-spec-review to re-verify only ${capability} and preserve unrelated baselines.`
737
+ : 'Next: run /system-spec-review for the full system. It must write _meta/review.md and _meta/verification.json.');
738
+ return 0;
739
+ }
740
+
741
+ if (command === 'check') {
742
+ const commandArgs = args.slice(1);
743
+ const manifest = readManifest(workspaceRoot);
744
+ const verification = specState.readVerification(workspaceRoot, manifest);
745
+ if (!verification) {
746
+ throw new Error('No verification metadata found. Run thachvd-kit spec verify, then /system-spec-review first.');
747
+ }
748
+ const report = specState.checkSpecDrift(workspaceRoot, manifest, verification);
749
+ console.log(specState.formatDriftReport(report));
750
+ if (hasFlag(commandArgs, '--strict') && report.stale_count > 0) {
751
+ process.exitCode = 1;
752
+ return 1;
753
+ }
754
+ return 0;
755
+ }
756
+
757
+ if (command === 'doctor') {
758
+ const commandArgs = args.slice(1);
759
+ const manifest = readManifest(workspaceRoot);
760
+ const report = specDoctor.runDoctor(workspaceRoot, manifest);
761
+ console.log(specDoctor.formatDoctorReport(report));
762
+ if (hasFlag(commandArgs, '--strict') && (report.counts.warn > 0 || report.counts.error > 0)) {
763
+ process.exitCode = 1;
764
+ return 1;
765
+ }
766
+ return 0;
767
+ }
768
+
769
+ if (command === 'link') {
770
+ const commandArgs = args.slice(1);
771
+ const manifest = readManifest(workspaceRoot);
772
+ if (!hasFlag(commandArgs, '--allow-unverified')) {
773
+ const verification = specState.readVerification(workspaceRoot, manifest);
774
+ if (!verification) {
775
+ throw new Error('Specs are not independently verified. Run thachvd-kit spec verify, then /system-spec-review first.');
776
+ }
777
+ specState.validateVerification(manifest, verification);
778
+ if (!fs.existsSync(specState.reviewPath(workspaceRoot, manifest))) {
779
+ throw new Error('Independent review report is missing: system-specs/_meta/review.md');
780
+ }
781
+ const unresolved = Object.entries(verification.capabilities)
782
+ .filter(([, value]) => value.status !== 'verified')
783
+ .map(([slug]) => slug);
784
+ if (unresolved.length > 0) {
785
+ throw new Error(`Cannot link unverified capabilities: ${unresolved.join(', ')}. Re-review them or use --allow-unverified explicitly.`);
786
+ }
787
+ const drift = specState.checkSpecDrift(workspaceRoot, manifest, verification);
788
+ const stale = Object.entries(drift.capabilities)
789
+ .filter(([, value]) => value.stale)
790
+ .map(([slug]) => slug);
791
+ if (stale.length > 0) {
792
+ throw new Error(`Cannot link stale/unknown capabilities: ${stale.join(', ')}. Refresh/re-verify them or use --allow-unverified explicitly.`);
793
+ }
794
+ }
795
+ const results = links.linkRepositories(workspaceRoot, manifest, {
796
+ dryRun: hasFlag(commandArgs, '--dry-run')
797
+ });
798
+ for (const result of results) {
799
+ const verb = result.dryRun ? (result.changed ? 'WOULD UPDATE' : 'OK') : (result.changed ? 'UPDATED' : 'OK');
800
+ console.log(`${result.ok ? verb : 'FAIL'} ${result.name} -> ${result.path || result.error}`);
801
+ }
802
+ if (results.some(result => !result.ok)) {
803
+ process.exitCode = 1;
804
+ return 1;
805
+ }
806
+ return 0;
807
+ }
808
+
809
+ if (command === 'status') {
810
+ const manifest = readManifest(workspaceRoot);
811
+ printStatus(workspaceRoot, manifest);
812
+ const progress = specState.readProgress(workspaceRoot, manifest);
813
+ const verification = specState.readVerification(workspaceRoot, manifest);
814
+ console.log(`Reverse progress: ${progress ? progress.status : 'not started'}`);
815
+ console.log(`Spec verification: ${verification ? (verification.verified_at || 'present') : 'not verified'}`);
816
+ return 0;
817
+ }
818
+
819
+ console.error(`Unknown spec subcommand: ${command}`);
820
+ printHelp();
821
+ process.exitCode = 1;
822
+ return 1;
823
+ } catch (error) {
824
+ console.error(`spec ${command} failed: ${error.message}`);
825
+ process.exitCode = 1;
826
+ return 1;
827
+ }
828
+ }
829
+
830
+ module.exports = {
831
+ MANIFEST_RELATIVE_PATH,
832
+ DEFAULT_SPEC_ROOT,
833
+ DEFAULT_SPEC_LANGUAGE,
834
+ SUPPORTED_SPEC_LANGUAGES,
835
+ DEFAULT_SPEC_PROFILE,
836
+ SUPPORTED_SPEC_PROFILES,
837
+ MANIFEST_SCHEMA_VERSION,
838
+ DISCOVERY_PROMPT_RELATIVE_PATH,
839
+ SYSTEM_REVERSE_PROMPT_RELATIVE_PATH,
840
+ SYSTEM_REVIEW_PROMPT_RELATIVE_PATH,
841
+ normalizeSpecLanguage,
842
+ normalizeSpecProfile,
843
+ languageInstruction,
844
+ profileInstruction,
845
+ discoverRepoPaths,
846
+ createManifest,
847
+ validateManifest,
848
+ readManifest,
849
+ initializeWorkspace,
850
+ indexWorkspace,
851
+ selectRepos,
852
+ generateDiscoveryPrompt,
853
+ writeDiscoveryPrompt,
854
+ generateSystemReversePrompt,
855
+ writeSystemReversePrompt,
856
+ generateSystemReviewPrompt,
857
+ writeSystemReviewPrompt,
858
+ runSpecCommand
859
+ };