brainclaw 1.16.0 → 1.18.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 (85) hide show
  1. package/README.md +22 -8
  2. package/dist/brainclaw-vscode.vsix +0 -0
  3. package/dist/cli/register-review.js +2 -2
  4. package/dist/commands/code-map.js +4 -1
  5. package/dist/commands/codev.js +61 -30
  6. package/dist/commands/doctor.js +14 -1
  7. package/dist/commands/harvest.js +241 -25
  8. package/dist/commands/inbox.js +10 -4
  9. package/dist/commands/loop.js +2 -2
  10. package/dist/commands/loops-handlers.js +82 -1
  11. package/dist/commands/mcp-catalog.js +12 -4
  12. package/dist/commands/mcp-read-handlers.js +90 -7
  13. package/dist/commands/mcp-schemas.generated.js +3 -0
  14. package/dist/commands/mcp-write-coordination.js +159 -40
  15. package/dist/commands/mcp.js +11 -2
  16. package/dist/core/agent-capability.js +7 -2
  17. package/dist/core/agent-files.js +53 -2
  18. package/dist/core/agent-integrations.js +1 -0
  19. package/dist/core/agentrun-reconciler.js +171 -7
  20. package/dist/core/agentruns.js +6 -1
  21. package/dist/core/code-map/aggregate.js +473 -0
  22. package/dist/core/code-map/backend.js +36 -10
  23. package/dist/core/code-map/freshness.js +36 -1
  24. package/dist/core/code-map/lang/c/imports.scm +12 -0
  25. package/dist/core/code-map/lang/c/index.js +150 -0
  26. package/dist/core/code-map/lang/c/tags.scm +68 -0
  27. package/dist/core/code-map/lang/cpp/imports.scm +14 -0
  28. package/dist/core/code-map/lang/cpp/index.js +149 -0
  29. package/dist/core/code-map/lang/cpp/tags.scm +87 -0
  30. package/dist/core/code-map/lang/csharp/imports.scm +20 -0
  31. package/dist/core/code-map/lang/csharp/index.js +224 -0
  32. package/dist/core/code-map/lang/csharp/tags.scm +63 -0
  33. package/dist/core/code-map/lang/go/imports.scm +13 -0
  34. package/dist/core/code-map/lang/go/index.js +139 -0
  35. package/dist/core/code-map/lang/go/tags.scm +36 -0
  36. package/dist/core/code-map/lang/providers.js +12 -1
  37. package/dist/core/code-map/lang/ruby/imports.scm +24 -0
  38. package/dist/core/code-map/lang/ruby/index.js +198 -0
  39. package/dist/core/code-map/lang/ruby/tags.scm +49 -0
  40. package/dist/core/code-map/lang/rust/imports.scm +44 -0
  41. package/dist/core/code-map/lang/rust/index.js +136 -0
  42. package/dist/core/code-map/lang/rust/tags.scm +47 -0
  43. package/dist/core/code-map/query.js +229 -80
  44. package/dist/core/code-map/types.js +18 -0
  45. package/dist/core/code-map/work-section.js +8 -7
  46. package/dist/core/codev-responses.js +16 -0
  47. package/dist/core/dispatcher.js +209 -29
  48. package/dist/core/execution-adapters.js +29 -3
  49. package/dist/core/ideation-loop-close.js +124 -0
  50. package/dist/core/loops/artifact-resolver.js +197 -0
  51. package/dist/core/loops/attempt-reservation.js +576 -0
  52. package/dist/core/loops/commit-intent.js +494 -0
  53. package/dist/core/loops/facade-schema.js +48 -0
  54. package/dist/core/loops/impl-bind.js +144 -0
  55. package/dist/core/loops/index.js +1 -1
  56. package/dist/core/loops/iteration-engine.js +29 -0
  57. package/dist/core/loops/lock.js +14 -0
  58. package/dist/core/loops/project-resolution.js +157 -0
  59. package/dist/core/loops/reconcile-turn.js +369 -0
  60. package/dist/core/loops/result-reducers.js +88 -0
  61. package/dist/core/loops/store.js +46 -7
  62. package/dist/core/loops/types.js +139 -11
  63. package/dist/core/loops/verbs.js +9 -3
  64. package/dist/core/loops/verify-command.js +209 -0
  65. package/dist/core/messaging.js +58 -5
  66. package/dist/core/review-loop-close.js +106 -34
  67. package/dist/core/review-loop-turn-dispatch.js +445 -0
  68. package/dist/core/runtime-signals.js +68 -0
  69. package/dist/core/schema.js +34 -0
  70. package/dist/core/worktree.js +240 -22
  71. package/dist/facts.js +10 -10
  72. package/dist/facts.json +9 -9
  73. package/dist/wasm/tree-sitter-c.wasm +0 -0
  74. package/dist/wasm/tree-sitter-c_sharp.wasm +0 -0
  75. package/dist/wasm/tree-sitter-cpp.wasm +0 -0
  76. package/dist/wasm/tree-sitter-go.wasm +0 -0
  77. package/dist/wasm/tree-sitter-ruby.wasm +0 -0
  78. package/dist/wasm/tree-sitter-rust.wasm +0 -0
  79. package/docs/cli.md +1 -1
  80. package/docs/code-map.md +22 -6
  81. package/docs/concepts/loop-engine.md +28 -2
  82. package/docs/concepts/observer-protocol.md +22 -0
  83. package/docs/integrations/codex.md +19 -3
  84. package/docs/mcp-schema-changelog.md +43 -1
  85. package/package.json +1 -1
