@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.
Files changed (145) hide show
  1. package/.forge/mcp.json +8 -0
  2. package/.forge/worktrees/pipeline-0a33c50d/lib/help-docs/01-settings.md +5 -5
  3. package/.forge/worktrees/pipeline-0a33c50d/lib/help-docs/07-projects.md +1 -1
  4. package/.forge/worktrees/pipeline-2ba01c10/lib/help-docs/01-settings.md +5 -5
  5. package/.forge/worktrees/pipeline-2ba01c10/lib/help-docs/07-projects.md +1 -1
  6. package/.forge/worktrees/pipeline-3156a8b3/lib/help-docs/01-settings.md +5 -5
  7. package/.forge/worktrees/pipeline-3156a8b3/lib/help-docs/07-projects.md +1 -1
  8. package/.forge/worktrees/pipeline-316c6574/lib/help-docs/01-settings.md +5 -5
  9. package/.forge/worktrees/pipeline-316c6574/lib/help-docs/07-projects.md +1 -1
  10. package/.forge/worktrees/pipeline-44a94121/lib/help-docs/01-settings.md +5 -5
  11. package/.forge/worktrees/pipeline-44a94121/lib/help-docs/07-projects.md +1 -1
  12. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/01-settings.md +5 -5
  13. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/07-projects.md +1 -1
  14. package/.forge/worktrees/pipeline-d1757a50/lib/help-docs/01-settings.md +5 -5
  15. package/.forge/worktrees/pipeline-d1757a50/lib/help-docs/07-projects.md +1 -1
  16. package/.forge/worktrees/pipeline-d59c2fe2/lib/help-docs/01-settings.md +5 -5
  17. package/.forge/worktrees/pipeline-d59c2fe2/lib/help-docs/07-projects.md +1 -1
  18. package/.forge/worktrees/pipeline-d6a6ef23/lib/help-docs/01-settings.md +5 -5
  19. package/.forge/worktrees/pipeline-d6a6ef23/lib/help-docs/07-projects.md +1 -1
  20. package/.forge/worktrees/pipeline-e7f78b7a/lib/help-docs/01-settings.md +5 -5
  21. package/.forge/worktrees/pipeline-e7f78b7a/lib/help-docs/07-projects.md +1 -1
  22. package/.forge/worktrees/pipeline-e97c13c7/lib/help-docs/01-settings.md +5 -5
  23. package/.forge/worktrees/pipeline-e97c13c7/lib/help-docs/07-projects.md +1 -1
  24. package/.forge/worktrees/pipeline-ecd7cb0f/lib/help-docs/01-settings.md +5 -5
  25. package/.forge/worktrees/pipeline-ecd7cb0f/lib/help-docs/07-projects.md +1 -1
  26. package/CLAUDE.md +2 -2
  27. package/RELEASE_NOTES.md +101 -5
  28. package/app/api/auth/check/route.ts +18 -0
  29. package/app/api/browser-bridge/route.ts +70 -0
  30. package/app/api/chat/sessions/[id]/events/route.ts +17 -0
  31. package/app/api/chat/sessions/[id]/fork/route.ts +15 -0
  32. package/app/api/chat/sessions/[id]/messages/route.ts +21 -0
  33. package/app/api/chat/sessions/[id]/route.ts +23 -0
  34. package/app/api/chat/sessions/route.ts +12 -0
  35. package/app/api/chat/temper-ping/route.ts +18 -0
  36. package/app/api/chat-proxy/[...path]/route.ts +83 -0
  37. package/app/api/connector-tool/route.ts +38 -0
  38. package/app/api/connectors/[id]/settings/route.ts +112 -0
  39. package/app/api/connectors/route.ts +108 -0
  40. package/app/api/health/tools/route.ts +14 -0
  41. package/app/api/issue-scanner-gitlab/route.ts +95 -0
  42. package/app/api/jobs/[id]/reset_dedup/route.ts +15 -0
  43. package/app/api/jobs/[id]/route.ts +31 -0
  44. package/app/api/jobs/[id]/run/route.ts +44 -0
  45. package/app/api/jobs/[id]/runs/[runId]/route.ts +15 -0
  46. package/app/api/jobs/[id]/runs/route.ts +15 -0
  47. package/app/api/jobs/preview/route.ts +193 -0
  48. package/app/api/jobs/route.ts +36 -0
  49. package/app/api/notify/test/route.ts +39 -7
  50. package/app/api/pipelines/[id]/route.ts +10 -1
  51. package/app/api/pipelines/route.ts +16 -2
  52. package/app/api/plugins/route.ts +40 -8
  53. package/app/api/project-sessions/route.ts +50 -10
  54. package/app/api/settings/route.ts +13 -0
  55. package/app/chat/page.tsx +531 -0
  56. package/bin/forge-server.mjs +3 -1
  57. package/cli/chat.ts +283 -0
  58. package/cli/jobs.ts +176 -0
  59. package/cli/mw.ts +28 -1
  60. package/cli/worktree.ts +245 -0
  61. package/components/ConnectorsPanel.tsx +275 -0
  62. package/components/Dashboard.tsx +90 -37
  63. package/components/JobsView.tsx +361 -0
  64. package/components/LogViewer.tsx +12 -2
  65. package/components/PipelineView.tsx +275 -56
  66. package/components/PluginsPanel.tsx +3 -1
  67. package/components/SettingsModal.tsx +229 -40
  68. package/components/SkillsPanel.tsx +12 -4
  69. package/components/TerminalLauncher.tsx +3 -1
  70. package/components/WebTerminal.tsx +32 -9
  71. package/components/WorkspaceView.tsx +18 -10
  72. package/docs/Connector-DeclarativeExtract-Handoff.md +471 -0
  73. package/docs/Connector-DeclarativeExtract-Spec.md +364 -0
  74. package/docs/Implementation-Plan-Browser-Agent.md +487 -0
  75. package/docs/Jobs-Design.md +240 -0
  76. package/docs/LOCAL-DEPLOY.md +3 -3
  77. package/docs/RFC-Browser-Connectors.md +509 -0
  78. package/lib/agents/index.ts +44 -6
  79. package/lib/agents/types.ts +1 -1
  80. package/lib/browser-bridge-standalone.ts +317 -0
  81. package/lib/builtin-plugins/github-api.yaml +93 -0
  82. package/lib/builtin-plugins/gitlab.yaml +860 -0
  83. package/lib/builtin-plugins/mantis.probe.js +176 -0
  84. package/lib/builtin-plugins/mantis.yaml +964 -0
  85. package/lib/builtin-plugins/pmdb.yaml +178 -0
  86. package/lib/builtin-plugins/teams.yaml +913 -0
  87. package/lib/chat/__test__/smoke.ts +30 -0
  88. package/lib/chat/agent-loop.ts +523 -0
  89. package/lib/chat/bridge-client.ts +59 -0
  90. package/lib/chat/llm/anthropic.ts +99 -0
  91. package/lib/chat/llm/index.ts +20 -0
  92. package/lib/chat/llm/openai.ts +215 -0
  93. package/lib/chat/llm/types.ts +42 -0
  94. package/lib/chat/local-memory.ts +300 -0
  95. package/lib/chat/memory-store.ts +87 -0
  96. package/lib/chat/memory-tools.ts +157 -0
  97. package/lib/chat/protocols/http.ts +118 -0
  98. package/lib/chat/protocols/shell.ts +101 -0
  99. package/lib/chat/proxy.ts +51 -0
  100. package/lib/chat/session-store.ts +272 -0
  101. package/lib/chat/telegram-bridge.ts +276 -0
  102. package/lib/chat/temper.ts +281 -0
  103. package/lib/chat/tool-dispatcher.ts +190 -0
  104. package/lib/chat/types.ts +50 -0
  105. package/lib/chat-standalone.ts +286 -0
  106. package/lib/crypto.ts +1 -1
  107. package/lib/health.ts +131 -0
  108. package/lib/help-docs/00-overview.md +2 -1
  109. package/lib/help-docs/01-settings.md +46 -25
  110. package/lib/help-docs/07-projects.md +1 -1
  111. package/lib/help-docs/10-troubleshooting.md +10 -2
  112. package/lib/help-docs/16-gitlab-autofix.md +114 -0
  113. package/lib/help-docs/17-connectors.md +322 -0
  114. package/lib/help-docs/18-chrome-mcp.md +134 -0
  115. package/lib/help-docs/19-jobs.md +140 -0
  116. package/lib/help-docs/20-mantis-bug-fix.md +115 -0
  117. package/lib/help-docs/CLAUDE.md +10 -0
  118. package/lib/init.ts +137 -50
  119. package/lib/iso-time.ts +30 -0
  120. package/lib/issue-scanner-gitlab.ts +281 -0
  121. package/lib/jobs/dispatcher.ts +217 -0
  122. package/lib/jobs/scheduler.ts +334 -0
  123. package/lib/jobs/store.ts +319 -0
  124. package/lib/jobs/types.ts +117 -0
  125. package/lib/pipeline-scheduler.ts +1 -6
  126. package/lib/pipeline.ts +790 -10
  127. package/lib/plugins/registry.ts +133 -8
  128. package/lib/plugins/templates.ts +83 -0
  129. package/lib/plugins/types.ts +140 -1
  130. package/lib/session-watcher.ts +36 -10
  131. package/lib/settings.ts +65 -33
  132. package/lib/skills.ts +3 -1
  133. package/lib/task-manager.ts +50 -22
  134. package/lib/telegram-bot.ts +71 -0
  135. package/lib/terminal-standalone.ts +58 -36
  136. package/lib/workspace/orchestrator.ts +1 -0
  137. package/middleware.ts +10 -0
  138. package/package.json +3 -2
  139. package/scripts/bench/README.md +1 -1
  140. package/scripts/bench/tasks/01-text-utils/validator.sh +1 -1
  141. package/scripts/bench/tasks/02-pagination/setup.sh +1 -1
  142. package/scripts/bench/tasks/02-pagination/validator.sh +1 -1
  143. package/scripts/bench/tasks/03-bug-fix/setup.sh +1 -1
  144. package/scripts/bench/tasks/03-bug-fix/validator.sh +1 -1
  145. 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. |
