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.
Files changed (75) hide show
  1. package/package.json +1 -1
  2. package/payload/KIT-VERSION +1 -1
  3. package/payload/planning/evals/eval-calibration.md +1 -1
  4. package/payload/planning/okrs/health-budgets.md +1 -1
  5. package/payload/state/discoveries.md +1 -1
  6. package/workspace/README.md +2 -0
  7. package/workspace/REMOTE.md +107 -65
  8. package/workspace/bin/pmos.js +4 -2
  9. package/workspace/first-paint-audit.js +6 -2
  10. package/workspace/fixtures/initiatives-api/list.json +13 -0
  11. package/workspace/fixtures/initiatives-api/roadmap.json +13 -0
  12. package/workspace/fixtures/report-api/draft.json +105 -0
  13. package/workspace/fixtures/reports-page/draft-biweekly-2026-10-16.md +12 -0
  14. package/workspace/guide/command-palette.md +1 -1
  15. package/workspace/guide/index.md +3 -1
  16. package/workspace/guide/initiatives-roadmap.md +1 -0
  17. package/workspace/guide/initiatives-screen.md +1 -0
  18. package/workspace/guide/launcher.md +51 -0
  19. package/workspace/guide/layout.md +7 -2
  20. package/workspace/guide/proactive-engine.md +21 -5
  21. package/workspace/guide/report-slides.md +28 -0
  22. package/workspace/guide/reports.md +2 -0
  23. package/workspace/lib/auto-sync.js +83 -0
  24. package/workspace/lib/board-fold.js +2 -2
  25. package/workspace/lib/breakdown-export.js +1 -6
  26. package/workspace/lib/brief.js +20 -2
  27. package/workspace/lib/goals.js +165 -0
  28. package/workspace/lib/initiative-rows.js +7 -3
  29. package/workspace/lib/launcher-cli.js +207 -0
  30. package/workspace/lib/plan-tree.js +15 -2
  31. package/workspace/lib/pmos-verbs.js +3 -0
  32. package/workspace/lib/proactive-cli.js +58 -2
  33. package/workspace/lib/proactive.js +2 -2
  34. package/workspace/lib/qr.js +293 -0
  35. package/workspace/lib/remote-access.js +184 -12
  36. package/workspace/lib/report-draft.js +34 -6
  37. package/workspace/lib/report-slides-data.js +141 -0
  38. package/workspace/lib/routes-initiatives.js +35 -2
  39. package/workspace/lib/routes-reports.js +3 -3
  40. package/workspace/lib/routes-timeline.js +1 -1
  41. package/workspace/lib/sync.js +61 -6
  42. package/workspace/package-lock.json +1 -1
  43. package/workspace/package.json +1 -1
  44. package/workspace/public/icons/apple-touch-icon.png +0 -0
  45. package/workspace/public/icons/icon-192.png +0 -0
  46. package/workspace/public/icons/icon-512.png +0 -0
  47. package/workspace/public/index.html +11 -0
  48. package/workspace/public/pm/app.js +55 -9
  49. package/workspace/public/pm/components.css +865 -1
  50. package/workspace/public/pm/dest-reports.js +8 -2
  51. package/workspace/public/pm/dest-today.js +8 -2
  52. package/workspace/public/pm/dock.js +73 -3
  53. package/workspace/public/pm/frame-copy.js +5 -0
  54. package/workspace/public/pm/initiative-list.js +18 -1
  55. package/workspace/public/pm/initiatives-copy.js +4 -0
  56. package/workspace/public/pm/panel-tabs.js +14 -12
  57. package/workspace/public/pm/print.css +11 -0
  58. package/workspace/public/pm/report-slides.js +438 -0
  59. package/workspace/public/pm/report-view.js +8 -2
  60. package/workspace/public/pm/reports-copy.js +3 -0
  61. package/workspace/public/pm/roadmap.js +23 -0
  62. package/workspace/public/pm/slides-copy.js +52 -0
  63. package/workspace/public/pm/today-copy.js +29 -0
  64. package/workspace/public/pm/today-goals.js +172 -0
  65. package/workspace/public/pm/tokens.css +13 -0
  66. package/workspace/public/pm/topbar.js +5 -2
  67. package/workspace/replaced-tests.json +36 -1
  68. package/workspace/server.js +165 -15
  69. package/workspace/skills/morning-brief.skill +15 -9
  70. package/workspace/skills/report-biweekly.skill +1 -0
  71. package/workspace/test-files.json +4 -0
  72. package/workspace/test-lists/run6.json +9 -0
  73. package/workspace/test-lists/workspace-launcher.json +5 -0
  74. package/workspace/test-lists/workspace-phone-app.json +5 -0
  75. 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.0",
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": {
@@ -1,3 +1,3 @@
1
- kernel-source: pmos@aa14199
1
+ kernel-source: pmos@439620f
2
2
  assembled: 2026-10-06