@@ -0,0 +1,144 @@
1
+ /**
2
+ * pln#632 impl-loop bind — the ENGINE action for an implementation loop's `bind` phase.
3
+ *
4
+ * The implementation protocol declares `bind` as "bind plan+sequence and dispatch" (see
5
+ * LOOP_PROTOCOLS.implementation in types.ts). This is that action: read the loop's linked
6
+ * sequence, dispatch its ready lanes via the EXISTING sequence spawner, then advance
7
+ * `bind → execute` so the loop enters its execute↔verify cycle.
8
+ *
9
+ * Reuses `dispatch()` ADDITIVELY via its `sequenceId` option (pln#632) so the loop drives
10
+ * its OWN linked sequence WITHOUT touching the project's global active-sequence pointer —
11
+ * no hijack of whatever sequence other bclaw_dispatch work is using. Mirrors the
12
+ * async-handler-spawns pattern already used by bclaw_coordinate(open_loop=true) for review
13
+ * loops; touches NO review/ideation dispatch. The live spawn happens only when this runs
14
+ * on a real loop (exactly like bclaw_dispatch) — `dryRun` previews with no spawn and no
15
+ * phase mutation, so the flow is unit-testable.
16
+ */
17
+ import { dispatch } from '../dispatcher.js';
18
+ import { listSequences } from '../sequence.js';
19
+ import { getLoop } from './store.js';
20
+ import { advance } from './verbs.js';
21
+ import { withLoopLock } from './lock.js';
22
+ /**
23
+ * Bind an implementation loop to its linked sequence and dispatch it. Async because the
24
+ * underlying spawn is async (the only awaited work; `dryRun` resolves synchronously).
25
+ *
26
+ * Idempotent: a loop already past `bind` returns a `noop` (never re-dispatches on a second
27
+ * bind after it advanced). A crash BETWEEN dispatch and advance leaves the loop in `bind`,
28
+ * so a retry re-dispatches only the still-unassigned lanes (dispatch skips lanes with an
29
+ * active assignment) and re-attempts the advance — safe crash-recovery.
30
+ */
31
+ export async function runImplBind(input, cwd) {
32
+ const { loop_id, dispatcherAgent } = input;
33
+ const loop = getLoop(loop_id, cwd);
34
+ if (!loop)
35
+ throw new Error(`unknown loop_id ${loop_id}`);
36
+ if (loop.kind !== 'implementation') {
37
+ throw new Error(`bind is only valid for implementation loops (loop ${loop_id} is kind='${loop.kind}'); review/ideation loops dispatch via bclaw_coordinate`);
38
+ }
39
+ // bind SPAWNS real workers via dispatch() — never do that on a loop that is not open.
40
+ // A paused loop keeps current_phase='bind' (pause only flips status), and open→close
41
+ // leaves the phase at 'bind' too, so the phase-idempotency check below is NOT sufficient
42
+ // to stop a spawn on a held/terminal loop (review Finding 1). Gate on status FIRST, so
43
+ // no dispatch fires and we don't spawn then throw at the advance.
44
+ if (loop.status !== 'open') {
45
+ throw new Error(`bind requires an open loop; loop ${loop_id} is ${loop.status} (resume a paused loop, or reopen a terminal one, before binding)`);
46
+ }
47
+ const sequenceId = loop.linked?.sequence_ids?.[0];
48
+ // Idempotency: `bind` is the loop's FIRST phase. A loop already past it was bound before.
49
+ if (loop.current_phase !== 'bind') {
50
+ return {
51
+ loop_id,
52
+ sequence_id: sequenceId ?? '',
53
+ action: 'noop',
54
+ dispatch: null,
55
+ messages_sent: 0,
56
+ reason: `loop is in phase '${loop.current_phase}', not 'bind' — already bound (idempotent)`,
57
+ };
58
+ }
59
+ if (!sequenceId) {
60
+ throw new Error(`impl-bind requires a linked sequence: open the implementation loop with linked.sequence_ids=[…] (the sequence whose lanes it executes). None found on ${loop_id}.`);
61
+ }
62
+ // Validate up-front (non-throwing lookup) so a missing sequence fails with a clear
63
+ // message rather than a silent null dispatch.
64
+ const seq = listSequences(cwd).find((s) => s.id === sequenceId);
65
+ if (!seq)
66
+ throw new Error(`linked sequence ${sequenceId} not found for loop ${loop_id}`);
67
+ const dispatched = await dispatch({
68
+ sequenceId,
69
+ dispatcherAgent,
70
+ dispatcherAgentId: input.dispatcherAgentId,
71
+ sessionId: input.sessionId,
72
+ dryRun: input.dryRun,
73
+ lanes: input.lanes,
74
+ autoExecute: input.autoExecute,
75
+ model: input.model,
76
+ maxAssignments: input.maxAssignments,
77
+ }, cwd ?? process.cwd());
78
+ const result = dispatched?.result ?? null;
79
+ const messages_sent = result?.messages_sent.length ?? 0;
80
+ // Guard a race: the sequence disappeared between validation and dispatch → don't advance.
81
+ if (!dispatched) {
82
+ return {
83
+ loop_id,
84
+ sequence_id: sequenceId,
85
+ action: 'noop',
86
+ dispatch: null,
87
+ messages_sent: 0,
88
+ reason: `sequence ${sequenceId} yielded no dispatch analysis (unavailable); loop stays in 'bind'`,
89
+ };
90
+ }
91
+ if (input.dryRun) {
92
+ return {
93
+ loop_id,
94
+ sequence_id: sequenceId,
95
+ action: 'preview',
96
+ dispatch: result,
97
+ messages_sent,
98
+ reason: `dry run: ${result?.delivery_plan.length ?? 0} lane(s) would dispatch; loop stays in 'bind' (no spawn, no advance)`,
99
+ };
100
+ }
101
+ // Real bind: advance bind → execute so the loop enters the execute↔verify cycle. The
102
+ // implementation protocol's `bind` phase carries no advance_gate, so the advance is
103
+ // unconditional FROM bind — but the phase-check and the advance must be ATOMIC or two
104
+ // racing binds each advance once and push the loop bind→execute→verify with no execute
105
+ // work done (review Finding 2). advance() does not self-lock (its callers — the facade's
106
+ // withLockedLoopMutation and the ideation closer — provide the lock), so we take the loop
107
+ // lock here and re-check the phase under it: only the bind that still sees 'bind' advances.
108
+ const advanced = withLoopLock({
109
+ cwd,
110
+ intent: 'impl-bind-advance',
111
+ agentId: dispatcherAgent,
112
+ scope: { kind: 'loop', loopId: loop_id },
113
+ work: () => {
114
+ const fresh = getLoop(loop_id, cwd);
115
+ // Raced: a concurrent bind (or a pause/close) moved the loop out of an open 'bind'
116
+ // state → do NOT advance again. Idempotent by construction.
117
+ if (!fresh || fresh.status !== 'open' || fresh.current_phase !== 'bind')
118
+ return null;
119
+ const a = advance({ id: loop_id, actor: dispatcherAgent }, cwd);
120
+ return { phase: a.loop.current_phase, auto_closed: a.auto_closed };
121
+ },
122
+ });
123
+ if (!advanced) {
124
+ return {
125
+ loop_id,
126
+ sequence_id: sequenceId,
127
+ action: 'noop',
128
+ dispatch: result,
129
+ messages_sent,
130
+ reason: `dispatched ${messages_sent} assignment(s); phase already advanced out of 'bind' under a concurrent bind (idempotent — not re-advanced)`,
131
+ };
132
+ }
133
+ return {
134
+ loop_id,
135
+ sequence_id: sequenceId,
136
+ action: 'bound',
137
+ advanced_to: advanced.phase,
138
+ auto_closed: advanced.auto_closed,
139
+ dispatch: result,
140
+ messages_sent,
141
+ reason: `dispatched ${messages_sent} assignment(s) on sequence ${sequenceId}; advanced bind → ${advanced.phase}`,
142
+ };
143
+ }
144
+ //# sourceMappingURL=impl-bind.js.map
@@ -1,7 +1,7 @@
1
1
  export * from './types.js';
