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
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
import { evaluatePhaseAdvanceGate } from './verbs.js';
|
|
2
|
+
/** Participants eligible to speak in the current phase. Ideation reuses the
|
|
3
|
+
* same durable slots across rounds: critics converse during critique, while
|
|
4
|
+
* the champion revises and synthesizes. Other protocols retain their legacy
|
|
5
|
+
* phase binding semantics. */
|
|
6
|
+
function eligiblePhaseSlots(loop) {
|
|
7
|
+
if (loop.kind === 'ideation') {
|
|
8
|
+
if (loop.current_phase === 'critique')
|
|
9
|
+
return loop.slots.filter((slot) => slot.role === 'critic');
|
|
10
|
+
if (loop.current_phase === 'proposal' || loop.current_phase === 'revision' || loop.current_phase === 'synthesis') {
|
|
11
|
+
return loop.slots.filter((slot) => slot.role === 'champion');
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return loop.slots.filter((slot) => (slot.phase ?? loop.current_phase) === loop.current_phase);
|
|
15
|
+
}
|
|
1
16
|
export function computeNextExpected(loop) {
|
|
2
17
|
if (loop.status === 'completed' || loop.status === 'cancelled' || loop.status === 'blocked') {
|
|
3
18
|
return null;
|
|
@@ -18,7 +33,7 @@ export function computeNextExpected(loop) {
|
|
|
18
33
|
if (loop.status === 'paused') {
|
|
19
34
|
return null;
|
|
20
35
|
}
|
|
21
|
-
const currentPhaseSlots =
|
|
36
|
+
const currentPhaseSlots = eligiblePhaseSlots(loop);
|
|
22
37
|
const openSlots = currentPhaseSlots.filter((s) => s.status === 'open');
|
|
23
38
|
if (openSlots.length > 0) {
|
|
24
39
|
const first = openSlots[0];
|
|
@@ -42,6 +57,46 @@ export function computeNextExpected(loop) {
|
|
|
42
57
|
blocking_on: assignedOrWorking.map((s) => s.slot_id),
|
|
43
58
|
};
|
|
44
59
|
}
|
|
60
|
+
// A completed ideation slot is reusable in the next phase/iteration. Pick
|
|
61
|
+
// the first participant that has not yet contributed to THIS round. This is
|
|
62
|
+
// what turns critique A → critique B → critique C → champion revision
|
|
63
|
+
// into an actual conversation instead of replaying slot A forever.
|
|
64
|
+
if (loop.kind === 'ideation') {
|
|
65
|
+
const awaitingRound = currentPhaseSlots.find((slot) => slot.status === 'done'
|
|
66
|
+
&& (slot.last_completed_phase !== loop.current_phase
|
|
67
|
+
|| slot.last_completed_iteration !== loop.iteration_count));
|
|
68
|
+
if (awaitingRound) {
|
|
69
|
+
return {
|
|
70
|
+
action: 'turn',
|
|
71
|
+
intent: 'bclaw_loop.turn',
|
|
72
|
+
reason: 'next sequential participant in the current ideation round',
|
|
73
|
+
phase: loop.current_phase,
|
|
74
|
+
slot_id: awaitingRound.slot_id,
|
|
75
|
+
role: awaitingRound.role,
|
|
76
|
+
blocking_on: [awaitingRound.slot_id],
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const currentPhase = loop.phases.find((phase) => phase.name === loop.current_phase);
|
|
81
|
+
const gate = evaluatePhaseAdvanceGate(loop, currentPhase?.advance_gate);
|
|
82
|
+
if (!gate.advance) {
|
|
83
|
+
// A failed/malformed worker result must lead back to a real evidence-bearing
|
|
84
|
+
// turn. Under strict evidence policy, add_artifact is audit-only and cannot
|
|
85
|
+
// satisfy the gate, so never suggest the champion/advance path here.
|
|
86
|
+
const replayable = currentPhaseSlots.filter((slot) => slot.status === 'failed');
|
|
87
|
+
const target = replayable[0] ?? currentPhaseSlots[0];
|
|
88
|
+
if (target) {
|
|
89
|
+
return {
|
|
90
|
+
action: 'turn',
|
|
91
|
+
intent: 'bclaw_loop.turn',
|
|
92
|
+
reason: `phase_gate_unmet: ${gate.gate_reason ?? 'required evidence is missing'}; replay a real slot turn (manual add_artifact does not count under strict evidence)`,
|
|
93
|
+
phase: loop.current_phase,
|
|
94
|
+
slot_id: target.slot_id,
|
|
95
|
+
role: target.role,
|
|
96
|
+
blocking_on: replayable.length > 0 ? replayable.map((slot) => slot.slot_id) : [target.slot_id],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
45
100
|
const phaseNames = loop.phases.map((p) => p.name);
|
|
46
101
|
const currentIndex = phaseNames.indexOf(loop.current_phase);
|
|
47
102
|
if (currentIndex >= 0 && currentIndex + 1 < phaseNames.length) {
|
|
@@ -506,6 +506,14 @@ function convergeLockedTurn(reservation, input, actor, cwd) {
|
|
|
506
506
|
// ── §6 reducer: validated result → loop artifacts + slot outcome. ──
|
|
507
507
|
const reducerInput = { lane, phase: reservation.phase, critiques: input.critiques };
|
|
508
508
|
const reduced = reducerForKind(loop.kind)(reducerInput, reservation);
|
|
509
|
+
if (reduced.slot_outcome === 'retryable') {
|
|
510
|
+
return {
|
|
511
|
+
reconciled: false,
|
|
512
|
+
reason: `repairable worker result: ${reduced.failure_reason ?? 'result does not satisfy the phase artifact contract'}; slot remains assigned and may be re-harvested or replayed`,
|
|
513
|
+
artifacts_added: 0,
|
|
514
|
+
loop_status: loop.status,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
509
517
|
artifacts_added = reduced.artifacts.length;
|
|
510
518
|
slot_outcome = reduced.slot_outcome;
|
|
511
519
|
// Record artifacts + complete the turn (crash-atomic WAL via complete_turn).
|
|
@@ -28,11 +28,11 @@ export const reviewReducer = (input, attempt) => {
|
|
|
28
28
|
}
|
|
29
29
|
if (phase === 'author_response') {
|
|
30
30
|
if (lane.artifact_type !== 'author_response') {
|
|
31
|
-
return { artifacts: [], slot_outcome: '
|
|
31
|
+
return { artifacts: [], slot_outcome: 'retryable', failure_reason: "review author_response requires artifact_type 'author_response'" };
|
|
32
32
|
}
|
|
33
33
|
const response = (lane.body ?? '').trim();
|
|
34
34
|
if (!response) {
|
|
35
|
-
return { artifacts: [], slot_outcome: '
|
|
35
|
+
return { artifacts: [], slot_outcome: 'retryable', failure_reason: 'review author_response produced no body' };
|
|
36
36
|
}
|
|
37
37
|
return {
|
|
38
38
|
artifacts: [{ phase, type: 'author_response', body: capLoopArtifactBody(response), produced_by: attempt.agent }],
|
|
@@ -43,7 +43,7 @@ export const reviewReducer = (input, attempt) => {
|
|
|
43
43
|
return { artifacts: [], slot_outcome: 'failed', failure_reason: `review phase '${phase}' has no worker-result contract` };
|
|
44
44
|
}
|
|
45
45
|
if (!lane.review_verdict) {
|
|
46
|
-
return { artifacts: [], slot_outcome: '
|
|
46
|
+
return { artifacts: [], slot_outcome: 'retryable', failure_reason: 'review lane completed without a review_verdict — cannot converge the loop' };
|
|
47
47
|
}
|
|
48
48
|
const summary = (lane.review_summary ?? '').trim();
|
|
49
49
|
const body = capLoopArtifactBody(lane.review_verdict === 'approve'
|
|
@@ -71,24 +71,26 @@ export const ideationReducer = (input, attempt) => {
|
|
|
71
71
|
return { artifacts: [], slot_outcome: 'failed', failure_reason: `ideation phase '${phase}' has no result contract` };
|
|
72
72
|
}
|
|
73
73
|
if (lane.artifact_type !== artifactType) {
|
|
74
|
-
return { artifacts: [], slot_outcome: '
|
|
74
|
+
return { artifacts: [], slot_outcome: 'retryable', failure_reason: `ideation phase '${phase}' expected artifact_type '${artifactType}', got '${lane.artifact_type}'` };
|
|
75
75
|
}
|
|
76
76
|
const body = (lane.body ?? lane.summary).trim();
|
|
77
77
|
if (!body)
|
|
78
|
-
return { artifacts: [], slot_outcome: '
|
|
78
|
+
return { artifacts: [], slot_outcome: 'retryable', failure_reason: `ideation ${phase} produced no body` };
|
|
79
79
|
if (artifactType === 'plan_draft') {
|
|
80
80
|
const addresses = [
|
|
81
|
-
...(lane.artifacts ?? [])
|
|
81
|
+
...(lane.artifacts ?? [])
|
|
82
|
+
.map((item) => typeof item === 'string' ? item : item.ref)
|
|
83
|
+
.filter((id) => /^art_[0-9a-z]+$/.test(id)),
|
|
82
84
|
...(critiques ?? []).flatMap((c) => c.addresses_critique ?? []),
|
|
83
85
|
];
|
|
84
86
|
const uniqueAddresses = [...new Set(addresses)];
|
|
85
87
|
if (uniqueAddresses.length === 0) {
|
|
86
|
-
return { artifacts: [], slot_outcome: '
|
|
88
|
+
return { artifacts: [], slot_outcome: 'retryable', failure_reason: 'ideation synthesis must cite critique artifact ids in lane.artifacts' };
|
|
87
89
|
}
|
|
88
90
|
if (!lane.implementation_verify) {
|
|
89
91
|
return {
|
|
90
92
|
artifacts: [],
|
|
91
|
-
slot_outcome: '
|
|
93
|
+
slot_outcome: 'retryable',
|
|
92
94
|
failure_reason: 'ideation synthesis must declare implementation_verify for deterministic downstream verification',
|
|
93
95
|
};
|
|
94
96
|
}
|
|
@@ -107,10 +109,10 @@ export const ideationReducer = (input, attempt) => {
|
|
|
107
109
|
return { artifacts: [{ phase, type: artifactType, body: capLoopArtifactBody(body), produced_by: attempt.agent }], slot_outcome: 'done' };
|
|
108
110
|
}
|
|
109
111
|
if (lane.artifact_type !== 'critique') {
|
|
110
|
-
return { artifacts: [], slot_outcome: '
|
|
112
|
+
return { artifacts: [], slot_outcome: 'retryable', failure_reason: "ideation critique requires artifact_type 'critique'" };
|
|
111
113
|
}
|
|
112
114
|
if (!critiques || critiques.length === 0) {
|
|
113
|
-
return { artifacts: [], slot_outcome: '
|
|
115
|
+
return { artifacts: [], slot_outcome: 'retryable', failure_reason: 'ideation critique lane produced no critiques (bare summary) — correct the lane result or replay this slot; gate stays shut' };
|
|
114
116
|
}
|
|
115
117
|
return {
|
|
116
118
|
artifacts: critiques.map((c) => ({
|
|
@@ -148,7 +150,7 @@ function typedPhaseReducer(kind, artifactByPhase) {
|
|
|
148
150
|
if (lane.artifact_type !== expectedType) {
|
|
149
151
|
return {
|
|
150
152
|
artifacts: [],
|
|
151
|
-
slot_outcome: '
|
|
153
|
+
slot_outcome: 'retryable',
|
|
152
154
|
failure_reason: `${kind} phase '${phase}' expected artifact_type '${expectedType}', got '${lane.artifact_type}'`,
|
|
153
155
|
};
|
|
154
156
|
}
|
|
@@ -156,7 +158,7 @@ function typedPhaseReducer(kind, artifactByPhase) {
|
|
|
156
158
|
// summary can never masquerade as a gate-driving repro or verify report.
|
|
157
159
|
const body = (lane.body ?? lane.summary).trim();
|
|
158
160
|
if (!body) {
|
|
159
|
-
return { artifacts: [], slot_outcome: '
|
|
161
|
+
return { artifacts: [], slot_outcome: 'retryable', failure_reason: `${kind} phase '${phase}' produced no artifact body` };
|
|
160
162
|
}
|
|
161
163
|
return {
|
|
162
164
|
artifacts: [{ phase, type: expectedType, body: capLoopArtifactBody(body), produced_by: attempt.agent }],
|
package/dist/core/loops/store.js
CHANGED
|
@@ -68,6 +68,7 @@ function buildSlot(partial) {
|
|
|
68
68
|
role: partial.role,
|
|
69
69
|
agent: partial.agent,
|
|
70
70
|
agent_id: partial.agent_id,
|
|
71
|
+
perspective: partial.perspective,
|
|
71
72
|
assignment_id: partial.assignment_id,
|
|
72
73
|
claim_id: partial.claim_id,
|
|
73
74
|
phase: partial.phase,
|
|
@@ -76,6 +77,9 @@ function buildSlot(partial) {
|
|
|
76
77
|
plan_ids: partial.plan_ids,
|
|
77
78
|
step_ids: partial.step_ids,
|
|
78
79
|
status: partial.status ?? 'open',
|
|
80
|
+
current_turn_id: partial.current_turn_id,
|
|
81
|
+
last_completed_phase: partial.last_completed_phase,
|
|
82
|
+
last_completed_iteration: partial.last_completed_iteration,
|
|
79
83
|
};
|
|
80
84
|
}
|
|
81
85
|
export function appendEvent(loopId, event, cwd,
|
package/dist/core/loops/types.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
export const LOOP_ARTIFACT_BODY_MAX_BYTES = 4096;
|
|
3
|
+
/** Ideation proposals are caller-authored task contracts, not worker summaries. */
|
|
4
|
+
export const LOOP_PROPOSAL_BODY_MAX_BYTES = 32 * 1024;
|
|
3
5
|
export const LOOP_KINDS = ['review', 'ideation', 'implementation', 'research', 'debug'];
|
|
4
6
|
export const LOOP_STATUSES = ['open', 'paused', 'completed', 'blocked', 'cancelled'];
|
|
5
7
|
export const REVIEW_MODES = ['asymmetric', 'symmetric'];
|
|
@@ -130,6 +132,8 @@ export const LoopVerifyConfigSchema = z.object({
|
|
|
130
132
|
});
|
|
131
133
|
export const LoopProtocolConfigSchema = z.object({
|
|
132
134
|
review_mode: z.enum(REVIEW_MODES).optional(),
|
|
135
|
+
/** Whether ideation participants take ordered turns or fan out together. */
|
|
136
|
+
ideation_schedule: z.enum(['sequential', 'parallel']).optional(),
|
|
133
137
|
iteration: LoopIterationSchema.optional(),
|
|
134
138
|
/** pln#632 — engine-run verify command (opener-provided; makes command_green real). */
|
|
135
139
|
verify: LoopVerifyConfigSchema.optional(),
|
|
@@ -160,6 +164,8 @@ export const LoopSlotSchema = z.object({
|
|
|
160
164
|
role: z.string().min(1),
|
|
161
165
|
agent: z.string().optional(),
|
|
162
166
|
agent_id: z.string().optional(),
|
|
167
|
+
/** Stable point of view/instruction for this participant across rounds. */
|
|
168
|
+
perspective: z.string().min(1).max(1000).optional(),
|
|
163
169
|
assignment_id: z.string().optional(),
|
|
164
170
|
claim_id: z.string().optional(),
|
|
165
171
|
phase: z.string().optional(),
|
|
@@ -178,6 +184,9 @@ export const LoopSlotSchema = z.object({
|
|
|
178
184
|
* reusable slot. Additive; wired onto the dispatch path in a later PR.
|
|
179
185
|
*/
|
|
180
186
|
current_turn_id: z.string().optional(),
|
|
187
|
+
/** Last successful contribution, used to rotate reusable slots each round. */
|
|
188
|
+
last_completed_phase: z.string().optional(),
|
|
189
|
+
last_completed_iteration: z.number().int().nonnegative().optional(),
|
|
181
190
|
});
|
|
182
191
|
// ───────────────────────────────────────────────────────────────────────
|
|
183
192
|
// pln#508 step 1 — bootstrap loop foundation: operator-interaction schemas
|
|
@@ -495,10 +504,13 @@ export const LoopArtifactSchema = z
|
|
|
495
504
|
evidence: EvidenceEnvelopeSchema.optional(),
|
|
496
505
|
})
|
|
497
506
|
.superRefine((artifact, ctx) => {
|
|
498
|
-
|
|
507
|
+
const bodyLimit = artifact.type === 'proposal'
|
|
508
|
+
? LOOP_PROPOSAL_BODY_MAX_BYTES
|
|
509
|
+
: LOOP_ARTIFACT_BODY_MAX_BYTES;
|
|
510
|
+
if (artifact.body !== undefined && Buffer.byteLength(artifact.body, 'utf8') > bodyLimit) {
|
|
499
511
|
ctx.addIssue({
|
|
500
512
|
code: z.ZodIssueCode.custom,
|
|
501
|
-
message: `LoopArtifact.body must be ≤ ${
|
|
513
|
+
message: `LoopArtifact.body must be ≤ ${bodyLimit} bytes; use a ref for larger content`,
|
|
502
514
|
path: ['body'],
|
|
503
515
|
});
|
|
504
516
|
}
|
package/dist/core/loops/verbs.js
CHANGED
|
@@ -662,7 +662,14 @@ function completeTurnCommit(input, cwd) {
|
|
|
662
662
|
// Map outcome → terminal slot.status so observers reading the thread can
|
|
663
663
|
// distinguish done/failed/cancelled without replaying the event journal.
|
|
664
664
|
const terminalStatus = outcome;
|
|
665
|
-
const updatedSlots = current.slots.map((s) => s.slot_id === slot.slot_id ? {
|
|
665
|
+
const updatedSlots = current.slots.map((s) => s.slot_id === slot.slot_id ? {
|
|
666
|
+
...s,
|
|
667
|
+
status: terminalStatus,
|
|
668
|
+
...(terminalStatus === 'done' ? {
|
|
669
|
+
last_completed_phase: slot.phase ?? current.current_phase,
|
|
670
|
+
last_completed_iteration: current.iteration_count,
|
|
671
|
+
} : {}),
|
|
672
|
+
} : s);
|
|
666
673
|
// A negative convergence signal is only valid after the critic window has
|
|
667
674
|
// causally closed. The last trusted critic completion records that boundary;
|
|
668
675
|
// mere absence of critique artifacts while a turn is open is never enough.
|
|
@@ -81,7 +81,7 @@ export function renderWorkerReplyProse(contract) {
|
|
|
81
81
|
`- MCP path: call \`${action.tool}\` with ${JSON.stringify(action.args)}`,
|
|
82
82
|
`- The body must be NON-EMPTY: an artifact without usable content does not count toward the gate.`,
|
|
83
83
|
`- Body cap: ${contract.body_max_bytes} bytes. If your output is larger, write the full version to a markdown file in your worktree and put a dense summary plus the file path in the body.`,
|
|
84
|
-
`- File fallback (no MCP):
|
|
84
|
+
`- File fallback (no MCP): write the worktree-root filename exactly as \`LANE-RESULT.json\` (uppercase, hyphen, no suffix or subdirectory); set "artifact_type":"${contract.requirements[0].type}" and put your full output in "body" — the harvester records it under this contract.`,
|
|
85
85
|
`- This contract is FROZEN for loop version ${contract.loop_version}, phase "${contract.phase}". If your submit reports a version conflict or the loop has advanced, your work is still recorded under phase "${contract.phase}" — do not re-target a newer phase.`,
|
|
86
86
|
];
|
|
87
87
|
if (contract.other_conditions.length > 0) {
|
|
@@ -402,6 +402,7 @@ export async function dispatchReviewLoopTurn(input) {
|
|
|
402
402
|
attempt_epoch: turnEcho.attempt_epoch,
|
|
403
403
|
workspace_digest: turnEcho.workspace_digest,
|
|
404
404
|
} : undefined,
|
|
405
|
+
artifactType: phase === 'author_response' ? 'author_response' : 'verdict',
|
|
405
406
|
cwd, // pln#638 PR-6b — the context envelope reads the store
|
|
406
407
|
});
|
|
407
408
|
const msg = sendMessage({
|
package/dist/core/schema.js
CHANGED
|
@@ -144,6 +144,16 @@ export const ProvenanceSchema = z.discriminatedUnion('kind', [
|
|
|
144
144
|
* entity-operations.ts which enforces the typed shape.
|
|
145
145
|
*/
|
|
146
146
|
export const ProvenancePassthroughSchema = z.unknown().optional();
|
|
147
|
+
/** Replayable empirical assertion attached to a perishable memory claim. */
|
|
148
|
+
export const MemoryVerificationSchema = z.object({
|
|
149
|
+
kind: z.enum(['command', 'query']),
|
|
150
|
+
input: z.union([z.string().min(1), z.array(z.string().min(1)).min(1)]),
|
|
151
|
+
expected: z.string().min(1),
|
|
152
|
+
observed: z.string().optional(),
|
|
153
|
+
verified_at: z.string().datetime(),
|
|
154
|
+
outcome: z.enum(['pass', 'fail']),
|
|
155
|
+
max_age_days: z.number().positive().optional(),
|
|
156
|
+
});
|
|
147
157
|
export const ConstraintSchema = z.object({
|
|
148
158
|
schema_version: z.number().int().positive().optional(),
|
|
149
159
|
id: z.string(),
|
|
@@ -163,6 +173,8 @@ export const ConstraintSchema = z.object({
|
|
|
163
173
|
related_paths: z.array(z.string()).optional(),
|
|
164
174
|
plan_id: z.string().optional(),
|
|
165
175
|
expires_at: z.string().optional(),
|
|
176
|
+
verified_at: z.string().optional(),
|
|
177
|
+
verify_cmd: z.string().optional(),
|
|
166
178
|
// pln#544 — memory-lifecycle (confirm/decay/reinforce). Symmetric across
|
|
167
179
|
// constraint/decision/trap. `verified_at` (pln#530 perishable-fact
|
|
168
180
|
// re-verification) is kept as a narrower legacy signal alongside.
|
|
@@ -175,6 +187,7 @@ export const ConstraintSchema = z.object({
|
|
|
175
187
|
/** Bounded event log (most recent N) — older events are dropped, the
|
|
176
188
|
* counts remain accurate. Empty / absent means "never confirmed". */
|
|
177
189
|
confirmations: z.array(MemoryConfirmationEventSchema).optional(),
|
|
190
|
+
verification: MemoryVerificationSchema.optional(),
|
|
178
191
|
provenance: ProvenancePassthroughSchema,
|
|
179
192
|
});
|
|
180
193
|
export const DecisionSchema = z.object({
|
|
@@ -207,6 +220,7 @@ export const DecisionSchema = z.object({
|
|
|
207
220
|
saved_me_count: z.number().int().nonnegative().optional(),
|
|
208
221
|
misled_me_count: z.number().int().nonnegative().optional(),
|
|
209
222
|
confirmations: z.array(MemoryConfirmationEventSchema).optional(),
|
|
223
|
+
verification: MemoryVerificationSchema.optional(),
|
|
210
224
|
provenance: ProvenancePassthroughSchema,
|
|
211
225
|
});
|
|
212
226
|
export const TrapSchema = z.object({
|
|
@@ -243,6 +257,7 @@ export const TrapSchema = z.object({
|
|
|
243
257
|
saved_me_count: z.number().int().nonnegative().optional(),
|
|
244
258
|
misled_me_count: z.number().int().nonnegative().optional(),
|
|
245
259
|
confirmations: z.array(MemoryConfirmationEventSchema).optional(),
|
|
260
|
+
verification: MemoryVerificationSchema.optional(),
|
|
246
261
|
provenance: ProvenancePassthroughSchema,
|
|
247
262
|
});
|
|
248
263
|
export const HandoffContractSchema = z.object({
|
|
@@ -1068,6 +1083,14 @@ export const RuntimeEventTypeSchema = z.enum([
|
|
|
1068
1083
|
* durable runtime event before a loop closer applies its smaller display cap.
|
|
1069
1084
|
*/
|
|
1070
1085
|
export const LANE_RESULT_BODY_MAX_BYTES = 64 * 1024;
|
|
1086
|
+
export const LaneResultArtifactSchema = z.union([
|
|
1087
|
+
z.string().min(1),
|
|
1088
|
+
z.object({
|
|
1089
|
+
type: z.string().min(1),
|
|
1090
|
+
ref: z.string().min(1),
|
|
1091
|
+
description: z.string().optional(),
|
|
1092
|
+
}),
|
|
1093
|
+
]);
|
|
1071
1094
|
export const LaneResultSchema = z.object({
|
|
1072
1095
|
assignment_id: z.string(),
|
|
1073
1096
|
/**
|
|
@@ -1089,7 +1112,7 @@ export const LaneResultSchema = z.object({
|
|
|
1089
1112
|
status: z.enum(['completed', 'blocked', 'failed']),
|
|
1090
1113
|
summary: z.string(),
|
|
1091
1114
|
/** Paths or refs the worker produced (commits, files, docs). */
|
|
1092
|
-
artifacts: z.array(
|
|
1115
|
+
artifacts: z.array(LaneResultArtifactSchema).optional(),
|
|
1093
1116
|
/** Files the worker changed in the worktree. */
|
|
1094
1117
|
files_changed: z.array(z.string()).optional(),
|
|
1095
1118
|
/** Free-form notes (blockers, follow-ups). */
|
package/dist/core/search.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { loadState } from './state.js';
|
|
2
2
|
import { listCandidates } from './candidates.js';
|
|
3
3
|
import { listSequences } from './sequence.js';
|
|
4
|
+
import { isTrapActive } from './traps.js';
|
|
4
5
|
const K1 = 1.5;
|
|
5
6
|
const B = 0.75;
|
|
6
7
|
function tokenize(text) {
|
|
@@ -23,11 +24,11 @@ function buildCorpus(state, includePending, cwd, includeLegacy = false) {
|
|
|
23
24
|
const textParts = [item.text, item.author ?? '', ...(item.tags ?? []), ...(item.related_paths ?? [])];
|
|
24
25
|
docs.push({ ...item, section, terms: tokenize(textParts.join(' ')) });
|
|
25
26
|
};
|
|
26
|
-
for (const c of state.active_constraints)
|
|
27
|
+
for (const c of state.active_constraints.filter((item) => item.status === 'active'))
|
|
27
28
|
add('constraints', c);
|
|
28
29
|
for (const d of state.recent_decisions)
|
|
29
30
|
add('decisions', d);
|
|
30
|
-
for (const t of state.known_traps)
|
|
31
|
+
for (const t of state.known_traps.filter((item) => isTrapActive(item)))
|
|
31
32
|
add('traps', { ...t, text: t.text });
|
|
32
33
|
for (const h of state.open_handoffs)
|
|
33
34
|
add('handoffs', { ...h, text: `${h.from} -> ${h.to}: ${h.text}` });
|
package/dist/core/spawn-check.js
CHANGED
|
@@ -96,7 +96,15 @@ export async function checkAgentSpawn(agent, options = {}) {
|
|
|
96
96
|
const stderrRaw = readLogTail(root, assignmentId, 'stderr', 800).trim();
|
|
97
97
|
const stderrTail = stderrRaw ? stderrRaw.split(/\r?\n/).filter(Boolean) : undefined;
|
|
98
98
|
if (completed) {
|
|
99
|
-
return {
|
|
99
|
+
return {
|
|
100
|
+
agent,
|
|
101
|
+
binary,
|
|
102
|
+
status: 'ok',
|
|
103
|
+
delivered,
|
|
104
|
+
completed: true,
|
|
105
|
+
duration_ms,
|
|
106
|
+
detail: `validation probe: ack + completed round-trip (${invoke.promptDelivery}, ${invoke.promptText?.length ?? 0} prompt bytes)`,
|
|
107
|
+
};
|
|
100
108
|
}
|
|
101
109
|
if (failed) {
|
|
102
110
|
const tail = stderrRaw || readLogTail(root, assignmentId, 'stdout', 400).trim();
|
package/dist/core/worktree.js
CHANGED
|
@@ -968,11 +968,9 @@ export function createWorktree(mainWorktreePath, branchName, options = {}) {
|
|
|
968
968
|
// the dispatch brief can tell the worker the truth. A failed install/copy is
|
|
969
969
|
// best-effort (non-fatal) but the worker must then install itself — the brief
|
|
970
970
|
// must NOT claim "node_modules is real, do not reinstall" over a failure.
|
|
971
|
-
let depsProvisioned;
|
|
972
971
|
if (provisionDeps) {
|
|
973
972
|
const provisionWarnings = provisionWorktreeDeps(depsMode, mainWorktreePath, targetPath, nodeModulesPaths);
|
|
974
973
|
symlinkWarnings.push(...provisionWarnings);
|
|
975
|
-
depsProvisioned = provisionWarnings.length === 0;
|
|
976
974
|
}
|
|
977
975
|
else if (depsMode === 'link') {
|
|
978
976
|
// trp_37b05a15 (field report, Next.js 16 / Turbopack) — the node_modules link
|
|
@@ -992,6 +990,15 @@ export function createWorktree(mainWorktreePath, branchName, options = {}) {
|
|
|
992
990
|
logger.warn(`[worktree] ${msg}`);
|
|
993
991
|
}
|
|
994
992
|
}
|
|
993
|
+
// Record observed availability, not only the requested provisioning mode.
|
|
994
|
+
// This is deliberately evaluated after link/install/copy so dispatch briefs
|
|
995
|
+
// cannot claim a main-repo dependency tree that never existed.
|
|
996
|
+
const depsPaths = [...new Set([
|
|
997
|
+
...requested.filter(isNodeModulesPath),
|
|
998
|
+
...detectWorkspaceNodeModules(targetPath),
|
|
999
|
+
])]
|
|
1000
|
+
.filter((relativePath) => fs.existsSync(path.join(targetPath, relativePath)));
|
|
1001
|
+
const depsProvisioned = depsMode === 'none' ? false : depsPaths.length > 0;
|
|
995
1002
|
// NOTE: .brainclaw/ is intentionally NOT symlinked.
|
|
996
1003
|
// Symlinking .brainclaw/ causes hooks and session_start to trigger on the
|
|
997
1004
|
// shared store, creating session conflicts and potentially blocking agents
|
|
@@ -1033,11 +1040,11 @@ export function createWorktree(mainWorktreePath, branchName, options = {}) {
|
|
|
1033
1040
|
// trp_37b05a15: how JS deps were provisioned (link junction / real install /
|
|
1034
1041
|
// copy / none) — non-default modes are recorded so a worker/supervisor knows
|
|
1035
1042
|
// whether node_modules is an out-of-root link (dev-server caveat) or in-root.
|
|
1036
|
-
// `deps_provisioned`
|
|
1037
|
-
//
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1043
|
+
// `deps_provisioned` and `deps_paths` record observed availability. They are
|
|
1044
|
+
// emitted for link mode too: requested capacity is not executable capacity.
|
|
1045
|
+
deps_mode: depsMode,
|
|
1046
|
+
deps_provisioned: depsProvisioned,
|
|
1047
|
+
deps_paths: depsPaths,
|
|
1041
1048
|
// pln#523: surface any shared-path link failures (e.g. node_modules junction
|
|
1042
1049
|
// that could not be created) so the worker / supervisor can see why a build
|
|
1043
1050
|
// might fail, instead of an invisible degradation.
|
package/dist/facts.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// Generated by scripts/emit-site-facts.mjs at build time. Do not edit manually.
|
|
2
|
-
// Source: brainclaw v1.28.
|
|
2
|
+
// Source: brainclaw v1.28.4 on 2026-08-28T07:25:30.486Z
|
|
3
3
|
export const FACTS = {
|
|
4
|
-
"version": "1.28.
|
|
5
|
-
"generated_at": "2026-08-
|
|
4
|
+
"version": "1.28.4",
|
|
5
|
+
"generated_at": "2026-08-28T07:25:30.486Z",
|
|
6
6
|
"tools": {
|
|
7
|
-
"count":
|
|
8
|
-
"published_count":
|
|
7
|
+
"count": 71,
|
|
8
|
+
"published_count": 69,
|
|
9
9
|
"names": [
|
|
10
10
|
"bclaw_bootstrap",
|
|
11
11
|
"bclaw_release_notes",
|
|
@@ -70,6 +70,7 @@ export const FACTS = {
|
|
|
70
70
|
"bclaw_assignment_update",
|
|
71
71
|
"bclaw_assignment_action",
|
|
72
72
|
"bclaw_harvest_candidates",
|
|
73
|
+
"bclaw_harvest",
|
|
73
74
|
"bclaw_find",
|
|
74
75
|
"bclaw_get",
|
|
75
76
|
"bclaw_create",
|
|
@@ -478,7 +479,7 @@ export const FACTS = {
|
|
|
478
479
|
},
|
|
479
480
|
"bench": {
|
|
480
481
|
"schema": "brainclaw.bench.v1",
|
|
481
|
-
"generated_at": "2026-08-
|
|
482
|
+
"generated_at": "2026-08-28T07:25:28.367Z",
|
|
482
483
|
"node_version": "v24.19.0",
|
|
483
484
|
"platform": "linux-x64",
|
|
484
485
|
"repeats": 3,
|
|
@@ -487,7 +488,7 @@ export const FACTS = {
|
|
|
487
488
|
"name": "cold_onboard",
|
|
488
489
|
"volume": "empty",
|
|
489
490
|
"description": "fresh machine → init → first useful context. Baseline for time-to-first-value.",
|
|
490
|
-
"duration_ms_median":
|
|
491
|
+
"duration_ms_median": 80,
|
|
491
492
|
"payload_chars_median": 1640,
|
|
492
493
|
"payload_tokens_est_median": 410
|
|
493
494
|
},
|
|
@@ -495,7 +496,7 @@ export const FACTS = {
|
|
|
495
496
|
"name": "warm_work",
|
|
496
497
|
"volume": "medium",
|
|
497
498
|
"description": "bclaw_work consult over a real-shaped store (~200 plans / 500 handoffs / 450 claims).",
|
|
498
|
-
"duration_ms_median":
|
|
499
|
+
"duration_ms_median": 124,
|
|
499
500
|
"payload_chars_median": 2626,
|
|
500
501
|
"payload_tokens_est_median": 657
|
|
501
502
|
},
|
|
@@ -503,7 +504,7 @@ export const FACTS = {
|
|
|
503
504
|
"name": "first_edit",
|
|
504
505
|
"volume": "medium",
|
|
505
506
|
"description": "code_find + code_brief on the fresh-agent path (missing index, first touch).",
|
|
506
|
-
"duration_ms_median":
|
|
507
|
+
"duration_ms_median": 11,
|
|
507
508
|
"payload_chars_median": 1629,
|
|
508
509
|
"payload_tokens_est_median": 407
|
|
509
510
|
}
|
package/dist/facts.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.28.
|
|
3
|
-
"generated_at": "2026-08-
|
|
2
|
+
"version": "1.28.4",
|
|
3
|
+
"generated_at": "2026-08-28T07:25:30.486Z",
|
|
4
4
|
"tools": {
|
|
5
|
-
"count":
|
|
6
|
-
"published_count":
|
|
5
|
+
"count": 71,
|
|
6
|
+
"published_count": 69,
|
|
7
7
|
"names": [
|
|
8
8
|
"bclaw_bootstrap",
|
|
9
9
|
"bclaw_release_notes",
|
|
@@ -68,6 +68,7 @@
|
|
|
68
68
|
"bclaw_assignment_update",
|
|
69
69
|
"bclaw_assignment_action",
|
|
70
70
|
"bclaw_harvest_candidates",
|
|
71
|
+
"bclaw_harvest",
|
|
71
72
|
"bclaw_find",
|
|
72
73
|
"bclaw_get",
|
|
73
74
|
"bclaw_create",
|
|
@@ -476,7 +477,7 @@
|
|
|
476
477
|
},
|
|
477
478
|
"bench": {
|
|
478
479
|
"schema": "brainclaw.bench.v1",
|
|
479
|
-
"generated_at": "2026-08-
|
|
480
|
+
"generated_at": "2026-08-28T07:25:28.367Z",
|
|
480
481
|
"node_version": "v24.19.0",
|
|
481
482
|
"platform": "linux-x64",
|
|
482
483
|
"repeats": 3,
|
|
@@ -485,7 +486,7 @@
|
|
|
485
486
|
"name": "cold_onboard",
|
|
486
487
|
"volume": "empty",
|
|
487
488
|
"description": "fresh machine → init → first useful context. Baseline for time-to-first-value.",
|
|
488
|
-
"duration_ms_median":
|
|
489
|
+
"duration_ms_median": 80,
|
|
489
490
|
"payload_chars_median": 1640,
|
|
490
491
|
"payload_tokens_est_median": 410
|
|
491
492
|
},
|
|
@@ -493,7 +494,7 @@
|
|
|
493
494
|
"name": "warm_work",
|
|
494
495
|
"volume": "medium",
|
|
495
496
|
"description": "bclaw_work consult over a real-shaped store (~200 plans / 500 handoffs / 450 claims).",
|
|
496
|
-
"duration_ms_median":
|
|
497
|
+
"duration_ms_median": 124,
|
|
497
498
|
"payload_chars_median": 2626,
|
|
498
499
|
"payload_tokens_est_median": 657
|
|
499
500
|
},
|
|
@@ -501,7 +502,7 @@
|
|
|
501
502
|
"name": "first_edit",
|
|
502
503
|
"volume": "medium",
|
|
503
504
|
"description": "code_find + code_brief on the fresh-agent path (missing index, first touch).",
|
|
504
|
-
"duration_ms_median":
|
|
505
|
+
"duration_ms_median": 11,
|
|
505
506
|
"payload_chars_median": 1629,
|
|
506
507
|
"payload_tokens_est_median": 407
|
|
507
508
|
}
|
package/docs/cli.md
CHANGED
|
@@ -639,11 +639,11 @@ Full reference (freshness model, supported languages, WASM bundling): [docs/code
|
|
|
639
639
|
|
|
640
640
|
### `brainclaw code-map status [--cascade]`
|
|
641
641
|
|
|
642
|
-
Store presence, freshness badge (`fresh` / `stale_changed_files` / `stale_extractor` / `stale_grammar` / `partial` / `missing_index`), and index stats (files, nodes, edges). Read-only. In a multi-project workspace, `--cascade` adds
|
|
642
|
+
Store presence, freshness badge (`fresh` / `stale_changed_files` / `stale_extractor` / `stale_grammar` / `partial` / `missing_index`), and index stats (files, nodes, edges). Read-only. In a multi-project workspace, `--cascade` adds compact coverage counts and names only non-fresh projects. On the MCP surface, the equivalent `bclaw_code_status(cascade=true)` also follows the latest durable cascade job.
|
|
643
643
|
|
|
644
644
|
### `brainclaw code-map refresh [--all|--changed] [--cascade]`
|
|
645
645
|
|
|
646
|
-
Build or update the index. `--changed` (default) re-parses only touched files; `--all` does a full re-index. Run this when status shows `missing_index` or a stale badge. Fails fast (never blocks) if another writer holds the project lock. In a multi-project workspace, `--cascade` refreshes
|
|
646
|
+
Build or update the index. `--changed` (default) re-parses only touched files; `--all` does a full re-index. Run this when status shows `missing_index` or a stale badge. Fails fast (never blocks) if another writer holds the project lock. In a multi-project workspace, `--cascade` synchronously refreshes each discovered project into its own store plus a root store scoped to files no child owns (zero double-indexing). The MCP equivalent starts a durable background job instead; follow it with `bclaw_code_status(cascade=true)`. See [docs/code-map.md](code-map.md#cascading-a-multi-project-workspace---cascade).
|
|
647
647
|
|
|
648
648
|
### `brainclaw code-map find <query> [--limit <n>]`
|
|
649
649
|
|
|
@@ -1185,6 +1185,39 @@ brainclaw update-handoff hnd_001 --review-verdict request_changes --reviewed-by
|
|
|
1185
1185
|
|
|
1186
1186
|
---
|
|
1187
1187
|
|
|
1188
|
+
## Worker result harvest
|
|
1189
|
+
|
|
1190
|
+
### `brainclaw harvest [assignment_id]`
|
|
1191
|
+
|
|
1192
|
+
Ingest a worker's `LANE-RESULT.json`, reconcile any bound loop turn, and report
|
|
1193
|
+
the exact continuation. Pass one assignment id or use `--all`; this is distinct
|
|
1194
|
+
from `harvest-candidates`, which imports proposed memory items.
|
|
1195
|
+
|
|
1196
|
+
| Option | Description |
|
|
1197
|
+
|---|---|
|
|
1198
|
+
| `--all` | Scan every managed worktree |
|
|
1199
|
+
| `--integrate` | Also commit a sandboxed worker's worktree diff and settle its lifecycle |
|
|
1200
|
+
| `--orphaned` | Recover a dead worker that left no lane result, without deleting/resetting work |
|
|
1201
|
+
| `--base <ref>` | Base ref used by orphan recovery (default `master`) |
|
|
1202
|
+
| `--dry-run` | Preview without writing events or markers |
|
|
1203
|
+
| `--worktree <path>` | Explicit worktree to scan; repeatable |
|
|
1204
|
+
| `--json` | Return lane results, warnings, and loop continuations as JSON |
|
|
1205
|
+
|
|
1206
|
+
```bash
|
|
1207
|
+
brainclaw harvest asgn_123
|
|
1208
|
+
brainclaw harvest --all --json
|
|
1209
|
+
brainclaw harvest --integrate asgn_123
|
|
1210
|
+
```
|
|
1211
|
+
|
|
1212
|
+
The exact filename is `LANE-RESULT.json`. A unique root-level JSON file whose
|
|
1213
|
+
schema and `assignment_id` match can be recovered when a worker chose the wrong
|
|
1214
|
+
name; Brainclaw refuses ambiguous candidates. `artifacts` accepts either string
|
|
1215
|
+
refs or `{type, ref, description?}` objects, while loop workers must also emit
|
|
1216
|
+
the `artifact_type` named in their brief. A repairable schema/contract error
|
|
1217
|
+
keeps the real turn replayable and eligible for corrected re-harvest.
|
|
1218
|
+
|
|
1219
|
+
---
|
|
1220
|
+
|
|
1188
1221
|
## Dispatch
|
|
1189
1222
|
|
|
1190
1223
|
The `dispatch` command group manages the local agent dispatcher: it analyzes the active sequence for lane readiness and assigns work to available agents.
|