brainclaw 0.28.0 → 1.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. package/README.md +193 -170
  2. package/dist/brainclaw-vscode.vsix +0 -0
  3. package/dist/cli.js +683 -23
  4. package/dist/commands/accept.js +3 -0
  5. package/dist/commands/add-step.js +11 -26
  6. package/dist/commands/agent-board.js +70 -3
  7. package/dist/commands/audit.js +19 -0
  8. package/dist/commands/check-policy.js +54 -0
  9. package/dist/commands/check-security-mcp.js +145 -0
  10. package/dist/commands/check-security.js +106 -0
  11. package/dist/commands/claim-resource.js +1 -0
  12. package/dist/commands/codev.js +672 -0
  13. package/dist/commands/compact.js +74 -0
  14. package/dist/commands/complete-step.js +16 -26
  15. package/dist/commands/constraint.js +8 -20
  16. package/dist/commands/decision.js +9 -20
  17. package/dist/commands/delete-plan.js +10 -12
  18. package/dist/commands/delete-step.js +16 -0
  19. package/dist/commands/dispatch.js +163 -0
  20. package/dist/commands/doctor.js +1122 -49
  21. package/dist/commands/enable-agent.js +1 -0
  22. package/dist/commands/export.js +280 -22
  23. package/dist/commands/handoff.js +33 -0
  24. package/dist/commands/harvest.js +189 -0
  25. package/dist/commands/hooks.js +82 -25
  26. package/dist/commands/inbox.js +169 -0
  27. package/dist/commands/init.js +38 -31
  28. package/dist/commands/install-hooks.js +71 -44
  29. package/dist/commands/link.js +89 -0
  30. package/dist/commands/list-claims.js +48 -3
  31. package/dist/commands/list-plans.js +129 -25
  32. package/dist/commands/loops-handlers.js +409 -0
  33. package/dist/commands/mcp-read-handlers.js +1628 -0
  34. package/dist/commands/mcp-schemas.generated.js +74 -0
  35. package/dist/commands/mcp.js +4244 -1475
  36. package/dist/commands/plan-resource.js +64 -0
  37. package/dist/commands/plan.js +12 -26
  38. package/dist/commands/prune.js +37 -2
  39. package/dist/commands/reflect.js +20 -7
  40. package/dist/commands/release-claim.js +11 -6
  41. package/dist/commands/release-notes.js +170 -0
  42. package/dist/commands/repair.js +210 -0
  43. package/dist/commands/run-profile.js +57 -0
  44. package/dist/commands/sequence.js +113 -0
  45. package/dist/commands/session-end.js +423 -14
  46. package/dist/commands/session-start.js +214 -41
  47. package/dist/commands/setup-security.js +103 -0
  48. package/dist/commands/setup.js +42 -4
  49. package/dist/commands/stale.js +109 -0
  50. package/dist/commands/switch.js +131 -10
  51. package/dist/commands/trap.js +14 -31
  52. package/dist/commands/update-handoff.js +63 -4
  53. package/dist/commands/update-plan.js +21 -28
  54. package/dist/commands/update-step.js +37 -0
  55. package/dist/commands/upgrade.js +313 -6
  56. package/dist/commands/usage.js +102 -0
  57. package/dist/commands/version.js +20 -0
  58. package/dist/commands/who.js +124 -0
  59. package/dist/commands/worktree.js +105 -0
  60. package/dist/core/actions.js +315 -0
  61. package/dist/core/agent-capability.js +610 -17
  62. package/dist/core/agent-context.js +7 -1
  63. package/dist/core/agent-files.js +1169 -85
  64. package/dist/core/agent-integrations.js +160 -5
  65. package/dist/core/agent-inventory.js +2 -0
  66. package/dist/core/agent-profiles.js +93 -0
  67. package/dist/core/agent-registry.js +162 -30
  68. package/dist/core/agentrun-reconciler.js +345 -0
  69. package/dist/core/agentruns.js +424 -0
  70. package/dist/core/ai-agent-detection.js +31 -10
  71. package/dist/core/archival.js +77 -0
  72. package/dist/core/assignment-sweeper.js +82 -0
  73. package/dist/core/assignments.js +367 -0
  74. package/dist/core/audit.js +30 -0
  75. package/dist/core/bootstrap.js +61 -10
  76. package/dist/core/brainclaw-version.js +94 -2
  77. package/dist/core/candidates.js +93 -2
  78. package/dist/core/claims.js +419 -0
  79. package/dist/core/codev-metrics.js +77 -0
  80. package/dist/core/codev-personas.js +31 -0
  81. package/dist/core/codev-plan-gen.js +35 -0
  82. package/dist/core/codev-prompts.js +74 -0
  83. package/dist/core/codev-responses.js +62 -0
  84. package/dist/core/codev-rounds.js +218 -0
  85. package/dist/core/config.js +4 -0
  86. package/dist/core/context.js +454 -34
  87. package/dist/core/coordination.js +201 -6
  88. package/dist/core/cross-project.js +230 -16
  89. package/dist/core/default-profiles/doctor.yaml +11 -0
  90. package/dist/core/default-profiles/janitor.yaml +11 -0
  91. package/dist/core/default-profiles/onboarder.yaml +11 -0
  92. package/dist/core/default-profiles/reviewer.yaml +13 -0
  93. package/dist/core/dispatcher.js +1189 -0
  94. package/dist/core/duplicates.js +2 -2
  95. package/dist/core/entity-operations.js +450 -0
  96. package/dist/core/entity-registry.js +344 -0
  97. package/dist/core/event-log.js +1 -0
  98. package/dist/core/events.js +106 -2
  99. package/dist/core/execution-adapters.js +154 -0
  100. package/dist/core/execution-context.js +63 -0
  101. package/dist/core/execution-profile.js +270 -0
  102. package/dist/core/execution.js +255 -0
  103. package/dist/core/facade-schema.js +81 -0
  104. package/dist/core/federation-cloud.js +99 -0
  105. package/dist/core/federation-message.js +52 -0
  106. package/dist/core/federation-transport.js +65 -0
  107. package/dist/core/gc-semantic.js +482 -0
  108. package/dist/core/governance.js +247 -0
  109. package/dist/core/guards.js +19 -0
  110. package/dist/core/ideation.js +72 -0
  111. package/dist/core/identity.js +252 -28
  112. package/dist/core/ids.js +6 -0
  113. package/dist/core/input-validation.js +2 -2
  114. package/dist/core/instruction-templates.js +344 -136
  115. package/dist/core/io.js +90 -11
  116. package/dist/core/lock.js +6 -2
  117. package/dist/core/loops/brief-assembly.js +213 -0
  118. package/dist/core/loops/facade-schema.js +148 -0
  119. package/dist/core/loops/index.js +7 -0
  120. package/dist/core/loops/iteration-engine.js +139 -0
  121. package/dist/core/loops/lock.js +385 -0
  122. package/dist/core/loops/store.js +201 -0
  123. package/dist/core/loops/types.js +403 -0
  124. package/dist/core/loops/verbs.js +534 -0
  125. package/dist/core/markdown.js +15 -3
  126. package/dist/core/memory-compactor.js +432 -0
  127. package/dist/core/memory-git.js +152 -8
  128. package/dist/core/messaging.js +278 -0
  129. package/dist/core/migration.js +32 -1
  130. package/dist/core/mutation-pipeline.js +4 -2
  131. package/dist/core/operations/memory-mutation.js +129 -0
  132. package/dist/core/operations/memory-write.js +78 -0
  133. package/dist/core/operations/plan.js +190 -0
  134. package/dist/core/policy.js +169 -0
  135. package/dist/core/repo-analysis.js +67 -0
  136. package/dist/core/reputation.js +9 -3
  137. package/dist/core/schema.js +546 -21
  138. package/dist/core/search.js +21 -2
  139. package/dist/core/security-cache.js +71 -0
  140. package/dist/core/security-guard.js +152 -0
  141. package/dist/core/security-scoring.js +86 -0
  142. package/dist/core/sequence.js +130 -0
  143. package/dist/core/socket-client.js +113 -0
  144. package/dist/core/staleness.js +246 -0
  145. package/dist/core/state.js +98 -22
  146. package/dist/core/store-resolution.js +54 -12
  147. package/dist/core/toml-writer.js +76 -0
  148. package/dist/core/upgrades/backup.js +232 -0
  149. package/dist/core/upgrades/health-check.js +169 -0
  150. package/dist/core/upgrades/patches/candidate-archive.js +145 -0
  151. package/dist/core/upgrades/patches/handoff-review-strip.js +128 -0
  152. package/dist/core/upgrades/patches/provenance-rollout.js +136 -0
  153. package/dist/core/upgrades/schema-version.js +97 -0
  154. package/dist/core/worktree.js +606 -0
  155. package/dist/facts.js +114 -0
  156. package/dist/facts.json +111 -0
  157. package/docs/architecture/project-refs.md +5 -1
  158. package/docs/cli.md +690 -43
  159. package/docs/concepts/ideation-loop.md +317 -0
  160. package/docs/concepts/loop-engine.md +456 -0
  161. package/docs/concepts/mcp-governance.md +268 -0
  162. package/docs/concepts/memory-staleness.md +122 -0
  163. package/docs/concepts/multi-agent-workflows.md +166 -0
  164. package/docs/concepts/plans-and-claims.md +31 -6
  165. package/docs/concepts/project-md-convention.md +35 -0
  166. package/docs/concepts/troubleshooting.md +220 -0
  167. package/docs/concepts/upgrade-cli.md +202 -0
  168. package/docs/concepts/upgrade-dogfood-procedure.md +114 -0
  169. package/docs/context-format-changelog.md +2 -2
  170. package/docs/context-format.md +2 -2
  171. package/docs/index.md +68 -0
  172. package/docs/integrations/agents.md +15 -16
  173. package/docs/integrations/cline.md +88 -0
  174. package/docs/integrations/codex.md +75 -23
  175. package/docs/integrations/continue.md +60 -0
  176. package/docs/integrations/copilot.md +67 -9
  177. package/docs/integrations/kilocode.md +72 -0
  178. package/docs/integrations/mcp.md +304 -21
  179. package/docs/integrations/mistral-vibe.md +122 -0
  180. package/docs/integrations/opencode.md +84 -0
  181. package/docs/integrations/overview.md +23 -8
  182. package/docs/integrations/roo.md +74 -0
  183. package/docs/integrations/windsurf.md +83 -0
  184. package/docs/mcp-schema-changelog.md +191 -1
  185. package/docs/playbooks/integration/index.md +121 -0
  186. package/docs/playbooks/productivity/index.md +102 -0
  187. package/docs/playbooks/team/index.md +122 -0
  188. package/docs/product/agent-first-model.md +184 -0
  189. package/docs/product/entity-model-audit.md +462 -0
  190. package/docs/quickstart-existing-project.md +135 -0
  191. package/docs/quickstart.md +124 -37
  192. package/docs/release-maintenance.md +79 -0
  193. package/docs/review.md +2 -0
  194. package/docs/server-operations.md +118 -0
  195. package/package.json +20 -12
  196. package/dist/commands/claude-desktop-extension.js +0 -18
  197. package/dist/commands/diff.js +0 -99
  198. package/dist/core/claude-desktop-extension.js +0 -224
