create-pmos 0.53.0 → 0.53.2
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/package.json +1 -1
- package/payload/KIT-VERSION +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/README.md +2 -0
- package/workspace/REMOTE.md +107 -65
- package/workspace/bin/pmos.js +4 -2
- package/workspace/first-paint-audit.js +6 -2
- package/workspace/fixtures/initiatives-api/list.json +13 -0
- package/workspace/fixtures/initiatives-api/roadmap.json +13 -0
- package/workspace/fixtures/report-api/draft.json +105 -0
- package/workspace/fixtures/reports-page/draft-biweekly-2026-10-16.md +12 -0
- package/workspace/guide/command-palette.md +1 -1
- package/workspace/guide/index.md +3 -1
- package/workspace/guide/initiatives-roadmap.md +1 -0
- package/workspace/guide/initiatives-screen.md +1 -0
- package/workspace/guide/launcher.md +51 -0
- package/workspace/guide/layout.md +7 -2
- package/workspace/guide/proactive-engine.md +21 -5
- package/workspace/guide/report-slides.md +28 -0
- package/workspace/guide/reports.md +2 -0
- package/workspace/lib/auto-sync.js +83 -0
- package/workspace/lib/board-fold.js +2 -2
- package/workspace/lib/breakdown-export.js +1 -6
- package/workspace/lib/brief.js +20 -2
- package/workspace/lib/goals.js +165 -0
- package/workspace/lib/initiative-rows.js +7 -3
- package/workspace/lib/launcher-cli.js +207 -0
- package/workspace/lib/plan-tree.js +15 -2
- package/workspace/lib/pmos-verbs.js +3 -0
- package/workspace/lib/proactive-cli.js +58 -2
- package/workspace/lib/proactive.js +2 -2
- package/workspace/lib/qr.js +293 -0
- package/workspace/lib/remote-access.js +184 -12
- package/workspace/lib/report-draft.js +34 -6
- package/workspace/lib/report-slides-data.js +141 -0
- package/workspace/lib/routes-initiatives.js +35 -2
- package/workspace/lib/routes-reports.js +3 -3
- package/workspace/lib/routes-timeline.js +1 -1
- package/workspace/lib/sync.js +61 -6
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +1 -1
- package/workspace/public/icons/apple-touch-icon.png +0 -0
- package/workspace/public/icons/icon-192.png +0 -0
- package/workspace/public/icons/icon-512.png +0 -0
- package/workspace/public/index.html +11 -0
- package/workspace/public/pm/app.js +55 -9
- package/workspace/public/pm/components.css +865 -1
- package/workspace/public/pm/dest-reports.js +8 -2
- package/workspace/public/pm/dest-today.js +8 -2
- package/workspace/public/pm/dock.js +73 -3
- package/workspace/public/pm/frame-copy.js +5 -0
- package/workspace/public/pm/initiative-list.js +18 -1
- package/workspace/public/pm/initiatives-copy.js +4 -0
- package/workspace/public/pm/panel-tabs.js +14 -12
- package/workspace/public/pm/print.css +11 -0
- package/workspace/public/pm/report-slides.js +438 -0
- package/workspace/public/pm/report-view.js +8 -2
- package/workspace/public/pm/reports-copy.js +3 -0
- package/workspace/public/pm/roadmap.js +23 -0
- package/workspace/public/pm/slides-copy.js +52 -0
- package/workspace/public/pm/today-copy.js +29 -0
- package/workspace/public/pm/today-goals.js +172 -0
- package/workspace/public/pm/tokens.css +13 -0
- package/workspace/public/pm/topbar.js +5 -2
- package/workspace/replaced-tests.json +36 -1
- package/workspace/server.js +165 -15
- package/workspace/skills/morning-brief.skill +15 -9
- package/workspace/skills/report-biweekly.skill +1 -0
- package/workspace/test-files.json +4 -0
- package/workspace/test-lists/run6.json +9 -0
- package/workspace/test-lists/workspace-launcher.json +5 -0
- package/workspace/test-lists/workspace-phone-app.json +5 -0
- package/workspace/test-lists/workspace-public-host.json +5 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.53.
|
|
3
|
+
"version": "0.53.2",
|
|
4
4
|
"description": "Scaffold the PMOS portable kit — run the PMOS product-management workflow (initiative → lane-fit spec → contract → build → three gates, with work-type lanes for features, research, maintenance, incidents, and content) entirely inside your own repo, with all state as git-versioned files. Includes `workflow-sweep`, which audits the state every board derives from and separates real defects from work you simply have not done. `create-pmos workspace` also installs + launches the local Terminal Workspace GUI. No database, no network, no telemetry.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
package/payload/KIT-VERSION
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Eval calibration log"
|
|
4
4
|
description: "Append-only record of every PM override of an evaluator verdict."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-06T02:40:00Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Health budgets"
|
|
4
4
|
description: "Closed registry of health-budget anchors (D59), edited only by the PM."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-06T02:40:00Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Discoveries and friction log"
|
|
4
4
|
description: "Append-only record of what runs discovered and where the process rubbed."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-06T02:40:00Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
package/workspace/README.md
CHANGED
|
@@ -33,6 +33,8 @@ pmos status # is it running? which sessions?
|
|
|
33
33
|
pmos stop # shut it down (scrollback is preserved; sessions respawn next time)
|
|
34
34
|
pmos doctor # diagnose the environment (node-pty arch, PTY spawn test, ports)
|
|
35
35
|
```
|
|
36
|
+
**Without a terminal.** `pmos launcher on` (once, from the repository you want) writes a PMOS app — `~/Applications/PMOS.app` on macOS, an app-menu entry on Linux — and a login item that starts the server at login without opening a browser. Both run `pmos up <repo>` with the PATH your terminal had at setup. `pmos launcher status` checks it, `pmos launcher off` removes it. Guide: [`guide/launcher.md`](guide/launcher.md).
|
|
37
|
+
|
|
36
38
|
**The port is found, not assumed.** A running server records the port it serves, so `pmos` in a second repo joins the workspace you already have open — whatever port it ended up on, and however it was started. If another program holds the default port (7433), `pmos` starts on the next free one and says so; `PMOS_WS_PORT=7500 pmos` pins it by hand. `pmos stop` shuts down every workspace server it finds, which is how you collapse two back into one.
|
|
37
39
|
|
|
38
40
|
<details><summary>Without the global install (running from the repo)</summary>
|
package/workspace/REMOTE.md
CHANGED
|
@@ -1,32 +1,23 @@
|
|
|
1
1
|
---
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Workspace remote access over a private tailnet (Tailscale)"
|
|
4
|
-
timestamp: 2026-
|
|
4
|
+
timestamp: 2026-10-06
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Reaching the workspace from your phone — over a private tailnet, never the public internet
|
|
8
8
|
|
|
9
|
-
The workspace server owns **real terminal sessions (PTYs)** running your `claude` on **your own
|
|
10
|
-
subscription**. By default it binds `127.0.0.1` only, so it is reachable solely from the machine it
|
|
11
|
-
runs on. This runbook enables reaching **the same local server** from another of your own devices —
|
|
12
|
-
a phone, a laptop — over a **private Tailscale tailnet**. It never exposes a public endpoint.
|
|
9
|
+
The workspace server owns **real terminal sessions (PTYs)** running your `claude` on **your own subscription**. By default it binds `127.0.0.1` only, so it is reachable solely from the machine it runs on. This runbook enables reaching **the same local server** from another of your own devices — a phone, a laptop — over a **private Tailscale tailnet**. The tailnet path never exposes a public endpoint. To use your own domain instead, see [Reaching the workspace at your own domain](#reaching-the-workspace-at-your-own-domain-cloudflare-tunnel--access): the server still binds loopback only, and Cloudflare forwards to it.
|
|
13
10
|
|
|
14
11
|
## Why a tailnet, and why no public endpoint (D62)
|
|
15
12
|
|
|
16
|
-
A [Tailscale](https://tailscale.com) tailnet is a private [WireGuard](https://www.wireguard.com/)
|
|
17
|
-
mesh between **your own devices**. Every device is authenticated and encrypted end-to-end; addresses
|
|
18
|
-
live in the CGNAT range `100.64.0.0/10` and are routable **only** between devices on your tailnet —
|
|
19
|
-
never from the public internet.
|
|
13
|
+
A [Tailscale](https://tailscale.com) tailnet is a private [WireGuard](https://www.wireguard.com/) mesh between **your own devices**. Every device is authenticated and encrypted end-to-end; addresses live in the CGNAT range `100.64.0.0/10` and are routable **only** between devices on your tailnet — never from the public internet.
|
|
20
14
|
|
|
21
15
|
PMOS's locked decision **D62** rejects, permanently:
|
|
22
16
|
|
|
23
17
|
- **any public endpoint** for the workspace server, and
|
|
24
18
|
- **any hosted PTY bridge** (proxying the terminal through a hosted, browser-reachable surface).
|
|
25
19
|
|
|
26
|
-
A public server that owns a PTY running your Claude subscription is severe exposure. The tailnet path
|
|
27
|
-
sidesteps that entirely: the phone attaches to the *same local server* across your own private
|
|
28
|
-
network. Accordingly, this feature will **only ever** bind loopback plus your machine's own tailnet
|
|
29
|
-
address. If you ask it to bind anything else (`0.0.0.0`, a LAN or public IP), it **refuses to start**.
|
|
20
|
+
A public server that owns a PTY running your Claude subscription is severe exposure. The tailnet path sidesteps that entirely: the phone attaches to the *same local server* across your own private network. Accordingly, this feature will **only ever** bind loopback plus your machine's own tailnet address. If you ask it to bind anything else (`0.0.0.0`, a LAN or public IP), it **refuses to start**.
|
|
30
21
|
|
|
31
22
|
## Setup
|
|
32
23
|
|
|
@@ -52,16 +43,15 @@ address. If you ask it to bind anything else (`0.0.0.0`, a LAN or public IP), it
|
|
|
52
43
|
loopback requests are token-free; every tailnet request must present the token.
|
|
53
44
|
```
|
|
54
45
|
|
|
55
|
-
**
|
|
46
|
+
Under those lines it prints the phone URL as a **QR code**, and the same URL as text (`http://100.x.y.z:7433/?token=<token>`). The token is shown only at startup and never logged again. A token you supply with `PMOS_TOKEN` is never printed, so that mode prints no QR code.
|
|
56
47
|
|
|
57
|
-
3. **Open it from your phone.**
|
|
48
|
+
3. **Open it from your phone.** Point the phone camera at the QR code and open the link. Or type the URL into the phone's browser. The server sets a sign-in cookie (7 days) and redirects to `http://100.x.y.z:7433/`, so the token does not stay in the address bar or the history.
|
|
58
49
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
50
|
+
If the phone has no valid cookie, it shows a **sign-in page**. Paste the token there. The field is a password field, so the phone's password manager can save it. This is also what you see after the cookie expires, or after a restart that generated a new token.
|
|
51
|
+
|
|
52
|
+
The client attaches to the **same live sessions** the desktop sees. Requests from the machine itself (`127.0.0.1`) never need the token.
|
|
62
53
|
|
|
63
|
-
|
|
64
|
-
page and its WebSocket. Requests from the machine itself (`127.0.0.1`) never need the token.
|
|
54
|
+
4. **Optional: put it on the home screen.** In Safari, tap Share › Add to Home Screen. In Chrome on Android, tap ⋮ › Add to Home screen. The PMOS icon then opens the workspace full screen, without the browser bar. On iOS the home-screen app has its own cookies, so it asks for the token once. On Android, Chrome opens a home-screen app full screen only from an HTTPS origin. Over the plain `http://100.x.y.z` address it adds a shortcut that opens in a normal browser tab. There is no offline mode: the workspace shows live state, and it needs the server to be running.
|
|
65
55
|
|
|
66
56
|
## Configuration
|
|
67
57
|
|
|
@@ -76,56 +66,108 @@ address. If you ask it to bind anything else (`0.0.0.0`, a LAN or public IP), it
|
|
|
76
66
|
|
|
77
67
|
## Security model (what is guaranteed)
|
|
78
68
|
|
|
79
|
-
- **Never public.** The only addresses the server will ever bind are loopback and a
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
- **
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
- **Origin/Host allowlist (D64).** Both the HTTP guard and the WebSocket-upgrade handshake reject any
|
|
86
|
-
request whose `Origin` is foreign (not same-origin with the `Host` it arrived on) or whose `Host`
|
|
87
|
-
hostname is not loopback/localhost or your tailnet address. This closes the cross-origin-localhost /
|
|
88
|
-
DNS-rebinding vector: without it, **any website open in your browser** could open a WebSocket to
|
|
89
|
-
`ws://127.0.0.1:<port>` and spawn a shell + type into it, because loopback peers are token-exempt.
|
|
90
|
-
A same-origin page served by the workspace passes; a cross-site page is rejected `403`. Native
|
|
91
|
-
clients / `curl` (which send no `Origin`) still pass this check and are then gated by the token.
|
|
92
|
-
- **No header/token logging (D08).** The request-guard path logs nothing about the request. The
|
|
93
|
-
token is printed at most once at startup (only when generated); a token you supply is never echoed.
|
|
94
|
-
- **Rate-limited control channel (D64).** The WS messages that spawn shells (`new`/`respawn`) and
|
|
95
|
-
feed a PTY (`input`) are per-connection rate-limited, bounding abuse if the socket is ever reached.
|
|
96
|
-
- **Independent layers.** The tailnet (WireGuard device auth), the bearer token, and the origin/host
|
|
97
|
-
allowlist all gate access — a device must be *on your tailnet*, *hold the token*, **and** present a
|
|
98
|
-
same-origin request from an allowed host.
|
|
69
|
+
- **Never public.** The only addresses the server will ever bind are loopback and a `100.64.0.0/10` tailnet address. `0.0.0.0`, `::`, LAN, and public IPs are refused — the server exits with a clear error rather than binding them. There is no code path to a public bind.
|
|
70
|
+
- **Token beyond loopback.** With remote mode on, every non-loopback HTTP request and WebSocket upgrade must present the bearer token, or it is rejected (`401` / socket closed). A browser page load gets the sign-in page with that `401`. The one exception is `POST /auth`, the sign-in form's target: it only compares the posted token and, on a match, sets the session cookie. It still sits behind the Origin/Host allowlist. Loopback requests stay token-free for local development, unless they arrived through a proxy (see [Why a proxy on the same machine needs special handling](#why-a-proxy-on-the-same-machine-needs-special-handling)). The comparison is constant-time.
|
|
71
|
+
- **Origin/Host allowlist (D64).** Both the HTTP guard and the WebSocket-upgrade handshake reject any request whose `Origin` is foreign (not same-origin with the `Host` it arrived on) or whose `Host` hostname is not loopback/localhost or your tailnet address. This closes the cross-origin-localhost / DNS-rebinding vector: without it, **any website open in your browser** could open a WebSocket to `ws://127.0.0.1:<port>` and spawn a shell + type into it, because loopback peers are token-exempt. A same-origin page served by the workspace passes; a cross-site page is rejected `403`. Native clients / `curl` (which send no `Origin`) still pass this check and are then gated by the token.
|
|
72
|
+
- **No header/token logging (D08).** The request-guard path logs nothing about the request. The token is printed at most once at startup (only when generated); a token you supply is never echoed.
|
|
73
|
+
- **Rate-limited control channel (D64).** The WS messages that spawn shells (`new`/`respawn`) and feed a PTY (`input`) are per-connection rate-limited, bounding abuse if the socket is ever reached.
|
|
74
|
+
- **Independent layers.** The tailnet (WireGuard device auth), the bearer token, and the origin/host allowlist all gate access — a device must be *on your tailnet*, *hold the token*, **and** present a same-origin request from an allowed host.
|
|
99
75
|
|
|
100
76
|
## Exposing to the internet — the required hardening (D64)
|
|
101
77
|
|
|
102
|
-
The workspace is **tailnet-first**. The tailnet path above is the supported way to reach it from
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
78
|
+
The workspace is **tailnet-first**. The tailnet path above is the supported way to reach it from another of your own devices and needs no internet exposure. If you nonetheless expose an instance to the internet, D64 permits it **only after all three of the following are in place — never before, and never as a raw public bind**:
|
|
79
|
+
|
|
80
|
+
1. **The bearer token for every non-loopback or proxied request.** A request that arrives through a reverse proxy or tunnel is never token-exempt, even though the proxy connects from `127.0.0.1` (see below).
|
|
81
|
+
2. **The Origin/Host allowlist** (already enforced — see above). `PMOS_PUBLIC_HOST` adds exactly one public hostname to it.
|
|
82
|
+
3. **TLS termination at the exposure boundary.** **The workspace server does not terminate TLS.** It speaks plain HTTP/WS and must sit behind something that terminates TLS for it. Do **not** put the raw HTTP port on a public interface — the server refuses non-loopback/non-tailnet binds precisely so this stays a conscious proxy decision, not an accident.
|
|
83
|
+
|
|
84
|
+
**Do not rewrite the forwarded `Host` header to a loopback address.** Earlier versions of this runbook advised it. Forward the public hostname unchanged and set `PMOS_PUBLIC_HOST` to it.
|
|
85
|
+
|
|
86
|
+
### Why a proxy on the same machine needs special handling
|
|
87
|
+
|
|
88
|
+
A tunnel or reverse proxy that runs on the workspace machine (cloudflared, Caddy, nginx) connects to the server from `127.0.0.1`. Loopback peers are token-exempt, so without a further check every internet request would look local. The server therefore treats a request as **proxied** when it carries any of these headers, and a proxied request must always present the token:
|
|
89
|
+
|
|
90
|
+
`Forwarded`, `X-Forwarded-For`, `X-Forwarded-Host`, `X-Forwarded-Proto`, `X-Real-IP`, `Cf-Connecting-IP`, `Cf-Ray`, `Cf-Access-Jwt-Assertion`
|
|
91
|
+
|
|
92
|
+
A request to the `PMOS_PUBLIC_HOST` hostname is proxied by definition, with or without these headers.
|
|
93
|
+
|
|
94
|
+
## Reaching the workspace at your own domain (Cloudflare Tunnel + Access)
|
|
106
95
|
|
|
107
|
-
|
|
108
|
-
2. **The Origin/Host allowlist** (already enforced — see above).
|
|
109
|
-
3. **TLS termination at the exposure boundary.** **The workspace server does not terminate TLS.** It
|
|
110
|
-
speaks plain HTTP/WS and must sit behind something that terminates TLS for it — Tailscale's own
|
|
111
|
-
HTTPS (`tailscale serve` / `tailscale funnel`) or a reverse proxy (Caddy, nginx, a tunnel) that
|
|
112
|
-
holds the certificate and forwards to the loopback/tailnet listener. Do **not** put the raw HTTP
|
|
113
|
-
port on a public interface — the server refuses non-loopback/non-tailnet binds precisely so this
|
|
114
|
-
stays a conscious proxy decision, not an accident.
|
|
96
|
+
This runbook puts the workspace at `https://pm-os.app/` (use your own hostname). Cloudflare holds the certificate and terminates TLS. **Cloudflare Access** asks for your Google or email login before any request reaches the machine. The workspace then asks for its own token. The workspace keeps running on **your own always-on machine**; Cloudflare forwards bytes to it. It does not run on Cloudflare Workers or Containers: it holds real terminal sessions, a git checkout and your agent CLI's login, and needs a persistent disk.
|
|
115
97
|
|
|
116
|
-
|
|
117
|
-
|
|
98
|
+
Two locks protect the workspace, and each one is enforced by the server:
|
|
99
|
+
|
|
100
|
+
| Lock | Who checks it | What happens without it |
|
|
101
|
+
|---|---|---|
|
|
102
|
+
| Cloudflare Access login | Cloudflare's edge. The server refuses a request to the public host that has no `Cf-Access-Jwt-Assertion` header. | `403`, before the sign-in page. |
|
|
103
|
+
| The workspace token | The server, constant-time. | `401` and the sign-in page. |
|
|
104
|
+
|
|
105
|
+
The Access check in the server is a **presence check**: it confirms that Access is in front, and it fails closed when you forget the Access application. It does not verify the assertion's signature, because that needs Cloudflare's signing keys fetched over the network, and the workspace makes no outbound requests (D79). The origin listens on loopback only, so the tunnel is the only way to reach it with the public hostname.
|
|
106
|
+
|
|
107
|
+
### Setup
|
|
108
|
+
|
|
109
|
+
1. **Pick the machine.** Use a machine that stays on: your desktop, or a small Linux VPS. Install the workspace there as usual, and sign in to your agent CLI in a workspace terminal.
|
|
110
|
+
|
|
111
|
+
2. **Move the domain's DNS to this tunnel.** In the Cloudflare dashboard, remove any existing record or Pages/Worker route for the hostname that you want to use. For `pm-os.app`, the retired PM-OS Pages project and Worker still own it.
|
|
112
|
+
|
|
113
|
+
3. **Create the Access application first.** In Cloudflare Zero Trust, go to **Access › Applications › Add an application › Self-hosted**. Set the domain to `pm-os.app`. Add one policy: **Allow**, **Emails**, your own email address. Save. Do this before the tunnel, so the hostname is never reachable without a login.
|
|
114
|
+
|
|
115
|
+
4. **Create the tunnel.** Install `cloudflared` on the machine, then:
|
|
116
|
+
|
|
117
|
+
```sh
|
|
118
|
+
cloudflared tunnel login
|
|
119
|
+
cloudflared tunnel create pmos
|
|
120
|
+
cloudflared tunnel route dns pmos pm-os.app
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Write `~/.cloudflared/config.yml`:
|
|
124
|
+
|
|
125
|
+
```yaml
|
|
126
|
+
tunnel: pmos
|
|
127
|
+
credentials-file: /home/<you>/.cloudflared/<tunnel-id>.json
|
|
128
|
+
ingress:
|
|
129
|
+
- hostname: pm-os.app
|
|
130
|
+
service: http://127.0.0.1:7433
|
|
131
|
+
- service: http_status:404
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Do not set `httpHostHeader`. The server must see the `Host: pm-os.app` that the browser sent.
|
|
135
|
+
|
|
136
|
+
5. **Start the workspace in public-host mode**, from `workspace/`:
|
|
137
|
+
|
|
138
|
+
```sh
|
|
139
|
+
PMOS_PUBLIC_HOST=pm-os.app PMOS_TOKEN='<a long random secret>' npm start
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Supply your own `PMOS_TOKEN` so that a restart does not sign out every device. Generate one with `openssl rand -base64 32`. Without `PMOS_TOKEN`, the server generates a token and prints it once.
|
|
143
|
+
|
|
144
|
+
6. **Start the tunnel:** `cloudflared tunnel run pmos`. To keep it running after a reboot, install it as a service (`sudo cloudflared service install`).
|
|
145
|
+
|
|
146
|
+
7. **Open `https://pm-os.app/`.** Cloudflare asks for your login. The workspace then shows its sign-in page. Paste the token. The workspace sets a `Secure` session cookie for 7 days.
|
|
147
|
+
|
|
148
|
+
### Check that it is closed
|
|
149
|
+
|
|
150
|
+
Run these from a machine that is **not** signed in to Access:
|
|
151
|
+
|
|
152
|
+
- `curl -sI https://pm-os.app/` returns a redirect to `*.cloudflareaccess.com`, not a workspace page.
|
|
153
|
+
- On the workspace machine, `curl -s -H 'Host: pm-os.app' http://127.0.0.1:7433/api/sessions` returns `403` (no Access assertion).
|
|
154
|
+
- On the workspace machine, `curl -s -H 'X-Forwarded-For: 1.2.3.4' http://127.0.0.1:7433/api/sessions` returns `401` (proxied, no token).
|
|
155
|
+
|
|
156
|
+
### Configuration
|
|
157
|
+
|
|
158
|
+
| Variable | Effect |
|
|
159
|
+
|---|---|
|
|
160
|
+
| `PMOS_PUBLIC_HOST=<hostname>` | Opt in. Add this one bare hostname to the Host allowlist, and require the token and the Access header for requests to it. The server still binds loopback only (plus the tailnet when `PMOS_REMOTE=tailnet` is also set). A scheme, port, path, IP address or `localhost` is refused, and the server does not start. |
|
|
161
|
+
| `PMOS_TOKEN=<t>` | The workspace token. Recommended in this mode. Never printed. |
|
|
118
162
|
|
|
119
163
|
## Troubleshooting
|
|
120
164
|
|
|
121
|
-
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
- **"
|
|
127
|
-
|
|
128
|
-
- **Phone
|
|
129
|
-
|
|
130
|
-
- **Phone can't connect at all** — confirm both devices show each other in the Tailscale app, and
|
|
131
|
-
that you used the `100.x.y.z` address, not a LAN IP.
|
|
165
|
+
- **`403` "the public host is only reachable through Cloudflare Access"** — the request reached the workspace without passing Access. Create the Access application for the hostname (Setup step 3).
|
|
166
|
+
- **`403` "forbidden origin/host" on the public hostname** — `PMOS_PUBLIC_HOST` is not set, or it names another hostname, or the tunnel rewrites the `Host` header. Remove `httpHostHeader` from the tunnel's ingress rule.
|
|
167
|
+
- **A local reverse proxy now gets `401`** — it adds a forwarding header, so its requests need the token. This is intended.
|
|
168
|
+
|
|
169
|
+
- **"refusing to bind … only loopback or a Tailscale tailnet address (100.64.0.0/10) is permitted"** — you passed `PMOS_BIND_ADDR` (or `PMOS_TAILNET_IP`) that isn't loopback or tailnet. Use your real `tailscale ip -4` value.
|
|
170
|
+
- **"no Tailscale tailnet address … was found"** — Tailscale isn't up on this machine. Run `tailscale up`, or set `PMOS_TAILNET_IP`.
|
|
171
|
+
- **"cannot bind 100.x.y.z:PORT — that address is not present on this machine"** — the pinned `PMOS_TAILNET_IP` isn't actually assigned to this machine. Check `tailscale ip -4`.
|
|
172
|
+
- **Phone shows the sign-in page** — the cookie is missing, expired, or from an older token. Paste the token, or scan the QR code again. Restarting the server without `PMOS_TOKEN` mints a new token, so every phone must sign in again. (API clients without a browser still get a JSON `401`.)
|
|
173
|
+
- **Phone can't connect at all** — confirm both devices show each other in the Tailscale app, and that you used the `100.x.y.z` address, not a LAN IP.
|
package/workspace/bin/pmos.js
CHANGED
|
@@ -45,7 +45,9 @@ const pidFile = (port) => path.join(STATE, D.pidFileName(port));
|
|
|
45
45
|
const logFile = (port) => path.join(STATE, `server-${port}.log`);
|
|
46
46
|
const APP_VERSION = (() => { try { return require('../package.json').version || null; } catch { return null; } })();
|
|
47
47
|
const urlOf = (port) => `http://127.0.0.1:${port}`;
|
|
48
|
-
|
|
48
|
+
// Tests set PMOS_NO_OPEN. `--no-open` is the login item's (`pmos launcher on`): a flag, not the env var, so the
|
|
49
|
+
// server it starts does not hand PMOS_NO_OPEN to every session, where a `pmos` typed later would open nothing.
|
|
50
|
+
const NO_OPEN = process.env.PMOS_NO_OPEN === '1' || process.argv.includes('--no-open');
|
|
49
51
|
|
|
50
52
|
const say = (s) => console.log(s);
|
|
51
53
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -442,7 +444,7 @@ function doctor() {
|
|
|
442
444
|
code = 0;
|
|
443
445
|
} else if (cmd === 'up' || cmd === 'start' || cmd === 'open') {
|
|
444
446
|
// `pmos open ../other-repo` — the path is the argument AFTER the verb
|
|
445
|
-
code = await up(process.argv[3] ? path.resolve(process.argv[3]) : process.cwd());
|
|
447
|
+
code = await up(process.argv[3] && !process.argv[3].startsWith('-') ? path.resolve(process.argv[3]) : process.cwd());
|
|
446
448
|
} else if (arg && !arg.startsWith('-')) {
|
|
447
449
|
code = await up(path.resolve(arg)); // `pmos ../other-repo`
|
|
448
450
|
} else {
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* [draggable=true] inside the viewport on first paint.
|
|
8
8
|
*
|
|
9
9
|
* EXPECT PRD Q5's budgets at 1280×800, counted outside the dock and the terminal
|
|
10
|
+
* SPECIMEN_A specimen A's own counts (2026-10-05), the calibration the audit must reproduce
|
|
10
11
|
* audit(phone) shoot.js's audit, verbatim; runs in the page (page.evaluate(audit, false))
|
|
11
12
|
* countIn([scope, exclude]) the same selector and visibility test, inside `scope` and outside `exclude`; in the page
|
|
12
13
|
* stableCount([scope, exclude, minus]) countIn once the screen is still (two animation frames apart); in the page,
|
|
@@ -16,7 +17,10 @@
|
|
|
16
17
|
* Do not tune the selector or the visibility test for one surface: change them here, for all, or not at all.
|
|
17
18
|
*/
|
|
18
19
|
|
|
19
|
-
|
|
20
|
+
// The budgets start at specimen A's counts. reports: 14 → 15 in Run 6 for Present, the one control the PM asked to keep
|
|
21
|
+
// with the slides (2026-10-06). The calibration stays specimen A's own count: the specimen did not change.
|
|
22
|
+
const SPECIMEN_A = Object.freeze({ today: 17, initiatives: 24, board: 33, reports: 14 });
|
|
23
|
+
const EXPECT = Object.freeze({ ...SPECIMEN_A, reports: 15 });
|
|
20
24
|
|
|
21
25
|
/* ── shoot.js audit(), copied verbatim (scratchpad/specimens2/shoot.js, 2026-10-05) ─────────────────────────────── */
|
|
22
26
|
// runs in the page
|
|
@@ -142,4 +146,4 @@ function installCounter(page) {
|
|
|
142
146
|
return page.addInitScript({ content: 'window.__countIn = ' + countIn.toString() + ';' });
|
|
143
147
|
}
|
|
144
148
|
|
|
145
|
-
module.exports = { EXPECT, audit, countIn, stableCount, installCounter };
|
|
149
|
+
module.exports = { EXPECT, SPECIMEN_A, audit, countIn, stableCount, installCounter };
|
|
@@ -172,5 +172,18 @@
|
|
|
172
172
|
"init": null,
|
|
173
173
|
"why": "line 9: not JSON"
|
|
174
174
|
}
|
|
175
|
+
],
|
|
176
|
+
"unrecorded": [
|
|
177
|
+
{
|
|
178
|
+
"id": "fx-orphan",
|
|
179
|
+
"title": "An initiative with a PRD and no record",
|
|
180
|
+
"artifacts": [
|
|
181
|
+
{
|
|
182
|
+
"name": "fx-orphan-prd.md",
|
|
183
|
+
"path": "planning/prd/fx-orphan-prd.md",
|
|
184
|
+
"role": "PRD"
|
|
185
|
+
}
|
|
186
|
+
]
|
|
187
|
+
}
|
|
175
188
|
]
|
|
176
189
|
}
|
|
@@ -204,5 +204,18 @@
|
|
|
204
204
|
"init": null,
|
|
205
205
|
"why": "line 9: not JSON"
|
|
206
206
|
}
|
|
207
|
+
],
|
|
208
|
+
"unrecorded": [
|
|
209
|
+
{
|
|
210
|
+
"id": "fx-orphan",
|
|
211
|
+
"title": "An initiative with a PRD and no record",
|
|
212
|
+
"artifacts": [
|
|
213
|
+
{
|
|
214
|
+
"name": "fx-orphan-prd.md",
|
|
215
|
+
"path": "planning/prd/fx-orphan-prd.md",
|
|
216
|
+
"role": "PRD"
|
|
217
|
+
}
|
|
218
|
+
]
|
|
219
|
+
}
|
|
207
220
|
]
|
|
208
221
|
}
|
|
@@ -173,5 +173,110 @@
|
|
|
173
173
|
"path": "planning/reports/definitions/biweekly.md",
|
|
174
174
|
"audience": "leadership"
|
|
175
175
|
}
|
|
176
|
+
},
|
|
177
|
+
"slides": {
|
|
178
|
+
"v": 1,
|
|
179
|
+
"meta": {
|
|
180
|
+
"product": "northwind",
|
|
181
|
+
"cadence": "Biweekly",
|
|
182
|
+
"slot": "2026-10-16",
|
|
183
|
+
"today": "2026-10-16",
|
|
184
|
+
"last": {
|
|
185
|
+
"from": "2026-10-02",
|
|
186
|
+
"to": "2026-10-16"
|
|
187
|
+
},
|
|
188
|
+
"next": {
|
|
189
|
+
"from": "2026-10-16",
|
|
190
|
+
"to": "2026-10-30"
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
"roadmap": {
|
|
194
|
+
"months": [
|
|
195
|
+
"2026-09",
|
|
196
|
+
"2026-10",
|
|
197
|
+
"2026-11",
|
|
198
|
+
"2026-12",
|
|
199
|
+
"2027-01",
|
|
200
|
+
"2027-02"
|
|
201
|
+
],
|
|
202
|
+
"today": "2026-10-16",
|
|
203
|
+
"groups": [
|
|
204
|
+
{
|
|
205
|
+
"name": "Now",
|
|
206
|
+
"rows": [
|
|
207
|
+
{
|
|
208
|
+
"id": "partner-sso",
|
|
209
|
+
"title": "Partner SSO",
|
|
210
|
+
"estimated": false,
|
|
211
|
+
"segments": [
|
|
212
|
+
{
|
|
213
|
+
"stage": "build",
|
|
214
|
+
"label": "Build",
|
|
215
|
+
"start": "2026-10-01",
|
|
216
|
+
"due": "2026-10-24"
|
|
217
|
+
}
|
|
218
|
+
],
|
|
219
|
+
"milestones": [
|
|
220
|
+
{
|
|
221
|
+
"title": "Pilot live",
|
|
222
|
+
"due": "2026-10-24",
|
|
223
|
+
"done": false
|
|
224
|
+
}
|
|
225
|
+
]
|
|
226
|
+
}
|
|
227
|
+
]
|
|
228
|
+
}
|
|
229
|
+
]
|
|
230
|
+
},
|
|
231
|
+
"list": [
|
|
232
|
+
{
|
|
233
|
+
"id": "partner-sso",
|
|
234
|
+
"title": "Partner SSO",
|
|
235
|
+
"status": "Build",
|
|
236
|
+
"pct": 58,
|
|
237
|
+
"next_milestone": {
|
|
238
|
+
"title": "Pilot live",
|
|
239
|
+
"due": "2026-10-24",
|
|
240
|
+
"overdue": false
|
|
241
|
+
},
|
|
242
|
+
"release": "2026-11-20"
|
|
243
|
+
}
|
|
244
|
+
],
|
|
245
|
+
"initiatives": [
|
|
246
|
+
{
|
|
247
|
+
"id": "partner-sso",
|
|
248
|
+
"title": "Partner SSO",
|
|
249
|
+
"status": "Build",
|
|
250
|
+
"pct": 58,
|
|
251
|
+
"done": 7,
|
|
252
|
+
"total": 12,
|
|
253
|
+
"next_milestone": {
|
|
254
|
+
"title": "Pilot live",
|
|
255
|
+
"due": "2026-10-24",
|
|
256
|
+
"overdue": false
|
|
257
|
+
},
|
|
258
|
+
"release": "2026-11-20",
|
|
259
|
+
"last": [
|
|
260
|
+
{
|
|
261
|
+
"title": "SAML metadata",
|
|
262
|
+
"stage": "Build",
|
|
263
|
+
"owner": "Sheryl",
|
|
264
|
+
"milestone": false,
|
|
265
|
+
"done": true,
|
|
266
|
+
"date": null
|
|
267
|
+
}
|
|
268
|
+
],
|
|
269
|
+
"next": [
|
|
270
|
+
{
|
|
271
|
+
"title": "Pilot live",
|
|
272
|
+
"stage": "Build",
|
|
273
|
+
"owner": null,
|
|
274
|
+
"milestone": true,
|
|
275
|
+
"done": false,
|
|
276
|
+
"date": "2026-10-24"
|
|
277
|
+
}
|
|
278
|
+
]
|
|
279
|
+
}
|
|
280
|
+
]
|
|
176
281
|
}
|
|
177
282
|
}
|
|
@@ -73,6 +73,16 @@ Stage: Build · On track · 7 of 12 (+4)
|
|
|
73
73
|
|
|
74
74
|
- Billing v2 (billing-v2), 8 Oct
|
|
75
75
|
|
|
76
|
+
**Changelog** (your release notes for the changelog slide)
|
|
77
|
+
|
|
78
|
+
<!-- pmos:note report.changelog -->
|
|
79
|
+
Launch · Exports: Audit log CSV export
|
|
80
|
+
|
|
81
|
+
Admins can download the audit log as a CSV file.
|
|
82
|
+
|
|
83
|
+
- Added: CSV writer for the audit log
|
|
84
|
+
<!-- /pmos:note -->
|
|
85
|
+
|
|
76
86
|
## Decided this cycle
|
|
77
87
|
|
|
78
88
|
- Audit log export moved to Now, 3 Oct (planning/runs/placements.jsonl)
|
|
@@ -95,3 +105,5 @@ For leadership only.
|
|
|
95
105
|
**Not reached**
|
|
96
106
|
|
|
97
107
|
- tracker: not refreshed since 2 Oct
|
|
108
|
+
|
|
109
|
+
<!-- pmos:slides v1 {"v":1,"meta":{"product":"northwind","cadence":"Biweekly","slot":"2026-10-16","today":"2026-10-16","last":{"from":"2026-10-02","to":"2026-10-16"},"next":{"from":"2026-10-16","to":"2026-10-30"}},"roadmap":{"months":["2026-09","2026-10","2026-11","2026-12","2027-01","2027-02"],"today":"2026-10-16","groups":[{"name":"Now","rows":[{"id":"audit-log","title":"Audit log export","estimated":false,"segments":[{"stage":"spec","label":"Spec","start":"2026-09-14","due":"2026-10-03"},{"stage":"build","label":"Build","start":"2026-10-04","due":"2026-10-28"},{"stage":"evaluate","label":"Evaluate","start":"2026-10-29","due":"2026-11-12"}],"milestones":[{"title":"Release","due":"2026-11-12","done":false}]},{"id":"partner-sso","title":"Partner SSO","estimated":false,"segments":[{"stage":"build","label":"Build","start":"2026-10-01","due":"2026-10-24"}],"milestones":[{"title":"Pilot live","due":"2026-10-24","done":false}]}]},{"name":"Next","rows":[{"id":"billing-v2","title":"Billing v2","estimated":true,"segments":[{"stage":"intake","label":"Intake","start":"2026-11-02","due":"2026-11-20"},{"stage":"spec","label":"Spec","start":"2026-11-23","due":"2026-12-18"}],"milestones":[]}]}]},"list":[{"id":"audit-log","title":"Audit log export","status":"Evaluate","pct":60,"next_milestone":{"title":"Retention rule","due":"2026-10-10","overdue":true},"release":"2026-11-12"},{"id":"partner-sso","title":"Partner SSO","status":"Build","pct":58,"next_milestone":{"title":"Pilot live","due":"2026-10-24","overdue":false},"release":null}],"initiatives":[{"id":"audit-log","title":"Audit log export","status":"Evaluate","pct":60,"done":3,"total":5,"next_milestone":{"title":"Retention rule","due":"2026-10-10","overdue":true},"release":"2026-11-12","last":[{"title":"CSV writer","stage":"Build","owner":"Sheryl","milestone":false,"done":true,"date":null},{"title":"Retention rule","stage":"Build","owner":"Ricky","milestone":true,"done":false,"date":"2026-10-10"}],"next":[{"title":"Retention rule","stage":"Build","owner":"Ricky","milestone":true,"done":false,"date":"2026-10-10"},{"title":"Export UI","stage":"Build","owner":"Lasantha","milestone":false,"done":false,"date":null}]},{"id":"partner-sso","title":"Partner SSO","status":"Build","pct":58,"done":7,"total":12,"next_milestone":{"title":"Pilot live","due":"2026-10-24","overdue":false},"release":null,"last":[],"next":[{"title":"Pilot live","stage":"Build","owner":null,"milestone":true,"done":false,"date":"2026-10-24"},{"title":"Pilot partners on SSO","stage":"Build","owner":"Agent","milestone":false,"done":false,"date":null}]}]} -->
|
|
@@ -67,7 +67,7 @@ The palette reads the lists of all three scopes when it opens. When a list canno
|
|
|
67
67
|
|
|
68
68
|
The palette opens as a sheet from the bottom edge. Each row is at least 48 pixels high, and the rows are reached by touch.
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
On a phone, the scratchpad is the **compose line** at the bottom of a terminal tab. (On a desktop, open it with **▲ scratchpad** under the dock's tabs.) Type a message there and press **Send**. The text is typed into the session and the line clears. The line keeps your text for each session until you send it, so a message you did not finish is still there when you come back.
|
|
71
71
|
|
|
72
72
|
## Related
|
|
73
73
|
|
package/workspace/guide/index.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
type: Guide
|
|
3
3
|
title: "Guide: start here"
|
|
4
|
-
timestamp: 2026-10-
|
|
4
|
+
timestamp: 2026-10-06
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Guide: start here
|
|
@@ -33,6 +33,7 @@ On a desktop the workspace is one frame. [Layout](layout.md) explains each part.
|
|
|
33
33
|
|
|
34
34
|
## Where to read next
|
|
35
35
|
|
|
36
|
+
- [Start the workspace without a terminal](launcher.md): the PMOS app, the login item and `pmos launcher`.
|
|
36
37
|
- [Layout](layout.md): the frame, the keys and the rule for where a list or a detail opens.
|
|
37
38
|
- [Statuses](statuses.md): what each status means, where it comes from, and what you can change.
|
|
38
39
|
- [Initiatives and next steps](initiatives-and-next-steps.md): the detail, the next steps, and the artifacts.
|
|
@@ -50,3 +51,4 @@ On a desktop the workspace is one frame. [Layout](layout.md) explains each part.
|
|
|
50
51
|
- [Initiatives › Timeline](initiatives-timeline.md): when each initiative runs, one row per initiative, and how to reorder rows by their grip.
|
|
51
52
|
- [Board](board.md): one Kanban for the items of every initiative, its filters, Release, and `pmos board`.
|
|
52
53
|
- [Integrated work](integrated-work.md): how `pmos verify-work` turns a merged commit into Done, and what "Done · your call · not merged" means.
|
|
54
|
+
- [Report slides](report-slides.md): how the Reports page shows a report as the PM-OS biweekly slides, Present and Print / PDF, and how your notes become the words on the slides.
|
|
@@ -29,6 +29,7 @@ The first time you open the Roadmap, the file has no lines yet. The workspace th
|
|
|
29
29
|
|
|
30
30
|
- An initiative with activity in the last 5 days reads **Now, unconfirmed**. Move it to confirm it, or move it to Next or Later.
|
|
31
31
|
- An initiative with no activity for 45 days reads **dormant**, at the end of Later.
|
|
32
|
+
- Documents with no initiative record (a PRD and rubric, or a prototype folder) are listed after dormant, under **with no record**. They are not placed.
|
|
32
33
|
- Every other initiative is in Later, the most recent activity first.
|
|
33
34
|
- An accepted or stopped initiative is never in Now. It goes to the end of Later.
|
|
34
35
|
|
|
@@ -30,6 +30,7 @@ Only you can move an initiative into Now. The workspace writes the line as yours
|
|
|
30
30
|
- **unconfirmed.** An initiative that reads Now only because it is active, with no line from you. Confirm it in the Cycle review.
|
|
31
31
|
- **proposed.** An agent proposed a move. It changes nothing until you keep it in the Cycle review.
|
|
32
32
|
- **Dormant.** An initiative with no activity for a long time. Press the dormant line under Later to show them.
|
|
33
|
+
- **No record yet.** A PRD, rubric or prototype folder that no initiative record points at. Press the "with no record" line under Later to show them. In List they are the last group. Press one to open its PRD. Only a record can be placed, so these have no menu.
|
|
33
34
|
|
|
34
35
|
## The List
|
|
35
36
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Guide
|
|
3
|
+
title: "Start the workspace without a terminal"
|
|
4
|
+
timestamp: 2026-10-06
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Start the workspace without a terminal
|
|
8
|
+
|
|
9
|
+
`pmos launcher` makes a PMOS app and a login item. After you set it up once, you do not need a terminal to start the workspace.
|
|
10
|
+
|
|
11
|
+
## Set it up once
|
|
12
|
+
|
|
13
|
+
Open a terminal in the repository that the app must open. Type:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
pmos launcher on
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
To set up a different repository, give its path: `pmos launcher on ~/code/my-product`.
|
|
20
|
+
|
|
21
|
+
This command writes two things:
|
|
22
|
+
|
|
23
|
+
| Thing | macOS | Linux |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| The PMOS app | `~/Applications/PMOS.app`. Open it from Spotlight or Launchpad, or drag it to the Dock. | An app menu entry, `~/.local/share/applications/pmos.desktop`. |
|
|
26
|
+
| The login item | `~/Library/LaunchAgents/com.pmos.workspace.plist` | `~/.config/autostart/pmos-workspace.desktop` |
|
|
27
|
+
|
|
28
|
+
To make only the app, without the login item, type `pmos launcher on --no-login`.
|
|
29
|
+
|
|
30
|
+
## What each one does
|
|
31
|
+
|
|
32
|
+
- **The app** opens the workspace in your browser. When no server runs, it starts one first. It does the same thing as `pmos` in a terminal.
|
|
33
|
+
- **The login item** starts the workspace server when you log in. It does not open a browser window. Open the app when you want the window.
|
|
34
|
+
|
|
35
|
+
Both run `pmos up` on the repository you chose. They use the PATH that your terminal had when you typed `pmos launcher on`. That is how they find `claude`, `git` and `tmux`.
|
|
36
|
+
|
|
37
|
+
What each start printed is in `launcher.log`, in the workspace state folder (`~/Library/Application Support/pmos` on macOS, `~/.local/state/pmos` on Linux).
|
|
38
|
+
|
|
39
|
+
## Check it
|
|
40
|
+
|
|
41
|
+
Type `pmos launcher status`. It shows the repository, the node program and the `pmos` program that the app uses, and whether the login item is installed. A path marked "not found" stops the app. If you move or upgrade node, type `pmos launcher on` again.
|
|
42
|
+
|
|
43
|
+
## Remove it
|
|
44
|
+
|
|
45
|
+
Type `pmos launcher off`. It removes the app and the login item. It removes only files that `pmos launcher on` wrote. A file with the same name that you wrote yourself stays.
|
|
46
|
+
|
|
47
|
+
## Limits
|
|
48
|
+
|
|
49
|
+
- The login item never starts remote (tailnet) mode, and it writes no token. To open the workspace from your phone, start remote mode yourself as `workspace/REMOTE.md` tells you.
|
|
50
|
+
- Native Windows is not supported. The workspace needs WSL on Windows. Type `pmos launcher on` inside WSL.
|
|
51
|
+
- The login item runs once, at login. If the server stops, open the app to start it again.
|