@aion0/forge 0.9.7 → 0.9.10
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
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
# Forge v0.9.
|
|
1
|
+
# Forge v0.9.10
|
|
2
2
|
|
|
3
3
|
Released: 2026-05-26
|
|
4
4
|
|
|
5
|
-
## Changes since v0.9.
|
|
5
|
+
## Changes since v0.9.9
|
|
6
6
|
|
|
7
7
|
### Other
|
|
8
|
-
-
|
|
9
|
-
-
|
|
8
|
+
- docs(pipelines): >→| switch audit checklist + v0.7.x case study
|
|
9
|
+
- fix(pipeline): for_each retry rewinds currentIndex past finalize
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
**Full Changelog**: https://github.com/aiwatching/forge/compare/v0.9.
|
|
12
|
+
**Full Changelog**: https://github.com/aiwatching/forge/compare/v0.9.9...v0.9.10
|
|
@@ -74,6 +74,7 @@ function toConnectorPayload(def: ConnectorDefinition, config: Record<string, any
|
|
|
74
74
|
...(hostMatch ? { host_match: hostMatch } : {}),
|
|
75
75
|
...(loginRedirect ? { login_redirect: loginRedirect } : {}),
|
|
76
76
|
runner,
|
|
77
|
+
...(def.tab_strategy ? { tab_strategy: def.tab_strategy } : {}),
|
|
77
78
|
/** Hint for the marketplace UI: a Test button is wired for this connector. */
|
|
78
79
|
has_test: !!def.test,
|
|
79
80
|
/** Optional human description of what the test does (no request shape exposed). */
|
|
@@ -360,7 +360,7 @@ function DagNodeCard({ nodeId, node, nodeDef, onViewTask, onRetry }: {
|
|
|
360
360
|
</button>
|
|
361
361
|
)}
|
|
362
362
|
{node.iterations > 1 && <span className="text-[9px] text-yellow-400">iter {node.iterations}</span>}
|
|
363
|
-
{node.status === 'failed' && onRetry && (
|
|
363
|
+
{(node.status === 'failed' || node.status === 'cancelled') && onRetry && (
|
|
364
364
|
<button
|
|
365
365
|
onClick={async () => { setRetryBusy(true); try { await onRetry(nodeId); } finally { setRetryBusy(false); } }}
|
|
366
366
|
disabled={retryBusy}
|
package/lib/connectors/types.ts
CHANGED
|
@@ -211,6 +211,18 @@ export interface ConnectorDefinition {
|
|
|
211
211
|
*/
|
|
212
212
|
runner?: ConnectorRunner;
|
|
213
213
|
|
|
214
|
+
/**
|
|
215
|
+
* How the runner acquires a tab to execute scripts in:
|
|
216
|
+
* - 'reuse' (default): query existing tabs matching host_match;
|
|
217
|
+
* navigate the first match. Saves tab churn but hijacks whatever
|
|
218
|
+
* page the user was viewing.
|
|
219
|
+
* - 'ephemeral': always open a fresh background tab; close it after
|
|
220
|
+
* the tool returns. User's own tabs are untouched. Slightly slower
|
|
221
|
+
* (5–10s per call on cold load) but invisible to the user. Use for
|
|
222
|
+
* connectors hit frequently by background pipelines (Mantis, etc.).
|
|
223
|
+
*/
|
|
224
|
+
tab_strategy?: 'reuse' | 'ephemeral';
|
|
225
|
+
|
|
214
226
|
// ─── 1:1 connector — top-level fields ──────────────────
|
|
215
227
|
/** Most connectors expose tools directly here. */
|
|
216
228
|
tools?: Record<string, ConnectorTool>;
|
|
@@ -108,6 +108,35 @@ relies on the discipline of blank lines between every statement. But for
|
|
|
108
108
|
any **new** node or **substantial rewrite**, use `|`. Don't sprinkle
|
|
109
109
|
shell into existing `>` nodes without re-checking blank-line spacing.
|
|
110
110
|
|
|
111
|
+
**Switching an existing node from `>` to `|` is NOT a 1-line change.**
|
|
112
|
+
Code originally written for `>` may have *deliberately* split a single
|
|
113
|
+
bash command across two yaml lines, relying on `>` to re-fold them into
|
|
114
|
+
one bash command line. Common shape:
|
|
115
|
+
|
|
116
|
+
```yaml
|
|
117
|
+
prompt: > # folded
|
|
118
|
+
RESULT=$(python3 - "$ARG1" "$ARG2" # ← these two yaml
|
|
119
|
+
"$ARG3" "$ARG4" <<'PY' # lines become ONE bash command
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Under `>`: `RESULT=$(python3 - "$ARG1" "$ARG2" "$ARG3" "$ARG4" <<'PY'` (good).
|
|
123
|
+
Under `|`: two separate bash statements, the second starting with
|
|
124
|
+
`"$ARG3"` — bash will try to expand and exec the variable as a command,
|
|
125
|
+
giving `command not found: <ARG3's value>`. Real-world failure mode from
|
|
126
|
+
fortinet-mantis-bug-fix v0.7.1 → v0.7.2.
|
|
127
|
+
|
|
128
|
+
Audit checklist when switching `>` → `|`:
|
|
129
|
+
|
|
130
|
+
1. Grep for `\$\(` followed by content that wraps across multiple yaml
|
|
131
|
+
lines without a `\` continuation. Collapse those to a single yaml line,
|
|
132
|
+
OR add explicit `\` continuations.
|
|
133
|
+
2. Grep for any non-blank yaml line whose previous non-blank line doesn't
|
|
134
|
+
end in `;`, `&&`, `||`, `|`, `\`, `{`, `(`, `then`, `else`, `do`,
|
|
135
|
+
`fi`, `done` — that's a candidate for "two yaml lines being folded
|
|
136
|
+
into one bash command under `>`".
|
|
137
|
+
3. Run `node -e "console.log(YAML.parse(fs.readFileSync('foo.yaml','utf8')).nodes['NODE'].prompt)" | head -200` and visually scan for any
|
|
138
|
+
`RESULT=$(cmd arg1\n"arg2"...)` shapes that won't survive the switch.
|
|
139
|
+
|
|
111
140
|
### Rule 2 — Don't use `eval $(echo "$X" | sed 's/^/export /')` to import env vars
|
|
112
141
|
|
|
113
142
|
This idiom looks innocent but breaks the moment any value contains a space,
|
package/lib/pipeline.ts
CHANGED
|
@@ -1562,12 +1562,13 @@ export async function retryNode(pipelineId: string, nodeId: string): Promise<{ o
|
|
|
1562
1562
|
|
|
1563
1563
|
const nodeState = pipeline.nodes[nodeId];
|
|
1564
1564
|
if (!nodeState) return { ok: false, error: `node '${nodeId}' is not in this pipeline` };
|
|
1565
|
-
// Allow retry on 'failed' OR 'running'. Running covers
|
|
1566
|
-
// forge-restart-orphaned case
|
|
1567
|
-
//
|
|
1568
|
-
//
|
|
1569
|
-
|
|
1570
|
-
|
|
1565
|
+
// Allow retry on 'failed' OR 'running' OR 'cancelled'. Running covers
|
|
1566
|
+
// the forge-restart-orphaned case (node thinks it's executing but the
|
|
1567
|
+
// underlying task is dead); cancelled covers user-cancelled pipelines
|
|
1568
|
+
// where the user later wants to resume from the cancelled node.
|
|
1569
|
+
// pending/done/skipped are still misclicks.
|
|
1570
|
+
if (nodeState.status !== 'failed' && nodeState.status !== 'running' && nodeState.status !== 'cancelled') {
|
|
1571
|
+
return { ok: false, error: `node is in status '${nodeState.status}' — only failed, running, or cancelled nodes can be retried` };
|
|
1571
1572
|
}
|
|
1572
1573
|
|
|
1573
1574
|
// If the node is "running", best-effort terminate the underlying task
|
|
@@ -1620,11 +1621,34 @@ export async function retryNode(pipelineId: string, nodeId: string): Promise<{ o
|
|
|
1620
1621
|
// Don't reset s.iterations — keep monotonic so retries show up in history.
|
|
1621
1622
|
}
|
|
1622
1623
|
|
|
1623
|
-
if (pipeline.status === 'failed' || pipeline.status === 'done') {
|
|
1624
|
+
if (pipeline.status === 'failed' || pipeline.status === 'done' || pipeline.status === 'cancelled') {
|
|
1624
1625
|
pipeline.status = 'running';
|
|
1625
1626
|
pipeline.completedAt = undefined;
|
|
1626
1627
|
}
|
|
1627
1628
|
|
|
1629
|
+
// for_each: when retry fires after the pipeline finalized, currentIndex
|
|
1630
|
+
// has already been ++'d past the last iter, so items[currentIndex] would
|
|
1631
|
+
// be undefined and the re-run would see {{<as>}} as empty. Rewind to the
|
|
1632
|
+
// last iter and pop its snapshot — checkPipelineCompletion will re-add it
|
|
1633
|
+
// when the retried nodes settle.
|
|
1634
|
+
//
|
|
1635
|
+
// Only handles the typical case (retry a node from the LAST iter).
|
|
1636
|
+
// Retrying an earlier failed iter when later iters already succeeded is
|
|
1637
|
+
// not supported here — the main DAG only reflects the last iter's state
|
|
1638
|
+
// anyway, so the user can't actually click on an earlier iter's node.
|
|
1639
|
+
if (
|
|
1640
|
+
pipeline.forEach &&
|
|
1641
|
+
pipeline.forEach.itemsResolved &&
|
|
1642
|
+
pipeline.forEach.total > 0 &&
|
|
1643
|
+
pipeline.forEach.currentIndex >= pipeline.forEach.total
|
|
1644
|
+
) {
|
|
1645
|
+
pipeline.forEach.currentIndex = pipeline.forEach.total - 1;
|
|
1646
|
+
const lastSnap = pipeline.forEach.iterations[pipeline.forEach.iterations.length - 1];
|
|
1647
|
+
if (lastSnap && lastSnap.index === pipeline.forEach.currentIndex) {
|
|
1648
|
+
pipeline.forEach.iterations.pop();
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1628
1652
|
savePipeline(pipeline);
|
|
1629
1653
|
await scheduleReadyNodes(pipeline, workflow);
|
|
1630
1654
|
return { ok: true };
|
package/package.json
CHANGED