@dev.sail.money/sailor 1.2.0-75 → 1.2.0-77

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,7 +5,7 @@
5
5
  * Do not edit manually — run `pnpm build` to regenerate.
6
6
  *
7
7
  * Spec version : 1.2.0
8
- * Generated at : 2026-06-17T16:48:08.178Z
8
+ * Generated at : 2026-06-18T13:14:28.996Z
9
9
  */
10
10
  export declare const SAIL_INTELLIGENCE_BASE_URL = "https://api.sail.money";
11
11
  export declare const SAIL_INTELLIGENCE_DOCS_URL = "https://api.sail.money/docs";
@@ -5,7 +5,7 @@
5
5
  * Do not edit manually — run `pnpm build` to regenerate.
6
6
  *
7
7
  * Spec version : 1.2.0
8
- * Generated at : 2026-06-17T16:48:08.178Z
8
+ * Generated at : 2026-06-18T13:14:28.996Z
9
9
  */
10
10
  export const SAIL_INTELLIGENCE_BASE_URL = "https://api.sail.money";
11
11
  export const SAIL_INTELLIGENCE_DOCS_URL = "https://api.sail.money/docs";
@@ -217,7 +217,7 @@ function checkSkills(errors) {
217
217
  errors.push(`templates/default/.agents/skills/${d}: missing SKILL.md`);
218
218
  continue;
219
219
  }
