@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,328 @@
1
+ "use strict";
2
+ /**
3
+ * Brain migration — one-shot parser that converts the hand-written
4
+ * agent/brain/Knowledge/shared.md (and siblings) into a structured
5
+ * SharedBrainDoc for seeding the Automerge CRDT layer.
6
+ *
7
+ * This is step 8 of the brain plan (cheeky-nibbling-raven.md).
8
+ * After this file has been run once on each hand-written knowledge
9
+ * file, the CRDT substrate is the source of truth and the hand-written
10
+ * files become historical snapshots.
11
+ *
12
+ * ## Design notes
13
+ *
14
+ * - **Pure function.** parseSharedMarkdown takes a raw string and
15
+ * returns a SharedBrainDoc. No I/O, no Date.now, no environment
16
+ * reads. The CLI command handles all I/O and timestamp-from-now
17
+ * decisions.
18
+ *
19
+ * - **Heuristic, not canonical.** This is a one-shot parse; we're
20
+ * allowed to lose fidelity on layout details as long as the
21
+ * content content survives. Each H2 section becomes one entry.
22
+ *
23
+ * - **Short sections become rules, long sections become lessons.**
24
+ * A rule is a short-form policy item (one to a few lines of plain
25
+ * text, often bullet-led). A lesson is a longer structured
26
+ * narrative. The heuristic: if a section's body has more than 200
27
+ * characters OR contains a code fence, it's a lesson; otherwise
28
+ * it's a rule. This maps the existing hand-written file's mix of
29
+ * short policy bullets and long narrative notes to the brain doc
30
+ * schema cleanly.
31
+ *
32
+ * - **HB# tags → timestamps.** Lines containing `HB#N` or `(HB#N,
33
+ * author)` extract both the heartbeat number and the author.
34
+ * Timestamps are derived from HB# via a caller-provided anchor
35
+ * (see MigrationContext below) rather than guessed — keeps the
36
+ * parser pure.
37
+ *
38
+ * - **Code fences are preserved verbatim.** We do NOT split on ##
39
+ * headers inside a ``` block — that would corrupt embedded code
40
+ * samples. Tracked via a simple in-fence toggle.
41
+ */
42
+ Object.defineProperty(exports, "__esModule", { value: true });
43
+ exports.parseSharedMarkdown = parseSharedMarkdown;
44
+ const HB_TAG_RE = /\(\s*HB#(\d+)(?:\s*,\s*([^\s)]+))?\s*\)/i;
45
+ const BARE_HB_RE = /\bHB#(\d+)\b/i;
46
+ /**
47
+ * Slugify a string for use as an entry id. Keeps lowercase letters,
48
+ * digits, and hyphens; collapses other characters to hyphens.
49
+ */
50
+ function slugify(s) {
51
+ return s
52
+ .toLowerCase()
53
+ .replace(/[^a-z0-9]+/g, '-')
54
+ .replace(/^-+|-+$/g, '')
55
+ .slice(0, 60);
56
+ }
57
+ /**
58
+ * Split raw markdown into sections delimited by `^## ` headers at
59
+ * the top level. Returns an ordered list of { header, body } pairs.
60
+ * Skips content before the first H2 (preamble) entirely — it's
61
+ * usually the file's title and intro paragraph.
62
+ *
63
+ * Code fences are respected: `##` inside a ``` block is treated as
64
+ * literal content, not a section break. This matters because the
65
+ * hand-written shared.md embeds shell command examples with
66
+ * triple-backtick fences, and one of those happens to include a
67
+ * markdown header in a commented line.
68
+ */
69
+ function splitSections(raw) {
70
+ const lines = raw.split(/\r?\n/);
71
+ const sections = [];
72
+ let inFence = false;
73
+ let current = null;
74
+ for (const line of lines) {
75
+ // Track code fence state so we don't treat `##` inside code as
76
+ // a new section.
77
+ if (/^```/.test(line)) {
78
+ inFence = !inFence;
79
+ if (current)
80
+ current.body.push(line);
81
+ continue;
82
+ }
83
+ if (!inFence && /^##\s+/.test(line)) {
84
+ if (current)
85
+ sections.push({ header: current.header, body: current.body.join('\n').trim() });
86
+ current = { header: line.replace(/^##\s+/, '').trim(), body: [] };
87
+ continue;
88
+ }
89
+ if (current)
90
+ current.body.push(line);
91
+ // lines before the first H2 are dropped (preamble)
92
+ }
93
+ if (current)
94
+ sections.push({ header: current.header, body: current.body.join('\n').trim() });
95
+ return sections;
96
+ }
97
+ /**
98
+ * Extract an HB#/author tag from a block of text. Returns the first
99
+ * match found (usually the header or the first body line). Returns
100
+ * null if nothing matches.
101
+ */
102
+ function extractHBTag(text) {
103
+ const m = HB_TAG_RE.exec(text);
104
+ if (m)
105
+ return { hb: Number(m[1]), author: m[2] ?? null };
106
+ const bare = BARE_HB_RE.exec(text);
107
+ if (bare)
108
+ return { hb: Number(bare[1]), author: null };
109
+ return null;
110
+ }
111
+ /**
112
+ * Detect whether the input is a modern `.generated.md` snapshot produced by
113
+ * `pop brain snapshot` (task #352+), not a hand-written `shared.md`. The
114
+ * discriminator is the DO-NOT-HAND-EDIT banner that the projector emits.
115
+ *
116
+ * Task #357 (HB#358): before this detection existed, parseSharedMarkdown
117
+ * tried the legacy H2 scanner against the generated.md format and found
118
+ * only 6 of 59 lessons — matching H2 headers like `## Lessons`,
119
+ * `## What worked this session`, etc., as lesson titles. This is a
120
+ * permanent format mismatch, not a parse bug; the solution is a
121
+ * different parser for each format, dispatched by banner detection.
122
+ */
123
+ function isModernGeneratedMd(raw) {
124
+ return /^# GENERATED BY `pop brain snapshot`/m.test(raw.slice(0, 500));
125
+ }
126
+ /**
127
+ * Parse a modern `.generated.md` snapshot (task #357 / HB#358).
128
+ *
129
+ * Structure produced by brain-projections.ts `projectShared`:
130
+ *
131
+ * # GENERATED BY `pop brain snapshot` — DO NOT HAND-EDIT
132
+ * <!-- ... -->
133
+ *
134
+ * # Shared Agent Brain — `pop.brain.shared`
135
+ * *Head CID: `bafk...`*
136
+ *
137
+ * ## Lessons
138
+ *
139
+ * ### <lesson title> [possibly containing (HB#N, author)]
140
+ * *author: <name> · at: <ISO timestamp> · id: <lesson-id>*
141
+ *
142
+ * <body paragraphs — may contain code fences AND `##` subheaders>
143
+ *
144
+ * ---
145
+ *
146
+ * ### <next lesson title>
147
+ * ...
148
+ *
149
+ * ## Removed lessons
150
+ * ...
151
+ *
152
+ * Rules for this parser:
153
+ * 1. Only consider content after the `## Lessons` header
154
+ * 2. A lesson starts at `### <title>` (H3)
155
+ * 3. The line immediately after (if matching `*author: ... · at: ... · id: ...*`)
156
+ * provides metadata
157
+ * 4. Body = everything until the NEXT `### <title>` OR one of the known
158
+ * terminator H2 headers (`## Removed lessons`, `## Other fields`)
159
+ * 5. `##` subheaders INSIDE a lesson body (e.g. `## What worked` inside a
160
+ * retro-style lesson) are content, not terminators
161
+ * 6. `---` horizontal rules between lessons are cosmetic and dropped
162
+ * 7. Code fences are respected: `###` inside ``` is literal content
163
+ *
164
+ * Every modern lesson is parsed as a lesson (not a rule). Rules only exist
165
+ * in the legacy hand-written format and aren't produced by the modern
166
+ * projector anymore.
167
+ */
168
+ function parseModernGeneratedMd(raw, ctx) {
169
+ const lessons = [];
170
+ const lines = raw.split(/\r?\n/);
171
+ // Find the `## Lessons` header. Everything before is banner + doc title.
172
+ let startIdx = -1;
173
+ for (let i = 0; i < lines.length; i++) {
174
+ if (/^##\s+Lessons\s*$/.test(lines[i])) {
175
+ startIdx = i + 1;
176
+ break;
177
+ }
178
+ }
179
+ if (startIdx === -1) {
180
+ // No lessons section — return empty. This is a valid case for a
181
+ // fresh generated.md before any lesson has been written.
182
+ return { rules: [], lessons: [] };
183
+ }
184
+ // Known terminator H2 headers that end the Lessons section.
185
+ const TERMINATORS = [
186
+ /^##\s+Removed lessons\s*$/,
187
+ /^##\s+Other fields\s*$/,
188
+ ];
189
+ const isTerminator = (line) => TERMINATORS.some(re => re.test(line));
190
+ // Metadata line format: `*author: X · at: ISO-TIMESTAMP · id: lesson-id*`
191
+ // Each field is separated by ` · ` (middle dot with spaces).
192
+ const METADATA_RE = /^\*author:\s*([^·]+?)\s*·\s*at:\s*([^·]+?)\s*·\s*id:\s*([^*\s]+)\s*\*?$/;
193
+ let inFence = false;
194
+ let pending = null;
195
+ const commitPending = () => {
196
+ if (!pending)
197
+ return;
198
+ // Title may contain HB#N tag. Extract for timestamp resolution and
199
+ // also strip from the title for cleanliness.
200
+ const tag = extractHBTag(pending.title);
201
+ // Prefer the metadata author field; fall back to HB# tag author;
202
+ // fall back to default.
203
+ const author = pending.author ?? tag?.author ?? ctx.defaultAuthor;
204
+ // Prefer the metadata `at:` ISO timestamp (parse to unix seconds).
205
+ // Fall back to HB-derived timestamp, then default.
206
+ let timestamp = ctx.defaultTimestamp;
207
+ if (pending.at) {
208
+ const parsed = Date.parse(pending.at);
209
+ if (Number.isFinite(parsed)) {
210
+ timestamp = Math.floor(parsed / 1000);
211
+ }
212
+ }
213
+ else if (tag && ctx.timestampForHB) {
214
+ timestamp = ctx.timestampForHB(tag.hb);
215
+ }
216
+ // Prefer the metadata `id:` field; fall back to slugified title.
217
+ const id = pending.id ?? slugify(pending.title) ?? `lesson-${lessons.length + 1}`;
218
+ // Join the body and trim trailing whitespace / trailing `---` separator.
219
+ const body = pending.bodyLines
220
+ .join('\n')
221
+ .replace(/\n+---\n*$/, '') // trim trailing horizontal rule
222
+ .trim();
223
+ if (body.length === 0 && !pending.id && !pending.author) {
224
+ // Empty lesson with no metadata — likely a parse artifact, skip.
225
+ pending = null;
226
+ return;
227
+ }
228
+ lessons.push({ id, title: pending.title, author, body, timestamp });
229
+ pending = null;
230
+ };
231
+ for (let i = startIdx; i < lines.length; i++) {
232
+ const line = lines[i];
233
+ // Track code fence state — `###` inside ``` is literal content.
234
+ if (/^```/.test(line)) {
235
+ inFence = !inFence;
236
+ if (pending)
237
+ pending.bodyLines.push(line);
238
+ continue;
239
+ }
240
+ if (!inFence) {
241
+ // Terminator: end of Lessons section. Commit + stop.
242
+ if (isTerminator(line)) {
243
+ commitPending();
244
+ break;
245
+ }
246
+ // New H3 = new lesson.
247
+ const h3Match = /^###\s+(.+?)\s*$/.exec(line);
248
+ if (h3Match) {
249
+ commitPending();
250
+ pending = {
251
+ title: h3Match[1],
252
+ author: null,
253
+ at: null,
254
+ id: null,
255
+ bodyLines: [],
256
+ };
257
+ continue;
258
+ }
259
+ // Metadata line immediately after the H3 (pending has no body yet).
260
+ if (pending && pending.bodyLines.length === 0 && pending.id === null) {
261
+ const metaMatch = METADATA_RE.exec(line);
262
+ if (metaMatch) {
263
+ pending.author = metaMatch[1];
264
+ pending.at = metaMatch[2];
265
+ pending.id = metaMatch[3];
266
+ continue;
267
+ }
268
+ }
269
+ }
270
+ // Content line — append to the pending lesson body.
271
+ if (pending)
272
+ pending.bodyLines.push(line);
273
+ }
274
+ // Commit the final lesson if we hit EOF without a terminator.
275
+ commitPending();
276
+ return { rules: [], lessons };
277
+ }
278
+ /**
279
+ * Parse the hand-written shared.md (or similar) into a SharedBrainDoc.
280
+ *
281
+ * Dispatches to `parseModernGeneratedMd` for modern snapshots produced by
282
+ * `pop brain snapshot` (detected via the DO-NOT-HAND-EDIT banner), or to
283
+ * the legacy H2-section scanner for hand-written shared.md files.
284
+ *
285
+ * Pure function — no I/O, no Date.now, no env reads. The caller must
286
+ * supply the migration context (default author, default timestamp,
287
+ * optional HB→timestamp function).
288
+ */
289
+ function parseSharedMarkdown(raw, ctx) {
290
+ // Task #357 dispatch: modern generated.md vs legacy hand-written.
291
+ if (isModernGeneratedMd(raw)) {
292
+ return parseModernGeneratedMd(raw, ctx);
293
+ }
294
+ const sections = splitSections(raw);
295
+ const rules = [];
296
+ const lessons = [];
297
+ for (const { header, body } of sections) {
298
+ if (body.length === 0)
299
+ continue;
300
+ const combinedForTagHunt = `${header}\n${body.split('\n').slice(0, 2).join('\n')}`;
301
+ const tag = extractHBTag(combinedForTagHunt);
302
+ const author = tag?.author ?? ctx.defaultAuthor;
303
+ const timestamp = tag && ctx.timestampForHB
304
+ ? ctx.timestampForHB(tag.hb)
305
+ : ctx.defaultTimestamp;
306
+ const hasCodeFence = /^```/m.test(body);
307
+ const isLesson = body.length > 200 || hasCodeFence;
308
+ const id = slugify(header) || `section-${lessons.length + rules.length + 1}`;
309
+ if (isLesson) {
310
+ lessons.push({
311
+ id,
312
+ title: header,
313
+ author,
314
+ body,
315
+ timestamp,
316
+ });
317
+ }
318
+ else {
319
+ rules.push({
320
+ id,
321
+ author,
322
+ text: body,
323
+ timestamp,
324
+ });
325
+ }
326
+ }
327
+ return { rules, lessons };
328
+ }
@@ -0,0 +1,271 @@
1
+ /**
2
+ * Unified brain write dispatcher (HB#324 principal-engineer ship-2).
3
+ *
4
+ * ## Why this module exists
5
+ *
6
+ * Before this module, every brain write command (append-lesson, edit-lesson,
7
+ * remove-lesson, new-project, advance-stage, remove-project, …) called
8
+ * `applyBrainChange` directly with a closure. That works fine in-process but
9
+ * cannot be routed through IPC to a running daemon — closures don't serialize.
10
+ *
11
+ * The brain daemon needs to be the single owner of libp2p/gossipsub when it's
12
+ * running (HB#312 dogfood wedge proved sequential agent sessions never overlap
13
+ * in wall-clock time, so a short-lived in-process libp2p can't reliably
14
+ * deliver a gossipsub announcement before exiting). For the daemon to be
15
+ * useful, every write has to route through it. For the daemon to be usable,
16
+ * the CLI commands need a transparent fallback when no daemon is running.
17
+ *
18
+ * The solution is to turn every brain write into a pure-data operation
19
+ * descriptor (`BrainOp`) and have two execution paths that share zero
20
+ * business logic:
21
+ *
22
+ * `dispatchOp(op)` Runs the op in the current process. Used by:
23
+ * (a) the daemon's IPC handler, and
24
+ * (b) CLI commands as a fallback when no daemon.
25
+ * `routedDispatch(op)` Entry point for CLI commands. Checks for a running
26
+ * daemon: if yes, sends `applyOp` via IPC; if no,
27
+ * calls dispatchOp locally.
28
+ *
29
+ * Both paths converge on the same `dispatchOp` function. There is no
30
+ * "local" vs "routed" business logic — only a transport decision.
31
+ *
32
+ * ## Fallback correctness (important)
33
+ *
34
+ * `routedDispatch` must be careful about when it falls back to local
35
+ * dispatch. If the daemon has already processed the write and only the
36
+ * *response* got lost, a silent local fallback causes a double-write.
37
+ *
38
+ * The rule:
39
+ * - Pre-connect IPC failure (ECONNREFUSED, ENOENT, daemon not running at
40
+ * all) → SAFE to fall back. The write definitely did not land in the
41
+ * daemon's process because we never opened the connection.
42
+ * - Post-connect IPC failure (ECONNRESET, EPIPE, timeout mid-call) → NOT
43
+ * safe. The daemon may have processed the write. Error out and let the
44
+ * operator decide.
45
+ *
46
+ * This is the opposite of retry-everything semantics, on purpose. Brain
47
+ * writes are append-mostly and deduplication is hard once the block lands.
48
+ *
49
+ * ## Adding a new op
50
+ *
51
+ * 1. Add the variant to the `BrainOp` discriminated union below.
52
+ * 2. Add the case to `dispatchOp`'s switch statement.
53
+ * 3. Replace the `applyBrainChange(doc, closure)` call in your command
54
+ * with `routedDispatch({type: 'yourOp', ...})`.
55
+ *
56
+ * The daemon's IPC handler is a one-line `return dispatchOp(params.op)` —
57
+ * it does not need to be updated for new ops.
58
+ */
59
+ export interface AppendLessonOp {
60
+ type: 'appendLesson';
61
+ docId: string;
62
+ id: string;
63
+ title: string;
64
+ body: string;
65
+ author: string;
66
+ timestamp: number;
67
+ /** Task #346: bypass write-time schema validation. Default false (strict). */
68
+ allowInvalidShape?: boolean;
69
+ }
70
+ export interface EditLessonOp {
71
+ type: 'editLesson';
72
+ docId: string;
73
+ lessonId: string;
74
+ fields: {
75
+ title?: string;
76
+ body?: string;
77
+ author?: string;
78
+ };
79
+ /** Bump the lesson's timestamp to now(), even when no fields change. */
80
+ touch: boolean;
81
+ /** Task #346: bypass write-time schema validation. Default false (strict). */
82
+ allowInvalidShape?: boolean;
83
+ }
84
+ export interface RemoveLessonOp {
85
+ type: 'removeLesson';
86
+ docId: string;
87
+ lessonId: string;
88
+ removedBy: string;
89
+ removedAt: number;
90
+ removedReason?: string;
91
+ /** Task #346: bypass write-time schema validation. Default false (strict). */
92
+ allowInvalidShape?: boolean;
93
+ }
94
+ export interface NewProjectOp {
95
+ type: 'newProject';
96
+ docId: string;
97
+ projectId: string;
98
+ /** Display name — matches the `name` field on the stored project object. */
99
+ name: string;
100
+ /** Optional short-form brief; omitted field when undefined. */
101
+ brief?: string;
102
+ /** Starting lifecycle stage (propose / discuss / plan / vote / execute / review / ship). */
103
+ stage: string;
104
+ /** Proposer label — default is lowercased wallet address. */
105
+ proposedBy: string;
106
+ /** Unix seconds — recorded as `proposedAt` on the stored project. */
107
+ proposedAt: number;
108
+ }
109
+ export interface AdvanceStageOp {
110
+ type: 'advanceStage';
111
+ docId: string;
112
+ projectId: string;
113
+ /** New lifecycle stage. */
114
+ newStage: string;
115
+ /** Unix seconds — recorded as `lastStageAdvanceAt` on the project. */
116
+ lastStageAdvanceAt: number;
117
+ }
118
+ export interface RemoveProjectOp {
119
+ type: 'removeProject';
120
+ docId: string;
121
+ projectId: string;
122
+ removedBy: string;
123
+ removedAt: number;
124
+ removedReason?: string;
125
+ }
126
+ export interface RetroChangeInput {
127
+ id: string;
128
+ summary: string;
129
+ details?: string;
130
+ status?: 'proposed' | 'agreed' | 'modified' | 'rejected' | 'filed';
131
+ }
132
+ export interface StartRetroOp {
133
+ type: 'startRetro';
134
+ docId: string;
135
+ retroId: string;
136
+ author: string;
137
+ /** HB number when the retro was started. */
138
+ hb: number;
139
+ window: {
140
+ from: number;
141
+ to: number;
142
+ };
143
+ observations: {
144
+ worked?: string;
145
+ didntWork?: string;
146
+ };
147
+ proposedChanges: RetroChangeInput[];
148
+ createdAt: number;
149
+ }
150
+ export interface RespondToRetroOp {
151
+ type: 'respondToRetro';
152
+ docId: string;
153
+ retroId: string;
154
+ author: string;
155
+ hb?: number;
156
+ message: string;
157
+ votePerChange?: Record<string, 'agree' | 'modify' | 'reject'>;
158
+ timestamp: number;
159
+ }
160
+ export interface UpdateChangeStatusOp {
161
+ type: 'updateChangeStatus';
162
+ docId: string;
163
+ retroId: string;
164
+ changeId: string;
165
+ newStatus: 'proposed' | 'agreed' | 'modified' | 'rejected' | 'filed';
166
+ filedTaskId?: string;
167
+ }
168
+ export interface RemoveRetroOp {
169
+ type: 'removeRetro';
170
+ docId: string;
171
+ retroId: string;
172
+ removedBy: string;
173
+ removedAt: number;
174
+ removedReason?: string;
175
+ }
176
+ export interface TagLessonOp {
177
+ type: 'tagLesson';
178
+ docId: string;
179
+ lessonId: string;
180
+ addTags: string[];
181
+ removeTags: string[];
182
+ /** Task #346: bypass write-time schema validation. Default false (strict). */
183
+ allowInvalidShape?: boolean;
184
+ }
185
+ export interface StartBrainstormOp {
186
+ type: 'startBrainstorm';
187
+ docId: string;
188
+ brainstormId: string;
189
+ title: string;
190
+ prompt: string;
191
+ author: string;
192
+ openedAt: number;
193
+ windowFromHB?: number;
194
+ windowToHB?: number;
195
+ }
196
+ export interface RespondToBrainstormOp {
197
+ type: 'respondToBrainstorm';
198
+ docId: string;
199
+ brainstormId: string;
200
+ author: string;
201
+ /** Message posted by the responding agent. Plain string, no schema. */
202
+ message?: string;
203
+ /** New idea to add to the brainstorm's ideas list. */
204
+ addIdea?: {
205
+ id: string;
206
+ message: string;
207
+ };
208
+ /** Map of idea id → stance (support/explore/oppose). Merges into existing votes without overwriting other agents' votes. */
209
+ votes?: Record<string, 'support' | 'explore' | 'oppose'>;
210
+ timestamp: number;
211
+ }
212
+ export interface PromoteIdeaOp {
213
+ type: 'promoteIdea';
214
+ docId: string;
215
+ brainstormId: string;
216
+ ideaId: string;
217
+ /** The pop.brain.projects id to link to after promotion. Caller creates the project separately via newProject; this op just records the back-reference. */
218
+ promotedProjectId: string;
219
+ promotedBy: string;
220
+ promotedAt: number;
221
+ }
222
+ export interface CloseBrainstormOp {
223
+ type: 'closeBrainstorm';
224
+ docId: string;
225
+ brainstormId: string;
226
+ closedBy: string;
227
+ closedAt: number;
228
+ /** Free-form reason for the close. */
229
+ reason?: string;
230
+ }
231
+ export interface RemoveBrainstormOp {
232
+ type: 'removeBrainstorm';
233
+ docId: string;
234
+ brainstormId: string;
235
+ removedBy: string;
236
+ removedAt: number;
237
+ removedReason?: string;
238
+ }
239
+ export type BrainOp = AppendLessonOp | EditLessonOp | RemoveLessonOp | NewProjectOp | AdvanceStageOp | RemoveProjectOp | StartRetroOp | RespondToRetroOp | UpdateChangeStatusOp | RemoveRetroOp | TagLessonOp | StartBrainstormOp | RespondToBrainstormOp | PromoteIdeaOp | CloseBrainstormOp | RemoveBrainstormOp;
240
+ export interface DispatchResult {
241
+ headCid: string;
242
+ /** The Ethereum address that signed the envelope (from POP_PRIVATE_KEY). */
243
+ envelopeAuthor: string;
244
+ /** True iff the op was routed through a running brain daemon. */
245
+ routedViaDaemon: boolean;
246
+ }
247
+ /**
248
+ * Apply a BrainOp in the current process. Translates the op descriptor to
249
+ * the corresponding Automerge change function and calls `applyBrainChange`,
250
+ * which signs the envelope, writes the block, updates the manifest, and
251
+ * publishes the new head CID via gossipsub.
252
+ *
253
+ * Same function is called from two places:
254
+ * - The daemon's `applyOp` IPC handler (when a CLI routed a write)
255
+ * - The CLI's `routedDispatch` fallback (when no daemon is running)
256
+ *
257
+ * Errors thrown from the change function propagate out as normal Error
258
+ * instances. For example, editLesson throws if the target lesson id is
259
+ * missing.
260
+ */
261
+ export declare function dispatchOp(op: BrainOp): Promise<DispatchResult>;
262
+ /**
263
+ * Route a BrainOp through a running brain daemon, or run it locally if no
264
+ * daemon is up.
265
+ *
266
+ * Fallback safety: we only fall back on PRE-CONNECT IPC errors (daemon not
267
+ * running, socket missing, connection refused). POST-CONNECT errors leave
268
+ * the write in an unknown state, so we error out and ask the operator to
269
+ * verify. See module header comment for the full rationale.
270
+ */
271
+ export declare function routedDispatch(op: BrainOp): Promise<DispatchResult>;