@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.
- package/.env.agent.template +20 -0
- package/README.md +46 -0
- package/brain/Config/agent-config.json +14 -0
- package/brain/Config/brain-allowlist.json +20 -0
- package/brain/Identity/goals.template.md +23 -0
- package/brain/Identity/how-i-think.md +406 -0
- package/brain/Identity/who-i-am.template.md +34 -0
- package/brain/Knowledge/BOOTSTRAP.md +66 -0
- package/brain/Knowledge/audit-corpus-index.json +406 -0
- package/brain/Knowledge/discussions.json +245 -0
- package/brain/Knowledge/pop.brain.brainstorms.generated.md +48 -0
- package/brain/Knowledge/pop.brain.brainstorms.genesis.bin +0 -0
- package/brain/Knowledge/pop.brain.heuristics.snapshot.bin +0 -0
- package/brain/Knowledge/pop.brain.projects.generated.md +16 -0
- package/brain/Knowledge/pop.brain.projects.genesis.bin +0 -0
- package/brain/Knowledge/pop.brain.retros.generated.md +91 -0
- package/brain/Knowledge/pop.brain.retros.genesis.bin +0 -0
- package/brain/Knowledge/pop.brain.shared.generated.md +3811 -0
- package/brain/Knowledge/pop.brain.shared.genesis.bin +0 -0
- package/brain/Knowledge/projects.md +181 -0
- package/brain/Knowledge/risk-framework.md +90 -0
- package/brain/Knowledge/shared.md +416 -0
- package/brain/Knowledge/sprint-priorities.md +439 -0
- package/brain/Memory/.gitkeep +0 -0
- package/dist/commands/agent/daily-digest.d.ts +24 -0
- package/dist/commands/agent/daily-digest.js +336 -0
- package/dist/commands/agent/delegate.d.ts +12 -0
- package/dist/commands/agent/delegate.js +91 -0
- package/dist/commands/agent/deploy-to-org.d.ts +20 -0
- package/dist/commands/agent/deploy-to-org.js +154 -0
- package/dist/commands/agent/index.d.ts +2 -0
- package/dist/commands/agent/index.js +27 -0
- package/dist/commands/agent/init.d.ts +19 -0
- package/dist/commands/agent/init.js +303 -0
- package/dist/commands/agent/onboard.d.ts +22 -0
- package/dist/commands/agent/onboard.js +192 -0
- package/dist/commands/agent/paymaster-status.d.ts +14 -0
- package/dist/commands/agent/paymaster-status.js +130 -0
- package/dist/commands/agent/register.d.ts +21 -0
- package/dist/commands/agent/register.js +116 -0
- package/dist/commands/agent/setup-sponsorship.d.ts +22 -0
- package/dist/commands/agent/setup-sponsorship.js +154 -0
- package/dist/commands/agent/status.d.ts +12 -0
- package/dist/commands/agent/status.js +171 -0
- package/dist/commands/agent/triage.d.ts +12 -0
- package/dist/commands/agent/triage.js +503 -0
- package/dist/commands/brain/advance-stage.d.ts +42 -0
- package/dist/commands/brain/advance-stage.js +206 -0
- package/dist/commands/brain/allowlist.d.ts +30 -0
- package/dist/commands/brain/allowlist.js +274 -0
- package/dist/commands/brain/append-lesson.d.ts +55 -0
- package/dist/commands/brain/append-lesson.js +245 -0
- package/dist/commands/brain/brainstorm.d.ts +154 -0
- package/dist/commands/brain/brainstorm.js +573 -0
- package/dist/commands/brain/daemon.d.ts +31 -0
- package/dist/commands/brain/daemon.js +348 -0
- package/dist/commands/brain/doctor.d.ts +27 -0
- package/dist/commands/brain/doctor.js +497 -0
- package/dist/commands/brain/edit-lesson.d.ts +51 -0
- package/dist/commands/brain/edit-lesson.js +248 -0
- package/dist/commands/brain/import-snapshot.d.ts +68 -0
- package/dist/commands/brain/import-snapshot.js +177 -0
- package/dist/commands/brain/index.d.ts +2 -0
- package/dist/commands/brain/index.js +67 -0
- package/dist/commands/brain/list.d.ts +21 -0
- package/dist/commands/brain/list.js +83 -0
- package/dist/commands/brain/migrate-projects.d.ts +44 -0
- package/dist/commands/brain/migrate-projects.js +209 -0
- package/dist/commands/brain/migrate.d.ts +74 -0
- package/dist/commands/brain/migrate.js +306 -0
- package/dist/commands/brain/new-project.d.ts +53 -0
- package/dist/commands/brain/new-project.js +226 -0
- package/dist/commands/brain/read.d.ts +24 -0
- package/dist/commands/brain/read.js +81 -0
- package/dist/commands/brain/remove-lesson.d.ts +47 -0
- package/dist/commands/brain/remove-lesson.js +206 -0
- package/dist/commands/brain/remove-project.d.ts +36 -0
- package/dist/commands/brain/remove-project.js +177 -0
- package/dist/commands/brain/retro-file-tasks.d.ts +84 -0
- package/dist/commands/brain/retro-file-tasks.js +372 -0
- package/dist/commands/brain/retro-list.d.ts +28 -0
- package/dist/commands/brain/retro-list.js +125 -0
- package/dist/commands/brain/retro-mark-change.d.ts +58 -0
- package/dist/commands/brain/retro-mark-change.js +176 -0
- package/dist/commands/brain/retro-remove.d.ts +36 -0
- package/dist/commands/brain/retro-remove.js +142 -0
- package/dist/commands/brain/retro-respond.d.ts +56 -0
- package/dist/commands/brain/retro-respond.js +250 -0
- package/dist/commands/brain/retro-show.d.ts +23 -0
- package/dist/commands/brain/retro-show.js +100 -0
- package/dist/commands/brain/retro-start.d.ts +55 -0
- package/dist/commands/brain/retro-start.js +311 -0
- package/dist/commands/brain/search.d.ts +48 -0
- package/dist/commands/brain/search.js +190 -0
- package/dist/commands/brain/snapshot.d.ts +32 -0
- package/dist/commands/brain/snapshot.js +243 -0
- package/dist/commands/brain/status.d.ts +15 -0
- package/dist/commands/brain/status.js +166 -0
- package/dist/commands/brain/subscribe.d.ts +28 -0
- package/dist/commands/brain/subscribe.js +90 -0
- package/dist/commands/brain/tag.d.ts +46 -0
- package/dist/commands/brain/tag.js +192 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +22 -0
- package/dist/lib/brain-daemon.d.ts +126 -0
- package/dist/lib/brain-daemon.js +811 -0
- package/dist/lib/brain-membership.d.ts +58 -0
- package/dist/lib/brain-membership.js +115 -0
- package/dist/lib/brain-migrate-projects.d.ts +43 -0
- package/dist/lib/brain-migrate-projects.js +247 -0
- package/dist/lib/brain-migrate.d.ts +77 -0
- package/dist/lib/brain-migrate.js +328 -0
- package/dist/lib/brain-ops.d.ts +271 -0
- package/dist/lib/brain-ops.js +571 -0
- package/dist/lib/brain-paths.d.ts +15 -0
- package/dist/lib/brain-paths.js +33 -0
- package/dist/lib/brain-projections.d.ts +216 -0
- package/dist/lib/brain-projections.js +829 -0
- package/dist/lib/brain-schemas.d.ts +36 -0
- package/dist/lib/brain-schemas.js +316 -0
- package/dist/lib/brain-signing.d.ts +103 -0
- package/dist/lib/brain-signing.js +256 -0
- package/dist/lib/brain.d.ts +198 -0
- package/dist/lib/brain.js +1057 -0
- package/dist/pop-agent.d.ts +1 -0
- package/dist/pop-agent.js +18 -0
- package/docs/agent.md +126 -0
- package/docs/agents/brain-anti-entropy.md +127 -0
- package/docs/agents/brain-cross-device-onboarding.md +210 -0
- package/docs/agents/brain-cross-machine-smoke.md +241 -0
- package/docs/agents/brain-layer-setup.md +725 -0
- package/docs/agents/offboarding-protocol.md +188 -0
- package/docs/agents/onboarding-protocol.md +243 -0
- package/docs/agents/running-an-agent.md +200 -0
- package/docs/brain.md +560 -0
- package/package.json +61 -0
- package/scripts/apply.sh +140 -0
- package/scripts/onboard.sh +205 -0
- package/scripts/setup-agent.ts +272 -0
|
@@ -0,0 +1,829 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Brain projections — pure functions that render an Automerge brain doc
|
|
4
|
+
* (as a plain JS object) to human-readable markdown.
|
|
5
|
+
*
|
|
6
|
+
* Called by `pop brain snapshot --doc <id>` and by the heartbeat skill
|
|
7
|
+
* at end of heartbeat. The output is a .generated.md file that lives
|
|
8
|
+
* alongside the hand-written brain knowledge files; step 8 of the plan
|
|
9
|
+
* is when the hand-written files are retired and the projection becomes
|
|
10
|
+
* the source of truth.
|
|
11
|
+
*
|
|
12
|
+
* ## Design notes
|
|
13
|
+
*
|
|
14
|
+
* - **Pure functions only.** No I/O, no CLI state, no Date.now(). Makes
|
|
15
|
+
* them trivially testable and idempotent: same input → byte-identical
|
|
16
|
+
* output.
|
|
17
|
+
*
|
|
18
|
+
* - **Schema-tolerant.** The current pop.brain.shared schema stores a
|
|
19
|
+
* `lessons` list. Older revisions may use `rules` / `operatingConstraints` /
|
|
20
|
+
* `orgState`. The projection renders whichever of those fields exist,
|
|
21
|
+
* in a canonical order, and falls back to a JSON dump for any
|
|
22
|
+
* unrecognized fields so future schema changes don't silently drop data.
|
|
23
|
+
*
|
|
24
|
+
* - **Top-of-file banner.** Every generated file starts with a
|
|
25
|
+
* DO-NOT-HAND-EDIT banner so reviewers can spot a hand-edit in a diff.
|
|
26
|
+
*
|
|
27
|
+
* - **Stable field ordering.** Lessons (and other lists) are rendered in
|
|
28
|
+
* index order. If we later want time-sorted output, sort by timestamp
|
|
29
|
+
* inside the renderer — do NOT sort by ID (the CRDT list order is
|
|
30
|
+
* already deterministic across replicas thanks to Automerge).
|
|
31
|
+
*/
|
|
32
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
33
|
+
exports.projectShared = projectShared;
|
|
34
|
+
exports.projectProjects = projectProjects;
|
|
35
|
+
exports.projectRetros = projectRetros;
|
|
36
|
+
exports.projectBrainstorms = projectBrainstorms;
|
|
37
|
+
exports.projectForDoc = projectForDoc;
|
|
38
|
+
const GENERATED_BANNER = '# GENERATED BY `pop brain snapshot` — DO NOT HAND-EDIT\n' +
|
|
39
|
+
'<!-- Source of truth: Automerge CRDT doc in ~/.pop-agent/brain/helia-blocks -->\n' +
|
|
40
|
+
'<!-- Edits to this file will be overwritten on the next `pop brain snapshot` run. -->\n';
|
|
41
|
+
/**
|
|
42
|
+
* Render one lesson as a markdown block.
|
|
43
|
+
* Lessons have inconsistent field naming historically (text vs body,
|
|
44
|
+
* title vs none) — prefer title for the header, body/text for the
|
|
45
|
+
* content, fall back to JSON if neither exists.
|
|
46
|
+
*/
|
|
47
|
+
/**
|
|
48
|
+
* Format a timestamp value that may be a unix-seconds number, a unix-millis
|
|
49
|
+
* number, or an already-formatted ISO string. Returns null if the value
|
|
50
|
+
* can't be coerced — callers skip the field rather than crash. This is
|
|
51
|
+
* the schema-tolerant principle applied to the timestamp field: agents
|
|
52
|
+
* have historically written `timestamp` as both numeric and ISO-string,
|
|
53
|
+
* and the projector must not crash on either.
|
|
54
|
+
*/
|
|
55
|
+
function formatTimestamp(ts) {
|
|
56
|
+
if (ts == null)
|
|
57
|
+
return null;
|
|
58
|
+
if (typeof ts === 'string') {
|
|
59
|
+
// Assume already-formatted ISO; validate by round-tripping.
|
|
60
|
+
const parsed = new Date(ts);
|
|
61
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString();
|
|
62
|
+
}
|
|
63
|
+
if (typeof ts === 'number' && Number.isFinite(ts)) {
|
|
64
|
+
// Heuristic: < 1e12 means unix seconds (≤ year 33658); else millis.
|
|
65
|
+
const ms = ts < 1e12 ? ts * 1000 : ts;
|
|
66
|
+
const parsed = new Date(ms);
|
|
67
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString();
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
function renderLesson(lesson, index) {
|
|
72
|
+
const header = lesson.title || lesson.id || `Lesson ${index + 1}`;
|
|
73
|
+
const body = lesson.body ?? lesson.text ?? '';
|
|
74
|
+
const meta = [];
|
|
75
|
+
if (lesson.author)
|
|
76
|
+
meta.push(`author: ${lesson.author}`);
|
|
77
|
+
const iso = formatTimestamp(lesson.timestamp ?? lesson.ts);
|
|
78
|
+
if (iso)
|
|
79
|
+
meta.push(`at: ${iso}`);
|
|
80
|
+
if (lesson.id)
|
|
81
|
+
meta.push(`id: ${lesson.id}`);
|
|
82
|
+
const lines = [];
|
|
83
|
+
lines.push(`### ${header}`);
|
|
84
|
+
if (meta.length > 0)
|
|
85
|
+
lines.push(`*${meta.join(' · ')}*`);
|
|
86
|
+
lines.push('');
|
|
87
|
+
if (body) {
|
|
88
|
+
lines.push(String(body));
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
// No body — dump the raw JSON so nothing is silently lost.
|
|
92
|
+
lines.push('```json');
|
|
93
|
+
lines.push(JSON.stringify(lesson, null, 2));
|
|
94
|
+
lines.push('```');
|
|
95
|
+
}
|
|
96
|
+
lines.push('');
|
|
97
|
+
return lines.join('\n');
|
|
98
|
+
}
|
|
99
|
+
function renderRule(rule, index) {
|
|
100
|
+
const lines = [];
|
|
101
|
+
const header = rule.id || `Rule ${index + 1}`;
|
|
102
|
+
lines.push(`- **${header}** — ${rule.text ?? JSON.stringify(rule)}`);
|
|
103
|
+
const meta = [];
|
|
104
|
+
if (rule.author)
|
|
105
|
+
meta.push(`author: ${rule.author}`);
|
|
106
|
+
const iso = formatTimestamp(rule.timestamp ?? rule.ts);
|
|
107
|
+
if (iso)
|
|
108
|
+
meta.push(`at: ${iso}`);
|
|
109
|
+
if (meta.length > 0) {
|
|
110
|
+
lines.push(` *${meta.join(' · ')}*`);
|
|
111
|
+
}
|
|
112
|
+
return lines.join('\n');
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Project a pop.brain.shared doc to markdown.
|
|
116
|
+
*
|
|
117
|
+
* Pure function — given the same input JS object, returns the exact
|
|
118
|
+
* same string every time. No Date.now(), no environment reads, no I/O.
|
|
119
|
+
*
|
|
120
|
+
* @param doc The Automerge doc as a plain JS object (from Automerge.toJS)
|
|
121
|
+
* @param headCid Optional head CID to include in the header for traceability
|
|
122
|
+
*/
|
|
123
|
+
function projectShared(doc, headCid = null) {
|
|
124
|
+
const parts = [];
|
|
125
|
+
parts.push(GENERATED_BANNER);
|
|
126
|
+
parts.push('# Shared Agent Brain — `pop.brain.shared`');
|
|
127
|
+
if (headCid)
|
|
128
|
+
parts.push(`*Head CID: \`${headCid}\`*`);
|
|
129
|
+
parts.push('');
|
|
130
|
+
// Rules — canonical short-form list.
|
|
131
|
+
if (Array.isArray(doc.rules) && doc.rules.length > 0) {
|
|
132
|
+
parts.push('## Rules');
|
|
133
|
+
parts.push('');
|
|
134
|
+
parts.push(doc.rules.map((r, i) => renderRule(r, i)).join('\n'));
|
|
135
|
+
parts.push('');
|
|
136
|
+
}
|
|
137
|
+
// Operating constraints — short-form list, same renderer as rules.
|
|
138
|
+
if (Array.isArray(doc.operatingConstraints) && doc.operatingConstraints.length > 0) {
|
|
139
|
+
parts.push('## Operating Constraints');
|
|
140
|
+
parts.push('');
|
|
141
|
+
parts.push(doc.operatingConstraints.map((c, i) => renderRule(c, i)).join('\n'));
|
|
142
|
+
parts.push('');
|
|
143
|
+
}
|
|
144
|
+
// Lessons — long-form, one block per lesson. The primary field today.
|
|
145
|
+
// Tombstoned lessons (removed === true) are filtered out of the main
|
|
146
|
+
// Lessons section but surfaced in a short "Removed lessons" summary
|
|
147
|
+
// at the bottom so operators can see something was removed without
|
|
148
|
+
// the content polluting the main view.
|
|
149
|
+
const allLessons = Array.isArray(doc.lessons) ? doc.lessons : [];
|
|
150
|
+
const liveLessons = allLessons.filter(l => l?.removed !== true);
|
|
151
|
+
const tombstoned = allLessons.filter(l => l?.removed === true);
|
|
152
|
+
if (liveLessons.length > 0) {
|
|
153
|
+
parts.push('## Lessons');
|
|
154
|
+
parts.push('');
|
|
155
|
+
parts.push(liveLessons.map((l, i) => renderLesson(l, i)).join('\n'));
|
|
156
|
+
}
|
|
157
|
+
else if (doc.lessons !== undefined && allLessons.length === 0) {
|
|
158
|
+
parts.push('## Lessons');
|
|
159
|
+
parts.push('');
|
|
160
|
+
parts.push('*(no lessons yet)*');
|
|
161
|
+
parts.push('');
|
|
162
|
+
}
|
|
163
|
+
// Org state — key/value dump.
|
|
164
|
+
if (doc.orgState && typeof doc.orgState === 'object' && Object.keys(doc.orgState).length > 0) {
|
|
165
|
+
parts.push('## Org State');
|
|
166
|
+
parts.push('');
|
|
167
|
+
for (const [k, v] of Object.entries(doc.orgState)) {
|
|
168
|
+
parts.push(`- **${k}**: ${typeof v === 'object' ? JSON.stringify(v) : String(v)}`);
|
|
169
|
+
}
|
|
170
|
+
parts.push('');
|
|
171
|
+
}
|
|
172
|
+
// Removed-lessons summary — count + last 3 ids only. Keeps the
|
|
173
|
+
// projection clean while making removals visible. Operators can
|
|
174
|
+
// still inspect full tombstone metadata via `pop brain read`.
|
|
175
|
+
if (tombstoned.length > 0) {
|
|
176
|
+
parts.push('## Removed lessons');
|
|
177
|
+
parts.push('');
|
|
178
|
+
parts.push(`*(${tombstoned.length} lesson${tombstoned.length === 1 ? '' : 's'} tombstoned)*`);
|
|
179
|
+
parts.push('');
|
|
180
|
+
const lastThree = tombstoned.slice(-3).map(l => l?.id ?? '(no id)');
|
|
181
|
+
for (const id of lastThree)
|
|
182
|
+
parts.push(`- ${id}`);
|
|
183
|
+
parts.push('');
|
|
184
|
+
}
|
|
185
|
+
// Unknown top-level fields — dump as JSON so schema evolution doesn't
|
|
186
|
+
// drop data on the floor.
|
|
187
|
+
const known = new Set(['lessons', 'rules', 'operatingConstraints', 'orgState']);
|
|
188
|
+
const unknownKeys = Object.keys(doc).filter(k => !known.has(k));
|
|
189
|
+
if (unknownKeys.length > 0) {
|
|
190
|
+
parts.push('## Other fields');
|
|
191
|
+
parts.push('');
|
|
192
|
+
parts.push('```json');
|
|
193
|
+
const subset = {};
|
|
194
|
+
for (const k of unknownKeys)
|
|
195
|
+
subset[k] = doc[k];
|
|
196
|
+
parts.push(JSON.stringify(subset, null, 2));
|
|
197
|
+
parts.push('```');
|
|
198
|
+
parts.push('');
|
|
199
|
+
}
|
|
200
|
+
// Always render an empty-doc marker so snapshotting a fresh doc still
|
|
201
|
+
// produces a non-degenerate file.
|
|
202
|
+
if ((!doc.lessons || (Array.isArray(doc.lessons) && doc.lessons.length === 0)) &&
|
|
203
|
+
(!doc.rules || (Array.isArray(doc.rules) && doc.rules.length === 0)) &&
|
|
204
|
+
(!doc.operatingConstraints || (Array.isArray(doc.operatingConstraints) && doc.operatingConstraints.length === 0)) &&
|
|
205
|
+
(!doc.orgState || Object.keys(doc.orgState || {}).length === 0) &&
|
|
206
|
+
unknownKeys.length === 0) {
|
|
207
|
+
parts.push('*(brain doc is empty — no content to project yet)*');
|
|
208
|
+
parts.push('');
|
|
209
|
+
}
|
|
210
|
+
// Ensure trailing newline so the file is POSIX-clean.
|
|
211
|
+
return parts.join('\n').replace(/\n+$/, '\n');
|
|
212
|
+
}
|
|
213
|
+
const STANCE_EMOJI = {
|
|
214
|
+
support: '✅',
|
|
215
|
+
oppose: '❌',
|
|
216
|
+
concerned: '⚠️',
|
|
217
|
+
question: '❓',
|
|
218
|
+
neutral: '➖',
|
|
219
|
+
};
|
|
220
|
+
function renderDiscussionEntry(entry) {
|
|
221
|
+
const bits = [];
|
|
222
|
+
if (entry.author)
|
|
223
|
+
bits.push(`**${entry.author}**`);
|
|
224
|
+
if (entry.stance) {
|
|
225
|
+
const emoji = STANCE_EMOJI[entry.stance] ?? '';
|
|
226
|
+
bits.push(`${emoji} ${entry.stance}`);
|
|
227
|
+
}
|
|
228
|
+
const iso = formatTimestamp(entry.timestamp);
|
|
229
|
+
if (iso)
|
|
230
|
+
bits.push(`*${iso}*`);
|
|
231
|
+
const header = bits.join(' · ');
|
|
232
|
+
const body = entry.message ?? entry.ipfs ?? '';
|
|
233
|
+
if (header && body)
|
|
234
|
+
return `- ${header}: ${body}`;
|
|
235
|
+
if (header)
|
|
236
|
+
return `- ${header}`;
|
|
237
|
+
return `- ${body}`;
|
|
238
|
+
}
|
|
239
|
+
function renderProject(p) {
|
|
240
|
+
const parts = [];
|
|
241
|
+
parts.push(`### ${p.name || p.id}`);
|
|
242
|
+
parts.push('');
|
|
243
|
+
const meta = [];
|
|
244
|
+
meta.push(`**Stage**: ${p.stage}`);
|
|
245
|
+
if (p.proposedBy) {
|
|
246
|
+
// Prefer HB# label when present (prose-era); fall back to ISO
|
|
247
|
+
// timestamp from proposedAt when the project was CLI-created.
|
|
248
|
+
let suffix = '';
|
|
249
|
+
if (p.proposedAtHB)
|
|
250
|
+
suffix = ` (HB#${p.proposedAtHB})`;
|
|
251
|
+
else {
|
|
252
|
+
const iso = formatTimestamp(p.proposedAt);
|
|
253
|
+
if (iso)
|
|
254
|
+
suffix = ` (${iso})`;
|
|
255
|
+
}
|
|
256
|
+
meta.push(`**Proposed by**: ${p.proposedBy}${suffix}`);
|
|
257
|
+
}
|
|
258
|
+
const lastAdvance = formatTimestamp(p.lastStageAdvanceAt);
|
|
259
|
+
if (lastAdvance)
|
|
260
|
+
meta.push(`**Last advance**: ${lastAdvance}`);
|
|
261
|
+
parts.push(meta.join(' · '));
|
|
262
|
+
parts.push('');
|
|
263
|
+
if (p.brief) {
|
|
264
|
+
parts.push('**Brief**');
|
|
265
|
+
parts.push('');
|
|
266
|
+
parts.push(p.brief.startsWith('http') ? `> ${p.brief}` : p.brief);
|
|
267
|
+
parts.push('');
|
|
268
|
+
}
|
|
269
|
+
if (Array.isArray(p.discussion) && p.discussion.length > 0) {
|
|
270
|
+
parts.push('**Discussion**');
|
|
271
|
+
parts.push('');
|
|
272
|
+
for (const entry of p.discussion)
|
|
273
|
+
parts.push(renderDiscussionEntry(entry));
|
|
274
|
+
parts.push('');
|
|
275
|
+
}
|
|
276
|
+
if (p.taskPlan) {
|
|
277
|
+
parts.push('**Task plan**');
|
|
278
|
+
parts.push('');
|
|
279
|
+
parts.push(p.taskPlan);
|
|
280
|
+
parts.push('');
|
|
281
|
+
}
|
|
282
|
+
if (p.proposal) {
|
|
283
|
+
parts.push('**Proposal**');
|
|
284
|
+
parts.push('');
|
|
285
|
+
parts.push(p.proposal);
|
|
286
|
+
parts.push('');
|
|
287
|
+
}
|
|
288
|
+
if (p.retrospective) {
|
|
289
|
+
parts.push('**Retrospective**');
|
|
290
|
+
parts.push('');
|
|
291
|
+
parts.push(p.retrospective);
|
|
292
|
+
parts.push('');
|
|
293
|
+
}
|
|
294
|
+
// Unknown per-project fields get dumped so nothing silently drops.
|
|
295
|
+
const known = new Set([
|
|
296
|
+
'id', 'name', 'stage', 'proposedBy', 'proposedAtHB', 'proposedAt',
|
|
297
|
+
'lastStageAdvanceAt', 'brief',
|
|
298
|
+
'discussion', 'taskPlan', 'proposal', 'retrospective',
|
|
299
|
+
'removed', 'removedAt', 'removedBy', 'removedReason',
|
|
300
|
+
]);
|
|
301
|
+
const extra = Object.keys(p).filter(k => !known.has(k));
|
|
302
|
+
if (extra.length > 0) {
|
|
303
|
+
parts.push('*Extra fields*');
|
|
304
|
+
parts.push('');
|
|
305
|
+
parts.push('```json');
|
|
306
|
+
const subset = {};
|
|
307
|
+
for (const k of extra)
|
|
308
|
+
subset[k] = p[k];
|
|
309
|
+
parts.push(JSON.stringify(subset, null, 2));
|
|
310
|
+
parts.push('```');
|
|
311
|
+
parts.push('');
|
|
312
|
+
}
|
|
313
|
+
return parts.join('\n');
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Project a pop.brain.projects doc to markdown.
|
|
317
|
+
*
|
|
318
|
+
* Pure function — same input produces byte-identical output.
|
|
319
|
+
* Schema-tolerant: missing project fields are skipped, unknown
|
|
320
|
+
* top-level fields dump as JSON, empty doc renders a placeholder
|
|
321
|
+
* rather than crashing or returning an empty string.
|
|
322
|
+
*/
|
|
323
|
+
function projectProjects(doc, headCid = null) {
|
|
324
|
+
const parts = [];
|
|
325
|
+
parts.push(GENERATED_BANNER);
|
|
326
|
+
parts.push('# Collaborative Projects — `pop.brain.projects`');
|
|
327
|
+
if (headCid)
|
|
328
|
+
parts.push(`*Head CID: \`${headCid}\`*`);
|
|
329
|
+
parts.push('');
|
|
330
|
+
// Partition into live vs tombstoned. Only live projects render in
|
|
331
|
+
// the Summary table + per-project detail blocks. Tombstoned projects
|
|
332
|
+
// surface in a compact '## Removed projects' summary at the bottom
|
|
333
|
+
// with count + last 3 ids only — same shape as remove-lesson's
|
|
334
|
+
// '## Removed lessons' section in projectShared.
|
|
335
|
+
const allProjects = Array.isArray(doc.projects) ? doc.projects : [];
|
|
336
|
+
const liveProjects = allProjects.filter(p => p?.removed !== true);
|
|
337
|
+
const tombstonedProjects = allProjects.filter(p => p?.removed === true);
|
|
338
|
+
if (liveProjects.length === 0 && tombstonedProjects.length === 0) {
|
|
339
|
+
parts.push('*(no projects yet)*');
|
|
340
|
+
parts.push('');
|
|
341
|
+
}
|
|
342
|
+
else if (liveProjects.length === 0) {
|
|
343
|
+
parts.push('*(no live projects — all entries tombstoned)*');
|
|
344
|
+
parts.push('');
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
// Summary table (live only).
|
|
348
|
+
parts.push('## Summary');
|
|
349
|
+
parts.push('');
|
|
350
|
+
parts.push('| ID | Name | Stage |');
|
|
351
|
+
parts.push('|---|---|---|');
|
|
352
|
+
for (const p of liveProjects) {
|
|
353
|
+
const id = (p.id ?? '(no id)').replace(/\|/g, '\\|');
|
|
354
|
+
const name = (p.name ?? '(no name)').replace(/\|/g, '\\|');
|
|
355
|
+
const stage = (p.stage ?? '(unknown)').replace(/\|/g, '\\|');
|
|
356
|
+
parts.push(`| ${id} | ${name} | ${stage} |`);
|
|
357
|
+
}
|
|
358
|
+
parts.push('');
|
|
359
|
+
// Per-project detail blocks (live only).
|
|
360
|
+
parts.push('## Projects');
|
|
361
|
+
parts.push('');
|
|
362
|
+
for (const p of liveProjects) {
|
|
363
|
+
parts.push(renderProject(p));
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
// Removed-projects summary — count + last 3 ids only. Keeps the
|
|
367
|
+
// projection clean while making removals visible. Operators can
|
|
368
|
+
// still inspect full tombstone metadata via `pop brain read`.
|
|
369
|
+
if (tombstonedProjects.length > 0) {
|
|
370
|
+
parts.push('## Removed projects');
|
|
371
|
+
parts.push('');
|
|
372
|
+
parts.push(`*(${tombstonedProjects.length} project${tombstonedProjects.length === 1 ? '' : 's'} tombstoned)*`);
|
|
373
|
+
parts.push('');
|
|
374
|
+
const lastThree = tombstonedProjects.slice(-3).map(p => p?.id ?? '(no id)');
|
|
375
|
+
for (const id of lastThree)
|
|
376
|
+
parts.push(`- ${id}`);
|
|
377
|
+
parts.push('');
|
|
378
|
+
}
|
|
379
|
+
// Unknown top-level fields — dump as JSON so schema evolution doesn't
|
|
380
|
+
// drop data on the floor (same principle as projectShared).
|
|
381
|
+
const known = new Set(['projects']);
|
|
382
|
+
const unknownKeys = Object.keys(doc).filter(k => !known.has(k));
|
|
383
|
+
if (unknownKeys.length > 0) {
|
|
384
|
+
parts.push('## Other fields');
|
|
385
|
+
parts.push('');
|
|
386
|
+
parts.push('```json');
|
|
387
|
+
const subset = {};
|
|
388
|
+
for (const k of unknownKeys)
|
|
389
|
+
subset[k] = doc[k];
|
|
390
|
+
parts.push(JSON.stringify(subset, null, 2));
|
|
391
|
+
parts.push('```');
|
|
392
|
+
parts.push('');
|
|
393
|
+
}
|
|
394
|
+
return parts.join('\n').replace(/\n+$/, '\n');
|
|
395
|
+
}
|
|
396
|
+
const CHANGE_STATUS_EMOJI = {
|
|
397
|
+
proposed: '📝',
|
|
398
|
+
agreed: '✅',
|
|
399
|
+
modified: '✏️',
|
|
400
|
+
rejected: '❌',
|
|
401
|
+
filed: '🎯',
|
|
402
|
+
};
|
|
403
|
+
function renderProposedChange(c) {
|
|
404
|
+
const emoji = CHANGE_STATUS_EMOJI[c.status] ?? '•';
|
|
405
|
+
const parts = [];
|
|
406
|
+
parts.push(`- ${emoji} **${c.id}** — ${c.summary}`);
|
|
407
|
+
if (c.status === 'filed' && c.filedTaskId) {
|
|
408
|
+
parts.push(` *filed as task #${c.filedTaskId}*`);
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
parts.push(` *status: ${c.status}*`);
|
|
412
|
+
}
|
|
413
|
+
if (c.details) {
|
|
414
|
+
// Indent the detail body under the bullet so it renders as part of the item.
|
|
415
|
+
const indented = c.details.split('\n').map(line => ` ${line}`).join('\n');
|
|
416
|
+
parts.push(indented);
|
|
417
|
+
}
|
|
418
|
+
return parts.join('\n');
|
|
419
|
+
}
|
|
420
|
+
function renderRetroDiscussionEntry(entry) {
|
|
421
|
+
const bits = [];
|
|
422
|
+
if (entry.author)
|
|
423
|
+
bits.push(`**${entry.author}**`);
|
|
424
|
+
if (entry.hb)
|
|
425
|
+
bits.push(`HB#${entry.hb}`);
|
|
426
|
+
const iso = formatTimestamp(entry.timestamp);
|
|
427
|
+
if (iso)
|
|
428
|
+
bits.push(`*${iso}*`);
|
|
429
|
+
const header = bits.join(' · ');
|
|
430
|
+
const lines = [];
|
|
431
|
+
lines.push(`- ${header}`);
|
|
432
|
+
if (entry.message) {
|
|
433
|
+
const indented = entry.message.split('\n').map(line => ` ${line}`).join('\n');
|
|
434
|
+
lines.push(indented);
|
|
435
|
+
}
|
|
436
|
+
if (entry.votePerChange && Object.keys(entry.votePerChange).length > 0) {
|
|
437
|
+
const votes = Object.entries(entry.votePerChange)
|
|
438
|
+
.map(([changeId, vote]) => `${changeId}=${vote}`)
|
|
439
|
+
.join(', ');
|
|
440
|
+
lines.push(` *votes: ${votes}*`);
|
|
441
|
+
}
|
|
442
|
+
return lines.join('\n');
|
|
443
|
+
}
|
|
444
|
+
function renderRetro(r) {
|
|
445
|
+
const parts = [];
|
|
446
|
+
parts.push(`### ${r.id}`);
|
|
447
|
+
parts.push('');
|
|
448
|
+
const meta = [];
|
|
449
|
+
if (r.author)
|
|
450
|
+
meta.push(`**Author**: ${r.author}`);
|
|
451
|
+
if (r.hb)
|
|
452
|
+
meta.push(`**At HB**: #${r.hb}`);
|
|
453
|
+
if (r.window) {
|
|
454
|
+
const fromLabel = r.window.from ? `#${r.window.from}` : '?';
|
|
455
|
+
const toLabel = r.window.to ? `#${r.window.to}` : '?';
|
|
456
|
+
meta.push(`**Window**: ${fromLabel}..${toLabel}`);
|
|
457
|
+
}
|
|
458
|
+
if (r.status)
|
|
459
|
+
meta.push(`**Status**: ${r.status}`);
|
|
460
|
+
const createdIso = formatTimestamp(r.createdAt);
|
|
461
|
+
if (createdIso)
|
|
462
|
+
meta.push(`**Created**: ${createdIso}`);
|
|
463
|
+
if (r.closedAt) {
|
|
464
|
+
const closedIso = formatTimestamp(r.closedAt);
|
|
465
|
+
if (closedIso)
|
|
466
|
+
meta.push(`**Closed**: ${closedIso}`);
|
|
467
|
+
}
|
|
468
|
+
parts.push(meta.join(' · '));
|
|
469
|
+
parts.push('');
|
|
470
|
+
if (r.observations?.worked) {
|
|
471
|
+
parts.push('**What worked**');
|
|
472
|
+
parts.push('');
|
|
473
|
+
parts.push(r.observations.worked);
|
|
474
|
+
parts.push('');
|
|
475
|
+
}
|
|
476
|
+
if (r.observations?.didntWork) {
|
|
477
|
+
parts.push('**What didn\'t work**');
|
|
478
|
+
parts.push('');
|
|
479
|
+
parts.push(r.observations.didntWork);
|
|
480
|
+
parts.push('');
|
|
481
|
+
}
|
|
482
|
+
if (Array.isArray(r.proposedChanges) && r.proposedChanges.length > 0) {
|
|
483
|
+
parts.push('**Proposed changes**');
|
|
484
|
+
parts.push('');
|
|
485
|
+
for (const change of r.proposedChanges)
|
|
486
|
+
parts.push(renderProposedChange(change));
|
|
487
|
+
parts.push('');
|
|
488
|
+
}
|
|
489
|
+
if (Array.isArray(r.discussion) && r.discussion.length > 0) {
|
|
490
|
+
parts.push('**Discussion**');
|
|
491
|
+
parts.push('');
|
|
492
|
+
for (const entry of r.discussion)
|
|
493
|
+
parts.push(renderRetroDiscussionEntry(entry));
|
|
494
|
+
parts.push('');
|
|
495
|
+
}
|
|
496
|
+
// Dump unknown per-retro fields (same pattern as renderProject).
|
|
497
|
+
const known = new Set([
|
|
498
|
+
'id', 'author', 'hb', 'window', 'observations',
|
|
499
|
+
'proposedChanges', 'discussion', 'status', 'createdAt', 'closedAt',
|
|
500
|
+
'removed', 'removedAt', 'removedBy', 'removedReason',
|
|
501
|
+
]);
|
|
502
|
+
const extra = Object.keys(r).filter(k => !known.has(k));
|
|
503
|
+
if (extra.length > 0) {
|
|
504
|
+
parts.push('*Extra fields*');
|
|
505
|
+
parts.push('');
|
|
506
|
+
parts.push('```json');
|
|
507
|
+
const subset = {};
|
|
508
|
+
for (const k of extra)
|
|
509
|
+
subset[k] = r[k];
|
|
510
|
+
parts.push(JSON.stringify(subset, null, 2));
|
|
511
|
+
parts.push('```');
|
|
512
|
+
parts.push('');
|
|
513
|
+
}
|
|
514
|
+
return parts.join('\n');
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Project a pop.brain.retros doc to markdown.
|
|
518
|
+
*
|
|
519
|
+
* Same invariants as projectShared / projectProjects: pure function,
|
|
520
|
+
* schema-tolerant, empty doc renders a placeholder.
|
|
521
|
+
*/
|
|
522
|
+
function projectRetros(doc, headCid = null) {
|
|
523
|
+
const parts = [];
|
|
524
|
+
parts.push(GENERATED_BANNER);
|
|
525
|
+
parts.push('# Session Retros — `pop.brain.retros`');
|
|
526
|
+
if (headCid)
|
|
527
|
+
parts.push(`*Head CID: \`${headCid}\`*`);
|
|
528
|
+
parts.push('');
|
|
529
|
+
const allRetros = Array.isArray(doc.retros) ? doc.retros : [];
|
|
530
|
+
const liveRetros = allRetros.filter(r => r?.removed !== true);
|
|
531
|
+
const tombstonedRetros = allRetros.filter(r => r?.removed === true);
|
|
532
|
+
if (liveRetros.length === 0 && tombstonedRetros.length === 0) {
|
|
533
|
+
parts.push('*(no retros yet)*');
|
|
534
|
+
parts.push('');
|
|
535
|
+
}
|
|
536
|
+
else if (liveRetros.length === 0) {
|
|
537
|
+
parts.push('*(no live retros — all entries tombstoned)*');
|
|
538
|
+
parts.push('');
|
|
539
|
+
}
|
|
540
|
+
else {
|
|
541
|
+
// Summary table (live only).
|
|
542
|
+
parts.push('## Summary');
|
|
543
|
+
parts.push('');
|
|
544
|
+
parts.push('| ID | Author | Window | Status | Changes |');
|
|
545
|
+
parts.push('|---|---|---|---|---|');
|
|
546
|
+
for (const r of liveRetros) {
|
|
547
|
+
const id = (r.id ?? '(no id)').replace(/\|/g, '\\|');
|
|
548
|
+
const author = (r.author ?? '(unknown)').replace(/\|/g, '\\|');
|
|
549
|
+
const windowLabel = r.window
|
|
550
|
+
? `#${r.window.from ?? '?'}..#${r.window.to ?? '?'}`
|
|
551
|
+
: '(no window)';
|
|
552
|
+
const status = r.status ?? 'open';
|
|
553
|
+
const changeCount = Array.isArray(r.proposedChanges) ? r.proposedChanges.length : 0;
|
|
554
|
+
parts.push(`| ${id} | ${author} | ${windowLabel} | ${status} | ${changeCount} |`);
|
|
555
|
+
}
|
|
556
|
+
parts.push('');
|
|
557
|
+
// Per-retro detail blocks (live only).
|
|
558
|
+
parts.push('## Retros');
|
|
559
|
+
parts.push('');
|
|
560
|
+
for (const r of liveRetros)
|
|
561
|
+
parts.push(renderRetro(r));
|
|
562
|
+
}
|
|
563
|
+
if (tombstonedRetros.length > 0) {
|
|
564
|
+
parts.push('## Removed retros');
|
|
565
|
+
parts.push('');
|
|
566
|
+
parts.push(`*(${tombstonedRetros.length} retro${tombstonedRetros.length === 1 ? '' : 's'} tombstoned)*`);
|
|
567
|
+
parts.push('');
|
|
568
|
+
const lastThree = tombstonedRetros.slice(-3).map(r => r?.id ?? '(no id)');
|
|
569
|
+
for (const id of lastThree)
|
|
570
|
+
parts.push(`- ${id}`);
|
|
571
|
+
parts.push('');
|
|
572
|
+
}
|
|
573
|
+
// Unknown top-level fields — dump as JSON.
|
|
574
|
+
const known = new Set(['retros']);
|
|
575
|
+
const unknownKeys = Object.keys(doc).filter(k => !known.has(k));
|
|
576
|
+
if (unknownKeys.length > 0) {
|
|
577
|
+
parts.push('## Other fields');
|
|
578
|
+
parts.push('');
|
|
579
|
+
parts.push('```json');
|
|
580
|
+
const subset = {};
|
|
581
|
+
for (const k of unknownKeys)
|
|
582
|
+
subset[k] = doc[k];
|
|
583
|
+
parts.push(JSON.stringify(subset, null, 2));
|
|
584
|
+
parts.push('```');
|
|
585
|
+
parts.push('');
|
|
586
|
+
}
|
|
587
|
+
return parts.join('\n').replace(/\n+$/, '\n');
|
|
588
|
+
}
|
|
589
|
+
const BRAINSTORM_STATUS_EMOJI = {
|
|
590
|
+
open: '💭',
|
|
591
|
+
voting: '🗳️',
|
|
592
|
+
promoted: '🎯',
|
|
593
|
+
closed: '✅',
|
|
594
|
+
};
|
|
595
|
+
const VOTE_STANCE_EMOJI = {
|
|
596
|
+
support: '✅',
|
|
597
|
+
explore: '🤔',
|
|
598
|
+
oppose: '❌',
|
|
599
|
+
};
|
|
600
|
+
function renderIdea(idea, index) {
|
|
601
|
+
const parts = [];
|
|
602
|
+
const header = idea.id ? `### Idea: ${idea.id}` : `### Idea ${index + 1}`;
|
|
603
|
+
parts.push(header);
|
|
604
|
+
const meta = [];
|
|
605
|
+
if (idea.author)
|
|
606
|
+
meta.push(`author: ${idea.author}`);
|
|
607
|
+
const iso = formatTimestamp(idea.timestamp);
|
|
608
|
+
if (iso)
|
|
609
|
+
meta.push(`at: ${iso}`);
|
|
610
|
+
if (idea.priority)
|
|
611
|
+
meta.push(`priority: ${idea.priority}`);
|
|
612
|
+
if (meta.length > 0)
|
|
613
|
+
parts.push(`*${meta.join(' · ')}*`);
|
|
614
|
+
if (idea.message) {
|
|
615
|
+
parts.push('');
|
|
616
|
+
parts.push(idea.message);
|
|
617
|
+
}
|
|
618
|
+
// Vote tally
|
|
619
|
+
const votes = idea.votes && typeof idea.votes === 'object' ? idea.votes : {};
|
|
620
|
+
const voteEntries = Object.entries(votes);
|
|
621
|
+
if (voteEntries.length > 0) {
|
|
622
|
+
parts.push('');
|
|
623
|
+
parts.push('**Votes**:');
|
|
624
|
+
const tally = { support: 0, explore: 0, oppose: 0 };
|
|
625
|
+
for (const [addr, stance] of voteEntries) {
|
|
626
|
+
const emoji = VOTE_STANCE_EMOJI[stance] ?? '•';
|
|
627
|
+
parts.push(`- ${emoji} \`${addr}\` — ${stance}`);
|
|
628
|
+
if (tally[stance] != null)
|
|
629
|
+
tally[stance]++;
|
|
630
|
+
}
|
|
631
|
+
parts.push(`- **Tally**: ${tally.support} support · ${tally.explore} explore · ${tally.oppose} oppose`);
|
|
632
|
+
}
|
|
633
|
+
// Promotion marker
|
|
634
|
+
if (idea.promotedProjectId) {
|
|
635
|
+
parts.push('');
|
|
636
|
+
const promotedMeta = [`**Promoted to project**: \`${idea.promotedProjectId}\``];
|
|
637
|
+
if (idea.promotedBy)
|
|
638
|
+
promotedMeta.push(`by ${idea.promotedBy}`);
|
|
639
|
+
const pIso = formatTimestamp(idea.promotedAt);
|
|
640
|
+
if (pIso)
|
|
641
|
+
promotedMeta.push(`at ${pIso}`);
|
|
642
|
+
parts.push(promotedMeta.join(' · '));
|
|
643
|
+
}
|
|
644
|
+
return parts.join('\n');
|
|
645
|
+
}
|
|
646
|
+
function renderBrainstormDiscussionEntry(entry) {
|
|
647
|
+
const bits = [];
|
|
648
|
+
if (entry.author)
|
|
649
|
+
bits.push(`**${entry.author}**`);
|
|
650
|
+
const iso = formatTimestamp(entry.timestamp);
|
|
651
|
+
if (iso)
|
|
652
|
+
bits.push(`*${iso}*`);
|
|
653
|
+
const header = bits.join(' · ');
|
|
654
|
+
const lines = [];
|
|
655
|
+
lines.push(`- ${header}`);
|
|
656
|
+
if (entry.message) {
|
|
657
|
+
const indented = entry.message.split('\n').map((l) => ` ${l}`).join('\n');
|
|
658
|
+
lines.push(indented);
|
|
659
|
+
}
|
|
660
|
+
return lines.join('\n');
|
|
661
|
+
}
|
|
662
|
+
function renderBrainstorm(b) {
|
|
663
|
+
const parts = [];
|
|
664
|
+
const emoji = BRAINSTORM_STATUS_EMOJI[b.status] ?? '•';
|
|
665
|
+
parts.push(`## ${emoji} ${b.title || b.id}`);
|
|
666
|
+
parts.push('');
|
|
667
|
+
const meta = [];
|
|
668
|
+
meta.push(`**Status**: ${b.status}`);
|
|
669
|
+
if (b.author)
|
|
670
|
+
meta.push(`**Author**: ${b.author}`);
|
|
671
|
+
const iso = formatTimestamp(b.openedAt);
|
|
672
|
+
if (iso)
|
|
673
|
+
meta.push(`**Opened**: ${iso}`);
|
|
674
|
+
if (b.window) {
|
|
675
|
+
const from = b.window.from != null ? `#${b.window.from}` : '?';
|
|
676
|
+
const to = b.window.to != null ? `#${b.window.to}` : '?';
|
|
677
|
+
meta.push(`**Window**: ${from}..${to}`);
|
|
678
|
+
}
|
|
679
|
+
if (b.id)
|
|
680
|
+
meta.push(`**ID**: \`${b.id}\``);
|
|
681
|
+
parts.push(meta.join(' · '));
|
|
682
|
+
parts.push('');
|
|
683
|
+
if (b.prompt) {
|
|
684
|
+
parts.push('**Prompt**:');
|
|
685
|
+
parts.push('');
|
|
686
|
+
parts.push(b.prompt);
|
|
687
|
+
parts.push('');
|
|
688
|
+
}
|
|
689
|
+
// Ideas
|
|
690
|
+
const ideas = Array.isArray(b.ideas) ? b.ideas : [];
|
|
691
|
+
if (ideas.length > 0) {
|
|
692
|
+
parts.push('### Ideas');
|
|
693
|
+
parts.push('');
|
|
694
|
+
ideas.forEach((idea, i) => {
|
|
695
|
+
parts.push(renderIdea(idea, i));
|
|
696
|
+
parts.push('');
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
else {
|
|
700
|
+
parts.push('*(no ideas posted yet)*');
|
|
701
|
+
parts.push('');
|
|
702
|
+
}
|
|
703
|
+
// Discussion
|
|
704
|
+
const discussion = Array.isArray(b.discussion) ? b.discussion : [];
|
|
705
|
+
if (discussion.length > 0) {
|
|
706
|
+
parts.push('### Discussion');
|
|
707
|
+
parts.push('');
|
|
708
|
+
for (const entry of discussion)
|
|
709
|
+
parts.push(renderBrainstormDiscussionEntry(entry));
|
|
710
|
+
parts.push('');
|
|
711
|
+
}
|
|
712
|
+
// Close / promotion state
|
|
713
|
+
if (b.status === 'closed' && b.closedReason) {
|
|
714
|
+
parts.push(`**Closed reason**: ${b.closedReason}`);
|
|
715
|
+
parts.push('');
|
|
716
|
+
}
|
|
717
|
+
if (Array.isArray(b.promotedToProjectIds) && b.promotedToProjectIds.length > 0) {
|
|
718
|
+
parts.push('**Promoted projects**:');
|
|
719
|
+
for (const pid of b.promotedToProjectIds)
|
|
720
|
+
parts.push(`- \`${pid}\``);
|
|
721
|
+
parts.push('');
|
|
722
|
+
}
|
|
723
|
+
return parts.join('\n');
|
|
724
|
+
}
|
|
725
|
+
function projectBrainstorms(doc, headCid = null) {
|
|
726
|
+
const parts = [];
|
|
727
|
+
parts.push(GENERATED_BANNER);
|
|
728
|
+
parts.push('# Cross-Agent Brainstorms — `pop.brain.brainstorms`');
|
|
729
|
+
if (headCid)
|
|
730
|
+
parts.push(`*Head CID: \`${headCid}\`*`);
|
|
731
|
+
parts.push('');
|
|
732
|
+
const allBrainstorms = Array.isArray(doc.brainstorms) ? doc.brainstorms : [];
|
|
733
|
+
const live = allBrainstorms.filter((b) => b?.removed !== true);
|
|
734
|
+
const tombstoned = allBrainstorms.filter((b) => b?.removed === true);
|
|
735
|
+
if (live.length === 0 && tombstoned.length === 0) {
|
|
736
|
+
parts.push('*(no brainstorms yet — open one with `pop brain brainstorm-start --title ... --prompt ...`)*');
|
|
737
|
+
parts.push('');
|
|
738
|
+
}
|
|
739
|
+
else if (live.length === 0) {
|
|
740
|
+
parts.push('*(no live brainstorms — all entries tombstoned)*');
|
|
741
|
+
parts.push('');
|
|
742
|
+
}
|
|
743
|
+
else {
|
|
744
|
+
// Summary table — active states ordered before closed/promoted
|
|
745
|
+
const statusOrder = { voting: 0, open: 1, promoted: 2, closed: 3 };
|
|
746
|
+
const sorted = [...live].sort((a, b) => {
|
|
747
|
+
const aRank = statusOrder[a.status] ?? 99;
|
|
748
|
+
const bRank = statusOrder[b.status] ?? 99;
|
|
749
|
+
if (aRank !== bRank)
|
|
750
|
+
return aRank - bRank;
|
|
751
|
+
return (b.openedAt ?? 0) - (a.openedAt ?? 0);
|
|
752
|
+
});
|
|
753
|
+
parts.push('## Summary');
|
|
754
|
+
parts.push('');
|
|
755
|
+
parts.push('| Status | ID | Title | Author | Ideas | Votes |');
|
|
756
|
+
parts.push('|---|---|---|---|---|---|');
|
|
757
|
+
for (const b of sorted) {
|
|
758
|
+
const id = (b.id ?? '(no id)').replace(/\|/g, '\\|');
|
|
759
|
+
const title = (b.title ?? '(no title)').replace(/\|/g, '\\|');
|
|
760
|
+
const author = (b.author ?? '?').slice(0, 10);
|
|
761
|
+
const ideaCount = Array.isArray(b.ideas) ? b.ideas.length : 0;
|
|
762
|
+
const voteCount = (b.ideas || []).reduce((sum, idea) => {
|
|
763
|
+
const v = idea?.votes;
|
|
764
|
+
return sum + (v && typeof v === 'object' ? Object.keys(v).length : 0);
|
|
765
|
+
}, 0);
|
|
766
|
+
const statusEmoji = BRAINSTORM_STATUS_EMOJI[b.status] ?? '•';
|
|
767
|
+
parts.push(`| ${statusEmoji} ${b.status} | \`${id}\` | ${title} | ${author} | ${ideaCount} | ${voteCount} |`);
|
|
768
|
+
}
|
|
769
|
+
parts.push('');
|
|
770
|
+
parts.push('## Brainstorms');
|
|
771
|
+
parts.push('');
|
|
772
|
+
for (const b of sorted) {
|
|
773
|
+
parts.push(renderBrainstorm(b));
|
|
774
|
+
parts.push('---');
|
|
775
|
+
parts.push('');
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
if (tombstoned.length > 0) {
|
|
779
|
+
parts.push('## Removed brainstorms');
|
|
780
|
+
parts.push('');
|
|
781
|
+
parts.push(`*(${tombstoned.length} brainstorm${tombstoned.length === 1 ? '' : 's'} tombstoned)*`);
|
|
782
|
+
parts.push('');
|
|
783
|
+
for (const b of tombstoned.slice(-5)) {
|
|
784
|
+
parts.push(`- \`${b.id ?? '(no id)'}\`${b.removedReason ? ` — ${b.removedReason}` : ''}`);
|
|
785
|
+
}
|
|
786
|
+
parts.push('');
|
|
787
|
+
}
|
|
788
|
+
// Unknown top-level fields — dump as JSON
|
|
789
|
+
const known = new Set(['brainstorms']);
|
|
790
|
+
const unknownKeys = Object.keys(doc).filter((k) => !known.has(k));
|
|
791
|
+
if (unknownKeys.length > 0) {
|
|
792
|
+
parts.push('## Other fields');
|
|
793
|
+
parts.push('');
|
|
794
|
+
parts.push('```json');
|
|
795
|
+
const subset = {};
|
|
796
|
+
for (const k of unknownKeys)
|
|
797
|
+
subset[k] = doc[k];
|
|
798
|
+
parts.push(JSON.stringify(subset, null, 2));
|
|
799
|
+
parts.push('```');
|
|
800
|
+
parts.push('');
|
|
801
|
+
}
|
|
802
|
+
return parts.join('\n').replace(/\n+$/, '\n');
|
|
803
|
+
}
|
|
804
|
+
const PROJECTOR_REGISTRY = [
|
|
805
|
+
{
|
|
806
|
+
match: (id) => id === 'pop.brain.projects' || id.startsWith('pop.brain.projects.'),
|
|
807
|
+
fn: projectProjects,
|
|
808
|
+
},
|
|
809
|
+
{
|
|
810
|
+
match: (id) => id === 'pop.brain.retros' || id.startsWith('pop.brain.retros.'),
|
|
811
|
+
fn: projectRetros,
|
|
812
|
+
},
|
|
813
|
+
{
|
|
814
|
+
match: (id) => id === 'pop.brain.brainstorms' || id.startsWith('pop.brain.brainstorms.'),
|
|
815
|
+
fn: projectBrainstorms,
|
|
816
|
+
},
|
|
817
|
+
];
|
|
818
|
+
/**
|
|
819
|
+
* Look up the right projector for a docId. Falls through to
|
|
820
|
+
* projectShared as the default so every previously-written snapshot
|
|
821
|
+
* call still works.
|
|
822
|
+
*/
|
|
823
|
+
function projectForDoc(docId, doc, headCid) {
|
|
824
|
+
for (const entry of PROJECTOR_REGISTRY) {
|
|
825
|
+
if (entry.match(docId))
|
|
826
|
+
return entry.fn(doc, headCid);
|
|
827
|
+
}
|
|
828
|
+
return projectShared(doc, headCid);
|
|
829
|
+
}
|