220
- const fm = readFileSync(skillFile, "utf-8").match(/^---\n([\s\S]*?)\n---/);
220
+ const fm = readFileSync(skillFile, "utf-8").match(/^---\r?\n([\s\S]*?)\r?\n---/);
221
221
  if (!fm) {
222
222
  errors.push(`${rel(skillFile)}: missing YAML frontmatter`);
223
223
  continue;
@@ -2,18 +2,13 @@
2
2
  /**
3
3
  * `sailor init` smoke test.
4
4
  *
5
- * Scaffolds a fresh project from the in-tree CLI bundle into a temp dir and
6
- * asserts the scaffold succeeded. This exists to catch the class of regression
7
- * the doc-drift gate structurally cannot — e.g. `packageRoot()` resolving to a
8
- * `bin.sailor` package that ships no `templates/`, which made `init` fail from a
9
- * monorepo checkout with "Template ... not found. Available: none".
5
+ * PASS 1 fresh init: scaffolds a new project and asserts expected files exist.
10
6
  *
11
- * It runs the REAL built bundle from a monorepo layout, which is exactly the
12
- * in-tree path that broke before. Pure Node + child_process; the only build
13
- * dependency is the CLI bundle (`pnpm --filter sailor build`).
7
+ * Template files are read live from disk (not bundled), so no rebuild is needed
8
+ * between runs the in-tree templates/default/ IS the "latest version".
14
9
  *
15
10
  * Run: node scripts/check-init.mjs (CI builds the CLI first)
16
- * Exit: 0 = scaffold OK, 1 = failure (prints what was missing).
11
+ * Exit: 0 = all passes OK, 1 = failure (prints what went wrong).
17
12
  */
18
13
 
19
14
  import { execFileSync } from "node:child_process";
@@ -65,6 +60,9 @@ try {
65
60
  "foundry.toml",
66
61
  "mandates",
67
62
  "AGENTS.md",
63
+ "CLAUDE.md",
64
+ "Dockerfile",
65
+ ".dockerignore",
68
66
  ".sail/contracts/interfaces/IPermission.sol",
69
67
  ".sail/contracts/interfaces/IBatchPermission.sol",
70
68
  "test/BoundedCallPermission.t.sol",
@@ -75,7 +73,11 @@ try {
75
73
  ".agents/skills/sail-transactions/SKILL.md",
76
74
  ".agents/skills/sail-mandates/SKILL.md",
77
75
  ".agents/skills/sail-mandates/references/approvals.md",
78
- ".agents/skills/sail-ci/SKILL.md",
76
+ ".agents/skills/sail-automation/SKILL.md",
77
+ ".agents/skills/sail-automation/references/docker-vm.md",
78
+ ".agents/skills/sail-automation/references/github-actions.md",
79
+ ".agents/skills/sail-automation/references/local-daemon.md",
80
+ ".agents/skills/sail-automation/references/self-hosted-runner.md",
79
81
  ".agents/skills/sail-extend/SKILL.md",
80
82
  ];
81
83
  for (const rel of mustExist) {
@@ -94,9 +96,10 @@ try {
94
96
  fail(`package.json still has "@sail/sdk": "workspace:*" — init did not resolve it`);
95
97
  }
96
98
 
97
- // Regression guard: an absolute path outside the cwd must be REJECTED, not
98
- // silently nested into `<cwd>/<abs path>`. (Pre-fix, `path.join` swallowed the
99
- // leading slash and scaffolded a bogus nested tree while printing success.)
99
+ // ── Regression guard: absolute path outside cwd ───────────────────────────
100
+ // An absolute path outside the cwd must be REJECTED, not silently nested into
101
+ // `<cwd>/<abs path>`. (Pre-fix, `path.join` swallowed the leading slash and
102
+ // scaffolded a bogus nested tree while printing success.)
100
103
  const outside = path.join(os.tmpdir(), "sailor-init-outside", "agent");
101
104
  let rejected = false;
102
105
  try {
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `sailor update` smoke tests.
4
+ *
5
+ * PASS 1 — standard update:
6
+ * Deletes a template-owned file and a user skill, runs `sailor update`,
7
+ * asserts the template file is restored and the user skill is untouched.
8
+ *
9
+ * PASS 2 — seeds missing user-space files:
10
+ * Deletes a user-space file (package.json) and dir (src/), runs update,
11
+ * asserts they are re-added. Asserts AGENTS.md / CLAUDE.md / Dockerfile
12
+ * are NOT overwritten when they already exist.
13
+ *
14
+ * PASS 3 — stale path pruning:
15
+ * Manually creates .agents/skills/sail-ci/ (orphan from old template version),
16
+ * runs update, asserts it is deleted and sail-automation is present.
17
+ *
18
+ * PASS 4 — init-on-existing errors:
19
+ * Runs `sailor init` inside the already-initialized project;
20
+ * asserts it exits non-zero with an "already initialized" message.
21
+ *
22
+ * Run: node scripts/check-update.mjs (CI builds the CLI first)
23
+ * Exit: 0 = all passes OK, 1 = failure (prints what went wrong).
24
+ */
25
+
26
+ import { execFileSync } from "node:child_process";
27
+ import fs from "node:fs";
28
+ import os from "node:os";
29
+ import path from "node:path";
30
+ import { fileURLToPath } from "node:url";
31
+
32
+ const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
33
+ const BUNDLE = path.join(ROOT, "packages/cli/dist/index.cjs");
34
+ const PROJECT = "smoke-update";
35
+
36
+ function fail(msg) {
37
+ console.error(`✗ update smoke test FAILED: ${msg}`);
38
+ process.exit(1);
39
+ }
40
+
41
+ if (!fs.existsSync(BUNDLE)) {
42
+ fail(`CLI bundle not found at ${BUNDLE}.\n Build it first: pnpm --filter sailor build`);
43
+ }
44
+
45
+ const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "sailor-update-smoke-"));
46
+ const dest = path.join(tmpRoot, PROJECT);
47
+
48
+ try {
49
+ // Bootstrap: fresh init so we have a valid project to update.
50
+ try {
51
+ execFileSync(process.execPath, [BUNDLE, "init", PROJECT], {
52
+ cwd: tmpRoot,
53
+ encoding: "utf-8",
54
+ stdio: ["ignore", "pipe", "pipe"],
55
+ });
56
+ } catch (err) {
57
+ const out = `${err.stdout ?? ""}${err.stderr ?? ""}`.trim();
58
+ fail(`bootstrap \`sailor init ${PROJECT}\` exited non-zero.\n ${out || err.message}`);
59
+ }
60
+
61
+ // ── PASS 1 — standard update ───────────────────────────────────────────────
62
+ // Delete a template-owned file and add a user skill that must survive update.
63
+ const templateOwned = path.join(dest, ".agents/skills/sail-automation/SKILL.md");
64
+ const cursorRules = path.join(dest, ".cursor/rules");
65
+ const userSkill = path.join(dest, ".agents/skills/my-custom-skill/SKILL.md");
66
+
67
+ fs.rmSync(templateOwned);
68
+ fs.rmSync(cursorRules);
69
+ fs.mkdirSync(path.dirname(userSkill), { recursive: true });
70
+ fs.writeFileSync(userSkill, "# custom skill\n", "utf-8");
71
+
72
+ try {
73
+ execFileSync(process.execPath, [BUNDLE, "update"], {
74
+ cwd: dest,
75
+ encoding: "utf-8",
76
+ stdio: ["ignore", "pipe", "pipe"],
77
+ });
78
+ } catch (err) {
79
+ const out = `${err.stdout ?? ""}${err.stderr ?? ""}`.trim();
80
+ fail(`Pass 1: \`sailor update\` exited non-zero.\n ${out || err.message}`);
81
+ }
82
+
83
+ if (!fs.existsSync(templateOwned))
84
+ fail(`Pass 1: update did not restore "${path.relative(dest, templateOwned)}"`);
85
+ if (!fs.existsSync(cursorRules))
86
+ fail(`Pass 1: update did not restore "${path.relative(dest, cursorRules)}"`);
87
+ if (!fs.existsSync(userSkill))
88
+ fail(`Pass 1: update removed user file "${path.relative(dest, userSkill)}" — must be preserved`);
89
+
90
+ console.log("✓ Pass 1 passed — template files restored, user skill preserved");
91
+
92
+ // ── PASS 2 — seeds missing user-space files ───────────────────────────────
93
+ const agentsMd = path.join(dest, "AGENTS.md");
94
+ const claudeMd = path.join(dest, "CLAUDE.md");
95
+ const dockerfile = path.join(dest, "Dockerfile");
96
+ const packageJson = path.join(dest, "package.json");
97
+ const srcDir = path.join(dest, "src");
98
+
99
+ // Record AGENTS.md content before update — it must not change.
100
+ const agentsContentBefore = fs.readFileSync(agentsMd, "utf-8");
101
+
102
+ fs.rmSync(packageJson);
103
+ fs.rmSync(srcDir, { recursive: true });
104
+
105
+ try {
106
+ execFileSync(process.execPath, [BUNDLE, "update"], {
107
+ cwd: dest,
108
+ encoding: "utf-8",
109
+ stdio: ["ignore", "pipe", "pipe"],
110
+ });
111
+ } catch (err) {
112
+ const out = `${err.stdout ?? ""}${err.stderr ?? ""}`.trim();
113
+ fail(`Pass 2: \`sailor update\` exited non-zero.\n ${out || err.message}`);
114
+ }
115
+
116
+ if (!fs.existsSync(packageJson))
117
+ fail(`Pass 2: update did not re-add missing "package.json"`);
118
+ if (!fs.existsSync(srcDir))
119
+ fail(`Pass 2: update did not re-add missing "src/" directory`);
120
+
121
+ // AGENTS.md, CLAUDE.md, Dockerfile must not be overwritten.
122
+ const agentsContentAfter = fs.readFileSync(agentsMd, "utf-8");
123
+ if (agentsContentAfter !== agentsContentBefore)
124
+ fail(`Pass 2: update overwrote "AGENTS.md" — user-space files must never be overwritten`);
125
+ if (!fs.existsSync(claudeMd))
126
+ fail(`Pass 2: "CLAUDE.md" is missing after update`);
127
+ if (!fs.existsSync(dockerfile))
128
+ fail(`Pass 2: "Dockerfile" is missing after update`);
129
+
130
+ console.log("✓ Pass 2 passed — missing user-space files seeded, existing ones untouched");
131
+
132
+ // ── PASS 3 — stale path pruning ───────────────────────────────────────────
133
+ const staleSkill = path.join(dest, ".agents/skills/sail-ci/SKILL.md");
134
+ fs.mkdirSync(path.dirname(staleSkill), { recursive: true });
135
+ fs.writeFileSync(staleSkill, "# old sail-ci skill\n", "utf-8");
136
+
137
+ try {
138
+ execFileSync(process.execPath, [BUNDLE, "update"], {
139
+ cwd: dest,
140
+ encoding: "utf-8",
141
+ stdio: ["ignore", "pipe", "pipe"],
142
+ });
143
+ } catch (err) {
144
+ const out = `${err.stdout ?? ""}${err.stderr ?? ""}`.trim();
145
+ fail(`Pass 3: \`sailor update\` exited non-zero.\n ${out || err.message}`);
146
+ }
147
+
148
+ if (fs.existsSync(path.join(dest, ".agents/skills/sail-ci")))
149
+ fail(`Pass 3: stale ".agents/skills/sail-ci" was not removed`);
150
+ if (!fs.existsSync(path.join(dest, ".agents/skills/sail-automation/SKILL.md")))
151
+ fail(`Pass 3: ".agents/skills/sail-automation/SKILL.md" missing after update`);
152
+
153
+ console.log("✓ Pass 3 passed — stale sail-ci skill pruned, sail-automation present");
154
+
155
+ // ── PASS 4 — init-on-existing errors ──────────────────────────────────────
156
+ let initRejected = false;
157
+ let initOutput = "";
158
+ try {
159
+ initOutput = execFileSync(process.execPath, [BUNDLE, "init"], {
160
+ cwd: dest,
161
+ encoding: "utf-8",
162
+ stdio: ["ignore", "pipe", "pipe"],
163
+ });
164
+ } catch (err) {
165
+ initRejected = true;
166
+ initOutput = `${err.stdout ?? ""}${err.stderr ?? ""}`.trim();
167
+ }
168
+
169
+ if (!initRejected)
170
+ fail(`Pass 4: \`sailor init\` on existing project did not exit non-zero — should refuse`);
171
+ if (!/already initialized/i.test(initOutput))
172
+ fail(`Pass 4: error message did not mention "already initialized".\n output: ${initOutput}`);
173
+
174
+ console.log("✓ Pass 4 passed — init on existing project correctly refused");
175
+ } finally {
176
+ fs.rmSync(tmpRoot, { recursive: true, force: true });
177
+ }
@@ -0,0 +1,50 @@
1
+ ---
2
+ name: sail-automation
3
+ description: Run the agent unattended — four options by reliability and infra overhead: (1) GitHub Actions cloud runner (zero infra, cron drifts), (2) self-hosted runner (reliable timing, user-managed machine), (3) Docker image — run locally or on any cloud VM via a registry, (4) local daemon on the project machine (no Docker, simplest). See references/ for each option. Use after sailor run --once works.
4
+ ---
5
+
6
+ # Sail automation — running the agent unattended
7
+
8
+ Confirm `sailor run --once` works first. Four options; pick by latency, infra comfort, and uptime needs:
9
+
10
+ | Option | Who it's for | Timing | Infra needed |
11
+ |---|---|---|---|
12
+ | [GitHub Actions](references/github-actions.md) | Any user, no infra knowledge | Low — cron drifts | None |
13
+ | [Self-hosted runner](references/self-hosted-runner.md) | Users who need time-precise execution | High — dedicated runner | Dedicated machine |
14
+ | [Docker](references/docker-vm.md) | Users with basic container knowledge | High — persistent process | Docker + any machine |
15
+ | [Local daemon](references/local-daemon.md) | Any user, runs on the project machine | Medium — depends on uptime | None |
16
+
17
+ ## Which option fits you?
18
+
19
+ **1. Do you want to run the agent in the cloud or on a local / dedicated machine?**
20
+
21
+ → **Local / dedicated machine**
22
+ The machine must be running 24/7 and connected to the internet — missed runs are silent.
23
+ Two options (no further questions needed):
24
+
25
+ - **[Local daemon](references/local-daemon.md)** — `sailor service install`. Simplest option: no extra tools, no extra steps. Runs directly in the project environment on the same machine.
26
+
27
+ - **[Docker](references/docker-vm.md)** — build the image and run it as a container on any machine that has Docker installed. Good if you want a portable setup or plan to move to a cloud deployment later — same image, no code changes.
28
+
29
+ → **Cloud**
30
+ Three options depending on timing requirements:
31
+
32
+ - **[GitHub Actions](references/github-actions.md)** — zero additional setup. Simplest cloud path. Execution timing is not guaranteed: cron drifts on GitHub's shared runners. Fine for daily DCA, hourly rebalances, treasury strategies.
33
+
34
+ - **Timing-sensitive (LP, liquidations, perps)?** Two options:
35
+
36
+ - **[Self-hosted runner](references/self-hosted-runner.md)** — install GitHub's runner on a machine you control (physical or cloud VM). Same workflow file as GitHub Actions, one line change. The runner picks up jobs immediately with no shared queue.
37
+
38
+ - **[Docker on a cloud VM](references/docker-vm.md)** — provision a VM on any provider, build the image locally, push to a registry, pull and run on the VM. Works with AWS, GCP, Azure, Oracle, Fly.io, and any other provider.
39
+
40
+ ## Cadence
41
+
42
+ Match the interval to volatility: **LP / perp → minutes; DCA / rebalance → daily; treasury → hourly–daily.** GitHub Actions cron is a *heartbeat/backstop* that drifts and skips under load — not low-latency; for that, use the self-hosted runner or local execution options.
43
+
44
+ ## Keys & trust
45
+
46
+ Cloud options commit only the **encrypted** keystore (`ci-keystore.json`); `SAIL_PASSPHRASE` is a secret, never committed. Local options (daemon, Docker) use keys already on disk — no export needed.
47
+
48
+ Regardless of host, the on-chain **mandate is the backstop** — it bounds the manager's permissions no matter where or how the agent runs.
49
+
50
+ A failing run's logs show the same stderr as the local runner (`reverted: <txHash>`, `skipped: no registered permission…`) — debug with the sail-transactions skill.
@@ -0,0 +1,113 @@
1
+ # Docker — run locally or deploy to any machine
2
+
3
+ **Who this is for:** users with basic Docker knowledge. Works on your local machine, a cloud VM, or any managed container service.
4
+
5
+ **Best for:** users who want a portable, self-contained execution environment — build once, run anywhere. Also the right choice if you want to test locally and then move to a cloud VM or container service without changing anything.
6
+
7
+ ---
8
+
9
+ ## Requirements
10
+
11
+ - Docker installed on the machine where the agent will run: https://docs.docker.com/get-docker/
12
+ - `ci-keystore.json` present in the project root — generate it with `sailor keys export-ci`
13
+ - `RPC_URL` and `SAIL_PASSPHRASE` available as environment variables (never baked into the image)
14
+
15
+ ---
16
+
17
+ ## Build the image
18
+
19
+ From the project root (where `Dockerfile` lives):
20
+
21
+ ```bash
22
+ docker build -t sailor-agent .
23
+ ```
24
+
25
+ ---
26
+
27
+ ## Run locally
28
+
29
+ ```bash
30
+ docker run -d --restart=always \
31
+ -e RPC_URL=<your-rpc-url> \
32
+ -e SAIL_PASSPHRASE=<your-passphrase> \
33
+ -e CHAIN_ID=8453 \
34
+ -e AGENT_INTERVAL=300 \
35
+ --name sailor-agent \
36
+ sailor-agent
37
+ ```
38
+
39
+ - `--restart=always` — Docker restarts the container automatically on crash or machine reboot (requires Docker daemon set to start on boot)
40
+ - `AGENT_INTERVAL` — seconds between runs; default 300 (5 min). Set to `60` for per-minute, `3600` for hourly, `86400` for daily
41
+ - Logs: `docker logs -f sailor-agent`
42
+ - Stop: `docker stop sailor-agent && docker rm sailor-agent`
43
+
44
+ ---
45
+
46
+ ## Push to a registry (to deploy elsewhere)
47
+
48
+ Build the image once, push to a registry, pull it on any machine.
49
+
50
+ **Docker Hub**
51
+
52
+ ```bash
53
+ docker tag sailor-agent <dockerhub-username>/sailor-agent:latest
54
+ docker push <dockerhub-username>/sailor-agent:latest
55
+ ```
56
+
57
+ **GitHub Container Registry (GHCR)**
58
+
59
+ ```bash
60
+ echo $GITHUB_TOKEN | docker login ghcr.io -u <github-username> --password-stdin
61
+ docker tag sailor-agent ghcr.io/<github-username>/sailor-agent:latest
62
+ docker push ghcr.io/<github-username>/sailor-agent:latest
63
+ ```
64
+
65
+ ---
66
+
67
+ ## Pull and run on any other machine
68
+
69
+ On the target machine (cloud VM, Raspberry Pi, VPS, etc.):
70
+
71
+ ```bash
72
+ docker pull <registry>/<image>:latest
73
+
74
+ docker run -d --restart=always \
75
+ -e RPC_URL=<your-rpc-url> \
76
+ -e SAIL_PASSPHRASE=<your-passphrase> \
77
+ -e CHAIN_ID=8453 \
78
+ -e AGENT_INTERVAL=300 \
79
+ <registry>/<image>:latest
80
+ ```
81
+
82
+ No code changes — the same image runs everywhere that has Docker.
83
+
84
+ ---
85
+
86
+ ## Use with managed container services
87
+
88
+ The same image works with any managed container service. Pass env vars through the service's UI — no code changes needed.
89
+
90
+ | Service | Notes |
91
+ |---|---|
92
+ | AWS ECS / Fargate | Task definition with env vars; Fargate = no VM to manage |
93
+ | Google Cloud Run | Trigger on schedule via Cloud Scheduler |
94
+ | Azure Container Instances | Simple one-off or always-on container |
95
+ | Fly.io | `fly launch` + set secrets via `fly secrets set` |
96
+ | Railway | Point to image in registry, set env vars in dashboard |
97
+ | Render | Background worker from Docker image |
98
+
99
+ ---
100
+
101
+ ## Updating the agent
102
+
103
+ When you change your strategy code:
104
+
105
+ ```bash
106
+ docker build -t sailor-agent .
107
+ docker tag sailor-agent <registry>/<image>:latest
108
+ docker push <registry>/<image>:latest
109
+ # on the target machine:
110
+ docker pull <registry>/<image>:latest
111
+ docker stop sailor-agent && docker rm sailor-agent
112
+ docker run -d --restart=always -e ... <registry>/<image>:latest
113
+ ```
@@ -0,0 +1,50 @@
1
+ # GitHub Actions — cloud-managed runner
2
+
3
+ **Who this is for:** any user. No infrastructure setup beyond a GitHub account.
4
+
5
+ **Best for:** daily DCA, slow rebalances, treasury strategies — anything where execution does not need to happen at a precise minute. If your strategy requires guaranteed timing (LP, perps, liquidations), use the [self-hosted runner](self-hosted-runner.md) or [Docker](docker-vm.md) options instead.
6
+
7
+ ---
8
+
9
+ ## How it works
10
+
11
+ Your repo contains `.github/workflows/agent-tick.yml`. GitHub runs this on a cron schedule using their shared runner pool and via `workflow_dispatch` for on-demand or externally triggered runs.
12
+
13
+ ## Timing limitation
14
+
15
+ GitHub's cron queue is shared across all users. Under load, scheduled jobs drift 5–30 minutes or are skipped entirely. This is a platform constraint — there is no workaround on GitHub's shared runners.
16
+
17
+ **Use `workflow_dispatch` as your primary trigger** and treat cron as a heartbeat/backstop. Fire `workflow_dispatch` from an external event (price alert, on-chain event, keeper) via:
18
+
19
+ ```bash
20
+ sailor trigger github
21
+ ```
22
+
23
+ ## Setup
24
+
25
+ Full setup walkthrough is in the main `sail-automation` skill. Summary:
26
+
27
+ 1. `sailor keys export-ci` — generates `ci-keystore.json` (encrypted; safe to commit)
28
+ 2. Commit state files and push
29
+ 3. Set `SAIL_PASSPHRASE` and `RPC_URL` as GitHub Actions secrets
30
+ 4. The workflow runs on the next cron tick or on `workflow_dispatch`
31
+
32
+ ```bash
33
+ gh secret set SAIL_PASSPHRASE
34
+ gh secret set RPC_URL
35
+ gh workflow run agent-tick.yml # trigger a manual run to verify
36
+ gh run view --log # check output
37
+ ```
38
+
39
+ ## Cron cadence
40
+
41
+ Edit the `cron:` line in `.github/workflows/agent-tick.yml`:
42
+
43
+ ```yaml
44
+ schedule:
45
+ - cron: "0 * * * *" # hourly (default placeholder — change this)
46
+ # - cron: "*/5 * * * *" # every 5 min (only reliable with a self-hosted runner)
47
+ # - cron: "0 0 * * *" # daily
48
+ ```
49
+
50
+ For sub-hourly cadence on GitHub's shared runners, the drift makes the schedule unreliable. Use the self-hosted runner option for that.
@@ -0,0 +1,34 @@
1
+ # Local daemon — run on the project machine
2
+
3
+ **Who this is for:** any user who wants simple automation without cloud accounts or containers. The agent runs directly on the machine where the project lives.
4
+
5
+ **Best for:** development, testing, daily/slow strategies, or users who keep their machine on 24/7. Not suitable for strategies that require guaranteed execution at a precise time — if the machine is off or disconnected, the run is silently skipped.
6
+
7
+ ---
8
+
9
+ ## Setup
10
+
11
+ Installs a system service that runs `sailor run --once` on a fixed interval. No keystore export or CI variable setup needed — the keys are already on disk in the project environment.
12
+
13
+ ```bash
14
+ sailor service install --interval 300 # every 5 min; tune to your strategy
15
+ sailor service status # check running state
16
+ sailor service logs -f # tail .sail/agent.log
17
+ sailor service stop # pause without uninstalling
18
+ sailor service uninstall # remove the service entirely
19
+ ```
20
+
21
+ The service is installed as:
22
+ - **macOS** — a `launchd` plist in `~/Library/LaunchAgents/`
23
+ - **Linux** — a `systemd` user unit
24
+ - **Windows** — a Task Scheduler entry
25
+
26
+ `--project` and `--chain` scope the service to a specific project or chain. `--force` overrides a TCC path error or unresolved passphrase prompt.
27
+
28
+ ## Limitations
29
+
30
+ - The machine must be powered on and internet-connected when the job fires
31
+ - Missed runs are silent — there is no retry
32
+ - Not suitable for 24/7 strategies unless the machine is always on
33
+
34
+ If you want a portable setup that can move to a cloud deployment later, use [Docker](docker-vm.md) instead — same machine, same result, but the image runs anywhere.
@@ -0,0 +1,72 @@
1
+ # Self-hosted runner — reliable timing on a dedicated machine
2
+
3
+ **Who this is for:** users who need time-precise execution. Requires a dedicated always-on machine that you manage.
4
+
5
+ **Best for:** LP strategies, perps, liquidations, or any strategy where the agent must run within seconds of the scheduled time. A self-hosted runner polls GitHub directly — it picks up jobs immediately, with no shared queue and no drift.
6
+
7
+ ---
8
+
9
+ ## How it works
10
+
11
+ Same `agent-tick.yml` workflow as the GitHub Actions option. One change: `runs-on: ubuntu-latest` becomes `runs-on: [self-hosted, linux]`. The job then runs on your machine instead of GitHub's shared pool.
12
+
13
+ ## Prerequisites
14
+
15
+ - A machine that is **always powered on** and **always connected to the internet**
16
+ - Do not use your personal computer — missed runs happen silently whenever it sleeps, restarts, or loses connectivity
17
+ - Recommended hardware: Raspberry Pi 4+, a dedicated mini PC (NUC, Intel BRIX, etc.), or a cloud VM on any provider
18
+
19
+ ## Setup
20
+
21
+ ### 1. Register the runner on GitHub
22
+
23
+ Go to your repo: **Settings → Actions → Runners → New self-hosted runner**
24
+
25
+ Follow the official GitHub guide for your OS:
26
+ https://docs.github.com/es/actions/how-tos/manage-runners/self-hosted-runners/add-runners
27
+
28
+ The guide walks you through downloading the runner application, configuring it, and starting it as a service so it restarts automatically on reboot.
29
+
30
+ ### 2. Update the workflow
31
+
32
+ In your local copy of `.github/workflows/agent-tick.yml`, change:
33
+
34
+ ```yaml
35
+ jobs:
36
+ tick:
37
+ runs-on: ubuntu-latest # ← change this
38
+ ```
39
+
40
+ to:
41
+
42
+ ```yaml
43
+ jobs:
44
+ tick:
45
+ runs-on: [self-hosted, linux] # ← your runner label
46
+ ```
47
+
48
+ Commit and push. The next run will be picked up by your runner.
49
+
50
+ ### 3. Verify
51
+
52
+ ```bash
53
+ gh run list --workflow agent-tick.yml # confirm runs show "self-hosted" runner
54
+ gh run view --log # check for errors
55
+ ```
56
+
57
+ ## Machine options
58
+
59
+ | Machine | Cost | Notes |
60
+ |---|---|---|
61
+ | Raspberry Pi 4 (2 GB+) | ~$45 one-time | Runs 24/7 on ~3W; plug into router via ethernet |
62
+ | Mini PC (NUC, BRIX) | ~$100–200 | More headroom; good if you run other services too |
63
+ | Cloud VM (Oracle Always Free) | Free | 2 AMD VMs permanently free; requires basic Linux knowledge |
64
+ | Cloud VM (any provider) | ~$4–10/month | AWS t3.micro, GCP e2-micro, Hetzner CX11, etc. |
65
+
66
+ ## Your responsibility
67
+
68
+ Sail does not manage this machine. You are responsible for:
69
+ - Keeping it powered on and connected
70
+ - OS updates and security patches
71
+ - Restarting the runner service if it stops (configure it to start on boot during setup)
72
+ - Monitoring that runs are completing as expected
@@ -9,6 +9,8 @@ description: Walks the agent through setting up a new Sailor project or resuming
9
9
 
10
10
  The project does not install `sailor` as a dependency, so invoke it with **`npx sailor <command>`** unless it is installed globally (`npm i -g @sail.money/sailor`, then `sailor` works bare). Every `sailor …` command in these skills assumes one of those. Confirm the toolchain up front and pin a recent version — `npx sailor@latest --version` — because an old cached `npx` build can be missing newer commands (e.g. `mandate simulate`); if a documented command reports "unknown command", you are on a stale version, not hitting a missing feature.
11
11
 
12
+ After upgrading the CLI, run `sailor update` from the project root to pull in updated skills, `AGENTS.md`, `Dockerfile`, and other tooling files. User files (`src/`, `mandates/`, `.sail/`, `package.json`) are never touched.
13
+
12
14
  Stage machine keyed off `.sail/`. Read the state, enter at the right stage, never re-run completed stages.
13
15
 
14
16
  ## Determine where the user is
@@ -64,7 +64,7 @@ Detailed procedures live in skills. If your tooling does not auto-discover skill
64
64
  | sail-servers | Starting, stopping, or health-checking the dashboard or signing station | `.agents/skills/sail-servers/SKILL.md` |
65
65
  | sail-transactions | Building dispatches or any EVM transaction for the agent | `.agents/skills/sail-transactions/SKILL.md` |
66
66
  | sail-mandates | Designing, authoring, testing, deploying, or authorizing permission contracts | `.agents/skills/sail-mandates/SKILL.md` |
67
- | sail-ci | Automating the agent on a schedule via GitHub Actions | `.agents/skills/sail-ci/SKILL.md` |
67
+ | sail-automation | Automating the agent GitHub Actions, self-hosted runner, Docker, or local daemon | `.agents/skills/sail-automation/SKILL.md` |
68
68
  | sail-extend | Notifications or a custom dashboard, once the agent is live | `.agents/skills/sail-extend/SKILL.md` |
69
69
 
70
70
  ## Invariants — apply to every turn
@@ -0,0 +1,18 @@
1
+ FROM node:20-slim
2
+ WORKDIR /app
3
+
4
+ COPY package*.json ./
5
+ RUN npm install
6
+
7
+ COPY . .
8
+
9
+ # Seconds between runs. Tune to your strategy: 60 = per-minute, 300 = 5 min, 86400 = daily.
10
+ ENV AGENT_INTERVAL=300
11
+
12
+ CMD ["sh", "-c", "\
13
+ mkdir -p .sail/keys && \
14
+ cp ci-keystore.json .sail/keys/manager.json && \
15
+ while true; do \
16
+ npx sailor run --once; \
17
+ sleep ${AGENT_INTERVAL}; \
18
+ done"]
@@ -0,0 +1,15 @@
1
+ # Secrets and local state — never bake into the image.
2
+ .sail/keys/
3
+ .sail/runtime/
4
+ .sail/state/
5
+ .sail/.env.local
6
+ .env
7
+ .env.*
8
+ !.env.example
9
+
10
+ # Build outputs
11
+ node_modules/
12
+ out/
13
+ cache/
14
+ broadcast/
15
+ dist/