refactor: rename project surface to mergegate

This commit is contained in:
林 駿甫 (Shunsuke Hayashi) 2026-04-10 14:50:57 +09:00
parent 3f1453b23c
commit 29c72fee83
111 changed files with 1586 additions and 873 deletions

View file

@ -5,7 +5,7 @@ GITHUB_TOKEN=ghp_your_github_token_here
# Repository Information # Repository Information
# Format: owner/repo (e.g., YourUsername/my-project) # Format: owner/repo (e.g., YourUsername/my-project)
REPOSITORY=ShunsukeHayashi/miyabi-cli-standalone REPOSITORY=ShunsukeHayashi/mergegate
# Anthropic API Key (optional for local development) # Anthropic API Key (optional for local development)
# Get key at: https://console.anthropic.com/ # Get key at: https://console.anthropic.com/

3
.gitignore vendored
View file

@ -176,3 +176,6 @@ yarn-error.log*
.ai/ .ai/
.env .env
.gitnexus .gitnexus
project_memory/task-events.jsonl
project_memory/tasks.snapshot.json
project_memory/.tasks.lock

View file

@ -1,7 +1,7 @@
# Repository Guidelines # Repository Guidelines
## Project Structure & Module Organization ## Project Structure & Module Organization
- Rust workspace lives under `crates/`: `miyabi-cli` (CLI entry), `miyabi-tui` (UI), and `miyabi-core` (shared logic, config, sessions, Polaris/DTP: `gate`, `store`, `lock`, `protocol`). Shared workspace config in `Cargo.toml`. - Rust workspace lives under `crates/`: `mergegate-cli` (CLI entry), `mergegate-tui` (UI), and `mergegate-core` (shared logic, config, sessions, MergeGate/DTP: `gate`, `store`, `lock`, `protocol`). Shared workspace config in `Cargo.toml`.
- ルートに `package.json` はない(過去テンプレート由来の記述は削除済み)。フロント系ツールが別ディレクトリにあれば、その README に従う。 - ルートに `package.json` はない(過去テンプレート由来の記述は削除済み)。フロント系ツールが別ディレクトリにあれば、その README に従う。
- Supporting assets: `.claude/` agent configs, `docs/` for design notes, `project_memory/` for Polaris タスク台帳(`tasks.json`)、`.github/` for CI workflows。 - Supporting assets: `.claude/` agent configs, `docs/` for design notes, `project_memory/` for Polaris タスク台帳(`tasks.json`)、`.github/` for CI workflows。

View file

@ -181,7 +181,7 @@ miyabi gate manual-complete <task-id> --reason "理由" --operator "名前"
<!-- gitnexus:start --> <!-- gitnexus:start -->
# GitNexus — Code Intelligence # GitNexus — Code Intelligence
This project is indexed by GitNexus as **miyabi-cli-standalone** (5691 symbols, 12441 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. This project is indexed by GitNexus as **miyabi-cli-standalone** (5866 symbols, 12893 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first. > If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.

575
Cargo.lock generated
View file

@ -146,6 +146,21 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "bit-set"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
dependencies = [
"bit-vec",
]
[[package]]
name = "bit-vec"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.10.0" version = "2.10.0"
@ -209,6 +224,12 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.42" version = "0.4.42"
@ -301,16 +322,6 @@ dependencies = [
"unicode-segmentation", "unicode-segmentation",
] ]
[[package]]
name = "core-foundation"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]] [[package]]
name = "core-foundation-sys" name = "core-foundation-sys"
version = "0.8.7" version = "0.8.7"
@ -498,15 +509,6 @@ version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "encoding_rs"
version = "0.8.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
dependencies = [
"cfg-if",
]
[[package]] [[package]]
name = "equivalent" name = "equivalent"
version = "1.0.2" version = "1.0.2"
@ -592,21 +594,6 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared",
]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]] [[package]]
name = "form_urlencoded" name = "form_urlencoded"
version = "1.2.2" version = "1.2.2"
@ -741,8 +728,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"js-sys",
"libc", "libc",
"wasi", "wasi",
"wasm-bindgen",
] ]
[[package]] [[package]]
@ -752,9 +741,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"js-sys",
"libc", "libc",
"r-efi", "r-efi",
"wasip2", "wasip2",
"wasm-bindgen",
] ]
[[package]] [[package]]
@ -778,25 +769,6 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
[[package]]
name = "h2"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386"
dependencies = [
"atomic-waker",
"bytes",
"fnv",
"futures-core",
"futures-sink",
"http",
"indexmap",
"slab",
"tokio",
"tokio-util",
"tracing",
]
[[package]] [[package]]
name = "half" name = "half"
version = "2.7.1" version = "2.7.1"
@ -881,7 +853,6 @@ dependencies = [
"bytes", "bytes",
"futures-channel", "futures-channel",
"futures-core", "futures-core",
"h2",
"http", "http",
"http-body", "http-body",
"httparse", "httparse",
@ -907,22 +878,7 @@ dependencies = [
"tokio", "tokio",
"tokio-rustls", "tokio-rustls",
"tower-service", "tower-service",
] "webpki-roots",
[[package]]
name = "hyper-tls"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
dependencies = [
"bytes",
"http-body-util",
"hyper",
"hyper-util",
"native-tls",
"tokio",
"tokio-native-tls",
"tower-service",
] ]
[[package]] [[package]]
@ -944,11 +900,9 @@ dependencies = [
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"socket2", "socket2",
"system-configuration",
"tokio", "tokio",
"tower-service", "tower-service",
"tracing", "tracing",
"windows-registry",
] ]
[[package]] [[package]]
@ -1302,6 +1256,12 @@ dependencies = [
"hashbrown 0.15.5", "hashbrown 0.15.5",
] ]
[[package]]
name = "lru-slab"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
[[package]] [[package]]
name = "matchers" name = "matchers"
version = "0.2.0" version = "0.2.0"
@ -1318,10 +1278,72 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
[[package]] [[package]]
name = "mime" name = "mergegate-cli"
version = "0.3.17" version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" "anyhow",
"chrono",
"clap",
"crossterm 0.29.0",
"mergegate-core",
"mergegate-tui",
"ratatui",
"serde",
"serde_json",
"tokio",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "mergegate-core"
version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
"chrono",
"dirs",
"fs2",
"futures",
"git2",
"glob",
"proptest",
"regex",
"reqwest",
"serde",
"serde_json",
"serde_yaml",
"tempfile",
"thiserror 2.0.17",
"tokio",
"toml",
"tracing",
"tracing-subscriber",
"uuid",
]
[[package]]
name = "mergegate-tui"
version = "0.1.0"
dependencies = [
"anyhow",
"arboard",
"chrono",
"crossterm 0.29.0",
"futures",
"mergegate-core",
"once_cell",
"pulldown-cmark",
"ratatui",
"serde",
"serde_json",
"syntect",
"textwrap",
"thiserror 2.0.17",
"tokio",
"unicode-width 0.2.0",
"uuid",
]
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
@ -1345,72 +1367,6 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "miyabi-cli"
version = "0.1.0"
dependencies = [
"anyhow",
"chrono",
"clap",
"crossterm 0.29.0",
"miyabi-core",
"miyabi-tui",
"ratatui",
"serde_json",
"tokio",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "miyabi-core"
version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
"chrono",
"dirs",
"fs2",
"futures",
"git2",
"glob",
"regex",
"reqwest",
"serde",
"serde_json",
"serde_yaml",
"tempfile",
"thiserror 2.0.17",
"tokio",
"toml",
"tracing",
"tracing-subscriber",
"uuid",
]
[[package]]
name = "miyabi-tui"
version = "0.1.0"
dependencies = [
"anyhow",
"arboard",
"chrono",
"crossterm 0.29.0",
"futures",
"miyabi-core",
"once_cell",
"pulldown-cmark",
"ratatui",
"serde",
"serde_json",
"syntect",
"textwrap",
"thiserror 2.0.17",
"tokio",
"unicode-width 0.2.0",
"uuid",
]
[[package]] [[package]]
name = "moxcms" name = "moxcms"
version = "0.7.9" version = "0.7.9"
@ -1421,23 +1377,6 @@ dependencies = [
"pxfm", "pxfm",
] ]
[[package]]
name = "native-tls"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
dependencies = [
"libc",
"log",
"openssl",
"openssl-probe",
"openssl-sys",
"schannel",
"security-framework",
"security-framework-sys",
"tempfile",
]
[[package]] [[package]]
name = "nu-ansi-term" name = "nu-ansi-term"
version = "0.50.3" version = "0.50.3"
@ -1569,38 +1508,21 @@ dependencies = [
"pkg-config", "pkg-config",
] ]
[[package]]
name = "openssl"
version = "0.10.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328"
dependencies = [
"bitflags",
"cfg-if",
"foreign-types",
"libc",
"once_cell",
"openssl-macros",
"openssl-sys",
]
[[package]]
name = "openssl-macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "openssl-probe" name = "openssl-probe"
version = "0.1.6" version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
[[package]]
name = "openssl-src"
version = "300.5.4+3.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a507b3792995dae9b0df8a1c1e3771e8418b7c2d9f0baeba32e6fe8b06c7cb72"
dependencies = [
"cc",
]
[[package]] [[package]]
name = "openssl-sys" name = "openssl-sys"
version = "0.9.111" version = "0.9.111"
@ -1609,6 +1531,7 @@ checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321"
dependencies = [ dependencies = [
"cc", "cc",
"libc", "libc",
"openssl-src",
"pkg-config", "pkg-config",
"vcpkg", "vcpkg",
] ]
@ -1713,6 +1636,15 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy",
]
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.103" version = "1.0.103"
@ -1722,6 +1654,25 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "proptest"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744"
dependencies = [
"bit-set",
"bit-vec",
"bitflags",
"num-traits",
"rand",
"rand_chacha",
"rand_xorshift",
"regex-syntax",
"rusty-fork",
"tempfile",
"unarray",
]
[[package]] [[package]]
name = "pulldown-cmark" name = "pulldown-cmark"
version = "0.12.2" version = "0.12.2"
@ -1750,6 +1701,12 @@ dependencies = [
"num-traits", "num-traits",
] ]
[[package]]
name = "quick-error"
version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
[[package]] [[package]]
name = "quick-error" name = "quick-error"
version = "2.0.1" version = "2.0.1"
@ -1765,6 +1722,61 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "quinn"
version = "0.11.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
dependencies = [
"bytes",
"cfg_aliases",
"pin-project-lite",
"quinn-proto",
"quinn-udp",
"rustc-hash",
"rustls",
"socket2",
"thiserror 2.0.17",
"tokio",
"tracing",
"web-time",
]
[[package]]
name = "quinn-proto"
version = "0.11.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31"
dependencies = [
"bytes",
"getrandom 0.3.4",
"lru-slab",
"rand",
"ring",
"rustc-hash",
"rustls",
"rustls-pki-types",
"slab",
"thiserror 2.0.17",
"tinyvec",
"tracing",
"web-time",
]
[[package]]
name = "quinn-udp"
version = "0.5.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
dependencies = [
"cfg_aliases",
"libc",
"once_cell",
"socket2",
"tracing",
"windows-sys 0.60.2",
]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.42" version = "1.0.42"
@ -1780,6 +1792,44 @@ version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "rand"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
dependencies = [
"getrandom 0.3.4",
]
[[package]]
name = "rand_xorshift"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a"
dependencies = [
"rand_core",
]
[[package]] [[package]]
name = "ratatui" name = "ratatui"
version = "0.29.0" version = "0.29.0"
@ -1858,30 +1908,27 @@ checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f"
dependencies = [ dependencies = [
"base64", "base64",
"bytes", "bytes",
"encoding_rs",
"futures-core", "futures-core",
"futures-util", "futures-util",
"h2",
"http", "http",
"http-body", "http-body",
"http-body-util", "http-body-util",
"hyper", "hyper",
"hyper-rustls", "hyper-rustls",
"hyper-tls",
"hyper-util", "hyper-util",
"js-sys", "js-sys",
"log", "log",
"mime",
"native-tls",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"quinn",
"rustls",
"rustls-pki-types", "rustls-pki-types",
"serde", "serde",
"serde_json", "serde_json",
"serde_urlencoded", "serde_urlencoded",
"sync_wrapper", "sync_wrapper",
"tokio", "tokio",
"tokio-native-tls", "tokio-rustls",
"tokio-util", "tokio-util",
"tower", "tower",
"tower-http", "tower-http",
@ -1891,6 +1938,7 @@ dependencies = [
"wasm-bindgen-futures", "wasm-bindgen-futures",
"wasm-streams", "wasm-streams",
"web-sys", "web-sys",
"webpki-roots",
] ]
[[package]] [[package]]
@ -1907,6 +1955,12 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "rustc-hash"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "0.38.44" version = "0.38.44"
@ -1940,6 +1994,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"ring",
"rustls-pki-types", "rustls-pki-types",
"rustls-webpki", "rustls-webpki",
"subtle", "subtle",
@ -1952,6 +2007,7 @@ version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a"
dependencies = [ dependencies = [
"web-time",
"zeroize", "zeroize",
] ]
@ -1972,6 +2028,18 @@ version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "rusty-fork"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2"
dependencies = [
"fnv",
"quick-error 1.2.3",
"tempfile",
"wait-timeout",
]
[[package]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.20" version = "1.0.20"
@ -1987,44 +2055,12 @@ dependencies = [
"winapi-util", "winapi-util",
] ]
[[package]]
name = "schannel"
version = "0.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1"
dependencies = [
"windows-sys 0.61.2",
]
[[package]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.2.0" version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "security-framework"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
dependencies = [
"bitflags",
"core-foundation",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.228" version = "1.0.228"
@ -2279,27 +2315,6 @@ dependencies = [
"yaml-rust", "yaml-rust",
] ]
[[package]]
name = "system-configuration"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
dependencies = [
"bitflags",
"core-foundation",
"system-configuration-sys",
]
[[package]]
name = "system-configuration-sys"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.23.0" version = "3.23.0"
@ -2382,7 +2397,7 @@ dependencies = [
"fax", "fax",
"flate2", "flate2",
"half", "half",
"quick-error", "quick-error 2.0.1",
"weezl", "weezl",
"zune-jpeg", "zune-jpeg",
] ]
@ -2428,6 +2443,21 @@ dependencies = [
"zerovec", "zerovec",
] ]
[[package]]
name = "tinyvec"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.48.0" version = "1.48.0"
@ -2455,16 +2485,6 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "tokio-native-tls"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
dependencies = [
"native-tls",
"tokio",
]
[[package]] [[package]]
name = "tokio-rustls" name = "tokio-rustls"
version = "0.26.4" version = "0.26.4"
@ -2654,6 +2674,12 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "unarray"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94"
[[package]] [[package]]
name = "unicase" name = "unicase"
version = "2.8.1" version = "2.8.1"
@ -2761,6 +2787,15 @@ version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "wait-timeout"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "walkdir" name = "walkdir"
version = "2.5.0" version = "2.5.0"
@ -2876,6 +2911,25 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "web-time"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "webpki-roots"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed"
dependencies = [
"rustls-pki-types",
]
[[package]] [[package]]
name = "weezl" name = "weezl"
version = "0.1.12" version = "0.1.12"
@ -2954,17 +3008,6 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-registry"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
dependencies = [
"windows-link",
"windows-result",
"windows-strings",
]
[[package]] [[package]]
name = "windows-result" name = "windows-result"
version = "0.4.1" version = "0.4.1"

