@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,190 @@
1
+ "use strict";
2
+ /**
3
+ * pop brain search — keyword / tag / author / HB filters over brain lesson docs.
4
+ *
5
+ * Task #347 (Retro #1 fallback #6). The pop.brain.shared / pop.brain.lessons
6
+ * doc has grown past the "cold read is cheap" threshold. Agents reaching for
7
+ * "is there a lesson about X" were grepping heartbeat-log.md as a faster
8
+ * proxy, which defeated the purpose of the canonical lesson substrate.
9
+ * This command makes lesson search cheaper than log grep.
10
+ *
11
+ * Filters compose as AND:
12
+ * --query case-insensitive substring match over title + body
13
+ * --tag exact tag match (one tag per invocation; run twice to AND)
14
+ * --author exact author match (0x lowercase)
15
+ * --since HB number: only lessons with timestamp >= the HB's wall-clock.
16
+ * Heuristic — the HB number is not stored on the lesson; we
17
+ * approximate by comparing the lesson timestamp against a
18
+ * provided unix-seconds lower bound passed as --since-ts.
19
+ *
20
+ * Output is ranked by timestamp descending (most recent first) since that's
21
+ * the useful default for "what happened lately about X". JSON mode for
22
+ * programmatic consumption.
23
+ */
24
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
25
+ if (k2 === undefined) k2 = k;
26
+ var desc = Object.getOwnPropertyDescriptor(m, k);
27
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
28
+ desc = { enumerable: true, get: function() { return m[k]; } };
29
+ }
30
+ Object.defineProperty(o, k2, desc);
31
+ }) : (function(o, m, k, k2) {
32
+ if (k2 === undefined) k2 = k;
33
+ o[k2] = m[k];
34
+ }));
35
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
36
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
37
+ }) : function(o, v) {
38
+ o["default"] = v;
39
+ });
40
+ var __importStar = (this && this.__importStar) || (function () {
41
+ var ownKeys = function(o) {
42
+ ownKeys = Object.getOwnPropertyNames || function (o) {
43
+ var ar = [];
44
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
45
+ return ar;
46
+ };
47
+ return ownKeys(o);
48
+ };
49
+ return function (mod) {
50
+ if (mod && mod.__esModule) return mod;
51
+ var result = {};
52
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
53
+ __setModuleDefault(result, mod);
54
+ return result;
55
+ };
56
+ })();
57
+ Object.defineProperty(exports, "__esModule", { value: true });
58
+ exports.searchHandler = void 0;
59
+ const brain_1 = require("../../lib/brain");
60
+ const output = __importStar(require("@poa-box/cli/lib/output"));
61
+ function firstSentence(body) {
62
+ if (!body)
63
+ return '';
64
+ const trimmed = body.trim();
65
+ const match = trimmed.match(/^(.{1,160}?[.!?])(\s|$)/);
66
+ if (match)
67
+ return match[1];
68
+ // No sentence terminator in first 160 chars — take first line or 160 chars.
69
+ const firstLine = trimmed.split('\n')[0];
70
+ return firstLine.length > 160 ? firstLine.slice(0, 157) + '…' : firstLine;
71
+ }
72
+ exports.searchHandler = {
73
+ builder: (yargs) => yargs
74
+ .option('doc', {
75
+ describe: 'Brain document ID (e.g. pop.brain.shared)',
76
+ type: 'string',
77
+ demandOption: true,
78
+ })
79
+ .option('query', {
80
+ describe: 'Case-insensitive substring match over title + body',
81
+ type: 'string',
82
+ })
83
+ .option('tag', {
84
+ describe: 'Filter to lessons whose tags include this exact string',
85
+ type: 'string',
86
+ })
87
+ .option('author', {
88
+ describe: 'Filter to lessons by this author address (0x lowercase)',
89
+ type: 'string',
90
+ })
91
+ .option('since-ts', {
92
+ describe: 'Only lessons with timestamp >= this unix-seconds value',
93
+ type: 'number',
94
+ })
95
+ .option('limit', {
96
+ describe: 'Max number of lessons to return (default 10)',
97
+ type: 'number',
98
+ default: 10,
99
+ }),
100
+ handler: async (argv) => {
101
+ try {
102
+ const { doc: currentDoc } = await (0, brain_1.openBrainDoc)(argv.doc);
103
+ const lessons = Array.isArray(currentDoc?.lessons) ? currentDoc.lessons : [];
104
+ const queryLower = argv.query ? argv.query.toLowerCase() : null;
105
+ const wantTag = argv.tag ?? null;
106
+ const wantAuthor = argv.author ? argv.author.toLowerCase() : null;
107
+ const sinceTs = typeof argv.sinceTs === 'number' ? argv.sinceTs : null;
108
+ const matched = lessons.filter((lesson) => {
109
+ if (!lesson || lesson.removed === true)
110
+ return false;
111
+ if (queryLower) {
112
+ const haystack = `${lesson.title ?? ''}\n${lesson.body ?? lesson.text ?? ''}`.toLowerCase();
113
+ if (!haystack.includes(queryLower))
114
+ return false;
115
+ }
116
+ if (wantTag) {
117
+ const tags = Array.isArray(lesson.tags) ? lesson.tags : [];
118
+ if (!tags.some((t) => t === wantTag))
119
+ return false;
120
+ }
121
+ if (wantAuthor) {
122
+ const author = typeof lesson.author === 'string' ? lesson.author.toLowerCase() : '';
123
+ if (author !== wantAuthor)
124
+ return false;
125
+ }
126
+ if (sinceTs !== null) {
127
+ const ts = typeof lesson.timestamp === 'number' ? lesson.timestamp : 0;
128
+ if (ts < sinceTs)
129
+ return false;
130
+ }
131
+ return true;
132
+ });
133
+ // Sort by timestamp descending. Non-numeric timestamps sort last.
134
+ matched.sort((a, b) => {
135
+ const ta = typeof a.timestamp === 'number' ? a.timestamp : 0;
136
+ const tb = typeof b.timestamp === 'number' ? b.timestamp : 0;
137
+ return tb - ta;
138
+ });
139
+ const limited = matched.slice(0, argv.limit ?? 10);
140
+ if (output.isJsonMode()) {
141
+ output.json({
142
+ docId: argv.doc,
143
+ filters: {
144
+ query: argv.query ?? null,
145
+ tag: argv.tag ?? null,
146
+ author: argv.author ?? null,
147
+ sinceTs: sinceTs,
148
+ },
149
+ totalMatched: matched.length,
150
+ returned: limited.length,
151
+ lessons: limited.map((l) => ({
152
+ id: l.id ?? null,
153
+ title: l.title ?? null,
154
+ author: l.author ?? null,
155
+ timestamp: l.timestamp ?? null,
156
+ tags: Array.isArray(l.tags) ? l.tags : [],
157
+ summary: firstSentence(l.body ?? l.text ?? ''),
158
+ })),
159
+ });
160
+ }
161
+ else {
162
+ console.log('');
163
+ console.log(` ${matched.length} matching lesson${matched.length === 1 ? '' : 's'} in ${argv.doc}` +
164
+ (matched.length > limited.length ? ` (showing most recent ${limited.length})` : ''));
165
+ console.log('');
166
+ if (limited.length === 0) {
167
+ console.log(' (no matches)');
168
+ console.log('');
169
+ }
170
+ for (const l of limited) {
171
+ const iso = typeof l.timestamp === 'number' ? new Date(l.timestamp * 1000).toISOString() : '?';
172
+ const tags = Array.isArray(l.tags) && l.tags.length > 0 ? ` [${l.tags.join(', ')}]` : '';
173
+ console.log(` • ${l.title ?? l.id ?? '(no title)'}${tags}`);
174
+ console.log(` ${l.author ?? '?'} · ${iso}`);
175
+ const summary = firstSentence(l.body ?? l.text ?? '');
176
+ if (summary)
177
+ console.log(` ${summary}`);
178
+ console.log('');
179
+ }
180
+ }
181
+ }
182
+ catch (err) {
183
+ output.error(err.message);
184
+ process.exitCode = 1;
185
+ }
186
+ finally {
187
+ await (0, brain_1.stopBrainNode)();
188
+ }
189
+ },
190
+ };
@@ -0,0 +1,32 @@
1
+ /**
2
+ * pop brain snapshot — project a brain doc to markdown and write it to
3
+ * agent/brain/Knowledge/<docId>.generated.md.
4
+ *
5
+ * This is the read-side counterpart to applyBrainChange. The heartbeat
6
+ * skill calls this at end-of-heartbeat so the projection is kept in
7
+ * sync on disk for human review and git archival. Step 8 of the plan
8
+ * is when the hand-written files are retired and this becomes the
9
+ * source of truth; until then, the output lives at a `.generated.md`
10
+ * suffix so reviewers can diff it against the hand-written original.
11
+ *
12
+ * Graceful bootstrap: if the doc has no head CID yet (manifest empty),
13
+ * the command exits 0 with a log line so that the heartbeat skill can
14
+ * call it unconditionally without blowing up on fresh agents.
15
+ */
16
+ import type { Argv, ArgumentsCamelCase } from 'yargs';
17
+ interface SnapshotArgs {
18
+ doc: string;
19
+ outputPath?: string;
20
+ force?: boolean;
21
+ }
22
+ export declare const snapshotHandler: {
23
+ builder: (yargs: Argv) => Argv<{
24
+ doc: string;
25
+ } & {
26
+ "output-path": string | undefined;
27
+ } & {
28
+ force: boolean;
29
+ }>;
30
+ handler: (argv: ArgumentsCamelCase<SnapshotArgs>) => Promise<void>;
31
+ };
32
+ export {};
@@ -0,0 +1,243 @@
1
+ "use strict";
2
+ /**
3
+ * pop brain snapshot — project a brain doc to markdown and write it to
4
+ * agent/brain/Knowledge/<docId>.generated.md.
5
+ *
6
+ * This is the read-side counterpart to applyBrainChange. The heartbeat
7
+ * skill calls this at end-of-heartbeat so the projection is kept in
8
+ * sync on disk for human review and git archival. Step 8 of the plan
9
+ * is when the hand-written files are retired and this becomes the
10
+ * source of truth; until then, the output lives at a `.generated.md`
11
+ * suffix so reviewers can diff it against the hand-written original.
12
+ *
13
+ * Graceful bootstrap: if the doc has no head CID yet (manifest empty),
14
+ * the command exits 0 with a log line so that the heartbeat skill can
15
+ * call it unconditionally without blowing up on fresh agents.
16
+ */
17
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
18
+ if (k2 === undefined) k2 = k;
19
+ var desc = Object.getOwnPropertyDescriptor(m, k);
20
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
21
+ desc = { enumerable: true, get: function() { return m[k]; } };
22
+ }
23
+ Object.defineProperty(o, k2, desc);
24
+ }) : (function(o, m, k, k2) {
25
+ if (k2 === undefined) k2 = k;
26
+ o[k2] = m[k];
27
+ }));
28
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
29
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
30
+ }) : function(o, v) {
31
+ o["default"] = v;
32
+ });
33
+ var __importStar = (this && this.__importStar) || (function () {
34
+ var ownKeys = function(o) {
35
+ ownKeys = Object.getOwnPropertyNames || function (o) {
36
+ var ar = [];
37
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
38
+ return ar;
39
+ };
40
+ return ownKeys(o);
41
+ };
42
+ return function (mod) {
43
+ if (mod && mod.__esModule) return mod;
44
+ var result = {};
45
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
46
+ __setModuleDefault(result, mod);
47
+ return result;
48
+ };
49
+ })();
50
+ Object.defineProperty(exports, "__esModule", { value: true });
51
+ exports.snapshotHandler = void 0;
52
+ const path_1 = require("path");
53
+ const brain_paths_1 = require("../../lib/brain-paths");
54
+ const fs_1 = require("fs");
55
+ const brain_1 = require("../../lib/brain");
56
+ const brain_projections_1 = require("../../lib/brain-projections");
57
+ const brain_migrate_1 = require("../../lib/brain-migrate");
58
+ const output = __importStar(require("@poa-box/cli/lib/output"));
59
+ /**
60
+ * Count unique lesson ids in a rendered `.generated.md` projection.
61
+ *
62
+ * Task #359 (HB#361): this previously used `/^### /gm` which matched EVERY
63
+ * line starting with `### ` in the markdown, INCLUDING lines inside lesson
64
+ * bodies that contain `### ` as literal content. After HB#358 (#357) taught
65
+ * the parser to preserve H3 body content, body H3s started inflating the
66
+ * naive count and the regression guard reported false mismatches.
67
+ *
68
+ * The fix is in two parts:
69
+ * 1. For the **local projection** side of the comparison, count
70
+ * `doc.lessons.filter(!removed).length` directly — the doc is in
71
+ * scope, no parsing needed, no inflation possible. Done inline at
72
+ * the call site.
73
+ * 2. For the **existing-file** side, we don't have the old doc, so we
74
+ * parse the markdown structurally (via the HB#358 parser) and count
75
+ * UNIQUE ids. Unique-id counting dedupes the ghost lessons produced
76
+ * by one lesson's body containing `### ` at line-start: the parser
77
+ * will start "new lessons" at each body H3, but those ghosts often
78
+ * share slug-fallback ids with their neighbors (or with the enclosing
79
+ * real lesson), so counting unique-ids compresses the inflation
80
+ * back out. The count reflects the best estimate of how many
81
+ * distinct-id lessons are structurally present in the file.
82
+ *
83
+ * For non-shared docs (projects, retros), the naive H3 count is still
84
+ * usable because their bodies don't typically contain `### ` lines —
85
+ * kept as a fallback.
86
+ */
87
+ function countLessonsInProjectedMarkdown(md) {
88
+ const parsed = (0, brain_migrate_1.parseSharedMarkdown)(md, {
89
+ defaultAuthor: 'snapshot-regression-guard',
90
+ defaultTimestamp: 0,
91
+ });
92
+ const uniqueIds = new Set();
93
+ for (const l of parsed.lessons) {
94
+ if (l && typeof l.id === 'string' && l.id !== '') {
95
+ uniqueIds.add(l.id);
96
+ }
97
+ }
98
+ return uniqueIds.size;
99
+ }
100
+ /**
101
+ * Legacy H3 counter — kept as a fallback for non-shared docs (projects,
102
+ * retros) whose projections don't embed H3 content inside bodies. New
103
+ * shared-doc logic uses `countLessonsInProjectedMarkdown` instead.
104
+ */
105
+ function countH3Items(md) {
106
+ const matches = md.match(/^### /gm);
107
+ return matches ? matches.length : 0;
108
+ }
109
+ /** Parse the "*Head CID: `...`*" line from a generated projection file. */
110
+ function parseExistingHeadCid(md) {
111
+ const m = /^\*Head CID: `([^`]+)`\*/m.exec(md);
112
+ return m?.[1] ?? null;
113
+ }
114
+ exports.snapshotHandler = {
115
+ builder: (yargs) => yargs
116
+ .option('doc', {
117
+ describe: 'Brain document ID (e.g. pop.brain.shared)',
118
+ type: 'string',
119
+ demandOption: true,
120
+ })
121
+ .option('output-path', {
122
+ describe: 'Explicit output path (default: agent/brain/Knowledge/<doc>.generated.md)',
123
+ type: 'string',
124
+ })
125
+ .option('force', {
126
+ describe: 'Overwrite the existing generated.md even if it would regress (fewer lessons/projects than the file currently on disk). Use only when you know local state is authoritative.',
127
+ type: 'boolean',
128
+ default: false,
129
+ }),
130
+ handler: async (argv) => {
131
+ const docId = argv.doc;
132
+ try {
133
+ const { doc, headCid } = await (0, brain_1.readBrainDoc)(docId);
134
+ // Bootstrap case: no local head yet. Log + exit 0 so the
135
+ // heartbeat skill can call us unconditionally.
136
+ if (!headCid && (!doc || Object.keys(doc).length === 0)) {
137
+ if (output.isJsonMode()) {
138
+ output.json({ status: 'no-op', reason: 'no local head for this doc', docId });
139
+ }
140
+ else {
141
+ console.log(`pop brain snapshot: no local head for "${docId}" — nothing to project yet.`);
142
+ }
143
+ return;
144
+ }
145
+ // Dispatch to the right projector based on docId. Unknown docIds
146
+ // fall through to projectShared so older callers don't regress.
147
+ const markdown = (0, brain_projections_1.projectForDoc)(docId, doc, headCid);
148
+ // Default path: the repo-TRACKED brain (packages/agent/brain/Knowledge).
149
+ // Resolved via getRepoBrainRoot, not process.cwd() — a cwd-built path
150
+ // recreated the pre-split agent/ tree as untracked junk after the move.
151
+ const outPath = argv.outputPath ??
152
+ (0, path_1.join)((0, brain_paths_1.getRepoBrainRoot)(), 'Knowledge', `${docId}.generated.md`);
153
+ const outDir = outPath.substring(0, outPath.lastIndexOf('/'));
154
+ if (!(0, fs_1.existsSync)(outDir))
155
+ (0, fs_1.mkdirSync)(outDir, { recursive: true });
156
+ // Regression guard (task #328): if the existing generated.md has
157
+ // MORE content items (H3 headers) than our local projection, the
158
+ // local state is probably behind the peer-merged team state and
159
+ // writing would silently regress the committed file. Refuse with
160
+ // a clear error unless --force. The heartbeat skill calls
161
+ // `pop brain snapshot ... || true` so exit-1 here lets the HB
162
+ // continue without committing the regressed file.
163
+ if ((0, fs_1.existsSync)(outPath) && !argv.force) {
164
+ const existingContent = (0, fs_1.readFileSync)(outPath, 'utf8');
165
+ // Task #359 (HB#361): shared-doc regression guard uses
166
+ // doc-direct counting for the local side and structural unique-id
167
+ // parsing for the existing-file side. Naive /^### /gm counting
168
+ // was inflated by body-embedded `### ` lines after HB#358 taught
169
+ // the parser to preserve them. Other docs fall back to the naive
170
+ // H3 counter (their bodies don't embed H3s).
171
+ const isSharedDoc = docId === 'pop.brain.shared';
172
+ let existingCount;
173
+ let newCount;
174
+ if (isSharedDoc) {
175
+ existingCount = countLessonsInProjectedMarkdown(existingContent);
176
+ // Authoritative count from the in-memory doc — no parsing,
177
+ // no ghost lessons from body-H3 inflation.
178
+ const liveLessons = Array.isArray(doc?.lessons)
179
+ ? doc.lessons.filter((l) => l?.removed !== true)
180
+ : [];
181
+ const liveIds = new Set();
182
+ for (const l of liveLessons) {
183
+ if (l && typeof l.id === 'string' && l.id !== '')
184
+ liveIds.add(l.id);
185
+ }
186
+ newCount = liveIds.size;
187
+ }
188
+ else {
189
+ existingCount = countH3Items(existingContent);
190
+ newCount = countH3Items(markdown);
191
+ }
192
+ if (newCount < existingCount) {
193
+ const existingCid = parseExistingHeadCid(existingContent);
194
+ const msg = `pop brain snapshot would regress ${outPath}: ` +
195
+ `existing file has ${existingCount} items (head ${existingCid ?? '?'}), ` +
196
+ `local doc projects to ${newCount} items (head ${headCid ?? '?'}). ` +
197
+ `This usually means the local state lacks peer-merged content. ` +
198
+ `Run \`pop brain subscribe --doc ${docId}\` first to sync, ` +
199
+ `or pass \`--force\` to overwrite anyway.`;
200
+ if (output.isJsonMode()) {
201
+ output.json({
202
+ status: 'refused',
203
+ reason: 'regression',
204
+ docId,
205
+ path: outPath,
206
+ existingCount,
207
+ newCount,
208
+ existingHead: existingCid,
209
+ localHead: headCid,
210
+ });
211
+ }
212
+ else {
213
+ console.error(msg);
214
+ }
215
+ process.exitCode = 1;
216
+ return;
217
+ }
218
+ }
219
+ (0, fs_1.writeFileSync)(outPath, markdown);
220
+ if (output.isJsonMode()) {
221
+ output.json({
222
+ status: 'ok',
223
+ docId,
224
+ headCid,
225
+ bytes: markdown.length,
226
+ path: outPath,
227
+ });
228
+ }
229
+ else {
230
+ console.log(`Wrote ${markdown.length} bytes to ${outPath}`);
231
+ if (headCid)
232
+ console.log(`Head CID: ${headCid}`);
233
+ }
234
+ }
235
+ catch (err) {
236
+ output.error(err.message);
237
+ process.exitCode = 1;
238
+ }
239
+ finally {
240
+ await (0, brain_1.stopBrainNode)();
241
+ }
242
+ },
243
+ };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * pop brain status — show the local brain layer state.
3
+ *
4
+ * MVP step 1: initialize a local Helia node, print the peer ID and
5
+ * listening addresses, report connected peer count. No CRDT ops yet;
6
+ * this is the "environment works" smoke test.
7
+ *
8
+ * Future steps will add doc sync status, known brain documents, and
9
+ * per-document sync state.
10
+ */
11
+ import type { Argv, ArgumentsCamelCase } from 'yargs';
12
+ export declare const statusHandler: {
13
+ builder: (yargs: Argv) => Argv<{}>;
14
+ handler: (_argv: ArgumentsCamelCase<{}>) => Promise<void>;
15
+ };
@@ -0,0 +1,166 @@
1
+ "use strict";
2
+ /**
3
+ * pop brain status — show the local brain layer state.
4
+ *
5
+ * MVP step 1: initialize a local Helia node, print the peer ID and
6
+ * listening addresses, report connected peer count. No CRDT ops yet;
7
+ * this is the "environment works" smoke test.
8
+ *
9
+ * Future steps will add doc sync status, known brain documents, and
10
+ * per-document sync state.
11
+ */
12
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ var desc = Object.getOwnPropertyDescriptor(m, k);
15
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
16
+ desc = { enumerable: true, get: function() { return m[k]; } };
17
+ }
18
+ Object.defineProperty(o, k2, desc);
19
+ }) : (function(o, m, k, k2) {
20
+ if (k2 === undefined) k2 = k;
21
+ o[k2] = m[k];
22
+ }));
23
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
24
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
25
+ }) : function(o, v) {
26
+ o["default"] = v;
27
+ });
28
+ var __importStar = (this && this.__importStar) || (function () {
29
+ var ownKeys = function(o) {
30
+ ownKeys = Object.getOwnPropertyNames || function (o) {
31
+ var ar = [];
32
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
33
+ return ar;
34
+ };
35
+ return ownKeys(o);
36
+ };
37
+ return function (mod) {
38
+ if (mod && mod.__esModule) return mod;
39
+ var result = {};
40
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
41
+ __setModuleDefault(result, mod);
42
+ return result;
43
+ };
44
+ })();
45
+ Object.defineProperty(exports, "__esModule", { value: true });
46
+ exports.statusHandler = void 0;
47
+ const brain_1 = require("../../lib/brain");
48
+ const brain_daemon_1 = require("../../lib/brain-daemon");
49
+ const output = __importStar(require("@poa-box/cli/lib/output"));
50
+ exports.statusHandler = {
51
+ builder: (yargs) => yargs,
52
+ handler: async (_argv) => {
53
+ // HB#364: when a daemon is running, query it via IPC instead of
54
+ // spinning up a second libp2p instance in the CLI process. The
55
+ // ephemeral-instance path produced misleading state (random listen
56
+ // port each call, 0 peers because the daemon owns the connections)
57
+ // and made cross-device onboarding debugging impossible. Fall back
58
+ // to the in-process libp2p path only when no daemon is running.
59
+ const daemonPid = (0, brain_daemon_1.getRunningDaemonPid)();
60
+ if (daemonPid !== null) {
61
+ try {
62
+ const daemonInfo = await (0, brain_daemon_1.sendIpcRequest)('status', {}, 5_000);
63
+ const info = {
64
+ peerId: daemonInfo.peerId,
65
+ peerIdSource: 'daemon',
66
+ peerKeyPath: daemonInfo.brainHome + '/peer-key.json',
67
+ listeningAddrs: daemonInfo.listenAddrs ?? [],
68
+ connectedPeers: daemonInfo.connections ?? 0,
69
+ bootstrapPeerCount: 0, // not exposed via IPC yet; pending #349 follow-up
70
+ subscribedTopics: daemonInfo.topics ?? [],
71
+ topicPeerCounts: {},
72
+ heliaVersion: 'daemon-owned',
73
+ blockstorePath: daemonInfo.brainHome + '/helia-blocks',
74
+ };
75
+ if (output.isJsonMode()) {
76
+ output.json({ status: 'ok', source: 'daemon', daemonPid, ...info });
77
+ }
78
+ else {
79
+ console.log('');
80
+ console.log(' Brain layer — P2P CRDT substrate (daemon-owned)');
81
+ console.log(' ' + '─'.repeat(60));
82
+ console.log(` Daemon PID: ${daemonPid}`);
83
+ console.log(` Peer ID: ${info.peerId}`);
84
+ console.log(` Connected peers: ${info.connectedPeers}`);
85
+ console.log(` Blockstore path: ${info.blockstorePath}`);
86
+ console.log('');
87
+ if (info.listeningAddrs.length > 0) {
88
+ console.log(' Listening on (use for POP_BRAIN_PEERS):');
89
+ for (const addr of info.listeningAddrs) {
90
+ console.log(` ${addr}`);
91
+ }
92
+ console.log('');
93
+ }
94
+ if (info.subscribedTopics.length > 0) {
95
+ console.log(' Subscribed topics:');
96
+ for (const t of info.subscribedTopics) {
97
+ console.log(` ${t}`);
98
+ }
99
+ }
100
+ console.log('');
101
+ }
102
+ return;
103
+ }
104
+ catch (err) {
105
+ // IPC failed — fall through to in-process path and surface the
106
+ // daemon issue as a warning so operators see it.
107
+ if (!output.isJsonMode()) {
108
+ console.error(` [warn] daemon IPC at PID ${daemonPid} failed: ${err.message}`);
109
+ console.error(` [warn] falling back to in-process libp2p probe (may show stale state)`);
110
+ }
111
+ }
112
+ }
113
+ const spin = output.spinner('Initializing brain node...');
114
+ spin.start();
115
+ try {
116
+ const info = await (0, brain_1.getBrainNodeInfo)();
117
+ spin.stop();
118
+ if (output.isJsonMode()) {
119
+ output.json({
120
+ status: 'ok',
121
+ ...info,
122
+ });
123
+ }
124
+ else {
125
+ console.log('');
126
+ console.log(' Brain layer — P2P CRDT substrate');
127
+ console.log(' ' + '─'.repeat(60));
128
+ console.log(` Helia version: ${info.heliaVersion}`);
129
+ console.log(` Peer ID: ${info.peerId}`);
130
+ console.log(` PeerId source: ${info.peerIdSource}`);
131
+ console.log(` Peer key file: ${info.peerKeyPath}`);
132
+ console.log(` Connected peers: ${info.connectedPeers}`);
133
+ console.log(` Bootstrap known: ${info.bootstrapPeerCount}`);
134
+ console.log(` Blockstore path: ${info.blockstorePath}`);
135
+ console.log('');
136
+ if (info.listeningAddrs.length > 0) {
137
+ console.log(' Listening on:');
138
+ for (const addr of info.listeningAddrs) {
139
+ console.log(` ${addr}`);
140
+ }
141
+ console.log('');
142
+ }
143
+ if (info.subscribedTopics.length > 0) {
144
+ console.log(' Subscribed topics:');
145
+ for (const t of info.subscribedTopics) {
146
+ const n = info.topicPeerCounts[t] ?? 0;
147
+ console.log(` ${t} (${n} peer${n === 1 ? '' : 's'})`);
148
+ }
149
+ }
150
+ else {
151
+ console.log(' (no subscribed topics yet — run `pop brain subscribe --doc <id>` to listen)');
152
+ }
153
+ console.log('');
154
+ }
155
+ }
156
+ catch (err) {
157
+ spin.stop();
158
+ output.error(err.message);
159
+ process.exit(1);
160
+ }
161
+ finally {
162
+ // Short-lived CLI invocation — clean shutdown so the process exits.
163
+ await (0, brain_1.stopBrainNode)();
164
+ }
165
+ },
166
+ };
@@ -0,0 +1,28 @@
1
+ /**
2
+ * pop brain subscribe — keep the process alive and log incoming head-CID
3
+ * announcements on a doc's gossipsub topic. The manual test surface for
4
+ * MVP step 5.
5
+ *
6
+ * Typical 2-terminal smoke test:
7
+ * Terminal A: pop brain subscribe --doc pop.brain.shared
8
+ * Terminal B: (any write that calls applyBrainChange — step 7 ships
9
+ * the write CLI; until then, a direct script or test)
10
+ *
11
+ * Terminal A logs each announcement it sees: docId, CID, claimed author
12
+ * (not trusted — see brain.ts#BrainHeadAnnouncement), the peer it came
13
+ * from. Block fetch + load is step 6.
14
+ */
15
+ import type { Argv, ArgumentsCamelCase } from 'yargs';
16
+ interface SubscribeArgs {
17
+ doc: string;
18
+ logOnly?: boolean;
19
+ }
20
+ export declare const subscribeHandler: {
21
+ builder: (yargs: Argv) => Argv<{
22
+ doc: string;
23
+ } & {
24
+ "log-only": boolean;
25
+ }>;
26
+ handler: (argv: ArgumentsCamelCase<SubscribeArgs>) => Promise<void>;
27
+ };
28
+ export {};