Skip to content

Commit 9157a5d

Browse files
Wojtek Grabskiclaude
andcommitted
docs: add Hermes to README grid + eliminate thin driver wrappers
- Add Hermes column to Claw Type Support matrix (7 drivers) - Document --context flag for claw build - Update CLAUDE.md, architecture plan, and UPDATING.md for Phase 4.8 - Remove thin wrapper functions in nanobot, picoclaw, nullclaw, microclaw drivers — call shared.* directly - Fix bare error returns to include driver prefix consistently Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 65b1df5 commit 9157a5d

File tree

11 files changed

+90
-149
lines changed

11 files changed

+90
-149
lines changed

CLAUDE.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ Clawdapus is infrastructure-layer governance for AI agent containers. The `claw`
3737
- `internal/driver/openclaw/` — JSON5-aware config injection, CLAWDAPUS.md generation, skill generation
3838
- `internal/driver/nanobot/` — Nanobot driver: JSON config, `/root/.nanobot` mounts, container-running health
3939
- `internal/driver/picoclaw/` — PicoClaw driver: `model_list[]` config, non-root mounts, HTTP `/health` probing
40+
- `internal/driver/hermes/` — Hermes driver: YAML config, workspace AGENTS.md, SOUL.md persona, cron/jobs.json, gateway health probing
4041
- `internal/driver/shared/` — Shared model/provider helpers, platform token mapping, CLAWDAPUS.md + handle skill generation
4142
- `internal/persona/` — Persona materialization: local copy + OCI pull via oras-go
4243
- `internal/pod/` — claw-pod.yml parser, compose emitter (includes x-claw.include composition)
@@ -59,6 +60,7 @@ Clawdapus is infrastructure-layer governance for AI agent containers. The `claw`
5960
| Phase 4 — cllama sidecar proxy (standalone + wiring + cost tracking + dashboard) | DONE |
6061
| Phase 4.5 — Interactive claw init & claw agent add (canonical layout) | DONE |
6162
| Phase 4.7 — Nanobot + PicoClaw drivers, shared helpers, scaffold parity | DONE |
63+
| Phase 4.8 — Hermes driver + shared helper extraction | DONE |
6264
| v0.2.0 — PERSONA runtime materialization + x-claw.include contract composition | DONE |
6365
| Phase 4.6 — Unified worker architecture (config, provision, diagnostic) | DESIGN |
6466
| Phase 5 — Drift scoring + cllama policy pipeline | NOT STARTED |
@@ -101,7 +103,8 @@ Clawdapus is infrastructure-layer governance for AI agent containers. The `claw`
101103
- **Nanobot driver**: Config at `/root/.nanobot/config.json`, workspace at `/root/.nanobot/workspace`, cron at `/root/.nanobot/cron/jobs.json`. CONFIGURE DSL: `nanobot config set <path> <value>`. Container-running health only (no HTTP endpoint). MVP channels: discord, telegram, slack
102104
- **PicoClaw driver**: Non-root user (`USER picoclaw`); mounts at `/home/picoclaw/.picoclaw` with `PICOCLAW_HOME`/`PICOCLAW_CONFIG` env overrides. Model-centric config: `model_list[]` with `model` field (e.g. `openai/<ref>` under cllama) + `agents.defaults.model_name`. HTTP `/health` + `/ready` on port 18790. Requires at least one supported HANDLE (fail-closed, matching upstream). 13 supported platforms including long-tail set
103105
- **Shared driver helpers** (`internal/driver/shared/`): `SplitModelRef`, `CollectProviders`, `NormalizeProvider`, `ResolveProviderAPIKey` extracted from microclaw/nullclaw duplication. `PlatformTokenVar` maps 14 platforms to env var names. `GenerateClawdapusMD` and `GenerateHandleSkill` shared across all drivers
104-
- **INVOKE 5-field cron only**: Both nanobot and picoclaw drivers validate and reject non-5-field cron expressions. `at`/`every` syntaxes deferred to future Clawfile parser extension
106+
- **INVOKE 5-field cron only**: Nanobot, picoclaw, and hermes drivers validate and reject non-5-field cron expressions. `at`/`every` syntaxes deferred to future Clawfile parser extension
107+
- **Hermes driver**: Two-root layout (`/root/.hermes` + `/workspace`). YAML config (`config.yaml`) + dotenv (`.env`). Effective `AGENTS.md` inlines CLAWDAPUS.md as infrastructure context. PERSONA via `SOUL.md` copy. INVOKE translates to Hermes-native `cron/jobs.json` with deterministic SHA256 IDs. cllama via `OPENAI_BASE_URL`/`OPENAI_API_KEY` in `.env`. Gateway health: `hermes gateway status || pgrep` fallback. Scoped to discord/telegram/slack (fail-closed on unsupported). Scaffold deferred until stable base image exists
105108
- **PERSONA runtime materialization**: `PERSONA <ref>` in Clawfile emits `claw.persona.default` label; `x-claw.persona` overrides at pod level. Local paths (relative, absolute, `file://`) are copied with path-traversal + symlink checks. Non-local refs pulled as OCI artifacts via oras-go. Mounted writable into runner; `CLAW_PERSONA_DIR` env set only when persona is present. Currently a deploy-time mount mechanism — not yet a complete identity system (no memory restoration, no snapshotting, no registry round-trips)
106109
- **x-claw.include contract composition**: `include` array in x-claw block modularizes the behavioral contract. Modes: `enforce` (hard rules, inlined), `guide` (recommendations, inlined), `reference` (informational, mounted as read-only skill files). ADR-009
107110

