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.
Files changed (128) hide show
  1. package/LICENSE +74 -0
  2. package/README.md +226 -0
  3. package/dist/cli.js +1037 -0
  4. package/dist/commands/accept.js +149 -0
  5. package/dist/commands/adapter-openclaw-import.js +75 -0
  6. package/dist/commands/add-step.js +35 -0
  7. package/dist/commands/agent-board.js +106 -0
  8. package/dist/commands/audit.js +35 -0
  9. package/dist/commands/bootstrap.js +34 -0
  10. package/dist/commands/capability.js +104 -0
  11. package/dist/commands/changes.js +112 -0
  12. package/dist/commands/check-constraints.js +63 -0
  13. package/dist/commands/claim-resource.js +54 -0
  14. package/dist/commands/claim.js +92 -0
  15. package/dist/commands/complete-step.js +34 -0
  16. package/dist/commands/constraint.js +44 -0
  17. package/dist/commands/context-diff.js +32 -0
  18. package/dist/commands/context.js +63 -0
  19. package/dist/commands/decision.js +45 -0
  20. package/dist/commands/delete-plan.js +20 -0
  21. package/dist/commands/diff.js +99 -0
  22. package/dist/commands/doctor.js +1275 -0
  23. package/dist/commands/enable-agent.js +63 -0
  24. package/dist/commands/env.js +46 -0
  25. package/dist/commands/estimation-report.js +167 -0
  26. package/dist/commands/explore.js +47 -0
  27. package/dist/commands/export.js +381 -0
  28. package/dist/commands/handoff.js +63 -0
  29. package/dist/commands/history.js +22 -0
  30. package/dist/commands/hooks.js +123 -0
  31. package/dist/commands/init.js +356 -0
  32. package/dist/commands/install-hooks.js +115 -0
  33. package/dist/commands/instruction.js +56 -0
  34. package/dist/commands/list-agents.js +44 -0
  35. package/dist/commands/list-claims.js +45 -0
  36. package/dist/commands/list-instructions.js +50 -0
  37. package/dist/commands/list-plans.js +48 -0
  38. package/dist/commands/mcp-worker.js +12 -0
  39. package/dist/commands/mcp.js +2272 -0
  40. package/dist/commands/memory.js +283 -0
  41. package/dist/commands/metrics.js +175 -0
  42. package/dist/commands/plan-resource.js +62 -0
  43. package/dist/commands/plan.js +76 -0
  44. package/dist/commands/prune-candidates.js +36 -0
  45. package/dist/commands/prune.js +48 -0
  46. package/dist/commands/pull.js +25 -0
  47. package/dist/commands/push.js +28 -0
  48. package/dist/commands/rebuild.js +14 -0
  49. package/dist/commands/reflect-runtime-note.js +74 -0
  50. package/dist/commands/reflect.js +286 -0
  51. package/dist/commands/register-agent.js +29 -0
  52. package/dist/commands/reject.js +52 -0
  53. package/dist/commands/release-claim.js +41 -0
  54. package/dist/commands/release-claims.js +67 -0
  55. package/dist/commands/review.js +242 -0
  56. package/dist/commands/rollback.js +156 -0
  57. package/dist/commands/runtime-note.js +144 -0
  58. package/dist/commands/runtime-status.js +49 -0
  59. package/dist/commands/search.js +36 -0
  60. package/dist/commands/session-end.js +187 -0
  61. package/dist/commands/session-start.js +147 -0
  62. package/dist/commands/set-trust.js +92 -0
  63. package/dist/commands/setup.js +446 -0
  64. package/dist/commands/show-candidate.js +31 -0
  65. package/dist/commands/star-candidate.js +28 -0
  66. package/dist/commands/status.js +133 -0
  67. package/dist/commands/sync.js +159 -0
  68. package/dist/commands/tool.js +126 -0
  69. package/dist/commands/trap.js +74 -0
  70. package/dist/commands/update-handoff.js +23 -0
  71. package/dist/commands/update-plan.js +37 -0
  72. package/dist/commands/upgrade.js +382 -0
  73. package/dist/commands/use-candidate.js +35 -0
  74. package/dist/commands/version.js +96 -0
  75. package/dist/commands/watch.js +215 -0
  76. package/dist/commands/whoami.js +104 -0
  77. package/dist/core/agent-context.js +340 -0
  78. package/dist/core/agent-files.js +874 -0
  79. package/dist/core/agent-integrations.js +135 -0
  80. package/dist/core/agent-inventory.js +401 -0
  81. package/dist/core/agent-registry.js +420 -0
  82. package/dist/core/ai-agent-detection.js +140 -0
  83. package/dist/core/audit.js +85 -0
  84. package/dist/core/bootstrap.js +658 -0
  85. package/dist/core/brainclaw-version.js +433 -0
  86. package/dist/core/candidates.js +137 -0
  87. package/dist/core/circuit-breaker.js +118 -0
  88. package/dist/core/claims.js +72 -0
  89. package/dist/core/config.js +86 -0
  90. package/dist/core/context-diff.js +122 -0
  91. package/dist/core/context.js +1212 -0
  92. package/dist/core/contradictions.js +270 -0
  93. package/dist/core/coordination.js +86 -0
  94. package/dist/core/cross-project.js +99 -0
  95. package/dist/core/duplicates.js +72 -0
  96. package/dist/core/event-log.js +152 -0
  97. package/dist/core/events.js +56 -0
  98. package/dist/core/execution-context.js +204 -0
  99. package/dist/core/freshness.js +87 -0
  100. package/dist/core/global-registry.js +182 -0
  101. package/dist/core/host.js +10 -0
  102. package/dist/core/identity.js +151 -0
  103. package/dist/core/ids.js +56 -0
  104. package/dist/core/input-validation.js +81 -0
  105. package/dist/core/instructions.js +117 -0
  106. package/dist/core/io.js +191 -0
  107. package/dist/core/json-store.js +63 -0
  108. package/dist/core/lifecycle.js +45 -0
  109. package/dist/core/lock.js +129 -0
  110. package/dist/core/logger.js +49 -0
  111. package/dist/core/machine-profile.js +332 -0
  112. package/dist/core/markdown.js +120 -0
  113. package/dist/core/memory-git.js +133 -0
  114. package/dist/core/migration.js +247 -0
  115. package/dist/core/project-registry.js +64 -0
  116. package/dist/core/reflection-safety.js +21 -0
  117. package/dist/core/repo-analysis.js +133 -0
  118. package/dist/core/reputation.js +409 -0
  119. package/dist/core/runtime.js +134 -0
  120. package/dist/core/schema.js +580 -0
  121. package/dist/core/search.js +115 -0
  122. package/dist/core/security.js +66 -0
  123. package/dist/core/setup-state.js +50 -0
  124. package/dist/core/state.js +83 -0
  125. package/dist/core/store-resolution.js +119 -0
  126. package/dist/core/sync-remote.js +83 -0
  127. package/dist/core/traps.js +86 -0
  128. 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