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,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inter-agent messaging operations.
|
|
3
|
+
*
|
|
4
|
+
* Messages are stored per-recipient agent in:
|
|
5
|
+
* .brainclaw/coordination/inbox/{agent_name}/{msg_id}.json
|
|
6
|
+
*
|
|
7
|
+
* No console.log, no process.exit, no MCP formatting.
|
|
8
|
+
* Both CLI commands and MCP handlers call these functions.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { generateIdWithLabel, nowISO } from './ids.js';
|
|
15
|
+
import { memoryDir } from './io.js';
|
|
16
|
+
import { mutate } from './mutation-pipeline.js';
|
|
17
|
+
import { loadVersionedJsonFile, saveVersionedJsonFile } from './migration.js';
|
|
18
|
+
import { InboxMessageSchema } from './schema.js';
|
|
19
|
+
import { commitMemoryChange } from './memory-git.js';
|
|
20
|
+
import { resolveAgentAlias } from './agent-capability.js';
|
|
21
|
+
// ── Paths ───────────────────────────────────────────────────
|
|
22
|
+
function inboxDir(cwd) {
|
|
23
|
+
return path.join(memoryDir(cwd), 'coordination', 'inbox');
|
|
24
|
+
}
|
|
25
|
+
function agentInboxDir(agent, cwd) {
|
|
26
|
+
// Resolve aliases (e.g. 'copilot' → 'github-copilot') then normalise for filesystem
|
|
27
|
+
const canonical = resolveAgentAlias(agent.toLowerCase());
|
|
28
|
+
const safe = canonical.replace(/[^a-z0-9_-]/g, '_');
|
|
29
|
+
return path.join(inboxDir(cwd), safe);
|
|
30
|
+
}
|
|
31
|
+
function ensureAgentInboxDir(agent, cwd) {
|
|
32
|
+
const dir = agentInboxDir(agent, cwd);
|
|
33
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
34
|
+
return dir;
|
|
35
|
+
}
|
|
36
|
+
// ── Load helpers ────────────────────────────────────────────
|
|
37
|
+
function loadMessagesFromDir(dirPath) {
|
|
38
|
+
if (!fs.existsSync(dirPath))
|
|
39
|
+
return [];
|
|
40
|
+
const files = fs.readdirSync(dirPath).filter(f => f.endsWith('.json'));
|
|
41
|
+
const items = [];
|
|
42
|
+
for (const file of files) {
|
|
43
|
+
try {
|
|
44
|
+
const result = loadVersionedJsonFile('message', path.join(dirPath, file));
|
|
45
|
+
items.push(InboxMessageSchema.parse(result.document));
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// skip invalid files
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return items.sort((a, b) => a.created_at.localeCompare(b.created_at));
|
|
52
|
+
}
|
|
53
|
+
export function sendMessage(input, cwd) {
|
|
54
|
+
return mutate({ cwd }, () => {
|
|
55
|
+
const { id, short_label } = generateIdWithLabel('inbox_messages', cwd);
|
|
56
|
+
const timestamp = nowISO();
|
|
57
|
+
const resolvedTo = resolveAgentAlias(input.to);
|
|
58
|
+
const message = {
|
|
59
|
+
id,
|
|
60
|
+
short_label,
|
|
61
|
+
from: input.from,
|
|
62
|
+
to: resolvedTo,
|
|
63
|
+
type: input.type,
|
|
64
|
+
text: input.text,
|
|
65
|
+
ref: input.ref,
|
|
66
|
+
payload: input.payload,
|
|
67
|
+
scope: input.scope,
|
|
68
|
+
requires_ack: input.requires_ack ?? false,
|
|
69
|
+
thread_id: input.thread_id,
|
|
70
|
+
status: 'pending',
|
|
71
|
+
created_at: timestamp,
|
|
72
|
+
updated_at: timestamp,
|
|
73
|
+
author: input.from,
|
|
74
|
+
author_id: input.author_id,
|
|
75
|
+
model: input.model,
|
|
76
|
+
project_id: input.project_id,
|
|
77
|
+
host_id: input.host_id,
|
|
78
|
+
session_id: input.session_id,
|
|
79
|
+
claim_id: input.claim_id,
|
|
80
|
+
assignment_id: input.assignment_id,
|
|
81
|
+
tags: input.tags ?? [],
|
|
82
|
+
};
|
|
83
|
+
const dir = ensureAgentInboxDir(input.to, cwd);
|
|
84
|
+
saveVersionedJsonFile('message', path.join(dir, `${id}.json`), message);
|
|
85
|
+
commitMemoryChange(`message ${id} sent to ${resolvedTo}`, cwd);
|
|
86
|
+
return { id, shortLabel: short_label, to: resolvedTo, type: input.type };
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/** Apply all inbox filters (status, type, thread_id, claim_id) to a message list. */
|
|
90
|
+
function applyInboxFilters(messages, input) {
|
|
91
|
+
let filtered = messages;
|
|
92
|
+
if (input.status)
|
|
93
|
+
filtered = filtered.filter(m => m.status === input.status);
|
|
94
|
+
if (input.type)
|
|
95
|
+
filtered = filtered.filter(m => m.type === input.type);
|
|
96
|
+
if (input.thread_id)
|
|
97
|
+
filtered = filtered.filter(m => m.thread_id === input.thread_id);
|
|
98
|
+
if (input.claimId) {
|
|
99
|
+
filtered = filtered.filter(m => m.claim_id === input.claimId ||
|
|
100
|
+
m.payload?.claim_id === input.claimId);
|
|
101
|
+
}
|
|
102
|
+
return filtered;
|
|
103
|
+
}
|
|
104
|
+
export function readInbox(input, cwd) {
|
|
105
|
+
const dir = agentInboxDir(input.agent, cwd);
|
|
106
|
+
// If markAsRead, do everything under a single lock to avoid race conditions.
|
|
107
|
+
// Re-read from disk inside the lock to get fresh state.
|
|
108
|
+
if (input.markAsRead) {
|
|
109
|
+
return mutate({ cwd }, () => {
|
|
110
|
+
// Fresh read inside lock
|
|
111
|
+
let messages = applyInboxFilters(loadMessagesFromDir(dir), input);
|
|
112
|
+
const total = messages.length;
|
|
113
|
+
const offset = input.offset ?? 0;
|
|
114
|
+
const limit = input.limit ?? 20;
|
|
115
|
+
const page = messages.slice(offset, offset + limit);
|
|
116
|
+
const timestamp = nowISO();
|
|
117
|
+
for (const msg of page) {
|
|
118
|
+
if (msg.status === 'pending') {
|
|
119
|
+
msg.status = 'read';
|
|
120
|
+
msg.read_at = timestamp;
|
|
121
|
+
msg.updated_at = timestamp;
|
|
122
|
+
saveVersionedJsonFile('message', path.join(dir, `${msg.id}.json`), msg);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
commitMemoryChange(`inbox read by ${input.agent}`, cwd);
|
|
126
|
+
return { total, offset, limit, messages: page };
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
// Read-only path: no lock needed
|
|
130
|
+
let messages = applyInboxFilters(loadMessagesFromDir(dir), input);
|
|
131
|
+
const total = messages.length;
|
|
132
|
+
const offset = input.offset ?? 0;
|
|
133
|
+
const limit = input.limit ?? 20;
|
|
134
|
+
const page = messages.slice(offset, offset + limit);
|
|
135
|
+
return { total, offset, limit, messages: page };
|
|
136
|
+
}
|
|
137
|
+
export function ackMessage(messageId, agent, cwd) {
|
|
138
|
+
return mutate({ cwd }, () => {
|
|
139
|
+
const dir = agentInboxDir(agent, cwd);
|
|
140
|
+
const messages = loadMessagesFromDir(dir);
|
|
141
|
+
const msg = messages.find(m => m.id === messageId || m.short_label === messageId);
|
|
142
|
+
if (!msg) {
|
|
143
|
+
throw new Error(`Message '${messageId}' not found in ${agent}'s inbox.`);
|
|
144
|
+
}
|
|
145
|
+
const timestamp = nowISO();
|
|
146
|
+
msg.status = 'acknowledged';
|
|
147
|
+
msg.ack_at = timestamp;
|
|
148
|
+
msg.updated_at = timestamp;
|
|
149
|
+
saveVersionedJsonFile('message', path.join(dir, `${msg.id}.json`), msg);
|
|
150
|
+
commitMemoryChange(`message ${msg.id} acknowledged by ${agent}`, cwd);
|
|
151
|
+
return { id: msg.id, status: msg.status };
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
// ── Archive Message ─────────────────────────────────────────
|
|
155
|
+
export function archiveMessage(messageId, agent, cwd) {
|
|
156
|
+
return mutate({ cwd }, () => {
|
|
157
|
+
const dir = agentInboxDir(agent, cwd);
|
|
158
|
+
const messages = loadMessagesFromDir(dir);
|
|
159
|
+
const msg = messages.find(m => m.id === messageId || m.short_label === messageId);
|
|
160
|
+
if (!msg) {
|
|
161
|
+
throw new Error(`Message '${messageId}' not found in ${agent}'s inbox.`);
|
|
162
|
+
}
|
|
163
|
+
msg.status = 'archived';
|
|
164
|
+
msg.updated_at = nowISO();
|
|
165
|
+
saveVersionedJsonFile('message', path.join(dir, `${msg.id}.json`), msg);
|
|
166
|
+
commitMemoryChange(`message ${msg.id} archived by ${agent}`, cwd);
|
|
167
|
+
return { id: msg.id, status: msg.status };
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
// ── Get Thread ──────────────────────────────────────────────
|
|
171
|
+
export function getThread(threadId, cwd, options) {
|
|
172
|
+
// Search across all agent inboxes for messages in this thread
|
|
173
|
+
const baseDir = inboxDir(cwd);
|
|
174
|
+
if (!fs.existsSync(baseDir))
|
|
175
|
+
return [];
|
|
176
|
+
const agents = fs.readdirSync(baseDir).filter(f => {
|
|
177
|
+
try {
|
|
178
|
+
return fs.statSync(path.join(baseDir, f)).isDirectory();
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
const allMessages = [];
|
|
185
|
+
for (const agent of agents) {
|
|
186
|
+
const agentDir = path.join(baseDir, agent);
|
|
187
|
+
const messages = loadMessagesFromDir(agentDir);
|
|
188
|
+
allMessages.push(...messages.filter(m => m.thread_id === threadId));
|
|
189
|
+
}
|
|
190
|
+
const sorted = allMessages.sort((a, b) => a.created_at.localeCompare(b.created_at));
|
|
191
|
+
if (options?.truncateText) {
|
|
192
|
+
const limit = options.truncateText;
|
|
193
|
+
for (const msg of sorted) {
|
|
194
|
+
if (msg.text && msg.text.length > limit) {
|
|
195
|
+
msg.text = msg.text.slice(0, limit) + '\n[...truncated]';
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return sorted;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Lightweight thread message count — reads file content as raw strings
|
|
203
|
+
* and checks for the thread_id without full JSON parsing. Used for polling
|
|
204
|
+
* to avoid OOM from repeated full-message loads.
|
|
205
|
+
*/
|
|
206
|
+
export function getThreadCount(threadId, cwd) {
|
|
207
|
+
const baseDir = inboxDir(cwd);
|
|
208
|
+
if (!fs.existsSync(baseDir))
|
|
209
|
+
return 0;
|
|
210
|
+
let agents;
|
|
211
|
+
try {
|
|
212
|
+
agents = fs.readdirSync(baseDir).filter(f => {
|
|
213
|
+
try {
|
|
214
|
+
return fs.statSync(path.join(baseDir, f)).isDirectory();
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
return 0;
|
|
223
|
+
}
|
|
224
|
+
let count = 0;
|
|
225
|
+
const needle = `"thread_id":"${threadId}"`;
|
|
226
|
+
const needleSpaced = `"thread_id": "${threadId}"`;
|
|
227
|
+
for (const agent of agents) {
|
|
228
|
+
const agentDir = path.join(baseDir, agent);
|
|
229
|
+
let files;
|
|
230
|
+
try {
|
|
231
|
+
files = fs.readdirSync(agentDir).filter(f => f.endsWith('.json'));
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
for (const file of files) {
|
|
237
|
+
try {
|
|
238
|
+
const raw = fs.readFileSync(path.join(agentDir, file), 'utf-8');
|
|
239
|
+
if (raw.includes(needle) || raw.includes(needleSpaced)) {
|
|
240
|
+
count++;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
// skip unreadable files
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return count;
|
|
249
|
+
}
|
|
250
|
+
// ── Count Pending ───────────────────────────────────────────
|
|
251
|
+
export function countPending(agent, cwd) {
|
|
252
|
+
const dir = agentInboxDir(agent, cwd);
|
|
253
|
+
const messages = loadMessagesFromDir(dir);
|
|
254
|
+
return messages.filter(m => m.status === 'pending').length;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Count actionable messages: pending + read-but-requires-ack (not yet acknowledged).
|
|
258
|
+
* This is the correct metric for board/session_start — a message that has been
|
|
259
|
+
* read but not acked is still actionable.
|
|
260
|
+
*/
|
|
261
|
+
export function countActionable(agent, cwd) {
|
|
262
|
+
const dir = agentInboxDir(agent, cwd);
|
|
263
|
+
const messages = loadMessagesFromDir(dir);
|
|
264
|
+
return messages.filter(m => m.status === 'pending' ||
|
|
265
|
+
(m.status === 'read' && m.requires_ack)).length;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Check if there's already a non-archived assign message for a given plan+agent combo.
|
|
269
|
+
* Used by dispatcher to avoid duplicate assignments.
|
|
270
|
+
*/
|
|
271
|
+
export function hasActiveAssignment(agent, planId, cwd) {
|
|
272
|
+
const dir = agentInboxDir(agent, cwd);
|
|
273
|
+
const messages = loadMessagesFromDir(dir);
|
|
274
|
+
return messages.some(m => m.type === 'assign' &&
|
|
275
|
+
m.ref === planId &&
|
|
276
|
+
m.status !== 'archived');
|
|
277
|
+
}
|
|
278
|
+
//# sourceMappingURL=messaging.js.map
|
package/dist/core/migration.js
CHANGED
|
@@ -2,7 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import YAML from 'yaml';
|
|
4
4
|
import { memoryDir, memoryPath, readFileSync, writeFileAtomic, resolveEntityDir } from './io.js';
|
|
5
|
-
import { BootstrapApplicationReceiptSchema, BootstrapImportPlanDocumentSchema, AgentIdentityDocumentSchema, BootstrapProfileDocumentSchema, CandidateSchema, ClaimSchema, ConfigSchema, MemorySeedDocumentSchema, ConstraintSchema, CurrentSessionStateSchema, DecisionSchema, HandoffSchema, InstructionEntrySchema, PlanItemSchema, ProjectIdentityDocumentSchema, RuntimeNoteSchema, SessionSnapshotSchema, TrapSchema, AiSurfaceTaskRequestSchema, ProjectCapabilitySchema, ProjectToolSchema, } from './schema.js';
|
|
5
|
+
import { BootstrapApplicationReceiptSchema, BootstrapImportPlanDocumentSchema, AgentIdentityDocumentSchema, BootstrapProfileDocumentSchema, CandidateSchema, ClaimSchema, ConfigSchema, MemorySeedDocumentSchema, ConstraintSchema, CurrentSessionStateSchema, DecisionSchema, HandoffSchema, InstructionEntrySchema, PlanItemSchema, SequenceSchema, ProjectIdentityDocumentSchema, RuntimeNoteSchema, SessionSnapshotSchema, TrapSchema, AiSurfaceTaskRequestSchema, ProjectCapabilitySchema, ProjectToolSchema, InboxMessageSchema, AssignmentSchema, AgentRunSchema, ActionRequiredSchema, } from './schema.js';
|
|
6
6
|
export class MigrationError extends Error {
|
|
7
7
|
kind;
|
|
8
8
|
documentType;
|
|
@@ -14,6 +14,7 @@ export class MigrationError extends Error {
|
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
const CURRENT_SCHEMA_VERSION = 2;
|
|
17
|
+
const NON_MESSAGE_INBOX_SUBDIRS = new Set(['accepted', 'rejected', 'cross-project']);
|
|
17
18
|
const registry = {
|
|
18
19
|
agent_identity: createRegistryEntry(AgentIdentityDocumentSchema),
|
|
19
20
|
bootstrap_application: createRegistryEntry(BootstrapApplicationReceiptSchema),
|
|
@@ -29,6 +30,7 @@ const registry = {
|
|
|
29
30
|
instruction: createRegistryEntry(InstructionEntrySchema),
|
|
30
31
|
memory_seed: createRegistryEntry(MemorySeedDocumentSchema),
|
|
31
32
|
plan: createRegistryEntry(PlanItemSchema),
|
|
33
|
+
sequence: createRegistryEntry(SequenceSchema),
|
|
32
34
|
project_identity: createRegistryEntry(ProjectIdentityDocumentSchema),
|
|
33
35
|
runtime_note: createRegistryEntry(RuntimeNoteSchema),
|
|
34
36
|
ai_surface_task: createRegistryEntry(AiSurfaceTaskRequestSchema),
|
|
@@ -36,6 +38,10 @@ const registry = {
|
|
|
36
38
|
capability: createRegistryEntry(ProjectCapabilitySchema),
|
|
37
39
|
tool: createRegistryEntry(ProjectToolSchema),
|
|
38
40
|
trap: createRegistryEntry(TrapSchema),
|
|
41
|
+
message: createRegistryEntry(InboxMessageSchema),
|
|
42
|
+
assignment: createRegistryEntry(AssignmentSchema),
|
|
43
|
+
agent_run: createRegistryEntry(AgentRunSchema),
|
|
44
|
+
action_required: createRegistryEntry(ActionRequiredSchema),
|
|
39
45
|
};
|
|
40
46
|
function createRegistryEntry(schema) {
|
|
41
47
|
return {
|
|
@@ -160,7 +166,9 @@ export function scanMigrationStatus(cwd) {
|
|
|
160
166
|
collectDirectory(entries, resolveEntityDir('traps-private', effectiveCwd, 'read'), 'trap', true);
|
|
161
167
|
collectDirectory(entries, resolveEntityDir('handoffs', effectiveCwd, 'read'), 'handoff');
|
|
162
168
|
collectDirectory(entries, resolveEntityDir('plans', effectiveCwd, 'read'), 'plan');
|
|
169
|
+
collectDirectory(entries, resolveEntityDir('sequences', effectiveCwd, 'read'), 'sequence');
|
|
163
170
|
collectDirectory(entries, resolveEntityDir('inbox', effectiveCwd, 'read'), 'candidate');
|
|
171
|
+
collectInboxMessages(entries, resolveEntityDir('inbox', effectiveCwd, 'read'));
|
|
164
172
|
collectDirectory(entries, resolveEntityDir('inbox/accepted', effectiveCwd, 'read'), 'candidate');
|
|
165
173
|
collectDirectory(entries, resolveEntityDir('inbox/rejected', effectiveCwd, 'read'), 'candidate');
|
|
166
174
|
collectDirectory(entries, resolveEntityDir('claims', effectiveCwd, 'read'), 'claim');
|
|
@@ -168,6 +176,8 @@ export function scanMigrationStatus(cwd) {
|
|
|
168
176
|
collectDirectory(entries, resolveEntityDir('runtime-hosts', effectiveCwd, 'read'), 'runtime_note', true);
|
|
169
177
|
collectDirectory(entries, resolveEntityDir('runtime-private', effectiveCwd, 'read'), 'runtime_note', true);
|
|
170
178
|
collectDirectory(entries, resolveEntityDir('surface-tasks', effectiveCwd, 'read'), 'ai_surface_task');
|
|
179
|
+
collectDirectory(entries, resolveEntityDir('runs', effectiveCwd, 'read'), 'agent_run');
|
|
180
|
+
collectDirectory(entries, resolveEntityDir('actions', effectiveCwd, 'read'), 'action_required');
|
|
171
181
|
collectDirectory(entries, resolveEntityDir('instructions', effectiveCwd, 'read'), 'instruction');
|
|
172
182
|
collectDirectory(entries, path.join(resolveEntityDir('bootstrap', effectiveCwd, 'read'), 'seeds'), 'memory_seed');
|
|
173
183
|
collectDirectory(entries, resolveEntityDir('agents', effectiveCwd, 'read'), 'agent_identity');
|
|
@@ -188,6 +198,27 @@ function collectDirectory(entries, dirPath, documentType, recursive = false) {
|
|
|
188
198
|
entries.push(buildCheckEntry(filepath, documentType));
|
|
189
199
|
}
|
|
190
200
|
}
|
|
201
|
+
function collectInboxMessages(entries, inboxRoot) {
|
|
202
|
+
if (!fs.existsSync(inboxRoot)) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
for (const entry of fs.readdirSync(inboxRoot).sort()) {
|
|
206
|
+
const fullPath = path.join(inboxRoot, entry);
|
|
207
|
+
let stat;
|
|
208
|
+
try {
|
|
209
|
+
stat = fs.statSync(fullPath);
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (!stat.isDirectory() || NON_MESSAGE_INBOX_SUBDIRS.has(entry)) {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
for (const filepath of listJsonFiles(fullPath, true)) {
|
|
218
|
+
entries.push(buildCheckEntry(filepath, 'message'));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
191
222
|
function listJsonFiles(dirPath, recursive) {
|
|
192
223
|
if (!fs.existsSync(dirPath)) {
|
|
193
224
|
return [];
|
|
@@ -19,11 +19,13 @@ export function mutate(optionsOrFn, maybeFn) {
|
|
|
19
19
|
const fn = typeof optionsOrFn === 'function' ? optionsOrFn : maybeFn;
|
|
20
20
|
const cwd = options.cwd ?? process.cwd();
|
|
21
21
|
const timeoutMs = options.timeoutMs ?? STORE_LOCK_TIMEOUT_MS;
|
|
22
|
-
ensureMemoryDir(cwd, options.preferredDirName);
|
|
23
22
|
const lockTarget = storeLockPath(cwd, options.preferredDirName);
|
|
24
23
|
const start = performance.now();
|
|
25
24
|
try {
|
|
26
|
-
const value = withLock(lockTarget, () =>
|
|
25
|
+
const value = withLock(lockTarget, () => {
|
|
26
|
+
ensureMemoryDir(cwd, options.preferredDirName);
|
|
27
|
+
return fn(cwd);
|
|
28
|
+
}, timeoutMs);
|
|
27
29
|
const durationMs = performance.now() - start;
|
|
28
30
|
if (durationMs > 1_000) {
|
|
29
31
|
logger.debug(`Slow mutation: ${durationMs.toFixed(0)}ms (cwd=${cwd})`);
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure business logic for updating and deleting memory items across store chains.
|
|
3
|
+
*
|
|
4
|
+
* Handles constraint, decision, and trap mutations with store chain resolution.
|
|
5
|
+
* No console.log, no process.exit, no MCP formatting.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
import { loadState, persistState } from '../state.js';
|
|
10
|
+
import { resolveStoreChain, resolveTargetStore } from '../store-resolution.js';
|
|
11
|
+
/**
|
|
12
|
+
* Walk the store chain to find a memory item by type and id.
|
|
13
|
+
* Searches by both id and short_label.
|
|
14
|
+
*/
|
|
15
|
+
export function findMemoryItemInChain(itemId, itemType, cwd) {
|
|
16
|
+
const chain = resolveStoreChain(cwd);
|
|
17
|
+
for (const store of chain) {
|
|
18
|
+
const state = loadState(store.cwd);
|
|
19
|
+
let item;
|
|
20
|
+
if (itemType === 'constraint') {
|
|
21
|
+
item = state.active_constraints.find((c) => c.id === itemId || c.short_label === itemId);
|
|
22
|
+
}
|
|
23
|
+
else if (itemType === 'decision') {
|
|
24
|
+
item = state.recent_decisions.find((d) => d.id === itemId || d.short_label === itemId);
|
|
25
|
+
}
|
|
26
|
+
else if (itemType === 'trap') {
|
|
27
|
+
item = state.known_traps.find((t) => t.id === itemId || t.short_label === itemId);
|
|
28
|
+
}
|
|
29
|
+
if (item) {
|
|
30
|
+
return { item, store, itemType };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
export function deleteMemoryItem(itemId, itemType, cwd) {
|
|
36
|
+
const found = findMemoryItemInChain(itemId, itemType, cwd);
|
|
37
|
+
if (!found) {
|
|
38
|
+
throw new Error(`${itemType} with id '${itemId}' not found in any store`);
|
|
39
|
+
}
|
|
40
|
+
const state = loadState(found.store.cwd);
|
|
41
|
+
if (itemType === 'constraint') {
|
|
42
|
+
state.active_constraints = state.active_constraints.filter((c) => c.id !== itemId && c.short_label !== itemId);
|
|
43
|
+
}
|
|
44
|
+
else if (itemType === 'decision') {
|
|
45
|
+
state.recent_decisions = state.recent_decisions.filter((d) => d.id !== itemId && d.short_label !== itemId);
|
|
46
|
+
}
|
|
47
|
+
else if (itemType === 'trap') {
|
|
48
|
+
state.known_traps = state.known_traps.filter((t) => t.id !== itemId && t.short_label !== itemId);
|
|
49
|
+
}
|
|
50
|
+
persistState(state, found.store.cwd);
|
|
51
|
+
return {
|
|
52
|
+
deletedId: itemId,
|
|
53
|
+
itemType,
|
|
54
|
+
storeRole: found.store.role,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export function updateMemoryItem(input, cwd) {
|
|
58
|
+
const found = findMemoryItemInChain(input.id, input.type, cwd);
|
|
59
|
+
if (!found) {
|
|
60
|
+
throw new Error(`${input.type} with id '${input.id}' not found in any store`);
|
|
61
|
+
}
|
|
62
|
+
const { item, store: sourceStore } = found;
|
|
63
|
+
const previousStore = sourceStore.role;
|
|
64
|
+
// Apply field updates
|
|
65
|
+
if (input.text)
|
|
66
|
+
item.text = input.text;
|
|
67
|
+
if (input.tags)
|
|
68
|
+
item.tags = input.tags;
|
|
69
|
+
if (input.status && input.type === 'trap') {
|
|
70
|
+
item.status = input.status;
|
|
71
|
+
}
|
|
72
|
+
if (input.patch) {
|
|
73
|
+
Object.assign(item, input.patch);
|
|
74
|
+
}
|
|
75
|
+
if (input.moveToStore) {
|
|
76
|
+
const targetCwd = resolveTargetStore(cwd, input.moveToStore);
|
|
77
|
+
// Delete from source
|
|
78
|
+
const sourceState = loadState(sourceStore.cwd);
|
|
79
|
+
if (input.type === 'constraint') {
|
|
80
|
+
sourceState.active_constraints = sourceState.active_constraints.filter((c) => c.id !== input.id);
|
|
81
|
+
}
|
|
82
|
+
else if (input.type === 'decision') {
|
|
83
|
+
sourceState.recent_decisions = sourceState.recent_decisions.filter((d) => d.id !== input.id);
|
|
84
|
+
}
|
|
85
|
+
else if (input.type === 'trap') {
|
|
86
|
+
sourceState.known_traps = sourceState.known_traps.filter((t) => t.id !== input.id);
|
|
87
|
+
}
|
|
88
|
+
persistState(sourceState, sourceStore.cwd);
|
|
89
|
+
// Add to target
|
|
90
|
+
const targetState = loadState(targetCwd);
|
|
91
|
+
if (input.type === 'constraint') {
|
|
92
|
+
targetState.active_constraints.push(item);
|
|
93
|
+
}
|
|
94
|
+
else if (input.type === 'decision') {
|
|
95
|
+
targetState.recent_decisions.push(item);
|
|
96
|
+
}
|
|
97
|
+
else if (input.type === 'trap') {
|
|
98
|
+
targetState.known_traps.push(item);
|
|
99
|
+
}
|
|
100
|
+
persistState(targetState, targetCwd);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
// Update in place
|
|
104
|
+
const state = loadState(sourceStore.cwd);
|
|
105
|
+
if (input.type === 'constraint') {
|
|
106
|
+
const idx = state.active_constraints.findIndex((c) => c.id === input.id);
|
|
107
|
+
if (idx >= 0)
|
|
108
|
+
state.active_constraints[idx] = item;
|
|
109
|
+
}
|
|
110
|
+
else if (input.type === 'decision') {
|
|
111
|
+
const idx = state.recent_decisions.findIndex((d) => d.id === input.id);
|
|
112
|
+
if (idx >= 0)
|
|
113
|
+
state.recent_decisions[idx] = item;
|
|
114
|
+
}
|
|
115
|
+
else if (input.type === 'trap') {
|
|
116
|
+
const idx = state.known_traps.findIndex((t) => t.id === input.id);
|
|
117
|
+
if (idx >= 0)
|
|
118
|
+
state.known_traps[idx] = item;
|
|
119
|
+
}
|
|
120
|
+
persistState(state, sourceStore.cwd);
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
updatedId: input.id,
|
|
124
|
+
itemType: input.type,
|
|
125
|
+
previousStore,
|
|
126
|
+
newStore: input.moveToStore,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=memory-mutation.js.map
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure business logic for creating memory items (decision, constraint, trap).
|
|
3
|
+
*
|
|
4
|
+
* No console.log, no process.exit, no MCP formatting.
|
|
5
|
+
* Both CLI commands and MCP handlers call these functions.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
import { mutateState } from '../state.js';
|
|
10
|
+
import { generateIdWithLabel, nowISO } from '../ids.js';
|
|
11
|
+
import { generateTrapIdWithLabel, saveOperationalTrap } from '../traps.js';
|
|
12
|
+
export function createDecision(input, cwd) {
|
|
13
|
+
const result = mutateState((state) => {
|
|
14
|
+
const { id, short_label } = generateIdWithLabel('recent_decisions', cwd);
|
|
15
|
+
const entry = {
|
|
16
|
+
id,
|
|
17
|
+
short_label,
|
|
18
|
+
text: input.text,
|
|
19
|
+
created_at: nowISO(),
|
|
20
|
+
author: input.author,
|
|
21
|
+
outcome: input.outcome,
|
|
22
|
+
tags: input.tags ?? [],
|
|
23
|
+
related_paths: input.relatedPaths,
|
|
24
|
+
plan_id: input.planId,
|
|
25
|
+
};
|
|
26
|
+
state.recent_decisions.push(entry);
|
|
27
|
+
return { id, shortLabel: short_label, text: input.text };
|
|
28
|
+
}, cwd);
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
export function createConstraint(input, cwd) {
|
|
32
|
+
const result = mutateState((state) => {
|
|
33
|
+
const { id, short_label } = generateIdWithLabel('active_constraints', cwd);
|
|
34
|
+
const entry = {
|
|
35
|
+
id,
|
|
36
|
+
short_label,
|
|
37
|
+
text: input.text,
|
|
38
|
+
created_at: nowISO(),
|
|
39
|
+
author: input.author,
|
|
40
|
+
status: 'active',
|
|
41
|
+
category: input.category,
|
|
42
|
+
tags: input.tags ?? [],
|
|
43
|
+
related_paths: input.relatedPaths,
|
|
44
|
+
};
|
|
45
|
+
state.active_constraints.push(entry);
|
|
46
|
+
return { id, shortLabel: short_label, text: input.text };
|
|
47
|
+
}, cwd);
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
export function createTrap(input, cwd) {
|
|
51
|
+
const visibility = input.visibility ?? 'shared';
|
|
52
|
+
const { id, short_label } = generateTrapIdWithLabel();
|
|
53
|
+
const entry = {
|
|
54
|
+
id,
|
|
55
|
+
short_label,
|
|
56
|
+
text: input.text,
|
|
57
|
+
created_at: nowISO(),
|
|
58
|
+
author: input.author,
|
|
59
|
+
status: input.status ?? 'active',
|
|
60
|
+
severity: input.severity ?? 'medium',
|
|
61
|
+
tags: input.tags ?? [],
|
|
62
|
+
related_paths: input.relatedPaths,
|
|
63
|
+
plan_id: input.planId,
|
|
64
|
+
visibility,
|
|
65
|
+
host_id: input.hostId,
|
|
66
|
+
expires_at: input.expiresAt,
|
|
67
|
+
};
|
|
68
|
+
if (visibility === 'shared') {
|
|
69
|
+
mutateState((state) => {
|
|
70
|
+
state.known_traps.push(entry);
|
|
71
|
+
}, cwd);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
saveOperationalTrap(entry, cwd);
|
|
75
|
+
}
|
|
76
|
+
return { id, shortLabel: short_label, text: input.text, visibility, hostId: input.hostId };
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=memory-write.js.map
|