@@ -0,0 +1,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
@@ -8,7 +8,7 @@ import { resolveEntityDir } from './io.js';
8
8
  import { mutate } from './mutation-pipeline.js';
9
9
  import { BootstrapApplicationReceiptSchema, BootstrapInterviewAnswerSchema, BootstrapInterviewPlanSchema, BootstrapInterviewQuestionSchema, BootstrapImportPlanDocumentSchema, BootstrapProfileDocumentSchema, BootstrapSuggestionDocumentSchema, MemorySeedDocumentSchema, } from './schema.js';
10
10
  import { loadVersionedJsonFile, saveVersionedJsonFile } from './migration.js';
11
- import { analyzeRepository } from './repo-analysis.js';
11
+ import { analyzeRepository, findNestedAgentsFiles } from './repo-analysis.js';
12
12
  import { buildExecutionContext, compactExecutionContext } from './execution-context.js';
13
13
  import { buildAgentToolingContext } from './agent-context.js';
14
14
  import { createInstruction, loadInstructions, saveInstruction } from './instructions.js';
@@ -101,6 +101,7 @@ export function runBootstrapProfile(options = {}) {
101
101
  importPlan,
102
102
  lastApplication,
103
103
  reusedProfile: false,
104
+ subProjects: artifacts.subProjects,
104
105
  };
