@sigrun/escalations 0.0.0-stage → 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/CHANGELOG.md +15 -0
- package/LICENSE +21 -0
- package/README.md +63 -2
- package/dist/dispatch.d.ts +14 -0
- package/dist/dispatch.js +165 -0
- package/dist/inbox.d.ts +31 -0
- package/dist/inbox.js +570 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +36 -0
- package/dist/json-file.d.ts +15 -0
- package/dist/json-file.js +78 -0
- package/dist/keys.d.ts +6 -0
- package/dist/keys.js +14 -0
- package/dist/lifecycle.d.ts +24 -0
- package/dist/lifecycle.js +159 -0
- package/dist/notify.d.ts +5 -0
- package/dist/notify.js +24 -0
- package/dist/section.d.ts +2 -0
- package/dist/section.js +5 -0
- package/dist/store.d.ts +22 -0
- package/dist/store.js +71 -0
- package/dist/sweep.d.ts +48 -0
- package/dist/sweep.js +173 -0
- package/dist/tools.d.ts +10 -0
- package/dist/tools.js +250 -0
- package/dist/transfer.d.ts +8 -0
- package/dist/transfer.js +41 -0
- package/dist/types.d.ts +66 -0
- package/dist/types.js +1 -0
- package/package.json +55 -5
package/dist/tools.js
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { Type } from "@earendil-works/pi-ai";
|
|
2
|
+
import { defineTool, } from "@earendil-works/pi-durable";
|
|
3
|
+
import { changedInbox } from "./keys.js";
|
|
4
|
+
import { TransitionError, claim, isSkip, raise, resolve } from "./lifecycle.js";
|
|
5
|
+
import { notifyRaised, notifyReopened } from "./notify.js";
|
|
6
|
+
import { bucketFor } from "./store.js";
|
|
7
|
+
const OptionSchema = Type.Object({
|
|
8
|
+
id: Type.String({
|
|
9
|
+
minLength: 1,
|
|
10
|
+
maxLength: 64,
|
|
11
|
+
pattern: "^[a-z0-9][a-z0-9_-]{0,63}$",
|
|
12
|
+
}),
|
|
13
|
+
label: Type.String({ minLength: 1, maxLength: 200 }),
|
|
14
|
+
detail: Type.Optional(Type.String({ maxLength: 2000 })),
|
|
15
|
+
});
|
|
16
|
+
const AnswerSchema = Type.Object({
|
|
17
|
+
option: Type.Optional(Type.String()),
|
|
18
|
+
instruction: Type.Optional(Type.String()),
|
|
19
|
+
by: Type.Optional(Type.String()),
|
|
20
|
+
at: Type.String(),
|
|
21
|
+
});
|
|
22
|
+
const ActSchema = Type.Object({
|
|
23
|
+
id: Type.String(),
|
|
24
|
+
key: Type.String(),
|
|
25
|
+
title: Type.String(),
|
|
26
|
+
body: Type.String(),
|
|
27
|
+
options: Type.Array(OptionSchema),
|
|
28
|
+
answer: AnswerSchema,
|
|
29
|
+
});
|
|
30
|
+
function textResult(text) {
|
|
31
|
+
return { output: [{ type: "text", text }] };
|
|
32
|
+
}
|
|
33
|
+
function errorResult(text) {
|
|
34
|
+
return { ...textResult(text), isError: true };
|
|
35
|
+
}
|
|
36
|
+
function transitionCode(error) {
|
|
37
|
+
return error instanceof TransitionError
|
|
38
|
+
? error.code
|
|
39
|
+
: error instanceof Error
|
|
40
|
+
? error.message
|
|
41
|
+
: String(error);
|
|
42
|
+
}
|
|
43
|
+
function answerText(e) {
|
|
44
|
+
const answer = e.answer;
|
|
45
|
+
if (answer === undefined)
|
|
46
|
+
return "none";
|
|
47
|
+
return [
|
|
48
|
+
answer.option === undefined ? undefined : `option: ${answer.option}`,
|
|
49
|
+
answer.instruction === undefined ? undefined : `instruction: ${answer.instruction}`,
|
|
50
|
+
]
|
|
51
|
+
.filter((part) => part !== undefined)
|
|
52
|
+
.join("; ");
|
|
53
|
+
}
|
|
54
|
+
function listText(skip, act) {
|
|
55
|
+
return [
|
|
56
|
+
"## Skip",
|
|
57
|
+
...(skip.length === 0 ? ["(none)"] : skip.map((key) => `- ${key}`)),
|
|
58
|
+
"## Act",
|
|
59
|
+
...(act.length === 0
|
|
60
|
+
? ["(none)"]
|
|
61
|
+
: act.flatMap((e) => [
|
|
62
|
+
`- ${e.id} — ${e.title} (${e.key})`,
|
|
63
|
+
` Body: ${e.body}`,
|
|
64
|
+
` Answer: ${answerText(e)}`,
|
|
65
|
+
])),
|
|
66
|
+
].join("\n");
|
|
67
|
+
}
|
|
68
|
+
/** Build the three host-identity-derived escalation tools over one store. */
|
|
69
|
+
export function createTools(deps) {
|
|
70
|
+
const now = deps.now ?? (() => new Date());
|
|
71
|
+
const caller = async (conversationId) => {
|
|
72
|
+
const meta = await deps.host.conversations.meta(conversationId);
|
|
73
|
+
return {
|
|
74
|
+
meta,
|
|
75
|
+
namespace: meta.jobId === undefined ? "user" : `job:${meta.jobId}`,
|
|
76
|
+
bucket: await bucketFor(deps.host, meta.owner),
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
const escalate = defineTool({
|
|
80
|
+
name: "escalate",
|
|
81
|
+
description: "Raise an asynchronous decision for the user. Use a stable issue key; an unresolved item with the same key is returned instead of duplicated.",
|
|
82
|
+
parameters: Type.Object({
|
|
83
|
+
key: Type.String({ minLength: 1, maxLength: 200 }),
|
|
84
|
+
title: Type.String({ minLength: 1, maxLength: 200 }),
|
|
85
|
+
body: Type.String({ minLength: 1, maxLength: 20_000 }),
|
|
86
|
+
options: Type.Array(OptionSchema, { minItems: 1, maxItems: 8 }),
|
|
87
|
+
allowInstruction: Type.Optional(Type.Boolean({ default: true })),
|
|
88
|
+
onAnswer: Type.Optional(Type.Union([
|
|
89
|
+
Type.Literal("next-run"),
|
|
90
|
+
Type.Literal("immediate"),
|
|
91
|
+
])),
|
|
92
|
+
}),
|
|
93
|
+
replay: "safe",
|
|
94
|
+
structuredOutputSchema: Type.Object({
|
|
95
|
+
id: Type.String(),
|
|
96
|
+
created: Type.Boolean(),
|
|
97
|
+
status: Type.String(),
|
|
98
|
+
}),
|
|
99
|
+
async execute(args, api) {
|
|
100
|
+
const { meta, namespace, bucket } = await caller(api.conversationId);
|
|
101
|
+
const onAnswer = args.onAnswer ?? (meta.jobId === undefined ? "immediate" : "next-run");
|
|
102
|
+
if (onAnswer === "next-run" && meta.jobId === undefined) {
|
|
103
|
+
return errorResult("next-run needs a scheduled job run; use immediate");
|
|
104
|
+
}
|
|
105
|
+
const owner = bucket.kind === "user" ? bucket.user : undefined;
|
|
106
|
+
const uniqueIds = new Set(args.options.map((option) => option.id));
|
|
107
|
+
if (uniqueIds.size !== args.options.length) {
|
|
108
|
+
return errorResult("escalate: option ids must be unique");
|
|
109
|
+
}
|
|
110
|
+
// The lookup and insertion share the store's one serialized callback.
|
|
111
|
+
const result = await deps.store.update(bucket, (items) => {
|
|
112
|
+
const existing = items.find((item) => item.namespace === namespace &&
|
|
113
|
+
item.key === args.key &&
|
|
114
|
+
item.status !== "resolved");
|
|
115
|
+
if (existing !== undefined) {
|
|
116
|
+
return { items, result: { item: existing, created: false } };
|
|
117
|
+
}
|
|
118
|
+
const item = raise({
|
|
119
|
+
...(owner === undefined ? {} : { owner }),
|
|
120
|
+
namespace,
|
|
121
|
+
key: args.key,
|
|
122
|
+
title: args.title,
|
|
123
|
+
body: args.body,
|
|
124
|
+
options: args.options,
|
|
125
|
+
allowInstruction: args.allowInstruction ?? true,
|
|
126
|
+
onAnswer,
|
|
127
|
+
source: {
|
|
128
|
+
...(meta.jobId === undefined ? {} : { jobId: meta.jobId }),
|
|
129
|
+
conversationId: api.conversationId,
|
|
130
|
+
},
|
|
131
|
+
}, now());
|
|
132
|
+
return { items: [...items, item], result: { item, created: true } };
|
|
133
|
+
});
|
|
134
|
+
if (result.created) {
|
|
135
|
+
await notifyRaised(deps.host, result.item);
|
|
136
|
+
changedInbox(deps.host, result.item.id);
|
|
137
|
+
}
|
|
138
|
+
const structuredOutput = {
|
|
139
|
+
id: result.item.id,
|
|
140
|
+
created: result.created,
|
|
141
|
+
status: result.item.status,
|
|
142
|
+
};
|
|
143
|
+
return {
|
|
144
|
+
output: [{
|
|
145
|
+
type: "text",
|
|
146
|
+
text: result.created
|
|
147
|
+
? `Escalation ${result.item.id} raised.`
|
|
148
|
+
: `Escalation ${result.item.id} already exists (status ${result.item.status}); not raised again.`,
|
|
149
|
+
}],
|
|
150
|
+
structuredOutput,
|
|
151
|
+
};
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
const escalationList = defineTool({
|
|
155
|
+
name: "escalation_list",
|
|
156
|
+
description: "List this job run's escalation keys to skip and answered next-run items to act on. Cron runs claim actionable items; other callers only see claims they already hold.",
|
|
157
|
+
parameters: Type.Object({}),
|
|
158
|
+
replay: "safe",
|
|
159
|
+
structuredOutputSchema: Type.Object({
|
|
160
|
+
skip: Type.Array(Type.String()),
|
|
161
|
+
act: Type.Array(ActSchema),
|
|
162
|
+
}),
|
|
163
|
+
async execute(_args, api) {
|
|
164
|
+
const { meta, namespace, bucket } = await caller(api.conversationId);
|
|
165
|
+
// Claiming and skip/act projection are one atomic check-then-write.
|
|
166
|
+
const result = await deps.store.update(bucket, (items) => {
|
|
167
|
+
let changed = false;
|
|
168
|
+
const next = items.map((item) => {
|
|
169
|
+
if (meta.source === "cron" &&
|
|
170
|
+
item.namespace === namespace &&
|
|
171
|
+
item.status === "answered" &&
|
|
172
|
+
item.onAnswer === "next-run") {
|
|
173
|
+
changed = true;
|
|
174
|
+
return claim(item, api.conversationId, now());
|
|
175
|
+
}
|
|
176
|
+
return item;
|
|
177
|
+
});
|
|
178
|
+
const visible = changed ? next : items;
|
|
179
|
+
const skip = visible
|
|
180
|
+
.filter((item) => item.namespace === namespace &&
|
|
181
|
+
isSkip(item) &&
|
|
182
|
+
item.claim?.conversationId !== api.conversationId)
|
|
183
|
+
.map((item) => item.key);
|
|
184
|
+
const act = visible.filter((item) => item.namespace === namespace &&
|
|
185
|
+
item.status === "claimed" &&
|
|
186
|
+
item.claim?.conversationId === api.conversationId);
|
|
187
|
+
return { items: visible, result: { skip, act } };
|
|
188
|
+
});
|
|
189
|
+
return {
|
|
190
|
+
output: [{ type: "text", text: listText(result.skip, result.act) }],
|
|
191
|
+
structuredOutput: {
|
|
192
|
+
skip: result.skip,
|
|
193
|
+
act: result.act.map((item) => ({
|
|
194
|
+
id: item.id,
|
|
195
|
+
key: item.key,
|
|
196
|
+
title: item.title,
|
|
197
|
+
body: item.body,
|
|
198
|
+
options: item.options.map((option) => ({ ...option })),
|
|
199
|
+
answer: { ...(item.answer ?? { at: item.updatedAt }) },
|
|
200
|
+
})),
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
const escalationResolve = defineTool({
|
|
206
|
+
name: "escalation_resolve",
|
|
207
|
+
description: "Resolve an escalation claimed by this conversation after carrying out its answer, or return it to open when the action failed.",
|
|
208
|
+
parameters: Type.Object({
|
|
209
|
+
id: Type.String({ minLength: 1 }),
|
|
210
|
+
outcome: Type.Union([Type.Literal("done"), Type.Literal("failed")]),
|
|
211
|
+
note: Type.String({ minLength: 1, maxLength: 4000 }),
|
|
212
|
+
}),
|
|
213
|
+
replay: "safe",
|
|
214
|
+
structuredOutputSchema: Type.Object({
|
|
215
|
+
id: Type.String(),
|
|
216
|
+
status: Type.String(),
|
|
217
|
+
}),
|
|
218
|
+
async execute(args, api) {
|
|
219
|
+
const { bucket } = await caller(api.conversationId);
|
|
220
|
+
try {
|
|
221
|
+
// Finding the id and checking its claimant precede the same write.
|
|
222
|
+
const item = await deps.store.update(bucket, (items) => {
|
|
223
|
+
const index = items.findIndex((candidate) => candidate.id === args.id);
|
|
224
|
+
if (index < 0)
|
|
225
|
+
throw new TransitionError("not_found");
|
|
226
|
+
const resolved = resolve(items[index], api.conversationId, args.outcome, args.note, now());
|
|
227
|
+
const next = [...items];
|
|
228
|
+
next[index] = resolved;
|
|
229
|
+
return { items: next, result: resolved };
|
|
230
|
+
});
|
|
231
|
+
if (args.outcome === "failed")
|
|
232
|
+
await notifyReopened(deps.host, item);
|
|
233
|
+
changedInbox(deps.host, item.id);
|
|
234
|
+
return {
|
|
235
|
+
output: [{
|
|
236
|
+
type: "text",
|
|
237
|
+
text: args.outcome === "done"
|
|
238
|
+
? `Escalation ${item.id} resolved.`
|
|
239
|
+
: `Escalation ${item.id} reopened (status ${item.status}).`,
|
|
240
|
+
}],
|
|
241
|
+
structuredOutput: { id: item.id, status: item.status },
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
return errorResult(`escalation_resolve: ${transitionCode(error)}`);
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
return [escalate, escalationList, escalationResolve];
|
|
250
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { EscalationStore } from "./store.js";
|
|
2
|
+
import type { Bucket } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Copy selected items to a new owner before removing them from the source.
|
|
5
|
+
* A crash between the updates leaves duplicates; the id check makes retrying
|
|
6
|
+
* the copy idempotent, so a later delivery can finish without losing items.
|
|
7
|
+
*/
|
|
8
|
+
export declare function transferItems(store: EscalationStore, source: Bucket, target: Bucket, ids: ReadonlySet<string>, now: Date): Promise<void>;
|
package/dist/transfer.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copy selected items to a new owner before removing them from the source.
|
|
3
|
+
* A crash between the updates leaves duplicates; the id check makes retrying
|
|
4
|
+
* the copy idempotent, so a later delivery can finish without losing items.
|
|
5
|
+
*/
|
|
6
|
+
export async function transferItems(store, source, target, ids, now) {
|
|
7
|
+
if (ids.size === 0)
|
|
8
|
+
return;
|
|
9
|
+
const sourceItems = (await store.list(source)).filter((item) => ids.has(item.id));
|
|
10
|
+
if (sourceItems.length === 0)
|
|
11
|
+
return;
|
|
12
|
+
const sourceOwner = source.kind === "user" ? source.user : "shared";
|
|
13
|
+
const targetOwner = target.kind === "user" ? target.user : undefined;
|
|
14
|
+
const idsInTarget = await store.update(target, (targetItems) => {
|
|
15
|
+
const held = new Set(targetItems.map((item) => item.id));
|
|
16
|
+
const additions = sourceItems
|
|
17
|
+
.filter((item) => !held.has(item.id))
|
|
18
|
+
.map((item) => ({
|
|
19
|
+
...item,
|
|
20
|
+
owner: targetOwner,
|
|
21
|
+
history: [
|
|
22
|
+
...item.history,
|
|
23
|
+
{
|
|
24
|
+
at: now.toISOString(),
|
|
25
|
+
event: "transferred",
|
|
26
|
+
note: `transferred from ${sourceOwner}`,
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
}));
|
|
30
|
+
return {
|
|
31
|
+
items: additions.length === 0
|
|
32
|
+
? targetItems
|
|
33
|
+
: [...targetItems, ...additions],
|
|
34
|
+
result: new Set([...held, ...additions.map((item) => item.id)]),
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
await store.update(source, (items) => ({
|
|
38
|
+
items: items.filter((item) => !idsInTarget.has(item.id)),
|
|
39
|
+
result: undefined,
|
|
40
|
+
}));
|
|
41
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export type EscalationStatus = "open" | "answered" | "claimed" | "resolved" | "dismissed" | "snoozed";
|
|
2
|
+
export type OnAnswer = "next-run" | "immediate";
|
|
3
|
+
export type ReopenReason = "snooze-ended" | "failed" | "interrupted" | "job-deleted";
|
|
4
|
+
export interface EscalationOption {
|
|
5
|
+
id: string;
|
|
6
|
+
label: string;
|
|
7
|
+
detail?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface EscalationAnswer {
|
|
10
|
+
option?: string;
|
|
11
|
+
instruction?: string;
|
|
12
|
+
by?: string;
|
|
13
|
+
at: string;
|
|
14
|
+
}
|
|
15
|
+
export interface HistoryEntry {
|
|
16
|
+
at: string;
|
|
17
|
+
event: "raised" | "answered" | "withdrawn" | "claimed" | "resolved" | "failed" | "interrupted" | "dismissed" | "restored" | "snoozed" | "snooze-ended" | "job-deleted" | "transferred";
|
|
18
|
+
note?: string;
|
|
19
|
+
answer?: EscalationAnswer;
|
|
20
|
+
}
|
|
21
|
+
export interface Escalation {
|
|
22
|
+
id: string;
|
|
23
|
+
owner?: string;
|
|
24
|
+
namespace: string;
|
|
25
|
+
key: string;
|
|
26
|
+
title: string;
|
|
27
|
+
body: string;
|
|
28
|
+
options: EscalationOption[];
|
|
29
|
+
allowInstruction: boolean;
|
|
30
|
+
onAnswer: OnAnswer;
|
|
31
|
+
source: {
|
|
32
|
+
jobId?: string;
|
|
33
|
+
conversationId?: number;
|
|
34
|
+
};
|
|
35
|
+
status: EscalationStatus;
|
|
36
|
+
answer?: EscalationAnswer;
|
|
37
|
+
claim?: {
|
|
38
|
+
conversationId: number;
|
|
39
|
+
at: string;
|
|
40
|
+
};
|
|
41
|
+
snoozedUntil?: string;
|
|
42
|
+
reopenReason?: ReopenReason;
|
|
43
|
+
orphaned?: boolean;
|
|
44
|
+
createdAt: string;
|
|
45
|
+
updatedAt: string;
|
|
46
|
+
resolvedAt?: string;
|
|
47
|
+
history: HistoryEntry[];
|
|
48
|
+
}
|
|
49
|
+
export type Bucket = {
|
|
50
|
+
kind: "user";
|
|
51
|
+
user: string;
|
|
52
|
+
} | {
|
|
53
|
+
kind: "shared";
|
|
54
|
+
};
|
|
55
|
+
export declare const SKIP_STATUSES: readonly EscalationStatus[];
|
|
56
|
+
export interface RaiseInput {
|
|
57
|
+
owner?: string;
|
|
58
|
+
namespace: string;
|
|
59
|
+
key: string;
|
|
60
|
+
title: string;
|
|
61
|
+
body: string;
|
|
62
|
+
options: EscalationOption[];
|
|
63
|
+
allowInstruction: boolean;
|
|
64
|
+
onAnswer: OnAnswer;
|
|
65
|
+
source: Escalation["source"];
|
|
66
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const SKIP_STATUSES = ["open", "answered", "claimed", "dismissed", "snoozed"];
|
package/package.json
CHANGED
|
@@ -1,6 +1,56 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"name": "@sigrun/escalations",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Escalation inbox extension for sigrun: async decisions for jobs and agents",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=22.19.0"
|
|
8
|
+
},
|
|
9
|
+
"main": "dist/index.js",
|
|
10
|
+
"types": "dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"CHANGELOG.md"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"test": "node scripts/check-manifest.mjs --expect-name @sigrun/escalations && vitest run",
|
|
23
|
+
"typecheck": "tsc --noEmit",
|
|
24
|
+
"build": "tsc -p tsconfig.build.json",
|
|
25
|
+
"prepublishOnly": "npm run build"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"@earendil-works/chord": "1.3.0 || 1.4.0",
|
|
29
|
+
"@earendil-works/pi-ai": "1.3.0 || 1.4.0",
|
|
30
|
+
"@earendil-works/pi-durable": "1.3.0 || 1.4.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@earendil-works/chord": "1.4.0",
|
|
34
|
+
"@earendil-works/pi-ai": "1.4.0",
|
|
35
|
+
"@earendil-works/pi-durable": "1.4.0",
|
|
36
|
+
"@sigrun/sigrun": "file:../sigrun",
|
|
37
|
+
"@sigrun/shared": "file:../sigrun/shared",
|
|
38
|
+
"@types/node": "^26.6.4",
|
|
39
|
+
"tsx": "^4.23.15",
|
|
40
|
+
"typescript": "^7.0.2",
|
|
41
|
+
"vitest": "^5.0.3"
|
|
42
|
+
},
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "github",
|
|
46
|
+
"url": "git+https://github.com/sigrun-ai/sigrun-escalations.git"
|
|
47
|
+
},
|
|
48
|
+
"bugs": {
|
|
49
|
+
"url": "https://github.com/sigrun-ai/sigrun-escalations/issues"
|
|
50
|
+
},
|
|
51
|
+
"homepage": "https://github.com/sigrun-ai/sigrun-escalations#readme",
|
|
52
|
+
"publishConfig": {
|
|
53
|
+
"access": "public",
|
|
54
|
+
"provenance": true
|
|
55
|
+
}
|
|
56
|
+
}
|