brainclaw 1.5.4 → 1.5.5
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 +52 -28
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/cli.js +35 -5
- package/dist/commands/assignment-resource.js +182 -0
- package/dist/commands/init.js +158 -22
- package/dist/commands/mcp-read-handlers.js +8 -4
- package/dist/commands/mcp.js +7 -3
- package/dist/commands/setup.js +87 -48
- package/dist/commands/switch.js +4 -0
- package/dist/core/agentruns.js +10 -0
- package/dist/core/assignments.js +29 -10
- package/dist/core/context.js +1 -1
- package/dist/core/coordination.js +1 -1
- package/dist/core/entity-operations.js +38 -1
- package/dist/core/entity-registry.js +11 -10
- package/dist/core/schema.js +3 -0
- package/dist/core/store-resolution.js +26 -16
- package/dist/facts.js +3 -3
- package/dist/facts.json +2 -2
- package/docs/cli.md +115 -30
- package/docs/integrations/agents.md +14 -14
- package/docs/integrations/codex.md +15 -12
- package/docs/integrations/mcp.md +10 -4
- package/docs/integrations/overview.md +12 -3
- package/docs/playbooks/productivity/index.md +3 -3
- package/docs/quickstart-existing-project.md +48 -28
- package/docs/quickstart.md +42 -28
- package/package.json +1 -1
package/dist/commands/mcp.js
CHANGED
|
@@ -4887,7 +4887,7 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
4887
4887
|
// dispatch analysis / review.
|
|
4888
4888
|
const { listAssignments: listAsgn } = await import('../core/assignments.js');
|
|
4889
4889
|
const predecessors = listAsgn(dispatchCwd, { claim_id: oldClaim.id })
|
|
4890
|
-
.filter((a) => a.status !== 'completed' && a.status !== 'expired' && a.status !== 'rerouted');
|
|
4890
|
+
.filter((a) => a.status !== 'completed' && a.status !== 'cancelled' && a.status !== 'expired' && a.status !== 'rerouted');
|
|
4891
4891
|
for (const predecessor of predecessors) {
|
|
4892
4892
|
try {
|
|
4893
4893
|
transitionAssignment(predecessor.id, 'rerouted', {
|
|
@@ -5495,7 +5495,10 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
5495
5495
|
* break tool execution.
|
|
5496
5496
|
*/
|
|
5497
5497
|
export async function executeMcpToolCall(payload) {
|
|
5498
|
-
const
|
|
5498
|
+
const baseCwd = payload.cwd;
|
|
5499
|
+
const cwd = payload.name === 'bclaw_switch'
|
|
5500
|
+
? baseCwd
|
|
5501
|
+
: resolveEffectiveCwd({ baseCwd });
|
|
5499
5502
|
const envClaimId = process.env.BRAINCLAW_CLAIM_ID?.trim() || undefined;
|
|
5500
5503
|
// ── Auto-session ────────────────────────────────────────────────────────────
|
|
5501
5504
|
let autoSessionId;
|
|
@@ -5522,7 +5525,7 @@ export async function executeMcpToolCall(payload) {
|
|
|
5522
5525
|
// no longer show session_id="unknown".
|
|
5523
5526
|
try {
|
|
5524
5527
|
const { listAssignments: listA, saveAssignment: saveA } = await import('../core/assignments.js');
|
|
5525
|
-
const active = listA(cwd, { claim_id: envClaimId }).filter((a) => !['completed', 'expired', 'rerouted'].includes(a.status));
|
|
5528
|
+
const active = listA(cwd, { claim_id: envClaimId }).filter((a) => !['completed', 'cancelled', 'expired', 'rerouted'].includes(a.status));
|
|
5526
5529
|
for (const a of active) {
|
|
5527
5530
|
if (!a.session_id) {
|
|
5528
5531
|
a.session_id = autoSessionId;
|
|
@@ -5545,6 +5548,7 @@ export async function executeMcpToolCall(payload) {
|
|
|
5545
5548
|
// ── Delegate to inner handler ───────────────────────────────────────────────
|
|
5546
5549
|
const outcome = await _executeMcpToolCallInner({
|
|
5547
5550
|
...payload,
|
|
5551
|
+
cwd,
|
|
5548
5552
|
connectionSessionId: effectiveConnectionSessionId,
|
|
5549
5553
|
});
|
|
5550
5554
|
// Apply legacy deprecation warning uniformly (Phase 3 slice 3g). Read tools
|
package/dist/commands/setup.js
CHANGED
|
@@ -305,6 +305,91 @@ export function printReloadReminder(detectedAgent) {
|
|
|
305
305
|
console.log(' → Restart your AI coding agent to pick up the new MCP configuration.');
|
|
306
306
|
}
|
|
307
307
|
}
|
|
308
|
+
function logDetectedAgentSurfaces(detectedName, detectedSurfaces) {
|
|
309
|
+
console.log('');
|
|
310
|
+
if (detectedName) {
|
|
311
|
+
console.log(`Detected AI agent: ${detectedName}`);
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
console.log('No AI agent detected automatically.');
|
|
315
|
+
}
|
|
316
|
+
const visibleSurfaces = detectedSurfaces.filter((surface) => surface.status !== 'not_detected');
|
|
317
|
+
if (visibleSurfaces.length > 0) {
|
|
318
|
+
console.log('Other AI work surfaces on this machine:');
|
|
319
|
+
for (const surface of visibleSurfaces) {
|
|
320
|
+
console.log(` - ${surface.display_name} [${surface.surface_kind}, ${surface.status}]`);
|
|
321
|
+
}
|
|
322
|
+
const usageHints = renderAiSurfaceUsageHints(visibleSurfaces);
|
|
323
|
+
if (usageHints.length > 0) {
|
|
324
|
+
console.log('');
|
|
325
|
+
console.log('Suggested uses:');
|
|
326
|
+
for (const line of usageHints) {
|
|
327
|
+
console.log(` ${line}`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
console.log('');
|
|
331
|
+
console.log('These surfaces are tracked separately from coding agents and will use tailored onboarding flows.');
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
async function resolveSelectedAgentsForSetup(options, detectedName) {
|
|
335
|
+
console.log('Supported agents:');
|
|
336
|
+
ALL_KNOWN_AGENTS.forEach((a, i) => {
|
|
337
|
+
const tag = a === detectedName ? ' ← detected' : '';
|
|
338
|
+
console.log(` ${i + 1}) ${a}${tag}`);
|
|
339
|
+
});
|
|
340
|
+
let agentChoice;
|
|
341
|
+
if (options.agents) {
|
|
342
|
+
agentChoice = options.agents;
|
|
343
|
+
}
|
|
344
|
+
else if (options.yes || !process.stdin.isTTY) {
|
|
345
|
+
agentChoice = detectedName ? 'detected' : 'all';
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
const defaultChoice = detectedName ? 'detected' : 'all';
|
|
349
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
350
|
+
try {
|
|
351
|
+
agentChoice = (await rl.question(`Configure agents: (d)etected, (a)ll, or numbers e.g. 1,3 [${defaultChoice}]: `)).trim() || defaultChoice;
|
|
352
|
+
}
|
|
353
|
+
finally {
|
|
354
|
+
rl.close();
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
const selectedAgents = parseAgentSelection(agentChoice, detectedName);
|
|
358
|
+
console.log(`Selected agents: ${selectedAgents.length === 0 ? '(none)' : selectedAgents.join(', ')}`);
|
|
359
|
+
return selectedAgents;
|
|
360
|
+
}
|
|
361
|
+
export async function runSetupMachine(options = {}) {
|
|
362
|
+
const env = process.env;
|
|
363
|
+
const detectedAi = detectAiAgent(env);
|
|
364
|
+
const detectedName = detectedAi?.name;
|
|
365
|
+
const testMode = process.env.BRAINCLAW_TEST_MODE === '1';
|
|
366
|
+
const detectedSurfaces = testMode ? [] : buildAiSurfaceInventory();
|
|
367
|
+
console.log(BRAINCLAW_ASCII);
|
|
368
|
+
console.log('Machine bootstrap only — no repositories will be scanned or initialized.');
|
|
369
|
+
logDetectedAgentSurfaces(detectedName, detectedSurfaces);
|
|
370
|
+
const selectedAgents = await resolveSelectedAgentsForSetup(options, detectedName);
|
|
371
|
+
console.log('\n→ Installing machine-level brainclaw prerequisites...');
|
|
372
|
+
const written = runGlobalInstall(selectedAgents, env);
|
|
373
|
+
if (written.length > 0) {
|
|
374
|
+
for (const filePath of written) {
|
|
375
|
+
console.log(` ✔ ${filePath}`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
console.log(' (all machine-level prerequisites already up to date)');
|
|
380
|
+
}
|
|
381
|
+
installVscodeExtension();
|
|
382
|
+
writeSetupState({
|
|
383
|
+
completed_at: new Date().toISOString(),
|
|
384
|
+
roots: [],
|
|
385
|
+
initialised_repos: [],
|
|
386
|
+
global_configs_written: selectedAgents,
|
|
387
|
+
}, env);
|
|
388
|
+
printReloadReminder(detectedName);
|
|
389
|
+
console.log('');
|
|
390
|
+
console.log('Next step: run `brainclaw init` inside the project you want to create or refresh.');
|
|
391
|
+
console.log('If the project already has .brainclaw/ and you want to register another agent explicitly, run `brainclaw enable-agent <agent-name>` there.');
|
|
392
|
+
}
|
|
308
393
|
// ─── Main CLI wizard ──────────────────────────────────────────────────────────
|
|
309
394
|
export async function runSetup(options = {}) {
|
|
310
395
|
const env = process.env;
|
|
@@ -399,54 +484,8 @@ export async function runSetup(options = {}) {
|
|
|
399
484
|
const detectedName = detectedAi?.name;
|
|
400
485
|
const testMode = process.env.BRAINCLAW_TEST_MODE === '1';
|
|
401
486
|
const detectedSurfaces = testMode ? [] : buildAiSurfaceInventory();
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
console.log(`Detected AI agent: ${detectedName}`);
|
|
405
|
-
}
|
|
406
|
-
else {
|
|
407
|
-
console.log('No AI agent detected automatically.');
|
|
408
|
-
}
|
|
409
|
-
const visibleSurfaces = detectedSurfaces.filter((surface) => surface.status !== 'not_detected');
|
|
410
|
-
if (visibleSurfaces.length > 0) {
|
|
411
|
-
console.log('Other AI work surfaces on this machine:');
|
|
412
|
-
for (const surface of visibleSurfaces) {
|
|
413
|
-
console.log(` - ${surface.display_name} [${surface.surface_kind}, ${surface.status}]`);
|
|
414
|
-
}
|
|
415
|
-
const usageHints = renderAiSurfaceUsageHints(visibleSurfaces);
|
|
416
|
-
if (usageHints.length > 0) {
|
|
417
|
-
console.log('');
|
|
418
|
-
console.log('Suggested uses:');
|
|
419
|
-
for (const line of usageHints) {
|
|
420
|
-
console.log(` ${line}`);
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
console.log('');
|
|
424
|
-
console.log('These surfaces are tracked separately from coding agents and will use tailored onboarding flows.');
|
|
425
|
-
}
|
|
426
|
-
console.log('Supported agents:');
|
|
427
|
-
ALL_KNOWN_AGENTS.forEach((a, i) => {
|
|
428
|
-
const tag = a === detectedName ? ' ← detected' : '';
|
|
429
|
-
console.log(` ${i + 1}) ${a}${tag}`);
|
|
430
|
-
});
|
|
431
|
-
let agentChoice;
|
|
432
|
-
if (options.agents) {
|
|
433
|
-
agentChoice = options.agents;
|
|
434
|
-
}
|
|
435
|
-
else if (options.yes || !process.stdin.isTTY) {
|
|
436
|
-
agentChoice = detectedName ? 'detected' : 'all';
|
|
437
|
-
}
|
|
438
|
-
else {
|
|
439
|
-
const defaultChoice = detectedName ? 'detected' : 'all';
|
|
440
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
441
|
-
try {
|
|
442
|
-
agentChoice = (await rl.question(`Configure agents: (d)etected, (a)ll, or numbers e.g. 1,3 [${defaultChoice}]: `)).trim() || defaultChoice;
|
|
443
|
-
}
|
|
444
|
-
finally {
|
|
445
|
-
rl.close();
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
const selectedAgents = parseAgentSelection(agentChoice, detectedName);
|
|
449
|
-
console.log(`Selected agents: ${selectedAgents.length === 0 ? '(none)' : selectedAgents.join(', ')}`);
|
|
487
|
+
logDetectedAgentSurfaces(detectedName, detectedSurfaces);
|
|
488
|
+
const selectedAgents = await resolveSelectedAgentsForSetup(options, detectedName);
|
|
450
489
|
// Step 5: Global install
|
|
451
490
|
console.log('\n→ Installing global brainclaw prerequisites...');
|
|
452
491
|
const written = runGlobalInstall(selectedAgents, env);
|
package/dist/commands/switch.js
CHANGED
|
@@ -244,6 +244,10 @@ function listProjects(wsRoot, json) {
|
|
|
244
244
|
* this returns the farthest one — the true multi-project workspace root.
|
|
245
245
|
*/
|
|
246
246
|
function findOutermostWorkspaceRoot(startDir) {
|
|
247
|
+
const envWorkspace = process.env.BRAINCLAW_CWD?.trim();
|
|
248
|
+
if (envWorkspace && memoryExists(path.resolve(envWorkspace))) {
|
|
249
|
+
return path.resolve(envWorkspace);
|
|
250
|
+
}
|
|
247
251
|
let dir = path.resolve(startDir);
|
|
248
252
|
const root = path.parse(dir).root;
|
|
249
253
|
const home = process.env.HOME || process.env.USERPROFILE || root;
|
package/dist/core/agentruns.js
CHANGED
|
@@ -320,6 +320,16 @@ export function syncAgentRunFromAssignmentTransition(assignment, newStatus, opti
|
|
|
320
320
|
artifacts: options.artifacts,
|
|
321
321
|
}, cwd);
|
|
322
322
|
return;
|
|
323
|
+
case 'cancelled':
|
|
324
|
+
if (!canTransitionRun(run, 'cancelled'))
|
|
325
|
+
return;
|
|
326
|
+
transitionAgentRun(run.id, 'cancelled', {
|
|
327
|
+
actor: options.actor,
|
|
328
|
+
actor_id: options.actor_id,
|
|
329
|
+
session_id: options.session_id,
|
|
330
|
+
status_reason: options.status_reason ?? 'Cancelled via assignment lifecycle',
|
|
331
|
+
}, cwd);
|
|
332
|
+
return;
|
|
323
333
|
case 'failed':
|
|
324
334
|
if (!canTransitionRun(run, 'failed'))
|
|
325
335
|
return;
|
package/dist/core/assignments.js
CHANGED
|
@@ -64,6 +64,22 @@ export function listAssignments(cwd, filter) {
|
|
|
64
64
|
items = items.filter((a) => a.sequence_id === filter.sequence_id);
|
|
65
65
|
return items;
|
|
66
66
|
}
|
|
67
|
+
export function deleteAssignment(id, cwd) {
|
|
68
|
+
const store = assignmentStore(cwd);
|
|
69
|
+
if (!store.exists(id)) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
mutate({ cwd }, () => {
|
|
73
|
+
const writableStore = new JsonStore({
|
|
74
|
+
dirPath: assignmentsDir(cwd, 'write'),
|
|
75
|
+
documentType: 'assignment',
|
|
76
|
+
getId: (a) => a.id,
|
|
77
|
+
sort: (a, b) => a.created_at.localeCompare(b.created_at),
|
|
78
|
+
});
|
|
79
|
+
writableStore.delete(id);
|
|
80
|
+
});
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
67
83
|
// ── ID Generation ────────────────────────────────────────────
|
|
68
84
|
export function generateAssignmentId(cwd) {
|
|
69
85
|
return generateIdWithLabel('assignments', cwd);
|
|
@@ -78,15 +94,15 @@ export function generateAssignmentId(cwd) {
|
|
|
78
94
|
* rerouted a still-unstarted lane.
|
|
79
95
|
*/
|
|
80
96
|
const VALID_TRANSITIONS = new Map([
|
|
81
|
-
['created', new Set(['offered', 'rerouted'])],
|
|
82
|
-
['offered', new Set(['accepted', 'failed', 'expired', 'rerouted'])],
|
|
83
|
-
['accepted', new Set(['started', 'timed_out', 'rerouted'])],
|
|
84
|
-
['started', new Set(['completed', 'failed', 'blocked', 'timed_out', 'rerouted'])],
|
|
85
|
-
['failed', new Set(['retrying', 'rerouted'])],
|
|
86
|
-
['timed_out', new Set(['retrying', 'rerouted'])],
|
|
87
|
-
['retrying', new Set(['offered', 'rerouted'])],
|
|
88
|
-
['blocked', new Set(['rerouted', 'started', 'failed'])],
|
|
89
|
-
// Terminal: completed, expired, rerouted (no outgoing transitions)
|
|
97
|
+
['created', new Set(['offered', 'rerouted', 'cancelled'])],
|
|
98
|
+
['offered', new Set(['accepted', 'failed', 'expired', 'rerouted', 'cancelled'])],
|
|
99
|
+
['accepted', new Set(['started', 'timed_out', 'rerouted', 'cancelled'])],
|
|
100
|
+
['started', new Set(['completed', 'failed', 'blocked', 'timed_out', 'rerouted', 'cancelled'])],
|
|
101
|
+
['failed', new Set(['retrying', 'rerouted', 'cancelled'])],
|
|
102
|
+
['timed_out', new Set(['retrying', 'rerouted', 'cancelled'])],
|
|
103
|
+
['retrying', new Set(['offered', 'rerouted', 'cancelled'])],
|
|
104
|
+
['blocked', new Set(['rerouted', 'started', 'failed', 'cancelled'])],
|
|
105
|
+
// Terminal: completed, cancelled, expired, rerouted (no outgoing transitions)
|
|
90
106
|
]);
|
|
91
107
|
export function validateTransition(from, to) {
|
|
92
108
|
const allowed = VALID_TRANSITIONS.get(from);
|
|
@@ -148,6 +164,9 @@ export function transitionAssignment(id, newStatus, options, cwd) {
|
|
|
148
164
|
case 'completed':
|
|
149
165
|
assignment.completed_at = now;
|
|
150
166
|
break;
|
|
167
|
+
case 'cancelled':
|
|
168
|
+
assignment.cancelled_at = now;
|
|
169
|
+
break;
|
|
151
170
|
case 'failed':
|
|
152
171
|
assignment.failed_at = now;
|
|
153
172
|
break;
|
|
@@ -280,7 +299,7 @@ export function createAssignment(options, cwd) {
|
|
|
280
299
|
}
|
|
281
300
|
// ── Active Assignment Lookup ─────────────────────────────────
|
|
282
301
|
/** Statuses that indicate a finished assignment (no longer active). */
|
|
283
|
-
const TERMINAL_STATUSES = new Set(['completed', 'expired', 'rerouted']);
|
|
302
|
+
const TERMINAL_STATUSES = new Set(['completed', 'cancelled', 'expired', 'rerouted']);
|
|
284
303
|
/**
|
|
285
304
|
* Return the most recently created non-terminal assignment for the given agent.
|
|
286
305
|
* When `claimId` is provided, it is used as a fast-path lookup before falling
|
package/dist/core/context.js
CHANGED
|
@@ -445,7 +445,7 @@ export function buildContext(options = {}) {
|
|
|
445
445
|
const currentSession = loadCurrentSession(contextCwd);
|
|
446
446
|
if (currentAgentIdentity || agent) {
|
|
447
447
|
const claimPlanIds = new Set(myClaims.map((c) => c.plan_id).filter(Boolean));
|
|
448
|
-
const activeAssignments = listAssignments(contextCwd, { agent: agentName }).filter((assignment) => !['completed', 'failed', 'expired', 'rerouted'].includes(assignment.status));
|
|
448
|
+
const activeAssignments = listAssignments(contextCwd, { agent: agentName }).filter((assignment) => !['completed', 'failed', 'cancelled', 'expired', 'rerouted'].includes(assignment.status));
|
|
449
449
|
const inProgressPlans = state.plan_items.filter((p) => p.status === 'in_progress' &&
|
|
450
450
|
(p.assignee === agentName || claimPlanIds.has(p.id)));
|
|
451
451
|
if (myClaims.length > 0 || activeAssignments.length > 0 || inProgressPlans.length > 0) {
|
|
@@ -88,7 +88,7 @@ export function buildCoordinationSnapshot(options = {}) {
|
|
|
88
88
|
: filteredClaims,
|
|
89
89
|
active_assignments: (agent
|
|
90
90
|
? listAssignments(options.cwd, { agent })
|
|
91
|
-
: listAssignments(options.cwd)).filter((assignment) => !['completed', 'failed', 'expired', 'rerouted'].includes(assignment.status) &&
|
|
91
|
+
: listAssignments(options.cwd)).filter((assignment) => !['completed', 'failed', 'cancelled', 'expired', 'rerouted'].includes(assignment.status) &&
|
|
92
92
|
(!project || !assignment.plan_id || filteredPlans.some((plan) => plan.id === assignment.plan_id))),
|
|
93
93
|
active_runs: (agent
|
|
94
94
|
? listAgentRuns(options.cwd, { agent })
|
|
@@ -17,7 +17,7 @@ import { archiveCandidate, listCandidates, loadCandidate, saveCandidate, } from
|
|
|
17
17
|
import { addCrossProjectLink, removeCrossProjectLink, resolveCrossProjectLinks, } from './cross-project.js';
|
|
18
18
|
import { listClaims } from './claims.js';
|
|
19
19
|
import { listActionRequired } from './actions.js';
|
|
20
|
-
import { listAssignments } from './assignments.js';
|
|
20
|
+
import { deleteAssignment, listAssignments, loadAssignment, saveAssignment, transitionAssignment } from './assignments.js';
|
|
21
21
|
import { listAgentRuns } from './agentruns.js';
|
|
22
22
|
import { deleteRuntimeNote, listRuntimeNotes, saveRuntimeNote, } from './runtime.js';
|
|
23
23
|
import { createConstraint, createDecision, createTrap, } from './operations/memory-write.js';
|
|
@@ -309,6 +309,14 @@ export function updateEntity(name, id, patch, cwd) {
|
|
|
309
309
|
saveRuntimeNote(patched, cwd);
|
|
310
310
|
return { entity: name, id };
|
|
311
311
|
}
|
|
312
|
+
case 'assignment': {
|
|
313
|
+
const assignment = loadAssignment(id, cwd);
|
|
314
|
+
if (!assignment)
|
|
315
|
+
throw new EntityNotFoundError(name, id);
|
|
316
|
+
const patched = { ...assignment, ...patch };
|
|
317
|
+
saveAssignment(patched, cwd);
|
|
318
|
+
return { entity: name, id };
|
|
319
|
+
}
|
|
312
320
|
case 'candidate': {
|
|
313
321
|
const candidate = loadCandidate(id, cwd);
|
|
314
322
|
const patched = { ...candidate, ...patch };
|
|
@@ -371,6 +379,28 @@ export function removeEntity(name, id, cwd, purge = false) {
|
|
|
371
379
|
const removed = removeCrossProjectLink(id, cwd);
|
|
372
380
|
return { entity: name, id: removed.name ?? removed.path, archived: false, purged: true };
|
|
373
381
|
}
|
|
382
|
+
case 'assignment': {
|
|
383
|
+
const assignment = loadAssignment(id, cwd);
|
|
384
|
+
if (!assignment)
|
|
385
|
+
throw new EntityNotFoundError(name, id);
|
|
386
|
+
if (purge) {
|
|
387
|
+
const deleted = deleteAssignment(id, cwd);
|
|
388
|
+
if (!deleted)
|
|
389
|
+
throw new EntityNotFoundError(name, id);
|
|
390
|
+
return { entity: name, id, archived: false, purged: true };
|
|
391
|
+
}
|
|
392
|
+
if (assignment.status === 'cancelled') {
|
|
393
|
+
return { entity: name, id, archived: true, purged: false };
|
|
394
|
+
}
|
|
395
|
+
if (ENTITY_REGISTRY.assignment.terminal.includes(assignment.status)) {
|
|
396
|
+
throw new Error(`assignment '${id}' is already terminal (${assignment.status}); use purge:true to hard-delete if needed`);
|
|
397
|
+
}
|
|
398
|
+
transitionAssignment(id, 'cancelled', {
|
|
399
|
+
actor: 'brainclaw',
|
|
400
|
+
status_reason: 'Archived via bclaw_remove',
|
|
401
|
+
}, cwd);
|
|
402
|
+
return { entity: name, id, archived: true, purged: false };
|
|
403
|
+
}
|
|
374
404
|
default:
|
|
375
405
|
throw new EntityOperationUnsupportedError(name, 'remove');
|
|
376
406
|
}
|
|
@@ -418,6 +448,13 @@ export function transitionEntity(name, id, to, cwd, _reason) {
|
|
|
418
448
|
}
|
|
419
449
|
throw new InvalidTransitionError(name, from, to);
|
|
420
450
|
}
|
|
451
|
+
case 'assignment': {
|
|
452
|
+
transitionAssignment(id, to, {
|
|
453
|
+
actor: 'brainclaw',
|
|
454
|
+
status_reason: _reason,
|
|
455
|
+
}, cwd);
|
|
456
|
+
return { entity: name, id, from, to, side_effects: sideEffects };
|
|
457
|
+
}
|
|
421
458
|
default:
|
|
422
459
|
throw new EntityOperationUnsupportedError(name, 'transition', `Lifecycle transitions for ${name} not yet wired.`);
|
|
423
460
|
}
|
|
@@ -234,24 +234,25 @@ const assignment = {
|
|
|
234
234
|
name: 'assignment',
|
|
235
235
|
shortLabelPrefix: 'asgn',
|
|
236
236
|
schema: AssignmentSchema,
|
|
237
|
-
updatable: ['
|
|
237
|
+
updatable: ['description', 'status_reason', 'tags'],
|
|
238
238
|
statusField: 'status',
|
|
239
239
|
transitions: {
|
|
240
|
-
created: ['offered', 'expired'],
|
|
241
|
-
offered: ['accepted', 'expired', 'rerouted'],
|
|
242
|
-
accepted: ['started', 'failed', 'timed_out'],
|
|
243
|
-
started: ['completed', 'failed', 'blocked', 'timed_out'],
|
|
244
|
-
failed: ['retrying'],
|
|
245
|
-
timed_out: ['retrying'],
|
|
246
|
-
retrying: ['offered'],
|
|
247
|
-
blocked: ['started', 'rerouted', 'failed'],
|
|
240
|
+
created: ['offered', 'expired', 'rerouted', 'cancelled'],
|
|
241
|
+
offered: ['accepted', 'expired', 'rerouted', 'failed', 'cancelled'],
|
|
242
|
+
accepted: ['started', 'failed', 'timed_out', 'rerouted', 'cancelled'],
|
|
243
|
+
started: ['completed', 'failed', 'blocked', 'timed_out', 'rerouted', 'cancelled'],
|
|
244
|
+
failed: ['retrying', 'rerouted', 'cancelled'],
|
|
245
|
+
timed_out: ['retrying', 'rerouted', 'cancelled'],
|
|
246
|
+
retrying: ['offered', 'rerouted', 'cancelled'],
|
|
247
|
+
blocked: ['started', 'rerouted', 'failed', 'cancelled'],
|
|
248
248
|
},
|
|
249
|
-
terminal: ['completed', 'expired', 'rerouted'],
|
|
249
|
+
terminal: ['completed', 'cancelled', 'expired', 'rerouted'],
|
|
250
250
|
sideEffects: {
|
|
251
251
|
'created->offered': ['timestamp:offered_at', 'event:assignment_offered', 'audit:assignment_offered'],
|
|
252
252
|
'offered->accepted': ['timestamp:accepted_at', 'event:assignment_accepted', 'sync:agent_run'],
|
|
253
253
|
'accepted->started': ['timestamp:started_at', 'event:assignment_started', 'sync:agent_run'],
|
|
254
254
|
'started->completed': ['timestamp:completed_at', 'event:assignment_completed', 'sync:agent_run'],
|
|
255
|
+
'started->cancelled': ['timestamp:cancelled_at', 'event:assignment_cancelled', 'sync:agent_run'],
|
|
255
256
|
'started->failed': ['timestamp:failed_at', 'event:assignment_failed', 'sync:agent_run'],
|
|
256
257
|
},
|
|
257
258
|
};
|
package/dist/core/schema.js
CHANGED
|
@@ -598,6 +598,7 @@ export const AssignmentStatusSchema = z.enum([
|
|
|
598
598
|
'accepted', // Worker acknowledged receipt
|
|
599
599
|
'started', // Worker reports active work begun
|
|
600
600
|
'completed', // Worker reports successful completion
|
|
601
|
+
'cancelled', // Supervisor/admin aborted the assignment explicitly
|
|
601
602
|
'failed', // Worker reports failure
|
|
602
603
|
'blocked', // Worker reports external blocker
|
|
603
604
|
'timed_out', // Sweeper detected no heartbeat within TTL
|
|
@@ -642,6 +643,7 @@ export const AssignmentSchema = z.object({
|
|
|
642
643
|
accepted_at: z.string().optional(),
|
|
643
644
|
started_at: z.string().optional(),
|
|
644
645
|
completed_at: z.string().optional(),
|
|
646
|
+
cancelled_at: z.string().optional(),
|
|
645
647
|
failed_at: z.string().optional(),
|
|
646
648
|
blocked_at: z.string().optional(),
|
|
647
649
|
timed_out_at: z.string().optional(),
|
|
@@ -814,6 +816,7 @@ export const RuntimeEventTypeSchema = z.enum([
|
|
|
814
816
|
'assignment_started',
|
|
815
817
|
'assignment_progress',
|
|
816
818
|
'assignment_completed',
|
|
819
|
+
'assignment_cancelled',
|
|
817
820
|
'assignment_failed',
|
|
818
821
|
'assignment_blocked',
|
|
819
822
|
'assignment_timed_out',
|
|
@@ -91,48 +91,53 @@ export function resolveTargetStore(cwd = process.cwd(), target = 'local', option
|
|
|
91
91
|
*
|
|
92
92
|
* Priority:
|
|
93
93
|
* 1. explicitCwd (--cwd flag)
|
|
94
|
-
* 2. BRAINCLAW_CWD env var →
|
|
95
|
-
* 3. BRAINCLAW_PROJECT env var → resolved by name/path from workspace
|
|
96
|
-
* 4. Session-scoped active project (from .current-session)
|
|
94
|
+
* 2. BRAINCLAW_CWD env var → workspace anchor injected by MCP configs
|
|
95
|
+
* 3. BRAINCLAW_PROJECT env var → resolved by name/path from workspace anchor
|
|
96
|
+
* 4. Session-scoped active project (from .current-session under the anchor)
|
|
97
97
|
* 5. Global active-project.json in workspace root
|
|
98
|
-
* 6. process.cwd()
|
|
98
|
+
* 6. Workspace anchor or process.cwd()
|
|
99
99
|
*/
|
|
100
100
|
export function resolveEffectiveCwd(options = {}) {
|
|
101
|
+
const baseCwd = path.resolve(options.baseCwd ?? process.cwd());
|
|
101
102
|
// 1. Explicit --cwd flag
|
|
102
103
|
if (options.explicitCwd) {
|
|
103
104
|
return path.resolve(options.explicitCwd);
|
|
104
105
|
}
|
|
105
|
-
// 2. BRAINCLAW_CWD env var — set by MCP configs to
|
|
106
|
-
// regardless of the IDE's process.cwd() at launch time
|
|
106
|
+
// 2. BRAINCLAW_CWD env var — set by MCP configs to anchor resolution to the
|
|
107
|
+
// workspace regardless of the IDE's process.cwd() at launch time. It is a
|
|
108
|
+
// workspace anchor, not the final answer: session/global active project
|
|
109
|
+
// state still overrides it.
|
|
110
|
+
let anchorCwd = baseCwd;
|
|
107
111
|
const envCwd = process.env.BRAINCLAW_CWD?.trim();
|
|
108
|
-
|
|
109
|
-
|
|
112
|
+
const hasEnvWorkspace = !!envCwd && fs.existsSync(path.join(path.resolve(envCwd), MEMORY_DIR, 'config.yaml'));
|
|
113
|
+
if (hasEnvWorkspace) {
|
|
114
|
+
anchorCwd = path.resolve(envCwd);
|
|
110
115
|
}
|
|
111
116
|
// 3. BRAINCLAW_PROJECT env var
|
|
112
117
|
const envProject = process.env.BRAINCLAW_PROJECT;
|
|
113
118
|
if (envProject) {
|
|
114
|
-
const resolved = resolveProjectRef(envProject,
|
|
119
|
+
const resolved = resolveProjectRef(envProject, anchorCwd, options.storeChainOptions);
|
|
115
120
|
if (resolved)
|
|
116
121
|
return resolved;
|
|
117
122
|
}
|
|
118
|
-
//
|
|
119
|
-
const session = loadCurrentSession(
|
|
123
|
+
// 4. Session-scoped active project (per-agent, no cross-agent interference)
|
|
124
|
+
const session = loadCurrentSession(anchorCwd);
|
|
120
125
|
if (session?.active_project) {
|
|
121
126
|
const sp = session.active_project;
|
|
122
127
|
if (fs.existsSync(path.join(sp.path, MEMORY_DIR, 'config.yaml'))) {
|
|
123
128
|
return sp.path;
|
|
124
129
|
}
|
|
125
130
|
}
|
|
126
|
-
//
|
|
127
|
-
const wsRoot = resolveWorkspaceRoot(
|
|
131
|
+
// 5. Global active-project.json from workspace root
|
|
132
|
+
const wsRoot = hasEnvWorkspace ? anchorCwd : resolveWorkspaceRoot(anchorCwd, options.storeChainOptions);
|
|
128
133
|
if (wsRoot) {
|
|
129
134
|
const active = loadActiveProject(wsRoot);
|
|
130
135
|
if (active && fs.existsSync(path.join(active.path, MEMORY_DIR, 'config.yaml'))) {
|
|
131
136
|
return active.path;
|
|
132
137
|
}
|
|
133
138
|
}
|
|
134
|
-
//
|
|
135
|
-
return
|
|
139
|
+
// 6. Default
|
|
140
|
+
return anchorCwd;
|
|
136
141
|
}
|
|
137
142
|
/**
|
|
138
143
|
* Find the workspace root (farthest store in the chain, or the one with
|
|
@@ -150,9 +155,14 @@ export function resolveWorkspaceRoot(cwd = process.cwd(), options = {}) {
|
|
|
150
155
|
* Returns undefined when the reference cannot be resolved to a valid brainclaw project.
|
|
151
156
|
*/
|
|
152
157
|
export function resolveProjectRef(ref, cwd = process.cwd(), storeChainOptions) {
|
|
158
|
+
const envWorkspace = process.env.BRAINCLAW_CWD?.trim();
|
|
159
|
+
const workspaceAnchor = envWorkspace && fs.existsSync(path.join(path.resolve(envWorkspace), MEMORY_DIR, 'config.yaml'))
|
|
160
|
+
? path.resolve(envWorkspace)
|
|
161
|
+
: undefined;
|
|
153
162
|
// Walk UP from real cwd to find the outermost .brainclaw/ — this avoids
|
|
154
163
|
// circular resolution when an active project narrows the workspace view.
|
|
155
|
-
const wsRoot =
|
|
164
|
+
const wsRoot = workspaceAnchor
|
|
165
|
+
?? findOutermostBrainclawRoot(cwd)
|
|
156
166
|
?? resolveWorkspaceRoot(cwd, storeChainOptions);
|
|
157
167
|
if (!wsRoot)
|
|
158
168
|
return undefined;
|
package/dist/facts.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// Generated by scripts/emit-site-facts.mjs at build time. Do not edit manually.
|
|
2
|
-
// Source: brainclaw v1.5.
|
|
2
|
+
// Source: brainclaw v1.5.5 on 2026-05-13T19:24:16.549Z
|
|
3
3
|
export const FACTS = {
|
|
4
|
-
"version": "1.5.
|
|
5
|
-
"generated_at": "2026-05-
|
|
4
|
+
"version": "1.5.5",
|
|
5
|
+
"generated_at": "2026-05-13T19:24:16.549Z",
|
|
6
6
|
"tools": {
|
|
7
7
|
"count": 60,
|
|
8
8
|
"published_count": 59,
|
package/dist/facts.json
CHANGED