@agent-native/core 0.0.0-beta-20260819222946 → 0.0.0-beta-20260819225910
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/corpus/README.md +1 -1
- package/corpus/templates/analytics/netlify.toml +4 -0
- package/corpus/templates/clips/actions/request-transcript.ts +10 -6
- package/corpus/templates/factory/AGENTS.md +17 -22
- package/corpus/templates/factory/actions/get-slack-feedback-context.ts +8 -28
- package/corpus/templates/factory/actions/govern-agent-native-pull-request.ts +67 -59
- package/corpus/templates/factory/actions/list-triage-items.ts +6 -1
- package/corpus/templates/factory/actions/reconcile-triage-run.ts +7 -1
- package/corpus/templates/factory/actions/start-builder-for-item.ts +68 -16
- package/corpus/templates/factory/server/connectors/slack.ts +62 -0
- package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +31 -18
- package/corpus/templates/factory/server/triage/ai-services-git.ts +2 -0
- package/corpus/templates/factory/server/triage/pr-monitor.ts +2 -1
- package/corpus/templates/factory/server/triage/pr-policy.ts +197 -19
- package/corpus/templates/factory/server/triage/review-skill-alignment.ts +127 -0
- package/corpus/templates/factory/server/triage/slack-client.ts +79 -6
- package/dist/client/MultiTabAssistantChat.js +28 -1
- package/dist/client/use-chat-threads.d.ts +1 -0
- package/dist/client/use-chat-threads.js +13 -3
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +5 -5
- package/dist/progress/routes.d.ts +1 -1
- package/dist/server/auth.js +2 -2
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +1 -1
package/corpus/README.md
CHANGED
|
@@ -22,6 +22,10 @@ AGENT_CHAT_DURABLE_BACKGROUND = "true"
|
|
|
22
22
|
AGENT_NATIVE_HOSTED_HARNESS = "true"
|
|
23
23
|
AGENT_NATIVE_ENABLE_KEEP_WARM = "1"
|
|
24
24
|
AGENT_NATIVE_DISABLE_KEEP_WARM_BACKGROUND = "1"
|
|
25
|
+
# Keep the public shell close to the current deploy. A stale shell can retain
|
|
26
|
+
# hashed modulepreload URLs after a Netlify asset publish and leave the app on
|
|
27
|
+
# its loading screen while those chunks return 404.
|
|
28
|
+
AGENT_NATIVE_SSR_CACHE = "30s"
|
|
25
29
|
# Analytics owns its schema at release time (`migrate:production` above), so
|
|
26
30
|
# production request functions skip migrations. Preview functions still run
|
|
27
31
|
# request-path migrations because previews do not run the production release
|
|
@@ -1526,18 +1526,22 @@ const requestTranscriptAction = defineAction({
|
|
|
1526
1526
|
transcribeWithBuilderModelFallback({
|
|
1527
1527
|
audioBytes: audioMedia.audioBytes,
|
|
1528
1528
|
mimeType: audioMedia.mimeType,
|
|
1529
|
-
diarize:
|
|
1529
|
+
diarize: true,
|
|
1530
1530
|
instructions: SPEECH_ONLY_TRANSCRIPTION_INSTRUCTIONS,
|
|
1531
1531
|
timeoutMs: builderTranscriptionTimeoutMs(rec.durationMs),
|
|
1532
1532
|
}),
|
|
1533
1533
|
);
|
|
1534
1534
|
|
|
1535
1535
|
const segments = (builderResult.segments ?? [])
|
|
1536
|
-
.map((s) =>
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1536
|
+
.map((s) => {
|
|
1537
|
+
const speaker = s.speakerLabel?.trim();
|
|
1538
|
+
return {
|
|
1539
|
+
startMs: s.startMs,
|
|
1540
|
+
endMs: s.endMs,
|
|
1541
|
+
text: s.text.trim(),
|
|
1542
|
+
...(speaker ? { speaker } : {}),
|
|
1543
|
+
};
|
|
1544
|
+
})
|
|
1541
1545
|
.filter((segment) => segment.text);
|
|
1542
1546
|
const normalizedTranscript = normalizeProviderTranscript(
|
|
1543
1547
|
builderResult.text,
|
|
@@ -24,18 +24,19 @@ decisions, feedback, agent runs, and provider audit records.
|
|
|
24
24
|
- Use the generic Slack adapter: clear-bug automations add 👀 and tag
|
|
25
25
|
`@builder.io`, ask Builder to run `/address-feedback`, and group repeated
|
|
26
26
|
reports into one Builder thread; GitHub/Sentry clear bugs use the Builder run
|
|
27
|
-
API.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
27
|
+
API. Follow `review-latest-feedback` for full-thread evidence, existing
|
|
28
|
+
reaction and owner checks, answered-clarification priority, one-question
|
|
29
|
+
clarification limits, durable rechecks, and the required `@agent-native`
|
|
30
|
+
disposition after every actionable 👀. Clips, Design, and Content stay
|
|
31
|
+
owner-managed for feedback dispatch.
|
|
32
|
+
- PR governance follows `review-prs`: verify BuilderIO membership and complete
|
|
33
|
+
diff/review/check evidence; skip drafts/current approvals and never approve
|
|
34
|
+
external authors. Keep ultra-scary security, auth, tenant-isolation,
|
|
35
|
+
secrets, data-loss, execution, payment, and deployment risks manual.
|
|
36
|
+
Verified members may pass ordinary uncertain checks or review feedback, but
|
|
37
|
+
record exact states and never call them clean. Apply the Alice/Content,
|
|
38
|
+
Nick/Slides, Enzo/Factory, Sid/Design, and docs-only exceptions only after
|
|
39
|
+
membership and the ultra-scary gate. Never auto-merge.
|
|
39
40
|
- Graph edits create immutable blueprint versions. AI proposes with `source=ai`;
|
|
40
41
|
a person reviews and publishes through the same action surface.
|
|
41
42
|
- Provider credentials belong to Dispatch/shared workspace integrations, never
|
|
@@ -80,17 +81,11 @@ decisions, feedback, agent runs, and provider audit records.
|
|
|
80
81
|
| `import-agent` / `import-agent-pack` / `list-agent-pack` | Import profiles or agent packs. |
|
|
81
82
|
| `start-workspace-app-creation` | Promote an agent and its pack into an app handoff. |
|
|
82
83
|
|
|
83
|
-
Rules start in shadow mode; hard guards
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
automations are seeded. Use the visual editor for graph changes and agent chat
|
|
84
|
+
Rules start in shadow mode; hard guards apply. Organization automations use
|
|
85
|
+
stored prompts; external mutations require durable, idempotent runs and
|
|
86
|
+
provider confirmation. Use the visual editor for graph changes and agent chat
|
|
87
87
|
for proposals; persist complete graphs with `save-factory-graph`. Change rules
|
|
88
|
-
through triage
|
|
89
|
-
|
|
90
|
-
The Slack, GitHub, and Sentry pollers are bounded ingestion adapters for the
|
|
91
|
-
legacy default triage queue. They do not define the complete tool surface.
|
|
92
|
-
Interactive and scheduled agents may discover additional connected provider or
|
|
93
|
-
MCP tools through the shared workspace context.
|
|
88
|
+
through triage actions, never graph JSON.
|
|
94
89
|
|
|
95
90
|
## Source Changes
|
|
96
91
|
|
|
@@ -2,7 +2,6 @@ import { defineAction } from "@agent-native/core/action";
|
|
|
2
2
|
import { and, eq } from "drizzle-orm";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
|
|
5
|
-
import type { SlackMessage } from "../server/connectors/slack.js";
|
|
6
5
|
import { getDb } from "../server/db/index.js";
|
|
7
6
|
import { triageConfig, triageItems } from "../server/db/schema.js";
|
|
8
7
|
import {
|
|
@@ -12,8 +11,6 @@ import {
|
|
|
12
11
|
import { recordFactoryAudit } from "../server/triage/audit.js";
|
|
13
12
|
import { createSlackReader } from "../server/triage/slack-client.js";
|
|
14
13
|
|
|
15
|
-
const MAX_THREAD_PAGES = 5;
|
|
16
|
-
|
|
17
14
|
export default defineAction({
|
|
18
15
|
description:
|
|
19
16
|
"Read the bounded full Slack thread for one Factory feedback item. Use this before deciding whether a report is a clear bug; unreadable or truncated context is not a green light.",
|
|
@@ -46,31 +43,11 @@ export default defineAction({
|
|
|
46
43
|
const workspace =
|
|
47
44
|
config?.slackWorkspace === "secondary" ? "secondary" : "primary";
|
|
48
45
|
const slack = createSlackReader({ ownerEmail: userEmail, orgId });
|
|
49
|
-
const messages
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
workspace,
|
|
55
|
-
item.channelId,
|
|
56
|
-
item.threadTs,
|
|
57
|
-
100,
|
|
58
|
-
cursor,
|
|
59
|
-
);
|
|
60
|
-
messages.push(...result.messages);
|
|
61
|
-
if (result.has_more && !result.next_cursor) {
|
|
62
|
-
throw new Error(
|
|
63
|
-
"Slack thread pagination is incomplete because the provider omitted its next cursor.",
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
if (!result.has_more) {
|
|
67
|
-
hasMore = false;
|
|
68
|
-
break;
|
|
69
|
-
}
|
|
70
|
-
hasMore = page === MAX_THREAD_PAGES - 1;
|
|
71
|
-
if (hasMore) break;
|
|
72
|
-
cursor = result.next_cursor;
|
|
73
|
-
}
|
|
46
|
+
const { messages, hasMore } = await slack.getCompleteThread(
|
|
47
|
+
workspace,
|
|
48
|
+
item.channelId,
|
|
49
|
+
item.threadTs,
|
|
50
|
+
);
|
|
74
51
|
|
|
75
52
|
await recordFactoryAudit(
|
|
76
53
|
context,
|
|
@@ -100,10 +77,13 @@ export default defineAction({
|
|
|
100
77
|
coverage: hasMore ? "partial" : "complete",
|
|
101
78
|
messages: messages.map((message) => ({
|
|
102
79
|
user: message.user ?? message.username ?? message.bot_id ?? null,
|
|
80
|
+
username: message.username ?? null,
|
|
81
|
+
botId: message.bot_id ?? null,
|
|
103
82
|
text: message.text,
|
|
104
83
|
ts: message.ts,
|
|
105
84
|
threadTs: message.thread_ts ?? item.threadTs,
|
|
106
85
|
replyCount: message.reply_count ?? 0,
|
|
86
|
+
reactions: message.reactions ?? [],
|
|
107
87
|
})),
|
|
108
88
|
};
|
|
109
89
|
},
|
|
@@ -24,7 +24,10 @@ import {
|
|
|
24
24
|
serializeTriageMetadata,
|
|
25
25
|
} from "../server/triage/metadata.js";
|
|
26
26
|
import { reconcileBabysitState } from "../server/triage/pr-babysit.js";
|
|
27
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
decidePullRequestGovernance,
|
|
29
|
+
hasCurrentPullRequestApproval,
|
|
30
|
+
} from "../server/triage/pr-policy.js";
|
|
28
31
|
|
|
29
32
|
function repositoryRef(value: string): { owner: string; repo: string } {
|
|
30
33
|
const match = /^([^/\s]+)\/([^/\s]+)$/.exec(value.trim());
|
|
@@ -103,7 +106,7 @@ async function hasVerifiedFactoryRun(input: {
|
|
|
103
106
|
|
|
104
107
|
export default defineAction({
|
|
105
108
|
description:
|
|
106
|
-
"Govern one agent-native pull request after fetching bounded GitHub and ai-services evidence. Auto-approve only
|
|
109
|
+
"Govern one agent-native pull request after fetching bounded GitHub and ai-services evidence. Auto-approve only under the current review-prs membership, owner, evidence, and ultra-scary gates. Never auto-merge. Clips, Design, and Content feedback remains owner-managed while their verified PR-owner exceptions still apply.",
|
|
107
110
|
schema: z.object({
|
|
108
111
|
repo: z.string().trim().min(1).max(256),
|
|
109
112
|
pullRequestNumber: z.number().int().positive(),
|
|
@@ -167,6 +170,24 @@ export default defineAction({
|
|
|
167
170
|
repository,
|
|
168
171
|
pullRequestNumber,
|
|
169
172
|
);
|
|
173
|
+
if (pullRequest.state !== "open" || pullRequest.draft) {
|
|
174
|
+
if (itemId) {
|
|
175
|
+
await getDb()
|
|
176
|
+
.update(triageItems)
|
|
177
|
+
.set({
|
|
178
|
+
status: "pr_observed",
|
|
179
|
+
updatedAt: new Date().toISOString(),
|
|
180
|
+
})
|
|
181
|
+
.where(and(eq(triageItems.id, itemId), eq(triageItems.orgId, orgId)));
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
ok: true,
|
|
185
|
+
action: "skipped",
|
|
186
|
+
reason: pullRequest.draft
|
|
187
|
+
? "Pull request is a draft and is excluded before evidence review."
|
|
188
|
+
: "Pull request is not open and is excluded before evidence review.",
|
|
189
|
+
};
|
|
190
|
+
}
|
|
170
191
|
const privateKey = await resolveConnectorSecret(
|
|
171
192
|
"BUILDER_PRIVATE_KEY",
|
|
172
193
|
userEmail,
|
|
@@ -186,6 +207,38 @@ export default defineAction({
|
|
|
186
207
|
`PR evidence is stale: GitHub reports ${pullRequest.headSha}, ai-services reports ${snapshot.headSha}.`,
|
|
187
208
|
);
|
|
188
209
|
}
|
|
210
|
+
if (hasCurrentPullRequestApproval(snapshot.reviews)) {
|
|
211
|
+
await recordFactoryAudit(
|
|
212
|
+
context,
|
|
213
|
+
{ userEmail, orgId },
|
|
214
|
+
{
|
|
215
|
+
action: "govern-agent-native-pull-request",
|
|
216
|
+
kind: "governance",
|
|
217
|
+
status: "skipped",
|
|
218
|
+
itemId: itemId ?? null,
|
|
219
|
+
source: "github",
|
|
220
|
+
sourceUrl: pullRequest.htmlUrl,
|
|
221
|
+
summary:
|
|
222
|
+
"Skipped the pull request because it already has a current non-dismissed approval.",
|
|
223
|
+
details: { repo, pullRequestNumber },
|
|
224
|
+
},
|
|
225
|
+
);
|
|
226
|
+
if (itemId) {
|
|
227
|
+
await getDb()
|
|
228
|
+
.update(triageItems)
|
|
229
|
+
.set({
|
|
230
|
+
status: "pr_observed",
|
|
231
|
+
updatedAt: new Date().toISOString(),
|
|
232
|
+
})
|
|
233
|
+
.where(and(eq(triageItems.id, itemId), eq(triageItems.orgId, orgId)));
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
ok: true,
|
|
237
|
+
action: "skipped",
|
|
238
|
+
reason:
|
|
239
|
+
"Pull request already has a current non-dismissed approval; no duplicate review was created.",
|
|
240
|
+
};
|
|
241
|
+
}
|
|
189
242
|
|
|
190
243
|
const checksPassed =
|
|
191
244
|
snapshot.coverage === "complete" &&
|
|
@@ -254,6 +307,7 @@ export default defineAction({
|
|
|
254
307
|
reviewFeedbackHandled,
|
|
255
308
|
autoApprove: governance.autoApprove,
|
|
256
309
|
autoMerge: governance.autoMerge,
|
|
310
|
+
ownerException: governance.ownerException,
|
|
257
311
|
ownerOwnedArea: governance.ownerOwnedArea ?? null,
|
|
258
312
|
guardResults: governance.guardResults,
|
|
259
313
|
},
|
|
@@ -341,7 +395,9 @@ export default defineAction({
|
|
|
341
395
|
const approval = await github.approvePullRequest(
|
|
342
396
|
repository,
|
|
343
397
|
pullRequestNumber,
|
|
344
|
-
|
|
398
|
+
governance.ownerException
|
|
399
|
+
? `Factory auto-approved under the verified ${governance.ownerException} owner exception; ordinary check and review states remain recorded.`
|
|
400
|
+
: "Factory auto-approved under verified BuilderIO membership; ordinary check and review states remain recorded.",
|
|
345
401
|
);
|
|
346
402
|
approvalUrl = approval.htmlUrl;
|
|
347
403
|
metadata.autoApprovedAt = new Date().toISOString();
|
|
@@ -361,61 +417,13 @@ export default defineAction({
|
|
|
361
417
|
await github.approvePullRequest(
|
|
362
418
|
repository,
|
|
363
419
|
pullRequestNumber,
|
|
364
|
-
|
|
420
|
+
governance.ownerException
|
|
421
|
+
? `Factory auto-approved under the verified ${governance.ownerException} owner exception; ordinary check and review states remain recorded.`
|
|
422
|
+
: "Factory auto-approved under verified BuilderIO membership; ordinary check and review states remain recorded.",
|
|
365
423
|
)
|
|
366
424
|
).htmlUrl;
|
|
367
425
|
}
|
|
368
426
|
|
|
369
|
-
if (!governance.autoMerge) {
|
|
370
|
-
await recordFactoryAudit(
|
|
371
|
-
context,
|
|
372
|
-
{ userEmail, orgId },
|
|
373
|
-
{
|
|
374
|
-
action: "govern-agent-native-pull-request",
|
|
375
|
-
kind: "external_action",
|
|
376
|
-
itemId: itemId ?? null,
|
|
377
|
-
source: "github",
|
|
378
|
-
sourceUrl: approvalUrl,
|
|
379
|
-
summary: "Approved the pull request after governance checks passed.",
|
|
380
|
-
details: { repo, pullRequestNumber, approvalUrl },
|
|
381
|
-
},
|
|
382
|
-
);
|
|
383
|
-
return {
|
|
384
|
-
ok: true,
|
|
385
|
-
action: "approved",
|
|
386
|
-
approvalUrl,
|
|
387
|
-
checksPassed,
|
|
388
|
-
reviewFeedbackHandled,
|
|
389
|
-
};
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
const merge = await github.mergePullRequest(
|
|
393
|
-
repository,
|
|
394
|
-
pullRequestNumber,
|
|
395
|
-
`Merge Factory bug fix #${pullRequestNumber}`,
|
|
396
|
-
snapshot.headSha,
|
|
397
|
-
);
|
|
398
|
-
if (itemId) {
|
|
399
|
-
const item = (
|
|
400
|
-
await getDb()
|
|
401
|
-
.select({ metadataJson: triageItems.metadataJson })
|
|
402
|
-
.from(triageItems)
|
|
403
|
-
.where(and(eq(triageItems.id, itemId), eq(triageItems.orgId, orgId)))
|
|
404
|
-
.limit(1)
|
|
405
|
-
)[0];
|
|
406
|
-
if (!item) throw new Error("Factory item disappeared after PR merge.");
|
|
407
|
-
const metadata = parseTriageMetadata(item.metadataJson);
|
|
408
|
-
metadata.autoMergedAt = new Date().toISOString();
|
|
409
|
-
metadata.mergeSha = merge.sha;
|
|
410
|
-
await getDb()
|
|
411
|
-
.update(triageItems)
|
|
412
|
-
.set({
|
|
413
|
-
metadataJson: serializeTriageMetadata(metadata),
|
|
414
|
-
status: "merged",
|
|
415
|
-
updatedAt: new Date().toISOString(),
|
|
416
|
-
})
|
|
417
|
-
.where(and(eq(triageItems.id, itemId), eq(triageItems.orgId, orgId)));
|
|
418
|
-
}
|
|
419
427
|
await recordFactoryAudit(
|
|
420
428
|
context,
|
|
421
429
|
{ userEmail, orgId },
|
|
@@ -424,16 +432,16 @@ export default defineAction({
|
|
|
424
432
|
kind: "external_action",
|
|
425
433
|
itemId: itemId ?? null,
|
|
426
434
|
source: "github",
|
|
427
|
-
sourceUrl:
|
|
428
|
-
summary:
|
|
429
|
-
|
|
435
|
+
sourceUrl: approvalUrl,
|
|
436
|
+
summary:
|
|
437
|
+
"Approved the pull request under the current review-prs policy.",
|
|
438
|
+
details: { repo, pullRequestNumber, approvalUrl },
|
|
430
439
|
},
|
|
431
440
|
);
|
|
432
441
|
return {
|
|
433
442
|
ok: true,
|
|
434
|
-
action: "
|
|
443
|
+
action: "approved",
|
|
435
444
|
approvalUrl,
|
|
436
|
-
mergeSha: merge.sha,
|
|
437
445
|
checksPassed,
|
|
438
446
|
reviewFeedbackHandled,
|
|
439
447
|
};
|
|
@@ -30,7 +30,12 @@ export default defineAction({
|
|
|
30
30
|
workspaceMemberIdentityFromContext(context),
|
|
31
31
|
);
|
|
32
32
|
const db = getDb();
|
|
33
|
-
const reviewStatuses =
|
|
33
|
+
const reviewStatuses =
|
|
34
|
+
source === "github"
|
|
35
|
+
? ["pr_observed"]
|
|
36
|
+
: source === "slack"
|
|
37
|
+
? ["received", "automation_started", "evidence_ready"]
|
|
38
|
+
: ["received"];
|
|
34
39
|
const items = await db
|
|
35
40
|
.select()
|
|
36
41
|
.from(triageItems)
|
|
@@ -20,7 +20,13 @@ import {
|
|
|
20
20
|
|
|
21
21
|
const reviewSchema = z.object({
|
|
22
22
|
author: z.string(),
|
|
23
|
-
state: z.enum([
|
|
23
|
+
state: z.enum([
|
|
24
|
+
"approved",
|
|
25
|
+
"changes_requested",
|
|
26
|
+
"commented",
|
|
27
|
+
"pending",
|
|
28
|
+
"dismissed",
|
|
29
|
+
]),
|
|
24
30
|
observedAt: z.string().datetime(),
|
|
25
31
|
});
|
|
26
32
|
const checkSchema = z.object({
|
|
@@ -26,7 +26,7 @@ import { detectOwnerOwnedArea } from "../server/triage/pr-policy.js";
|
|
|
26
26
|
import { createSlackReader } from "../server/triage/slack-client.js";
|
|
27
27
|
|
|
28
28
|
const replyTextPrefix =
|
|
29
|
-
"@builder.io please run /address-feedback in the repo to address this feedback. Read the address-feedback, address-feedback-with-replies, review-latest-feedback, and review-prs skills as relevant, inspect the full thread and linked evidence, and fix the owning boundary. Please send a PR when ready.";
|
|
29
|
+
"@builder.io please run /address-feedback in the repo to address this feedback. Read the address-feedback, address-feedback-with-replies, review-latest-feedback, and review-prs skills as relevant, inspect the full thread and linked evidence, and fix the owning boundary. Please send a PR when ready, then have the @agent-native bot post a concise Fixed, In progress, or Clarification needed disposition in this same thread; an 👀 reaction or this handoff alone is not completion.";
|
|
30
30
|
const legacyReplyTextPrefix = "@builderio please fix this in a reply.";
|
|
31
31
|
|
|
32
32
|
type RelatedFeedbackItem = {
|
|
@@ -474,30 +474,53 @@ export default defineAction({
|
|
|
474
474
|
const workspace =
|
|
475
475
|
config?.slackWorkspace === "secondary" ? "secondary" : "primary";
|
|
476
476
|
const slack = createSlackReader({ ownerEmail: userEmail, orgId });
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
477
|
+
const thread = await slack.getCompleteThread(
|
|
478
|
+
workspace,
|
|
479
|
+
item.channelId,
|
|
480
|
+
item.threadTs,
|
|
481
|
+
);
|
|
482
|
+
const completeThreads = new Map([[item.id, thread]]);
|
|
483
|
+
if (thread.hasMore) {
|
|
484
|
+
throw new Error(
|
|
485
|
+
"Slack thread is truncated; refusing to dispatch without complete evidence.",
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
for (const related of relatedItems) {
|
|
489
|
+
if (!related.channelId || !related.threadTs) {
|
|
490
|
+
throw new Error(
|
|
491
|
+
"Grouped Slack feedback is missing a channel or thread identity.",
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
const relatedThread = await slack.getCompleteThread(
|
|
495
|
+
workspace,
|
|
496
|
+
related.channelId,
|
|
497
|
+
related.threadTs,
|
|
498
|
+
);
|
|
499
|
+
completeThreads.set(related.id, relatedThread);
|
|
500
|
+
if (relatedThread.hasMore) {
|
|
501
|
+
throw new Error(
|
|
502
|
+
"A grouped Slack thread is truncated; refusing to dispatch without complete evidence.",
|
|
503
|
+
);
|
|
484
504
|
}
|
|
485
|
-
|
|
505
|
+
}
|
|
506
|
+
for (const feedbackItem of [item, ...relatedItems]) {
|
|
507
|
+
const reactionState = await slack.getEyesReaction(
|
|
486
508
|
workspace,
|
|
487
509
|
feedbackItem.channelId!,
|
|
488
510
|
feedbackItem.threadTs!,
|
|
489
511
|
);
|
|
512
|
+
const reaction = reactionState.eyesPresent
|
|
513
|
+
? { added: false, already_present: true }
|
|
514
|
+
: await slack.addEyesReaction(
|
|
515
|
+
workspace,
|
|
516
|
+
feedbackItem.channelId!,
|
|
517
|
+
feedbackItem.threadTs!,
|
|
518
|
+
);
|
|
490
519
|
await writeMetadata(feedbackItem.id, orgId, {
|
|
491
520
|
slackEyesReactedAt: new Date().toISOString(),
|
|
492
521
|
slackEyesReactionAlreadyPresent: reaction.already_present,
|
|
493
522
|
});
|
|
494
523
|
}
|
|
495
|
-
const thread = await slack.getThread(
|
|
496
|
-
workspace,
|
|
497
|
-
item.channelId,
|
|
498
|
-
item.threadTs,
|
|
499
|
-
100,
|
|
500
|
-
);
|
|
501
524
|
const hasCurrentBuilderReply = thread.messages.some((message) =>
|
|
502
525
|
message.text.includes(replyTextPrefix),
|
|
503
526
|
);
|
|
@@ -510,7 +533,7 @@ export default defineAction({
|
|
|
510
533
|
),
|
|
511
534
|
);
|
|
512
535
|
if (
|
|
513
|
-
thread.
|
|
536
|
+
thread.hasMore &&
|
|
514
537
|
!hasCurrentBuilderReply &&
|
|
515
538
|
!hasLegacyBuilderReply
|
|
516
539
|
) {
|
|
@@ -534,6 +557,35 @@ export default defineAction({
|
|
|
534
557
|
slackBuilderReplyTs: posted.ts,
|
|
535
558
|
});
|
|
536
559
|
}
|
|
560
|
+
const agentNative = await slack.getAgentNativeIdentity(workspace);
|
|
561
|
+
for (const feedbackItem of [item, ...relatedItems]) {
|
|
562
|
+
const feedbackThread = completeThreads.get(feedbackItem.id);
|
|
563
|
+
if (!feedbackThread) {
|
|
564
|
+
throw new Error(
|
|
565
|
+
`Slack thread evidence is missing for Factory item ${feedbackItem.id}.`,
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
const hasAgentNativeDisposition = feedbackThread.messages.some(
|
|
569
|
+
(message) =>
|
|
570
|
+
(message.user === agentNative.userId ||
|
|
571
|
+
message.username?.trim().toLowerCase() === "agent-native") &&
|
|
572
|
+
/^(Fixed|In progress|Clarification needed):/i.test(
|
|
573
|
+
message.text.trim(),
|
|
574
|
+
),
|
|
575
|
+
);
|
|
576
|
+
if (hasAgentNativeDisposition) continue;
|
|
577
|
+
const disposition = await slack.postThreadReply(
|
|
578
|
+
workspace,
|
|
579
|
+
feedbackItem.channelId!,
|
|
580
|
+
feedbackItem.threadTs!,
|
|
581
|
+
`Thanks - Builder owns this feedback cluster and will follow up after verification. In progress: ${reason}`,
|
|
582
|
+
);
|
|
583
|
+
await writeMetadata(feedbackItem.id, orgId, {
|
|
584
|
+
slackDispositionAt: new Date().toISOString(),
|
|
585
|
+
slackDispositionTs: disposition.ts,
|
|
586
|
+
slackDisposition: "In progress",
|
|
587
|
+
});
|
|
588
|
+
}
|
|
537
589
|
const clusterItemIds = [itemId, ...relatedItems.map(({ id }) => id)];
|
|
538
590
|
const clusterMetadata = {
|
|
539
591
|
feedbackClusterRepresentativeId: itemId,
|
|
@@ -14,6 +14,14 @@ export interface SlackMessage {
|
|
|
14
14
|
thread_ts?: string;
|
|
15
15
|
reply_count?: number;
|
|
16
16
|
permalink?: string;
|
|
17
|
+
reactions?: Array<{ name: string; count?: number; users?: string[] }>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface SlackAuthTestResult {
|
|
21
|
+
userId: string;
|
|
22
|
+
userName: string;
|
|
23
|
+
teamId: string;
|
|
24
|
+
teamName: string;
|
|
17
25
|
}
|
|
18
26
|
|
|
19
27
|
export interface SlackTeamInfo {
|
|
@@ -45,6 +53,10 @@ export interface SlackReactionResult {
|
|
|
45
53
|
already_present: boolean;
|
|
46
54
|
}
|
|
47
55
|
|
|
56
|
+
export interface SlackReactionState {
|
|
57
|
+
eyesPresent: boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
48
60
|
export interface SlackPostMessageResult {
|
|
49
61
|
ok?: boolean;
|
|
50
62
|
error?: string;
|
|
@@ -219,6 +231,56 @@ export async function getThread(
|
|
|
219
231
|
};
|
|
220
232
|
}
|
|
221
233
|
|
|
234
|
+
export async function authTest(
|
|
235
|
+
workspace: Workspace,
|
|
236
|
+
tokenResolver?: SlackTokenResolver,
|
|
237
|
+
): Promise<SlackAuthTestResult> {
|
|
238
|
+
const data = await slackApi<{
|
|
239
|
+
user_id?: string;
|
|
240
|
+
user?: string;
|
|
241
|
+
team_id?: string;
|
|
242
|
+
team?: string;
|
|
243
|
+
}>(workspace, "auth.test", undefined, tokenResolver);
|
|
244
|
+
if (
|
|
245
|
+
typeof data.user_id !== "string" ||
|
|
246
|
+
typeof data.user !== "string" ||
|
|
247
|
+
typeof data.team_id !== "string" ||
|
|
248
|
+
typeof data.team !== "string"
|
|
249
|
+
) {
|
|
250
|
+
throw new Error("Slack auth.test response is missing bot identity.");
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
userId: data.user_id,
|
|
254
|
+
userName: data.user,
|
|
255
|
+
teamId: data.team_id,
|
|
256
|
+
teamName: data.team,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export async function getEyesReaction(
|
|
261
|
+
workspace: Workspace,
|
|
262
|
+
channelId: string,
|
|
263
|
+
timestamp: string,
|
|
264
|
+
tokenResolver?: SlackTokenResolver,
|
|
265
|
+
): Promise<SlackReactionState> {
|
|
266
|
+
const data = await slackApi<{
|
|
267
|
+
message?: { reactions?: Array<{ name?: string; count?: number }> };
|
|
268
|
+
}>(
|
|
269
|
+
workspace,
|
|
270
|
+
"reactions.get",
|
|
271
|
+
{ channel: channelId, timestamp },
|
|
272
|
+
tokenResolver,
|
|
273
|
+
);
|
|
274
|
+
if (!data.message) {
|
|
275
|
+
throw new Error("Slack reaction response is missing the message.");
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
eyesPresent: (data.message.reactions ?? []).some(
|
|
279
|
+
(reaction) => reaction.name === "eyes" && (reaction.count ?? 0) > 0,
|
|
280
|
+
),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
222
284
|
export async function addEyesReaction(
|
|
223
285
|
workspace: Workspace,
|
|
224
286
|
channelId: string,
|