Skip to content

Releases: wwppee/pilot

v0.7.0 — /workflows MVP (reusable agent workflow templates)

Choose a tag to compare

@wwppee wwppee released this 16 Jul 18:00

v0.7.0 — /workflows MVP (reusable agent workflow templates)

First release of the workflow concept that replaces v0.4's
L1/L2/L3/L4 capability layers. The biggest product pivot
since v0.4: instead of "absorb an npm package and file it
under L1 or L2", the user now composes sequences of
LLM-powered steps
in a visual editor. Each step holds its
own model config (provider + model + key ref + system
prompt + tools), edges describe the data flow, and the
user can save the result as a reusable template.

This is a major version bump because the product
position changed, even though no field on a /compose
connection changed.

What you can do

  • Open /workflows and see every saved workflow as a
    card (name, description, step count, connection count,
    updated time).
  • Click + New workflow, type a kebab-case id
    (e.g. research-and-test), hit Enter → empty editor
    opens, ready to add steps.
  • Each step has its own form: name, model (provider +
    model name), system prompt, input template, output var,
    tools, on-failure strategy (stop / skip / retry /
    escalate).
  • Connect steps with edges (each step's "Connect to…"
    button opens a dropdown of the other steps). The SVG
    preview on the right shows the data flow live.
  • Auto-layout button writes back a BFS-computed
    layout to each node's position so the preview
    survives reload.
  • Save is disabled until you change something; the
    status text shows "Saved at {time}" or "•" (unsaved).
  • Duplicate loads + saves under a new id; the new
    editor opens on the copy.
  • Delete with a confirm dialog.

What you CAN'T do yet (v0.7.1+)

  • Drag-and-drop on the SVG preview. The form is the only
    way to edit a step today.
  • Run the workflow. The runtime (drive a pi session
    through the node sequence) is v0.7.3.
  • Field-level data mapping on edges. Today the whole
    outputVar is bound.

Files

Area Files
Data model web/src/lib/types.ts (+5 types)
Server core src/core/workflow.ts (new, ~250 lines)
Service + API src/core/service.ts, service-impl.ts, server.ts
Browser API web/src/lib/pilot-browser.ts, pilot.ts
List page web/src/app/workflows/page.tsx, WorkflowListView.tsx
Editor web/src/app/workflows/[id]/page.tsx, WorkflowEditor.tsx
Styles web/src/app/workflows/workflow.css
i18n web/src/lib/i18n/{types,dict.en,dict.zh}.ts
Nav web/src/components/NavLinks.tsx (1 new entry)
Tests test/unit/workflow.test.ts (9), web/tests/workflow-layout.test.ts (7)

How to try

cd ~/path/to/pilot
git pull origin main
git checkout v0.7.0
pilot dashboard
# open http://localhost:<port>/workflows
# click "+ New workflow" → type "research-and-test" → Enter
# editor opens, click "+ Add step" a few times, fill in model + prompt
# click "Connect to…" on each step to chain them
# click "Auto-layout" to tidy the preview
# click "Save"
# close the editor, go back to /workflows, your workflow is there

Stats

  • root: 622/622 ✓ (+9 in workflow.test.ts)
  • web: 245/245 ✓ (+7 in workflow-layout.test.ts)
  • i18n: 0 placeholder mismatches across 1015 shared keys
  • format:check root + web: ✓
  • lint: ✓
  • tsc: ✓
  • production build: not run (UI / CRUD only)

v0.6.23 — /compose mobile layout hotfix (P1 from user testing)

Choose a tag to compare

@wwppee wwppee released this 16 Jul 04:26

v0.6.23 — /compose mobile layout hotfix (P1 bug)

User reported (with screenshot) that /compose becomes unusable at
viewport widths < 1024px: the sidebar's natural content height
pushed the canvas + inspector off-screen, leaving only the session
list visible.

