brainclaw 0.29.2 → 1.5.4
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/LICENSE +21 -74
- package/README.md +199 -176
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/cli.js +710 -25
- 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 +269 -0
- package/dist/commands/mcp.js +4224 -1501
- 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 +100 -2
- 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 +33 -5
- 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/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 +381 -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/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 +110 -25
- 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/reputation.js +9 -3
- package/dist/core/schema.js +491 -6
- 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 +43 -11
- 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/product/positioning.md +10 -10
- 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 +21 -13
- 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
|
@@ -38,6 +38,8 @@ export function buildExecutionContext(options = {}) {
|
|
|
38
38
|
branch,
|
|
39
39
|
git_status: gitStatus,
|
|
40
40
|
has_remote: hasRemote,
|
|
41
|
+
git_worktree: detectGitWorktree(cwd, runner),
|
|
42
|
+
commits_behind_main: branch ? detectCommitsBehindMain(cwd, branch, runner) : undefined,
|
|
41
43
|
toolchains: detectToolchains(cwd, runner),
|
|
42
44
|
env_signals: captureEnvSignals(env),
|
|
43
45
|
};
|
|
@@ -50,6 +52,7 @@ export function compactExecutionContext(snapshot) {
|
|
|
50
52
|
branch: snapshot.branch,
|
|
51
53
|
git_status: snapshot.git_status,
|
|
52
54
|
has_remote: snapshot.has_remote,
|
|
55
|
+
commits_behind_main: snapshot.commits_behind_main,
|
|
53
56
|
toolchains: snapshot.toolchains.filter((tool) => tool.available),
|
|
54
57
|
};
|
|
55
58
|
}
|
|
@@ -64,7 +67,17 @@ export function renderExecutionContextSummary(snapshot, includeEnvSignals = fals
|
|
|
64
67
|
lines.push(`Git branch: ${snapshot.branch}`);
|
|
65
68
|
}
|
|
66
69
|
lines.push(`Git status: ${snapshot.git_status}`);
|
|
70
|
+
if ('git_worktree' in snapshot && snapshot.git_worktree) {
|
|
71
|
+
const wt = snapshot.git_worktree;
|
|
72
|
+
lines.push(`Git worktree: ${wt.worktree_path}${wt.is_linked_worktree ? ' (linked)' : ' (main)'}`);
|
|
73
|
+
if (wt.is_linked_worktree) {
|
|
74
|
+
lines.push(`Main worktree: ${wt.main_worktree_path}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
67
77
|
lines.push(`Git remote: ${snapshot.has_remote ? 'configured' : 'none'}`);
|
|
78
|
+
if ('commits_behind_main' in snapshot && snapshot.commits_behind_main && snapshot.commits_behind_main > 0) {
|
|
79
|
+
lines.push(`⚠ Branch is ${snapshot.commits_behind_main} commit(s) behind master. Consider rebasing before editing.`);
|
|
80
|
+
}
|
|
68
81
|
const availableToolchains = snapshot.toolchains.filter((tool) => tool.available);
|
|
69
82
|
if (availableToolchains.length > 0) {
|
|
70
83
|
lines.push(`Toolchains: ${availableToolchains.map((tool) => `${tool.name}${tool.version ? ` ${tool.version}` : ''}`).join(', ')}`);
|
|
@@ -95,6 +108,33 @@ function detectGitBranch(cwd, runner) {
|
|
|
95
108
|
const branch = result.stdout.trim();
|
|
96
109
|
return branch && branch !== 'HEAD' ? branch : undefined;
|
|
97
110
|
}
|
|
111
|
+
function detectGitWorktree(cwd, runner) {
|
|
112
|
+
const gitDir = runner('git', ['rev-parse', '--git-dir'], cwd);
|
|
113
|
+
const toplevel = runner('git', ['rev-parse', '--show-toplevel'], cwd);
|
|
114
|
+
if (gitDir.status !== 0 || toplevel.status !== 0)
|
|
115
|
+
return undefined;
|
|
116
|
+
const gitDirPath = path.resolve(cwd, gitDir.stdout.trim());
|
|
117
|
+
const worktreePath = toplevel.stdout.trim();
|
|
118
|
+
// Main worktree: resolve via git-common-dir (points to the shared .git for linked worktrees)
|
|
119
|
+
const commonDir = runner('git', ['rev-parse', '--git-common-dir'], cwd);
|
|
120
|
+
let mainWorktreePath = worktreePath;
|
|
121
|
+
let isLinked = false;
|
|
122
|
+
if (commonDir.status === 0) {
|
|
123
|
+
const commonDirPath = path.resolve(cwd, commonDir.stdout.trim());
|
|
124
|
+
// For linked worktrees, git-common-dir !== git-dir
|
|
125
|
+
if (path.normalize(commonDirPath) !== path.normalize(gitDirPath)) {
|
|
126
|
+
isLinked = true;
|
|
127
|
+
// The main worktree is the parent of the common .git directory
|
|
128
|
+
mainWorktreePath = path.dirname(commonDirPath);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
git_dir: gitDirPath,
|
|
133
|
+
worktree_path: worktreePath,
|
|
134
|
+
main_worktree_path: mainWorktreePath,
|
|
135
|
+
is_linked_worktree: isLinked,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
98
138
|
function detectGitStatus(cwd, runner) {
|
|
99
139
|
const result = runner('git', ['status', '--porcelain'], cwd);
|
|
100
140
|
if (result.status !== 0) {
|
|
@@ -109,6 +149,29 @@ function detectGitRemote(cwd, runner) {
|
|
|
109
149
|
}
|
|
110
150
|
return result.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).length > 0;
|
|
111
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* Detect how many commits the current branch is behind the main branch.
|
|
154
|
+
* Tries master then main as the reference branch.
|
|
155
|
+
* Returns undefined if not in a git repo or on the main branch itself.
|
|
156
|
+
*/
|
|
157
|
+
function detectCommitsBehindMain(cwd, currentBranch, runner) {
|
|
158
|
+
// Don't check if already on main branch
|
|
159
|
+
if (currentBranch === 'master' || currentBranch === 'main')
|
|
160
|
+
return undefined;
|
|
161
|
+
// Try both master and main, return the highest count found.
|
|
162
|
+
// This handles repos where both branches exist but only one is the real reference.
|
|
163
|
+
let maxBehind;
|
|
164
|
+
for (const mainBranch of ['master', 'main']) {
|
|
165
|
+
const result = runner('git', ['rev-list', '--count', `${currentBranch}..${mainBranch}`], cwd);
|
|
166
|
+
if (result.status === 0) {
|
|
167
|
+
const count = parseInt(result.stdout.trim(), 10);
|
|
168
|
+
if (!isNaN(count) && (maxBehind === undefined || count > maxBehind)) {
|
|
169
|
+
maxBehind = count;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return maxBehind;
|
|
174
|
+
}
|
|
112
175
|
function detectToolchains(cwd, runner) {
|
|
113
176
|
if (runner === defaultRunner && cachedToolchains) {
|
|
114
177
|
return cachedToolchains;
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent execution profile (pln#496 step stp_0339a439).
|
|
3
|
+
*
|
|
4
|
+
* The dispatch system needs to know how to spawn each agent on each host:
|
|
5
|
+
* - which shell to invoke (`set X=...` for cmd, `$env:X=...` for pwsh,
|
|
6
|
+
* `X=...` for bash/zsh)
|
|
7
|
+
* - which OS we are on (path quoting, sandbox semantics)
|
|
8
|
+
* - where Node lives if we have to invoke the CLI binary directly
|
|
9
|
+
* - which spawn mechanism (fresh CLI, long-running app-server, IDE
|
|
10
|
+
* extension IPC) is right for this agent
|
|
11
|
+
* - how to set the working directory (just spawn from cwd, cd into a
|
|
12
|
+
* worktree first, or rely solely on env vars)
|
|
13
|
+
* - which sandbox profile to ask the agent for (codex needs
|
|
14
|
+
* `workspace-write` on local-only commits per
|
|
15
|
+
* trap_review_sandbox_blocks_source_access)
|
|
16
|
+
*
|
|
17
|
+
* Today the dispatcher hardcodes most of this in OS-specific branches
|
|
18
|
+
* (`set X=...` strings inside a Windows-only branch, etc.). The agent
|
|
19
|
+
* registry has the right level of granularity but no place to record
|
|
20
|
+
* execution-relevant facts. This module fills that gap.
|
|
21
|
+
*
|
|
22
|
+
* Design choices:
|
|
23
|
+
*
|
|
24
|
+
* 1. **Host vs per-agent fields.** The host detection is one value
|
|
25
|
+
* (this machine is Windows + pwsh + nodejs). Each agent inherits the
|
|
26
|
+
* host context and may override individual fields (e.g. an agent that
|
|
27
|
+
* needs bash even on Windows would override `shell`). Lookup uses
|
|
28
|
+
* `getExecutionProfile(agentName, inventory)` which merges host +
|
|
29
|
+
* agent override and supplies safe defaults so the dispatcher never
|
|
30
|
+
* receives undefined for required fields.
|
|
31
|
+
*
|
|
32
|
+
* 2. **Backward-compatible.** Both the host record and the per-agent
|
|
33
|
+
* override are optional. Inventories written before this module
|
|
34
|
+
* deserialize cleanly; the dispatcher applies defaults via
|
|
35
|
+
* `getExecutionProfile`. The OS-aware spawn step (stp_a9afe59d)
|
|
36
|
+
* consumes these fields in a follow-up commit.
|
|
37
|
+
*
|
|
38
|
+
* 3. **No assumption about sandbox semantics.** `sandbox_profile` is a
|
|
39
|
+
* hint to the agent invocation template; not every agent honours it,
|
|
40
|
+
* and brainclaw does not enforce sandbox at the OS level. The default
|
|
41
|
+
* is 'none' so legacy invocations stay literally identical.
|
|
42
|
+
*
|
|
43
|
+
* @module
|
|
44
|
+
*/
|
|
45
|
+
import { spawnSync } from 'node:child_process';
|
|
46
|
+
// ── Types ──────────────────────────────────────────────────────────────────
|
|
47
|
+
/** Shells brainclaw knows how to emit env-set syntax for. */
|
|
48
|
+
export const SHELLS = ['bash', 'pwsh', 'cmd', 'sh', 'zsh'];
|
|
49
|
+
/** Coarse OS family. Matches process.platform's coverage of brainclaw's tier-A platforms. */
|
|
50
|
+
export const OPERATING_SYSTEMS = ['win', 'mac', 'linux'];
|
|
51
|
+
/** Spawn mechanism the dispatcher should use for this agent. */
|
|
52
|
+
export const SPAWN_METHODS = ['cli', 'app-server', 'extension-ipc', 'cli_spawn_legacy'];
|
|
53
|
+
/** Working-directory strategy at spawn time. */
|
|
54
|
+
export const WORKING_DIR_STRATEGIES = ['cwd', 'worktree-cd', 'env-only'];
|
|
55
|
+
/** Sandbox hint passed to the agent invocation template. */
|
|
56
|
+
export const SANDBOX_PROFILES = ['workspace-write', 'read-only', 'none'];
|
|
57
|
+
// ── Detection ──────────────────────────────────────────────────────────────
|
|
58
|
+
/**
|
|
59
|
+
* Map process.platform onto our coarse OS enum. Anything outside the
|
|
60
|
+
* tier-A trio resolves to 'linux' as the most permissive default — it
|
|
61
|
+
* does not lie about what the host is, but it lets the dispatcher pick
|
|
62
|
+
* a working set of shell-syntax decisions instead of refusing to spawn.
|
|
63
|
+
*/
|
|
64
|
+
function detectOs(platform = process.platform) {
|
|
65
|
+
if (platform === 'win32')
|
|
66
|
+
return 'win';
|
|
67
|
+
if (platform === 'darwin')
|
|
68
|
+
return 'mac';
|
|
69
|
+
return 'linux';
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Sniff the host shell from environment variables. POSIX uses $SHELL;
|
|
73
|
+
* Windows has neither $SHELL nor a single canonical answer — we look at
|
|
74
|
+
* COMSPEC (cmd) and PSModulePath (pwsh) and prefer pwsh when both are
|
|
75
|
+
* set since modern Windows agent integrations rely on it (codex CLI
|
|
76
|
+
* resolves through pwsh.exe on Windows per pln#475).
|
|
77
|
+
*/
|
|
78
|
+
function detectShell(env = process.env, platform = process.platform) {
|
|
79
|
+
// Windows first because it lacks $SHELL and uses different env vars.
|
|
80
|
+
if (platform === 'win32') {
|
|
81
|
+
if (env.PSModulePath)
|
|
82
|
+
return 'pwsh';
|
|
83
|
+
if (env.COMSPEC)
|
|
84
|
+
return 'cmd';
|
|
85
|
+
// Fallback: assume pwsh on modern Windows. Empirically more agents
|
|
86
|
+
// tolerate pwsh than cmd, and this matches the brainclaw dispatch
|
|
87
|
+
// template that ships today.
|
|
88
|
+
return 'pwsh';
|
|
89
|
+
}
|
|
90
|
+
const shellEnv = env.SHELL;
|
|
91
|
+
if (shellEnv) {
|
|
92
|
+
const lower = shellEnv.toLowerCase();
|
|
93
|
+
if (lower.endsWith('/zsh') || lower.endsWith('\\zsh'))
|
|
94
|
+
return 'zsh';
|
|
95
|
+
if (lower.endsWith('/bash') || lower.endsWith('\\bash'))
|
|
96
|
+
return 'bash';
|
|
97
|
+
if (lower.endsWith('/sh') || lower.endsWith('\\sh'))
|
|
98
|
+
return 'sh';
|
|
99
|
+
// Other shells (fish, csh, …) — bash is the safest fallback for
|
|
100
|
+
// brainclaw's `X=value cmd` env-set pattern.
|
|
101
|
+
return 'bash';
|
|
102
|
+
}
|
|
103
|
+
// POSIX without $SHELL — extremely rare but bash is the canonical default.
|
|
104
|
+
return 'bash';
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Best-effort path to the running Node binary.
|
|
108
|
+
*
|
|
109
|
+
* `process.execPath` is the canonical answer for the current process —
|
|
110
|
+
* it always points to the Node that's running brainclaw, which is the
|
|
111
|
+
* Node any spawned agent should also see if PATH is misconfigured.
|
|
112
|
+
*
|
|
113
|
+
* On Windows this is a `.exe`; on POSIX it's a binary. Either way, the
|
|
114
|
+
* dispatcher can fall back to it when `node` is not on PATH.
|
|
115
|
+
*/
|
|
116
|
+
function detectNodePath() {
|
|
117
|
+
try {
|
|
118
|
+
return process.execPath || undefined;
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
export function detectHostExecutionProfile(options = {}) {
|
|
125
|
+
const env = options.env ?? process.env;
|
|
126
|
+
const platform = options.platform ?? process.platform;
|
|
127
|
+
return {
|
|
128
|
+
shell: detectShell(env, platform),
|
|
129
|
+
os: detectOs(platform),
|
|
130
|
+
node_path: detectNodePath(),
|
|
131
|
+
spawn_method: 'cli',
|
|
132
|
+
working_dir_strategy: 'cwd',
|
|
133
|
+
sandbox_profile: 'none',
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
// ── Resolution ─────────────────────────────────────────────────────────────
|
|
137
|
+
/**
|
|
138
|
+
* Hard-coded defaults that apply when neither the host record nor the
|
|
139
|
+
* per-agent override specify a field. Kept centralised here so the
|
|
140
|
+
* dispatcher never has to encode them.
|
|
141
|
+
*/
|
|
142
|
+
const DEFAULT_PROFILE = {
|
|
143
|
+
shell: 'bash',
|
|
144
|
+
os: 'linux',
|
|
145
|
+
node_path: undefined,
|
|
146
|
+
spawn_method: 'cli',
|
|
147
|
+
working_dir_strategy: 'cwd',
|
|
148
|
+
sandbox_profile: 'none',
|
|
149
|
+
};
|
|
150
|
+
/**
|
|
151
|
+
* Merge host + per-agent override + module defaults. The agent override
|
|
152
|
+
* takes priority over the host, the host takes priority over module
|
|
153
|
+
* defaults. Missing fields cascade silently — no exception is ever
|
|
154
|
+
* thrown for a partially populated input, which matters because
|
|
155
|
+
* inventories saved before this module gained the new fields must still
|
|
156
|
+
* resolve cleanly.
|
|
157
|
+
*/
|
|
158
|
+
export function resolveExecutionProfile(host, agentOverride) {
|
|
159
|
+
return {
|
|
160
|
+
shell: agentOverride?.shell ?? host?.shell ?? DEFAULT_PROFILE.shell,
|
|
161
|
+
os: agentOverride?.os ?? host?.os ?? DEFAULT_PROFILE.os,
|
|
162
|
+
node_path: agentOverride?.node_path ?? host?.node_path ?? DEFAULT_PROFILE.node_path,
|
|
163
|
+
spawn_method: agentOverride?.spawn_method ?? host?.spawn_method ?? DEFAULT_PROFILE.spawn_method,
|
|
164
|
+
working_dir_strategy: agentOverride?.working_dir_strategy ?? host?.working_dir_strategy ?? DEFAULT_PROFILE.working_dir_strategy,
|
|
165
|
+
sandbox_profile: agentOverride?.sandbox_profile ?? host?.sandbox_profile ?? DEFAULT_PROFILE.sandbox_profile,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
// ── Env-set rendering ──────────────────────────────────────────────────────
|
|
169
|
+
/**
|
|
170
|
+
* Render a single `KEY=VALUE` environment assignment in the syntax of
|
|
171
|
+
* the named shell. Used by the OS-aware spawn step (stp_a9afe59d) to
|
|
172
|
+
* generate spawn commands the host shell will actually parse.
|
|
173
|
+
*
|
|
174
|
+
* bash/zsh/sh → KEY="VALUE"
|
|
175
|
+
* pwsh → $env:KEY="VALUE"
|
|
176
|
+
* cmd → set KEY=VALUE
|
|
177
|
+
*
|
|
178
|
+
* Values are double-quoted in shells that support it; cmd uses the bare
|
|
179
|
+
* `set NAME=VALUE` form because cmd's quoting rules are pathological for
|
|
180
|
+
* embedded equals/quotes. Callers who need cmd-safe values must escape
|
|
181
|
+
* upstream — this helper is intentionally thin.
|
|
182
|
+
*/
|
|
183
|
+
export function renderEnvSet(shell, key, value) {
|
|
184
|
+
switch (shell) {
|
|
185
|
+
case 'pwsh': return `$env:${key}="${value}"`;
|
|
186
|
+
case 'cmd': return `set ${key}=${value}`;
|
|
187
|
+
case 'bash':
|
|
188
|
+
case 'zsh':
|
|
189
|
+
case 'sh':
|
|
190
|
+
default: return `${key}="${value}"`;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// ── Spawn prefix (used by dispatcher + execution adapters) ─────────────────
|
|
194
|
+
/**
|
|
195
|
+
* Build a shell-correct prefix for an inline env-var set followed by a
|
|
196
|
+
* command. Centralises what dispatcher.ts:buildEnvPrefix,
|
|
197
|
+
* execution-adapters.ts:buildManualEnvPrefix, and the inline branch in
|
|
198
|
+
* mcp.ts spawn dispatch were duplicating before pln#496 step
|
|
199
|
+
* stp_a9afe59d.
|
|
200
|
+
*
|
|
201
|
+
* Output by shell (legacy bytes preserved for bash and cmd — see codex
|
|
202
|
+
* review findings on commit 87a9f73, asgn_02c3c742):
|
|
203
|
+
* bash / zsh / sh : `BRAINCLAW_CLAIM_ID=clm_xxx ` (unquoted, legacy)
|
|
204
|
+
* pwsh : `$env:BRAINCLAW_CLAIM_ID="clm_xxx"; `
|
|
205
|
+
* cmd : `set BRAINCLAW_CLAIM_ID=clm_xxx && `
|
|
206
|
+
*
|
|
207
|
+
* The unquoted bash form matches the pre-pln#496 dispatcher.ts:buildEnvPrefix
|
|
208
|
+
* output exactly, so any caller string-matching the legacy bytes keeps
|
|
209
|
+
* working. `renderEnvSet` still emits the quoted form for general use.
|
|
210
|
+
*
|
|
211
|
+
* Returns an empty string when claimId is empty or the dry-run sentinel —
|
|
212
|
+
* callers use the prefix as `${prefix}<command>` so concatenation stays
|
|
213
|
+
* safe.
|
|
214
|
+
*
|
|
215
|
+
* Defaults: when `shell` is omitted:
|
|
216
|
+
* - On Windows hosts → `cmd`. The historical brainclaw behaviour
|
|
217
|
+
* unconditionally used `set X=Y && ` on Windows regardless of whether
|
|
218
|
+
* pwsh was available. Picking pwsh based on PSModulePath sniffing
|
|
219
|
+
* would be a regression for hosts whose dispatch pipeline runs
|
|
220
|
+
* through child_process.spawn(shell:true) — Windows resolves that
|
|
221
|
+
* to cmd, not pwsh, so a pwsh prefix would not parse. Pwsh becomes
|
|
222
|
+
* opt-in via explicit `{ shell: 'pwsh' }` (used by Phase 3 app-server
|
|
223
|
+
* integrations that already speak pwsh natively).
|
|
224
|
+
* - On POSIX hosts → host detection (bash / zsh / sh / default bash).
|
|
225
|
+
*/
|
|
226
|
+
export function buildClaimEnvPrefix(claimId, options) {
|
|
227
|
+
if (!claimId || claimId === '(dry-run)')
|
|
228
|
+
return '';
|
|
229
|
+
let shell;
|
|
230
|
+
if (options?.shell) {
|
|
231
|
+
shell = options.shell;
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
// Pre-pln#496 default: Windows always emits cmd syntax. The smart
|
|
235
|
+
// detector exists for explicit callers (Phase 3 app-server) that
|
|
236
|
+
// know they want pwsh. See codex review on commit 87a9f73 — the
|
|
237
|
+
// PSModulePath sniff was a regression when used as the host
|
|
238
|
+
// default for the legacy spawn pipeline.
|
|
239
|
+
shell = process.platform === 'win32' ? 'cmd' : detectHostExecutionProfile().shell;
|
|
240
|
+
}
|
|
241
|
+
switch (shell) {
|
|
242
|
+
case 'cmd': return `set BRAINCLAW_CLAIM_ID=${claimId} && `;
|
|
243
|
+
case 'pwsh': return `$env:BRAINCLAW_CLAIM_ID="${claimId}"; `;
|
|
244
|
+
case 'bash':
|
|
245
|
+
case 'zsh':
|
|
246
|
+
case 'sh':
|
|
247
|
+
default: return `BRAINCLAW_CLAIM_ID=${claimId} `;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
// ── Verification helper (used by setup / doctor) ───────────────────────────
|
|
251
|
+
/**
|
|
252
|
+
* Try `node --version` from the resolved profile's `node_path` to confirm
|
|
253
|
+
* the binary actually executes. Returns the version string when it does,
|
|
254
|
+
* undefined otherwise. Cheap enough to call from setup; not called from
|
|
255
|
+
* the dispatch hot path.
|
|
256
|
+
*/
|
|
257
|
+
export function verifyNodeBinary(nodePath, timeoutMs = 5000) {
|
|
258
|
+
if (!nodePath)
|
|
259
|
+
return undefined;
|
|
260
|
+
try {
|
|
261
|
+
const result = spawnSync(nodePath, ['--version'], { encoding: 'utf-8', timeout: timeoutMs, windowsHide: true });
|
|
262
|
+
if (result.status !== 0)
|
|
263
|
+
return undefined;
|
|
264
|
+
return (result.stdout ?? '').trim() || undefined;
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
return undefined;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
//# sourceMappingURL=execution-profile.js.map
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E2E dispatch execution — spawn agent processes after claim+inbox delivery.
|
|
3
|
+
*
|
|
4
|
+
* This module bridges the gap between dispatch (claim + inbox + brief) and
|
|
5
|
+
* actual agent execution. It detects whether the coordinator can spawn CLI
|
|
6
|
+
* processes and, if so, launches the agent in a detached subprocess.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { getCapabilityProfile } from './agent-capability.js';
|
|
13
|
+
import { appendAuditEntry } from './audit.js';
|
|
14
|
+
import { loadAllSessions } from './identity.js';
|
|
15
|
+
import { loadConfig } from './config.js';
|
|
16
|
+
import { loadAssignment } from './assignments.js';
|
|
17
|
+
import { defaultExecutionAdapter, } from './execution-adapters.js';
|
|
18
|
+
function sleep(ms) {
|
|
19
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Compute the brief-ack sentinel file path for an assignment.
|
|
23
|
+
*
|
|
24
|
+
* pln#476 — half-2 of trp#59. The dispatcher prefixes the spawn command
|
|
25
|
+
* with a shell `touch`/`type nul >` step that creates this file BEFORE
|
|
26
|
+
* the agent binary executes. Its existence proves the spawn actually
|
|
27
|
+
* started doing work — independent of whether the agent has the
|
|
28
|
+
* brainclaw MCP wired (codex spawned without MCP cannot call
|
|
29
|
+
* bclaw_assignment_update; the ack file lets us recognize a healthy
|
|
30
|
+
* spawn anyway).
|
|
31
|
+
*/
|
|
32
|
+
export function getAssignmentAckPath(cwd, assignmentId) {
|
|
33
|
+
return path.join(cwd, '.brainclaw', 'coordination', 'runtime', 'ack', `${assignmentId}.ack`);
|
|
34
|
+
}
|
|
35
|
+
function isAssignmentAcked(assignmentId, cwd) {
|
|
36
|
+
// Fast path: the brief-ack sentinel was written by the worker shell.
|
|
37
|
+
if (fs.existsSync(getAssignmentAckPath(cwd, assignmentId)))
|
|
38
|
+
return true;
|
|
39
|
+
// Standard path: the worker called bclaw_assignment_update via MCP and
|
|
40
|
+
// moved the assignment past the offered/created state.
|
|
41
|
+
const assignment = loadAssignment(assignmentId, cwd);
|
|
42
|
+
return !!assignment && assignment.status !== 'created' && assignment.status !== 'offered';
|
|
43
|
+
}
|
|
44
|
+
async function waitForAssignmentHandshake(assignmentId, cwd, timeoutMs) {
|
|
45
|
+
const deadline = Date.now() + timeoutMs;
|
|
46
|
+
while (Date.now() < deadline) {
|
|
47
|
+
if (isAssignmentAcked(assignmentId, cwd))
|
|
48
|
+
return true;
|
|
49
|
+
await sleep(100);
|
|
50
|
+
}
|
|
51
|
+
return isAssignmentAcked(assignmentId, cwd);
|
|
52
|
+
}
|
|
53
|
+
// ── Helpers ────────────────────────────────────────────────
|
|
54
|
+
/** Parse a duration string like '4h', '30m', '1d' to milliseconds. */
|
|
55
|
+
function parseDurationMs(value) {
|
|
56
|
+
const match = /^(\d+)([mhd])$/i.exec(value.trim());
|
|
57
|
+
if (!match)
|
|
58
|
+
return 4 * 3_600_000; // default 4h
|
|
59
|
+
const amount = parseInt(match[1], 10);
|
|
60
|
+
const unit = match[2].toLowerCase();
|
|
61
|
+
if (unit === 'm')
|
|
62
|
+
return amount * 60_000;
|
|
63
|
+
if (unit === 'h')
|
|
64
|
+
return amount * 3_600_000;
|
|
65
|
+
return amount * 86_400_000;
|
|
66
|
+
}
|
|
67
|
+
export function checkActiveInstance(agentName, cwd) {
|
|
68
|
+
const sessions = loadAllSessions(cwd);
|
|
69
|
+
let ttlStr = '4h';
|
|
70
|
+
try {
|
|
71
|
+
ttlStr = loadConfig(cwd).implicit_session_ttl ?? '4h';
|
|
72
|
+
}
|
|
73
|
+
catch { /* use default */ }
|
|
74
|
+
const SESSION_STALE_MS = parseDurationMs(ttlStr);
|
|
75
|
+
const now = Date.now();
|
|
76
|
+
const activeSessions = [];
|
|
77
|
+
for (const session of sessions) {
|
|
78
|
+
if (session.agent !== agentName)
|
|
79
|
+
continue;
|
|
80
|
+
const lastSeen = new Date(session.last_seen_at).getTime();
|
|
81
|
+
if (isNaN(lastSeen))
|
|
82
|
+
continue;
|
|
83
|
+
if (now - lastSeen < SESSION_STALE_MS) {
|
|
84
|
+
activeSessions.push(session.session_id);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const profile = getCapabilityProfile(agentName);
|
|
88
|
+
const maxAllowed = profile?.max_concurrent_tasks ?? 1;
|
|
89
|
+
const activeCount = activeSessions.length;
|
|
90
|
+
const canSpawnMore = activeCount < maxAllowed;
|
|
91
|
+
return {
|
|
92
|
+
active: !canSpawnMore, // backward compat: active=true means "cannot spawn more"
|
|
93
|
+
canSpawnMore,
|
|
94
|
+
activeCount,
|
|
95
|
+
maxAllowed,
|
|
96
|
+
reason: canSpawnMore
|
|
97
|
+
? `Agent ${agentName} has capacity (${activeCount}/${maxAllowed} slots used)`
|
|
98
|
+
: `Agent ${agentName} at capacity (${activeCount}/${maxAllowed} slots used)`,
|
|
99
|
+
activeSessions,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
// ── Spawn detection ─────────────────────────────────────────
|
|
103
|
+
/**
|
|
104
|
+
* Check if the coordinator can spawn an agent as a CLI subprocess.
|
|
105
|
+
*
|
|
106
|
+
* Returns canSpawn=true when:
|
|
107
|
+
* 1. The target agent has runtime.canBeSpawnedCli = true in its profile
|
|
108
|
+
* 2. The current process is NOT running inside an MCP stdio sandbox
|
|
109
|
+
* (heuristic: stdin is a TTY, or BRAINCLAW_CAN_SPAWN env is set)
|
|
110
|
+
*/
|
|
111
|
+
export function canSpawnAgent(agentName) {
|
|
112
|
+
return defaultExecutionAdapter.canSpawn(agentName);
|
|
113
|
+
}
|
|
114
|
+
// ── Process spawning ────────────────────────────────────────
|
|
115
|
+
/**
|
|
116
|
+
* Spawn an agent CLI process in a detached, fire-and-forget mode.
|
|
117
|
+
*
|
|
118
|
+
* The spawned process is fully detached from the parent — it won't block
|
|
119
|
+
* the coordinator and survives parent exit. The PID is returned for
|
|
120
|
+
* tracking purposes.
|
|
121
|
+
*/
|
|
122
|
+
export function executeDispatchedCommand(invoke, options) {
|
|
123
|
+
return defaultExecutionAdapter.start(invoke, options);
|
|
124
|
+
}
|
|
125
|
+
// ── Execution orchestrator ──────────────────────────────────
|
|
126
|
+
/**
|
|
127
|
+
* Attempt E2E execution after dispatch delivery.
|
|
128
|
+
*
|
|
129
|
+
* This is the main entry point called by bclaw_coordinate and bclaw_dispatch
|
|
130
|
+
* after claim+inbox+brief delivery is complete.
|
|
131
|
+
*
|
|
132
|
+
* - If autoExecute=true and agent is spawnable: spawn and return delivered_and_started
|
|
133
|
+
* - If autoExecute=false or not spawnable: return command_ready_manual with command string
|
|
134
|
+
* - If spawn fails: log warning, fallback to command_ready_manual
|
|
135
|
+
*/
|
|
136
|
+
export async function attemptExecution(invoke, options) {
|
|
137
|
+
const adapter = options.adapter ?? defaultExecutionAdapter;
|
|
138
|
+
// No invoke command available (IDE-only agents, etc.)
|
|
139
|
+
if (!invoke) {
|
|
140
|
+
return { execution_status: 'inbox_only' };
|
|
141
|
+
}
|
|
142
|
+
const spawnCheck = adapter.canSpawn(options.agent);
|
|
143
|
+
// Opt-out or can't spawn: return command for manual execution
|
|
144
|
+
// Prepend BRAINCLAW_CLAIM_ID so manual copy-paste still routes correctly
|
|
145
|
+
if (!options.autoExecute || !spawnCheck.canSpawn) {
|
|
146
|
+
const manual = adapter.prepareManualCommand(invoke, options);
|
|
147
|
+
return {
|
|
148
|
+
execution_status: 'command_ready_manual',
|
|
149
|
+
command: manual.command,
|
|
150
|
+
shell: manual.shell,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
// Capacity guard: skip if agent is at max concurrent tasks
|
|
154
|
+
if (options.cwd) {
|
|
155
|
+
const instanceCheck = checkActiveInstance(options.agent, options.cwd);
|
|
156
|
+
if (!instanceCheck.canSpawnMore) {
|
|
157
|
+
appendAuditEntry({
|
|
158
|
+
actor: options.dispatcherAgent,
|
|
159
|
+
actor_id: options.dispatcherAgentId,
|
|
160
|
+
action: 'spawn_failed',
|
|
161
|
+
item_id: options.claimId,
|
|
162
|
+
item_type: 'claim',
|
|
163
|
+
scope: options.agent,
|
|
164
|
+
after: { reason: instanceCheck.reason, active_sessions: instanceCheck.activeSessions, skipped: true },
|
|
165
|
+
}, options.cwd);
|
|
166
|
+
const manual = adapter.prepareManualCommand(invoke, options);
|
|
167
|
+
return {
|
|
168
|
+
execution_status: 'command_ready_manual',
|
|
169
|
+
command: manual.command,
|
|
170
|
+
shell: manual.shell,
|
|
171
|
+
error: `Spawn skipped: ${instanceCheck.reason}. Use the command manually.`,
|
|
172
|
+
failure_kind: 'spawn_capacity',
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// Attempt spawn (await handles both sync and async adapters)
|
|
177
|
+
try {
|
|
178
|
+
// pln#476: pass ackRoot=options.cwd so the spawn wrap writes the
|
|
179
|
+
// brief-ack sentinel under the project's coordination dir (not the
|
|
180
|
+
// worktree's local store), where waitForAssignmentHandshake reads.
|
|
181
|
+
const result = await adapter.start(invoke, { ...options, ackRoot: options.cwd });
|
|
182
|
+
if (options.assignmentId && options.cwd) {
|
|
183
|
+
// pln#475: TTL bumped from 5000 → 30000ms. Real workers (claude-code,
|
|
184
|
+
// codex) take 8–15s to load the runtime + open the inbox + call
|
|
185
|
+
// bclaw_assignment_update(accepted). 5s caused legitimate spawns to be
|
|
186
|
+
// marked failed (trp#59 — observed during P1 dispatch on 2026-04-25).
|
|
187
|
+
// Override with BRAINCLAW_HANDSHAKE_TIMEOUT_MS for very fast / very slow
|
|
188
|
+
// environments. options.handshakeTimeoutMs (programmatic) wins over env.
|
|
189
|
+
const envTimeout = process.env.BRAINCLAW_HANDSHAKE_TIMEOUT_MS;
|
|
190
|
+
const parsedEnvTimeout = envTimeout ? Number.parseInt(envTimeout, 10) : NaN;
|
|
191
|
+
const handshakeTimeoutMs = options.handshakeTimeoutMs ??
|
|
192
|
+
(Number.isFinite(parsedEnvTimeout) && parsedEnvTimeout > 0 ? parsedEnvTimeout : 30_000);
|
|
193
|
+
const handshakeOk = await waitForAssignmentHandshake(options.assignmentId, options.cwd, handshakeTimeoutMs);
|
|
194
|
+
if (!handshakeOk) {
|
|
195
|
+
appendAuditEntry({
|
|
196
|
+
actor: options.dispatcherAgent,
|
|
197
|
+
actor_id: options.dispatcherAgentId,
|
|
198
|
+
action: 'spawn_failed',
|
|
199
|
+
item_id: options.claimId,
|
|
200
|
+
item_type: 'claim',
|
|
201
|
+
scope: options.agent,
|
|
202
|
+
after: { reason: `No assignment handshake within ${handshakeTimeoutMs}ms`, pid: result.pid, command: invoke.bashCommand },
|
|
203
|
+
}, options.cwd);
|
|
204
|
+
const manual = adapter.prepareManualCommand(invoke, options);
|
|
205
|
+
return {
|
|
206
|
+
execution_status: 'command_ready_manual',
|
|
207
|
+
command: manual.command,
|
|
208
|
+
shell: manual.shell,
|
|
209
|
+
error: `Spawn launched (pid ${result.pid}) but assignment ${options.assignmentId} did not acknowledge within ${handshakeTimeoutMs}ms`,
|
|
210
|
+
failure_kind: 'spawn_no_handshake',
|
|
211
|
+
pid: result.pid,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
// Audit success
|
|
216
|
+
appendAuditEntry({
|
|
217
|
+
actor: options.dispatcherAgent,
|
|
218
|
+
actor_id: options.dispatcherAgentId,
|
|
219
|
+
action: 'agent_spawned',
|
|
220
|
+
item_id: options.claimId,
|
|
221
|
+
item_type: 'claim',
|
|
222
|
+
scope: options.agent,
|
|
223
|
+
after: { pid: result.pid, command: invoke.bashCommand, worktree_path: options.worktreePath },
|
|
224
|
+
}, options.cwd);
|
|
225
|
+
return {
|
|
226
|
+
execution_status: 'delivered_and_started',
|
|
227
|
+
pid: result.pid,
|
|
228
|
+
command: invoke.bashCommand,
|
|
229
|
+
started_at: result.started_at,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
catch (err) {
|
|
233
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
234
|
+
// Audit failure
|
|
235
|
+
appendAuditEntry({
|
|
236
|
+
actor: options.dispatcherAgent,
|
|
237
|
+
actor_id: options.dispatcherAgentId,
|
|
238
|
+
action: 'spawn_failed',
|
|
239
|
+
item_id: options.claimId,
|
|
240
|
+
item_type: 'claim',
|
|
241
|
+
scope: options.agent,
|
|
242
|
+
after: { error: errorMsg, command: invoke.bashCommand },
|
|
243
|
+
}, options.cwd);
|
|
244
|
+
// Graceful fallback — include BRAINCLAW_CLAIM_ID for manual routing
|
|
245
|
+
const manual = adapter.prepareManualCommand(invoke, options);
|
|
246
|
+
return {
|
|
247
|
+
execution_status: 'command_ready_manual',
|
|
248
|
+
command: manual.command,
|
|
249
|
+
shell: manual.shell,
|
|
250
|
+
error: `Spawn failed (${errorMsg}), falling back to manual execution`,
|
|
251
|
+
failure_kind: 'spawn_failed',
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
//# sourceMappingURL=execution.js.map
|