@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,36 @@
1
+ /**
2
+ * Brain doc schemas — write-time shape validation (Task #346, HB#168).
3
+ *
4
+ * Retro #1 change #5: currently applyBrainChange() accepts any doc shape
5
+ * and the projection layer (brain-projections.ts) tolerates bad shapes at
6
+ * read time via formatTimestamp() and schema-tolerant renderers. That
7
+ * approach means bad data enters the doc and stays forever (Automerge
8
+ * field renames are merge hazards per HB#248). This module validates
9
+ * shape at WRITE time so canonically-broken entries never enter the doc.
10
+ *
11
+ * DESIGN:
12
+ * - Per-doc-id validators. pop.brain.shared, pop.brain.projects,
13
+ * pop.brain.retros each have their own shape. Unknown doc ids return
14
+ * `{ ok: true, warnings: [...] }` — permissionless schema evolution.
15
+ * - Validators check ITEMS of known arrays for required fields. Extra
16
+ * fields are allowed (extensibility); missing required fields are errors.
17
+ * - applyBrainChange diffs pre-vs-post validity: if the pre-change doc
18
+ * was already invalid, the bad state was inherited, not introduced, and
19
+ * the new write is NOT rejected. Only regressions (valid → invalid) are
20
+ * rejected. This preserves the constraint "existing bad entries stay
21
+ * readable" while still preventing new bad writes.
22
+ * - Per-command --allow-invalid-shape bypass lives at the CLI layer and is
23
+ * plumbed through applyBrainChange's options bag.
24
+ *
25
+ * SCHEMAS ARE DEFINED ONCE HERE and NOT duplicated in the CLI commands.
26
+ */
27
+ export interface ValidationResult {
28
+ ok: boolean;
29
+ errors: string[];
30
+ warnings: string[];
31
+ }
32
+ /**
33
+ * Dispatch entry point. Returns { ok, errors, warnings }. Unknown doc ids
34
+ * are permitted (schema evolution) with a warning, not an error.
35
+ */
36
+ export declare function validateBrainDocShape(docId: string, doc: any): ValidationResult;
@@ -0,0 +1,316 @@
1
+ "use strict";
2
+ /**
3
+ * Brain doc schemas — write-time shape validation (Task #346, HB#168).
4
+ *
5
+ * Retro #1 change #5: currently applyBrainChange() accepts any doc shape
6
+ * and the projection layer (brain-projections.ts) tolerates bad shapes at
7
+ * read time via formatTimestamp() and schema-tolerant renderers. That
8
+ * approach means bad data enters the doc and stays forever (Automerge
9
+ * field renames are merge hazards per HB#248). This module validates
10
+ * shape at WRITE time so canonically-broken entries never enter the doc.
11
+ *
12
+ * DESIGN:
13
+ * - Per-doc-id validators. pop.brain.shared, pop.brain.projects,
14
+ * pop.brain.retros each have their own shape. Unknown doc ids return
15
+ * `{ ok: true, warnings: [...] }` — permissionless schema evolution.
16
+ * - Validators check ITEMS of known arrays for required fields. Extra
17
+ * fields are allowed (extensibility); missing required fields are errors.
18
+ * - applyBrainChange diffs pre-vs-post validity: if the pre-change doc
19
+ * was already invalid, the bad state was inherited, not introduced, and
20
+ * the new write is NOT rejected. Only regressions (valid → invalid) are
21
+ * rejected. This preserves the constraint "existing bad entries stay
22
+ * readable" while still preventing new bad writes.
23
+ * - Per-command --allow-invalid-shape bypass lives at the CLI layer and is
24
+ * plumbed through applyBrainChange's options bag.
25
+ *
26
+ * SCHEMAS ARE DEFINED ONCE HERE and NOT duplicated in the CLI commands.
27
+ */
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.validateBrainDocShape = validateBrainDocShape;
30
+ /**
31
+ * Lesson schema used by pop.brain.shared and (historically) pop.brain.lessons.
32
+ * Canonical shape: { id, author, title, body, timestamp, removed? }.
33
+ * Legacy tolerance: `text` is accepted as a synonym for `body`, `ts` for
34
+ * `timestamp` — the projection layer reads both (HB#297 formatTimestamp).
35
+ * A lesson must have at least one of {body, text} AND at least one of
36
+ * {title, id}. That's the minimum renderable shape.
37
+ */
38
+ function validateLesson(lesson, index, errors) {
39
+ if (lesson == null || typeof lesson !== 'object') {
40
+ errors.push(`lessons[${index}]: not an object`);
41
+ return;
42
+ }
43
+ const hasBody = (typeof lesson.body === 'string' && lesson.body.length > 0) ||
44
+ (typeof lesson.text === 'string' && lesson.text.length > 0);
45
+ if (!hasBody) {
46
+ errors.push(`lessons[${index}]: missing required body/text (one must be a non-empty string)`);
47
+ }
48
+ const hasTitle = (typeof lesson.title === 'string' && lesson.title.length > 0) ||
49
+ (typeof lesson.id === 'string' && lesson.id.length > 0);
50
+ if (!hasTitle) {
51
+ errors.push(`lessons[${index}]: missing required title/id (one must be a non-empty string)`);
52
+ }
53
+ if (lesson.timestamp != null && lesson.ts != null) {
54
+ // Not an error; just unusual. Don't warn noisily.
55
+ }
56
+ if (lesson.timestamp != null &&
57
+ typeof lesson.timestamp !== 'number' &&
58
+ typeof lesson.timestamp !== 'string') {
59
+ errors.push(`lessons[${index}]: timestamp must be number or ISO string`);
60
+ }
61
+ // Task #347: optional tags field. Must be an array of strings when present.
62
+ // Backwards compatible: existing lessons without tags still validate.
63
+ // Tag vocabulary is free-form — no enforcement of category:/topic:/etc.
64
+ if (lesson.tags != null) {
65
+ if (!Array.isArray(lesson.tags)) {
66
+ errors.push(`lessons[${index}]: tags must be an array of strings`);
67
+ }
68
+ else {
69
+ for (let i = 0; i < lesson.tags.length; i++) {
70
+ if (typeof lesson.tags[i] !== 'string') {
71
+ errors.push(`lessons[${index}]: tags[${i}] must be a string`);
72
+ }
73
+ }
74
+ }
75
+ }
76
+ }
77
+ function validateRule(rule, index, errors) {
78
+ if (rule == null || typeof rule !== 'object') {
79
+ errors.push(`rules[${index}]: not an object`);
80
+ return;
81
+ }
82
+ if (typeof rule.text !== 'string' || rule.text.length === 0) {
83
+ errors.push(`rules[${index}]: missing required text (non-empty string)`);
84
+ }
85
+ }
86
+ /**
87
+ * pop.brain.shared — lessons + rules + operatingConstraints + orgState.
88
+ * Arrays are optional (first-write bootstrap) but when present their items
89
+ * must validate.
90
+ */
91
+ function validateSharedDoc(doc, errors, warnings) {
92
+ if (doc == null || typeof doc !== 'object') {
93
+ errors.push('pop.brain.shared: doc is not an object');
94
+ return;
95
+ }
96
+ if (doc.lessons != null) {
97
+ if (!Array.isArray(doc.lessons)) {
98
+ errors.push('pop.brain.shared: lessons must be an array');
99
+ }
100
+ else {
101
+ doc.lessons.forEach((l, i) => validateLesson(l, i, errors));
102
+ }
103
+ }
104
+ if (doc.rules != null) {
105
+ if (!Array.isArray(doc.rules)) {
106
+ errors.push('pop.brain.shared: rules must be an array');
107
+ }
108
+ else {
109
+ doc.rules.forEach((r, i) => validateRule(r, i, errors));
110
+ }
111
+ }
112
+ if (doc.operatingConstraints != null && !Array.isArray(doc.operatingConstraints)) {
113
+ errors.push('pop.brain.shared: operatingConstraints must be an array');
114
+ }
115
+ if (doc.orgState != null && typeof doc.orgState !== 'object') {
116
+ errors.push('pop.brain.shared: orgState must be an object');
117
+ }
118
+ void warnings;
119
+ }
120
+ // HB#183 (lesson cross-module-enum-drift): the source of truth for the
121
+ // project lifecycle stages is the ProjectStage union type in
122
+ // src/lib/brain-projections.ts. We MUST NOT retype the values here —
123
+ // drift between the schema and the projection breaks at runtime
124
+ // (HB#180 incident: schema enum was {proposed, building, ...},
125
+ // canonical was {propose, discuss, ...}, validator rejected legitimate
126
+ // new-project writes). The literal-array-as-type pattern below lets
127
+ // TypeScript keep both the runtime Set and the union type in sync from
128
+ // one declaration. If the canonical lifecycle ever changes,
129
+ // brain-projections.ts ProjectStage stays the source of truth — update
130
+ // that union, then update PROJECT_STAGES here in lockstep, and the
131
+ // vitest "accepts all canonical lifecycle stages" case fails loudly
132
+ // if drift creeps back in.
133
+ // The literal tuple is what gives us a runtime Set; the satisfies clause
134
+ // below proves at compile time that this list is structurally identical
135
+ // to the ProjectStage union from brain-projections. If brain-projections
136
+ // adds or removes a stage and this list isn't updated, tsc fails the
137
+ // build at the satisfies line — drift is impossible.
138
+ const PROJECT_STAGES = ['propose', 'discuss', 'plan', 'vote', 'execute', 'review', 'ship'];
139
+ // Compile-time bidirectional drift check.
140
+ // Direction 1 (literal → union): every literal in PROJECT_STAGES must
141
+ // be a valid ProjectStage. tsc fails if you add a typo to the tuple.
142
+ const _stagesAreValid = PROJECT_STAGES;
143
+ const _stagesMatchUnion = true;
144
+ void _stagesAreValid;
145
+ void _stagesMatchUnion;
146
+ const VALID_PROJECT_STAGES = new Set(PROJECT_STAGES);
147
+ function validateProject(p, index, errors) {
148
+ if (p == null || typeof p !== 'object') {
149
+ errors.push(`projects[${index}]: not an object`);
150
+ return;
151
+ }
152
+ if (typeof p.id !== 'string' || p.id.length === 0) {
153
+ errors.push(`projects[${index}]: missing required id (non-empty string)`);
154
+ }
155
+ if (typeof p.name !== 'string' || p.name.length === 0) {
156
+ errors.push(`projects[${index}]: missing required name (non-empty string)`);
157
+ }
158
+ if (typeof p.stage !== 'string' || !VALID_PROJECT_STAGES.has(p.stage)) {
159
+ errors.push(`projects[${index}]: stage must be one of ${[...VALID_PROJECT_STAGES].join('|')}, got ${JSON.stringify(p.stage)}`);
160
+ }
161
+ }
162
+ function validateProjectsDoc(doc, errors) {
163
+ if (doc == null || typeof doc !== 'object') {
164
+ errors.push('pop.brain.projects: doc is not an object');
165
+ return;
166
+ }
167
+ if (doc.projects != null) {
168
+ if (!Array.isArray(doc.projects)) {
169
+ errors.push('pop.brain.projects: projects must be an array');
170
+ }
171
+ else {
172
+ doc.projects.forEach((p, i) => validateProject(p, i, errors));
173
+ }
174
+ }
175
+ }
176
+ function validateRetro(r, index, errors) {
177
+ if (r == null || typeof r !== 'object') {
178
+ errors.push(`retros[${index}]: not an object`);
179
+ return;
180
+ }
181
+ if (typeof r.id !== 'string' || r.id.length === 0) {
182
+ errors.push(`retros[${index}]: missing required id`);
183
+ }
184
+ if (r.proposedChanges != null && !Array.isArray(r.proposedChanges)) {
185
+ errors.push(`retros[${index}]: proposedChanges must be an array`);
186
+ }
187
+ if (r.discussion != null && !Array.isArray(r.discussion)) {
188
+ errors.push(`retros[${index}]: discussion must be an array`);
189
+ }
190
+ }
191
+ function validateRetrosDoc(doc, errors) {
192
+ if (doc == null || typeof doc !== 'object') {
193
+ errors.push('pop.brain.retros: doc is not an object');
194
+ return;
195
+ }
196
+ if (doc.retros != null) {
197
+ if (!Array.isArray(doc.retros)) {
198
+ errors.push('pop.brain.retros: retros must be an array');
199
+ }
200
+ else {
201
+ doc.retros.forEach((r, i) => validateRetro(r, i, errors));
202
+ }
203
+ }
204
+ }
205
+ // --- pop.brain.brainstorms (task #354 phase a, HB#207) -------------
206
+ //
207
+ // Brainstorms are a forward-looking cross-agent ideation surface:
208
+ // { id, title, prompt, author, status, ideas[], window, removed? }.
209
+ // Distinct from pop.brain.retros (reactive session retrospectives)
210
+ // and pop.brain.projects (lifecycle state machine) — this doc is
211
+ // where new questions get posted, ideas get debated + voted, and
212
+ // top-ranked ideas get promoted to pop.brain.projects at the propose
213
+ // stage. See docs/agents/brain-layer-setup.md and the #354 task description
214
+ // for the full lifecycle.
215
+ const VALID_BRAINSTORM_STATUSES = ['open', 'voting', 'closed', 'promoted'];
216
+ const VALID_BRAINSTORM_STATUS_SET = new Set(VALID_BRAINSTORM_STATUSES);
217
+ const VALID_VOTE_STANCES = ['support', 'explore', 'oppose'];
218
+ const VALID_VOTE_STANCE_SET = new Set(VALID_VOTE_STANCES);
219
+ function validateIdea(idea, brainstormId, ideaIndex, errors) {
220
+ if (idea == null || typeof idea !== 'object') {
221
+ errors.push(`brainstorms[${brainstormId}].ideas[${ideaIndex}]: not an object`);
222
+ return;
223
+ }
224
+ if (typeof idea.id !== 'string' || idea.id.length === 0) {
225
+ errors.push(`brainstorms[${brainstormId}].ideas[${ideaIndex}]: missing required id`);
226
+ }
227
+ if (typeof idea.message !== 'string' || idea.message.length === 0) {
228
+ errors.push(`brainstorms[${brainstormId}].ideas[${ideaIndex}]: missing required message`);
229
+ }
230
+ if (idea.author != null && typeof idea.author !== 'string') {
231
+ errors.push(`brainstorms[${brainstormId}].ideas[${ideaIndex}]: author must be a string when present`);
232
+ }
233
+ if (idea.votes != null) {
234
+ if (typeof idea.votes !== 'object' || Array.isArray(idea.votes)) {
235
+ errors.push(`brainstorms[${brainstormId}].ideas[${ideaIndex}]: votes must be an object keyed by agent address`);
236
+ }
237
+ else {
238
+ for (const [addr, stance] of Object.entries(idea.votes)) {
239
+ if (typeof stance !== 'string' || !VALID_VOTE_STANCE_SET.has(stance)) {
240
+ errors.push(`brainstorms[${brainstormId}].ideas[${ideaIndex}].votes[${addr}]: stance must be one of ${[...VALID_VOTE_STANCES].join('|')}, got ${JSON.stringify(stance)}`);
241
+ }
242
+ }
243
+ }
244
+ }
245
+ if (idea.priority != null && !['high', 'medium', 'low'].includes(idea.priority)) {
246
+ errors.push(`brainstorms[${brainstormId}].ideas[${ideaIndex}]: priority must be high|medium|low when present`);
247
+ }
248
+ }
249
+ function validateBrainstorm(b, index, errors) {
250
+ if (b == null || typeof b !== 'object') {
251
+ errors.push(`brainstorms[${index}]: not an object`);
252
+ return;
253
+ }
254
+ const bid = b.id ?? `<index ${index}>`;
255
+ if (typeof b.id !== 'string' || b.id.length === 0) {
256
+ errors.push(`brainstorms[${index}]: missing required id`);
257
+ }
258
+ if (typeof b.title !== 'string' || b.title.length === 0) {
259
+ errors.push(`brainstorms[${bid}]: missing required title`);
260
+ }
261
+ if (typeof b.status !== 'string' || !VALID_BRAINSTORM_STATUS_SET.has(b.status)) {
262
+ errors.push(`brainstorms[${bid}]: status must be one of ${[...VALID_BRAINSTORM_STATUSES].join('|')}, got ${JSON.stringify(b.status)}`);
263
+ }
264
+ if (b.ideas != null) {
265
+ if (!Array.isArray(b.ideas)) {
266
+ errors.push(`brainstorms[${bid}]: ideas must be an array`);
267
+ }
268
+ else {
269
+ b.ideas.forEach((idea, i) => validateIdea(idea, bid, i, errors));
270
+ }
271
+ }
272
+ if (b.promotedToProjectIds != null && !Array.isArray(b.promotedToProjectIds)) {
273
+ errors.push(`brainstorms[${bid}]: promotedToProjectIds must be an array`);
274
+ }
275
+ }
276
+ function validateBrainstormsDoc(doc, errors) {
277
+ if (doc == null || typeof doc !== 'object') {
278
+ errors.push('pop.brain.brainstorms: doc is not an object');
279
+ return;
280
+ }
281
+ if (doc.brainstorms != null) {
282
+ if (!Array.isArray(doc.brainstorms)) {
283
+ errors.push('pop.brain.brainstorms: brainstorms must be an array');
284
+ }
285
+ else {
286
+ doc.brainstorms.forEach((b, i) => validateBrainstorm(b, i, errors));
287
+ }
288
+ }
289
+ }
290
+ /**
291
+ * Dispatch entry point. Returns { ok, errors, warnings }. Unknown doc ids
292
+ * are permitted (schema evolution) with a warning, not an error.
293
+ */
294
+ function validateBrainDocShape(docId, doc) {
295
+ const errors = [];
296
+ const warnings = [];
297
+ switch (docId) {
298
+ case 'pop.brain.shared':
299
+ case 'pop.brain.lessons':
300
+ validateSharedDoc(doc, errors, warnings);
301
+ break;
302
+ case 'pop.brain.projects':
303
+ validateProjectsDoc(doc, errors);
304
+ break;
305
+ case 'pop.brain.retros':
306
+ validateRetrosDoc(doc, errors);
307
+ break;
308
+ case 'pop.brain.brainstorms':
309
+ validateBrainstormsDoc(doc, errors);
310
+ break;
311
+ default:
312
+ warnings.push(`unknown doc id "${docId}" — no schema registered, accepting any shape. ` +
313
+ `Add a validator to src/lib/brain-schemas.ts to enforce one.`);
314
+ }
315
+ return { ok: errors.length === 0, errors, warnings };
316
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Brain-layer change signing — ECDSA over snapshot bytes.
3
+ *
4
+ * Each brain CRDT change gets wrapped in a signed envelope before being
5
+ * written to the blockstore. The envelope's sig authenticates the full
6
+ * Automerge snapshot against an Ethereum address derived from the
7
+ * existing POP_PRIVATE_KEY. No Nostr keys, no Schnorr, no second PKI.
8
+ *
9
+ * Envelope format (v1):
10
+ *
11
+ * {
12
+ * v: 1,
13
+ * author: "0xABCD...", // Ethereum address (lowercase)
14
+ * timestamp: 1776200000, // unix seconds
15
+ * automerge: "0xDEADBEEF", // full Automerge.save() bytes, hex
16
+ * sig: "0xABC..." // ECDSA over keccak256(author|ts|automerge)
17
+ * }
18
+ *
19
+ * Serialized as UTF-8 JSON, stored as a raw-codec IPLD block. The CID
20
+ * covers the whole envelope, so the sig is content-addressed alongside
21
+ * the data it authenticates.
22
+ *
23
+ * On read, the projection layer:
24
+ * 1. Unmarshal the envelope JSON
25
+ * 2. Verify the sig recovers to `author`
26
+ * 3. Check `author` against the allowlist at
27
+ * agent/brain/Config/brain-allowlist.json
28
+ * 4. If all OK, extract the Automerge bytes and merge
29
+ *
30
+ * Sync layer stays permissionless — any peer can gossip any CID. Auth
31
+ * happens at read time so the network is resilient against relay
32
+ * operators (there are none, but the principle stands for any future
33
+ * transport).
34
+ */
35
+ export interface BrainChangeEnvelope {
36
+ v: 1;
37
+ author: string;
38
+ timestamp: number;
39
+ automerge: string;
40
+ sig: string;
41
+ }
42
+ /**
43
+ * Sign an Automerge snapshot with the wallet derived from POP_PRIVATE_KEY.
44
+ * Returns a v1 envelope ready to be JSON-encoded and written as a block.
45
+ */
46
+ export declare function signBrainChange(automergeBytes: Uint8Array, privateKey?: string): Promise<BrainChangeEnvelope>;
47
+ /**
48
+ * Verify an envelope's signature and return the recovered author.
49
+ * Throws if the envelope is malformed or the signature doesn't verify.
50
+ *
51
+ * NOTE: this only checks authenticity (sig corresponds to `author`);
52
+ * it does NOT check authorization (whether `author` is allowed to
53
+ * write to this doc). That's the allowlist check — see isAllowedAuthor.
54
+ */
55
+ export declare function verifyBrainChange(envelope: BrainChangeEnvelope): string;
56
+ /**
57
+ * Extract the Automerge snapshot bytes from an envelope.
58
+ * Does NOT verify the signature — caller must run verifyBrainChange first.
59
+ */
60
+ export declare function unwrapAutomergeBytes(envelope: BrainChangeEnvelope): Uint8Array;
61
+ export interface AllowlistEntry {
62
+ address: string;
63
+ name?: string;
64
+ addedAt?: string;
65
+ addedBy?: string;
66
+ }
67
+ /**
68
+ * Path to the git-tracked brain-allowlist.json. Single source of truth
69
+ * for who is permitted to write to brain docs. Edited via governance
70
+ * (or via `pop brain allowlist add/remove`, which writes to the same
71
+ * file and leaves the git review gate in place).
72
+ *
73
+ * Exported so command handlers can write to the same path without
74
+ * hard-coding it independently.
75
+ */
76
+ export declare function getAllowlistPath(): string;
77
+ export declare function loadAllowlist(): AllowlistEntry[];
78
+ /**
79
+ * Check whether a given address is in the allowlist.
80
+ * Case-insensitive on the address.
81
+ */
82
+ export declare function isAllowedAuthor(address: string): boolean;
83
+ /**
84
+ * Combined auth check: verify signature + allowlist membership.
85
+ * Returns the authenticated author on success; throws otherwise.
86
+ */
87
+ export declare function authenticateAndAuthorize(envelope: BrainChangeEnvelope): string;
88
+ export type AuthorizationMode = 'dynamic' | 'static-fallback' | 'both-agree';
89
+ export interface AuthorizationResult {
90
+ allowed: boolean;
91
+ mode: AuthorizationMode;
92
+ /** Populated only when fallback fired. Empty string otherwise. */
93
+ fallbackReason: string;
94
+ }
95
+ /**
96
+ * Async authorization check: on-chain org membership first, static
97
+ * JSON allowlist second. Does NOT throw — returns a result object the
98
+ * caller can inspect for logging purposes. Callers then decide whether
99
+ * to reject the change or accept it based on `.allowed`.
100
+ *
101
+ * This is the new canonical authorization for brain read paths.
102
+ */
103
+ export declare function isAuthorizedAuthor(address: string): Promise<AuthorizationResult>;