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,451 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP memory write-tool handlers.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from mcp.ts (pln#622 PR4) — mechanical move of the note /
|
|
5
|
+
* quick-capture / compaction / memory-item mutation / candidate-harvest write
|
|
6
|
+
* handlers. Behavior is unchanged; each handler receives the tool-call payload
|
|
7
|
+
* plus a {@link McpWriteMemoryContext} carrying the model resolved once per
|
|
8
|
+
* write call plus the two mcp.ts-local helpers these handlers reference
|
|
9
|
+
* (session-id-from-env and the cross-project arg reader), passed by reference
|
|
10
|
+
* so this module never imports ./mcp.js (dependency-direction guard, PR1).
|
|
11
|
+
*
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
import { writeCrossProjectSignal, resolveCrossProjectWritableTarget } from '../core/cross-project.js';
|
|
15
|
+
import { appendAuditEntry } from '../core/audit.js';
|
|
16
|
+
import { nowISO, generateId } from '../core/ids.js';
|
|
17
|
+
import { buildOperationalIdentity } from '../core/identity.js';
|
|
18
|
+
import { validateMcpInput, validateMcpField } from '../core/input-validation.js';
|
|
19
|
+
import { deleteMemoryItem, updateMemoryItem } from '../core/operations/memory-mutation.js';
|
|
20
|
+
import { assessMemoryPressure, buildCompactionTemplate, applyCompaction } from '../core/gc-semantic.js';
|
|
21
|
+
import { createRuntimeNote } from './runtime-note.js';
|
|
22
|
+
import { createCandidateFromInput } from './reflect.js';
|
|
23
|
+
import { harvestCandidates } from './harvest.js';
|
|
24
|
+
import { ensureTrust, scanMcpWriteText, appendSecurityWarnings } from './mcp-write-support.js';
|
|
25
|
+
import { toolResponse, createToolErrorResponse, } from './mcp-contract.js';
|
|
26
|
+
function scoreKeywordMatches(text, patterns) {
|
|
27
|
+
return patterns.reduce((score, pattern) => score + (pattern.test(text) ? 1 : 0), 0);
|
|
28
|
+
}
|
|
29
|
+
function classifyQuickCapture(text) {
|
|
30
|
+
const normalized = text.trim().toLowerCase();
|
|
31
|
+
const decisionPatterns = [
|
|
32
|
+
/\b(decide|decision|decided|prefer|preferred|policy|convention|standard|standardize|adopt|chosen|choose|settled on)\b/,
|
|
33
|
+
/\b(use|default to|go with|route through|switch to|migrate to|move to)\b/,
|
|
34
|
+
/^(use|prefer|adopt|standardize|route|switch|migrate)\b/,
|
|
35
|
+
];
|
|
36
|
+
const trapPatterns = [
|
|
37
|
+
/\b(trap|warning|beware|avoid|never|don't|do not|risk|risky|gotcha|workaround)\b/,
|
|
38
|
+
/\b(bug|broken|breaks|failure|fails|failing|flaky|blocked|missing|crash|regression|timeout|deadlock|race condition|leak)\b/,
|
|
39
|
+
/\b(error|incident|problem|issue|hang|stuck|retry)\b/,
|
|
40
|
+
];
|
|
41
|
+
const decisionScore = scoreKeywordMatches(normalized, decisionPatterns);
|
|
42
|
+
const trapScore = scoreKeywordMatches(normalized, trapPatterns);
|
|
43
|
+
if (decisionScore > 0 && trapScore > 0 && Math.abs(decisionScore - trapScore) <= 1) {
|
|
44
|
+
return {
|
|
45
|
+
target: 'note',
|
|
46
|
+
reason: 'ambiguous_keywords',
|
|
47
|
+
decisionScore,
|
|
48
|
+
trapScore,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if (trapScore >= 2 && trapScore >= decisionScore + 1) {
|
|
52
|
+
return {
|
|
53
|
+
target: 'trap',
|
|
54
|
+
reason: 'trap_keywords',
|
|
55
|
+
decisionScore,
|
|
56
|
+
trapScore,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
if (decisionScore >= 2 && decisionScore >= trapScore + 1) {
|
|
60
|
+
return {
|
|
61
|
+
target: 'decision',
|
|
62
|
+
reason: 'decision_keywords',
|
|
63
|
+
decisionScore,
|
|
64
|
+
trapScore,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
target: 'note',
|
|
69
|
+
reason: decisionScore === trapScore && decisionScore > 0 ? 'ambiguous_keywords' : 'low_confidence',
|
|
70
|
+
decisionScore,
|
|
71
|
+
trapScore,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function formatQuickCaptureNoteText(text, context) {
|
|
75
|
+
if (!context) {
|
|
76
|
+
return text;
|
|
77
|
+
}
|
|
78
|
+
return `${text}\n\nContext: ${context}`;
|
|
79
|
+
}
|
|
80
|
+
// ── Handlers ─────────────────────────────────────────────────────────────
|
|
81
|
+
export function handleBclawWriteNote(payload, ctx) {
|
|
82
|
+
const { args, cwd, connectionSessionId } = payload;
|
|
83
|
+
const resolved = ensureTrust(args, { nameField: 'agent', idField: 'agentId' }, 'contributor', cwd, connectionSessionId);
|
|
84
|
+
if (resolved.error) {
|
|
85
|
+
return { response: createToolErrorResponse(resolved.error.kind, resolved.error.message, resolved.error.details) };
|
|
86
|
+
}
|
|
87
|
+
const text = String(args.text ?? '');
|
|
88
|
+
const tags = args.tags ?? [];
|
|
89
|
+
const inputValidation = validateMcpInput(text, tags);
|
|
90
|
+
if (!inputValidation.ok) {
|
|
91
|
+
return { response: createToolErrorResponse('validation_error', inputValidation.errors[0]?.message ?? 'Invalid input', inputValidation.errors) };
|
|
92
|
+
}
|
|
93
|
+
const identity = resolved.identity;
|
|
94
|
+
// S2 (pln#623): scan write text on the MCP path, same control point as the
|
|
95
|
+
// CLI adapters. A strict-mode block refuses the write; warn-mode matches are
|
|
96
|
+
// surfaced on the response.
|
|
97
|
+
const scan = scanMcpWriteText(text, cwd);
|
|
98
|
+
if (scan.blockResponse)
|
|
99
|
+
return { response: scan.blockResponse };
|
|
100
|
+
// Cross-project push
|
|
101
|
+
const crossProjectTarget = ctx.getCrossProjectArg(args, 'crossProject', 'cross_project');
|
|
102
|
+
if (crossProjectTarget) {
|
|
103
|
+
try {
|
|
104
|
+
const opIdentity = buildOperationalIdentity(identity.agent_name, cwd, {
|
|
105
|
+
agentId: identity.agent_id,
|
|
106
|
+
sessionId: connectionSessionId,
|
|
107
|
+
});
|
|
108
|
+
const signal = writeCrossProjectSignal(resolveCrossProjectWritableTarget(crossProjectTarget, 'runtime_note', cwd), 'runtime_note', {
|
|
109
|
+
schema_version: 2,
|
|
110
|
+
id: generateId('runtime_note'),
|
|
111
|
+
agent: opIdentity.agent,
|
|
112
|
+
agent_id: opIdentity.agent_id,
|
|
113
|
+
project_id: opIdentity.project_id ?? '',
|
|
114
|
+
session_id: opIdentity.session_id,
|
|
115
|
+
text,
|
|
116
|
+
created_at: nowISO(),
|
|
117
|
+
tags,
|
|
118
|
+
visibility: 'shared',
|
|
119
|
+
host_id: opIdentity.host_id ?? '',
|
|
120
|
+
note_type: 'observation',
|
|
121
|
+
}, cwd);
|
|
122
|
+
return {
|
|
123
|
+
response: appendSecurityWarnings(toolResponse({
|
|
124
|
+
content: [{ type: 'text', text: `✔ Cross-project runtime note signaled to '${signal.target_project.name}' [${signal.id}]` }],
|
|
125
|
+
signal_id: signal.id,
|
|
126
|
+
entity_type: signal.entity_type,
|
|
127
|
+
note_id: signal.payload.id,
|
|
128
|
+
target_project: signal.target_project.name,
|
|
129
|
+
target_path: signal.target_project.path,
|
|
130
|
+
}), scan.warnings),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
catch (e) {
|
|
134
|
+
return { response: createToolErrorResponse('validation_error', e instanceof Error ? e.message : String(e)) };
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const result = createRuntimeNote(text, {
|
|
138
|
+
agent: identity.agent_name,
|
|
139
|
+
agentId: identity.agent_id,
|
|
140
|
+
tag: tags,
|
|
141
|
+
visibility: args.visibility ?? 'shared',
|
|
142
|
+
ttl: args.ttl,
|
|
143
|
+
autoReflect: args.autoReflect,
|
|
144
|
+
cwd,
|
|
145
|
+
sessionId: connectionSessionId,
|
|
146
|
+
model: ctx.currentModel,
|
|
147
|
+
}, false);
|
|
148
|
+
return {
|
|
149
|
+
response: appendSecurityWarnings(toolResponse({
|
|
150
|
+
content: [{ type: 'text', text: `✔ Note created [${result.noteId}]` }],
|
|
151
|
+
note_id: result.noteId,
|
|
152
|
+
session_id: result.sessionId,
|
|
153
|
+
auto_reflect_attempted: result.autoReflectAttempted,
|
|
154
|
+
detected_type: result.detectedType,
|
|
155
|
+
candidate_id: result.candidateId,
|
|
156
|
+
promoted_item_id: result.promotedItemId,
|
|
157
|
+
skip_reason: result.skipReason,
|
|
158
|
+
}), scan.warnings),
|
|
159
|
+
nextConnectionSessionId: ctx.explicitSessionIdFromEnv() ? undefined : result.sessionId,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
export function handleBclawQuickCapture(payload, ctx) {
|
|
163
|
+
const { args, cwd, connectionSessionId } = payload;
|
|
164
|
+
const resolved = ensureTrust(args, { nameField: 'agent', idField: 'agentId' }, 'contributor', cwd, connectionSessionId);
|
|
165
|
+
if (resolved.error) {
|
|
166
|
+
return { response: createToolErrorResponse(resolved.error.kind, resolved.error.message, resolved.error.details) };
|
|
167
|
+
}
|
|
168
|
+
const text = String(args.text ?? '');
|
|
169
|
+
const inputValidation = validateMcpInput(text);
|
|
170
|
+
if (!inputValidation.ok) {
|
|
171
|
+
return { response: createToolErrorResponse('validation_error', inputValidation.errors[0]?.message ?? 'Invalid input', inputValidation.errors) };
|
|
172
|
+
}
|
|
173
|
+
// S2 (pln#623): scan the captured text (same control point as the CLI).
|
|
174
|
+
const scan = scanMcpWriteText(text, cwd);
|
|
175
|
+
if (scan.blockResponse)
|
|
176
|
+
return { response: scan.blockResponse };
|
|
177
|
+
const context = typeof args.context === 'string' ? args.context.trim() : undefined;
|
|
178
|
+
if (context) {
|
|
179
|
+
const contextCheck = validateMcpField(context, 'context');
|
|
180
|
+
if (!contextCheck.ok) {
|
|
181
|
+
return { response: createToolErrorResponse('validation_error', contextCheck.message) };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const identity = resolved.identity;
|
|
185
|
+
// Caller-asserted classification wins (pln#542): the calling agent
|
|
186
|
+
// declares decision/trap/constraint/note; keyword heuristics are the
|
|
187
|
+
// fallback when no type is given.
|
|
188
|
+
const assertedType = typeof args.type === 'string' ? args.type.trim().toLowerCase() : undefined;
|
|
189
|
+
let classification;
|
|
190
|
+
if (assertedType === 'decision' || assertedType === 'trap' || assertedType === 'constraint' || assertedType === 'note') {
|
|
191
|
+
classification = { target: assertedType, reason: 'caller_asserted', decisionScore: 0, trapScore: 0 };
|
|
192
|
+
}
|
|
193
|
+
else if (assertedType !== undefined) {
|
|
194
|
+
return { response: createToolErrorResponse('validation_error', `type must be one of: decision, trap, constraint, note (got '${assertedType}')`) };
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
classification = classifyQuickCapture(text);
|
|
198
|
+
}
|
|
199
|
+
if (classification.target === 'note') {
|
|
200
|
+
const result = createRuntimeNote(formatQuickCaptureNoteText(text, context), {
|
|
201
|
+
agent: identity.agent_name,
|
|
202
|
+
agentId: identity.agent_id,
|
|
203
|
+
tag: ['quick-capture'],
|
|
204
|
+
visibility: 'shared',
|
|
205
|
+
cwd,
|
|
206
|
+
sessionId: connectionSessionId,
|
|
207
|
+
model: ctx.currentModel,
|
|
208
|
+
}, false);
|
|
209
|
+
return {
|
|
210
|
+
response: appendSecurityWarnings(toolResponse({
|
|
211
|
+
content: [{ type: 'text', text: `✔ Quick capture saved as runtime note [${result.noteId}]` }],
|
|
212
|
+
structuredContent: {
|
|
213
|
+
classification: classification.target,
|
|
214
|
+
classification_reason: classification.reason,
|
|
215
|
+
decision_score: classification.decisionScore,
|
|
216
|
+
trap_score: classification.trapScore,
|
|
217
|
+
note_id: result.noteId,
|
|
218
|
+
session_id: result.sessionId,
|
|
219
|
+
context,
|
|
220
|
+
next_actions: [
|
|
221
|
+
{ tool: 'bclaw_quick_capture', args: { text: '<same text>', type: 'decision' }, when: 'this was actually a durable decision/trap/constraint — re-capture with an asserted type' },
|
|
222
|
+
],
|
|
223
|
+
},
|
|
224
|
+
}), scan.warnings),
|
|
225
|
+
nextConnectionSessionId: ctx.explicitSessionIdFromEnv() ? undefined : result.sessionId,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
const capture = createCandidateFromInput(text, classification.target, {
|
|
229
|
+
tag: ['quick-capture'],
|
|
230
|
+
author: identity.agent_name,
|
|
231
|
+
authorId: identity.agent_id,
|
|
232
|
+
sessionId: connectionSessionId,
|
|
233
|
+
source: 'mcp:quick-capture',
|
|
234
|
+
path: context,
|
|
235
|
+
cwd,
|
|
236
|
+
}, false, false, true);
|
|
237
|
+
const statusText = capture.writeThrough
|
|
238
|
+
? `✔ Quick capture promoted as ${classification.target} [${capture.promotedItemId}]`
|
|
239
|
+
: `✔ Quick capture saved as ${classification.target} candidate [${capture.candidateId}]`;
|
|
240
|
+
const captureNextActions = capture.writeThrough
|
|
241
|
+
? [
|
|
242
|
+
{ tool: 'bclaw_get', args: { entity: classification.target, id: capture.promotedItemId }, when: 'to verify the promoted item' },
|
|
243
|
+
]
|
|
244
|
+
: [
|
|
245
|
+
{ tool: 'bclaw_get', args: { entity: 'candidate', id: capture.candidateId }, when: 'to review the pending candidate (contradiction metadata is advisory)' },
|
|
246
|
+
];
|
|
247
|
+
return {
|
|
248
|
+
response: appendSecurityWarnings(toolResponse({
|
|
249
|
+
content: [{ type: 'text', text: statusText }],
|
|
250
|
+
structuredContent: {
|
|
251
|
+
classification: classification.target,
|
|
252
|
+
classification_reason: classification.reason,
|
|
253
|
+
decision_score: classification.decisionScore,
|
|
254
|
+
trap_score: classification.trapScore,
|
|
255
|
+
candidate_id: capture.candidateId,
|
|
256
|
+
promoted_item_id: capture.promotedItemId,
|
|
257
|
+
write_through: capture.writeThrough,
|
|
258
|
+
// Advisory only (cnd_abe61d68): contradictions are metadata on
|
|
259
|
+
// the candidate, never a promotion blocker.
|
|
260
|
+
contradiction_summary: capture.contradictionSummary,
|
|
261
|
+
contradictions_detected: capture.contradictionsDetected?.map((item) => ({
|
|
262
|
+
severity: item.severity,
|
|
263
|
+
reason: item.reason,
|
|
264
|
+
conflicts_with: item.conflicts_with,
|
|
265
|
+
})),
|
|
266
|
+
context,
|
|
267
|
+
next_actions: captureNextActions,
|
|
268
|
+
},
|
|
269
|
+
}), scan.warnings),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
export function handleBclawCompact(payload, _ctx) {
|
|
273
|
+
const { args, cwd, connectionSessionId } = payload;
|
|
274
|
+
const resolved = ensureTrust(args, { nameField: 'agent', idField: 'agentId' }, 'trusted', cwd, connectionSessionId);
|
|
275
|
+
if (resolved.error) {
|
|
276
|
+
return { response: createToolErrorResponse(resolved.error.kind, resolved.error.message, resolved.error.details) };
|
|
277
|
+
}
|
|
278
|
+
const archiveIds = args.archiveIds;
|
|
279
|
+
const isPhase2 = archiveIds && archiveIds.length > 0;
|
|
280
|
+
if (isPhase2) {
|
|
281
|
+
// Phase 2: apply compaction — archive specified items and create new memories
|
|
282
|
+
const result = applyCompaction({
|
|
283
|
+
archiveIds,
|
|
284
|
+
newItems: args.newItems,
|
|
285
|
+
author: resolved.identity?.agent_name,
|
|
286
|
+
authorId: resolved.identity?.agent_id,
|
|
287
|
+
cwd,
|
|
288
|
+
});
|
|
289
|
+
const lines = [];
|
|
290
|
+
lines.push(`✔ Compacted ${result.archived_count} item(s).`);
|
|
291
|
+
if (result.created_count > 0) {
|
|
292
|
+
lines.push(`Created ${result.created_count} new memory item(s): ${result.created_ids.join(', ')}`);
|
|
293
|
+
}
|
|
294
|
+
lines.push(`Backup: ${result.backup_path}`);
|
|
295
|
+
return {
|
|
296
|
+
response: toolResponse({
|
|
297
|
+
content: [{ type: 'text', text: lines.join('\n') }],
|
|
298
|
+
...result,
|
|
299
|
+
}),
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
// Phase 1: assess pressure and return compaction template
|
|
303
|
+
const assessment = assessMemoryPressure(cwd);
|
|
304
|
+
const maxItems = args.maxItems ?? 20;
|
|
305
|
+
const selected = assessment.eligible_items.slice(0, maxItems);
|
|
306
|
+
const template = selected.length > 0 ? buildCompactionTemplate(selected) : undefined;
|
|
307
|
+
const lines = [];
|
|
308
|
+
lines.push(`Memory pressure: ${assessment.pressure ? 'YES' : 'no'} (${assessment.done_plans} done plans, ${assessment.closed_handoffs} closed handoffs)`);
|
|
309
|
+
lines.push(`Thresholds: plans >= ${assessment.thresholds.plans}, handoffs >= ${assessment.thresholds.handoffs}`);
|
|
310
|
+
lines.push(`Eligible items: ${assessment.eligible_items.length}`);
|
|
311
|
+
if (template) {
|
|
312
|
+
lines.push('');
|
|
313
|
+
lines.push(template);
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
lines.push('No items eligible for compaction.');
|
|
317
|
+
}
|
|
318
|
+
return {
|
|
319
|
+
response: toolResponse({
|
|
320
|
+
content: [{ type: 'text', text: lines.join('\n') }],
|
|
321
|
+
pressure: assessment.pressure,
|
|
322
|
+
done_plans: assessment.done_plans,
|
|
323
|
+
closed_handoffs: assessment.closed_handoffs,
|
|
324
|
+
eligible_count: assessment.eligible_items.length,
|
|
325
|
+
eligible_ids: selected.map(i => i.id),
|
|
326
|
+
}),
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
export function handleBclawDeleteMemory(payload, _ctx) {
|
|
330
|
+
const { args, cwd, connectionSessionId } = payload;
|
|
331
|
+
const resolved = ensureTrust(args, { nameField: 'agent', idField: 'agentId' }, 'trusted', cwd, connectionSessionId);
|
|
332
|
+
if (resolved.error) {
|
|
333
|
+
return { response: createToolErrorResponse(resolved.error.kind, resolved.error.message, resolved.error.details) };
|
|
334
|
+
}
|
|
335
|
+
const itemId = String(args.id ?? '').trim();
|
|
336
|
+
const itemType = String(args.type ?? '').trim();
|
|
337
|
+
if (!itemId)
|
|
338
|
+
return { response: createToolErrorResponse('validation_error', 'Missing required argument: id') };
|
|
339
|
+
if (!itemType)
|
|
340
|
+
return { response: createToolErrorResponse('validation_error', 'Missing required argument: type') };
|
|
341
|
+
if (!['constraint', 'decision', 'trap'].includes(itemType)) {
|
|
342
|
+
return { response: createToolErrorResponse('validation_error', `Invalid type: ${itemType}`) };
|
|
343
|
+
}
|
|
344
|
+
try {
|
|
345
|
+
const result = deleteMemoryItem(itemId, itemType, cwd);
|
|
346
|
+
appendAuditEntry({ actor: resolved.identity.agent_name, actor_id: resolved.identity.agent_id, action: 'delete', item_id: itemId, item_type: itemType }, cwd);
|
|
347
|
+
return {
|
|
348
|
+
response: toolResponse({
|
|
349
|
+
content: [{ type: 'text', text: `✔ Deleted [${itemId}] (${itemType})` }],
|
|
350
|
+
deleted_id: result.deletedId,
|
|
351
|
+
item_type: result.itemType,
|
|
352
|
+
store_level: result.storeRole,
|
|
353
|
+
}),
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
catch (err) {
|
|
357
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
358
|
+
if (msg.includes('not found')) {
|
|
359
|
+
return { response: createToolErrorResponse('not_found', msg) };
|
|
360
|
+
}
|
|
361
|
+
return { response: createToolErrorResponse('operation_error', msg) };
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
export function handleBclawUpdateMemory(payload, _ctx) {
|
|
365
|
+
const { args, cwd, connectionSessionId } = payload;
|
|
366
|
+
const resolved = ensureTrust(args, { nameField: 'agent', idField: 'agentId' }, 'trusted', cwd, connectionSessionId);
|
|
367
|
+
if (resolved.error) {
|
|
368
|
+
return { response: createToolErrorResponse(resolved.error.kind, resolved.error.message, resolved.error.details) };
|
|
369
|
+
}
|
|
370
|
+
const itemId = String(args.id ?? '').trim();
|
|
371
|
+
const itemType = String(args.type ?? '').trim();
|
|
372
|
+
const newText = args.text ? String(args.text).trim() : undefined;
|
|
373
|
+
const newTags = Array.isArray(args.tags) ? args.tags.map((t) => String(t).trim()) : undefined;
|
|
374
|
+
const newStatus = args.status ? String(args.status).trim() : undefined;
|
|
375
|
+
const moveToStore = args.moveToStore ? String(args.moveToStore).trim() : undefined;
|
|
376
|
+
if (!itemId)
|
|
377
|
+
return { response: createToolErrorResponse('validation_error', 'Missing required argument: id') };
|
|
378
|
+
if (!itemType)
|
|
379
|
+
return { response: createToolErrorResponse('validation_error', 'Missing required argument: type') };
|
|
380
|
+
if (!['constraint', 'decision', 'trap'].includes(itemType)) {
|
|
381
|
+
return { response: createToolErrorResponse('validation_error', `Invalid type for update: ${itemType}`) };
|
|
382
|
+
}
|
|
383
|
+
if (!newText && !newTags && !newStatus && !moveToStore) {
|
|
384
|
+
return { response: createToolErrorResponse('validation_error', 'At least one of text, tags, status, or moveToStore must be provided') };
|
|
385
|
+
}
|
|
386
|
+
if (moveToStore && !['local', 'repo', 'workspace', 'user'].includes(moveToStore)) {
|
|
387
|
+
return { response: createToolErrorResponse('validation_error', `Invalid moveToStore target: ${moveToStore}`) };
|
|
388
|
+
}
|
|
389
|
+
if (newStatus && itemType !== 'trap') {
|
|
390
|
+
return { response: createToolErrorResponse('validation_error', 'status updates are only supported for traps') };
|
|
391
|
+
}
|
|
392
|
+
if (newStatus && !['active', 'resolved', 'expired'].includes(newStatus)) {
|
|
393
|
+
return { response: createToolErrorResponse('validation_error', `Invalid trap status: ${newStatus}`) };
|
|
394
|
+
}
|
|
395
|
+
try {
|
|
396
|
+
const result = updateMemoryItem({
|
|
397
|
+
id: itemId,
|
|
398
|
+
type: itemType,
|
|
399
|
+
text: newText,
|
|
400
|
+
tags: newTags,
|
|
401
|
+
status: newStatus,
|
|
402
|
+
moveToStore: moveToStore,
|
|
403
|
+
}, cwd);
|
|
404
|
+
appendAuditEntry({ actor: resolved.identity.agent_name, actor_id: resolved.identity.agent_id, action: 'update', item_id: itemId, item_type: itemType }, cwd);
|
|
405
|
+
return {
|
|
406
|
+
response: toolResponse({
|
|
407
|
+
content: [{ type: 'text', text: `✔ Updated [${itemId}] (${itemType})` }],
|
|
408
|
+
updated_id: result.updatedId,
|
|
409
|
+
item_type: result.itemType,
|
|
410
|
+
previous_store: result.previousStore,
|
|
411
|
+
new_store: result.newStore,
|
|
412
|
+
}),
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
catch (err) {
|
|
416
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
417
|
+
if (msg.includes('not found')) {
|
|
418
|
+
return { response: createToolErrorResponse('not_found', msg) };
|
|
419
|
+
}
|
|
420
|
+
return { response: createToolErrorResponse('operation_error', msg) };
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
export function handleBclawHarvestCandidates(payload, _ctx) {
|
|
424
|
+
const { args, cwd, connectionSessionId } = payload;
|
|
425
|
+
const resolved = ensureTrust(args, { nameField: 'agent', idField: 'agentId' }, 'trusted', cwd, connectionSessionId);
|
|
426
|
+
if (resolved.error) {
|
|
427
|
+
return { response: createToolErrorResponse(resolved.error.kind, resolved.error.message, resolved.error.details) };
|
|
428
|
+
}
|
|
429
|
+
const resolvedIdentity = resolved.identity;
|
|
430
|
+
const worktreePaths = Array.isArray(args.worktreePaths) ? args.worktreePaths : undefined;
|
|
431
|
+
const dryRun = args.dryRun === true;
|
|
432
|
+
const harvestResult = harvestCandidates({
|
|
433
|
+
worktreePaths,
|
|
434
|
+
dryRun,
|
|
435
|
+
cwd,
|
|
436
|
+
agent: resolvedIdentity.agent_name,
|
|
437
|
+
});
|
|
438
|
+
const dryTag = dryRun ? ' (dry-run)' : '';
|
|
439
|
+
const summary = `✔ Harvest complete${dryTag}: ${harvestResult.harvested.length} imported, ${harvestResult.skipped.length} skipped, ${harvestResult.errors.length} error(s).`;
|
|
440
|
+
return {
|
|
441
|
+
response: toolResponse({
|
|
442
|
+
content: [{ type: 'text', text: summary }],
|
|
443
|
+
harvested: harvestResult.harvested.length,
|
|
444
|
+
skipped: harvestResult.skipped.length,
|
|
445
|
+
errors: harvestResult.errors,
|
|
446
|
+
candidates: harvestResult.harvested.map((c) => ({ id: c.id, type: c.type })),
|
|
447
|
+
dry_run: dryRun,
|
|
448
|
+
}),
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
//# sourceMappingURL=mcp-write-memory.js.map
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP sequence write-tool handlers.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from mcp.ts (pln#622 PR4) — mechanical move of the
|
|
5
|
+
* create / update / delete sequence write handlers. Behavior is unchanged;
|
|
6
|
+
* each handler receives the tool-call payload plus a
|
|
7
|
+
* {@link McpWriteSequencesContext} carrying the model resolved once per write
|
|
8
|
+
* call at the assembly point.
|
|
9
|
+
*
|
|
10
|
+
* This module must never import ./mcp.js (dependency-direction guard,
|
|
11
|
+
* pln#622 PR1).
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
15
|
+
import { appendAuditEntry } from '../core/audit.js';
|
|
16
|
+
import { createSequence, updateSequence, deleteSequence } from '../core/sequence.js';
|
|
17
|
+
import { ensureTrust } from './mcp-write-support.js';
|
|
18
|
+
import { toolResponse, createToolErrorResponse, } from './mcp-contract.js';
|
|
19
|
+
export function handleBclawCreateSequence(payload, ctx) {
|
|
20
|
+
const { args, cwd, connectionSessionId } = payload;
|
|
21
|
+
const resolved = ensureTrust(args, { nameField: 'agent', idField: 'agentId' }, 'contributor', cwd, connectionSessionId);
|
|
22
|
+
if (resolved.error) {
|
|
23
|
+
return { response: createToolErrorResponse(resolved.error.kind, resolved.error.message, resolved.error.details) };
|
|
24
|
+
}
|
|
25
|
+
const sequenceName = String(args.name ?? '').trim();
|
|
26
|
+
if (!sequenceName) {
|
|
27
|
+
return { response: createToolErrorResponse('validation_error', 'Missing required argument: name') };
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const result = createSequence({
|
|
31
|
+
name: sequenceName,
|
|
32
|
+
description: args.description,
|
|
33
|
+
status: args.status,
|
|
34
|
+
owner: args.owner,
|
|
35
|
+
items: Array.isArray(args.items) ? args.items : [],
|
|
36
|
+
tags: Array.isArray(args.tags) ? args.tags : [],
|
|
37
|
+
author: resolved.identity.agent_name,
|
|
38
|
+
authorId: resolved.identity.agent_id,
|
|
39
|
+
model: ctx.currentModel,
|
|
40
|
+
}, cwd);
|
|
41
|
+
appendAuditEntry({ actor: resolved.identity.agent_name, actor_id: resolved.identity.agent_id, action: 'create', item_id: result.id, item_type: 'sequence' }, cwd);
|
|
42
|
+
return {
|
|
43
|
+
response: toolResponse({
|
|
44
|
+
content: [{ type: 'text', text: `✔ Sequence added: [${result.id}] ${sequenceName}` }],
|
|
45
|
+
sequence_id: result.id,
|
|
46
|
+
}),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
return { response: createToolErrorResponse('operation_error', err instanceof Error ? err.message : String(err)) };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export function handleBclawUpdateSequence(payload, _ctx) {
|
|
54
|
+
const { args, cwd, connectionSessionId } = payload;
|
|
55
|
+
const resolved = ensureTrust(args, { nameField: 'agent', idField: 'agentId' }, 'contributor', cwd, connectionSessionId);
|
|
56
|
+
if (resolved.error) {
|
|
57
|
+
return { response: createToolErrorResponse(resolved.error.kind, resolved.error.message, resolved.error.details) };
|
|
58
|
+
}
|
|
59
|
+
const sequenceId = String(args.id ?? '').trim();
|
|
60
|
+
if (!sequenceId) {
|
|
61
|
+
return { response: createToolErrorResponse('validation_error', 'Missing required argument: id') };
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
const result = updateSequence({
|
|
65
|
+
id: sequenceId,
|
|
66
|
+
name: args.name,
|
|
67
|
+
description: args.description,
|
|
68
|
+
status: args.status,
|
|
69
|
+
owner: args.owner,
|
|
70
|
+
items: Array.isArray(args.items) ? args.items : undefined,
|
|
71
|
+
tags: Array.isArray(args.tags) ? args.tags : undefined,
|
|
72
|
+
}, cwd);
|
|
73
|
+
appendAuditEntry({ actor: resolved.identity.agent_name, actor_id: resolved.identity.agent_id, action: 'update', item_id: result.id, item_type: 'sequence' }, cwd);
|
|
74
|
+
return {
|
|
75
|
+
response: toolResponse({
|
|
76
|
+
content: [{ type: 'text', text: `✔ Sequence updated: [${result.id}] ${result.name}` }],
|
|
77
|
+
sequence_id: result.id,
|
|
78
|
+
status: result.status,
|
|
79
|
+
}),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
84
|
+
if (msg.includes('not found')) {
|
|
85
|
+
return { response: createToolErrorResponse('not_found', msg) };
|
|
86
|
+
}
|
|
87
|
+
return { response: createToolErrorResponse('operation_error', msg) };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
export function handleBclawDeleteSequence(payload, _ctx) {
|
|
91
|
+
const { args, cwd, connectionSessionId } = payload;
|
|
92
|
+
const resolved = ensureTrust(args, { nameField: 'agent', idField: 'agentId' }, 'trusted', cwd, connectionSessionId);
|
|
93
|
+
if (resolved.error) {
|
|
94
|
+
return { response: createToolErrorResponse(resolved.error.kind, resolved.error.message, resolved.error.details) };
|
|
95
|
+
}
|
|
96
|
+
const dsqId = String(args.id ?? '').trim();
|
|
97
|
+
if (!dsqId)
|
|
98
|
+
return { response: createToolErrorResponse('validation_error', 'Missing required argument: id') };
|
|
99
|
+
try {
|
|
100
|
+
const result = deleteSequence(dsqId, cwd);
|
|
101
|
+
return {
|
|
102
|
+
response: toolResponse({
|
|
103
|
+
content: [{ type: 'text', text: `✔ Sequence deleted: [${result.id}] ${result.name}` }],
|
|
104
|
+
sequence_id: result.id,
|
|
105
|
+
}),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
110
|
+
if (msg.includes('not found')) {
|
|
111
|
+
return { response: createToolErrorResponse('not_found', msg) };
|
|
112
|
+
}
|
|
113
|
+
return { response: createToolErrorResponse('operation_error', msg) };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=mcp-write-sequences.js.map
|