README.md

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ Clawdapus extends two formats you already know:
177177
| `claw agent add` | _(none)_ | Add agents while preserving existing layout (`--layout auto|canonical|flat`) |
178178
| `Clawfile` | `Dockerfile` | Build an immutable agent image |
179179
| `claw-pod.yml` | `docker-compose.yml` | Run a governed agent fleet |
180-
| `claw build` | `docker build` | Transpile + build OCI image |
180+
| `claw build` | `docker build` | Transpile + build OCI image (`--context` for separate build context) |
181181
| `claw up` | `docker compose up` | Enforce + deploy |
182182

183183
Any valid Dockerfile is a valid Clawfile. Any valid `docker-compose.yml` is a valid `claw-pod.yml`. Extended directives live in namespaces Docker already ignores. Eject from Clawdapus anytime — you still have a working OCI image and a working compose file.
@@ -190,7 +190,7 @@ The Clawfile extends the Dockerfile with directives that the `claw build` prepro
190190

191191
| Directive | Purpose |
192192
|---|---|
193-
| `CLAW_TYPE` | Selects the runtime driver (openclaw, nanobot, picoclaw, nanoclaw, microclaw, nullclaw) |
193+
| `CLAW_TYPE` | Selects the runtime driver (openclaw, hermes, nanobot, picoclaw, nanoclaw, microclaw, nullclaw) |
194194
| `AGENT` | Names the behavioral contract file |
195195
| `PERSONA` | Imports a persona workspace — local path or OCI artifact ref |
196196
| `MODEL` | Binds named model slots to providers |
@@ -210,23 +210,23 @@ The Clawfile extends the Dockerfile with directives that the `claw build` prepro
210210

211211
Pick a driver based on what you need. All drivers support `MODEL`, `AGENT`, `CLLAMA`, and `CONFIGURE`.
212212

