brainclaw 1.17.0 → 1.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/commands/code-map.js +4 -1
- package/dist/commands/codev.js +61 -30
- package/dist/commands/doctor.js +14 -1
- package/dist/commands/harvest.js +223 -43
- package/dist/commands/inbox.js +10 -4
- package/dist/commands/install-hooks.js +184 -27
- 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-claims.js +57 -0
- package/dist/commands/mcp-write-coordination.js +216 -57
- package/dist/commands/mcp-write-entities.js +11 -0
- package/dist/commands/mcp.js +29 -2
- package/dist/commands/session-end.js +15 -0
- package/dist/commands/session-start.js +19 -0
- package/dist/core/agentrun-reconciler.js +171 -7
- package/dist/core/agentruns.js +6 -1
- package/dist/core/claim-conformity.js +193 -0
- package/dist/core/claim-scope.js +155 -0
- package/dist/core/claims.js +127 -2
- package/dist/core/code-map/aggregate.js +473 -0
- package/dist/core/code-map/backend.js +36 -10
- package/dist/core/code-map/freshness.js +36 -1
- package/dist/core/code-map/lang/c/imports.scm +12 -0
- package/dist/core/code-map/lang/c/index.js +150 -0
- package/dist/core/code-map/lang/c/tags.scm +68 -0
- package/dist/core/code-map/lang/cpp/imports.scm +14 -0
- package/dist/core/code-map/lang/cpp/index.js +149 -0
- package/dist/core/code-map/lang/cpp/tags.scm +87 -0
- package/dist/core/code-map/lang/csharp/imports.scm +20 -0
- package/dist/core/code-map/lang/csharp/index.js +224 -0
- package/dist/core/code-map/lang/csharp/tags.scm +63 -0
- package/dist/core/code-map/lang/go/imports.scm +13 -0
- package/dist/core/code-map/lang/go/index.js +139 -0
- package/dist/core/code-map/lang/go/tags.scm +36 -0
- package/dist/core/code-map/lang/providers.js +12 -1
- package/dist/core/code-map/lang/ruby/imports.scm +24 -0
- package/dist/core/code-map/lang/ruby/index.js +198 -0
- package/dist/core/code-map/lang/ruby/tags.scm +49 -0
- package/dist/core/code-map/lang/rust/imports.scm +44 -0
- package/dist/core/code-map/lang/rust/index.js +136 -0
- package/dist/core/code-map/lang/rust/tags.scm +47 -0
- package/dist/core/code-map/query.js +229 -80
- package/dist/core/code-map/types.js +18 -0
- package/dist/core/code-map/work-section.js +8 -7
- package/dist/core/codev-responses.js +16 -0
- package/dist/core/dispatcher.js +176 -22
- package/dist/core/execution-adapters.js +29 -3
- package/dist/core/facade-schema.js +32 -0
- package/dist/core/guidance-telemetry.js +197 -0
- package/dist/core/ideation-loop-close.js +152 -0
- package/dist/core/instruction-templates.js +11 -3
- 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 +49 -4
- package/dist/core/loops/verify-command.js +209 -0
- package/dist/core/messaging.js +58 -5
- package/dist/core/next-actions.js +157 -0
- package/dist/core/review-loop-close.js +27 -6
- package/dist/core/review-loop-turn-dispatch.js +290 -28
- package/dist/core/runtime-signals.js +68 -0
- package/dist/core/schema.js +64 -0
- package/dist/core/surface-freshness.js +150 -0
- package/dist/core/warnings.js +98 -0
- package/dist/core/worktree.js +24 -0
- package/dist/facts.js +9 -9
- package/dist/facts.json +8 -8
- package/dist/wasm/tree-sitter-c.wasm +0 -0
- package/dist/wasm/tree-sitter-c_sharp.wasm +0 -0
- package/dist/wasm/tree-sitter-cpp.wasm +0 -0
- package/dist/wasm/tree-sitter-go.wasm +0 -0
- package/dist/wasm/tree-sitter-ruby.wasm +0 -0
- package/dist/wasm/tree-sitter-rust.wasm +0 -0
- package/docs/cli.md +1 -1
- package/docs/code-map.md +22 -6
- package/docs/concepts/loop-engine.md +24 -0
- package/docs/concepts/observer-protocol.md +22 -0
- package/docs/concepts/plans-and-claims.md +57 -0
- package/docs/integrations/claude-code.md +53 -0
- package/docs/integrations/mcp.md +45 -0
- package/docs/mcp-schema-changelog.md +118 -2
- package/package.json +1 -1
|
@@ -19,6 +19,9 @@ import { buildContext } from '../core/context.js';
|
|
|
19
19
|
import { checkBrainclawInstallableUpdate, getInstalledBrainclawVersion, renderBrainclawInstallableUpdateNotice } from '../core/brainclaw-version.js';
|
|
20
20
|
import { loadConfig } from '../core/config.js';
|
|
21
21
|
import { generateClaimId, loadClaim, saveClaim, adoptClaimSession, releaseClaimWithCascade } from '../core/claims.js';
|
|
22
|
+
import { releaseClaimNextActions } from '../core/next-actions.js';
|
|
23
|
+
import { reconcileClaimConformity } from '../core/claim-conformity.js';
|
|
24
|
+
import { pushStructuredWarning } from '../core/warnings.js';
|
|
22
25
|
import { checkPolicy } from '../core/policy.js';
|
|
23
26
|
import { createWorktree as coreCreateWorktree, sanitizeBranchComponent } from '../core/worktree.js';
|
|
24
27
|
import { startSession } from './session-start.js';
|
|
@@ -251,6 +254,14 @@ export async function handleBclawReleaseClaim(payload, ctx) {
|
|
|
251
254
|
session_id: connectionSessionId,
|
|
252
255
|
override: coordinatorOverrideRequested,
|
|
253
256
|
};
|
|
257
|
+
// pln#636 C2 — read the claim BEFORE the cascade: release is what closes it,
|
|
258
|
+
// and the conformity comparison needs its baseline + declared footprint.
|
|
259
|
+
// Best-effort by construction; a missing claim just means no advisory.
|
|
260
|
+
let claimBeforeRelease;
|
|
261
|
+
try {
|
|
262
|
+
claimBeforeRelease = loadClaim(claimId, cwd);
|
|
263
|
+
}
|
|
264
|
+
catch { /* conformity is advisory — never block a release on it */ }
|
|
254
265
|
let cascadeResult;
|
|
255
266
|
try {
|
|
256
267
|
cascadeResult = releaseClaimWithCascade(claimId, {
|
|
@@ -268,12 +279,38 @@ export async function handleBclawReleaseClaim(payload, ctx) {
|
|
|
268
279
|
planTransitioned ? ` — plan ${cascadePlanId} → ${cascadeNewStatus}` : '',
|
|
269
280
|
planWarning ? ` ⚠ ${planWarning}` : '',
|
|
270
281
|
].join('');
|
|
282
|
+
// pln#634 — release is the single most protocol-loaded moment of the daily
|
|
283
|
+
// loop (it is where the plan cascade either fires or refuses), and it shipped
|
|
284
|
+
// pure data. Derived from what the cascade actually decided.
|
|
285
|
+
const releaseActions = releaseClaimNextActions({
|
|
286
|
+
claimId,
|
|
287
|
+
planId: cascadePlanId,
|
|
288
|
+
planTransitioned,
|
|
289
|
+
planWarning,
|
|
290
|
+
requestedPlanStatus: typeof args.planStatus === 'string' ? args.planStatus : undefined,
|
|
291
|
+
});
|
|
292
|
+
// pln#636 C2 — release is the natural reconcile point: the work is finished, so
|
|
293
|
+
// the footprint is final. Emits ONLY on a concrete, path-resolvable violation;
|
|
294
|
+
// every doubt (no baseline, prose scope, reaped worktree) stays silent.
|
|
295
|
+
const conformityWarnings = [];
|
|
296
|
+
const conformityDetails = [];
|
|
297
|
+
if (claimBeforeRelease) {
|
|
298
|
+
try {
|
|
299
|
+
const conformity = reconcileClaimConformity(claimBeforeRelease, cwd);
|
|
300
|
+
if (conformity.warning) {
|
|
301
|
+
pushStructuredWarning(conformityWarnings, conformityDetails, conformity.warning);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
catch { /* advisory only */ }
|
|
305
|
+
}
|
|
271
306
|
return {
|
|
272
307
|
response: toolResponse({
|
|
273
308
|
content: [{ type: 'text', text: summaryText }],
|
|
274
309
|
claim_id: claimId,
|
|
275
310
|
...(planTransitioned ? { plan_id: cascadePlanId, plan_status: cascadeNewStatus } : {}),
|
|
276
311
|
...(planWarning ? { plan_warning: planWarning, plan_id: cascadePlanId } : {}),
|
|
312
|
+
...(conformityWarnings.length ? { warnings: conformityWarnings, warning_details: conformityDetails } : {}),
|
|
313
|
+
...(releaseActions.length ? { next_actions: releaseActions } : {}),
|
|
277
314
|
}),
|
|
278
315
|
};
|
|
279
316
|
}
|
|
@@ -540,6 +577,23 @@ export async function handleBclawAssignmentUpdate(payload, ctx) {
|
|
|
540
577
|
actor: callerAgent,
|
|
541
578
|
actor_id: resolved.identity.agent_id,
|
|
542
579
|
}, cwd);
|
|
580
|
+
// pln#636 C2 — reconcile the linked claim's scope BEFORE the cascade below
|
|
581
|
+
// releases it (after release there is no claim left to read). This is the
|
|
582
|
+
// lifecycle boundary a worker crosses when it reports its own completion.
|
|
583
|
+
const asgnConformityWarnings = [];
|
|
584
|
+
const asgnConformityDetails = [];
|
|
585
|
+
if (status === 'completed' && assignment.claim_id) {
|
|
586
|
+
try {
|
|
587
|
+
const linkedClaim = loadClaim(assignment.claim_id, cwd);
|
|
588
|
+
if (linkedClaim) {
|
|
589
|
+
const conformity = reconcileClaimConformity(linkedClaim, cwd);
|
|
590
|
+
if (conformity.warning) {
|
|
591
|
+
pushStructuredWarning(asgnConformityWarnings, asgnConformityDetails, conformity.warning);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
catch { /* advisory only — never block a completion report */ }
|
|
596
|
+
}
|
|
543
597
|
// trp#928 — cascade-release the assignment's linked claim on completion.
|
|
544
598
|
// Before this landing an obedient worker had to make TWO calls to close
|
|
545
599
|
// the loop (bclaw_assignment_update status=completed AND
|
|
@@ -628,6 +682,9 @@ export async function handleBclawAssignmentUpdate(payload, ctx) {
|
|
|
628
682
|
...(result.assignment.completed_at && { completed_at: result.assignment.completed_at }),
|
|
629
683
|
last_heartbeat_at: result.assignment.last_heartbeat_at,
|
|
630
684
|
...(createdActionId ? { action_id: createdActionId } : {}),
|
|
685
|
+
...(asgnConformityWarnings.length
|
|
686
|
+
? { warnings: asgnConformityWarnings, warning_details: asgnConformityDetails }
|
|
687
|
+
: {}),
|
|
631
688
|
},
|
|
632
689
|
},
|
|
633
690
|
};
|
|
@@ -20,12 +20,16 @@ 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';
|
|
24
|
+
import { coordinateNextActions, dispatchNextActions } from '../core/next-actions.js';
|
|
25
|
+
import { agentValidationFailedWarning, planAlreadyAssignedWarning, pushStructuredWarning, scopeAlreadyClaimedWarning, } from '../core/warnings.js';
|
|
23
26
|
import { ackMessage, getThread, hasActiveAssignment, sendMessage } from '../core/messaging.js';
|
|
24
27
|
import { dispatch, dispatchReview, generateDispatchBrief } from '../core/dispatcher.js';
|
|
25
28
|
import { CoordinateRequestSchema } from '../core/facade-schema.js';
|
|
26
29
|
import { buildInvokeCommand, getCapabilityProfile, getSpawnableAgents, resolveModel, validateAgentForDispatch, } from '../core/agent-capability.js';
|
|
27
30
|
import { attemptExecution } from '../core/execution.js';
|
|
28
31
|
import { createAgentRun, transitionAgentRun } from '../core/agentruns.js';
|
|
32
|
+
import { prepareTurnOwnedReviewDispatch, turnOwnedReviewEnabled } from '../core/review-loop-turn-dispatch.js';
|
|
29
33
|
import { createAssignment, generateAssignmentId, patchAssignmentMessageId, transitionAssignment, } from '../core/assignments.js';
|
|
30
34
|
import { createToolErrorResponse, toolResponse, } from './mcp-contract.js';
|
|
31
35
|
import { handleMcpReadToolCall } from './mcp-read-handlers.js';
|
|
@@ -171,12 +175,25 @@ export async function handleBclawDispatch(args, ctx) {
|
|
|
171
175
|
item_type: 'dispatch',
|
|
172
176
|
scope: `${dispatchResult.messages_sent.length} assignments`,
|
|
173
177
|
}, cwd);
|
|
178
|
+
// pln#634 — the text body already tells a human what to do next; the
|
|
179
|
+
// structured payload told an agent nothing. Derived from the real cycle
|
|
180
|
+
// outcome: verification targets for what spawned, analysis for what is
|
|
181
|
+
// blocked, and a re-run hint for a dry run. Manual launch commands are
|
|
182
|
+
// deliberately NOT mirrored here — they are not MCP-callable.
|
|
183
|
+
const dispatchActions = dispatchNextActions({
|
|
184
|
+
spawnedTargets: spawned
|
|
185
|
+
.map((m) => m.assignment_id ?? m.run_id ?? m.claim_id)
|
|
186
|
+
.filter((id) => typeof id === 'string' && id.length > 0),
|
|
187
|
+
blockedCount: analysis.blocked.length,
|
|
188
|
+
dryRun: !!args.dryRun,
|
|
189
|
+
});
|
|
174
190
|
return {
|
|
175
191
|
response: toolResponse({
|
|
176
192
|
content: [{ type: 'text', text: lines.join('\n') }],
|
|
177
193
|
...dispatchResult,
|
|
178
194
|
sequence_id: analysis.sequence.id,
|
|
179
195
|
dry_run: !!args.dryRun,
|
|
196
|
+
...(dispatchActions.length ? { next_actions: dispatchActions } : {}),
|
|
180
197
|
}),
|
|
181
198
|
};
|
|
182
199
|
}
|
|
@@ -228,11 +245,13 @@ export function handleBclawSendMessage(args, ctx) {
|
|
|
228
245
|
}, cwd);
|
|
229
246
|
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
247
|
const threadInfo = threadId ? ` thread:${threadId}` : '';
|
|
248
|
+
const warningLine = result.warning ? `\n⚠ ${result.warning}` : '';
|
|
231
249
|
return {
|
|
232
250
|
response: toolResponse({
|
|
233
|
-
content: [{ type: 'text', text: `✔ Message sent: [${result.shortLabel}] ${msgType} → ${to}${threadInfo}` }],
|
|
251
|
+
content: [{ type: 'text', text: `✔ Message sent: [${result.shortLabel}] ${msgType} → ${to}${threadInfo}${warningLine}` }],
|
|
234
252
|
message_id: result.id,
|
|
235
253
|
thread_id: threadId,
|
|
254
|
+
...(result.warning ? { warning: result.warning } : {}),
|
|
236
255
|
}),
|
|
237
256
|
};
|
|
238
257
|
}
|
|
@@ -270,7 +289,7 @@ export function handleBclawAckMessage(args, ctx) {
|
|
|
270
289
|
}
|
|
271
290
|
}
|
|
272
291
|
export async function handleBclawCoordinate(args, ctx) {
|
|
273
|
-
const { cwd, connectionSessionId, currentModel } = ctx;
|
|
292
|
+
const { cwd, connectionSessionId, currentModel, effectiveScope } = ctx;
|
|
274
293
|
const startMs = Date.now();
|
|
275
294
|
const parseResult = CoordinateRequestSchema.safeParse(args);
|
|
276
295
|
if (!parseResult.success) {
|
|
@@ -304,6 +323,11 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
304
323
|
// for the intents that actually spawn a worktree worker). See the
|
|
305
324
|
// assessDirtyDispatchGuard call after the cross-project block.
|
|
306
325
|
const warnings = [];
|
|
326
|
+
// pln#635 — structured sibling of `warnings`. Deliberately a SUBSET: the
|
|
327
|
+
// codes that carry a recovery path write here, prose advisories stay
|
|
328
|
+
// string-only for now (`warnings` remains the complete channel — see
|
|
329
|
+
// core/warnings.ts for the reference-threading reason).
|
|
330
|
+
const warningDetails = [];
|
|
307
331
|
const artifacts = [];
|
|
308
332
|
const side_effects = [];
|
|
309
333
|
// can_5e62334e — codex sandboxed dispatches cannot commit in worktrees
|
|
@@ -356,6 +380,35 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
356
380
|
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
381
|
}
|
|
358
382
|
const effectiveAutoExecute = isCrossProject ? false : req.autoExecute;
|
|
383
|
+
// pln#521 P1 — project resolution gate. A review loop written into the wrong
|
|
384
|
+
// store is worse than one that never opened: candidate, claim, assignment and
|
|
385
|
+
// loop all persist where nobody is watching, and the reviewer spawns against
|
|
386
|
+
// the wrong repo (DGX misroute). So when this store can host several projects
|
|
387
|
+
// and NONE was selected, refuse here — before the pre-flight spawn and before
|
|
388
|
+
// the first write — instead of silently defaulting to cwd. Explicit `project`,
|
|
389
|
+
// a session switch, or an active-project pointer all count as a choice; ref /
|
|
390
|
+
// scope / path never do (B3 rejected, art_e29e88878209). Scoped to the failing
|
|
391
|
+
// path (review + open_loop): every other intent keeps its routing untouched.
|
|
392
|
+
let projectResolution;
|
|
393
|
+
if (req.intent === 'review' && req.open_loop === true) {
|
|
394
|
+
// Resolve from `cwd`, not `dispatchCwd`: an explicit project name is
|
|
395
|
+
// resolvable from the SOURCE store (its links / store chain), which is
|
|
396
|
+
// exactly what produced dispatchCwd above. Re-resolving from the target
|
|
397
|
+
// could fail for a link whose name differs from the target's project_name.
|
|
398
|
+
const resolution = validateLoopProjectResolution({
|
|
399
|
+
cwd,
|
|
400
|
+
projectArg: req.project,
|
|
401
|
+
activeSource: effectiveScope?.active_source,
|
|
402
|
+
});
|
|
403
|
+
if (!resolution.ok) {
|
|
404
|
+
return {
|
|
405
|
+
response: createToolErrorResponse(resolution.code, resolution.message, {
|
|
406
|
+
candidates: resolution.candidates,
|
|
407
|
+
}),
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
projectResolution = resolution;
|
|
411
|
+
}
|
|
359
412
|
// can_30c295b4 / trp#371 Tier 2 — scope-aware dirty-working-tree guard.
|
|
360
413
|
// Intents that spawn a worktree worker from HEAD can review/edit stale code,
|
|
361
414
|
// so they are guarded; consult/summarize (no worktree) are not. pln#626
|
|
@@ -403,7 +456,7 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
403
456
|
/** Run E2E execution phase on prepared delivery entries. Returns overall execution status. */
|
|
404
457
|
const runCoordinateExecution = async (prepared, opts) => {
|
|
405
458
|
let overall = 'inbox_only';
|
|
406
|
-
for (const { entry, invoke, worktreePath } of prepared) {
|
|
459
|
+
for (const { entry, invoke, worktreePath, turnEcho } of prepared) {
|
|
407
460
|
const execResult = await attemptExecution(invoke, {
|
|
408
461
|
agent: entry.agent,
|
|
409
462
|
autoExecute: opts.autoExecute,
|
|
@@ -414,6 +467,8 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
414
467
|
dispatcherAgentId: opts.senderAgentId,
|
|
415
468
|
cwd: opts.cwd,
|
|
416
469
|
requireWorktree: true, // pln#531: never spawn a worker in the integration repo
|
|
470
|
+
turnEcho, // pln#630 — turn-owned reviewer (initial dispatch): the ack-wrapper writes the
|
|
471
|
+
// turn-keyed completion sentinel. undefined for every non-turn-owned entry.
|
|
417
472
|
});
|
|
418
473
|
entry.execution_status = execResult.execution_status;
|
|
419
474
|
// pln#626 Phase 1 — carry the machine-readable reason (+ failure_kind) to
|
|
@@ -436,7 +491,23 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
436
491
|
// emit three identical context-free warnings (pln#626 Phase 1 R5).
|
|
437
492
|
if (execResult.error)
|
|
438
493
|
opts.warnings.push(`${entry.agent}: ${execResult.error}`);
|
|
439
|
-
if (
|
|
494
|
+
if (turnEcho) {
|
|
495
|
+
// pln#630 risk #1 — a turn-owned reviewer's run was ALREADY created (`created`) by
|
|
496
|
+
// prepareTurnOwnedReviewDispatch. Do NOT mint a second run here (double-mint). Transition
|
|
497
|
+
// the deterministic run → running on a real spawn (mirrors dispatchReviewLoopTurn); leave
|
|
498
|
+
// it `created` otherwise so the no-sentinel legacy fallback (turnOwnedLaneEvidence) + the
|
|
499
|
+
// pre-run lease reconciler govern it. Non-turn-owned entries take the unchanged else-branch.
|
|
500
|
+
if (execResult.execution_status === 'delivered_and_started') {
|
|
501
|
+
try {
|
|
502
|
+
transitionAgentRun(turnEcho.run_id, 'running', {
|
|
503
|
+
actor: opts.senderAgent, actor_id: opts.senderAgentId, pid: execResult.pid,
|
|
504
|
+
status_reason: 'turn-owned reviewer spawned by coordinator',
|
|
505
|
+
}, opts.cwd);
|
|
506
|
+
}
|
|
507
|
+
catch { /* best-effort — the reconciler converges if this races */ }
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
else if (entry.assignment_id && entry.claim_id) {
|
|
440
511
|
if (execResult.failure_kind === 'spawn_no_handshake') {
|
|
441
512
|
try {
|
|
442
513
|
const run = createAgentRun({
|
|
@@ -643,8 +714,7 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
643
714
|
// retry loops can react (e.g. fall back to a different agent).
|
|
644
715
|
const check = validateAgentForDispatch(agentName, { requireSpawnable: true });
|
|
645
716
|
if (!check.valid) {
|
|
646
|
-
warnings
|
|
647
|
-
warning: 'agent_validation_failed',
|
|
717
|
+
pushStructuredWarning(warnings, warningDetails, agentValidationFailedWarning({
|
|
648
718
|
agent: agentName,
|
|
649
719
|
code: check.code,
|
|
650
720
|
reason: check.reason,
|
|
@@ -656,21 +726,19 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
656
726
|
const assignScope = req.scope ?? req.task;
|
|
657
727
|
// Guard: warn if there is already a non-archived assign message for this agent+scope
|
|
658
728
|
if (hasActiveAssignment(agentName, assignScope, dispatchCwd)) {
|
|
659
|
-
warnings
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
existing_agent: agentName,
|
|
729
|
+
pushStructuredWarning(warnings, warningDetails, planAlreadyAssignedWarning({
|
|
730
|
+
planId: assignScope,
|
|
731
|
+
existingAgent: agentName,
|
|
663
732
|
}));
|
|
664
733
|
}
|
|
665
734
|
// Guard: warn if there is already an active claim on the same scope
|
|
666
735
|
const conflictingClaims = listClaims(dispatchCwd).filter((c) => c.status === 'active' && c.scope === assignScope);
|
|
667
736
|
if (conflictingClaims.length > 0) {
|
|
668
737
|
const existing = conflictingClaims[0];
|
|
669
|
-
warnings
|
|
670
|
-
warning: 'scope_already_claimed',
|
|
738
|
+
pushStructuredWarning(warnings, warningDetails, scopeAlreadyClaimedWarning({
|
|
671
739
|
scope: assignScope,
|
|
672
|
-
|
|
673
|
-
|
|
740
|
+
existingAgent: existing.agent,
|
|
741
|
+
existingClaimId: existing.id,
|
|
674
742
|
}));
|
|
675
743
|
}
|
|
676
744
|
const claimResult = createCoordinatorClaim({
|
|
@@ -969,38 +1037,82 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
969
1037
|
id: claimResult.claimId,
|
|
970
1038
|
});
|
|
971
1039
|
let reviewAssignmentId;
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1040
|
+
let reviewTurnEcho;
|
|
1041
|
+
// pln#630 — turn-own the INITIAL reviewer dispatch (same default + kill-switch as the
|
|
1042
|
+
// fix cycle). Skipped for cross-project reviews (no local worktree/sentinel → they never
|
|
1043
|
+
// spawn here). WON: prepare minted the DETERMINISTIC assignment + run + turn()-bound the
|
|
1044
|
+
// slot, so we reuse prep.assignmentId for the brief/message/linkage below and carry the
|
|
1045
|
+
// turnEcho so the ack-wrapper writes the turn-keyed sentinel. DENIED: the exactly-once
|
|
1046
|
+
// fence says this dispatch is NOT the spawner — do NOT spawn, do NOT fall back to legacy
|
|
1047
|
+
// (that is the double-spawn hole), and do NOT release the (possibly shared) claim; leave
|
|
1048
|
+
// the slot for reconcile/self-heal. LEGACY: the unchanged inline mint runs.
|
|
1049
|
+
let usedTurnOwned = false;
|
|
1050
|
+
if (turnOwnedReviewEnabled() && !req.project) {
|
|
1051
|
+
const prep = prepareTurnOwnedReviewDispatch({
|
|
1052
|
+
loopId: loop.id,
|
|
1053
|
+
slotId: slot.slot_id,
|
|
978
1054
|
agent: slot.agent ?? '',
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
1055
|
+
agentId: slot.agent_id,
|
|
1056
|
+
phase: 'findings',
|
|
1057
|
+
task: req.task,
|
|
982
1058
|
description: reviewDescription,
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1059
|
+
scope: reviewScope,
|
|
1060
|
+
claimId: claimResult.claimId,
|
|
1061
|
+
worktreePath: claimResult.worktreePath,
|
|
1062
|
+
dispatcherAgent: senderAgent,
|
|
1063
|
+
dispatcherAgentId: senderAgentId,
|
|
1064
|
+
sessionId: connectionSessionId,
|
|
1065
|
+
isReviewer: true,
|
|
1066
|
+
cwd: dispatchCwd,
|
|
1067
|
+
});
|
|
1068
|
+
if (prep.kind === 'won') {
|
|
1069
|
+
reviewAssignmentId = prep.assignmentId; // deterministic — harvest correlates on it
|
|
1070
|
+
reviewTurnEcho = { turn_id: prep.turnId, run_id: prep.runId, nonce: prep.nonce };
|
|
1071
|
+
out.artifacts.push({ type: 'assignment', id: prep.assignmentId });
|
|
1072
|
+
usedTurnOwned = true; // prepare already created the assignment + run + bound the slot
|
|
1073
|
+
}
|
|
1074
|
+
else if (prep.kind === 'denied') {
|
|
1075
|
+
out.partial = true;
|
|
1076
|
+
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`);
|
|
1077
|
+
continue; // MUST NOT spawn AND MUST NOT legacy-fallback beside a live reservation
|
|
1078
|
+
}
|
|
1079
|
+
// prep.kind === 'legacy' (pre-identity failure) → fall through to the inline mint.
|
|
987
1080
|
}
|
|
988
|
-
|
|
989
|
-
|
|
1081
|
+
if (!usedTurnOwned) {
|
|
1082
|
+
try {
|
|
1083
|
+
const preId = generateAssignmentId(dispatchCwd);
|
|
1084
|
+
const assignment = createAssignment({
|
|
1085
|
+
id: preId.id,
|
|
1086
|
+
short_label: preId.short_label,
|
|
1087
|
+
claim_id: claimResult.claimId,
|
|
1088
|
+
agent: slot.agent ?? '',
|
|
1089
|
+
dispatcher_agent: senderAgent,
|
|
1090
|
+
dispatcher_session_id: connectionSessionId,
|
|
1091
|
+
scope: reviewScope,
|
|
1092
|
+
description: reviewDescription,
|
|
1093
|
+
tags: ['coordinate', 'review', 'loop'],
|
|
1094
|
+
}, dispatchCwd);
|
|
1095
|
+
reviewAssignmentId = assignment.id;
|
|
1096
|
+
out.artifacts.push({ type: 'assignment', id: assignment.id });
|
|
1097
|
+
}
|
|
1098
|
+
catch (asgErr) {
|
|
1099
|
+
out.warnings.push(`Review assignment creation failed for slot ${slot.slot_id}: ${asgErr instanceof Error ? asgErr.message : String(asgErr)}`);
|
|
1100
|
+
}
|
|
1101
|
+
// pln#628 Focus 4B (BLOCKING 2) — assign the slot NOW that the
|
|
1102
|
+
// claim/assignment exist, binding their ids onto the slot so the
|
|
1103
|
+
// harvest close resolves this exact reviewer by assignment_id. Runs
|
|
1104
|
+
// even if assignment creation failed (undefined id → the harvest
|
|
1105
|
+
// falls back to the legacy agent match for this one slot). (For the
|
|
1106
|
+
// turn-owned WON path, prepare already turn()-bound the slot.)
|
|
1107
|
+
turn({
|
|
1108
|
+
id: loop.id,
|
|
1109
|
+
slot_id: slot.slot_id,
|
|
1110
|
+
actor: creatorActor,
|
|
1111
|
+
input: req.task,
|
|
1112
|
+
assignment_id: reviewAssignmentId,
|
|
1113
|
+
claim_id: claimResult.claimId,
|
|
1114
|
+
}, dispatchCwd);
|
|
990
1115
|
}
|
|
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
1116
|
const reviewBrief = buildCoordinateBrief(slot.agent ?? '', reviewDescription + reviewVerdictBriefSuffix, {
|
|
1005
1117
|
claimId: claimResult.claimId,
|
|
1006
1118
|
scope: reviewScope,
|
|
@@ -1045,6 +1157,7 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
1045
1157
|
entry: queued.entry,
|
|
1046
1158
|
invoke: queued.invoke,
|
|
1047
1159
|
worktreePath: claimResult.worktreePath,
|
|
1160
|
+
turnEcho: reviewTurnEcho,
|
|
1048
1161
|
});
|
|
1049
1162
|
}
|
|
1050
1163
|
catch (dispatchErr) {
|
|
@@ -1105,6 +1218,16 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
1105
1218
|
result = {
|
|
1106
1219
|
candidate_id: output.candidateId,
|
|
1107
1220
|
selected_targets: resolvedAgents,
|
|
1221
|
+
// pln#521 P1 (B4) — echo the routing decision so an operator can see WHERE
|
|
1222
|
+
// the loop landed without reverse-engineering cwd and store state. Present
|
|
1223
|
+
// for open_loop reviews (the gated path); the reasoning behind the decision
|
|
1224
|
+
// ships as `_resolution_trace` on dispatch_status, not here.
|
|
1225
|
+
...(projectResolution
|
|
1226
|
+
? {
|
|
1227
|
+
project_cwd: projectResolution.project_cwd,
|
|
1228
|
+
...(projectResolution.project_name ? { project_name: projectResolution.project_name } : {}),
|
|
1229
|
+
}
|
|
1230
|
+
: {}),
|
|
1108
1231
|
// pln#626 Phase 1 (review rework) — expose the reviewer delivery entries
|
|
1109
1232
|
// so review is as honest as assign/reroute: each entry's execution_reason
|
|
1110
1233
|
// (set by runCoordinateExecution) feeds the top-level derivation, so a
|
|
@@ -1149,8 +1272,7 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
1149
1272
|
// trp#51: validate target agent before creating a new claim.
|
|
1150
1273
|
const check = validateAgentForDispatch(newAgentName, { requireSpawnable: true });
|
|
1151
1274
|
if (!check.valid) {
|
|
1152
|
-
warnings
|
|
1153
|
-
warning: 'agent_validation_failed',
|
|
1275
|
+
pushStructuredWarning(warnings, warningDetails, agentValidationFailedWarning({
|
|
1154
1276
|
agent: newAgentName,
|
|
1155
1277
|
code: check.code,
|
|
1156
1278
|
reason: check.reason,
|
|
@@ -1537,8 +1659,7 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
1537
1659
|
// behind that only fails later at spawn time.
|
|
1538
1660
|
const critCheck = validateAgentForDispatch(slot.agent, { requireSpawnable: true });
|
|
1539
1661
|
if (!critCheck.valid) {
|
|
1540
|
-
warnings
|
|
1541
|
-
warning: 'agent_validation_failed',
|
|
1662
|
+
pushStructuredWarning(warnings, warningDetails, agentValidationFailedWarning({
|
|
1542
1663
|
agent: slot.agent,
|
|
1543
1664
|
code: critCheck.code,
|
|
1544
1665
|
reason: critCheck.reason,
|
|
@@ -1550,12 +1671,6 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
1550
1671
|
slotRole: slot.role,
|
|
1551
1672
|
memoryProvider: provider,
|
|
1552
1673
|
});
|
|
1553
|
-
turn({
|
|
1554
|
-
id: loopId,
|
|
1555
|
-
slot_id: slot.slot_id,
|
|
1556
|
-
actor: creatorActor,
|
|
1557
|
-
input: briefResult.text,
|
|
1558
|
-
}, dispatchCwd);
|
|
1559
1674
|
// pln#626 Phase 2 (Option B) — spawn the critic as a worktree-isolated
|
|
1560
1675
|
// worker, mirroring the intent=assign / review chain. Each critic gets
|
|
1561
1676
|
// its OWN claim + worktree (scope is unique per slot) so parallel
|
|
@@ -1605,6 +1720,23 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
1605
1720
|
catch (asgErr) {
|
|
1606
1721
|
warnings.push(`ideate assignment creation failed for slot ${slot.slot_id}: ${asgErr instanceof Error ? asgErr.message : String(asgErr)}`);
|
|
1607
1722
|
}
|
|
1723
|
+
// pln#629 — bind the slot to its claim/assignment NOW that both
|
|
1724
|
+
// exist (mirrors the review path, pln#628 BLOCKING 2). The turn()
|
|
1725
|
+
// used to fire BEFORE the assignment was created, leaving
|
|
1726
|
+
// slot.assignment_id undefined: bclaw_loop get's reconcile then
|
|
1727
|
+
// skipped the critic slot (loops-handlers.ts `if (!assignmentId)
|
|
1728
|
+
// continue`) and dispatch_status(lop_) resolved no assignment, so
|
|
1729
|
+
// ideate loops could never be reconciled (trp_dfe0b941 /
|
|
1730
|
+
// trp_2187b340 / trp_1de94516). Runs even if assignment creation
|
|
1731
|
+
// failed (undefined id → legacy agent-match fallback, as review).
|
|
1732
|
+
turn({
|
|
1733
|
+
id: loopId,
|
|
1734
|
+
slot_id: slot.slot_id,
|
|
1735
|
+
actor: creatorActor,
|
|
1736
|
+
input: briefResult.text,
|
|
1737
|
+
assignment_id: criticAssignmentId,
|
|
1738
|
+
claim_id: claimResult.claimId,
|
|
1739
|
+
}, dispatchCwd);
|
|
1608
1740
|
// pln#626 Phase 2 — the critique-only contract must reach the
|
|
1609
1741
|
// DELIVERED brief, not just the claim record: buildCoordinateBrief
|
|
1610
1742
|
// wraps this in a worker envelope, so prepend the constraint + the
|
|
@@ -1754,8 +1886,17 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
1754
1886
|
// attach a self-documenting `verify_with` hint pointing at the assignment
|
|
1755
1887
|
// record. Callers should not take delivered_and_started at face value —
|
|
1756
1888
|
// it only attests the brief-ack sentinel was touched, not that the worker
|
|
1757
|
-
// is doing useful work.
|
|
1758
|
-
//
|
|
1889
|
+
// is doing useful work.
|
|
1890
|
+
//
|
|
1891
|
+
// pln#634 — the hint used to tell callers to expect "OS pid alive", which
|
|
1892
|
+
// directly contradicts the protocol every instruction file ships
|
|
1893
|
+
// (instruction-templates.ts: trust dispatch_status, NEVER the tracked pid).
|
|
1894
|
+
// On Windows an ack-wrapped spawn runs under cmd.exe, so agent_run.pid is
|
|
1895
|
+
// the wrapper — it exits by design and reads dead while the worker is alive
|
|
1896
|
+
// and committing (trp_7fc3e3c4). An obedient agent following the old text
|
|
1897
|
+
// killed healthy workers. The field shape is unchanged (retro-compat), the
|
|
1898
|
+
// expectation no longer lies, and the authoritative call now also ships in
|
|
1899
|
+
// `next_actions` below as bclaw_dispatch_status.
|
|
1759
1900
|
let verifyWith;
|
|
1760
1901
|
if (resultExecStatus === 'delivered_and_started') {
|
|
1761
1902
|
const firstAssignment = artifacts.find((a) => a.type === 'assignment');
|
|
@@ -1764,11 +1905,25 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
1764
1905
|
action: 'bclaw_find',
|
|
1765
1906
|
entity: 'agent_run',
|
|
1766
1907
|
filter: { assignment_id: firstAssignment.id },
|
|
1767
|
-
expected_when_alive: 'agent_run with status="running" AND
|
|
1908
|
+
expected_when_alive: 'agent_run with status="running" AND last_event_at within the last few minutes. '
|
|
1909
|
+
+ 'Do NOT judge liveness from agent_run.pid — on an ack-wrapped spawn that pid is the '
|
|
1910
|
+
+ `wrapper, not the worker. Prefer bclaw_dispatch_status(target_id: "${firstAssignment.id}") `
|
|
1911
|
+
+ 'for a sentinel-based verdict.',
|
|
1768
1912
|
see_also: 'docs/concepts/dispatch-lifecycle.md',
|
|
1769
1913
|
};
|
|
1770
1914
|
}
|
|
1771
1915
|
}
|
|
1916
|
+
// pln#634 — outcome-derived affordances on the coordinate facade. Derived
|
|
1917
|
+
// from what actually happened (did anything spawn? was a loop opened?), not
|
|
1918
|
+
// from the intent alone; empty means the key is omitted rather than shipping
|
|
1919
|
+
// an empty array for an agent to parse.
|
|
1920
|
+
const coordinateActions = coordinateNextActions({
|
|
1921
|
+
intent: req.intent,
|
|
1922
|
+
assignmentIds: artifacts.filter((a) => a.type === 'assignment').map((a) => a.id),
|
|
1923
|
+
loopId: artifacts.find((a) => a.type === 'loop')?.id,
|
|
1924
|
+
candidateId: artifacts.find((a) => a.type === 'candidate')?.id,
|
|
1925
|
+
executionStatus: resultExecStatus,
|
|
1926
|
+
});
|
|
1772
1927
|
const facadeResponse = {
|
|
1773
1928
|
status: facadeStatus,
|
|
1774
1929
|
intent: req.intent,
|
|
@@ -1780,6 +1935,8 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
1780
1935
|
...(resultExecStatus ? { execution_status: resultExecStatus } : {}),
|
|
1781
1936
|
...(resultExecReason ? { execution_reason: resultExecReason } : {}),
|
|
1782
1937
|
...(verifyWith ? { verify_with: verifyWith } : {}),
|
|
1938
|
+
...(coordinateActions.length ? { next_actions: coordinateActions } : {}),
|
|
1939
|
+
...(warningDetails.length ? { warning_details: warningDetails } : {}),
|
|
1783
1940
|
};
|
|
1784
1941
|
const summaryParts = [`✔ bclaw_coordinate [${req.intent}] targets=${resolvedAgents.length}`];
|
|
1785
1942
|
if (resultExecStatus)
|
|
@@ -1806,7 +1963,9 @@ export async function handleBclawLoop(args, ctx) {
|
|
|
1806
1963
|
}
|
|
1807
1964
|
// pln#562 step 4 — dispatching a turn hands work to another agent; gate
|
|
1808
1965
|
// it at the same trust bar as the other dispatch surfaces.
|
|
1809
|
-
|
|
1966
|
+
// pln#632 — `bind` also SPAWNS real workers (it dispatches the loop's linked
|
|
1967
|
+
// sequence), so it is gated at the same 'trusted' bar as turn-dispatch / coordinate.
|
|
1968
|
+
if ((args?.intent === 'turn' && args?.dispatch === true) || args?.intent === 'bind') {
|
|
1810
1969
|
const resolved = ensureTrust(args, { nameField: 'agent', idField: 'agentId' }, 'trusted', cwd, connectionSessionId);
|
|
1811
1970
|
if (resolved.error) {
|
|
1812
1971
|
return { response: createToolErrorResponse(resolved.error.kind, resolved.error.message, resolved.error.details) };
|
|
@@ -1814,7 +1973,7 @@ export async function handleBclawLoop(args, ctx) {
|
|
|
1814
1973
|
}
|
|
1815
1974
|
const { handleBclawLoop: runLoopIntent } = await import('./loops-handlers.js');
|
|
1816
1975
|
const targetCwd = resolveProjectCwd(args?.project, cwd);
|
|
1817
|
-
const result = runLoopIntent({ args: args, cwd: targetCwd });
|
|
1976
|
+
const result = await runLoopIntent({ args: args, cwd: targetCwd, sessionId: connectionSessionId });
|
|
1818
1977
|
return {
|
|
1819
1978
|
response: toolResponse({
|
|
1820
1979
|
content: [{ type: 'text', text: result.summary }],
|
|
@@ -23,6 +23,7 @@ import { createPlan, deletePlan as deletePlanOp } from '../core/operations/plan.
|
|
|
23
23
|
import { loadCandidate } from '../core/candidates.js';
|
|
24
24
|
import { resolveCrossProjectWritableTarget, resolveProjectCwd, writeCrossProjectSignal } from '../core/cross-project.js';
|
|
25
25
|
import { hasMinimumTrustLevel } from '../core/agent-registry.js';
|
|
26
|
+
import { createEntityNextActions, transitionNextActions } from '../core/next-actions.js';
|
|
26
27
|
import { acceptCandidate } from './accept.js';
|
|
27
28
|
import { rejectCandidate } from './reject.js';
|
|
28
29
|
import { applyHandoffUpdates } from './update-handoff.js';
|
|
@@ -432,6 +433,10 @@ export function handleBclawCreate(payload, ctx) {
|
|
|
432
433
|
const createContent = autoRepair
|
|
433
434
|
? [{ type: 'text', text: createText }, { type: 'text', text: renderAutoRepairWarning(autoRepair, actor ?? 'unknown') }]
|
|
434
435
|
: [{ type: 'text', text: createText }];
|
|
436
|
+
// pln#634 — a freshly created plan whose steps are never added is the most
|
|
437
|
+
// common half-finished shape in the store; a sequence with no readiness
|
|
438
|
+
// check is the second. Only those two emit a follow-up.
|
|
439
|
+
const createActions = createEntityNextActions({ entity, id: result.id });
|
|
435
440
|
return {
|
|
436
441
|
response: appendSecurityWarnings(toolResponse({
|
|
437
442
|
content: createContent,
|
|
@@ -441,6 +446,7 @@ export function handleBclawCreate(payload, ctx) {
|
|
|
441
446
|
active_source: autoSwitched ? 'auto_switch' : targetScope.active_source,
|
|
442
447
|
...(autoSwitched ? { auto_switched: true } : {}),
|
|
443
448
|
...(autoRepair ? { auto_repair: autoRepair } : {}),
|
|
449
|
+
...(createActions.length ? { next_actions: createActions } : {}),
|
|
444
450
|
},
|
|
445
451
|
}), createScan.warnings),
|
|
446
452
|
};
|
|
@@ -600,6 +606,10 @@ export function handleBclawTransition(payload, ctx) {
|
|
|
600
606
|
const transitionContent = auto_repair
|
|
601
607
|
? [{ type: 'text', text: transitionText }, { type: 'text', text: renderAutoRepairWarning(auto_repair, agent_name) }]
|
|
602
608
|
: [{ type: 'text', text: transitionText }];
|
|
609
|
+
// pln#634 — only the two transitions that imply an unambiguous next call
|
|
610
|
+
// emit anything (plan → in_progress / blocked); everything else is terminal
|
|
611
|
+
// for the caller and returns nothing rather than inventing busywork.
|
|
612
|
+
const transitionActions = transitionNextActions({ entity, id, to });
|
|
603
613
|
return {
|
|
604
614
|
response: toolResponse({
|
|
605
615
|
content: transitionContent,
|
|
@@ -609,6 +619,7 @@ export function handleBclawTransition(payload, ctx) {
|
|
|
609
619
|
active_source: autoSwitched ? 'auto_switch' : targetScope.active_source,
|
|
610
620
|
...(autoSwitched ? { auto_switched: true } : {}),
|
|
611
621
|
...(auto_repair ? { auto_repair } : {}),
|
|
622
|
+
...(transitionActions.length ? { next_actions: transitionActions } : {}),
|
|
612
623
|
},
|
|
613
624
|
}),
|
|
614
625
|
};
|