brainclaw 1.7.2 → 1.7.3
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 +116 -102
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/cli.js +13 -1
- package/dist/commands/harvest.js +124 -1
- package/dist/commands/mcp.js +1 -1
- package/dist/core/agent-inventory.js +54 -7
- package/dist/core/dirty-scope.js +11 -5
- package/dist/core/dispatcher.js +10 -0
- package/dist/core/instruction-templates.js +1 -1
- package/dist/core/schema.js +18 -0
- package/dist/core/worktree.js +146 -7
- package/dist/facts.js +3 -3
- package/dist/facts.json +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ If you've ever:
|
|
|
14
14
|
- watched two coworkers (or two agents) **edit the same files** without knowing it,
|
|
15
15
|
- or **gave up running multiple agents in parallel** because keeping them in sync was a pain,
|
|
16
16
|
|
|
17
|
-
brainclaw gives you durable shared state across sessions, agents, and teammates. Plans, claims, handoffs, decisions, and traps live in `.brainclaw/`, work identically across any compatible agent (Claude Code, Codex, Copilot, Cline, OpenCode, Cursor, Windsurf, Kilocode, Roo Code, Continue, Mistral Vibe, Hermes, Antigravity/Gemini CLI, …), and stay accessible whether you orchestrate them in parallel or pick them up one after another.
|
|
17
|
+
brainclaw gives you durable shared state across sessions, agents, and teammates. Plans, claims, handoffs, decisions, and traps live in `.brainclaw/`, work identically across any compatible agent (Claude Code, Codex, Copilot, Cline, OpenCode, Cursor, Windsurf, Kilocode, Roo Code, Continue, Mistral Vibe, Hermes, Antigravity/Gemini CLI, …), and stay accessible whether you orchestrate them in parallel or pick them up one after another.
|
|
18
18
|
|
|
19
19
|
Use it two ways — **together or separately**:
|
|
20
20
|
|
|
@@ -81,8 +81,8 @@ brainclaw is designed to sit alongside the coding agents teams are already using
|
|
|
81
81
|
|
|
82
82
|
| Logo | Agent | Tier | What brainclaw configures |
|
|
83
83
|
|---|---|---|---|
|
|
84
|
-
| [](https://github.com/openclaw/openclaw) | **[OpenClaw](https://github.com/openclaw/openclaw)** | B | MCP + brainclaw skill (SKILL.md) for structured project memory |
|
|
85
|
-
| [](https://github.com/NousResearch/hermes-agent) | **[Hermes Agent](https://github.com/NousResearch/hermes-agent)** | B | MCP + universal `.agents/skills/brainclaw/SKILL.md` |
|
|
84
|
+
| [](https://github.com/openclaw/openclaw) | **[OpenClaw](https://github.com/openclaw/openclaw)** | B | MCP + brainclaw skill (SKILL.md) for structured project memory |
|
|
85
|
+
| [](https://github.com/NousResearch/hermes-agent) | **[Hermes Agent](https://github.com/NousResearch/hermes-agent)** | B | MCP + universal `.agents/skills/brainclaw/SKILL.md` |
|
|
86
86
|
| [](https://github.com/qwibitai/nanoclaw) | **[NanoClaw](https://github.com/qwibitai/nanoclaw)** | C | brainclaw skill — messaging agent (WhatsApp, Telegram, Slack) |
|
|
87
87
|
| [](https://github.com/NVIDIA/NemoClaw) | **[NemoClaw](https://github.com/NVIDIA/NemoClaw)** | C | brainclaw skill — NVIDIA enterprise agent stack |
|
|
88
88
|
| [](https://github.com/sipeed/picoclaw) | **[PicoClaw](https://github.com/sipeed/picoclaw)** | C | brainclaw skill — edge/IoT agent (Go, <10MB RAM) |
|
|
@@ -109,52 +109,52 @@ If you want the least surprising setup today, use Linux first. If you are on Win
|
|
|
109
109
|
|
|
110
110
|
---
|
|
111
111
|
|
|
112
|
-
## Get Started
|
|
113
|
-
|
|
114
|
-
### 1. Let your coding agent lead
|
|
115
|
-
|
|
116
|
-
The smoothest first-run path is agent-first:
|
|
117
|
-
|
|
118
|
-
1. ask your coding agent to inspect the package and explain what brainclaw does
|
|
119
|
-
2. ask it to install brainclaw and initialize or join the project you're working on
|
|
120
|
-
3. use the CLI yourself when you need an explicit operator or fallback path
|
|
121
|
-
|
|
122
|
-
If you want to drive setup manually, use the steps below.
|
|
123
|
-
|
|
124
|
-
### 2. Install
|
|
125
|
-
|
|
126
|
-
```bash
|
|
127
|
-
npm install -g brainclaw
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
### 3. Bootstrap this machine
|
|
131
|
-
|
|
132
|
-
```bash
|
|
133
|
-
brainclaw setup-machine --yes
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
This detects the installed coding agents on the current machine, writes the machine-level MCP and user config Brainclaw manages for that detected set, and does **not** scan or initialize repositories.
|
|
137
|
-
|
|
138
|
-
### 4. Initialize or refresh the current project
|
|
139
|
-
|
|
140
|
-
```bash
|
|
141
|
-
cd your-project
|
|
142
|
-
brainclaw init
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
`brainclaw init` is now safe to rerun. It creates `.brainclaw/` when the project is new, or refreshes the managed Brainclaw and agent integration files when the project already has memory.
|
|
146
|
-
|
|
147
|
-
If you are explicitly adding another agent to an existing Brainclaw project, use:
|
|
148
|
-
|
|
149
|
-
```bash
|
|
150
|
-
brainclaw enable-agent <agent-name>
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
### 5. Restart your agent
|
|
154
|
-
|
|
155
|
-
Restart your coding agent (or reload MCP servers) so it picks up the new configuration. After that, brainclaw tools are available.
|
|
156
|
-
|
|
157
|
-
### 6. Start working
|
|
112
|
+
## Get Started
|
|
113
|
+
|
|
114
|
+
### 1. Let your coding agent lead
|
|
115
|
+
|
|
116
|
+
The smoothest first-run path is agent-first:
|
|
117
|
+
|
|
118
|
+
1. ask your coding agent to inspect the package and explain what brainclaw does
|
|
119
|
+
2. ask it to install brainclaw and initialize or join the project you're working on
|
|
120
|
+
3. use the CLI yourself when you need an explicit operator or fallback path
|
|
121
|
+
|
|
122
|
+
If you want to drive setup manually, use the steps below.
|
|
123
|
+
|
|
124
|
+
### 2. Install
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
npm install -g brainclaw
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### 3. Bootstrap this machine
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
brainclaw setup-machine --yes
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
This detects the installed coding agents on the current machine, writes the machine-level MCP and user config Brainclaw manages for that detected set, and does **not** scan or initialize repositories.
|
|
137
|
+
|
|
138
|
+
### 4. Initialize or refresh the current project
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
cd your-project
|
|
142
|
+
brainclaw init
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`brainclaw init` is now safe to rerun. It creates `.brainclaw/` when the project is new, or refreshes the managed Brainclaw and agent integration files when the project already has memory.
|
|
146
|
+
|
|
147
|
+
If you are explicitly adding another agent to an existing Brainclaw project, use:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
brainclaw enable-agent <agent-name>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### 5. Restart your agent
|
|
154
|
+
|
|
155
|
+
Restart your coding agent (or reload MCP servers) so it picks up the new configuration. After that, brainclaw tools are available.
|
|
156
|
+
|
|
157
|
+
### 6. Start working
|
|
158
158
|
|
|
159
159
|
Pick one of the canonical entry points depending on what you're doing:
|
|
160
160
|
|
|
@@ -186,7 +186,7 @@ For agents without MCP (e.g. Copilot reads `.github/copilot-instructions.md`), r
|
|
|
186
186
|
brainclaw export --detect --write
|
|
187
187
|
```
|
|
188
188
|
|
|
189
|
-
### 7. Verify it works
|
|
189
|
+
### 7. Verify it works
|
|
190
190
|
|
|
191
191
|
```bash
|
|
192
192
|
brainclaw status # see active sessions, claims, plans
|
|
@@ -197,11 +197,11 @@ brainclaw agent-board # see what each agent is doing
|
|
|
197
197
|
|
|
198
198
|
To configure brainclaw for all your repos and agents at once:
|
|
199
199
|
|
|
200
|
-
```bash
|
|
201
|
-
brainclaw setup --yes
|
|
202
|
-
```
|
|
203
|
-
|
|
204
|
-
This is the broader multi-repo wizard. It bootstraps the machine, scans your project roots, and initializes selected repositories in one pass.
|
|
200
|
+
```bash
|
|
201
|
+
brainclaw setup --yes
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
This is the broader multi-repo wizard. It bootstraps the machine, scans your project roots, and initializes selected repositories in one pass.
|
|
205
205
|
|
|
206
206
|
### Existing projects
|
|
207
207
|
|
|
@@ -232,7 +232,7 @@ Still sharp:
|
|
|
232
232
|
1. **Same-checkout concurrent edits** — running two agents in the *same* working tree (no per-claim worktree) is still the wrong answer. Use the dispatch path (auto-worktree per claim) instead of raw concurrent CLI sessions.
|
|
233
233
|
2. **Cross-machine sync** — federation across machines is on the roadmap, not in v1.x. Today brainclaw's store is local and one-machine-per-project.
|
|
234
234
|
3. **Spawn-and-forget assumptions** — spawned workers don't always commit their work cleanly. The brief-ack file confirms the spawn started; in the worst case the coordinator harvests open changes.
|
|
235
|
-
4. **Live state for hook-less agents** — Tier B/C agents without lifecycle hooks (Cursor, Cline, Windsurf, Copilot, Continue, Kilocode, Mistral Vibe, Hermes) get live context via `.live.md` companions regenerated on session-end and handoff, not via real-time push.
|
|
235
|
+
4. **Live state for hook-less agents** — Tier B/C agents without lifecycle hooks (Cursor, Cline, Windsurf, Copilot, Continue, Kilocode, Mistral Vibe, Hermes) get live context via `.live.md` companions regenerated on session-end and handoff, not via real-time push.
|
|
236
236
|
|
|
237
237
|
Recommended use today:
|
|
238
238
|
|
|
@@ -343,42 +343,56 @@ 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.7.
|
|
349
|
-
|
|
350
|
-
- **
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
`
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
`
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
`
|
|
378
|
-
|
|
379
|
-
### v1.
|
|
380
|
-
|
|
381
|
-
- **
|
|
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.3
|
|
349
|
+
|
|
350
|
+
- **Multi-agent dispatch hardening for JS/TS monorepos** — dispatched worktrees
|
|
351
|
+
junction-link per-package `node_modules` (npm / yarn / pnpm workspaces), not
|
|
352
|
+
just the root, and surface failed links instead of swallowing them; `brainclaw
|
|
353
|
+
worktree clean` now garbage-collects merged worktrees past birth-noise instead
|
|
354
|
+
of skipping them all; the agent inventory reports an agent `spawnable` when its
|
|
355
|
+
binary is on PATH even if `--version` is slow to start; dispatch-verification
|
|
356
|
+
guidance leads with `bclaw_dispatch_status` (not the untrustworthy Windows
|
|
357
|
+
wrapper pid); and a new `LANE-RESULT.json` convention + `brainclaw harvest
|
|
358
|
+
<assignment_id>` give workers a standard, MCP-free result channel. The dispatch
|
|
359
|
+
dirty-guard also ignores `.claude/`, `.cursor/`, and `.codex/` agent-local
|
|
360
|
+
config. (pln#523, pln#524, pln#525, pln#526, trp#371, trp#427, trp#428)
|
|
361
|
+
|
|
362
|
+
### v1.7.2
|
|
363
|
+
|
|
364
|
+
- **Sequence MCP tools are agent-first by default** — sequence creation,
|
|
365
|
+
listing, update, and deletion tools are now in the default MCP catalog, with
|
|
366
|
+
explicit lane item schemas (`planId`, optional `stepId`, `rank`,
|
|
367
|
+
dependencies, lane metadata) and matching canonical CRUD validation for
|
|
368
|
+
`entity="sequence"`.
|
|
369
|
+
|
|
370
|
+
### v1.7.1
|
|
371
|
+
|
|
372
|
+
- **MCP project context isolation fix** — `bclaw_switch` now keeps MCP switches
|
|
373
|
+
session-scoped even when the agent session has to be resolved or created on
|
|
374
|
+
the fly. Session lookup honors explicit session IDs, avoids adopting another
|
|
375
|
+
live process's session, detects Codex via native `CODEX_*` runtime variables,
|
|
376
|
+
and `bclaw_switch(list=true)` reports the session active project with
|
|
377
|
+
`active_source`.
|
|
378
|
+
|
|
379
|
+
### v1.7.0
|
|
380
|
+
|
|
381
|
+
- **Dispatch reliability + scope-aware dirty guard** — evidence-first
|
|
382
|
+
`agent_run` reconciliation avoids false terminal states, `bclaw_coordinate`
|
|
383
|
+
accepts pinned refs and a scope-aware `allow_dirty` guard, and the Hermes
|
|
384
|
+
agent integration joins the supported surfaces.
|
|
385
|
+
|
|
386
|
+
### v1.6.0
|
|
387
|
+
|
|
388
|
+
- **Bootstrap loop + cross-project agent workflow** — the bootstrap ideation
|
|
389
|
+
preset can materialize `PROJECT.md`, `bclaw_init_project` initializes and links
|
|
390
|
+
arbitrary project paths, and `project=` routing reaches `bclaw_work` /
|
|
391
|
+
`bclaw_loop` for linked-project operations.
|
|
392
|
+
|
|
393
|
+
### v1.5.3
|
|
394
|
+
|
|
395
|
+
- **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.
|
|
382
396
|
- **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.
|
|
383
397
|
|
|
384
398
|
### v1.5.2
|
|
@@ -436,15 +450,15 @@ For older releases (v0.x and the early v1.0 launch series), `git log` on `master
|
|
|
436
450
|
|
|
437
451
|
---
|
|
438
452
|
|
|
439
|
-
## License
|
|
440
|
-
|
|
441
|
-
brainclaw core is published under the [MIT License](LICENSE) — (c) 2024-2026 Juan Berdah.
|
|
442
|
-
|
|
443
|
-
The licensing split is simple:
|
|
444
|
-
|
|
445
|
-
- the local-first brainclaw core is MIT
|
|
446
|
-
- cloud shared-memory, remote collaboration services, advanced dashboards, and related hosted add-ons will live in separate commercial products
|
|
447
|
-
|
|
448
|
-
The MIT core covers what makes brainclaw useful inside a repo today: local project memory, local MCP and CLI coordination, onboarding and bootstrap, plans, claims, handoffs, runtime notes, and local agent integrations.
|
|
453
|
+
## License
|
|
454
|
+
|
|
455
|
+
brainclaw core is published under the [MIT License](LICENSE) — (c) 2024-2026 Juan Berdah.
|
|
456
|
+
|
|
457
|
+
The licensing split is simple:
|
|
458
|
+
|
|
459
|
+
- the local-first brainclaw core is MIT
|
|
460
|
+
- cloud shared-memory, remote collaboration services, advanced dashboards, and related hosted add-ons will live in separate commercial products
|
|
461
|
+
|
|
462
|
+
The MIT core covers what makes brainclaw useful inside a repo today: local project memory, local MCP and CLI coordination, onboarding and bootstrap, plans, claims, handoffs, runtime notes, and local agent integrations.
|
|
449
463
|
|
|
450
464
|
The goal is not to close brainclaw down. The goal is to keep the local-first core open and genuinely useful on its own, while keeping hosted collaboration features separate.
|
|
Binary file
|
package/dist/cli.js
CHANGED
|
@@ -104,7 +104,7 @@ import { runDiscover } from './commands/discover.js';
|
|
|
104
104
|
import { runMigrate } from './commands/migrate.js';
|
|
105
105
|
import { runRunProfile } from './commands/run-profile.js';
|
|
106
106
|
import { runCompact } from './commands/compact.js';
|
|
107
|
-
import { runHarvestCandidates } from './commands/harvest.js';
|
|
107
|
+
import { runHarvestCandidates, runHarvestLane } from './commands/harvest.js';
|
|
108
108
|
import { runQuestionsCommand } from './commands/questions.js';
|
|
109
109
|
import { runReplyCommand } from './commands/reply.js';
|
|
110
110
|
import { requireRegisteredAgentIdentity } from './core/agent-registry.js';
|
|
@@ -1009,6 +1009,18 @@ program
|
|
|
1009
1009
|
const globalOpts = program.opts();
|
|
1010
1010
|
runHarvestCandidates({ ...options, cwd: globalOpts.cwd });
|
|
1011
1011
|
});
|
|
1012
|
+
// --- harvest (lane results, pln#526) ---
|
|
1013
|
+
program
|
|
1014
|
+
.command('harvest [assignment_id]')
|
|
1015
|
+
.description('Harvest a worker LANE-RESULT.json from its worktree into the project (pass an assignment id, or --all)')
|
|
1016
|
+
.option('--all', 'Harvest every lane result found across worktrees')
|
|
1017
|
+
.option('--dry-run', 'Preview without writing events/markers')
|
|
1018
|
+
.option('--worktree <path>', 'Explicit worktree path to scan (repeatable)', collect, [])
|
|
1019
|
+
.option('--json', 'Output as JSON')
|
|
1020
|
+
.action((assignmentId, options) => {
|
|
1021
|
+
const globalOpts = program.opts();
|
|
1022
|
+
runHarvestLane(assignmentId, { ...options, cwd: globalOpts.cwd });
|
|
1023
|
+
});
|
|
1012
1024
|
// --- prune-candidates ---
|
|
1013
1025
|
program
|
|
1014
1026
|
.command('prune-candidates')
|
package/dist/commands/harvest.js
CHANGED
|
@@ -14,7 +14,7 @@ import fs from 'node:fs';
|
|
|
14
14
|
import os from 'node:os';
|
|
15
15
|
import path from 'node:path';
|
|
16
16
|
import crypto from 'node:crypto';
|
|
17
|
-
import { CandidateSchema } from '../core/schema.js';
|
|
17
|
+
import { CandidateSchema, LaneResultSchema } from '../core/schema.js';
|
|
18
18
|
import { listCandidates, listArchivedCandidates, saveCandidate } from '../core/candidates.js';
|
|
19
19
|
import { createRuntimeEvent } from '../core/events.js';
|
|
20
20
|
import { memoryExists } from '../core/io.js';
|
|
@@ -186,4 +186,127 @@ export function runHarvestCandidates(options = {}) {
|
|
|
186
186
|
}
|
|
187
187
|
console.log(`\n✔ Harvest complete${dryTag}: ${result.harvested.length} imported, ${result.skipped.length} skipped, ${result.errors.length} error(s).`);
|
|
188
188
|
}
|
|
189
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
190
|
+
// pln#526 — LANE-RESULT convention
|
|
191
|
+
//
|
|
192
|
+
// A dispatched worker writes a single `LANE-RESULT.json` at its worktree root
|
|
193
|
+
// as its final step. This is the standard, brief-boilerplate-free channel for a
|
|
194
|
+
// worker (especially a sandboxed one that cannot reach MCP) to report its
|
|
195
|
+
// outcome. The coordinator ingests it with `brainclaw harvest <assignment_id>`.
|
|
196
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
197
|
+
/** Conventional path of a worker's lane-result file at the worktree root. */
|
|
198
|
+
export function getLaneResultPath(worktreePath) {
|
|
199
|
+
return path.join(worktreePath, 'LANE-RESULT.json');
|
|
200
|
+
}
|
|
201
|
+
/** Idempotency marker so a lane-result is harvested once. */
|
|
202
|
+
function laneHarvestedMarkerPath(cwd, assignmentId) {
|
|
203
|
+
return path.join(cwd, '.brainclaw', 'coordination', 'runtime', 'result', `${assignmentId}.harvested`);
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Scan worktrees for `LANE-RESULT.json`, validate, and ingest each: emit a
|
|
207
|
+
* `lane_result_harvested` runtime event (durable + queryable) and drop an
|
|
208
|
+
* idempotency marker so re-runs skip it. The worker's actual code lives in the
|
|
209
|
+
* worktree/branch; this surfaces the structured outcome (status + summary) the
|
|
210
|
+
* coordinator needs to converge the lane.
|
|
211
|
+
*/
|
|
212
|
+
export function harvestLaneResults(options = {}) {
|
|
213
|
+
const cwd = options.cwd ?? process.cwd();
|
|
214
|
+
const agent = options.agent ?? 'coordinator';
|
|
215
|
+
const result = { harvested: [], skipped: [], errors: [] };
|
|
216
|
+
const worktreePaths = (options.worktreePaths && options.worktreePaths.length > 0)
|
|
217
|
+
? options.worktreePaths
|
|
218
|
+
: autoDetectWorktreePaths(cwd);
|
|
219
|
+
for (const worktreePath of worktreePaths) {
|
|
220
|
+
const file = getLaneResultPath(worktreePath);
|
|
221
|
+
if (!fs.existsSync(file))
|
|
222
|
+
continue;
|
|
223
|
+
let lane;
|
|
224
|
+
try {
|
|
225
|
+
lane = LaneResultSchema.parse(JSON.parse(fs.readFileSync(file, 'utf-8')));
|
|
226
|
+
}
|
|
227
|
+
catch (err) {
|
|
228
|
+
result.errors.push(`Failed to parse ${file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
// Assignment filter (when harvesting a specific lane).
|
|
232
|
+
if (options.assignmentId && lane.assignment_id !== options.assignmentId)
|
|
233
|
+
continue;
|
|
234
|
+
const marker = laneHarvestedMarkerPath(cwd, lane.assignment_id);
|
|
235
|
+
if (fs.existsSync(marker)) {
|
|
236
|
+
result.skipped.push(lane.assignment_id);
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (!options.dryRun) {
|
|
240
|
+
try {
|
|
241
|
+
createRuntimeEvent({
|
|
242
|
+
agent,
|
|
243
|
+
event_type: 'lane_result_harvested',
|
|
244
|
+
text: `Lane result for ${lane.assignment_id}: ${lane.status} — ${lane.summary.slice(0, 120)}`,
|
|
245
|
+
tags: ['harvest', 'lane-result', lane.status],
|
|
246
|
+
assignment_id: lane.assignment_id,
|
|
247
|
+
metadata: {
|
|
248
|
+
assignment_id: lane.assignment_id,
|
|
249
|
+
status: lane.status,
|
|
250
|
+
artifacts: lane.artifacts ?? [],
|
|
251
|
+
files_changed: lane.files_changed ?? [],
|
|
252
|
+
source_worktree: worktreePath,
|
|
253
|
+
},
|
|
254
|
+
}, cwd);
|
|
255
|
+
fs.mkdirSync(path.dirname(marker), { recursive: true });
|
|
256
|
+
fs.writeFileSync(marker, new Date(0).toISOString(), 'utf-8');
|
|
257
|
+
}
|
|
258
|
+
catch (err) {
|
|
259
|
+
result.errors.push(`Failed to ingest lane result for ${lane.assignment_id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
result.harvested.push(lane);
|
|
264
|
+
}
|
|
265
|
+
return result;
|
|
266
|
+
}
|
|
267
|
+
export function runHarvestLane(assignmentId, options = {}) {
|
|
268
|
+
const cwd = options.cwd ?? process.cwd();
|
|
269
|
+
if (!memoryExists(cwd)) {
|
|
270
|
+
console.error('Error: .brainclaw/ not found. Run `brainclaw init` first.');
|
|
271
|
+
process.exit(1);
|
|
272
|
+
}
|
|
273
|
+
if (!assignmentId && !options.all) {
|
|
274
|
+
console.error('Error: provide an <assignment_id>, or pass --all to harvest every lane result.');
|
|
275
|
+
process.exit(1);
|
|
276
|
+
}
|
|
277
|
+
const result = harvestLaneResults({
|
|
278
|
+
assignmentId: options.all ? undefined : assignmentId,
|
|
279
|
+
worktreePaths: options.worktree,
|
|
280
|
+
dryRun: options.dryRun,
|
|
281
|
+
cwd,
|
|
282
|
+
});
|
|
283
|
+
if (options.json) {
|
|
284
|
+
console.log(JSON.stringify({
|
|
285
|
+
harvested: result.harvested,
|
|
286
|
+
skipped: result.skipped,
|
|
287
|
+
errors: result.errors,
|
|
288
|
+
}, null, 2));
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
const dryTag = options.dryRun ? ' (dry-run)' : '';
|
|
292
|
+
if (result.harvested.length === 0 && result.skipped.length === 0 && result.errors.length === 0) {
|
|
293
|
+
console.log(assignmentId ? `No LANE-RESULT.json found for ${assignmentId}.` : 'No lane results found in any worktree.');
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
for (const lane of result.harvested) {
|
|
297
|
+
const verb = options.dryRun ? ' (dry-run) Would harvest' : ' ✔ Harvested';
|
|
298
|
+
console.log(`${verb} [${lane.assignment_id}] ${lane.status}: ${lane.summary.slice(0, 100)}`);
|
|
299
|
+
if (lane.files_changed?.length)
|
|
300
|
+
console.log(` files: ${lane.files_changed.slice(0, 8).join(', ')}`);
|
|
301
|
+
if (lane.notes)
|
|
302
|
+
console.log(` notes: ${lane.notes.slice(0, 120)}`);
|
|
303
|
+
}
|
|
304
|
+
for (const id of result.skipped) {
|
|
305
|
+
console.log(` ⟳ Skipped (already harvested): ${id}`);
|
|
306
|
+
}
|
|
307
|
+
for (const err of result.errors) {
|
|
308
|
+
console.error(` ✗ ${err}`);
|
|
309
|
+
}
|
|
310
|
+
console.log(`\n✔ Lane harvest complete${dryTag}: ${result.harvested.length} harvested, ${result.skipped.length} skipped, ${result.errors.length} error(s).`);
|
|
311
|
+
}
|
|
189
312
|
//# sourceMappingURL=harvest.js.map
|
package/dist/commands/mcp.js
CHANGED
|
@@ -956,7 +956,7 @@ const MCP_WRITE_TOOLS = [
|
|
|
956
956
|
},
|
|
957
957
|
{
|
|
958
958
|
name: 'bclaw_coordinate',
|
|
959
|
-
description: 'Multi-agent coordination facade: assign tasks to agents (with claims), consult agents (no claim), create a review candidate, open an ideation loop, reroute an active claim to another agent, or summarize a thread. Returns a FacadeResponse with selected_targets, delivery_plan, artifacts, side_effects, and execution_status. IMPORTANT — execution_status semantics: `delivered_and_started` means the spawn wrapper touched the brief-ack sentinel (`.brainclaw/coordination/runtime/ack/<assignment_id>.ack`) — NOT that the worker is doing useful work. Spawned workers may still die silently before consuming the brief (cf. trap trp_38f63ea4). To verify a dispatch is actually alive,
|
|
959
|
+
description: 'Multi-agent coordination facade: assign tasks to agents (with claims), consult agents (no claim), create a review candidate, open an ideation loop, reroute an active claim to another agent, or summarize a thread. Returns a FacadeResponse with selected_targets, delivery_plan, artifacts, side_effects, and execution_status. IMPORTANT — execution_status semantics: `delivered_and_started` means the spawn wrapper touched the brief-ack sentinel (`.brainclaw/coordination/runtime/ack/<assignment_id>.ack`) — NOT that the worker is doing useful work. Spawned workers may still die silently before consuming the brief (cf. trap trp_38f63ea4). To verify a dispatch is actually alive, call `bclaw_dispatch_status(target_id=<asgn_…>)` — it reads the runtime sentinels (ack/heartbeat/completed/failed) plus captured stdout/stderr tails and returns a single health verdict + recommended next action (this is the `verify_with` target attached to the response). Do NOT diagnose liveness from the tracked pid: on Windows an ack-wrapped spawn runs under cmd.exe, so `agent_run.pid` is the wrapper (which exits early by design), NOT the real worker — `Get-Process -Id <pid>` reads it dead while the worker is alive and committing. The reconciler trusts the sentinels and infers `completed` from a post-start commit even when the worker never called bclaw_assignment_update. See docs/concepts/dispatch-lifecycle.md for the full FSM + diagnostic decision tree, and docs/integrations/<agent>.md for per-agent spawn semantics (notably codex.md re sandbox MCP availability).',
|
|
960
960
|
annotations: { tier: 'facade', category: 'coordination', headlessApproval: 'auto' },
|
|
961
961
|
inputSchema: {
|
|
962
962
|
type: 'object',
|
|
@@ -5,6 +5,14 @@ import { spawnSync } from 'node:child_process';
|
|
|
5
5
|
import yaml from 'yaml';
|
|
6
6
|
import { MEMORY_DIR } from './io.js';
|
|
7
7
|
import { detectHostExecutionProfile, } from './execution-profile.js';
|
|
8
|
+
import { getCapabilityProfile } from './agent-capability.js';
|
|
9
|
+
/**
|
|
10
|
+
* trp#427 — cold-start CLI `--version` probes need headroom; a 3s timeout
|
|
11
|
+
* false-negatived claude-code on first launch. The spawnable check (binary on
|
|
12
|
+
* PATH) is the robust signal, so this only affects version-string capture
|
|
13
|
+
* latency, not the installed/spawnable decision.
|
|
14
|
+
*/
|
|
15
|
+
const VERSION_PROBE_TIMEOUT_MS = 8000;
|
|
8
16
|
function tryCommand(command, args, timeout = 5000) {
|
|
9
17
|
try {
|
|
10
18
|
const r = spawnSync(command, args, { encoding: 'utf-8', timeout, windowsHide: true });
|
|
@@ -14,12 +22,40 @@ function tryCommand(command, args, timeout = 5000) {
|
|
|
14
22
|
return { ok: false, stdout: '' };
|
|
15
23
|
}
|
|
16
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* trp#427 — fast PATH resolution for a binary (no process launch, unlike a
|
|
27
|
+
* `--version` probe). Uses `where` (Windows) / `which` (POSIX).
|
|
28
|
+
*/
|
|
29
|
+
function isBinaryOnPath(binary) {
|
|
30
|
+
if (!binary)
|
|
31
|
+
return false;
|
|
32
|
+
try {
|
|
33
|
+
const cmd = process.platform === 'win32' ? 'where' : 'which';
|
|
34
|
+
const r = spawnSync(cmd, [binary], { encoding: 'utf-8', timeout: 3000, windowsHide: true });
|
|
35
|
+
return r.status === 0 && (r.stdout ?? '').trim().length > 0;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* trp#427 — an agent is SPAWNABLE when its capability profile is CLI-spawnable,
|
|
43
|
+
* declares an invoke binary, and that binary resolves on PATH. Decoupled from
|
|
44
|
+
* the `--version` health probe so a slow cold-start CLI is never misreported as
|
|
45
|
+
* "not installed" / undispatchable.
|
|
46
|
+
*/
|
|
47
|
+
export function detectSpawnable(agentName) {
|
|
48
|
+
const profile = getCapabilityProfile(agentName);
|
|
49
|
+
if (!profile || !profile.runtime?.canBeSpawnedCli || !profile.invoke_binary)
|
|
50
|
+
return false;
|
|
51
|
+
return isBinaryOnPath(profile.invoke_binary);
|
|
52
|
+
}
|
|
17
53
|
const AGENT_DEFINITIONS = [
|
|
18
54
|
{
|
|
19
55
|
name: 'claude-code',
|
|
20
56
|
detect: (_home, env) => {
|
|
21
57
|
// Check if claude CLI is available
|
|
22
|
-
const cli = tryCommand('claude', ['--version'],
|
|
58
|
+
const cli = tryCommand('claude', ['--version'], VERSION_PROBE_TIMEOUT_MS);
|
|
23
59
|
if (cli.ok) {
|
|
24
60
|
const ver = cli.stdout.trim().match(/(\d+\.\d+\.\d+)/)?.[1];
|
|
25
61
|
return { installed: true, method: 'claude CLI', version: ver };
|
|
@@ -81,7 +117,7 @@ const AGENT_DEFINITIONS = [
|
|
|
81
117
|
if (fs.existsSync(codexDir)) {
|
|
82
118
|
return { installed: true, method: '~/.codex directory' };
|
|
83
119
|
}
|
|
84
|
-
const cli = tryCommand('codex', ['--version'],
|
|
120
|
+
const cli = tryCommand('codex', ['--version'], VERSION_PROBE_TIMEOUT_MS);
|
|
85
121
|
if (cli.ok) {
|
|
86
122
|
const ver = cli.stdout.trim().match(/(\d+\.\d+\.\d+)/)?.[1];
|
|
87
123
|
return { installed: true, method: 'codex CLI', version: ver };
|
|
@@ -252,7 +288,7 @@ const AGENT_DEFINITIONS = [
|
|
|
252
288
|
if (fs.existsSync(path.join(home, '.gemini', 'antigravity'))) {
|
|
253
289
|
return { installed: true, method: '~/.gemini/antigravity directory' };
|
|
254
290
|
}
|
|
255
|
-
const cli = tryCommand('gemini', ['--version'],
|
|
291
|
+
const cli = tryCommand('gemini', ['--version'], VERSION_PROBE_TIMEOUT_MS);
|
|
256
292
|
if (cli.ok) {
|
|
257
293
|
return { installed: true, method: 'gemini CLI', version: cli.stdout.trim() };
|
|
258
294
|
}
|
|
@@ -309,7 +345,7 @@ const AGENT_DEFINITIONS = [
|
|
|
309
345
|
if (fs.existsSync(path.join(home, '.hermes'))) {
|
|
310
346
|
return { installed: true, method: '~/.hermes directory' };
|
|
311
347
|
}
|
|
312
|
-
const cli = tryCommand('hermes', ['--version'],
|
|
348
|
+
const cli = tryCommand('hermes', ['--version'], VERSION_PROBE_TIMEOUT_MS);
|
|
313
349
|
if (cli.ok) {
|
|
314
350
|
return { installed: true, method: 'hermes CLI', version: cli.stdout.trim() };
|
|
315
351
|
}
|
|
@@ -332,14 +368,23 @@ const AGENT_DEFINITIONS = [
|
|
|
332
368
|
/**
|
|
333
369
|
* Detect ALL installed agents on this machine (not just the running one).
|
|
334
370
|
*/
|
|
335
|
-
export function buildAgentInventory(homeDir = os.homedir(), env = process.env) {
|
|
371
|
+
export function buildAgentInventory(homeDir = os.homedir(), env = process.env, opts = {}) {
|
|
372
|
+
const spawnableResolver = opts.spawnableResolver ?? detectSpawnable;
|
|
336
373
|
const agents = AGENT_DEFINITIONS.map(def => {
|
|
337
374
|
const detection = def.detect(homeDir, env);
|
|
375
|
+
const spawnable = spawnableResolver(def.name);
|
|
376
|
+
// trp#427: an agent brainclaw can spawn (invoke binary on PATH) IS installed,
|
|
377
|
+
// even when the cold-start `--version` probe timed out. This decouples the
|
|
378
|
+
// dispatch decision (getInstalledAgentNames) from probe latency.
|
|
379
|
+
const installed = detection.installed || spawnable;
|
|
338
380
|
return {
|
|
339
381
|
name: def.name,
|
|
340
|
-
installed
|
|
341
|
-
detection_method: detection.
|
|
382
|
+
installed,
|
|
383
|
+
detection_method: detection.installed
|
|
384
|
+
? detection.method
|
|
385
|
+
: (spawnable ? 'spawnable: invoke binary on PATH' : detection.method),
|
|
342
386
|
version: detection.version,
|
|
387
|
+
spawnable,
|
|
343
388
|
models: def.models,
|
|
344
389
|
native_tools: def.native_tools,
|
|
345
390
|
mcp_support: def.mcp_support,
|
|
@@ -415,6 +460,8 @@ export function renderAgentInventorySummary(inventory) {
|
|
|
415
460
|
features.push('Rules');
|
|
416
461
|
if (agent.hooks_support)
|
|
417
462
|
features.push('Hooks');
|
|
463
|
+
if (agent.spawnable)
|
|
464
|
+
features.push('Spawnable');
|
|
418
465
|
lines.push(` Features: ${features.join(', ') || 'none'}`);
|
|
419
466
|
if (agent.instruction_file) {
|
|
420
467
|
lines.push(` Instructions: ${agent.instruction_file}`);
|
package/dist/core/dirty-scope.js
CHANGED
|
@@ -44,13 +44,19 @@ function defaultRunGit(cwd, args) {
|
|
|
44
44
|
return { ok: false, stdout: '' };
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* Top-level directories that are dirty as a side effect of coordination /
|
|
49
|
+
* agent tooling, never part of a dispatch's code scope:
|
|
50
|
+
* - `.brainclaw`, `.git` — coordination store + VCS metadata.
|
|
51
|
+
* - `.claude`, `.cursor`, `.codex` — per-agent local config (trp#371). A
|
|
52
|
+
* worker leaving these dirty (Claude Code settings, etc.) must not block an
|
|
53
|
+
* otherwise-safe dispatch of an unrelated code scope.
|
|
54
|
+
*/
|
|
55
|
+
const SYSTEM_DIRTY_DIRS = ['.brainclaw', '.git', '.claude', '.cursor', '.codex'];
|
|
56
|
+
/** True for coordination/store/agent-config paths that are dirty as a side effect of tooling. */
|
|
48
57
|
export function isSystemDirtyPath(p) {
|
|
49
58
|
const norm = p.replace(/\\/g, '/');
|
|
50
|
-
return norm ===
|
|
51
|
-
|| norm.startsWith('.brainclaw/')
|
|
52
|
-
|| norm === '.git'
|
|
53
|
-
|| norm.startsWith('.git/');
|
|
59
|
+
return SYSTEM_DIRTY_DIRS.some((dir) => norm === dir || norm.startsWith(dir + '/'));
|
|
54
60
|
}
|
|
55
61
|
/**
|
|
56
62
|
* Parse `git status --porcelain=v1 -z` output into a flat list of paths.
|
package/dist/core/dispatcher.js
CHANGED
|
@@ -238,6 +238,13 @@ export function buildProtocolSection(options) {
|
|
|
238
238
|
}
|
|
239
239
|
if (options?.worktreePath) {
|
|
240
240
|
parts.push(`Worktree: ${options.worktreePath}`);
|
|
241
|
+
// pln#523: tell the worker how dependencies are provisioned so it does not
|
|
242
|
+
// stall trying to install them. node_modules (and per-package node_modules in
|
|
243
|
+
// monorepos) are junction-linked from the main repo — run builds/typecheck
|
|
244
|
+
// directly. If they are missing, do NOT `npm install` in the worktree: check
|
|
245
|
+
// `.brainclaw-worktree.json` → `symlink_warnings` (a link may have failed,
|
|
246
|
+
// e.g. cross-volume) and validate the build centrally with the coordinator.
|
|
247
|
+
parts.push('Dependencies: node_modules is linked from the main repo (incl. monorepo per-package). Build/typecheck directly; if deps are missing, do NOT npm install here — see .brainclaw-worktree.json symlink_warnings and validate centrally.');
|
|
241
248
|
}
|
|
242
249
|
parts.push('');
|
|
243
250
|
// Assignment lifecycle protocol (Agent SDK)
|
|
@@ -254,6 +261,9 @@ export function buildProtocolSection(options) {
|
|
|
254
261
|
parts.push(`${options.worktreePath ? '7' : '6'}. Release the claim: bclaw_release_claim(${claimRef}, planStatus: "done") — required for hard_after gating to unblock downstream tasks`);
|
|
255
262
|
parts.push(`${options.worktreePath ? '8' : '7'}. If blocked: bclaw_assignment_update(status: "blocked", blocker: "...")`);
|
|
256
263
|
parts.push(`${options.worktreePath ? '9' : '8'}. If failed: bclaw_assignment_update(status: "failed", error_message: "...")`);
|
|
264
|
+
// pln#526: standard fallback channel — works even when MCP is unreachable
|
|
265
|
+
// (sandboxed agents). The coordinator ingests it with `brainclaw harvest`.
|
|
266
|
+
parts.push(`Final fallback (if bclaw_assignment_update / MCP is unavailable, e.g. a sandboxed agent): write LANE-RESULT.json at the worktree root — {"assignment_id":"${options.assignmentId}","status":"completed|blocked|failed","summary":"<what you did>","files_changed":["..."],"artifacts":["..."]}. The coordinator harvests it via \`brainclaw harvest ${options.assignmentId}\`.`);
|
|
257
267
|
}
|
|
258
268
|
else if (options?.claimId) {
|
|
259
269
|
parts.push('1. Call bclaw_session_start to register your session');
|
|
@@ -237,7 +237,7 @@ function renderSessionProtocol() {
|
|
|
237
237
|
'',
|
|
238
238
|
'Do NOT call `bclaw_loop(intent=open)` directly — it creates a loop structure without dispatch, so the reviewer/participant never gets the work. Use the goal entries above.',
|
|
239
239
|
'',
|
|
240
|
-
'_How to verify a dispatch actually worked:_ `execution_status="delivered_and_started"` only means the brief-ack sentinel was touched — it does NOT mean the worker is doing useful work.
|
|
240
|
+
'_How to verify a dispatch actually worked:_ `execution_status="delivered_and_started"` only means the brief-ack sentinel was touched — it does NOT mean the worker is doing useful work. (1) Call `bclaw_dispatch_status(target_id=<asgn_…|clm_…|lop_…|run_…>)` — the purpose-built facade: it resolves the linked entities, reads the runtime sentinels (`ack` / `heartbeat` / `completed` / `failed`) and the captured stdout/stderr tails, checks pid liveness, and returns a single health verdict plus a recommended next action. This is the `verify_with` target named in the coordinate/dispatch response — prefer it over assembling the picture by hand. (2) Do NOT diagnose liveness from the tracked pid yourself: on Windows an ack-wrapped spawn runs under a `cmd.exe` shell, so `agent_run.pid` is the wrapper (which exits early by design), NOT the real worker — `Get-Process -Id <pid>` reads it dead while the worker is alive and committing. Trust the sentinel-derived verdict instead; the reconciler already infers `completed` from a post-start commit on the worktree branch even when the worker never called `bclaw_assignment_update`. (3) Fallback only if the facade is unavailable: `bclaw_find(entity="agent_run", filter={assignment_id})` plus the captured streams at `.brainclaw/coordination/runtime/log/<assignment_id>.{stdout,stderr}.log` — note that `claude -p` buffers stdout until exit, so an empty log mid-run is expected; use the `heartbeat` sentinel as the live progress signal, not stdout. Full FSM tables + diagnostic decision tree in `docs/concepts/dispatch-lifecycle.md`.',
|
|
241
241
|
].join('\n');
|
|
242
242
|
}
|
|
243
243
|
function renderUserWorkflow() {
|
package/dist/core/schema.js
CHANGED
|
@@ -855,7 +855,25 @@ export const RuntimeEventTypeSchema = z.enum([
|
|
|
855
855
|
'run_interrupted',
|
|
856
856
|
'plan_cascade_to_done',
|
|
857
857
|
'candidate_harvested',
|
|
858
|
+
'lane_result_harvested',
|
|
858
859
|
]);
|
|
860
|
+
/**
|
|
861
|
+
* pln#526 — LANE-RESULT convention. A dispatched worker writes a single
|
|
862
|
+
* `LANE-RESULT.json` at its worktree root as its final step (a fallback that
|
|
863
|
+
* works even when bclaw_assignment_update / MCP is unavailable, e.g. sandboxed
|
|
864
|
+
* agents). The coordinator ingests it with `brainclaw harvest <assignment_id>`.
|
|
865
|
+
*/
|
|
866
|
+
export const LaneResultSchema = z.object({
|
|
867
|
+
assignment_id: z.string(),
|
|
868
|
+
status: z.enum(['completed', 'blocked', 'failed']),
|
|
869
|
+
summary: z.string(),
|
|
870
|
+
/** Paths or refs the worker produced (commits, files, docs). */
|
|
871
|
+
artifacts: z.array(z.string()).optional(),
|
|
872
|
+
/** Files the worker changed in the worktree. */
|
|
873
|
+
files_changed: z.array(z.string()).optional(),
|
|
874
|
+
/** Free-form notes (blockers, follow-ups). */
|
|
875
|
+
notes: z.string().optional(),
|
|
876
|
+
});
|
|
859
877
|
export const RuntimeEventSchema = z.object({
|
|
860
878
|
id: z.string(),
|
|
861
879
|
agent: z.string(),
|
package/dist/core/worktree.js
CHANGED
|
@@ -3,6 +3,9 @@ import fs from 'node:fs';
|
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { spawnSync } from 'node:child_process';
|
|
6
|
+
import yaml from 'yaml';
|
|
7
|
+
import { logger } from './logger.js';
|
|
8
|
+
import { parsePorcelainZ, isSystemDirtyPath } from './dirty-scope.js';
|
|
6
9
|
/** Normalizes a path for use in git CLI arguments (forward slashes on Windows). */
|
|
7
10
|
function gitPath(p) {
|
|
8
11
|
return p.replace(/\\/g, '/');
|
|
@@ -37,6 +40,85 @@ export function detectStackSharedPaths(projectRoot) {
|
|
|
37
40
|
}
|
|
38
41
|
return [...result];
|
|
39
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* pln#523 — read declared monorepo workspace globs from npm/yarn/bun
|
|
45
|
+
* `workspaces` (package.json) and pnpm-workspace.yaml. Returns the raw
|
|
46
|
+
* patterns (e.g. "packages/*", "apps/api"); empty when the project is not a
|
|
47
|
+
* workspace root or the manifests are absent/invalid.
|
|
48
|
+
*/
|
|
49
|
+
export function readWorkspacePatterns(projectRoot) {
|
|
50
|
+
const patterns = [];
|
|
51
|
+
// npm / yarn / bun: package.json "workspaces" (array, or { packages: [...] })
|
|
52
|
+
try {
|
|
53
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(projectRoot, 'package.json'), 'utf-8'));
|
|
54
|
+
const ws = pkg.workspaces;
|
|
55
|
+
if (Array.isArray(ws))
|
|
56
|
+
patterns.push(...ws);
|
|
57
|
+
else if (ws && Array.isArray(ws.packages))
|
|
58
|
+
patterns.push(...ws.packages);
|
|
59
|
+
}
|
|
60
|
+
catch { /* no / invalid package.json — not a node workspace root */ }
|
|
61
|
+
// pnpm: pnpm-workspace.yaml "packages"
|
|
62
|
+
try {
|
|
63
|
+
const parsed = yaml.parse(fs.readFileSync(path.join(projectRoot, 'pnpm-workspace.yaml'), 'utf-8'));
|
|
64
|
+
if (parsed && Array.isArray(parsed.packages))
|
|
65
|
+
patterns.push(...parsed.packages);
|
|
66
|
+
}
|
|
67
|
+
catch { /* no pnpm workspace file */ }
|
|
68
|
+
return [...new Set(patterns)];
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* pln#523 — resolve monorepo workspace globs to the per-package `node_modules`
|
|
72
|
+
* directories that actually exist on disk. Hoisted monorepos (all deps at the
|
|
73
|
+
* root) need only the root link from detectStackSharedPaths; this additionally
|
|
74
|
+
* covers packages that keep a LOCAL node_modules (pnpm, nohoist, partial
|
|
75
|
+
* hoisting) so a dispatched worker can build/typecheck a sub-package, not just
|
|
76
|
+
* the root — the exact gap behind a worker stalling on `tsc` in a worktree.
|
|
77
|
+
*
|
|
78
|
+
* Pattern shapes supported without a glob dependency (zero-runtime-dep policy):
|
|
79
|
+
* - exact dir: "apps/api"
|
|
80
|
+
* - single wildcard: "packages/*" → immediate child directories
|
|
81
|
+
* - deep wildcard: "packages/**" → treated as one level ("packages/*")
|
|
82
|
+
* Negations ("!pkg/excluded") are skipped — they only narrow coverage and a
|
|
83
|
+
* missing link degrades gracefully to central validation.
|
|
84
|
+
*
|
|
85
|
+
* Returns relative paths with forward slashes (e.g. "apps/api/node_modules").
|
|
86
|
+
*/
|
|
87
|
+
export function detectWorkspaceNodeModules(projectRoot) {
|
|
88
|
+
const patterns = readWorkspacePatterns(projectRoot);
|
|
89
|
+
if (patterns.length === 0)
|
|
90
|
+
return [];
|
|
91
|
+
const result = new Set();
|
|
92
|
+
const addIfHasNodeModules = (relPkgDir) => {
|
|
93
|
+
const rel = `${relPkgDir.replace(/\\/g, '/').replace(/\/+$/, '')}/node_modules`;
|
|
94
|
+
if (fs.existsSync(path.join(projectRoot, rel)))
|
|
95
|
+
result.add(rel);
|
|
96
|
+
};
|
|
97
|
+
for (const raw of patterns) {
|
|
98
|
+
const pattern = raw.trim();
|
|
99
|
+
if (!pattern || pattern.startsWith('!'))
|
|
100
|
+
continue;
|
|
101
|
+
const wildcardIdx = pattern.indexOf('*');
|
|
102
|
+
if (wildcardIdx === -1) {
|
|
103
|
+
addIfHasNodeModules(pattern);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
// Base dir = the path segment before the first wildcard.
|
|
107
|
+
const base = pattern.slice(0, wildcardIdx).replace(/\/+$/, '');
|
|
108
|
+
let children = [];
|
|
109
|
+
try {
|
|
110
|
+
children = fs
|
|
111
|
+
.readdirSync(path.join(projectRoot, base), { withFileTypes: true })
|
|
112
|
+
.filter((d) => d.isDirectory())
|
|
113
|
+
.map((d) => d.name);
|
|
114
|
+
}
|
|
115
|
+
catch { /* base dir absent — skip this pattern */ }
|
|
116
|
+
for (const child of children) {
|
|
117
|
+
addIfHasNodeModules(base ? `${base}/${child}` : child);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return [...result];
|
|
121
|
+
}
|
|
40
122
|
function canonicalizeScopePath(target) {
|
|
41
123
|
let resolved;
|
|
42
124
|
try {
|
|
@@ -191,6 +273,7 @@ export function findWorktreePathForBranch(worktrees, branchName) {
|
|
|
191
273
|
* Returns the absolute path to the newly created worktree.
|
|
192
274
|
*/
|
|
193
275
|
export function createWorktree(mainWorktreePath, branchName, options = {}) {
|
|
276
|
+
const symlinkWarnings = [];
|
|
194
277
|
const trySymlinkSharedPath = (entryName) => {
|
|
195
278
|
const sourcePath = path.join(mainWorktreePath, entryName);
|
|
196
279
|
const linkPath = path.join(targetPath, entryName);
|
|
@@ -205,8 +288,20 @@ export function createWorktree(mainWorktreePath, branchName, options = {}) {
|
|
|
205
288
|
}
|
|
206
289
|
fs.symlinkSync(sourcePath, linkPath, 'junction');
|
|
207
290
|
}
|
|
208
|
-
catch {
|
|
209
|
-
//
|
|
291
|
+
catch (err) {
|
|
292
|
+
// pln#523: do NOT swallow silently. A missing node_modules junction is
|
|
293
|
+
// exactly what leaves a dispatched worker unable to build/typecheck in its
|
|
294
|
+
// worktree (it then stalls on `tsc` or npm scripts). Record a structured
|
|
295
|
+
// warning — surfaced in the worktree sidecar + logger — instead of an
|
|
296
|
+
// invisible degradation. Linking remains best-effort (non-fatal).
|
|
297
|
+
const sameVolume = path.parse(sourcePath).root.toLowerCase() === path.parse(targetPath).root.toLowerCase();
|
|
298
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
299
|
+
const hint = sameVolume
|
|
300
|
+
? ''
|
|
301
|
+
: ' (source and worktree are on different volumes — directory junctions require the same volume; deps cannot be linked here, validate builds centrally)';
|
|
302
|
+
const msg = `Failed to link '${entryName}' into worktree: ${reason}${hint}`;
|
|
303
|
+
symlinkWarnings.push(msg);
|
|
304
|
+
logger.warn(`[worktree] ${msg}`);
|
|
210
305
|
}
|
|
211
306
|
};
|
|
212
307
|
// Guard: bare repos have no working tree
|
|
@@ -256,7 +351,15 @@ export function createWorktree(mainWorktreePath, branchName, options = {}) {
|
|
|
256
351
|
// pln#480: auto-detect shared paths from stack markers + config overrides.
|
|
257
352
|
// `dist` intentionally excluded — build outputs must be per-worktree
|
|
258
353
|
// (EBUSY during clean:dist when MCP/extension holds a handle on junction target).
|
|
259
|
-
|
|
354
|
+
// pln#523: also link per-package node_modules for JS/TS monorepos so workers
|
|
355
|
+
// can build/typecheck sub-packages, not just the root. Set
|
|
356
|
+
// BRAINCLAW_NO_LINK_DEPS=1 to disable auto dependency linking (e.g. when the
|
|
357
|
+
// worktree lives on a different volume and central validation is preferred);
|
|
358
|
+
// explicit options.sharedPaths are still honored.
|
|
359
|
+
const linkDepsDisabled = process.env.BRAINCLAW_NO_LINK_DEPS === '1';
|
|
360
|
+
const detected = linkDepsDisabled
|
|
361
|
+
? []
|
|
362
|
+
: [...detectStackSharedPaths(mainWorktreePath), ...detectWorkspaceNodeModules(mainWorktreePath)];
|
|
260
363
|
const extra = options.sharedPaths ?? [];
|
|
261
364
|
const excluded = new Set(options.excludeShared ?? []);
|
|
262
365
|
const sharedPaths = [...new Set([...detected, ...extra])].filter((p) => !excluded.has(p));
|
|
@@ -282,6 +385,10 @@ export function createWorktree(mainWorktreePath, branchName, options = {}) {
|
|
|
282
385
|
base_ref: baseRef,
|
|
283
386
|
reset_existing_branch: options.resetExistingBranch === true,
|
|
284
387
|
git_advice: 'git add ONLY specific files, NEVER git add -A.',
|
|
388
|
+
// pln#523: surface any shared-path link failures (e.g. node_modules junction
|
|
389
|
+
// that could not be created) so the worker / supervisor can see why a build
|
|
390
|
+
// might fail, instead of an invisible degradation.
|
|
391
|
+
...(symlinkWarnings.length > 0 ? { symlink_warnings: symlinkWarnings } : {}),
|
|
285
392
|
};
|
|
286
393
|
fs.writeFileSync(path.join(targetPath, '.brainclaw-worktree.json'), JSON.stringify(meta, null, 2));
|
|
287
394
|
return targetPath;
|
|
@@ -510,6 +617,30 @@ export function removeWorktree(mainWorktreePath, worktreePath, options = {}) {
|
|
|
510
617
|
export function pruneWorktrees(mainWorktreePath) {
|
|
511
618
|
runGit(['worktree', 'prune'], mainWorktreePath);
|
|
512
619
|
}
|
|
620
|
+
/**
|
|
621
|
+
* Files brainclaw itself writes into a worktree AT BIRTH — they are never user
|
|
622
|
+
* work and must not count as "uncommitted changes" when deciding whether a
|
|
623
|
+
* merged worktree can be GC'd:
|
|
624
|
+
* - `.gitignore` — copied from the main repo by createWorktree; on
|
|
625
|
+
* Windows autocrlf flags it as ` M .gitignore`,
|
|
626
|
+
* which previously made EVERY brainclaw worktree
|
|
627
|
+
* look dirty and skipped the clean forever.
|
|
628
|
+
* - `.brainclaw-worktree.json` — the sidecar metadata createWorktree writes.
|
|
629
|
+
* Combined with isSystemDirtyPath (.brainclaw/, .git/, agent config dirs).
|
|
630
|
+
*/
|
|
631
|
+
const WORKTREE_BIRTH_NOISE = new Set(['.gitignore', '.brainclaw-worktree.json']);
|
|
632
|
+
/**
|
|
633
|
+
* True when a worktree's `git status --porcelain=v1 -z` output contains ONLY
|
|
634
|
+
* brainclaw birth artifacts / coordination-store noise — i.e. no real user work
|
|
635
|
+
* would be lost by removing it. Empty output (fully clean) also returns true.
|
|
636
|
+
*/
|
|
637
|
+
export function worktreeHasOnlyBirthNoise(statusZStdout) {
|
|
638
|
+
const paths = parsePorcelainZ(statusZStdout);
|
|
639
|
+
return paths.every((p) => {
|
|
640
|
+
const norm = p.replace(/\\/g, '/');
|
|
641
|
+
return WORKTREE_BIRTH_NOISE.has(norm) || isSystemDirtyPath(norm);
|
|
642
|
+
});
|
|
643
|
+
}
|
|
513
644
|
/**
|
|
514
645
|
* Removes worktrees whose branch has been fully merged into the current branch
|
|
515
646
|
* (typically master/main after a merge). Also removes brainclaw-managed
|
|
@@ -539,10 +670,13 @@ export function cleanMergedWorktrees(mainWorktreePath, options = {}) {
|
|
|
539
670
|
if (!isMerged) {
|
|
540
671
|
continue;
|
|
541
672
|
}
|
|
542
|
-
// Check for uncommitted changes
|
|
673
|
+
// Check for uncommitted changes — but ignore brainclaw birth-noise
|
|
674
|
+
// (.gitignore autocrlf, the sidecar, coordination store). Without this,
|
|
675
|
+
// every merged brainclaw worktree looked dirty and was skipped forever,
|
|
676
|
+
// so `worktree clean` removed nothing and worktrees accumulated (pln#525).
|
|
543
677
|
if (!options.force) {
|
|
544
|
-
const status = runGit(['status', '--porcelain'], wt.path);
|
|
545
|
-
if (status.ok && status.stdout
|
|
678
|
+
const status = runGit(['status', '--porcelain=v1', '-z', '--untracked-files=normal'], wt.path);
|
|
679
|
+
if (status.ok && !worktreeHasOnlyBirthNoise(status.stdout)) {
|
|
546
680
|
result.skipped.push({ path: wt.path, reason: 'uncommitted changes' });
|
|
547
681
|
continue;
|
|
548
682
|
}
|
|
@@ -552,7 +686,12 @@ export function cleanMergedWorktrees(mainWorktreePath, options = {}) {
|
|
|
552
686
|
continue;
|
|
553
687
|
}
|
|
554
688
|
try {
|
|
555
|
-
|
|
689
|
+
// Reaching here means EITHER options.force OR the birth-noise gate above
|
|
690
|
+
// passed (no real user work). In both cases git's own `worktree remove`
|
|
691
|
+
// must be forced: otherwise it refuses on the untracked sidecar /
|
|
692
|
+
// autocrlf .gitignore that we already classified as discardable noise
|
|
693
|
+
// (pln#525 — the refusal that left every merged worktree un-GC-able).
|
|
694
|
+
removeWorktree(mainWorktreePath, wt.path, { force: true });
|
|
556
695
|
result.removed.push(wt.path);
|
|
557
696
|
}
|
|
558
697
|
catch {
|
package/dist/facts.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// Generated by scripts/emit-site-facts.mjs at build time. Do not edit manually.
|
|
2
|
-
// Source: brainclaw v1.7.
|
|
2
|
+
// Source: brainclaw v1.7.3 on 2026-06-05T21:43:11.193Z
|
|
3
3
|
export const FACTS = {
|
|
4
|
-
"version": "1.7.
|
|
5
|
-
"generated_at": "2026-06-
|
|
4
|
+
"version": "1.7.3",
|
|
5
|
+
"generated_at": "2026-06-05T21:43:11.193Z",
|
|
6
6
|
"tools": {
|
|
7
7
|
"count": 62,
|
|
8
8
|
"published_count": 61,
|
package/dist/facts.json
CHANGED