3
3
  mode: vendor
@@ -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-06T00:08:33Z"}
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-06T00:08:33Z"}
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-06T00:08:33Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-06T02:40:00Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -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>
@@ -1,32 +1,23 @@
1
1
  ---
2
2
  type: Reference
3
3
  title: "Workspace remote access over a private tailnet (Tailscale)"
4
- timestamp: 2026-07-26
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
- **Copy the token now** — it is shown only once and never logged again.
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.** In the phone's browser (on the same tailnet), open:
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
- http://100.x.y.z:7433/?token=<token>
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
- The client attaches to the **same live sessions** the desktop sees; the token authorizes both the
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
- `100.64.0.0/10` tailnet address. `0.0.0.0`, `::`, LAN, and public IPs are refused — the server
81
- exits with a clear error rather than binding them. There is no code path to a public bind.
82
- - **Token beyond loopback.** With remote mode on, every non-loopback HTTP request and WebSocket
83
- upgrade must present the bearer token, or it is rejected (`401` / socket closed). Loopback requests
84
- stay token-free for local development. The comparison is constant-time.
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
- another of your own devices and needs no internet exposure. If you nonetheless expose an instance to
104
- the internet, D64 permits it **only after all three of the following are in place — never before, and
105
- never as a raw public bind**:
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
- 1. **The bearer token for every non-loopback peer** (already enforced in remote mode).
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
- When a reverse proxy forwards to the workspace, keep the forwarded `Host` set to the loopback/tailnet
117
- host the server actually binds, so the Origin/Host allowlist still recognises same-origin requests.
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
- - **"refusing to bind … only loopback or a Tailscale tailnet address (100.64.0.0/10) is permitted"**
122
- — you passed `PMOS_BIND_ADDR` (or `PMOS_TAILNET_IP`) that isn't loopback or tailnet. Use your real
123
- `tailscale ip -4` value.
124
- - **"no Tailscale tailnet address … was found"** — Tailscale isn't up on this machine. Run
125
- `tailscale up`, or set `PMOS_TAILNET_IP`.
126
- - **"cannot bind 100.x.y.z:PORT — that address is not present on this machine"** — the pinned
127
- `PMOS_TAILNET_IP` isn't actually assigned to this machine. Check `tailscale ip -4`.
128
- - **Phone gets 401** — the token is missing or wrong. Re-open with `?token=<token>` (or send
129
- `Authorization: Bearer <token>`). Restarting the server without `PMOS_TOKEN` mints a new token.
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.
@@ -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
- const NO_OPEN = process.env.PMOS_NO_OPEN === '1'; // tests set this
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
- const EXPECT = Object.freeze({ today: 17, initiatives: 24, board: 33, reports: 14 });
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
- There is no separate capture box. The scratchpad remains in one place only: the **compose line** at the bottom of a terminal tab. 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.
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
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  type: Guide
3
3
  title: "Guide: start here"
4
- timestamp: 2026-10-05
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.