ferrycat 0.4.0 → 0.5.0

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/README.md CHANGED
@@ -13,8 +13,8 @@
13
13
  [![Glama](https://img.shields.io/badge/Glama-MCP_Server-18181b?style=flat)](https://glama.ai/mcp/servers/GuoBug/FerryCat)
14
14
  [![License](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
15
15
 
16
- > **FerryCat** (`ferrycat`) is a lightweight, deterministic cross-host AI agent handoff and code review bus built on the Model Context Protocol (MCP) and Node.js built-in `node:sqlite` (WAL mode).
17
- > Purpose-built to safely bridge isolated desktop IDE agent hosts (such as Google Antigravity, WorkBuddy, Claude Code, and Cursor) across separate OS processes without context drift or blind reviews on stale files.
16
+ > **FerryCat** (`ferrycat`) is a lightweight, deterministic cross-host AI agent handoff, code review, and verification harness built on the Model Context Protocol (MCP) and Node.js built-in `node:sqlite` (WAL mode).
17
+ > Purpose-built to safely bridge isolated desktop IDE agent hosts (such as Google Antigravity, WorkBuddy, Claude Code, and Cursor) across separate OS processes—enabling Actor-Critic evaluation loops, static analysis gates, and SWE-bench-style patch verification without context drift or blind reviews on stale files.
18
18
 
19
19
  <p align="center">
20
20
  <img src="https://guobug.github.io/assets/images/flowchart-ferrycat-cross-host-demand-and-workflow-en.svg" alt="FerryCat Architecture and Workflow" width="960" />
@@ -24,6 +24,26 @@
24
24
 
25
25
  ---
26
26
 
27
+ ## Quick Glance (See It in 60s)
28
+
29
+ No IDE setup or config file edits required. Watch the entire deterministic loop execute inside an isolated sandbox in ~2 seconds:
30
+
31
+ ```bash
32
+ npx ferrycat demo
33
+ ```
34
+
35
+ ```text
36
+ ⚡ FerryCat Zero-Config Sandbox Demo ⚡
37
+ ✔ [1/4] Contract Dispatched Locked: src/calc.ts (sha256: ce3ba9a5...bc5d)
38
+ ✔ [2/4] Contract Claimed Lease: 7424261d-9a3f-4a86-a7ff-de278c273e96
39
+ ✔ [3/4] Execution & Check Heartbeat 1 renewal | CHANGES_REQUESTED (loose equality caught)
40
+ ✔ [4/4] Contract Completed TOCTOU: 0 dirty files | Status: COMPLETED (elapsed: 2.1s)
41
+ 🎉 End-to-end multi-agent handoff verified successfully!
42
+ ```
43
+ > 💡 Real benchmark: spins up an isolated child-process sandbox, triggers 1 real lease renewal, and runs a static analysis & verification gate that catches loose equality at `src/calc.ts:6`. Options (`--keep` / `--json` / `--no-color`) detailed in [Quickstart §0](#0-see-what-it-actually-does-first-ferrycat-demo).
44
+
45
+ ---
46
+
27
47
  ## Core Pain Points & Engineering Solutions
28
48
 
29
49
  While the official MCP `Tasks` specification (2025-11 / 2026-07) addresses long-running operations within a **single `Client ↔ Server` connection**, it does not solve cross-host collaboration on a developer workstation:
@@ -87,6 +107,8 @@ Every state transition is bound to an explicit SQL transaction with optimistic c
87
107
  └───────────┘ └───────────────────┘ └───────────────┘
88
108
  ```
89
109
 
110
+ > *Note on `POINTER_STALE` vs. `SELF_CONTAINED` Patch Mode (FC-202)*: `POINTER_STALE` is terminal whenever on-disk pointer files mismatch the dispatched SHA-256 baseline, **except** when a read-only contract (`code_review` / `architecture_audit`) attaches a verified unified `git_patch` (`--patch-file`) that satisfies all three gates: every declared file block carries at least one `@@` hunk with real `+`/`-` content (`EMPTY_PATCH_HUNKS` / `EMPTY_PATCH_CONTENT` otherwise), and the touched paths cover every pointer file (`PATCH_DOES_NOT_COVER_POINTERS` otherwise). In that `SELF_CONTAINED` mode, `poll_next_task` returns `patch_mode: "SELF_CONTAINED"` with an explicit `workspace_warning` instructing the reviewer to inspect `git_patch` rather than stale on-disk files.
111
+
90
112
  Holder-side lifecycle controls on a `CLAIMED` contract (all require `agent_id === target_agent`):
91
113
 
92
114
  | Call | Transition | Guard rails |
@@ -97,7 +119,7 @@ Holder-side lifecycle controls on a `CLAIMED` contract (all require `agent_id ==
97
119
 
98
120
  ### 2. Full-File SHA-256 Fingerprinting, Pre-Flight Verify & TOCTOU Soft-Failure Buffering (ADR-0004)
99
121
  - **Dispatch Snapshot**: `dispatch_contract` reads target files on disk, recording byte `size`, full-file `sha256` (64 hex chars), `first_line`, and `last_line`. If a file was modified within the last 3 seconds, a `warning` is emitted to alert the requester of potential unsaved IDE buffers.
100
- - **Claim Verification**: `poll_next_task` verifies `Existence -> Byte Size -> Full-File SHA-256` (never rejecting on `mtime_ms` alone to tolerate no-op IDE saves). On mismatch, it waits `150ms` and retries once to rule out transient IDE flushes before marking the contract `POINTER_STALE`.
122
+ - **Claim Verification**: `poll_next_task` verifies `Existence -> Byte Size -> Full-File SHA-256` (never rejecting on `mtime_ms` alone to tolerate no-op IDE saves). On mismatch, it waits `150ms` asynchronously (non-blocking, FC-203) and retries once to rule out transient IDE flushes before marking the contract `POINTER_STALE` — unless the contract is a read-only review/audit carrying a verified unified `git_patch` covering all pointer files (`SELF_CONTAINED` patch mode, FC-202), in which case it claims with an explicit `workspace_warning` and records `__verified_on_disk__: "NO"` and `__unverified_pointers__` in `verified_sha256`.
101
123
  - **Pre-Flight Verify (`verify_contract`)**: A strictly read-only check allowing workers or scripts to audit pointer health (`valid: true` vs `valid: false` with dirty files list) before submitting.
102
124
  - **TOCTOU Soft-Failure Buffering**: `fulfill_contract` checks on-disk pointer fingerprints. If dirty pointers are detected:
103
125
  - **Strike 1** returns `RETRY_NEEDED`, leaves the contract `CLAIMED`, renews the execution window, and lists dirty files with recovery instructions (`git stash` / `git checkout -- <file>`).
@@ -135,7 +157,7 @@ Holder-side lifecycle controls on a `CLAIMED` contract (all require `agent_id ==
135
157
 
136
158
  ## Controlled Daemon & Declarative Handlers (ADR-0005)
137
159
 
138
- FerryCat provides a controlled daemon mode governed by SQLite microsecond CAS leases on `daemon_locks` and declarative scripts defined in `ferrycat.config.json`:
160
+ FerryCat provides a controlled daemon mode governed by SQLite microsecond CAS leases on `daemon_locks` and declarative scripts defined in `ferrycat.config.json` (ideal for binding static analysis gates like `tsc` / `eslint` / AST checks or automated test suites like `Jest` / `Vitest` / `Pytest` as deterministic verification workers):
139
161
 
140
162
  ```bash
141
163
  # Run a single dispatch cycle with declared handlers
@@ -143,8 +165,13 @@ ferrycat-cli daemon --config ./ferrycat.config.json --once
143
165
 
144
166
  # Start continuous daemon loop
145
167
  ferrycat-cli daemon --config ./ferrycat.config.json
168
+
169
+ # Inspect lease ownership + write-ahead journal health (read-only)
170
+ ferrycat-cli daemon --status
146
171
  ```
147
172
 
173
+ `daemon --status` reports `journal_degraded` / `last_journal_error` / `journal_path` / `journal_entries` alongside lease ownership, so a silently unwritable write-ahead journal (which would disarm crash recovery) is visible instead of failing mute.
174
+
148
175
  Example `ferrycat.config.json`:
149
176
  ```json
150
177
  {
@@ -210,7 +237,7 @@ Sandbox: /tmp/ferrycat-demo-c5iK6V
210
237
 
211
238
  ### 1. Recommended: Zero-Install MCP Mounting (via npx)
212
239
 
213
- No local clone or manual compilation required. Mount directly into your IDE's MCP configuration using `npx -y ferrycat`:
240
+ No local clone or manual compilation required. Run `npx ferrycat init --yes` to auto-detect installed hosts (Antigravity, WorkBuddy, Cursor, VS Code, Windsurf, Claude Desktop, Claude Code) and safely merge the `ferrycat` server entry (with backup and JSONC protection), or mount manually into your IDE's MCP configuration using `npx -y ferrycat`:
214
241
 
215
242
  #### Antigravity Configuration
216
243
  Edit `%USERPROFILE%\.gemini\antigravity\mcp_config.json` (or `~/.gemini/antigravity/mcp_config.json` on macOS/Linux):
@@ -288,7 +315,25 @@ npx ferrycat verify <CONTRACT_ID>
288
315
  Poll the queue, claim tasks, and execute local review/test scripts with automatic failure closure:
289
316
  ```bash
290
317
  # Safely run a script upon task arrival (no shell quotation hazards)
291
- npx ferrycat worker --agent codex --mode=exec --exec-file ./scripts/review.sh
318
+ npx ferrycat worker --agent codex --mode=exec --exec-file ./scripts/review.sh --delivery envelope
319
+ # See docs/examples/ for dispatch.example.json, fulfill.example.json, and handler.example.cjs templates.
320
+ ```
321
+
322
+ ### 4. Zero-Config Sandbox Walkthrough (demo)
323
+ Run the self-contained multi-agent lifecycle simulation without configuring an MCP server:
324
+ ```bash
325
+ npx ferrycat demo
326
+ ```
327
+ Options (`--keep` to retain SQLite for manual inspection, `--json` for CI assertions) detailed in [Quickstart §0](#0-see-what-it-actually-does-first-ferrycat-demo).
328
+
329
+ ### 5. Production Controlled Daemon (daemon, ADR-0005)
330
+ Run the headless orchestration daemon coordinating background workers via atomic CAS leases and declarative script bindings:
331
+ ```bash
332
+ # Launch daemon using project-local declarative configuration
333
+ npx ferrycat daemon --config ./ferrycat.config.json
334
+
335
+ # Run single poll/dispatch iteration for a targeted agent and exit
336
+ npx ferrycat daemon --agent workbuddy --once
292
337
  ```
293
338
 
294
339
  ---
@@ -297,20 +342,22 @@ npx ferrycat worker --agent codex --mode=exec --exec-file ./scripts/review.sh
297
342
 
298
343
  | Tool Name | Role | Transactional Behavior |
299
344
  | :--- | :--- | :--- |
300
- | **`dispatch_contract`** | Requester | Enforces path blacklist $\to$ computes full-file SHA-256 & line bounds $\to$ inserts `SUBMITTED`. |
301
- | **`poll_next_task`** | Worker | Sweeps expired TTLs (read-before-write) $\to$ claims via `changes === 1` (`CLAIMED`) $\to$ verifies disk SHA-256 (or marks `POINTER_STALE`). |
345
+ | **`dispatch_contract`** | Requester | Enforces path blacklist $\to$ computes full-file SHA-256 & line bounds (plus optional unified `git_patch` / `--patch-file` validation covering pointers, FC-202) $\to$ inserts `SUBMITTED`. |
346
+ | **`poll_next_task`** | Worker | Sweeps expired TTLs (read-before-write) $\to$ claims via `changes === 1` (`CLAIMED`) $\to$ verifies disk SHA-256 with async 150ms retry (FC-203) or falls back to `SELF_CONTAINED` patch mode with `workspace_warning` (otherwise marks `POINTER_STALE`). |
302
347
  | **`fulfill_contract`** | Worker | Re-verifies SHA-256 (TOCTOU guard) $\to$ validates Ajv JSON Schema ($\le 2$ retries via `RETRY_NEEDED`, 3rd marks `VALIDATION_FAILED`) $\to$ marks `COMPLETED`. |
303
348
  | **`release_contract`** | Worker (holder) | Hands a `CLAIMED` contract back to the queue tail as `SUBMITTED`. `retry_count` is preserved (contract-level circuit breaker); `extend_count` is reset. Max 3 releases; refused once past the absolute lifetime deadline. |
304
349
  | **`extend_ttl`** | Worker (holder) | Heartbeat: restarts the execution window (`claimed_at = now`), max 5 renewals and bounded total lifetime. |
305
350
  | **`fail_contract`** | Worker (holder) | Marks a `CLAIMED` contract terminal `FAILED` with a mandatory `reason`. |
306
351
  | **`verify_contract`** | Worker / Any (Read-Only) | Pre-flight check: validates on-disk pointer files against the baseline SHA-256 without mutating contract state (ADR-0004). |
307
- | **`inspect_mesh_state`** | Any (Read-Only) | Pure read-only query of contract states, outputs, and dynamic `is_logically_expired` flags. |
352
+ | **`inspect_mesh_state`** | Any (Read-Only) | Pure read-only query of contract states, outputs, `patch_sha256` / `patch_bytes` (opt-in `include_patch: true`), and dynamic `is_logically_expired` flags. |
308
353
 
309
354
  > 💡 **Architecture Decision Records & Engineering Verification**:
310
355
  > - [ADR-0001: Cross-Host SQLite WAL Bus & Human-Scheduled Poll Model](docs/adr/0001-cross-host-sqlite-wal-and-poll-model.md) (Why `node:sqlite` WAL + `poll_next_task` instead of 60s Blocking Hold or single-connection MCP Tasks)
311
356
  > - [ADR-0002: Full-File SHA-256 Fingerprinting, TOCTOU Guard & Cross-Model Audit](docs/adr/0002-fingerprint-toctou-and-cross-model-audit.md) (Stale-context prevention and real-world cross-host verification records)
312
357
  > - [ADR-0003: GUI Sandbox vs CLI Process Hybrid Layering Architecture & Wake-Up Boundaries](docs/adr/0003-gui-cli-hybrid-architecture.md) (Desktop HITL approval vs in-house CLI dual-channel autonomy)
313
- > - [ADR-0004: Separation of Review and Mutation (SRM), TOCTOU Soft-Failure Buffering & Immutable Baseline Protocol](docs/adr/0004-review-mutation-separation-and-toctou-buffer.md) (How to prevent review abortion from concurrent or well-intended reviewer edits)
358
+ > - [ADR-0004: Separation of Review and Mutation (SRM), TOCTOU Soft-Failure Buffering & Immutable Baseline Protocol](docs/adr/0004-review-mutation-separation-and-toctou-buffer.md) (How to prevent review abortion from concurrent or well-intended reviewer edits; live mesh case: contract `fc_1793098036393_aea8bb9b`)
359
+ > - [ADR-0005: Controlled Daemon, Atomic CAS Micro-Leases & Safe Execution Boundaries](docs/adr/0005-controlled-daemon-and-process-supervision.md) (Microsecond atomic CAS leases on `daemon_locks` and declarative whitelist handler enforcement)
360
+ > - [ADR-0006: Dual-Track TOCTOU Semantics & Post-Mutation SHA-256 Fulfillment](docs/adr/0006-dual-track-mutation-fulfillment.md) (Why `implementation` / `test_generation` tasks commit post-mutation SHA-256 into `verified_sha256` while preserving pre-mutation baselines in `pointers`; live mesh case: `fc_1793100801409_4eedf544`)
314
361
  > - [FerryCat Requirements Backlog & Roadmap](docs/roadmap.md)
315
362
 
316
363
  ---
package/README_zh.md CHANGED
@@ -13,8 +13,8 @@
13
13
  [![Glama](https://img.shields.io/badge/Glama-MCP_Server-18181b?style=flat)](https://glama.ai/mcp/servers/GuoBug/FerryCat)
14
14
  [![License](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
15
15
 
16
- > **FerryCat** (`ferrycat`) 是一个基于 Model Context Protocol (MCP) 与 Node.js 内置 `node:sqlite`(WAL 并发模式)构建的轻量级、确定性跨宿主 AI Agent 工单交接与交叉评审总线。
17
- > 专为解决单机桌面环境下不同 IDE 宿主(如 Google Antigravity、WorkBuddy、Claude Code、Cursor)因进程物理隔离导致的上下文割裂、未落盘代码静默盲审与非结构化交付痛点而设计。
16
+ > **FerryCat** (`ferrycat`) 是一个基于 Model Context Protocol (MCP) 与 Node.js 内置 `node:sqlite`(WAL 并发模式)构建的轻量级、确定性跨宿主 AI Agent 工单交接、交叉评审与评测验证底座(Verification Harness)。
17
+ > 专为解决单机桌面环境下不同 IDE 宿主(如 Google Antigravity、WorkBuddy、Claude Code、Cursor)因进程物理隔离导致的上下文割裂、未落盘代码静默盲审与非结构化交付痛点而设计,支持构建 Actor-Critic 双智能体评测、静态分析守卫(Static Analysis Gate)与 SWE-bench 风格的补丁验证流水线。
18
18
 
19
19
  <p align="center">
20
20
  <img src="https://guobug.github.io/assets/images/flowchart-ferrycat-cross-host-demand-and-workflow.svg" alt="FerryCat 跨宿主契约摆渡全景" width="960" />
@@ -24,6 +24,26 @@
24
24
 
25
25
  ---
26
26
 
27
+ ## 60 秒看懂它 (Quick Glance)
28
+
29
+ 无需安装配置任何 IDE,无需修改配置文件,一条命令在系统临时目录跑通完整沙箱闭环:
30
+
31
+ ```bash
32
+ npx ferrycat demo
33
+ ```
34
+
35
+ ```text
36
+ ⚡ FerryCat Zero-Config Sandbox Demo ⚡
37
+ ✔ [1/4] Contract Dispatched Locked: src/calc.ts (sha256: ce3ba9a5...bc5d)
38
+ ✔ [2/4] Contract Claimed Lease: 7424261d-9a3f-4a86-a7ff-de278c273e96
39
+ ✔ [3/4] Execution & Check Heartbeat 1 renewal | CHANGES_REQUESTED (loose equality caught)
40
+ ✔ [4/4] Contract Completed TOCTOU: 0 dirty files | Status: COMPLETED (elapsed: 2.1s)
41
+ 🎉 End-to-end multi-agent handoff verified successfully!
42
+ ```
43
+ > 💡 实测数据:2 秒真实拉起独立子进程沙箱、真实心跳续约 1 次、通过静态分析与验证守卫真实检出 `src/calc.ts:6` 的松散相等缺陷。详细参数(`--keep` / `--json` / `--no-color`)与底层机制见 [快速上手 §0](#0-先用一条命令看它到底是什么ferrycat-demo)。
44
+
45
+ ---
46
+
27
47
  ## 核心痛点与工程解法
28
48
 
29
49
  MCP 官方的 `Tasks` 规范(2025-11 / 2026-07)解决的是**单条 `Client ↔ Server` 连接内部**的长耗时任务状态跟踪,但无法解决桌面端跨宿主协同的四个物理难题:
@@ -87,6 +107,8 @@ MCP 官方的 `Tasks` 规范(2025-11 / 2026-07)解决的是**单条 `Client
87
107
  └───────────┘ └───────────────────┘ └───────────────┘
88
108
  ```
89
109
 
110
+ > *关于 `POINTER_STALE` 与 `SELF_CONTAINED` 补丁模式的边界说明 (FC-202)*:当磁盘文件与派单基线 SHA-256 不一致时,工单默认流转至终态 `POINTER_STALE`;**唯一例外**是只读审查类任务(`code_review` / `architecture_audit`)附带了同时通过三重闸门的统一差异补丁(`git_patch` / `--patch-file`):每个声明文件块必须含至少一个带真实 `+`/`-` 内容的 `@@` hunk(否则 `EMPTY_PATCH_HUNKS` / `EMPTY_PATCH_CONTENT`),且补丁触及路径必须覆盖全部指针文件(否则 `PATCH_DOES_NOT_COVER_POINTERS`)。在此 `SELF_CONTAINED` 模式下,`poll_next_task` 返回 `patch_mode: "SELF_CONTAINED"` 及显式告警 `workspace_warning`,指示审查方以 `git_patch` 为准,且交单后 `verified_sha256` 显式标记 `__verified_on_disk__: "NO"` 与 `__unverified_pointers__`。
111
+
90
112
  持单方对 `CLAIMED` 工单的生命周期控制(均要求 `agent_id === target_agent`):
91
113
 
92
114
  | 调用 | 状态流转 | 护栏 |
@@ -97,7 +119,7 @@ MCP 官方的 `Tasks` 规范(2025-11 / 2026-07)解决的是**单条 `Client
97
119
 
98
120
  ### 2. 全文件 SHA-256 指纹校验、预检与 TOCTOU 软失败缓冲 (ADR-0004)
99
121
  - **派单快照**:`dispatch_contract` 读取磁盘文件,记录字节数 `size`、全文件 `sha256`(64 位 hex)以及非空首末行 `first_line` / `last_line`。若文件在最近 3 秒内刚被修改,返回 `warning` 提醒确认 IDE 缓冲区已落盘。
100
- - **领单核验**:`poll_next_task` 严格按 `存在性 -> 字节数 size -> 全文件 sha256` 顺序核验(绝不以 `mtime` 单独判死刑,避免 IDE 无变更保存导致误杀)。若不一致,等待 `150ms` 重试一次排除瞬态写入,仍不一致则直接置为终态 `POINTER_STALE`。
122
+ - **领单核验**:`poll_next_task` 严格按 `存在性 -> 字节数 size -> 全文件 sha256` 顺序核验(绝不以 `mtime` 单独判死刑,避免 IDE 无变更保存导致误杀)。若不一致,异步等待 `150ms` 重试一次(非阻塞,FC-203)排除瞬态写入;若仍不一致,除附带完整覆盖指针的 `git_patch` 只读任务降级为 `SELF_CONTAINED` 模式(附带 `workspace_warning`)外,其余直接置为终态 `POINTER_STALE`。
101
123
  - **只读预检 (`verify_contract`)**:纯只读命令,允许 Worker 或外部脚本在交单前核验指针健康度(返回 `valid: true/false` 与脏文件清单),不消耗工单状态。
102
124
  - **TOCTOU 软失败缓冲**:`fulfill_contract` 在交单时二次核验全文件指纹。若检测到脏指针:
103
125
  - **第 1 次超标(Strike 1)**:返回 `RETRY_NEEDED`,工单保持 `CLAIMED` 并自动续期执行窗口,返回脏文件清单与抢救指引(`git stash` / `git checkout -- <file>`)。
@@ -135,7 +157,7 @@ MCP 官方的 `Tasks` 规范(2025-11 / 2026-07)解决的是**单条 `Client
135
157
 
136
158
  ## 守护进程与声明式处理 (Controlled Daemon, ADR-0005)
137
159
 
138
- FerryCat 支持受控后台守护进程,通过 SQLite 微事务 CAS 租约锁实现多进程安全抢占与故障转移,并强制通过 `ferrycat.config.json` 静态绑定脚本处理器:
160
+ FerryCat 支持受控后台守护进程,通过 SQLite 微事务 CAS 租约锁实现多进程安全抢占与故障转移,并强制通过 `ferrycat.config.json` 静态绑定脚本处理器(非常适合挂载 `tsc` / `eslint` / AST 静态分析守卫,或接入 `Jest` / `Vitest` / `Pytest` 自动化测试套件作为确定性验证 Worker):
139
161
 
140
162
  ```bash
141
163
  # 单次轮询分发并执行已声明的处理脚本
@@ -143,6 +165,8 @@ ferrycat-cli daemon --config ./ferrycat.config.json --once
143
165
 
144
166
  # 启动后台守护主循环
145
167
  ferrycat-cli daemon --config ./ferrycat.config.json
168
+ # 只读查询租约归属与预写日志健康度
169
+ ferrycat-cli daemon --status
146
170
  ```
147
171
 
148
172
  配置文件形态示例(`ferrycat.config.json`):
@@ -209,7 +233,7 @@ Sandbox: C:\Users\...\Temp\ferrycat-demo-c5iK6V
209
233
 
210
234
  ### 1. 推荐:零配置免安装挂载 (npx)
211
235
 
212
- 无需手动克隆仓库或本地编译,直接在各 IDE 宿主的 MCP 配置文件中声明(基于全局或 `npx -y ferrycat`):
236
+ 无需手动克隆仓库或本地编译。推荐直接运行 `npx ferrycat init --yes` 自动探测本机已安装的 IDE 宿主(Antigravity、WorkBuddy、Cursor、VS Code、Windsurf、Claude Desktop、Claude Code)并安全合并注入配置(自带原文件备份与 JSONC 注释保护),或在各 IDE 宿主的 MCP 配置文件中手动声明(基于全局或 `npx -y ferrycat`):
213
237
 
214
238
  #### Antigravity 宿主配置
215
239
  编辑 `%USERPROFILE%\.gemini\antigravity\mcp_config.json`(macOS/Linux 为 `~/.gemini/antigravity/mcp_config.json`):
@@ -287,7 +311,25 @@ npx ferrycat verify <CONTRACT_ID>
287
311
  监听契约队列并直接拉起本地脚本执行(支持安全脚本执行与非零退出自动熔断为 `FAILED`):
288
312
  ```bash
289
313
  # 启动常驻 Worker 监听并自动执行指定脚本(无 Shell 注入风险)
290
- npx ferrycat worker --agent codex --mode=exec --exec-file ./scripts/review.sh
314
+ npx ferrycat worker --agent codex --mode=exec --exec-file ./scripts/review.sh --delivery envelope
315
+ # 标准载荷与 Handler 范例模板见 docs/examples/(dispatch.example.json / fulfill.example.json / handler.example.cjs)
316
+ ```
317
+
318
+ ### 4. 零配置一键沙箱演练 (demo)
319
+ 在系统临时目录中构建沙箱,验证两端 Agent 派单、认领、心跳保活与交单的完整流转:
320
+ ```bash
321
+ npx ferrycat demo
322
+ ```
323
+ 完整参数(`--keep` 保留 SQLite 供手动 inspect、`--json` 供 CI 断言等)见 [快速上手 §0](#0-先用一条命令看它到底是什么ferrycat-demo)。
324
+
325
+ ### 5. 受控常驻守护进程(daemon,ADR-0005)
326
+ 脱离前台控制台,基于 SQLite WAL 短事务原子 CAS 租约与声明式配置自动拉起白名单脚本,无人值守但严格受控:
327
+ ```bash
328
+ # 依据当前目录 ferrycat.config.json 启动常驻守护中枢
329
+ npx ferrycat daemon --config ./ferrycat.config.json
330
+
331
+ # 指定监听单一 Agent 且仅处理一次工单后退出(适合轻量调度)
332
+ npx ferrycat daemon --agent workbuddy --once
291
333
  ```
292
334
 
293
335
  ---
@@ -296,20 +338,22 @@ npx ferrycat worker --agent codex --mode=exec --exec-file ./scripts/review.sh
296
338
 
297
339
  | 工具名 | 调用角色 | 职责与事务行为 |
298
340
  | :--- | :--- | :--- |
299
- | **`dispatch_contract`** | 发起方 (Requester) | 校验路径黑名单 $\to$ 计算全文件 SHA-256 与首末行快照 $\to$ 写入 `SUBMITTED`。 |
300
- | **`poll_next_task`** | 接收方 (Worker) | 先读后写清理超时工单 $\to$ `changes === 1` 原子抢占为 `CLAIMED` $\to$ 校验文件指纹(不匹配则置为 `POINTER_STALE`)。 |
341
+ | **`dispatch_contract`** | 发起方 (Requester) | 校验路径黑名单 $\to$ 计算全文件 SHA-256 与首末行快照(以及可选的 `git_patch` / `--patch-file` 统一补丁及指针覆盖校验,FC-202)$\to$ 写入 `SUBMITTED`。 |
342
+ | **`poll_next_task`** | 接收方 (Worker) | 先读后写清理超时工单 $\to$ `changes === 1` 原子抢占为 `CLAIMED` $\to$ 校验文件指纹(含 150ms 异步重试 FC-203;只读任务附带完整覆盖指针的 `git_patch` 时进入 `SELF_CONTAINED` 模式并返回 `workspace_warning`,否则置为 `POINTER_STALE`)。 |
301
343
  | **`verify_contract`** | 接收方 / 任意方 (Read-Only) | 只读预检:校验磁盘文件指纹与基线是否一致,不变更任何状态(ADR-0004)。 |
302
344
  | **`fulfill_contract`** | 接收方 (Worker) | 二次复核文件指纹(TOCTOU 软失败缓冲:Strike 1 返回 `RETRY_NEEDED`,Strike 2 置为 `POINTER_STALE` 并在 `salvaged_result` 抢救产出)$\to$ 校验 Ajv JSON Schema $\to$ 通过则置为 `COMPLETED`。 |
303
345
  | **`release_contract`** | 持单方 (Worker) | 将 `CLAIMED` 工单退回队尾重新置为 `SUBMITTED`。保留 `retry_count`(工单级熔断配额),清零 `extend_count`。最多退回 3 次;超出绝对生命期上限时拒绝。 |
304
346
  | **`extend_ttl`** | 持单方 (Worker) | 心跳续期:重置执行窗口(`claimed_at = now`),最多 5 次,总生命周期有上限。 |
305
347
  | **`fail_contract`** | 持单方 (Worker) | 携带必填 `reason` 将 `CLAIMED` 工单置为终态 `FAILED`。 |
306
- | **`inspect_mesh_state`** | 任意方 (Read-Only) | 纯只读查询工单状态、结构化产出与错误原因,动态计算 `is_logically_expired`。 |
348
+ | **`inspect_mesh_state`** | 任意方 (Read-Only) | 纯只读查询工单状态、结构化产出、`patch_sha256` / `patch_bytes`(可通过 `include_patch: true` 显式读取补丁正文)与错误原因,动态计算 `is_logically_expired`。 |
307
349
 
308
350
  > 💡 **架构决策记录 (ADR) 与工程验证沉淀**:
309
351
  > - [ADR-0001: 采用跨宿主 SQLite WAL 总线与人工调度即时轮询模型](docs/adr/0001-cross-host-sqlite-wal-and-poll-model_zh.md)(为什么采用 `node:sqlite` WAL + `poll_next_task`,而非 60s 长轮询挂起或单连接 MCP Tasks)
310
352
  > - [ADR-0002: 全文件 SHA-256 指纹防盲审、TOCTOU 闭合与跨宿主多模型实测记录](docs/adr/0002-fingerprint-toctou-and-cross-model-audit_zh.md)(指针陈旧防护设计与 3 模型真实互审复盘记录)
311
353
  > - [ADR-0003: GUI 沙箱与 CLI 进程混合分层架构、唤醒边界与双通道 Worker 选型](docs/adr/0003-gui-cli-hybrid-architecture_zh.md)(桌面人在回路审批闸 vs 自研 CLI 双通道自治)
312
- > - [ADR-0004: 审议分离协议、TOCTOU 软失败缓冲与审查期不可变基线规范](docs/adr/0004-review-mutation-separation-and-toctou-buffer_zh.md)(如何彻底规避审查者“边审边修”或并发写入引发的工单报废)
354
+ > - [ADR-0004: 审议分离协议、TOCTOU 软失败缓冲与审查期不可变基线规范](docs/adr/0004-review-mutation-separation-and-toctou-buffer_zh.md)(如何彻底规避审查者“边审边修”或并发写入引发的工单报废;真实案例见 2026-10-27 真实总线工单 `fc_1793098036393_aea8bb9b` 漂移拦截)
355
+ > - [ADR-0005: 受控守护进程、短事务 CAS 租约与安全执行边界](docs/adr/0005-controlled-daemon-and-process-supervision_zh.md)(微秒级原子 CAS 租约如何防死锁、声明式 handler 如何将任意 Shell 注入面彻底收敛)
356
+ > - [ADR-0006: 读写双轨 TOCTOU 语义与变更型任务双哈希交单存证协议](docs/adr/0006-dual-track-mutation-fulfillment_zh.md)(为什么 `implementation` / `test_generation` 任务在 `pointers` 保留修改前基线并在 `verified_sha256` 落库修改后产物哈希;实战案例:`fc_1793100801409_4eedf544`)
313
357
  > - [FerryCat 演进路线图 (Roadmap)](docs/roadmap_zh.md)
314
358
 
315
359
  ---
package/bin/ferrycat.js CHANGED
@@ -12,6 +12,7 @@ if (major < 22 || (major === 22 && minor < 5)) {
12
12
 
13
13
  const CLI_SUBCOMMANDS = new Set([
14
14
  'demo',
15
+ 'init',
15
16
  'dispatch',
16
17
  'poll',
17
18
  'verify',
package/dist/cli.js CHANGED
@@ -6,6 +6,7 @@ import { ZodError } from 'zod';
6
6
  import { expandTilde, FerryCatEngine } from './engine.js';
7
7
  import { FerryCatDaemon } from './daemon.js';
8
8
  import { runDemo } from './demo.js';
9
+ import { runInit } from './init.js';
9
10
  import { executeTaskAndFulfill, terminateActiveChildrenSync, } from './supervisor.js';
10
11
  import { DispatchContractSchema, ExtendTtlSchema, FailContractSchema, FulfillContractSchema, InspectMeshStateSchema, PollNextTaskSchema, ReleaseContractSchema, VerifyContractSchema, WorkerCommandSchema, } from './schemas.js';
11
12
  function loadPackageVersion() {
@@ -37,6 +38,7 @@ Commands:
37
38
  worker Run background watcher, single-shot claim, or automated script execution
38
39
  daemon Run multi-tenant production daemon with CAS micro-lease and declarative handlers (ADR-0005)
39
40
  demo Zero-config sandbox walkthrough of the full contract lifecycle (no MCP setup needed)
41
+ init Detect installed MCP hosts (Antigravity, WorkBuddy, Cursor, VS Code, ...) and add FerryCat to them
40
42
 
41
43
  Common Options:
42
44
  --agent <id> Agent identity (overrides AGENT_NAME env var)
@@ -58,16 +60,18 @@ Examples:
58
60
  # 4. Run worker in explicit mode (watch | claim | exec) with quote-free --exec-file
59
61
  ferrycat-cli worker --agent workbuddy --mode=exec --once --exec-file ./scripts/review.js`;
60
62
  const SUBCOMMAND_HELP = {
61
- dispatch: `Usage: ferrycat-cli dispatch [--agent <requester>] [--file <path|->] ['{...}']
63
+ dispatch: `Usage: ferrycat-cli dispatch [--agent <requester>] [--patch-file <path>] [--file <path|->] ['{...}']
62
64
 
63
65
  Dispatches a new contract and computes full-file SHA-256 fingerprints for all pointer files.
64
66
 
65
67
  Options:
66
- --agent <id> Requester agent identifier (overrides requester_agent in payload)
67
- --file <path | -> JSON payload file path, or "-" to read from stdin
68
+ --agent <id> Requester agent identifier (overrides requester_agent in payload)
69
+ --patch-file <path> Optional unified git diff file path to attach as self-contained git_patch (FC-202)
70
+ --file <path | -> JSON payload file path, or "-" to read from stdin
68
71
 
69
72
  Examples:
70
73
  ferrycat-cli dispatch --file dispatch.json
74
+ ferrycat-cli dispatch --file dispatch.json --patch-file changes.patch
71
75
  cat dispatch.json | ferrycat-cli dispatch --agent antigravity -`,
72
76
  poll: `Usage: ferrycat-cli poll [--agent <worker_id>]
73
77
 
@@ -159,7 +163,7 @@ Options:
159
163
  Examples:
160
164
  ferrycat-cli inspect --target workbuddy --status ACTIVE
161
165
  ferrycat-cli inspect --id fc_123 --json`,
162
- worker: `Usage: ferrycat-cli worker [--agent <id>] [--mode <watch|claim|exec>] [--once] [--interval <ms>] [--exec <cmd>] [--exec-file <script>]
166
+ worker: `Usage: ferrycat-cli worker [--agent <id>] [--mode <watch|claim|exec>] [--once] [--interval <ms>] [--exec <cmd>] [--exec-file <script>] [--pass-env <NAME>] [--delivery <auto|envelope>]
163
167
 
164
168
  Runs the FerryCat CLI worker in watcher, single-claim, or automated execution mode.
165
169
 
@@ -173,12 +177,23 @@ Options:
173
177
  --interval <ms> Polling interval in milliseconds for persistent loop (>= 100, default: 1000)
174
178
  --exec-file <script> Recommended: script file (.js/.mjs/.cjs/.ts/.ps1/.sh) executed directly via execFile without shell interpolation
175
179
  --exec <cmd> Shell command to run on claim (UNSAFE: executes via system shell, contract JSON piped via stdin)
180
+ --pass-env <NAME> Forward an extra host env var into the child (repeatable)
181
+ --delivery <auto|envelope>
182
+ How child stdout becomes the deliverable (REQ-ENG-04):
183
+ auto: {"ferrycat_delivery":{...}} envelope, or a JSON object with
184
+ result_summary / structured_output, or plain text (default)
185
+ envelope: only the ferrycat_delivery envelope is accepted; anything
186
+ else marks the contract FAILED (stdout is salvaged)
187
+
188
+ Delivery envelope (one stdout line, other lines are treated as logs):
189
+ {"ferrycat_delivery":{"result_summary":"...","structured_output":{...}}}
176
190
 
177
191
  Examples:
178
192
  ferrycat-cli worker --agent workbuddy --mode=watch
179
193
  ferrycat-cli worker --agent workbuddy --mode=claim
180
- ferrycat-cli worker --agent workbuddy --mode=exec --once --exec-file ./worker-handler.js`,
181
- daemon: `Usage: ferrycat-cli daemon [--config <path>] [--agent <id>] [--once] [--lease-ttl <sec>] [--interval <ms>]
194
+ ferrycat-cli worker --agent workbuddy --mode=exec --once --exec-file ./worker-handler.js
195
+ ferrycat-cli worker --agent workbuddy --mode=exec --exec-file ./handler.js --delivery envelope`,
196
+ daemon: `Usage: ferrycat-cli daemon [--config <path>] [--agent <id>] [--once] [--status] [--lease-ttl <sec>] [--interval <ms>]
182
197
 
183
198
  Runs the FerryCat production daemon (ADR-0005). Coordinates background workers using short-transaction
184
199
  CAS leases on the daemon_locks table and executes declarative scripts defined in ferrycat.config.json.
@@ -187,13 +202,16 @@ Options:
187
202
  --config <path> Path to ferrycat.config.json (defaults to ./ferrycat.config.json or ~/.ferrycat/config.json)
188
203
  --agent <id> Optional target agent filter (overrides config defaults)
189
204
  --once Run a single dispatch cycle and exit
205
+ --status Print lease ownership and write-ahead journal health (journal_degraded,
206
+ last_journal_error, journal_path, entries) as JSON, then exit. Read-only.
190
207
  --lease-ttl <sec> Daemon CAS lease TTL in seconds (default: 30)
191
208
  --interval <ms> Poll interval in milliseconds (default: 1000)
192
209
  --no-notify Disable console notices
193
210
 
194
211
  Examples:
195
212
  ferrycat-cli daemon --config ./ferrycat.config.json
196
- ferrycat-cli daemon --agent workbuddy --once`,
213
+ ferrycat-cli daemon --agent workbuddy --once
214
+ ferrycat-cli daemon --status`,
197
215
  demo: `Usage: ferrycat-cli demo [--json] [--keep] [--no-color]
198
216
 
199
217
  Runs a complete contract lifecycle (dispatch -> claim -> supervised execution -> TOCTOU
@@ -211,6 +229,45 @@ Examples:
211
229
  ferrycat-cli demo
212
230
  ferrycat-cli demo --json
213
231
  ferrycat-cli demo --keep`,
232
+ init: `Usage: ferrycat-cli init [--client <id>]... [--agent <name>] [--config-path <file>] [--local] [--yes] [--dry-run] [--force] [--list] [--server-name <key>] [--json]
233
+
234
+ Detects locally installed MCP hosts and merges a "ferrycat" stdio server entry
235
+ ({"command":"npx","args":["-y","ferrycat"],"env":{"AGENT_NAME":"<host>"}}) into each host's
236
+ MCP config file. Each host gets its own AGENT_NAME so contracts can be routed between them.
237
+
238
+ Supported hosts (--client ids):
239
+ antigravity ~/.gemini/antigravity/mcp_config.json
240
+ workbuddy ~/.workbuddy/.mcp.json (or ~/.workbuddy/mcp.json if already present)
241
+ cursor ~/.cursor/mcp.json
242
+ vscode <AppData>/Code/User/mcp.json ("servers" format, type: stdio)
243
+ windsurf ~/.codeium/windsurf/mcp_config.json
244
+ claude-desktop <AppData>/Claude/claude_desktop_config.json
245
+ claude-code ./.mcp.json (project scope, current directory)
246
+
247
+ Safety:
248
+ - Unparseable configs (comments / broken JSON) are never rewritten; the snippet is printed instead.
249
+ - An existing, different "ferrycat" entry is kept unless --force is given.
250
+ - Existing files are backed up as <file>.ferrycat-bak-<timestamp> before writing.
251
+ - In a non-interactive shell without --yes, init only prints the plan (dry run).
252
+
253
+ Options:
254
+ --client <id> Only configure these hosts (repeatable or comma-separated). Hosts that
255
+ are not detected are configured only when named explicitly.
256
+ --agent <name> Override AGENT_NAME for all selected hosts (default: the host id)
257
+ --config-path <file> Override target config file path (requires a single --client)
258
+ --local Point at this checkout's local dist/index.js instead of npx -y ferrycat
259
+ --yes, -y Apply without per-host confirmation
260
+ --dry-run Show what would change without writing
261
+ --force Replace an existing, different "ferrycat" entry
262
+ --list Only print detection results
263
+ --server-name <key> MCP server key to write (default: ferrycat)
264
+ --json Emit the structured result
265
+
266
+ Examples:
267
+ ferrycat-cli init
268
+ ferrycat-cli init --list
269
+ ferrycat-cli init --client workbuddy --client antigravity --yes
270
+ ferrycat-cli init --client cursor --agent cursor-reviewer --dry-run`,
214
271
  };
215
272
  function stripBom(content) {
216
273
  return content.replace(/^\uFEFF/, '');
@@ -373,7 +430,16 @@ function parseArgs(argv) {
373
430
  flagOverrides.claim_token = requireFlagValue(token, rest[++i]);
374
431
  }
375
432
  else if (token === '--status') {
376
- flagOverrides.status = requireFlagValue('--status', rest[++i]);
433
+ // Dual purpose: `inspect --status <STATUS>` filters by contract status, while
434
+ // `daemon --status` is a bare boolean that prints lease + journal health.
435
+ const next = rest[i + 1];
436
+ if (next !== undefined && !next.startsWith('-')) {
437
+ flagOverrides.status = next;
438
+ i += 1;
439
+ }
440
+ else {
441
+ flagOverrides.status = true;
442
+ }
377
443
  }
378
444
  else if (token === '--reason') {
379
445
  flagOverrides.reason = requireFlagValue('--reason', rest[++i]);
@@ -381,6 +447,14 @@ function parseArgs(argv) {
381
447
  else if (token === '--summary') {
382
448
  flagOverrides.result_summary = requireFlagValue('--summary', rest[++i]);
383
449
  }
450
+ else if (token === '--patch-file') {
451
+ const patchPath = requireFlagValue('--patch-file', rest[++i]);
452
+ const resolvedPatch = path.resolve(expandTilde(patchPath));
453
+ if (!fs.existsSync(resolvedPatch)) {
454
+ throw new Error(`Patch file not found: "${patchPath}" (resolved: "${resolvedPatch}").`);
455
+ }
456
+ flagOverrides.git_patch = fs.readFileSync(resolvedPatch, 'utf8');
457
+ }
384
458
  else if (token === '--limit') {
385
459
  const rawLimit = requireFlagValue('--limit', rest[++i]);
386
460
  const num = Number(rawLimit);
@@ -427,6 +501,40 @@ function parseArgs(argv) {
427
501
  }
428
502
  flagOverrides.exec_file = scriptPath;
429
503
  }
504
+ else if (token === '--client') {
505
+ const clientId = requireFlagValue('--client', rest[++i]).trim();
506
+ const existing = flagOverrides.clients ?? [];
507
+ existing.push(...clientId.split(',').map((c) => c.trim()).filter(Boolean));
508
+ flagOverrides.clients = existing;
509
+ }
510
+ else if (token === '--server-name') {
511
+ flagOverrides.server_name = requireFlagValue('--server-name', rest[++i]).trim();
512
+ }
513
+ else if (token === '--config-path') {
514
+ flagOverrides.config_path = requireFlagValue('--config-path', rest[++i]).trim();
515
+ }
516
+ else if (token === '--local') {
517
+ flagOverrides.local = true;
518
+ }
519
+ else if (token === '--yes' || token === '-y') {
520
+ flagOverrides.yes = true;
521
+ }
522
+ else if (token === '--dry-run') {
523
+ flagOverrides.dry_run = true;
524
+ }
525
+ else if (token === '--force') {
526
+ flagOverrides.force = true;
527
+ }
528
+ else if (token === '--list') {
529
+ flagOverrides.list = true;
530
+ }
531
+ else if (token === '--delivery') {
532
+ const mode = requireFlagValue('--delivery', rest[++i]).trim();
533
+ if (mode !== 'auto' && mode !== 'envelope') {
534
+ throw new Error(`Invalid --delivery "${mode}": expected "auto" or "envelope".`);
535
+ }
536
+ flagOverrides.delivery = mode;
537
+ }
430
538
  else if (token === '--pass-env') {
431
539
  const varName = requireFlagValue('--pass-env', rest[++i]).trim();
432
540
  if (!varName) {
@@ -567,7 +675,7 @@ async function runWorkerLoop(engine, agentId, intervalMs, spec) {
567
675
  while (running) {
568
676
  try {
569
677
  if (hasExec) {
570
- const pollRes = engine.pollNextTask({ agent_id: resolvedAgent });
678
+ const pollRes = await engine.pollNextTask({ agent_id: resolvedAgent });
571
679
  if (pollRes.has_task && pollRes.contract) {
572
680
  console.log(JSON.stringify({
573
681
  event: 'TASK_CLAIMED',
@@ -794,7 +902,7 @@ async function main() {
794
902
  }
795
903
  case 'poll': {
796
904
  engine = new FerryCatEngine();
797
- output = engine.pollNextTask(PollNextTaskSchema.parse(data));
905
+ output = await engine.pollNextTask(PollNextTaskSchema.parse(data));
798
906
  console.log(JSON.stringify(output, null, 2));
799
907
  break;
800
908
  }
@@ -881,10 +989,11 @@ async function main() {
881
989
  execCmd: validated.exec,
882
990
  execFile: validated.exec_file,
883
991
  passEnv: validated.pass_env,
992
+ delivery: validated.delivery,
884
993
  };
885
994
  engine = new FerryCatEngine();
886
995
  if (effectiveOnce) {
887
- const pollRes = engine.pollNextTask({ agent_id: resolvedAgent });
996
+ const pollRes = await engine.pollNextTask({ agent_id: resolvedAgent });
888
997
  if (!pollRes.has_task || !pollRes.contract) {
889
998
  // Preserve engine's specific poll status/message/stale_error (fixes G8)
890
999
  output = {
@@ -924,6 +1033,7 @@ async function main() {
924
1033
  const configPath = data.config;
925
1034
  const targetAgent = data.target_agent ?? data.agent_id;
926
1035
  const once = Boolean(data.once);
1036
+ const wantStatus = Boolean(data.status);
927
1037
  const leaseTtl = data.lease_ttl_sec ? Number(data.lease_ttl_sec) : undefined;
928
1038
  const intervalMs = data.interval_ms ? Number(data.interval_ms) : undefined;
929
1039
  const notify = data.notify !== false;
@@ -934,11 +1044,18 @@ async function main() {
934
1044
  pollIntervalMs: intervalMs,
935
1045
  notify,
936
1046
  });
937
- if (once) {
1047
+ if (wantStatus) {
1048
+ // R7: read-only inspection of lease ownership + write-ahead journal health.
1049
+ const status = daemon.getStatus();
1050
+ daemon.engine.close();
1051
+ console.log(JSON.stringify({ ok: true, daemon_status: status }, null, 2));
1052
+ }
1053
+ else if (once) {
938
1054
  const res = await daemon.pollAndDispatchOnce();
1055
+ const status = daemon.getStatus();
939
1056
  daemon.releaseLease();
940
1057
  daemon.engine.close();
941
- output = { ok: true, daemon_once: res };
1058
+ output = { ok: true, daemon_once: res, journal_degraded: status.journal_degraded };
942
1059
  console.log(JSON.stringify(output, null, 2));
943
1060
  }
944
1061
  else {
@@ -948,6 +1065,32 @@ async function main() {
948
1065
  }
949
1066
  break;
950
1067
  }
1068
+ case 'init': {
1069
+ const serverName = data.server_name;
1070
+ if (serverName !== undefined && !/^[A-Za-z0-9_.-]{1,64}$/.test(serverName)) {
1071
+ throw new Error(`Invalid --server-name "${serverName}": use 1-64 chars of [A-Za-z0-9_.-].`);
1072
+ }
1073
+ const initResult = await runInit({
1074
+ clients: data.clients,
1075
+ agent: data.agent_id,
1076
+ serverName,
1077
+ configPath: data.config_path,
1078
+ local: Boolean(data.local),
1079
+ yes: Boolean(data.yes),
1080
+ dryRun: Boolean(data.dry_run),
1081
+ force: Boolean(data.force),
1082
+ list: Boolean(data.list),
1083
+ json: parsed.format === 'json',
1084
+ interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY),
1085
+ });
1086
+ if (parsed.format === 'json') {
1087
+ console.log(JSON.stringify(initResult, null, 2));
1088
+ }
1089
+ if (!initResult.ok) {
1090
+ process.exitCode = 1;
1091
+ }
1092
+ break;
1093
+ }
951
1094
  case 'demo': {
952
1095
  const demoResult = await runDemo({
953
1096
  json: parsed.format === 'json',
@@ -963,7 +1106,7 @@ async function main() {
963
1106
  break;
964
1107
  }
965
1108
  default:
966
- throw new Error(`Unknown subcommand "${command}". Supported commands: dispatch, poll, verify, fulfill, release, extend, fail, inspect, worker, daemon, demo. Run "ferrycat-cli --help" for usage.`);
1109
+ throw new Error(`Unknown subcommand "${command}". Supported commands: dispatch, poll, verify, fulfill, release, extend, fail, inspect, worker, daemon, demo, init. Run "ferrycat-cli --help" for usage.`);
967
1110
  }
968
1111
  }
969
1112
  catch (err) {