@aion0/forge 0.10.53 → 0.10.56
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/RELEASE_NOTES.md +14 -3
- package/app/api/activity/summary/route.ts +30 -0
- package/app/api/cache/route.ts +125 -41
- package/app/api/chat/sessions/[id]/abort/route.ts +14 -0
- package/app/api/chat/sessions/[id]/note/route.ts +16 -0
- package/app/api/files/[...path]/route.ts +94 -0
- package/app/api/scratch/[...path]/route.ts +5 -0
- package/app/chat/page.tsx +237 -36
- package/app/files/[...path]/page.tsx +22 -0
- package/components/Dashboard.tsx +82 -26
- package/components/PipelineView.tsx +40 -7
- package/components/ScratchViewer.tsx +14 -3
- package/lib/chat/agent-loop.ts +95 -2
- package/lib/chat/input-queue.ts +159 -0
- package/lib/chat/link-patterns.ts +28 -5
- package/lib/chat/tool-dispatcher.ts +270 -17
- package/lib/chat/turn-control.ts +109 -0
- package/lib/chat-standalone.ts +75 -21
- package/lib/help-docs/10-troubleshooting.md +16 -0
- package/lib/help-docs/17-connectors.md +19 -0
- package/lib/help-docs/25-chat-tools.md +125 -0
- package/lib/help-docs/CLAUDE.md +2 -0
- package/lib/init.ts +14 -0
- package/lib/pipeline.ts +11 -0
- package/lib/scratch-cleanup.ts +25 -16
- package/lib/task-manager.ts +30 -0
- package/package.json +1 -1
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Chat tools — what the chat agent can do directly
|
|
2
|
+
|
|
3
|
+
The chat agent (Forge's `/chat` page + the IDE plugin + Telegram bot) talks
|
|
4
|
+
to LLMs that can call **builtin tools** without round-tripping through a
|
|
5
|
+
background task. These cover the everyday "Forge knows / Forge owns" cases
|
|
6
|
+
so the agent doesn't have to dispatch a Claude CLI task just to read a
|
|
7
|
+
file or fire a pipeline.
|
|
8
|
+
|
|
9
|
+
This doc lists the tools the agent has by default and the workflows they
|
|
10
|
+
unlock. (Connector tools — `mantis.search_bugs`, `gitlab.list_my_todos`,
|
|
11
|
+
etc. — are documented in `17-connectors.md`.)
|
|
12
|
+
|
|
13
|
+
## File access — `<dataDir>/`
|
|
14
|
+
|
|
15
|
+
Forge's data dir (`~/.forge/data/` by default, override with `FORGE_DATA_DIR`)
|
|
16
|
+
holds everything Forge owns: pipelines, schedules, prompts, connectors,
|
|
17
|
+
the sqlite DB, the encryption key, and two cache pools — `cloned-projects/`
|
|
18
|
+
and `tmp/`. The chat agent can read most of it and write to `tmp/`.
|
|
19
|
+
|
|
20
|
+
### `save_tmp_file` — write to `<dataDir>/tmp/`
|
|
21
|
+
Use case: the user says *"save this report to a file"*, *"give me a
|
|
22
|
+
downloadable copy"*, *"export the results"*. The agent already has the
|
|
23
|
+
content in context — no CLI task needed.
|
|
24
|
+
|
|
25
|
+
The file lands in `<dataDir>/tmp/<filename>`. The response includes:
|
|
26
|
+
- `path`: `tmp/<filename>` — chat auto-links this string to the in-browser
|
|
27
|
+
viewer at `/files/tmp/<filename>` (renders markdown, download button)
|
|
28
|
+
- `file_url`: `file://<abs-path>` — opens directly in Chrome on localhost,
|
|
29
|
+
or the user can right-click → Show in Finder/Explorer
|
|
30
|
+
- `local_path`: absolute filesystem path
|
|
31
|
+
|
|
32
|
+
`tmp/` is **cache-managed** — counts in the user-menu Cache panel and gets
|
|
33
|
+
swept by the janitor after `scratchRetentionDays` (default 7 days). Tell
|
|
34
|
+
users their saved file is ephemeral; if they want it permanent, ask them
|
|
35
|
+
to copy it out.
|
|
36
|
+
|
|
37
|
+
### `read_forge_file` — read anywhere under `<dataDir>/`
|
|
38
|
+
Path is dataDir-relative: `tmp/foo.md`, `scratch/report.md`, `flows/x.yaml`,
|
|
39
|
+
`prompts/y.yaml`, `connectors/mantis/manifest.yaml`, etc. Use this instead
|
|
40
|
+
of dispatching a `cat` task when the user wants to inspect a Forge-owned
|
|
41
|
+
file.
|
|
42
|
+
|
|
43
|
+
Sensitive items at the dataDir root are refused: `.encrypt-key`,
|
|
44
|
+
`.enterprise-keys.json`, any `*.db*`, `forge.log*`, `*-tokens.json`. The
|
|
45
|
+
chat agent can't accidentally leak these into chat context.
|
|
46
|
+
|
|
47
|
+
Pass `as_base64: true` for binary files (pdf, images, zip).
|
|
48
|
+
|
|
49
|
+
### `list_forge_files` — list files anywhere under `<dataDir>/`
|
|
50
|
+
Pass `dir` as a dataDir-relative subdir (`tmp`, `scratch`, `flows`,
|
|
51
|
+
`connectors/mantis`). Each entry returns `path`, `kind` (file/dir),
|
|
52
|
+
`size`, `mtime`, and a `file_url` (file:// link).
|
|
53
|
+
|
|
54
|
+
Use this for *"what files are in tmp?"* / *"list my flows"* — never
|
|
55
|
+
dispatch a `ls` task for these.
|
|
56
|
+
|
|
57
|
+
### `scratch://<path>` — connector-arg ref protocol
|
|
58
|
+
Use case: the agent wants to **upload** a Forge-owned file to a connector
|
|
59
|
+
(e.g. `onedrive.upload_file`, `teams.send_message` with an attachment).
|
|
60
|
+
Instead of reading the file into context and base64-encoding it via the
|
|
61
|
+
model (unreliable for non-ASCII, expensive for large files), pass
|
|
62
|
+
`scratch://tmp/report.md` as the connector arg's value. Forge resolves
|
|
63
|
+
the path under `<dataDir>/`, base64-encodes server-side, then dispatches.
|
|
64
|
+
|
|
65
|
+
`scratch://` is the historical name for the **ref protocol**, not the
|
|
66
|
+
`scratch/` subdir — the path after it is dataDir-relative. Bare forms
|
|
67
|
+
(`scratch/<file>`, `/api/scratch/<file>`) are also accepted for
|
|
68
|
+
back-compat. Same sensitive-file blacklist as `read_forge_file`.
|
|
69
|
+
|
|
70
|
+
## Background tasks
|
|
71
|
+
|
|
72
|
+
### `dispatch_task` — fire-and-forget a Claude CLI task
|
|
73
|
+
For longer-running asks the user wants in the background ("analyze this
|
|
74
|
+
codebase and write findings", "run the test suite and summarize"). Returns
|
|
75
|
+
a `task_id`. Pair with `get_task_status` (or `start_watch`) to follow up.
|
|
76
|
+
|
|
77
|
+
### `cancel_task` — stop a running task by id
|
|
78
|
+
Use when the user says *"停止 / cancel / kill that task"*. Safe to call
|
|
79
|
+
on already-terminal tasks (returns ok + a note). Much cleaner than
|
|
80
|
+
telling the user to open the Tasks UI.
|
|
81
|
+
|
|
82
|
+
### `get_task_status` — check a dispatched task
|
|
83
|
+
Returns `status`, `terminal`, `result_summary` (truncated to 1KB),
|
|
84
|
+
`error`, `completed_at`. For long-running polls, prefer `start_watch`
|
|
85
|
+
over manual polling — see `24-watch.md`.
|
|
86
|
+
|
|
87
|
+
## Pipelines + schedules
|
|
88
|
+
|
|
89
|
+
The chat agent owns the full schedule CRUD surface and pipeline triggers:
|
|
90
|
+
`create_schedule`, `list_schedules`, `update_schedule`, `delete_schedule`,
|
|
91
|
+
`run_schedule_now`, `trigger_pipeline`, `get_pipeline_status`. See
|
|
92
|
+
`05-pipelines.md` and `13-schedules.md` for details on what the
|
|
93
|
+
underlying objects look like.
|
|
94
|
+
|
|
95
|
+
## Stop / mid-task interjection
|
|
96
|
+
|
|
97
|
+
The user can interrupt a runaway tool-call loop at any time:
|
|
98
|
+
- **Stop button** — aborts the loop at the next iteration boundary, OR
|
|
99
|
+
between parallel tool calls (so a 5-way parallel batch can be stopped
|
|
100
|
+
mid-way). Persists a "⏹ Stopped by user." message in the thread.
|
|
101
|
+
- **Send during streaming** — typed text is queued as a **note** for the
|
|
102
|
+
running turn; the loop splices it in as a user message at the start of
|
|
103
|
+
the next iteration. The first such note carries a `[mid-task
|
|
104
|
+
interjection — …]` prefix so the model treats it as an authoritative
|
|
105
|
+
redirect, not ambient chat.
|
|
106
|
+
|
|
107
|
+
Both work on the web `/chat` page and the extension. If the user opens the
|
|
108
|
+
same chat session from two clients (e.g. extension + webchat) and posts a
|
|
109
|
+
follow-up while a turn is running, the server merges it into the running
|
|
110
|
+
loop as a note — no parallel turns on one session.
|
|
111
|
+
|
|
112
|
+
See `10-troubleshooting.md` § "Chat keeps looping" for recovery when a
|
|
113
|
+
runaway loop pre-dates Stop being available.
|
|
114
|
+
|
|
115
|
+
## Help + introspection
|
|
116
|
+
|
|
117
|
+
`list_help_docs` + `read_help_doc` give the agent access to these same
|
|
118
|
+
docs so it can answer Forge questions directly. `list_forge_context`
|
|
119
|
+
returns the current instance's projects / agent profiles / installed
|
|
120
|
+
skills — call it before passing project / agent / skill names to
|
|
121
|
+
`dispatch_task` or `trigger_pipeline` to validate them.
|
|
122
|
+
|
|
123
|
+
`add_enterprise_key` registers a pasted enterprise marketplace key
|
|
124
|
+
(`fortinet:github_pat_…`) and triggers an immediate sync — see
|
|
125
|
+
`01-settings.md` § Marketplace Providers.
|
package/lib/help-docs/CLAUDE.md
CHANGED
|
@@ -51,6 +51,7 @@ The token is valid for 24 hours. Store it in a variable and reuse for all API ca
|
|
|
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
52
|
| `23-automation-states.md` | Fortinet pipeline automation: GitLab MR stage labels, Mantis status flow, Teams notify policy |
|
|
53
53
|
| `24-watch.md` | Background watches — async polling of long jobs (device upgrade, Jenkins build, test run) that report back in chat. Two triggers: connectors' declarative `async` block, and the `start_watch` builtin the assistant calls on the fly. Where to see/cancel them. |
|
|
54
|
+
| `25-chat-tools.md` | Builtin tools the chat agent has by default — file access (`save_tmp_file` / `read_forge_file` / `list_forge_files`), `scratch://` connector-ref protocol, `dispatch_task` / `cancel_task`, schedules CRUD, pipelines, Stop + mid-task notes. |
|
|
54
55
|
|
|
55
56
|
## Matching questions to docs
|
|
56
57
|
|
|
@@ -86,3 +87,4 @@ The token is valid for 24 hours. Store it in a variable and reuse for all API ca
|
|
|
86
87
|
- Recipe / "From recipe" form / parameterized job → tell user: **recipes deprecated** along with Jobs; fire pipelines manually or via Schedules.
|
|
87
88
|
- 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)
|
|
88
89
|
- Background watch / "watch this build" / "tell me when the upgrade is done" / progress chip / start_watch / async long job / why is the assistant polling → `24-watch.md`
|
|
90
|
+
- save_tmp_file / read_forge_file / list_forge_files / cancel_task / scratch:// ref / `<dataDir>/tmp/` / Stop / mid-task note / "what tools does the chat agent have" → `25-chat-tools.md`
|
package/lib/init.ts
CHANGED
|
@@ -182,6 +182,20 @@ export function ensureInitialized() {
|
|
|
182
182
|
}, 60 * 60 * 1000);
|
|
183
183
|
} catch {}
|
|
184
184
|
|
|
185
|
+
// Reconcile orphaned tasks — any DB row at status='running' or 'queued'
|
|
186
|
+
// at startup is by definition stuck (its parent next-server process is
|
|
187
|
+
// gone; we're booting the new one). Without this, the Activity panel
|
|
188
|
+
// shows zombie tasks indefinitely and dispatch_task can collide with
|
|
189
|
+
// stale project locks. Idempotent — second boot finds zero.
|
|
190
|
+
time('reconcileOrphanedTasks', () => {
|
|
191
|
+
try {
|
|
192
|
+
const { reconcileOrphanedTasks } = require('./task-manager');
|
|
193
|
+
reconcileOrphanedTasks();
|
|
194
|
+
} catch (e) {
|
|
195
|
+
console.warn('[init] reconcileOrphanedTasks failed:', (e as Error).message);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
185
199
|
// Usage scanner — defer to next tick so it doesn't block ensureInitialized().
|
|
186
200
|
// On a host with hundreds of project dirs in ~/.claude/projects/, the
|
|
187
201
|
// synchronous readdirSync + statSync loop can take 5-10s; running it on
|
package/lib/pipeline.ts
CHANGED
|
@@ -1589,6 +1589,17 @@ export async function retryNode(pipelineId: string, nodeId: string): Promise<{ o
|
|
|
1589
1589
|
// underlying task is dead); cancelled covers user-cancelled pipelines
|
|
1590
1590
|
// where the user later wants to resume from the cancelled node.
|
|
1591
1591
|
// pending/done/skipped are still misclicks.
|
|
1592
|
+
//
|
|
1593
|
+
// 'skipped' specifically is NOT retriable here — in forEach pipelines
|
|
1594
|
+
// orchestrator marks a per-iter step 'skipped' to indicate "this
|
|
1595
|
+
// iteration failed but we're continuing to the next item" (not the
|
|
1596
|
+
// usual "upstream-failed → skip downstream" semantic). Letting retry
|
|
1597
|
+
// touch a skipped node would risk re-firing iteration logic that the
|
|
1598
|
+
// orchestrator already decided to abandon, terminating the whole
|
|
1599
|
+
// for_each loop. If a real upstream-failure-cascade arises and the
|
|
1600
|
+
// user needs to retry a skipped downstream, the right path is to
|
|
1601
|
+
// retry the failed root explicitly — its BFS-downstream reset will
|
|
1602
|
+
// pull this node back to pending.
|
|
1592
1603
|
if (nodeState.status !== 'failed' && nodeState.status !== 'running' && nodeState.status !== 'cancelled') {
|
|
1593
1604
|
return { ok: false, error: `node is in status '${nodeState.status}' — only failed, running, or cancelled nodes can be retried` };
|
|
1594
1605
|
}
|
package/lib/scratch-cleanup.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Cache-dir janitor — deletes stale files under:
|
|
3
|
+
* <dataDir>/tmp/ — chat-saved files (save_tmp_file)
|
|
4
|
+
* <dataDir>/scratch/ — legacy chat-saved location (kept for back-compat)
|
|
3
5
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* Retention is settings.scratchRetentionDays (default 7).
|
|
6
|
+
* Without periodic sweeping the dirs grow forever. Retention is
|
|
7
|
+
* settings.scratchRetentionDays (default 7).
|
|
7
8
|
*
|
|
8
|
-
* Conservative scope: only
|
|
9
|
-
* -
|
|
10
|
-
*
|
|
11
|
-
* - Skips subdirectories — could be user-created project trees we don't own
|
|
9
|
+
* Conservative scope: only top-level files. Skips dotfiles, the
|
|
10
|
+
* scratch-project marker CLAUDE.md, and subdirs (could be user-created
|
|
11
|
+
* project trees we don't own).
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { readdirSync, statSync, unlinkSync } from 'node:fs';
|
|
@@ -29,16 +29,13 @@ function retentionMs(): number {
|
|
|
29
29
|
return days * ONE_DAY_MS;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
const root = join(getDataDir(), 'scratch');
|
|
32
|
+
function sweepDir(root: string, cutoff: number, skipNames: Set<string>): { scanned: number; deleted: number; freedBytes: number } {
|
|
34
33
|
let scanned = 0, deleted = 0, freedBytes = 0;
|
|
35
|
-
const maxAge = retentionMs();
|
|
36
|
-
const cutoff = Date.now() - maxAge;
|
|
37
34
|
let entries: string[] = [];
|
|
38
35
|
try { entries = readdirSync(root); } catch { return { scanned, deleted, freedBytes }; }
|
|
39
36
|
for (const name of entries) {
|
|
40
37
|
if (name.startsWith('.')) continue;
|
|
41
|
-
if (name
|
|
38
|
+
if (skipNames.has(name)) continue;
|
|
42
39
|
const p = join(root, name);
|
|
43
40
|
let st;
|
|
44
41
|
try { st = statSync(p); } catch { continue }
|
|
@@ -48,12 +45,24 @@ export function cleanupScratch(): { scanned: number; deleted: number; freedBytes
|
|
|
48
45
|
try { unlinkSync(p); deleted++; freedBytes += st.size; }
|
|
49
46
|
catch (e) { console.warn('[scratch-cleanup] unlink failed:', p, (e as Error).message); }
|
|
50
47
|
}
|
|
51
|
-
if (deleted > 0) {
|
|
52
|
-
console.log(`[scratch-cleanup] deleted ${deleted}/${scanned} files, freed ${freedBytes} bytes`);
|
|
53
|
-
}
|
|
54
48
|
return { scanned, deleted, freedBytes };
|
|
55
49
|
}
|
|
56
50
|
|
|
51
|
+
export function cleanupScratch(): { scanned: number; deleted: number; freedBytes: number } {
|
|
52
|
+
const cutoff = Date.now() - retentionMs();
|
|
53
|
+
const tmp = sweepDir(join(getDataDir(), 'tmp'), cutoff, new Set());
|
|
54
|
+
const scratch = sweepDir(join(getDataDir(), 'scratch'), cutoff, new Set(['CLAUDE.md']));
|
|
55
|
+
const total = {
|
|
56
|
+
scanned: tmp.scanned + scratch.scanned,
|
|
57
|
+
deleted: tmp.deleted + scratch.deleted,
|
|
58
|
+
freedBytes: tmp.freedBytes + scratch.freedBytes,
|
|
59
|
+
};
|
|
60
|
+
if (total.deleted > 0) {
|
|
61
|
+
console.log(`[scratch-cleanup] deleted ${total.deleted}/${total.scanned} files (tmp=${tmp.deleted}, scratch=${scratch.deleted}), freed ${total.freedBytes} bytes`);
|
|
62
|
+
}
|
|
63
|
+
return total;
|
|
64
|
+
}
|
|
65
|
+
|
|
57
66
|
let started = false;
|
|
58
67
|
export function startScratchCleanup(): void {
|
|
59
68
|
if (started) return;
|
package/lib/task-manager.ts
CHANGED
|
@@ -123,6 +123,36 @@ export function getTask(id: string): Task | null {
|
|
|
123
123
|
return rowToTask(row);
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Reconcile orphaned 'running' tasks. Tasks spawn child processes
|
|
128
|
+
* owned by next-server (lib/claude-process); when next-server exits
|
|
129
|
+
* (forge restart / crash / stop), those processes die but the DB row
|
|
130
|
+
* stays at status='running' forever. Result: Activity panel /
|
|
131
|
+
* /api/activity/summary keeps showing zombie tasks the user never
|
|
132
|
+
* started; new dispatches can collide with stuck project locks.
|
|
133
|
+
*
|
|
134
|
+
* Called once at init.ts startup. Any row still showing 'running' is
|
|
135
|
+
* by definition orphaned — its parent next-server process is gone
|
|
136
|
+
* (otherwise we wouldn't be in startup). Mark all as failed with a
|
|
137
|
+
* clear error so the user knows it was a restart, not a real failure.
|
|
138
|
+
*
|
|
139
|
+
* Idempotent — second run finds zero rows to update.
|
|
140
|
+
*/
|
|
141
|
+
export function reconcileOrphanedTasks(): number {
|
|
142
|
+
const r = db().prepare(`
|
|
143
|
+
UPDATE tasks
|
|
144
|
+
SET status = 'failed',
|
|
145
|
+
error = COALESCE(NULLIF(error, ''), 'orphaned by server restart — task process did not survive restart'),
|
|
146
|
+
completed_at = datetime('now')
|
|
147
|
+
WHERE status IN ('running', 'queued')
|
|
148
|
+
`).run();
|
|
149
|
+
const n = (r.changes as number) || 0;
|
|
150
|
+
if (n > 0) {
|
|
151
|
+
console.log(`[task-manager] reconciled ${n} orphaned task(s) (running→failed)`);
|
|
152
|
+
}
|
|
153
|
+
return n;
|
|
154
|
+
}
|
|
155
|
+
|
|
126
156
|
export function listTasks(status?: TaskStatus): Task[] {
|
|
127
157
|
let query = 'SELECT * FROM tasks';
|
|
128
158
|
const params: string[] = [];
|
package/package.json
CHANGED