cipher-security 2.0.8 → 2.2.0
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/bin/cipher.js +11 -1
- package/lib/agent-runtime/handlers/architect.js +199 -0
- package/lib/agent-runtime/handlers/base.js +240 -0
- package/lib/agent-runtime/handlers/blue.js +220 -0
- package/lib/agent-runtime/handlers/incident.js +161 -0
- package/lib/agent-runtime/handlers/privacy.js +190 -0
- package/lib/agent-runtime/handlers/purple.js +209 -0
- package/lib/agent-runtime/handlers/recon.js +174 -0
- package/lib/agent-runtime/handlers/red.js +246 -0
- package/lib/agent-runtime/handlers/researcher.js +170 -0
- package/lib/agent-runtime/handlers.js +35 -0
- package/lib/agent-runtime/index.js +196 -0
- package/lib/agent-runtime/parser.js +316 -0
- package/lib/analyze/consistency.js +566 -0
- package/lib/analyze/constitution.js +110 -0
- package/lib/analyze/sharding.js +251 -0
- package/lib/autonomous/agent-tool.js +165 -0
- package/lib/autonomous/feedback-loop.js +13 -6
- package/lib/autonomous/framework.js +17 -0
- package/lib/autonomous/handoff.js +506 -0
- package/lib/autonomous/modes/blue.js +26 -0
- package/lib/autonomous/modes/red.js +585 -0
- package/lib/autonomous/modes/researcher.js +322 -0
- package/lib/autonomous/researcher.js +12 -45
- package/lib/autonomous/runner.js +9 -537
- package/lib/benchmark/agent.js +88 -26
- package/lib/benchmark/baselines.js +3 -0
- package/lib/benchmark/claude-code-solver.js +254 -0
- package/lib/benchmark/cognitive.js +283 -0
- package/lib/benchmark/index.js +12 -2
- package/lib/benchmark/knowledge.js +281 -0
- package/lib/benchmark/llm.js +156 -15
- package/lib/benchmark/models.js +5 -2
- package/lib/benchmark/nyu-ctf.js +192 -0
- package/lib/benchmark/overthewire.js +347 -0
- package/lib/benchmark/picoctf.js +281 -0
- package/lib/benchmark/prompts.js +280 -0
- package/lib/benchmark/registry.js +219 -0
- package/lib/benchmark/remote-solver.js +356 -0
- package/lib/benchmark/remote-target.js +263 -0
- package/lib/benchmark/reporter.js +35 -0
- package/lib/benchmark/runner.js +174 -10
- package/lib/benchmark/sandbox.js +35 -0
- package/lib/benchmark/scorer.js +22 -4
- package/lib/benchmark/solver.js +34 -1
- package/lib/benchmark/tools.js +262 -16
- package/lib/commands.js +9 -0
- package/lib/execution/council.js +434 -0
- package/lib/execution/parallel.js +292 -0
- package/lib/gates/circuit-breaker.js +135 -0
- package/lib/gates/confidence.js +302 -0
- package/lib/gates/corrections.js +219 -0
- package/lib/gates/self-check.js +245 -0
- package/lib/gateway/commands.js +727 -0
- package/lib/guardrails/engine.js +364 -0
- package/lib/mcp/server.js +349 -3
- package/lib/memory/compressor.js +94 -7
- package/lib/pipeline/hooks.js +288 -0
- package/lib/pipeline/index.js +11 -0
- package/lib/review/budget.js +210 -0
- package/lib/review/engine.js +526 -0
- package/lib/review/layers/acceptance-auditor.js +279 -0
- package/lib/review/layers/blind-hunter.js +500 -0
- package/lib/review/layers/defense-in-depth.js +209 -0
- package/lib/review/layers/edge-case-hunter.js +266 -0
- package/lib/review/panel.js +519 -0
- package/lib/review/two-stage.js +244 -0
- package/lib/session/cost-tracker.js +203 -0
- package/lib/session/logger.js +349 -0
- package/package.json +1 -1
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
// Copyright (c) 2026 defconxt. All rights reserved.
|
|
2
|
+
// Licensed under AGPL-3.0 — see LICENSE file for details.
|
|
3
|
+
// CIPHER is a trademark of defconxt.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* CIPHER Cross-Artifact Consistency Analyzer
|
|
7
|
+
*
|
|
8
|
+
* Scans the CIPHER artifact ecosystem (commands, agents, skills, knowledge,
|
|
9
|
+
* CLAUDE.md) for stale references, orphan artifacts, conflicting instructions,
|
|
10
|
+
* and coverage gaps.
|
|
11
|
+
*
|
|
12
|
+
* @module analyze/consistency
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { readFileSync, readdirSync, existsSync, statSync } from 'node:fs';
|
|
16
|
+
import { join, resolve, dirname, basename } from 'node:path';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
|
|
19
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Finding types
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
/** @enum {string} */
|
|
26
|
+
export const IssueSeverity = Object.freeze({
|
|
27
|
+
ERROR: 'error',
|
|
28
|
+
WARNING: 'warning',
|
|
29
|
+
INFO: 'info',
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
/** @enum {string} */
|
|
33
|
+
export const IssueCategory = Object.freeze({
|
|
34
|
+
STALE_REF: 'stale-reference',
|
|
35
|
+
ORPHAN: 'orphan-artifact',
|
|
36
|
+
MISSING: 'missing-artifact',
|
|
37
|
+
MODE_MISMATCH: 'mode-mismatch',
|
|
38
|
+
COVERAGE_GAP: 'coverage-gap',
|
|
39
|
+
DUPLICATE: 'duplicate',
|
|
40
|
+
STRUCTURE: 'structure-issue',
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A single consistency issue.
|
|
45
|
+
*/
|
|
46
|
+
export class ConsistencyIssue {
|
|
47
|
+
/**
|
|
48
|
+
* @param {object} opts
|
|
49
|
+
* @param {string} opts.category - Issue category
|
|
50
|
+
* @param {string} opts.severity - error|warning|info
|
|
51
|
+
* @param {string} opts.file - File where the issue was found
|
|
52
|
+
* @param {number} [opts.line] - Line number
|
|
53
|
+
* @param {string} opts.message - Human-readable description
|
|
54
|
+
* @param {string} [opts.reference] - The stale/missing reference
|
|
55
|
+
* @param {string} [opts.suggestion] - How to fix
|
|
56
|
+
*/
|
|
57
|
+
constructor(opts = {}) {
|
|
58
|
+
this.category = opts.category ?? IssueCategory.STRUCTURE;
|
|
59
|
+
this.severity = opts.severity ?? IssueSeverity.WARNING;
|
|
60
|
+
this.file = opts.file ?? '';
|
|
61
|
+
this.line = opts.line ?? 0;
|
|
62
|
+
this.message = opts.message ?? '';
|
|
63
|
+
this.reference = opts.reference ?? '';
|
|
64
|
+
this.suggestion = opts.suggestion ?? '';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
toReport() {
|
|
68
|
+
const loc = this.line ? `${this.file}:${this.line}` : this.file;
|
|
69
|
+
const sev = this.severity.toUpperCase();
|
|
70
|
+
const lines = [`[${sev}] ${this.category} — ${loc}`, ` ${this.message}`];
|
|
71
|
+
if (this.reference) lines.push(` Reference: ${this.reference}`);
|
|
72
|
+
if (this.suggestion) lines.push(` Fix: ${this.suggestion}`);
|
|
73
|
+
return lines.join('\n');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// Artifact Indexer
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Index all CIPHER artifacts from a repo root.
|
|
83
|
+
*/
|
|
84
|
+
export class ArtifactIndex {
|
|
85
|
+
/**
|
|
86
|
+
* @param {string} repoRoot - Path to CIPHER repo root
|
|
87
|
+
*/
|
|
88
|
+
constructor(repoRoot) {
|
|
89
|
+
this.root = repoRoot;
|
|
90
|
+
/** @type {Map<string, string>} command name → file path */
|
|
91
|
+
this.commands = new Map();
|
|
92
|
+
/** @type {Map<string, string>} agent name → file path */
|
|
93
|
+
this.agents = new Map();
|
|
94
|
+
/** @type {Set<string>} knowledge filenames (basename) */
|
|
95
|
+
this.knowledgeFiles = new Set();
|
|
96
|
+
/** @type {Map<string, {hasSkillMd: boolean, hasAgentJs: boolean, skillCount: number}>} */
|
|
97
|
+
this.skills = new Map();
|
|
98
|
+
/** @type {Set<string>} modes defined in CLAUDE.md */
|
|
99
|
+
this.modes = new Set();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Scan all artifact directories and populate the index.
|
|
104
|
+
*/
|
|
105
|
+
index() {
|
|
106
|
+
this._indexModes();
|
|
107
|
+
this._indexCommands();
|
|
108
|
+
this._indexAgents();
|
|
109
|
+
this._indexKnowledge();
|
|
110
|
+
this._indexSkills();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
_indexModes() {
|
|
114
|
+
const claudePath = join(this.root, 'CLAUDE.md');
|
|
115
|
+
if (!existsSync(claudePath)) return;
|
|
116
|
+
const content = readFileSync(claudePath, 'utf-8');
|
|
117
|
+
// Extract modes from ### `[MODE: X]` headers
|
|
118
|
+
const modeRe = /\[MODE:\s*([A-Z]+)\]/g;
|
|
119
|
+
let match;
|
|
120
|
+
while ((match = modeRe.exec(content)) !== null) {
|
|
121
|
+
this.modes.add(match[1]);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
_indexCommands() {
|
|
126
|
+
const dir = join(this.root, 'commands');
|
|
127
|
+
if (!existsSync(dir)) return;
|
|
128
|
+
for (const file of readdirSync(dir)) {
|
|
129
|
+
if (!file.endsWith('.md')) continue;
|
|
130
|
+
const name = file.replace(/\.md$/, '');
|
|
131
|
+
this.commands.set(name, join(dir, file));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
_indexAgents() {
|
|
136
|
+
const dir = join(this.root, 'agents');
|
|
137
|
+
if (!existsSync(dir)) return;
|
|
138
|
+
for (const file of readdirSync(dir)) {
|
|
139
|
+
if (!file.endsWith('.md')) continue;
|
|
140
|
+
const name = file.replace(/\.md$/, '');
|
|
141
|
+
this.agents.set(name, join(dir, file));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
_indexKnowledge() {
|
|
146
|
+
const dir = join(this.root, 'knowledge');
|
|
147
|
+
if (!existsSync(dir)) return;
|
|
148
|
+
for (const file of readdirSync(dir)) {
|
|
149
|
+
if (!file.endsWith('.md')) continue;
|
|
150
|
+
this.knowledgeFiles.add(file);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
_indexSkills() {
|
|
155
|
+
const dir = join(this.root, 'skills');
|
|
156
|
+
if (!existsSync(dir)) return;
|
|
157
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
158
|
+
if (!entry.isDirectory()) continue;
|
|
159
|
+
const skillDir = join(dir, entry.name);
|
|
160
|
+
const hasSkillMd = existsSync(join(skillDir, 'SKILL.md'));
|
|
161
|
+
const techniquesDir = join(skillDir, 'techniques');
|
|
162
|
+
let hasAgentJs = false;
|
|
163
|
+
let skillCount = 0;
|
|
164
|
+
if (existsSync(techniquesDir)) {
|
|
165
|
+
for (const sub of readdirSync(techniquesDir, { withFileTypes: true })) {
|
|
166
|
+
if (sub.isDirectory()) {
|
|
167
|
+
skillCount++;
|
|
168
|
+
const scriptsDir = join(techniquesDir, sub.name, 'scripts');
|
|
169
|
+
if (existsSync(join(scriptsDir, 'agent.js')) || existsSync(join(techniquesDir, sub.name, 'agent.js'))) {
|
|
170
|
+
hasAgentJs = true;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
this.skills.set(entry.name, { hasSkillMd, hasAgentJs, skillCount });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
// Consistency Checks
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Check for stale knowledge references in commands.
|
|
186
|
+
* Commands reference knowledge as `knowledge/filename.md`.
|
|
187
|
+
*
|
|
188
|
+
* @param {ArtifactIndex} index
|
|
189
|
+
* @returns {ConsistencyIssue[]}
|
|
190
|
+
*/
|
|
191
|
+
function checkCommandKnowledgeRefs(index) {
|
|
192
|
+
const issues = [];
|
|
193
|
+
for (const [name, filePath] of index.commands) {
|
|
194
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
195
|
+
const lines = content.split('\n');
|
|
196
|
+
for (let i = 0; i < lines.length; i++) {
|
|
197
|
+
const knowledgeRe = /knowledge\/([a-zA-Z0-9_-]+\.md)/g;
|
|
198
|
+
let match;
|
|
199
|
+
while ((match = knowledgeRe.exec(lines[i])) !== null) {
|
|
200
|
+
const ref = match[1];
|
|
201
|
+
if (!index.knowledgeFiles.has(ref)) {
|
|
202
|
+
issues.push(new ConsistencyIssue({
|
|
203
|
+
category: IssueCategory.STALE_REF,
|
|
204
|
+
severity: IssueSeverity.ERROR,
|
|
205
|
+
file: `commands/${name}.md`,
|
|
206
|
+
line: i + 1,
|
|
207
|
+
message: `References knowledge file that does not exist.`,
|
|
208
|
+
reference: `knowledge/${ref}`,
|
|
209
|
+
suggestion: `Remove the reference or create knowledge/${ref}.`,
|
|
210
|
+
}));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return issues;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Check for stale knowledge references in agents.
|
|
220
|
+
* Agents reference knowledge as bare filenames in a YAML list.
|
|
221
|
+
*
|
|
222
|
+
* @param {ArtifactIndex} index
|
|
223
|
+
* @returns {ConsistencyIssue[]}
|
|
224
|
+
*/
|
|
225
|
+
function checkAgentKnowledgeRefs(index) {
|
|
226
|
+
const issues = [];
|
|
227
|
+
for (const [name, filePath] of index.agents) {
|
|
228
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
229
|
+
const lines = content.split('\n');
|
|
230
|
+
let inKnowledge = false;
|
|
231
|
+
for (let i = 0; i < lines.length; i++) {
|
|
232
|
+
const line = lines[i];
|
|
233
|
+
if (/^knowledge:/.test(line)) {
|
|
234
|
+
inKnowledge = true;
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (inKnowledge) {
|
|
238
|
+
const itemMatch = line.match(/^\s+-\s+(.+\.md)\s*$/);
|
|
239
|
+
if (itemMatch) {
|
|
240
|
+
const ref = itemMatch[1].trim();
|
|
241
|
+
if (!index.knowledgeFiles.has(ref)) {
|
|
242
|
+
issues.push(new ConsistencyIssue({
|
|
243
|
+
category: IssueCategory.STALE_REF,
|
|
244
|
+
severity: IssueSeverity.ERROR,
|
|
245
|
+
file: `agents/${name}.md`,
|
|
246
|
+
line: i + 1,
|
|
247
|
+
message: `References knowledge file that does not exist.`,
|
|
248
|
+
reference: ref,
|
|
249
|
+
suggestion: `Remove from knowledge list or create knowledge/${ref}.`,
|
|
250
|
+
}));
|
|
251
|
+
}
|
|
252
|
+
} else if (!/^\s+-/.test(line) && line.trim() !== '') {
|
|
253
|
+
inKnowledge = false; // End of YAML list
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return issues;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Check for orphan knowledge files — not referenced by any command or agent.
|
|
263
|
+
*
|
|
264
|
+
* @param {ArtifactIndex} index
|
|
265
|
+
* @returns {ConsistencyIssue[]}
|
|
266
|
+
*/
|
|
267
|
+
function checkOrphanKnowledge(index) {
|
|
268
|
+
const issues = [];
|
|
269
|
+
// Collect all referenced knowledge files
|
|
270
|
+
const referenced = new Set();
|
|
271
|
+
|
|
272
|
+
for (const [, filePath] of index.commands) {
|
|
273
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
274
|
+
const re = /knowledge\/([a-zA-Z0-9_-]+\.md)/g;
|
|
275
|
+
let match;
|
|
276
|
+
while ((match = re.exec(content)) !== null) {
|
|
277
|
+
referenced.add(match[1]);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
for (const [, filePath] of index.agents) {
|
|
282
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
283
|
+
const lines = content.split('\n');
|
|
284
|
+
let inKnowledge = false;
|
|
285
|
+
for (const line of lines) {
|
|
286
|
+
if (/^knowledge:/.test(line)) { inKnowledge = true; continue; }
|
|
287
|
+
if (inKnowledge) {
|
|
288
|
+
const itemMatch = line.match(/^\s+-\s+(.+\.md)\s*$/);
|
|
289
|
+
if (itemMatch) referenced.add(itemMatch[1].trim());
|
|
290
|
+
else if (!/^\s+-/.test(line) && line.trim() !== '') inKnowledge = false;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Also check if knowledge files reference each other
|
|
296
|
+
for (const kf of index.knowledgeFiles) {
|
|
297
|
+
if (kf === '00-MASTER-INDEX.md') continue; // Index file is expected to be standalone
|
|
298
|
+
if (!referenced.has(kf)) {
|
|
299
|
+
issues.push(new ConsistencyIssue({
|
|
300
|
+
category: IssueCategory.ORPHAN,
|
|
301
|
+
severity: IssueSeverity.INFO,
|
|
302
|
+
file: `knowledge/${kf}`,
|
|
303
|
+
message: `Knowledge file not referenced by any command or agent.`,
|
|
304
|
+
suggestion: `Add reference in a relevant command or agent, or remove if obsolete.`,
|
|
305
|
+
}));
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return issues;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Check for mode mismatches — agents/commands claiming modes not in CLAUDE.md.
|
|
313
|
+
*
|
|
314
|
+
* @param {ArtifactIndex} index
|
|
315
|
+
* @returns {ConsistencyIssue[]}
|
|
316
|
+
*/
|
|
317
|
+
function checkModeMismatches(index) {
|
|
318
|
+
const issues = [];
|
|
319
|
+
if (index.modes.size === 0) return issues;
|
|
320
|
+
|
|
321
|
+
// Check agents
|
|
322
|
+
for (const [name, filePath] of index.agents) {
|
|
323
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
324
|
+
const modeMatch = content.match(/^mode:\s*([A-Z]+)\s*$/m);
|
|
325
|
+
if (modeMatch) {
|
|
326
|
+
const mode = modeMatch[1];
|
|
327
|
+
if (!index.modes.has(mode)) {
|
|
328
|
+
issues.push(new ConsistencyIssue({
|
|
329
|
+
category: IssueCategory.MODE_MISMATCH,
|
|
330
|
+
severity: IssueSeverity.WARNING,
|
|
331
|
+
file: `agents/${name}.md`,
|
|
332
|
+
message: `Agent declares mode "${mode}" which is not defined in CLAUDE.md.`,
|
|
333
|
+
suggestion: `Add [MODE: ${mode}] to CLAUDE.md or fix the agent mode.`,
|
|
334
|
+
}));
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Check commands for MODE references
|
|
340
|
+
for (const [name, filePath] of index.commands) {
|
|
341
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
342
|
+
const modeRe = /\[MODE:\s*([A-Z]+)\]/g;
|
|
343
|
+
let match;
|
|
344
|
+
while ((match = modeRe.exec(content)) !== null) {
|
|
345
|
+
if (!index.modes.has(match[1])) {
|
|
346
|
+
issues.push(new ConsistencyIssue({
|
|
347
|
+
category: IssueCategory.MODE_MISMATCH,
|
|
348
|
+
severity: IssueSeverity.WARNING,
|
|
349
|
+
file: `commands/${name}.md`,
|
|
350
|
+
message: `Command references mode "${match[1]}" not defined in CLAUDE.md.`,
|
|
351
|
+
suggestion: `Fix the mode reference or add the mode to CLAUDE.md.`,
|
|
352
|
+
}));
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return issues;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Check for structural issues in skills.
|
|
361
|
+
*
|
|
362
|
+
* @param {ArtifactIndex} index
|
|
363
|
+
* @returns {ConsistencyIssue[]}
|
|
364
|
+
*/
|
|
365
|
+
function checkSkillStructure(index) {
|
|
366
|
+
const issues = [];
|
|
367
|
+
for (const [name, info] of index.skills) {
|
|
368
|
+
if (!info.hasSkillMd) {
|
|
369
|
+
issues.push(new ConsistencyIssue({
|
|
370
|
+
category: IssueCategory.MISSING,
|
|
371
|
+
severity: IssueSeverity.WARNING,
|
|
372
|
+
file: `skills/${name}/`,
|
|
373
|
+
message: `Skill directory missing SKILL.md.`,
|
|
374
|
+
suggestion: `Add SKILL.md with name, description, domain, tags.`,
|
|
375
|
+
}));
|
|
376
|
+
}
|
|
377
|
+
if (info.skillCount > 0 && !info.hasAgentJs) {
|
|
378
|
+
issues.push(new ConsistencyIssue({
|
|
379
|
+
category: IssueCategory.MISSING,
|
|
380
|
+
severity: IssueSeverity.INFO,
|
|
381
|
+
file: `skills/${name}/`,
|
|
382
|
+
message: `Skill has ${info.skillCount} techniques but no agent.js found in any.`,
|
|
383
|
+
suggestion: `Add agent.js to technique subdirectories for autonomous execution.`,
|
|
384
|
+
}));
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return issues;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Check for duplicate skill names (same directory name pattern).
|
|
392
|
+
*
|
|
393
|
+
* @param {ArtifactIndex} index
|
|
394
|
+
* @returns {ConsistencyIssue[]}
|
|
395
|
+
*/
|
|
396
|
+
function checkDuplicateSkills(index) {
|
|
397
|
+
// Look for cipher-X skills that overlap with X skills
|
|
398
|
+
const issues = [];
|
|
399
|
+
const names = [...index.skills.keys()];
|
|
400
|
+
const cipherPrefixed = names.filter((n) => n.startsWith('cipher-'));
|
|
401
|
+
for (const cp of cipherPrefixed) {
|
|
402
|
+
const base = cp.replace(/^cipher-/, '');
|
|
403
|
+
// Check if there's a non-prefixed version or similar
|
|
404
|
+
const similar = names.filter(
|
|
405
|
+
(n) => n !== cp && (n === base || n.endsWith(`-${base}`) || n.startsWith(`${base}-`)),
|
|
406
|
+
);
|
|
407
|
+
if (similar.length > 0) {
|
|
408
|
+
issues.push(new ConsistencyIssue({
|
|
409
|
+
category: IssueCategory.DUPLICATE,
|
|
410
|
+
severity: IssueSeverity.INFO,
|
|
411
|
+
file: `skills/${cp}/`,
|
|
412
|
+
message: `Possible overlap with: ${similar.join(', ')}.`,
|
|
413
|
+
suggestion: `Verify these are distinct skills, not duplicates.`,
|
|
414
|
+
}));
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return issues;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// ---------------------------------------------------------------------------
|
|
421
|
+
// ConsistencyAnalyzer — main orchestrator
|
|
422
|
+
// ---------------------------------------------------------------------------
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Run all consistency checks and return unified results.
|
|
426
|
+
*/
|
|
427
|
+
export class ConsistencyAnalyzer {
|
|
428
|
+
/**
|
|
429
|
+
* @param {string} [repoRoot] - Path to CIPHER repo root (auto-detected if omitted)
|
|
430
|
+
*/
|
|
431
|
+
constructor(repoRoot) {
|
|
432
|
+
this.root = repoRoot ?? this._findRoot();
|
|
433
|
+
this.index = new ArtifactIndex(this.root);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
_findRoot() {
|
|
437
|
+
let dir = resolve(__dirname, '..', '..');
|
|
438
|
+
for (let i = 0; i < 10; i++) {
|
|
439
|
+
if (existsSync(join(dir, 'skills')) && existsSync(join(dir, 'CLAUDE.md'))) {
|
|
440
|
+
return dir;
|
|
441
|
+
}
|
|
442
|
+
const parent = dirname(dir);
|
|
443
|
+
if (parent === dir) break;
|
|
444
|
+
dir = parent;
|
|
445
|
+
}
|
|
446
|
+
return process.cwd();
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Run all checks.
|
|
451
|
+
* @returns {ConsistencyResult}
|
|
452
|
+
*/
|
|
453
|
+
analyze() {
|
|
454
|
+
const t0 = Date.now();
|
|
455
|
+
this.index.index();
|
|
456
|
+
|
|
457
|
+
const issues = [
|
|
458
|
+
...checkCommandKnowledgeRefs(this.index),
|
|
459
|
+
...checkAgentKnowledgeRefs(this.index),
|
|
460
|
+
...checkOrphanKnowledge(this.index),
|
|
461
|
+
...checkModeMismatches(this.index),
|
|
462
|
+
...checkSkillStructure(this.index),
|
|
463
|
+
...checkDuplicateSkills(this.index),
|
|
464
|
+
];
|
|
465
|
+
|
|
466
|
+
// Sort: errors first, then warnings, then info
|
|
467
|
+
const sevRank = { error: 2, warning: 1, info: 0 };
|
|
468
|
+
issues.sort((a, b) => (sevRank[b.severity] ?? 0) - (sevRank[a.severity] ?? 0));
|
|
469
|
+
|
|
470
|
+
return new ConsistencyResult({
|
|
471
|
+
issues,
|
|
472
|
+
stats: {
|
|
473
|
+
commands: this.index.commands.size,
|
|
474
|
+
agents: this.index.agents.size,
|
|
475
|
+
knowledgeFiles: this.index.knowledgeFiles.size,
|
|
476
|
+
skills: this.index.skills.size,
|
|
477
|
+
modes: this.index.modes.size,
|
|
478
|
+
},
|
|
479
|
+
totalTime: Date.now() - t0,
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// ---------------------------------------------------------------------------
|
|
485
|
+
// ConsistencyResult
|
|
486
|
+
// ---------------------------------------------------------------------------
|
|
487
|
+
|
|
488
|
+
export class ConsistencyResult {
|
|
489
|
+
constructor({ issues = [], stats = {}, totalTime = 0 } = {}) {
|
|
490
|
+
this.issues = issues;
|
|
491
|
+
this.stats = stats;
|
|
492
|
+
this.totalTime = totalTime;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
get counts() {
|
|
496
|
+
const c = { error: 0, warning: 0, info: 0 };
|
|
497
|
+
for (const issue of this.issues) {
|
|
498
|
+
c[issue.severity] = (c[issue.severity] ?? 0) + 1;
|
|
499
|
+
}
|
|
500
|
+
return c;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
get summary() {
|
|
504
|
+
const c = this.counts;
|
|
505
|
+
const parts = [];
|
|
506
|
+
if (c.error) parts.push(`${c.error} error${c.error !== 1 ? 's' : ''}`);
|
|
507
|
+
if (c.warning) parts.push(`${c.warning} warning${c.warning !== 1 ? 's' : ''}`);
|
|
508
|
+
if (c.info) parts.push(`${c.info} info`);
|
|
509
|
+
const total = this.issues.length;
|
|
510
|
+
const detail = parts.length ? ` (${parts.join(', ')})` : '';
|
|
511
|
+
return `${total} issue${total !== 1 ? 's' : ''}${detail} — scanned ${this.stats.commands} commands, ${this.stats.agents} agents, ${this.stats.knowledgeFiles} knowledge docs, ${this.stats.skills} skill domains in ${this.totalTime}ms`;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
toReport() {
|
|
515
|
+
const lines = [
|
|
516
|
+
'═══════════════════════════════════════════════════════',
|
|
517
|
+
' CIPHER Artifact Consistency Report',
|
|
518
|
+
'═══════════════════════════════════════════════════════',
|
|
519
|
+
'',
|
|
520
|
+
`Summary: ${this.summary}`,
|
|
521
|
+
'',
|
|
522
|
+
];
|
|
523
|
+
|
|
524
|
+
if (this.issues.length === 0) {
|
|
525
|
+
lines.push('No issues found. All artifacts are consistent.');
|
|
526
|
+
} else {
|
|
527
|
+
// Group by category
|
|
528
|
+
const grouped = new Map();
|
|
529
|
+
for (const issue of this.issues) {
|
|
530
|
+
if (!grouped.has(issue.category)) grouped.set(issue.category, []);
|
|
531
|
+
grouped.get(issue.category).push(issue);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
for (const [category, catIssues] of grouped) {
|
|
535
|
+
lines.push(`── ${category} (${catIssues.length}) ──`);
|
|
536
|
+
for (const issue of catIssues) {
|
|
537
|
+
lines.push(issue.toReport());
|
|
538
|
+
}
|
|
539
|
+
lines.push('');
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
lines.push('───────────────────────────────────────────────────────');
|
|
544
|
+
lines.push(`Artifact counts: ${this.stats.commands} commands, ${this.stats.agents} agents, ${this.stats.knowledgeFiles} knowledge, ${this.stats.skills} skill domains, ${this.stats.modes} modes`);
|
|
545
|
+
|
|
546
|
+
return lines.join('\n');
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
toJSON() {
|
|
550
|
+
return {
|
|
551
|
+
summary: this.summary,
|
|
552
|
+
counts: this.counts,
|
|
553
|
+
stats: this.stats,
|
|
554
|
+
totalTime: this.totalTime,
|
|
555
|
+
issues: this.issues.map((i) => ({
|
|
556
|
+
category: i.category,
|
|
557
|
+
severity: i.severity,
|
|
558
|
+
file: i.file,
|
|
559
|
+
line: i.line,
|
|
560
|
+
message: i.message,
|
|
561
|
+
reference: i.reference,
|
|
562
|
+
suggestion: i.suggestion,
|
|
563
|
+
})),
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// Copyright (c) 2026 defconxt. All rights reserved.
|
|
2
|
+
// Licensed under AGPL-3.0 — see LICENSE file for details.
|
|
3
|
+
// CIPHER is a trademark of defconxt.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* CIPHER Constitution Versioning
|
|
7
|
+
*
|
|
8
|
+
* Tracks CLAUDE.md changes via content hash. Provides version info
|
|
9
|
+
* for audit trails and consistency checking.
|
|
10
|
+
*
|
|
11
|
+
* @module analyze/constitution
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
15
|
+
import { createHash } from 'node:crypto';
|
|
16
|
+
import { join, resolve, dirname } from 'node:path';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
|
|
19
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Constitution version info.
|
|
23
|
+
*/
|
|
24
|
+
export class ConstitutionVersion {
|
|
25
|
+
/**
|
|
26
|
+
* @param {object} opts
|
|
27
|
+
* @param {string} opts.hash - SHA-256 hash of CLAUDE.md content
|
|
28
|
+
* @param {string} opts.shortHash - First 8 chars of hash
|
|
29
|
+
* @param {number} opts.modes - Number of modes defined
|
|
30
|
+
* @param {string[]} opts.modeNames - Mode names
|
|
31
|
+
* @param {number} opts.lines - Total line count
|
|
32
|
+
* @param {number} opts.size - File size in bytes
|
|
33
|
+
* @param {string} opts.path - File path
|
|
34
|
+
*/
|
|
35
|
+
constructor(opts = {}) {
|
|
36
|
+
this.hash = opts.hash ?? '';
|
|
37
|
+
this.shortHash = opts.shortHash ?? '';
|
|
38
|
+
this.modes = opts.modes ?? 0;
|
|
39
|
+
this.modeNames = opts.modeNames ?? [];
|
|
40
|
+
this.lines = opts.lines ?? 0;
|
|
41
|
+
this.size = opts.size ?? 0;
|
|
42
|
+
this.path = opts.path ?? '';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
toReport() {
|
|
46
|
+
return [
|
|
47
|
+
`Constitution: ${this.path}`,
|
|
48
|
+
`Version hash: ${this.shortHash}`,
|
|
49
|
+
`Modes: ${this.modeNames.join(', ')} (${this.modes})`,
|
|
50
|
+
`Size: ${this.lines} lines, ${this.size} bytes`,
|
|
51
|
+
].join('\n');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
toJSON() {
|
|
55
|
+
return {
|
|
56
|
+
hash: this.hash,
|
|
57
|
+
shortHash: this.shortHash,
|
|
58
|
+
modes: this.modes,
|
|
59
|
+
modeNames: this.modeNames,
|
|
60
|
+
lines: this.lines,
|
|
61
|
+
size: this.size,
|
|
62
|
+
path: this.path,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get constitution version info from CLAUDE.md.
|
|
69
|
+
*
|
|
70
|
+
* @param {string} [repoRoot] - Path to repo root (auto-detected if omitted)
|
|
71
|
+
* @returns {ConstitutionVersion|null}
|
|
72
|
+
*/
|
|
73
|
+
export function getConstitutionVersion(repoRoot) {
|
|
74
|
+
const root = repoRoot ?? findRoot();
|
|
75
|
+
const claudePath = join(root, 'CLAUDE.md');
|
|
76
|
+
|
|
77
|
+
if (!existsSync(claudePath)) return null;
|
|
78
|
+
|
|
79
|
+
const content = readFileSync(claudePath, 'utf-8');
|
|
80
|
+
const hash = createHash('sha256').update(content).digest('hex');
|
|
81
|
+
|
|
82
|
+
// Extract modes
|
|
83
|
+
const modeNames = [];
|
|
84
|
+
const modeRe = /\[MODE:\s*([A-Z]+)\]/g;
|
|
85
|
+
let match;
|
|
86
|
+
while ((match = modeRe.exec(content)) !== null) {
|
|
87
|
+
if (!modeNames.includes(match[1])) modeNames.push(match[1]);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return new ConstitutionVersion({
|
|
91
|
+
hash,
|
|
92
|
+
shortHash: hash.slice(0, 8),
|
|
93
|
+
modes: modeNames.length,
|
|
94
|
+
modeNames,
|
|
95
|
+
lines: content.split('\n').length,
|
|
96
|
+
size: Buffer.byteLength(content),
|
|
97
|
+
path: claudePath,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function findRoot() {
|
|
102
|
+
let dir = resolve(__dirname, '..', '..');
|
|
103
|
+
for (let i = 0; i < 10; i++) {
|
|
104
|
+
if (existsSync(join(dir, 'CLAUDE.md'))) return dir;
|
|
105
|
+
const parent = dirname(dir);
|
|
106
|
+
if (parent === dir) break;
|
|
107
|
+
dir = parent;
|
|
108
|
+
}
|
|
109
|
+
return process.cwd();
|
|
110
|
+
}
|