@@ -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
- // Add timestamps to all console output
75
- try { const { initLogger } = require('./logger'); initLogger(); } catch {}
76
-
77
- // Migrate old data layout (~/.forge/* ~/.forge/data/*) on first run
78
- try { const { migrateDataDir } = require('./dirs'); migrateDataDir(); } catch {}
79
-
80
- // Migrate plaintext secrets on startup
81
- migrateSecrets();
82
-
83
- // Cleanup old notifications
84
- try {
85
- const { cleanupNotifications } = require('./notifications');
86
- cleanupNotifications();
87
- } catch {}
88
-
89
- // Auto-detect claude path if not configured
90
- autoDetectAgents();
91
-
92
- // Install/update forge skills to ~/.claude/skills/ on every startup
93
- try {
94
- const { installForgeSkills } = require('./workspace/skill-installer');
95
- installForgeSkills('', '', '', Number(process.env.PORT) || 8403);
96
- console.log('[init] Forge skills installed/updated');
97
- } catch {}
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
- try {
101
- const { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync } = require('node:fs');
102
- const { join: joinPath } = require('node:path');
103
- const { getConfigDir, getDataDir } = require('./dirs');
104
- const helpDir = joinPath(getConfigDir(), 'help');
105
- const sourceDir = joinPath(process.cwd(), 'lib', 'help-docs');
106
- if (existsSync(sourceDir)) {
107
- if (!existsSync(helpDir)) mkdirSync(helpDir, { recursive: true });
108
- for (const f of readdirSync(sourceDir)) {
109
- if (f.endsWith('.md')) writeFileSync(joinPath(helpDir, f), readFileSync(joinPath(sourceDir, f)));
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
- const claudeMd = joinPath(helpDir, 'CLAUDE.md');
112
- if (existsSync(claudeMd)) writeFileSync(joinPath(getDataDir(), 'CLAUDE.md'), readFileSync(claudeMd));
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 — scan JSONL files for token usage on startup + every hour
124
- try {
125
- const { scanUsage } = require('./usage-scanner');
126
- scanUsage();
127
- setInterval(() => { try { scanUsage(); } catch {} }, 60 * 60 * 1000);
128
- } catch {}
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
- try {
138
- const { startScheduler } = require('./pipeline-scheduler');
139
- startScheduler();
140
- } catch {}
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
+ }
@@ -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
+ }