213-
| | `openclaw` | `nanoclaw` | `nanobot` | `picoclaw` | `nullclaw` | `microclaw` |
214-
|---|:---:|:---:|:---:|:---:|:---:|:---:|
215-
| **Runtime** | [OpenClaw](https://openclaw.ai) | [Claude Agent SDK](https://github.com/anthropics/claude-code) | [Nanobot](https://github.com/HKUDS/nanobot) | [PicoClaw](https://github.com/sipeed/picoclaw) | [NullClaw](https://github.com/nullclaw/nullclaw) | [MicroClaw](https://github.com/microclaw/microclaw) |
216-
| `claw init` scaffold | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
217-
| HANDLE: Discord | ✅ | — | ✅ | ✅ | ✅ | ✅ |
218-
| HANDLE: Telegram | — | — | ✅ | ✅ | ✅ | ✅ |
219-
| HANDLE: Slack | — | — | ✅ | ✅ | ✅ | ✅ |
220-
| HANDLE: long-tail ¹ | — | — | — | ✅ | — | — |
221-
| INVOKE (cron) | ✅ | — | ✅ | ✅ | ✅ | — |
222-
| Structured health | ✅ | — | — | ✅ | ✅ | — |
223-
| Read-only rootfs | ✅ | — | ✅ | ✅ | ✅ | — |
224-
| Non-root container | — | — | — | ✅ | — | — |
225-
226-
Ordered by current upstream repo popularity as of March 8, 2026.
213+
| | `openclaw` | `hermes` | `nanoclaw` | `nanobot` | `picoclaw` | `nullclaw` | `microclaw` |
214+
|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
215+
| **Runtime** | [OpenClaw](https://openclaw.ai) | [Hermes](https://github.com/hermes-ai/hermes) | [Claude Agent SDK](https://github.com/anthropics/claude-code) | [Nanobot](https://github.com/HKUDS/nanobot) | [PicoClaw](https://github.com/sipeed/picoclaw) | [NullClaw](https://github.com/nullclaw/nullclaw) | [MicroClaw](https://github.com/microclaw/microclaw) |
216+
| `claw init` scaffold | ✅ | — ² | ✅ | ✅ | ✅ | ✅ | ✅ |
217+
| HANDLE: Discord | ✅ | ✅ | — | ✅ | ✅ | ✅ | ✅ |
218+
| HANDLE: Telegram | — | ✅ | — | ✅ | ✅ | ✅ | ✅ |
219+
| HANDLE: Slack | — | ✅ | — | ✅ | ✅ | ✅ | ✅ |
220+
| HANDLE: long-tail ¹ | — | — | — | — | ✅ | — | — |
221+
| INVOKE (cron) | ✅ | ✅ | — | ✅ | ✅ | ✅ | — |
222+
| Structured health | ✅ | ✅ | — | — | ✅ | ✅ | — |
223+
| Read-only rootfs | ✅ | ✅ | — | ✅ | ✅ | ✅ | — |
224+
| Non-root container | — | — | — | — | ✅ | — | — |
225+
227226
¹ PicoClaw long-tail: WhatsApp, Feishu, LINE, QQ, DingTalk, OneBot, WeCom, WeCom App, Pico, MaixCam.
227+
² Hermes scaffold deferred until a stable base image is available; driver is fully functional via `claw build` + `claw up`.
228228

229-
`claw init` also scaffolds `generic` (alpine:3.20, no driver enforcement) for custom runtimes.
229+
`claw init` scaffolds `generic` (alpine:3.20, no driver enforcement) for custom runtimes.
230230

231231
### OpenClaw Discord Routing Compatibility
232232

@@ -436,6 +436,7 @@ Bots install things. That's how real work gets done. Tracked mutation is evoluti
436436
| Phase 4 — Shared governance proxy integration + credential starvation | Done |
437437
| Phase 4.5 — Interactive claw init & claw agent add (canonical layout) | Done |
438438
| Phase 4.7 — Nanobot + PicoClaw drivers, shared helpers, scaffold parity | Done |
439+
| Phase 4.8 — Hermes driver + shared helper extraction | Done |
439440
| Phase 4.6 — Unified worker architecture (config, provision, diagnostic) | Design |
440441
| Phase 5 — Drift scoring + fleet governance | Planned |
441442
| Phase 6 — Recipe promotion + worker mode | Planned |

docs/UPDATING.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ A planning file can be deleted when:
7070

7171
---
7272

73-
## Current planning file status (as of 2026-03-08)
73+
## Current planning file status (as of 2026-03-17)
7474

7575
| File | Status | Action |
7676
|------|--------|--------|
@@ -79,3 +79,4 @@ A planning file can be deleted when:
7979
| `2026-02-26-phase4-cllama-sidecar.md` | Shipped scope DONE; policy pipeline deferred to Phase 5 | Keep as reference for deferred policy work |
8080
| `2026-02-27-worker-architecture-unified.md` | Phase 4.6 DESIGN | Keep; implement next |
8181
| `2026-03-01-driver-parity-matrix.md` | Implemented (Phase 4.7) | Keep until verified in production |
82+
| `2026-03-16-hermes-driver-and-shared-refactor.md` | Implemented (Phase 4.8) | Keep until verified in production |

docs/plans/2026-02-18-clawdapus-architecture.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
| Phase 4 — cllama sidecar + cost tracking + dashboard || **DONE** — standalone proxy, pod wiring, credential starvation, cost hooks, SSE dashboard; policy pipeline deferred to Phase 5 |
2222
| Phase 4.5 — Interactive claw init & claw agent add || **DONE** — canonical layout, layout auto-detection |
2323
| Phase 4.7 — Nanobot + PicoClaw drivers, scaffold parity || **DONE** — 6 driver types, shared helpers, rollcall spike |
24+
| Phase 4.8 — Hermes driver + shared helper extraction || **DONE** — 7 driver types, deeper shared extraction (SetPath, ParseConfigSetCommand, ExecInContainer, IsFiveFieldCron) |
2425
| v0.2.0 — PERSONA + x-claw.include || **DONE** — persona materialization (local + OCI), contract composition (enforce/guide/reference) |
2526
| Phase 4.6 — Unified worker architecture || **DESIGN**`docs/plans/2026-02-27-worker-architecture-unified.md` |
2627
| Phase 5 — Drift scoring + cllama policy pipeline || NOT STARTED |

internal/driver/microclaw/driver.go

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -32,20 +32,18 @@ func (d *Driver) Validate(rc *driver.ResolvedClaw) error {
3232
return fmt.Errorf("microclaw driver: agent file %q not found: %w", rc.AgentHostPath, err)
3333
}
3434

35-
modelRef, err := primaryModelRef(rc.Models)
35+
modelRef, err := shared.PrimaryModelRef(rc.Models)
3636
if err != nil {
37-
return err
37+
return fmt.Errorf("microclaw driver: %w", err)
3838
}
3939
provider, _, ok := shared.SplitModelRef(modelRef)
4040
if !ok {
4141
return fmt.Errorf("microclaw driver: invalid MODEL primary %q (expected provider/model)", modelRef)
4242
}
4343

44-
if len(rc.Configures) > 0 {
45-
for _, cmd := range rc.Configures {
46-
if _, _, err := parseConfigSetCommand(cmd); err != nil {
47-
return fmt.Errorf("microclaw driver: unsupported CONFIGURE command %q: %w", cmd, err)
48-
}
44+
for _, cmd := range rc.Configures {
45+
if _, _, err := parseConfigSetCommand(cmd); err != nil {
46+
return fmt.Errorf("microclaw driver: unsupported CONFIGURE command %q: %w", cmd, err)
4947
}
5048
}
5149

@@ -101,7 +99,7 @@ func (d *Driver) Materialize(rc *driver.ResolvedClaw, opts driver.MaterializeOpt
10199
if err != nil {
102100
return nil, fmt.Errorf("microclaw driver: apply CONFIGURE %q: %w", cmd, err)
103101
}
104-
if err := setPath(cfg, path, value); err != nil {
102+
if err := shared.SetPath(cfg, path, value); err != nil {
105103
return nil, fmt.Errorf("microclaw driver: apply CONFIGURE %q: %w", cmd, err)
106104
}
107105
}
@@ -240,9 +238,9 @@ func (d *Driver) HealthProbe(ref driver.ContainerRef) (*driver.Health, error) {
240238
}
241239

242240
func generateConfig(rc *driver.ResolvedClaw) (map[string]interface{}, error) {
243-
modelRef, err := primaryModelRef(rc.Models)
241+
modelRef, err := shared.PrimaryModelRef(rc.Models)
244242
if err != nil {
245-
return nil, err
243+
return nil, fmt.Errorf("microclaw driver: %w", err)
246244
}
247245
provider, modelID, ok := shared.SplitModelRef(modelRef)
248246
if !ok {
@@ -343,13 +341,6 @@ func generateConfig(rc *driver.ResolvedClaw) (map[string]interface{}, error) {
343341
return cfg, nil
344342
}
345343

346-
func primaryModelRef(models map[string]string) (string, error) {
347-
ref, err := shared.PrimaryModelRef(models)
348-
if err != nil {
349-
return "", fmt.Errorf("microclaw driver: %w", err)
350-
}
351-
return ref, nil
352-
}
353344

354345
func discordAllowedChannels(h *driver.HandleInfo) []uint64 {
355346
if h == nil {
@@ -438,6 +429,3 @@ func parseConfigSetCommand(cmd string) (string, interface{}, error) {
438429
return path, value, nil
439430
}
440431

441-
func setPath(obj map[string]interface{}, path string, value interface{}) error {
442-
return shared.SetPath(obj, path, value)
443-
}

internal/driver/nanobot/config.go

Lines changed: 18 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,15 @@ import (
1414
func GenerateConfig(rc *driver.ResolvedClaw) ([]byte, error) {
1515
config := make(map[string]interface{})
1616

17-
modelRef, err := primaryModelRef(rc.Models)
17+
modelRef, err := shared.PrimaryModelRef(rc.Models)
1818
if err != nil {
19-
return nil, err
19+
return nil, fmt.Errorf("nanobot driver: %w", err)
2020
}
2121

22-
if err := setPath(config, "agents.defaults.model", modelRef); err != nil {
22+
if err := shared.SetPath(config, "agents.defaults.model", modelRef); err != nil {
2323
return nil, fmt.Errorf("config generation: %w", err)
2424
}
25-
if err := setPath(config, "agents.defaults.workspace", "/root/.nanobot/workspace"); err != nil {
25+
if err := shared.SetPath(config, "agents.defaults.workspace", "/root/.nanobot/workspace"); err != nil {
2626
return nil, fmt.Errorf("config generation: %w", err)
2727
}
2828

@@ -33,17 +33,17 @@ func GenerateConfig(rc *driver.ResolvedClaw) ([]byte, error) {
3333
firstProxy := cllama.ProxyBaseURL(rc.Cllama[0])
3434
for _, provider := range shared.CollectProviders(rc.Models) {
3535
base := "providers." + provider
36-
if err := setPath(config, base+".base_url", firstProxy); err != nil {
36+
if err := shared.SetPath(config, base+".base_url", firstProxy); err != nil {
3737
return nil, fmt.Errorf("config generation: cllama provider %q base_url: %w", provider, err)
3838
}
39-
if err := setPath(config, base+".api_key", rc.CllamaToken); err != nil {
39+
if err := shared.SetPath(config, base+".api_key", rc.CllamaToken); err != nil {
4040
return nil, fmt.Errorf("config generation: cllama provider %q api_key: %w", provider, err)
4141
}
4242
}
4343
} else {
4444
for _, provider := range shared.CollectProviders(rc.Models) {
4545
if token := shared.ResolveProviderAPIKey(provider, rc.Environment); token != "" {
46-
if err := setPath(config, "providers."+provider+".api_key", token); err != nil {
46+
if err := shared.SetPath(config, "providers."+provider+".api_key", token); err != nil {
4747
return nil, fmt.Errorf("config generation: provider %q api_key: %w", provider, err)
4848
}
4949
}
@@ -53,11 +53,11 @@ func GenerateConfig(rc *driver.ResolvedClaw) ([]byte, error) {
5353
for platform, h := range rc.Handles {
5454
switch strings.ToLower(platform) {
5555
case "discord":
56-
if err := setPath(config, "channels.discord.enabled", true); err != nil {
56+
if err := shared.SetPath(config, "channels.discord.enabled", true); err != nil {
5757
return nil, fmt.Errorf("config generation: HANDLE discord: %w", err)
5858
}
5959
if token := shared.ResolveEnvTokenFromMap(rc.Environment, "DISCORD_BOT_TOKEN"); token != "" {
60-
if err := setPath(config, "channels.discord.token", token); err != nil {
60+
if err := shared.SetPath(config, "channels.discord.token", token); err != nil {
6161
return nil, fmt.Errorf("config generation: HANDLE discord: %w", err)
6262
}
6363
}
@@ -67,37 +67,37 @@ func GenerateConfig(rc *driver.ResolvedClaw) ([]byte, error) {
6767
if gid == "" {
6868
continue
6969
}
70-
if err := setPath(config, "channels.discord.guild_id", gid); err != nil {
70+
if err := shared.SetPath(config, "channels.discord.guild_id", gid); err != nil {
7171
return nil, fmt.Errorf("config generation: HANDLE discord: %w", err)
7272
}
7373
break
7474
}
7575
}
7676
case "telegram":
77-
if err := setPath(config, "channels.telegram.enabled", true); err != nil {
77+
if err := shared.SetPath(config, "channels.telegram.enabled", true); err != nil {
7878
return nil, fmt.Errorf("config generation: HANDLE telegram: %w", err)
7979
}
8080
if token := shared.ResolveEnvTokenFromMap(rc.Environment, "TELEGRAM_BOT_TOKEN"); token != "" {
81-
if err := setPath(config, "channels.telegram.bot_token", token); err != nil {
81+
if err := shared.SetPath(config, "channels.telegram.bot_token", token); err != nil {
8282
return nil, fmt.Errorf("config generation: HANDLE telegram: %w", err)
8383
}
8484
}
8585
case "slack":
86-
if err := setPath(config, "channels.slack.enabled", true); err != nil {
86+
if err := shared.SetPath(config, "channels.slack.enabled", true); err != nil {
8787
return nil, fmt.Errorf("config generation: HANDLE slack: %w", err)
8888
}
8989
if token := shared.ResolveEnvTokenFromMap(rc.Environment, "SLACK_BOT_TOKEN"); token != "" {
90-
if err := setPath(config, "channels.slack.bot_token", token); err != nil {
90+
if err := shared.SetPath(config, "channels.slack.bot_token", token); err != nil {
9191
return nil, fmt.Errorf("config generation: HANDLE slack: %w", err)
9292
}
9393
}
9494
if appToken := shared.ResolveEnvTokenFromMap(rc.Environment, "SLACK_APP_TOKEN"); appToken != "" {
95-
if err := setPath(config, "channels.slack.app_token", appToken); err != nil {
95+
if err := shared.SetPath(config, "channels.slack.app_token", appToken); err != nil {
9696
return nil, fmt.Errorf("config generation: HANDLE slack: %w", err)
9797
}
9898
}
9999
if signingSecret := shared.ResolveEnvTokenFromMap(rc.Environment, "SLACK_SIGNING_SECRET"); signingSecret != "" {
100-
if err := setPath(config, "channels.slack.signing_secret", signingSecret); err != nil {
100+
if err := shared.SetPath(config, "channels.slack.signing_secret", signingSecret); err != nil {
101101
return nil, fmt.Errorf("config generation: HANDLE slack: %w", err)
102102
}
103103
}
@@ -108,30 +108,14 @@ func GenerateConfig(rc *driver.ResolvedClaw) ([]byte, error) {
108108

109109
// Apply CONFIGURE directives last so operator settings override defaults.
110110
for _, cmd := range rc.Configures {
111-
path, value, err := parseConfigSetCommand(cmd)
111+
path, value, err := shared.ParseConfigSetCommand(cmd, "nanobot")
112112
if err != nil {
113113
return nil, fmt.Errorf("config generation: %w", err)
114114
}
115-
if err := setPath(config, path, value); err != nil {
115+
if err := shared.SetPath(config, path, value); err != nil {
116116
return nil, fmt.Errorf("config generation: %w", err)
117117
}
118118
}
119119

120120
return json.MarshalIndent(config, "", " ")
121121
}
122-
123-
func parseConfigSetCommand(cmd string) (string, interface{}, error) {
124-
return shared.ParseConfigSetCommand(cmd, "nanobot")
125-
}
126-
127-
func primaryModelRef(models map[string]string) (string, error) {
128-
ref, err := shared.PrimaryModelRef(models)
129-
if err != nil {
130-
return "", fmt.Errorf("nanobot driver: %w", err)
131-
}
132-
return ref, nil
133-
}
134-
135-
func setPath(obj map[string]interface{}, path string, value interface{}) error {
136-
return shared.SetPath(obj, path, value)
137-
}

0 commit comments

Comments
 (0)