@aion0/forge 0.10.31 → 0.10.32
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 +4 -4
- package/lib/chat/tool-dispatcher.ts +83 -32
- package/lib/watch/start-watch-tool.ts +4 -2
- package/package.json +1 -1
package/RELEASE_NOTES.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
# Forge v0.10.
|
|
1
|
+
# Forge v0.10.32
|
|
2
2
|
|
|
3
3
|
Released: 2026-06-02
|
|
4
4
|
|
|
5
|
-
## Changes since v0.10.
|
|
5
|
+
## Changes since v0.10.31
|
|
6
6
|
|
|
7
7
|
### Other
|
|
8
|
-
- fix(watch):
|
|
8
|
+
- fix(watch): builtin pollers return JSON + add get_task_status
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
**Full Changelog**: https://github.com/aiwatching/forge/compare/v0.10.
|
|
11
|
+
**Full Changelog**: https://github.com/aiwatching/forge/compare/v0.10.31...v0.10.32
|
|
@@ -163,50 +163,61 @@ const BUILTINS: Record<string, BuiltinHandler> = {
|
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
const pipeline = startPipeline(params.workflow, stringInput, { skills: skills.length ? skills : undefined });
|
|
166
|
-
|
|
166
|
+
const fresh = getPipeline(pipeline.id) || pipeline;
|
|
167
|
+
const errors: string[] = [];
|
|
167
168
|
if (pipeline.status === 'failed') {
|
|
168
|
-
const fresh = getPipeline(pipeline.id) || pipeline;
|
|
169
|
-
const errs: string[] = [];
|
|
170
169
|
for (const [nid, n] of Object.entries(fresh.nodes || {})) {
|
|
171
|
-
if ((n as any).error)
|
|
170
|
+
if ((n as any).error) errors.push(`${nid}: ${(n as any).error}`);
|
|
172
171
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
// failure mode (empty source). Warn the LLM explicitly so it doesn't
|
|
177
|
-
// claim success.
|
|
178
|
-
const fresh = getPipeline(pipeline.id) || pipeline;
|
|
172
|
+
}
|
|
173
|
+
let warning: string | undefined;
|
|
174
|
+
if (pipeline.status === 'done') {
|
|
179
175
|
const forEach = (fresh as any).forEach;
|
|
180
176
|
if (forEach && typeof forEach === 'object') {
|
|
181
177
|
const iters = Array.isArray(forEach.iterations) ? forEach.iterations.length : 0;
|
|
182
178
|
const total = typeof forEach.total === 'number' ? forEach.total : iters;
|
|
183
179
|
if (total === 0) {
|
|
184
|
-
|
|
180
|
+
warning = 'Pipeline finished with 0 iterations — likely empty source list. This is NOT a success; the work the user asked for did NOT happen. Re-check input fields (especially the one feeding for_each.source) and retry.';
|
|
185
181
|
}
|
|
186
182
|
}
|
|
187
|
-
} else {
|
|
188
|
-
line += '. Watch progress in the Pipelines view.';
|
|
189
183
|
}
|
|
190
|
-
return
|
|
184
|
+
return JSON.stringify({
|
|
185
|
+
ok: pipeline.status !== 'failed',
|
|
186
|
+
pipeline_id: pipeline.id,
|
|
187
|
+
workflow: params.workflow,
|
|
188
|
+
status: pipeline.status,
|
|
189
|
+
terminal: pipeline.status !== 'running',
|
|
190
|
+
...(errors.length ? { errors: errors.join(' | ').slice(0, 500) } : {}),
|
|
191
|
+
...(warning ? { warning } : {}),
|
|
192
|
+
hint: pipeline.status === 'running'
|
|
193
|
+
? 'To wait for completion: start_watch poll="get_pipeline_status" poll_args={pipeline_id:"' + pipeline.id + '"} done_match={path:"status",equals:"done"} fail_path="status==failed". Do NOT poll get_pipeline_status in this conversation.'
|
|
194
|
+
: undefined,
|
|
195
|
+
});
|
|
191
196
|
},
|
|
192
197
|
|
|
193
198
|
// Query a pipeline run's status + per-node results by id (pairs with
|
|
194
|
-
// trigger_pipeline's returned id).
|
|
199
|
+
// trigger_pipeline's returned id). Returns JSON so start_watch's
|
|
200
|
+
// done_match={path:"status",equals:"done"} can resolve — a string-shaped
|
|
201
|
+
// response leaves watches polling forever.
|
|
195
202
|
get_pipeline_status: async (input) => {
|
|
196
203
|
const params = (input as { pipeline_id?: string } | undefined) || {};
|
|
197
|
-
if (!params.pipeline_id) return
|
|
204
|
+
if (!params.pipeline_id) return JSON.stringify({ ok: false, error: 'pipeline_id is required (returned by trigger_pipeline)' });
|
|
198
205
|
const { getPipeline } = await import('../pipeline');
|
|
199
206
|
const pipeline = getPipeline(params.pipeline_id);
|
|
200
|
-
if (!pipeline) return `Pipeline "${params.pipeline_id}" not found
|
|
201
|
-
const nodes = Object.entries(pipeline.nodes || {}).map(([id, n]) => {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
207
|
+
if (!pipeline) return JSON.stringify({ ok: false, error: `Pipeline "${params.pipeline_id}" not found` });
|
|
208
|
+
const nodes = Object.entries(pipeline.nodes || {}).map(([id, n]) => ({
|
|
209
|
+
id,
|
|
210
|
+
status: n.status,
|
|
211
|
+
...(n.error ? { error: n.error } : {}),
|
|
212
|
+
outputs: Object.fromEntries(Object.entries(n.outputs || {}).map(([k, v]) => [k, String(v).slice(0, 500)])),
|
|
213
|
+
}));
|
|
214
|
+
return JSON.stringify({
|
|
215
|
+
id: pipeline.id,
|
|
216
|
+
status: pipeline.status,
|
|
217
|
+
terminal: pipeline.status !== 'running',
|
|
218
|
+
workflowName: pipeline.workflowName,
|
|
219
|
+
nodes,
|
|
220
|
+
});
|
|
210
221
|
},
|
|
211
222
|
|
|
212
223
|
// Surface Forge's local context (projects + agents + skills) so the chat
|
|
@@ -260,11 +271,11 @@ const BUILTINS: Record<string, BuiltinHandler> = {
|
|
|
260
271
|
// caller can ask "what's the status of task <id>?" later — we don't block.
|
|
261
272
|
dispatch_task: async (input) => {
|
|
262
273
|
const params = (input as { project?: string; prompt?: string; agent?: string } | undefined) || {};
|
|
263
|
-
if (!params.prompt) return
|
|
274
|
+
if (!params.prompt) return JSON.stringify({ ok: false, error: 'prompt is required' });
|
|
264
275
|
const { getProjectInfo, SCRATCH_PROJECT_NAME } = await import('../projects');
|
|
265
276
|
const projectName = params.project?.trim() || SCRATCH_PROJECT_NAME;
|
|
266
277
|
const project = getProjectInfo(projectName);
|
|
267
|
-
if (!project) return
|
|
278
|
+
if (!project) return JSON.stringify({ ok: false, error: `project "${projectName}" not found` });
|
|
268
279
|
const { createTask } = await import('../task-manager');
|
|
269
280
|
const task = createTask({
|
|
270
281
|
projectName: project.name,
|
|
@@ -273,7 +284,33 @@ const BUILTINS: Record<string, BuiltinHandler> = {
|
|
|
273
284
|
conversationId: '',
|
|
274
285
|
agent: params.agent || undefined,
|
|
275
286
|
});
|
|
276
|
-
return
|
|
287
|
+
return JSON.stringify({
|
|
288
|
+
ok: true,
|
|
289
|
+
task_id: task.id,
|
|
290
|
+
project: project.name,
|
|
291
|
+
status: task.status,
|
|
292
|
+
hint: 'To wait for completion: start_watch poll="get_task_status" poll_args={task_id:"' + task.id + '"} done_path="terminal". Do NOT poll get_task_status in this conversation.',
|
|
293
|
+
});
|
|
294
|
+
},
|
|
295
|
+
|
|
296
|
+
// Companion to dispatch_task — read a task's status + result. Returns JSON
|
|
297
|
+
// so start_watch can poll via done_path="terminal" or done_match
|
|
298
|
+
// {path:"status", equals:"done"}.
|
|
299
|
+
get_task_status: async (input) => {
|
|
300
|
+
const params = (input as { task_id?: string } | undefined) || {};
|
|
301
|
+
if (!params.task_id) return JSON.stringify({ ok: false, error: 'task_id is required (returned by dispatch_task)' });
|
|
302
|
+
const { getTask } = await import('../task-manager');
|
|
303
|
+
const task = getTask(params.task_id);
|
|
304
|
+
if (!task) return JSON.stringify({ ok: false, error: `Task "${params.task_id}" not found` });
|
|
305
|
+
return JSON.stringify({
|
|
306
|
+
id: task.id,
|
|
307
|
+
status: task.status,
|
|
308
|
+
terminal: task.status === 'done' || task.status === 'failed' || task.status === 'cancelled',
|
|
309
|
+
project: task.projectName,
|
|
310
|
+
...(task.resultSummary ? { result_summary: String(task.resultSummary).slice(0, 1000) } : {}),
|
|
311
|
+
...(task.error ? { error: String(task.error).slice(0, 500) } : {}),
|
|
312
|
+
...(task.completedAt ? { completed_at: task.completedAt } : {}),
|
|
313
|
+
});
|
|
277
314
|
},
|
|
278
315
|
|
|
279
316
|
// List Forge's own help/documentation files so the chat agent can answer
|
|
@@ -315,7 +352,7 @@ export const BUILTIN_TOOL_DEFS: BuiltinToolDef[] = [
|
|
|
315
352
|
},
|
|
316
353
|
{
|
|
317
354
|
name: 'trigger_pipeline',
|
|
318
|
-
description: 'Trigger a Forge pipeline workflow (YAML under flows/). Two-step usage: (1) call with NO args first — returns every workflow + its input schema (which fields are required vs have defaults). (2) call again with workflow=<name> and input={...} passing ONLY required fields and any optional fields the user explicitly specified. NEVER pass invented placeholder values for optional fields with defaults — omit them and the default is used. If the
|
|
355
|
+
description: 'Trigger a Forge pipeline workflow (YAML under flows/). Two-step usage: (1) call with NO args first — returns every workflow + its input schema (which fields are required vs have defaults). (2) call again with workflow=<name> and input={...} passing ONLY required fields and any optional fields the user explicitly specified. NEVER pass invented placeholder values for optional fields with defaults — omit them and the default is used. On success returns JSON: {ok, pipeline_id, workflow, status, terminal, errors?, warning?, hint?}. If the run is still running, follow the hint — call start_watch on get_pipeline_status and STOP polling in this conversation.',
|
|
319
356
|
input_schema: {
|
|
320
357
|
type: 'object',
|
|
321
358
|
properties: {
|
|
@@ -337,7 +374,7 @@ export const BUILTIN_TOOL_DEFS: BuiltinToolDef[] = [
|
|
|
337
374
|
},
|
|
338
375
|
{
|
|
339
376
|
name: 'get_pipeline_status',
|
|
340
|
-
description: "Check a Forge pipeline run's live status + per-node results by id. Pass pipeline_id (returned by trigger_pipeline) to get
|
|
377
|
+
description: "Check a Forge pipeline run's live status + per-node results by id. Pass pipeline_id (returned by trigger_pipeline) to get a JSON object: {id, status: 'running'|'done'|'failed'|'cancelled', terminal: bool, workflowName, nodes: [{id, status, error?, outputs}]}. Use whenever the user asks how a running or finished pipeline is doing. For start_watch, pair this with done_match={path:\"status\",equals:\"done\"} (or done_path=\"terminal\" to fire on ANY terminal state) and fail_path=... per your needs.",
|
|
341
378
|
input_schema: {
|
|
342
379
|
type: 'object',
|
|
343
380
|
properties: {
|
|
@@ -356,7 +393,7 @@ export const BUILTIN_TOOL_DEFS: BuiltinToolDef[] = [
|
|
|
356
393
|
},
|
|
357
394
|
{
|
|
358
395
|
name: 'dispatch_task',
|
|
359
|
-
description: 'Dispatch a one-shot background Claude task in a Forge project. Use for longer-running asks the user wants to fire-and-forget ("analyze X codebase and write findings to a file", "run the test suite and summarize failures"). Returns
|
|
396
|
+
description: 'Dispatch a one-shot background Claude task in a Forge project. Use for longer-running asks the user wants to fire-and-forget ("analyze X codebase and write findings to a file", "run the test suite and summarize failures"). Returns JSON: {ok, task_id, project, status, hint}. The task runs in the background; if the user wants to be notified on completion, follow the hint — call start_watch on get_task_status and STOP polling in this conversation.',
|
|
360
397
|
input_schema: {
|
|
361
398
|
type: 'object',
|
|
362
399
|
properties: {
|
|
@@ -376,6 +413,20 @@ export const BUILTIN_TOOL_DEFS: BuiltinToolDef[] = [
|
|
|
376
413
|
required: ['prompt'],
|
|
377
414
|
},
|
|
378
415
|
},
|
|
416
|
+
{
|
|
417
|
+
name: 'get_task_status',
|
|
418
|
+
description: "Check a dispatched Forge task's status + result by id. Pass task_id (returned by dispatch_task). Returns JSON: {id, status: 'queued'|'running'|'done'|'failed'|'cancelled', terminal: bool, project, result_summary?, error?, completed_at?}. For start_watch, use done_path=\"terminal\" (fires on done/failed/cancelled) or done_match={path:\"status\",equals:\"done\"}.",
|
|
419
|
+
input_schema: {
|
|
420
|
+
type: 'object',
|
|
421
|
+
properties: {
|
|
422
|
+
task_id: {
|
|
423
|
+
type: 'string',
|
|
424
|
+
description: 'Task id (returned by dispatch_task).',
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
required: ['task_id'],
|
|
428
|
+
},
|
|
429
|
+
},
|
|
379
430
|
{
|
|
380
431
|
name: 'list_help_docs',
|
|
381
432
|
description: "List Forge's own documentation files. Call this FIRST whenever the user asks how Forge itself works — its features, settings/config, setup, or troubleshooting (e.g. pipelines, schedules, connectors, telegram, tunnel, workspace/smiths, skills, crafts, usage/cost, agents/models). Returns doc filenames; then read the relevant one(s) with read_help_doc and answer from their content. No arguments.",
|
|
@@ -36,11 +36,13 @@ export function buildStartWatchTool(sessionId: string | null): StartWatchTool {
|
|
|
36
36
|
name: 'start_watch',
|
|
37
37
|
description:
|
|
38
38
|
'Register a BACKGROUND WATCH that polls a tool until done, then posts the result back here — for long-running jobs you just kicked off (a Forge pipeline, a Jenkins build, a test run, a device upgrade). Use this INSTEAD of polling in conversation: call the trigger tool, then call start_watch and STOP — Forge polls in the background and a completion message arrives in this chat. ' +
|
|
39
|
-
'Pick `poll` = the read tool that reports status. Two forms accepted: (a) connector tool "<connector>.<tool>" e.g. "jenkins.get_build", "gitlab.get_pipeline"; (b) BARE builtin name
|
|
39
|
+
'Pick `poll` = the read tool that reports status. Two forms accepted: (a) connector tool "<connector>.<tool>" e.g. "jenkins.get_build", "gitlab.get_pipeline"; (b) BARE builtin name — "get_pipeline_status" (pair with poll_args={pipeline_id} and done_match={path:"status",equals:"done"}) or "get_task_status" (pair with poll_args={task_id} and done_path="terminal"). Set `poll_args` to call it with (e.g. the build number you predicted via get_next_build_number). ' +
|
|
40
|
+
'Give a done condition: `done_match` {path, equals} on the poll result (e.g. path "result" equals "SUCCESS"), or `done_path` (a result path that becomes truthy). You usually already saw the poll tool\'s output once, so you know the right field. Optional `fail_path` (truthy = failed). ' +
|
|
41
|
+
'If the poll tool returns NON-JSON text (e.g. some shell-protocol tools), the result is wrapped as {_raw: "...full output..."}, so use done_match={path:"_raw",contains:"DONE"} to grep markers in the output. Tune `interval_sec`/`timeout_sec` to the job (pipeline ≈ 30s / 1800s, build ≈ 60s / 1800s).',
|
|
40
42
|
input_schema: {
|
|
41
43
|
type: 'object',
|
|
42
44
|
properties: {
|
|
43
|
-
poll: { type: 'string', description: 'Tool to poll. Either "<connector>.<tool>" e.g. "jenkins.get_build", OR a bare builtin name
|
|
45
|
+
poll: { type: 'string', description: 'Tool to poll. Either "<connector>.<tool>" e.g. "jenkins.get_build", OR a bare builtin name — "get_pipeline_status" (Forge pipelines) or "get_task_status" (dispatched tasks). Must be a read/status tool.' },
|
|
44
46
|
poll_args: { type: 'object', description: 'Args to call the poll tool with each tick, e.g. {"job_path":"job/foo","build_number":18}. Concrete values, not templates.' },
|
|
45
47
|
done_match: {
|
|
46
48
|
type: 'object',
|
package/package.json
CHANGED