@pinet/slack-bridge 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 (179) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +570 -0
  3. package/dist/activity-log.d.ts +62 -0
  4. package/dist/activity-log.js +293 -0
  5. package/dist/agent-completion-runtime.d.ts +17 -0
  6. package/dist/agent-completion-runtime.js +26 -0
  7. package/dist/agent-event-runtime.d.ts +13 -0
  8. package/dist/agent-event-runtime.js +27 -0
  9. package/dist/agent-prompt-guidance.d.ts +22 -0
  10. package/dist/agent-prompt-guidance.js +46 -0
  11. package/dist/broker/adapters/slack.d.ts +74 -0
  12. package/dist/broker/adapters/slack.js +563 -0
  13. package/dist/broker/adapters/types.d.ts +1 -0
  14. package/dist/broker/adapters/types.js +1 -0
  15. package/dist/broker/agent-messaging.d.ts +1 -0
  16. package/dist/broker/agent-messaging.js +1 -0
  17. package/dist/broker/auth.d.ts +1 -0
  18. package/dist/broker/auth.js +1 -0
  19. package/dist/broker/client.d.ts +167 -0
  20. package/dist/broker/client.js +624 -0
  21. package/dist/broker/control-plane-dashboard.d.ts +98 -0
  22. package/dist/broker/control-plane-dashboard.js +213 -0
  23. package/dist/broker/ghost-reaper.d.ts +49 -0
  24. package/dist/broker/ghost-reaper.js +209 -0
  25. package/dist/broker/index.d.ts +36 -0
  26. package/dist/broker/index.js +90 -0
  27. package/dist/broker/leader.d.ts +1 -0
  28. package/dist/broker/leader.js +1 -0
  29. package/dist/broker/maintenance.d.ts +1 -0
  30. package/dist/broker/maintenance.js +1 -0
  31. package/dist/broker/message-send.d.ts +1 -0
  32. package/dist/broker/message-send.js +1 -0
  33. package/dist/broker/paths.d.ts +1 -0
  34. package/dist/broker/paths.js +1 -0
  35. package/dist/broker/raw-tcp-loopback.d.ts +1 -0
  36. package/dist/broker/raw-tcp-loopback.js +1 -0
  37. package/dist/broker/router.d.ts +1 -0
  38. package/dist/broker/router.js +1 -0
  39. package/dist/broker/schema.d.ts +9 -0
  40. package/dist/broker/schema.js +63 -0
  41. package/dist/broker/socket-server.d.ts +120 -0
  42. package/dist/broker/socket-server.js +1087 -0
  43. package/dist/broker/types.d.ts +1 -0
  44. package/dist/broker/types.js +1 -0
  45. package/dist/broker-delivery.d.ts +10 -0
  46. package/dist/broker-delivery.js +26 -0
  47. package/dist/broker-inbound-persistence.d.ts +11 -0
  48. package/dist/broker-inbound-persistence.js +22 -0
  49. package/dist/broker-prompt-loader.d.ts +34 -0
  50. package/dist/broker-prompt-loader.js +185 -0
  51. package/dist/broker-runtime-access.d.ts +14 -0
  52. package/dist/broker-runtime-access.js +20 -0
  53. package/dist/broker-runtime.d.ts +100 -0
  54. package/dist/broker-runtime.js +533 -0
  55. package/dist/broker-thread-owner-hints.d.ts +10 -0
  56. package/dist/broker-thread-owner-hints.js +14 -0
  57. package/dist/canvases.d.ts +80 -0
  58. package/dist/canvases.js +221 -0
  59. package/dist/command-registration-runtime.d.ts +9 -0
  60. package/dist/command-registration-runtime.js +9 -0
  61. package/dist/core-tool-guardrails.d.ts +24 -0
  62. package/dist/core-tool-guardrails.js +66 -0
  63. package/dist/deploy-manifest.d.ts +27 -0
  64. package/dist/deploy-manifest.js +197 -0
  65. package/dist/follower-delivery.d.ts +16 -0
  66. package/dist/follower-delivery.js +70 -0
  67. package/dist/follower-runtime.d.ts +61 -0
  68. package/dist/follower-runtime.js +350 -0
  69. package/dist/git-metadata.d.ts +35 -0
  70. package/dist/git-metadata.js +88 -0
  71. package/dist/guardrails.d.ts +69 -0
  72. package/dist/guardrails.js +283 -0
  73. package/dist/helpers.d.ts +550 -0
  74. package/dist/helpers.js +2711 -0
  75. package/dist/home-tab.d.ts +27 -0
  76. package/dist/home-tab.js +242 -0
  77. package/dist/imessage-tools.d.ts +33 -0
  78. package/dist/imessage-tools.js +96 -0
  79. package/dist/inbox-drain-runtime.d.ts +30 -0
  80. package/dist/inbox-drain-runtime.js +71 -0
  81. package/dist/index.d.ts +2 -0
  82. package/dist/index.js +1330 -0
  83. package/dist/persisted-runtime-state.d.ts +52 -0
  84. package/dist/persisted-runtime-state.js +98 -0
  85. package/dist/pinet-activity-formatting.d.ts +20 -0
  86. package/dist/pinet-activity-formatting.js +43 -0
  87. package/dist/pinet-agent-status.d.ts +36 -0
  88. package/dist/pinet-agent-status.js +48 -0
  89. package/dist/pinet-commands.d.ts +70 -0
  90. package/dist/pinet-commands.js +402 -0
  91. package/dist/pinet-confirmation-replies.d.ts +7 -0
  92. package/dist/pinet-confirmation-replies.js +30 -0
  93. package/dist/pinet-control-plane-dashboard.d.ts +34 -0
  94. package/dist/pinet-control-plane-dashboard.js +86 -0
  95. package/dist/pinet-home-tabs.d.ts +36 -0
  96. package/dist/pinet-home-tabs.js +101 -0
  97. package/dist/pinet-maintenance-delivery.d.ts +22 -0
  98. package/dist/pinet-maintenance-delivery.js +45 -0
  99. package/dist/pinet-mesh-ops.d.ts +97 -0
  100. package/dist/pinet-mesh-ops.js +266 -0
  101. package/dist/pinet-registration-gate.d.ts +14 -0
  102. package/dist/pinet-registration-gate.js +42 -0
  103. package/dist/pinet-remote-control-acks.d.ts +16 -0
  104. package/dist/pinet-remote-control-acks.js +46 -0
  105. package/dist/pinet-remote-control.d.ts +17 -0
  106. package/dist/pinet-remote-control.js +74 -0
  107. package/dist/pinet-runtime-composition.d.ts +27 -0
  108. package/dist/pinet-runtime-composition.js +20 -0
  109. package/dist/pinet-tools.d.ts +67 -0
  110. package/dist/pinet-tools.js +1211 -0
  111. package/dist/prompts/broker/default.md +41 -0
  112. package/dist/prompts/broker/tmux.md +49 -0
  113. package/dist/ralph-loop.d.ts +76 -0
  114. package/dist/ralph-loop.js +524 -0
  115. package/dist/reaction-triggers.d.ts +25 -0
  116. package/dist/reaction-triggers.js +171 -0
  117. package/dist/repo-tool-guardrails.d.ts +20 -0
  118. package/dist/repo-tool-guardrails.js +81 -0
  119. package/dist/runtime-agent-context.d.ts +93 -0
  120. package/dist/runtime-agent-context.js +302 -0
  121. package/dist/runtime-mode.d.ts +10 -0
  122. package/dist/runtime-mode.js +39 -0
  123. package/dist/scheduled-wakeups.d.ts +1 -0
  124. package/dist/scheduled-wakeups.js +1 -0
  125. package/dist/session-ui-runtime.d.ts +20 -0
  126. package/dist/session-ui-runtime.js +163 -0
  127. package/dist/single-player-runtime.d.ts +71 -0
  128. package/dist/single-player-runtime.js +436 -0
  129. package/dist/skins/cosmere.json +1571 -0
  130. package/dist/skins/foundation.json +1304 -0
  131. package/dist/slack-access.d.ts +186 -0
  132. package/dist/slack-access.js +462 -0
  133. package/dist/slack-api.d.ts +2 -0
  134. package/dist/slack-api.js +1 -0
  135. package/dist/slack-block-kit.d.ts +36 -0
  136. package/dist/slack-block-kit.js +242 -0
  137. package/dist/slack-export.d.ts +31 -0
  138. package/dist/slack-export.js +206 -0
  139. package/dist/slack-message-context.d.ts +15 -0
  140. package/dist/slack-message-context.js +207 -0
  141. package/dist/slack-modals.d.ts +13 -0
  142. package/dist/slack-modals.js +79 -0
  143. package/dist/slack-pinet-runtime-adapter.d.ts +16 -0
  144. package/dist/slack-pinet-runtime-adapter.js +72 -0
  145. package/dist/slack-presence.d.ts +36 -0
  146. package/dist/slack-presence.js +89 -0
  147. package/dist/slack-request-runtime.d.ts +8 -0
  148. package/dist/slack-request-runtime.js +19 -0
  149. package/dist/slack-runtime-access.d.ts +28 -0
  150. package/dist/slack-runtime-access.js +107 -0
  151. package/dist/slack-scope-diagnostics.d.ts +31 -0
  152. package/dist/slack-scope-diagnostics.js +235 -0
  153. package/dist/slack-socket-dedup.d.ts +7 -0
  154. package/dist/slack-socket-dedup.js +135 -0
  155. package/dist/slack-thread-status.d.ts +41 -0
  156. package/dist/slack-thread-status.js +156 -0
  157. package/dist/slack-tool-policy-runtime.d.ts +36 -0
  158. package/dist/slack-tool-policy-runtime.js +96 -0
  159. package/dist/slack-tools.d.ts +52 -0
  160. package/dist/slack-tools.js +2688 -0
  161. package/dist/slack-turn-guardrails.d.ts +19 -0
  162. package/dist/slack-turn-guardrails.js +36 -0
  163. package/dist/slack-upload.d.ts +49 -0
  164. package/dist/slack-upload.js +224 -0
  165. package/dist/task-assignments.d.ts +45 -0
  166. package/dist/task-assignments.js +527 -0
  167. package/dist/thread-confirmations.d.ts +22 -0
  168. package/dist/thread-confirmations.js +139 -0
  169. package/dist/tool-registration-runtime.d.ts +13 -0
  170. package/dist/tool-registration-runtime.js +13 -0
  171. package/dist/ttl-cache.d.ts +47 -0
  172. package/dist/ttl-cache.js +105 -0
  173. package/manifest.yaml +57 -0
  174. package/package.json +60 -0
  175. package/skills/pinet-skin-creator/SKILL.md +109 -0
  176. package/skills/pinet-skin-creator/references/descriptor-format.md +99 -0
  177. package/skills/pinet-skin-creator/references/safety-checklist.md +56 -0
  178. package/skills/pinet-skin-creator/templates/pinet-skin-descriptor.json +77 -0
  179. package/skills/slack-bridge/SKILL.md +360 -0
