@celsoft/coordinator 0.0.0 → 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 +11 -0
- package/README.md +107 -0
- package/THIRD_PARTY_NOTICES.md +2005 -0
- package/agents/console.md +20 -0
- package/agents/free.md +110 -0
- package/build/0-JP6XL6LB.js +17 -0
- package/build/1-A52YZWNS.js +17 -0
- package/build/2-CLH2NM6F.js +32 -0
- package/build/3-4N467IB6.js +45 -0
- package/build/4-WL5H4LUW.js +29 -0
- package/build/_layout.svelte-KPUSX7LQ.js +27 -0
- package/build/_page.svelte-22H56UR6.js +7972 -0
- package/build/_page.svelte-CVWUNRQC.js +135 -0
- package/build/_page.svelte-LTZXBHMT.js +57 -0
- package/build/_server.ts-3KVBYN6P.js +20 -0
- package/build/_server.ts-3POPKVEM.js +47 -0
- package/build/_server.ts-5735T2V5.js +31 -0
- package/build/_server.ts-5VXGNWV2.js +38 -0
- package/build/_server.ts-645KP7WP.js +42 -0
- package/build/_server.ts-A3AA24Z2.js +28 -0
- package/build/_server.ts-CITUOEM6.js +58 -0
- package/build/_server.ts-I7HMTPOA.js +25 -0
- package/build/_server.ts-M5BQ4LLX.js +30 -0
- package/build/_server.ts-OB4O27HF.js +683 -0
- package/build/_server.ts-OJT7HE7M.js +15 -0
- package/build/_server.ts-OYGWVLZM.js +12 -0
- package/build/_server.ts-T75UVMQP.js +38 -0
- package/build/_server.ts-WYZYNHUZ.js +28 -0
- package/build/_server.ts-Z26EB47B.js +486 -0
- package/build/chunk-4CIG53L5.js +33 -0
- package/build/chunk-BJTJS3XY.js +338 -0
- package/build/chunk-F2CNWJZM.js +14 -0
- package/build/chunk-HGYE3UTU.js +416 -0
- package/build/chunk-KAYW2W7H.js +1311 -0
- package/build/chunk-LFP3UPCM.js +10 -0
- package/build/chunk-M44JEFEH.js +62 -0
- package/build/chunk-MC5AGLMY.js +39 -0
- package/build/chunk-NCZ77J5E.js +19798 -0
- package/build/chunk-NVNLDPUJ.js +2122 -0
- package/build/chunk-PDCON4KS.js +494 -0
- package/build/chunk-Q4OQNY5M.js +25 -0
- package/build/chunk-QCUOR2U5.js +75 -0
- package/build/chunk-R6RYZZWA.js +46 -0
- package/build/chunk-SOJTASZC.js +51 -0
- package/build/chunk-SU26PJVX.js +410 -0
- package/build/chunk-U5BSBG4R.js +506 -0
- package/build/chunk-UEKOZGRO.js +37 -0
- package/build/chunk-W6EFUWJ2.js +9 -0
- package/build/chunk-WOPREK6Q.js +117 -0
- package/build/chunk-WWJ3F4QS.js +127 -0
- package/build/chunk-ZGZJEEE3.js +1091 -0
- package/build/client/_app/immutable/assets/0.BGa5ZNDM.css +1 -0
- package/build/client/_app/immutable/chunks/BUrrLYDt.js +2 -0
- package/build/client/_app/immutable/chunks/BgFr_EyZ.js +4 -0
- package/build/client/_app/immutable/chunks/Bjy-W4x2.js +82 -0
- package/build/client/_app/immutable/chunks/C8qZab0y.js +56 -0
- package/build/client/_app/immutable/chunks/C90fLsA5.js +4 -0
- package/build/client/_app/immutable/chunks/CRzLMJPS.js +1 -0
- package/build/client/_app/immutable/chunks/DLT1iIlz.js +2 -0
- package/build/client/_app/immutable/chunks/pIrVVOVY.js +2 -0
- package/build/client/_app/immutable/entry/app.0DERehLW.js +3 -0
- package/build/client/_app/immutable/entry/payload.DSmR2FwN.js +2 -0
- package/build/client/_app/immutable/entry/start.B7srFiqh.js +3 -0
- package/build/client/_app/immutable/nodes/0.aD9vJoxR.js +2 -0
- package/build/client/_app/immutable/nodes/1.Br4FL91i.js +2 -0
- package/build/client/_app/immutable/nodes/2.vl6OYfu3.js +8 -0
- package/build/client/_app/immutable/nodes/3.VYqJOLAF.js +2 -0
- package/build/client/_app/immutable/nodes/4.jF9UVlFN.js +2 -0
- package/build/client/_app/version.json +1 -0
- package/build/client/apple-touch-icon.png +0 -0
- package/build/client/favicon.ico +0 -0
- package/build/client/favicon.svg +48 -0
- package/build/client/icon-192.png +0 -0
- package/build/client/icon-512.png +0 -0
- package/build/client/icon-maskable-512.png +0 -0
- package/build/client/icon.svg +44 -0
- package/build/client/manifest.webmanifest +15 -0
- package/build/error.svelte-JXZDY65F.js +51 -0
- package/build/handler.js +716 -0
- package/build/hooks.server-34IMHRKC.js +43 -0
- package/build/instance-NP3FJIWW.js +2967 -0
- package/client/client.mjs +28 -0
- package/client/hooks.mjs +7 -0
- package/client/register.mjs +5 -0
- package/dist/cli.js +62826 -0
- package/dist/port.js +36691 -0
- package/package.json +20 -1
- package/skills/agents/.claude-plugin/plugin.json +5 -0
- package/skills/agents/hooks/hooks.json +1 -0
- package/skills/agents/hooks/register.ts +57 -0
- package/skills/agents/skills/bus-work/SKILL.md +66 -0
- package/skills/agents/skills/plan/SKILL.md +98 -0
- package/skills/console/.claude-plugin/plugin.json +5 -0
- package/skills/console/hooks/hooks.json +1 -0
- package/skills/console/hooks/register.ts +10 -0
- package/skills/console/skills/bus/SKILL.md +61 -0
- package/skills/default/.claude-plugin/plugin.json +5 -0
- package/skills/default/skills/git-workflow/SKILL.md +294 -0
- package/skills/default/skills/git-workflow/mise-tasks/_lib.sh +107 -0
- package/skills/default/skills/git-workflow/mise-tasks/clone +36 -0
- package/skills/default/skills/git-workflow/mise-tasks/repo-list +8 -0
- package/skills/default/skills/git-workflow/mise-tasks/unpushed +26 -0
- package/skills/default/skills/git-workflow/mise-tasks/worktree-add +60 -0
- package/skills/default/skills/github/SKILL.md +208 -0
- package/skills/default/skills/github/scripts/gh-org +196 -0
- package/skills/default/skills/plan-page/SKILL.md +300 -0
- package/skills/default/skills/plan-page/assets/index.html +854 -0
- package/skills/default/skills/plan-page/scripts/inline +46 -0
- package/skills/default/skills/plan-page/scripts/stage +26 -0
- package/skills/default/skills/secrets/SKILL.md +134 -0
- package/skills/default/skills/secrets/scripts/secrets-check +87 -0
- package/skills/default/skills/writing-style/SKILL.md +45 -0
- package/skills/extra/.claude-plugin/plugin.json +5 -0
- package/skills/extra/skills/artifacts/SKILL.md +52 -0
- package/skills/extra/skills/github-actions/SKILL.md +78 -0
- package/skills/extra/skills/github-actions/scan +32 -0
- package/skills/extra/skills/markdown-review/SKILL.md +70 -0
- package/skills/extra/skills/markdown-review/assets/index.html +201 -0
- package/skills/extra/skills/markdown-review/scripts/stage +36 -0
- package/skills/extra/skills/mise-usage/SKILL.md +155 -0
- package/skills/extra/skills/plugin-authoring/SKILL.md +317 -0
- package/skills/extra/skills/project-conventions/SKILL.md +64 -0
- package/skills/extra/skills/questions/SKILL.md +156 -0
- package/skills/extra/skills/questions/assets/index.html +376 -0
- package/skills/extra/skills/questions/scripts/stage +26 -0
- package/skills/extra/skills/serve/SKILL.md +165 -0
- package/skills/extra/skills/serve/scripts/serve +268 -0
- package/skills/extra/skills/serve/scripts/server.py +345 -0
- package/skills/extra/skills/skill-dependencies/SKILL.md +65 -0
- package/skills/extra/skills/skill-maintenance/SKILL.md +101 -0
- package/skills/extra/skills/ticketing/SKILL.md +125 -0
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
import { createRequire as __cr } from 'node:module'; const require = __cr(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
Branch,
|
|
4
|
+
LandingMethod,
|
|
5
|
+
RepoSettingsSchema,
|
|
6
|
+
Slot,
|
|
7
|
+
Track
|
|
8
|
+
} from "./chunk-HGYE3UTU.js";
|
|
9
|
+
import {
|
|
10
|
+
ISSUE_TYPES,
|
|
11
|
+
IssueAddress,
|
|
12
|
+
Repo,
|
|
13
|
+
Slug,
|
|
14
|
+
external_exports
|
|
15
|
+
} from "./chunk-NCZ77J5E.js";
|
|
16
|
+
|
|
17
|
+
// build/server/chunks/log.js
|
|
18
|
+
import { appendFileSync, existsSync, readFileSync, renameSync, statSync } from "node:fs";
|
|
19
|
+
import { dirname, join } from "node:path";
|
|
20
|
+
import { fileURLToPath } from "node:url";
|
|
21
|
+
function packageRoot(from) {
|
|
22
|
+
for (let dir = from; ; dir = dirname(dir)) {
|
|
23
|
+
const file = join(dir, "package.json");
|
|
24
|
+
if (existsSync(file) && PACKAGE_NAMES.includes(JSON.parse(readFileSync(file, "utf8")).name)) return dir;
|
|
25
|
+
if (dirname(dir) === dir) throw new Error(`no coordinator package.json above ${from}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
var PACKAGE_NAMES = ["@celsoft/coordinator", "coordinator"];
|
|
29
|
+
var ROOT = packageRoot(dirname(fileURLToPath(import.meta.url)));
|
|
30
|
+
join(ROOT, "agents");
|
|
31
|
+
join(ROOT, "client");
|
|
32
|
+
join(ROOT, "skills");
|
|
33
|
+
function now() {
|
|
34
|
+
return (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "+00:00");
|
|
35
|
+
}
|
|
36
|
+
var USER = "user";
|
|
37
|
+
var CONSOLE = "console";
|
|
38
|
+
function slotName(repo, track) {
|
|
39
|
+
return `${repo}@${track}`;
|
|
40
|
+
}
|
|
41
|
+
var SessionState = external_exports.enum([
|
|
42
|
+
"starting",
|
|
43
|
+
"idle",
|
|
44
|
+
"working",
|
|
45
|
+
"blocked",
|
|
46
|
+
"done",
|
|
47
|
+
"unknown",
|
|
48
|
+
"gone",
|
|
49
|
+
"stopped",
|
|
50
|
+
"failed",
|
|
51
|
+
"paused"
|
|
52
|
+
]);
|
|
53
|
+
var Leash = external_exports.enum(["gated", "free"]);
|
|
54
|
+
var AskKind = external_exports.enum([
|
|
55
|
+
"plan",
|
|
56
|
+
"done",
|
|
57
|
+
"pr",
|
|
58
|
+
"question"
|
|
59
|
+
]);
|
|
60
|
+
external_exports.object({
|
|
61
|
+
id: external_exports.string(),
|
|
62
|
+
track: Track.default("session"),
|
|
63
|
+
type: external_exports.string().nullable().default(null),
|
|
64
|
+
/** A profile it was started with on top of its repo's, by an action or a start naming one; kept when it is brought back up. */
|
|
65
|
+
profile: external_exports.string().nullable().default(null),
|
|
66
|
+
parent: external_exports.string().nullable().default(null),
|
|
67
|
+
task: external_exports.string().nullable().default(null),
|
|
68
|
+
repo: external_exports.string().nullable().default(null),
|
|
69
|
+
cwd: external_exports.string().nullable().default(null),
|
|
70
|
+
pane: external_exports.string().nullable().default(null),
|
|
71
|
+
workspace: external_exports.string().nullable().default(null),
|
|
72
|
+
token: external_exports.string().nullable().default(null),
|
|
73
|
+
state: SessionState.default("starting"),
|
|
74
|
+
/** Which part of its work the slot is in, from the Stage registry (stages.ts); carried to the slot's next agent. */
|
|
75
|
+
stage: external_exports.string().default("planning"),
|
|
76
|
+
/** The stage's description: the agent's own words on what it is doing. */
|
|
77
|
+
status: external_exports.string().nullable().default(null),
|
|
78
|
+
status_at: external_exports.string().nullable().default(null),
|
|
79
|
+
/** When Herdr last saw it come to rest (idle or done), or null while it works. */
|
|
80
|
+
idle_since: external_exports.string().nullable().default(null),
|
|
81
|
+
/** How many turns Herdr has seen it finish since it started, for Reminders. */
|
|
82
|
+
turns: external_exports.number().int().default(0),
|
|
83
|
+
started: external_exports.string().default(() => now()),
|
|
84
|
+
ended: external_exports.string().nullable().default(null),
|
|
85
|
+
adopted: external_exports.boolean().default(false),
|
|
86
|
+
/** The page has offered to clean it up once it ended, so it never offers again. */
|
|
87
|
+
cleanup_offered: external_exports.boolean().default(false),
|
|
88
|
+
leash: Leash.default("gated"),
|
|
89
|
+
/** The slot's own Properties, set by anyone with slot_property, shown under its fixed ones. */
|
|
90
|
+
properties: external_exports.array(external_exports.object({
|
|
91
|
+
name: external_exports.string(),
|
|
92
|
+
value: external_exports.string(),
|
|
93
|
+
link: external_exports.string().nullable().default(null),
|
|
94
|
+
by: external_exports.string(),
|
|
95
|
+
at: external_exports.string()
|
|
96
|
+
})).default([]),
|
|
97
|
+
/** The slot's Badges, set by anyone with slot_badge, shown beside its state and stage. */
|
|
98
|
+
badges: external_exports.array(external_exports.object({
|
|
99
|
+
name: external_exports.string(),
|
|
100
|
+
label: external_exports.string(),
|
|
101
|
+
tone: external_exports.enum([
|
|
102
|
+
"neutral",
|
|
103
|
+
"info",
|
|
104
|
+
"success",
|
|
105
|
+
"warning",
|
|
106
|
+
"danger"
|
|
107
|
+
]).catch("neutral"),
|
|
108
|
+
by: external_exports.string(),
|
|
109
|
+
at: external_exports.string()
|
|
110
|
+
})).default([]),
|
|
111
|
+
/** After its work lands the agent stays up, instead of the bus ending it. */
|
|
112
|
+
keep_open: external_exports.boolean().default(false),
|
|
113
|
+
/** When it last reported its work landed; the bus ends it once it comes to rest, unless keep_open. */
|
|
114
|
+
landed_at: external_exports.string().nullable().default(null),
|
|
115
|
+
/** The slot the user gave control of this one: its bus messages count as theirs. */
|
|
116
|
+
lead: external_exports.string().nullable().default(null),
|
|
117
|
+
/** Its last landing, held back while its followers work: the issue closes and the agent ends once they are all done. */
|
|
118
|
+
finishing: external_exports.object({
|
|
119
|
+
link: external_exports.string().nullable(),
|
|
120
|
+
note: external_exports.string().nullable(),
|
|
121
|
+
about: external_exports.string().nullable(),
|
|
122
|
+
commit: external_exports.string().nullable(),
|
|
123
|
+
also: external_exports.array(external_exports.string()).default([])
|
|
124
|
+
}).nullable().default(null)
|
|
125
|
+
});
|
|
126
|
+
function slotOf(s) {
|
|
127
|
+
return s.repo ? slotName(s.repo, s.track) : null;
|
|
128
|
+
}
|
|
129
|
+
function latestPerSlot(sessions) {
|
|
130
|
+
const slots = /* @__PURE__ */ new Map();
|
|
131
|
+
for (const s of sessions) {
|
|
132
|
+
const slot = slotOf(s);
|
|
133
|
+
if (!slot) continue;
|
|
134
|
+
const other = slots.get(slot);
|
|
135
|
+
const later = (a, b) => Number(isLive(a)) - Number(isLive(b)) || cmp(a.started, b.started);
|
|
136
|
+
if (!other || later(s, other) > 0) slots.set(slot, s);
|
|
137
|
+
}
|
|
138
|
+
return [...slots.values()];
|
|
139
|
+
}
|
|
140
|
+
function followersOf(sessions, lead) {
|
|
141
|
+
return latestPerSlot(sessions).filter((s) => s.lead === lead).map((s) => slotOf(s)).sort();
|
|
142
|
+
}
|
|
143
|
+
function unfinishedFollowers(sessions, lead) {
|
|
144
|
+
const latest = new Map(latestPerSlot(sessions).map((s) => [slotOf(s), s]));
|
|
145
|
+
return followersOf(sessions, lead).filter((f) => latest.get(f)?.stage !== "landed");
|
|
146
|
+
}
|
|
147
|
+
function isLive(s) {
|
|
148
|
+
return s.ended === null;
|
|
149
|
+
}
|
|
150
|
+
function openAsk(messages, slot) {
|
|
151
|
+
for (let i = messages.length - 1; i >= 0; i--) if (messages[i].from === slot) return messages[i].type === "ask" ? messages[i] : null;
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
external_exports.object({
|
|
155
|
+
id: external_exports.number().int(),
|
|
156
|
+
at: external_exports.string().default(() => now()),
|
|
157
|
+
from: external_exports.string(),
|
|
158
|
+
to: external_exports.string(),
|
|
159
|
+
type: external_exports.string(),
|
|
160
|
+
body: external_exports.record(external_exports.string(), external_exports.any()),
|
|
161
|
+
delivered: external_exports.string().nullable().default(null),
|
|
162
|
+
started: external_exports.string().nullable().default(null),
|
|
163
|
+
/** For a Message from the user: the door it came through, the page, the CLI, or a Landing Script run. */
|
|
164
|
+
via: external_exports.string().nullable().default(null)
|
|
165
|
+
});
|
|
166
|
+
function issueAddress(repo, slug) {
|
|
167
|
+
return `${repo}#${slug}`;
|
|
168
|
+
}
|
|
169
|
+
var IssueState = external_exports.enum([
|
|
170
|
+
"open",
|
|
171
|
+
"held",
|
|
172
|
+
"closed"
|
|
173
|
+
]);
|
|
174
|
+
var IssueType = external_exports.enum(ISSUE_TYPES);
|
|
175
|
+
var IssueMove = external_exports.enum([
|
|
176
|
+
"top",
|
|
177
|
+
"up",
|
|
178
|
+
"down"
|
|
179
|
+
]);
|
|
180
|
+
var NoteSchema = external_exports.object({
|
|
181
|
+
by: external_exports.string(),
|
|
182
|
+
at: external_exports.string().default(() => now()),
|
|
183
|
+
text: external_exports.string()
|
|
184
|
+
});
|
|
185
|
+
var ThrottleLevel = external_exports.enum([
|
|
186
|
+
"ok",
|
|
187
|
+
"soft",
|
|
188
|
+
"hard"
|
|
189
|
+
]);
|
|
190
|
+
var HeldSchema = external_exports.object({
|
|
191
|
+
id: external_exports.number().int(),
|
|
192
|
+
kind: external_exports.enum(["start", "bring_up"]),
|
|
193
|
+
sender: external_exports.string(),
|
|
194
|
+
body: external_exports.record(external_exports.string(), external_exports.any()),
|
|
195
|
+
at: external_exports.string().default(() => now())
|
|
196
|
+
});
|
|
197
|
+
external_exports.object({
|
|
198
|
+
level: ThrottleLevel.default("ok"),
|
|
199
|
+
held: external_exports.array(HeldSchema).default([]),
|
|
200
|
+
next_held: external_exports.number().int().default(1),
|
|
201
|
+
pausing: external_exports.object({
|
|
202
|
+
slot: external_exports.string(),
|
|
203
|
+
session: external_exports.string(),
|
|
204
|
+
asked: external_exports.string()
|
|
205
|
+
}).nullable().default(null),
|
|
206
|
+
last: external_exports.string().nullable().default(null),
|
|
207
|
+
told: external_exports.array(external_exports.string()).default([])
|
|
208
|
+
});
|
|
209
|
+
var PlanSchema = external_exports.object({
|
|
210
|
+
name: external_exports.string().default("Plan"),
|
|
211
|
+
link: external_exports.string(),
|
|
212
|
+
by: external_exports.string(),
|
|
213
|
+
at: external_exports.string().default(() => now()),
|
|
214
|
+
approved_at: external_exports.string().nullable().default(null),
|
|
215
|
+
/** The version of the document it links to that the user approved, when it links to one. */
|
|
216
|
+
approved_version: external_exports.number().int().nullable().default(null)
|
|
217
|
+
});
|
|
218
|
+
var RequestSchema = external_exports.object({
|
|
219
|
+
by: external_exports.string(),
|
|
220
|
+
reason: external_exports.string(),
|
|
221
|
+
at: external_exports.string().default(() => now())
|
|
222
|
+
});
|
|
223
|
+
external_exports.object({
|
|
224
|
+
repo: Repo,
|
|
225
|
+
slug: Slug,
|
|
226
|
+
title: external_exports.string(),
|
|
227
|
+
type: IssueType.default("task"),
|
|
228
|
+
description: external_exports.string().default(""),
|
|
229
|
+
state: IssueState.default("open"),
|
|
230
|
+
/** When it last entered its state; held and closed issues list by it, newest first. */
|
|
231
|
+
state_at: external_exports.string().nullable().default(null),
|
|
232
|
+
opened_by: external_exports.string(),
|
|
233
|
+
opened_at: external_exports.string().default(() => now()),
|
|
234
|
+
rank: external_exports.number().int().default(0),
|
|
235
|
+
holder: Slot.nullable().default(null),
|
|
236
|
+
request: RequestSchema.nullable().default(null),
|
|
237
|
+
comments: external_exports.array(NoteSchema).default([]),
|
|
238
|
+
/** Its plans, oldest first. */
|
|
239
|
+
plans: external_exports.array(PlanSchema).default([]),
|
|
240
|
+
/** The single plan an issue kept before it had many, carried into plans on load. */
|
|
241
|
+
plan: PlanSchema.nullable().optional(),
|
|
242
|
+
closed_by: external_exports.string().nullable().default(null),
|
|
243
|
+
closed_at: external_exports.string().nullable().default(null),
|
|
244
|
+
closing_note: external_exports.string().nullable().default(null),
|
|
245
|
+
/** Closed through a landing (land with end, or landed), the only close that frees the issues waiting on it. */
|
|
246
|
+
landed: external_exports.boolean().default(false),
|
|
247
|
+
/** The issues it depends on, in any repo: it is blocked while any of them has not landed. */
|
|
248
|
+
blocked_by: external_exports.array(IssueAddress).default([]),
|
|
249
|
+
/** Repos whose slots on its holder's track follow the holder, brought up when it is started or passed; the user's word alone. */
|
|
250
|
+
followers: external_exports.array(Repo).default([]),
|
|
251
|
+
/** Links to the issue's work outside the bus, oldest first, such as its ticket. */
|
|
252
|
+
links: external_exports.array(external_exports.object({
|
|
253
|
+
label: external_exports.string(),
|
|
254
|
+
url: external_exports.string(),
|
|
255
|
+
by: external_exports.string(),
|
|
256
|
+
at: external_exports.string()
|
|
257
|
+
})).default([])
|
|
258
|
+
}).transform(({ plan, ...i }) => {
|
|
259
|
+
const state = i.state === "closed" ? "closed" : i.holder ? "held" : "open";
|
|
260
|
+
return {
|
|
261
|
+
...i,
|
|
262
|
+
plans: i.plans.length || !plan ? i.plans : [plan],
|
|
263
|
+
state,
|
|
264
|
+
holder: state === "closed" ? null : i.holder,
|
|
265
|
+
landed: state === "closed" && i.landed,
|
|
266
|
+
state_at: i.state_at ?? i.closed_at ?? i.opened_at
|
|
267
|
+
};
|
|
268
|
+
});
|
|
269
|
+
var IssueGroupSchema = external_exports.object({
|
|
270
|
+
issues: external_exports.array(Slug).min(2),
|
|
271
|
+
why: external_exports.string()
|
|
272
|
+
});
|
|
273
|
+
external_exports.object({
|
|
274
|
+
at: external_exports.string(),
|
|
275
|
+
weighed: external_exports.array(Slug),
|
|
276
|
+
groups: external_exports.array(IssueGroupSchema)
|
|
277
|
+
});
|
|
278
|
+
external_exports.object({
|
|
279
|
+
repo: Repo,
|
|
280
|
+
path: external_exports.string(),
|
|
281
|
+
default_branch: Branch,
|
|
282
|
+
added: external_exports.string().default(() => now()),
|
|
283
|
+
added_by: external_exports.string(),
|
|
284
|
+
/** Found by the one-time import of the clones under the development folder, rather than added. */
|
|
285
|
+
imported: external_exports.boolean().default(false),
|
|
286
|
+
/** Its settings, each a Field Schema in repoSettings.ts, set by the user with repo_settings. */
|
|
287
|
+
settings: RepoSettingsSchema.default(() => RepoSettingsSchema.parse({}))
|
|
288
|
+
});
|
|
289
|
+
external_exports.object({
|
|
290
|
+
login: external_exports.string(),
|
|
291
|
+
/** The private key's path, anchored when stored. */
|
|
292
|
+
ssh_key: external_exports.string(),
|
|
293
|
+
name: external_exports.string(),
|
|
294
|
+
email: external_exports.string(),
|
|
295
|
+
orgs: external_exports.array(external_exports.string()).default([]),
|
|
296
|
+
added: external_exports.string().default(() => now()),
|
|
297
|
+
/** Found by the one-time import from the github skill's global gh-org keys, rather than added. */
|
|
298
|
+
imported: external_exports.boolean().default(false)
|
|
299
|
+
});
|
|
300
|
+
var LandingState = external_exports.enum([
|
|
301
|
+
"landing",
|
|
302
|
+
"pr_open",
|
|
303
|
+
"landed",
|
|
304
|
+
"refused",
|
|
305
|
+
"failed",
|
|
306
|
+
"closed",
|
|
307
|
+
"dry_run"
|
|
308
|
+
]);
|
|
309
|
+
external_exports.object({
|
|
310
|
+
slot: external_exports.string(),
|
|
311
|
+
repo: external_exports.string(),
|
|
312
|
+
branch: external_exports.string(),
|
|
313
|
+
/** The slot's last landing: it closes the issue and ends the agent once it rests. */
|
|
314
|
+
end: external_exports.boolean().default(false),
|
|
315
|
+
method: LandingMethod.nullable().default(null),
|
|
316
|
+
/** The commit checked, and the default branch's commit it sat on. */
|
|
317
|
+
commit: external_exports.string().nullable().default(null),
|
|
318
|
+
base: external_exports.string().nullable().default(null),
|
|
319
|
+
commits: external_exports.array(external_exports.string()).default([]),
|
|
320
|
+
title: external_exports.string().default(""),
|
|
321
|
+
body: external_exports.string().default(""),
|
|
322
|
+
/** Its Change Summary as the agent's latest land wrote it: one line, what was built, and what was hard. */
|
|
323
|
+
summary: external_exports.string().default(""),
|
|
324
|
+
built: external_exports.string().default(""),
|
|
325
|
+
challenges: external_exports.string().default(""),
|
|
326
|
+
/** The bus asked the slot to rebase (behind, moved, or conflicting), and whether a land since changed the summary. */
|
|
327
|
+
rebase: external_exports.boolean().default(false),
|
|
328
|
+
revised: external_exports.boolean().default(false),
|
|
329
|
+
/** Once landed, the id of its Change Summary in the repo's history. */
|
|
330
|
+
change: external_exports.number().int().nullable().default(null),
|
|
331
|
+
about: external_exports.string().nullable().default(null),
|
|
332
|
+
/** Other issues the slot holds that this landing finishes, closed with about. */
|
|
333
|
+
also: external_exports.array(external_exports.string()).default([]),
|
|
334
|
+
state: LandingState.default("landing"),
|
|
335
|
+
reason: external_exports.string().nullable().default(null),
|
|
336
|
+
link: external_exports.string().nullable().default(null),
|
|
337
|
+
pr: external_exports.number().int().nullable().default(null),
|
|
338
|
+
account: external_exports.string().nullable().default(null),
|
|
339
|
+
/** What the PR watcher has already told the slot: review and comment ids, the checks' last state, and mergeability. */
|
|
340
|
+
seen: external_exports.array(external_exports.string()).default([]),
|
|
341
|
+
checks: external_exports.string().nullable().default(null),
|
|
342
|
+
mergeable: external_exports.string().nullable().default(null),
|
|
343
|
+
/** When the PR last changed on GitHub, and when the watcher last asked. */
|
|
344
|
+
touched_at: external_exports.string().nullable().default(null),
|
|
345
|
+
polled_at: external_exports.string().nullable().default(null),
|
|
346
|
+
/** During landing's last run: when, how it ended, and its output's last line. */
|
|
347
|
+
during: external_exports.object({
|
|
348
|
+
at: external_exports.string(),
|
|
349
|
+
ok: external_exports.boolean(),
|
|
350
|
+
note: external_exports.string()
|
|
351
|
+
}).nullable().default(null),
|
|
352
|
+
at: external_exports.string().default(() => now()),
|
|
353
|
+
updated_at: external_exports.string().default(() => now())
|
|
354
|
+
});
|
|
355
|
+
external_exports.object({
|
|
356
|
+
id: external_exports.number().int(),
|
|
357
|
+
repo: external_exports.string(),
|
|
358
|
+
slot: external_exports.string(),
|
|
359
|
+
title: external_exports.string(),
|
|
360
|
+
summary: external_exports.string(),
|
|
361
|
+
built: external_exports.string(),
|
|
362
|
+
challenges: external_exports.string(),
|
|
363
|
+
about: external_exports.string().nullable(),
|
|
364
|
+
method: LandingMethod,
|
|
365
|
+
link: external_exports.string(),
|
|
366
|
+
pr: external_exports.number().int().nullable(),
|
|
367
|
+
commit: external_exports.string().nullable(),
|
|
368
|
+
commits: external_exports.number().int(),
|
|
369
|
+
revised: external_exports.boolean(),
|
|
370
|
+
at: external_exports.string()
|
|
371
|
+
});
|
|
372
|
+
var WorkPrState = external_exports.enum([
|
|
373
|
+
"open",
|
|
374
|
+
"closed",
|
|
375
|
+
"dry_run"
|
|
376
|
+
]);
|
|
377
|
+
var FollowerRequestState = external_exports.enum([
|
|
378
|
+
"pending",
|
|
379
|
+
"approved",
|
|
380
|
+
"declined"
|
|
381
|
+
]);
|
|
382
|
+
external_exports.object({
|
|
383
|
+
id: external_exports.number().int(),
|
|
384
|
+
lead: Slot,
|
|
385
|
+
repo: Repo,
|
|
386
|
+
type: external_exports.string(),
|
|
387
|
+
task: external_exports.string(),
|
|
388
|
+
existing: external_exports.boolean().default(false),
|
|
389
|
+
state: FollowerRequestState.default("pending"),
|
|
390
|
+
note: external_exports.string().nullable().default(null),
|
|
391
|
+
at: external_exports.string().default(() => now()),
|
|
392
|
+
answered_at: external_exports.string().nullable().default(null)
|
|
393
|
+
});
|
|
394
|
+
external_exports.object({
|
|
395
|
+
slot: external_exports.string(),
|
|
396
|
+
repo: external_exports.string(),
|
|
397
|
+
head: external_exports.string(),
|
|
398
|
+
base: external_exports.string(),
|
|
399
|
+
title: external_exports.string(),
|
|
400
|
+
about: external_exports.string().nullable().default(null),
|
|
401
|
+
number: external_exports.number().int().nullable().default(null),
|
|
402
|
+
link: external_exports.string().nullable().default(null),
|
|
403
|
+
account: external_exports.string().nullable().default(null),
|
|
404
|
+
state: WorkPrState.default("open"),
|
|
405
|
+
reason: external_exports.string().nullable().default(null),
|
|
406
|
+
at: external_exports.string().default(() => now()),
|
|
407
|
+
updated_at: external_exports.string().default(() => now())
|
|
408
|
+
});
|
|
409
|
+
function addressOfIssue(i) {
|
|
410
|
+
return issueAddress(i.repo, i.slug);
|
|
411
|
+
}
|
|
412
|
+
var STATE_ORDER = {
|
|
413
|
+
open: 0,
|
|
414
|
+
held: 1,
|
|
415
|
+
closed: 2
|
|
416
|
+
};
|
|
417
|
+
function issueOrder(a, b) {
|
|
418
|
+
return cmp(a.repo.toLowerCase(), b.repo.toLowerCase()) || STATE_ORDER[a.state] - STATE_ORDER[b.state] || (a.state === "open" ? a.rank - b.rank || cmp(a.opened_at, b.opened_at) : cmp(b.state_at, a.state_at));
|
|
419
|
+
}
|
|
420
|
+
var unclosed = (i) => i.state !== "closed";
|
|
421
|
+
var waitingOn = (i, find) => i.blocked_by.filter((b) => !find(b)?.landed);
|
|
422
|
+
function cmp(a, b) {
|
|
423
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
424
|
+
}
|
|
425
|
+
var Refused = class extends Error {
|
|
426
|
+
refusal = true;
|
|
427
|
+
};
|
|
428
|
+
function isRefusal(e) {
|
|
429
|
+
return e instanceof Error && e.refusal === true;
|
|
430
|
+
}
|
|
431
|
+
function invalid(prefix, error) {
|
|
432
|
+
return new Refused(prefix + error.issues.map((i) => `${i.path.join(".") || "body"}: ${i.message}`).join("; "));
|
|
433
|
+
}
|
|
434
|
+
var MAX_BYTES = 2e6;
|
|
435
|
+
var KEY = Symbol.for("coordinator.log");
|
|
436
|
+
var holder = globalThis;
|
|
437
|
+
function stamp() {
|
|
438
|
+
const d = /* @__PURE__ */ new Date();
|
|
439
|
+
const pad = (n, w = 2) => String(n).padStart(w, "0");
|
|
440
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())},${pad(d.getMilliseconds(), 3)}`;
|
|
441
|
+
}
|
|
442
|
+
function rotate(path) {
|
|
443
|
+
if (!existsSync(path) || statSync(path).size < MAX_BYTES) return;
|
|
444
|
+
for (let n = 2; n >= 1; n--) if (existsSync(`${path}.${n}`)) renameSync(`${path}.${n}`, `${path}.${n + 1}`);
|
|
445
|
+
renameSync(path, `${path}.1`);
|
|
446
|
+
}
|
|
447
|
+
function write(level, name, text) {
|
|
448
|
+
const line = `${stamp()} ${level} coordinator.${name}: ${text}`;
|
|
449
|
+
console.error(line);
|
|
450
|
+
const file = holder[KEY];
|
|
451
|
+
if (file) {
|
|
452
|
+
rotate(file);
|
|
453
|
+
appendFileSync(file, line + "\n", { mode: 384 });
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
function format$1(text, args) {
|
|
457
|
+
let i = 0;
|
|
458
|
+
return text.replace(/%s/g, () => String(args[i++]));
|
|
459
|
+
}
|
|
460
|
+
function get(name) {
|
|
461
|
+
return {
|
|
462
|
+
info: (text, ...args) => write("INFO", name, format$1(text, args)),
|
|
463
|
+
warning: (text, ...args) => write("WARNING", name, format$1(text, args)),
|
|
464
|
+
exception: (error, text, ...args) => write("ERROR", name, `${format$1(text, args)}
|
|
465
|
+
${error instanceof Error ? error.stack : String(error)}`)
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export {
|
|
470
|
+
now,
|
|
471
|
+
USER,
|
|
472
|
+
CONSOLE,
|
|
473
|
+
SessionState,
|
|
474
|
+
Leash,
|
|
475
|
+
AskKind,
|
|
476
|
+
slotOf,
|
|
477
|
+
latestPerSlot,
|
|
478
|
+
followersOf,
|
|
479
|
+
unfinishedFollowers,
|
|
480
|
+
isLive,
|
|
481
|
+
openAsk,
|
|
482
|
+
IssueState,
|
|
483
|
+
IssueType,
|
|
484
|
+
IssueMove,
|
|
485
|
+
addressOfIssue,
|
|
486
|
+
issueOrder,
|
|
487
|
+
unclosed,
|
|
488
|
+
waitingOn,
|
|
489
|
+
cmp,
|
|
490
|
+
Refused,
|
|
491
|
+
isRefusal,
|
|
492
|
+
invalid,
|
|
493
|
+
get
|
|
494
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createRequire as __cr } from 'node:module'; const require = __cr(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
ORG,
|
|
4
|
+
external_exports
|
|
5
|
+
} from "./chunk-NCZ77J5E.js";
|
|
6
|
+
|
|
7
|
+
// build/server/chunks/login.js
|
|
8
|
+
var GitHubName = external_exports.string().trim().regex(ORG, "Use a GitHub name: letters, digits, and single dashes, at most 39.");
|
|
9
|
+
var CommitName = external_exports.string().trim().min(1, "Name is required.").max(200, "Keep the name under 200 characters.");
|
|
10
|
+
var CommitEmail = external_exports.email("Enter an email address, like you@example.com.");
|
|
11
|
+
var SshKey = external_exports.string().trim().max(1e3).regex(/^(~\/|\/)[^\0\n]+$/, "Name a key file by its full path, or ~/.ssh/....").nullable();
|
|
12
|
+
external_exports.object({
|
|
13
|
+
login: GitHubName,
|
|
14
|
+
ssh_key: SshKey.default(null),
|
|
15
|
+
name: CommitName,
|
|
16
|
+
email: CommitEmail,
|
|
17
|
+
orgs: external_exports.array(GitHubName).default([])
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export {
|
|
21
|
+
GitHubName,
|
|
22
|
+
CommitName,
|
|
23
|
+
CommitEmail,
|
|
24
|
+
SshKey
|
|
25
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { createRequire as __cr } from 'node:module'; const require = __cr(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
get
|
|
4
|
+
} from "./chunk-PDCON4KS.js";
|
|
5
|
+
import {
|
|
6
|
+
ISSUE_TYPES,
|
|
7
|
+
IssueTitle,
|
|
8
|
+
SLUG_RULE,
|
|
9
|
+
Slug,
|
|
10
|
+
external_exports
|
|
11
|
+
} from "./chunk-NCZ77J5E.js";
|
|
12
|
+
|
|
13
|
+
// build/server/chunks/drafting.js
|
|
14
|
+
get("drafting");
|
|
15
|
+
var TRIES = 3;
|
|
16
|
+
var WORDS = 4;
|
|
17
|
+
var MAX = 24;
|
|
18
|
+
function shorten(name2) {
|
|
19
|
+
const [first, ...rest] = name2.split("-").slice(0, WORDS);
|
|
20
|
+
let out = first.slice(0, MAX);
|
|
21
|
+
for (const word of rest) {
|
|
22
|
+
if (out.length + 1 + word.length > MAX) break;
|
|
23
|
+
out += `-${word}`;
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
var name = (min) => external_exports.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, SLUG_RULE).transform(shorten).pipe(Slug.min(min, `at least ${min} characters`));
|
|
28
|
+
external_exports.object({
|
|
29
|
+
title: IssueTitle,
|
|
30
|
+
type: external_exports.enum(ISSUE_TYPES),
|
|
31
|
+
slug: name(1)
|
|
32
|
+
});
|
|
33
|
+
external_exports.object({ track: name(3) });
|
|
34
|
+
external_exports.object({ groups: external_exports.array(external_exports.object({
|
|
35
|
+
issues: external_exports.array(Slug),
|
|
36
|
+
why: external_exports.string().trim().min(1).max(160)
|
|
37
|
+
})) });
|
|
38
|
+
var SHORT = `descriptive and easy to remember, but leaning short: at most ${WORDS} words, usually 2 or 3; keep the words that tell this work apart, drop the rest, and use common abbreviations (int, mgmt, config, auth, repo, msg, impl, perms, env, dev, docs), but never go generic (int-repo-mgmt, not internal-repo-management or repo-fix)`;
|
|
39
|
+
`${ISSUE_TYPES.map((t) => `"${t}"`).join("|")}${SHORT}${MAX}`;
|
|
40
|
+
async function untaken(draft, nameOf, renamed, taken, avoid = []) {
|
|
41
|
+
avoid = [...avoid];
|
|
42
|
+
let last = null;
|
|
43
|
+
for (let n2 = 0; n2 < TRIES; n2++) {
|
|
44
|
+
last = await draft(avoid);
|
|
45
|
+
if (!last) return null;
|
|
46
|
+
if (!taken(nameOf(last))) return last;
|
|
47
|
+
avoid.push(nameOf(last));
|
|
48
|
+
}
|
|
49
|
+
const base = nameOf(last).slice(0, 20).replace(/-+$/, "");
|
|
50
|
+
let n = 2;
|
|
51
|
+
while (taken(`${base}-${n}`)) n++;
|
|
52
|
+
return renamed(last, `${base}-${n}`);
|
|
53
|
+
}
|
|
54
|
+
function draftFree(drafter, text, taken) {
|
|
55
|
+
return untaken((avoid) => drafter(text, avoid), (d) => d.slug, (d, slug) => ({
|
|
56
|
+
...d,
|
|
57
|
+
slug
|
|
58
|
+
}), taken);
|
|
59
|
+
}
|
|
60
|
+
async function trackFor(namer, slug, text, taken) {
|
|
61
|
+
if (!taken(slug)) return {
|
|
62
|
+
track: slug,
|
|
63
|
+
suggested: false
|
|
64
|
+
};
|
|
65
|
+
const track = await untaken((avoid) => namer(text, avoid), (t) => t, (_t, name2) => name2, taken, [slug]);
|
|
66
|
+
return track ? {
|
|
67
|
+
track,
|
|
68
|
+
suggested: true
|
|
69
|
+
} : null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export {
|
|
73
|
+
draftFree,
|
|
74
|
+
trackFor
|
|
75
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { createRequire as __cr } from 'node:module'; const require = __cr(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
Refused,
|
|
4
|
+
get,
|
|
5
|
+
invalid,
|
|
6
|
+
isRefusal
|
|
7
|
+
} from "./chunk-PDCON4KS.js";
|
|
8
|
+
import {
|
|
9
|
+
json
|
|
10
|
+
} from "./chunk-SU26PJVX.js";
|
|
11
|
+
|
|
12
|
+
// build/server/chunks/answer.js
|
|
13
|
+
var LOG = get("http");
|
|
14
|
+
async function answer(event, act) {
|
|
15
|
+
try {
|
|
16
|
+
return json(await act());
|
|
17
|
+
} catch (e) {
|
|
18
|
+
if (isRefusal(e)) {
|
|
19
|
+
LOG.warning("refused %s %s: %s", event.request.method, event.url.pathname, e.message);
|
|
20
|
+
return json({ error: e.message }, { status: 400 });
|
|
21
|
+
}
|
|
22
|
+
throw e;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async function body(event) {
|
|
26
|
+
let data;
|
|
27
|
+
try {
|
|
28
|
+
const text = await event.request.text();
|
|
29
|
+
data = text ? JSON.parse(text) : {};
|
|
30
|
+
} catch {
|
|
31
|
+
throw new Refused("body is not JSON");
|
|
32
|
+
}
|
|
33
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) throw new Refused("body must be an object");
|
|
34
|
+
return data;
|
|
35
|
+
}
|
|
36
|
+
async function parsed(event, schema) {
|
|
37
|
+
const result = schema.safeParse(await body(event));
|
|
38
|
+
if (!result.success) throw invalid("", result.error);
|
|
39
|
+
return result.data;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export {
|
|
43
|
+
answer,
|
|
44
|
+
body,
|
|
45
|
+
parsed
|
|
46
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { createRequire as __cr } from 'node:module'; const require = __cr(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
Slot
|
|
4
|
+
} from "./chunk-HGYE3UTU.js";
|
|
5
|
+
import {
|
|
6
|
+
Repo,
|
|
7
|
+
external_exports
|
|
8
|
+
} from "./chunk-NCZ77J5E.js";
|
|
9
|
+
|
|
10
|
+
// build/server/chunks/secrets.js
|
|
11
|
+
var SecretName = external_exports.string().max(64, { error: "A secret name is at most 64 characters." }).regex(/^[A-Z_][A-Z0-9_]*$/, { error: "A secret name is capitals, digits, and underscores, not starting with a digit, such as STRIPE_KEY." });
|
|
12
|
+
var SecretValue = external_exports.string().min(1, { error: "Type a value, then save." }).max(8192, { error: "A secret is at most 8192 characters." }).regex(/^[^\r\n]*$/, { error: "A secret is one line. Remove the line break, then save." });
|
|
13
|
+
external_exports.enum([
|
|
14
|
+
"filled",
|
|
15
|
+
"empty",
|
|
16
|
+
"missing"
|
|
17
|
+
]);
|
|
18
|
+
var changes = external_exports.record(SecretName, SecretValue).refine((c) => Object.keys(c).length > 0, "Change at least one secret.").optional();
|
|
19
|
+
var SecretsRequest = external_exports.union([external_exports.strictObject({
|
|
20
|
+
repo: Repo,
|
|
21
|
+
changes
|
|
22
|
+
}), external_exports.strictObject({
|
|
23
|
+
slot: Slot,
|
|
24
|
+
changes
|
|
25
|
+
})]);
|
|
26
|
+
var SECRETS_CATEGORY = "secrets";
|
|
27
|
+
var STATE_TEXT = {
|
|
28
|
+
filled: "In .env.secrets.",
|
|
29
|
+
empty: "In .env.secrets with no value yet.",
|
|
30
|
+
missing: "Declared in secrets.tmpl, not in .env.secrets yet. Saving adds it."
|
|
31
|
+
};
|
|
32
|
+
function secretsCategory(view, where = "repo") {
|
|
33
|
+
return external_exports.object(Object.fromEntries(view.keys.map((k) => [k.name, SecretValue.default("").meta({
|
|
34
|
+
title: k.name,
|
|
35
|
+
description: k.declared ? STATE_TEXT[k.state] : `${STATE_TEXT[k.state]} Not declared in secrets.tmpl.`,
|
|
36
|
+
secret: true,
|
|
37
|
+
placeholder: "Not set"
|
|
38
|
+
})]))).meta({
|
|
39
|
+
title: "Secrets",
|
|
40
|
+
description: view.ignored ? `The names in this ${where}'s .env.secrets and secrets.tmpl. A change is written to the file only when you save it, and no agent ever sees a value.` : `.env.secrets is not gitignored in this ${where}, so nothing can be saved. Add it to .gitignore first.`,
|
|
41
|
+
icon: "key-round",
|
|
42
|
+
save: "button"
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export {
|
|
47
|
+
SecretName,
|
|
48
|
+
SecretsRequest,
|
|
49
|
+
SECRETS_CATEGORY,
|
|
50
|
+
secretsCategory
|
|
51
|
+
};
|