@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.
Files changed (128) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +109 -0
  3. package/client/active-forge.ts +47 -0
  4. package/client/board-alert.tsx +236 -0
  5. package/client/foreign-link.tsx +33 -0
  6. package/client/hook-queue-panel.tsx +412 -0
  7. package/client/issues-pill.tsx +1976 -0
  8. package/client/label-chip.tsx +116 -0
  9. package/client/linkifier.tsx +280 -0
  10. package/client/pill-label.ts +183 -0
  11. package/client/vendor/paseo-plugin-helper/command-center.ts +43 -0
  12. package/client/vendor/paseo-plugin-helper/components/AboutSection.tsx +493 -0
  13. package/client/vendor/paseo-plugin-helper/components/AttentionBeacon.tsx +250 -0
  14. package/client/vendor/paseo-plugin-helper/components/Badge.tsx +156 -0
  15. package/client/vendor/paseo-plugin-helper/components/Button.tsx +178 -0
  16. package/client/vendor/paseo-plugin-helper/components/Card.tsx +225 -0
  17. package/client/vendor/paseo-plugin-helper/components/CodeBlock.tsx +196 -0
  18. package/client/vendor/paseo-plugin-helper/components/Collapsible.tsx +277 -0
  19. package/client/vendor/paseo-plugin-helper/components/CommandBox.tsx +172 -0
  20. package/client/vendor/paseo-plugin-helper/components/CopyButton.tsx +180 -0
  21. package/client/vendor/paseo-plugin-helper/components/DataTable.tsx +200 -0
  22. package/client/vendor/paseo-plugin-helper/components/EmptyState.tsx +97 -0
  23. package/client/vendor/paseo-plugin-helper/components/HighlightedText.tsx +70 -0
  24. package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +73 -0
  25. package/client/vendor/paseo-plugin-helper/components/KeyValue.tsx +446 -0
  26. package/client/vendor/paseo-plugin-helper/components/MetricGauge.tsx +247 -0
  27. package/client/vendor/paseo-plugin-helper/components/ProgressBar.tsx +117 -0
  28. package/client/vendor/paseo-plugin-helper/components/Responsive.tsx +53 -0
  29. package/client/vendor/paseo-plugin-helper/components/SearchInput.tsx +118 -0
  30. package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +80 -0
  31. package/client/vendor/paseo-plugin-helper/components/Select.tsx +215 -0
  32. package/client/vendor/paseo-plugin-helper/components/StatusDot.tsx +80 -0
  33. package/client/vendor/paseo-plugin-helper/components/Tabs.tsx +319 -0
  34. package/client/vendor/paseo-plugin-helper/components/TextInput.tsx +150 -0
  35. package/client/vendor/paseo-plugin-helper/components/Toggle.tsx +163 -0
  36. package/client/vendor/paseo-plugin-helper/components/TruncatedText.tsx +157 -0
  37. package/client/vendor/paseo-plugin-helper/components/index.ts +25 -0
  38. package/client/vendor/paseo-plugin-helper/custom-pills.tsx +224 -0
  39. package/client/vendor/paseo-plugin-helper/forge-icon.tsx +79 -0
  40. package/client/vendor/paseo-plugin-helper/host.ts +277 -0
  41. package/client/vendor/paseo-plugin-helper/icon.tsx +39 -0
  42. package/client/vendor/paseo-plugin-helper/index.ts +28 -0
  43. package/client/vendor/paseo-plugin-helper/layout/ActionBar.tsx +49 -0
  44. package/client/vendor/paseo-plugin-helper/layout/FormRow.tsx +103 -0
  45. package/client/vendor/paseo-plugin-helper/layout/Grid.tsx +65 -0
  46. package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +378 -0
  47. package/client/vendor/paseo-plugin-helper/layout/ModalContent.tsx +49 -0
  48. package/client/vendor/paseo-plugin-helper/layout/Row.tsx +39 -0
  49. package/client/vendor/paseo-plugin-helper/layout/Stack.tsx +39 -0
  50. package/client/vendor/paseo-plugin-helper/layout/index.ts +7 -0
  51. package/client/vendor/paseo-plugin-helper/panel.tsx +81 -0
  52. package/client/vendor/paseo-plugin-helper/pill.tsx +884 -0
  53. package/client/vendor/paseo-plugin-helper/query-refresh.ts +79 -0
  54. package/client/vendor/paseo-plugin-helper/query.ts +66 -0
  55. package/client/vendor/paseo-plugin-helper/settings-screen.tsx +372 -0
  56. package/client/vendor/paseo-plugin-helper/settings.ts +181 -0
  57. package/client/vendor/paseo-plugin-helper/shared-settings.ts +46 -0
  58. package/client/vendor/paseo-plugin-helper/snapshot.ts +68 -0
  59. package/client/vendor/paseo-plugin-helper/surface.tsx +80 -0
  60. package/client/vendor/paseo-plugin-helper/theme/color-utils.ts +118 -0
  61. package/client/vendor/paseo-plugin-helper/theme/flair.ts +76 -0
  62. package/client/vendor/paseo-plugin-helper/theme/host-variables.ts +121 -0
  63. package/client/vendor/paseo-plugin-helper/theme/index.ts +7 -0
  64. package/client/vendor/paseo-plugin-helper/theme/provider.tsx +214 -0
  65. package/client/vendor/paseo-plugin-helper/theme/responsive.ts +213 -0
  66. package/client/vendor/paseo-plugin-helper/theme/tokens.ts +161 -0
  67. package/client/vendor/paseo-plugin-helper/theme/useResponsive.ts +57 -0
  68. package/client/vendor/paseo-plugin-helper/utils/clipboard.ts +149 -0
  69. package/client/vendor/paseo-plugin-helper/utils/haptics.ts +34 -0
  70. package/client/webhook-card.tsx +198 -0
  71. package/docs/specs/forge-workflow-gui.md +820 -0
  72. package/docs/workflow.md +339 -0
  73. package/examples/README.md +108 -0
  74. package/examples/hook-service/README.md +66 -0
  75. package/examples/hook-service/forge-hook.service +28 -0
  76. package/examples/hook-service/hook-server.mjs +265 -0
  77. package/examples/hook-service/hook.env.example +21 -0
  78. package/examples/labels/README.md +64 -0
  79. package/examples/labels/label-base.yaml +117 -0
  80. package/examples/skills/coding-agent/SKILL.md +262 -0
  81. package/examples/skills/coding-agent-fgjx/SKILL.md +271 -0
  82. package/examples/skills/orchestrator/SKILL.md +133 -0
  83. package/examples/skills/orchestrator-fgjx/SKILL.md +139 -0
  84. package/examples/tools/README.md +68 -0
  85. package/examples/tools/fgjx +464 -0
  86. package/package.json +38 -0
  87. package/paseo-plugin.json +4 -0
  88. package/server/forge-client.ts +484 -0
  89. package/server/forge-guard.ts +70 -0
  90. package/server/git-origin.ts +70 -0
  91. package/server/hook-queue.ts +127 -0
  92. package/server/issues.ts +542 -0
  93. package/server/settings.ts +52 -0
  94. package/server/vendor/paseo-plugin-helper/agent.ts +85 -0
  95. package/server/vendor/paseo-plugin-helper/custom-pills.ts +344 -0
  96. package/server/vendor/paseo-plugin-helper/index.ts +18 -0
  97. package/server/vendor/paseo-plugin-helper/jsonc.ts +78 -0
  98. package/server/vendor/paseo-plugin-helper/logger.ts +210 -0
  99. package/server/vendor/paseo-plugin-helper/mcp-config.ts +367 -0
  100. package/server/vendor/paseo-plugin-helper/mcp-injection.ts +85 -0
  101. package/server/vendor/paseo-plugin-helper/network.ts +91 -0
  102. package/server/vendor/paseo-plugin-helper/plugins.ts +160 -0
  103. package/server/vendor/paseo-plugin-helper/process.ts +186 -0
  104. package/server/vendor/paseo-plugin-helper/redact.ts +86 -0
  105. package/server/vendor/paseo-plugin-helper/rpc-guard.ts +77 -0
  106. package/server/vendor/paseo-plugin-helper/settings.ts +97 -0
  107. package/server/vendor/paseo-plugin-helper/shared-settings.ts +243 -0
  108. package/server/vendor/paseo-plugin-helper/storage.ts +244 -0
  109. package/server/vendor/paseo-plugin-helper/system.ts +128 -0
  110. package/server/vendor/paseo-plugin-helper/task.ts +116 -0
  111. package/server/vendor/paseo-plugin-helper/version.ts +153 -0
  112. package/server/vendor/paseo-plugin-helper/workspace-beacon.ts +418 -0
  113. package/shared/hook-queue.ts +140 -0
  114. package/shared/issues.ts +1750 -0
  115. package/shared/vendor/paseo-plugin-helper/README.md +11 -0
  116. package/shared/vendor/paseo-plugin-helper/async.ts +35 -0
  117. package/shared/vendor/paseo-plugin-helper/custom-pills.ts +169 -0
  118. package/shared/vendor/paseo-plugin-helper/forge.ts +110 -0
  119. package/shared/vendor/paseo-plugin-helper/formatters.ts +271 -0
  120. package/shared/vendor/paseo-plugin-helper/highlight.ts +184 -0
  121. package/shared/vendor/paseo-plugin-helper/index.ts +10 -0
  122. package/shared/vendor/paseo-plugin-helper/rpc.ts +72 -0
  123. package/shared/vendor/paseo-plugin-helper/settings.ts +138 -0
  124. package/shared/vendor/paseo-plugin-helper/suite-settings.ts +17 -0
  125. package/shared/vendor/paseo-plugin-helper/suppressed.ts +31 -0
  126. package/shared/vendor/paseo-plugin-helper/types.ts +36 -0
  127. package/shared/version.ts +2 -0
  128. 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"