brainclaw 1.28.2 → 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 +25 -4
- 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 +97 -26
- package/dist/commands/mcp-write-entities.js +5 -2
- package/dist/commands/mcp-write-memory.js +87 -1
- package/dist/commands/mcp.js +41 -9
- package/dist/core/code-map/aggregate.js +20 -7
- package/dist/core/code-map/backend.js +17 -7
- package/dist/core/code-map/cascade-jobs.js +174 -0
- package/dist/core/code-map/cascade-worker.js +15 -0
- package/dist/core/code-map/cascade.js +63 -26
- package/dist/core/code-map/query.js +6 -3
- 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 +80 -8
- package/dist/core/entity-registry.js +3 -3
- package/dist/core/execution-adapters.js +18 -1
- 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/spawn-check.js +9 -1
- package/dist/core/worktree.js +14 -7
- package/dist/facts.js +10 -9
- package/dist/facts.json +9 -8
- package/docs/cli.md +35 -2
- package/docs/code-map.md +20 -9
- package/docs/concepts/ideation-loop.md +35 -14
- package/docs/integrations/mcp.md +15 -5
- package/docs/mcp-schema-changelog.md +72 -6
- package/package.json +1 -1
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
|
|
@@ -15,12 +15,13 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import path from 'node:path';
|
|
17
17
|
import { loadState, mutateState } from './state.js';
|
|
18
|
+
import { detectDuplicates } from './duplicates.js';
|
|
18
19
|
import { archiveCandidate, listCandidates, loadCandidate, saveCandidate, } from './candidates.js';
|
|
19
20
|
import { addCrossProjectLink, removeCrossProjectLink, resolveCrossProjectLinks, } from './cross-project.js';
|
|
20
21
|
import { findActiveClaimsForPlan, listClaims, loadClaim, logCascadeReleaseResult, markClaimStale, releaseClaimsCascade, releaseClaimWithCascade, saveClaim, } from './claims.js';
|
|
21
22
|
import { listActionRequired } from './actions.js';
|
|
22
23
|
import { listAgentIdentities } from './agent-registry.js';
|
|
23
|
-
import { getCapabilityProfile, getSpawnableAgents } from './agent-capability.js';
|
|
24
|
+
import { getCapabilityProfile, getSpawnableAgents, validateAgentForDispatch } from './agent-capability.js';
|
|
24
25
|
import { buildReputationSnapshot, toPublicReputationSummary } from './reputation.js';
|
|
25
26
|
import { loadAllSessions } from './identity.js';
|
|
26
27
|
import { loadInstructions } from './instructions.js';
|
|
@@ -39,7 +40,7 @@ import { createPlan, deletePlan, updatePlan, } from './operations/plan.js';
|
|
|
39
40
|
import { ENTITY_NAMES, ENTITY_REGISTRY, isValidTransition, } from './entity-registry.js';
|
|
40
41
|
import { generateId } from './ids.js';
|
|
41
42
|
import { mergeHandoffReview } from './handoff-review.js';
|
|
42
|
-
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';
|
|
43
44
|
/**
|
|
44
45
|
* Default provenance stamp applied on create when the caller does not
|
|
45
46
|
* supply one. `user` kind with whatever author is in the payload; the
|
|
@@ -271,7 +272,14 @@ export function listEntities(name, cwd, filter = {}) {
|
|
|
271
272
|
: fieldFiltered.filter((item) => isLegacyProvenance(item)).length;
|
|
272
273
|
const excludedLowConfidenceAutoReflect = fieldFiltered.filter((item) => isLowConfidenceAutoReflect(item, filter)).length;
|
|
273
274
|
const filtered = fieldFiltered.filter((item) => passesProvenanceFilter(item, filter));
|
|
274
|
-
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);
|
|
275
283
|
return {
|
|
276
284
|
entity: name,
|
|
277
285
|
total: filtered.length,
|
|
@@ -300,6 +308,16 @@ export function boundListResult(result, offset, charBudget = DEFAULT_FIND_CHAR_B
|
|
|
300
308
|
items = items.slice(0, items.length - drop);
|
|
301
309
|
omittedForSize = result.items.length - items.length;
|
|
302
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
|
+
}
|
|
303
321
|
const returned = items.length;
|
|
304
322
|
const hasMore = offset + returned < result.total;
|
|
305
323
|
const bounded = {
|
|
@@ -308,6 +326,7 @@ export function boundListResult(result, offset, charBudget = DEFAULT_FIND_CHAR_B
|
|
|
308
326
|
returned,
|
|
309
327
|
has_more: hasMore,
|
|
310
328
|
...(omittedForSize > 0 ? { omitted_for_size: omittedForSize } : {}),
|
|
329
|
+
...(oversizedItemProjected ? { oversized_item_projected: true } : {}),
|
|
311
330
|
};
|
|
312
331
|
if (hasMore) {
|
|
313
332
|
bounded.next_offset = offset + returned;
|
|
@@ -315,6 +334,9 @@ export function boundListResult(result, offset, charBudget = DEFAULT_FIND_CHAR_B
|
|
|
315
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).`
|
|
316
335
|
: `Returned ${returned} of ${result.total} ${result.entity} item(s). Page with filter.offset=${bounded.next_offset}, or narrow the filter.`;
|
|
317
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
|
+
}
|
|
318
340
|
return bounded;
|
|
319
341
|
}
|
|
320
342
|
/**
|
|
@@ -387,6 +409,11 @@ function loadAgentsForRead(cwd, filter) {
|
|
|
387
409
|
: undefined;
|
|
388
410
|
const project = (doc) => {
|
|
389
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;
|
|
390
417
|
if (reputationById)
|
|
391
418
|
row.reputation = reputationById.get(String(row.id));
|
|
392
419
|
return row;
|
|
@@ -407,7 +434,13 @@ function loadAgentsForRead(cwd, filter) {
|
|
|
407
434
|
existing.dispatchable = true;
|
|
408
435
|
continue;
|
|
409
436
|
}
|
|
410
|
-
|
|
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);
|
|
411
444
|
}
|
|
412
445
|
return [...byName.values()];
|
|
413
446
|
}
|
|
@@ -511,6 +544,19 @@ export function getEntity(name, idOrShortLabel, cwd) {
|
|
|
511
544
|
// ─── CREATE ────────────────────────────────────────────────────────────
|
|
512
545
|
export function createEntity(name, data, cwd) {
|
|
513
546
|
assertKnownEntity(name, 'create');
|
|
547
|
+
const proximityKinds = new Set(['decision', 'constraint', 'trap']);
|
|
548
|
+
const nearby = proximityKinds.has(name) && typeof data.text === 'string'
|
|
549
|
+
? detectDuplicates(data.text, name, loadState(cwd), listCandidates(undefined, cwd).filter((candidate) => candidate.status === 'pending')).slice(0, 3).map((match) => ({
|
|
550
|
+
id: match.id,
|
|
551
|
+
source: match.source,
|
|
552
|
+
reason: match.reason,
|
|
553
|
+
preview: match.text.length > 160 ? `${match.text.slice(0, 157)}…` : match.text,
|
|
554
|
+
}))
|
|
555
|
+
: [];
|
|
556
|
+
const result = (base) => ({
|
|
557
|
+
...base,
|
|
558
|
+
...(nearby.length ? { nearby_items: nearby } : {}),
|
|
559
|
+
});
|
|
514
560
|
switch (name) {
|
|
515
561
|
case 'plan': {
|
|
516
562
|
// Explicit field whitelist + required-author check brings plan create in line
|
|
@@ -530,7 +576,7 @@ export function createEntity(name, data, cwd) {
|
|
|
530
576
|
estimatedEffort: data.estimated_effort,
|
|
531
577
|
}, cwd);
|
|
532
578
|
stampProvenanceOnStateItem('plan', res.id, defaultProvenance(data), cwd);
|
|
533
|
-
return { entity: name, id: res.id, short_label: res.shortLabel };
|
|
579
|
+
return result({ entity: name, id: res.id, short_label: res.shortLabel });
|
|
534
580
|
}
|
|
535
581
|
case 'decision': {
|
|
536
582
|
const res = createDecision({
|
|
@@ -542,7 +588,8 @@ export function createEntity(name, data, cwd) {
|
|
|
542
588
|
planId: data.plan_id,
|
|
543
589
|
}, cwd);
|
|
544
590
|
stampProvenanceOnStateItem('decision', res.id, defaultProvenance(data), cwd);
|
|
545
|
-
|
|
591
|
+
stampMemoryVerification('decision', res.id, data, cwd);
|
|
592
|
+
return result({ entity: name, id: res.id, short_label: res.shortLabel });
|
|
546
593
|
}
|
|
547
594
|
case 'constraint': {
|
|
548
595
|
const res = createConstraint({
|
|
@@ -553,7 +600,8 @@ export function createEntity(name, data, cwd) {
|
|
|
553
600
|
relatedPaths: data.related_paths,
|
|
554
601
|
}, cwd);
|
|
555
602
|
stampProvenanceOnStateItem('constraint', res.id, defaultProvenance(data), cwd);
|
|
556
|
-
|
|
603
|
+
stampMemoryVerification('constraint', res.id, data, cwd);
|
|
604
|
+
return result({ entity: name, id: res.id, short_label: res.shortLabel });
|
|
557
605
|
}
|
|
558
606
|
case 'trap': {
|
|
559
607
|
const res = createTrap({
|
|
@@ -564,7 +612,8 @@ export function createEntity(name, data, cwd) {
|
|
|
564
612
|
relatedPaths: data.related_paths,
|
|
565
613
|
}, cwd);
|
|
566
614
|
stampProvenanceOnStateItem('trap', res.id, defaultProvenance(data), cwd);
|
|
567
|
-
|
|
615
|
+
stampMemoryVerification('trap', res.id, data, cwd);
|
|
616
|
+
return result({ entity: name, id: res.id, short_label: res.shortLabel });
|
|
568
617
|
}
|
|
569
618
|
case 'runtime_note': {
|
|
570
619
|
const id = generateId('runtime_note');
|
|
@@ -1090,6 +1139,29 @@ function stampProvenanceOnStateItem(name, id, provenance, cwd) {
|
|
|
1090
1139
|
item.provenance = provenance;
|
|
1091
1140
|
}, cwd);
|
|
1092
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
|
+
}
|
|
1093
1165
|
function requireString(data, field) {
|
|
1094
1166
|
const value = data[field];
|
|
1095
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',
|
|
@@ -213,6 +213,12 @@ export function withCodexWorkspaceRoot(invoke, agent, worktreePath, isWin32 = pr
|
|
|
213
213
|
}
|
|
214
214
|
return { ...invoke, args, bashCommand };
|
|
215
215
|
}
|
|
216
|
+
/** Refuse a stdin-delivery invoke that would otherwise inherit `/dev/null`. */
|
|
217
|
+
export function assertPromptDelivery(invoke) {
|
|
218
|
+
if (invoke.promptDelivery === 'stdin_pipe' && !invoke.promptText?.trim()) {
|
|
219
|
+
throw new Error('Invalid stdin_pipe invocation: promptText is empty; refusing to spawn an agent with ignored stdin.');
|
|
220
|
+
}
|
|
221
|
+
}
|
|
216
222
|
export class CliExecutionAdapter {
|
|
217
223
|
id = 'cli';
|
|
218
224
|
canSpawn(agentName) {
|
|
@@ -281,6 +287,7 @@ export class CliExecutionAdapter {
|
|
|
281
287
|
};
|
|
282
288
|
}
|
|
283
289
|
start(invoke, options) {
|
|
290
|
+
assertPromptDelivery(invoke);
|
|
284
291
|
const isWin32 = process.platform === 'win32';
|
|
285
292
|
invoke = withCodexWorkspaceRoot(invoke, options.agent, options.worktreePath, isWin32);
|
|
286
293
|
// F7 (trp_0e5150d3): route worker env through buildWorkerIdentityEnv so the
|
|
@@ -317,7 +324,7 @@ export class CliExecutionAdapter {
|
|
|
317
324
|
}
|
|
318
325
|
const spawnExecutable = resolvedExecutable ?? invoke.executable;
|
|
319
326
|
const useShell = isWin32 && /\.(cmd|bat)$/i.test(spawnExecutable);
|
|
320
|
-
const needsStdin = invoke.promptDelivery === 'stdin_pipe'
|
|
327
|
+
const needsStdin = invoke.promptDelivery === 'stdin_pipe';
|
|
321
328
|
// pln#520 step 4: when we ack-wrap, the SHELL redirects stdout/stderr to the
|
|
322
329
|
// per-assignment log files (fds passed via stdio are NOT inherited through
|
|
323
330
|
// the cmd.exe → .cmd → node shim — the empty-logs bug of can_f792cacd), and
|
|
@@ -358,6 +365,13 @@ export class CliExecutionAdapter {
|
|
|
358
365
|
contractBootstrapPath,
|
|
359
366
|
expectedWorkspacePath: options.worktreePath,
|
|
360
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
|
+
}
|
|
361
375
|
child = spawn(wrappedCmd, [], {
|
|
362
376
|
detached: !isWin32,
|
|
363
377
|
shell: true,
|
|
@@ -388,6 +402,9 @@ export class CliExecutionAdapter {
|
|
|
388
402
|
child.stdin.write(invoke.promptText);
|
|
389
403
|
child.stdin.end();
|
|
390
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', () => { });
|
|
391
408
|
child.unref();
|
|
392
409
|
const pid = child.pid;
|
|
393
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())
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { LANE_RESULT_BODY_MAX_BYTES, LaneResultSchema, } from './schema.js';
|
|
4
|
+
export const LANE_RESULT_FILENAME = 'LANE-RESULT.json';
|
|
5
|
+
const MAX_RESULT_FILE_BYTES = LANE_RESULT_BODY_MAX_BYTES + 16 * 1024;
|
|
6
|
+
function parseLaneResultFile(file) {
|
|
7
|
+
const stat = fs.statSync(file);
|
|
8
|
+
if (!stat.isFile() || stat.size > MAX_RESULT_FILE_BYTES)
|
|
9
|
+
return undefined;
|
|
10
|
+
return LaneResultSchema.parse(JSON.parse(fs.readFileSync(file, 'utf-8')));
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Resolve a worker's terminal result without trusting arbitrary paths.
|
|
14
|
+
*
|
|
15
|
+
* The protocol filename is exact and remains authoritative. When an agent
|
|
16
|
+
* nevertheless renames it, recover only a UNIQUE schema-valid JSON file at the
|
|
17
|
+
* worktree root (never recursively, never through a symlink), optionally bound
|
|
18
|
+
* to the requested assignment id. Ambiguity is surfaced instead of guessed.
|
|
19
|
+
*/
|
|
20
|
+
export function resolveLaneResultFile(worktreePath, assignmentId) {
|
|
21
|
+
const canonicalPath = path.join(worktreePath, LANE_RESULT_FILENAME);
|
|
22
|
+
let foreignCanonical;
|
|
23
|
+
if (fs.existsSync(canonicalPath)) {
|
|
24
|
+
try {
|
|
25
|
+
const lane = parseLaneResultFile(canonicalPath);
|
|
26
|
+
if (!lane) {
|
|
27
|
+
return { kind: 'invalid', path: canonicalPath, error: 'file is not a regular bounded lane-result file' };
|
|
28
|
+
}
|
|
29
|
+
const found = { kind: 'found', path: canonicalPath, lane, canonical: true };
|
|
30
|
+
if (!assignmentId || lane.assignment_id === assignmentId)
|
|
31
|
+
return found;
|
|
32
|
+
foreignCanonical = found;
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
return {
|
|
36
|
+
kind: 'invalid',
|
|
37
|
+
path: canonicalPath,
|
|
38
|
+
error: err instanceof Error ? err.message : String(err),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
let entries;
|
|
43
|
+
try {
|
|
44
|
+
entries = fs.readdirSync(worktreePath, { withFileTypes: true });
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return foreignCanonical ?? { kind: 'absent' };
|
|
48
|
+
}
|
|
49
|
+
const recovered = [];
|
|
50
|
+
for (const entry of entries) {
|
|
51
|
+
if (!entry.isFile() || !entry.name.toLowerCase().endsWith('.json') || entry.name === LANE_RESULT_FILENAME)
|
|
52
|
+
continue;
|
|
53
|
+
const candidatePath = path.join(worktreePath, entry.name);
|
|
54
|
+
try {
|
|
55
|
+
const lane = parseLaneResultFile(candidatePath);
|
|
56
|
+
if (lane && (!assignmentId || lane.assignment_id === assignmentId)) {
|
|
57
|
+
recovered.push({ path: candidatePath, lane });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// Ordinary project JSON and malformed non-canonical files are not lane results.
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (recovered.length === 1) {
|
|
65
|
+
return { kind: 'found', ...recovered[0], canonical: false };
|
|
66
|
+
}
|
|
67
|
+
if (recovered.length > 1) {
|
|
68
|
+
return { kind: 'ambiguous', paths: recovered.map((item) => item.path).sort() };
|
|
69
|
+
}
|
|
70
|
+
return foreignCanonical ?? { kind: 'absent' };
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=lane-result-file.js.map
|
|
@@ -82,6 +82,7 @@ export async function dispatchLoopTurn(input) {
|
|
|
82
82
|
const phaseBrief = buildIdeationBrief({
|
|
83
83
|
thread: loop,
|
|
84
84
|
slotRole: slot.role,
|
|
85
|
+
slotPerspective: slot.perspective,
|
|
85
86
|
memoryProvider: provider,
|
|
86
87
|
seedText: input.task,
|
|
87
88
|
scopeHints: slot.scope_hint ? slot.scope_hint.split(',').map((value) => value.trim()) : [],
|
|
@@ -180,6 +181,7 @@ export async function dispatchLoopTurn(input) {
|
|
|
180
181
|
attempt_epoch: prepared.attempt_epoch,
|
|
181
182
|
workspace_digest: prepared.workspace_digest,
|
|
182
183
|
} : undefined,
|
|
184
|
+
artifactType: policy.expected_artifacts?.[0]?.loop_artifact_type,
|
|
183
185
|
cwd: input.cwd,
|
|
184
186
|
});
|
|
185
187
|
const message = sendMessage({
|
|
@@ -45,7 +45,7 @@ const LOOP_INTERNAL_CATEGORIES = new Set([
|
|
|
45
45
|
'synthesis_artifact',
|
|
46
46
|
]);
|
|
47
47
|
export function buildIdeationBrief(input) {
|
|
48
|
-
const { thread, slotRole, memoryProvider, maxChars = DEFAULT_MAX_CHARS, topKPerCategory = DEFAULT_TOP_K_PER_CATEGORY, seedText, scopeHints = [], } = input;
|
|
48
|
+
const { thread, slotRole, slotPerspective, memoryProvider, maxChars = DEFAULT_MAX_CHARS, topKPerCategory = DEFAULT_TOP_K_PER_CATEGORY, seedText, scopeHints = [], } = input;
|
|
49
49
|
const proposal = findProposalArtifact(thread);
|
|
50
50
|
const proposalText = seedText?.trim() || proposal?.body?.trim() || '(no proposal seed found)';
|
|
51
51
|
// Resolve which memory categories the current phase wants. If the
|
|
@@ -64,13 +64,15 @@ export function buildIdeationBrief(input) {
|
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
// Loop-internal categories: pulled from thread.artifacts directly.
|
|
67
|
-
//
|
|
68
|
-
//
|
|
67
|
+
// Critique history includes contributions already made in the current
|
|
68
|
+
// round. Sequential ideation depends on this: participant B challenges A,
|
|
69
|
+
// then C sees both, instead of producing isolated first impressions.
|
|
70
|
+
// Revision history similarly includes the latest available revision.
|
|
69
71
|
// synthesis_artifact → the most recent synthesis output (if any).
|
|
70
72
|
const priorArtifactsBlock = includesLoopInternal
|
|
71
73
|
? renderPriorArtifactsBlock(thread, requestedCategories)
|
|
72
74
|
: '';
|
|
73
|
-
const header = renderHeader(thread, slotRole, currentPhaseDef?.name ?? thread.current_phase);
|
|
75
|
+
const header = renderHeader(thread, slotRole, currentPhaseDef?.name ?? thread.current_phase, slotPerspective);
|
|
74
76
|
const proposalBlock = renderProposalBlock(proposalText);
|
|
75
77
|
const memoryBlock = renderMemoryBlock(fetchedItemsByCategory);
|
|
76
78
|
const closing = renderClosingInstructions(slotRole, thread.current_phase);
|
|
@@ -114,7 +116,7 @@ function expandUserFacingCategories(requested) {
|
|
|
114
116
|
// Drop loop-internal categories — they're handled separately.
|
|
115
117
|
return requested.filter((c) => !LOOP_INTERNAL_CATEGORIES.has(c) && c !== '*');
|
|
116
118
|
}
|
|
117
|
-
function renderHeader(thread, slotRole, phase) {
|
|
119
|
+
function renderHeader(thread, slotRole, phase, perspective) {
|
|
118
120
|
const lines = [
|
|
119
121
|
`# ${thread.kind}_loop brief`,
|
|
120
122
|
`loop: ${thread.id}`,
|
|
@@ -125,6 +127,8 @@ function renderHeader(thread, slotRole, phase) {
|
|
|
125
127
|
];
|
|
126
128
|
if (thread.goal)
|
|
127
129
|
lines.push(`goal: ${thread.goal}`);
|
|
130
|
+
if (perspective)
|
|
131
|
+
lines.push(`perspective: ${perspective}`);
|
|
128
132
|
return lines.join('\n');
|
|
129
133
|
}
|
|
130
134
|
function normalizeScope(value) {
|
|
@@ -168,9 +172,9 @@ function renderPriorArtifactsBlock(thread, requested) {
|
|
|
168
172
|
const wantsSynthesis = requested.includes('*') || requested.includes('synthesis_artifact');
|
|
169
173
|
const sections = [];
|
|
170
174
|
if (wantsCritique) {
|
|
171
|
-
const priorCritique = thread.artifacts.filter((a) => a.type === 'critique' && (a.iteration ?? 0)
|
|
175
|
+
const priorCritique = thread.artifacts.filter((a) => a.type === 'critique' && (a.iteration ?? 0) <= thread.iteration_count);
|
|
172
176
|
if (priorCritique.length > 0) {
|
|
173
|
-
const lines = ['### critique_history (
|
|
177
|
+
const lines = ['### critique_history (conversation so far)'];
|
|
174
178
|
for (const a of priorCritique) {
|
|
175
179
|
lines.push(`- [${a.artifact_id}] (iter ${a.iteration ?? 0}) ${truncateLine(a.body)}`);
|
|
176
180
|
}
|
|
@@ -178,7 +182,7 @@ function renderPriorArtifactsBlock(thread, requested) {
|
|
|
178
182
|
}
|
|
179
183
|
}
|
|
180
184
|
if (wantsRevision) {
|
|
181
|
-
const priorRevision = thread.artifacts.filter((a) => a.phase === 'revision' && (a.iteration ?? 0)
|
|
185
|
+
const priorRevision = thread.artifacts.filter((a) => a.phase === 'revision' && (a.iteration ?? 0) <= thread.iteration_count);
|
|
182
186
|
if (priorRevision.length > 0) {
|
|
183
187
|
const lines = ['### revision_history (prior iterations)'];
|
|
184
188
|
for (const a of priorRevision) {
|
|
@@ -200,12 +204,16 @@ function renderPriorArtifactsBlock(thread, requested) {
|
|
|
200
204
|
return ['## prior loop artifacts', ...sections].join('\n\n');
|
|
201
205
|
}
|
|
202
206
|
function renderClosingInstructions(slotRole, phase) {
|
|
203
|
-
|
|
207
|
+
const lines = [
|
|
204
208
|
`## what to produce`,
|
|
205
209
|
`- Phase "${phase}" expects you to act in role "${slotRole}".`,
|
|
206
210
|
`- Emit findings as LoopArtifacts via bclaw_loop intent='complete_turn' or 'add_artifact'.`,
|
|
207
|
-
|
|
208
|
-
|
|
211
|
+
];
|
|
212
|
+
if (phase === 'critique') {
|
|
213
|
+
lines.push(`- Treat memory items as investigation leads, never as proof that the current worktree still behaves that way.`, `- Verify every finding about the current implementation against the worktree. Cite at least one concrete file path plus a line, symbol, assertion, or test/command result.`, `- If you cannot verify a memory-backed concern in the worktree, label it as an unverified question instead of reporting it as a finding.`);
|
|
214
|
+
}
|
|
215
|
+
lines.push(`- Cite the memory ids you relied on so the synthesis can audit coverage.`);
|
|
216
|
+
return lines.join('\n');
|
|
209
217
|
}
|
|
210
218
|
function truncateLine(s, maxLen = 200) {
|
|
211
219
|
if (!s)
|