@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,571 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Unified brain write dispatcher (HB#324 principal-engineer ship-2).
|
|
4
|
+
*
|
|
5
|
+
* ## Why this module exists
|
|
6
|
+
*
|
|
7
|
+
* Before this module, every brain write command (append-lesson, edit-lesson,
|
|
8
|
+
* remove-lesson, new-project, advance-stage, remove-project, …) called
|
|
9
|
+
* `applyBrainChange` directly with a closure. That works fine in-process but
|
|
10
|
+
* cannot be routed through IPC to a running daemon — closures don't serialize.
|
|
11
|
+
*
|
|
12
|
+
* The brain daemon needs to be the single owner of libp2p/gossipsub when it's
|
|
13
|
+
* running (HB#312 dogfood wedge proved sequential agent sessions never overlap
|
|
14
|
+
* in wall-clock time, so a short-lived in-process libp2p can't reliably
|
|
15
|
+
* deliver a gossipsub announcement before exiting). For the daemon to be
|
|
16
|
+
* useful, every write has to route through it. For the daemon to be usable,
|
|
17
|
+
* the CLI commands need a transparent fallback when no daemon is running.
|
|
18
|
+
*
|
|
19
|
+
* The solution is to turn every brain write into a pure-data operation
|
|
20
|
+
* descriptor (`BrainOp`) and have two execution paths that share zero
|
|
21
|
+
* business logic:
|
|
22
|
+
*
|
|
23
|
+
* `dispatchOp(op)` Runs the op in the current process. Used by:
|
|
24
|
+
* (a) the daemon's IPC handler, and
|
|
25
|
+
* (b) CLI commands as a fallback when no daemon.
|
|
26
|
+
* `routedDispatch(op)` Entry point for CLI commands. Checks for a running
|
|
27
|
+
* daemon: if yes, sends `applyOp` via IPC; if no,
|
|
28
|
+
* calls dispatchOp locally.
|
|
29
|
+
*
|
|
30
|
+
* Both paths converge on the same `dispatchOp` function. There is no
|
|
31
|
+
* "local" vs "routed" business logic — only a transport decision.
|
|
32
|
+
*
|
|
33
|
+
* ## Fallback correctness (important)
|
|
34
|
+
*
|
|
35
|
+
* `routedDispatch` must be careful about when it falls back to local
|
|
36
|
+
* dispatch. If the daemon has already processed the write and only the
|
|
37
|
+
* *response* got lost, a silent local fallback causes a double-write.
|
|
38
|
+
*
|
|
39
|
+
* The rule:
|
|
40
|
+
* - Pre-connect IPC failure (ECONNREFUSED, ENOENT, daemon not running at
|
|
41
|
+
* all) → SAFE to fall back. The write definitely did not land in the
|
|
42
|
+
* daemon's process because we never opened the connection.
|
|
43
|
+
* - Post-connect IPC failure (ECONNRESET, EPIPE, timeout mid-call) → NOT
|
|
44
|
+
* safe. The daemon may have processed the write. Error out and let the
|
|
45
|
+
* operator decide.
|
|
46
|
+
*
|
|
47
|
+
* This is the opposite of retry-everything semantics, on purpose. Brain
|
|
48
|
+
* writes are append-mostly and deduplication is hard once the block lands.
|
|
49
|
+
*
|
|
50
|
+
* ## Adding a new op
|
|
51
|
+
*
|
|
52
|
+
* 1. Add the variant to the `BrainOp` discriminated union below.
|
|
53
|
+
* 2. Add the case to `dispatchOp`'s switch statement.
|
|
54
|
+
* 3. Replace the `applyBrainChange(doc, closure)` call in your command
|
|
55
|
+
* with `routedDispatch({type: 'yourOp', ...})`.
|
|
56
|
+
*
|
|
57
|
+
* The daemon's IPC handler is a one-line `return dispatchOp(params.op)` —
|
|
58
|
+
* it does not need to be updated for new ops.
|
|
59
|
+
*/
|
|
60
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
61
|
+
exports.dispatchOp = dispatchOp;
|
|
62
|
+
exports.routedDispatch = routedDispatch;
|
|
63
|
+
const brain_1 = require("./brain");
|
|
64
|
+
const brain_daemon_1 = require("./brain-daemon");
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Local dispatch — runs in the current process
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
/**
|
|
69
|
+
* Apply a BrainOp in the current process. Translates the op descriptor to
|
|
70
|
+
* the corresponding Automerge change function and calls `applyBrainChange`,
|
|
71
|
+
* which signs the envelope, writes the block, updates the manifest, and
|
|
72
|
+
* publishes the new head CID via gossipsub.
|
|
73
|
+
*
|
|
74
|
+
* Same function is called from two places:
|
|
75
|
+
* - The daemon's `applyOp` IPC handler (when a CLI routed a write)
|
|
76
|
+
* - The CLI's `routedDispatch` fallback (when no daemon is running)
|
|
77
|
+
*
|
|
78
|
+
* Errors thrown from the change function propagate out as normal Error
|
|
79
|
+
* instances. For example, editLesson throws if the target lesson id is
|
|
80
|
+
* missing.
|
|
81
|
+
*/
|
|
82
|
+
async function dispatchOp(op) {
|
|
83
|
+
let applied;
|
|
84
|
+
switch (op.type) {
|
|
85
|
+
case 'appendLesson': {
|
|
86
|
+
applied = await (0, brain_1.applyBrainChange)(op.docId, (doc) => {
|
|
87
|
+
if (!Array.isArray(doc.lessons))
|
|
88
|
+
doc.lessons = [];
|
|
89
|
+
doc.lessons.push({
|
|
90
|
+
id: op.id,
|
|
91
|
+
title: op.title,
|
|
92
|
+
author: op.author,
|
|
93
|
+
body: op.body,
|
|
94
|
+
timestamp: op.timestamp,
|
|
95
|
+
});
|
|
96
|
+
}, { allowInvalidShape: op.allowInvalidShape });
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
case 'editLesson': {
|
|
100
|
+
applied = await (0, brain_1.applyBrainChange)(op.docId, (doc) => {
|
|
101
|
+
if (!Array.isArray(doc.lessons)) {
|
|
102
|
+
throw new Error(`no lessons list in doc ${op.docId}`);
|
|
103
|
+
}
|
|
104
|
+
const lesson = doc.lessons.find((l) => l && l.id === op.lessonId);
|
|
105
|
+
if (!lesson) {
|
|
106
|
+
throw new Error(`lesson ${op.lessonId} not found in ${op.docId}`);
|
|
107
|
+
}
|
|
108
|
+
if (op.fields.title !== undefined)
|
|
109
|
+
lesson.title = op.fields.title;
|
|
110
|
+
if (op.fields.body !== undefined)
|
|
111
|
+
lesson.body = op.fields.body;
|
|
112
|
+
if (op.fields.author !== undefined)
|
|
113
|
+
lesson.author = op.fields.author;
|
|
114
|
+
if (op.touch)
|
|
115
|
+
lesson.timestamp = Math.floor(Date.now() / 1000);
|
|
116
|
+
}, { allowInvalidShape: op.allowInvalidShape });
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
case 'removeLesson': {
|
|
120
|
+
applied = await (0, brain_1.applyBrainChange)(op.docId, (doc) => {
|
|
121
|
+
if (!Array.isArray(doc.lessons)) {
|
|
122
|
+
throw new Error(`no lessons list in doc ${op.docId}`);
|
|
123
|
+
}
|
|
124
|
+
const lesson = doc.lessons.find((l) => l && l.id === op.lessonId);
|
|
125
|
+
if (!lesson) {
|
|
126
|
+
throw new Error(`lesson ${op.lessonId} not found in ${op.docId}`);
|
|
127
|
+
}
|
|
128
|
+
// Soft-delete tombstone — see brain-projections.ts filter logic.
|
|
129
|
+
lesson.removed = true;
|
|
130
|
+
lesson.removedAt = op.removedAt;
|
|
131
|
+
lesson.removedBy = op.removedBy;
|
|
132
|
+
if (op.removedReason)
|
|
133
|
+
lesson.removedReason = op.removedReason;
|
|
134
|
+
}, { allowInvalidShape: op.allowInvalidShape });
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
case 'newProject': {
|
|
138
|
+
applied = await (0, brain_1.applyBrainChange)(op.docId, (doc) => {
|
|
139
|
+
if (!Array.isArray(doc.projects))
|
|
140
|
+
doc.projects = [];
|
|
141
|
+
// Collision check — upstream should have resolved with slug + suffix,
|
|
142
|
+
// but we defend here because routedDispatch is the last line before
|
|
143
|
+
// applyBrainChange.
|
|
144
|
+
if (doc.projects.some((p) => p && p.id === op.projectId)) {
|
|
145
|
+
throw new Error(`project id ${op.projectId} already exists in ${op.docId}`);
|
|
146
|
+
}
|
|
147
|
+
const entry = {
|
|
148
|
+
id: op.projectId,
|
|
149
|
+
name: op.name,
|
|
150
|
+
stage: op.stage,
|
|
151
|
+
proposedBy: op.proposedBy,
|
|
152
|
+
proposedAt: op.proposedAt,
|
|
153
|
+
};
|
|
154
|
+
if (op.brief !== undefined && op.brief !== '')
|
|
155
|
+
entry.brief = op.brief;
|
|
156
|
+
doc.projects.push(entry);
|
|
157
|
+
});
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
case 'advanceStage': {
|
|
161
|
+
applied = await (0, brain_1.applyBrainChange)(op.docId, (doc) => {
|
|
162
|
+
if (!Array.isArray(doc.projects)) {
|
|
163
|
+
throw new Error(`no projects list in doc ${op.docId}`);
|
|
164
|
+
}
|
|
165
|
+
const idx = doc.projects.findIndex((p) => p && p.id === op.projectId);
|
|
166
|
+
if (idx === -1) {
|
|
167
|
+
throw new Error(`project ${op.projectId} not found in ${op.docId}`);
|
|
168
|
+
}
|
|
169
|
+
const project = doc.projects[idx];
|
|
170
|
+
if (project.removed) {
|
|
171
|
+
throw new Error(`project ${op.projectId} is tombstoned — cannot advance`);
|
|
172
|
+
}
|
|
173
|
+
project.stage = op.newStage;
|
|
174
|
+
project.lastStageAdvanceAt = op.lastStageAdvanceAt;
|
|
175
|
+
});
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
case 'removeProject': {
|
|
179
|
+
applied = await (0, brain_1.applyBrainChange)(op.docId, (doc) => {
|
|
180
|
+
if (!Array.isArray(doc.projects)) {
|
|
181
|
+
throw new Error(`no projects list in doc ${op.docId}`);
|
|
182
|
+
}
|
|
183
|
+
const project = doc.projects.find((p) => p && p.id === op.projectId);
|
|
184
|
+
if (!project) {
|
|
185
|
+
throw new Error(`project ${op.projectId} not found in ${op.docId}`);
|
|
186
|
+
}
|
|
187
|
+
project.removed = true;
|
|
188
|
+
project.removedAt = op.removedAt;
|
|
189
|
+
project.removedBy = op.removedBy;
|
|
190
|
+
if (op.removedReason)
|
|
191
|
+
project.removedReason = op.removedReason;
|
|
192
|
+
});
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
case 'startRetro': {
|
|
196
|
+
applied = await (0, brain_1.applyBrainChange)(op.docId, (doc) => {
|
|
197
|
+
if (!Array.isArray(doc.retros))
|
|
198
|
+
doc.retros = [];
|
|
199
|
+
if (doc.retros.some((r) => r && r.id === op.retroId)) {
|
|
200
|
+
throw new Error(`retro id ${op.retroId} already exists in ${op.docId}`);
|
|
201
|
+
}
|
|
202
|
+
// Schema validation at write time (task constraint).
|
|
203
|
+
if (!op.author)
|
|
204
|
+
throw new Error('startRetro: author is required');
|
|
205
|
+
if (!op.window || typeof op.window.from !== 'number' || typeof op.window.to !== 'number') {
|
|
206
|
+
throw new Error('startRetro: window.from and window.to are required numbers');
|
|
207
|
+
}
|
|
208
|
+
if (op.window.to < op.window.from) {
|
|
209
|
+
throw new Error(`startRetro: window.to (${op.window.to}) must be >= window.from (${op.window.from})`);
|
|
210
|
+
}
|
|
211
|
+
if (!Array.isArray(op.proposedChanges)) {
|
|
212
|
+
throw new Error('startRetro: proposedChanges must be an array');
|
|
213
|
+
}
|
|
214
|
+
// Enforce unique change ids within a retro.
|
|
215
|
+
const seenIds = new Set();
|
|
216
|
+
for (const change of op.proposedChanges) {
|
|
217
|
+
if (!change.id || !change.summary) {
|
|
218
|
+
throw new Error('startRetro: each proposed change requires id and summary');
|
|
219
|
+
}
|
|
220
|
+
if (seenIds.has(change.id)) {
|
|
221
|
+
throw new Error(`startRetro: duplicate change id "${change.id}"`);
|
|
222
|
+
}
|
|
223
|
+
seenIds.add(change.id);
|
|
224
|
+
}
|
|
225
|
+
const entry = {
|
|
226
|
+
id: op.retroId,
|
|
227
|
+
author: op.author,
|
|
228
|
+
hb: op.hb,
|
|
229
|
+
window: { from: op.window.from, to: op.window.to },
|
|
230
|
+
observations: {},
|
|
231
|
+
proposedChanges: op.proposedChanges.map(c => {
|
|
232
|
+
const out = {
|
|
233
|
+
id: c.id,
|
|
234
|
+
summary: c.summary,
|
|
235
|
+
status: c.status ?? 'proposed',
|
|
236
|
+
};
|
|
237
|
+
if (c.details !== undefined && c.details !== '')
|
|
238
|
+
out.details = c.details;
|
|
239
|
+
return out;
|
|
240
|
+
}),
|
|
241
|
+
discussion: [],
|
|
242
|
+
status: 'open',
|
|
243
|
+
createdAt: op.createdAt,
|
|
244
|
+
};
|
|
245
|
+
if (op.observations.worked !== undefined && op.observations.worked !== '') {
|
|
246
|
+
entry.observations.worked = op.observations.worked;
|
|
247
|
+
}
|
|
248
|
+
if (op.observations.didntWork !== undefined && op.observations.didntWork !== '') {
|
|
249
|
+
entry.observations.didntWork = op.observations.didntWork;
|
|
250
|
+
}
|
|
251
|
+
doc.retros.push(entry);
|
|
252
|
+
});
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
case 'respondToRetro': {
|
|
256
|
+
applied = await (0, brain_1.applyBrainChange)(op.docId, (doc) => {
|
|
257
|
+
if (!Array.isArray(doc.retros)) {
|
|
258
|
+
throw new Error(`no retros list in doc ${op.docId}`);
|
|
259
|
+
}
|
|
260
|
+
const retro = doc.retros.find((r) => r && r.id === op.retroId);
|
|
261
|
+
if (!retro) {
|
|
262
|
+
throw new Error(`retro ${op.retroId} not found in ${op.docId}`);
|
|
263
|
+
}
|
|
264
|
+
if (retro.removed) {
|
|
265
|
+
throw new Error(`retro ${op.retroId} is tombstoned — cannot respond`);
|
|
266
|
+
}
|
|
267
|
+
if (!Array.isArray(retro.discussion))
|
|
268
|
+
retro.discussion = [];
|
|
269
|
+
// Validate votes refer to real change ids if provided.
|
|
270
|
+
if (op.votePerChange) {
|
|
271
|
+
const changeIds = new Set((retro.proposedChanges ?? []).map((c) => c?.id).filter((id) => typeof id === 'string'));
|
|
272
|
+
for (const changeId of Object.keys(op.votePerChange)) {
|
|
273
|
+
if (!changeIds.has(changeId)) {
|
|
274
|
+
throw new Error(`respondToRetro: vote references unknown change id "${changeId}" ` +
|
|
275
|
+
`(retro has: ${Array.from(changeIds).join(', ') || '(none)'})`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
const entry = {
|
|
280
|
+
author: op.author,
|
|
281
|
+
message: op.message,
|
|
282
|
+
timestamp: op.timestamp,
|
|
283
|
+
};
|
|
284
|
+
if (op.hb !== undefined)
|
|
285
|
+
entry.hb = op.hb;
|
|
286
|
+
if (op.votePerChange && Object.keys(op.votePerChange).length > 0) {
|
|
287
|
+
entry.votePerChange = { ...op.votePerChange };
|
|
288
|
+
}
|
|
289
|
+
retro.discussion.push(entry);
|
|
290
|
+
// Advance retro.status to 'discussed' on first response.
|
|
291
|
+
if (retro.status === 'open')
|
|
292
|
+
retro.status = 'discussed';
|
|
293
|
+
});
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
case 'updateChangeStatus': {
|
|
297
|
+
applied = await (0, brain_1.applyBrainChange)(op.docId, (doc) => {
|
|
298
|
+
if (!Array.isArray(doc.retros)) {
|
|
299
|
+
throw new Error(`no retros list in doc ${op.docId}`);
|
|
300
|
+
}
|
|
301
|
+
const retro = doc.retros.find((r) => r && r.id === op.retroId);
|
|
302
|
+
if (!retro) {
|
|
303
|
+
throw new Error(`retro ${op.retroId} not found in ${op.docId}`);
|
|
304
|
+
}
|
|
305
|
+
if (!Array.isArray(retro.proposedChanges)) {
|
|
306
|
+
throw new Error(`retro ${op.retroId} has no proposedChanges list`);
|
|
307
|
+
}
|
|
308
|
+
const change = retro.proposedChanges.find((c) => c && c.id === op.changeId);
|
|
309
|
+
if (!change) {
|
|
310
|
+
throw new Error(`change ${op.changeId} not found in retro ${op.retroId} ` +
|
|
311
|
+
`(available: ${retro.proposedChanges.map((c) => c?.id).join(', ')})`);
|
|
312
|
+
}
|
|
313
|
+
change.status = op.newStatus;
|
|
314
|
+
if (op.newStatus === 'filed' && op.filedTaskId) {
|
|
315
|
+
change.filedTaskId = op.filedTaskId;
|
|
316
|
+
}
|
|
317
|
+
// If all changes are filed or rejected, mark the retro as shipped.
|
|
318
|
+
const stillOpen = retro.proposedChanges.some((c) => c && c.status !== 'filed' && c.status !== 'rejected');
|
|
319
|
+
if (!stillOpen) {
|
|
320
|
+
retro.status = 'shipped';
|
|
321
|
+
retro.closedAt = Math.floor(Date.now() / 1000);
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
case 'removeRetro': {
|
|
327
|
+
applied = await (0, brain_1.applyBrainChange)(op.docId, (doc) => {
|
|
328
|
+
if (!Array.isArray(doc.retros)) {
|
|
329
|
+
throw new Error(`no retros list in doc ${op.docId}`);
|
|
330
|
+
}
|
|
331
|
+
const retro = doc.retros.find((r) => r && r.id === op.retroId);
|
|
332
|
+
if (!retro) {
|
|
333
|
+
throw new Error(`retro ${op.retroId} not found in ${op.docId}`);
|
|
334
|
+
}
|
|
335
|
+
retro.removed = true;
|
|
336
|
+
retro.removedAt = op.removedAt;
|
|
337
|
+
retro.removedBy = op.removedBy;
|
|
338
|
+
if (op.removedReason)
|
|
339
|
+
retro.removedReason = op.removedReason;
|
|
340
|
+
});
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
case 'tagLesson': {
|
|
344
|
+
applied = await (0, brain_1.applyBrainChange)(op.docId, (doc) => {
|
|
345
|
+
if (!Array.isArray(doc.lessons)) {
|
|
346
|
+
throw new Error(`no lessons list in doc ${op.docId}`);
|
|
347
|
+
}
|
|
348
|
+
const lesson = doc.lessons.find((l) => l && l.id === op.lessonId);
|
|
349
|
+
if (!lesson) {
|
|
350
|
+
throw new Error(`lesson ${op.lessonId} not found in ${op.docId}`);
|
|
351
|
+
}
|
|
352
|
+
if (!Array.isArray(lesson.tags))
|
|
353
|
+
lesson.tags = [];
|
|
354
|
+
for (const t of op.addTags) {
|
|
355
|
+
if (!lesson.tags.includes(t))
|
|
356
|
+
lesson.tags.push(t);
|
|
357
|
+
}
|
|
358
|
+
for (const t of op.removeTags) {
|
|
359
|
+
const idx = lesson.tags.indexOf(t);
|
|
360
|
+
if (idx >= 0)
|
|
361
|
+
lesson.tags.splice(idx, 1);
|
|
362
|
+
}
|
|
363
|
+
}, { allowInvalidShape: op.allowInvalidShape });
|
|
364
|
+
break;
|
|
365
|
+
}
|
|
366
|
+
// --- Brainstorms (task #354 phase b, HB#208) ----------------------
|
|
367
|
+
case 'startBrainstorm': {
|
|
368
|
+
applied = await (0, brain_1.applyBrainChange)(op.docId, (doc) => {
|
|
369
|
+
if (!Array.isArray(doc.brainstorms))
|
|
370
|
+
doc.brainstorms = [];
|
|
371
|
+
if (doc.brainstorms.some((b) => b && b.id === op.brainstormId)) {
|
|
372
|
+
throw new Error(`brainstorm ${op.brainstormId} already exists in ${op.docId}`);
|
|
373
|
+
}
|
|
374
|
+
const entry = {
|
|
375
|
+
id: op.brainstormId,
|
|
376
|
+
title: op.title,
|
|
377
|
+
prompt: op.prompt,
|
|
378
|
+
author: op.author,
|
|
379
|
+
openedAt: op.openedAt,
|
|
380
|
+
status: 'open',
|
|
381
|
+
ideas: [],
|
|
382
|
+
};
|
|
383
|
+
if (op.windowFromHB != null || op.windowToHB != null) {
|
|
384
|
+
entry.window = {};
|
|
385
|
+
if (op.windowFromHB != null)
|
|
386
|
+
entry.window.from = op.windowFromHB;
|
|
387
|
+
if (op.windowToHB != null)
|
|
388
|
+
entry.window.to = op.windowToHB;
|
|
389
|
+
}
|
|
390
|
+
doc.brainstorms.push(entry);
|
|
391
|
+
});
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
case 'respondToBrainstorm': {
|
|
395
|
+
applied = await (0, brain_1.applyBrainChange)(op.docId, (doc) => {
|
|
396
|
+
if (!Array.isArray(doc.brainstorms)) {
|
|
397
|
+
throw new Error(`no brainstorms list in doc ${op.docId}`);
|
|
398
|
+
}
|
|
399
|
+
const brainstorm = doc.brainstorms.find((b) => b && b.id === op.brainstormId);
|
|
400
|
+
if (!brainstorm) {
|
|
401
|
+
throw new Error(`brainstorm ${op.brainstormId} not found in ${op.docId}`);
|
|
402
|
+
}
|
|
403
|
+
if (brainstorm.removed || brainstorm.status === 'closed') {
|
|
404
|
+
throw new Error(`brainstorm ${op.brainstormId} is ${brainstorm.removed ? 'removed' : 'closed'} — cannot respond`);
|
|
405
|
+
}
|
|
406
|
+
if (!Array.isArray(brainstorm.ideas))
|
|
407
|
+
brainstorm.ideas = [];
|
|
408
|
+
// Add a new idea if requested
|
|
409
|
+
if (op.addIdea) {
|
|
410
|
+
if (brainstorm.ideas.some((i) => i && i.id === op.addIdea.id)) {
|
|
411
|
+
throw new Error(`idea ${op.addIdea.id} already exists in brainstorm ${op.brainstormId}`);
|
|
412
|
+
}
|
|
413
|
+
brainstorm.ideas.push({
|
|
414
|
+
id: op.addIdea.id,
|
|
415
|
+
author: op.author,
|
|
416
|
+
message: op.addIdea.message,
|
|
417
|
+
timestamp: op.timestamp,
|
|
418
|
+
votes: {},
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
// Merge votes into existing ideas — per-agent slot so concurrent
|
|
422
|
+
// writes from different agents don't overwrite each other's votes
|
|
423
|
+
if (op.votes) {
|
|
424
|
+
for (const [ideaId, stance] of Object.entries(op.votes)) {
|
|
425
|
+
const idea = brainstorm.ideas.find((i) => i && i.id === ideaId);
|
|
426
|
+
if (!idea) {
|
|
427
|
+
throw new Error(`cannot vote on unknown idea ${ideaId} in brainstorm ${op.brainstormId}`);
|
|
428
|
+
}
|
|
429
|
+
if (!idea.votes || typeof idea.votes !== 'object')
|
|
430
|
+
idea.votes = {};
|
|
431
|
+
idea.votes[op.author] = stance;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
// Auto-advance status from open to voting once any vote lands
|
|
435
|
+
if (op.votes && Object.keys(op.votes).length > 0 && brainstorm.status === 'open') {
|
|
436
|
+
brainstorm.status = 'voting';
|
|
437
|
+
}
|
|
438
|
+
// Record the discussion message (separate from ideas — a response
|
|
439
|
+
// can have a message without adding an idea or casting a vote)
|
|
440
|
+
if (op.message) {
|
|
441
|
+
if (!Array.isArray(brainstorm.discussion))
|
|
442
|
+
brainstorm.discussion = [];
|
|
443
|
+
brainstorm.discussion.push({
|
|
444
|
+
author: op.author,
|
|
445
|
+
message: op.message,
|
|
446
|
+
timestamp: op.timestamp,
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
case 'promoteIdea': {
|
|
453
|
+
applied = await (0, brain_1.applyBrainChange)(op.docId, (doc) => {
|
|
454
|
+
if (!Array.isArray(doc.brainstorms)) {
|
|
455
|
+
throw new Error(`no brainstorms list in doc ${op.docId}`);
|
|
456
|
+
}
|
|
457
|
+
const brainstorm = doc.brainstorms.find((b) => b && b.id === op.brainstormId);
|
|
458
|
+
if (!brainstorm) {
|
|
459
|
+
throw new Error(`brainstorm ${op.brainstormId} not found in ${op.docId}`);
|
|
460
|
+
}
|
|
461
|
+
const idea = (brainstorm.ideas || []).find((i) => i && i.id === op.ideaId);
|
|
462
|
+
if (!idea) {
|
|
463
|
+
throw new Error(`idea ${op.ideaId} not found in brainstorm ${op.brainstormId}`);
|
|
464
|
+
}
|
|
465
|
+
if (!Array.isArray(brainstorm.promotedToProjectIds))
|
|
466
|
+
brainstorm.promotedToProjectIds = [];
|
|
467
|
+
if (!brainstorm.promotedToProjectIds.includes(op.promotedProjectId)) {
|
|
468
|
+
brainstorm.promotedToProjectIds.push(op.promotedProjectId);
|
|
469
|
+
}
|
|
470
|
+
// Mark the idea itself as promoted so the projection can show
|
|
471
|
+
// the link visibly
|
|
472
|
+
idea.promotedAt = op.promotedAt;
|
|
473
|
+
idea.promotedBy = op.promotedBy;
|
|
474
|
+
idea.promotedProjectId = op.promotedProjectId;
|
|
475
|
+
// Advance the brainstorm to promoted status if it wasn't already
|
|
476
|
+
if (brainstorm.status !== 'closed' && brainstorm.status !== 'promoted') {
|
|
477
|
+
brainstorm.status = 'promoted';
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
break;
|
|
481
|
+
}
|
|
482
|
+
case 'closeBrainstorm': {
|
|
483
|
+
applied = await (0, brain_1.applyBrainChange)(op.docId, (doc) => {
|
|
484
|
+
if (!Array.isArray(doc.brainstorms)) {
|
|
485
|
+
throw new Error(`no brainstorms list in doc ${op.docId}`);
|
|
486
|
+
}
|
|
487
|
+
const brainstorm = doc.brainstorms.find((b) => b && b.id === op.brainstormId);
|
|
488
|
+
if (!brainstorm) {
|
|
489
|
+
throw new Error(`brainstorm ${op.brainstormId} not found in ${op.docId}`);
|
|
490
|
+
}
|
|
491
|
+
brainstorm.status = 'closed';
|
|
492
|
+
brainstorm.closedAt = op.closedAt;
|
|
493
|
+
brainstorm.closedBy = op.closedBy;
|
|
494
|
+
if (op.reason)
|
|
495
|
+
brainstorm.closedReason = op.reason;
|
|
496
|
+
});
|
|
497
|
+
break;
|
|
498
|
+
}
|
|
499
|
+
case 'removeBrainstorm': {
|
|
500
|
+
applied = await (0, brain_1.applyBrainChange)(op.docId, (doc) => {
|
|
501
|
+
if (!Array.isArray(doc.brainstorms)) {
|
|
502
|
+
throw new Error(`no brainstorms list in doc ${op.docId}`);
|
|
503
|
+
}
|
|
504
|
+
const brainstorm = doc.brainstorms.find((b) => b && b.id === op.brainstormId);
|
|
505
|
+
if (!brainstorm) {
|
|
506
|
+
throw new Error(`brainstorm ${op.brainstormId} not found in ${op.docId}`);
|
|
507
|
+
}
|
|
508
|
+
brainstorm.removed = true;
|
|
509
|
+
brainstorm.removedAt = op.removedAt;
|
|
510
|
+
brainstorm.removedBy = op.removedBy;
|
|
511
|
+
if (op.removedReason)
|
|
512
|
+
brainstorm.removedReason = op.removedReason;
|
|
513
|
+
});
|
|
514
|
+
break;
|
|
515
|
+
}
|
|
516
|
+
default: {
|
|
517
|
+
// Exhaustiveness check — TypeScript will complain if a BrainOp variant
|
|
518
|
+
// is missing from the switch.
|
|
519
|
+
const _exhaustive = op;
|
|
520
|
+
throw new Error(`Unknown BrainOp type: ${_exhaustive?.type}`);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return {
|
|
524
|
+
headCid: applied.headCid,
|
|
525
|
+
envelopeAuthor: applied.author,
|
|
526
|
+
routedViaDaemon: false,
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
// ---------------------------------------------------------------------------
|
|
530
|
+
// Routed dispatch — sends to daemon when running, falls back locally
|
|
531
|
+
// ---------------------------------------------------------------------------
|
|
532
|
+
/**
|
|
533
|
+
* Route a BrainOp through a running brain daemon, or run it locally if no
|
|
534
|
+
* daemon is up.
|
|
535
|
+
*
|
|
536
|
+
* Fallback safety: we only fall back on PRE-CONNECT IPC errors (daemon not
|
|
537
|
+
* running, socket missing, connection refused). POST-CONNECT errors leave
|
|
538
|
+
* the write in an unknown state, so we error out and ask the operator to
|
|
539
|
+
* verify. See module header comment for the full rationale.
|
|
540
|
+
*/
|
|
541
|
+
async function routedDispatch(op) {
|
|
542
|
+
const daemonPid = (0, brain_daemon_1.getRunningDaemonPid)();
|
|
543
|
+
if (daemonPid === null) {
|
|
544
|
+
// No daemon → run locally. Correct + safe.
|
|
545
|
+
return await dispatchOp(op);
|
|
546
|
+
}
|
|
547
|
+
try {
|
|
548
|
+
const ipcResult = await (0, brain_daemon_1.sendIpcRequest)('applyOp', { op }, 15_000);
|
|
549
|
+
return {
|
|
550
|
+
headCid: ipcResult.headCid,
|
|
551
|
+
envelopeAuthor: ipcResult.envelopeAuthor,
|
|
552
|
+
routedViaDaemon: true,
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
catch (err) {
|
|
556
|
+
// BrainIpcError carries a `.phase` field: 'pre-connect' means the
|
|
557
|
+
// connection was never established (safe to fall back); 'post-connect'
|
|
558
|
+
// means the write may or may not have landed (ambiguous — do NOT fall
|
|
559
|
+
// back, surface to operator).
|
|
560
|
+
const isPreConnect = err instanceof brain_daemon_1.BrainIpcError
|
|
561
|
+
? err.phase === 'pre-connect'
|
|
562
|
+
: (err?.code === 'ECONNREFUSED' || err?.code === 'ENOENT');
|
|
563
|
+
if (isPreConnect) {
|
|
564
|
+
return await dispatchOp(op);
|
|
565
|
+
}
|
|
566
|
+
throw new Error(`Brain IPC failed mid-call (PID ${daemonPid}): ${err?.message ?? err}. ` +
|
|
567
|
+
`The write may or may not have landed in the daemon. ` +
|
|
568
|
+
`Verify with: pop brain read --doc ${op.docId} — and then either ` +
|
|
569
|
+
`retry the command or stop the daemon (pop brain daemon stop) before retrying.`);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the REPO-TRACKED brain lives.
|
|
3
|
+
*
|
|
4
|
+
* The package split moved it from <repo>/agent/brain to
|
|
5
|
+
* <repo>/packages/agent/brain. Three call sites used to build the old path
|
|
6
|
+
* from process.cwd(), and after the move they all failed SILENTLY: genesis
|
|
7
|
+
* seeding never happened (regressing the task #352 fix), the committed signer
|
|
8
|
+
* allowlist stopped loading, and `pop brain snapshot` grew a fresh untracked
|
|
9
|
+
* agent/ tree at the repo root instead of updating the tracked projection.
|
|
10
|
+
*
|
|
11
|
+
* Resolve relative to THIS package first (dist/lib → ../../brain), then fall
|
|
12
|
+
* back to cwd-based layouts so an agent invoking the command from an older
|
|
13
|
+
* checkout, or from outside the repo, still finds a brain if one exists.
|
|
14
|
+
*/
|
|
15
|
+
export declare function getRepoBrainRoot(): string;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Where the REPO-TRACKED brain lives.
|
|
4
|
+
*
|
|
5
|
+
* The package split moved it from <repo>/agent/brain to
|
|
6
|
+
* <repo>/packages/agent/brain. Three call sites used to build the old path
|
|
7
|
+
* from process.cwd(), and after the move they all failed SILENTLY: genesis
|
|
8
|
+
* seeding never happened (regressing the task #352 fix), the committed signer
|
|
9
|
+
* allowlist stopped loading, and `pop brain snapshot` grew a fresh untracked
|
|
10
|
+
* agent/ tree at the repo root instead of updating the tracked projection.
|
|
11
|
+
*
|
|
12
|
+
* Resolve relative to THIS package first (dist/lib → ../../brain), then fall
|
|
13
|
+
* back to cwd-based layouts so an agent invoking the command from an older
|
|
14
|
+
* checkout, or from outside the repo, still finds a brain if one exists.
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.getRepoBrainRoot = getRepoBrainRoot;
|
|
18
|
+
const path_1 = require("path");
|
|
19
|
+
const fs_1 = require("fs");
|
|
20
|
+
function getRepoBrainRoot() {
|
|
21
|
+
const candidates = [
|
|
22
|
+
(0, path_1.resolve)(__dirname, '..', '..', 'brain'), // dist/lib → packages/agent/brain
|
|
23
|
+
(0, path_1.join)(process.cwd(), 'packages', 'agent', 'brain'), // repo root as cwd
|
|
24
|
+
(0, path_1.join)(process.cwd(), 'agent', 'brain'), // pre-split layout
|
|
25
|
+
];
|
|
26
|
+
for (const c of candidates) {
|
|
27
|
+
if ((0, fs_1.existsSync)(c))
|
|
28
|
+
return c;
|
|
29
|
+
}
|
|
30
|
+
// Nothing exists yet (fresh checkout mid-build): prefer the package-rooted
|
|
31
|
+
// path so anything created lands in the TRACKED location.
|
|
32
|
+
return candidates[0];
|
|
33
|
+
}
|