@gugu910/pi-slack-bridge 0.1.3

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 (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +299 -0
  3. package/dist/activity-log.js +304 -0
  4. package/dist/broker/adapters/slack.js +645 -0
  5. package/dist/broker/adapters/types.js +3 -0
  6. package/dist/broker/agent-messaging.js +154 -0
  7. package/dist/broker/auth.js +97 -0
  8. package/dist/broker/client.js +495 -0
  9. package/dist/broker/control-plane-canvas.js +357 -0
  10. package/dist/broker/index.js +125 -0
  11. package/dist/broker/leader.js +133 -0
  12. package/dist/broker/maintenance.js +135 -0
  13. package/dist/broker/paths.js +69 -0
  14. package/dist/broker/router.js +287 -0
  15. package/dist/broker/schema.js +1492 -0
  16. package/dist/broker/socket-server.js +665 -0
  17. package/dist/broker/types.js +12 -0
  18. package/dist/broker-delivery.js +34 -0
  19. package/dist/canvases.js +175 -0
  20. package/dist/deploy-manifest.js +238 -0
  21. package/dist/follower-delivery.js +83 -0
  22. package/dist/git-metadata.js +95 -0
  23. package/dist/guardrails.js +197 -0
  24. package/dist/helpers.js +2128 -0
  25. package/dist/home-tab.js +240 -0
  26. package/dist/index.js +3086 -0
  27. package/dist/pinet-commands.js +244 -0
  28. package/dist/ralph-loop.js +385 -0
  29. package/dist/reaction-triggers.js +160 -0
  30. package/dist/scheduled-wakeups.js +71 -0
  31. package/dist/slack-api.js +5 -0
  32. package/dist/slack-block-kit.js +425 -0
  33. package/dist/slack-export.js +214 -0
  34. package/dist/slack-modals.js +269 -0
  35. package/dist/slack-presence.js +98 -0
  36. package/dist/slack-socket-dedup.js +143 -0
  37. package/dist/slack-tools.js +1715 -0
  38. package/dist/slack-upload.js +147 -0
  39. package/dist/task-assignments.js +403 -0
  40. package/dist/ttl-cache.js +110 -0
  41. package/manifest.yaml +57 -0
  42. package/package.json +45 -0
@@ -0,0 +1,244 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerPinetCommands = registerPinetCommands;
4
+ const helpers_js_1 = require("./helpers.js");
5
+ const activity_log_js_1 = require("./activity-log.js");
6
+ // ─── Registration ────────────────────────────────────────
7
+ function registerPinetCommands(pi, deps) {
8
+ pi.registerCommand("pinet-start", {
9
+ description: "Start Pinet as the broker (Slack connection + message routing)",
10
+ handler: async (_args, ctx) => {
11
+ if (deps.pinetRegistrationBlocked()) {
12
+ ctx.ui.notify(deps.getPinetRegistrationBlockReason(), "warning");
13
+ return;
14
+ }
15
+ if (deps.pinetEnabled()) {
16
+ ctx.ui.notify(`Pinet already running (${deps.brokerRole()})`, "info");
17
+ return;
18
+ }
19
+ deps.setExtCtx(ctx);
20
+ try {
21
+ await deps.connectAsBroker(ctx);
22
+ }
23
+ catch (err) {
24
+ ctx.ui.notify(`Pinet broker failed: ${errorMsg(err)}`, "error");
25
+ deps.setExtStatus(ctx, "error");
26
+ }
27
+ },
28
+ });
29
+ pi.registerCommand("pinet-follow", {
30
+ description: "Connect to an existing Pinet broker as a follower",
31
+ handler: async (_args, ctx) => {
32
+ if (deps.pinetRegistrationBlocked()) {
33
+ ctx.ui.notify(deps.getPinetRegistrationBlockReason(), "warning");
34
+ return;
35
+ }
36
+ if (deps.pinetEnabled()) {
37
+ ctx.ui.notify(`Pinet already running (${deps.brokerRole()})`, "info");
38
+ return;
39
+ }
40
+ deps.setExtCtx(ctx);
41
+ try {
42
+ await deps.connectAsFollower(ctx);
43
+ ctx.ui.notify(`${deps.agentEmoji()} ${deps.agentName()} — following broker`, "info");
44
+ }
45
+ catch (err) {
46
+ ctx.ui.notify(`Pinet follow failed: ${errorMsg(err)}`, "error");
47
+ deps.setExtStatus(ctx, "error");
48
+ }
49
+ },
50
+ });
51
+ pi.registerCommand("pinet-unfollow", {
52
+ description: "Disconnect from the Pinet broker and keep working locally",
53
+ handler: async (_args, ctx) => {
54
+ if (!deps.pinetEnabled() || deps.brokerRole() == null) {
55
+ ctx.ui.notify("Pinet not running. Use /pinet-start or /pinet-follow.", "info");
56
+ return;
57
+ }
58
+ if (deps.brokerRole() !== "follower") {
59
+ ctx.ui.notify("Pinet is running as broker; /pinet-unfollow only applies to followers.", "warning");
60
+ return;
61
+ }
62
+ const { unregisterError } = await deps.disconnectFollower(ctx);
63
+ if (unregisterError) {
64
+ ctx.ui.notify(`Pinet follower disconnected locally, but broker deregistration failed: ${unregisterError}`, "warning");
65
+ return;
66
+ }
67
+ ctx.ui.notify(`${deps.agentEmoji()} ${deps.agentName()} — disconnected from broker; local session still running`, "info");
68
+ },
69
+ });
70
+ pi.registerCommand("pinet-reload", {
71
+ description: "Tell a connected Pinet agent to reload itself",
72
+ handler: async (args, ctx) => {
73
+ const target = args.trim();
74
+ if (!target) {
75
+ ctx.ui.notify("Usage: /pinet-reload <agent-name-or-id>", "warning");
76
+ return;
77
+ }
78
+ try {
79
+ const result = await deps.sendPinetAgentMessage(target, "/reload");
80
+ ctx.ui.notify(`Sent /reload to ${result.target}`, "info");
81
+ }
82
+ catch (err) {
83
+ ctx.ui.notify(`Pinet reload failed: ${errorMsg(err)}`, "error");
84
+ }
85
+ },
86
+ });
87
+ pi.registerCommand("pinet-exit", {
88
+ description: "Tell a connected Pinet agent to exit gracefully",
89
+ handler: async (args, ctx) => {
90
+ const target = args.trim();
91
+ if (!target) {
92
+ ctx.ui.notify("Usage: /pinet-exit <agent-name-or-id>", "warning");
93
+ return;
94
+ }
95
+ try {
96
+ const result = await deps.sendPinetAgentMessage(target, "/exit");
97
+ ctx.ui.notify(`Sent /exit to ${result.target}`, "info");
98
+ }
99
+ catch (err) {
100
+ ctx.ui.notify(`Pinet exit failed: ${errorMsg(err)}`, "error");
101
+ }
102
+ },
103
+ });
104
+ pi.registerCommand("pinet-free", {
105
+ description: "Mark this Pinet agent idle/free for new work",
106
+ handler: async (_args, ctx) => {
107
+ if (!deps.pinetEnabled()) {
108
+ ctx.ui.notify("Pinet not running. Use /pinet-start or /pinet-follow.", "info");
109
+ return;
110
+ }
111
+ try {
112
+ const result = deps.signalAgentFree(ctx, { requirePinet: true });
113
+ const suffix = result.drainedQueuedInbox
114
+ ? ` Processing ${result.queuedInboxCount} queued inbox item${result.queuedInboxCount === 1 ? "" : "s"} now.`
115
+ : result.queuedInboxCount > 0
116
+ ? ` ${result.queuedInboxCount} queued inbox item${result.queuedInboxCount === 1 ? " remains" : "s remain"}.`
117
+ : "";
118
+ ctx.ui.notify(`Marked ${deps.agentEmoji()} ${deps.agentName()} idle/free for new work.${suffix}`, "info");
119
+ }
120
+ catch (err) {
121
+ ctx.ui.notify(`Pinet free failed: ${errorMsg(err)}`, "error");
122
+ }
123
+ },
124
+ });
125
+ pi.registerCommand("pinet-skin", {
126
+ description: "Regenerate the mesh naming/personality skin from a theme",
127
+ handler: async (args, ctx) => {
128
+ if (!deps.pinetEnabled() || deps.brokerRole() == null) {
129
+ ctx.ui.notify("Pinet not running. Use /pinet-start or /pinet-follow.", "info");
130
+ return;
131
+ }
132
+ if (deps.brokerRole() !== "broker") {
133
+ ctx.ui.notify("/pinet-skin can only run on the active broker.", "warning");
134
+ return;
135
+ }
136
+ try {
137
+ const result = deps.applyMeshSkin(args);
138
+ ctx.ui.notify(`Applied mesh skin "${result.theme}" to ${result.updatedAgents.length} agent${result.updatedAgents.length === 1 ? "" : "s"}.`, "info");
139
+ }
140
+ catch (err) {
141
+ ctx.ui.notify(`Pinet skin failed: ${errorMsg(err)}`, "error");
142
+ }
143
+ },
144
+ });
145
+ pi.registerCommand("pinet-status", {
146
+ description: "Show Pinet status",
147
+ handler: async (_args, ctx) => {
148
+ if (!deps.pinetEnabled()) {
149
+ ctx.ui.notify("Pinet not running. Use /pinet-start or /pinet-follow.", "info");
150
+ return;
151
+ }
152
+ const mode = deps.brokerRole() === "broker" ? "broker" : "follower";
153
+ const ownedCount = [...deps.threads().values()].filter((t) => (0, helpers_js_1.agentOwnsThread)(t.owner, deps.agentName(), deps.agentAliases(), deps.agentOwnerToken())).length;
154
+ const users = deps.allowedUsers();
155
+ const allowlistInfo = users
156
+ ? `Allowed users: ${[...users].join(", ")}`
157
+ : "Allowed users: all (no allowlist set)";
158
+ const s = deps.settings();
159
+ const defaultChInfo = s.defaultChannel
160
+ ? `Default channel: ${s.defaultChannel}`
161
+ : "Default channel: none";
162
+ const activityLogInfo = s.logChannel
163
+ ? `Activity log: ${s.logChannel} (${s.logLevel ?? "actions"})`
164
+ : "Activity log: disabled";
165
+ const lbm = deps.lastBrokerMaintenance();
166
+ const brokerHealthInfo = mode === "broker" && lbm
167
+ ? [
168
+ `Pending backlog: ${lbm.pendingBacklogCount}`,
169
+ `Last maintenance: assigned ${lbm.assignedBacklogCount}, reaped ${lbm.reapedAgentIds.length}, repaired ${lbm.repairedThreadClaims}`,
170
+ ...(lbm.anomalies.length > 0 ? [`Health: ${lbm.anomalies.join("; ")}`] : []),
171
+ ]
172
+ : [];
173
+ const brokerCanvasInfo = mode === "broker"
174
+ ? [
175
+ `Control plane canvas: ${deps.isBrokerControlPlaneCanvasEnabled()
176
+ ? (deps.getConfiguredBrokerControlPlaneCanvasId() ??
177
+ `pending (${deps.getConfiguredBrokerControlPlaneCanvasChannel() ?? "no target"})`)
178
+ : "disabled"}`,
179
+ ...(deps.lastBrokerControlPlaneCanvasRefreshAt()
180
+ ? [`Canvas refreshed: ${deps.lastBrokerControlPlaneCanvasRefreshAt()}`]
181
+ : []),
182
+ ...(deps.lastBrokerControlPlaneCanvasError()
183
+ ? [`Canvas status: ${deps.lastBrokerControlPlaneCanvasError()}`]
184
+ : []),
185
+ `Home tab viewers: ${deps.getBrokerControlPlaneHomeTabViewerIds().length}`,
186
+ ...(deps.lastBrokerControlPlaneHomeTabRefreshAt()
187
+ ? [`Home tab refreshed: ${deps.lastBrokerControlPlaneHomeTabRefreshAt()}`]
188
+ : []),
189
+ ...(deps.lastBrokerControlPlaneHomeTabError()
190
+ ? [`Home tab status: ${deps.lastBrokerControlPlaneHomeTabError()}`]
191
+ : []),
192
+ ]
193
+ : [];
194
+ ctx.ui.notify([
195
+ `Mode: ${mode}`,
196
+ `Agent: ${deps.agentEmoji()} ${deps.agentName()}`,
197
+ `Bot: ${deps.botUserId() ?? "unknown"}`,
198
+ `Connection: ${mode}`,
199
+ `Skin: ${deps.activeSkinTheme() ?? "(legacy/manual)"}`,
200
+ ...(deps.agentPersonality() ? [`Persona: ${deps.agentPersonality()}`] : []),
201
+ `Threads: ${deps.threads().size} (${ownedCount} owned by ${deps.agentName()})`,
202
+ `DM channel: ${deps.lastDmChannel() ?? "none yet"}`,
203
+ allowlistInfo,
204
+ defaultChInfo,
205
+ activityLogInfo,
206
+ ...brokerHealthInfo,
207
+ ...brokerCanvasInfo,
208
+ ].join("\n"), "info");
209
+ },
210
+ });
211
+ const showActivityLogs = async (_args, ctx) => {
212
+ const s = deps.settings();
213
+ const channelInfo = s.logChannel ? `${s.logChannel} (${s.logLevel ?? "actions"})` : "disabled";
214
+ ctx.ui.notify([
215
+ `Activity log channel: ${channelInfo}`,
216
+ (0, activity_log_js_1.formatRecentActivityLogEntries)(deps.activityLogger().getRecentEntries(10)),
217
+ ].join("\n\n"), s.logChannel ? "info" : "warning");
218
+ };
219
+ pi.registerCommand("pinet-logs", {
220
+ description: "Show recent broker activity log entries",
221
+ handler: showActivityLogs,
222
+ });
223
+ pi.registerCommand("slack-logs", {
224
+ description: "Show recent broker activity log entries",
225
+ handler: showActivityLogs,
226
+ });
227
+ pi.registerCommand("pinet-rename", {
228
+ description: "Rename this Pinet agent",
229
+ handler: async (args, ctx) => {
230
+ const newName = args.trim();
231
+ if (!newName) {
232
+ const fresh = (0, helpers_js_1.generateAgentName)(undefined, deps.brokerRole() === "broker" ? "broker" : "worker");
233
+ deps.applyLocalAgentIdentity(fresh.name, fresh.emoji, deps.agentPersonality());
234
+ }
235
+ else {
236
+ deps.applyLocalAgentIdentity(newName, deps.agentEmoji(), deps.agentPersonality());
237
+ }
238
+ ctx.ui.notify(`${deps.agentEmoji()} Agent renamed to: ${deps.agentName()}`, "info");
239
+ },
240
+ });
241
+ }
242
+ function errorMsg(err) {
243
+ return err instanceof Error ? err.message : String(err);
244
+ }
@@ -0,0 +1,385 @@
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.createRalphLoopState = createRalphLoopState;
37
+ exports.resetRalphLoopState = resetRalphLoopState;
38
+ exports.runRalphLoopCycle = runRalphLoopCycle;
39
+ exports.startRalphLoop = startRalphLoop;
40
+ exports.stopRalphLoop = stopRalphLoop;
41
+ const os = __importStar(require("node:os"));
42
+ const helpers_js_1 = require("./helpers.js");
43
+ const socket_server_js_1 = require("./broker/socket-server.js");
44
+ const client_js_1 = require("./broker/client.js");
45
+ const task_assignments_js_1 = require("./task-assignments.js");
46
+ const git_metadata_js_1 = require("./git-metadata.js");
47
+ function createRalphLoopState() {
48
+ return {
49
+ timer: null,
50
+ running: false,
51
+ nudges: new Map(),
52
+ reportedGhosts: new Set(),
53
+ nonGhostSignature: "",
54
+ hadOutstandingAnomalies: false,
55
+ followUpAt: 0,
56
+ followUpPending: false,
57
+ taskAssignmentReportSignature: "",
58
+ pendingTaskAssignmentReport: null,
59
+ };
60
+ }
61
+ function resetRalphLoopState(state) {
62
+ if (state.timer) {
63
+ clearInterval(state.timer);
64
+ state.timer = null;
65
+ }
66
+ state.running = false;
67
+ state.nudges.clear();
68
+ state.reportedGhosts.clear();
69
+ state.nonGhostSignature = "";
70
+ state.hadOutstandingAnomalies = false;
71
+ state.followUpAt = 0;
72
+ state.followUpPending = false;
73
+ state.taskAssignmentReportSignature = "";
74
+ state.pendingTaskAssignmentReport = null;
75
+ }
76
+ // ─── Core loop ───────────────────────────────────────────
77
+ async function runRalphLoopCycle(ctx, state, deps) {
78
+ const db = deps.getBrokerDb();
79
+ const selfId = deps.getBrokerAgentId();
80
+ if (!db || !selfId || state.running)
81
+ return;
82
+ state.running = true;
83
+ const cycleStartedAt = new Date().toISOString();
84
+ const cycleStartMs = Date.now();
85
+ try {
86
+ deps.runMaintenance(ctx);
87
+ const currentBranch = (await (0, git_metadata_js_1.probeGitBranch)(process.cwd())) ?? null;
88
+ const workloads = db.getAllAgents().map((agent) => ({
89
+ ...agent,
90
+ pendingInboxCount: db.getPendingInboxCount(agent.id),
91
+ ownedThreadCount: db.getOwnedThreadCount(agent.id),
92
+ }));
93
+ const pendingBacklogCount = db.getBacklogCount("pending");
94
+ const evaluationOptions = {
95
+ now: Date.now(),
96
+ heartbeatTimeoutMs: socket_server_js_1.DEFAULT_HEARTBEAT_TIMEOUT_MS,
97
+ heartbeatIntervalMs: client_js_1.HEARTBEAT_INTERVAL_MS,
98
+ stuckWorkingThresholdMs: helpers_js_1.DEFAULT_RALPH_LOOP_STUCK_WORKING_THRESHOLD_MS,
99
+ pendingBacklogCount,
100
+ currentBranch,
101
+ brokerHeartbeatActive: deps.heartbeatTimerActive(),
102
+ brokerMaintenanceActive: deps.maintenanceTimerActive(),
103
+ brokerAgentId: selfId,
104
+ };
105
+ const evaluation = (0, helpers_js_1.evaluateRalphLoopCycle)(workloads, evaluationOptions);
106
+ const now = Date.now();
107
+ const nudgeAgentIds = new Set(evaluation.nudgeAgentIds);
108
+ for (const workload of workloads) {
109
+ if (!nudgeAgentIds.has(workload.id)) {
110
+ state.nudges.delete(workload.id);
111
+ continue;
112
+ }
113
+ const lastNudgeAt = state.nudges.get(workload.id) ?? 0;
114
+ if (now - lastNudgeAt < helpers_js_1.DEFAULT_RALPH_LOOP_NUDGE_COOLDOWN_MS) {
115
+ continue;
116
+ }
117
+ deps.sendMaintenanceMessage(workload.id, (0, helpers_js_1.buildRalphLoopNudgeMessage)(workload.pendingInboxCount, workload.ownedThreadCount, cycleStartedAt));
118
+ state.nudges.set(workload.id, now);
119
+ }
120
+ const ghostRewrite = (0, helpers_js_1.rewriteRalphLoopGhostAnomalies)(evaluation, state.reportedGhosts);
121
+ state.reportedGhosts.clear();
122
+ for (const ghostId of ghostRewrite.nextReportedGhostIds) {
123
+ state.reportedGhosts.add(ghostId);
124
+ }
125
+ const visibleEvaluation = ghostRewrite.evaluation;
126
+ const visibleSignature = (0, helpers_js_1.buildRalphLoopAnomalySignature)(visibleEvaluation);
127
+ const nonGhostSignature = ghostRewrite.nonGhostAnomalies.join("|");
128
+ const hasOutstandingAnomalies = evaluation.anomalies.length > 0;
129
+ const ralphNotifications = (0, helpers_js_1.buildRalphLoopCycleNotifications)(visibleEvaluation, cycleStartedAt);
130
+ const followUpPrompt = ghostRewrite.newGhostIds.length === 0 &&
131
+ ghostRewrite.clearedGhostIds.length > 0 &&
132
+ ghostRewrite.nonGhostAnomalies.length === 0
133
+ ? null
134
+ : ralphNotifications.followUpPrompt;
135
+ const agentsById = new Map(workloads.map((workload) => [workload.id, { emoji: workload.emoji, name: workload.name }]));
136
+ let projectedAssignments = [];
137
+ const rawTrackedAssignments = db.listTaskAssignments();
138
+ const trackedAssignmentSourceIds = [
139
+ ...new Set(rawTrackedAssignments
140
+ .map((assignment) => assignment.sourceMessageId)
141
+ .filter((messageId) => messageId != null)),
142
+ ];
143
+ const trackedAssignments = (0, task_assignments_js_1.normalizeTrackedTaskAssignments)(rawTrackedAssignments, new Map(db
144
+ .getMessagesByIds(trackedAssignmentSourceIds)
145
+ .map((message) => [message.id, message.body])));
146
+ if (trackedAssignments.length === 0) {
147
+ state.pendingTaskAssignmentReport = null;
148
+ state.taskAssignmentReportSignature = "";
149
+ }
150
+ else {
151
+ const resolvedAssignments = await (0, task_assignments_js_1.resolveTaskAssignments)(trackedAssignments, process.cwd());
152
+ const changedAssignments = resolvedAssignments.filter(task_assignments_js_1.hasTaskAssignmentStatusChange);
153
+ projectedAssignments = resolvedAssignments.map((assignment) => {
154
+ if ((0, task_assignments_js_1.hasTaskAssignmentStatusChange)(assignment)) {
155
+ db.updateTaskAssignmentProgress(assignment.id, assignment.nextStatus, assignment.nextPrNumber);
156
+ }
157
+ return { ...assignment, status: assignment.nextStatus, prNumber: assignment.nextPrNumber };
158
+ });
159
+ if (changedAssignments.length > 0) {
160
+ const openedCount = changedAssignments.filter((a) => a.nextStatus === "pr_open").length;
161
+ const mergedCount = changedAssignments.filter((a) => a.nextStatus === "pr_merged").length;
162
+ const closedCount = changedAssignments.filter((a) => a.nextStatus === "pr_closed").length;
163
+ const tone = closedCount > 0 ? "warning" : mergedCount > 0 || openedCount > 0 ? "success" : "info";
164
+ const title = mergedCount > 0
165
+ ? mergedCount === 1
166
+ ? "Task merged"
167
+ : "Tasks merged"
168
+ : openedCount > 0
169
+ ? openedCount === 1
170
+ ? "Worker completion recorded"
171
+ : "Worker completions recorded"
172
+ : "Task progress updated";
173
+ const summaryParts = [];
174
+ if (openedCount > 0)
175
+ summaryParts.push(`${openedCount} worker completion${openedCount === 1 ? "" : "s"} moved to PR open`);
176
+ if (mergedCount > 0)
177
+ summaryParts.push(`${mergedCount} PR${mergedCount === 1 ? "" : "s"} merged`);
178
+ if (closedCount > 0)
179
+ summaryParts.push(`${closedCount} PR${closedCount === 1 ? "" : "s"} closed`);
180
+ if (summaryParts.length === 0) {
181
+ summaryParts.push(`${changedAssignments.length} tracked assignment${changedAssignments.length === 1 ? " changed" : "s changed"}`);
182
+ }
183
+ deps.logActivity({
184
+ kind: "task_progress",
185
+ level: "actions",
186
+ title,
187
+ summary: summaryParts.join("; "),
188
+ details: changedAssignments.map((a) => {
189
+ const next = deps.summarizeTrackedAssignmentStatus(a.nextStatus, a.nextPrNumber, a.branch);
190
+ return `${deps.formatTrackedAgent(a.agentId)} — #${a.issueNumber}: ${next.summary}`;
191
+ }),
192
+ fields: [
193
+ { label: "Updated", value: changedAssignments.length },
194
+ { label: "Merged", value: mergedCount },
195
+ { label: "PR open", value: openedCount },
196
+ { label: "Cycle", value: cycleStartedAt },
197
+ ],
198
+ tone,
199
+ });
200
+ }
201
+ state.pendingTaskAssignmentReport = (0, task_assignments_js_1.getPendingTaskAssignmentReport)(projectedAssignments, agentsById, state.taskAssignmentReportSignature, cycleStartedAt);
202
+ }
203
+ const shouldDeliverFollowUp = followUpPrompt != null &&
204
+ (0, helpers_js_1.shouldDeliverRalphLoopFollowUp)({
205
+ signature: visibleSignature,
206
+ lastDeliveredAt: state.followUpAt,
207
+ now,
208
+ cooldownMs: helpers_js_1.DEFAULT_RALPH_LOOP_FOLLOW_UP_COOLDOWN_MS,
209
+ pending: state.followUpPending,
210
+ idle: ctx.isIdle?.() ?? true,
211
+ });
212
+ if (shouldDeliverFollowUp && followUpPrompt) {
213
+ deps.trySendFollowUp(followUpPrompt, () => {
214
+ state.followUpPending = true;
215
+ state.followUpAt = now;
216
+ });
217
+ }
218
+ if (state.pendingTaskAssignmentReport && (ctx.isIdle?.() ?? true)) {
219
+ const reportToDeliver = state.pendingTaskAssignmentReport;
220
+ deps.trySendFollowUp(reportToDeliver.message, () => {
221
+ state.taskAssignmentReportSignature = reportToDeliver.signature;
222
+ state.pendingTaskAssignmentReport = null;
223
+ });
224
+ }
225
+ const shouldWarn = ghostRewrite.newGhostIds.length > 0 ||
226
+ (nonGhostSignature.length > 0 && nonGhostSignature !== state.nonGhostSignature);
227
+ const shouldInform = ghostRewrite.clearedGhostIds.length > 0 && visibleEvaluation.anomalies.length > 0;
228
+ if (shouldWarn) {
229
+ ctx.ui.notify(ralphNotifications.anomalyStatus ?? "RALPH loop anomaly detected", "warning");
230
+ }
231
+ else if (shouldInform) {
232
+ ctx.ui.notify(ralphNotifications.anomalyStatus ?? "RALPH loop anomaly detected", "info");
233
+ }
234
+ else if (!hasOutstandingAnomalies && state.hadOutstandingAnomalies) {
235
+ ctx.ui.notify(ralphNotifications.recoveryStatus, "info");
236
+ }
237
+ if (shouldWarn || shouldInform) {
238
+ deps.logActivity({
239
+ kind: "ralph_event",
240
+ level: "actions",
241
+ title: shouldWarn ? "RALPH anomaly detected" : "RALPH status updated",
242
+ summary: ralphNotifications.anomalyStatus ?? "RALPH loop anomaly detected",
243
+ details: visibleEvaluation.anomalies,
244
+ fields: [
245
+ { label: "Ghosts", value: visibleEvaluation.ghostAgentIds.length },
246
+ { label: "Stuck", value: visibleEvaluation.stuckAgentIds.length },
247
+ { label: "Nudged", value: visibleEvaluation.nudgeAgentIds.length },
248
+ { label: "Backlog", value: pendingBacklogCount },
249
+ { label: "Follow-up", value: shouldDeliverFollowUp },
250
+ ],
251
+ tone: shouldWarn ? "warning" : "info",
252
+ });
253
+ }
254
+ else if (!hasOutstandingAnomalies && state.hadOutstandingAnomalies) {
255
+ deps.logActivity({
256
+ kind: "ralph_event",
257
+ level: "actions",
258
+ title: "RALPH recovered",
259
+ summary: ralphNotifications.recoveryStatus,
260
+ details: ["Previous ghost/stall/backlog anomalies cleared."],
261
+ fields: [
262
+ { label: "Backlog", value: pendingBacklogCount },
263
+ { label: "Idle workers", value: visibleEvaluation.idleDrainAgentIds.length },
264
+ ],
265
+ tone: "success",
266
+ });
267
+ }
268
+ else {
269
+ deps.logActivity({
270
+ kind: "ralph_cycle",
271
+ level: "verbose",
272
+ title: "RALPH cycle",
273
+ summary: visibleEvaluation.anomalies.length > 0
274
+ ? `${visibleEvaluation.anomalies.length} anomaly entries observed this cycle.`
275
+ : "Broker health steady this cycle.",
276
+ details: visibleEvaluation.anomalies.length > 0 ? visibleEvaluation.anomalies : undefined,
277
+ fields: [
278
+ { label: "Ghosts", value: visibleEvaluation.ghostAgentIds.length },
279
+ { label: "Stuck", value: visibleEvaluation.stuckAgentIds.length },
280
+ { label: "Nudged", value: visibleEvaluation.nudgeAgentIds.length },
281
+ { label: "Idle", value: visibleEvaluation.idleDrainAgentIds.length },
282
+ { label: "Backlog", value: pendingBacklogCount },
283
+ ],
284
+ tone: visibleEvaluation.anomalies.length > 0 ? "warning" : "info",
285
+ });
286
+ }
287
+ state.nonGhostSignature = nonGhostSignature;
288
+ state.hadOutstandingAnomalies = hasOutstandingAnomalies;
289
+ let recentRalphCycles = [];
290
+ try {
291
+ const cycleCompletedAt = new Date().toISOString();
292
+ db.recordRalphCycle({
293
+ startedAt: cycleStartedAt,
294
+ completedAt: cycleCompletedAt,
295
+ durationMs: Date.now() - cycleStartMs,
296
+ ghostAgentIds: visibleEvaluation.ghostAgentIds,
297
+ nudgeAgentIds: visibleEvaluation.nudgeAgentIds,
298
+ idleDrainAgentIds: visibleEvaluation.idleDrainAgentIds,
299
+ stuckAgentIds: visibleEvaluation.stuckAgentIds,
300
+ anomalies: visibleEvaluation.anomalies,
301
+ anomalySignature: visibleSignature,
302
+ followUpDelivered: shouldDeliverFollowUp,
303
+ agentCount: workloads.filter((w) => !w.disconnectedAt).length,
304
+ backlogCount: pendingBacklogCount,
305
+ });
306
+ recentRalphCycles = db.getRecentRalphCycles(5).map((cycle) => ({
307
+ startedAt: cycle.startedAt,
308
+ completedAt: cycle.completedAt,
309
+ durationMs: cycle.durationMs,
310
+ ghostAgentIds: cycle.ghostAgentIds,
311
+ stuckAgentIds: cycle.stuckAgentIds,
312
+ anomalies: cycle.anomalies,
313
+ followUpDelivered: cycle.followUpDelivered,
314
+ agentCount: cycle.agentCount,
315
+ backlogCount: cycle.backlogCount,
316
+ }));
317
+ }
318
+ catch {
319
+ /* best effort */
320
+ }
321
+ const controlPlaneInput = {
322
+ workloads,
323
+ evaluation: visibleEvaluation,
324
+ evaluationOptions,
325
+ maintenance: deps.getLastMaintenance(),
326
+ assignments: projectedAssignments,
327
+ recentCycles: recentRalphCycles,
328
+ cycleStartedAt,
329
+ cycleDurationMs: Date.now() - cycleStartMs,
330
+ currentBranch,
331
+ homedir: os.homedir(),
332
+ };
333
+ const controlPlaneSnapshot = deps.buildControlPlaneDashboardSnapshot(controlPlaneInput);
334
+ deps.setLastHomeTabSnapshot(controlPlaneSnapshot);
335
+ try {
336
+ await deps.refreshCanvasDashboard(ctx, controlPlaneInput);
337
+ }
338
+ catch (canvasErr) {
339
+ const canvasMessage = `Pinet broker control plane canvas refresh failed: ${errorMsg(canvasErr)}`;
340
+ if (canvasMessage !== deps.getLastCanvasError()) {
341
+ ctx.ui.notify(canvasMessage, "warning");
342
+ }
343
+ deps.setLastCanvasError(canvasMessage);
344
+ }
345
+ try {
346
+ await deps.refreshHomeTabs(ctx, controlPlaneSnapshot, cycleStartedAt);
347
+ }
348
+ catch (homeTabErr) {
349
+ const homeTabMessage = `Pinet Home tab publish failed: ${errorMsg(homeTabErr)}`;
350
+ if (homeTabMessage !== deps.getLastHomeTabError()) {
351
+ ctx.ui.notify(homeTabMessage, "warning");
352
+ }
353
+ deps.setLastHomeTabError(homeTabMessage);
354
+ }
355
+ }
356
+ catch (err) {
357
+ ctx.ui.notify((0, helpers_js_1.buildRalphLoopStatusMessage)(`failed: ${errorMsg(err)}`, cycleStartedAt), "error");
358
+ deps.logActivity({
359
+ kind: "ralph_error",
360
+ level: "errors",
361
+ title: "RALPH loop failed",
362
+ summary: errorMsg(err),
363
+ fields: [{ label: "Cycle", value: cycleStartedAt }],
364
+ tone: "error",
365
+ });
366
+ }
367
+ finally {
368
+ state.running = false;
369
+ }
370
+ }
371
+ // ─── Timer management ────────────────────────────────────
372
+ function startRalphLoop(ctx, state, deps) {
373
+ stopRalphLoop(state);
374
+ state.timer = setInterval(() => {
375
+ void runRalphLoopCycle(ctx, state, deps);
376
+ }, helpers_js_1.DEFAULT_RALPH_LOOP_INTERVAL_MS);
377
+ state.timer.unref?.();
378
+ void runRalphLoopCycle(ctx, state, deps);
379
+ }
380
+ function stopRalphLoop(state) {
381
+ resetRalphLoopState(state);
382
+ }
383
+ function errorMsg(err) {
384
+ return err instanceof Error ? err.message : String(err);
385
+ }