@aion0/forge 0.9.1 → 0.9.3

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 (71) hide show
  1. package/RELEASE_NOTES.md +5 -5
  2. package/app/api/agents/[id]/test/route.ts +150 -0
  3. package/app/api/connectors/[id]/sync-cli/route.ts +73 -0
  4. package/app/api/connectors/tool-test/route.ts +70 -0
  5. package/app/api/jobs/[id]/cancel/route.ts +50 -0
  6. package/app/api/jobs/[id]/dispatched-pipelines/route.ts +24 -0
  7. package/app/api/jobs/[id]/run/route.ts +22 -2
  8. package/app/api/jobs/route.ts +11 -1
  9. package/app/api/pipelines/[id]/schema/route.ts +53 -0
  10. package/app/api/pipelines/bulk-delete/route.ts +39 -0
  11. package/app/api/pipelines/gc/route.ts +27 -0
  12. package/app/api/schedules/[id]/cancel/route.ts +27 -0
  13. package/app/api/schedules/[id]/route.ts +173 -0
  14. package/app/api/schedules/[id]/run/route.ts +45 -0
  15. package/app/api/schedules/[id]/runs/route.ts +22 -0
  16. package/app/api/schedules/[id]/stop/route.ts +33 -0
  17. package/app/api/schedules/route.ts +175 -0
  18. package/app/api/tasks/bulk-delete/route.ts +47 -0
  19. package/bin/forge-server.mjs +22 -1
  20. package/cli/mw.mjs +186 -7657
  21. package/cli/mw.ts +26 -0
  22. package/components/ConnectorsPanel.tsx +46 -0
  23. package/components/Dashboard.tsx +23 -10
  24. package/components/JobsView.tsx +245 -6
  25. package/components/PipelineEditor.tsx +38 -1
  26. package/components/PipelineView.tsx +325 -4
  27. package/components/ScheduleCreateModal.tsx +1507 -0
  28. package/components/SchedulesView.tsx +605 -0
  29. package/components/SettingsModal.tsx +106 -0
  30. package/docs/Team-Workflow-Integration.md +487 -0
  31. package/docs/UI-Design-Brief-SidePanel.md +278 -0
  32. package/lib/__tests__/foreach-batch-yaml.test.ts +33 -0
  33. package/lib/__tests__/foreach-before.test.ts +201 -0
  34. package/lib/__tests__/foreach-parse.test.ts +114 -0
  35. package/lib/__tests__/foreach-snapshot.test.ts +112 -0
  36. package/lib/__tests__/foreach-source.test.ts +105 -0
  37. package/lib/__tests__/foreach-template.test.ts +112 -0
  38. package/lib/chat/agent-loop.ts +3 -3
  39. package/lib/chat-standalone.ts +26 -1
  40. package/lib/claude-process.ts +8 -5
  41. package/lib/connectors/sync.ts +8 -2
  42. package/lib/crypto.ts +1 -1
  43. package/lib/dirs.ts +22 -7
  44. package/lib/help-docs/05-pipelines.md +171 -0
  45. package/lib/help-docs/13-schedules.md +165 -0
  46. package/lib/help-docs/23-automation-states.md +148 -0
  47. package/lib/help-docs/CLAUDE.md +6 -6
  48. package/lib/init.ts +25 -6
  49. package/lib/jobs/recipes.ts +3 -2
  50. package/lib/jobs/scheduler.ts +215 -11
  51. package/lib/jobs/store.ts +79 -3
  52. package/lib/jobs/types.ts +31 -0
  53. package/lib/logger.ts +1 -1
  54. package/lib/notify.ts +13 -6
  55. package/lib/pipeline-gc.ts +105 -0
  56. package/lib/pipeline-scheduler.ts +29 -0
  57. package/lib/pipeline.ts +811 -330
  58. package/lib/schedules/action-runner.ts +257 -0
  59. package/lib/schedules/scheduler.ts +422 -0
  60. package/lib/schedules/state.ts +41 -0
  61. package/lib/schedules/store.ts +618 -0
  62. package/lib/schedules/types.ts +117 -0
  63. package/lib/settings.ts +35 -0
  64. package/lib/task-manager.ts +56 -13
  65. package/lib/telegram-bot.ts +9 -3
  66. package/lib/workflow-marketplace.ts +7 -1
  67. package/lib/workspace/skill-installer.ts +7 -6
  68. package/package.json +3 -1
  69. package/lib/help-docs/19-jobs.md +0 -145
  70. package/lib/help-docs/20-mantis-bug-fix.md +0 -115
  71. 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 /&lt;skill&gt; 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.
@@ -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
- | `19-jobs.md` | Jobs scheduled connector polls that dedup and fan out to Pipeline / Chat |
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 / dedup / periodic fetch / Teams poll / Mantis bug poll → `19-jobs.md`
84
- - Mantis bug fix pipeline / mantis-bug-fix-and-mr / open MR for Mantis bug / notify Teams from pipeline / connector-tool endpoint → `20-mantis-bug-fix.md`
85
- - Recipe / parameterized job / "From recipe" form / mr-review-fix / MR review auto-fix / triage instructions / act vs skip vs close vs noop / forge:no-fix-needed / forge:no-merge label / gitlab_mr_url param / per-MR vs per-comment / Marketplace local-only filter → `22-recipes.md`
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 — periodic connector polls that fan out to pipelines / chat
193
- time('jobs-scheduler', () => {
194
- try {
195
- const { startScheduler: startJobsScheduler } = require('./jobs/scheduler');
196
- startJobsScheduler();
197
- } catch (e) { console.error('[jobs-scheduler] start failed', e); }
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
@@ -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) {