@poa-box/agent 0.1.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 (139) hide show
  1. package/.env.agent.template +20 -0
  2. package/README.md +46 -0
  3. package/brain/Config/agent-config.json +14 -0
  4. package/brain/Config/brain-allowlist.json +20 -0
  5. package/brain/Identity/goals.template.md +23 -0
  6. package/brain/Identity/how-i-think.md +406 -0
  7. package/brain/Identity/who-i-am.template.md +34 -0
  8. package/brain/Knowledge/BOOTSTRAP.md +66 -0
  9. package/brain/Knowledge/audit-corpus-index.json +406 -0
  10. package/brain/Knowledge/discussions.json +245 -0
  11. package/brain/Knowledge/pop.brain.brainstorms.generated.md +48 -0
  12. package/brain/Knowledge/pop.brain.brainstorms.genesis.bin +0 -0
  13. package/brain/Knowledge/pop.brain.heuristics.snapshot.bin +0 -0
  14. package/brain/Knowledge/pop.brain.projects.generated.md +16 -0
  15. package/brain/Knowledge/pop.brain.projects.genesis.bin +0 -0
  16. package/brain/Knowledge/pop.brain.retros.generated.md +91 -0
  17. package/brain/Knowledge/pop.brain.retros.genesis.bin +0 -0
  18. package/brain/Knowledge/pop.brain.shared.generated.md +3811 -0
  19. package/brain/Knowledge/pop.brain.shared.genesis.bin +0 -0
  20. package/brain/Knowledge/projects.md +181 -0
  21. package/brain/Knowledge/risk-framework.md +90 -0
  22. package/brain/Knowledge/shared.md +416 -0
  23. package/brain/Knowledge/sprint-priorities.md +439 -0
  24. package/brain/Memory/.gitkeep +0 -0
  25. package/dist/commands/agent/daily-digest.d.ts +24 -0
  26. package/dist/commands/agent/daily-digest.js +336 -0
  27. package/dist/commands/agent/delegate.d.ts +12 -0
  28. package/dist/commands/agent/delegate.js +91 -0
  29. package/dist/commands/agent/deploy-to-org.d.ts +20 -0
  30. package/dist/commands/agent/deploy-to-org.js +154 -0
  31. package/dist/commands/agent/index.d.ts +2 -0
  32. package/dist/commands/agent/index.js +27 -0
  33. package/dist/commands/agent/init.d.ts +19 -0
  34. package/dist/commands/agent/init.js +303 -0
  35. package/dist/commands/agent/onboard.d.ts +22 -0
  36. package/dist/commands/agent/onboard.js +192 -0
  37. package/dist/commands/agent/paymaster-status.d.ts +14 -0
  38. package/dist/commands/agent/paymaster-status.js +130 -0
  39. package/dist/commands/agent/register.d.ts +21 -0
  40. package/dist/commands/agent/register.js +116 -0
  41. package/dist/commands/agent/setup-sponsorship.d.ts +22 -0
  42. package/dist/commands/agent/setup-sponsorship.js +154 -0
  43. package/dist/commands/agent/status.d.ts +12 -0
  44. package/dist/commands/agent/status.js +171 -0
  45. package/dist/commands/agent/triage.d.ts +12 -0
  46. package/dist/commands/agent/triage.js +503 -0
  47. package/dist/commands/brain/advance-stage.d.ts +42 -0
  48. package/dist/commands/brain/advance-stage.js +206 -0
  49. package/dist/commands/brain/allowlist.d.ts +30 -0
  50. package/dist/commands/brain/allowlist.js +274 -0
  51. package/dist/commands/brain/append-lesson.d.ts +55 -0
  52. package/dist/commands/brain/append-lesson.js +245 -0
  53. package/dist/commands/brain/brainstorm.d.ts +154 -0
  54. package/dist/commands/brain/brainstorm.js +573 -0
  55. package/dist/commands/brain/daemon.d.ts +31 -0
  56. package/dist/commands/brain/daemon.js +348 -0
  57. package/dist/commands/brain/doctor.d.ts +27 -0
  58. package/dist/commands/brain/doctor.js +497 -0
  59. package/dist/commands/brain/edit-lesson.d.ts +51 -0
  60. package/dist/commands/brain/edit-lesson.js +248 -0
  61. package/dist/commands/brain/import-snapshot.d.ts +68 -0
  62. package/dist/commands/brain/import-snapshot.js +177 -0
  63. package/dist/commands/brain/index.d.ts +2 -0
  64. package/dist/commands/brain/index.js +67 -0
  65. package/dist/commands/brain/list.d.ts +21 -0
  66. package/dist/commands/brain/list.js +83 -0
  67. package/dist/commands/brain/migrate-projects.d.ts +44 -0
  68. package/dist/commands/brain/migrate-projects.js +209 -0
  69. package/dist/commands/brain/migrate.d.ts +74 -0
  70. package/dist/commands/brain/migrate.js +306 -0
  71. package/dist/commands/brain/new-project.d.ts +53 -0
  72. package/dist/commands/brain/new-project.js +226 -0
  73. package/dist/commands/brain/read.d.ts +24 -0
  74. package/dist/commands/brain/read.js +81 -0
  75. package/dist/commands/brain/remove-lesson.d.ts +47 -0
  76. package/dist/commands/brain/remove-lesson.js +206 -0
  77. package/dist/commands/brain/remove-project.d.ts +36 -0
  78. package/dist/commands/brain/remove-project.js +177 -0
  79. package/dist/commands/brain/retro-file-tasks.d.ts +84 -0
  80. package/dist/commands/brain/retro-file-tasks.js +372 -0
  81. package/dist/commands/brain/retro-list.d.ts +28 -0
  82. package/dist/commands/brain/retro-list.js +125 -0
  83. package/dist/commands/brain/retro-mark-change.d.ts +58 -0
  84. package/dist/commands/brain/retro-mark-change.js +176 -0
  85. package/dist/commands/brain/retro-remove.d.ts +36 -0
  86. package/dist/commands/brain/retro-remove.js +142 -0
  87. package/dist/commands/brain/retro-respond.d.ts +56 -0
  88. package/dist/commands/brain/retro-respond.js +250 -0
  89. package/dist/commands/brain/retro-show.d.ts +23 -0
  90. package/dist/commands/brain/retro-show.js +100 -0
  91. package/dist/commands/brain/retro-start.d.ts +55 -0
  92. package/dist/commands/brain/retro-start.js +311 -0
  93. package/dist/commands/brain/search.d.ts +48 -0
  94. package/dist/commands/brain/search.js +190 -0
  95. package/dist/commands/brain/snapshot.d.ts +32 -0
  96. package/dist/commands/brain/snapshot.js +243 -0
  97. package/dist/commands/brain/status.d.ts +15 -0
  98. package/dist/commands/brain/status.js +166 -0
  99. package/dist/commands/brain/subscribe.d.ts +28 -0
  100. package/dist/commands/brain/subscribe.js +90 -0
  101. package/dist/commands/brain/tag.d.ts +46 -0
  102. package/dist/commands/brain/tag.js +192 -0
  103. package/dist/index.d.ts +17 -0
  104. package/dist/index.js +22 -0
  105. package/dist/lib/brain-daemon.d.ts +126 -0
  106. package/dist/lib/brain-daemon.js +811 -0
  107. package/dist/lib/brain-membership.d.ts +58 -0
  108. package/dist/lib/brain-membership.js +115 -0
  109. package/dist/lib/brain-migrate-projects.d.ts +43 -0
  110. package/dist/lib/brain-migrate-projects.js +247 -0
  111. package/dist/lib/brain-migrate.d.ts +77 -0
  112. package/dist/lib/brain-migrate.js +328 -0
  113. package/dist/lib/brain-ops.d.ts +271 -0
  114. package/dist/lib/brain-ops.js +571 -0
  115. package/dist/lib/brain-paths.d.ts +15 -0
  116. package/dist/lib/brain-paths.js +33 -0
  117. package/dist/lib/brain-projections.d.ts +216 -0
  118. package/dist/lib/brain-projections.js +829 -0
  119. package/dist/lib/brain-schemas.d.ts +36 -0
  120. package/dist/lib/brain-schemas.js +316 -0
  121. package/dist/lib/brain-signing.d.ts +103 -0
  122. package/dist/lib/brain-signing.js +256 -0
  123. package/dist/lib/brain.d.ts +198 -0
  124. package/dist/lib/brain.js +1057 -0
  125. package/dist/pop-agent.d.ts +1 -0
  126. package/dist/pop-agent.js +18 -0
  127. package/docs/agent.md +126 -0
  128. package/docs/agents/brain-anti-entropy.md +127 -0
  129. package/docs/agents/brain-cross-device-onboarding.md +210 -0
  130. package/docs/agents/brain-cross-machine-smoke.md +241 -0
  131. package/docs/agents/brain-layer-setup.md +725 -0
  132. package/docs/agents/offboarding-protocol.md +188 -0
  133. package/docs/agents/onboarding-protocol.md +243 -0
  134. package/docs/agents/running-an-agent.md +200 -0
  135. package/docs/brain.md +560 -0
  136. package/package.json +61 -0
  137. package/scripts/apply.sh +140 -0
  138. package/scripts/onboard.sh +205 -0
  139. package/scripts/setup-agent.ts +272 -0
