brainclaw 0.29.2 → 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 (195) hide show
  1. package/README.md +193 -170
  2. package/dist/brainclaw-vscode.vsix +0 -0
  3. package/dist/cli.js +673 -24
  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 +4221 -1501
  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 +100 -2
  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 +33 -5
  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/brainclaw-version.js +94 -2
  76. package/dist/core/candidates.js +93 -2
  77. package/dist/core/claims.js +419 -0
  78. package/dist/core/codev-metrics.js +77 -0
  79. package/dist/core/codev-personas.js +31 -0
  80. package/dist/core/codev-plan-gen.js +35 -0
  81. package/dist/core/codev-prompts.js +74 -0
  82. package/dist/core/codev-responses.js +62 -0
  83. package/dist/core/codev-rounds.js +218 -0
  84. package/dist/core/config.js +4 -0
  85. package/dist/core/context.js +381 -34
  86. package/dist/core/coordination.js +201 -6
  87. package/dist/core/cross-project.js +230 -16
  88. package/dist/core/default-profiles/doctor.yaml +11 -0
  89. package/dist/core/default-profiles/janitor.yaml +11 -0
  90. package/dist/core/default-profiles/onboarder.yaml +11 -0
  91. package/dist/core/default-profiles/reviewer.yaml +13 -0
  92. package/dist/core/dispatcher.js +1189 -0
  93. package/dist/core/duplicates.js +2 -2
  94. package/dist/core/entity-operations.js +450 -0
  95. package/dist/core/entity-registry.js +344 -0
  96. package/dist/core/events.js +106 -2
  97. package/dist/core/execution-adapters.js +154 -0
  98. package/dist/core/execution-context.js +63 -0
  99. package/dist/core/execution-profile.js +270 -0
  100. package/dist/core/execution.js +255 -0
  101. package/dist/core/facade-schema.js +81 -0
  102. package/dist/core/federation-cloud.js +99 -0
  103. package/dist/core/federation-message.js +52 -0
  104. package/dist/core/federation-transport.js +65 -0
  105. package/dist/core/gc-semantic.js +482 -0
  106. package/dist/core/governance.js +247 -0
  107. package/dist/core/guards.js +19 -0
  108. package/dist/core/ideation.js +72 -0
  109. package/dist/core/identity.js +110 -25
  110. package/dist/core/ids.js +6 -0
  111. package/dist/core/input-validation.js +2 -2
  112. package/dist/core/instruction-templates.js +344 -136
  113. package/dist/core/io.js +90 -11
  114. package/dist/core/lock.js +6 -2
  115. package/dist/core/loops/brief-assembly.js +213 -0
  116. package/dist/core/loops/facade-schema.js +148 -0
  117. package/dist/core/loops/index.js +7 -0
  118. package/dist/core/loops/iteration-engine.js +139 -0
  119. package/dist/core/loops/lock.js +385 -0
  120. package/dist/core/loops/store.js +201 -0
  121. package/dist/core/loops/types.js +403 -0
  122. package/dist/core/loops/verbs.js +534 -0
  123. package/dist/core/markdown.js +15 -3
  124. package/dist/core/memory-compactor.js +432 -0
  125. package/dist/core/memory-git.js +152 -8
  126. package/dist/core/messaging.js +278 -0
  127. package/dist/core/migration.js +32 -1
  128. package/dist/core/mutation-pipeline.js +4 -2
  129. package/dist/core/operations/memory-mutation.js +129 -0
  130. package/dist/core/operations/memory-write.js +78 -0
  131. package/dist/core/operations/plan.js +190 -0
  132. package/dist/core/policy.js +169 -0
  133. package/dist/core/reputation.js +9 -3
  134. package/dist/core/schema.js +491 -6
  135. package/dist/core/search.js +21 -2
  136. package/dist/core/security-cache.js +71 -0
  137. package/dist/core/security-guard.js +152 -0
  138. package/dist/core/security-scoring.js +86 -0
  139. package/dist/core/sequence.js +130 -0
  140. package/dist/core/socket-client.js +113 -0
  141. package/dist/core/staleness.js +246 -0
  142. package/dist/core/state.js +98 -22
  143. package/dist/core/store-resolution.js +43 -11
  144. package/dist/core/toml-writer.js +76 -0
  145. package/dist/core/upgrades/backup.js +232 -0
  146. package/dist/core/upgrades/health-check.js +169 -0
  147. package/dist/core/upgrades/patches/candidate-archive.js +145 -0
  148. package/dist/core/upgrades/patches/handoff-review-strip.js +128 -0
  149. package/dist/core/upgrades/patches/provenance-rollout.js +136 -0
  150. package/dist/core/upgrades/schema-version.js +97 -0
  151. package/dist/core/worktree.js +606 -0
  152. package/dist/facts.js +114 -0
  153. package/dist/facts.json +111 -0
  154. package/docs/architecture/project-refs.md +5 -1
  155. package/docs/cli.md +690 -43
  156. package/docs/concepts/ideation-loop.md +317 -0
  157. package/docs/concepts/loop-engine.md +456 -0
  158. package/docs/concepts/mcp-governance.md +268 -0
  159. package/docs/concepts/memory-staleness.md +122 -0
  160. package/docs/concepts/multi-agent-workflows.md +166 -0
  161. package/docs/concepts/plans-and-claims.md +31 -6
  162. package/docs/concepts/project-md-convention.md +35 -0
  163. package/docs/concepts/troubleshooting.md +220 -0
  164. package/docs/concepts/upgrade-cli.md +202 -0
  165. package/docs/concepts/upgrade-dogfood-procedure.md +114 -0
  166. package/docs/context-format-changelog.md +2 -2
  167. package/docs/context-format.md +2 -2
  168. package/docs/index.md +68 -0
  169. package/docs/integrations/agents.md +15 -16
  170. package/docs/integrations/cline.md +88 -0
  171. package/docs/integrations/codex.md +75 -23
  172. package/docs/integrations/continue.md +60 -0
  173. package/docs/integrations/copilot.md +67 -9
  174. package/docs/integrations/kilocode.md +72 -0
  175. package/docs/integrations/mcp.md +304 -21
  176. package/docs/integrations/mistral-vibe.md +122 -0
  177. package/docs/integrations/opencode.md +84 -0
  178. package/docs/integrations/overview.md +23 -8
  179. package/docs/integrations/roo.md +74 -0
  180. package/docs/integrations/windsurf.md +83 -0
  181. package/docs/mcp-schema-changelog.md +191 -1
  182. package/docs/playbooks/integration/index.md +121 -0
  183. package/docs/playbooks/productivity/index.md +102 -0
  184. package/docs/playbooks/team/index.md +122 -0
  185. package/docs/product/agent-first-model.md +184 -0
  186. package/docs/product/entity-model-audit.md +462 -0
  187. package/docs/quickstart-existing-project.md +135 -0
  188. package/docs/quickstart.md +124 -37
  189. package/docs/release-maintenance.md +79 -0
  190. package/docs/review.md +2 -0
  191. package/docs/server-operations.md +118 -0
  192. package/package.json +20 -12
  193. package/dist/commands/claude-desktop-extension.js +0 -18
  194. package/dist/commands/diff.js +0 -99
  195. package/dist/core/claude-desktop-extension.js +0 -224
