brainclaw 1.7.5 → 1.9.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 +28 -11
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/cli.js +139 -13
- package/dist/commands/add-step.js +1 -1
- package/dist/commands/bootstrap.js +2 -26
- package/dist/commands/check-security-mcp.js +50 -33
- package/dist/commands/check-security.js +86 -43
- package/dist/commands/claim.js +22 -21
- package/dist/commands/confirm.js +26 -0
- package/dist/commands/context-diff.js +1 -1
- package/dist/commands/dispatch-watch.js +142 -0
- package/dist/commands/doctor.js +113 -2
- package/dist/commands/estimation-report.js +115 -16
- package/dist/commands/harvest.js +502 -16
- package/dist/commands/init.js +123 -21
- package/dist/commands/loops-handlers.js +4 -0
- package/dist/commands/mcp-read-handlers.js +198 -29
- package/dist/commands/mcp.js +615 -92
- package/dist/commands/memory.js +21 -17
- package/dist/commands/migrate.js +81 -17
- package/dist/commands/prune.js +78 -4
- package/dist/commands/reflect.js +26 -20
- package/dist/commands/register-agent.js +57 -1
- package/dist/commands/repair.js +20 -0
- package/dist/commands/session-end.js +15 -6
- package/dist/commands/session-start.js +18 -1
- package/dist/commands/setup-security.js +39 -18
- package/dist/commands/setup.js +26 -27
- package/dist/commands/stale.js +16 -2
- package/dist/commands/uninstall.js +126 -34
- package/dist/commands/update-step.js +6 -0
- package/dist/commands/worktree.js +60 -0
- package/dist/core/actions.js +12 -3
- package/dist/core/agent-capability.js +11 -13
- package/dist/core/agent-files.js +844 -547
- package/dist/core/agent-integrations.js +0 -3
- package/dist/core/agent-inventory.js +67 -0
- package/dist/core/agent-registry.js +163 -29
- package/dist/core/agentrun-reconciler.js +33 -2
- package/dist/core/agentruns.js +7 -1
- package/dist/core/ai-agent-detection.js +31 -44
- package/dist/core/archival.js +15 -9
- package/dist/core/assignment-reconciler.js +56 -0
- package/dist/core/assignment-sweeper.js +127 -4
- package/dist/core/assignments.js +69 -11
- package/dist/core/bootstrap.js +233 -67
- package/dist/core/brainclaw-version.js +22 -0
- package/dist/core/candidates.js +21 -1
- package/dist/core/claims.js +313 -150
- package/dist/core/config.js +6 -1
- package/dist/core/context-diff.js +148 -20
- package/dist/core/context.js +129 -8
- package/dist/core/coordination.js +22 -3
- package/dist/core/dispatch-status.js +109 -5
- package/dist/core/dispatcher.js +65 -11
- package/dist/core/entity-operations.js +45 -24
- package/dist/core/entity-registry.js +31 -5
- package/dist/core/event-log.js +138 -21
- package/dist/core/events/checkpoint.js +258 -0
- package/dist/core/events/genesis.js +220 -0
- package/dist/core/events/journal.js +507 -0
- package/dist/core/events/materialize.js +126 -0
- package/dist/core/events/registry-post-image.js +110 -0
- package/dist/core/events/verify.js +109 -0
- package/dist/core/execution-adapters.js +23 -0
- package/dist/core/execution.js +25 -0
- package/dist/core/facade-schema.js +48 -0
- package/dist/core/gc-semantic.js +130 -5
- package/dist/core/handoff-snapshot.js +68 -0
- package/dist/core/ids.js +19 -8
- package/dist/core/instruction-templates.js +34 -115
- package/dist/core/io.js +39 -3
- package/dist/core/json-store.js +10 -1
- package/dist/core/lock.js +153 -28
- package/dist/core/loops/bootstrap-acquire.js +25 -1
- package/dist/core/loops/facade-schema.js +2 -0
- package/dist/core/loops/hooks/survey-signals-baseline.js +36 -0
- package/dist/core/loops/index.js +1 -0
- package/dist/core/loops/presets/bootstrap.js +7 -0
- package/dist/core/loops/store.js +17 -0
- package/dist/core/loops/verbs.js +24 -1
- package/dist/core/markdown.js +8 -76
- package/dist/core/mcp-command-resolution.js +245 -0
- package/dist/core/memory-compactor.js +5 -3
- package/dist/core/memory-lifecycle.js +282 -0
- package/dist/core/merge-risk.js +150 -0
- package/dist/core/messaging.js +8 -1
- package/dist/core/migration.js +11 -1
- package/dist/core/observer-mode.js +26 -0
- package/dist/core/operations/memory-mutation.js +90 -65
- package/dist/core/operations/plan.js +27 -1
- package/dist/core/protocol-skills.js +210 -0
- package/dist/core/reflection-safety.js +6 -7
- package/dist/core/reputation.js +84 -2
- package/dist/core/runtime-signals.js +71 -9
- package/dist/core/runtime.js +84 -1
- package/dist/core/schema.js +125 -0
- package/dist/core/security-detectors.js +125 -0
- package/dist/core/security-extract.js +189 -0
- package/dist/core/security-guard.js +107 -29
- package/dist/core/security-packages.js +121 -0
- package/dist/core/security-scoring.js +76 -9
- package/dist/core/security.js +34 -2
- package/dist/core/sequence.js +11 -2
- package/dist/core/setup-flow.js +141 -13
- package/dist/core/spawn-check.js +110 -4
- package/dist/core/staleness.js +109 -1
- package/dist/core/state.js +250 -54
- package/dist/core/store-resolution.js +19 -5
- package/dist/core/worktree.js +169 -7
- package/dist/facts.js +8 -8
- package/dist/facts.json +7 -7
- package/docs/PROTOCOL.md +223 -0
- package/docs/cli.md +11 -10
- package/docs/concepts/coordinator-runbook.md +129 -0
- package/docs/concepts/dispatch-lifecycle.md +17 -0
- package/docs/concepts/event-log-store-critique-A.md +333 -0
- package/docs/concepts/event-log-store-critique-B.md +353 -0
- package/docs/concepts/event-log-store-phase0-measurements.md +58 -0
- package/docs/concepts/event-log-store-proposal-A.md +365 -0
- package/docs/concepts/event-log-store-proposal-B.md +404 -0
- package/docs/concepts/event-log-store.md +928 -0
- package/docs/concepts/identity-model-proposal.md +371 -0
- package/docs/concepts/memory.md +5 -4
- package/docs/concepts/observer-protocol.md +361 -0
- package/docs/concepts/parallel-merge-protocol.md +71 -0
- package/docs/concepts/plans-and-claims.md +43 -0
- package/docs/concepts/skills.md +78 -0
- package/docs/concepts/workspace-bootstrapping.md +61 -0
- package/docs/integrations/agents.md +4 -4
- package/docs/integrations/cline.md +10 -11
- package/docs/integrations/codex.md +2 -2
- package/docs/integrations/continue.md +5 -5
- package/docs/integrations/copilot.md +14 -12
- package/docs/integrations/openclaw.md +7 -6
- package/docs/integrations/overview.md +7 -7
- package/docs/integrations/roo.md +3 -3
- package/docs/integrations/windsurf.md +6 -6
- package/docs/mcp-schema-changelog.md +51 -20
- package/docs/quickstart.md +48 -47
- package/docs/security.md +174 -15
- package/docs/storage.md +4 -2
- package/package.json +8 -6
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { runBootstrapProfile } from '../../bootstrap.js';
|
|
2
|
+
const DEFAULT_MAX_SEEDS = 20;
|
|
3
|
+
const DEFAULT_MAX_BYTES = 3800;
|
|
4
|
+
export function buildSurveySignalsBaseline(cwd, opts = {}) {
|
|
5
|
+
const maxSeeds = opts.maxSeeds ?? DEFAULT_MAX_SEEDS;
|
|
6
|
+
const maxBytes = opts.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
7
|
+
const result = runBootstrapProfile({ cwd });
|
|
8
|
+
const baseline = {
|
|
9
|
+
generated_at: new Date().toISOString(),
|
|
10
|
+
source: 'deterministic_scanner',
|
|
11
|
+
summary: result.profile.summary,
|
|
12
|
+
workspace_kind: result.profile.workspace_kind,
|
|
13
|
+
onboarding_mode: result.profile.onboarding_mode,
|
|
14
|
+
confidence: result.profile.confidence,
|
|
15
|
+
sources_scanned: result.profile.sources_scanned,
|
|
16
|
+
native_instruction_files: result.profile.native_instruction_files,
|
|
17
|
+
gaps: result.profile.gaps,
|
|
18
|
+
seed_count: result.seeds.length,
|
|
19
|
+
seeds: result.seeds.slice(0, maxSeeds).map((seed) => ({
|
|
20
|
+
kind: seed.seed_kind,
|
|
21
|
+
text: seed.text,
|
|
22
|
+
source_kind: seed.source_kind,
|
|
23
|
+
source_ref: seed.source_ref,
|
|
24
|
+
confidence: seed.confidence,
|
|
25
|
+
})),
|
|
26
|
+
seeds_truncated: result.seeds.length > maxSeeds,
|
|
27
|
+
};
|
|
28
|
+
// Fit the byte budget by shedding seeds from the tail; the histogram-level
|
|
29
|
+
// fields are small and always kept.
|
|
30
|
+
while (baseline.seeds.length > 0 && Buffer.byteLength(JSON.stringify(baseline), 'utf8') > maxBytes) {
|
|
31
|
+
baseline.seeds.pop();
|
|
32
|
+
baseline.seeds_truncated = true;
|
|
33
|
+
}
|
|
34
|
+
return baseline;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=survey-signals-baseline.js.map
|
package/dist/core/loops/index.js
CHANGED
|
@@ -8,6 +8,7 @@ export { BOOTSTRAP_PRESET } from './presets/bootstrap.js';
|
|
|
8
8
|
export { writeProjectMdSafe, } from './hooks/bootstrap-write.js';
|
|
9
9
|
export { notifyOperatorOnInputRequested } from './hooks/notify-operator.js';
|
|
10
10
|
export { readSurveySources, } from './hooks/survey-source-reader.js';
|
|
11
|
+
export { buildSurveySignalsBaseline, } from './hooks/survey-signals-baseline.js';
|
|
11
12
|
export { acquireLock, hashRequest, recordConflict, withLoopLock, DEFAULT_MAX_MUTATION_DURATION_MS, IDEMPOTENCY_TTL_MS, LEASE_GRACE_MS, LEASE_WINDOW_MS, IdempotencyKeyReusedError, IdempotencyOwnerMismatchError, LockLostError, LockTimeoutError, VersionConflictError, } from './lock.js';
|
|
12
13
|
export { acquireBootstrapLoop, findExistingBootstrapLoop, BootstrapCoordinationInProgressError, } from './bootstrap-acquire.js';
|
|
13
14
|
//# sourceMappingURL=index.js.map
|
|
@@ -22,6 +22,13 @@ export const BOOTSTRAP_PRESET = {
|
|
|
22
22
|
// is unchanged — see types.ts). Empirical motivation: TranslaVox cold-start
|
|
23
23
|
// missed the actual GCP Speech+Translate pipeline because survey scanned only
|
|
24
24
|
// topology + manifests (can_0160d6c4).
|
|
25
|
+
//
|
|
26
|
+
// pln#557 step 4 — the deterministic scanner (runBootstrapProfile) now
|
|
27
|
+
// seeds this phase: acquireBootstrapLoop attaches a `signals_baseline`
|
|
28
|
+
// artifact (toolchain, topology, native rules, top seeds) at open time.
|
|
29
|
+
// The champion enriches that baseline into its signals_report — survey
|
|
30
|
+
// quality stops depending on per-agent re-discovery, and bclaw_bootstrap
|
|
31
|
+
// becomes an internal helper of the loop rather than a competing door.
|
|
25
32
|
{
|
|
26
33
|
name: 'survey',
|
|
27
34
|
context_filter: ['project_vision', 'decisions', 'plans', 'feedback'],
|
package/dist/core/loops/store.js
CHANGED
|
@@ -3,6 +3,7 @@ import fs from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { memoryDir, writeFileAtomic } from '../io.js';
|
|
5
5
|
import { nowISO } from '../ids.js';
|
|
6
|
+
import { convergeAssignmentToTerminal } from '../assignments.js';
|
|
6
7
|
import { writeProjectMdSafe } from './hooks/bootstrap-write.js';
|
|
7
8
|
import { notifyOperatorOnInputRequested } from './hooks/notify-operator.js';
|
|
8
9
|
import { DEFAULT_PROTOCOLS, LoopArtifactSchema, LoopEventSchema, LoopThreadSchema, } from './types.js';
|
|
@@ -416,6 +417,22 @@ export function closeLoop(input, cwd) {
|
|
|
416
417
|
reason: input.reason,
|
|
417
418
|
}, cwd);
|
|
418
419
|
writeThreadFile(next, cwd);
|
|
420
|
+
// pln#563: converge slot assignments so they don't fossilize in `offered`.
|
|
421
|
+
// A review-loop assignment exists only to drive the turn; closing the loop is
|
|
422
|
+
// the authoritative end of that work. File-based / sandboxed workers never
|
|
423
|
+
// report a terminal status themselves, and the coordinator can't cross-update
|
|
424
|
+
// a worker-owned assignment (trp#291) — so the loop close (a system action)
|
|
425
|
+
// is the right place. Best-effort: a missing or already-terminal assignment
|
|
426
|
+
// must never block the close.
|
|
427
|
+
const assignmentTerminal = input.final_status === 'completed' ? 'completed' : 'cancelled';
|
|
428
|
+
for (const slot of next.slots) {
|
|
429
|
+
if (!slot.assignment_id)
|
|
430
|
+
continue;
|
|
431
|
+
try {
|
|
432
|
+
convergeAssignmentToTerminal(slot.assignment_id, assignmentTerminal, `loop ${input.id} closed (${input.final_status})`, cwd);
|
|
433
|
+
}
|
|
434
|
+
catch { /* never block loop close on assignment convergence */ }
|
|
435
|
+
}
|
|
419
436
|
return next;
|
|
420
437
|
}
|
|
421
438
|
//# sourceMappingURL=store.js.map
|
package/dist/core/loops/verbs.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
2
|
import { nowISO } from '../ids.js';
|
|
3
|
+
import { convergeAssignmentToTerminal } from '../assignments.js';
|
|
3
4
|
import { writeProjectMdSafe } from './hooks/bootstrap-write.js';
|
|
4
5
|
import { appendEvent, closeLoop, generateMutationId, getLoop, listLoopEvents, writeThreadFile, } from './store.js';
|
|
5
6
|
import { LoopArtifactSchema, PAUSE_REASONS, } from './types.js';
|
|
@@ -19,6 +20,17 @@ function loadLoopOrThrow(id, cwd) {
|
|
|
19
20
|
throw new Error(`unknown loop_id ${id}`);
|
|
20
21
|
return loop;
|
|
21
22
|
}
|
|
23
|
+
function convergeSlotAssignmentsForClosedLoop(thread, finalStatus, cwd) {
|
|
24
|
+
const assignmentTerminal = finalStatus === 'completed' ? 'completed' : 'cancelled';
|
|
25
|
+
for (const slot of thread.slots) {
|
|
26
|
+
if (!slot.assignment_id)
|
|
27
|
+
continue;
|
|
28
|
+
try {
|
|
29
|
+
convergeAssignmentToTerminal(slot.assignment_id, assignmentTerminal, `loop ${thread.id} closed (${finalStatus})`, cwd);
|
|
30
|
+
}
|
|
31
|
+
catch { /* never block loop close on assignment convergence */ }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
22
34
|
/* ========================= Stop-condition evaluator ======================= */
|
|
23
35
|
function isVerdictAccepted(artifact) {
|
|
24
36
|
if (artifact.type !== 'verdict')
|
|
@@ -369,6 +381,7 @@ function commitClosedTransition(thread, final_status, actor, reason, cwd) {
|
|
|
369
381
|
reason,
|
|
370
382
|
}, cwd);
|
|
371
383
|
writeThreadFile(next, cwd);
|
|
384
|
+
convergeSlotAssignmentsForClosedLoop(next, final_status, cwd);
|
|
372
385
|
return next;
|
|
373
386
|
}
|
|
374
387
|
function resolveTurnSlot(thread, input) {
|
|
@@ -402,6 +415,7 @@ export function turn(input, cwd) {
|
|
|
402
415
|
status: 'assigned',
|
|
403
416
|
phase: current.current_phase,
|
|
404
417
|
assignment_id: input.assignment_id ?? slot.assignment_id,
|
|
418
|
+
claim_id: input.claim_id ?? slot.claim_id,
|
|
405
419
|
}
|
|
406
420
|
: slot);
|
|
407
421
|
const next = {
|
|
@@ -435,7 +449,13 @@ export function complete_turn(input, cwd) {
|
|
|
435
449
|
throw new Error(`complete_turn: slot_id "${input.slot_id}" not in loop`);
|
|
436
450
|
// Slot-bound auth. Only enforced when caller_agent_id is supplied (MCP entry path).
|
|
437
451
|
if (input.caller_agent_id !== undefined && !input.admin_override) {
|
|
438
|
-
|
|
452
|
+
// Instance binding (pln#562 step 4): a claim-bound slot is owned by the
|
|
453
|
+
// INSTANCE holding that claim. When both sides carry claim info, claim
|
|
454
|
+
// equality decides; otherwise fall back to the legacy agent_id check.
|
|
455
|
+
const claimBound = slot.claim_id !== undefined && input.caller_claim_id !== undefined;
|
|
456
|
+
const ownerMatches = claimBound
|
|
457
|
+
? slot.claim_id === input.caller_claim_id
|
|
458
|
+
: slot.agent_id !== undefined && slot.agent_id === input.caller_agent_id;
|
|
439
459
|
const creatorMatches = current.created_by === input.caller_agent_id;
|
|
440
460
|
if (!ownerMatches && !creatorMatches) {
|
|
441
461
|
throw new Error('unauthorized_slot_write');
|
|
@@ -998,6 +1018,9 @@ export function provideInput(input, cwd) {
|
|
|
998
1018
|
}, cwd);
|
|
999
1019
|
}
|
|
1000
1020
|
writeThreadFile(next, cwd);
|
|
1021
|
+
if (fileApplyResolution !== undefined) {
|
|
1022
|
+
convergeSlotAssignmentsForClosedLoop(next, 'completed', cwd);
|
|
1023
|
+
}
|
|
1001
1024
|
assertOpenQuestionsInvariant(next, 'provide_input');
|
|
1002
1025
|
return { thread: next, artifact_id: newArtifact.artifact_id, duplicate: false };
|
|
1003
1026
|
}
|
package/dist/core/markdown.js
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
import { listClaims } from './claims.js';
|
|
2
1
|
import { loadInstructions } from './instructions.js';
|
|
3
2
|
import { memoryPath, writeFileAtomic } from './io.js';
|
|
4
3
|
import { isTrapActive } from './traps.js';
|
|
5
4
|
import { logger } from './logger.js';
|
|
6
5
|
export function generateMarkdown(state, cwd) {
|
|
7
|
-
const lines = [
|
|
6
|
+
const lines = [
|
|
7
|
+
'# Project Memory',
|
|
8
|
+
'',
|
|
9
|
+
'> Legacy derived summary generated from canonical Brainclaw memory.',
|
|
10
|
+
'> PROJECT.md at the workspace root is the durable project vision.',
|
|
11
|
+
'> For active claims, plans, handoffs, and agent state, use `brainclaw agent-board` or MCP board context.',
|
|
12
|
+
'',
|
|
13
|
+
];
|
|
8
14
|
const instructions = loadInstructions(cwd).filter((entry) => entry.active);
|
|
9
|
-
const claims = listClaims(cwd).filter((claim) => claim.status === 'active');
|
|
10
15
|
lines.push('## Shared instructions');
|
|
11
16
|
if (instructions.length === 0) {
|
|
12
17
|
lines.push('- (none)');
|
|
@@ -19,50 +24,6 @@ export function generateMarkdown(state, cwd) {
|
|
|
19
24
|
}
|
|
20
25
|
}
|
|
21
26
|
lines.push('');
|
|
22
|
-
lines.push('## Active claims');
|
|
23
|
-
if (claims.length === 0) {
|
|
24
|
-
lines.push('- (none)');
|
|
25
|
-
}
|
|
26
|
-
else {
|
|
27
|
-
for (const claim of claims) {
|
|
28
|
-
const meta = [claim.scope];
|
|
29
|
-
if (claim.plan_id)
|
|
30
|
-
meta.push(`plan: ${claim.plan_id}`);
|
|
31
|
-
if (claim.project)
|
|
32
|
-
meta.push(`project: ${claim.project}`);
|
|
33
|
-
lines.push(`- **[${claim.id}]** ${claim.agent} → ${claim.description} _(${meta.join(', ')})_`);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
lines.push('');
|
|
37
|
-
lines.push('## Shared plan');
|
|
38
|
-
const activePlans = state.plan_items.filter((plan) => plan.status !== 'done' && plan.status !== 'dropped');
|
|
39
|
-
if (activePlans.length === 0) {
|
|
40
|
-
lines.push('- (none)');
|
|
41
|
-
}
|
|
42
|
-
else {
|
|
43
|
-
for (const plan of activePlans) {
|
|
44
|
-
const meta = [plan.status, plan.priority];
|
|
45
|
-
if (plan.assignee)
|
|
46
|
-
meta.push(`assignee: ${plan.assignee}`);
|
|
47
|
-
if (plan.project)
|
|
48
|
-
meta.push(`project: ${plan.project}`);
|
|
49
|
-
if (plan.steps && plan.steps.length > 0) {
|
|
50
|
-
const done = plan.steps.filter((s) => s.status === 'done').length;
|
|
51
|
-
meta.push(`${done}/${plan.steps.length} steps`);
|
|
52
|
-
}
|
|
53
|
-
const tags = plan.tags.length ? ` [${plan.tags.join(', ')}]` : '';
|
|
54
|
-
const paths = plan.related_paths?.length ? ` → ${plan.related_paths.join(', ')}` : '';
|
|
55
|
-
lines.push(`- **[${plan.id}]** ${plan.text} _(${meta.join(', ')})_${paths}${tags}`);
|
|
56
|
-
if (plan.steps && plan.steps.length > 0) {
|
|
57
|
-
for (const step of plan.steps) {
|
|
58
|
-
const check = step.status === 'done' ? 'x' : ' ';
|
|
59
|
-
const assign = step.assignee ? ` _(${step.assignee})_` : '';
|
|
60
|
-
lines.push(` - [${check}] [${step.id}] ${step.text}${assign}`);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
lines.push('');
|
|
66
27
|
lines.push('## Active constraints');
|
|
67
28
|
if (state.active_constraints.length === 0) {
|
|
68
29
|
lines.push('- (none)');
|
|
@@ -100,35 +61,6 @@ export function generateMarkdown(state, cwd) {
|
|
|
100
61
|
}
|
|
101
62
|
}
|
|
102
63
|
lines.push('');
|
|
103
|
-
lines.push('## Open handoffs');
|
|
104
|
-
const MAX_HANDOFFS = 10;
|
|
105
|
-
const MAX_HANDOFF_TEXT = 500;
|
|
106
|
-
const activeHandoffs = state.open_handoffs
|
|
107
|
-
.filter((h) => h.status === 'open')
|
|
108
|
-
.sort((a, b) => b.created_at.localeCompare(a.created_at))
|
|
109
|
-
.slice(0, MAX_HANDOFFS);
|
|
110
|
-
const totalOpen = state.open_handoffs.filter((h) => h.status === 'open').length;
|
|
111
|
-
if (activeHandoffs.length === 0) {
|
|
112
|
-
lines.push('- (none)');
|
|
113
|
-
}
|
|
114
|
-
else {
|
|
115
|
-
for (const h of activeHandoffs) {
|
|
116
|
-
const tags = h.tags.length ? ` [${h.tags.join(', ')}]` : '';
|
|
117
|
-
const paths = h.related_paths?.length ? ` → ${h.related_paths.join(', ')}` : '';
|
|
118
|
-
const meta = [h.status];
|
|
119
|
-
if (h.plan_id)
|
|
120
|
-
meta.push(`plan: ${h.plan_id}`);
|
|
121
|
-
if (h.project)
|
|
122
|
-
meta.push(`project: ${h.project}`);
|
|
123
|
-
const text = h.text.length > MAX_HANDOFF_TEXT ? h.text.slice(0, MAX_HANDOFF_TEXT) + '...' : h.text;
|
|
124
|
-
lines.push(`- **[${h.id}]** ${h.from} → ${h.to}: ${text} _(${meta.join(', ')})_${paths}${tags}`);
|
|
125
|
-
}
|
|
126
|
-
const omitted = totalOpen - activeHandoffs.length;
|
|
127
|
-
if (omitted > 0) {
|
|
128
|
-
lines.push(`- _(${omitted} older handoffs omitted — use \`bclaw_read_handoff\` to inspect)_`);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
lines.push('');
|
|
132
64
|
return lines.join('\n');
|
|
133
65
|
}
|
|
134
66
|
/**
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP command resolution + shared writer plumbing (pln#546 step 3 extraction).
|
|
3
|
+
*
|
|
4
|
+
* Extracted from agent-files.ts so the binary-resolution / shim-tracing logic
|
|
5
|
+
* can evolve independently of the per-agent writers. Three concerns live here:
|
|
6
|
+
*
|
|
7
|
+
* 1. Resolving the brainclaw MCP server invocation from any host shell
|
|
8
|
+
* (which/where → cli.js shim → absolute node + cli.js pair). Falls back
|
|
9
|
+
* to `npx brainclaw mcp` when nothing else resolves.
|
|
10
|
+
* 2. Building the `brainclawMcpEntry` JSON shape that every MCP writer emits
|
|
11
|
+
* under `mcpServers.brainclaw` (or its agent-specific equivalent).
|
|
12
|
+
* 3. Hook-command rendering — `getBclawCliParts` + `buildHookCommand` for
|
|
13
|
+
* writers that emit session-start / Stop / context-diff entries.
|
|
14
|
+
*
|
|
15
|
+
* The `_forceResolve` module flag is private to this file; callers toggle it
|
|
16
|
+
* via `withForcedResolve(cb)` so the next pair of `brainclawMcpEntry` calls
|
|
17
|
+
* overwrites existing absolute paths (used by `patchAllMcpConfigs` post-upgrade).
|
|
18
|
+
*/
|
|
19
|
+
import fs from 'node:fs';
|
|
20
|
+
import os from 'node:os';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import { spawnSync } from 'node:child_process';
|
|
23
|
+
import { fileURLToPath } from 'node:url';
|
|
24
|
+
export function isJsonObject(value) {
|
|
25
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
26
|
+
}
|
|
27
|
+
/** Cached MCP command — resolved once per process. */
|
|
28
|
+
let cachedMcpCommand;
|
|
29
|
+
/** Module-level flag: when true, brainclawMcpEntry overwrites existing paths. */
|
|
30
|
+
let _forceResolve = false;
|
|
31
|
+
/**
|
|
32
|
+
* Resolve the brainclaw command for MCP configs.
|
|
33
|
+
* Returns `{ command: "<node>", args: ["<cli.js>", "mcp"] }` so the config
|
|
34
|
+
* works in non-login shells (VS Code Server, MCP subprocesses) on all OSes.
|
|
35
|
+
*
|
|
36
|
+
* Strategy:
|
|
37
|
+
* 1. Find the brainclaw bin via which/where
|
|
38
|
+
* 2. Trace from the bin/shim to the actual cli.js entry point
|
|
39
|
+
* 3. Pair it with the absolute node path
|
|
40
|
+
* Falls back to 'npx brainclaw mcp' if resolution fails.
|
|
41
|
+
*/
|
|
42
|
+
function resolveBrainclawMcpCommand() {
|
|
43
|
+
const nodeBin = process.execPath;
|
|
44
|
+
// 1. Try to resolve the cli.js from the installed brainclaw binary
|
|
45
|
+
const cliJs = resolveBrainclawCliJs();
|
|
46
|
+
if (cliJs) {
|
|
47
|
+
return { command: nodeBin, args: [cliJs, 'mcp'] };
|
|
48
|
+
}
|
|
49
|
+
// 2. Fallback: npx (relies on PATH, may resolve wrong version)
|
|
50
|
+
return { command: 'npx', args: ['brainclaw', 'mcp'] };
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Trace from the brainclaw bin/shim to the actual dist/cli.js file.
|
|
54
|
+
* Works on Windows (.cmd shim), macOS/Linux (symlink to bin stub).
|
|
55
|
+
*/
|
|
56
|
+
function resolveBrainclawCliJs() {
|
|
57
|
+
// Strategy A: find via which/where and trace to cli.js
|
|
58
|
+
const whichCmd = os.platform() === 'win32' ? 'where' : 'which';
|
|
59
|
+
try {
|
|
60
|
+
const result = spawnSync(whichCmd, ['brainclaw'], { encoding: 'utf-8', timeout: 3000 });
|
|
61
|
+
if (result.status === 0) {
|
|
62
|
+
const resolved = result.stdout.trim().split(/\r?\n/)[0]?.trim();
|
|
63
|
+
if (resolved) {
|
|
64
|
+
const cliJs = traceToCliJs(resolved);
|
|
65
|
+
if (cliJs)
|
|
66
|
+
return cliJs;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// Non-fatal — try next strategy
|
|
72
|
+
}
|
|
73
|
+
// Strategy B: resolve from this file's own package (we ARE brainclaw)
|
|
74
|
+
try {
|
|
75
|
+
const ownCliJs = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'cli.js');
|
|
76
|
+
if (fs.existsSync(ownCliJs))
|
|
77
|
+
return ownCliJs;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// Non-fatal
|
|
81
|
+
}
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Given a bin path (shim or symlink), trace to the dist/cli.js entry point.
|
|
86
|
+
*
|
|
87
|
+
* Windows: .cmd shim contains a line like `"%_prog%" "%dp0%\node_modules\brainclaw\dist\cli.js" %*`
|
|
88
|
+
* Unix: bin is a symlink → resolve to real path → go up to package root → dist/cli.js
|
|
89
|
+
*/
|
|
90
|
+
function traceToCliJs(binPath) {
|
|
91
|
+
const isWindows = os.platform() === 'win32';
|
|
92
|
+
if (isWindows) {
|
|
93
|
+
// Read the .cmd shim and extract the cli.js path
|
|
94
|
+
const cmdPath = binPath.endsWith('.cmd') ? binPath : `${binPath}.cmd`;
|
|
95
|
+
try {
|
|
96
|
+
const content = fs.readFileSync(cmdPath, 'utf-8');
|
|
97
|
+
// Match patterns like: "%dp0%\node_modules\brainclaw\dist\cli.js"
|
|
98
|
+
const match = content.match(/%dp0%\\([^\s"]+cli\.js)/);
|
|
99
|
+
if (match) {
|
|
100
|
+
const shimDir = path.dirname(cmdPath);
|
|
101
|
+
const cliJs = path.resolve(shimDir, match[1]);
|
|
102
|
+
if (fs.existsSync(cliJs))
|
|
103
|
+
return cliJs;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// Fall through
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
// Unix: follow symlink chain to the real bin, then find cli.js
|
|
112
|
+
try {
|
|
113
|
+
const realBin = fs.realpathSync(binPath);
|
|
114
|
+
// Typical layout: .../node_modules/.bin/brainclaw → ../brainclaw/dist/cli.js
|
|
115
|
+
// Or: .../node_modules/brainclaw/dist/cli.js (direct)
|
|
116
|
+
if (realBin.endsWith('cli.js') && fs.existsSync(realBin))
|
|
117
|
+
return realBin;
|
|
118
|
+
// The bin stub typically lives at node_modules/brainclaw/dist/cli.js
|
|
119
|
+
// or node_modules/.bin/brainclaw → ../brainclaw/dist/cli.js
|
|
120
|
+
const packageRoot = findPackageRoot(realBin);
|
|
121
|
+
if (packageRoot) {
|
|
122
|
+
const cliJs = path.join(packageRoot, 'dist', 'cli.js');
|
|
123
|
+
if (fs.existsSync(cliJs))
|
|
124
|
+
return cliJs;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// Fall through
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
/** Walk up from a file to find the nearest directory containing package.json with name "brainclaw". */
|
|
134
|
+
function findPackageRoot(from) {
|
|
135
|
+
let dir = path.dirname(from);
|
|
136
|
+
for (let i = 0; i < 10; i++) {
|
|
137
|
+
const pkgPath = path.join(dir, 'package.json');
|
|
138
|
+
try {
|
|
139
|
+
if (fs.existsSync(pkgPath)) {
|
|
140
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
141
|
+
if (pkg.name === 'brainclaw')
|
|
142
|
+
return dir;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
catch { /* continue */ }
|
|
146
|
+
const parent = path.dirname(dir);
|
|
147
|
+
if (parent === dir)
|
|
148
|
+
break;
|
|
149
|
+
dir = parent;
|
|
150
|
+
}
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
export function getBrainclawMcpCommand() {
|
|
154
|
+
if (!cachedMcpCommand) {
|
|
155
|
+
cachedMcpCommand = resolveBrainclawMcpCommand();
|
|
156
|
+
}
|
|
157
|
+
return cachedMcpCommand;
|
|
158
|
+
}
|
|
159
|
+
/** Reset the cached MCP command so it gets re-resolved on next access. */
|
|
160
|
+
export function resetMcpCommandCache() {
|
|
161
|
+
cachedMcpCommand = undefined;
|
|
162
|
+
}
|
|
163
|
+
/** Test/internal helper — read the current force-resolve state. */
|
|
164
|
+
export function isForceResolveEnabled() {
|
|
165
|
+
return _forceResolve;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Run `cb` with force-resolve mode enabled, so `brainclawMcpEntry` overwrites
|
|
169
|
+
* any existing absolute paths in user MCP configs. Always restores the prior
|
|
170
|
+
* state, even when `cb` throws.
|
|
171
|
+
*/
|
|
172
|
+
export function withForcedResolve(cb) {
|
|
173
|
+
const previous = _forceResolve;
|
|
174
|
+
_forceResolve = true;
|
|
175
|
+
try {
|
|
176
|
+
return cb();
|
|
177
|
+
}
|
|
178
|
+
finally {
|
|
179
|
+
_forceResolve = previous;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Build a complete MCP server entry with relay model env injection.
|
|
184
|
+
* Merges with the existing entry to preserve manual edits (e.g. custom command
|
|
185
|
+
* path, additional env vars, extra args). Only sets defaults for missing fields.
|
|
186
|
+
*
|
|
187
|
+
* When `workspacePath` is provided, injects BRAINCLAW_CWD into the env so
|
|
188
|
+
* the MCP server resolves the correct workspace root regardless of the IDE's
|
|
189
|
+
* process.cwd() at launch time.
|
|
190
|
+
*/
|
|
191
|
+
export function brainclawMcpEntry(agentName, existing, workspacePath) {
|
|
192
|
+
const defaults = getBrainclawMcpCommand();
|
|
193
|
+
const ex = isJsonObject(existing) ? existing : {};
|
|
194
|
+
const exEnv = isJsonObject(ex.env) ? ex.env : {};
|
|
195
|
+
// When _forceResolve is true (post-upgrade), always use newly resolved paths.
|
|
196
|
+
// Otherwise preserve existing command if it's an absolute path (manual edit).
|
|
197
|
+
// CRITICAL: once we decide to preserve the command, we MUST also preserve
|
|
198
|
+
// the args. Previously args was always overwritten, which silently clobbered
|
|
199
|
+
// manual customizations (--cwd, --debug, etc.) and broke setups on DGX.
|
|
200
|
+
// See trp#12 + pln#450.
|
|
201
|
+
const useExisting = !_forceResolve && typeof ex.command === 'string' && ex.command !== 'npx';
|
|
202
|
+
const existingArgs = Array.isArray(ex.args) ? ex.args : undefined;
|
|
203
|
+
return {
|
|
204
|
+
command: useExisting ? ex.command : defaults.command,
|
|
205
|
+
args: useExisting && existingArgs ? existingArgs : defaults.args,
|
|
206
|
+
// Merge env: preserve user-added vars, ensure BRAINCLAW_AGENT is set
|
|
207
|
+
env: {
|
|
208
|
+
...exEnv,
|
|
209
|
+
BRAINCLAW_AGENT: agentName,
|
|
210
|
+
...(workspacePath ? { BRAINCLAW_CWD: workspacePath } : {}),
|
|
211
|
+
},
|
|
212
|
+
// Preserve timeout if set
|
|
213
|
+
...(typeof ex.timeout === 'number' ? { timeout: ex.timeout } : {}),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
export function quoteShellArg(arg) {
|
|
217
|
+
if (/^[A-Za-z0-9_./:=+-]+$/.test(arg))
|
|
218
|
+
return arg;
|
|
219
|
+
return `"${arg.replace(/"/g, '\\"')}"`;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Resolve the brainclaw CLI invocation for hook configs.
|
|
223
|
+
* Returns shell-safe parts like `["<node>", "<cli.js>"]` or `["npx", "brainclaw"]`.
|
|
224
|
+
*/
|
|
225
|
+
export function getBclawCliParts() {
|
|
226
|
+
const mcpCmd = getBrainclawMcpCommand();
|
|
227
|
+
if (mcpCmd.command === 'npx')
|
|
228
|
+
return ['npx', 'brainclaw'];
|
|
229
|
+
const argsWithoutMcp = [...mcpCmd.args];
|
|
230
|
+
if (argsWithoutMcp[argsWithoutMcp.length - 1] === 'mcp') {
|
|
231
|
+
argsWithoutMcp.pop();
|
|
232
|
+
}
|
|
233
|
+
return [
|
|
234
|
+
mcpCmd.command.replace(/\\/g, '/'),
|
|
235
|
+
...argsWithoutMcp.map((arg) => arg.replace(/\\/g, '/')),
|
|
236
|
+
];
|
|
237
|
+
}
|
|
238
|
+
export function buildHookCommand(args, shell = os.platform() === 'win32' ? 'powershell' : 'bash') {
|
|
239
|
+
const rendered = [...getBclawCliParts(), ...args].map(quoteShellArg).join(' ');
|
|
240
|
+
if (shell === 'powershell') {
|
|
241
|
+
return `& ${rendered} 2>$null`;
|
|
242
|
+
}
|
|
243
|
+
return `${rendered} 2>/dev/null`;
|
|
244
|
+
}
|
|
245
|
+
//# sourceMappingURL=mcp-command-resolution.js.map
|
|
@@ -17,7 +17,7 @@ import fs from 'node:fs';
|
|
|
17
17
|
import path from 'node:path';
|
|
18
18
|
import { normalise, similarity } from './duplicates.js';
|
|
19
19
|
import { resolveEntityDir } from './io.js';
|
|
20
|
-
import { loadState,
|
|
20
|
+
import { loadState, persistState } from './state.js';
|
|
21
21
|
import { mutate } from './mutation-pipeline.js';
|
|
22
22
|
import { logger } from './logger.js';
|
|
23
23
|
const DEFAULT_SIMILARITY_THRESHOLD = 0.55;
|
|
@@ -118,7 +118,9 @@ export function analyzeAndApply(options = {}) {
|
|
|
118
118
|
archivedCount = applied.archivedCount;
|
|
119
119
|
mergedClusters = applied.mergedClusters;
|
|
120
120
|
staleArchived = applied.staleArchived;
|
|
121
|
-
|
|
121
|
+
// deleteMissing: archived items must have their live files unlinked; the RMW
|
|
122
|
+
// is atomic (loadState above runs under this mutate() lock).
|
|
123
|
+
persistState(state, cwd, { writeProjectMarkdown: false, deleteMissing: true });
|
|
122
124
|
});
|
|
123
125
|
return { report, result: { archivedCount, mergedClusters, staleArchived } };
|
|
124
126
|
}
|
|
@@ -138,7 +140,7 @@ export function applyCompaction(report, options = {}) {
|
|
|
138
140
|
archivedCount = applied.archivedCount;
|
|
139
141
|
mergedClusters = applied.mergedClusters;
|
|
140
142
|
staleArchived = applied.staleArchived;
|
|
141
|
-
|
|
143
|
+
persistState(state, cwd, { writeProjectMarkdown: false, deleteMissing: true });
|
|
142
144
|
});
|
|
143
145
|
return { archivedCount, mergedClusters, staleArchived };
|
|
144
146
|
}
|