brainclaw 1.12.0 → 1.14.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 +40 -0
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/cli.js +11 -3
- package/dist/commands/claim-resource.js +1 -0
- package/dist/commands/doctor.js +98 -0
- package/dist/commands/estimation-report.js +1 -1
- package/dist/commands/harvest.js +38 -24
- package/dist/commands/mcp.js +328 -46
- package/dist/commands/release-claim.js +21 -1
- package/dist/commands/session-start.js +16 -1
- package/dist/core/agent-capability.js +15 -4
- package/dist/core/agent-registry.js +7 -1
- package/dist/core/assignment-sweeper.js +92 -11
- 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/gc-semantic.js +79 -0
- package/dist/core/hint-aging.js +188 -0
- package/dist/core/hygiene-policy.js +77 -0
- package/dist/core/loops/store.js +33 -0
- package/dist/core/reputation.js +18 -0
- package/dist/core/schema.js +29 -0
- package/dist/core/worktree.js +198 -22
- package/dist/facts.js +36 -3
- package/dist/facts.json +35 -2
- package/docs/concepts/dispatch-supervisor.md +393 -0
- package/docs/mcp-schema-changelog.md +7 -2
- package/package.json +6 -4
package/dist/commands/mcp.js
CHANGED
|
@@ -9,6 +9,10 @@ import { generatedSchemas } from './mcp-schemas.generated.js';
|
|
|
9
9
|
import { getTriggeredItems, renderTriggeredItems } from '../core/lifecycle.js';
|
|
10
10
|
import { resolveCrossProjectLinks, resolveCrossProjectWritableTarget, resolveProjectCwd, writeCrossProjectSignal } from '../core/cross-project.js';
|
|
11
11
|
import { buildContext, renderContextMarkdown, renderContextPromptTemplate, renderContextBriefing } from '../core/context.js';
|
|
12
|
+
import { ageStaleWarnings, ageWorkflowHints, loadServeRegistry } from '../core/hint-aging.js';
|
|
13
|
+
import { loadHygienePolicy } from '../core/hygiene-policy.js';
|
|
14
|
+
import { sweepAssignmentsAtReadPath, selectReadPathSweepCandidates } from '../core/assignment-sweeper.js';
|
|
15
|
+
import { loadAssignment } from '../core/assignments.js';
|
|
12
16
|
import { buildCoordinationSnapshot } from '../core/coordination.js';
|
|
13
17
|
import { checkBrainclawInstallableUpdate, getInstalledBrainclawVersion, readDiskBrainclawVersion, renderBrainclawInstallableUpdateNotice } from '../core/brainclaw-version.js';
|
|
14
18
|
import { loadConfig } from '../core/config.js';
|
|
@@ -31,13 +35,14 @@ import { rejectCandidate } from './reject.js';
|
|
|
31
35
|
import { startSession } from './session-start.js';
|
|
32
36
|
import { endSession } from './session-end.js';
|
|
33
37
|
import { applyHandoffUpdates } from './update-handoff.js';
|
|
34
|
-
import { AgentIdentityResolutionError, AgentTrustError, findAgentIdentityById, findAgentIdentityByName, hasMinimumTrustLevel, normalizeAgentName, requireMinimumTrustLevel, requireRegisteredAgentIdentity, resolveCurrentAgentIdentity, resolveCurrentModel, ensureAgentRegisteredForDispatch, } from '../core/agent-registry.js';
|
|
38
|
+
import { AgentIdentityResolutionError, AgentTrustError, findAgentIdentityById, findAgentIdentityByName, hasMinimumTrustLevel, normalizeAgentName, requireMinimumTrustLevel, requireRegisteredAgentIdentity, resolveCurrentAgentIdentity, resolveCurrentModel, ensureAgentRegisteredForDispatch, resolveOrAutoRegisterAgentIdentity, } from '../core/agent-registry.js';
|
|
35
39
|
import { appendAuditEntry } from '../core/audit.js';
|
|
36
40
|
import { nowISO, generateId } from '../core/ids.js';
|
|
37
|
-
import { buildOperationalIdentity, loadAllSessions, loadSessionById } from '../core/identity.js';
|
|
41
|
+
import { buildOperationalIdentity, loadAllSessions, loadCurrentSession, loadSessionById, saveCurrentSession } from '../core/identity.js';
|
|
38
42
|
import { validateMcpInput, validateMcpField } from '../core/input-validation.js';
|
|
39
43
|
import { createCapability, createTool as createRegistryTool } from '../core/registries.js';
|
|
40
44
|
import { detectAiAgent } from '../core/ai-agent-detection.js';
|
|
45
|
+
import { isObserverMode } from '../core/observer-mode.js';
|
|
41
46
|
import { checkGitPresence, scanGitRepos, parseRoots, parseRepoSelection, parseAgentSelection, getDetectedSetupAgentNames, getInstalledAgentNames, runGlobalInstall, initReposAndConfigureAgents, readSetupState, ALL_KNOWN_AGENTS, } from './setup.js';
|
|
42
47
|
import { buildAgentInventory } from '../core/agent-inventory.js';
|
|
43
48
|
import { findOutermostBrainclawRoot, resolveEffectiveCwd, resolveEffectiveCwdInfo, resolveProjectRef, resolveTargetStore } from '../core/store-resolution.js';
|
|
@@ -656,13 +661,17 @@ const MCP_WRITE_TOOLS = [
|
|
|
656
661
|
},
|
|
657
662
|
{
|
|
658
663
|
name: 'bclaw_release_claim',
|
|
659
|
-
description: 'Release a work claim.',
|
|
664
|
+
description: 'Release a work claim. Callers own their own claims; a trusted+ coordinator releasing another agent\'s claim MUST pass coordinator_override:true (audited).',
|
|
660
665
|
annotations: { tier: 'standard', category: 'coordination', headlessApproval: 'auto' },
|
|
661
666
|
inputSchema: {
|
|
662
667
|
type: 'object',
|
|
663
668
|
properties: {
|
|
664
669
|
id: { type: 'string', description: 'Claim ID to release.' },
|
|
665
670
|
planStatus: { type: 'string', description: 'Optional: update linked plan status.' },
|
|
671
|
+
coordinator_override: {
|
|
672
|
+
type: 'boolean',
|
|
673
|
+
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.',
|
|
674
|
+
},
|
|
666
675
|
},
|
|
667
676
|
required: ['id'],
|
|
668
677
|
},
|
|
@@ -1188,7 +1197,7 @@ const MCP_WRITE_TOOLS = [
|
|
|
1188
1197
|
type: 'object',
|
|
1189
1198
|
properties: {
|
|
1190
1199
|
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.' },
|
|
1191
|
-
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).
|
|
1200
|
+
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.' },
|
|
1192
1201
|
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`.' },
|
|
1193
1202
|
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.' },
|
|
1194
1203
|
},
|
|
@@ -1256,7 +1265,7 @@ const MCP_WRITE_TOOLS = [
|
|
|
1256
1265
|
},
|
|
1257
1266
|
{
|
|
1258
1267
|
name: 'bclaw_transition',
|
|
1259
|
-
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.',
|
|
1268
|
+
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).',
|
|
1260
1269
|
annotations: { tier: 'standard', category: 'memory', headlessApproval: 'prompt' },
|
|
1261
1270
|
inputSchema: {
|
|
1262
1271
|
type: 'object',
|
|
@@ -1266,6 +1275,7 @@ const MCP_WRITE_TOOLS = [
|
|
|
1266
1275
|
to: { type: 'string', description: 'Target status.' },
|
|
1267
1276
|
reason: { type: 'string', description: 'Optional free-text reason, audited alongside the transition.' },
|
|
1268
1277
|
project: { type: 'string', description: 'Optional: name of a linked project to transition the entity in. Defaults to the current project.' },
|
|
1278
|
+
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.' },
|
|
1269
1279
|
},
|
|
1270
1280
|
required: ['entity', 'id', 'to'],
|
|
1271
1281
|
},
|
|
@@ -1829,10 +1839,22 @@ function ensureTrust(args, fields, level, cwd, sessionId) {
|
|
|
1829
1839
|
* missing field — which would then be silently GC'd by the state sync loop
|
|
1830
1840
|
* (see fix plan pln_5f44426c).
|
|
1831
1841
|
*
|
|
1832
|
-
* pln#562 step 3 —
|
|
1833
|
-
* author
|
|
1834
|
-
* invalid on read and silently GC'd
|
|
1835
|
-
*
|
|
1842
|
+
* pln#562 step 3 — a write that would create a record with a missing/'unknown'
|
|
1843
|
+
* author must never be silent (that produced records that passed creation but
|
|
1844
|
+
* were schema-invalid on read and silently GC'd from disk).
|
|
1845
|
+
*
|
|
1846
|
+
* pln#608 — extended with auto-repair: when the caller has no session but a
|
|
1847
|
+
* derivable agent name (arg / $BRAINCLAW_AGENT_NAME / detected AI agent),
|
|
1848
|
+
* fall through to `resolveOrAutoRegisterAgentIdentity` and materialize the
|
|
1849
|
+
* session via `buildOperationalIdentity({ persistImplicitSession: true })`
|
|
1850
|
+
* (same mechanic as switchProject:86-106 and session-start). The freshly-
|
|
1851
|
+
* created session is tagged `auto_created` so aggressive harvesting can
|
|
1852
|
+
* distinguish it from operator sessions (pln#602). The caller receives
|
|
1853
|
+
* `auto_repair` and surfaces it as a warning — never silent.
|
|
1854
|
+
*
|
|
1855
|
+
* KEEP (still a hard error, doctrine boundary): the identity is ambiguous
|
|
1856
|
+
* (no name in args, no env signal, no detectable agent). We do not invent
|
|
1857
|
+
* an identity — invoke intent is unclear and the write would misattribute.
|
|
1836
1858
|
*/
|
|
1837
1859
|
function resolveCanonicalAuthor(args, cwd, connectionSessionId) {
|
|
1838
1860
|
const resolved = resolveMutationIdentity(args, { nameField: 'agent', idField: 'agentId' }, cwd, connectionSessionId);
|
|
@@ -1842,9 +1864,92 @@ function resolveCanonicalAuthor(args, cwd, connectionSessionId) {
|
|
|
1842
1864
|
agent_id: resolved.identity.agent_id,
|
|
1843
1865
|
};
|
|
1844
1866
|
}
|
|
1845
|
-
const
|
|
1846
|
-
|
|
1847
|
-
|
|
1867
|
+
const strictError = 'error' in resolved && resolved.error ? resolved.error : undefined;
|
|
1868
|
+
// KEEP (doctrine boundary): a pinned principal that rejected the caller args
|
|
1869
|
+
// is a SPOOF/MISMATCH, not an ambiguous first-write. Never auto-repair over
|
|
1870
|
+
// it — silently re-attributing would defeat pln#562 step 3. The strict error
|
|
1871
|
+
// already carries the pointer to a curator override.
|
|
1872
|
+
if (resolveConnectionPrincipal(cwd, connectionSessionId)) {
|
|
1873
|
+
throw new Error(`cannot resolve mutation author: ${strictError?.message ?? 'principal mismatch'}`);
|
|
1874
|
+
}
|
|
1875
|
+
// Observer processes are read-only dashboards/inspectors. Even when an env
|
|
1876
|
+
// variable leaks an agent name into the observer process, canonical writes
|
|
1877
|
+
// must not use the auto-repair path because it can mint identity/session
|
|
1878
|
+
// state as a side effect.
|
|
1879
|
+
if (isObserverMode()) {
|
|
1880
|
+
throw new Error(`cannot resolve mutation author: ${strictError?.message ?? 'observer mode cannot auto-repair identity/session state'}`);
|
|
1881
|
+
}
|
|
1882
|
+
const explicitName = typeof args.agent === 'string' ? args.agent : undefined;
|
|
1883
|
+
const explicitId = typeof args.agentId === 'string' ? args.agentId : undefined;
|
|
1884
|
+
// resolveOrAutoRegisterAgentIdentity's fall-through helper only reads
|
|
1885
|
+
// BRAINCLAW_AGENT / OPENCLAW_AGENT. resolveCurrentAgentIdentity also honors
|
|
1886
|
+
// BRAINCLAW_AGENT_NAME, and dispatched workers set both. Normalize here so
|
|
1887
|
+
// an env-declared name is a first-class signal to the auto-repair path.
|
|
1888
|
+
const envAgentName = explicitName
|
|
1889
|
+
?? (process.env.BRAINCLAW_AGENT_NAME?.trim() || undefined)
|
|
1890
|
+
?? (process.env.BRAINCLAW_AGENT?.trim() || undefined);
|
|
1891
|
+
let identity;
|
|
1892
|
+
let autoRegistered;
|
|
1893
|
+
try {
|
|
1894
|
+
const outcome = resolveOrAutoRegisterAgentIdentity({
|
|
1895
|
+
agentName: envAgentName,
|
|
1896
|
+
agentId: explicitId,
|
|
1897
|
+
cwd,
|
|
1898
|
+
allowCurrent: true,
|
|
1899
|
+
allowEnv: true,
|
|
1900
|
+
});
|
|
1901
|
+
identity = outcome.identity;
|
|
1902
|
+
autoRegistered = outcome.auto_registered;
|
|
1903
|
+
}
|
|
1904
|
+
catch (err) {
|
|
1905
|
+
// Genuine ambiguity — no derivable name. Stays a hard error (KEEP: doctrine
|
|
1906
|
+
// boundary is "ambiguous intent → refuse with next_action", not silence).
|
|
1907
|
+
const detail = err instanceof Error ? err.message : (strictError?.message ?? String(err));
|
|
1908
|
+
throw new Error(`cannot resolve mutation author: ${detail} `
|
|
1909
|
+
+ 'Pass a registered agent, set $BRAINCLAW_AGENT_NAME, '
|
|
1910
|
+
+ 'or register with `brainclaw register-agent <name>` before writing.', { cause: err });
|
|
1911
|
+
}
|
|
1912
|
+
const explicitSessionId = connectionSessionId?.trim() || explicitSessionIdFromEnv();
|
|
1913
|
+
const hadSessionBefore = explicitSessionId
|
|
1914
|
+
? Boolean(loadSessionById(explicitSessionId, cwd))
|
|
1915
|
+
: Boolean(loadCurrentSession(cwd));
|
|
1916
|
+
let sessionAutoCreated;
|
|
1917
|
+
try {
|
|
1918
|
+
const opIdentity = buildOperationalIdentity(identity.agent_name, cwd, {
|
|
1919
|
+
agentId: identity.agent_id,
|
|
1920
|
+
sessionId: explicitSessionId,
|
|
1921
|
+
persistImplicitSession: true,
|
|
1922
|
+
});
|
|
1923
|
+
if (!hadSessionBefore && opIdentity.session_id) {
|
|
1924
|
+
sessionAutoCreated = opIdentity.session_id;
|
|
1925
|
+
const session = loadSessionById(opIdentity.session_id, cwd);
|
|
1926
|
+
if (session && !session.auto_created) {
|
|
1927
|
+
saveCurrentSession({ ...session, auto_created: true }, cwd);
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
catch { /* best-effort — write can still proceed without a persisted session */ }
|
|
1932
|
+
const autoRepair = (autoRegistered || sessionAutoCreated)
|
|
1933
|
+
? {
|
|
1934
|
+
...(autoRegistered ? { agent_auto_registered: true } : {}),
|
|
1935
|
+
...(sessionAutoCreated ? { session_auto_created: sessionAutoCreated } : {}),
|
|
1936
|
+
}
|
|
1937
|
+
: undefined;
|
|
1938
|
+
return {
|
|
1939
|
+
agent_name: identity.agent_name,
|
|
1940
|
+
agent_id: identity.agent_id,
|
|
1941
|
+
...(autoRepair ? { auto_repair: autoRepair } : {}),
|
|
1942
|
+
};
|
|
1943
|
+
}
|
|
1944
|
+
function renderAutoRepairWarning(auto_repair, agent_name) {
|
|
1945
|
+
const parts = [];
|
|
1946
|
+
if (auto_repair.agent_auto_registered) {
|
|
1947
|
+
parts.push(`agent '${agent_name}' auto-registered (first use). Run \`brainclaw register-agent ${agent_name}\` to set capabilities and trust level.`);
|
|
1948
|
+
}
|
|
1949
|
+
if (auto_repair.session_auto_created) {
|
|
1950
|
+
parts.push(`session ${auto_repair.session_auto_created} auto-created for this write.`);
|
|
1951
|
+
}
|
|
1952
|
+
return `⚠️ auto-repair: ${parts.join(' ')}`;
|
|
1848
1953
|
}
|
|
1849
1954
|
function explicitSessionIdFromEnv() {
|
|
1850
1955
|
return process.env.BRAINCLAW_SESSION_ID?.trim()
|
|
@@ -3561,18 +3666,43 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
3561
3666
|
catch {
|
|
3562
3667
|
return { response: createToolErrorResponse('not_found', `Claim not found: ${claimId}`) };
|
|
3563
3668
|
}
|
|
3564
|
-
// pln#562 step 5 — release is ownership-checked like acquisition
|
|
3565
|
-
// adoption.
|
|
3566
|
-
//
|
|
3669
|
+
// pln#562 step 5 + trp#928 — release is ownership-checked like acquisition
|
|
3670
|
+
// and adoption. Under the trp#928 tightening the coordinator override is
|
|
3671
|
+
// OPT-IN via coordinator_override:true (implicit "trusted+ = always
|
|
3672
|
+
// override" was too magic — a coordinator releasing a worker's claim
|
|
3673
|
+
// should be a visible act, not a silent side-effect of trust). The
|
|
3674
|
+
// ownership check still enforces:
|
|
3675
|
+
// - owner-of-claim releases (identity matches): allowed, no override needed
|
|
3676
|
+
// - non-owner releases without coordinator_override: rejected loudly (the
|
|
3677
|
+
// error message points the caller at coordinator_override so it is
|
|
3678
|
+
// executable — pln#607 rule).
|
|
3679
|
+
// - non-owner releases with coordinator_override:true but not trusted+:
|
|
3680
|
+
// trust_error (privilege escalation prevention).
|
|
3681
|
+
// - non-owner releases with coordinator_override:true and trusted+:
|
|
3682
|
+
// allowed, audited (auditReleaseOverride).
|
|
3567
3683
|
const releaseIdentity = resolveMutationIdentity(args, { nameField: 'agent', idField: 'agentId' }, cwd, connectionSessionId);
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3684
|
+
if ('error' in releaseIdentity && releaseIdentity.error) {
|
|
3685
|
+
const { kind, message, details } = releaseIdentity.error;
|
|
3686
|
+
return { response: createToolErrorResponse(kind, message, details) };
|
|
3687
|
+
}
|
|
3688
|
+
if (!('identity' in releaseIdentity) || !releaseIdentity.identity) {
|
|
3689
|
+
return { response: createToolErrorResponse('identity_error', 'No registered agent identity resolved for bclaw_release_claim.') };
|
|
3690
|
+
}
|
|
3691
|
+
const coordinatorOverrideRequested = args.coordinator_override === true;
|
|
3692
|
+
if (coordinatorOverrideRequested) {
|
|
3693
|
+
const trustLevel = releaseIdentity.identity.trust_level ?? 'contributor';
|
|
3694
|
+
if (!hasMinimumTrustLevel(trustLevel, 'trusted')) {
|
|
3695
|
+
return {
|
|
3696
|
+
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.`),
|
|
3697
|
+
};
|
|
3574
3698
|
}
|
|
3575
|
-
|
|
3699
|
+
}
|
|
3700
|
+
const releaseAuth = {
|
|
3701
|
+
agent: releaseIdentity.identity.agent_name,
|
|
3702
|
+
agent_id: releaseIdentity.identity.agent_id,
|
|
3703
|
+
session_id: connectionSessionId,
|
|
3704
|
+
override: coordinatorOverrideRequested,
|
|
3705
|
+
};
|
|
3576
3706
|
let cascadeResult;
|
|
3577
3707
|
try {
|
|
3578
3708
|
cascadeResult = releaseClaimWithCascade(claimId, {
|
|
@@ -4067,6 +4197,39 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
4067
4197
|
actor: callerAgent,
|
|
4068
4198
|
actor_id: resolved.identity.agent_id,
|
|
4069
4199
|
}, cwd);
|
|
4200
|
+
// trp#928 — cascade-release the assignment's linked claim on completion.
|
|
4201
|
+
// Before this landing an obedient worker had to make TWO calls to close
|
|
4202
|
+
// the loop (bclaw_assignment_update status=completed AND
|
|
4203
|
+
// bclaw_release_claim); dispatch briefs enumerate both, but not every
|
|
4204
|
+
// sandboxed worker gets through both, and the coordinator's harvest path
|
|
4205
|
+
// only releases on --integrate — so contributor-driven completions left
|
|
4206
|
+
// claims active. The worker's own identity owns the claim (session
|
|
4207
|
+
// adoption), so ownership matches and no coordinator_override is needed.
|
|
4208
|
+
// Silent success/failure is unacceptable: log per-claim outcome.
|
|
4209
|
+
if (status === 'completed' && assignment.claim_id) {
|
|
4210
|
+
try {
|
|
4211
|
+
const { releaseClaimsCascade, logCascadeReleaseResult } = await import('../core/claims.js');
|
|
4212
|
+
const cascade = releaseClaimsCascade([assignment.claim_id], {
|
|
4213
|
+
cwd,
|
|
4214
|
+
planStatus: 'done',
|
|
4215
|
+
auth: {
|
|
4216
|
+
agent: callerAgent,
|
|
4217
|
+
agent_id: resolved.identity.agent_id,
|
|
4218
|
+
session_id: effectiveSessionId,
|
|
4219
|
+
override: false,
|
|
4220
|
+
},
|
|
4221
|
+
});
|
|
4222
|
+
logCascadeReleaseResult({
|
|
4223
|
+
actor: callerAgent,
|
|
4224
|
+
trigger: 'assignment_completed',
|
|
4225
|
+
assignment_id: assignmentId,
|
|
4226
|
+
claim_id: assignment.claim_id,
|
|
4227
|
+
cascade,
|
|
4228
|
+
cwd,
|
|
4229
|
+
});
|
|
4230
|
+
}
|
|
4231
|
+
catch { /* never block the update on cascade release */ }
|
|
4232
|
+
}
|
|
4070
4233
|
// When accepted: auto-acknowledge the inbox message (replaces bclaw_ack_message)
|
|
4071
4234
|
if (status === 'accepted' && assignment.message_id) {
|
|
4072
4235
|
try {
|
|
@@ -4936,6 +5099,45 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
4936
5099
|
}
|
|
4937
5100
|
}
|
|
4938
5101
|
}
|
|
5102
|
+
let stalePostAging;
|
|
5103
|
+
let staleAggregate;
|
|
5104
|
+
let hintsPostAging;
|
|
5105
|
+
let hintsAggregate;
|
|
5106
|
+
if (contextResult) {
|
|
5107
|
+
try {
|
|
5108
|
+
const policy = loadHygienePolicy(targetCwd);
|
|
5109
|
+
if (!policy.disabled) {
|
|
5110
|
+
// Filter candidates from the projection (no extra reads) — only
|
|
5111
|
+
// assignments whose surfaced last_heartbeat_at is old enough to
|
|
5112
|
+
// possibly cross a family TTL. Zero read overhead when the open
|
|
5113
|
+
// work is fresh (the common case).
|
|
5114
|
+
const openAssignments = contextResult.open_work?.active_assignments ?? [];
|
|
5115
|
+
// Codex PR#48 finding 3 (pln#578 guardrail): select candidate ids
|
|
5116
|
+
// from the already-surfaced projection — created/terminal rows are
|
|
5117
|
+
// dropped BEFORE any full loadAssignment, so a healthy store costs
|
|
5118
|
+
// zero extra file reads. Selection logic is unit-tested in
|
|
5119
|
+
// selectReadPathSweepCandidates.
|
|
5120
|
+
const candidateIds = selectReadPathSweepCandidates(openAssignments, policy, Date.now());
|
|
5121
|
+
if (candidateIds.length > 0) {
|
|
5122
|
+
const full = candidateIds
|
|
5123
|
+
.map((id) => loadAssignment(id, targetCwd))
|
|
5124
|
+
.filter((a) => a !== undefined);
|
|
5125
|
+
sweepAssignmentsAtReadPath(full, targetCwd, {
|
|
5126
|
+
actor: 'bclaw_work-readpath',
|
|
5127
|
+
policy,
|
|
5128
|
+
});
|
|
5129
|
+
}
|
|
5130
|
+
const registry = loadServeRegistry(targetCwd);
|
|
5131
|
+
const aged = ageStaleWarnings(contextResult.stale_warnings ?? [], targetCwd, { policy, registry });
|
|
5132
|
+
stalePostAging = aged.warnings;
|
|
5133
|
+
staleAggregate = aged.aggregate;
|
|
5134
|
+
const agedHints = ageWorkflowHints(contextResult.workflow_hints ?? [], targetCwd, { policy, registry });
|
|
5135
|
+
hintsPostAging = agedHints.hints;
|
|
5136
|
+
hintsAggregate = agedHints.aggregate;
|
|
5137
|
+
}
|
|
5138
|
+
}
|
|
5139
|
+
catch { /* non-fatal — hygiene must never break bclaw_work */ }
|
|
5140
|
+
}
|
|
4939
5141
|
// Build the full context result, then compact it if requested.
|
|
4940
5142
|
// Compact mode (default) strips the heavy ContextResult down to a
|
|
4941
5143
|
// minimal summary that fits within MCP token limits (~25k chars).
|
|
@@ -4951,7 +5153,8 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
4951
5153
|
status: item.extra ?? 'unknown',
|
|
4952
5154
|
plan_id: item.plan_id,
|
|
4953
5155
|
}));
|
|
4954
|
-
const
|
|
5156
|
+
const stalePool = stalePostAging ?? contextResult.stale_warnings ?? [];
|
|
5157
|
+
const staleTop3 = stalePool.slice(0, 3).map((w) => ({
|
|
4955
5158
|
id: w.id,
|
|
4956
5159
|
entity: w.entity,
|
|
4957
5160
|
text: w.text.slice(0, 80),
|
|
@@ -4975,6 +5178,7 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
4975
5178
|
: {}),
|
|
4976
5179
|
}
|
|
4977
5180
|
: undefined;
|
|
5181
|
+
const hintsPool = hintsPostAging ?? contextResult.workflow_hints ?? [];
|
|
4978
5182
|
resultPayload = {
|
|
4979
5183
|
context_schema: contextResult.context_schema,
|
|
4980
5184
|
profile: contextResult.profile,
|
|
@@ -4983,7 +5187,9 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
4983
5187
|
context_diff: trimmedDiff ?? null,
|
|
4984
5188
|
plan_summary: planItems,
|
|
4985
5189
|
stale_warnings: staleTop3,
|
|
4986
|
-
|
|
5190
|
+
...(staleAggregate ? { stale_warnings_aggregate: staleAggregate } : {}),
|
|
5191
|
+
workflow_hints: hintsPool.slice(0, 3),
|
|
5192
|
+
...(hintsAggregate ? { workflow_hints_aggregate: hintsAggregate } : {}),
|
|
4987
5193
|
claim_conflicts: contextResult.claim_conflicts ?? [],
|
|
4988
5194
|
open_work: contextResult.open_work ?? null,
|
|
4989
5195
|
_compact: true,
|
|
@@ -6515,17 +6721,38 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
6515
6721
|
// only checks known keys), letting the caller believe the filter had
|
|
6516
6722
|
// applied when it hadn't. Under the new contract, an unknown key is
|
|
6517
6723
|
// a validation_error listing the keys actually honored.
|
|
6724
|
+
// trp#928 — the entity-scoping error is now first-class: the doc says
|
|
6725
|
+
// assignment_id/claim_id/message_id are entity='agent_run' only, but
|
|
6726
|
+
// before this the rejection message called them 'unknown', misleading
|
|
6727
|
+
// callers who'd cross-reference the description. Now the message names
|
|
6728
|
+
// the constraint AND the entity that DOES accept the key so the user
|
|
6729
|
+
// can fix the call without hunting through docs. (pln#599 docs-vs-facts.)
|
|
6518
6730
|
const KNOWN_FILTER_KEYS = new Set([
|
|
6519
6731
|
'status', 'tag', 'tags', 'author', 'plan_id', 'source', 'auto_generated',
|
|
6520
6732
|
'assignment_id', 'claim_id', 'message_id',
|
|
6521
6733
|
'limit', 'offset', 'includeLegacy', 'minAutoReflectConfidence',
|
|
6522
6734
|
]);
|
|
6523
6735
|
const agentRunOnlyFilterKeys = new Set(['assignment_id', 'claim_id', 'message_id']);
|
|
6524
|
-
const
|
|
6525
|
-
|
|
6736
|
+
const providedKeys = Object.keys(filter);
|
|
6737
|
+
const unknownKeys = providedKeys.filter((k) => !KNOWN_FILTER_KEYS.has(k));
|
|
6738
|
+
const misScopedKeys = providedKeys.filter((k) => agentRunOnlyFilterKeys.has(k) && entity !== 'agent_run');
|
|
6739
|
+
if (unknownKeys.length > 0 || misScopedKeys.length > 0) {
|
|
6740
|
+
const parts = [];
|
|
6741
|
+
if (unknownKeys.length > 0) {
|
|
6742
|
+
parts.push(`Unknown filter key(s): ${unknownKeys.map((k) => `"${k}"`).join(', ')}. Accepted keys: ${[...KNOWN_FILTER_KEYS].sort().join(', ')}.`);
|
|
6743
|
+
}
|
|
6744
|
+
if (misScopedKeys.length > 0) {
|
|
6745
|
+
parts.push(`Filter key(s) ${misScopedKeys.map((k) => `"${k}"`).join(', ')} are only valid for entity="agent_run" `
|
|
6746
|
+
+ `(this call used entity="${entity}"). `
|
|
6747
|
+
+ `Retry with entity="agent_run", or drop the ${misScopedKeys.join('/')} filter.`);
|
|
6748
|
+
}
|
|
6526
6749
|
return {
|
|
6527
|
-
response: createToolErrorResponse('validation_error',
|
|
6528
|
-
|
|
6750
|
+
response: createToolErrorResponse('validation_error', parts.join(' '), {
|
|
6751
|
+
unknown_keys: unknownKeys,
|
|
6752
|
+
mis_scoped_keys: misScopedKeys,
|
|
6753
|
+
accepted_keys: [...KNOWN_FILTER_KEYS].sort(),
|
|
6754
|
+
agent_run_only_keys: [...agentRunOnlyFilterKeys],
|
|
6755
|
+
}),
|
|
6529
6756
|
};
|
|
6530
6757
|
}
|
|
6531
6758
|
const result = listEntities(entity, targetCwd, filter);
|
|
@@ -6664,29 +6891,36 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
6664
6891
|
const data = { ...rawData };
|
|
6665
6892
|
let actor = typeof data.author === 'string' ? data.author : undefined;
|
|
6666
6893
|
let actorId = typeof data.agent_id === 'string' ? data.agent_id : undefined;
|
|
6894
|
+
let autoRepair;
|
|
6667
6895
|
if (data.author === undefined) {
|
|
6668
|
-
const
|
|
6669
|
-
data.author = agent_name;
|
|
6896
|
+
const author = resolveCanonicalAuthor(args, cwd, connectionSessionId);
|
|
6897
|
+
data.author = author.agent_name;
|
|
6670
6898
|
if (data.agent === undefined)
|
|
6671
|
-
data.agent = agent_name;
|
|
6672
|
-
if (data.agent_id === undefined && agent_id)
|
|
6673
|
-
data.agent_id = agent_id;
|
|
6674
|
-
actor = agent_name;
|
|
6675
|
-
actorId = agent_id;
|
|
6899
|
+
data.agent = author.agent_name;
|
|
6900
|
+
if (data.agent_id === undefined && author.agent_id)
|
|
6901
|
+
data.agent_id = author.agent_id;
|
|
6902
|
+
actor = author.agent_name;
|
|
6903
|
+
actorId = author.agent_id;
|
|
6904
|
+
autoRepair = author.auto_repair;
|
|
6676
6905
|
}
|
|
6677
6906
|
else if (data.agent === undefined) {
|
|
6678
6907
|
data.agent = data.author;
|
|
6679
6908
|
}
|
|
6680
6909
|
const result = createEntity(entity, data, targetCwd);
|
|
6681
6910
|
appendAuditEntry({ actor: actor ?? 'unknown', ...(actorId ? { actor_id: actorId } : {}), action: 'create', item_id: result.id, item_type: entity }, targetCwd);
|
|
6911
|
+
const createText = `✔ created ${entity} ${result.id}${autoSwitched ? ` (auto-switched → ${targetScope.resolved_project.name ?? targetScope.resolved_project.path})` : ''}`;
|
|
6912
|
+
const createContent = autoRepair
|
|
6913
|
+
? [{ type: 'text', text: createText }, { type: 'text', text: renderAutoRepairWarning(autoRepair, actor ?? 'unknown') }]
|
|
6914
|
+
: [{ type: 'text', text: createText }];
|
|
6682
6915
|
return {
|
|
6683
6916
|
response: toolResponse({
|
|
6684
|
-
content:
|
|
6917
|
+
content: createContent,
|
|
6685
6918
|
structuredContent: {
|
|
6686
6919
|
...result,
|
|
6687
6920
|
resolved_project: targetScope.resolved_project,
|
|
6688
6921
|
active_source: autoSwitched ? 'auto_switch' : targetScope.active_source,
|
|
6689
6922
|
...(autoSwitched ? { auto_switched: true } : {}),
|
|
6923
|
+
...(autoRepair ? { auto_repair: autoRepair } : {}),
|
|
6690
6924
|
},
|
|
6691
6925
|
}),
|
|
6692
6926
|
};
|
|
@@ -6702,16 +6936,21 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
6702
6936
|
const patch = (args.patch ?? {});
|
|
6703
6937
|
const targetCwd = resolveProjectCwd(args.project, cwd);
|
|
6704
6938
|
const targetScope = scopeMetadataForTarget(args, targetCwd, scopeInfo);
|
|
6705
|
-
const { agent_name, agent_id } = resolveCanonicalAuthor(args, cwd, connectionSessionId);
|
|
6939
|
+
const { agent_name, agent_id, auto_repair } = resolveCanonicalAuthor(args, cwd, connectionSessionId);
|
|
6706
6940
|
const result = updateEntity(entity, id, patch, targetCwd);
|
|
6707
6941
|
appendAuditEntry({ actor: agent_name, ...(agent_id ? { actor_id: agent_id } : {}), action: 'update', item_id: id, item_type: entity }, targetCwd);
|
|
6942
|
+
const updateText = `✔ updated ${entity} ${id}`;
|
|
6943
|
+
const updateContent = auto_repair
|
|
6944
|
+
? [{ type: 'text', text: updateText }, { type: 'text', text: renderAutoRepairWarning(auto_repair, agent_name) }]
|
|
6945
|
+
: [{ type: 'text', text: updateText }];
|
|
6708
6946
|
return {
|
|
6709
6947
|
response: toolResponse({
|
|
6710
|
-
content:
|
|
6948
|
+
content: updateContent,
|
|
6711
6949
|
structuredContent: {
|
|
6712
6950
|
...result,
|
|
6713
6951
|
resolved_project: targetScope.resolved_project,
|
|
6714
6952
|
active_source: targetScope.active_source,
|
|
6953
|
+
...(auto_repair ? { auto_repair } : {}),
|
|
6715
6954
|
},
|
|
6716
6955
|
}),
|
|
6717
6956
|
};
|
|
@@ -6727,16 +6966,21 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
6727
6966
|
const purge = args.purge === true;
|
|
6728
6967
|
const targetCwd = resolveProjectCwd(args.project, cwd);
|
|
6729
6968
|
const targetScope = scopeMetadataForTarget(args, targetCwd, scopeInfo);
|
|
6730
|
-
const { agent_name, agent_id } = resolveCanonicalAuthor(args, cwd, connectionSessionId);
|
|
6969
|
+
const { agent_name, agent_id, auto_repair } = resolveCanonicalAuthor(args, cwd, connectionSessionId);
|
|
6731
6970
|
const result = removeEntity(entity, id, targetCwd, purge);
|
|
6732
6971
|
appendAuditEntry({ actor: agent_name, ...(agent_id ? { actor_id: agent_id } : {}), action: 'delete', item_id: id, item_type: entity, reason: purge ? 'purged' : 'archived' }, targetCwd);
|
|
6972
|
+
const removeText = `✔ removed ${entity} ${id}`;
|
|
6973
|
+
const removeContent = auto_repair
|
|
6974
|
+
? [{ type: 'text', text: removeText }, { type: 'text', text: renderAutoRepairWarning(auto_repair, agent_name) }]
|
|
6975
|
+
: [{ type: 'text', text: removeText }];
|
|
6733
6976
|
return {
|
|
6734
6977
|
response: toolResponse({
|
|
6735
|
-
content:
|
|
6978
|
+
content: removeContent,
|
|
6736
6979
|
structuredContent: {
|
|
6737
6980
|
...result,
|
|
6738
6981
|
resolved_project: targetScope.resolved_project,
|
|
6739
6982
|
active_source: targetScope.active_source,
|
|
6983
|
+
...(auto_repair ? { auto_repair } : {}),
|
|
6740
6984
|
},
|
|
6741
6985
|
}),
|
|
6742
6986
|
};
|
|
@@ -6752,13 +6996,20 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
6752
6996
|
const toProject = String(args.to_project ?? '');
|
|
6753
6997
|
const fromProject = typeof args.from_project === 'string' ? args.from_project : undefined;
|
|
6754
6998
|
const force = args.force === true;
|
|
6755
|
-
const { agent_name, agent_id } = resolveCanonicalAuthor(args, cwd, connectionSessionId);
|
|
6999
|
+
const { agent_name, agent_id, auto_repair } = resolveCanonicalAuthor(args, cwd, connectionSessionId);
|
|
6756
7000
|
const result = relocateEntity({ entity, id, toProject, fromProject, force, cwd, actor: agent_name, actorId: agent_id });
|
|
6757
7001
|
const warn = result.warnings.length ? ` (${result.warnings.length} warning(s))` : '';
|
|
7002
|
+
const moveText = `✔ moved ${entity} ${id} → ${result.to}${warn}`;
|
|
7003
|
+
const moveContent = auto_repair
|
|
7004
|
+
? [{ type: 'text', text: moveText }, { type: 'text', text: renderAutoRepairWarning(auto_repair, agent_name) }]
|
|
7005
|
+
: [{ type: 'text', text: moveText }];
|
|
6758
7006
|
return {
|
|
6759
7007
|
response: toolResponse({
|
|
6760
|
-
content:
|
|
6761
|
-
structuredContent: {
|
|
7008
|
+
content: moveContent,
|
|
7009
|
+
structuredContent: {
|
|
7010
|
+
...result,
|
|
7011
|
+
...(auto_repair ? { auto_repair } : {}),
|
|
7012
|
+
},
|
|
6762
7013
|
}),
|
|
6763
7014
|
};
|
|
6764
7015
|
}
|
|
@@ -6788,17 +7039,48 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
6788
7039
|
const to = String(args.to ?? '');
|
|
6789
7040
|
const reason = args.reason;
|
|
6790
7041
|
const targetScope = scopeMetadataForTarget(args, targetCwd, scopeInfo);
|
|
6791
|
-
const { agent_name, agent_id } = resolveCanonicalAuthor(args, cwd, connectionSessionId);
|
|
6792
|
-
|
|
7042
|
+
const { agent_name, agent_id, auto_repair } = resolveCanonicalAuthor(args, cwd, connectionSessionId);
|
|
7043
|
+
// trp#928 — claim transitions consume the ReleaseClaimAuth ownership
|
|
7044
|
+
// check (released/stale both mutate a claim owned by SOME agent). Reuse
|
|
7045
|
+
// the same coordinator_override opt-in as bclaw_release_claim so both
|
|
7046
|
+
// paths have identical trust semantics and the same executable error.
|
|
7047
|
+
let transitionAuth;
|
|
7048
|
+
if (entity === 'claim') {
|
|
7049
|
+
const transitionIdentity = resolveMutationIdentity(args, { nameField: 'agent', idField: 'agentId' }, targetCwd, connectionSessionId);
|
|
7050
|
+
const coordinatorOverrideRequested = args.coordinator_override === true;
|
|
7051
|
+
if (coordinatorOverrideRequested) {
|
|
7052
|
+
const identity = 'identity' in transitionIdentity ? transitionIdentity.identity : undefined;
|
|
7053
|
+
const trustLevel = identity?.trust_level ?? 'contributor';
|
|
7054
|
+
if (!hasMinimumTrustLevel(trustLevel, 'trusted')) {
|
|
7055
|
+
return {
|
|
7056
|
+
response: createToolErrorResponse('trust_error', `coordinator_override:true requires trust_level 'trusted' or higher — caller is '${trustLevel}'.`),
|
|
7057
|
+
};
|
|
7058
|
+
}
|
|
7059
|
+
}
|
|
7060
|
+
transitionAuth = 'identity' in transitionIdentity && transitionIdentity.identity
|
|
7061
|
+
? {
|
|
7062
|
+
agent: transitionIdentity.identity.agent_name,
|
|
7063
|
+
agent_id: transitionIdentity.identity.agent_id,
|
|
7064
|
+
session_id: connectionSessionId,
|
|
7065
|
+
override: coordinatorOverrideRequested,
|
|
7066
|
+
}
|
|
7067
|
+
: undefined;
|
|
7068
|
+
}
|
|
7069
|
+
const result = transitionEntity(entity, id, to, targetCwd, reason, transitionAuth);
|
|
6793
7070
|
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);
|
|
7071
|
+
const transitionText = `✔ ${entity} ${id}: ${result.from} → ${to}${autoSwitched ? ` (auto-switched → ${targetScope.resolved_project.name ?? targetScope.resolved_project.path})` : ''}`;
|
|
7072
|
+
const transitionContent = auto_repair
|
|
7073
|
+
? [{ type: 'text', text: transitionText }, { type: 'text', text: renderAutoRepairWarning(auto_repair, agent_name) }]
|
|
7074
|
+
: [{ type: 'text', text: transitionText }];
|
|
6794
7075
|
return {
|
|
6795
7076
|
response: toolResponse({
|
|
6796
|
-
content:
|
|
7077
|
+
content: transitionContent,
|
|
6797
7078
|
structuredContent: {
|
|
6798
7079
|
...result,
|
|
6799
7080
|
resolved_project: targetScope.resolved_project,
|
|
6800
7081
|
active_source: autoSwitched ? 'auto_switch' : targetScope.active_source,
|
|
6801
7082
|
...(autoSwitched ? { auto_switched: true } : {}),
|
|
7083
|
+
...(auto_repair ? { auto_repair } : {}),
|
|
6802
7084
|
},
|
|
6803
7085
|
}),
|
|
6804
7086
|
};
|
|
@@ -3,16 +3,36 @@ import { mutate } from '../core/mutation-pipeline.js';
|
|
|
3
3
|
import { loadClaim, listClaims, releaseClaim } from '../core/claims.js';
|
|
4
4
|
import { rebuildProjectMd } from '../core/markdown.js';
|
|
5
5
|
import { loadState, mutateState } from '../core/state.js';
|
|
6
|
+
import { requireMinimumTrustLevel, requireRegisteredAgentIdentity } from '../core/agent-registry.js';
|
|
6
7
|
export function runReleaseClaim(id, options = {}) {
|
|
7
8
|
if (!memoryExists(options.cwd)) {
|
|
8
9
|
console.error('Error: .brainclaw/ not found. Run `brainclaw init` first.');
|
|
9
10
|
process.exit(1);
|
|
10
11
|
}
|
|
11
12
|
try {
|
|
13
|
+
// Surface split (trp#928 follow-up): the ownership gate lives on the MCP
|
|
14
|
+
// surface (bclaw_release_claim / bclaw_transition), where agent callers
|
|
15
|
+
// carry a session-bound identity. The CLI `release-claim <id>` is the
|
|
16
|
+
// operator/scripting surface and keeps its historic unguarded semantics —
|
|
17
|
+
// the e2e contract (collaboration.test.ts) has always released cross-agent
|
|
18
|
+
// claims from an env-identified CLI. Deriving an ambient identity here and
|
|
19
|
+
// gating on it turned every operator release into a false ownership
|
|
20
|
+
// mismatch (silent-default anti-pattern, pln#607). `--coordinator-override`
|
|
21
|
+
// stays available to make a cross-agent release explicit and audited.
|
|
22
|
+
let releaseAuth;
|
|
23
|
+
if (options.coordinatorOverride) {
|
|
24
|
+
const identity = requireRegisteredAgentIdentity({ cwd: options.cwd, allowCurrent: true, allowEnv: true });
|
|
25
|
+
requireMinimumTrustLevel(identity, 'trusted');
|
|
26
|
+
releaseAuth = {
|
|
27
|
+
agent: identity.agent_name,
|
|
28
|
+
agent_id: identity.agent_id,
|
|
29
|
+
override: true,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
12
32
|
let claim = loadClaim(id, options.cwd);
|
|
13
33
|
mutate({ cwd: options.cwd }, () => {
|
|
14
34
|
const existing = loadClaim(id, options.cwd);
|
|
15
|
-
claim = releaseClaim(id, options.cwd);
|
|
35
|
+
claim = releaseClaim(id, options.cwd, releaseAuth);
|
|
16
36
|
if (existing.plan_id) {
|
|
17
37
|
const updated = mutateState((state) => {
|
|
18
38
|
const plan = state.plan_items.find((item) => item.id === existing.plan_id);
|