@@ -0,0 +1,482 @@
1
+ /**
2
+ * LLM-driven semantic memory compaction.
3
+ *
4
+ * Two-phase protocol:
5
+ * Phase 1 (assessMemoryPressure): returns pressure flag + eligible items + template
6
+ * Phase 2 (applyCompaction): archives specified items, creates new memory entries
7
+ *
8
+ * @module
9
+ */
10
+ import fs from 'node:fs';
11
+ import path from 'node:path';
12
+ import { loadState, mutateState } from './state.js';
13
+ import { resolveEntityDir } from './io.js';
14
+ import { logger } from './logger.js';
15
+ import { generateIdWithLabel, nowISO } from './ids.js';
16
+ const PLAN_PRESSURE_THRESHOLD = 50;
17
+ const HANDOFF_PRESSURE_THRESHOLD = 30;
18
+ const DEFAULT_MAX_ITEMS = 20;
19
+ const DEFAULT_MIN_AGE_DAYS = 7;
20
+ export function checkMemoryPressure(cwd) {
21
+ const effectiveCwd = cwd ?? process.cwd();
22
+ const state = loadState(effectiveCwd);
23
+ const donePlans = state.plan_items.filter(p => p.status === 'done' || p.status === 'dropped').length;
24
+ const closedHandoffs = state.open_handoffs.filter(h => h.status === 'closed').length;
25
+ const eligible = countEligibleItems(state, DEFAULT_MIN_AGE_DAYS);
26
+ return {
27
+ memory_pressure: donePlans >= PLAN_PRESSURE_THRESHOLD || closedHandoffs >= HANDOFF_PRESSURE_THRESHOLD,
28
+ done_plans: donePlans,
29
+ closed_handoffs: closedHandoffs,
30
+ eligible_items: eligible,
31
+ thresholds: { plans: PLAN_PRESSURE_THRESHOLD, handoffs: HANDOFF_PRESSURE_THRESHOLD },
32
+ };
33
+ }
34
+ export function assessMemoryPressure(cwd) {
35
+ const effectiveCwd = cwd ?? process.cwd();
36
+ const state = loadState(effectiveCwd);
37
+ const cutoff = new Date(Date.now() - DEFAULT_MIN_AGE_DAYS * 24 * 60 * 60 * 1000).toISOString();
38
+ const donePlans = state.plan_items.filter(p => p.status === 'done' || p.status === 'dropped').length;
39
+ const closedHandoffs = state.open_handoffs.filter(h => h.status === 'closed').length;
40
+ const eligible = [];
41
+ for (const plan of state.plan_items) {
42
+ if (plan.status !== 'done' && plan.status !== 'dropped')
43
+ continue;
44
+ const completedAt = plan.completed_at ?? plan.updated_at ?? plan.created_at;
45
+ if (completedAt > cutoff)
46
+ continue;
47
+ eligible.push(planToCompactable(plan));
48
+ }
49
+ for (const handoff of state.open_handoffs) {
50
+ if (handoff.status !== 'closed')
51
+ continue;
52
+ if (handoff.created_at > cutoff)
53
+ continue;
54
+ eligible.push(handoffToCompactable(handoff));
55
+ }
56
+ eligible.sort((a, b) => a.created_at.localeCompare(b.created_at));
57
+ return {
58
+ pressure: donePlans >= PLAN_PRESSURE_THRESHOLD || closedHandoffs >= HANDOFF_PRESSURE_THRESHOLD,
59
+ done_plans: donePlans,
60
+ closed_handoffs: closedHandoffs,
61
+ eligible_items: eligible,
62
+ thresholds: { plans: PLAN_PRESSURE_THRESHOLD, handoffs: HANDOFF_PRESSURE_THRESHOLD },
63
+ };
64
+ }
65
+ function collectEligible(state, cutoff) {
66
+ const eligible = [];
67
+ for (const plan of state.plan_items) {
68
+ if (plan.status !== 'done' && plan.status !== 'dropped')
69
+ continue;
70
+ const completedAt = plan.completed_at ?? plan.updated_at ?? plan.created_at;
71
+ if (completedAt > cutoff)
72
+ continue;
73
+ eligible.push(planToCompactable(plan));
74
+ }
75
+ for (const handoff of state.open_handoffs) {
76
+ if (handoff.status !== 'closed')
77
+ continue;
78
+ if (handoff.created_at > cutoff)
79
+ continue;
80
+ eligible.push(handoffToCompactable(handoff));
81
+ }
82
+ eligible.sort((a, b) => a.created_at.localeCompare(b.created_at));
83
+ return eligible;
84
+ }
85
+ export function compact(options = {}) {
86
+ const cwd = options.cwd ?? process.cwd();
87
+ const maxItems = options.maxItems ?? DEFAULT_MAX_ITEMS;
88
+ const minAgeDays = options.minAgeDays ?? DEFAULT_MIN_AGE_DAYS;
89
+ const dryRun = options.dryRun ?? false;
90
+ const purgeReleasedClaims = options.purgeReleasedClaims ?? true;
91
+ const purgeSessionNotes = options.purgeSessionNotes ?? true;
92
+ const dedupHandoffs = options.dedupHandoffs ?? true;
93
+ const cutoff = new Date(Date.now() - minAgeDays * 24 * 60 * 60 * 1000).toISOString();
94
+ // Extensions (pln#436): file-direct sweeps, independent of mutateState
95
+ // since claims and runtime_notes live in their own stores.
96
+ const claimsArchived = purgeReleasedClaims ? archiveReleasedClaims(cwd, minAgeDays, dryRun) : 0;
97
+ const sessionNotesArchived = purgeSessionNotes ? archiveSessionNotes(cwd, minAgeDays, dryRun) : 0;
98
+ const handoffsDeduped = dedupHandoffs ? dedupAutoHandoffs(cwd, dryRun) : 0;
99
+ if (dryRun) {
100
+ const state = loadState(cwd);
101
+ const eligible = collectEligible(state, cutoff);
102
+ const selected = eligible.slice(0, maxItems);
103
+ return {
104
+ dry_run: true,
105
+ eligible_count: eligible.length,
106
+ archived_count: 0,
107
+ archived_items: selected,
108
+ template: selected.length > 0 ? buildCompactionTemplate(selected) : undefined,
109
+ claims_archived: claimsArchived,
110
+ session_notes_archived: sessionNotesArchived,
111
+ handoffs_deduped: handoffsDeduped,
112
+ };
113
+ }
114
+ return mutateState((state) => {
115
+ const eligible = collectEligible(state, cutoff);
116
+ const selected = eligible.slice(0, maxItems);
117
+ if (selected.length === 0) {
118
+ return {
119
+ dry_run: false,
120
+ eligible_count: eligible.length,
121
+ archived_count: 0,
122
+ archived_items: [],
123
+ claims_archived: claimsArchived,
124
+ session_notes_archived: sessionNotesArchived,
125
+ handoffs_deduped: handoffsDeduped,
126
+ };
127
+ }
128
+ const backupPath = createBackup(selected, cwd);
129
+ const archived = archiveCompactedItems(selected, cwd);
130
+ const archivedIds = new Set(archived.map(a => a.id));
131
+ state.plan_items = state.plan_items.filter(p => !archivedIds.has(p.id));
132
+ state.open_handoffs = state.open_handoffs.filter(h => !archivedIds.has(h.id));
133
+ return {
134
+ dry_run: false,
135
+ eligible_count: eligible.length,
136
+ archived_count: archived.length,
137
+ archived_items: archived,
138
+ backup_path: backupPath,
139
+ template: buildCompactionTemplate(archived),
140
+ claims_archived: claimsArchived,
141
+ session_notes_archived: sessionNotesArchived,
142
+ handoffs_deduped: handoffsDeduped,
143
+ };
144
+ }, cwd);
145
+ }
146
+ export function buildCompactionTemplate(items) {
147
+ const planItems = items.filter(i => i.type === 'plan');
148
+ const handoffItems = items.filter(i => i.type === 'handoff');
149
+ const lines = [];
150
+ lines.push('# Semantic Compaction Template');
151
+ lines.push('');
152
+ lines.push(items.length + ' item(s) have been archived from active memory.');
153
+ lines.push('Review the items below and create durable memory entries for any insights worth preserving.');
154
+ lines.push('');
155
+ if (planItems.length > 0) {
156
+ lines.push('## Archived Plans (' + planItems.length + ')');
157
+ for (const item of planItems) {
158
+ lines.push('');
159
+ lines.push('### ' + item.id + ' [' + item.status + '] \u2014 ' + (item.tags.join(', ') || 'untagged'));
160
+ lines.push('Created: ' + item.created_at.slice(0, 10) + (item.completed_at ? ' | Completed: ' + item.completed_at.slice(0, 10) : ''));
161
+ lines.push(item.text);
162
+ }
163
+ lines.push('');
164
+ }
165
+ if (handoffItems.length > 0) {
166
+ lines.push('## Archived Handoffs (' + handoffItems.length + ')');
167
+ for (const item of handoffItems) {
168
+ lines.push('');
169
+ lines.push('### ' + item.id + ' [' + item.status + '] \u2014 ' + (item.tags.join(', ') || 'untagged'));
170
+ lines.push('Created: ' + item.created_at.slice(0, 10) + ' | Author: ' + (item.author ?? 'unknown'));
171
+ lines.push(item.text);
172
+ }
173
+ lines.push('');
174
+ }
175
+ lines.push('## Instructions');
176
+ lines.push('');
177
+ lines.push('Summarize these ' + items.length + ' items. For each insight worth preserving, create a durable memory entry using bclaw_update_memory with type constraint, decision, or trap.');
178
+ lines.push('');
179
+ lines.push('Focus on:');
180
+ lines.push('- **Traps learned**: What recurring problems or pitfalls emerged?');
181
+ lines.push('- **Decisions confirmed**: What architectural or process decisions were validated?');
182
+ lines.push('- **Patterns observed**: What recurring themes appear across items?');
183
+ lines.push('');
184
+ lines.push('Do NOT re-create the original items. Distill them into concise, actionable memory entries.');
185
+ return lines.join('\n');
186
+ }
187
+ export function applyCompaction(options) {
188
+ const cwd = options.cwd ?? process.cwd();
189
+ return mutateState((state) => {
190
+ const toArchive = [];
191
+ for (const id of options.archiveIds) {
192
+ const plan = state.plan_items.find(p => p.id === id);
193
+ if (plan && (plan.status === 'done' || plan.status === 'dropped')) {
194
+ toArchive.push(planToCompactable(plan));
195
+ continue;
196
+ }
197
+ const handoff = state.open_handoffs.find(h => h.id === id);
198
+ if (handoff && handoff.status === 'closed') {
199
+ toArchive.push(handoffToCompactable(handoff));
200
+ }
201
+ }
202
+ const backupPath = createBackup(toArchive, cwd);
203
+ const archived = archiveCompactedItems(toArchive, cwd);
204
+ const archivedIds = new Set(archived.map(a => a.id));
205
+ state.plan_items = state.plan_items.filter(p => !archivedIds.has(p.id));
206
+ state.open_handoffs = state.open_handoffs.filter(h => !archivedIds.has(h.id));
207
+ const createdIds = [];
208
+ if (options.newItems && options.newItems.length > 0) {
209
+ const author = options.author ?? 'compaction';
210
+ const authorId = options.authorId;
211
+ for (const newItem of options.newItems) {
212
+ const { id, short_label } = generateIdWithLabel(newItem.type, cwd);
213
+ const now = nowISO();
214
+ const base = {
215
+ id, short_label, text: newItem.text, created_at: now, author,
216
+ ...(authorId ? { author_id: authorId } : {}),
217
+ tags: newItem.tags ?? ['compaction'],
218
+ };
219
+ if (newItem.type === 'constraint') {
220
+ state.active_constraints.push({ ...base, status: 'active' });
221
+ }
222
+ else if (newItem.type === 'decision') {
223
+ state.recent_decisions.push({ ...base });
224
+ }
225
+ else if (newItem.type === 'trap') {
226
+ state.known_traps.push({
227
+ ...base, status: 'active',
228
+ severity: newItem.severity ?? 'medium',
229
+ visibility: 'shared',
230
+ });
231
+ }
232
+ createdIds.push(id);
233
+ }
234
+ }
235
+ return {
236
+ archived_count: archived.length,
237
+ archived_ids: archived.map(a => a.id),
238
+ created_count: createdIds.length,
239
+ created_ids: createdIds,
240
+ backup_path: backupPath,
241
+ };
242
+ }, cwd);
243
+ }
244
+ function archiveCompactedItems(items, cwd) {
245
+ const archived = [];
246
+ for (const item of items) {
247
+ const entityDir = item.type === 'plan' ? 'plans' : 'handoffs';
248
+ const readDir = resolveEntityDir(entityDir, cwd, 'read');
249
+ const writeDir = resolveEntityDir(entityDir, cwd, 'write');
250
+ const sourcePath = path.join(readDir, item.id + '.json');
251
+ const archivePath = path.join(writeDir, 'compacted.jsonl');
252
+ if (!fs.existsSync(sourcePath)) {
253
+ logger.debug('gc-semantic: source file not found, skipping: ' + sourcePath);
254
+ continue;
255
+ }
256
+ try {
257
+ const content = fs.readFileSync(sourcePath, 'utf-8');
258
+ const parsed = JSON.parse(content);
259
+ parsed._compacted_at = new Date().toISOString();
260
+ parsed._compaction_type = 'semantic';
261
+ fs.mkdirSync(path.dirname(archivePath), { recursive: true });
262
+ fs.appendFileSync(archivePath, JSON.stringify(parsed) + '\n', 'utf-8');
263
+ archived.push(item);
264
+ }
265
+ catch (err) {
266
+ logger.debug('gc-semantic: failed to archive ' + item.id + ':', err);
267
+ }
268
+ }
269
+ return archived;
270
+ }
271
+ function createBackup(items, cwd) {
272
+ const writeDir = resolveEntityDir('gc-backups', cwd, 'write');
273
+ fs.mkdirSync(writeDir, { recursive: true });
274
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
275
+ const backupPath = path.join(writeDir, 'compact-' + timestamp + '.jsonl');
276
+ for (const item of items) {
277
+ const entityDir = item.type === 'plan' ? 'plans' : 'handoffs';
278
+ const readDir = resolveEntityDir(entityDir, cwd, 'read');
279
+ const sourcePath = path.join(readDir, item.id + '.json');
280
+ if (!fs.existsSync(sourcePath))
281
+ continue;
282
+ try {
283
+ const content = fs.readFileSync(sourcePath, 'utf-8');
284
+ fs.appendFileSync(backupPath, content.trim() + '\n', 'utf-8');
285
+ }
286
+ catch (err) {
287
+ logger.debug('gc-semantic: backup failed for ' + item.id + ':', err);
288
+ }
289
+ }
290
+ return backupPath;
291
+ }
292
+ function planToCompactable(plan) {
293
+ return {
294
+ id: plan.id, type: 'plan', text: plan.text, created_at: plan.created_at,
295
+ completed_at: plan.completed_at, status: plan.status, tags: plan.tags, author: plan.author,
296
+ };
297
+ }
298
+ function handoffToCompactable(handoff) {
299
+ return {
300
+ id: handoff.id, type: 'handoff', text: handoff.text, created_at: handoff.created_at,
301
+ status: handoff.status, tags: handoff.tags, author: handoff.author,
302
+ };
303
+ }
304
+ function countEligibleItems(state, minAgeDays) {
305
+ const cutoff = new Date(Date.now() - minAgeDays * 24 * 60 * 60 * 1000).toISOString();
306
+ let count = 0;
307
+ for (const plan of state.plan_items) {
308
+ if (plan.status !== 'done' && plan.status !== 'dropped')
309
+ continue;
310
+ const completedAt = plan.completed_at ?? plan.updated_at ?? plan.created_at;
311
+ if (completedAt <= cutoff)
312
+ count++;
313
+ }
314
+ for (const handoff of state.open_handoffs) {
315
+ if (handoff.status !== 'closed')
316
+ continue;
317
+ if (handoff.created_at <= cutoff)
318
+ count++;
319
+ }
320
+ return count;
321
+ }
322
+ /**
323
+ * Archive released claims older than the cutoff. Claims live in their own
324
+ * JsonStore (.brainclaw/coordination/claims/*.json), independent of the
325
+ * mutateState pipeline, so this is a direct file sweep with a compacted.jsonl
326
+ * + gc-backup trail matching the plans/handoffs compaction contract.
327
+ */
328
+ function archiveReleasedClaims(cwd, minAgeDays, dryRun) {
329
+ const cutoff = new Date(Date.now() - minAgeDays * 24 * 60 * 60 * 1000).toISOString();
330
+ const claimsDir = path.join(cwd, '.brainclaw', 'coordination', 'claims');
331
+ if (!fs.existsSync(claimsDir))
332
+ return 0;
333
+ const files = fs.readdirSync(claimsDir).filter(f => f.endsWith('.json'));
334
+ const eligible = [];
335
+ for (const file of files) {
336
+ try {
337
+ const content = fs.readFileSync(path.join(claimsDir, file), 'utf-8');
338
+ const parsed = JSON.parse(content);
339
+ const status = typeof parsed.status === 'string' ? parsed.status : '';
340
+ const releasedAt = typeof parsed.released_at === 'string' ? parsed.released_at
341
+ : typeof parsed.updated_at === 'string' ? parsed.updated_at
342
+ : typeof parsed.created_at === 'string' ? parsed.created_at : '';
343
+ if (status !== 'released')
344
+ continue;
345
+ if (!releasedAt || releasedAt > cutoff)
346
+ continue;
347
+ eligible.push({ file, content });
348
+ }
349
+ catch {
350
+ // Skip unparseable files — they'll be surfaced by loadDirectoryItems'
351
+ // logger.warn in a separate pass (data-loss fix guarantees preservation).
352
+ }
353
+ }
354
+ if (dryRun || eligible.length === 0)
355
+ return eligible.length;
356
+ // Archive to compacted.jsonl + backup + unlink.
357
+ const archivePath = path.join(claimsDir, 'compacted.jsonl');
358
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
359
+ const backupPath = path.join(cwd, '.brainclaw', 'gc-backups', `compact-claims-${timestamp}.jsonl`);
360
+ fs.mkdirSync(path.dirname(backupPath), { recursive: true });
361
+ for (const { file, content } of eligible) {
362
+ const parsed = JSON.parse(content);
363
+ parsed._compacted_at = new Date().toISOString();
364
+ parsed._compaction_type = 'released-claim';
365
+ fs.appendFileSync(archivePath, JSON.stringify(parsed) + '\n', 'utf-8');
366
+ fs.appendFileSync(backupPath, content.trim() + '\n', 'utf-8');
367
+ fs.unlinkSync(path.join(claimsDir, file));
368
+ }
369
+ return eligible.length;
370
+ }
371
+ /**
372
+ * Archive session-lifecycle runtime_notes older than the cutoff. These are
373
+ * the "Session started / ended" auto-generated notes (tagged `session`) which
374
+ * accumulate at 1000s per project and bury the real human signal.
375
+ */
376
+ function archiveSessionNotes(cwd, minAgeDays, dryRun) {
377
+ const cutoff = new Date(Date.now() - minAgeDays * 24 * 60 * 60 * 1000).toISOString();
378
+ const runtimeDir = path.join(cwd, '.brainclaw', 'coordination', 'runtime');
379
+ if (!fs.existsSync(runtimeDir))
380
+ return 0;
381
+ const eligible = [];
382
+ // Runtime notes are nested: runtime/<agent>/<id>.json
383
+ for (const agent of fs.readdirSync(runtimeDir)) {
384
+ const agentDir = path.join(runtimeDir, agent);
385
+ if (!fs.statSync(agentDir).isDirectory())
386
+ continue;
387
+ const files = fs.readdirSync(agentDir).filter(f => f.endsWith('.json'));
388
+ for (const file of files) {
389
+ const filePath = path.join(agentDir, file);
390
+ try {
391
+ const content = fs.readFileSync(filePath, 'utf-8');
392
+ const parsed = JSON.parse(content);
393
+ const tags = Array.isArray(parsed.tags) ? parsed.tags : [];
394
+ const isSession = tags.some(t => t === 'session');
395
+ const createdAt = typeof parsed.created_at === 'string' ? parsed.created_at : '';
396
+ if (!isSession || !createdAt || createdAt > cutoff)
397
+ continue;
398
+ eligible.push({ filePath, content });
399
+ }
400
+ catch {
401
+ // Skip unparseable
402
+ }
403
+ }
404
+ }
405
+ if (dryRun || eligible.length === 0)
406
+ return eligible.length;
407
+ const archivePath = path.join(runtimeDir, 'session-notes-archive.jsonl');
408
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
409
+ const backupPath = path.join(cwd, '.brainclaw', 'gc-backups', `compact-session-notes-${timestamp}.jsonl`);
410
+ fs.mkdirSync(path.dirname(backupPath), { recursive: true });
411
+ for (const { filePath, content } of eligible) {
412
+ const parsed = JSON.parse(content);
413
+ parsed._compacted_at = new Date().toISOString();
414
+ parsed._compaction_type = 'session-note';
415
+ fs.appendFileSync(archivePath, JSON.stringify(parsed) + '\n', 'utf-8');
416
+ fs.appendFileSync(backupPath, content.trim() + '\n', 'utf-8');
417
+ fs.unlinkSync(filePath);
418
+ }
419
+ return eligible.length;
420
+ }
421
+ /**
422
+ * Deduplicate auto-generated session-end handoffs. These carry the same
423
+ * commits list when several sessions close on the same project state, so the
424
+ * board ends up with N near-identical handoff rows. Group by a signature
425
+ * built from the commits block and keep only the most recent per group.
426
+ */
427
+ function dedupAutoHandoffs(cwd, dryRun) {
428
+ const handoffsDir = path.join(cwd, '.brainclaw', 'coordination', 'handoffs');
429
+ if (!fs.existsSync(handoffsDir))
430
+ return 0;
431
+ const files = fs.readdirSync(handoffsDir).filter(f => f.endsWith('.json'));
432
+ // signature -> [{file, created_at, content}]
433
+ const groups = new Map();
434
+ for (const file of files) {
435
+ const filePath = path.join(handoffsDir, file);
436
+ try {
437
+ const content = fs.readFileSync(filePath, 'utf-8');
438
+ const parsed = JSON.parse(content);
439
+ const text = typeof parsed.text === 'string' ? parsed.text : '';
440
+ // Auto-generated session-end handoffs start with "Session sess_… — auto-generated handoff"
441
+ // and enumerate commits in a block. Skip non-matching handoffs (human-authored).
442
+ if (!text.startsWith('Session sess_') || !text.includes('auto-generated handoff'))
443
+ continue;
444
+ // Signature: first 100 chars of the "Commits:" block.
445
+ const commitsIdx = text.indexOf('Commits:');
446
+ const sig = commitsIdx >= 0 ? text.slice(commitsIdx, commitsIdx + 100) : text.slice(0, 100);
447
+ const createdAt = typeof parsed.created_at === 'string' ? parsed.created_at : '';
448
+ const list = groups.get(sig) ?? [];
449
+ list.push({ file, createdAt, content });
450
+ groups.set(sig, list);
451
+ }
452
+ catch {
453
+ // Skip unparseable
454
+ }
455
+ }
456
+ // For each group with >1 entries, keep the most recent and archive the rest.
457
+ const toArchive = [];
458
+ for (const list of groups.values()) {
459
+ if (list.length <= 1)
460
+ continue;
461
+ list.sort((a, b) => b.createdAt.localeCompare(a.createdAt)); // newest first
462
+ for (const entry of list.slice(1)) {
463
+ toArchive.push({ filePath: path.join(handoffsDir, entry.file), content: entry.content });
464
+ }
465
+ }
466
+ if (dryRun || toArchive.length === 0)
467
+ return toArchive.length;
468
+ const archivePath = path.join(handoffsDir, 'compacted.jsonl');
469
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
470
+ const backupPath = path.join(cwd, '.brainclaw', 'gc-backups', `compact-handoffs-dedup-${timestamp}.jsonl`);
471
+ fs.mkdirSync(path.dirname(backupPath), { recursive: true });
472
+ for (const { filePath, content } of toArchive) {
473
+ const parsed = JSON.parse(content);
474
+ parsed._compacted_at = new Date().toISOString();
475
+ parsed._compaction_type = 'handoff-dedup';
476
+ fs.appendFileSync(archivePath, JSON.stringify(parsed) + '\n', 'utf-8');
477
+ fs.appendFileSync(backupPath, content.trim() + '\n', 'utf-8');
478
+ fs.unlinkSync(filePath);
479
+ }
480
+ return toArchive.length;
481
+ }
482
+ //# sourceMappingURL=gc-semantic.js.map