brainclaw 1.15.0 → 1.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -4
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/cli/register-capture.js +209 -0
- package/dist/cli/register-code-map.js +19 -0
- package/dist/cli/register-coordination.js +472 -0
- package/dist/cli/register-federation.js +258 -0
- package/dist/cli/register-lifecycle.js +436 -0
- package/dist/cli/register-memory-context.js +502 -0
- package/dist/cli/register-planning.js +167 -0
- package/dist/cli/register-review.js +149 -0
- package/dist/cli/shared.js +5 -0
- package/dist/cli.js +212 -2183
- package/dist/commands/dispatch-watch.js +25 -2
- package/dist/commands/harvest.js +107 -20
- package/dist/commands/mcp-catalog.js +1438 -0
- package/dist/commands/mcp-contract.js +33 -0
- package/dist/commands/mcp-presentation.js +27 -0
- package/dist/commands/mcp-read-handlers.js +72 -36
- package/dist/commands/mcp-write-admin.js +328 -0
- package/dist/commands/mcp-write-claims.js +864 -0
- package/dist/commands/mcp-write-coordination.js +1825 -0
- package/dist/commands/mcp-write-entities.js +620 -0
- package/dist/commands/mcp-write-memory.js +451 -0
- package/dist/commands/mcp-write-sequences.js +116 -0
- package/dist/commands/mcp-write-support.js +367 -0
- package/dist/commands/mcp.js +261 -5584
- package/dist/commands/update-handoff.js +28 -42
- package/dist/core/agent-capability.js +38 -16
- package/dist/core/agent-files.js +54 -3
- package/dist/core/agent-integrations.js +1 -0
- package/dist/core/coordination.js +5 -2
- package/dist/core/cross-project.js +35 -1
- package/dist/core/dispatcher.js +67 -27
- package/dist/core/entity-operations.js +335 -12
- package/dist/core/entity-registry.js +72 -9
- package/dist/core/execution.js +28 -4
- package/dist/core/facade-schema.js +18 -4
- package/dist/core/handoff-review.js +35 -0
- package/dist/core/protocol-tool-policy.js +113 -0
- package/dist/core/review-loop-close.js +184 -0
- package/dist/core/review-loop-turn-dispatch.js +183 -0
- package/dist/core/schema.js +24 -2
- package/dist/core/security-detectors.js +35 -6
- package/dist/core/security.js +32 -12
- package/dist/core/worktree.js +274 -12
- package/dist/facts.js +13 -11
- package/dist/facts.json +12 -10
- package/docs/PROTOCOL.md +7 -3
- package/docs/concepts/coordinator-runbook.md +3 -0
- package/docs/concepts/dispatch-lifecycle.md +4 -4
- package/docs/concepts/loop-engine.md +6 -2
- package/docs/concepts/troubleshooting.md +1 -1
- package/docs/integrations/codex.md +22 -6
- package/docs/integrations/overview.md +1 -1
- package/docs/mcp-schema-changelog.md +137 -2
- package/docs/playbooks/orchestration.md +1 -1
- package/docs/product/entity-model-audit.md +3 -2
- package/docs/security.md +22 -1
- package/package.json +3 -1
|
@@ -42,6 +42,24 @@ export function evaluateWatchTick(input) {
|
|
|
42
42
|
return 'running';
|
|
43
43
|
}
|
|
44
44
|
const AGENT_CHILD_NAMES = ['claude', 'codex', 'copilot', 'node'];
|
|
45
|
+
/**
|
|
46
|
+
* Parse `ps -A -o ppid=,comm=` output and return the lowercased command names
|
|
47
|
+
* whose parent pid equals `ppid`. Pure + exported so the ppid-filter logic is
|
|
48
|
+
* unit-tested independently of the platform the test runs on (trp_3b096bf4:
|
|
49
|
+
* BSD `ps` on macOS rejects the GNU `--ppid` flag, so we filter here instead).
|
|
50
|
+
*/
|
|
51
|
+
export function parseChildCommsByPpid(psOutput, ppid) {
|
|
52
|
+
const target = Math.floor(ppid);
|
|
53
|
+
return psOutput
|
|
54
|
+
.split('\n')
|
|
55
|
+
.map((line) => {
|
|
56
|
+
const m = /^\s*(\d+)\s+(.*)$/.exec(line);
|
|
57
|
+
if (!m || Number(m[1]) !== target)
|
|
58
|
+
return undefined;
|
|
59
|
+
return m[2].trim().toLowerCase();
|
|
60
|
+
})
|
|
61
|
+
.filter((n) => !!n);
|
|
62
|
+
}
|
|
45
63
|
/**
|
|
46
64
|
* Does a real agent process live under the wrapper pid?
|
|
47
65
|
* Returns undefined when the observation itself fails (never treated as death).
|
|
@@ -59,10 +77,15 @@ export function probeAgentChildAlive(wrapperPid) {
|
|
|
59
77
|
const names = out.split(/\r?\n/).map((l) => l.trim().toLowerCase()).filter(Boolean);
|
|
60
78
|
return names.some((n) => AGENT_CHILD_NAMES.some((a) => n.startsWith(a)));
|
|
61
79
|
}
|
|
62
|
-
|
|
80
|
+
// BSD `ps` (macOS) does NOT support the GNU `--ppid` flag — it errored on
|
|
81
|
+
// every poll on macOS and broke child-pid discovery there (trp_3b096bf4).
|
|
82
|
+
// List every process with its parent pid + command (portable across Linux
|
|
83
|
+
// and macOS — same `-A -o …=` form already used in ai-surface-inventory.ts)
|
|
84
|
+
// and filter by ppid in parseChildCommsByPpid.
|
|
85
|
+
const out = execFileSync('ps', ['-A', '-o', 'ppid=,comm='], {
|
|
63
86
|
encoding: 'utf-8', timeout: 15000,
|
|
64
87
|
});
|
|
65
|
-
const names = out
|
|
88
|
+
const names = parseChildCommsByPpid(out, wrapperPid);
|
|
66
89
|
return names.some((n) => AGENT_CHILD_NAMES.some((a) => n.includes(a)));
|
|
67
90
|
}
|
|
68
91
|
catch {
|
package/dist/commands/harvest.js
CHANGED
|
@@ -22,6 +22,8 @@ import { loadAssignment, transitionAssignment } from '../core/assignments.js';
|
|
|
22
22
|
import { loadClaim, releaseClaimsCascade, logCascadeReleaseResult } from '../core/claims.js';
|
|
23
23
|
import { getCapabilityProfile, dispatchCanCommit } from '../core/agent-capability.js';
|
|
24
24
|
import { commitWorktreeOnBehalf, worktreesBaseDir, resolveGitToplevel } from '../core/worktree.js';
|
|
25
|
+
import { closeReviewLoopFromLaneResult } from '../core/review-loop-close.js';
|
|
26
|
+
import { dispatchReviewLoopTurn } from '../core/review-loop-turn-dispatch.js';
|
|
25
27
|
/**
|
|
26
28
|
* Auto-detect all worktree directories under the brainclaw-managed base dir.
|
|
27
29
|
* Returns subdirectories that exist on disk (may or may not have an inbox).
|
|
@@ -112,10 +114,11 @@ function collectWorktreeCandidateFiles(worktreePath) {
|
|
|
112
114
|
/**
|
|
113
115
|
* Harvest candidates from worktree inboxes into the main project store.
|
|
114
116
|
*
|
|
115
|
-
* This is the coordinator-side fix for gap 5 of E2E test n°1:
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
117
|
+
* This is the coordinator-side fix for gap 5 of E2E test n°1: a worker that
|
|
118
|
+
* cannot write to the main store — a genuinely MCP-less agent, or (post
|
|
119
|
+
* dec#133) a sandboxed codex whose `.git` is read-only so it cannot commit —
|
|
120
|
+
* leaves candidates in its worktree inbox; the coordinator calls
|
|
121
|
+
* `harvestCandidates` to sync them.
|
|
119
122
|
*
|
|
120
123
|
* @returns HarvestResult with counts of harvested, skipped, and errors.
|
|
121
124
|
*/
|
|
@@ -231,8 +234,9 @@ export function runHarvestCandidates(options = {}) {
|
|
|
231
234
|
//
|
|
232
235
|
// A dispatched worker writes a single `LANE-RESULT.json` at its worktree root
|
|
233
236
|
// as its final step. This is the standard, brief-boilerplate-free channel for a
|
|
234
|
-
// worker (
|
|
235
|
-
// outcome. The coordinator ingests it with
|
|
237
|
+
// worker (a genuinely MCP-less agent, or a sandboxed codex that cannot git
|
|
238
|
+
// commit) to report its outcome. The coordinator ingests it with
|
|
239
|
+
// `brainclaw harvest <assignment_id>`.
|
|
236
240
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
237
241
|
/** Conventional path of a worker's lane-result file at the worktree root. */
|
|
238
242
|
export function getLaneResultPath(worktreePath) {
|
|
@@ -269,6 +273,21 @@ export function harvestLaneResults(options = {}) {
|
|
|
269
273
|
// Assignment filter (when harvesting a specific lane).
|
|
270
274
|
if (options.assignmentId && lane.assignment_id !== options.assignmentId)
|
|
271
275
|
continue;
|
|
276
|
+
// pln#628 Focus 4B (Codex review of #87 BLOCKING 1) — a review lane must
|
|
277
|
+
// close/advance its loop on the plain report-only harvest path too, not only
|
|
278
|
+
// on `--integrate`. closeReviewLoopFromLaneResult is convergent + idempotent
|
|
279
|
+
// (a terminal loop is a no-op; a stuck approve is resumed), so firing it here
|
|
280
|
+
// AND in integrateLaneResults is safe — and it runs BEFORE the harvested
|
|
281
|
+
// marker short-circuits below, so a re-harvest still resumes a stuck loop.
|
|
282
|
+
// PR2: cycleOnRequestChanges=false — the report path only closes on approve;
|
|
283
|
+
// it must NOT advance a request_changes cycle it cannot follow through on
|
|
284
|
+
// (no re-dispatch, no claim retention). `harvest --integrate` owns the cycle.
|
|
285
|
+
try {
|
|
286
|
+
const laneAssignment = loadAssignment(lane.assignment_id, cwd);
|
|
287
|
+
if (laneAssignment)
|
|
288
|
+
closeReviewLoopFromLaneResult(laneAssignment, lane, agent, cwd, { cycleOnRequestChanges: false });
|
|
289
|
+
}
|
|
290
|
+
catch { /* never block harvest on loop-close */ }
|
|
272
291
|
const marker = laneHarvestedMarkerPath(cwd, lane.assignment_id);
|
|
273
292
|
if (fs.existsSync(marker)) {
|
|
274
293
|
result.skipped.push(lane.assignment_id);
|
|
@@ -376,7 +395,7 @@ function forceCompleteAssignment(assignmentId, artifacts, statusReason, actor, c
|
|
|
376
395
|
export function integrateLaneResults(options = {}) {
|
|
377
396
|
const cwd = options.cwd ?? process.cwd();
|
|
378
397
|
const actor = options.agent ?? 'coordinator';
|
|
379
|
-
const result = { integrated: [], skipped: [], errors: [] };
|
|
398
|
+
const result = { integrated: [], skipped: [], errors: [], next_turns: [] };
|
|
380
399
|
const worktreePaths = resolveLaneScanPaths(options, cwd);
|
|
381
400
|
for (const worktreePath of worktreePaths) {
|
|
382
401
|
const file = getLaneResultPath(worktreePath);
|
|
@@ -444,16 +463,55 @@ export function integrateLaneResults(options = {}) {
|
|
|
444
463
|
...entry.files_changed.slice(0, 50).map((f) => ({ type: 'file', ref: f })),
|
|
445
464
|
];
|
|
446
465
|
entry.assignment_completed = forceCompleteAssignment(lane.assignment_id, artifacts, `pln#534 on-behalf integration: ${lane.summary.slice(0, 120)}`, actor, cwd);
|
|
447
|
-
//
|
|
448
|
-
//
|
|
449
|
-
//
|
|
450
|
-
//
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
reasons.push(`
|
|
466
|
+
// pln#628 Focus 4B — map this lane onto its review loop BEFORE deciding
|
|
467
|
+
// teardown: PR1 records the verdict + advances (auto-close on approve);
|
|
468
|
+
// PR2 continues the fix cycle on request_changes (bump round, emit a
|
|
469
|
+
// next_turn) unless the iteration cap is hit. This is the --integrate
|
|
470
|
+
// path, so it MAY cycle (it can re-dispatch AND retain the claim). No-op
|
|
471
|
+
// for non-review lanes / lanes without a verdict; never throws.
|
|
472
|
+
const loopClose = closeReviewLoopFromLaneResult(assignment, lane, actor, cwd);
|
|
473
|
+
if (loopClose) {
|
|
474
|
+
entry.review_loop = loopClose;
|
|
475
|
+
reasons.push(`review-loop ${loopClose.loop_id}: ${loopClose.action} — ${loopClose.reason}`);
|
|
476
|
+
if (loopClose.next_turn) {
|
|
477
|
+
result.next_turns.push({ loop_id: loopClose.loop_id, ...loopClose.next_turn });
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
// PR2 claim-teardown gate. Skip the release when either:
|
|
481
|
+
// (a) keep_claim — the symmetric fix cycle reuses the claim/worktree for
|
|
482
|
+
// the re-dispatched turn (commits accumulate on one branch); or
|
|
483
|
+
// (b) Codex review P0 — an idempotent re-harvest of an OLD lane whose
|
|
484
|
+
// loop is still OPEN returns a `noop` (the reviewer slot is now bound
|
|
485
|
+
// to a NEWER assignment under an active cycle). Releasing here would
|
|
486
|
+
// tear down the reused claim/worktree out from under the live turn
|
|
487
|
+
// and strand the fix cycle. The loop machinery owns the lifecycle
|
|
488
|
+
// while it is open; only a terminal close (approve/blocked, action
|
|
489
|
+
// 'closed') or an asymmetric hand-off ('advanced' without keep_claim)
|
|
490
|
+
// releases here. A `noop` on a TERMINAL loop still releases (safe —
|
|
491
|
+
// the closing pass already released, so this is a no-op).
|
|
492
|
+
const loopStillOpen = loopClose?.loop_status !== undefined &&
|
|
493
|
+
!['completed', 'cancelled', 'blocked'].includes(loopClose.loop_status);
|
|
494
|
+
const keepClaimAlive = loopClose?.keep_claim === true || (loopClose?.action === 'noop' && loopStillOpen);
|
|
495
|
+
if (keepClaimAlive) {
|
|
496
|
+
// The next_turn spawn (async) is awaited by runHarvestLane. The
|
|
497
|
+
// assignment for THIS turn is still completed above.
|
|
498
|
+
entry.claim_released = false;
|
|
499
|
+
reasons.push(loopClose?.keep_claim
|
|
500
|
+
? 'claim kept alive for review fix-cycle re-dispatch (PR2)'
|
|
501
|
+
: 'claim left intact — idempotent re-harvest on an active review loop (no strand)');
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
// trp#928 — use the cascade helper (was releaseClaimWithCascade — same
|
|
505
|
+
// logic for the last-claim rule but the cascade wrapper LOGS per-claim,
|
|
506
|
+
// so a silent ownership failure is observable in the runtime event log
|
|
507
|
+
// rather than only in this in-memory `reasons` string).
|
|
508
|
+
const cascade = releaseClaimsCascade([assignment.claim_id], { cwd, planStatus: 'done' });
|
|
509
|
+
logCascadeReleaseResult({ actor, trigger: 'harvest_integrate', assignment_id: lane.assignment_id, claim_id: assignment.claim_id, cascade, cwd });
|
|
510
|
+
const claimEntry = cascade.entries[0];
|
|
511
|
+
entry.claim_released = claimEntry?.released === true;
|
|
512
|
+
if (claimEntry && !claimEntry.released) {
|
|
513
|
+
reasons.push(`claim release ${claimEntry.reason}${claimEntry.error ? `: ${claimEntry.error}` : ''}`);
|
|
514
|
+
}
|
|
457
515
|
}
|
|
458
516
|
}
|
|
459
517
|
else {
|
|
@@ -672,7 +730,7 @@ export function harvestOrphaned(options) {
|
|
|
672
730
|
}
|
|
673
731
|
return report;
|
|
674
732
|
}
|
|
675
|
-
export function runHarvestLane(assignmentId, options = {}) {
|
|
733
|
+
export async function runHarvestLane(assignmentId, options = {}) {
|
|
676
734
|
const cwd = options.cwd ?? process.cwd();
|
|
677
735
|
if (!memoryExists(cwd)) {
|
|
678
736
|
console.error('Error: .brainclaw/ not found. Run `brainclaw init` first.');
|
|
@@ -734,8 +792,30 @@ export function runHarvestLane(assignmentId, options = {}) {
|
|
|
734
792
|
dryRun: options.dryRun,
|
|
735
793
|
cwd,
|
|
736
794
|
});
|
|
795
|
+
// pln#628 Focus 4B PR2 — spawn the review fix-cycle turns the sync integrate
|
|
796
|
+
// pass emitted. Re-dispatches the SAME reviewer into the SAME (kept) worktree
|
|
797
|
+
// to apply the requested changes + re-review. Dry-run only reports them.
|
|
798
|
+
const dispatchedTurns = [];
|
|
799
|
+
if (!options.dryRun) {
|
|
800
|
+
for (const nt of integ.next_turns) {
|
|
801
|
+
const dispatched = await dispatchReviewLoopTurn({
|
|
802
|
+
loopId: nt.loop_id,
|
|
803
|
+
slot: { slot_id: nt.slot_id, role: nt.role, agent: nt.agent, agent_id: nt.agent_id },
|
|
804
|
+
phase: nt.phase,
|
|
805
|
+
task: nt.task,
|
|
806
|
+
dispatcherAgent: options.agent ?? 'coordinator',
|
|
807
|
+
cwd,
|
|
808
|
+
// NO worktreeBaseRef: reuse the kept worktree so the fixes accumulate;
|
|
809
|
+
// pinning a ref would reset the branch and wipe prior-round commits.
|
|
810
|
+
});
|
|
811
|
+
dispatchedTurns.push({
|
|
812
|
+
loop_id: nt.loop_id, agent: nt.agent, iteration: nt.iteration,
|
|
813
|
+
execution_status: dispatched.execution_status, error: dispatched.error,
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
}
|
|
737
817
|
if (options.json) {
|
|
738
|
-
console.log(JSON.stringify(integ, null, 2));
|
|
818
|
+
console.log(JSON.stringify({ ...integ, dispatched_turns: dispatchedTurns }, null, 2));
|
|
739
819
|
return;
|
|
740
820
|
}
|
|
741
821
|
const dry = options.dryRun ? ' (dry-run)' : '';
|
|
@@ -762,7 +842,14 @@ export function runHarvestLane(assignmentId, options = {}) {
|
|
|
762
842
|
}
|
|
763
843
|
for (const err of integ.errors)
|
|
764
844
|
console.error(` ✗ ${err}`);
|
|
765
|
-
|
|
845
|
+
for (const dt of dispatchedTurns) {
|
|
846
|
+
const status = dt.error ? `error: ${dt.error}` : (dt.execution_status ?? 'unknown');
|
|
847
|
+
console.log(` ↻ Fix-cycle re-dispatch [${dt.loop_id}] round ${dt.iteration} → ${dt.agent} (${status})`);
|
|
848
|
+
}
|
|
849
|
+
if (options.dryRun && integ.next_turns.length > 0) {
|
|
850
|
+
console.log(` (dry-run) ${integ.next_turns.length} fix-cycle turn(s) would be re-dispatched.`);
|
|
851
|
+
}
|
|
852
|
+
console.log(`\n✔ Lane integrate complete${dry}: ${integ.integrated.length} integrated, ${dispatchedTurns.length} re-dispatched, ${integ.errors.length} error(s).`);
|
|
766
853
|
return;
|
|
767
854
|
}
|
|
768
855
|
const result = harvestLaneResults({
|