brainclaw 1.14.0 → 1.16.0

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 (63) hide show
  1. package/README.md +16 -263
  2. package/dist/brainclaw-vscode.vsix +0 -0
  3. package/dist/cli/register-capture.js +209 -0
  4. package/dist/cli/register-code-map.js +19 -0
  5. package/dist/cli/register-coordination.js +472 -0
  6. package/dist/cli/register-federation.js +258 -0
  7. package/dist/cli/register-lifecycle.js +436 -0
  8. package/dist/cli/register-memory-context.js +502 -0
  9. package/dist/cli/register-planning.js +167 -0
  10. package/dist/cli/register-review.js +149 -0
  11. package/dist/cli/shared.js +5 -0
  12. package/dist/cli.js +212 -2015
  13. package/dist/commands/dispatch-watch.js +25 -2
  14. package/dist/commands/harvest.js +31 -6
  15. package/dist/commands/mcp-catalog.js +1438 -0
  16. package/dist/commands/mcp-contract.js +33 -0
  17. package/dist/commands/mcp-presentation.js +27 -0
  18. package/dist/commands/mcp-read-handlers.js +72 -36
  19. package/dist/commands/mcp-write-admin.js +328 -0
  20. package/dist/commands/mcp-write-claims.js +864 -0
  21. package/dist/commands/mcp-write-coordination.js +1825 -0
  22. package/dist/commands/mcp-write-entities.js +620 -0
  23. package/dist/commands/mcp-write-memory.js +451 -0
  24. package/dist/commands/mcp-write-sequences.js +116 -0
  25. package/dist/commands/mcp-write-support.js +367 -0
  26. package/dist/commands/mcp.js +261 -5570
  27. package/dist/commands/update-handoff.js +28 -42
  28. package/dist/core/agent-capability.js +31 -14
  29. package/dist/core/agent-files.js +1 -1
  30. package/dist/core/agent-registry.js +51 -3
  31. package/dist/core/claims.js +18 -0
  32. package/dist/core/coordination.js +5 -2
  33. package/dist/core/cross-project.js +35 -1
  34. package/dist/core/dispatcher.js +34 -20
  35. package/dist/core/entity-operations.js +335 -12
  36. package/dist/core/entity-registry.js +72 -9
  37. package/dist/core/execution.js +28 -4
  38. package/dist/core/facade-schema.js +30 -4
  39. package/dist/core/federation-cloud.js +142 -11
  40. package/dist/core/federation-outbox.js +292 -0
  41. package/dist/core/federation-signing.js +115 -0
  42. package/dist/core/handoff-review.js +35 -0
  43. package/dist/core/io.js +6 -0
  44. package/dist/core/protocol-tool-policy.js +113 -0
  45. package/dist/core/review-loop-close.js +115 -0
  46. package/dist/core/schema.js +25 -2
  47. package/dist/core/security-detectors.js +35 -6
  48. package/dist/core/security.js +32 -12
  49. package/dist/core/worktree.js +98 -9
  50. package/dist/facts.js +13 -11
  51. package/dist/facts.json +12 -10
  52. package/docs/PROTOCOL.md +7 -3
  53. package/docs/concepts/coordinator-runbook.md +3 -0
  54. package/docs/concepts/dispatch-lifecycle.md +4 -4
  55. package/docs/concepts/loop-engine.md +3 -1
  56. package/docs/concepts/troubleshooting.md +1 -1
  57. package/docs/integrations/codex.md +3 -3
  58. package/docs/integrations/overview.md +1 -1
  59. package/docs/mcp-schema-changelog.md +153 -2
  60. package/docs/playbooks/orchestration.md +1 -1
  61. package/docs/product/entity-model-audit.md +3 -2
  62. package/docs/security.md +22 -1
  63. package/package.json +3 -1
