@aion0/forge 0.6.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.forge/mcp.json +8 -0
- package/.forge/worktrees/pipeline-0a33c50d/lib/help-docs/01-settings.md +5 -5
- package/.forge/worktrees/pipeline-0a33c50d/lib/help-docs/07-projects.md +1 -1
- package/.forge/worktrees/pipeline-2ba01c10/lib/help-docs/01-settings.md +5 -5
- package/.forge/worktrees/pipeline-2ba01c10/lib/help-docs/07-projects.md +1 -1
- package/.forge/worktrees/pipeline-3156a8b3/lib/help-docs/01-settings.md +5 -5
- package/.forge/worktrees/pipeline-3156a8b3/lib/help-docs/07-projects.md +1 -1
- package/.forge/worktrees/pipeline-316c6574/lib/help-docs/01-settings.md +5 -5
- package/.forge/worktrees/pipeline-316c6574/lib/help-docs/07-projects.md +1 -1
- package/.forge/worktrees/pipeline-44a94121/lib/help-docs/01-settings.md +5 -5
- package/.forge/worktrees/pipeline-44a94121/lib/help-docs/07-projects.md +1 -1
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/01-settings.md +5 -5
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/07-projects.md +1 -1
- package/.forge/worktrees/pipeline-d1757a50/lib/help-docs/01-settings.md +5 -5
- package/.forge/worktrees/pipeline-d1757a50/lib/help-docs/07-projects.md +1 -1
- package/.forge/worktrees/pipeline-d59c2fe2/lib/help-docs/01-settings.md +5 -5
- package/.forge/worktrees/pipeline-d59c2fe2/lib/help-docs/07-projects.md +1 -1
- package/.forge/worktrees/pipeline-d6a6ef23/lib/help-docs/01-settings.md +5 -5
- package/.forge/worktrees/pipeline-d6a6ef23/lib/help-docs/07-projects.md +1 -1
- package/.forge/worktrees/pipeline-e7f78b7a/lib/help-docs/01-settings.md +5 -5
- package/.forge/worktrees/pipeline-e7f78b7a/lib/help-docs/07-projects.md +1 -1
- package/.forge/worktrees/pipeline-e97c13c7/lib/help-docs/01-settings.md +5 -5
- package/.forge/worktrees/pipeline-e97c13c7/lib/help-docs/07-projects.md +1 -1
- package/.forge/worktrees/pipeline-ecd7cb0f/lib/help-docs/01-settings.md +5 -5
- package/.forge/worktrees/pipeline-ecd7cb0f/lib/help-docs/07-projects.md +1 -1
- package/CLAUDE.md +2 -2
- package/RELEASE_NOTES.md +101 -5
- package/app/api/auth/check/route.ts +18 -0
- package/app/api/browser-bridge/route.ts +70 -0
- package/app/api/chat/sessions/[id]/events/route.ts +17 -0
- package/app/api/chat/sessions/[id]/fork/route.ts +15 -0
- package/app/api/chat/sessions/[id]/messages/route.ts +21 -0
- package/app/api/chat/sessions/[id]/route.ts +23 -0
- package/app/api/chat/sessions/route.ts +12 -0
- package/app/api/chat/temper-ping/route.ts +18 -0
- package/app/api/chat-proxy/[...path]/route.ts +83 -0
- package/app/api/connector-tool/route.ts +38 -0
- package/app/api/connectors/[id]/settings/route.ts +112 -0
- package/app/api/connectors/route.ts +108 -0
- package/app/api/health/tools/route.ts +14 -0
- package/app/api/issue-scanner-gitlab/route.ts +95 -0
- package/app/api/jobs/[id]/reset_dedup/route.ts +15 -0
- package/app/api/jobs/[id]/route.ts +31 -0
- package/app/api/jobs/[id]/run/route.ts +44 -0
- package/app/api/jobs/[id]/runs/[runId]/route.ts +15 -0
- package/app/api/jobs/[id]/runs/route.ts +15 -0
- package/app/api/jobs/preview/route.ts +193 -0
- package/app/api/jobs/route.ts +36 -0
- package/app/api/notify/test/route.ts +39 -7
- package/app/api/pipelines/[id]/route.ts +10 -1
- package/app/api/pipelines/route.ts +16 -2
- package/app/api/plugins/route.ts +40 -8
- package/app/api/project-sessions/route.ts +50 -10
- package/app/api/settings/route.ts +13 -0
- package/app/chat/page.tsx +531 -0
- package/bin/forge-server.mjs +3 -1
- package/cli/chat.ts +283 -0
- package/cli/jobs.ts +176 -0
- package/cli/mw.ts +28 -1
- package/cli/worktree.ts +245 -0
- package/components/ConnectorsPanel.tsx +275 -0
- package/components/Dashboard.tsx +90 -37
- package/components/JobsView.tsx +361 -0
- package/components/LogViewer.tsx +12 -2
- package/components/PipelineView.tsx +275 -56
- package/components/PluginsPanel.tsx +3 -1
- package/components/SettingsModal.tsx +229 -40
- package/components/SkillsPanel.tsx +12 -4
- package/components/TerminalLauncher.tsx +3 -1
- package/components/WebTerminal.tsx +32 -9
- package/components/WorkspaceView.tsx +18 -10
- package/docs/Connector-DeclarativeExtract-Handoff.md +471 -0
- package/docs/Connector-DeclarativeExtract-Spec.md +364 -0
- package/docs/Implementation-Plan-Browser-Agent.md +487 -0
- package/docs/Jobs-Design.md +240 -0
- package/docs/LOCAL-DEPLOY.md +3 -3
- package/docs/RFC-Browser-Connectors.md +509 -0
- package/lib/agents/index.ts +44 -6
- package/lib/agents/types.ts +1 -1
- package/lib/browser-bridge-standalone.ts +317 -0
- package/lib/builtin-plugins/github-api.yaml +93 -0
- package/lib/builtin-plugins/gitlab.yaml +860 -0
- package/lib/builtin-plugins/mantis.probe.js +176 -0
- package/lib/builtin-plugins/mantis.yaml +964 -0
- package/lib/builtin-plugins/pmdb.yaml +178 -0
- package/lib/builtin-plugins/teams.yaml +913 -0
- package/lib/chat/__test__/smoke.ts +30 -0
- package/lib/chat/agent-loop.ts +523 -0
- package/lib/chat/bridge-client.ts +59 -0
- package/lib/chat/llm/anthropic.ts +99 -0
- package/lib/chat/llm/index.ts +20 -0
- package/lib/chat/llm/openai.ts +215 -0
- package/lib/chat/llm/types.ts +42 -0
- package/lib/chat/local-memory.ts +300 -0
- package/lib/chat/memory-store.ts +87 -0
- package/lib/chat/memory-tools.ts +157 -0
- package/lib/chat/protocols/http.ts +118 -0
- package/lib/chat/protocols/shell.ts +101 -0
- package/lib/chat/proxy.ts +51 -0
- package/lib/chat/session-store.ts +272 -0
- package/lib/chat/telegram-bridge.ts +276 -0
- package/lib/chat/temper.ts +281 -0
- package/lib/chat/tool-dispatcher.ts +190 -0
- package/lib/chat/types.ts +50 -0
- package/lib/chat-standalone.ts +286 -0
- package/lib/crypto.ts +1 -1
- package/lib/health.ts +131 -0
- package/lib/help-docs/00-overview.md +2 -1
- package/lib/help-docs/01-settings.md +46 -25
- package/lib/help-docs/07-projects.md +1 -1
- package/lib/help-docs/10-troubleshooting.md +10 -2
- package/lib/help-docs/16-gitlab-autofix.md +114 -0
- package/lib/help-docs/17-connectors.md +322 -0
- package/lib/help-docs/18-chrome-mcp.md +134 -0
- package/lib/help-docs/19-jobs.md +140 -0
- package/lib/help-docs/20-mantis-bug-fix.md +115 -0
- package/lib/help-docs/CLAUDE.md +10 -0
- package/lib/init.ts +137 -50
- package/lib/iso-time.ts +30 -0
- package/lib/issue-scanner-gitlab.ts +281 -0
- package/lib/jobs/dispatcher.ts +217 -0
- package/lib/jobs/scheduler.ts +334 -0
- package/lib/jobs/store.ts +319 -0
- package/lib/jobs/types.ts +117 -0
- package/lib/pipeline-scheduler.ts +1 -6
- package/lib/pipeline.ts +790 -10
- package/lib/plugins/registry.ts +133 -8
- package/lib/plugins/templates.ts +83 -0
- package/lib/plugins/types.ts +140 -1
- package/lib/session-watcher.ts +36 -10
- package/lib/settings.ts +65 -33
- package/lib/skills.ts +3 -1
- package/lib/task-manager.ts +50 -22
- package/lib/telegram-bot.ts +71 -0
- package/lib/terminal-standalone.ts +58 -36
- package/lib/workspace/orchestrator.ts +1 -0
- package/middleware.ts +10 -0
- package/package.json +3 -2
- package/scripts/bench/README.md +1 -1
- package/scripts/bench/tasks/01-text-utils/validator.sh +1 -1
- package/scripts/bench/tasks/02-pagination/setup.sh +1 -1
- package/scripts/bench/tasks/02-pagination/validator.sh +1 -1
- package/scripts/bench/tasks/03-bug-fix/setup.sh +1 -1
- package/scripts/bench/tasks/03-bug-fix/validator.sh +1 -1
- package/src/core/db/database.ts +21 -12
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Jobs
|
|
2
|
+
|
|
3
|
+
A **Job** runs a connector tool on a schedule, dedups items, and dispatches
|
|
4
|
+
each new item to a Pipeline or a Chat session.
|
|
5
|
+
|
|
6
|
+
Typical examples:
|
|
7
|
+
- Every 30 min, search Mantis for open bugs in version 25.4 → for each new
|
|
8
|
+
bug, trigger the `bug-triage` pipeline bound to project `my-app`.
|
|
9
|
+
- Every 5 min, list Teams messages in the current chat → for each new
|
|
10
|
+
message, POST to a chat session that drafts a reply.
|
|
11
|
+
|
|
12
|
+
Job is a separate primitive from Task and Pipeline. The CLI keeps
|
|
13
|
+
`forge task` (single agent invocation) and `forge pipeline` (DAG of tasks)
|
|
14
|
+
unchanged. `forge jobs` is new.
|
|
15
|
+
|
|
16
|
+
## Anatomy
|
|
17
|
+
|
|
18
|
+
| Field | Purpose |
|
|
19
|
+
|---|---|
|
|
20
|
+
| `source_connector`, `source_tool` | Which connector + tool to call each tick (e.g. `mantis.search_bugs`) |
|
|
21
|
+
| `source_input` | JSON passed verbatim to the tool |
|
|
22
|
+
| `items_path` | Dotted path into the tool's response to find the item array (`bugs`, empty = whole response if already an array) |
|
|
23
|
+
| `dedup_field` | Field on each item used as the dedup key (e.g. `id`) |
|
|
24
|
+
| `schedule_interval_minutes` | How often the scheduler ticks this job |
|
|
25
|
+
| `dispatch_type` | `pipeline` or `chat` |
|
|
26
|
+
| `dispatch_params` | Shape depends on dispatch_type — see below |
|
|
27
|
+
|
|
28
|
+
### `dispatch_type: pipeline`
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"workflow_name": "bug-triage",
|
|
33
|
+
"project_path": "/Users/me/code/my-app",
|
|
34
|
+
"project_name": "my-app",
|
|
35
|
+
"input_template": {
|
|
36
|
+
"bug_id": "{{item.id}}",
|
|
37
|
+
"summary": "{{item.summary}}"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Per new item, Forge renders `input_template` and calls the existing
|
|
43
|
+
`triggerPipeline`. The pipeline shows up in the regular Pipelines view.
|
|
44
|
+
|
|
45
|
+
### `dispatch_type: chat`
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"agent_profile": "my-litellm",
|
|
50
|
+
"session_title_template": "Bug {{item.id}}: {{item.summary}}",
|
|
51
|
+
"message_template": "Triage bug {{item.id}}.\nSummary: {{item.summary}}",
|
|
52
|
+
"reuse_session": false
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Per new item, Forge creates (or reuses, if `reuse_session: true`) a chat
|
|
57
|
+
session and POSTs a message rendered from `message_template`. The chat
|
|
58
|
+
agent gets the message and decides what tools to call.
|
|
59
|
+
|
|
60
|
+
## Template syntax
|
|
61
|
+
|
|
62
|
+
Only `{{item.<dotted.path>}}` is supported in v1. Missing paths render to
|
|
63
|
+
empty string; objects render via `JSON.stringify`. `{{item}}` alone dumps
|
|
64
|
+
the full item.
|
|
65
|
+
|
|
66
|
+
## Backfill guard
|
|
67
|
+
|
|
68
|
+
New jobs default to `mark_existing_as_seen: true` — on first tick, every
|
|
69
|
+
item the connector returns is added to `job_seen` but **no dispatch
|
|
70
|
+
happens**. This stops a fresh job from firing on historical data. Set to
|
|
71
|
+
`false` at create time to dispatch immediately on first tick.
|
|
72
|
+
|
|
73
|
+
## CLI
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
forge jobs # list
|
|
77
|
+
forge jobs show <id> # full JSON detail
|
|
78
|
+
forge jobs runs <id> # recent ticks (summary line per run)
|
|
79
|
+
forge jobs dispatches <id> <run_id> # per-item dispatches for one run (target ids)
|
|
80
|
+
forge jobs run <id> # fire now (manual trigger)
|
|
81
|
+
forge jobs enable <id>
|
|
82
|
+
forge jobs disable <id>
|
|
83
|
+
forge jobs reset <id> # wipe dedup state — next tick re-processes everything
|
|
84
|
+
forge jobs rm <id>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Tracking results
|
|
88
|
+
|
|
89
|
+
| Want to know | Where |
|
|
90
|
+
|---|---|
|
|
91
|
+
| Job list, schedule, enabled, last/next run | Extension → Jobs tab |
|
|
92
|
+
| Last 10 ticks summary (seen/new/dispatched, error) | Expand a job in Jobs tab |
|
|
93
|
+
| Which items got dispatched in a single tick | Expand a run row — shows item_key + preview + target id + open-link |
|
|
94
|
+
| What the resulting pipeline run did | Click "open →" on a pipeline dispatch — deep-links to Forge web Pipelines view |
|
|
95
|
+
| What the resulting chat session said | Click "open →" on a chat dispatch — switches the side panel to chat tab and loads that session |
|
|
96
|
+
| Run / dispatch detail from terminal | `forge jobs runs <id>` then `forge jobs dispatches <id> <run_id>` |
|
|
97
|
+
| Raw rows | sqlite: `select * from job_runs / job_dispatches` |
|
|
98
|
+
| Server-side log lines (every tick / error / dispatch) | Forge web → Logs view, search `[jobs]` (per-job: `[jobs] <job_id>`). The Jobs view has "View logs" buttons that pre-fill this filter. CLI: `tail -f ~/.forge/data/forge.log \| grep '\[jobs\]'`. |
|
|
99
|
+
| Why a run shows 0 dispatches | Read the italic note under the run row — the scheduler writes a `notes` field explaining backfill / non-JSON response / items_path mismatch. |
|
|
100
|
+
|
|
101
|
+
Create + edit happen via the Forge extension Jobs tab, or by `curl`ing
|
|
102
|
+
`POST /api/jobs`. There is no YAML-on-disk format for Jobs (definitions
|
|
103
|
+
live in sqlite).
|
|
104
|
+
|
|
105
|
+
## Lifecycle
|
|
106
|
+
|
|
107
|
+
1. Scheduler ticks every 60s. For each enabled job where `next_run_at`
|
|
108
|
+
has elapsed:
|
|
109
|
+
2. Skip if a previous tick is still running (idempotent).
|
|
110
|
+
3. Advance `next_run_at = now + schedule_interval_minutes`.
|
|
111
|
+
4. Spawn a background tick:
|
|
112
|
+
- Call the connector via the chat tool-dispatcher (handles
|
|
113
|
+
`http` / `shell` / `browser` protocols uniformly).
|
|
114
|
+
- Parse the response — find items via `items_path`.
|
|
115
|
+
- For each item, compute `dedup_key = item[dedup_field]`,
|
|
116
|
+
`INSERT OR IGNORE INTO job_seen`. If new: dispatch.
|
|
117
|
+
- Update the `job_runs` row with counts + status.
|
|
118
|
+
|
|
119
|
+
## Browser-protocol caveat
|
|
120
|
+
|
|
121
|
+
If `source_connector` is a `protocol: browser` connector (Mantis, GitLab,
|
|
122
|
+
Teams, PMDB), the job tick can only fire **when the extension bridge is
|
|
123
|
+
connected** — the tool needs a live tab and `chrome.scripting`. A tick
|
|
124
|
+
with no extension fails with `connector ... failed: No extension
|
|
125
|
+
connected to the bridge`. The run is marked errored; the next tick
|
|
126
|
+
retries.
|
|
127
|
+
|
|
128
|
+
For 24/7 background polling, prefer connectors with `protocol: http`
|
|
129
|
+
(e.g. `github-api`) — those run server-side and don't need a browser.
|
|
130
|
+
|
|
131
|
+
## Tables
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
jobs — definition (one row per job)
|
|
135
|
+
job_runs — one row per tick (success / failure)
|
|
136
|
+
job_seen — dedup keys per job (PRIMARY KEY (job_id, dedup_key))
|
|
137
|
+
job_dispatches — one row per per-item dispatch attempt (link to pipeline run / chat session)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
All cascaded on `ON DELETE`.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Mantis → Bug Fix → MR pipeline
|
|
2
|
+
|
|
3
|
+
End-to-end: a Mantis bug surfaces (via a Forge **Job** polling mantis.get_bug
|
|
4
|
+
/ mantis.search_bugs) → triggers a Pipeline → Pipeline checks out the right
|
|
5
|
+
base branch in a worktree → headless Claude implements the fix → pipeline
|
|
6
|
+
opens a GitLab MR via `glab` → pipeline pings the assignee + reporter on
|
|
7
|
+
Teams with the MR URL.
|
|
8
|
+
|
|
9
|
+
Mirrors the `gitlab-issue-fix-and-review` builtin but driven by Mantis
|
|
10
|
+
content (description / priority / category / assignee / reporter), MR
|
|
11
|
+
opens against an explicit `base_branch` you pass in from the Job (because
|
|
12
|
+
Mantis doesn't carry milestones the way GitLab issues do).
|
|
13
|
+
|
|
14
|
+
## Builtin name
|
|
15
|
+
|
|
16
|
+
`mantis-bug-fix-and-mr` — registered in Forge's built-in workflow set,
|
|
17
|
+
visible in the Pipelines view's workflow dropdown and pickable from the
|
|
18
|
+
extension Jobs tab when you wire a Pipeline-dispatch Job.
|
|
19
|
+
|
|
20
|
+
## Inputs (set by the Job's `input_template`)
|
|
21
|
+
|
|
22
|
+
| Key | Required | Source |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| `bug_id` | yes | `{{item.id}}` |
|
|
25
|
+
| `project` | yes | injected by `triggerPipeline` from the Job's project setting |
|
|
26
|
+
| `base_branch` | **yes** | `{{item.product_version}}` mapped to a branch, OR a literal like `"release/25.4"` you write into the template |
|
|
27
|
+
| `summary` | yes | `{{item.summary}}` |
|
|
28
|
+
| `description` | yes | `{{item.description}}` |
|
|
29
|
+
| `priority` | opt | `{{item.priority}}` |
|
|
30
|
+
| `category` | opt | `{{item.category}}` |
|
|
31
|
+
| `assignee` | opt | `{{item.assignee}}` — used as Teams chat name |
|
|
32
|
+
| `reporter` | opt | `{{item.reporter}}` — used as Teams chat name |
|
|
33
|
+
| `extra_context` | opt | literal hint text for Claude |
|
|
34
|
+
| `mr_title_template` | opt | default `Fix Mantis #{bug_id}: {summary}` |
|
|
35
|
+
| `mr_body_template` | opt | default closes-ref + Claude summary; vars `{bug_id} {summary} {description} {claude_summary}` |
|
|
36
|
+
| `teams_message_template` | opt | default `🤖 Mantis #{bug_id} fixed — please review MR: {mr_url}\nBug: {summary}`; vars `{bug_id} {summary} {role} {mr_url}` |
|
|
37
|
+
|
|
38
|
+
## Nodes
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
resolve parse git remote → HOST + PROJECT_PATH; check glab; require base_branch
|
|
42
|
+
worktree-setup git worktree add -b fix/mantis-<id> .forge/worktrees/mantis-<id> origin/<base>
|
|
43
|
+
fix-code headless Claude — reads bug context, edits in worktree, commits
|
|
44
|
+
push-and-mr if any commits: push fix/mantis-<id>, glab mr create → MR_URL
|
|
45
|
+
notify-teams curl /api/connector-tool teams.send_message twice (assignee + reporter)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`fix-code` runs as a normal Claude task (not shell) so it can think, browse,
|
|
49
|
+
edit, and commit. `notify-teams` short-circuits if `push-and-mr` couldn't
|
|
50
|
+
create an MR (e.g. Claude made no changes).
|
|
51
|
+
|
|
52
|
+
## Talking to connectors from a pipeline
|
|
53
|
+
|
|
54
|
+
`notify-teams` calls `POST http://127.0.0.1:8403/api/connector-tool` —
|
|
55
|
+
a loopback-only endpoint that wraps `lib/chat/tool-dispatcher`. Body:
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{ "plugin_id": "teams", "tool": "send_message",
|
|
59
|
+
"input": { "name": "Alice", "text": "MR: https://..." } }
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Returns the standard `{ content, is_error }` tool-result shape. Works for
|
|
63
|
+
`browser` / `http` / `shell` protocol connectors. Browser-protocol calls
|
|
64
|
+
need the extension bridge connected at pipeline runtime (Chrome open +
|
|
65
|
+
extension signed in).
|
|
66
|
+
|
|
67
|
+
## Wiring it up from a Job
|
|
68
|
+
|
|
69
|
+
1. Confirm `glab` is installed + authed for the target host:
|
|
70
|
+
`glab auth login --hostname <your-gitlab.example.com>`
|
|
71
|
+
2. Make sure the project has a git remote (`git remote get-url origin`) the
|
|
72
|
+
`glab mr create` call can reach.
|
|
73
|
+
3. Forge extension → Jobs → + New job. Pick:
|
|
74
|
+
- Connector / tool: `mantis` / `get_bug` (single bug) or `search_bugs` (a query)
|
|
75
|
+
- dispatch: Pipeline → workflow `mantis-bug-fix-and-mr` → your project
|
|
76
|
+
- `input_template` (auto-prefilled when you pick the workflow; map item
|
|
77
|
+
keys to inputs, hardcode `base_branch` if Mantis doesn't carry it):
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"bug_id": "{{item.id}}",
|
|
81
|
+
"summary": "{{item.summary}}",
|
|
82
|
+
"description": "{{item.description}}",
|
|
83
|
+
"priority": "{{item.priority}}",
|
|
84
|
+
"category": "{{item.category}}",
|
|
85
|
+
"assignee": "{{item.assignee}}",
|
|
86
|
+
"reporter": "{{item.reporter}}",
|
|
87
|
+
"base_branch": "release/25.4"
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
4. Save → Run now (first tick is a backfill-no-op; click Reset dedup +
|
|
91
|
+
Run now to force one actual dispatch for testing).
|
|
92
|
+
|
|
93
|
+
## Customising
|
|
94
|
+
|
|
95
|
+
- Branch derivation rules (e.g. `target_version` → `release/<major.minor>`):
|
|
96
|
+
edit the `resolve` node in your local copy at
|
|
97
|
+
`~/.forge/data/flows/mantis-bug-fix-and-mr.yaml` (a copy will be created
|
|
98
|
+
when you click Edit in the Pipelines view; once a local file exists it
|
|
99
|
+
overrides the builtin).
|
|
100
|
+
- MR title / body templates: set via the Job's `input_template` — no
|
|
101
|
+
pipeline edit needed.
|
|
102
|
+
- Teams routing: today it uses the Mantis username verbatim as the Teams
|
|
103
|
+
chat name (substring match). For better mapping, post-process in
|
|
104
|
+
`notify-teams` to convert username → real name via a lookup table or
|
|
105
|
+
a second connector call.
|
|
106
|
+
|
|
107
|
+
## Troubleshooting
|
|
108
|
+
|
|
109
|
+
| Symptom | Cause + fix |
|
|
110
|
+
|---|---|
|
|
111
|
+
| `ERROR: base_branch is required` | The Job's `input_template` didn't supply it — Mantis bugs don't always carry one. Hardcode it in the template or map from `product_version`. |
|
|
112
|
+
| `NO_CHANGES — Claude did not commit` | Bug description was too thin or Claude couldn't find the affected code. Add hints via `extra_context` or open the worktree manually and iterate. |
|
|
113
|
+
| `glab mr create` returns nothing | Token expired / target branch protected / source branch already has an open MR. The pipeline falls back to `glab mr view` to surface the existing URL — check Pipelines log. |
|
|
114
|
+
| Teams send returns `No extension connected to the bridge` | The pipeline ran when Chrome / extension weren't online. Notification fails but the MR still landed; re-fire the `notify-teams` node manually or message the people yourself. |
|
|
115
|
+
| Mantis username doesn't match any Teams chat | The fuzzy substring match in `teams.send_message` falls back to whatever the LLM-less DOM script can match. Add a lookup step before `notify-teams` to translate names. |
|
package/lib/help-docs/CLAUDE.md
CHANGED
|
@@ -45,6 +45,11 @@ The token is valid for 24 hours. Store it in a variable and reuse for all API ca
|
|
|
45
45
|
| `12-usage.md` | Token usage analytics — charts, heatmap, cost estimation, by model/project/source |
|
|
46
46
|
| `13-ide-plugins.md` | VSCode extension + IntelliJ plugin — install, tabs, multi-connection, agent terminal launching |
|
|
47
47
|
| `15-crafts.md` | Crafts — project-scoped mini-app tabs with SDK; AI-generated via "+ Craft" button |
|
|
48
|
+
| `16-gitlab-autofix.md` | GitLab issue auto-fix — worktree + base-branch rule + Premium epic context + image attachments |
|
|
49
|
+
| `17-connectors.md` | Browser extension connectors — plugin schema, HTTP API, settings sync, Mantis + GitLab built-ins |
|
|
50
|
+
| `18-chrome-mcp.md` | Connect Forge Claude Code sessions to a real Chrome via chrome-devtools-mcp — dev-time browser access for connector authoring |
|
|
51
|
+
| `19-jobs.md` | Jobs — scheduled connector polls that dedup and fan out to Pipeline / Chat |
|
|
52
|
+
| `20-mantis-bug-fix.md` | Mantis → Bug Fix → MR builtin pipeline (mantis-bug-fix-and-mr) |
|
|
48
53
|
|
|
49
54
|
## Matching questions to docs
|
|
50
55
|
|
|
@@ -69,3 +74,8 @@ The token is valid for 24 hours. Store it in a variable and reuse for all API ca
|
|
|
69
74
|
- VSCode/IntelliJ/IDE plugin/extension/marketplace → `13-ide-plugins.md`
|
|
70
75
|
- vsce/vsix/JetBrains marketplace publish → `13-ide-plugins.md`
|
|
71
76
|
- Craft/custom tab/mini-app/extend project/AI-generated tab/builder → `15-crafts.md`
|
|
77
|
+
- GitLab/glab/MR/merge request/issue auto-fix/epic → `16-gitlab-autofix.md`
|
|
78
|
+
- Connector/browser extension/plugin schema/Mantis/category=connector → `17-connectors.md`
|
|
79
|
+
- Chrome MCP / chrome-devtools-mcp / dev-time browser / CDP / remote debugging → `18-chrome-mcp.md`
|
|
80
|
+
- Job / scheduled job / connector poll / dedup / periodic fetch / Teams poll / Mantis bug poll → `19-jobs.md`
|
|
81
|
+
- 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`
|
package/lib/init.ts
CHANGED
|
@@ -71,47 +71,60 @@ export function ensureInitialized() {
|
|
|
71
71
|
if (gInit[initKey]) return;
|
|
72
72
|
gInit[initKey] = true;
|
|
73
73
|
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
} catch {}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
74
|
+
// Per-step timing — earlier startup hangs were invisible because all
|
|
75
|
+
// these steps log heterogeneously (or not at all). \`time(label, fn)\`
|
|
76
|
+
// prints \`[init] <label> took 1234ms\` for anything taking >250ms so
|
|
77
|
+
// we can spot the slow one without instrumenting elsewhere.
|
|
78
|
+
const time = <T>(label: string, fn: () => T): T => {
|
|
79
|
+
const t = Date.now();
|
|
80
|
+
try { return fn(); }
|
|
81
|
+
finally {
|
|
82
|
+
const ms = Date.now() - t;
|
|
83
|
+
if (ms >= 250) console.log(`[init] ${label} took ${ms}ms`);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
time('logger', () => { try { const { initLogger } = require('./logger'); initLogger(); } catch {} });
|
|
88
|
+
time('migrateDataDir', () => { try { const { migrateDataDir } = require('./dirs'); migrateDataDir(); } catch {} });
|
|
89
|
+
time('migrateSecrets', migrateSecrets);
|
|
90
|
+
time('migratePluginSecrets', () => {
|
|
91
|
+
try {
|
|
92
|
+
const { migratePluginSecrets } = require('./plugins/registry');
|
|
93
|
+
migratePluginSecrets();
|
|
94
|
+
} catch (e) { console.warn('[init] migratePluginSecrets failed:', (e as Error).message); }
|
|
95
|
+
});
|
|
96
|
+
time('cleanupNotifications', () => { try { const { cleanupNotifications } = require('./notifications'); cleanupNotifications(); } catch {} });
|
|
97
|
+
time('autoDetectAgents', autoDetectAgents);
|
|
98
|
+
time('logToolStatus', () => {
|
|
99
|
+
try { const { logToolStatus } = require('./health'); logToolStatus(); }
|
|
100
|
+
catch (e) { console.warn('[tools] health check failed:', (e as Error).message); }
|
|
101
|
+
});
|
|
102
|
+
time('installForgeSkills', () => {
|
|
103
|
+
try {
|
|
104
|
+
const { installForgeSkills } = require('./workspace/skill-installer');
|
|
105
|
+
installForgeSkills('', '', '', Number(process.env.PORT) || 8403);
|
|
106
|
+
console.log('[init] Forge skills installed/updated');
|
|
107
|
+
} catch {}
|
|
108
|
+
});
|
|
98
109
|
|
|
99
110
|
// Sync help docs + CLAUDE.md to data dir on startup
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (
|
|
108
|
-
|
|
109
|
-
|
|
111
|
+
time('syncHelpDocs', () => {
|
|
112
|
+
try {
|
|
113
|
+
const { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync } = require('node:fs');
|
|
114
|
+
const { join: joinPath } = require('node:path');
|
|
115
|
+
const { getConfigDir, getDataDir } = require('./dirs');
|
|
116
|
+
const helpDir = joinPath(getConfigDir(), 'help');
|
|
117
|
+
const sourceDir = joinPath(process.cwd(), 'lib', 'help-docs');
|
|
118
|
+
if (existsSync(sourceDir)) {
|
|
119
|
+
if (!existsSync(helpDir)) mkdirSync(helpDir, { recursive: true });
|
|
120
|
+
for (const f of readdirSync(sourceDir)) {
|
|
121
|
+
if (f.endsWith('.md')) writeFileSync(joinPath(helpDir, f), readFileSync(joinPath(sourceDir, f)));
|
|
122
|
+
}
|
|
123
|
+
const claudeMd = joinPath(helpDir, 'CLAUDE.md');
|
|
124
|
+
if (existsSync(claudeMd)) writeFileSync(joinPath(getDataDir(), 'CLAUDE.md'), readFileSync(claudeMd));
|
|
110
125
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
} catch {}
|
|
126
|
+
} catch {}
|
|
127
|
+
});
|
|
115
128
|
|
|
116
129
|
// Sync skills registry (async, non-blocking) — on startup + every 30 min
|
|
117
130
|
try {
|
|
@@ -120,24 +133,44 @@ export function ensureInitialized() {
|
|
|
120
133
|
setInterval(() => { syncSkills().catch(() => {}); }, 60 * 60 * 1000);
|
|
121
134
|
} catch {}
|
|
122
135
|
|
|
123
|
-
// Usage scanner —
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
136
|
+
// Usage scanner — defer to next tick so it doesn't block ensureInitialized().
|
|
137
|
+
// On a host with hundreds of project dirs in ~/.claude/projects/, the
|
|
138
|
+
// synchronous readdirSync + statSync loop can take 5-10s; running it on
|
|
139
|
+
// the critical path of the first API request made startup feel hung.
|
|
140
|
+
// setImmediate keeps it in the same process but yields the event loop first.
|
|
141
|
+
setImmediate(() => {
|
|
142
|
+
time('scanUsage (initial)', () => {
|
|
143
|
+
try { const { scanUsage } = require('./usage-scanner'); scanUsage(); } catch {}
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
time('require usage-scanner + hourly', () => {
|
|
147
|
+
try {
|
|
148
|
+
const { scanUsage } = require('./usage-scanner');
|
|
149
|
+
setInterval(() => { try { scanUsage(); } catch {} }, 60 * 60 * 1000);
|
|
150
|
+
} catch {}
|
|
151
|
+
});
|
|
129
152
|
|
|
130
153
|
// Task runner is safe in every worker (DB-level coordination)
|
|
131
|
-
ensureRunnerStarted
|
|
154
|
+
time('ensureRunnerStarted', ensureRunnerStarted);
|
|
132
155
|
|
|
133
156
|
// Session watcher is safe (file-based, idempotent)
|
|
134
|
-
startWatcherLoop
|
|
157
|
+
time('startWatcherLoop', startWatcherLoop);
|
|
135
158
|
|
|
136
159
|
// Pipeline scheduler — periodic execution + issue scanning for project-bound workflows
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
160
|
+
time('pipeline-scheduler', () => {
|
|
161
|
+
try {
|
|
162
|
+
const { startScheduler } = require('./pipeline-scheduler');
|
|
163
|
+
startScheduler();
|
|
164
|
+
} catch {}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Jobs scheduler — periodic connector polls that fan out to pipelines / chat
|
|
168
|
+
time('jobs-scheduler', () => {
|
|
169
|
+
try {
|
|
170
|
+
const { startScheduler: startJobsScheduler } = require('./jobs/scheduler');
|
|
171
|
+
startJobsScheduler();
|
|
172
|
+
} catch (e) { console.error('[jobs-scheduler] start failed', e); }
|
|
173
|
+
});
|
|
141
174
|
|
|
142
175
|
// If services are managed externally (forge-server), skip
|
|
143
176
|
if (process.env.FORGE_EXTERNAL_SERVICES === '1') {
|
|
@@ -163,6 +196,8 @@ export function ensureInitialized() {
|
|
|
163
196
|
startTerminalProcess();
|
|
164
197
|
startTelegramProcess(); // spawns telegram-standalone
|
|
165
198
|
startWorkspaceProcess(); // spawns workspace-standalone
|
|
199
|
+
startBrowserBridgeProcess(); // spawns browser-bridge-standalone
|
|
200
|
+
startChatProcess(); // spawns chat-standalone
|
|
166
201
|
|
|
167
202
|
const settings = loadSettings();
|
|
168
203
|
if (settings.tunnelAutoStart) {
|
|
@@ -264,3 +299,55 @@ function launchWorkspaceDaemon() {
|
|
|
264
299
|
workspaceChild.on('exit', () => { workspaceChild = null; });
|
|
265
300
|
console.log('[workspace] Started daemon (pid:', workspaceChild.pid, ')');
|
|
266
301
|
}
|
|
302
|
+
|
|
303
|
+
let chatChild: ReturnType<typeof spawn> | null = null;
|
|
304
|
+
|
|
305
|
+
function startChatProcess() {
|
|
306
|
+
if (chatChild) return;
|
|
307
|
+
|
|
308
|
+
const chatPort = Number(process.env.CHAT_PORT) || 8408;
|
|
309
|
+
|
|
310
|
+
const net = require('node:net');
|
|
311
|
+
const tester = net.createServer();
|
|
312
|
+
tester.once('error', () => {
|
|
313
|
+
console.log(`[chat] Port ${chatPort} already in use, reusing existing`);
|
|
314
|
+
});
|
|
315
|
+
tester.once('listening', () => {
|
|
316
|
+
tester.close();
|
|
317
|
+
const script = join(process.cwd(), 'lib', 'chat-standalone.ts');
|
|
318
|
+
chatChild = spawn('npx', ['tsx', script], {
|
|
319
|
+
stdio: ['ignore', 'inherit', 'inherit'],
|
|
320
|
+
env: { ...process.env },
|
|
321
|
+
detached: false,
|
|
322
|
+
});
|
|
323
|
+
chatChild.on('exit', () => { chatChild = null; });
|
|
324
|
+
console.log('[chat] Started standalone (pid:', chatChild.pid, ')');
|
|
325
|
+
});
|
|
326
|
+
tester.listen(chatPort);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
let bridgeChild: ReturnType<typeof spawn> | null = null;
|
|
330
|
+
|
|
331
|
+
function startBrowserBridgeProcess() {
|
|
332
|
+
if (bridgeChild) return;
|
|
333
|
+
|
|
334
|
+
const bridgePort = Number(process.env.BRIDGE_PORT) || 8407;
|
|
335
|
+
|
|
336
|
+
const net = require('node:net');
|
|
337
|
+
const tester = net.createServer();
|
|
338
|
+
tester.once('error', () => {
|
|
339
|
+
console.log(`[bridge] Port ${bridgePort} already in use, reusing existing`);
|
|
340
|
+
});
|
|
341
|
+
tester.once('listening', () => {
|
|
342
|
+
tester.close();
|
|
343
|
+
const script = join(process.cwd(), 'lib', 'browser-bridge-standalone.ts');
|
|
344
|
+
bridgeChild = spawn('npx', ['tsx', script], {
|
|
345
|
+
stdio: ['ignore', 'inherit', 'inherit'],
|
|
346
|
+
env: { ...process.env },
|
|
347
|
+
detached: false,
|
|
348
|
+
});
|
|
349
|
+
bridgeChild.on('exit', () => { bridgeChild = null; });
|
|
350
|
+
console.log('[bridge] Started standalone (pid:', bridgeChild.pid, ')');
|
|
351
|
+
});
|
|
352
|
+
tester.listen(bridgePort);
|
|
353
|
+
}
|
package/lib/iso-time.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite-friendly timestamp normalization.
|
|
3
|
+
*
|
|
4
|
+
* SQLite's `datetime('now')` returns "2026-05-19 18:30:00" — UTC, but no
|
|
5
|
+
* timezone marker. When that string lands in JavaScript:
|
|
6
|
+
* new Date('2026-05-19 18:30:00') // parsed as LOCAL time (wrong)
|
|
7
|
+
*
|
|
8
|
+
* Browsers shift the displayed value by the user's offset — a UTC 18:30
|
|
9
|
+
* looks like 18:30 in their local clock if treated as local, hours off
|
|
10
|
+
* from the real moment. Frontend Date.toLocaleString() then renders the
|
|
11
|
+
* wrong number.
|
|
12
|
+
*
|
|
13
|
+
* Append a 'Z' so JS treats it as UTC, and Date.toLocaleString() will
|
|
14
|
+
* automatically convert to the user's local timezone.
|
|
15
|
+
*
|
|
16
|
+
* Idempotent: if the string already has a 'Z' or a numeric offset, or
|
|
17
|
+
* is already in ISO 8601 (T separator), it's returned unchanged.
|
|
18
|
+
*/
|
|
19
|
+
export function toIsoUTC(s: string | null | undefined): string | null {
|
|
20
|
+
if (!s) return null;
|
|
21
|
+
// Already ISO with T separator → just ensure it has a TZ marker.
|
|
22
|
+
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(s)) {
|
|
23
|
+
return /Z$|[+-]\d{2}:?\d{2}$/.test(s) ? s : s + 'Z';
|
|
24
|
+
}
|
|
25
|
+
// SQLite datetime('now') shape: "YYYY-MM-DD HH:MM:SS"
|
|
26
|
+
if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(\.\d+)?$/.test(s)) {
|
|
27
|
+
return s.replace(' ', 'T') + 'Z';
|
|
28
|
+
}
|
|
29
|
+
return s;
|
|
30
|
+
}
|