brainclaw 1.7.0 → 1.7.2
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 +36 -5
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/cli.js +12 -2
- package/dist/commands/dispatch.js +2 -0
- package/dist/commands/doctor.js +17 -0
- package/dist/commands/mcp.js +31 -7
- package/dist/commands/switch.js +40 -8
- package/dist/core/agent-capability.js +67 -0
- package/dist/core/agentrun-reconciler.js +126 -52
- package/dist/core/coordination.js +10 -9
- package/dist/core/dispatcher.js +99 -29
- package/dist/core/entity-operations.js +54 -1
- package/dist/core/execution-adapters.js +32 -51
- package/dist/core/execution.js +14 -8
- package/dist/core/identity.js +40 -13
- package/dist/core/instruction-templates.js +4 -3
- package/dist/core/runtime-signals.js +102 -0
- package/dist/core/spawn-check.js +125 -0
- package/dist/facts.js +3 -3
- package/dist/facts.json +2 -2
- package/docs/cli.md +8 -4
- package/docs/integrations/mcp.md +48 -15
- package/docs/mcp-schema-changelog.md +16 -5
- package/docs/playbooks/team/index.md +7 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -343,11 +343,42 @@ npm run test:coverage # with coverage report
|
|
|
343
343
|
|
|
344
344
|
## Changelog
|
|
345
345
|
|
|
346
|
-
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)`).
|
|
347
|
-
|
|
348
|
-
### v1.
|
|
349
|
-
|
|
350
|
-
- **
|
|
346
|
+
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)`).
|
|
347
|
+
|
|
348
|
+
### v1.7.2
|
|
349
|
+
|
|
350
|
+
- **Sequence MCP tools are agent-first by default** — sequence creation,
|
|
351
|
+
listing, update, and deletion tools are now in the default MCP catalog, with
|
|
352
|
+
explicit lane item schemas (`planId`, optional `stepId`, `rank`,
|
|
353
|
+
dependencies, lane metadata) and matching canonical CRUD validation for
|
|
354
|
+
`entity="sequence"`.
|
|
355
|
+
|
|
356
|
+
### v1.7.1
|
|
357
|
+
|
|
358
|
+
- **MCP project context isolation fix** — `bclaw_switch` now keeps MCP switches
|
|
359
|
+
session-scoped even when the agent session has to be resolved or created on
|
|
360
|
+
the fly. Session lookup honors explicit session IDs, avoids adopting another
|
|
361
|
+
live process's session, detects Codex via native `CODEX_*` runtime variables,
|
|
362
|
+
and `bclaw_switch(list=true)` reports the session active project with
|
|
363
|
+
`active_source`.
|
|
364
|
+
|
|
365
|
+
### v1.7.0
|
|
366
|
+
|
|
367
|
+
- **Dispatch reliability + scope-aware dirty guard** — evidence-first
|
|
368
|
+
`agent_run` reconciliation avoids false terminal states, `bclaw_coordinate`
|
|
369
|
+
accepts pinned refs and a scope-aware `allow_dirty` guard, and the Hermes
|
|
370
|
+
agent integration joins the supported surfaces.
|
|
371
|
+
|
|
372
|
+
### v1.6.0
|
|
373
|
+
|
|
374
|
+
- **Bootstrap loop + cross-project agent workflow** — the bootstrap ideation
|
|
375
|
+
preset can materialize `PROJECT.md`, `bclaw_init_project` initializes and links
|
|
376
|
+
arbitrary project paths, and `project=` routing reaches `bclaw_work` /
|
|
377
|
+
`bclaw_loop` for linked-project operations.
|
|
378
|
+
|
|
379
|
+
### v1.5.3
|
|
380
|
+
|
|
381
|
+
- **Cross-project canonical grammar + CLI parity** (pln#359, all phases) — the canonical grammar (`bclaw_find / get / create / update / remove / transition`), `bclaw_context`, and `bclaw_coordinate` now accept an optional `project: <name>` argument that routes the operation to a linked project. Two link kinds are recognised: `cross_project_links` (sibling/peer projects in `config.yaml`, `brainclaw link list`) and workspace store-chain children. Arbitrary directory paths are rejected — adoption requires an explicit link, which gives the user a single point of control over what an agent can reach. Identity is sourced from the caller's home registry; entity writes + audit log entries land in the target. Unknown project names throw `validation_error` with a hint listing the configured links — no silent fallback. Cross-project `bclaw_coordinate` is **inbox-only**: claim/assignment/message all land in the target, the target agent picks the brief up async via its own `bclaw_work`, and auto-spawn from the source process is force-disabled because the spawn cwd / worktree are tied to the target's git repo (a warning surfaces in `FacadeResponse.warnings`). The CLI exposes the same as a global `--project <name>` flag, mutually exclusive with `--cwd`. Refs: helper `resolveProjectCwd` in `src/core/cross-project.ts`, MCP write/read handler dispatch in `src/commands/mcp.ts` and `src/commands/mcp-read-handlers.ts`, `--project` plumbing in `src/cli.ts` preAction, surface advertisement in `src/core/instruction-templates.ts`, plus tests in `tests/unit/cross-project.test.ts` (10 unit cases on the helper), `tests/unit/bclaw-coordinate.test.ts` (4 cross-project routing cases), and `tests/cli-cross-project.test.ts` (5 e2e cases). Closes the `--cwd` workaround pattern that had been the day-to-day shape of multi-project sessions.
|
|
351
382
|
- **Site facts contract** (umbrella `pln_7fdfd70d` sprint 0) — new `scripts/emit-site-facts.mjs` emits `dist/facts.{js,json}` from `MCP_TOOL_NAMES` + `ENTITY_NAMES` so the brainclaw-site (and any consumer) can pull live tool/entity counts at build time without forking the values into a hand-maintained config. The package `files` list ships `dist/facts.json`; build:cli runs the emitter as part of the chain.
|
|
352
383
|
|
|
353
384
|
### v1.5.2
|
|
Binary file
|
package/dist/cli.js
CHANGED
|
@@ -30,7 +30,7 @@ import { runInstruction } from './commands/instruction.js';
|
|
|
30
30
|
import { runListAgents } from './commands/list-agents.js';
|
|
31
31
|
import { runSurfaceTaskResource } from './commands/surface-task-resource.js';
|
|
32
32
|
import { runListInstructions } from './commands/list-instructions.js';
|
|
33
|
-
import { runDoctor } from './commands/doctor.js';
|
|
33
|
+
import { runDoctor, runDoctorSpawnCheck } from './commands/doctor.js';
|
|
34
34
|
import { runRepair } from './commands/repair.js';
|
|
35
35
|
import { runStale } from './commands/stale.js';
|
|
36
36
|
import { runRebuild } from './commands/rebuild.js';
|
|
@@ -681,7 +681,13 @@ program
|
|
|
681
681
|
.option('--repair', 'Rebuild dist/ when the MCP runtime is missing or stale')
|
|
682
682
|
.option('--after-migration', 'Run the v1.0 post-migration health check only (exits non-zero on any failure)')
|
|
683
683
|
.option('--dispatch', 'Run dispatch-health diagnostic only: reconcile open agent_runs and report stuck/unverified/silent failures (pln#496 step stp_8c072d75)')
|
|
684
|
-
.
|
|
684
|
+
.option('--spawn-check', 'Real spawn round-trip per installed agent before dispatch (pln#520 step 2): validates delivery + handshake on this host, exits non-zero on any installed-agent failure')
|
|
685
|
+
.option('--spawn-check-timeout <ms>', 'Per-agent timeout for --spawn-check (default 15000)', parseInt)
|
|
686
|
+
.action(async (options) => {
|
|
687
|
+
if (options.spawnCheck) {
|
|
688
|
+
await runDoctorSpawnCheck({ cwd: options.cwd, json: options.json, timeoutMs: options.spawnCheckTimeout });
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
685
691
|
runDoctor({ ...options, afterMigration: options.afterMigration, dispatch: options.dispatch });
|
|
686
692
|
});
|
|
687
693
|
// --- repair (Phase 4 Sprint 2 Lane C / pln#397) ---
|
|
@@ -1441,6 +1447,8 @@ dispatchCmd
|
|
|
1441
1447
|
.option('--agents <names>', 'Comma-separated list of agents to dispatch to')
|
|
1442
1448
|
.option('--lanes <names>', 'Comma-separated list of lanes to dispatch')
|
|
1443
1449
|
.option('--max <n>', 'Maximum assignments', parseInt)
|
|
1450
|
+
.option('--max-concurrency <n>', 'Opt-in cap on concurrent instances per host-binary (default: unlimited)', parseInt)
|
|
1451
|
+
.option('--model <name>', 'Model to run, decoupled from agent identity (e.g. --model sonnet)')
|
|
1444
1452
|
.option('--dry', 'Preview assignments without sending messages')
|
|
1445
1453
|
.option('--spawn', 'Autonomously launch CLI agents with invoke templates')
|
|
1446
1454
|
.option('--agent <name>', 'Dispatcher agent name')
|
|
@@ -1450,6 +1458,8 @@ dispatchCmd
|
|
|
1450
1458
|
agents: options.agents,
|
|
1451
1459
|
lanes: options.lanes,
|
|
1452
1460
|
max: options.max,
|
|
1461
|
+
maxConcurrency: options.maxConcurrency,
|
|
1462
|
+
model: options.model,
|
|
1453
1463
|
dry: options.dry,
|
|
1454
1464
|
spawn: options.spawn,
|
|
1455
1465
|
agent: options.agent,
|
|
@@ -87,6 +87,8 @@ export async function runDispatch(options) {
|
|
|
87
87
|
dryRun: options.dry,
|
|
88
88
|
dispatcherAgent,
|
|
89
89
|
autoExecute: options.spawn,
|
|
90
|
+
maxConcurrency: options.maxConcurrency,
|
|
91
|
+
model: options.model,
|
|
90
92
|
}, effectiveCwd);
|
|
91
93
|
if (!result) {
|
|
92
94
|
console.log('No active sequence found.');
|
package/dist/commands/doctor.js
CHANGED
|
@@ -3,6 +3,7 @@ import fs from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import * as childProcess from 'node:child_process';
|
|
5
5
|
import { reconcileAllOpenRuns } from '../core/agentrun-reconciler.js';
|
|
6
|
+
import { runSpawnCheck, renderSpawnCheckReport } from '../core/spawn-check.js';
|
|
6
7
|
import { loadAgentRun } from '../core/agentruns.js';
|
|
7
8
|
import { listAgentIdentities, resolveCurrentAgentIdentity } from '../core/agent-registry.js';
|
|
8
9
|
import { listCapabilities as listRegistryCapabilities, listTools as listRegistryTools } from '../core/registries.js';
|
|
@@ -565,6 +566,22 @@ function renderDispatchHealthHumanReport(report) {
|
|
|
565
566
|
}
|
|
566
567
|
return lines.join('\n');
|
|
567
568
|
}
|
|
569
|
+
/**
|
|
570
|
+
* pln#520 step 2 — `brainclaw doctor --spawn-check`. Real spawn round-trip per
|
|
571
|
+
* installed agent on the current host. Exits non-zero if any installed agent
|
|
572
|
+
* fails (so it gates CI / a pre-dispatch pre-flight).
|
|
573
|
+
*/
|
|
574
|
+
export async function runDoctorSpawnCheck(options = {}) {
|
|
575
|
+
const report = await runSpawnCheck(options);
|
|
576
|
+
if (options.json) {
|
|
577
|
+
console.log(JSON.stringify(report, null, 2));
|
|
578
|
+
}
|
|
579
|
+
else {
|
|
580
|
+
console.log(renderSpawnCheckReport(report));
|
|
581
|
+
}
|
|
582
|
+
if (report.exit_code !== 0)
|
|
583
|
+
process.exit(report.exit_code);
|
|
584
|
+
}
|
|
568
585
|
export function runDoctor(options = {}) {
|
|
569
586
|
if (options.dispatch) {
|
|
570
587
|
const report = runDispatchHealthCheck(options);
|
package/dist/commands/mcp.js
CHANGED
|
@@ -57,6 +57,30 @@ export const SCHEMA_VERSION = '1.0.0';
|
|
|
57
57
|
export const MCP_PROTOCOL_VERSIONS = ['2025-11-25', '2024-11-05'];
|
|
58
58
|
export const MCP_SERVER_NOT_INITIALIZED = -32002;
|
|
59
59
|
const MCP_RUNTIME_REPAIR_COMMAND = 'brainclaw doctor --repair';
|
|
60
|
+
const SEQUENCE_ITEM_INPUT_SCHEMA = {
|
|
61
|
+
type: 'object',
|
|
62
|
+
description: 'Sequence lane item. planId is required; stepId optionally narrows dispatch/readiness to a specific plan step.',
|
|
63
|
+
properties: {
|
|
64
|
+
planId: { type: 'string', minLength: 1, description: 'Plan item ID referenced by this sequence item.' },
|
|
65
|
+
stepId: { type: 'string', minLength: 1, description: 'Optional plan step ID inside planId for step-level dispatch/readiness.' },
|
|
66
|
+
rank: { type: 'number', minimum: 1, description: 'Positive integer ordering key. Ranks must be unique within a sequence.' },
|
|
67
|
+
hard_after: {
|
|
68
|
+
type: 'array',
|
|
69
|
+
items: { type: 'string' },
|
|
70
|
+
description: 'Sequence item planId values that must complete before this item becomes ready.',
|
|
71
|
+
},
|
|
72
|
+
soft_after: {
|
|
73
|
+
type: 'array',
|
|
74
|
+
items: { type: 'string' },
|
|
75
|
+
description: 'Advisory predecessor planId values; they inform ordering but do not block readiness.',
|
|
76
|
+
},
|
|
77
|
+
lane: { type: 'string', description: 'Optional lane label used for parallel dispatch grouping and filtering.' },
|
|
78
|
+
scope_hint: { type: 'string', description: 'Optional file/path scope hint for claim and brief generation.' },
|
|
79
|
+
rationale: { type: 'string', description: 'Optional explanation for this item or dependency placement.' },
|
|
80
|
+
},
|
|
81
|
+
required: ['planId', 'rank'],
|
|
82
|
+
additionalProperties: false,
|
|
83
|
+
};
|
|
60
84
|
const { $defs: loopPhaseDefs, ...loopPhaseItemSchema } = generatedSchemas.LoopPhase;
|
|
61
85
|
const loopSlotInputItemSchema = generatedSchemas.LoopSlotInput;
|
|
62
86
|
export const MCP_READ_TOOLS = [
|
|
@@ -160,7 +184,7 @@ export const MCP_READ_TOOLS = [
|
|
|
160
184
|
{
|
|
161
185
|
name: 'bclaw_list_sequences',
|
|
162
186
|
description: 'List coordination sequences with optional filters on status and id.',
|
|
163
|
-
annotations: { tier: '
|
|
187
|
+
annotations: { tier: 'standard', category: 'coordination', headlessApproval: 'auto' },
|
|
164
188
|
inputSchema: {
|
|
165
189
|
type: 'object',
|
|
166
190
|
properties: {
|
|
@@ -624,7 +648,7 @@ const MCP_WRITE_TOOLS = [
|
|
|
624
648
|
{
|
|
625
649
|
name: 'bclaw_create_sequence',
|
|
626
650
|
description: 'Create a coordination sequence shared by agents.',
|
|
627
|
-
annotations: { tier: '
|
|
651
|
+
annotations: { tier: 'standard', category: 'coordination', headlessApproval: 'prompt' },
|
|
628
652
|
inputSchema: {
|
|
629
653
|
type: 'object',
|
|
630
654
|
properties: {
|
|
@@ -632,7 +656,7 @@ const MCP_WRITE_TOOLS = [
|
|
|
632
656
|
description: { type: 'string', description: 'Optional sequence description.' },
|
|
633
657
|
status: { type: 'string', description: 'Status: draft, active, archived.' },
|
|
634
658
|
owner: { type: 'string', description: 'Optional sequence owner.' },
|
|
635
|
-
items: { type: 'array', description: 'Sequence items in rank order.', items:
|
|
659
|
+
items: { type: 'array', description: 'Sequence items in rank order.', items: SEQUENCE_ITEM_INPUT_SCHEMA },
|
|
636
660
|
tags: { type: 'array', items: { type: 'string' }, description: 'Optional tags.' },
|
|
637
661
|
agent: { type: 'string', description: 'Agent name.' },
|
|
638
662
|
agentId: { type: 'string', description: 'Registered agent id.' },
|
|
@@ -643,7 +667,7 @@ const MCP_WRITE_TOOLS = [
|
|
|
643
667
|
{
|
|
644
668
|
name: 'bclaw_update_sequence',
|
|
645
669
|
description: 'Update a coordination sequence status, metadata, or items.',
|
|
646
|
-
annotations: { tier: '
|
|
670
|
+
annotations: { tier: 'standard', category: 'coordination', headlessApproval: 'prompt' },
|
|
647
671
|
inputSchema: {
|
|
648
672
|
type: 'object',
|
|
649
673
|
properties: {
|
|
@@ -652,7 +676,7 @@ const MCP_WRITE_TOOLS = [
|
|
|
652
676
|
description: { type: 'string', description: 'Optional new description.' },
|
|
653
677
|
status: { type: 'string', description: 'Status: draft, active, archived.' },
|
|
654
678
|
owner: { type: 'string', description: 'Optional sequence owner.' },
|
|
655
|
-
items: { type: 'array', description: 'Optional replacement items array.', items:
|
|
679
|
+
items: { type: 'array', description: 'Optional replacement items array.', items: SEQUENCE_ITEM_INPUT_SCHEMA },
|
|
656
680
|
tags: { type: 'array', items: { type: 'string' }, description: 'Optional replacement tags.' },
|
|
657
681
|
agent: { type: 'string', description: 'Agent name.' },
|
|
658
682
|
agentId: { type: 'string', description: 'Registered agent id.' },
|
|
@@ -754,7 +778,7 @@ const MCP_WRITE_TOOLS = [
|
|
|
754
778
|
{
|
|
755
779
|
name: 'bclaw_delete_sequence',
|
|
756
780
|
description: 'Delete a sequence by ID. Requires trusted or curator trust level.',
|
|
757
|
-
annotations: { tier: '
|
|
781
|
+
annotations: { tier: 'standard', category: 'coordination', headlessApproval: 'prompt' },
|
|
758
782
|
inputSchema: {
|
|
759
783
|
type: 'object',
|
|
760
784
|
properties: {
|
|
@@ -1092,7 +1116,7 @@ const MCP_WRITE_TOOLS = [
|
|
|
1092
1116
|
inputSchema: {
|
|
1093
1117
|
type: 'object',
|
|
1094
1118
|
properties: {
|
|
1095
|
-
entity: { type: 'string', description: 'Entity name: plan | decision | constraint | trap | handoff | runtime_note | candidate | claim | action | assignment | agent_run | cross_project_link. Others not yet wired.' },
|
|
1119
|
+
entity: { type: 'string', description: 'Entity name: plan | decision | constraint | trap | handoff | runtime_note | candidate | sequence | claim | action | assignment | agent_run | cross_project_link. Others not yet wired.' },
|
|
1096
1120
|
filter: { type: 'object', description: 'Filter keys: status, tag (single tag), tags (array, any-match), author, plan_id, source, auto_generated, limit, offset, includeLegacy (bool, default false), minAutoReflectConfidence (0-1, default 0.6). entity=agent_run also accepts assignment_id, claim_id, message_id.' },
|
|
1097
1121
|
project: { type: 'string', description: 'Optional: name (or path/basename) of a linked project to query. Defaults to the current project. Only cross_project_links (config.yaml) and workspace store-chain children are accepted — list with `brainclaw link list`.' },
|
|
1098
1122
|
},
|
package/dist/commands/switch.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { loadActiveProject, saveActiveProject, clearActiveProject } from '../core/active-project.js';
|
|
3
|
-
import { loadCurrentSession, saveCurrentSession } from '../core/identity.js';
|
|
3
|
+
import { buildOperationalIdentity, loadCurrentSession, saveCurrentSession } from '../core/identity.js';
|
|
4
4
|
import { memoryExists } from '../core/io.js';
|
|
5
5
|
import { resolveProjectRef } from '../core/store-resolution.js';
|
|
6
|
-
import { resolveProjectCwd } from '../core/cross-project.js';
|
|
6
|
+
import { resolveCrossProjectLinks, resolveProjectCwd } from '../core/cross-project.js';
|
|
7
7
|
import { scanNestedBrainclawProjects } from '../core/workspace-projects.js';
|
|
8
8
|
import { loadConfig } from '../core/config.js';
|
|
9
9
|
/**
|
|
@@ -43,8 +43,12 @@ export function switchProject(projectRef, options = {}) {
|
|
|
43
43
|
}
|
|
44
44
|
catch { /* name is optional */ }
|
|
45
45
|
const now = new Date().toISOString();
|
|
46
|
-
const session = loadCurrentSession(cwd);
|
|
47
46
|
const sessionOnly = options.sessionOnly ?? true;
|
|
47
|
+
let session = loadCurrentSession(cwd);
|
|
48
|
+
if (!session && sessionOnly) {
|
|
49
|
+
buildOperationalIdentity(undefined, cwd, { persistImplicitSession: true });
|
|
50
|
+
session = loadCurrentSession(cwd);
|
|
51
|
+
}
|
|
48
52
|
if (session && sessionOnly) {
|
|
49
53
|
saveCurrentSession({
|
|
50
54
|
...session,
|
|
@@ -52,6 +56,9 @@ export function switchProject(projectRef, options = {}) {
|
|
|
52
56
|
}, cwd);
|
|
53
57
|
return { switched: true, path: resolved, name: projectName, scope: 'session', workspace_root: wsRoot };
|
|
54
58
|
}
|
|
59
|
+
if (sessionOnly) {
|
|
60
|
+
throw new Error('Cannot switch project without an active agent session. Start with bclaw_work or bclaw_session_start first.');
|
|
61
|
+
}
|
|
55
62
|
if (session) {
|
|
56
63
|
// Also write to session even when not sessionOnly
|
|
57
64
|
saveCurrentSession({
|
|
@@ -75,15 +82,30 @@ export function listAvailableProjects(cwd) {
|
|
|
75
82
|
if (!wsRoot) {
|
|
76
83
|
throw new Error('No brainclaw workspace found.');
|
|
77
84
|
}
|
|
78
|
-
const
|
|
85
|
+
const sessionActive = loadCurrentSession(cwd)?.active_project;
|
|
86
|
+
const globalActive = loadActiveProject(wsRoot);
|
|
87
|
+
const active = sessionActive ?? globalActive;
|
|
88
|
+
const activeSource = sessionActive ? 'session' : globalActive ? 'global' : 'none';
|
|
79
89
|
const projects = [];
|
|
90
|
+
const seen = new Set();
|
|
91
|
+
const addProject = (project) => {
|
|
92
|
+
const projectPath = path.resolve(project.path);
|
|
93
|
+
if (seen.has(projectPath))
|
|
94
|
+
return;
|
|
95
|
+
seen.add(projectPath);
|
|
96
|
+
projects.push({
|
|
97
|
+
...project,
|
|
98
|
+
path: projectPath,
|
|
99
|
+
active: active?.path ? path.resolve(active.path) === projectPath : false,
|
|
100
|
+
});
|
|
101
|
+
};
|
|
80
102
|
if (memoryExists(wsRoot)) {
|
|
81
103
|
try {
|
|
82
104
|
const config = loadConfig(wsRoot);
|
|
83
|
-
|
|
105
|
+
addProject({ name: config.project_name, path: wsRoot, relative_path: '.' });
|
|
84
106
|
}
|
|
85
107
|
catch {
|
|
86
|
-
|
|
108
|
+
addProject({ path: wsRoot, relative_path: '.' });
|
|
87
109
|
}
|
|
88
110
|
}
|
|
89
111
|
const children = scanNestedBrainclawProjects(wsRoot, 7);
|
|
@@ -92,9 +114,19 @@ export function listAvailableProjects(cwd) {
|
|
|
92
114
|
if (childPath === wsRoot)
|
|
93
115
|
continue;
|
|
94
116
|
const rel = path.relative(wsRoot, childPath) || '.';
|
|
95
|
-
|
|
117
|
+
addProject({ name: child.project_name, path: childPath, relative_path: rel });
|
|
118
|
+
}
|
|
119
|
+
for (const link of resolveCrossProjectLinks(wsRoot)) {
|
|
120
|
+
if (!link.available)
|
|
121
|
+
continue;
|
|
122
|
+
const linkPath = path.resolve(link.absolutePath);
|
|
123
|
+
addProject({
|
|
124
|
+
name: link.projectName,
|
|
125
|
+
path: linkPath,
|
|
126
|
+
relative_path: path.relative(wsRoot, linkPath) || '.',
|
|
127
|
+
});
|
|
96
128
|
}
|
|
97
|
-
return { workspace_root: wsRoot, projects };
|
|
129
|
+
return { workspace_root: wsRoot, active_source: activeSource, projects };
|
|
98
130
|
}
|
|
99
131
|
export function runSwitch(projectRef, options = {}) {
|
|
100
132
|
// Use real cwd, not effective cwd — switch must see the full workspace
|
|
@@ -46,6 +46,10 @@ const PROFILES = {
|
|
|
46
46
|
invoke_binary: 'claude',
|
|
47
47
|
invoke_review_template: 'claude -p --allowedTools "Read,Glob,Grep" {prompt}',
|
|
48
48
|
invoke_consult_template: 'claude -p --allowedTools "Read,Glob,Grep" {prompt}',
|
|
49
|
+
// pln#520 step 3: model is selectable via `--model` — no need for a
|
|
50
|
+
// per-model pseudo-identity. `claude-sonnet` below is now redundant
|
|
51
|
+
// (run `claude-code --model sonnet`) and kept only for back-compat.
|
|
52
|
+
model_flag: '--model',
|
|
49
53
|
},
|
|
50
54
|
'claude-sonnet': {
|
|
51
55
|
name: 'claude-sonnet', category: 'code-agent', workflowModel: 'interactive',
|
|
@@ -323,6 +327,63 @@ export function getCapabilityProfile(name) {
|
|
|
323
327
|
const resolved = resolveAgentAlias(name);
|
|
324
328
|
return _customProfiles.get(resolved) ?? PROFILES[resolved];
|
|
325
329
|
}
|
|
330
|
+
/**
|
|
331
|
+
* pln#520 step 3 — concurrency is a resolvable execution-config value, NOT a
|
|
332
|
+
* structural constant baked into agent identity.
|
|
333
|
+
*
|
|
334
|
+
* The host resource a concurrency cap actually protects is the binary on the
|
|
335
|
+
* machine (its API quota / its RAM/CPU footprint), not the agent label.
|
|
336
|
+
* `resolveResourceKey` returns that shared key so callers count usage across
|
|
337
|
+
* every identity that drives one binary. This kills the can_dc4e4a11 bug:
|
|
338
|
+
* `claude-code` and `claude-sonnet` are the SAME `claude` binary on the SAME
|
|
339
|
+
* host but were counted separately (3 + 6 → up to 9 concurrent `claude`
|
|
340
|
+
* processes, oversubscribing the machine + API).
|
|
341
|
+
*/
|
|
342
|
+
export function resolveResourceKey(name) {
|
|
343
|
+
const profile = getCapabilityProfile(name);
|
|
344
|
+
return profile?.invoke_binary ?? resolveAgentAlias(name);
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Resolve the concurrency limit for an agent. `Infinity` = unlimited.
|
|
348
|
+
*
|
|
349
|
+
* Resolution chain (highest priority first), decoupled from agent identity:
|
|
350
|
+
* 1. explicit `override` (e.g. `brainclaw dispatch --max-concurrency N`)
|
|
351
|
+
* 2. host opt-in cap via `BRAINCLAW_MAX_CONCURRENCY` (protect one machine / quota)
|
|
352
|
+
* 3. structural floor — agents that cannot run headless in parallel
|
|
353
|
+
* (IDE / desktop agents, i.e. not CLI-spawnable) stay hard-capped at their
|
|
354
|
+
* profile `max_concurrent_tasks` (you can't spawn N IDE windows headlessly)
|
|
355
|
+
* 4. default for parallelizable CLI agents: UNLIMITED. There is no arbitrary
|
|
356
|
+
* per-identity throttle — the operator opts into a cap when they want one.
|
|
357
|
+
*
|
|
358
|
+
* When a finite cap applies it is enforced per host-binary resource
|
|
359
|
+
* (see `resolveResourceKey`), so all variants of one binary share the pool.
|
|
360
|
+
*/
|
|
361
|
+
export function resolveConcurrencyLimit(name, opts = {}) {
|
|
362
|
+
if (opts.override !== undefined && opts.override > 0)
|
|
363
|
+
return opts.override;
|
|
364
|
+
const envCap = Number(process.env.BRAINCLAW_MAX_CONCURRENCY);
|
|
365
|
+
if (Number.isFinite(envCap) && envCap > 0)
|
|
366
|
+
return envCap;
|
|
367
|
+
const profile = getCapabilityProfile(name);
|
|
368
|
+
if (!profile?.runtime?.canBeSpawnedCli)
|
|
369
|
+
return profile?.max_concurrent_tasks ?? 1;
|
|
370
|
+
return Infinity;
|
|
371
|
+
}
|
|
372
|
+
/** JSON-safe rendering of a concurrency limit: `Infinity` → `null` (= unlimited). */
|
|
373
|
+
export function serializeConcurrencyLimit(limit) {
|
|
374
|
+
return Number.isFinite(limit) ? limit : null;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* pln#520 step 3 — resolve the model for a dispatch, decoupled from agent
|
|
378
|
+
* identity. Chain (highest priority first): explicit override (e.g.
|
|
379
|
+
* `dispatch --model`) → lane model → identity model → profile default.
|
|
380
|
+
* Returns `undefined` when nothing in the chain specifies one (the agent's
|
|
381
|
+
* template default applies).
|
|
382
|
+
*/
|
|
383
|
+
export function resolveModel(name, opts = {}) {
|
|
384
|
+
const profile = getCapabilityProfile(name);
|
|
385
|
+
return opts.override ?? opts.lane ?? opts.identity ?? profile?.default_model;
|
|
386
|
+
}
|
|
326
387
|
/**
|
|
327
388
|
* Escape a string for safe use as a double-quoted shell argument.
|
|
328
389
|
* Escapes characters that have special meaning inside double-quotes
|
|
@@ -490,6 +551,12 @@ export function buildInvokeCommand(name, prompt, options = {}) {
|
|
|
490
551
|
const rawTokens = parseTemplateString(templateStr);
|
|
491
552
|
if (rawTokens.length === 0)
|
|
492
553
|
return undefined;
|
|
554
|
+
// pln#520 step 3: inject the resolved model right after the binary so model
|
|
555
|
+
// choice is decoupled from agent identity. Only when the profile declares a
|
|
556
|
+
// `model_flag` and the template doesn't already pin a model (don't double it).
|
|
557
|
+
if (options.model && profile.model_flag && !rawTokens.includes(profile.model_flag)) {
|
|
558
|
+
rawTokens.splice(1, 0, profile.model_flag, options.model);
|
|
559
|
+
}
|
|
493
560
|
const executable = rawTokens[0];
|
|
494
561
|
const interpolatedTokens = rawTokens.slice(1).map((tok) => tok === '{prompt}' ? embeddedPrompt : tok);
|
|
495
562
|
// ── 5. Build the args array ───────────────────────────────────────────────
|