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
@@ -0,0 +1,97 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const START = '<!-- thachvd-kit:system-specs:start -->';
5
+ const END = '<!-- thachvd-kit:system-specs:end -->';
6
+
7
+ function portable(value) {
8
+ return value.split(path.sep).join('/');
9
+ }
10
+
11
+ function relativeReference(fromDir, targetPath) {
12
+ const relative = portable(path.relative(fromDir, targetPath) || '.');
13
+ return relative.startsWith('.') ? relative : `./${relative}`;
14
+ }
15
+
16
+ function renderBlock(workspaceRoot, repoRoot, manifest) {
17
+ const specRoot = path.resolve(workspaceRoot, manifest.spec_root);
18
+ const manifestPath = path.join(workspaceRoot, '.thachvd', 'system.json');
19
+ const specRef = relativeReference(repoRoot, specRoot);
20
+ const manifestRef = relativeReference(repoRoot, manifestPath);
21
+ return `${START}
22
+ ## System Specifications
23
+
24
+ This repository is part of the **${manifest.name}** documented system.
25
+
26
+ - System manifest: \`${manifestRef}\`
27
+ - System specs: \`${specRef}\`
28
+
29
+ Before changing observable behavior:
30
+
31
+ 1. Read \`${specRef}/architecture/system-overview.md\`.
32
+ 2. Read \`${specRef}/architecture/capability-map.md\` and identify the affected capability.
33
+ 3. Read the relevant capability PRD/design and any cross-repository flow or contract it touches.
34
+ 4. If \`${specRef}/_meta/verification.json\` exists, run \`thachvd-kit spec check\` before relying on the spec; investigate STALE/UNKNOWN capabilities against code.
35
+ 5. Treat implementation and tests as the current source of truth. If code and reviewed spec disagree, report the drift; do not silently choose one.
36
+ 6. Update/re-verify reviewed specs when an approved behavior or contract change makes them stale.
37
+
38
+ Do not infer business rationale from code when the rationale is undocumented.
39
+ ${END}`;
40
+ }
41
+
42
+ function upsertManagedBlock(content, block) {
43
+ const start = content.indexOf(START);
44
+ const end = content.indexOf(END);
45
+ if ((start >= 0) !== (end >= 0) || (start >= 0 && end < start)) {
46
+ throw new Error('Malformed thachvd-kit system-spec markers in AGENTS.md');
47
+ }
48
+ if (start >= 0 && end >= start) {
49
+ const after = end + END.length;
50
+ return content.slice(0, start) + block + content.slice(after);
51
+ }
52
+ const trimmed = content.replace(/\s+$/, '');
53
+ return `${trimmed}${trimmed ? '\n\n' : ''}${block}\n`;
54
+ }
55
+
56
+ function linkRepositories(workspaceRoot, manifest, options = {}) {
57
+ const results = [];
58
+ for (const repo of manifest.repos) {
59
+ const repoRoot = path.resolve(workspaceRoot, repo.path);
60
+ if (!fs.existsSync(repoRoot) || !fs.statSync(repoRoot).isDirectory()) {
61
+ results.push({ name: repo.name, ok: false, error: 'repository path is missing' });
62
+ continue;
63
+ }
64
+ const agentsPath = path.join(repoRoot, 'AGENTS.md');
65
+ const existing = fs.existsSync(agentsPath) ? fs.readFileSync(agentsPath, 'utf8') : '';
66
+ try {
67
+ const block = renderBlock(workspaceRoot, repoRoot, manifest);
68
+ const next = upsertManagedBlock(existing, block);
69
+ const changed = next !== existing;
70
+ if (changed && !options.dryRun) fs.writeFileSync(agentsPath, next, 'utf8');
71
+ results.push({
72
+ name: repo.name,
73
+ ok: true,
74
+ changed,
75
+ dryRun: options.dryRun === true,
76
+ path: portable(path.relative(workspaceRoot, agentsPath))
77
+ });
78
+ } catch (error) {
79
+ results.push({
80
+ name: repo.name,
81
+ ok: false,
82
+ path: portable(path.relative(workspaceRoot, agentsPath)),
83
+ error: error.message
84
+ });
85
+ }
86
+ }
87
+ return results;
88
+ }
89
+
90
+ module.exports = {
91
+ START,
92
+ END,
93
+ relativeReference,
94
+ renderBlock,
95
+ upsertManagedBlock,
96
+ linkRepositories
97
+ };
@@ -0,0 +1,74 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { spawnSync } = require('child_process');
4
+
5
+ const MARKETPLACE = 'shinpr/claude-code-workflows';
6
+ const MARKETPLACE_NAME = 'claude-code-workflows';
7
+
8
+ function commandExists(command) {
9
+ const checker = process.platform === 'win32' ? 'where' : 'command';
10
+ const args = process.platform === 'win32' ? [command] : ['-v', command];
11
+ return spawnSync(checker, args, { encoding: 'utf8', shell: process.platform !== 'win32' }).status === 0;
12
+ }
13
+
14
+ function setupRecipePlugin(workspaceRoot, manifest, options = {}) {
15
+ const plugin = options.fullstack ? 'dev-workflows-fullstack' : 'dev-workflows';
16
+ const commands = [
17
+ ['plugin', 'marketplace', 'add', MARKETPLACE, '--scope', 'project'],
18
+ ['plugin', 'install', `${plugin}@${MARKETPLACE_NAME}`, '--scope', 'project']
19
+ ];
20
+ if (options.skipBinaryCheck !== true && !options.dryRun && !commandExists('claude')) {
21
+ return { ok: false, plugin, commands, error: 'Claude Code CLI is not installed; install the plugin manually or use another client.', results: [] };
22
+ }
23
+ const spawn = options.spawn || spawnSync;
24
+ const results = [];
25
+ for (const repo of manifest.repos) {
26
+ const cwd = path.resolve(workspaceRoot, repo.path);
27
+ if (!fs.existsSync(cwd) || !fs.statSync(cwd).isDirectory()) {
28
+ results.push({ name: repo.name, ok: false, error: 'repository path is missing' });
29
+ continue;
30
+ }
31
+ if (options.dryRun) {
32
+ results.push({ name: repo.name, ok: true, dryRun: true, cwd, commands });
33
+ continue;
34
+ }
35
+ let repoOk = true;
36
+ for (const args of commands) {
37
+ const result = spawn('claude', args, {
38
+ cwd,
39
+ encoding: 'utf8',
40
+ stdio: 'inherit',
41
+ shell: process.platform === 'win32'
42
+ });
43
+ if (result.status !== 0) {
44
+ results.push({ name: repo.name, ok: false, cwd, failedCommand: ['claude', ...args], status: result.status });
45
+ repoOk = false;
46
+ break;
47
+ }
48
+ }
49
+ if (repoOk) results.push({ name: repo.name, ok: true, cwd });
50
+ }
51
+ return {
52
+ ok: results.length === manifest.repos.length && results.every(result => result.ok),
53
+ plugin,
54
+ commands,
55
+ dryRun: options.dryRun === true,
56
+ results
57
+ };
58
+ }
59
+
60
+ function normalizeCapability(value) {
61
+ if (!value || typeof value !== 'string') throw new Error('Capability name is required');
62
+ const slug = value.trim().toLowerCase()
63
+ .replace(/[^a-z0-9]+/g, '-')
64
+ .replace(/^-+|-+$/g, '');
65
+ if (!slug) throw new Error('Capability name must contain letters or numbers');
66
+ return slug;
67
+ }
68
+
69
+ module.exports = {
70
+ MARKETPLACE,
71
+ MARKETPLACE_NAME,
72
+ setupRecipePlugin,
73
+ normalizeCapability
74
+ };
@@ -0,0 +1,415 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { spawnSync } = require('child_process');
4
+
5
+ const META_DIR = '_meta';
6
+ const VERIFICATION_FILE = 'verification.json';
7
+ const REVIEW_FILE = 'review.md';
8
+ const PROGRESS_FILE = 'reverse-progress.json';
9
+ const VERIFICATION_SCHEMA_VERSION = 1;
10
+ const PROGRESS_SCHEMA_VERSION = 1;
11
+
12
+ function portable(value) {
13
+ return value.split(path.sep).join('/');
14
+ }
15
+
16
+ function nowIso() {
17
+ return new Date().toISOString();
18
+ }
19
+
20
+ function metaRoot(workspaceRoot, manifest) {
21
+ return path.join(workspaceRoot, manifest.spec_root, META_DIR);
22
+ }
23
+
24
+ function verificationPath(workspaceRoot, manifest) {
25
+ return path.join(metaRoot(workspaceRoot, manifest), VERIFICATION_FILE);
26
+ }
27
+
28
+ function reviewPath(workspaceRoot, manifest) {
29
+ return path.join(metaRoot(workspaceRoot, manifest), REVIEW_FILE);
30
+ }
31
+
32
+ function progressSlug(capability) {
33
+ if (!capability) return null;
34
+ const slug = String(capability).trim().toLowerCase()
35
+ .replace(/[^a-z0-9]+/g, '-')
36
+ .replace(/^-+|-+$/g, '');
37
+ if (!slug) throw new Error('Capability name must contain letters or numbers');
38
+ return slug;
39
+ }
40
+
41
+ function progressFileName(capability = null) {
42
+ const slug = progressSlug(capability);
43
+ return slug ? `reverse-progress-${slug}.json` : PROGRESS_FILE;
44
+ }
45
+
46
+ function progressPath(workspaceRoot, manifest, capability = null) {
47
+ return path.join(metaRoot(workspaceRoot, manifest), progressFileName(capability));
48
+ }
49
+
50
+ function ensureMetaRoot(workspaceRoot, manifest) {
51
+ const directory = metaRoot(workspaceRoot, manifest);
52
+ fs.mkdirSync(directory, { recursive: true });
53
+ return directory;
54
+ }
55
+
56
+ function runGit(repoRoot, args, options = {}) {
57
+ const spawn = options.spawn || spawnSync;
58
+ const result = spawn('git', args, {
59
+ cwd: repoRoot,
60
+ encoding: 'utf8'
61
+ });
62
+ if (result.status !== 0) {
63
+ const stderr = typeof result.stderr === 'string' ? result.stderr.trim() : '';
64
+ return { ok: false, error: stderr || `git ${args.join(' ')} failed`, status: result.status };
65
+ }
66
+ return { ok: true, stdout: typeof result.stdout === 'string' ? result.stdout.trim() : '' };
67
+ }
68
+
69
+ function getRepoHead(repoRoot, options = {}) {
70
+ const result = runGit(repoRoot, ['rev-parse', 'HEAD'], options);
71
+ if (!result.ok) return result;
72
+ return { ok: true, commit: result.stdout };
73
+ }
74
+
75
+ function captureRepoHeads(workspaceRoot, manifest, options = {}) {
76
+ const heads = {};
77
+ for (const repo of manifest.repos) {
78
+ const repoRoot = path.resolve(workspaceRoot, repo.path);
79
+ if (!fs.existsSync(repoRoot)) {
80
+ heads[repo.name] = { ok: false, path: repo.path, error: 'repository path is missing' };
81
+ continue;
82
+ }
83
+ const head = getRepoHead(repoRoot, options);
84
+ heads[repo.name] = head.ok
85
+ ? { ok: true, path: repo.path, commit: head.commit }
86
+ : { ok: false, path: repo.path, error: head.error };
87
+ }
88
+ return heads;
89
+ }
90
+
91
+ function readJsonFile(filePath, label) {
92
+ if (!fs.existsSync(filePath)) return null;
93
+ try {
94
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
95
+ } catch (error) {
96
+ throw new Error(`Cannot parse ${label}: ${error.message}`);
97
+ }
98
+ }
99
+
100
+ function readVerification(workspaceRoot, manifest) {
101
+ const filePath = verificationPath(workspaceRoot, manifest);
102
+ const data = readJsonFile(filePath, `${manifest.spec_root}/${META_DIR}/${VERIFICATION_FILE}`);
103
+ if (!data) return null;
104
+ if (data.schema_version !== VERIFICATION_SCHEMA_VERSION) {
105
+ throw new Error(`Unsupported verification schema_version: ${data.schema_version}`);
106
+ }
107
+ if (data.system && data.system !== manifest.name) {
108
+ throw new Error(`verification system mismatch: expected ${manifest.name}, got ${data.system}`);
109
+ }
110
+ if (!data.repositories || typeof data.repositories !== 'object') {
111
+ throw new Error('verification.json requires repositories');
112
+ }
113
+ if (!data.capabilities || typeof data.capabilities !== 'object') {
114
+ throw new Error('verification.json requires capabilities');
115
+ }
116
+ return data;
117
+ }
118
+
119
+ function normalizeSourcePath(value) {
120
+ if (typeof value !== 'string' || !value.trim()) return null;
121
+ const raw = value.trim().replace(/\\/g, '/');
122
+ if (raw.startsWith('/') || /^[A-Za-z]:\//.test(raw)) {
123
+ throw new Error(`source path must be repository-relative: ${value}`);
124
+ }
125
+ const normalized = raw.replace(/^\.\//, '').replace(/\/+/g, '/').replace(/\/$/, '');
126
+ if (!normalized || normalized === '.' || normalized === '..' || normalized.startsWith('../') || normalized.includes('/../')) {
127
+ throw new Error(`source path must stay inside the repository: ${value}`);
128
+ }
129
+ return normalized;
130
+ }
131
+
132
+ function normalizeSourcePaths(value) {
133
+ if (!Array.isArray(value)) return [];
134
+ return [...new Set(value
135
+ .map(normalizeSourcePath)
136
+ .filter(Boolean))];
137
+ }
138
+
139
+ function validateVerification(manifest, data) {
140
+ if (!data || typeof data !== 'object') throw new Error('verification data must be an object');
141
+ if (data.schema_version !== VERIFICATION_SCHEMA_VERSION) {
142
+ throw new Error(`verification schema_version must be ${VERIFICATION_SCHEMA_VERSION}`);
143
+ }
144
+ if (data.system && data.system !== manifest.name) {
145
+ throw new Error(`verification system mismatch: expected ${manifest.name}, got ${data.system}`);
146
+ }
147
+ const repoNames = new Set(manifest.repos.map(repo => repo.name));
148
+ for (const repo of manifest.repos) {
149
+ if (!data.repositories || !data.repositories[repo.name]) {
150
+ throw new Error(`verification is missing repository: ${repo.name}`);
151
+ }
152
+ }
153
+ for (const [repoName, repoState] of Object.entries(data.repositories || {})) {
154
+ if (!repoNames.has(repoName)) throw new Error(`verification references unknown repository: ${repoName}`);
155
+ if (!repoState || typeof repoState.commit !== 'string' || !/^[0-9a-f]{40}$/i.test(repoState.commit.trim())) {
156
+ throw new Error(`verification repository ${repoName} requires a full Git commit SHA`);
157
+ }
158
+ }
159
+ const capabilityEntries = Object.entries(data.capabilities || {});
160
+ if (capabilityEntries.length === 0) throw new Error('verification requires at least one capability');
161
+ for (const [slug, capability] of capabilityEntries) {
162
+ if (!capability || typeof capability !== 'object') throw new Error(`invalid capability verification: ${slug}`);
163
+ if (!['verified', 'needs_review', 'blocked'].includes(capability.status)) {
164
+ throw new Error(`invalid verification status for ${slug}: ${capability.status}`);
165
+ }
166
+ capability.source_paths = capability.source_paths || {};
167
+ capability.verified_commits = capability.verified_commits || {};
168
+ for (const [repoName, sourcePaths] of Object.entries(capability.source_paths)) {
169
+ if (!repoNames.has(repoName)) throw new Error(`capability ${slug} references unknown repository: ${repoName}`);
170
+ capability.source_paths[repoName] = normalizeSourcePaths(sourcePaths);
171
+ const commit = capability.verified_commits[repoName];
172
+ if (capability.status === 'verified' && (!commit || !/^[0-9a-f]{40}$/i.test(commit))) {
173
+ throw new Error(`verified capability ${slug} requires verified_commits.${repoName}`);
174
+ }
175
+ }
176
+ for (const [repoName, commit] of Object.entries(capability.verified_commits)) {
177
+ if (!repoNames.has(repoName)) throw new Error(`capability ${slug} verified_commits references unknown repository: ${repoName}`);
178
+ if (!/^[0-9a-f]{40}$/i.test(String(commit))) {
179
+ throw new Error(`capability ${slug} has invalid verified commit for ${repoName}`);
180
+ }
181
+ }
182
+
183
+ if (capability.status === 'verified') {
184
+ const sourceCount = Object.values(capability.source_paths)
185
+ .reduce((sum, paths) => sum + paths.length, 0);
186
+ if (sourceCount === 0) {
187
+ throw new Error(`verified capability ${slug} requires at least one source path`);
188
+ }
189
+ if (!capability.evidence || typeof capability.evidence !== 'object') {
190
+ throw new Error(`verified capability ${slug} requires evidence counts`);
191
+ }
192
+ const requiredCounts = [
193
+ 'implementation_claims_checked',
194
+ 'test_claims_checked',
195
+ 'unsupported_claims',
196
+ 'potential_gaps',
197
+ 'broken_anchors'
198
+ ];
199
+ for (const key of requiredCounts) {
200
+ const value = capability.evidence[key];
201
+ if (!Number.isInteger(value) || value < 0) {
202
+ throw new Error(`verified capability ${slug} requires non-negative integer evidence.${key}`);
203
+ }
204
+ }
205
+ for (const key of ['unsupported_claims', 'potential_gaps', 'broken_anchors']) {
206
+ if (capability.evidence[key] !== 0) {
207
+ throw new Error(`verified capability ${slug} cannot have ${key} > 0; use needs_review instead`);
208
+ }
209
+ }
210
+ }
211
+ }
212
+ return data;
213
+ }
214
+
215
+ function createProgress(manifest, capability = null) {
216
+ return {
217
+ schema_version: PROGRESS_SCHEMA_VERSION,
218
+ system: manifest.name,
219
+ mode: capability ? 'targeted' : 'full',
220
+ target: capability || null,
221
+ started_at: nowIso(),
222
+ updated_at: nowIso(),
223
+ status: 'pending',
224
+ capabilities: {}
225
+ };
226
+ }
227
+
228
+ function readProgress(workspaceRoot, manifest, capability = null) {
229
+ const filePath = progressPath(workspaceRoot, manifest, capability);
230
+ const data = readJsonFile(filePath, `${manifest.spec_root}/${META_DIR}/${progressFileName(capability)}`);
231
+ if (!data) return null;
232
+ if (data.schema_version !== PROGRESS_SCHEMA_VERSION) {
233
+ throw new Error(`Unsupported reverse progress schema_version: ${data.schema_version}`);
234
+ }
235
+ return data;
236
+ }
237
+
238
+ function ensureProgress(workspaceRoot, manifest, capability = null, options = {}) {
239
+ ensureMetaRoot(workspaceRoot, manifest);
240
+ const filePath = progressPath(workspaceRoot, manifest, capability);
241
+ const existing = readProgress(workspaceRoot, manifest, capability);
242
+ if (existing && !options.reset) {
243
+ const sameMode = existing.mode === (capability ? 'targeted' : 'full');
244
+ const sameTarget = (existing.target || null) === (capability || null);
245
+ if (sameMode && sameTarget && existing.status !== 'completed') {
246
+ return { path: filePath, progress: existing, created: false, resumed: true };
247
+ }
248
+ if (sameMode && sameTarget && existing.status === 'completed') {
249
+ return { path: filePath, progress: existing, created: false, resumed: false, completed: true };
250
+ }
251
+ }
252
+ const progress = createProgress(manifest, capability);
253
+ fs.writeFileSync(filePath, JSON.stringify(progress, null, 2) + '\n', 'utf8');
254
+ return { path: filePath, progress, created: true, resumed: false };
255
+ }
256
+
257
+ function pathMatches(changedFile, sourcePath) {
258
+ const changed = portable(changedFile).replace(/^\.\//, '');
259
+ const source = portable(sourcePath).replace(/^\.\//, '').replace(/\/$/, '');
260
+ return changed === source || changed.startsWith(source + '/') || source.startsWith(changed + '/');
261
+ }
262
+
263
+ function changedFilesSince(repoRoot, baseCommit, options = {}) {
264
+ const head = getRepoHead(repoRoot, options);
265
+ if (!head.ok) return { ok: false, error: head.error };
266
+
267
+ const ancestry = runGit(repoRoot, ['cat-file', '-e', `${baseCommit}^{commit}`], options);
268
+ if (!ancestry.ok) {
269
+ return { ok: false, commit: head.commit, error: `verified commit is unavailable locally: ${baseCommit}` };
270
+ }
271
+
272
+ const changed = new Set();
273
+ if (head.commit !== baseCommit) {
274
+ const diff = runGit(repoRoot, ['diff', '--name-only', '--no-renames', `${baseCommit}..${head.commit}`], options);
275
+ if (!diff.ok) return { ok: false, commit: head.commit, error: diff.error };
276
+ for (const file of diff.stdout ? diff.stdout.split(/\r?\n/).filter(Boolean) : []) changed.add(portable(file));
277
+ }
278
+
279
+ for (const args of [
280
+ ['diff', '--name-only', '--no-renames', 'HEAD'],
281
+ ['diff', '--name-only', '--no-renames', '--cached'],
282
+ ['ls-files', '--others', '--exclude-standard']
283
+ ]) {
284
+ const result = runGit(repoRoot, args, options);
285
+ if (!result.ok) return { ok: false, commit: head.commit, error: result.error };
286
+ for (const file of result.stdout ? result.stdout.split(/\r?\n/).filter(Boolean) : []) changed.add(portable(file));
287
+ }
288
+
289
+ return { ok: true, commit: head.commit, changed: [...changed].sort() };
290
+ }
291
+
292
+ function checkSpecDrift(workspaceRoot, manifest, verification, options = {}) {
293
+ validateVerification(manifest, verification);
294
+ const repoStates = {};
295
+ for (const repo of manifest.repos) {
296
+ const verified = verification.repositories[repo.name];
297
+ const repoRoot = path.resolve(workspaceRoot, repo.path);
298
+ const head = getRepoHead(repoRoot, options);
299
+ repoStates[repo.name] = head.ok
300
+ ? { ok: true, path: repo.path, verified_commit: verified.commit, commit: head.commit }
301
+ : { ok: false, path: repo.path, verified_commit: verified.commit, error: head.error };
302
+ }
303
+
304
+ const capabilities = {};
305
+ const deltaCache = new Map();
306
+ for (const [slug, capability] of Object.entries(verification.capabilities)) {
307
+ if (capability.status !== 'verified') {
308
+ capabilities[slug] = {
309
+ status: capability.status,
310
+ stale: true,
311
+ reason: `verification status is ${capability.status}`,
312
+ changed_files: {}
313
+ };
314
+ continue;
315
+ }
316
+
317
+ let stale = false;
318
+ let unknown = false;
319
+ const changedFiles = {};
320
+ const reasons = [];
321
+
322
+ for (const repo of manifest.repos) {
323
+ const repoState = repoStates[repo.name];
324
+ const sourcePaths = normalizeSourcePaths((capability.source_paths || {})[repo.name]);
325
+ if (sourcePaths.length === 0) continue;
326
+
327
+ if (!repoState || !repoState.ok) {
328
+ unknown = true;
329
+ reasons.push(`${repo.name}: ${repoState ? repoState.error : 'missing repository state'}`);
330
+ continue;
331
+ }
332
+
333
+ const baseline = (capability.verified_commits || {})[repo.name]
334
+ || verification.repositories[repo.name].commit;
335
+ const repoRoot = path.resolve(workspaceRoot, repo.path);
336
+ const cacheKey = `${repo.name}:${baseline}`;
337
+ let delta = deltaCache.get(cacheKey);
338
+ if (!delta) {
339
+ delta = changedFilesSince(repoRoot, baseline, options);
340
+ deltaCache.set(cacheKey, delta);
341
+ }
342
+ if (!delta.ok) {
343
+ unknown = true;
344
+ reasons.push(`${repo.name}: ${delta.error}`);
345
+ continue;
346
+ }
347
+
348
+ const relevant = delta.changed.filter(file => sourcePaths.some(source => pathMatches(file, source)));
349
+ if (relevant.length > 0) {
350
+ stale = true;
351
+ changedFiles[repo.name] = relevant;
352
+ }
353
+ }
354
+
355
+ capabilities[slug] = {
356
+ status: unknown ? 'unknown' : (stale ? 'stale' : 'current'),
357
+ stale: stale || unknown,
358
+ reason: reasons.join('; ') || null,
359
+ changed_files: changedFiles
360
+ };
361
+ }
362
+
363
+ return {
364
+ checked_at: nowIso(),
365
+ repositories: repoStates,
366
+ capabilities,
367
+ stale_count: Object.values(capabilities).filter(item => item.stale).length
368
+ };
369
+ }
370
+
371
+ function formatDriftReport(report) {
372
+ const lines = ['System spec drift check', ''];
373
+ for (const [slug, result] of Object.entries(report.capabilities)) {
374
+ const marker = result.status === 'current' ? 'OK' : result.status.toUpperCase();
375
+ lines.push(`${marker} ${slug}`);
376
+ if (result.reason) lines.push(` ${result.reason}`);
377
+ for (const [repoName, files] of Object.entries(result.changed_files || {})) {
378
+ lines.push(` ${repoName}: ${files.length} relevant changed file(s)`);
379
+ for (const file of files.slice(0, 10)) lines.push(` - ${file}`);
380
+ if (files.length > 10) lines.push(` - ... ${files.length - 10} more`);
381
+ }
382
+ }
383
+ lines.push('');
384
+ lines.push(`Potentially stale/unknown capabilities: ${report.stale_count}`);
385
+ return lines.join('\n');
386
+ }
387
+
388
+ module.exports = {
389
+ META_DIR,
390
+ VERIFICATION_FILE,
391
+ REVIEW_FILE,
392
+ PROGRESS_FILE,
393
+ VERIFICATION_SCHEMA_VERSION,
394
+ PROGRESS_SCHEMA_VERSION,
395
+ metaRoot,
396
+ verificationPath,
397
+ reviewPath,
398
+ progressSlug,
399
+ progressFileName,
400
+ progressPath,
401
+ ensureMetaRoot,
402
+ getRepoHead,
403
+ captureRepoHeads,
404
+ readVerification,
405
+ validateVerification,
406
+ createProgress,
407
+ readProgress,
408
+ ensureProgress,
409
+ normalizeSourcePath,
410
+ normalizeSourcePaths,
411
+ pathMatches,
412
+ changedFilesSince,
413
+ checkSpecDrift,
414
+ formatDriftReport
415
+ };