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,424 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentRun lifecycle — concrete execution attempts for an Assignment.
|
|
3
|
+
*
|
|
4
|
+
* Assignment stays the business coordination object.
|
|
5
|
+
* AgentRun tracks one concrete launch / pickup / retry attempt.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import { AgentRunSchema } from './schema.js';
|
|
11
|
+
import { resolveEntityDir } from './io.js';
|
|
12
|
+
import { mutate } from './mutation-pipeline.js';
|
|
13
|
+
import { nowISO, generateIdWithLabel } from './ids.js';
|
|
14
|
+
import { JsonStore } from './json-store.js';
|
|
15
|
+
import { appendAuditEntry } from './audit.js';
|
|
16
|
+
import { appendEvent } from './event-log.js';
|
|
17
|
+
import { createRuntimeEvent } from './events.js';
|
|
18
|
+
function agentRunsDir(cwd, mode = 'read') {
|
|
19
|
+
return resolveEntityDir('runs', cwd, mode);
|
|
20
|
+
}
|
|
21
|
+
function ensureAgentRunsDir(cwd) {
|
|
22
|
+
const dir = agentRunsDir(cwd, 'write');
|
|
23
|
+
if (!fs.existsSync(dir)) {
|
|
24
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function agentRunStore(cwd) {
|
|
28
|
+
return new JsonStore({
|
|
29
|
+
dirPath: agentRunsDir(cwd, 'read'),
|
|
30
|
+
documentType: 'agent_run',
|
|
31
|
+
getId: (run) => run.id,
|
|
32
|
+
sort: (a, b) => {
|
|
33
|
+
const byAssignment = a.assignment_id.localeCompare(b.assignment_id);
|
|
34
|
+
if (byAssignment !== 0)
|
|
35
|
+
return byAssignment;
|
|
36
|
+
return a.created_at.localeCompare(b.created_at);
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
export function saveAgentRun(run, cwd) {
|
|
41
|
+
mutate({ cwd }, () => {
|
|
42
|
+
ensureAgentRunsDir(cwd);
|
|
43
|
+
const store = new JsonStore({
|
|
44
|
+
dirPath: agentRunsDir(cwd, 'write'),
|
|
45
|
+
documentType: 'agent_run',
|
|
46
|
+
getId: (item) => item.id,
|
|
47
|
+
sort: (a, b) => a.created_at.localeCompare(b.created_at),
|
|
48
|
+
});
|
|
49
|
+
store.save(AgentRunSchema.parse(run));
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
export function loadAgentRun(id, cwd) {
|
|
53
|
+
try {
|
|
54
|
+
return agentRunStore(cwd).load(id);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export function listAgentRuns(cwd, filter) {
|
|
61
|
+
let runs = agentRunStore(cwd).list();
|
|
62
|
+
if (filter?.status)
|
|
63
|
+
runs = runs.filter((run) => run.status === filter.status);
|
|
64
|
+
if (filter?.agent)
|
|
65
|
+
runs = runs.filter((run) => run.agent === filter.agent);
|
|
66
|
+
if (filter?.assignment_id)
|
|
67
|
+
runs = runs.filter((run) => run.assignment_id === filter.assignment_id);
|
|
68
|
+
if (filter?.claim_id)
|
|
69
|
+
runs = runs.filter((run) => run.claim_id === filter.claim_id);
|
|
70
|
+
if (filter?.plan_id)
|
|
71
|
+
runs = runs.filter((run) => run.plan_id === filter.plan_id);
|
|
72
|
+
if (filter?.sequence_id)
|
|
73
|
+
runs = runs.filter((run) => run.sequence_id === filter.sequence_id);
|
|
74
|
+
if (filter?.transport)
|
|
75
|
+
runs = runs.filter((run) => run.transport === filter.transport);
|
|
76
|
+
return runs;
|
|
77
|
+
}
|
|
78
|
+
export function generateAgentRunId(cwd) {
|
|
79
|
+
return generateIdWithLabel('runs', cwd);
|
|
80
|
+
}
|
|
81
|
+
function nextAttemptIndex(assignmentId, cwd) {
|
|
82
|
+
const existing = listAgentRuns(cwd, { assignment_id: assignmentId });
|
|
83
|
+
if (existing.length === 0)
|
|
84
|
+
return 1;
|
|
85
|
+
return Math.max(...existing.map((run) => run.attempt_index)) + 1;
|
|
86
|
+
}
|
|
87
|
+
export function findLatestAgentRunForAssignment(assignmentId, cwd) {
|
|
88
|
+
const runs = listAgentRuns(cwd, { assignment_id: assignmentId });
|
|
89
|
+
if (runs.length === 0)
|
|
90
|
+
return undefined;
|
|
91
|
+
return [...runs].sort((left, right) => {
|
|
92
|
+
if (left.attempt_index !== right.attempt_index)
|
|
93
|
+
return right.attempt_index - left.attempt_index;
|
|
94
|
+
return right.created_at.localeCompare(left.created_at);
|
|
95
|
+
})[0];
|
|
96
|
+
}
|
|
97
|
+
const VALID_TRANSITIONS = new Map([
|
|
98
|
+
['created', new Set(['launching', 'waiting_input', 'running', 'cancelled', 'interrupted'])],
|
|
99
|
+
['launching', new Set(['waiting_input', 'running', 'failed', 'cancelled', 'timed_out', 'interrupted'])],
|
|
100
|
+
['waiting_input', new Set(['launching', 'running', 'blocked', 'cancelled', 'timed_out', 'interrupted'])],
|
|
101
|
+
['running', new Set(['blocked', 'completed', 'failed', 'cancelled', 'timed_out', 'interrupted'])],
|
|
102
|
+
['blocked', new Set(['running', 'cancelled', 'timed_out', 'interrupted'])],
|
|
103
|
+
['completed', new Set()],
|
|
104
|
+
['failed', new Set()],
|
|
105
|
+
['cancelled', new Set()],
|
|
106
|
+
['timed_out', new Set()],
|
|
107
|
+
['interrupted', new Set()],
|
|
108
|
+
]);
|
|
109
|
+
export function createAgentRun(options, cwd) {
|
|
110
|
+
const generated = options.id ? undefined : generateAgentRunId(cwd);
|
|
111
|
+
const now = nowISO();
|
|
112
|
+
const run = AgentRunSchema.parse({
|
|
113
|
+
schema_version: 1,
|
|
114
|
+
id: options.id ?? generated.id,
|
|
115
|
+
short_label: options.short_label ?? generated.short_label,
|
|
116
|
+
assignment_id: options.assignment_id,
|
|
117
|
+
claim_id: options.claim_id,
|
|
118
|
+
message_id: options.message_id,
|
|
119
|
+
plan_id: options.plan_id,
|
|
120
|
+
sequence_id: options.sequence_id,
|
|
121
|
+
retry_of_run_id: options.retry_of_run_id,
|
|
122
|
+
attempt_index: options.attempt_index ?? nextAttemptIndex(options.assignment_id, cwd),
|
|
123
|
+
agent: options.agent,
|
|
124
|
+
agent_id: options.agent_id,
|
|
125
|
+
session_id: options.session_id,
|
|
126
|
+
transport: options.transport,
|
|
127
|
+
status: options.status ?? 'created',
|
|
128
|
+
status_reason: options.status_reason,
|
|
129
|
+
scope: options.scope,
|
|
130
|
+
description: options.description,
|
|
131
|
+
worktree_path: options.worktree_path,
|
|
132
|
+
command: options.command,
|
|
133
|
+
shell: options.shell,
|
|
134
|
+
pid: options.pid,
|
|
135
|
+
provider_run_id: options.provider_run_id,
|
|
136
|
+
created_at: now,
|
|
137
|
+
updated_at: now,
|
|
138
|
+
last_event_at: now,
|
|
139
|
+
...(options.status === 'launching' ? { launched_at: now } : {}),
|
|
140
|
+
...(options.status === 'running' ? { started_at: now, launched_at: now } : {}),
|
|
141
|
+
...(options.status === 'waiting_input' ? { launched_at: now } : {}),
|
|
142
|
+
artifacts: [],
|
|
143
|
+
tags: options.tags ?? [],
|
|
144
|
+
});
|
|
145
|
+
saveAgentRun(run, cwd);
|
|
146
|
+
emitAgentRunEvent(run, 'run_created', options.agent, cwd);
|
|
147
|
+
if (run.status !== 'created') {
|
|
148
|
+
emitAgentRunEvent(run, `run_${run.status}`, options.agent, cwd);
|
|
149
|
+
}
|
|
150
|
+
appendAuditEntry({
|
|
151
|
+
actor: options.agent,
|
|
152
|
+
actor_id: options.agent_id,
|
|
153
|
+
action: 'create',
|
|
154
|
+
item_id: run.id,
|
|
155
|
+
item_type: 'agent_run',
|
|
156
|
+
after: { assignment_id: run.assignment_id, status: run.status, transport: run.transport },
|
|
157
|
+
scope: run.scope,
|
|
158
|
+
session_id: run.session_id,
|
|
159
|
+
}, cwd);
|
|
160
|
+
return run;
|
|
161
|
+
}
|
|
162
|
+
export function transitionAgentRun(id, newStatus, options = {}, cwd) {
|
|
163
|
+
const run = loadAgentRun(id, cwd);
|
|
164
|
+
if (!run)
|
|
165
|
+
throw new Error(`AgentRun not found: ${id}`);
|
|
166
|
+
if (run.status === newStatus) {
|
|
167
|
+
const now = nowISO();
|
|
168
|
+
run.updated_at = now;
|
|
169
|
+
run.last_event_at = now;
|
|
170
|
+
if (options.session_id)
|
|
171
|
+
run.session_id = options.session_id;
|
|
172
|
+
if (options.status_reason)
|
|
173
|
+
run.status_reason = options.status_reason;
|
|
174
|
+
if (options.pid)
|
|
175
|
+
run.pid = options.pid;
|
|
176
|
+
if (options.provider_run_id)
|
|
177
|
+
run.provider_run_id = options.provider_run_id;
|
|
178
|
+
if (options.artifacts?.length)
|
|
179
|
+
run.artifacts = [...run.artifacts, ...options.artifacts];
|
|
180
|
+
saveAgentRun(run, cwd);
|
|
181
|
+
return { run, previous_status: newStatus, idempotent: true };
|
|
182
|
+
}
|
|
183
|
+
const allowed = VALID_TRANSITIONS.get(run.status);
|
|
184
|
+
if (!allowed?.has(newStatus)) {
|
|
185
|
+
throw new Error(`Invalid AgentRun transition: ${run.status} -> ${newStatus}`);
|
|
186
|
+
}
|
|
187
|
+
const previous_status = run.status;
|
|
188
|
+
const now = nowISO();
|
|
189
|
+
run.status = newStatus;
|
|
190
|
+
run.updated_at = now;
|
|
191
|
+
run.last_event_at = now;
|
|
192
|
+
if (options.session_id)
|
|
193
|
+
run.session_id = options.session_id;
|
|
194
|
+
if (options.status_reason)
|
|
195
|
+
run.status_reason = options.status_reason;
|
|
196
|
+
if (options.error_message)
|
|
197
|
+
run.error_message = options.error_message;
|
|
198
|
+
if (options.artifacts?.length)
|
|
199
|
+
run.artifacts = [...run.artifacts, ...options.artifacts];
|
|
200
|
+
if (options.pid)
|
|
201
|
+
run.pid = options.pid;
|
|
202
|
+
if (options.provider_run_id)
|
|
203
|
+
run.provider_run_id = options.provider_run_id;
|
|
204
|
+
switch (newStatus) {
|
|
205
|
+
case 'launching':
|
|
206
|
+
run.launched_at = now;
|
|
207
|
+
break;
|
|
208
|
+
case 'waiting_input':
|
|
209
|
+
run.launched_at ??= now;
|
|
210
|
+
break;
|
|
211
|
+
case 'running':
|
|
212
|
+
run.launched_at ??= now;
|
|
213
|
+
run.started_at = now;
|
|
214
|
+
break;
|
|
215
|
+
case 'blocked':
|
|
216
|
+
run.blocked_at = now;
|
|
217
|
+
break;
|
|
218
|
+
case 'completed':
|
|
219
|
+
run.completed_at = now;
|
|
220
|
+
break;
|
|
221
|
+
case 'failed':
|
|
222
|
+
run.failed_at = now;
|
|
223
|
+
break;
|
|
224
|
+
case 'cancelled':
|
|
225
|
+
run.cancelled_at = now;
|
|
226
|
+
break;
|
|
227
|
+
case 'timed_out':
|
|
228
|
+
run.timed_out_at = now;
|
|
229
|
+
break;
|
|
230
|
+
case 'interrupted':
|
|
231
|
+
run.interrupted_at = now;
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
saveAgentRun(run, cwd);
|
|
235
|
+
emitAgentRunEvent(run, `run_${newStatus}`, options.actor, cwd);
|
|
236
|
+
appendAuditEntry({
|
|
237
|
+
actor: options.actor ?? run.agent,
|
|
238
|
+
actor_id: options.actor_id,
|
|
239
|
+
action: 'update',
|
|
240
|
+
item_id: run.id,
|
|
241
|
+
item_type: 'agent_run',
|
|
242
|
+
before: { status: previous_status },
|
|
243
|
+
after: { status: newStatus, reason: options.status_reason },
|
|
244
|
+
scope: run.scope,
|
|
245
|
+
session_id: options.session_id,
|
|
246
|
+
}, cwd);
|
|
247
|
+
return { run, previous_status };
|
|
248
|
+
}
|
|
249
|
+
export function recordAgentRunProgress(id, options = {}, cwd) {
|
|
250
|
+
const run = loadAgentRun(id, cwd);
|
|
251
|
+
if (!run)
|
|
252
|
+
throw new Error(`AgentRun not found: ${id}`);
|
|
253
|
+
if (run.status === 'created' || run.status === 'launching' || run.status === 'waiting_input' || run.status === 'blocked') {
|
|
254
|
+
transitionAgentRun(id, 'running', {
|
|
255
|
+
actor: options.actor,
|
|
256
|
+
actor_id: options.actor_id,
|
|
257
|
+
session_id: options.session_id,
|
|
258
|
+
status_reason: options.message,
|
|
259
|
+
artifacts: options.artifacts,
|
|
260
|
+
}, cwd);
|
|
261
|
+
return loadAgentRun(id, cwd);
|
|
262
|
+
}
|
|
263
|
+
if (run.status !== 'running') {
|
|
264
|
+
throw new Error(`Cannot record progress: run ${id} is ${run.status}, expected running`);
|
|
265
|
+
}
|
|
266
|
+
const now = nowISO();
|
|
267
|
+
run.updated_at = now;
|
|
268
|
+
run.last_event_at = now;
|
|
269
|
+
if (options.session_id)
|
|
270
|
+
run.session_id = options.session_id;
|
|
271
|
+
if (options.message)
|
|
272
|
+
run.status_reason = options.message;
|
|
273
|
+
if (options.artifacts?.length)
|
|
274
|
+
run.artifacts = [...run.artifacts, ...options.artifacts];
|
|
275
|
+
saveAgentRun(run, cwd);
|
|
276
|
+
emitAgentRunEvent(run, 'run_running', options.actor, cwd);
|
|
277
|
+
return run;
|
|
278
|
+
}
|
|
279
|
+
/** Check if a run transition is allowed before attempting it (best-effort sync). */
|
|
280
|
+
function canTransitionRun(run, target) {
|
|
281
|
+
if (run.status === target)
|
|
282
|
+
return true; // idempotent
|
|
283
|
+
const allowed = VALID_TRANSITIONS.get(run.status);
|
|
284
|
+
return !!allowed?.has(target);
|
|
285
|
+
}
|
|
286
|
+
export function syncAgentRunFromAssignmentTransition(assignment, newStatus, options = {}, cwd) {
|
|
287
|
+
const run = findLatestAgentRunForAssignment(assignment.id, cwd);
|
|
288
|
+
if (!run)
|
|
289
|
+
return;
|
|
290
|
+
switch (newStatus) {
|
|
291
|
+
case 'accepted': {
|
|
292
|
+
const now = nowISO();
|
|
293
|
+
run.updated_at = now;
|
|
294
|
+
run.last_event_at = now;
|
|
295
|
+
if (options.session_id)
|
|
296
|
+
run.session_id = options.session_id;
|
|
297
|
+
if (options.status_reason)
|
|
298
|
+
run.status_reason = options.status_reason;
|
|
299
|
+
saveAgentRun(run, cwd);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
case 'started':
|
|
303
|
+
if (!canTransitionRun(run, 'running'))
|
|
304
|
+
return;
|
|
305
|
+
transitionAgentRun(run.id, 'running', {
|
|
306
|
+
actor: options.actor,
|
|
307
|
+
actor_id: options.actor_id,
|
|
308
|
+
session_id: options.session_id,
|
|
309
|
+
status_reason: options.status_reason,
|
|
310
|
+
}, cwd);
|
|
311
|
+
return;
|
|
312
|
+
case 'completed':
|
|
313
|
+
if (!canTransitionRun(run, 'completed'))
|
|
314
|
+
return;
|
|
315
|
+
transitionAgentRun(run.id, 'completed', {
|
|
316
|
+
actor: options.actor,
|
|
317
|
+
actor_id: options.actor_id,
|
|
318
|
+
session_id: options.session_id,
|
|
319
|
+
status_reason: options.status_reason,
|
|
320
|
+
artifacts: options.artifacts,
|
|
321
|
+
}, cwd);
|
|
322
|
+
return;
|
|
323
|
+
case 'failed':
|
|
324
|
+
if (!canTransitionRun(run, 'failed'))
|
|
325
|
+
return;
|
|
326
|
+
transitionAgentRun(run.id, 'failed', {
|
|
327
|
+
actor: options.actor,
|
|
328
|
+
actor_id: options.actor_id,
|
|
329
|
+
session_id: options.session_id,
|
|
330
|
+
status_reason: options.status_reason,
|
|
331
|
+
artifacts: options.artifacts,
|
|
332
|
+
error_message: options.error_message,
|
|
333
|
+
}, cwd);
|
|
334
|
+
return;
|
|
335
|
+
case 'blocked':
|
|
336
|
+
if (!canTransitionRun(run, 'blocked'))
|
|
337
|
+
return;
|
|
338
|
+
transitionAgentRun(run.id, 'blocked', {
|
|
339
|
+
actor: options.actor,
|
|
340
|
+
actor_id: options.actor_id,
|
|
341
|
+
session_id: options.session_id,
|
|
342
|
+
status_reason: options.status_reason,
|
|
343
|
+
}, cwd);
|
|
344
|
+
return;
|
|
345
|
+
case 'timed_out':
|
|
346
|
+
if (!canTransitionRun(run, 'timed_out'))
|
|
347
|
+
return;
|
|
348
|
+
transitionAgentRun(run.id, 'timed_out', {
|
|
349
|
+
actor: options.actor,
|
|
350
|
+
actor_id: options.actor_id,
|
|
351
|
+
session_id: options.session_id,
|
|
352
|
+
status_reason: options.status_reason,
|
|
353
|
+
}, cwd);
|
|
354
|
+
return;
|
|
355
|
+
case 'expired':
|
|
356
|
+
case 'rerouted':
|
|
357
|
+
if (!canTransitionRun(run, 'interrupted'))
|
|
358
|
+
return;
|
|
359
|
+
transitionAgentRun(run.id, 'interrupted', {
|
|
360
|
+
actor: options.actor,
|
|
361
|
+
actor_id: options.actor_id,
|
|
362
|
+
session_id: options.session_id,
|
|
363
|
+
status_reason: options.status_reason ?? `${newStatus} via assignment lifecycle`,
|
|
364
|
+
}, cwd);
|
|
365
|
+
return;
|
|
366
|
+
case 'retrying':
|
|
367
|
+
if (!['completed', 'failed', 'cancelled', 'timed_out', 'interrupted'].includes(run.status)) {
|
|
368
|
+
if (!canTransitionRun(run, 'interrupted'))
|
|
369
|
+
return;
|
|
370
|
+
transitionAgentRun(run.id, 'interrupted', {
|
|
371
|
+
actor: options.actor,
|
|
372
|
+
actor_id: options.actor_id,
|
|
373
|
+
session_id: options.session_id,
|
|
374
|
+
status_reason: options.status_reason ?? 'Retry requested at assignment level',
|
|
375
|
+
}, cwd);
|
|
376
|
+
}
|
|
377
|
+
return;
|
|
378
|
+
default:
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
function emitAgentRunEvent(run, action, actor, cwd) {
|
|
383
|
+
const text = `${run.description} [${run.status}/${run.transport}]${run.status_reason ? ` — ${run.status_reason}` : ''}`;
|
|
384
|
+
appendEvent({
|
|
385
|
+
ts: nowISO(),
|
|
386
|
+
agent: actor ?? run.agent,
|
|
387
|
+
agent_id: run.agent_id,
|
|
388
|
+
action,
|
|
389
|
+
item_type: 'agent_run',
|
|
390
|
+
item_id: run.id,
|
|
391
|
+
summary: `${run.status}: ${run.description.slice(0, 80)}`,
|
|
392
|
+
}, cwd);
|
|
393
|
+
try {
|
|
394
|
+
createRuntimeEvent({
|
|
395
|
+
agent: actor ?? run.agent,
|
|
396
|
+
agent_id: run.agent_id,
|
|
397
|
+
session_id: run.session_id,
|
|
398
|
+
event_type: action,
|
|
399
|
+
text,
|
|
400
|
+
tags: ['agent-runtime', 'run'],
|
|
401
|
+
assignment_id: run.assignment_id,
|
|
402
|
+
run_id: run.id,
|
|
403
|
+
claim_id: run.claim_id,
|
|
404
|
+
message_id: run.message_id,
|
|
405
|
+
plan_id: run.plan_id,
|
|
406
|
+
sequence_id: run.sequence_id,
|
|
407
|
+
scope: run.scope,
|
|
408
|
+
transport: run.transport,
|
|
409
|
+
status: run.status,
|
|
410
|
+
status_reason: run.status_reason,
|
|
411
|
+
related_paths: [run.scope],
|
|
412
|
+
metadata: {
|
|
413
|
+
attempt_index: run.attempt_index,
|
|
414
|
+
retry_of_run_id: run.retry_of_run_id,
|
|
415
|
+
provider_run_id: run.provider_run_id,
|
|
416
|
+
protocol: 'brainclaw.agent_runtime.v0',
|
|
417
|
+
},
|
|
418
|
+
}, cwd);
|
|
419
|
+
}
|
|
420
|
+
catch {
|
|
421
|
+
/* best-effort: runtime event emission should not break run lifecycle */
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
//# sourceMappingURL=agentruns.js.map
|
|
@@ -32,17 +32,19 @@ export function detectAiAgent(env = process.env, homeDir = os.homedir()) {
|
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
34
|
// Claude Code — tested BEFORE Copilot because both can be present in VS Code.
|
|
35
|
-
// CLAUDE_CODE_VERSION is set by Claude Code CLI; CLAUDECODE is set by the VS Code extension
|
|
36
|
-
|
|
35
|
+
// CLAUDE_CODE_VERSION is set by Claude Code CLI; CLAUDECODE is set by the VS Code extension;
|
|
36
|
+
// CLAUDE_AGENT_SDK_VERSION is set on remote/SSH; CLAUDE_CODE_ENTRYPOINT indicates launch source.
|
|
37
|
+
if (env.CLAUDE_CODE_VERSION || env.CLAUDECODE || env.CLAUDE_AGENT_SDK_VERSION || env.CLAUDE_CODE_ENTRYPOINT || env.ANTHROPIC_AI_PRODUCT === 'claude-code') {
|
|
38
|
+
const source = env.CLAUDE_CODE_VERSION ? 'CLAUDE_CODE_VERSION env var'
|
|
39
|
+
: env.CLAUDECODE ? 'CLAUDECODE env var'
|
|
40
|
+
: env.CLAUDE_AGENT_SDK_VERSION ? 'CLAUDE_AGENT_SDK_VERSION env var'
|
|
41
|
+
: env.CLAUDE_CODE_ENTRYPOINT ? 'CLAUDE_CODE_ENTRYPOINT env var'
|
|
42
|
+
: 'ANTHROPIC_AI_PRODUCT env var';
|
|
37
43
|
return {
|
|
38
44
|
name: 'claude-code',
|
|
39
45
|
kind: 'agent',
|
|
40
46
|
trust_level: 'trusted',
|
|
41
|
-
detection_source:
|
|
42
|
-
? 'CLAUDE_CODE_VERSION env var'
|
|
43
|
-
: env.CLAUDECODE
|
|
44
|
-
? 'CLAUDECODE env var'
|
|
45
|
-
: 'ANTHROPIC_AI_PRODUCT env var',
|
|
47
|
+
detection_source: source,
|
|
46
48
|
};
|
|
47
49
|
}
|
|
48
50
|
// Cursor IDE
|
|
@@ -86,13 +88,20 @@ export function detectAiAgent(env = process.env, homeDir = os.homedir()) {
|
|
|
86
88
|
detection_source: env.GITHUB_COPILOT_PRODUCT ? 'GITHUB_COPILOT_PRODUCT env var' : 'GITHUB_COPILOT_TOKEN env var',
|
|
87
89
|
};
|
|
88
90
|
}
|
|
89
|
-
// OpenAI Codex CLI
|
|
90
|
-
|
|
91
|
+
// OpenAI Codex CLI — detect via active runtime env vars, not ~/.codex directory
|
|
92
|
+
// (the directory persists after install and causes permanent false positives).
|
|
93
|
+
// Real Codex env vars observed: CODEX_THREAD_ID, CODEX_CI, CODEX_INTERNAL_ORIGINATOR_OVERRIDE.
|
|
94
|
+
if (env.CODEX_THREAD_ID || env.CODEX_CI || env.CODEX_INTERNAL_ORIGINATOR_OVERRIDE || env.CODEX_AGENT || env.CODEX_SESSION_ID) {
|
|
95
|
+
const source = env.CODEX_THREAD_ID ? 'CODEX_THREAD_ID env var'
|
|
96
|
+
: env.CODEX_CI ? 'CODEX_CI env var'
|
|
97
|
+
: env.CODEX_INTERNAL_ORIGINATOR_OVERRIDE ? 'CODEX_INTERNAL_ORIGINATOR_OVERRIDE env var'
|
|
98
|
+
: env.CODEX_AGENT ? 'CODEX_AGENT env var'
|
|
99
|
+
: 'CODEX_SESSION_ID env var';
|
|
91
100
|
return {
|
|
92
101
|
name: 'codex',
|
|
93
102
|
kind: 'agent',
|
|
94
103
|
trust_level: 'trusted',
|
|
95
|
-
detection_source:
|
|
104
|
+
detection_source: source,
|
|
96
105
|
};
|
|
97
106
|
}
|
|
98
107
|
// OpenCode
|
|
@@ -140,6 +149,18 @@ export function detectAiAgent(env = process.env, homeDir = os.homedir()) {
|
|
|
140
149
|
detection_source: env.OPENCLAW_SESSION_ID || env.OPENCLAW_AGENT ? 'OPENCLAW_* env var' : '~/.openclaw directory',
|
|
141
150
|
};
|
|
142
151
|
}
|
|
152
|
+
// Mistral Vibe — no dedicated session env var documented (per pln#489 research).
|
|
153
|
+
// Detect via the user-level config dir (~/.vibe/) which Mistral creates on first
|
|
154
|
+
// run, or via VIBE_HOME override. Tested last so other agents with dedicated
|
|
155
|
+
// session env vars take precedence.
|
|
156
|
+
if (env.VIBE_HOME || fs.existsSync(path.join(homeDir, '.vibe'))) {
|
|
157
|
+
return {
|
|
158
|
+
name: 'mistral-vibe',
|
|
159
|
+
kind: 'agent',
|
|
160
|
+
trust_level: 'trusted',
|
|
161
|
+
detection_source: env.VIBE_HOME ? 'VIBE_HOME env var' : '~/.vibe directory',
|
|
162
|
+
};
|
|
163
|
+
}
|
|
143
164
|
return undefined;
|
|
144
165
|
}
|
|
145
166
|
/**
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { resolveEntityDir } from './io.js';
|
|
4
|
+
import { logger } from './logger.js';
|
|
5
|
+
/** Default age threshold: items older than 30 days are eligible for archival. */
|
|
6
|
+
const DEFAULT_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
|
|
7
|
+
/**
|
|
8
|
+
* Archive done plans and closed handoffs older than maxAgeMs to JSONL cold storage.
|
|
9
|
+
* Each entity type gets its own archive file (e.g. coordination/plans/archive.jsonl).
|
|
10
|
+
* Archived items are appended as one JSON line per item, then the source file is deleted.
|
|
11
|
+
* This is lossless — all data is preserved in the archive.
|
|
12
|
+
*/
|
|
13
|
+
export function archiveStalePlansAndHandoffs(cwd, maxAgeMs = DEFAULT_MAX_AGE_MS) {
|
|
14
|
+
const results = [];
|
|
15
|
+
const cutoff = new Date(Date.now() - maxAgeMs).toISOString();
|
|
16
|
+
results.push(archiveEntity('plans', cutoff, (item) => {
|
|
17
|
+
return item.status === 'done' || item.status === 'dropped';
|
|
18
|
+
}, cwd));
|
|
19
|
+
results.push(archiveEntity('handoffs', cutoff, (item) => {
|
|
20
|
+
return item.status === 'closed';
|
|
21
|
+
}, cwd));
|
|
22
|
+
return results.filter(r => r.archived > 0);
|
|
23
|
+
}
|
|
24
|
+
function archiveEntity(entity, cutoffDate, isEligible, cwd) {
|
|
25
|
+
const dir = resolveEntityDir(entity, cwd ?? process.cwd(), 'read');
|
|
26
|
+
const archivePath = path.join(dir, 'archive.jsonl');
|
|
27
|
+
if (!fs.existsSync(dir)) {
|
|
28
|
+
return { archived: 0, entity, archivePath };
|
|
29
|
+
}
|
|
30
|
+
let archived = 0;
|
|
31
|
+
const files = fs.readdirSync(dir).filter(f => f.endsWith('.json') && f !== 'archive.json');
|
|
32
|
+
for (const file of files) {
|
|
33
|
+
const filePath = path.join(dir, file);
|
|
34
|
+
try {
|
|
35
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
36
|
+
const item = JSON.parse(content);
|
|
37
|
+
// Check eligibility: correct status AND older than cutoff
|
|
38
|
+
const createdAt = (item.completed_at ?? item.updated_at ?? item.created_at);
|
|
39
|
+
if (!isEligible(item))
|
|
40
|
+
continue;
|
|
41
|
+
if (createdAt && createdAt > cutoffDate)
|
|
42
|
+
continue;
|
|
43
|
+
// Append to archive JSONL
|
|
44
|
+
fs.mkdirSync(path.dirname(archivePath), { recursive: true });
|
|
45
|
+
fs.appendFileSync(archivePath, JSON.stringify(item) + '\n', 'utf-8');
|
|
46
|
+
// Delete source file
|
|
47
|
+
fs.unlinkSync(filePath);
|
|
48
|
+
archived++;
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
logger.debug(`Failed to archive ${entity}/${file}:`, err);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return { archived, entity, archivePath };
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Read archived items from a JSONL archive file.
|
|
58
|
+
* Useful for search/audit across archived entities.
|
|
59
|
+
*/
|
|
60
|
+
export function readArchive(entity, cwd) {
|
|
61
|
+
const dir = resolveEntityDir(entity, cwd ?? process.cwd(), 'read');
|
|
62
|
+
const archivePath = path.join(dir, 'archive.jsonl');
|
|
63
|
+
if (!fs.existsSync(archivePath))
|
|
64
|
+
return [];
|
|
65
|
+
const lines = fs.readFileSync(archivePath, 'utf-8').split('\n').filter(Boolean);
|
|
66
|
+
const items = [];
|
|
67
|
+
for (const line of lines) {
|
|
68
|
+
try {
|
|
69
|
+
items.push(JSON.parse(line));
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// skip malformed lines
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return items;
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=archival.js.map
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Assignment timeout sweeper — detects stuck/expired assignments.
|
|
3
|
+
*
|
|
4
|
+
* Runs opportunistically (no daemon): integrated into dispatch().
|
|
5
|
+
* Future: integrate into session_start() and expose as CLI `brainclaw sweep`.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
import { listAssignments, transitionAssignment } from './assignments.js';
|
|
10
|
+
// ── Sweeper ──────────────────────────────────────────────────
|
|
11
|
+
/**
|
|
12
|
+
* Scan all active assignments and timeout those past their TTL.
|
|
13
|
+
*
|
|
14
|
+
* - `started` assignments with no heartbeat within `heartbeat_ttl_ms` → `timed_out`
|
|
15
|
+
* - `offered` assignments not accepted within `acceptance_ttl_ms` → `expired`
|
|
16
|
+
*
|
|
17
|
+
* @param cwd - Project root
|
|
18
|
+
* @param options.nowMs - Override current time for testing
|
|
19
|
+
* @param options.actor - Actor name for audit trail (default: 'sweeper')
|
|
20
|
+
*/
|
|
21
|
+
export function sweepAssignments(cwd, options) {
|
|
22
|
+
const now = options?.nowMs ?? Date.now();
|
|
23
|
+
const actor = options?.actor ?? 'sweeper';
|
|
24
|
+
const result = { timed_out: [], expired: [] };
|
|
25
|
+
const all = listAssignments(cwd);
|
|
26
|
+
for (const assignment of all) {
|
|
27
|
+
// Check started assignments for heartbeat timeout
|
|
28
|
+
if (assignment.status === 'started') {
|
|
29
|
+
const lastBeat = assignment.last_heartbeat_at ?? assignment.started_at;
|
|
30
|
+
if (!lastBeat)
|
|
31
|
+
continue;
|
|
32
|
+
const ageMs = now - new Date(lastBeat).getTime();
|
|
33
|
+
if (ageMs > assignment.heartbeat_ttl_ms) {
|
|
34
|
+
try {
|
|
35
|
+
transitionAssignment(assignment.id, 'timed_out', {
|
|
36
|
+
status_reason: `No heartbeat for ${Math.round(ageMs / 60_000)} minutes (TTL: ${Math.round(assignment.heartbeat_ttl_ms / 60_000)}min)`,
|
|
37
|
+
actor,
|
|
38
|
+
}, cwd);
|
|
39
|
+
result.timed_out.push({ assignment_id: assignment.id, agent: assignment.agent, age_ms: ageMs });
|
|
40
|
+
}
|
|
41
|
+
catch { /* skip: transition may fail if status changed concurrently */ }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// Check accepted assignments that never started (accepted but worker died before starting)
|
|
45
|
+
if (assignment.status === 'accepted') {
|
|
46
|
+
const acceptedAt = assignment.accepted_at ?? assignment.last_heartbeat_at;
|
|
47
|
+
if (!acceptedAt)
|
|
48
|
+
continue;
|
|
49
|
+
const ageMs = now - new Date(acceptedAt).getTime();
|
|
50
|
+
// Use acceptance_ttl for accepted→timed_out (same window: agent should start quickly after accepting)
|
|
51
|
+
if (ageMs > assignment.acceptance_ttl_ms) {
|
|
52
|
+
try {
|
|
53
|
+
transitionAssignment(assignment.id, 'timed_out', {
|
|
54
|
+
status_reason: `Accepted but not started within ${Math.round(ageMs / 60_000)} minutes`,
|
|
55
|
+
actor,
|
|
56
|
+
}, cwd);
|
|
57
|
+
result.timed_out.push({ assignment_id: assignment.id, agent: assignment.agent, age_ms: ageMs });
|
|
58
|
+
}
|
|
59
|
+
catch { /* skip */ }
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Check offered assignments for acceptance timeout
|
|
63
|
+
if (assignment.status === 'offered') {
|
|
64
|
+
const offeredAt = assignment.offered_at;
|
|
65
|
+
if (!offeredAt)
|
|
66
|
+
continue;
|
|
67
|
+
const ageMs = now - new Date(offeredAt).getTime();
|
|
68
|
+
if (ageMs > assignment.acceptance_ttl_ms) {
|
|
69
|
+
try {
|
|
70
|
+
transitionAssignment(assignment.id, 'expired', {
|
|
71
|
+
status_reason: `Not accepted within ${Math.round(ageMs / 60_000)} minutes (TTL: ${Math.round(assignment.acceptance_ttl_ms / 60_000)}min)`,
|
|
72
|
+
actor,
|
|
73
|
+
}, cwd);
|
|
74
|
+
result.expired.push({ assignment_id: assignment.id, agent: assignment.agent, age_ms: ageMs });
|
|
75
|
+
}
|
|
76
|
+
catch { /* skip */ }
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=assignment-sweeper.js.map
|