brainclaw 0.28.0 → 1.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. package/README.md +193 -170
  2. package/dist/brainclaw-vscode.vsix +0 -0
  3. package/dist/cli.js +683 -23
  4. package/dist/commands/accept.js +3 -0
  5. package/dist/commands/add-step.js +11 -26
  6. package/dist/commands/agent-board.js +70 -3
  7. package/dist/commands/audit.js +19 -0
  8. package/dist/commands/check-policy.js +54 -0
  9. package/dist/commands/check-security-mcp.js +145 -0
  10. package/dist/commands/check-security.js +106 -0
  11. package/dist/commands/claim-resource.js +1 -0
  12. package/dist/commands/codev.js +672 -0
  13. package/dist/commands/compact.js +74 -0
  14. package/dist/commands/complete-step.js +16 -26
  15. package/dist/commands/constraint.js +8 -20
  16. package/dist/commands/decision.js +9 -20
  17. package/dist/commands/delete-plan.js +10 -12
  18. package/dist/commands/delete-step.js +16 -0
  19. package/dist/commands/dispatch.js +163 -0
  20. package/dist/commands/doctor.js +1122 -49
  21. package/dist/commands/enable-agent.js +1 -0
  22. package/dist/commands/export.js +280 -22
  23. package/dist/commands/handoff.js +33 -0
  24. package/dist/commands/harvest.js +189 -0
  25. package/dist/commands/hooks.js +82 -25
  26. package/dist/commands/inbox.js +169 -0
  27. package/dist/commands/init.js +38 -31
  28. package/dist/commands/install-hooks.js +71 -44
  29. package/dist/commands/link.js +89 -0
  30. package/dist/commands/list-claims.js +48 -3
  31. package/dist/commands/list-plans.js +129 -25
  32. package/dist/commands/loops-handlers.js +409 -0
  33. package/dist/commands/mcp-read-handlers.js +1628 -0
  34. package/dist/commands/mcp-schemas.generated.js +74 -0
  35. package/dist/commands/mcp.js +4244 -1475
  36. package/dist/commands/plan-resource.js +64 -0
  37. package/dist/commands/plan.js +12 -26
  38. package/dist/commands/prune.js +37 -2
  39. package/dist/commands/reflect.js +20 -7
  40. package/dist/commands/release-claim.js +11 -6
  41. package/dist/commands/release-notes.js +170 -0
  42. package/dist/commands/repair.js +210 -0
  43. package/dist/commands/run-profile.js +57 -0
  44. package/dist/commands/sequence.js +113 -0
  45. package/dist/commands/session-end.js +423 -14
  46. package/dist/commands/session-start.js +214 -41
  47. package/dist/commands/setup-security.js +103 -0
  48. package/dist/commands/setup.js +42 -4
  49. package/dist/commands/stale.js +109 -0
  50. package/dist/commands/switch.js +131 -10
  51. package/dist/commands/trap.js +14 -31
  52. package/dist/commands/update-handoff.js +63 -4
  53. package/dist/commands/update-plan.js +21 -28
  54. package/dist/commands/update-step.js +37 -0
  55. package/dist/commands/upgrade.js +313 -6
  56. package/dist/commands/usage.js +102 -0
  57. package/dist/commands/version.js +20 -0
  58. package/dist/commands/who.js +124 -0
  59. package/dist/commands/worktree.js +105 -0
  60. package/dist/core/actions.js +315 -0
  61. package/dist/core/agent-capability.js +610 -17
  62. package/dist/core/agent-context.js +7 -1
  63. package/dist/core/agent-files.js +1169 -85
  64. package/dist/core/agent-integrations.js +160 -5
  65. package/dist/core/agent-inventory.js +2 -0
  66. package/dist/core/agent-profiles.js +93 -0
  67. package/dist/core/agent-registry.js +162 -30
  68. package/dist/core/agentrun-reconciler.js +345 -0
  69. package/dist/core/agentruns.js +424 -0
  70. package/dist/core/ai-agent-detection.js +31 -10
  71. package/dist/core/archival.js +77 -0
  72. package/dist/core/assignment-sweeper.js +82 -0
  73. package/dist/core/assignments.js +367 -0
  74. package/dist/core/audit.js +30 -0
  75. package/dist/core/bootstrap.js +61 -10
  76. package/dist/core/brainclaw-version.js +94 -2
  77. package/dist/core/candidates.js +93 -2
  78. package/dist/core/claims.js +419 -0
  79. package/dist/core/codev-metrics.js +77 -0
  80. package/dist/core/codev-personas.js +31 -0
  81. package/dist/core/codev-plan-gen.js +35 -0
  82. package/dist/core/codev-prompts.js +74 -0
  83. package/dist/core/codev-responses.js +62 -0
  84. package/dist/core/codev-rounds.js +218 -0
  85. package/dist/core/config.js +4 -0
  86. package/dist/core/context.js +454 -34
  87. package/dist/core/coordination.js +201 -6
  88. package/dist/core/cross-project.js +230 -16
  89. package/dist/core/default-profiles/doctor.yaml +11 -0
  90. package/dist/core/default-profiles/janitor.yaml +11 -0
  91. package/dist/core/default-profiles/onboarder.yaml +11 -0
  92. package/dist/core/default-profiles/reviewer.yaml +13 -0
  93. package/dist/core/dispatcher.js +1189 -0
  94. package/dist/core/duplicates.js +2 -2
  95. package/dist/core/entity-operations.js +450 -0
  96. package/dist/core/entity-registry.js +344 -0
  97. package/dist/core/event-log.js +1 -0
  98. package/dist/core/events.js +106 -2
  99. package/dist/core/execution-adapters.js +154 -0
  100. package/dist/core/execution-context.js +63 -0
  101. package/dist/core/execution-profile.js +270 -0
  102. package/dist/core/execution.js +255 -0
  103. package/dist/core/facade-schema.js +81 -0
  104. package/dist/core/federation-cloud.js +99 -0
  105. package/dist/core/federation-message.js +52 -0
  106. package/dist/core/federation-transport.js +65 -0
  107. package/dist/core/gc-semantic.js +482 -0
  108. package/dist/core/governance.js +247 -0
  109. package/dist/core/guards.js +19 -0
  110. package/dist/core/ideation.js +72 -0
  111. package/dist/core/identity.js +252 -28
  112. package/dist/core/ids.js +6 -0
  113. package/dist/core/input-validation.js +2 -2
  114. package/dist/core/instruction-templates.js +344 -136
  115. package/dist/core/io.js +90 -11
  116. package/dist/core/lock.js +6 -2
  117. package/dist/core/loops/brief-assembly.js +213 -0
  118. package/dist/core/loops/facade-schema.js +148 -0
  119. package/dist/core/loops/index.js +7 -0
  120. package/dist/core/loops/iteration-engine.js +139 -0
  121. package/dist/core/loops/lock.js +385 -0
  122. package/dist/core/loops/store.js +201 -0
  123. package/dist/core/loops/types.js +403 -0
  124. package/dist/core/loops/verbs.js +534 -0
  125. package/dist/core/markdown.js +15 -3
  126. package/dist/core/memory-compactor.js +432 -0
  127. package/dist/core/memory-git.js +152 -8
  128. package/dist/core/messaging.js +278 -0
  129. package/dist/core/migration.js +32 -1
  130. package/dist/core/mutation-pipeline.js +4 -2
  131. package/dist/core/operations/memory-mutation.js +129 -0
  132. package/dist/core/operations/memory-write.js +78 -0
  133. package/dist/core/operations/plan.js +190 -0
  134. package/dist/core/policy.js +169 -0
  135. package/dist/core/repo-analysis.js +67 -0
  136. package/dist/core/reputation.js +9 -3
  137. package/dist/core/schema.js +546 -21
  138. package/dist/core/search.js +21 -2
  139. package/dist/core/security-cache.js +71 -0
  140. package/dist/core/security-guard.js +152 -0
  141. package/dist/core/security-scoring.js +86 -0
  142. package/dist/core/sequence.js +130 -0
  143. package/dist/core/socket-client.js +113 -0
  144. package/dist/core/staleness.js +246 -0
  145. package/dist/core/state.js +98 -22
  146. package/dist/core/store-resolution.js +54 -12
  147. package/dist/core/toml-writer.js +76 -0
  148. package/dist/core/upgrades/backup.js +232 -0
  149. package/dist/core/upgrades/health-check.js +169 -0
  150. package/dist/core/upgrades/patches/candidate-archive.js +145 -0
  151. package/dist/core/upgrades/patches/handoff-review-strip.js +128 -0
  152. package/dist/core/upgrades/patches/provenance-rollout.js +136 -0
  153. package/dist/core/upgrades/schema-version.js +97 -0
  154. package/dist/core/worktree.js +606 -0
  155. package/dist/facts.js +114 -0
  156. package/dist/facts.json +111 -0
  157. package/docs/architecture/project-refs.md +5 -1
  158. package/docs/cli.md +690 -43
  159. package/docs/concepts/ideation-loop.md +317 -0
  160. package/docs/concepts/loop-engine.md +456 -0
  161. package/docs/concepts/mcp-governance.md +268 -0
  162. package/docs/concepts/memory-staleness.md +122 -0
  163. package/docs/concepts/multi-agent-workflows.md +166 -0
  164. package/docs/concepts/plans-and-claims.md +31 -6
  165. package/docs/concepts/project-md-convention.md +35 -0
  166. package/docs/concepts/troubleshooting.md +220 -0
  167. package/docs/concepts/upgrade-cli.md +202 -0
  168. package/docs/concepts/upgrade-dogfood-procedure.md +114 -0
  169. package/docs/context-format-changelog.md +2 -2
  170. package/docs/context-format.md +2 -2
  171. package/docs/index.md +68 -0
  172. package/docs/integrations/agents.md +15 -16
  173. package/docs/integrations/cline.md +88 -0
  174. package/docs/integrations/codex.md +75 -23
  175. package/docs/integrations/continue.md +60 -0
  176. package/docs/integrations/copilot.md +67 -9
  177. package/docs/integrations/kilocode.md +72 -0
  178. package/docs/integrations/mcp.md +304 -21
  179. package/docs/integrations/mistral-vibe.md +122 -0
  180. package/docs/integrations/opencode.md +84 -0
  181. package/docs/integrations/overview.md +23 -8
  182. package/docs/integrations/roo.md +74 -0
  183. package/docs/integrations/windsurf.md +83 -0
  184. package/docs/mcp-schema-changelog.md +191 -1
  185. package/docs/playbooks/integration/index.md +121 -0
  186. package/docs/playbooks/productivity/index.md +102 -0
  187. package/docs/playbooks/team/index.md +122 -0
  188. package/docs/product/agent-first-model.md +184 -0
  189. package/docs/product/entity-model-audit.md +462 -0
  190. package/docs/quickstart-existing-project.md +135 -0
  191. package/docs/quickstart.md +124 -37
  192. package/docs/release-maintenance.md +79 -0
  193. package/docs/review.md +2 -0
  194. package/docs/server-operations.md +118 -0
  195. package/package.json +20 -12
  196. package/dist/commands/claude-desktop-extension.js +0 -18
  197. package/dist/commands/diff.js +0 -99
  198. package/dist/core/claude-desktop-extension.js +0 -224