2
2
  export { AwaitingFileApplyApprovalError, closeLoop, ensureLoopsDir, generateLoopId, generateMutationId, generateSlotId, getLoop, listLoopEvents, listLoops, openLoop, writeThreadFile, } from './store.js';
3
3
  export { add_artifact, advance, complete_turn, evaluatePhaseAdvanceGate, evaluateStopCondition, pause, provideInput, reconcileOpenQuestions, requestInput, resume, sweepPauseTimeouts, turn, } from './verbs.js';
4
- export { decideNextPhase, artifactsInIteration, noNewCritiqueInIteration, hasCriticSignalInIteration, } from './iteration-engine.js';
4
+ export { decideNextPhase, artifactsInIteration, noNewCritiqueInIteration, hasCriticSignalInIteration, hasPassingVerifyReportInIteration, } from './iteration-engine.js';
5
5
  export { computeNextExpected, } from './next-expected.js';
6
6
  export { buildIdeationBrief, } from './brief-assembly.js';
7
7
  export { BOOTSTRAP_PRESET } from './presets/bootstrap.js';
@@ -92,6 +92,15 @@ export function decideNextPhase(thread, protocol) {
92
92
  reason: 'no_new_critique_artifacts',
93
93
  };
94
94
  }
95
+ if (iterationBlock.exit_when === 'command_green' &&
96
+ hasPassingVerifyReportInIteration(thread, thread.iteration_count)) {
97
+ return {
98
+ kind: 'exit_cycle',
99
+ target: postCycleTarget,
100
+ iteration: thread.iteration_count,
101
+ reason: 'command_green',
102
+ };
103
+ }
95
104
  // Cap check. If incrementing iteration_count would exceed the cap,