@@ -0,0 +1,620 @@
1
+ /**
2
+ * MCP entity write-tool handlers.
3
+ *
4
+ * Extracted from mcp.ts (pln#622 PR4) — mechanical move of the canonical
5
+ * grammar (create/update/remove/transition/move), plan/candidate creation,
6
+ * candidate accept/reject, plan deletion, and handoff correction/update write
7
+ * handlers. Behavior is unchanged; each handler receives the tool-call payload
8
+ * plus a {@link McpWriteEntitiesContext} carrying the per-call scope and the
9
+ * mcp.ts-local helpers that remain in the assembly point (cross-project
10
+ * guards, the execution write-target resolver, the cross-project arg reader,
11
+ * and the legacy error formatter), passed by reference so this module never
12
+ * imports ./mcp.js (dependency-direction guard, pln#622 PR1).
13
+ *
14
+ * @module
15
+ */
16
+ import { appendAuditEntry } from '../core/audit.js';
17
+ import { nowISO, generateIdWithLabel } from '../core/ids.js';
18
+ import { loadState, persistState, saveState } from '../core/state.js';
19
+ import { ENTITY_REGISTRY } from '../core/entity-registry.js';
20
+ import { createEntity, updateEntity, removeEntity, transitionEntity } from '../core/entity-operations.js';
21
+ import { relocateEntity } from '../core/operations/relocate.js';
22
+ import { createPlan, deletePlan as deletePlanOp } from '../core/operations/plan.js';
23
+ import { loadCandidate } from '../core/candidates.js';
24
+ import { resolveCrossProjectWritableTarget, resolveProjectCwd, writeCrossProjectSignal } from '../core/cross-project.js';
25
+ import { hasMinimumTrustLevel } from '../core/agent-registry.js';
26
+ import { acceptCandidate } from './accept.js';
27
+ import { rejectCandidate } from './reject.js';
28
+ import { applyHandoffUpdates } from './update-handoff.js';
29
+ import { ensureTrust, resolveMutationIdentity, resolveCanonicalAuthor, renderAutoRepairWarning, scopeMetadataForTarget, scanMcpWriteText, appendSecurityWarnings, } from './mcp-write-support.js';
30
+ import { SCHEMA_VERSION, toolResponse, createToolErrorResponse, } from './mcp-contract.js';
31
+ export function handleBclawCreatePlan(payload, ctx) {
32
+ const { args, cwd, connectionSessionId } = payload;
33
+ const crossProjectError = ctx.blockCrossProjectExecution('plan', args);
34
+ if (crossProjectError) {
35
+ return { response: crossProjectError };
36
+ }
37
+ const resolved = ensureTrust(args, { nameField: 'agent', idField: 'agentId' }, 'contributor', cwd, connectionSessionId);
38
+ if (resolved.error) {
39
+ return { response: createToolErrorResponse(resolved.error.kind, resolved.error.message, resolved.error.details) };
40
+ }
41
+ const planText = String(args.text ?? '').trim();
42
+ if (!planText) {
43
+ return { response: createToolErrorResponse('validation_error', 'Missing required argument: text') };
44
+ }
45
+ // S2 (pln#623): scan write text on the MCP path (same control point as the CLI).
46
+ const scan = scanMcpWriteText(planText, cwd);
47
+ if (scan.blockResponse)
48
+ return { response: scan.blockResponse };
49
+ try {
50
+ const created = createPlan({
51
+ text: planText,
52
+ author: resolved.identity.agent_name,
53
+ type: args.type,
54
+ priority: args.priority,
55
+ assignee: args.assignee,
56
+ project: args.project,
57
+ tags: Array.isArray(args.tags) ? args.tags : undefined,
58
+ relatedPaths: Array.isArray(args.related_paths) ? args.related_paths : undefined,
59
+ dependsOn: Array.isArray(args.depends_on) ? args.depends_on : undefined,
60
+ estimatedEffort: typeof args.estimate === 'number'
61
+ ? args.estimate
62
+ : typeof args.estimated_effort === 'number'
63
+ ? args.estimated_effort
64
+ : undefined,
65
+ }, cwd);
66
+ appendAuditEntry({
67
+ actor: resolved.identity.agent_name,
68
+ actor_id: resolved.identity.agent_id,
69
+ action: 'create',
70
+ item_id: created.id,
71
+ item_type: 'plan',
72
+ }, cwd);
73
+ return {
74
+ response: appendSecurityWarnings(toolResponse({
75
+ content: [{ type: 'text', text: `✔ Plan added: [${created.shortLabel}] ${created.text}` }],
76
+ plan_id: created.id,
77
+ short_label: created.shortLabel,
78
+ text: created.text,
79
+ }), scan.warnings),
80
+ };
81
+ }
82
+ catch (error) {
83
+ return { response: ctx.createLegacyToolExecutionErrorResponse(error) };
84
+ }
85
+ }
86
+ export function handleBclawCreateCandidate(payload, ctx) {
87
+ const { args, cwd, connectionSessionId } = payload;
88
+ const resolved = ensureTrust(args, { nameField: 'agent', idField: 'agentId' }, 'contributor', cwd, connectionSessionId);
89
+ if (resolved.error) {
90
+ return { response: createToolErrorResponse(resolved.error.kind, resolved.error.message, resolved.error.details) };
91
+ }
92
+ const candidateText = String(args.text ?? '').trim();
93
+ if (!candidateText) {
94
+ return { response: createToolErrorResponse('validation_error', 'Missing required argument: text') };
95
+ }
96
+ const candidateType = String(args.type ?? '').trim();
97
+ if (!['constraint', 'decision', 'trap', 'handoff'].includes(candidateType)) {
98
+ return { response: createToolErrorResponse('validation_error', 'type must be one of: constraint, decision, trap, handoff') };
99
+ }
100
+ // S2 (pln#623): scan write text on the MCP path (same control point as the CLI).
101
+ const scan = scanMcpWriteText(candidateText, cwd);
102
+ if (scan.blockResponse)
103
+ return { response: scan.blockResponse };
104
+ try {
105
+ const created = createEntity('candidate', {
106
+ text: candidateText,
107
+ type: candidateType,
108
+ author: resolved.identity.agent_name,
109
+ tags: Array.isArray(args.tags) ? args.tags : undefined,
110
+ source: 'agent',
111
+ ...(typeof args.origin === 'string' ? { origin: String(args.origin) } : {}),
112
+ ...(typeof args.severity === 'string' ? { severity: String(args.severity) } : {}),
113
+ ...(typeof args.from === 'string' ? { from: String(args.from) } : {}),
114
+ ...(typeof args.to === 'string' ? { to: String(args.to) } : {}),
115
+ ...(typeof args.narrative === 'string' ? { narrative: String(args.narrative) } : {}),
116
+ ...(Array.isArray(args.related_paths) ? { related_paths: args.related_paths } : {}),
117
+ ...(typeof args.plan_id === 'string' ? { plan_id: String(args.plan_id) } : {}),
118
+ }, cwd);
119
+ appendAuditEntry({
120
+ actor: resolved.identity.agent_name,
121
+ actor_id: resolved.identity.agent_id,
122
+ action: 'create',
123
+ item_id: created.id,
124
+ item_type: 'candidate',
125
+ }, cwd);
126
+ const targetProjectArg = ctx.getCrossProjectArg(args, 'targetProject', 'target_project');
127
+ if (targetProjectArg) {
128
+ const signal = writeCrossProjectSignal(resolveCrossProjectWritableTarget(targetProjectArg, 'candidate', cwd), 'candidate', loadCandidate(created.id, cwd), cwd);
129
+ return {
130
+ response: appendSecurityWarnings(toolResponse({
131
+ content: [{ type: 'text', text: `✔ Candidate created [${created.id}] and signaled to '${signal.target_project.name}' [${signal.id}]` }],
132
+ candidate_id: created.id,
133
+ short_label: created.short_label,
134
+ signal_id: signal.id,
135
+ entity_type: signal.entity_type,
136
+ target_project: signal.target_project.name,
137
+ target_path: signal.target_project.path,
138
+ }), scan.warnings),
139
+ };
140
+ }
141
+ return {
142
+ response: appendSecurityWarnings(toolResponse({
143
+ content: [{ type: 'text', text: `✔ Candidate created [${created.id}]` }],
144
+ candidate_id: created.id,
145
+ short_label: created.short_label,
146
+ }), scan.warnings),
147
+ };
148
+ }
149
+ catch (error) {
150
+ return { response: ctx.createLegacyToolExecutionErrorResponse(error) };
151
+ }
152
+ }
153
+ export function handleBclawAccept(payload, ctx) {
154
+ const { args, cwd, connectionSessionId } = payload;
155
+ const resolved = ensureTrust(args, { nameField: 'by', idField: 'byId' }, 'trusted', cwd, connectionSessionId);
156
+ if (resolved.error) {
157
+ return { response: createToolErrorResponse(resolved.error.kind, resolved.error.message, resolved.error.details) };
158
+ }
159
+ try {
160
+ const id = String(args.id ?? '').trim();
161
+ if (!id) {
162
+ return { response: createToolErrorResponse('validation_error', 'Missing required argument: id') };
163
+ }
164
+ const result = acceptCandidate(id, resolved.identity.agent_name, cwd, resolved.identity.agent_id);
165
+ return {
166
+ response: toolResponse({
167
+ content: [{ type: 'text', text: `✔ Promoted to ${result.candidate_type} [${result.promoted_item_id}]` }],
168
+ candidate_id: result.candidate_id,
169
+ candidate_type: result.candidate_type,
170
+ promoted_item_id: result.promoted_item_id,
171
+ actor: result.actor,
172
+ }),
173
+ };
174
+ }
175
+ catch (error) {
176
+ return { response: ctx.createLegacyToolExecutionErrorResponse(error) };
177
+ }
178
+ }
179
+ export function handleBclawReject(payload, ctx) {
180
+ const { args, cwd, connectionSessionId } = payload;
181
+ const resolved = ensureTrust(args, { nameField: 'by', idField: 'byId' }, 'trusted', cwd, connectionSessionId);
182
+ if (resolved.error) {
183
+ return { response: createToolErrorResponse(resolved.error.kind, resolved.error.message, resolved.error.details) };
184
+ }
185
+ try {
186
+ const id = String(args.id ?? '').trim();
187
+ if (!id) {
188
+ return { response: createToolErrorResponse('validation_error', 'Missing required argument: id') };
189
+ }
190
+ const result = rejectCandidate(id, typeof args.reason === 'string' ? args.reason : undefined, resolved.identity.agent_name, cwd, resolved.identity.agent_id);
191
+ return {
192
+ response: toolResponse({
193
+ content: [{ type: 'text', text: `✔ Candidate rejected [${result.candidate_id}]` }],
194
+ candidate_id: result.candidate_id,
195
+ actor: result.actor,
196
+ }),
197
+ };
198
+ }
199
+ catch (error) {
200
+ return { response: ctx.createLegacyToolExecutionErrorResponse(error) };
201
+ }
202
+ }
203
+ export function handleBclawDeletePlan(payload, ctx) {
204
+ const { args, cwd, connectionSessionId } = payload;
205
+ const dpLoc = ctx.resolveExecutionWriteTarget('plan', args, cwd, connectionSessionId);
206
+ if (dpLoc.block) {
207
+ return { response: dpLoc.block };
208
+ }
209
+ const dpTargetCwd = dpLoc.targetCwd;
210
+ const resolved = ensureTrust(args, { nameField: 'agent', idField: 'agentId' }, 'trusted', cwd, connectionSessionId);
211
+ if (resolved.error) {
212
+ return { response: createToolErrorResponse(resolved.error.kind, resolved.error.message, resolved.error.details) };
213
+ }
214
+ const dpId = String(args.id ?? '').trim();
215
+ if (!dpId)
216
+ return { response: createToolErrorResponse('validation_error', 'Missing required argument: id') };
217
+ try {
218
+ const result = deletePlanOp(dpId, dpTargetCwd);
219
+ return {
220
+ response: toolResponse({
221
+ content: [{ type: 'text', text: `✔ Plan deleted: [${result.id}] ${result.text.slice(0, 80)}` }],
222
+ plan_id: result.id,
223
+ }),
224
+ };
225
+ }
226
+ catch (err) {
227
+ const msg = err instanceof Error ? err.message : String(err);
228
+ if (msg.includes('not found')) {
229
+ return { response: createToolErrorResponse('not_found', msg) };
230
+ }
231
+ return { response: createToolErrorResponse('operation_error', msg) };
232
+ }
233
+ }
234
+ export function handleBclawCorrectHandoff(payload, _ctx) {
235
+ const { args, cwd, connectionSessionId } = payload;
236
+ // Phase 3 slice 3e — P6.1 tombstone correction. Writes a new
237
+ // handoff that supersedes the original. Both records stay on
238
+ // disk (federation-safe); the original becomes pinned via
239
+ // `superseded_by`.
240
+ const originalId = String(args.originalId ?? '').trim();
241
+ if (!originalId) {
242
+ return { response: createToolErrorResponse('validation_error', 'Missing required argument: originalId') };
243
+ }
244
+ const resolved = ensureTrust(args, { nameField: 'agent', idField: 'agentId' }, 'contributor', cwd, connectionSessionId);
245
+ if (resolved.error) {
246
+ return { response: createToolErrorResponse(resolved.error.kind, resolved.error.message, resolved.error.details) };
247
+ }
248
+ const resolvedIdentity = resolved.identity;
249
+ const state = loadState(cwd);
250
+ const original = state.open_handoffs.find((h) => h.id === originalId);
251
+ if (!original) {
252
+ return { response: createToolErrorResponse('not_found', `Handoff not found: ${originalId}`) };
253
+ }
254
+ if (original.superseded_by) {
255
+ return { response: createToolErrorResponse('validation_error', `Handoff ${originalId} was already superseded by ${original.superseded_by}. Correct the current tip instead.`) };
256
+ }
257
+ // Phase 3 slice 3e fixup (Sonnet review #3): refuse to supersede a
258
+ // handoff in a terminal status per EntityRegistry. A closed handoff
259
+ // is immutable history — corrections would logically dangle.
260
+ if (original.status && ENTITY_REGISTRY.handoff.terminal.includes(original.status)) {
261
+ return { response: createToolErrorResponse('validation_error', `Handoff ${originalId} is in terminal status '${original.status}'. Cannot supersede a closed handoff.`) };
262
+ }
263
+ const { id: newId, short_label } = generateIdWithLabel('open_handoffs', cwd);
264
+ const reason = typeof args.reason === 'string' && args.reason ? args.reason : undefined;
265
+ const overrideText = typeof args.text === 'string' && args.text ? args.text : undefined;
266
+ const correctionText = overrideText
267
+ ?? `${original.text}\n\n---\n[correction] ${reason ?? 'superseded by later record'}`;
268
+ const overrideNarrative = typeof args.narrative === 'string' && args.narrative ? args.narrative : original.narrative;
269
+ const tags = Array.isArray(args.tags) ? args.tags : original.tags;
270
+ const correction = {
271
+ ...original,
272
+ id: newId,
273
+ short_label,
274
+ text: correctionText,
275
+ narrative: overrideNarrative,
276
+ tags,
277
+ created_at: nowISO(),
278
+ author: resolvedIdentity.agent_name,
279
+ author_id: resolvedIdentity.agent_id,
280
+ session_id: connectionSessionId,
281
+ review: undefined,
282
+ supersedes: originalId,
283
+ };
284
+ delete correction.superseded_by;
285
+ original.superseded_by = newId;
286
+ state.open_handoffs.push(correction);
287
+ persistState(state, cwd);
288
+ appendAuditEntry({
289
+ actor: resolvedIdentity.agent_name,
290
+ actor_id: resolvedIdentity.agent_id,
291
+ action: 'create',
292
+ item_id: newId,
293
+ item_type: 'handoff',
294
+ scope: `supersedes:${originalId}`,
295
+ }, cwd);
296
+ return {
297
+ response: toolResponse({
298
+ content: [{ type: 'text', text: `✔ correction handoff [${short_label ?? newId}] supersedes ${originalId}` }],
299
+ id: newId,
300
+ short_label,
301
+ supersedes: originalId,
302
+ schema_version: SCHEMA_VERSION,
303
+ }),
304
+ };
305
+ }
306
+ export function handleBclawUpdateHandoff(payload, ctx) {
307
+ const { args, cwd, connectionSessionId } = payload;
308
+ const handoffId = String(args.id ?? '').trim();
309
+ if (!handoffId) {
310
+ return { response: createToolErrorResponse('validation_error', 'Missing required argument: id') };
311
+ }
312
+ const resolved = ensureTrust(args, { nameField: 'agent', idField: 'agentId' }, 'contributor', cwd, connectionSessionId);
313
+ if (resolved.error) {
314
+ return { response: createToolErrorResponse(resolved.error.kind, resolved.error.message, resolved.error.details) };
315
+ }
316
+ const resolvedIdentity = resolved.identity;
317
+ const state = loadState(cwd);
318
+ const handoff = state.open_handoffs.find((h) => h.id === handoffId);
319
+ if (!handoff) {
320
+ return { response: createToolErrorResponse('not_found', `Handoff not found: ${handoffId}`) };
321
+ }
322
+ applyHandoffUpdates(handoff, {
323
+ status: args.status,
324
+ to: typeof args.to === 'string' ? String(args.to) : undefined,
325
+ narrative: typeof args.narrative === 'string' ? String(args.narrative) : undefined,
326
+ files_touched: Array.isArray(args.files_touched) ? args.files_touched : undefined,
327
+ pre_conditions: Array.isArray(args.pre_conditions) ? args.pre_conditions : undefined,
328
+ post_conditions: Array.isArray(args.post_conditions) ? args.post_conditions : undefined,
329
+ tests_to_verify: Array.isArray(args.tests_to_verify) ? args.tests_to_verify : undefined,
330
+ linked_plans: Array.isArray(args.linked_plans) ? args.linked_plans : undefined,
331
+ reviewer: typeof args.reviewer === 'string' ? String(args.reviewer) : undefined,
332
+ review_verdict: args.review_verdict,
333
+ reviewed_by: typeof args.reviewed_by === 'string' ? String(args.reviewed_by) : undefined,
334
+ review_summary: typeof args.review_summary === 'string' ? String(args.review_summary) : undefined,
335
+ blocking_issues: Array.isArray(args.blocking_issues) ? args.blocking_issues : undefined,
336
+ suggestions: Array.isArray(args.suggestions) ? args.suggestions : undefined,
337
+ });
338
+ saveState(state, cwd);
339
+ appendAuditEntry({ actor: resolvedIdentity.agent_name, actor_id: resolvedIdentity.agent_id, action: 'update', item_id: handoffId, item_type: 'handoff' }, cwd);
340
+ const targetProjectArg = ctx.getCrossProjectArg(args, 'targetProject', 'target_project');
341
+ if (targetProjectArg) {
342
+ try {
343
+ const targetLink = resolveCrossProjectWritableTarget(targetProjectArg, 'handoff', cwd);
344
+ const signal = writeCrossProjectSignal(targetLink, 'handoff', handoff, cwd);
345
+ return {
346
+ response: toolResponse({
347
+ content: [{ type: 'text', text: `✔ Handoff updated locally and signaled to ${targetLink.projectName} [${signal.id}]` }],
348
+ signal_id: signal.id,
349
+ entity_type: signal.entity_type,
350
+ handoff_id: handoffId,
351
+ status: handoff.status,
352
+ to: handoff.to,
353
+ review: handoff.review,
354
+ target_project: targetLink.projectName,
355
+ target_path: targetLink.absolutePath,
356
+ schema_version: SCHEMA_VERSION,
357
+ }),
358
+ };
359
+ }
360
+ catch (error) {
361
+ return { response: createToolErrorResponse('validation_error', error instanceof Error ? error.message : String(error)) };
362
+ }
363
+ }
364
+ return {
365
+ response: toolResponse({
366
+ content: [{ type: 'text', text: `✔ Handoff updated: [${handoffId}] ${handoff.from} → ${handoff.to} (${handoff.status})` }],
367
+ handoff_id: handoffId,
368
+ status: handoff.status,
369
+ to: handoff.to,
370
+ review: handoff.review,
371
+ schema_version: SCHEMA_VERSION,
372
+ }),
373
+ };
374
+ }
375
+ export function handleBclawCreate(payload, ctx) {
376
+ const { args, cwd, connectionSessionId } = payload;
377
+ try {
378
+ const entity = String(args.entity ?? '');
379
+ // Execution entities (plan/claim) auto-localize into a workspace sibling
380
+ // when project=X names one: session+switch then write locally. Only
381
+ // federated links / unknown names are blocked (signaling-only boundary).
382
+ let targetCwd;
383
+ let autoSwitched = false;
384
+ if (entity === 'claim' || entity === 'plan') {
385
+ const loc = ctx.resolveExecutionWriteTarget(entity, args, cwd, connectionSessionId);
386
+ if (loc.block)
387
+ return { response: loc.block };
388
+ targetCwd = loc.targetCwd;
389
+ autoSwitched = loc.autoSwitched;
390
+ }
391
+ else {
392
+ targetCwd = resolveProjectCwd(args.project, cwd);
393
+ }
394
+ const rawData = (args.data ?? {});
395
+ const targetScope = scopeMetadataForTarget(args, targetCwd, ctx.scopeInfo);
396
+ // Auto-fill identity fields. Without this, a caller who omits author/agent
397
+ // creates a schema-invalid record that is silently dropped on read and
398
+ // GC'd from disk on the next mutation.
399
+ // Identity is resolved against the SOURCE cwd (the agent's own
400
+ // project/registry), not the target — an agent doesn't need to be
401
+ // registered in the target project to write into it. An explicitly
402
+ // supplied data.author is honored as content-level attribution
403
+ // (cross-project signaling writers may not be registered locally);
404
+ // when author is MISSING, resolution is mandatory and failure is a
405
+ // hard validation_error (pln#562 step 3) — never author:'unknown'.
406
+ const data = { ...rawData };
407
+ let actor = typeof data.author === 'string' ? data.author : undefined;
408
+ let actorId = typeof data.agent_id === 'string' ? data.agent_id : undefined;
409
+ let autoRepair;
410
+ if (data.author === undefined) {
411
+ const author = resolveCanonicalAuthor(args, cwd, connectionSessionId);
412
+ data.author = author.agent_name;
413
+ if (data.agent === undefined)
414
+ data.agent = author.agent_name;
415
+ if (data.agent_id === undefined && author.agent_id)
416
+ data.agent_id = author.agent_id;
417
+ actor = author.agent_name;
418
+ actorId = author.agent_id;
419
+ autoRepair = author.auto_repair;
420
+ }
421
+ else if (data.agent === undefined) {
422
+ data.agent = data.author;
423
+ }
424
+ // S2 (pln#623): scan the primary text field on the MCP path, mirroring the
425
+ // CLI write adapters (which scan the entity text before persisting).
426
+ const createScan = scanMcpWriteText(typeof data.text === 'string' ? data.text : '', targetCwd);
427
+ if (createScan.blockResponse)
428
+ return { response: createScan.blockResponse };
429
+ const result = createEntity(entity, data, targetCwd);
430
+ appendAuditEntry({ actor: actor ?? 'unknown', ...(actorId ? { actor_id: actorId } : {}), action: 'create', item_id: result.id, item_type: entity }, targetCwd);
431
+ const createText = `✔ created ${entity} ${result.id}${autoSwitched ? ` (auto-switched → ${targetScope.resolved_project.name ?? targetScope.resolved_project.path})` : ''}`;
432
+ const createContent = autoRepair
433
+ ? [{ type: 'text', text: createText }, { type: 'text', text: renderAutoRepairWarning(autoRepair, actor ?? 'unknown') }]
434
+ : [{ type: 'text', text: createText }];
435
+ return {
436
+ response: appendSecurityWarnings(toolResponse({
437
+ content: createContent,
438
+ structuredContent: {
439
+ ...result,
440
+ resolved_project: targetScope.resolved_project,
441
+ active_source: autoSwitched ? 'auto_switch' : targetScope.active_source,
442
+ ...(autoSwitched ? { auto_switched: true } : {}),
443
+ ...(autoRepair ? { auto_repair: autoRepair } : {}),
444
+ },
445
+ }), createScan.warnings),
446
+ };
447
+ }
448
+ catch (error) {
449
+ return { response: createToolErrorResponse('validation_error', error.message) };
450
+ }
451
+ }
452
+ export function handleBclawUpdate(payload, ctx) {
453
+ const { args, cwd, connectionSessionId } = payload;
454
+ try {
455
+ const entity = String(args.entity ?? '');
456
+ const id = String(args.id ?? '');
457
+ const patch = (args.patch ?? {});
458
+ const targetCwd = resolveProjectCwd(args.project, cwd);
459
+ const targetScope = scopeMetadataForTarget(args, targetCwd, ctx.scopeInfo);
460
+ const { agent_name, agent_id, auto_repair } = resolveCanonicalAuthor(args, cwd, connectionSessionId);
461
+ // S2 (pln#623): scan the patched text field on the MCP path (CLI parity).
462
+ const updateScan = scanMcpWriteText(typeof patch.text === 'string' ? patch.text : '', targetCwd);
463
+ if (updateScan.blockResponse)
464
+ return { response: updateScan.blockResponse };
465
+ const result = updateEntity(entity, id, patch, targetCwd);
466
+ appendAuditEntry({ actor: agent_name, ...(agent_id ? { actor_id: agent_id } : {}), action: 'update', item_id: id, item_type: entity }, targetCwd);
467
+ const updateText = `✔ updated ${entity} ${id}`;
468
+ const updateContent = auto_repair
469
+ ? [{ type: 'text', text: updateText }, { type: 'text', text: renderAutoRepairWarning(auto_repair, agent_name) }]
470
+ : [{ type: 'text', text: updateText }];
471
+ return {
472
+ response: appendSecurityWarnings(toolResponse({
473
+ content: updateContent,
474
+ structuredContent: {
475
+ ...result,
476
+ resolved_project: targetScope.resolved_project,
477
+ active_source: targetScope.active_source,
478
+ ...(auto_repair ? { auto_repair } : {}),
479
+ },
480
+ }), updateScan.warnings),
481
+ };
482
+ }
483
+ catch (error) {
484
+ return { response: createToolErrorResponse('validation_error', error.message) };
485
+ }
486
+ }
487
+ export function handleBclawRemove(payload, ctx) {
488
+ const { args, cwd, connectionSessionId } = payload;
489
+ try {
490
+ const entity = String(args.entity ?? '');
491
+ const id = String(args.id ?? '');
492
+ const purge = args.purge === true;
493
+ const targetCwd = resolveProjectCwd(args.project, cwd);
494
+ const targetScope = scopeMetadataForTarget(args, targetCwd, ctx.scopeInfo);
495
+ const { agent_name, agent_id, auto_repair } = resolveCanonicalAuthor(args, cwd, connectionSessionId);
496
+ const result = removeEntity(entity, id, targetCwd, purge);
497
+ appendAuditEntry({ actor: agent_name, ...(agent_id ? { actor_id: agent_id } : {}), action: 'delete', item_id: id, item_type: entity, reason: purge ? 'purged' : 'archived' }, targetCwd);
498
+ const removeText = `✔ removed ${entity} ${id}`;
499
+ const removeContent = auto_repair
500
+ ? [{ type: 'text', text: removeText }, { type: 'text', text: renderAutoRepairWarning(auto_repair, agent_name) }]
501
+ : [{ type: 'text', text: removeText }];
502
+ return {
503
+ response: toolResponse({
504
+ content: removeContent,
505
+ structuredContent: {
506
+ ...result,
507
+ resolved_project: targetScope.resolved_project,
508
+ active_source: targetScope.active_source,
509
+ ...(auto_repair ? { auto_repair } : {}),
510
+ },
511
+ }),
512
+ };
513
+ }
514
+ catch (error) {
515
+ return { response: createToolErrorResponse('validation_error', error.message) };
516
+ }
517
+ }
518
+ export function handleBclawMove(payload, _ctx) {
519
+ const { args, cwd, connectionSessionId } = payload;
520
+ try {
521
+ const entity = String(args.entity ?? '');
522
+ const id = String(args.id ?? '');
523
+ const toProject = String(args.to_project ?? '');
524
+ const fromProject = typeof args.from_project === 'string' ? args.from_project : undefined;
525
+ const force = args.force === true;
526
+ const { agent_name, agent_id, auto_repair } = resolveCanonicalAuthor(args, cwd, connectionSessionId);
527
+ const result = relocateEntity({ entity, id, toProject, fromProject, force, cwd, actor: agent_name, actorId: agent_id });
528
+ const warn = result.warnings.length ? ` (${result.warnings.length} warning(s))` : '';
529
+ const moveText = `✔ moved ${entity} ${id} → ${result.to}${warn}`;
530
+ const moveContent = auto_repair
531
+ ? [{ type: 'text', text: moveText }, { type: 'text', text: renderAutoRepairWarning(auto_repair, agent_name) }]
532
+ : [{ type: 'text', text: moveText }];
533
+ return {
534
+ response: toolResponse({
535
+ content: moveContent,
536
+ structuredContent: {
537
+ ...result,
538
+ ...(auto_repair ? { auto_repair } : {}),
539
+ },
540
+ }),
541
+ };
542
+ }
543
+ catch (error) {
544
+ return { response: createToolErrorResponse('validation_error', error.message) };
545
+ }
546
+ }
547
+ export function handleBclawTransition(payload, ctx) {
548
+ const { args, cwd, connectionSessionId } = payload;
549
+ try {
550
+ const entity = String(args.entity ?? '');
551
+ // Same auto-localize as bclaw_create: a workspace sibling named by
552
+ // project=X is switched into and transitioned locally; only federated
553
+ // links / unknown names are blocked (signaling-only boundary).
554
+ let targetCwd;
555
+ let autoSwitched = false;
556
+ if (entity === 'claim' || entity === 'plan') {
557
+ const loc = ctx.resolveExecutionWriteTarget(entity, args, cwd, connectionSessionId);
558
+ if (loc.block)
559
+ return { response: loc.block };
560
+ targetCwd = loc.targetCwd;
561
+ autoSwitched = loc.autoSwitched;
562
+ }
563
+ else {
564
+ targetCwd = resolveProjectCwd(args.project, cwd);
565
+ }
566
+ const id = String(args.id ?? '');
567
+ const to = String(args.to ?? '');
568
+ const reason = args.reason;
569
+ const targetScope = scopeMetadataForTarget(args, targetCwd, ctx.scopeInfo);
570
+ const { agent_name, agent_id, auto_repair } = resolveCanonicalAuthor(args, cwd, connectionSessionId);
571
+ // trp#928 — claim transitions consume the ReleaseClaimAuth ownership
572
+ // check (released/stale both mutate a claim owned by SOME agent). Reuse
573
+ // the same coordinator_override opt-in as bclaw_release_claim so both
574
+ // paths have identical trust semantics and the same executable error.
575
+ let transitionAuth;
576
+ if (entity === 'claim') {
577
+ const transitionIdentity = resolveMutationIdentity(args, { nameField: 'agent', idField: 'agentId' }, targetCwd, connectionSessionId);
578
+ const coordinatorOverrideRequested = args.coordinator_override === true;
579
+ if (coordinatorOverrideRequested) {
580
+ const identity = 'identity' in transitionIdentity ? transitionIdentity.identity : undefined;
581
+ const trustLevel = identity?.trust_level ?? 'contributor';
582
+ if (!hasMinimumTrustLevel(trustLevel, 'trusted')) {
583
+ return {
584
+ response: createToolErrorResponse('trust_error', `coordinator_override:true requires trust_level 'trusted' or higher — caller is '${trustLevel}'.`),
585
+ };
586
+ }
587
+ }
588
+ transitionAuth = 'identity' in transitionIdentity && transitionIdentity.identity
589
+ ? {
590
+ agent: transitionIdentity.identity.agent_name,
591
+ agent_id: transitionIdentity.identity.agent_id,
592
+ session_id: connectionSessionId,
593
+ override: coordinatorOverrideRequested,
594
+ }
595
+ : undefined;
596
+ }
597
+ const result = transitionEntity(entity, id, to, targetCwd, reason, transitionAuth);
598
+ appendAuditEntry({ actor: agent_name, ...(agent_id ? { actor_id: agent_id } : {}), action: 'update', item_id: id, item_type: entity, reason: `transition ${result.from} → ${to}${reason ? ` (${reason})` : ''}` }, targetCwd);
599
+ const transitionText = `✔ ${entity} ${id}: ${result.from} → ${to}${autoSwitched ? ` (auto-switched → ${targetScope.resolved_project.name ?? targetScope.resolved_project.path})` : ''}`;
600
+ const transitionContent = auto_repair
601
+ ? [{ type: 'text', text: transitionText }, { type: 'text', text: renderAutoRepairWarning(auto_repair, agent_name) }]
602
+ : [{ type: 'text', text: transitionText }];
603
+ return {
604
+ response: toolResponse({
605
+ content: transitionContent,
606
+ structuredContent: {
607
+ ...result,
608
+ resolved_project: targetScope.resolved_project,
609
+ active_source: autoSwitched ? 'auto_switch' : targetScope.active_source,
610
+ ...(autoSwitched ? { auto_switched: true } : {}),
611
+ ...(auto_repair ? { auto_repair } : {}),
612
+ },
613
+ }),
614
+ };
615
+ }
616
+ catch (error) {
617
+ return { response: createToolErrorResponse('validation_error', error.message) };
618
+ }
619
+ }
620
+ //# sourceMappingURL=mcp-write-entities.js.map