brainclaw 0.28.0 → 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 +683 -23
- 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 +4244 -1475
- 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 +131 -10
- 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 +124 -0
- 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/bootstrap.js +61 -10
- 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 +454 -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/event-log.js +1 -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 +252 -28
- 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/repo-analysis.js +67 -0
- package/dist/core/reputation.js +9 -3
- package/dist/core/schema.js +546 -21
- 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 +54 -12
- 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
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { AssignmentSchema } from './schema.js';
|
|
3
|
+
import { resolveEntityDir } from './io.js';
|
|
4
|
+
import { mutate } from './mutation-pipeline.js';
|
|
5
|
+
import { nowISO, generateIdWithLabel } from './ids.js';
|
|
6
|
+
import { JsonStore } from './json-store.js';
|
|
7
|
+
import { appendAuditEntry } from './audit.js';
|
|
8
|
+
import { appendEvent } from './event-log.js';
|
|
9
|
+
import { createRuntimeEvent } from './events.js';
|
|
10
|
+
import { findLatestAgentRunForAssignment, recordAgentRunProgress, syncAgentRunFromAssignmentTransition } from './agentruns.js';
|
|
11
|
+
// ── Directory / Store ────────────────────────────────────────
|
|
12
|
+
function assignmentsDir(cwd, mode = 'read') {
|
|
13
|
+
return resolveEntityDir('assignments', cwd, mode);
|
|
14
|
+
}
|
|
15
|
+
function ensureAssignmentsDir(cwd) {
|
|
16
|
+
const dir = assignmentsDir(cwd, 'write');
|
|
17
|
+
if (!fs.existsSync(dir)) {
|
|
18
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function assignmentStore(cwd) {
|
|
22
|
+
return new JsonStore({
|
|
23
|
+
dirPath: assignmentsDir(cwd, 'read'),
|
|
24
|
+
documentType: 'assignment',
|
|
25
|
+
getId: (a) => a.id,
|
|
26
|
+
sort: (a, b) => a.created_at.localeCompare(b.created_at),
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
// ── CRUD ─────────────────────────────────────────────────────
|
|
30
|
+
export function saveAssignment(assignment, cwd) {
|
|
31
|
+
mutate({ cwd }, () => {
|
|
32
|
+
ensureAssignmentsDir(cwd);
|
|
33
|
+
const store = new JsonStore({
|
|
34
|
+
dirPath: assignmentsDir(cwd, 'write'),
|
|
35
|
+
documentType: 'assignment',
|
|
36
|
+
getId: (a) => a.id,
|
|
37
|
+
sort: (a, b) => a.created_at.localeCompare(b.created_at),
|
|
38
|
+
});
|
|
39
|
+
store.save(AssignmentSchema.parse(assignment));
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
export function loadAssignment(id, cwd) {
|
|
43
|
+
// JsonStore.load throws when the id is missing; honor the declared
|
|
44
|
+
// "| undefined" return type so callers (e.g. transitionAssignment)
|
|
45
|
+
// can emit their own 'Assignment not found' error with the right wording.
|
|
46
|
+
try {
|
|
47
|
+
return assignmentStore(cwd).load(id);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export function listAssignments(cwd, filter) {
|
|
54
|
+
let items = assignmentStore(cwd).list();
|
|
55
|
+
if (filter?.status)
|
|
56
|
+
items = items.filter((a) => a.status === filter.status);
|
|
57
|
+
if (filter?.agent)
|
|
58
|
+
items = items.filter((a) => a.agent === filter.agent);
|
|
59
|
+
if (filter?.claim_id)
|
|
60
|
+
items = items.filter((a) => a.claim_id === filter.claim_id);
|
|
61
|
+
if (filter?.plan_id)
|
|
62
|
+
items = items.filter((a) => a.plan_id === filter.plan_id);
|
|
63
|
+
if (filter?.sequence_id)
|
|
64
|
+
items = items.filter((a) => a.sequence_id === filter.sequence_id);
|
|
65
|
+
return items;
|
|
66
|
+
}
|
|
67
|
+
// ── ID Generation ────────────────────────────────────────────
|
|
68
|
+
export function generateAssignmentId(cwd) {
|
|
69
|
+
return generateIdWithLabel('assignments', cwd);
|
|
70
|
+
}
|
|
71
|
+
// ── Status FSM ───────────────────────────────────────────────
|
|
72
|
+
/** Valid transitions: from → Set<to>.
|
|
73
|
+
*
|
|
74
|
+
* `rerouted` is reachable from every non-terminal state (pln#451 / trp#61):
|
|
75
|
+
* rerouting a claim must close the predecessor assignment regardless of where
|
|
76
|
+
* it was in the FSM. Previously only `blocked` could reach `rerouted`, which
|
|
77
|
+
* left assignments stuck in `created` or `offered` when the coordinator
|
|
78
|
+
* rerouted a still-unstarted lane.
|
|
79
|
+
*/
|
|
80
|
+
const VALID_TRANSITIONS = new Map([
|
|
81
|
+
['created', new Set(['offered', 'rerouted'])],
|
|
82
|
+
['offered', new Set(['accepted', 'failed', 'expired', 'rerouted'])],
|
|
83
|
+
['accepted', new Set(['started', 'timed_out', 'rerouted'])],
|
|
84
|
+
['started', new Set(['completed', 'failed', 'blocked', 'timed_out', 'rerouted'])],
|
|
85
|
+
['failed', new Set(['retrying', 'rerouted'])],
|
|
86
|
+
['timed_out', new Set(['retrying', 'rerouted'])],
|
|
87
|
+
['retrying', new Set(['offered', 'rerouted'])],
|
|
88
|
+
['blocked', new Set(['rerouted', 'started', 'failed'])],
|
|
89
|
+
// Terminal: completed, expired, rerouted (no outgoing transitions)
|
|
90
|
+
]);
|
|
91
|
+
export function validateTransition(from, to) {
|
|
92
|
+
const allowed = VALID_TRANSITIONS.get(from);
|
|
93
|
+
if (!allowed || !allowed.has(to)) {
|
|
94
|
+
return { valid: false, reason: `Invalid transition: ${from} → ${to}` };
|
|
95
|
+
}
|
|
96
|
+
return { valid: true };
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Transition an assignment to a new status with FSM validation.
|
|
100
|
+
* Same-status transitions are idempotent no-ops (returns current state
|
|
101
|
+
* with idempotent=true instead of throwing). This handles network retries
|
|
102
|
+
* where a worker calls accepted/started again after a timeout.
|
|
103
|
+
* Updates relevant timestamps, emits event and audit entry.
|
|
104
|
+
*/
|
|
105
|
+
export function transitionAssignment(id, newStatus, options, cwd) {
|
|
106
|
+
const assignment = loadAssignment(id, cwd);
|
|
107
|
+
if (!assignment) {
|
|
108
|
+
throw new Error(`Assignment not found: ${id}`);
|
|
109
|
+
}
|
|
110
|
+
// Idempotent: same-status transition is a no-op (handles network retries)
|
|
111
|
+
if (assignment.status === newStatus) {
|
|
112
|
+
// Still update heartbeat for liveness tracking
|
|
113
|
+
assignment.last_heartbeat_at = nowISO();
|
|
114
|
+
assignment.updated_at = assignment.last_heartbeat_at;
|
|
115
|
+
saveAssignment(assignment, cwd);
|
|
116
|
+
return { assignment, previous_status: newStatus, idempotent: true };
|
|
117
|
+
}
|
|
118
|
+
const validation = validateTransition(assignment.status, newStatus);
|
|
119
|
+
if (!validation.valid) {
|
|
120
|
+
throw new Error(validation.reason);
|
|
121
|
+
}
|
|
122
|
+
const previous_status = assignment.status;
|
|
123
|
+
const now = nowISO();
|
|
124
|
+
// Update status
|
|
125
|
+
assignment.status = newStatus;
|
|
126
|
+
assignment.updated_at = now;
|
|
127
|
+
assignment.last_heartbeat_at = now;
|
|
128
|
+
if (options.status_reason)
|
|
129
|
+
assignment.status_reason = options.status_reason;
|
|
130
|
+
if (options.session_id)
|
|
131
|
+
assignment.session_id = options.session_id;
|
|
132
|
+
if (options.error_message)
|
|
133
|
+
assignment.error_message = options.error_message;
|
|
134
|
+
if (options.artifacts?.length) {
|
|
135
|
+
assignment.artifacts = [...assignment.artifacts, ...options.artifacts];
|
|
136
|
+
}
|
|
137
|
+
// Set transition-specific timestamps
|
|
138
|
+
switch (newStatus) {
|
|
139
|
+
case 'offered':
|
|
140
|
+
assignment.offered_at = now;
|
|
141
|
+
break;
|
|
142
|
+
case 'accepted':
|
|
143
|
+
assignment.accepted_at = now;
|
|
144
|
+
break;
|
|
145
|
+
case 'started':
|
|
146
|
+
assignment.started_at = now;
|
|
147
|
+
break;
|
|
148
|
+
case 'completed':
|
|
149
|
+
assignment.completed_at = now;
|
|
150
|
+
break;
|
|
151
|
+
case 'failed':
|
|
152
|
+
assignment.failed_at = now;
|
|
153
|
+
break;
|
|
154
|
+
case 'blocked':
|
|
155
|
+
assignment.blocked_at = now;
|
|
156
|
+
break;
|
|
157
|
+
case 'timed_out':
|
|
158
|
+
assignment.timed_out_at = now;
|
|
159
|
+
break;
|
|
160
|
+
case 'expired':
|
|
161
|
+
assignment.expired_at = now;
|
|
162
|
+
break;
|
|
163
|
+
case 'rerouted':
|
|
164
|
+
assignment.rerouted_at = now;
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
saveAssignment(assignment, cwd);
|
|
168
|
+
emitAssignmentEvent(assignment, `assignment_${newStatus}`, options.actor, cwd);
|
|
169
|
+
if (options.syncAgentRun !== false) {
|
|
170
|
+
try {
|
|
171
|
+
syncAgentRunFromAssignmentTransition(assignment, newStatus, {
|
|
172
|
+
actor: options.actor,
|
|
173
|
+
actor_id: options.actor_id,
|
|
174
|
+
session_id: options.session_id,
|
|
175
|
+
status_reason: options.status_reason,
|
|
176
|
+
artifacts: options.artifacts,
|
|
177
|
+
error_message: options.error_message,
|
|
178
|
+
}, cwd);
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
/* best-effort: run state should not break assignment lifecycle */
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
appendAuditEntry({
|
|
185
|
+
actor: options.actor ?? assignment.agent,
|
|
186
|
+
actor_id: options.actor_id,
|
|
187
|
+
action: 'update',
|
|
188
|
+
item_id: assignment.id,
|
|
189
|
+
item_type: 'assignment',
|
|
190
|
+
before: { status: previous_status },
|
|
191
|
+
after: { status: newStatus, reason: options.status_reason },
|
|
192
|
+
scope: assignment.scope,
|
|
193
|
+
session_id: options.session_id,
|
|
194
|
+
}, cwd);
|
|
195
|
+
return { assignment, previous_status };
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Record progress on a started assignment (heartbeat).
|
|
199
|
+
* Updates last_heartbeat_at without changing status.
|
|
200
|
+
*/
|
|
201
|
+
export function recordProgress(id, options, cwd) {
|
|
202
|
+
const assignment = loadAssignment(id, cwd);
|
|
203
|
+
if (!assignment) {
|
|
204
|
+
throw new Error(`Assignment not found: ${id}`);
|
|
205
|
+
}
|
|
206
|
+
if (assignment.status !== 'started') {
|
|
207
|
+
throw new Error(`Cannot record progress: assignment ${id} is ${assignment.status}, expected started`);
|
|
208
|
+
}
|
|
209
|
+
const now = nowISO();
|
|
210
|
+
assignment.last_heartbeat_at = now;
|
|
211
|
+
assignment.updated_at = now;
|
|
212
|
+
if (options.message)
|
|
213
|
+
assignment.status_reason = options.message;
|
|
214
|
+
if (options.artifacts?.length) {
|
|
215
|
+
assignment.artifacts = [...assignment.artifacts, ...options.artifacts];
|
|
216
|
+
}
|
|
217
|
+
saveAssignment(assignment, cwd);
|
|
218
|
+
emitAssignmentEvent(assignment, 'assignment_progress', options.actor, cwd);
|
|
219
|
+
try {
|
|
220
|
+
const latestRun = findLatestAgentRunForAssignment(assignment.id, cwd);
|
|
221
|
+
if (latestRun) {
|
|
222
|
+
recordAgentRunProgress(latestRun.id, {
|
|
223
|
+
message: options.message,
|
|
224
|
+
artifacts: options.artifacts,
|
|
225
|
+
actor: options.actor,
|
|
226
|
+
actor_id: options.actor_id,
|
|
227
|
+
session_id: options.session_id,
|
|
228
|
+
}, cwd);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
/* best-effort */
|
|
233
|
+
}
|
|
234
|
+
return assignment;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Create a new assignment. Called by the dispatcher after creating a claim
|
|
238
|
+
* and sending an inbox message.
|
|
239
|
+
*/
|
|
240
|
+
export function createAssignment(options, cwd) {
|
|
241
|
+
const generated = options.id ? undefined : generateAssignmentId(cwd);
|
|
242
|
+
const id = options.id ?? generated.id;
|
|
243
|
+
const short_label = options.short_label ?? generated.short_label;
|
|
244
|
+
const assignment = AssignmentSchema.parse({
|
|
245
|
+
schema_version: 1,
|
|
246
|
+
id,
|
|
247
|
+
short_label,
|
|
248
|
+
claim_id: options.claim_id,
|
|
249
|
+
message_id: options.message_id,
|
|
250
|
+
plan_id: options.plan_id,
|
|
251
|
+
sequence_id: options.sequence_id,
|
|
252
|
+
correlation_id: options.correlation_id,
|
|
253
|
+
agent: options.agent,
|
|
254
|
+
agent_id: options.agent_id,
|
|
255
|
+
dispatcher_agent: options.dispatcher_agent,
|
|
256
|
+
dispatcher_session_id: options.dispatcher_session_id,
|
|
257
|
+
scope: options.scope,
|
|
258
|
+
description: options.description,
|
|
259
|
+
lane: options.lane,
|
|
260
|
+
worktree_path: options.worktree_path,
|
|
261
|
+
status: 'created',
|
|
262
|
+
created_at: nowISO(),
|
|
263
|
+
heartbeat_ttl_ms: options.heartbeat_ttl_ms,
|
|
264
|
+
acceptance_ttl_ms: options.acceptance_ttl_ms,
|
|
265
|
+
max_retries: options.max_retries,
|
|
266
|
+
retry_count: 0,
|
|
267
|
+
artifacts: [],
|
|
268
|
+
tags: options.tags ?? [],
|
|
269
|
+
});
|
|
270
|
+
saveAssignment(assignment, cwd);
|
|
271
|
+
emitAssignmentEvent(assignment, 'assignment_created', options.dispatcher_agent, cwd);
|
|
272
|
+
appendAuditEntry({
|
|
273
|
+
actor: options.dispatcher_agent,
|
|
274
|
+
action: 'create',
|
|
275
|
+
item_id: assignment.id,
|
|
276
|
+
item_type: 'assignment',
|
|
277
|
+
after: { agent: options.agent, scope: options.scope, claim_id: options.claim_id },
|
|
278
|
+
}, cwd);
|
|
279
|
+
return assignment;
|
|
280
|
+
}
|
|
281
|
+
// ── Active Assignment Lookup ─────────────────────────────────
|
|
282
|
+
/** Statuses that indicate a finished assignment (no longer active). */
|
|
283
|
+
const TERMINAL_STATUSES = new Set(['completed', 'expired', 'rerouted']);
|
|
284
|
+
/**
|
|
285
|
+
* Return the most recently created non-terminal assignment for the given agent.
|
|
286
|
+
* When `claimId` is provided, it is used as a fast-path lookup before falling
|
|
287
|
+
* back to an agent-wide scan.
|
|
288
|
+
*/
|
|
289
|
+
export function getActiveAssignmentForAgent(agentId, cwd, claimId) {
|
|
290
|
+
if (claimId) {
|
|
291
|
+
const byClaim = listAssignments(cwd, { claim_id: claimId });
|
|
292
|
+
const active = byClaim.filter((a) => !TERMINAL_STATUSES.has(a.status));
|
|
293
|
+
// listAssignments returns ascending-by-created_at — last is most recent
|
|
294
|
+
if (active.length > 0)
|
|
295
|
+
return active[active.length - 1];
|
|
296
|
+
}
|
|
297
|
+
if (!agentId)
|
|
298
|
+
return undefined;
|
|
299
|
+
const all = listAssignments(cwd);
|
|
300
|
+
const active = all.filter((a) => a.agent_id === agentId && !TERMINAL_STATUSES.has(a.status));
|
|
301
|
+
return active.length > 0 ? active[active.length - 1] : undefined;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Bump `last_heartbeat_at` on the most recent active assignment for the given
|
|
305
|
+
* claim (or agent). Best-effort — throws are suppressed by the caller.
|
|
306
|
+
*/
|
|
307
|
+
export function bumpActiveAssignmentHeartbeat(claimId, agentId, cwd) {
|
|
308
|
+
const assignment = getActiveAssignmentForAgent(agentId ?? '', cwd, claimId);
|
|
309
|
+
if (!assignment)
|
|
310
|
+
return false;
|
|
311
|
+
const now = nowISO();
|
|
312
|
+
assignment.last_heartbeat_at = now;
|
|
313
|
+
assignment.updated_at = now;
|
|
314
|
+
saveAssignment(assignment, cwd);
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
317
|
+
// ── Post-creation patches ────────────────────────────────────
|
|
318
|
+
/** Attach the inbox message_id after the message has been sent (message is created after assignment). */
|
|
319
|
+
export function patchAssignmentMessageId(id, messageId, cwd) {
|
|
320
|
+
const assignment = loadAssignment(id, cwd);
|
|
321
|
+
if (!assignment)
|
|
322
|
+
return;
|
|
323
|
+
assignment.message_id = messageId;
|
|
324
|
+
saveAssignment(assignment, cwd);
|
|
325
|
+
}
|
|
326
|
+
// ── Event Emission ───────────────────────────────────────────
|
|
327
|
+
function emitAssignmentEvent(assignment, action, actor, cwd) {
|
|
328
|
+
const text = `${assignment.description} [${assignment.status}]${assignment.status_reason ? ` — ${assignment.status_reason}` : ''}`;
|
|
329
|
+
appendEvent({
|
|
330
|
+
ts: nowISO(),
|
|
331
|
+
agent: actor ?? assignment.agent,
|
|
332
|
+
agent_id: assignment.agent_id,
|
|
333
|
+
action: action,
|
|
334
|
+
item_type: 'assignment',
|
|
335
|
+
item_id: assignment.id,
|
|
336
|
+
summary: `${assignment.status}: ${assignment.description.slice(0, 80)}`,
|
|
337
|
+
}, cwd);
|
|
338
|
+
try {
|
|
339
|
+
createRuntimeEvent({
|
|
340
|
+
agent: actor ?? assignment.agent,
|
|
341
|
+
agent_id: assignment.agent_id,
|
|
342
|
+
project_id: undefined,
|
|
343
|
+
session_id: assignment.session_id,
|
|
344
|
+
event_type: action,
|
|
345
|
+
text,
|
|
346
|
+
tags: ['agent-runtime', 'assignment'],
|
|
347
|
+
assignment_id: assignment.id,
|
|
348
|
+
claim_id: assignment.claim_id,
|
|
349
|
+
message_id: assignment.message_id,
|
|
350
|
+
plan_id: assignment.plan_id,
|
|
351
|
+
sequence_id: assignment.sequence_id,
|
|
352
|
+
correlation_id: assignment.correlation_id,
|
|
353
|
+
scope: assignment.scope,
|
|
354
|
+
status: assignment.status,
|
|
355
|
+
status_reason: assignment.status_reason,
|
|
356
|
+
related_paths: [assignment.scope],
|
|
357
|
+
metadata: {
|
|
358
|
+
dispatcher_agent: assignment.dispatcher_agent,
|
|
359
|
+
protocol: 'brainclaw.agent_runtime.v0',
|
|
360
|
+
},
|
|
361
|
+
}, cwd);
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
/* best-effort: runtime event emission should not break assignment lifecycle */
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
//# sourceMappingURL=assignments.js.map
|
package/dist/core/audit.js
CHANGED
|
@@ -6,6 +6,7 @@ import { nowISO } from './ids.js';
|
|
|
6
6
|
import { logger } from './logger.js';
|
|
7
7
|
import { appendEvent } from './event-log.js';
|
|
8
8
|
const AUDIT_LOG_FILE = 'audit.log';
|
|
9
|
+
const MAX_AUDIT_LOG_BYTES = 10 * 1024 * 1024; // 10MB
|
|
9
10
|
/** Map audit actions to event-log actions (subset that overlaps) */
|
|
10
11
|
const AUDIT_TO_EVENT_ACTION = {
|
|
11
12
|
create: 'create',
|
|
@@ -18,6 +19,8 @@ const AUDIT_TO_EVENT_ACTION = {
|
|
|
18
19
|
session_start: 'session_start',
|
|
19
20
|
session_end: 'session_end',
|
|
20
21
|
rollback: 'rollback',
|
|
22
|
+
promote_direct: 'create',
|
|
23
|
+
trust_change: 'update',
|
|
21
24
|
};
|
|
22
25
|
function auditLogPath(cwd) {
|
|
23
26
|
return path.join(memoryDir(cwd), AUDIT_LOG_FILE);
|
|
@@ -25,6 +28,7 @@ function auditLogPath(cwd) {
|
|
|
25
28
|
export function appendAuditEntry(entry, cwd) {
|
|
26
29
|
try {
|
|
27
30
|
mutate({ cwd }, () => {
|
|
31
|
+
rotateAuditLogIfNeeded(cwd);
|
|
28
32
|
const full = {
|
|
29
33
|
timestamp: nowISO(),
|
|
30
34
|
actor: entry.actor,
|
|
@@ -35,6 +39,10 @@ export function appendAuditEntry(entry, cwd) {
|
|
|
35
39
|
after: entry.after,
|
|
36
40
|
actor_id: entry.actor_id,
|
|
37
41
|
reason: entry.reason,
|
|
42
|
+
scope: entry.scope,
|
|
43
|
+
session_id: entry.session_id,
|
|
44
|
+
host_id: entry.host_id,
|
|
45
|
+
actor_session: entry.actor_session,
|
|
38
46
|
};
|
|
39
47
|
const line = JSON.stringify(Object.fromEntries(Object.entries(full).filter(([, v]) => v !== undefined)));
|
|
40
48
|
fs.appendFileSync(auditLogPath(cwd), line + '\n', 'utf-8');
|
|
@@ -83,4 +91,26 @@ export function readAuditLog(options = {}, cwd) {
|
|
|
83
91
|
}
|
|
84
92
|
return entries;
|
|
85
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Rotate audit.log if it exceeds MAX_AUDIT_LOG_BYTES.
|
|
96
|
+
* Archives to audit.{timestamp}.log in the same directory.
|
|
97
|
+
*/
|
|
98
|
+
export function rotateAuditLogIfNeeded(cwd) {
|
|
99
|
+
const logPath = auditLogPath(cwd);
|
|
100
|
+
if (!fs.existsSync(logPath))
|
|
101
|
+
return false;
|
|
102
|
+
try {
|
|
103
|
+
const stat = fs.statSync(logPath);
|
|
104
|
+
if (stat.size < MAX_AUDIT_LOG_BYTES)
|
|
105
|
+
return false;
|
|
106
|
+
const archiveName = `audit.${Date.now()}.log`;
|
|
107
|
+
const archivePath = path.join(memoryDir(cwd), archiveName);
|
|
108
|
+
fs.renameSync(logPath, archivePath);
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
logger.debug('Failed to rotate audit log:', err);
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
86
116
|
//# sourceMappingURL=audit.js.map
|
package/dist/core/bootstrap.js
CHANGED
|
@@ -8,7 +8,7 @@ import { resolveEntityDir } from './io.js';
|
|
|
8
8
|
import { mutate } from './mutation-pipeline.js';
|
|
9
9
|
import { BootstrapApplicationReceiptSchema, BootstrapInterviewAnswerSchema, BootstrapInterviewPlanSchema, BootstrapInterviewQuestionSchema, BootstrapImportPlanDocumentSchema, BootstrapProfileDocumentSchema, BootstrapSuggestionDocumentSchema, MemorySeedDocumentSchema, } from './schema.js';
|
|
10
10
|
import { loadVersionedJsonFile, saveVersionedJsonFile } from './migration.js';
|
|
11
|
-
import { analyzeRepository } from './repo-analysis.js';
|
|
11
|
+
import { analyzeRepository, findNestedAgentsFiles } from './repo-analysis.js';
|
|
12
12
|
import { buildExecutionContext, compactExecutionContext } from './execution-context.js';
|
|
13
13
|
import { buildAgentToolingContext } from './agent-context.js';
|
|
14
14
|
import { createInstruction, loadInstructions, saveInstruction } from './instructions.js';
|
|
@@ -101,6 +101,7 @@ export function runBootstrapProfile(options = {}) {
|
|
|
101
101
|
importPlan,
|
|
102
102
|
lastApplication,
|
|
103
103
|
reusedProfile: false,
|
|
104
|
+
subProjects: artifacts.subProjects,
|
|
104
105
|
};
|
|
105
106
|
}
|
|
106
107
|
export function listBootstrapSeeds(cwd) {
|
|
@@ -183,6 +184,18 @@ export function renderBootstrapSummary(result) {
|
|
|
183
184
|
if (result.lastApplication && !result.lastApplication.uninstalled_at) {
|
|
184
185
|
lines.push(`Last bootstrap import: ${result.lastApplication.managed_artifacts.length} managed artifact(s) from ${result.lastApplication.applied_at}`);
|
|
185
186
|
}
|
|
187
|
+
if (result.subProjects && result.subProjects.length > 0) {
|
|
188
|
+
lines.push('');
|
|
189
|
+
lines.push(`Sub-projects discovered (${result.subProjects.length}):`);
|
|
190
|
+
for (const sp of result.subProjects.slice(0, 20)) {
|
|
191
|
+
lines.push(` ${sp}`);
|
|
192
|
+
}
|
|
193
|
+
if (result.subProjects.length > 20) {
|
|
194
|
+
lines.push(` ... and ${result.subProjects.length - 20} more`);
|
|
195
|
+
}
|
|
196
|
+
lines.push('');
|
|
197
|
+
lines.push('Use: brainclaw bootstrap --for <sub-project-path> --refresh');
|
|
198
|
+
}
|
|
186
199
|
if (result.importPlan.suggestions.length > 0) {
|
|
187
200
|
lines.push('');
|
|
188
201
|
lines.push('Import proposal:');
|
|
@@ -231,14 +244,19 @@ export function renderBootstrapInterview(result, audience = 'any') {
|
|
|
231
244
|
function buildBootstrapArtifacts(input) {
|
|
232
245
|
const sourcesScanned = [];
|
|
233
246
|
const seeds = [];
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
247
|
+
// When target is an absolute directory path, use it as the scan root
|
|
248
|
+
// so that instruction files, README, and AGENTS.md are discovered from
|
|
249
|
+
// the target scope — not from the workspace root. This fixes bootstrap
|
|
250
|
+
// returning wrong signals for monorepo sub-projects.
|
|
251
|
+
const scanRoot = resolveBootstrapScanRoot(input.cwd, input.target);
|
|
252
|
+
const workspace = classifyWorkspace(scanRoot);
|
|
253
|
+
const nativeInstructionFiles = discoverNativeInstructionFiles(scanRoot);
|
|
254
|
+
const readmePath = findFirstExisting(scanRoot, README_CANDIDATES);
|
|
237
255
|
if (readmePath) {
|
|
238
256
|
sourcesScanned.push('README');
|
|
239
257
|
seeds.push(...extractReadmeSeeds(readmePath, input.target));
|
|
240
258
|
}
|
|
241
|
-
const agentsPath = path.join(
|
|
259
|
+
const agentsPath = path.join(scanRoot, 'AGENTS.md');
|
|
242
260
|
const agentsPresent = fs.existsSync(agentsPath);
|
|
243
261
|
if (agentsPresent) {
|
|
244
262
|
sourcesScanned.push('AGENTS.md');
|
|
@@ -246,9 +264,9 @@ function buildBootstrapArtifacts(input) {
|
|
|
246
264
|
}
|
|
247
265
|
if (nativeInstructionFiles.length > 0) {
|
|
248
266
|
sourcesScanned.push('native_instructions');
|
|
249
|
-
seeds.push(...extractNativeInstructionSeeds(nativeInstructionFiles.map((relativePath) => path.join(
|
|
267
|
+
seeds.push(...extractNativeInstructionSeeds(nativeInstructionFiles.map((relativePath) => path.join(scanRoot, relativePath)), scanRoot, input.target));
|
|
250
268
|
}
|
|
251
|
-
const manifestResult = extractManifestSeeds(
|
|
269
|
+
const manifestResult = extractManifestSeeds(scanRoot, input.target);
|
|
252
270
|
if (manifestResult.seeds.length > 0) {
|
|
253
271
|
sourcesScanned.push(...manifestResult.sources);
|
|
254
272
|
seeds.push(...manifestResult.seeds);
|
|
@@ -265,16 +283,16 @@ function buildBootstrapArtifacts(input) {
|
|
|
265
283
|
sourcesScanned.push('local_mcp');
|
|
266
284
|
seeds.push(...extractMcpSeeds(agentTooling.mcp_servers, input.target));
|
|
267
285
|
}
|
|
268
|
-
const repoAnalysis = analyzeRepository(
|
|
286
|
+
const repoAnalysis = analyzeRepository(scanRoot);
|
|
269
287
|
sourcesScanned.push('repo-analysis');
|
|
270
288
|
seeds.push(...extractRepoAnalysisSeeds(repoAnalysis, input.target));
|
|
271
289
|
// Additional brownfield sources (step 12)
|
|
272
|
-
const additionalSeeds = extractAdditionalBrownfieldSeeds(
|
|
290
|
+
const additionalSeeds = extractAdditionalBrownfieldSeeds(scanRoot, input.target);
|
|
273
291
|
if (additionalSeeds.seeds.length > 0) {
|
|
274
292
|
sourcesScanned.push(...additionalSeeds.sources);
|
|
275
293
|
seeds.push(...additionalSeeds.seeds);
|
|
276
294
|
}
|
|
277
|
-
const gitProbe = probeGit(
|
|
295
|
+
const gitProbe = probeGit(scanRoot, input.target);
|
|
278
296
|
if (gitProbe.available) {
|
|
279
297
|
sourcesScanned.push('git');
|
|
280
298
|
seeds.push(...gitProbe.hotspotSeeds);
|
|
@@ -342,6 +360,13 @@ function buildBootstrapArtifacts(input) {
|
|
|
342
360
|
schema_version: DERIVED_SCHEMA_VERSION,
|
|
343
361
|
})),
|
|
344
362
|
importPlan,
|
|
363
|
+
// For multi-project workspaces without a target, list discovered sub-projects
|
|
364
|
+
subProjects: (!input.target && repoAnalysis.recommendedMode === 'multi-project')
|
|
365
|
+
? findNestedAgentsFiles(scanRoot, 8)
|
|
366
|
+
.filter((p) => p !== 'AGENTS.md') // exclude root AGENTS.md
|
|
367
|
+
.map((p) => path.dirname(p))
|
|
368
|
+
.filter((d) => d !== '.')
|
|
369
|
+
: undefined,
|
|
345
370
|
};
|
|
346
371
|
}
|
|
347
372
|
function extractReadmeSeeds(filepath, target) {
|
|
@@ -846,6 +871,9 @@ function buildSummary(input) {
|
|
|
846
871
|
parts.push(`Onboarding mode: ${input.onboardingMode}.`);
|
|
847
872
|
parts.push(`Confidence: ${input.confidence}.`);
|
|
848
873
|
parts.push(`Repository mode looks ${input.repoAnalysis.recommendedMode}.`);
|
|
874
|
+
if (input.repoAnalysis.recommendedMode === 'multi-project' && !input.target) {
|
|
875
|
+
parts.push('This is a multi-project workspace — use --for <path> to bootstrap a specific sub-project.');
|
|
876
|
+
}
|
|
849
877
|
if (input.agentsPresent) {
|
|
850
878
|
parts.push('AGENTS.md detected and summarized.');
|
|
851
879
|
}
|
|
@@ -1639,6 +1667,29 @@ function normalizeTarget(target) {
|
|
|
1639
1667
|
const trimmed = target?.trim();
|
|
1640
1668
|
return trimmed && trimmed.length > 0 ? trimmed : undefined;
|
|
1641
1669
|
}
|
|
1670
|
+
/**
|
|
1671
|
+
* Resolve where to scan for project files (README, AGENTS.md, manifests).
|
|
1672
|
+
* If target is an absolute directory path, scan from there.
|
|
1673
|
+
* Otherwise fall back to cwd (the workspace/store root).
|
|
1674
|
+
*/
|
|
1675
|
+
function resolveBootstrapScanRoot(cwd, target) {
|
|
1676
|
+
if (!target)
|
|
1677
|
+
return cwd;
|
|
1678
|
+
const resolved = path.isAbsolute(target) ? target : path.resolve(cwd, target);
|
|
1679
|
+
try {
|
|
1680
|
+
if (fs.statSync(resolved).isDirectory())
|
|
1681
|
+
return resolved;
|
|
1682
|
+
}
|
|
1683
|
+
catch { /* not a directory or doesn't exist */ }
|
|
1684
|
+
// Target is a file path or glob — use its parent directory if it exists
|
|
1685
|
+
const parent = path.dirname(resolved);
|
|
1686
|
+
try {
|
|
1687
|
+
if (fs.statSync(parent).isDirectory())
|
|
1688
|
+
return parent;
|
|
1689
|
+
}
|
|
1690
|
+
catch { /* fall back to cwd */ }
|
|
1691
|
+
return cwd;
|
|
1692
|
+
}
|
|
1642
1693
|
// ─── Step 12: Additional brownfield sources ──────────────────────────────────
|
|
1643
1694
|
const CI_WORKFLOW_DIRS = ['.github/workflows', '.gitlab'];
|
|
1644
1695
|
const CI_FILES = ['.gitlab-ci.yml', 'Jenkinsfile', '.circleci/config.yml'];
|