@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,98 @@
1
+ import type { RalphLoopAgentWorkload, RalphLoopEvaluationOptions, RalphLoopEvaluationResult } from "../helpers.js";
2
+ import type { ResolvedTaskAssignment } from "../task-assignments.js";
3
+ import type { BrokerMaintenanceResult } from "./maintenance.js";
4
+ import type { PinetLaneInfo } from "./types.js";
5
+ export type BrokerControlPlaneRecentCycle = {
6
+ startedAt: string;
7
+ completedAt: string | null;
8
+ durationMs: number | null;
9
+ ghostAgentIds: string[];
10
+ stuckAgentIds: string[];
11
+ anomalies: string[];
12
+ followUpDelivered: boolean;
13
+ agentCount: number;
14
+ backlogCount: number;
15
+ };
16
+ export interface BrokerControlPlaneAgentRow {
17
+ id: string;
18
+ role: string;
19
+ label: string;
20
+ status: string;
21
+ health: string;
22
+ workload: string;
23
+ taskSummary: string;
24
+ heartbeat: string;
25
+ branch: string;
26
+ worktree: string;
27
+ }
28
+ export interface BrokerControlPlaneDashboardSnapshot {
29
+ cycleStartedAt: string;
30
+ cycleDurationMs: number;
31
+ currentBranch: string | null;
32
+ ralphSnooze?: {
33
+ active: boolean;
34
+ until: string | null;
35
+ reason: string | null;
36
+ source: string | null;
37
+ emptyCycleCount: number;
38
+ } | null;
39
+ totalAgents: number;
40
+ liveAgents: number;
41
+ brokerCount: number;
42
+ workerCount: number;
43
+ idleWorkers: number;
44
+ workingWorkers: number;
45
+ ghostAgents: number;
46
+ stuckAgents: number;
47
+ pendingBacklogCount: number;
48
+ nudgesThisCycle: number;
49
+ idleDrainCandidates: number;
50
+ assignedBacklogCount: number;
51
+ reapedAgents: number;
52
+ repairedThreadClaims: number;
53
+ maintenanceAnomalies: string[];
54
+ anomalies: string[];
55
+ taskCounts: {
56
+ assigned: number;
57
+ branchPushed: number;
58
+ openPrs: number;
59
+ mergedPrs: number;
60
+ closedPrs: number;
61
+ };
62
+ activeTasks: string[];
63
+ recentOutcomes: string[];
64
+ activeLanes: string[];
65
+ detachedLanes: string[];
66
+ roster: BrokerControlPlaneAgentRow[];
67
+ recentCycles: Array<{
68
+ startedAt: string;
69
+ duration: string;
70
+ agentCount: number;
71
+ backlogCount: number;
72
+ ghostCount: number;
73
+ stuckCount: number;
74
+ anomalySummary: string;
75
+ followUpDelivered: boolean;
76
+ }>;
77
+ }
78
+ export interface BuildBrokerControlPlaneDashboardSnapshotInput {
79
+ workloads: RalphLoopAgentWorkload[];
80
+ evaluation: RalphLoopEvaluationResult;
81
+ evaluationOptions?: RalphLoopEvaluationOptions;
82
+ maintenance: BrokerMaintenanceResult | null;
83
+ assignments: Array<Pick<ResolvedTaskAssignment, "agentId" | "issueNumber" | "branch" | "status" | "prNumber" | "updatedAt" | "issueState">>;
84
+ lanes?: PinetLaneInfo[];
85
+ recentCycles: BrokerControlPlaneRecentCycle[];
86
+ cycleStartedAt: string;
87
+ cycleDurationMs: number;
88
+ currentBranch: string | null;
89
+ homedir?: string;
90
+ snooze?: {
91
+ active: boolean;
92
+ until: string | null;
93
+ reason: string | null;
94
+ source: string | null;
95
+ emptyCycleCount: number;
96
+ } | null;
97
+ }
98
+ export declare function buildBrokerControlPlaneDashboardSnapshot(input: BuildBrokerControlPlaneDashboardSnapshotInput): BrokerControlPlaneDashboardSnapshot;
@@ -0,0 +1,213 @@
1
+ import { buildAgentDisplayInfo, shortenPath } from "../helpers.js";
2
+ function formatDuration(ms) {
3
+ if (ms == null || !Number.isFinite(ms))
4
+ return "n/a";
5
+ if (ms < 1_000)
6
+ return `${Math.round(ms)}ms`;
7
+ const seconds = ms / 1_000;
8
+ if (seconds < 60)
9
+ return `${seconds.toFixed(seconds < 10 ? 1 : 0)}s`;
10
+ const minutes = seconds / 60;
11
+ return `${minutes.toFixed(minutes < 10 ? 1 : 0)}m`;
12
+ }
13
+ function formatTimestamp(value) {
14
+ const parsed = Date.parse(value);
15
+ if (Number.isNaN(parsed)) {
16
+ return value;
17
+ }
18
+ return new Date(parsed)
19
+ .toISOString()
20
+ .replace("T", " ")
21
+ .replace(".000", "")
22
+ .replace(/:\d\dZ$/, "Z");
23
+ }
24
+ function truncateText(value, maxLength) {
25
+ if (value.length <= maxLength)
26
+ return value;
27
+ return `${value.slice(0, Math.max(0, maxLength - 3))}...`;
28
+ }
29
+ function getAgentRole(agent) {
30
+ const capabilitiesRole = agent.metadata?.capabilities?.role;
31
+ const metadataRole = agent.metadata?.role;
32
+ return capabilitiesRole ?? metadataRole ?? "worker";
33
+ }
34
+ function buildWorkloadSummary(workload) {
35
+ const parts = [
36
+ `${workload.pendingInboxCount} inbox`,
37
+ `${workload.ownedThreadCount} thread${workload.ownedThreadCount === 1 ? "" : "s"}`,
38
+ ];
39
+ return parts.join(" / ");
40
+ }
41
+ function formatTaskStatusShort(assignment) {
42
+ switch (assignment.status) {
43
+ case "pr_open":
44
+ return `#${assignment.issueNumber} PR #${assignment.prNumber ?? "?"} open`;
45
+ case "pr_merged":
46
+ return `#${assignment.issueNumber} PR #${assignment.prNumber ?? "?"} merged`;
47
+ case "pr_closed":
48
+ return `#${assignment.issueNumber} PR #${assignment.prNumber ?? "?"} closed`;
49
+ case "branch_pushed":
50
+ return `#${assignment.issueNumber} pushed ${assignment.branch ?? "branch"}`;
51
+ case "assigned":
52
+ default:
53
+ return `#${assignment.issueNumber} assigned`;
54
+ }
55
+ }
56
+ function formatLaneStatusShort(lane) {
57
+ const refs = [
58
+ lane.issueNumber != null ? `#${lane.issueNumber}` : null,
59
+ lane.prNumber != null ? `PR #${lane.prNumber}` : null,
60
+ lane.pmMode ? "PM" : null,
61
+ ].filter((ref) => Boolean(ref));
62
+ const owner = lane.ownerAgentId ? ` owner ${lane.ownerAgentId}` : "";
63
+ const lead = lane.implementationLeadAgentId ? ` lead ${lane.implementationLeadAgentId}` : "";
64
+ return `${lane.laneId} [${lane.state}]${refs.length > 0 ? ` (${refs.join(" · ")})` : ""}${owner}${lead}${lane.summary ? ` — ${lane.summary}` : ""}`;
65
+ }
66
+ function summarizeAgentTasks(assignments) {
67
+ if (assignments.length === 0)
68
+ return "—";
69
+ const ordered = [...assignments].sort((left, right) => left.issueNumber - right.issueNumber);
70
+ const visible = ordered.slice(0, 2).map(formatTaskStatusShort);
71
+ if (ordered.length > 2) {
72
+ visible.push(`+${ordered.length - 2} more`);
73
+ }
74
+ return visible.join("; ");
75
+ }
76
+ function summarizeCycleAnomalies(anomalies) {
77
+ if (anomalies.length === 0)
78
+ return "healthy";
79
+ return truncateText(anomalies.join("; "), 72);
80
+ }
81
+ export function buildBrokerControlPlaneDashboardSnapshot(input) {
82
+ const homedir = input.homedir ?? process.env.HOME ?? "";
83
+ const visibleAssignments = input.assignments.filter((assignment) => assignment.issueState !== "CLOSED");
84
+ const assignmentsByAgent = new Map();
85
+ for (const assignment of visibleAssignments) {
86
+ const bucket = assignmentsByAgent.get(assignment.agentId);
87
+ const summaryAssignment = {
88
+ issueNumber: assignment.issueNumber,
89
+ status: assignment.status,
90
+ prNumber: assignment.prNumber,
91
+ branch: assignment.branch,
92
+ };
93
+ if (bucket) {
94
+ bucket.push(summaryAssignment);
95
+ }
96
+ else {
97
+ assignmentsByAgent.set(assignment.agentId, [summaryAssignment]);
98
+ }
99
+ }
100
+ const displays = input.workloads
101
+ .map((workload) => ({
102
+ workload,
103
+ display: buildAgentDisplayInfo(workload, input.evaluationOptions ?? {}),
104
+ }))
105
+ .sort((left, right) => {
106
+ const leftRole = getAgentRole(left.display);
107
+ const rightRole = getAgentRole(right.display);
108
+ if (leftRole !== rightRole) {
109
+ return leftRole === "broker" ? -1 : 1;
110
+ }
111
+ return left.display.name.localeCompare(right.display.name);
112
+ });
113
+ const brokerCount = displays.filter(({ display }) => getAgentRole(display) === "broker").length;
114
+ const workerDisplays = displays.filter(({ display }) => getAgentRole(display) !== "broker");
115
+ const liveAgents = displays.filter(({ workload }) => !workload.disconnectedAt).length;
116
+ const roster = displays.map(({ workload, display }) => {
117
+ const worktree = display.metadata?.worktreeKind === "linked" && display.metadata.worktreePath
118
+ ? shortenPath(display.metadata.worktreePath, homedir)
119
+ : display.metadata?.worktreeKind === "main"
120
+ ? "main checkout"
121
+ : "—";
122
+ const branch = display.metadata?.branch ?? "—";
123
+ const heartbeat = display.heartbeatSummary ?? display.leaseSummary ?? "unknown";
124
+ const health = display.stuck
125
+ ? `${display.health ?? "unknown"} / stuck`
126
+ : (display.health ?? "unknown");
127
+ return {
128
+ id: display.id,
129
+ role: getAgentRole(display),
130
+ label: `${display.emoji} ${display.name}`,
131
+ status: display.status,
132
+ health,
133
+ workload: buildWorkloadSummary(workload),
134
+ taskSummary: summarizeAgentTasks(assignmentsByAgent.get(display.id) ?? []),
135
+ heartbeat,
136
+ branch,
137
+ worktree,
138
+ };
139
+ });
140
+ const sortedAssignments = [...visibleAssignments].sort((left, right) => Date.parse(right.updatedAt) - Date.parse(left.updatedAt));
141
+ const sortedLanes = [...(input.lanes ?? [])].sort((left, right) => Date.parse(right.updatedAt) - Date.parse(left.updatedAt));
142
+ const activeTasks = sortedAssignments
143
+ .filter((assignment) => assignment.status === "assigned" ||
144
+ assignment.status === "branch_pushed" ||
145
+ assignment.status === "pr_open")
146
+ .map((assignment) => formatTaskStatusShort(assignment))
147
+ .slice(0, 8);
148
+ const recentOutcomes = sortedAssignments
149
+ .filter((assignment) => assignment.status === "pr_merged" || assignment.status === "pr_closed")
150
+ .map((assignment) => formatTaskStatusShort(assignment))
151
+ .slice(0, 8);
152
+ return {
153
+ cycleStartedAt: input.cycleStartedAt,
154
+ cycleDurationMs: input.cycleDurationMs,
155
+ currentBranch: input.currentBranch,
156
+ ralphSnooze: input.snooze
157
+ ? {
158
+ active: input.snooze.active,
159
+ until: input.snooze.until,
160
+ reason: input.snooze.reason,
161
+ source: input.snooze.source,
162
+ emptyCycleCount: input.snooze.emptyCycleCount,
163
+ }
164
+ : null,
165
+ totalAgents: displays.length,
166
+ liveAgents,
167
+ brokerCount,
168
+ workerCount: workerDisplays.length,
169
+ idleWorkers: workerDisplays.filter(({ display }) => display.status === "idle").length,
170
+ workingWorkers: workerDisplays.filter(({ display }) => display.status === "working").length,
171
+ ghostAgents: input.evaluation.ghostAgentIds.length,
172
+ stuckAgents: input.evaluation.stuckAgentIds.length,
173
+ pendingBacklogCount: input.maintenance?.pendingBacklogCount ?? 0,
174
+ nudgesThisCycle: input.evaluation.nudgeAgentIds.length,
175
+ idleDrainCandidates: input.evaluation.idleDrainAgentIds.length,
176
+ assignedBacklogCount: input.maintenance?.assignedBacklogCount ?? 0,
177
+ reapedAgents: input.maintenance?.reapedAgentIds.length ?? 0,
178
+ repairedThreadClaims: input.maintenance?.repairedThreadClaims ?? 0,
179
+ maintenanceAnomalies: input.maintenance?.anomalies ?? [],
180
+ anomalies: input.evaluation.anomalies,
181
+ taskCounts: {
182
+ assigned: visibleAssignments.filter((assignment) => assignment.status === "assigned").length,
183
+ branchPushed: visibleAssignments.filter((assignment) => assignment.status === "branch_pushed")
184
+ .length,
185
+ openPrs: visibleAssignments.filter((assignment) => assignment.status === "pr_open").length,
186
+ mergedPrs: visibleAssignments.filter((assignment) => assignment.status === "pr_merged")
187
+ .length,
188
+ closedPrs: visibleAssignments.filter((assignment) => assignment.status === "pr_closed")
189
+ .length,
190
+ },
191
+ activeTasks,
192
+ recentOutcomes,
193
+ activeLanes: sortedLanes
194
+ .filter((lane) => lane.state !== "done" && lane.state !== "cancelled" && lane.state !== "detached")
195
+ .map(formatLaneStatusShort)
196
+ .slice(0, 8),
197
+ detachedLanes: sortedLanes
198
+ .filter((lane) => lane.state === "detached")
199
+ .map(formatLaneStatusShort)
200
+ .slice(0, 8),
201
+ roster,
202
+ recentCycles: input.recentCycles.slice(0, 5).map((cycle) => ({
203
+ startedAt: formatTimestamp(cycle.startedAt),
204
+ duration: formatDuration(cycle.durationMs),
205
+ agentCount: cycle.agentCount,
206
+ backlogCount: cycle.backlogCount,
207
+ ghostCount: cycle.ghostAgentIds.length,
208
+ stuckCount: cycle.stuckAgentIds.length,
209
+ anomalySummary: summarizeCycleAnomalies(cycle.anomalies),
210
+ followUpDelivered: cycle.followUpDelivered,
211
+ })),
212
+ };
213
+ }
@@ -0,0 +1,49 @@
1
+ import type { AgentInfo } from "./types.js";
2
+ export type GhostReaperSignal = "SIGTERM" | "SIGKILL";
3
+ export interface ProcessSnapshot {
4
+ pid: number;
5
+ command: string;
6
+ startedAt?: string | null;
7
+ }
8
+ export interface GhostReaperDeps {
9
+ inspectProcess?: (pid: number) => ProcessSnapshot | null;
10
+ signalProcess?: (pid: number, signal: GhostReaperSignal) => void;
11
+ setTimeout?: (callback: () => void, delayMs: number) => unknown;
12
+ clearTimeout?: (handle: unknown) => void;
13
+ now?: () => number;
14
+ brokerAgentId?: () => string | null;
15
+ getAgentById?: (agentId: string) => AgentInfo | null;
16
+ }
17
+ export interface BrokerGhostReaperOptions {
18
+ killAfterMs?: number;
19
+ }
20
+ export type GhostReapDecision = {
21
+ eligible: true;
22
+ agentId: string;
23
+ pid: number;
24
+ command: string;
25
+ } | {
26
+ eligible: false;
27
+ agentId: string;
28
+ pid?: number;
29
+ reason: string;
30
+ };
31
+ export interface GhostReapAttempt {
32
+ agentId: string;
33
+ pid?: number;
34
+ action: "sigterm" | "sigkill" | "skipped" | "pending" | "failed";
35
+ reason?: string;
36
+ }
37
+ export interface GhostReapResult {
38
+ attempts: GhostReapAttempt[];
39
+ signaledAgentIds: string[];
40
+ skippedAgentIds: string[];
41
+ }
42
+ export declare function isBrokerManagedAgent(agent: Pick<AgentInfo, "metadata">): boolean;
43
+ export declare function inspectProcess(pid: number): ProcessSnapshot | null;
44
+ export declare function decideGhostReapEligibility(agent: AgentInfo, snapshot: ProcessSnapshot | null, brokerAgentId?: string | null, now?: number): GhostReapDecision;
45
+ export interface BrokerGhostReaper {
46
+ reapGhosts: (agents: AgentInfo[]) => GhostReapResult;
47
+ dispose: () => void;
48
+ }
49
+ export declare function createBrokerGhostReaper(deps?: GhostReaperDeps, options?: BrokerGhostReaperOptions): BrokerGhostReaper;
@@ -0,0 +1,209 @@
1
+ import { execFileSync } from "node:child_process";
2
+ const DEFAULT_KILL_AFTER_MS = 10_000;
3
+ const PS_LSTART_MAX_FRACTION_MS = 999;
4
+ const CLOCK_SKEW_TOLERANCE_MS = 1_000;
5
+ function asRecord(value) {
6
+ return value && typeof value === "object" && !Array.isArray(value)
7
+ ? value
8
+ : null;
9
+ }
10
+ function asString(value) {
11
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
12
+ }
13
+ export function isBrokerManagedAgent(agent) {
14
+ const metadata = asRecord(agent.metadata);
15
+ if (!metadata)
16
+ return false;
17
+ if (metadata.brokerManaged === true)
18
+ return true;
19
+ const managed = asRecord(metadata.pinetBrokerManaged);
20
+ return managed?.brokerManaged === true;
21
+ }
22
+ function getBrokerManagedBy(metadata) {
23
+ if (!metadata)
24
+ return null;
25
+ const direct = asString(metadata.brokerManagedBy);
26
+ if (direct)
27
+ return direct;
28
+ const managed = asRecord(metadata.pinetBrokerManaged);
29
+ return asString(managed?.brokerAgentId);
30
+ }
31
+ function commandLooksLikePiFollower(command) {
32
+ const lower = command.toLowerCase();
33
+ return lower.includes("pi-coding-agent") || /(^|[/\s])pi(\s|$)/.test(lower);
34
+ }
35
+ function parsePsSnapshot(pid, output) {
36
+ const line = output.trim();
37
+ if (!line)
38
+ return null;
39
+ const match = line.match(/^(\d+)\s+(\S+\s+\S+\s+\d+\s+\d{2}:\d{2}:\d{2}\s+\d{4})\s+(.+)$/);
40
+ if (!match)
41
+ return null;
42
+ const parsedPid = Number(match[1]);
43
+ if (!Number.isFinite(parsedPid) || parsedPid !== pid)
44
+ return null;
45
+ return {
46
+ pid: parsedPid,
47
+ startedAt: match[2] ?? null,
48
+ command: match[3]?.trim() ?? "",
49
+ };
50
+ }
51
+ export function inspectProcess(pid) {
52
+ if (!Number.isInteger(pid) || pid <= 1)
53
+ return null;
54
+ try {
55
+ const output = execFileSync("ps", ["-p", String(pid), "-o", "pid=", "-o", "lstart=", "-o", "command="], {
56
+ encoding: "utf8",
57
+ stdio: ["ignore", "pipe", "ignore"],
58
+ });
59
+ return parsePsSnapshot(pid, output);
60
+ }
61
+ catch {
62
+ return null;
63
+ }
64
+ }
65
+ function defaultSignalProcess(pid, signal) {
66
+ process.kill(pid, signal);
67
+ }
68
+ function parseIso(value) {
69
+ if (!value)
70
+ return null;
71
+ const ms = Date.parse(value);
72
+ return Number.isNaN(ms) ? null : ms;
73
+ }
74
+ export function decideGhostReapEligibility(agent, snapshot, brokerAgentId, now = Date.now()) {
75
+ const metadata = asRecord(agent.metadata);
76
+ const pid = agent.pid;
77
+ if (!Number.isInteger(pid) || pid <= 1) {
78
+ return { eligible: false, agentId: agent.id, reason: "missing_or_unsafe_pid" };
79
+ }
80
+ if (pid === process.pid) {
81
+ return { eligible: false, agentId: agent.id, pid, reason: "refuse_current_process" };
82
+ }
83
+ if (agent.metadata?.role === "broker") {
84
+ return { eligible: false, agentId: agent.id, pid, reason: "broker_role" };
85
+ }
86
+ if (!isBrokerManagedAgent(agent)) {
87
+ return { eligible: false, agentId: agent.id, pid, reason: "not_broker_managed" };
88
+ }
89
+ const managedBy = getBrokerManagedBy(metadata);
90
+ if (!brokerAgentId) {
91
+ return { eligible: false, agentId: agent.id, pid, reason: "missing_broker_identity" };
92
+ }
93
+ if (!managedBy) {
94
+ return { eligible: false, agentId: agent.id, pid, reason: "missing_broker_managed_by" };
95
+ }
96
+ if (managedBy !== brokerAgentId) {
97
+ return { eligible: false, agentId: agent.id, pid, reason: "managed_by_different_broker" };
98
+ }
99
+ if (!agent.disconnectedAt) {
100
+ return { eligible: false, agentId: agent.id, pid, reason: "agent_not_disconnected" };
101
+ }
102
+ if (!snapshot) {
103
+ return { eligible: false, agentId: agent.id, pid, reason: "process_not_found" };
104
+ }
105
+ if (snapshot.pid !== pid) {
106
+ return { eligible: false, agentId: agent.id, pid, reason: "pid_mismatch" };
107
+ }
108
+ if (!commandLooksLikePiFollower(snapshot.command)) {
109
+ return { eligible: false, agentId: agent.id, pid, reason: "unexpected_command" };
110
+ }
111
+ const startedAtMs = parseIso(snapshot.startedAt);
112
+ if (startedAtMs == null) {
113
+ return { eligible: false, agentId: agent.id, pid, reason: "missing_process_start_time" };
114
+ }
115
+ const connectedAtMs = parseIso(agent.connectedAt);
116
+ if (connectedAtMs == null) {
117
+ return { eligible: false, agentId: agent.id, pid, reason: "missing_agent_connected_at" };
118
+ }
119
+ const disconnectedAtMs = parseIso(agent.disconnectedAt);
120
+ if (disconnectedAtMs == null) {
121
+ return { eligible: false, agentId: agent.id, pid, reason: "missing_agent_disconnected_at" };
122
+ }
123
+ if (startedAtMs + PS_LSTART_MAX_FRACTION_MS >= disconnectedAtMs) {
124
+ return { eligible: false, agentId: agent.id, pid, reason: "pid_reused_after_disconnect" };
125
+ }
126
+ if (startedAtMs + PS_LSTART_MAX_FRACTION_MS >= connectedAtMs) {
127
+ return { eligible: false, agentId: agent.id, pid, reason: "pid_reused_after_registration" };
128
+ }
129
+ if (disconnectedAtMs > now + CLOCK_SKEW_TOLERANCE_MS) {
130
+ return { eligible: false, agentId: agent.id, pid, reason: "disconnect_time_in_future" };
131
+ }
132
+ return { eligible: true, agentId: agent.id, pid, command: snapshot.command };
133
+ }
134
+ export function createBrokerGhostReaper(deps = {}, options = {}) {
135
+ const inspect = deps.inspectProcess ?? inspectProcess;
136
+ const signal = deps.signalProcess ?? defaultSignalProcess;
137
+ const schedule = deps.setTimeout ?? ((callback, delayMs) => setTimeout(callback, delayMs));
138
+ const cancel = deps.clearTimeout ?? ((handle) => clearTimeout(handle));
139
+ const now = deps.now ?? (() => Date.now());
140
+ const killAfterMs = options.killAfterMs ?? DEFAULT_KILL_AFTER_MS;
141
+ const pendingKills = new Map();
142
+ function scheduleBoundedKill(agent, pid) {
143
+ if (pendingKills.has(pid))
144
+ return;
145
+ const handle = schedule(() => {
146
+ pendingKills.delete(pid);
147
+ const latestAgent = deps.getAgentById?.(agent.id) ?? agent;
148
+ if (!latestAgent || latestAgent.pid !== pid)
149
+ return;
150
+ const snapshot = inspect(pid);
151
+ const decision = decideGhostReapEligibility(latestAgent, snapshot, deps.brokerAgentId?.(), now());
152
+ if (!decision.eligible)
153
+ return;
154
+ try {
155
+ signal(pid, "SIGKILL");
156
+ }
157
+ catch {
158
+ // Best effort: maintenance should never crash while cleaning up ghosts.
159
+ }
160
+ }, killAfterMs);
161
+ pendingKills.set(pid, handle);
162
+ }
163
+ function reapGhosts(agents) {
164
+ const attempts = [];
165
+ const signaledAgentIds = [];
166
+ const skippedAgentIds = [];
167
+ for (const agent of agents) {
168
+ const pid = Number.isInteger(agent.pid) ? agent.pid : undefined;
169
+ if (pid != null && pendingKills.has(pid)) {
170
+ attempts.push({
171
+ agentId: agent.id,
172
+ pid,
173
+ action: "pending",
174
+ reason: "bounded_kill_pending",
175
+ });
176
+ continue;
177
+ }
178
+ const snapshot = pid != null ? inspect(pid) : null;
179
+ const decision = decideGhostReapEligibility(agent, snapshot, deps.brokerAgentId?.(), now());
180
+ if (!decision.eligible) {
181
+ skippedAgentIds.push(agent.id);
182
+ attempts.push({ agentId: agent.id, pid, action: "skipped", reason: decision.reason });
183
+ continue;
184
+ }
185
+ try {
186
+ signal(decision.pid, "SIGTERM");
187
+ signaledAgentIds.push(agent.id);
188
+ attempts.push({ agentId: agent.id, pid: decision.pid, action: "sigterm" });
189
+ scheduleBoundedKill(agent, decision.pid);
190
+ }
191
+ catch (error) {
192
+ attempts.push({
193
+ agentId: agent.id,
194
+ pid: decision.pid,
195
+ action: "failed",
196
+ reason: error instanceof Error ? error.message : String(error),
197
+ });
198
+ }
199
+ }
200
+ return { attempts, signaledAgentIds, skippedAgentIds };
201
+ }
202
+ function dispose() {
203
+ for (const handle of pendingKills.values()) {
204
+ cancel(handle);
205
+ }
206
+ pendingKills.clear();
207
+ }
208
+ return { reapGhosts, dispose };
209
+ }
@@ -0,0 +1,36 @@
1
+ import { BrokerDB } from "./schema.js";
2
+ import { BrokerSocketServer } from "./socket-server.js";
3
+ import type { ListenTarget } from "./socket-server.js";
4
+ import { LeaderLock } from "./leader.js";
5
+ import type { MessageAdapter } from "./types.js";
6
+ export { BrokerDB } from "./schema.js";
7
+ export { BrokerSocketServer } from "./socket-server.js";
8
+ export { LeaderLock } from "./leader.js";
9
+ export type { ListenTarget } from "./socket-server.js";
10
+ export type { AgentMessageCallback, AgentRegistrationResolver } from "./socket-server.js";
11
+ export type { AgentInfo, ThreadInfo, ScheduledWakeupInfo, ScheduledWakeupDelivery, BrokerMessage, InboxEntry, InboundMessage, OutboundMessage, AdapterCapabilityRequest, AdapterCapabilityResult, MessageAdapter, JsonRpcRequest, JsonRpcResponse, TaskAssignmentInfo, TaskAssignmentStatus, } from "./types.js";
12
+ export interface BrokerOptions {
13
+ dbPath?: string;
14
+ /** Unix socket path (shorthand for { type: "unix", path }) */
15
+ socketPath?: string;
16
+ /** Full listen target — overrides socketPath when provided */
17
+ listenTarget?: ListenTarget;
18
+ lockPath?: string;
19
+ meshSecret?: string;
20
+ meshSecretPath?: string;
21
+ }
22
+ export interface Broker {
23
+ db: BrokerDB;
24
+ server: BrokerSocketServer;
25
+ lock: LeaderLock;
26
+ adapters: MessageAdapter[];
27
+ addAdapter(adapter: MessageAdapter): void;
28
+ stop(): Promise<void>;
29
+ }
30
+ /**
31
+ * Start the broker: acquire leader lock, initialize SQLite, start the Unix socket server.
32
+ * Only one broker may run at a time — enforced by a PID lock file.
33
+ *
34
+ * Throws if another broker process already holds the lock.
35
+ */
36
+ export declare function startBroker(options?: BrokerOptions): Promise<Broker>;