@haposoft/cafekit 0.15.3 → 0.16.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/bin/__tests__/codex-hooks.test.js +140 -0
- package/bin/__tests__/codex-native.test.js +2 -2
- package/bin/__tests__/develop-contract.test.js +1120 -0
- package/bin/__tests__/installer-safety.test.js +510 -2
- package/bin/__tests__/package-inventory.test.js +258 -0
- package/bin/__tests__/secret-scanner.test.js +157 -0
- package/bin/__tests__/validator-grounding.test.js +713 -0
- package/bin/install.js +5 -1
- package/bin/lib/codex-install.js +21 -6
- package/bin/lib/context.js +17 -6
- package/bin/lib/copy-utils.js +31 -0
- package/bin/lib/instruction-blocks.js +85 -0
- package/bin/lib/managed-writer.js +4 -1
- package/bin/lib/opencode-install.js +75 -30
- package/bin/phases/claude-runtime.js +103 -4
- package/bin/phases/codex-runtime.js +9 -5
- package/bin/phases/post-install.js +29 -22
- package/bin/phases/root-config.js +89 -10
- package/package.json +14 -4
- package/src/claude/CLAUDE.md +6 -122
- package/src/claude/agents/code-auditor.md +28 -22
- package/src/claude/agents/debugger.md +3 -3
- package/src/claude/agents/deployer.md +1 -1
- package/src/claude/agents/{god-developer.md → implementer.md} +2 -2
- package/src/claude/agents/project-manager.md +1 -1
- package/src/claude/agents/spec-maker.md +2 -2
- package/src/claude/hooks/__tests__/spec-gate.test.js +356 -5
- package/src/claude/hooks/agent.cjs +7 -14
- package/src/claude/hooks/rules.cjs +33 -53
- package/src/claude/hooks/spec-gate.cjs +66 -21
- package/src/claude/hooks/spec-state.cjs +5 -0
- package/src/claude/migration-manifest.json +9 -2
- package/src/claude/runtime.json +0 -3
- package/src/claude/scripts/generate-skill-catalog.cjs +1 -1
- package/src/claude/scripts/scan-staged-secrets.cjs +170 -0
- package/src/claude/scripts/spec-ground.cjs +189 -83
- package/src/claude/scripts/validate-spec-output.cjs +294 -30
- package/src/claude/scripts/workflow-policy.cjs +609 -0
- package/src/claude/skills/ai-multimodal/SKILL.md +2 -2
- package/src/claude/skills/ai-multimodal/references/audio-processing.md +6 -6
- package/src/claude/skills/ai-multimodal/references/video-analysis.md +1 -1
- package/src/claude/skills/backend-development/SKILL.md +4 -4
- package/src/claude/skills/backend-development/references/backend-architecture.md +1 -1
- package/src/claude/skills/backend-development/references/backend-devops.md +2 -2
- package/src/claude/skills/backend-development/references/backend-mindset.md +3 -3
- package/src/claude/skills/backend-development/references/backend-performance.md +5 -5
- package/src/claude/skills/backend-development/references/backend-security.md +55 -3
- package/src/claude/skills/backend-development/references/backend-technologies.md +5 -5
- package/src/claude/skills/backend-development/references/backend-testing.md +2 -2
- package/src/claude/skills/brainstorm/SKILL.md +2 -3
- package/src/claude/skills/code-review/SKILL.md +12 -8
- package/src/claude/skills/code-review/references/verification-gate.md +4 -4
- package/src/claude/skills/debug/SKILL.md +2 -0
- package/src/claude/skills/develop/SKILL.md +100 -52
- package/src/claude/skills/develop/references/parallel-waves.md +105 -26
- package/src/claude/skills/develop/references/quality-gate.md +78 -38
- package/src/claude/skills/develop/references/subagent-patterns.md +2 -2
- package/src/claude/skills/git/SKILL.md +44 -50
- package/src/claude/skills/hotfix/SKILL.md +10 -8
- package/src/claude/skills/hotfix/references/parallel-patterns.md +2 -2
- package/src/claude/skills/hotfix/references/review-cycle.md +26 -21
- package/src/claude/skills/inspect/SKILL.md +12 -30
- package/src/claude/skills/mobile-development/SKILL.md +12 -12
- package/src/claude/skills/mobile-development/references/mobile-android.md +7 -7
- package/src/claude/skills/mobile-development/references/mobile-best-practices.md +22 -23
- package/src/claude/skills/mobile-development/references/mobile-frameworks.md +13 -13
- package/src/claude/skills/mobile-development/references/mobile-ios.md +1 -1
- package/src/claude/skills/mobile-development/references/mobile-mindset.md +17 -19
- package/src/claude/skills/question/SKILL.md +13 -13
- package/src/claude/skills/research/SKILL.md +2 -2
- package/src/claude/skills/specs/SKILL.md +32 -15
- package/src/claude/skills/specs/references/review.md +3 -3
- package/src/claude/skills/specs/rules/tasks-generation.md +1 -1
- package/src/claude/skills/specs/templates/spec-state.json +7 -3
- package/src/claude/skills/sync/SKILL.md +26 -1
- package/src/claude/skills/sync/references/sync-protocols.md +3 -3
- package/src/claude/skills/test/SKILL.md +12 -4
- package/src/claude/skills/test/references/failure-triage.md +17 -17
- package/src/claude/skills/test/references/test-memory.md +2 -2
- package/src/codex/AGENTS.md +16 -24
- package/src/codex/hooks/lib/spec-receipt.cjs +57 -4
- package/src/codex/hooks/rules.cjs +27 -26
- package/src/codex/hooks/spec-gate.cjs +19 -8
- package/src/codex/hooks/spec-state.cjs +10 -1
- package/src/common/AGENTS.md +43 -0
- package/src/opencode/AGENTS.md +21 -121
- package/src/opencode/plugins/rules.ts +26 -34
- package/src/opencode/plugins/spec-gate.ts +283 -0
- package/src/claude/skills/inspect/references/external-gemini-inspection.md +0 -169
|
@@ -2,40 +2,31 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* CafeKit spec GROUNDING checker (Specs v2, Layer 2).
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* Why active-grep instead of opt-in: field tests showed opt-in checks get
|
|
13
|
-
* skipped by the author. Grounding runs unconditionally over whatever paths the
|
|
14
|
-
* tasks already declare, so it cannot be dodged by choosing a different format.
|
|
15
|
-
*
|
|
16
|
-
* Usage:
|
|
17
|
-
* node spec-ground.cjs <specDir> [--root <work-context>]
|
|
18
|
-
*
|
|
19
|
-
* <specDir> : path to specs/<feature> (relative or absolute)
|
|
20
|
-
* --root : work tree the paths resolve against. Default: two levels up from
|
|
21
|
-
* specDir (…/<root>/specs/<feature> -> <root>). Override for
|
|
22
|
-
* monorepos where the spec lives apart from the code.
|
|
23
|
-
*
|
|
24
|
-
* Exit: 0 = GROUNDED, 1 = FAIL (missing path), 2 = usage error.
|
|
5
|
+
* Verifies every task Related Files declaration against the real work tree.
|
|
6
|
+
* Paths are relative, actions are explicit, and globs must match at least one
|
|
7
|
+
* filesystem entry. Create on existing paths is an error. Lifecycle ordering
|
|
8
|
+
* (Create before Modify/Delete/Read) is enforced within task and via
|
|
9
|
+
* dependency DAG for cross-task references; cross-task producer/consumer
|
|
10
|
+
* requires a valid spec.json task_registry (fail-closed).
|
|
25
11
|
*/
|
|
26
12
|
|
|
27
13
|
const fs = require('fs');
|
|
28
14
|
const path = require('path');
|
|
29
15
|
|
|
16
|
+
const ACTIONS = new Set(['create', 'modify', 'delete', 'read']);
|
|
17
|
+
|
|
30
18
|
function usage() {
|
|
31
19
|
console.error('Usage: node spec-ground.cjs <specDir> [--root <work-context>]');
|
|
32
20
|
}
|
|
33
21
|
|
|
34
22
|
function parseArgs(argv) {
|
|
35
23
|
const args = { specDir: null, root: null };
|
|
36
|
-
for (let i = 2; i < argv.length; i
|
|
37
|
-
if (argv[i] === '--root')
|
|
38
|
-
|
|
24
|
+
for (let i = 2; i < argv.length; i += 1) {
|
|
25
|
+
if (argv[i] === '--root') {
|
|
26
|
+
args.root = argv[++i];
|
|
27
|
+
if (!args.root) throw new Error('--root requires a value');
|
|
28
|
+
} else if (!args.specDir) args.specDir = argv[i];
|
|
29
|
+
else throw new Error(`Unknown argument: ${argv[i]}`);
|
|
39
30
|
}
|
|
40
31
|
return args;
|
|
41
32
|
}
|
|
@@ -45,102 +36,217 @@ function listTaskFiles(specDir) {
|
|
|
45
36
|
if (!fs.existsSync(tasksDir)) return [];
|
|
46
37
|
return fs
|
|
47
38
|
.readdirSync(tasksDir, { withFileTypes: true })
|
|
48
|
-
.filter((
|
|
49
|
-
.map((
|
|
39
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
|
|
40
|
+
.map((entry) => path.join(tasksDir, entry.name))
|
|
50
41
|
.sort();
|
|
51
42
|
}
|
|
52
43
|
|
|
53
|
-
/**
|
|
54
|
-
* Pull rows out of a markdown `## Related Files` table. Returns
|
|
55
|
-
* [{ path, action, taskFile }]. Tables look like:
|
|
56
|
-
* | Path | Action | Description |
|
|
57
|
-
* | `front/x.vue` | Modify | ... |
|
|
58
|
-
*/
|
|
59
44
|
function parseRelatedFiles(content, taskFile) {
|
|
60
|
-
const rows = [];
|
|
61
45
|
const lines = content.split('\n');
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
46
|
+
const headingIndex = lines.findIndex((line) => /^##+\s+Related Files\s*$/i.test(line));
|
|
47
|
+
if (headingIndex < 0) return { present: false, rows: [] };
|
|
48
|
+
|
|
49
|
+
const rows = [];
|
|
50
|
+
for (let index = headingIndex + 1; index < lines.length; index += 1) {
|
|
51
|
+
const line = lines[index];
|
|
52
|
+
if (/^##+\s+/.test(line)) break;
|
|
67
53
|
if (!line.trim().startsWith('|')) continue;
|
|
54
|
+
const cells = line.split('|').map((cell) => cell.trim());
|
|
55
|
+
const filePath = (cells[1] || '').replace(/`/g, '').trim();
|
|
56
|
+
const action = (cells[2] || '').replace(/`/g, '').trim().toLowerCase();
|
|
57
|
+
if (!filePath || filePath.toLowerCase() === 'path' || /^-+$/.test(filePath)) continue;
|
|
58
|
+
rows.push({ path: filePath, action, taskFile: path.basename(taskFile) });
|
|
59
|
+
}
|
|
60
|
+
return { present: true, rows };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function unsafePath(filePath) {
|
|
64
|
+
return path.isAbsolute(filePath)
|
|
65
|
+
|| /^[A-Za-z]:[\\/]/.test(filePath)
|
|
66
|
+
|| filePath.split(/[\\/]+/).includes('..');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function expandBraces(pattern) {
|
|
70
|
+
const match = pattern.match(/\{([^{}]*)\}/);
|
|
71
|
+
if (!match) return [pattern];
|
|
72
|
+
return match[1].split(',').flatMap((part) => expandBraces(
|
|
73
|
+
`${pattern.slice(0, match.index)}${part}${pattern.slice(match.index + match[0].length)}`,
|
|
74
|
+
));
|
|
75
|
+
}
|
|
68
76
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
-
if (
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
+
function globRegExp(pattern) {
|
|
78
|
+
let source = '^';
|
|
79
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
80
|
+
const char = pattern[index];
|
|
81
|
+
if (char === '*') {
|
|
82
|
+
if (pattern[index + 1] === '*') {
|
|
83
|
+
index += 1;
|
|
84
|
+
if (pattern[index + 1] === '/') index += 1;
|
|
85
|
+
source += '.*';
|
|
86
|
+
} else source += '[^/]*';
|
|
87
|
+
} else if (char === '?') source += '[^/]';
|
|
88
|
+
else source += char.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
77
89
|
}
|
|
78
|
-
return
|
|
90
|
+
return new RegExp(`${source}$`);
|
|
79
91
|
}
|
|
80
92
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
|
|
93
|
+
function relativeEntries(root) {
|
|
94
|
+
const entries = [];
|
|
95
|
+
function walk(current, relative) {
|
|
96
|
+
let children;
|
|
97
|
+
try { children = fs.readdirSync(current, { withFileTypes: true }); } catch { return; }
|
|
98
|
+
for (const entry of children) {
|
|
99
|
+
const nextRelative = relative ? `${relative}/${entry.name}` : entry.name;
|
|
100
|
+
const next = path.join(current, entry.name);
|
|
101
|
+
entries.push(nextRelative);
|
|
102
|
+
if (entry.isDirectory() && !entry.isSymbolicLink()) walk(next, nextRelative);
|
|
103
|
+
}
|
|
88
104
|
}
|
|
89
|
-
|
|
105
|
+
walk(root, '');
|
|
106
|
+
return entries;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function matchingEntries(root, filePath) {
|
|
110
|
+
if (!/[{}*?]/.test(filePath)) return fs.existsSync(path.join(root, filePath)) ? [filePath] : [];
|
|
111
|
+
const candidates = relativeEntries(root);
|
|
112
|
+
const matches = new Set();
|
|
113
|
+
for (const expanded of expandBraces(filePath)) {
|
|
114
|
+
const matcher = globRegExp(expanded.replaceAll('\\', '/'));
|
|
115
|
+
for (const candidate of candidates) if (matcher.test(candidate)) matches.add(candidate);
|
|
116
|
+
}
|
|
117
|
+
return [...matches];
|
|
90
118
|
}
|
|
91
119
|
|
|
92
120
|
function main() {
|
|
93
|
-
|
|
94
|
-
|
|
121
|
+
let args;
|
|
122
|
+
try { args = parseArgs(process.argv); } catch (error) {
|
|
123
|
+
usage();
|
|
124
|
+
console.error(`- ${error.message}`);
|
|
125
|
+
process.exit(2);
|
|
126
|
+
}
|
|
127
|
+
if (!args.specDir) { usage(); process.exit(2); }
|
|
95
128
|
|
|
96
|
-
const specDir = path.resolve(process.cwd(),
|
|
129
|
+
const specDir = path.resolve(process.cwd(), args.specDir);
|
|
97
130
|
if (!fs.existsSync(specDir)) {
|
|
98
|
-
console.error(`FAIL ${
|
|
131
|
+
console.error(`FAIL ${args.specDir}\n- spec directory does not exist`);
|
|
99
132
|
process.exit(1);
|
|
100
133
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
? path.resolve(process.cwd(), rootInput)
|
|
134
|
+
const root = args.root
|
|
135
|
+
? path.resolve(process.cwd(), args.root)
|
|
104
136
|
: path.resolve(specDir, '..', '..');
|
|
105
137
|
|
|
106
138
|
const taskFiles = listTaskFiles(specDir);
|
|
107
139
|
const allRows = [];
|
|
108
|
-
|
|
109
|
-
|
|
140
|
+
const rowsByTask = new Map();
|
|
141
|
+
const errors = [];
|
|
142
|
+
const warnings = [];
|
|
143
|
+
for (const taskFile of taskFiles) {
|
|
144
|
+
const section = parseRelatedFiles(fs.readFileSync(taskFile, 'utf8'), taskFile);
|
|
145
|
+
const basename = path.basename(taskFile);
|
|
146
|
+
if (!rowsByTask.has(basename)) rowsByTask.set(basename, []);
|
|
147
|
+
if (!section.present) errors.push(`${basename}: missing Related Files section`);
|
|
148
|
+
else if (section.rows.length === 0) errors.push(`${basename}: Related Files section must not be empty`);
|
|
149
|
+
for (const row of section.rows) {
|
|
150
|
+
if (!ACTIONS.has(row.action)) errors.push(`${row.taskFile}: unsupported Related Files action "${row.action}" for ${row.path}`);
|
|
151
|
+
if (unsafePath(row.path)) errors.push(`${row.taskFile}: Related Files path must be relative and stay within work root: ${row.path}`);
|
|
152
|
+
allRows.push(row);
|
|
153
|
+
rowsByTask.get(basename).push(row);
|
|
154
|
+
}
|
|
110
155
|
}
|
|
111
156
|
|
|
112
|
-
//
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
157
|
+
// Lifecycle ordering: Create must precede Modify/Delete/Read within same task
|
|
158
|
+
for (const [taskFile, rows] of rowsByTask.entries()) {
|
|
159
|
+
const createIndexByPath = new Map();
|
|
160
|
+
rows.forEach((row, idx) => {
|
|
161
|
+
if (row.action === 'create' && !createIndexByPath.has(row.path)) createIndexByPath.set(row.path, idx);
|
|
162
|
+
});
|
|
163
|
+
rows.forEach((row, idx) => {
|
|
164
|
+
if (!['modify', 'delete', 'read'].includes(row.action)) return;
|
|
165
|
+
if (!createIndexByPath.has(row.path)) return;
|
|
166
|
+
const createIdx = createIndexByPath.get(row.path);
|
|
167
|
+
if (createIdx > idx) {
|
|
168
|
+
errors.push(`${taskFile}: Create must precede ${row.action} for ${row.path} within Related Files order`);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
}
|
|
116
172
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
173
|
+
// Cross-task lifecycle via spec.json dependency DAG (fail-closed when registry missing)
|
|
174
|
+
let taskRegistry = null;
|
|
175
|
+
try {
|
|
176
|
+
const specJson = JSON.parse(fs.readFileSync(path.join(specDir, 'spec.json'), 'utf8'));
|
|
177
|
+
if (specJson.task_registry && typeof specJson.task_registry === 'object' && !Array.isArray(specJson.task_registry)) taskRegistry = specJson.task_registry;
|
|
178
|
+
} catch {}
|
|
179
|
+
const producers = new Map();
|
|
180
|
+
for (const [taskFile, rows] of rowsByTask.entries()) {
|
|
181
|
+
for (const row of rows) {
|
|
182
|
+
if (row.action !== 'create') continue;
|
|
183
|
+
if (!producers.has(row.path)) producers.set(row.path, { taskFile });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const crossTaskConsumers = [];
|
|
187
|
+
for (const [taskFile, rows] of rowsByTask.entries()) {
|
|
188
|
+
for (const row of rows) {
|
|
189
|
+
if (!['modify', 'delete', 'read'].includes(row.action)) continue;
|
|
190
|
+
const producer = producers.get(row.path);
|
|
191
|
+
if (!producer) continue;
|
|
192
|
+
if (producer.taskFile === taskFile) continue;
|
|
193
|
+
crossTaskConsumers.push({ taskFile, row, producer });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (crossTaskConsumers.length > 0 && !taskRegistry) {
|
|
197
|
+
for (const { taskFile, row, producer } of crossTaskConsumers) {
|
|
198
|
+
errors.push(`${taskFile}: ${row.action} of ${row.path} has producer ${producer.taskFile} but spec.json task_registry is missing or invalid — cannot verify cross-task dependency (fail-closed)`);
|
|
199
|
+
}
|
|
200
|
+
} else if (taskRegistry) {
|
|
201
|
+
// Build basename-indexed dependencies
|
|
202
|
+
const dependencies = new Map();
|
|
203
|
+
for (const [key, entry] of Object.entries(taskRegistry)) {
|
|
204
|
+
const base = path.basename(key);
|
|
205
|
+
const deps = new Set((entry.dependencies || []).map((d) => path.basename(d)));
|
|
206
|
+
dependencies.set(base, deps);
|
|
207
|
+
}
|
|
208
|
+
function dependsOn(taskBase, ancestorBase, seen = new Set()) {
|
|
209
|
+
if (taskBase === ancestorBase) return true;
|
|
210
|
+
if (seen.has(taskBase)) return false;
|
|
211
|
+
seen.add(taskBase);
|
|
212
|
+
const deps = dependencies.get(taskBase) || new Set();
|
|
213
|
+
for (const dep of deps) if (dependsOn(dep, ancestorBase, seen)) return true;
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
for (const { taskFile, row, producer } of crossTaskConsumers) {
|
|
217
|
+
if (!dependsOn(taskFile, producer.taskFile)) {
|
|
218
|
+
errors.push(`${taskFile}: ${row.action} of ${row.path} must depend on creator ${producer.taskFile}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
120
222
|
|
|
223
|
+
const createdPaths = new Set(allRows.filter((row) => row.action === 'create').map((row) => row.path));
|
|
224
|
+
let checked = 0;
|
|
121
225
|
for (const row of allRows) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
226
|
+
if (!ACTIONS.has(row.action) || unsafePath(row.path)) continue;
|
|
227
|
+
const matches = matchingEntries(root, row.path);
|
|
228
|
+
if (/[{}*?]/.test(row.path) && matches.length === 0) {
|
|
229
|
+
errors.push(`${row.taskFile}: glob matches no paths in work tree: ${row.path}`);
|
|
125
230
|
continue;
|
|
126
231
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
232
|
+
if (row.action === 'create') {
|
|
233
|
+
if (matches.length > 0) errors.push(`${row.taskFile}: Create path already exists in work tree (would overwrite): ${row.path} — use Modify instead of Create for existing files`);
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
checked += 1;
|
|
237
|
+
if (matches.length === 0 && !createdPaths.has(row.path)) {
|
|
130
238
|
errors.push(`${row.taskFile}: ${row.action} path not found in work tree: ${row.path}`);
|
|
131
239
|
}
|
|
132
240
|
}
|
|
133
241
|
|
|
134
|
-
for (const
|
|
135
|
-
|
|
136
|
-
const rel = path.relative(process.cwd(), specDir) || specDir;
|
|
242
|
+
for (const warning of warnings) console.warn(`[WARN] ${warning}`);
|
|
243
|
+
const relativeSpec = path.relative(process.cwd(), specDir) || specDir;
|
|
137
244
|
if (errors.length > 0) {
|
|
138
|
-
console.error(`FAIL ${
|
|
139
|
-
for (const
|
|
140
|
-
console.error(`\n${errors.length} missing / ${checked} checked path(s).`);
|
|
245
|
+
console.error(`FAIL ${relativeSpec} (root: ${root})`);
|
|
246
|
+
for (const error of errors) console.error(`- ${error}`);
|
|
141
247
|
process.exit(1);
|
|
142
248
|
}
|
|
143
|
-
console.log(`GROUNDED ${
|
|
249
|
+
console.log(`GROUNDED ${relativeSpec} (${checked} path(s) verified against ${root})`);
|
|
144
250
|
}
|
|
145
251
|
|
|
146
252
|
main();
|