@@ -0,0 +1,571 @@
1
+ "use strict";
2
+ /**
3
+ * Unified brain write dispatcher (HB#324 principal-engineer ship-2).
4
+ *
5
+ * ## Why this module exists
6
+ *
7
+ * Before this module, every brain write command (append-lesson, edit-lesson,
8
+ * remove-lesson, new-project, advance-stage, remove-project, …) called
9
+ * `applyBrainChange` directly with a closure. That works fine in-process but
10
+ * cannot be routed through IPC to a running daemon — closures don't serialize.
11
+ *
12
+ * The brain daemon needs to be the single owner of libp2p/gossipsub when it's
13
+ * running (HB#312 dogfood wedge proved sequential agent sessions never overlap
14
+ * in wall-clock time, so a short-lived in-process libp2p can't reliably
15
+ * deliver a gossipsub announcement before exiting). For the daemon to be
16
+ * useful, every write has to route through it. For the daemon to be usable,
17
+ * the CLI commands need a transparent fallback when no daemon is running.
18
+ *
19
+ * The solution is to turn every brain write into a pure-data operation
20
+ * descriptor (`BrainOp`) and have two execution paths that share zero
21
+ * business logic:
22
+ *
23
+ * `dispatchOp(op)` Runs the op in the current process. Used by:
24
+ * (a) the daemon's IPC handler, and
25
+ * (b) CLI commands as a fallback when no daemon.
26
+ * `routedDispatch(op)` Entry point for CLI commands. Checks for a running
27
+ * daemon: if yes, sends `applyOp` via IPC; if no,
28
+ * calls dispatchOp locally.
29
+ *
30
+ * Both paths converge on the same `dispatchOp` function. There is no
31
+ * "local" vs "routed" business logic — only a transport decision.
32
+ *
33
+ * ## Fallback correctness (important)
34
+ *
35
+ * `routedDispatch` must be careful about when it falls back to local
36
+ * dispatch. If the daemon has already processed the write and only the
37
+ * *response* got lost, a silent local fallback causes a double-write.
38
+ *
39
+ * The rule:
40
+ * - Pre-connect IPC failure (ECONNREFUSED, ENOENT, daemon not running at
41
+ * all) → SAFE to fall back. The write definitely did not land in the
42
+ * daemon's process because we never opened the connection.
43
+ * - Post-connect IPC failure (ECONNRESET, EPIPE, timeout mid-call) → NOT
44
+ * safe. The daemon may have processed the write. Error out and let the
45
+ * operator decide.
46
+ *
47
+ * This is the opposite of retry-everything semantics, on purpose. Brain
48
+ * writes are append-mostly and deduplication is hard once the block lands.
49
+ *
50
+ * ## Adding a new op
51
+ *
52
+ * 1. Add the variant to the `BrainOp` discriminated union below.
53
+ * 2. Add the case to `dispatchOp`'s switch statement.
54
+ * 3. Replace the `applyBrainChange(doc, closure)` call in your command
55
+ * with `routedDispatch({type: 'yourOp', ...})`.
56
+ *
57
+ * The daemon's IPC handler is a one-line `return dispatchOp(params.op)` —
58
+ * it does not need to be updated for new ops.
59
+ */
60
+ Object.defineProperty(exports, "__esModule", { value: true });
61
+ exports.dispatchOp = dispatchOp;
62
+ exports.routedDispatch = routedDispatch;
63
+ const brain_1 = require("./brain");
64
+ const brain_daemon_1 = require("./brain-daemon");
65
+ // ---------------------------------------------------------------------------
66
+ // Local dispatch — runs in the current process
67
+ // ---------------------------------------------------------------------------
68
+ /**
69
+ * Apply a BrainOp in the current process. Translates the op descriptor to
70
+ * the corresponding Automerge change function and calls `applyBrainChange`,
71
+ * which signs the envelope, writes the block, updates the manifest, and
72
+ * publishes the new head CID via gossipsub.
73
+ *
74
+ * Same function is called from two places:
75
+ * - The daemon's `applyOp` IPC handler (when a CLI routed a write)
76
+ * - The CLI's `routedDispatch` fallback (when no daemon is running)
77
+ *
78
+ * Errors thrown from the change function propagate out as normal Error
79
+ * instances. For example, editLesson throws if the target lesson id is
80
+ * missing.
81
+ */
82
+ async function dispatchOp(op) {
83
+ let applied;
84
+ switch (op.type) {
85
+ case 'appendLesson': {
86
+ applied = await (0, brain_1.applyBrainChange)(op.docId, (doc) => {
87
+ if (!Array.isArray(doc.lessons))
88
+ doc.lessons = [];
89
+ doc.lessons.push({
90
+ id: op.id,
91
+ title: op.title,
92
+ author: op.author,
93
+ body: op.body,
94
+ timestamp: op.timestamp,
95
+ });
96
+ }, { allowInvalidShape: op.allowInvalidShape });
97
+ break;
98
+ }
99
+ case 'editLesson': {
100
+ applied = await (0, brain_1.applyBrainChange)(op.docId, (doc) => {
101
+ if (!Array.isArray(doc.lessons)) {
102
+ throw new Error(`no lessons list in doc ${op.docId}`);
103
+ }
104
+ const lesson = doc.lessons.find((l) => l && l.id === op.lessonId);
105
+ if (!lesson) {
106
+ throw new Error(`lesson ${op.lessonId} not found in ${op.docId}`);
107
+ }
108
+ if (op.fields.title !== undefined)
109
+ lesson.title = op.fields.title;
110
+ if (op.fields.body !== undefined)
111
+ lesson.body = op.fields.body;
112
+ if (op.fields.author !== undefined)
113
+ lesson.author = op.fields.author;
114
+ if (op.touch)
115
+ lesson.timestamp = Math.floor(Date.now() / 1000);
116
+ }, { allowInvalidShape: op.allowInvalidShape });
117
+ break;
118
+ }
119
+ case 'removeLesson': {
120
+ applied = await (0, brain_1.applyBrainChange)(op.docId, (doc) => {
121
+ if (!Array.isArray(doc.lessons)) {
122
+ throw new Error(`no lessons list in doc ${op.docId}`);
123
+ }
124
+ const lesson = doc.lessons.find((l) => l && l.id === op.lessonId);
125
+ if (!lesson) {
126
+ throw new Error(`lesson ${op.lessonId} not found in ${op.docId}`);
127
+ }
128
+ // Soft-delete tombstone — see brain-projections.ts filter logic.
129
+ lesson.removed = true;
130
+ lesson.removedAt = op.removedAt;
131
+ lesson.removedBy = op.removedBy;
132
+ if (op.removedReason)
133
+ lesson.removedReason = op.removedReason;
134
+ }, { allowInvalidShape: op.allowInvalidShape });
135
+ break;
136
+ }
137
+ case 'newProject': {
138
+ applied = await (0, brain_1.applyBrainChange)(op.docId, (doc) => {
139
+ if (!Array.isArray(doc.projects))
140
+ doc.projects = [];
141
+ // Collision check — upstream should have resolved with slug + suffix,
142
+ // but we defend here because routedDispatch is the last line before
143
+ // applyBrainChange.
144
+ if (doc.projects.some((p) => p && p.id === op.projectId)) {
145
+ throw new Error(`project id ${op.projectId} already exists in ${op.docId}`);
146
+ }
147
+ const entry = {
148
+ id: op.projectId,
149
+ name: op.name,
150
+ stage: op.stage,
151
+ proposedBy: op.proposedBy,
152
+ proposedAt: op.proposedAt,
153
+ };
154
+ if (op.brief !== undefined && op.brief !== '')
155
+ entry.brief = op.brief;
156
+ doc.projects.push(entry);
157
+ });
158
+ break;
159
+ }
160
+ case 'advanceStage': {
161
+ applied = await (0, brain_1.applyBrainChange)(op.docId, (doc) => {
162
+ if (!Array.isArray(doc.projects)) {
163
+ throw new Error(`no projects list in doc ${op.docId}`);
164
+ }
165
+ const idx = doc.projects.findIndex((p) => p && p.id === op.projectId);
166
+ if (idx === -1) {
167
+ throw new Error(`project ${op.projectId} not found in ${op.docId}`);
168
+ }
169
+ const project = doc.projects[idx];
170
+ if (project.removed) {
171
+ throw new Error(`project ${op.projectId} is tombstoned — cannot advance`);
172
+ }
173
+ project.stage = op.newStage;
174
+ project.lastStageAdvanceAt = op.lastStageAdvanceAt;
175
+ });
176
+ break;
177
+ }
178
+ case 'removeProject': {
179
+ applied = await (0, brain_1.applyBrainChange)(op.docId, (doc) => {
180
+ if (!Array.isArray(doc.projects)) {
181
+ throw new Error(`no projects list in doc ${op.docId}`);
182
+ }
183
+ const project = doc.projects.find((p) => p && p.id === op.projectId);
184
+ if (!project) {
185
+ throw new Error(`project ${op.projectId} not found in ${op.docId}`);
186
+ }
187
+ project.removed = true;
188
+ project.removedAt = op.removedAt;
189
+ project.removedBy = op.removedBy;
190
+ if (op.removedReason)
191
+ project.removedReason = op.removedReason;
192
+ });
193
+ break;
194
+ }
195
+ case 'startRetro': {
196
+ applied = await (0, brain_1.applyBrainChange)(op.docId, (doc) => {
197
+ if (!Array.isArray(doc.retros))
198
+ doc.retros = [];
199
+ if (doc.retros.some((r) => r && r.id === op.retroId)) {
200
+ throw new Error(`retro id ${op.retroId} already exists in ${op.docId}`);
201
+ }
202
+ // Schema validation at write time (task constraint).
203
+ if (!op.author)
204
+ throw new Error('startRetro: author is required');
205
+ if (!op.window || typeof op.window.from !== 'number' || typeof op.window.to !== 'number') {
206
+ throw new Error('startRetro: window.from and window.to are required numbers');
207
+ }
208
+ if (op.window.to < op.window.from) {
209
+ throw new Error(`startRetro: window.to (${op.window.to}) must be >= window.from (${op.window.from})`);
210
+ }
211
+ if (!Array.isArray(op.proposedChanges)) {
212
+ throw new Error('startRetro: proposedChanges must be an array');
213
+ }
214
+ // Enforce unique change ids within a retro.
215
+ const seenIds = new Set();
216
+ for (const change of op.proposedChanges) {
217
+ if (!change.id || !change.summary) {
218
+ throw new Error('startRetro: each proposed change requires id and summary');
219
+ }
220
+ if (seenIds.has(change.id)) {
221
+ throw new Error(`startRetro: duplicate change id "${change.id}"`);
222
+ }
223
+ seenIds.add(change.id);
224
+ }
225
+ const entry = {
226
+ id: op.retroId,
227
+ author: op.author,
228
+ hb: op.hb,
229
+ window: { from: op.window.from, to: op.window.to },
230
+ observations: {},
231
+ proposedChanges: op.proposedChanges.map(c => {
232
+ const out = {
233
+ id: c.id,
234
+ summary: c.summary,
235
+ status: c.status ?? 'proposed',
236
+ };
237
+ if (c.details !== undefined && c.details !== '')
238
+ out.details = c.details;
239
+ return out;
240
+ }),
241
+ discussion: [],
242
+ status: 'open',
243
+ createdAt: op.createdAt,
244
+ };
245
+ if (op.observations.worked !== undefined && op.observations.worked !== '') {
246
+ entry.observations.worked = op.observations.worked;
247
+ }
248
+ if (op.observations.didntWork !== undefined && op.observations.didntWork !== '') {
249
+ entry.observations.didntWork = op.observations.didntWork;
250
+ }
251
+ doc.retros.push(entry);
252
+ });
253
+ break;
254
+ }
255
+ case 'respondToRetro': {
256
+ applied = await (0, brain_1.applyBrainChange)(op.docId, (doc) => {
257
+ if (!Array.isArray(doc.retros)) {
258
+ throw new Error(`no retros list in doc ${op.docId}`);
259
+ }
260
+ const retro = doc.retros.find((r) => r && r.id === op.retroId);
261
+ if (!retro) {
262
+ throw new Error(`retro ${op.retroId} not found in ${op.docId}`);
263
+ }
264
+ if (retro.removed) {
265
+ throw new Error(`retro ${op.retroId} is tombstoned — cannot respond`);
266
+ }
267
+ if (!Array.isArray(retro.discussion))
268
+ retro.discussion = [];
269
+ // Validate votes refer to real change ids if provided.
270
+ if (op.votePerChange) {
271
+ const changeIds = new Set((retro.proposedChanges ?? []).map((c) => c?.id).filter((id) => typeof id === 'string'));
272
+ for (const changeId of Object.keys(op.votePerChange)) {
273
+ if (!changeIds.has(changeId)) {
274
+ throw new Error(`respondToRetro: vote references unknown change id "${changeId}" ` +
275
+ `(retro has: ${Array.from(changeIds).join(', ') || '(none)'})`);
276
+ }
277
+ }
278
+ }
279
+ const entry = {
280
+ author: op.author,
281
+ message: op.message,
282
+ timestamp: op.timestamp,
283
+ };
284
+ if (op.hb !== undefined)
285
+ entry.hb = op.hb;
286
+ if (op.votePerChange && Object.keys(op.votePerChange).length > 0) {
287
+ entry.votePerChange = { ...op.votePerChange };
288
+ }
289
+ retro.discussion.push(entry);
290
+ // Advance retro.status to 'discussed' on first response.
291
+ if (retro.status === 'open')
292
+ retro.status = 'discussed';
293
+ });
294
+ break;
295
+ }
296
+ case 'updateChangeStatus': {
297
+ applied = await (0, brain_1.applyBrainChange)(op.docId, (doc) => {
298
+ if (!Array.isArray(doc.retros)) {
299
+ throw new Error(`no retros list in doc ${op.docId}`);
300
+ }
301
+ const retro = doc.retros.find((r) => r && r.id === op.retroId);
302
+ if (!retro) {
303
+ throw new Error(`retro ${op.retroId} not found in ${op.docId}`);
304
+ }
305
+ if (!Array.isArray(retro.proposedChanges)) {
306
+ throw new Error(`retro ${op.retroId} has no proposedChanges list`);
307
+ }
308
+ const change = retro.proposedChanges.find((c) => c && c.id === op.changeId);
309
+ if (!change) {
310
+ throw new Error(`change ${op.changeId} not found in retro ${op.retroId} ` +
311
+ `(available: ${retro.proposedChanges.map((c) => c?.id).join(', ')})`);
312
+ }
313
+ change.status = op.newStatus;
314
+ if (op.newStatus === 'filed' && op.filedTaskId) {
315
+ change.filedTaskId = op.filedTaskId;
316
+ }
317
+ // If all changes are filed or rejected, mark the retro as shipped.
318
+ const stillOpen = retro.proposedChanges.some((c) => c && c.status !== 'filed' && c.status !== 'rejected');
319
+ if (!stillOpen) {
320
+ retro.status = 'shipped';
321
+ retro.closedAt = Math.floor(Date.now() / 1000);
322
+ }
323
+ });
324
+ break;
325
+ }
326
+ case 'removeRetro': {
327
+ applied = await (0, brain_1.applyBrainChange)(op.docId, (doc) => {
328
+ if (!Array.isArray(doc.retros)) {
329
+ throw new Error(`no retros list in doc ${op.docId}`);
330
+ }
331
+ const retro = doc.retros.find((r) => r && r.id === op.retroId);
332
+ if (!retro) {
333
+ throw new Error(`retro ${op.retroId} not found in ${op.docId}`);
334
+ }
335
+ retro.removed = true;
336
+ retro.removedAt = op.removedAt;
337
+ retro.removedBy = op.removedBy;
338
+ if (op.removedReason)
339
+ retro.removedReason = op.removedReason;
340
+ });
341
+ break;
342
+ }
343
+ case 'tagLesson': {
344
+ applied = await (0, brain_1.applyBrainChange)(op.docId, (doc) => {
345
+ if (!Array.isArray(doc.lessons)) {
346
+ throw new Error(`no lessons list in doc ${op.docId}`);
347
+ }
348
+ const lesson = doc.lessons.find((l) => l && l.id === op.lessonId);
349
+ if (!lesson) {
350
+ throw new Error(`lesson ${op.lessonId} not found in ${op.docId}`);
351
+ }
352
+ if (!Array.isArray(lesson.tags))
353
+ lesson.tags = [];
354
+ for (const t of op.addTags) {
355
+ if (!lesson.tags.includes(t))
356
+ lesson.tags.push(t);
357
+ }
358
+ for (const t of op.removeTags) {
359
+ const idx = lesson.tags.indexOf(t);
360
+ if (idx >= 0)
361
+ lesson.tags.splice(idx, 1);
362
+ }
363
+ }, { allowInvalidShape: op.allowInvalidShape });
364
+ break;
365
+ }
366
+ // --- Brainstorms (task #354 phase b, HB#208) ----------------------
367
+ case 'startBrainstorm': {
368
+ applied = await (0, brain_1.applyBrainChange)(op.docId, (doc) => {
369
+ if (!Array.isArray(doc.brainstorms))
370
+ doc.brainstorms = [];
371
+ if (doc.brainstorms.some((b) => b && b.id === op.brainstormId)) {
372
+ throw new Error(`brainstorm ${op.brainstormId} already exists in ${op.docId}`);
373
+ }
374
+ const entry = {
375
+ id: op.brainstormId,
376
+ title: op.title,
377
+ prompt: op.prompt,
378
+ author: op.author,
379
+ openedAt: op.openedAt,
380
+ status: 'open',
381
+ ideas: [],
382
+ };
383
+ if (op.windowFromHB != null || op.windowToHB != null) {
384
+ entry.window = {};
385
+ if (op.windowFromHB != null)
386
+ entry.window.from = op.windowFromHB;
387
+ if (op.windowToHB != null)
388
+ entry.window.to = op.windowToHB;
389
+ }
390
+ doc.brainstorms.push(entry);
391
+ });
392
+ break;
393
+ }
394
+ case 'respondToBrainstorm': {
395
+ applied = await (0, brain_1.applyBrainChange)(op.docId, (doc) => {
396
+ if (!Array.isArray(doc.brainstorms)) {
397
+ throw new Error(`no brainstorms list in doc ${op.docId}`);
398
+ }
399
+ const brainstorm = doc.brainstorms.find((b) => b && b.id === op.brainstormId);
400
+ if (!brainstorm) {
401
+ throw new Error(`brainstorm ${op.brainstormId} not found in ${op.docId}`);
402
+ }
403
+ if (brainstorm.removed || brainstorm.status === 'closed') {
404
+ throw new Error(`brainstorm ${op.brainstormId} is ${brainstorm.removed ? 'removed' : 'closed'} — cannot respond`);
405
+ }
406
+ if (!Array.isArray(brainstorm.ideas))
407
+ brainstorm.ideas = [];
408
+ // Add a new idea if requested
409
+ if (op.addIdea) {
410
+ if (brainstorm.ideas.some((i) => i && i.id === op.addIdea.id)) {
411
+ throw new Error(`idea ${op.addIdea.id} already exists in brainstorm ${op.brainstormId}`);
412
+ }
413
+ brainstorm.ideas.push({
414
+ id: op.addIdea.id,
415
+ author: op.author,
416
+ message: op.addIdea.message,
417
+ timestamp: op.timestamp,
418
+ votes: {},
419
+ });
420
+ }
421
+ // Merge votes into existing ideas — per-agent slot so concurrent
422
+ // writes from different agents don't overwrite each other's votes
423
+ if (op.votes) {
424
+ for (const [ideaId, stance] of Object.entries(op.votes)) {
425
+ const idea = brainstorm.ideas.find((i) => i && i.id === ideaId);
426
+ if (!idea) {
427
+ throw new Error(`cannot vote on unknown idea ${ideaId} in brainstorm ${op.brainstormId}`);
428
+ }
429
+ if (!idea.votes || typeof idea.votes !== 'object')
430
+ idea.votes = {};
431
+ idea.votes[op.author] = stance;
432
+ }
433
+ }
434
+ // Auto-advance status from open to voting once any vote lands
435
+ if (op.votes && Object.keys(op.votes).length > 0 && brainstorm.status === 'open') {
436
+ brainstorm.status = 'voting';
437
+ }
438
+ // Record the discussion message (separate from ideas — a response
439
+ // can have a message without adding an idea or casting a vote)
440
+ if (op.message) {
441
+ if (!Array.isArray(brainstorm.discussion))
442
+ brainstorm.discussion = [];
443
+ brainstorm.discussion.push({
444
+ author: op.author,
445
+ message: op.message,
446
+ timestamp: op.timestamp,
447
+ });
448
+ }
449
+ });
450
+ break;
451
+ }
452
+ case 'promoteIdea': {
453
+ applied = await (0, brain_1.applyBrainChange)(op.docId, (doc) => {
454
+ if (!Array.isArray(doc.brainstorms)) {
455
+ throw new Error(`no brainstorms list in doc ${op.docId}`);
456
+ }
457
+ const brainstorm = doc.brainstorms.find((b) => b && b.id === op.brainstormId);
458
+ if (!brainstorm) {
459
+ throw new Error(`brainstorm ${op.brainstormId} not found in ${op.docId}`);
460
+ }
461
+ const idea = (brainstorm.ideas || []).find((i) => i && i.id === op.ideaId);
462
+ if (!idea) {
463
+ throw new Error(`idea ${op.ideaId} not found in brainstorm ${op.brainstormId}`);
464
+ }
465
+ if (!Array.isArray(brainstorm.promotedToProjectIds))
466
+ brainstorm.promotedToProjectIds = [];
467
+ if (!brainstorm.promotedToProjectIds.includes(op.promotedProjectId)) {
468
+ brainstorm.promotedToProjectIds.push(op.promotedProjectId);
469
+ }
470
+ // Mark the idea itself as promoted so the projection can show
471
+ // the link visibly
472
+ idea.promotedAt = op.promotedAt;
473
+ idea.promotedBy = op.promotedBy;
474
+ idea.promotedProjectId = op.promotedProjectId;
475
+ // Advance the brainstorm to promoted status if it wasn't already
476
+ if (brainstorm.status !== 'closed' && brainstorm.status !== 'promoted') {
477
+ brainstorm.status = 'promoted';
478
+ }
479
+ });
480
+ break;
481
+ }
482
+ case 'closeBrainstorm': {
483
+ applied = await (0, brain_1.applyBrainChange)(op.docId, (doc) => {
484
+ if (!Array.isArray(doc.brainstorms)) {
485
+ throw new Error(`no brainstorms list in doc ${op.docId}`);
486
+ }
487
+ const brainstorm = doc.brainstorms.find((b) => b && b.id === op.brainstormId);
488
+ if (!brainstorm) {
489
+ throw new Error(`brainstorm ${op.brainstormId} not found in ${op.docId}`);
490
+ }
491
+ brainstorm.status = 'closed';
492
+ brainstorm.closedAt = op.closedAt;
493
+ brainstorm.closedBy = op.closedBy;
494
+ if (op.reason)
495
+ brainstorm.closedReason = op.reason;
496
+ });
497
+ break;
498
+ }
499
+ case 'removeBrainstorm': {
500
+ applied = await (0, brain_1.applyBrainChange)(op.docId, (doc) => {
501
+ if (!Array.isArray(doc.brainstorms)) {
502
+ throw new Error(`no brainstorms list in doc ${op.docId}`);
503
+ }
504
+ const brainstorm = doc.brainstorms.find((b) => b && b.id === op.brainstormId);
505
+ if (!brainstorm) {
506
+ throw new Error(`brainstorm ${op.brainstormId} not found in ${op.docId}`);
507
+ }
508
+ brainstorm.removed = true;
509
+ brainstorm.removedAt = op.removedAt;
510
+ brainstorm.removedBy = op.removedBy;
511
+ if (op.removedReason)
512
+ brainstorm.removedReason = op.removedReason;
513
+ });
514
+ break;
515
+ }
516
+ default: {
517
+ // Exhaustiveness check — TypeScript will complain if a BrainOp variant
518
+ // is missing from the switch.
519
+ const _exhaustive = op;
520
+ throw new Error(`Unknown BrainOp type: ${_exhaustive?.type}`);
521
+ }
522
+ }
523
+ return {
524
+ headCid: applied.headCid,
525
+ envelopeAuthor: applied.author,
526
+ routedViaDaemon: false,
527
+ };
528
+ }
529
+ // ---------------------------------------------------------------------------
530
+ // Routed dispatch — sends to daemon when running, falls back locally
531
+ // ---------------------------------------------------------------------------
532
+ /**
533
+ * Route a BrainOp through a running brain daemon, or run it locally if no
534
+ * daemon is up.
535
+ *
536
+ * Fallback safety: we only fall back on PRE-CONNECT IPC errors (daemon not
537
+ * running, socket missing, connection refused). POST-CONNECT errors leave
538
+ * the write in an unknown state, so we error out and ask the operator to
539
+ * verify. See module header comment for the full rationale.
540
+ */
541
+ async function routedDispatch(op) {
542
+ const daemonPid = (0, brain_daemon_1.getRunningDaemonPid)();
543
+ if (daemonPid === null) {
544
+ // No daemon → run locally. Correct + safe.
545
+ return await dispatchOp(op);
546
+ }
547
+ try {
548
+ const ipcResult = await (0, brain_daemon_1.sendIpcRequest)('applyOp', { op }, 15_000);
549
+ return {
550
+ headCid: ipcResult.headCid,
551
+ envelopeAuthor: ipcResult.envelopeAuthor,
552
+ routedViaDaemon: true,
553
+ };
554
+ }
555
+ catch (err) {
556
+ // BrainIpcError carries a `.phase` field: 'pre-connect' means the
557
+ // connection was never established (safe to fall back); 'post-connect'
558
+ // means the write may or may not have landed (ambiguous — do NOT fall
559
+ // back, surface to operator).
560
+ const isPreConnect = err instanceof brain_daemon_1.BrainIpcError
561
+ ? err.phase === 'pre-connect'
562
+ : (err?.code === 'ECONNREFUSED' || err?.code === 'ENOENT');
563
+ if (isPreConnect) {
564
+ return await dispatchOp(op);
565
+ }
566
+ throw new Error(`Brain IPC failed mid-call (PID ${daemonPid}): ${err?.message ?? err}. ` +
567
+ `The write may or may not have landed in the daemon. ` +
568
+ `Verify with: pop brain read --doc ${op.docId} — and then either ` +
569
+ `retry the command or stop the daemon (pop brain daemon stop) before retrying.`);
570
+ }
571
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Where the REPO-TRACKED brain lives.
3
+ *
4
+ * The package split moved it from <repo>/agent/brain to
5
+ * <repo>/packages/agent/brain. Three call sites used to build the old path
6
+ * from process.cwd(), and after the move they all failed SILENTLY: genesis
7
+ * seeding never happened (regressing the task #352 fix), the committed signer
8
+ * allowlist stopped loading, and `pop brain snapshot` grew a fresh untracked
9
+ * agent/ tree at the repo root instead of updating the tracked projection.
10
+ *
11
+ * Resolve relative to THIS package first (dist/lib → ../../brain), then fall
12
+ * back to cwd-based layouts so an agent invoking the command from an older
13
+ * checkout, or from outside the repo, still finds a brain if one exists.
14
+ */
15
+ export declare function getRepoBrainRoot(): string;
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ /**
3
+ * Where the REPO-TRACKED brain lives.
4
+ *
5
+ * The package split moved it from <repo>/agent/brain to
6
+ * <repo>/packages/agent/brain. Three call sites used to build the old path
7
+ * from process.cwd(), and after the move they all failed SILENTLY: genesis
8
+ * seeding never happened (regressing the task #352 fix), the committed signer
9
+ * allowlist stopped loading, and `pop brain snapshot` grew a fresh untracked
10
+ * agent/ tree at the repo root instead of updating the tracked projection.
11
+ *
12
+ * Resolve relative to THIS package first (dist/lib → ../../brain), then fall
13
+ * back to cwd-based layouts so an agent invoking the command from an older
14
+ * checkout, or from outside the repo, still finds a brain if one exists.
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.getRepoBrainRoot = getRepoBrainRoot;
18
+ const path_1 = require("path");
19
+ const fs_1 = require("fs");
20
+ function getRepoBrainRoot() {
21
+ const candidates = [
22
+ (0, path_1.resolve)(__dirname, '..', '..', 'brain'), // dist/lib → packages/agent/brain
23
+ (0, path_1.join)(process.cwd(), 'packages', 'agent', 'brain'), // repo root as cwd
24
+ (0, path_1.join)(process.cwd(), 'agent', 'brain'), // pre-split layout
25
+ ];
26
+ for (const c of candidates) {
27
+ if ((0, fs_1.existsSync)(c))
28
+ return c;
29
+ }
30
+ // Nothing exists yet (fresh checkout mid-build): prefer the package-rooted
31
+ // path so anything created lands in the TRACKED location.
32
+ return candidates[0];
33
+ }