@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,497 @@
1
+ "use strict";
2
+ /**
3
+ * pop brain doctor — single-command health check for brain layer setup.
4
+ *
5
+ * Answers "is my brain setup healthy?" in one invocation. Complements
6
+ * `docs/agents/brain-layer-setup.md` (setup doc tells you HOW; doctor tells
7
+ * you IF the setup is actually working).
8
+ *
9
+ * Output shape:
10
+ * ✓ check name — pass
11
+ * ⚠ check name — warn (not fatal, but worth knowing)
12
+ * ✗ check name — fail (exit 1)
13
+ *
14
+ * Exit codes:
15
+ * 0 — all pass or pass+warn (warnings don't fail the run)
16
+ * 1 — any ✗ fail
17
+ *
18
+ * --json returns { checks: [{name, status, detail, elapsed?}], summary }
19
+ *
20
+ * Design: every check is self-contained and independent. A failure in
21
+ * one check does NOT stop later checks from running — the operator
22
+ * wants the full picture, not the first stumble.
23
+ */
24
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
25
+ if (k2 === undefined) k2 = k;
26
+ var desc = Object.getOwnPropertyDescriptor(m, k);
27
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
28
+ desc = { enumerable: true, get: function() { return m[k]; } };
29
+ }
30
+ Object.defineProperty(o, k2, desc);
31
+ }) : (function(o, m, k, k2) {
32
+ if (k2 === undefined) k2 = k;
33
+ o[k2] = m[k];
34
+ }));
35
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
36
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
37
+ }) : function(o, v) {
38
+ o["default"] = v;
39
+ });
40
+ var __importStar = (this && this.__importStar) || (function () {
41
+ var ownKeys = function(o) {
42
+ ownKeys = Object.getOwnPropertyNames || function (o) {
43
+ var ar = [];
44
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
45
+ return ar;
46
+ };
47
+ return ownKeys(o);
48
+ };
49
+ return function (mod) {
50
+ if (mod && mod.__esModule) return mod;
51
+ var result = {};
52
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
53
+ __setModuleDefault(result, mod);
54
+ return result;
55
+ };
56
+ })();
57
+ Object.defineProperty(exports, "__esModule", { value: true });
58
+ exports.doctorHandler = void 0;
59
+ const fs_1 = require("fs");
60
+ const path_1 = require("path");
61
+ const ethers_1 = require("ethers");
62
+ const brain_1 = require("../../lib/brain");
63
+ const brain_signing_1 = require("../../lib/brain-signing");
64
+ const brain_daemon_1 = require("../../lib/brain-daemon");
65
+ const output = __importStar(require("@poa-box/cli/lib/output"));
66
+ const ADDRESS_RE = /^0x[0-9a-fA-F]{64}$/;
67
+ async function checkPrivateKey() {
68
+ const key = process.env.POP_PRIVATE_KEY;
69
+ if (!key) {
70
+ return {
71
+ name: 'env: POP_PRIVATE_KEY',
72
+ status: 'fail',
73
+ detail: 'not set — export POP_PRIVATE_KEY or source ~/.pop-agent/.env',
74
+ };
75
+ }
76
+ if (!ADDRESS_RE.test(key)) {
77
+ return {
78
+ name: 'env: POP_PRIVATE_KEY',
79
+ status: 'fail',
80
+ detail: `malformed — must be 0x-prefixed 64-char hex string (got ${key.length} chars)`,
81
+ };
82
+ }
83
+ try {
84
+ const addr = new ethers_1.ethers.Wallet(key).address.toLowerCase();
85
+ return {
86
+ name: 'env: POP_PRIVATE_KEY',
87
+ status: 'pass',
88
+ detail: `signs as ${addr}`,
89
+ };
90
+ }
91
+ catch (err) {
92
+ return {
93
+ name: 'env: POP_PRIVATE_KEY',
94
+ status: 'fail',
95
+ detail: `invalid key — ${err.message}`,
96
+ };
97
+ }
98
+ }
99
+ function checkBrainHome() {
100
+ const home = (0, brain_1.getBrainHome)();
101
+ try {
102
+ (0, fs_1.accessSync)(home, fs_1.constants.W_OK);
103
+ const source = process.env.POP_BRAIN_HOME ? 'override' : 'default';
104
+ return {
105
+ name: 'brain home',
106
+ status: 'pass',
107
+ detail: `${home} (${source}, writable)`,
108
+ };
109
+ }
110
+ catch {
111
+ return {
112
+ name: 'brain home',
113
+ status: 'fail',
114
+ detail: `${home} is not writable`,
115
+ };
116
+ }
117
+ }
118
+ async function checkPeerKey() {
119
+ const path = (0, path_1.join)((0, brain_1.getBrainHome)(), 'peer-key.json');
120
+ if (!(0, fs_1.existsSync)(path)) {
121
+ return {
122
+ name: 'peer-key.json',
123
+ status: 'info',
124
+ detail: 'does not exist yet (will be created on next initBrainNode)',
125
+ };
126
+ }
127
+ try {
128
+ const raw = JSON.parse((0, fs_1.readFileSync)(path, 'utf8'));
129
+ if (typeof raw?.privateKey !== 'string' || !raw.privateKey.startsWith('0x')) {
130
+ return {
131
+ name: 'peer-key.json',
132
+ status: 'warn',
133
+ detail: 'malformed — missing hex privateKey field; delete to regenerate',
134
+ };
135
+ }
136
+ const { privateKeyFromProtobuf } = await new Function('s', 'return import(s)')('@libp2p/crypto/keys');
137
+ const bytes = Uint8Array.from(Buffer.from(raw.privateKey.slice(2), 'hex'));
138
+ const pk = privateKeyFromProtobuf(bytes);
139
+ return {
140
+ name: 'peer-key.json',
141
+ status: 'pass',
142
+ detail: `persisted format OK (${pk.type ?? 'key'}, round-trips through privateKeyFromProtobuf)`,
143
+ };
144
+ }
145
+ catch (err) {
146
+ return {
147
+ name: 'peer-key.json',
148
+ status: 'warn',
149
+ detail: `unreadable (${err.message}) — delete to regenerate on next boot`,
150
+ };
151
+ }
152
+ }
153
+ async function checkLibp2pInit() {
154
+ const start = Date.now();
155
+ try {
156
+ const node = await (0, brain_1.initBrainNode)();
157
+ const peerId = node.libp2p.peerId.toString();
158
+ const elapsed = Date.now() - start;
159
+ return {
160
+ result: {
161
+ name: 'libp2p init',
162
+ status: 'pass',
163
+ detail: `peer ${peerId.slice(0, 16)}...${peerId.slice(-8)}`,
164
+ elapsed,
165
+ },
166
+ node,
167
+ };
168
+ }
169
+ catch (err) {
170
+ return {
171
+ result: {
172
+ name: 'libp2p init',
173
+ status: 'fail',
174
+ detail: `createLibp2p failed — ${err.message}`,
175
+ elapsed: Date.now() - start,
176
+ },
177
+ node: null,
178
+ };
179
+ }
180
+ }
181
+ async function checkBootstrap(node) {
182
+ if (!node) {
183
+ return {
184
+ name: 'bootstrap peers',
185
+ status: 'info',
186
+ detail: 'skipped (libp2p init failed)',
187
+ };
188
+ }
189
+ // Wait up to 15 seconds for the peer store to contain at least one
190
+ // bootstrap peer. Polls every 1s so we exit as soon as discovery
191
+ // succeeds rather than always waiting the full 15.
192
+ const start = Date.now();
193
+ const deadline = start + 15_000;
194
+ let found = 0;
195
+ while (Date.now() < deadline) {
196
+ try {
197
+ const peers = await node.libp2p.peerStore.all();
198
+ found = peers.length;
199
+ if (found > 0)
200
+ break;
201
+ }
202
+ catch {
203
+ // ignore transient peer store errors
204
+ }
205
+ await new Promise((r) => setTimeout(r, 1000));
206
+ }
207
+ const elapsed = Date.now() - start;
208
+ if (found === 0) {
209
+ return {
210
+ name: 'bootstrap peers',
211
+ status: 'warn',
212
+ detail: `0 peers after ${Math.round(elapsed / 1000)}s — DNS may be flaky, local reads still work`,
213
+ elapsed,
214
+ };
215
+ }
216
+ return {
217
+ name: 'bootstrap peers',
218
+ status: 'pass',
219
+ detail: `${found} peer${found === 1 ? '' : 's'} in store after ${Math.round(elapsed / 1000)}s`,
220
+ elapsed,
221
+ };
222
+ }
223
+ function checkAllowlist() {
224
+ const key = process.env.POP_PRIVATE_KEY;
225
+ if (!key || !ADDRESS_RE.test(key)) {
226
+ return {
227
+ name: 'allowlist signer',
228
+ status: 'info',
229
+ detail: 'skipped (no valid POP_PRIVATE_KEY)',
230
+ };
231
+ }
232
+ const allowlist = (0, brain_signing_1.loadAllowlist)();
233
+ if (allowlist.length === 0) {
234
+ return {
235
+ name: 'allowlist signer',
236
+ status: 'warn',
237
+ detail: 'allowlist is empty — add at least one address via `pop brain allowlist add`',
238
+ };
239
+ }
240
+ try {
241
+ const addr = new ethers_1.ethers.Wallet(key).address.toLowerCase();
242
+ if ((0, brain_signing_1.isAllowedAuthor)(addr)) {
243
+ return {
244
+ name: 'allowlist signer',
245
+ status: 'pass',
246
+ detail: `${addr} is allowlisted`,
247
+ };
248
+ }
249
+ return {
250
+ name: 'allowlist signer',
251
+ status: 'warn',
252
+ detail: `${addr} NOT in allowlist — reads work, writes won't propagate. Fix: pop brain allowlist add --address ${addr}`,
253
+ };
254
+ }
255
+ catch {
256
+ return {
257
+ name: 'allowlist signer',
258
+ status: 'info',
259
+ detail: 'skipped (wallet construction failed)',
260
+ };
261
+ }
262
+ }
263
+ async function checkDynamicMembership() {
264
+ // Task #330: surface whether the on-chain dynamic allowlist is reachable
265
+ // and how many authorized signers it resolves to. Falls back to a warn,
266
+ // never a fail, because the static JSON is a legitimate offline path.
267
+ const { tryFetchOrgMembers } = await Promise.resolve().then(() => __importStar(require('../../lib/brain-membership')));
268
+ try {
269
+ const members = await tryFetchOrgMembers();
270
+ const staticCount = (0, brain_signing_1.loadAllowlist)().length;
271
+ if (members === null) {
272
+ return {
273
+ name: 'dynamic allowlist',
274
+ status: 'warn',
275
+ detail: `subgraph unreachable — using static fallback (${staticCount} entries). Set POP_DEFAULT_ORG / POP_DEFAULT_CHAIN or check network.`,
276
+ };
277
+ }
278
+ const mode = staticCount === 0 ? 'dynamic'
279
+ : members.size === 0 ? 'static-only'
280
+ : 'both';
281
+ return {
282
+ name: 'dynamic allowlist',
283
+ status: 'pass',
284
+ detail: `${members.size} on-chain member${members.size === 1 ? '' : 's'}, ${staticCount} static entries (mode: ${mode})`,
285
+ };
286
+ }
287
+ catch (err) {
288
+ return {
289
+ name: 'dynamic allowlist',
290
+ status: 'warn',
291
+ detail: `check crashed — ${err?.message ?? 'unknown'}`,
292
+ };
293
+ }
294
+ }
295
+ function checkDocManifest() {
296
+ const path = (0, path_1.join)((0, brain_1.getBrainHome)(), 'doc-heads.json');
297
+ if (!(0, fs_1.existsSync)(path)) {
298
+ return {
299
+ name: 'doc-heads manifest',
300
+ status: 'info',
301
+ detail: 'no manifest yet (no brain docs written from this home)',
302
+ };
303
+ }
304
+ try {
305
+ const raw = JSON.parse((0, fs_1.readFileSync)(path, 'utf8'));
306
+ const docIds = Object.keys(raw);
307
+ return {
308
+ name: 'doc-heads manifest',
309
+ status: 'pass',
310
+ detail: `${docIds.length} doc${docIds.length === 1 ? '' : 's'}${docIds.length > 0 ? ' — ' + docIds.slice(0, 3).join(', ') + (docIds.length > 3 ? ', ...' : '') : ''}`,
311
+ };
312
+ }
313
+ catch (err) {
314
+ return {
315
+ name: 'doc-heads manifest',
316
+ status: 'warn',
317
+ detail: `unreadable — ${err.message}`,
318
+ };
319
+ }
320
+ }
321
+ async function checkSubscribedTopics(node) {
322
+ if (!node) {
323
+ return {
324
+ name: 'subscribed topics',
325
+ status: 'info',
326
+ detail: 'skipped (libp2p init failed)',
327
+ };
328
+ }
329
+ try {
330
+ const pubsub = node.libp2p.services?.pubsub;
331
+ if (!pubsub) {
332
+ return {
333
+ name: 'subscribed topics',
334
+ status: 'warn',
335
+ detail: 'pubsub service not configured',
336
+ };
337
+ }
338
+ const topics = pubsub.getTopics?.() ?? [];
339
+ if (topics.length === 0) {
340
+ return {
341
+ name: 'subscribed topics',
342
+ status: 'info',
343
+ detail: 'no topics subscribed (no local docs to auto-subscribe)',
344
+ };
345
+ }
346
+ const peerSummary = topics
347
+ .map((t) => {
348
+ const n = pubsub.getSubscribers?.(t)?.length ?? 0;
349
+ return `${t.replace(/^pop\/brain\//, '').replace(/\/v1$/, '')}(${n})`;
350
+ })
351
+ .join(', ');
352
+ return {
353
+ name: 'subscribed topics',
354
+ status: 'pass',
355
+ detail: `${topics.length} — ${peerSummary}`,
356
+ };
357
+ }
358
+ catch (err) {
359
+ return {
360
+ name: 'subscribed topics',
361
+ status: 'warn',
362
+ detail: `inspection failed — ${err.message}`,
363
+ };
364
+ }
365
+ }
366
+ /**
367
+ * Task #371: check whether local brain docs have synced with at least one peer.
368
+ *
369
+ * Uses the daemon's IPC status as a proxy: if incomingMerges > 0, content has
370
+ * flowed from a peer, confirming history overlap. This avoids the expense of
371
+ * opening Automerge docs and comparing change hashes in a diagnostic command.
372
+ */
373
+ async function checkPeerSyncOverlap() {
374
+ const docs = (0, brain_1.listBrainDocs)();
375
+ if (docs.length === 0) {
376
+ return {
377
+ name: 'peer sync overlap',
378
+ status: 'info',
379
+ detail: 'no local docs — nothing to compare',
380
+ };
381
+ }
382
+ let daemonRunning = false;
383
+ try {
384
+ const pidStr = (0, fs_1.readFileSync)((0, brain_daemon_1.getDaemonPidPath)(), 'utf8').trim();
385
+ const pid = parseInt(pidStr, 10);
386
+ if (pid > 0) {
387
+ process.kill(pid, 0);
388
+ daemonRunning = true;
389
+ }
390
+ }
391
+ catch { /* no PID file or process gone */ }
392
+ if (!daemonRunning) {
393
+ return {
394
+ name: 'peer sync overlap',
395
+ status: 'warn',
396
+ detail: `${docs.length} local doc(s) but daemon not running — cannot verify peer overlap. Start with: pop brain daemon start`,
397
+ };
398
+ }
399
+ try {
400
+ const status = await (0, brain_daemon_1.sendIpcRequest)('status', {}, 3000);
401
+ const merges = status.incomingMerges || 0;
402
+ const rejects = status.incomingRejects || 0;
403
+ const announces = status.incomingAnnouncements || 0;
404
+ if (merges > 0) {
405
+ return {
406
+ name: 'peer sync overlap',
407
+ status: 'pass',
408
+ detail: `${merges} merge(s) received from peers — history overlap confirmed`,
409
+ };
410
+ }
411
+ if (announces > 0 && merges === 0) {
412
+ return {
413
+ name: 'peer sync overlap',
414
+ status: 'warn',
415
+ detail: `${announces} announcement(s) received but 0 merges — peers exist but content may be disjoint${rejects > 0 ? ` (${rejects} rejects)` : ''}`,
416
+ };
417
+ }
418
+ return {
419
+ name: 'peer sync overlap',
420
+ status: 'warn',
421
+ detail: `daemon running but 0 announcements — no peer activity since daemon start`,
422
+ };
423
+ }
424
+ catch (err) {
425
+ return {
426
+ name: 'peer sync overlap',
427
+ status: 'warn',
428
+ detail: `daemon IPC failed — ${err.message}`,
429
+ };
430
+ }
431
+ }
432
+ exports.doctorHandler = {
433
+ builder: (yargs) => yargs,
434
+ handler: async (_argv) => {
435
+ const checks = [];
436
+ const spin = output.isJsonMode() ? null : output.spinner('Running brain health checks...');
437
+ spin?.start();
438
+ try {
439
+ // Non-libp2p checks first (fast, synchronous).
440
+ checks.push(await checkPrivateKey());
441
+ checks.push(checkBrainHome());
442
+ checks.push(await checkPeerKey());
443
+ checks.push(checkAllowlist());
444
+ checks.push(await checkDynamicMembership());
445
+ checks.push(checkDocManifest());
446
+ // libp2p init is the integration check — may take a few seconds.
447
+ const { result: initResult, node } = await checkLibp2pInit();
448
+ checks.push(initResult);
449
+ // Bootstrap + topic checks only if libp2p init succeeded.
450
+ checks.push(await checkBootstrap(node));
451
+ checks.push(await checkSubscribedTopics(node));
452
+ // Task #371: peer sync overlap check. If the daemon is running and
453
+ // local docs exist, check whether the daemon has received any merges
454
+ // (incomingMerges > 0 means content has flowed from at least one peer,
455
+ // confirming history overlap). If daemon isn't running, warn that
456
+ // overlap can't be verified.
457
+ checks.push(await checkPeerSyncOverlap());
458
+ spin?.stop();
459
+ const pass = checks.filter((c) => c.status === 'pass').length;
460
+ const warn = checks.filter((c) => c.status === 'warn').length;
461
+ const fail = checks.filter((c) => c.status === 'fail').length;
462
+ const info = checks.filter((c) => c.status === 'info').length;
463
+ if (output.isJsonMode()) {
464
+ output.json({
465
+ status: fail > 0 ? 'error' : 'ok',
466
+ checks,
467
+ summary: { pass, warn, fail, info },
468
+ });
469
+ }
470
+ else {
471
+ console.log('');
472
+ console.log(' Brain health check');
473
+ console.log(' ' + '─'.repeat(60));
474
+ for (const c of checks) {
475
+ const icon = c.status === 'pass' ? '✓' :
476
+ c.status === 'warn' ? '⚠' :
477
+ c.status === 'fail' ? '✗' : 'ℹ';
478
+ const line = ` ${icon} ${c.name.padEnd(24)} ${c.detail}`;
479
+ console.log(line);
480
+ }
481
+ console.log('');
482
+ console.log(` Summary: ${pass} pass · ${warn} warn · ${fail} fail · ${info} info`);
483
+ console.log('');
484
+ }
485
+ if (fail > 0)
486
+ process.exitCode = 1;
487
+ }
488
+ catch (err) {
489
+ spin?.stop();
490
+ output.error(`brain doctor crashed: ${err.message}`);
491
+ process.exitCode = 1;
492
+ }
493
+ finally {
494
+ await (0, brain_1.stopBrainNode)();
495
+ }
496
+ },
497
+ };
@@ -0,0 +1,51 @@
1
+ /**
2
+ * pop brain edit-lesson — update fields on an existing brain lesson.
3
+ *
4
+ * Finds a lesson by its id inside a brain doc, mutates the provided
5
+ * fields in-place, and persists the result via applyBrainChange so
6
+ * the edit goes through the full sign → persist → gossipsub publish
7
+ * pipeline. Edits are idempotent from the user's perspective: running
8
+ * the same edit twice produces the same final state (the second run
9
+ * is a no-op if no field actually changes).
10
+ *
11
+ * Scope: edit in place only. No remove, no re-id, no ordering mutation.
12
+ * Remove requires CRDT tombstone semantics which are a separate ship.
13
+ */
14
+ import type { Argv, ArgumentsCamelCase } from 'yargs';
15
+ interface EditArgs {
16
+ doc: string;
17
+ lessonId: string;
18
+ title?: string;
19
+ body?: string;
20
+ bodyFile?: string;
21
+ author?: string;
22
+ touch?: boolean;
23
+ allowInvalidShape?: boolean;
24
+ 'idempotency-key'?: string;
25
+ 'no-idempotency'?: boolean;
26
+ }
27
+ export declare const editLessonHandler: {
28
+ builder: (yargs: Argv) => Argv<{
29
+ doc: string;
30
+ } & {
31
+ "lesson-id": string;
32
+ } & {
33
+ title: string | undefined;
34
+ } & {
35
+ body: string | undefined;
36
+ } & {
37
+ "body-file": string | undefined;
38
+ } & {
39
+ author: string | undefined;
40
+ } & {
41
+ touch: boolean;
42
+ } & {
43
+ "allow-invalid-shape": boolean;
44
+ } & {
45
+ "idempotency-key": string | undefined;
46
+ } & {
47
+ "no-idempotency": boolean;
48
+ }>;
49
+ handler: (argv: ArgumentsCamelCase<EditArgs>) => Promise<void>;
50
+ };
51
+ export {};