brainclaw 1.14.0 → 1.16.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 +16 -263
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/cli/register-capture.js +209 -0
- package/dist/cli/register-code-map.js +19 -0
- package/dist/cli/register-coordination.js +472 -0
- package/dist/cli/register-federation.js +258 -0
- package/dist/cli/register-lifecycle.js +436 -0
- package/dist/cli/register-memory-context.js +502 -0
- package/dist/cli/register-planning.js +167 -0
- package/dist/cli/register-review.js +149 -0
- package/dist/cli/shared.js +5 -0
- package/dist/cli.js +212 -2015
- package/dist/commands/dispatch-watch.js +25 -2
- package/dist/commands/harvest.js +31 -6
- package/dist/commands/mcp-catalog.js +1438 -0
- package/dist/commands/mcp-contract.js +33 -0
- package/dist/commands/mcp-presentation.js +27 -0
- package/dist/commands/mcp-read-handlers.js +72 -36
- package/dist/commands/mcp-write-admin.js +328 -0
- package/dist/commands/mcp-write-claims.js +864 -0
- package/dist/commands/mcp-write-coordination.js +1825 -0
- package/dist/commands/mcp-write-entities.js +620 -0
- package/dist/commands/mcp-write-memory.js +451 -0
- package/dist/commands/mcp-write-sequences.js +116 -0
- package/dist/commands/mcp-write-support.js +367 -0
- package/dist/commands/mcp.js +261 -5570
- package/dist/commands/update-handoff.js +28 -42
- package/dist/core/agent-capability.js +31 -14
- package/dist/core/agent-files.js +1 -1
- package/dist/core/agent-registry.js +51 -3
- package/dist/core/claims.js +18 -0
- package/dist/core/coordination.js +5 -2
- package/dist/core/cross-project.js +35 -1
- package/dist/core/dispatcher.js +34 -20
- package/dist/core/entity-operations.js +335 -12
- package/dist/core/entity-registry.js +72 -9
- package/dist/core/execution.js +28 -4
- package/dist/core/facade-schema.js +30 -4
- package/dist/core/federation-cloud.js +142 -11
- package/dist/core/federation-outbox.js +292 -0
- package/dist/core/federation-signing.js +115 -0
- package/dist/core/handoff-review.js +35 -0
- package/dist/core/io.js +6 -0
- package/dist/core/protocol-tool-policy.js +113 -0
- package/dist/core/review-loop-close.js +115 -0
- package/dist/core/schema.js +25 -2
- package/dist/core/security-detectors.js +35 -6
- package/dist/core/security.js +32 -12
- package/dist/core/worktree.js +98 -9
- package/dist/facts.js +13 -11
- package/dist/facts.json +12 -10
- package/docs/PROTOCOL.md +7 -3
- package/docs/concepts/coordinator-runbook.md +3 -0
- package/docs/concepts/dispatch-lifecycle.md +4 -4
- package/docs/concepts/loop-engine.md +3 -1
- package/docs/concepts/troubleshooting.md +1 -1
- package/docs/integrations/codex.md +3 -3
- package/docs/integrations/overview.md +1 -1
- package/docs/mcp-schema-changelog.md +153 -2
- package/docs/playbooks/orchestration.md +1 -1
- package/docs/product/entity-model-audit.md +3 -2
- package/docs/security.md +22 -1
- package/package.json +3 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { loadState, persistState } from '../core/state.js';
|
|
2
2
|
import { memoryExists } from '../core/io.js';
|
|
3
|
-
import {
|
|
3
|
+
import { mergeHandoffReview } from '../core/handoff-review.js';
|
|
4
4
|
export function applyHandoffUpdates(handoff, options = {}) {
|
|
5
5
|
if (options.status)
|
|
6
6
|
handoff.status = options.status;
|
|
@@ -18,47 +18,33 @@ export function applyHandoffUpdates(handoff, options = {}) {
|
|
|
18
18
|
if (Object.keys(contractUpdates).length > 0) {
|
|
19
19
|
handoff.contract = { ...handoff.contract, ...contractUpdates };
|
|
20
20
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
review.summary = options.review_summary;
|
|
49
|
-
if (options.blocking_issues !== undefined)
|
|
50
|
-
review.blocking_issues = options.blocking_issues;
|
|
51
|
-
if (options.suggestions !== undefined)
|
|
52
|
-
review.suggestions = options.suggestions;
|
|
53
|
-
const reviewCompleted = options.review_verdict !== undefined ||
|
|
54
|
-
options.reviewed_by !== undefined ||
|
|
55
|
-
options.review_summary !== undefined ||
|
|
56
|
-
options.blocking_issues !== undefined ||
|
|
57
|
-
options.suggestions !== undefined;
|
|
58
|
-
if (reviewCompleted) {
|
|
59
|
-
review.reviewed_at = nowISO();
|
|
60
|
-
}
|
|
61
|
-
handoff.review = review;
|
|
21
|
+
// Translate the flat options into a nested review patch and delegate the
|
|
22
|
+
// merge + reviewed_at stamping to the shared core helper — the SINGLE source
|
|
23
|
+
// of truth shared with updateEntity(handoff) so the two write paths cannot
|
|
24
|
+
// drift (pln#625 Phase 3, Codex review of #84).
|
|
25
|
+
const reviewPatch = {};
|
|
26
|
+
if (options.reviewer !== undefined)
|
|
27
|
+
reviewPatch.reviewer = options.reviewer;
|
|
28
|
+
if (options.requester !== undefined)
|
|
29
|
+
reviewPatch.requester = options.requester;
|
|
30
|
+
if (options.requested_at !== undefined)
|
|
31
|
+
reviewPatch.requested_at = options.requested_at;
|
|
32
|
+
if (options.review_thread_id !== undefined)
|
|
33
|
+
reviewPatch.thread_id = options.review_thread_id;
|
|
34
|
+
if (options.review_message_id !== undefined)
|
|
35
|
+
reviewPatch.message_id = options.review_message_id;
|
|
36
|
+
if (options.review_verdict !== undefined)
|
|
37
|
+
reviewPatch.verdict = options.review_verdict;
|
|
38
|
+
if (options.reviewed_by !== undefined)
|
|
39
|
+
reviewPatch.reviewed_by = options.reviewed_by;
|
|
40
|
+
if (options.review_summary !== undefined)
|
|
41
|
+
reviewPatch.summary = options.review_summary;
|
|
42
|
+
if (options.blocking_issues !== undefined)
|
|
43
|
+
reviewPatch.blocking_issues = options.blocking_issues;
|
|
44
|
+
if (options.suggestions !== undefined)
|
|
45
|
+
reviewPatch.suggestions = options.suggestions;
|
|
46
|
+
if (Object.keys(reviewPatch).length > 0) {
|
|
47
|
+
handoff.review = mergeHandoffReview(handoff.review, reviewPatch);
|
|
62
48
|
}
|
|
63
49
|
return handoff;
|
|
64
50
|
}
|
|
@@ -147,10 +147,16 @@ const PROFILES = {
|
|
|
147
147
|
invoke_binary: 'opencode',
|
|
148
148
|
invoke_review_template: 'opencode "{prompt}"',
|
|
149
149
|
},
|
|
150
|
-
// Sandbox note
|
|
151
|
-
// the
|
|
152
|
-
//
|
|
153
|
-
//
|
|
150
|
+
// Sandbox note (CORRECTED — dec#133, empirical probe codex 0.144.4, 2026-07-18):
|
|
151
|
+
// the earlier belief that `--sandbox workspace-write` blocks brainclaw MCP was
|
|
152
|
+
// FALSE and never re-verified. The MCP server runs as a SEPARATE process outside
|
|
153
|
+
// the sandbox, and `approval_policy=never` (baked into the invoke template below)
|
|
154
|
+
// auto-approves every tool call in headless mode — so MCP reads/writes are
|
|
155
|
+
// reachable from a sandboxed codex run. The REAL residual constraint is `git
|
|
156
|
+
// commit`: the sandbox root excludes `.git`, so the coordinator commits the
|
|
157
|
+
// worktree diff at harvest time (see dispatchCanCommit / harvest.ts). Candidates
|
|
158
|
+
// can still be dropped as filesystem JSON as a fallback, but MCP is not the
|
|
159
|
+
// blocker.
|
|
154
160
|
codex: {
|
|
155
161
|
name: 'codex', category: 'code-agent', workflowModel: 'task-based',
|
|
156
162
|
hasMcp: true, hasHooks: false, hasAutoApprove: false, hasSkills: true, hasRules: true,
|
|
@@ -693,25 +699,36 @@ export function resolveBriefMode(agentName) {
|
|
|
693
699
|
* pln#528 — capability matrix DERIVED from the spawn template, so it stays in
|
|
694
700
|
* sync with how each agent is actually invoked (no per-profile duplication).
|
|
695
701
|
*
|
|
696
|
-
*
|
|
697
|
-
*
|
|
698
|
-
*
|
|
699
|
-
*
|
|
700
|
-
*
|
|
701
|
-
*
|
|
702
|
-
*
|
|
702
|
+
* pln#628 Focus 4A CORRECTION (dec#133, empirical probe codex 0.144.4): the
|
|
703
|
+
* original pln#528 belief — that a `--sandbox` spawn "does NOT wire the brainclaw
|
|
704
|
+
* MCP server" — was a FALSE premise that was never re-verified. In reality the MCP
|
|
705
|
+
* server is a separate out-of-sandbox process and `approval_policy=never`
|
|
706
|
+
* auto-approves every tool call, so MCP is reachable from a sandboxed run. The
|
|
707
|
+
* ONE residual constraint the sandbox actually imposes is `git commit` (.git sits
|
|
708
|
+
* outside the writable root). So the two capabilities are now decoupled: sandbox
|
|
709
|
+
* ⇏ no-MCP, sandbox ⇒ no-commit.
|
|
703
710
|
*/
|
|
704
711
|
export function isSandboxedSpawn(profile) {
|
|
705
712
|
return /--sandbox\b/.test(profile.invoke_template ?? '');
|
|
706
713
|
}
|
|
707
|
-
/**
|
|
714
|
+
/**
|
|
715
|
+
* Whether the agent, AS SPAWNED by the dispatcher, can reach brainclaw MCP.
|
|
716
|
+
*
|
|
717
|
+
* pln#628 Focus 4A: this is NO LONGER gated by isSandboxedSpawn. dec#133 proved
|
|
718
|
+
* empirically that a sandboxed codex run reaches MCP (both whitelisted and
|
|
719
|
+
* non-whitelisted tools fired) — the sandbox does not sever MCP, it only makes
|
|
720
|
+
* `.git` read-only. MCP reachability therefore tracks `runtime.mcp_direct` alone;
|
|
721
|
+
* the commit constraint is expressed separately by dispatchCanCommit.
|
|
722
|
+
*/
|
|
708
723
|
export function dispatchHasMcp(profile) {
|
|
709
|
-
return profile.runtime.mcp_direct
|
|
724
|
+
return profile.runtime.mcp_direct;
|
|
710
725
|
}
|
|
711
726
|
/**
|
|
712
727
|
* Whether the spawned worker can `git commit`. A sandbox whose root excludes
|
|
713
728
|
* `.git` cannot — the coordinator must integrate the worker's output instead of
|
|
714
|
-
* relying on a self-commit handoff.
|
|
729
|
+
* relying on a self-commit handoff. NOTE (dec#133): commit-from-sandbox was NOT
|
|
730
|
+
* verified to work even on Windows, so this stays conservative (sandbox ⇒ no
|
|
731
|
+
* commit); do not relax it to a platform check without an empirical probe.
|
|
715
732
|
*/
|
|
716
733
|
export function dispatchCanCommit(profile) {
|
|
717
734
|
return !isSandboxedSpawn(profile);
|
package/dist/core/agent-files.js
CHANGED
|
@@ -3,7 +3,7 @@ import os from 'node:os';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { spawnSync } from 'node:child_process';
|
|
5
5
|
import yaml from 'yaml';
|
|
6
|
-
import { MCP_HEADLESS_AUTO_TOOL_NAMES, MCP_CANONICAL_GRAMMAR_TOOL_NAMES, REMOVED_IN_V1_TOOLS } from '
|
|
6
|
+
import { MCP_HEADLESS_AUTO_TOOL_NAMES, MCP_CANONICAL_GRAMMAR_TOOL_NAMES, REMOVED_IN_V1_TOOLS } from './protocol-tool-policy.js';
|
|
7
7
|
import { renderToml, tomlArrayTableHasEntry } from './toml-writer.js';
|
|
8
8
|
import { PROTOCOL_SKILLS, renderProtocolSkill } from './protocol-skills.js';
|
|
9
9
|
import { getInstalledBrainclawVersion } from './brainclaw-version.js';
|
|
@@ -140,8 +140,17 @@ function ensureParentDir(filepath) {
|
|
|
140
140
|
fs.mkdirSync(dir, { recursive: true });
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
|
-
|
|
144
|
-
|
|
143
|
+
/**
|
|
144
|
+
* Canonical Ed25519 public-key fingerprint (pln#101): sha256 over the PEM with
|
|
145
|
+
* carriage returns stripped and surrounding whitespace trimmed, so a trailing
|
|
146
|
+
* newline or CRLF (e.g. introduced by copy-paste through the cloud UI) does NOT
|
|
147
|
+
* change the fingerprint. The cloud computes the same canonical value — see
|
|
148
|
+
* brainclaw-cloud/src/handlers/agents.ts fingerprintPem — so a local↔remote
|
|
149
|
+
* match is a reliable proof of the same key regardless of PEM formatting.
|
|
150
|
+
*/
|
|
151
|
+
export function fingerprintPublicKeyPem(publicKeyPem) {
|
|
152
|
+
const canonical = publicKeyPem.replace(/\r/g, '').trim();
|
|
153
|
+
return crypto.createHash('sha256').update(canonical).digest('hex');
|
|
145
154
|
}
|
|
146
155
|
function buildIdentityKey(agentId, env = process.env, forceRegenerate = false) {
|
|
147
156
|
migrateLegacyAgentKey(agentId, env);
|
|
@@ -168,10 +177,49 @@ function buildIdentityKey(agentId, env = process.env, forceRegenerate = false) {
|
|
|
168
177
|
return {
|
|
169
178
|
algorithm: 'ed25519',
|
|
170
179
|
public_key: publicKeyPem,
|
|
171
|
-
fingerprint:
|
|
180
|
+
fingerprint: fingerprintPublicKeyPem(publicKeyPem),
|
|
172
181
|
created_at: createdAt,
|
|
173
182
|
};
|
|
174
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* Load an agent's Ed25519 signing material for cloud request signing (pln#100).
|
|
186
|
+
*
|
|
187
|
+
* Reads the private key from the neutral key store (~/.brainclaw/keys/),
|
|
188
|
+
* migrating from the legacy CODEX_HOME location if needed, and derives the SPKI
|
|
189
|
+
* public-key PEM plus its fingerprint — the same sha256(pem) the cloud stores as
|
|
190
|
+
* agents.key_fingerprint, so a local↔remote fingerprint match is a byte-for-byte
|
|
191
|
+
* proof of the same key. Returns undefined when no key has been generated yet:
|
|
192
|
+
* signing NEVER silently mints a key (the public key must be registered with the
|
|
193
|
+
* cloud first). Use registerAgentIdentity({ generateFingerprint: true }) to mint one.
|
|
194
|
+
*/
|
|
195
|
+
export function loadAgentSigningKey(agentId, env = process.env) {
|
|
196
|
+
migrateLegacyAgentKey(agentId, env);
|
|
197
|
+
const filepath = agentKeyPath(agentId);
|
|
198
|
+
if (!fs.existsSync(filepath))
|
|
199
|
+
return undefined;
|
|
200
|
+
const privateKeyPem = fs.readFileSync(filepath, 'utf-8');
|
|
201
|
+
const privateKey = crypto.createPrivateKey(privateKeyPem);
|
|
202
|
+
// @types/node 26 dropped the KeyObject overload from createPublicKey's signature
|
|
203
|
+
// (see buildIdentityKey) — cast to a parameter type the .d.ts still accepts.
|
|
204
|
+
const publicKeyPem = crypto
|
|
205
|
+
.createPublicKey(privateKey)
|
|
206
|
+
.export({ type: 'spki', format: 'pem' })
|
|
207
|
+
.toString();
|
|
208
|
+
return { privateKeyPem, publicKeyPem, fingerprint: fingerprintPublicKeyPem(publicKeyPem) };
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Ensure an agent has an Ed25519 signing key, WITHOUT rotating an existing one
|
|
212
|
+
* (pln#101). Generates the keypair on first call, returns the existing key on
|
|
213
|
+
* subsequent calls — so it is safe to run after the public key has been
|
|
214
|
+
* approved in the cloud (rotating would break the fingerprint match). Returns
|
|
215
|
+
* the SPKI public-key PEM + its sha256(pem) fingerprint.
|
|
216
|
+
*/
|
|
217
|
+
export function ensureAgentSigningKey(agentId, env = process.env) {
|
|
218
|
+
const key = buildIdentityKey(agentId, env, false);
|
|
219
|
+
if (!key)
|
|
220
|
+
throw new Error(`Failed to derive signing key for agent ${agentId}`);
|
|
221
|
+
return { publicKeyPem: key.public_key, fingerprint: key.fingerprint };
|
|
222
|
+
}
|
|
175
223
|
function withIdentityKey(agent, env = process.env, forceRegenerate = false) {
|
|
176
224
|
return {
|
|
177
225
|
...agent,
|
package/dist/core/claims.js
CHANGED
|
@@ -14,6 +14,7 @@ import { loadSessionById } from './identity.js';
|
|
|
14
14
|
import { loadState, persistState } from './state.js';
|
|
15
15
|
import { createRuntimeEvent } from './events.js';
|
|
16
16
|
import { emitRegistryPostImage, registryFaultPoint } from './events/registry-post-image.js';
|
|
17
|
+
import { maybeEnqueueClaimTransition, isFederationEnqueueActive } from './federation-outbox.js';
|
|
17
18
|
/** Parse duration string like '4h', '30m' to ms. */
|
|
18
19
|
function parseTtl(value) {
|
|
19
20
|
const match = /^(\d+)([mhd])$/i.exec(value.trim());
|
|
@@ -69,9 +70,26 @@ function saveClaimUnlocked(claim, cwd, options) {
|
|
|
69
70
|
// pln#568 (I2): journal the post-image BEFORE the projection write, so a
|
|
70
71
|
// crash can only leave the journal ahead of the projection, never behind.
|
|
71
72
|
const created = !store.exists(parsed.id);
|
|
73
|
+
// Federation (pln#101): capture the PREVIOUS status BEFORE the write so we can
|
|
74
|
+
// diff it after (create or active↔terminal transition ⇒ enqueue for cloud
|
|
75
|
+
// sync). Only pay the prev-load when federation is actually active; this whole
|
|
76
|
+
// block runs under the store mutation mutex, which serializes rev reservation.
|
|
77
|
+
const fedActive = isFederationEnqueueActive(cwd, options?.federation?.suppressEnqueue);
|
|
78
|
+
let fedPrevStatus;
|
|
79
|
+
if (fedActive) {
|
|
80
|
+
try {
|
|
81
|
+
fedPrevStatus = loadClaimFromAnyDir(parsed.id, cwd).status;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
fedPrevStatus = undefined;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
72
87
|
emitRegistryPostImage('claim', parsed, { created, agent: parsed.agent, agent_id: parsed.agent_id, session_id: parsed.session_id, cwd });
|
|
73
88
|
registryFaultPoint('after_registry_journal');
|
|
74
89
|
store.save(parsed);
|
|
90
|
+
if (fedActive) {
|
|
91
|
+
maybeEnqueueClaimTransition(parsed, fedPrevStatus, fedPrevStatus === undefined, cwd, options?.federation?.suppressEnqueue);
|
|
92
|
+
}
|
|
75
93
|
const writeDir = claimsDir(cwd, 'write');
|
|
76
94
|
for (const dirPath of claimDirs(cwd)) {
|
|
77
95
|
if (dirPath === writeDir)
|
|
@@ -288,8 +288,11 @@ function buildIncomingSignalsSummary(cwd) {
|
|
|
288
288
|
return {
|
|
289
289
|
id: signal.id,
|
|
290
290
|
entity_type: signal.entity_type,
|
|
291
|
-
|
|
292
|
-
|
|
291
|
+
// Defence-in-depth: listIncomingCrossProjectSignals already filters
|
|
292
|
+
// wrong-shape envelopes, but never let a single missing field crash the
|
|
293
|
+
// whole board render (trp_e90b3198).
|
|
294
|
+
from_project: signal.from_project?.name ?? '?',
|
|
295
|
+
from_agent: signal.from_agent?.name ?? '?',
|
|
293
296
|
created_at: signal.created_at,
|
|
294
297
|
preview: text.length > 120 ? text.slice(0, 117) + '...' : text,
|
|
295
298
|
};
|
|
@@ -128,6 +128,33 @@ export function writeCrossProjectSignal(target, entityType, payload, sourceCwd)
|
|
|
128
128
|
fs.writeFileSync(filepath, JSON.stringify(signal, null, 2) + '\n', 'utf-8');
|
|
129
129
|
return signal;
|
|
130
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* Runtime shape guard for a cross-project signal envelope. A second signaling
|
|
133
|
+
* subsystem can drop schema-incompatible (but valid-JSON) files into the same
|
|
134
|
+
* directory; without this guard a consumer that reads envelope.from_project.name
|
|
135
|
+
* / from_agent.name / created_at crashes with a TypeError on every read
|
|
136
|
+
* (e.g. bclaw_context board — reachable purely locally). Guarding here — the
|
|
137
|
+
* single source of these envelopes — keeps every consumer safe.
|
|
138
|
+
*/
|
|
139
|
+
const CROSS_PROJECT_SIGNAL_ENTITIES = new Set(['candidate', 'handoff', 'runtime_note']);
|
|
140
|
+
function isCrossProjectSignalEnvelope(value) {
|
|
141
|
+
if (!value || typeof value !== 'object')
|
|
142
|
+
return false;
|
|
143
|
+
const v = value;
|
|
144
|
+
const fromProject = v.from_project;
|
|
145
|
+
const fromAgent = v.from_agent;
|
|
146
|
+
return typeof v.id === 'string'
|
|
147
|
+
&& typeof v.entity_type === 'string'
|
|
148
|
+
// entity_type must be one of ours — a foreign subsystem's value would flow
|
|
149
|
+
// downstream as a bogus type.
|
|
150
|
+
&& CROSS_PROJECT_SIGNAL_ENTITIES.has(v.entity_type)
|
|
151
|
+
&& typeof v.created_at === 'string'
|
|
152
|
+
&& typeof fromProject?.name === 'string'
|
|
153
|
+
&& typeof fromAgent?.name === 'string'
|
|
154
|
+
// payload MUST be a non-null object: the consumer does `'text' in payload`,
|
|
155
|
+
// which throws a TypeError on a primitive/null payload (Codex review of #85).
|
|
156
|
+
&& typeof v.payload === 'object' && v.payload !== null;
|
|
157
|
+
}
|
|
131
158
|
/**
|
|
132
159
|
* Lists cross-project signals materialized in the local inbox.
|
|
133
160
|
*/
|
|
@@ -142,7 +169,14 @@ export function listIncomingCrossProjectSignals(cwd) {
|
|
|
142
169
|
continue;
|
|
143
170
|
const filepath = path.join(dir, entry);
|
|
144
171
|
try {
|
|
145
|
-
|
|
172
|
+
const parsed = JSON.parse(fs.readFileSync(filepath, 'utf-8'));
|
|
173
|
+
// Skip files that are valid JSON but not our envelope shape (schema drift
|
|
174
|
+
// from another signaling subsystem sharing this directory) — matching the
|
|
175
|
+
// existing "ignore malformed" intent, but for wrong-shape as well as
|
|
176
|
+
// wrong-syntax.
|
|
177
|
+
if (isCrossProjectSignalEnvelope(parsed)) {
|
|
178
|
+
signals.push(parsed);
|
|
179
|
+
}
|
|
146
180
|
}
|
|
147
181
|
catch {
|
|
148
182
|
// Ignore malformed signal files.
|
package/dist/core/dispatcher.js
CHANGED
|
@@ -307,9 +307,11 @@ export function buildProtocolSection(options) {
|
|
|
307
307
|
if (options.worktreePath) {
|
|
308
308
|
parts.push('**Compile check**: before every commit, `tsc --noEmit` (or the project build) must pass — a per-worktree pre-commit gate may enforce this and reject the commit otherwise. Do not bypass with --no-verify unless you intend to hand off a known-broken state.');
|
|
309
309
|
}
|
|
310
|
-
// pln#526: standard fallback channel — works even
|
|
311
|
-
//
|
|
312
|
-
|
|
310
|
+
// pln#526: standard fallback channel — works even if bclaw_assignment_update
|
|
311
|
+
// fails in your environment. pln#628 Focus 4A: sandbox is NO LONGER a reason
|
|
312
|
+
// MCP is unavailable (dec#133), so this is framed as a generic fallback, not a
|
|
313
|
+
// sandbox instruction. The coordinator ingests it with `brainclaw harvest`.
|
|
314
|
+
parts.push(`Final fallback (if bclaw_assignment_update / MCP is unavailable in your environment): write LANE-RESULT.json at the worktree root — {"assignment_id":"${options.assignmentId}","status":"completed|blocked|failed","summary":"<what you did>","files_changed":["..."],"artifacts":["..."]}. The coordinator harvests it via \`brainclaw harvest ${options.assignmentId}\`.`);
|
|
313
315
|
}
|
|
314
316
|
else if (options?.claimId) {
|
|
315
317
|
parts.push('1. Call bclaw_session_start to register your session');
|
|
@@ -468,22 +470,25 @@ export function generateBrief(plan, item, cwd, briefMode, options) {
|
|
|
468
470
|
if (mode === 'full') {
|
|
469
471
|
parts.push(buildProtocolSection(options));
|
|
470
472
|
}
|
|
471
|
-
// pln#
|
|
472
|
-
//
|
|
473
|
-
//
|
|
474
|
-
//
|
|
475
|
-
//
|
|
476
|
-
//
|
|
477
|
-
//
|
|
478
|
-
// the
|
|
479
|
-
//
|
|
473
|
+
// pln#628 Focus 4A — transport addendum, now keyed to the ACTUAL missing
|
|
474
|
+
// capability. Originally (pln#528) this fired for any sandboxed spawn and
|
|
475
|
+
// claimed "no MCP + no commit". dec#133 proved the "no MCP" half FALSE: a
|
|
476
|
+
// sandboxed codex reaches MCP (separate out-of-sandbox process +
|
|
477
|
+
// approval_policy=never). dispatchHasMcp now tracks runtime.mcp_direct alone,
|
|
478
|
+
// so this block only fires for genuinely MCP-less agents (nanoclaw/nemoclaw/
|
|
479
|
+
// zeroclaw). For them the Protocol section's MCP lifecycle does not apply and
|
|
480
|
+
// the file protocol is the sole channel. Sandboxed-but-MCP-capable agents
|
|
481
|
+
// (codex) no longer receive a self-contradictory "MCP NOT reachable / Do NOT
|
|
482
|
+
// call bclaw_*" note: their coherent message is carried by the Protocol section
|
|
483
|
+
// (MCP primary + LANE-RESULT.json fallback) and working-defaults (canCommit=
|
|
484
|
+
// false → the coordinator commits their worktree at harvest).
|
|
480
485
|
if (briefProfile && !dispatchHasMcp(briefProfile)) {
|
|
481
|
-
parts.push('## ⚠ Transport:
|
|
482
|
-
parts.push('Your runtime
|
|
486
|
+
parts.push('## ⚠ Transport: no MCP (file protocol only)');
|
|
487
|
+
parts.push('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:');
|
|
483
488
|
const asgn = options?.assignmentId ?? '<assignment_id>';
|
|
484
489
|
parts.push(`- When done, write LANE-RESULT.json at the worktree root: {"assignment_id":"${asgn}","status":"completed|blocked|failed","summary":"<what you did>","files_changed":["..."]}.`);
|
|
485
490
|
parts.push('- Capture decisions/traps as candidate JSON under .brainclaw/coordination/inbox/ (the coordinator harvests them).');
|
|
486
|
-
parts.push('- Do NOT call bclaw_* tools — they are unavailable here. The coordinator harvests your result and integrates
|
|
491
|
+
parts.push('- Do NOT call bclaw_* tools — they are unavailable here. The coordinator harvests your result and integrates it.');
|
|
487
492
|
parts.push('');
|
|
488
493
|
}
|
|
489
494
|
// Codex-specific constraints: focus and speed guidance for sandboxed runs.
|
|
@@ -525,14 +530,17 @@ export function generateDispatchBrief(options) {
|
|
|
525
530
|
assignmentId: options.assignmentId,
|
|
526
531
|
}));
|
|
527
532
|
}
|
|
528
|
-
// pln#
|
|
533
|
+
// pln#628 Focus 4A — transport addendum keyed to the ACTUAL missing capability
|
|
534
|
+
// (see generateBrief for the full rationale + dec#133). Fires only for
|
|
535
|
+
// genuinely MCP-less agents; sandboxed-but-MCP-capable codex no longer gets a
|
|
536
|
+
// self-contradictory "no MCP / Do NOT call bclaw_*" note.
|
|
529
537
|
if (taskBriefProfile && !dispatchHasMcp(taskBriefProfile)) {
|
|
530
|
-
parts.push('## ⚠ Transport:
|
|
531
|
-
parts.push('Your runtime
|
|
538
|
+
parts.push('## ⚠ Transport: no MCP (file protocol only)');
|
|
539
|
+
parts.push('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:');
|
|
532
540
|
const asgn = options.assignmentId ?? '<assignment_id>';
|
|
533
541
|
parts.push(`- When done, write LANE-RESULT.json at the worktree root: {"assignment_id":"${asgn}","status":"completed|blocked|failed","summary":"<what you did>","files_changed":["..."]}.`);
|
|
534
542
|
parts.push('- Capture decisions/traps as candidate JSON under .brainclaw/coordination/inbox/ (the coordinator harvests them).');
|
|
535
|
-
parts.push('- Do NOT call bclaw_* tools — they are unavailable here. The coordinator harvests your result and integrates
|
|
543
|
+
parts.push('- Do NOT call bclaw_* tools — they are unavailable here. The coordinator harvests your result and integrates it.');
|
|
536
544
|
parts.push('');
|
|
537
545
|
}
|
|
538
546
|
// Codex-specific constraints: focus and speed guidance for sandboxed runs
|
|
@@ -902,13 +910,19 @@ export async function dispatch(options, cwd) {
|
|
|
902
910
|
requireWorktree: true, // pln#531: never spawn a worker in the integration repo
|
|
903
911
|
});
|
|
904
912
|
entry.execution_status = execResult.execution_status;
|
|
913
|
+
// pln#626 Phase 1 — mirror the coordinate path: carry the reason so a
|
|
914
|
+
// command_ready_manual sequence item says WHY it didn't spawn.
|
|
915
|
+
if (execResult.execution_reason)
|
|
916
|
+
entry.execution_reason = execResult.execution_reason;
|
|
917
|
+
if (execResult.failure_kind)
|
|
918
|
+
entry.failure_kind = execResult.failure_kind;
|
|
905
919
|
if (execResult.pid)
|
|
906
920
|
entry.pid = execResult.pid;
|
|
907
921
|
if (execResult.execution_status === 'delivered_and_started') {
|
|
908
922
|
entry.channel = 'spawned_cli';
|
|
909
923
|
}
|
|
910
924
|
if (execResult.error)
|
|
911
|
-
result.warnings.push(execResult.error);
|
|
925
|
+
result.warnings.push(`${entry.agent}: ${execResult.error}`);
|
|
912
926
|
if (entry.assignment_id && entry.claim_id) {
|
|
913
927
|
if (execResult.failure_kind === 'spawn_no_handshake') {
|
|
914
928
|
try {
|