96
105
  // exit via max_iterations. (iteration_count is 0-indexed; cap=3 means
97
106
  // iterations 0, 1, 2 are allowed; refusing iteration 3 → exit.)
@@ -136,4 +145,24 @@ export function noNewCritiqueInIteration(thread, iteration) {
136
145
  export function hasCriticSignalInIteration(thread, iteration) {
137
146
  return thread.artifacts.some((a) => (a.iteration ?? 0) === iteration && a.type === 'critic_signal');
138
147
  }
148
+ /**
149
+ * `exit_when='command_green'` predicate (pln#609): true when the just-finished
150
+ * iteration produced a PASSING `verify_report` artifact (body.passed === true).
151
+ * The verify command ran during the verify turn and recorded the report — this
152
+ * is a PURE read; no command is executed here (execution seam = verify-command.ts,
153
+ * Increment 2). Iteration-window scoped so a green from a prior iteration can
154
+ * never satisfy the current one; absence reads as false.
155
+ */
156
+ export function hasPassingVerifyReportInIteration(thread, iteration) {
157
+ return artifactsInIteration(thread, iteration).some((a) => {
158
+ if (a.type !== 'verify_report')
159
+ return false;
160
+ try {
161
+ return JSON.parse(a.body ?? '{}').passed === true;
162
+ }
163
+ catch {
164
+ return false;
165
+ }
166
+ });
167
+ }
139
168
  //# sourceMappingURL=iteration-engine.js.map
@@ -4,6 +4,7 @@ import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { memoryDir } from '../io.js';
6
6
  import { nowISO } from '../ids.js';
7
+ import { recoverPendingIntents } from './commit-intent.js';
7
8
  /**
8
9
  * Per-loop exclusive lock + idempotency + fencing helpers.
9
10
  *
@@ -298,6 +299,19 @@ export function withLoopLock(options) {
298
299
  intent: options.intent,
299
300
  });
300
301
  try {
302
+ // pln#630 PR1b (dec#137) — recovery-before-mutate at the loop-lock entry
303
+ // boundary. Any durable-but-unapplied commit intent is replayed to the
304
+ // journal+thread BEFORE we read the version / run the work, so a mutation
305
+ // never starts from a half-applied state and currentVersion() sees the
306
+ // caught-up thread. Not in getLoop: the loop lock is not re-entrant and
307
+ // verbs call getLoop while already holding it. FAILS CLOSED (PR #102 review
308
+ // HIGH): a genuine recovery error (torn/unreadable journal, I/O) propagates
309
+ // and aborts the mutation rather than proceeding against corrupt state;
310
+ // quarantined conflicts/corruption are handled inside recoverPendingIntents
311
+ // and do not throw.
312
+ if (options.scope.kind === 'loop') {
313
+ recoverPendingIntents(options.scope.loopId, options.cwd);
314
+ }
301
315
  // Idempotency short-circuit (inside lock).
302
316
  if (options.clientRequestId && options.requestPayload !== undefined) {
303
317
  const loopIdForKey = options.scope.kind === 'loop'
@@ -0,0 +1,157 @@
1
+ /**
2
+ * pln#521 P1 — loop project resolution gate.
3
+ *
4
+ * A review loop that lands in the wrong store is worse than a review loop that
5
+ * never opened: the candidate, claim, assignment and loop all persist in a
6
+ * project nobody is watching, and the reviewer spawns against the wrong repo
7
+ * (DGX misroute, 2026-05). This module answers ONE question before any
8
+ * persistence happens: *which project does this loop belong to, and did anyone
9
+ * actually choose it?*
10
+ *
11
+ * Resolution ladder (B2 of art_18e7c6101880):
12
+ * 1. explicit `project` argument → routed, source='explicit'
13
+ * 2. a real selector already won upstream → routed, source=<that selector>
14
+ * (env_project / session / cwd_child / global — `resolveEffectiveCwdInfo`
15
+ * has already applied them by the time a handler receives its cwd)
16
+ * 3. bare cwd fallback in a store that can host several projects
17
+ * → `needs_project_selection`
18
+ *
19
+ * There is deliberately NO inference from ref / scope / path: B3 was rejected
20
+ * (art_e29e88878209) because a wrong guess costs more than an explicit choice.
21
+ *
22
+ * @module
23
+ */
24
+ import path from 'node:path';
25
+ import { loadConfig } from '../config.js';
26
+ import { resolveProjectCwd } from '../cross-project.js';
27
+ import { resolvePrimaryStore } from '../store-resolution.js';
28
+ import { scanNestedBrainclawProjects, summarizeWorkspaceProjects } from '../workspace-projects.js';
29
+ /**
30
+ * Decide which project a loop belongs to, or refuse when nobody chose.
31
+ *
32
+ * Throws only when an explicit `project` cannot be resolved — that error is
33
+ * `resolveProjectCwd`'s (unknown project, unavailable link) and callers already
34
+ * surface it as a tool error rather than silently falling back.
35
+ */
36
+ export function validateLoopProjectResolution(input) {
37
+ const sourceCwd = path.resolve(input.cwd);
38
+ const projectArg = input.projectArg?.trim() || undefined;
39
+ // 1. Explicit project wins outright and needs no ambiguity check: the caller
40
+ // named it, so there is nothing left to disambiguate.
41
+ if (projectArg) {
42
+ const projectCwd = path.resolve(resolveProjectCwd(projectArg, sourceCwd));
43
+ return resolved(projectCwd, 'explicit', {
44
+ source_cwd: sourceCwd,
45
+ effective_cwd: projectCwd,
46
+ active_source: 'explicit',
47
+ project_arg: projectArg,
48
+ candidates: [],
49
+ });
50
+ }
51
+ // 2. A selector other than the bare cwd fallback means somebody DID choose —
52
+ // `--cwd`, BRAINCLAW_PROJECT, a session switch, the physical child store,
53
+ // or the workspace's active-project pointer. Honour it as-is.
54
+ const activeSource = input.activeSource ?? 'cwd';
55
+ if (activeSource !== 'cwd') {
56
+ return resolved(sourceCwd, activeSource, {
57
+ source_cwd: sourceCwd,
58
+ effective_cwd: sourceCwd,
59
+ active_source: activeSource,
60
+ candidates: [],
61
+ });
62
+ }
63
+ // 3. Bare fallback. Harmless in a single-project store (there is exactly one
64
+ // answer), a misroute waiting to happen in a store that hosts children.
65
+ const candidates = collectCandidateProjects(sourceCwd);
66
+ const trace = {
67
+ source_cwd: sourceCwd,
68
+ effective_cwd: sourceCwd,
69
+ active_source: 'cwd',
70
+ candidates,
71
+ };
72
+ if (candidates.length === 0) {
73
+ return resolved(sourceCwd, 'cwd', trace);
74
+ }
75
+ const rendered = candidates
76
+ .map((c) => {
77
+ const label = c.name ?? (c.path ? path.basename(c.path) : '(unnamed)');
78
+ return ` - ${label}${c.path ? ` (${c.path})` : ''}`;
79
+ })
80
+ .join('\n');
81
+ return {
82
+ ok: false,
83
+ code: 'needs_project_selection',
84
+ message: `This store hosts ${candidates.length} project(s) and no project was selected, so the loop would land in `
85
+ + `${sourceCwd} by default rather than where the work is. Nothing was created.\n`
86
+ + `Candidates:\n${rendered}\n`
87
+ + `Choose one explicitly — pass project='<name>' on this call, or make it sticky with `
88
+ + `bclaw_switch(project='<name>'). Ref/scope/path are never used to guess (B3, art_e29e88878209).`,
89
+ candidates,
90
+ trace,
91
+ };
92
+ }
93
+ function resolved(projectCwd, source, trace) {
94
+ return {
95
+ ok: true,
96
+ project_cwd: projectCwd,
97
+ project_name: projectNameFor(projectCwd),
98
+ source,
99
+ trace,
100
+ };
101
+ }
102
+ function projectNameFor(cwd) {
103
+ try {
104
+ return loadConfig(cwd).project_name;
105
+ }
106
+ catch {
107
+ return undefined;
108
+ }
109
+ }
110
+ /**
111
+ * Sibling projects the store at `cwd` can host. Empty for an ordinary
112
+ * single-project repo — which is what keeps this gate a strict no-op there.
113
+ *
114
+ * Two signals, both already load-bearing elsewhere in the codebase:
115
+ * `project_mode === 'multi-project'` (the convention used by doctor / status /
116
+ * code-map) and a `workspace`-role store with nested project stores under it.
117
+ */
118
+ function collectCandidateProjects(cwd) {
119
+ let config;
120
+ try {
121
+ config = loadConfig(cwd);
122
+ }
123
+ catch {
124
+ // Uninitialised store: loop creation will fail downstream on its own terms.
125
+ // Refusing here would only swap a clear error for a vaguer one.
126
+ return [];
127
+ }
128
+ const seen = new Map();
129
+ const add = (candidate) => {
130
+ // A config-declared namespace has no store of its own yet, so it is keyed
131
+ // by name; anything on disk is keyed by its absolute path.
132
+ const abs = candidate.path ? path.resolve(cwd, candidate.path) : undefined;
133
+ if (abs === cwd)
134
+ return;
135
+ const key = abs ?? `name:${candidate.name ?? ''}`;
136
+ if (seen.has(key))
137
+ return;
138
+ seen.set(key, { ...candidate, ...(abs ? { path: abs } : {}) });
139
+ };
140
+ if (config.project_mode === 'multi-project') {
141
+ for (const project of summarizeWorkspaceProjects(cwd, config).discovered_projects) {
142
+ add({
143
+ name: project.project_name,
144
+ // `config` entries carry a namespace name in `path`, not a real path.
145
+ ...(project.source === 'config' ? {} : { path: project.path }),
146
+ source: project.source,
147
+ });
148
+ }
149
+ }
150
+ if (resolvePrimaryStore(cwd)?.role === 'workspace') {
151
+ for (const project of scanNestedBrainclawProjects(cwd)) {
152
+ add({ name: project.project_name, path: project.path, source: 'store_chain' });
153
+ }
154
+ }
155
+ return [...seen.values()].sort((a, b) => (a.path ?? a.name ?? '').localeCompare(b.path ?? b.name ?? ''));
156
+ }
157
+ //# sourceMappingURL=project-resolution.js.map