brainclaw 1.28.3 → 1.28.4
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 +6 -0
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/commands/harvest.js +67 -25
- package/dist/commands/loops-handlers.js +28 -1
- package/dist/commands/mcp-catalog.js +23 -2
- package/dist/commands/mcp-read-handlers.js +15 -1
- package/dist/commands/mcp-schemas.generated.js +13 -0
- package/dist/commands/mcp-write-coordination.js +84 -22
- package/dist/commands/mcp-write-memory.js +87 -1
- package/dist/commands/mcp.js +16 -2
- package/dist/core/context.js +16 -3
- package/dist/core/dispatch-status.js +36 -14
- package/dist/core/dispatcher.js +28 -20
- package/dist/core/entity-operations.js +62 -4
- package/dist/core/entity-registry.js +3 -3
- package/dist/core/execution-adapters.js +10 -0
- package/dist/core/facade-schema.js +10 -0
- package/dist/core/ideation-loop-close.js +3 -1
- package/dist/core/lane-result-file.js +72 -0
- package/dist/core/loop-turn-dispatch.js +2 -0
- package/dist/core/loops/brief-assembly.js +19 -11
- package/dist/core/loops/next-expected.js +56 -1
- package/dist/core/loops/reconcile-turn.js +8 -0
- package/dist/core/loops/result-reducers.js +14 -12
- package/dist/core/loops/store.js +4 -0
- package/dist/core/loops/types.js +14 -2
- package/dist/core/loops/verbs.js +8 -1
- package/dist/core/loops/worker-reply-contract.js +1 -1
- package/dist/core/protocol-tool-policy.js +1 -0
- package/dist/core/review-loop-turn-dispatch.js +1 -0
- package/dist/core/schema.js +24 -1
- package/dist/core/search.js +3 -2
- package/dist/core/worktree.js +14 -7
- package/dist/facts.js +9 -8
- package/dist/facts.json +8 -7
- package/docs/cli.md +33 -0
- package/docs/concepts/ideation-loop.md +35 -14
- package/docs/integrations/mcp.md +13 -3
- package/docs/mcp-schema-changelog.md +42 -6
- package/package.json +1 -1
|
@@ -20,7 +20,8 @@ import { deleteMemoryItem, updateMemoryItem } from '../core/operations/memory-mu
|
|
|
20
20
|
import { assessMemoryPressure, buildCompactionTemplate, applyCompaction } from '../core/gc-semantic.js';
|
|
21
21
|
import { createRuntimeNote } from './runtime-note.js';
|
|
22
22
|
import { createCandidateFromInput } from './reflect.js';
|
|
23
|
-
import { harvestCandidates } from './harvest.js';
|
|
23
|
+
import { harvestCandidates, harvestLaneResults, integrateLaneResults } from './harvest.js';
|
|
24
|
+
import { dispatchReviewLoopTurn } from '../core/review-loop-turn-dispatch.js';
|
|
24
25
|
import { ensureTrust, scanMcpWriteText, appendSecurityWarnings } from './mcp-write-support.js';
|
|
25
26
|
import { toolResponse, createToolErrorResponse, } from './mcp-contract.js';
|
|
26
27
|
function scoreKeywordMatches(text, patterns) {
|
|
@@ -448,4 +449,89 @@ export function handleBclawHarvestCandidates(payload, _ctx) {
|
|
|
448
449
|
}),
|
|
449
450
|
};
|
|
450
451
|
}
|
|
452
|
+
/** MCP parity for the CLI lane-result harvest path (distinct from candidates). */
|
|
453
|
+
export async function handleBclawHarvestLane(payload) {
|
|
454
|
+
const { args, cwd, connectionSessionId } = payload;
|
|
455
|
+
const resolved = ensureTrust(args, { nameField: 'agent', idField: 'agentId' }, 'trusted', cwd, connectionSessionId);
|
|
456
|
+
if (resolved.error) {
|
|
457
|
+
return { response: createToolErrorResponse(resolved.error.kind, resolved.error.message, resolved.error.details) };
|
|
458
|
+
}
|
|
459
|
+
const assignmentId = typeof args.assignmentId === 'string' ? args.assignmentId : undefined;
|
|
460
|
+
const all = args.all === true;
|
|
461
|
+
if (!assignmentId && !all) {
|
|
462
|
+
return { response: createToolErrorResponse('validation_error', 'Provide assignmentId, or set all=true to scan every managed lane.') };
|
|
463
|
+
}
|
|
464
|
+
if (assignmentId && all) {
|
|
465
|
+
return { response: createToolErrorResponse('validation_error', 'assignmentId and all=true are mutually exclusive.') };
|
|
466
|
+
}
|
|
467
|
+
const worktreePaths = Array.isArray(args.worktreePaths) ? args.worktreePaths.filter((value) => typeof value === 'string') : undefined;
|
|
468
|
+
const dryRun = args.dryRun === true;
|
|
469
|
+
const actor = resolved.identity.agent_name;
|
|
470
|
+
if (args.integrate === true) {
|
|
471
|
+
const integrated = integrateLaneResults({ assignmentId, worktreePaths, dryRun, cwd, agent: actor });
|
|
472
|
+
const dispatchedTurns = [];
|
|
473
|
+
if (!dryRun) {
|
|
474
|
+
for (const next of integrated.next_turns) {
|
|
475
|
+
const dispatched = await dispatchReviewLoopTurn({
|
|
476
|
+
loopId: next.loop_id,
|
|
477
|
+
slot: { slot_id: next.slot_id, role: next.role, agent: next.agent, agent_id: next.agent_id },
|
|
478
|
+
phase: next.phase,
|
|
479
|
+
task: next.task,
|
|
480
|
+
dispatcherAgent: actor,
|
|
481
|
+
dispatcherAgentId: resolved.identity.agent_id,
|
|
482
|
+
cwd,
|
|
483
|
+
});
|
|
484
|
+
dispatchedTurns.push({
|
|
485
|
+
loop_id: next.loop_id,
|
|
486
|
+
agent: next.agent,
|
|
487
|
+
iteration: next.iteration,
|
|
488
|
+
execution_status: dispatched.execution_status,
|
|
489
|
+
error: dispatched.error,
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
return {
|
|
494
|
+
response: toolResponse({
|
|
495
|
+
content: [{ type: 'text', text: `✔ Lane integrate${dryRun ? ' (dry-run)' : ''}: ${integrated.integrated.length} integrated, ${dispatchedTurns.length} re-dispatched, ${integrated.errors.length} error(s).` }],
|
|
496
|
+
...integrated,
|
|
497
|
+
dispatched_turns: dispatchedTurns,
|
|
498
|
+
dry_run: dryRun,
|
|
499
|
+
}),
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
const harvested = harvestLaneResults({ assignmentId, worktreePaths, dryRun, cwd, agent: actor });
|
|
503
|
+
const continuationActions = harvested.continuations.flatMap((continuation) => {
|
|
504
|
+
const next = continuation.next_expected;
|
|
505
|
+
if (!next)
|
|
506
|
+
return [];
|
|
507
|
+
if (next.action === 'turn' && next.slot_id) {
|
|
508
|
+
return [{
|
|
509
|
+
tool: 'bclaw_loop',
|
|
510
|
+
args: { intent: 'turn', loop_id: continuation.loop_id, slot_id: next.slot_id, dispatch: true },
|
|
511
|
+
when: next.reason ?? 'dispatch the next sequential loop participant',
|
|
512
|
+
}];
|
|
513
|
+
}
|
|
514
|
+
if (next.action === 'advance') {
|
|
515
|
+
return [{ tool: 'bclaw_loop', args: { intent: 'advance', loop_id: continuation.loop_id }, when: 'the current phase gate is satisfied' }];
|
|
516
|
+
}
|
|
517
|
+
return [{ tool: 'bclaw_loop', args: { intent: 'get', loop_id: continuation.loop_id }, when: next.reason ?? `inspect the expected ${next.action} action` }];
|
|
518
|
+
});
|
|
519
|
+
return {
|
|
520
|
+
response: toolResponse({
|
|
521
|
+
content: [{ type: 'text', text: `✔ Lane harvest${dryRun ? ' (dry-run)' : ''}: ${harvested.harvested.length} harvested, ${harvested.skipped.length} skipped, ${harvested.errors.length} error(s), ${harvested.warnings.length} warning(s).` }],
|
|
522
|
+
structuredContent: {
|
|
523
|
+
harvested: harvested.harvested,
|
|
524
|
+
skipped: harvested.skipped,
|
|
525
|
+
errors: harvested.errors,
|
|
526
|
+
warnings: harvested.warnings,
|
|
527
|
+
continuations: harvested.continuations,
|
|
528
|
+
dry_run: dryRun,
|
|
529
|
+
next_actions: [
|
|
530
|
+
...continuationActions,
|
|
531
|
+
...harvested.warnings.flatMap((warning) => warning.next_actions ?? []),
|
|
532
|
+
],
|
|
533
|
+
},
|
|
534
|
+
}),
|
|
535
|
+
};
|
|
536
|
+
}
|
|
451
537
|
//# sourceMappingURL=mcp-write-memory.js.map
|
package/dist/commands/mcp.js
CHANGED
|
@@ -48,7 +48,7 @@ import { handleBclawClaim, handleBclawReleaseClaim, handleBclawSessionStart, han
|
|
|
48
48
|
// Sequence write handlers extracted in pln#622 PR4.
|
|
49
49
|
import { handleBclawCreateSequence, handleBclawUpdateSequence, handleBclawDeleteSequence, } from './mcp-write-sequences.js';
|
|
50
50
|
// Memory write handlers extracted in pln#622 PR4.
|
|
51
|
-
import { handleBclawWriteNote, handleBclawQuickCapture, handleBclawCompact, handleBclawDeleteMemory, handleBclawUpdateMemory, handleBclawHarvestCandidates, } from './mcp-write-memory.js';
|
|
51
|
+
import { handleBclawWriteNote, handleBclawQuickCapture, handleBclawCompact, handleBclawDeleteMemory, handleBclawUpdateMemory, handleBclawHarvestCandidates, handleBclawHarvestLane, } from './mcp-write-memory.js';
|
|
52
52
|
// Admin / provisioning write handlers extracted in pln#622 PR4.
|
|
53
53
|
import { handleBclawSetup, handleBclawInitProject, handleBclawAddCapability, handleBclawAddTool, } from './mcp-write-admin.js';
|
|
54
54
|
// Entity write handlers extracted in pln#622 PR4.
|
|
@@ -1736,6 +1736,9 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
1736
1736
|
if (name === 'bclaw_harvest_candidates') {
|
|
1737
1737
|
return handleBclawHarvestCandidates(payload, writeMemoryCtx);
|
|
1738
1738
|
}
|
|
1739
|
+
if (name === 'bclaw_harvest') {
|
|
1740
|
+
return await handleBclawHarvestLane(payload);
|
|
1741
|
+
}
|
|
1739
1742
|
// ── Canonical CRUD verbs (Phase 3 slice 3b) ──────────────────────
|
|
1740
1743
|
//
|
|
1741
1744
|
// Thin wrappers around src/core/entity-operations.ts. Behind
|
|
@@ -1835,6 +1838,17 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
1835
1838
|
};
|
|
1836
1839
|
}
|
|
1837
1840
|
const result = listEntities(entity, targetCwd, filter);
|
|
1841
|
+
const requestedFields = Array.isArray(args.fields)
|
|
1842
|
+
? args.fields.filter((field) => typeof field === 'string' && field.length > 0)
|
|
1843
|
+
: [];
|
|
1844
|
+
if (requestedFields.length > 0) {
|
|
1845
|
+
result.items = result.items.map((item) => {
|
|
1846
|
+
if (!item || typeof item !== 'object')
|
|
1847
|
+
return item;
|
|
1848
|
+
const row = item;
|
|
1849
|
+
return Object.fromEntries(requestedFields.filter((field) => row[field] !== undefined).map((field) => [field, row[field]]));
|
|
1850
|
+
});
|
|
1851
|
+
}
|
|
1838
1852
|
// pln#491 — bound the payload (count is already capped by applyPaging;
|
|
1839
1853
|
// this caps SIZE) so a verbose result set never overflows the MCP token
|
|
1840
1854
|
// cap and silently pushes the agent to the CLI (trp#449). Advertises
|
|
@@ -1850,7 +1864,7 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
1850
1864
|
{ tool: 'bclaw_get', args: { entity, id: '<id from items>', ...(args.project ? { project: args.project } : {}), ...(args.budget_tokens ? { budget_tokens: args.budget_tokens } : {}) }, when: 'to read one item in full' },
|
|
1851
1865
|
];
|
|
1852
1866
|
if (bounded.has_more) {
|
|
1853
|
-
nextActions.push({ tool: 'bclaw_find', args: { entity, filter: { ...filter, offset: bounded.next_offset }, ...(args.project ? { project: args.project } : {}), ...(args.budget_tokens ? { budget_tokens: args.budget_tokens } : {}) }, when: 'to fetch the next page' });
|
|
1867
|
+
nextActions.push({ tool: 'bclaw_find', args: { entity, filter: { ...filter, offset: bounded.next_offset }, ...(requestedFields.length ? { fields: requestedFields } : {}), ...(args.project ? { project: args.project } : {}), ...(args.budget_tokens ? { budget_tokens: args.budget_tokens } : {}) }, when: 'to fetch the next page' });
|
|
1854
1868
|
}
|
|
1855
1869
|
// structuredContent is the canonical MCP return channel that clients
|
|
1856
1870
|
// (VS Code extension, Codex, etc.) read for machine-parseable data.
|
package/dist/core/context.js
CHANGED
|
@@ -29,6 +29,19 @@ import { isTrapActive, listOperationalTraps } from './traps.js';
|
|
|
29
29
|
import { buildEstimationReport } from '../commands/estimation-report.js';
|
|
30
30
|
import { detectStaleness } from './staleness.js';
|
|
31
31
|
export const CONTEXT_SCHEMA_VERSION = '1.2';
|
|
32
|
+
function verificationStatus(item) {
|
|
33
|
+
const verification = item.verification;
|
|
34
|
+
if (!verification)
|
|
35
|
+
return undefined;
|
|
36
|
+
if (verification.outcome === 'fail')
|
|
37
|
+
return 'verification:fail';
|
|
38
|
+
if (verification.max_age_days !== undefined) {
|
|
39
|
+
const ageMs = Date.now() - new Date(verification.verified_at).getTime();
|
|
40
|
+
if (Number.isFinite(ageMs) && ageMs > verification.max_age_days * 86_400_000)
|
|
41
|
+
return 'verification:stale';
|
|
42
|
+
}
|
|
43
|
+
return 'verification:pass';
|
|
44
|
+
}
|
|
32
45
|
export function buildContext(options = {}) {
|
|
33
46
|
const requestedCwd = options.cwd ?? process.cwd();
|
|
34
47
|
const contextCwd = resolveContextStoreCwd(requestedCwd, options.target);
|
|
@@ -95,7 +108,7 @@ export function buildContext(options = {}) {
|
|
|
95
108
|
related_paths: c.related_paths,
|
|
96
109
|
score: 0,
|
|
97
110
|
reasons: [],
|
|
98
|
-
extra: c.status,
|
|
111
|
+
extra: [c.status, verificationStatus(c)].filter(Boolean).join(', '),
|
|
99
112
|
plan_id: c.plan_id,
|
|
100
113
|
provenance: {
|
|
101
114
|
actor: c.author,
|
|
@@ -125,7 +138,7 @@ export function buildContext(options = {}) {
|
|
|
125
138
|
related_paths: d.related_paths,
|
|
126
139
|
score: 0,
|
|
127
140
|
reasons: [],
|
|
128
|
-
extra: d.related_paths?.join(', '),
|
|
141
|
+
extra: [d.related_paths?.join(', '), verificationStatus(d)].filter(Boolean).join(', ') || undefined,
|
|
129
142
|
plan_id: d.plan_id,
|
|
130
143
|
provenance: {
|
|
131
144
|
actor: d.author,
|
|
@@ -155,7 +168,7 @@ export function buildContext(options = {}) {
|
|
|
155
168
|
related_paths: t.related_paths,
|
|
156
169
|
score: 0,
|
|
157
170
|
reasons: [],
|
|
158
|
-
extra: `${t.severity}, visibility:${t.visibility ?? 'shared'}`,
|
|
171
|
+
extra: [`${t.severity}, visibility:${t.visibility ?? 'shared'}, status:${t.status}`, verificationStatus(t)].filter(Boolean).join(', '),
|
|
159
172
|
plan_id: t.plan_id,
|
|
160
173
|
provenance: {
|
|
161
174
|
actor: t.author,
|
|
@@ -28,9 +28,9 @@ import { loadClaim } from './claims.js';
|
|
|
28
28
|
import { getLoop, listLoops } from './loops/store.js';
|
|
29
29
|
import { isProcessAlive } from './agentrun-reconciler.js';
|
|
30
30
|
import { findRuntimeNoteById } from './runtime.js';
|
|
31
|
-
import { latestActivityMs, decodeOemAwareBuffer, getRuntimeLogPath, getRuntimeSignalPath } from './runtime-signals.js';
|
|
31
|
+
import { latestActivityMs, decodeOemAwareBuffer, getRuntimeLogPath, getRuntimeSignalPath, readCompletionSignals } from './runtime-signals.js';
|
|
32
32
|
import { currentAttemptRunIdForAssignment } from './loops/attempt-reservation.js';
|
|
33
|
-
import {
|
|
33
|
+
import { resolveLaneResultFile } from './lane-result-file.js';
|
|
34
34
|
const DEFAULT_TAIL = 20;
|
|
35
35
|
const DEFAULT_STALL_MS = 5 * 60_000;
|
|
36
36
|
const DEFAULT_BASE_REF = 'master';
|
|
@@ -291,6 +291,23 @@ function computeDiagnosis(assignment, agentRun, runtime, options) {
|
|
|
291
291
|
: `Worker reported "${lr.status}". Read the LANE-RESULT summary + stderr; address the blocker or reroute.`,
|
|
292
292
|
};
|
|
293
293
|
}
|
|
294
|
+
if (runtime.terminal_signal) {
|
|
295
|
+
const signal = runtime.terminal_signal;
|
|
296
|
+
if (signal.status === 'contradictory') {
|
|
297
|
+
return {
|
|
298
|
+
health: 'unknown',
|
|
299
|
+
summary: 'both completed and failed terminal sentinels exist; outcome is contradictory and no terminal projection was inferred',
|
|
300
|
+
recommended_next_action: 'Inspect LANE-RESULT.json and both log files; preserve the worktree and reconcile the contradiction before retrying.',
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
health: 'terminal',
|
|
305
|
+
summary: `worker wrapper emitted the canonical ${signal.status} terminal signal${agentRun && !TERMINAL_RUN_STATUSES.has(agentRun.status) ? ` (agent_run still ${agentRun.status})` : ''}`,
|
|
306
|
+
recommended_next_action: signal.status === 'completed'
|
|
307
|
+
? 'Run bclaw_harvest (or `brainclaw harvest <assignment_id>`) to ingest LANE-RESULT and converge the Assignment/Claim.'
|
|
308
|
+
: 'Read stderr and LANE-RESULT if present, then replay or reroute the failed slot.',
|
|
309
|
+
};
|
|
310
|
+
}
|
|
294
311
|
// pln#554 — git evidence is the #2 signal, ABOVE process sentinels and
|
|
295
312
|
// administrative status: commits ahead of base with a clean tracked tree
|
|
296
313
|
// means the worker delivered everything to the branch, even if its pid is
|
|
@@ -359,18 +376,11 @@ function computeDiagnosis(assignment, agentRun, runtime, options) {
|
|
|
359
376
|
?? 'Read .stderr.log for the exit reason; then trigger reconciliation by calling bclaw_find(entity="agent_run") again, or cancel + reroute.',
|
|
360
377
|
};
|
|
361
378
|
}
|
|
362
|
-
if (runtime.pid_alive === true && stallAge > options.stallMs && fsActive) {
|
|
363
|
-
return {
|
|
364
|
-
health: 'healthy',
|
|
365
|
-
summary: `agent_run alive (pid=${runtime.pid}); last_event_at stale (${Math.round(stallAge / 1000)}s) but filesystem active ${Math.round((fsAge ?? 0) / 1000)}s ago — working through a long op without a heartbeat`,
|
|
366
|
-
recommended_next_action: 'No action — the worker is actively writing to logs/worktree. Re-check periodically until terminal.',
|
|
367
|
-
};
|
|
368
|
-
}
|
|
369
379
|
if (runtime.pid_alive === true && stallAge > options.stallMs) {
|
|
370
380
|
return {
|
|
371
381
|
health: 'stalled',
|
|
372
|
-
summary: `agent_run
|
|
373
|
-
recommended_next_action: '
|
|
382
|
+
summary: `agent_run pid=${runtime.pid} is alive but no explicit progress/heartbeat arrived for ${Math.round(stallAge / 1000)}s${fsActive ? `; filesystem activity ${Math.round((fsAge ?? 0) / 1000)}s ago is context, not proof of worker progress` : ''}`,
|
|
383
|
+
recommended_next_action: 'Inspect stdout/stderr and the expected artifact path. If no phase artifact or progress heartbeat is advancing, replay/reroute the slot; do not treat PID or unrelated filesystem activity as health.',
|
|
374
384
|
};
|
|
375
385
|
}
|
|
376
386
|
if (runtime.pid_alive === true) {
|
|
@@ -436,6 +446,9 @@ export function getDispatchStatus(options) {
|
|
|
436
446
|
const stderrPath = assignmentId
|
|
437
447
|
? getRuntimeLogPath(projectRoot, assignmentId, 'stderr', runtimeRunId)
|
|
438
448
|
: undefined;
|
|
449
|
+
const completionSignals = assignmentId
|
|
450
|
+
? readCompletionSignals(projectRoot, assignmentId, runtimeRunId)
|
|
451
|
+
: {};
|
|
439
452
|
// pln#527 — filesystem-activity age: max mtime across the captured logs + the
|
|
440
453
|
// run's worktree files (skipping junctions). The truer liveness signal when
|
|
441
454
|
// the heartbeat / last_event_at is stale during a long single operation.
|
|
@@ -460,8 +473,9 @@ export function getDispatchStatus(options) {
|
|
|
460
473
|
let laneResult;
|
|
461
474
|
let laneResultStale;
|
|
462
475
|
if (worktreeForFs) {
|
|
463
|
-
|
|
464
|
-
|
|
476
|
+
const resolvedLaneResult = resolveLaneResultFile(worktreeForFs, assignmentId);
|
|
477
|
+
if (resolvedLaneResult.kind === 'found') {
|
|
478
|
+
const parsed = resolvedLaneResult.lane;
|
|
465
479
|
if (parsed.assignment_id === assignmentId) {
|
|
466
480
|
laneResult = { status: parsed.status, summary: parsed.summary };
|
|
467
481
|
}
|
|
@@ -469,7 +483,6 @@ export function getDispatchStatus(options) {
|
|
|
469
483
|
laneResultStale = { assignment_id: parsed.assignment_id, status: parsed.status, summary: parsed.summary };
|
|
470
484
|
}
|
|
471
485
|
}
|
|
472
|
-
catch { /* no / invalid LANE-RESULT.json */ }
|
|
473
486
|
}
|
|
474
487
|
// pln#554 — worktree git evidence (commits ahead of base + dirty tracked files).
|
|
475
488
|
const evidence = gitEvidence(worktreeForFs, options.base_ref ?? DEFAULT_BASE_REF);
|
|
@@ -480,6 +493,15 @@ export function getDispatchStatus(options) {
|
|
|
480
493
|
exists: ackPath ? fs.existsSync(ackPath) : false,
|
|
481
494
|
path: ackPath,
|
|
482
495
|
},
|
|
496
|
+
...(assignmentId && (completionSignals.completed || completionSignals.failed) ? {
|
|
497
|
+
terminal_signal: {
|
|
498
|
+
status: completionSignals.completed && completionSignals.failed
|
|
499
|
+
? 'contradictory'
|
|
500
|
+
: completionSignals.completed ? 'completed' : 'failed',
|
|
501
|
+
completed_path: getRuntimeSignalPath(projectRoot, assignmentId, 'completed', runtimeRunId),
|
|
502
|
+
failed_path: getRuntimeSignalPath(projectRoot, assignmentId, 'failed', runtimeRunId),
|
|
503
|
+
},
|
|
504
|
+
} : {}),
|
|
483
505
|
log_files: {
|
|
484
506
|
stdout: stdoutPath ? readLogTail(stdoutPath, tailLines) : undefined,
|
|
485
507
|
stderr: stderrPath ? readLogTail(stderrPath, tailLines) : undefined,
|
package/dist/core/dispatcher.js
CHANGED
|
@@ -37,7 +37,7 @@ import { buildClaimEnvPrefix } from './execution-profile.js';
|
|
|
37
37
|
import { getActiveSequence, listSequences } from './sequence.js';
|
|
38
38
|
import { loadState, persistState } from './state.js';
|
|
39
39
|
import { listClaims, createCoordinatorClaim, attachAssignmentMessageToClaim, linkClaimToAssignment, assessClaimLiveness } from './claims.js';
|
|
40
|
-
import { sanitizeBranchComponent, isBranchMergedByContent, probeLocalBranch, isGitRepo } from './worktree.js';
|
|
40
|
+
import { sanitizeBranchComponent, isBranchMergedByContent, probeLocalBranch, isGitRepo, detectWorkspaceNodeModules } from './worktree.js';
|
|
41
41
|
import { listAgentIdentities, ensureAgentRegisteredForDispatch } from './agent-registry.js';
|
|
42
42
|
import { sendMessage, hasActiveAssignment } from './messaging.js';
|
|
43
43
|
import { memoryDir } from './io.js';
|
|
@@ -328,7 +328,7 @@ export function buildWorkingDefaultsSection(opts) {
|
|
|
328
328
|
'',
|
|
329
329
|
].join('\n');
|
|
330
330
|
}
|
|
331
|
-
export function laneResultShape(assignmentId, contractRef, fence) {
|
|
331
|
+
export function laneResultShape(assignmentId, contractRef, fence, artifactType) {
|
|
332
332
|
const asgn = assignmentId ?? '<assignment_id>';
|
|
333
333
|
const generation = fence
|
|
334
334
|
? `,"turn_id":"${fence.turn_id}","run_id":"${fence.run_id}","nonce":"${fence.nonce}","attempt_epoch":${fence.attempt_epoch},"workspace_digest":"${fence.workspace_digest}"`
|
|
@@ -336,10 +336,11 @@ export function laneResultShape(assignmentId, contractRef, fence) {
|
|
|
336
336
|
const contract = contractRef
|
|
337
337
|
? `,"execution_contract_hash":"${contractRef.hash}","capability_snapshot_hash":"${contractRef.snapshot_hash}"`
|
|
338
338
|
: '';
|
|
339
|
-
|
|
339
|
+
const artifact = artifactType ? `,"artifact_type":"${artifactType}"` : '';
|
|
340
|
+
return `{"assignment_id":"${asgn}"${generation}${contract},"status":"completed|blocked|failed","summary":"<one line>"${artifact},"body":"<your full output — the reasoning, not just a label>","files_changed":["..."],"artifacts":["<ref>",{"type":"file|commit|artifact","ref":"<path-or-id>","description":"<optional>"}]}`;
|
|
340
341
|
}
|
|
341
342
|
export function buildTransportSection(opts) {
|
|
342
|
-
const laneResult = `write
|
|
343
|
+
const laneResult = `write the terminal envelope at the worktree ROOT with the exact filename LANE-RESULT.json: ${laneResultShape(opts.assignmentId, opts.executionContractRef, opts.attemptFence, opts.artifactType)}. The filename is protocol-significant: do not translate it, replace the hyphen, add a suffix, or place it in a subdirectory. The artifacts array accepts either string refs or {type,ref,description} objects; for a loop turn, artifact_type is required exactly as shown`;
|
|
343
344
|
const acceptance = opts.executionContractRef
|
|
344
345
|
? [
|
|
345
346
|
`Execution contract: ${opts.executionContractRef.hash}`,
|
|
@@ -408,8 +409,8 @@ export function buildProtocolSection(options) {
|
|
|
408
409
|
if (options?.worktreePath) {
|
|
409
410
|
parts.push(`Worktree: ${options.worktreePath}`);
|
|
410
411
|
// pln#523 / trp_37b05a15: tell the worker how dependencies are provisioned so
|
|
411
|
-
// it does not stall trying to (re)install them. The
|
|
412
|
-
// the
|
|
412
|
+
// it does not stall trying to (re)install them. The sidecar records intent,
|
|
413
|
+
// but the brief only claims availability after checking the worktree itself.
|
|
413
414
|
// - link (default): node_modules (incl. monorepo per-package) is
|
|
414
415
|
// junction-linked from the main repo — build/typecheck directly; do NOT
|
|
415
416
|
// `npm install`. An out-of-root symlink, so `next dev`/Turbopack rejects
|
|
@@ -417,28 +418,34 @@ export function buildProtocolSection(options) {
|
|
|
417
418
|
// - install/copy: node_modules is a REAL in-root directory — everything,
|
|
418
419
|
// including a dev server, works directly; no reinstall needed.
|
|
419
420
|
// - none: no deps provisioned — run the project's install first.
|
|
420
|
-
let depsMode
|
|
421
|
+
let depsMode;
|
|
421
422
|
let depsProvisioned;
|
|
423
|
+
let recordedPaths = [];
|
|
422
424
|
try {
|
|
423
425
|
const sidecar = JSON.parse(fs.readFileSync(path.join(options.worktreePath, '.brainclaw-worktree.json'), 'utf-8'));
|
|
424
|
-
|
|
425
|
-
depsMode = sidecar.deps_mode;
|
|
426
|
+
depsMode = sidecar.deps_mode;
|
|
426
427
|
depsProvisioned = sidecar.deps_provisioned;
|
|
428
|
+
recordedPaths = Array.isArray(sidecar.deps_paths) ? sidecar.deps_paths : [];
|
|
427
429
|
}
|
|
428
|
-
catch { /* sidecar absent/unreadable —
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
430
|
+
catch { /* sidecar absent/unreadable — do not infer provisioning */ }
|
|
431
|
+
const detectedPaths = detectWorkspaceNodeModules(options.worktreePath)
|
|
432
|
+
.filter((relativePath) => fs.existsSync(path.join(options.worktreePath, relativePath)));
|
|
433
|
+
const verifiedPaths = [...new Set([...recordedPaths, ...detectedPaths])]
|
|
434
|
+
.filter((relativePath) => fs.existsSync(path.join(options.worktreePath, relativePath)));
|
|
435
|
+
if (verifiedPaths.length > 0 && (depsMode === 'install' || depsMode === 'copy')) {
|
|
436
|
+
parts.push(`Dependencies: verified in-root node_modules at ${verifiedPaths.join(', ')} (deps_mode=${depsMode}). Use the repository's local scripts/binaries; do NOT reinstall.`);
|
|
433
437
|
}
|
|
434
|
-
else if (
|
|
435
|
-
parts.push(`Dependencies: node_modules
|
|
438
|
+
else if (verifiedPaths.length > 0 && depsMode === 'link') {
|
|
439
|
+
parts.push(`Dependencies: verified node_modules at ${verifiedPaths.join(', ')} (deps_mode=link). Use the repository's local scripts/binaries; do NOT reinstall. These may be out-of-root links, so next dev/Turbopack can require deps_mode=install.`);
|
|
436
440
|
}
|
|
437
441
|
else if (depsMode === 'none') {
|
|
438
|
-
parts.push('Dependencies: none were provisioned (deps_mode=none)
|
|
442
|
+
parts.push('Dependencies: none were provisioned (deps_mode=none). Do not invoke npx or any network-backed install unless the task explicitly authorizes it; report local validation as blocked when required tools are unavailable.');
|
|
439
443
|
}
|
|
440
444
|
else {
|
|
441
|
-
|
|
445
|
+
const attempted = depsProvisioned === false && depsMode
|
|
446
|
+
? ` Provisioning was attempted with deps_mode=${depsMode} but did not produce a usable node_modules tree.`
|
|
447
|
+
: '';
|
|
448
|
+
parts.push(`Dependencies: no node_modules directory was verified in this worktree.${attempted} Do not invoke npx or any network-backed install unless the task explicitly authorizes it; report local validation as blocked when required tools are unavailable. See .brainclaw-worktree.json symlink_warnings when present.`);
|
|
442
449
|
}
|
|
443
450
|
}
|
|
444
451
|
parts.push('');
|
|
@@ -711,8 +718,8 @@ export function buildContextEnvelopeSection(cwd) {
|
|
|
711
718
|
}
|
|
712
719
|
const newestFirst = (items) => [...items].sort((a, b) => (b.created_at ?? '').localeCompare(a.created_at ?? '') || (a.id ?? '').localeCompare(b.id ?? ''));
|
|
713
720
|
const clip = (text) => text.length > CONTEXT_ENVELOPE_ITEM_MAX_CHARS ? `${text.slice(0, CONTEXT_ENVELOPE_ITEM_MAX_CHARS - 1)}…` : text;
|
|
714
|
-
const constraints = newestFirst(state.active_constraints ?? []);
|
|
715
|
-
const traps = newestFirst(state.known_traps ?? []).slice(0, CONTEXT_ENVELOPE_TOP_K);
|
|
721
|
+
const constraints = newestFirst((state.active_constraints ?? []).filter((item) => item.status === 'active'));
|
|
722
|
+
const traps = newestFirst((state.known_traps ?? []).filter((item) => item.status === 'active')).slice(0, CONTEXT_ENVELOPE_TOP_K);
|
|
716
723
|
const decisions = newestFirst(state.recent_decisions ?? []).slice(0, CONTEXT_ENVELOPE_TOP_K);
|
|
717
724
|
if (constraints.length === 0 && traps.length === 0 && decisions.length === 0)
|
|
718
725
|
return '';
|
|
@@ -777,6 +784,7 @@ export function generateDispatchBrief(options) {
|
|
|
777
784
|
worktreePath: options.worktreePath,
|
|
778
785
|
assignmentId: options.assignmentId,
|
|
779
786
|
attemptFence: options.attemptFence,
|
|
787
|
+
artifactType: options.artifactType,
|
|
780
788
|
}));
|
|
781
789
|
}
|
|
782
790
|
// pln#628 Focus 4A — transport addendum keyed to the ACTUAL missing capability
|
|
@@ -21,7 +21,7 @@ import { addCrossProjectLink, removeCrossProjectLink, resolveCrossProjectLinks,
|
|
|
21
21
|
import { findActiveClaimsForPlan, listClaims, loadClaim, logCascadeReleaseResult, markClaimStale, releaseClaimsCascade, releaseClaimWithCascade, saveClaim, } from './claims.js';
|
|
22
22
|
import { listActionRequired } from './actions.js';
|
|
23
23
|
import { listAgentIdentities } from './agent-registry.js';
|
|
24
|
-
import { getCapabilityProfile, getSpawnableAgents } from './agent-capability.js';
|
|
24
|
+
import { getCapabilityProfile, getSpawnableAgents, validateAgentForDispatch } from './agent-capability.js';
|
|
25
25
|
import { buildReputationSnapshot, toPublicReputationSummary } from './reputation.js';
|
|
26
26
|
import { loadAllSessions } from './identity.js';
|
|
27
27
|
import { loadInstructions } from './instructions.js';
|
|
@@ -40,7 +40,7 @@ import { createPlan, deletePlan, updatePlan, } from './operations/plan.js';
|
|
|
40
40
|
import { ENTITY_NAMES, ENTITY_REGISTRY, isValidTransition, } from './entity-registry.js';
|
|
41
41
|
import { generateId } from './ids.js';
|
|
42
42
|
import { mergeHandoffReview } from './handoff-review.js';
|
|
43
|
-
import { CandidateTypeSchema, ConstraintCategorySchema, DecisionOutcomeSchema, HandoffContractSchema, HandoffReviewSchema, MemoryVisibilitySchema, PlanTypeEnumSchema, PrioritySchema, RuntimeNoteTypeSchema, SequenceStatusSchema, SeveritySchema, } from './schema.js';
|
|
43
|
+
import { CandidateTypeSchema, ConstraintCategorySchema, DecisionOutcomeSchema, HandoffContractSchema, HandoffReviewSchema, MemoryVisibilitySchema, PlanTypeEnumSchema, PrioritySchema, RuntimeNoteTypeSchema, SequenceStatusSchema, SeveritySchema, MemoryVerificationSchema, } from './schema.js';
|
|
44
44
|
/**
|
|
45
45
|
* Default provenance stamp applied on create when the caller does not
|
|
46
46
|
* supply one. `user` kind with whatever author is in the payload; the
|
|
@@ -272,7 +272,14 @@ export function listEntities(name, cwd, filter = {}) {
|
|
|
272
272
|
: fieldFiltered.filter((item) => isLegacyProvenance(item)).length;
|
|
273
273
|
const excludedLowConfidenceAutoReflect = fieldFiltered.filter((item) => isLowConfidenceAutoReflect(item, filter)).length;
|
|
274
274
|
const filtered = fieldFiltered.filter((item) => passesProvenanceFilter(item, filter));
|
|
275
|
-
const
|
|
275
|
+
const newestFirst = [...filtered].sort((a, b) => {
|
|
276
|
+
const left = a;
|
|
277
|
+
const right = b;
|
|
278
|
+
const leftDate = String(left.updated_at ?? left.created_at ?? '');
|
|
279
|
+
const rightDate = String(right.updated_at ?? right.created_at ?? '');
|
|
280
|
+
return rightDate.localeCompare(leftDate) || String(left.id ?? '').localeCompare(String(right.id ?? ''));
|
|
281
|
+
});
|
|
282
|
+
const paged = applyPaging(newestFirst, filter);
|
|
276
283
|
return {
|
|
277
284
|
entity: name,
|
|
278
285
|
total: filtered.length,
|
|
@@ -301,6 +308,16 @@ export function boundListResult(result, offset, charBudget = DEFAULT_FIND_CHAR_B
|
|
|
301
308
|
items = items.slice(0, items.length - drop);
|
|
302
309
|
omittedForSize = result.items.length - items.length;
|
|
303
310
|
}
|
|
311
|
+
let oversizedItemProjected = false;
|
|
312
|
+
if (items.length === 1 && JSON.stringify(items).length > charBudget) {
|
|
313
|
+
const item = items[0];
|
|
314
|
+
if (item && typeof item === 'object') {
|
|
315
|
+
const row = item;
|
|
316
|
+
const compactKeys = ['id', 'short_label', 'status', 'created_at', 'updated_at', 'agent', 'scope', 'plan_id'];
|
|
317
|
+
items = [Object.fromEntries(compactKeys.filter((key) => row[key] !== undefined).map((key) => [key, row[key]]))];
|
|
318
|
+
oversizedItemProjected = true;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
304
321
|
const returned = items.length;
|
|
305
322
|
const hasMore = offset + returned < result.total;
|
|
306
323
|
const bounded = {
|
|
@@ -309,6 +326,7 @@ export function boundListResult(result, offset, charBudget = DEFAULT_FIND_CHAR_B
|
|
|
309
326
|
returned,
|
|
310
327
|
has_more: hasMore,
|
|
311
328
|
...(omittedForSize > 0 ? { omitted_for_size: omittedForSize } : {}),
|
|
329
|
+
...(oversizedItemProjected ? { oversized_item_projected: true } : {}),
|
|
312
330
|
};
|
|
313
331
|
if (hasMore) {
|
|
314
332
|
bounded.next_offset = offset + returned;
|
|
@@ -316,6 +334,9 @@ export function boundListResult(result, offset, charBudget = DEFAULT_FIND_CHAR_B
|
|
|
316
334
|
? `Payload size-bounded: returned ${returned} of ${result.total} ${result.entity} item(s). Fetch more with filter.offset=${bounded.next_offset}, or narrow the filter (status/tag/author).`
|
|
317
335
|
: `Returned ${returned} of ${result.total} ${result.entity} item(s). Page with filter.offset=${bounded.next_offset}, or narrow the filter.`;
|
|
318
336
|
}
|
|
337
|
+
else if (oversizedItemProjected) {
|
|
338
|
+
bounded.hint = 'The matching item exceeded budget_tokens and was projected to identity/status fields. Use bclaw_get for the full item or request explicit bclaw_find fields.';
|
|
339
|
+
}
|
|
319
340
|
return bounded;
|
|
320
341
|
}
|
|
321
342
|
/**
|
|
@@ -388,6 +409,11 @@ function loadAgentsForRead(cwd, filter) {
|
|
|
388
409
|
: undefined;
|
|
389
410
|
const project = (doc) => {
|
|
390
411
|
const row = projectAgentForRead(doc);
|
|
412
|
+
const availability = validateAgentForDispatch(doc.agent_name, { requireSpawnable: true });
|
|
413
|
+
row.declared_spawnable = getCapabilityProfile(doc.agent_name)?.runtime.canBeSpawnedCli ?? false;
|
|
414
|
+
row.executable_now = availability.valid;
|
|
415
|
+
row.availability_code = availability.code;
|
|
416
|
+
row.availability_reason = availability.reason;
|
|
391
417
|
if (reputationById)
|
|
392
418
|
row.reputation = reputationById.get(String(row.id));
|
|
393
419
|
return row;
|
|
@@ -408,7 +434,13 @@ function loadAgentsForRead(cwd, filter) {
|
|
|
408
434
|
existing.dispatchable = true;
|
|
409
435
|
continue;
|
|
410
436
|
}
|
|
411
|
-
|
|
437
|
+
const row = projectCatalogAgentForRead(name);
|
|
438
|
+
const availability = validateAgentForDispatch(name, { requireSpawnable: true });
|
|
439
|
+
row.declared_spawnable = true;
|
|
440
|
+
row.executable_now = availability.valid;
|
|
441
|
+
row.availability_code = availability.code;
|
|
442
|
+
row.availability_reason = availability.reason;
|
|
443
|
+
byName.set(name, row);
|
|
412
444
|
}
|
|
413
445
|
return [...byName.values()];
|
|
414
446
|
}
|
|
@@ -556,6 +588,7 @@ export function createEntity(name, data, cwd) {
|
|
|
556
588
|
planId: data.plan_id,
|
|
557
589
|
}, cwd);
|
|
558
590
|
stampProvenanceOnStateItem('decision', res.id, defaultProvenance(data), cwd);
|
|
591
|
+
stampMemoryVerification('decision', res.id, data, cwd);
|
|
559
592
|
return result({ entity: name, id: res.id, short_label: res.shortLabel });
|
|
560
593
|
}
|
|
561
594
|
case 'constraint': {
|
|
@@ -567,6 +600,7 @@ export function createEntity(name, data, cwd) {
|
|
|
567
600
|
relatedPaths: data.related_paths,
|
|
568
601
|
}, cwd);
|
|
569
602
|
stampProvenanceOnStateItem('constraint', res.id, defaultProvenance(data), cwd);
|
|
603
|
+
stampMemoryVerification('constraint', res.id, data, cwd);
|
|
570
604
|
return result({ entity: name, id: res.id, short_label: res.shortLabel });
|
|
571
605
|
}
|
|
572
606
|
case 'trap': {
|
|
@@ -578,6 +612,7 @@ export function createEntity(name, data, cwd) {
|
|
|
578
612
|
relatedPaths: data.related_paths,
|
|
579
613
|
}, cwd);
|
|
580
614
|
stampProvenanceOnStateItem('trap', res.id, defaultProvenance(data), cwd);
|
|
615
|
+
stampMemoryVerification('trap', res.id, data, cwd);
|
|
581
616
|
return result({ entity: name, id: res.id, short_label: res.shortLabel });
|
|
582
617
|
}
|
|
583
618
|
case 'runtime_note': {
|
|
@@ -1104,6 +1139,29 @@ function stampProvenanceOnStateItem(name, id, provenance, cwd) {
|
|
|
1104
1139
|
item.provenance = provenance;
|
|
1105
1140
|
}, cwd);
|
|
1106
1141
|
}
|
|
1142
|
+
function stampMemoryVerification(name, id, data, cwd) {
|
|
1143
|
+
const verification = data.verification === undefined
|
|
1144
|
+
? undefined
|
|
1145
|
+
: MemoryVerificationSchema.parse(data.verification);
|
|
1146
|
+
const verifiedAt = typeof data.verified_at === 'string' ? data.verified_at : verification?.verified_at;
|
|
1147
|
+
const verifyCmd = typeof data.verify_cmd === 'string' ? data.verify_cmd : undefined;
|
|
1148
|
+
if (!verification && !verifiedAt && !verifyCmd)
|
|
1149
|
+
return;
|
|
1150
|
+
mutateState((state) => {
|
|
1151
|
+
const bucket = name === 'decision' ? state.recent_decisions
|
|
1152
|
+
: name === 'constraint' ? state.active_constraints
|
|
1153
|
+
: state.known_traps;
|
|
1154
|
+
const item = bucket.find((entry) => entry.id === id);
|
|
1155
|
+
if (!item)
|
|
1156
|
+
return;
|
|
1157
|
+
if (verification)
|
|
1158
|
+
item.verification = verification;
|
|
1159
|
+
if (verifiedAt)
|
|
1160
|
+
item.verified_at = verifiedAt;
|
|
1161
|
+
if (verifyCmd)
|
|
1162
|
+
item.verify_cmd = verifyCmd;
|
|
1163
|
+
}, cwd);
|
|
1164
|
+
}
|
|
1107
1165
|
function requireString(data, field) {
|
|
1108
1166
|
const value = data[field];
|
|
1109
1167
|
if (typeof value !== 'string' || !value) {
|
|
@@ -117,7 +117,7 @@ const decision = {
|
|
|
117
117
|
shortLabelPrefix: 'dec',
|
|
118
118
|
schema: DecisionSchema,
|
|
119
119
|
updatable: [
|
|
120
|
-
'text', 'tags', 'outcome', 'scope', 'related_paths', 'verified_at', 'verify_cmd',
|
|
120
|
+
'text', 'tags', 'outcome', 'scope', 'related_paths', 'verified_at', 'verify_cmd', 'verification',
|
|
121
121
|
// pln#544 lifecycle (touched via memory-lifecycle.ts recordMemoryEvent;
|
|
122
122
|
// exposing them here keeps bclaw_update straight-through for tests and
|
|
123
123
|
// operator backfills).
|
|
@@ -140,7 +140,7 @@ const constraint = {
|
|
|
140
140
|
shortLabelPrefix: 'cst',
|
|
141
141
|
schema: ConstraintSchema,
|
|
142
142
|
updatable: [
|
|
143
|
-
'text', 'tags', 'category', 'scope', 'related_paths', 'expires_at',
|
|
143
|
+
'text', 'tags', 'category', 'scope', 'related_paths', 'expires_at', 'verified_at', 'verify_cmd', 'verification',
|
|
144
144
|
// pln#544 lifecycle — see decision.updatable.
|
|
145
145
|
'last_confirmed_at', 'last_infirmed_at',
|
|
146
146
|
'confirmation_count', 'infirmation_count',
|
|
@@ -164,7 +164,7 @@ const trap = {
|
|
|
164
164
|
schema: TrapSchema,
|
|
165
165
|
updatable: [
|
|
166
166
|
'text', 'tags', 'severity', 'scope', 'related_paths', 'expires_at', 'platform_scope',
|
|
167
|
-
'verified_at', 'verify_cmd',
|
|
167
|
+
'verified_at', 'verify_cmd', 'verification',
|
|
168
168
|
// pln#544 lifecycle — see decision.updatable.
|
|
169
169
|
'last_confirmed_at', 'last_infirmed_at',
|
|
170
170
|
'confirmation_count', 'infirmation_count',
|
|
@@ -365,6 +365,13 @@ export class CliExecutionAdapter {
|
|
|
365
365
|
contractBootstrapPath,
|
|
366
366
|
expectedWorkspacePath: options.worktreePath,
|
|
367
367
|
}, isWin32, options.turnEcho);
|
|
368
|
+
// Materialize advertised log paths before spawn. A shell/bootstrap error
|
|
369
|
+
// can otherwise happen before redirection creates either file.
|
|
370
|
+
for (const stream of ['stdout', 'stderr']) {
|
|
371
|
+
const logPath = getRuntimeLogPath(signalRoot, options.assignmentId, stream, runtimeRunId);
|
|
372
|
+
if (!fs.existsSync(logPath))
|
|
373
|
+
fs.writeFileSync(logPath, '', { encoding: 'utf8', mode: 0o600 });
|
|
374
|
+
}
|
|
368
375
|
child = spawn(wrappedCmd, [], {
|
|
369
376
|
detached: !isWin32,
|
|
370
377
|
shell: true,
|
|
@@ -395,6 +402,9 @@ export class CliExecutionAdapter {
|
|
|
395
402
|
child.stdin.write(invoke.promptText);
|
|
396
403
|
child.stdin.end();
|
|
397
404
|
}
|
|
405
|
+
// Preserve fire-and-forget semantics while keeping an exit observer so
|
|
406
|
+
// libuv reaps the direct wrapper child on long-lived POSIX coordinators.
|
|
407
|
+
child.once('exit', () => { });
|
|
398
408
|
child.unref();
|
|
399
409
|
const pid = child.pid;
|
|
400
410
|
if (!pid) {
|
|
@@ -36,6 +36,16 @@ export const CoordinateRequestSchema = z.object({
|
|
|
36
36
|
task: z.string(),
|
|
37
37
|
scope: z.string().optional(),
|
|
38
38
|
targetAgents: z.array(z.string()).optional(),
|
|
39
|
+
/**
|
|
40
|
+
* Ideation critic scheduling. The default is deliberately sequential: the
|
|
41
|
+
* loop still requires every critic artifact, but only the first open critic
|
|
42
|
+
* is dispatched initially and the driver takes the next real turn after
|
|
43
|
+
* harvest. Parallel fan-out remains available as an explicit throughput
|
|
44
|
+
* trade-off.
|
|
45
|
+
*/
|
|
46
|
+
ideation_schedule: z.enum(['sequential', 'parallel']).optional().default('sequential'),
|
|
47
|
+
/** Optional per-slot lenses, positionally aligned with targetAgents. */
|
|
48
|
+
criticPerspectives: z.array(z.string().min(1).max(1000)).optional(),
|
|
39
49
|
constraints: z.record(z.string(), z.unknown()).optional(),
|
|
40
50
|
threadId: z.string().optional(),
|
|
41
51
|
/** Optional pipeline provenance persisted when open_loop creates a loop. */
|
|
@@ -107,7 +107,9 @@ export function closeIdeationLoopFromLaneResult(assignment, lane, actor, cwd) {
|
|
|
107
107
|
// Prefer the typed envelope, but honor the legacy artifacts labels too:
|
|
108
108
|
// coverage_gap used to be silently invisible to a critique gate.
|
|
109
109
|
const reportedArtifactType = lane.artifact_type?.trim()
|
|
110
|
-
?? lane.artifacts
|
|
110
|
+
?? lane.artifacts
|
|
111
|
+
?.map((item) => typeof item === 'string' ? item : item.type)
|
|
112
|
+
.find((label) => /^[a-z][a-z0-9_]*$/.test(label) && label !== expectedArtifactType);
|
|
111
113
|
const body = lane.body?.trim();
|
|
112
114
|
const critique = body || [lane.summary, lane.notes]
|
|
113
115
|
.map((s) => (s ?? '').trim())
|