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.
- package/README.md +16 -263
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/cli/register-capture.js +209 -0
- package/dist/cli/register-code-map.js +19 -0
- package/dist/cli/register-coordination.js +472 -0
- package/dist/cli/register-federation.js +258 -0
- package/dist/cli/register-lifecycle.js +436 -0
- package/dist/cli/register-memory-context.js +502 -0
- package/dist/cli/register-planning.js +167 -0
- package/dist/cli/register-review.js +149 -0
- package/dist/cli/shared.js +5 -0
- package/dist/cli.js +212 -2015
- package/dist/commands/dispatch-watch.js +25 -2
- package/dist/commands/harvest.js +31 -6
- package/dist/commands/mcp-catalog.js +1438 -0
- package/dist/commands/mcp-contract.js +33 -0
- package/dist/commands/mcp-presentation.js +27 -0
- package/dist/commands/mcp-read-handlers.js +72 -36
- package/dist/commands/mcp-write-admin.js +328 -0
- package/dist/commands/mcp-write-claims.js +864 -0
- package/dist/commands/mcp-write-coordination.js +1825 -0
- package/dist/commands/mcp-write-entities.js +620 -0
- package/dist/commands/mcp-write-memory.js +451 -0
- package/dist/commands/mcp-write-sequences.js +116 -0
- package/dist/commands/mcp-write-support.js +367 -0
- package/dist/commands/mcp.js +261 -5570
- package/dist/commands/update-handoff.js +28 -42
- package/dist/core/agent-capability.js +31 -14
- package/dist/core/agent-files.js +1 -1
- package/dist/core/agent-registry.js +51 -3
- package/dist/core/claims.js +18 -0
- package/dist/core/coordination.js +5 -2
- package/dist/core/cross-project.js +35 -1
- package/dist/core/dispatcher.js +34 -20
- package/dist/core/entity-operations.js +335 -12
- package/dist/core/entity-registry.js +72 -9
- package/dist/core/execution.js +28 -4
- package/dist/core/facade-schema.js +30 -4
- package/dist/core/federation-cloud.js +142 -11
- package/dist/core/federation-outbox.js +292 -0
- package/dist/core/federation-signing.js +115 -0
- package/dist/core/handoff-review.js +35 -0
- package/dist/core/io.js +6 -0
- package/dist/core/protocol-tool-policy.js +113 -0
- package/dist/core/review-loop-close.js +115 -0
- package/dist/core/schema.js +25 -2
- package/dist/core/security-detectors.js +35 -6
- package/dist/core/security.js +32 -12
- package/dist/core/worktree.js +98 -9
- package/dist/facts.js +13 -11
- package/dist/facts.json +12 -10
- package/docs/PROTOCOL.md +7 -3
- package/docs/concepts/coordinator-runbook.md +3 -0
- package/docs/concepts/dispatch-lifecycle.md +4 -4
- package/docs/concepts/loop-engine.md +3 -1
- package/docs/concepts/troubleshooting.md +1 -1
- package/docs/integrations/codex.md +3 -3
- package/docs/integrations/overview.md +1 -1
- package/docs/mcp-schema-changelog.md +153 -2
- package/docs/playbooks/orchestration.md +1 -1
- package/docs/product/entity-model-audit.md +3 -2
- package/docs/security.md +22 -1
- 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
|