@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,58 @@
1
+ /**
2
+ * Brain dynamic allowlist — derive the authorized-author set from on-chain
3
+ * organization membership instead of a hand-maintained JSON file.
4
+ *
5
+ * Task #330 (HB#312 Hudson directive): the static brain-allowlist.json
6
+ * blocks the "clone repo → onboard → apply → vouched → fully in" flow
7
+ * because new members have to be added to the JSON file by hand after
8
+ * their vouch lands. This module replaces the JSON with a subgraph-backed
9
+ * lookup of active members of the configured org.
10
+ *
11
+ * Design:
12
+ *
13
+ * - Cache the full member address set for ~5 minutes (addresses in
14
+ * lowercase). Invalidating per-address on every verify would hammer
15
+ * the subgraph; per-org batch fetch is the right granularity.
16
+ *
17
+ * - On network error, throw. Callers (the verify path) catch and fall
18
+ * back to the static allowlist. The fallback log is emitted by the
19
+ * caller, not here, because this module is pure lookup.
20
+ *
21
+ * - Org and chain are read from POP_DEFAULT_ORG / POP_DEFAULT_CHAIN
22
+ * (or POP_BRAIN_ORG / POP_BRAIN_CHAIN if set — a future multi-org
23
+ * brain layer will need per-doc org, but MVP is one-brain-per-org).
24
+ *
25
+ * - The subgraph query is a narrow extract of the triage.ts pattern:
26
+ * just organization.users with membershipStatus = Active, nothing
27
+ * else. No tasks, no proposals, no treasury.
28
+ *
29
+ * The JSON allowlist stays in place for:
30
+ * (1) fresh clones before the first subgraph request succeeds
31
+ * (2) offline / air-gapped operators
32
+ * (3) manual emergency overrides for keys outside the DAO
33
+ */
34
+ /**
35
+ * Fetch the active member address set for an org. Caches per (chainId, orgId).
36
+ * Throws on subgraph error — callers handle fallback.
37
+ *
38
+ * @param orgIdOrName Org identifier (POP_DEFAULT_ORG if undefined)
39
+ * @param chainId Chain ID (POP_DEFAULT_CHAIN if undefined)
40
+ * @param opts.ttlMs Override cache TTL (default 5 min)
41
+ */
42
+ export declare function fetchOrgMembers(orgIdOrName?: string, chainId?: number, opts?: {
43
+ ttlMs?: number;
44
+ }): Promise<Set<string>>;
45
+ /**
46
+ * Is the given address currently an active member of the configured org?
47
+ * Returns true if the address holds the member hat, false if not found.
48
+ * Throws on network error — caller handles fallback.
49
+ */
50
+ export declare function isOrgMember(address: string, orgIdOrName?: string, chainId?: number): Promise<boolean>;
51
+ /** Invalidate the cache (useful for tests and pop brain doctor refreshes). */
52
+ export declare function clearMembershipCache(): void;
53
+ /**
54
+ * Non-throwing variant: returns the member set or null on any error.
55
+ * Used by pop brain doctor to show current dynamic membership without
56
+ * bailing the whole health check.
57
+ */
58
+ export declare function tryFetchOrgMembers(orgIdOrName?: string, chainId?: number): Promise<Set<string> | null>;
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+ /**
3
+ * Brain dynamic allowlist — derive the authorized-author set from on-chain
4
+ * organization membership instead of a hand-maintained JSON file.
5
+ *
6
+ * Task #330 (HB#312 Hudson directive): the static brain-allowlist.json
7
+ * blocks the "clone repo → onboard → apply → vouched → fully in" flow
8
+ * because new members have to be added to the JSON file by hand after
9
+ * their vouch lands. This module replaces the JSON with a subgraph-backed
10
+ * lookup of active members of the configured org.
11
+ *
12
+ * Design:
13
+ *
14
+ * - Cache the full member address set for ~5 minutes (addresses in
15
+ * lowercase). Invalidating per-address on every verify would hammer
16
+ * the subgraph; per-org batch fetch is the right granularity.
17
+ *
18
+ * - On network error, throw. Callers (the verify path) catch and fall
19
+ * back to the static allowlist. The fallback log is emitted by the
20
+ * caller, not here, because this module is pure lookup.
21
+ *
22
+ * - Org and chain are read from POP_DEFAULT_ORG / POP_DEFAULT_CHAIN
23
+ * (or POP_BRAIN_ORG / POP_BRAIN_CHAIN if set — a future multi-org
24
+ * brain layer will need per-doc org, but MVP is one-brain-per-org).
25
+ *
26
+ * - The subgraph query is a narrow extract of the triage.ts pattern:
27
+ * just organization.users with membershipStatus = Active, nothing
28
+ * else. No tasks, no proposals, no treasury.
29
+ *
30
+ * The JSON allowlist stays in place for:
31
+ * (1) fresh clones before the first subgraph request succeeds
32
+ * (2) offline / air-gapped operators
33
+ * (3) manual emergency overrides for keys outside the DAO
34
+ */
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.fetchOrgMembers = fetchOrgMembers;
37
+ exports.isOrgMember = isOrgMember;
38
+ exports.clearMembershipCache = clearMembershipCache;
39
+ exports.tryFetchOrgMembers = tryFetchOrgMembers;
40
+ const subgraph_1 = require("@poa-box/cli/lib/subgraph");
41
+ const resolve_1 = require("@poa-box/cli/lib/resolve");
42
+ const networks_1 = require("@poa-box/cli/config/networks");
43
+ const MEMBERS_QUERY = `
44
+ query BrainMembers($orgId: Bytes!) {
45
+ organization(id: $orgId) {
46
+ users(first: 100) {
47
+ address
48
+ membershipStatus
49
+ }
50
+ }
51
+ }
52
+ `;
53
+ const cache = new Map();
54
+ const DEFAULT_TTL_MS = 5 * 60 * 1000;
55
+ function cacheKey(orgId, chainId) {
56
+ return `${chainId}:${orgId.toLowerCase()}`;
57
+ }
58
+ /**
59
+ * Fetch the active member address set for an org. Caches per (chainId, orgId).
60
+ * Throws on subgraph error — callers handle fallback.
61
+ *
62
+ * @param orgIdOrName Org identifier (POP_DEFAULT_ORG if undefined)
63
+ * @param chainId Chain ID (POP_DEFAULT_CHAIN if undefined)
64
+ * @param opts.ttlMs Override cache TTL (default 5 min)
65
+ */
66
+ async function fetchOrgMembers(orgIdOrName, chainId, opts = {}) {
67
+ const effectiveChain = chainId ?? (process.env.POP_BRAIN_CHAIN ? Number(process.env.POP_BRAIN_CHAIN) : undefined);
68
+ const network = (0, networks_1.resolveNetworkConfig)(effectiveChain);
69
+ const resolved = await (0, resolve_1.resolveOrgModules)(orgIdOrName ?? process.env.POP_BRAIN_ORG ?? process.env.POP_DEFAULT_ORG, network.chainId);
70
+ const key = cacheKey(resolved.orgId, network.chainId);
71
+ const ttl = opts.ttlMs ?? DEFAULT_TTL_MS;
72
+ const hit = cache.get(key);
73
+ if (hit && hit.expiresAt > Date.now()) {
74
+ return hit.members;
75
+ }
76
+ const result = await (0, subgraph_1.query)(MEMBERS_QUERY, { orgId: resolved.orgId }, network.chainId);
77
+ const org = result?.organization;
78
+ if (!org) {
79
+ throw new Error(`Brain membership: org ${resolved.orgId} not found in subgraph`);
80
+ }
81
+ const members = new Set();
82
+ for (const u of org.users || []) {
83
+ if (u.membershipStatus === 'Active' && u.address) {
84
+ members.add(String(u.address).toLowerCase());
85
+ }
86
+ }
87
+ cache.set(key, { members, expiresAt: Date.now() + ttl });
88
+ return members;
89
+ }
90
+ /**
91
+ * Is the given address currently an active member of the configured org?
92
+ * Returns true if the address holds the member hat, false if not found.
93
+ * Throws on network error — caller handles fallback.
94
+ */
95
+ async function isOrgMember(address, orgIdOrName, chainId) {
96
+ const members = await fetchOrgMembers(orgIdOrName, chainId);
97
+ return members.has(address.toLowerCase());
98
+ }
99
+ /** Invalidate the cache (useful for tests and pop brain doctor refreshes). */
100
+ function clearMembershipCache() {
101
+ cache.clear();
102
+ }
103
+ /**
104
+ * Non-throwing variant: returns the member set or null on any error.
105
+ * Used by pop brain doctor to show current dynamic membership without
106
+ * bailing the whole health check.
107
+ */
108
+ async function tryFetchOrgMembers(orgIdOrName, chainId) {
109
+ try {
110
+ return await fetchOrgMembers(orgIdOrName, chainId);
111
+ }
112
+ catch {
113
+ return null;
114
+ }
115
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Brain projects migration — one-shot parser for agent/brain/Knowledge/projects.md
3
+ * into a ProjectsBrainDoc.
4
+ *
5
+ * This parses the "Active Projects" H2 section only. Each '### <Name>'
6
+ * subsection becomes a BrainProject entry. Structured fields we extract:
7
+ *
8
+ * - Stage — from "- **Stage**: X"
9
+ * - Proposed by — from "- **Proposed by**: <author> (HB#N)"
10
+ * - Brief — from "- **Brief**: ..." (possibly multi-line with
11
+ * continuation indented under the bullet)
12
+ *
13
+ * Unstructured fields we preserve as raw markdown strings attached to
14
+ * the project:
15
+ *
16
+ * - Discussion → taskPlan field, prefixed '(Raw markdown from
17
+ * projects.md — parse in v2)' (TODO: split into
18
+ * ProjectDiscussionEntry[] with author / stance /
19
+ * timestamp fields when we have time)
20
+ * - Task Plan → concatenated into taskPlan
21
+ * - Proposal → concatenated into proposal
22
+ * - Retrospective → concatenated into retrospective
23
+ *
24
+ * Same discipline as parseSharedMarkdown from brain-migrate.ts: pure
25
+ * function, no I/O, no Date.now, caller-supplied MigrationContext for
26
+ * default timestamps + author.
27
+ *
28
+ * Scope: "Active Projects" H2 only. The "On-Chain Projects" table, the
29
+ * "How to Propose a Project" H2, the "Completed Projects" H2, and the
30
+ * "Lessons Learned" H2 are all skipped — they're prose / metadata, not
31
+ * projects-to-migrate.
32
+ */
33
+ import type { ProjectsBrainDoc } from './brain-projections';
34
+ import type { MigrationContext } from './brain-migrate';
35
+ /**
36
+ * Parse projects.md → ProjectsBrainDoc. Pure function.
37
+ *
38
+ * Only looks at the "Active Projects" H2 section. Each H3 becomes a
39
+ * BrainProject with structured stage/proposedBy/proposedAtHB/brief
40
+ * and a raw-markdown taskPlan preserving Discussion + Task Plan
41
+ * content for later structured parsing.
42
+ */
43
+ export declare function parseProjectsMarkdown(raw: string, ctx: MigrationContext): ProjectsBrainDoc;
@@ -0,0 +1,247 @@
1
+ "use strict";
2
+ /**
3
+ * Brain projects migration — one-shot parser for agent/brain/Knowledge/projects.md
4
+ * into a ProjectsBrainDoc.
5
+ *
6
+ * This parses the "Active Projects" H2 section only. Each '### <Name>'
7
+ * subsection becomes a BrainProject entry. Structured fields we extract:
8
+ *
9
+ * - Stage — from "- **Stage**: X"
10
+ * - Proposed by — from "- **Proposed by**: <author> (HB#N)"
11
+ * - Brief — from "- **Brief**: ..." (possibly multi-line with
12
+ * continuation indented under the bullet)
13
+ *
14
+ * Unstructured fields we preserve as raw markdown strings attached to
15
+ * the project:
16
+ *
17
+ * - Discussion → taskPlan field, prefixed '(Raw markdown from
18
+ * projects.md — parse in v2)' (TODO: split into
19
+ * ProjectDiscussionEntry[] with author / stance /
20
+ * timestamp fields when we have time)
21
+ * - Task Plan → concatenated into taskPlan
22
+ * - Proposal → concatenated into proposal
23
+ * - Retrospective → concatenated into retrospective
24
+ *
25
+ * Same discipline as parseSharedMarkdown from brain-migrate.ts: pure
26
+ * function, no I/O, no Date.now, caller-supplied MigrationContext for
27
+ * default timestamps + author.
28
+ *
29
+ * Scope: "Active Projects" H2 only. The "On-Chain Projects" table, the
30
+ * "How to Propose a Project" H2, the "Completed Projects" H2, and the
31
+ * "Lessons Learned" H2 are all skipped — they're prose / metadata, not
32
+ * projects-to-migrate.
33
+ */
34
+ Object.defineProperty(exports, "__esModule", { value: true });
35
+ exports.parseProjectsMarkdown = parseProjectsMarkdown;
36
+ const STAGE_WORDS = {
37
+ propose: 'propose',
38
+ discuss: 'discuss',
39
+ plan: 'plan',
40
+ vote: 'vote',
41
+ execute: 'execute',
42
+ review: 'review',
43
+ ship: 'ship',
44
+ };
45
+ const ACTIVE_PROJECTS_H2_RE = /^##\s+Active Projects\b/i;
46
+ const H2_RE = /^##\s+/;
47
+ const H3_RE = /^###\s+/;
48
+ const HB_TAG_RE = /\(\s*HB#(\d+)(?:\s*,\s*([^\s)]+))?\s*\)/i;
49
+ function slugify(s) {
50
+ return s
51
+ .toLowerCase()
52
+ .replace(/[^a-z0-9]+/g, '-')
53
+ .replace(/^-+|-+$/g, '')
54
+ .slice(0, 60);
55
+ }
56
+ /**
57
+ * Extract the "Active Projects" H2 block as a list of lines. Returns
58
+ * an empty array if the section isn't found. Fence-aware (won't split
59
+ * on `### ` inside a code block).
60
+ */
61
+ function extractActiveProjectsBlock(raw) {
62
+ const lines = raw.split(/\r?\n/);
63
+ const out = [];
64
+ let inside = false;
65
+ let inFence = false;
66
+ for (const line of lines) {
67
+ if (/^```/.test(line)) {
68
+ inFence = !inFence;
69
+ if (inside)
70
+ out.push(line);
71
+ continue;
72
+ }
73
+ if (!inFence && H2_RE.test(line)) {
74
+ if (ACTIVE_PROJECTS_H2_RE.test(line)) {
75
+ inside = true;
76
+ continue;
77
+ }
78
+ if (inside)
79
+ break; // Hit the next H2 — end of section.
80
+ }
81
+ if (inside)
82
+ out.push(line);
83
+ }
84
+ return out;
85
+ }
86
+ /**
87
+ * Split an Active Projects block into per-project sections keyed by
88
+ * the H3 header. Fence-aware.
89
+ */
90
+ function splitProjectSections(lines) {
91
+ const sections = [];
92
+ let current = null;
93
+ let inFence = false;
94
+ for (const line of lines) {
95
+ if (/^```/.test(line)) {
96
+ inFence = !inFence;
97
+ if (current)
98
+ current.body.push(line);
99
+ continue;
100
+ }
101
+ if (!inFence && H3_RE.test(line)) {
102
+ if (current)
103
+ sections.push(current);
104
+ current = { header: line.replace(H3_RE, '').trim(), body: [] };
105
+ continue;
106
+ }
107
+ if (current)
108
+ current.body.push(line);
109
+ }
110
+ if (current)
111
+ sections.push(current);
112
+ return sections;
113
+ }
114
+ /**
115
+ * Parse one project's body lines into structured fields. The shape of
116
+ * the hand-written projects.md body is a flat list of bullets, some of
117
+ * which have nested continuation lines and some of which have nested
118
+ * sub-bullets (Discussion / Task Plan). We scan line by line and use
119
+ * the '- **Field**:' prefix as a section marker.
120
+ */
121
+ function parseProjectBody(header, lines, ctx) {
122
+ const id = slugify(header) || 'project';
123
+ const project = {
124
+ id,
125
+ name: header,
126
+ stage: 'propose', // Default — overwritten if a Stage line is found.
127
+ };
128
+ let section = 'none';
129
+ const accum = {
130
+ brief: [],
131
+ discussion: [],
132
+ taskPlan: [],
133
+ proposal: [],
134
+ retrospective: [],
135
+ };
136
+ for (const rawLine of lines) {
137
+ const line = rawLine;
138
+ // New bullet — dispatch based on the field name.
139
+ const bulletMatch = /^-\s+\*\*([^*]+)\*\*:?\s*(.*)$/.exec(line);
140
+ if (bulletMatch) {
141
+ const fieldRaw = bulletMatch[1].trim().toLowerCase();
142
+ const rest = bulletMatch[2] ?? '';
143
+ // Stage line — extract value directly; do not push to any section.
144
+ if (fieldRaw === 'stage') {
145
+ const stageWord = rest.trim().toLowerCase().split(/\s+/)[0];
146
+ const normalized = STAGE_WORDS[stageWord];
147
+ if (normalized)
148
+ project.stage = normalized;
149
+ section = 'none';
150
+ continue;
151
+ }
152
+ // Proposed by: "<author> (HB#N)"
153
+ if (fieldRaw === 'proposed by') {
154
+ const hb = HB_TAG_RE.exec(rest);
155
+ if (hb) {
156
+ const hbN = Number(hb[1]);
157
+ const authorInTag = hb[2] ?? null;
158
+ project.proposedAtHB = hbN;
159
+ project.proposedBy = authorInTag ?? rest.replace(HB_TAG_RE, '').trim() ?? ctx.defaultAuthor;
160
+ if (ctx.timestampForHB)
161
+ project.proposedAt = ctx.timestampForHB(hbN);
162
+ }
163
+ else {
164
+ project.proposedBy = rest.trim() || ctx.defaultAuthor;
165
+ }
166
+ section = 'none';
167
+ continue;
168
+ }
169
+ // Brief — multi-line accumulator.
170
+ if (fieldRaw === 'brief') {
171
+ section = 'brief';
172
+ if (rest.trim())
173
+ accum.brief.push(rest.trim());
174
+ continue;
175
+ }
176
+ // Discussion — nested bullets preserved as raw markdown.
177
+ if (fieldRaw === 'discussion') {
178
+ section = 'discussion';
179
+ if (rest.trim())
180
+ accum.discussion.push(rest.trim());
181
+ continue;
182
+ }
183
+ // Task plan — same treatment.
184
+ if (fieldRaw === 'task plan') {
185
+ section = 'taskPlan';
186
+ if (rest.trim())
187
+ accum.taskPlan.push(rest.trim());
188
+ continue;
189
+ }
190
+ if (fieldRaw === 'proposal') {
191
+ section = 'proposal';
192
+ if (rest.trim())
193
+ accum.proposal.push(rest.trim());
194
+ continue;
195
+ }
196
+ if (fieldRaw === 'retrospective') {
197
+ section = 'retrospective';
198
+ if (rest.trim())
199
+ accum.retrospective.push(rest.trim());
200
+ continue;
201
+ }
202
+ // Unknown named bullet (e.g. "- **Execution Status**:") — push
203
+ // to the current section as a raw line so nothing is dropped.
204
+ if (section !== 'none')
205
+ accum[section].push(line);
206
+ continue;
207
+ }
208
+ // Continuation line (indented or bare text). Push to the current
209
+ // section if we're in one; drop otherwise.
210
+ if (section !== 'none' && line.trim() !== '') {
211
+ accum[section].push(line);
212
+ }
213
+ }
214
+ if (accum.brief.length > 0)
215
+ project.brief = accum.brief.join('\n').trim();
216
+ // Compose discussion + task plan into the taskPlan field as raw
217
+ // markdown for MVP. The structured ProjectDiscussionEntry[] parse
218
+ // is v2 work.
219
+ const discussionMd = accum.discussion.length > 0
220
+ ? '**Discussion (raw)**\n' + accum.discussion.join('\n').trim()
221
+ : '';
222
+ const taskPlanMd = accum.taskPlan.length > 0
223
+ ? '**Task plan (raw)**\n' + accum.taskPlan.join('\n').trim()
224
+ : '';
225
+ const combined = [discussionMd, taskPlanMd].filter(s => s).join('\n\n');
226
+ if (combined)
227
+ project.taskPlan = combined;
228
+ if (accum.proposal.length > 0)
229
+ project.proposal = accum.proposal.join('\n').trim();
230
+ if (accum.retrospective.length > 0)
231
+ project.retrospective = accum.retrospective.join('\n').trim();
232
+ return project;
233
+ }
234
+ /**
235
+ * Parse projects.md → ProjectsBrainDoc. Pure function.
236
+ *
237
+ * Only looks at the "Active Projects" H2 section. Each H3 becomes a
238
+ * BrainProject with structured stage/proposedBy/proposedAtHB/brief
239
+ * and a raw-markdown taskPlan preserving Discussion + Task Plan
240
+ * content for later structured parsing.
241
+ */
242
+ function parseProjectsMarkdown(raw, ctx) {
243
+ const block = extractActiveProjectsBlock(raw);
244
+ const sections = splitProjectSections(block);
245
+ const projects = sections.map(s => parseProjectBody(s.header, s.body, ctx));
246
+ return { projects };
247
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Brain migration — one-shot parser that converts the hand-written
3
+ * agent/brain/Knowledge/shared.md (and siblings) into a structured
4
+ * SharedBrainDoc for seeding the Automerge CRDT layer.
5
+ *
6
+ * This is step 8 of the brain plan (cheeky-nibbling-raven.md).
7
+ * After this file has been run once on each hand-written knowledge
8
+ * file, the CRDT substrate is the source of truth and the hand-written
9
+ * files become historical snapshots.
10
+ *
11
+ * ## Design notes
12
+ *
13
+ * - **Pure function.** parseSharedMarkdown takes a raw string and
14
+ * returns a SharedBrainDoc. No I/O, no Date.now, no environment
15
+ * reads. The CLI command handles all I/O and timestamp-from-now
16
+ * decisions.
17
+ *
18
+ * - **Heuristic, not canonical.** This is a one-shot parse; we're
19
+ * allowed to lose fidelity on layout details as long as the
20
+ * content content survives. Each H2 section becomes one entry.
21
+ *
22
+ * - **Short sections become rules, long sections become lessons.**
23
+ * A rule is a short-form policy item (one to a few lines of plain
24
+ * text, often bullet-led). A lesson is a longer structured
25
+ * narrative. The heuristic: if a section's body has more than 200
26
+ * characters OR contains a code fence, it's a lesson; otherwise
27
+ * it's a rule. This maps the existing hand-written file's mix of
28
+ * short policy bullets and long narrative notes to the brain doc
29
+ * schema cleanly.
30
+ *
31
+ * - **HB# tags → timestamps.** Lines containing `HB#N` or `(HB#N,
32
+ * author)` extract both the heartbeat number and the author.
33
+ * Timestamps are derived from HB# via a caller-provided anchor
34
+ * (see MigrationContext below) rather than guessed — keeps the
35
+ * parser pure.
36
+ *
37
+ * - **Code fences are preserved verbatim.** We do NOT split on ##
38
+ * headers inside a ``` block — that would corrupt embedded code
39
+ * samples. Tracked via a simple in-fence toggle.
40
+ */
41
+ import type { SharedBrainDoc } from './brain-projections';
42
+ export interface MigrationContext {
43
+ /**
44
+ * Default author to assign to sections where no HB#/author tag was
45
+ * found in the source. Typically the agent running the migration.
46
+ */
47
+ defaultAuthor: string;
48
+ /**
49
+ * Unix-seconds timestamp representing "now" for the migration run.
50
+ * Sections without their own HB# tag get this timestamp — a
51
+ * reasonable proxy for "discovered at migration time."
52
+ */
53
+ defaultTimestamp: number;
54
+ /**
55
+ * Caller-supplied function that turns a heartbeat number into a
56
+ * unix-seconds timestamp. Pass in the "best guess" curve, typically
57
+ * `now - (currentHB - N) * 15 * 60`. If omitted, all HB# tagged
58
+ * sections fall back to defaultTimestamp.
59
+ */
60
+ timestampForHB?: (hb: number) => number;
61
+ }
62
+ export interface ParsedBrainDoc extends SharedBrainDoc {
63
+ rules: NonNullable<SharedBrainDoc['rules']>;
64
+ lessons: NonNullable<SharedBrainDoc['lessons']>;
65
+ }
66
+ /**
67
+ * Parse the hand-written shared.md (or similar) into a SharedBrainDoc.
68
+ *
69
+ * Dispatches to `parseModernGeneratedMd` for modern snapshots produced by
70
+ * `pop brain snapshot` (detected via the DO-NOT-HAND-EDIT banner), or to
71
+ * the legacy H2-section scanner for hand-written shared.md files.
72
+ *
73
+ * Pure function — no I/O, no Date.now, no env reads. The caller must
74
+ * supply the migration context (default author, default timestamp,
75
+ * optional HB→timestamp function).
76
+ */
77
+ export declare function parseSharedMarkdown(raw: string, ctx: MigrationContext): ParsedBrainDoc;