brainclaw 0.19.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/LICENSE +74 -0
- package/README.md +226 -0
- package/dist/cli.js +1037 -0
- package/dist/commands/accept.js +149 -0
- package/dist/commands/adapter-openclaw-import.js +75 -0
- package/dist/commands/add-step.js +35 -0
- package/dist/commands/agent-board.js +106 -0
- package/dist/commands/audit.js +35 -0
- package/dist/commands/bootstrap.js +34 -0
- package/dist/commands/capability.js +104 -0
- package/dist/commands/changes.js +112 -0
- package/dist/commands/check-constraints.js +63 -0
- package/dist/commands/claim-resource.js +54 -0
- package/dist/commands/claim.js +92 -0
- package/dist/commands/complete-step.js +34 -0
- package/dist/commands/constraint.js +44 -0
- package/dist/commands/context-diff.js +32 -0
- package/dist/commands/context.js +63 -0
- package/dist/commands/decision.js +45 -0
- package/dist/commands/delete-plan.js +20 -0
- package/dist/commands/diff.js +99 -0
- package/dist/commands/doctor.js +1275 -0
- package/dist/commands/enable-agent.js +63 -0
- package/dist/commands/env.js +46 -0
- package/dist/commands/estimation-report.js +167 -0
- package/dist/commands/explore.js +47 -0
- package/dist/commands/export.js +381 -0
- package/dist/commands/handoff.js +63 -0
- package/dist/commands/history.js +22 -0
- package/dist/commands/hooks.js +123 -0
- package/dist/commands/init.js +356 -0
- package/dist/commands/install-hooks.js +115 -0
- package/dist/commands/instruction.js +56 -0
- package/dist/commands/list-agents.js +44 -0
- package/dist/commands/list-claims.js +45 -0
- package/dist/commands/list-instructions.js +50 -0
- package/dist/commands/list-plans.js +48 -0
- package/dist/commands/mcp-worker.js +12 -0
- package/dist/commands/mcp.js +2272 -0
- package/dist/commands/memory.js +283 -0
- package/dist/commands/metrics.js +175 -0
- package/dist/commands/plan-resource.js +62 -0
- package/dist/commands/plan.js +76 -0
- package/dist/commands/prune-candidates.js +36 -0
- package/dist/commands/prune.js +48 -0
- package/dist/commands/pull.js +25 -0
- package/dist/commands/push.js +28 -0
- package/dist/commands/rebuild.js +14 -0
- package/dist/commands/reflect-runtime-note.js +74 -0
- package/dist/commands/reflect.js +286 -0
- package/dist/commands/register-agent.js +29 -0
- package/dist/commands/reject.js +52 -0
- package/dist/commands/release-claim.js +41 -0
- package/dist/commands/release-claims.js +67 -0
- package/dist/commands/review.js +242 -0
- package/dist/commands/rollback.js +156 -0
- package/dist/commands/runtime-note.js +144 -0
- package/dist/commands/runtime-status.js +49 -0
- package/dist/commands/search.js +36 -0
- package/dist/commands/session-end.js +187 -0
- package/dist/commands/session-start.js +147 -0
- package/dist/commands/set-trust.js +92 -0
- package/dist/commands/setup.js +446 -0
- package/dist/commands/show-candidate.js +31 -0
- package/dist/commands/star-candidate.js +28 -0
- package/dist/commands/status.js +133 -0
- package/dist/commands/sync.js +159 -0
- package/dist/commands/tool.js +126 -0
- package/dist/commands/trap.js +74 -0
- package/dist/commands/update-handoff.js +23 -0
- package/dist/commands/update-plan.js +37 -0
- package/dist/commands/upgrade.js +382 -0
- package/dist/commands/use-candidate.js +35 -0
- package/dist/commands/version.js +96 -0
- package/dist/commands/watch.js +215 -0
- package/dist/commands/whoami.js +104 -0
- package/dist/core/agent-context.js +340 -0
- package/dist/core/agent-files.js +874 -0
- package/dist/core/agent-integrations.js +135 -0
- package/dist/core/agent-inventory.js +401 -0
- package/dist/core/agent-registry.js +420 -0
- package/dist/core/ai-agent-detection.js +140 -0
- package/dist/core/audit.js +85 -0
- package/dist/core/bootstrap.js +658 -0
- package/dist/core/brainclaw-version.js +433 -0
- package/dist/core/candidates.js +137 -0
- package/dist/core/circuit-breaker.js +118 -0
- package/dist/core/claims.js +72 -0
- package/dist/core/config.js +86 -0
- package/dist/core/context-diff.js +122 -0
- package/dist/core/context.js +1212 -0
- package/dist/core/contradictions.js +270 -0
- package/dist/core/coordination.js +86 -0
- package/dist/core/cross-project.js +99 -0
- package/dist/core/duplicates.js +72 -0
- package/dist/core/event-log.js +152 -0
- package/dist/core/events.js +56 -0
- package/dist/core/execution-context.js +204 -0
- package/dist/core/freshness.js +87 -0
- package/dist/core/global-registry.js +182 -0
- package/dist/core/host.js +10 -0
- package/dist/core/identity.js +151 -0
- package/dist/core/ids.js +56 -0
- package/dist/core/input-validation.js +81 -0
- package/dist/core/instructions.js +117 -0
- package/dist/core/io.js +191 -0
- package/dist/core/json-store.js +63 -0
- package/dist/core/lifecycle.js +45 -0
- package/dist/core/lock.js +129 -0
- package/dist/core/logger.js +49 -0
- package/dist/core/machine-profile.js +332 -0
- package/dist/core/markdown.js +120 -0
- package/dist/core/memory-git.js +133 -0
- package/dist/core/migration.js +247 -0
- package/dist/core/project-registry.js +64 -0
- package/dist/core/reflection-safety.js +21 -0
- package/dist/core/repo-analysis.js +133 -0
- package/dist/core/reputation.js +409 -0
- package/dist/core/runtime.js +134 -0
- package/dist/core/schema.js +580 -0
- package/dist/core/search.js +115 -0
- package/dist/core/security.js +66 -0
- package/dist/core/setup-state.js +50 -0
- package/dist/core/state.js +83 -0
- package/dist/core/store-resolution.js +119 -0
- package/dist/core/sync-remote.js +83 -0
- package/dist/core/traps.js +86 -0
- package/package.json +60 -0
|
@@ -0,0 +1,1275 @@
|
|
|
1
|
+
import { listAgentIdentities, resolveCurrentAgentIdentity } from '../core/agent-registry.js';
|
|
2
|
+
import { buildReputationSummary } from '../core/reputation.js';
|
|
3
|
+
import { buildCircuitBreakerSnapshot } from '../core/circuit-breaker.js';
|
|
4
|
+
import { loadState } from '../core/state.js';
|
|
5
|
+
import { loadConfig } from '../core/config.js';
|
|
6
|
+
import { doctorCheck } from '../core/security.js';
|
|
7
|
+
import { getVisibleMemoryVersion, readContextMarker } from '../core/freshness.js';
|
|
8
|
+
import { generateMarkdown } from '../core/markdown.js';
|
|
9
|
+
import { loadProjectIdentity, projectIdentityExists } from '../core/project-registry.js';
|
|
10
|
+
import { findInstructionConflicts, loadInstructions } from '../core/instructions.js';
|
|
11
|
+
import { memoryExists, memoryPath, readFileSync } from '../core/io.js';
|
|
12
|
+
import { logger } from '../core/logger.js';
|
|
13
|
+
import { listCandidates, listArchivedCandidates } from '../core/candidates.js';
|
|
14
|
+
import { listClaims } from '../core/claims.js';
|
|
15
|
+
import { listRuntimeNotes } from '../core/runtime.js';
|
|
16
|
+
import { isTrapExpired, listOperationalTraps } from '../core/traps.js';
|
|
17
|
+
import { scanText } from '../core/security.js';
|
|
18
|
+
import { listRuntimeEvents } from '../core/events.js';
|
|
19
|
+
import { resolveEventSessionId } from '../core/identity.js';
|
|
20
|
+
import { detectContradictions } from '../core/contradictions.js';
|
|
21
|
+
import { scanMigrationStatus } from '../core/migration.js';
|
|
22
|
+
import { buildAgentToolingContext } from '../core/agent-context.js';
|
|
23
|
+
import { assessAgentIntegrationReadiness } from '../core/agent-integrations.js';
|
|
24
|
+
import { assessBrainclawVersion } from '../core/brainclaw-version.js';
|
|
25
|
+
import { resolveStoreChain } from '../core/store-resolution.js';
|
|
26
|
+
import { resolveCrossProjectLinks, detectCrossProjectCycles } from '../core/cross-project.js';
|
|
27
|
+
import { auditLocalAgentWorkspaceFiles, ensureGitignoreEntries } from '../core/agent-files.js';
|
|
28
|
+
const BACKLOG_KEYWORDS = /\b(TODO|NEXT|backlog|next[\s-]step|action[\s-]item|prochaine?s?\s+étapes?|à\s+faire)\b/i;
|
|
29
|
+
function hasBacklogPatterns(text) {
|
|
30
|
+
const lines = text.split(/\r?\n/);
|
|
31
|
+
const bulletOrNumbered = lines.some((l) => /^\s*[-*•]\s+\w/.test(l) || /^\s*\d+\.\s+\w/.test(l));
|
|
32
|
+
return bulletOrNumbered || BACKLOG_KEYWORDS.test(text) || /\[[ x]\]/.test(text);
|
|
33
|
+
}
|
|
34
|
+
export function runDoctor(options = {}) {
|
|
35
|
+
if (!memoryExists(options.cwd)) {
|
|
36
|
+
console.error('Error: .brainclaw/ not found. Run `brainclaw init` first.');
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
let hasIssues = false;
|
|
40
|
+
const checks = [];
|
|
41
|
+
let migrationEntries = [];
|
|
42
|
+
let agentGitHygieneFixed = [];
|
|
43
|
+
// Validate config
|
|
44
|
+
let config;
|
|
45
|
+
try {
|
|
46
|
+
config = loadConfig(options.cwd);
|
|
47
|
+
checks.push({ name: 'config', status: 'ok', message: 'config.yaml is valid' });
|
|
48
|
+
if (!options.json) {
|
|
49
|
+
console.log('✔ config.yaml is valid');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
54
|
+
checks.push({ name: 'config', status: 'error', message: `config.yaml is invalid: ${msg}` });
|
|
55
|
+
console.error(`✗ config.yaml is invalid: ${msg}`);
|
|
56
|
+
hasIssues = true;
|
|
57
|
+
if (options.json) {
|
|
58
|
+
console.log(JSON.stringify({ ok: false, checks, metrics: {} }, null, 2));
|
|
59
|
+
}
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (options.fixAgentIgnore) {
|
|
63
|
+
const initialAudit = auditLocalAgentWorkspaceFiles(options.cwd ?? process.cwd());
|
|
64
|
+
if (initialAudit.missingGitignorePaths.length > 0) {
|
|
65
|
+
ensureGitignoreEntries(options.cwd ?? process.cwd(), initialAudit.missingGitignorePaths);
|
|
66
|
+
agentGitHygieneFixed = initialAudit.missingGitignorePaths;
|
|
67
|
+
if (!options.json) {
|
|
68
|
+
console.log(`✔ Added generated local agent files to .gitignore: ${agentGitHygieneFixed.join(', ')}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Validate state
|
|
73
|
+
let state;
|
|
74
|
+
try {
|
|
75
|
+
state = loadState(options.cwd);
|
|
76
|
+
checks.push({ name: 'state', status: 'ok', message: 'state is valid' });
|
|
77
|
+
if (!options.json) {
|
|
78
|
+
console.log('✔ state is valid');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch (e) {
|
|
82
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
83
|
+
checks.push({ name: 'state', status: 'error', message: `state is invalid: ${msg}` });
|
|
84
|
+
console.error(`✗ state is invalid: ${msg}`);
|
|
85
|
+
hasIssues = true;
|
|
86
|
+
if (options.json) {
|
|
87
|
+
console.log(JSON.stringify({ ok: false, checks, metrics: {} }, null, 2));
|
|
88
|
+
}
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (options.migrationCheck) {
|
|
92
|
+
migrationEntries = scanMigrationStatus(options.cwd);
|
|
93
|
+
const outdated = migrationEntries.filter((entry) => entry.status === 'outdated');
|
|
94
|
+
const invalid = migrationEntries.filter((entry) => entry.status === 'invalid');
|
|
95
|
+
if (outdated.length > 0) {
|
|
96
|
+
checks.push({
|
|
97
|
+
name: 'schema_migrations',
|
|
98
|
+
status: 'warn',
|
|
99
|
+
message: `${outdated.length} document(s) require schema migration.`,
|
|
100
|
+
});
|
|
101
|
+
if (!options.json) {
|
|
102
|
+
console.warn(`⚠ ${outdated.length} document(s) require schema migration.`);
|
|
103
|
+
for (const entry of outdated.slice(0, 20)) {
|
|
104
|
+
console.warn(` - ${entry.path} [${entry.documentType}] v${entry.detectedVersion} -> v${entry.currentVersion}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
hasIssues = true;
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
checks.push({
|
|
111
|
+
name: 'schema_migrations',
|
|
112
|
+
status: 'ok',
|
|
113
|
+
message: 'No documents require schema migration',
|
|
114
|
+
});
|
|
115
|
+
if (!options.json) {
|
|
116
|
+
console.log('✔ No documents require schema migration');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (invalid.length > 0) {
|
|
120
|
+
checks.push({
|
|
121
|
+
name: 'schema_migration_errors',
|
|
122
|
+
status: 'error',
|
|
123
|
+
message: `${invalid.length} document(s) are invalid or unreadable for migration.`,
|
|
124
|
+
});
|
|
125
|
+
if (!options.json) {
|
|
126
|
+
console.warn(`⚠ ${invalid.length} document(s) are invalid or unreadable for migration.`);
|
|
127
|
+
for (const entry of invalid.slice(0, 20)) {
|
|
128
|
+
console.warn(` - ${entry.path} [${entry.documentType}] ${entry.error ?? 'invalid document'}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
hasIssues = true;
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
checks.push({
|
|
135
|
+
name: 'schema_migration_errors',
|
|
136
|
+
status: 'ok',
|
|
137
|
+
message: 'No invalid versioned documents found',
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (config.project_mode === 'multi-project' && (config.projects?.known.length ?? 0) === 0) {
|
|
142
|
+
checks.push({
|
|
143
|
+
name: 'project_mode',
|
|
144
|
+
status: 'warn',
|
|
145
|
+
message: 'project_mode is multi-project but no project namespaces are configured yet.',
|
|
146
|
+
});
|
|
147
|
+
if (!options.json) {
|
|
148
|
+
console.warn('⚠ project_mode is multi-project but no project namespaces are configured yet.');
|
|
149
|
+
}
|
|
150
|
+
hasIssues = true;
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
checks.push({
|
|
154
|
+
name: 'project_mode',
|
|
155
|
+
status: 'ok',
|
|
156
|
+
message: `project_mode=${config.project_mode}, strategy=${config.projects?.strategy ?? 'manual'}, known_projects=${config.projects?.known.length ?? 0}`,
|
|
157
|
+
});
|
|
158
|
+
if (!options.json) {
|
|
159
|
+
console.log(`✔ project mode: ${config.project_mode} (${config.projects?.strategy ?? 'manual'})`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
if (projectIdentityExists(options.cwd)) {
|
|
164
|
+
const projectIdentity = loadProjectIdentity(options.cwd);
|
|
165
|
+
if (!config.project_id) {
|
|
166
|
+
checks.push({
|
|
167
|
+
name: 'project_identity',
|
|
168
|
+
status: 'warn',
|
|
169
|
+
message: `project.identity.json exists but config.yaml has no project_id. Expected ${projectIdentity.project_id}.`,
|
|
170
|
+
});
|
|
171
|
+
if (!options.json) {
|
|
172
|
+
console.warn(`⚠ project.identity.json exists but config.yaml has no project_id. Expected ${projectIdentity.project_id}.`);
|
|
173
|
+
}
|
|
174
|
+
hasIssues = true;
|
|
175
|
+
}
|
|
176
|
+
else if (config.project_id !== projectIdentity.project_id) {
|
|
177
|
+
checks.push({
|
|
178
|
+
name: 'project_identity',
|
|
179
|
+
status: 'warn',
|
|
180
|
+
message: `project_id mismatch between config.yaml (${config.project_id}) and project.identity.json (${projectIdentity.project_id}).`,
|
|
181
|
+
});
|
|
182
|
+
if (!options.json) {
|
|
183
|
+
console.warn(`⚠ project_id mismatch between config.yaml (${config.project_id}) and project.identity.json (${projectIdentity.project_id}).`);
|
|
184
|
+
}
|
|
185
|
+
hasIssues = true;
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
checks.push({
|
|
189
|
+
name: 'project_identity',
|
|
190
|
+
status: 'ok',
|
|
191
|
+
message: `project_id=${projectIdentity.project_id}`,
|
|
192
|
+
});
|
|
193
|
+
if (!options.json) {
|
|
194
|
+
console.log(`✔ project identity: ${projectIdentity.project_id}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
else if (config.project_id) {
|
|
199
|
+
checks.push({
|
|
200
|
+
name: 'project_identity',
|
|
201
|
+
status: 'warn',
|
|
202
|
+
message: `config.yaml has project_id=${config.project_id} but project.identity.json is missing.`,
|
|
203
|
+
});
|
|
204
|
+
if (!options.json) {
|
|
205
|
+
console.warn(`⚠ config.yaml has project_id=${config.project_id} but project.identity.json is missing.`);
|
|
206
|
+
}
|
|
207
|
+
hasIssues = true;
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
checks.push({
|
|
211
|
+
name: 'project_identity',
|
|
212
|
+
status: 'ok',
|
|
213
|
+
message: 'No project identity configured yet',
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
catch (e) {
|
|
218
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
219
|
+
checks.push({ name: 'project_identity', status: 'warn', message: `project identity is invalid: ${msg}` });
|
|
220
|
+
if (!options.json) {
|
|
221
|
+
console.warn(`⚠ project identity is invalid: ${msg}`);
|
|
222
|
+
}
|
|
223
|
+
hasIssues = true;
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
const registeredAgents = listAgentIdentities(options.cwd);
|
|
227
|
+
if (config.current_agent || config.current_agent_id) {
|
|
228
|
+
const currentAgent = resolveCurrentAgentIdentity(options.cwd);
|
|
229
|
+
if (!currentAgent) {
|
|
230
|
+
checks.push({
|
|
231
|
+
name: 'agent_identity',
|
|
232
|
+
status: 'warn',
|
|
233
|
+
message: `Current agent is configured (${config.current_agent ?? 'unknown'} / ${config.current_agent_id ?? 'unknown'}) but no matching registry entry was found.`,
|
|
234
|
+
});
|
|
235
|
+
if (!options.json) {
|
|
236
|
+
console.warn(`⚠ Current agent is configured (${config.current_agent ?? 'unknown'} / ${config.current_agent_id ?? 'unknown'}) but no matching registry entry was found.`);
|
|
237
|
+
}
|
|
238
|
+
hasIssues = true;
|
|
239
|
+
}
|
|
240
|
+
else if ((config.current_agent && config.current_agent !== currentAgent.agent_name)
|
|
241
|
+
|| (config.current_agent_id && config.current_agent_id !== currentAgent.agent_id)) {
|
|
242
|
+
checks.push({
|
|
243
|
+
name: 'agent_identity',
|
|
244
|
+
status: 'warn',
|
|
245
|
+
message: `Current agent config does not match registry entry (${currentAgent.agent_name} / ${currentAgent.agent_id}).`,
|
|
246
|
+
});
|
|
247
|
+
if (!options.json) {
|
|
248
|
+
console.warn(`⚠ Current agent config does not match registry entry (${currentAgent.agent_name} / ${currentAgent.agent_id}).`);
|
|
249
|
+
}
|
|
250
|
+
hasIssues = true;
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
checks.push({
|
|
254
|
+
name: 'agent_identity',
|
|
255
|
+
status: 'ok',
|
|
256
|
+
message: `current_agent=${currentAgent.agent_name}, agent_id=${currentAgent.agent_id}, registered_agents=${registeredAgents.length}`,
|
|
257
|
+
});
|
|
258
|
+
if (!options.json) {
|
|
259
|
+
console.log(`✔ current agent: ${currentAgent.agent_name} (${currentAgent.agent_id})`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
checks.push({
|
|
265
|
+
name: 'agent_identity',
|
|
266
|
+
status: 'ok',
|
|
267
|
+
message: `No current agent configured (${registeredAgents.length} registered agent(s))`,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
catch (e) {
|
|
272
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
273
|
+
checks.push({ name: 'agent_identity', status: 'warn', message: `agent identity is invalid: ${msg}` });
|
|
274
|
+
if (!options.json) {
|
|
275
|
+
console.warn(`⚠ agent identity is invalid: ${msg}`);
|
|
276
|
+
}
|
|
277
|
+
hasIssues = true;
|
|
278
|
+
}
|
|
279
|
+
// Warn if no curator is registered
|
|
280
|
+
try {
|
|
281
|
+
const allAgents = listAgentIdentities(options.cwd);
|
|
282
|
+
const hasCurator = allAgents.some((a) => a.trust_level === 'curator');
|
|
283
|
+
if (!hasCurator && allAgents.length > 0) {
|
|
284
|
+
checks.push({
|
|
285
|
+
name: 'no_curator',
|
|
286
|
+
status: 'warn',
|
|
287
|
+
message: 'No curator registered. Run `brainclaw set-trust <agent> --level curator` or `brainclaw register-agent <name> --curator` to designate a project owner.',
|
|
288
|
+
});
|
|
289
|
+
if (!options.json) {
|
|
290
|
+
console.warn('⚠ No curator registered — run `brainclaw set-trust <agent> --level curator` to designate a project owner.');
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
catch { /* non-fatal */ }
|
|
295
|
+
const agentTooling = buildAgentToolingContext({ cwd: options.cwd });
|
|
296
|
+
if (agentTooling.agents_md_present && agentTooling.agents_rules.length === 0) {
|
|
297
|
+
checks.push({
|
|
298
|
+
name: 'agent_rules',
|
|
299
|
+
status: 'warn',
|
|
300
|
+
message: 'AGENTS.md is present but no actionable rules were extracted.',
|
|
301
|
+
});
|
|
302
|
+
if (!options.json) {
|
|
303
|
+
console.warn('⚠ AGENTS.md is present but no actionable rules were extracted.');
|
|
304
|
+
}
|
|
305
|
+
hasIssues = true;
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
checks.push({
|
|
309
|
+
name: 'agent_rules',
|
|
310
|
+
status: 'ok',
|
|
311
|
+
message: agentTooling.agents_md_present
|
|
312
|
+
? `${agentTooling.agents_rules.length} actionable agent rule(s) detected`
|
|
313
|
+
: 'No AGENTS.md detected',
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
const incompleteSkills = agentTooling.skills.filter((skill) => !skill.description && !skill.scripts_present && !skill.references_present && !skill.assets_present);
|
|
317
|
+
if (incompleteSkills.length > 0) {
|
|
318
|
+
checks.push({
|
|
319
|
+
name: 'agent_skills',
|
|
320
|
+
status: 'warn',
|
|
321
|
+
message: `${incompleteSkills.length} skill(s) look incomplete or under-described.`,
|
|
322
|
+
});
|
|
323
|
+
if (!options.json) {
|
|
324
|
+
console.warn(`⚠ ${incompleteSkills.length} skill(s) look incomplete or under-described.`);
|
|
325
|
+
}
|
|
326
|
+
hasIssues = true;
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
checks.push({
|
|
330
|
+
name: 'agent_skills',
|
|
331
|
+
status: 'ok',
|
|
332
|
+
message: `${agentTooling.skills.length} skill(s) inventoried`,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
const missingMcpCommands = agentTooling.mcp_servers.filter((server) => server.availability === 'missing_command');
|
|
336
|
+
if (missingMcpCommands.length > 0) {
|
|
337
|
+
checks.push({
|
|
338
|
+
name: 'agent_mcp',
|
|
339
|
+
status: 'warn',
|
|
340
|
+
message: `${missingMcpCommands.length} stdio MCP server(s) are configured with a missing local command.`,
|
|
341
|
+
});
|
|
342
|
+
if (!options.json) {
|
|
343
|
+
console.warn(`⚠ ${missingMcpCommands.length} stdio MCP server(s) are configured with a missing local command.`);
|
|
344
|
+
}
|
|
345
|
+
hasIssues = true;
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
checks.push({
|
|
349
|
+
name: 'agent_mcp',
|
|
350
|
+
status: 'ok',
|
|
351
|
+
message: `${agentTooling.mcp_servers.length} MCP server(s) inventoried`,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
const integrationReadiness = assessAgentIntegrationReadiness(config, options.cwd ?? process.cwd());
|
|
355
|
+
const missingIntegrations = integrationReadiness.filter((entry) => !entry.ready);
|
|
356
|
+
if (missingIntegrations.length > 0) {
|
|
357
|
+
checks.push({
|
|
358
|
+
name: 'agent_integrations',
|
|
359
|
+
status: 'warn',
|
|
360
|
+
message: `${missingIntegrations.length} declared agent integration(s) are not fully activated on this machine/workspace.`,
|
|
361
|
+
details: missingIntegrations,
|
|
362
|
+
});
|
|
363
|
+
if (!options.json) {
|
|
364
|
+
console.warn(`⚠ ${missingIntegrations.length} declared agent integration(s) are not fully activated on this machine/workspace.`);
|
|
365
|
+
}
|
|
366
|
+
hasIssues = true;
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
checks.push({
|
|
370
|
+
name: 'agent_integrations',
|
|
371
|
+
status: 'ok',
|
|
372
|
+
message: `${integrationReadiness.length} declared agent integration(s) are fully activated`,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
const agentGitHygiene = auditLocalAgentWorkspaceFiles(options.cwd ?? process.cwd());
|
|
376
|
+
if (!agentGitHygiene.isGitRepo) {
|
|
377
|
+
checks.push({
|
|
378
|
+
name: 'agent_git_hygiene',
|
|
379
|
+
status: 'ok',
|
|
380
|
+
message: 'No Git worktree detected; agent git hygiene audit skipped',
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
else if (agentGitHygiene.presentPaths.length === 0) {
|
|
384
|
+
checks.push({
|
|
385
|
+
name: 'agent_git_hygiene',
|
|
386
|
+
status: 'ok',
|
|
387
|
+
message: 'No local-only Brainclaw agent files detected in the workspace',
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
else if (agentGitHygiene.hasIssues) {
|
|
391
|
+
const parts = [];
|
|
392
|
+
if (agentGitHygiene.missingGitignorePaths.length > 0) {
|
|
393
|
+
parts.push(`${agentGitHygiene.missingGitignorePaths.length} file(s) should be added to .gitignore`);
|
|
394
|
+
}
|
|
395
|
+
if (agentGitHygiene.trackedPaths.length > 0) {
|
|
396
|
+
parts.push(`${agentGitHygiene.trackedPaths.length} file(s) are still tracked by Git`);
|
|
397
|
+
}
|
|
398
|
+
const fixHint = agentGitHygiene.missingGitignorePaths.length > 0
|
|
399
|
+
? ' Run `brainclaw doctor --fix-agent-ignore` to add ignore entries.'
|
|
400
|
+
: '';
|
|
401
|
+
checks.push({
|
|
402
|
+
name: 'agent_git_hygiene',
|
|
403
|
+
status: 'warn',
|
|
404
|
+
message: `${parts.join('; ')}.${fixHint}`.trim(),
|
|
405
|
+
details: agentGitHygiene,
|
|
406
|
+
});
|
|
407
|
+
if (!options.json) {
|
|
408
|
+
console.warn(`⚠ Agent git hygiene: ${parts.join('; ')}.`);
|
|
409
|
+
if (agentGitHygiene.missingGitignorePaths.length > 0) {
|
|
410
|
+
console.warn(` Missing .gitignore entries: ${agentGitHygiene.missingGitignorePaths.join(', ')}`);
|
|
411
|
+
console.warn(' Fix: run `brainclaw doctor --fix-agent-ignore`');
|
|
412
|
+
}
|
|
413
|
+
if (agentGitHygiene.trackedPaths.length > 0) {
|
|
414
|
+
console.warn(` Tracked local agent files: ${agentGitHygiene.trackedPaths.join(', ')}`);
|
|
415
|
+
console.warn(' After updating .gitignore, untrack them with `git rm --cached <path>` as needed.');
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
hasIssues = true;
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
checks.push({
|
|
422
|
+
name: 'agent_git_hygiene',
|
|
423
|
+
status: 'ok',
|
|
424
|
+
message: `${agentGitHygiene.presentPaths.length} local-only Brainclaw agent file(s) are ignored correctly`,
|
|
425
|
+
details: agentGitHygiene,
|
|
426
|
+
});
|
|
427
|
+
if (!options.json) {
|
|
428
|
+
console.log(`✔ Agent git hygiene: ${agentGitHygiene.presentPaths.length} local-only file(s) are ignored correctly`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
const brainclawVersion = assessBrainclawVersion(config);
|
|
432
|
+
if (brainclawVersion.status === 'upgrade_required' || brainclawVersion.status === 'invalid_config') {
|
|
433
|
+
checks.push({
|
|
434
|
+
name: 'brainclaw_version',
|
|
435
|
+
status: 'warn',
|
|
436
|
+
message: brainclawVersion.message,
|
|
437
|
+
details: brainclawVersion,
|
|
438
|
+
});
|
|
439
|
+
if (!options.json) {
|
|
440
|
+
console.warn(`⚠ ${brainclawVersion.message}`);
|
|
441
|
+
if (brainclawVersion.upgrade_message) {
|
|
442
|
+
console.warn(` Benefits: ${brainclawVersion.upgrade_message}`);
|
|
443
|
+
}
|
|
444
|
+
if (brainclawVersion.upgrade_command) {
|
|
445
|
+
console.warn(` Upgrade: ${brainclawVersion.upgrade_command}`);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
hasIssues = true;
|
|
449
|
+
}
|
|
450
|
+
else if (brainclawVersion.status === 'update_available') {
|
|
451
|
+
checks.push({
|
|
452
|
+
name: 'brainclaw_version',
|
|
453
|
+
status: 'warn',
|
|
454
|
+
message: brainclawVersion.message,
|
|
455
|
+
details: brainclawVersion,
|
|
456
|
+
});
|
|
457
|
+
if (!options.json) {
|
|
458
|
+
console.warn(`⚠ ${brainclawVersion.message}`);
|
|
459
|
+
if (brainclawVersion.upgrade_message) {
|
|
460
|
+
console.warn(` Benefits: ${brainclawVersion.upgrade_message}`);
|
|
461
|
+
}
|
|
462
|
+
if (brainclawVersion.upgrade_command) {
|
|
463
|
+
console.warn(` Upgrade: ${brainclawVersion.upgrade_command}`);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
hasIssues = true;
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
checks.push({
|
|
470
|
+
name: 'brainclaw_version',
|
|
471
|
+
status: 'ok',
|
|
472
|
+
message: brainclawVersion.message,
|
|
473
|
+
});
|
|
474
|
+
if (!options.json) {
|
|
475
|
+
console.log(`✔ ${brainclawVersion.message}`);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
// Check project.md consistency
|
|
479
|
+
try {
|
|
480
|
+
const currentMd = readFileSync(memoryPath('project.md', options.cwd));
|
|
481
|
+
const expectedMd = generateMarkdown(state, options.cwd);
|
|
482
|
+
if (currentMd === expectedMd) {
|
|
483
|
+
checks.push({ name: 'markdown_sync', status: 'ok', message: 'project.md is in sync with state' });
|
|
484
|
+
if (!options.json) {
|
|
485
|
+
console.log('✔ project.md is in sync with state');
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
else {
|
|
489
|
+
checks.push({ name: 'markdown_sync', status: 'warn', message: 'project.md is out of sync with state. Run `brainclaw rebuild` to fix.' });
|
|
490
|
+
console.warn('⚠ project.md is out of sync with state. Run `brainclaw rebuild` to fix.');
|
|
491
|
+
hasIssues = true;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
catch (err) {
|
|
495
|
+
logger.debug('Failed to check project.md sync:', err);
|
|
496
|
+
checks.push({ name: 'markdown_sync', status: 'warn', message: 'project.md is missing. Run `brainclaw rebuild` to regenerate.' });
|
|
497
|
+
console.warn('⚠ project.md is missing. Run `brainclaw rebuild` to regenerate.');
|
|
498
|
+
hasIssues = true;
|
|
499
|
+
}
|
|
500
|
+
// Security scan on state
|
|
501
|
+
const warnings = doctorCheck(state, config);
|
|
502
|
+
if (warnings.length === 0) {
|
|
503
|
+
checks.push({ name: 'state_security', status: 'ok', message: 'No sensitive content detected in state' });
|
|
504
|
+
if (!options.json) {
|
|
505
|
+
console.log('✔ No sensitive content detected in state');
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
else {
|
|
509
|
+
hasIssues = true;
|
|
510
|
+
checks.push({ name: 'state_security', status: 'warn', message: `${warnings.length} sensitive content warning(s) detected in state` });
|
|
511
|
+
if (!options.json) {
|
|
512
|
+
console.log('');
|
|
513
|
+
console.log('State warnings:');
|
|
514
|
+
}
|
|
515
|
+
for (const w of warnings) {
|
|
516
|
+
if (!options.json) {
|
|
517
|
+
console.warn(` - ${w.message}`);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
const planItems = state.plan_items;
|
|
522
|
+
const instructions = loadInstructions(options.cwd);
|
|
523
|
+
const activePlans = planItems.filter((plan) => plan.status !== 'done' && plan.status !== 'dropped');
|
|
524
|
+
const blockedPlans = planItems.filter((plan) => plan.status === 'blocked');
|
|
525
|
+
const unassignedInProgress = planItems.filter((plan) => plan.status === 'in_progress' && !plan.assignee);
|
|
526
|
+
if (!options.json) {
|
|
527
|
+
console.log(`✔ Shared plan: ${activePlans.length} active, ${blockedPlans.length} blocked`);
|
|
528
|
+
}
|
|
529
|
+
if (unassignedInProgress.length > 0) {
|
|
530
|
+
checks.push({
|
|
531
|
+
name: 'plan_assignment',
|
|
532
|
+
status: 'warn',
|
|
533
|
+
message: `${unassignedInProgress.length} in-progress plan item(s) have no assignee.`,
|
|
534
|
+
});
|
|
535
|
+
if (!options.json) {
|
|
536
|
+
console.warn(`⚠ ${unassignedInProgress.length} in-progress plan item(s) have no assignee.`);
|
|
537
|
+
}
|
|
538
|
+
hasIssues = true;
|
|
539
|
+
}
|
|
540
|
+
else {
|
|
541
|
+
checks.push({
|
|
542
|
+
name: 'plan_assignment',
|
|
543
|
+
status: 'ok',
|
|
544
|
+
message: 'All in-progress plan items have an assignee',
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
const unknownProjects = activePlans
|
|
548
|
+
.map((plan) => plan.project)
|
|
549
|
+
.filter((project) => typeof project === 'string' && project.length > 0)
|
|
550
|
+
.filter((project) => config.project_mode === 'multi-project' && !config.projects.known.includes(project));
|
|
551
|
+
if (unknownProjects.length > 0) {
|
|
552
|
+
const deduped = [...new Set(unknownProjects)];
|
|
553
|
+
checks.push({
|
|
554
|
+
name: 'plan_projects',
|
|
555
|
+
status: 'warn',
|
|
556
|
+
message: `Plan items reference unknown project namespace(s): ${deduped.join(', ')}`,
|
|
557
|
+
});
|
|
558
|
+
if (!options.json) {
|
|
559
|
+
console.warn(`⚠ Plan items reference unknown project namespace(s): ${deduped.join(', ')}`);
|
|
560
|
+
}
|
|
561
|
+
hasIssues = true;
|
|
562
|
+
}
|
|
563
|
+
else {
|
|
564
|
+
checks.push({
|
|
565
|
+
name: 'plan_projects',
|
|
566
|
+
status: 'ok',
|
|
567
|
+
message: 'Plan items reference known project namespaces',
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
const activeInstructions = instructions.filter((entry) => entry.active);
|
|
571
|
+
const instructionConflicts = findInstructionConflicts(instructions);
|
|
572
|
+
if (!options.json) {
|
|
573
|
+
console.log(`✔ Shared instructions: ${activeInstructions.length} active`);
|
|
574
|
+
}
|
|
575
|
+
if (instructionConflicts.length > 0) {
|
|
576
|
+
const summary = instructionConflicts
|
|
577
|
+
.map((conflict) => `${conflict.layer}${conflict.scope ? `:${conflict.scope}` : ''} (${conflict.ids.join(', ')})`)
|
|
578
|
+
.join('; ');
|
|
579
|
+
checks.push({
|
|
580
|
+
name: 'instruction_conflicts',
|
|
581
|
+
status: 'warn',
|
|
582
|
+
message: `Multiple active instructions share the same layer/scope: ${summary}`,
|
|
583
|
+
});
|
|
584
|
+
if (!options.json) {
|
|
585
|
+
console.warn(`⚠ Multiple active instructions share the same layer/scope: ${summary}`);
|
|
586
|
+
}
|
|
587
|
+
hasIssues = true;
|
|
588
|
+
}
|
|
589
|
+
else {
|
|
590
|
+
checks.push({
|
|
591
|
+
name: 'instruction_conflicts',
|
|
592
|
+
status: 'ok',
|
|
593
|
+
message: 'Instruction layers resolve without active scope conflicts',
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
// --- Reflective memory checks ---
|
|
597
|
+
const pending = listCandidates('pending', options.cwd);
|
|
598
|
+
const accepted = listArchivedCandidates('accepted', options.cwd);
|
|
599
|
+
const rejected = listArchivedCandidates('rejected', options.cwd);
|
|
600
|
+
if (!options.json) {
|
|
601
|
+
console.log('');
|
|
602
|
+
console.log(`Reflective memory: ${pending.length} pending, ${accepted.length} accepted, ${rejected.length} rejected`);
|
|
603
|
+
}
|
|
604
|
+
// Governance checks
|
|
605
|
+
const approvalPolicy = config.governance?.approval_policy ?? 'review';
|
|
606
|
+
const curators = config.governance?.curators ?? [];
|
|
607
|
+
if (approvalPolicy === 'strict' && curators.length === 0) {
|
|
608
|
+
checks.push({ name: 'governance_config', status: 'warn', message: 'governance.approval_policy is strict but no governance.curators are configured.' });
|
|
609
|
+
console.warn('⚠ governance.approval_policy is strict but no governance.curators are configured.');
|
|
610
|
+
hasIssues = true;
|
|
611
|
+
}
|
|
612
|
+
else {
|
|
613
|
+
checks.push({ name: 'governance_config', status: 'ok', message: `approval_policy=${approvalPolicy}, curators=${curators.length}` });
|
|
614
|
+
}
|
|
615
|
+
const maxPending = config.reflective_memory?.max_pending ?? 50;
|
|
616
|
+
const promotionStarsThreshold = config.reflective_memory?.promotion_stars_threshold ?? 3;
|
|
617
|
+
const promotionUsesThreshold = config.reflective_memory?.promotion_uses_threshold ?? 2;
|
|
618
|
+
const reviewSlaHours = config.governance?.review_sla_hours ?? 24;
|
|
619
|
+
const promotionReady = pending.filter((c) => (c.star_count ?? 0) >= promotionStarsThreshold || (c.usage_count ?? 0) >= promotionUsesThreshold);
|
|
620
|
+
const pendingOverdue = pending.filter((c) => {
|
|
621
|
+
const ageHours = Math.floor((Date.now() - Date.parse(c.created_at)) / (1000 * 60 * 60));
|
|
622
|
+
return ageHours > reviewSlaHours;
|
|
623
|
+
});
|
|
624
|
+
const reviewed = [...accepted, ...rejected]
|
|
625
|
+
.filter((c) => c.resolved_at)
|
|
626
|
+
.map((c) => {
|
|
627
|
+
const created = Date.parse(c.created_at);
|
|
628
|
+
const resolved = Date.parse(c.resolved_at);
|
|
629
|
+
return Math.max(0, resolved - created) / (1000 * 60 * 60);
|
|
630
|
+
});
|
|
631
|
+
const avgReviewHours = reviewed.length > 0
|
|
632
|
+
? reviewed.reduce((sum, value) => sum + value, 0) / reviewed.length
|
|
633
|
+
: 0;
|
|
634
|
+
if (!options.json) {
|
|
635
|
+
console.log(`Governance review KPI: pending_overdue=${pendingOverdue.length}, avg_review_hours=${avgReviewHours.toFixed(1)}, review_sla_hours=${reviewSlaHours}`);
|
|
636
|
+
console.log(`Promotion signal: ${promotionReady.length} candidate(s) reached ${promotionStarsThreshold} star(s) or ${promotionUsesThreshold} use(s)`);
|
|
637
|
+
}
|
|
638
|
+
if (promotionReady.length > 0) {
|
|
639
|
+
checks.push({
|
|
640
|
+
name: 'promotion_signals',
|
|
641
|
+
status: 'warn',
|
|
642
|
+
message: `${promotionReady.length} pending candidate(s) reached the promotion threshold (${promotionStarsThreshold} star(s) or ${promotionUsesThreshold} use(s)).`,
|
|
643
|
+
});
|
|
644
|
+
if (!options.json) {
|
|
645
|
+
console.warn(`⚠ ${promotionReady.length} pending candidate(s) reached the promotion threshold (${promotionStarsThreshold} star(s) or ${promotionUsesThreshold} use(s)).`);
|
|
646
|
+
}
|
|
647
|
+
hasIssues = true;
|
|
648
|
+
}
|
|
649
|
+
else {
|
|
650
|
+
checks.push({
|
|
651
|
+
name: 'promotion_signals',
|
|
652
|
+
status: 'ok',
|
|
653
|
+
message: `No pending candidate reached the promotion threshold (${promotionStarsThreshold} star(s) or ${promotionUsesThreshold} use(s)).`,
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
if (pendingOverdue.length > 0) {
|
|
657
|
+
checks.push({ name: 'governance_sla', status: 'warn', message: `${pendingOverdue.length} pending candidate(s) are beyond review SLA (${reviewSlaHours}h).` });
|
|
658
|
+
console.warn(`⚠ ${pendingOverdue.length} pending candidate(s) are beyond review SLA (${reviewSlaHours}h).`);
|
|
659
|
+
hasIssues = true;
|
|
660
|
+
}
|
|
661
|
+
else {
|
|
662
|
+
checks.push({ name: 'governance_sla', status: 'ok', message: `No pending candidate beyond SLA (${reviewSlaHours}h)` });
|
|
663
|
+
}
|
|
664
|
+
if (pending.length > maxPending) {
|
|
665
|
+
checks.push({ name: 'pending_capacity', status: 'warn', message: `Too many pending candidates (${pending.length} > ${maxPending}).` });
|
|
666
|
+
console.warn(`⚠ Too many pending candidates (${pending.length} > ${maxPending}). Consider reviewing or pruning.`);
|
|
667
|
+
hasIssues = true;
|
|
668
|
+
}
|
|
669
|
+
else {
|
|
670
|
+
checks.push({ name: 'pending_capacity', status: 'ok', message: `Pending candidates within limit (${pending.length}/${maxPending})` });
|
|
671
|
+
}
|
|
672
|
+
// Scan candidate texts for sensitive content
|
|
673
|
+
const candidateWarnings = [];
|
|
674
|
+
for (const c of pending) {
|
|
675
|
+
const cw = scanText(c.text, config);
|
|
676
|
+
for (const w of cw) {
|
|
677
|
+
candidateWarnings.push(`${w.message} in candidate ${c.id}`);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
if (candidateWarnings.length > 0) {
|
|
681
|
+
hasIssues = true;
|
|
682
|
+
checks.push({ name: 'candidate_security', status: 'warn', message: `${candidateWarnings.length} warning(s) in pending candidates` });
|
|
683
|
+
if (!options.json) {
|
|
684
|
+
console.log('');
|
|
685
|
+
console.log('Candidate warnings:');
|
|
686
|
+
}
|
|
687
|
+
for (const w of candidateWarnings) {
|
|
688
|
+
if (!options.json) {
|
|
689
|
+
console.warn(` - ${w}`);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
else if (pending.length > 0) {
|
|
694
|
+
checks.push({ name: 'candidate_security', status: 'ok', message: 'No sensitive content detected in pending candidates' });
|
|
695
|
+
if (!options.json) {
|
|
696
|
+
console.log('✔ No sensitive content detected in pending candidates');
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
else {
|
|
700
|
+
checks.push({ name: 'candidate_security', status: 'ok', message: 'No pending candidates to scan' });
|
|
701
|
+
}
|
|
702
|
+
// Stale rejected candidates
|
|
703
|
+
const pruneDays = config.reflective_memory?.prune_rejected_after_days ?? 30;
|
|
704
|
+
const cutoff = new Date(Date.now() - pruneDays * 24 * 60 * 60 * 1000).toISOString();
|
|
705
|
+
const staleRejected = rejected.filter(c => (c.resolved_at ?? c.created_at) < cutoff);
|
|
706
|
+
if (staleRejected.length > 0) {
|
|
707
|
+
checks.push({ name: 'rejected_cleanup', status: 'warn', message: `${staleRejected.length} rejected candidate(s) older than ${pruneDays} days.` });
|
|
708
|
+
console.warn(`⚠ ${staleRejected.length} rejected candidate(s) older than ${pruneDays} days. Run \`brainclaw prune-candidates\` to clean up.`);
|
|
709
|
+
hasIssues = true;
|
|
710
|
+
}
|
|
711
|
+
else {
|
|
712
|
+
checks.push({ name: 'rejected_cleanup', status: 'ok', message: `No stale rejected candidates beyond ${pruneDays} days` });
|
|
713
|
+
}
|
|
714
|
+
// --- Contradiction detection ---
|
|
715
|
+
try {
|
|
716
|
+
const contradictions = detectContradictions(state);
|
|
717
|
+
if (contradictions.length > 0) {
|
|
718
|
+
hasIssues = true;
|
|
719
|
+
checks.push({
|
|
720
|
+
name: 'contradictions',
|
|
721
|
+
status: 'warn',
|
|
722
|
+
message: `${contradictions.length} potential contradiction(s) detected in state.`,
|
|
723
|
+
details: contradictions.slice(0, 5).map((item) => ({
|
|
724
|
+
item_id: item.item_id,
|
|
725
|
+
conflicts_with: item.conflicts_with,
|
|
726
|
+
section: item.section,
|
|
727
|
+
kind: item.kind,
|
|
728
|
+
severity: item.severity,
|
|
729
|
+
score: item.score,
|
|
730
|
+
reason: item.reason,
|
|
731
|
+
})),
|
|
732
|
+
});
|
|
733
|
+
if (!options.json) {
|
|
734
|
+
console.warn(`⚠ ${contradictions.length} potential contradiction(s) detected:`);
|
|
735
|
+
for (const c of contradictions.slice(0, 5)) {
|
|
736
|
+
console.warn(` - [${c.item_id}] vs [${c.conflicts_with}] (${c.severity}, score ${c.score}): ${c.reason}`);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
else {
|
|
741
|
+
checks.push({ name: 'contradictions', status: 'ok', message: 'No contradictions detected in state' });
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
catch (err) {
|
|
745
|
+
logger.debug('Skipping contradictions check (module unavailable):', err);
|
|
746
|
+
}
|
|
747
|
+
// --- Expired items check ---
|
|
748
|
+
const nowIso = new Date().toISOString();
|
|
749
|
+
const expiredNotes = listRuntimeNotes(undefined, options.cwd).filter(n => n.expires_at && n.expires_at < nowIso);
|
|
750
|
+
const expiredConstraints = state.active_constraints.filter(c => c.expires_at && c.expires_at < nowIso && c.status === 'active');
|
|
751
|
+
const expiredTraps = state.known_traps.filter((t) => isTrapExpired(t, nowIso));
|
|
752
|
+
const totalExpired = expiredNotes.length + expiredConstraints.length + expiredTraps.length;
|
|
753
|
+
if (totalExpired > 0) {
|
|
754
|
+
checks.push({ name: 'expired_items', status: 'warn', message: `${totalExpired} expired item(s): ${expiredConstraints.length} constraints, ${expiredNotes.length} notes, ${expiredTraps.length} traps. Run \`brainclaw prune --expired\` to clean up.` });
|
|
755
|
+
if (!options.json) {
|
|
756
|
+
console.warn(`⚠ ${totalExpired} expired item(s). Run \`brainclaw prune --expired\` to clean up.`);
|
|
757
|
+
}
|
|
758
|
+
hasIssues = true;
|
|
759
|
+
}
|
|
760
|
+
else {
|
|
761
|
+
checks.push({ name: 'expired_items', status: 'ok', message: 'No expired items found' });
|
|
762
|
+
}
|
|
763
|
+
// --- Claims checks ---
|
|
764
|
+
const claims = listClaims(options.cwd);
|
|
765
|
+
const activeClaims = claims.filter(c => c.status === 'active');
|
|
766
|
+
if (!options.json) {
|
|
767
|
+
console.log('');
|
|
768
|
+
console.log(`Claims: ${activeClaims.length} active, ${claims.length - activeClaims.length} released`);
|
|
769
|
+
}
|
|
770
|
+
// Check for duplicate scope claims
|
|
771
|
+
const scopeCounts = new Map();
|
|
772
|
+
for (const c of activeClaims) {
|
|
773
|
+
const list = scopeCounts.get(c.scope) ?? [];
|
|
774
|
+
list.push(c.id);
|
|
775
|
+
scopeCounts.set(c.scope, list);
|
|
776
|
+
}
|
|
777
|
+
for (const [scope, ids] of scopeCounts) {
|
|
778
|
+
if (ids.length > 1) {
|
|
779
|
+
checks.push({ name: 'claim_collisions', status: 'warn', message: `Multiple active claims on scope "${scope}": ${ids.join(', ')}` });
|
|
780
|
+
console.warn(`⚠ Multiple active claims on scope "${scope}": ${ids.join(', ')}`);
|
|
781
|
+
hasIssues = true;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
if (!checks.some((c) => c.name === 'claim_collisions')) {
|
|
785
|
+
checks.push({ name: 'claim_collisions', status: 'ok', message: 'No overlapping active claims detected' });
|
|
786
|
+
}
|
|
787
|
+
// Warn if active claims have no linked plan
|
|
788
|
+
const unlinkedClaims = activeClaims.filter((c) => !c.plan_id);
|
|
789
|
+
if (unlinkedClaims.length > 0) {
|
|
790
|
+
const ids = unlinkedClaims.map((c) => c.id).join(', ');
|
|
791
|
+
checks.push({
|
|
792
|
+
name: 'claim_plan_link',
|
|
793
|
+
status: 'warn',
|
|
794
|
+
message: `${unlinkedClaims.length} active claim(s) have no linked plan item: ${ids}. Pass planId when calling bclaw_claim to trace work to the backlog.`,
|
|
795
|
+
});
|
|
796
|
+
if (!options.json) {
|
|
797
|
+
console.warn(`⚠ ${unlinkedClaims.length} active claim(s) have no linked plan item: ${ids}`);
|
|
798
|
+
console.warn(' Pass planId when calling bclaw_claim to trace work to the backlog.');
|
|
799
|
+
}
|
|
800
|
+
hasIssues = true;
|
|
801
|
+
}
|
|
802
|
+
else if (activeClaims.length > 0) {
|
|
803
|
+
checks.push({ name: 'claim_plan_link', status: 'ok', message: `All ${activeClaims.length} active claim(s) are linked to a plan item` });
|
|
804
|
+
if (!options.json) {
|
|
805
|
+
console.log(`✔ Claim plan links: all ${activeClaims.length} claim(s) linked`);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
else {
|
|
809
|
+
checks.push({ name: 'claim_plan_link', status: 'ok', message: 'No active claims to check' });
|
|
810
|
+
}
|
|
811
|
+
// --- Runtime notes checks ---
|
|
812
|
+
const notes = listRuntimeNotes(undefined, options.cwd);
|
|
813
|
+
const localTraps = listOperationalTraps({}, options.cwd);
|
|
814
|
+
if (!options.json) {
|
|
815
|
+
console.log(`Runtime notes: ${notes.length} total`);
|
|
816
|
+
console.log(`Local traps: ${localTraps.length} visible on this host`);
|
|
817
|
+
}
|
|
818
|
+
const marker = readContextMarker(options.cwd);
|
|
819
|
+
const visibleMemoryVersion = getVisibleMemoryVersion({ cwd: options.cwd });
|
|
820
|
+
if (marker?.memory_version && marker.memory_version !== visibleMemoryVersion) {
|
|
821
|
+
checks.push({
|
|
822
|
+
name: 'context_freshness',
|
|
823
|
+
status: 'warn',
|
|
824
|
+
message: `Last context read is stale for this host (marker ${marker.memory_version}, current ${visibleMemoryVersion}).`,
|
|
825
|
+
});
|
|
826
|
+
if (!options.json) {
|
|
827
|
+
console.warn('⚠ Last context read is stale for this host. Run `brainclaw context` again before acting on old memory.');
|
|
828
|
+
}
|
|
829
|
+
hasIssues = true;
|
|
830
|
+
}
|
|
831
|
+
else {
|
|
832
|
+
checks.push({
|
|
833
|
+
name: 'context_freshness',
|
|
834
|
+
status: 'ok',
|
|
835
|
+
message: marker?.memory_version ? 'Last context marker matches current visible memory version' : 'No context marker found',
|
|
836
|
+
});
|
|
837
|
+
if (!options.json && marker?.memory_version) {
|
|
838
|
+
console.log('✔ Context freshness marker matches current visible memory');
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
// --- Runtime events integrity checks ---
|
|
842
|
+
const events = listRuntimeEvents(options.cwd);
|
|
843
|
+
if (events.length > 0) {
|
|
844
|
+
const sessions = new Map();
|
|
845
|
+
for (const event of events) {
|
|
846
|
+
const sessionValue = resolveEventSessionId(event);
|
|
847
|
+
if (!sessionValue)
|
|
848
|
+
continue;
|
|
849
|
+
const set = sessions.get(sessionValue) ?? new Set();
|
|
850
|
+
set.add(event.event_type);
|
|
851
|
+
sessions.set(sessionValue, set);
|
|
852
|
+
}
|
|
853
|
+
if (sessions.size > 0) {
|
|
854
|
+
let incompleteSessions = 0;
|
|
855
|
+
for (const [sessionId, eventTypes] of sessions.entries()) {
|
|
856
|
+
if (!eventTypes.has('task_finished')) {
|
|
857
|
+
incompleteSessions++;
|
|
858
|
+
checks.push({ name: 'runtime_sessions', status: 'warn', message: `Runtime session '${sessionId}' has no task_finished event.` });
|
|
859
|
+
console.warn(`⚠ Runtime session '${sessionId}' has no task_finished event.`);
|
|
860
|
+
hasIssues = true;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
if (!options.json) {
|
|
864
|
+
console.log(`Runtime events: ${events.length} total across ${sessions.size} session(s)`);
|
|
865
|
+
}
|
|
866
|
+
if (incompleteSessions === 0) {
|
|
867
|
+
checks.push({ name: 'runtime_sessions', status: 'ok', message: 'Runtime sessions look consistent' });
|
|
868
|
+
if (!options.json) {
|
|
869
|
+
console.log('✔ Runtime sessions look consistent');
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
else {
|
|
874
|
+
checks.push({ name: 'runtime_sessions', status: 'ok', message: `Runtime events: ${events.length} total (no session metadata)` });
|
|
875
|
+
if (!options.json) {
|
|
876
|
+
console.log(`Runtime events: ${events.length} total (no session metadata)`);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
else {
|
|
881
|
+
checks.push({ name: 'runtime_sessions', status: 'ok', message: 'No runtime events found' });
|
|
882
|
+
}
|
|
883
|
+
const metrics = {
|
|
884
|
+
active_instructions: activeInstructions.length,
|
|
885
|
+
active_plan_items: activePlans.length,
|
|
886
|
+
blocked_plan_items: blockedPlans.length,
|
|
887
|
+
promotion_ready_candidates: promotionReady.length,
|
|
888
|
+
pending_candidates: pending.length,
|
|
889
|
+
accepted_candidates: accepted.length,
|
|
890
|
+
rejected_candidates: rejected.length,
|
|
891
|
+
pending_overdue: pendingOverdue.length,
|
|
892
|
+
avg_review_hours: Number(avgReviewHours.toFixed(1)),
|
|
893
|
+
review_sla_hours: reviewSlaHours,
|
|
894
|
+
active_claims: activeClaims.length,
|
|
895
|
+
released_claims: claims.length - activeClaims.length,
|
|
896
|
+
runtime_notes: notes.length,
|
|
897
|
+
visible_local_traps: localTraps.length,
|
|
898
|
+
memory_version: visibleMemoryVersion,
|
|
899
|
+
stale_context: Boolean(marker?.memory_version && marker.memory_version !== visibleMemoryVersion),
|
|
900
|
+
runtime_events: events.length,
|
|
901
|
+
agent_rules: agentTooling.agents_rules.length,
|
|
902
|
+
local_skills: agentTooling.skills.length,
|
|
903
|
+
incomplete_skills: incompleteSkills.length,
|
|
904
|
+
local_mcp_servers: agentTooling.mcp_servers.length,
|
|
905
|
+
missing_mcp_commands: missingMcpCommands.length,
|
|
906
|
+
agent_git_hygiene_present: agentGitHygiene.presentPaths.length,
|
|
907
|
+
agent_git_hygiene_missing_ignore: agentGitHygiene.missingGitignorePaths.length,
|
|
908
|
+
agent_git_hygiene_tracked: agentGitHygiene.trackedPaths.length,
|
|
909
|
+
declared_agent_integrations: integrationReadiness.length,
|
|
910
|
+
integration_activation_gaps: missingIntegrations.length,
|
|
911
|
+
brainclaw_cli_version: brainclawVersion.cli_version,
|
|
912
|
+
required_brainclaw_version: brainclawVersion.minimum_brainclaw_version,
|
|
913
|
+
recommended_brainclaw_version: brainclawVersion.recommended_brainclaw_version,
|
|
914
|
+
};
|
|
915
|
+
const reputationSummary = buildReputationSummary(options.cwd);
|
|
916
|
+
if (reputationSummary.enabled) {
|
|
917
|
+
checks.push({
|
|
918
|
+
name: 'reputation_summary',
|
|
919
|
+
status: 'ok',
|
|
920
|
+
message: `tracked_agents=${reputationSummary.tracked_agents}, avg_internal_trust=${reputationSummary.avg_internal_trust}`,
|
|
921
|
+
});
|
|
922
|
+
if (!options.json) {
|
|
923
|
+
console.log(`Reputation: ${reputationSummary.tracked_agents} tracked agent(s), avg trust ${reputationSummary.avg_internal_trust}`);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
// Circuit-breaker health check
|
|
927
|
+
const circuitSnapshot = buildCircuitBreakerSnapshot(options.cwd);
|
|
928
|
+
if (circuitSnapshot.tripped_agents.length > 0) {
|
|
929
|
+
const names = circuitSnapshot.tripped_agents.map(a => `${a.agent_key}(${a.rejection_count}/${a.threshold})`).join(', ');
|
|
930
|
+
checks.push({
|
|
931
|
+
name: 'circuit_breaker',
|
|
932
|
+
status: 'warn',
|
|
933
|
+
message: `${circuitSnapshot.tripped_agents.length} agent(s) in circuit-breaker: ${names}`,
|
|
934
|
+
details: circuitSnapshot.tripped_agents,
|
|
935
|
+
});
|
|
936
|
+
hasIssues = true;
|
|
937
|
+
if (!options.json) {
|
|
938
|
+
console.warn(`⚠ Circuit-breaker: ${circuitSnapshot.tripped_agents.length} agent(s) suspended from auto-promote: ${names}`);
|
|
939
|
+
console.warn(` Use 'brainclaw set-trust <agent> --reset-breaker' to restore.`);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
else {
|
|
943
|
+
checks.push({ name: 'circuit_breaker', status: 'ok', message: 'No agents in circuit-breaker' });
|
|
944
|
+
if (!options.json) {
|
|
945
|
+
console.log('✔ Circuit-breaker: no agents suspended');
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
// --- Store hierarchy check ---
|
|
949
|
+
try {
|
|
950
|
+
const storeChain = resolveStoreChain(options.cwd ?? process.cwd());
|
|
951
|
+
if (storeChain.length > 1) {
|
|
952
|
+
const chainDesc = storeChain
|
|
953
|
+
.map((s) => `${s.role}(d=${s.depth})`)
|
|
954
|
+
.join(' → ');
|
|
955
|
+
// Warn if multiple stores declare the same non-unknown role
|
|
956
|
+
const roleCounts = new Map();
|
|
957
|
+
for (const s of storeChain) {
|
|
958
|
+
if (s.role !== 'unknown') {
|
|
959
|
+
roleCounts.set(s.role, (roleCounts.get(s.role) ?? 0) + 1);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
const duplicateRoles = [...roleCounts.entries()]
|
|
963
|
+
.filter(([, count]) => count > 1)
|
|
964
|
+
.map(([role]) => role);
|
|
965
|
+
if (duplicateRoles.length > 0) {
|
|
966
|
+
checks.push({
|
|
967
|
+
name: 'store_hierarchy',
|
|
968
|
+
status: 'warn',
|
|
969
|
+
message: `Store hierarchy has duplicate roles: ${duplicateRoles.join(', ')}. Chain: ${chainDesc}`,
|
|
970
|
+
details: storeChain,
|
|
971
|
+
});
|
|
972
|
+
if (!options.json) {
|
|
973
|
+
console.warn(`⚠ Store hierarchy has duplicate roles (${duplicateRoles.join(', ')}): ${chainDesc}`);
|
|
974
|
+
}
|
|
975
|
+
hasIssues = true;
|
|
976
|
+
}
|
|
977
|
+
else {
|
|
978
|
+
checks.push({
|
|
979
|
+
name: 'store_hierarchy',
|
|
980
|
+
status: 'ok',
|
|
981
|
+
message: `Store chain (${storeChain.length} stores): ${chainDesc}`,
|
|
982
|
+
details: storeChain,
|
|
983
|
+
});
|
|
984
|
+
if (!options.json) {
|
|
985
|
+
console.log(`✔ Store chain (${storeChain.length}): ${chainDesc}`);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
else {
|
|
990
|
+
checks.push({
|
|
991
|
+
name: 'store_hierarchy',
|
|
992
|
+
status: 'ok',
|
|
993
|
+
message: 'Single store — no parent stores found in hierarchy',
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
catch {
|
|
998
|
+
// non-fatal
|
|
999
|
+
}
|
|
1000
|
+
// Cross-project links validation
|
|
1001
|
+
try {
|
|
1002
|
+
const links = resolveCrossProjectLinks(options.cwd);
|
|
1003
|
+
if (links.length > 0) {
|
|
1004
|
+
const unavailable = links.filter((l) => !l.available);
|
|
1005
|
+
if (unavailable.length > 0) {
|
|
1006
|
+
hasIssues = true;
|
|
1007
|
+
const paths = unavailable.map((l) => l.path).join(', ');
|
|
1008
|
+
checks.push({ name: 'cross_project_links', status: 'error', message: `cross_project_links: ${unavailable.length} unreachable path(s): ${paths}` });
|
|
1009
|
+
if (!options.json)
|
|
1010
|
+
console.error(`✗ cross_project_links: ${unavailable.length} unreachable path(s): ${paths}`);
|
|
1011
|
+
}
|
|
1012
|
+
else {
|
|
1013
|
+
checks.push({ name: 'cross_project_links', status: 'ok', message: `cross_project_links: ${links.length} link(s) OK` });
|
|
1014
|
+
if (!options.json)
|
|
1015
|
+
console.log(`✔ cross_project_links: ${links.length} link(s) OK`);
|
|
1016
|
+
}
|
|
1017
|
+
const cycles = detectCrossProjectCycles(options.cwd);
|
|
1018
|
+
if (cycles.length > 0) {
|
|
1019
|
+
hasIssues = true;
|
|
1020
|
+
checks.push({ name: 'cross_project_cycles', status: 'error', message: `cross_project_links cycle detected: ${cycles[0].join(' → ')}` });
|
|
1021
|
+
if (!options.json)
|
|
1022
|
+
console.error(`✗ cross_project_links cycle: ${cycles[0].join(' → ')}`);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
catch {
|
|
1027
|
+
// non-fatal
|
|
1028
|
+
}
|
|
1029
|
+
// --- Backlog patterns in open handoffs ---
|
|
1030
|
+
try {
|
|
1031
|
+
const openHandoffs = state.open_handoffs.filter((h) => h.status === 'open');
|
|
1032
|
+
const handoffsWithBacklog = openHandoffs.filter((h) => !h.plan_id && hasBacklogPatterns(h.text));
|
|
1033
|
+
if (handoffsWithBacklog.length > 0) {
|
|
1034
|
+
const ids = handoffsWithBacklog.map((h) => h.id).join(', ');
|
|
1035
|
+
checks.push({
|
|
1036
|
+
name: 'handoff_backlog',
|
|
1037
|
+
status: 'warn',
|
|
1038
|
+
message: `${handoffsWithBacklog.length} open handoff(s) contain backlog patterns without a linked plan: ${ids}. Run \`brainclaw plan create "<text>"\` to formalize.`,
|
|
1039
|
+
details: handoffsWithBacklog.map((h) => ({ id: h.id, from: h.from, to: h.to })),
|
|
1040
|
+
});
|
|
1041
|
+
if (!options.json) {
|
|
1042
|
+
console.warn(`⚠ ${handoffsWithBacklog.length} open handoff(s) contain unformalized backlog (no plan linked): ${ids}`);
|
|
1043
|
+
console.warn(' Run `brainclaw plan create "<description>"` to create formal plan items.');
|
|
1044
|
+
}
|
|
1045
|
+
hasIssues = true;
|
|
1046
|
+
}
|
|
1047
|
+
else {
|
|
1048
|
+
checks.push({
|
|
1049
|
+
name: 'handoff_backlog',
|
|
1050
|
+
status: 'ok',
|
|
1051
|
+
message: openHandoffs.length > 0
|
|
1052
|
+
? `${openHandoffs.length} open handoff(s) checked — no unformalized backlog detected`
|
|
1053
|
+
: 'No open handoffs to check',
|
|
1054
|
+
});
|
|
1055
|
+
if (!options.json && openHandoffs.length > 0) {
|
|
1056
|
+
console.log(`✔ Handoff backlog: ${openHandoffs.length} open handoff(s) checked, all covered`);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
// Check scope hygiene - warn if machine-level items are at project level
|
|
1060
|
+
try {
|
|
1061
|
+
const MACHINE_TAGS = ['windows', 'wsl', 'powershell', 'ssh', 'linux', 'macos', 'env', 'path', 'node-path'];
|
|
1062
|
+
const chain = resolveStoreChain(options.cwd);
|
|
1063
|
+
const userStoreExists = chain.some((s) => s.role === 'user');
|
|
1064
|
+
if (userStoreExists) {
|
|
1065
|
+
const projectState = loadState(options.cwd);
|
|
1066
|
+
const projectLevelItems = [];
|
|
1067
|
+
// Collect project-level items with machine-generic tags
|
|
1068
|
+
for (const constraint of projectState.active_constraints) {
|
|
1069
|
+
const hasMachineTag = constraint.tags.some((t) => MACHINE_TAGS.includes(t.toLowerCase()));
|
|
1070
|
+
if (hasMachineTag)
|
|
1071
|
+
projectLevelItems.push({ id: constraint.id, type: 'constraint', text: constraint.text, tags: constraint.tags });
|
|
1072
|
+
}
|
|
1073
|
+
for (const decision of projectState.recent_decisions) {
|
|
1074
|
+
const hasMachineTag = decision.tags.some((t) => MACHINE_TAGS.includes(t.toLowerCase()));
|
|
1075
|
+
if (hasMachineTag)
|
|
1076
|
+
projectLevelItems.push({ id: decision.id, type: 'decision', text: decision.text, tags: decision.tags });
|
|
1077
|
+
}
|
|
1078
|
+
for (const trap of projectState.known_traps) {
|
|
1079
|
+
const hasMachineTag = trap.tags.some((t) => MACHINE_TAGS.includes(t.toLowerCase()));
|
|
1080
|
+
if (hasMachineTag)
|
|
1081
|
+
projectLevelItems.push({ id: trap.id, type: 'trap', text: trap.text, tags: trap.tags });
|
|
1082
|
+
}
|
|
1083
|
+
if (projectLevelItems.length > 0) {
|
|
1084
|
+
const itemDescr = projectLevelItems.map((i) => `${i.type}[${i.id.slice(0, 8)}]`).join(', ');
|
|
1085
|
+
checks.push({
|
|
1086
|
+
name: 'scope_hygiene',
|
|
1087
|
+
status: 'warn',
|
|
1088
|
+
message: `${projectLevelItems.length} project-level item(s) have machine-generic tags: ${itemDescr}. Consider moving to user store.`,
|
|
1089
|
+
details: projectLevelItems,
|
|
1090
|
+
});
|
|
1091
|
+
if (!options.json) {
|
|
1092
|
+
console.warn(`⚠ Scope hygiene: ${projectLevelItems.length} project-level item(s) with machine tags (windows, wsl, ssh, etc.) should be in user store`);
|
|
1093
|
+
projectLevelItems.forEach((item) => {
|
|
1094
|
+
console.warn(` - [${item.id.slice(0, 8)}] ${item.type}: tags=[${item.tags.join(', ')}]`);
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
else {
|
|
1099
|
+
checks.push({ name: 'scope_hygiene', status: 'ok', message: 'No machine-level items detected at project scope' });
|
|
1100
|
+
if (!options.json) {
|
|
1101
|
+
console.log('✔ Scope hygiene: no machine-level items at project scope');
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
else {
|
|
1106
|
+
checks.push({ name: 'scope_hygiene', status: 'ok', message: 'User store not configured (scope hygiene skipped)' });
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
catch { /* non-fatal */ }
|
|
1110
|
+
// Check for cross-level duplicates
|
|
1111
|
+
try {
|
|
1112
|
+
const chain = resolveStoreChain(options.cwd);
|
|
1113
|
+
const allConstraints = new Map();
|
|
1114
|
+
const allDecisions = new Map();
|
|
1115
|
+
const allTraps = new Map();
|
|
1116
|
+
// Collect items from all stores
|
|
1117
|
+
for (const store of chain) {
|
|
1118
|
+
try {
|
|
1119
|
+
const storeState = loadState(store.cwd);
|
|
1120
|
+
const storeName = store.role;
|
|
1121
|
+
for (const constraint of storeState.active_constraints) {
|
|
1122
|
+
const key = constraint.text.slice(0, 60).toLowerCase();
|
|
1123
|
+
if (!allConstraints.has(key))
|
|
1124
|
+
allConstraints.set(key, []);
|
|
1125
|
+
allConstraints.get(key).push({ id: constraint.id, store: storeName, text: constraint.text });
|
|
1126
|
+
}
|
|
1127
|
+
for (const decision of storeState.recent_decisions) {
|
|
1128
|
+
const key = decision.text.slice(0, 60).toLowerCase();
|
|
1129
|
+
if (!allDecisions.has(key))
|
|
1130
|
+
allDecisions.set(key, []);
|
|
1131
|
+
allDecisions.get(key).push({ id: decision.id, store: storeName, text: decision.text });
|
|
1132
|
+
}
|
|
1133
|
+
for (const trap of storeState.known_traps) {
|
|
1134
|
+
const key = trap.text.slice(0, 60).toLowerCase();
|
|
1135
|
+
if (!allTraps.has(key))
|
|
1136
|
+
allTraps.set(key, []);
|
|
1137
|
+
allTraps.get(key).push({ id: trap.id, store: storeName, text: trap.text });
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
catch { /* skip stores that can't be read */ }
|
|
1141
|
+
}
|
|
1142
|
+
// Find duplicates (same text at different levels)
|
|
1143
|
+
const duplicates = [];
|
|
1144
|
+
allConstraints.forEach((items, key) => {
|
|
1145
|
+
if (items.length > 1 && new Set(items.map((i) => i.store)).size > 1) {
|
|
1146
|
+
duplicates.push({
|
|
1147
|
+
type: 'constraint',
|
|
1148
|
+
text: items[0].text,
|
|
1149
|
+
items: items.map((i) => ({ id: i.id, store: i.store })),
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
});
|
|
1153
|
+
allDecisions.forEach((items, key) => {
|
|
1154
|
+
if (items.length > 1 && new Set(items.map((i) => i.store)).size > 1) {
|
|
1155
|
+
duplicates.push({
|
|
1156
|
+
type: 'decision',
|
|
1157
|
+
text: items[0].text,
|
|
1158
|
+
items: items.map((i) => ({ id: i.id, store: i.store })),
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
});
|
|
1162
|
+
allTraps.forEach((items, key) => {
|
|
1163
|
+
if (items.length > 1 && new Set(items.map((i) => i.store)).size > 1) {
|
|
1164
|
+
duplicates.push({
|
|
1165
|
+
type: 'trap',
|
|
1166
|
+
text: items[0].text,
|
|
1167
|
+
items: items.map((i) => ({ id: i.id, store: i.store })),
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1170
|
+
});
|
|
1171
|
+
if (duplicates.length > 0) {
|
|
1172
|
+
checks.push({
|
|
1173
|
+
name: 'cross_level_duplicates',
|
|
1174
|
+
status: 'warn',
|
|
1175
|
+
message: `${duplicates.length} potential duplicate(s) detected across store levels. Review and deduplicate if needed.`,
|
|
1176
|
+
details: duplicates.slice(0, 10), // Limit to 10 for brevity
|
|
1177
|
+
});
|
|
1178
|
+
if (!options.json) {
|
|
1179
|
+
console.warn(`⚠ Cross-level duplicates: ${duplicates.length} potential duplicate(s) across store levels`);
|
|
1180
|
+
duplicates.slice(0, 10).forEach((dup) => {
|
|
1181
|
+
console.warn(` - ${dup.type}: "${dup.text.slice(0, 40)}..." at [${dup.items.map((i) => i.store).join(', ')}]`);
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
else {
|
|
1186
|
+
checks.push({ name: 'cross_level_duplicates', status: 'ok', message: 'No cross-level duplicates detected' });
|
|
1187
|
+
if (!options.json) {
|
|
1188
|
+
console.log('✔ Cross-level duplicates: no duplicates across store levels');
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
// Metadata consistency checks
|
|
1192
|
+
const capabilities = state.recent_decisions.filter((d) => d.tags.includes('capability'));
|
|
1193
|
+
const tools = state.recent_decisions.filter((d) => d.tags.includes('tool'));
|
|
1194
|
+
const metadataIssues = [];
|
|
1195
|
+
// Check capabilities completeness
|
|
1196
|
+
capabilities.forEach((cap) => {
|
|
1197
|
+
const category = cap.tags.find((t) => t !== 'capability');
|
|
1198
|
+
if (!category) {
|
|
1199
|
+
metadataIssues.push(`Capability [${cap.id}] missing category`);
|
|
1200
|
+
}
|
|
1201
|
+
if (!cap.text || cap.text.trim().length === 0) {
|
|
1202
|
+
metadataIssues.push(`Capability [${cap.id}] has empty description`);
|
|
1203
|
+
}
|
|
1204
|
+
});
|
|
1205
|
+
// Check tools completeness
|
|
1206
|
+
tools.forEach((tool) => {
|
|
1207
|
+
const type = tool.tags.find((t) => t !== 'tool');
|
|
1208
|
+
if (!type) {
|
|
1209
|
+
metadataIssues.push(`Tool [${tool.id}] missing type`);
|
|
1210
|
+
}
|
|
1211
|
+
if (!tool.text || tool.text.trim().length === 0) {
|
|
1212
|
+
metadataIssues.push(`Tool [${tool.id}] has empty description`);
|
|
1213
|
+
}
|
|
1214
|
+
});
|
|
1215
|
+
if (metadataIssues.length > 0) {
|
|
1216
|
+
checks.push({
|
|
1217
|
+
name: 'metadata_consistency',
|
|
1218
|
+
status: 'warn',
|
|
1219
|
+
message: `${metadataIssues.length} metadata inconsistency(ies) found. Capabilities/tools may be incomplete.`,
|
|
1220
|
+
details: metadataIssues.slice(0, 10),
|
|
1221
|
+
});
|
|
1222
|
+
if (!options.json) {
|
|
1223
|
+
console.warn(`⚠ Metadata consistency: ${metadataIssues.length} issue(s) found`);
|
|
1224
|
+
metadataIssues.slice(0, 10).forEach((issue) => {
|
|
1225
|
+
console.warn(` - ${issue}`);
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
else {
|
|
1230
|
+
checks.push({
|
|
1231
|
+
name: 'metadata_consistency',
|
|
1232
|
+
status: 'ok',
|
|
1233
|
+
message: `Metadata consistency OK (${capabilities.length} capabilities, ${tools.length} tools)`,
|
|
1234
|
+
});
|
|
1235
|
+
if (!options.json) {
|
|
1236
|
+
console.log(`✔ Metadata consistency: ${capabilities.length} capabilities, ${tools.length} tools registered`);
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
catch { /* non-fatal */ }
|
|
1241
|
+
}
|
|
1242
|
+
catch { /* non-fatal */ }
|
|
1243
|
+
if (options.json) {
|
|
1244
|
+
console.log(JSON.stringify({
|
|
1245
|
+
ok: !hasIssues,
|
|
1246
|
+
checks,
|
|
1247
|
+
metrics: {
|
|
1248
|
+
...metrics,
|
|
1249
|
+
migration_outdated_documents: migrationEntries.filter((entry) => entry.status === 'outdated').length,
|
|
1250
|
+
migration_invalid_documents: migrationEntries.filter((entry) => entry.status === 'invalid').length,
|
|
1251
|
+
reputation_enabled: reputationSummary.enabled,
|
|
1252
|
+
reputation_tracked_agents: reputationSummary.tracked_agents,
|
|
1253
|
+
reputation_avg_internal_trust: reputationSummary.avg_internal_trust,
|
|
1254
|
+
reputation_current_agent_trust: reputationSummary.current_agent_trust ?? 0,
|
|
1255
|
+
circuit_breaker_tripped_count: circuitSnapshot.tripped_agents.length,
|
|
1256
|
+
circuit_breaker_threshold: circuitSnapshot.threshold,
|
|
1257
|
+
circuit_breaker_window_days: circuitSnapshot.window_days,
|
|
1258
|
+
agent_git_hygiene_fixed: agentGitHygieneFixed.length,
|
|
1259
|
+
},
|
|
1260
|
+
migration: options.migrationCheck
|
|
1261
|
+
? {
|
|
1262
|
+
entries: migrationEntries,
|
|
1263
|
+
outdated: migrationEntries.filter((entry) => entry.status === 'outdated').length,
|
|
1264
|
+
invalid: migrationEntries.filter((entry) => entry.status === 'invalid').length,
|
|
1265
|
+
}
|
|
1266
|
+
: undefined,
|
|
1267
|
+
}, null, 2));
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
if (!hasIssues) {
|
|
1271
|
+
console.log('');
|
|
1272
|
+
console.log('All checks passed.');
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
//# sourceMappingURL=doctor.js.map
|