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