@xpufx/paseo-forges 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 +109 -0
- package/client/active-forge.ts +47 -0
- package/client/board-alert.tsx +236 -0
- package/client/foreign-link.tsx +33 -0
- package/client/hook-queue-panel.tsx +412 -0
- package/client/issues-pill.tsx +1976 -0
- package/client/label-chip.tsx +116 -0
- package/client/linkifier.tsx +280 -0
- package/client/pill-label.ts +183 -0
- package/client/vendor/paseo-plugin-helper/command-center.ts +43 -0
- package/client/vendor/paseo-plugin-helper/components/AboutSection.tsx +493 -0
- package/client/vendor/paseo-plugin-helper/components/AttentionBeacon.tsx +250 -0
- package/client/vendor/paseo-plugin-helper/components/Badge.tsx +156 -0
- package/client/vendor/paseo-plugin-helper/components/Button.tsx +178 -0
- package/client/vendor/paseo-plugin-helper/components/Card.tsx +225 -0
- package/client/vendor/paseo-plugin-helper/components/CodeBlock.tsx +196 -0
- package/client/vendor/paseo-plugin-helper/components/Collapsible.tsx +277 -0
- package/client/vendor/paseo-plugin-helper/components/CommandBox.tsx +172 -0
- package/client/vendor/paseo-plugin-helper/components/CopyButton.tsx +180 -0
- package/client/vendor/paseo-plugin-helper/components/DataTable.tsx +200 -0
- package/client/vendor/paseo-plugin-helper/components/EmptyState.tsx +97 -0
- package/client/vendor/paseo-plugin-helper/components/HighlightedText.tsx +70 -0
- package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +73 -0
- package/client/vendor/paseo-plugin-helper/components/KeyValue.tsx +446 -0
- package/client/vendor/paseo-plugin-helper/components/MetricGauge.tsx +247 -0
- package/client/vendor/paseo-plugin-helper/components/ProgressBar.tsx +117 -0
- package/client/vendor/paseo-plugin-helper/components/Responsive.tsx +53 -0
- package/client/vendor/paseo-plugin-helper/components/SearchInput.tsx +118 -0
- package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +80 -0
- package/client/vendor/paseo-plugin-helper/components/Select.tsx +215 -0
- package/client/vendor/paseo-plugin-helper/components/StatusDot.tsx +80 -0
- package/client/vendor/paseo-plugin-helper/components/Tabs.tsx +319 -0
- package/client/vendor/paseo-plugin-helper/components/TextInput.tsx +150 -0
- package/client/vendor/paseo-plugin-helper/components/Toggle.tsx +163 -0
- package/client/vendor/paseo-plugin-helper/components/TruncatedText.tsx +157 -0
- package/client/vendor/paseo-plugin-helper/components/index.ts +25 -0
- package/client/vendor/paseo-plugin-helper/custom-pills.tsx +224 -0
- package/client/vendor/paseo-plugin-helper/forge-icon.tsx +79 -0
- package/client/vendor/paseo-plugin-helper/host.ts +277 -0
- package/client/vendor/paseo-plugin-helper/icon.tsx +39 -0
- package/client/vendor/paseo-plugin-helper/index.ts +28 -0
- package/client/vendor/paseo-plugin-helper/layout/ActionBar.tsx +49 -0
- package/client/vendor/paseo-plugin-helper/layout/FormRow.tsx +103 -0
- package/client/vendor/paseo-plugin-helper/layout/Grid.tsx +65 -0
- package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +378 -0
- package/client/vendor/paseo-plugin-helper/layout/ModalContent.tsx +49 -0
- package/client/vendor/paseo-plugin-helper/layout/Row.tsx +39 -0
- package/client/vendor/paseo-plugin-helper/layout/Stack.tsx +39 -0
- package/client/vendor/paseo-plugin-helper/layout/index.ts +7 -0
- package/client/vendor/paseo-plugin-helper/panel.tsx +81 -0
- package/client/vendor/paseo-plugin-helper/pill.tsx +884 -0
- package/client/vendor/paseo-plugin-helper/query-refresh.ts +79 -0
- package/client/vendor/paseo-plugin-helper/query.ts +66 -0
- package/client/vendor/paseo-plugin-helper/settings-screen.tsx +372 -0
- package/client/vendor/paseo-plugin-helper/settings.ts +181 -0
- package/client/vendor/paseo-plugin-helper/shared-settings.ts +46 -0
- package/client/vendor/paseo-plugin-helper/snapshot.ts +68 -0
- package/client/vendor/paseo-plugin-helper/surface.tsx +80 -0
- package/client/vendor/paseo-plugin-helper/theme/color-utils.ts +118 -0
- package/client/vendor/paseo-plugin-helper/theme/flair.ts +76 -0
- package/client/vendor/paseo-plugin-helper/theme/host-variables.ts +121 -0
- package/client/vendor/paseo-plugin-helper/theme/index.ts +7 -0
- package/client/vendor/paseo-plugin-helper/theme/provider.tsx +214 -0
- package/client/vendor/paseo-plugin-helper/theme/responsive.ts +213 -0
- package/client/vendor/paseo-plugin-helper/theme/tokens.ts +161 -0
- package/client/vendor/paseo-plugin-helper/theme/useResponsive.ts +57 -0
- package/client/vendor/paseo-plugin-helper/utils/clipboard.ts +149 -0
- package/client/vendor/paseo-plugin-helper/utils/haptics.ts +34 -0
- package/client/webhook-card.tsx +198 -0
- package/docs/specs/forge-workflow-gui.md +820 -0
- package/docs/workflow.md +339 -0
- package/examples/README.md +108 -0
- package/examples/hook-service/README.md +66 -0
- package/examples/hook-service/forge-hook.service +28 -0
- package/examples/hook-service/hook-server.mjs +265 -0
- package/examples/hook-service/hook.env.example +21 -0
- package/examples/labels/README.md +64 -0
- package/examples/labels/label-base.yaml +117 -0
- package/examples/skills/coding-agent/SKILL.md +262 -0
- package/examples/skills/coding-agent-fgjx/SKILL.md +271 -0
- package/examples/skills/orchestrator/SKILL.md +133 -0
- package/examples/skills/orchestrator-fgjx/SKILL.md +139 -0
- package/examples/tools/README.md +68 -0
- package/examples/tools/fgjx +464 -0
- package/package.json +38 -0
- package/paseo-plugin.json +4 -0
- package/server/forge-client.ts +484 -0
- package/server/forge-guard.ts +70 -0
- package/server/git-origin.ts +70 -0
- package/server/hook-queue.ts +127 -0
- package/server/issues.ts +542 -0
- package/server/settings.ts +52 -0
- package/server/vendor/paseo-plugin-helper/agent.ts +85 -0
- package/server/vendor/paseo-plugin-helper/custom-pills.ts +344 -0
- package/server/vendor/paseo-plugin-helper/index.ts +18 -0
- package/server/vendor/paseo-plugin-helper/jsonc.ts +78 -0
- package/server/vendor/paseo-plugin-helper/logger.ts +210 -0
- package/server/vendor/paseo-plugin-helper/mcp-config.ts +367 -0
- package/server/vendor/paseo-plugin-helper/mcp-injection.ts +85 -0
- package/server/vendor/paseo-plugin-helper/network.ts +91 -0
- package/server/vendor/paseo-plugin-helper/plugins.ts +160 -0
- package/server/vendor/paseo-plugin-helper/process.ts +186 -0
- package/server/vendor/paseo-plugin-helper/redact.ts +86 -0
- package/server/vendor/paseo-plugin-helper/rpc-guard.ts +77 -0
- package/server/vendor/paseo-plugin-helper/settings.ts +97 -0
- package/server/vendor/paseo-plugin-helper/shared-settings.ts +243 -0
- package/server/vendor/paseo-plugin-helper/storage.ts +244 -0
- package/server/vendor/paseo-plugin-helper/system.ts +128 -0
- package/server/vendor/paseo-plugin-helper/task.ts +116 -0
- package/server/vendor/paseo-plugin-helper/version.ts +153 -0
- package/server/vendor/paseo-plugin-helper/workspace-beacon.ts +418 -0
- package/shared/hook-queue.ts +140 -0
- package/shared/issues.ts +1750 -0
- package/shared/vendor/paseo-plugin-helper/README.md +11 -0
- package/shared/vendor/paseo-plugin-helper/async.ts +35 -0
- package/shared/vendor/paseo-plugin-helper/custom-pills.ts +169 -0
- package/shared/vendor/paseo-plugin-helper/forge.ts +110 -0
- package/shared/vendor/paseo-plugin-helper/formatters.ts +271 -0
- package/shared/vendor/paseo-plugin-helper/highlight.ts +184 -0
- package/shared/vendor/paseo-plugin-helper/index.ts +10 -0
- package/shared/vendor/paseo-plugin-helper/rpc.ts +72 -0
- package/shared/vendor/paseo-plugin-helper/settings.ts +138 -0
- package/shared/vendor/paseo-plugin-helper/suite-settings.ts +17 -0
- package/shared/vendor/paseo-plugin-helper/suppressed.ts +31 -0
- package/shared/vendor/paseo-plugin-helper/types.ts +36 -0
- package/shared/version.ts +2 -0
- package/shared/webhook.ts +223 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Generic Forgejo/Gitea-family webhook -> Paseo bridge (EXAMPLE SKELETON).
|
|
4
|
+
*
|
|
5
|
+
* This is a teaching skeleton, not a supported service. It implements the
|
|
6
|
+
* contract described in ../../docs/workflow.md §2 with placeholders only:
|
|
7
|
+
*
|
|
8
|
+
* POST /hook authenticated webhook delivery; summarize + queue + send
|
|
9
|
+
* POST /orchestrate register which agent owns a repo (loopback needs no secret)
|
|
10
|
+
*
|
|
11
|
+
* Configuration is environment-only; see ./hook.env.example. Replace the
|
|
12
|
+
* placeholders, review the TODOs, and run it either as a systemd service
|
|
13
|
+
* (./forge-hook.service) or as a Paseo workspace service.
|
|
14
|
+
*
|
|
15
|
+
* Deliberately omitted from this skeleton (see the docs for why they matter):
|
|
16
|
+
* - shared-secret rotation and constant-time comparison of the bearer token
|
|
17
|
+
* - durable queueing across restarts
|
|
18
|
+
* - event coalescing / debounce with slash-command bypass
|
|
19
|
+
* - validating the payload against a strict schema
|
|
20
|
+
*/
|
|
21
|
+
import http from "node:http";
|
|
22
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
23
|
+
import { execFile } from "node:child_process";
|
|
24
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
25
|
+
import { join } from "node:path";
|
|
26
|
+
|
|
27
|
+
const HOST = process.env.HOOK_HOST ?? "127.0.0.1";
|
|
28
|
+
const PORT = Number(process.env.HOOK_PORT ?? 8099);
|
|
29
|
+
const PASEO_BIN = process.env.PASEO_BIN ?? "paseo";
|
|
30
|
+
const STATE_DIR = process.env.HOOK_STATE_DIR ?? join(process.cwd(), ".forge-hook");
|
|
31
|
+
const FALLBACK_AGENT_ID = process.env.HOOK_DEFAULT_AGENT_ID ?? "";
|
|
32
|
+
|
|
33
|
+
/** Secret wins from a file when WEBHOOK_SECRET_FILE is set, else inline. */
|
|
34
|
+
function readSecret() {
|
|
35
|
+
const file = process.env.WEBHOOK_SECRET_FILE;
|
|
36
|
+
if (file) {
|
|
37
|
+
try {
|
|
38
|
+
return readFileSync(file, "utf8").trim();
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.error(`cannot read WEBHOOK_SECRET_FILE (${file}): ${error.message}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return process.env.WEBHOOK_SECRET ?? "";
|
|
44
|
+
}
|
|
45
|
+
const SECRET = readSecret();
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Routing key: forge-qualified host/owner/repo, derived from the payload so one
|
|
49
|
+
// process can serve many repos regardless of its working directory.
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
function repoKey(body) {
|
|
53
|
+
const repository = body?.repository ?? {};
|
|
54
|
+
const url = repository.html_url ?? repository.clone_url ?? repository.ssh_url ?? "";
|
|
55
|
+
try {
|
|
56
|
+
const parsed = new URL(url);
|
|
57
|
+
const [owner, ...rest] = parsed.pathname.replace(/^\/+/, "").replace(/\.git$/, "").split("/");
|
|
58
|
+
const repo = rest.join("/");
|
|
59
|
+
if (!parsed.hostname || !owner || !repo) return null;
|
|
60
|
+
return `${parsed.hostname.toLowerCase()}/${owner.toLowerCase()}/${repo.toLowerCase()}`;
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Per-repo orchestrator state. The file is the authority; agent name/role
|
|
68
|
+
// labels (set elsewhere) are only a UI projection.
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
function statePath(key) {
|
|
72
|
+
return join(STATE_DIR, `${key.replace(/[^a-z0-9._-]+/gi, "_")}.json`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function readOrchestrator(key) {
|
|
76
|
+
try {
|
|
77
|
+
const parsed = JSON.parse(readFileSync(statePath(key), "utf8"));
|
|
78
|
+
if (typeof parsed?.agentId === "string" && parsed.agentId) return parsed.agentId;
|
|
79
|
+
} catch {
|
|
80
|
+
// No file yet.
|
|
81
|
+
}
|
|
82
|
+
return FALLBACK_AGENT_ID || null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function writeOrchestrator(key, agentId) {
|
|
86
|
+
mkdirSync(STATE_DIR, { recursive: true });
|
|
87
|
+
const target = statePath(key);
|
|
88
|
+
const tmp = `${target}.${process.pid}.tmp`;
|
|
89
|
+
writeFileSync(tmp, `${JSON.stringify({ agentId, updatedAt: new Date().toISOString() }, null, 2)}\n`, {
|
|
90
|
+
mode: 0o600,
|
|
91
|
+
});
|
|
92
|
+
renameSync(tmp, target);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// Auth: accept a bearer token, a plain secret header, or a Forgejo HMAC of the
|
|
97
|
+
// raw body. The secret never reaches an agent.
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
function authorized(req, raw) {
|
|
101
|
+
if (!SECRET) return true; // TODO: refuse to start when unset in production.
|
|
102
|
+
const auth = req.headers["authorization"] ?? "";
|
|
103
|
+
if (auth === `Bearer ${SECRET}`) return true;
|
|
104
|
+
if ((req.headers["x-webhook-secret"] ?? "") === SECRET) return true;
|
|
105
|
+
const signature = req.headers["x-forgejo-signature"] ?? "";
|
|
106
|
+
if (!signature) return false;
|
|
107
|
+
const mac = createHmac("sha256", SECRET).update(raw, "utf8").digest("hex");
|
|
108
|
+
if (mac.length !== signature.length) return false;
|
|
109
|
+
try {
|
|
110
|
+
return timingSafeEqual(Buffer.from(mac), Buffer.from(signature));
|
|
111
|
+
} catch {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function isLoopback(req) {
|
|
117
|
+
const address = req.socket?.remoteAddress ?? "";
|
|
118
|
+
return address === "127.0.0.1" || address === "::1" || address === "::ffff:127.0.0.1";
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
// Human-readable summary. The forges plugin parses this shape into a timeline
|
|
123
|
+
// card (shared/webhook.ts); keep the prefix and bracket format if you want it.
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
const HOOK_HEAD = "🔔 Forgejo webhook incoming";
|
|
127
|
+
|
|
128
|
+
function summarize(event, body) {
|
|
129
|
+
const repo = body?.repository?.full_name ?? repoKey(body) ?? "unknown repo";
|
|
130
|
+
const sender = body?.sender?.login ?? "unknown";
|
|
131
|
+
if (event === "ping") return `${HOOK_HEAD} (ping test) ${repo} (by ${sender})`;
|
|
132
|
+
const subject = body?.issue ?? body?.pull_request ?? {};
|
|
133
|
+
const action = body?.action ?? "";
|
|
134
|
+
const label = subject.number != null ? `${repo}#${subject.number} ${subject.title ?? ""}`.trim() : repo;
|
|
135
|
+
const line = `${HOOK_HEAD} [${event}:${action}] ${label} (by ${sender})`.trim();
|
|
136
|
+
const url = subject.html_url ?? body?.repository?.html_url ?? "";
|
|
137
|
+
return url && !line.includes(url) ? `${line} ${url}` : line;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
// Delivery: one `paseo send` at a time per repo. The daemon rejects a second
|
|
142
|
+
// send while an agent already has an active run, so serialize and retry later.
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
const pending = new Map();
|
|
146
|
+
const draining = new Set();
|
|
147
|
+
|
|
148
|
+
function enqueue(key, message) {
|
|
149
|
+
const list = pending.get(key) ?? [];
|
|
150
|
+
if (!list.includes(message)) list.push(message);
|
|
151
|
+
pending.set(key, list);
|
|
152
|
+
return list.length;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function deliver(agentId, message) {
|
|
156
|
+
return new Promise((resolve) => {
|
|
157
|
+
execFile(PASEO_BIN, ["send", agentId, message], (error) => {
|
|
158
|
+
if (error) console.error(`paseo send failed: ${error.message}`);
|
|
159
|
+
resolve(!error);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function drain(key) {
|
|
165
|
+
if (draining.has(key)) return;
|
|
166
|
+
draining.add(key);
|
|
167
|
+
try {
|
|
168
|
+
for (;;) {
|
|
169
|
+
const list = pending.get(key);
|
|
170
|
+
if (!list || list.length === 0) {
|
|
171
|
+
pending.delete(key);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const agentId = readOrchestrator(key);
|
|
175
|
+
if (!agentId) {
|
|
176
|
+
console.error(`no orchestrator for ${key}; holding ${list.length} message(s)`);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (!(await deliver(agentId, list[0]))) return; // Retried by the interval below.
|
|
180
|
+
list.shift();
|
|
181
|
+
}
|
|
182
|
+
} finally {
|
|
183
|
+
draining.delete(key);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function handleMessage(key, message) {
|
|
188
|
+
const depth = enqueue(key, message);
|
|
189
|
+
console.log(`queued ${key} queue=${depth}`);
|
|
190
|
+
void drain(key);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
// HTTP surface.
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
|
|
197
|
+
function handleHook(req, raw, res) {
|
|
198
|
+
const event = String(req.headers["x-forgejo-event"] ?? "unknown");
|
|
199
|
+
let body;
|
|
200
|
+
try {
|
|
201
|
+
body = JSON.parse(raw || "{}");
|
|
202
|
+
} catch {
|
|
203
|
+
res.writeHead(400).end("bad json");
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const key = repoKey(body);
|
|
207
|
+
const message = summarize(event, body);
|
|
208
|
+
console.log(`${new Date().toISOString()} event=${event} key=${key ?? "?"} ${message}`);
|
|
209
|
+
res.writeHead(202).end("accepted");
|
|
210
|
+
if (!key) {
|
|
211
|
+
console.error("no repo key in payload; event dropped");
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
handleMessage(key, message);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function handleOrchestrate(raw, res) {
|
|
218
|
+
let body;
|
|
219
|
+
try {
|
|
220
|
+
body = JSON.parse(raw || "{}");
|
|
221
|
+
} catch {
|
|
222
|
+
res.writeHead(400).end("bad json");
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const agentId = String(body?.agentId ?? "").trim();
|
|
226
|
+
const key = typeof body?.repo === "string" ? body.repo.trim().toLowerCase() : "";
|
|
227
|
+
if (!agentId || !key) {
|
|
228
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
229
|
+
res.end(JSON.stringify({ error: "agentId and repo are required" }));
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
writeOrchestrator(key, agentId);
|
|
233
|
+
console.log(`orchestrator ${key} -> ${agentId}`);
|
|
234
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
235
|
+
res.end(JSON.stringify({ key, agentId }));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const server = http.createServer((req, res) => {
|
|
239
|
+
const path = req.url?.split("?")[0];
|
|
240
|
+
if (req.method !== "POST" || (path !== "/hook" && path !== "/orchestrate")) {
|
|
241
|
+
res.writeHead(404).end("not found");
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
let raw = "";
|
|
245
|
+
req.on("data", (chunk) => {
|
|
246
|
+
raw += chunk;
|
|
247
|
+
if (raw.length > 256 * 1024) req.destroy();
|
|
248
|
+
});
|
|
249
|
+
req.on("end", () => {
|
|
250
|
+
const loopbackControl = path === "/orchestrate" && isLoopback(req);
|
|
251
|
+
if (!loopbackControl && SECRET && !authorized(req, raw)) {
|
|
252
|
+
res.writeHead(403).end("bad secret");
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (path === "/orchestrate") handleOrchestrate(raw, res);
|
|
256
|
+
else handleHook(req, raw, res);
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// Periodically retry anything held because the target was busy or unset.
|
|
261
|
+
setInterval(() => {
|
|
262
|
+
for (const key of pending.keys()) void drain(key);
|
|
263
|
+
}, Number(process.env.HOOK_RETRY_MS ?? 30000)).unref?.();
|
|
264
|
+
|
|
265
|
+
server.listen(PORT, HOST, () => console.log(`forge-hook listening on ${HOST}:${PORT}`));
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Copy to /etc/forge-hook/hook.env (mode 0600) and replace every placeholder.
|
|
2
|
+
# Consumed by hook-server.mjs. None of these are read by the forges plugin.
|
|
3
|
+
|
|
4
|
+
# HTTP listener. Keep it on loopback unless you put TLS + auth in front.
|
|
5
|
+
HOOK_HOST=127.0.0.1
|
|
6
|
+
HOOK_PORT=8099
|
|
7
|
+
|
|
8
|
+
# Shared secret the forge is configured to send. Supply EITHER an inline
|
|
9
|
+
# secret or a path to a file containing it (the file wins when both are set).
|
|
10
|
+
# WEBHOOK_SECRET=replace-with-a-long-random-string
|
|
11
|
+
WEBHOOK_SECRET_FILE=/etc/forge-hook/webhook.secret
|
|
12
|
+
|
|
13
|
+
# Paseo CLI used to deliver a message to an agent.
|
|
14
|
+
PASEO_BIN=/usr/local/bin/paseo
|
|
15
|
+
|
|
16
|
+
# Per-repo orchestrator state (must be writable by the service user).
|
|
17
|
+
HOOK_STATE_DIR=/var/lib/forge-hook
|
|
18
|
+
|
|
19
|
+
# Fallback target when a repo has no orchestrator registered yet. Leave empty
|
|
20
|
+
# to hold queued messages instead of guessing.
|
|
21
|
+
HOOK_DEFAULT_AGENT_ID=
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Label base (example)
|
|
2
|
+
|
|
3
|
+
A generic, apply-able seed for the scoped labels the agent workflow depends on:
|
|
4
|
+
`state/`, `priority/`, `attention/`, `spec/`, plus an optional `flag/stop-work`
|
|
5
|
+
circuit breaker. `exclusive: true` is what gives each scope its
|
|
6
|
+
single-occupancy behavior (apply one, the previous mate evicts itself).
|
|
7
|
+
|
|
8
|
+
See [`../../docs/workflow.md`](../../docs/workflow.md) §3 for how the labels
|
|
9
|
+
drive the board and the plugin.
|
|
10
|
+
|
|
11
|
+
## File
|
|
12
|
+
|
|
13
|
+
`label-base.yaml` — a YAML label template in the format Forgejo/Gitea read
|
|
14
|
+
(`labels:` list, each with `name`, `color`, `exclusive`, `description`).
|
|
15
|
+
|
|
16
|
+
## Apply it
|
|
17
|
+
|
|
18
|
+
### As an instance label template (Forgejo >= 1.19)
|
|
19
|
+
|
|
20
|
+
Copy the file into the instance's custom label directory and it becomes a
|
|
21
|
+
selectable label set when creating repositories:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
sudo cp label-base.yaml "$FORGEJO_CUSTOM/options/label/agent-workflow.yaml"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Label templates are applied at **repository creation time** (choose the set in
|
|
28
|
+
the create-repo dialog or pass `issue_labels` to the API). For an existing repo
|
|
29
|
+
there is no single "apply template" call.
|
|
30
|
+
|
|
31
|
+
### To an existing repo via the API
|
|
32
|
+
|
|
33
|
+
Fetch the template's labels and create each one. Example with `jq`:
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
FORGEJO_URL=https://forge.example.com
|
|
37
|
+
OWNER=your-org
|
|
38
|
+
REPO=your-repo
|
|
39
|
+
TOKEN=<your-personal-access-token>
|
|
40
|
+
|
|
41
|
+
jq -c '.labels[]' label-base.yaml | while read -r label; do
|
|
42
|
+
curl -s -X POST \
|
|
43
|
+
-H "Authorization: token ${TOKEN}" \
|
|
44
|
+
-H "Content-Type: application/json" \
|
|
45
|
+
-d "$(jq -n --argjson l "$label" '{name:$l.name,color:$l.color,description:$l.description,exclusive:$l.exclusive}')" \
|
|
46
|
+
"${FORGEJO_URL}/api/v1/repos/${OWNER}/${REPO}/labels"
|
|
47
|
+
done
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### By hand
|
|
51
|
+
|
|
52
|
+
The UI's **Labels → New Label** accepts a `scope/value` name and an
|
|
53
|
+
**Exclusive** checkbox. This is the slowest option but needs no API token.
|
|
54
|
+
|
|
55
|
+
## Notes
|
|
56
|
+
|
|
57
|
+
- Label colors render in the plugin's chips; keep them 6-digit hex (with or
|
|
58
|
+
without `#`). Scoped chips always split into scope + value segments.
|
|
59
|
+
- The four documented scopes are enough to run the workflow. Add your own
|
|
60
|
+
scopes freely — the plugin derives the live vocabulary from whatever labels
|
|
61
|
+
are actually on the board, so it degrades gracefully if you rename or omit
|
|
62
|
+
any of these.
|
|
63
|
+
- Applying a new label in an exclusive scope requires deleting the old one only
|
|
64
|
+
if the label was created **without** `exclusive: true`. Keep the flag set.
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Generic workflow label base for a Forgejo/Gitea-family repository.
|
|
2
|
+
#
|
|
3
|
+
# Apply as an advanced label template (Forgejo >= 1.19), or create the same
|
|
4
|
+
# labels through the UI/API. `exclusive: true` makes a scoped label mutually
|
|
5
|
+
# exclusive per scope: applying one evicts any other label with the same scope,
|
|
6
|
+
# which is what the forges plugin's chips and state transitions rely on.
|
|
7
|
+
#
|
|
8
|
+
# Colors are 6-digit hex. Edit names/colors/descriptions freely; if you rename
|
|
9
|
+
# a scope, keep the `scope/value` shape and update your Skills to match.
|
|
10
|
+
|
|
11
|
+
labels:
|
|
12
|
+
# ---------------------------------------------------------------------------
|
|
13
|
+
# Execution lifecycle. One state per issue; advance by applying the next one.
|
|
14
|
+
# ---------------------------------------------------------------------------
|
|
15
|
+
- name: "state/0-triage"
|
|
16
|
+
exclusive: true
|
|
17
|
+
color: "#1d76db"
|
|
18
|
+
description: "New issue awaiting initial triage"
|
|
19
|
+
|
|
20
|
+
- name: "state/1-wip"
|
|
21
|
+
exclusive: true
|
|
22
|
+
color: "#0e7c6b"
|
|
23
|
+
description: "Active implementation in progress"
|
|
24
|
+
|
|
25
|
+
- name: "state/2-review"
|
|
26
|
+
exclusive: true
|
|
27
|
+
color: "#a6700b"
|
|
28
|
+
description: "Awaiting internal review before operator verification"
|
|
29
|
+
|
|
30
|
+
- name: "state/3-verify"
|
|
31
|
+
exclusive: true
|
|
32
|
+
color: "#6e40c9"
|
|
33
|
+
description: "Awaiting human operator signoff or live verification"
|
|
34
|
+
|
|
35
|
+
- name: "state/4-done"
|
|
36
|
+
exclusive: true
|
|
37
|
+
color: "#1a7f37"
|
|
38
|
+
description: "Accepted"
|
|
39
|
+
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
# Priority / queue ordering. Unlabeled issues are treated as normal.
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
- name: "priority/0-SOS"
|
|
44
|
+
exclusive: true
|
|
45
|
+
color: "#d1242f"
|
|
46
|
+
description: "Urgent dispatch; preempts all standard work"
|
|
47
|
+
|
|
48
|
+
- name: "priority/1-high"
|
|
49
|
+
exclusive: true
|
|
50
|
+
color: "#e85d04"
|
|
51
|
+
description: "High priority"
|
|
52
|
+
|
|
53
|
+
- name: "priority/2-normal"
|
|
54
|
+
exclusive: true
|
|
55
|
+
color: "#1d76db"
|
|
56
|
+
description: "Normal priority (default when no priority label is set)"
|
|
57
|
+
|
|
58
|
+
- name: "priority/3-low"
|
|
59
|
+
exclusive: true
|
|
60
|
+
color: "#59636e"
|
|
61
|
+
description: "Low priority"
|
|
62
|
+
|
|
63
|
+
- name: "priority/4-backburner"
|
|
64
|
+
exclusive: true
|
|
65
|
+
color: "#8c959f"
|
|
66
|
+
description: "Parked; surface only when idle"
|
|
67
|
+
|
|
68
|
+
# ---------------------------------------------------------------------------
|
|
69
|
+
# Attention / who acts next. This is the operator's steering channel.
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
- name: "attention/0-orchestrator"
|
|
72
|
+
exclusive: true
|
|
73
|
+
color: "#8250df"
|
|
74
|
+
description: "Requires orchestrator triage, decomposition, or dispatch"
|
|
75
|
+
|
|
76
|
+
- name: "attention/1-agent"
|
|
77
|
+
exclusive: true
|
|
78
|
+
color: "#0e7c6b"
|
|
79
|
+
description: "Available for an agent to claim and act on"
|
|
80
|
+
|
|
81
|
+
- name: "attention/2-user"
|
|
82
|
+
exclusive: true
|
|
83
|
+
color: "#d1242f"
|
|
84
|
+
description: "Blocked on human operator clarification or decision"
|
|
85
|
+
|
|
86
|
+
- name: "attention/3-ignore"
|
|
87
|
+
exclusive: true
|
|
88
|
+
color: "#6b7280"
|
|
89
|
+
description: "Suppress automated triage unless escalated"
|
|
90
|
+
|
|
91
|
+
# ---------------------------------------------------------------------------
|
|
92
|
+
# Specification / pre-code shaping. Optional but useful for large tickets.
|
|
93
|
+
# ---------------------------------------------------------------------------
|
|
94
|
+
- name: "spec/0-needed"
|
|
95
|
+
exclusive: true
|
|
96
|
+
color: "#a6700b"
|
|
97
|
+
description: "Needs a technical specification and boundary constraints"
|
|
98
|
+
|
|
99
|
+
- name: "spec/1-checklist"
|
|
100
|
+
exclusive: true
|
|
101
|
+
color: "#1d76db"
|
|
102
|
+
description: "Actionable checklist shaped and ready for human review"
|
|
103
|
+
|
|
104
|
+
- name: "spec/2-approved"
|
|
105
|
+
exclusive: true
|
|
106
|
+
color: "#1a7f37"
|
|
107
|
+
description: "Specification and checklist approved for implementation"
|
|
108
|
+
|
|
109
|
+
# ---------------------------------------------------------------------------
|
|
110
|
+
# Optional: circuit breaker referenced by the orchestrator example Skill.
|
|
111
|
+
# Add more scopes the same way; the plugin derives live scopes from the board,
|
|
112
|
+
# so anything you define here shows up in its chips.
|
|
113
|
+
# ---------------------------------------------------------------------------
|
|
114
|
+
- name: "flag/stop-work"
|
|
115
|
+
exclusive: true
|
|
116
|
+
color: "#d1242f"
|
|
117
|
+
description: "Halt all work on this issue immediately"
|