@@ -36,13 +36,13 @@ export function detectDuplicates(candidateText, candidateType, state, pendingCan
36
36
  }
37
37
  return matches;
38
38
  }
39
- function normalise(text) {
39
+ export function normalise(text) {
40
40
  return text.toLowerCase().replace(/[^a-z0-9\s]/g, '').replace(/\s+/g, ' ').trim();
41
41
  }
42
42
  /**
43
43
  * Bigram-based similarity (Dice coefficient). Fast, no dependencies.
44
44
  */
45
- function similarity(a, b) {
45
+ export function similarity(a, b) {
46
46
  if (a === b)
47
47
  return 1;
48
48
  if (a.length < 2 || b.length < 2)
@@ -0,0 +1,450 @@
1
+ /**
2
+ * Entity Operations — dispatch layer between the canonical CRUD verbs
3
+ * (bclaw_find/get/create/update/remove/transition) and the entity-specific
4
+ * code in src/core/*.
5
+ *
6
+ * Phase 3 slice 3b (pln_c6472192). Keeps imperative code where it lives
7
+ * (per P6.2) — this module only routes.
8
+ *
9
+ * MVP wiring (this landing): plan, decision, constraint, trap,
10
+ * runtime_note, candidate. Other entities throw
11
+ * `EntityOperationUnsupportedError` with a pointer at the legacy tool
12
+ * until later slices wire them in.
13
+ */
14
+ import path from 'node:path';
15
+ import { loadState, persistState } from './state.js';
16
+ import { archiveCandidate, listCandidates, loadCandidate, saveCandidate, } from './candidates.js';
17
+ import { addCrossProjectLink, removeCrossProjectLink, resolveCrossProjectLinks, } from './cross-project.js';
18
+ import { listClaims } from './claims.js';
19
+ import { listActionRequired } from './actions.js';
20
+ import { listAssignments } from './assignments.js';
21
+ import { listAgentRuns } from './agentruns.js';
22
+ import { deleteRuntimeNote, listRuntimeNotes, saveRuntimeNote, } from './runtime.js';
23
+ import { createConstraint, createDecision, createTrap, } from './operations/memory-write.js';
24
+ import { deleteMemoryItem, findMemoryItemInChain, updateMemoryItem, } from './operations/memory-mutation.js';
25
+ import { createPlan, deletePlan, updatePlan, } from './operations/plan.js';
26
+ import { ENTITY_REGISTRY, isValidTransition, } from './entity-registry.js';
27
+ import { generateId } from './ids.js';
28
+ /**
29
+ * Default provenance stamp applied on create when the caller does not
30
+ * supply one. `user` kind with whatever author is in the payload; the
31
+ * caller can override to 'agent', 'auto_reflect', etc. at write time.
32
+ */
33
+ function defaultProvenance(data) {
34
+ const author = typeof data.author === 'string' ? data.author : undefined;
35
+ if (author)
36
+ return { kind: 'user', author };
37
+ return { kind: 'user' };
38
+ }
39
+ /**
40
+ * Default read filter: exclude legacy provenance and low-confidence
41
+ * auto_reflect records below 0.6. Override via `includeLegacy` /
42
+ * `minAutoReflectConfidence` in the filter.
43
+ */
44
+ const DEFAULT_MIN_AUTO_REFLECT_CONFIDENCE = 0.6;
45
+ function passesProvenanceFilter(item, filter) {
46
+ const provenance = item.provenance;
47
+ if (!provenance)
48
+ return true; // pre-provenance record — never filter out.
49
+ if (provenance.kind === 'legacy') {
50
+ return filter.includeLegacy === true;
51
+ }
52
+ if (provenance.kind === 'auto_reflect') {
53
+ const threshold = typeof filter.minAutoReflectConfidence === 'number'
54
+ ? filter.minAutoReflectConfidence
55
+ : DEFAULT_MIN_AUTO_REFLECT_CONFIDENCE;
56
+ const confidence = provenance.confidence ?? 0;
57
+ return confidence >= threshold;
58
+ }
59
+ return true;
60
+ }
61
+ /** Thrown when a verb is not yet wired for a given entity. */
62
+ export class EntityOperationUnsupportedError extends Error {
63
+ constructor(entity, verb, hint) {
64
+ super(`bclaw_${verb}(entity='${entity}') is not yet wired.` +
65
+ (hint ? ` ${hint}` : ' Use the legacy tool for now.'));
66
+ this.name = 'EntityOperationUnsupportedError';
67
+ }
68
+ }
69
+ export class EntityNotFoundError extends Error {
70
+ constructor(entity, id) {
71
+ super(`${entity} with id '${id}' not found`);
72
+ this.name = 'EntityNotFoundError';
73
+ }
74
+ }
75
+ export class InvalidTransitionError extends Error {
76
+ constructor(entity, from, to) {
77
+ super(`Invalid transition for ${entity}: ${from} -> ${to}`);
78
+ this.name = 'InvalidTransitionError';
79
+ }
80
+ }
81
+ // ─── FIND ─────────────────────────────────────────────────────────────
82
+ export function listEntities(name, cwd, filter = {}) {
83
+ const all = loadAll(name, cwd);
84
+ const filtered = applyFilter(all, filter);
85
+ const paged = applyPaging(filtered, filter);
86
+ return { entity: name, total: filtered.length, items: paged };
87
+ }
88
+ function loadAll(name, cwd) {
89
+ switch (name) {
90
+ case 'plan': return loadState(cwd).plan_items;
91
+ case 'decision': return loadState(cwd).recent_decisions;
92
+ case 'constraint': return loadState(cwd).active_constraints;
93
+ case 'trap': return loadState(cwd).known_traps;
94
+ case 'handoff': return loadState(cwd).open_handoffs;
95
+ case 'candidate': return listCandidates(undefined, cwd);
96
+ case 'runtime_note': return listRuntimeNotes(undefined, cwd);
97
+ case 'claim': return listClaims(cwd);
98
+ case 'action': return listActionRequired(cwd);
99
+ case 'assignment': return listAssignments(cwd);
100
+ case 'agent_run': return listAgentRuns(cwd);
101
+ case 'cross_project_link': return resolveCrossProjectLinks(cwd);
102
+ default:
103
+ throw new EntityOperationUnsupportedError(name, 'find');
104
+ }
105
+ }
106
+ function applyFilter(items, filter) {
107
+ let result = items;
108
+ result = result.filter((item) => passesProvenanceFilter(item, filter));
109
+ if (filter.status) {
110
+ result = result.filter((item) => item.status === filter.status);
111
+ }
112
+ if (filter.tag) {
113
+ result = result.filter((item) => Array.isArray(item.tags) && item.tags.includes(filter.tag));
114
+ }
115
+ if (filter.author) {
116
+ result = result.filter((item) => item.author === filter.author);
117
+ }
118
+ if (filter.plan_id) {
119
+ result = result.filter((item) => item.plan_id === filter.plan_id);
120
+ }
121
+ if (filter.source) {
122
+ result = result.filter((item) => item.source === filter.source);
123
+ }
124
+ if (typeof filter.auto_generated === 'boolean') {
125
+ result = result.filter((item) => {
126
+ const source = typeof item.source === 'string' ? item.source : undefined;
127
+ const origin = typeof item.origin === 'string' ? item.origin : undefined;
128
+ const isAuto = source === 'auto' || origin?.startsWith('session-end') === true;
129
+ return filter.auto_generated ? isAuto : !isAuto;
130
+ });
131
+ }
132
+ return result;
133
+ }
134
+ function applyPaging(items, filter) {
135
+ const offset = Math.max(0, filter.offset ?? 0);
136
+ const limit = filter.limit ?? 50;
137
+ return items.slice(offset, offset + limit);
138
+ }
139
+ // ─── GET ───────────────────────────────────────────────────────────────
140
+ export function getEntity(name, idOrShortLabel, cwd) {
141
+ if (name === 'cross_project_link') {
142
+ const links = resolveCrossProjectLinks(cwd);
143
+ const hit = links.find((l) => l.name === idOrShortLabel ||
144
+ l.projectName === idOrShortLabel ||
145
+ l.path === idOrShortLabel ||
146
+ l.absolutePath === idOrShortLabel ||
147
+ path.basename(l.absolutePath) === idOrShortLabel);
148
+ if (!hit)
149
+ throw new EntityNotFoundError(name, idOrShortLabel);
150
+ return hit;
151
+ }
152
+ const items = loadAll(name, cwd);
153
+ const hit = items.find((item) => item.id === idOrShortLabel || item.short_label === idOrShortLabel);
154
+ if (!hit)
155
+ throw new EntityNotFoundError(name, idOrShortLabel);
156
+ return hit;
157
+ }
158
+ // ─── CREATE ────────────────────────────────────────────────────────────
159
+ export function createEntity(name, data, cwd) {
160
+ switch (name) {
161
+ case 'plan': {
162
+ // Explicit field whitelist + required-author check brings plan create in line
163
+ // with decision/constraint/trap above. Previous behaviour (`data as CreatePlanInput`)
164
+ // bypassed validation and allowed schema-invalid plan files on disk, which
165
+ // were then silently GC'd at the next state mutation. See fix plan pln_5f44426c.
166
+ const res = createPlan({
167
+ text: requireString(data, 'text'),
168
+ author: requireString(data, 'author'),
169
+ type: data.type,
170
+ priority: data.priority,
171
+ assignee: data.assignee,
172
+ project: data.project,
173
+ tags: data.tags,
174
+ relatedPaths: data.related_paths,
175
+ dependsOn: data.depends_on,
176
+ estimatedEffort: data.estimated_effort,
177
+ }, cwd);
178
+ stampProvenanceOnStateItem('plan', res.id, defaultProvenance(data), cwd);
179
+ return { entity: name, id: res.id, short_label: res.shortLabel };
180
+ }
181
+ case 'decision': {
182
+ const res = createDecision({
183
+ text: requireString(data, 'text'),
184
+ author: requireString(data, 'author'),
185
+ outcome: data.outcome,
186
+ tags: data.tags,
187
+ relatedPaths: data.related_paths,
188
+ planId: data.plan_id,
189
+ }, cwd);
190
+ stampProvenanceOnStateItem('decision', res.id, defaultProvenance(data), cwd);
191
+ return { entity: name, id: res.id, short_label: res.shortLabel };
192
+ }
193
+ case 'constraint': {
194
+ const res = createConstraint({
195
+ text: requireString(data, 'text'),
196
+ author: requireString(data, 'author'),
197
+ category: data.category,
198
+ tags: data.tags,
199
+ relatedPaths: data.related_paths,
200
+ }, cwd);
201
+ stampProvenanceOnStateItem('constraint', res.id, defaultProvenance(data), cwd);
202
+ return { entity: name, id: res.id, short_label: res.shortLabel };
203
+ }
204
+ case 'trap': {
205
+ const res = createTrap({
206
+ text: requireString(data, 'text'),
207
+ author: requireString(data, 'author'),
208
+ severity: (data.severity ?? 'medium'),
209
+ tags: data.tags,
210
+ relatedPaths: data.related_paths,
211
+ }, cwd);
212
+ stampProvenanceOnStateItem('trap', res.id, defaultProvenance(data), cwd);
213
+ return { entity: name, id: res.id, short_label: res.shortLabel };
214
+ }
215
+ case 'runtime_note': {
216
+ const id = generateId('runtime_note');
217
+ const note = {
218
+ id,
219
+ agent: requireString(data, 'agent'),
220
+ text: requireString(data, 'text'),
221
+ created_at: new Date().toISOString(),
222
+ tags: data.tags ?? [],
223
+ visibility: data.visibility ?? 'shared',
224
+ note_type: data.note_type ?? 'observation',
225
+ provenance: defaultProvenance(data),
226
+ ...(data.agent_id ? { agent_id: data.agent_id } : {}),
227
+ ...(data.project_id ? { project_id: data.project_id } : {}),
228
+ ...(data.session_id ? { session_id: data.session_id } : {}),
229
+ ...(data.plan_id ? { plan_id: data.plan_id } : {}),
230
+ };
231
+ saveRuntimeNote(note, cwd);
232
+ return { entity: name, id };
233
+ }
234
+ case 'candidate': {
235
+ const id = generateId('candidate');
236
+ const candidate = {
237
+ id,
238
+ type: requireString(data, 'type'),
239
+ text: requireString(data, 'text'),
240
+ created_at: new Date().toISOString(),
241
+ author: requireString(data, 'author'),
242
+ tags: data.tags ?? [],
243
+ status: 'pending',
244
+ star_count: 0,
245
+ starred_by: [],
246
+ usage_count: 0,
247
+ usage_events: [],
248
+ source: data.source,
249
+ origin: data.origin,
250
+ provenance: defaultProvenance(data),
251
+ };
252
+ saveCandidate(candidate, cwd);
253
+ return { entity: name, id };
254
+ }
255
+ case 'cross_project_link': {
256
+ const link = addCrossProjectLink({
257
+ path: requireString(data, 'path'),
258
+ name: data.name,
259
+ role: data.role,
260
+ channels: data.channels,
261
+ force: data.force === true,
262
+ cwd,
263
+ });
264
+ return { entity: name, id: link.name ?? link.path };
265
+ }
266
+ default:
267
+ throw new EntityOperationUnsupportedError(name, 'create');
268
+ }
269
+ }
270
+ // ─── UPDATE ────────────────────────────────────────────────────────────
271
+ export function updateEntity(name, id, patch, cwd) {
272
+ const spec = ENTITY_REGISTRY[name];
273
+ const invalidFields = Object.keys(patch).filter((field) => !spec.updatable.includes(field));
274
+ if (invalidFields.length > 0) {
275
+ throw new Error(`Fields not updatable on ${name}: [${invalidFields.join(', ')}]. ` +
276
+ `Use bclaw_transition for status changes.`);
277
+ }
278
+ switch (name) {
279
+ case 'plan': {
280
+ // Pass the whole patch through the generic escape-hatch so EntityRegistry-
281
+ // declared updatable fields (text, tags, estimated_effort, depends_on)
282
+ // actually land. The typed surface still covers status/assignee/priority/
283
+ // actualEffort for legacy CLI callers — see UpdatePlanInput.
284
+ updatePlan({
285
+ id,
286
+ patch: patch,
287
+ }, cwd);
288
+ return { entity: name, id };
289
+ }
290
+ case 'decision':
291
+ case 'constraint':
292
+ case 'trap': {
293
+ // Same generic-patch escape-hatch for memory items. Registry declares
294
+ // severity, scope, related_paths, expires_at, etc. as updatable; the
295
+ // legacy explicit text/tags whitelist silently dropped them.
296
+ updateMemoryItem({
297
+ id,
298
+ type: name,
299
+ patch: patch,
300
+ }, cwd);
301
+ return { entity: name, id };
302
+ }
303
+ case 'runtime_note': {
304
+ const notes = listRuntimeNotes(undefined, cwd);
305
+ const note = notes.find((n) => n.id === id);
306
+ if (!note)
307
+ throw new EntityNotFoundError(name, id);
308
+ const patched = { ...note, ...patch };
309
+ saveRuntimeNote(patched, cwd);
310
+ return { entity: name, id };
311
+ }
312
+ case 'candidate': {
313
+ const candidate = loadCandidate(id, cwd);
314
+ const patched = { ...candidate, ...patch };
315
+ saveCandidate(patched, cwd);
316
+ return { entity: name, id };
317
+ }
318
+ case 'cross_project_link': {
319
+ // In-place patch: find by id (= name/path), remove, re-add with merged
320
+ // fields. Same path semantics as resolveCrossProjectTarget so callers can
321
+ // pass either the link `name` or the original `path`.
322
+ const current = getEntity(name, id, cwd);
323
+ removeCrossProjectLink(current.name ?? current.path, cwd);
324
+ const merged = addCrossProjectLink({
325
+ path: current.path,
326
+ name: patch.name ?? current.name,
327
+ role: (patch.role ?? current.role),
328
+ channels: patch.channels ?? current.channels,
329
+ cwd,
330
+ force: true,
331
+ });
332
+ return { entity: name, id: merged.name ?? merged.path };
333
+ }
334
+ default:
335
+ throw new EntityOperationUnsupportedError(name, 'update');
336
+ }
337
+ }
338
+ // ─── REMOVE ────────────────────────────────────────────────────────────
339
+ export function removeEntity(name, id, cwd, purge = false) {
340
+ switch (name) {
341
+ case 'plan': {
342
+ deletePlan(id, cwd);
343
+ return { entity: name, id, archived: !purge, purged: purge };
344
+ }
345
+ case 'decision':
346
+ case 'constraint':
347
+ case 'trap': {
348
+ const found = findMemoryItemInChain(id, name, cwd);
349
+ if (!found)
350
+ throw new EntityNotFoundError(name, id);
351
+ deleteMemoryItem(id, name, cwd);
352
+ return { entity: name, id, archived: false, purged: true };
353
+ }
354
+ case 'runtime_note': {
355
+ const notes = listRuntimeNotes(undefined, cwd);
356
+ const note = notes.find((n) => n.id === id);
357
+ if (!note)
358
+ throw new EntityNotFoundError(name, id);
359
+ const ok = deleteRuntimeNote(note, cwd);
360
+ if (!ok)
361
+ throw new EntityNotFoundError(name, id);
362
+ return { entity: name, id, archived: false, purged: true };
363
+ }
364
+ case 'candidate': {
365
+ // Remove = archive to rejected. `purge` would delete the file; not exposed yet.
366
+ const candidate = loadCandidate(id, cwd);
367
+ archiveCandidate(candidate, 'rejected', cwd);
368
+ return { entity: name, id, archived: true, purged: false };
369
+ }
370
+ case 'cross_project_link': {
371
+ const removed = removeCrossProjectLink(id, cwd);
372
+ return { entity: name, id: removed.name ?? removed.path, archived: false, purged: true };
373
+ }
374
+ default:
375
+ throw new EntityOperationUnsupportedError(name, 'remove');
376
+ }
377
+ }
378
+ // ─── TRANSITION ───────────────────────────────────────────────────────
379
+ export function transitionEntity(name, id, to, cwd, _reason) {
380
+ const spec = ENTITY_REGISTRY[name];
381
+ if (!spec.statusField) {
382
+ throw new Error(`${name} has no lifecycle (statusField is undefined)`);
383
+ }
384
+ const current = getEntity(name, id, cwd);
385
+ const from = current[spec.statusField];
386
+ if (!from) {
387
+ throw new Error(`${name} '${id}' has no '${spec.statusField}' field set`);
388
+ }
389
+ if (!isValidTransition(name, from, to)) {
390
+ throw new InvalidTransitionError(name, from, to);
391
+ }
392
+ const key = `${from}->${to}`;
393
+ const sideEffects = spec.sideEffects[key] ?? [];
394
+ switch (name) {
395
+ case 'plan': {
396
+ updatePlan({ id, status: to }, cwd);
397
+ return { entity: name, id, from, to, side_effects: sideEffects };
398
+ }
399
+ case 'decision':
400
+ case 'constraint':
401
+ case 'trap': {
402
+ const state = loadState(cwd);
403
+ const bucket = name === 'decision' ? state.recent_decisions
404
+ : name === 'constraint' ? state.active_constraints
405
+ : state.known_traps;
406
+ const item = bucket.find((x) => x.id === id);
407
+ if (!item)
408
+ throw new EntityNotFoundError(name, id);
409
+ item[spec.statusField] = to;
410
+ persistState(state, cwd);
411
+ return { entity: name, id, from, to, side_effects: sideEffects };
412
+ }
413
+ case 'candidate': {
414
+ const candidate = loadCandidate(id, cwd);
415
+ if (to === 'accepted' || to === 'rejected') {
416
+ archiveCandidate(candidate, to, cwd);
417
+ return { entity: name, id, from, to, side_effects: sideEffects };
418
+ }
419
+ throw new InvalidTransitionError(name, from, to);
420
+ }
421
+ default:
422
+ throw new EntityOperationUnsupportedError(name, 'transition', `Lifecycle transitions for ${name} not yet wired.`);
423
+ }
424
+ }
425
+ // ─── Helpers ──────────────────────────────────────────────────────────
426
+ /**
427
+ * Stamp provenance on a state-resident record (plan, decision, constraint, trap)
428
+ * immediately after create. Writes one extra persistState call; acceptable for
429
+ * v1 since create is infrequent compared to reads.
430
+ */
431
+ function stampProvenanceOnStateItem(name, id, provenance, cwd) {
432
+ const state = loadState(cwd);
433
+ const bucket = name === 'plan' ? state.plan_items
434
+ : name === 'decision' ? state.recent_decisions
435
+ : name === 'constraint' ? state.active_constraints
436
+ : state.known_traps;
437
+ const item = bucket.find((x) => x.id === id);
438
+ if (!item)
439
+ return;
440
+ item.provenance = provenance;
441
+ persistState(state, cwd);
442
+ }
443
+ function requireString(data, field) {
444
+ const value = data[field];
445
+ if (typeof value !== 'string' || !value) {
446
+ throw new Error(`Missing required field: ${field}`);
447
+ }
448
+ return value;
449
+ }
450
+ //# sourceMappingURL=entity-operations.js.map