@aion0/forge 0.9.1 → 0.9.2
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/RELEASE_NOTES.md +60 -5
- package/app/api/agents/[id]/test/route.ts +150 -0
- package/app/api/connectors/[id]/sync-cli/route.ts +73 -0
- package/app/api/connectors/tool-test/route.ts +70 -0
- package/app/api/jobs/[id]/cancel/route.ts +50 -0
- package/app/api/jobs/[id]/dispatched-pipelines/route.ts +24 -0
- package/app/api/jobs/[id]/run/route.ts +22 -2
- package/app/api/jobs/route.ts +11 -1
- package/app/api/pipelines/[id]/schema/route.ts +53 -0
- package/app/api/pipelines/bulk-delete/route.ts +39 -0
- package/app/api/pipelines/gc/route.ts +27 -0
- package/app/api/schedules/[id]/cancel/route.ts +27 -0
- package/app/api/schedules/[id]/route.ts +173 -0
- package/app/api/schedules/[id]/run/route.ts +45 -0
- package/app/api/schedules/[id]/runs/route.ts +22 -0
- package/app/api/schedules/[id]/stop/route.ts +33 -0
- package/app/api/schedules/route.ts +175 -0
- package/app/api/tasks/bulk-delete/route.ts +47 -0
- package/bin/forge-server.mjs +22 -1
- package/cli/mw.mjs +186 -7657
- package/cli/mw.ts +26 -0
- package/components/ConnectorsPanel.tsx +46 -0
- package/components/Dashboard.tsx +23 -10
- package/components/JobsView.tsx +245 -6
- package/components/PipelineEditor.tsx +38 -1
- package/components/PipelineView.tsx +325 -4
- package/components/ScheduleCreateModal.tsx +1507 -0
- package/components/SchedulesView.tsx +605 -0
- package/components/SettingsModal.tsx +106 -0
- package/docs/Team-Workflow-Integration.md +487 -0
- package/docs/UI-Design-Brief-SidePanel.md +278 -0
- package/lib/__tests__/foreach-batch-yaml.test.ts +33 -0
- package/lib/__tests__/foreach-before.test.ts +201 -0
- package/lib/__tests__/foreach-parse.test.ts +114 -0
- package/lib/__tests__/foreach-snapshot.test.ts +112 -0
- package/lib/__tests__/foreach-source.test.ts +105 -0
- package/lib/__tests__/foreach-template.test.ts +112 -0
- package/lib/chat/agent-loop.ts +3 -3
- package/lib/chat-standalone.ts +26 -1
- package/lib/claude-process.ts +8 -5
- package/lib/connectors/sync.ts +8 -2
- package/lib/crypto.ts +1 -1
- package/lib/dirs.ts +22 -7
- package/lib/help-docs/05-pipelines.md +171 -0
- package/lib/help-docs/13-schedules.md +165 -0
- package/lib/help-docs/23-automation-states.md +148 -0
- package/lib/help-docs/CLAUDE.md +6 -6
- package/lib/init.ts +25 -6
- package/lib/jobs/recipes.ts +3 -2
- package/lib/jobs/scheduler.ts +215 -11
- package/lib/jobs/store.ts +79 -3
- package/lib/jobs/types.ts +31 -0
- package/lib/logger.ts +1 -1
- package/lib/notify.ts +13 -6
- package/lib/pipeline-gc.ts +105 -0
- package/lib/pipeline-scheduler.ts +29 -0
- package/lib/pipeline.ts +811 -330
- package/lib/schedules/action-runner.ts +257 -0
- package/lib/schedules/scheduler.ts +422 -0
- package/lib/schedules/state.ts +41 -0
- package/lib/schedules/store.ts +618 -0
- package/lib/schedules/types.ts +117 -0
- package/lib/settings.ts +35 -0
- package/lib/task-manager.ts +56 -13
- package/lib/workflow-marketplace.ts +7 -1
- package/lib/workspace/skill-installer.ts +7 -6
- package/package.json +3 -1
- package/lib/help-docs/19-jobs.md +0 -145
- package/lib/help-docs/20-mantis-bug-fix.md +0 -115
- package/lib/help-docs/22-recipes.md +0 -124
|
@@ -12,6 +12,21 @@ description: "What this workflow does"
|
|
|
12
12
|
input:
|
|
13
13
|
feature: "Feature description" # required input fields
|
|
14
14
|
priority: "Priority level (optional)"
|
|
15
|
+
# OR extended spec when you want richer Schedule UI controls:
|
|
16
|
+
# bug_id:
|
|
17
|
+
# description: "Mantis bug id"
|
|
18
|
+
# type: integer # string | integer | number | boolean | enum
|
|
19
|
+
# required: true
|
|
20
|
+
# default: 0
|
|
21
|
+
# resolution:
|
|
22
|
+
# type: enum
|
|
23
|
+
# enum: [fixed, wont_fix, duplicate]
|
|
24
|
+
# default: fixed
|
|
25
|
+
# user_prompt:
|
|
26
|
+
# description: "Focus the fix on…"
|
|
27
|
+
# multiline: true
|
|
28
|
+
# Both forms can be mixed in the same input: block. Pipeline runtime
|
|
29
|
+
# treats every value as a string template variable regardless of type.
|
|
15
30
|
vars:
|
|
16
31
|
project: my-app # default variables
|
|
17
32
|
nodes:
|
|
@@ -69,6 +84,94 @@ nodes:
|
|
|
69
84
|
|
|
70
85
|
**Shell escaping**: Template values in shell mode are automatically escaped (single quotes `'` → `'\''`) to prevent injection.
|
|
71
86
|
|
|
87
|
+
## Authoring discipline — common YAML/shell footguns
|
|
88
|
+
|
|
89
|
+
Pipeline yaml mixes two languages (YAML block scalars + bash) and most of
|
|
90
|
+
the production bugs we've hit live in their interaction. The 3 rules below
|
|
91
|
+
prevent ~90% of them. Follow them whenever you write or modify a `mode:
|
|
92
|
+
shell` node — they cost nothing and save retries.
|
|
93
|
+
|
|
94
|
+
### Rule 1 — `mode: shell` prompts use `|`, never `>`
|
|
95
|
+
|
|
96
|
+
YAML has two block scalar styles, and they treat newlines very differently:
|
|
97
|
+
|
|
98
|
+
- **`prompt: |` (literal)** — newlines preserved verbatim. Bash receives the
|
|
99
|
+
script exactly as written. **Always use this for shell scripts.**
|
|
100
|
+
- **`prompt: >` (folded)** — adjacent same-indent lines are joined into ONE
|
|
101
|
+
line separated by a single space. Only blank lines preserve real
|
|
102
|
+
newlines. In shell scripts this silently produces things like
|
|
103
|
+
`STAGE_LABEL="x" if [ ... ]; then` (two statements glued together with a
|
|
104
|
+
space), causing `syntax error near unexpected token 'fi'` at runtime.
|
|
105
|
+
|
|
106
|
+
If you see an existing node using `>` that already works, leave it — it
|
|
107
|
+
relies on the discipline of blank lines between every statement. But for
|
|
108
|
+
any **new** node or **substantial rewrite**, use `|`. Don't sprinkle
|
|
109
|
+
shell into existing `>` nodes without re-checking blank-line spacing.
|
|
110
|
+
|
|
111
|
+
### Rule 2 — Don't use `eval $(echo "$X" | sed 's/^/export /')` to import env vars
|
|
112
|
+
|
|
113
|
+
This idiom looks innocent but breaks the moment any value contains a space,
|
|
114
|
+
colon, or hyphen — bash word-splits the joined string and tries to
|
|
115
|
+
`export` each word as a separate identifier, hitting things like
|
|
116
|
+
`export: 'pre-review': not a valid identifier`.
|
|
117
|
+
|
|
118
|
+
Use the safer form:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
while IFS='=' read -r __k __v; do
|
|
122
|
+
[[ "$__k" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] && export "$__k=$__v"
|
|
123
|
+
done <<< "$VAR"
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
This reads key=value pairs line by line, accepts any value content
|
|
127
|
+
(including spaces / colons / newlines), and only exports lines that have
|
|
128
|
+
a valid identifier as key.
|
|
129
|
+
|
|
130
|
+
### Rule 3 — Don't put `cat <<EOF` heredocs inside `$(...)` when content can contain `` ` `` or `'`
|
|
131
|
+
|
|
132
|
+
Bash's `$(...)` parser scans the body for nested backticks and apostrophes
|
|
133
|
+
**even when the heredoc body is quoted (`<<'EOF'`)**. If the substituted
|
|
134
|
+
content has markdown code spans like `` `GroupManager.getX()` `` or
|
|
135
|
+
contractions like `FortiGate's`, parsing crashes with `syntax error near
|
|
136
|
+
unexpected token ')'`.
|
|
137
|
+
|
|
138
|
+
Use a file detour:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
__FORGE_F=/tmp/forge-something.$$
|
|
142
|
+
cat <<'__FORGE_MARK_x7k9z2q4__' > "$__FORGE_F"
|
|
143
|
+
{{raw:nodes.X.outputs.Y}}
|
|
144
|
+
__FORGE_MARK_x7k9z2q4__
|
|
145
|
+
VAR=$(< "$__FORGE_F")
|
|
146
|
+
rm -f "$__FORGE_F"
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
The `cat <<EOF > file` is OUTSIDE any `$(...)`, so bash never parses the
|
|
150
|
+
content. `$(< file)` reads the file as a whole, also no parsing. The
|
|
151
|
+
`{{raw:...}}` template variant skips Forge's ANSI-C shell escape so
|
|
152
|
+
multi-line content keeps real newlines.
|
|
153
|
+
|
|
154
|
+
### Rule 4 — Prefer adding new nodes over editing existing ones
|
|
155
|
+
|
|
156
|
+
If a pipeline is already working in production, prefer:
|
|
157
|
+
- **Adding a new node** that runs alongside / after the working ones
|
|
158
|
+
- **Composing in a new `mode: shell` block** that calls into the existing
|
|
159
|
+
outputs via `{{nodes.X.outputs.Y}}`
|
|
160
|
+
|
|
161
|
+
Over:
|
|
162
|
+
- Splicing new shell statements into the middle of an existing prompt
|
|
163
|
+
block (which forces the YAML scalar to be re-parsed and can trigger
|
|
164
|
+
hidden rule-1 / rule-2 problems)
|
|
165
|
+
|
|
166
|
+
This keeps the blast radius of each change to one new node.
|
|
167
|
+
|
|
168
|
+
### Quick mental checklist before editing any `prompt:` block
|
|
169
|
+
|
|
170
|
+
1. Is this `|` or `>`? (decide indent + blank-line strategy)
|
|
171
|
+
2. Are there ANY `eval ... | sed export` patterns nearby? (replace with the safer `while IFS='=' read` form)
|
|
172
|
+
3. Are there `cat <<EOF` inside `$(...)`? (move to the file-detour pattern)
|
|
173
|
+
4. Could I add a new node instead of modifying this one?
|
|
174
|
+
|
|
72
175
|
## Template Variables
|
|
73
176
|
|
|
74
177
|
Templates use `{{...}}` syntax and are resolved before execution:
|
|
@@ -76,6 +179,74 @@ Templates use `{{...}}` syntax and are resolved before execution:
|
|
|
76
179
|
- `{{input.xxx}}` — pipeline input values provided at trigger time
|
|
77
180
|
- `{{vars.xxx}}` — workflow-level variables defined in YAML
|
|
78
181
|
- `{{nodes.<node_id>.outputs.<output_name>}}` — outputs from completed nodes
|
|
182
|
+
- `{{run.tmp_dir}}` — per-run scratch dir (see "Per-run scratch dir" below)
|
|
183
|
+
- `{{raw:...}}` — prefix that bypasses shell-mode ANSI-C escaping (use inside quoted heredocs / files / jq stdin so multi-line content keeps real newlines)
|
|
184
|
+
|
|
185
|
+
## Per-run scratch dir (`{{run.tmp_dir}}`)
|
|
186
|
+
|
|
187
|
+
Each pipeline run gets a private scratch directory at:
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
<project_dir>/.forge/worktrees/pipeline-<runId>/
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
`<project_dir>` is resolved from the pipeline's `{{input.project}}` (the
|
|
194
|
+
input parameter most Forge pipelines already declare). If the pipeline
|
|
195
|
+
has no `input.project`, the template renders as empty string and you
|
|
196
|
+
should fall back to OS `/tmp/` for scratch.
|
|
197
|
+
|
|
198
|
+
**Use this instead of `/tmp/`** for any per-run scratch files. Three big
|
|
199
|
+
wins over OS /tmp:
|
|
200
|
+
|
|
201
|
+
1. **Per-run isolation** — different pipeline runs never collide on the
|
|
202
|
+
same file, even if both are processing the same MR / bug id.
|
|
203
|
+
2. **Bulk cleanup** — the whole dir is `rm -rf`'d when GC fires; no
|
|
204
|
+
need to track individual `/tmp/foo-12345-bar` filenames.
|
|
205
|
+
3. **Survives reboot** — `~/.../.forge/worktrees/` is on persistent disk;
|
|
206
|
+
OS `/tmp/` is wiped at boot, taking your failed-run forensics with it.
|
|
207
|
+
|
|
208
|
+
### Lifecycle
|
|
209
|
+
|
|
210
|
+
| Status | When tmp_dir is wiped |
|
|
211
|
+
|---|---|
|
|
212
|
+
| `done` | Immediately when status flips (settings.pipelineTmpCleanDoneImmediate) |
|
|
213
|
+
| `failed` | After `settings.pipelineTmpKeepFailedDays` (default 3) — by background GC |
|
|
214
|
+
| `cancelled` | After `settings.pipelineTmpKeepCancelledDays` (default 3) — by background GC |
|
|
215
|
+
| `running` | Never (auto-extends as long as the pipeline is alive) |
|
|
216
|
+
| (orphan, no state)| After 7 days, by background GC |
|
|
217
|
+
|
|
218
|
+
GC runs every `settings.pipelineTmpGcIntervalHours` (default 6h). Run
|
|
219
|
+
on demand with `forge pipeline gc [--dry-run]`.
|
|
220
|
+
|
|
221
|
+
### Convention — sub-dirs per item
|
|
222
|
+
|
|
223
|
+
When a pipeline processes multiple items (MRs, Mantis bugs, etc.),
|
|
224
|
+
create a sub-dir per item inside `{{run.tmp_dir}}` so individual items
|
|
225
|
+
are clearly isolated. Naming:
|
|
226
|
+
|
|
227
|
+
| Item type | Sub-dir |
|
|
228
|
+
|---|---|
|
|
229
|
+
| GitLab MR | `mr-<iid>/` |
|
|
230
|
+
| Mantis bug | `mantis-<bug_id>/` |
|
|
231
|
+
| Generic id | `<kind>-<id>/` |
|
|
232
|
+
|
|
233
|
+
Even for single-item pipelines (one bug per run) use the same convention
|
|
234
|
+
— makes future batch versions cleanly composable.
|
|
235
|
+
|
|
236
|
+
Example (shell node):
|
|
237
|
+
|
|
238
|
+
```yaml
|
|
239
|
+
prompt: |
|
|
240
|
+
set -e
|
|
241
|
+
MR_DIR="{{run.tmp_dir}}/mr-${MR_IID}"
|
|
242
|
+
mkdir -p "$MR_DIR"
|
|
243
|
+
glab api ... > "$MR_DIR/meta.json"
|
|
244
|
+
glab api ... > "$MR_DIR/diff.json"
|
|
245
|
+
# ...
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
The framework creates `pipeline-<runId>/` for you; you only have to
|
|
249
|
+
`mkdir -p` your own sub-dir.
|
|
79
250
|
|
|
80
251
|
Node IDs can contain hyphens (e.g., `{{nodes.fetch-issue.outputs.data}}`).
|
|
81
252
|
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# Schedules
|
|
2
|
+
|
|
3
|
+
**Schedule = a pipeline + input + trigger.** Forge runs the named pipeline with
|
|
4
|
+
your input whenever the trigger fires. The pipeline is the only execution unit;
|
|
5
|
+
Schedule just decides *when* to fire.
|
|
6
|
+
|
|
7
|
+
**Note:** Jobs (the older subsystem) is deprecated — its UI is hidden and the
|
|
8
|
+
scheduler no longer ticks. Any existing rows in the `jobs` table are inert.
|
|
9
|
+
Use Schedules for all timed automation. If a pipeline needs iteration, write
|
|
10
|
+
it inside the pipeline yaml using `for_each:` — Schedule just fires the
|
|
11
|
+
pipeline; iteration logic stays in the workflow.
|
|
12
|
+
|
|
13
|
+
## Creating a Schedule (3 steps)
|
|
14
|
+
|
|
15
|
+
Open **Schedules** tab → **+ New schedule** in Forge web or extension:
|
|
16
|
+
|
|
17
|
+
1. **Pick a pipeline** from the list. Each pipeline shows its description.
|
|
18
|
+
2. **Fill input**. The form depends on body kind:
|
|
19
|
+
- **Pipeline** body: auto-renders from the pipeline's declared `input:`
|
|
20
|
+
block. Required fields are marked `*`. Long-form fields render as textarea.
|
|
21
|
+
- **Skill** body: a fixed two-field form — `project` (which Forge project
|
|
22
|
+
the skill runs in) and `user_prompt` (the user message sent to Claude).
|
|
23
|
+
The skill is loaded via `--append-system-prompt`.
|
|
24
|
+
- **Connector Tool** body: a JSON textarea passed verbatim as the
|
|
25
|
+
tool's input. The tool's `input_schema` (if declared) is shown
|
|
26
|
+
below the editor for reference.
|
|
27
|
+
3. **Name, trigger, action**:
|
|
28
|
+
- **Trigger**:
|
|
29
|
+
- `Every N minutes` — period polling
|
|
30
|
+
- `Cron` — full cron expression (e.g. `0 9 * * 1-5` = 9am Mon-Fri)
|
|
31
|
+
- `Once at <time>` — single shot, auto-disables after firing
|
|
32
|
+
- `Manual only` — never auto-fires; user must click Run now
|
|
33
|
+
- **Action** (what to do with the body's output):
|
|
34
|
+
- `None` (default) — body self-handles its output; nothing else fires
|
|
35
|
+
- `Chat` — append the body output to a Chat session as a synthetic
|
|
36
|
+
assistant message. Configure `session_id` (pick from the dropdown)
|
|
37
|
+
and an optional `prefix`. The message header is auto-prepended with
|
|
38
|
+
the schedule name + fire time so the recipient can tell where it
|
|
39
|
+
came from.
|
|
40
|
+
- `Email` — send the body output via SMTP. Configure SMTP transport
|
|
41
|
+
once in `~/.forge/data/settings.yaml`:
|
|
42
|
+
|
|
43
|
+
```yaml
|
|
44
|
+
smtpHost: smtp.gmail.com
|
|
45
|
+
smtpPort: 587
|
|
46
|
+
smtpSecure: false # true for 465 (implicit TLS), false for 587 (STARTTLS)
|
|
47
|
+
smtpUser: forge@example.com
|
|
48
|
+
smtpPassword: <app-password> # encrypted at rest
|
|
49
|
+
smtpFrom: Forge <forge@example.com>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Per-schedule action config:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
to alice@example.com (or list)
|
|
56
|
+
subject_template "Daily bugs — {date}"
|
|
57
|
+
body_template "{body_output}"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Templates support `{date}` (YYYY-MM-DD) and `{body_output}`.
|
|
61
|
+
Requires `nodemailer` installed in the Forge repo
|
|
62
|
+
(`pnpm add nodemailer`).
|
|
63
|
+
- `Telegram` — post the body output via the same Telegram bot
|
|
64
|
+
Forge uses for task notifications. Reuses `telegramBotToken`
|
|
65
|
+
from Settings. `chat_id` is optional in the action config —
|
|
66
|
+
blank falls back to `settings.telegramChatId`. Optional
|
|
67
|
+
`prefix` is prepended; default `parse_mode: Markdown`.
|
|
68
|
+
|
|
69
|
+
Actions fire only when the body completes with status `done` and
|
|
70
|
+
produces non-empty output (unless `action_skip_on_empty` is false).
|
|
71
|
+
The `schedule_runs.action_status` column records `done | failed |
|
|
72
|
+
skipped` independently of the body status.
|
|
73
|
+
|
|
74
|
+
## Schedule states
|
|
75
|
+
|
|
76
|
+
Computed from `enabled` + inflight pipelines + last run status:
|
|
77
|
+
|
|
78
|
+
| State | Meaning | UI |
|
|
79
|
+
|---|---|---|
|
|
80
|
+
| `idle` | Enabled, nothing running, last run OK (or never run) | green |
|
|
81
|
+
| `running` | Enabled, ≥1 pipeline this schedule launched is in-flight | orange (pulsing) |
|
|
82
|
+
| `last_failed` | Enabled, last run ended in failure | red |
|
|
83
|
+
| `paused` | `enabled=false` | gray |
|
|
84
|
+
|
|
85
|
+
A paused Schedule with inflight pipelines shows `paused` plus a count badge
|
|
86
|
+
("3 still running") so you know the running work is still going even though
|
|
87
|
+
no new pipelines will be triggered.
|
|
88
|
+
|
|
89
|
+
## Actions
|
|
90
|
+
|
|
91
|
+
The action bar adapts to current state:
|
|
92
|
+
|
|
93
|
+
| Button | Effect |
|
|
94
|
+
|---|---|
|
|
95
|
+
| **Run now** | Manually fire — `trigger='manual'`. If something's already running, asks to cancel-and-rerun |
|
|
96
|
+
| **Cancel** | Kill the in-flight pipelines this Schedule launched. Does NOT change `enabled` |
|
|
97
|
+
| **Pause** | Set `enabled=false`. Running pipelines keep going |
|
|
98
|
+
| **Resume** | Set `enabled=true` (only shown when paused and nothing inflight) |
|
|
99
|
+
| **Stop** | Pause + Cancel in one shot — full halt |
|
|
100
|
+
| **Edit** | Modify input / trigger / name. Pipeline can't be changed (create a new Schedule) |
|
|
101
|
+
| **Delete** | Remove the Schedule. Optionally cancel running pipelines too |
|
|
102
|
+
|
|
103
|
+
## Tracked pipelines
|
|
104
|
+
|
|
105
|
+
Expanding a Schedule row shows the pipelines it has launched. Click any row to
|
|
106
|
+
open the Pipeline view for node-level details. The list shows current running
|
|
107
|
+
pipelines plus the most recent completed one (full history is in the pipeline
|
|
108
|
+
view).
|
|
109
|
+
|
|
110
|
+
## API
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
GET /api/schedules list + decorate state
|
|
114
|
+
POST /api/schedules create
|
|
115
|
+
GET /api/schedules/<id> single
|
|
116
|
+
PATCH /api/schedules/<id> update input/trigger/name/enabled
|
|
117
|
+
DELETE /api/schedules/<id>?cancel_inflight=1 remove
|
|
118
|
+
|
|
119
|
+
POST /api/schedules/<id>/run?cancel_inflight=1 manual fire
|
|
120
|
+
POST /api/schedules/<id>/cancel kill all inflight
|
|
121
|
+
POST /api/schedules/<id>/stop pause + cancel
|
|
122
|
+
GET /api/schedules/<id>/runs?limit=20 history
|
|
123
|
+
|
|
124
|
+
GET /api/pipelines/<name>/schema input fields (for UI form)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Storage
|
|
128
|
+
|
|
129
|
+
- `schedules` — configuration. Key fields:
|
|
130
|
+
- `body_kind` (`pipeline | skill | connector_tool`) + `body_ref` —
|
|
131
|
+
what this schedule fires
|
|
132
|
+
- `input` (JSON) — params for the body
|
|
133
|
+
- `skills` (JSON `string[]`) — extra Forge skills attached to the
|
|
134
|
+
dispatched body. Forwarded as `--append-system-prompt` so Claude
|
|
135
|
+
sees "use /<skill> as appropriate"; missing skills auto-installed
|
|
136
|
+
into the target project at dispatch (same plumbing as `Job.skills`).
|
|
137
|
+
Ignored for `body_kind=connector_tool` (no Claude task spawned).
|
|
138
|
+
For `body_kind=skill`, merged with `body_ref` (de-duped).
|
|
139
|
+
- `action_kind` (`none | chat | email | telegram`) + `action_config`
|
|
140
|
+
(JSON) — what to do with the body's output. **Phase 1 only ships
|
|
141
|
+
`body_kind=pipeline` and `action_kind=none`**; other kinds land in
|
|
142
|
+
later phases.
|
|
143
|
+
- `schedule_kind` + interval/at/cron — trigger
|
|
144
|
+
- `schedule_runs` — every body a Schedule has launched:
|
|
145
|
+
- `target_id` — pipeline_id (or future task/dispatch id)
|
|
146
|
+
- `status` — body status (`started / done / failed / cancelled`)
|
|
147
|
+
- `body_output` — captured body output, fed into action
|
|
148
|
+
- `action_status` — independent of body status
|
|
149
|
+
|
|
150
|
+
V1 (`pipeline_schedules` / `pipeline_schedule_runs` tables, with
|
|
151
|
+
`pipeline_name` column) is auto-migrated into V2 on first boot.
|
|
152
|
+
|
|
153
|
+
## Scheduler tick
|
|
154
|
+
|
|
155
|
+
A 60s timer runs in `lib/schedules/scheduler.ts`. Each tick:
|
|
156
|
+
|
|
157
|
+
1. Reconcile any stale "started" runs against the actual pipeline JSON state
|
|
158
|
+
2. For every enabled, non-manual Schedule whose `next_run_at <= now`:
|
|
159
|
+
- Advance `next_run_at` for the next firing
|
|
160
|
+
- Skip if this Schedule already has an inflight pipeline
|
|
161
|
+
- Otherwise call `startPipeline(pipeline_name, input)` and write a
|
|
162
|
+
`pipeline_schedule_runs` row
|
|
163
|
+
|
|
164
|
+
No sequential / on_failure / dedup / retry logic at the Schedule level — those
|
|
165
|
+
are pipeline responsibilities.
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# Automation state machine — labels, comments, statuses
|
|
2
|
+
|
|
3
|
+
This page documents what Forge's automation pipelines write to external
|
|
4
|
+
systems (GitLab, Mantis, Teams, Forge chat). Use it as the single reference
|
|
5
|
+
when wiring new schedules, triaging an MR, or auditing what Forge has
|
|
6
|
+
already done.
|
|
7
|
+
|
|
8
|
+
Three pipelines operate on a Mantis bug → GitLab MR pair:
|
|
9
|
+
|
|
10
|
+
| Pipeline | Triggered by | Touches |
|
|
11
|
+
|-----------------------------|---------------------------|----------------------|
|
|
12
|
+
| `fortinet-mantis-bug-fix` | Manual fire / Schedule | Mantis + GitLab |
|
|
13
|
+
| `fortinet-mr-pre-review` | Manual fire / Schedule | GitLab |
|
|
14
|
+
| `fortinet-mr-review` | Manual fire / Schedule | GitLab (+ Teams) |
|
|
15
|
+
|
|
16
|
+
## Stage labels (mutually exclusive)
|
|
17
|
+
|
|
18
|
+
Every pipeline marks the GitLab MR with a `forge:stage/*` label when it
|
|
19
|
+
finishes. The labels are mutually exclusive — each pipeline removes the
|
|
20
|
+
others before adding its own. Reading the label tells you "how far through
|
|
21
|
+
the automation funnel" the MR is.
|
|
22
|
+
|
|
23
|
+
| Stage label | Set by | Meaning |
|
|
24
|
+
|------------------------------|------------------------------|---------------------------------------------------------|
|
|
25
|
+
| `forge:stage/needs-review` | `fortinet-mantis-bug-fix` | MR just created by Forge; nobody has reviewed it yet. |
|
|
26
|
+
| `forge:stage/pre-reviewed` | `fortinet-mr-pre-review` | Forge posted an AI pre-review comment. |
|
|
27
|
+
| `forge:stage/reviewed` | `fortinet-mr-review` | Forge ran a full review round (replied / labelled / closed). |
|
|
28
|
+
|
|
29
|
+
**One-time setup per GitLab project:** create those three labels in the
|
|
30
|
+
project's Labels page. Pipelines call `glab api ... add_labels=...`; the
|
|
31
|
+
label has to exist or the API call silently fails (best-effort — no error
|
|
32
|
+
escalation, just `STAGE_LABEL=failed` in the run output).
|
|
33
|
+
|
|
34
|
+
## Verdict labels (orthogonal to stage labels)
|
|
35
|
+
|
|
36
|
+
`fortinet-mr-review` also adds a label that describes its **decision**.
|
|
37
|
+
These can co-exist with stage labels.
|
|
38
|
+
|
|
39
|
+
| Verdict label | When |
|
|
40
|
+
|------------------------|---------------------------------------------------------------------|
|
|
41
|
+
| `forge:fix-applied` | Triage said ACT, Claude pushed a fix commit, MR updated. |
|
|
42
|
+
| `forge:no-fix-needed` | Triage said SKIP (e.g. style-only review notes — no action needed). |
|
|
43
|
+
| `forge:no-merge` | Triage said CLOSE — MR is invalid; pipeline also closes the MR. |
|
|
44
|
+
|
|
45
|
+
## Per-pipeline detail
|
|
46
|
+
|
|
47
|
+
### 1. `fortinet-mantis-bug-fix`
|
|
48
|
+
|
|
49
|
+
Input: Mantis bug id (+ project). Walks the bug, opens a worktree on the
|
|
50
|
+
target branch, runs Claude to write a fix, pushes, opens MR, then
|
|
51
|
+
post-processes both sides.
|
|
52
|
+
|
|
53
|
+
| Step | Side effect |
|
|
54
|
+
|---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
55
|
+
| `push-and-mr` | Creates GitLab MR (or finds existing for the branch). **Sets `forge:stage/needs-review`** + removes other `forge:stage/*` labels. |
|
|
56
|
+
| `mark-fixed` | Mantis **resolution → fixed**. Status (e.g. New / Assigned / Resolved) is left untouched — reviewer of the MR decides. Best-effort via Mantis connector + `connector-tool` bridge. |
|
|
57
|
+
| `notify-mantis` | Adds a Mantis **bugnote** containing the MR URL, branch, and Claude's fix summary. Format starts with "Auto-fix applied — MR opened." and ends with `ref: autofix-<bug_id>`. Best-effort. |
|
|
58
|
+
| `notify-teams` | Posts a Teams chat message to the configured user/group with MR URL + summary. Best-effort. |
|
|
59
|
+
| `cleanup` | Removes the temporary worktree. |
|
|
60
|
+
|
|
61
|
+
If `push-and-mr` finds no diff to commit (Claude bailed without changes),
|
|
62
|
+
`mark-fixed` skips (no resolution change), and `notify-mantis` posts a
|
|
63
|
+
**"Auto-fix attempted — no fix applied"** bugnote instead. So Mantis is
|
|
64
|
+
always notified one way or the other.
|
|
65
|
+
|
|
66
|
+
### 2. `fortinet-mr-pre-review`
|
|
67
|
+
|
|
68
|
+
Input: GitLab MR URL. Fetches metadata + diff + discussion, runs Claude
|
|
69
|
+
to draft a short analysis, posts ONE comment on the MR.
|
|
70
|
+
|
|
71
|
+
| Step | Side effect |
|
|
72
|
+
|----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
73
|
+
| `ingest` | Fetches MR meta / diff / notes. **Skip detection:** finds the last Forge pre-review note (body starts with `🤖 Forge pre-review:`); if there are no new commits AND no new non-forge notes since, sets `SKIP_PRE_REVIEW=yes`. Downstream steps short-circuit. |
|
|
74
|
+
| `analyze` | Claude produces VERDICT (`ready-to-merge` / `needs-review`) + FINDINGS + REPLY DRAFT. |
|
|
75
|
+
| `post-comment` | Posts one GitLab note. First line is always `**Recommendation:** ✅ Looks safe to merge.` or `**Recommendation:** ⚠️ Needs another review — <reason>.`. **Sets `forge:stage/pre-reviewed`** + removes other `forge:stage/*`. On `SKIP_PRE_REVIEW=yes`, doesn't post or touch labels. |
|
|
76
|
+
| `notify-chat` | Opens a Forge chat session seeded with the analysis (or "skipped" if skip path). |
|
|
77
|
+
|
|
78
|
+
The skip path means re-running pre-review (manually or on a schedule)
|
|
79
|
+
won't spam the MR with duplicate comments. Adding any commit or non-forge
|
|
80
|
+
note to the MR re-arms the pipeline.
|
|
81
|
+
|
|
82
|
+
### 3. `fortinet-mr-review`
|
|
83
|
+
|
|
84
|
+
Input: GitLab MR URL + reviewer notes. Triages the discussion, optionally
|
|
85
|
+
fixes code + pushes, replies on the MR.
|
|
86
|
+
|
|
87
|
+
| Step | Side effect |
|
|
88
|
+
|------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
89
|
+
| `ingest` | Fetches MR + reviewer notes since last Forge reply. |
|
|
90
|
+
| `triage` | Claude decides one of: `ACT` (push a fix), `SKIP` (no action), `CLOSE` (abandon the MR), `NOOP` (no new reviewer feedback, break self-reply loop). |
|
|
91
|
+
| `fix` | (ACT only) Claude edits + commits + pushes to the MR branch. |
|
|
92
|
+
| `reply` | Posts one GitLab note (`🤖 Forge: <RESULT block>`). Then: <ul><li>**Verdict label** by status: `pushed` → `forge:fix-applied`, `skipped` → `forge:no-fix-needed`, `close` → `forge:no-merge` + closes the MR.</li><li>**Stage label** (every status): `forge:stage/reviewed` + removes other `forge:stage/*`.</li></ul> |
|
|
93
|
+
| `cleanup` | Removes the temporary worktree; posts to Teams. |
|
|
94
|
+
|
|
95
|
+
`NOOP` doesn't touch labels or post anything — it exists purely to avoid
|
|
96
|
+
the pipeline replying to its own previous reply.
|
|
97
|
+
|
|
98
|
+
## Comments — who posts what
|
|
99
|
+
|
|
100
|
+
Every Forge-generated comment has a recognisable prefix so you can grep /
|
|
101
|
+
filter them in the MR or in Mantis:
|
|
102
|
+
|
|
103
|
+
| Source | Where | Prefix |
|
|
104
|
+
|-------------------------------------------|-------------------|-------------------------------------|
|
|
105
|
+
| `fortinet-mantis-bug-fix` → notify-mantis | Mantis bugnote | `Auto-fix applied — MR opened.` / `Auto-fix attempted — no fix applied.` (ends with `ref: autofix-<bug_id>`) |
|
|
106
|
+
| `fortinet-mr-pre-review` → post-comment | GitLab MR note | `🤖 Forge pre-review:` |
|
|
107
|
+
| `fortinet-mr-review` → reply | GitLab MR note | `🤖 Forge:` (followed by RESULT block) |
|
|
108
|
+
|
|
109
|
+
## Mantis side
|
|
110
|
+
|
|
111
|
+
| What | Set by |
|
|
112
|
+
|----------------------------|-----------------------------------------------------------------------------------------------------------------------|
|
|
113
|
+
| **Resolution → fixed** | `fortinet-mantis-bug-fix` → `mark-fixed`, only when an MR was actually created. Best-effort via Mantis connector. |
|
|
114
|
+
| Bugnote with MR + summary | `fortinet-mantis-bug-fix` → `notify-mantis`. Always posts (one variant if MR created, another "no fix applied" variant if Claude bailed). |
|
|
115
|
+
|
|
116
|
+
Mantis **status** is intentionally never auto-changed — the human reviewer
|
|
117
|
+
who merges the MR decides whether the bug moves to Resolved / Closed.
|
|
118
|
+
|
|
119
|
+
## Teams side
|
|
120
|
+
|
|
121
|
+
Both `fortinet-mantis-bug-fix` and `fortinet-mr-review` (via their final
|
|
122
|
+
`cleanup` / notify steps) send a Teams chat message with the MR URL and
|
|
123
|
+
brief summary, IF the Teams connector is configured. Best-effort — Teams
|
|
124
|
+
unreachable doesn't fail the pipeline.
|
|
125
|
+
|
|
126
|
+
## Forge chat seeds
|
|
127
|
+
|
|
128
|
+
`fortinet-mr-pre-review` → `notify-chat` opens a new Forge chat session
|
|
129
|
+
seeded with the analysis + four next-step options (Approve / Request
|
|
130
|
+
changes / Look closer / Nothing). The chat session id is returned so a
|
|
131
|
+
Schedule action can re-use the same session or open a fresh one each run.
|
|
132
|
+
|
|
133
|
+
## Quick reference — "where is this MR right now?"
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
Look at the MR's labels:
|
|
137
|
+
|
|
138
|
+
forge:stage/needs-review → bug-fix pipeline just opened it
|
|
139
|
+
forge:stage/pre-reviewed → AI pre-review posted; awaiting human
|
|
140
|
+
forge:stage/reviewed → Forge did a full review round
|
|
141
|
+
(no forge:stage/* label) → not Forge-managed, or pre-stage-label era
|
|
142
|
+
|
|
143
|
+
forge:fix-applied → Forge pushed a fix commit in this round
|
|
144
|
+
forge:no-fix-needed → Forge looked, decided no action
|
|
145
|
+
forge:no-merge → Forge / triage decided to close
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Look at the latest comment for the actual analysis / reply text.
|
package/lib/help-docs/CLAUDE.md
CHANGED
|
@@ -49,13 +49,13 @@ The token is valid for 24 hours. Store it in a variable and reuse for all API ca
|
|
|
49
49
|
| `17-connectors.md` | Connectors — independent subsystem, marketplace fetched from `forge-connectors` registry, no built-ins. Manifest schema, HTTP API, install/update flow, pre-v0.9 migration. |
|
|
50
50
|
| `21-build-connector.md` | **Authoring** a custom connector — interview script, manifest template (browser / http / shell protocols), how to install locally via the Forge data dir or a zip upload. Use this when the user asks to BUILD a connector, not when they ask about an existing one. |
|
|
51
51
|
| `18-chrome-mcp.md` | Connect Forge Claude Code sessions to a real Chrome via chrome-devtools-mcp — dev-time browser access for connector authoring |
|
|
52
|
-
| `
|
|
53
|
-
| `20-mantis-bug-fix.md` | Mantis → Bug Fix → MR builtin pipeline (mantis-bug-fix-and-mr) |
|
|
54
|
-
| `22-recipes.md` | Recipes (parameterized Job templates) + the `mr-review-fix` pipeline — gitlab_mr_url param, 4-action triage (act/skip/close/noop), GitLab label setup, per-MR vs per-comment dispatch, Marketplace ↔ local-copy split |
|
|
52
|
+
| `23-automation-states.md` | Fortinet pipeline automation: GitLab MR stage labels, Mantis status flow, Teams notify policy |
|
|
55
53
|
|
|
56
54
|
## Matching questions to docs
|
|
57
55
|
|
|
58
56
|
- Pipeline/workflow/DAG/YAML → `05-pipelines.md`
|
|
57
|
+
- Writing / editing a pipeline YAML / "Claude wrote a node that fails with `syntax error near unexpected token`" / `prompt: >` vs `prompt: |` / `eval ... | sed export` / `cat <<EOF` inside `$()` → `05-pipelines.md` (Authoring discipline section)
|
|
58
|
+
- Pipeline scratch dir / `{{run.tmp_dir}}` / per-run isolation / `/tmp/` vs `.forge/worktrees/pipeline-<id>` / pipeline GC / `forge pipeline gc` / where to put node scratch files → `05-pipelines.md` (Per-run scratch dir section)
|
|
59
59
|
- Issue/PR/auto-fix → `09-issue-autofix.md` + `05-pipelines.md`
|
|
60
60
|
- Telegram/notification → `02-telegram.md`
|
|
61
61
|
- Tunnel/remote/cloudflare → `03-tunnel.md`
|
|
@@ -80,6 +80,6 @@ The token is valid for 24 hours. Store it in a variable and reuse for all API ca
|
|
|
80
80
|
- Connector/connector marketplace/install connector/forge-connectors registry/Mantis manifest → `17-connectors.md`
|
|
81
81
|
- Build / author / write / create a new connector / "make me a connector for X" / custom connector → `21-build-connector.md`
|
|
82
82
|
- Chrome MCP / chrome-devtools-mcp / dev-time browser / CDP / remote debugging → `18-chrome-mcp.md`
|
|
83
|
-
- Job / scheduled job / connector poll /
|
|
84
|
-
-
|
|
85
|
-
-
|
|
83
|
+
- Job / scheduled job / connector poll / "Jobs tab" → tell user: **Jobs is deprecated**; use Schedules (`13-schedules.md`) instead.
|
|
84
|
+
- Recipe / "From recipe" form / parameterized job → tell user: **recipes deprecated** along with Jobs; fire pipelines manually or via Schedules.
|
|
85
|
+
- Mantis bug fix / fortinet-mantis-bug-fix / open MR for Mantis bug / fortinet-mr-review / pre-review / GitLab stage labels → `23-automation-states.md` (kept Fortinet pipelines)
|
package/lib/init.ts
CHANGED
|
@@ -178,6 +178,21 @@ export function ensureInitialized() {
|
|
|
178
178
|
// Task runner is safe in every worker (DB-level coordination)
|
|
179
179
|
time('ensureRunnerStarted', ensureRunnerStarted);
|
|
180
180
|
|
|
181
|
+
// Pipeline tmp-dir GC — scan project worktrees/pipeline-<id>/ + delete
|
|
182
|
+
// expired (failed/cancelled past retention). Interval from settings,
|
|
183
|
+
// clamped to >= 1h to avoid runaway IO. Runs in background only.
|
|
184
|
+
try {
|
|
185
|
+
const { gcPipelineTmp } = require('./pipeline-gc');
|
|
186
|
+
const { loadSettings: ls } = require('./settings');
|
|
187
|
+
const hours = Math.max(1, Number(ls().pipelineTmpGcIntervalHours) || 6);
|
|
188
|
+
setInterval(() => {
|
|
189
|
+
try {
|
|
190
|
+
const r = gcPipelineTmp();
|
|
191
|
+
if (r.removed.length) console.log(`[pipeline-gc] swept ${r.removed.length}/${r.scanned} dir(s)`);
|
|
192
|
+
} catch (e) { console.warn('[pipeline-gc] sweep failed:', (e as Error).message); }
|
|
193
|
+
}, hours * 60 * 60 * 1000);
|
|
194
|
+
} catch (e) { console.warn('[pipeline-gc] setup failed:', (e as Error).message); }
|
|
195
|
+
|
|
181
196
|
// Session watcher is safe (file-based, idempotent)
|
|
182
197
|
time('startWatcherLoop', startWatcherLoop);
|
|
183
198
|
|
|
@@ -189,12 +204,16 @@ export function ensureInitialized() {
|
|
|
189
204
|
} catch {}
|
|
190
205
|
});
|
|
191
206
|
|
|
192
|
-
// Jobs scheduler —
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
207
|
+
// Jobs scheduler — DEPRECATED, no longer started. Backend code remains
|
|
208
|
+
// (lib/jobs/, app/api/jobs/) for reversion / data inspection only.
|
|
209
|
+
// Any rows in the `jobs` table will NOT tick. Use Schedules instead.
|
|
210
|
+
|
|
211
|
+
// Schedules scheduler — newer pure-pipeline triggers. Runs parallel to Jobs.
|
|
212
|
+
time('schedules-scheduler', () => {
|
|
213
|
+
import('./schedules/scheduler').then(({ startSchedulesScheduler }) => {
|
|
214
|
+
try { startSchedulesScheduler(); }
|
|
215
|
+
catch (e) { console.error('[schedules-scheduler] start failed', e); }
|
|
216
|
+
}).catch((e) => console.error('[schedules-scheduler] import failed', e));
|
|
198
217
|
});
|
|
199
218
|
|
|
200
219
|
// If services are managed externally (forge-server), skip
|
package/lib/jobs/recipes.ts
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
* connector items yet; the scheduler later renders {{item.X}} per row.
|
|
33
33
|
*/
|
|
34
34
|
|
|
35
|
-
import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
35
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from 'node:fs';
|
|
36
36
|
import { join } from 'node:path';
|
|
37
37
|
import YAML from 'yaml';
|
|
38
38
|
import { homedir } from 'node:os';
|
|
@@ -124,7 +124,6 @@ export function deleteRecipe(name: string): boolean {
|
|
|
124
124
|
if (!NAME_RE.test(name)) return false;
|
|
125
125
|
const path = join(recipesDir(), `${name}.yaml`);
|
|
126
126
|
const altPath = join(recipesDir(), `${name}.yml`);
|
|
127
|
-
const { unlinkSync } = require('node:fs');
|
|
128
127
|
if (existsSync(path)) { unlinkSync(path); return true; }
|
|
129
128
|
if (existsSync(altPath)) { unlinkSync(altPath); return true; }
|
|
130
129
|
return false;
|
|
@@ -283,6 +282,8 @@ export function instantiateRecipe(name: string, rawParams: Record<string, any>):
|
|
|
283
282
|
max_per_tick: typeof rendered.max_per_tick === 'number'
|
|
284
283
|
? rendered.max_per_tick
|
|
285
284
|
: (rendered.max_per_tick != null && rendered.max_per_tick !== '' ? Number(rendered.max_per_tick) : undefined),
|
|
285
|
+
concurrency_mode: rendered.concurrency_mode === 'parallel' ? 'parallel' : undefined,
|
|
286
|
+
on_failure: rendered.on_failure === 'stop' ? 'stop' : undefined,
|
|
286
287
|
});
|
|
287
288
|
return { ok: true, job };
|
|
288
289
|
} catch (e) {
|