@@ -0,0 +1,524 @@
1
+ import * as os from "node:os";
2
+ import { evaluateRalphLoopCycle, rewriteRalphLoopGhostAnomalies, buildAgentDisplayInfo, buildRalphLoopNudgeMessage, buildRalphLoopAnomalySignature, buildRalphLoopCycleNotifications, buildRalphLoopStatusMessage, shouldDeliverRalphLoopFollowUp, filterAgentsForMeshVisibility, resolveRalphLoopIntervalMs, resolveRalphSnoozeAfterEmptyCycles, resolveRalphSnoozeDurationMs, DEFAULT_RALPH_LOOP_NUDGE_COOLDOWN_MS, DEFAULT_RALPH_LOOP_FOLLOW_UP_COOLDOWN_MS, DEFAULT_RALPH_LOOP_STUCK_WORKING_THRESHOLD_MS, } from "./helpers.js";
3
+ import { DEFAULT_HEARTBEAT_TIMEOUT_MS } from "./broker/socket-server.js";
4
+ import { HEARTBEAT_INTERVAL_MS } from "./broker/client.js";
5
+ import { getPendingTaskAssignmentReport, hasTaskAssignmentStatusChange, normalizeTrackedTaskAssignments, resolveTaskAssignments, } from "./task-assignments.js";
6
+ import { probeGitBranch } from "./git-metadata.js";
7
+ export function createRalphLoopState() {
8
+ return {
9
+ timer: null,
10
+ running: false,
11
+ nudges: new Map(),
12
+ reportedGhosts: new Set(),
13
+ ghostBaselineHydrated: false,
14
+ nonGhostSignature: "",
15
+ hadOutstandingAnomalies: false,
16
+ followUpAt: 0,
17
+ followUpPending: false,
18
+ followUpSignature: "",
19
+ taskAssignmentReportSignature: "",
20
+ pendingTaskAssignmentReport: null,
21
+ snoozeUntilMs: 0,
22
+ snoozeReason: null,
23
+ snoozeSource: null,
24
+ snoozeEmptyCycleCount: 0,
25
+ };
26
+ }
27
+ export function resetRalphLoopState(state) {
28
+ if (state.timer) {
29
+ clearInterval(state.timer);
30
+ state.timer = null;
31
+ }
32
+ state.running = false;
33
+ state.nudges.clear();
34
+ state.reportedGhosts.clear();
35
+ state.ghostBaselineHydrated = false;
36
+ state.nonGhostSignature = "";
37
+ state.hadOutstandingAnomalies = false;
38
+ state.followUpAt = 0;
39
+ state.followUpPending = false;
40
+ state.followUpSignature = "";
41
+ state.taskAssignmentReportSignature = "";
42
+ state.pendingTaskAssignmentReport = null;
43
+ clearRalphLoopSnooze(state);
44
+ }
45
+ export function getRalphLoopSnoozeStatus(state, now = Date.now()) {
46
+ const remainingMs = Math.max(0, state.snoozeUntilMs - now);
47
+ return {
48
+ active: remainingMs > 0,
49
+ until: remainingMs > 0 ? new Date(state.snoozeUntilMs).toISOString() : null,
50
+ remainingMs,
51
+ reason: remainingMs > 0 ? state.snoozeReason : null,
52
+ source: remainingMs > 0 ? state.snoozeSource : null,
53
+ emptyCycleCount: state.snoozeEmptyCycleCount,
54
+ };
55
+ }
56
+ export function setRalphLoopSnooze(state, input) {
57
+ const now = input.now ?? Date.now();
58
+ state.snoozeUntilMs = now + Math.max(0, Math.trunc(input.durationMs));
59
+ state.snoozeReason = input.reason?.trim() || null;
60
+ state.snoozeSource = input.source ?? "manual";
61
+ return getRalphLoopSnoozeStatus(state, now);
62
+ }
63
+ export function clearRalphLoopSnooze(state) {
64
+ state.snoozeUntilMs = 0;
65
+ state.snoozeReason = null;
66
+ state.snoozeSource = null;
67
+ state.snoozeEmptyCycleCount = 0;
68
+ }
69
+ function isActiveTrackedAssignment(assignment) {
70
+ return (assignment.issueState !== "CLOSED" &&
71
+ assignment.status !== "pr_merged" &&
72
+ assignment.status !== "pr_closed");
73
+ }
74
+ function formatTrackedAssignmentIssueList(issueNumbers) {
75
+ return issueNumbers.map((issueNumber) => `#${issueNumber}`).join(", ");
76
+ }
77
+ function buildTrackedAssignmentIdleReplyStallAnomaly(agentName, issueNumbers) {
78
+ const label = issueNumbers.length === 1 ? "assignment" : "assignments";
79
+ return `${agentName} idle after tracked ${label} ${formatTrackedAssignmentIssueList(issueNumbers)} without any agent reply to the original sender`;
80
+ }
81
+ export function buildTrackedAssignmentReplyNudgeMessage(issueNumbers, cycleStartedAt) {
82
+ const prefix = cycleStartedAt ? `RALPH LOOP nudge (${cycleStartedAt})` : "RALPH LOOP nudge";
83
+ const label = issueNumbers.length === 1 ? "assignment" : "assignments";
84
+ return `${prefix}: you are idle after tracked ${label} ${formatTrackedAssignmentIssueList(issueNumbers)} and still have not sent any agent reply to the original sender. Please report outcome or blocker now.`;
85
+ }
86
+ export function applyTrackedAssignmentIdleReplyStalls(evaluation, workloads, awaitingReplyAssignments, options = {}) {
87
+ const idleHealthyWorkloads = new Map(workloads
88
+ .filter((workload) => {
89
+ if (workload.status !== "idle" || workload.disconnectedAt != null) {
90
+ return false;
91
+ }
92
+ return buildAgentDisplayInfo({ emoji: "", ...workload }, options).health === "healthy";
93
+ })
94
+ .map((workload) => [workload.id, workload]));
95
+ const pendingIssuesByAgent = new Map();
96
+ for (const assignment of awaitingReplyAssignments) {
97
+ if (!idleHealthyWorkloads.has(assignment.agentId)) {
98
+ continue;
99
+ }
100
+ const issues = pendingIssuesByAgent.get(assignment.agentId) ?? new Set();
101
+ issues.add(assignment.issueNumber);
102
+ pendingIssuesByAgent.set(assignment.agentId, issues);
103
+ }
104
+ const result = new Map();
105
+ for (const [agentId, issues] of pendingIssuesByAgent) {
106
+ const issueNumbers = [...issues].sort((left, right) => left - right);
107
+ if (!evaluation.nudgeAgentIds.includes(agentId)) {
108
+ evaluation.nudgeAgentIds.push(agentId);
109
+ }
110
+ const agentName = idleHealthyWorkloads.get(agentId)?.name ?? agentId;
111
+ evaluation.anomalies.push(buildTrackedAssignmentIdleReplyStallAnomaly(agentName, issueNumbers));
112
+ result.set(agentId, issueNumbers);
113
+ }
114
+ return result;
115
+ }
116
+ // ─── Callbacks ───────────────────────────────────────────
117
+ export function hydrateRalphLoopReportedGhosts(state, recentCycles) {
118
+ if (state.ghostBaselineHydrated) {
119
+ return;
120
+ }
121
+ if (state.reportedGhosts.size > 0) {
122
+ state.ghostBaselineHydrated = true;
123
+ return;
124
+ }
125
+ for (const ghostId of recentCycles[0]?.ghostAgentIds ?? []) {
126
+ state.reportedGhosts.add(ghostId);
127
+ }
128
+ state.ghostBaselineHydrated = true;
129
+ }
130
+ // ─── Core loop ───────────────────────────────────────────
131
+ export async function runRalphLoopCycle(ctx, state, deps) {
132
+ const db = deps.getBrokerDb();
133
+ const selfId = deps.getBrokerAgentId();
134
+ if (!db || !selfId || state.running)
135
+ return;
136
+ state.running = true;
137
+ const cycleStartedAt = new Date().toISOString();
138
+ const cycleStartMs = Date.now();
139
+ try {
140
+ hydrateRalphLoopReportedGhosts(state, db.getRecentRalphCycles(1));
141
+ deps.runMaintenance(ctx);
142
+ const lastMaintenance = deps.getLastMaintenance();
143
+ const currentBranch = (await probeGitBranch(process.cwd())) ?? null;
144
+ const now = Date.now();
145
+ const recentGhostWindowMs = DEFAULT_HEARTBEAT_TIMEOUT_MS * 2;
146
+ const workloads = filterAgentsForMeshVisibility(db.getAllAgents(), {
147
+ now,
148
+ includeGhosts: true,
149
+ recentDisconnectWindowMs: recentGhostWindowMs,
150
+ }).map((agent) => ({
151
+ ...agent,
152
+ pendingInboxCount: db.getPendingInboxCount(agent.id),
153
+ ownedThreadCount: db.getOwnedThreadCount(agent.id),
154
+ }));
155
+ const pendingBacklogCount = db.getBacklogCount("pending");
156
+ const evaluationOptions = {
157
+ now,
158
+ heartbeatTimeoutMs: DEFAULT_HEARTBEAT_TIMEOUT_MS,
159
+ heartbeatIntervalMs: HEARTBEAT_INTERVAL_MS,
160
+ stuckWorkingThresholdMs: DEFAULT_RALPH_LOOP_STUCK_WORKING_THRESHOLD_MS,
161
+ pendingBacklogCount,
162
+ currentBranch,
163
+ brokerHeartbeatActive: deps.heartbeatTimerActive(),
164
+ brokerMaintenanceActive: deps.maintenanceTimerActive(),
165
+ brokerAgentId: selfId,
166
+ };
167
+ const evaluation = evaluateRalphLoopCycle(workloads, evaluationOptions);
168
+ const trackedAssignmentIdleReplyStalls = applyTrackedAssignmentIdleReplyStalls(evaluation, workloads, db.listTaskAssignmentsAwaitingFirstReply(), evaluationOptions);
169
+ const nudgeAgentIds = new Set(evaluation.nudgeAgentIds);
170
+ for (const workload of workloads) {
171
+ if (!nudgeAgentIds.has(workload.id)) {
172
+ state.nudges.delete(workload.id);
173
+ continue;
174
+ }
175
+ const lastNudgeAt = state.nudges.get(workload.id) ?? 0;
176
+ if (now - lastNudgeAt < DEFAULT_RALPH_LOOP_NUDGE_COOLDOWN_MS) {
177
+ continue;
178
+ }
179
+ const trackedAssignmentIssues = trackedAssignmentIdleReplyStalls.get(workload.id);
180
+ deps.sendMaintenanceMessage(workload.id, trackedAssignmentIssues
181
+ ? buildTrackedAssignmentReplyNudgeMessage(trackedAssignmentIssues, cycleStartedAt)
182
+ : buildRalphLoopNudgeMessage(workload.pendingInboxCount, workload.ownedThreadCount, cycleStartedAt));
183
+ state.nudges.set(workload.id, now);
184
+ }
185
+ const ghostRewrite = rewriteRalphLoopGhostAnomalies(evaluation, state.reportedGhosts, {
186
+ suppressedGhostIds: lastMaintenance?.reapedAgentIds ?? [],
187
+ });
188
+ state.reportedGhosts.clear();
189
+ for (const ghostId of ghostRewrite.nextReportedGhostIds) {
190
+ state.reportedGhosts.add(ghostId);
191
+ }
192
+ const visibleEvaluation = ghostRewrite.evaluation;
193
+ const visibleSignature = buildRalphLoopAnomalySignature(visibleEvaluation);
194
+ const nonGhostSignature = ghostRewrite.nonGhostAnomalies.join("|");
195
+ const hasOutstandingAnomalies = visibleEvaluation.ghostAgentIds.length > 0 || visibleEvaluation.anomalies.length > 0;
196
+ const ralphNotifications = buildRalphLoopCycleNotifications(visibleEvaluation, cycleStartedAt);
197
+ const followUpPrompt = ghostRewrite.newGhostIds.length === 0 &&
198
+ ghostRewrite.clearedGhostIds.length > 0 &&
199
+ ghostRewrite.nonGhostAnomalies.length === 0
200
+ ? null
201
+ : ralphNotifications.followUpPrompt;
202
+ const agentsById = new Map(workloads.map((workload) => [workload.id, { emoji: workload.emoji, name: workload.name }]));
203
+ let projectedAssignments = [];
204
+ let taskProgressChanged = false;
205
+ const rawTrackedAssignments = db.listTaskAssignments();
206
+ const trackedAssignmentSourceIds = [
207
+ ...new Set(rawTrackedAssignments
208
+ .map((assignment) => assignment.sourceMessageId)
209
+ .filter((messageId) => messageId != null)),
210
+ ];
211
+ const trackedAssignments = normalizeTrackedTaskAssignments(rawTrackedAssignments, new Map(db
212
+ .getMessagesByIds(trackedAssignmentSourceIds)
213
+ .map((message) => [message.id, message.body])));
214
+ if (trackedAssignments.length === 0) {
215
+ state.pendingTaskAssignmentReport = null;
216
+ state.taskAssignmentReportSignature = "";
217
+ }
218
+ else {
219
+ const resolvedAssignments = await resolveTaskAssignments(trackedAssignments, process.cwd());
220
+ const changedAssignments = resolvedAssignments.filter(hasTaskAssignmentStatusChange);
221
+ taskProgressChanged = changedAssignments.length > 0;
222
+ projectedAssignments = resolvedAssignments.map((assignment) => {
223
+ if (hasTaskAssignmentStatusChange(assignment)) {
224
+ db.updateTaskAssignmentProgress(assignment.id, assignment.nextStatus, assignment.nextPrNumber);
225
+ }
226
+ return { ...assignment, status: assignment.nextStatus, prNumber: assignment.nextPrNumber };
227
+ });
228
+ if (changedAssignments.length > 0) {
229
+ const openedCount = changedAssignments.filter((a) => a.nextStatus === "pr_open").length;
230
+ const mergedCount = changedAssignments.filter((a) => a.nextStatus === "pr_merged").length;
231
+ const closedCount = changedAssignments.filter((a) => a.nextStatus === "pr_closed").length;
232
+ const tone = closedCount > 0 ? "warning" : mergedCount > 0 || openedCount > 0 ? "success" : "info";
233
+ const title = mergedCount > 0
234
+ ? mergedCount === 1
235
+ ? "Task merged"
236
+ : "Tasks merged"
237
+ : openedCount > 0
238
+ ? openedCount === 1
239
+ ? "Worker completion recorded"
240
+ : "Worker completions recorded"
241
+ : "Task progress updated";
242
+ const summaryParts = [];
243
+ if (openedCount > 0)
244
+ summaryParts.push(`${openedCount} worker completion${openedCount === 1 ? "" : "s"} moved to PR open`);
245
+ if (mergedCount > 0)
246
+ summaryParts.push(`${mergedCount} PR${mergedCount === 1 ? "" : "s"} merged`);
247
+ if (closedCount > 0)
248
+ summaryParts.push(`${closedCount} PR${closedCount === 1 ? "" : "s"} closed`);
249
+ if (summaryParts.length === 0) {
250
+ summaryParts.push(`${changedAssignments.length} tracked assignment${changedAssignments.length === 1 ? " changed" : "s changed"}`);
251
+ }
252
+ deps.logActivity({
253
+ kind: "task_progress",
254
+ level: "actions",
255
+ title,
256
+ summary: summaryParts.join("; "),
257
+ details: changedAssignments.map((a) => {
258
+ const next = deps.summarizeTrackedAssignmentStatus(a.nextStatus, a.nextPrNumber, a.branch);
259
+ return `${deps.formatTrackedAgent(a.agentId)} — #${a.issueNumber}: ${next.summary}`;
260
+ }),
261
+ fields: [
262
+ { label: "Updated", value: changedAssignments.length },
263
+ { label: "Merged", value: mergedCount },
264
+ { label: "PR open", value: openedCount },
265
+ { label: "Cycle", value: cycleStartedAt },
266
+ ],
267
+ tone,
268
+ });
269
+ }
270
+ state.pendingTaskAssignmentReport = getPendingTaskAssignmentReport(projectedAssignments, agentsById, state.taskAssignmentReportSignature, cycleStartedAt);
271
+ }
272
+ const activeWorkingAgentIds = workloads
273
+ .filter((workload) => workload.id !== selfId &&
274
+ workload.status === "working" &&
275
+ !visibleEvaluation.ghostAgentIds.includes(workload.id))
276
+ .map((workload) => workload.id);
277
+ const activeTrackedAssignmentCount = projectedAssignments.filter(isActiveTrackedAssignment).length;
278
+ const activeWorkCount = activeWorkingAgentIds.length + activeTrackedAssignmentCount;
279
+ const snoozeSettings = deps.getSettings?.() ?? {};
280
+ const snoozeStatusBefore = getRalphLoopSnoozeStatus(state, now);
281
+ const emptyCycle = activeWorkCount === 0 &&
282
+ !hasOutstandingAnomalies &&
283
+ pendingBacklogCount === 0 &&
284
+ (lastMaintenance?.assignedBacklogCount ?? 0) === 0 &&
285
+ (lastMaintenance?.anomalies.length ?? 0) === 0 &&
286
+ state.pendingTaskAssignmentReport == null &&
287
+ !taskProgressChanged;
288
+ const shouldWakeSnooze = snoozeStatusBefore.active && !emptyCycle;
289
+ if (shouldWakeSnooze) {
290
+ clearRalphLoopSnooze(state);
291
+ deps.logActivity({
292
+ kind: "ralph_event",
293
+ level: "actions",
294
+ title: "RALPH snooze ended",
295
+ summary: "RALPH woke because work, anomalies, or task progress appeared.",
296
+ fields: [
297
+ { label: "Backlog", value: pendingBacklogCount },
298
+ { label: "Anomalies", value: visibleEvaluation.anomalies.length },
299
+ { label: "Active work", value: activeWorkCount },
300
+ { label: "Cycle", value: cycleStartedAt },
301
+ ],
302
+ tone: "info",
303
+ });
304
+ }
305
+ if (emptyCycle) {
306
+ state.snoozeEmptyCycleCount += 1;
307
+ }
308
+ else {
309
+ state.snoozeEmptyCycleCount = 0;
310
+ }
311
+ let snoozeStartedThisCycle = false;
312
+ const autoSnoozeAfterEmptyCycles = resolveRalphSnoozeAfterEmptyCycles(snoozeSettings);
313
+ if (emptyCycle &&
314
+ !getRalphLoopSnoozeStatus(state, now).active &&
315
+ autoSnoozeAfterEmptyCycles > 0 &&
316
+ state.snoozeEmptyCycleCount >= autoSnoozeAfterEmptyCycles) {
317
+ setRalphLoopSnooze(state, {
318
+ durationMs: resolveRalphSnoozeDurationMs(snoozeSettings),
319
+ reason: `${state.snoozeEmptyCycleCount} empty RALPH cycles`,
320
+ source: "auto",
321
+ now,
322
+ });
323
+ snoozeStartedThisCycle = true;
324
+ }
325
+ const snoozeStatus = getRalphLoopSnoozeStatus(state, now);
326
+ const quietEmptyCycle = snoozeStatus.active && emptyCycle;
327
+ const shouldWarn = ghostRewrite.newGhostIds.length > 0 ||
328
+ (nonGhostSignature.length > 0 && nonGhostSignature !== state.nonGhostSignature);
329
+ const shouldInform = ghostRewrite.clearedGhostIds.length > 0 && visibleEvaluation.anomalies.length > 0;
330
+ const shouldDeliverFollowUp = followUpPrompt != null &&
331
+ shouldDeliverRalphLoopFollowUp({
332
+ signature: visibleSignature,
333
+ lastDeliveredSignature: state.followUpSignature,
334
+ lastDeliveredAt: state.followUpAt,
335
+ now,
336
+ cooldownMs: DEFAULT_RALPH_LOOP_FOLLOW_UP_COOLDOWN_MS,
337
+ pending: state.followUpPending,
338
+ idle: ctx.isIdle?.() ?? true,
339
+ }) &&
340
+ (shouldWarn || shouldInform);
341
+ if (shouldDeliverFollowUp && followUpPrompt) {
342
+ deps.trySendFollowUp(followUpPrompt, () => {
343
+ state.followUpPending = true;
344
+ state.followUpAt = now;
345
+ state.followUpSignature = visibleSignature;
346
+ });
347
+ }
348
+ if (!hasOutstandingAnomalies) {
349
+ state.followUpSignature = "";
350
+ }
351
+ if (state.pendingTaskAssignmentReport && (ctx.isIdle?.() ?? true)) {
352
+ const reportToDeliver = state.pendingTaskAssignmentReport;
353
+ deps.trySendFollowUp(reportToDeliver.message, () => {
354
+ state.taskAssignmentReportSignature = reportToDeliver.signature;
355
+ state.pendingTaskAssignmentReport = null;
356
+ });
357
+ }
358
+ if (shouldWarn) {
359
+ ctx.ui.notify(ralphNotifications.anomalyStatus ?? "RALPH loop anomaly detected", "warning");
360
+ }
361
+ else if (shouldInform) {
362
+ ctx.ui.notify(ralphNotifications.anomalyStatus ?? "RALPH loop anomaly detected", "info");
363
+ }
364
+ else if (!hasOutstandingAnomalies && state.hadOutstandingAnomalies) {
365
+ ctx.ui.notify(ralphNotifications.recoveryStatus, "info");
366
+ }
367
+ if (shouldWarn || shouldInform) {
368
+ deps.logActivity({
369
+ kind: "ralph_event",
370
+ level: "actions",
371
+ title: shouldWarn ? "RALPH anomaly detected" : "RALPH status updated",
372
+ summary: ralphNotifications.anomalyStatus ?? "RALPH loop anomaly detected",
373
+ details: visibleEvaluation.anomalies,
374
+ fields: [
375
+ { label: "Ghosts", value: visibleEvaluation.ghostAgentIds.length },
376
+ { label: "Stuck", value: visibleEvaluation.stuckAgentIds.length },
377
+ { label: "Nudged", value: visibleEvaluation.nudgeAgentIds.length },
378
+ { label: "Backlog", value: pendingBacklogCount },
379
+ { label: "Follow-up", value: shouldDeliverFollowUp },
380
+ ],
381
+ tone: shouldWarn ? "warning" : "info",
382
+ });
383
+ }
384
+ else if (!hasOutstandingAnomalies && state.hadOutstandingAnomalies) {
385
+ deps.logActivity({
386
+ kind: "ralph_event",
387
+ level: "actions",
388
+ title: "RALPH recovered",
389
+ summary: ralphNotifications.recoveryStatus,
390
+ details: ["Previous ghost/stall/backlog anomalies cleared."],
391
+ fields: [
392
+ { label: "Backlog", value: pendingBacklogCount },
393
+ { label: "Idle workers", value: visibleEvaluation.idleDrainAgentIds.length },
394
+ ],
395
+ tone: "success",
396
+ });
397
+ }
398
+ else if (quietEmptyCycle) {
399
+ if (snoozeStartedThisCycle) {
400
+ deps.logActivity({
401
+ kind: "ralph_event",
402
+ level: "actions",
403
+ title: "RALPH snoozed",
404
+ summary: `RALPH will stay quiet until ${snoozeStatus.until ?? "the snooze expires"} after ${state.snoozeEmptyCycleCount} empty cycle${state.snoozeEmptyCycleCount === 1 ? "" : "s"}.`,
405
+ fields: [
406
+ { label: "Source", value: snoozeStatus.source ?? "unknown" },
407
+ { label: "Reason", value: snoozeStatus.reason ?? "none" },
408
+ { label: "Cycle", value: cycleStartedAt },
409
+ ],
410
+ tone: "info",
411
+ });
412
+ }
413
+ }
414
+ else {
415
+ deps.logActivity({
416
+ kind: "ralph_cycle",
417
+ level: "verbose",
418
+ title: "RALPH cycle",
419
+ summary: visibleEvaluation.anomalies.length > 0
420
+ ? `${visibleEvaluation.anomalies.length} anomaly entries observed this cycle.`
421
+ : "Broker health steady this cycle.",
422
+ details: visibleEvaluation.anomalies.length > 0 ? visibleEvaluation.anomalies : undefined,
423
+ fields: [
424
+ { label: "Ghosts", value: visibleEvaluation.ghostAgentIds.length },
425
+ { label: "Stuck", value: visibleEvaluation.stuckAgentIds.length },
426
+ { label: "Nudged", value: visibleEvaluation.nudgeAgentIds.length },
427
+ { label: "Idle", value: visibleEvaluation.idleDrainAgentIds.length },
428
+ { label: "Backlog", value: pendingBacklogCount },
429
+ ],
430
+ tone: visibleEvaluation.anomalies.length > 0 ? "warning" : "info",
431
+ });
432
+ }
433
+ state.nonGhostSignature = nonGhostSignature;
434
+ state.hadOutstandingAnomalies = hasOutstandingAnomalies;
435
+ let recentRalphCycles = [];
436
+ try {
437
+ const cycleCompletedAt = new Date().toISOString();
438
+ db.recordRalphCycle({
439
+ startedAt: cycleStartedAt,
440
+ completedAt: cycleCompletedAt,
441
+ durationMs: Date.now() - cycleStartMs,
442
+ ghostAgentIds: visibleEvaluation.ghostAgentIds,
443
+ nudgeAgentIds: visibleEvaluation.nudgeAgentIds,
444
+ idleDrainAgentIds: visibleEvaluation.idleDrainAgentIds,
445
+ stuckAgentIds: visibleEvaluation.stuckAgentIds,
446
+ anomalies: visibleEvaluation.anomalies,
447
+ anomalySignature: visibleSignature,
448
+ followUpDelivered: shouldDeliverFollowUp,
449
+ agentCount: workloads.filter((w) => !w.disconnectedAt).length,
450
+ backlogCount: pendingBacklogCount,
451
+ });
452
+ recentRalphCycles = db.getRecentRalphCycles(5).map((cycle) => ({
453
+ startedAt: cycle.startedAt,
454
+ completedAt: cycle.completedAt,
455
+ durationMs: cycle.durationMs,
456
+ ghostAgentIds: cycle.ghostAgentIds,
457
+ stuckAgentIds: cycle.stuckAgentIds,
458
+ anomalies: cycle.anomalies,
459
+ followUpDelivered: cycle.followUpDelivered,
460
+ agentCount: cycle.agentCount,
461
+ backlogCount: cycle.backlogCount,
462
+ }));
463
+ }
464
+ catch {
465
+ /* best effort */
466
+ }
467
+ const controlPlaneInput = {
468
+ workloads,
469
+ evaluation: visibleEvaluation,
470
+ evaluationOptions,
471
+ maintenance: lastMaintenance,
472
+ assignments: projectedAssignments,
473
+ lanes: db.listPinetLanes({ includeDone: true }),
474
+ recentCycles: recentRalphCycles,
475
+ cycleStartedAt,
476
+ cycleDurationMs: Date.now() - cycleStartMs,
477
+ currentBranch,
478
+ homedir: os.homedir(),
479
+ snooze: snoozeStatus,
480
+ };
481
+ const controlPlaneSnapshot = deps.buildControlPlaneDashboardSnapshot(controlPlaneInput);
482
+ deps.setLastHomeTabSnapshot(controlPlaneSnapshot);
483
+ try {
484
+ await deps.refreshHomeTabs(ctx, controlPlaneSnapshot, cycleStartedAt);
485
+ }
486
+ catch (homeTabErr) {
487
+ const homeTabMessage = `Pinet Home tab publish failed: ${errorMsg(homeTabErr)}`;
488
+ if (homeTabMessage !== deps.getLastHomeTabError()) {
489
+ ctx.ui.notify(homeTabMessage, "warning");
490
+ }
491
+ deps.setLastHomeTabError(homeTabMessage);
492
+ }
493
+ }
494
+ catch (err) {
495
+ ctx.ui.notify(buildRalphLoopStatusMessage(`failed: ${errorMsg(err)}`, cycleStartedAt), "error");
496
+ deps.logActivity({
497
+ kind: "ralph_error",
498
+ level: "errors",
499
+ title: "RALPH loop failed",
500
+ summary: errorMsg(err),
501
+ fields: [{ label: "Cycle", value: cycleStartedAt }],
502
+ tone: "error",
503
+ });
504
+ }
505
+ finally {
506
+ state.running = false;
507
+ }
508
+ }
509
+ // ─── Timer management ────────────────────────────────────
510
+ export function startRalphLoop(ctx, state, deps) {
511
+ stopRalphLoop(state);
512
+ const intervalMs = resolveRalphLoopIntervalMs(deps.getSettings?.() ?? {});
513
+ state.timer = setInterval(() => {
514
+ void runRalphLoopCycle(ctx, state, deps);
515
+ }, intervalMs);
516
+ state.timer.unref?.();
517
+ void runRalphLoopCycle(ctx, state, deps);
518
+ }
519
+ export function stopRalphLoop(state) {
520
+ resetRalphLoopState(state);
521
+ }
522
+ function errorMsg(err) {
523
+ return err instanceof Error ? err.message : String(err);
524
+ }
@@ -0,0 +1,25 @@
1
+ export interface ReactionCommandTemplate {
2
+ action: string;
3
+ prompt: string;
4
+ }
5
+ export type ReactionCommandSetting = string | {
6
+ action?: string;
7
+ prompt?: string;
8
+ };
9
+ export type ReactionCommandSettings = Record<string, ReactionCommandSetting>;
10
+ export declare const DEFAULT_REACTION_COMMANDS: Record<string, ReactionCommandTemplate>;
11
+ export declare function normalizeReactionName(input: string): string;
12
+ export declare function resolveReactionCommands(settings: ReactionCommandSettings | undefined): Map<string, ReactionCommandTemplate>;
13
+ export declare function formatReactionDisplay(reactionName: string): string;
14
+ export interface ReactionTriggerMessageInput {
15
+ reactionName: string;
16
+ command: ReactionCommandTemplate;
17
+ reactorName: string;
18
+ channel: string;
19
+ threadTs: string;
20
+ messageTs: string;
21
+ reactedMessageText: string;
22
+ reactedMessageAuthor: string;
23
+ }
24
+ export declare function buildReactionTriggerMessage(input: ReactionTriggerMessageInput): string;
25
+ export declare function buildReactionPromptGuidelines(): string[];