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,21 +1,32 @@
1
- import { findAgentIdentityByName, resolveAgentScope, resolveCurrentAgentIdentity } from './agent-registry.js';
1
+ import { findAgentIdentityByName, listAgentIdentities, resolveAgentScope, resolveCurrentAgentIdentity } from './agent-registry.js';
2
2
  import { loadConfig } from './config.js';
3
3
  import { resolveCurrentHostId } from './host.js';
4
4
  import { listClaims } from './claims.js';
5
+ import { listAssignments } from './assignments.js';
6
+ import { listAgentRuns } from './agentruns.js';
7
+ import { listActionRequired } from './actions.js';
8
+ import { getActiveSequence } from './sequence.js';
9
+ import { resolveCrossProjectLinks, listIncomingCrossProjectSignals } from './cross-project.js';
5
10
  import { inferProjectFromTarget, loadInstructions, resolveInstructions } from './instructions.js';
6
11
  import { buildReputationSummary, findAgentReputationSummary } from './reputation.js';
7
12
  import { listRuntimeNotes } from './runtime.js';
8
13
  import { loadState, persistState } from './state.js';
14
+ import { getCapabilityProfile } from './agent-capability.js';
15
+ import { loadAllSessions } from './identity.js';
16
+ import { countActionable } from './messaging.js';
17
+ import { listCandidates } from './candidates.js';
18
+ import { pullSignalsFromLinkedProjects } from './federation-transport.js';
9
19
  export function buildCoordinationSnapshot(options = {}) {
10
20
  const config = loadConfig(options.cwd);
11
21
  const state = loadState(options.cwd);
12
22
  const currentHost = resolveCurrentHostId();
13
23
  const project = options.project ?? inferProjectFromTarget(options.target, config);
14
- const agent = resolveAgentScope(options.agent, options.cwd);
24
+ const agent = options.skipAgentAutoDetect ? undefined : resolveAgentScope(options.agent, options.cwd);
15
25
  const resolvedAgentIdentity = agent
16
26
  ? (options.agent ? findAgentIdentityByName(agent, options.cwd) : resolveCurrentAgentIdentity(options.cwd))
17
27
  : undefined;
18
28
  const claims = listClaims(options.cwd).filter((claim) => claim.status === 'active');
29
+ const activeSequence = getActiveSequence(options.cwd);
19
30
  const runtimeNotes = listRuntimeNotes({
20
31
  agent,
21
32
  hostId: options.host,
@@ -75,37 +86,221 @@ export function buildCoordinationSnapshot(options = {}) {
75
86
  active_claims: agent
76
87
  ? filteredClaims.filter((claim) => claim.agent === agent)
77
88
  : filteredClaims,
89
+ active_assignments: (agent
90
+ ? listAssignments(options.cwd, { agent })
91
+ : listAssignments(options.cwd)).filter((assignment) => !['completed', 'failed', 'expired', 'rerouted'].includes(assignment.status) &&
92
+ (!project || !assignment.plan_id || filteredPlans.some((plan) => plan.id === assignment.plan_id))),
93
+ active_runs: (agent
94
+ ? listAgentRuns(options.cwd, { agent })
95
+ : listAgentRuns(options.cwd)).filter((run) => !['completed', 'failed', 'cancelled', 'timed_out', 'interrupted'].includes(run.status) &&
96
+ (!project || !run.plan_id || filteredPlans.some((plan) => plan.id === run.plan_id))),
97
+ active_actions: (agent
98
+ ? listActionRequired(options.cwd, { agent, status: 'pending' })
99
+ : listActionRequired(options.cwd, { status: 'pending' })).filter((action) => !project || !action.plan_id || filteredPlans.some((plan) => plan.id === action.plan_id)),
100
+ active_sequence: enrichSequenceWithPlanStatus(activeSequence, state.plan_items),
78
101
  runtime_notes: filteredNotes,
79
102
  session_meta_hidden: sessionMetaHidden,
80
103
  open_handoffs: filteredHandoffs,
81
104
  resolved_instructions: instructions,
82
105
  reputation_summary: reputationSummary,
83
106
  agent_reputation: agentReputation,
84
- other_agents: buildOtherAgentsSummary(filteredClaims, filteredNotes, agent),
107
+ other_agents: buildOtherAgentsSummary(filteredClaims, runtimeNotes, agent, options.cwd),
108
+ linked_projects: buildLinkedProjectsSummary(options.cwd),
109
+ incoming_signals: buildIncomingSignalsSummary(options.cwd),
110
+ known_traps: state.known_traps
111
+ .filter((t) => t.visibility === 'shared' && (!t.status || t.status === 'active'))
112
+ .sort((a, b) => severityOrder(b.severity) - severityOrder(a.severity)),
113
+ pending_candidates: listCandidates('pending', options.cwd),
114
+ inbox_pending: agent ? countActionable(agent, options.cwd ?? process.cwd()) : 0,
85
115
  };
86
116
  }
87
- function buildOtherAgentsSummary(claims, notes, currentAgent) {
117
+ function enrichSequenceWithPlanStatus(sequence, allPlans) {
118
+ if (!sequence)
119
+ return sequence;
120
+ const planMap = new Map(allPlans.map(p => [p.id, p]));
121
+ return {
122
+ ...sequence,
123
+ items: sequence.items.map((item) => {
124
+ const plan = planMap.get(item.planId);
125
+ let planStatus = plan?.status ?? 'unknown';
126
+ let planText = plan?.text?.slice(0, 80) ?? item.planId;
127
+ if (item.stepId && plan?.steps) {
128
+ const step = plan.steps.find((s) => s.id === item.stepId);
129
+ if (step) {
130
+ planStatus = step.status === 'done' ? 'done' : plan.status;
131
+ planText = step.text.slice(0, 80);
132
+ }
133
+ }
134
+ return {
135
+ ...item,
136
+ plan_status: planStatus,
137
+ plan_text: planText,
138
+ plan_priority: plan?.priority,
139
+ plan_assignee: plan?.assignee,
140
+ };
141
+ }),
142
+ };
143
+ }
144
+ /** Parse a duration string like '4h', '30m', '1d' to milliseconds. */
145
+ function parseBoardTtl(value) {
146
+ const match = /^(\d+)([mhd])$/i.exec(value.trim());
147
+ if (!match)
148
+ return 4 * 3_600_000;
149
+ const amount = parseInt(match[1], 10);
150
+ const unit = match[2].toLowerCase();
151
+ if (unit === 'm')
152
+ return amount * 60_000;
153
+ if (unit === 'h')
154
+ return amount * 3_600_000;
155
+ return amount * 86_400_000;
156
+ }
157
+ function buildOtherAgentsSummary(claims, notes, currentAgent, cwd) {
158
+ // Count active sessions per agent for instance_count — use config TTL
159
+ const sessions = loadAllSessions(cwd);
160
+ const now = Date.now();
161
+ let ttlStr = '4h';
162
+ try {
163
+ ttlStr = loadConfig(cwd).implicit_session_ttl ?? '4h';
164
+ }
165
+ catch { /* use default */ }
166
+ const TTL_MS = parseBoardTtl(ttlStr);
167
+ const sessionCounts = new Map();
168
+ for (const s of sessions) {
169
+ const lastSeen = new Date(s.last_seen_at).getTime();
170
+ if (!isNaN(lastSeen) && now - lastSeen < TTL_MS) {
171
+ sessionCounts.set(s.agent, (sessionCounts.get(s.agent) ?? 0) + 1);
172
+ }
173
+ }
174
+ // Start from ALL registered agents — they always appear
88
175
  const agentMap = new Map();
176
+ for (const identity of listAgentIdentities(cwd)) {
177
+ if (identity.agent_name === currentAgent)
178
+ continue;
179
+ const profile = getCapabilityProfile(identity.agent_name);
180
+ const maxTasks = profile?.max_concurrent_tasks ?? 1;
181
+ agentMap.set(identity.agent_name, {
182
+ name: identity.agent_name,
183
+ trust_level: identity.trust_level ?? 'contributor',
184
+ claim_count: 0,
185
+ scopes: [],
186
+ has_open_session: false,
187
+ instance_count: sessionCounts.get(identity.agent_name) ?? 0,
188
+ max_tasks: maxTasks,
189
+ slots_remaining: maxTasks, // will be reduced when claims are counted
190
+ });
191
+ }
192
+ // Enrich with active claims
89
193
  for (const claim of claims) {
90
194
  if (claim.agent === currentAgent)
91
195
  continue;
92
- const existing = agentMap.get(claim.agent) ?? { name: claim.agent, claim_count: 0, scopes: [] };
196
+ const profile = getCapabilityProfile(claim.agent);
197
+ const maxTasks = profile?.max_concurrent_tasks ?? 1;
198
+ const existing = agentMap.get(claim.agent) ?? {
199
+ name: claim.agent, trust_level: 'contributor', claim_count: 0, scopes: [],
200
+ has_open_session: false, instance_count: sessionCounts.get(claim.agent) ?? 0,
201
+ max_tasks: maxTasks, slots_remaining: maxTasks,
202
+ };
93
203
  existing.claim_count++;
204
+ existing.slots_remaining = Math.max(0, existing.max_tasks - existing.claim_count);
94
205
  existing.scopes.push(claim.scope);
95
206
  if (!existing.last_active || claim.created_at > existing.last_active) {
96
207
  existing.last_active = claim.created_at;
97
208
  }
98
209
  agentMap.set(claim.agent, existing);
99
210
  }
211
+ // Enrich with runtime notes (including session lifecycle) for last_active + open session detection
100
212
  for (const note of notes) {
101
213
  if (note.agent === currentAgent)
102
214
  continue;
103
215
  const existing = agentMap.get(note.agent);
104
- if (existing && (!existing.last_active || note.created_at > existing.last_active)) {
216
+ if (!existing)
217
+ continue; // skip unregistered agents in notes
218
+ if (!existing.last_active || note.created_at > existing.last_active) {
105
219
  existing.last_active = note.created_at;
106
220
  }
221
+ if (note.note_type === 'session_start') {
222
+ existing.has_open_session = true;
223
+ }
224
+ if (note.note_type === 'session_end') {
225
+ existing.has_open_session = false;
226
+ }
107
227
  }
108
228
  const result = [...agentMap.values()];
109
229
  return result.length > 0 ? result : undefined;
110
230
  }
231
+ function buildLinkedProjectsSummary(cwd) {
232
+ const links = resolveCrossProjectLinks(cwd);
233
+ if (links.length === 0)
234
+ return undefined;
235
+ const summaries = [];
236
+ for (const link of links) {
237
+ const summary = {
238
+ name: link.projectName,
239
+ path: link.path,
240
+ role: link.role,
241
+ available: link.available,
242
+ active_claims: 0,
243
+ active_plans: 0,
244
+ agents: [],
245
+ };
246
+ if (link.available) {
247
+ try {
248
+ const claims = listClaims(link.absolutePath).filter(c => c.status === 'active');
249
+ const state = loadState(link.absolutePath);
250
+ const plans = state.plan_items.filter(p => p.status !== 'done' && p.status !== 'dropped');
251
+ summary.active_claims = claims.length;
252
+ summary.active_plans = plans.length;
253
+ const agentSet = new Set();
254
+ for (const c of claims)
255
+ agentSet.add(c.agent);
256
+ summary.agents = [...agentSet];
257
+ }
258
+ catch { /* linked project read failed, skip */ }
259
+ }
260
+ summaries.push(summary);
261
+ }
262
+ return summaries.length > 0 ? summaries : undefined;
263
+ }
264
+ function buildIncomingSignalsSummary(cwd) {
265
+ const signals = listIncomingCrossProjectSignals(cwd);
266
+ const incomingSignals = signals.map((signal) => {
267
+ const text = 'text' in signal.payload ? signal.payload.text : '';
268
+ return {
269
+ id: signal.id,
270
+ entity_type: signal.entity_type,
271
+ from_project: signal.from_project.name,
272
+ from_agent: signal.from_agent.name,
273
+ created_at: signal.created_at,
274
+ preview: text.length > 120 ? text.slice(0, 117) + '...' : text,
275
+ };
276
+ });
277
+ try {
278
+ const fedSignals = pullSignalsFromLinkedProjects(cwd);
279
+ for (const sig of fedSignals) {
280
+ const payloadPreview = typeof sig.payload === 'string'
281
+ ? sig.payload.slice(0, 120)
282
+ : JSON.stringify(sig.payload).slice(0, 120);
283
+ incomingSignals.push({
284
+ id: sig.id,
285
+ entity_type: sig.type,
286
+ from_project: sig.from.project_name,
287
+ from_agent: sig.from.agent_name,
288
+ preview: payloadPreview,
289
+ created_at: sig.created_at,
290
+ });
291
+ }
292
+ }
293
+ catch { /* non-fatal */ }
294
+ if (incomingSignals.length === 0)
295
+ return undefined;
296
+ return incomingSignals;
297
+ }
298
+ function severityOrder(severity) {
299
+ switch (severity) {
300
+ case 'high': return 3;
301
+ case 'medium': return 2;
302
+ case 'low': return 1;
303
+ default: return 0;
304
+ }
305
+ }
111
306
  //# sourceMappingURL=coordination.js.map
@@ -1,8 +1,34 @@
1
+ import fs from 'node:fs';
1
2
  import path from 'node:path';
2
- import { loadConfig } from './config.js';
3
+ import { loadConfig, saveConfig } from './config.js';
3
4
  import { loadState } from './state.js';
4
- import { saveRuntimeNote } from './runtime.js';
5
- import { memoryExists } from './io.js';
5
+ import { generateId, nowISO } from './ids.js';
6
+ import { memoryExists, resolveEntityDir } from './io.js';
7
+ import { CrossProjectLinkSchema } from './schema.js';
8
+ import { resolveProjectRef } from './store-resolution.js';
9
+ function crossProjectSignalDir(cwd, mode = 'read') {
10
+ return path.join(resolveEntityDir('inbox', cwd ?? process.cwd(), mode), 'cross-project');
11
+ }
12
+ function ensureCrossProjectSignalDir(cwd) {
13
+ const dir = crossProjectSignalDir(cwd, 'write');
14
+ if (!fs.existsSync(dir)) {
15
+ fs.mkdirSync(dir, { recursive: true });
16
+ }
17
+ return dir;
18
+ }
19
+ export function resolveCrossProjectWritableTarget(nameOrPath, entityType, cwd) {
20
+ const link = resolveCrossProjectTarget(nameOrPath, cwd);
21
+ if (link.role !== 'publisher') {
22
+ throw new Error(`Cross-project link to '${link.projectName}' is role=subscriber — cannot push ${entityType} signals. Set role: publisher to enable push.`);
23
+ }
24
+ if (!link.available) {
25
+ throw new Error(`Target project not found or not initialized: ${link.absolutePath}`);
26
+ }
27
+ if (link.channels?.length && !link.channels.includes(entityType)) {
28
+ throw new Error(`Cross-project link to '${link.projectName}' does not allow ${entityType} signals. Allowed channels: ${link.channels.join(', ')}.`);
29
+ }
30
+ return link;
31
+ }
6
32
  /**
7
33
  * Resolves cross_project_links from config, converting relative paths to absolute.
8
34
  */
@@ -65,22 +91,64 @@ export function loadCrossProjectState(absolutePath) {
65
91
  return loadState(absolutePath);
66
92
  }
67
93
  /**
68
- * Writes a runtime note into a target (publisher-linked) project's runtime dir.
69
- * Used by bclaw_write_note --cross-project.
94
+ * Writes a structured signal into a target (publisher-linked) project's inbox.
70
95
  */
71
- export function writeCrossProjectNote(targetAbsolutePath, note, sourceCwd) {
72
- const links = resolveCrossProjectLinks(sourceCwd);
73
- const link = links.find((l) => path.resolve(l.absolutePath) === path.resolve(targetAbsolutePath));
74
- if (!link) {
75
- throw new Error(`No cross_project_link configured for path: ${targetAbsolutePath}`);
76
- }
77
- if (link.role !== 'publisher') {
78
- throw new Error(`Cross-project link to '${link.projectName}' is role=subscriber cannot write notes. Set role: publisher to enable push.`);
96
+ export function writeCrossProjectSignal(target, entityType, payload, sourceCwd) {
97
+ const link = typeof target === 'string'
98
+ ? resolveCrossProjectWritableTarget(target, entityType, sourceCwd)
99
+ : target;
100
+ const sourceRoot = path.resolve(sourceCwd ?? process.cwd());
101
+ const sourceConfig = loadConfig(sourceRoot);
102
+ const agentName = 'author' in payload ? payload.author : ('agent' in payload ? payload.agent : 'unknown');
103
+ const agentId = 'author_id' in payload ? payload.author_id : ('agent_id' in payload ? payload.agent_id : undefined);
104
+ const signal = {
105
+ schema_version: 1,
106
+ id: generateId('sig'),
107
+ entity_type: entityType,
108
+ created_at: nowISO(),
109
+ from_project: {
110
+ id: sourceConfig.project_id,
111
+ name: sourceConfig.project_name ?? path.basename(sourceRoot),
112
+ path: sourceRoot,
113
+ },
114
+ from_agent: {
115
+ name: agentName,
116
+ id: agentId,
117
+ host_id: payload.host_id,
118
+ session_id: payload.session_id,
119
+ },
120
+ target_project: {
121
+ name: link.projectName,
122
+ path: link.absolutePath,
123
+ },
124
+ payload,
125
+ };
126
+ const dir = ensureCrossProjectSignalDir(link.absolutePath);
127
+ const filepath = path.join(dir, `${signal.id}.json`);
128
+ fs.writeFileSync(filepath, JSON.stringify(signal, null, 2) + '\n', 'utf-8');
129
+ return signal;
130
+ }
131
+ /**
132
+ * Lists cross-project signals materialized in the local inbox.
133
+ */
134
+ export function listIncomingCrossProjectSignals(cwd) {
135
+ const dir = crossProjectSignalDir(cwd);
136
+ if (!fs.existsSync(dir)) {
137
+ return [];
79
138
  }
80
- if (!link.available) {
81
- throw new Error(`Target project not found or not initialized: ${targetAbsolutePath}`);
139
+ const signals = [];
140
+ for (const entry of fs.readdirSync(dir)) {
141
+ if (!entry.endsWith('.json'))
142
+ continue;
143
+ const filepath = path.join(dir, entry);
144
+ try {
145
+ signals.push(JSON.parse(fs.readFileSync(filepath, 'utf-8')));
146
+ }
147
+ catch {
148
+ // Ignore malformed signal files.
149
+ }
82
150
  }
83
- saveRuntimeNote({ ...note, note_type: 'cross_project' }, targetAbsolutePath);
151
+ return signals.sort((a, b) => a.created_at.localeCompare(b.created_at));
84
152
  }
85
153
  /**
86
154
  * Returns the absolute path of a cross-project link by name or path fragment.
@@ -96,4 +164,150 @@ export function resolveCrossProjectTarget(nameOrPath, cwd) {
96
164
  }
97
165
  return match;
98
166
  }
167
+ /**
168
+ * Resolve a `project` argument (name, path, or basename) to an absolute cwd
169
+ * usable by entity-operations / state / etc. Powers the optional `project?`
170
+ * parameter on the canonical grammar (bclaw_find/get/create/update/remove/
171
+ * transition/context/coordinate) — pln#359.
172
+ *
173
+ * Cross-project switching is intentionally limited to **linked projects only**
174
+ * — projects the user has explicitly opted into. Two link kinds count:
175
+ *
176
+ * • cross_project_links (peer/sibling links via config.yaml).
177
+ * • workspace store-chain children (monorepo-style nested projects), via
178
+ * `resolveProjectRef`. These are also "linked" — the parent workspace
179
+ * enumerates them through its config / discovery scan.
180
+ *
181
+ * Arbitrary directory paths that aren't reachable via either link kind are
182
+ * rejected. Adoption requires an explicit `brainclaw link add` or workspace
183
+ * registration — single point of control over what an agent can reach.
184
+ *
185
+ * Resolution order:
186
+ * 1. `projectArg` undefined or empty → return `currentCwd` unchanged.
187
+ * 2. `projectArg` matches the current project's `project_name` (from config)
188
+ * OR its directory basename → `currentCwd`.
189
+ * 3. `projectArg` matches a cross_project_link → that link's `absolutePath`,
190
+ * provided the link is `available` (target dir exists + brainclaw-init).
191
+ * Match keys: projectName, name, path, absolutePath, basename(absolutePath).
192
+ * 4. `projectArg` matches a workspace store-chain child via resolveProjectRef
193
+ * → that absolute path.
194
+ * 5. Otherwise → throw with a hint listing
195
+ * the configured cross_project_links so the agent can self-correct.
196
+ *
197
+ * Errors are intentionally explicit rather than falling back silently — a
198
+ * misrouted write is far worse than a clean "unknown project" error.
199
+ */
200
+ export function resolveProjectCwd(projectArg, currentCwd) {
201
+ if (!projectArg || projectArg.trim() === '')
202
+ return currentCwd;
203
+ const trimmed = projectArg.trim();
204
+ const baseCwd = path.resolve(currentCwd);
205
+ // Case 2: matches current project (by configured name OR by basename)
206
+ try {
207
+ const currentConfig = loadConfig(currentCwd);
208
+ if (currentConfig.project_name === trimmed)
209
+ return currentCwd;
210
+ }
211
+ catch { /* no config in current cwd — fall through */ }
212
+ if (path.basename(baseCwd) === trimmed)
213
+ return currentCwd;
214
+ // Case 3: matches a configured cross_project_link
215
+ const links = resolveCrossProjectLinks(currentCwd);
216
+ const linkMatch = links.find((l) => l.projectName === trimmed ||
217
+ l.name === trimmed ||
218
+ l.path === trimmed ||
219
+ l.absolutePath === trimmed ||
220
+ path.basename(l.absolutePath) === trimmed);
221
+ if (linkMatch) {
222
+ if (!linkMatch.available) {
223
+ throw new Error(`Cross-project link '${linkMatch.projectName}' is not available at ${linkMatch.absolutePath} ` +
224
+ `(target dir missing or not brainclaw-initialised).`);
225
+ }
226
+ return linkMatch.absolutePath;
227
+ }
228
+ // Case 4: matches a workspace store-chain child (monorepo-style nesting)
229
+ const wsHit = resolveProjectRef(trimmed, currentCwd);
230
+ if (wsHit)
231
+ return wsHit;
232
+ // Case 5: nothing matched — throw with helpful hint
233
+ const knownLinks = links.map((l) => l.projectName).join(', ') || '<none>';
234
+ throw new Error(`Unknown project: '${projectArg}'. Configured cross_project_links: ${knownLinks}. ` +
235
+ `Add one with \`brainclaw link add <path>\` or check config.yaml. ` +
236
+ `Workspace store-chain children are also accepted.`);
237
+ }
238
+ /**
239
+ * Add a new cross_project_link entry to config.yaml.
240
+ *
241
+ * - Resolves a relative input path against `cwd` for the existence check, but
242
+ * stores it as-given (relative paths are friendly for shared configs).
243
+ * - Validates the target directory exists and is brainclaw-initialised.
244
+ * - Derives `name` from the linked project's project_name when possible,
245
+ * else from the basename of the resolved path.
246
+ * - Refuses duplicates by `name` or `path` unless `force: true`.
247
+ */
248
+ export function addCrossProjectLink(input) {
249
+ const baseCwd = path.resolve(input.cwd ?? process.cwd());
250
+ const inputPath = input.path.trim();
251
+ if (!inputPath) {
252
+ throw new Error('path is required');
253
+ }
254
+ const absolutePath = path.isAbsolute(inputPath) ? inputPath : path.resolve(baseCwd, inputPath);
255
+ if (!fs.existsSync(absolutePath)) {
256
+ throw new Error(`Target path does not exist: ${absolutePath}`);
257
+ }
258
+ if (!memoryExists(absolutePath)) {
259
+ throw new Error(`Target is not brainclaw-initialised (no .brainclaw/ found): ${absolutePath}`);
260
+ }
261
+ let derivedName = input.name?.trim();
262
+ if (!derivedName) {
263
+ try {
264
+ derivedName = loadConfig(absolutePath).project_name;
265
+ }
266
+ catch { /* fall through to basename */ }
267
+ }
268
+ derivedName = derivedName ?? path.basename(absolutePath);
269
+ const config = loadConfig(input.cwd);
270
+ const existing = config.cross_project_links ?? [];
271
+ const conflict = existing.find((l) => l.name === derivedName ||
272
+ l.path === inputPath ||
273
+ path.resolve(baseCwd, l.path) === absolutePath);
274
+ if (conflict && !input.force) {
275
+ throw new Error(`Cross-project link already exists (name='${conflict.name ?? path.basename(conflict.path)}', path='${conflict.path}'). Use force: true to replace.`);
276
+ }
277
+ const link = CrossProjectLinkSchema.parse({
278
+ path: inputPath,
279
+ name: derivedName,
280
+ role: input.role ?? 'subscriber',
281
+ ...(input.channels?.length ? { channels: input.channels } : {}),
282
+ });
283
+ const next = conflict
284
+ ? existing.map((l) => (l === conflict ? link : l))
285
+ : [...existing, link];
286
+ saveConfig({ ...config, cross_project_links: next }, input.cwd);
287
+ return link;
288
+ }
289
+ /**
290
+ * Remove a cross_project_link entry from config.yaml.
291
+ *
292
+ * Matches by `name`, exact `path`, resolved absolute path, or basename of
293
+ * the resolved path — same matching rules as `resolveCrossProjectTarget`.
294
+ */
295
+ export function removeCrossProjectLink(nameOrPath, cwd) {
296
+ const baseCwd = path.resolve(cwd ?? process.cwd());
297
+ const config = loadConfig(cwd);
298
+ const links = config.cross_project_links ?? [];
299
+ const match = links.find((l) => {
300
+ const abs = path.isAbsolute(l.path) ? l.path : path.resolve(baseCwd, l.path);
301
+ return l.name === nameOrPath
302
+ || l.path === nameOrPath
303
+ || abs === nameOrPath
304
+ || path.basename(abs) === nameOrPath;
305
+ });
306
+ if (!match) {
307
+ throw new Error(`No cross_project_link found matching: '${nameOrPath}'`);
308
+ }
309
+ const next = links.filter((l) => l !== match);
310
+ saveConfig({ ...config, cross_project_links: next }, cwd);
311
+ return match;
312
+ }
99
313
  //# sourceMappingURL=cross-project.js.map
@@ -0,0 +1,11 @@
1
+ name: doctor
2
+ description: Run diagnostics and fix issues automatically
3
+ trust_level: trusted
4
+ trigger: manual
5
+ prompt: >-
6
+ Run bclaw_doctor, analyze the findings, and fix what can be fixed
7
+ automatically. For issues requiring human judgment, create plans.
8
+ invoke: codex exec --full-auto "{prompt}"
9
+ tags:
10
+ - diagnostics
11
+ - built-in
@@ -0,0 +1,11 @@
1
+ name: janitor
2
+ description: Clean up stale claims, archive old notes, check for orphaned files
3
+ trust_level: contributor
4
+ trigger: manual
5
+ prompt: >-
6
+ Clean up this project: prune stale claims, archive old runtime notes, check
7
+ for orphaned files. Use bclaw_doctor, prune, and release-claims.
8
+ invoke: codex exec --full-auto "{prompt}"
9
+ tags:
10
+ - maintenance
11
+ - built-in
@@ -0,0 +1,11 @@
1
+ name: onboarder
2
+ description: Generate a project summary for new team members
3
+ trust_level: observer
4
+ trigger: manual
5
+ prompt: >-
6
+ Generate a project summary for a new team member. Read the board, plans,
7
+ decisions, constraints, and produce a concise onboarding document.
8
+ invoke: claude -p "{prompt}" --allowedTools "Edit,Write,Bash,Read,Glob,Grep"
9
+ tags:
10
+ - onboarding
11
+ - built-in
@@ -0,0 +1,13 @@
1
+ name: reviewer
2
+ description: Review pending candidates and give structured opinions
3
+ trust_level: trusted
4
+ trigger: manual
5
+ prompt: >-
6
+ Review pending candidates in this project. For each, give a structured
7
+ opinion (approve/reject with reasons). Use
8
+ bclaw_find(entity: "candidate", filter: {status: "pending"}) and
9
+ bclaw_transition(entity: "candidate", id, to: "accepted" | "rejected").
10
+ invoke: claude -p "{prompt}" --allowedTools "Edit,Write,Bash,Read,Glob,Grep"
11
+ tags:
12
+ - review
13
+ - built-in