@aion0/forge 0.10.79 → 0.10.80
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 +8 -5
- package/app/api/tasks/[id]/hook/stop/route.ts +15 -0
- package/app/api/tasks/route.ts +2 -1
- package/cli/mw.mjs +7 -5
- package/cli/mw.ts +8 -6
- package/components/Dashboard.tsx +6 -2
- package/components/TaskDetail.tsx +28 -1
- package/components/TmuxTaskTerminal.tsx +105 -0
- package/components/WebTerminal.tsx +7 -0
- package/docs/design_automation_records/Automation Redesign.dc.html +2019 -0
- package/docs/design_automation_records/README.md +232 -0
- package/lib/chat/agent-loop.ts +6 -0
- package/lib/chat/tool-dispatcher.ts +110 -9
- package/lib/help-docs/05-pipelines.md +31 -0
- package/lib/help-docs/25-chat-tools.md +23 -0
- package/lib/pipeline.ts +27 -3
- package/lib/task-manager.ts +73 -3
- package/lib/task-tmux-backend.ts +625 -0
- package/lib/workspace/skill-installer.ts +18 -8
- package/package.json +1 -1
- package/proxy.ts +5 -4
- package/src/core/db/database.ts +1 -0
- package/src/types/index.ts +3 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# Handoff: Forge Automation — Pipeline & Task Redesign
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
This is a redesign of the **Automation** area of Forge (a CI/agentic-automation product). It restructures the area into four pages with a clearer information architecture and fixes two core pain points: pipeline runs took too many clicks to inspect, and task logs were spread across tabs.
|
|
5
|
+
|
|
6
|
+
The four pages:
|
|
7
|
+
1. **Pipeline** — view & edit pipeline *definitions* (steps, commands, triggers). No run data here.
|
|
8
|
+
2. **Pipeline Record** — a flat, filterable table of *all* pipeline execution records across every pipeline. Rows expand inline to show that run's own steps + the failed-step log. Failed runs can be **retried**; running runs can be **cancelled**.
|
|
9
|
+
3. **Task** — tasks are one-shot (created → run once → done), so there is no separate "task definition." This single page combines **Create Task** + a flat, filterable list of recent/running tasks. Rows expand inline into a compact log/result/diff cockpit.
|
|
10
|
+
4. **Task Detail** — a dedicated full page for a single task showing *everything*: command/prompt, full searchable log, lineage back to its pipeline run, a details sidebar, a phases timeline, and Result + Git Diff.
|
|
11
|
+
|
|
12
|
+
A persistent **top navigation** (Schedules · Pipeline · Pipeline Record · Task) switches between pages.
|
|
13
|
+
|
|
14
|
+
## About the Design Files
|
|
15
|
+
The file in this bundle (`Automation Redesign.dc.html`) is a **design reference created in HTML** — a working prototype showing the intended look and behavior. It is **not production code to copy directly.** It is authored in a bespoke template runtime ("Design Component" / `.dc.html`) used only for prototyping; do not try to reuse that runtime.
|
|
16
|
+
|
|
17
|
+
Your task is to **recreate these designs in the target codebase's existing environment** (React, Vue, Svelte, etc.) using its established component library, state patterns, data-fetching, and styling approach. If no front-end environment exists yet, pick the most appropriate framework for the project and implement there. Treat the HTML as the source of truth for layout, spacing, color, typography, copy, and interaction — but wire the data to real APIs and use the app's real components.
|
|
18
|
+
|
|
19
|
+
## Fidelity
|
|
20
|
+
**High-fidelity.** Colors, typography, spacing, and interactions are final and intended to be reproduced faithfully. Exact hex values, sizes, and copy are given below. The one caveat: all data in the prototype is **mock data** (modeled on a `fortinet-mantis-bug-fix-batch` pipeline). Replace it with real API data; keep the layouts and visual treatment.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Design Tokens
|
|
25
|
+
|
|
26
|
+
### Color — surfaces (dark, near-black terminal aesthetic)
|
|
27
|
+
| Token | Hex | Usage |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| `bg/app` | `#08090b` | App background, content area |
|
|
30
|
+
| `bg/deep` | `#070809` | Log panels, deepest wells |
|
|
31
|
+
| `bg/panel` | `#0b0c0e` | Card panel background |
|
|
32
|
+
| `bg/raised` | `#0d0e11` | Top bar, table headers, section headers |
|
|
33
|
+
| `bg/well` | `#0a0b0d` | Sidebars, output wells |
|
|
34
|
+
| `bg/card` | `#0f1013` | Overview cards |
|
|
35
|
+
| `bg/row-active` | `#121317` / `#15171b` | Selected/expanded row |
|
|
36
|
+
| `bg/input` | `#08090b` | Inputs, search fields |
|
|
37
|
+
| `bg/chip` | `#1a1d22` | Neutral buttons / segmented active |
|
|
38
|
+
|
|
39
|
+
### Color — borders & lines
|
|
40
|
+
| Token | Hex | Usage |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| `border/strong` | `#2a2e35` | Button borders, modal border |
|
|
43
|
+
| `border/default` | `#22252b` | Card borders, input borders |
|
|
44
|
+
| `border/line` | `#1d1f24` | Section dividers |
|
|
45
|
+
| `border/faint` | `#16181c` / `#131519` / `#15171a` | Row dividers, inner hairlines |
|
|
46
|
+
|
|
47
|
+
### Color — status & accent
|
|
48
|
+
| Token | Hex | Meaning |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| `accent/brand` (orange) | `#ff7d2e` | Primary action (Re-run, Create, New), brand mark |
|
|
51
|
+
| `status/passed` (green) | `#46c25a` | passed / done / exit 0 |
|
|
52
|
+
| `status/failed` (red) | `#f15b4a` | failed / cancelled / errors / exit 1 |
|
|
53
|
+
| `status/running` (blue) | `#4f9dff` | running + all hyperlinks / active tab |
|
|
54
|
+
| `status/skipped` (gray) | `#33363d` | skipped / pending step bars |
|
|
55
|
+
| `kind/llm` (purple) | `#a978f0` | LLM-prompt step badge, cost values |
|
|
56
|
+
| `kind/shell` (yellow) | `#d8a23f` | Shell-command step badge |
|
|
57
|
+
|
|
58
|
+
Task status colors: done = `#4f9dff` (blue), failed = `#f15b4a` (red), running = `#4f9dff` (blue, with pulse), other/dim = `#7d828b`.
|
|
59
|
+
|
|
60
|
+
### Color — text
|
|
61
|
+
| Token | Hex | Usage |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| `text/primary` | `#f3f4f6` | Headings, key values |
|
|
64
|
+
| `text/body` | `#cdd0d6` | Default body text |
|
|
65
|
+
| `text/secondary` | `#c2c6cd` / `#c9cdd3` | Row labels |
|
|
66
|
+
| `text/muted` | `#8b9098` / `#9aa0a8` | Descriptions |
|
|
67
|
+
| `text/dim` | `#7d828b` / `#6b7079` | Meta, timestamps |
|
|
68
|
+
| `text/faint` | `#565b63` / `#5e636b` | Uppercase column labels, line numbers |
|
|
69
|
+
| `text/ghost` | `#3f434b` / `#34373d` | Log line-number gutter, separators |
|
|
70
|
+
|
|
71
|
+
### Typography
|
|
72
|
+
- **Mono (primary UI + all data):** `'JetBrains Mono', ui-monospace, monospace`. Weights 400/500/600/700. This is the dominant typeface — IDs, logs, table cells, commands, most labels.
|
|
73
|
+
- **Sans (prose only):** `'Inter', sans-serif`. Used for page titles, descriptive paragraphs, and Overview cards.
|
|
74
|
+
- Sizes (px): page title 18–26 / 700; section heading 13–15 / 700; row text 11.5–13; meta 10–11; uppercase eyebrow labels 10–11 / 600 with `letter-spacing: 1px` and `text-transform: uppercase`.
|
|
75
|
+
- Line numbers and log text: 12–12.5px, `line-height: 1.9`.
|
|
76
|
+
|
|
77
|
+
### Spacing, radius, effects
|
|
78
|
+
- Radius: cards `12px`; inputs/buttons/chips `7–8px`; pills/badges `5–8px`; quick-filter chips (rounded) `16px`; status dots `50%`.
|
|
79
|
+
- Card border: `1px solid #20232a`, radius `12px`, `overflow: hidden`.
|
|
80
|
+
- Row vertical padding: `10–12px`; cell gap: `12px`.
|
|
81
|
+
- Shadows: modal `0 24px 60px rgba(0,0,0,.5)`; status dots get a glow `0 0 6–8px <color>66/77`.
|
|
82
|
+
- Status-dot glow pattern: `box-shadow: 0 0 7px <color>77`.
|
|
83
|
+
|
|
84
|
+
### Keyframes (animations)
|
|
85
|
+
```css
|
|
86
|
+
@keyframes blink { 0%,100%{opacity:1} 50%{opacity:.25} } /* live "running" dot */
|
|
87
|
+
@keyframes pulseRed{ 0%,100%{box-shadow:0 0 0 0 rgba(241,91,74,0)} 50%{box-shadow:0 0 0 4px rgba(241,91,74,.18)} } /* running step pulse */
|
|
88
|
+
@keyframes slideIn { from{opacity:0; transform:translateY(4px)} to{opacity:1; transform:none} } /* inline-expand reveal */
|
|
89
|
+
```
|
|
90
|
+
Scrollbars: 9px, thumb `#2b2e34` radius 6px, transparent track.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Screens / Views
|
|
95
|
+
|
|
96
|
+
### Global chrome
|
|
97
|
+
- **Top app bar** (`#0d0e11`, bottom border `#1d1f24`, padding `11px 20px`): orange rounded logo square (gradient `135deg, #ff7d2e, #ff5c4d`), "Forge / Automation" breadcrumb, and a right-aligned status legend (passed/failed/running/skipped swatches, 9×9px rounded `2px`).
|
|
98
|
+
- **Page top-nav** (inside each page card, `#0d0e11`, bottom border): tabs `Schedules · Pipeline · Pipeline Record · Task`. Active tab = `#f3f4f6` text + 2px bottom border `#4f9dff` + weight 600; inactive = `#6b7079`. (Schedules is inert/out-of-scope.) The Task tab is "active" for both the Task list and the Task Detail page.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
### 1. Pipeline (manage definitions)
|
|
103
|
+
**Purpose:** view and edit pipeline definitions — their ordered steps, each step's type (shell/LLM) and command/prompt, and trigger.
|
|
104
|
+
|
|
105
|
+
**Layout:** two-pane inside the page card, min-width 900px.
|
|
106
|
+
- **Left rail (248px, border-right):** header row "Pipelines · 6" + an orange `+ New` button (`#ff7d2e`, text `#0b0c0e`, radius 6px). Scrollable list of pipelines; each item: status dot (last-run color) + name (`12.5px/600`), and a sub-line with pipeline id (mono, `#6b7079`), step count, run count. Selected item: left border `2px #ff7d2e`, bg `#15171b`.
|
|
107
|
+
- **Right detail:** header (padding `18px 22px`, border-bottom) with pipeline name (`19px/700`), mono pipeline id, a `查看运行记录 →` link (blue) that jumps to Pipeline Record filtered to this pipeline, and an **Edit** toggle button (default: ghost `#1a1d22` border `#2a2e35`; when editing: green `#46c25a` bg, dark text, label "Done"). A meta row shows `trigger · steps · runs · last <status dot>`.
|
|
108
|
+
- **Steps list:** bordered container (`1px #1d1f24`, radius 10). Each step row (`12px 15px`, divider): when editing, a drag handle `⠿` (`#3f434b`) and a delete `✕`; index number (mono, faint); step name (`13px/600`); a kind badge (`shell` = yellow `#d8a23f`, `llm` = purple `#a978f0`, format `1px 7px` radius 5, bg `<color>1c`); and below, the command/prompt in mono `11.5px #7d828b`. When editing, an `+ Add step` link (green) appears above the list.
|
|
109
|
+
|
|
110
|
+
**Edit mode is visual only in the prototype** (drag handles, add/delete affordances appear) — implement real editing per the codebase (reorder, inline edit of name/command, add/remove).
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
### 2. Pipeline Record (all execution records)
|
|
115
|
+
**Purpose:** one flat table of every run across all pipelines; find a run by condition, expand it to see its own steps + the failed-step log, and retry/cancel.
|
|
116
|
+
|
|
117
|
+
**Layout:** page card, the table area is min-width 1040px and **horizontally scrollable**; an **outer wrapper scrolls (overflow:auto, max-height 560px) with the min-width on an inner div** so that a right-pinned actions column can stay visible (see "Pinned actions" below).
|
|
118
|
+
|
|
119
|
+
**Filter bar** (padding `13px 18px`, border-bottom, stacked rows, min-width 880px):
|
|
120
|
+
- Search input (flex, mono) — placeholder "按 run id / pipeline / 步骤名 筛选…", with a clear `×` when non-empty.
|
|
121
|
+
- A view toggle segmented control: **平铺 Flat** (default) / **按 pipeline 分组**.
|
|
122
|
+
- A count readout: `<total> runs · <failed> failed` (failed count in red).
|
|
123
|
+
- Segmented filter groups, each a pill-row in a `#0c0d10` bordered container (`padding:3px`, inner pills `6px 12px` radius 7): **Status** (All/Failed/Running/Passed — active colors red/blue/green), **Time** (24h/7d/30d/All — default 7d), **Trigger** (All/◷ Scheduled/☞ Manual/⚡ Webhook), **Pipeline** (All + each pipeline name; active = orange).
|
|
124
|
+
|
|
125
|
+
**Column header (sticky top, `#0a0b0d`):** chevron | Status | Pipeline ID | Pipeline | Run ID | Steps | Result | Trigger | Duration | Started | **Actions** (pinned right). Header labels are uppercase faint `10px`.
|
|
126
|
+
|
|
127
|
+
**Run row** (flex, gap 12, `11px 16px`, divider; expanded row: left border 2px status color + bg `#121317`):
|
|
128
|
+
- chevron `▸/▾` (faint, width 12)
|
|
129
|
+
- Status cell (width 74): status dot (8×8, glow) + status label in status color, `11px/600`
|
|
130
|
+
- Pipeline ID (width 90, mono `#6b7079`) — only in flat view
|
|
131
|
+
- Pipeline name (width 196, `12px/600`) — only in flat view
|
|
132
|
+
- Run ID (width 80, mono `#c9cdd3`)
|
|
133
|
+
- **Steps bar** (flex, min 64): a thin segmented bar — one `6px` segment per step colored by that step's status (green/red/blue/gray), `gap 1.5px`, radius 1
|
|
134
|
+
- Result/summary (width 150): `"failed · <step>"` (red) / `"running · <step>"` (blue) / `"N steps passed"` (dim)
|
|
135
|
+
- Trigger (width 92): glyph + name, dim
|
|
136
|
+
- Duration (width 64, right), Started (width 62, right)
|
|
137
|
+
- **Pinned Actions cell** (width ~104, `position: sticky; right: 0`, with a left-fading gradient background `linear-gradient(90deg, transparent, #0c0d10 26%)`, `pointer-events:none` on wrapper, `pointer-events:auto` on the button). Shows a quick action: **↻ Retry** (orange-tinted) for failed runs, **■ Cancel** (red-tinted) for running runs, nothing for passed. **This pinned column is essential** — without it the actions scroll off the right edge of the wide table.
|
|
138
|
+
|
|
139
|
+
**Group view** (`按 pipeline 分组`): a collapsible header per pipeline (`#0d0e11`): chevron, last-run dot, pipeline name (`13px/700`), run count, a red `N failed` badge, and a 12-cell recent-status sparkline on the right (each `7×14` rounded `2px`). Expanding shows that pipeline's rows (same row component, but without the pipeline id/name columns).
|
|
140
|
+
|
|
141
|
+
**Expanded run detail** (inline, `slideIn` animation, bg `#08090b`, left border = run status color):
|
|
142
|
+
- **Actions toolbar** (`#0d0e11`): an "Actions" label + buttons. Failed → `↻ Retry from <step>` + `↻ Re-run all`. Running → `■ Cancel` + `⊙ Follow live`. Passed → `↻ Re-run`. Buttons are neutral chips (`#1a1d22`, border `#2a2e35`).
|
|
143
|
+
- **This run's own steps** (horizontal, scrollable): a label "`N steps · 此 run 自己的步骤 · 点步骤切日志`" then a row of step pills — each pill: status glyph (✓/✕/◐/–) in status color + step name + (focused pill gets a colored border/bg; running pill pulses). Clicking a pill changes the focused step. **Important:** each run renders *its own* step list (runs are heterogeneous — different runs can have different steps), so never assume a fixed step schema.
|
|
144
|
+
- **Focused-step panel** (split): left = the step's **log** (mono `11.5px`, error lines red, `✓` green, `→` blue) with an `Open task detail →` link (blue) that opens the Task Detail page for that step's task; right (width ~268–300) = **Step output** key/value list (`key` faint width ~84–96, `value` mono colored).
|
|
145
|
+
|
|
146
|
+
**Defaults:** flat view, time = 7d, the `25b1fc48` (failed apply-fix) run pre-expanded.
|
|
147
|
+
|
|
148
|
+
**Retry behavior:** Retry creates a *new* run with status `running` prepended to the list (id like `re1_25b1`), and resets filters to show it. **Cancel** flips a running run to a cancelled state (rendered as failed, status label "cancelled").
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
### 3. Task (create + records, one-shot)
|
|
153
|
+
**Purpose:** create a one-shot task and browse recent/running tasks. Tasks have no separate definition — create = run.
|
|
154
|
+
|
|
155
|
+
**Header:** Inter title "Task · 任务" + a small "页面" tag; description paragraph explaining tasks are one-shot.
|
|
156
|
+
|
|
157
|
+
**Filter bar** (like Pipeline Record, min-width 980px):
|
|
158
|
+
- Search (mono) placeholder "按 task id / 命令 / 步骤 / pipeline 搜索…", count readout `<n> tasks · <failed> failed`, and an orange **`+ 新建任务`** button.
|
|
159
|
+
- **Quick filters** — a row of rounded (16px) chips: `全部` / `失败` (red) / `运行中` (blue) / `scratch` (yellow) / `有费用` (purple) / `今天` (blue). Active chip = colored border + `<color>1e` bg + colored text.
|
|
160
|
+
- Segmented filters: **Status** (All/Done/Failed/Running), **Provider** (All + distinct providers), **Time** (24h/7d/30d/All), **Pipeline** (All + names).
|
|
161
|
+
|
|
162
|
+
**Column header (sticky):** Status | Task ID | Pipeline · Step | Provider | Command | Cost | Dur | Started | (expand chevron).
|
|
163
|
+
|
|
164
|
+
**Task row** (flex, gap 12, `10px 16px`, divider; expanded: bg `#121317` + left border status color):
|
|
165
|
+
- Status cell (width 76): dot + status text in status color
|
|
166
|
+
- Task ID (width 86, mono `#c9cdd3`)
|
|
167
|
+
- Pipeline · Step (width 200): pipeline name on top (`12px`), step name below (`10.5px #6b7079`)
|
|
168
|
+
- Provider (width 80, `11.5px/600`; "scratch" = dim)
|
|
169
|
+
- Command (flex, mono `11px #8b9098`, ellipsized)
|
|
170
|
+
- Cost (width 58, purple, right), Dur (width 58, right), Started (width 62, right)
|
|
171
|
+
- expand chevron `▸/▾` (width 16, right)
|
|
172
|
+
|
|
173
|
+
**Inline expanded cockpit** (compact T3, bg `#070809`, left border status color):
|
|
174
|
+
- Header: live dot, `task://<id>`, step name (`12px/700`), status badge; right side: `打开完整详情 ↗` (blue → opens Task Detail page), plus `↻ Retry` (failed) / `■ Cancel` (running) / `Re-run`.
|
|
175
|
+
- A search + "errors N" toggle row (`#0a0b0d`).
|
|
176
|
+
- **Log** (mono `12.5px`, line numbers in ghost gutter, error lines get red left-gutter `3px` + faint red row bg) with a **right-edge minimap** (13px column; one `5px` mark per line; red marks = error lines).
|
|
177
|
+
- Below: **Result** (mono `11px`, `white-space:pre`) and **Git Diff** side by side (add lines green on `#46c25a14`, del red on `#f15b4a14`, hunk purple, meta dim).
|
|
178
|
+
|
|
179
|
+
**Create Task modal** (centered, overlay `rgba(4,5,7,.72)`, panel 560px, `#0e1013`, border `#2a2e35`, radius 14, shadow):
|
|
180
|
+
- Header "新建任务" + subtitle "创建即运行 · 一次性" + `×`.
|
|
181
|
+
- **Provider / Repo** `<select>` (FortiNAC / scratch / canary / regress / depbump / docs).
|
|
182
|
+
- **Type** segmented: `⌘ Shell` (active = yellow `#d8a23f` bg) / `✦ LLM Prompt` (active = purple `#a978f0` bg).
|
|
183
|
+
- **Command / Prompt** `<textarea>` (min-height 96, mono), placeholder changes with type.
|
|
184
|
+
- Footer: hint text, `取消`, and an orange **`▶ 创建并运行`** (disabled/greyed until the field is non-empty).
|
|
185
|
+
- On run: prepend a new task `{ status: running, provider, step: command|prompt, summary: <input> }` to the list, close modal, reset filters so it's visible.
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
### 4. Task Detail (full single-task page)
|
|
190
|
+
**Purpose:** the complete view of one task — richer than the inline peek. Reached from Pipeline Record's `Open task detail →` and the Task list's `打开完整详情 ↗`.
|
|
191
|
+
|
|
192
|
+
**Layout:** breadcrumb `← Task / Task Detail` above the card; inside the card: top-nav (Task active), header, lineage banner, a main/sidebar split, then Result + Diff full-width. Min-width 900px.
|
|
193
|
+
- **Header** (`18px 22px`, border-bottom): big status dot (11×11, glow), step name (`19px/700` mono), status badge, mono task id; right: **↻ Retry** (failed, solid orange) / **■ Cancel** (running, red-tinted) / **Re-run** (neutral).
|
|
194
|
+
- **Lineage banner** (`#0e1622`, border `#1d2b3d`, clickable): `来自 <pipeline name> › run <run id> (blue) › <step>` + right link `在 Pipeline Record 中查看 ↗`. Clicking jumps to Pipeline Record with that run expanded. Hidden for ad-hoc/scratch tasks.
|
|
195
|
+
- **Main column** (flex 1, border-right):
|
|
196
|
+
- **Command / Prompt** block: uppercase label, then a bordered well (`#070809`, border `#20232a`, radius 8) with the command in mono `12px`.
|
|
197
|
+
- **Log** header row: "Log" label + line count + an inline search field + "errors N" toggle.
|
|
198
|
+
- **Log body** (max-height 300, mono `12.5px/1.95`, line numbers ghost gutter width 26, error rows tinted) + **right minimap** (13px).
|
|
199
|
+
- **Sidebar** (width 280, `#0a0b0d`):
|
|
200
|
+
- **Details** key/value list: Task ID, Status (colored), Type (shell yellow / LLM purple), Provider/Repo, Exit code (0 green / 1 red / — dim), Started, Duration, Cost (purple), and for LLM tasks: Model (`claude-sonnet-4.6`) + Tokens; for bugfix pipeline: Branch (`fix/...`, blue). Each: faint key (width 108) + mono value.
|
|
201
|
+
- **Phases** timeline: queued → provision env → execute → collect output, each with a status glyph + label. Failed task: execute = ✕ red, collect = skipped; running: execute = ◐ blue, collect = pending; done: all green.
|
|
202
|
+
- **Result + Git Diff** (full-width, border-top, split): same treatment as the inline cockpit, max-height 200 each.
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## Interactions & Behavior
|
|
207
|
+
- **Navigation:** top-nav tabs and a left rail both set the active page. No full page reload; client-side view switch.
|
|
208
|
+
- **Row expand/collapse:** clicking a Pipeline Record run row or a Task row toggles an inline detail region (`slideIn`, 0.15s). Only one expanded at a time (single `expandedRun` / `expandedTask` id).
|
|
209
|
+
- **Step focus:** inside an expanded run, clicking a step pill swaps the focused-step log/output (per-run focus map).
|
|
210
|
+
- **Cross-links:** Pipeline → Pipeline Record (filtered); Pipeline Record step → Task Detail (mapped to that step's task); Task list/peek → Task Detail; Task Detail lineage → Pipeline Record (run expanded).
|
|
211
|
+
- **Retry:** (runs and tasks) spawns a new `running` record at the top; resets filters so it's visible.
|
|
212
|
+
- **Cancel:** flips a `running` record to cancelled (shown as failed).
|
|
213
|
+
- **Create Task:** modal; run button disabled until command/prompt non-empty; on submit prepend running task.
|
|
214
|
+
- **Search/filter:** all filters compose (AND). Search matches id / name / step / command / provider. Quick-filter chips are shortcuts that set the underlying filter state.
|
|
215
|
+
- **Live "running":** running dots use the `blink` keyframe; running steps/cells use `pulseRed`.
|
|
216
|
+
- **Pinned actions column** in Pipeline Record uses `position: sticky; right: 0` against a full-width scroll wrapper so Retry/Cancel never scroll out of view.
|
|
217
|
+
|
|
218
|
+
## State Management
|
|
219
|
+
Single view-model/state object (mirror in the target app's state layer):
|
|
220
|
+
- `activePage`: `'pipe' | 'p1' (pipeline record) | 'trecord' (task) | 'tdetail' (task detail)` (+ legacy exploration views, ignore).
|
|
221
|
+
- Pipeline Record: `recStatus, recPipe, recTime, recTrigger, recSearch, recGroup ('timeline'|'pipeline'), expandedRun, runFocus{runId:stepName}, expandedGroups{}, extraRuns[] (retried), cancelledRuns{}`.
|
|
222
|
+
- Pipeline mgmt: `selectedPipelineId, editMode`.
|
|
223
|
+
- Task: `trStatus, trProvider, trPipe, trTime, trSearch, trCost(bool), expandedTask, createdTasks[], cancelledTasks{}, createModalOpen, createForm{provider,mode,cmd}`.
|
|
224
|
+
- Task Detail: `selectedTaskId`, plus shared log controls `taskSearch, errorsOnly`.
|
|
225
|
+
- Data needs (replace mock): list of pipeline definitions (id, name, trigger, ordered steps each with type + command); list of run records (id, pipeline, status, started, duration, trigger, per-step statuses, focused/failed step, per-step logs + outputs); list of task records (id, status, provider, pipeline+step, command/prompt, cost, duration, started, parent run id, log lines, result JSON, git diff).
|
|
226
|
+
|
|
227
|
+
## Assets
|
|
228
|
+
None. No image/icon files — all glyphs are Unicode characters (`✓ ✕ ◐ – ▸ ▾ ↻ ■ ⊙ ⌕ ↗ → › ⠿ ⌘ ✦ ◷ ☞ ⚡`) and the only fonts are Google Fonts **JetBrains Mono** and **Inter**. Substitute the codebase's icon set if preferred; keep meanings.
|
|
229
|
+
|
|
230
|
+
## Files
|
|
231
|
+
- `Automation Redesign.dc.html` — the full prototype containing all four final pages (plus the top-nav, Overview, and earlier design explorations behind other nav items, which are reference-only).
|
|
232
|
+
- The earlier exploration rounds live in sibling files in the project (`Automation Redesign — Round 1/2.dc.html`) and are **not** part of this handoff — the four pages above are the settled design.
|
package/lib/chat/agent-loop.ts
CHANGED
|
@@ -402,6 +402,12 @@ function buildSystemPrompt(
|
|
|
402
402
|
'',
|
|
403
403
|
'- Reply without tools ONLY when no system + no time question is involved.',
|
|
404
404
|
'',
|
|
405
|
+
'',
|
|
406
|
+
'Files & task results — NEVER fabricate:',
|
|
407
|
+
'- A dispatched task\'s real deliverable is usually a FILE it wrote into the project repo (e.g. "docs/report.md"), NOT its result_summary. To get it, call read_project_file({project, path}). read_forge_file only reaches Forge\'s data dir (tmp/scratch/flows/...), NOT project repos.',
|
|
408
|
+
'- If a file read returns not-found / a tool fails, SAY SO plainly and report the path you tried. NEVER reconstruct, summarize-from-memory, or invent file contents — a guessed report presented as the real one is a serious error.',
|
|
409
|
+
'- To read any file (a task\'s output, a source file), prefer read_project_file / read_forge_file over spawning a dispatch_task. Never start a task just to cat/display a file.',
|
|
410
|
+
'',
|
|
405
411
|
'Other:',
|
|
406
412
|
'- Call get_current_time when asked about "now" or "today".',
|
|
407
413
|
'Keep replies short and direct.',
|
|
@@ -214,7 +214,7 @@ const BUILTINS: Record<string, BuiltinHandler> = {
|
|
|
214
214
|
// required (no default) vs optional (have default) so the agent can omit
|
|
215
215
|
// optional ones rather than passing wrong placeholder values.
|
|
216
216
|
trigger_pipeline: async (input) => {
|
|
217
|
-
const params = (input as { workflow?: string; input?: Record<string, unknown>; skills?: unknown } | undefined) || {};
|
|
217
|
+
const params = (input as { workflow?: string; input?: Record<string, unknown>; skills?: unknown; backend?: unknown } | undefined) || {};
|
|
218
218
|
const { listWorkflows, startPipeline, getPipeline, getWorkflow } = await import('../pipeline');
|
|
219
219
|
if (!params.workflow) {
|
|
220
220
|
const workflows = listWorkflows();
|
|
@@ -327,7 +327,13 @@ const BUILTINS: Record<string, BuiltinHandler> = {
|
|
|
327
327
|
}
|
|
328
328
|
}
|
|
329
329
|
|
|
330
|
-
|
|
330
|
+
// Runtime backend override: "use tmux" / "用 tmux 方式" makes every task in
|
|
331
|
+
// this run use the tmux backend, regardless of the workflow's declared default.
|
|
332
|
+
const backendOverride = params.backend === 'tmux' || params.backend === 'headless' ? params.backend : undefined;
|
|
333
|
+
const pipeline = startPipeline(params.workflow, stringInput, {
|
|
334
|
+
skills: skills.length ? skills : undefined,
|
|
335
|
+
backend: backendOverride,
|
|
336
|
+
});
|
|
331
337
|
const fresh = getPipeline(pipeline.id) || pipeline;
|
|
332
338
|
const errors: string[] = [];
|
|
333
339
|
if (pipeline.status === 'failed') {
|
|
@@ -522,6 +528,56 @@ const BUILTINS: Record<string, BuiltinHandler> = {
|
|
|
522
528
|
});
|
|
523
529
|
},
|
|
524
530
|
|
|
531
|
+
// Read a file from inside a PROJECT repo (the working dir a task ran in),
|
|
532
|
+
// NOT the Forge data dir. This is the companion to read_forge_file: a
|
|
533
|
+
// dispatch_task that writes `docs/report.md` writes it into the project
|
|
534
|
+
// repo, which resolveDataPath (read_forge_file) can never reach. Path is
|
|
535
|
+
// resolved relative to the project root with traversal + sensitive-file
|
|
536
|
+
// guards so it can't escape the repo or leak secrets.
|
|
537
|
+
read_project_file: async (input) => {
|
|
538
|
+
const params = (input as { project?: string; path?: string; filename?: string; as_base64?: boolean } | undefined) || {};
|
|
539
|
+
const projectName = (params.project || '').trim();
|
|
540
|
+
const rel = (params.path || params.filename || '').trim();
|
|
541
|
+
if (!projectName) return JSON.stringify({ ok: false, error: 'project is required (Forge project name — call list_forge_context for valid names)' });
|
|
542
|
+
if (!rel) return JSON.stringify({ ok: false, error: 'path is required (project-relative, e.g. "docs/report.md")' });
|
|
543
|
+
const { getProjectInfo } = await import('../projects');
|
|
544
|
+
const proj = getProjectInfo(projectName);
|
|
545
|
+
if (!proj) return JSON.stringify({ ok: false, error: `Project not found: ${projectName}. Call list_forge_context for valid names.` });
|
|
546
|
+
const { resolve, isAbsolute, sep } = await import('node:path');
|
|
547
|
+
const root = proj.path;
|
|
548
|
+
if (isAbsolute(rel) || rel.split(/[\\/]/).includes('..')) {
|
|
549
|
+
return JSON.stringify({ ok: false, error: 'path must be project-relative — no leading "/" and no ".." segments' });
|
|
550
|
+
}
|
|
551
|
+
const target = resolve(root, rel);
|
|
552
|
+
if (target !== root && !target.startsWith(root + sep)) {
|
|
553
|
+
return JSON.stringify({ ok: false, error: 'resolved path escapes the project directory' });
|
|
554
|
+
}
|
|
555
|
+
// Sensitive-file guard: never hand back secrets even from inside a repo.
|
|
556
|
+
if (/(^|\/)\.git\//.test(rel) || /(^|\/)\.env(\.|$)/i.test(rel) || /\.(pem|key|p12|pfx)$/i.test(rel) || /(^|\/)\.encrypt-key$/.test(rel) || /(^|\/)id_(rsa|ed25519)$/.test(rel)) {
|
|
557
|
+
return JSON.stringify({ ok: false, error: `Refused: "${rel}" looks like a sensitive file (git internals / env / private key).` });
|
|
558
|
+
}
|
|
559
|
+
const { readFile } = await import('node:fs/promises');
|
|
560
|
+
let buf: Buffer;
|
|
561
|
+
try { buf = await readFile(target); }
|
|
562
|
+
catch { return JSON.stringify({ ok: false, error: `File not found in project ${projectName}: ${rel}` }); }
|
|
563
|
+
if (params.as_base64) {
|
|
564
|
+
return JSON.stringify({
|
|
565
|
+
ok: true, project: projectName, path: rel,
|
|
566
|
+
local_path: target, file_url: `file://${target}`,
|
|
567
|
+
encoding: 'base64', size_bytes: buf.length, content: buf.toString('base64'),
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
const MAX = 256 * 1024;
|
|
571
|
+
const truncated = buf.length > MAX;
|
|
572
|
+
return JSON.stringify({
|
|
573
|
+
ok: true, project: projectName, path: rel,
|
|
574
|
+
local_path: target, file_url: `file://${target}`,
|
|
575
|
+
encoding: 'utf-8', size_bytes: buf.length, truncated,
|
|
576
|
+
content: buf.subarray(0, MAX).toString('utf8'),
|
|
577
|
+
...(truncated ? { note: `content truncated to ${MAX} bytes — use as_base64 for the full file` } : {}),
|
|
578
|
+
});
|
|
579
|
+
},
|
|
580
|
+
|
|
525
581
|
// Extract a zip/tar/gz archive sitting in tmp/ (e.g. one a connector just
|
|
526
582
|
// downloaded via the _files channel) into tmp/<base>-extracted/, then
|
|
527
583
|
// return the file listing so the agent can read_forge_file each entry.
|
|
@@ -657,7 +713,7 @@ const BUILTINS: Record<string, BuiltinHandler> = {
|
|
|
657
713
|
// required (defaults to 'scratch' if not given). Returns the task id; the
|
|
658
714
|
// caller can ask "what's the status of task <id>?" later — we don't block.
|
|
659
715
|
dispatch_task: async (input) => {
|
|
660
|
-
const params = (input as { project?: string; prompt?: string; agent?: string } | undefined) || {};
|
|
716
|
+
const params = (input as { project?: string; prompt?: string; agent?: string; backend?: string } | undefined) || {};
|
|
661
717
|
if (!params.prompt) return JSON.stringify({ ok: false, error: 'prompt is required' });
|
|
662
718
|
const { getProjectInfo, SCRATCH_PROJECT_NAME } = await import('../projects');
|
|
663
719
|
const projectName = params.project?.trim() || SCRATCH_PROJECT_NAME;
|
|
@@ -670,6 +726,7 @@ const BUILTINS: Record<string, BuiltinHandler> = {
|
|
|
670
726
|
prompt: params.prompt,
|
|
671
727
|
conversationId: '',
|
|
672
728
|
agent: params.agent || undefined,
|
|
729
|
+
backend: params.backend === 'tmux' ? 'tmux' : undefined,
|
|
673
730
|
});
|
|
674
731
|
return JSON.stringify({
|
|
675
732
|
ok: true,
|
|
@@ -701,20 +758,37 @@ const BUILTINS: Record<string, BuiltinHandler> = {
|
|
|
701
758
|
// so start_watch can poll via done_path="terminal" or done_match
|
|
702
759
|
// {path:"status", equals:"done"}.
|
|
703
760
|
get_task_status: async (input) => {
|
|
704
|
-
const params = (input as { task_id?: string } | undefined) || {};
|
|
761
|
+
const params = (input as { task_id?: string; full?: boolean } | undefined) || {};
|
|
705
762
|
if (!params.task_id) return JSON.stringify({ ok: false, error: 'task_id is required (returned by dispatch_task)' });
|
|
706
763
|
const { getTask } = await import('../task-manager');
|
|
707
764
|
const task = getTask(params.task_id);
|
|
708
765
|
if (!task) return JSON.stringify({ ok: false, error: `Task "${params.task_id}" not found` });
|
|
709
|
-
|
|
766
|
+
// result_summary is a short headline (capped in DB). With full:true we also
|
|
767
|
+
// return the tail of the task's own log so the caller can see the real
|
|
768
|
+
// narration instead of guessing — but if the task wrote a file, the file is
|
|
769
|
+
// the deliverable; read it with read_project_file rather than parsing this.
|
|
770
|
+
const summaryCap = params.full ? 4000 : 1000;
|
|
771
|
+
const base: Record<string, unknown> = {
|
|
710
772
|
id: task.id,
|
|
711
773
|
status: task.status,
|
|
712
774
|
terminal: task.status === 'done' || task.status === 'failed' || task.status === 'cancelled',
|
|
713
775
|
project: task.projectName,
|
|
714
|
-
...(task.resultSummary ? { result_summary: String(task.resultSummary).slice(0,
|
|
776
|
+
...(task.resultSummary ? { result_summary: String(task.resultSummary).slice(0, summaryCap) } : {}),
|
|
715
777
|
...(task.error ? { error: String(task.error).slice(0, 500) } : {}),
|
|
716
778
|
...(task.completedAt ? { completed_at: task.completedAt } : {}),
|
|
717
|
-
}
|
|
779
|
+
};
|
|
780
|
+
if (params.full) {
|
|
781
|
+
const { getTaskLogSlice } = await import('../task-manager');
|
|
782
|
+
const { entries } = getTaskLogSlice(task.id, { limit: 60, truncate: 4000 });
|
|
783
|
+
const text = entries
|
|
784
|
+
.map((e: any) => (typeof e.content === 'string' ? e.content : ''))
|
|
785
|
+
.filter(Boolean)
|
|
786
|
+
.join('\n')
|
|
787
|
+
.slice(-12000);
|
|
788
|
+
if (text) base.output_tail = text;
|
|
789
|
+
base.note = 'output_tail = the task\'s own log narration (last entries). If the task wrote a file in the project repo (e.g. docs/report.md), THAT file is the real deliverable — read it with read_project_file. Never reconstruct/guess file contents.';
|
|
790
|
+
}
|
|
791
|
+
return JSON.stringify(base);
|
|
718
792
|
},
|
|
719
793
|
|
|
720
794
|
// List Forge's own help/documentation files so the chat agent can answer
|
|
@@ -963,6 +1037,11 @@ export const BUILTIN_TOOL_DEFS: BuiltinToolDef[] = [
|
|
|
963
1037
|
items: { type: 'string' },
|
|
964
1038
|
description: 'Forge skills (by name) to make available to every Claude task inside the pipeline — injected via --append-system-prompt. Pass when the user mentions skill names ("用 git-savvy", "with the code-reviewer skill"). Call list_forge_context to validate names. Omit if the user didn\'t mention any.',
|
|
965
1039
|
},
|
|
1040
|
+
backend: {
|
|
1041
|
+
type: 'string',
|
|
1042
|
+
enum: ['tmux', 'headless'],
|
|
1043
|
+
description: 'Execution backend override for EVERY task in this run. Set "tmux" when the user says "use tmux" / "用 tmux 方式" / "subscription mode" (interactive claude, subscription billing). Set "headless" to force default claude -p. OMIT to honor whatever the workflow YAML declares (default headless). This overrides the workflow\'s own backend: field.',
|
|
1044
|
+
},
|
|
966
1045
|
},
|
|
967
1046
|
},
|
|
968
1047
|
},
|
|
@@ -987,7 +1066,7 @@ export const BUILTIN_TOOL_DEFS: BuiltinToolDef[] = [
|
|
|
987
1066
|
},
|
|
988
1067
|
{
|
|
989
1068
|
name: 'dispatch_task',
|
|
990
|
-
description: 'Dispatch a one-shot background Claude CLI task in a Forge project. EXPENSIVE — spawns a fresh Claude subprocess that reads/edits code in the target project. Use ONLY for genuine codebase work: "analyze X repo and write findings", "run the test suite and summarize", "fix bug in <project>", "refactor module Y". \n\nDO NOT use for: \n • Saving a file with content YOU already have → use save_tmp_file. \n • Reading a Forge-owned file (tmp/scratch/flows/prompts/...) → use read_forge_file. \n • Listing files in <dataDir>/ → use list_forge_files. \n • Running a pipeline → use trigger_pipeline. \n • Inspecting a saved task → use get_task_status. \n\nFor "create / write / save a file with this content" the right tool is ALWAYS save_tmp_file — the LLM has the content, no CLI subprocess needed. \n\nIf the user\'s ask is ambiguous (might be a quick save vs a real codebase task), STOP and ask before dispatching — a user reporting "I just wanted a file" after seeing a task spawn is a clear signal you misclassified. \n\nReturns JSON: {ok, task_id, project, status, hint}. The task runs in the background; if the user wants completion notification, follow the hint — call start_watch on get_task_status and STOP polling in this conversation.',
|
|
1069
|
+
description: 'Dispatch a one-shot background Claude CLI task in a Forge project. EXPENSIVE — spawns a fresh Claude subprocess that reads/edits code in the target project. Use ONLY for genuine codebase work: "analyze X repo and write findings", "run the test suite and summarize", "fix bug in <project>", "refactor module Y". \n\nDO NOT use for: \n • Saving a file with content YOU already have → use save_tmp_file. \n • Reading a Forge-owned file (tmp/scratch/flows/prompts/...) → use read_forge_file. \n • Reading a file inside a PROJECT repo (e.g. a report a prior task wrote to docs/X.md, or any source file) → use read_project_file. NEVER spawn a task just to cat/display a file. \n • Listing files in <dataDir>/ → use list_forge_files. \n • Running a pipeline → use trigger_pipeline. \n • Inspecting a saved task → use get_task_status. \n\nFor "create / write / save a file with this content" the right tool is ALWAYS save_tmp_file — the LLM has the content, no CLI subprocess needed. \n\nIf the user\'s ask is ambiguous (might be a quick save vs a real codebase task), STOP and ask before dispatching — a user reporting "I just wanted a file" after seeing a task spawn is a clear signal you misclassified. \n\nReturns JSON: {ok, task_id, project, status, hint}. The task runs in the background; if the user wants completion notification, follow the hint — call start_watch on get_task_status and STOP polling in this conversation.',
|
|
991
1070
|
input_schema: {
|
|
992
1071
|
type: 'object',
|
|
993
1072
|
properties: {
|
|
@@ -1003,6 +1082,11 @@ export const BUILTIN_TOOL_DEFS: BuiltinToolDef[] = [
|
|
|
1003
1082
|
type: 'string',
|
|
1004
1083
|
description: 'Optional agent id override. Omit to use the project default.',
|
|
1005
1084
|
},
|
|
1085
|
+
backend: {
|
|
1086
|
+
type: 'string',
|
|
1087
|
+
enum: ['tmux'],
|
|
1088
|
+
description: 'Set to "tmux" to run via interactive tmux session (subscription billing, no API key needed). Omit for default (claude -p, API billing). Use tmux when the user says "use tmux", "subscription mode", or "interactive mode". Set it based on the CURRENT user request only — do NOT carry a previous task\'s tmux choice onto unrelated follow-ups (e.g. a trivial file read), and prefer read_project_file/read_forge_file over a task entirely for reads.',
|
|
1089
|
+
},
|
|
1006
1090
|
},
|
|
1007
1091
|
required: ['prompt'],
|
|
1008
1092
|
},
|
|
@@ -1037,6 +1121,19 @@ export const BUILTIN_TOOL_DEFS: BuiltinToolDef[] = [
|
|
|
1037
1121
|
required: ['filename'],
|
|
1038
1122
|
},
|
|
1039
1123
|
},
|
|
1124
|
+
{
|
|
1125
|
+
name: 'read_project_file',
|
|
1126
|
+
description: 'Read a file from inside a PROJECT repo — the working directory a dispatch_task ran in. THIS is how you read a file a task produced (e.g. a task that "writes findings to docs/report.md" puts it in the project repo, NOT the Forge data dir — read_forge_file CANNOT reach it). Whenever get_task_status says a task wrote/saved a file at a repo-relative path, read it here. `project` is the Forge project name (same value you pass to dispatch_task/pipeline input.project); `path` is repo-relative ("docs/report.md", "src/foo.ts"). Returns decoded UTF-8 (capped 256KB; as_base64:true for binary). Path traversal (../) and sensitive files (.git internals, .env, private keys) are refused. NEVER fabricate file contents — if this returns not-found, tell the user; do not reconstruct from memory.',
|
|
1127
|
+
input_schema: {
|
|
1128
|
+
type: 'object',
|
|
1129
|
+
properties: {
|
|
1130
|
+
project: { type: 'string', description: 'Forge project name (e.g. "FortiNAC"). Call list_forge_context if unsure of valid names.' },
|
|
1131
|
+
path: { type: 'string', description: 'Repo-relative path, e.g. "docs/mantis-1296959-analysis.md", "src/index.ts". No leading "/" and no ".." segments.' },
|
|
1132
|
+
as_base64: { type: 'boolean', description: 'Return raw bytes base64-encoded instead of decoded UTF-8. Use for binary files.' },
|
|
1133
|
+
},
|
|
1134
|
+
required: ['project', 'path'],
|
|
1135
|
+
},
|
|
1136
|
+
},
|
|
1040
1137
|
{
|
|
1041
1138
|
name: 'extract_archive',
|
|
1042
1139
|
description: 'Unpack an archive sitting in tmp/ (e.g. one a connector just downloaded — owa.download_attachment etc.) into tmp/<base>-extracted/, and return the file listing. USE THIS when an attachment / download is a .zip / .tar / .tar.gz / .tgz / .gz and you need to read what is inside — you have no shell, this runs unzip/tar for you. Then read individual entries with read_forge_file using each returned `path`. Returns JSON {ok, extracted_dir, count, files:[{path,size_bytes}]}.',
|
|
@@ -1073,7 +1170,7 @@ export const BUILTIN_TOOL_DEFS: BuiltinToolDef[] = [
|
|
|
1073
1170
|
},
|
|
1074
1171
|
{
|
|
1075
1172
|
name: 'get_task_status',
|
|
1076
|
-
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\"}.",
|
|
1173
|
+
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?, output_tail?}. `result_summary` is a SHORT headline — if the task wrote a file in the project repo (e.g. docs/report.md), read that file with read_project_file; it is the real deliverable. Set full:true to also get `output_tail` (the task's own log narration) when no file was written. NEVER reconstruct/guess a task's output from memory. For start_watch, use done_path=\"terminal\" (fires on done/failed/cancelled) or done_match={path:\"status\",equals:\"done\"}.",
|
|
1077
1174
|
input_schema: {
|
|
1078
1175
|
type: 'object',
|
|
1079
1176
|
properties: {
|
|
@@ -1081,6 +1178,10 @@ export const BUILTIN_TOOL_DEFS: BuiltinToolDef[] = [
|
|
|
1081
1178
|
type: 'string',
|
|
1082
1179
|
description: 'Task id (returned by dispatch_task).',
|
|
1083
1180
|
},
|
|
1181
|
+
full: {
|
|
1182
|
+
type: 'boolean',
|
|
1183
|
+
description: 'Also return `output_tail` (tail of the task log) and a longer result_summary. Use when result_summary is too short to answer and the task did NOT write a file you can read with read_project_file.',
|
|
1184
|
+
},
|
|
1084
1185
|
},
|
|
1085
1186
|
required: ['task_id'],
|
|
1086
1187
|
},
|
|
@@ -109,6 +109,37 @@ pipeline.
|
|
|
109
109
|
| `outputs` | Extract results (see Output Extraction) | `[]` |
|
|
110
110
|
| `routes` | Conditional routing to next nodes (see Routing) | `[]` |
|
|
111
111
|
| `max_iterations` | Max loop iterations for routed nodes | `3` |
|
|
112
|
+
| `backend` | `tmux` (interactive claude, subscription billing) or `headless` (`claude -p`). Overrides the workflow-level `backend`. | inherits workflow |
|
|
113
|
+
|
|
114
|
+
### Execution backend (`backend: tmux`)
|
|
115
|
+
|
|
116
|
+
By default every node runs **headless** (`claude -p`, API billing). Set a
|
|
117
|
+
top-level `backend: tmux` to run all nodes as interactive claude inside a
|
|
118
|
+
dedicated per-node tmux session (subscription billing, no API key). A node can
|
|
119
|
+
override with its own `backend:`.
|
|
120
|
+
|
|
121
|
+
```yaml
|
|
122
|
+
name: my-pipeline
|
|
123
|
+
backend: tmux # all nodes use tmux by default
|
|
124
|
+
nodes:
|
|
125
|
+
build:
|
|
126
|
+
project: my-app
|
|
127
|
+
prompt: "..."
|
|
128
|
+
deploy:
|
|
129
|
+
backend: headless # this one node opts back to claude -p
|
|
130
|
+
project: my-app
|
|
131
|
+
prompt: "..."
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Each node still gets its **own** tmux session (`fgt-<taskId>`). Sharing one
|
|
135
|
+
session across nodes is not yet supported.
|
|
136
|
+
|
|
137
|
+
**Runtime override from chat.** You don't have to edit the YAML — when you
|
|
138
|
+
fire a pipeline from chat you can say *"fix bug 1234 with the
|
|
139
|
+
mantis-bug-fix pipeline, **use tmux**"* and the assistant passes
|
|
140
|
+
`backend: tmux` to `trigger_pipeline`, switching every task in that run to
|
|
141
|
+
tmux regardless of the workflow's declared default. `backend: headless`
|
|
142
|
+
forces the opposite. Omitting it honors the YAML.
|
|
112
143
|
|
|
113
144
|
## Node Modes
|
|
114
145
|
|
|
@@ -46,6 +46,24 @@ chat agent can't accidentally leak these into chat context.
|
|
|
46
46
|
|
|
47
47
|
Pass `as_base64: true` for binary files (pdf, images, zip).
|
|
48
48
|
|
|
49
|
+
### `read_project_file` — read a file inside a PROJECT repo
|
|
50
|
+
`read_forge_file` only reaches `<dataDir>/`. When a `dispatch_task` writes
|
|
51
|
+
its findings to a repo file (e.g. *"document your analysis in
|
|
52
|
+
`docs/report.md`"*), that file lands in the **project working directory**,
|
|
53
|
+
not the Forge data dir — so `read_forge_file` returns "not found". Use
|
|
54
|
+
`read_project_file({project, path})` instead:
|
|
55
|
+
|
|
56
|
+
- `project` — the Forge project name (same value as `dispatch_task` /
|
|
57
|
+
pipeline `input.project`, e.g. `FortiNAC`).
|
|
58
|
+
- `path` — repo-relative (`docs/report.md`, `src/index.ts`). No leading
|
|
59
|
+
`/`, no `..` segments.
|
|
60
|
+
|
|
61
|
+
Path traversal and sensitive files (`.git/` internals, `.env`, private
|
|
62
|
+
keys) are refused. Returns decoded UTF-8 (capped 256 KB; `as_base64: true`
|
|
63
|
+
for binary). **Never** dispatch a `cat` task to read a file — and never
|
|
64
|
+
reconstruct file contents from memory if the read fails; report the path
|
|
65
|
+
that wasn't found.
|
|
66
|
+
|
|
49
67
|
### `list_forge_files` — list files anywhere under `<dataDir>/`
|
|
50
68
|
Pass `dir` as a dataDir-relative subdir (`tmp`, `scratch`, `flows`,
|
|
51
69
|
`connectors/mantis`). Each entry returns `path`, `kind` (file/dir),
|
|
@@ -84,6 +102,11 @@ Returns `status`, `terminal`, `result_summary` (truncated to 1KB),
|
|
|
84
102
|
`error`, `completed_at`. For long-running polls, prefer `start_watch`
|
|
85
103
|
over manual polling — see `24-watch.md`.
|
|
86
104
|
|
|
105
|
+
`result_summary` is a short headline. If the task's real output is a file
|
|
106
|
+
it wrote into the repo, read that file with `read_project_file` — that's
|
|
107
|
+
the deliverable. Pass `full: true` to also get `output_tail` (the tail of
|
|
108
|
+
the task's own log) and a longer summary when no file was written.
|
|
109
|
+
|
|
87
110
|
## Pipelines + schedules
|
|
88
111
|
|
|
89
112
|
The chat agent owns the full schedule CRUD surface and pipeline triggers:
|