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
@@ -1,16 +1,30 @@
1
1
  import { execSync } from 'node:child_process';
2
+ import path from 'node:path';
2
3
  import { memoryExists } from '../core/io.js';
3
4
  import { buildOperationalIdentity, clearCurrentSession } from '../core/identity.js';
4
5
  import { buildContextDiff } from '../core/context-diff.js';
5
6
  import { listClaims, releaseClaim } from '../core/claims.js';
6
7
  import { listRuntimeNotes, saveRuntimeNote, generateRuntimeNoteId } from '../core/runtime.js';
7
- import { loadState } from '../core/state.js';
8
+ import { loadState, persistState } from '../core/state.js';
9
+ import { listArchivedCandidates, listCandidates } from '../core/candidates.js';
10
+ import { createFederationMessage } from '../core/federation-message.js';
11
+ import { pushSignal } from '../core/federation-transport.js';
12
+ import { loadConfig } from '../core/config.js';
13
+ import { resolveCrossProjectLinks } from '../core/cross-project.js';
8
14
  import { createCandidateFromInput } from './reflect.js';
9
15
  import { suggestCandidateTypes } from './reflect-runtime-note.js';
10
- import { nowISO } from '../core/ids.js';
11
- import { appendAuditEntry } from '../core/audit.js';
16
+ import { generateIdWithLabel, nowISO } from '../core/ids.js';
17
+ import { appendAuditEntry, readAuditLog } from '../core/audit.js';
12
18
  import { requireMinimumTrustLevel, requireRegisteredAgentIdentity } from '../core/agent-registry.js';
13
19
  import { loadSessionSnapshot } from '../commands/session-start.js';