View file

@ -1,9 +1,9 @@
[workspace] [workspace]
resolver = "2" resolver = "2"
members = [ members = [
"crates/miyabi-cli", "crates/mergegate-cli",
"crates/miyabi-tui", "crates/mergegate-tui",
"crates/miyabi-core", "crates/mergegate-core",
] ]
[workspace.package] [workspace.package]
@ -12,9 +12,9 @@ edition = "2021"
rust-version = "1.75" rust-version = "1.75"
authors = ["Miyabi Team"] authors = ["Miyabi Team"]
license = "BUSL-1.1" license = "BUSL-1.1"
repository = "https://github.com/ShunsukeHayashi/miyabi-cli-standalone" repository = "https://github.com/ShunsukeHayashi/mergegate"
homepage = "https://github.com/ShunsukeHayashi/miyabi-cli-standalone" homepage = "https://github.com/ShunsukeHayashi/mergegate"
description = "Miyabi - Autonomous AI Development Framework" description = "MergeGate - Deterministic task execution and merge workflow for AI-assisted development"
keywords = ["ai", "cli", "tui", "autonomous", "development"] keywords = ["ai", "cli", "tui", "autonomous", "development"]
categories = ["command-line-utilities", "development-tools"] categories = ["command-line-utilities", "development-tools"]

View file

@ -282,7 +282,7 @@ cargo clippy --all-targets -- -D warnings
cargo fmt --all --check cargo fmt --all --check
# 実行 # 実行
cargo run -p miyabi-cli cargo run -p mergegate-cli --bin mergegate
``` ```
## ライセンス ## ライセンス

View file

@ -1,2 +1,13 @@
# miyabi-cli-standalone # MergeGate
Autonomous development powered by Agentic OS Deterministic execution protocol for AI-assisted development.
Execution layer for GitNexus-style code intelligence:
- `GitNexus`: understand the codebase
- `MergeGate`: execute changes safely
Current CLI entrypoints: `miyabi` and `mergegate`
- `miyabi tui` / `mergegate tui`: terminal assistant
- `miyabi gate` / `mergegate gate`: deterministic task execution, file locks, and PR/merge workflow
Start with `cargo build --release`, then run `./target/release/mergegate --help` or `./target/release/mergegate gate guide`.

134
README.md
View file

@ -1,34 +1,62 @@
# Miyabi CLI # MergeGate
A powerful terminal-based AI assistant with TUI (Terminal User Interface) built in Rust. MergeGate is a deterministic execution protocol for AI-assisted development.
It is the execution layer for GitNexus-style code intelligence: GitNexus helps agents understand what will break, and MergeGate makes them execute that work in a safe, verifiable order.
In short:
- `GitNexus`: understand the codebase
- `MergeGate`: execute changes safely
The project currently supports both `miyabi` and `mergegate` as CLI entrypoints. `miyabi` remains the legacy/default command, and `mergegate` is the product-aligned alias.
This repository has two common entry points:
- `miyabi tui` / `mergegate tui`: interactive terminal assistant
- `miyabi gate` / `mergegate gate`: deterministic task execution and file-lock workflow for repo work
If you arrived here because of deterministic execution, task locks, PR gates, or agent-safe repo work, start with `mergegate gate` or `miyabi gate`, not the TUI.
## 60-Second Setup ## 60-Second Setup
```bash ```bash
# 1. Clone and build # 1. Clone and build
git clone https://github.com/ShunsukeHayashi/miyabi-cli-standalone.git git clone https://github.com/ShunsukeHayashi/mergegate.git
cd miyabi-cli-standalone cd mergegate
cargo build --release cargo build --release
# 2. Set API key # 2. Set API key
export ANTHROPIC_API_KEY="sk-ant-..." export ANTHROPIC_API_KEY="sk-ant-..."
# 3. Run # 3. Run
./target/release/miyabi tui ./target/release/mergegate tui
``` ```
That's it! Start chatting with Claude in your terminal. That's it. Start with the TUI, or jump straight into `mergegate gate` for repo workflow control.
--- ---
## Features ## Why MergeGate
- **Interactive TUI** - Beautiful terminal interface with markdown rendering - **Execution layer for code intelligence** - Pair blast-radius understanding with deterministic execution, not chat-only automation
- **Chat Mode** - Conversational AI assistant - **Deterministic task execution** - Register, analyze, lock, review, and merge in a verifiable order
- **Agent Mode** - Autonomous task execution with tool approval - **Repo-safe agent workflow** - File locks and gate checks reduce accidental overlap and unsafe edits
- **Session Management** - Persist and resume conversations - **Interactive TUI** - Terminal assistant for chat, prompts, and agent execution
- **Agent mode** - Autonomous execution with tool approval
- **Session management** - Persist and resume conversations
- **Configurable** - TOML-based configuration - **Configurable** - TOML-based configuration
- **Extended Thinking** - Support for Claude 4.5+ extended thinking
## Positioning
MergeGate was designed for teams that already believe code understanding is not enough.
Knowing the blast radius of a change is only half of the problem. Agents also need a protocol for when work starts, which files are locked, what must be recorded before editing, and what counts as done.
That is the role split:
- `GitNexus`: query, context, impact, detect-changes, rename, wiki, graph exploration
- `MergeGate`: register, impact record, assign, file lock, branch, PR, merge, completion discipline
If GitNexus answers "what is this change connected to?", MergeGate answers "how do we carry it through safely?"
## Installation ## Installation
@ -40,16 +68,22 @@ That's it! Start chatting with Claude in your terminal.
### Build from Source ### Build from Source
```bash ```bash
git clone https://github.com/ShunsukeHayashi/miyabi-cli-standalone.git git clone https://github.com/ShunsukeHayashi/mergegate.git
cd miyabi-cli-standalone cd mergegate
cargo build --release cargo build --release
``` ```
Binary will be at `target/release/miyabi`. Binary will be at `target/release/miyabi` and `target/release/mergegate`.
## Quick Start ## Quick Start
### 1. Generate Config Choose the path that matches what you want to do.
### A. Use MergeGate as a terminal assistant
Run the TUI after configuring your API key.
#### 1. Generate Config
```bash ```bash
./target/release/miyabi init ./target/release/miyabi init
@ -57,7 +91,7 @@ Binary will be at `target/release/miyabi`.
This creates `~/.miyabi/config.toml`. This creates `~/.miyabi/config.toml`.
### 2. Set API Key #### 2. Set API Key
Edit `~/.miyabi/config.toml`: Edit `~/.miyabi/config.toml`:
@ -72,7 +106,7 @@ Or use environment variable:
export ANTHROPIC_API_KEY="sk-ant-..." export ANTHROPIC_API_KEY="sk-ant-..."
``` ```
### 3. Launch TUI #### 3. Launch TUI
```bash ```bash
./target/release/miyabi tui ./target/release/miyabi tui
@ -80,8 +114,55 @@ export ANTHROPIC_API_KEY="sk-ant-..."
./target/release/miyabi ./target/release/miyabi
``` ```
### B. Use MergeGate as a task execution gate
If you are working inside a repository and want task registration, impact tracking, file locks, and PR/merge recording, use `mergegate gate` or `miyabi gate`.
#### 1. Check whether the repo is already initialized
```bash
./target/release/miyabi gate status
```
If you see `tasks: 0`, that means the ledger exists but is currently empty.
#### 2. Initialize MergeGate for this repo if needed
```bash
./target/release/miyabi gate init
```
This creates `project_memory/tasks.json` and prepares the repo for gate-managed work.
#### 3. Read the built-in workflow guide
```bash
./target/release/miyabi gate guide
```
#### 4. Start a task
```bash
./target/release/miyabi gate register --issue 123 --title "Fix login redirect"
./target/release/miyabi gate impact issue-123 --risk medium --symbols 3
./target/release/miyabi gate assign issue-123 --agent codex --node macbook --files "src/auth.rs"
```
Typical flow:
1. `register`
2. `impact`
3. `assign`
4. implement
5. `branch`
6. `pr`
7. `merge` or `manual-complete`
## Usage ## Usage
`MergeGate` is the product and documentation name.
Both `miyabi` and `mergegate` work today.
### CLI Commands ### CLI Commands
```bash ```bash
@ -94,6 +175,11 @@ miyabi sessions -d <id> # Delete session
miyabi sessions -e <id> # Export session to JSON miyabi sessions -e <id> # Export session to JSON
miyabi version # Show version info miyabi version # Show version info
miyabi agent <prompt> # Run autonomous agent miyabi agent <prompt> # Run autonomous agent
miyabi gate status # Show task ledger status
miyabi gate init # Initialize MergeGate in the current repo
miyabi gate guide # Show the full gate workflow guide
mergegate gate status # Same gate workflow with product-aligned command
mergegate tui # Same TUI with product-aligned command
``` ```
### CLI Options ### CLI Options
@ -187,7 +273,7 @@ Agent mode features:
## Core Library Features ## Core Library Features
The `miyabi-core` crate provides powerful utilities for building AI applications. The `mergegate-core` crate provides powerful utilities for building AI applications.
### Git Utilities ### Git Utilities
@ -373,10 +459,10 @@ flags.load_from_map(config);
## Project Structure ## Project Structure
``` ```
miyabi-cli-standalone/ mergegate/
├── crates/ ├── crates/
│ ├── miyabi-cli/ # CLI entry point │ ├── mergegate-cli/ # CLI entry point
│ ├── miyabi-core/ # Core library │ ├── mergegate-core/ # Core library
│ │ ├── agent.rs # Agent system │ │ ├── agent.rs # Agent system
│ │ ├── anthropic.rs # Anthropic API client │ │ ├── anthropic.rs # Anthropic API client
│ │ ├── cache.rs # TTL cache system │ │ ├── cache.rs # TTL cache system
@ -391,7 +477,7 @@ miyabi-cli-standalone/
│ │ ├── session.rs # Session management │ │ ├── session.rs # Session management
│ │ ├── tools.rs # Built-in tools │ │ ├── tools.rs # Built-in tools
│ │ └── ... │ │ └── ...
│ └── miyabi-tui/ # TUI implementation │ └── mergegate-tui/ # TUI implementation
│ ├── app.rs # Main application │ ├── app.rs # Main application
│ ├── views.rs # UI views │ ├── views.rs # UI views
│ └── ... │ └── ...
@ -482,7 +568,7 @@ RUST_LOG=debug ./target/release/miyabi tui
- Check `miyabi --help` for CLI options - Check `miyabi --help` for CLI options
- Press `F1` in TUI for keybindings - Press `F1` in TUI for keybindings
- Open an issue: https://github.com/ShunsukeHayashi/miyabi-cli-standalone/issues - Open an issue: https://github.com/ShunsukeHayashi/mergegate/issues
## License ## License

