@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.
- package/LICENSE +21 -0
- package/README.md +570 -0
- package/dist/activity-log.d.ts +62 -0
- package/dist/activity-log.js +293 -0
- package/dist/agent-completion-runtime.d.ts +17 -0
- package/dist/agent-completion-runtime.js +26 -0
- package/dist/agent-event-runtime.d.ts +13 -0
- package/dist/agent-event-runtime.js +27 -0
- package/dist/agent-prompt-guidance.d.ts +22 -0
- package/dist/agent-prompt-guidance.js +46 -0
- package/dist/broker/adapters/slack.d.ts +74 -0
- package/dist/broker/adapters/slack.js +563 -0
- package/dist/broker/adapters/types.d.ts +1 -0
- package/dist/broker/adapters/types.js +1 -0
- package/dist/broker/agent-messaging.d.ts +1 -0
- package/dist/broker/agent-messaging.js +1 -0
- package/dist/broker/auth.d.ts +1 -0
- package/dist/broker/auth.js +1 -0
- package/dist/broker/client.d.ts +167 -0
- package/dist/broker/client.js +624 -0
- package/dist/broker/control-plane-dashboard.d.ts +98 -0
- package/dist/broker/control-plane-dashboard.js +213 -0
- package/dist/broker/ghost-reaper.d.ts +49 -0
- package/dist/broker/ghost-reaper.js +209 -0
- package/dist/broker/index.d.ts +36 -0
- package/dist/broker/index.js +90 -0
- package/dist/broker/leader.d.ts +1 -0
- package/dist/broker/leader.js +1 -0
- package/dist/broker/maintenance.d.ts +1 -0
- package/dist/broker/maintenance.js +1 -0
- package/dist/broker/message-send.d.ts +1 -0
- package/dist/broker/message-send.js +1 -0
- package/dist/broker/paths.d.ts +1 -0
- package/dist/broker/paths.js +1 -0
- package/dist/broker/raw-tcp-loopback.d.ts +1 -0
- package/dist/broker/raw-tcp-loopback.js +1 -0
- package/dist/broker/router.d.ts +1 -0
- package/dist/broker/router.js +1 -0
- package/dist/broker/schema.d.ts +9 -0
- package/dist/broker/schema.js +63 -0
- package/dist/broker/socket-server.d.ts +120 -0
- package/dist/broker/socket-server.js +1087 -0
- package/dist/broker/types.d.ts +1 -0
- package/dist/broker/types.js +1 -0
- package/dist/broker-delivery.d.ts +10 -0
- package/dist/broker-delivery.js +26 -0
- package/dist/broker-inbound-persistence.d.ts +11 -0
- package/dist/broker-inbound-persistence.js +22 -0
- package/dist/broker-prompt-loader.d.ts +34 -0
- package/dist/broker-prompt-loader.js +185 -0
- package/dist/broker-runtime-access.d.ts +14 -0
- package/dist/broker-runtime-access.js +20 -0
- package/dist/broker-runtime.d.ts +100 -0
- package/dist/broker-runtime.js +533 -0
- package/dist/broker-thread-owner-hints.d.ts +10 -0
- package/dist/broker-thread-owner-hints.js +14 -0
- package/dist/canvases.d.ts +80 -0
- package/dist/canvases.js +221 -0
- package/dist/command-registration-runtime.d.ts +9 -0
- package/dist/command-registration-runtime.js +9 -0
- package/dist/core-tool-guardrails.d.ts +24 -0
- package/dist/core-tool-guardrails.js +66 -0
- package/dist/deploy-manifest.d.ts +27 -0
- package/dist/deploy-manifest.js +197 -0
- package/dist/follower-delivery.d.ts +16 -0
- package/dist/follower-delivery.js +70 -0
- package/dist/follower-runtime.d.ts +61 -0
- package/dist/follower-runtime.js +350 -0
- package/dist/git-metadata.d.ts +35 -0
- package/dist/git-metadata.js +88 -0
- package/dist/guardrails.d.ts +69 -0
- package/dist/guardrails.js +283 -0
- package/dist/helpers.d.ts +550 -0
- package/dist/helpers.js +2711 -0
- package/dist/home-tab.d.ts +27 -0
- package/dist/home-tab.js +242 -0
- package/dist/imessage-tools.d.ts +33 -0
- package/dist/imessage-tools.js +96 -0
- package/dist/inbox-drain-runtime.d.ts +30 -0
- package/dist/inbox-drain-runtime.js +71 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1330 -0
- package/dist/persisted-runtime-state.d.ts +52 -0
- package/dist/persisted-runtime-state.js +98 -0
- package/dist/pinet-activity-formatting.d.ts +20 -0
- package/dist/pinet-activity-formatting.js +43 -0
- package/dist/pinet-agent-status.d.ts +36 -0
- package/dist/pinet-agent-status.js +48 -0
- package/dist/pinet-commands.d.ts +70 -0
- package/dist/pinet-commands.js +402 -0
- package/dist/pinet-confirmation-replies.d.ts +7 -0
- package/dist/pinet-confirmation-replies.js +30 -0
- package/dist/pinet-control-plane-dashboard.d.ts +34 -0
- package/dist/pinet-control-plane-dashboard.js +86 -0
- package/dist/pinet-home-tabs.d.ts +36 -0
- package/dist/pinet-home-tabs.js +101 -0
- package/dist/pinet-maintenance-delivery.d.ts +22 -0
- package/dist/pinet-maintenance-delivery.js +45 -0
- package/dist/pinet-mesh-ops.d.ts +97 -0
- package/dist/pinet-mesh-ops.js +266 -0
- package/dist/pinet-registration-gate.d.ts +14 -0
- package/dist/pinet-registration-gate.js +42 -0
- package/dist/pinet-remote-control-acks.d.ts +16 -0
- package/dist/pinet-remote-control-acks.js +46 -0
- package/dist/pinet-remote-control.d.ts +17 -0
- package/dist/pinet-remote-control.js +74 -0
- package/dist/pinet-runtime-composition.d.ts +27 -0
- package/dist/pinet-runtime-composition.js +20 -0
- package/dist/pinet-tools.d.ts +67 -0
- package/dist/pinet-tools.js +1211 -0
- package/dist/prompts/broker/default.md +41 -0
- package/dist/prompts/broker/tmux.md +49 -0
- package/dist/ralph-loop.d.ts +76 -0
- package/dist/ralph-loop.js +524 -0
- package/dist/reaction-triggers.d.ts +25 -0
- package/dist/reaction-triggers.js +171 -0
- package/dist/repo-tool-guardrails.d.ts +20 -0
- package/dist/repo-tool-guardrails.js +81 -0
- package/dist/runtime-agent-context.d.ts +93 -0
- package/dist/runtime-agent-context.js +302 -0
- package/dist/runtime-mode.d.ts +10 -0
- package/dist/runtime-mode.js +39 -0
- package/dist/scheduled-wakeups.d.ts +1 -0
- package/dist/scheduled-wakeups.js +1 -0
- package/dist/session-ui-runtime.d.ts +20 -0
- package/dist/session-ui-runtime.js +163 -0
- package/dist/single-player-runtime.d.ts +71 -0
- package/dist/single-player-runtime.js +436 -0
- package/dist/skins/cosmere.json +1571 -0
- package/dist/skins/foundation.json +1304 -0
- package/dist/slack-access.d.ts +186 -0
- package/dist/slack-access.js +462 -0
- package/dist/slack-api.d.ts +2 -0
- package/dist/slack-api.js +1 -0
- package/dist/slack-block-kit.d.ts +36 -0
- package/dist/slack-block-kit.js +242 -0
- package/dist/slack-export.d.ts +31 -0
- package/dist/slack-export.js +206 -0
- package/dist/slack-message-context.d.ts +15 -0
- package/dist/slack-message-context.js +207 -0
- package/dist/slack-modals.d.ts +13 -0
- package/dist/slack-modals.js +79 -0
- package/dist/slack-pinet-runtime-adapter.d.ts +16 -0
- package/dist/slack-pinet-runtime-adapter.js +72 -0
- package/dist/slack-presence.d.ts +36 -0
- package/dist/slack-presence.js +89 -0
- package/dist/slack-request-runtime.d.ts +8 -0
- package/dist/slack-request-runtime.js +19 -0
- package/dist/slack-runtime-access.d.ts +28 -0
- package/dist/slack-runtime-access.js +107 -0
- package/dist/slack-scope-diagnostics.d.ts +31 -0
- package/dist/slack-scope-diagnostics.js +235 -0
- package/dist/slack-socket-dedup.d.ts +7 -0
- package/dist/slack-socket-dedup.js +135 -0
- package/dist/slack-thread-status.d.ts +41 -0
- package/dist/slack-thread-status.js +156 -0
- package/dist/slack-tool-policy-runtime.d.ts +36 -0
- package/dist/slack-tool-policy-runtime.js +96 -0
- package/dist/slack-tools.d.ts +52 -0
- package/dist/slack-tools.js +2688 -0
- package/dist/slack-turn-guardrails.d.ts +19 -0
- package/dist/slack-turn-guardrails.js +36 -0
- package/dist/slack-upload.d.ts +49 -0
- package/dist/slack-upload.js +224 -0
- package/dist/task-assignments.d.ts +45 -0
- package/dist/task-assignments.js +527 -0
- package/dist/thread-confirmations.d.ts +22 -0
- package/dist/thread-confirmations.js +139 -0
- package/dist/tool-registration-runtime.d.ts +13 -0
- package/dist/tool-registration-runtime.js +13 -0
- package/dist/ttl-cache.d.ts +47 -0
- package/dist/ttl-cache.js +105 -0
- package/manifest.yaml +57 -0
- package/package.json +60 -0
- package/skills/pinet-skin-creator/SKILL.md +109 -0
- package/skills/pinet-skin-creator/references/descriptor-format.md +99 -0
- package/skills/pinet-skin-creator/references/safety-checklist.md +56 -0
- package/skills/pinet-skin-creator/templates/pinet-skin-descriptor.json +77 -0
- package/skills/slack-bridge/SKILL.md +360 -0
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
const execFileAsync = promisify(execFile);
|
|
4
|
+
const WORKTREE_BRANCH_REGEX = /\bgit\s+worktree\s+add\b[^\n]*?\s-b\s+([^\s`"',;]+)/i;
|
|
5
|
+
const CHECKOUT_BRANCH_REGEX = /\bgit\s+(?:checkout|switch)\s+-[cb]\s+([^\s`"',;]+)/i;
|
|
6
|
+
const EXPLICIT_BRANCH_LABEL_REGEX = /\bbranch(?:\s+to\s+work\s+on|\s+name)?\s*:\s*[`"']?([A-Za-z0-9._/-]+)\b/i;
|
|
7
|
+
const ISSUE_PR_LINE_REGEX = /(?:^|\n)\s*(?:[-*]\s*)?issue\/pr\s*:\s*#(\d+)\b/gi;
|
|
8
|
+
const ISSUE_LINE_REGEX = /(?:^|\n)\s*(?:[-*]\s*)?issue\s*:\s*#(\d+)\b/gi;
|
|
9
|
+
const ISSUE_HEADING_REGEX = /(?:^|\n)\s*(?:[-*]\s*)?issue\s+#(\d+)(?=\s*(?:[—–:-]|$))/gi;
|
|
10
|
+
const TASK_ISSUE_REGEX = /(?:^|\n)\s*(?:[-*]\s*)?task\s*:\s*[^\n#]*\bissue\s*#(\d+)\b/gi;
|
|
11
|
+
const NEW_TASK_ISSUE_REGEX = /(?:^|\n)\s*(?:[-*]\s*)?new(?:\s+[a-z-]+){0,3}\s+task\b[^\n#]*\bissue\s*#(\d+)\b/gi;
|
|
12
|
+
const FOLLOW_UP_TASK_ISSUE_REGEX = /(?:^|\n)\s*(?:[-*]\s*)?follow-up\s+task(?:\s+from)?\s+issue\s*#(\d+)\b/gi;
|
|
13
|
+
const GITHUB_REPO_URL_REGEX = /github\.com\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)\/(?:issues|pull)\/(\d+)\b/gi;
|
|
14
|
+
const GITHUB_REPO_ISSUE_URL_REGEX = /github\.com\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)\/issues\/(\d+)\b/gi;
|
|
15
|
+
const REPO_LABEL_REGEX = /(?:^|\n)\s*(?:[-*]\s*)?(?:repo|repository)\s*:\s*(?:https?:\/\/github\.com\/)?([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)\b/i;
|
|
16
|
+
const READ_ONLY_REVIEW_REGEX = /\b(?:read-only|do not mutate|do not edit|review-only|review only|second-pass|second pass|code review)\b/i;
|
|
17
|
+
const REVIEW_TASK_REGEX = /\b(?:review|code review)\b/i;
|
|
18
|
+
const QA_ONLY_TASK_REGEX = /\b(?:qa-only|qa only|test-only|test only|verify-only|verify only|validation-only|validation only)\b/i;
|
|
19
|
+
const QA_TASK_REGEX = /\b(?:qa|test|verify|validation|visual verify|visual verification)\b/i;
|
|
20
|
+
const MERGE_TASK_REGEX = /\bmerge\s+pr\s*#?\d+|\bmerge-only\b|\bmerge only\b/i;
|
|
21
|
+
const INTERACTIVE_TASK_REGEX = /\b(?:interactive-session|human-gated|manual|browser session)\b/i;
|
|
22
|
+
const IMPLEMENTATION_TASK_REGEX = /\b(?:implementation lane|implement|fix|build|change|patch|create a pr|open a pr|branch|worktree)\b/i;
|
|
23
|
+
async function runCommand(file, args, cwd, runner) {
|
|
24
|
+
try {
|
|
25
|
+
const result = await runner(file, args, { cwd, encoding: "utf-8" });
|
|
26
|
+
const stdout = typeof result.stdout === "string" ? result.stdout : result.stdout?.toString();
|
|
27
|
+
const trimmed = stdout?.trim();
|
|
28
|
+
return trimmed ? trimmed : undefined;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async function runJsonCommand(file, args, cwd, runner) {
|
|
35
|
+
const stdout = await runCommand(file, args, cwd, runner);
|
|
36
|
+
if (!stdout) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
return JSON.parse(stdout);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function normalizeMessageForTaskParsing(message) {
|
|
47
|
+
return message.replace(/\r\n/g, "\n").replace(/[`*_]/g, "");
|
|
48
|
+
}
|
|
49
|
+
function parseBranch(message) {
|
|
50
|
+
const normalized = normalizeMessageForTaskParsing(message);
|
|
51
|
+
const match = normalized.match(WORKTREE_BRANCH_REGEX) ??
|
|
52
|
+
normalized.match(CHECKOUT_BRANCH_REGEX) ??
|
|
53
|
+
normalized.match(EXPLICIT_BRANCH_LABEL_REGEX);
|
|
54
|
+
const branch = match?.[1]?.trim().replace(/[.,;:]+$/, "");
|
|
55
|
+
return branch ? branch : null;
|
|
56
|
+
}
|
|
57
|
+
function parseIssueNumbers(message) {
|
|
58
|
+
const normalized = normalizeMessageForTaskParsing(message);
|
|
59
|
+
const issueNumbers = new Set();
|
|
60
|
+
for (const regex of [
|
|
61
|
+
ISSUE_PR_LINE_REGEX,
|
|
62
|
+
ISSUE_LINE_REGEX,
|
|
63
|
+
ISSUE_HEADING_REGEX,
|
|
64
|
+
TASK_ISSUE_REGEX,
|
|
65
|
+
NEW_TASK_ISSUE_REGEX,
|
|
66
|
+
FOLLOW_UP_TASK_ISSUE_REGEX,
|
|
67
|
+
]) {
|
|
68
|
+
for (const match of normalized.matchAll(regex)) {
|
|
69
|
+
const issueNumber = Number(match[1]);
|
|
70
|
+
if (Number.isFinite(issueNumber)) {
|
|
71
|
+
issueNumbers.add(issueNumber);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
for (const match of normalized.matchAll(GITHUB_REPO_ISSUE_URL_REGEX)) {
|
|
76
|
+
const issueNumber = Number(match[3]);
|
|
77
|
+
if (Number.isFinite(issueNumber)) {
|
|
78
|
+
issueNumbers.add(issueNumber);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return [...issueNumbers].sort((left, right) => left - right);
|
|
82
|
+
}
|
|
83
|
+
function parseRepo(message, issueNumber) {
|
|
84
|
+
const normalized = normalizeMessageForTaskParsing(message);
|
|
85
|
+
for (const match of normalized.matchAll(GITHUB_REPO_ISSUE_URL_REGEX)) {
|
|
86
|
+
if (Number(match[3]) === issueNumber) {
|
|
87
|
+
return { repoOwner: match[1] ?? null, repoName: match[2] ?? null };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const repoMatch = normalized.match(REPO_LABEL_REGEX);
|
|
91
|
+
if (repoMatch) {
|
|
92
|
+
return { repoOwner: repoMatch[1] ?? null, repoName: repoMatch[2] ?? null };
|
|
93
|
+
}
|
|
94
|
+
const urlRepos = new Map();
|
|
95
|
+
for (const match of normalized.matchAll(GITHUB_REPO_URL_REGEX)) {
|
|
96
|
+
if (match[1] && match[2]) {
|
|
97
|
+
urlRepos.set(`${match[1].toLowerCase()}/${match[2].toLowerCase()}`, {
|
|
98
|
+
repoOwner: match[1],
|
|
99
|
+
repoName: match[2],
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (urlRepos.size === 1) {
|
|
104
|
+
return [...urlRepos.values()][0] ?? { repoOwner: null, repoName: null };
|
|
105
|
+
}
|
|
106
|
+
return { repoOwner: null, repoName: null };
|
|
107
|
+
}
|
|
108
|
+
function parseTaskKind(message, branch) {
|
|
109
|
+
const normalized = normalizeMessageForTaskParsing(message);
|
|
110
|
+
if (MERGE_TASK_REGEX.test(normalized))
|
|
111
|
+
return "merge";
|
|
112
|
+
if (INTERACTIVE_TASK_REGEX.test(normalized))
|
|
113
|
+
return "interactive";
|
|
114
|
+
if (READ_ONLY_REVIEW_REGEX.test(normalized))
|
|
115
|
+
return "review";
|
|
116
|
+
if (QA_ONLY_TASK_REGEX.test(normalized))
|
|
117
|
+
return "qa";
|
|
118
|
+
if (branch || IMPLEMENTATION_TASK_REGEX.test(normalized))
|
|
119
|
+
return "implementation";
|
|
120
|
+
if (REVIEW_TASK_REGEX.test(normalized))
|
|
121
|
+
return "review";
|
|
122
|
+
if (QA_TASK_REGEX.test(normalized))
|
|
123
|
+
return "qa";
|
|
124
|
+
return "unknown";
|
|
125
|
+
}
|
|
126
|
+
export function extractTaskAssignmentsFromMessage(message) {
|
|
127
|
+
const branch = parseBranch(message);
|
|
128
|
+
const taskKind = parseTaskKind(message, branch);
|
|
129
|
+
return parseIssueNumbers(message).map((issueNumber) => ({
|
|
130
|
+
issueNumber,
|
|
131
|
+
branch,
|
|
132
|
+
...parseRepo(message, issueNumber),
|
|
133
|
+
taskKind,
|
|
134
|
+
}));
|
|
135
|
+
}
|
|
136
|
+
function compareTaskAssignmentRecency(left, right) {
|
|
137
|
+
const updatedAt = Date.parse(right.updatedAt) - Date.parse(left.updatedAt);
|
|
138
|
+
if (updatedAt !== 0) {
|
|
139
|
+
return updatedAt;
|
|
140
|
+
}
|
|
141
|
+
const createdAt = Date.parse(right.createdAt) - Date.parse(left.createdAt);
|
|
142
|
+
if (createdAt !== 0) {
|
|
143
|
+
return createdAt;
|
|
144
|
+
}
|
|
145
|
+
return right.id - left.id;
|
|
146
|
+
}
|
|
147
|
+
function canonicalizeTaskAssignmentFromSourceMessage(assignment, sourceMessagesById) {
|
|
148
|
+
if (assignment.sourceMessageId == null) {
|
|
149
|
+
return assignment;
|
|
150
|
+
}
|
|
151
|
+
const sourceMessage = sourceMessagesById.get(assignment.sourceMessageId);
|
|
152
|
+
if (!sourceMessage) {
|
|
153
|
+
return assignment;
|
|
154
|
+
}
|
|
155
|
+
const parsedAssignments = extractTaskAssignmentsFromMessage(sourceMessage);
|
|
156
|
+
if (parsedAssignments.length === 0) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
const matchingAssignment = parsedAssignments.find((candidate) => candidate.issueNumber === assignment.issueNumber);
|
|
160
|
+
if (matchingAssignment) {
|
|
161
|
+
return {
|
|
162
|
+
...assignment,
|
|
163
|
+
branch: matchingAssignment.branch,
|
|
164
|
+
repoOwner: matchingAssignment.repoOwner ?? assignment.repoOwner,
|
|
165
|
+
repoName: matchingAssignment.repoName ?? assignment.repoName,
|
|
166
|
+
taskKind: matchingAssignment.taskKind,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
if (parsedAssignments.length === 1) {
|
|
170
|
+
const [canonicalAssignment] = parsedAssignments;
|
|
171
|
+
return {
|
|
172
|
+
...assignment,
|
|
173
|
+
issueNumber: canonicalAssignment.issueNumber,
|
|
174
|
+
branch: canonicalAssignment.branch,
|
|
175
|
+
repoOwner: canonicalAssignment.repoOwner ?? assignment.repoOwner,
|
|
176
|
+
repoName: canonicalAssignment.repoName ?? assignment.repoName,
|
|
177
|
+
taskKind: canonicalAssignment.taskKind,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
export function normalizeTrackedTaskAssignments(assignments, sourceMessagesById = new Map()) {
|
|
183
|
+
const canonicalAssignments = assignments
|
|
184
|
+
.map((assignment) => canonicalizeTaskAssignmentFromSourceMessage(assignment, sourceMessagesById))
|
|
185
|
+
.filter((assignment) => assignment != null)
|
|
186
|
+
.sort(compareTaskAssignmentRecency);
|
|
187
|
+
const visibleAssignments = [];
|
|
188
|
+
const seenIssueKeys = new Set();
|
|
189
|
+
for (const assignment of canonicalAssignments) {
|
|
190
|
+
const repoKey = assignment.repoOwner && assignment.repoName
|
|
191
|
+
? `${assignment.repoOwner.toLowerCase()}/${assignment.repoName.toLowerCase()}`
|
|
192
|
+
: assignment.repoRoot
|
|
193
|
+
? `root:${assignment.repoRoot}`
|
|
194
|
+
: "repo_unknown";
|
|
195
|
+
const issueKey = `${repoKey}#${assignment.issueNumber}`;
|
|
196
|
+
if (seenIssueKeys.has(issueKey)) {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
seenIssueKeys.add(issueKey);
|
|
200
|
+
visibleAssignments.push(assignment);
|
|
201
|
+
}
|
|
202
|
+
return visibleAssignments;
|
|
203
|
+
}
|
|
204
|
+
function normalizePullRequests(prs) {
|
|
205
|
+
if (!Array.isArray(prs)) {
|
|
206
|
+
return [];
|
|
207
|
+
}
|
|
208
|
+
return prs.filter((pr) => typeof pr?.number === "number" &&
|
|
209
|
+
typeof pr?.state === "string" &&
|
|
210
|
+
typeof pr?.headRefName === "string");
|
|
211
|
+
}
|
|
212
|
+
function chooseBestPullRequest(prs) {
|
|
213
|
+
if (prs.length === 0) {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
const score = (pr) => {
|
|
217
|
+
if (pr.mergedAt)
|
|
218
|
+
return 3;
|
|
219
|
+
if (pr.state.toUpperCase() === "OPEN")
|
|
220
|
+
return 2;
|
|
221
|
+
return 1;
|
|
222
|
+
};
|
|
223
|
+
return ([...prs].sort((left, right) => {
|
|
224
|
+
const byScore = score(right) - score(left);
|
|
225
|
+
if (byScore !== 0)
|
|
226
|
+
return byScore;
|
|
227
|
+
const leftMergedAt = left.mergedAt ? Date.parse(left.mergedAt) : 0;
|
|
228
|
+
const rightMergedAt = right.mergedAt ? Date.parse(right.mergedAt) : 0;
|
|
229
|
+
if (rightMergedAt !== leftMergedAt)
|
|
230
|
+
return rightMergedAt - leftMergedAt;
|
|
231
|
+
return right.number - left.number;
|
|
232
|
+
})[0] ?? null);
|
|
233
|
+
}
|
|
234
|
+
async function resolveBaseRef(cwd, runner) {
|
|
235
|
+
for (const ref of ["origin/main", "main"]) {
|
|
236
|
+
const resolved = await runCommand("git", ["rev-parse", "--verify", "--quiet", ref], cwd, runner);
|
|
237
|
+
if (resolved) {
|
|
238
|
+
return ref;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
async function getBranchAheadCount(branch, baseRef, cwd, runner) {
|
|
244
|
+
if (!branch || !baseRef) {
|
|
245
|
+
return 0;
|
|
246
|
+
}
|
|
247
|
+
let maxAheadCount = 0;
|
|
248
|
+
const refs = [...new Set([branch, `origin/${branch}`])];
|
|
249
|
+
for (const ref of refs) {
|
|
250
|
+
const resolved = await runCommand("git", ["rev-parse", "--verify", "--quiet", ref], cwd, runner);
|
|
251
|
+
if (!resolved) {
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
const count = await runCommand("git", ["rev-list", "--count", `${baseRef}..${ref}`], cwd, runner);
|
|
255
|
+
const aheadCount = Number.parseInt(count ?? "0", 10);
|
|
256
|
+
if (Number.isFinite(aheadCount) && aheadCount > maxAheadCount) {
|
|
257
|
+
maxAheadCount = aheadCount;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return maxAheadCount;
|
|
261
|
+
}
|
|
262
|
+
function getRepoArg(assignment) {
|
|
263
|
+
return assignment.repoOwner && assignment.repoName
|
|
264
|
+
? ["--repo", `${assignment.repoOwner}/${assignment.repoName}`]
|
|
265
|
+
: [];
|
|
266
|
+
}
|
|
267
|
+
async function getPullRequestForBranch(assignment, cwd, runner) {
|
|
268
|
+
if (!assignment.branch) {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
const rawPrs = await runJsonCommand("gh", [
|
|
272
|
+
"pr",
|
|
273
|
+
"list",
|
|
274
|
+
...getRepoArg(assignment),
|
|
275
|
+
"--head",
|
|
276
|
+
assignment.branch,
|
|
277
|
+
"--state",
|
|
278
|
+
"all",
|
|
279
|
+
"--json",
|
|
280
|
+
"number,state,mergedAt,headRefName",
|
|
281
|
+
], cwd, runner);
|
|
282
|
+
if (rawPrs === undefined) {
|
|
283
|
+
return undefined;
|
|
284
|
+
}
|
|
285
|
+
const prs = normalizePullRequests(rawPrs);
|
|
286
|
+
return chooseBestPullRequest(prs);
|
|
287
|
+
}
|
|
288
|
+
async function getPullRequestByNumber(prNumber, assignment, cwd, runner) {
|
|
289
|
+
const pr = await runJsonCommand("gh", [
|
|
290
|
+
"pr",
|
|
291
|
+
"view",
|
|
292
|
+
String(prNumber),
|
|
293
|
+
...getRepoArg(assignment),
|
|
294
|
+
"--json",
|
|
295
|
+
"number,state,mergedAt,headRefName",
|
|
296
|
+
], cwd, runner);
|
|
297
|
+
if (pr === undefined) {
|
|
298
|
+
return undefined;
|
|
299
|
+
}
|
|
300
|
+
return normalizePullRequests([pr])[0] ?? null;
|
|
301
|
+
}
|
|
302
|
+
async function getIssueByNumber(assignment, cwd, runner) {
|
|
303
|
+
const issue = await runJsonCommand("gh", [
|
|
304
|
+
"issue",
|
|
305
|
+
"view",
|
|
306
|
+
String(assignment.issueNumber),
|
|
307
|
+
...getRepoArg(assignment),
|
|
308
|
+
"--json",
|
|
309
|
+
"number,state",
|
|
310
|
+
], cwd, runner);
|
|
311
|
+
if (issue === undefined) {
|
|
312
|
+
return undefined;
|
|
313
|
+
}
|
|
314
|
+
if (issue && typeof issue.number === "number" && typeof issue.state === "string") {
|
|
315
|
+
return issue;
|
|
316
|
+
}
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
function normalizeIssueState(issue) {
|
|
320
|
+
const state = issue?.state?.toUpperCase();
|
|
321
|
+
if (state === "OPEN" || state === "CLOSED") {
|
|
322
|
+
return state;
|
|
323
|
+
}
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
function hasTrackedPullRequestLink(assignment, pr) {
|
|
327
|
+
return (assignment.prNumber === pr.number ||
|
|
328
|
+
assignment.status === "pr_open" ||
|
|
329
|
+
assignment.status === "pr_merged" ||
|
|
330
|
+
assignment.status === "pr_closed");
|
|
331
|
+
}
|
|
332
|
+
function resolveTaskStatus(assignment, branchAheadCount, pr) {
|
|
333
|
+
if (pr === undefined && assignment.status.startsWith("pr_")) {
|
|
334
|
+
return { nextStatus: assignment.status, nextPrNumber: assignment.prNumber };
|
|
335
|
+
}
|
|
336
|
+
if (pr?.state.toUpperCase() === "OPEN") {
|
|
337
|
+
return { nextStatus: "pr_open", nextPrNumber: pr.number };
|
|
338
|
+
}
|
|
339
|
+
if (pr?.mergedAt) {
|
|
340
|
+
if (hasTrackedPullRequestLink(assignment, pr)) {
|
|
341
|
+
return { nextStatus: "pr_merged", nextPrNumber: pr.number };
|
|
342
|
+
}
|
|
343
|
+
if (branchAheadCount > 0) {
|
|
344
|
+
return { nextStatus: "branch_pushed", nextPrNumber: null };
|
|
345
|
+
}
|
|
346
|
+
return { nextStatus: "assigned", nextPrNumber: null };
|
|
347
|
+
}
|
|
348
|
+
if (pr) {
|
|
349
|
+
if (hasTrackedPullRequestLink(assignment, pr)) {
|
|
350
|
+
return { nextStatus: "pr_closed", nextPrNumber: pr.number };
|
|
351
|
+
}
|
|
352
|
+
if (branchAheadCount > 0) {
|
|
353
|
+
return { nextStatus: "branch_pushed", nextPrNumber: null };
|
|
354
|
+
}
|
|
355
|
+
return { nextStatus: "assigned", nextPrNumber: null };
|
|
356
|
+
}
|
|
357
|
+
if (branchAheadCount > 0) {
|
|
358
|
+
return { nextStatus: "branch_pushed", nextPrNumber: null };
|
|
359
|
+
}
|
|
360
|
+
return { nextStatus: "assigned", nextPrNumber: null };
|
|
361
|
+
}
|
|
362
|
+
export async function resolveTaskAssignments(assignments, cwd = process.cwd(), runner = execFileAsync) {
|
|
363
|
+
if (assignments.length === 0) {
|
|
364
|
+
return [];
|
|
365
|
+
}
|
|
366
|
+
const baseRefCache = new Map();
|
|
367
|
+
const resolveBaseRefForCwd = (repoCwd) => {
|
|
368
|
+
const cached = baseRefCache.get(repoCwd);
|
|
369
|
+
if (cached)
|
|
370
|
+
return cached;
|
|
371
|
+
const promise = resolveBaseRef(repoCwd, runner);
|
|
372
|
+
baseRefCache.set(repoCwd, promise);
|
|
373
|
+
return promise;
|
|
374
|
+
};
|
|
375
|
+
const branchProgressCache = new Map();
|
|
376
|
+
const pullRequestByNumberCache = new Map();
|
|
377
|
+
const issueByNumberCache = new Map();
|
|
378
|
+
const resolveBranchProgress = (assignment) => {
|
|
379
|
+
const repoCwd = assignment.repoRoot ?? (assignment.repoOwner && assignment.repoName ? null : null);
|
|
380
|
+
const commandCwd = repoCwd ?? cwd;
|
|
381
|
+
const cacheKey = `${assignment.repoOwner ?? ""}/${assignment.repoName ?? ""}:${repoCwd ?? "repo_root_unavailable"}:${assignment.branch ?? ""}`;
|
|
382
|
+
const cached = branchProgressCache.get(cacheKey);
|
|
383
|
+
if (cached) {
|
|
384
|
+
return cached;
|
|
385
|
+
}
|
|
386
|
+
const canResolveRemotePr = repoCwd != null || (assignment.repoOwner != null && assignment.repoName != null);
|
|
387
|
+
const branchAheadCountPromise = repoCwd
|
|
388
|
+
? resolveBaseRefForCwd(repoCwd).then((baseRef) => getBranchAheadCount(assignment.branch, baseRef, repoCwd, runner))
|
|
389
|
+
: Promise.resolve(0);
|
|
390
|
+
const promise = Promise.all([
|
|
391
|
+
branchAheadCountPromise,
|
|
392
|
+
canResolveRemotePr ? getPullRequestForBranch(assignment, commandCwd, runner) : null,
|
|
393
|
+
]).then(([branchAheadCount, pr]) => ({ branchAheadCount, pr }));
|
|
394
|
+
branchProgressCache.set(cacheKey, promise);
|
|
395
|
+
return promise;
|
|
396
|
+
};
|
|
397
|
+
const resolvePullRequestByNumber = (prNumber, assignment, repoCwd) => {
|
|
398
|
+
const cacheKey = `${assignment.repoOwner ?? ""}/${assignment.repoName ?? ""}:${repoCwd}#${prNumber}`;
|
|
399
|
+
const cached = pullRequestByNumberCache.get(cacheKey);
|
|
400
|
+
if (cached) {
|
|
401
|
+
return cached;
|
|
402
|
+
}
|
|
403
|
+
const promise = getPullRequestByNumber(prNumber, assignment, repoCwd, runner);
|
|
404
|
+
pullRequestByNumberCache.set(cacheKey, promise);
|
|
405
|
+
return promise;
|
|
406
|
+
};
|
|
407
|
+
const resolveIssueByNumber = (assignment) => {
|
|
408
|
+
const cacheKey = `${assignment.repoOwner ?? ""}/${assignment.repoName ?? ""}#${assignment.issueNumber}`;
|
|
409
|
+
const cached = issueByNumberCache.get(cacheKey);
|
|
410
|
+
if (cached) {
|
|
411
|
+
return cached;
|
|
412
|
+
}
|
|
413
|
+
const promise = getIssueByNumber(assignment, cwd, runner);
|
|
414
|
+
issueByNumberCache.set(cacheKey, promise);
|
|
415
|
+
return promise;
|
|
416
|
+
};
|
|
417
|
+
return Promise.all(assignments.map(async (assignment) => {
|
|
418
|
+
const { branchAheadCount, pr } = await resolveBranchProgress(assignment);
|
|
419
|
+
const canResolveStoredPr = assignment.repoRoot != null ||
|
|
420
|
+
(assignment.repoOwner != null && assignment.repoName != null);
|
|
421
|
+
const resolvedPr = pr == null && assignment.prNumber != null && canResolveStoredPr
|
|
422
|
+
? await resolvePullRequestByNumber(assignment.prNumber, assignment, assignment.repoRoot ?? cwd)
|
|
423
|
+
: pr;
|
|
424
|
+
const issueState = normalizeIssueState(await resolveIssueByNumber(assignment));
|
|
425
|
+
const { nextStatus, nextPrNumber } = resolveTaskStatus(assignment, branchAheadCount, resolvedPr);
|
|
426
|
+
return {
|
|
427
|
+
...assignment,
|
|
428
|
+
nextStatus,
|
|
429
|
+
nextPrNumber,
|
|
430
|
+
branchAheadCount,
|
|
431
|
+
issueState,
|
|
432
|
+
};
|
|
433
|
+
}));
|
|
434
|
+
}
|
|
435
|
+
export function hasTaskAssignmentStatusChange(assignment) {
|
|
436
|
+
return (assignment.status !== assignment.nextStatus || assignment.prNumber !== assignment.nextPrNumber);
|
|
437
|
+
}
|
|
438
|
+
function isImplementationProgressTask(taskKind) {
|
|
439
|
+
return taskKind === "implementation" || taskKind === "unknown";
|
|
440
|
+
}
|
|
441
|
+
function formatTaskProgressFragment(assignment) {
|
|
442
|
+
if (!isImplementationProgressTask(assignment.taskKind)) {
|
|
443
|
+
return `#${assignment.issueNumber} → ${assignment.taskKind} task, no implementation PR expected`;
|
|
444
|
+
}
|
|
445
|
+
switch (assignment.status) {
|
|
446
|
+
case "pr_merged":
|
|
447
|
+
return `#${assignment.issueNumber} → PR #${assignment.prNumber ?? "?"} MERGED ✅`;
|
|
448
|
+
case "pr_open":
|
|
449
|
+
return `#${assignment.issueNumber} → PR #${assignment.prNumber ?? "?"} OPEN 🔄`;
|
|
450
|
+
case "pr_closed":
|
|
451
|
+
return `#${assignment.issueNumber} → PR #${assignment.prNumber ?? "?"} CLOSED ⚠️`;
|
|
452
|
+
case "branch_pushed":
|
|
453
|
+
return `#${assignment.issueNumber} → commits on ${assignment.branch ?? "tracked branch"}, no PR 👀`;
|
|
454
|
+
case "assigned":
|
|
455
|
+
default:
|
|
456
|
+
if (!assignment.repoOwner && !assignment.repoName && !assignment.repoRoot) {
|
|
457
|
+
return `#${assignment.issueNumber} → repo unknown; progress not checked ⚠️`;
|
|
458
|
+
}
|
|
459
|
+
if (assignment.branch &&
|
|
460
|
+
assignment.repoOwner &&
|
|
461
|
+
assignment.repoName &&
|
|
462
|
+
!assignment.repoRoot) {
|
|
463
|
+
return `#${assignment.issueNumber} → no PR found for ${assignment.branch}; commits not checked (repo root unavailable) ⚠️`;
|
|
464
|
+
}
|
|
465
|
+
return `#${assignment.issueNumber} → no commits, no PR ⚠️`;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
function getVisibleTaskAssignmentReportEntries(assignments) {
|
|
469
|
+
return assignments.filter((assignment) => assignment.issueState !== "CLOSED" &&
|
|
470
|
+
assignment.status !== "pr_merged" &&
|
|
471
|
+
(assignment.status !== "pr_closed" || isImplementationProgressTask(assignment.taskKind)));
|
|
472
|
+
}
|
|
473
|
+
function formatAgentLabel(agentId, agentsById) {
|
|
474
|
+
const agent = agentsById.get(agentId);
|
|
475
|
+
if (!agent) {
|
|
476
|
+
return agentId;
|
|
477
|
+
}
|
|
478
|
+
return `${agent.emoji} ${agent.name}`.trim();
|
|
479
|
+
}
|
|
480
|
+
function getAgentSortKey(agentId, agentsById) {
|
|
481
|
+
const agent = agentsById.get(agentId);
|
|
482
|
+
return agent?.name ?? agentId;
|
|
483
|
+
}
|
|
484
|
+
export function buildTaskAssignmentReport(assignments, agentsById, cycleStartedAt) {
|
|
485
|
+
const visibleAssignments = getVisibleTaskAssignmentReportEntries(assignments);
|
|
486
|
+
if (visibleAssignments.length === 0) {
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
const grouped = new Map();
|
|
490
|
+
for (const assignment of visibleAssignments) {
|
|
491
|
+
const bucket = grouped.get(assignment.agentId);
|
|
492
|
+
if (bucket) {
|
|
493
|
+
bucket.push(assignment);
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
grouped.set(assignment.agentId, [assignment]);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
const lines = [...grouped.entries()]
|
|
500
|
+
.sort(([leftAgentId], [rightAgentId]) => {
|
|
501
|
+
const leftLabel = getAgentSortKey(leftAgentId, agentsById);
|
|
502
|
+
const rightLabel = getAgentSortKey(rightAgentId, agentsById);
|
|
503
|
+
return leftLabel.localeCompare(rightLabel);
|
|
504
|
+
})
|
|
505
|
+
.map(([agentId, agentAssignments]) => {
|
|
506
|
+
const summary = [...agentAssignments]
|
|
507
|
+
.sort((left, right) => left.issueNumber - right.issueNumber)
|
|
508
|
+
.map((assignment) => formatTaskProgressFragment(assignment))
|
|
509
|
+
.join("; ");
|
|
510
|
+
return `- ${formatAgentLabel(agentId, agentsById)}: ${summary}`;
|
|
511
|
+
});
|
|
512
|
+
const header = cycleStartedAt
|
|
513
|
+
? ["RALPH LOOP — WORKER STATUS:", `Timestamp: ${cycleStartedAt}`]
|
|
514
|
+
: ["RALPH LOOP — WORKER STATUS:"];
|
|
515
|
+
return [...header, ...lines].join("\n");
|
|
516
|
+
}
|
|
517
|
+
export function getPendingTaskAssignmentReport(assignments, agentsById, lastDeliveredSignature, cycleStartedAt) {
|
|
518
|
+
const signature = buildTaskAssignmentReport(assignments, agentsById);
|
|
519
|
+
if (!signature || signature === lastDeliveredSignature) {
|
|
520
|
+
return null;
|
|
521
|
+
}
|
|
522
|
+
const message = buildTaskAssignmentReport(assignments, agentsById, cycleStartedAt);
|
|
523
|
+
if (!message) {
|
|
524
|
+
return null;
|
|
525
|
+
}
|
|
526
|
+
return { message, signature };
|
|
527
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ConfirmationRequest } from "./helpers.js";
|
|
2
|
+
import { type SecurityGuardrails } from "./guardrails.js";
|
|
3
|
+
export interface RegisterConfirmationRequestResult {
|
|
4
|
+
status: "created" | "refreshed" | "conflict";
|
|
5
|
+
conflict?: ConfirmationRequest;
|
|
6
|
+
}
|
|
7
|
+
export interface ThreadConfirmationPolicy {
|
|
8
|
+
formatAction: (action: string) => string;
|
|
9
|
+
registerRequest: (threadTs: string, tool: string, action: string) => RegisterConfirmationRequestResult;
|
|
10
|
+
consumeReply: (threadTs: string, text: string, options?: {
|
|
11
|
+
receivedAt?: string | number | Date;
|
|
12
|
+
}) => {
|
|
13
|
+
approved: boolean;
|
|
14
|
+
} | null;
|
|
15
|
+
requireToolPolicy: (toolName: string, threadTs: string | undefined, action: string) => void;
|
|
16
|
+
}
|
|
17
|
+
export interface CreateThreadConfirmationPolicyDeps {
|
|
18
|
+
getGuardrails: () => SecurityGuardrails;
|
|
19
|
+
now?: () => number;
|
|
20
|
+
ttlMs?: number;
|
|
21
|
+
}
|
|
22
|
+
export declare function createThreadConfirmationPolicy(deps: CreateThreadConfirmationPolicyDeps): ThreadConfirmationPolicy;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { DEFAULT_CONFIRMATION_REQUEST_TTL_MS, confirmationRequestMatches, consumeMatchingConfirmationRequest, isThreadConfirmationStateEmpty, normalizeThreadConfirmationState, registerThreadConfirmationRequest, } from "./helpers.js";
|
|
2
|
+
import { isConfirmationApproval, isConfirmationRejection, isToolBlocked, toolNeedsConfirmation, } from "./guardrails.js";
|
|
3
|
+
export function createThreadConfirmationPolicy(deps) {
|
|
4
|
+
const threadConfirmationStates = new Map();
|
|
5
|
+
const getNow = deps.now ?? Date.now;
|
|
6
|
+
const ttlMs = deps.ttlMs ?? DEFAULT_CONFIRMATION_REQUEST_TTL_MS;
|
|
7
|
+
function storeThreadConfirmationState(threadTs, state, now = getNow()) {
|
|
8
|
+
const normalized = normalizeThreadConfirmationState(state, now, ttlMs);
|
|
9
|
+
if (isThreadConfirmationStateEmpty(normalized)) {
|
|
10
|
+
threadConfirmationStates.delete(threadTs);
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
threadConfirmationStates.set(threadTs, normalized);
|
|
14
|
+
return normalized;
|
|
15
|
+
}
|
|
16
|
+
function sweepThreadConfirmationStates(now = getNow()) {
|
|
17
|
+
for (const [threadTs, state] of threadConfirmationStates) {
|
|
18
|
+
storeThreadConfirmationState(threadTs, state, now);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function getThreadConfirmationState(threadTs) {
|
|
22
|
+
sweepThreadConfirmationStates();
|
|
23
|
+
let state = threadConfirmationStates.get(threadTs);
|
|
24
|
+
if (!state) {
|
|
25
|
+
state = { pending: [], approved: [], rejected: [] };
|
|
26
|
+
threadConfirmationStates.set(threadTs, state);
|
|
27
|
+
}
|
|
28
|
+
return state;
|
|
29
|
+
}
|
|
30
|
+
function cleanupThreadConfirmationState(threadTs) {
|
|
31
|
+
const state = threadConfirmationStates.get(threadTs);
|
|
32
|
+
if (!state)
|
|
33
|
+
return;
|
|
34
|
+
storeThreadConfirmationState(threadTs, state);
|
|
35
|
+
}
|
|
36
|
+
function formatAction(action) {
|
|
37
|
+
return JSON.stringify(action);
|
|
38
|
+
}
|
|
39
|
+
function registerRequest(threadTs, tool, action) {
|
|
40
|
+
const now = getNow();
|
|
41
|
+
const result = registerThreadConfirmationRequest(getThreadConfirmationState(threadTs), {
|
|
42
|
+
toolPattern: tool,
|
|
43
|
+
action,
|
|
44
|
+
requestedAt: now,
|
|
45
|
+
}, now);
|
|
46
|
+
storeThreadConfirmationState(threadTs, result.state, now);
|
|
47
|
+
return {
|
|
48
|
+
status: result.status,
|
|
49
|
+
conflict: result.conflict,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function consumeReply(threadTs, text, options = {}) {
|
|
53
|
+
sweepThreadConfirmationStates();
|
|
54
|
+
const state = threadConfirmationStates.get(threadTs);
|
|
55
|
+
if (!state || state.pending.length === 0)
|
|
56
|
+
return null;
|
|
57
|
+
const trimmed = text.trim();
|
|
58
|
+
const isApproval = isConfirmationApproval(trimmed);
|
|
59
|
+
const isRejection = isConfirmationRejection(trimmed);
|
|
60
|
+
if (!isApproval && !isRejection)
|
|
61
|
+
return null;
|
|
62
|
+
const request = state.pending[0];
|
|
63
|
+
if (!request)
|
|
64
|
+
return null;
|
|
65
|
+
if (options.receivedAt !== undefined) {
|
|
66
|
+
const receivedAtMs = options.receivedAt instanceof Date
|
|
67
|
+
? options.receivedAt.getTime()
|
|
68
|
+
: typeof options.receivedAt === "number"
|
|
69
|
+
? options.receivedAt
|
|
70
|
+
: Date.parse(options.receivedAt);
|
|
71
|
+
if (!Number.isFinite(receivedAtMs) || receivedAtMs < request.requestedAt) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
state.pending.shift();
|
|
76
|
+
if (isApproval) {
|
|
77
|
+
state.approved.push(request);
|
|
78
|
+
cleanupThreadConfirmationState(threadTs);
|
|
79
|
+
return { approved: true };
|
|
80
|
+
}
|
|
81
|
+
state.rejected.push(request);
|
|
82
|
+
cleanupThreadConfirmationState(threadTs);
|
|
83
|
+
return { approved: false };
|
|
84
|
+
}
|
|
85
|
+
function getConfirmationDecision(threadTs, toolName, action) {
|
|
86
|
+
sweepThreadConfirmationStates();
|
|
87
|
+
const state = threadConfirmationStates.get(threadTs);
|
|
88
|
+
if (!state)
|
|
89
|
+
return null;
|
|
90
|
+
const approved = consumeMatchingConfirmationRequest(state.approved, toolName, action);
|
|
91
|
+
if (approved) {
|
|
92
|
+
cleanupThreadConfirmationState(threadTs);
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
const rejected = consumeMatchingConfirmationRequest(state.rejected, toolName, action);
|
|
96
|
+
if (rejected) {
|
|
97
|
+
cleanupThreadConfirmationState(threadTs);
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
function requireToolPolicy(toolName, threadTs, action) {
|
|
103
|
+
const guardrails = deps.getGuardrails();
|
|
104
|
+
if (isToolBlocked(toolName, guardrails)) {
|
|
105
|
+
throw new Error(`Tool "${toolName}" is blocked by Slack security guardrails.`);
|
|
106
|
+
}
|
|
107
|
+
if (!toolNeedsConfirmation(toolName, guardrails)) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const quotedAction = formatAction(action);
|
|
111
|
+
if (!threadTs) {
|
|
112
|
+
throw new Error(`Tool "${toolName}" requires confirmation for action ${quotedAction}. Include a thread_ts and call slack with action "confirm_action" before executing this tool.`);
|
|
113
|
+
}
|
|
114
|
+
const decision = getConfirmationDecision(threadTs, toolName, action);
|
|
115
|
+
if (decision === true)
|
|
116
|
+
return;
|
|
117
|
+
if (decision === false) {
|
|
118
|
+
throw new Error(`Tool "${toolName}" was denied by Slack user confirmation for action ${quotedAction}.`);
|
|
119
|
+
}
|
|
120
|
+
sweepThreadConfirmationStates();
|
|
121
|
+
const state = threadConfirmationStates.get(threadTs);
|
|
122
|
+
const pendingMatch = state?.pending.find((request) => confirmationRequestMatches(request, toolName, action)) ??
|
|
123
|
+
null;
|
|
124
|
+
if (pendingMatch) {
|
|
125
|
+
throw new Error(`Tool "${toolName}" requires confirmation for action ${quotedAction}. A matching confirmation request is already pending in thread ${threadTs}; wait for the user's approval first.`);
|
|
126
|
+
}
|
|
127
|
+
const pendingConflict = state?.pending[0];
|
|
128
|
+
if (pendingConflict) {
|
|
129
|
+
throw new Error(`Thread ${threadTs} already has a pending confirmation for tool "${pendingConflict.toolPattern}" and action ${formatAction(pendingConflict.action)}. Wait for a reply or expiry before requesting another action in the same thread.`);
|
|
130
|
+
}
|
|
131
|
+
throw new Error(`Tool "${toolName}" requires confirmation for action ${quotedAction}. Call slack with action "confirm_action" in thread ${threadTs} using tool "${toolName}" and action ${quotedAction}, then wait for the user's approval first.`);
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
formatAction,
|
|
135
|
+
registerRequest,
|
|
136
|
+
consumeReply,
|
|
137
|
+
requireToolPolicy,
|
|
138
|
+
};
|
|
139
|
+
}
|