20
+ import { extractFilesFromDiff } from '../commands/handoff.js';
21
+ import { suggestCompaction } from '../core/memory-compactor.js';
22
+ import { dispatchReview } from '../core/dispatcher.js';
23
+ export const REFLECTION_QUESTIONS = [
24
+ 'What was the biggest time waste in this session, and how could it have been avoided?',
25
+ 'What should have been done differently (design, process, or approach)?',
26
+ 'What should brainclaw itself improve based on this session?',
27
+ ];
14
28
  export function runSessionEnd(options = {}) {
15
29
  try {
16
30
  const result = endSession(options);
@@ -40,9 +54,43 @@ export function runSessionEnd(options = {}) {
40
54
  if (options.autoReflect) {
41
55
  console.log(` Candidates created from auto-reflect: ${result.candidates_created}`);
42
56
  }
57
+ if (result.handoff) {
58
+ console.log(` Reflected handoff: ${result.handoff.handoff_id}${result.handoff.plan_id ? ` (${result.handoff.plan_id})` : ''}`);
59
+ if (result.handoff.review_dispatched) {
60
+ console.log(` Review dispatched: ${result.handoff.reviewer}${result.handoff.review_message_id ? ` [${result.handoff.review_message_id}]` : ''}`);
61
+ }
62
+ else if (result.handoff.review_skip_reason) {
63
+ console.log(` Review not dispatched: ${result.handoff.review_skip_reason}`);
64
+ }
65
+ }
43
66
  if (result.context_diff) {
44
67
  console.log(` ${result.context_diff}`);
45
68
  }
69
+ if (result.session_stats) {
70
+ console.log(' Session stats:');
71
+ console.log(` duration: ${result.session_stats.session_duration_minutes} min`);
72
+ console.log(` file edits: ${result.session_stats.file_edits_count}`);
73
+ console.log(` claims created: ${result.session_stats.claims_created}`);
74
+ console.log(` memory writes: ${result.session_stats.memory_writes}`);
75
+ console.log(` plan updates: ${result.session_stats.plan_updates}`);
76
+ console.log(` candidates created: ${result.session_stats.candidates_created}`);
77
+ if (result.session_stats.last_brainclaw_write) {
78
+ console.log(` last brainclaw write: ${result.session_stats.last_brainclaw_write}`);
79
+ }
80
+ for (const warning of result.session_stats.warnings) {
81
+ console.log(` warning: ${warning}`);
82
+ }
83
+ }
84
+ if (result.compaction_hint) {
85
+ console.log(` 💡 ${result.compaction_hint}`);
86
+ }
87
+ if (result.reflection_prompt) {
88
+ console.log('\n📝 Session reflection:');
89
+ for (let i = 0; i < result.reflection_prompt.questions.length; i++) {
90
+ console.log(` ${i + 1}. ${result.reflection_prompt.questions[i]}`);
91
+ }
92
+ console.log(`\n → Answer with: brainclaw note "your reflection" --tag reflection --tag session:${result.session_id}`);
93
+ }
46
94
  }
47
95
  catch (e) {
48
96
  console.error(`Error: ${e instanceof Error ? e.message : String(e)}`);
@@ -111,9 +159,8 @@ export function endSession(options = {}) {
111
159
  visibility: 'shared',
112
160
  note_type: 'session_end',
113
161
  }, options.cwd);
114
- appendAuditEntry({ action: 'session_end', actor: actor.agent, actor_id: actor.agent_id, item_id: sessionId, item_type: 'session' }, options.cwd);
115
- clearCurrentSession(options.cwd, sessionId);
116
- // Reflect-handoff: generate a handoff candidate from git commits since session start
162
+ // Reflect-handoff: materialize an open handoff from git commits since session start
163
+ let reflectedHandoff;
117
164
  if (options.reflectHandoff) {
118
165
  try {
119
166
  const snapshot = loadSessionSnapshot(sessionId, options.cwd);
@@ -123,25 +170,79 @@ export function endSession(options = {}) {
123
170
  const commits = execSync(`git log --oneline ${ref}..HEAD`, { encoding: 'utf-8', cwd }).trim();
124
171
  const diffStat = execSync(`git diff --stat ${ref}..HEAD`, { encoding: 'utf-8', cwd }).trim();
125
172
  if (commits) {
126
- const releasedScopes = listClaims(options.cwd)
127
- .filter((c) => c.status === 'released' && c.agent === registered.agent_name)
128
- .map((c) => c.scope)
129
- .join(', ');
173
+ const releasedClaims = listClaims(options.cwd)
174
+ .filter((c) => c.status === 'released' && c.agent === registered.agent_name);
175
+ const releasedScopes = releasedClaims.map((c) => c.scope).join(', ');
176
+ // Extract files touched from the full diff for the contract
177
+ let filesTouched = [];
178
+ let fullDiff;
179
+ try {
180
+ fullDiff = execSync(`git diff ${ref}..HEAD`, { encoding: 'utf-8', cwd, maxBuffer: 10 * 1024 * 1024 }).trim();
181
+ filesTouched = extractFilesFromDiff(fullDiff);
182
+ }
183
+ catch { /* fall back to empty */ }
184
+ // Extract linked plan IDs from released claims
185
+ const linkedPlans = [...new Set(releasedClaims.map((c) => c.plan_id).filter(Boolean))];
186
+ const primaryPlanId = linkedPlans.length === 1 ? linkedPlans[0] : undefined;
187
+ // Build contract metadata for the handoff text
188
+ const contractLines = [];
189
+ if (filesTouched.length > 0)
190
+ contractLines.push(`Files touched: ${filesTouched.join(', ')}`);
191
+ if (linkedPlans.length > 0)
192
+ contractLines.push(`Linked plans: ${linkedPlans.join(', ')}`);
130
193
  const handoffText = [
131
194
  `Session ${sessionId} — auto-generated handoff`,
195
+ options.narrative ? `\nNarrative: ${options.narrative}` : '',
132
196
  '',
133
197
  `Commits:\n${commits}`,
134
198
  diffStat ? `\nChanged files:\n${diffStat}` : '',
135
199
  releasedScopes ? `\nReleased claims: ${releasedScopes}` : '',
200
+ contractLines.length > 0 ? `\nContract:\n${contractLines.join('\n')}` : '',
136
201
  summaryText !== `Session ended — ${sessionNotes.length} runtime note(s) created` ? `\nSummary: ${summaryText}` : '',
137
202
  ].filter(Boolean).join('\n');
138
- createCandidateFromInput(handoffText, 'handoff', {
203
+ const narrativeParts = [
204
+ options.narrative?.trim(),
205
+ summaryText !== `Session ended — ${sessionNotes.length} runtime note(s) created` ? summaryText : undefined,
206
+ ].filter((value) => Boolean(value && value.trim().length > 0));
207
+ const materialized = materializeSessionHandoff({
139
208
  author: actor.agent,
140
209
  authorId: actor.agent_id,
141
210
  sessionId,
142
- source: `session-end:git-diff:${sessionId}`,
211
+ text: handoffText,
212
+ narrative: narrativeParts.length > 0 ? narrativeParts.join('\n\n') : undefined,
213
+ planId: primaryPlanId,
214
+ linkedPlans,
215
+ filesTouched,
216
+ fullDiff,
143
217
  cwd: options.cwd,
144
- }, false, false, true);
218
+ });
219
+ reflectedHandoff = {
220
+ handoff_id: materialized.handoff_id,
221
+ plan_id: materialized.plan_id,
222
+ review_dispatched: false,
223
+ review_skip_reason: options.dispatchReview ? 'Reflected handoff is not reviewable yet' : undefined,
224
+ };
225
+ if (options.dispatchReview) {
226
+ const reviewResult = dispatchReview({
227
+ handoffId: materialized.handoff_id,
228
+ reviewer: options.reviewer,
229
+ dispatcherAgent: actor.agent,
230
+ dispatcherAgentId: actor.agent_id,
231
+ sessionId,
232
+ }, options.cwd ?? process.cwd());
233
+ const sent = reviewResult.reviews_sent.find((entry) => entry.handoff_id === materialized.handoff_id);
234
+ const skipped = reviewResult.skipped.find((entry) => entry.handoff_id === materialized.handoff_id);
235
+ if (sent) {
236
+ reflectedHandoff.review_dispatched = true;
237
+ reflectedHandoff.reviewer = sent.reviewer;
238
+ reflectedHandoff.review_message_id = sent.message_id;
239
+ reflectedHandoff.review_skip_reason = undefined;
240
+ updateReflectedHandoffRecipient(materialized.handoff_id, sent.reviewer, options.cwd);
241
+ }
242
+ else if (skipped) {
243
+ reflectedHandoff.review_skip_reason = skipped.reason;
244
+ }
245
+ }
145
246
  }
146
247
  }
147
248
  catch { /* non-fatal — no git or no commits */ }
@@ -162,7 +263,7 @@ export function endSession(options = {}) {
162
263
  authorId: note.agent_id,
163
264
  projectId: note.project_id,
164
265
  sessionId: note.session_id,
165
- source: `runtime-note:${note.agent}:${note.id}`,
266
+ source: 'auto',
166
267
  cwd: options.cwd,
167
268
  }, false, false, true);
168
269
  if (creation.candidateId) {
@@ -173,6 +274,46 @@ export function endSession(options = {}) {
173
274
  }
174
275
  }
175
276
  }
277
+ const sessionStats = buildSessionStats({
278
+ sessionId,
279
+ sessionStartedAt: loadSessionSnapshot(sessionId, options.cwd)?.started_at,
280
+ agent: actor.agent,
281
+ agentId: actor.agent_id,
282
+ notesInSession: sessionNotes,
283
+ cwd: options.cwd,
284
+ });
285
+ // Memory compaction hint (best-effort, non-fatal)
286
+ let compactionHint;
287
+ try {
288
+ compactionHint = suggestCompaction(state);
289
+ }
290
+ catch { /* non-fatal */ }
291
+ let pushedSignals = 0;
292
+ try {
293
+ pushedSignals = pushSessionFederationSignals({
294
+ sessionId,
295
+ actor,
296
+ sessionNotes,
297
+ cwd: options.cwd,
298
+ });
299
+ }
300
+ catch {
301
+ // Non-fatal
302
+ }
303
+ if (pushedSignals > 0 && !options.json) {
304
+ console.log(`✔ Pushed ${pushedSignals} signal(s) to linked projects`);
305
+ }
306
+ appendAuditEntry({
307
+ action: 'session_end',
308
+ actor: actor.agent,
309
+ actor_id: actor.agent_id,
310
+ item_id: sessionId,
311
+ item_type: 'session',
312
+ session_id: sessionId,
313
+ host_id: actor.host_id,
314
+ after: sessionStats,
315
+ }, options.cwd);
316
+ clearCurrentSession(options.cwd, sessionId);
176
317
  const result = {
177
318
  session_id: sessionId,
178
319
  agent: actor.agent,
@@ -181,7 +322,275 @@ export function endSession(options = {}) {
181
322
  context_diff: contextDiff,
182
323
  summary: summaryText,
183
324
  open_work_warning: openWorkWarning,
325
+ session_stats: sessionStats,
326
+ compaction_hint: compactionHint,
327
+ ...(reflectedHandoff ? { handoff: reflectedHandoff } : {}),
184
328
  };
329
+ if (options.reflect) {
330
+ result.reflection_prompt = {
331
+ questions: [...REFLECTION_QUESTIONS],
332
+ instruction: `Please reflect on this session and answer each question. Write your answers using bclaw_write_note with tags ["reflection", "session:${sessionId}"]. One note per question, or a single combined note.`,
333
+ };
334
+ }
185
335
  return result;
186
336
  }
337
+ function materializeSessionHandoff(input) {
338
+ const cwd = input.cwd ?? process.cwd();
339
+ const state = loadState(cwd);
340
+ const { id, short_label } = generateIdWithLabel('open_handoffs', cwd);
341
+ state.open_handoffs.push({
342
+ id,
343
+ short_label,
344
+ from: input.author,
345
+ to: 'reviewer',
346
+ text: input.text,
347
+ created_at: nowISO(),
348
+ author: input.author,
349
+ author_id: input.authorId,
350
+ session_id: input.sessionId,
351
+ status: 'open',
352
+ plan_id: input.planId,
353
+ narrative: input.narrative,
354
+ tags: ['auto-handoff', `session:${input.sessionId}`],
355
+ related_paths: input.filesTouched.length > 0 ? input.filesTouched : undefined,
356
+ contract: input.filesTouched.length > 0 || input.linkedPlans.length > 0
357
+ ? {
358
+ files_touched: input.filesTouched.length > 0 ? input.filesTouched : undefined,
359
+ linked_plans: input.linkedPlans.length > 0 ? input.linkedPlans : undefined,
360
+ }
361
+ : undefined,
362
+ snapshot: input.fullDiff ? { diff: input.fullDiff } : undefined,
363
+ });
364
+ persistState(state, cwd);
365
+ return { handoff_id: id, plan_id: input.planId };
366
+ }
367
+ function updateReflectedHandoffRecipient(handoffId, reviewer, cwd) {
368
+ const effectiveCwd = cwd ?? process.cwd();
369
+ const state = loadState(effectiveCwd);
370
+ const handoff = state.open_handoffs.find((entry) => entry.id === handoffId);
371
+ if (!handoff)
372
+ return;
373
+ handoff.to = reviewer;
374
+ persistState(state, effectiveCwd);
375
+ }
376
+ function pushSessionFederationSignals(input) {
377
+ const cwd = input.cwd ?? process.cwd();
378
+ const config = loadConfig(cwd);
379
+ const links = resolveCrossProjectLinks(cwd);
380
+ const publisherLinks = links.filter((link) => link.role === 'publisher' && link.available);
381
+ if (!config.cross_project_links?.length || publisherLinks.length === 0) {
382
+ return 0;
383
+ }
384
+ const currentState = loadState(cwd);
385
+ const sessionHandoffs = currentState.open_handoffs.filter((handoff) => handoff.session_id === input.sessionId);
386
+ const sessionCandidates = [
387
+ ...listCandidates(undefined, cwd),
388
+ ...listArchivedCandidates('accepted', cwd),
389
+ ...listArchivedCandidates('rejected', cwd),
390
+ ].filter((candidate) => candidate.session_id === input.sessionId);
391
+ const sessionRuntimeNotes = input.sessionNotes.filter((note) => note.session_id === input.sessionId);
392
+ const fromProjectName = config.project_name ?? path.basename(cwd);
393
+ const seen = new Set();
394
+ let pushed = 0;
395
+ const pushEntitySignal = (entityType, entity) => {
396
+ const target = extractCrossProjectTarget(entity);
397
+ if (!target)
398
+ return;
399
+ const link = resolvePublisherLink(target, publisherLinks, entityType, cwd);
400
+ if (!link)
401
+ return;
402
+ const dedupeKey = `${entityType}:${entity.id}:${link.absolutePath}`;
403
+ if (seen.has(dedupeKey))
404
+ return;
405
+ seen.add(dedupeKey);
406
+ const message = createFederationMessage({
407
+ version: 1,
408
+ from: {
409
+ project_id: input.actor.project_id ?? config.project_id,
410
+ project_name: fromProjectName,
411
+ project_path: cwd,
412
+ agent_name: input.actor.agent,
413
+ agent_id: input.actor.agent_id,
414
+ host_id: input.actor.host_id,
415
+ },
416
+ to: {
417
+ project_name: link.projectName,
418
+ project_path: link.absolutePath,
419
+ },
420
+ type: entityType,
421
+ payload: entity,
422
+ causal_parent: input.sessionId,
423
+ });
424
+ pushSignal(link.absolutePath, message);
425
+ pushed++;
426
+ };
427
+ for (const handoff of sessionHandoffs) {
428
+ pushEntitySignal('handoff', handoff);
429
+ }
430
+ for (const candidate of sessionCandidates) {
431
+ pushEntitySignal('candidate', candidate);
432
+ }
433
+ for (const note of sessionRuntimeNotes) {
434
+ pushEntitySignal('runtime_note', note);
435
+ }
436
+ return pushed;
437
+ }
438
+ function resolvePublisherLink(target, publisherLinks, entityType, cwd) {
439
+ const normalized = target.trim().toLowerCase();
440
+ if (!normalized)
441
+ return undefined;
442
+ const absoluteTarget = path.resolve(cwd, target).toLowerCase();
443
+ return publisherLinks.find((link) => {
444
+ if (link.channels?.length && !link.channels.includes(entityType)) {
445
+ return false;
446
+ }
447
+ const matchesByLabel = [link.projectName, link.name, link.path, link.absolutePath, path.basename(link.absolutePath)]
448
+ .filter((entry) => Boolean(entry))
449
+ .some((entry) => entry.toLowerCase() === normalized);
450
+ if (matchesByLabel)
451
+ return true;
452
+ return path.resolve(link.absolutePath).toLowerCase() === absoluteTarget;
453
+ });
454
+ }
455
+ function extractCrossProjectTarget(entity) {
456
+ const direct = extractTargetValue(entity.target_project)
457
+ ?? extractTargetValue(entity.targetProject)
458
+ ?? extractTargetValue(entity.cross_project)
459
+ ?? extractTargetValue(entity.crossProject);
460
+ if (direct)
461
+ return direct;
462
+ const metadata = entity.metadata;
463
+ if (isRecord(metadata)) {
464
+ const metadataTarget = extractTargetValue(metadata.target_project)
465
+ ?? extractTargetValue(metadata.targetProject)
466
+ ?? extractTargetValue(metadata.cross_project)
467
+ ?? extractTargetValue(metadata.crossProject);
468
+ if (metadataTarget)
469
+ return metadataTarget;
470
+ }
471
+ const tags = entity.tags;
472
+ if (Array.isArray(tags)) {
473
+ for (const rawTag of tags) {
474
+ if (typeof rawTag !== 'string')
475
+ continue;
476
+ const parsed = parseTargetTag(rawTag);
477
+ if (parsed)
478
+ return parsed;
479
+ }
480
+ }
481
+ return undefined;
482
+ }
483
+ function extractTargetValue(value) {
484
+ if (typeof value === 'string') {
485
+ const trimmed = value.trim();
486
+ return trimmed.length > 0 ? trimmed : undefined;
487
+ }
488
+ if (!isRecord(value))
489
+ return undefined;
490
+ for (const key of ['path', 'project_path', 'name', 'project_name']) {
491
+ const nested = value[key];
492
+ if (typeof nested === 'string' && nested.trim().length > 0) {
493
+ return nested.trim();
494
+ }
495
+ }
496
+ return undefined;
497
+ }
498
+ function parseTargetTag(tag) {
499
+ const match = tag.match(/^(?:target_project|target-project|targetProject|cross_project|cross-project)\s*:\s*(.+)$/i);
500
+ if (!match)
501
+ return undefined;
502
+ const target = match[1].trim();
503
+ return target.length > 0 ? target : undefined;
504
+ }
505
+ function isRecord(value) {
506
+ return Boolean(value) && typeof value === 'object';
507
+ }
508
+ const SESSION_MEMORY_WRITE_ACTIONS = ['create', 'update', 'delete', 'accept', 'reject', 'trust_change', 'promote_direct', 'rollback'];
509
+ function buildSessionStats(input) {
510
+ if (!input.sessionStartedAt) {
511
+ return undefined;
512
+ }
513
+ const startedAtMs = Date.parse(input.sessionStartedAt);
514
+ if (!Number.isFinite(startedAtMs)) {
515
+ return undefined;
516
+ }
517
+ const auditEntries = readAuditLog({ since: input.sessionStartedAt, actor: input.agentId ?? input.agent }, input.cwd)
518
+ .filter((entry) => belongsToSession(entry, input.sessionId));
519
+ const runtimeWrites = input.notesInSession.filter((note) => (note.note_type ?? 'observation') === 'observation');
520
+ const claimsCreated = auditEntries.filter((entry) => entry.action === 'claim').length;
521
+ const planUpdates = auditEntries.filter((entry) => entry.item_type === 'plan' && ['create', 'update', 'delete'].includes(entry.action)).length;
522
+ const memoryWrites = auditEntries.filter((entry) => SESSION_MEMORY_WRITE_ACTIONS.includes(entry.action)).length + runtimeWrites.length;
523
+ const lastBrainclawWrite = [
524
+ ...runtimeWrites.map((note) => note.created_at),
525
+ ...auditEntries.filter((entry) => SESSION_MEMORY_WRITE_ACTIONS.includes(entry.action)).map((entry) => entry.timestamp),
526
+ ].sort().at(-1);
527
+ const candidatesCreated = countSessionCandidates(input.sessionId, input.cwd);
528
+ const fileEditsCount = countSessionEditedFiles(input.sessionId, input.cwd);
529
+ const warnings = [];
530
+ if (fileEditsCount > 0 && claimsCreated === 0) {
531
+ warnings.push(`${fileEditsCount} file edit(s) with 0 claims created`);
532
+ }
533
+ if (fileEditsCount > 0 && memoryWrites === 0) {
534
+ warnings.push(`${fileEditsCount} file edit(s) with 0 memory writes suggests decisions or traps may have been missed`);
535
+ }
536
+ return {
537
+ session_duration_minutes: Math.max(0, Math.floor((Date.now() - startedAtMs) / 60_000)),
538
+ file_edits_count: fileEditsCount,
539
+ claims_created: claimsCreated,
540
+ memory_writes: memoryWrites,
541
+ plan_updates: planUpdates,
542
+ candidates_created: candidatesCreated,
543
+ last_brainclaw_write: lastBrainclawWrite,
544
+ warnings,
545
+ };
546
+ }
547
+ function belongsToSession(entry, sessionId) {
548
+ return !entry.session_id || entry.session_id === sessionId;
549
+ }
550
+ function countSessionCandidates(sessionId, cwd) {
551
+ const authored = [
552
+ ...listCandidates(undefined, cwd),
553
+ ...listArchivedCandidates('accepted', cwd),
554
+ ...listArchivedCandidates('rejected', cwd),
555
+ ];
556
+ return authored.filter((candidate) => candidate.session_id === sessionId).length;
557
+ }
558
+ function countSessionEditedFiles(sessionId, cwd) {
559
+ const snapshot = loadSessionSnapshot(sessionId, cwd);
560
+ const repoCwd = cwd ?? process.cwd();
561
+ try {
562
+ const touched = new Set();
563
+ if (snapshot?.git_sha) {
564
+ for (const pathEntry of execSync(`git diff --name-only ${snapshot.git_sha}..HEAD`, {
565
+ cwd: repoCwd,
566
+ encoding: 'utf-8',
567
+ stdio: ['ignore', 'pipe', 'ignore'],
568
+ }).split(/\r?\n/).filter((entry) => Boolean(entry) && shouldCountEditedPath(entry))) {
569
+ touched.add(pathEntry);
570
+ }
571
+ }
572
+ for (const pathEntry of execSync('git diff --name-only HEAD', {
573
+ cwd: repoCwd,
574
+ encoding: 'utf-8',
575
+ stdio: ['ignore', 'pipe', 'ignore'],
576
+ }).split(/\r?\n/).filter((entry) => Boolean(entry) && shouldCountEditedPath(entry))) {
577
+ touched.add(pathEntry);
578
+ }
579
+ for (const pathEntry of execSync('git ls-files --others --exclude-standard', {
580
+ cwd: repoCwd,
581
+ encoding: 'utf-8',
582
+ stdio: ['ignore', 'pipe', 'ignore'],
583
+ }).split(/\r?\n/).filter((entry) => Boolean(entry) && shouldCountEditedPath(entry))) {
584
+ touched.add(pathEntry);
585
+ }
586
+ return touched.size;
587
+ }
588
+ catch {
589
+ return 0;
590
+ }
591
+ }
592
+ function shouldCountEditedPath(relativePath) {
593
+ const normalized = relativePath.replace(/\\/g, '/');
594
+ return !normalized.startsWith('.brainclaw/') && !normalized.startsWith('.git/');
595
+ }
187
596
  //# sourceMappingURL=session-end.js.map