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,864 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP claim/assignment write-tool handlers.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from mcp.ts (pln#622 PR3b) — mechanical move of the
|
|
5
|
+
* claim / session / assignment / plan-step write handlers. Behavior is
|
|
6
|
+
* unchanged; each handler receives the tool-call payload plus a
|
|
7
|
+
* {@link McpWriteClaimsContext} carrying the helpers that remain in the
|
|
8
|
+
* mcp.ts assembly point because they are shared with other write domains.
|
|
9
|
+
*
|
|
10
|
+
* This module must never import ./mcp.js (dependency-direction guard,
|
|
11
|
+
* pln#622 PR1).
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { getTriggeredItems, renderTriggeredItems } from '../core/lifecycle.js';
|
|
18
|
+
import { buildContext } from '../core/context.js';
|
|
19
|
+
import { checkBrainclawInstallableUpdate, getInstalledBrainclawVersion, renderBrainclawInstallableUpdateNotice } from '../core/brainclaw-version.js';
|
|
20
|
+
import { loadConfig } from '../core/config.js';
|
|
21
|
+
import { generateClaimId, loadClaim, saveClaim, adoptClaimSession, releaseClaimWithCascade } from '../core/claims.js';
|
|
22
|
+
import { checkPolicy } from '../core/policy.js';
|
|
23
|
+
import { createWorktree as coreCreateWorktree, sanitizeBranchComponent } from '../core/worktree.js';
|
|
24
|
+
import { startSession } from './session-start.js';
|
|
25
|
+
import { endSession } from './session-end.js';
|
|
26
|
+
import { hasMinimumTrustLevel } from '../core/agent-registry.js';
|
|
27
|
+
import { appendAuditEntry } from '../core/audit.js';
|
|
28
|
+
import { nowISO } from '../core/ids.js';
|
|
29
|
+
import { buildOperationalIdentity, loadAllSessions } from '../core/identity.js';
|
|
30
|
+
import { validateMcpField } from '../core/input-validation.js';
|
|
31
|
+
import { resolveTargetStore } from '../core/store-resolution.js';
|
|
32
|
+
import { buildCoordinationSnapshot } from '../core/coordination.js';
|
|
33
|
+
import { countActionable } from '../core/messaging.js';
|
|
34
|
+
import { addStep as addStepOp, completeStep as completeStepOp, updateStep as updateStepOp, deleteStep as deleteStepOp } from '../core/operations/plan.js';
|
|
35
|
+
import { toolResponse, createToolErrorResponse, normaliseFormat, } from './mcp-contract.js';
|
|
36
|
+
import { renderContextForMcp } from './mcp-presentation.js';
|
|
37
|
+
export function parseTtl(ttl) {
|
|
38
|
+
const match = /^(\d+)([mhd])$/.exec(ttl.trim().toLowerCase());
|
|
39
|
+
if (!match)
|
|
40
|
+
return undefined;
|
|
41
|
+
const value = parseInt(match[1], 10);
|
|
42
|
+
const unit = match[2];
|
|
43
|
+
const ms = unit === 'm' ? value * 60_000 : unit === 'h' ? value * 3_600_000 : value * 86_400_000;
|
|
44
|
+
return new Date(Date.now() + ms).toISOString();
|
|
45
|
+
}
|
|
46
|
+
export async function handleBclawClaim(payload, ctx) {
|
|
47
|
+
const { name, args, cwd, connectionSessionId } = payload;
|
|
48
|
+
// project=X naming a workspace sibling auto-localizes (session+switch then
|
|
49
|
+
// claim locally); federated links / unknown names stay blocked.
|
|
50
|
+
const claimLoc = ctx.resolveExecutionWriteTarget('claim', args, cwd, connectionSessionId);
|
|
51
|
+
if (claimLoc.block) {
|
|
52
|
+
return { response: claimLoc.block };
|
|
53
|
+
}
|
|
54
|
+
const effectiveClaimCwd = claimLoc.targetCwd;
|
|
55
|
+
const claimAutoSwitched = claimLoc.autoSwitched;
|
|
56
|
+
const storeTarget = args.store ?? 'local';
|
|
57
|
+
const claimCwd = resolveTargetStore(effectiveClaimCwd, storeTarget);
|
|
58
|
+
const resolved = ctx.ensureTrust(args, { nameField: 'agent', idField: 'agentId' }, 'contributor', cwd, connectionSessionId);
|
|
59
|
+
if (resolved.error) {
|
|
60
|
+
return { response: createToolErrorResponse(resolved.error.kind, resolved.error.message, resolved.error.details) };
|
|
61
|
+
}
|
|
62
|
+
const claimScope = String(args.scope ?? '').trim();
|
|
63
|
+
const claimDescription = String(args.description ?? '').trim();
|
|
64
|
+
const scopeCheck = validateMcpField(claimScope, 'scope');
|
|
65
|
+
if (!scopeCheck.ok) {
|
|
66
|
+
return { response: createToolErrorResponse('validation_error', scopeCheck.message) };
|
|
67
|
+
}
|
|
68
|
+
const descCheck = validateMcpField(claimDescription, 'description');
|
|
69
|
+
if (!descCheck.ok) {
|
|
70
|
+
return { response: createToolErrorResponse('validation_error', descCheck.message) };
|
|
71
|
+
}
|
|
72
|
+
const resolvedIdentity = resolved.identity;
|
|
73
|
+
const identity = {
|
|
74
|
+
...buildOperationalIdentity(resolvedIdentity.agent_name, cwd, {
|
|
75
|
+
agentId: resolvedIdentity.agent_id,
|
|
76
|
+
sessionId: connectionSessionId,
|
|
77
|
+
}),
|
|
78
|
+
project_id: loadConfig(claimCwd).project_id,
|
|
79
|
+
};
|
|
80
|
+
const claimId = generateClaimId();
|
|
81
|
+
let worktreePath;
|
|
82
|
+
let worktreeWarn = '';
|
|
83
|
+
// trp#431: advisory mode skips worktree creation. Default is to create an
|
|
84
|
+
// isolated worktree (multi-agent safety), but when the work already lives
|
|
85
|
+
// (uncommitted) in the main tree a fresh worktree is counterproductive and
|
|
86
|
+
// the agent ends up skipping the claim. Pass advisory:true (or
|
|
87
|
+
// worktree:false) for an advisory-only lock with no worktree.
|
|
88
|
+
const advisoryClaim = args.advisory === true || args.worktree === false;
|
|
89
|
+
if (!advisoryClaim) {
|
|
90
|
+
// Shared slug logic (trp#950): collision-resistant when the scope
|
|
91
|
+
// exceeds the branch-component cap, and identical to createCoordinatorClaim.
|
|
92
|
+
const branchSlug = sanitizeBranchComponent(claimScope);
|
|
93
|
+
const worktreeBranch = args.worktreeBranch?.trim() || `feat/${branchSlug}`;
|
|
94
|
+
try {
|
|
95
|
+
worktreePath = coreCreateWorktree(claimCwd, worktreeBranch, {
|
|
96
|
+
sessionId: identity.session_id,
|
|
97
|
+
agent: identity.agent,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
catch (wtErr) {
|
|
101
|
+
worktreeWarn = `\n⚠ Worktree creation failed: ${wtErr instanceof Error ? wtErr.message : String(wtErr)}`;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const claimTtl = args.ttl;
|
|
105
|
+
const claimExpiresAt = claimTtl ? parseTtl(claimTtl) : undefined;
|
|
106
|
+
const rawHandoffMode = args.handoffMode;
|
|
107
|
+
if (rawHandoffMode && rawHandoffMode !== 'self-commit' && rawHandoffMode !== 'integrator') {
|
|
108
|
+
return { response: toolResponse({ content: [{ type: 'text', text: `Invalid handoffMode: "${rawHandoffMode}". Must be "self-commit" or "integrator".` }], isError: true }) };
|
|
109
|
+
}
|
|
110
|
+
const handoffMode = rawHandoffMode ?? 'self-commit';
|
|
111
|
+
saveClaim({
|
|
112
|
+
id: claimId,
|
|
113
|
+
agent: identity.agent,
|
|
114
|
+
agent_id: identity.agent_id,
|
|
115
|
+
user: process.env.USER || process.env.USERNAME || undefined,
|
|
116
|
+
project_id: identity.project_id,
|
|
117
|
+
host_id: identity.host_id,
|
|
118
|
+
session_id: identity.session_id,
|
|
119
|
+
scope: claimScope,
|
|
120
|
+
description: claimDescription,
|
|
121
|
+
created_at: nowISO(),
|
|
122
|
+
status: 'active',
|
|
123
|
+
plan_id: args.planId,
|
|
124
|
+
model: ctx.currentModel,
|
|
125
|
+
worktree_path: worktreePath,
|
|
126
|
+
expires_at: claimExpiresAt,
|
|
127
|
+
handoff_mode: handoffMode,
|
|
128
|
+
}, claimCwd);
|
|
129
|
+
appendAuditEntry({ actor: resolvedIdentity.agent_name, actor_id: resolvedIdentity.agent_id, action: 'claim', item_id: claimId, item_type: 'claim', scope: claimScope, session_id: identity.session_id, host_id: identity.host_id }, claimCwd);
|
|
130
|
+
// Post-claim policy check: surface constraints/traps as warnings
|
|
131
|
+
const policyResult = checkPolicy({
|
|
132
|
+
scope: claimScope,
|
|
133
|
+
agent: resolvedIdentity.agent_name,
|
|
134
|
+
agentId: resolvedIdentity.agent_id,
|
|
135
|
+
cwd: claimCwd,
|
|
136
|
+
});
|
|
137
|
+
let policyWarn = '';
|
|
138
|
+
const policyWarnings = policyResult.warnings.filter(w => w.kind !== 'no_claim');
|
|
139
|
+
if (policyWarnings.length > 0) {
|
|
140
|
+
policyWarn = '\n\nPolicy warnings for this scope:';
|
|
141
|
+
for (const w of policyWarnings) {
|
|
142
|
+
const idLabel = w.id ? ` (${w.id})` : '';
|
|
143
|
+
policyWarn += `\n ⚠ [${w.kind}]${idLabel} ${w.message}`;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const postClaimItems = getTriggeredItems('trigger:post-claim', claimCwd);
|
|
147
|
+
const postClaimText = renderTriggeredItems(postClaimItems);
|
|
148
|
+
const noPlanWarn = !args.planId
|
|
149
|
+
? '\n⚠ No plan item linked to this claim. Run bclaw_create_plan first and pass planId to track this work formally.'
|
|
150
|
+
: '';
|
|
151
|
+
// Branch guardrail: warn if on master/main without a worktree
|
|
152
|
+
let branchWarn = '';
|
|
153
|
+
if (!worktreePath) {
|
|
154
|
+
try {
|
|
155
|
+
const { execSync } = await import('node:child_process');
|
|
156
|
+
const branch = execSync('git branch --show-current', { cwd: claimCwd, encoding: 'utf-8' }).trim();
|
|
157
|
+
if (branch === 'master' || branch === 'main') {
|
|
158
|
+
const branchSlug = sanitizeBranchComponent(claimScope);
|
|
159
|
+
branchWarn = `\n⚠️ You are on ${branch}. Create a feature branch before editing: git checkout -b feat/${branchSlug}`;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
catch { /* git not available, skip warning */ }
|
|
163
|
+
}
|
|
164
|
+
// Stale-branch detection: warn if behind master
|
|
165
|
+
let staleBranchWarn = '';
|
|
166
|
+
try {
|
|
167
|
+
const { execSync: execSyncSB } = await import('node:child_process');
|
|
168
|
+
const currentBranch = execSyncSB('git branch --show-current', { cwd: claimCwd, encoding: 'utf-8' }).trim();
|
|
169
|
+
if (currentBranch && currentBranch !== 'master' && currentBranch !== 'main') {
|
|
170
|
+
for (const mainBranch of ['master', 'main']) {
|
|
171
|
+
try {
|
|
172
|
+
const behind = execSyncSB(`git rev-list --count ${currentBranch}..${mainBranch}`, { cwd: claimCwd, encoding: 'utf-8' }).trim();
|
|
173
|
+
const count = parseInt(behind, 10);
|
|
174
|
+
if (count > 0) {
|
|
175
|
+
staleBranchWarn = `\n⚠ Branch is ${count} commit(s) behind ${mainBranch}. Consider rebasing before editing.`;
|
|
176
|
+
}
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
catch { /* branch doesn't exist, try next */ }
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
catch { /* git not available */ }
|
|
184
|
+
const worktreeNote = worktreePath ? `\n Worktree: ${worktreePath}` : '';
|
|
185
|
+
const expiryNote = claimExpiresAt ? `\n Expires: ${claimExpiresAt.slice(0, 16).replace('T', ' ')} UTC` : '';
|
|
186
|
+
const handoffNote = handoffMode ? `\n Handoff: ${handoffMode} (another agent will review and merge)` : '';
|
|
187
|
+
const autoSwitchNote = claimAutoSwitched ? `\n Auto-switched → ${ctx.projectInfoForCwd(effectiveClaimCwd).name ?? effectiveClaimCwd}` : '';
|
|
188
|
+
const claimText = `✔ Claimed scope [${claimId}]${worktreeNote}${expiryNote}${handoffNote}${autoSwitchNote}${noPlanWarn}${worktreeWarn}${branchWarn}${staleBranchWarn}${policyWarn}${postClaimText ? `\n${postClaimText}` : ''}`;
|
|
189
|
+
return {
|
|
190
|
+
response: ctx.appendLegacyMcpToolWarning(toolResponse({
|
|
191
|
+
content: [{ type: 'text', text: claimText }],
|
|
192
|
+
claim_id: claimId,
|
|
193
|
+
session_id: identity.session_id,
|
|
194
|
+
worktree_path: worktreePath,
|
|
195
|
+
triggered_items: postClaimItems,
|
|
196
|
+
...(claimAutoSwitched ? { auto_switched: true, resolved_project: ctx.projectInfoForCwd(effectiveClaimCwd) } : {}),
|
|
197
|
+
}), name),
|
|
198
|
+
nextConnectionSessionId: ctx.explicitSessionIdFromEnv() ? undefined : identity.session_id,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
export async function handleBclawReleaseClaim(payload, ctx) {
|
|
202
|
+
const { args, cwd, connectionSessionId } = payload;
|
|
203
|
+
const crossProjectError = ctx.blockCrossProjectExecution('claim', args);
|
|
204
|
+
if (crossProjectError) {
|
|
205
|
+
return { response: crossProjectError };
|
|
206
|
+
}
|
|
207
|
+
const claimId = String(args.id ?? '').trim();
|
|
208
|
+
if (!claimId) {
|
|
209
|
+
return { response: createToolErrorResponse('validation_error', 'Missing required argument: id') };
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
loadClaim(claimId, cwd); // validate existence before delegating
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
return { response: createToolErrorResponse('not_found', `Claim not found: ${claimId}`) };
|
|
216
|
+
}
|
|
217
|
+
// pln#562 step 5 + trp#928 — release is ownership-checked like acquisition
|
|
218
|
+
// and adoption. Under the trp#928 tightening the coordinator override is
|
|
219
|
+
// OPT-IN via coordinator_override:true (implicit "trusted+ = always
|
|
220
|
+
// override" was too magic — a coordinator releasing a worker's claim
|
|
221
|
+
// should be a visible act, not a silent side-effect of trust). The
|
|
222
|
+
// ownership check still enforces:
|
|
223
|
+
// - owner-of-claim releases (identity matches): allowed, no override needed
|
|
224
|
+
// - non-owner releases without coordinator_override: rejected loudly (the
|
|
225
|
+
// error message points the caller at coordinator_override so it is
|
|
226
|
+
// executable — pln#607 rule).
|
|
227
|
+
// - non-owner releases with coordinator_override:true but not trusted+:
|
|
228
|
+
// trust_error (privilege escalation prevention).
|
|
229
|
+
// - non-owner releases with coordinator_override:true and trusted+:
|
|
230
|
+
// allowed, audited (auditReleaseOverride).
|
|
231
|
+
const releaseIdentity = ctx.resolveMutationIdentity(args, { nameField: 'agent', idField: 'agentId' }, cwd, connectionSessionId);
|
|
232
|
+
if ('error' in releaseIdentity && releaseIdentity.error) {
|
|
233
|
+
const { kind, message, details } = releaseIdentity.error;
|
|
234
|
+
return { response: createToolErrorResponse(kind, message, details) };
|
|
235
|
+
}
|
|
236
|
+
if (!('identity' in releaseIdentity) || !releaseIdentity.identity) {
|
|
237
|
+
return { response: createToolErrorResponse('identity_error', 'No registered agent identity resolved for bclaw_release_claim.') };
|
|
238
|
+
}
|
|
239
|
+
const coordinatorOverrideRequested = args.coordinator_override === true;
|
|
240
|
+
if (coordinatorOverrideRequested) {
|
|
241
|
+
const trustLevel = releaseIdentity.identity.trust_level ?? 'contributor';
|
|
242
|
+
if (!hasMinimumTrustLevel(trustLevel, 'trusted')) {
|
|
243
|
+
return {
|
|
244
|
+
response: createToolErrorResponse('trust_error', `coordinator_override:true requires trust_level 'trusted' or higher — caller is '${trustLevel}'. Ask a curator to elevate the agent, or have the claim owner release it.`),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const releaseAuth = {
|
|
249
|
+
agent: releaseIdentity.identity.agent_name,
|
|
250
|
+
agent_id: releaseIdentity.identity.agent_id,
|
|
251
|
+
session_id: connectionSessionId,
|
|
252
|
+
override: coordinatorOverrideRequested,
|
|
253
|
+
};
|
|
254
|
+
let cascadeResult;
|
|
255
|
+
try {
|
|
256
|
+
cascadeResult = releaseClaimWithCascade(claimId, {
|
|
257
|
+
planStatus: args.planStatus,
|
|
258
|
+
cwd,
|
|
259
|
+
auth: releaseAuth,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
catch (err) {
|
|
263
|
+
return { response: createToolErrorResponse('trust_error', err instanceof Error ? err.message : String(err)) };
|
|
264
|
+
}
|
|
265
|
+
const { planTransitioned, planWarning, planId: cascadePlanId, newPlanStatus: cascadeNewStatus } = cascadeResult;
|
|
266
|
+
const summaryText = [
|
|
267
|
+
`✔ Released claim [${claimId}]`,
|
|
268
|
+
planTransitioned ? ` — plan ${cascadePlanId} → ${cascadeNewStatus}` : '',
|
|
269
|
+
planWarning ? ` ⚠ ${planWarning}` : '',
|
|
270
|
+
].join('');
|
|
271
|
+
return {
|
|
272
|
+
response: toolResponse({
|
|
273
|
+
content: [{ type: 'text', text: summaryText }],
|
|
274
|
+
claim_id: claimId,
|
|
275
|
+
...(planTransitioned ? { plan_id: cascadePlanId, plan_status: cascadeNewStatus } : {}),
|
|
276
|
+
...(planWarning ? { plan_warning: planWarning, plan_id: cascadePlanId } : {}),
|
|
277
|
+
}),
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
export async function handleBclawSessionStart(payload, ctx) {
|
|
281
|
+
const { name, args, cwd, connectionSessionId } = payload;
|
|
282
|
+
if (ctx.isLegacyMcpToolFacadeDisabled(name)) {
|
|
283
|
+
return { response: ctx.createLegacyMcpToolDisabledResponse() };
|
|
284
|
+
}
|
|
285
|
+
const crossProjectError = ctx.blockCrossProjectExecution('session', args);
|
|
286
|
+
if (crossProjectError) {
|
|
287
|
+
return { response: crossProjectError };
|
|
288
|
+
}
|
|
289
|
+
const resolved = ctx.ensureTrust(args, { nameField: 'agent', idField: 'agentId' }, 'contributor', cwd, connectionSessionId);
|
|
290
|
+
// For identity_error on session start, let startSession handle auto-registration
|
|
291
|
+
// instead of returning an immediate error (implements "don't require pre-registration to start").
|
|
292
|
+
if (resolved.error && resolved.error.kind !== 'identity_error') {
|
|
293
|
+
return { response: createToolErrorResponse(resolved.error.kind, resolved.error.message, resolved.error.details) };
|
|
294
|
+
}
|
|
295
|
+
const result = await startSession({
|
|
296
|
+
agent: resolved.identity?.agent_name ?? (typeof args.agent === 'string' ? args.agent : undefined),
|
|
297
|
+
agentId: resolved.identity?.agent_id ?? (typeof args.agentId === 'string' ? args.agentId : undefined),
|
|
298
|
+
context: args.context,
|
|
299
|
+
maintenanceMode: args.maintenanceMode === 'fast' ? 'fast' : 'full',
|
|
300
|
+
cwd,
|
|
301
|
+
});
|
|
302
|
+
const postSessionStartItems = getTriggeredItems('trigger:post-session-start', cwd);
|
|
303
|
+
const postSessionStartText = renderTriggeredItems(postSessionStartItems);
|
|
304
|
+
const sessionUpdateConfig = loadConfig(cwd);
|
|
305
|
+
const sessionUpdateCheck = checkBrainclawInstallableUpdate(sessionUpdateConfig, cwd, { useDefaultNpmSource: true });
|
|
306
|
+
const sessionUpdateNotice = renderBrainclawInstallableUpdateNotice(sessionUpdateCheck);
|
|
307
|
+
// Stale instructions guardrail: check if agent files match current version
|
|
308
|
+
let staleInstructionsWarn = '';
|
|
309
|
+
{
|
|
310
|
+
const currentVersion = getInstalledBrainclawVersion();
|
|
311
|
+
const agentFilesToCheck = ['CLAUDE.md', 'AGENTS.md', '.github/copilot-instructions.md'];
|
|
312
|
+
for (const relPath of agentFilesToCheck) {
|
|
313
|
+
try {
|
|
314
|
+
const fullPath = path.join(cwd, relPath);
|
|
315
|
+
const content = fs.readFileSync(fullPath, 'utf-8').slice(0, 200);
|
|
316
|
+
const match = content.match(/brainclaw v(\d+\.\d+\.\d+)/);
|
|
317
|
+
if (match && match[1] !== currentVersion) {
|
|
318
|
+
staleInstructionsWarn = `\n⚠️ Agent instruction files are stale (generated by v${match[1]}, current is v${currentVersion}). Run: brainclaw export --all`;
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
catch { /* file doesn't exist, skip */ }
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
// Claim adoption: if BRAINCLAW_CLAIM_ID is set (spawned by dispatcher),
|
|
326
|
+
// adopt the claim by writing session_id into it. This links claim→session.
|
|
327
|
+
let adoptedClaimId;
|
|
328
|
+
const envClaimId = process.env.BRAINCLAW_CLAIM_ID;
|
|
329
|
+
if (envClaimId && result.session_id) {
|
|
330
|
+
try {
|
|
331
|
+
const adoptResult = adoptClaimSession(envClaimId, result.session_id, cwd);
|
|
332
|
+
if (adoptResult.adopted) {
|
|
333
|
+
adoptedClaimId = envClaimId;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
catch { /* best-effort — claim may not exist or be already adopted */ }
|
|
337
|
+
}
|
|
338
|
+
const sessionStartMsgParts = ['✔ Session started'];
|
|
339
|
+
if (result.auto_registered) {
|
|
340
|
+
sessionStartMsgParts.push(`\n⚠️ Agent '${result.agent}' was auto-registered (first use). Run \`brainclaw register-agent ${result.agent}\` to set capabilities and trust level.`);
|
|
341
|
+
}
|
|
342
|
+
if (adoptedClaimId)
|
|
343
|
+
sessionStartMsgParts.push(`\n🔗 Adopted claim ${adoptedClaimId} — use bclaw_read_inbox with claimId to see your assignment.`);
|
|
344
|
+
if (staleInstructionsWarn)
|
|
345
|
+
sessionStartMsgParts.push(staleInstructionsWarn);
|
|
346
|
+
if (sessionUpdateNotice)
|
|
347
|
+
sessionStartMsgParts.push(sessionUpdateNotice);
|
|
348
|
+
if (postSessionStartText)
|
|
349
|
+
sessionStartMsgParts.push(postSessionStartText);
|
|
350
|
+
if (result.memory_pressure) {
|
|
351
|
+
sessionStartMsgParts.push(`\n⚠️ Memory pressure detected: ${result.memory_pressure.done_plans} done plans, ${result.memory_pressure.closed_handoffs} closed handoffs (${result.memory_pressure.eligible_items} eligible for compaction). Consider running bclaw_compact to archive old items and create durable summaries.`);
|
|
352
|
+
}
|
|
353
|
+
// Inbox notification
|
|
354
|
+
const agentNameForInbox = resolved.identity?.agent_name ?? result.agent;
|
|
355
|
+
if (agentNameForInbox) {
|
|
356
|
+
const actionableCount = countActionable(agentNameForInbox, cwd);
|
|
357
|
+
if (actionableCount > 0) {
|
|
358
|
+
sessionStartMsgParts.push(`\n📬 You have ${actionableCount} actionable message(s) in your inbox. Use bclaw_read_inbox to check.`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
const sessionStartMsg = sessionStartMsgParts.join('\n');
|
|
362
|
+
const contentParts = [{ type: 'text', text: sessionStartMsg }];
|
|
363
|
+
const inboxPending = agentNameForInbox ? countActionable(agentNameForInbox, cwd) : 0;
|
|
364
|
+
const structured = {
|
|
365
|
+
session_id: result.session_id,
|
|
366
|
+
agent: result.agent,
|
|
367
|
+
context_target: result.context_target,
|
|
368
|
+
inbox_pending: inboxPending,
|
|
369
|
+
...(result.auto_registered ? { auto_registered: true } : {}),
|
|
370
|
+
...(result.memory_pressure ? { memory_pressure: result.memory_pressure } : {}),
|
|
371
|
+
};
|
|
372
|
+
if (args.includeContext) {
|
|
373
|
+
const contextAgent = resolved.identity?.agent_name ?? result.agent;
|
|
374
|
+
const previousSession = loadAllSessions(cwd)
|
|
375
|
+
.find((session) => session.agent === contextAgent && session.session_id !== result.session_id);
|
|
376
|
+
const ctxResult = buildContext({
|
|
377
|
+
target: args.context,
|
|
378
|
+
agent: contextAgent,
|
|
379
|
+
profile: args.contextProfile,
|
|
380
|
+
cwd,
|
|
381
|
+
sinceSession: previousSession?.session_id,
|
|
382
|
+
});
|
|
383
|
+
const format = normaliseFormat(args.contextFormat);
|
|
384
|
+
const ctxText = renderContextForMcp(ctxResult, format, {});
|
|
385
|
+
contentParts.push({ type: 'text', text: ctxText || 'No relevant memory found.' });
|
|
386
|
+
structured.context = ctxResult;
|
|
387
|
+
}
|
|
388
|
+
if (args.includeBoard) {
|
|
389
|
+
const board = buildCoordinationSnapshot({
|
|
390
|
+
agent: resolved.identity?.agent_name ?? result.agent,
|
|
391
|
+
autoAcknowledge: true,
|
|
392
|
+
cwd,
|
|
393
|
+
});
|
|
394
|
+
const boardLines = [];
|
|
395
|
+
boardLines.push(`Active plans: ${board.active_plans.length}`);
|
|
396
|
+
for (const plan of board.active_plans.slice(0, 10)) {
|
|
397
|
+
const claims = plan.claims.length ? ` claims=${plan.claims.map((c) => c.agent).join(',')}` : '';
|
|
398
|
+
boardLines.push(`- [${plan.id}] ${plan.text} (${plan.status}, ${plan.priority})${claims}`);
|
|
399
|
+
}
|
|
400
|
+
boardLines.push(`Active claims: ${board.active_claims.length}`);
|
|
401
|
+
for (const claim of board.active_claims.slice(0, 10)) {
|
|
402
|
+
boardLines.push(`- [${claim.id}] ${claim.agent} -> ${claim.scope}`);
|
|
403
|
+
}
|
|
404
|
+
if (board.active_sequence) {
|
|
405
|
+
boardLines.push(`Active sequence: ${board.active_sequence.name} (${board.active_sequence.status})`);
|
|
406
|
+
for (const item of board.active_sequence.items.slice(0, 5)) {
|
|
407
|
+
const lane = item.lane ? ` lane=${item.lane}` : '';
|
|
408
|
+
boardLines.push(`- #${item.rank} ${item.planId}${lane}`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
boardLines.push(`Open handoffs: ${board.open_handoffs.length}`);
|
|
412
|
+
for (const handoff of board.open_handoffs.slice(0, 5)) {
|
|
413
|
+
boardLines.push(`- [${handoff.id}] ${handoff.from} -> ${handoff.to}: ${handoff.text}`);
|
|
414
|
+
}
|
|
415
|
+
if (board.inbox_pending > 0) {
|
|
416
|
+
boardLines.push(`📬 Inbox: ${board.inbox_pending} pending message(s)`);
|
|
417
|
+
}
|
|
418
|
+
contentParts.push({ type: 'text', text: boardLines.join('\n') });
|
|
419
|
+
structured.board = board;
|
|
420
|
+
}
|
|
421
|
+
return {
|
|
422
|
+
response: ctx.appendLegacyMcpToolWarning(toolResponse({
|
|
423
|
+
content: contentParts,
|
|
424
|
+
...structured,
|
|
425
|
+
}), name),
|
|
426
|
+
nextConnectionSessionId: ctx.explicitSessionIdFromEnv() ? undefined : result.session_id,
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
export async function handleBclawSessionEnd(payload, ctx) {
|
|
430
|
+
const { args, cwd, connectionSessionId } = payload;
|
|
431
|
+
const crossProjectError = ctx.blockCrossProjectExecution('session', args);
|
|
432
|
+
if (crossProjectError) {
|
|
433
|
+
return { response: crossProjectError };
|
|
434
|
+
}
|
|
435
|
+
const resolved = ctx.ensureTrust(args, { nameField: 'agent', idField: 'agentId' }, 'contributor', cwd, connectionSessionId);
|
|
436
|
+
if (resolved.error) {
|
|
437
|
+
return { response: createToolErrorResponse(resolved.error.kind, resolved.error.message, resolved.error.details) };
|
|
438
|
+
}
|
|
439
|
+
const result = await endSession({
|
|
440
|
+
session: args.session,
|
|
441
|
+
agent: resolved.identity?.agent_name,
|
|
442
|
+
agentId: resolved.identity?.agent_id,
|
|
443
|
+
summary: args.summary,
|
|
444
|
+
narrative: args.narrative,
|
|
445
|
+
autoReflect: args.autoReflect,
|
|
446
|
+
autoRelease: args.autoRelease,
|
|
447
|
+
reflectHandoff: args.reflectHandoff,
|
|
448
|
+
dispatchReview: args.dispatchReview,
|
|
449
|
+
reviewer: args.reviewer,
|
|
450
|
+
reflect: args.reflect,
|
|
451
|
+
cwd,
|
|
452
|
+
});
|
|
453
|
+
const preSessionEndItems = getTriggeredItems('trigger:pre-session-end', cwd);
|
|
454
|
+
const preSessionEndText = renderTriggeredItems(preSessionEndItems);
|
|
455
|
+
const endUpdateConfig = loadConfig(cwd);
|
|
456
|
+
const endUpdateCheck = checkBrainclawInstallableUpdate(endUpdateConfig, cwd, { useDefaultNpmSource: true });
|
|
457
|
+
const endUpdateNotice = renderBrainclawInstallableUpdateNotice(endUpdateCheck);
|
|
458
|
+
const parts = ['✔ Session ended'];
|
|
459
|
+
if (endUpdateNotice)
|
|
460
|
+
parts.push(endUpdateNotice);
|
|
461
|
+
if (preSessionEndText)
|
|
462
|
+
parts.push(preSessionEndText);
|
|
463
|
+
if (result.reflection_prompt) {
|
|
464
|
+
parts.push('\n📝 Session reflection — please answer these questions:');
|
|
465
|
+
for (let i = 0; i < result.reflection_prompt.questions.length; i++) {
|
|
466
|
+
parts.push(` ${i + 1}. ${result.reflection_prompt.questions[i]}`);
|
|
467
|
+
}
|
|
468
|
+
parts.push(`\n${result.reflection_prompt.instruction}`);
|
|
469
|
+
}
|
|
470
|
+
return {
|
|
471
|
+
response: toolResponse({
|
|
472
|
+
content: [{ type: 'text', text: parts.join('\n') }],
|
|
473
|
+
session_id: result.session_id,
|
|
474
|
+
notes_in_session: result.notes_in_session,
|
|
475
|
+
candidates_created: result.candidates_created,
|
|
476
|
+
context_diff: result.context_diff,
|
|
477
|
+
triggered_items: preSessionEndItems,
|
|
478
|
+
...(result.handoff ? { handoff: result.handoff } : {}),
|
|
479
|
+
...(result.reflection_prompt ? { reflection_prompt: result.reflection_prompt } : {}),
|
|
480
|
+
}),
|
|
481
|
+
nextConnectionSessionId: null,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
export async function handleBclawAssignmentUpdate(payload, ctx) {
|
|
485
|
+
const { args, cwd, connectionSessionId } = payload;
|
|
486
|
+
// Contributor trust: lowest dispatchable level. The agent-owner guard
|
|
487
|
+
// below ensures only the assigned agent can update its own assignment.
|
|
488
|
+
const resolved = ctx.ensureTrust(args, { nameField: 'agent', idField: 'agentId' }, 'contributor', cwd, connectionSessionId);
|
|
489
|
+
if (resolved.error) {
|
|
490
|
+
return { response: createToolErrorResponse(resolved.error.kind, resolved.error.message, resolved.error.details) };
|
|
491
|
+
}
|
|
492
|
+
try {
|
|
493
|
+
const assignmentId = typeof args.assignment_id === 'string' ? args.assignment_id : undefined;
|
|
494
|
+
const status = typeof args.status === 'string' ? args.status : undefined;
|
|
495
|
+
if (!assignmentId)
|
|
496
|
+
return { response: createToolErrorResponse('input_error', 'assignment_id is required') };
|
|
497
|
+
if (!status)
|
|
498
|
+
return { response: createToolErrorResponse('input_error', 'status is required') };
|
|
499
|
+
const message = args.message;
|
|
500
|
+
const errorMessage = args.error_message;
|
|
501
|
+
const blocker = args.blocker;
|
|
502
|
+
const actionRequiredInput = args.action_required;
|
|
503
|
+
const artifacts = Array.isArray(args.artifacts) ? args.artifacts : undefined;
|
|
504
|
+
// Warn if no active session (audit trail will be incomplete)
|
|
505
|
+
const effectiveSessionId = connectionSessionId ?? 'unknown';
|
|
506
|
+
const { loadAssignment, transitionAssignment: transitionAsgn, recordProgress: recordProg } = await import('../core/assignments.js');
|
|
507
|
+
const assignment = loadAssignment(assignmentId, cwd);
|
|
508
|
+
if (!assignment) {
|
|
509
|
+
return { response: createToolErrorResponse('not_found', `Assignment not found: ${assignmentId}`) };
|
|
510
|
+
}
|
|
511
|
+
// Agent guard: only the assigned agent can update
|
|
512
|
+
const callerAgent = resolved.identity.agent_name;
|
|
513
|
+
if (assignment.agent !== callerAgent) {
|
|
514
|
+
return { response: createToolErrorResponse('trust_error', `Agent ${callerAgent} cannot update assignment owned by ${assignment.agent}`) };
|
|
515
|
+
}
|
|
516
|
+
if (status === 'progress') {
|
|
517
|
+
const updated = recordProg(assignmentId, {
|
|
518
|
+
message,
|
|
519
|
+
artifacts,
|
|
520
|
+
actor: callerAgent,
|
|
521
|
+
actor_id: resolved.identity.agent_id,
|
|
522
|
+
session_id: effectiveSessionId,
|
|
523
|
+
}, cwd);
|
|
524
|
+
return {
|
|
525
|
+
response: {
|
|
526
|
+
content: [{ type: 'text', text: `Assignment ${assignmentId} heartbeat recorded` }],
|
|
527
|
+
structuredContent: { assignment_id: assignmentId, status: updated.status, last_heartbeat_at: updated.last_heartbeat_at },
|
|
528
|
+
},
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
// Map status to FSM transition
|
|
532
|
+
const statusReason = status === 'failed' ? errorMessage
|
|
533
|
+
: status === 'blocked' ? blocker
|
|
534
|
+
: message;
|
|
535
|
+
const result = transitionAsgn(assignmentId, status, {
|
|
536
|
+
session_id: effectiveSessionId,
|
|
537
|
+
status_reason: statusReason,
|
|
538
|
+
artifacts,
|
|
539
|
+
error_message: errorMessage,
|
|
540
|
+
actor: callerAgent,
|
|
541
|
+
actor_id: resolved.identity.agent_id,
|
|
542
|
+
}, cwd);
|
|
543
|
+
// trp#928 — cascade-release the assignment's linked claim on completion.
|
|
544
|
+
// Before this landing an obedient worker had to make TWO calls to close
|
|
545
|
+
// the loop (bclaw_assignment_update status=completed AND
|
|
546
|
+
// bclaw_release_claim); dispatch briefs enumerate both, but not every
|
|
547
|
+
// sandboxed worker gets through both, and the coordinator's harvest path
|
|
548
|
+
// only releases on --integrate — so contributor-driven completions left
|
|
549
|
+
// claims active. The worker's own identity owns the claim (session
|
|
550
|
+
// adoption), so ownership matches and no coordinator_override is needed.
|
|
551
|
+
// Silent success/failure is unacceptable: log per-claim outcome.
|
|
552
|
+
if (status === 'completed' && assignment.claim_id) {
|
|
553
|
+
try {
|
|
554
|
+
const { releaseClaimsCascade, logCascadeReleaseResult } = await import('../core/claims.js');
|
|
555
|
+
const cascade = releaseClaimsCascade([assignment.claim_id], {
|
|
556
|
+
cwd,
|
|
557
|
+
planStatus: 'done',
|
|
558
|
+
auth: {
|
|
559
|
+
agent: callerAgent,
|
|
560
|
+
agent_id: resolved.identity.agent_id,
|
|
561
|
+
session_id: effectiveSessionId,
|
|
562
|
+
override: false,
|
|
563
|
+
},
|
|
564
|
+
});
|
|
565
|
+
logCascadeReleaseResult({
|
|
566
|
+
actor: callerAgent,
|
|
567
|
+
trigger: 'assignment_completed',
|
|
568
|
+
assignment_id: assignmentId,
|
|
569
|
+
claim_id: assignment.claim_id,
|
|
570
|
+
cascade,
|
|
571
|
+
cwd,
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
catch { /* never block the update on cascade release */ }
|
|
575
|
+
}
|
|
576
|
+
// When accepted: auto-acknowledge the inbox message (replaces bclaw_ack_message)
|
|
577
|
+
if (status === 'accepted' && assignment.message_id) {
|
|
578
|
+
try {
|
|
579
|
+
const { ackMessage } = await import('../core/messaging.js');
|
|
580
|
+
// pln#562 step 4 — scope the ack to this assignment's claim so a
|
|
581
|
+
// same-named sibling instance cannot consume the message.
|
|
582
|
+
ackMessage(assignment.message_id, callerAgent, cwd, { claimId: assignment.claim_id });
|
|
583
|
+
}
|
|
584
|
+
catch { /* best-effort: don't fail the update if ack fails */ }
|
|
585
|
+
}
|
|
586
|
+
let createdActionId;
|
|
587
|
+
if (status === 'blocked' && actionRequiredInput) {
|
|
588
|
+
const kind = String(actionRequiredInput.kind ?? '').trim();
|
|
589
|
+
const title = String(actionRequiredInput.title ?? '').trim();
|
|
590
|
+
const prompt = String(actionRequiredInput.prompt ?? '').trim();
|
|
591
|
+
if (!['approval', 'user_input', 'clarification', 'plan_approval'].includes(kind) || !title || !prompt) {
|
|
592
|
+
return { response: createToolErrorResponse('validation_error', 'action_required must include kind, title, and prompt when status=blocked') };
|
|
593
|
+
}
|
|
594
|
+
const { createActionRequired } = await import('../core/actions.js');
|
|
595
|
+
const { findLatestAgentRunForAssignment } = await import('../core/agentruns.js');
|
|
596
|
+
const latestRun = findLatestAgentRunForAssignment(assignmentId, cwd);
|
|
597
|
+
const action = createActionRequired({
|
|
598
|
+
assignment_id: assignmentId,
|
|
599
|
+
run_id: latestRun?.id,
|
|
600
|
+
claim_id: assignment.claim_id,
|
|
601
|
+
message_id: assignment.message_id,
|
|
602
|
+
plan_id: assignment.plan_id,
|
|
603
|
+
sequence_id: assignment.sequence_id,
|
|
604
|
+
agent: callerAgent,
|
|
605
|
+
agent_id: resolved.identity.agent_id,
|
|
606
|
+
session_id: effectiveSessionId,
|
|
607
|
+
kind: kind,
|
|
608
|
+
scope: assignment.scope,
|
|
609
|
+
title,
|
|
610
|
+
prompt,
|
|
611
|
+
options: Array.isArray(actionRequiredInput.options) ? actionRequiredInput.options.map(String) : [],
|
|
612
|
+
response_schema: (actionRequiredInput.response_schema && typeof actionRequiredInput.response_schema === 'object')
|
|
613
|
+
? actionRequiredInput.response_schema
|
|
614
|
+
: undefined,
|
|
615
|
+
tags: Array.isArray(actionRequiredInput.tags) ? actionRequiredInput.tags.map(String) : ['action-required'],
|
|
616
|
+
}, cwd);
|
|
617
|
+
createdActionId = action.id;
|
|
618
|
+
}
|
|
619
|
+
return {
|
|
620
|
+
response: {
|
|
621
|
+
content: [{ type: 'text', text: `Assignment ${assignmentId} updated: ${result.previous_status} → ${status}` }],
|
|
622
|
+
structuredContent: {
|
|
623
|
+
assignment_id: assignmentId,
|
|
624
|
+
status,
|
|
625
|
+
previous_status: result.previous_status,
|
|
626
|
+
...(result.assignment.accepted_at && { accepted_at: result.assignment.accepted_at }),
|
|
627
|
+
...(result.assignment.started_at && { started_at: result.assignment.started_at }),
|
|
628
|
+
...(result.assignment.completed_at && { completed_at: result.assignment.completed_at }),
|
|
629
|
+
last_heartbeat_at: result.assignment.last_heartbeat_at,
|
|
630
|
+
...(createdActionId ? { action_id: createdActionId } : {}),
|
|
631
|
+
},
|
|
632
|
+
},
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
catch (err) {
|
|
636
|
+
return { response: createToolErrorResponse('operation_error', err instanceof Error ? err.message : String(err)) };
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
export async function handleBclawAssignmentAction(payload, ctx) {
|
|
640
|
+
const { args, cwd, connectionSessionId } = payload;
|
|
641
|
+
const resolved = ctx.ensureTrust(args, { nameField: 'agent', idField: 'agentId' }, 'contributor', cwd, connectionSessionId);
|
|
642
|
+
if (resolved.error) {
|
|
643
|
+
return { response: createToolErrorResponse(resolved.error.kind, resolved.error.message, resolved.error.details) };
|
|
644
|
+
}
|
|
645
|
+
try {
|
|
646
|
+
const actionId = typeof args.action_id === 'string' ? args.action_id : undefined;
|
|
647
|
+
const outcome = typeof args.outcome === 'string' ? args.outcome : undefined;
|
|
648
|
+
if (!actionId)
|
|
649
|
+
return { response: createToolErrorResponse('input_error', 'action_id is required') };
|
|
650
|
+
if (!outcome || !['resolved', 'rejected', 'cancelled'].includes(outcome)) {
|
|
651
|
+
return { response: createToolErrorResponse('validation_error', 'outcome must be one of: resolved, rejected, cancelled') };
|
|
652
|
+
}
|
|
653
|
+
const { resolveActionRequired, loadActionRequired } = await import('../core/actions.js');
|
|
654
|
+
// Guard: an agent cannot resolve its own action (defeats approval workflow)
|
|
655
|
+
const pendingAction = loadActionRequired(actionId, cwd);
|
|
656
|
+
if (pendingAction && pendingAction.agent === resolved.identity.agent_name) {
|
|
657
|
+
return { response: createToolErrorResponse('trust_error', `Agent '${resolved.identity.agent_name}' cannot resolve its own action. A supervisor or different agent must respond.`) };
|
|
658
|
+
}
|
|
659
|
+
const action = resolveActionRequired(actionId, {
|
|
660
|
+
outcome: outcome,
|
|
661
|
+
text: typeof args.text === 'string' ? args.text : undefined,
|
|
662
|
+
payload: args.payload && typeof args.payload === 'object' ? args.payload : undefined,
|
|
663
|
+
responded_by: resolved.identity.agent_name,
|
|
664
|
+
responded_by_id: resolved.identity.agent_id,
|
|
665
|
+
session_id: connectionSessionId ?? 'unknown',
|
|
666
|
+
}, cwd);
|
|
667
|
+
return {
|
|
668
|
+
response: {
|
|
669
|
+
content: [{ type: 'text', text: `Action ${actionId} ${action.status}` }],
|
|
670
|
+
structuredContent: {
|
|
671
|
+
action_id: action.id,
|
|
672
|
+
assignment_id: action.assignment_id,
|
|
673
|
+
run_id: action.run_id,
|
|
674
|
+
status: action.status,
|
|
675
|
+
resolved_at: action.resolved_at,
|
|
676
|
+
response: action.response,
|
|
677
|
+
},
|
|
678
|
+
},
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
catch (err) {
|
|
682
|
+
return { response: createToolErrorResponse('operation_error', err instanceof Error ? err.message : String(err)) };
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
export async function handleBclawAddStep(payload, ctx) {
|
|
686
|
+
const { args, cwd, connectionSessionId } = payload;
|
|
687
|
+
const stepLoc = ctx.resolveExecutionWriteTarget('plan', args, cwd, connectionSessionId);
|
|
688
|
+
if (stepLoc.block) {
|
|
689
|
+
return { response: stepLoc.block };
|
|
690
|
+
}
|
|
691
|
+
const resolved = ctx.ensureTrust(args, { nameField: 'agent', idField: 'agentId' }, 'contributor', cwd, connectionSessionId);
|
|
692
|
+
if (resolved.error) {
|
|
693
|
+
return { response: createToolErrorResponse(resolved.error.kind, resolved.error.message, resolved.error.details) };
|
|
694
|
+
}
|
|
695
|
+
const stepPlanId = String(args.planId ?? '').trim();
|
|
696
|
+
const stepData = args.data && typeof args.data === 'object' && !Array.isArray(args.data)
|
|
697
|
+
? args.data
|
|
698
|
+
: {};
|
|
699
|
+
if ((args.text !== undefined || args.assignee !== undefined) && Object.keys(stepData).length > 0) {
|
|
700
|
+
console.warn('[brainclaw:warn] bclaw_add_step received legacy top-level fields alongside data.*; using data.* values');
|
|
701
|
+
}
|
|
702
|
+
const stepTextRaw = stepData.text ?? stepData.title ?? args.text;
|
|
703
|
+
const stepText = typeof stepTextRaw === 'string' ? stepTextRaw.trim() : '';
|
|
704
|
+
const stepAssignee = (stepData.assignee ?? args.assignee);
|
|
705
|
+
const stepEstimated = (stepData.estimated_effort ?? args.estimated_effort);
|
|
706
|
+
const stepActual = (stepData.actual_effort ?? args.actual_effort);
|
|
707
|
+
if (!stepPlanId)
|
|
708
|
+
return { response: createToolErrorResponse('validation_error', 'Missing required argument: planId') };
|
|
709
|
+
if (!stepText)
|
|
710
|
+
return { response: createToolErrorResponse('validation_error', 'Missing required argument: data.text') };
|
|
711
|
+
const stepTargetCwd = stepLoc.targetCwd;
|
|
712
|
+
try {
|
|
713
|
+
const result = addStepOp({ planId: stepPlanId, text: stepText, assignee: stepAssignee, estimatedEffort: stepEstimated, actualEffort: stepActual }, stepTargetCwd);
|
|
714
|
+
return {
|
|
715
|
+
response: toolResponse({
|
|
716
|
+
content: [{ type: 'text', text: `✔ Step added: [${result.stepId}] ${stepText} (${result.doneSteps}/${result.totalSteps} done)` }],
|
|
717
|
+
step_id: result.stepId,
|
|
718
|
+
plan_id: result.planId,
|
|
719
|
+
progress: { done: result.doneSteps, total: result.totalSteps },
|
|
720
|
+
}),
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
catch (err) {
|
|
724
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
725
|
+
if (msg.includes('not found')) {
|
|
726
|
+
return { response: createToolErrorResponse('not_found', msg) };
|
|
727
|
+
}
|
|
728
|
+
return { response: createToolErrorResponse('operation_error', msg) };
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
export async function handleBclawCompleteStep(payload, ctx) {
|
|
732
|
+
const { args, cwd, connectionSessionId } = payload;
|
|
733
|
+
const csLoc = ctx.resolveExecutionWriteTarget('plan', args, cwd, connectionSessionId);
|
|
734
|
+
if (csLoc.block) {
|
|
735
|
+
return { response: csLoc.block };
|
|
736
|
+
}
|
|
737
|
+
const resolved = ctx.ensureTrust(args, { nameField: 'agent', idField: 'agentId' }, 'contributor', cwd, connectionSessionId);
|
|
738
|
+
if (resolved.error) {
|
|
739
|
+
return { response: createToolErrorResponse(resolved.error.kind, resolved.error.message, resolved.error.details) };
|
|
740
|
+
}
|
|
741
|
+
const csPlanId = String(args.planId ?? '').trim();
|
|
742
|
+
const csStepId = String(args.stepId ?? '').trim();
|
|
743
|
+
if (!csPlanId)
|
|
744
|
+
return { response: createToolErrorResponse('validation_error', 'Missing required argument: planId') };
|
|
745
|
+
if (!csStepId)
|
|
746
|
+
return { response: createToolErrorResponse('validation_error', 'Missing required argument: stepId') };
|
|
747
|
+
const csTargetCwd = csLoc.targetCwd;
|
|
748
|
+
try {
|
|
749
|
+
const result = completeStepOp({ planId: csPlanId, stepId: csStepId }, csTargetCwd);
|
|
750
|
+
return {
|
|
751
|
+
response: toolResponse({
|
|
752
|
+
content: [{ type: 'text', text: `✔ Step completed: [${result.stepId}] (${result.doneSteps}/${result.totalSteps} done)` }],
|
|
753
|
+
step_id: result.stepId,
|
|
754
|
+
plan_id: result.planId,
|
|
755
|
+
progress: { done: result.doneSteps, total: result.totalSteps },
|
|
756
|
+
all_done: result.doneSteps === result.totalSteps,
|
|
757
|
+
}),
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
catch (err) {
|
|
761
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
762
|
+
if (msg.includes('not found')) {
|
|
763
|
+
return { response: createToolErrorResponse('not_found', msg) };
|
|
764
|
+
}
|
|
765
|
+
return { response: createToolErrorResponse('operation_error', msg) };
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
export async function handleBclawUpdateStep(payload, ctx) {
|
|
769
|
+
const { args, cwd, connectionSessionId } = payload;
|
|
770
|
+
const usLoc = ctx.resolveExecutionWriteTarget('plan', args, cwd, connectionSessionId);
|
|
771
|
+
if (usLoc.block) {
|
|
772
|
+
return { response: usLoc.block };
|
|
773
|
+
}
|
|
774
|
+
const resolved = ctx.ensureTrust(args, { nameField: 'agent', idField: 'agentId' }, 'contributor', cwd, connectionSessionId);
|
|
775
|
+
if (resolved.error) {
|
|
776
|
+
return { response: createToolErrorResponse(resolved.error.kind, resolved.error.message, resolved.error.details) };
|
|
777
|
+
}
|
|
778
|
+
const usPlanId = String(args.planId ?? '').trim();
|
|
779
|
+
const usStepId = String(args.stepId ?? '').trim();
|
|
780
|
+
if (!usPlanId)
|
|
781
|
+
return { response: createToolErrorResponse('validation_error', 'Missing required argument: planId') };
|
|
782
|
+
if (!usStepId)
|
|
783
|
+
return { response: createToolErrorResponse('validation_error', 'Missing required argument: stepId') };
|
|
784
|
+
const validStatuses = ['todo', 'in_progress', 'testing', 'done', 'blocked'];
|
|
785
|
+
if (args.status && !validStatuses.includes(String(args.status))) {
|
|
786
|
+
return { response: createToolErrorResponse('validation_error', `Invalid status: ${args.status}. Valid: ${validStatuses.join(', ')}`) };
|
|
787
|
+
}
|
|
788
|
+
const usTargetCwd = usLoc.targetCwd;
|
|
789
|
+
try {
|
|
790
|
+
const result = updateStepOp({
|
|
791
|
+
planId: usPlanId,
|
|
792
|
+
stepId: usStepId,
|
|
793
|
+
status: args.status,
|
|
794
|
+
text: args.text,
|
|
795
|
+
assignee: args.assignee,
|
|
796
|
+
estimatedEffort: args.estimated_effort,
|
|
797
|
+
actualEffort: args.actual_effort,
|
|
798
|
+
}, usTargetCwd);
|
|
799
|
+
const changes = [];
|
|
800
|
+
if (args.status)
|
|
801
|
+
changes.push(`status=${args.status}`);
|
|
802
|
+
if (args.text)
|
|
803
|
+
changes.push('text updated');
|
|
804
|
+
if (args.assignee !== undefined)
|
|
805
|
+
changes.push(`assignee=${args.assignee || 'unassigned'}`);
|
|
806
|
+
if (args.estimated_effort !== undefined)
|
|
807
|
+
changes.push(`estimate=${args.estimated_effort}`);
|
|
808
|
+
if (args.actual_effort !== undefined)
|
|
809
|
+
changes.push(`actual=${args.actual_effort}`);
|
|
810
|
+
return {
|
|
811
|
+
response: toolResponse({
|
|
812
|
+
content: [{ type: 'text', text: `✔ Step updated: [${result.stepId}] ${changes.join(', ')} (${result.doneSteps}/${result.totalSteps} done)` }],
|
|
813
|
+
step_id: result.stepId,
|
|
814
|
+
plan_id: result.planId,
|
|
815
|
+
progress: { done: result.doneSteps, total: result.totalSteps },
|
|
816
|
+
all_done: result.doneSteps === result.totalSteps,
|
|
817
|
+
}),
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
catch (err) {
|
|
821
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
822
|
+
if (msg.includes('not found')) {
|
|
823
|
+
return { response: createToolErrorResponse('not_found', msg) };
|
|
824
|
+
}
|
|
825
|
+
return { response: createToolErrorResponse('operation_error', msg) };
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
export async function handleBclawDeleteStep(payload, ctx) {
|
|
829
|
+
const { args, cwd, connectionSessionId } = payload;
|
|
830
|
+
const dsLoc = ctx.resolveExecutionWriteTarget('plan', args, cwd, connectionSessionId);
|
|
831
|
+
if (dsLoc.block) {
|
|
832
|
+
return { response: dsLoc.block };
|
|
833
|
+
}
|
|
834
|
+
const resolved = ctx.ensureTrust(args, { nameField: 'agent', idField: 'agentId' }, 'contributor', cwd, connectionSessionId);
|
|
835
|
+
if (resolved.error) {
|
|
836
|
+
return { response: createToolErrorResponse(resolved.error.kind, resolved.error.message, resolved.error.details) };
|
|
837
|
+
}
|
|
838
|
+
const dsPlanId = String(args.planId ?? '').trim();
|
|
839
|
+
const dsStepId = String(args.stepId ?? '').trim();
|
|
840
|
+
if (!dsPlanId)
|
|
841
|
+
return { response: createToolErrorResponse('validation_error', 'Missing required argument: planId') };
|
|
842
|
+
if (!dsStepId)
|
|
843
|
+
return { response: createToolErrorResponse('validation_error', 'Missing required argument: stepId') };
|
|
844
|
+
const dsTargetCwd = dsLoc.targetCwd;
|
|
845
|
+
try {
|
|
846
|
+
const result = deleteStepOp({ planId: dsPlanId, stepId: dsStepId }, dsTargetCwd);
|
|
847
|
+
return {
|
|
848
|
+
response: toolResponse({
|
|
849
|
+
content: [{ type: 'text', text: `✔ Step deleted: [${result.stepId}] (${result.doneSteps}/${result.totalSteps} remaining)` }],
|
|
850
|
+
step_id: result.stepId,
|
|
851
|
+
plan_id: result.planId,
|
|
852
|
+
progress: { done: result.doneSteps, total: result.totalSteps },
|
|
853
|
+
}),
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
catch (err) {
|
|
857
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
858
|
+
if (msg.includes('not found')) {
|
|
859
|
+
return { response: createToolErrorResponse('not_found', msg) };
|
|
860
|
+
}
|
|
861
|
+
return { response: createToolErrorResponse('operation_error', msg) };
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
//# sourceMappingURL=mcp-write-claims.js.map
|