View file

@ -4,7 +4,7 @@
## 1.1 verify_merge ラッパー (~50行) ## 1.1 verify_merge ラッパー (~50行)
- [ ] `crates/miyabi-core/src/protocol.rs``verify_merge()` を追加 - [x] `crates/miyabi-core/src/protocol.rs``verify_merge()` を追加
- 既存の `get_pull_request()` (github.rs) を呼ぶ - 既存の `get_pull_request()` (github.rs) を呼ぶ
- `PullRequest.merge_commit_sha` を取得 - `PullRequest.merge_commit_sha` を取得
- `pr.state == "merged"` を検証 - `pr.state == "merged"` を検証
@ -12,59 +12,53 @@
- tasks.json 更新: merge_commit + state → done - tasks.json 更新: merge_commit + state → done
- ロック解放 - ロック解放
- 後続タスク blocked → pending - 後続タスク blocked → pending
- [ ] CLI に `miyabi gate verify-merge <task-id>` サブコマンド追加 - [x] CLI に `miyabi gate verify-merge <task-id>` サブコマンド追加
- [ ] テスト: mock PullRequest で merged → done 遷移 - [x] テスト: mock PullRequest で merged → done 遷移
## 1.2 escape hatch (~50行) ## 1.2 escape hatch (~50行)
- [ ] `protocol.rs``force_unlock()` 追加 - [x] `protocol.rs``force_unlock()` 追加
``` ```
fn force_unlock(&self, task_id: &str, reason: &str, operator: &str) -> Result<()> fn force_unlock(&self, task_id: &str, reason: &str, operator: &str) -> Result<()>
``` ```
- ロック即解放 - ロック即解放
- event log に reason + operator を記録 - event log に reason + operator を記録
- state は変更しないimplementing のまま) - state は変更しないimplementing のまま)
- [ ] `protocol.rs``manual_complete()` 追加 - [x] `protocol.rs``manual_complete()` 追加
``` ```
fn manual_complete(&self, task_id: &str, reason: &str, operator: &str) -> Result<()> fn manual_complete(&self, task_id: &str, reason: &str, operator: &str) -> Result<()>
``` ```
- PR/merge なしで done に遷移 - PR/merge なしで done に遷移
- event log に reason + operator + "manual" を記録 - event log に reason + operator + "manual" を記録
- CompletionMode::Manual として区別 - `CompletionMode::Manual` として区別2026-04-10 実装反映)
- [ ] CLI に `miyabi gate force-unlock <task-id> --reason R --operator O` 追加 - [x] CLI に `miyabi gate force-unlock <task-id> --reason R --operator O` 追加
- [ ] CLI に `miyabi gate manual-complete <task-id> --reason R --operator O` 追加 - [x] CLI に `miyabi gate manual-complete <task-id> --reason R --operator O` 追加
- [ ] テスト: force_unlock → ロック解放確認 - [x] テスト: force_unlock → ロック解放確認
- [ ] テスト: manual_complete → done 遷移 + event 記録 - [x] テスト: manual_complete → done 遷移 + event 記録
## 1.3 E2E テスト (~100行) ## 1.3 E2E テスト (~100行)
- [ ] `crates/miyabi-core/src/protocol.rs` の tests モジュールに E2E 追加 - [x] `crates/miyabi-core/src/protocol.rs` の tests モジュールに E2E 追加`full_lifecycle_register_to_merged_releases_lock` — マージ完了時は `TaskState::Merged`
``` ```
#[test] #[test]
fn full_lifecycle_register_to_done() { fn full_lifecycle_register_to_merged_releases_lock() {
// 1. register(issue=1, title="test") // register → impact → assign → branch → pr → merge(40hex)
// 2. check_dependencies → ready // assert: Merged, lock == None
// 3. record_impact(risk=LOW)
// 4. assign_and_lock(agent="test", files=["src/test.rs"])
// 5. record_branch("feature/issue-1-test")
// 6. record_pr(42)
// 7. record_merge("a1b2c3d4...40hex")
// 8. assert: state == Done, lock == None
} }
``` ```
- [ ] GATE 拒否テスト: issue=0 → GateError - [x] GATE 拒否テスト: issue=0 → GateError既存テスト群でカバー
- [ ] GATE 拒否テスト: impact なしで assign → GateError - [x] GATE 拒否テスト: impact なしで assign → GateError既存
- [ ] GATE 拒否テスト: HIGH risk + 承認なし → GateError - [x] GATE 拒否テスト: HIGH risk + 承認なし → GateError既存
- [ ] GATE 拒否テスト: ロック競合 → LockError - [x] GATE 拒否テスト: ロック競合 → LockError既存
- [ ] GATE 拒否テスト: 不正 SHA → GateError - [x] GATE 拒否テスト: 不正 SHA → GateError既存
- [ ] escape hatch テスト: force_unlock + manual_complete - [x] escape hatch テスト: force_unlock + manual_complete
## 1.4 最終確認 ## 1.4 最終確認
- [ ] `cargo test --all` → 全 GREEN - [x] `cargo test --all` → 全 GREEN
- [ ] `cargo clippy --all-targets --all-features -- -D warnings` → ゼロエラー - [x] `cargo clippy --all-targets --all-features -- -D warnings` → ゼロエラー
- [ ] `cargo build --release` → リリースビルド成功 - [x] `cargo build --release` → リリースビルド成功
- [ ] `npx gitnexus analyze --force` → 再インデックス - [ ] `npx gitnexus analyze --force` → 再インデックス(必要時に手動)
- [ ] `git tag v1.0-dtp-complete` - [ ] `git tag v1.0-dtp-complete`(リリース判断後)
- [ ] `git push origin v1.0-dtp-complete` - [ ] `git push origin v1.0-dtp-complete`
- [ ] `~/bin/announce "Polaris v1.0 完成。全 GATE 実装済み。テスト GREEN。"` - [ ] `~/bin/announce "Polaris v1.0 完成。全 GATE 実装済み。テスト GREEN。"`

View file

@ -1,5 +1,5 @@
[package] [package]
name = "miyabi-cli" name = "mergegate-cli"
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
@ -7,16 +7,20 @@ authors.workspace = true
license.workspace = true license.workspace = true
repository.workspace = true repository.workspace = true
homepage.workspace = true homepage.workspace = true
description = "Miyabi CLI - Main command-line interface" description = "MergeGate CLI - Main command-line interface for deterministic task execution and agent-assisted development"
[[bin]] [[bin]]
name = "miyabi" name = "miyabi"
path = "src/main.rs" path = "src/main.rs"
[[bin]]
name = "mergegate"
path = "src/bin/mergegate.rs"
[dependencies] [dependencies]
# Workspace crates # Workspace crates
miyabi-core = { path = "../miyabi-core" } miyabi-core = { package = "mergegate-core", path = "../mergegate-core" }
miyabi-tui = { path = "../miyabi-tui" } miyabi-tui = { package = "mergegate-tui", path = "../mergegate-tui" }
# CLI # CLI
clap = { workspace = true } clap = { workspace = true }
@ -32,6 +36,7 @@ tokio = { workspace = true }
anyhow = { workspace = true } anyhow = { workspace = true }
# Serialization # Serialization
serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
# Utilities # Utilities

View file

@ -0,0 +1 @@
include!("../main.rs");

View file

