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,367 @@
1
+ import fs from 'node:fs';
2
+ import { AssignmentSchema } from './schema.js';
3
+ import { resolveEntityDir } from './io.js';
4
+ import { mutate } from './mutation-pipeline.js';
5
+ import { nowISO, generateIdWithLabel } from './ids.js';
6
+ import { JsonStore } from './json-store.js';
7
+ import { appendAuditEntry } from './audit.js';
8
+ import { appendEvent } from './event-log.js';
9
+ import { createRuntimeEvent } from './events.js';
10
+ import { findLatestAgentRunForAssignment, recordAgentRunProgress, syncAgentRunFromAssignmentTransition } from './agentruns.js';
11
+ // ── Directory / Store ────────────────────────────────────────
12
+ function assignmentsDir(cwd, mode = 'read') {
13
+ return resolveEntityDir('assignments', cwd, mode);
14
+ }
15
+ function ensureAssignmentsDir(cwd) {
16
+ const dir = assignmentsDir(cwd, 'write');
17
+ if (!fs.existsSync(dir)) {
18
+ fs.mkdirSync(dir, { recursive: true });
19
+ }
20
+ }
21
+ function assignmentStore(cwd) {
22
+ return new JsonStore({
23
+ dirPath: assignmentsDir(cwd, 'read'),
24
+ documentType: 'assignment',
25
+ getId: (a) => a.id,
26
+ sort: (a, b) => a.created_at.localeCompare(b.created_at),
27
+ });
28
+ }
29
+ // ── CRUD ─────────────────────────────────────────────────────
30
+ export function saveAssignment(assignment, cwd) {
31
+ mutate({ cwd }, () => {
32
+ ensureAssignmentsDir(cwd);
33
+ const store = new JsonStore({
34
+ dirPath: assignmentsDir(cwd, 'write'),
35
+ documentType: 'assignment',
36
+ getId: (a) => a.id,
37
+ sort: (a, b) => a.created_at.localeCompare(b.created_at),
38
+ });
39
+ store.save(AssignmentSchema.parse(assignment));
40
+ });
41
+ }
42
+ export function loadAssignment(id, cwd) {
43
+ // JsonStore.load throws when the id is missing; honor the declared
44
+ // "| undefined" return type so callers (e.g. transitionAssignment)
45
+ // can emit their own 'Assignment not found' error with the right wording.
46
+ try {
47
+ return assignmentStore(cwd).load(id);
48
+ }
49
+ catch {
50
+ return undefined;
51
+ }
52
+ }
53
+ export function listAssignments(cwd, filter) {
54
+ let items = assignmentStore(cwd).list();
55
+ if (filter?.status)
56
+ items = items.filter((a) => a.status === filter.status);
57
+ if (filter?.agent)
58
+ items = items.filter((a) => a.agent === filter.agent);
59
+ if (filter?.claim_id)
60
+ items = items.filter((a) => a.claim_id === filter.claim_id);
61
+ if (filter?.plan_id)
62
+ items = items.filter((a) => a.plan_id === filter.plan_id);
63
+ if (filter?.sequence_id)
64
+ items = items.filter((a) => a.sequence_id === filter.sequence_id);
65
+ return items;
66
+ }
67
+ // ── ID Generation ────────────────────────────────────────────
68
+ export function generateAssignmentId(cwd) {
69
+ return generateIdWithLabel('assignments', cwd);
70
+ }
71
+ // ── Status FSM ───────────────────────────────────────────────
72
+ /** Valid transitions: from → Set<to>.
73
+ *
74
+ * `rerouted` is reachable from every non-terminal state (pln#451 / trp#61):
75
+ * rerouting a claim must close the predecessor assignment regardless of where
76
+ * it was in the FSM. Previously only `blocked` could reach `rerouted`, which
77
+ * left assignments stuck in `created` or `offered` when the coordinator
78
+ * rerouted a still-unstarted lane.
79
+ */
80
+ const VALID_TRANSITIONS = new Map([
81
+ ['created', new Set(['offered', 'rerouted'])],
82
+ ['offered', new Set(['accepted', 'failed', 'expired', 'rerouted'])],
83
+ ['accepted', new Set(['started', 'timed_out', 'rerouted'])],
84
+ ['started', new Set(['completed', 'failed', 'blocked', 'timed_out', 'rerouted'])],
85
+ ['failed', new Set(['retrying', 'rerouted'])],
86
+ ['timed_out', new Set(['retrying', 'rerouted'])],
87
+ ['retrying', new Set(['offered', 'rerouted'])],
88
+ ['blocked', new Set(['rerouted', 'started', 'failed'])],
89
+ // Terminal: completed, expired, rerouted (no outgoing transitions)
90
+ ]);
91
+ export function validateTransition(from, to) {
92
+ const allowed = VALID_TRANSITIONS.get(from);
93
+ if (!allowed || !allowed.has(to)) {
94
+ return { valid: false, reason: `Invalid transition: ${from} → ${to}` };
95
+ }
96
+ return { valid: true };
97
+ }
98
+ /**
99
+ * Transition an assignment to a new status with FSM validation.
100
+ * Same-status transitions are idempotent no-ops (returns current state
101
+ * with idempotent=true instead of throwing). This handles network retries
102
+ * where a worker calls accepted/started again after a timeout.
103
+ * Updates relevant timestamps, emits event and audit entry.
104
+ */
105
+ export function transitionAssignment(id, newStatus, options, cwd) {
106
+ const assignment = loadAssignment(id, cwd);
107
+ if (!assignment) {
108
+ throw new Error(`Assignment not found: ${id}`);
109
+ }
110
+ // Idempotent: same-status transition is a no-op (handles network retries)
111
+ if (assignment.status === newStatus) {
112
+ // Still update heartbeat for liveness tracking
113
+ assignment.last_heartbeat_at = nowISO();
114
+ assignment.updated_at = assignment.last_heartbeat_at;
115
+ saveAssignment(assignment, cwd);
116
+ return { assignment, previous_status: newStatus, idempotent: true };
117
+ }
118
+ const validation = validateTransition(assignment.status, newStatus);
119
+ if (!validation.valid) {
120
+ throw new Error(validation.reason);
121
+ }
122
+ const previous_status = assignment.status;
123
+ const now = nowISO();
124
+ // Update status
125
+ assignment.status = newStatus;
126
+ assignment.updated_at = now;
127
+ assignment.last_heartbeat_at = now;
128
+ if (options.status_reason)
129
+ assignment.status_reason = options.status_reason;
130
+ if (options.session_id)
131
+ assignment.session_id = options.session_id;
132
+ if (options.error_message)
133
+ assignment.error_message = options.error_message;
134
+ if (options.artifacts?.length) {
135
+ assignment.artifacts = [...assignment.artifacts, ...options.artifacts];
136
+ }
137
+ // Set transition-specific timestamps
138
+ switch (newStatus) {
139
+ case 'offered':
140
+ assignment.offered_at = now;
141
+ break;
142
+ case 'accepted':
143
+ assignment.accepted_at = now;
144
+ break;
145
+ case 'started':
146
+ assignment.started_at = now;
147
+ break;
148
+ case 'completed':
149
+ assignment.completed_at = now;
150
+ break;
151
+ case 'failed':
152
+ assignment.failed_at = now;
153
+ break;
154
+ case 'blocked':
155
+ assignment.blocked_at = now;
156
+ break;
157
+ case 'timed_out':
158
+ assignment.timed_out_at = now;
159
+ break;
160
+ case 'expired':
161
+ assignment.expired_at = now;
162
+ break;
163
+ case 'rerouted':
164
+ assignment.rerouted_at = now;
165
+ break;
166
+ }
167
+ saveAssignment(assignment, cwd);
168
+ emitAssignmentEvent(assignment, `assignment_${newStatus}`, options.actor, cwd);
169
+ if (options.syncAgentRun !== false) {
170
+ try {
171
+ syncAgentRunFromAssignmentTransition(assignment, newStatus, {
172
+ actor: options.actor,
173
+ actor_id: options.actor_id,
174
+ session_id: options.session_id,
175
+ status_reason: options.status_reason,
176
+ artifacts: options.artifacts,
177
+ error_message: options.error_message,
178
+ }, cwd);
179
+ }
180
+ catch {
181
+ /* best-effort: run state should not break assignment lifecycle */
182
+ }
183
+ }
184
+ appendAuditEntry({
185
+ actor: options.actor ?? assignment.agent,
186
+ actor_id: options.actor_id,
187
+ action: 'update',
188
+ item_id: assignment.id,
189
+ item_type: 'assignment',
190
+ before: { status: previous_status },
191
+ after: { status: newStatus, reason: options.status_reason },
192
+ scope: assignment.scope,
193
+ session_id: options.session_id,
194
+ }, cwd);
195
+ return { assignment, previous_status };
196
+ }
197
+ /**
198
+ * Record progress on a started assignment (heartbeat).
199
+ * Updates last_heartbeat_at without changing status.
200
+ */
201
+ export function recordProgress(id, options, cwd) {
202
+ const assignment = loadAssignment(id, cwd);
203
+ if (!assignment) {
204
+ throw new Error(`Assignment not found: ${id}`);
205
+ }
206
+ if (assignment.status !== 'started') {
207
+ throw new Error(`Cannot record progress: assignment ${id} is ${assignment.status}, expected started`);
208
+ }
209
+ const now = nowISO();
210
+ assignment.last_heartbeat_at = now;
211
+ assignment.updated_at = now;
212
+ if (options.message)
213
+ assignment.status_reason = options.message;
214
+ if (options.artifacts?.length) {
215
+ assignment.artifacts = [...assignment.artifacts, ...options.artifacts];
216
+ }
217
+ saveAssignment(assignment, cwd);
218
+ emitAssignmentEvent(assignment, 'assignment_progress', options.actor, cwd);
219
+ try {
220
+ const latestRun = findLatestAgentRunForAssignment(assignment.id, cwd);
221
+ if (latestRun) {
222
+ recordAgentRunProgress(latestRun.id, {
223
+ message: options.message,
224
+ artifacts: options.artifacts,
225
+ actor: options.actor,
226
+ actor_id: options.actor_id,
227
+ session_id: options.session_id,
228
+ }, cwd);
229
+ }
230
+ }
231
+ catch {
232
+ /* best-effort */
233
+ }
234
+ return assignment;
235
+ }
236
+ /**
237
+ * Create a new assignment. Called by the dispatcher after creating a claim
238
+ * and sending an inbox message.
239
+ */
240
+ export function createAssignment(options, cwd) {
241
+ const generated = options.id ? undefined : generateAssignmentId(cwd);
242
+ const id = options.id ?? generated.id;
243
+ const short_label = options.short_label ?? generated.short_label;
244
+ const assignment = AssignmentSchema.parse({
245
+ schema_version: 1,
246
+ id,
247
+ short_label,
248
+ claim_id: options.claim_id,
249
+ message_id: options.message_id,
250
+ plan_id: options.plan_id,
251
+ sequence_id: options.sequence_id,
252
+ correlation_id: options.correlation_id,
253
+ agent: options.agent,
254
+ agent_id: options.agent_id,
255
+ dispatcher_agent: options.dispatcher_agent,
256
+ dispatcher_session_id: options.dispatcher_session_id,
257
+ scope: options.scope,
258
+ description: options.description,
259
+ lane: options.lane,
260
+ worktree_path: options.worktree_path,
261
+ status: 'created',
262
+ created_at: nowISO(),
263
+ heartbeat_ttl_ms: options.heartbeat_ttl_ms,
264
+ acceptance_ttl_ms: options.acceptance_ttl_ms,
265
+ max_retries: options.max_retries,
266
+ retry_count: 0,
267
+ artifacts: [],
268
+ tags: options.tags ?? [],
269
+ });
270
+ saveAssignment(assignment, cwd);
271
+ emitAssignmentEvent(assignment, 'assignment_created', options.dispatcher_agent, cwd);
272
+ appendAuditEntry({
273
+ actor: options.dispatcher_agent,
274
+ action: 'create',
275
+ item_id: assignment.id,
276
+ item_type: 'assignment',
277
+ after: { agent: options.agent, scope: options.scope, claim_id: options.claim_id },
278
+ }, cwd);
279
+ return assignment;
280
+ }
281
+ // ── Active Assignment Lookup ─────────────────────────────────
282
+ /** Statuses that indicate a finished assignment (no longer active). */
283
+ const TERMINAL_STATUSES = new Set(['completed', 'expired', 'rerouted']);
284
+ /**
285
+ * Return the most recently created non-terminal assignment for the given agent.
286
+ * When `claimId` is provided, it is used as a fast-path lookup before falling
287
+ * back to an agent-wide scan.
288
+ */
289
+ export function getActiveAssignmentForAgent(agentId, cwd, claimId) {
290
+ if (claimId) {
291
+ const byClaim = listAssignments(cwd, { claim_id: claimId });
292
+ const active = byClaim.filter((a) => !TERMINAL_STATUSES.has(a.status));
293
+ // listAssignments returns ascending-by-created_at — last is most recent
294
+ if (active.length > 0)
295
+ return active[active.length - 1];
296
+ }
297
+ if (!agentId)
298
+ return undefined;
299
+ const all = listAssignments(cwd);
300
+ const active = all.filter((a) => a.agent_id === agentId && !TERMINAL_STATUSES.has(a.status));
301
+ return active.length > 0 ? active[active.length - 1] : undefined;
302
+ }
303
+ /**
304
+ * Bump `last_heartbeat_at` on the most recent active assignment for the given
305
+ * claim (or agent). Best-effort — throws are suppressed by the caller.
306
+ */
307
+ export function bumpActiveAssignmentHeartbeat(claimId, agentId, cwd) {
308
+ const assignment = getActiveAssignmentForAgent(agentId ?? '', cwd, claimId);
309
+ if (!assignment)
310
+ return false;
311
+ const now = nowISO();
312
+ assignment.last_heartbeat_at = now;
313
+ assignment.updated_at = now;
314
+ saveAssignment(assignment, cwd);
315
+ return true;
316
+ }
317
+ // ── Post-creation patches ────────────────────────────────────
318
+ /** Attach the inbox message_id after the message has been sent (message is created after assignment). */
319
+ export function patchAssignmentMessageId(id, messageId, cwd) {
320
+ const assignment = loadAssignment(id, cwd);
321
+ if (!assignment)
322
+ return;
323
+ assignment.message_id = messageId;
324
+ saveAssignment(assignment, cwd);
325
+ }
326
+ // ── Event Emission ───────────────────────────────────────────
327
+ function emitAssignmentEvent(assignment, action, actor, cwd) {
328
+ const text = `${assignment.description} [${assignment.status}]${assignment.status_reason ? ` — ${assignment.status_reason}` : ''}`;
329
+ appendEvent({
330
+ ts: nowISO(),
331
+ agent: actor ?? assignment.agent,
332
+ agent_id: assignment.agent_id,
333
+ action: action,
334
+ item_type: 'assignment',
335
+ item_id: assignment.id,
336
+ summary: `${assignment.status}: ${assignment.description.slice(0, 80)}`,
337
+ }, cwd);
338
+ try {
339
+ createRuntimeEvent({
340
+ agent: actor ?? assignment.agent,
341
+ agent_id: assignment.agent_id,
342
+ project_id: undefined,
343
+ session_id: assignment.session_id,
344
+ event_type: action,
345
+ text,
346
+ tags: ['agent-runtime', 'assignment'],
347
+ assignment_id: assignment.id,
348
+ claim_id: assignment.claim_id,
349
+ message_id: assignment.message_id,
350
+ plan_id: assignment.plan_id,
351
+ sequence_id: assignment.sequence_id,
352
+ correlation_id: assignment.correlation_id,
353
+ scope: assignment.scope,
354
+ status: assignment.status,
355
+ status_reason: assignment.status_reason,
356
+ related_paths: [assignment.scope],
357
+ metadata: {
358
+ dispatcher_agent: assignment.dispatcher_agent,
359
+ protocol: 'brainclaw.agent_runtime.v0',
360
+ },
361
+ }, cwd);
362
+ }
363
+ catch {
364
+ /* best-effort: runtime event emission should not break assignment lifecycle */
365
+ }
366
+ }
367
+ //# sourceMappingURL=assignments.js.map
@@ -6,6 +6,7 @@ import { nowISO } from './ids.js';
6
6
  import { logger } from './logger.js';
