pi-taskflow 0.0.21 → 0.0.22

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/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  All notable changes to pi-taskflow are documented here. This project follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format.
4
4
 
5
+ ## [0.0.22] — 2026-06-10
6
+
7
+ > Dogfooding release. The `dogfood-full` self-audit taskflow (which itself
8
+ > exercises all 9 phase types + when/join/retry/budget/cache/eval/flow-def/
9
+ > loop/tournament/approval) ran against the codebase and surfaced these fixes.
10
+
11
+ ### Added
12
+ - **Live auto-refresh for the `/tf runs` panel.** The run-history panel was a static snapshot taken when opened, so a background (detached) run's progress never updated while watching. It now polls run state on a 1s interval and re-renders only when a run's status/`updatedAt` actually changes — phase progress (including `map`/`parallel` `subProgress` like `24/24`) updates live. The user's selection follows the same `runId` across refreshes, a green `● live` tag shows while any run is running, and the refresh timer is cleared on close (`dispose()`) and `unref`'d so it never keeps the event loop alive. Fully backward-compatible: without live hooks the panel renders statically as before.
13
+ - 5 new tests (`test/runs-view.test.ts`): refresh-on-change, no-render-when-unchanged, dispose-stops-timer, selection-follows-runId, back-compat-no-hooks.
14
+
15
+ ### Fixed
16
+ - **`safeParse` now prefers a `json`-tagged fence in multi-fence output.** When an LLM phase emitted an evidence block (e.g. ```` ```typescript ````) *before* the ```` ```json ```` payload, the old single-match regex grabbed the first fence, failed to parse, and the balanced-bracket fallback was misled by braces in the prose — `safeParse` returned `undefined` and any downstream `map` phase failed with `'over' did not resolve to an array`. It now scans every fenced block and tries `json`-tagged ones first, then untagged. (3 new multi-fence tests.)
17
+ - **Unresolved interpolation refs are surfaced as phase warnings.** `interpolate()` returns `missing[]` (placeholders with no source), but the runtime discarded it on the main task path — so `{args.typo}` or a `{steps.x.output}` without `dependsOn` was silently left intact in the dispatched task. The `interpolate.ts` doc comment promised "a recorded warning" that no code produced. The runtime now logs `[taskflow] phase X: unresolved refs ...` and attaches the message to `PhaseState.warnings` (persisted in the run record, visible in `/tf runs`). Doc comment corrected to match.
18
+
5
19
  ## [0.0.21] — 2026-06-10
6
20
 
7
21
  ### Added
package/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
  <a href="./LICENSE"><img src="https://img.shields.io/badge/license-MIT-43D9AD?style=flat-square" alt="MIT license"></a>
9
9
  <a href="#whats-inside"><img src="https://img.shields.io/badge/runtime%20deps-0-43D9AD?style=flat-square" alt="zero runtime dependencies"></a>
10
10
  <a href="https://github.com/heggria/pi-taskflow/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/heggria/pi-taskflow/ci.yml?branch=main&style=flat-square&label=CI" alt="CI status"></a>
11
- <a href="#whats-inside"><img src="https://img.shields.io/badge/tests-601-6E8BFF?style=flat-square" alt="601 tests"></a>
11
+ <a href="#whats-inside"><img src="https://img.shields.io/badge/tests-608-6E8BFF?style=flat-square" alt="608 tests"></a>
12
12
  <a href="#whats-inside"><img src="https://img.shields.io/badge/dogfooded-%E2%9C%93-43D9AD?style=flat-square" alt="dogfooded"></a>
13
13
  <a href="https://pi.dev"><img src="https://img.shields.io/badge/for-Pi%20coding%20agent-B692FF?style=flat-square" alt="for the Pi coding agent"></a>
14
14
  </p>
@@ -608,12 +608,12 @@ Copy one into `.pi/taskflows/<name>.json` (or `~/.pi/agent/taskflows/`) and it r
608
608
 
609
609
  <div align="center">
610
610
 
611
- **0 runtime dependencies** · **601 tests** · **9 phase types** · **cross-session resume** · **cross-run memoization** · **~7.7k LOC runtime**
611
+ **0 runtime dependencies** · **608 tests** · **9 phase types** · **cross-session resume** · **cross-run memoization** · **~7.7k LOC runtime**
612
612
 
613
613
  </div>
614
614
 
615
615
  - **Zero runtime dependencies.** No `dependencies` field — the runtime is built entirely on Node built-ins (`fs` / `path` / `os` / `child_process` / `crypto`). The file lock is `fs.openSync("wx")`, not a third-party library.
616
- - **601 tests across 25 test files** covering concurrency, atomic file locking (8-process race regressions), path-traversal hardening, cross-session resume, cross-run cache freshness (flow/thinking/tools key isolation, fingerprint invalidation, TTL/LRU eviction), gate verdicts, budget caps, retry/backoff, approval flows, loop termination, tournament judging, sub-flow composition, callback isolation, the idle watchdog, model-role init config, and parseModelFromLabel with parenthesized-model-name regression.
616
+ - **608 tests across 26 test files** covering concurrency, atomic file locking (8-process race regressions), path-traversal hardening, cross-session resume, cross-run cache freshness (flow/thinking/tools key isolation, fingerprint invalidation, TTL/LRU eviction), gate verdicts, budget caps, retry/backoff, approval flows, loop termination, tournament judging, sub-flow composition, live run-history refresh, callback isolation, the idle watchdog, model-role init config, and parseModelFromLabel with parenthesized-model-name regression.
617
617
  - **Hardened by design.** Path-traversal defense (lexical + `realpath`), runId validation, HTML/error sanitization, atomic writes, stale-lock stealing via `rename`, and an idle watchdog that kills wedged subagents.
618
618
  - **Dogfooded.** Every new feature has to survive the project's own `self-improve` taskflow before it ships.
619
619
 
@@ -6,11 +6,15 @@
6
6
  * before deciding. Every line is padded to the full dialog width so the
7
7
  * overlay composites cleanly (no see-through, no ghosting in scrollback).
8
8
  *
9
- * While the dialog is open, SGR mouse reporting is enabled so the wheel
10
- * scrolls the viewport instead of the terminal scrollback. It is restored
11
- * on dispose.
9
+ * Mouse tracking is intentionally NOT used here. Enabling terminal-level
10
+ * SGR mouse reporting (DECSET 1000h/1006h) to capture wheel events would
11
+ * interfere with the terminal's native scrollback after the dialog closes,
12
+ * because the restore sequence depends on the overlay framework reliably
13
+ * calling dispose — which is not guaranteed across all lifecycle paths.
14
+ * Keyboard scrolling (↑↓/PgUp/PgDn/Home/End/j/k/g/G) covers the same
15
+ * ground without risking a stuck mouse-tracking mode.
12
16
  *
13
- * Keys: wheel/↑↓ scroll · PgUp/PgDn page · Home/End jump ·
17
+ * Keys: ↑↓ scroll · PgUp/PgDn page · Home/End jump ·
14
18
  * a/Enter approve · e edit (guidance) · r/Esc reject.
15
19
  */
16
20
 
@@ -28,31 +32,16 @@ export interface ApprovalViewOptions {
28
32
  upstream?: string;
29
33
  }
30
34
 
31
- /** Minimal writer used to toggle terminal mouse reporting. */
32
- export interface TerminalWriter {
33
- write(data: string): void;
34
- }
35
-
36
35
  const FALLBACK_ROWS = 24;
37
- /** Wheel ticks scroll this many lines. */
38
- const WHEEL_STEP = 3;
39
- /** SGR mouse sequence: ESC [ < B ; X ; Y (M|m) */
40
- const MOUSE_SGR = /^\x1b\[<(\d+);(\d+);(\d+)([Mm])$/;
41
- /** Enable basic mouse tracking + SGR encoding. */
42
- const MOUSE_ON = "\x1b[?1000h\x1b[?1006h";
43
- /** Restore: disable SGR encoding + mouse tracking. */
44
- const MOUSE_OFF = "\x1b[?1006l\x1b[?1000l";
45
36
 
46
37
  export class ApprovalViewComponent {
47
38
  private theme: Theme;
48
39
  private opts: ApprovalViewOptions;
49
40
  private onDone: (choice: ApprovalChoice) => void;
50
41
  private getRows: () => number;
51
- private term?: TerminalWriter;
52
42
  private scrollOffset = 0;
53
43
  private cachedWidth?: number;
54
44
  private cachedBody?: string[];
55
- private mouseEnabled = false;
56
45
  private decided = false;
57
46
 
58
47
  constructor(
@@ -60,43 +49,19 @@ export class ApprovalViewComponent {
60
49
  opts: ApprovalViewOptions,
61
50
  onDone: (choice: ApprovalChoice) => void,
62
51
  getRows?: () => number,
63
- term?: TerminalWriter,
64
52
  ) {
65
53
  this.theme = theme;
66
54
  this.opts = opts;
67
55
  this.onDone = onDone;
68
56
  this.getRows = getRows ?? (() => FALLBACK_ROWS);
69
- this.term = term;
70
- this.enableMouse();
71
- }
72
-
73
- private enableMouse(): void {
74
- if (this.term && !this.mouseEnabled) {
75
- try {
76
- this.term.write(MOUSE_ON);
77
- this.mouseEnabled = true;
78
- } catch {
79
- // non-tty / closed stream — wheel support is best-effort
80
- }
81
- }
82
57
  }
83
58
 
84
- /** Restore terminal mouse state. Idempotent; call from the overlay's dispose. */
85
- dispose(): void {
86
- if (this.term && this.mouseEnabled) {
87
- this.mouseEnabled = false;
88
- try {
89
- this.term.write(MOUSE_OFF);
90
- } catch {
91
- // ignore
92
- }
93
- }
94
- }
59
+ /** No-op kept for compatibility with Pi TUI overlay dispose contract. */
60
+ dispose(): void {}
95
61
 
96
62
  private decide(choice: ApprovalChoice): void {
97
63
  if (this.decided) return;
98
64
  this.decided = true;
99
- this.dispose();
100
65
  this.onDone(choice);
101
66
  }
102
67
 
@@ -155,17 +120,6 @@ export class ApprovalViewComponent {
155
120
  }
156
121
 
157
122
  handleInput(data: string): void {
158
- // Mouse events (SGR) — wheel scrolls, everything else is swallowed.
159
- const mouse = MOUSE_SGR.exec(data);
160
- if (mouse) {
161
- const b = Number(mouse[1]);
162
- if (b & 64) {
163
- // Wheel: low two bits 0 = up, 1 = down.
164
- if ((b & 3) === 0) this.clampScroll(-WHEEL_STEP);
165
- else if ((b & 3) === 1) this.clampScroll(WHEEL_STEP);
166
- }
167
- return;
168
- }
169
123
  // Decisions
170
124
  if (matchesKey(data, "return") || data === "a" || data === "y") {
171
125
  this.decide("approve");
@@ -251,7 +205,7 @@ export class ApprovalViewComponent {
251
205
 
252
206
  // Key hints
253
207
  lines.push(this.hrule(width, "├", "┤"));
254
- const scrollHint = cap > 0 ? "wheel/↑↓/PgUp/PgDn scroll · " : "";
208
+ const scrollHint = cap > 0 ? "↑↓/PgUp/PgDn scroll · " : "";
255
209
  lines.push(this.row(th.fg("dim", `${scrollHint}a/Enter approve · e edit · r/Esc reject`), width));
256
210
  lines.push(this.hrule(width, "╰", "╯"));
257
211
  return lines;
@@ -206,7 +206,6 @@ async function runFlow(
206
206
  },
207
207
  done,
208
208
  () => tui.terminal.rows,
209
- tui.terminal,
210
209
  );
211
210
  const onAbort = () => done("reject");
212
211
  signal?.addEventListener("abort", onAbort, { once: true });
@@ -799,8 +798,13 @@ export default function (pi: ExtensionAPI) {
799
798
  );
800
799
  return;
801
800
  }
802
- const result = await ctx.ui.custom<RunHistoryResult | undefined>((_tui, theme, _kb, done) => {
803
- return new RunHistoryComponent(runs, theme, (r) => done(r));
801
+ const result = await ctx.ui.custom<RunHistoryResult | undefined>((tui, theme, _kb, done) => {
802
+ const comp = new RunHistoryComponent(runs, theme, (r) => done(r), {
803
+ refresh: () => listRuns(ctx.cwd, 50),
804
+ requestRender: () => tui.requestRender(),
805
+ intervalMs: 1000,
806
+ });
807
+ return comp;
804
808
  });
805
809
  if (result?.action === "resume") {
806
810
  if (ctx.isIdle()) {
@@ -8,8 +8,11 @@
8
8
  * {previous.output} alias for the immediately-preceding completed phase output
9
9
  * {item} / {item.f} map loop variable (or custom name via phase.as)
10
10
  *
11
- * Unknown placeholders are left intact (with a recorded warning) rather than
12
- * throwing, so a partially-specified task still runs.
11
+ * Unknown placeholders are left intact rather than throwing, so a
12
+ * partially-specified task still runs. The unresolved refs are returned in
13
+ * `missing[]`; the runtime surfaces them as a phase warning (see
14
+ * `warnUnresolvedRefs` in runtime.ts) — logged and persisted to
15
+ * `PhaseState.warnings`.
13
16
  */
14
17
 
15
18
  export interface InterpolationContext {
@@ -123,13 +126,21 @@ export function safeParse(text: string): unknown {
123
126
  } catch {
124
127
  // noop
125
128
  }
126
- // Extract from a ```json fenced block
127
- const fence = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i);
128
- if (fence) {
129
+ // Extract from fenced blocks. Outputs often contain multiple fences
130
+ // (e.g. a ```typescript evidence block before the ```json payload), so try
131
+ // every fence — json-tagged blocks first, then untagged/other blocks.
132
+ const fenceRe = /```(\w*)[ \t]*\r?\n?([\s\S]*?)```/g;
133
+ const fenced: { lang: string; body: string }[] = [];
134
+ let fm: RegExpExecArray | null;
135
+ while ((fm = fenceRe.exec(trimmed)) !== null) {
136
+ fenced.push({ lang: fm[1].toLowerCase(), body: fm[2].trim() });
137
+ }
138
+ const ordered = [...fenced.filter((b) => b.lang === "json"), ...fenced.filter((b) => b.lang !== "json")];
139
+ for (const block of ordered) {
129
140
  try {
130
- return JSON.parse(fence[1].trim());
141
+ return JSON.parse(block.body);
131
142
  } catch {
132
- // noop
143
+ // noop — try the next fence
133
144
  }
134
145
  }
135
146
  // Extract the first balanced [...] or {...}
@@ -40,6 +40,19 @@ function isResumable(r: RunState): boolean {
40
40
  return r.status === "paused" || r.status === "failed";
41
41
  }
42
42
 
43
+ /** Detect whether a refreshed run list differs from the current one in any way
44
+ * the panel renders (status, updatedAt, phase progress, membership). */
45
+ function hasChanged(prev: RunState[], next: RunState[]): boolean {
46
+ if (prev.length !== next.length) return true;
47
+ const byId = new Map(prev.map((r) => [r.runId, r]));
48
+ for (const n of next) {
49
+ const p = byId.get(n.runId);
50
+ if (!p) return true;
51
+ if (p.status !== n.status || p.updatedAt !== n.updatedAt) return true;
52
+ }
53
+ return false;
54
+ }
55
+
43
56
  export class RunHistoryComponent {
44
57
  private runs: RunState[];
45
58
  private theme: Theme;
@@ -48,14 +61,62 @@ export class RunHistoryComponent {
48
61
  private mode: "list" | "detail" = "list";
49
62
  private cachedWidth?: number;
50
63
  private cachedLines?: string[];
64
+ /** Live-refresh wiring: re-read run state from disk while the panel is open
65
+ * so background (detached) runs show live progress without reopening. */
66
+ private timer?: ReturnType<typeof setInterval>;
67
+ private refresh?: () => RunState[];
68
+ private requestRender?: () => void;
51
69
 
52
- constructor(runs: RunState[], theme: Theme, onDone: (result?: RunHistoryResult) => void) {
70
+ constructor(
71
+ runs: RunState[],
72
+ theme: Theme,
73
+ onDone: (result?: RunHistoryResult) => void,
74
+ /** Optional live-refresh hooks. When both are provided the panel polls
75
+ * `refresh()` on an interval and calls `requestRender()` if anything changed. */
76
+ live?: { refresh: () => RunState[]; requestRender: () => void; intervalMs?: number },
77
+ ) {
53
78
  if (!runs.length) {
54
79
  throw new Error("RunHistoryComponent requires at least one run");
55
80
  }
56
81
  this.runs = runs;
57
82
  this.theme = theme;
58
83
  this.onDone = onDone;
84
+ if (live) {
85
+ this.refresh = live.refresh;
86
+ this.requestRender = live.requestRender;
87
+ const intervalMs = Math.max(250, live.intervalMs ?? 1000);
88
+ this.timer = setInterval(() => this.poll(), intervalMs);
89
+ // Don't keep the event loop alive just for the panel refresh.
90
+ (this.timer as { unref?: () => void }).unref?.();
91
+ }
92
+ }
93
+
94
+ /** Re-read run state; if anything changed, refresh the cached render. */
95
+ private poll(): void {
96
+ if (!this.refresh) return;
97
+ let next: RunState[];
98
+ try {
99
+ next = this.refresh();
100
+ } catch {
101
+ return; // transient read/lock error — try again next tick
102
+ }
103
+ if (!next.length) return;
104
+ if (!hasChanged(this.runs, next)) return;
105
+ // Preserve the user's selection by runId across refreshes.
106
+ const selectedId = this.runs[this.selected]?.runId;
107
+ this.runs = next;
108
+ const idx = next.findIndex((r) => r.runId === selectedId);
109
+ this.selected = idx >= 0 ? idx : Math.min(this.selected, next.length - 1);
110
+ this.invalidate();
111
+ this.requestRender?.();
112
+ }
113
+
114
+ /** Stop the refresh timer when the panel closes. */
115
+ dispose(): void {
116
+ if (this.timer) {
117
+ clearInterval(this.timer);
118
+ this.timer = undefined;
119
+ }
59
120
  }
60
121
 
61
122
  handleInput(data: string): void {
@@ -104,7 +165,8 @@ export class RunHistoryComponent {
104
165
  for (const l of renderProgress(run, th).split("\n")) lines.push(truncateToWidth(l, width));
105
166
  lines.push("");
106
167
  const hint = isResumable(run) ? "Esc back · r resume" : "Esc back";
107
- lines.push(truncateToWidth(` ${th.fg("dim", hint)}`, width));
168
+ const liveTag = this.timer && run.status === "running" ? th.fg("success", " ● live") : "";
169
+ lines.push(truncateToWidth(` ${th.fg("dim", hint)}${liveTag}`, width));
108
170
  lines.push("");
109
171
  this.cachedWidth = width;
110
172
  this.cachedLines = lines;
@@ -129,7 +191,11 @@ export class RunHistoryComponent {
129
191
  });
130
192
 
131
193
  lines.push("");
132
- lines.push(truncateToWidth(` ${th.fg("dim", "↑↓ select · Enter details · r resume · q close")}`, width));
194
+ const anyRunning = this.runs.some((r) => r.status === "running");
195
+ const liveHint = this.timer && anyRunning ? th.fg("success", " ● live") : "";
196
+ lines.push(
197
+ truncateToWidth(` ${th.fg("dim", "↑↓ select · Enter details · r resume · q close")}${liveHint}`, width),
198
+ );
133
199
  lines.push("");
134
200
 
135
201
  this.cachedWidth = width;
@@ -87,8 +87,7 @@ function buildInterpolationContext(
87
87
  return { args: state.args, steps, previousOutput, locals };
88
88
  }
89
89
 
90
- function resultToPhaseState(id: string, r: RunResult, inputHash: string, parseJson: boolean): PhaseState {
91
- const failed = isFailed(r);
90
+ function resultToPhaseState(id: string, r: RunResult, inputHash: string, parseJson: boolean): PhaseState { const failed = isFailed(r);
92
91
  const attempts = attemptsOf(r);
93
92
  // For failed phases, embed the error info in the output so downstream
94
93
  // phases (and the user) can see what went wrong. The raw r.output is
@@ -110,6 +109,22 @@ function resultToPhaseState(id: string, r: RunResult, inputHash: string, parseJs
110
109
  };
111
110
  }
112
111
 
112
+ /**
113
+ * Surface unresolved interpolation placeholders (the `missing[]` from
114
+ * `interpolate()`). Without this they are silently left intact in the task —
115
+ * the doc comment in interpolate.ts promises "a recorded warning". We both
116
+ * log to the console and return a string to attach to PhaseState.warnings so
117
+ * the warning is persisted in the run record and visible in `/tf runs`.
118
+ * Returns undefined when nothing is missing.
119
+ */
120
+ function warnUnresolvedRefs(phaseId: string, missing: string[]): string | undefined {
121
+ if (!missing.length) return undefined;
122
+ const unique = Array.from(new Set(missing));
123
+ const msg = `unresolved refs in task: ${unique.map((m) => `{${m}}`).join(", ")} — left intact (check dependsOn / placeholder spelling)`;
124
+ console.warn(`[taskflow] phase '${phaseId}': ${msg}`);
125
+ return msg;
126
+ }
127
+
113
128
  /** Attempts recorded by the retry wrapper (defaults to 1). */
114
129
  function attemptsOf(r: RunResult): number {
115
130
  const a = r.attempts;
@@ -582,7 +597,9 @@ async function executePhase(
582
597
  return ps;
583
598
  }
584
599
  }
585
- const { text } = interpolate(phase.task ?? "", ctx);
600
+ const interp = interpolate(phase.task ?? "", ctx);
601
+ const text = interp.text;
602
+ const refWarning = warnUnresolvedRefs(phase.id, interp.missing);
586
603
  const fullTask = preRead + text;
587
604
  const agentName = resolveAgent(phase.agent, deps, state);
588
605
  const inputHash = cacheKey(cc, [phase.id, agentName, phase.model ?? "", fullTask]);
@@ -591,6 +608,7 @@ async function executePhase(
591
608
 
592
609
  const r = await runOne(agentName, fullTask, liveSink(state, phase.id, emitProgress));
593
610
  const ps = resultToPhaseState(phase.id, r, inputHash, parseJson);
611
+ if (refWarning) ps.warnings = [...(ps.warnings ?? []), refWarning];
594
612
  if (type === "gate" && ps.status === "done") ps.gate = parseGateVerdict(r.output);
595
613
 
596
614
  // onBlock:retry — re-execute upstream + gate until pass or max attempts.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-taskflow",
3
- "version": "0.0.21",
3
+ "version": "0.0.22",
4
4
  "description": "A declarative, verifiable graph of task nodes for the Pi coding agent — not a workflow you script, but a DAG you declare: statically verified before it runs, with dynamic fan-out, gates, isolated subagent context, resumable runs, and saveable commands.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -37,7 +37,7 @@
37
37
  ],
38
38
  "scripts": {
39
39
  "typecheck": "tsc --noEmit",
40
- "test": "PI_TASKFLOW_BUILTIN_AGENTS_DIR= node --experimental-strip-types --test test/interpolate.test.ts test/condition.test.ts test/schema.test.ts test/usage.test.ts test/runtime.test.ts test/features.test.ts test/runner.test.ts test/store.test.ts test/agents.test.ts test/init.test.ts test/render.test.ts test/approval-view.test.ts test/desugar.test.ts test/cache.test.ts test/loop.test.ts test/tournament.test.ts test/verify.test.ts test/gate-eval.test.ts test/transient-error.test.ts test/runtime-branches.test.ts test/interpolate-extended.test.ts test/store-extended.test.ts test/flow-def.test.ts test/detached.test.ts",
40
+ "test": "PI_TASKFLOW_BUILTIN_AGENTS_DIR= node --experimental-strip-types --test test/interpolate.test.ts test/condition.test.ts test/schema.test.ts test/usage.test.ts test/runtime.test.ts test/features.test.ts test/runner.test.ts test/store.test.ts test/agents.test.ts test/init.test.ts test/render.test.ts test/approval-view.test.ts test/desugar.test.ts test/cache.test.ts test/loop.test.ts test/tournament.test.ts test/verify.test.ts test/gate-eval.test.ts test/transient-error.test.ts test/runtime-branches.test.ts test/interpolate-extended.test.ts test/store-extended.test.ts test/flow-def.test.ts test/detached.test.ts test/runs-view.test.ts",
41
41
  "test:e2e": "PI_TASKFLOW_PI_BIN=pi node --experimental-strip-types test/e2e.mts",
42
42
  "test:dogfood-cache": "node --experimental-strip-types test/dogfood-cache.mts"
43
43
  },