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 +57 -10
- package/README_zh.md +54 -10
- package/bin/ferrycat.js +1 -0
- package/dist/cli.js +157 -14
- package/dist/cli.js.map +1 -1
- package/dist/daemon.d.ts +137 -1
- package/dist/daemon.js +673 -10
- package/dist/daemon.js.map +1 -1
- package/dist/delivery.d.ts +48 -0
- package/dist/delivery.js +158 -0
- package/dist/delivery.js.map +1 -0
- package/dist/demo.js +10 -7
- package/dist/demo.js.map +1 -1
- package/dist/engine.d.ts +15 -1
- package/dist/engine.js +287 -29
- package/dist/engine.js.map +1 -1
- package/dist/fingerprint.d.ts +60 -4
- package/dist/fingerprint.js +438 -9
- package/dist/fingerprint.js.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts +62 -0
- package/dist/init.js +356 -0
- package/dist/init.js.map +1 -0
- package/dist/schemas.d.ts +64 -0
- package/dist/schemas.js +30 -0
- package/dist/schemas.js.map +1 -1
- package/dist/supervisor.d.ts +10 -0
- package/dist/supervisor.js +34 -26
- package/dist/supervisor.js.map +1 -1
- package/dist/types.d.ts +20 -0
- package/package.json +16 -4
- package/server.json +3 -3
package/README.md
CHANGED
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
[](https://glama.ai/mcp/servers/GuoBug/FerryCat)
|
|
14
14
|
[](LICENSE)
|
|
15
15
|
|
|
16
|
-
> **FerryCat** (`ferrycat`) is a lightweight, deterministic cross-host AI agent handoff
|
|
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.
|
|
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
|
[](https://glama.ai/mcp/servers/GuoBug/FerryCat)
|
|
14
14
|
[](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
|
|
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
|
-
|
|
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
|
|
300
|
-
| **`poll_next_task`** | 接收方 (Worker) | 先读后写清理超时工单 $\to$ `changes === 1` 原子抢占为 `CLAIMED` $\to$
|
|
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) |
|
|
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
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>
|
|
67
|
-
--file <path
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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) {
|