brainclaw 1.16.0 → 1.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -8
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/cli/register-review.js +2 -2
- package/dist/commands/code-map.js +4 -1
- package/dist/commands/codev.js +61 -30
- package/dist/commands/doctor.js +14 -1
- package/dist/commands/harvest.js +241 -25
- package/dist/commands/inbox.js +10 -4
- package/dist/commands/loop.js +2 -2
- package/dist/commands/loops-handlers.js +82 -1
- package/dist/commands/mcp-catalog.js +12 -4
- package/dist/commands/mcp-read-handlers.js +90 -7
- package/dist/commands/mcp-schemas.generated.js +3 -0
- package/dist/commands/mcp-write-coordination.js +159 -40
- package/dist/commands/mcp.js +11 -2
- package/dist/core/agent-capability.js +7 -2
- package/dist/core/agent-files.js +53 -2
- package/dist/core/agent-integrations.js +1 -0
- package/dist/core/agentrun-reconciler.js +171 -7
- package/dist/core/agentruns.js +6 -1
- package/dist/core/code-map/aggregate.js +473 -0
- package/dist/core/code-map/backend.js +36 -10
- package/dist/core/code-map/freshness.js +36 -1
- package/dist/core/code-map/lang/c/imports.scm +12 -0
- package/dist/core/code-map/lang/c/index.js +150 -0
- package/dist/core/code-map/lang/c/tags.scm +68 -0
- package/dist/core/code-map/lang/cpp/imports.scm +14 -0
- package/dist/core/code-map/lang/cpp/index.js +149 -0
- package/dist/core/code-map/lang/cpp/tags.scm +87 -0
- package/dist/core/code-map/lang/csharp/imports.scm +20 -0
- package/dist/core/code-map/lang/csharp/index.js +224 -0
- package/dist/core/code-map/lang/csharp/tags.scm +63 -0
- package/dist/core/code-map/lang/go/imports.scm +13 -0
- package/dist/core/code-map/lang/go/index.js +139 -0
- package/dist/core/code-map/lang/go/tags.scm +36 -0
- package/dist/core/code-map/lang/providers.js +12 -1
- package/dist/core/code-map/lang/ruby/imports.scm +24 -0
- package/dist/core/code-map/lang/ruby/index.js +198 -0
- package/dist/core/code-map/lang/ruby/tags.scm +49 -0
- package/dist/core/code-map/lang/rust/imports.scm +44 -0
- package/dist/core/code-map/lang/rust/index.js +136 -0
- package/dist/core/code-map/lang/rust/tags.scm +47 -0
- package/dist/core/code-map/query.js +229 -80
- package/dist/core/code-map/types.js +18 -0
- package/dist/core/code-map/work-section.js +8 -7
- package/dist/core/codev-responses.js +16 -0
- package/dist/core/dispatcher.js +209 -29
- package/dist/core/execution-adapters.js +29 -3
- package/dist/core/ideation-loop-close.js +124 -0
- package/dist/core/loops/artifact-resolver.js +197 -0
- package/dist/core/loops/attempt-reservation.js +576 -0
- package/dist/core/loops/commit-intent.js +494 -0
- package/dist/core/loops/facade-schema.js +48 -0
- package/dist/core/loops/impl-bind.js +144 -0
- package/dist/core/loops/index.js +1 -1
- package/dist/core/loops/iteration-engine.js +29 -0
- package/dist/core/loops/lock.js +14 -0
- package/dist/core/loops/project-resolution.js +157 -0
- package/dist/core/loops/reconcile-turn.js +369 -0
- package/dist/core/loops/result-reducers.js +88 -0
- package/dist/core/loops/store.js +46 -7
- package/dist/core/loops/types.js +139 -11
- package/dist/core/loops/verbs.js +9 -3
- package/dist/core/loops/verify-command.js +209 -0
- package/dist/core/messaging.js +58 -5
- package/dist/core/review-loop-close.js +106 -34
- package/dist/core/review-loop-turn-dispatch.js +445 -0
- package/dist/core/runtime-signals.js +68 -0
- package/dist/core/schema.js +34 -0
- package/dist/core/worktree.js +240 -22
- package/dist/facts.js +10 -10
- package/dist/facts.json +9 -9
- package/dist/wasm/tree-sitter-c.wasm +0 -0
- package/dist/wasm/tree-sitter-c_sharp.wasm +0 -0
- package/dist/wasm/tree-sitter-cpp.wasm +0 -0
- package/dist/wasm/tree-sitter-go.wasm +0 -0
- package/dist/wasm/tree-sitter-ruby.wasm +0 -0
- package/dist/wasm/tree-sitter-rust.wasm +0 -0
- package/docs/cli.md +1 -1
- package/docs/code-map.md +22 -6
- package/docs/concepts/loop-engine.md +28 -2
- package/docs/concepts/observer-protocol.md +22 -0
- package/docs/integrations/codex.md +19 -3
- package/docs/mcp-schema-changelog.md +43 -1
- package/package.json +1 -1
|
@@ -20,12 +20,14 @@ import { appendAuditEntry } from '../core/audit.js';
|
|
|
20
20
|
import { nowISO } from '../core/ids.js';
|
|
21
21
|
import { validateMcpField } from '../core/input-validation.js';
|
|
22
22
|
import { generateCandidateIdWithLabel, saveCandidate } from '../core/candidates.js';
|
|
23
|
+
import { validateLoopProjectResolution } from '../core/loops/project-resolution.js';
|
|
23
24
|
import { ackMessage, getThread, hasActiveAssignment, sendMessage } from '../core/messaging.js';
|
|
24
25
|
import { dispatch, dispatchReview, generateDispatchBrief } from '../core/dispatcher.js';
|
|
25
26
|
import { CoordinateRequestSchema } from '../core/facade-schema.js';
|
|
26
27
|
import { buildInvokeCommand, getCapabilityProfile, getSpawnableAgents, resolveModel, validateAgentForDispatch, } from '../core/agent-capability.js';
|
|
27
28
|
import { attemptExecution } from '../core/execution.js';
|
|
28
29
|
import { createAgentRun, transitionAgentRun } from '../core/agentruns.js';
|
|
30
|
+
import { prepareTurnOwnedReviewDispatch, turnOwnedReviewEnabled } from '../core/review-loop-turn-dispatch.js';
|
|
29
31
|
import { createAssignment, generateAssignmentId, patchAssignmentMessageId, transitionAssignment, } from '../core/assignments.js';
|
|
30
32
|
import { createToolErrorResponse, toolResponse, } from './mcp-contract.js';
|
|
31
33
|
import { handleMcpReadToolCall } from './mcp-read-handlers.js';
|
|
@@ -228,11 +230,13 @@ export function handleBclawSendMessage(args, ctx) {
|
|
|
228
230
|
}, cwd);
|
|
229
231
|
appendAuditEntry({ actor: resolved.identity.agent_name, actor_id: resolved.identity.agent_id, action: 'create', item_id: result.id, item_type: 'message', scope: to }, cwd);
|
|
230
232
|
const threadInfo = threadId ? ` thread:${threadId}` : '';
|
|
233
|
+
const warningLine = result.warning ? `\n⚠ ${result.warning}` : '';
|
|
231
234
|
return {
|
|
232
235
|
response: toolResponse({
|
|
233
|
-
content: [{ type: 'text', text: `✔ Message sent: [${result.shortLabel}] ${msgType} → ${to}${threadInfo}` }],
|
|
236
|
+
content: [{ type: 'text', text: `✔ Message sent: [${result.shortLabel}] ${msgType} → ${to}${threadInfo}${warningLine}` }],
|
|
234
237
|
message_id: result.id,
|
|
235
238
|
thread_id: threadId,
|
|
239
|
+
...(result.warning ? { warning: result.warning } : {}),
|
|
236
240
|
}),
|
|
237
241
|
};
|
|
238
242
|
}
|
|
@@ -270,7 +274,7 @@ export function handleBclawAckMessage(args, ctx) {
|
|
|
270
274
|
}
|
|
271
275
|
}
|
|
272
276
|
export async function handleBclawCoordinate(args, ctx) {
|
|
273
|
-
const { cwd, connectionSessionId, currentModel } = ctx;
|
|
277
|
+
const { cwd, connectionSessionId, currentModel, effectiveScope } = ctx;
|
|
274
278
|
const startMs = Date.now();
|
|
275
279
|
const parseResult = CoordinateRequestSchema.safeParse(args);
|
|
276
280
|
if (!parseResult.success) {
|
|
@@ -356,6 +360,35 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
356
360
|
warnings.push(`cross-project dispatch (project='${req.project}') — auto-spawn disabled; the target agent picks up the brief async via its own bclaw_work.`);
|
|
357
361
|
}
|
|
358
362
|
const effectiveAutoExecute = isCrossProject ? false : req.autoExecute;
|
|
363
|
+
// pln#521 P1 — project resolution gate. A review loop written into the wrong
|
|
364
|
+
// store is worse than one that never opened: candidate, claim, assignment and
|
|
365
|
+
// loop all persist where nobody is watching, and the reviewer spawns against
|
|
366
|
+
// the wrong repo (DGX misroute). So when this store can host several projects
|
|
367
|
+
// and NONE was selected, refuse here — before the pre-flight spawn and before
|
|
368
|
+
// the first write — instead of silently defaulting to cwd. Explicit `project`,
|
|
369
|
+
// a session switch, or an active-project pointer all count as a choice; ref /
|
|
370
|
+
// scope / path never do (B3 rejected, art_e29e88878209). Scoped to the failing
|
|
371
|
+
// path (review + open_loop): every other intent keeps its routing untouched.
|
|
372
|
+
let projectResolution;
|
|
373
|
+
if (req.intent === 'review' && req.open_loop === true) {
|
|
374
|
+
// Resolve from `cwd`, not `dispatchCwd`: an explicit project name is
|
|
375
|
+
// resolvable from the SOURCE store (its links / store chain), which is
|
|
376
|
+
// exactly what produced dispatchCwd above. Re-resolving from the target
|
|
377
|
+
// could fail for a link whose name differs from the target's project_name.
|
|
378
|
+
const resolution = validateLoopProjectResolution({
|
|
379
|
+
cwd,
|
|
380
|
+
projectArg: req.project,
|
|
381
|
+
activeSource: effectiveScope?.active_source,
|
|
382
|
+
});
|
|
383
|
+
if (!resolution.ok) {
|
|
384
|
+
return {
|
|
385
|
+
response: createToolErrorResponse(resolution.code, resolution.message, {
|
|
386
|
+
candidates: resolution.candidates,
|
|
387
|
+
}),
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
projectResolution = resolution;
|
|
391
|
+
}
|
|
359
392
|
// can_30c295b4 / trp#371 Tier 2 — scope-aware dirty-working-tree guard.
|
|
360
393
|
// Intents that spawn a worktree worker from HEAD can review/edit stale code,
|
|
361
394
|
// so they are guarded; consult/summarize (no worktree) are not. pln#626
|
|
@@ -403,7 +436,7 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
403
436
|
/** Run E2E execution phase on prepared delivery entries. Returns overall execution status. */
|
|
404
437
|
const runCoordinateExecution = async (prepared, opts) => {
|
|
405
438
|
let overall = 'inbox_only';
|
|
406
|
-
for (const { entry, invoke, worktreePath } of prepared) {
|
|
439
|
+
for (const { entry, invoke, worktreePath, turnEcho } of prepared) {
|
|
407
440
|
const execResult = await attemptExecution(invoke, {
|
|
408
441
|
agent: entry.agent,
|
|
409
442
|
autoExecute: opts.autoExecute,
|
|
@@ -414,6 +447,8 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
414
447
|
dispatcherAgentId: opts.senderAgentId,
|
|
415
448
|
cwd: opts.cwd,
|
|
416
449
|
requireWorktree: true, // pln#531: never spawn a worker in the integration repo
|
|
450
|
+
turnEcho, // pln#630 — turn-owned reviewer (initial dispatch): the ack-wrapper writes the
|
|
451
|
+
// turn-keyed completion sentinel. undefined for every non-turn-owned entry.
|
|
417
452
|
});
|
|
418
453
|
entry.execution_status = execResult.execution_status;
|
|
419
454
|
// pln#626 Phase 1 — carry the machine-readable reason (+ failure_kind) to
|
|
@@ -436,7 +471,23 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
436
471
|
// emit three identical context-free warnings (pln#626 Phase 1 R5).
|
|
437
472
|
if (execResult.error)
|
|
438
473
|
opts.warnings.push(`${entry.agent}: ${execResult.error}`);
|
|
439
|
-
if (
|
|
474
|
+
if (turnEcho) {
|
|
475
|
+
// pln#630 risk #1 — a turn-owned reviewer's run was ALREADY created (`created`) by
|
|
476
|
+
// prepareTurnOwnedReviewDispatch. Do NOT mint a second run here (double-mint). Transition
|
|
477
|
+
// the deterministic run → running on a real spawn (mirrors dispatchReviewLoopTurn); leave
|
|
478
|
+
// it `created` otherwise so the no-sentinel legacy fallback (turnOwnedLaneEvidence) + the
|
|
479
|
+
// pre-run lease reconciler govern it. Non-turn-owned entries take the unchanged else-branch.
|
|
480
|
+
if (execResult.execution_status === 'delivered_and_started') {
|
|
481
|
+
try {
|
|
482
|
+
transitionAgentRun(turnEcho.run_id, 'running', {
|
|
483
|
+
actor: opts.senderAgent, actor_id: opts.senderAgentId, pid: execResult.pid,
|
|
484
|
+
status_reason: 'turn-owned reviewer spawned by coordinator',
|
|
485
|
+
}, opts.cwd);
|
|
486
|
+
}
|
|
487
|
+
catch { /* best-effort — the reconciler converges if this races */ }
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
else if (entry.assignment_id && entry.claim_id) {
|
|
440
491
|
if (execResult.failure_kind === 'spawn_no_handshake') {
|
|
441
492
|
try {
|
|
442
493
|
const run = createAgentRun({
|
|
@@ -969,38 +1020,82 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
969
1020
|
id: claimResult.claimId,
|
|
970
1021
|
});
|
|
971
1022
|
let reviewAssignmentId;
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1023
|
+
let reviewTurnEcho;
|
|
1024
|
+
// pln#630 — turn-own the INITIAL reviewer dispatch (same default + kill-switch as the
|
|
1025
|
+
// fix cycle). Skipped for cross-project reviews (no local worktree/sentinel → they never
|
|
1026
|
+
// spawn here). WON: prepare minted the DETERMINISTIC assignment + run + turn()-bound the
|
|
1027
|
+
// slot, so we reuse prep.assignmentId for the brief/message/linkage below and carry the
|
|
1028
|
+
// turnEcho so the ack-wrapper writes the turn-keyed sentinel. DENIED: the exactly-once
|
|
1029
|
+
// fence says this dispatch is NOT the spawner — do NOT spawn, do NOT fall back to legacy
|
|
1030
|
+
// (that is the double-spawn hole), and do NOT release the (possibly shared) claim; leave
|
|
1031
|
+
// the slot for reconcile/self-heal. LEGACY: the unchanged inline mint runs.
|
|
1032
|
+
let usedTurnOwned = false;
|
|
1033
|
+
if (turnOwnedReviewEnabled() && !req.project) {
|
|
1034
|
+
const prep = prepareTurnOwnedReviewDispatch({
|
|
1035
|
+
loopId: loop.id,
|
|
1036
|
+
slotId: slot.slot_id,
|
|
978
1037
|
agent: slot.agent ?? '',
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
1038
|
+
agentId: slot.agent_id,
|
|
1039
|
+
phase: 'findings',
|
|
1040
|
+
task: req.task,
|
|
982
1041
|
description: reviewDescription,
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1042
|
+
scope: reviewScope,
|
|
1043
|
+
claimId: claimResult.claimId,
|
|
1044
|
+
worktreePath: claimResult.worktreePath,
|
|
1045
|
+
dispatcherAgent: senderAgent,
|
|
1046
|
+
dispatcherAgentId: senderAgentId,
|
|
1047
|
+
sessionId: connectionSessionId,
|
|
1048
|
+
isReviewer: true,
|
|
1049
|
+
cwd: dispatchCwd,
|
|
1050
|
+
});
|
|
1051
|
+
if (prep.kind === 'won') {
|
|
1052
|
+
reviewAssignmentId = prep.assignmentId; // deterministic — harvest correlates on it
|
|
1053
|
+
reviewTurnEcho = { turn_id: prep.turnId, run_id: prep.runId, nonce: prep.nonce };
|
|
1054
|
+
out.artifacts.push({ type: 'assignment', id: prep.assignmentId });
|
|
1055
|
+
usedTurnOwned = true; // prepare already created the assignment + run + bound the slot
|
|
1056
|
+
}
|
|
1057
|
+
else if (prep.kind === 'denied') {
|
|
1058
|
+
out.partial = true;
|
|
1059
|
+
out.warnings.push(`open_loop: turn-owned reviewer dispatch denied for slot ${slot.slot_id} (${prep.reason}); not spawning — slot left for reconcile/self-heal`);
|
|
1060
|
+
continue; // MUST NOT spawn AND MUST NOT legacy-fallback beside a live reservation
|
|
1061
|
+
}
|
|
1062
|
+
// prep.kind === 'legacy' (pre-identity failure) → fall through to the inline mint.
|
|
987
1063
|
}
|
|
988
|
-
|
|
989
|
-
|
|
1064
|
+
if (!usedTurnOwned) {
|
|
1065
|
+
try {
|
|
1066
|
+
const preId = generateAssignmentId(dispatchCwd);
|
|
1067
|
+
const assignment = createAssignment({
|
|
1068
|
+
id: preId.id,
|
|
1069
|
+
short_label: preId.short_label,
|
|
1070
|
+
claim_id: claimResult.claimId,
|
|
1071
|
+
agent: slot.agent ?? '',
|
|
1072
|
+
dispatcher_agent: senderAgent,
|
|
1073
|
+
dispatcher_session_id: connectionSessionId,
|
|
1074
|
+
scope: reviewScope,
|
|
1075
|
+
description: reviewDescription,
|
|
1076
|
+
tags: ['coordinate', 'review', 'loop'],
|
|
1077
|
+
}, dispatchCwd);
|
|
1078
|
+
reviewAssignmentId = assignment.id;
|
|
1079
|
+
out.artifacts.push({ type: 'assignment', id: assignment.id });
|
|
1080
|
+
}
|
|
1081
|
+
catch (asgErr) {
|
|
1082
|
+
out.warnings.push(`Review assignment creation failed for slot ${slot.slot_id}: ${asgErr instanceof Error ? asgErr.message : String(asgErr)}`);
|
|
1083
|
+
}
|
|
1084
|
+
// pln#628 Focus 4B (BLOCKING 2) — assign the slot NOW that the
|
|
1085
|
+
// claim/assignment exist, binding their ids onto the slot so the
|
|
1086
|
+
// harvest close resolves this exact reviewer by assignment_id. Runs
|
|
1087
|
+
// even if assignment creation failed (undefined id → the harvest
|
|
1088
|
+
// falls back to the legacy agent match for this one slot). (For the
|
|
1089
|
+
// turn-owned WON path, prepare already turn()-bound the slot.)
|
|
1090
|
+
turn({
|
|
1091
|
+
id: loop.id,
|
|
1092
|
+
slot_id: slot.slot_id,
|
|
1093
|
+
actor: creatorActor,
|
|
1094
|
+
input: req.task,
|
|
1095
|
+
assignment_id: reviewAssignmentId,
|
|
1096
|
+
claim_id: claimResult.claimId,
|
|
1097
|
+
}, dispatchCwd);
|
|
990
1098
|
}
|
|
991
|
-
// pln#628 Focus 4B (BLOCKING 2) — assign the slot NOW that the
|
|
992
|
-
// claim/assignment exist, binding their ids onto the slot so the
|
|
993
|
-
// harvest close resolves this exact reviewer by assignment_id. Runs
|
|
994
|
-
// even if assignment creation failed (undefined id → the harvest
|
|
995
|
-
// falls back to the legacy agent match for this one slot).
|
|
996
|
-
turn({
|
|
997
|
-
id: loop.id,
|
|
998
|
-
slot_id: slot.slot_id,
|
|
999
|
-
actor: creatorActor,
|
|
1000
|
-
input: req.task,
|
|
1001
|
-
assignment_id: reviewAssignmentId,
|
|
1002
|
-
claim_id: claimResult.claimId,
|
|
1003
|
-
}, dispatchCwd);
|
|
1004
1099
|
const reviewBrief = buildCoordinateBrief(slot.agent ?? '', reviewDescription + reviewVerdictBriefSuffix, {
|
|
1005
1100
|
claimId: claimResult.claimId,
|
|
1006
1101
|
scope: reviewScope,
|
|
@@ -1045,6 +1140,7 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
1045
1140
|
entry: queued.entry,
|
|
1046
1141
|
invoke: queued.invoke,
|
|
1047
1142
|
worktreePath: claimResult.worktreePath,
|
|
1143
|
+
turnEcho: reviewTurnEcho,
|
|
1048
1144
|
});
|
|
1049
1145
|
}
|
|
1050
1146
|
catch (dispatchErr) {
|
|
@@ -1105,6 +1201,16 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
1105
1201
|
result = {
|
|
1106
1202
|
candidate_id: output.candidateId,
|
|
1107
1203
|
selected_targets: resolvedAgents,
|
|
1204
|
+
// pln#521 P1 (B4) — echo the routing decision so an operator can see WHERE
|
|
1205
|
+
// the loop landed without reverse-engineering cwd and store state. Present
|
|
1206
|
+
// for open_loop reviews (the gated path); the reasoning behind the decision
|
|
1207
|
+
// ships as `_resolution_trace` on dispatch_status, not here.
|
|
1208
|
+
...(projectResolution
|
|
1209
|
+
? {
|
|
1210
|
+
project_cwd: projectResolution.project_cwd,
|
|
1211
|
+
...(projectResolution.project_name ? { project_name: projectResolution.project_name } : {}),
|
|
1212
|
+
}
|
|
1213
|
+
: {}),
|
|
1108
1214
|
// pln#626 Phase 1 (review rework) — expose the reviewer delivery entries
|
|
1109
1215
|
// so review is as honest as assign/reroute: each entry's execution_reason
|
|
1110
1216
|
// (set by runCoordinateExecution) feeds the top-level derivation, so a
|
|
@@ -1550,12 +1656,6 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
1550
1656
|
slotRole: slot.role,
|
|
1551
1657
|
memoryProvider: provider,
|
|
1552
1658
|
});
|
|
1553
|
-
turn({
|
|
1554
|
-
id: loopId,
|
|
1555
|
-
slot_id: slot.slot_id,
|
|
1556
|
-
actor: creatorActor,
|
|
1557
|
-
input: briefResult.text,
|
|
1558
|
-
}, dispatchCwd);
|
|
1559
1659
|
// pln#626 Phase 2 (Option B) — spawn the critic as a worktree-isolated
|
|
1560
1660
|
// worker, mirroring the intent=assign / review chain. Each critic gets
|
|
1561
1661
|
// its OWN claim + worktree (scope is unique per slot) so parallel
|
|
@@ -1605,6 +1705,23 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
1605
1705
|
catch (asgErr) {
|
|
1606
1706
|
warnings.push(`ideate assignment creation failed for slot ${slot.slot_id}: ${asgErr instanceof Error ? asgErr.message : String(asgErr)}`);
|
|
1607
1707
|
}
|
|
1708
|
+
// pln#629 — bind the slot to its claim/assignment NOW that both
|
|
1709
|
+
// exist (mirrors the review path, pln#628 BLOCKING 2). The turn()
|
|
1710
|
+
// used to fire BEFORE the assignment was created, leaving
|
|
1711
|
+
// slot.assignment_id undefined: bclaw_loop get's reconcile then
|
|
1712
|
+
// skipped the critic slot (loops-handlers.ts `if (!assignmentId)
|
|
1713
|
+
// continue`) and dispatch_status(lop_) resolved no assignment, so
|
|
1714
|
+
// ideate loops could never be reconciled (trp_dfe0b941 /
|
|
1715
|
+
// trp_2187b340 / trp_1de94516). Runs even if assignment creation
|
|
1716
|
+
// failed (undefined id → legacy agent-match fallback, as review).
|
|
1717
|
+
turn({
|
|
1718
|
+
id: loopId,
|
|
1719
|
+
slot_id: slot.slot_id,
|
|
1720
|
+
actor: creatorActor,
|
|
1721
|
+
input: briefResult.text,
|
|
1722
|
+
assignment_id: criticAssignmentId,
|
|
1723
|
+
claim_id: claimResult.claimId,
|
|
1724
|
+
}, dispatchCwd);
|
|
1608
1725
|
// pln#626 Phase 2 — the critique-only contract must reach the
|
|
1609
1726
|
// DELIVERED brief, not just the claim record: buildCoordinateBrief
|
|
1610
1727
|
// wraps this in a worker envelope, so prepend the constraint + the
|
|
@@ -1806,7 +1923,9 @@ export async function handleBclawLoop(args, ctx) {
|
|
|
1806
1923
|
}
|
|
1807
1924
|
// pln#562 step 4 — dispatching a turn hands work to another agent; gate
|
|
1808
1925
|
// it at the same trust bar as the other dispatch surfaces.
|
|
1809
|
-
|
|
1926
|
+
// pln#632 — `bind` also SPAWNS real workers (it dispatches the loop's linked
|
|
1927
|
+
// sequence), so it is gated at the same 'trusted' bar as turn-dispatch / coordinate.
|
|
1928
|
+
if ((args?.intent === 'turn' && args?.dispatch === true) || args?.intent === 'bind') {
|
|
1810
1929
|
const resolved = ensureTrust(args, { nameField: 'agent', idField: 'agentId' }, 'trusted', cwd, connectionSessionId);
|
|
1811
1930
|
if (resolved.error) {
|
|
1812
1931
|
return { response: createToolErrorResponse(resolved.error.kind, resolved.error.message, resolved.error.details) };
|
|
@@ -1814,7 +1933,7 @@ export async function handleBclawLoop(args, ctx) {
|
|
|
1814
1933
|
}
|
|
1815
1934
|
const { handleBclawLoop: runLoopIntent } = await import('./loops-handlers.js');
|
|
1816
1935
|
const targetCwd = resolveProjectCwd(args?.project, cwd);
|
|
1817
|
-
const result = runLoopIntent({ args: args, cwd: targetCwd });
|
|
1936
|
+
const result = await runLoopIntent({ args: args, cwd: targetCwd, sessionId: connectionSessionId });
|
|
1818
1937
|
return {
|
|
1819
1938
|
response: toolResponse({
|
|
1820
1939
|
content: [{ type: 'text', text: result.summary }],
|
package/dist/commands/mcp.js
CHANGED
|
@@ -27,7 +27,7 @@ import { switchProject } from './switch.js';
|
|
|
27
27
|
import { assessBootstrapNeed, resolveEmptyMemoryRecommendation } from '../core/setup-flow.js';
|
|
28
28
|
import { WorkRequestSchema } from '../core/facade-schema.js';
|
|
29
29
|
import { codeMapWorkSection, codeMapRefreshNextActions } from '../core/code-map/work-section.js';
|
|
30
|
-
import { sweepDeadPidRunningAgentRunsAtRead } from '../core/agentrun-reconciler.js';
|
|
30
|
+
import { sweepDeadPidRunningAgentRunsAtRead, sweepTurnOwnedPreRunLeaseAtRead } from '../core/agentrun-reconciler.js';
|
|
31
31
|
import { bumpActiveAssignmentHeartbeat } from '../core/assignments.js';
|
|
32
32
|
import { handleBclawAckMessage, handleBclawCoordinate, handleBclawDispatch, handleBclawLoop, handleBclawSendMessage, } from './mcp-write-coordination.js';
|
|
33
33
|
import { ensureTrust, resolveMutationIdentity, explicitSessionIdFromEnv, projectInfoForCwd, scopeMetadataForTarget, } from './mcp-write-support.js';
|
|
@@ -1556,7 +1556,10 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
1556
1556
|
};
|
|
1557
1557
|
}
|
|
1558
1558
|
if (name === 'bclaw_coordinate') {
|
|
1559
|
-
|
|
1559
|
+
// pln#521 P1 — scopeInfo carries WHICH selector produced `cwd`, which the
|
|
1560
|
+
// review open_loop project gate needs to distinguish a chosen project
|
|
1561
|
+
// from the bare cwd fallback.
|
|
1562
|
+
return await handleBclawCoordinate(args, { cwd, connectionSessionId, currentModel, effectiveScope: scopeInfo });
|
|
1560
1563
|
}
|
|
1561
1564
|
if (name === 'bclaw_loop') {
|
|
1562
1565
|
return await handleBclawLoop(args, { cwd, connectionSessionId });
|
|
@@ -1871,6 +1874,12 @@ export async function executeMcpToolCall(payload) {
|
|
|
1871
1874
|
sweepDeadPidRunningAgentRunsAtRead(cwd);
|
|
1872
1875
|
}
|
|
1873
1876
|
catch { /* best-effort */ }
|
|
1877
|
+
// pln#630 PR2c-lease: converge turn-owned runs still `created`/`launching`
|
|
1878
|
+
// on their dispatch/launch lease (inert until live turn-owned dispatch).
|
|
1879
|
+
try {
|
|
1880
|
+
sweepTurnOwnedPreRunLeaseAtRead(cwd);
|
|
1881
|
+
}
|
|
1882
|
+
catch { /* best-effort */ }
|
|
1874
1883
|
}
|
|
1875
1884
|
// ── Delegate to inner handler ───────────────────────────────────────────────
|
|
1876
1885
|
const outcome = await _executeMcpToolCallInner({
|
|
@@ -159,10 +159,15 @@ const PROFILES = {
|
|
|
159
159
|
// blocker.
|
|
160
160
|
codex: {
|
|
161
161
|
name: 'codex', category: 'code-agent', workflowModel: 'task-based',
|
|
162
|
-
|
|
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,
|
|
163
168
|
instructionFile: 'AGENTS.md', sharedInstructionFile: true, mcpConfigScope: 'machine', templateTier: 'A',
|
|
164
169
|
role_capabilities: ['execute', 'review'],
|
|
165
|
-
runtime: { mcp_direct: true, hooks:
|
|
170
|
+
runtime: { mcp_direct: true, hooks: true, canBeSpawnedCli: true, canSpawnOtherCli: false, inbox: true },
|
|
166
171
|
max_concurrent_tasks: 5,
|
|
167
172
|
// pln#475: prefer stdin_pipe to avoid Windows cmd.exe arg-parsing breaking
|
|
168
173
|
// long prompts. codex.cmd resolves through cmd shell, where embedded
|
package/dist/core/agent-files.js
CHANGED
|
@@ -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': [
|