mobile-agent-orchestrator 0.0.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +270 -2
- package/bin/mobile-agent-orchestrator.mjs +513 -0
- package/lib/detect.mjs +117 -0
- package/lib/hosts.mjs +48 -0
- package/lib/installer.mjs +934 -0
- package/lib/prompt.mjs +250 -0
- package/package.json +36 -8
- package/skills/mobile-agent-orchestrator/SKILL.md +48 -0
- package/skills/mobile-agent-orchestrator/references/agent-hosts.md +45 -0
- package/skills/mobile-agent-orchestrator/references/guided-install.md +89 -0
- package/skills/mobile-agent-orchestrator/references/platform-matrix.md +73 -0
- package/skills/mobile-agent-orchestrator/references/verification-and-recovery.md +64 -0
package/lib/prompt.mjs
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { resolveUniqueDestinations, targets as writableTargets } from "./hosts.mjs";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Skill directory name written under each destination. Kept in sync with `SKILL_DIRECTORY` in
|
|
5
|
+
* lib/hosts.mjs (not exported there).
|
|
6
|
+
*/
|
|
7
|
+
const SKILL_DIRECTORY = "mobile-agent-orchestrator";
|
|
8
|
+
|
|
9
|
+
/** Exact install suggestion rendered for Pi, which is detect-only and never a write target. */
|
|
10
|
+
const PI_INSTALL_COMMAND = "pi install npm:mobile-agent-orchestrator";
|
|
11
|
+
|
|
12
|
+
/** Destination hints shown next to the scope choices. */
|
|
13
|
+
const SCOPE_HINTS = {
|
|
14
|
+
user: "~/.agents/skills and ~/.claude/skills",
|
|
15
|
+
project: "<cwd>/.agents/skills and <cwd>/.claude/skills",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Default destination renderer: groups via the real dedup logic in lib/hosts.mjs using display
|
|
20
|
+
* label roots ("~" / "<cwd>"), then renders portable forward-slash labels. This module never
|
|
21
|
+
* touches the filesystem, so the label roots are never resolved as real paths by the caller.
|
|
22
|
+
*/
|
|
23
|
+
function defaultDescribeDestinations(selectedTargets, scope) {
|
|
24
|
+
const base = scope === "user" ? "~" : "<cwd>";
|
|
25
|
+
const groups = resolveUniqueDestinations(selectedTargets, scope, { home: "/~", cwd: "/<cwd>" });
|
|
26
|
+
return groups.map((group) => {
|
|
27
|
+
const segments = group.destination.split(/[\\/]+/);
|
|
28
|
+
const directory = segments.includes(".claude") ? ".claude/skills" : ".agents/skills";
|
|
29
|
+
return `${base}/${directory}/${SKILL_DIRECTORY} (${group.targets.join(", ")})`;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Minimal line reader over an injected readable stream. Answers are never echoed: only the
|
|
35
|
+
* prompt strings are written to `output`, and input is consumed silently. A trailing line
|
|
36
|
+
* without a newline is delivered once at end-of-input (mirroring node:readline); every read
|
|
37
|
+
* after end-of-input resolves to `null`, which callers treat as cancellation.
|
|
38
|
+
*/
|
|
39
|
+
function createLineReader(input) {
|
|
40
|
+
let buffer = "";
|
|
41
|
+
let ended = false;
|
|
42
|
+
let pending = null;
|
|
43
|
+
|
|
44
|
+
const settle = (line) => {
|
|
45
|
+
const reader = pending;
|
|
46
|
+
pending = null;
|
|
47
|
+
reader.resolve(line);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const flush = () => {
|
|
51
|
+
if (!pending) return;
|
|
52
|
+
const newlineIndex = buffer.indexOf("\n");
|
|
53
|
+
if (newlineIndex !== -1) {
|
|
54
|
+
const line = buffer.slice(0, newlineIndex).replace(/\r$/, "");
|
|
55
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
56
|
+
settle(line);
|
|
57
|
+
} else if (ended) {
|
|
58
|
+
const line = buffer.length > 0 ? buffer.replace(/\r$/, "") : null;
|
|
59
|
+
buffer = "";
|
|
60
|
+
settle(line);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const onData = (chunk) => {
|
|
65
|
+
buffer += chunk.toString();
|
|
66
|
+
flush();
|
|
67
|
+
};
|
|
68
|
+
const onEnd = () => {
|
|
69
|
+
ended = true;
|
|
70
|
+
flush();
|
|
71
|
+
};
|
|
72
|
+
const onError = () => {
|
|
73
|
+
ended = true;
|
|
74
|
+
flush();
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
input.on("data", onData);
|
|
78
|
+
input.on("end", onEnd);
|
|
79
|
+
input.on("error", onError);
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
readLine() {
|
|
83
|
+
if (pending) throw new Error("Concurrent readLine calls are not supported.");
|
|
84
|
+
return new Promise((resolve) => {
|
|
85
|
+
pending = { resolve };
|
|
86
|
+
flush();
|
|
87
|
+
});
|
|
88
|
+
},
|
|
89
|
+
/**
|
|
90
|
+
* Detach from the input stream: remove every listener this reader attached, settle any
|
|
91
|
+
* pending read (as cancellation, i.e. `null`), and stop pumping the stream. Callers must
|
|
92
|
+
* invoke this on every exit path; otherwise listeners left on a terminal-backed stdin keep
|
|
93
|
+
* the process alive after the flow completes.
|
|
94
|
+
*/
|
|
95
|
+
dispose() {
|
|
96
|
+
input.removeListener("data", onData);
|
|
97
|
+
input.removeListener("end", onEnd);
|
|
98
|
+
input.removeListener("error", onError);
|
|
99
|
+
if (pending) settle(null);
|
|
100
|
+
if (typeof input.pause === "function") input.pause();
|
|
101
|
+
// Unref-style cleanup only fires on real handle-backed streams (e.g. process.stdin);
|
|
102
|
+
// in-memory fixtures such as PassThrough/Readable have no unref and are unaffected.
|
|
103
|
+
if (typeof input.unref === "function") input.unref();
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Split hosts into the rendered list lines and the ordered selectable target ids. Pi is rendered
|
|
110
|
+
* as an informational line with the supported install command and is never selectable. Unknown
|
|
111
|
+
* host ids are ignored (the prompt module is a pass-through over detection results).
|
|
112
|
+
*/
|
|
113
|
+
function renderHostList(hosts) {
|
|
114
|
+
const knownTargets = new Set(writableTargets);
|
|
115
|
+
const lines = [];
|
|
116
|
+
const selectable = [];
|
|
117
|
+
for (const host of hosts) {
|
|
118
|
+
if (host.id === "pi") {
|
|
119
|
+
lines.push(` pi - detect-only; install with: ${PI_INSTALL_COMMAND}`);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (!knownTargets.has(host.id)) continue;
|
|
123
|
+
const number = selectable.length + 1;
|
|
124
|
+
selectable.push(host.id);
|
|
125
|
+
const status = host.detected
|
|
126
|
+
? `detected${host.evidence ? ` (${host.evidence})` : ""}`
|
|
127
|
+
: "not detected";
|
|
128
|
+
lines.push(` [${number}] ${host.id} - ${status}`);
|
|
129
|
+
}
|
|
130
|
+
return { lines, selectable };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Parse a multi-select answer. Accepts the alias "a"/"all" (case-insensitive) for every
|
|
135
|
+
* selectable target, or comma-separated numbers. Duplicates collapse; the result is normalized
|
|
136
|
+
* to the canonical selectable order. Returns null when any token is invalid (no partial
|
|
137
|
+
* acceptance of garbage).
|
|
138
|
+
*/
|
|
139
|
+
function parseSelection(line, selectable) {
|
|
140
|
+
const normalized = line.trim().toLowerCase();
|
|
141
|
+
if (normalized === "a" || normalized === "all") return [...selectable];
|
|
142
|
+
if (normalized.length === 0) return null;
|
|
143
|
+
const chosen = new Set();
|
|
144
|
+
for (const token of normalized.split(",")) {
|
|
145
|
+
if (!/^\d+$/.test(token.trim())) return null;
|
|
146
|
+
const number = Number(token.trim());
|
|
147
|
+
if (number < 1 || number > selectable.length) return null;
|
|
148
|
+
chosen.add(selectable[number - 1]);
|
|
149
|
+
}
|
|
150
|
+
return selectable.filter((id) => chosen.has(id));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Parse a scope answer ("1"/"user" or "2"/"project", case-insensitive); null when invalid. */
|
|
154
|
+
function parseScope(line) {
|
|
155
|
+
const normalized = line.trim().toLowerCase();
|
|
156
|
+
if (normalized === "1" || normalized === "user") return "user";
|
|
157
|
+
if (normalized === "2" || normalized === "project") return "project";
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Deterministic interactive selection flow with fully injected streams. Renders the detected
|
|
163
|
+
* hosts, asks for targets, scope, and confirmation, and returns `{ targets, scope }` on
|
|
164
|
+
* confirmation or `null` on cancellation (end-of-input at any prompt, or any non-confirm answer
|
|
165
|
+
* at the confirmation step). Reads no filesystem state beyond its arguments and performs no
|
|
166
|
+
* writes; the caller owns all filesystem effects. The input stream is always released before
|
|
167
|
+
* the returned promise settles: every exit path disposes the line reader (removing its
|
|
168
|
+
* listeners and stopping the stream) so an open terminal-backed stdin cannot keep the process
|
|
169
|
+
* alive.
|
|
170
|
+
*
|
|
171
|
+
* @param {{
|
|
172
|
+
* hosts: Array<{ id: string, detected: boolean, evidence: string }>,
|
|
173
|
+
* input: import("node:stream").Readable,
|
|
174
|
+
* output: import("node:stream").Writable,
|
|
175
|
+
* describeDestinations?: (selectedTargets: string[], scope: string) => string[],
|
|
176
|
+
* }} options
|
|
177
|
+
* @returns {Promise<{ targets: string[], scope: string } | null>}
|
|
178
|
+
*/
|
|
179
|
+
export async function selectInstallPlan({
|
|
180
|
+
hosts,
|
|
181
|
+
input,
|
|
182
|
+
output,
|
|
183
|
+
describeDestinations = defaultDescribeDestinations,
|
|
184
|
+
}) {
|
|
185
|
+
if (!Array.isArray(hosts)) {
|
|
186
|
+
throw new TypeError("selectInstallPlan requires a hosts array from detectHosts().");
|
|
187
|
+
}
|
|
188
|
+
if (!input || typeof input.on !== "function") {
|
|
189
|
+
throw new TypeError("selectInstallPlan requires an injectable input stream.");
|
|
190
|
+
}
|
|
191
|
+
if (!output || typeof output.write !== "function") {
|
|
192
|
+
throw new TypeError("selectInstallPlan requires an injectable output stream.");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
output.write("Detected coding-agent CLIs:\n");
|
|
196
|
+
const { lines, selectable } = renderHostList(hosts);
|
|
197
|
+
for (const line of lines) output.write(`${line}\n`);
|
|
198
|
+
if (selectable.length === 0) {
|
|
199
|
+
output.write("No installable coding-agent targets are supported.\n");
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const reader = createLineReader(input);
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
let selected = null;
|
|
207
|
+
while (selected === null) {
|
|
208
|
+
output.write(
|
|
209
|
+
`Select targets to install (comma-separated numbers 1-${selectable.length}, or "a" for all): `,
|
|
210
|
+
);
|
|
211
|
+
const answer = await reader.readLine();
|
|
212
|
+
if (answer === null) return null;
|
|
213
|
+
selected = parseSelection(answer, selectable);
|
|
214
|
+
if (selected === null) {
|
|
215
|
+
output.write(
|
|
216
|
+
`Invalid selection. Enter comma-separated numbers between 1 and ${selectable.length}, or "a" for all.\n`,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
output.write("\nSelect scope:\n");
|
|
222
|
+
output.write(` [1] user - installs to ${SCOPE_HINTS.user}\n`);
|
|
223
|
+
output.write(` [2] project - installs to ${SCOPE_HINTS.project}\n`);
|
|
224
|
+
let scope = null;
|
|
225
|
+
while (scope === null) {
|
|
226
|
+
output.write("Select scope (1/2): ");
|
|
227
|
+
const answer = await reader.readLine();
|
|
228
|
+
if (answer === null) return null;
|
|
229
|
+
scope = parseScope(answer);
|
|
230
|
+
if (scope === null) {
|
|
231
|
+
output.write('Invalid scope. Enter "1" (user) or "2" (project).\n');
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
output.write("\nInstall plan:\n");
|
|
236
|
+
output.write(` scope: ${scope}\n`);
|
|
237
|
+
output.write(" destinations:\n");
|
|
238
|
+
for (const line of describeDestinations(selected, scope)) {
|
|
239
|
+
output.write(` ${line}\n`);
|
|
240
|
+
}
|
|
241
|
+
output.write("Proceed? [y/N] ");
|
|
242
|
+
const confirmation = await reader.readLine();
|
|
243
|
+
if (confirmation === null) return null;
|
|
244
|
+
if (!/^(?:y|yes)$/i.test(confirmation.trim())) return null;
|
|
245
|
+
return { targets: selected, scope };
|
|
246
|
+
} finally {
|
|
247
|
+
// Every exit path (confirmation, cancellation, EOF, thrown error) must release the input.
|
|
248
|
+
reader.dispose();
|
|
249
|
+
}
|
|
250
|
+
}
|
package/package.json
CHANGED
|
@@ -1,17 +1,45 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mobile-agent-orchestrator",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "A guided Agent Skill for a private, recoverable mobile AI-agent environment.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"private": false,
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"registry": "https://registry.npmjs.org/",
|
|
9
|
+
"access": "public"
|
|
10
|
+
},
|
|
6
11
|
"author": "egdev6",
|
|
7
12
|
"repository": {
|
|
8
13
|
"type": "git",
|
|
9
|
-
"url": "https://github.com/egdev6/mobile-agent-orchestrator"
|
|
14
|
+
"url": "git+https://github.com/egdev6/mobile-agent-orchestrator.git"
|
|
10
15
|
},
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
"homepage": "https://github.com/egdev6/mobile-agent-orchestrator",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/egdev6/mobile-agent-orchestrator/issues"
|
|
19
|
+
},
|
|
20
|
+
"bin": {
|
|
21
|
+
"mobile-agent-orchestrator": "bin/mobile-agent-orchestrator.mjs"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"skills/",
|
|
25
|
+
"bin/",
|
|
26
|
+
"lib/"
|
|
27
|
+
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"validate": "node scripts/validate-package.mjs",
|
|
30
|
+
"pack:check": "npm pack --dry-run --json",
|
|
31
|
+
"test": "npm run validate && node --test"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"pi-package",
|
|
35
|
+
"pi-skill",
|
|
36
|
+
"mobile-agent",
|
|
37
|
+
"tailscale",
|
|
38
|
+
"mosh"
|
|
39
|
+
],
|
|
40
|
+
"pi": {
|
|
41
|
+
"skills": [
|
|
42
|
+
"./skills"
|
|
43
|
+
]
|
|
16
44
|
}
|
|
17
45
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mobile-agent-orchestrator
|
|
3
|
+
description: "Trigger: mobile agent setup, mobile orchestrator, Moshi, Mosh, Tailscale, remote mobile agent. Guide a human through a safe, host-aware mobile AI-agent installation."
|
|
4
|
+
license: MIT
|
|
5
|
+
metadata:
|
|
6
|
+
author: egdev6
|
|
7
|
+
version: "0.2.0"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Activation Contract
|
|
11
|
+
|
|
12
|
+
Use for human-supervised installation of a mobile AI-agent environment and its host-supported lifecycle or checkpoint path. This is an Agent Skill, not an MCP server. Support only macOS native or Windows 11 with WSL2 (Windows lifecycle, WSL Linux execution). Detect state read-only before change. On Windows native without WSL, stop and guide WSL2; never offer degraded native Windows. Do not assume that the invoking CLI provides Pi's persistence or lifecycle behavior.
|
|
13
|
+
|
|
14
|
+
## Hard Rules
|
|
15
|
+
|
|
16
|
+
Never request or persist private keys, pairing tokens, sign-in URLs, QR contents, or credentials. Use device-local pairing or hidden terminal input; QR data is temporary secret material. Never open public SSH or router ports. For full Mosh, run Tailscale exactly once: on macOS or WSL, never Windows and WSL together. Disable Tailscale SSH with Moshi/OpenSSH key auth. Inspect remote installers before execution or prefer signed package managers.
|
|
17
|
+
|
|
18
|
+
Ask one question only when detection leaves a real decision or authorization. Obtain explicit approval before elevated installs, firewall/auth changes, cloud pairing, scheduled tasks, reboot, or destructive rollback. Configure firewall restrictions before enabling services, preserve local recovery, and validate SSH syntax before restart. Do not claim live processes, Herdr/tmux, or systemd survive physical reboot. Use only observed, host-specific checkpoint or resume evidence; Pi JSONL resume behavior is a Pi-specific capability and must not be generalized.
|
|
19
|
+
|
|
20
|
+
## Decision Gates
|
|
21
|
+
|
|
22
|
+
| Detected condition | Required action |
|
|
23
|
+
| --- | --- |
|
|
24
|
+
| Unsupported platform or missing WSL2 | Stop with the supported-path guidance. |
|
|
25
|
+
| Existing service, key, firewall, or Tailscale state | Present read-only evidence and ask whether to preserve, repair, or stop. |
|
|
26
|
+
| Installation, pairing, hardening, task, or reboot needed | Explain impact, rollback, and request the single required approval. |
|
|
27
|
+
| Key choice | Ask Easy Pair versus manual Ed25519; harden only after a tested key. |
|
|
28
|
+
| Moshi setup | Ask minimal versus full privacy consent, then integrations and notifications separately. |
|
|
29
|
+
|
|
30
|
+
## Execution Steps
|
|
31
|
+
|
|
32
|
+
1. Follow the platform matrix and record only non-sensitive state.
|
|
33
|
+
2. Establish permissions, rollback, local recovery, and network exposure boundaries.
|
|
34
|
+
3. Install and verify OpenSSH, Tailscale, mobile clients, Moshi, `moshi-hook`, Mosh, and Herdr or tmux using the guided reference.
|
|
35
|
+
4. Test key authentication before optional strict SSH hardening; keep Tailscale SSH off.
|
|
36
|
+
5. Configure the approved lifecycle path, privacy level, integrations, notifications, and the host-supported checkpoint/resume procedure. On Pi, this may be the supported local JSONL procedure; do not invent an equivalent for another CLI.
|
|
37
|
+
6. Run disconnect and approved reboot recovery verification. Mark cold-boot behavior as pending whenever observed evidence is unavailable.
|
|
38
|
+
|
|
39
|
+
## Output Contract
|
|
40
|
+
|
|
41
|
+
Report platform, detected state, approvals granted or declined, mutations performed, non-sensitive verification evidence, rollback path, and pending recovery evidence. State exactly why a step stopped. Never report a reboot or service-survival claim without observed evidence.
|
|
42
|
+
|
|
43
|
+
## References
|
|
44
|
+
|
|
45
|
+
- [Platform matrix](references/platform-matrix.md)
|
|
46
|
+
- [Guided install](references/guided-install.md)
|
|
47
|
+
- [Verification and recovery](references/verification-and-recovery.md)
|
|
48
|
+
- [Agent hosts](references/agent-hosts.md)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Agent hosts reference
|
|
2
|
+
|
|
3
|
+
Compatibility claims in this file are limited to what current official host documentation states. Behavior that official documentation does not confirm is marked **Unverified** and must be observed locally before being relied on. Skill locations do not imply lifecycle guarantees: session persistence and resume behavior are host-specific.
|
|
4
|
+
|
|
5
|
+
This skill is one canonical Agent Skill directory (`SKILL.md` plus on-demand references). Every host below can discover the same skill content through its own documented locations; no host-specific behavior is inferred from compatibility alone.
|
|
6
|
+
|
|
7
|
+
## Host skill locations
|
|
8
|
+
|
|
9
|
+
| Host | Project scope | User scope | Source |
|
|
10
|
+
| --- | --- | --- | --- |
|
|
11
|
+
| Pi | `.pi/skills/`; Agent Skills root `.agents/skills/` (project discovery walks ancestors up to the repository root). The `skills/` directory or `pi.skills` array in `package.json` is package discovery for `pi install`, not a general project skill root | `~/.pi/agent/skills/` (default agent directory) and `~/.agents/skills/`; additional directories via the `skills` setting in `settings.json` | Pi documentation; `pi install npm:...|git:...|./local` for package installs |
|
|
12
|
+
| Claude Code | `.claude/skills/<name>/SKILL.md` | `~/.claude/skills/<name>/SKILL.md` | Claude Code docs, "Skills" |
|
|
13
|
+
| Codex CLI | `.agents/skills/` at the launch directory, parent directories, and the repository root | `$HOME/.agents/skills/` | Codex docs, "Agent Skills" |
|
|
14
|
+
| Gemini CLI | `.gemini/skills/` with `.agents/skills/` as an interoperable alias | `~/.gemini/skills/` with `~/.agents/skills/` as an interoperable alias | Gemini CLI docs, "Agent Skills" |
|
|
15
|
+
| OpenCode | `.opencode/skills/`, `.claude/skills/`, `.agents/skills/` | `~/.config/opencode/skills/`, `~/.claude/skills/`, `~/.agents/skills/` | OpenCode docs, "Skills" |
|
|
16
|
+
|
|
17
|
+
All five hosts implement the open [Agent Skills specification](https://agentskills.io/specification): one directory per skill containing a `SKILL.md` with YAML frontmatter (`name`, `description` required). Within this repository, Pi package discovery stays on the `pi.skills: ./skills` manifest and must not be changed; other hosts copy or link the same `skills/mobile-agent-orchestrator/` directory into one of the documented roots above.
|
|
18
|
+
|
|
19
|
+
### Host-specific notes
|
|
20
|
+
|
|
21
|
+
- **Pi.** Project `.agents/skills/` discovery walks from the working directory through ancestors and stops at the repository root. Personal package installs are recorded in `~/.pi/agent/settings.json`; project package installs in `.pi/settings.json` after project trust. Pi session persistence is JSONL under `~/.pi/agent/sessions/`, with `--continue`, `--resume`, and `/fork`. **This JSONL resume behavior is Pi-specific and must not be generalized to any other host.**
|
|
22
|
+
- **Claude Code.** Project skills load from `.claude/skills/` in the launch directory and every parent up to the repository root; nested skills activate when Claude works on files in their subtree. Symlinked skill folders are read from the symlink target. Personal skills in `~/.claude/skills/` do not load in Cowork or cloud sessions. Transcript-based resume uses `claude --continue` / `claude --resume`.
|
|
23
|
+
- **Codex CLI.** Codex scans `.agents/skills` from the launch directory up to the repository root and reads `$HOME/.agents/skills` for user scope; it follows symlinked skill folders. Skills sharing a name are not merged; both appear in selectors. Session resume uses `codex resume`.
|
|
24
|
+
- **Gemini CLI.** User skills load from `~/.gemini/skills/` or the `~/.agents/skills/` alias; workspace skills from `.gemini/skills/` or the `.agents/skills/` alias. Within a tier, the `.agents/skills/` alias takes precedence. Session resume uses `gemini --resume` or the `/resume` session browser.
|
|
25
|
+
- **OpenCode.** Project-local paths are discovered by walking up from the working directory to the git worktree root across `.opencode/skills/`, `.claude/skills/`, and `.agents/skills/`; global paths load from the three user roots above. Session resume uses `--continue` (last session) or `--session <id>`; `--fork` branches a resumed session and must be combined with one of those (for example `--continue --fork`).
|
|
26
|
+
|
|
27
|
+
## Session persistence boundary
|
|
28
|
+
|
|
29
|
+
Session resume is a property of the invoking CLI, not of the skill. The mechanisms above (Pi JSONL, Claude Code transcripts, `codex resume`, Gemini CLI session browser, OpenCode `--continue`/`--session`) recover **agent conversation state** only.
|
|
30
|
+
|
|
31
|
+
**Herdr** is an agent-aware multiplexer: it keeps agent panes running on the host across mobile-client disconnects and network drops and tracks per-pane agent state (blocked, working, done). Like tmux, it does not preserve live processes across a physical reboot. **Agent conversation recovery after a disconnect or reboot is a separate, CLI-owned capability** and must be verified with the evidence rules in the [verification and recovery reference](verification-and-recovery.md): mark cold-boot behavior as pending whenever observed evidence is unavailable.
|
|
32
|
+
|
|
33
|
+
Whether Herdr integrates with each CLI's resume flow beyond pane survival is **Unverified**; consult current official Herdr and Moshi documentation and observe local behavior before claiming it.
|
|
34
|
+
|
|
35
|
+
## Sources (official documentation, accessed 2026-09-30)
|
|
36
|
+
|
|
37
|
+
- Pi: Agent Skills and package discovery — Pi coding agent documentation v0.99.1 (`docs/skills.md`, `docs/packages.md`); session format and resume — `docs/session-format.md`, `docs/sessions.md`.
|
|
38
|
+
- Claude Code: Skills — https://code.claude.com/docs/en/skills; sessions and resume — https://code.claude.com/docs/en/sessions.
|
|
39
|
+
- Codex: Agent Skills — https://developers.openai.com/codex/skills; CLI reference (`codex resume`) — https://developers.openai.com/codex/cli/reference.
|
|
40
|
+
- Gemini CLI: Agent Skills — https://geminicli.com/docs/cli/skills/; session management — https://geminicli.com/docs/cli/session-management/.
|
|
41
|
+
- OpenCode: Skills — https://opencode.ai/docs/skills/; CLI flags — https://opencode.ai/docs/cli/.
|
|
42
|
+
- Agent Skills specification — https://agentskills.io/specification.
|
|
43
|
+
- Moshi/Herdr integration — https://getmoshi.app/docs/herdr.
|
|
44
|
+
|
|
45
|
+
Claims not listed under an official source above remain **Unverified** and require local observation before use.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Guided install
|
|
2
|
+
|
|
3
|
+
Proceed in order. Lines labelled **Read-only** discover state. Lines labelled **Mutation** require the stated approval immediately before execution. Do not combine independent approvals or ask about a decision already resolved by detection.
|
|
4
|
+
|
|
5
|
+
## 1. Network and OpenSSH baseline
|
|
6
|
+
|
|
7
|
+
**Read-only:** identify installed tools and listeners without exposing configuration secrets.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
command -v ssh sshd tailscale mosh tmux
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Then run exactly one matching read-only listener check:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Linux or WSL.
|
|
17
|
+
ss -lnt
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# macOS; lsof is stock and needs no additional package.
|
|
22
|
+
lsof -nP -iTCP -sTCP:LISTEN
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
If OpenSSH server installation is needed, explain the signed package-manager source, required elevation, service effect, rollback package/service action, and local recovery path. Ask for approval, then use the current platform package documentation rather than an uninspected remote installer. Do not enable a listener until its firewall rule is constrained to the private overlay interface or equivalent private source range. Never add router forwarding or a public-IP rule.
|
|
26
|
+
|
|
27
|
+
Before any SSH restart, validate the generated configuration with the platform's documented syntax check, retain the current connected session, and test a second connection. A failed validation or missing recovery path stops the flow.
|
|
28
|
+
|
|
29
|
+
## 2. Tailscale placement and mobile clients
|
|
30
|
+
|
|
31
|
+
**Read-only:** confirm whether Tailscale is installed, signed in, running, and whether its SSH feature is enabled.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
tailscale status
|
|
35
|
+
tailscale netcheck
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
If unavailable, prefer the current signed vendor package path. Inspect any downloaded installer before execution. Ask approval before sign-in, pairing, or changing network state. For full Mosh, choose exactly one placement: macOS on the native target, or WSL on the Windows route. Detect and resolve a concurrent Windows-and-WSL placement before proceeding.
|
|
39
|
+
|
|
40
|
+
After the overlay connection works, disable Tailscale SSH for the Moshi/OpenSSH key-auth route and verify the resulting state. Install and sign in to the official Tailscale Android or iOS app only with consent. Device authentication, URLs, and QR codes remain on the device; treat a QR code as a temporary secret and never transcribe it.
|
|
41
|
+
|
|
42
|
+
## 3. Key choice and SSH hardening
|
|
43
|
+
|
|
44
|
+
Ask one question: **Easy Pair or manual Ed25519 key?** Describe the available local pairing mechanism only if it is detected; otherwise select manual key setup.
|
|
45
|
+
|
|
46
|
+
For manual setup, generate and keep the private key on the approved client device. The public-key command is safe to show; its output is not to be copied into chat:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# Mutation after approval; choose a device-local path and passphrase prompt.
|
|
50
|
+
ssh-keygen -t ed25519 -a 64 -f <private-key-path> -C "mobile-agent"
|
|
51
|
+
|
|
52
|
+
# Read-only test after public-key installation by an out-of-chat method.
|
|
53
|
+
ssh -o BatchMode=yes -o PreferredAuthentications=publickey <account>@<private-overlay-name> true
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Do not disable password or interactive authentication until this test succeeds from the intended mobile route and a local recovery path exists. Then ask a separate approval for strict hardening. Apply only documented OpenSSH settings, validate syntax before reload, reload rather than blindly restart when supported, and retest the key connection before closing the existing session.
|
|
57
|
+
|
|
58
|
+
## 4. Moshi, `moshi-hook`, Mosh, and terminal host
|
|
59
|
+
|
|
60
|
+
Use current official Moshi and `moshi-hook` documentation. First detect whether each component exists and whether the maintainer supplies a signed package-manager release. Do not execute an opaque curl-pipe-shell installer; inspect a downloaded script before execution. Ask for installation approval separately from cloud pairing approval.
|
|
61
|
+
|
|
62
|
+
Ask one privacy question: **minimal or full Moshi consent?** Minimal enables only the pairing and session capability necessary for the chosen route. Full additionally enables only the current official features the human explicitly accepts. Present data handling and revocation evidence from current official documentation before either choice.
|
|
63
|
+
|
|
64
|
+
Ask separately which detected integrations to enable and whether notifications are wanted. Offer only discovered integrations plus **none**; do not invent providers. Ask before granting each integration permission or sending a test notification.
|
|
65
|
+
|
|
66
|
+
Install Mosh from a signed package source after approval and use the private-overlay name for connection testing:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# Read-only: confirm local clients and server availability.
|
|
70
|
+
mosh --version
|
|
71
|
+
tmux -V
|
|
72
|
+
|
|
73
|
+
# Interactive test; does not place a key or token in chat.
|
|
74
|
+
mosh --ssh="ssh -o PreferredAuthentications=publickey" <account>@<private-overlay-name>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Choose Herdr when installed and supported by current documentation; otherwise use tmux. A typical tmux session command is:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
tmux new-session -As mobile-agent
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Neither Herdr nor tmux preserves live processes across physical reboot. Use a persisted checkpoint or resume mechanism only when the invoking host documents and the human verifies it; Pi JSONL sessions may resume from disk under Pi's supported behavior, with no equivalent assumed elsewhere.
|
|
84
|
+
|
|
85
|
+
## 5. Lifecycle, checkpoints, and notifications
|
|
86
|
+
|
|
87
|
+
On the Windows route, follow the approved WSL systemd, linger, and Windows keepalive sequence in the platform matrix. On macOS, require current official lifecycle evidence before creating a persistent service. Never state that the service will survive reboot until it has passed the recovery verification.
|
|
88
|
+
|
|
89
|
+
Ask whether to enable session checkpoints only after the chosen session host is working. Configure only the invoking host's supported checkpoint format and local storage; do not claim that another CLI can resume Pi JSONL sessions. All live processes die on reboot. For Pi, record the supported JSONL session resume command or UI route without recording session contents. Enable approved notifications last and verify with a non-sensitive test event.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Platform matrix
|
|
2
|
+
|
|
3
|
+
Use these checks before any mutation. Record only non-sensitive results. Ask no question if the next safe action is already determined.
|
|
4
|
+
|
|
5
|
+
## Stable targets
|
|
6
|
+
|
|
7
|
+
| Detection | Route | Lifecycle boundary |
|
|
8
|
+
| --- | --- | --- |
|
|
9
|
+
| `Darwin` | macOS native | macOS owns lifecycle and execution. |
|
|
10
|
+
| Windows host explicitly confirmed as Windows 11 with a working WSL2 distribution | Windows 11 + WSL2 | Windows owns startup; WSL owns Linux services and agent execution. |
|
|
11
|
+
| Windows native without WSL2 | Stop | Guide WSL2 installation; do not configure a native-Windows substitute. |
|
|
12
|
+
| Other platform or unclear state | Stop | Explain that no stable target was detected. |
|
|
13
|
+
|
|
14
|
+
## Read-only preflight
|
|
15
|
+
|
|
16
|
+
Run only the checks relevant to the current shell:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# Read-only: identify the Unix execution environment.
|
|
20
|
+
uname -s
|
|
21
|
+
uname -r
|
|
22
|
+
|
|
23
|
+
# Read-only: macOS version when Darwin was detected.
|
|
24
|
+
sw_vers
|
|
25
|
+
|
|
26
|
+
# Read-only: identify a WSL Linux environment.
|
|
27
|
+
grep -Ei 'microsoft|wsl' /proc/sys/kernel/osrelease
|
|
28
|
+
|
|
29
|
+
# Read-only: run from Windows PowerShell. Stop unless Caption explicitly says Windows 11.
|
|
30
|
+
$os = Get-CimInstance -ClassName Win32_OperatingSystem
|
|
31
|
+
$os | Select-Object Caption, Version, BuildNumber
|
|
32
|
+
if ($os.Caption -notmatch '\bWindows 11\b') { throw "Windows 11 is required; stop this route." }
|
|
33
|
+
|
|
34
|
+
# Read-only: run from Windows PowerShell or Command Prompt after the Windows 11 gate passes.
|
|
35
|
+
wsl --status
|
|
36
|
+
wsl --version
|
|
37
|
+
wsl --list --verbose
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
On Windows, continue only when the Windows version gate explicitly reports Windows 11, WSL version 2, and a selected distribution. Stop rather than infer Windows 11 from WSL or partial signals. If WSL2 is absent, explain that the human must approve the current Microsoft-documented WSL2 installation path and any restart it requires.
|
|
41
|
+
|
|
42
|
+
## Placement decisions
|
|
43
|
+
|
|
44
|
+
### macOS native
|
|
45
|
+
|
|
46
|
+
Use macOS as the single execution target for OpenSSH, Tailscale, Mosh, and the session host. Before proposing a lifecycle daemon, firewall change, or persistent-login setting, obtain current official Apple documentation and collect local evidence. This draft intentionally supplies no exact macOS firewall or lifecycle command because those commands must be verified for the installed macOS release.
|
|
47
|
+
|
|
48
|
+
### Windows 11 + WSL2
|
|
49
|
+
|
|
50
|
+
Windows is the lifecycle host; the selected WSL2 distribution is the Linux execution target. For the full-Mosh route, Tailscale must run in WSL only. If the Windows Tailscale client is active, present the conflict and require approval to stop or remove that placement before enabling WSL Tailscale. Never run both concurrently for this route.
|
|
51
|
+
|
|
52
|
+
Ask separately for approval before each of these mutations:
|
|
53
|
+
|
|
54
|
+
1. Enabling WSL systemd by editing the distribution configuration and restarting WSL.
|
|
55
|
+
2. Creating a Windows startup scheduled task that invokes the selected distribution's approved keepalive/service-start action.
|
|
56
|
+
3. Enabling user linger in WSL so approved user services can start without an interactive login.
|
|
57
|
+
|
|
58
|
+
Use current Microsoft and distribution documentation to construct the exact scheduled-task and systemd commands. Verify each with read-only checks such as:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
# Read-only: from WSL after systemd is enabled.
|
|
62
|
+
systemctl is-system-running
|
|
63
|
+
loginctl show-user "$USER" -p Linger
|
|
64
|
+
|
|
65
|
+
# Read-only: from Windows after the task is created.
|
|
66
|
+
schtasks /query /tn "<task-name>" /fo LIST /v
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Linger and a startup task can start configured services after boot; they do not preserve running agent processes across a physical reboot. A host-specific persisted checkpoint may resume only when that host documents and the human observes the behavior. Pi JSONL resume is specific to Pi and must not be claimed for other CLIs.
|
|
70
|
+
|
|
71
|
+
## Permission and rollback gate
|
|
72
|
+
|
|
73
|
+
Before the first change, identify the invoking user, privilege boundary, current SSH access path, installed service state, and how to undo each proposed mutation. Keep an already-working local console or separate authenticated session available before touching firewall or SSH authentication. If that recovery path cannot be demonstrated, stop rather than making the change.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Verification and recovery
|
|
2
|
+
|
|
3
|
+
Collect non-sensitive evidence after every approved mutation. A command failing because a component is absent is a finding, not permission to install or change it.
|
|
4
|
+
|
|
5
|
+
## Verification sequence
|
|
6
|
+
|
|
7
|
+
1. **Private network:** confirm the selected target is present on the overlay and no public port or router forwarding was created.
|
|
8
|
+
2. **SSH:** test public-key authentication in a second session before closing the recovery session. Confirm Tailscale SSH remains disabled for this route.
|
|
9
|
+
3. **Mosh:** establish a mobile-route session, briefly disconnect the client network, reconnect, and record whether Mosh reconnects as expected.
|
|
10
|
+
4. **Host:** confirm Herdr or tmux behavior, checkpoint creation, selected integration authorization, and notification delivery without recording contents.
|
|
11
|
+
5. **Lifecycle:** only after explicit reboot approval, test the documented start and the invoking host's documented checkpoint/resume path. Pi JSONL resume is tested only when Pi is the host.
|
|
12
|
+
|
|
13
|
+
Useful read-only checks:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Overlay evidence.
|
|
17
|
+
tailscale status
|
|
18
|
+
tailscale netcheck
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Run exactly one matching read-only listener check:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# Linux or WSL.
|
|
25
|
+
ss -lnt
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# macOS; lsof is stock and needs no additional package.
|
|
30
|
+
lsof -nP -iTCP -sTCP:LISTEN
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# SSH configuration and non-interactive key-auth test.
|
|
35
|
+
sshd -T
|
|
36
|
+
ssh -o BatchMode=yes -o PreferredAuthentications=publickey <account>@<private-overlay-name> true
|
|
37
|
+
|
|
38
|
+
# Service and user-linger evidence on a systemd Linux target.
|
|
39
|
+
systemctl --user --no-pager status <service-name>
|
|
40
|
+
loginctl show-user "$USER" -p Linger
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Interpret listener output with the firewall and overlay policy; a listening port is not by itself evidence of public exposure. Confirm that no public address, router forwarding rule, or broad firewall exception was introduced. If current platform tooling cannot prove this, stop and request local network evidence rather than assuming safety.
|
|
44
|
+
|
|
45
|
+
## Recovery decision tree
|
|
46
|
+
|
|
47
|
+
| Failure | Safe response |
|
|
48
|
+
| --- | --- |
|
|
49
|
+
| Key test fails after hardening | Keep the existing session open; restore the last known-good authentication setting through the local recovery path; validate before reload. |
|
|
50
|
+
| SSH syntax validation fails | Do not reload or restart; correct or restore the staged configuration while the existing session remains available. |
|
|
51
|
+
| Tailscale appears on both Windows and WSL | Stop the full-Mosh route; preserve one reachable path; request approval to remove or stop one placement. |
|
|
52
|
+
| Mosh does not reconnect | Keep SSH recovery available; inspect overlay and SSH evidence first; do not expose a public port as a workaround. |
|
|
53
|
+
| Keepalive, linger, or lifecycle fails | Collect service/task status and current official documentation; do not claim persistence. |
|
|
54
|
+
| Reboot loses a live terminal process | Treat as expected; restart required processes through the approved lifecycle path and resume only a host-supported persisted checkpoint. Pi JSONL resume is Pi-specific. |
|
|
55
|
+
|
|
56
|
+
## Reboot gate
|
|
57
|
+
|
|
58
|
+
Before reboot, ask explicit approval and state that all live processes will die. Verify a local recovery path, the key-auth path, rollback instructions, and the Windows task or macOS lifecycle evidence first. After reboot, check the actual startup state, re-establish private-overlay SSH and Mosh, then resume a persisted checkpoint only if the invoking host supports it and the path is available. For Pi, this may be the local JSONL session path.
|
|
59
|
+
|
|
60
|
+
**Draft evidence status:** cold-boot and post-reboot behavior remain pending until a human observes and records this sequence on the selected supported target. Do not mark the installation reboot-verified before that evidence exists.
|
|
61
|
+
|
|
62
|
+
## Rollback
|
|
63
|
+
|
|
64
|
+
Rollback only the approved mutation that caused the failure: disable the newly created service or task, restore the saved SSH configuration, remove the narrow firewall rule if it was newly added, or sign out/remove the newly added client according to current vendor documentation. Do not delete keys, sessions, or unrelated configuration as a generic rollback step. Any destructive rollback requires a fresh explicit approval.
|