brainclaw 1.11.1 → 1.13.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 +32 -0
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/cli.js +9 -2
- package/dist/commands/claim-resource.js +1 -0
- package/dist/commands/estimation-report.js +1 -1
- package/dist/commands/harvest.js +30 -22
- package/dist/commands/mcp.js +447 -99
- package/dist/commands/release-claim.js +21 -1
- package/dist/core/agent-capability.js +15 -4
- package/dist/core/agent-registry.js +7 -1
- package/dist/core/claims.js +160 -1
- package/dist/core/context.js +11 -4
- package/dist/core/dispatch-status.js +113 -7
- package/dist/core/entity-operations.js +51 -3
- package/dist/core/loops/store.js +33 -0
- package/dist/core/reputation.js +18 -0
- package/dist/core/schema.js +7 -0
- package/dist/core/worktree.js +146 -22
- package/dist/facts.js +36 -3
- package/dist/facts.json +35 -2
- package/docs/mcp-schema-changelog.md +7 -2
- package/package.json +6 -4
package/dist/commands/mcp.js
CHANGED
|
@@ -7,14 +7,14 @@ import { fileURLToPath } from 'node:url';
|
|
|
7
7
|
import { Worker } from 'node:worker_threads';
|
|
8
8
|
import { generatedSchemas } from './mcp-schemas.generated.js';
|
|
9
9
|
import { getTriggeredItems, renderTriggeredItems } from '../core/lifecycle.js';
|
|
10
|
-
import { resolveCrossProjectWritableTarget, resolveProjectCwd, writeCrossProjectSignal } from '../core/cross-project.js';
|
|
10
|
+
import { resolveCrossProjectLinks, resolveCrossProjectWritableTarget, resolveProjectCwd, writeCrossProjectSignal } from '../core/cross-project.js';
|
|
11
11
|
import { buildContext, renderContextMarkdown, renderContextPromptTemplate, renderContextBriefing } from '../core/context.js';
|
|
12
12
|
import { buildCoordinationSnapshot } from '../core/coordination.js';
|
|
13
13
|
import { checkBrainclawInstallableUpdate, getInstalledBrainclawVersion, readDiskBrainclawVersion, renderBrainclawInstallableUpdateNotice } from '../core/brainclaw-version.js';
|
|
14
14
|
import { loadConfig } from '../core/config.js';
|
|
15
15
|
import { collectLoadValidationWarnings, findLoadValidationWarning, loadState, persistState, saveState } from '../core/state.js';
|
|
16
16
|
import { generateIdWithLabel } from '../core/ids.js';
|
|
17
|
-
import { memoryExists } from '../core/io.js';
|
|
17
|
+
import { memoryExists, MEMORY_DIR } from '../core/io.js';
|
|
18
18
|
import { generateCandidateIdWithLabel, loadCandidate, saveCandidate } from '../core/candidates.js';
|
|
19
19
|
import { createEntity, getEntity, listEntities, boundListResult, DEFAULT_FIND_CHAR_BUDGET, removeEntity, transitionEntity, updateEntity, } from '../core/entity-operations.js';
|
|
20
20
|
import { relocateEntity } from '../core/operations/relocate.js';
|
|
@@ -31,16 +31,18 @@ import { rejectCandidate } from './reject.js';
|
|
|
31
31
|
import { startSession } from './session-start.js';
|
|
32
32
|
import { endSession } from './session-end.js';
|
|
33
33
|
import { applyHandoffUpdates } from './update-handoff.js';
|
|
34
|
-
import { AgentIdentityResolutionError, AgentTrustError, findAgentIdentityById, findAgentIdentityByName, hasMinimumTrustLevel, normalizeAgentName, requireMinimumTrustLevel, requireRegisteredAgentIdentity, resolveCurrentAgentIdentity, resolveCurrentModel, ensureAgentRegisteredForDispatch, } from '../core/agent-registry.js';
|
|
34
|
+
import { AgentIdentityResolutionError, AgentTrustError, findAgentIdentityById, findAgentIdentityByName, hasMinimumTrustLevel, normalizeAgentName, requireMinimumTrustLevel, requireRegisteredAgentIdentity, resolveCurrentAgentIdentity, resolveCurrentModel, ensureAgentRegisteredForDispatch, resolveOrAutoRegisterAgentIdentity, } from '../core/agent-registry.js';
|
|
35
35
|
import { appendAuditEntry } from '../core/audit.js';
|
|
36
36
|
import { nowISO, generateId } from '../core/ids.js';
|
|
37
|
-
import { buildOperationalIdentity, loadAllSessions, loadSessionById } from '../core/identity.js';
|
|
37
|
+
import { buildOperationalIdentity, loadAllSessions, loadCurrentSession, loadSessionById, saveCurrentSession } from '../core/identity.js';
|
|
38
38
|
import { validateMcpInput, validateMcpField } from '../core/input-validation.js';
|
|
39
39
|
import { createCapability, createTool as createRegistryTool } from '../core/registries.js';
|
|
40
40
|
import { detectAiAgent } from '../core/ai-agent-detection.js';
|
|
41
|
+
import { isObserverMode } from '../core/observer-mode.js';
|
|
41
42
|
import { checkGitPresence, scanGitRepos, parseRoots, parseRepoSelection, parseAgentSelection, getDetectedSetupAgentNames, getInstalledAgentNames, runGlobalInstall, initReposAndConfigureAgents, readSetupState, ALL_KNOWN_AGENTS, } from './setup.js';
|
|
42
43
|
import { buildAgentInventory } from '../core/agent-inventory.js';
|
|
43
|
-
import { resolveEffectiveCwd, resolveEffectiveCwdInfo, resolveProjectRef, resolveTargetStore } from '../core/store-resolution.js';
|
|
44
|
+
import { findOutermostBrainclawRoot, resolveEffectiveCwd, resolveEffectiveCwdInfo, resolveProjectRef, resolveTargetStore } from '../core/store-resolution.js';
|
|
45
|
+
import { switchProject } from './switch.js';
|
|
44
46
|
import { assessBootstrapNeed, probeForQuickSetup, buildQuickSetupProbeResponse, buildOnboardingPreview, resolveEmptyMemoryRecommendation } from '../core/setup-flow.js';
|
|
45
47
|
import { ensureUserStore, resolveHomeDir } from '../core/setup-state.js';
|
|
46
48
|
import { createPlan, addStep as addStepOp, completeStep as completeStepOp, updateStep as updateStepOp, deleteStep as deleteStepOp, deletePlan as deletePlanOp } from '../core/operations/plan.js';
|
|
@@ -655,13 +657,17 @@ const MCP_WRITE_TOOLS = [
|
|
|
655
657
|
},
|
|
656
658
|
{
|
|
657
659
|
name: 'bclaw_release_claim',
|
|
658
|
-
description: 'Release a work claim.',
|
|
660
|
+
description: 'Release a work claim. Callers own their own claims; a trusted+ coordinator releasing another agent\'s claim MUST pass coordinator_override:true (audited).',
|
|
659
661
|
annotations: { tier: 'standard', category: 'coordination', headlessApproval: 'auto' },
|
|
660
662
|
inputSchema: {
|
|
661
663
|
type: 'object',
|
|
662
664
|
properties: {
|
|
663
665
|
id: { type: 'string', description: 'Claim ID to release.' },
|
|
664
666
|
planStatus: { type: 'string', description: 'Optional: update linked plan status.' },
|
|
667
|
+
coordinator_override: {
|
|
668
|
+
type: 'boolean',
|
|
669
|
+
description: 'Opt-in override for a trusted+ caller releasing a claim they do NOT own (cross-agent teardown, ghost-claim cleanup). Rejected for contributor-level callers; audited when used. trp#928.',
|
|
670
|
+
},
|
|
665
671
|
},
|
|
666
672
|
required: ['id'],
|
|
667
673
|
},
|
|
@@ -1187,7 +1193,7 @@ const MCP_WRITE_TOOLS = [
|
|
|
1187
1193
|
type: 'object',
|
|
1188
1194
|
properties: {
|
|
1189
1195
|
entity: { type: 'string', description: 'Entity name: plan | decision | constraint | trap | handoff | runtime_note | candidate | sequence | claim | action | assignment | agent_run | cross_project_link. Others not yet wired.' },
|
|
1190
|
-
filter: { type: 'object', description: 'Filter keys: status, tag (single tag), tags (array, any-match), author, plan_id, source, auto_generated, limit, offset, includeLegacy (bool, default false), minAutoReflectConfidence (0-1, default 0.6).
|
|
1196
|
+
filter: { type: 'object', description: 'Filter keys (ANY entity): status, tag (single tag), tags (array, any-match), author, plan_id, source, auto_generated, limit, offset, includeLegacy (bool, default false), minAutoReflectConfidence (0-1, default 0.6). ENTITY-SCOPED keys (rejected with a validation_error if used with any other entity): assignment_id, claim_id, message_id — ONLY for entity="agent_run". Unknown/mis-scoped keys are rejected loudly.' },
|
|
1191
1197
|
project: { type: 'string', description: 'Optional: name (or path/basename) of a linked project to query. Defaults to the current project. Only cross_project_links (config.yaml) and workspace store-chain children are accepted — list with `brainclaw link list`.' },
|
|
1192
1198
|
budget_tokens: { type: 'number', description: 'Optional token budget for the page payload (~4 chars/token). Tightens the default size cap; pagination metadata (has_more/next_offset) still applies.' },
|
|
1193
1199
|
},
|
|
@@ -1255,7 +1261,7 @@ const MCP_WRITE_TOOLS = [
|
|
|
1255
1261
|
},
|
|
1256
1262
|
{
|
|
1257
1263
|
name: 'bclaw_transition',
|
|
1258
|
-
description: 'Transition an entity to a new status. Validated against EntityRegistry.transitions. Returns the triggered side-effect tags. Pass `project` to transition an entity in a linked project instead of the current one.',
|
|
1264
|
+
description: 'Transition an entity to a new status. Validated against EntityRegistry.transitions. Returns the triggered side-effect tags. Pass `project` to transition an entity in a linked project instead of the current one. For entity="claim": released/stale transitions are ownership-checked — non-owners must pass coordinator_override:true (trusted+ trust level required).',
|
|
1259
1265
|
annotations: { tier: 'standard', category: 'memory', headlessApproval: 'prompt' },
|
|
1260
1266
|
inputSchema: {
|
|
1261
1267
|
type: 'object',
|
|
@@ -1265,6 +1271,7 @@ const MCP_WRITE_TOOLS = [
|
|
|
1265
1271
|
to: { type: 'string', description: 'Target status.' },
|
|
1266
1272
|
reason: { type: 'string', description: 'Optional free-text reason, audited alongside the transition.' },
|
|
1267
1273
|
project: { type: 'string', description: 'Optional: name of a linked project to transition the entity in. Defaults to the current project.' },
|
|
1274
|
+
coordinator_override: { type: 'boolean', description: 'entity="claim" only: opt-in override for a trusted+ caller releasing/staling a claim they do NOT own. Audited when used. trp#928.' },
|
|
1268
1275
|
},
|
|
1269
1276
|
required: ['entity', 'id', 'to'],
|
|
1270
1277
|
},
|
|
@@ -1828,10 +1835,22 @@ function ensureTrust(args, fields, level, cwd, sessionId) {
|
|
|
1828
1835
|
* missing field — which would then be silently GC'd by the state sync loop
|
|
1829
1836
|
* (see fix plan pln_5f44426c).
|
|
1830
1837
|
*
|
|
1831
|
-
* pln#562 step 3 —
|
|
1832
|
-
* author
|
|
1833
|
-
* invalid on read and silently GC'd
|
|
1834
|
-
*
|
|
1838
|
+
* pln#562 step 3 — a write that would create a record with a missing/'unknown'
|
|
1839
|
+
* author must never be silent (that produced records that passed creation but
|
|
1840
|
+
* were schema-invalid on read and silently GC'd from disk).
|
|
1841
|
+
*
|
|
1842
|
+
* pln#608 — extended with auto-repair: when the caller has no session but a
|
|
1843
|
+
* derivable agent name (arg / $BRAINCLAW_AGENT_NAME / detected AI agent),
|
|
1844
|
+
* fall through to `resolveOrAutoRegisterAgentIdentity` and materialize the
|
|
1845
|
+
* session via `buildOperationalIdentity({ persistImplicitSession: true })`
|
|
1846
|
+
* (same mechanic as switchProject:86-106 and session-start). The freshly-
|
|
1847
|
+
* created session is tagged `auto_created` so aggressive harvesting can
|
|
1848
|
+
* distinguish it from operator sessions (pln#602). The caller receives
|
|
1849
|
+
* `auto_repair` and surfaces it as a warning — never silent.
|
|
1850
|
+
*
|
|
1851
|
+
* KEEP (still a hard error, doctrine boundary): the identity is ambiguous
|
|
1852
|
+
* (no name in args, no env signal, no detectable agent). We do not invent
|
|
1853
|
+
* an identity — invoke intent is unclear and the write would misattribute.
|
|
1835
1854
|
*/
|
|
1836
1855
|
function resolveCanonicalAuthor(args, cwd, connectionSessionId) {
|
|
1837
1856
|
const resolved = resolveMutationIdentity(args, { nameField: 'agent', idField: 'agentId' }, cwd, connectionSessionId);
|
|
@@ -1841,9 +1860,92 @@ function resolveCanonicalAuthor(args, cwd, connectionSessionId) {
|
|
|
1841
1860
|
agent_id: resolved.identity.agent_id,
|
|
1842
1861
|
};
|
|
1843
1862
|
}
|
|
1844
|
-
const
|
|
1845
|
-
|
|
1846
|
-
|
|
1863
|
+
const strictError = 'error' in resolved && resolved.error ? resolved.error : undefined;
|
|
1864
|
+
// KEEP (doctrine boundary): a pinned principal that rejected the caller args
|
|
1865
|
+
// is a SPOOF/MISMATCH, not an ambiguous first-write. Never auto-repair over
|
|
1866
|
+
// it — silently re-attributing would defeat pln#562 step 3. The strict error
|
|
1867
|
+
// already carries the pointer to a curator override.
|
|
1868
|
+
if (resolveConnectionPrincipal(cwd, connectionSessionId)) {
|
|
1869
|
+
throw new Error(`cannot resolve mutation author: ${strictError?.message ?? 'principal mismatch'}`);
|
|
1870
|
+
}
|
|
1871
|
+
// Observer processes are read-only dashboards/inspectors. Even when an env
|
|
1872
|
+
// variable leaks an agent name into the observer process, canonical writes
|
|
1873
|
+
// must not use the auto-repair path because it can mint identity/session
|
|
1874
|
+
// state as a side effect.
|
|
1875
|
+
if (isObserverMode()) {
|
|
1876
|
+
throw new Error(`cannot resolve mutation author: ${strictError?.message ?? 'observer mode cannot auto-repair identity/session state'}`);
|
|
1877
|
+
}
|
|
1878
|
+
const explicitName = typeof args.agent === 'string' ? args.agent : undefined;
|
|
1879
|
+
const explicitId = typeof args.agentId === 'string' ? args.agentId : undefined;
|
|
1880
|
+
// resolveOrAutoRegisterAgentIdentity's fall-through helper only reads
|
|
1881
|
+
// BRAINCLAW_AGENT / OPENCLAW_AGENT. resolveCurrentAgentIdentity also honors
|
|
1882
|
+
// BRAINCLAW_AGENT_NAME, and dispatched workers set both. Normalize here so
|
|
1883
|
+
// an env-declared name is a first-class signal to the auto-repair path.
|
|
1884
|
+
const envAgentName = explicitName
|
|
1885
|
+
?? (process.env.BRAINCLAW_AGENT_NAME?.trim() || undefined)
|
|
1886
|
+
?? (process.env.BRAINCLAW_AGENT?.trim() || undefined);
|
|
1887
|
+
let identity;
|
|
1888
|
+
let autoRegistered;
|
|
1889
|
+
try {
|
|
1890
|
+
const outcome = resolveOrAutoRegisterAgentIdentity({
|
|
1891
|
+
agentName: envAgentName,
|
|
1892
|
+
agentId: explicitId,
|
|
1893
|
+
cwd,
|
|
1894
|
+
allowCurrent: true,
|
|
1895
|
+
allowEnv: true,
|
|
1896
|
+
});
|
|
1897
|
+
identity = outcome.identity;
|
|
1898
|
+
autoRegistered = outcome.auto_registered;
|
|
1899
|
+
}
|
|
1900
|
+
catch (err) {
|
|
1901
|
+
// Genuine ambiguity — no derivable name. Stays a hard error (KEEP: doctrine
|
|
1902
|
+
// boundary is "ambiguous intent → refuse with next_action", not silence).
|
|
1903
|
+
const detail = err instanceof Error ? err.message : (strictError?.message ?? String(err));
|
|
1904
|
+
throw new Error(`cannot resolve mutation author: ${detail} `
|
|
1905
|
+
+ 'Pass a registered agent, set $BRAINCLAW_AGENT_NAME, '
|
|
1906
|
+
+ 'or register with `brainclaw register-agent <name>` before writing.', { cause: err });
|
|
1907
|
+
}
|
|
1908
|
+
const explicitSessionId = connectionSessionId?.trim() || explicitSessionIdFromEnv();
|
|
1909
|
+
const hadSessionBefore = explicitSessionId
|
|
1910
|
+
? Boolean(loadSessionById(explicitSessionId, cwd))
|
|
1911
|
+
: Boolean(loadCurrentSession(cwd));
|
|
1912
|
+
let sessionAutoCreated;
|
|
1913
|
+
try {
|
|
1914
|
+
const opIdentity = buildOperationalIdentity(identity.agent_name, cwd, {
|
|
1915
|
+
agentId: identity.agent_id,
|
|
1916
|
+
sessionId: explicitSessionId,
|
|
1917
|
+
persistImplicitSession: true,
|
|
1918
|
+
});
|
|
1919
|
+
if (!hadSessionBefore && opIdentity.session_id) {
|
|
1920
|
+
sessionAutoCreated = opIdentity.session_id;
|
|
1921
|
+
const session = loadSessionById(opIdentity.session_id, cwd);
|
|
1922
|
+
if (session && !session.auto_created) {
|
|
1923
|
+
saveCurrentSession({ ...session, auto_created: true }, cwd);
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
catch { /* best-effort — write can still proceed without a persisted session */ }
|
|
1928
|
+
const autoRepair = (autoRegistered || sessionAutoCreated)
|
|
1929
|
+
? {
|
|
1930
|
+
...(autoRegistered ? { agent_auto_registered: true } : {}),
|
|
1931
|
+
...(sessionAutoCreated ? { session_auto_created: sessionAutoCreated } : {}),
|
|
1932
|
+
}
|
|
1933
|
+
: undefined;
|
|
1934
|
+
return {
|
|
1935
|
+
agent_name: identity.agent_name,
|
|
1936
|
+
agent_id: identity.agent_id,
|
|
1937
|
+
...(autoRepair ? { auto_repair: autoRepair } : {}),
|
|
1938
|
+
};
|
|
1939
|
+
}
|
|
1940
|
+
function renderAutoRepairWarning(auto_repair, agent_name) {
|
|
1941
|
+
const parts = [];
|
|
1942
|
+
if (auto_repair.agent_auto_registered) {
|
|
1943
|
+
parts.push(`agent '${agent_name}' auto-registered (first use). Run \`brainclaw register-agent ${agent_name}\` to set capabilities and trust level.`);
|
|
1944
|
+
}
|
|
1945
|
+
if (auto_repair.session_auto_created) {
|
|
1946
|
+
parts.push(`session ${auto_repair.session_auto_created} auto-created for this write.`);
|
|
1947
|
+
}
|
|
1948
|
+
return `⚠️ auto-repair: ${parts.join(' ')}`;
|
|
1847
1949
|
}
|
|
1848
1950
|
function explicitSessionIdFromEnv() {
|
|
1849
1951
|
return process.env.BRAINCLAW_SESSION_ID?.trim()
|
|
@@ -2647,6 +2749,100 @@ function blockCrossProjectExecution(entity, args) {
|
|
|
2647
2749
|
return createToolErrorResponse('validation_error', error instanceof Error ? error.message : String(error));
|
|
2648
2750
|
}
|
|
2649
2751
|
}
|
|
2752
|
+
function matchesCrossProjectLink(ref, cwd) {
|
|
2753
|
+
const trimmed = ref.trim();
|
|
2754
|
+
if (!trimmed)
|
|
2755
|
+
return false;
|
|
2756
|
+
const linkRoots = new Set([path.resolve(cwd), path.resolve(resolveWorkspaceAnchor(cwd))]);
|
|
2757
|
+
for (const root of linkRoots) {
|
|
2758
|
+
for (const link of resolveCrossProjectLinks(root)) {
|
|
2759
|
+
if (link.projectName === trimmed
|
|
2760
|
+
|| link.name === trimmed
|
|
2761
|
+
|| link.path === trimmed
|
|
2762
|
+
|| link.absolutePath === trimmed
|
|
2763
|
+
|| path.basename(link.absolutePath) === trimmed) {
|
|
2764
|
+
return true;
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
return false;
|
|
2769
|
+
}
|
|
2770
|
+
/**
|
|
2771
|
+
* Resolve the destination store for an execution-entity write (plan / claim and
|
|
2772
|
+
* their sub-objects: steps).
|
|
2773
|
+
*
|
|
2774
|
+
* The signaling-only boundary (cnd cross_project_signaling_vs_execution) forbids
|
|
2775
|
+
* driving execution entities into ANOTHER project — but that rule is about
|
|
2776
|
+
* FEDERATION (cross_project_links / other machines), not about workspace siblings
|
|
2777
|
+
* in the same monorepo. Switching into a sibling and creating a plan there is a
|
|
2778
|
+
* purely local operation, and the one the agent actually wants.
|
|
2779
|
+
*
|
|
2780
|
+
* So when `project=X` resolves to a workspace store-chain child (resolveProjectRef
|
|
2781
|
+
* hits — it only matches projects reachable WITHIN this workspace, never a
|
|
2782
|
+
* federated link), we AUTO-LOCALIZE: open a session + session-scoped switch into X
|
|
2783
|
+
* (sticky, per-agent — switchProject auto-creates the session if missing), then
|
|
2784
|
+
* write locally in X. Federated links and unknown names stay blocked.
|
|
2785
|
+
*
|
|
2786
|
+
* DGX dogfood 2026-06-27: without this an agent on the /srv monorepo cannot
|
|
2787
|
+
* `bclaw_create(entity=plan, project=<child>)` — it was rejected as cross-project —
|
|
2788
|
+
* so plans silently fell back to the default project instead.
|
|
2789
|
+
*/
|
|
2790
|
+
function resolveExecutionWriteTarget(entity, args, cwd, connectionSessionId) {
|
|
2791
|
+
const targetProject = getCrossProjectArg(args, 'targetProject', 'target_project', 'crossProject', 'cross_project', 'project');
|
|
2792
|
+
if (!targetProject) {
|
|
2793
|
+
return { targetCwd: cwd, autoSwitched: false };
|
|
2794
|
+
}
|
|
2795
|
+
if (matchesCrossProjectLink(targetProject, cwd)) {
|
|
2796
|
+
const block = blockCrossProjectExecution(entity, args);
|
|
2797
|
+
return {
|
|
2798
|
+
block: block ?? createToolErrorResponse('validation_error', `Cross-project execution write blocked: ${targetProject}`),
|
|
2799
|
+
targetCwd: cwd,
|
|
2800
|
+
autoSwitched: false,
|
|
2801
|
+
};
|
|
2802
|
+
}
|
|
2803
|
+
// Workspace store-chain child (or the workspace root / the current project)?
|
|
2804
|
+
const wsHit = resolveProjectRef(targetProject, cwd);
|
|
2805
|
+
if (wsHit) {
|
|
2806
|
+
// Same-workspace → auto-localize. Switch the session into X (sticky,
|
|
2807
|
+
// session-scoped) so subsequent un-qualified writes follow, and persist the
|
|
2808
|
+
// session under the workspace anchor where resolveEffectiveCwd probes for it
|
|
2809
|
+
// — NOT the effective child cwd, or stickiness would be invisible on the next
|
|
2810
|
+
// call. The switch is best-effort: the write still localizes to wsHit below.
|
|
2811
|
+
let autoSwitched = false;
|
|
2812
|
+
try {
|
|
2813
|
+
const anchor = resolveWorkspaceAnchor(cwd);
|
|
2814
|
+
const sessionId = connectionSessionId ?? explicitSessionIdFromEnv();
|
|
2815
|
+
switchProject(targetProject, { cwd: anchor, sessionOnly: true, sessionId });
|
|
2816
|
+
autoSwitched = true;
|
|
2817
|
+
}
|
|
2818
|
+
catch {
|
|
2819
|
+
/* sticky switch is best-effort */
|
|
2820
|
+
}
|
|
2821
|
+
return { targetCwd: wsHit, autoSwitched, resolvedProject: projectInfoForCwd(wsHit) };
|
|
2822
|
+
}
|
|
2823
|
+
// Not a workspace child → federated link or unknown name. The signaling-only
|
|
2824
|
+
// boundary stands: execution entities never cross a federation boundary.
|
|
2825
|
+
const block = blockCrossProjectExecution(entity, args);
|
|
2826
|
+
return {
|
|
2827
|
+
block: block ?? createToolErrorResponse('validation_error', `Unknown project: ${targetProject}`),
|
|
2828
|
+
targetCwd: cwd,
|
|
2829
|
+
autoSwitched: false,
|
|
2830
|
+
};
|
|
2831
|
+
}
|
|
2832
|
+
/**
|
|
2833
|
+
* Workspace anchor for persisting a session switch — mirrors resolveEffectiveCwd's
|
|
2834
|
+
* anchor selection (BRAINCLAW_CWD when it is a real store, else the outermost
|
|
2835
|
+
* store walking up from cwd) so an auto-switch is found on the next resolution.
|
|
2836
|
+
*/
|
|
2837
|
+
function resolveWorkspaceAnchor(cwd) {
|
|
2838
|
+
const env = process.env.BRAINCLAW_CWD?.trim();
|
|
2839
|
+
if (env) {
|
|
2840
|
+
const resolved = path.resolve(env);
|
|
2841
|
+
if (fs.existsSync(path.join(resolved, MEMORY_DIR, 'config.yaml')))
|
|
2842
|
+
return resolved;
|
|
2843
|
+
}
|
|
2844
|
+
return findOutermostBrainclawRoot(cwd) ?? cwd;
|
|
2845
|
+
}
|
|
2650
2846
|
// Read handlers moved to mcp-read-handlers.ts
|
|
2651
2847
|
import { handleMcpReadToolCall } from './mcp-read-handlers.js';
|
|
2652
2848
|
export { handleMcpReadToolCall };
|
|
@@ -3300,21 +3496,17 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
3300
3496
|
}
|
|
3301
3497
|
}
|
|
3302
3498
|
if (name === 'bclaw_claim') {
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
const
|
|
3310
|
-
|
|
3311
|
-
const resolvedProject = resolveProjectRef(claimProjectArg, cwd);
|
|
3312
|
-
if (resolvedProject)
|
|
3313
|
-
effectiveClaimCwd = resolvedProject;
|
|
3314
|
-
}
|
|
3499
|
+
// project=X naming a workspace sibling auto-localizes (session+switch then
|
|
3500
|
+
// claim locally); federated links / unknown names stay blocked.
|
|
3501
|
+
const claimLoc = resolveExecutionWriteTarget('claim', args, cwd, connectionSessionId);
|
|
3502
|
+
if (claimLoc.block) {
|
|
3503
|
+
return { response: claimLoc.block };
|
|
3504
|
+
}
|
|
3505
|
+
const effectiveClaimCwd = claimLoc.targetCwd;
|
|
3506
|
+
const claimAutoSwitched = claimLoc.autoSwitched;
|
|
3315
3507
|
const storeTarget = args.store ?? 'local';
|
|
3316
3508
|
const claimCwd = resolveTargetStore(effectiveClaimCwd, storeTarget);
|
|
3317
|
-
const resolved = ensureTrust(args, { nameField: 'agent', idField: 'agentId' }, 'contributor',
|
|
3509
|
+
const resolved = ensureTrust(args, { nameField: 'agent', idField: 'agentId' }, 'contributor', cwd, connectionSessionId);
|
|
3318
3510
|
if (resolved.error) {
|
|
3319
3511
|
return { response: createToolErrorResponse(resolved.error.kind, resolved.error.message, resolved.error.details) };
|
|
3320
3512
|
}
|
|
@@ -3329,10 +3521,13 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
3329
3521
|
return { response: createToolErrorResponse('validation_error', descCheck.message) };
|
|
3330
3522
|
}
|
|
3331
3523
|
const resolvedIdentity = resolved.identity;
|
|
3332
|
-
const identity =
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3524
|
+
const identity = {
|
|
3525
|
+
...buildOperationalIdentity(resolvedIdentity.agent_name, cwd, {
|
|
3526
|
+
agentId: resolvedIdentity.agent_id,
|
|
3527
|
+
sessionId: connectionSessionId,
|
|
3528
|
+
}),
|
|
3529
|
+
project_id: loadConfig(claimCwd).project_id,
|
|
3530
|
+
};
|
|
3336
3531
|
const claimId = generateClaimId();
|
|
3337
3532
|
let worktreePath;
|
|
3338
3533
|
let worktreeWarn = '';
|
|
@@ -3438,7 +3633,8 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
3438
3633
|
const worktreeNote = worktreePath ? `\n Worktree: ${worktreePath}` : '';
|
|
3439
3634
|
const expiryNote = claimExpiresAt ? `\n Expires: ${claimExpiresAt.slice(0, 16).replace('T', ' ')} UTC` : '';
|
|
3440
3635
|
const handoffNote = handoffMode ? `\n Handoff: ${handoffMode} (another agent will review and merge)` : '';
|
|
3441
|
-
const
|
|
3636
|
+
const autoSwitchNote = claimAutoSwitched ? `\n Auto-switched → ${projectInfoForCwd(effectiveClaimCwd).name ?? effectiveClaimCwd}` : '';
|
|
3637
|
+
const claimText = `✔ Claimed scope [${claimId}]${worktreeNote}${expiryNote}${handoffNote}${autoSwitchNote}${noPlanWarn}${worktreeWarn}${branchWarn}${staleBranchWarn}${policyWarn}${postClaimText ? `\n${postClaimText}` : ''}`;
|
|
3442
3638
|
return {
|
|
3443
3639
|
response: appendLegacyMcpToolWarning(toolResponse({
|
|
3444
3640
|
content: [{ type: 'text', text: claimText }],
|
|
@@ -3446,6 +3642,7 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
3446
3642
|
session_id: identity.session_id,
|
|
3447
3643
|
worktree_path: worktreePath,
|
|
3448
3644
|
triggered_items: postClaimItems,
|
|
3645
|
+
...(claimAutoSwitched ? { auto_switched: true, resolved_project: projectInfoForCwd(effectiveClaimCwd) } : {}),
|
|
3449
3646
|
}), name),
|
|
3450
3647
|
nextConnectionSessionId: explicitSessionIdFromEnv() ? undefined : identity.session_id,
|
|
3451
3648
|
};
|
|
@@ -3465,18 +3662,43 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
3465
3662
|
catch {
|
|
3466
3663
|
return { response: createToolErrorResponse('not_found', `Claim not found: ${claimId}`) };
|
|
3467
3664
|
}
|
|
3468
|
-
// pln#562 step 5 — release is ownership-checked like acquisition
|
|
3469
|
-
// adoption.
|
|
3470
|
-
//
|
|
3665
|
+
// pln#562 step 5 + trp#928 — release is ownership-checked like acquisition
|
|
3666
|
+
// and adoption. Under the trp#928 tightening the coordinator override is
|
|
3667
|
+
// OPT-IN via coordinator_override:true (implicit "trusted+ = always
|
|
3668
|
+
// override" was too magic — a coordinator releasing a worker's claim
|
|
3669
|
+
// should be a visible act, not a silent side-effect of trust). The
|
|
3670
|
+
// ownership check still enforces:
|
|
3671
|
+
// - owner-of-claim releases (identity matches): allowed, no override needed
|
|
3672
|
+
// - non-owner releases without coordinator_override: rejected loudly (the
|
|
3673
|
+
// error message points the caller at coordinator_override so it is
|
|
3674
|
+
// executable — pln#607 rule).
|
|
3675
|
+
// - non-owner releases with coordinator_override:true but not trusted+:
|
|
3676
|
+
// trust_error (privilege escalation prevention).
|
|
3677
|
+
// - non-owner releases with coordinator_override:true and trusted+:
|
|
3678
|
+
// allowed, audited (auditReleaseOverride).
|
|
3471
3679
|
const releaseIdentity = resolveMutationIdentity(args, { nameField: 'agent', idField: 'agentId' }, cwd, connectionSessionId);
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3680
|
+
if ('error' in releaseIdentity && releaseIdentity.error) {
|
|
3681
|
+
const { kind, message, details } = releaseIdentity.error;
|
|
3682
|
+
return { response: createToolErrorResponse(kind, message, details) };
|
|
3683
|
+
}
|
|
3684
|
+
if (!('identity' in releaseIdentity) || !releaseIdentity.identity) {
|
|
3685
|
+
return { response: createToolErrorResponse('identity_error', 'No registered agent identity resolved for bclaw_release_claim.') };
|
|
3686
|
+
}
|
|
3687
|
+
const coordinatorOverrideRequested = args.coordinator_override === true;
|
|
3688
|
+
if (coordinatorOverrideRequested) {
|
|
3689
|
+
const trustLevel = releaseIdentity.identity.trust_level ?? 'contributor';
|
|
3690
|
+
if (!hasMinimumTrustLevel(trustLevel, 'trusted')) {
|
|
3691
|
+
return {
|
|
3692
|
+
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.`),
|
|
3693
|
+
};
|
|
3478
3694
|
}
|
|
3479
|
-
|
|
3695
|
+
}
|
|
3696
|
+
const releaseAuth = {
|
|
3697
|
+
agent: releaseIdentity.identity.agent_name,
|
|
3698
|
+
agent_id: releaseIdentity.identity.agent_id,
|
|
3699
|
+
session_id: connectionSessionId,
|
|
3700
|
+
override: coordinatorOverrideRequested,
|
|
3701
|
+
};
|
|
3480
3702
|
let cascadeResult;
|
|
3481
3703
|
try {
|
|
3482
3704
|
cascadeResult = releaseClaimWithCascade(claimId, {
|
|
@@ -3971,6 +4193,39 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
3971
4193
|
actor: callerAgent,
|
|
3972
4194
|
actor_id: resolved.identity.agent_id,
|
|
3973
4195
|
}, cwd);
|
|
4196
|
+
// trp#928 — cascade-release the assignment's linked claim on completion.
|
|
4197
|
+
// Before this landing an obedient worker had to make TWO calls to close
|
|
4198
|
+
// the loop (bclaw_assignment_update status=completed AND
|
|
4199
|
+
// bclaw_release_claim); dispatch briefs enumerate both, but not every
|
|
4200
|
+
// sandboxed worker gets through both, and the coordinator's harvest path
|
|
4201
|
+
// only releases on --integrate — so contributor-driven completions left
|
|
4202
|
+
// claims active. The worker's own identity owns the claim (session
|
|
4203
|
+
// adoption), so ownership matches and no coordinator_override is needed.
|
|
4204
|
+
// Silent success/failure is unacceptable: log per-claim outcome.
|
|
4205
|
+
if (status === 'completed' && assignment.claim_id) {
|
|
4206
|
+
try {
|
|
4207
|
+
const { releaseClaimsCascade, logCascadeReleaseResult } = await import('../core/claims.js');
|
|
4208
|
+
const cascade = releaseClaimsCascade([assignment.claim_id], {
|
|
4209
|
+
cwd,
|
|
4210
|
+
planStatus: 'done',
|
|
4211
|
+
auth: {
|
|
4212
|
+
agent: callerAgent,
|
|
4213
|
+
agent_id: resolved.identity.agent_id,
|
|
4214
|
+
session_id: effectiveSessionId,
|
|
4215
|
+
override: false,
|
|
4216
|
+
},
|
|
4217
|
+
});
|
|
4218
|
+
logCascadeReleaseResult({
|
|
4219
|
+
actor: callerAgent,
|
|
4220
|
+
trigger: 'assignment_completed',
|
|
4221
|
+
assignment_id: assignmentId,
|
|
4222
|
+
claim_id: assignment.claim_id,
|
|
4223
|
+
cascade,
|
|
4224
|
+
cwd,
|
|
4225
|
+
});
|
|
4226
|
+
}
|
|
4227
|
+
catch { /* never block the update on cascade release */ }
|
|
4228
|
+
}
|
|
3974
4229
|
// When accepted: auto-acknowledge the inbox message (replaces bclaw_ack_message)
|
|
3975
4230
|
if (status === 'accepted' && assignment.message_id) {
|
|
3976
4231
|
try {
|
|
@@ -4232,9 +4487,9 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
4232
4487
|
}
|
|
4233
4488
|
}
|
|
4234
4489
|
if (name === 'bclaw_add_step') {
|
|
4235
|
-
const
|
|
4236
|
-
if (
|
|
4237
|
-
return { response:
|
|
4490
|
+
const stepLoc = resolveExecutionWriteTarget('plan', args, cwd, connectionSessionId);
|
|
4491
|
+
if (stepLoc.block) {
|
|
4492
|
+
return { response: stepLoc.block };
|
|
4238
4493
|
}
|
|
4239
4494
|
const resolved = ensureTrust(args, { nameField: 'agent', idField: 'agentId' }, 'contributor', cwd, connectionSessionId);
|
|
4240
4495
|
if (resolved.error) {
|
|
@@ -4256,7 +4511,7 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
4256
4511
|
return { response: createToolErrorResponse('validation_error', 'Missing required argument: planId') };
|
|
4257
4512
|
if (!stepText)
|
|
4258
4513
|
return { response: createToolErrorResponse('validation_error', 'Missing required argument: data.text') };
|
|
4259
|
-
const stepTargetCwd =
|
|
4514
|
+
const stepTargetCwd = stepLoc.targetCwd;
|
|
4260
4515
|
try {
|
|
4261
4516
|
const result = addStepOp({ planId: stepPlanId, text: stepText, assignee: stepAssignee, estimatedEffort: stepEstimated, actualEffort: stepActual }, stepTargetCwd);
|
|
4262
4517
|
return {
|
|
@@ -4277,9 +4532,9 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
4277
4532
|
}
|
|
4278
4533
|
}
|
|
4279
4534
|
if (name === 'bclaw_complete_step') {
|
|
4280
|
-
const
|
|
4281
|
-
if (
|
|
4282
|
-
return { response:
|
|
4535
|
+
const csLoc = resolveExecutionWriteTarget('plan', args, cwd, connectionSessionId);
|
|
4536
|
+
if (csLoc.block) {
|
|
4537
|
+
return { response: csLoc.block };
|
|
4283
4538
|
}
|
|
4284
4539
|
const resolved = ensureTrust(args, { nameField: 'agent', idField: 'agentId' }, 'contributor', cwd, connectionSessionId);
|
|
4285
4540
|
if (resolved.error) {
|
|
@@ -4291,7 +4546,7 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
4291
4546
|
return { response: createToolErrorResponse('validation_error', 'Missing required argument: planId') };
|
|
4292
4547
|
if (!csStepId)
|
|
4293
4548
|
return { response: createToolErrorResponse('validation_error', 'Missing required argument: stepId') };
|
|
4294
|
-
const csTargetCwd =
|
|
4549
|
+
const csTargetCwd = csLoc.targetCwd;
|
|
4295
4550
|
try {
|
|
4296
4551
|
const result = completeStepOp({ planId: csPlanId, stepId: csStepId }, csTargetCwd);
|
|
4297
4552
|
return {
|
|
@@ -4313,9 +4568,9 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
4313
4568
|
}
|
|
4314
4569
|
}
|
|
4315
4570
|
if (name === 'bclaw_update_step') {
|
|
4316
|
-
const
|
|
4317
|
-
if (
|
|
4318
|
-
return { response:
|
|
4571
|
+
const usLoc = resolveExecutionWriteTarget('plan', args, cwd, connectionSessionId);
|
|
4572
|
+
if (usLoc.block) {
|
|
4573
|
+
return { response: usLoc.block };
|
|
4319
4574
|
}
|
|
4320
4575
|
const resolved = ensureTrust(args, { nameField: 'agent', idField: 'agentId' }, 'contributor', cwd, connectionSessionId);
|
|
4321
4576
|
if (resolved.error) {
|
|
@@ -4331,7 +4586,7 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
4331
4586
|
if (args.status && !validStatuses.includes(String(args.status))) {
|
|
4332
4587
|
return { response: createToolErrorResponse('validation_error', `Invalid status: ${args.status}. Valid: ${validStatuses.join(', ')}`) };
|
|
4333
4588
|
}
|
|
4334
|
-
const usTargetCwd =
|
|
4589
|
+
const usTargetCwd = usLoc.targetCwd;
|
|
4335
4590
|
try {
|
|
4336
4591
|
const result = updateStepOp({
|
|
4337
4592
|
planId: usPlanId,
|
|
@@ -4372,9 +4627,9 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
4372
4627
|
}
|
|
4373
4628
|
}
|
|
4374
4629
|
if (name === 'bclaw_delete_step') {
|
|
4375
|
-
const
|
|
4376
|
-
if (
|
|
4377
|
-
return { response:
|
|
4630
|
+
const dsLoc = resolveExecutionWriteTarget('plan', args, cwd, connectionSessionId);
|
|
4631
|
+
if (dsLoc.block) {
|
|
4632
|
+
return { response: dsLoc.block };
|
|
4378
4633
|
}
|
|
4379
4634
|
const resolved = ensureTrust(args, { nameField: 'agent', idField: 'agentId' }, 'contributor', cwd, connectionSessionId);
|
|
4380
4635
|
if (resolved.error) {
|
|
@@ -4386,7 +4641,7 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
4386
4641
|
return { response: createToolErrorResponse('validation_error', 'Missing required argument: planId') };
|
|
4387
4642
|
if (!dsStepId)
|
|
4388
4643
|
return { response: createToolErrorResponse('validation_error', 'Missing required argument: stepId') };
|
|
4389
|
-
const dsTargetCwd =
|
|
4644
|
+
const dsTargetCwd = dsLoc.targetCwd;
|
|
4390
4645
|
try {
|
|
4391
4646
|
const result = deleteStepOp({ planId: dsPlanId, stepId: dsStepId }, dsTargetCwd);
|
|
4392
4647
|
return {
|
|
@@ -4407,10 +4662,11 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
4407
4662
|
}
|
|
4408
4663
|
}
|
|
4409
4664
|
if (name === 'bclaw_delete_plan') {
|
|
4410
|
-
const
|
|
4411
|
-
if (
|
|
4412
|
-
return { response:
|
|
4665
|
+
const dpLoc = resolveExecutionWriteTarget('plan', args, cwd, connectionSessionId);
|
|
4666
|
+
if (dpLoc.block) {
|
|
4667
|
+
return { response: dpLoc.block };
|
|
4413
4668
|
}
|
|
4669
|
+
const dpTargetCwd = dpLoc.targetCwd;
|
|
4414
4670
|
const resolved = ensureTrust(args, { nameField: 'agent', idField: 'agentId' }, 'trusted', cwd, connectionSessionId);
|
|
4415
4671
|
if (resolved.error) {
|
|
4416
4672
|
return { response: createToolErrorResponse(resolved.error.kind, resolved.error.message, resolved.error.details) };
|
|
@@ -4419,7 +4675,7 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
4419
4675
|
if (!dpId)
|
|
4420
4676
|
return { response: createToolErrorResponse('validation_error', 'Missing required argument: id') };
|
|
4421
4677
|
try {
|
|
4422
|
-
const result = deletePlanOp(dpId,
|
|
4678
|
+
const result = deletePlanOp(dpId, dpTargetCwd);
|
|
4423
4679
|
return {
|
|
4424
4680
|
response: toolResponse({
|
|
4425
4681
|
content: [{ type: 'text', text: `✔ Plan deleted: [${result.id}] ${result.text.slice(0, 80)}` }],
|
|
@@ -6418,17 +6674,38 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
6418
6674
|
// only checks known keys), letting the caller believe the filter had
|
|
6419
6675
|
// applied when it hadn't. Under the new contract, an unknown key is
|
|
6420
6676
|
// a validation_error listing the keys actually honored.
|
|
6677
|
+
// trp#928 — the entity-scoping error is now first-class: the doc says
|
|
6678
|
+
// assignment_id/claim_id/message_id are entity='agent_run' only, but
|
|
6679
|
+
// before this the rejection message called them 'unknown', misleading
|
|
6680
|
+
// callers who'd cross-reference the description. Now the message names
|
|
6681
|
+
// the constraint AND the entity that DOES accept the key so the user
|
|
6682
|
+
// can fix the call without hunting through docs. (pln#599 docs-vs-facts.)
|
|
6421
6683
|
const KNOWN_FILTER_KEYS = new Set([
|
|
6422
6684
|
'status', 'tag', 'tags', 'author', 'plan_id', 'source', 'auto_generated',
|
|
6423
6685
|
'assignment_id', 'claim_id', 'message_id',
|
|
6424
6686
|
'limit', 'offset', 'includeLegacy', 'minAutoReflectConfidence',
|
|
6425
6687
|
]);
|
|
6426
6688
|
const agentRunOnlyFilterKeys = new Set(['assignment_id', 'claim_id', 'message_id']);
|
|
6427
|
-
const
|
|
6428
|
-
|
|
6689
|
+
const providedKeys = Object.keys(filter);
|
|
6690
|
+
const unknownKeys = providedKeys.filter((k) => !KNOWN_FILTER_KEYS.has(k));
|
|
6691
|
+
const misScopedKeys = providedKeys.filter((k) => agentRunOnlyFilterKeys.has(k) && entity !== 'agent_run');
|
|
6692
|
+
if (unknownKeys.length > 0 || misScopedKeys.length > 0) {
|
|
6693
|
+
const parts = [];
|
|
6694
|
+
if (unknownKeys.length > 0) {
|
|
6695
|
+
parts.push(`Unknown filter key(s): ${unknownKeys.map((k) => `"${k}"`).join(', ')}. Accepted keys: ${[...KNOWN_FILTER_KEYS].sort().join(', ')}.`);
|
|
6696
|
+
}
|
|
6697
|
+
if (misScopedKeys.length > 0) {
|
|
6698
|
+
parts.push(`Filter key(s) ${misScopedKeys.map((k) => `"${k}"`).join(', ')} are only valid for entity="agent_run" `
|
|
6699
|
+
+ `(this call used entity="${entity}"). `
|
|
6700
|
+
+ `Retry with entity="agent_run", or drop the ${misScopedKeys.join('/')} filter.`);
|
|
6701
|
+
}
|
|
6429
6702
|
return {
|
|
6430
|
-
response: createToolErrorResponse('validation_error',
|
|
6431
|
-
|
|
6703
|
+
response: createToolErrorResponse('validation_error', parts.join(' '), {
|
|
6704
|
+
unknown_keys: unknownKeys,
|
|
6705
|
+
mis_scoped_keys: misScopedKeys,
|
|
6706
|
+
accepted_keys: [...KNOWN_FILTER_KEYS].sort(),
|
|
6707
|
+
agent_run_only_keys: [...agentRunOnlyFilterKeys],
|
|
6708
|
+
}),
|
|
6432
6709
|
};
|
|
6433
6710
|
}
|
|
6434
6711
|
const result = listEntities(entity, targetCwd, filter);
|
|
@@ -6537,15 +6814,22 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
6537
6814
|
if (name === 'bclaw_create') {
|
|
6538
6815
|
try {
|
|
6539
6816
|
const entity = String(args.entity ?? '');
|
|
6540
|
-
// Execution entities
|
|
6541
|
-
//
|
|
6817
|
+
// Execution entities (plan/claim) auto-localize into a workspace sibling
|
|
6818
|
+
// when project=X names one: session+switch then write locally. Only
|
|
6819
|
+
// federated links / unknown names are blocked (signaling-only boundary).
|
|
6820
|
+
let targetCwd;
|
|
6821
|
+
let autoSwitched = false;
|
|
6542
6822
|
if (entity === 'claim' || entity === 'plan') {
|
|
6543
|
-
const
|
|
6544
|
-
if (
|
|
6545
|
-
return { response:
|
|
6823
|
+
const loc = resolveExecutionWriteTarget(entity, args, cwd, connectionSessionId);
|
|
6824
|
+
if (loc.block)
|
|
6825
|
+
return { response: loc.block };
|
|
6826
|
+
targetCwd = loc.targetCwd;
|
|
6827
|
+
autoSwitched = loc.autoSwitched;
|
|
6828
|
+
}
|
|
6829
|
+
else {
|
|
6830
|
+
targetCwd = resolveProjectCwd(args.project, cwd);
|
|
6546
6831
|
}
|
|
6547
6832
|
const rawData = (args.data ?? {});
|
|
6548
|
-
const targetCwd = resolveProjectCwd(args.project, cwd);
|
|
6549
6833
|
const targetScope = scopeMetadataForTarget(args, targetCwd, scopeInfo);
|
|
6550
6834
|
// Auto-fill identity fields. Without this, a caller who omits author/agent
|
|
6551
6835
|
// creates a schema-invalid record that is silently dropped on read and
|
|
@@ -6560,28 +6844,36 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
6560
6844
|
const data = { ...rawData };
|
|
6561
6845
|
let actor = typeof data.author === 'string' ? data.author : undefined;
|
|
6562
6846
|
let actorId = typeof data.agent_id === 'string' ? data.agent_id : undefined;
|
|
6847
|
+
let autoRepair;
|
|
6563
6848
|
if (data.author === undefined) {
|
|
6564
|
-
const
|
|
6565
|
-
data.author = agent_name;
|
|
6849
|
+
const author = resolveCanonicalAuthor(args, cwd, connectionSessionId);
|
|
6850
|
+
data.author = author.agent_name;
|
|
6566
6851
|
if (data.agent === undefined)
|
|
6567
|
-
data.agent = agent_name;
|
|
6568
|
-
if (data.agent_id === undefined && agent_id)
|
|
6569
|
-
data.agent_id = agent_id;
|
|
6570
|
-
actor = agent_name;
|
|
6571
|
-
actorId = agent_id;
|
|
6852
|
+
data.agent = author.agent_name;
|
|
6853
|
+
if (data.agent_id === undefined && author.agent_id)
|
|
6854
|
+
data.agent_id = author.agent_id;
|
|
6855
|
+
actor = author.agent_name;
|
|
6856
|
+
actorId = author.agent_id;
|
|
6857
|
+
autoRepair = author.auto_repair;
|
|
6572
6858
|
}
|
|
6573
6859
|
else if (data.agent === undefined) {
|
|
6574
6860
|
data.agent = data.author;
|
|
6575
6861
|
}
|
|
6576
6862
|
const result = createEntity(entity, data, targetCwd);
|
|
6577
6863
|
appendAuditEntry({ actor: actor ?? 'unknown', ...(actorId ? { actor_id: actorId } : {}), action: 'create', item_id: result.id, item_type: entity }, targetCwd);
|
|
6864
|
+
const createText = `✔ created ${entity} ${result.id}${autoSwitched ? ` (auto-switched → ${targetScope.resolved_project.name ?? targetScope.resolved_project.path})` : ''}`;
|
|
6865
|
+
const createContent = autoRepair
|
|
6866
|
+
? [{ type: 'text', text: createText }, { type: 'text', text: renderAutoRepairWarning(autoRepair, actor ?? 'unknown') }]
|
|
6867
|
+
: [{ type: 'text', text: createText }];
|
|
6578
6868
|
return {
|
|
6579
6869
|
response: toolResponse({
|
|
6580
|
-
content:
|
|
6870
|
+
content: createContent,
|
|
6581
6871
|
structuredContent: {
|
|
6582
6872
|
...result,
|
|
6583
6873
|
resolved_project: targetScope.resolved_project,
|
|
6584
|
-
active_source: targetScope.active_source,
|
|
6874
|
+
active_source: autoSwitched ? 'auto_switch' : targetScope.active_source,
|
|
6875
|
+
...(autoSwitched ? { auto_switched: true } : {}),
|
|
6876
|
+
...(autoRepair ? { auto_repair: autoRepair } : {}),
|
|
6585
6877
|
},
|
|
6586
6878
|
}),
|
|
6587
6879
|
};
|
|
@@ -6597,16 +6889,21 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
6597
6889
|
const patch = (args.patch ?? {});
|
|
6598
6890
|
const targetCwd = resolveProjectCwd(args.project, cwd);
|
|
6599
6891
|
const targetScope = scopeMetadataForTarget(args, targetCwd, scopeInfo);
|
|
6600
|
-
const { agent_name, agent_id } = resolveCanonicalAuthor(args, cwd, connectionSessionId);
|
|
6892
|
+
const { agent_name, agent_id, auto_repair } = resolveCanonicalAuthor(args, cwd, connectionSessionId);
|
|
6601
6893
|
const result = updateEntity(entity, id, patch, targetCwd);
|
|
6602
6894
|
appendAuditEntry({ actor: agent_name, ...(agent_id ? { actor_id: agent_id } : {}), action: 'update', item_id: id, item_type: entity }, targetCwd);
|
|
6895
|
+
const updateText = `✔ updated ${entity} ${id}`;
|
|
6896
|
+
const updateContent = auto_repair
|
|
6897
|
+
? [{ type: 'text', text: updateText }, { type: 'text', text: renderAutoRepairWarning(auto_repair, agent_name) }]
|
|
6898
|
+
: [{ type: 'text', text: updateText }];
|
|
6603
6899
|
return {
|
|
6604
6900
|
response: toolResponse({
|
|
6605
|
-
content:
|
|
6901
|
+
content: updateContent,
|
|
6606
6902
|
structuredContent: {
|
|
6607
6903
|
...result,
|
|
6608
6904
|
resolved_project: targetScope.resolved_project,
|
|
6609
6905
|
active_source: targetScope.active_source,
|
|
6906
|
+
...(auto_repair ? { auto_repair } : {}),
|
|
6610
6907
|
},
|
|
6611
6908
|
}),
|
|
6612
6909
|
};
|
|
@@ -6622,16 +6919,21 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
6622
6919
|
const purge = args.purge === true;
|
|
6623
6920
|
const targetCwd = resolveProjectCwd(args.project, cwd);
|
|
6624
6921
|
const targetScope = scopeMetadataForTarget(args, targetCwd, scopeInfo);
|
|
6625
|
-
const { agent_name, agent_id } = resolveCanonicalAuthor(args, cwd, connectionSessionId);
|
|
6922
|
+
const { agent_name, agent_id, auto_repair } = resolveCanonicalAuthor(args, cwd, connectionSessionId);
|
|
6626
6923
|
const result = removeEntity(entity, id, targetCwd, purge);
|
|
6627
6924
|
appendAuditEntry({ actor: agent_name, ...(agent_id ? { actor_id: agent_id } : {}), action: 'delete', item_id: id, item_type: entity, reason: purge ? 'purged' : 'archived' }, targetCwd);
|
|
6925
|
+
const removeText = `✔ removed ${entity} ${id}`;
|
|
6926
|
+
const removeContent = auto_repair
|
|
6927
|
+
? [{ type: 'text', text: removeText }, { type: 'text', text: renderAutoRepairWarning(auto_repair, agent_name) }]
|
|
6928
|
+
: [{ type: 'text', text: removeText }];
|
|
6628
6929
|
return {
|
|
6629
6930
|
response: toolResponse({
|
|
6630
|
-
content:
|
|
6931
|
+
content: removeContent,
|
|
6631
6932
|
structuredContent: {
|
|
6632
6933
|
...result,
|
|
6633
6934
|
resolved_project: targetScope.resolved_project,
|
|
6634
6935
|
active_source: targetScope.active_source,
|
|
6936
|
+
...(auto_repair ? { auto_repair } : {}),
|
|
6635
6937
|
},
|
|
6636
6938
|
}),
|
|
6637
6939
|
};
|
|
@@ -6647,13 +6949,20 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
6647
6949
|
const toProject = String(args.to_project ?? '');
|
|
6648
6950
|
const fromProject = typeof args.from_project === 'string' ? args.from_project : undefined;
|
|
6649
6951
|
const force = args.force === true;
|
|
6650
|
-
const { agent_name, agent_id } = resolveCanonicalAuthor(args, cwd, connectionSessionId);
|
|
6952
|
+
const { agent_name, agent_id, auto_repair } = resolveCanonicalAuthor(args, cwd, connectionSessionId);
|
|
6651
6953
|
const result = relocateEntity({ entity, id, toProject, fromProject, force, cwd, actor: agent_name, actorId: agent_id });
|
|
6652
6954
|
const warn = result.warnings.length ? ` (${result.warnings.length} warning(s))` : '';
|
|
6955
|
+
const moveText = `✔ moved ${entity} ${id} → ${result.to}${warn}`;
|
|
6956
|
+
const moveContent = auto_repair
|
|
6957
|
+
? [{ type: 'text', text: moveText }, { type: 'text', text: renderAutoRepairWarning(auto_repair, agent_name) }]
|
|
6958
|
+
: [{ type: 'text', text: moveText }];
|
|
6653
6959
|
return {
|
|
6654
6960
|
response: toolResponse({
|
|
6655
|
-
content:
|
|
6656
|
-
structuredContent: {
|
|
6961
|
+
content: moveContent,
|
|
6962
|
+
structuredContent: {
|
|
6963
|
+
...result,
|
|
6964
|
+
...(auto_repair ? { auto_repair } : {}),
|
|
6965
|
+
},
|
|
6657
6966
|
}),
|
|
6658
6967
|
};
|
|
6659
6968
|
}
|
|
@@ -6664,28 +6973,67 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
6664
6973
|
if (name === 'bclaw_transition') {
|
|
6665
6974
|
try {
|
|
6666
6975
|
const entity = String(args.entity ?? '');
|
|
6667
|
-
// Same
|
|
6668
|
-
//
|
|
6976
|
+
// Same auto-localize as bclaw_create: a workspace sibling named by
|
|
6977
|
+
// project=X is switched into and transitioned locally; only federated
|
|
6978
|
+
// links / unknown names are blocked (signaling-only boundary).
|
|
6979
|
+
let targetCwd;
|
|
6980
|
+
let autoSwitched = false;
|
|
6669
6981
|
if (entity === 'claim' || entity === 'plan') {
|
|
6670
|
-
const
|
|
6671
|
-
if (
|
|
6672
|
-
return { response:
|
|
6982
|
+
const loc = resolveExecutionWriteTarget(entity, args, cwd, connectionSessionId);
|
|
6983
|
+
if (loc.block)
|
|
6984
|
+
return { response: loc.block };
|
|
6985
|
+
targetCwd = loc.targetCwd;
|
|
6986
|
+
autoSwitched = loc.autoSwitched;
|
|
6987
|
+
}
|
|
6988
|
+
else {
|
|
6989
|
+
targetCwd = resolveProjectCwd(args.project, cwd);
|
|
6673
6990
|
}
|
|
6674
6991
|
const id = String(args.id ?? '');
|
|
6675
6992
|
const to = String(args.to ?? '');
|
|
6676
6993
|
const reason = args.reason;
|
|
6677
|
-
const targetCwd = resolveProjectCwd(args.project, cwd);
|
|
6678
6994
|
const targetScope = scopeMetadataForTarget(args, targetCwd, scopeInfo);
|
|
6679
|
-
const { agent_name, agent_id } = resolveCanonicalAuthor(args, cwd, connectionSessionId);
|
|
6680
|
-
|
|
6995
|
+
const { agent_name, agent_id, auto_repair } = resolveCanonicalAuthor(args, cwd, connectionSessionId);
|
|
6996
|
+
// trp#928 — claim transitions consume the ReleaseClaimAuth ownership
|
|
6997
|
+
// check (released/stale both mutate a claim owned by SOME agent). Reuse
|
|
6998
|
+
// the same coordinator_override opt-in as bclaw_release_claim so both
|
|
6999
|
+
// paths have identical trust semantics and the same executable error.
|
|
7000
|
+
let transitionAuth;
|
|
7001
|
+
if (entity === 'claim') {
|
|
7002
|
+
const transitionIdentity = resolveMutationIdentity(args, { nameField: 'agent', idField: 'agentId' }, targetCwd, connectionSessionId);
|
|
7003
|
+
const coordinatorOverrideRequested = args.coordinator_override === true;
|
|
7004
|
+
if (coordinatorOverrideRequested) {
|
|
7005
|
+
const identity = 'identity' in transitionIdentity ? transitionIdentity.identity : undefined;
|
|
7006
|
+
const trustLevel = identity?.trust_level ?? 'contributor';
|
|
7007
|
+
if (!hasMinimumTrustLevel(trustLevel, 'trusted')) {
|
|
7008
|
+
return {
|
|
7009
|
+
response: createToolErrorResponse('trust_error', `coordinator_override:true requires trust_level 'trusted' or higher — caller is '${trustLevel}'.`),
|
|
7010
|
+
};
|
|
7011
|
+
}
|
|
7012
|
+
}
|
|
7013
|
+
transitionAuth = 'identity' in transitionIdentity && transitionIdentity.identity
|
|
7014
|
+
? {
|
|
7015
|
+
agent: transitionIdentity.identity.agent_name,
|
|
7016
|
+
agent_id: transitionIdentity.identity.agent_id,
|
|
7017
|
+
session_id: connectionSessionId,
|
|
7018
|
+
override: coordinatorOverrideRequested,
|
|
7019
|
+
}
|
|
7020
|
+
: undefined;
|
|
7021
|
+
}
|
|
7022
|
+
const result = transitionEntity(entity, id, to, targetCwd, reason, transitionAuth);
|
|
6681
7023
|
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);
|
|
7024
|
+
const transitionText = `✔ ${entity} ${id}: ${result.from} → ${to}${autoSwitched ? ` (auto-switched → ${targetScope.resolved_project.name ?? targetScope.resolved_project.path})` : ''}`;
|
|
7025
|
+
const transitionContent = auto_repair
|
|
7026
|
+
? [{ type: 'text', text: transitionText }, { type: 'text', text: renderAutoRepairWarning(auto_repair, agent_name) }]
|
|
7027
|
+
: [{ type: 'text', text: transitionText }];
|
|
6682
7028
|
return {
|
|
6683
7029
|
response: toolResponse({
|
|
6684
|
-
content:
|
|
7030
|
+
content: transitionContent,
|
|
6685
7031
|
structuredContent: {
|
|
6686
7032
|
...result,
|
|
6687
7033
|
resolved_project: targetScope.resolved_project,
|
|
6688
|
-
active_source: targetScope.active_source,
|
|
7034
|
+
active_source: autoSwitched ? 'auto_switch' : targetScope.active_source,
|
|
7035
|
+
...(autoSwitched ? { auto_switched: true } : {}),
|
|
7036
|
+
...(auto_repair ? { auto_repair } : {}),
|
|
6689
7037
|
},
|
|
6690
7038
|
}),
|
|
6691
7039
|
};
|