105
106
  }
106
107
  export function listBootstrapSeeds(cwd) {
@@ -183,6 +184,18 @@ export function renderBootstrapSummary(result) {
183
184
  if (result.lastApplication && !result.lastApplication.uninstalled_at) {
184
185
  lines.push(`Last bootstrap import: ${result.lastApplication.managed_artifacts.length} managed artifact(s) from ${result.lastApplication.applied_at}`);
185
186
  }
187
+ if (result.subProjects && result.subProjects.length > 0) {
188
+ lines.push('');
189
+ lines.push(`Sub-projects discovered (${result.subProjects.length}):`);
190
+ for (const sp of result.subProjects.slice(0, 20)) {
191
+ lines.push(` ${sp}`);
192
+ }
193
+ if (result.subProjects.length > 20) {
194
+ lines.push(` ... and ${result.subProjects.length - 20} more`);
195
+ }
196
+ lines.push('');
197
+ lines.push('Use: brainclaw bootstrap --for <sub-project-path> --refresh');
198
+ }
186
199
  if (result.importPlan.suggestions.length > 0) {
187
200
  lines.push('');
188
201
  lines.push('Import proposal:');
@@ -231,14 +244,19 @@ export function renderBootstrapInterview(result, audience = 'any') {
231
244
  function buildBootstrapArtifacts(input) {
232
245
  const sourcesScanned = [];
233
246
  const seeds = [];
234
- const workspace = classifyWorkspace(input.cwd);
235
- const nativeInstructionFiles = discoverNativeInstructionFiles(input.cwd);
236
- const readmePath = findFirstExisting(input.cwd, README_CANDIDATES);
247
+ // When target is an absolute directory path, use it as the scan root
248
+ // so that instruction files, README, and AGENTS.md are discovered from
249
+ // the target scope — not from the workspace root. This fixes bootstrap
250
+ // returning wrong signals for monorepo sub-projects.
251
+ const scanRoot = resolveBootstrapScanRoot(input.cwd, input.target);
252
+ const workspace = classifyWorkspace(scanRoot);
253
+ const nativeInstructionFiles = discoverNativeInstructionFiles(scanRoot);
254
+ const readmePath = findFirstExisting(scanRoot, README_CANDIDATES);
237
255
  if (readmePath) {
238
256
  sourcesScanned.push('README');
239
257
  seeds.push(...extractReadmeSeeds(readmePath, input.target));
240
258
  }
241
- const agentsPath = path.join(input.cwd, 'AGENTS.md');
259
+ const agentsPath = path.join(scanRoot, 'AGENTS.md');
242
260
  const agentsPresent = fs.existsSync(agentsPath);
243
261
  if (agentsPresent) {
244
262
  sourcesScanned.push('AGENTS.md');
@@ -246,9 +264,9 @@ function buildBootstrapArtifacts(input) {
246
264
  }
247
265
  if (nativeInstructionFiles.length > 0) {
248
266
  sourcesScanned.push('native_instructions');
249
- seeds.push(...extractNativeInstructionSeeds(nativeInstructionFiles.map((relativePath) => path.join(input.cwd, relativePath)), input.cwd, input.target));
267
+ seeds.push(...extractNativeInstructionSeeds(nativeInstructionFiles.map((relativePath) => path.join(scanRoot, relativePath)), scanRoot, input.target));
250
268
  }
251
- const manifestResult = extractManifestSeeds(input.cwd, input.target);
269
+ const manifestResult = extractManifestSeeds(scanRoot, input.target);
252
270
  if (manifestResult.seeds.length > 0) {
253
271
  sourcesScanned.push(...manifestResult.sources);
254
272
  seeds.push(...manifestResult.seeds);
@@ -265,16 +283,16 @@ function buildBootstrapArtifacts(input) {
265
283
  sourcesScanned.push('local_mcp');
266
284
  seeds.push(...extractMcpSeeds(agentTooling.mcp_servers, input.target));
267
285
  }
268
- const repoAnalysis = analyzeRepository(input.cwd);
286
+ const repoAnalysis = analyzeRepository(scanRoot);
269
287
  sourcesScanned.push('repo-analysis');
270
288
  seeds.push(...extractRepoAnalysisSeeds(repoAnalysis, input.target));
271
289
  // Additional brownfield sources (step 12)
272
- const additionalSeeds = extractAdditionalBrownfieldSeeds(input.cwd, input.target);
290
+ const additionalSeeds = extractAdditionalBrownfieldSeeds(scanRoot, input.target);
273
291
  if (additionalSeeds.seeds.length > 0) {
274
292
  sourcesScanned.push(...additionalSeeds.sources);
275
293
  seeds.push(...additionalSeeds.seeds);
276
294
  }
277
- const gitProbe = probeGit(input.cwd, input.target);
295
+ const gitProbe = probeGit(scanRoot, input.target);
278
296
  if (gitProbe.available) {
279
297
  sourcesScanned.push('git');
280
298
  seeds.push(...gitProbe.hotspotSeeds);
@@ -342,6 +360,13 @@ function buildBootstrapArtifacts(input) {
342
360
  schema_version: DERIVED_SCHEMA_VERSION,
343
361
  })),
344
362
  importPlan,
363
+ // For multi-project workspaces without a target, list discovered sub-projects
364
+ subProjects: (!input.target && repoAnalysis.recommendedMode === 'multi-project')
365
+ ? findNestedAgentsFiles(scanRoot, 8)
366
+ .filter((p) => p !== 'AGENTS.md') // exclude root AGENTS.md
367
+ .map((p) => path.dirname(p))
368
+ .filter((d) => d !== '.')
369
+ : undefined,
345
370
  };
346
371
  }
347
372
  function extractReadmeSeeds(filepath, target) {
@@ -846,6 +871,9 @@ function buildSummary(input) {
846
871
  parts.push(`Onboarding mode: ${input.onboardingMode}.`);
847
872
  parts.push(`Confidence: ${input.confidence}.`);
848
873
  parts.push(`Repository mode looks ${input.repoAnalysis.recommendedMode}.`);
874
+ if (input.repoAnalysis.recommendedMode === 'multi-project' && !input.target) {
875
+ parts.push('This is a multi-project workspace — use --for <path> to bootstrap a specific sub-project.');
876
+ }
849
877
  if (input.agentsPresent) {
850
878
  parts.push('AGENTS.md detected and summarized.');
851
879
  }
@@ -1639,6 +1667,29 @@ function normalizeTarget(target) {
1639
1667
  const trimmed = target?.trim();
1640
1668
  return trimmed && trimmed.length > 0 ? trimmed : undefined;
1641
1669
  }
1670
+ /**
1671
+ * Resolve where to scan for project files (README, AGENTS.md, manifests).
1672
+ * If target is an absolute directory path, scan from there.
1673
+ * Otherwise fall back to cwd (the workspace/store root).
1674
+ */
1675
+ function resolveBootstrapScanRoot(cwd, target) {
1676
+ if (!target)
1677
+ return cwd;
1678
+ const resolved = path.isAbsolute(target) ? target : path.resolve(cwd, target);
1679
+ try {
1680
+ if (fs.statSync(resolved).isDirectory())
1681
+ return resolved;
1682
+ }
1683
+ catch { /* not a directory or doesn't exist */ }
1684
+ // Target is a file path or glob — use its parent directory if it exists
1685
+ const parent = path.dirname(resolved);
1686
+ try {
1687
+ if (fs.statSync(parent).isDirectory())
1688
+ return parent;
1689
+ }
1690
+ catch { /* fall back to cwd */ }
1691
+ return cwd;
1692
+ }
1642
1693
  // ─── Step 12: Additional brownfield sources ──────────────────────────────────
1643
1694
  const CI_WORKFLOW_DIRS = ['.github/workflows', '.gitlab'];
1644
1695
  const CI_FILES = ['.gitlab-ci.yml', 'Jenkinsfile', '.circleci/config.yml'];