7
7
  import { appendEvent } from './event-log.js';
8
8
  const AUDIT_LOG_FILE = 'audit.log';
9
+ const MAX_AUDIT_LOG_BYTES = 10 * 1024 * 1024; // 10MB
9
10
  /** Map audit actions to event-log actions (subset that overlaps) */
10
11
  const AUDIT_TO_EVENT_ACTION = {
11
12
  create: 'create',
@@ -18,6 +19,8 @@ const AUDIT_TO_EVENT_ACTION = {
18
19
  session_start: 'session_start',
19
20
  session_end: 'session_end',
20
21
  rollback: 'rollback',
22
+ promote_direct: 'create',
23
+ trust_change: 'update',
21
24
  };
22
25
  function auditLogPath(cwd) {
23
26
  return path.join(memoryDir(cwd), AUDIT_LOG_FILE);
@@ -25,6 +28,7 @@ function auditLogPath(cwd) {
25
28
  export function appendAuditEntry(entry, cwd) {
26
29
  try {
27
30
  mutate({ cwd }, () => {
31
+ rotateAuditLogIfNeeded(cwd);
28
32
  const full = {
29
33
  timestamp: nowISO(),
30
34
  actor: entry.actor,
@@ -35,6 +39,10 @@ export function appendAuditEntry(entry, cwd) {
35
39
  after: entry.after,
36
40
  actor_id: entry.actor_id,
37
41
  reason: entry.reason,
42
+ scope: entry.scope,
43
+ session_id: entry.session_id,
44
+ host_id: entry.host_id,
45
+ actor_session: entry.actor_session,
38
46
  };
39
47
  const line = JSON.stringify(Object.fromEntries(Object.entries(full).filter(([, v]) => v !== undefined)));
40
48
  fs.appendFileSync(auditLogPath(cwd), line + '\n', 'utf-8');
@@ -83,4 +91,26 @@ export function readAuditLog(options = {}, cwd) {
83
91
  }
84
92
  return entries;
85
93
  }
94
+ /**
95
+ * Rotate audit.log if it exceeds MAX_AUDIT_LOG_BYTES.
96
+ * Archives to audit.{timestamp}.log in the same directory.
97
+ */
98
+ export function rotateAuditLogIfNeeded(cwd) {
99
+ const logPath = auditLogPath(cwd);
100
+ if (!fs.existsSync(logPath))
101
+ return false;
102
+ try {
103
+ const stat = fs.statSync(logPath);
104
+ if (stat.size < MAX_AUDIT_LOG_BYTES)
105
+ return false;
106
+ const archiveName = `audit.${Date.now()}.log`;
107
+ const archivePath = path.join(memoryDir(cwd), archiveName);
108
+ fs.renameSync(logPath, archivePath);
109
+ return true;
110
+ }
111
+ catch (err) {
112
+ logger.debug('Failed to rotate audit log:', err);
113
+ return false;
114
+ }
115
+ }
86
116
  //# sourceMappingURL=audit.js.map
@@ -3,7 +3,7 @@ import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import { spawnSync } from 'node:child_process';
5
5
  import { fileURLToPath } from 'node:url';
6
- import { BrainclawLocalReleaseManifestSchema, } from './schema.js';
6
+ import { AgentReleaseNotesSchema, BrainclawLocalReleaseManifestSchema, } from './schema.js';
7
7
  export const DEFAULT_LOCAL_RELEASES_DIR = '.releases';
8
8
  export const DEFAULT_LOCAL_RELEASE_MANIFEST_PATH = `${DEFAULT_LOCAL_RELEASES_DIR}/brainclaw-local.json`;
9
9
  export const DEFAULT_NPM_UPDATE_PACKAGE = 'brainclaw';
@@ -15,7 +15,75 @@ const DEFAULT_NPM_UPDATE_SOURCE = {
15
15
  package_name: DEFAULT_NPM_UPDATE_PACKAGE,
16
16
  dist_tag: DEFAULT_NPM_UPDATE_DIST_TAG,
17
17
  };
18
+ /**
19
+ * Scan PATH for all brainclaw installations and their versions.
20
+ * Detects when multiple versions are installed (e.g. global + user-local)
21
+ * which causes confusion about which version is actually running.
22
+ */
23
+ export function detectConcurrentInstallations() {
24
+ const currentVersion = getInstalledBrainclawVersion();
25
+ const installations = [];
26
+ const seen = new Set();
27
+ // On Windows, `where` returns all matches. On Unix, `which -a` does.
28
+ const isWindows = os.platform() === 'win32';
29
+ const result = isWindows
30
+ ? spawnSync('where', ['brainclaw'], { encoding: 'utf-8', timeout: 5000 })
31
+ : spawnSync('which', ['-a', 'brainclaw'], { encoding: 'utf-8', timeout: 5000 });
32
+ if (result.status !== 0)
33
+ return [];
34
+ const paths = result.stdout.split(/\r?\n/).map(l => l.trim()).filter(Boolean);
35
+ for (const binPath of paths) {
36
+ // Normalize to avoid duplicates from symlinks
37
+ let realPath;
38
+ try {
39
+ realPath = isWindows ? binPath : fs.realpathSync(binPath);
40
+ }
41
+ catch {
42
+ realPath = binPath;
43
+ }
44
+ if (seen.has(realPath))
45
+ continue;
46
+ seen.add(realPath);
47
+ // Get version by running the binary
48
+ let version = 'unknown';
49
+ try {
50
+ const vResult = spawnSync(binPath, ['--version'], { encoding: 'utf-8', timeout: 5000 });
51
+ if (vResult.status === 0) {
52
+ version = vResult.stdout.trim().split(/\r?\n/)[0]?.trim() ?? 'unknown';
53
+ }
54
+ }
55
+ catch { /* ignore */ }
56
+ installations.push({
57
+ path: binPath,
58
+ version,
59
+ isCurrent: version === currentVersion,
60
+ });
61
+ }
62
+ return installations;
63
+ }
18
64
  let cachedCliVersion;
65
+ let cachedPackageJsonPath;
66
+ /**
67
+ * Read the brainclaw version from disk (package.json), bypassing the in-memory cache.
68
+ * Used by the MCP server to detect when a new version has been installed while the
69
+ * long-running MCP process is still running with old code.
70
+ */
71
+ export function readDiskBrainclawVersion() {
72
+ if (cachedPackageJsonPath === undefined) {
73
+ cachedPackageJsonPath = findOwnPackageJson() ?? '';
74
+ }
75
+ if (!cachedPackageJsonPath)
76
+ return '0.0.0';
77
+ try {
78
+ const parsed = JSON.parse(fs.readFileSync(cachedPackageJsonPath, 'utf-8'));
79
+ return typeof parsed.version === 'string' && parsed.version.trim().length > 0
80
+ ? parsed.version.trim()
81
+ : '0.0.0';
82
+ }
83
+ catch {
84
+ return '0.0.0';
85
+ }
86
+ }
19
87
  export function getInstalledBrainclawVersion() {
20
88
  if (cachedCliVersion) {
21
89
  return cachedCliVersion;
@@ -126,7 +194,23 @@ export function renderBrainclawInstallableUpdateNotice(updateCheck) {
126
194
  if (updateCheck.install_command) {
127
195
  lines.push(`Install: ${updateCheck.install_command}`);
128
196
  }
129
- if (updateCheck.release_notes) {
197
+ const arn = updateCheck.agent_release_notes;
198
+ if (arn) {
199
+ lines.push(`Summary: ${arn.summary}`);
200
+ if (arn.breaking_risk && arn.breaking_risk !== 'none') {
201
+ lines.push(`Breaking risk: ${arn.breaking_risk}`);
202
+ }
203
+ if (arn.highlights && arn.highlights.length > 0) {
204
+ lines.push('Highlights:');
205
+ for (const h of arn.highlights) {
206
+ lines.push(` • ${h}`);
207
+ }
208
+ }
209
+ if (arn.action_recommendation) {
210
+ lines.push(`Action: ${arn.action_recommendation}`);
211
+ }
212
+ }
213
+ else if (updateCheck.release_notes) {
130
214
  lines.push(`Why update: ${updateCheck.release_notes}`);
131
215
  }
132
216
  return lines.join('\n');
@@ -161,6 +245,9 @@ export function publishLocalBrainclawRelease(cwd, options = {}) {
161
245
  const projectManifestPath = toPortablePath(path.relative(cwd, manifestPath));
162
246
  const installCommand = `npm install -g "${projectArtifactPath}"`;
163
247
  const releaseNotes = options.releaseNotes?.trim() || null;
248
+ const agentReleaseNotes = options.agentReleaseNotes
249
+ ? AgentReleaseNotesSchema.parse(options.agentReleaseNotes)
250
+ : null;
164
251
  const manifest = BrainclawLocalReleaseManifestSchema.parse({
165
252
  version: 1,
166
253
  channel: 'local-pack',
@@ -170,6 +257,7 @@ export function publishLocalBrainclawRelease(cwd, options = {}) {
170
257
  artifact_path: manifestArtifactPath,
171
258
  install_command: installCommand,
172
259
  release_notes: releaseNotes ?? undefined,
260
+ agent_release_notes: agentReleaseNotes ?? undefined,
173
261
  });
174
262
  fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf-8');
175
263
  return {
@@ -179,6 +267,7 @@ export function publishLocalBrainclawRelease(cwd, options = {}) {
179
267
  artifact_path: projectArtifactPath,
180
268
  install_command: installCommand,
181
269
  release_notes: releaseNotes,
270
+ agent_release_notes: agentReleaseNotes,
182
271
  };
183
272
  }
184
273
  function checkNpmInstallableUpdate(source, config, cwd, options, defaultSource) {
@@ -343,6 +432,7 @@ function checkLocalPackInstallableUpdate(manifestPath, config, cwd) {
343
432
  const installCommand = manifest.install_command?.trim()
344
433
  || (artifactPath ? `npm install -g "${artifactPath}"` : config?.brainclaw_upgrade_command?.trim() || null);
345
434
  const releaseNotes = manifest.release_notes?.trim() || config?.brainclaw_upgrade_message?.trim() || null;
435
+ const agentReleaseNotes = manifest.agent_release_notes ?? null;
346
436
  const installedVersion = getInstalledBrainclawVersion();
347
437
  if (compareVersions(installedVersion, latestVersion) < 0) {
348
438
  return {
@@ -353,6 +443,7 @@ function checkLocalPackInstallableUpdate(manifestPath, config, cwd) {
353
443
  artifact_path: artifactPath,
354
444
  install_command: installCommand,
355
445
  release_notes: releaseNotes,
446
+ agent_release_notes: agentReleaseNotes,
356
447
  status: 'update_available',
357
448
  message: `A newer installable brainclaw build is available: ${latestVersion} (installed ${installedVersion}).`,
358
449
  default_source: false,
@@ -366,6 +457,7 @@ function checkLocalPackInstallableUpdate(manifestPath, config, cwd) {
366
457
  artifact_path: artifactPath,
367
458
  install_command: installCommand,
368
459
  release_notes: releaseNotes,
460
+ agent_release_notes: agentReleaseNotes,
369
461
  status: 'up_to_date',
370
462
  message: `Installed brainclaw ${installedVersion} is up to date for the configured local-pack channel.`,
371
463
  default_source: false,