brainclaw 1.7.5 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -11
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/cli.js +139 -13
- package/dist/commands/add-step.js +1 -1
- package/dist/commands/bootstrap.js +2 -26
- package/dist/commands/check-security-mcp.js +50 -33
- package/dist/commands/check-security.js +86 -43
- package/dist/commands/claim.js +22 -21
- package/dist/commands/confirm.js +26 -0
- package/dist/commands/context-diff.js +1 -1
- package/dist/commands/dispatch-watch.js +142 -0
- package/dist/commands/doctor.js +113 -2
- package/dist/commands/estimation-report.js +115 -16
- package/dist/commands/harvest.js +502 -16
- package/dist/commands/init.js +123 -21
- package/dist/commands/loops-handlers.js +4 -0
- package/dist/commands/mcp-read-handlers.js +198 -29
- package/dist/commands/mcp.js +615 -92
- package/dist/commands/memory.js +21 -17
- package/dist/commands/migrate.js +81 -17
- package/dist/commands/prune.js +78 -4
- package/dist/commands/reflect.js +26 -20
- package/dist/commands/register-agent.js +57 -1
- package/dist/commands/repair.js +20 -0
- package/dist/commands/session-end.js +15 -6
- package/dist/commands/session-start.js +18 -1
- package/dist/commands/setup-security.js +39 -18
- package/dist/commands/setup.js +26 -27
- package/dist/commands/stale.js +16 -2
- package/dist/commands/uninstall.js +126 -34
- package/dist/commands/update-step.js +6 -0
- package/dist/commands/worktree.js +60 -0
- package/dist/core/actions.js +12 -3
- package/dist/core/agent-capability.js +11 -13
- package/dist/core/agent-files.js +844 -547
- package/dist/core/agent-integrations.js +0 -3
- package/dist/core/agent-inventory.js +67 -0
- package/dist/core/agent-registry.js +163 -29
- package/dist/core/agentrun-reconciler.js +33 -2
- package/dist/core/agentruns.js +7 -1
- package/dist/core/ai-agent-detection.js +31 -44
- package/dist/core/archival.js +15 -9
- package/dist/core/assignment-reconciler.js +56 -0
- package/dist/core/assignment-sweeper.js +127 -4
- package/dist/core/assignments.js +69 -11
- package/dist/core/bootstrap.js +233 -67
- package/dist/core/brainclaw-version.js +22 -0
- package/dist/core/candidates.js +21 -1
- package/dist/core/claims.js +313 -150
- package/dist/core/config.js +6 -1
- package/dist/core/context-diff.js +148 -20
- package/dist/core/context.js +129 -8
- package/dist/core/coordination.js +22 -3
- package/dist/core/dispatch-status.js +109 -5
- package/dist/core/dispatcher.js +65 -11
- package/dist/core/entity-operations.js +45 -24
- package/dist/core/entity-registry.js +31 -5
- package/dist/core/event-log.js +138 -21
- package/dist/core/events/checkpoint.js +258 -0
- package/dist/core/events/genesis.js +220 -0
- package/dist/core/events/journal.js +507 -0
- package/dist/core/events/materialize.js +126 -0
- package/dist/core/events/registry-post-image.js +110 -0
- package/dist/core/events/verify.js +109 -0
- package/dist/core/execution-adapters.js +23 -0
- package/dist/core/execution.js +25 -0
- package/dist/core/facade-schema.js +48 -0
- package/dist/core/gc-semantic.js +130 -5
- package/dist/core/handoff-snapshot.js +68 -0
- package/dist/core/ids.js +19 -8
- package/dist/core/instruction-templates.js +34 -115
- package/dist/core/io.js +39 -3
- package/dist/core/json-store.js +10 -1
- package/dist/core/lock.js +153 -28
- package/dist/core/loops/bootstrap-acquire.js +25 -1
- package/dist/core/loops/facade-schema.js +2 -0
- package/dist/core/loops/hooks/survey-signals-baseline.js +36 -0
- package/dist/core/loops/index.js +1 -0
- package/dist/core/loops/presets/bootstrap.js +7 -0
- package/dist/core/loops/store.js +17 -0
- package/dist/core/loops/verbs.js +24 -1
- package/dist/core/markdown.js +8 -76
- package/dist/core/mcp-command-resolution.js +245 -0
- package/dist/core/memory-compactor.js +5 -3
- package/dist/core/memory-lifecycle.js +282 -0
- package/dist/core/merge-risk.js +150 -0
- package/dist/core/messaging.js +8 -1
- package/dist/core/migration.js +11 -1
- package/dist/core/observer-mode.js +26 -0
- package/dist/core/operations/memory-mutation.js +90 -65
- package/dist/core/operations/plan.js +27 -1
- package/dist/core/protocol-skills.js +210 -0
- package/dist/core/reflection-safety.js +6 -7
- package/dist/core/reputation.js +84 -2
- package/dist/core/runtime-signals.js +71 -9
- package/dist/core/runtime.js +84 -1
- package/dist/core/schema.js +125 -0
- package/dist/core/security-detectors.js +125 -0
- package/dist/core/security-extract.js +189 -0
- package/dist/core/security-guard.js +107 -29
- package/dist/core/security-packages.js +121 -0
- package/dist/core/security-scoring.js +76 -9
- package/dist/core/security.js +34 -2
- package/dist/core/sequence.js +11 -2
- package/dist/core/setup-flow.js +141 -13
- package/dist/core/spawn-check.js +110 -4
- package/dist/core/staleness.js +109 -1
- package/dist/core/state.js +250 -54
- package/dist/core/store-resolution.js +19 -5
- package/dist/core/worktree.js +169 -7
- package/dist/facts.js +8 -8
- package/dist/facts.json +7 -7
- package/docs/PROTOCOL.md +223 -0
- package/docs/cli.md +11 -10
- package/docs/concepts/coordinator-runbook.md +129 -0
- package/docs/concepts/dispatch-lifecycle.md +17 -0
- package/docs/concepts/event-log-store-critique-A.md +333 -0
- package/docs/concepts/event-log-store-critique-B.md +353 -0
- package/docs/concepts/event-log-store-phase0-measurements.md +58 -0
- package/docs/concepts/event-log-store-proposal-A.md +365 -0
- package/docs/concepts/event-log-store-proposal-B.md +404 -0
- package/docs/concepts/event-log-store.md +928 -0
- package/docs/concepts/identity-model-proposal.md +371 -0
- package/docs/concepts/memory.md +5 -4
- package/docs/concepts/observer-protocol.md +361 -0
- package/docs/concepts/parallel-merge-protocol.md +71 -0
- package/docs/concepts/plans-and-claims.md +43 -0
- package/docs/concepts/skills.md +78 -0
- package/docs/concepts/workspace-bootstrapping.md +61 -0
- package/docs/integrations/agents.md +4 -4
- package/docs/integrations/cline.md +10 -11
- package/docs/integrations/codex.md +2 -2
- package/docs/integrations/continue.md +5 -5
- package/docs/integrations/copilot.md +14 -12
- package/docs/integrations/openclaw.md +7 -6
- package/docs/integrations/overview.md +7 -7
- package/docs/integrations/roo.md +3 -3
- package/docs/integrations/windsurf.md +6 -6
- package/docs/mcp-schema-changelog.md +51 -20
- package/docs/quickstart.md +48 -47
- package/docs/security.md +174 -15
- package/docs/storage.md +4 -2
- package/package.json +8 -6
package/dist/core/claims.js
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
3
4
|
import { ClaimSchema } from './schema.js';
|
|
4
5
|
import { resolveEntityDir } from './io.js';
|
|
5
6
|
import { mutate } from './mutation-pipeline.js';
|
|
6
7
|
import { nowISO } from './ids.js';
|
|
7
8
|
import { JsonStore } from './json-store.js';
|
|
8
9
|
import { loadConfig } from './config.js';
|
|
9
|
-
import { createWorktree, resetWorktreeToRef } from './worktree.js';
|
|
10
|
+
import { createWorktree, resetWorktreeToRef, removeWorktree, sanitizeBranchComponent } from './worktree.js';
|
|
10
11
|
import { appendAuditEntry } from './audit.js';
|
|
11
12
|
import { refreshLiveCompanions } from '../commands/export.js';
|
|
12
13
|
import { loadSessionById } from './identity.js';
|
|
13
14
|
import { loadState, persistState } from './state.js';
|
|
14
15
|
import { createRuntimeEvent } from './events.js';
|
|
16
|
+
import { emitRegistryPostImage, registryFaultPoint } from './events/registry-post-image.js';
|
|
15
17
|
/** Parse duration string like '4h', '30m' to ms. */
|
|
16
18
|
function parseTtl(value) {
|
|
17
19
|
const match = /^(\d+)([mhd])$/i.exec(value.trim());
|
|
@@ -28,35 +30,75 @@ function parseTtl(value) {
|
|
|
28
30
|
function claimsDir(cwd, mode = 'read') {
|
|
29
31
|
return resolveEntityDir('claims', cwd ?? process.cwd(), mode);
|
|
30
32
|
}
|
|
33
|
+
function claimDirs(cwd) {
|
|
34
|
+
const effectiveCwd = cwd ?? process.cwd();
|
|
35
|
+
return Array.from(new Set([
|
|
36
|
+
claimsDir(effectiveCwd, 'write'),
|
|
37
|
+
claimsDir(effectiveCwd, 'read'),
|
|
38
|
+
]));
|
|
39
|
+
}
|
|
31
40
|
export function ensureClaimsDir(cwd) {
|
|
32
41
|
const dir = claimsDir(cwd, 'write');
|
|
33
42
|
if (!fs.existsSync(dir)) {
|
|
34
43
|
fs.mkdirSync(dir, { recursive: true });
|
|
35
44
|
}
|
|
36
45
|
}
|
|
37
|
-
function
|
|
46
|
+
function claimStoreForDir(dirPath) {
|
|
38
47
|
return new JsonStore({
|
|
39
|
-
dirPath
|
|
48
|
+
dirPath,
|
|
40
49
|
documentType: 'claim',
|
|
41
50
|
getId: (claim) => claim.id,
|
|
42
51
|
sort: (a, b) => a.created_at.localeCompare(b.created_at),
|
|
43
52
|
});
|
|
44
53
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
function writeClaimStore(cwd) {
|
|
55
|
+
return claimStoreForDir(claimsDir(cwd, 'write'));
|
|
56
|
+
}
|
|
57
|
+
function loadClaimFromAnyDir(id, cwd) {
|
|
58
|
+
for (const dirPath of claimDirs(cwd)) {
|
|
59
|
+
const store = claimStoreForDir(dirPath);
|
|
60
|
+
if (store.exists(id))
|
|
61
|
+
return store.load(id);
|
|
62
|
+
}
|
|
63
|
+
throw new Error(`claim '${id}' not found`);
|
|
64
|
+
}
|
|
65
|
+
function saveClaimUnlocked(claim, cwd, options) {
|
|
66
|
+
ensureClaimsDir(cwd);
|
|
67
|
+
const store = writeClaimStore(cwd);
|
|
68
|
+
const parsed = ClaimSchema.parse(claim);
|
|
69
|
+
// pln#568 (I2): journal the post-image BEFORE the projection write, so a
|
|
70
|
+
// crash can only leave the journal ahead of the projection, never behind.
|
|
71
|
+
const created = !store.exists(parsed.id);
|
|
72
|
+
emitRegistryPostImage('claim', parsed, { created, agent: parsed.agent, agent_id: parsed.agent_id, session_id: parsed.session_id, cwd });
|
|
73
|
+
registryFaultPoint('after_registry_journal');
|
|
74
|
+
store.save(parsed);
|
|
75
|
+
const writeDir = claimsDir(cwd, 'write');
|
|
76
|
+
for (const dirPath of claimDirs(cwd)) {
|
|
77
|
+
if (dirPath === writeDir)
|
|
78
|
+
continue;
|
|
79
|
+
const legacyPath = path.join(dirPath, `${claim.id}.json`);
|
|
80
|
+
try {
|
|
81
|
+
if (fs.existsSync(legacyPath))
|
|
82
|
+
fs.unlinkSync(legacyPath);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// Best effort: listClaims() reads both dirs, so a missed cleanup remains visible.
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Auto-refresh live companions after claim changes (non-fatal). Sweep loops
|
|
89
|
+
// pass refreshCompanions:false and refresh ONCE after the loop — review
|
|
90
|
+
// follow-up O5: a per-save refresh inside the critical section compounded an
|
|
91
|
+
// O(store) cost on every iteration.
|
|
92
|
+
if (options?.refreshCompanions !== false) {
|
|
56
93
|
try {
|
|
57
94
|
refreshLiveCompanions(cwd);
|
|
58
95
|
}
|
|
59
96
|
catch { /* best-effort */ }
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
export function saveClaim(claim, cwd) {
|
|
100
|
+
mutate({ cwd }, () => {
|
|
101
|
+
saveClaimUnlocked(claim, cwd);
|
|
60
102
|
});
|
|
61
103
|
}
|
|
62
104
|
/**
|
|
@@ -84,22 +126,63 @@ export function acquireClaimScope(input, cwd) {
|
|
|
84
126
|
plan_id: input.plan_id,
|
|
85
127
|
model: input.model,
|
|
86
128
|
};
|
|
87
|
-
|
|
129
|
+
saveClaimUnlocked(claim, cwd);
|
|
88
130
|
return { acquired: true, claim };
|
|
89
131
|
});
|
|
90
132
|
}
|
|
91
133
|
export function loadClaim(id, cwd) {
|
|
92
|
-
return
|
|
134
|
+
return loadClaimFromAnyDir(id, cwd);
|
|
93
135
|
}
|
|
94
136
|
export function listClaims(cwd) {
|
|
95
|
-
|
|
137
|
+
const byId = new Map();
|
|
138
|
+
for (const dirPath of claimDirs(cwd)) {
|
|
139
|
+
for (const claim of claimStoreForDir(dirPath).list()) {
|
|
140
|
+
if (!byId.has(claim.id))
|
|
141
|
+
byId.set(claim.id, claim);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return Array.from(byId.values()).sort((a, b) => a.created_at.localeCompare(b.created_at));
|
|
96
145
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
claim.
|
|
101
|
-
|
|
102
|
-
|
|
146
|
+
function assertReleaseOwnership(claim, auth) {
|
|
147
|
+
if (!auth)
|
|
148
|
+
return { overrideUsed: false };
|
|
149
|
+
const ownerMatches = (auth.session_id !== undefined && claim.session_id !== undefined && auth.session_id === claim.session_id)
|
|
150
|
+
|| (auth.agent_id !== undefined && claim.agent_id !== undefined && auth.agent_id === claim.agent_id)
|
|
151
|
+
|| (auth.agent !== undefined && claim.agent === auth.agent);
|
|
152
|
+
if (ownerMatches)
|
|
153
|
+
return { overrideUsed: false };
|
|
154
|
+
if (auth.override)
|
|
155
|
+
return { overrideUsed: true };
|
|
156
|
+
throw new Error(`claim '${claim.id}' is held by '${claim.agent}'${claim.session_id ? ` (session ${claim.session_id})` : ''}; `
|
|
157
|
+
+ `caller '${auth.agent ?? auth.agent_id ?? auth.session_id ?? 'unknown'}' does not own it. `
|
|
158
|
+
+ 'Coordinator-level callers may release with override.');
|
|
159
|
+
}
|
|
160
|
+
function auditReleaseOverride(claim, auth, cwd) {
|
|
161
|
+
appendAuditEntry({
|
|
162
|
+
actor: auth.agent ?? 'coordinator',
|
|
163
|
+
actor_id: auth.agent_id,
|
|
164
|
+
action: 'release_claim',
|
|
165
|
+
item_id: claim.id,
|
|
166
|
+
item_type: 'claim',
|
|
167
|
+
scope: claim.scope,
|
|
168
|
+
session_id: auth.session_id,
|
|
169
|
+
after: { ownership_override: true, claim_owner: claim.agent },
|
|
170
|
+
}, cwd);
|
|
171
|
+
}
|
|
172
|
+
export function releaseClaim(id, cwd, auth) {
|
|
173
|
+
let overrideUsed = false;
|
|
174
|
+
const released = mutate({ cwd }, () => {
|
|
175
|
+
const claim = loadClaim(id, cwd);
|
|
176
|
+
overrideUsed = assertReleaseOwnership(claim, auth).overrideUsed;
|
|
177
|
+
claim.status = 'released';
|
|
178
|
+
claim.released_at = nowISO();
|
|
179
|
+
saveClaimUnlocked(claim, cwd);
|
|
180
|
+
return claim;
|
|
181
|
+
});
|
|
182
|
+
if (overrideUsed && auth) {
|
|
183
|
+
auditReleaseOverride(released, auth, cwd);
|
|
184
|
+
}
|
|
185
|
+
return released;
|
|
103
186
|
}
|
|
104
187
|
/**
|
|
105
188
|
* Release a claim and optionally cascade the status to its linked plan.
|
|
@@ -113,83 +196,95 @@ export function releaseClaim(id, cwd) {
|
|
|
113
196
|
* Emits `plan_cascade_to_done` runtime event when auto-transitioning to done.
|
|
114
197
|
*/
|
|
115
198
|
export function releaseClaimWithCascade(id, options = {}) {
|
|
116
|
-
const { planStatus, cwd } = options;
|
|
117
|
-
|
|
118
|
-
const
|
|
119
|
-
|
|
199
|
+
const { planStatus, cwd, auth } = options;
|
|
200
|
+
let overrideUsed = false;
|
|
201
|
+
const result = mutate({ cwd }, () => {
|
|
202
|
+
// Release the claim (idempotent: already-released claims are returned as-is)
|
|
203
|
+
const claim = loadClaim(id, cwd);
|
|
204
|
+
if (claim.status === 'released') {
|
|
205
|
+
return { claim, planTransitioned: false };
|
|
206
|
+
}
|
|
207
|
+
overrideUsed = assertReleaseOwnership(claim, auth).overrideUsed;
|
|
208
|
+
claim.status = 'released';
|
|
209
|
+
claim.released_at = nowISO();
|
|
210
|
+
saveClaimUnlocked(claim, cwd);
|
|
211
|
+
// No cascade requested or no linked plan
|
|
212
|
+
if (!planStatus || !claim.plan_id) {
|
|
213
|
+
return { claim, planTransitioned: false };
|
|
214
|
+
}
|
|
215
|
+
const state = loadState(cwd);
|
|
216
|
+
const plan = state.plan_items.find((item) => item.id === claim.plan_id);
|
|
217
|
+
if (!plan) {
|
|
218
|
+
return { claim, planTransitioned: false };
|
|
219
|
+
}
|
|
220
|
+
const ts = nowISO();
|
|
221
|
+
if (planStatus === 'blocked') {
|
|
222
|
+
// Always propagate blocked status to plan
|
|
223
|
+
plan.status = 'blocked';
|
|
224
|
+
plan.updated_at = ts;
|
|
225
|
+
persistState(state, cwd);
|
|
226
|
+
return { claim, planTransitioned: true, planId: plan.id, newPlanStatus: 'blocked' };
|
|
227
|
+
}
|
|
228
|
+
if (planStatus === 'done') {
|
|
229
|
+
// Count OTHER active claims on the same plan (current claim already released above)
|
|
230
|
+
const otherActive = listClaims(cwd).filter((c) => c.status === 'active' && c.plan_id === claim.plan_id && c.id !== id);
|
|
231
|
+
if (otherActive.length > 0) {
|
|
232
|
+
const planWarning = `Plan has ${otherActive.length} other active claim(s); staying in_progress`;
|
|
233
|
+
return {
|
|
234
|
+
claim,
|
|
235
|
+
planTransitioned: false,
|
|
236
|
+
planWarning,
|
|
237
|
+
planId: plan.id,
|
|
238
|
+
newPlanStatus: plan.status,
|
|
239
|
+
otherActiveClaimsCount: otherActive.length,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
// Last active claim released → auto-transition plan to done
|
|
243
|
+
plan.status = 'done';
|
|
244
|
+
if (!plan.completed_at)
|
|
245
|
+
plan.completed_at = ts;
|
|
246
|
+
plan.updated_at = ts;
|
|
247
|
+
persistState(state, cwd);
|
|
248
|
+
return { claim, planTransitioned: true, planId: plan.id, newPlanStatus: 'done' };
|
|
249
|
+
}
|
|
250
|
+
// planStatus='todo', 'in_progress', or other — no cascade
|
|
120
251
|
return { claim, planTransitioned: false };
|
|
121
|
-
}
|
|
122
|
-
claim.status = 'released';
|
|
123
|
-
claim.released_at = nowISO();
|
|
124
|
-
saveClaim(claim, cwd);
|
|
252
|
+
});
|
|
125
253
|
appendAuditEntry({
|
|
126
|
-
actor: claim.agent,
|
|
127
|
-
actor_id: claim.agent_id,
|
|
254
|
+
actor: result.claim.agent,
|
|
255
|
+
actor_id: result.claim.agent_id,
|
|
128
256
|
action: 'release_claim',
|
|
129
257
|
item_id: id,
|
|
130
258
|
item_type: 'claim',
|
|
131
|
-
scope: claim.scope,
|
|
132
|
-
session_id: claim.session_id,
|
|
133
|
-
host_id: claim.host_id,
|
|
259
|
+
scope: result.claim.scope,
|
|
260
|
+
session_id: result.claim.session_id,
|
|
261
|
+
host_id: result.claim.host_id,
|
|
134
262
|
}, cwd);
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
return { claim, planTransitioned: false };
|
|
263
|
+
if (overrideUsed && auth) {
|
|
264
|
+
auditReleaseOverride(result.claim, auth, cwd);
|
|
138
265
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
plan.status = 'blocked';
|
|
148
|
-
plan.updated_at = ts;
|
|
149
|
-
persistState(state, cwd);
|
|
150
|
-
return { claim, planTransitioned: true, planId: plan.id, newPlanStatus: 'blocked' };
|
|
266
|
+
if (result.planWarning && result.planId) {
|
|
267
|
+
appendAuditEntry({
|
|
268
|
+
actor: result.claim.agent,
|
|
269
|
+
action: 'update',
|
|
270
|
+
item_id: result.planId,
|
|
271
|
+
item_type: 'plan',
|
|
272
|
+
after: { cascade_blocked: true, reason: result.planWarning },
|
|
273
|
+
}, cwd);
|
|
151
274
|
}
|
|
152
|
-
if (
|
|
153
|
-
// Count OTHER active claims on the same plan (current claim already released above)
|
|
154
|
-
const otherActive = listClaims(cwd).filter((c) => c.status === 'active' && c.plan_id === claim.plan_id && c.id !== id);
|
|
155
|
-
if (otherActive.length > 0) {
|
|
156
|
-
const planWarning = `Plan has ${otherActive.length} other active claim(s); staying in_progress`;
|
|
157
|
-
appendAuditEntry({
|
|
158
|
-
actor: claim.agent,
|
|
159
|
-
action: 'update',
|
|
160
|
-
item_id: plan.id,
|
|
161
|
-
item_type: 'plan',
|
|
162
|
-
after: { cascade_blocked: true, reason: planWarning },
|
|
163
|
-
}, cwd);
|
|
164
|
-
return {
|
|
165
|
-
claim,
|
|
166
|
-
planTransitioned: false,
|
|
167
|
-
planWarning,
|
|
168
|
-
planId: plan.id,
|
|
169
|
-
newPlanStatus: plan.status,
|
|
170
|
-
otherActiveClaimsCount: otherActive.length,
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
// Last active claim released → auto-transition plan to done
|
|
174
|
-
plan.status = 'done';
|
|
175
|
-
if (!plan.completed_at)
|
|
176
|
-
plan.completed_at = ts;
|
|
177
|
-
plan.updated_at = ts;
|
|
178
|
-
persistState(state, cwd);
|
|
275
|
+
if (result.newPlanStatus === 'done' && result.planId) {
|
|
179
276
|
createRuntimeEvent({
|
|
180
|
-
agent: claim.agent,
|
|
181
|
-
agent_id: claim.agent_id,
|
|
277
|
+
agent: result.claim.agent,
|
|
278
|
+
agent_id: result.claim.agent_id,
|
|
182
279
|
event_type: 'plan_cascade_to_done',
|
|
183
280
|
claim_id: id,
|
|
184
|
-
plan_id:
|
|
185
|
-
session_id: claim.session_id,
|
|
186
|
-
host_id: claim.host_id,
|
|
187
|
-
text: `Plan ${
|
|
281
|
+
plan_id: result.planId,
|
|
282
|
+
session_id: result.claim.session_id,
|
|
283
|
+
host_id: result.claim.host_id,
|
|
284
|
+
text: `Plan ${result.planId} auto-transitioned to done — last active claim released by ${result.claim.agent}`,
|
|
188
285
|
}, cwd);
|
|
189
|
-
return { claim, planTransitioned: true, planId: plan.id, newPlanStatus: 'done' };
|
|
190
286
|
}
|
|
191
|
-
|
|
192
|
-
return { claim, planTransitioned: false };
|
|
287
|
+
return result;
|
|
193
288
|
}
|
|
194
289
|
export function generateClaimId() {
|
|
195
290
|
const rand = crypto.randomBytes(4).toString('hex');
|
|
@@ -202,19 +297,26 @@ export function isClaimExpired(claim) {
|
|
|
202
297
|
}
|
|
203
298
|
/** Mark active claims past their expires_at as released. Returns count of expired claims. */
|
|
204
299
|
export function expireStaleActiveClaims(cwd) {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
300
|
+
return mutate({ cwd }, () => {
|
|
301
|
+
const all = listClaims(cwd);
|
|
302
|
+
let count = 0;
|
|
303
|
+
const now = nowISO();
|
|
304
|
+
for (const claim of all) {
|
|
305
|
+
if (claim.status === 'active' && isClaimExpired(claim)) {
|
|
306
|
+
claim.status = 'released';
|
|
307
|
+
claim.released_at = now;
|
|
308
|
+
saveClaimUnlocked(claim, cwd, { refreshCompanions: false });
|
|
309
|
+
count++;
|
|
310
|
+
}
|
|
215
311
|
}
|
|
216
|
-
|
|
217
|
-
|
|
312
|
+
if (count > 0) {
|
|
313
|
+
try {
|
|
314
|
+
refreshLiveCompanions(cwd);
|
|
315
|
+
}
|
|
316
|
+
catch { /* best-effort */ }
|
|
317
|
+
}
|
|
318
|
+
return count;
|
|
319
|
+
});
|
|
218
320
|
}
|
|
219
321
|
/** Default stale threshold: 24 hours. */
|
|
220
322
|
const DEFAULT_STALE_HOURS = 24;
|
|
@@ -369,33 +471,40 @@ export function isClaimStale(claim, thresholdHours, cwd) {
|
|
|
369
471
|
export function releaseStaleClaimsFromOtherAgents(currentAgent, cwd, currentSessionId) {
|
|
370
472
|
const config = loadConfig(cwd);
|
|
371
473
|
const thresholdHours = config.claims?.auto_release_after_hours ?? DEFAULT_STALE_HOURS;
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
474
|
+
return mutate({ cwd }, () => {
|
|
475
|
+
const all = listClaims(cwd);
|
|
476
|
+
const now = nowISO();
|
|
477
|
+
const released = [];
|
|
478
|
+
const warned = [];
|
|
479
|
+
for (const claim of all) {
|
|
480
|
+
if (claim.status !== 'active')
|
|
481
|
+
continue;
|
|
482
|
+
// Session-aware skip: if the caller names its current session, only that
|
|
483
|
+
// session's claims are off-limits. Otherwise fall back to the legacy
|
|
484
|
+
// "skip same agent" rule.
|
|
485
|
+
if (currentSessionId) {
|
|
486
|
+
if (claim.session_id === currentSessionId)
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
else if (claim.agent === currentAgent) {
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
const { status } = assessClaimLiveness(claim, { thresholdHours, cwd });
|
|
493
|
+
if (status === 'live' || status === 'young')
|
|
385
494
|
continue;
|
|
495
|
+
claim.status = 'released';
|
|
496
|
+
claim.released_at = now;
|
|
497
|
+
saveClaimUnlocked(claim, cwd, { refreshCompanions: false });
|
|
498
|
+
released.push(claim);
|
|
386
499
|
}
|
|
387
|
-
|
|
388
|
-
|
|
500
|
+
if (released.length > 0) {
|
|
501
|
+
try {
|
|
502
|
+
refreshLiveCompanions(cwd);
|
|
503
|
+
}
|
|
504
|
+
catch { /* best-effort */ }
|
|
389
505
|
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
continue;
|
|
393
|
-
claim.status = 'released';
|
|
394
|
-
claim.released_at = now;
|
|
395
|
-
store.save(claim);
|
|
396
|
-
released.push(claim);
|
|
397
|
-
}
|
|
398
|
-
return { released, warned };
|
|
506
|
+
return { released, warned };
|
|
507
|
+
});
|
|
399
508
|
}
|
|
400
509
|
/**
|
|
401
510
|
* Create a coordinator-owned claim with worktree isolation.
|
|
@@ -445,8 +554,11 @@ export function createCoordinatorClaim(options) {
|
|
|
445
554
|
const claimId = generateClaimId();
|
|
446
555
|
let worktreePath;
|
|
447
556
|
let worktreeWarning;
|
|
448
|
-
// Create isolated worktree (matching bclaw_claim MCP handler behavior)
|
|
449
|
-
|
|
557
|
+
// Create isolated worktree (matching bclaw_claim MCP handler behavior).
|
|
558
|
+
// can_45316d5c: the slug must be a valid git ref component — scopes like
|
|
559
|
+
// `.github/workflows` previously produced `feat/.github-…` (leading dot),
|
|
560
|
+
// which git rejects and the whole spawn failed.
|
|
561
|
+
const branchSlug = sanitizeBranchComponent(options.scope);
|
|
450
562
|
const worktreeBranch = `feat/${branchSlug}`;
|
|
451
563
|
try {
|
|
452
564
|
worktreePath = createWorktree(options.cwd, worktreeBranch, {
|
|
@@ -463,24 +575,58 @@ export function createCoordinatorClaim(options) {
|
|
|
463
575
|
catch (err) {
|
|
464
576
|
worktreeWarning = `Worktree creation failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
465
577
|
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
578
|
+
const result = mutate({ cwd: options.cwd }, () => {
|
|
579
|
+
const racedScopeClaim = listClaims(options.cwd).find((claim) => claim.status === 'active' && claim.scope === options.scope);
|
|
580
|
+
if (racedScopeClaim) {
|
|
581
|
+
if (racedScopeClaim.agent === options.agent) {
|
|
582
|
+
return {
|
|
583
|
+
claimId: racedScopeClaim.id,
|
|
584
|
+
worktreePath: racedScopeClaim.worktree_path,
|
|
585
|
+
worktreeWarning,
|
|
586
|
+
reusedExisting: true,
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
return {
|
|
590
|
+
claimId: racedScopeClaim.id,
|
|
591
|
+
worktreePath: racedScopeClaim.worktree_path,
|
|
592
|
+
reusedExisting: true,
|
|
593
|
+
scopeConflict: true,
|
|
594
|
+
conflictAgent: racedScopeClaim.agent,
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
saveClaimUnlocked({
|
|
598
|
+
id: claimId,
|
|
599
|
+
agent: options.agent,
|
|
600
|
+
scope: options.scope,
|
|
601
|
+
description: options.description,
|
|
602
|
+
plan_id: options.planId,
|
|
603
|
+
created_at: nowISO(),
|
|
604
|
+
status: 'active',
|
|
605
|
+
worktree_path: worktreePath,
|
|
606
|
+
}, options.cwd);
|
|
607
|
+
return { claimId, worktreePath, worktreeWarning, reusedExisting: false };
|
|
608
|
+
});
|
|
609
|
+
// Review follow-up O1 (lop_e2d566765b8b4ce3): when the in-lock re-check finds
|
|
610
|
+
// a raced claim, the worktree created moments earlier (outside the lock) is
|
|
611
|
+
// orphaned — nobody would ever remove it. Decision: delete it (it is seconds
|
|
612
|
+
// old and contains only birth artifacts; a reuse-pool is not worth the
|
|
613
|
+
// bookkeeping). Best-effort and outside the critical section.
|
|
614
|
+
if (result.reusedExisting && worktreePath && worktreePath !== result.worktreePath) {
|
|
615
|
+
try {
|
|
616
|
+
removeWorktree(options.cwd, worktreePath, { force: true });
|
|
617
|
+
}
|
|
618
|
+
catch { /* best-effort GC — a leftover dir is caught by worktree clean */ }
|
|
619
|
+
}
|
|
620
|
+
if (!result.reusedExisting && !result.scopeConflict) {
|
|
621
|
+
appendAuditEntry({
|
|
622
|
+
actor: options.dispatcherAgent,
|
|
623
|
+
action: 'claim',
|
|
624
|
+
item_id: result.claimId,
|
|
625
|
+
item_type: 'claim',
|
|
626
|
+
scope: options.scope,
|
|
627
|
+
}, options.cwd);
|
|
628
|
+
}
|
|
629
|
+
return result;
|
|
484
630
|
}
|
|
485
631
|
// ── Claim lifecycle helpers for multi-instance dispatch ────
|
|
486
632
|
/**
|
|
@@ -488,15 +634,32 @@ export function createCoordinatorClaim(options) {
|
|
|
488
634
|
* Called by the dispatcher after sending the inbox message.
|
|
489
635
|
*/
|
|
490
636
|
export function attachAssignmentMessageToClaim(claimId, messageId, cwd) {
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
637
|
+
mutate({ cwd }, () => {
|
|
638
|
+
const claim = loadClaim(claimId, cwd);
|
|
639
|
+
claim.assignment_message_id = messageId;
|
|
640
|
+
saveClaimUnlocked(claim, cwd);
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* sprint 1.5 — patch a claim's worktree_path so a coordinator can register a
|
|
645
|
+
* manually created worktree (or correct a stale path) without hand-editing the
|
|
646
|
+
* store. Surfaced through bclaw_update(entity="claim", patch={worktree_path}).
|
|
647
|
+
*/
|
|
648
|
+
export function patchClaimWorktreePath(claimId, worktreePath, cwd) {
|
|
649
|
+
return mutate({ cwd }, () => {
|
|
650
|
+
const claim = loadClaim(claimId, cwd);
|
|
651
|
+
claim.worktree_path = worktreePath;
|
|
652
|
+
saveClaimUnlocked(claim, cwd);
|
|
653
|
+
return claim;
|
|
654
|
+
});
|
|
494
655
|
}
|
|
495
656
|
/** Link a claim to its Assignment entity (Agent SDK runtime protocol). */
|
|
496
657
|
export function linkClaimToAssignment(claimId, assignmentId, cwd) {
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
658
|
+
mutate({ cwd }, () => {
|
|
659
|
+
const claim = loadClaim(claimId, cwd);
|
|
660
|
+
claim.assignment_id = assignmentId;
|
|
661
|
+
saveClaimUnlocked(claim, cwd);
|
|
662
|
+
});
|
|
500
663
|
}
|
|
501
664
|
/**
|
|
502
665
|
* Adopt a claim from a spawned instance's session.
|
|
@@ -538,7 +701,7 @@ export function adoptClaimSession(claimId, sessionId, cwd) {
|
|
|
538
701
|
}
|
|
539
702
|
claim.session_id = sessionId;
|
|
540
703
|
claim.adopted_at = nowISO();
|
|
541
|
-
|
|
704
|
+
saveClaimUnlocked(claim, cwd);
|
|
542
705
|
return { adopted: true, reason: 'ok' };
|
|
543
706
|
});
|
|
544
707
|
}
|
package/dist/core/config.js
CHANGED
|
@@ -37,6 +37,11 @@ export function defaultConfig(projectName, options = {}) {
|
|
|
37
37
|
mode: 'warn',
|
|
38
38
|
strict_redaction: false,
|
|
39
39
|
block_sensitive_paths: true,
|
|
40
|
+
token_detection: {
|
|
41
|
+
enabled: true,
|
|
42
|
+
entropy: { enabled: true, min_length: 32, min_entropy: 4.0 },
|
|
43
|
+
detectors: {},
|
|
44
|
+
},
|
|
40
45
|
},
|
|
41
46
|
markdown: {
|
|
42
47
|
max_items_per_section: 20,
|
|
@@ -56,7 +61,7 @@ export function defaultConfig(projectName, options = {}) {
|
|
|
56
61
|
},
|
|
57
62
|
governance: {
|
|
58
63
|
approval_policy: 'review',
|
|
59
|
-
curators: [],
|
|
64
|
+
curators: options.curatorName ? [options.curatorName] : [],
|
|
60
65
|
review_sla_hours: 24,
|
|
61
66
|
},
|
|
62
67
|
reputation: {
|