create-pmos 0.53.0 → 0.53.1
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 +24 -63
- package/workspace/bin/pmos.js +4 -2
- package/workspace/guide/command-palette.md +1 -1
- package/workspace/guide/index.md +2 -1
- package/workspace/guide/launcher.md +51 -0
- package/workspace/guide/layout.md +4 -0
- package/workspace/guide/proactive-engine.md +21 -5
- 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 +93 -1
- package/workspace/lib/report-draft.js +1 -2
- package/workspace/lib/routes-timeline.js +1 -1
- 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 +39 -8
- package/workspace/public/pm/components.css +82 -1
- 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/panel-tabs.js +14 -12
- package/workspace/public/pm/today-copy.js +29 -0
- package/workspace/public/pm/today-goals.js +172 -0
- package/workspace/replaced-tests.json +13 -0
- package/workspace/server.js +134 -4
- package/workspace/skills/morning-brief.skill +15 -9
- package/workspace/test-files.json +2 -0
- package/workspace/test-lists/workspace-launcher.json +5 -0
- package/workspace/test-lists/workspace-phone-app.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.1",
|
|
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-06T01:59:33Z"}
|
|
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-06T01:59:33Z"}
|
|
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-06T01:59:33Z"}
|
|
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**. It never exposes a public endpoint.
|
|
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,27 @@ 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. 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**:
|
|
106
79
|
|
|
107
80
|
1. **The bearer token for every non-loopback peer** (already enforced in remote mode).
|
|
108
81
|
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.
|
|
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 — Tailscale's own HTTPS (`tailscale serve` / `tailscale funnel`) or a reverse proxy (Caddy, nginx, a tunnel) that holds the certificate and forwards to the loopback/tailnet listener. 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.
|
|
115
83
|
|
|
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.
|
|
84
|
+
When a reverse proxy forwards to the workspace, keep the forwarded `Host` set to the loopback/tailnet host the server actually binds, so the Origin/Host allowlist still recognises same-origin requests.
|
|
118
85
|
|
|
119
86
|
## Troubleshooting
|
|
120
87
|
|
|
121
|
-
- **"refusing to bind … only loopback or a Tailscale tailnet address (100.64.0.0/10) is permitted"**
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
- **
|
|
125
|
-
|
|
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.
|
|
88
|
+
- **"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.
|
|
89
|
+
- **"no Tailscale tailnet address … was found"** — Tailscale isn't up on this machine. Run `tailscale up`, or set `PMOS_TAILNET_IP`.
|
|
90
|
+
- **"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`.
|
|
91
|
+
- **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`.)
|
|
92
|
+
- **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 {
|
|
@@ -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.
|
|
@@ -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.
|
|
@@ -41,6 +41,9 @@ The dock holds your terminals, documents, conversations, skills and web pages. I
|
|
|
41
41
|
- The dock never opens by itself. A tab that opens while the dock is a rail waits in the dock until you open it.
|
|
42
42
|
- **+ Session** in the dock starts a new session. Its menu also has **Session with options** and one item for each skill of the repository. A skill opens in its own tab in the dock.
|
|
43
43
|
- The Web button in the dock opens a web page in a tab.
|
|
44
|
+
- To make the dock wider or narrower, drag its left edge. With the edge focused, the left and right arrow keys change the width in steps. Double-click the edge to go back to the default width. The width is kept in this browser.
|
|
45
|
+
- **Focus** in the dock gives the dock the whole window. The place and the detail are hidden, and the dock stays full width on every place. Press **Exit focus**, or ⌘⇧J (Ctrl-Shift-J), to show the place again. Folding the dock also ends Focus.
|
|
46
|
+
- **▲ scratchpad** in the bar under the tabs opens the scratchpad. The pad keeps one note for each terminal session, and one for the repository when no terminal is in front. **Send to terminal** types the note into the session in front.
|
|
44
47
|
|
|
45
48
|
Folding the dock, or going to another place, does not touch your terminals.
|
|
46
49
|
|
|
@@ -63,6 +66,7 @@ The workspace is neutral. A primary button is the text colour on the page colour
|
|
|
63
66
|
| g t, g i, g b, g r | Show Today, Initiatives, Board or Reports. |
|
|
64
67
|
| ⌘K (Ctrl-K) | Open or close the command palette. |
|
|
65
68
|
| ⌘J (Ctrl-J) | Open or fold the dock. |
|
|
69
|
+
| ⌘⇧J (Ctrl-Shift-J) | Focus: give the dock the whole window, or show the place again. |
|
|
66
70
|
| j, k | Move down or up the rows of the place. |
|
|
67
71
|
| Enter | Open the row that has the focus. |
|
|
68
72
|
| Space | Tick the row. Space never opens a row. On another button of the row, for example ⋯, Space presses that button. |
|
|
@@ -1,20 +1,35 @@
|
|
|
1
1
|
---
|
|
2
2
|
type: Guide
|
|
3
3
|
title: "Guide: Today and the proactive engine"
|
|
4
|
-
description: "What the Today item shows, how the morning brief is made, and how the workspace works out what is waiting for you."
|
|
5
|
-
timestamp: 2026-10-
|
|
4
|
+
description: "What the Today item shows, how your goals for the day are prepared, how the morning brief is made, and how the workspace works out what is waiting for you."
|
|
5
|
+
timestamp: 2026-10-06
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Guide: Today and the proactive engine
|
|
9
9
|
|
|
10
10
|
The today item shows what is waiting for you now. The workspace works it out by itself from the records of your repositories. You do not ask for it.
|
|
11
11
|
|
|
12
|
+
## Goals for today
|
|
13
|
+
|
|
14
|
+
At the top of Today you can list what you want done today. A goal is one line of text, and it can name an initiative.
|
|
15
|
+
|
|
16
|
+
- Open **Goals for today** and type a goal. Press Enter to add it. A day has at most 7 goals, each at most 140 characters.
|
|
17
|
+
- Press **Done**, **Drop**, **Reopen** or **Remove** on a goal. A goal is open, done or dropped.
|
|
18
|
+
- Press **Prepare my day**. The workspace makes the daily 06:45 morning-brief schedule if this repository has none, and runs it now. The session reads your goals, your calendar, and the mentions of you in Slack, Jira and Confluence. It ties each meeting, mention and step that moves a goal to that goal.
|
|
19
|
+
- Each goal shows its steps: yours first, then the agent's. A step comes from the brief, or from a Today card on the goal's initiative. A goal with no step says so. The session does not invent a step.
|
|
20
|
+
- A step is under a goal only when the brief names the goal, or when the step and the goal name the same initiative. The workspace never matches words.
|
|
21
|
+
- An agent step that names an initiative has **Prepare**. It shows the step, the branch and the prompt, the same as background preparation. **Queue it** approves that exact prompt. Background preparation must be on in the settings. Otherwise the queue says so and nothing is queued.
|
|
22
|
+
- What fits no goal is listed under **Also on your plate**, and items the session is less than 0.5 sure about under **Worth asking**.
|
|
23
|
+
- Today also says how much free time you have until 18:00, the event coming up, and which sources were not read.
|
|
24
|
+
- If the brief is for another day, it is not used. Only the Today cards are shown under the goals.
|
|
25
|
+
- The goals are stored in the workspace state folder on this computer, one file for each day (`goals/<date>.json`). They are not committed.
|
|
26
|
+
|
|
12
27
|
## The morning brief
|
|
13
28
|
|
|
14
29
|
The top of Today is the morning brief: a greeting, the day's calendar, the priorities and a list of things worth asking about. The brief is not computed by the workspace. A skill called morning-brief runs in an agent session. The session reads the sources, and then submits one brief with `pmos proactive brief submit`. The workspace server only checks the brief and stores it. It makes no model call and reaches no calendar or mailbox.
|
|
15
30
|
|
|
16
31
|
- Press **schedule the morning brief** in Today to make a daily schedule at 06:45. The schedule is an ordinary scheduled task, so it runs only while the workspace server runs. A sleeping computer gets no brief. A schedule that is made after its time of day for the first time runs within a minute, so pressing the button at 10:00 starts one brief session straight away.
|
|
17
|
-
- Each source is a setting: calendar, wispr, mail, jira and confluence. Mail is off by default and the skill reads only subjects and senders, never bodies. A source that is off cannot be reported as read.
|
|
32
|
+
- Each source is a setting: calendar, wispr, mail, slack, jira and confluence. Mail is off by default and the skill reads only subjects and senders, never bodies. Slack is on by default. In Slack, Jira and Confluence the skill reads the mentions of you: where, who, and a one-line summary. It never copies a message. A source that is off cannot be reported as read.
|
|
18
33
|
- The brief names the day it is for. If that is not today, Today says it is out of date.
|
|
19
34
|
- An item the session is less than 0.5 sure about is shown under worth asking, not as a priority. The workspace sets that bucket from the confidence. The session cannot choose it.
|
|
20
35
|
- The order is fixed: higher confidence first, then your items before the agent's, then by title.
|
|
@@ -85,7 +100,7 @@ A verdict never changes a status, a lifecycle record or an acceptance. It is rec
|
|
|
85
100
|
|---|---|
|
|
86
101
|
| engine on | Off stops automatic scans. The scan now button still works. |
|
|
87
102
|
| watch: needs, returned, review, schedule, stalled | Turn one trigger off. |
|
|
88
|
-
| brief sources | One switch for each of calendar, wispr, mail, jira and confluence. |
|
|
103
|
+
| brief sources | One switch for each of calendar, wispr, mail, slack, jira and confluence. |
|
|
89
104
|
| background prepare | Off by default. On lets the workspace start a session for a plan you approved. |
|
|
90
105
|
| typed scoring | Off by default. On lets the skill send titles and metadata to the Typesafe service. |
|
|
91
106
|
| name | Used in the greeting. |
|
|
@@ -102,6 +117,7 @@ Text that you type in a setting (the name, the stalled days, the quiet hours) or
|
|
|
102
117
|
- `pmos proactive today` lists the items.
|
|
103
118
|
- `pmos proactive scan` scans now.
|
|
104
119
|
- `pmos proactive settings` shows the settings. Add `--enabled`, `--trigger NAME=on|off`, `--stalled-days`, `--quiet HH:MM-HH:MM|none`, `--pause-learning`, `--background-prepare on|off`, `--brief-source NAME=on|off`, `--brief-scoring on|off` or `--name` to change one.
|
|
120
|
+
- `pmos proactive goals` lists today's goals with their steps. `pmos proactive goals add TEXT [--initiative ID]` adds one. `done N`, `drop N`, `reopen N` and `remove N` change the Nth goal. `pmos proactive goals plan` does the same as **Prepare my day**.
|
|
105
121
|
- `pmos proactive brief` shows today's brief. `pmos proactive brief submit --file PATH` submits one. `pmos proactive brief schedule` makes the 06:45 schedule for the current repository.
|
|
106
122
|
- `pmos proactive prepare` shows the queue. `pmos proactive prepare plan --initiative ID` shows a plan and its hash. `pmos proactive prepare queue --initiative ID --item ITEM --sha HASH` approves it. `pmos proactive prepare cancel ID` cancels an entry. `pmos proactive prepare run` looks at the queue now.
|
|
107
123
|
- `pmos proactive learn` shows the rollup, the weights, the narrowed triggers and the lessons. `pmos proactive learn run` takes the weekly review now. `pmos proactive learn lessons` lists the approved lessons. `pmos proactive learn approve ID`, `reject ID` and `retire ID` decide a lesson. `pmos proactive learn restore TRIGGER` brings back a narrowed trigger. `pmos proactive learn reset` makes it ignore earlier feedback.
|
|
@@ -110,7 +126,7 @@ Text that you type in a setting (the name, the stalled days, the quiet hours) or
|
|
|
110
126
|
## Limits
|
|
111
127
|
|
|
112
128
|
- It runs only while the workspace server runs. It does not catch up for time the server was off. A sleeping computer gets no scan.
|
|
113
|
-
- The items from the records come from this computer. The workspace itself does not read your mail
|
|
129
|
+
- The items from the records come from this computer. The workspace itself does not read your mail, your calendar, Slack, Jira or Confluence. Only the morning-brief session does, and only the sources you switched on.
|
|
114
130
|
- It does not start a session, push, send, post, accept or merge. It only shows items and records your feedback, and learns from it as described above. The one exception is background preparation, which starts a session only for a plan you approved, as described above.
|
|
115
131
|
- An unreadable repository is listed as not read. An empty list is not proof that nothing waits there.
|
|
116
132
|
- The records stay in the workspace state folder on this computer. They are not committed.
|
|
@@ -68,8 +68,8 @@ function workInitiatives(root) {
|
|
|
68
68
|
for (const id of eng.listInitiatives(root)) {
|
|
69
69
|
let pl;
|
|
70
70
|
try { pl = planTree.profileAndLane(root, id); } catch { continue; }
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
if (pl.group !== 'work') continue;
|
|
72
|
+
const def = (pl.defs && pl.defs[pl.lane]) || {};
|
|
73
73
|
let title = id;
|
|
74
74
|
if (pl.record) {
|
|
75
75
|
try { title = String((eng.parseLeadingFrontmatter(fs.readFileSync(pl.record, 'utf8')) || {}).title || '').trim() || id; } catch { title = id; }
|
|
@@ -18,7 +18,6 @@
|
|
|
18
18
|
const fs = require('fs');
|
|
19
19
|
const planItems = require('./plan-items.js');
|
|
20
20
|
const planTree = require('./plan-tree.js');
|
|
21
|
-
const rows = require('./initiative-rows.js');
|
|
22
21
|
const consoleState = require('./console-state');
|
|
23
22
|
|
|
24
23
|
const STATE_LABEL = { todo: 'Todo', doing: 'Doing', review: 'Review', done: 'Done' };
|
|
@@ -51,11 +50,7 @@ function exportBreakdown(root, id) {
|
|
|
51
50
|
if (!loaded.ok) return refuse(500, loaded.error);
|
|
52
51
|
let fm = {};
|
|
53
52
|
try { fm = loaded.engine.parseLeadingFrontmatter(fs.readFileSync(file, 'utf8')) || {}; } catch { fm = {}; }
|
|
54
|
-
|
|
55
|
-
let groups;
|
|
56
|
-
try { groups = rows.workGroupOf(root); } catch { groups = {}; }
|
|
57
|
-
const declared = String(fm.lane || '').trim();
|
|
58
|
-
if (groups[Object.prototype.hasOwnProperty.call(groups, declared) ? declared : lane] !== 'work') {
|
|
53
|
+
if (planTree.profileAndLane(root, id).group !== 'work') {
|
|
59
54
|
return refuse(404, `${id} is not on a work lane; a document is a file and has no breakdown to export`);
|
|
60
55
|
}
|
|
61
56
|
const fold = planItems.readFold(root, { now: 0 });
|
package/workspace/lib/brief.js
CHANGED
|
@@ -10,12 +10,16 @@
|
|
|
10
10
|
* greeting(now, name) → the one line Today opens with
|
|
11
11
|
* localDate(now) → YYYY-MM-DD on this machine's clock
|
|
12
12
|
*
|
|
13
|
+
* Goals (initiative `today-goals`, PRD G3): an item or an event may name one of the day's goals with `goal`. With
|
|
14
|
+
* `ctx.goalIds` given, a goal that is not in that day's record is refused. An item may say it is a `mention` of the PM
|
|
15
|
+
* (Slack, Jira, Confluence, mail): a boolean, nothing more.
|
|
16
|
+
*
|
|
13
17
|
* The bucket is derived: an item under WORTH_ASKING confidence is "worth asking", whatever the session said. The order is
|
|
14
18
|
* fixed and stated: higher confidence first, then PM-owned before agent-owned, then by title.
|
|
15
19
|
*/
|
|
16
20
|
const crypto = require('crypto');
|
|
17
21
|
|
|
18
|
-
const SOURCES = ['calendar', 'wispr', 'mail', 'jira', 'confluence'];
|
|
22
|
+
const SOURCES = ['calendar', 'wispr', 'mail', 'slack', 'jira', 'confluence'];
|
|
19
23
|
const ITEM_SOURCES = [...SOURCES, 'derived'];
|
|
20
24
|
const SOURCE_STATES = ['read', 'not-reached', 'off'];
|
|
21
25
|
const OWNERS = ['pm', 'agent'];
|
|
@@ -24,6 +28,7 @@ const WORTH_ASKING = 0.5;
|
|
|
24
28
|
const MAX_ITEMS = 12;
|
|
25
29
|
const MAX_EVENTS = 20;
|
|
26
30
|
const INI_RE = /^[a-z0-9][a-z0-9._-]*$/i;
|
|
31
|
+
const GOAL_RE = /^g-[0-9a-f]{8}$/;
|
|
27
32
|
const clip = (s, n) => String(s == null ? '' : s).replace(/\s+/g, ' ').trim().slice(0, n);
|
|
28
33
|
|
|
29
34
|
const pad = (n) => String(n).padStart(2, '0');
|
|
@@ -40,6 +45,13 @@ const tooLong = (v, n) => typeof v === 'string' && v.trim().length > n;
|
|
|
40
45
|
function validateBrief(raw, ctx = {}) {
|
|
41
46
|
const now = ctx.now || Date.now();
|
|
42
47
|
const settings = ctx.settings || {};
|
|
48
|
+
const goalIds = Array.isArray(ctx.goalIds) ? new Set(ctx.goalIds) : null;
|
|
49
|
+
const badGoal = (g, at) => {
|
|
50
|
+
if (g === undefined || g === null) return null;
|
|
51
|
+
if (typeof g !== 'string' || !GOAL_RE.test(g)) return `${at}: goal is not a goal id`;
|
|
52
|
+
if (goalIds && !goalIds.has(g)) return `${at}: goal ${g} is not one of the goals for that day`;
|
|
53
|
+
return null;
|
|
54
|
+
};
|
|
43
55
|
const on = (src) => !settings.briefSources || settings.briefSources[src] !== false;
|
|
44
56
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return { ok: false, error: 'a brief is a JSON object' };
|
|
45
57
|
const date = raw.date === undefined ? localDate(now) : raw.date;
|
|
@@ -68,7 +80,9 @@ function validateBrief(raw, ctx = {}) {
|
|
|
68
80
|
if (typeof e.start !== 'string' || Number.isNaN(Date.parse(e.start))) return { ok: false, error: `calendar event ${i + 1}: start must be a date and time` };
|
|
69
81
|
if (e.end !== undefined && (typeof e.end !== 'string' || Number.isNaN(Date.parse(e.end)))) return { ok: false, error: `calendar event ${i + 1}: end must be a date and time` };
|
|
70
82
|
if (e.with !== undefined && tooLong(e.with, 120)) return { ok: false, error: `calendar event ${i + 1}: "with" is over 120 characters` };
|
|
71
|
-
|
|
83
|
+
const gErr = badGoal(e.goal, `calendar event ${i + 1}`);
|
|
84
|
+
if (gErr) return { ok: false, error: gErr };
|
|
85
|
+
calendar.push({ start: e.start, end: e.end || null, title: clip(e.title, 120), with: e.with ? clip(e.with, 120) : null, ...(e.goal ? { goal: e.goal } : {}) });
|
|
72
86
|
}
|
|
73
87
|
calendar.sort((a, b) => Date.parse(a.start) - Date.parse(b.start));
|
|
74
88
|
|
|
@@ -90,11 +104,15 @@ function validateBrief(raw, ctx = {}) {
|
|
|
90
104
|
if (!EFFORTS.includes(x.effort)) return { ok: false, error: `${at}: effort must be one of ${EFFORTS.join(', ')}` };
|
|
91
105
|
if (typeof x.confidence !== 'number' || !Number.isFinite(x.confidence) || x.confidence < 0 || x.confidence > 1) return { ok: false, error: `${at}: confidence must be a number from 0 to 1` };
|
|
92
106
|
if (x.initiative_id !== undefined && x.initiative_id !== null && (typeof x.initiative_id !== 'string' || !INI_RE.test(x.initiative_id))) return { ok: false, error: `${at}: initiative_id is not an initiative id` };
|
|
107
|
+
const gErr = badGoal(x.goal, at);
|
|
108
|
+
if (gErr) return { ok: false, error: gErr };
|
|
109
|
+
if (x.mention !== undefined && typeof x.mention !== 'boolean') return { ok: false, error: `${at}: mention must be true or false` };
|
|
93
110
|
const title = clip(x.title, 140);
|
|
94
111
|
items.push({
|
|
95
112
|
id: crypto.createHash('sha1').update(`${date}|${x.source}|${title}`).digest('hex').slice(0, 12),
|
|
96
113
|
title, why_now: clip(x.why_now, 240), evidence: clip(x.evidence, 240), source: x.source, initiative_id: x.initiative_id || null,
|
|
97
114
|
owner: x.owner, effort: x.effort, confidence: x.confidence, worth_asking: x.confidence < WORTH_ASKING,
|
|
115
|
+
...(x.goal ? { goal: x.goal } : {}), ...(x.mention ? { mention: true } : {}),
|
|
98
116
|
});
|
|
99
117
|
}
|
|
100
118
|
items.sort((a, b) => b.confidence - a.confidence || (a.owner === b.owner ? 0 : a.owner === 'pm' ? -1 : 1) || a.title.localeCompare(b.title));
|