brainclaw 1.5.5 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -4
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/cli.js +124 -7
- package/dist/commands/bootstrap-loop.js +206 -0
- package/dist/commands/loop.js +156 -0
- package/dist/commands/loops-handlers.js +110 -55
- package/dist/commands/mcp-read-handlers.js +37 -0
- package/dist/commands/mcp-schemas.generated.js +33 -0
- package/dist/commands/mcp.js +661 -207
- package/dist/commands/questions.js +180 -0
- package/dist/commands/reply.js +190 -0
- package/dist/commands/session-end.js +105 -3
- package/dist/commands/session-start.js +32 -53
- package/dist/commands/setup.js +64 -13
- package/dist/commands/switch.js +17 -1
- package/dist/core/agent-capability.js +19 -0
- package/dist/core/agent-files.js +86 -0
- package/dist/core/agent-integrations.js +34 -0
- package/dist/core/agent-inventory.js +27 -0
- package/dist/core/agentrun-reconciler.js +130 -0
- package/dist/core/ai-agent-detection.js +17 -1
- package/dist/core/claims.js +54 -3
- package/dist/core/dirty-scope.js +242 -0
- package/dist/core/dispatch-status.js +219 -0
- package/dist/core/entity-operations.js +128 -9
- package/dist/core/execution-adapters.js +38 -2
- package/dist/core/facade-schema.js +71 -0
- package/dist/core/federation-cloud.js +27 -12
- package/dist/core/federation-materialize.js +57 -0
- package/dist/core/instruction-templates.js +2 -0
- package/dist/core/loops/bootstrap-acquire.js +195 -0
- package/dist/core/loops/facade-schema.js +68 -1
- package/dist/core/loops/hooks/bootstrap-write.js +144 -0
- package/dist/core/loops/hooks/notify-operator.js +148 -0
- package/dist/core/loops/hooks/survey-source-reader.js +256 -0
- package/dist/core/loops/index.js +8 -2
- package/dist/core/loops/next-expected.js +63 -0
- package/dist/core/loops/presets/bootstrap.js +75 -0
- package/dist/core/loops/presets/index.js +16 -0
- package/dist/core/loops/store.js +224 -4
- package/dist/core/loops/types.js +346 -1
- package/dist/core/loops/verbs.js +739 -6
- package/dist/core/schema.js +30 -2
- package/dist/core/state.js +62 -0
- package/dist/core/worktree.js +58 -0
- package/dist/facts.js +360 -5
- package/dist/facts.json +359 -4
- package/docs/cli.md +10 -7
- package/docs/concepts/dispatch-lifecycle.md +228 -0
- package/docs/concepts/loop-engine.md +55 -0
- package/docs/concepts/multi-agent-workflows.md +167 -166
- package/docs/concepts/troubleshooting.md +36 -2
- package/docs/index.md +4 -3
- package/docs/integrations/hermes.md +78 -0
- package/docs/integrations/overview.md +22 -18
- package/docs/mcp-schema-changelog.md +8 -1
- package/docs/quickstart-existing-project.md +1 -1
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ If you've ever:
|
|
|
14
14
|
- watched two coworkers (or two agents) **edit the same files** without knowing it,
|
|
15
15
|
- or **gave up running multiple agents in parallel** because keeping them in sync was a pain,
|
|
16
16
|
|
|
17
|
-
brainclaw gives you durable shared state across sessions, agents, and teammates. Plans, claims, handoffs, decisions, and traps live in `.brainclaw/`, work identically across any compatible agent (Claude Code, Codex, Copilot, Cline, OpenCode, Cursor, Windsurf, Kilocode, Roo Code, Continue, Mistral Vibe, Antigravity/Gemini CLI, …), and stay accessible whether you orchestrate them in parallel or pick them up one after another.
|
|
17
|
+
brainclaw gives you durable shared state across sessions, agents, and teammates. Plans, claims, handoffs, decisions, and traps live in `.brainclaw/`, work identically across any compatible agent (Claude Code, Codex, Copilot, Cline, OpenCode, Cursor, Windsurf, Kilocode, Roo Code, Continue, Mistral Vibe, Hermes, Antigravity/Gemini CLI, …), and stay accessible whether you orchestrate them in parallel or pick them up one after another.
|
|
18
18
|
|
|
19
19
|
Use it two ways — **together or separately**:
|
|
20
20
|
|
|
@@ -81,7 +81,8 @@ brainclaw is designed to sit alongside the coding agents teams are already using
|
|
|
81
81
|
|
|
82
82
|
| Logo | Agent | Tier | What brainclaw configures |
|
|
83
83
|
|---|---|---|---|
|
|
84
|
-
| [](https://github.com/openclaw/openclaw) | **[OpenClaw](https://github.com/openclaw/openclaw)** | B | MCP + brainclaw skill (SKILL.md) for structured project memory |
|
|
84
|
+
| [](https://github.com/openclaw/openclaw) | **[OpenClaw](https://github.com/openclaw/openclaw)** | B | MCP + brainclaw skill (SKILL.md) for structured project memory |
|
|
85
|
+
| [](https://github.com/NousResearch/hermes-agent) | **[Hermes Agent](https://github.com/NousResearch/hermes-agent)** | B | MCP + universal `.agents/skills/brainclaw/SKILL.md` |
|
|
85
86
|
| [](https://github.com/qwibitai/nanoclaw) | **[NanoClaw](https://github.com/qwibitai/nanoclaw)** | C | brainclaw skill — messaging agent (WhatsApp, Telegram, Slack) |
|
|
86
87
|
| [](https://github.com/NVIDIA/NemoClaw) | **[NemoClaw](https://github.com/NVIDIA/NemoClaw)** | C | brainclaw skill — NVIDIA enterprise agent stack |
|
|
87
88
|
| [](https://github.com/sipeed/picoclaw) | **[PicoClaw](https://github.com/sipeed/picoclaw)** | C | brainclaw skill — edge/IoT agent (Go, <10MB RAM) |
|
|
@@ -132,7 +133,7 @@ npm install -g brainclaw
|
|
|
132
133
|
brainclaw setup-machine --yes
|
|
133
134
|
```
|
|
134
135
|
|
|
135
|
-
This detects the installed agents on the current machine, writes the machine-level MCP and user config Brainclaw manages, and does **not** scan or initialize repositories.
|
|
136
|
+
This detects the installed coding agents on the current machine, writes the machine-level MCP and user config Brainclaw manages for that detected set, and does **not** scan or initialize repositories.
|
|
136
137
|
|
|
137
138
|
### 4. Initialize or refresh the current project
|
|
138
139
|
|
|
@@ -231,7 +232,7 @@ Still sharp:
|
|
|
231
232
|
1. **Same-checkout concurrent edits** — running two agents in the *same* working tree (no per-claim worktree) is still the wrong answer. Use the dispatch path (auto-worktree per claim) instead of raw concurrent CLI sessions.
|
|
232
233
|
2. **Cross-machine sync** — federation across machines is on the roadmap, not in v1.x. Today brainclaw's store is local and one-machine-per-project.
|
|
233
234
|
3. **Spawn-and-forget assumptions** — spawned workers don't always commit their work cleanly. The brief-ack file confirms the spawn started; in the worst case the coordinator harvests open changes.
|
|
234
|
-
4. **Live state for hook-less agents** — Tier B/C agents without lifecycle hooks (Cursor, Cline, Windsurf, Copilot, Continue, Kilocode, Mistral Vibe) get live context via `.live.md` companions regenerated on session-end and handoff, not via real-time push.
|
|
235
|
+
4. **Live state for hook-less agents** — Tier B/C agents without lifecycle hooks (Cursor, Cline, Windsurf, Copilot, Continue, Kilocode, Mistral Vibe, Hermes) get live context via `.live.md` companions regenerated on session-end and handoff, not via real-time push.
|
|
235
236
|
|
|
236
237
|
Recommended use today:
|
|
237
238
|
|
|
Binary file
|
package/dist/cli.js
CHANGED
|
@@ -105,6 +105,8 @@ import { runMigrate } from './commands/migrate.js';
|
|
|
105
105
|
import { runRunProfile } from './commands/run-profile.js';
|
|
106
106
|
import { runCompact } from './commands/compact.js';
|
|
107
107
|
import { runHarvestCandidates } from './commands/harvest.js';
|
|
108
|
+
import { runQuestionsCommand } from './commands/questions.js';
|
|
109
|
+
import { runReplyCommand } from './commands/reply.js';
|
|
108
110
|
import { requireRegisteredAgentIdentity } from './core/agent-registry.js';
|
|
109
111
|
const program = new Command();
|
|
110
112
|
function collect(value, previous) {
|
|
@@ -194,8 +196,10 @@ program
|
|
|
194
196
|
logger.info(`Cleaned ${removed} orphan lock/tmp file(s) in ${memoryDir()}`);
|
|
195
197
|
}
|
|
196
198
|
}
|
|
197
|
-
else if (explicitCwd) {
|
|
198
|
-
// For
|
|
199
|
+
else if (explicitCwd && cmdName !== 'init') {
|
|
200
|
+
// For setup commands, still respect explicit --cwd / --project but nothing else.
|
|
201
|
+
// init receives cwd through runInit options so scaffold writers do not
|
|
202
|
+
// depend on changing process.cwd().
|
|
199
203
|
process.chdir(path.resolve(explicitCwd));
|
|
200
204
|
}
|
|
201
205
|
});
|
|
@@ -212,8 +216,13 @@ program
|
|
|
212
216
|
.option('--no-analyze-repo', 'Skip repository analysis when suggesting a project mode')
|
|
213
217
|
.option('--no-ai-scan', 'Skip AI surface scan during init')
|
|
214
218
|
.option('--scan', 'Scan subdirectories for service boundaries and suggest init targets')
|
|
219
|
+
.option('--cwd <path>', 'Override working directory for init scaffolding (parity with other CLI commands)')
|
|
215
220
|
.action(async (options) => {
|
|
216
|
-
|
|
221
|
+
// pln#515 step 1: commander binds --cwd to the program-level option even
|
|
222
|
+
// when it appears after `init`, so resolve via program.opts() and feed
|
|
223
|
+
// runInit's existing options.cwd plumb.
|
|
224
|
+
const programCwd = program.opts().cwd;
|
|
225
|
+
await runInit({ ...options, cwd: options.cwd ?? programCwd });
|
|
217
226
|
});
|
|
218
227
|
// --- setup ---
|
|
219
228
|
program
|
|
@@ -1296,8 +1305,8 @@ program
|
|
|
1296
1305
|
.option('--maintenance-mode <mode>', 'Maintenance mode: full (default) or fast')
|
|
1297
1306
|
.option('--include-context', 'Output full project context after starting session (replaces separate context call)')
|
|
1298
1307
|
.option('--json', 'Output as JSON')
|
|
1299
|
-
.action((options) => {
|
|
1300
|
-
runSessionStart(options);
|
|
1308
|
+
.action(async (options) => {
|
|
1309
|
+
await runSessionStart(options);
|
|
1301
1310
|
});
|
|
1302
1311
|
// --- session-end ---
|
|
1303
1312
|
program
|
|
@@ -1313,8 +1322,8 @@ program
|
|
|
1313
1322
|
.option('--reviewer <name>', 'Explicit reviewer to route the reflected handoff review to')
|
|
1314
1323
|
.option('--reflect', 'Include structured reflection questions for the agent to answer')
|
|
1315
1324
|
.option('--json', 'Output as JSON')
|
|
1316
|
-
.action((options) => {
|
|
1317
|
-
runSessionEnd({
|
|
1325
|
+
.action(async (options) => {
|
|
1326
|
+
await runSessionEnd({
|
|
1318
1327
|
...options,
|
|
1319
1328
|
autoReflect: options.autoReflect,
|
|
1320
1329
|
autoRelease: options.autoRelease,
|
|
@@ -1873,6 +1882,114 @@ if (isCodevEnabled()) {
|
|
|
1873
1882
|
runCodevMetrics(thread, { ...options, cwd: globalOpts.cwd });
|
|
1874
1883
|
});
|
|
1875
1884
|
}
|
|
1885
|
+
// --- questions (operator-question artifacts across loops; pln#508 step 4) ---
|
|
1886
|
+
program
|
|
1887
|
+
.command('questions')
|
|
1888
|
+
.description('List pending operator_question artifacts across loops in the current project')
|
|
1889
|
+
.option('--loop <loop_id>', 'Filter to a single loop')
|
|
1890
|
+
.option('--status <status>', 'Filter by status: awaiting (default), answered, timed_out', 'awaiting')
|
|
1891
|
+
.option('--mine', 'Filter to questions targeted at the current agent (v1 heuristic: humans see all awaiting)')
|
|
1892
|
+
.option('--json', 'Output as JSON')
|
|
1893
|
+
.action((options) => {
|
|
1894
|
+
const globalOpts = program.opts();
|
|
1895
|
+
const status = options.status;
|
|
1896
|
+
if (!['awaiting', 'answered', 'timed_out'].includes(status)) {
|
|
1897
|
+
console.error(`Error: --status must be one of awaiting|answered|timed_out (got "${options.status}")`);
|
|
1898
|
+
process.exit(1);
|
|
1899
|
+
}
|
|
1900
|
+
runQuestionsCommand({
|
|
1901
|
+
loop: options.loop,
|
|
1902
|
+
status,
|
|
1903
|
+
mine: options.mine,
|
|
1904
|
+
json: options.json,
|
|
1905
|
+
}, globalOpts.cwd);
|
|
1906
|
+
});
|
|
1907
|
+
// --- bootstrap-loop (open/join/status/cancel a bootstrap loop; pln#513 step 3) ---
|
|
1908
|
+
program
|
|
1909
|
+
.command('bootstrap-loop')
|
|
1910
|
+
.description('Open or join a bootstrap loop on the current project, or query its status')
|
|
1911
|
+
.option('--status', 'Report current state')
|
|
1912
|
+
.option('--cancel', 'Cancel the active bootstrap loop')
|
|
1913
|
+
.option('--yes', 'Skip confirmation prompts')
|
|
1914
|
+
.option('--json', 'Machine-readable output')
|
|
1915
|
+
.action(async (options) => {
|
|
1916
|
+
const globalOpts = program.opts();
|
|
1917
|
+
const { runBootstrapLoopCommand } = await import('./commands/bootstrap-loop.js');
|
|
1918
|
+
await runBootstrapLoopCommand(options, globalOpts.cwd);
|
|
1919
|
+
});
|
|
1920
|
+
// --- loop (drive loop turn verbs; pln#517 step 2) ---
|
|
1921
|
+
const loopCmd = program
|
|
1922
|
+
.command('loop')
|
|
1923
|
+
.description('Drive a loop turn (turn / complete-turn / advance / add-artifact)');
|
|
1924
|
+
loopCmd
|
|
1925
|
+
.command('turn <loop_id>')
|
|
1926
|
+
.description('Issue a turn assignment on a slot')
|
|
1927
|
+
.requiredOption('--slot <slot_id>', 'Target slot id (lsl_...)')
|
|
1928
|
+
.option('--input <text>', 'Free-form input passed to the slot')
|
|
1929
|
+
.option('--role <role>', 'Slot role (resolves the first non-done slot with that role)')
|
|
1930
|
+
.option('--assignment-id <id>', 'Dispatcher-provided assignment id to record on the slot')
|
|
1931
|
+
.option('--json', 'Machine-readable output')
|
|
1932
|
+
.action(async (loop_id, options) => {
|
|
1933
|
+
const globalOpts = program.opts();
|
|
1934
|
+
const { runLoopCommand } = await import('./commands/loop.js');
|
|
1935
|
+
await runLoopCommand('turn', { loop_id }, options, globalOpts.cwd);
|
|
1936
|
+
});
|
|
1937
|
+
loopCmd
|
|
1938
|
+
.command('complete-turn <loop_id>')
|
|
1939
|
+
.description('Complete a slot turn')
|
|
1940
|
+
.requiredOption('--slot <slot_id>', 'Target slot id (lsl_...)')
|
|
1941
|
+
.requiredOption('--outcome <outcome>', 'Turn outcome: done, failed, or cancelled')
|
|
1942
|
+
.option('--failure-reason <text>', 'Reason when outcome is failed')
|
|
1943
|
+
.option('--artifact <json>', 'JSON object payload for an artifact to attach')
|
|
1944
|
+
.option('--json', 'Machine-readable output')
|
|
1945
|
+
.action(async (loop_id, options) => {
|
|
1946
|
+
const globalOpts = program.opts();
|
|
1947
|
+
const { runLoopCommand } = await import('./commands/loop.js');
|
|
1948
|
+
await runLoopCommand('complete-turn', { loop_id }, options, globalOpts.cwd);
|
|
1949
|
+
});
|
|
1950
|
+
loopCmd
|
|
1951
|
+
.command('advance <loop_id>')
|
|
1952
|
+
.description('Advance a loop to its next phase')
|
|
1953
|
+
.option('--to-phase <name>', 'Explicit target phase')
|
|
1954
|
+
.option('--force', 'Bypass phase gate checks')
|
|
1955
|
+
.option('--reason <text>', 'Reason to record on the phase advance event')
|
|
1956
|
+
.option('--json', 'Machine-readable output')
|
|
1957
|
+
.action(async (loop_id, options) => {
|
|
1958
|
+
const globalOpts = program.opts();
|
|
1959
|
+
const { runLoopCommand } = await import('./commands/loop.js');
|
|
1960
|
+
await runLoopCommand('advance', { loop_id }, options, globalOpts.cwd);
|
|
1961
|
+
});
|
|
1962
|
+
loopCmd
|
|
1963
|
+
.command('add-artifact <loop_id>')
|
|
1964
|
+
.description('Attach an artifact to a loop')
|
|
1965
|
+
.requiredOption('--phase <phase>', 'Artifact phase')
|
|
1966
|
+
.requiredOption('--type <type>', 'Artifact type')
|
|
1967
|
+
.requiredOption('--body <json-or-text>', 'Artifact body as JSON or text')
|
|
1968
|
+
.option('--produced-by <agent>', 'Agent that produced the artifact')
|
|
1969
|
+
.option('--ref <ref>', 'JSON ref object, e.g. {"kind":"plan","id":"pln_..."}')
|
|
1970
|
+
.option('--json', 'Machine-readable output')
|
|
1971
|
+
.action(async (loop_id, options) => {
|
|
1972
|
+
const globalOpts = program.opts();
|
|
1973
|
+
const { runLoopCommand } = await import('./commands/loop.js');
|
|
1974
|
+
await runLoopCommand('add-artifact', { loop_id }, options, globalOpts.cwd);
|
|
1975
|
+
});
|
|
1976
|
+
// --- reply (provide_input to an operator_question; pln#508 step 4) ---
|
|
1977
|
+
program
|
|
1978
|
+
.command('reply <qst_id>')
|
|
1979
|
+
.description('Resolve an operator_question artifact (wraps bclaw_loop.provide_input)')
|
|
1980
|
+
.option('--answer <text>', 'Free-form answer text')
|
|
1981
|
+
.option('--choose <option_id>', 'Pick one of the question\'s structured options[].id')
|
|
1982
|
+
.option('--skip', 'Materialize the question\'s suggested_default')
|
|
1983
|
+
.option('--json', 'Output as JSON')
|
|
1984
|
+
.action((qstId, options) => {
|
|
1985
|
+
const globalOpts = program.opts();
|
|
1986
|
+
runReplyCommand(qstId, {
|
|
1987
|
+
answer: options.answer,
|
|
1988
|
+
choose: options.choose,
|
|
1989
|
+
skip: options.skip,
|
|
1990
|
+
json: options.json,
|
|
1991
|
+
}, globalOpts.cwd);
|
|
1992
|
+
});
|
|
1876
1993
|
// --- run (agent profiles) ---
|
|
1877
1994
|
program
|
|
1878
1995
|
.command('run [profile-name]')
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import readline from 'node:readline';
|
|
2
|
+
import { memoryExists } from '../core/io.js';
|
|
3
|
+
import { acquireBootstrapLoop, BootstrapCoordinationInProgressError, closeLoop, computeNextExpected, findExistingBootstrapLoop, } from '../core/loops/index.js';
|
|
4
|
+
import { resolveCurrentAgentName } from '../core/agent-registry.js';
|
|
5
|
+
function fail(message, exitCode, opts) {
|
|
6
|
+
if (opts.json) {
|
|
7
|
+
console.log(JSON.stringify({ ok: false, error: message }, null, 2));
|
|
8
|
+
}
|
|
9
|
+
else {
|
|
10
|
+
console.error(`Error: ${message}`);
|
|
11
|
+
}
|
|
12
|
+
process.exit(exitCode);
|
|
13
|
+
}
|
|
14
|
+
function parseQuestionBody(artifact) {
|
|
15
|
+
if (artifact.type !== 'operator_question' || !artifact.body)
|
|
16
|
+
return undefined;
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(artifact.body);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function formatNextExpected(hint) {
|
|
25
|
+
if (!hint)
|
|
26
|
+
return ' (loop has no further expected action)';
|
|
27
|
+
const bits = [` next: ${hint.action} (${hint.intent})`];
|
|
28
|
+
if (hint.phase)
|
|
29
|
+
bits.push(` phase: ${hint.phase}`);
|
|
30
|
+
if (hint.slot_id)
|
|
31
|
+
bits.push(` slot: ${hint.slot_id}${hint.role ? ` [${hint.role}]` : ''}`);
|
|
32
|
+
if (hint.from_phase && hint.to_phase)
|
|
33
|
+
bits.push(` ${hint.from_phase} → ${hint.to_phase}`);
|
|
34
|
+
if (hint.blocking_on.length)
|
|
35
|
+
bits.push(` blocking_on: ${hint.blocking_on.join(', ')}`);
|
|
36
|
+
if (hint.reason)
|
|
37
|
+
bits.push(` reason: ${hint.reason}`);
|
|
38
|
+
return bits.join('\n');
|
|
39
|
+
}
|
|
40
|
+
function buildResult(action, loop) {
|
|
41
|
+
const hint = action === 'cancelled' ? null : computeNextExpected(loop);
|
|
42
|
+
const openQuestions = loop.open_questions;
|
|
43
|
+
const result = {
|
|
44
|
+
ok: true,
|
|
45
|
+
action,
|
|
46
|
+
loop_id: loop.id,
|
|
47
|
+
current_phase: loop.current_phase,
|
|
48
|
+
status: loop.status,
|
|
49
|
+
open_questions: openQuestions,
|
|
50
|
+
next_expected: hint,
|
|
51
|
+
};
|
|
52
|
+
if (loop.pause_reason) {
|
|
53
|
+
result.pause_reason = loop.pause_reason;
|
|
54
|
+
}
|
|
55
|
+
if (loop.pending_file_apply) {
|
|
56
|
+
result.pending_file_apply = {
|
|
57
|
+
target_path: loop.pending_file_apply.target_path,
|
|
58
|
+
artifact_id: loop.pending_file_apply.artifact_id,
|
|
59
|
+
diff_artifact_id: loop.pending_file_apply.diff_artifact_id,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (action === 'joined') {
|
|
63
|
+
result.joined_existing = true;
|
|
64
|
+
}
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
function printHuman(result, loop) {
|
|
68
|
+
const prefix = result.action === 'opened'
|
|
69
|
+
? '✔ Opened bootstrap loop'
|
|
70
|
+
: result.action === 'joined'
|
|
71
|
+
? '↺ Joined existing bootstrap loop'
|
|
72
|
+
: result.action === 'cancelled'
|
|
73
|
+
? '✔ Cancelled bootstrap loop'
|
|
74
|
+
: '• Bootstrap loop status';
|
|
75
|
+
console.log(`${prefix} ${loop.id}`);
|
|
76
|
+
console.log(` phase: ${loop.current_phase}`);
|
|
77
|
+
console.log(` status: ${loop.status}`);
|
|
78
|
+
if (result.pause_reason) {
|
|
79
|
+
console.log(` pause_reason: ${result.pause_reason}`);
|
|
80
|
+
}
|
|
81
|
+
if (loop.open_questions.length > 0) {
|
|
82
|
+
console.log(` open_questions: ${loop.open_questions.length} (${loop.open_questions.join(', ')})`);
|
|
83
|
+
for (const q of loop.artifacts) {
|
|
84
|
+
const body = parseQuestionBody(q);
|
|
85
|
+
if (body && loop.open_questions.includes(body.question_id)) {
|
|
86
|
+
console.log(` - ${body.question_id}: ${body.question_text}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (result.pending_file_apply) {
|
|
91
|
+
console.log(` pending_file_apply: ${result.pending_file_apply.target_path}`);
|
|
92
|
+
}
|
|
93
|
+
if (loop.slots.length > 0) {
|
|
94
|
+
const slotDesc = loop.slots
|
|
95
|
+
.map((s) => `${s.role}${s.agent ? `=${s.agent}` : ''} [${s.status}]`)
|
|
96
|
+
.join(', ');
|
|
97
|
+
console.log(` slots: ${slotDesc}`);
|
|
98
|
+
}
|
|
99
|
+
if (result.action !== 'cancelled') {
|
|
100
|
+
console.log(formatNextExpected(result.next_expected ?? null));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async function confirmCancel(loopId) {
|
|
104
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
105
|
+
try {
|
|
106
|
+
const answer = await new Promise((resolve) => {
|
|
107
|
+
rl.question(`Cancel bootstrap loop ${loopId}? This sets status='cancelled'. [y/N] `, (a) => resolve(a));
|
|
108
|
+
});
|
|
109
|
+
return /^y(es)?$/i.test(answer.trim());
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
rl.close();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* `brainclaw bootstrap-loop` — open, join, query, or cancel the bootstrap
|
|
117
|
+
* loop on the current project. The open/join path delegates to
|
|
118
|
+
* `acquireBootstrapLoop` (src/core/loops/bootstrap-acquire.ts) which
|
|
119
|
+
* implements the same coordination-lock singleton acquire used by the MCP
|
|
120
|
+
* bclaw_coordinate facade. Two concurrent CLI invocations will now converge
|
|
121
|
+
* on the same loop rather than opening duplicates (pln#518 step 1).
|
|
122
|
+
*/
|
|
123
|
+
export async function runBootstrapLoopCommand(options = {}, cwd) {
|
|
124
|
+
if (!memoryExists(cwd)) {
|
|
125
|
+
fail('.brainclaw/ not found. Run `brainclaw init` first.', 1, options);
|
|
126
|
+
}
|
|
127
|
+
const modes = [];
|
|
128
|
+
if (options.status)
|
|
129
|
+
modes.push('--status');
|
|
130
|
+
if (options.cancel)
|
|
131
|
+
modes.push('--cancel');
|
|
132
|
+
if (modes.length > 1) {
|
|
133
|
+
fail(`--status and --cancel are mutually exclusive (got: ${modes.join(', ')})`, 1, options);
|
|
134
|
+
}
|
|
135
|
+
if (options.status) {
|
|
136
|
+
const existing = findExistingBootstrapLoop(cwd);
|
|
137
|
+
if (!existing) {
|
|
138
|
+
fail('no active bootstrap loop on this project. Run `brainclaw bootstrap-loop` to open one.', 1, options);
|
|
139
|
+
}
|
|
140
|
+
const result = buildResult('status', existing);
|
|
141
|
+
if (options.json) {
|
|
142
|
+
console.log(JSON.stringify(result, null, 2));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
printHuman(result, existing);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (options.cancel) {
|
|
149
|
+
const existing = findExistingBootstrapLoop(cwd);
|
|
150
|
+
if (!existing) {
|
|
151
|
+
fail('no active bootstrap loop to cancel on this project.', 1, options);
|
|
152
|
+
}
|
|
153
|
+
if (!options.yes) {
|
|
154
|
+
const confirmed = await confirmCancel(existing.id);
|
|
155
|
+
if (!confirmed) {
|
|
156
|
+
fail('cancellation aborted by operator.', 1, options);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const actor = resolveCurrentAgentName(cwd);
|
|
160
|
+
let closed;
|
|
161
|
+
try {
|
|
162
|
+
closed = closeLoop({
|
|
163
|
+
id: existing.id,
|
|
164
|
+
final_status: 'cancelled',
|
|
165
|
+
reason: 'operator_cancelled',
|
|
166
|
+
actor,
|
|
167
|
+
}, cwd);
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
171
|
+
fail(`closeLoop verb rejected the call: ${msg}`, 2, options);
|
|
172
|
+
}
|
|
173
|
+
const result = buildResult('cancelled', closed);
|
|
174
|
+
if (options.json) {
|
|
175
|
+
console.log(JSON.stringify(result, null, 2));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
printHuman(result, closed);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
// No-args: delegate to the singleton acquire path (pln#518 step 1).
|
|
182
|
+
// acquireBootstrapLoop handles find-existing + coordination-lock + openLoop,
|
|
183
|
+
// preventing two concurrent CLI invocations from both calling openLoop.
|
|
184
|
+
const actor = resolveCurrentAgentName(cwd);
|
|
185
|
+
let loop;
|
|
186
|
+
let action;
|
|
187
|
+
try {
|
|
188
|
+
const acquired = acquireBootstrapLoop({ actor }, cwd);
|
|
189
|
+
loop = acquired.loop;
|
|
190
|
+
action = acquired.action;
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
if (err instanceof BootstrapCoordinationInProgressError) {
|
|
194
|
+
fail(err.message, 2, options);
|
|
195
|
+
}
|
|
196
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
197
|
+
fail(`bootstrap-loop acquire failed: ${msg}`, 2, options);
|
|
198
|
+
}
|
|
199
|
+
const result = buildResult(action, loop);
|
|
200
|
+
if (options.json) {
|
|
201
|
+
console.log(JSON.stringify(result, null, 2));
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
printHuman(result, loop);
|
|
205
|
+
}
|
|
206
|
+
//# sourceMappingURL=bootstrap-loop.js.map
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { memoryExists } from '../core/io.js';
|
|
2
|
+
import { handleBclawLoop } from './loops-handlers.js';
|
|
3
|
+
function fail(message, exitCode, opts) {
|
|
4
|
+
if (opts.json) {
|
|
5
|
+
console.log(JSON.stringify({ ok: false, error: message }, null, 2));
|
|
6
|
+
}
|
|
7
|
+
else {
|
|
8
|
+
console.error(`Error: ${message}`);
|
|
9
|
+
}
|
|
10
|
+
process.exit(exitCode);
|
|
11
|
+
}
|
|
12
|
+
function requireLoopId(args, opts) {
|
|
13
|
+
const loopId = args.loop_id;
|
|
14
|
+
if (!loopId || !/^lop_[0-9a-z]+$/.test(loopId)) {
|
|
15
|
+
fail(`invalid loop_id "${loopId ?? ''}" — expected format lop_<hex>`, 1, opts);
|
|
16
|
+
}
|
|
17
|
+
return loopId;
|
|
18
|
+
}
|
|
19
|
+
function requireOption(value, flag, opts) {
|
|
20
|
+
if (value === undefined || value === '') {
|
|
21
|
+
fail(`${flag} is required`, 1, opts);
|
|
22
|
+
}
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
function parseJsonObject(raw, flag, opts) {
|
|
26
|
+
let parsed;
|
|
27
|
+
try {
|
|
28
|
+
parsed = JSON.parse(raw);
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
32
|
+
fail(`${flag} must be valid JSON object syntax: ${message}`, 1, opts);
|
|
33
|
+
}
|
|
34
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
35
|
+
fail(`${flag} must be a JSON object`, 1, opts);
|
|
36
|
+
}
|
|
37
|
+
return parsed;
|
|
38
|
+
}
|
|
39
|
+
function parseOptionalRef(raw, opts) {
|
|
40
|
+
if (raw === undefined)
|
|
41
|
+
return undefined;
|
|
42
|
+
const parsed = parseJsonObject(raw, '--ref', opts);
|
|
43
|
+
if (typeof parsed.kind !== 'string' || typeof parsed.id !== 'string') {
|
|
44
|
+
fail('--ref must be a JSON object with string fields { "kind", "id" }', 1, opts);
|
|
45
|
+
}
|
|
46
|
+
return { kind: parsed.kind, id: parsed.id };
|
|
47
|
+
}
|
|
48
|
+
function parseBody(raw) {
|
|
49
|
+
try {
|
|
50
|
+
return JSON.stringify(JSON.parse(raw));
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return raw;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function parseOutcome(opts) {
|
|
57
|
+
const outcome = opts.outcome;
|
|
58
|
+
if (outcome !== 'done' && outcome !== 'failed' && outcome !== 'cancelled') {
|
|
59
|
+
fail(`--outcome must be one of done|failed|cancelled (got "${outcome ?? ''}")`, 1, opts);
|
|
60
|
+
}
|
|
61
|
+
return outcome;
|
|
62
|
+
}
|
|
63
|
+
function formatNextExpected(hint) {
|
|
64
|
+
if (!hint)
|
|
65
|
+
return ' (loop has no further expected action)';
|
|
66
|
+
const bits = [` next: ${hint.action} (${hint.intent})`];
|
|
67
|
+
if (hint.phase)
|
|
68
|
+
bits.push(` phase: ${hint.phase}`);
|
|
69
|
+
if (hint.slot_id)
|
|
70
|
+
bits.push(` slot: ${hint.slot_id}${hint.role ? ` [${hint.role}]` : ''}`);
|
|
71
|
+
if (hint.from_phase && hint.to_phase)
|
|
72
|
+
bits.push(` ${hint.from_phase} -> ${hint.to_phase}`);
|
|
73
|
+
if (hint.blocking_on.length)
|
|
74
|
+
bits.push(` blocking_on: ${hint.blocking_on.join(', ')}`);
|
|
75
|
+
if (hint.reason)
|
|
76
|
+
bits.push(` reason: ${hint.reason}`);
|
|
77
|
+
return bits.join('\n');
|
|
78
|
+
}
|
|
79
|
+
function buildRequest(subcommand, loopId, opts) {
|
|
80
|
+
switch (subcommand) {
|
|
81
|
+
case 'turn':
|
|
82
|
+
return {
|
|
83
|
+
intent: 'turn',
|
|
84
|
+
loop_id: loopId,
|
|
85
|
+
slot_id: requireOption(opts.slot, '--slot <slot_id>', opts),
|
|
86
|
+
input: requireOption(opts.input, '--input <text>', opts),
|
|
87
|
+
role: opts.role,
|
|
88
|
+
assignment_id: opts.assignmentId,
|
|
89
|
+
};
|
|
90
|
+
case 'complete-turn': {
|
|
91
|
+
const artifact = opts.artifact
|
|
92
|
+
? parseJsonObject(opts.artifact, '--artifact', opts)
|
|
93
|
+
: undefined;
|
|
94
|
+
return {
|
|
95
|
+
intent: 'complete_turn',
|
|
96
|
+
loop_id: loopId,
|
|
97
|
+
slot_id: requireOption(opts.slot, '--slot <slot_id>', opts),
|
|
98
|
+
outcome: parseOutcome(opts),
|
|
99
|
+
failure_reason: opts.failureReason,
|
|
100
|
+
artifact,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
case 'advance':
|
|
104
|
+
return {
|
|
105
|
+
intent: 'advance',
|
|
106
|
+
loop_id: loopId,
|
|
107
|
+
to_phase: opts.toPhase,
|
|
108
|
+
force: opts.force,
|
|
109
|
+
reason: opts.reason,
|
|
110
|
+
};
|
|
111
|
+
case 'add-artifact':
|
|
112
|
+
return {
|
|
113
|
+
intent: 'add_artifact',
|
|
114
|
+
loop_id: loopId,
|
|
115
|
+
artifact: {
|
|
116
|
+
phase: requireOption(opts.phase, '--phase <phase>', opts),
|
|
117
|
+
type: requireOption(opts.type, '--type <type>', opts),
|
|
118
|
+
body: parseBody(requireOption(opts.body, '--body <json-or-text>', opts)),
|
|
119
|
+
produced_by: opts.producedBy,
|
|
120
|
+
ref: parseOptionalRef(opts.ref, opts),
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
export function runLoopCommand(subcommand, args, options = {}, cwd) {
|
|
126
|
+
if (!memoryExists(cwd)) {
|
|
127
|
+
console.error('Error: .brainclaw/ not found. Run `brainclaw init` first.');
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
const loopId = requireLoopId(args, options);
|
|
131
|
+
const request = buildRequest(subcommand, loopId, options);
|
|
132
|
+
const handled = handleBclawLoop({ args: request, cwd });
|
|
133
|
+
if (handled.response.status !== 'ok') {
|
|
134
|
+
const message = handled.response.error ?? handled.summary;
|
|
135
|
+
fail(`bclaw_loop.${String(request.intent)} rejected the call: ${message}`, 2, options);
|
|
136
|
+
}
|
|
137
|
+
const result = handled.response.result;
|
|
138
|
+
const loop = result.loop;
|
|
139
|
+
const out = {
|
|
140
|
+
ok: true,
|
|
141
|
+
action: subcommand,
|
|
142
|
+
loop_id: loop?.id ?? loopId,
|
|
143
|
+
current_phase: loop?.current_phase,
|
|
144
|
+
status: loop?.status,
|
|
145
|
+
next_expected: result.next_expected ?? null,
|
|
146
|
+
auto_closed: result.auto_closed || undefined,
|
|
147
|
+
};
|
|
148
|
+
if (options.json) {
|
|
149
|
+
console.log(JSON.stringify(out, null, 2));
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
console.log(`OK loop ${subcommand} ${out.loop_id}${out.current_phase ? ` phase=${out.current_phase}` : ''}${out.status ? ` status=${out.status}` : ''}`);
|
|
153
|
+
console.log(formatNextExpected(out.next_expected));
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
//# sourceMappingURL=loop.js.map
|