brainclaw 0.29.2 → 1.5.3
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 +193 -170
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/cli.js +673 -24
- package/dist/commands/accept.js +3 -0
- package/dist/commands/add-step.js +11 -26
- package/dist/commands/agent-board.js +70 -3
- package/dist/commands/audit.js +19 -0
- package/dist/commands/check-policy.js +54 -0
- package/dist/commands/check-security-mcp.js +145 -0
- package/dist/commands/check-security.js +106 -0
- package/dist/commands/claim-resource.js +1 -0
- package/dist/commands/codev.js +672 -0
- package/dist/commands/compact.js +74 -0
- package/dist/commands/complete-step.js +16 -26
- package/dist/commands/constraint.js +8 -20
- package/dist/commands/decision.js +9 -20
- package/dist/commands/delete-plan.js +10 -12
- package/dist/commands/delete-step.js +16 -0
- package/dist/commands/dispatch.js +163 -0
- package/dist/commands/doctor.js +1122 -49
- package/dist/commands/enable-agent.js +1 -0
- package/dist/commands/export.js +280 -22
- package/dist/commands/handoff.js +33 -0
- package/dist/commands/harvest.js +189 -0
- package/dist/commands/hooks.js +82 -25
- package/dist/commands/inbox.js +169 -0
- package/dist/commands/init.js +38 -31
- package/dist/commands/install-hooks.js +71 -44
- package/dist/commands/link.js +89 -0
- package/dist/commands/list-claims.js +48 -3
- package/dist/commands/list-plans.js +129 -25
- package/dist/commands/loops-handlers.js +409 -0
- package/dist/commands/mcp-read-handlers.js +1628 -0
- package/dist/commands/mcp-schemas.generated.js +74 -0
- package/dist/commands/mcp.js +4221 -1501
- package/dist/commands/plan-resource.js +64 -0
- package/dist/commands/plan.js +12 -26
- package/dist/commands/prune.js +37 -2
- package/dist/commands/reflect.js +20 -7
- package/dist/commands/release-claim.js +11 -6
- package/dist/commands/release-notes.js +170 -0
- package/dist/commands/repair.js +210 -0
- package/dist/commands/run-profile.js +57 -0
- package/dist/commands/sequence.js +113 -0
- package/dist/commands/session-end.js +423 -14
- package/dist/commands/session-start.js +214 -41
- package/dist/commands/setup-security.js +103 -0
- package/dist/commands/setup.js +42 -4
- package/dist/commands/stale.js +109 -0
- package/dist/commands/switch.js +100 -2
- package/dist/commands/trap.js +14 -31
- package/dist/commands/update-handoff.js +63 -4
- package/dist/commands/update-plan.js +21 -28
- package/dist/commands/update-step.js +37 -0
- package/dist/commands/upgrade.js +313 -6
- package/dist/commands/usage.js +102 -0
- package/dist/commands/version.js +20 -0
- package/dist/commands/who.js +33 -5
- package/dist/commands/worktree.js +105 -0
- package/dist/core/actions.js +315 -0
- package/dist/core/agent-capability.js +610 -17
- package/dist/core/agent-context.js +7 -1
- package/dist/core/agent-files.js +1169 -85
- package/dist/core/agent-integrations.js +160 -5
- package/dist/core/agent-inventory.js +2 -0
- package/dist/core/agent-profiles.js +93 -0
- package/dist/core/agent-registry.js +162 -30
- package/dist/core/agentrun-reconciler.js +345 -0
- package/dist/core/agentruns.js +424 -0
- package/dist/core/ai-agent-detection.js +31 -10
- package/dist/core/archival.js +77 -0
- package/dist/core/assignment-sweeper.js +82 -0
- package/dist/core/assignments.js +367 -0
- package/dist/core/audit.js +30 -0
- package/dist/core/brainclaw-version.js +94 -2
- package/dist/core/candidates.js +93 -2
- package/dist/core/claims.js +419 -0
- package/dist/core/codev-metrics.js +77 -0
- package/dist/core/codev-personas.js +31 -0
- package/dist/core/codev-plan-gen.js +35 -0
- package/dist/core/codev-prompts.js +74 -0
- package/dist/core/codev-responses.js +62 -0
- package/dist/core/codev-rounds.js +218 -0
- package/dist/core/config.js +4 -0
- package/dist/core/context.js +381 -34
- package/dist/core/coordination.js +201 -6
- package/dist/core/cross-project.js +230 -16
- package/dist/core/default-profiles/doctor.yaml +11 -0
- package/dist/core/default-profiles/janitor.yaml +11 -0
- package/dist/core/default-profiles/onboarder.yaml +11 -0
- package/dist/core/default-profiles/reviewer.yaml +13 -0
- package/dist/core/dispatcher.js +1189 -0
- package/dist/core/duplicates.js +2 -2
- package/dist/core/entity-operations.js +450 -0
- package/dist/core/entity-registry.js +344 -0
- package/dist/core/events.js +106 -2
- package/dist/core/execution-adapters.js +154 -0
- package/dist/core/execution-context.js +63 -0
- package/dist/core/execution-profile.js +270 -0
- package/dist/core/execution.js +255 -0
- package/dist/core/facade-schema.js +81 -0
- package/dist/core/federation-cloud.js +99 -0
- package/dist/core/federation-message.js +52 -0
- package/dist/core/federation-transport.js +65 -0
- package/dist/core/gc-semantic.js +482 -0
- package/dist/core/governance.js +247 -0
- package/dist/core/guards.js +19 -0
- package/dist/core/ideation.js +72 -0
- package/dist/core/identity.js +110 -25
- package/dist/core/ids.js +6 -0
- package/dist/core/input-validation.js +2 -2
- package/dist/core/instruction-templates.js +344 -136
- package/dist/core/io.js +90 -11
- package/dist/core/lock.js +6 -2
- package/dist/core/loops/brief-assembly.js +213 -0
- package/dist/core/loops/facade-schema.js +148 -0
- package/dist/core/loops/index.js +7 -0
- package/dist/core/loops/iteration-engine.js +139 -0
- package/dist/core/loops/lock.js +385 -0
- package/dist/core/loops/store.js +201 -0
- package/dist/core/loops/types.js +403 -0
- package/dist/core/loops/verbs.js +534 -0
- package/dist/core/markdown.js +15 -3
- package/dist/core/memory-compactor.js +432 -0
- package/dist/core/memory-git.js +152 -8
- package/dist/core/messaging.js +278 -0
- package/dist/core/migration.js +32 -1
- package/dist/core/mutation-pipeline.js +4 -2
- package/dist/core/operations/memory-mutation.js +129 -0
- package/dist/core/operations/memory-write.js +78 -0
- package/dist/core/operations/plan.js +190 -0
- package/dist/core/policy.js +169 -0
- package/dist/core/reputation.js +9 -3
- package/dist/core/schema.js +491 -6
- package/dist/core/search.js +21 -2
- package/dist/core/security-cache.js +71 -0
- package/dist/core/security-guard.js +152 -0
- package/dist/core/security-scoring.js +86 -0
- package/dist/core/sequence.js +130 -0
- package/dist/core/socket-client.js +113 -0
- package/dist/core/staleness.js +246 -0
- package/dist/core/state.js +98 -22
- package/dist/core/store-resolution.js +43 -11
- package/dist/core/toml-writer.js +76 -0
- package/dist/core/upgrades/backup.js +232 -0
- package/dist/core/upgrades/health-check.js +169 -0
- package/dist/core/upgrades/patches/candidate-archive.js +145 -0
- package/dist/core/upgrades/patches/handoff-review-strip.js +128 -0
- package/dist/core/upgrades/patches/provenance-rollout.js +136 -0
- package/dist/core/upgrades/schema-version.js +97 -0
- package/dist/core/worktree.js +606 -0
- package/dist/facts.js +114 -0
- package/dist/facts.json +111 -0
- package/docs/architecture/project-refs.md +5 -1
- package/docs/cli.md +690 -43
- package/docs/concepts/ideation-loop.md +317 -0
- package/docs/concepts/loop-engine.md +456 -0
- package/docs/concepts/mcp-governance.md +268 -0
- package/docs/concepts/memory-staleness.md +122 -0
- package/docs/concepts/multi-agent-workflows.md +166 -0
- package/docs/concepts/plans-and-claims.md +31 -6
- package/docs/concepts/project-md-convention.md +35 -0
- package/docs/concepts/troubleshooting.md +220 -0
- package/docs/concepts/upgrade-cli.md +202 -0
- package/docs/concepts/upgrade-dogfood-procedure.md +114 -0
- package/docs/context-format-changelog.md +2 -2
- package/docs/context-format.md +2 -2
- package/docs/index.md +68 -0
- package/docs/integrations/agents.md +15 -16
- package/docs/integrations/cline.md +88 -0
- package/docs/integrations/codex.md +75 -23
- package/docs/integrations/continue.md +60 -0
- package/docs/integrations/copilot.md +67 -9
- package/docs/integrations/kilocode.md +72 -0
- package/docs/integrations/mcp.md +304 -21
- package/docs/integrations/mistral-vibe.md +122 -0
- package/docs/integrations/opencode.md +84 -0
- package/docs/integrations/overview.md +23 -8
- package/docs/integrations/roo.md +74 -0
- package/docs/integrations/windsurf.md +83 -0
- package/docs/mcp-schema-changelog.md +191 -1
- package/docs/playbooks/integration/index.md +121 -0
- package/docs/playbooks/productivity/index.md +102 -0
- package/docs/playbooks/team/index.md +122 -0
- package/docs/product/agent-first-model.md +184 -0
- package/docs/product/entity-model-audit.md +462 -0
- package/docs/quickstart-existing-project.md +135 -0
- package/docs/quickstart.md +124 -37
- package/docs/release-maintenance.md +79 -0
- package/docs/review.md +2 -0
- package/docs/server-operations.md +118 -0
- package/package.json +20 -12
- package/dist/commands/claude-desktop-extension.js +0 -18
- package/dist/commands/diff.js +0 -99
- package/dist/core/claude-desktop-extension.js +0 -224
|
@@ -1,21 +1,32 @@
|
|
|
1
|
-
import { findAgentIdentityByName, resolveAgentScope, resolveCurrentAgentIdentity } from './agent-registry.js';
|
|
1
|
+
import { findAgentIdentityByName, listAgentIdentities, resolveAgentScope, resolveCurrentAgentIdentity } from './agent-registry.js';
|
|
2
2
|
import { loadConfig } from './config.js';
|
|
3
3
|
import { resolveCurrentHostId } from './host.js';
|
|
4
4
|
import { listClaims } from './claims.js';
|
|
5
|
+
import { listAssignments } from './assignments.js';
|
|
6
|
+
import { listAgentRuns } from './agentruns.js';
|
|
7
|
+
import { listActionRequired } from './actions.js';
|
|
8
|
+
import { getActiveSequence } from './sequence.js';
|
|
9
|
+
import { resolveCrossProjectLinks, listIncomingCrossProjectSignals } from './cross-project.js';
|
|
5
10
|
import { inferProjectFromTarget, loadInstructions, resolveInstructions } from './instructions.js';
|
|
6
11
|
import { buildReputationSummary, findAgentReputationSummary } from './reputation.js';
|
|
7
12
|
import { listRuntimeNotes } from './runtime.js';
|
|
8
13
|
import { loadState, persistState } from './state.js';
|
|
14
|
+
import { getCapabilityProfile } from './agent-capability.js';
|
|
15
|
+
import { loadAllSessions } from './identity.js';
|
|
16
|
+
import { countActionable } from './messaging.js';
|
|
17
|
+
import { listCandidates } from './candidates.js';
|
|
18
|
+
import { pullSignalsFromLinkedProjects } from './federation-transport.js';
|
|
9
19
|
export function buildCoordinationSnapshot(options = {}) {
|
|
10
20
|
const config = loadConfig(options.cwd);
|
|
11
21
|
const state = loadState(options.cwd);
|
|
12
22
|
const currentHost = resolveCurrentHostId();
|
|
13
23
|
const project = options.project ?? inferProjectFromTarget(options.target, config);
|
|
14
|
-
const agent = resolveAgentScope(options.agent, options.cwd);
|
|
24
|
+
const agent = options.skipAgentAutoDetect ? undefined : resolveAgentScope(options.agent, options.cwd);
|
|
15
25
|
const resolvedAgentIdentity = agent
|
|
16
26
|
? (options.agent ? findAgentIdentityByName(agent, options.cwd) : resolveCurrentAgentIdentity(options.cwd))
|
|
17
27
|
: undefined;
|
|
18
28
|
const claims = listClaims(options.cwd).filter((claim) => claim.status === 'active');
|
|
29
|
+
const activeSequence = getActiveSequence(options.cwd);
|
|
19
30
|
const runtimeNotes = listRuntimeNotes({
|
|
20
31
|
agent,
|
|
21
32
|
hostId: options.host,
|
|
@@ -75,37 +86,221 @@ export function buildCoordinationSnapshot(options = {}) {
|
|
|
75
86
|
active_claims: agent
|
|
76
87
|
? filteredClaims.filter((claim) => claim.agent === agent)
|
|
77
88
|
: filteredClaims,
|
|
89
|
+
active_assignments: (agent
|
|
90
|
+
? listAssignments(options.cwd, { agent })
|
|
91
|
+
: listAssignments(options.cwd)).filter((assignment) => !['completed', 'failed', 'expired', 'rerouted'].includes(assignment.status) &&
|
|
92
|
+
(!project || !assignment.plan_id || filteredPlans.some((plan) => plan.id === assignment.plan_id))),
|
|
93
|
+
active_runs: (agent
|
|
94
|
+
? listAgentRuns(options.cwd, { agent })
|
|
95
|
+
: listAgentRuns(options.cwd)).filter((run) => !['completed', 'failed', 'cancelled', 'timed_out', 'interrupted'].includes(run.status) &&
|
|
96
|
+
(!project || !run.plan_id || filteredPlans.some((plan) => plan.id === run.plan_id))),
|
|
97
|
+
active_actions: (agent
|
|
98
|
+
? listActionRequired(options.cwd, { agent, status: 'pending' })
|
|
99
|
+
: listActionRequired(options.cwd, { status: 'pending' })).filter((action) => !project || !action.plan_id || filteredPlans.some((plan) => plan.id === action.plan_id)),
|
|
100
|
+
active_sequence: enrichSequenceWithPlanStatus(activeSequence, state.plan_items),
|
|
78
101
|
runtime_notes: filteredNotes,
|
|
79
102
|
session_meta_hidden: sessionMetaHidden,
|
|
80
103
|
open_handoffs: filteredHandoffs,
|
|
81
104
|
resolved_instructions: instructions,
|
|
82
105
|
reputation_summary: reputationSummary,
|
|
83
106
|
agent_reputation: agentReputation,
|
|
84
|
-
other_agents: buildOtherAgentsSummary(filteredClaims,
|
|
107
|
+
other_agents: buildOtherAgentsSummary(filteredClaims, runtimeNotes, agent, options.cwd),
|
|
108
|
+
linked_projects: buildLinkedProjectsSummary(options.cwd),
|
|
109
|
+
incoming_signals: buildIncomingSignalsSummary(options.cwd),
|
|
110
|
+
known_traps: state.known_traps
|
|
111
|
+
.filter((t) => t.visibility === 'shared' && (!t.status || t.status === 'active'))
|
|
112
|
+
.sort((a, b) => severityOrder(b.severity) - severityOrder(a.severity)),
|
|
113
|
+
pending_candidates: listCandidates('pending', options.cwd),
|
|
114
|
+
inbox_pending: agent ? countActionable(agent, options.cwd ?? process.cwd()) : 0,
|
|
85
115
|
};
|
|
86
116
|
}
|
|
87
|
-
function
|
|
117
|
+
function enrichSequenceWithPlanStatus(sequence, allPlans) {
|
|
118
|
+
if (!sequence)
|
|
119
|
+
return sequence;
|
|
120
|
+
const planMap = new Map(allPlans.map(p => [p.id, p]));
|
|
121
|
+
return {
|
|
122
|
+
...sequence,
|
|
123
|
+
items: sequence.items.map((item) => {
|
|
124
|
+
const plan = planMap.get(item.planId);
|
|
125
|
+
let planStatus = plan?.status ?? 'unknown';
|
|
126
|
+
let planText = plan?.text?.slice(0, 80) ?? item.planId;
|
|
127
|
+
if (item.stepId && plan?.steps) {
|
|
128
|
+
const step = plan.steps.find((s) => s.id === item.stepId);
|
|
129
|
+
if (step) {
|
|
130
|
+
planStatus = step.status === 'done' ? 'done' : plan.status;
|
|
131
|
+
planText = step.text.slice(0, 80);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
...item,
|
|
136
|
+
plan_status: planStatus,
|
|
137
|
+
plan_text: planText,
|
|
138
|
+
plan_priority: plan?.priority,
|
|
139
|
+
plan_assignee: plan?.assignee,
|
|
140
|
+
};
|
|
141
|
+
}),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
/** Parse a duration string like '4h', '30m', '1d' to milliseconds. */
|
|
145
|
+
function parseBoardTtl(value) {
|
|
146
|
+
const match = /^(\d+)([mhd])$/i.exec(value.trim());
|
|
147
|
+
if (!match)
|
|
148
|
+
return 4 * 3_600_000;
|
|
149
|
+
const amount = parseInt(match[1], 10);
|
|
150
|
+
const unit = match[2].toLowerCase();
|
|
151
|
+
if (unit === 'm')
|
|
152
|
+
return amount * 60_000;
|
|
153
|
+
if (unit === 'h')
|
|
154
|
+
return amount * 3_600_000;
|
|
155
|
+
return amount * 86_400_000;
|
|
156
|
+
}
|
|
157
|
+
function buildOtherAgentsSummary(claims, notes, currentAgent, cwd) {
|
|
158
|
+
// Count active sessions per agent for instance_count — use config TTL
|
|
159
|
+
const sessions = loadAllSessions(cwd);
|
|
160
|
+
const now = Date.now();
|
|
161
|
+
let ttlStr = '4h';
|
|
162
|
+
try {
|
|
163
|
+
ttlStr = loadConfig(cwd).implicit_session_ttl ?? '4h';
|
|
164
|
+
}
|
|
165
|
+
catch { /* use default */ }
|
|
166
|
+
const TTL_MS = parseBoardTtl(ttlStr);
|
|
167
|
+
const sessionCounts = new Map();
|
|
168
|
+
for (const s of sessions) {
|
|
169
|
+
const lastSeen = new Date(s.last_seen_at).getTime();
|
|
170
|
+
if (!isNaN(lastSeen) && now - lastSeen < TTL_MS) {
|
|
171
|
+
sessionCounts.set(s.agent, (sessionCounts.get(s.agent) ?? 0) + 1);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// Start from ALL registered agents — they always appear
|
|
88
175
|
const agentMap = new Map();
|
|
176
|
+
for (const identity of listAgentIdentities(cwd)) {
|
|
177
|
+
if (identity.agent_name === currentAgent)
|
|
178
|
+
continue;
|
|
179
|
+
const profile = getCapabilityProfile(identity.agent_name);
|
|
180
|
+
const maxTasks = profile?.max_concurrent_tasks ?? 1;
|
|
181
|
+
agentMap.set(identity.agent_name, {
|
|
182
|
+
name: identity.agent_name,
|
|
183
|
+
trust_level: identity.trust_level ?? 'contributor',
|
|
184
|
+
claim_count: 0,
|
|
185
|
+
scopes: [],
|
|
186
|
+
has_open_session: false,
|
|
187
|
+
instance_count: sessionCounts.get(identity.agent_name) ?? 0,
|
|
188
|
+
max_tasks: maxTasks,
|
|
189
|
+
slots_remaining: maxTasks, // will be reduced when claims are counted
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
// Enrich with active claims
|
|
89
193
|
for (const claim of claims) {
|
|
90
194
|
if (claim.agent === currentAgent)
|
|
91
195
|
continue;
|
|
92
|
-
const
|
|
196
|
+
const profile = getCapabilityProfile(claim.agent);
|
|
197
|
+
const maxTasks = profile?.max_concurrent_tasks ?? 1;
|
|
198
|
+
const existing = agentMap.get(claim.agent) ?? {
|
|
199
|
+
name: claim.agent, trust_level: 'contributor', claim_count: 0, scopes: [],
|
|
200
|
+
has_open_session: false, instance_count: sessionCounts.get(claim.agent) ?? 0,
|
|
201
|
+
max_tasks: maxTasks, slots_remaining: maxTasks,
|
|
202
|
+
};
|
|
93
203
|
existing.claim_count++;
|
|
204
|
+
existing.slots_remaining = Math.max(0, existing.max_tasks - existing.claim_count);
|
|
94
205
|
existing.scopes.push(claim.scope);
|
|
95
206
|
if (!existing.last_active || claim.created_at > existing.last_active) {
|
|
96
207
|
existing.last_active = claim.created_at;
|
|
97
208
|
}
|
|
98
209
|
agentMap.set(claim.agent, existing);
|
|
99
210
|
}
|
|
211
|
+
// Enrich with runtime notes (including session lifecycle) for last_active + open session detection
|
|
100
212
|
for (const note of notes) {
|
|
101
213
|
if (note.agent === currentAgent)
|
|
102
214
|
continue;
|
|
103
215
|
const existing = agentMap.get(note.agent);
|
|
104
|
-
if (
|
|
216
|
+
if (!existing)
|
|
217
|
+
continue; // skip unregistered agents in notes
|
|
218
|
+
if (!existing.last_active || note.created_at > existing.last_active) {
|
|
105
219
|
existing.last_active = note.created_at;
|
|
106
220
|
}
|
|
221
|
+
if (note.note_type === 'session_start') {
|
|
222
|
+
existing.has_open_session = true;
|
|
223
|
+
}
|
|
224
|
+
if (note.note_type === 'session_end') {
|
|
225
|
+
existing.has_open_session = false;
|
|
226
|
+
}
|
|
107
227
|
}
|
|
108
228
|
const result = [...agentMap.values()];
|
|
109
229
|
return result.length > 0 ? result : undefined;
|
|
110
230
|
}
|
|
231
|
+
function buildLinkedProjectsSummary(cwd) {
|
|
232
|
+
const links = resolveCrossProjectLinks(cwd);
|
|
233
|
+
if (links.length === 0)
|
|
234
|
+
return undefined;
|
|
235
|
+
const summaries = [];
|
|
236
|
+
for (const link of links) {
|
|
237
|
+
const summary = {
|
|
238
|
+
name: link.projectName,
|
|
239
|
+
path: link.path,
|
|
240
|
+
role: link.role,
|
|
241
|
+
available: link.available,
|
|
242
|
+
active_claims: 0,
|
|
243
|
+
active_plans: 0,
|
|
244
|
+
agents: [],
|
|
245
|
+
};
|
|
246
|
+
if (link.available) {
|
|
247
|
+
try {
|
|
248
|
+
const claims = listClaims(link.absolutePath).filter(c => c.status === 'active');
|
|
249
|
+
const state = loadState(link.absolutePath);
|
|
250
|
+
const plans = state.plan_items.filter(p => p.status !== 'done' && p.status !== 'dropped');
|
|
251
|
+
summary.active_claims = claims.length;
|
|
252
|
+
summary.active_plans = plans.length;
|
|
253
|
+
const agentSet = new Set();
|
|
254
|
+
for (const c of claims)
|
|
255
|
+
agentSet.add(c.agent);
|
|
256
|
+
summary.agents = [...agentSet];
|
|
257
|
+
}
|
|
258
|
+
catch { /* linked project read failed, skip */ }
|
|
259
|
+
}
|
|
260
|
+
summaries.push(summary);
|
|
261
|
+
}
|
|
262
|
+
return summaries.length > 0 ? summaries : undefined;
|
|
263
|
+
}
|
|
264
|
+
function buildIncomingSignalsSummary(cwd) {
|
|
265
|
+
const signals = listIncomingCrossProjectSignals(cwd);
|
|
266
|
+
const incomingSignals = signals.map((signal) => {
|
|
267
|
+
const text = 'text' in signal.payload ? signal.payload.text : '';
|
|
268
|
+
return {
|
|
269
|
+
id: signal.id,
|
|
270
|
+
entity_type: signal.entity_type,
|
|
271
|
+
from_project: signal.from_project.name,
|
|
272
|
+
from_agent: signal.from_agent.name,
|
|
273
|
+
created_at: signal.created_at,
|
|
274
|
+
preview: text.length > 120 ? text.slice(0, 117) + '...' : text,
|
|
275
|
+
};
|
|
276
|
+
});
|
|
277
|
+
try {
|
|
278
|
+
const fedSignals = pullSignalsFromLinkedProjects(cwd);
|
|
279
|
+
for (const sig of fedSignals) {
|
|
280
|
+
const payloadPreview = typeof sig.payload === 'string'
|
|
281
|
+
? sig.payload.slice(0, 120)
|
|
282
|
+
: JSON.stringify(sig.payload).slice(0, 120);
|
|
283
|
+
incomingSignals.push({
|
|
284
|
+
id: sig.id,
|
|
285
|
+
entity_type: sig.type,
|
|
286
|
+
from_project: sig.from.project_name,
|
|
287
|
+
from_agent: sig.from.agent_name,
|
|
288
|
+
preview: payloadPreview,
|
|
289
|
+
created_at: sig.created_at,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
catch { /* non-fatal */ }
|
|
294
|
+
if (incomingSignals.length === 0)
|
|
295
|
+
return undefined;
|
|
296
|
+
return incomingSignals;
|
|
297
|
+
}
|
|
298
|
+
function severityOrder(severity) {
|
|
299
|
+
switch (severity) {
|
|
300
|
+
case 'high': return 3;
|
|
301
|
+
case 'medium': return 2;
|
|
302
|
+
case 'low': return 1;
|
|
303
|
+
default: return 0;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
111
306
|
//# sourceMappingURL=coordination.js.map
|
|
@@ -1,8 +1,34 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
1
2
|
import path from 'node:path';
|
|
2
|
-
import { loadConfig } from './config.js';
|
|
3
|
+
import { loadConfig, saveConfig } from './config.js';
|
|
3
4
|
import { loadState } from './state.js';
|
|
4
|
-
import {
|
|
5
|
-
import { memoryExists } from './io.js';
|
|
5
|
+
import { generateId, nowISO } from './ids.js';
|
|
6
|
+
import { memoryExists, resolveEntityDir } from './io.js';
|
|
7
|
+
import { CrossProjectLinkSchema } from './schema.js';
|
|
8
|
+
import { resolveProjectRef } from './store-resolution.js';
|
|
9
|
+
function crossProjectSignalDir(cwd, mode = 'read') {
|
|
10
|
+
return path.join(resolveEntityDir('inbox', cwd ?? process.cwd(), mode), 'cross-project');
|
|
11
|
+
}
|
|
12
|
+
function ensureCrossProjectSignalDir(cwd) {
|
|
13
|
+
const dir = crossProjectSignalDir(cwd, 'write');
|
|
14
|
+
if (!fs.existsSync(dir)) {
|
|
15
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
return dir;
|
|
18
|
+
}
|
|
19
|
+
export function resolveCrossProjectWritableTarget(nameOrPath, entityType, cwd) {
|
|
20
|
+
const link = resolveCrossProjectTarget(nameOrPath, cwd);
|
|
21
|
+
if (link.role !== 'publisher') {
|
|
22
|
+
throw new Error(`Cross-project link to '${link.projectName}' is role=subscriber — cannot push ${entityType} signals. Set role: publisher to enable push.`);
|
|
23
|
+
}
|
|
24
|
+
if (!link.available) {
|
|
25
|
+
throw new Error(`Target project not found or not initialized: ${link.absolutePath}`);
|
|
26
|
+
}
|
|
27
|
+
if (link.channels?.length && !link.channels.includes(entityType)) {
|
|
28
|
+
throw new Error(`Cross-project link to '${link.projectName}' does not allow ${entityType} signals. Allowed channels: ${link.channels.join(', ')}.`);
|
|
29
|
+
}
|
|
30
|
+
return link;
|
|
31
|
+
}
|
|
6
32
|
/**
|
|
7
33
|
* Resolves cross_project_links from config, converting relative paths to absolute.
|
|
8
34
|
*/
|
|
@@ -65,22 +91,64 @@ export function loadCrossProjectState(absolutePath) {
|
|
|
65
91
|
return loadState(absolutePath);
|
|
66
92
|
}
|
|
67
93
|
/**
|
|
68
|
-
* Writes a
|
|
69
|
-
* Used by bclaw_write_note --cross-project.
|
|
94
|
+
* Writes a structured signal into a target (publisher-linked) project's inbox.
|
|
70
95
|
*/
|
|
71
|
-
export function
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
96
|
+
export function writeCrossProjectSignal(target, entityType, payload, sourceCwd) {
|
|
97
|
+
const link = typeof target === 'string'
|
|
98
|
+
? resolveCrossProjectWritableTarget(target, entityType, sourceCwd)
|
|
99
|
+
: target;
|
|
100
|
+
const sourceRoot = path.resolve(sourceCwd ?? process.cwd());
|
|
101
|
+
const sourceConfig = loadConfig(sourceRoot);
|
|
102
|
+
const agentName = 'author' in payload ? payload.author : ('agent' in payload ? payload.agent : 'unknown');
|
|
103
|
+
const agentId = 'author_id' in payload ? payload.author_id : ('agent_id' in payload ? payload.agent_id : undefined);
|
|
104
|
+
const signal = {
|
|
105
|
+
schema_version: 1,
|
|
106
|
+
id: generateId('sig'),
|
|
107
|
+
entity_type: entityType,
|
|
108
|
+
created_at: nowISO(),
|
|
109
|
+
from_project: {
|
|
110
|
+
id: sourceConfig.project_id,
|
|
111
|
+
name: sourceConfig.project_name ?? path.basename(sourceRoot),
|
|
112
|
+
path: sourceRoot,
|
|
113
|
+
},
|
|
114
|
+
from_agent: {
|
|
115
|
+
name: agentName,
|
|
116
|
+
id: agentId,
|
|
117
|
+
host_id: payload.host_id,
|
|
118
|
+
session_id: payload.session_id,
|
|
119
|
+
},
|
|
120
|
+
target_project: {
|
|
121
|
+
name: link.projectName,
|
|
122
|
+
path: link.absolutePath,
|
|
123
|
+
},
|
|
124
|
+
payload,
|
|
125
|
+
};
|
|
126
|
+
const dir = ensureCrossProjectSignalDir(link.absolutePath);
|
|
127
|
+
const filepath = path.join(dir, `${signal.id}.json`);
|
|
128
|
+
fs.writeFileSync(filepath, JSON.stringify(signal, null, 2) + '\n', 'utf-8');
|
|
129
|
+
return signal;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Lists cross-project signals materialized in the local inbox.
|
|
133
|
+
*/
|
|
134
|
+
export function listIncomingCrossProjectSignals(cwd) {
|
|
135
|
+
const dir = crossProjectSignalDir(cwd);
|
|
136
|
+
if (!fs.existsSync(dir)) {
|
|
137
|
+
return [];
|
|
79
138
|
}
|
|
80
|
-
|
|
81
|
-
|
|
139
|
+
const signals = [];
|
|
140
|
+
for (const entry of fs.readdirSync(dir)) {
|
|
141
|
+
if (!entry.endsWith('.json'))
|
|
142
|
+
continue;
|
|
143
|
+
const filepath = path.join(dir, entry);
|
|
144
|
+
try {
|
|
145
|
+
signals.push(JSON.parse(fs.readFileSync(filepath, 'utf-8')));
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// Ignore malformed signal files.
|
|
149
|
+
}
|
|
82
150
|
}
|
|
83
|
-
|
|
151
|
+
return signals.sort((a, b) => a.created_at.localeCompare(b.created_at));
|
|
84
152
|
}
|
|
85
153
|
/**
|
|
86
154
|
* Returns the absolute path of a cross-project link by name or path fragment.
|
|
@@ -96,4 +164,150 @@ export function resolveCrossProjectTarget(nameOrPath, cwd) {
|
|
|
96
164
|
}
|
|
97
165
|
return match;
|
|
98
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* Resolve a `project` argument (name, path, or basename) to an absolute cwd
|
|
169
|
+
* usable by entity-operations / state / etc. Powers the optional `project?`
|
|
170
|
+
* parameter on the canonical grammar (bclaw_find/get/create/update/remove/
|
|
171
|
+
* transition/context/coordinate) — pln#359.
|
|
172
|
+
*
|
|
173
|
+
* Cross-project switching is intentionally limited to **linked projects only**
|
|
174
|
+
* — projects the user has explicitly opted into. Two link kinds count:
|
|
175
|
+
*
|
|
176
|
+
* • cross_project_links (peer/sibling links via config.yaml).
|
|
177
|
+
* • workspace store-chain children (monorepo-style nested projects), via
|
|
178
|
+
* `resolveProjectRef`. These are also "linked" — the parent workspace
|
|
179
|
+
* enumerates them through its config / discovery scan.
|
|
180
|
+
*
|
|
181
|
+
* Arbitrary directory paths that aren't reachable via either link kind are
|
|
182
|
+
* rejected. Adoption requires an explicit `brainclaw link add` or workspace
|
|
183
|
+
* registration — single point of control over what an agent can reach.
|
|
184
|
+
*
|
|
185
|
+
* Resolution order:
|
|
186
|
+
* 1. `projectArg` undefined or empty → return `currentCwd` unchanged.
|
|
187
|
+
* 2. `projectArg` matches the current project's `project_name` (from config)
|
|
188
|
+
* OR its directory basename → `currentCwd`.
|
|
189
|
+
* 3. `projectArg` matches a cross_project_link → that link's `absolutePath`,
|
|
190
|
+
* provided the link is `available` (target dir exists + brainclaw-init).
|
|
191
|
+
* Match keys: projectName, name, path, absolutePath, basename(absolutePath).
|
|
192
|
+
* 4. `projectArg` matches a workspace store-chain child via resolveProjectRef
|
|
193
|
+
* → that absolute path.
|
|
194
|
+
* 5. Otherwise → throw with a hint listing
|
|
195
|
+
* the configured cross_project_links so the agent can self-correct.
|
|
196
|
+
*
|
|
197
|
+
* Errors are intentionally explicit rather than falling back silently — a
|
|
198
|
+
* misrouted write is far worse than a clean "unknown project" error.
|
|
199
|
+
*/
|
|
200
|
+
export function resolveProjectCwd(projectArg, currentCwd) {
|
|
201
|
+
if (!projectArg || projectArg.trim() === '')
|
|
202
|
+
return currentCwd;
|
|
203
|
+
const trimmed = projectArg.trim();
|
|
204
|
+
const baseCwd = path.resolve(currentCwd);
|
|
205
|
+
// Case 2: matches current project (by configured name OR by basename)
|
|
206
|
+
try {
|
|
207
|
+
const currentConfig = loadConfig(currentCwd);
|
|
208
|
+
if (currentConfig.project_name === trimmed)
|
|
209
|
+
return currentCwd;
|
|
210
|
+
}
|
|
211
|
+
catch { /* no config in current cwd — fall through */ }
|
|
212
|
+
if (path.basename(baseCwd) === trimmed)
|
|
213
|
+
return currentCwd;
|
|
214
|
+
// Case 3: matches a configured cross_project_link
|
|
215
|
+
const links = resolveCrossProjectLinks(currentCwd);
|
|
216
|
+
const linkMatch = links.find((l) => l.projectName === trimmed ||
|
|
217
|
+
l.name === trimmed ||
|
|
218
|
+
l.path === trimmed ||
|
|
219
|
+
l.absolutePath === trimmed ||
|
|
220
|
+
path.basename(l.absolutePath) === trimmed);
|
|
221
|
+
if (linkMatch) {
|
|
222
|
+
if (!linkMatch.available) {
|
|
223
|
+
throw new Error(`Cross-project link '${linkMatch.projectName}' is not available at ${linkMatch.absolutePath} ` +
|
|
224
|
+
`(target dir missing or not brainclaw-initialised).`);
|
|
225
|
+
}
|
|
226
|
+
return linkMatch.absolutePath;
|
|
227
|
+
}
|
|
228
|
+
// Case 4: matches a workspace store-chain child (monorepo-style nesting)
|
|
229
|
+
const wsHit = resolveProjectRef(trimmed, currentCwd);
|
|
230
|
+
if (wsHit)
|
|
231
|
+
return wsHit;
|
|
232
|
+
// Case 5: nothing matched — throw with helpful hint
|
|
233
|
+
const knownLinks = links.map((l) => l.projectName).join(', ') || '<none>';
|
|
234
|
+
throw new Error(`Unknown project: '${projectArg}'. Configured cross_project_links: ${knownLinks}. ` +
|
|
235
|
+
`Add one with \`brainclaw link add <path>\` or check config.yaml. ` +
|
|
236
|
+
`Workspace store-chain children are also accepted.`);
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Add a new cross_project_link entry to config.yaml.
|
|
240
|
+
*
|
|
241
|
+
* - Resolves a relative input path against `cwd` for the existence check, but
|
|
242
|
+
* stores it as-given (relative paths are friendly for shared configs).
|
|
243
|
+
* - Validates the target directory exists and is brainclaw-initialised.
|
|
244
|
+
* - Derives `name` from the linked project's project_name when possible,
|
|
245
|
+
* else from the basename of the resolved path.
|
|
246
|
+
* - Refuses duplicates by `name` or `path` unless `force: true`.
|
|
247
|
+
*/
|
|
248
|
+
export function addCrossProjectLink(input) {
|
|
249
|
+
const baseCwd = path.resolve(input.cwd ?? process.cwd());
|
|
250
|
+
const inputPath = input.path.trim();
|
|
251
|
+
if (!inputPath) {
|
|
252
|
+
throw new Error('path is required');
|
|
253
|
+
}
|
|
254
|
+
const absolutePath = path.isAbsolute(inputPath) ? inputPath : path.resolve(baseCwd, inputPath);
|
|
255
|
+
if (!fs.existsSync(absolutePath)) {
|
|
256
|
+
throw new Error(`Target path does not exist: ${absolutePath}`);
|
|
257
|
+
}
|
|
258
|
+
if (!memoryExists(absolutePath)) {
|
|
259
|
+
throw new Error(`Target is not brainclaw-initialised (no .brainclaw/ found): ${absolutePath}`);
|
|
260
|
+
}
|
|
261
|
+
let derivedName = input.name?.trim();
|
|
262
|
+
if (!derivedName) {
|
|
263
|
+
try {
|
|
264
|
+
derivedName = loadConfig(absolutePath).project_name;
|
|
265
|
+
}
|
|
266
|
+
catch { /* fall through to basename */ }
|
|
267
|
+
}
|
|
268
|
+
derivedName = derivedName ?? path.basename(absolutePath);
|
|
269
|
+
const config = loadConfig(input.cwd);
|
|
270
|
+
const existing = config.cross_project_links ?? [];
|
|
271
|
+
const conflict = existing.find((l) => l.name === derivedName ||
|
|
272
|
+
l.path === inputPath ||
|
|
273
|
+
path.resolve(baseCwd, l.path) === absolutePath);
|
|
274
|
+
if (conflict && !input.force) {
|
|
275
|
+
throw new Error(`Cross-project link already exists (name='${conflict.name ?? path.basename(conflict.path)}', path='${conflict.path}'). Use force: true to replace.`);
|
|
276
|
+
}
|
|
277
|
+
const link = CrossProjectLinkSchema.parse({
|
|
278
|
+
path: inputPath,
|
|
279
|
+
name: derivedName,
|
|
280
|
+
role: input.role ?? 'subscriber',
|
|
281
|
+
...(input.channels?.length ? { channels: input.channels } : {}),
|
|
282
|
+
});
|
|
283
|
+
const next = conflict
|
|
284
|
+
? existing.map((l) => (l === conflict ? link : l))
|
|
285
|
+
: [...existing, link];
|
|
286
|
+
saveConfig({ ...config, cross_project_links: next }, input.cwd);
|
|
287
|
+
return link;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Remove a cross_project_link entry from config.yaml.
|
|
291
|
+
*
|
|
292
|
+
* Matches by `name`, exact `path`, resolved absolute path, or basename of
|
|
293
|
+
* the resolved path — same matching rules as `resolveCrossProjectTarget`.
|
|
294
|
+
*/
|
|
295
|
+
export function removeCrossProjectLink(nameOrPath, cwd) {
|
|
296
|
+
const baseCwd = path.resolve(cwd ?? process.cwd());
|
|
297
|
+
const config = loadConfig(cwd);
|
|
298
|
+
const links = config.cross_project_links ?? [];
|
|
299
|
+
const match = links.find((l) => {
|
|
300
|
+
const abs = path.isAbsolute(l.path) ? l.path : path.resolve(baseCwd, l.path);
|
|
301
|
+
return l.name === nameOrPath
|
|
302
|
+
|| l.path === nameOrPath
|
|
303
|
+
|| abs === nameOrPath
|
|
304
|
+
|| path.basename(abs) === nameOrPath;
|
|
305
|
+
});
|
|
306
|
+
if (!match) {
|
|
307
|
+
throw new Error(`No cross_project_link found matching: '${nameOrPath}'`);
|
|
308
|
+
}
|
|
309
|
+
const next = links.filter((l) => l !== match);
|
|
310
|
+
saveConfig({ ...config, cross_project_links: next }, cwd);
|
|
311
|
+
return match;
|
|
312
|
+
}
|
|
99
313
|
//# sourceMappingURL=cross-project.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
name: doctor
|
|
2
|
+
description: Run diagnostics and fix issues automatically
|
|
3
|
+
trust_level: trusted
|
|
4
|
+
trigger: manual
|
|
5
|
+
prompt: >-
|
|
6
|
+
Run bclaw_doctor, analyze the findings, and fix what can be fixed
|
|
7
|
+
automatically. For issues requiring human judgment, create plans.
|
|
8
|
+
invoke: codex exec --full-auto "{prompt}"
|
|
9
|
+
tags:
|
|
10
|
+
- diagnostics
|
|
11
|
+
- built-in
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
name: janitor
|
|
2
|
+
description: Clean up stale claims, archive old notes, check for orphaned files
|
|
3
|
+
trust_level: contributor
|
|
4
|
+
trigger: manual
|
|
5
|
+
prompt: >-
|
|
6
|
+
Clean up this project: prune stale claims, archive old runtime notes, check
|
|
7
|
+
for orphaned files. Use bclaw_doctor, prune, and release-claims.
|
|
8
|
+
invoke: codex exec --full-auto "{prompt}"
|
|
9
|
+
tags:
|
|
10
|
+
- maintenance
|
|
11
|
+
- built-in
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
name: onboarder
|
|
2
|
+
description: Generate a project summary for new team members
|
|
3
|
+
trust_level: observer
|
|
4
|
+
trigger: manual
|
|
5
|
+
prompt: >-
|
|
6
|
+
Generate a project summary for a new team member. Read the board, plans,
|
|
7
|
+
decisions, constraints, and produce a concise onboarding document.
|
|
8
|
+
invoke: claude -p "{prompt}" --allowedTools "Edit,Write,Bash,Read,Glob,Grep"
|
|
9
|
+
tags:
|
|
10
|
+
- onboarding
|
|
11
|
+
- built-in
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
name: reviewer
|
|
2
|
+
description: Review pending candidates and give structured opinions
|
|
3
|
+
trust_level: trusted
|
|
4
|
+
trigger: manual
|
|
5
|
+
prompt: >-
|
|
6
|
+
Review pending candidates in this project. For each, give a structured
|
|
7
|
+
opinion (approve/reject with reasons). Use
|
|
8
|
+
bclaw_find(entity: "candidate", filter: {status: "pending"}) and
|
|
9
|
+
bclaw_transition(entity: "candidate", id, to: "accepted" | "rejected").
|
|
10
|
+
invoke: claude -p "{prompt}" --allowedTools "Edit,Write,Bash,Read,Glob,Grep"
|
|
11
|
+
tags:
|
|
12
|
+
- review
|
|
13
|
+
- built-in
|