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,345 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentRun reconciliation — silent-completion recovery + post-spawn health-check.
|
|
3
|
+
*
|
|
4
|
+
* Closes two gaps observed empirically in May 2026 dispatches:
|
|
5
|
+
*
|
|
6
|
+
* 1. **Silent completion** (pln#496 step stp_344f99b3). Dispatched workers
|
|
7
|
+
* sometimes finish their work and exit without ever calling
|
|
8
|
+
* `bclaw_assignment_update(status: 'completed')`. The agent_run stays in
|
|
9
|
+
* `running` forever, blocking review loops that wait on `run_completed`
|
|
10
|
+
* to converge. Concrete witnesses: codex review of pln#494 (37 min silent
|
|
11
|
+
* run_running, no completion event) and codex review of pln#480 (2h26
|
|
12
|
+
* silent). Also: claude-code worker on pln#480 implementation — committed
|
|
13
|
+
* fine but never released the claim.
|
|
14
|
+
*
|
|
15
|
+
* 2. **Post-spawn health check** (pln#496 step stp_e2b4429c). The dispatcher
|
|
16
|
+
* facade returns `delivered_and_started` as soon as the spawn fires, even
|
|
17
|
+
* when the process dies milliseconds later or hangs without producing any
|
|
18
|
+
* life-sign. Callers have no way to distinguish "spawned, working" from
|
|
19
|
+
* "spawned, already dead". A 60s grace window followed by an evidence
|
|
20
|
+
* check tells callers when the spawn is unverified.
|
|
21
|
+
*
|
|
22
|
+
* Approach: lazy reconciliation, no daemon. A single `reconcileAgentRun()`
|
|
23
|
+
* function inspects evidence (process liveness, claim release, post-start
|
|
24
|
+
* commits on the worktree branch) and either transitions the run to a
|
|
25
|
+
* terminal state with `inferred=true` provenance or emits a synthetic
|
|
26
|
+
* `delivered_but_unverified` runtime event without changing run status.
|
|
27
|
+
*
|
|
28
|
+
* Callers integrate this at read paths so stale runs converge on access
|
|
29
|
+
* (bclaw_assignment_events, bclaw_loop intent=get) and the supervisor can
|
|
30
|
+
* also trigger it explicitly via `brainclaw doctor --dispatch` (separate
|
|
31
|
+
* step stp_8c072d75 — wired in later).
|
|
32
|
+
*
|
|
33
|
+
* @module
|
|
34
|
+
*/
|
|
35
|
+
import { spawnSync } from 'node:child_process';
|
|
36
|
+
import { loadAgentRun, transitionAgentRun, listAgentRuns } from './agentruns.js';
|
|
37
|
+
import { loadClaim } from './claims.js';
|
|
38
|
+
import { loadAssignment } from './assignments.js';
|
|
39
|
+
import { createRuntimeEvent } from './events.js';
|
|
40
|
+
import { nowISO } from './ids.js';
|
|
41
|
+
// ── Constants ──────────────────────────────────────────────────────────────
|
|
42
|
+
/**
|
|
43
|
+
* Minimum age before a run is eligible for reconciliation. Below this, the
|
|
44
|
+
* worker has not been given a fair chance to emit its first life-sign.
|
|
45
|
+
* Default 60 000 ms = 60 s — matches the pln#496 spec.
|
|
46
|
+
*/
|
|
47
|
+
export const DEFAULT_HEALTH_CHECK_GRACE_MS = 60_000;
|
|
48
|
+
/**
|
|
49
|
+
* Age past which a run with no evidence of life and a dead process is
|
|
50
|
+
* declared `failed` with `silent_termination_no_evidence`. Default 30 min.
|
|
51
|
+
*/
|
|
52
|
+
export const DEFAULT_STALE_AFTER_MS = 30 * 60_000;
|
|
53
|
+
const TERMINAL_STATUSES = new Set([
|
|
54
|
+
'completed', 'failed', 'cancelled', 'timed_out', 'interrupted',
|
|
55
|
+
]);
|
|
56
|
+
// ── Process liveness ───────────────────────────────────────────────────────
|
|
57
|
+
/**
|
|
58
|
+
* Cross-platform "is this PID alive" check. `process.kill(pid, 0)` does NOT
|
|
59
|
+
* actually send a signal — it queries existence. ESRCH = dead, EPERM = alive
|
|
60
|
+
* but owned by another user (still counts as alive for our purposes).
|
|
61
|
+
*
|
|
62
|
+
* Returns undefined when no PID is tracked on the run, so the caller can
|
|
63
|
+
* distinguish "definitely dead" from "we have no way to know".
|
|
64
|
+
*/
|
|
65
|
+
export function isProcessAlive(pid) {
|
|
66
|
+
if (pid === undefined || pid === 0)
|
|
67
|
+
return undefined;
|
|
68
|
+
try {
|
|
69
|
+
process.kill(pid, 0);
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
if (typeof err === 'object' && err && 'code' in err) {
|
|
74
|
+
const code = err.code;
|
|
75
|
+
if (code === 'ESRCH')
|
|
76
|
+
return false;
|
|
77
|
+
if (code === 'EPERM')
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// ── Evidence collection ────────────────────────────────────────────────────
|
|
84
|
+
function safeGit(args, cwd) {
|
|
85
|
+
try {
|
|
86
|
+
const result = spawnSync('git', args, { cwd, encoding: 'utf-8', windowsHide: true });
|
|
87
|
+
if (result.status !== 0)
|
|
88
|
+
return { ok: false, stdout: '' };
|
|
89
|
+
return { ok: true, stdout: (result.stdout ?? '').toString() };
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return { ok: false, stdout: '' };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* True when the worktree branch has at least one commit whose committer
|
|
97
|
+
* timestamp is at or after the run's start. A worker that committed its
|
|
98
|
+
* work — even without releasing the claim or emitting run_completed — leaves
|
|
99
|
+
* this evidence behind.
|
|
100
|
+
*
|
|
101
|
+
* Failure cases (returns false defensively, never throws):
|
|
102
|
+
* - run.worktree_path missing or no longer on disk
|
|
103
|
+
* - git not on PATH
|
|
104
|
+
* - branch detached / corrupt
|
|
105
|
+
*/
|
|
106
|
+
function hasPostStartCommitEvidence(run) {
|
|
107
|
+
if (!run.worktree_path)
|
|
108
|
+
return false;
|
|
109
|
+
if (!run.started_at && !run.created_at)
|
|
110
|
+
return false;
|
|
111
|
+
const startISO = run.started_at ?? run.created_at;
|
|
112
|
+
// git log on HEAD since the start timestamp, format author + commit times.
|
|
113
|
+
// We intentionally check committer time (%ct) because rebases preserve
|
|
114
|
+
// author time but reset committer time to the rebase moment, and we want
|
|
115
|
+
// to count the actual operation that landed on the branch.
|
|
116
|
+
const result = safeGit(['log', `--since=${startISO}`, '-n', '1', '--format=%H'], run.worktree_path);
|
|
117
|
+
if (!result.ok)
|
|
118
|
+
return false;
|
|
119
|
+
return result.stdout.trim().length > 0;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Best-effort evidence collection. Each individual signal is wrapped in
|
|
123
|
+
* try/catch so partial failures (missing claim, dead worktree path) do not
|
|
124
|
+
* propagate as errors — they just contribute zero evidence.
|
|
125
|
+
*
|
|
126
|
+
* `cwd` MUST be threaded so loadClaim / loadAssignment look in the right
|
|
127
|
+
* brainclaw store. Tests that supplied cwd to reconcileAgentRun but not
|
|
128
|
+
* here saw all evidence as zero because the loaders defaulted to
|
|
129
|
+
* process.cwd().
|
|
130
|
+
*/
|
|
131
|
+
export function collectEvidence(run, cwd, options) {
|
|
132
|
+
const now = options?.nowMs ?? Date.now();
|
|
133
|
+
const startISO = run.started_at ?? run.created_at;
|
|
134
|
+
const age_ms = now - new Date(startISO).getTime();
|
|
135
|
+
let has_post_start_commit = false;
|
|
136
|
+
try {
|
|
137
|
+
has_post_start_commit = hasPostStartCommitEvidence(run);
|
|
138
|
+
}
|
|
139
|
+
catch { /* defensive */ }
|
|
140
|
+
let claim_released = false;
|
|
141
|
+
try {
|
|
142
|
+
const claim = loadClaim(run.claim_id, cwd);
|
|
143
|
+
claim_released = claim?.status === 'released';
|
|
144
|
+
}
|
|
145
|
+
catch { /* defensive */ }
|
|
146
|
+
let assignment_completed = false;
|
|
147
|
+
try {
|
|
148
|
+
const assignment = loadAssignment(run.assignment_id, cwd);
|
|
149
|
+
assignment_completed = assignment?.status === 'completed';
|
|
150
|
+
}
|
|
151
|
+
catch { /* defensive */ }
|
|
152
|
+
const process_alive = isProcessAlive(run.pid);
|
|
153
|
+
return { age_ms, has_post_start_commit, claim_released, assignment_completed, process_alive };
|
|
154
|
+
}
|
|
155
|
+
function anyCompletionEvidence(evidence) {
|
|
156
|
+
return evidence.has_post_start_commit
|
|
157
|
+
|| evidence.claim_released
|
|
158
|
+
|| evidence.assignment_completed;
|
|
159
|
+
}
|
|
160
|
+
function describeEvidence(evidence) {
|
|
161
|
+
const reasons = [];
|
|
162
|
+
if (evidence.has_post_start_commit)
|
|
163
|
+
reasons.push('post-start commit on worktree branch');
|
|
164
|
+
if (evidence.claim_released)
|
|
165
|
+
reasons.push('claim released');
|
|
166
|
+
if (evidence.assignment_completed)
|
|
167
|
+
reasons.push('assignment marked completed');
|
|
168
|
+
if (reasons.length === 0) {
|
|
169
|
+
if (evidence.process_alive === false)
|
|
170
|
+
reasons.push('process dead, no completion signal');
|
|
171
|
+
else if (evidence.process_alive === true)
|
|
172
|
+
reasons.push('process still alive');
|
|
173
|
+
else
|
|
174
|
+
reasons.push('no PID tracked');
|
|
175
|
+
}
|
|
176
|
+
return reasons.join(' + ');
|
|
177
|
+
}
|
|
178
|
+
// ── Synthetic event for unverified spawns ──────────────────────────────────
|
|
179
|
+
function emitUnverifiedEvent(run, evidence, actor, cwd) {
|
|
180
|
+
try {
|
|
181
|
+
createRuntimeEvent({
|
|
182
|
+
agent: actor,
|
|
183
|
+
session_id: run.session_id,
|
|
184
|
+
event_type: 'run_running',
|
|
185
|
+
text: `Spawn unverified after ${Math.round(evidence.age_ms / 1000)}s — no life-sign detected (process_alive=${evidence.process_alive}, post_start_commit=${evidence.has_post_start_commit}, claim_released=${evidence.claim_released})`,
|
|
186
|
+
tags: ['agent-runtime', 'run', 'reconciler', 'health-check'],
|
|
187
|
+
assignment_id: run.assignment_id,
|
|
188
|
+
run_id: run.id,
|
|
189
|
+
claim_id: run.claim_id,
|
|
190
|
+
plan_id: run.plan_id,
|
|
191
|
+
sequence_id: run.sequence_id,
|
|
192
|
+
scope: run.scope,
|
|
193
|
+
transport: run.transport,
|
|
194
|
+
status: run.status,
|
|
195
|
+
status_reason: 'delivered_but_unverified',
|
|
196
|
+
related_paths: run.scope ? [run.scope] : [],
|
|
197
|
+
metadata: {
|
|
198
|
+
reconciler: true,
|
|
199
|
+
evidence_age_ms: evidence.age_ms,
|
|
200
|
+
protocol: 'brainclaw.agent_runtime.reconciler.v0',
|
|
201
|
+
},
|
|
202
|
+
}, cwd);
|
|
203
|
+
}
|
|
204
|
+
catch { /* best-effort */ }
|
|
205
|
+
}
|
|
206
|
+
// ── Public API ─────────────────────────────────────────────────────────────
|
|
207
|
+
/**
|
|
208
|
+
* Reconcile a single agent_run against runtime evidence.
|
|
209
|
+
*
|
|
210
|
+
* No-op for terminal statuses or runs younger than the grace window.
|
|
211
|
+
* For older runs:
|
|
212
|
+
* - If any completion evidence exists → transition to `completed` with
|
|
213
|
+
* `status_reason='inferred=true; …'`. This unblocks loops waiting on
|
|
214
|
+
* `run_completed` even when the worker forgot to call
|
|
215
|
+
* bclaw_assignment_update.
|
|
216
|
+
* - Else if process is provably dead and run is past the stale threshold
|
|
217
|
+
* → transition to `failed` with `status_reason='silent_termination_no_evidence'`.
|
|
218
|
+
* - Else if past grace but not yet stale, no evidence either way → emit a
|
|
219
|
+
* non-mutating `delivered_but_unverified` runtime event so callers can
|
|
220
|
+
* surface the uncertainty.
|
|
221
|
+
*
|
|
222
|
+
* The function is pure-evidence: it never inspects in-memory state of the
|
|
223
|
+
* dispatcher, so it can be called from any process / any session that has
|
|
224
|
+
* read access to the brainclaw store.
|
|
225
|
+
*/
|
|
226
|
+
export function reconcileAgentRun(runId, cwd, options = {}) {
|
|
227
|
+
const run = loadAgentRun(runId, cwd);
|
|
228
|
+
if (!run) {
|
|
229
|
+
const evidence = {
|
|
230
|
+
age_ms: 0, has_post_start_commit: false, claim_released: false,
|
|
231
|
+
assignment_completed: false, process_alive: undefined,
|
|
232
|
+
};
|
|
233
|
+
return {
|
|
234
|
+
run_id: runId, action: 'no_op', reason: 'run not found', evidence,
|
|
235
|
+
previous_status: 'created', current_status: 'created',
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
const previous_status = run.status;
|
|
239
|
+
const evidence = collectEvidence(run, cwd, { nowMs: options.nowMs });
|
|
240
|
+
// Never touch terminal runs — they already converged.
|
|
241
|
+
if (TERMINAL_STATUSES.has(run.status)) {
|
|
242
|
+
return {
|
|
243
|
+
run_id: runId, action: 'no_op', reason: `run already terminal (${run.status})`,
|
|
244
|
+
evidence, previous_status, current_status: run.status,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
const grace = options.healthCheckGraceMs ?? DEFAULT_HEALTH_CHECK_GRACE_MS;
|
|
248
|
+
const stale = options.staleAfterMs ?? DEFAULT_STALE_AFTER_MS;
|
|
249
|
+
const actor = options.actor ?? 'reconciler';
|
|
250
|
+
// Below grace window: too early to draw any conclusion. Caller should
|
|
251
|
+
// re-poll later.
|
|
252
|
+
if (evidence.age_ms < grace) {
|
|
253
|
+
return {
|
|
254
|
+
run_id: runId, action: 'no_op', reason: `under grace window (${grace}ms)`,
|
|
255
|
+
evidence, previous_status, current_status: run.status,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
// Recovery: any completion evidence outside the grace window → infer success.
|
|
259
|
+
if (anyCompletionEvidence(evidence)) {
|
|
260
|
+
try {
|
|
261
|
+
transitionAgentRun(runId, 'completed', {
|
|
262
|
+
actor,
|
|
263
|
+
status_reason: `inferred=true; evidence: ${describeEvidence(evidence)}`,
|
|
264
|
+
}, cwd);
|
|
265
|
+
return {
|
|
266
|
+
run_id: runId, action: 'inferred_completed',
|
|
267
|
+
reason: `inferred=true; ${describeEvidence(evidence)}`,
|
|
268
|
+
evidence, previous_status, current_status: 'completed',
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
catch (err) {
|
|
272
|
+
// Transition might fail if the run is no longer in a transitionable
|
|
273
|
+
// state (race with explicit completion). Treat as no-op.
|
|
274
|
+
return {
|
|
275
|
+
run_id: runId, action: 'no_op',
|
|
276
|
+
reason: `transition rejected: ${err instanceof Error ? err.message : String(err)}`,
|
|
277
|
+
evidence, previous_status, current_status: run.status,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
// Failure inference: stale + dead process + no evidence.
|
|
282
|
+
if (evidence.age_ms >= stale && evidence.process_alive === false) {
|
|
283
|
+
try {
|
|
284
|
+
transitionAgentRun(runId, 'failed', {
|
|
285
|
+
actor,
|
|
286
|
+
status_reason: 'silent_termination_no_evidence',
|
|
287
|
+
}, cwd);
|
|
288
|
+
return {
|
|
289
|
+
run_id: runId, action: 'inferred_failed',
|
|
290
|
+
reason: 'silent_termination_no_evidence',
|
|
291
|
+
evidence, previous_status, current_status: 'failed',
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
catch (err) {
|
|
295
|
+
return {
|
|
296
|
+
run_id: runId, action: 'no_op',
|
|
297
|
+
reason: `failure transition rejected: ${err instanceof Error ? err.message : String(err)}`,
|
|
298
|
+
evidence, previous_status, current_status: run.status,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
// Health-check window: past grace, not yet stale, no evidence either way.
|
|
303
|
+
// Emit a non-mutating event so callers see the uncertainty without
|
|
304
|
+
// forcing a transition based on incomplete data.
|
|
305
|
+
emitUnverifiedEvent(run, evidence, actor, cwd);
|
|
306
|
+
return {
|
|
307
|
+
run_id: runId, action: 'health_check_unverified',
|
|
308
|
+
reason: `delivered_but_unverified (age=${Math.round(evidence.age_ms / 1000)}s, process_alive=${evidence.process_alive})`,
|
|
309
|
+
evidence, previous_status, current_status: run.status,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Reconcile every non-terminal agent_run matching `filter`. Useful for
|
|
314
|
+
* batch sweeps from `bclaw_assignment_events` or `brainclaw doctor --dispatch`.
|
|
315
|
+
* Errors per-run are isolated — one bad run does not abort the sweep.
|
|
316
|
+
*/
|
|
317
|
+
export function reconcileAllOpenRuns(cwd, filter = {}, options = {}) {
|
|
318
|
+
const results = [];
|
|
319
|
+
// Run statuses we consider open / candidates for reconciliation.
|
|
320
|
+
const OPEN = ['created', 'launching', 'waiting_input', 'running', 'blocked'];
|
|
321
|
+
for (const status of OPEN) {
|
|
322
|
+
const runs = listAgentRuns(cwd, { ...filter, status });
|
|
323
|
+
for (const run of runs) {
|
|
324
|
+
try {
|
|
325
|
+
results.push(reconcileAgentRun(run.id, cwd, options));
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
results.push({
|
|
329
|
+
run_id: run.id, action: 'no_op', reason: 'reconcile threw — skipped',
|
|
330
|
+
evidence: { age_ms: 0, has_post_start_commit: false, claim_released: false, assignment_completed: false, process_alive: undefined },
|
|
331
|
+
previous_status: run.status, current_status: run.status,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return results;
|
|
337
|
+
}
|
|
338
|
+
// Re-export key helpers for tests.
|
|
339
|
+
export { TERMINAL_STATUSES };
|
|
340
|
+
export const __testing = {
|
|
341
|
+
describeEvidence,
|
|
342
|
+
anyCompletionEvidence,
|
|
343
|
+
};
|
|
344
|
+
void nowISO; // placeholder to keep import alive if a future refactor needs it
|
|
345
|
+
//# sourceMappingURL=agentrun-reconciler.js.map
|