brainclaw 1.16.0 → 1.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -8
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/cli/register-review.js +2 -2
- package/dist/commands/code-map.js +4 -1
- package/dist/commands/codev.js +61 -30
- package/dist/commands/doctor.js +14 -1
- package/dist/commands/harvest.js +241 -25
- package/dist/commands/inbox.js +10 -4
- package/dist/commands/loop.js +2 -2
- package/dist/commands/loops-handlers.js +82 -1
- package/dist/commands/mcp-catalog.js +12 -4
- package/dist/commands/mcp-read-handlers.js +90 -7
- package/dist/commands/mcp-schemas.generated.js +3 -0
- package/dist/commands/mcp-write-coordination.js +159 -40
- package/dist/commands/mcp.js +11 -2
- package/dist/core/agent-capability.js +7 -2
- package/dist/core/agent-files.js +53 -2
- package/dist/core/agent-integrations.js +1 -0
- package/dist/core/agentrun-reconciler.js +171 -7
- package/dist/core/agentruns.js +6 -1
- package/dist/core/code-map/aggregate.js +473 -0
- package/dist/core/code-map/backend.js +36 -10
- package/dist/core/code-map/freshness.js +36 -1
- package/dist/core/code-map/lang/c/imports.scm +12 -0
- package/dist/core/code-map/lang/c/index.js +150 -0
- package/dist/core/code-map/lang/c/tags.scm +68 -0
- package/dist/core/code-map/lang/cpp/imports.scm +14 -0
- package/dist/core/code-map/lang/cpp/index.js +149 -0
- package/dist/core/code-map/lang/cpp/tags.scm +87 -0
- package/dist/core/code-map/lang/csharp/imports.scm +20 -0
- package/dist/core/code-map/lang/csharp/index.js +224 -0
- package/dist/core/code-map/lang/csharp/tags.scm +63 -0
- package/dist/core/code-map/lang/go/imports.scm +13 -0
- package/dist/core/code-map/lang/go/index.js +139 -0
- package/dist/core/code-map/lang/go/tags.scm +36 -0
- package/dist/core/code-map/lang/providers.js +12 -1
- package/dist/core/code-map/lang/ruby/imports.scm +24 -0
- package/dist/core/code-map/lang/ruby/index.js +198 -0
- package/dist/core/code-map/lang/ruby/tags.scm +49 -0
- package/dist/core/code-map/lang/rust/imports.scm +44 -0
- package/dist/core/code-map/lang/rust/index.js +136 -0
- package/dist/core/code-map/lang/rust/tags.scm +47 -0
- package/dist/core/code-map/query.js +229 -80
- package/dist/core/code-map/types.js +18 -0
- package/dist/core/code-map/work-section.js +8 -7
- package/dist/core/codev-responses.js +16 -0
- package/dist/core/dispatcher.js +209 -29
- package/dist/core/execution-adapters.js +29 -3
- package/dist/core/ideation-loop-close.js +124 -0
- package/dist/core/loops/artifact-resolver.js +197 -0
- package/dist/core/loops/attempt-reservation.js +576 -0
- package/dist/core/loops/commit-intent.js +494 -0
- package/dist/core/loops/facade-schema.js +48 -0
- package/dist/core/loops/impl-bind.js +144 -0
- package/dist/core/loops/index.js +1 -1
- package/dist/core/loops/iteration-engine.js +29 -0
- package/dist/core/loops/lock.js +14 -0
- package/dist/core/loops/project-resolution.js +157 -0
- package/dist/core/loops/reconcile-turn.js +369 -0
- package/dist/core/loops/result-reducers.js +88 -0
- package/dist/core/loops/store.js +46 -7
- package/dist/core/loops/types.js +139 -11
- package/dist/core/loops/verbs.js +9 -3
- package/dist/core/loops/verify-command.js +209 -0
- package/dist/core/messaging.js +58 -5
- package/dist/core/review-loop-close.js +106 -34
- package/dist/core/review-loop-turn-dispatch.js +445 -0
- package/dist/core/runtime-signals.js +68 -0
- package/dist/core/schema.js +34 -0
- package/dist/core/worktree.js +240 -22
- package/dist/facts.js +10 -10
- package/dist/facts.json +9 -9
- package/dist/wasm/tree-sitter-c.wasm +0 -0
- package/dist/wasm/tree-sitter-c_sharp.wasm +0 -0
- package/dist/wasm/tree-sitter-cpp.wasm +0 -0
- package/dist/wasm/tree-sitter-go.wasm +0 -0
- package/dist/wasm/tree-sitter-ruby.wasm +0 -0
- package/dist/wasm/tree-sitter-rust.wasm +0 -0
- package/docs/cli.md +1 -1
- package/docs/code-map.md +22 -6
- package/docs/concepts/loop-engine.md +28 -2
- package/docs/concepts/observer-protocol.md +22 -0
- package/docs/integrations/codex.md +19 -3
- package/docs/mcp-schema-changelog.md +43 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -44,7 +44,7 @@ It sits alongside your coding agents and gives them a shared state layer they ca
|
|
|
44
44
|
| **Project memory** | constraints, decisions, traps, handoffs, and layered instructions agents can resume from |
|
|
45
45
|
| **Coordination state** | shared plans, file claims (dispatched work isolated in Git Worktrees), runtime notes, and board views for active work |
|
|
46
46
|
| **Agent-ready context** | compact, prompt-sized context built from real workspace state instead of stale instructions |
|
|
47
|
-
| **Code Map** | a Tree-sitter symbol + import index (JS/TS, Python, PHP, Java) so agents ask "where is X / what should I read first" before editing, with related decisions/traps attached — `bclaw_code_find` / `bclaw_code_brief`, see [code map](docs/code-map.md) |
|
|
47
|
+
| **Code Map** | a Tree-sitter symbol + import index (11 languages — JS/TS, Python, PHP, Java, Go, Rust, C#, Ruby, C, C++) so agents ask "where is X / what should I read first" before editing, with related decisions/traps attached — `bclaw_code_find` / `bclaw_code_brief`, see [code map](docs/code-map.md) |
|
|
48
48
|
| **Native agent files** | auto-writes `CLAUDE.md`, `AGENTS.md`, `GEMINI.md`, `.cursor/rules/`, `.windsurfrules`, and similar local guidance |
|
|
49
49
|
| **Multi-turn loops** | review and ideation loops with structured phases, iteration semantics, and per-phase memory filters — see[loop engine](docs/concepts/loop-engine.md) and [ideation loop](docs/concepts/ideation-loop.md) |
|
|
50
50
|
| **Machine AI surface discovery** | detects local coding agents plus desktop AI work surfaces such as ChatGPT Desktop and Gemini CLI |
|
|
@@ -55,7 +55,7 @@ It sits alongside your coding agents and gives them a shared state layer they ca
|
|
|
55
55
|
|
|
56
56
|
## Code Map
|
|
57
57
|
|
|
58
|
-
When an agent (or you) is about to edit unfamiliar code, the first question is *"where is this, and what should I read first?"* Code Map answers it without grepping: a per-project [Tree-sitter](https://tree-sitter.github.io/) index of the symbols each file defines (functions, classes, types, React components/hooks), what it imports/exports, and how files relate — across **JS / TS / TSX, Python, PHP, and
|
|
58
|
+
When an agent (or you) is about to edit unfamiliar code, the first question is *"where is this, and what should I read first?"* Code Map answers it without grepping: a per-project [Tree-sitter](https://tree-sitter.github.io/) index of the symbols each file defines (functions, classes, types, React components/hooks), what it imports/exports, and how files relate — across **11 languages: JS / TS / TSX, Python, PHP, Java, Go, Rust, C#, Ruby, C, and C++**.
|
|
59
59
|
|
|
60
60
|
```bash
|
|
61
61
|
brainclaw code-map find useAuth # locate a symbol / component / hook by name
|
|
@@ -103,7 +103,7 @@ brainclaw is designed to sit alongside the coding agents teams are already using
|
|
|
103
103
|
| Logo | Agent | Tier | What brainclaw configures |
|
|
104
104
|
| ------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ | ---- | ----------------------------------------------------------------------- |
|
|
105
105
|
| [](https://github.com/anthropics/claude-code) | **[Claude Code](https://github.com/anthropics/claude-code)** | A | MCP + CLAUDE.md + hooks + auto-approve + permissions + /brainclaw skill |
|
|
106
|
-
| [](https://openai.com/codex/) | **[Codex](https://openai.com/codex/)** | A | MCP + AGENTS.md + skills
|
|
106
|
+
| [](https://openai.com/codex/) | **[Codex](https://openai.com/codex/)** | A | MCP + AGENTS.md + hooks + skills |
|
|
107
107
|
| [](https://cursor.com/en-US) | **[Cursor](https://cursor.com/en-US)** | A | MCP (machine) + .cursor/rules/ + hooks + skills |
|
|
108
108
|
| [](https://windsurf.com/) | **[Windsurf](https://windsurf.com/)** | A | MCP (machine) + .windsurfrules + .windsurf/rules/ |
|
|
109
109
|
| [](https://github.com/cline/cline) | **[Cline](https://github.com/cline/cline)** | A | MCP + auto-approve + .clinerules/ |
|
|
@@ -256,8 +256,8 @@ bclaw_coordinate({
|
|
|
256
256
|
});
|
|
257
257
|
|
|
258
258
|
// 4. The reviewer's verdict is harvested from its LANE-RESULT.json and the loop
|
|
259
|
-
// auto-closes on approve
|
|
260
|
-
// (bclaw_loop remains available to
|
|
259
|
+
// auto-closes on approve; request_changes re-dispatches the fix cycle
|
|
260
|
+
// autonomously. (bclaw_loop remains available to inspect or drive by hand.)
|
|
261
261
|
bclaw_loop({ intent: "get", loop_id: "lop_abc" }); // inspect status any time
|
|
262
262
|
```
|
|
263
263
|
|
|
@@ -270,7 +270,7 @@ Brainclaw's Loop Engine moves beyond manual ping-pong by formalizing multi-turn
|
|
|
270
270
|
|
|
271
271
|
Each loop maintains a structured lifecycle, explicit phases, iteration bounds, and per-phase memory filters, executed seamlessly via `bclaw_loop`.
|
|
272
272
|
|
|
273
|
-
**Autonomous convergence (pln#628 Focus 4B):** a dispatched reviewer doesn't need to be driven by hand. It writes its verdict (`review_verdict: approve | request_changes`) into its `LANE-RESULT.json`; when the coordinator harvests the lane, brainclaw records the verdict on the loop and **auto-closes it on approve** — the review loop reaches `reviewer_green` with no human ping-pong. `request_changes
|
|
273
|
+
**Autonomous convergence (pln#628 Focus 4B + pln#630):** a dispatched reviewer doesn't need to be driven by hand. It writes its verdict (`review_verdict: approve | request_changes`) into its `LANE-RESULT.json`; when the coordinator harvests the lane, brainclaw records the verdict on the loop and **auto-closes it on approve** — the review loop reaches `reviewer_green` with no human ping-pong. On `request_changes`, brainclaw **runs the fix→re-review cycle autonomously**: it bumps the round, retains the worktree, and re-dispatches — through an exactly-once turn-attempt state machine (immutable attempt records behind an atomic launch fence, on by default; kill-switch `BRAINCLAW_TURN_OWNED_REVIEW=0`) so a turn is never double-spawned, with a bounded round cap that lands on `blocked` instead of looping forever.
|
|
274
274
|
|
|
275
275
|
## Enterprise Ready: Mono-repo & Micro-services
|
|
276
276
|
|
|
@@ -319,7 +319,7 @@ Still sharp:
|
|
|
319
319
|
|
|
320
320
|
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.
|
|
321
321
|
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.
|
|
322
|
-
3. **Next.js / Turbopack dev server in a worktree** — the provisioned `node_modules` symlink points outside the worktree root, which `next dev` (Turbopack) rejects (build/tsc/vitest are fine)
|
|
322
|
+
3. **Next.js / Turbopack dev server in a worktree** — in the default `link` mode the provisioned `node_modules` symlink points outside the worktree root, which `next dev` (Turbopack) rejects (build/tsc/vitest are fine); brainclaw warns. Fix: opt into a Turbopack-compatible **per-worktree dependency mode** — set `worktree.deps_mode: install` (or `copy`) in config, or `BRAINCLAW_WORKTREE_DEPS_MODE=install`, so the worktree gets a real in-root `node_modules` (see [Multi-stack worktree](#multi-stack-worktree)).
|
|
323
323
|
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.
|
|
324
324
|
4. **Live state for hook-less agents** — supported hook-less file surfaces such as Cline, Windsurf, Continue, Antigravity/Gemini CLI, and Mistral Vibe can get live context via `.live.md` companions regenerated on session-end and handoff, not via real-time push.
|
|
325
325
|
|
|
@@ -349,12 +349,26 @@ Maven, Gradle, and Cargo are intentionally excluded — their dependency caches
|
|
|
349
349
|
|
|
350
350
|
Build outputs like `dist` are **not** symlinked — they must be per-worktree to avoid EBUSY errors when other processes hold handles on the output directory.
|
|
351
351
|
|
|
352
|
-
|
|
352
|
+
### Dependency provisioning mode (`deps_mode`)
|
|
353
|
+
|
|
354
|
+
How a worktree gets its JS `node_modules` is controlled by `deps_mode` (default `link`):
|
|
355
|
+
|
|
356
|
+
| Mode | How `node_modules` is provisioned | Turbopack / `next dev` | Cost |
|
|
357
|
+
| --------- | ------------------------------------------------------------------------------- | ---------------------- | ---- |
|
|
358
|
+
| `link` | junction/symlink to the main tree (out-of-root) | ❌ rejected | instant, zero disk |
|
|
359
|
+
| `install` | runs the detected package manager's install at the worktree root (real in-root) | ✅ works | slower, may hit network/cache |
|
|
360
|
+
| `copy` | recursively copies `node_modules` from the main tree (real in-root) | ✅ works | disk-heavy, offline |
|
|
361
|
+
| `none` | provisions nothing (central validation / manual install) | n/a | instant |
|
|
362
|
+
|
|
363
|
+
> **Next.js / Turbopack caveat.** In the default `link` mode the `node_modules` link points **outside** the worktree root. `tsc`, `vitest`, and production `build` follow it fine, but `next dev` (Turbopack) panics on it. brainclaw detects Next.js projects and surfaces a `symlink_warnings` note at worktree creation. For dev-server work, switch to `deps_mode: install` (or `copy`) — the worktree then gets a real in-root `node_modules` that Turbopack accepts. The package manager is auto-detected from the lockfile (`pnpm-lock.yaml` → pnpm, `yarn.lock` → yarn, `bun.lock*` → bun, else npm) or the `packageManager` field.
|
|
364
|
+
|
|
365
|
+
Precedence: `BRAINCLAW_WORKTREE_DEPS_MODE` (env) > `BRAINCLAW_NO_LINK_DEPS=1` (→ `none`, backward compat) > `worktree.deps_mode` (config) > `link`. Install timeout: `BRAINCLAW_WORKTREE_INSTALL_TIMEOUT_MS` (default 10 min).
|
|
353
366
|
|
|
354
367
|
Override detection in `.brainclaw/config.yaml`:
|
|
355
368
|
|
|
356
369
|
```yaml
|
|
357
370
|
worktree:
|
|
371
|
+
deps_mode: install # link (default) | install | copy | none
|
|
358
372
|
shared_paths: [".cache"] # additive to auto-detected
|
|
359
373
|
exclude_shared: ["node_modules"] # opt-out a detected entry
|
|
360
374
|
```
|
|
Binary file
|
|
@@ -103,9 +103,9 @@ export function registerReviewCommands(program) {
|
|
|
103
103
|
.option('--dry-run', 'Preview without writing events/markers')
|
|
104
104
|
.option('--worktree <path>', 'Explicit worktree path to scan (repeatable)', collect, [])
|
|
105
105
|
.option('--json', 'Output as JSON')
|
|
106
|
-
.action((assignmentId, options) => {
|
|
106
|
+
.action(async (assignmentId, options) => {
|
|
107
107
|
const globalOpts = program.opts();
|
|
108
|
-
runHarvestLane(assignmentId, { ...options, cwd: globalOpts.cwd });
|
|
108
|
+
await runHarvestLane(assignmentId, { ...options, cwd: globalOpts.cwd });
|
|
109
109
|
});
|
|
110
110
|
// --- prune-candidates ---
|
|
111
111
|
program
|
|
@@ -15,7 +15,10 @@ function badgeLine(badge) {
|
|
|
15
15
|
const detail = detailKeys.length
|
|
16
16
|
? ` (${detailKeys.map((k) => `${k}=${JSON.stringify(badge.details[k])}`).join(', ')})`
|
|
17
17
|
: '';
|
|
18
|
-
|
|
18
|
+
// pln#601 — lead with the coarse rollup (uniform across all read surfaces), then
|
|
19
|
+
// the precise status + details. `coarse` may be absent on legacy/hand-built badges.
|
|
20
|
+
const coarse = badge.coarse ? `${badge.coarse} · ` : '';
|
|
21
|
+
return `Freshness: ${coarse}${badge.status}${detail}`;
|
|
19
22
|
}
|
|
20
23
|
export async function runCodeMap(subcommand, args, options = {}) {
|
|
21
24
|
const normalized = (subcommand ?? '').trim().toLowerCase();
|
package/dist/commands/codev.js
CHANGED
|
@@ -20,6 +20,7 @@ import path from 'node:path';
|
|
|
20
20
|
import os from 'node:os';
|
|
21
21
|
import { memoryExists, readProjectVision } from '../core/io.js';
|
|
22
22
|
import { sendMessage, getThread, getThreadCount } from '../core/messaging.js';
|
|
23
|
+
import { writePhaseArtifact } from '../core/codev-responses.js';
|
|
23
24
|
import { resolveCurrentAgentName } from '../core/agent-registry.js';
|
|
24
25
|
import { CODEV_PERSONAS, listPersonas } from '../core/codev-personas.js';
|
|
25
26
|
import { buildContext } from '../core/context.js';
|
|
@@ -36,6 +37,36 @@ function toSlug(topic) {
|
|
|
36
37
|
function sanitizeForPath(slug) {
|
|
37
38
|
return slug.replace(/[<>:"/\\|?*]/g, '_');
|
|
38
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Head of each phase body kept inline in the thread message (pln#627 Phase C).
|
|
42
|
+
* Comfortably covers the largest thread readback (`getThread(truncateText: 3000)`
|
|
43
|
+
* used to build the next phase's brief), so rerouting the full body to the
|
|
44
|
+
* artifact store leaves CoDev's iterative context byte-identical while the
|
|
45
|
+
* inbox message stays a few KB instead of hundreds.
|
|
46
|
+
*/
|
|
47
|
+
export const CODEV_INBOX_HEAD_CHARS = 4000;
|
|
48
|
+
/**
|
|
49
|
+
* Persist a phase body to the artifact store and post a bounded head + pointer
|
|
50
|
+
* to the thread instead of the full text (pln#627 Phase C). Returns the
|
|
51
|
+
* sendMessage result. The full body is always readable at the returned relPath.
|
|
52
|
+
*/
|
|
53
|
+
export function postPhase(opts) {
|
|
54
|
+
const { agent, threadId, label, text, tags, cwd } = opts;
|
|
55
|
+
const artifact = writePhaseArtifact(threadId, label, text, cwd);
|
|
56
|
+
const body = text.length > CODEV_INBOX_HEAD_CHARS
|
|
57
|
+
? `${text.slice(0, CODEV_INBOX_HEAD_CHARS)}\n\n[…full phase body (${artifact.charCount} chars) at ${artifact.relPath}]`
|
|
58
|
+
: text;
|
|
59
|
+
return sendMessage({
|
|
60
|
+
from: agent,
|
|
61
|
+
to: agent,
|
|
62
|
+
type: 'rfc',
|
|
63
|
+
text: body,
|
|
64
|
+
ref: artifact.relPath,
|
|
65
|
+
thread_id: threadId,
|
|
66
|
+
payload: { artifact_path: artifact.relPath, phase: label, char_count: artifact.charCount },
|
|
67
|
+
tags,
|
|
68
|
+
}, cwd);
|
|
69
|
+
}
|
|
39
70
|
const STOP_WORDS = new Set([
|
|
40
71
|
'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
|
|
41
72
|
'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could',
|
|
@@ -106,14 +137,14 @@ export function runCodev(topic, options = {}) {
|
|
|
106
137
|
.map(t => `[${t.severity}] ${t.text}`);
|
|
107
138
|
// ── Phase 1: Exposition ───────────────────────────────────
|
|
108
139
|
const expositionText = buildExposition(topic, vision, relatedPlans, constraints, traps);
|
|
109
|
-
const opening =
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
140
|
+
const opening = postPhase({
|
|
141
|
+
agent,
|
|
142
|
+
threadId,
|
|
143
|
+
label: 'exposition',
|
|
113
144
|
text: expositionText,
|
|
114
|
-
thread_id: threadId,
|
|
115
145
|
tags: ['codev', 'phase:exposition'],
|
|
116
|
-
|
|
146
|
+
cwd,
|
|
147
|
+
});
|
|
117
148
|
// ── Resolve spawn agents ──────────────────────────────────
|
|
118
149
|
let spawnAgents = [];
|
|
119
150
|
if (options.spawn) {
|
|
@@ -238,14 +269,14 @@ export function runCodev(topic, options = {}) {
|
|
|
238
269
|
// Each spawned agent gets the exposition + their persona brief — that's enough context.
|
|
239
270
|
const threadHistory = options.spawn ? [] : getThread(threadId, cwd, { truncateText: 2000 });
|
|
240
271
|
const brief = buildConsultantBrief(persona, expositionText, 'clarification', threadHistory);
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
272
|
+
postPhase({
|
|
273
|
+
agent,
|
|
274
|
+
threadId,
|
|
275
|
+
label: `clarification_${persona.name}`,
|
|
245
276
|
text: brief,
|
|
246
|
-
thread_id: threadId,
|
|
247
277
|
tags: ['codev', 'phase:clarification', `persona:${persona.name}`],
|
|
248
|
-
|
|
278
|
+
cwd,
|
|
279
|
+
});
|
|
249
280
|
if (options.spawn && spawnAgents.length > 0) {
|
|
250
281
|
const targetAgent = spawnAgents[i % spawnAgents.length];
|
|
251
282
|
console.log(` → ${persona.name} (clarification) → ${targetAgent.name}`);
|
|
@@ -267,28 +298,28 @@ export function runCodev(topic, options = {}) {
|
|
|
267
298
|
const clarificationMessages = getThread(threadId, cwd, { truncateText: 3000 })
|
|
268
299
|
.filter(m => m.tags?.includes('phase:clarification'));
|
|
269
300
|
const contractText = buildContract(topic, clarificationMessages, decisions, constraints, traps);
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
301
|
+
postPhase({
|
|
302
|
+
agent,
|
|
303
|
+
threadId,
|
|
304
|
+
label: 'contract',
|
|
274
305
|
text: contractText,
|
|
275
|
-
thread_id: threadId,
|
|
276
306
|
tags: ['codev', 'phase:contract'],
|
|
277
|
-
|
|
307
|
+
cwd,
|
|
308
|
+
});
|
|
278
309
|
// ── Phase 4: Consultation briefs ──────────────────────────
|
|
279
310
|
for (let i = 0; i < personas.length; i++) {
|
|
280
311
|
const persona = personas[i];
|
|
281
312
|
// For spawned agents, include only the contract (not full history) to avoid message explosion
|
|
282
313
|
const threadHistory = options.spawn ? [] : getThread(threadId, cwd, { truncateText: 2000 });
|
|
283
314
|
const brief = buildConsultantBrief(persona, expositionText, 'consultation', threadHistory);
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
315
|
+
postPhase({
|
|
316
|
+
agent,
|
|
317
|
+
threadId,
|
|
318
|
+
label: `consultation_${persona.name}`,
|
|
288
319
|
text: brief,
|
|
289
|
-
thread_id: threadId,
|
|
290
320
|
tags: ['codev', 'phase:consultation', `persona:${persona.name}`],
|
|
291
|
-
|
|
321
|
+
cwd,
|
|
322
|
+
});
|
|
292
323
|
if (options.spawn && spawnAgents.length > 0) {
|
|
293
324
|
const targetAgent = spawnAgents[i % spawnAgents.length];
|
|
294
325
|
console.log(` → ${persona.name} (consultation) → ${targetAgent.name}`);
|
|
@@ -310,14 +341,14 @@ export function runCodev(topic, options = {}) {
|
|
|
310
341
|
const consultationMessages = getThread(threadId, cwd, { truncateText: 3000 })
|
|
311
342
|
.filter(m => m.tags?.includes('phase:consultation'));
|
|
312
343
|
const synthesisText = buildSynthesis(topic, consultationMessages);
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
344
|
+
postPhase({
|
|
345
|
+
agent,
|
|
346
|
+
threadId,
|
|
347
|
+
label: 'synthesis',
|
|
317
348
|
text: synthesisText,
|
|
318
|
-
thread_id: threadId,
|
|
319
349
|
tags: ['codev', 'phase:synthesis'],
|
|
320
|
-
|
|
350
|
+
cwd,
|
|
351
|
+
});
|
|
321
352
|
// ── Output ────────────────────────────────────────────────
|
|
322
353
|
if (options.json) {
|
|
323
354
|
console.log(JSON.stringify({
|
package/dist/commands/doctor.js
CHANGED
|
@@ -497,6 +497,7 @@ export function runDispatchHealthCheck(options = {}) {
|
|
|
497
497
|
const inferred_completed = [];
|
|
498
498
|
const health_check_unverified = [];
|
|
499
499
|
const inferred_failed = [];
|
|
500
|
+
const inferred_cancelled = [];
|
|
500
501
|
let no_op_open = 0;
|
|
501
502
|
for (const result of results) {
|
|
502
503
|
const run = loadAgentRun(result.run_id, options.cwd);
|
|
@@ -525,6 +526,9 @@ export function runDispatchHealthCheck(options = {}) {
|
|
|
525
526
|
case 'inferred_failed':
|
|
526
527
|
inferred_failed.push(summary);
|
|
527
528
|
break;
|
|
529
|
+
case 'inferred_cancelled':
|
|
530
|
+
inferred_cancelled.push(summary);
|
|
531
|
+
break;
|
|
528
532
|
case 'no_op':
|
|
529
533
|
no_op_open += 1;
|
|
530
534
|
break;
|
|
@@ -536,6 +540,7 @@ export function runDispatchHealthCheck(options = {}) {
|
|
|
536
540
|
inferred_completed,
|
|
537
541
|
health_check_unverified,
|
|
538
542
|
inferred_failed,
|
|
543
|
+
inferred_cancelled,
|
|
539
544
|
no_op_open,
|
|
540
545
|
exit_code: inferred_failed.length > 0 ? 1 : 0,
|
|
541
546
|
};
|
|
@@ -565,7 +570,15 @@ function renderDispatchHealthHumanReport(report) {
|
|
|
565
570
|
}
|
|
566
571
|
lines.push('');
|
|
567
572
|
}
|
|
568
|
-
if (report.
|
|
573
|
+
if (report.inferred_cancelled.length > 0) {
|
|
574
|
+
lines.push(`⊘ ${report.inferred_cancelled.length} turn-owned run(s) cancelled on lease expiry (reserved, never launched):`);
|
|
575
|
+
for (const r of report.inferred_cancelled) {
|
|
576
|
+
lines.push(` - ${r.run_id} ${r.agent} (${r.scope}) — ${r.reason}`);
|
|
577
|
+
}
|
|
578
|
+
lines.push('');
|
|
579
|
+
}
|
|
580
|
+
if (report.inferred_failed.length === 0 && report.inferred_completed.length === 0
|
|
581
|
+
&& report.health_check_unverified.length === 0 && report.inferred_cancelled.length === 0) {
|
|
569
582
|
lines.push(`✔ No dispatch issues detected (${report.no_op_open} open run(s) within grace window or already healthy).`);
|
|
570
583
|
}
|
|
571
584
|
else {
|
package/dist/commands/harvest.js
CHANGED
|
@@ -23,6 +23,80 @@ import { loadClaim, releaseClaimsCascade, logCascadeReleaseResult } from '../cor
|
|
|
23
23
|
import { getCapabilityProfile, dispatchCanCommit } from '../core/agent-capability.js';
|
|
24
24
|
import { commitWorktreeOnBehalf, worktreesBaseDir, resolveGitToplevel } from '../core/worktree.js';
|
|
25
25
|
import { closeReviewLoopFromLaneResult } from '../core/review-loop-close.js';
|
|
26
|
+
import { closeIdeationLoopFromLaneResult } from '../core/ideation-loop-close.js';
|
|
27
|
+
import { dispatchReviewLoopTurn, turnOwnedReviewEnabled } from '../core/review-loop-turn-dispatch.js';
|
|
28
|
+
import { reconcileTurn } from '../core/loops/reconcile-turn.js';
|
|
29
|
+
import { findReservationByAssignmentId } from '../core/loops/attempt-reservation.js';
|
|
30
|
+
import { readCompletionSignals } from '../core/runtime-signals.js';
|
|
31
|
+
/**
|
|
32
|
+
* pln#630 PR3a — finalize a TURN-OWNED review lane via the exactly-once `reconcileTurn`
|
|
33
|
+
* instead of the legacy `closeReviewLoopFromLaneResult`. Returns `undefined` for a legacy
|
|
34
|
+
* (non-reserved) lane so the caller runs the unchanged legacy path — this is the
|
|
35
|
+
* exactly-one-finalizer discriminator: a lane is turn-owned iff a reservation OWNS its
|
|
36
|
+
* assignment_id (only the turn-owned dispatch writes a reservation file).
|
|
37
|
+
*
|
|
38
|
+
* Evidence sourcing (the load-bearing subtlety): a real reviewer's LANE-RESULT.json is
|
|
39
|
+
* KEYLESS — the review brief never asks the worker to echo turn_id/run_id/nonce — so
|
|
40
|
+
* read-strict `reconcileTurn` (which matches lane.{turn_id,run_id,nonce} against the
|
|
41
|
+
* attempt) would REJECT it. We source the keys authoritatively: turn_id + run_id are
|
|
42
|
+
* deterministic from the reservation, and the NONCE — the non-derivable proof that THIS
|
|
43
|
+
* launch generation actually ran — comes from the coordinator's completion SENTINEL
|
|
44
|
+
* (written mechanically by the ack-wrapper with the launch-grant token). A caller/test
|
|
45
|
+
* that already supplies keyed lanes is honored (lane.* wins); a stale generation's
|
|
46
|
+
* sentinel carries the old token → still rejected, preserving the anti-stale guarantee.
|
|
47
|
+
*/
|
|
48
|
+
/**
|
|
49
|
+
* The turn-owned FINALIZATION discriminator (pln#630, review Finding 1). A lane finalizes via
|
|
50
|
+
* the exactly-once reconcileTurn ONLY if a committed reservation OWNS it AND turn-keyed evidence
|
|
51
|
+
* (the nonce) is available — from the lane or the coordinator's completion SENTINEL. Without the
|
|
52
|
+
* nonce, reconcileTurn's read-strict gate can NEVER converge: this is reachable in production
|
|
53
|
+
* when a turn-owned dispatch WON the fence but did not ack-wrap-spawn (inbox_only / IDE-only
|
|
54
|
+
* reviewer, command_ready_manual, capacity cap, BRAINCLAW_NO_SPAWN, worktree-creation failure) —
|
|
55
|
+
* it minted a reservation but no sentinel will ever be written. Returning undefined there routes
|
|
56
|
+
* the lane to the LEGACY presence-based closer so the loop still converges instead of stalling
|
|
57
|
+
* forever. This is SAFE: the exactly-once SPAWN guarantee is enforced at DISPATCH by the launch
|
|
58
|
+
* fence (already run), so using legacy FINALIZATION for a sentinel-less lane reintroduces no
|
|
59
|
+
* double-spawn; and a sentinel that lands after a legacy close makes a later reconcile a
|
|
60
|
+
* terminal-loop idempotent no-op.
|
|
61
|
+
*/
|
|
62
|
+
function turnOwnedLaneEvidence(lane, cwd) {
|
|
63
|
+
const reservation = findReservationByAssignmentId(lane.assignment_id, cwd);
|
|
64
|
+
if (!reservation)
|
|
65
|
+
return undefined; // legacy lane (no reservation)
|
|
66
|
+
const nonce = lane.nonce ?? readCompletionSignals(cwd, reservation.child_ids.assignment_id).completed?.nonce;
|
|
67
|
+
if (!nonce)
|
|
68
|
+
return undefined; // reservation but NO turn-keyed evidence → legacy finalization
|
|
69
|
+
return { reservation, nonce };
|
|
70
|
+
}
|
|
71
|
+
function reconcileTurnOwnedReviewLane(lane, cwd) {
|
|
72
|
+
const ev = turnOwnedLaneEvidence(lane, cwd);
|
|
73
|
+
if (!ev)
|
|
74
|
+
return undefined; // legacy lane OR no turn-keyed evidence — caller runs the legacy path
|
|
75
|
+
const { reservation, nonce } = ev;
|
|
76
|
+
const enrichedLane = {
|
|
77
|
+
...lane,
|
|
78
|
+
turn_id: lane.turn_id ?? reservation.turn_id,
|
|
79
|
+
run_id: lane.run_id ?? reservation.child_ids.run_id,
|
|
80
|
+
nonce,
|
|
81
|
+
};
|
|
82
|
+
const result = reconcileTurn({ turn_id: reservation.turn_id, lane: enrichedLane, cwd });
|
|
83
|
+
return { reservation, result };
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Map a `reconcileTurn` result onto the `ReviewLoopCloseResult` shape harvest records for
|
|
87
|
+
* observability (entry.review_loop / CLI). No keep_claim / next_turn: the request_changes
|
|
88
|
+
* turn-owned re-dispatch (fix cycle) is pln#630 PR3b — deferred and non-corrupting (the
|
|
89
|
+
* loop stays open awaiting its next turn, identical to the legacy asymmetric path).
|
|
90
|
+
*/
|
|
91
|
+
function reconcileToReviewLoopResult(reservation, rr, lane) {
|
|
92
|
+
return {
|
|
93
|
+
loop_id: reservation.loop_id,
|
|
94
|
+
verdict: lane.review_verdict === 'request_changes' ? 'request_changes' : 'approve',
|
|
95
|
+
action: rr.auto_closed ? 'closed' : rr.reconciled ? 'advanced' : 'noop',
|
|
96
|
+
reason: rr.reason,
|
|
97
|
+
loop_status: rr.loop_status,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
26
100
|
/**
|
|
27
101
|
* Auto-detect all worktree directories under the brainclaw-managed base dir.
|
|
28
102
|
* Returns subdirectories that exist on disk (may or may not have an inbox).
|
|
@@ -278,10 +352,27 @@ export function harvestLaneResults(options = {}) {
|
|
|
278
352
|
// (a terminal loop is a no-op; a stuck approve is resumed), so firing it here
|
|
279
353
|
// AND in integrateLaneResults is safe — and it runs BEFORE the harvested
|
|
280
354
|
// marker short-circuits below, so a re-harvest still resumes a stuck loop.
|
|
355
|
+
// PR2: cycleOnRequestChanges=false — the report path only closes on approve;
|
|
356
|
+
// it must NOT advance a request_changes cycle it cannot follow through on
|
|
357
|
+
// (no re-dispatch, no claim retention). `harvest --integrate` owns the cycle.
|
|
281
358
|
try {
|
|
282
359
|
const laneAssignment = loadAssignment(lane.assignment_id, cwd);
|
|
283
|
-
if (laneAssignment)
|
|
284
|
-
|
|
360
|
+
if (laneAssignment) {
|
|
361
|
+
// pln#630 PR3a — a TURN-OWNED review lane (reservation + turn-keyed evidence) is finalized
|
|
362
|
+
// ONLY by reconcileTurn on the `--integrate` path (which owns claim/worktree teardown). The
|
|
363
|
+
// report path must NOT pre-empt it with a legacy close (which would terminalize the loop and
|
|
364
|
+
// strand the turn-owned run). So skip the legacy review-close for such a lane. Kill-switch
|
|
365
|
+
// (=0), a legacy lane (no reservation), OR a reservation WITHOUT evidence (review Finding 1:
|
|
366
|
+
// an inbox_only/non-ack-wrapped dispatch that never wrote a sentinel) → false → the lane
|
|
367
|
+
// takes the unchanged legacy close so it still converges. Ideation stays legacy (review-only).
|
|
368
|
+
const laneIsTurnOwned = turnOwnedReviewEnabled() && !!turnOwnedLaneEvidence(lane, cwd);
|
|
369
|
+
if (!laneIsTurnOwned) {
|
|
370
|
+
closeReviewLoopFromLaneResult(laneAssignment, lane, agent, cwd, { cycleOnRequestChanges: false });
|
|
371
|
+
}
|
|
372
|
+
// pln#521 P2-bis — the ideation analog: a critic lane records its critique +
|
|
373
|
+
// advances the ideation loop. Returns undefined for non-ideate scopes (no-op here).
|
|
374
|
+
closeIdeationLoopFromLaneResult(laneAssignment, lane, agent, cwd);
|
|
375
|
+
}
|
|
285
376
|
}
|
|
286
377
|
catch { /* never block harvest on loop-close */ }
|
|
287
378
|
const marker = laneHarvestedMarkerPath(cwd, lane.assignment_id);
|
|
@@ -391,7 +482,7 @@ function forceCompleteAssignment(assignmentId, artifacts, statusReason, actor, c
|
|
|
391
482
|
export function integrateLaneResults(options = {}) {
|
|
392
483
|
const cwd = options.cwd ?? process.cwd();
|
|
393
484
|
const actor = options.agent ?? 'coordinator';
|
|
394
|
-
const result = { integrated: [], skipped: [], errors: [] };
|
|
485
|
+
const result = { integrated: [], skipped: [], errors: [], next_turns: [] };
|
|
395
486
|
const worktreePaths = resolveLaneScanPaths(options, cwd);
|
|
396
487
|
for (const worktreePath of worktreePaths) {
|
|
397
488
|
const file = getLaneResultPath(worktreePath);
|
|
@@ -459,26 +550,122 @@ export function integrateLaneResults(options = {}) {
|
|
|
459
550
|
...entry.files_changed.slice(0, 50).map((f) => ({ type: 'file', ref: f })),
|
|
460
551
|
];
|
|
461
552
|
entry.assignment_completed = forceCompleteAssignment(lane.assignment_id, artifacts, `pln#534 on-behalf integration: ${lane.summary.slice(0, 120)}`, actor, cwd);
|
|
462
|
-
//
|
|
463
|
-
//
|
|
464
|
-
//
|
|
465
|
-
//
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
553
|
+
// pln#628 Focus 4B — map this lane onto its review loop BEFORE deciding
|
|
554
|
+
// teardown: PR1 records the verdict + advances (auto-close on approve);
|
|
555
|
+
// PR2 continues the fix cycle on request_changes (bump round, emit a
|
|
556
|
+
// next_turn) unless the iteration cap is hit. This is the --integrate
|
|
557
|
+
// path, so it MAY cycle (it can re-dispatch AND retain the claim). No-op
|
|
558
|
+
// for non-review lanes / lanes without a verdict; never throws.
|
|
559
|
+
// pln#521 P2-bis — the ideation analog (record critique + advance the ideation
|
|
560
|
+
// loop). Turn-owned is REVIEW-ONLY (pln#630), so ideation always uses the legacy
|
|
561
|
+
// path. It is independent of the review-loop close (a lane's scope is review-loop
|
|
562
|
+
// XOR ideate-loop), so it runs once here regardless of the branch below.
|
|
563
|
+
const ideationClose = closeIdeationLoopFromLaneResult(assignment, lane, actor, cwd);
|
|
564
|
+
if (ideationClose) {
|
|
565
|
+
reasons.push(`ideate-loop ${ideationClose.loop_id}: ${ideationClose.action} — ${ideationClose.reason}`);
|
|
566
|
+
}
|
|
567
|
+
// pln#630 PR3a — a TURN-OWNED review lane finalizes via the exactly-once
|
|
568
|
+
// reconcileTurn, which REPLACES the legacy closer + teardown gate for this lane
|
|
569
|
+
// (exactly-one finalizer per lane). Kill-switch (=0), a legacy (non-reserved) lane, OR a
|
|
570
|
+
// reservation with NO turn-keyed evidence (review Finding 1) → `turnOwned` is undefined
|
|
571
|
+
// and the unchanged legacy `else` block runs so the loop still converges.
|
|
572
|
+
const turnOwned = turnOwnedReviewEnabled()
|
|
573
|
+
? reconcileTurnOwnedReviewLane(lane, cwd)
|
|
574
|
+
: undefined;
|
|
575
|
+
if (turnOwned) {
|
|
576
|
+
const { reservation, result: rr } = turnOwned;
|
|
577
|
+
entry.review_loop = reconcileToReviewLoopResult(reservation, rr, lane);
|
|
578
|
+
reasons.push(`turn-owned reconcile ${reservation.loop_id}: ${entry.review_loop.action} — ${rr.reason}${rr.conflict ? ' [CONFLICT — held]' : ''}`);
|
|
579
|
+
// pln#630 PR3b — a symmetric request_changes bumped the round + retained the claim
|
|
580
|
+
// and handed back the next fix-cycle turn. Push it exactly like the legacy path so
|
|
581
|
+
// the existing async re-dispatch loop spawns round N+1 into the reused worktree. The
|
|
582
|
+
// fresh iteration means deriveTurnId mints a new turn_id → the launch fence admits
|
|
583
|
+
// exactly one spawn (a stale re-emit of the same turn_id is denied).
|
|
584
|
+
if (rr.next_turn) {
|
|
585
|
+
result.next_turns.push({ loop_id: reservation.loop_id, ...rr.next_turn });
|
|
586
|
+
}
|
|
587
|
+
// Claim/run/assignment settling is OWNED by reconcileTurn, so we do NOT run the
|
|
588
|
+
// legacy teardown gate — just reflect the resulting claim state. Settlement
|
|
589
|
+
// semantics (reconcile-turn.ts, review #1): an ACCEPTED lane — approve OR
|
|
590
|
+
// request_changes, both settle the slot 'done' — completes the assignment AND
|
|
591
|
+
// releases the claim; only a REJECTED/superseded lane (evidence mismatch/absent,
|
|
592
|
+
// §13 R4 conflict) leaves the claim intact for a retry. No next_turns push: the
|
|
593
|
+
// request_changes fix-cycle re-dispatch — AND re-establishing the claim/worktree
|
|
594
|
+
// that the release above implies — is pln#630 PR3b (deferred; the loop stays open
|
|
595
|
+
// awaiting its next turn, so nothing is corrupted, only not-yet-autonomous).
|
|
596
|
+
try {
|
|
597
|
+
entry.claim_released = loadClaim(assignment.claim_id, cwd)?.status === 'released';
|
|
598
|
+
}
|
|
599
|
+
catch {
|
|
600
|
+
entry.claim_released = false;
|
|
601
|
+
}
|
|
602
|
+
// A turn-owned lane that reached here HAS turn-keyed evidence (turnOwnedLaneEvidence
|
|
603
|
+
// gated on a present nonce — the missing-sentinel/non-spawn case already fell back to
|
|
604
|
+
// legacy, review Finding 1). So a non-convergence here means the evidence MISMATCHED
|
|
605
|
+
// the live attempt (stale/superseded generation) — which must NOT fall back to legacy
|
|
606
|
+
// (that would converge on evidence for a DIFFERENT generation). Emit an observable
|
|
607
|
+
// event so an operator/doctor sees the (correctly) withheld convergence. (An R4
|
|
608
|
+
// conflict already journals its own event, so it is excluded here.)
|
|
609
|
+
if (!rr.reconciled && !rr.conflict && lane.review_verdict) {
|
|
610
|
+
try {
|
|
611
|
+
createRuntimeEvent({
|
|
612
|
+
agent: actor,
|
|
613
|
+
event_type: 'run_blocked',
|
|
614
|
+
text: `harvest: turn-owned review lane ${lane.assignment_id} carried verdict '${lane.review_verdict}' but reconcile did not converge (${rr.reason}); loop ${reservation.loop_id} left OPEN — needs the completion sentinel or a manual turn`,
|
|
615
|
+
tags: ['harvest', 'reconcile', 'turn-owned', 'unconverged'],
|
|
616
|
+
assignment_id: lane.assignment_id,
|
|
617
|
+
run_id: reservation.child_ids.run_id,
|
|
618
|
+
status_reason: 'turn_owned_evidence_unaccepted',
|
|
619
|
+
}, cwd);
|
|
620
|
+
}
|
|
621
|
+
catch { /* observability best-effort */ }
|
|
622
|
+
}
|
|
472
623
|
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
624
|
+
else {
|
|
625
|
+
const loopClose = closeReviewLoopFromLaneResult(assignment, lane, actor, cwd);
|
|
626
|
+
if (loopClose) {
|
|
627
|
+
entry.review_loop = loopClose;
|
|
628
|
+
reasons.push(`review-loop ${loopClose.loop_id}: ${loopClose.action} — ${loopClose.reason}`);
|
|
629
|
+
if (loopClose.next_turn) {
|
|
630
|
+
result.next_turns.push({ loop_id: loopClose.loop_id, ...loopClose.next_turn });
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
// PR2 claim-teardown gate. Skip the release when either:
|
|
634
|
+
// (a) keep_claim — the symmetric fix cycle reuses the claim/worktree for
|
|
635
|
+
// the re-dispatched turn (commits accumulate on one branch); or
|
|
636
|
+
// (b) Codex review P0 — an idempotent re-harvest of an OLD lane whose
|
|
637
|
+
// loop is still OPEN returns a `noop` (the reviewer slot is now bound
|
|
638
|
+
// to a NEWER assignment under an active cycle). Releasing here would
|
|
639
|
+
// tear down the reused claim/worktree out from under the live turn
|
|
640
|
+
// and strand the fix cycle. The loop machinery owns the lifecycle
|
|
641
|
+
// while it is open; only a terminal close (approve/blocked, action
|
|
642
|
+
// 'closed') or an asymmetric hand-off ('advanced' without keep_claim)
|
|
643
|
+
// releases here. A `noop` on a TERMINAL loop still releases (safe —
|
|
644
|
+
// the closing pass already released, so this is a no-op).
|
|
645
|
+
const loopStillOpen = loopClose?.loop_status !== undefined &&
|
|
646
|
+
!['completed', 'cancelled', 'blocked'].includes(loopClose.loop_status);
|
|
647
|
+
const keepClaimAlive = loopClose?.keep_claim === true || (loopClose?.action === 'noop' && loopStillOpen);
|
|
648
|
+
if (keepClaimAlive) {
|
|
649
|
+
// The next_turn spawn (async) is awaited by runHarvestLane. The
|
|
650
|
+
// assignment for THIS turn is still completed above.
|
|
651
|
+
entry.claim_released = false;
|
|
652
|
+
reasons.push(loopClose?.keep_claim
|
|
653
|
+
? 'claim kept alive for review fix-cycle re-dispatch (PR2)'
|
|
654
|
+
: 'claim left intact — idempotent re-harvest on an active review loop (no strand)');
|
|
655
|
+
}
|
|
656
|
+
else {
|
|
657
|
+
// trp#928 — use the cascade helper (was releaseClaimWithCascade — same
|
|
658
|
+
// logic for the last-claim rule but the cascade wrapper LOGS per-claim,
|
|
659
|
+
// so a silent ownership failure is observable in the runtime event log
|
|
660
|
+
// rather than only in this in-memory `reasons` string).
|
|
661
|
+
const cascade = releaseClaimsCascade([assignment.claim_id], { cwd, planStatus: 'done' });
|
|
662
|
+
logCascadeReleaseResult({ actor, trigger: 'harvest_integrate', assignment_id: lane.assignment_id, claim_id: assignment.claim_id, cascade, cwd });
|
|
663
|
+
const claimEntry = cascade.entries[0];
|
|
664
|
+
entry.claim_released = claimEntry?.released === true;
|
|
665
|
+
if (claimEntry && !claimEntry.released) {
|
|
666
|
+
reasons.push(`claim release ${claimEntry.reason}${claimEntry.error ? `: ${claimEntry.error}` : ''}`);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
482
669
|
}
|
|
483
670
|
}
|
|
484
671
|
else {
|
|
@@ -697,7 +884,7 @@ export function harvestOrphaned(options) {
|
|
|
697
884
|
}
|
|
698
885
|
return report;
|
|
699
886
|
}
|
|
700
|
-
export function runHarvestLane(assignmentId, options = {}) {
|
|
887
|
+
export async function runHarvestLane(assignmentId, options = {}) {
|
|
701
888
|
const cwd = options.cwd ?? process.cwd();
|
|
702
889
|
if (!memoryExists(cwd)) {
|
|
703
890
|
console.error('Error: .brainclaw/ not found. Run `brainclaw init` first.');
|
|
@@ -759,8 +946,30 @@ export function runHarvestLane(assignmentId, options = {}) {
|
|
|
759
946
|
dryRun: options.dryRun,
|
|
760
947
|
cwd,
|
|
761
948
|
});
|
|
949
|
+
// pln#628 Focus 4B PR2 — spawn the review fix-cycle turns the sync integrate
|
|
950
|
+
// pass emitted. Re-dispatches the SAME reviewer into the SAME (kept) worktree
|
|
951
|
+
// to apply the requested changes + re-review. Dry-run only reports them.
|
|
952
|
+
const dispatchedTurns = [];
|
|
953
|
+
if (!options.dryRun) {
|
|
954
|
+
for (const nt of integ.next_turns) {
|
|
955
|
+
const dispatched = await dispatchReviewLoopTurn({
|
|
956
|
+
loopId: nt.loop_id,
|
|
957
|
+
slot: { slot_id: nt.slot_id, role: nt.role, agent: nt.agent, agent_id: nt.agent_id },
|
|
958
|
+
phase: nt.phase,
|
|
959
|
+
task: nt.task,
|
|
960
|
+
dispatcherAgent: options.agent ?? 'coordinator',
|
|
961
|
+
cwd,
|
|
962
|
+
// NO worktreeBaseRef: reuse the kept worktree so the fixes accumulate;
|
|
963
|
+
// pinning a ref would reset the branch and wipe prior-round commits.
|
|
964
|
+
});
|
|
965
|
+
dispatchedTurns.push({
|
|
966
|
+
loop_id: nt.loop_id, agent: nt.agent, iteration: nt.iteration,
|
|
967
|
+
execution_status: dispatched.execution_status, error: dispatched.error,
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
}
|
|
762
971
|
if (options.json) {
|
|
763
|
-
console.log(JSON.stringify(integ, null, 2));
|
|
972
|
+
console.log(JSON.stringify({ ...integ, dispatched_turns: dispatchedTurns }, null, 2));
|
|
764
973
|
return;
|
|
765
974
|
}
|
|
766
975
|
const dry = options.dryRun ? ' (dry-run)' : '';
|
|
@@ -787,7 +996,14 @@ export function runHarvestLane(assignmentId, options = {}) {
|
|
|
787
996
|
}
|
|
788
997
|
for (const err of integ.errors)
|
|
789
998
|
console.error(` ✗ ${err}`);
|
|
790
|
-
|
|
999
|
+
for (const dt of dispatchedTurns) {
|
|
1000
|
+
const status = dt.error ? `error: ${dt.error}` : (dt.execution_status ?? 'unknown');
|
|
1001
|
+
console.log(` ↻ Fix-cycle re-dispatch [${dt.loop_id}] round ${dt.iteration} → ${dt.agent} (${status})`);
|
|
1002
|
+
}
|
|
1003
|
+
if (options.dryRun && integ.next_turns.length > 0) {
|
|
1004
|
+
console.log(` (dry-run) ${integ.next_turns.length} fix-cycle turn(s) would be re-dispatched.`);
|
|
1005
|
+
}
|
|
1006
|
+
console.log(`\n✔ Lane integrate complete${dry}: ${integ.integrated.length} integrated, ${dispatchedTurns.length} re-dispatched, ${integ.errors.length} error(s).`);
|
|
791
1007
|
return;
|
|
792
1008
|
}
|
|
793
1009
|
const result = harvestLaneResults({
|