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.
Files changed (198) hide show
  1. package/README.md +193 -170
  2. package/dist/brainclaw-vscode.vsix +0 -0
  3. package/dist/cli.js +683 -23
  4. package/dist/commands/accept.js +3 -0
  5. package/dist/commands/add-step.js +11 -26
  6. package/dist/commands/agent-board.js +70 -3
  7. package/dist/commands/audit.js +19 -0
  8. package/dist/commands/check-policy.js +54 -0
  9. package/dist/commands/check-security-mcp.js +145 -0
  10. package/dist/commands/check-security.js +106 -0
  11. package/dist/commands/claim-resource.js +1 -0
  12. package/dist/commands/codev.js +672 -0
  13. package/dist/commands/compact.js +74 -0
  14. package/dist/commands/complete-step.js +16 -26
  15. package/dist/commands/constraint.js +8 -20
  16. package/dist/commands/decision.js +9 -20
  17. package/dist/commands/delete-plan.js +10 -12
  18. package/dist/commands/delete-step.js +16 -0
  19. package/dist/commands/dispatch.js +163 -0
  20. package/dist/commands/doctor.js +1122 -49
  21. package/dist/commands/enable-agent.js +1 -0
  22. package/dist/commands/export.js +280 -22
  23. package/dist/commands/handoff.js +33 -0
  24. package/dist/commands/harvest.js +189 -0
  25. package/dist/commands/hooks.js +82 -25
  26. package/dist/commands/inbox.js +169 -0
  27. package/dist/commands/init.js +38 -31
  28. package/dist/commands/install-hooks.js +71 -44
  29. package/dist/commands/link.js +89 -0
  30. package/dist/commands/list-claims.js +48 -3
  31. package/dist/commands/list-plans.js +129 -25
  32. package/dist/commands/loops-handlers.js +409 -0
  33. package/dist/commands/mcp-read-handlers.js +1628 -0
  34. package/dist/commands/mcp-schemas.generated.js +74 -0
  35. package/dist/commands/mcp.js +4244 -1475
  36. package/dist/commands/plan-resource.js +64 -0
  37. package/dist/commands/plan.js +12 -26
  38. package/dist/commands/prune.js +37 -2
  39. package/dist/commands/reflect.js +20 -7
  40. package/dist/commands/release-claim.js +11 -6
  41. package/dist/commands/release-notes.js +170 -0
  42. package/dist/commands/repair.js +210 -0
  43. package/dist/commands/run-profile.js +57 -0
  44. package/dist/commands/sequence.js +113 -0
  45. package/dist/commands/session-end.js +423 -14
  46. package/dist/commands/session-start.js +214 -41
  47. package/dist/commands/setup-security.js +103 -0
  48. package/dist/commands/setup.js +42 -4
  49. package/dist/commands/stale.js +109 -0
  50. package/dist/commands/switch.js +131 -10
  51. package/dist/commands/trap.js +14 -31
  52. package/dist/commands/update-handoff.js +63 -4
  53. package/dist/commands/update-plan.js +21 -28
  54. package/dist/commands/update-step.js +37 -0
  55. package/dist/commands/upgrade.js +313 -6
  56. package/dist/commands/usage.js +102 -0
  57. package/dist/commands/version.js +20 -0
  58. package/dist/commands/who.js +124 -0
  59. package/dist/commands/worktree.js +105 -0
  60. package/dist/core/actions.js +315 -0
  61. package/dist/core/agent-capability.js +610 -17
  62. package/dist/core/agent-context.js +7 -1
  63. package/dist/core/agent-files.js +1169 -85
  64. package/dist/core/agent-integrations.js +160 -5
  65. package/dist/core/agent-inventory.js +2 -0
  66. package/dist/core/agent-profiles.js +93 -0
  67. package/dist/core/agent-registry.js +162 -30
  68. package/dist/core/agentrun-reconciler.js +345 -0
  69. package/dist/core/agentruns.js +424 -0
  70. package/dist/core/ai-agent-detection.js +31 -10
  71. package/dist/core/archival.js +77 -0
  72. package/dist/core/assignment-sweeper.js +82 -0
  73. package/dist/core/assignments.js +367 -0
  74. package/dist/core/audit.js +30 -0
  75. package/dist/core/bootstrap.js +61 -10
  76. package/dist/core/brainclaw-version.js +94 -2
  77. package/dist/core/candidates.js +93 -2
  78. package/dist/core/claims.js +419 -0
  79. package/dist/core/codev-metrics.js +77 -0
  80. package/dist/core/codev-personas.js +31 -0
  81. package/dist/core/codev-plan-gen.js +35 -0
  82. package/dist/core/codev-prompts.js +74 -0
  83. package/dist/core/codev-responses.js +62 -0
  84. package/dist/core/codev-rounds.js +218 -0
  85. package/dist/core/config.js +4 -0
  86. package/dist/core/context.js +454 -34
  87. package/dist/core/coordination.js +201 -6
  88. package/dist/core/cross-project.js +230 -16
  89. package/dist/core/default-profiles/doctor.yaml +11 -0
  90. package/dist/core/default-profiles/janitor.yaml +11 -0
  91. package/dist/core/default-profiles/onboarder.yaml +11 -0
  92. package/dist/core/default-profiles/reviewer.yaml +13 -0
  93. package/dist/core/dispatcher.js +1189 -0
  94. package/dist/core/duplicates.js +2 -2
  95. package/dist/core/entity-operations.js +450 -0
  96. package/dist/core/entity-registry.js +344 -0
  97. package/dist/core/event-log.js +1 -0
  98. package/dist/core/events.js +106 -2
  99. package/dist/core/execution-adapters.js +154 -0
  100. package/dist/core/execution-context.js +63 -0
  101. package/dist/core/execution-profile.js +270 -0
  102. package/dist/core/execution.js +255 -0
  103. package/dist/core/facade-schema.js +81 -0
  104. package/dist/core/federation-cloud.js +99 -0
  105. package/dist/core/federation-message.js +52 -0
  106. package/dist/core/federation-transport.js +65 -0
  107. package/dist/core/gc-semantic.js +482 -0
  108. package/dist/core/governance.js +247 -0
  109. package/dist/core/guards.js +19 -0
  110. package/dist/core/ideation.js +72 -0
  111. package/dist/core/identity.js +252 -28
  112. package/dist/core/ids.js +6 -0
  113. package/dist/core/input-validation.js +2 -2
  114. package/dist/core/instruction-templates.js +344 -136
  115. package/dist/core/io.js +90 -11
  116. package/dist/core/lock.js +6 -2
  117. package/dist/core/loops/brief-assembly.js +213 -0
  118. package/dist/core/loops/facade-schema.js +148 -0
  119. package/dist/core/loops/index.js +7 -0
  120. package/dist/core/loops/iteration-engine.js +139 -0
  121. package/dist/core/loops/lock.js +385 -0
  122. package/dist/core/loops/store.js +201 -0
  123. package/dist/core/loops/types.js +403 -0
  124. package/dist/core/loops/verbs.js +534 -0
  125. package/dist/core/markdown.js +15 -3
  126. package/dist/core/memory-compactor.js +432 -0
  127. package/dist/core/memory-git.js +152 -8
  128. package/dist/core/messaging.js +278 -0
  129. package/dist/core/migration.js +32 -1
  130. package/dist/core/mutation-pipeline.js +4 -2
  131. package/dist/core/operations/memory-mutation.js +129 -0
  132. package/dist/core/operations/memory-write.js +78 -0
  133. package/dist/core/operations/plan.js +190 -0
  134. package/dist/core/policy.js +169 -0
  135. package/dist/core/repo-analysis.js +67 -0
  136. package/dist/core/reputation.js +9 -3
  137. package/dist/core/schema.js +546 -21
  138. package/dist/core/search.js +21 -2
  139. package/dist/core/security-cache.js +71 -0
  140. package/dist/core/security-guard.js +152 -0
  141. package/dist/core/security-scoring.js +86 -0
  142. package/dist/core/sequence.js +130 -0
  143. package/dist/core/socket-client.js +113 -0
  144. package/dist/core/staleness.js +246 -0
  145. package/dist/core/state.js +98 -22
  146. package/dist/core/store-resolution.js +54 -12
  147. package/dist/core/toml-writer.js +76 -0
  148. package/dist/core/upgrades/backup.js +232 -0
  149. package/dist/core/upgrades/health-check.js +169 -0
  150. package/dist/core/upgrades/patches/candidate-archive.js +145 -0
  151. package/dist/core/upgrades/patches/handoff-review-strip.js +128 -0
  152. package/dist/core/upgrades/patches/provenance-rollout.js +136 -0
  153. package/dist/core/upgrades/schema-version.js +97 -0
  154. package/dist/core/worktree.js +606 -0
  155. package/dist/facts.js +114 -0
  156. package/dist/facts.json +111 -0
  157. package/docs/architecture/project-refs.md +5 -1
  158. package/docs/cli.md +690 -43
  159. package/docs/concepts/ideation-loop.md +317 -0
  160. package/docs/concepts/loop-engine.md +456 -0
  161. package/docs/concepts/mcp-governance.md +268 -0
  162. package/docs/concepts/memory-staleness.md +122 -0
  163. package/docs/concepts/multi-agent-workflows.md +166 -0
  164. package/docs/concepts/plans-and-claims.md +31 -6
  165. package/docs/concepts/project-md-convention.md +35 -0
  166. package/docs/concepts/troubleshooting.md +220 -0
  167. package/docs/concepts/upgrade-cli.md +202 -0
  168. package/docs/concepts/upgrade-dogfood-procedure.md +114 -0
  169. package/docs/context-format-changelog.md +2 -2
  170. package/docs/context-format.md +2 -2
  171. package/docs/index.md +68 -0
  172. package/docs/integrations/agents.md +15 -16
  173. package/docs/integrations/cline.md +88 -0
  174. package/docs/integrations/codex.md +75 -23
  175. package/docs/integrations/continue.md +60 -0
  176. package/docs/integrations/copilot.md +67 -9
  177. package/docs/integrations/kilocode.md +72 -0
  178. package/docs/integrations/mcp.md +304 -21
  179. package/docs/integrations/mistral-vibe.md +122 -0
  180. package/docs/integrations/opencode.md +84 -0
  181. package/docs/integrations/overview.md +23 -8
  182. package/docs/integrations/roo.md +74 -0
  183. package/docs/integrations/windsurf.md +83 -0
  184. package/docs/mcp-schema-changelog.md +191 -1
  185. package/docs/playbooks/integration/index.md +121 -0
  186. package/docs/playbooks/productivity/index.md +102 -0
  187. package/docs/playbooks/team/index.md +122 -0
  188. package/docs/product/agent-first-model.md +184 -0
  189. package/docs/product/entity-model-audit.md +462 -0
  190. package/docs/quickstart-existing-project.md +135 -0
  191. package/docs/quickstart.md +124 -37
  192. package/docs/release-maintenance.md +79 -0
  193. package/docs/review.md +2 -0
  194. package/docs/server-operations.md +118 -0
  195. package/package.json +20 -12
  196. package/dist/commands/claude-desktop-extension.js +0 -18
  197. package/dist/commands/diff.js +0 -99
  198. 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
