brainclaw 1.17.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 +5 -5
- package/dist/brainclaw-vscode.vsix +0 -0
- 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 +196 -42
- 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/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 +176 -22
- 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 +5 -2
- package/dist/core/review-loop-turn-dispatch.js +290 -28
- package/dist/core/runtime-signals.js +68 -0
- package/dist/core/schema.js +24 -0
- package/dist/core/worktree.js +24 -0
- package/dist/facts.js +9 -9
- package/dist/facts.json +8 -8
- 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 +24 -0
- package/docs/concepts/observer-protocol.md +22 -0
- 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
|
|
@@ -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
|
|
|
Binary file
|
|
@@ -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,7 +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 {
|
|
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
|
+
}
|
|
27
100
|
/**
|
|
28
101
|
* Auto-detect all worktree directories under the brainclaw-managed base dir.
|
|
29
102
|
* Returns subdirectories that exist on disk (may or may not have an inbox).
|
|
@@ -284,8 +357,22 @@ export function harvestLaneResults(options = {}) {
|
|
|
284
357
|
// (no re-dispatch, no claim retention). `harvest --integrate` owns the cycle.
|
|
285
358
|
try {
|
|
286
359
|
const laneAssignment = loadAssignment(lane.assignment_id, cwd);
|
|
287
|
-
if (laneAssignment)
|
|
288
|
-
|
|
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
|
+
}
|
|
289
376
|
}
|
|
290
377
|
catch { /* never block harvest on loop-close */ }
|
|
291
378
|
const marker = laneHarvestedMarkerPath(cwd, lane.assignment_id);
|
|
@@ -469,48 +556,115 @@ export function integrateLaneResults(options = {}) {
|
|
|
469
556
|
// next_turn) unless the iteration cap is hit. This is the --integrate
|
|
470
557
|
// path, so it MAY cycle (it can re-dispatch AND retain the claim). No-op
|
|
471
558
|
// for non-review lanes / lanes without a verdict; never throws.
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
}
|
|
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}`);
|
|
479
566
|
}
|
|
480
|
-
//
|
|
481
|
-
//
|
|
482
|
-
//
|
|
483
|
-
//
|
|
484
|
-
//
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
//
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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
|
+
}
|
|
502
623
|
}
|
|
503
624
|
else {
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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
|
+
}
|
|
514
668
|
}
|
|
515
669
|
}
|
|
516
670
|
}
|
package/dist/commands/inbox.js
CHANGED
|
@@ -21,12 +21,15 @@ export function runInboxList(options) {
|
|
|
21
21
|
}
|
|
22
22
|
const effectiveCwd = cwd ?? process.cwd();
|
|
23
23
|
const agent = options.agent ?? resolveCurrentAgentName(effectiveCwd) ?? 'unknown';
|
|
24
|
-
|
|
24
|
+
// --all widens past the core actionable default (pln#627 Phase A); without
|
|
25
|
+
// includeAll, a bare undefined status now hides acknowledged + archived.
|
|
26
|
+
const includeAll = options.all === true;
|
|
27
|
+
const status = includeAll ? undefined : options.status ?? 'pending';
|
|
25
28
|
const msgType = options.type;
|
|
26
29
|
const threadId = options.thread;
|
|
27
30
|
let messages;
|
|
28
31
|
if (options.localOnly) {
|
|
29
|
-
const result = readInbox({ agent, status, type: msgType, thread_id: threadId, markAsRead: false }, effectiveCwd);
|
|
32
|
+
const result = readInbox({ agent, status, includeAll, type: msgType, thread_id: threadId, markAsRead: false }, effectiveCwd);
|
|
30
33
|
messages = result.messages;
|
|
31
34
|
}
|
|
32
35
|
else {
|
|
@@ -36,7 +39,7 @@ export function runInboxList(options) {
|
|
|
36
39
|
for (const store of chain) {
|
|
37
40
|
try {
|
|
38
41
|
// Fetch all messages from this store (no pagination limit)
|
|
39
|
-
const storeResult = readInbox({ agent, status, type: msgType, thread_id: threadId, markAsRead: false, limit: 1_000_000 }, store.cwd);
|
|
42
|
+
const storeResult = readInbox({ agent, status, includeAll, type: msgType, thread_id: threadId, markAsRead: false, limit: 1_000_000 }, store.cwd);
|
|
40
43
|
for (const msg of storeResult.messages) {
|
|
41
44
|
if (!seenIds.has(msg.id)) {
|
|
42
45
|
seenIds.add(msg.id);
|
|
@@ -48,7 +51,7 @@ export function runInboxList(options) {
|
|
|
48
51
|
}
|
|
49
52
|
// Fallback when no chain found
|
|
50
53
|
if (messages.length === 0 && chain.length === 0) {
|
|
51
|
-
const result = readInbox({ agent, status, type: msgType, thread_id: threadId, markAsRead: false }, effectiveCwd);
|
|
54
|
+
const result = readInbox({ agent, status, includeAll, type: msgType, thread_id: threadId, markAsRead: false }, effectiveCwd);
|
|
52
55
|
messages = result.messages;
|
|
53
56
|
}
|
|
54
57
|
}
|
|
@@ -140,6 +143,9 @@ export function runInboxSend(to, text, options) {
|
|
|
140
143
|
}
|
|
141
144
|
else {
|
|
142
145
|
console.log(`✔ Message sent: [${result.shortLabel}] ${result.type} → ${result.to}`);
|
|
146
|
+
if (result.warning) {
|
|
147
|
+
console.warn(`⚠ ${result.warning}`);
|
|
148
|
+
}
|
|
143
149
|
}
|
|
144
150
|
}
|
|
145
151
|
catch (err) {
|
package/dist/commands/loop.js
CHANGED
|
@@ -122,14 +122,14 @@ function buildRequest(subcommand, loopId, opts) {
|
|
|
122
122
|
};
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
|
-
export function runLoopCommand(subcommand, args, options = {}, cwd) {
|
|
125
|
+
export async function runLoopCommand(subcommand, args, options = {}, cwd) {
|
|
126
126
|
if (!memoryExists(cwd)) {
|
|
127
127
|
console.error('Error: .brainclaw/ not found. Run `brainclaw init` first.');
|
|
128
128
|
process.exit(1);
|
|
129
129
|
}
|
|
130
130
|
const loopId = requireLoopId(args, options);
|
|
131
131
|
const request = buildRequest(subcommand, loopId, options);
|
|
132
|
-
const handled = handleBclawLoop({ args: request, cwd });
|
|
132
|
+
const handled = await handleBclawLoop({ args: request, cwd });
|
|
133
133
|
if (handled.response.status !== 'ok') {
|
|
134
134
|
const message = handled.response.error ?? handled.summary;
|
|
135
135
|
fail(`bclaw_loop.${String(request.intent)} rejected the call: ${message}`, 2, options);
|