brainclaw 0.19.6 → 0.19.7
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/dist/commands/accept.js +102 -104
- package/dist/commands/add-step.js +3 -5
- package/dist/commands/capability.js +3 -5
- package/dist/commands/claim.js +14 -12
- package/dist/commands/complete-step.js +3 -5
- package/dist/commands/constraint.js +3 -5
- package/dist/commands/decision.js +3 -6
- package/dist/commands/delete-plan.js +3 -5
- package/dist/commands/handoff.js +3 -5
- package/dist/commands/instruction.js +16 -9
- package/dist/commands/mcp.js +11 -21
- package/dist/commands/memory.js +4 -7
- package/dist/commands/plan.js +3 -5
- package/dist/commands/prune.js +27 -25
- package/dist/commands/reflect.js +3 -5
- package/dist/commands/release-claim.js +23 -20
- package/dist/commands/release-claims.js +22 -21
- package/dist/commands/rollback.js +2 -2
- package/dist/commands/tool.js +3 -5
- package/dist/commands/trap.js +3 -5
- package/dist/commands/update-handoff.js +3 -5
- package/dist/commands/update-plan.js +3 -5
- package/dist/commands/upgrade.js +2 -2
- package/dist/core/audit.js +25 -25
- package/dist/core/candidates.js +10 -6
- package/dist/core/claims.js +10 -8
- package/dist/core/coordination.js +2 -2
- package/dist/core/instructions.js +4 -2
- package/dist/core/io.js +8 -0
- package/dist/core/lock.js +18 -2
- package/dist/core/runtime.js +18 -14
- package/dist/core/state.js +21 -4
- package/package.json +7 -8
package/dist/commands/prune.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { loadState, saveState } from '../core/state.js';
|
|
2
|
-
import { memoryExists } from '../core/io.js';
|
|
2
|
+
import { memoryExists, memoryPath, withStoreLock, writeFileAtomic } from '../core/io.js';
|
|
3
|
+
import { generateMarkdown } from '../core/markdown.js';
|
|
3
4
|
import { deleteRuntimeNote, listRuntimeNotes } from '../core/runtime.js';
|
|
4
5
|
import { expireStaleActiveClaims } from '../core/claims.js';
|
|
5
6
|
export function runPrune(options = {}) {
|
|
@@ -8,36 +9,37 @@ export function runPrune(options = {}) {
|
|
|
8
9
|
console.error('Project memory not initialized. Run `brainclaw init` first.');
|
|
9
10
|
process.exit(1);
|
|
10
11
|
}
|
|
11
|
-
const state = loadState(cwd);
|
|
12
12
|
const now = new Date().toISOString();
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
for (const c of state.active_constraints) {
|
|
16
|
-
if (c.status === 'active' && c.expires_at && c.expires_at < now) {
|
|
17
|
-
c.status = 'expired';
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
// Filter out constraints that have been expired for a while to keep memory clean
|
|
21
|
-
// For the MVP, we just remove them if they are expired
|
|
22
|
-
state.active_constraints = state.active_constraints.filter(c => c.status !== 'expired');
|
|
23
|
-
const prunedCount = originalLength - state.active_constraints.length;
|
|
24
|
-
saveState(state, cwd);
|
|
25
|
-
const expiredClaimsCount = expireStaleActiveClaims(cwd);
|
|
13
|
+
let prunedCount = 0;
|
|
14
|
+
let expiredClaimsCount = 0;
|
|
26
15
|
let expiredNotesCount = 0;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
for (const
|
|
31
|
-
if (
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
16
|
+
withStoreLock(cwd, () => {
|
|
17
|
+
const state = loadState(cwd);
|
|
18
|
+
const originalLength = state.active_constraints.length;
|
|
19
|
+
for (const c of state.active_constraints) {
|
|
20
|
+
if (c.status === 'active' && c.expires_at && c.expires_at < now) {
|
|
21
|
+
c.status = 'expired';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
state.active_constraints = state.active_constraints.filter(c => c.status !== 'expired');
|
|
25
|
+
prunedCount = originalLength - state.active_constraints.length;
|
|
26
|
+
saveState(state, cwd);
|
|
27
|
+
expiredClaimsCount = expireStaleActiveClaims(cwd);
|
|
28
|
+
if (options.expired) {
|
|
29
|
+
const notes = listRuntimeNotes(undefined, cwd);
|
|
30
|
+
for (const note of notes) {
|
|
31
|
+
if (note.expires_at && note.expires_at < now) {
|
|
32
|
+
try {
|
|
33
|
+
if (deleteRuntimeNote(note, cwd)) {
|
|
34
|
+
expiredNotesCount++;
|
|
35
|
+
}
|
|
35
36
|
}
|
|
37
|
+
catch { /* ignore */ }
|
|
36
38
|
}
|
|
37
|
-
catch { /* ignore */ }
|
|
38
39
|
}
|
|
39
40
|
}
|
|
40
|
-
|
|
41
|
+
writeFileAtomic(memoryPath('project.md', cwd), generateMarkdown(loadState(cwd), cwd));
|
|
42
|
+
});
|
|
41
43
|
if (options.expired) {
|
|
42
44
|
console.log(`✔ Pruned ${prunedCount} expired constraints, ${expiredNotesCount} expired runtime notes, ${expiredClaimsCount} expired claims.`);
|
|
43
45
|
}
|
package/dist/commands/reflect.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
-
import { memoryExists
|
|
2
|
+
import { memoryExists } from '../core/io.js';
|
|
3
3
|
import { loadConfig } from '../core/config.js';
|
|
4
4
|
import { buildOperationalIdentity } from '../core/identity.js';
|
|
5
|
-
import { loadState,
|
|
6
|
-
import { generateMarkdown } from '../core/markdown.js';
|
|
5
|
+
import { loadState, persistState } from '../core/state.js';
|
|
7
6
|
import { scanText } from '../core/security.js';
|
|
8
7
|
import { nowISO, generateIdWithLabel } from '../core/ids.js';
|
|
9
8
|
import { saveCandidate, generateCandidateIdWithLabel, listCandidates } from '../core/candidates.js';
|
|
@@ -265,8 +264,7 @@ function promoteCandidateToState(candidate, cwd) {
|
|
|
265
264
|
break;
|
|
266
265
|
}
|
|
267
266
|
}
|
|
268
|
-
|
|
269
|
-
writeFileAtomic(memoryPath('project.md', cwd), generateMarkdown(state));
|
|
267
|
+
persistState(state, cwd);
|
|
270
268
|
return promotedItemId;
|
|
271
269
|
}
|
|
272
270
|
export function mapEventTypeToCandidateType(eventType) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { memoryExists } from '../core/io.js';
|
|
2
|
-
import { memoryPath, writeFileAtomic } from '../core/io.js';
|
|
2
|
+
import { memoryPath, withStoreLock, writeFileAtomic } from '../core/io.js';
|
|
3
3
|
import { loadClaim, listClaims, releaseClaim } from '../core/claims.js';
|
|
4
4
|
import { generateMarkdown } from '../core/markdown.js';
|
|
5
5
|
import { loadState, saveState } from '../core/state.js';
|
|
@@ -9,27 +9,30 @@ export function runReleaseClaim(id, options = {}) {
|
|
|
9
9
|
process.exit(1);
|
|
10
10
|
}
|
|
11
11
|
try {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
if (
|
|
18
|
-
const
|
|
19
|
-
if (
|
|
20
|
-
|
|
12
|
+
let claim = loadClaim(id, options.cwd);
|
|
13
|
+
withStoreLock(options.cwd, () => {
|
|
14
|
+
const existing = loadClaim(id, options.cwd);
|
|
15
|
+
claim = releaseClaim(id, options.cwd);
|
|
16
|
+
let state = loadState(options.cwd);
|
|
17
|
+
if (existing.plan_id) {
|
|
18
|
+
const plan = state.plan_items.find((item) => item.id === existing.plan_id);
|
|
19
|
+
if (plan) {
|
|
20
|
+
const otherActiveClaims = listClaims(options.cwd).filter((item) => item.status === 'active' && item.plan_id === existing.plan_id);
|
|
21
|
+
if (options.planStatus) {
|
|
22
|
+
plan.status = options.planStatus;
|
|
23
|
+
}
|
|
24
|
+
else if (otherActiveClaims.length === 0 && plan.status === 'in_progress') {
|
|
25
|
+
plan.status = 'todo';
|
|
26
|
+
}
|
|
27
|
+
if (otherActiveClaims.length === 0 && plan.assignee === existing.agent) {
|
|
28
|
+
plan.assignee = undefined;
|
|
29
|
+
}
|
|
30
|
+
plan.updated_at = new Date().toISOString();
|
|
31
|
+
saveState(state, options.cwd);
|
|
21
32
|
}
|
|
22
|
-
else if (otherActiveClaims.length === 0 && plan.status === 'in_progress') {
|
|
23
|
-
plan.status = 'todo';
|
|
24
|
-
}
|
|
25
|
-
if (otherActiveClaims.length === 0 && plan.assignee === existing.agent) {
|
|
26
|
-
plan.assignee = undefined;
|
|
27
|
-
}
|
|
28
|
-
plan.updated_at = new Date().toISOString();
|
|
29
|
-
saveState(state, options.cwd);
|
|
30
33
|
}
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
writeFileAtomic(memoryPath('project.md', options.cwd), generateMarkdown(state, options.cwd));
|
|
35
|
+
});
|
|
33
36
|
console.log(`✔ Claim [${id}] released (was: ${claim.agent} → ${claim.scope})`);
|
|
34
37
|
}
|
|
35
38
|
catch (e) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execSync } from 'node:child_process';
|
|
2
|
-
import { memoryExists, memoryPath, writeFileAtomic } from '../core/io.js';
|
|
2
|
+
import { memoryExists, memoryPath, withStoreLock, writeFileAtomic } from '../core/io.js';
|
|
3
3
|
import { listClaims, releaseClaim } from '../core/claims.js';
|
|
4
4
|
import { generateMarkdown } from '../core/markdown.js';
|
|
5
5
|
import { loadState, saveState } from '../core/state.js';
|
|
@@ -35,31 +35,32 @@ export function runReleaseClaims(options = {}) {
|
|
|
35
35
|
const toRelease = claims.filter((c) => changedFiles.some((f) => scopeMatchesFile(c.scope, f)));
|
|
36
36
|
if (toRelease.length === 0)
|
|
37
37
|
process.exit(0);
|
|
38
|
-
let state = loadState(options.cwd);
|
|
39
38
|
let released = 0;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
plan.status
|
|
52
|
-
|
|
53
|
-
|
|
39
|
+
withStoreLock(options.cwd, () => {
|
|
40
|
+
let state = loadState(options.cwd);
|
|
41
|
+
for (const claim of toRelease) {
|
|
42
|
+
try {
|
|
43
|
+
releaseClaim(claim.id, options.cwd);
|
|
44
|
+
released++;
|
|
45
|
+
if (claim.plan_id) {
|
|
46
|
+
state = loadState(options.cwd);
|
|
47
|
+
const plan = state.plan_items.find((p) => p.id === claim.plan_id);
|
|
48
|
+
if (plan) {
|
|
49
|
+
const remaining = listClaims(options.cwd).filter((c) => c.status === 'active' && c.plan_id === claim.plan_id);
|
|
50
|
+
if (remaining.length === 0 && plan.status === 'in_progress') {
|
|
51
|
+
plan.status = 'todo';
|
|
52
|
+
plan.updated_at = new Date().toISOString();
|
|
53
|
+
saveState(state, options.cwd);
|
|
54
|
+
}
|
|
54
55
|
}
|
|
55
56
|
}
|
|
57
|
+
console.log(`✔ Auto-released claim [${claim.id}]: ${claim.scope}`);
|
|
56
58
|
}
|
|
57
|
-
|
|
59
|
+
catch { /* skip individual failures */ }
|
|
58
60
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
writeFileAtomic(memoryPath('project.md', options.cwd), generateMarkdown(state, options.cwd));
|
|
61
|
+
state = loadState(options.cwd);
|
|
62
|
+
writeFileAtomic(memoryPath('project.md', options.cwd), generateMarkdown(state, options.cwd));
|
|
63
|
+
});
|
|
63
64
|
if (released > 0) {
|
|
64
65
|
console.log(`brainclaw: ${released} claim(s) auto-released after merge.`);
|
|
65
66
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { memoryExists } from '../core/io.js';
|
|
2
2
|
import { readAuditLog } from '../core/audit.js';
|
|
3
|
-
import { loadState,
|
|
3
|
+
import { loadState, persistState } from '../core/state.js';
|
|
4
4
|
import { loadCandidate, saveCandidate } from '../core/candidates.js';
|
|
5
5
|
import { appendAuditEntry } from '../core/audit.js';
|
|
6
6
|
import { buildOperationalIdentity } from '../core/identity.js';
|
|
@@ -128,7 +128,7 @@ function applyRollback(entry, actorName, options) {
|
|
|
128
128
|
state.known_traps.push(before);
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
|
-
|
|
131
|
+
persistState(state);
|
|
132
132
|
}
|
|
133
133
|
else {
|
|
134
134
|
return { ok: false, message: `Rollback not supported for item_type '${item_type}'.` };
|
package/dist/commands/tool.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { loadState,
|
|
1
|
+
import { loadState, persistState } from '../core/state.js';
|
|
2
2
|
import { resolveCurrentAgentName } from '../core/agent-registry.js';
|
|
3
|
-
import { memoryExists
|
|
4
|
-
import { generateMarkdown } from '../core/markdown.js';
|
|
3
|
+
import { memoryExists } from '../core/io.js';
|
|
5
4
|
import { generateIdWithLabel, nowISO } from '../core/ids.js';
|
|
6
5
|
import { loadConfig } from '../core/config.js';
|
|
7
6
|
import { scanText } from '../core/security.js';
|
|
@@ -89,8 +88,7 @@ function runToolAdd(name, description, options, cwd) {
|
|
|
89
88
|
// For now, store as decision to avoid schema migration
|
|
90
89
|
// Will migrate to separate tool storage in v0.16
|
|
91
90
|
state.recent_decisions.push(entry);
|
|
92
|
-
|
|
93
|
-
writeFileAtomic(memoryPath('project.md', cwd), generateMarkdown(state));
|
|
91
|
+
persistState(state, cwd);
|
|
94
92
|
console.log(`✔ Tool added: [${id}] ${name}`);
|
|
95
93
|
console.log(' (Stored in decisions for now; will move to dedicated registry in v0.16)');
|
|
96
94
|
}
|
package/dist/commands/trap.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { loadState,
|
|
1
|
+
import { loadState, persistState } from '../core/state.js';
|
|
2
2
|
import { resolveCurrentAgentName } from '../core/agent-registry.js';
|
|
3
3
|
import { resolveCurrentHostId } from '../core/host.js';
|
|
4
4
|
import { loadConfig } from '../core/config.js';
|
|
5
|
-
import { generateMarkdown } from '../core/markdown.js';
|
|
6
5
|
import { nowISO } from '../core/ids.js';
|
|
7
6
|
import { scanText } from '../core/security.js';
|
|
8
|
-
import { memoryExists
|
|
7
|
+
import { memoryExists } from '../core/io.js';
|
|
9
8
|
import { generateTrapIdWithLabel, saveOperationalTrap } from '../core/traps.js';
|
|
10
9
|
import { validateCliInput, validateCliTtl } from '../core/input-validation.js';
|
|
11
10
|
import { resolveTargetStore } from '../core/store-resolution.js';
|
|
@@ -49,8 +48,7 @@ export function runTrap(text, options = {}) {
|
|
|
49
48
|
};
|
|
50
49
|
if (visibility === 'shared') {
|
|
51
50
|
state.known_traps.push(entry);
|
|
52
|
-
|
|
53
|
-
writeFileAtomic(memoryPath('project.md', cwd), generateMarkdown(state, cwd));
|
|
51
|
+
persistState(state, cwd);
|
|
54
52
|
}
|
|
55
53
|
else {
|
|
56
54
|
saveOperationalTrap(entry, cwd);
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { loadState,
|
|
2
|
-
import { memoryExists
|
|
3
|
-
import { generateMarkdown } from '../core/markdown.js';
|
|
1
|
+
import { loadState, persistState } from '../core/state.js';
|
|
2
|
+
import { memoryExists } from '../core/io.js';
|
|
4
3
|
export function runUpdateHandoff(id, options = {}) {
|
|
5
4
|
if (!memoryExists()) {
|
|
6
5
|
console.error('Error: .brainclaw/ not found. Run `brainclaw init` first.');
|
|
@@ -16,8 +15,7 @@ export function runUpdateHandoff(id, options = {}) {
|
|
|
16
15
|
handoff.status = options.status;
|
|
17
16
|
if (options.to !== undefined)
|
|
18
17
|
handoff.to = options.to;
|
|
19
|
-
|
|
20
|
-
writeFileAtomic(memoryPath('project.md'), generateMarkdown(state));
|
|
18
|
+
persistState(state);
|
|
21
19
|
console.log(`✔ Handoff updated: [${handoff.id}] ${handoff.from} → ${handoff.to} (${handoff.status})`);
|
|
22
20
|
}
|
|
23
21
|
//# sourceMappingURL=update-handoff.js.map
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { loadState,
|
|
2
|
-
import { memoryExists
|
|
3
|
-
import { generateMarkdown } from '../core/markdown.js';
|
|
1
|
+
import { loadState, persistState } from '../core/state.js';
|
|
2
|
+
import { memoryExists } from '../core/io.js';
|
|
4
3
|
import { nowISO } from '../core/ids.js';
|
|
5
4
|
export function runUpdatePlan(id, options = {}) {
|
|
6
5
|
if (!memoryExists(options.cwd)) {
|
|
@@ -30,8 +29,7 @@ export function runUpdatePlan(id, options = {}) {
|
|
|
30
29
|
if (options.actualEffort)
|
|
31
30
|
plan.actual_effort = options.actualEffort;
|
|
32
31
|
plan.updated_at = timestamp;
|
|
33
|
-
|
|
34
|
-
writeFileAtomic(memoryPath('project.md', options.cwd), generateMarkdown(state, options.cwd));
|
|
32
|
+
persistState(state, options.cwd);
|
|
35
33
|
console.log(`✔ Plan item updated: [${plan.id}] ${plan.text}`);
|
|
36
34
|
}
|
|
37
35
|
//# sourceMappingURL=update-plan.js.map
|
package/dist/commands/upgrade.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { ensureMemoryDir, memoryDir, memoryExists } from '../core/io.js';
|
|
4
|
-
import { loadState,
|
|
4
|
+
import { loadState, persistState } from '../core/state.js';
|
|
5
5
|
import { scanMigrationStatus } from '../core/migration.js';
|
|
6
6
|
import { commitMemoryChange, initMemoryRepo } from '../core/memory-git.js';
|
|
7
7
|
import { BRAINCLAW_SECTION_END, BRAINCLAW_SECTION_START, buildBrainclawSection, buildClaudeCodeCommandText, ensureClaudeCodeCommand, hasBrainclawSection, } from '../core/agent-files.js';
|
|
@@ -114,7 +114,7 @@ export function runUpgrade(options = {}) {
|
|
|
114
114
|
// Execute schema migrations by re-saving state (loadState auto-migrates via Zod parse)
|
|
115
115
|
if (outdated.length > 0) {
|
|
116
116
|
const state = loadState(cwd);
|
|
117
|
-
|
|
117
|
+
persistState(state, cwd);
|
|
118
118
|
}
|
|
119
119
|
const refreshedAgentFiles = refreshManagedWorkspaceAgentFiles(cwd);
|
|
120
120
|
// Clean up empty legacy directories (recursively removes empty subdirs first)
|
package/dist/core/audit.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { memoryDir } from './io.js';
|
|
3
|
+
import { memoryDir, withStoreLock } from './io.js';
|
|
4
4
|
import { nowISO } from './ids.js';
|
|
5
5
|
import { logger } from './logger.js';
|
|
6
6
|
import { appendEvent } from './event-log.js';
|
|
@@ -23,31 +23,31 @@ function auditLogPath(cwd) {
|
|
|
23
23
|
}
|
|
24
24
|
export function appendAuditEntry(entry, cwd) {
|
|
25
25
|
try {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
item_type: entry.item_type,
|
|
32
|
-
before: entry.before,
|
|
33
|
-
after: entry.after,
|
|
34
|
-
actor_id: entry.actor_id,
|
|
35
|
-
reason: entry.reason,
|
|
36
|
-
};
|
|
37
|
-
// Remove undefined fields for compactness
|
|
38
|
-
const line = JSON.stringify(Object.fromEntries(Object.entries(full).filter(([, v]) => v !== undefined)));
|
|
39
|
-
fs.appendFileSync(auditLogPath(cwd), line + '\n', 'utf-8');
|
|
40
|
-
// Mirror to structured event log
|
|
41
|
-
const eventAction = AUDIT_TO_EVENT_ACTION[entry.action];
|
|
42
|
-
if (eventAction) {
|
|
43
|
-
appendEvent({
|
|
44
|
-
action: eventAction,
|
|
45
|
-
item_type: entry.item_type ?? 'state',
|
|
26
|
+
withStoreLock(cwd, () => {
|
|
27
|
+
const full = {
|
|
28
|
+
timestamp: nowISO(),
|
|
29
|
+
actor: entry.actor,
|
|
30
|
+
action: entry.action,
|
|
46
31
|
item_id: entry.item_id,
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
32
|
+
item_type: entry.item_type,
|
|
33
|
+
before: entry.before,
|
|
34
|
+
after: entry.after,
|
|
35
|
+
actor_id: entry.actor_id,
|
|
36
|
+
reason: entry.reason,
|
|
37
|
+
};
|
|
38
|
+
const line = JSON.stringify(Object.fromEntries(Object.entries(full).filter(([, v]) => v !== undefined)));
|
|
39
|
+
fs.appendFileSync(auditLogPath(cwd), line + '\n', 'utf-8');
|
|
40
|
+
const eventAction = AUDIT_TO_EVENT_ACTION[entry.action];
|
|
41
|
+
if (eventAction) {
|
|
42
|
+
appendEvent({
|
|
43
|
+
action: eventAction,
|
|
44
|
+
item_type: entry.item_type ?? 'state',
|
|
45
|
+
item_id: entry.item_id,
|
|
46
|
+
agent: entry.actor,
|
|
47
|
+
agent_id: entry.actor_id,
|
|
48
|
+
}, cwd);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
51
|
}
|
|
52
52
|
catch (err) {
|
|
53
53
|
logger.debug('Failed to write audit log entry:', err);
|
package/dist/core/candidates.js
CHANGED
|
@@ -2,7 +2,7 @@ import crypto from 'node:crypto';
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { CandidateSchema } from './schema.js';
|
|
5
|
-
import { resolveEntityDir } from './io.js';
|
|
5
|
+
import { resolveEntityDir, withStoreLock } from './io.js';
|
|
6
6
|
import { nowISO, getNextShortLabel } from './ids.js';
|
|
7
7
|
import { JsonStore } from './json-store.js';
|
|
8
8
|
function inboxDir(cwd, mode = 'read') {
|
|
@@ -35,8 +35,10 @@ function candidateStore(dest = 'pending', cwd) {
|
|
|
35
35
|
});
|
|
36
36
|
}
|
|
37
37
|
export function saveCandidate(candidate, cwd) {
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
withStoreLock(cwd, () => {
|
|
39
|
+
ensureInboxDirs(cwd);
|
|
40
|
+
candidateStore('pending', cwd).save(CandidateSchema.parse(candidate));
|
|
41
|
+
});
|
|
40
42
|
}
|
|
41
43
|
export function loadCandidate(id, cwd) {
|
|
42
44
|
return candidateStore('pending', cwd).load(id);
|
|
@@ -49,9 +51,11 @@ export function listCandidates(status, cwd) {
|
|
|
49
51
|
return status ? candidates.filter((candidate) => candidate.status === status) : candidates;
|
|
50
52
|
}
|
|
51
53
|
export function archiveCandidate(candidate, dest, cwd) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
54
|
+
withStoreLock(cwd, () => {
|
|
55
|
+
ensureInboxDirs(cwd);
|
|
56
|
+
candidateStore(dest, cwd).save(CandidateSchema.parse(candidate));
|
|
57
|
+
candidateStore('pending', cwd).delete(candidate.id);
|
|
58
|
+
});
|
|
55
59
|
}
|
|
56
60
|
export function listArchivedCandidates(dest, cwd) {
|
|
57
61
|
return candidateStore(dest, cwd).list();
|
package/dist/core/claims.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import { ClaimSchema } from './schema.js';
|
|
4
|
-
import { resolveEntityDir } from './io.js';
|
|
4
|
+
import { resolveEntityDir, withStoreLock } from './io.js';
|
|
5
5
|
import { nowISO } from './ids.js';
|
|
6
6
|
import { JsonStore } from './json-store.js';
|
|
7
7
|
function claimsDir(cwd, mode = 'read') {
|
|
@@ -22,14 +22,16 @@ function claimStore(cwd) {
|
|
|
22
22
|
});
|
|
23
23
|
}
|
|
24
24
|
export function saveClaim(claim, cwd) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
withStoreLock(cwd, () => {
|
|
26
|
+
ensureClaimsDir(cwd);
|
|
27
|
+
const writeStore = new JsonStore({
|
|
28
|
+
dirPath: claimsDir(cwd, 'write'),
|
|
29
|
+
documentType: 'claim',
|
|
30
|
+
getId: (c) => c.id,
|
|
31
|
+
sort: (a, b) => a.created_at.localeCompare(b.created_at),
|
|
32
|
+
});
|
|
33
|
+
writeStore.save(ClaimSchema.parse(claim));
|
|
31
34
|
});
|
|
32
|
-
writeStore.save(ClaimSchema.parse(claim));
|
|
33
35
|
}
|
|
34
36
|
export function loadClaim(id, cwd) {
|
|
35
37
|
return claimStore(cwd).load(id);
|
|
@@ -5,7 +5,7 @@ import { listClaims } from './claims.js';
|
|
|
5
5
|
import { inferProjectFromTarget, loadInstructions, resolveInstructions } from './instructions.js';
|
|
6
6
|
import { buildReputationSummary, findAgentReputationSummary } from './reputation.js';
|
|
7
7
|
import { listRuntimeNotes } from './runtime.js';
|
|
8
|
-
import { loadState,
|
|
8
|
+
import { loadState, persistState } from './state.js';
|
|
9
9
|
export function buildCoordinationSnapshot(options = {}) {
|
|
10
10
|
const config = loadConfig(options.cwd);
|
|
11
11
|
const state = loadState(options.cwd);
|
|
@@ -58,7 +58,7 @@ export function buildCoordinationSnapshot(options = {}) {
|
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
if (changed)
|
|
61
|
-
|
|
61
|
+
persistState(state, options.cwd);
|
|
62
62
|
}
|
|
63
63
|
return {
|
|
64
64
|
project_id: config.project_id,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
-
import { resolveEntityDir } from './io.js';
|
|
2
|
+
import { resolveEntityDir, withStoreLock } from './io.js';
|
|
3
3
|
import { generateId } from './ids.js';
|
|
4
4
|
import { InstructionEntrySchema } from './schema.js';
|
|
5
5
|
import { JsonStore } from './json-store.js';
|
|
@@ -19,7 +19,9 @@ export function loadInstructions(cwd) {
|
|
|
19
19
|
return instructionStore(cwd).list();
|
|
20
20
|
}
|
|
21
21
|
export function saveInstruction(entry, cwd) {
|
|
22
|
-
|
|
22
|
+
withStoreLock(cwd, () => {
|
|
23
|
+
instructionStore(cwd, 'write').save(InstructionEntrySchema.parse(entry));
|
|
24
|
+
});
|
|
23
25
|
}
|
|
24
26
|
export function createInstruction(text, options, cwd) {
|
|
25
27
|
const entries = loadInstructions(cwd);
|
package/dist/core/io.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { withLock, cleanStaleLocks } from './lock.js';
|
|
4
4
|
export const MEMORY_DIR = '.brainclaw';
|
|
5
|
+
const STORE_LOCK_BASENAME = '.store-mutation';
|
|
5
6
|
const RETRYABLE_RENAME_ERROR_CODES = new Set(['EPERM', 'EBUSY', 'EACCES']);
|
|
6
7
|
const DEFAULT_RENAME_RETRY_ATTEMPTS = 6;
|
|
7
8
|
const DEFAULT_RENAME_RETRY_DELAY_MS = 25;
|
|
@@ -72,6 +73,9 @@ export function memoryDir(cwd = process.cwd(), preferredDirName) {
|
|
|
72
73
|
export function memoryPath(filename, cwd, preferredDirName) {
|
|
73
74
|
return path.join(memoryDir(cwd, preferredDirName), filename);
|
|
74
75
|
}
|
|
76
|
+
export function storeLockPath(cwd, preferredDirName) {
|
|
77
|
+
return memoryPath(STORE_LOCK_BASENAME, cwd, preferredDirName);
|
|
78
|
+
}
|
|
75
79
|
export function memoryExists(cwd, preferredDirName) {
|
|
76
80
|
return fs.existsSync(memoryDir(cwd, preferredDirName));
|
|
77
81
|
}
|
|
@@ -95,6 +99,10 @@ export function ensureMemoryDir(cwd, preferredDirName) {
|
|
|
95
99
|
}
|
|
96
100
|
}
|
|
97
101
|
}
|
|
102
|
+
export function withStoreLock(cwd = process.cwd(), fn, preferredDirName) {
|
|
103
|
+
ensureMemoryDir(cwd, preferredDirName);
|
|
104
|
+
return withLock(storeLockPath(cwd, preferredDirName), fn);
|
|
105
|
+
}
|
|
98
106
|
/** Check if a path is a file, or a directory with at least one entry. */
|
|
99
107
|
function hasContent(p) {
|
|
100
108
|
try {
|
package/dist/core/lock.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
const DEFAULT_TIMEOUT_MS = 2000;
|
|
4
4
|
const LOCK_RETRY_INTERVAL_MS = 50;
|
|
5
5
|
const LOCK_EXPIRY_MS = 5000;
|
|
6
|
+
const heldLocks = new Map();
|
|
6
7
|
function lockFilePath(targetPath) {
|
|
7
8
|
return targetPath + '.lock';
|
|
8
9
|
}
|
|
@@ -59,22 +60,37 @@ function tryBreakLock(lockPath) {
|
|
|
59
60
|
}
|
|
60
61
|
export function acquireLock(targetPath, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
61
62
|
const lockPath = lockFilePath(targetPath);
|
|
63
|
+
const heldCount = heldLocks.get(lockPath);
|
|
64
|
+
if (heldCount) {
|
|
65
|
+
heldLocks.set(lockPath, heldCount + 1);
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
62
68
|
const deadline = Date.now() + timeoutMs;
|
|
63
69
|
const dir = path.dirname(lockPath);
|
|
64
70
|
if (!fs.existsSync(dir)) {
|
|
65
71
|
fs.mkdirSync(dir, { recursive: true });
|
|
66
72
|
}
|
|
67
73
|
while (Date.now() < deadline) {
|
|
68
|
-
if (tryCreateLock(lockPath))
|
|
74
|
+
if (tryCreateLock(lockPath)) {
|
|
75
|
+
heldLocks.set(lockPath, 1);
|
|
69
76
|
return true;
|
|
70
|
-
|
|
77
|
+
}
|
|
78
|
+
if (tryBreakLock(lockPath)) {
|
|
79
|
+
heldLocks.set(lockPath, 1);
|
|
71
80
|
return true;
|
|
81
|
+
}
|
|
72
82
|
syncSleep(Math.min(LOCK_RETRY_INTERVAL_MS, deadline - Date.now()));
|
|
73
83
|
}
|
|
74
84
|
return false;
|
|
75
85
|
}
|
|
76
86
|
export function releaseLock(targetPath) {
|
|
77
87
|
const lockPath = lockFilePath(targetPath);
|
|
88
|
+
const heldCount = heldLocks.get(lockPath);
|
|
89
|
+
if (heldCount && heldCount > 1) {
|
|
90
|
+
heldLocks.set(lockPath, heldCount - 1);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
heldLocks.delete(lockPath);
|
|
78
94
|
try {
|
|
79
95
|
if (fs.existsSync(lockPath)) {
|
|
80
96
|
fs.unlinkSync(lockPath);
|
package/dist/core/runtime.js
CHANGED
|
@@ -2,7 +2,7 @@ import crypto from 'node:crypto';
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { resolveCurrentHostId, sanitizeHostId } from './host.js';
|
|
5
|
-
import { resolveEntityDir } from './io.js';
|
|
5
|
+
import { resolveEntityDir, withStoreLock } from './io.js';
|
|
6
6
|
import { loadVersionedJsonFile, saveVersionedJsonFile } from './migration.js';
|
|
7
7
|
import { RuntimeNoteSchema } from './schema.js';
|
|
8
8
|
import { commitMemoryChange } from './memory-git.js';
|
|
@@ -40,13 +40,15 @@ export function saveRuntimeNote(note, cwd) {
|
|
|
40
40
|
const persistedNote = visibility === 'shared'
|
|
41
41
|
? { ...note, visibility, host_id: hostId }
|
|
42
42
|
: { ...note, visibility, host_id: hostId };
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
43
|
+
withStoreLock(cwd, () => {
|
|
44
|
+
ensureRuntimeDir(note.agent, cwd, visibility, hostId);
|
|
45
|
+
const filepath = visibility === 'shared'
|
|
46
|
+
? path.join(sharedAgentDir(note.agent, cwd, 'write'), `${note.id}.json`)
|
|
47
|
+
: path.join(hostAgentDir(visibility, hostId, note.agent, cwd, 'write'), `${note.id}.json`);
|
|
48
|
+
saveVersionedJsonFile('runtime_note', filepath, RuntimeNoteSchema.parse(persistedNote));
|
|
49
|
+
appendEvent({ action: 'create', item_type: 'runtime_note', item_id: note.id, agent: note.agent, agent_id: note.agent_id }, cwd);
|
|
50
|
+
commitMemoryChange(`runtime note: ${note.note_type ?? 'note'} (${note.agent})`, cwd);
|
|
51
|
+
});
|
|
50
52
|
}
|
|
51
53
|
export function runtimeNotePath(note, cwd) {
|
|
52
54
|
const visibility = note.visibility ?? 'shared';
|
|
@@ -56,12 +58,14 @@ export function runtimeNotePath(note, cwd) {
|
|
|
56
58
|
: path.join(hostAgentDir(visibility, hostId, note.agent, cwd), `${note.id}.json`);
|
|
57
59
|
}
|
|
58
60
|
export function deleteRuntimeNote(note, cwd) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
return withStoreLock(cwd, () => {
|
|
62
|
+
const filepath = runtimeNotePath(note, cwd);
|
|
63
|
+
if (!fs.existsSync(filepath)) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
fs.unlinkSync(filepath);
|
|
67
|
+
return true;
|
|
68
|
+
});
|
|
65
69
|
}
|
|
66
70
|
function readAgentNotes(dir, agent) {
|
|
67
71
|
if (!fs.existsSync(dir))
|