What changed (CSS-only)

  • At <1024px, .compose-grid switches from display: grid to
    display: flex; flex-direction: column; height: calc(100vh - 200px).
  • Sidebar capped at 35vh (a search bar + filter + a scrollable
    items list, more on scroll).
  • Canvas gets flex: 1; min-height: 0 so it fills the remaining
    space and shrinks to fit instead of overflowing.
  • Inspector is unchanged (already position: fixed on mobile,
    removed from the flex flow).
  • Desktop layout (≥1024px) is unchanged — original 3-column
    grid preserved.

How to verify

git pull origin main
git checkout v0.6.23
pilot dashboard
# Resize the browser window below 1024px wide
# You should see: toolbar at top, sidebar (capped), canvas (the
# bulk of the visible area), and the inspector bottom-sheet
# unchanged at the bottom

Lesson

  • Unit tests + tsc + lint verify the code works, not that the user
    can see what they need to see. Headless tests don't exercise
    rendered layout.
  • "Tests pass ≠ UI works." Add a smoke checklist for any
    viewport-sized page: render at 800×600, 1024×768, 1440×900 and
    verify all primary panels are visible.

Thanks to the user for the screenshot — that's the only reason this
bug got fixed.

v0.6.22 — useHistoryStack hook extracted from ComposeBoard.tsx

Choose a tag to compare

@wwppee wwppee released this 16 Jul 03:26

v0.6.22 — useHistoryStack hook extracted from ComposeBoard.tsx

The first slice of the long-deferred "ComposeBoard hooks/state 抽离"
backlog item. The undo/redo stack is the most self-contained piece
of ComposeBoard state — it only reads state (passed in), writes
to setState / setSelectedId / announce (all passed in), and
consumes the pure applyEntry / invertEntry functions that
already live in lib/compose-history.ts. So it's the lowest-risk
extraction: the behaviour is unchanged, the public surface is
mechanical, and any regression is caught by a new dedicated test
suite.

What got extracted

  • The { past, future } state.
  • The commit callback (apply + push entry + announce label).
  • The undo / redo callbacks (apply inverted / forward entry,
    push onto the other stack, announce).
  • The "coalesce arrow-key moves" logic from moveBlock — the
    single thing the lib version didn't know about.
  • The "clear history on wholesale canvas replacement" path
    (load board / import JSON / reset canvas).

New hook surface

const { history, commit, pushEntry, pushOrMergeMoveEntry,
        clearHistory, undo, redo, canUndo, canRedo }
  = useHistoryStack({ state, setState, setSelectedId, announce, t });

The 5 entry-point methods map to 5 distinct use cases:

Method Used by Why a separate method
commit 20+ callbacks (connection edits, addBlock, connect, disconnect, ...) Standard "I have a before/after transition to record" path.
pushEntry endBlockDrag State already mutated; recording only the final delta.
pushOrMergeMoveEntry moveBlock (arrow-key handler) Holding an arrow key fires many moveBlock calls; merge into ONE undo step.
clearHistory loadBoardFromServer, importJson, resetCanvas Wholesale canvas replacement.
undo / redo keyboard handler, toolbar buttons Apply inverted / forward entry, push onto the other stack, announce.

Files touched (8)

Area Files
New hook web/src/app/compose/use-history-stack.ts (209 lines, well-commented)
Refactored web/src/app/compose/ComposeBoard.tsx (-40 lines net)
Tests web/tests/use-history-stack.test.tsx (12 new tests, 516 lines)
Docs CHANGELOG.md, AGENTS.md
Versions package.json, web/package.json (both → 0.6.22)

File size

  • ComposeBoard.tsx: 2184 → 2144 lines (-40). The drop is
    smaller than the extracted code because the useHistoryStack
    call site + explanatory comments take ~30 lines. The
    cognitive drop is bigger — the commit / undo / redo
    triplet is now testable in isolation.