@ -1,8 +1,9 @@
//! Miyabi CLI - Main entry point /// MergeGate CLI - Main entry point
use chrono::Duration as ChronoDuration; use chrono::Duration as ChronoDuration;
use clap::{Parser, Subcommand, ValueEnum}; use clap::{CommandFactory, FromArgMatches, Parser, Subcommand, ValueEnum};
use miyabi_core::{FeatureFlagManager, RulesLoader}; use miyabi_core::{FeatureFlagManager, RulesLoader};
use serde::Serialize;
use std::collections::HashMap; use std::collections::HashMap;
use std::fs; use std::fs;
use std::io::{self, BufRead, BufReader, Write}; use std::io::{self, BufRead, BufReader, Write};
@ -13,6 +14,7 @@ use tracing_subscriber::EnvFilter;
/// Global feature flags manager /// Global feature flags manager
static FEATURE_FLAGS: std::sync::OnceLock<FeatureFlagManager> = std::sync::OnceLock::new(); static FEATURE_FLAGS: std::sync::OnceLock<FeatureFlagManager> = std::sync::OnceLock::new();
static INVOKED_BINARY_NAME: std::sync::OnceLock<String> = std::sync::OnceLock::new();
/// Get the global feature flags manager /// Get the global feature flags manager
pub fn feature_flags() -> &'static FeatureFlagManager { pub fn feature_flags() -> &'static FeatureFlagManager {
@ -27,9 +29,40 @@ pub fn feature_flags() -> &'static FeatureFlagManager {
}) })
} }
fn detect_invoked_binary_name() -> String {
std::env::args()
.next()
.and_then(|path| {
std::path::Path::new(&path)
.file_name()
.map(|name| name.to_string_lossy().into_owned())
})
.filter(|name| !name.is_empty())
.unwrap_or_else(|| "miyabi".to_string())
}
fn current_binary_name() -> &'static str {
INVOKED_BINARY_NAME
.get_or_init(detect_invoked_binary_name)
.as_str()
}
fn gate_command(command: &str) -> String {
if command.is_empty() {
format!("{} gate", current_binary_name())
} else {
format!("{} gate {}", current_binary_name(), command)
}
}
fn agent_guide() -> String {
AGENT_GUIDE_TEMPLATE
.replace("{{GATE}}", &gate_command(""))
.replace("{{BINARY}}", current_binary_name())
}
#[derive(Parser)] #[derive(Parser)]
#[command(name = "miyabi")] #[command(author, version, about = "MergeGate - Deterministic task execution and merge workflow for AI-assisted development", long_about = None)]
#[command(author, version, about = "Miyabi - Autonomous AI Development Framework", long_about = None)]
struct Cli { struct Cli {
/// Model to use (overrides config) /// Model to use (overrides config)
#[arg(short, long)] #[arg(short, long)]
@ -102,12 +135,15 @@ enum Commands {
}, },
#[command( #[command(
about = "Deterministic Task Protocol gate controls", about = "Deterministic Task Protocol gate controls",
long_about = "Run miyabi gate init to set up a new project.\n\nDeterministic Task Protocol gate controls" long_about = "Run the gate init command to set up a new project.\n\nDeterministic Task Protocol gate controls"
)] )]
Gate { Gate {
/// Output format /// Output format
#[arg(long, value_enum, default_value_t = OutputFormat::Text)] #[arg(long, value_enum, default_value_t = OutputFormat::Text)]
format: OutputFormat, format: OutputFormat,
/// Emit a hook-friendly JSON event envelope to stdout
#[arg(long)]
emit_event: bool,
/// Path to the task ledger JSON file /// Path to the task ledger JSON file
#[arg(long, default_value = "project_memory/tasks.json")] #[arg(long, default_value = "project_memory/tasks.json")]
store_path: PathBuf, store_path: PathBuf,
@ -278,7 +314,7 @@ enum GateCommand {
}, },
} }
#[derive(Debug)] #[derive(Debug, Serialize)]
struct AssignPlanAttachment { struct AssignPlanAttachment {
attachment_type: String, attachment_type: String,
source: String, source: String,
@ -286,7 +322,7 @@ struct AssignPlanAttachment {
content: String, content: String,
} }
#[derive(Debug)] #[derive(Debug, Serialize)]
struct AssignExecutionPlan { struct AssignExecutionPlan {
task_title: String, task_title: String,
risk_level: Option<String>, risk_level: Option<String>,
@ -296,7 +332,7 @@ struct AssignExecutionPlan {
next_steps: Vec<String>, next_steps: Vec<String>,
} }
#[derive(Debug)] #[derive(Debug, Serialize)]
struct InitStatus { struct InitStatus {
initialized: bool, initialized: bool,
current_dir: String, current_dir: String,
@ -409,7 +445,11 @@ async fn main() -> anyhow::Result<()> {
.with_env_filter(EnvFilter::from_default_env()) .with_env_filter(EnvFilter::from_default_env())
.init(); .init();
let cli = Cli::parse(); let invoked_binary_name = detect_invoked_binary_name();
let _ = INVOKED_BINARY_NAME.set(invoked_binary_name.clone());
let clap_binary_name: &'static str = Box::leak(invoked_binary_name.into_boxed_str());
let matches = Cli::command().name(clap_binary_name).get_matches();
let cli = Cli::from_arg_matches(&matches).unwrap_or_else(|error| error.exit());
match cli.command { match cli.command {
Some(Commands::Tui) | None => { Some(Commands::Tui) | None => {
@ -874,10 +914,11 @@ async fn main() -> anyhow::Result<()> {
} }
Some(Commands::Gate { Some(Commands::Gate {
format, format,
emit_event,
store_path, store_path,
command, command,
}) => { }) => {
let code = handle_gate_command(&format, &store_path, command)?; let code = handle_gate_command(&format, emit_event, &store_path, command)?;
std::process::exit(code); std::process::exit(code);
} }
Some(Commands::Openclaw { command }) => { Some(Commands::Openclaw { command }) => {
@ -1245,6 +1286,7 @@ async fn main() -> anyhow::Result<()> {
fn handle_gate_command( fn handle_gate_command(
format: &OutputFormat, format: &OutputFormat,
emit_event: bool,
store_path: &std::path::Path, store_path: &std::path::Path,
command: GateCommand, command: GateCommand,
) -> anyhow::Result<i32> { ) -> anyhow::Result<i32> {
@ -1263,7 +1305,10 @@ fn handle_gate_command(
let result = match command { let result = match command {
GateCommand::Init => { GateCommand::Init => {
initialize_gate_project(format, store_path)?; let status = initialize_gate_project(format, emit_event, store_path)?;
if emit_event {
emit_gate_event("gate_initialized", None, &status);
}
Ok(()) Ok(())
} }
GateCommand::Register { GateCommand::Register {
@ -1297,7 +1342,9 @@ fn handle_gate_command(
&node, &node,
) )
.map(|task| { .map(|task| {
if matches!(format, OutputFormat::Json) { if emit_event {
emit_gate_event("task_registered", Some(&task.id), &task);
} else if matches!(format, OutputFormat::Json) {
println!("{}", serde_json::to_string_pretty(&task).unwrap()); println!("{}", serde_json::to_string_pretty(&task).unwrap());
} else { } else {
println!("registered: {} ({})", task.id, task.title); println!("registered: {} ({})", task.id, task.title);
@ -1312,20 +1359,22 @@ fn handle_gate_command(
.status(task_id.as_deref()) .status(task_id.as_deref())
.map(|status| match status { .map(|status| match status {
StatusReport::Task(task) => { StatusReport::Task(task) => {
if matches!(format, OutputFormat::Json) { if emit_event {
emit_gate_event("task_status", Some(&task.id), &task);
} else if matches!(format, OutputFormat::Json) {
println!("{}", serde_json::to_string_pretty(&task).unwrap()); println!("{}", serde_json::to_string_pretty(&task).unwrap());
} else { } else {
println!("{}: {:?} - {}", task.id, task.current_state, task.title); print_gate_task_status(&task);
} }
} }
StatusReport::Snapshot(snapshot) => { StatusReport::Snapshot(snapshot) => {
if matches!(format, OutputFormat::Json) { if emit_event {
emit_gate_event("status_snapshot", None, &snapshot);
} else if matches!(format, OutputFormat::Json) {
println!("{}", serde_json::to_string_pretty(&snapshot).unwrap()); println!("{}", serde_json::to_string_pretty(&snapshot).unwrap());
} else { } else {
println!("tasks: {}", snapshot.tasks.len()); let dispatchable = protocol.dispatchable().ok();
for task in snapshot.tasks { print_gate_snapshot_status(&snapshot, dispatchable.as_ref());
println!(" {} [{:?}] {}", task.id, task.current_state, task.title);
}
} }
} }
}) })
@ -1340,15 +1389,14 @@ fn handle_gate_command(
.and_then(|result| { .and_then(|result| {
let attachments = protocol.attach_context(&task_id, actor, &node)?; let attachments = protocol.attach_context(&task_id, actor, &node)?;
let plan = build_assign_execution_plan(&result.task, attachments); let plan = build_assign_execution_plan(&result.task, attachments);
if matches!(format, OutputFormat::Json) { let output = serde_json::json!({
println!(
"{}",
serde_json::to_string_pretty(&serde_json::json!({
"assignment": result, "assignment": result,
"plan": assign_plan_to_json(&plan), "plan": assign_plan_to_json(&plan),
})) });
.unwrap() if emit_event {
); emit_gate_event("lock_acquired", Some(&task_id), &output);
} else if matches!(format, OutputFormat::Json) {
println!("{}", serde_json::to_string_pretty(&output).unwrap());
} else { } else {
println!("assigned: {} -> {}@{}", result.task.id, agent, agent_node); println!("assigned: {} -> {}@{}", result.task.id, agent, agent_node);
print_assign_execution_plan(&result, &plan); print_assign_execution_plan(&result, &plan);
@ -1383,7 +1431,9 @@ fn handle_gate_command(
&node, &node,
) )
.map(|task| { .map(|task| {
if matches!(format, OutputFormat::Json) { if emit_event {
emit_gate_event("impact_recorded", Some(&task.id), &task);
} else if matches!(format, OutputFormat::Json) {
println!("{}", serde_json::to_string_pretty(&task).unwrap()); println!("{}", serde_json::to_string_pretty(&task).unwrap());
} else { } else {
println!("impact recorded: {}", task.id); println!("impact recorded: {}", task.id);
@ -1392,7 +1442,9 @@ fn handle_gate_command(
GateCommand::Branch { task_id, name } => protocol GateCommand::Branch { task_id, name } => protocol
.record_branch(&task_id, &name, actor, &node) .record_branch(&task_id, &name, actor, &node)
.map(|task| { .map(|task| {
if matches!(format, OutputFormat::Json) { if emit_event {
emit_gate_event("branch_created", Some(&task.id), &task);
} else if matches!(format, OutputFormat::Json) {
println!("{}", serde_json::to_string_pretty(&task).unwrap()); println!("{}", serde_json::to_string_pretty(&task).unwrap());
} else { } else {
println!("branch recorded: {} -> {}", task.id, name); println!("branch recorded: {} -> {}", task.id, name);
@ -1402,7 +1454,9 @@ fn handle_gate_command(
protocol protocol
.attach_context(&task_id, actor, &node) .attach_context(&task_id, actor, &node)
.map(|attachments| { .map(|attachments| {
if matches!(format, OutputFormat::Json) { if emit_event {
emit_gate_event("context_attached", Some(&task_id), &attachments);
} else if matches!(format, OutputFormat::Json) {
println!("{}", serde_json::to_string_pretty(&attachments).unwrap()); println!("{}", serde_json::to_string_pretty(&attachments).unwrap());
} else if attachments.is_empty() { } else if attachments.is_empty() {
println!("no context attachments: {}", task_id); println!("no context attachments: {}", task_id);
@ -1427,7 +1481,9 @@ fn handle_gate_command(
protocol protocol
.refresh_context(&task_id, actor, &node) .refresh_context(&task_id, actor, &node)
.map(|attachments| { .map(|attachments| {
if matches!(format, OutputFormat::Json) { if emit_event {
emit_gate_event("context_refreshed", Some(&task_id), &attachments);
} else if matches!(format, OutputFormat::Json) {
println!("{}", serde_json::to_string_pretty(&attachments).unwrap()); println!("{}", serde_json::to_string_pretty(&attachments).unwrap());
} else if attachments.is_empty() { } else if attachments.is_empty() {
println!("no context attachments: {}", task_id); println!("no context attachments: {}", task_id);
@ -1448,7 +1504,9 @@ fn handle_gate_command(
GateCommand::Pr { task_id, number } => protocol GateCommand::Pr { task_id, number } => protocol
.record_pr(&task_id, number, actor, &node) .record_pr(&task_id, number, actor, &node)
.map(|task| { .map(|task| {
if matches!(format, OutputFormat::Json) { if emit_event {
emit_gate_event("pr_created", Some(&task.id), &task);
} else if matches!(format, OutputFormat::Json) {
println!("{}", serde_json::to_string_pretty(&task).unwrap()); println!("{}", serde_json::to_string_pretty(&task).unwrap());
} else { } else {
println!("pr recorded: {} -> #{}", task.id, number); println!("pr recorded: {} -> #{}", task.id, number);
@ -1457,7 +1515,9 @@ fn handle_gate_command(
GateCommand::Merge { task_id, sha } => protocol GateCommand::Merge { task_id, sha } => protocol
.record_merge(&task_id, &sha, actor, &node) .record_merge(&task_id, &sha, actor, &node)
.map(|task| { .map(|task| {
if matches!(format, OutputFormat::Json) { if emit_event {
emit_gate_event("task_completed", Some(&task.id), &task);
} else if matches!(format, OutputFormat::Json) {
println!("{}", serde_json::to_string_pretty(&task).unwrap()); println!("{}", serde_json::to_string_pretty(&task).unwrap());
} else { } else {
println!("merge recorded: {} -> {}", task.id, sha); println!("merge recorded: {} -> {}", task.id, sha);
@ -1466,7 +1526,9 @@ fn handle_gate_command(
GateCommand::VerifyMerge { task_id, repo } => protocol GateCommand::VerifyMerge { task_id, repo } => protocol
.verify_merge(&task_id, &repo, actor, &node) .verify_merge(&task_id, &repo, actor, &node)
.map(|task| { .map(|task| {
if matches!(format, OutputFormat::Json) { if emit_event {
emit_gate_event("task_completed", Some(&task.id), &task);
} else if matches!(format, OutputFormat::Json) {
println!("{}", serde_json::to_string_pretty(&task).unwrap()); println!("{}", serde_json::to_string_pretty(&task).unwrap());
} else { } else {
let sha = task let sha = task
@ -1484,7 +1546,9 @@ fn handle_gate_command(
} => protocol } => protocol
.force_unlock(&task_id, &reason, &operator) .force_unlock(&task_id, &reason, &operator)
.map(|task| { .map(|task| {
if matches!(format, OutputFormat::Json) { if emit_event {
emit_gate_event("lock_released", Some(&task.id), &task);
} else if matches!(format, OutputFormat::Json) {
println!("{}", serde_json::to_string_pretty(&task).unwrap()); println!("{}", serde_json::to_string_pretty(&task).unwrap());
} else { } else {
println!("lock released: {} by {}", task.id, operator); println!("lock released: {} by {}", task.id, operator);
@ -1497,14 +1561,18 @@ fn handle_gate_command(
} => protocol } => protocol
.manual_complete(&task_id, &reason, &operator) .manual_complete(&task_id, &reason, &operator)
.map(|task| { .map(|task| {
if matches!(format, OutputFormat::Json) { if emit_event {
emit_gate_event("task_completed", Some(&task.id), &task);
} else if matches!(format, OutputFormat::Json) {
println!("{}", serde_json::to_string_pretty(&task).unwrap()); println!("{}", serde_json::to_string_pretty(&task).unwrap());
} else { } else {
println!("task completed manually: {} by {}", task.id, operator); println!("task completed manually: {} by {}", task.id, operator);
} }
}), }),
GateCommand::Locks => protocol.locks().map(|locks| { GateCommand::Locks => protocol.locks().map(|locks| {
if matches!(format, OutputFormat::Json) { if emit_event {
emit_gate_event("locks_reported", None, &locks);
} else if matches!(format, OutputFormat::Json) {
println!("{}", serde_json::to_string_pretty(&locks).unwrap()); println!("{}", serde_json::to_string_pretty(&locks).unwrap());
} else if locks.is_empty() { } else if locks.is_empty() {
println!("no active locks"); println!("no active locks");
@ -1515,7 +1583,9 @@ fn handle_gate_command(
} }
}), }),
GateCommand::Dag => protocol.dag().map(|report| { GateCommand::Dag => protocol.dag().map(|report| {
if matches!(format, OutputFormat::Json) { if emit_event {
emit_gate_event("dag_reported", None, &report);
} else if matches!(format, OutputFormat::Json) {
println!("{}", serde_json::to_string_pretty(&report).unwrap()); println!("{}", serde_json::to_string_pretty(&report).unwrap());
} else { } else {
for (index, level) in report.levels.iter().enumerate() { for (index, level) in report.levels.iter().enumerate() {
@ -1524,10 +1594,16 @@ fn handle_gate_command(
} }
}), }),
GateCommand::Dispatchable => protocol.dispatchable().map(|report| { GateCommand::Dispatchable => protocol.dispatchable().map(|report| {
if matches!(format, OutputFormat::Json) { if emit_event {
emit_gate_event("dispatchable_reported", None, &report);
} else if matches!(format, OutputFormat::Json) {
println!("{}", serde_json::to_string_pretty(&report).unwrap()); println!("{}", serde_json::to_string_pretty(&report).unwrap());
} else if report.tasks.is_empty() { } else if report.tasks.is_empty() {
println!("no dispatchable tasks"); println!("no dispatchable tasks");
println!("next:");
println!(" {}", gate_command("status"));
println!(" {}", gate_command("dag"));
println!(" {}", gate_command("guide"));
} else { } else {
for task in report.tasks { for task in report.tasks {
println!("{} [{}] {}", task.id, task.priority, task.title); println!("{} [{}] {}", task.id, task.priority, task.title);
@ -1536,6 +1612,13 @@ fn handle_gate_command(
}), }),
GateCommand::Serve { port } => { GateCommand::Serve { port } => {
serve_dashboard(store_path, port)?; serve_dashboard(store_path, port)?;
if emit_event {
emit_gate_event(
"dashboard_started",
None,
serde_json::json!({ "port": port }),
);
}
Ok(()) Ok(())
} }
GateCommand::Dream { GateCommand::Dream {
@ -1553,7 +1636,9 @@ fn handle_gate_command(
protocol protocol
.dream(since, auto, &repo_root, actor, &node) .dream(since, auto, &repo_root, actor, &node)
.and_then(|report| { .and_then(|report| {
if matches!(format, OutputFormat::Json) { if emit_event {
emit_gate_event("dream_recorded", None, &report);
} else if matches!(format, OutputFormat::Json) {
println!("{}", serde_json::to_string_pretty(&report).unwrap()); println!("{}", serde_json::to_string_pretty(&report).unwrap());
} else { } else {
print_dream_report(&report); print_dream_report(&report);
@ -1586,15 +1671,14 @@ fn handle_gate_command(
Err(ProtocolError::input("heartbeat currently requires --all")) Err(ProtocolError::input("heartbeat currently requires --all"))
} else { } else {
protocol.heartbeat_all().map(|renewed| { protocol.heartbeat_all().map(|renewed| {
if matches!(format, OutputFormat::Json) { let output = serde_json::json!({
println!(
"{}",
serde_json::to_string_pretty(&serde_json::json!({
"renewed": renewed, "renewed": renewed,
"count": renewed.len(), "count": renewed.len(),
})) });
.unwrap() if emit_event {
); emit_gate_event("heartbeat_renewed", None, &output);
} else if matches!(format, OutputFormat::Json) {
println!("{}", serde_json::to_string_pretty(&output).unwrap());
} else { } else {
println!("renewed leases: {}", renewed.len()); println!("renewed leases: {}", renewed.len());
for task_id in renewed { for task_id in renewed {
@ -1605,7 +1689,7 @@ fn handle_gate_command(
} }
} }
GateCommand::Guide => { GateCommand::Guide => {
print!("{AGENT_GUIDE}"); print!("{}", agent_guide());
Ok(()) Ok(())
} }
}; };
@ -1613,19 +1697,19 @@ fn handle_gate_command(
Ok(match result { Ok(match result {
Ok(()) => 0, Ok(()) => 0,
Err(ProtocolError::GateRejected(message)) => { Err(ProtocolError::GateRejected(message)) => {
emit_gate_error(format, "gate_rejected", &message); emit_gate_error(format, emit_event, "gate_rejected", &message);
1 1
} }
Err(ProtocolError::DependencyBlocked(message)) => { Err(ProtocolError::DependencyBlocked(message)) => {
emit_gate_error(format, "gate_rejected", &message); emit_gate_error(format, emit_event, "gate_rejected", &message);
1 1
} }
Err(ProtocolError::Input(message)) => { Err(ProtocolError::Input(message)) => {
emit_gate_error(format, "input_error", &message); emit_gate_error(format, emit_event, "input_error", &message);
2 2
} }
Err(ProtocolError::Internal(error)) => { Err(ProtocolError::Internal(error)) => {
emit_gate_error(format, "internal_error", &error.to_string()); emit_gate_error(format, emit_event, "internal_error", &error.to_string());
1 1
} }
}) })
@ -1678,17 +1762,23 @@ fn assign_next_steps(
miyabi_core::store::CompletionMode::GithubPr => vec![ miyabi_core::store::CompletionMode::GithubPr => vec![
"1. Create branch".to_string(), "1. Create branch".to_string(),
"2. Make changes".to_string(), "2. Make changes".to_string(),
format!("3. miyabi gate branch {task_id} ..."), format!("3. {} {task_id} ...", gate_command("branch")),
format!("4. miyabi gate pr {task_id} ..."), format!("4. {} {task_id} ...", gate_command("pr")),
format!("5. miyabi gate merge {task_id} ..."), format!("5. {} {task_id} ...", gate_command("merge")),
], ],
miyabi_core::store::CompletionMode::Manual => vec![ miyabi_core::store::CompletionMode::Manual => vec![
"1. Complete the work".to_string(), "1. Complete the work".to_string(),
format!("2. miyabi gate manual-complete {task_id} --reason ... --operator ..."), format!(
"2. {} {task_id} --reason ... --operator ...",
gate_command("manual-complete")
),
], ],
miyabi_core::store::CompletionMode::ExternalOp => vec![ miyabi_core::store::CompletionMode::ExternalOp => vec![
"1. Complete external operation".to_string(), "1. Complete external operation".to_string(),
format!("2. miyabi gate manual-complete {task_id} --reason ... --operator ..."), format!(
"2. {} {task_id} --reason ... --operator ...",
gate_command("manual-complete")
),
], ],
} }
} }
@ -1737,10 +1827,145 @@ fn print_assign_execution_plan(
} }
} }
fn print_gate_task_status(task: &miyabi_core::store::ExecutionTask) {
println!("task: {}", task.id);
println!("title: {}", task.title);
println!("state: {:?}", task.current_state);
if task.issue_number > 0 {
println!("issue: #{}", task.issue_number);
}
println!("completion mode: {:?}", task.completion_mode);
if let Some(impact) = &task.impact {
println!(
"impact: {:?} (symbols: {})",
impact.risk_level, impact.affected_symbols
);
} else {
println!("impact: not recorded");
}
match task.current_state {
miyabi_core::store::TaskState::Pending | miyabi_core::store::TaskState::Draft => {
if task.impact.is_none() {
println!("next:");
println!(" {} {} --risk low --symbols 0", gate_command("impact"), task.id);
println!(
" {} {} --agent <name> --node <machine> --files \"path/to/file\"",
gate_command("assign"),
task.id
);
} else {
println!("next:");
println!(
" {} {} --agent <name> --node <machine> --files \"path/to/file\"",
gate_command("assign"),
task.id
);
}
}
miyabi_core::store::TaskState::Implementing => {
println!("next:");
println!(" {} {} <branch-name>", gate_command("branch"), task.id);
println!(" {} {}", gate_command("attach"), task.id);
}
miyabi_core::store::TaskState::Reviewing => {
println!("next:");
println!(" {} {} <number>", gate_command("pr"), task.id);
println!(" {} {} <40-char-sha>", gate_command("merge"), task.id);
}
miyabi_core::store::TaskState::Merged | miyabi_core::store::TaskState::Done => {
println!("next:");
println!(" {}", gate_command("dispatchable"));
}
_ => {}
}
}
fn print_gate_snapshot_status(
snapshot: &miyabi_core::store::TasksSnapshot,
dispatchable: Option<&miyabi_core::protocol::DispatchableReport>,
) {
if snapshot.tasks.is_empty() {
println!("tasks: 0");
println!("status: ready to initialize or register your first task");
println!("this is not an error. it means the ledger is empty.");
println!("next:");
println!(" {}", gate_command("init"));
println!(" {} --issue <N> --title \"Your task\"", gate_command("register"));
println!(" {}", gate_command("guide"));
return;
}
let total = snapshot.tasks.len();
let completed = snapshot
.tasks
.iter()
.filter(|task| {
matches!(
task.current_state,
miyabi_core::store::TaskState::Done | miyabi_core::store::TaskState::Merged
)
})
.count();
let active = snapshot
.tasks
.iter()
.filter(|task| {
matches!(
task.current_state,
miyabi_core::store::TaskState::Implementing
| miyabi_core::store::TaskState::Reviewing
| miyabi_core::store::TaskState::Analyzing
)
})
.count();
let waiting = total.saturating_sub(completed + active);
let dispatchable_count = dispatchable.map(|report| report.count).unwrap_or(0);
println!(
"tasks: {} (dispatchable: {}, locks: {})",
total,
dispatchable_count,
snapshot.file_locks.len()
);
println!(
"summary: {} completed, {} active, {} waiting",
completed, active, waiting
);
if let Some(report) = dispatchable {
if !report.tasks.is_empty() {
println!("next tasks:");
for task in report.tasks.iter().take(3) {
println!(" {} [{}] {}", task.id, task.priority, task.title);
}
println!("next:");
println!(" {} <task-id>", gate_command("status"));
println!(
" {} <task-id> --agent <name> --node <machine> --files \"path/to/file\"",
gate_command("assign")
);
} else {
println!("next:");
println!(" {}", gate_command("dag"));
println!(" {}", gate_command("locks"));
println!(" {}", gate_command("guide"));
}
}
println!("all tasks:");
for task in &snapshot.tasks {
println!(" {} [{:?}] {}", task.id, task.current_state, task.title);
}
}
fn initialize_gate_project( fn initialize_gate_project(
format: &OutputFormat, format: &OutputFormat,
emit_event: bool,
store_path: &std::path::Path, store_path: &std::path::Path,
) -> anyhow::Result<()> { ) -> anyhow::Result<InitStatus> {
let current_dir = std::env::current_dir()?; let current_dir = std::env::current_dir()?;
let created_path = store_path.display().to_string(); let created_path = store_path.display().to_string();
let initialized = if store_path.exists() { let initialized = if store_path.exists() {
@ -1773,6 +1998,10 @@ fn initialize_gate_project(
github_project_detected, github_project_detected,
}; };
if emit_event {
return Ok(status);
}
if matches!(format, OutputFormat::Json) { if matches!(format, OutputFormat::Json) {
println!( println!(
"{}", "{}",
@ -1785,15 +2014,15 @@ fn initialize_gate_project(
"gitignore_updated": status.gitignore_updated, "gitignore_updated": status.gitignore_updated,
"github_project_detected": status.github_project_detected, "github_project_detected": status.github_project_detected,
"next_steps": [ "next_steps": [
"miyabi gate register --issue <N> --title ...", gate_command("status"),
"miyabi gate status", gate_command("guide"),
"miyabi gate --help" format!("{} --issue <N> --title ...", gate_command("register"))
], ],
}))? }))?
); );
} else { } else {
if status.initialized { if status.initialized {
println!("Polaris initialized in {}", status.current_dir); println!("MergeGate initialized in {}", status.current_dir);
println!("Created: {}", status.created_path); println!("Created: {}", status.created_path);
} else { } else {
println!("Already initialized"); println!("Already initialized");
@ -1805,13 +2034,13 @@ fn initialize_gate_project(
println!("⚠️ No GitHub remote. Run: gh repo create <name> --private"); println!("⚠️ No GitHub remote. Run: gh repo create <name> --private");
} }
println!("Next steps:"); println!("Next steps:");
println!(" miyabi gate register --issue <N> --title ..."); println!(" {}", gate_command("status"));
println!(" miyabi gate status"); println!(" {}", gate_command("guide"));
println!(" miyabi gate --help"); println!(" {} --issue <N> --title ...", gate_command("register"));
print_init_checklist(&status); print_init_checklist(&status);
} }
Ok(()) Ok(status)
} }
fn print_init_checklist(status: &InitStatus) { fn print_init_checklist(status: &InitStatus) {
@ -2011,8 +2240,24 @@ fn print_dream_report(report: &miyabi_core::DreamReport) {
} }
} }
fn emit_gate_error(format: &OutputFormat, kind: &str, message: &str) { fn emit_gate_event(event: &str, task_id: Option<&str>, payload: impl serde::Serialize) {
if matches!(format, OutputFormat::Json) { println!(
"{}",
serde_json::to_string_pretty(&serde_json::json!({
"event": event,
"task_id": task_id,
"source": "miyabi-gate",
"ts": chrono::Utc::now(),
"payload": payload,
}))
.unwrap()
);
}
fn emit_gate_error(format: &OutputFormat, emit_event: bool, kind: &str, message: &str) {
if emit_event {
emit_gate_event(kind, None, serde_json::json!({ "message": message }));
} else if matches!(format, OutputFormat::Json) {
println!( println!(
"{}", "{}",
serde_json::to_string_pretty(&serde_json::json!({ serde_json::to_string_pretty(&serde_json::json!({
@ -2026,12 +2271,12 @@ fn emit_gate_error(format: &OutputFormat, kind: &str, message: &str) {
} }
} }
const AGENT_GUIDE: &str = r#" const AGENT_GUIDE_TEMPLATE: &str = r#"
# Polaris (miyabi-gate) Agent Guide # MergeGate ({{GATE}}) Agent Guide
## What is this? ## What is this?
Polaris is a deterministic task execution protocol. It enforces a strict MergeGate is a deterministic task execution protocol. It enforces a strict
workflow so that any agent on any machine produces the same verifiable result. workflow so that any agent on any machine produces the same verifiable result.
Tasks are tracked in project_memory/tasks.json, not in conversation memory. Tasks are tracked in project_memory/tasks.json, not in conversation memory.
@ -2047,53 +2292,53 @@ Tasks are tracked in project_memory/tasks.json, not in conversation memory.
``` ```
Step 1: Register Step 1: Register
miyabi-gate gate register --issue <N> --title "Task description" {{GATE}} register --issue <N> --title "Task description"
Step 2: Impact analysis Step 2: Impact analysis
miyabi-gate gate impact <task-id> --risk <low|medium|high|critical> --symbols <N> {{GATE}} impact <task-id> --risk <low|medium|high|critical> --symbols <N>
# Add --approve if risk is high or critical # Add --approve if risk is high or critical
Step 3: Assign (acquires file locks) Step 3: Assign (acquires file locks)
miyabi-gate gate assign <task-id> --agent <your-name> --node <machine> --files "file1.rs,file2.rs" {{GATE}} assign <task-id> --agent <your-name> --node <machine> --files "file1.rs,file2.rs"
# This prints an execution plan and context attachments. Read them. # This prints an execution plan and context attachments. Read them.
Step 4: Work Step 4: Work
# Edit ONLY the locked files. Pre-commit hook blocks unlocked files. # Edit ONLY the locked files. Pre-commit hook blocks unlocked files.
Step 5: Branch Step 5: Branch
miyabi-gate gate branch <task-id> feature/issue-<N>-<slug> {{GATE}} branch <task-id> feature/issue-<N>-<slug>
Step 6: PR Step 6: PR
miyabi-gate gate pr <task-id> <PR-number> {{GATE}} pr <task-id> <PR-number>
Step 7: Merge Step 7: Merge
miyabi-gate gate merge <task-id> <merge-commit-sha> {{GATE}} merge <task-id> <merge-commit-sha>
``` ```
## For document-only tasks (no PR needed) ## For document-only tasks (no PR needed)
``` ```
miyabi-gate gate register --issue <N> --title "Doc task" --completion-mode manual {{GATE}} register --issue <N> --title "Doc task" --completion-mode manual
miyabi-gate gate impact <task-id> --risk low --symbols 0 {{GATE}} impact <task-id> --risk low --symbols 0
miyabi-gate gate assign <task-id> --agent <name> --node <machine> --files "docs/file.md" {{GATE}} assign <task-id> --agent <name> --node <machine> --files "docs/file.md"
# ... do the work ... # ... do the work ...
miyabi-gate gate manual-complete <task-id> --reason "reason" --operator <name> {{GATE}} manual-complete <task-id> --reason "reason" --operator <name>
``` ```
## Checking state ## Checking state
``` ```
miyabi-gate gate status # All tasks {{GATE}} status # All tasks
miyabi-gate gate status <task-id> # One task {{GATE}} status <task-id> # One task
miyabi-gate gate locks # Active file locks {{GATE}} locks # Active file locks
miyabi-gate gate dag # Dependency graph {{GATE}} dag # Dependency graph
miyabi-gate gate dispatchable # Tasks ready to work on {{GATE}} dispatchable # Tasks ready to work on
miyabi-gate gate attach <task-id> # View context attachments {{GATE}} attach <task-id> # View context attachments
``` ```
## Context attachments (auto-injected on assign) ## Context attachments (auto-injected on assign)
When you run `assign`, Polaris automatically attaches: When you run `assign`, MergeGate automatically attaches:
- GitHub Issue body - GitHub Issue body
- Impact analysis result - Impact analysis result
- Obsidian vault notes matching the task title (if OBSIDIAN_VAULT_PATH is set) - Obsidian vault notes matching the task title (if OBSIDIAN_VAULT_PATH is set)
@ -2101,15 +2346,17 @@ When you run `assign`, Polaris automatically attaches:
- First 30 lines of each locked file - First 30 lines of each locked file
- First 30 lines of depth-1 impact files (direct callers) - First 30 lines of depth-1 impact files (direct callers)
Use `miyabi-gate gate attach <task-id> --format json` to get this as JSON Use `{{GATE}} attach <task-id>` to inspect the attachments.
Use `{{GATE}} --format json status` or `{{GATE}} --format json locks`
when you need machine-readable output from supported commands.
for programmatic injection into prompts. for programmatic injection into prompts.
## Emergency commands ## Emergency commands
``` ```
miyabi-gate gate force-unlock <task-id> --reason "why" --operator <name> {{GATE}} force-unlock <task-id> --reason "why" --operator <name>
miyabi-gate gate manual-complete <task-id> --reason "why" --operator <name> {{GATE}} manual-complete <task-id> --reason "why" --operator <name>
miyabi-gate gate heartbeat --all # Renew all lease heartbeats {{GATE}} heartbeat --all # Renew all lease heartbeats
``` ```
## Exit codes ## Exit codes
@ -2128,23 +2375,23 @@ cargo clippy --all-targets --all-features -- -D warnings
## Self-improvement ## Self-improvement
``` ```
miyabi-gate gate dream # Extract learnings from event log {{GATE}} dream # Extract learnings from event log
miyabi-gate gate dream --auto # Also write High learnings to docs/ and update SKILL.md {{GATE}} dream --auto # Also write High learnings to docs/ and update SKILL.md
miyabi-gate gate serve # Web dashboard at localhost:4848 {{GATE}} serve # Web dashboard at localhost:4848
``` ```
## Command Reference ## Command Reference
### init ### init
Initialize project memory in the current repo. Initialize project memory in the current repo.
miyabi-gate gate init {{GATE}} init
### register ### register
Register a new task. Creates an entry in tasks.json. Register a new task. Creates an entry in tasks.json.
miyabi-gate gate register --issue <N> --title "Title" {{GATE}} register --issue <N> --title "Title"
miyabi-gate gate register --issue <N> --title "Title" --completion-mode manual {{GATE}} register --issue <N> --title "Title" --completion-mode manual
miyabi-gate gate register --issue <N> --title "Title" --dependencies dep-1,dep-2 {{GATE}} register --issue <N> --title "Title" --dependencies dep-1,dep-2
miyabi-gate gate register --issue <N> --title "Title" --no-bus {{GATE}} register --issue <N> --title "Title" --no-bus
Options: Options:
--issue <N> GitHub issue number (required, 0 = auto-create) --issue <N> GitHub issue number (required, 0 = auto-create)
--title <TEXT> Task title (required) --title <TEXT> Task title (required)
@ -2157,9 +2404,9 @@ miyabi-gate gate serve # Web dashboard at localhost:4848
### impact ### impact
Record impact analysis for a task. Record impact analysis for a task.
miyabi-gate gate impact <task-id> --risk low --symbols 3 {{GATE}} impact <task-id> --risk low --symbols 3
miyabi-gate gate impact <task-id> --risk high --symbols 12 --approve {{GATE}} impact <task-id> --risk high --symbols 12 --approve
miyabi-gate gate impact <task-id> --risk medium --symbols 5 --depth1 "src/a.rs,src/b.rs" {{GATE}} impact <task-id> --risk medium --symbols 5 --depth1 "src/a.rs,src/b.rs"
Options: Options:
--risk <LEVEL> low | medium | high | critical --risk <LEVEL> low | medium | high | critical
--symbols <N> Number of affected symbols --symbols <N> Number of affected symbols
@ -2170,7 +2417,7 @@ miyabi-gate gate serve # Web dashboard at localhost:4848
### assign ### assign
Acquire file locks and start implementation. Acquire file locks and start implementation.
miyabi-gate gate assign <task-id> --agent codex --node macbook --files "src/main.rs,src/lib.rs" {{GATE}} assign <task-id> --agent codex --node macbook --files "src/main.rs,src/lib.rs"
Options: Options:
--agent <NAME> Agent name (required) --agent <NAME> Agent name (required)
--node <NAME> Machine name (required) --node <NAME> Machine name (required)
@ -2178,63 +2425,61 @@ miyabi-gate gate serve # Web dashboard at localhost:4848
### branch ### branch
Record branch creation. Record branch creation.
miyabi-gate gate branch <task-id> feature/issue-45-auth {{GATE}} branch <task-id> feature/issue-45-auth
### pr ### pr
Record PR creation. Record PR creation.
miyabi-gate gate pr <task-id> 88 {{GATE}} pr <task-id> 88
### merge ### merge
Record merge verification. Releases locks and unblocks dependents. Record merge verification. Releases locks and unblocks dependents.
miyabi-gate gate merge <task-id> <40-char-SHA> {{GATE}} merge <task-id> <40-char-SHA>
### status ### status
Show task status. Show task status.
miyabi-gate gate status # All tasks {{GATE}} status # All tasks
miyabi-gate gate status <task-id> # One task {{GATE}} status <task-id> # One task
miyabi-gate gate status --format json {{GATE}} --format json status
### locks ### locks
List active file locks. List active file locks.
miyabi-gate gate locks {{GATE}} locks
miyabi-gate gate locks --format json {{GATE}} --format json locks
### dag ### dag
Show DAG dependency levels. Show DAG dependency levels.
miyabi-gate gate dag {{GATE}} dag
### dispatchable ### dispatchable
Show tasks ready to be worked on (dependencies resolved, no lock). Show tasks ready to be worked on (dependencies resolved, no lock).
miyabi-gate gate dispatchable {{GATE}} dispatchable
miyabi-gate gate dispatchable --format json
### attach ### attach
View context attachments for a task. View context attachments for a task.
miyabi-gate gate attach <task-id> {{GATE}} attach <task-id>
miyabi-gate gate attach <task-id> --format json
### refresh ### refresh
Force-refresh context attachments (clears cache). Force-refresh context attachments (clears cache).
miyabi-gate gate refresh <task-id> {{GATE}} refresh <task-id>
### verify-merge ### verify-merge
Verify merge state via GitHub API. Verify merge state via GitHub API.
miyabi-gate gate verify-merge <task-id> --repo owner/repo {{GATE}} verify-merge <task-id> --repo owner/repo
### force-unlock ### force-unlock
Emergency: release a lock without completing the task. Emergency: release a lock without completing the task.
miyabi-gate gate force-unlock <task-id> --reason "why" --operator "name" {{GATE}} force-unlock <task-id> --reason "why" --operator "name"
### manual-complete ### manual-complete
Complete a task without merge verification (for doc/ops tasks). Complete a task without merge verification (for doc/ops tasks).
miyabi-gate gate manual-complete <task-id> --reason "why" --operator "name" {{GATE}} manual-complete <task-id> --reason "why" --operator "name"
### dream ### dream
Analyze event logs and extract learnings. Analyze event logs and extract learnings.
miyabi-gate gate dream {{GATE}} dream
miyabi-gate gate dream --since 24h {{GATE}} dream --since 24h
miyabi-gate gate dream --auto {{GATE}} dream --auto
miyabi-gate gate dream --auto --vault-path /path/to/obsidian {{GATE}} dream --auto --vault-path /path/to/obsidian
Options: Options:
--since <DURATION> Filter events (e.g. 24h, 7d, 30m) --since <DURATION> Filter events (e.g. 24h, 7d, 30m)
--auto Write High learnings to docs/ + update SKILL.md --auto Write High learnings to docs/ + update SKILL.md
@ -2242,18 +2487,18 @@ miyabi-gate gate serve # Web dashboard at localhost:4848
### heartbeat ### heartbeat
Renew lock lease heartbeats. Renew lock lease heartbeats.
miyabi-gate gate heartbeat --all {{GATE}} heartbeat --all
### serve ### serve
Start web dashboard. Start web dashboard.
miyabi-gate gate serve {{GATE}} serve
miyabi-gate gate serve --port 8080 {{GATE}} serve --port 8080
Options: Options:
--port <N> Port number (default: 4848) --port <N> Port number (default: 4848)
### guide ### guide
Print this guide. Print this guide.
miyabi-gate gate guide {{GATE}} guide
### Global options (apply to all gate commands) ### Global options (apply to all gate commands)
--format <text|json> Output format (default: text) --format <text|json> Output format (default: text)
@ -2265,7 +2510,7 @@ const POLARIS_DASHBOARD_HTML: &str = r##"<!DOCTYPE html>
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Polaris Dashboard</title> <title>MergeGate Dashboard</title>
<style> <style>
:root { :root {
color-scheme: light; color-scheme: light;
@ -2371,7 +2616,7 @@ const POLARIS_DASHBOARD_HTML: &str = r##"<!DOCTYPE html>
<body> <body>
<div class="shell"> <div class="shell">
<header> <header>
<h1>Polaris Dashboard</h1> <h1>MergeGate Dashboard</h1>
<p class="subtitle">Deterministic Task Protocol live view</p> <p class="subtitle">Deterministic Task Protocol live view</p>
<p class="meta" id="meta">Loading...</p> <p class="meta" id="meta">Loading...</p>
</header> </header>
@ -2574,7 +2819,7 @@ fn serve_dashboard(store_path: &std::path::Path, port: u16) -> anyhow::Result<()
store_path.to_path_buf(), store_path.to_path_buf(),
); );
let listener = TcpListener::bind(("127.0.0.1", port))?; let listener = TcpListener::bind(("127.0.0.1", port))?;
println!("Polaris Dashboard listening on http://127.0.0.1:{port}"); println!("MergeGate Dashboard listening on http://127.0.0.1:{port}");
for stream in listener.incoming() { for stream in listener.incoming() {
match stream { match stream {
@ -2706,7 +2951,7 @@ fn bus_enqueue(task_id: &str, title: &str, store_path: &std::path::Path) {
let entry = serde_json::json!({ let entry = serde_json::json!({
"ts": chrono::Utc::now().to_rfc3339(), "ts": chrono::Utc::now().to_rfc3339(),
"agent": std::env::var("POLARIS_AGENT_ID").unwrap_or_else(|_| "system".into()), "agent": std::env::var("POLARIS_AGENT_ID").unwrap_or_else(|_| "system".into()),
"skill": "polaris-ops", "skill": "mergegate-ops",
"task": format!("register: {title} ({task_id})"), "task": format!("register: {title} ({task_id})"),
"result": "queued", "result": "queued",
"score": 0.0, "score": 0.0,
@ -2718,7 +2963,11 @@ fn bus_enqueue(task_id: &str, title: &str, store_path: &std::path::Path) {
.open(&path) .open(&path)
{ {
use std::io::Write; use std::io::Write;
let _ = writeln!(file, "{}", serde_json::to_string(&entry).unwrap_or_default()); let _ = writeln!(
file,
"{}",
serde_json::to_string(&entry).unwrap_or_default()
);
} }
} }
} }

View file

@ -1,5 +1,5 @@
[package] [package]
name = "miyabi-core" name = "mergegate-core"
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
@ -7,7 +7,7 @@ authors.workspace = true
license.workspace = true license.workspace = true
repository.workspace = true repository.workspace = true
homepage.workspace = true homepage.workspace = true
description = "Miyabi Core - Shared types and utilities" description = "MergeGate Core - Shared types and utilities"
[dependencies] [dependencies]
tokio = { workspace = true } tokio = { workspace = true }

View file

@ -154,7 +154,7 @@ where
/// Append a "Common Rejection Patterns" section to SKILL.md if gate rejections are detected. /// Append a "Common Rejection Patterns" section to SKILL.md if gate rejections are detected.
fn update_skill_md_from_patterns(report: &DreamReport, repo_root: &Path) -> Result<()> { fn update_skill_md_from_patterns(report: &DreamReport, repo_root: &Path) -> Result<()> {
let skill_path = repo_root.join("skills/polaris-ops/SKILL.md"); let skill_path = repo_root.join("skills/mergegate-ops/SKILL.md");
if !skill_path.exists() { if !skill_path.exists() {
return Ok(()); return Ok(());
} }
@ -672,10 +672,10 @@ mod tests {
#[test] #[test]
fn update_skill_md_appends_when_no_marker() { fn update_skill_md_appends_when_no_marker() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let skills_dir = tmp.path().join("skills/polaris-ops"); let skills_dir = tmp.path().join("skills/mergegate-ops");
fs::create_dir_all(&skills_dir).unwrap(); fs::create_dir_all(&skills_dir).unwrap();
let skill_path = skills_dir.join("SKILL.md"); let skill_path = skills_dir.join("SKILL.md");
fs::write(&skill_path, "# Polaris Ops\n\nExisting content.\n").unwrap(); fs::write(&skill_path, "# MergeGate Ops\n\nExisting content.\n").unwrap();
let mut rejections = HashMap::new(); let mut rejections = HashMap::new();
rejections.insert("GATE 3".to_string(), 2); rejections.insert("GATE 3".to_string(), 2);
@ -690,7 +690,7 @@ mod tests {
update_skill_md_from_patterns(&report, tmp.path()).unwrap(); update_skill_md_from_patterns(&report, tmp.path()).unwrap();
let content = fs::read_to_string(&skill_path).unwrap(); let content = fs::read_to_string(&skill_path).unwrap();
assert!(content.contains("# Polaris Ops")); assert!(content.contains("# MergeGate Ops"));
assert!(content.contains("Existing content.")); assert!(content.contains("Existing content."));
assert!(content.contains("## よくある拒否パターン(自動生成)")); assert!(content.contains("## よくある拒否パターン(自動生成)"));
assert!(content.contains("GATE 3")); assert!(content.contains("GATE 3"));
@ -699,12 +699,12 @@ mod tests {
#[test] #[test]
fn update_skill_md_replaces_marker_preserves_following_sections() { fn update_skill_md_replaces_marker_preserves_following_sections() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let skills_dir = tmp.path().join("skills/polaris-ops"); let skills_dir = tmp.path().join("skills/mergegate-ops");
fs::create_dir_all(&skills_dir).unwrap(); fs::create_dir_all(&skills_dir).unwrap();
let skill_path = skills_dir.join("SKILL.md"); let skill_path = skills_dir.join("SKILL.md");
fs::write( fs::write(
&skill_path, &skill_path,
"# Polaris Ops\n\n\ "# MergeGate Ops\n\n\
## \n\n\ ## \n\n\
| GATE | | |\n|------|------|--------|\n| old | 1 | old |\n\n\ | GATE | | |\n|------|------|--------|\n| old | 1 | old |\n\n\
## \n\n- important link\n", ## \n\n- important link\n",

View file

@ -475,13 +475,13 @@ mod tests {
.unwrap(); .unwrap();
for file in &files { for file in &files {
let conflict = manager.has_conflict(&[file.clone()]).unwrap(); let conflict = manager.has_conflict(std::slice::from_ref(file)).unwrap();
assert!(conflict.conflicting); assert!(conflict.conflicting);
} }
manager.release_lock("task-a").unwrap(); manager.release_lock("task-a").unwrap();
for file in &files { for file in &files {
let conflict = manager.has_conflict(&[file.clone()]).unwrap(); let conflict = manager.has_conflict(std::slice::from_ref(file)).unwrap();
assert!(!conflict.conflicting); assert!(!conflict.conflicting);
} }
} }
@ -550,11 +550,11 @@ mod proptest_tests {
) { ) {
let (_tmp, ss, mgr) = setup(); let (_tmp, ss, mgr) = setup();
seed(&ss, "task-a"); seed(&ss, "task-a");
mgr.acquire_lock("task-a", "agent", "node", &[file.clone()]).unwrap(); mgr.acquire_lock("task-a", "agent", "node", std::slice::from_ref(&file)).unwrap();
let c = mgr.has_conflict(&[file.clone()]).unwrap(); let c = mgr.has_conflict(std::slice::from_ref(&file)).unwrap();
prop_assert!(c.conflicting); prop_assert!(c.conflicting);
mgr.release_lock("task-a").unwrap(); mgr.release_lock("task-a").unwrap();
let c = mgr.has_conflict(&[file]).unwrap(); let c = mgr.has_conflict(std::slice::from_ref(&file)).unwrap();
prop_assert!(!c.conflicting); prop_assert!(!c.conflicting);
} }
@ -565,8 +565,8 @@ mod proptest_tests {
let (_tmp, ss, mgr) = setup(); let (_tmp, ss, mgr) = setup();
seed(&ss, "task-a"); seed(&ss, "task-a");
seed(&ss, "task-b"); seed(&ss, "task-b");
mgr.acquire_lock("task-a", "agent", "node", &[file.clone()]).unwrap(); mgr.acquire_lock("task-a", "agent", "node", std::slice::from_ref(&file)).unwrap();
let result = mgr.acquire_lock("task-b", "agent", "node", &[file]); let result = mgr.acquire_lock("task-b", "agent", "node", std::slice::from_ref(&file));
prop_assert!(result.is_err()); prop_assert!(result.is_err());
} }

View file

@ -626,7 +626,7 @@ impl DeterministicExecutionProtocol {
pub fn dispatchable(&self) -> ProtocolResult<DispatchableReport> { pub fn dispatchable(&self) -> ProtocolResult<DispatchableReport> {
let snapshot = self.snapshot_store.load().map_err(ProtocolError::from)?; let snapshot = self.snapshot_store.load().map_err(ProtocolError::from)?;
let tasks = snapshot let tasks: Vec<ExecutionTask> = snapshot
.tasks .tasks
.iter() .iter()
.filter(|task| matches!(task.current_state, TaskState::Pending | TaskState::Blocked)) .filter(|task| matches!(task.current_state, TaskState::Pending | TaskState::Blocked))
@ -645,7 +645,12 @@ impl DeterministicExecutionProtocol {
}) })
.cloned() .cloned()
.collect(); .collect();
Ok(DispatchableReport { tasks }) let task_ids = tasks.iter().map(|task| task.id.clone()).collect();
Ok(DispatchableReport {
count: tasks.len(),
task_ids,
tasks,
})
} }
fn attach_context_with_limit( fn attach_context_with_limit(
@ -1087,6 +1092,8 @@ pub struct DagReport {
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DispatchableReport { pub struct DispatchableReport {
pub count: usize,
pub task_ids: Vec<String>,
pub tasks: Vec<ExecutionTask>, pub tasks: Vec<ExecutionTask>,
} }
@ -1789,6 +1796,8 @@ mod tests {
} }
let dispatchable = protocol.dispatchable().unwrap(); let dispatchable = protocol.dispatchable().unwrap();
assert_eq!(dispatchable.count, 1);
assert_eq!(dispatchable.task_ids, vec!["phase-a".to_string()]);
assert_eq!(dispatchable.tasks.len(), 1); assert_eq!(dispatchable.tasks.len(), 1);
assert_eq!(dispatchable.tasks[0].id, "phase-a"); assert_eq!(dispatchable.tasks[0].id, "phase-a");
} }

View file

@ -1002,10 +1002,16 @@ mod tests {
assert_eq!(task.current_state, TaskState::Implementing); assert_eq!(task.current_state, TaskState::Implementing);
assert!(task.lock.is_some()); assert!(task.lock.is_some());
assert!(task.impact.is_some()); assert!(task.impact.is_some());
assert_eq!(task.impact.as_ref().unwrap().risk_level, ImpactRiskLevel::High); assert_eq!(
task.impact.as_ref().unwrap().risk_level,
ImpactRiskLevel::High
);
assert!(task.github_evidence.is_some()); assert!(task.github_evidence.is_some());
assert_eq!(task.github_evidence.as_ref().unwrap().pr_number, 42); assert_eq!(task.github_evidence.as_ref().unwrap().pr_number, 42);
assert_eq!(task.github_evidence.as_ref().unwrap().pr_state, GitHubPrState::Open); assert_eq!(
task.github_evidence.as_ref().unwrap().pr_state,
GitHubPrState::Open
);
// file_locks should be populated from legacy lock // file_locks should be populated from legacy lock
assert!(snapshot.file_locks.contains_key("src/main.rs")); assert!(snapshot.file_locks.contains_key("src/main.rs"));
} }
@ -1083,7 +1089,7 @@ mod proptest_tests {
proptest! { proptest! {
#[test] #[test]
fn cas_rejects_stale_version(version in 1u64..100) { fn cas_rejects_stale_version(_version in 1u64..100) {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let store = SnapshotStore::new( let store = SnapshotStore::new(
tmp.path().join("snap.json"), tmp.path().join("snap.json"),
@ -1101,7 +1107,7 @@ mod proptest_tests {
#[test] #[test]
fn upsert_is_idempotent(n in 1usize..10) { fn upsert_is_idempotent(n in 1usize..10) {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let store = SnapshotStore::new( let _store = SnapshotStore::new(
tmp.path().join("snap.json"), tmp.path().join("snap.json"),
tmp.path().join(".lock"), tmp.path().join(".lock"),
); );

View file

@ -1,5 +1,5 @@
[package] [package]
name = "miyabi-tui" name = "mergegate-tui"
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
@ -7,7 +7,7 @@ authors.workspace = true
license.workspace = true license.workspace = true
repository.workspace = true repository.workspace = true
homepage.workspace = true homepage.workspace = true
description = "Miyabi TUI - Terminal User Interface following Codex patterns" description = "MergeGate TUI - Terminal interface for MergeGate and agent-assisted development"
[[bin]] [[bin]]
name = "miyabi-tui" name = "miyabi-tui"
@ -19,7 +19,7 @@ path = "src/lib.rs"
[dependencies] [dependencies]
# Workspace dependencies # Workspace dependencies
miyabi-core = { path = "../miyabi-core" } miyabi-core = { package = "mergegate-core", path = "../mergegate-core" }
arboard = { version = "3", optional = true } arboard = { version = "3", optional = true }
# TUI Framework # TUI Framework

View file

@ -101,7 +101,7 @@ impl App {
}); });
let welcome_message = if client.is_some() { let welcome_message = if client.is_some() {
"Welcome to Miyabi CLI! Type your message and press Enter. Press Ctrl+P for commands, F1 for help." "Welcome to MergeGate. Type your message and press Enter. Press Ctrl+P for commands, F1 for help."
} else { } else {
"⚠ ANTHROPIC_API_KEY not set. Please set it in config or environment to use Claude API." "⚠ ANTHROPIC_API_KEY not set. Please set it in config or environment to use Claude API."
}; };

View file

@ -1,4 +1,4 @@
# Getting Started with miyabi-cli-standalone # Getting Started with MergeGate
Miyabiプロジェクトへようこそこのガイドでは、ゼロからMiyabiを使い始めるまでの手順を詳しく解説します。 Miyabiプロジェクトへようこそこのガイドでは、ゼロからMiyabiを使い始めるまでの手順を詳しく解説します。
@ -66,18 +66,18 @@ echo $ANTHROPIC_API_KEY
#### 2.1 GitHub CLI使用推奨 #### 2.1 GitHub CLI使用推奨
```bash ```bash
cd miyabi-cli-standalone cd mergegate
gh repo create miyabi-cli-standalone --private --source=. --remote=origin gh repo create mergegate --private --source=. --remote=origin
``` ```
#### 2.2 手動作成 #### 2.2 手動作成
1. https://github.com/new にアクセス 1. https://github.com/new にアクセス
2. Repository nameに `miyabi-cli-standalone` を入力 2. Repository nameに `mergegate` を入力
3. "Private"を選択 3. "Private"を選択
4. "Create repository"をクリック 4. "Create repository"をクリック
5. ローカルリポジトリと接続: 5. ローカルリポジトリと接続:
```bash ```bash
git remote add origin https://github.com/YOUR_USERNAME/miyabi-cli-standalone.git git remote add origin https://github.com/YOUR_USERNAME/mergegate.git
git branch -M main git branch -M main
git add . git add .
git commit -m "feat: initial commit 🚀" git commit -m "feat: initial commit 🚀"

View file

@ -1,6 +1,6 @@
# Operation Plan - miyabi-cli-standalone # Operation Plan - MergeGate
**Project**: miyabi-cli-standalone **Project**: mergegate
**Created**: 2025-11-22 **Created**: 2025-11-22
**Execution Method**: Miyabi Autonomous Agents **Execution Method**: Miyabi Autonomous Agents
@ -49,11 +49,11 @@ miyabi agent run coordinator --issue 24 # Tool Trait
miyabi status miyabi status
# Issue一覧 # Issue一覧
gh issue list --repo ShunsukeHayashi/miyabi-cli-standalone \ gh issue list --repo ShunsukeHayashi/mergegate \
--label "🏗️ state:implementing" --label "🏗️ state:implementing"
# PR確認 # PR確認
gh pr list --repo ShunsukeHayashi/miyabi-cli-standalone gh pr list --repo ShunsukeHayashi/mergegate
``` ```
--- ---
@ -64,7 +64,7 @@ gh pr list --repo ShunsukeHayashi/miyabi-cli-standalone
```bash ```bash
# 1. 前日のPRをレビュー # 1. 前日のPRをレビュー
gh pr list --repo ShunsukeHayashi/miyabi-cli-standalone --state open gh pr list --repo ShunsukeHayashi/mergegate --state open
# 2. ブロッカー確認 # 2. ブロッカー確認
gh issue list --label "🚫 state:blocked" gh issue list --label "🚫 state:blocked"
@ -124,7 +124,7 @@ gh issue list --label "📥 state:pending" --milestone "v0.2.0 - Advanced Text R
miyabi status miyabi status
# GitHub Issues # GitHub Issues
gh issue list --repo ShunsukeHayashi/miyabi-cli-standalone gh issue list --repo ShunsukeHayashi/mergegate
# ラベル別 # ラベル別
gh issue list --label "📊 priority:P0-Critical" gh issue list --label "📊 priority:P0-Critical"
@ -233,7 +233,7 @@ cargo clippy --all-targets
```bash ```bash
# マイルストーン一覧 # マイルストーン一覧
gh api repos/ShunsukeHayashi/miyabi-cli-standalone/milestones gh api repos/ShunsukeHayashi/mergegate/milestones
# 特定マイルストーンの進捗 # 特定マイルストーンの進捗
gh issue list --milestone "v0.2.0 - Advanced Text Rendering" gh issue list --milestone "v0.2.0 - Advanced Text Rendering"
@ -328,7 +328,7 @@ gh issue list --label "🚫 state:blocked"
```bash ```bash
# 今すぐ開始 # 今すぐ開始
cd /Users/shunsuke/Dev/miyabi-cli-standalone cd /Users/shunsuke/Dev/mergegate
# Critical Path開始 # Critical Path開始
miyabi agent run coordinator --issue 19 # API Client (最重要) miyabi agent run coordinator --issue 19 # API Client (最重要)

View file

@ -1,4 +1,4 @@
# Preparation Operations - miyabi-cli-standalone # Preparation Operations - MergeGate
**Purpose**: Sprint 1開始前の準備作業チェックリスト **Purpose**: Sprint 1開始前の準備作業チェックリスト
**Target Date**: 2025-11-25 (Sprint 1 Start) **Target Date**: 2025-11-25 (Sprint 1 Start)
@ -71,7 +71,7 @@
- [ ] **必要なラベル確認** - [ ] **必要なラベル確認**
```bash ```bash
gh label list --repo ShunsukeHayashi/miyabi-cli-standalone | wc -l gh label list --repo ShunsukeHayashi/mergegate | wc -l
# Expected: 45+ labels # Expected: 45+ labels
``` ```
@ -85,7 +85,7 @@
- [ ] **マイルストーン確認** - [ ] **マイルストーン確認**
```bash ```bash
gh api repos/ShunsukeHayashi/miyabi-cli-standalone/milestones | jq '.[].title' gh api repos/ShunsukeHayashi/mergegate/milestones | jq '.[].title'
``` ```
Expected: Expected:
@ -98,7 +98,7 @@
- [ ] **Issue一覧確認** - [ ] **Issue一覧確認**
```bash ```bash
gh issue list --repo ShunsukeHayashi/miyabi-cli-standalone --state all | wc -l gh issue list --repo ShunsukeHayashi/mergegate --state all | wc -l
# Expected: 28 issues # Expected: 28 issues
``` ```
@ -266,7 +266,7 @@
- [ ] **Pulse確認** - [ ] **Pulse確認**
``` ```
https://github.com/ShunsukeHayashi/miyabi-cli-standalone/pulse https://github.com/ShunsukeHayashi/mergegate/pulse
``` ```
--- ---
@ -277,7 +277,7 @@
- [ ] **Issue #10 (MarkdownStream core)** - [ ] **Issue #10 (MarkdownStream core)**
```bash ```bash
gh issue view 10 --repo ShunsukeHayashi/miyabi-cli-standalone gh issue view 10 --repo ShunsukeHayashi/mergegate
``` ```
- [ ] **Issue #15 (DiffRender core)** - [ ] **Issue #15 (DiffRender core)**
@ -316,7 +316,7 @@
```bash ```bash
# 1. 環境確認 # 1. 環境確認
cd /Users/shunsuke/Dev/miyabi-cli-standalone cd /Users/shunsuke/Dev/mergegate
miyabi status miyabi status
# 2. Critical Path 開始 # 2. Critical Path 開始

View file

@ -1,4 +1,4 @@
# Product Specification - miyabi-cli-standalone # Product Specification - MergeGate
**Version**: 1.0.0 **Version**: 1.0.0
**Status**: Draft **Status**: Draft

View file

@ -1,6 +1,6 @@
# Sprint Planning - miyabi-cli-standalone # Sprint Planning - MergeGate
**Project**: miyabi-cli-standalone **Project**: mergegate
**Total Sprints**: 4 **Total Sprints**: 4
**Sprint Duration**: 5 days (1 week) **Sprint Duration**: 5 days (1 week)
**Start Date**: 2025-11-25 **Start Date**: 2025-11-25
@ -389,7 +389,7 @@ gh issue list --label "🏗️ state:implementing"
### Sprint 1 Quick Start ### Sprint 1 Quick Start
```bash ```bash
cd /Users/shunsuke/Dev/miyabi-cli-standalone cd /Users/shunsuke/Dev/mergegate
# Critical Path開始 # Critical Path開始
miyabi agent run coordinator --issue 19 # API Client miyabi agent run coordinator --issue 19 # API Client

View file

@ -1,4 +1,4 @@
# Miyabi CLI ユーザーマニュアル # MergeGate ユーザーマニュアル
**Version**: 0.1.0 **Version**: 0.1.0
**Last Updated**: 2025-11-23 **Last Updated**: 2025-11-23
@ -22,7 +22,14 @@
## はじめに ## はじめに
Miyabi CLIは、ターミナルで動作するAIアシスタントです。Claude APIを使用して、対話型のチャットや自律的なタスク実行が可能です。 MergeGate は、AI-assisted development 向けの deterministic task execution and merge workflow です。CLI では `miyabi``mergegate` の両方を使え、対話型の TUI と `gate` ベースの repo workflow を提供します。
設計思想はシンプルです。
- `GitNexus`: コードベースを理解する
- `MergeGate`: 変更を安全に実行する
影響範囲が分かるだけでは、AI エージェントは安全に開発できません。どの task を登録し、どのファイルを lock し、どの順序で branch / PR / merge まで進めるかを protocol として固定するのが MergeGate の役割です。
### 主な機能 ### 主な機能
@ -44,21 +51,21 @@ Miyabi CLIは、ターミナルで動作するAIアシスタントです。Claud
```bash ```bash
# 1. リポジトリをクローン # 1. リポジトリをクローン
git clone https://github.com/ShunsukeHayashi/miyabi-cli-standalone.git git clone https://github.com/ShunsukeHayashi/mergegate.git
cd miyabi-cli-standalone cd mergegate
# 2. リリースビルド # 2. リリースビルド
cargo build --release cargo build --release
# 3. バイナリの確認 # 3. バイナリの確認
ls -la target/release/miyabi ls -la target/release/miyabi target/release/mergegate
``` ```
### パスを通す(オプション) ### パスを通す(オプション)
```bash ```bash
# ~/.bashrc または ~/.zshrc に追加 # ~/.bashrc または ~/.zshrc に追加
export PATH="$PATH:/path/to/miyabi-cli-standalone/target/release" export PATH="$PATH:/path/to/mergegate/target/release"
# 設定を反映 # 設定を反映
source ~/.bashrc # または source ~/.zshrc source ~/.bashrc # または source ~/.zshrc
@ -528,7 +535,7 @@ max_retries = 5
### 問題報告 ### 問題報告
GitHub Issues: https://github.com/ShunsukeHayashi/miyabi-cli-standalone/issues GitHub Issues: https://github.com/ShunsukeHayashi/mergegate/issues
### TUIでのヘルプ ### TUIでのヘルプ

View file

@ -1,6 +1,6 @@
# Work Breakdown Structure (WBS) # Work Breakdown Structure (WBS)
**Project**: miyabi-cli-standalone **Project**: mergegate
**Version**: v1.0.0 **Version**: v1.0.0
**Created**: 2025-11-22 **Created**: 2025-11-22

View file

@ -1,6 +1,6 @@
# 改善サイクル — シータサイクル分析に基づく詳細プラン # 改善サイクル — シータサイクル分析に基づく詳細プラン
_Polaris タスク: issue-86 | 2026-04-10_ _MergeGate タスク: issue-86 | 2026-04-10_
--- ---

View file

@ -13,13 +13,13 @@
## リポジトリ ## リポジトリ
- **Miyabi-G-K/miyabi-cli-standalone**: Private - **ShunsukeHayashi/mergegate**: Private
- **Miyabi-G-K/deterministic-task-protocol**: Private - **Miyabi-G-K/deterministic-task-protocol**: Private
- 公開しない。npm にも公開しない。 - 公開しない。npm にも公開しない。
## 保護対象 ## 保護対象
1. **Polaris (DTP) アーキテクチャ** 1. **MergeGate (DTP) アーキテクチャ**
- GATE チェーンによる確定的状態遷移 - GATE チェーンによる確定的状態遷移
- ファイルロック + DAG + ステートマシンの三位一体 - ファイルロック + DAG + ステートマシンの三位一体
- ワークツリー不要の論理的並列分離 - ワークツリー不要の論理的並列分離
@ -29,7 +29,7 @@
- ドリーミングevent log → 学び昇格) - ドリーミングevent log → 学び昇格)
- シータサイクル統合 - シータサイクル統合
3. **miyabi gate CLI** 3. **MergeGate CLI**
- 11+ サブコマンドの実装 - 11+ サブコマンドの実装
- pre-commit / post-commit hook 統合 - pre-commit / post-commit hook 統合
- Bus ドッキングブリッジ - Bus ドッキングブリッジ

Some files were not shown because too many files have changed in this diff Show more