brainclaw 1.10.2 → 1.11.1
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 +21 -0
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/cli.js +17 -1
- package/dist/commands/code-map.js +17 -2
- package/dist/commands/context-diff.js +31 -0
- package/dist/commands/doctor.js +23 -1
- package/dist/commands/mcp.js +48 -6
- package/dist/commands/move.js +35 -0
- package/dist/commands/session-end.js +8 -1
- package/dist/commands/session-start.js +8 -1
- package/dist/commands/setup.js +80 -22
- package/dist/core/agent-files.js +93 -3
- package/dist/core/agent-registry.js +16 -1
- package/dist/core/code-map/backend.js +76 -15
- package/dist/core/code-map/cascade.js +116 -0
- package/dist/core/code-map/refresh.js +0 -0
- package/dist/core/hook-log.js +43 -0
- package/dist/core/instruction-templates.js +1 -1
- package/dist/core/loops/store.js +31 -1
- package/dist/core/operations/relocate.js +130 -0
- package/dist/core/store-resolution.js +52 -8
- package/dist/core/worktree.js +86 -0
- package/dist/facts.js +7 -6
- package/dist/facts.json +6 -5
- package/docs/cli.md +26 -4
- package/docs/code-map.md +28 -4
- package/docs/concepts/dispatch-lifecycle.md +26 -0
- package/docs/integrations/mcp.md +2 -0
- package/docs/mcp-schema-changelog.md +27 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -417,6 +417,27 @@ npm run test:coverage # with coverage report
|
|
|
417
417
|
|
|
418
418
|
For older releases (v0.x and the early v1.0 launch series), `git log` on `master` is the source of truth — every release commit follows the `chore(release): bump version to <semver>` convention, and the matching feature/fix commits reference their plan id (e.g. `feat(mcp): self-heal ... (pln#478)`).
|
|
419
419
|
|
|
420
|
+
### v1.11.1
|
|
421
|
+
|
|
422
|
+
Agent-identity & session-hook resilience, from a fresh-CLI dogfood on a monorepo:
|
|
423
|
+
|
|
424
|
+
- **Session hooks no longer spam `UserPromptSubmit hook error` on every prompt.** Root cause was an agent-identity error whose own remediation (`register-agent --set-current`) the resolver ignored, swallowed by `2>/dev/null`. Now: the error hint points at what actually works (`--agent` / `$BRAINCLAW_AGENT_NAME`), a **single registered agent auto-resolves** with no env signal, and `session-start`/`context-diff`/`session-end` run with `--hook` so they degrade to exit 0 + `~/.brainclaw/hook.log` instead of erroring the prompt loop. Multi-agent safety (pln#562) is unchanged.
|
|
425
|
+
- **`brainclaw doctor --fix-hooks`** purges stale/broken/duplicate brainclaw hooks across all Claude Code settings scopes (user + cwd) and rewrites the canonical ones — for the broken hooks that accumulate where `setup`'s git-repo discovery never reached.
|
|
426
|
+
- **`setup` repo discovery recurses** (bounded, skipping `node_modules`/build dirs) so repos nested deep in a workspace are found instead of silently missed, and it registers the detected agent so the hooks it installs can resolve an identity.
|
|
427
|
+
|
|
428
|
+
### v1.11.0
|
|
429
|
+
|
|
430
|
+
Monorepo project-resolution + Code Map, cross-project relocation, and dispatch-worktree hygiene — from a multi-project (monorepo) dogfood:
|
|
431
|
+
|
|
432
|
+
- **`bclaw_switch` to the monorepo root now works, and a switch is honored everywhere.** An agent inside a child project could not switch *up* to the workspace-root project, and a session-scoped switch was silently lost (sessions are stored per-cwd, but the resolver read them only at the workspace anchor). The resolver now matches the root project by `project_name`/`project_id` and probes the anchor / physical cwd / workspace root for the session, so `bclaw_switch` is authoritative for every subsequent call — including Code Map — without `--cwd`.
|
|
433
|
+
- **`code-map refresh --cascade`** — monorepo-native, per-project indexing: one command at the root refreshes **every** nested project into its own store plus a child-scoped root store, with **zero double-indexing** (even under nesting). `code-map status --cascade` adds a per-child recap. Opt-in; single-project repos are unchanged.
|
|
434
|
+
- **`brainclaw move <entity> <id> --to <project>` / `bclaw_move`** — id-preserving cross-project relocation, closing the gap the switch bug exposed (items created in the wrong store). Execution-local entities (claim/assignment/agent_run/session) stay put; refuses id collisions and active-claim moves; audits both stores.
|
|
435
|
+
- **Sub-agent worktrees auto-clean on loop close** — a completed review/dispatch loop garbage-collects its worktrees (junction-safe; keeps any that are alive, un-harvested, or carry unmerged commits). Opt out with `BRAINCLAW_NO_WORKTREE_GC=1`.
|
|
436
|
+
|
|
437
|
+
### v1.10.2
|
|
438
|
+
|
|
439
|
+
Dispatch worktree-creation hardening (parallel-lane dispatch on a large repo): the claim-scope branch slug is length-capped *before* its trailing-dot strip (a scope ending in `…Page.astro` no longer truncates to an invalid `feat/…Page.` ref), and `git worktree add` gets a dedicated timeout (120s default; override `BRAINCLAW_WORKTREE_ADD_TIMEOUT_MS`) so a several-hundred-file checkout isn't SIGTERM-killed mid-materialize.
|
|
440
|
+
|
|
420
441
|
### v1.10.1
|
|
421
442
|
|
|
422
443
|
Code Map fast-follows from the 1.10.0 dogfood, plus a lint cleanup:
|
|
Binary file
|
package/dist/cli.js
CHANGED
|
@@ -20,6 +20,7 @@ import { runUpdatePlan } from './commands/update-plan.js';
|
|
|
20
20
|
import { runDeletePlan } from './commands/delete-plan.js';
|
|
21
21
|
import { runPlanResource } from './commands/plan-resource.js';
|
|
22
22
|
import { runCodeMap } from './commands/code-map.js';
|
|
23
|
+
import { runMove } from './commands/move.js';
|
|
23
24
|
import { runSequenceResource } from './commands/sequence.js';
|
|
24
25
|
import { runAddStep } from './commands/add-step.js';
|
|
25
26
|
import { runDeleteStep } from './commands/delete-step.js';
|
|
@@ -606,6 +607,7 @@ program
|
|
|
606
607
|
.option('--json', 'Output as JSON')
|
|
607
608
|
.option('--all', 'For refresh: enumerate all supported files (full refresh)')
|
|
608
609
|
.option('--changed', 'For refresh: only changed files (default)')
|
|
610
|
+
.option('--cascade', 'For refresh/status in a multi-project workspace: cascade across every nested project (each gets its own store; the root store is scoped to files no child owns)')
|
|
609
611
|
.option('--limit <n>', 'Max results for find/brief', (v) => parseInt(v, 10))
|
|
610
612
|
.action((subcommand, args, options) => {
|
|
611
613
|
void runCodeMap(subcommand, args, options).catch((err) => {
|
|
@@ -613,6 +615,15 @@ program
|
|
|
613
615
|
process.exit(1);
|
|
614
616
|
});
|
|
615
617
|
});
|
|
618
|
+
// --- move (cross-project relocation, pln#595) ---
|
|
619
|
+
program
|
|
620
|
+
.command('move <entity> <id>')
|
|
621
|
+
.description('Relocate a brainclaw item to another project, id-preserving (multi-project workspaces). Relocatable: plan, decision, constraint, trap, handoff, sequence.')
|
|
622
|
+
.requiredOption('--to <project>', 'Target project (name, path, or basename)')
|
|
623
|
+
.option('--from <project>', 'Source project (defaults to the current project)')
|
|
624
|
+
.option('--force', 'Move even if an active claim references the item')
|
|
625
|
+
.option('--json', 'Output as JSON')
|
|
626
|
+
.action((entity, id, options) => runMove(entity, id, options));
|
|
616
627
|
// --- list-plans ---
|
|
617
628
|
program
|
|
618
629
|
.command('list-plans')
|
|
@@ -763,6 +774,7 @@ program
|
|
|
763
774
|
.option('--json', 'Output as JSON dashboard')
|
|
764
775
|
.option('--migration-check', 'Report versioned documents that need schema migration')
|
|
765
776
|
.option('--fix-agent-ignore', 'Add missing .gitignore entries for generated local Brainclaw agent files')
|
|
777
|
+
.option('--fix-hooks', 'Purge stale/broken/duplicate brainclaw session hooks across all Claude Code settings scopes (user + cwd) and rewrite the canonical ones')
|
|
766
778
|
.option('--fix', 'Fix auto-resolvable issues (e.g. drifting MCP configs)')
|
|
767
779
|
.option('--repair', 'Rebuild dist/ when the MCP runtime is missing or stale')
|
|
768
780
|
.option('--after-migration', 'Run the v1.0 post-migration health check only (exits non-zero on any failure)')
|
|
@@ -1424,6 +1436,7 @@ program
|
|
|
1424
1436
|
.option('--model <id>', 'Model identifier (e.g. claude-sonnet-4-6)')
|
|
1425
1437
|
.option('--maintenance-mode <mode>', 'Maintenance mode: full (default) or fast')
|
|
1426
1438
|
.option('--include-context', 'Output full project context after starting session (replaces separate context call)')
|
|
1439
|
+
.option('--hook', 'Hook mode: degrade to exit 0 + ~/.brainclaw/hook.log on failure (advisory session hooks)')
|
|
1427
1440
|
.option('--json', 'Output as JSON')
|
|
1428
1441
|
.action(async (options) => {
|
|
1429
1442
|
await runSessionStart(options);
|
|
@@ -1441,6 +1454,7 @@ program
|
|
|
1441
1454
|
.option('--dispatch-review', 'When used with --reflect-handoff, auto-dispatch a code review if the handoff is reviewable')
|
|
1442
1455
|
.option('--reviewer <name>', 'Explicit reviewer to route the reflected handoff review to')
|
|
1443
1456
|
.option('--no-reflect', 'Suppress the dogfooding reflection prompt (project + your surfaces/skills/tools), shown by default')
|
|
1457
|
+
.option('--hook', 'Hook mode: degrade to exit 0 + ~/.brainclaw/hook.log on failure (advisory Stop hook)')
|
|
1444
1458
|
.option('--json', 'Output as JSON')
|
|
1445
1459
|
.action(async (options) => {
|
|
1446
1460
|
await runSessionEnd({
|
|
@@ -1451,6 +1465,7 @@ program
|
|
|
1451
1465
|
dispatchReview: options.dispatchReview,
|
|
1452
1466
|
reviewer: options.reviewer,
|
|
1453
1467
|
reflect: options.reflect,
|
|
1468
|
+
hook: options.hook,
|
|
1454
1469
|
});
|
|
1455
1470
|
});
|
|
1456
1471
|
// --- whoami ---
|
|
@@ -1742,9 +1757,10 @@ program
|
|
|
1742
1757
|
.description('Show what changed in memory since last context read, a session start, or a given timestamp')
|
|
1743
1758
|
.option('--since <date>', 'Show changes since this ISO date')
|
|
1744
1759
|
.option('--session <id>', 'Show changes since the start of this session')
|
|
1760
|
+
.option('--hook', 'Hook mode: exit 0 silently when there is no diff baseline (advisory session hooks)')
|
|
1745
1761
|
.option('--json', 'Output as JSON')
|
|
1746
1762
|
.action((options) => {
|
|
1747
|
-
runContextDiff({ since: options.since, session: options.session, json: options.json });
|
|
1763
|
+
runContextDiff({ since: options.since, session: options.session, json: options.json, hook: options.hook });
|
|
1748
1764
|
});
|
|
1749
1765
|
program
|
|
1750
1766
|
.command('capability <subcommand> [args...]')
|
|
@@ -22,13 +22,13 @@ export async function runCodeMap(subcommand, args, options = {}) {
|
|
|
22
22
|
const be = backend();
|
|
23
23
|
const cwd = options.cwd;
|
|
24
24
|
if (normalized === 'status') {
|
|
25
|
-
const status = await be.status({ cwd });
|
|
25
|
+
const status = await be.status({ cwd, cascade: options.cascade });
|
|
26
26
|
printStatus(status, options);
|
|
27
27
|
return;
|
|
28
28
|
}
|
|
29
29
|
if (normalized === 'refresh') {
|
|
30
30
|
const scope = options.all ? 'all' : 'changed';
|
|
31
|
-
const result = await be.refresh({ scope, cwd });
|
|
31
|
+
const result = await be.refresh({ scope, cwd, cascade: options.cascade });
|
|
32
32
|
printRefresh(result, options);
|
|
33
33
|
return;
|
|
34
34
|
}
|
|
@@ -74,6 +74,13 @@ function printStatus(status, options) {
|
|
|
74
74
|
else {
|
|
75
75
|
console.log(' Stats: (none — index not built)');
|
|
76
76
|
}
|
|
77
|
+
if (status.cascade) {
|
|
78
|
+
console.log(` Workspace: ${status.cascade.indexed_children}/${status.cascade.total_children} child project(s) indexed`);
|
|
79
|
+
for (const child of status.cascade.children) {
|
|
80
|
+
const files = child.files_indexed === null ? '' : ` (${child.files_indexed} files)`;
|
|
81
|
+
console.log(` [${child.freshness}] ${child.path}${files}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
77
84
|
}
|
|
78
85
|
function printRefresh(result, options) {
|
|
79
86
|
if (options.json) {
|
|
@@ -86,6 +93,14 @@ function printRefresh(result, options) {
|
|
|
86
93
|
if (result.lock_status)
|
|
87
94
|
console.log(` Status: ${result.lock_status}`);
|
|
88
95
|
console.log(` ${badgeLine(result.freshness_badge)}`);
|
|
96
|
+
if (result.cascade) {
|
|
97
|
+
const c = result.cascade;
|
|
98
|
+
console.log(` Cascade: ${c.children_refreshed} child project(s) + root (scoped)`);
|
|
99
|
+
console.log(` [root] . — ${c.root_result.files_parsed} files parsed`);
|
|
100
|
+
for (const child of c.children) {
|
|
101
|
+
console.log(` ${child.path} — ${child.files_parsed} files parsed (${child.freshness})`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
89
104
|
}
|
|
90
105
|
function printFind(result, options) {
|
|
91
106
|
if (options.json) {
|
|
@@ -4,18 +4,45 @@ import { loadInstructions, resolveInstructions } from '../core/instructions.js';
|
|
|
4
4
|
import { memoryExists } from '../core/io.js';
|
|
5
5
|
import { loadState } from '../core/state.js';
|
|
6
6
|
import { isTrapActive } from '../core/traps.js';
|
|
7
|
+
import { logHookDiagnostic } from '../core/hook-log.js';
|
|
7
8
|
/**
|
|
8
9
|
* Hybrid context-diff: always includes critical anchors (active claims,
|
|
9
10
|
* top traps, instructions) so the agent stays grounded, plus the memory
|
|
10
11
|
* delta since last context read.
|
|
11
12
|
*/
|
|
12
13
|
export function runContextDiff(options = {}) {
|
|
14
|
+
try {
|
|
15
|
+
contextDiffBody(options);
|
|
16
|
+
}
|
|
17
|
+
catch (e) {
|
|
18
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
19
|
+
if (options.hook) {
|
|
20
|
+
// Advisory hook (trp#917): any failure past the early guards — e.g. a
|
|
21
|
+
// corrupt session snapshot / claim / instruction file feeding buildContextDiff
|
|
22
|
+
// or buildCriticalAnchors — must NOT error the prompt loop. Log + exit 0.
|
|
23
|
+
logHookDiagnostic(`context-diff skipped: ${message}`);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
console.error(`Error: ${message}`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function contextDiffBody(options) {
|
|
13
31
|
if (!memoryExists(options.cwd)) {
|
|
32
|
+
if (options.hook) {
|
|
33
|
+
logHookDiagnostic('context-diff skipped: .brainclaw/ not found');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
14
36
|
console.error('Error: .brainclaw/ not found. Run `brainclaw init` first.');
|
|
15
37
|
process.exit(1);
|
|
16
38
|
}
|
|
17
39
|
const resolved = resolveContextDiffSince(options);
|
|
18
40
|
if (!resolved.since) {
|
|
41
|
+
if (options.hook) {
|
|
42
|
+
// No diff baseline yet (e.g. first prompt before a session marker exists).
|
|
43
|
+
// Nothing to surface — exit 0 silently so the hook never errors (trp#917).
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
19
46
|
if (options.session) {
|
|
20
47
|
console.error(`Error: session '${options.session}' not found in session snapshots or audit log.`);
|
|
21
48
|
process.exit(1);
|
|
@@ -25,6 +52,10 @@ export function runContextDiff(options = {}) {
|
|
|
25
52
|
}
|
|
26
53
|
const diff = buildContextDiff({ ...options, includeItems: true });
|
|
27
54
|
if (!diff) {
|
|
55
|
+
if (options.hook) {
|
|
56
|
+
logHookDiagnostic('context-diff skipped: unable to build diff');
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
28
59
|
console.error('Error: unable to build context diff.');
|
|
29
60
|
process.exit(1);
|
|
30
61
|
}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
3
4
|
import path from 'node:path';
|
|
4
5
|
import * as childProcess from 'node:child_process';
|
|
5
6
|
import { reconcileAllOpenRuns } from '../core/agentrun-reconciler.js';
|
|
@@ -35,7 +36,7 @@ import { assessBrainclawVersion, detectConcurrentInstallations } from '../core/b
|
|
|
35
36
|
import { resolveStoreChain } from '../core/store-resolution.js';
|
|
36
37
|
import { listWorktrees, detectSharedCheckoutRisk } from '../core/worktree.js';
|
|
37
38
|
import { resolveCrossProjectLinks, detectCrossProjectCycles } from '../core/cross-project.js';
|
|
38
|
-
import { auditLocalAgentWorkspaceFiles, ensureGitignoreEntries, patchAllMcpConfigs } from '../core/agent-files.js';
|
|
39
|
+
import { auditLocalAgentWorkspaceFiles, ensureGitignoreEntries, patchAllMcpConfigs, fixClaudeCodeHooksAllScopes } from '../core/agent-files.js';
|
|
39
40
|
import { summarizeWorkspaceProjects } from '../core/workspace-projects.js';
|
|
40
41
|
import { detectStaleness, staleSummary } from '../core/staleness.js';
|
|
41
42
|
import { InboxMessageSchema } from '../core/schema.js';
|
|
@@ -654,6 +655,27 @@ export function runDoctor(options = {}) {
|
|
|
654
655
|
process.exit(report.exit_code);
|
|
655
656
|
return;
|
|
656
657
|
}
|
|
658
|
+
if (options.fixHooks) {
|
|
659
|
+
const cwd = options.cwd ?? process.cwd();
|
|
660
|
+
const results = fixClaudeCodeHooksAllScopes(cwd, os.homedir());
|
|
661
|
+
const fixed = results.filter((r) => r.changed);
|
|
662
|
+
if (options.json) {
|
|
663
|
+
console.log(JSON.stringify({ ok: true, action: 'fix-hooks', results }, null, 2));
|
|
664
|
+
}
|
|
665
|
+
else if (fixed.length === 0) {
|
|
666
|
+
console.log('✔ No stale/broken brainclaw session hooks found across Claude Code settings scopes.');
|
|
667
|
+
}
|
|
668
|
+
else {
|
|
669
|
+
for (const r of fixed) {
|
|
670
|
+
const suffix = r.collapsed > 0
|
|
671
|
+
? ` (collapsed ${r.collapsed} stale/duplicate entr${r.collapsed === 1 ? 'y' : 'ies'})`
|
|
672
|
+
: '';
|
|
673
|
+
console.log(`✔ Sanitized brainclaw session hooks in ${r.filePath}${suffix}`);
|
|
674
|
+
}
|
|
675
|
+
console.log(' → Restart your Claude Code session to pick up the canonical hooks.');
|
|
676
|
+
}
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
657
679
|
if (options.repair) {
|
|
658
680
|
try {
|
|
659
681
|
const result = repairDistRuntime(options);
|
package/dist/commands/mcp.js
CHANGED
|
@@ -17,6 +17,7 @@ import { generateIdWithLabel } from '../core/ids.js';
|
|
|
17
17
|
import { memoryExists } from '../core/io.js';
|
|
18
18
|
import { generateCandidateIdWithLabel, loadCandidate, saveCandidate } from '../core/candidates.js';
|
|
19
19
|
import { createEntity, getEntity, listEntities, boundListResult, DEFAULT_FIND_CHAR_BUDGET, removeEntity, transitionEntity, updateEntity, } from '../core/entity-operations.js';
|
|
20
|
+
import { relocateEntity } from '../core/operations/relocate.js';
|
|
20
21
|
import { handoffDiffPreviewNote } from '../core/handoff-snapshot.js';
|
|
21
22
|
import { ENTITY_REGISTRY } from '../core/entity-registry.js';
|
|
22
23
|
import { generateClaimId, listClaims, loadClaim, saveClaim, createCoordinatorClaim, adoptClaimSession, attachAssignmentMessageToClaim, linkClaimToAssignment, releaseClaimWithCascade } from '../core/claims.js';
|
|
@@ -452,11 +453,13 @@ export const MCP_READ_TOOLS = [
|
|
|
452
453
|
},
|
|
453
454
|
{
|
|
454
455
|
name: 'bclaw_code_status',
|
|
455
|
-
description: 'Code Map status for this project: store presence, freshness badge (fresh / stale_changed_files / stale_extractor / stale_grammar / stale_git_head / partial / missing_index), and index stats (files, nodes, edges). Read-only; never refreshes. Pair with bclaw_code_refresh when freshness is missing_index or stale.',
|
|
456
|
+
description: 'Code Map status for this project: store presence, freshness badge (fresh / stale_changed_files / stale_extractor / stale_grammar / stale_git_head / partial / missing_index), and index stats (files, nodes, edges). Read-only; never refreshes. Pair with bclaw_code_refresh when freshness is missing_index or stale. In a multi-project workspace, cascade=true adds a per-child recap (which nested projects have a built index vs missing_index).',
|
|
456
457
|
annotations: { tier: 'standard', category: 'discovery', headlessApproval: 'auto' },
|
|
457
458
|
inputSchema: {
|
|
458
459
|
type: 'object',
|
|
459
|
-
properties: {
|
|
460
|
+
properties: {
|
|
461
|
+
cascade: { type: 'boolean', description: 'Multi-project workspace recap: also report per-child store presence + freshness for every nested project. No-op outside a multi-project workspace.' },
|
|
462
|
+
},
|
|
460
463
|
},
|
|
461
464
|
},
|
|
462
465
|
{
|
|
@@ -489,12 +492,13 @@ export const MCP_READ_TOOLS = [
|
|
|
489
492
|
const MCP_WRITE_TOOLS = [
|
|
490
493
|
{
|
|
491
494
|
name: 'bclaw_code_refresh',
|
|
492
|
-
description: 'Rebuild the Code Map index for this project (Tree-sitter parse + shards + indexes, behind the per-project lock). scope="changed" (default) reparses changed files; scope="all" does a full refresh + compaction. A live competing lock fails fast with a clear status — refresh never blocks. Returns the resulting freshness_badge.',
|
|
495
|
+
description: 'Rebuild the Code Map index for this project (Tree-sitter parse + shards + indexes, behind the per-project lock). scope="changed" (default) reparses changed files; scope="all" does a full refresh + compaction. A live competing lock fails fast with a clear status — refresh never blocks. Returns the resulting freshness_badge. In a multi-project workspace, cascade=true refreshes EVERY nested project into its own store + the root store scoped to files no child owns (zero double-indexing) — so one call at the root indexes the whole monorepo per-project.',
|
|
493
496
|
annotations: { tier: 'standard', category: 'discovery', headlessApproval: 'prompt' },
|
|
494
497
|
inputSchema: {
|
|
495
498
|
type: 'object',
|
|
496
499
|
properties: {
|
|
497
500
|
scope: { type: 'string', enum: ['changed', 'all'], description: 'changed (default) reparses changed files only; all does a full refresh with orphan compaction.' },
|
|
501
|
+
cascade: { type: 'boolean', description: 'Multi-project cascade: refresh every nested brainclaw project + a child-scoped root store. No-op outside a multi-project workspace.' },
|
|
498
502
|
},
|
|
499
503
|
},
|
|
500
504
|
},
|
|
@@ -1265,6 +1269,22 @@ const MCP_WRITE_TOOLS = [
|
|
|
1265
1269
|
required: ['entity', 'id', 'to'],
|
|
1266
1270
|
},
|
|
1267
1271
|
},
|
|
1272
|
+
{
|
|
1273
|
+
name: 'bclaw_move',
|
|
1274
|
+
description: 'Relocate a brainclaw item to another project in a multi-project workspace, PRESERVING its id (so pln#/dec# references stay stable). Relocatable entities: plan, decision, constraint, trap, handoff, sequence. Execution-local entities (claim, assignment, agent_run, session) are NOT relocatable — they stay in the project where the work ran. Refuses on id collision in the target, a missing source, or an active claim on the item (unless force). Audits both stores.',
|
|
1275
|
+
annotations: { tier: 'standard', category: 'memory', headlessApproval: 'prompt' },
|
|
1276
|
+
inputSchema: {
|
|
1277
|
+
type: 'object',
|
|
1278
|
+
properties: {
|
|
1279
|
+
entity: { type: 'string', description: 'Entity name (plan, decision, constraint, trap, handoff, sequence).' },
|
|
1280
|
+
id: { type: 'string', description: 'Entity id to move.' },
|
|
1281
|
+
to_project: { type: 'string', description: 'Target project: name, path, or basename.' },
|
|
1282
|
+
from_project: { type: 'string', description: 'Source project (defaults to the current project).' },
|
|
1283
|
+
force: { type: 'boolean', description: 'Move even if an active claim references the item. Default false.' },
|
|
1284
|
+
},
|
|
1285
|
+
required: ['entity', 'id', 'to_project'],
|
|
1286
|
+
},
|
|
1287
|
+
},
|
|
1268
1288
|
];
|
|
1269
1289
|
/**
|
|
1270
1290
|
* Combined catalog of every brainclaw MCP tool descriptor (read + write).
|
|
@@ -2653,7 +2673,7 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
2653
2673
|
const { JsonlBackend } = await import('../core/code-map/backend.js');
|
|
2654
2674
|
const be = new JsonlBackend();
|
|
2655
2675
|
if (name === 'bclaw_code_status') {
|
|
2656
|
-
const status = await be.status({ cwd });
|
|
2676
|
+
const status = await be.status({ cwd, cascade: args.cascade === true });
|
|
2657
2677
|
return {
|
|
2658
2678
|
response: toolResponse({
|
|
2659
2679
|
content: [{ type: 'text', text: `Code Map: ${status.store_exists ? 'store present' : 'no store'} — freshness=${status.freshness_badge.status}` }],
|
|
@@ -2663,10 +2683,11 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
2663
2683
|
}
|
|
2664
2684
|
if (name === 'bclaw_code_refresh') {
|
|
2665
2685
|
const scope = args.scope === 'all' ? 'all' : 'changed';
|
|
2666
|
-
const result = await be.refresh({ scope, cwd });
|
|
2686
|
+
const result = await be.refresh({ scope, cwd, cascade: args.cascade === true });
|
|
2687
|
+
const cascadeNote = result.cascade ? ` cascade=${result.cascade.children_refreshed} child(ren)+root` : '';
|
|
2667
2688
|
return {
|
|
2668
2689
|
response: toolResponse({
|
|
2669
|
-
content: [{ type: 'text', text: `Code Map refresh [${result.scope}]: ran=${result.ran} freshness=${result.freshness_badge.status}${result.lock_status ? ` (${result.lock_status})` : ''}` }],
|
|
2690
|
+
content: [{ type: 'text', text: `Code Map refresh [${result.scope}]: ran=${result.ran} freshness=${result.freshness_badge.status}${cascadeNote}${result.lock_status ? ` (${result.lock_status})` : ''}` }],
|
|
2670
2691
|
structuredContent: { ...result, freshness_badge: result.freshness_badge },
|
|
2671
2692
|
}),
|
|
2672
2693
|
};
|
|
@@ -6619,6 +6640,27 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
6619
6640
|
return { response: createToolErrorResponse('validation_error', error.message) };
|
|
6620
6641
|
}
|
|
6621
6642
|
}
|
|
6643
|
+
if (name === 'bclaw_move') {
|
|
6644
|
+
try {
|
|
6645
|
+
const entity = String(args.entity ?? '');
|
|
6646
|
+
const id = String(args.id ?? '');
|
|
6647
|
+
const toProject = String(args.to_project ?? '');
|
|
6648
|
+
const fromProject = typeof args.from_project === 'string' ? args.from_project : undefined;
|
|
6649
|
+
const force = args.force === true;
|
|
6650
|
+
const { agent_name, agent_id } = resolveCanonicalAuthor(args, cwd, connectionSessionId);
|
|
6651
|
+
const result = relocateEntity({ entity, id, toProject, fromProject, force, cwd, actor: agent_name, actorId: agent_id });
|
|
6652
|
+
const warn = result.warnings.length ? ` (${result.warnings.length} warning(s))` : '';
|
|
6653
|
+
return {
|
|
6654
|
+
response: toolResponse({
|
|
6655
|
+
content: [{ type: 'text', text: `✔ moved ${entity} ${id} → ${result.to}${warn}` }],
|
|
6656
|
+
structuredContent: { ...result },
|
|
6657
|
+
}),
|
|
6658
|
+
};
|
|
6659
|
+
}
|
|
6660
|
+
catch (error) {
|
|
6661
|
+
return { response: createToolErrorResponse('validation_error', error.message) };
|
|
6662
|
+
}
|
|
6663
|
+
}
|
|
6622
6664
|
if (name === 'bclaw_transition') {
|
|
6623
6665
|
try {
|
|
6624
6666
|
const entity = String(args.entity ?? '');
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `brainclaw move <entity> <id> --to <project>` — id-preserving cross-project
|
|
3
|
+
* relocation (pln#595). Thin CLI adapter over core relocateEntity().
|
|
4
|
+
*/
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import { relocateEntity } from '../core/operations/relocate.js';
|
|
7
|
+
export function runMove(entity, id, options) {
|
|
8
|
+
if (!options.to) {
|
|
9
|
+
console.error('Error: --to <project> is required.');
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
const result = relocateEntity({
|
|
14
|
+
entity: entity,
|
|
15
|
+
id,
|
|
16
|
+
toProject: options.to,
|
|
17
|
+
fromProject: options.from,
|
|
18
|
+
force: options.force,
|
|
19
|
+
cwd: options.cwd ?? process.cwd(),
|
|
20
|
+
actor: process.env.BRAINCLAW_AGENT_NAME || os.userInfo().username || 'unknown',
|
|
21
|
+
});
|
|
22
|
+
if (options.json) {
|
|
23
|
+
console.log(JSON.stringify(result, null, 2));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
console.log(`✔ Moved ${result.entity} ${result.id} (${result.subdir}) → ${result.to}`);
|
|
27
|
+
for (const w of result.warnings)
|
|
28
|
+
console.log(` ⚠ ${w}`);
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
console.error(`Error: ${err.message}`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=move.js.map
|
|
@@ -39,6 +39,7 @@ import { extractFilesFromDiff } from '../commands/handoff.js';
|
|
|
39
39
|
import { capHandoffDiff } from '../core/handoff-snapshot.js';
|
|
40
40
|
import { suggestCompaction } from '../core/memory-compactor.js';
|
|
41
41
|
import { dispatchReview } from '../core/dispatcher.js';
|
|
42
|
+
import { logHookDiagnostic } from '../core/hook-log.js';
|
|
42
43
|
export const REFLECTION_QUESTIONS = [
|
|
43
44
|
'Dogfooding the project — using brainclaw to do real work this session, what friction did you hit (slow reads, confusing surfaces, missing affordances, awkward workflows)? What concrete change to the project would have removed it?',
|
|
44
45
|
'Your surfaces, skills & tools — did your generated surface files (CLAUDE.md / agent surface), skills (SKILL.md), or tools (MCP / CLI) help or get in the way? Name at least one concrete edit that would make them serve you better next time.',
|
|
@@ -113,7 +114,13 @@ export async function runSessionEnd(options = {}) {
|
|
|
113
114
|
}
|
|
114
115
|
}
|
|
115
116
|
catch (e) {
|
|
116
|
-
|
|
117
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
118
|
+
if (options.hook) {
|
|
119
|
+
// Advisory Stop hook (trp#917): never fail the prompt loop. Log + exit 0.
|
|
120
|
+
logHookDiagnostic(`session-end skipped: ${message}`);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
console.error(`Error: ${message}`);
|
|
117
124
|
process.exit(1);
|
|
118
125
|
}
|
|
119
126
|
}
|
|
@@ -11,6 +11,7 @@ import { writeContextMarker } from '../core/freshness.js';
|
|
|
11
11
|
import { saveRuntimeNote, generateRuntimeNoteId } from '../core/runtime.js';
|
|
12
12
|
import { nowISO, generateId } from '../core/ids.js';
|
|
13
13
|
import { appendAuditEntry } from '../core/audit.js';
|
|
14
|
+
import { logHookDiagnostic } from '../core/hook-log.js';
|
|
14
15
|
import { releaseStaleClaimsFromOtherAgents } from '../core/claims.js';
|
|
15
16
|
import { SessionSnapshotSchema } from '../core/schema.js';
|
|
16
17
|
import { auditLocalAgentWorkspaceFiles } from '../core/agent-files.js';
|
|
@@ -93,7 +94,13 @@ export async function runSessionStart(options = {}) {
|
|
|
93
94
|
}
|
|
94
95
|
}
|
|
95
96
|
catch (e) {
|
|
96
|
-
|
|
97
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
98
|
+
if (options.hook) {
|
|
99
|
+
// Advisory hook (trp#917): never fail the prompt loop. Log + exit 0.
|
|
100
|
+
logHookDiagnostic(`session-start skipped: ${message}`);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
console.error(`Error: ${message}`);
|
|
97
104
|
process.exit(1);
|
|
98
105
|
}
|
|
99
106
|
}
|
package/dist/commands/setup.js
CHANGED
|
@@ -12,6 +12,7 @@ import { ensureClaudeCodeUserSettings, ensureClaudeCodeUserCommand, ensureCursor
|
|
|
12
12
|
import { MEMORY_DIR, memoryExists } from '../core/io.js';
|
|
13
13
|
import { ensureUserStore, readSetupState, resolveHomeDir, writeSetupState } from '../core/setup-state.js';
|
|
14
14
|
import { writeDetectedAgentHooks } from './hooks.js';
|
|
15
|
+
import { normalizeAgentName, findAgentIdentityByName, registerAgentIdentity } from '../core/agent-registry.js';
|
|
15
16
|
export { readSetupState } from '../core/setup-state.js';
|
|
16
17
|
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
17
18
|
export const ALL_KNOWN_AGENTS = [
|
|
@@ -83,37 +84,59 @@ export function parseRoots(input, env = process.env) {
|
|
|
83
84
|
return results;
|
|
84
85
|
}
|
|
85
86
|
// ─── Step 2: Scan repos ───────────────────────────────────────────────────────
|
|
86
|
-
|
|
87
|
+
/** Heavy / generated directories never worth descending into when hunting repos. */
|
|
88
|
+
const SCAN_SKIP_DIRS = new Set([
|
|
89
|
+
'node_modules', '.git', 'dist', 'dist-test', 'build', 'out', '.next',
|
|
90
|
+
'coverage', 'vendor', '.venv', 'venv', '__pycache__', '.cache', 'target', '.gradle',
|
|
91
|
+
]);
|
|
92
|
+
/**
|
|
93
|
+
* Discover git repositories under each root, recursing up to `maxDepth` levels.
|
|
94
|
+
*
|
|
95
|
+
* trp#918: the previous scan was depth-1 only (root + immediate children), so in
|
|
96
|
+
* a workspace like /srv with repos nested at /srv/dev/repos/global/<svc> it found
|
|
97
|
+
* just the shallow ones and silently missed the rest. This walk descends (skipping
|
|
98
|
+
* heavy/build/hidden dirs, bounded by `maxDepth`) and surfaces every directory
|
|
99
|
+
* that contains a `.git`. It keeps descending past a found repo because a
|
|
100
|
+
* workspace repo can legitimately contain independent child repos (the prior
|
|
101
|
+
* depth-1 scan already surfaced depth-1 children); the user selects which to init.
|
|
102
|
+
*/
|
|
103
|
+
export function scanGitRepos(roots, maxDepth = 6) {
|
|
87
104
|
const repos = [];
|
|
88
105
|
const seen = new Set();
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
candidates.push(path.join(root, entry.name));
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
catch {
|
|
100
|
-
// skip unreadable dirs
|
|
101
|
-
}
|
|
102
|
-
for (const candidate of candidates) {
|
|
103
|
-
const repoPath = path.resolve(candidate);
|
|
104
|
-
if (seen.has(repoPath))
|
|
105
|
-
continue;
|
|
106
|
-
if (isBrainclawInternalPath(repoPath))
|
|
107
|
-
continue;
|
|
108
|
-
if (!fs.existsSync(path.join(repoPath, '.git')))
|
|
109
|
-
continue;
|
|
106
|
+
const walk = (dir, depth) => {
|
|
107
|
+
const repoPath = path.resolve(dir);
|
|
108
|
+
if (seen.has(repoPath))
|
|
109
|
+
return;
|
|
110
|
+
if (isBrainclawInternalPath(repoPath))
|
|
111
|
+
return;
|
|
112
|
+
if (fs.existsSync(path.join(repoPath, '.git'))) {
|
|
110
113
|
seen.add(repoPath);
|
|
111
114
|
repos.push({
|
|
112
115
|
path: repoPath,
|
|
113
116
|
name: path.basename(repoPath) || repoPath,
|
|
114
117
|
alreadyInitialised: memoryExists(repoPath),
|
|
115
118
|
});
|
|
119
|
+
// Keep descending — a workspace repo may contain independent child repos.
|
|
120
|
+
}
|
|
121
|
+
if (depth >= maxDepth)
|
|
122
|
+
return;
|
|
123
|
+
let entries;
|
|
124
|
+
try {
|
|
125
|
+
entries = fs.readdirSync(repoPath, { withFileTypes: true });
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return; // unreadable dir — skip
|
|
116
129
|
}
|
|
130
|
+
for (const entry of entries) {
|
|
131
|
+
if (!entry.isDirectory())
|
|
132
|
+
continue;
|
|
133
|
+
if (SCAN_SKIP_DIRS.has(entry.name) || entry.name.startsWith('.'))
|
|
134
|
+
continue;
|
|
135
|
+
walk(path.join(repoPath, entry.name), depth + 1);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
for (const root of roots) {
|
|
139
|
+
walk(root, 0);
|
|
117
140
|
}
|
|
118
141
|
return repos;
|
|
119
142
|
}
|
|
@@ -309,6 +332,39 @@ export async function initReposAndConfigureAgents(selectedRepos, selectedAgents,
|
|
|
309
332
|
}
|
|
310
333
|
return { initialisedRepos, configActions };
|
|
311
334
|
}
|
|
335
|
+
/**
|
|
336
|
+
* Ensure a resolvable session identity exists (pln#596 / trp#917). setup installs
|
|
337
|
+
* session hooks that need an agent identity; a hook with no identity now degrades
|
|
338
|
+
* silently (exit 0, logs to ~/.brainclaw/hook.log) but won't inject context. If we
|
|
339
|
+
* detected the running agent, register it in each selected repo so the hook resolves
|
|
340
|
+
* immediately. Otherwise emit a clear, actionable note — we don't guess an agent (a
|
|
341
|
+
* wrong identity is worse than none, and the single-registered-agent fallback wants
|
|
342
|
+
* exactly one).
|
|
343
|
+
*/
|
|
344
|
+
export function ensureSessionIdentityForRepos(repoPaths, detectedName) {
|
|
345
|
+
if (detectedName) {
|
|
346
|
+
const normalized = normalizeAgentName(detectedName);
|
|
347
|
+
let registered = 0;
|
|
348
|
+
for (const repoPath of repoPaths) {
|
|
349
|
+
try {
|
|
350
|
+
if (!findAgentIdentityByName(normalized, repoPath)) {
|
|
351
|
+
registerAgentIdentity({ agentName: normalized, kind: 'agent', trustLevel: 'contributor', cwd: repoPath });
|
|
352
|
+
registered++;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
/* best-effort — identity registration must not abort setup */
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (registered > 0) {
|
|
360
|
+
console.log(` ✔ Registered session identity '${normalized}' in ${registered} repo(s) so session hooks resolve immediately.`);
|
|
361
|
+
}
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
console.log('\n⚠ No agent identity registered yet — session hooks will no-op (exit 0) until one resolves.');
|
|
365
|
+
console.log(' Set `export BRAINCLAW_AGENT_NAME=<agent>` in your shell, or run `brainclaw register-agent <name>`.');
|
|
366
|
+
console.log(' (A single registered agent then resolves automatically; running inside a detected agent auto-registers on first session.)');
|
|
367
|
+
}
|
|
312
368
|
// ─── Step 7: Reload reminder ──────────────────────────────────────────────────
|
|
313
369
|
export function printReloadReminder(detectedAgent) {
|
|
314
370
|
console.log('');
|
|
@@ -540,6 +596,8 @@ export async function runSetup(options = {}) {
|
|
|
540
596
|
// Step 6: Init repos + configure agents
|
|
541
597
|
console.log('\n→ Initialising repositories and configuring agents...');
|
|
542
598
|
const { initialisedRepos, configActions } = await initReposAndConfigureAgents(selectedRepos, selectedAgents, env);
|
|
599
|
+
// Step 6.5: ensure a resolvable session identity for the hooks we just installed.
|
|
600
|
+
ensureSessionIdentityForRepos(selectedRepos.map((r) => r.path), detectedName);
|
|
543
601
|
// Step 7: VS Code extension
|
|
544
602
|
installVscodeExtension();
|
|
545
603
|
// Save state
|