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.
Files changed (58) hide show
  1. package/README.md +5 -4
  2. package/dist/brainclaw-vscode.vsix +0 -0
  3. package/dist/cli.js +124 -7
  4. package/dist/commands/bootstrap-loop.js +206 -0
  5. package/dist/commands/loop.js +156 -0
  6. package/dist/commands/loops-handlers.js +110 -55
  7. package/dist/commands/mcp-read-handlers.js +37 -0
  8. package/dist/commands/mcp-schemas.generated.js +33 -0
  9. package/dist/commands/mcp.js +661 -207
  10. package/dist/commands/questions.js +180 -0
  11. package/dist/commands/reply.js +190 -0
  12. package/dist/commands/session-end.js +105 -3
  13. package/dist/commands/session-start.js +32 -53
  14. package/dist/commands/setup.js +64 -13
  15. package/dist/commands/switch.js +17 -1
  16. package/dist/core/agent-capability.js +19 -0
  17. package/dist/core/agent-files.js +86 -0
  18. package/dist/core/agent-integrations.js +34 -0
  19. package/dist/core/agent-inventory.js +27 -0
  20. package/dist/core/agentrun-reconciler.js +130 -0
  21. package/dist/core/ai-agent-detection.js +17 -1
  22. package/dist/core/claims.js +54 -3
  23. package/dist/core/dirty-scope.js +242 -0
  24. package/dist/core/dispatch-status.js +219 -0
  25. package/dist/core/entity-operations.js +128 -9
  26. package/dist/core/execution-adapters.js +38 -2
  27. package/dist/core/facade-schema.js +71 -0
  28. package/dist/core/federation-cloud.js +27 -12
  29. package/dist/core/federation-materialize.js +57 -0
  30. package/dist/core/instruction-templates.js +2 -0
  31. package/dist/core/loops/bootstrap-acquire.js +195 -0
  32. package/dist/core/loops/facade-schema.js +68 -1
  33. package/dist/core/loops/hooks/bootstrap-write.js +144 -0
  34. package/dist/core/loops/hooks/notify-operator.js +148 -0
  35. package/dist/core/loops/hooks/survey-source-reader.js +256 -0
  36. package/dist/core/loops/index.js +8 -2
  37. package/dist/core/loops/next-expected.js +63 -0
  38. package/dist/core/loops/presets/bootstrap.js +75 -0
  39. package/dist/core/loops/presets/index.js +16 -0
  40. package/dist/core/loops/store.js +224 -4
  41. package/dist/core/loops/types.js +346 -1
  42. package/dist/core/loops/verbs.js +739 -6
  43. package/dist/core/schema.js +30 -2
  44. package/dist/core/state.js +62 -0
  45. package/dist/core/worktree.js +58 -0
  46. package/dist/facts.js +360 -5
  47. package/dist/facts.json +359 -4
  48. package/docs/cli.md +10 -7
  49. package/docs/concepts/dispatch-lifecycle.md +228 -0
  50. package/docs/concepts/loop-engine.md +55 -0
  51. package/docs/concepts/multi-agent-workflows.md +167 -166
  52. package/docs/concepts/troubleshooting.md +36 -2
  53. package/docs/index.md +4 -3
  54. package/docs/integrations/hermes.md +78 -0
  55. package/docs/integrations/overview.md +22 -18
  56. package/docs/mcp-schema-changelog.md +8 -1
  57. package/docs/quickstart-existing-project.md +1 -1
  58. 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
- | [![OpenClaw](https://img.shields.io/badge/OpenClaw-FF6B35?logoColor=white)](https://github.com/openclaw/openclaw) | **[OpenClaw](https://github.com/openclaw/openclaw)** | B | MCP + brainclaw skill (SKILL.md) for structured project memory |
84
+ | [![OpenClaw](https://img.shields.io/badge/OpenClaw-FF6B35?logoColor=white)](https://github.com/openclaw/openclaw) | **[OpenClaw](https://github.com/openclaw/openclaw)** | B | MCP + brainclaw skill (SKILL.md) for structured project memory |
85
+ | [![Hermes](https://img.shields.io/badge/Hermes-111111?logoColor=white)](https://github.com/NousResearch/hermes-agent) | **[Hermes Agent](https://github.com/NousResearch/hermes-agent)** | B | MCP + universal `.agents/skills/brainclaw/SKILL.md` |
85
86
  | [![NanoClaw](https://img.shields.io/badge/NanoClaw-4A90D9?logoColor=white)](https://github.com/qwibitai/nanoclaw) | **[NanoClaw](https://github.com/qwibitai/nanoclaw)** | C | brainclaw skill — messaging agent (WhatsApp, Telegram, Slack) |
86
87
  | [![NemoClaw](https://img.shields.io/badge/NemoClaw-76B900?logo=nvidia&logoColor=white)](https://github.com/NVIDIA/NemoClaw) | **[NemoClaw](https://github.com/NVIDIA/NemoClaw)** | C | brainclaw skill — NVIDIA enterprise agent stack |
87
88
  | [![PicoClaw](https://img.shields.io/badge/PicoClaw-00ADD8?logo=go&logoColor=white)](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 init/setup, still respect explicit --cwd / --project but nothing else
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
- await runInit(options);
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