brainclaw 0.28.0 → 1.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +193 -170
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/cli.js +683 -23
- package/dist/commands/accept.js +3 -0
- package/dist/commands/add-step.js +11 -26
- package/dist/commands/agent-board.js +70 -3
- package/dist/commands/audit.js +19 -0
- package/dist/commands/check-policy.js +54 -0
- package/dist/commands/check-security-mcp.js +145 -0
- package/dist/commands/check-security.js +106 -0
- package/dist/commands/claim-resource.js +1 -0
- package/dist/commands/codev.js +672 -0
- package/dist/commands/compact.js +74 -0
- package/dist/commands/complete-step.js +16 -26
- package/dist/commands/constraint.js +8 -20
- package/dist/commands/decision.js +9 -20
- package/dist/commands/delete-plan.js +10 -12
- package/dist/commands/delete-step.js +16 -0
- package/dist/commands/dispatch.js +163 -0
- package/dist/commands/doctor.js +1122 -49
- package/dist/commands/enable-agent.js +1 -0
- package/dist/commands/export.js +280 -22
- package/dist/commands/handoff.js +33 -0
- package/dist/commands/harvest.js +189 -0
- package/dist/commands/hooks.js +82 -25
- package/dist/commands/inbox.js +169 -0
- package/dist/commands/init.js +38 -31
- package/dist/commands/install-hooks.js +71 -44
- package/dist/commands/link.js +89 -0
- package/dist/commands/list-claims.js +48 -3
- package/dist/commands/list-plans.js +129 -25
- package/dist/commands/loops-handlers.js +409 -0
- package/dist/commands/mcp-read-handlers.js +1628 -0
- package/dist/commands/mcp-schemas.generated.js +74 -0
- package/dist/commands/mcp.js +4244 -1475
- package/dist/commands/plan-resource.js +64 -0
- package/dist/commands/plan.js +12 -26
- package/dist/commands/prune.js +37 -2
- package/dist/commands/reflect.js +20 -7
- package/dist/commands/release-claim.js +11 -6
- package/dist/commands/release-notes.js +170 -0
- package/dist/commands/repair.js +210 -0
- package/dist/commands/run-profile.js +57 -0
- package/dist/commands/sequence.js +113 -0
- package/dist/commands/session-end.js +423 -14
- package/dist/commands/session-start.js +214 -41
- package/dist/commands/setup-security.js +103 -0
- package/dist/commands/setup.js +42 -4
- package/dist/commands/stale.js +109 -0
- package/dist/commands/switch.js +131 -10
- package/dist/commands/trap.js +14 -31
- package/dist/commands/update-handoff.js +63 -4
- package/dist/commands/update-plan.js +21 -28
- package/dist/commands/update-step.js +37 -0
- package/dist/commands/upgrade.js +313 -6
- package/dist/commands/usage.js +102 -0
- package/dist/commands/version.js +20 -0
- package/dist/commands/who.js +124 -0
- package/dist/commands/worktree.js +105 -0
- package/dist/core/actions.js +315 -0
- package/dist/core/agent-capability.js +610 -17
- package/dist/core/agent-context.js +7 -1
- package/dist/core/agent-files.js +1169 -85
- package/dist/core/agent-integrations.js +160 -5
- package/dist/core/agent-inventory.js +2 -0
- package/dist/core/agent-profiles.js +93 -0
- package/dist/core/agent-registry.js +162 -30
- package/dist/core/agentrun-reconciler.js +345 -0
- package/dist/core/agentruns.js +424 -0
- package/dist/core/ai-agent-detection.js +31 -10
- package/dist/core/archival.js +77 -0
- package/dist/core/assignment-sweeper.js +82 -0
- package/dist/core/assignments.js +367 -0
- package/dist/core/audit.js +30 -0
- package/dist/core/bootstrap.js +61 -10
- package/dist/core/brainclaw-version.js +94 -2
- package/dist/core/candidates.js +93 -2
- package/dist/core/claims.js +419 -0
- package/dist/core/codev-metrics.js +77 -0
- package/dist/core/codev-personas.js +31 -0
- package/dist/core/codev-plan-gen.js +35 -0
- package/dist/core/codev-prompts.js +74 -0
- package/dist/core/codev-responses.js +62 -0
- package/dist/core/codev-rounds.js +218 -0
- package/dist/core/config.js +4 -0
- package/dist/core/context.js +454 -34
- package/dist/core/coordination.js +201 -6
- package/dist/core/cross-project.js +230 -16
- package/dist/core/default-profiles/doctor.yaml +11 -0
- package/dist/core/default-profiles/janitor.yaml +11 -0
- package/dist/core/default-profiles/onboarder.yaml +11 -0
- package/dist/core/default-profiles/reviewer.yaml +13 -0
- package/dist/core/dispatcher.js +1189 -0
- package/dist/core/duplicates.js +2 -2
- package/dist/core/entity-operations.js +450 -0
- package/dist/core/entity-registry.js +344 -0
- package/dist/core/event-log.js +1 -0
- package/dist/core/events.js +106 -2
- package/dist/core/execution-adapters.js +154 -0
- package/dist/core/execution-context.js +63 -0
- package/dist/core/execution-profile.js +270 -0
- package/dist/core/execution.js +255 -0
- package/dist/core/facade-schema.js +81 -0
- package/dist/core/federation-cloud.js +99 -0
- package/dist/core/federation-message.js +52 -0
- package/dist/core/federation-transport.js +65 -0
- package/dist/core/gc-semantic.js +482 -0
- package/dist/core/governance.js +247 -0
- package/dist/core/guards.js +19 -0
- package/dist/core/ideation.js +72 -0
- package/dist/core/identity.js +252 -28
- package/dist/core/ids.js +6 -0
- package/dist/core/input-validation.js +2 -2
- package/dist/core/instruction-templates.js +344 -136
- package/dist/core/io.js +90 -11
- package/dist/core/lock.js +6 -2
- package/dist/core/loops/brief-assembly.js +213 -0
- package/dist/core/loops/facade-schema.js +148 -0
- package/dist/core/loops/index.js +7 -0
- package/dist/core/loops/iteration-engine.js +139 -0
- package/dist/core/loops/lock.js +385 -0
- package/dist/core/loops/store.js +201 -0
- package/dist/core/loops/types.js +403 -0
- package/dist/core/loops/verbs.js +534 -0
- package/dist/core/markdown.js +15 -3
- package/dist/core/memory-compactor.js +432 -0
- package/dist/core/memory-git.js +152 -8
- package/dist/core/messaging.js +278 -0
- package/dist/core/migration.js +32 -1
- package/dist/core/mutation-pipeline.js +4 -2
- package/dist/core/operations/memory-mutation.js +129 -0
- package/dist/core/operations/memory-write.js +78 -0
- package/dist/core/operations/plan.js +190 -0
- package/dist/core/policy.js +169 -0
- package/dist/core/repo-analysis.js +67 -0
- package/dist/core/reputation.js +9 -3
- package/dist/core/schema.js +546 -21
- package/dist/core/search.js +21 -2
- package/dist/core/security-cache.js +71 -0
- package/dist/core/security-guard.js +152 -0
- package/dist/core/security-scoring.js +86 -0
- package/dist/core/sequence.js +130 -0
- package/dist/core/socket-client.js +113 -0
- package/dist/core/staleness.js +246 -0
- package/dist/core/state.js +98 -22
- package/dist/core/store-resolution.js +54 -12
- package/dist/core/toml-writer.js +76 -0
- package/dist/core/upgrades/backup.js +232 -0
- package/dist/core/upgrades/health-check.js +169 -0
- package/dist/core/upgrades/patches/candidate-archive.js +145 -0
- package/dist/core/upgrades/patches/handoff-review-strip.js +128 -0
- package/dist/core/upgrades/patches/provenance-rollout.js +136 -0
- package/dist/core/upgrades/schema-version.js +97 -0
- package/dist/core/worktree.js +606 -0
- package/dist/facts.js +114 -0
- package/dist/facts.json +111 -0
- package/docs/architecture/project-refs.md +5 -1
- package/docs/cli.md +690 -43
- package/docs/concepts/ideation-loop.md +317 -0
- package/docs/concepts/loop-engine.md +456 -0
- package/docs/concepts/mcp-governance.md +268 -0
- package/docs/concepts/memory-staleness.md +122 -0
- package/docs/concepts/multi-agent-workflows.md +166 -0
- package/docs/concepts/plans-and-claims.md +31 -6
- package/docs/concepts/project-md-convention.md +35 -0
- package/docs/concepts/troubleshooting.md +220 -0
- package/docs/concepts/upgrade-cli.md +202 -0
- package/docs/concepts/upgrade-dogfood-procedure.md +114 -0
- package/docs/context-format-changelog.md +2 -2
- package/docs/context-format.md +2 -2
- package/docs/index.md +68 -0
- package/docs/integrations/agents.md +15 -16
- package/docs/integrations/cline.md +88 -0
- package/docs/integrations/codex.md +75 -23
- package/docs/integrations/continue.md +60 -0
- package/docs/integrations/copilot.md +67 -9
- package/docs/integrations/kilocode.md +72 -0
- package/docs/integrations/mcp.md +304 -21
- package/docs/integrations/mistral-vibe.md +122 -0
- package/docs/integrations/opencode.md +84 -0
- package/docs/integrations/overview.md +23 -8
- package/docs/integrations/roo.md +74 -0
- package/docs/integrations/windsurf.md +83 -0
- package/docs/mcp-schema-changelog.md +191 -1
- package/docs/playbooks/integration/index.md +121 -0
- package/docs/playbooks/productivity/index.md +102 -0
- package/docs/playbooks/team/index.md +122 -0
- package/docs/product/agent-first-model.md +184 -0
- package/docs/product/entity-model-audit.md +462 -0
- package/docs/quickstart-existing-project.md +135 -0
- package/docs/quickstart.md +124 -37
- package/docs/release-maintenance.md +79 -0
- package/docs/review.md +2 -0
- package/docs/server-operations.md +118 -0
- package/package.json +20 -12
- package/dist/commands/claude-desktop-extension.js +0 -18
- package/dist/commands/diff.js +0 -99
- package/dist/core/claude-desktop-extension.js +0 -224
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Governance posture report.
|
|
3
|
+
*
|
|
4
|
+
* Aggregates claims, constraints, traps, instructions, and audit entries
|
|
5
|
+
* into a structured governance snapshot. No scores, no synthetic metrics —
|
|
6
|
+
* only verifiable facts.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
import { listClaims, isClaimExpired } from './claims.js';
|
|
11
|
+
import { loadState } from './state.js';
|
|
12
|
+
import { loadInstructions, resolveInstructions } from './instructions.js';
|
|
13
|
+
import { readAuditLog } from './audit.js';
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Report builder
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
export function buildGovernanceReport(options = {}) {
|
|
18
|
+
const cwd = options.cwd;
|
|
19
|
+
const now = new Date();
|
|
20
|
+
const last24h = new Date(now.getTime() - 24 * 60 * 60 * 1000).toISOString();
|
|
21
|
+
// --- Constitution (global instructions) ---
|
|
22
|
+
const allInstructions = loadInstructions(cwd);
|
|
23
|
+
const activeInstructions = resolveInstructions(allInstructions, {});
|
|
24
|
+
const globalInstructions = activeInstructions.filter(i => i.layer === 'global');
|
|
25
|
+
// --- Red Lines (constraints) ---
|
|
26
|
+
const state = loadState(cwd);
|
|
27
|
+
const activeConstraints = state.active_constraints.filter(c => c.status === 'active');
|
|
28
|
+
const constraintsByCategory = {};
|
|
29
|
+
for (const c of activeConstraints) {
|
|
30
|
+
const cat = c.category ?? 'other';
|
|
31
|
+
(constraintsByCategory[cat] ??= []).push(c);
|
|
32
|
+
}
|
|
33
|
+
const highSeverityCount = activeConstraints.filter(c => c.tags?.includes('high') || c.category === 'security').length;
|
|
34
|
+
// --- Claims ---
|
|
35
|
+
const allClaims = listClaims(cwd);
|
|
36
|
+
const activeClaims = allClaims.filter(c => c.status === 'active' && !isClaimExpired(c));
|
|
37
|
+
const expiredUnreleased = allClaims.filter(c => c.status === 'active' && isClaimExpired(c));
|
|
38
|
+
const claimsByAgent = {};
|
|
39
|
+
for (const c of activeClaims) {
|
|
40
|
+
claimsByAgent[c.agent] = (claimsByAgent[c.agent] ?? 0) + 1;
|
|
41
|
+
}
|
|
42
|
+
// Apply filters
|
|
43
|
+
let filteredActiveClaims = activeClaims;
|
|
44
|
+
let filteredExpired = expiredUnreleased;
|
|
45
|
+
if (options.agent) {
|
|
46
|
+
const agentLower = options.agent.toLowerCase();
|
|
47
|
+
filteredActiveClaims = activeClaims.filter(c => c.agent.toLowerCase() === agentLower || c.agent_id?.toLowerCase() === agentLower);
|
|
48
|
+
filteredExpired = expiredUnreleased.filter(c => c.agent.toLowerCase() === agentLower || c.agent_id?.toLowerCase() === agentLower);
|
|
49
|
+
}
|
|
50
|
+
// --- Traps (shared visibility only — machine/private traps are environment-specific) ---
|
|
51
|
+
const openTraps = state.known_traps.filter(t => t.status === 'active' && (t.visibility === 'shared' || !t.visibility));
|
|
52
|
+
const trapsBySeverity = {};
|
|
53
|
+
for (const t of openTraps) {
|
|
54
|
+
trapsBySeverity[t.severity] = (trapsBySeverity[t.severity] ?? 0) + 1;
|
|
55
|
+
}
|
|
56
|
+
// --- Recent activity ---
|
|
57
|
+
const recentEntries = readAuditLog({ since: last24h }, cwd);
|
|
58
|
+
const claimsLast24h = recentEntries.filter(e => e.action === 'claim').length;
|
|
59
|
+
const releasesLast24h = recentEntries.filter(e => e.action === 'release_claim').length;
|
|
60
|
+
// Detect mutations without claim — check creates/updates that aren't claim/release/session actions
|
|
61
|
+
const mutationActions = new Set(['create', 'update', 'delete', 'promote_direct']);
|
|
62
|
+
const sessionTypes = new Set(['session', 'claim']);
|
|
63
|
+
const actionsWithoutClaim = recentEntries.filter(e => {
|
|
64
|
+
if (!mutationActions.has(e.action))
|
|
65
|
+
return false;
|
|
66
|
+
if (sessionTypes.has(e.item_type ?? ''))
|
|
67
|
+
return false;
|
|
68
|
+
// Check if the actor had any active claim at that time
|
|
69
|
+
const actorClaims = activeClaims.filter(c => c.agent === e.actor);
|
|
70
|
+
return actorClaims.length === 0;
|
|
71
|
+
});
|
|
72
|
+
// --- Recommendations ---
|
|
73
|
+
const recommendations = [];
|
|
74
|
+
if (expiredUnreleased.length > 0) {
|
|
75
|
+
recommendations.push(`${expiredUnreleased.length} expired claim(s) need release. Run: bclaw release-claims --expired`);
|
|
76
|
+
}
|
|
77
|
+
if (openTraps.length > 0) {
|
|
78
|
+
const highTraps = openTraps.filter(t => t.severity === 'high');
|
|
79
|
+
if (highTraps.length > 0) {
|
|
80
|
+
recommendations.push(`${highTraps.length} high-severity trap(s) open. Review before editing related files.`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (actionsWithoutClaim.length > 0) {
|
|
84
|
+
recommendations.push(`${actionsWithoutClaim.length} mutation(s) detected without active claim in last 24h.`);
|
|
85
|
+
}
|
|
86
|
+
if (globalInstructions.length === 0) {
|
|
87
|
+
recommendations.push('No global instructions set. Consider adding governance rules via: bclaw instruction add --layer global');
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
generated_at: now.toISOString(),
|
|
91
|
+
scope_filter: options.scope,
|
|
92
|
+
agent_filter: options.agent,
|
|
93
|
+
constitution: {
|
|
94
|
+
global_instructions: globalInstructions,
|
|
95
|
+
total: globalInstructions.length,
|
|
96
|
+
},
|
|
97
|
+
red_lines: {
|
|
98
|
+
constraints_by_category: constraintsByCategory,
|
|
99
|
+
high_severity_count: highSeverityCount,
|
|
100
|
+
total: activeConstraints.length,
|
|
101
|
+
},
|
|
102
|
+
claims: {
|
|
103
|
+
active: filteredActiveClaims.map(toClaimSummary),
|
|
104
|
+
expired_unreleased: filteredExpired.map(toClaimSummary),
|
|
105
|
+
by_agent: claimsByAgent,
|
|
106
|
+
total_active: filteredActiveClaims.length,
|
|
107
|
+
total_expired_unreleased: filteredExpired.length,
|
|
108
|
+
},
|
|
109
|
+
traps: {
|
|
110
|
+
open: openTraps.map(toTrapSummary),
|
|
111
|
+
by_severity: trapsBySeverity,
|
|
112
|
+
total_open: openTraps.length,
|
|
113
|
+
},
|
|
114
|
+
recent_activity: {
|
|
115
|
+
claims_last_24h: claimsLast24h,
|
|
116
|
+
releases_last_24h: releasesLast24h,
|
|
117
|
+
actions_without_claim: actionsWithoutClaim,
|
|
118
|
+
},
|
|
119
|
+
recommendations,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// Markdown renderer
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
export function renderGovernanceMarkdown(report) {
|
|
126
|
+
const lines = [];
|
|
127
|
+
const ts = report.generated_at.slice(0, 16).replace('T', ' ');
|
|
128
|
+
lines.push(`# Governance Posture Report`);
|
|
129
|
+
lines.push(`Generated: ${ts} UTC`);
|
|
130
|
+
if (report.scope_filter)
|
|
131
|
+
lines.push(`Scope: ${report.scope_filter}`);
|
|
132
|
+
if (report.agent_filter)
|
|
133
|
+
lines.push(`Agent: ${report.agent_filter}`);
|
|
134
|
+
lines.push('');
|
|
135
|
+
// --- Summary ---
|
|
136
|
+
lines.push('## Summary');
|
|
137
|
+
lines.push(`- Active claims: ${report.claims.total_active}`);
|
|
138
|
+
lines.push(`- Expired (unreleased): ${report.claims.total_expired_unreleased}`);
|
|
139
|
+
lines.push(`- Active constraints: ${report.red_lines.total}`);
|
|
140
|
+
lines.push(`- Open traps: ${report.traps.total_open}`);
|
|
141
|
+
lines.push(`- Global instructions: ${report.constitution.total}`);
|
|
142
|
+
lines.push(`- Claims (24h): ${report.recent_activity.claims_last_24h} created, ${report.recent_activity.releases_last_24h} released`);
|
|
143
|
+
if (report.recent_activity.actions_without_claim.length > 0) {
|
|
144
|
+
lines.push(`- **Mutations without claim (24h): ${report.recent_activity.actions_without_claim.length}**`);
|
|
145
|
+
}
|
|
146
|
+
lines.push('');
|
|
147
|
+
// --- Constitution ---
|
|
148
|
+
if (report.constitution.total > 0) {
|
|
149
|
+
lines.push('## Constitution (Global Instructions)');
|
|
150
|
+
for (const ins of report.constitution.global_instructions) {
|
|
151
|
+
lines.push(`- ${ins.text}`);
|
|
152
|
+
}
|
|
153
|
+
lines.push('');
|
|
154
|
+
}
|
|
155
|
+
// --- Red Lines ---
|
|
156
|
+
if (report.red_lines.total > 0) {
|
|
157
|
+
lines.push('## Red Lines (Active Constraints)');
|
|
158
|
+
for (const [category, constraints] of Object.entries(report.red_lines.constraints_by_category)) {
|
|
159
|
+
lines.push(`### ${category} (${constraints.length})`);
|
|
160
|
+
for (const c of constraints) {
|
|
161
|
+
const paths = c.related_paths?.length ? ` — ${c.related_paths.join(', ')}` : '';
|
|
162
|
+
lines.push(`- [${c.id}] ${c.text}${paths}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
lines.push('');
|
|
166
|
+
}
|
|
167
|
+
// --- Claims ---
|
|
168
|
+
if (report.claims.total_active > 0) {
|
|
169
|
+
lines.push('## Active Claims');
|
|
170
|
+
const agentEntries = Object.entries(report.claims.by_agent).sort((a, b) => b[1] - a[1]);
|
|
171
|
+
if (agentEntries.length > 1) {
|
|
172
|
+
lines.push(`By agent: ${agentEntries.map(([a, n]) => `${a} (${n})`).join(', ')}`);
|
|
173
|
+
lines.push('');
|
|
174
|
+
}
|
|
175
|
+
for (const c of report.claims.active) {
|
|
176
|
+
const planNote = c.plan_id ? ` [${c.plan_id}]` : '';
|
|
177
|
+
const expiryNote = c.expires_at ? ` (expires ${c.expires_at.slice(0, 16).replace('T', ' ')})` : '';
|
|
178
|
+
lines.push(`- [${c.id}] ${c.agent} → ${c.scope}: ${c.description}${planNote}${expiryNote}`);
|
|
179
|
+
}
|
|
180
|
+
lines.push('');
|
|
181
|
+
}
|
|
182
|
+
if (report.claims.total_expired_unreleased > 0) {
|
|
183
|
+
lines.push('## Expired Claims (Need Release)');
|
|
184
|
+
for (const c of report.claims.expired_unreleased) {
|
|
185
|
+
lines.push(`- [${c.id}] ${c.agent} → ${c.scope}: ${c.description} (expired ${c.expires_at?.slice(0, 16).replace('T', ' ') ?? '?'})`);
|
|
186
|
+
}
|
|
187
|
+
lines.push('');
|
|
188
|
+
}
|
|
189
|
+
// --- Traps ---
|
|
190
|
+
if (report.traps.total_open > 0) {
|
|
191
|
+
lines.push('## Open Traps');
|
|
192
|
+
const sevEntries = Object.entries(report.traps.by_severity);
|
|
193
|
+
if (sevEntries.length > 0) {
|
|
194
|
+
lines.push(`By severity: ${sevEntries.map(([s, n]) => `${s} (${n})`).join(', ')}`);
|
|
195
|
+
lines.push('');
|
|
196
|
+
}
|
|
197
|
+
for (const t of report.traps.open) {
|
|
198
|
+
const paths = t.related_paths?.length ? ` — ${t.related_paths.join(', ')}` : '';
|
|
199
|
+
lines.push(`- [${t.id}] [${t.severity}] ${t.text}${paths}`);
|
|
200
|
+
}
|
|
201
|
+
lines.push('');
|
|
202
|
+
}
|
|
203
|
+
// --- Mutations without claim ---
|
|
204
|
+
if (report.recent_activity.actions_without_claim.length > 0) {
|
|
205
|
+
lines.push('## Mutations Without Claim (Last 24h)');
|
|
206
|
+
for (const e of report.recent_activity.actions_without_claim.slice(0, 20)) {
|
|
207
|
+
const scope = e.scope ? ` → ${e.scope}` : '';
|
|
208
|
+
lines.push(`- ${e.timestamp.slice(0, 16).replace('T', ' ')} [${e.actor}] ${e.action} ${e.item_type ?? ''}${scope}`);
|
|
209
|
+
}
|
|
210
|
+
if (report.recent_activity.actions_without_claim.length > 20) {
|
|
211
|
+
lines.push(` ... and ${report.recent_activity.actions_without_claim.length - 20} more`);
|
|
212
|
+
}
|
|
213
|
+
lines.push('');
|
|
214
|
+
}
|
|
215
|
+
// --- Recommendations ---
|
|
216
|
+
if (report.recommendations.length > 0) {
|
|
217
|
+
lines.push('## Recommendations');
|
|
218
|
+
for (const r of report.recommendations) {
|
|
219
|
+
lines.push(`- ${r}`);
|
|
220
|
+
}
|
|
221
|
+
lines.push('');
|
|
222
|
+
}
|
|
223
|
+
return lines.join('\n');
|
|
224
|
+
}
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
// Helpers
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
228
|
+
function toClaimSummary(c) {
|
|
229
|
+
return {
|
|
230
|
+
id: c.id,
|
|
231
|
+
agent: c.agent,
|
|
232
|
+
scope: c.scope,
|
|
233
|
+
description: c.description,
|
|
234
|
+
created_at: c.created_at,
|
|
235
|
+
expires_at: c.expires_at,
|
|
236
|
+
plan_id: c.plan_id,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function toTrapSummary(t) {
|
|
240
|
+
return {
|
|
241
|
+
id: t.id,
|
|
242
|
+
text: t.text,
|
|
243
|
+
severity: t.severity,
|
|
244
|
+
related_paths: t.related_paths,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
//# sourceMappingURL=governance.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared guard utilities for CLI commands.
|
|
3
|
+
*
|
|
4
|
+
* These replace the duplicated memoryExists + process.exit(1) pattern
|
|
5
|
+
* found across 70+ command files.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
import { memoryExists } from './io.js';
|
|
10
|
+
/**
|
|
11
|
+
* Abort the CLI process if .brainclaw/ is not found at the given path.
|
|
12
|
+
*/
|
|
13
|
+
export function requireInitialized(cwd) {
|
|
14
|
+
if (!memoryExists(cwd)) {
|
|
15
|
+
console.error('Error: .brainclaw/ not found. Run `brainclaw init` first.');
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=guards.js.map
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { memoryDir } from './io.js';
|
|
5
|
+
export const IdeationRoundSchema = z.object({
|
|
6
|
+
schema_version: z.literal(1),
|
|
7
|
+
thread_id: z.string(),
|
|
8
|
+
round_number: z.number().int().min(0),
|
|
9
|
+
round_type: z.enum(['position', 'reaction', 'convergence']),
|
|
10
|
+
positions: z.array(z.object({
|
|
11
|
+
persona: z.string(),
|
|
12
|
+
agent: z.string(),
|
|
13
|
+
text: z.string(),
|
|
14
|
+
duration_ms: z.number().optional(),
|
|
15
|
+
})),
|
|
16
|
+
tensions: z.array(z.string()).default([]),
|
|
17
|
+
convergences: z.array(z.string()).default([]),
|
|
18
|
+
created_at: z.string(),
|
|
19
|
+
});
|
|
20
|
+
function sanitizeForPath(slug) {
|
|
21
|
+
return slug.replace(/[<>:"/\\|?*]/g, '_');
|
|
22
|
+
}
|
|
23
|
+
export function ideationDir(threadSlug, cwd) {
|
|
24
|
+
return path.join(memoryDir(cwd), 'coordination', 'ideation', sanitizeForPath(threadSlug));
|
|
25
|
+
}
|
|
26
|
+
export function saveIdeationRound(threadSlug, round, cwd) {
|
|
27
|
+
const dir = ideationDir(threadSlug, cwd);
|
|
28
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
29
|
+
const filePath = path.join(dir, `round_${round.round_number}.json`);
|
|
30
|
+
fs.writeFileSync(filePath, JSON.stringify(round, null, 2), 'utf8');
|
|
31
|
+
}
|
|
32
|
+
export function loadIdeationRound(threadSlug, roundNumber, cwd) {
|
|
33
|
+
const filePath = path.join(ideationDir(threadSlug, cwd), `round_${roundNumber}.json`);
|
|
34
|
+
if (!fs.existsSync(filePath)) {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
39
|
+
return IdeationRoundSchema.parse(JSON.parse(raw));
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function listIdeationThreads(cwd) {
|
|
46
|
+
const base = path.join(memoryDir(cwd), 'coordination', 'ideation');
|
|
47
|
+
if (!fs.existsSync(base))
|
|
48
|
+
return [];
|
|
49
|
+
return fs.readdirSync(base, { withFileTypes: true })
|
|
50
|
+
.filter(e => e.isDirectory())
|
|
51
|
+
.map(e => e.name)
|
|
52
|
+
.sort();
|
|
53
|
+
}
|
|
54
|
+
export function listIdeationRounds(threadSlug, cwd) {
|
|
55
|
+
const dir = ideationDir(threadSlug, cwd);
|
|
56
|
+
if (!fs.existsSync(dir)) {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
const rounds = [];
|
|
60
|
+
for (const entry of fs.readdirSync(dir)) {
|
|
61
|
+
const match = /^round_(\d+)\.json$/.exec(entry);
|
|
62
|
+
if (!match) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const round = loadIdeationRound(threadSlug, Number(match[1]), cwd);
|
|
66
|
+
if (round) {
|
|
67
|
+
rounds.push(round);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return rounds.sort((a, b) => a.round_number - b.round_number);
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=ideation.js.map
|
package/dist/core/identity.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
3
4
|
import path from 'node:path';
|
|
4
5
|
import { requireRegisteredAgentIdentity } from './agent-registry.js';
|
|
5
6
|
import { loadConfig } from './config.js';
|
|
@@ -7,7 +8,9 @@ import { resolveCurrentHostId } from './host.js';
|
|
|
7
8
|
import { memoryDir } from './io.js';
|
|
8
9
|
import { loadVersionedJsonFile, saveVersionedJsonFile } from './migration.js';
|
|
9
10
|
import { CurrentSessionStateSchema } from './schema.js';
|
|
10
|
-
const
|
|
11
|
+
const SESSIONS_DIR = 'sessions';
|
|
12
|
+
const LEGACY_SESSION_FILE = '.current-session';
|
|
13
|
+
// --- Public API ---
|
|
11
14
|
export function resolveCurrentSessionId(env = process.env, cwd, options = {}) {
|
|
12
15
|
const value = env.BRAINCLAW_SESSION_ID?.trim()
|
|
13
16
|
|| env.OPENCLAW_SESSION_ID?.trim()
|
|
@@ -64,61 +67,281 @@ export function resolveEventSessionId(event) {
|
|
|
64
67
|
? metadataSession
|
|
65
68
|
: undefined;
|
|
66
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Load the current session for this agent+user combo.
|
|
72
|
+
* Checks sessions/ directory first, falls back to legacy .current-session.
|
|
73
|
+
*/
|
|
67
74
|
export function loadCurrentSession(cwd) {
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
75
|
+
const dir = sessionsDir(cwd);
|
|
76
|
+
const currentUser = resolveCurrentUser();
|
|
77
|
+
const currentAgent = resolveCurrentAgentName();
|
|
78
|
+
// 1. Look in sessions/ directory for a matching session
|
|
79
|
+
if (fs.existsSync(dir) && currentAgent) {
|
|
80
|
+
const files = fs.readdirSync(dir).filter(f => f.endsWith('.json'));
|
|
81
|
+
const ttlMs = parseDurationToMs(loadConfigSafe(cwd)?.implicit_session_ttl ?? '4h');
|
|
82
|
+
const now = Date.now();
|
|
83
|
+
for (const file of files) {
|
|
84
|
+
try {
|
|
85
|
+
const migration = loadVersionedJsonFile('current_session', path.join(dir, file));
|
|
86
|
+
const session = {
|
|
87
|
+
...CurrentSessionStateSchema.parse(migration.document),
|
|
88
|
+
schema_version: migration.metadata.currentVersion,
|
|
89
|
+
};
|
|
90
|
+
// Strict match: agent name must match, user must match (when both are known)
|
|
91
|
+
if (session.agent !== currentAgent)
|
|
92
|
+
continue;
|
|
93
|
+
const userMatch = !session.user || !currentUser || session.user === currentUser;
|
|
94
|
+
const alive = (now - Date.parse(session.last_seen_at)) <= ttlMs;
|
|
95
|
+
if (userMatch && alive) {
|
|
96
|
+
return session;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// skip invalid session files
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// 2. Legacy fallback: .current-session
|
|
105
|
+
const legacyPath = path.join(memoryDir(cwd), LEGACY_SESSION_FILE);
|
|
106
|
+
if (fs.existsSync(legacyPath)) {
|
|
107
|
+
try {
|
|
108
|
+
const migration = loadVersionedJsonFile('current_session', legacyPath);
|
|
109
|
+
return {
|
|
110
|
+
...CurrentSessionStateSchema.parse(migration.document),
|
|
111
|
+
schema_version: migration.metadata.currentVersion,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
71
117
|
}
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Load a specific session by ID.
|
|
122
|
+
*/
|
|
123
|
+
export function loadSessionById(sessionId, cwd) {
|
|
124
|
+
const filepath = sessionFilePath(sessionId, cwd);
|
|
125
|
+
if (!fs.existsSync(filepath))
|
|
126
|
+
return undefined;
|
|
72
127
|
try {
|
|
73
|
-
|
|
128
|
+
const migration = loadVersionedJsonFile('current_session', filepath);
|
|
129
|
+
return {
|
|
130
|
+
...CurrentSessionStateSchema.parse(migration.document),
|
|
131
|
+
schema_version: migration.metadata.currentVersion,
|
|
132
|
+
};
|
|
74
133
|
}
|
|
75
134
|
catch {
|
|
76
135
|
return undefined;
|
|
77
136
|
}
|
|
78
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* Load ALL sessions (active + stale) from the sessions/ directory.
|
|
140
|
+
*/
|
|
141
|
+
export function loadAllSessions(cwd) {
|
|
142
|
+
const dir = sessionsDir(cwd);
|
|
143
|
+
if (!fs.existsSync(dir))
|
|
144
|
+
return [];
|
|
145
|
+
const files = fs.readdirSync(dir).filter(f => f.endsWith('.json'));
|
|
146
|
+
const sessions = [];
|
|
147
|
+
for (const file of files) {
|
|
148
|
+
try {
|
|
149
|
+
const migration = loadVersionedJsonFile('current_session', path.join(dir, file));
|
|
150
|
+
sessions.push({
|
|
151
|
+
...CurrentSessionStateSchema.parse(migration.document),
|
|
152
|
+
schema_version: migration.metadata.currentVersion,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
// skip invalid
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return sessions.sort((a, b) => b.last_seen_at.localeCompare(a.last_seen_at));
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Save a session to the sessions/ directory.
|
|
163
|
+
*/
|
|
79
164
|
export function saveCurrentSession(session, cwd) {
|
|
80
|
-
|
|
165
|
+
const dir = sessionsDir(cwd);
|
|
166
|
+
if (!fs.existsSync(dir)) {
|
|
167
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
168
|
+
}
|
|
169
|
+
const filepath = sessionFilePath(session.session_id, cwd);
|
|
170
|
+
saveVersionedJsonFile('current_session', filepath, CurrentSessionStateSchema.parse(session));
|
|
81
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* Clear a session. If sessionId is provided, only clear that specific session.
|
|
174
|
+
*/
|
|
82
175
|
export function clearCurrentSession(cwd, sessionId) {
|
|
83
|
-
|
|
84
|
-
|
|
176
|
+
if (sessionId) {
|
|
177
|
+
// Remove specific session file
|
|
178
|
+
const filepath = sessionFilePath(sessionId, cwd);
|
|
179
|
+
try {
|
|
180
|
+
fs.unlinkSync(filepath);
|
|
181
|
+
}
|
|
182
|
+
catch { /* ignore */ }
|
|
85
183
|
return;
|
|
86
184
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
185
|
+
// Clear the session for the current agent+user
|
|
186
|
+
const session = loadCurrentSession(cwd);
|
|
187
|
+
if (session) {
|
|
188
|
+
const filepath = sessionFilePath(session.session_id, cwd);
|
|
189
|
+
try {
|
|
190
|
+
fs.unlinkSync(filepath);
|
|
191
|
+
}
|
|
192
|
+
catch { /* ignore */ }
|
|
193
|
+
}
|
|
194
|
+
// Also clean legacy file
|
|
195
|
+
const legacyPath = path.join(memoryDir(cwd), LEGACY_SESSION_FILE);
|
|
196
|
+
try {
|
|
197
|
+
fs.unlinkSync(legacyPath);
|
|
198
|
+
}
|
|
199
|
+
catch { /* ignore */ }
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Remove stale sessions that have exceeded the TTL.
|
|
203
|
+
* Returns the number of sessions removed.
|
|
204
|
+
*/
|
|
205
|
+
export function gcStaleSessions(cwd, ttlOverride) {
|
|
206
|
+
const dir = sessionsDir(cwd);
|
|
207
|
+
if (!fs.existsSync(dir))
|
|
208
|
+
return 0;
|
|
209
|
+
const ttlMs = parseDurationToMs(ttlOverride ?? loadConfigSafe(cwd)?.implicit_session_ttl ?? '4h');
|
|
210
|
+
const now = Date.now();
|
|
211
|
+
let removed = 0;
|
|
212
|
+
const files = fs.readdirSync(dir).filter(f => f.endsWith('.json'));
|
|
213
|
+
for (const file of files) {
|
|
214
|
+
try {
|
|
215
|
+
const migration = loadVersionedJsonFile('current_session', path.join(dir, file));
|
|
216
|
+
const session = {
|
|
217
|
+
...CurrentSessionStateSchema.parse(migration.document),
|
|
218
|
+
schema_version: migration.metadata.currentVersion,
|
|
219
|
+
};
|
|
220
|
+
if (now - Date.parse(session.last_seen_at) > ttlMs) {
|
|
221
|
+
fs.unlinkSync(path.join(dir, file));
|
|
222
|
+
removed++;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
// Remove unparseable files too
|
|
227
|
+
try {
|
|
228
|
+
fs.unlinkSync(path.join(dir, file));
|
|
229
|
+
removed++;
|
|
230
|
+
}
|
|
231
|
+
catch { /* ignore */ }
|
|
91
232
|
}
|
|
92
233
|
}
|
|
234
|
+
return removed;
|
|
235
|
+
}
|
|
236
|
+
// --- Internal helpers ---
|
|
237
|
+
function sessionsDir(cwd) {
|
|
238
|
+
return path.join(memoryDir(cwd), SESSIONS_DIR);
|
|
239
|
+
}
|
|
240
|
+
function sessionFilePath(sessionId, cwd) {
|
|
241
|
+
return path.join(sessionsDir(cwd), `${sessionId}.json`);
|
|
242
|
+
}
|
|
243
|
+
function resolveCurrentUser() {
|
|
244
|
+
return process.env.USER || process.env.USERNAME || os.userInfo().username || undefined;
|
|
245
|
+
}
|
|
246
|
+
function resolveCurrentAgentName() {
|
|
247
|
+
if (process.env.BRAINCLAW_AGENT_NAME)
|
|
248
|
+
return process.env.BRAINCLAW_AGENT_NAME;
|
|
249
|
+
if (process.env.CLAUDE_CODE_VERSION)
|
|
250
|
+
return 'claude-code';
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
function loadConfigSafe(cwd) {
|
|
93
254
|
try {
|
|
94
|
-
|
|
255
|
+
return loadConfig(cwd);
|
|
95
256
|
}
|
|
96
257
|
catch {
|
|
97
|
-
|
|
258
|
+
return undefined;
|
|
98
259
|
}
|
|
99
260
|
}
|
|
100
|
-
function
|
|
101
|
-
|
|
261
|
+
function isPidAlive(pid) {
|
|
262
|
+
try {
|
|
263
|
+
process.kill(pid, 0);
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
102
269
|
}
|
|
270
|
+
/**
|
|
271
|
+
* Find the session matching the current process among all active sessions.
|
|
272
|
+
*
|
|
273
|
+
* Resolution order:
|
|
274
|
+
* 1. Preferred session ID (explicit env var / parameter) → exact match
|
|
275
|
+
* 2. Same agent + user + host + same PID → refresh (same process reconnecting)
|
|
276
|
+
* 3. Same agent + user + host + dead PID → reclaim stale session
|
|
277
|
+
* 4. No match → create new session
|
|
278
|
+
*
|
|
279
|
+
* Crucially, if another session exists for the same agent+user+host but with
|
|
280
|
+
* a LIVE different PID, it is left untouched — that's a parallel instance.
|
|
281
|
+
*/
|
|
103
282
|
function resolveImplicitSession(cwd, options) {
|
|
104
|
-
const current = loadCurrentSession(cwd);
|
|
105
283
|
const persistImplicit = options.persistImplicit ?? true;
|
|
106
|
-
const ttlMs = parseDurationToMs(
|
|
284
|
+
const ttlMs = parseDurationToMs(loadConfigSafe(cwd)?.implicit_session_ttl ?? '4h');
|
|
107
285
|
const now = new Date();
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
286
|
+
const currentUser = resolveCurrentUser();
|
|
287
|
+
const currentPid = process.pid;
|
|
288
|
+
// 1. If a preferred session ID is given, try exact match first
|
|
289
|
+
if (options.preferredSessionId) {
|
|
290
|
+
const exact = loadSessionById(options.preferredSessionId, cwd);
|
|
291
|
+
if (exact && now.getTime() - Date.parse(exact.last_seen_at) <= ttlMs) {
|
|
292
|
+
const refreshed = {
|
|
293
|
+
...exact,
|
|
294
|
+
last_seen_at: now.toISOString(),
|
|
295
|
+
user: exact.user || currentUser,
|
|
296
|
+
pid: currentPid,
|
|
297
|
+
};
|
|
298
|
+
if (persistImplicit)
|
|
299
|
+
saveCurrentSession(refreshed, cwd);
|
|
300
|
+
return refreshed;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
// 2. Scan all sessions for PID-aware matching
|
|
304
|
+
const allSessions = loadAllSessions(cwd);
|
|
305
|
+
let samePidSession;
|
|
306
|
+
let deadPidSession;
|
|
307
|
+
for (const session of allSessions) {
|
|
308
|
+
if (session.agent !== options.agentName)
|
|
309
|
+
continue;
|
|
310
|
+
if (session.agent_id !== options.agentId)
|
|
311
|
+
continue;
|
|
312
|
+
if (session.host_id !== options.hostId)
|
|
313
|
+
continue;
|
|
314
|
+
if (currentUser && session.user && session.user !== currentUser)
|
|
315
|
+
continue;
|
|
316
|
+
if (now.getTime() - Date.parse(session.last_seen_at) > ttlMs)
|
|
317
|
+
continue;
|
|
318
|
+
// Same PID = same process reconnecting (e.g. MCP server refreshing)
|
|
319
|
+
if (session.pid === currentPid) {
|
|
320
|
+
samePidSession = session;
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
// Different PID but alive = parallel instance, do NOT reclaim
|
|
324
|
+
if (session.pid && isPidAlive(session.pid)) {
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
// Dead PID = stale session, candidate for reclaim
|
|
328
|
+
if (!deadPidSession) {
|
|
329
|
+
deadPidSession = session;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
const toRefresh = samePidSession ?? deadPidSession;
|
|
333
|
+
if (toRefresh) {
|
|
113
334
|
const refreshed = {
|
|
114
|
-
...
|
|
335
|
+
...toRefresh,
|
|
115
336
|
last_seen_at: now.toISOString(),
|
|
337
|
+
user: toRefresh.user || currentUser,
|
|
338
|
+
pid: currentPid,
|
|
116
339
|
};
|
|
117
|
-
if (persistImplicit)
|
|
340
|
+
if (persistImplicit)
|
|
118
341
|
saveCurrentSession(refreshed, cwd);
|
|
119
|
-
}
|
|
120
342
|
return refreshed;
|
|
121
343
|
}
|
|
344
|
+
// 3. No match — create new session
|
|
122
345
|
const created = {
|
|
123
346
|
session_id: options.preferredSessionId ?? generateImplicitSessionId(),
|
|
124
347
|
started_at: now.toISOString(),
|
|
@@ -126,10 +349,11 @@ function resolveImplicitSession(cwd, options) {
|
|
|
126
349
|
agent: options.agentName,
|
|
127
350
|
agent_id: options.agentId,
|
|
128
351
|
host_id: options.hostId,
|
|
352
|
+
user: currentUser,
|
|
353
|
+
pid: currentPid,
|
|
129
354
|
};
|
|
130
|
-
if (persistImplicit)
|
|
355
|
+
if (persistImplicit)
|
|
131
356
|
saveCurrentSession(created, cwd);
|
|
132
|
-
}
|
|
133
357
|
return created;
|
|
134
358
|
}
|
|
135
359
|
function parseDurationToMs(value) {
|