@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,503 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.triageHandler = void 0;
37
+ const ethers_1 = require("ethers");
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
40
+ const os_1 = require("os");
41
+ const subgraph_1 = require("@poa-box/cli/lib/subgraph");
42
+ const resolve_1 = require("@poa-box/cli/lib/resolve");
43
+ const networks_1 = require("@poa-box/cli/config/networks");
44
+ const contracts_1 = require("@poa-box/cli/lib/contracts");
45
+ const helpers_1 = require("@poa-box/cli/commands/vote/helpers");
46
+ const no_alloc_cache_1 = require("@poa-box/cli/lib/no-alloc-cache");
47
+ const output = __importStar(require("@poa-box/cli/lib/output"));
48
+ const FETCH_TRIAGE_DATA = `
49
+ query FetchTriageData($orgId: Bytes!) {
50
+ organization(id: $orgId) {
51
+ name
52
+ participationToken { totalSupply }
53
+ users(first: 100) {
54
+ address
55
+ participationTokenBalance
56
+ membershipStatus
57
+ totalTasksCompleted
58
+ account { username }
59
+ }
60
+ hybridVoting {
61
+ proposals(first: 100) {
62
+ proposalId
63
+ title
64
+ status
65
+ endTimestamp
66
+ winningOption
67
+ votes { voter }
68
+ }
69
+ }
70
+ taskManager {
71
+ projects(where: { deleted: false }, first: 100) {
72
+ tasks(first: 1000) {
73
+ taskId
74
+ title
75
+ status
76
+ assignee
77
+ assigneeUsername
78
+ rejectionCount
79
+ }
80
+ }
81
+ }
82
+ paymentManager {
83
+ distributions(where: { status: "Active" }, first: 20) {
84
+ distributionId
85
+ claims { claimer }
86
+ }
87
+ }
88
+ }
89
+ }
90
+ `;
91
+ exports.triageHandler = {
92
+ builder: (yargs) => yargs,
93
+ handler: async (argv) => {
94
+ const spin = output.spinner('Running triage...');
95
+ spin.start();
96
+ try {
97
+ // --- Gather data ---
98
+ const key = argv.privateKey || process.env.POP_PRIVATE_KEY;
99
+ if (!key)
100
+ throw new Error('No private key configured');
101
+ const wallet = new ethers_1.ethers.Wallet(key);
102
+ const myAddr = wallet.address.toLowerCase();
103
+ const modules = await (0, resolve_1.resolveOrgModules)(argv.org, argv.chain);
104
+ const networkConfig = (0, networks_1.resolveNetworkConfig)(argv.chain);
105
+ const provider = new ethers_1.ethers.providers.JsonRpcProvider(networkConfig.resolvedRpc);
106
+ const [gasBalance, orgData] = await Promise.all([
107
+ provider.getBalance(wallet.address),
108
+ (0, subgraph_1.query)(FETCH_TRIAGE_DATA, { orgId: modules.orgId }, argv.chain),
109
+ ]);
110
+ const org = orgData.organization;
111
+ if (!org)
112
+ throw new Error('Organization not found');
113
+ const now = Math.floor(Date.now() / 1000);
114
+ const actions = [];
115
+ const changes = [];
116
+ // --- Load last known state for change detection ---
117
+ const orgStatePath = path.join((0, os_1.homedir)(), '.pop-agent', 'brain', 'Memory', 'org-state.md');
118
+ let lastState = '';
119
+ try {
120
+ lastState = fs.readFileSync(orgStatePath, 'utf8');
121
+ }
122
+ catch { }
123
+ const activeMembers = org.users.filter((u) => u.membershipStatus === 'Active');
124
+ const totalSupply = ethers_1.ethers.utils.formatEther(org.participationToken?.totalSupply || '0');
125
+ const allTasks = (org.taskManager?.projects || []).flatMap((p) => p.tasks || []);
126
+ const proposals = org.hybridVoting?.proposals || [];
127
+ // --- 1. BLOCKERS (CRITICAL) ---
128
+ // Gas check
129
+ const gasEther = parseFloat(ethers_1.ethers.utils.formatEther(gasBalance));
130
+ if (gasEther < 0.01) {
131
+ actions.push({ priority: 'CRITICAL', type: 'gas', detail: `Gas critically low: ${gasEther.toFixed(4)} ${networkConfig.nativeCurrency.symbol}. Fund wallet immediately.` });
132
+ }
133
+ else if (gasEther < 0.1) {
134
+ actions.push({ priority: 'HIGH', type: 'gas', detail: `Gas low: ${gasEther.toFixed(3)} ${networkConfig.nativeCurrency.symbol}. Consider refueling.` });
135
+ }
136
+ // Rejected tasks
137
+ const myRejected = allTasks.filter((t) => t.assignee?.toLowerCase() === myAddr &&
138
+ t.status === 'Assigned' &&
139
+ parseInt(t.rejectionCount || '0') > 0);
140
+ for (const t of myRejected) {
141
+ actions.push({ priority: 'CRITICAL', type: 'rejected', detail: `Task #${t.taskId} "${t.title}" was rejected (${t.rejectionCount}x). Fix and re-submit before new work.`, data: { taskId: t.taskId } });
142
+ }
143
+ // --- 2. QUEUE (HIGH) ---
144
+ // Expired proposals needing announce (with callStatic to filter zombies)
145
+ const expiredCandidates = proposals.filter((p) => p.status === 'Active' &&
146
+ p.winningOption == null &&
147
+ p.endTimestamp && parseInt(p.endTimestamp) < now);
148
+ if (expiredCandidates.length > 0) {
149
+ try {
150
+ const votingContracts = await (0, helpers_1.resolveVotingContracts)(argv.org, argv.chain);
151
+ const hybridAddr = votingContracts.hybridVotingAddress;
152
+ if (hybridAddr) {
153
+ const votingContract = (0, contracts_1.createReadContract)(hybridAddr, 'HybridVotingNew', provider);
154
+ for (const p of expiredCandidates) {
155
+ try {
156
+ await votingContract.callStatic.announceWinner(p.proposalId);
157
+ // callStatic succeeded — this proposal CAN be announced
158
+ actions.push({ priority: 'HIGH', type: 'announce', detail: `Proposal #${p.proposalId} "${p.title}" expired — run announce-all.`, data: { proposalId: p.proposalId } });
159
+ }
160
+ catch {
161
+ // callStatic failed — zombie proposal, skip silently
162
+ }
163
+ }
164
+ }
165
+ }
166
+ catch {
167
+ // Voting contract resolution failed — fall back to listing all expired
168
+ for (const p of expiredCandidates) {
169
+ actions.push({ priority: 'HIGH', type: 'announce', detail: `Proposal #${p.proposalId} "${p.title}" expired — run announce-all.`, data: { proposalId: p.proposalId } });
170
+ }
171
+ }
172
+ }
173
+ // Unvoted proposals
174
+ const activeProposals = proposals.filter((p) => p.status === 'Active' && p.winningOption == null && !(parseInt(p.endTimestamp) < now));
175
+ for (const p of activeProposals) {
176
+ const hasVoted = (p.votes || []).some((v) => v.voter?.toLowerCase() === myAddr);
177
+ if (!hasVoted) {
178
+ const minutesLeft = p.endTimestamp ? Math.floor((parseInt(p.endTimestamp) - now) / 60) : Infinity;
179
+ const urgency = minutesLeft < 60 ? 'CRITICAL' : minutesLeft < 360 ? 'HIGH' : 'MEDIUM';
180
+ actions.push({ priority: urgency, type: 'vote', detail: `Proposal #${p.proposalId} "${p.title}" — unvoted, ${minutesLeft < 60 ? minutesLeft + ' min left!' : Math.floor(minutesLeft / 60) + 'h left'}.`, data: { proposalId: p.proposalId, minutesLeft } });
181
+ }
182
+ }
183
+ // Pending reviews (submitted by others)
184
+ const pendingReviews = allTasks.filter((t) => t.status === 'Submitted' && t.assignee?.toLowerCase() !== myAddr);
185
+ for (const t of pendingReviews) {
186
+ actions.push({ priority: 'HIGH', type: 'review', detail: `Task #${t.taskId} "${t.title}" by ${t.assigneeUsername || 'unknown'} — needs review.`, data: { taskId: t.taskId } });
187
+ }
188
+ // Batch-review prompt (task #406, HB#485 throughput fix).
189
+ // When review backlog exceeds 5, surface a dedicated batch-review
190
+ // action so the heartbeat skill prioritizes clearing the queue.
191
+ if (pendingReviews.length > 5) {
192
+ actions.unshift({
193
+ priority: 'HIGH',
194
+ type: 'batch-review',
195
+ detail: `Review backlog: ${pendingReviews.length} tasks pending. Dedicate this heartbeat to batch review (up to 5 per HB).`,
196
+ data: { count: pendingReviews.length },
197
+ });
198
+ }
199
+ // Open retros needing response (task #344). Surface a HIGH action
200
+ // when an open retro exists whose author is NOT me AND I have
201
+ // not yet posted a response. The retro must be "fresh" — created
202
+ // within the last ~75 minutes (~5 HBs at 15-min cadence) so we
203
+ // don't pester the on-call agent with stale retros indefinitely.
204
+ //
205
+ // Cost guard: check doc-heads.json directly (cheap filesystem
206
+ // read) before spinning up the helia node for a brain read. The
207
+ // typical case is "no retros doc yet" which should skip the
208
+ // expensive path entirely.
209
+ try {
210
+ const manifestPath = path.join((0, os_1.homedir)(), '.pop-agent', 'brain', 'doc-heads.json');
211
+ if (fs.existsSync(manifestPath)) {
212
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
213
+ if (manifest['pop.brain.retros']) {
214
+ // Dynamic import so the non-retro-using triage path doesn't
215
+ // pay the brain.ts module-load cost (which pulls in ethers +
216
+ // automerge + a lazy helia reference).
217
+ const { readBrainDoc, stopBrainNode } = require('../../lib/brain');
218
+ try {
219
+ const { doc: retrosDoc } = await readBrainDoc('pop.brain.retros');
220
+ const retros = Array.isArray(retrosDoc?.retros) ? retrosDoc.retros : [];
221
+ const freshThresholdSecs = 75 * 60; // 5 HBs x 15 min
222
+ const nowSecs = Math.floor(Date.now() / 1000);
223
+ for (const retro of retros) {
224
+ if (!retro || retro.removed)
225
+ continue;
226
+ if (retro.status !== 'open' && retro.status !== 'discussed')
227
+ continue;
228
+ const author = (retro.author ?? '').toLowerCase();
229
+ if (!author || author === myAddr)
230
+ continue;
231
+ const age = retro.createdAt ? nowSecs - retro.createdAt : Infinity;
232
+ if (age > freshThresholdSecs)
233
+ continue;
234
+ const discussion = Array.isArray(retro.discussion) ? retro.discussion : [];
235
+ const alreadyResponded = discussion.some((e) => (e?.author ?? '').toLowerCase() === myAddr);
236
+ if (alreadyResponded)
237
+ continue;
238
+ const changeCount = Array.isArray(retro.proposedChanges)
239
+ ? retro.proposedChanges.length
240
+ : 0;
241
+ actions.push({
242
+ priority: 'HIGH',
243
+ type: 'retro-respond',
244
+ detail: `Retro "${retro.id}" by ${author.slice(0, 10)} needs your response ` +
245
+ `(${changeCount} proposed change${changeCount === 1 ? '' : 's'}, ` +
246
+ `${Math.floor(age / 60)}min old). ` +
247
+ `Run: pop brain retro show ${retro.id} && pop brain retro respond --to ${retro.id} --message "..."`,
248
+ data: {
249
+ retroId: retro.id,
250
+ author,
251
+ changeCount,
252
+ ageSeconds: age,
253
+ },
254
+ });
255
+ }
256
+ }
257
+ finally {
258
+ // Tear down the brain node so the triage process can exit
259
+ // cleanly. readBrainDoc caches the helia instance and it
260
+ // will hold the event loop open otherwise.
261
+ try {
262
+ await stopBrainNode();
263
+ }
264
+ catch { /* best-effort */ }
265
+ }
266
+ }
267
+ }
268
+ }
269
+ catch {
270
+ // Brain retro check is best-effort — a missing manifest, a
271
+ // malformed doc, or a transient helia error should never break
272
+ // triage for the rest of the org state.
273
+ }
274
+ // Open brainstorms needing response (task #354 phase c, HB#209).
275
+ // Same shape as the retro hook above but for pop.brain.brainstorms.
276
+ // Surface HIGH action when an open brainstorm exists whose author is
277
+ // NOT me AND I have not yet posted a message, added an idea, or cast
278
+ // a vote. "Fresh" window is the same 75-min (5 HB) threshold.
279
+ try {
280
+ const manifestPath = path.join((0, os_1.homedir)(), '.pop-agent', 'brain', 'doc-heads.json');
281
+ if (fs.existsSync(manifestPath)) {
282
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
283
+ if (manifest['pop.brain.brainstorms']) {
284
+ const { readBrainDoc, stopBrainNode } = require('../../lib/brain');
285
+ try {
286
+ const { doc: brainstormsDoc } = await readBrainDoc('pop.brain.brainstorms');
287
+ const brainstorms = Array.isArray(brainstormsDoc?.brainstorms)
288
+ ? brainstormsDoc.brainstorms
289
+ : [];
290
+ const freshThresholdSecs = 75 * 60; // same as retro cadence
291
+ const nowSecs = Math.floor(Date.now() / 1000);
292
+ for (const b of brainstorms) {
293
+ if (!b || b.removed)
294
+ continue;
295
+ // Only "open" or "voting" brainstorms need responses;
296
+ // "closed" and "promoted" have been resolved already
297
+ if (b.status !== 'open' && b.status !== 'voting')
298
+ continue;
299
+ const author = (b.author ?? '').toLowerCase();
300
+ if (!author || author === myAddr)
301
+ continue;
302
+ const age = b.openedAt ? nowSecs - b.openedAt : Infinity;
303
+ if (age > freshThresholdSecs)
304
+ continue;
305
+ // Have I already engaged with this brainstorm? Check both
306
+ // the discussion array (for --message posts) and any idea's
307
+ // votes map (for --vote casts)
308
+ const discussion = Array.isArray(b.discussion) ? b.discussion : [];
309
+ const postedMessage = discussion.some((e) => (e?.author ?? '').toLowerCase() === myAddr);
310
+ const ideas = Array.isArray(b.ideas) ? b.ideas : [];
311
+ const voted = ideas.some((idea) => {
312
+ const votes = idea?.votes;
313
+ return votes && typeof votes === 'object' && votes[myAddr] != null;
314
+ });
315
+ const addedIdea = ideas.some((idea) => (idea?.author ?? '').toLowerCase() === myAddr);
316
+ if (postedMessage || voted || addedIdea)
317
+ continue;
318
+ const ideaCount = ideas.length;
319
+ actions.push({
320
+ priority: 'HIGH',
321
+ type: 'brainstorm-respond',
322
+ detail: `Brainstorm "${b.id}" by ${author.slice(0, 10)} needs your response ` +
323
+ `(${ideaCount} idea${ideaCount === 1 ? '' : 's'}, ` +
324
+ `status=${b.status}, ${Math.floor(age / 60)}min old). ` +
325
+ `Run: pop brain read --doc pop.brain.brainstorms --json | less && ` +
326
+ `pop brain brainstorm-respond --id ${b.id} --message "..." [--add-idea "..."] [--vote idea-id=support]`,
327
+ data: {
328
+ brainstormId: b.id,
329
+ author,
330
+ ideaCount,
331
+ status: b.status,
332
+ ageSeconds: age,
333
+ },
334
+ });
335
+ }
336
+ }
337
+ finally {
338
+ try {
339
+ await stopBrainNode();
340
+ }
341
+ catch { /* best-effort */ }
342
+ }
343
+ }
344
+ }
345
+ }
346
+ catch {
347
+ // Brainstorm check is best-effort — same isolation as the retro check.
348
+ }
349
+ // Unclaimed distributions — skip ones known to have no allocation for this address
350
+ const noAllocSet = (0, no_alloc_cache_1.getNoAllocationSet)(myAddr);
351
+ const orgIdLower = modules.orgId.toLowerCase();
352
+ const unclaimedDist = (org.paymentManager?.distributions || []).filter((d) => {
353
+ // Skip if I've already claimed it
354
+ if ((d.claims || []).some((c) => c.claimer?.toLowerCase() === myAddr))
355
+ return false;
356
+ // Skip if I've verified I have no allocation in this distribution before
357
+ const cacheKey = `${orgIdLower}-${d.distributionId}`;
358
+ if (noAllocSet.has(cacheKey))
359
+ return false;
360
+ return true;
361
+ });
362
+ if (unclaimedDist.length > 0) {
363
+ actions.push({ priority: 'HIGH', type: 'claim', detail: `${unclaimedDist.length} unclaimed distribution(s) — run claim-mine.` });
364
+ }
365
+ // --- 3. WORK (MEDIUM) ---
366
+ // My assigned tasks
367
+ const myAssigned = allTasks.filter((t) => t.assignee?.toLowerCase() === myAddr && t.status === 'Assigned' && parseInt(t.rejectionCount || '0') === 0);
368
+ for (const t of myAssigned) {
369
+ actions.push({ priority: 'MEDIUM', type: 'work', detail: `Task #${t.taskId} "${t.title}" assigned to you.`, data: { taskId: t.taskId } });
370
+ }
371
+ // Open tasks available to claim
372
+ const openTasks = allTasks.filter((t) => t.status === 'Open');
373
+ if (openTasks.length > 0) {
374
+ actions.push({ priority: 'MEDIUM', type: 'claim-task', detail: `${openTasks.length} open task(s) available to claim.`, data: { tasks: openTasks.map((t) => ({ id: t.taskId, title: t.title })) } });
375
+ }
376
+ // --- 4. PLAN (LOW) ---
377
+ const hasWork = myAssigned.length > 0 || openTasks.length > 0 || pendingReviews.length > 0;
378
+ if (!hasWork && expiredCandidates.length === 0 && myRejected.length === 0) {
379
+ actions.push({ priority: 'LOW', type: 'plan', detail: 'Board is empty — planning mandatory. Read goals.md, create tasks, explore capabilities.' });
380
+ }
381
+ // --- 4b. AUDIT OPPORTUNITIES (when board is empty) ---
382
+ if (!hasWork) {
383
+ try {
384
+ const { queryAllChains } = require('@poa-box/cli/lib/subgraph');
385
+ const exploreQuery = `query($first:Int!){organizations(first:$first){name users(first:100){membershipStatus} taskManager{projects(where:{deleted:false},first:10){tasks(first:200){status}}} hybridVoting{proposals(first:50){status}}}}`;
386
+ const exploreResults = await queryAllChains(exploreQuery, { first: 10 });
387
+ for (const cr of exploreResults) {
388
+ if (!cr.data?.organizations)
389
+ continue;
390
+ for (const org of cr.data.organizations) {
391
+ if (org.name === 'Argus' || /^test/i.test(org.name))
392
+ continue;
393
+ const members = (org.users || []).filter((u) => u.membershipStatus === 'Active').length;
394
+ if (members < 2)
395
+ continue;
396
+ const allTasks = (org.taskManager?.projects || []).flatMap((p) => p.tasks || []);
397
+ const completed = allTasks.filter((t) => t.status === 'Completed').length;
398
+ const proposals = org.hybridVoting?.proposals?.length || 0;
399
+ if (members >= 3 && proposals === 0) {
400
+ actions.push({ priority: 'MEDIUM', type: 'audit-opportunity', detail: `${org.name} (${cr.name}): ${members} members, 0 proposals — audit opportunity.` });
401
+ }
402
+ }
403
+ }
404
+ }
405
+ catch { /* non-critical */ }
406
+ }
407
+ // --- 5. CHANGE DETECTION ---
408
+ // New members
409
+ const memberNames = activeMembers.map((u) => u.account?.username || u.address.slice(0, 10));
410
+ for (const name of memberNames) {
411
+ if (lastState && !lastState.includes(name)) {
412
+ changes.push({ priority: 'INFO', type: 'member_joined', detail: `New member: ${name}` });
413
+ }
414
+ }
415
+ // PT milestones
416
+ const ptNum = parseFloat(totalSupply);
417
+ const milestones = [100, 250, 500, 1000, 2000, 5000];
418
+ for (const m of milestones) {
419
+ if (ptNum >= m && lastState && !lastState.includes(`${m}`)) {
420
+ // Rough check — could false-positive but better than nothing
421
+ }
422
+ }
423
+ // Proposal state changes
424
+ const executedProposals = proposals.filter((p) => p.status === 'Executed');
425
+ for (const p of executedProposals) {
426
+ if (lastState && !lastState.includes(`#${p.proposalId}`)) {
427
+ changes.push({ priority: 'INFO', type: 'proposal_executed', detail: `Proposal #${p.proposalId} "${p.title}" was executed.` });
428
+ }
429
+ }
430
+ // Sort actions by priority
431
+ const priorityOrder = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3, INFO: 4 };
432
+ actions.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]);
433
+ spin.stop();
434
+ // --- Output ---
435
+ const context = {
436
+ gas: `${gasEther.toFixed(3)} ${networkConfig.nativeCurrency.symbol}`,
437
+ gasStatus: gasEther < 0.01 ? 'CRITICAL' : gasEther < 0.1 ? 'LOW' : 'HEALTHY',
438
+ members: activeMembers.length,
439
+ ptSupply: Math.round(ptNum),
440
+ pendingVotes: actions.filter(a => a.type === 'vote').length,
441
+ pendingReviews: pendingReviews.length,
442
+ rejectedTasks: myRejected.length,
443
+ openTasks: openTasks.length,
444
+ assignedTasks: myAssigned.length,
445
+ boardState: hasWork ? 'has-work' : 'empty',
446
+ };
447
+ if (output.isJsonMode()) {
448
+ output.json({ actions, changes, context });
449
+ }
450
+ else {
451
+ console.log('');
452
+ console.log(' Agent Triage');
453
+ console.log(' ════════════');
454
+ if (actions.length === 0) {
455
+ console.log(' No actions needed.');
456
+ }
457
+ else {
458
+ for (const a of actions) {
459
+ const icon = a.priority === 'CRITICAL' ? '\x1b[31m!!\x1b[0m' :
460
+ a.priority === 'HIGH' ? '\x1b[33m!\x1b[0m' :
461
+ a.priority === 'MEDIUM' ? '\x1b[36m·\x1b[0m' :
462
+ a.priority === 'LOW' ? '\x1b[90m○\x1b[0m' : '\x1b[90mℹ\x1b[0m';
463
+ console.log(` ${icon} [${a.priority}] ${a.detail}`);
464
+ }
465
+ }
466
+ if (changes.length > 0) {
467
+ console.log('');
468
+ console.log(' Changes since last heartbeat:');
469
+ for (const c of changes) {
470
+ console.log(` △ ${c.detail}`);
471
+ }
472
+ }
473
+ console.log('');
474
+ console.log(` Context: ${context.members} members | ${context.ptSupply} PT | Gas: ${context.gas} (${context.gasStatus}) | Board: ${context.boardState}`);
475
+ console.log('');
476
+ }
477
+ // --- Auto-update org-state.md for next triage's change detection ---
478
+ const executedIds = executedProposals.map((p) => `#${p.proposalId}`).join(', ');
479
+ const memberList = activeMembers.map((u) => u.account?.username || u.address.slice(0, 10)).join(', ');
480
+ const stateSnapshot = [
481
+ `# Org State — ${org.name}`,
482
+ `*Auto-updated by triage: ${new Date().toISOString()}*`,
483
+ '',
484
+ `Members: ${memberList}`,
485
+ `PT Supply: ${Math.round(ptNum)}`,
486
+ `Completed tasks: ${allTasks.filter((t) => t.status === 'Completed').length}`,
487
+ `Executed proposals: ${executedIds}`,
488
+ `Gas: ${gasEther.toFixed(3)} ${networkConfig.nativeCurrency.symbol}`,
489
+ ].join('\n');
490
+ try {
491
+ fs.writeFileSync(orgStatePath, stateSnapshot + '\n');
492
+ }
493
+ catch {
494
+ // Non-critical — org-state.md write failure shouldn't break triage
495
+ }
496
+ }
497
+ catch (err) {
498
+ spin.stop();
499
+ output.error(err.message);
500
+ process.exit(1);
501
+ }
502
+ },
503
+ };
@@ -0,0 +1,42 @@
1
+ /**
2
+ * pop brain advance-stage — move a project through the lifecycle.
3
+ *
4
+ * Second write command for the pop.brain.projects doc shape. Finds a
5
+ * project by id, advances its stage to either an explicit --to target
6
+ * or the next stage in the canonical PROPOSE → DISCUSS → PLAN → VOTE
7
+ * → EXECUTE → REVIEW → SHIP sequence, and records a lastStageAdvanceAt
8
+ * timestamp. Short-circuits no-op transitions, errors cleanly on
9
+ * not-found, and refuses to advance past 'ship'.
10
+ *
11
+ * Design note: the canonical order is a SOFT convention at the data
12
+ * layer — any stage string is accepted on disk (so legacy / scripted
13
+ * writes can use custom stages). advance-stage enforces the order
14
+ * only for the --to-omitted "next stage" case. Passing --to <stage>
15
+ * sets the stage directly without checking that the transition makes
16
+ * sense (this allows 'discuss → ship' when the operator knows what
17
+ * they're doing, at the cost of letting a typo jump over stages).
18
+ */
19
+ import type { Argv, ArgumentsCamelCase } from 'yargs';
20
+ import type { ProjectStage } from '../../lib/brain-projections';
21
+ interface AdvanceArgs {
22
+ doc: string;
23
+ projectId: string;
24
+ to?: ProjectStage;
25
+ 'idempotency-key'?: string;
26
+ 'no-idempotency'?: boolean;
27
+ }
28
+ export declare const advanceStageHandler: {
29
+ builder: (yargs: Argv) => Argv<{
30
+ doc: string;
31
+ } & {
32
+ "project-id": string;
33
+ } & {
34
+ to: ProjectStage | undefined;
35
+ } & {
36
+ "idempotency-key": string | undefined;
37
+ } & {
38
+ "no-idempotency": boolean;
39
+ }>;
40
+ handler: (argv: ArgumentsCamelCase<AdvanceArgs>) => Promise<void>;
41
+ };
42
+ export {};