Backlog after v0.6.22

  • v0.6.23+ — More ComposeBoard.tsx extractions: drag/drop,
    server persistence, keyboard shortcuts, view state. The
    pattern established here (custom hook that owns a slice of
    state and exposes a small public surface) should make the
    next 4-5 extractions mechanical, but each is its own release.
  • block-center avoidance for orthogonal routes (real A* grid
    router) — still the v0.6.20 followup, never started
  • per-direction palette — still the v0.6.19 followup, never started

v0.6.21 — cleanup batch (AGENTS.md + empty state + placeholder audit)

Choose a tag to compare

@wwppee wwppee released this 16 Jul 01:13

v0.6.21 — Cleanup batch (AGENTS.md + empty state dedup + placeholder audit)

A small user-flagged cleanup release that closes three leftover
P2/P3 items. No new features, no schema bump; this is a
hotfix-shaped release that nudges a few long-standing paper cuts
and adds a regression test so the placeholder audit doesn't
drift again.

What's in the box

  • AGENTS.md version drift fixed. Two places (the "30 秒
    判断题" header and the "Last updated" footer) said v0.6.14
    even though the project is at v0.6.20. Bumped both.
  • /usage empty state no longer duplicates its hint. The
    usage.empty title used to re-state the "run pi with a real
    model" message that usage.empty.hint already said, so the
    user saw the actionable message twice. Now the title is a
    short descriptive label and the hint carries the actionable
    step alone.
  • Placeholder parameter audit closed. v0.6.16 closed 8 of
    15 historical placeholder mismatches between en and zh but
    punted the rest. v0.6.21 closes the last 7:
    • 2 hardcoded-"1" in en singular keys (blockCount.one,
      packageCount.one) — both now use {n} like their
      *.other counterparts
    • 5 en-only plural-suffix placeholders ({s} / {es})
      — dropped in favor of always-plural English
    • 1 zh missing {n} (tools.subtitle) — added
  • Regression test added. tests/i18n.test.ts now has a
    placeholder consistency across locales block that walks
    every key in both en and zh, asserts the placeholder sets
    match, and dumps the full offender list on failure.

Files touched (7)

Area Files
Docs AGENTS.md (2 version refs), CHANGELOG.md (v0.6.21 entry)
i18n web/src/lib/i18n/dict.en.ts (5 strings), web/src/lib/i18n/dict.zh.ts (1 string)
Tests web/tests/i18n.test.ts (1 new describe block)
Versions package.json, web/package.json (both → 0.6.21)

Backlog after v0.6.21

  • v0.6.22 — block-center avoidance (real A* grid router on
    top of the v0.6.20 route enum) + hooks 抽离
  • per-direction palette (e.g. "all forward connections get this
    color") — deferred; per-edge is the v0.6.19 minimum

v0.6.20 — per-edge routing style (curve / orthogonal)

Choose a tag to compare

@wwppee wwppee released this 16 Jul 01:02

v0.6.20 — Per-edge routing style (curve / orthogonal)

Each /compose edge can now pick between the original cubic bezier
("curve", the v0.6.19 look) and a 3-segment right-angle polyline
("orthogonal", Visio / Lucidchart style). The choice is per-edge,
so a single board can mix both: a "main flow" line curves smoothly
while a "control plane" line goes through right angles.

What you get

  • Inspector gets a 5th control: a <select> with the two
    options. The label ("Routing" / "路径") and option labels
    ("Curve" / "曲线" and "Orthogonal" / "直角") are i18n'd.
  • Canvas uses a single SVG <path> for both styles. The
    curve case is the original cubic bezier; the orthogonal
    case is a 3-segment polyline (right → up/down → right). Both
    end with a horizontal segment, so the v0.6.18 marker logic
    (markerStart / markerEnd with
    orient="auto-start-reverse") keeps working without any marker
    changes — the orthogonal case's arrow head still sits on the
    last horizontal segment pointing right (or left for backward).

Scope of v0.6.20 (deliberately minimal)

  • The two routing styles are the v0.6.20 surface — pick one,
    the renderer takes care of the rest.
  • Block-center avoidance is out of scope. A connection that
    goes through other blocks in the middle will still do so with
    "orthogonal". A real A* grid router (or visibility-graph) on
    top of this enum is a separate concern and would need its own
    release. The route field is designed to extend (a future
    obstacleAvoidance?: boolean can layer on top without a v7
    bump).

Files touched (15)

Area Files
Schema web/src/lib/types.ts, src/core/compose-boards.ts (zod enum + v6 version union)
Compose state web/src/lib/compose-history.ts, web/src/app/compose/ComposeBoard.tsx
UI web/src/app/compose/Inspector.tsx, web/src/app/compose/ConnectionPath.tsx
i18n web/src/lib/i18n/types.ts, web/src/lib/i18n/dict.en.ts, web/src/lib/i18n/dict.zh.ts (+4 keys)
Docs CHANGELOG.md, docs/roadmap.md
Tests 3 schema + 4 history + 1 backward-compat (v5↔v6 round-trip)

Backward compat

route is optional in the schema and omitted by default
same pattern as v0.6.18's dir and v0.6.19's color. A v0.6.19
board saved before this release round-trips through v0.6.20
byte-identical. The schema is now v6; v1 / v2 / v3 / v4 / v5 boards
all continue to load.

Why a minor bump

Same hotfix / additive-feature precedent as v0.6.6, v0.6.13,
v0.6.17, v0.6.18, v0.6.19: v0.6.20 is purely additive (every
v0.6.19 board still opens and edits fine), so a minor bump is in
line with the project's release conventions.

Backlog after v0.6.20

  • v0.6.21 — block-center avoidance (real A* grid router on
    top of the v0.6.20 route enum) + hooks 抽离
  • placeholder parameter audit (15 keys remain hardcoded)
  • per-direction palette (e.g. "all forward connections get this
    color") — deferred; per-edge is the v0.6.19 minimum

v0.6.19 — per-edge connection color (hex picker)

Choose a tag to compare

@wwppee wwppee released this 16 Jul 00:55

v0.6.19 — Per-edge connection color (hex picker)

Each /compose edge can now pick its own color. The Inspector gets a
native color swatch next to the existing label / kind / direction
controls, and the SVG line + arrow head render in the picked color
through the existing currentColor cascade — no new marker shapes.

What you get

  • Inspector gets a 4th control: <input type="color"> for the hex
    swatch, plus a small reset button (visible only when a color is
    set). Click reset to drop the override and fall back to the theme
    accent — same as a v0.6.18 edge.
  • Canvas threads the color through style.color on the wrapping
    <g>. The line stroke and the marker fill both consume
    currentColor, so the single style change cascades to both ends.
    Selected vs unselected markers still differ via the existing
    markerId swap; the color is independent of selection state.
  • Format is hex-only#rgb / #rgba / #rrggbb / #rrggbbaa.
    Named colors and rgb() / hsl() are deliberately rejected so the
    "use theme accent" semantic stays clean (a user who wants theme
    default should leave the field empty, not set a color that
    coincidentally matches).

Files touched (15)

Area Files
Schema web/src/lib/types.ts, src/core/compose-boards.ts (zod regex + v5 version union)
Compose state web/src/lib/compose-history.ts, web/src/app/compose/ComposeBoard.tsx
UI web/src/app/compose/Inspector.tsx, web/src/app/compose/ConnectionPath.tsx
i18n web/src/lib/i18n/types.ts, web/src/lib/i18n/dict.en.ts, web/src/lib/i18n/dict.zh.ts (+5 keys)
Docs CHANGELOG.md, docs/roadmap.md
Tests 2 schema + 4 history + 2 backward-compat (v4↔v5 round-trip)

Backward compat

color is optional in the schema and omitted by default
same pattern as v0.6.18's dir field. A v0.6.18 board saved before
this release round-trips through v0.6.19 byte-identical (no
color: undefined floating around, no migration script needed).
The schema is now v5; v1 / v2 / v3 / v4 boards all continue to load.

Why a minor bump

Same hotfix / additive-feature precedent as v0.6.6, v0.6.13, v0.6.17,
v0.6.18: v0.6.19 is purely additive (every v0.6.18 board still opens
and edits fine), so a minor bump is in line with the project's
release-please conventions.

Backlog after v0.6.19

  • v0.6.20 — auto-route (avoid overlapping edges on the canvas)
  • v0.6.21 — hooks 抽离 (move web/src/app/api hooks into src/hooks/)
  • placeholder parameter audit (15 keys remain hardcoded)
  • per-direction palette (e.g. "all forward connections get this
    color") — deferred; per-edge is the v0.6.19 minimum and the
    schema's color?: string field already supports a future palette
    expansion without a v6 bump.

v0.6.18 — connection direction: forward / backward / bidirectional

Choose a tag to compare

@wwppee wwppee released this 15 Jul 22:32

v0.6.18 — connection direction: forward / backward / bidirectional

Connections are no longer one-way. Each connection now has an explicit
dir: "forward" | "backward" | "bidirectional", rendered with SVG arrow
markers so the topology reads at a glance.

What you get

  • Inspector shows a third select next to label: A → B / B → A / A ↔ B
    (arrow glyphs are locale-stable, so no translation is needed).
  • Canvas renders marker-start (backward) and marker-end (forward) for
    the same edge; bidirectional gets arrows on both ends. The same marker id
    is reused with orient="auto-start-reverse" — no new asset.
  • Up to 3 connections per unordered node pair — one per direction. Dedupe
    is keyed on the triple (from, to, dir).
  • forward is the default, and is stored as the absence of dir (not as an
    explicit "forward" field), so a v0.6.17 board round-trips through v0.6.18
    byte-identical.

Files touched (15)

Area Files
Schema web/src/lib/types.ts, src/core/compose-boards.ts
Compose state web/src/lib/compose-history.ts, web/src/app/compose/ComposeBoard.tsx
UI web/src/app/compose/Inspector.tsx, web/src/app/compose/ConnectionPath.tsx
i18n web/src/lib/i18n/types.ts, web/src/lib/i18n/dict.en.ts, web/src/lib/i18n/dict.zh.ts (+5 keys)
Docs CHANGELOG.md, docs/roadmap.md
Tests 3 unit + 1 history test updated for v3/v4 version unions

Why a minor bump (and not a patch)

Per the project's hotfix precedent (v0.6.6, v0.6.13, v0.6.17), additive
schema/UI features ship as minor bumps. v0.6.18 is additive: every v0.6.17
board still opens and edits fine.

Backlog after v0.6.18

  • v0.6.19 — connection color (per-edge / per-direction palette)
  • v0.6.20 — auto-route (avoid overlapping edges on the canvas)
  • v0.6.21 — hooks 抽离 (move web/src/app/api hooks into src/hooks/)
  • placeholder parameter audit (15 keys remain hardcoded)

v0.6.17 — /usage range picker active label is now white (1-line visual hotfix)

Choose a tag to compare

@wwppee wwppee released this 15 Jul 18:39

v0.6.17 — /usage range picker active label is now white (1-line visual hotfix)

A follow-up to v0.6.16: the user reported that the active range button label read as "green and unreadable" on their display.

Root cause

v0.6.16 used text-[var(--bg)] (#0b0d10) on bg-[var(--accent)] (#79c0ff) — both colors sit in the same dark-blue value range, and at the small text-xs font size the contrast degrades to the point where the label visually merges with the active pill on many display profiles.

Fix

  • Active range label is now text-white (not text-[var(--bg)]). Pure white on the saturated #79c0ff background passes WCAG AA on every display profile (≥ 4.5:1 contrast for the 12px label size).
  • Non-active labels keep their text-[var(--text-muted)] so the visual hierarchy "muted → active" still reads correctly.

Stats

  • root tests: 543/543 ✓ (unchanged)
  • web tests: 214/214 ✓ (unchanged)
  • format:check root + web: ✓
  • tsc root + web: ✓

Lesson

i18n complete ≠ UX complete. v0.6.16 shipped with all i18n keys wired up but the active button's color combination turned out to be unreadable on some displays. Always let the user actually look at the result before claiming "done" — tsc + format + lint + tests doesn't catch a contrast bug.

Commit

  • bc3bb02 v0.6.17: /usage range picker active label is now white (visual hotfix)

v0.6.16 — 6 more i18n cleanups + 1 UX polish (4-button range picker)

Choose a tag to compare

@wwppee wwppee released this 15 Jul 18:26

v0.6.16 — 6 more i18n cleanups + 1 UX polish (4-button range picker)

A focused cleanup release that closes a small user-reported backlog of i18n hardcoded strings + one toolbar visual issue flagged from the /usage page screenshot.

P1 — i18n hardcoded strings (4 fixes)

  • profiles/[name]/page.tsx:61 "✓ Created" banner. Was <div>✓ Created <code>{name}</code>.</div> — the leading "✓ Created" string was raw English. Now uses RichT with profiles.createdBanner = "✓ Created {name}." (en) / "✓ 已创建 {name}。" (zh).
  • profiles/[name]/page.tsx:83 not-found error card. Was <div>Profile <code>{name}</code> not found.</div>. Now RichT with profiles.notFound = "Profile {name} not found." / "未找到 Profile {name}。".
  • profiles/[name]/page.tsx:195 env section heading. Was <h2>env (read-only — edit TOML directly)</h2> — English-only. New key profiles.envHeading (en/zh).
  • policy/page.tsx:150 load error title. Was <h2><T k="error.couldntLoad.title" />: policies</h2><T> part was i18n'd but the trailing raw : policies rendered as English even in zh, producing "加载失败: policies". Folded the noun into a single i18n key: policy.loadErrorTitle.

P2 — relative-time suffix hardcoded English (1 fix)

  • Inspector.tsx:726-737 formatRelative() was English shorthand only. Returned ${sec}s ago / ${min}m ago / etc. — the postfix "ago" was always English. Now each suffix is an i18n key: compose.inspector.time.{second,minute,hour,day,month,year}. The helper is module-level so it can't useT(); the callers (the session-detail inspector block) pass their t in explicitly: formatRelative(iso, t).

P3 — translation consistency (1 fix)

  • dict.zh.ts "fork" 翻译不一致. try.session.forkHere was "从此处派生" and try.hint.forkFromHere was "从这里分叉". Aligned to "从此处派生" in both.

P3 — placeholder parameter drift (deferred)

  • dict.zh.ts has 15 keys where the placeholder list doesn't match dict.en.ts exactly. None of these impact the rendered output. Cleaning this up would require auditing every call site; punted to v0.6.17+ (or whenever someone next adds a new locale).

UX — /usage range picker

  • Active tab no longer "shrinks". Added min-w-[5rem] so all four pills share a minimum width (5rem fits the longest current label in any locale; longer future labels grow as needed).
  • Active state visual + aria-current="page". The active button now also gets font-semibold. aria-current="page" makes the active tab discoverable to screen readers.
  • Non-active hover gains color + bg. text-[var(--text-muted)] hover:text-[var(--text)] hover:bg-[var(--surface-2)] — hover gives both a brighter text color and a subtle bg fill.

Stats

  • root tests: 543/543 ✓ (unchanged)
  • web tests: 214/214 ✓ (unchanged)
  • format:check root + web: ✓
  • tsc root + web: ✓
  • production build (next build): ✓

Deliberately NOT done (v0.6.17+ backlog)

  • multiple connections (A↔B 双向)
  • connection color 自定义
  • auto-route 避开 block 中心
  • ComposeBoard.tsx hooks/state 抽离
  • full placeholder parameter audit (15 keys) — see P3 above

Commit

  • 1ee471f v0.6.16: 6 more i18n cleanups + /usage 4-button UX polish

v0.6.15 — pilot forge absorb now lazy-inits ~/.pilot/capabilities/ + clearer EPERM error

Choose a tag to compare

@wwppee wwppee released this 15 Jul 18:15

v0.6.15 — pilot forge absorb now lazy-inits ~/.pilot/capabilities/ + clearer EPERM error

A user-reported hotfix: pilot forge absorb <pkg> failed with EPERM: operation not permitted, mkdir '/Users/feng/.pilot/capabilities' on macOS sandboxed shells (Cursor / VSCode devcontainer / sandboxed Terminal). The directory was only ever created by pilot init, and users who skipped init hit a bare permission error with no actionable hint.

The fix in one line

forgeAbsorb now ensures ~/.pilot/capabilities/ exists before writing, instead of relying on the user having run pilot init first.

P0 — silent failure on a real-user path (1 fix)

  • forgeAbsorb now lazy-inits the capabilities directory. New ensurePilotCapabilitiesDir(home) helper in core/types.ts does the mkdir recursive: true before the per-id capDir mkdir. Idempotent — a no-op when the directory already exists, so the hot-path cost is one syscall for users who have run pilot init (the common case). Users who skipped init and jumped straight to absorb will now have the directory materialised by absorb itself.
  • Actionable EPERM/EACCES error message. The previous error was the raw Failed to write /Users/feng/.pilot/capabilities/caveman-code/capability.json: EPERM: operation not permitted, mkdir '/Users/feng/.pilot/capabilities' — technically correct but gave no hint about why or what to do. The new error reads:

    Cannot write /Users/feng/.pilot/capabilities/caveman-code/capability.json: operation not permitted (EPERM). Your shell is sandboxed or otherwise blocked from writing to ~/.pilot/. Run \pilot init` from a non-sandboxed Terminal, or check that /Users/feng/.pilot/capabilities is accessible.The detection checkserr.code === "EPERM" || "EACCES"` specifically — generic IO errors (disk full, read-only volume, etc.) still get the original bare message because they don't have a one-line "do this" fix.

Testability hook

  • ensurePilotCapabilitiesDir reads an optional globalThis[Symbol.for("pilot.test.ensureCapabilities")] override before falling through to the real mkdir. Production code never sets this; tests in forge.test.ts use it to inject a synthetic EPERM failure without having to mock the read-only ESM node:fs/promises module. The hook is documented in the helper's docstring.

Tests

  • root: 543/543 ✓ (was 541 in v0.6.14; +2 forgeAbsorb regression cases — one for the lazy-init happy path, one for the EPERM error message)
  • web: 214/214 ✓ (unchanged — the fix is server-side core)
  • format:check root + web: ✓
  • lint (root eslint src test --max-warnings 0): ✓
  • tsc root + web: ✓
  • production build (next build): ✓

Operator note for the reporting user

If you're reading this because pilot forge absorb is failing in your shell: open a non-sandboxed Terminal (/Applications/Utilities/Terminal.app) and run pilot init once. That creates ~/.pilot/capabilities/ in a context where your shell actually has write permission. Subsequent pilot forge absorb calls will work, even from sandboxed shells (v0.6.15 will lazy-init the directory on first use, so re-running pilot init is no longer required).

Deliberately NOT done (v0.6.16+ backlog)

  • multiple connections (A↔B 双向)
  • connection color 自定义
  • auto-route 避开 block 中心
  • ComposeBoard.tsx hooks/state 抽离

Commit

  • ff1a9fc v0.6.15: forge absorb now lazy-inits ~/.pilot/capabilities/ + clearer EPERM error