brainclaw 1.19.1 → 1.20.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/dist/brainclaw-vscode.vsix +0 -0
- package/dist/commands/mcp-write-claims.js +35 -21
- package/dist/commands/mcp-write-coordination.js +11 -0
- package/dist/commands/mcp.js +19 -0
- package/dist/commands/session-end.js +7 -0
- package/dist/commands/session-start.js +27 -7
- package/dist/commands/watch.js +6 -1
- package/dist/core/dispatcher.js +204 -29
- package/dist/core/execution.js +8 -0
- package/dist/core/loops/brief-assembly.js +16 -5
- package/dist/core/loops/index.js +1 -0
- package/dist/core/loops/worker-reply-contract.js +93 -0
- package/dist/core/review-loop-turn-dispatch.js +1 -0
- package/dist/facts.js +9 -9
- package/dist/facts.json +8 -8
- package/docs/mcp-schema-changelog.md +14 -0
- package/package.json +1 -1
|
Binary file
|
|
@@ -12,11 +12,9 @@
|
|
|
12
12
|
*
|
|
13
13
|
* @module
|
|
14
14
|
*/
|
|
15
|
-
import fs from 'node:fs';
|
|
16
|
-
import path from 'node:path';
|
|
17
15
|
import { getTriggeredItems, renderTriggeredItems } from '../core/lifecycle.js';
|
|
18
16
|
import { buildContext } from '../core/context.js';
|
|
19
|
-
import { checkBrainclawInstallableUpdate,
|
|
17
|
+
import { checkBrainclawInstallableUpdate, renderBrainclawInstallableUpdateNotice } from '../core/brainclaw-version.js';
|
|
20
18
|
import { loadConfig } from '../core/config.js';
|
|
21
19
|
import { generateClaimId, loadClaim, saveClaim, adoptClaimSession, releaseClaimWithCascade, claimBaselineFields } from '../core/claims.js';
|
|
22
20
|
import { releaseClaimNextActions } from '../core/next-actions.js';
|
|
@@ -346,24 +344,14 @@ export async function handleBclawSessionStart(payload, ctx) {
|
|
|
346
344
|
const sessionUpdateConfig = loadConfig(cwd);
|
|
347
345
|
const sessionUpdateCheck = checkBrainclawInstallableUpdate(sessionUpdateConfig, cwd, { useDefaultNpmSource: true });
|
|
348
346
|
const sessionUpdateNotice = renderBrainclawInstallableUpdateNotice(sessionUpdateCheck);
|
|
349
|
-
// Stale
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
const content = fs.readFileSync(fullPath, 'utf-8').slice(0, 200);
|
|
358
|
-
const match = content.match(/brainclaw v(\d+\.\d+\.\d+)/);
|
|
359
|
-
if (match && match[1] !== currentVersion) {
|
|
360
|
-
staleInstructionsWarn = `\n⚠️ Agent instruction files are stale (generated by v${match[1]}, current is v${currentVersion}). Run: brainclaw export --all`;
|
|
361
|
-
break;
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
catch { /* file doesn't exist, skip */ }
|
|
365
|
-
}
|
|
366
|
-
}
|
|
347
|
+
// Stale-surfaces advisory. This REPLACES a hand-rolled guardrail that lived
|
|
348
|
+
// here: a regex over the first 200 chars of 3 hardcoded files. That was a
|
|
349
|
+
// second, divergent freshness check running next to the proper one — the same
|
|
350
|
+
// dual-source drift class as the two LANE-RESULT shapes (pln#638 PR-4). The
|
|
351
|
+
// pln#638 2b reconcile in startSession covers ~25 registry-derived paths with
|
|
352
|
+
// a real parser; its result was computed by this very call and then dropped
|
|
353
|
+
// from the response (Fable audit P0.2). Now it IS the guardrail.
|
|
354
|
+
const staleInstructionsWarn = result.stale_surfaces ? `\n⚠️ ${result.stale_surfaces.message}` : '';
|
|
367
355
|
// Claim adoption: if BRAINCLAW_CLAIM_ID is set (spawned by dispatcher),
|
|
368
356
|
// adopt the claim by writing session_id into it. This links claim→session.
|
|
369
357
|
let adoptedClaimId;
|
|
@@ -383,6 +371,10 @@ export async function handleBclawSessionStart(payload, ctx) {
|
|
|
383
371
|
}
|
|
384
372
|
if (adoptedClaimId)
|
|
385
373
|
sessionStartMsgParts.push(`\n🔗 Adopted claim ${adoptedClaimId} — use bclaw_read_inbox with claimId to see your assignment.`);
|
|
374
|
+
if (result.shared_checkout_warning) {
|
|
375
|
+
const others = result.shared_checkout_warning.other_sessions.map((s) => s.agent).join(', ');
|
|
376
|
+
sessionStartMsgParts.push(`\n⚠️ Shared checkout: ${others} also working in this worktree — claim your scope before editing.`);
|
|
377
|
+
}
|
|
386
378
|
if (staleInstructionsWarn)
|
|
387
379
|
sessionStartMsgParts.push(staleInstructionsWarn);
|
|
388
380
|
if (sessionUpdateNotice)
|
|
@@ -410,6 +402,15 @@ export async function handleBclawSessionStart(payload, ctx) {
|
|
|
410
402
|
inbox_pending: inboxPending,
|
|
411
403
|
...(result.auto_registered ? { auto_registered: true } : {}),
|
|
412
404
|
...(result.memory_pressure ? { memory_pressure: result.memory_pressure } : {}),
|
|
405
|
+
// pln#638 2b — the field startSession computes; it was previously dropped
|
|
406
|
+
// here, making the whole feature reachable only via CLI --json.
|
|
407
|
+
...(result.stale_surfaces
|
|
408
|
+
? { warnings: [result.stale_surfaces.message], warning_details: [result.stale_surfaces] }
|
|
409
|
+
: {}),
|
|
410
|
+
// Fifth computed-then-dropped field, caught by the seam guard: other live
|
|
411
|
+
// sessions on the same checkout. An agent about to edit needs this more
|
|
412
|
+
// than a human does.
|
|
413
|
+
...(result.shared_checkout_warning ? { shared_checkout_warning: result.shared_checkout_warning } : {}),
|
|
413
414
|
};
|
|
414
415
|
if (args.includeContext) {
|
|
415
416
|
const contextAgent = resolved.identity?.agent_name ?? result.agent;
|
|
@@ -502,6 +503,13 @@ export async function handleBclawSessionEnd(payload, ctx) {
|
|
|
502
503
|
parts.push(endUpdateNotice);
|
|
503
504
|
if (preSessionEndText)
|
|
504
505
|
parts.push(preSessionEndText);
|
|
506
|
+
// pln#636 C2 session-end sweep — the backstop trigger. endSession computed
|
|
507
|
+
// these and this handler dropped them (Fable audit P1): of the four C2
|
|
508
|
+
// boundaries, this one emitted nowhere. Text part + structured channel below.
|
|
509
|
+
if (result.scope_warnings?.length) {
|
|
510
|
+
for (const w of result.scope_warnings)
|
|
511
|
+
parts.push(`\n⚠️ ${w.message}`);
|
|
512
|
+
}
|
|
505
513
|
if (result.reflection_prompt) {
|
|
506
514
|
parts.push('\n📝 Session reflection — please answer these questions:');
|
|
507
515
|
for (let i = 0; i < result.reflection_prompt.questions.length; i++) {
|
|
@@ -519,6 +527,12 @@ export async function handleBclawSessionEnd(payload, ctx) {
|
|
|
519
527
|
triggered_items: preSessionEndItems,
|
|
520
528
|
...(result.handoff ? { handoff: result.handoff } : {}),
|
|
521
529
|
...(result.reflection_prompt ? { reflection_prompt: result.reflection_prompt } : {}),
|
|
530
|
+
...(result.scope_warnings?.length
|
|
531
|
+
? {
|
|
532
|
+
warnings: result.scope_warnings.map((w) => w.message),
|
|
533
|
+
warning_details: result.scope_warnings,
|
|
534
|
+
}
|
|
535
|
+
: {}),
|
|
522
536
|
}),
|
|
523
537
|
nextConnectionSessionId: null,
|
|
524
538
|
};
|
|
@@ -626,6 +626,11 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
626
626
|
scope: options?.scope,
|
|
627
627
|
worktreePath: options?.worktreePath,
|
|
628
628
|
assignmentId: options?.assignmentId,
|
|
629
|
+
// pln#638 PR-6b — the envelope must read the TARGET project's store: on a
|
|
630
|
+
// cross-project dispatch, defaulting to process.cwd() would inline the
|
|
631
|
+
// WRONG project's constraints/traps into the worker's brief.
|
|
632
|
+
cwd: dispatchCwd,
|
|
633
|
+
contextEnvelope: options?.contextEnvelope,
|
|
629
634
|
});
|
|
630
635
|
};
|
|
631
636
|
const toMessageSummary = (deliveryPlan) => deliveryPlan.map((entry) => ({
|
|
@@ -1750,6 +1755,12 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
1750
1755
|
scope: criticScope,
|
|
1751
1756
|
worktreePath: claimResult.worktreePath,
|
|
1752
1757
|
assignmentId: criticAssignmentId,
|
|
1758
|
+
// The ideation brief above already inlines a BM25-selected,
|
|
1759
|
+
// budget-managed memory bundle (with its own truncation warning).
|
|
1760
|
+
// The generic context envelope would double-carry the same traps
|
|
1761
|
+
// and break the documented ~48K content cap + dispatch-envelope
|
|
1762
|
+
// math pinned by ideation-loop-e2e.
|
|
1763
|
+
contextEnvelope: false,
|
|
1753
1764
|
});
|
|
1754
1765
|
const queued = queueCoordinateMessage({
|
|
1755
1766
|
agent: slot.agent,
|
package/dist/commands/mcp.js
CHANGED
|
@@ -1238,6 +1238,9 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
1238
1238
|
const targetCwd = resolveProjectCwd(workReq.project, cwd);
|
|
1239
1239
|
const useCompact = workReq.compact !== false; // default true
|
|
1240
1240
|
const warnings = [];
|
|
1241
|
+
// pln#635 structured channel — populated alongside `warnings` and attached
|
|
1242
|
+
// to the response only when non-empty.
|
|
1243
|
+
const warningDetails = [];
|
|
1241
1244
|
// Step 1: implicit session start (handles auto-registration internally)
|
|
1242
1245
|
let sessionResult;
|
|
1243
1246
|
try {
|
|
@@ -1254,6 +1257,21 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
1254
1257
|
if (sessionResult.auto_registered) {
|
|
1255
1258
|
warnings.push(`Agent '${sessionResult.agent}' was auto-registered (first use). Run \`brainclaw register-agent ${sessionResult.agent}\` to set capabilities and trust level.`);
|
|
1256
1259
|
}
|
|
1260
|
+
// pln#638 2b — surface the stale-surfaces advisory on the CANONICAL entry
|
|
1261
|
+
// point. Until the Fable audit, bclaw_work could never even compute this
|
|
1262
|
+
// (startSession defaulted to 'fast' and the check was gated on 'full'),
|
|
1263
|
+
// so the feature shipped in 1.19.0 unreachable from the one call the
|
|
1264
|
+
// session protocol tells every agent to make first.
|
|
1265
|
+
if (sessionResult.stale_surfaces) {
|
|
1266
|
+
warnings.push(sessionResult.stale_surfaces.message);
|
|
1267
|
+
warningDetails.push(sessionResult.stale_surfaces);
|
|
1268
|
+
}
|
|
1269
|
+
// Same seam, same fix: other live sessions on this checkout is exactly
|
|
1270
|
+
// what an agent entering via bclaw_work must hear before it edits.
|
|
1271
|
+
if (sessionResult.shared_checkout_warning) {
|
|
1272
|
+
const others = sessionResult.shared_checkout_warning.other_sessions.map((s) => s.agent).join(', ');
|
|
1273
|
+
warnings.push(`Shared checkout: ${others} also working in ${sessionResult.shared_checkout_warning.worktree_path} — claim your scope before editing.`);
|
|
1274
|
+
}
|
|
1257
1275
|
// Step 2: build context for requested scope. The "what's new" diff is
|
|
1258
1276
|
// surfaced for ALL intents (pln#390 regression fix): intent='resume'
|
|
1259
1277
|
// anchors it on the agent's previous session; every other intent gets
|
|
@@ -1530,6 +1548,7 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
1530
1548
|
claim_status: claimStatus,
|
|
1531
1549
|
session_id: sessionResult.session_id,
|
|
1532
1550
|
warnings,
|
|
1551
|
+
...(warningDetails.length ? { warning_details: warningDetails } : {}),
|
|
1533
1552
|
duration_ms: Date.now() - startMs,
|
|
1534
1553
|
bootstrap_recommended: bootstrapRecommended,
|
|
1535
1554
|
bootstrap_verdict: bootstrapVerdict,
|
|
@@ -107,6 +107,13 @@ export async function runSessionEnd(options = {}) {
|
|
|
107
107
|
if (result.compaction_hint) {
|
|
108
108
|
console.log(` 💡 ${result.compaction_hint}`);
|
|
109
109
|
}
|
|
110
|
+
// pln#636 C2 backstop — computed since 1.19.0, printed nowhere until the
|
|
111
|
+
// Fable audit (P1): the human output skipped it, so it was --json only.
|
|
112
|
+
if (result.scope_warnings?.length) {
|
|
113
|
+
for (const w of result.scope_warnings) {
|
|
114
|
+
console.warn(`⚠ ${w.message}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
110
117
|
if (result.reflection_prompt) {
|
|
111
118
|
console.log('\n📝 Session reflection:');
|
|
112
119
|
for (let i = 0; i < result.reflection_prompt.questions.length; i++) {
|
|
@@ -97,6 +97,21 @@ export async function runSessionStart(options = {}) {
|
|
|
97
97
|
console.warn(` ${c.agent} → ${c.scope}`);
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
|
+
// pln#638 2b, wired for humans too (Fable audit P0.3): the field existed on
|
|
101
|
+
// the result but the human output never printed it — visible only via --json.
|
|
102
|
+
if (snapshot.stale_surfaces) {
|
|
103
|
+
console.warn(`⚠ ${snapshot.stale_surfaces.message}`);
|
|
104
|
+
}
|
|
105
|
+
// Fifth instance of the computed-then-dropped class, caught by the new seam
|
|
106
|
+
// guard on its first run: built since the shared-checkout detection landed,
|
|
107
|
+
// read by nothing. Two agents editing one checkout is precisely what a human
|
|
108
|
+
// at the terminal needs to hear about.
|
|
109
|
+
if (snapshot.shared_checkout_warning) {
|
|
110
|
+
const others = snapshot.shared_checkout_warning.other_sessions
|
|
111
|
+
.map((s) => `${s.agent} (${s.session_id}${s.branch ? `, ${s.branch}` : ''})`)
|
|
112
|
+
.join(', ');
|
|
113
|
+
console.warn(`⚠ Shared checkout: ${others} ${snapshot.shared_checkout_warning.other_sessions.length === 1 ? 'is' : 'are'} also working in ${snapshot.shared_checkout_warning.worktree_path}. Claim your scope before editing.`);
|
|
114
|
+
}
|
|
100
115
|
}
|
|
101
116
|
catch (e) {
|
|
102
117
|
const message = e instanceof Error ? e.message : String(e);
|
|
@@ -291,15 +306,20 @@ export async function startSession(options = {}) {
|
|
|
291
306
|
// daemon, no watcher (feedback_lazy_reconcile_pattern). Advisory only: nothing
|
|
292
307
|
// is regenerated here, because regeneration is an explicit act and silently
|
|
293
308
|
// rewriting a file the operator may have edited would be worse than a warning.
|
|
309
|
+
// NOT gated on maintenanceMode — deliberately, and this is a fix, not an
|
|
310
|
+
// oversight (Fable audit P0). The check is ~25 existsSync + a few 4KB head
|
|
311
|
+
// reads, nothing like the sweeps/federation/GC that justify the 'full' gate.
|
|
312
|
+
// Gating it on 'full' made it unreachable from `bclaw_work`, which calls
|
|
313
|
+
// startSession without maintenanceMode (→ 'fast') and is the entry point the
|
|
314
|
+
// session protocol tells every agent to use — so the warning shipped in
|
|
315
|
+
// 1.19.0 and never fired for the population it was built for.
|
|
294
316
|
let staleSurfaces;
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
staleSurfaces = staleSurfaceWarning(freshness, currentVersion);
|
|
300
|
-
}
|
|
301
|
-
catch { /* non-fatal */ }
|
|
317
|
+
try {
|
|
318
|
+
const currentVersion = getInstalledBrainclawVersion();
|
|
319
|
+
const freshness = reconcileSurfaceFreshness(options.cwd ?? process.cwd(), currentVersion);
|
|
320
|
+
staleSurfaces = staleSurfaceWarning(freshness, currentVersion);
|
|
302
321
|
}
|
|
322
|
+
catch { /* non-fatal */ }
|
|
303
323
|
// Materialize incoming federation signals from linked projects (Phase 0 — local)
|
|
304
324
|
if (maintenanceMode === 'full') {
|
|
305
325
|
try {
|
package/dist/commands/watch.js
CHANGED
|
@@ -4,7 +4,7 @@ import { loadState } from '../core/state.js';
|
|
|
4
4
|
import { listRuntimeNotes } from '../core/runtime.js';
|
|
5
5
|
import { listCandidates } from '../core/candidates.js';
|
|
6
6
|
import { readAuditLog } from '../core/audit.js';
|
|
7
|
-
import { listClaims, saveClaim, generateClaimId, ensureClaimsDir } from '../core/claims.js';
|
|
7
|
+
import { listClaims, saveClaim, generateClaimId, ensureClaimsDir, claimBaselineFields } from '../core/claims.js';
|
|
8
8
|
import { resolveCurrentAgentName } from '../core/agent-registry.js';
|
|
9
9
|
function emit(event) {
|
|
10
10
|
process.stdout.write(JSON.stringify(event) + '\n');
|
|
@@ -197,6 +197,11 @@ export function runWatch(options = {}) {
|
|
|
197
197
|
description: `auto-claim: ${filename}`,
|
|
198
198
|
created_at: new Date().toISOString(),
|
|
199
199
|
status: 'active',
|
|
200
|
+
// pln#636 C0-b / trp#1292 — the FIFTH creation path, missed by the
|
|
201
|
+
// 1.19.1 fix and invisible to its guard test: this one assigns the
|
|
202
|
+
// literal to a variable, so a scan looking for `saveClaim({` never saw
|
|
203
|
+
// it. Found by the ideation critic that reviewed the guard.
|
|
204
|
+
...claimBaselineFields(),
|
|
200
205
|
};
|
|
201
206
|
try {
|
|
202
207
|
saveClaim(claim);
|
package/dist/core/dispatcher.js
CHANGED
|
@@ -326,6 +326,88 @@ export function buildWorkingDefaultsSection(opts) {
|
|
|
326
326
|
'',
|
|
327
327
|
].join('\n');
|
|
328
328
|
}
|
|
329
|
+
/**
|
|
330
|
+
* pln#638 PR-4 — the transport section, for BOTH declared-MCP and MCP-less agents.
|
|
331
|
+
*
|
|
332
|
+
* WHY THIS EXISTS AS ONE FUNCTION. The MCP-less block was duplicated verbatim in
|
|
333
|
+
* `generateBrief` and `generateDispatchBrief`; that duplication is exactly what
|
|
334
|
+
* lets two brief paths drift, which is the class of bug PR-3 just fixed one layer
|
|
335
|
+
* up. One function, two callers.
|
|
336
|
+
*
|
|
337
|
+
* WHY A DECLARED-MCP AGENT ALSO GETS A SECTION — the part that was missing. A
|
|
338
|
+
* profile's `runtime.mcp_direct` is a STATIC flag: it asserts nothing about
|
|
339
|
+
* whether the config exists on this machine, whether the server started, or
|
|
340
|
+
* whether stdio came up. Proven in production during pln#638's own ideation: a
|
|
341
|
+
* codex critic ran with `mcp_direct=true` and NO reachable MCP. Its 3654-character
|
|
342
|
+
* critique survived only because the brief happened to spell out a file fallback
|
|
343
|
+
* by hand. So the brief must never ASSERT the capability — it states the
|
|
344
|
+
* expectation and names the fallback, and the worker decides from what it
|
|
345
|
+
* actually observes.
|
|
346
|
+
*
|
|
347
|
+
* The store path is deliberately NOT mentioned. `.brainclaw/` is gitignored
|
|
348
|
+
* (.gitignore:10), so it does not exist in a worker's worktree — the previous
|
|
349
|
+
* wording told workers to write candidates into a directory they cannot see.
|
|
350
|
+
*/
|
|
351
|
+
/**
|
|
352
|
+
* The ONE LANE-RESULT shape every brief quotes.
|
|
353
|
+
*
|
|
354
|
+
* There used to be two. `buildProtocolSection` has emitted a fallback since
|
|
355
|
+
* pln#526 with `{summary, files_changed, artifacts}` and NO `body`, while the
|
|
356
|
+
* transport section below asked for `body`. A full-mode worker with an assignment
|
|
357
|
+
* id received both and had to pick — and a worker that followed the older one
|
|
358
|
+
* recreated trp_8efdbf9d (a substantial review collapsed into a one-line summary
|
|
359
|
+
* because the contract had nowhere to put the reasoning). Caught in review by
|
|
360
|
+
* Fable before this shipped.
|
|
361
|
+
*/
|
|
362
|
+
export function laneResultShape(assignmentId) {
|
|
363
|
+
const asgn = assignmentId ?? '<assignment_id>';
|
|
364
|
+
return `{"assignment_id":"${asgn}","status":"completed|blocked|failed","summary":"<one line>","body":"<your full output — the reasoning, not just a label>","files_changed":["..."],"artifacts":["..."]}`;
|
|
365
|
+
}
|
|
366
|
+
export function buildTransportSection(opts) {
|
|
367
|
+
const laneResult = `write LANE-RESULT.json at the worktree ROOT: ${laneResultShape(opts.assignmentId)}`;
|
|
368
|
+
if (!opts.hasMcp) {
|
|
369
|
+
return [
|
|
370
|
+
'## ⚠ Transport: no MCP (file protocol only)',
|
|
371
|
+
'Your runtime has no brainclaw MCP access — any `bclaw_*` instruction above does NOT apply to you. Report your outcome via the FILE protocol only; it is authoritative for this run:',
|
|
372
|
+
`- When done, ${laneResult}.`,
|
|
373
|
+
// RESTORED after review. Removing this orphaned a real, shipped consumer:
|
|
374
|
+
// `collectWorktreeCandidateFiles` (harvest.ts:191-205) scans exactly this
|
|
375
|
+
// directory inside WORKER worktrees, and `bclaw_harvest_candidates` exposes
|
|
376
|
+
// it. My removal rested on a wrong inference — `.gitignore:10` means the
|
|
377
|
+
// path is not CHECKED OUT, not that a worker cannot create it. Being
|
|
378
|
+
// gitignored is the feature: candidates never pollute worker commits.
|
|
379
|
+
'- Capture decisions/traps as candidate JSON under `.brainclaw/coordination/inbox/` in your worktree — create the directory if it does not exist (it is gitignored, so a fresh worktree will not have it). The coordinator harvests them.',
|
|
380
|
+
'- Do NOT call bclaw_* tools — they are unavailable here. The coordinator harvests your result and integrates it.',
|
|
381
|
+
'',
|
|
382
|
+
].join('\n');
|
|
383
|
+
}
|
|
384
|
+
return [
|
|
385
|
+
'## Transport: MCP expected, file fallback if not',
|
|
386
|
+
'Your profile declares brainclaw MCP access, but that is a DECLARATION, not a verified fact — the config may be absent on this machine or the server may not have started. Decide from what you actually observe:',
|
|
387
|
+
'- If `bclaw_*` tools respond: use them, as instructed above.',
|
|
388
|
+
`- If they are unavailable or error: do not stop and do not discard your work — ${laneResult}. The coordinator harvests it.`,
|
|
389
|
+
'',
|
|
390
|
+
].join('\n');
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* pln#638 PR-6 — lane lifecycle doctrine (settled by loop lop_2d838a638b1e2956):
|
|
394
|
+
*
|
|
395
|
+
* Native hooks = interactive agents. Dispatched workers = dispatcher-owned
|
|
396
|
+
* lifecycle. A lane's unit of identity is assignment + claim + AgentRun —
|
|
397
|
+
* NEVER a session. Sessions carry interactive side-effects (auto-release
|
|
398
|
+
* frees ALL of an agent's claims at session_end) that would leak across
|
|
399
|
+
* lanes, and N parallel lanes would contend on the session lock (observed:
|
|
400
|
+
* a worker's global hook stalled 5s on a session LOCK, not on a missing
|
|
401
|
+
* store).
|
|
402
|
+
*
|
|
403
|
+
* The three lifecycle needs hooks used to cover are met elsewhere:
|
|
404
|
+
* - shared context at start → inlined ContextEnvelope in the brief
|
|
405
|
+
* (buildContextEnvelopeSection — MCP is a refresh, not a prerequisite);
|
|
406
|
+
* - progress/closure → bclaw_assignment_update + the wrapper's
|
|
407
|
+
* mechanical completed/failed sentinels (see attemptExecution);
|
|
408
|
+
* - business closure → the coordinator's harvest/report path.
|
|
409
|
+
* Transport completion never releases claims or triggers review.
|
|
410
|
+
*/
|
|
329
411
|
export function buildProtocolSection(options) {
|
|
330
412
|
const parts = [];
|
|
331
413
|
parts.push('## Protocol');
|
|
@@ -395,30 +477,37 @@ export function buildProtocolSection(options) {
|
|
|
395
477
|
// fails in your environment. pln#628 Focus 4A: sandbox is NO LONGER a reason
|
|
396
478
|
// MCP is unavailable (dec#133), so this is framed as a generic fallback, not a
|
|
397
479
|
// sandbox instruction. The coordinator ingests it with `brainclaw harvest`.
|
|
398
|
-
|
|
480
|
+
// Quotes the SHARED shape (laneResultShape) so this and the transport section
|
|
481
|
+
// cannot disagree about what a worker should write — they used to.
|
|
482
|
+
parts.push(`Final fallback (if bclaw_assignment_update / MCP is unavailable in your environment): write LANE-RESULT.json at the worktree root — ${laneResultShape(options.assignmentId)}. The coordinator harvests it via \`brainclaw harvest ${options.assignmentId}\`.`);
|
|
399
483
|
}
|
|
400
484
|
else if (options?.claimId) {
|
|
401
|
-
|
|
485
|
+
// pln#638 PR-6a — NO session lifecycle in a dispatched lane's brief. The
|
|
486
|
+
// ideation loop settled this (design of record: pln638-pr6 synthesis): a
|
|
487
|
+
// session is an AGENT's lifecycle and its effects overflow the lane —
|
|
488
|
+
// `session-end --auto-release` releases ALL of the agent's active claims and
|
|
489
|
+
// the handoff aggregates ALL its commits, so one finishing lane could tear
|
|
490
|
+
// down its siblings' work. The engine already agrees: SESSION_ID is
|
|
491
|
+
// deliberately scrubbed from the worker env (execution-profile.ts). The
|
|
492
|
+
// lane's lifecycle is the claim (+ assignment when present), nothing more.
|
|
402
493
|
if (options.worktreePath) {
|
|
403
|
-
parts.push(`
|
|
494
|
+
parts.push(`1. cd into the worktree: ${options.worktreePath}`);
|
|
404
495
|
}
|
|
405
|
-
parts.push(`${options.worktreePath ? '
|
|
406
|
-
parts.push(`${options.worktreePath ? '
|
|
407
|
-
parts.push(
|
|
496
|
+
parts.push(`${options.worktreePath ? '2' : '1'}. Work on the assigned scope (claim already active)`);
|
|
497
|
+
parts.push(`${options.worktreePath ? '3' : '2'}. Release the claim: bclaw_release_claim(id: "${options.claimId}", planStatus: "done") — required for hard_after gating to unblock downstream tasks`);
|
|
498
|
+
parts.push('Do NOT call bclaw_session_start / bclaw_session_end: sessions belong to interactive agents, and session-end tears down claims beyond this lane.');
|
|
408
499
|
}
|
|
409
500
|
else {
|
|
410
|
-
parts.push('1. Call
|
|
411
|
-
parts.push('2.
|
|
412
|
-
parts.push('3.
|
|
413
|
-
parts.push('
|
|
414
|
-
parts.push('5. Call bclaw_session_end with a narrative when done');
|
|
501
|
+
parts.push('1. Call bclaw_claim to claim the scope before editing');
|
|
502
|
+
parts.push('2. Work in the worktree created by the claim');
|
|
503
|
+
parts.push('3. Release the claim when done: bclaw_release_claim(id: "clm_xxx", planStatus: "done") — required for hard_after sequence gating to unlock the next step');
|
|
504
|
+
parts.push('Do NOT call bclaw_session_start / bclaw_session_end: sessions belong to interactive agents, and session-end tears down claims beyond this lane.');
|
|
415
505
|
}
|
|
416
506
|
parts.push('');
|
|
417
507
|
parts.push('## Available tools');
|
|
418
508
|
if (options?.assignmentId) {
|
|
419
509
|
parts.push('- bclaw_assignment_update (report lifecycle: accepted/started/progress/completed/failed/blocked)');
|
|
420
510
|
}
|
|
421
|
-
parts.push('- bclaw_session_start, bclaw_session_end (session lifecycle)');
|
|
422
511
|
if (!options?.claimId) {
|
|
423
512
|
parts.push('- bclaw_claim, bclaw_release_claim (scope ownership)');
|
|
424
513
|
}
|
|
@@ -498,6 +587,10 @@ export function generateBrief(plan, item, cwd, briefMode, options) {
|
|
|
498
587
|
}
|
|
499
588
|
// pln#554 step 4 — working defaults (incremental commits + validation bar).
|
|
500
589
|
parts.push(buildWorkingDefaultsSection({ canCommit: briefProfile ? dispatchCanCommit(briefProfile) : true }));
|
|
590
|
+
// pln#638 PR-6b — survival context rides in the brief; MCP is a refresh.
|
|
591
|
+
const envelope = buildContextEnvelopeSection(cwd);
|
|
592
|
+
if (envelope)
|
|
593
|
+
parts.push(envelope);
|
|
501
594
|
// Steps if any
|
|
502
595
|
if (plan.steps?.length) {
|
|
503
596
|
parts.push('## Steps');
|
|
@@ -566,14 +659,19 @@ export function generateBrief(plan, item, cwd, briefMode, options) {
|
|
|
566
659
|
// call bclaw_*" note: their coherent message is carried by the Protocol section
|
|
567
660
|
// (MCP primary + LANE-RESULT.json fallback) and working-defaults (canCommit=
|
|
568
661
|
// false → the coordinator commits their worktree at harvest).
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
662
|
+
// pln#638 PR-4 — emitted for EVERY agent, not only MCP-less ones: a declared-MCP
|
|
663
|
+
// worker needs the conditional fallback too (see buildTransportSection for the
|
|
664
|
+
// production incident that proved it).
|
|
665
|
+
//
|
|
666
|
+
// An UNRESOLVED profile gets the conditional form rather than silence — review
|
|
667
|
+
// caught that gating on a truthy profile left an unknown agent with a
|
|
668
|
+
// fully-MCP-asserting brief and no fallback, which is the case the principle
|
|
669
|
+
// "never assert an unverifiable capability" exists for.
|
|
670
|
+
if (options?.agent) {
|
|
671
|
+
parts.push(buildTransportSection({
|
|
672
|
+
hasMcp: briefProfile ? dispatchHasMcp(briefProfile) : true,
|
|
673
|
+
assignmentId: options.assignmentId,
|
|
674
|
+
}));
|
|
577
675
|
}
|
|
578
676
|
// Codex-specific constraints: focus and speed guidance for sandboxed runs.
|
|
579
677
|
// Gated on agent identity (not brief mode) so future non-codex compact consumers
|
|
@@ -586,6 +684,68 @@ export function generateBrief(plan, item, cwd, briefMode, options) {
|
|
|
586
684
|
}
|
|
587
685
|
return parts.join('\n');
|
|
588
686
|
}
|
|
687
|
+
/** Character budget for the inlined context envelope — bounded by design
|
|
688
|
+
* (trp#179: oversized payloads), and deliberately small: this is the survival
|
|
689
|
+
* kit, not the library. */
|
|
690
|
+
const CONTEXT_ENVELOPE_MAX_CHARS = 3000;
|
|
691
|
+
const CONTEXT_ENVELOPE_ITEM_MAX_CHARS = 220;
|
|
692
|
+
const CONTEXT_ENVELOPE_TOP_K = 6;
|
|
693
|
+
/**
|
|
694
|
+
* pln#638 PR-6b — the ContextEnvelope: constraints/traps/decisions INLINED into
|
|
695
|
+
* the dispatch brief, bounded and deterministic.
|
|
696
|
+
*
|
|
697
|
+
* Why inline instead of "call bclaw_context": that instruction assumes the MCP
|
|
698
|
+
* is reachable, and the whole PR-6 design was forced by a production run where
|
|
699
|
+
* the declared-MCP flag was true and the server was not there (critic A of
|
|
700
|
+
* lop_2d838a638b1e2956 lost its context channel and worked blind). The brief is
|
|
701
|
+
* the ONE artifact every tier demonstrably receives — so the survival context
|
|
702
|
+
* rides in it, and MCP remains an optional refresh, never a precondition.
|
|
703
|
+
*
|
|
704
|
+
* Deterministic: newest-first by created_at (tie-broken by id), fixed caps, no
|
|
705
|
+
* clock reads — the same store state always renders the same envelope.
|
|
706
|
+
* NOT scope-filtered, deliberately: relevance guessing risks hiding the one
|
|
707
|
+
* trap that mattered (the inverted-default lesson of claim-scope, applied to
|
|
708
|
+
* context selection). Bounded instead.
|
|
709
|
+
*/
|
|
710
|
+
export function buildContextEnvelopeSection(cwd) {
|
|
711
|
+
let state;
|
|
712
|
+
try {
|
|
713
|
+
state = loadState(cwd);
|
|
714
|
+
}
|
|
715
|
+
catch {
|
|
716
|
+
return ''; // no store, no envelope — never block a brief on context
|
|
717
|
+
}
|
|
718
|
+
const newestFirst = (items) => [...items].sort((a, b) => (b.created_at ?? '').localeCompare(a.created_at ?? '') || (a.id ?? '').localeCompare(b.id ?? ''));
|
|
719
|
+
const clip = (text) => text.length > CONTEXT_ENVELOPE_ITEM_MAX_CHARS ? `${text.slice(0, CONTEXT_ENVELOPE_ITEM_MAX_CHARS - 1)}…` : text;
|
|
720
|
+
const constraints = newestFirst(state.active_constraints ?? []);
|
|
721
|
+
const traps = newestFirst(state.known_traps ?? []).slice(0, CONTEXT_ENVELOPE_TOP_K);
|
|
722
|
+
const decisions = newestFirst(state.recent_decisions ?? []).slice(0, CONTEXT_ENVELOPE_TOP_K);
|
|
723
|
+
if (constraints.length === 0 && traps.length === 0 && decisions.length === 0)
|
|
724
|
+
return '';
|
|
725
|
+
const lines = [
|
|
726
|
+
'## Project context (inlined — MCP is a refresh, not a prerequisite)',
|
|
727
|
+
`Snapshot: ${constraints.length} constraint(s), ${traps.length}/${(state.known_traps ?? []).length} trap(s), ${decisions.length}/${(state.recent_decisions ?? []).length} decision(s), newest-first.`,
|
|
728
|
+
];
|
|
729
|
+
const push = (title, items) => {
|
|
730
|
+
if (items.length === 0)
|
|
731
|
+
return;
|
|
732
|
+
lines.push(`### ${title}`);
|
|
733
|
+
for (const item of items)
|
|
734
|
+
lines.push(`- [${item.id ?? '?'}] ${clip((item.text ?? '').replace(/\s+/g, ' ').trim())}`);
|
|
735
|
+
};
|
|
736
|
+
push('Active constraints (binding)', constraints);
|
|
737
|
+
push('Known traps', traps);
|
|
738
|
+
push('Recent decisions', decisions);
|
|
739
|
+
lines.push('');
|
|
740
|
+
// Enforce the total budget by dropping whole trailing lines — a clipped list
|
|
741
|
+
// stays valid markdown, a mid-line cut does not.
|
|
742
|
+
let text = lines.join('\n');
|
|
743
|
+
while (text.length > CONTEXT_ENVELOPE_MAX_CHARS && lines.length > 2) {
|
|
744
|
+
lines.splice(lines.length - 2, 1); // drop the last content line, keep the trailing ''
|
|
745
|
+
text = lines.join('\n');
|
|
746
|
+
}
|
|
747
|
+
return text;
|
|
748
|
+
}
|
|
589
749
|
export function generateDispatchBrief(options) {
|
|
590
750
|
const briefMode = resolveBriefMode(options.agent);
|
|
591
751
|
const parts = [];
|
|
@@ -607,6 +767,13 @@ export function generateDispatchBrief(options) {
|
|
|
607
767
|
}
|
|
608
768
|
// pln#554 step 4 — working defaults (incremental commits + validation bar).
|
|
609
769
|
parts.push(buildWorkingDefaultsSection({ canCommit: taskBriefProfile ? dispatchCanCommit(taskBriefProfile) : true }));
|
|
770
|
+
// pln#638 PR-6b — same envelope as generateBrief (see buildContextEnvelopeSection).
|
|
771
|
+
// Suppressed only when the content curates its own memory (contextEnvelope: false).
|
|
772
|
+
if (options.contextEnvelope !== false) {
|
|
773
|
+
const taskEnvelope = buildContextEnvelopeSection(options.cwd);
|
|
774
|
+
if (taskEnvelope)
|
|
775
|
+
parts.push(taskEnvelope);
|
|
776
|
+
}
|
|
610
777
|
if (briefMode === 'full') {
|
|
611
778
|
parts.push(buildProtocolSection({
|
|
612
779
|
claimId: options.claimId,
|
|
@@ -618,14 +785,14 @@ export function generateDispatchBrief(options) {
|
|
|
618
785
|
// (see generateBrief for the full rationale + dec#133). Fires only for
|
|
619
786
|
// genuinely MCP-less agents; sandboxed-but-MCP-capable codex no longer gets a
|
|
620
787
|
// self-contradictory "no MCP / Do NOT call bclaw_*" note.
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
parts.push(
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
788
|
+
// pln#638 PR-4 — same shared section as generateBrief; the wording used to be
|
|
789
|
+
// duplicated verbatim in both builders, which is how they drift. Unresolved
|
|
790
|
+
// profile → conditional form, never silence (see generateBrief for why).
|
|
791
|
+
if (options.agent) {
|
|
792
|
+
parts.push(buildTransportSection({
|
|
793
|
+
hasMcp: taskBriefProfile ? dispatchHasMcp(taskBriefProfile) : true,
|
|
794
|
+
assignmentId: options.assignmentId,
|
|
795
|
+
}));
|
|
629
796
|
}
|
|
630
797
|
// Codex-specific constraints: focus and speed guidance for sandboxed runs
|
|
631
798
|
if (options.agent === 'codex') {
|
|
@@ -911,7 +1078,15 @@ export async function dispatch(options, cwd) {
|
|
|
911
1078
|
// --- Dry-run path: skip assignment creation and message sending ---
|
|
912
1079
|
if (options.dryRun) {
|
|
913
1080
|
const briefMode = resolveBriefMode(targetAgent);
|
|
914
|
-
|
|
1081
|
+
// pln#638 PR-3 — `agent` MUST be passed here, exactly as the real dispatch
|
|
1082
|
+
// path below does. Without it `generateBrief` cannot resolve the capability
|
|
1083
|
+
// profile, and THREE things silently diverge from what would actually be
|
|
1084
|
+
// sent: working-defaults claims `canCommit: true` (wrong, and dangerous, for
|
|
1085
|
+
// a sandboxed worker whose .git is read-only), the MCP-less LANE-RESULT
|
|
1086
|
+
// section is omitted for a tier-C agent, and the liveness section loses its
|
|
1087
|
+
// `sandboxed` flag. A --dry-run that previews a DIFFERENT brief than the one
|
|
1088
|
+
// that ships is worse than no preview.
|
|
1089
|
+
const brief = generateBrief(readyItem.plan, readyItem.item, cwd, briefMode, { claimId, worktreePath, agent: targetAgent });
|
|
915
1090
|
const invokeCmd = buildInvokeCommand(targetAgent, brief, { model: resolveModel(targetAgent, { override: options.model }) });
|
|
916
1091
|
if (invokeCmd) {
|
|
917
1092
|
const cmdPrefix = buildEnvPrefix(claimId);
|
package/dist/core/execution.js
CHANGED
|
@@ -138,6 +138,14 @@ export function executeDispatchedCommand(invoke, options) {
|
|
|
138
138
|
* - If autoExecute=true and agent is spawnable: spawn and return delivered_and_started
|
|
139
139
|
* - If autoExecute=false or not spawnable: return command_ready_manual with command string
|
|
140
140
|
* - If spawn fails: log warning, fallback to command_ready_manual
|
|
141
|
+
*
|
|
142
|
+
* pln#638 PR-6 — lifecycle boundary (the other half lives on
|
|
143
|
+
* buildProtocolSection in dispatcher.ts): the wrapper spawned here emits
|
|
144
|
+
* ack/heartbeat/completed/failed sentinels MECHANICALLY from the process exit
|
|
145
|
+
* code. That is TRANSPORT completion only. It never opens or closes sessions
|
|
146
|
+
* (lanes have none), never releases the lane's claim, and never triggers a
|
|
147
|
+
* review — business completion is proven exclusively by the coordinator's
|
|
148
|
+
* harvest/report path reading LANE-RESULT or bclaw_assignment_update.
|
|
141
149
|
*/
|
|
142
150
|
export async function attemptExecution(invoke, options) {
|
|
143
151
|
const adapter = options.adapter ?? defaultExecutionAdapter;
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* — N items dropped)" tail so the operator can see when content was
|
|
20
20
|
* dropped.
|
|
21
21
|
*/
|
|
22
|
+
import { deriveWorkerReplyContract, renderWorkerReplyProse } from './worker-reply-contract.js';
|
|
22
23
|
const DEFAULT_MAX_CHARS = 48_000;
|
|
23
24
|
const DEFAULT_TOP_K_PER_CATEGORY = 8;
|
|
24
25
|
/**
|
|
@@ -73,14 +74,24 @@ export function buildIdeationBrief(input) {
|
|
|
73
74
|
const proposalBlock = renderProposalBlock(proposalText);
|
|
74
75
|
const memoryBlock = renderMemoryBlock(fetchedItemsByCategory);
|
|
75
76
|
const closing = renderClosingInstructions(slotRole, thread.current_phase);
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
|
|
77
|
+
// pln#638 PR-5 — the deliverable contract, derived from the CURRENT phase's
|
|
78
|
+
// gate and frozen into the brief at dispatch time. This is the structural fix
|
|
79
|
+
// for proof #1 (a critic typed `coverage_gap`, invisible to the critique gate):
|
|
80
|
+
// the expected type was always known here; it just never travelled. FIXED
|
|
81
|
+
// part, never truncated — a brief that keeps its memory bundle but loses its
|
|
82
|
+
// reply contract would recreate the bug the section exists to prevent.
|
|
83
|
+
const contract = deriveWorkerReplyContract(thread);
|
|
84
|
+
const contractBlock = contract ? renderWorkerReplyProse(contract) : '';
|
|
85
|
+
// Compose with truncation. The proposal seed, header, closing and contract
|
|
86
|
+
// are fixed; memory + prior artifacts share the remaining budget. Memory
|
|
87
|
+
// before prior-artifacts so the critic always sees fresh adversarial pressure.
|
|
88
|
+
const fixedParts = [header, proposalBlock, closing, contractBlock];
|
|
80
89
|
const fixedSize = fixedParts.reduce((n, s) => n + s.length, 0);
|
|
81
90
|
const remainingBudget = Math.max(0, maxChars - fixedSize);
|
|
82
91
|
const { text: truncatedMemory, truncated, droppedItems, includedItems } = truncateToBudget([memoryBlock, priorArtifactsBlock].filter((s) => s.length > 0), fetchedItemsByCategory, remainingBudget);
|
|
83
|
-
|
|
92
|
+
// The contract closes the brief: the last thing a worker reads is how to
|
|
93
|
+
// reply so its work counts.
|
|
94
|
+
const text = [header, proposalBlock, truncatedMemory, closing, contractBlock]
|
|
84
95
|
.filter((s) => s.length > 0)
|
|
85
96
|
.join('\n\n');
|
|
86
97
|
return { text, truncated, includedItems, droppedItems, categoriesUsed };
|
package/dist/core/loops/index.js
CHANGED
|
@@ -11,4 +11,5 @@ export { readSurveySources, } from './hooks/survey-source-reader.js';
|
|
|
11
11
|
export { buildSurveySignalsBaseline, } from './hooks/survey-signals-baseline.js';
|
|
12
12
|
export { acquireLock, hashRequest, recordConflict, withLoopLock, DEFAULT_MAX_MUTATION_DURATION_MS, IDEMPOTENCY_TTL_MS, LEASE_GRACE_MS, LEASE_WINDOW_MS, IdempotencyKeyReusedError, IdempotencyOwnerMismatchError, LockLostError, LockTimeoutError, VersionConflictError, } from './lock.js';
|
|
13
13
|
export { acquireBootstrapLoop, findExistingBootstrapLoop, BootstrapCoordinationInProgressError, } from './bootstrap-acquire.js';
|
|
14
|
+
export { deriveWorkerReplyContract, renderWorkerReplyProse, workerReplyNextAction, } from './worker-reply-contract.js';
|
|
14
15
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { LOOP_ARTIFACT_BODY_MAX_BYTES } from './types.js';
|
|
2
|
+
/** Recursively extract artifact requirements from a gate, preserving all/any. */
|
|
3
|
+
function extractFromGate(gate) {
|
|
4
|
+
switch (gate.kind) {
|
|
5
|
+
case 'min_artifacts_by_type':
|
|
6
|
+
return { requirements: [{ type: gate.type, n: gate.n, scope: gate.scope }], other: [], composition: 'single' };
|
|
7
|
+
case 'artifact_produced':
|
|
8
|
+
return { requirements: [{ type: gate.type }], other: [], composition: 'single' };
|
|
9
|
+
case 'all':
|
|
10
|
+
case 'any': {
|
|
11
|
+
const parts = gate.conditions.map(extractFromGate);
|
|
12
|
+
const requirements = parts.flatMap((p) => p.requirements);
|
|
13
|
+
const other = parts.flatMap((p) => p.other);
|
|
14
|
+
// Nested compositions collapse to the OUTER combinator for prose purposes;
|
|
15
|
+
// the exact tree is the engine's business, the worker only needs to know
|
|
16
|
+
// whether one deliverable suffices or all are needed.
|
|
17
|
+
return {
|
|
18
|
+
requirements,
|
|
19
|
+
other,
|
|
20
|
+
composition: requirements.length > 1 ? gate.kind : 'single',
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
default:
|
|
24
|
+
// reviewer_green, no_open_questions, iterations, manual, phase_reached —
|
|
25
|
+
// conditions a worker cannot satisfy by typing an artifact correctly.
|
|
26
|
+
return { requirements: [], other: [gate.kind], composition: 'single' };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Derive the contract for the loop's CURRENT phase, or undefined when the phase
|
|
31
|
+
* has no artifact-typed gate — in which case no section is emitted and no
|
|
32
|
+
* obligation is invented (silence over fabrication, as everywhere else).
|
|
33
|
+
*/
|
|
34
|
+
export function deriveWorkerReplyContract(thread) {
|
|
35
|
+
const phaseDef = thread.phases.find((p) => p.name === thread.current_phase);
|
|
36
|
+
const gate = phaseDef?.advance_gate;
|
|
37
|
+
if (!gate)
|
|
38
|
+
return undefined;
|
|
39
|
+
const { requirements, other, composition } = extractFromGate(gate);
|
|
40
|
+
if (requirements.length === 0)
|
|
41
|
+
return undefined;
|
|
42
|
+
return {
|
|
43
|
+
loop_id: thread.id,
|
|
44
|
+
phase: thread.current_phase,
|
|
45
|
+
requirements,
|
|
46
|
+
composition,
|
|
47
|
+
other_conditions: [...new Set(other)],
|
|
48
|
+
loop_version: thread.version,
|
|
49
|
+
body_max_bytes: LOOP_ARTIFACT_BODY_MAX_BYTES,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The {tool, args, when} a worker with MCP should call — derived from the SAME
|
|
54
|
+
* contract the prose renders, so the two cannot disagree.
|
|
55
|
+
*/
|
|
56
|
+
export function workerReplyNextAction(contract) {
|
|
57
|
+
const primary = contract.requirements[0];
|
|
58
|
+
return {
|
|
59
|
+
tool: 'bclaw_loop',
|
|
60
|
+
args: {
|
|
61
|
+
intent: 'add_artifact',
|
|
62
|
+
loop_id: contract.loop_id,
|
|
63
|
+
artifact: {
|
|
64
|
+
phase: contract.phase,
|
|
65
|
+
type: primary.type,
|
|
66
|
+
body: '<your full output — non-empty, or it does not count toward the gate>',
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
when: `your ${primary.type} is ready — the phase gate only counts artifacts of this exact type`,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/** Render the brief section. Every value comes off the contract object. */
|
|
73
|
+
export function renderWorkerReplyProse(contract) {
|
|
74
|
+
const action = workerReplyNextAction(contract);
|
|
75
|
+
const typeList = contract.requirements
|
|
76
|
+
.map((r) => `\`${r.type}\`${r.n ? ` (n≥${r.n}, ${r.scope ?? 'phase'} scope)` : ''}`)
|
|
77
|
+
.join(contract.composition === 'any' ? ' OR ' : ' AND ');
|
|
78
|
+
const lines = [
|
|
79
|
+
`## Deliverable contract — loop ${contract.loop_id}, phase "${contract.phase}"`,
|
|
80
|
+
`The phase gate counts artifact type(s): ${typeList}. Any other type — however good the content — is INVISIBLE to the gate and stalls the loop.`,
|
|
81
|
+
`- MCP path: call \`${action.tool}\` with ${JSON.stringify(action.args)}`,
|
|
82
|
+
`- The body must be NON-EMPTY: an artifact without usable content does not count toward the gate.`,
|
|
83
|
+
`- Body cap: ${contract.body_max_bytes} bytes. If your output is larger, write the full version to a markdown file in your worktree and put a dense summary plus the file path in the body.`,
|
|
84
|
+
`- File fallback (no MCP): in LANE-RESULT.json set "artifact_type":"${contract.requirements[0].type}" and put your full output in "body" — the harvester records it under this contract.`,
|
|
85
|
+
`- This contract is FROZEN for loop version ${contract.loop_version}, phase "${contract.phase}". If your submit reports a version conflict or the loop has advanced, your work is still recorded under phase "${contract.phase}" — do not re-target a newer phase.`,
|
|
86
|
+
];
|
|
87
|
+
if (contract.other_conditions.length > 0) {
|
|
88
|
+
lines.push(`- Note: the gate also requires ${contract.other_conditions.join(', ')} — producing artifacts alone may not advance the phase; that part is the coordinator's to satisfy.`);
|
|
89
|
+
}
|
|
90
|
+
lines.push('');
|
|
91
|
+
return lines.join('\n');
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=worker-reply-contract.js.map
|
|
@@ -366,6 +366,7 @@ export async function dispatchReviewLoopTurn(input) {
|
|
|
366
366
|
scope,
|
|
367
367
|
worktreePath: claimResult.worktreePath,
|
|
368
368
|
assignmentId,
|
|
369
|
+
cwd, // pln#638 PR-6b — the context envelope reads the store
|
|
369
370
|
});
|
|
370
371
|
const msg = sendMessage({
|
|
371
372
|
from: input.dispatcherAgent,
|
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.
|
|
2
|
+
// Source: brainclaw v1.20.0 on 2026-08-02T17:17:41.989Z
|
|
3
3
|
export const FACTS = {
|
|
4
|
-
"version": "1.
|
|
5
|
-
"generated_at": "2026-08-
|
|
4
|
+
"version": "1.20.0",
|
|
5
|
+
"generated_at": "2026-08-02T17:17:41.989Z",
|
|
6
6
|
"tools": {
|
|
7
7
|
"count": 67,
|
|
8
8
|
"published_count": 65,
|
|
@@ -474,7 +474,7 @@ export const FACTS = {
|
|
|
474
474
|
},
|
|
475
475
|
"bench": {
|
|
476
476
|
"schema": "brainclaw.bench.v1",
|
|
477
|
-
"generated_at": "2026-08-
|
|
477
|
+
"generated_at": "2026-08-02T17:17:40.300Z",
|
|
478
478
|
"node_version": "v24.18.0",
|
|
479
479
|
"platform": "linux-x64",
|
|
480
480
|
"repeats": 3,
|
|
@@ -483,7 +483,7 @@ export const FACTS = {
|
|
|
483
483
|
"name": "cold_onboard",
|
|
484
484
|
"volume": "empty",
|
|
485
485
|
"description": "fresh machine → init → first useful context. Baseline for time-to-first-value.",
|
|
486
|
-
"duration_ms_median":
|
|
486
|
+
"duration_ms_median": 65,
|
|
487
487
|
"payload_chars_median": 1640,
|
|
488
488
|
"payload_tokens_est_median": 410
|
|
489
489
|
},
|
|
@@ -491,15 +491,15 @@ export const FACTS = {
|
|
|
491
491
|
"name": "warm_work",
|
|
492
492
|
"volume": "medium",
|
|
493
493
|
"description": "bclaw_work consult over a real-shaped store (~200 plans / 500 handoffs / 450 claims).",
|
|
494
|
-
"duration_ms_median":
|
|
495
|
-
"payload_chars_median":
|
|
496
|
-
"payload_tokens_est_median":
|
|
494
|
+
"duration_ms_median": 101,
|
|
495
|
+
"payload_chars_median": 2625,
|
|
496
|
+
"payload_tokens_est_median": 656
|
|
497
497
|
},
|
|
498
498
|
{
|
|
499
499
|
"name": "first_edit",
|
|
500
500
|
"volume": "medium",
|
|
501
501
|
"description": "code_find + code_brief on the fresh-agent path (missing index, first touch).",
|
|
502
|
-
"duration_ms_median":
|
|
502
|
+
"duration_ms_median": 10,
|
|
503
503
|
"payload_chars_median": 499,
|
|
504
504
|
"payload_tokens_est_median": 125
|
|
505
505
|
}
|
package/dist/facts.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.
|
|
3
|
-
"generated_at": "2026-08-
|
|
2
|
+
"version": "1.20.0",
|
|
3
|
+
"generated_at": "2026-08-02T17:17:41.989Z",
|
|
4
4
|
"tools": {
|
|
5
5
|
"count": 67,
|
|
6
6
|
"published_count": 65,
|
|
@@ -472,7 +472,7 @@
|
|
|
472
472
|
},
|
|
473
473
|
"bench": {
|
|
474
474
|
"schema": "brainclaw.bench.v1",
|
|
475
|
-
"generated_at": "2026-08-
|
|
475
|
+
"generated_at": "2026-08-02T17:17:40.300Z",
|
|
476
476
|
"node_version": "v24.18.0",
|
|
477
477
|
"platform": "linux-x64",
|
|
478
478
|
"repeats": 3,
|
|
@@ -481,7 +481,7 @@
|
|
|
481
481
|
"name": "cold_onboard",
|
|
482
482
|
"volume": "empty",
|
|
483
483
|
"description": "fresh machine → init → first useful context. Baseline for time-to-first-value.",
|
|
484
|
-
"duration_ms_median":
|
|
484
|
+
"duration_ms_median": 65,
|
|
485
485
|
"payload_chars_median": 1640,
|
|
486
486
|
"payload_tokens_est_median": 410
|
|
487
487
|
},
|
|
@@ -489,15 +489,15 @@
|
|
|
489
489
|
"name": "warm_work",
|
|
490
490
|
"volume": "medium",
|
|
491
491
|
"description": "bclaw_work consult over a real-shaped store (~200 plans / 500 handoffs / 450 claims).",
|
|
492
|
-
"duration_ms_median":
|
|
493
|
-
"payload_chars_median":
|
|
494
|
-
"payload_tokens_est_median":
|
|
492
|
+
"duration_ms_median": 101,
|
|
493
|
+
"payload_chars_median": 2625,
|
|
494
|
+
"payload_tokens_est_median": 656
|
|
495
495
|
},
|
|
496
496
|
{
|
|
497
497
|
"name": "first_edit",
|
|
498
498
|
"volume": "medium",
|
|
499
499
|
"description": "code_find + code_brief on the fresh-agent path (missing index, first touch).",
|
|
500
|
-
"duration_ms_median":
|
|
500
|
+
"duration_ms_median": 10,
|
|
501
501
|
"payload_chars_median": 499,
|
|
502
502
|
"payload_tokens_est_median": 125
|
|
503
503
|
}
|
|
@@ -8,6 +8,20 @@ guarantees this changelog follows.
|
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
+
## [1.20.0] — 2026-08-02
|
|
12
|
+
|
|
13
|
+
**Fixed — 1.19.0 contract entries now actually emitted (#158)**
|
|
14
|
+
- `generated_surfaces_stale` (surfaced as `stale_surfaces`) and session-end
|
|
15
|
+
`scope_warnings` were documented below in 1.19.0 but computed-then-dropped
|
|
16
|
+
before the MCP boundary on the surfaces agents actually call. They now reach
|
|
17
|
+
`bclaw_session_start`, `bclaw_session_end` and `bclaw_work` responses as
|
|
18
|
+
`warnings` + `warning_details`. `shared_checkout_warning` (session-start)
|
|
19
|
+
rides the same seam.
|
|
20
|
+
- Read contract only: no tool added/removed/renamed, no inputSchema change,
|
|
21
|
+
no surface-fingerprint movement. The dispatch-brief changes this release
|
|
22
|
+
(transport section, context envelope, worker reply contract, lane session
|
|
23
|
+
prohibition) are payload text, not protocol.
|
|
24
|
+
|
|
11
25
|
## [1.19.0] — 2026-08-01
|
|
12
26
|
|
|
13
27
|
**Added — `warning_details` on the facade response contract (pln#635)**
|