@aion0/forge 0.9.8 → 0.9.11

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,8 +1,8 @@
1
- # Forge v0.9.8
1
+ # Forge v0.9.11
2
2
 
3
3
  Released: 2026-05-26
4
4
 
5
- ## Changes since v0.9.7
5
+ ## Changes since v0.9.10
6
6
 
7
7
 
8
- **Full Changelog**: https://github.com/aiwatching/forge/compare/v0.9.7...v0.9.8
8
+ **Full Changelog**: https://github.com/aiwatching/forge/compare/v0.9.10...v0.9.11
@@ -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). */
@@ -88,7 +88,7 @@ interface Workflow {
88
88
  }
89
89
 
90
90
  interface PipelineNodeState {
91
- status: 'pending' | 'running' | 'done' | 'failed' | 'skipped';
91
+ status: 'pending' | 'running' | 'done' | 'failed' | 'skipped' | 'cancelled';
92
92
  taskId?: string;
93
93
  outputs: Record<string, string>;
94
94
  iterations: number;
@@ -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}
@@ -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 the
1566
- // forge-restart-orphaned case: the node thinks it's executing but
1567
- // the underlying task is dead. Anything else (pending/done/skipped)
1568
- // is a misclick.
1569
- if (nodeState.status !== 'failed' && nodeState.status !== 'running') {
1570
- return { ok: false, error: `node is in status '${nodeState.status}' only failed or running nodes can be retried` };
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aion0/forge",
3
- "version": "0.9.8",
3
+ "version": "0.9.11",
4
4
  "description": "Unified AI workflow platform — multi-model task orchestration, persistent sessions, web terminal, remote access",
5
5
  "type": "module",
6
6
  "scripts": {