@@ -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 CURRENT_SESSION_FILE = '.current-session';
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 filepath = currentSessionPath(cwd);
69
- if (!fs.existsSync(filepath)) {
70
- return undefined;
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
- return CurrentSessionStateSchema.parse(loadVersionedJsonFile('current_session', filepath).document);
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
- saveVersionedJsonFile('current_session', currentSessionPath(cwd), CurrentSessionStateSchema.parse(session));
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
- const filepath = currentSessionPath(cwd);
84
- if (!fs.existsSync(filepath)) {
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
- if (sessionId) {
88
- const current = loadCurrentSession(cwd);
89
- if (!current || current.session_id !== sessionId) {
90
- return;
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
- fs.unlinkSync(filepath);
255
+ return loadConfig(cwd);
95
256
  }
96
257
  catch {
97
- // Ignore cleanup races.
258
+ return undefined;
98
259
  }
99
260
  }
100
- function currentSessionPath(cwd) {
101
- return path.join(memoryDir(cwd), CURRENT_SESSION_FILE);
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(loadConfig(cwd).implicit_session_ttl ?? '4h');
284
+ const ttlMs = parseDurationToMs(loadConfigSafe(cwd)?.implicit_session_ttl ?? '4h');
107
285
  const now = new Date();
108
- if (current
109
- && current.agent === options.agentName
110
- && current.agent_id === options.agentId
111
- && current.host_id === options.hostId
112
- && now.getTime() - Date.parse(current.last_seen_at) <= ttlMs) {
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
- ...current,
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) {