brainclaw 1.15.0 → 1.17.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 +25 -4
- 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 -2183
- package/dist/commands/dispatch-watch.js +25 -2
- package/dist/commands/harvest.js +107 -20
- 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 -5584
- package/dist/commands/update-handoff.js +28 -42
- package/dist/core/agent-capability.js +38 -16
- package/dist/core/agent-files.js +54 -3
- package/dist/core/agent-integrations.js +1 -0
- package/dist/core/coordination.js +5 -2
- package/dist/core/cross-project.js +35 -1
- package/dist/core/dispatcher.js +67 -27
- 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 +18 -4
- package/dist/core/handoff-review.js +35 -0
- package/dist/core/protocol-tool-policy.js +113 -0
- package/dist/core/review-loop-close.js +184 -0
- package/dist/core/review-loop-turn-dispatch.js +183 -0
- package/dist/core/schema.js +24 -2
- package/dist/core/security-detectors.js +35 -6
- package/dist/core/security.js +32 -12
- package/dist/core/worktree.js +274 -12
- 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 +6 -2
- package/docs/concepts/troubleshooting.md +1 -1
- package/docs/integrations/codex.md +22 -6
- package/docs/integrations/overview.md +1 -1
- package/docs/mcp-schema-changelog.md +137 -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,16 +147,27 @@ 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
|
+
// hooks: Codex gained a native lifecycle hook surface (SessionStart /
|
|
163
|
+
// UserPromptSubmit / Stop / PreToolUse / … via .codex/hooks.json or [hooks]
|
|
164
|
+
// in config.toml; developers.openai.com/codex/hooks, verified 2026-07 —
|
|
165
|
+
// trp_fe75dafc). brainclaw writes .codex/hooks.json (ensureCodexHooks),
|
|
166
|
+
// giving Codex the same session-lifecycle wiring as Claude Code.
|
|
167
|
+
hasMcp: true, hasHooks: true, hasAutoApprove: false, hasSkills: true, hasRules: true,
|
|
157
168
|
instructionFile: 'AGENTS.md', sharedInstructionFile: true, mcpConfigScope: 'machine', templateTier: 'A',
|
|
158
169
|
role_capabilities: ['execute', 'review'],
|
|
159
|
-
runtime: { mcp_direct: true, hooks:
|
|
170
|
+
runtime: { mcp_direct: true, hooks: true, canBeSpawnedCli: true, canSpawnOtherCli: false, inbox: true },
|
|
160
171
|
max_concurrent_tasks: 5,
|
|
161
172
|
// pln#475: prefer stdin_pipe to avoid Windows cmd.exe arg-parsing breaking
|
|
162
173
|
// long prompts. codex.cmd resolves through cmd shell, where embedded
|
|
@@ -693,25 +704,36 @@ export function resolveBriefMode(agentName) {
|
|
|
693
704
|
* pln#528 — capability matrix DERIVED from the spawn template, so it stays in
|
|
694
705
|
* sync with how each agent is actually invoked (no per-profile duplication).
|
|
695
706
|
*
|
|
696
|
-
*
|
|
697
|
-
*
|
|
698
|
-
*
|
|
699
|
-
*
|
|
700
|
-
*
|
|
701
|
-
*
|
|
702
|
-
*
|
|
707
|
+
* pln#628 Focus 4A CORRECTION (dec#133, empirical probe codex 0.144.4): the
|
|
708
|
+
* original pln#528 belief — that a `--sandbox` spawn "does NOT wire the brainclaw
|
|
709
|
+
* MCP server" — was a FALSE premise that was never re-verified. In reality the MCP
|
|
710
|
+
* server is a separate out-of-sandbox process and `approval_policy=never`
|
|
711
|
+
* auto-approves every tool call, so MCP is reachable from a sandboxed run. The
|
|
712
|
+
* ONE residual constraint the sandbox actually imposes is `git commit` (.git sits
|
|
713
|
+
* outside the writable root). So the two capabilities are now decoupled: sandbox
|
|
714
|
+
* ⇏ no-MCP, sandbox ⇒ no-commit.
|
|
703
715
|
*/
|
|
704
716
|
export function isSandboxedSpawn(profile) {
|
|
705
717
|
return /--sandbox\b/.test(profile.invoke_template ?? '');
|
|
706
718
|
}
|
|
707
|
-
/**
|
|
719
|
+
/**
|
|
720
|
+
* Whether the agent, AS SPAWNED by the dispatcher, can reach brainclaw MCP.
|
|
721
|
+
*
|
|
722
|
+
* pln#628 Focus 4A: this is NO LONGER gated by isSandboxedSpawn. dec#133 proved
|
|
723
|
+
* empirically that a sandboxed codex run reaches MCP (both whitelisted and
|
|
724
|
+
* non-whitelisted tools fired) — the sandbox does not sever MCP, it only makes
|
|
725
|
+
* `.git` read-only. MCP reachability therefore tracks `runtime.mcp_direct` alone;
|
|
726
|
+
* the commit constraint is expressed separately by dispatchCanCommit.
|
|
727
|
+
*/
|
|
708
728
|
export function dispatchHasMcp(profile) {
|
|
709
|
-
return profile.runtime.mcp_direct
|
|
729
|
+
return profile.runtime.mcp_direct;
|
|
710
730
|
}
|
|
711
731
|
/**
|
|
712
732
|
* Whether the spawned worker can `git commit`. A sandbox whose root excludes
|
|
713
733
|
* `.git` cannot — the coordinator must integrate the worker's output instead of
|
|
714
|
-
* relying on a self-commit handoff.
|
|
734
|
+
* relying on a self-commit handoff. NOTE (dec#133): commit-from-sandbox was NOT
|
|
735
|
+
* verified to work even on Windows, so this stays conservative (sandbox ⇒ no
|
|
736
|
+
* commit); do not relax it to a platform check without an empirical probe.
|
|
715
737
|
*/
|
|
716
738
|
export function dispatchCanCommit(profile) {
|
|
717
739
|
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';
|
|
@@ -314,6 +314,7 @@ const ANTIGRAVITY_MCP_RELATIVE_PATH = '.gemini/antigravity/mcp_config.json';
|
|
|
314
314
|
const ANTIGRAVITY_HOOKS_RELATIVE_PATH = '.gemini/antigravity/hooks.json';
|
|
315
315
|
const CURSOR_HOOKS_RELATIVE_PATH = '.cursor/hooks.json';
|
|
316
316
|
const COPILOT_HOOKS_RELATIVE_PATH = '.github/copilot/hooks.json';
|
|
317
|
+
const CODEX_HOOKS_RELATIVE_PATH = '.codex/hooks.json';
|
|
317
318
|
const OPENCLAW_MCP_RELATIVE_PATH = '.openclaw/mcp.json';
|
|
318
319
|
const VSCODE_EXTENSIONS_RELATIVE_PATH = '.vscode/extensions.json';
|
|
319
320
|
const UNIVERSAL_SKILL_RELATIVE_PATH = '.agents/skills/brainclaw/SKILL.md';
|
|
@@ -1686,7 +1687,10 @@ export function ensureCodexMcpConfig(homeDir, env = process.env) {
|
|
|
1686
1687
|
'\n[mcp_servers.brainclaw]',
|
|
1687
1688
|
`command = "${normalizedCommand}"`,
|
|
1688
1689
|
`args = [${normalizedArgs.map(a => `"${a}"`).join(', ')}]`,
|
|
1689
|
-
|
|
1690
|
+
// Codex renamed this field to `_sec` (developers.openai.com/codex/extend/mcp;
|
|
1691
|
+
// the docs note "uses _sec, not _ms"). The old `startup_timeout_ms` is an
|
|
1692
|
+
// unrecognized key → Codex silently falls back to its default startup timeout.
|
|
1693
|
+
'startup_timeout_sec = 20',
|
|
1690
1694
|
'',
|
|
1691
1695
|
'[mcp_servers.brainclaw.env]',
|
|
1692
1696
|
'BRAINCLAW_AGENT = "codex"',
|
|
@@ -2115,6 +2119,53 @@ export function ensureAntigravityHooks(homeDir) {
|
|
|
2115
2119
|
relativePath: ANTIGRAVITY_HOOKS_RELATIVE_PATH,
|
|
2116
2120
|
};
|
|
2117
2121
|
}
|
|
2122
|
+
/**
|
|
2123
|
+
* Writes `.codex/hooks.json` — Codex CLI's native lifecycle hooks config
|
|
2124
|
+
* (project scope). Codex gained a full hook surface (developers.openai.com/codex/hooks,
|
|
2125
|
+
* verified 2026-07 — trp_fe75dafc); this wires brainclaw's session lifecycle to it,
|
|
2126
|
+
* mirroring the Claude Code / Antigravity hook writers.
|
|
2127
|
+
*
|
|
2128
|
+
* Events (PascalCase, per the Codex schema): `SessionStart` loads shared context,
|
|
2129
|
+
* `UserPromptSubmit` surfaces the context diff, `Stop` runs session-end cleanup.
|
|
2130
|
+
* File shape: `{ "hooks": { "<Event>": [ { "matcher": "", "hooks": [ { "type": "command", "command": "…" } ] } ] } }`
|
|
2131
|
+
* — the top-level `hooks` wrapper + per-entry `hooks` array (`matcher: ""` = match all).
|
|
2132
|
+
*
|
|
2133
|
+
* brainclaw OWNS these three event arrays (overwrite, not merge) — the same
|
|
2134
|
+
* contract as the Cursor / Antigravity `hooks.json` writers. This is
|
|
2135
|
+
* unconditionally idempotent regardless of how the CLI path resolves, with no
|
|
2136
|
+
* cross-upgrade pile-up. It deliberately does NOT use command-recognition to
|
|
2137
|
+
* preserve user entries within these events: recognizing brainclaw's own hook
|
|
2138
|
+
* path-independently requires matching bare CLI subcommands, which over-matches
|
|
2139
|
+
* legitimate user hooks that merely pass `session-start`/etc. as an argument
|
|
2140
|
+
* (Codex review of #94, round 2). Other events the user defines are untouched
|
|
2141
|
+
* (only these three keys are set).
|
|
2142
|
+
*/
|
|
2143
|
+
export function ensureCodexHooks(cwd) {
|
|
2144
|
+
const filePath = path.join(cwd, CODEX_HOOKS_RELATIVE_PATH);
|
|
2145
|
+
const existing = readJsonObject(filePath);
|
|
2146
|
+
if (existing === undefined) {
|
|
2147
|
+
return skippedAutoConfigResult('rule', 'Codex session hooks', filePath, CODEX_HOOKS_RELATIVE_PATH);
|
|
2148
|
+
}
|
|
2149
|
+
const hooks = isJsonObject(existing.hooks) ? { ...existing.hooks } : {};
|
|
2150
|
+
const sessionStartCmd = buildHookCommand(['session-start', '--include-context']);
|
|
2151
|
+
const contextDiffCmd = buildHookCommand(['context-diff']);
|
|
2152
|
+
const sessionEndCmd = buildHookCommand(['session-end', '--auto-release', '--reflect', '--reflect-handoff', '--dispatch-review']);
|
|
2153
|
+
hooks.SessionStart = [buildCommandHookEntry(sessionStartCmd)];
|
|
2154
|
+
hooks.UserPromptSubmit = [buildCommandHookEntry(contextDiffCmd)];
|
|
2155
|
+
hooks.Stop = [buildCommandHookEntry(sessionEndCmd)];
|
|
2156
|
+
const { created, updated } = writeJsonFileIfChanged(filePath, {
|
|
2157
|
+
...existing,
|
|
2158
|
+
hooks,
|
|
2159
|
+
});
|
|
2160
|
+
return {
|
|
2161
|
+
kind: 'rule',
|
|
2162
|
+
label: 'Codex session hooks',
|
|
2163
|
+
created,
|
|
2164
|
+
updated,
|
|
2165
|
+
filePath,
|
|
2166
|
+
relativePath: CODEX_HOOKS_RELATIVE_PATH,
|
|
2167
|
+
};
|
|
2168
|
+
}
|
|
2118
2169
|
/**
|
|
2119
2170
|
* Writes `.github/copilot/hooks.json` — GitHub Copilot's native hooks config.
|
|
2120
2171
|
* Events: sessionStart, userPromptSubmitted, sessionEnd (camelCase).
|
|
@@ -2284,7 +2335,7 @@ export const AGENT_WIRING_REGISTRY = {
|
|
|
2284
2335
|
writeProtocolSkills,
|
|
2285
2336
|
],
|
|
2286
2337
|
userWriters: [(ctx) => ensureCodexMcpConfig(ctx.homeDir, ctx.env)],
|
|
2287
|
-
hookWriters: [],
|
|
2338
|
+
hookWriters: [(ctx) => ensureCodexHooks(ctx.cwd)],
|
|
2288
2339
|
},
|
|
2289
2340
|
continue: {
|
|
2290
2341
|
workspaceWriters: [(ctx) => ensureContinueMcpConfig(ctx.cwd)],
|
|
@@ -61,6 +61,7 @@ const DEFAULT_SURFACES = {
|
|
|
61
61
|
'codex': [
|
|
62
62
|
{ kind: 'instructions', location: 'workspace', path: 'AGENTS.md' },
|
|
63
63
|
{ kind: 'mcp', location: 'machine', path: '.codex/config.toml' },
|
|
64
|
+
{ kind: 'hook', location: 'workspace', path: '.codex/hooks.json' },
|
|
64
65
|
{ kind: 'skill', location: 'workspace', path: '.agents/skills/brainclaw/SKILL.md' },
|
|
65
66
|
],
|
|
66
67
|
'opencode': [
|
|
@@ -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
|
@@ -279,13 +279,39 @@ export function buildProtocolSection(options) {
|
|
|
279
279
|
}
|
|
280
280
|
if (options?.worktreePath) {
|
|
281
281
|
parts.push(`Worktree: ${options.worktreePath}`);
|
|
282
|
-
// pln#523: tell the worker how dependencies are provisioned so
|
|
283
|
-
// stall trying to install them.
|
|
284
|
-
//
|
|
285
|
-
//
|
|
286
|
-
//
|
|
287
|
-
//
|
|
288
|
-
|
|
282
|
+
// pln#523 / trp_37b05a15: tell the worker how dependencies are provisioned so
|
|
283
|
+
// it does not stall trying to (re)install them. The authoritative record is
|
|
284
|
+
// the worktree's `.brainclaw-worktree.json` → `deps_mode` (absent ⇒ `link`).
|
|
285
|
+
// - link (default): node_modules (incl. monorepo per-package) is
|
|
286
|
+
// junction-linked from the main repo — build/typecheck directly; do NOT
|
|
287
|
+
// `npm install`. An out-of-root symlink, so `next dev`/Turbopack rejects
|
|
288
|
+
// it (build/tsc/vitest are fine).
|
|
289
|
+
// - install/copy: node_modules is a REAL in-root directory — everything,
|
|
290
|
+
// including a dev server, works directly; no reinstall needed.
|
|
291
|
+
// - none: no deps provisioned — run the project's install first.
|
|
292
|
+
let depsMode = 'link';
|
|
293
|
+
let depsProvisioned;
|
|
294
|
+
try {
|
|
295
|
+
const sidecar = JSON.parse(fs.readFileSync(path.join(options.worktreePath, '.brainclaw-worktree.json'), 'utf-8'));
|
|
296
|
+
if (sidecar.deps_mode)
|
|
297
|
+
depsMode = sidecar.deps_mode;
|
|
298
|
+
depsProvisioned = sidecar.deps_provisioned;
|
|
299
|
+
}
|
|
300
|
+
catch { /* sidecar absent/unreadable — assume the default `link` */ }
|
|
301
|
+
if ((depsMode === 'install' || depsMode === 'copy') && depsProvisioned === false) {
|
|
302
|
+
// Codex review P1: provisioning was ATTEMPTED but FAILED (best-effort, non-fatal).
|
|
303
|
+
// Do not claim node_modules is usable — tell the worker to install it.
|
|
304
|
+
parts.push(`Dependencies: in-root provisioning was attempted (deps_mode=${depsMode}) but FAILED — node_modules may be missing or incomplete. Run the project's install (npm/pnpm/yarn/bun) in the worktree before building; see .brainclaw-worktree.json symlink_warnings for the failure.`);
|
|
305
|
+
}
|
|
306
|
+
else if (depsMode === 'install' || depsMode === 'copy') {
|
|
307
|
+
parts.push(`Dependencies: node_modules is a real in-root directory (deps_mode=${depsMode}) — build, typecheck, and dev server all work directly; do NOT reinstall. If anything is missing, see .brainclaw-worktree.json symlink_warnings.`);
|
|
308
|
+
}
|
|
309
|
+
else if (depsMode === 'none') {
|
|
310
|
+
parts.push('Dependencies: none were provisioned (deps_mode=none) — run the project\'s install (npm/pnpm/yarn/bun) in the worktree before building.');
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
parts.push('Dependencies: node_modules is linked from the main repo (incl. monorepo per-package). Build/typecheck directly; if deps are missing, do NOT npm install here — see .brainclaw-worktree.json symlink_warnings and validate centrally. (Out-of-root symlink: next dev/Turbopack needs deps_mode=install.)');
|
|
314
|
+
}
|
|
289
315
|
}
|
|
290
316
|
parts.push('');
|
|
291
317
|
// Assignment lifecycle protocol (Agent SDK)
|
|
@@ -307,9 +333,11 @@ export function buildProtocolSection(options) {
|
|
|
307
333
|
if (options.worktreePath) {
|
|
308
334
|
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
335
|
}
|
|
310
|
-
// pln#526: standard fallback channel — works even
|
|
311
|
-
//
|
|
312
|
-
|
|
336
|
+
// pln#526: standard fallback channel — works even if bclaw_assignment_update
|
|
337
|
+
// fails in your environment. pln#628 Focus 4A: sandbox is NO LONGER a reason
|
|
338
|
+
// MCP is unavailable (dec#133), so this is framed as a generic fallback, not a
|
|
339
|
+
// sandbox instruction. The coordinator ingests it with `brainclaw harvest`.
|
|
340
|
+
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
341
|
}
|
|
314
342
|
else if (options?.claimId) {
|
|
315
343
|
parts.push('1. Call bclaw_session_start to register your session');
|
|
@@ -468,22 +496,25 @@ export function generateBrief(plan, item, cwd, briefMode, options) {
|
|
|
468
496
|
if (mode === 'full') {
|
|
469
497
|
parts.push(buildProtocolSection(options));
|
|
470
498
|
}
|
|
471
|
-
// pln#
|
|
472
|
-
//
|
|
473
|
-
//
|
|
474
|
-
//
|
|
475
|
-
//
|
|
476
|
-
//
|
|
477
|
-
//
|
|
478
|
-
// the
|
|
479
|
-
//
|
|
499
|
+
// pln#628 Focus 4A — transport addendum, now keyed to the ACTUAL missing
|
|
500
|
+
// capability. Originally (pln#528) this fired for any sandboxed spawn and
|
|
501
|
+
// claimed "no MCP + no commit". dec#133 proved the "no MCP" half FALSE: a
|
|
502
|
+
// sandboxed codex reaches MCP (separate out-of-sandbox process +
|
|
503
|
+
// approval_policy=never). dispatchHasMcp now tracks runtime.mcp_direct alone,
|
|
504
|
+
// so this block only fires for genuinely MCP-less agents (nanoclaw/nemoclaw/
|
|
505
|
+
// zeroclaw). For them the Protocol section's MCP lifecycle does not apply and
|
|
506
|
+
// the file protocol is the sole channel. Sandboxed-but-MCP-capable agents
|
|
507
|
+
// (codex) no longer receive a self-contradictory "MCP NOT reachable / Do NOT
|
|
508
|
+
// call bclaw_*" note: their coherent message is carried by the Protocol section
|
|
509
|
+
// (MCP primary + LANE-RESULT.json fallback) and working-defaults (canCommit=
|
|
510
|
+
// false → the coordinator commits their worktree at harvest).
|
|
480
511
|
if (briefProfile && !dispatchHasMcp(briefProfile)) {
|
|
481
|
-
parts.push('## ⚠ Transport:
|
|
482
|
-
parts.push('Your runtime
|
|
512
|
+
parts.push('## ⚠ Transport: no MCP (file protocol only)');
|
|
513
|
+
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
514
|
const asgn = options?.assignmentId ?? '<assignment_id>';
|
|
484
515
|
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
516
|
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
|
|
517
|
+
parts.push('- Do NOT call bclaw_* tools — they are unavailable here. The coordinator harvests your result and integrates it.');
|
|
487
518
|
parts.push('');
|
|
488
519
|
}
|
|
489
520
|
// Codex-specific constraints: focus and speed guidance for sandboxed runs.
|
|
@@ -525,14 +556,17 @@ export function generateDispatchBrief(options) {
|
|
|
525
556
|
assignmentId: options.assignmentId,
|
|
526
557
|
}));
|
|
527
558
|
}
|
|
528
|
-
// pln#
|
|
559
|
+
// pln#628 Focus 4A — transport addendum keyed to the ACTUAL missing capability
|
|
560
|
+
// (see generateBrief for the full rationale + dec#133). Fires only for
|
|
561
|
+
// genuinely MCP-less agents; sandboxed-but-MCP-capable codex no longer gets a
|
|
562
|
+
// self-contradictory "no MCP / Do NOT call bclaw_*" note.
|
|
529
563
|
if (taskBriefProfile && !dispatchHasMcp(taskBriefProfile)) {
|
|
530
|
-
parts.push('## ⚠ Transport:
|
|
531
|
-
parts.push('Your runtime
|
|
564
|
+
parts.push('## ⚠ Transport: no MCP (file protocol only)');
|
|
565
|
+
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
566
|
const asgn = options.assignmentId ?? '<assignment_id>';
|
|
533
567
|
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
568
|
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
|
|
569
|
+
parts.push('- Do NOT call bclaw_* tools — they are unavailable here. The coordinator harvests your result and integrates it.');
|
|
536
570
|
parts.push('');
|
|
537
571
|
}
|
|
538
572
|
// Codex-specific constraints: focus and speed guidance for sandboxed runs
|
|
@@ -902,13 +936,19 @@ export async function dispatch(options, cwd) {
|
|
|
902
936
|
requireWorktree: true, // pln#531: never spawn a worker in the integration repo
|
|
903
937
|
});
|
|
904
938
|
entry.execution_status = execResult.execution_status;
|
|
939
|
+
// pln#626 Phase 1 — mirror the coordinate path: carry the reason so a
|
|
940
|
+
// command_ready_manual sequence item says WHY it didn't spawn.
|
|
941
|
+
if (execResult.execution_reason)
|
|
942
|
+
entry.execution_reason = execResult.execution_reason;
|
|
943
|
+
if (execResult.failure_kind)
|
|
944
|
+
entry.failure_kind = execResult.failure_kind;
|
|
905
945
|
if (execResult.pid)
|
|
906
946
|
entry.pid = execResult.pid;
|
|
907
947
|
if (execResult.execution_status === 'delivered_and_started') {
|
|
908
948
|
entry.channel = 'spawned_cli';
|
|
909
949
|
}
|
|
910
950
|
if (execResult.error)
|
|
911
|
-
result.warnings.push(execResult.error);
|
|
951
|
+
result.warnings.push(`${entry.agent}: ${execResult.error}`);
|
|
912
952
|
if (entry.assignment_id && entry.claim_id) {
|
|
913
953
|
if (execResult.failure_kind === 'spawn_no_handshake') {
|
|
914
954
|
try {
|