create-pmos 0.155.0 → 0.157.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -20,6 +20,22 @@ You never need to write code. You need the courage to say what you want, and the
20
20
 
21
21
  ## Getting started (5 minutes)
22
22
 
23
+ **New computer, nothing installed?** Paste one line. It installs what is missing (Node.js, git, Claude Code), makes a `PMOS` folder in your home folder, adds the kit and opens the PMOS app. No Homebrew, no WSL, and no admin password on Windows. Run it again at any time: it skips what is already there.
24
+
25
+ Mac (Terminal):
26
+
27
+ ```bash
28
+ curl -fsSL https://unpkg.com/create-pmos/install.sh | bash
29
+ ```
30
+
31
+ Windows (PowerShell):
32
+
33
+ ```powershell
34
+ powershell -ExecutionPolicy Bypass -c "iex ((New-Object Net.WebClient).DownloadString('https://unpkg.com/create-pmos/install.ps1'))"
35
+ ```
36
+
37
+ The Mac line also works on Linux. On a Mac without git, Apple's Command Line Tools window opens once: click Install. The first agent session in the app asks you to sign in to Claude.
38
+
23
39
  **Have an existing project?**
24
40
 
25
41
  ```bash
@@ -98,7 +114,8 @@ Some machines lock down npm's global folder. If the activation prints a warning
98
114
 
99
115
  ### What to expect
100
116
 
101
- - **Requirements:** Node 18+. macOS, Linux, and Windows. Activation (`create-pmos workspace`) builds `node-pty` once with a C toolchain — most dev machines already have one; `pmos doctor` names the exact fix if not.
117
+ - **Requirements:** Node 18+. macOS, Windows 10 (1809) or later, and Linux; Windows runs natively, with no WSL. On macOS and Windows `node-pty` ships prebuilt; on Linux, activation builds it once with a C toolchain. `pmos doctor` names the exact fix when something is missing. Setup CI runs the one-line setup on clean macOS, Windows and Linux runners.
118
+ - **Not on Windows yet:** `pmos launcher on` (start at login), and meeting-notes transcription (its engine is Apple Silicon only).
102
119
  - **It's local and private.** The server binds `127.0.0.1` and is never exposed to the network. Your agent CLI (Claude Code, Codex, a plain shell) runs **untouched** — its own login, slash commands, and keybindings all work; the workspace is the terminal, not a wrapper.
103
120
  - **The gates are the same gates.** `approve` / `return` / `accept` in the GUI write the exact file-mode records (`state/acceptances.jsonl`) this kit already uses — only ever on your click. Nothing is recorded automatically.
104
121
  - **Works with or without the kit.** Best in a repo you've run `create-pmos` in (it reads and writes `pmos/state/`), but it opens any repo — a plain project just records into the server's own state.
@@ -84,7 +84,8 @@ const lstat = (p) => { try { return fs.lstatSync(p); } catch { return null; } };
84
84
  const real = (p) => { try { return fs.realpathSync(p); } catch { return null; } };
85
85
 
86
86
  /**
87
- * Link `.claude/skills/<name>` to `pmos/skills-native/<name>` for every kit skill that has none.
87
+ * Link `.claude/skills/<name>` to `pmos/skills-native/<name>` for every kit skill that has none
88
+ * (a relative symlink; a junction on Windows).
88
89
  * @returns {{ linked: string[], ok: string[], blocked: {name:string, reason:string}[], badDescription: string[], errors: string[] }}
89
90
  */
90
91
  function linkSkills(repoRoot) {
@@ -100,10 +101,14 @@ function linkSkills(repoRoot) {
100
101
  if (!descriptionHasText(text)) { out.badDescription.push(e.name); continue; }
101
102
 
102
103
  const link = path.join(dest, e.name);
103
- const target = `../../pmos/skills-native/${e.name}`;
104
+ // Windows refuses a directory symlink unless Developer Mode is on or the shell is elevated. A
105
+ // junction needs neither; it takes an absolute target. Everywhere else the link stays relative,
106
+ // so the repository can move.
107
+ const target = process.platform === 'win32' ? path.join(native, e.name) : `../../pmos/skills-native/${e.name}`;
108
+ const type = process.platform === 'win32' ? 'junction' : 'dir';
104
109
  const st = lstat(link);
105
110
  if (st === null) {
106
- try { fs.mkdirSync(dest, { recursive: true }); fs.symlinkSync(target, link, 'dir'); out.linked.push(e.name); }
111
+ try { fs.mkdirSync(dest, { recursive: true }); fs.symlinkSync(target, link, type); out.linked.push(e.name); }
107
112
  catch (err) { out.errors.push(`${e.name}: ${err.message}`); }
108
113
  continue;
109
114
  }
@@ -113,8 +118,8 @@ function linkSkills(repoRoot) {
113
118
  if (r === null) {
114
119
  let was = '';
115
120
  try { was = fs.readlinkSync(link); } catch { /* keep blocked */ }
116
- if (/(^|\/)pmos\/skills-native\//.test(was)) {
117
- try { fs.unlinkSync(link); fs.symlinkSync(target, link, 'dir'); out.linked.push(e.name); }
121
+ if (/(^|[\\/])pmos[\\/]skills-native[\\/]/.test(was)) {
122
+ try { fs.unlinkSync(link); fs.symlinkSync(target, link, type); out.linked.push(e.name); }
118
123
  catch (err) { out.errors.push(`${e.name}: ${err.message}`); }
119
124
  continue;
120
125
  }
package/install.ps1 ADDED
@@ -0,0 +1,171 @@
1
+ # PMOS one-line setup for Windows (initiative one-line-setup).
2
+ #
3
+ # powershell -ExecutionPolicy Bypass -c "iex ((New-Object Net.WebClient).DownloadString('https://unpkg.com/create-pmos/install.ps1'))"
4
+ #
5
+ # Installs what is missing, makes a PMOS folder and opens the PMOS app. No admin rights, no WSL:
6
+ # 1. Node.js latest LTS from nodejs.org, into %LOCALAPPDATA%\PMOS\node
7
+ # 2. git the official Portable Git, into %LOCALAPPDATA%\PMOS\git (Claude Code needs its bash)
8
+ # 3. Claude Code its own installer, claude.ai/install.ps1
9
+ # 4. the PMOS kit in %USERPROFILE%\PMOS (npx create-pmos)
10
+ # 5. the PMOS app (npx create-pmos workspace), which opens in the browser
11
+ # Every step checks first and skips what is already there, so running the line again is safe.
12
+ #
13
+ # Settings (environment variables):
14
+ # PMOS_DIR=<folder> the PMOS folder (default: %USERPROFILE%\PMOS)
15
+ # PMOS_NO_OPEN=1 do not open the browser at the end
16
+ # PMOS_SKIP_CLAUDE=1 do not install Claude Code
17
+ # PMOS_PKG=<spec> the create-pmos package to run (default: create-pmos@latest)
18
+
19
+ $ErrorActionPreference = 'Stop'
20
+ $ProgressPreference = 'SilentlyContinue' # Invoke-WebRequest is many times slower with the progress bar on
21
+ [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
22
+
23
+ $PmosHome = Join-Path $env:LOCALAPPDATA 'PMOS'
24
+ $PmosDir = if ($env:PMOS_DIR) { $env:PMOS_DIR } else { Join-Path $env:USERPROFILE 'PMOS' }
25
+ $PmosPkg = if ($env:PMOS_PKG) { $env:PMOS_PKG } else { 'create-pmos@latest' }
26
+ $NodeMin = 18
27
+
28
+ function Say($m) { Write-Host "==> $m" -ForegroundColor Cyan }
29
+ function Ok($m) { Write-Host " [ok] $m" -ForegroundColor Green }
30
+ function Fail($m, $try) {
31
+ Write-Host ""
32
+ Write-Host "x $m" -ForegroundColor Red
33
+ if ($try) { Write-Host " Try: $try" }
34
+ # A pasted line runs inside the user's own PowerShell window: throw instead of exit, so the window stays open.
35
+ throw "PMOS setup stopped: $m"
36
+ }
37
+ function Have($c) { [bool](Get-Command $c -ErrorAction SilentlyContinue) }
38
+
39
+ # Put a folder on PATH now and for new windows (the user's own PATH; no admin needed).
40
+ function Add-Path($dir) {
41
+ if (-not (($env:Path -split ';') -contains $dir)) { $env:Path = "$dir;$env:Path" }
42
+ $user = [Environment]::GetEnvironmentVariable('Path', 'User')
43
+ if (-not $user) { $user = '' }
44
+ if (-not (($user -split ';') -contains $dir)) {
45
+ [Environment]::SetEnvironmentVariable('Path', ($dir + ';' + $user).TrimEnd(';'), 'User')
46
+ }
47
+ }
48
+ function Get-File($url, $out) {
49
+ try { Invoke-WebRequest -Uri $url -OutFile $out -UseBasicParsing }
50
+ catch { Fail "Could not download $url" 'check the internet connection, then paste the line again' }
51
+ }
52
+ function Node-Major {
53
+ try { [int]((& node -p "process.versions.node.split('.')[0]") 2>$null) } catch { 0 }
54
+ }
55
+
56
+ $arch = if ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64' -or $env:PROCESSOR_ARCHITEW6432 -eq 'ARM64') { 'arm64' } else { 'x64' }
57
+ New-Item -ItemType Directory -Force -Path $PmosHome | Out-Null
58
+
59
+ Write-Host ""
60
+ Write-Host "PMOS setup - this installs what is missing and opens PMOS."
61
+ Write-Host ""
62
+
63
+ # --- 1. Node.js ---
64
+ Say 'Checking Node.js'
65
+ $nodeDir = Join-Path $PmosHome 'node'
66
+ if (Test-Path (Join-Path $nodeDir 'node.exe')) { Add-Path $nodeDir }
67
+ if ((Have node) -and ((Node-Major) -ge $NodeMin)) {
68
+ Ok "Node.js $(& node -v) is installed"
69
+ } else {
70
+ Say "Installing Node.js (latest LTS) into $nodeDir"
71
+ try { $index = Invoke-RestMethod -Uri 'https://nodejs.org/dist/index.json' -UseBasicParsing }
72
+ catch { Fail 'Could not read the Node.js version list from nodejs.org.' 'check the internet connection, then paste the line again' }
73
+ $ver = ($index | Where-Object { $_.lts } | Select-Object -First 1).version
74
+ $zip = Join-Path $env:TEMP "node-$ver-win-$arch.zip"
75
+ Get-File "https://nodejs.org/dist/$ver/node-$ver-win-$arch.zip" $zip
76
+ $unz = Join-Path $env:TEMP "pmos-node-$ver"
77
+ if (Test-Path $unz) { Remove-Item -Recurse -Force $unz }
78
+ Expand-Archive -Path $zip -DestinationPath $unz -Force
79
+ if (Test-Path $nodeDir) { Remove-Item -Recurse -Force $nodeDir }
80
+ Move-Item -Path (Join-Path $unz "node-$ver-win-$arch") -Destination $nodeDir
81
+ Remove-Item -Force $zip; Remove-Item -Recurse -Force $unz -ErrorAction SilentlyContinue
82
+ Add-Path $nodeDir
83
+ Ok "Node.js $(& node -v) installed"
84
+ }
85
+ # Windows blocks PowerShell scripts by default, and npm's `pmos` and `npx` commands are partly scripts.
86
+ # RemoteSigned for this user only (no admin) is the setting npm's own docs give.
87
+ $policy = Get-ExecutionPolicy -Scope CurrentUser
88
+ if ($policy -eq 'Undefined' -or $policy -eq 'Restricted') {
89
+ try { Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned -Force; Ok 'PowerShell may now run the pmos command (RemoteSigned, this user only)' }
90
+ catch { Write-Host " note: could not change the PowerShell script setting; type pmos.cmd instead of pmos" }
91
+ }
92
+ # npm's global folder is where `pmos` lands. It must exist and be on PATH. It is %APPDATA%\npm unless
93
+ # npm is set up otherwise, so ask npm.
94
+ New-Item -ItemType Directory -Force -Path (Join-Path $env:APPDATA 'npm') | Out-Null
95
+ $npmGlobal = ((& npm.cmd prefix -g) | Select-Object -Last 1).Trim()
96
+ if (-not $npmGlobal) { $npmGlobal = Join-Path $env:APPDATA 'npm' }
97
+ New-Item -ItemType Directory -Force -Path $npmGlobal | Out-Null
98
+ Add-Path $npmGlobal
99
+
100
+ # --- 2. git ---
101
+ Say 'Checking git'
102
+ $gitDir = Join-Path $PmosHome 'git'
103
+ if (Test-Path (Join-Path $gitDir 'cmd\git.exe')) { Add-Path (Join-Path $gitDir 'cmd') }
104
+ if (Have git) {
105
+ Ok (& git --version)
106
+ } else {
107
+ Say "Installing Portable Git into $gitDir"
108
+ try {
109
+ $h = @{ 'User-Agent' = 'pmos-setup' }
110
+ if ($env:GITHUB_TOKEN) { $h['Authorization'] = "Bearer $env:GITHUB_TOKEN" } # CI only: the anonymous rate limit is shared per address
111
+ $rel = Invoke-RestMethod -Uri 'https://api.github.com/repos/git-for-windows/git/releases/latest' -UseBasicParsing -Headers $h
112
+ } catch { Fail 'Could not read the Git for Windows release list from GitHub.' 'check the internet connection, then paste the line again' }
113
+ $want = if ($arch -eq 'arm64') { 'PortableGit-*-arm64.7z.exe' } else { 'PortableGit-*-64-bit.7z.exe' }
114
+ $asset = $rel.assets | Where-Object { $_.name -like $want } | Select-Object -First 1
115
+ if (-not $asset) { Fail 'Could not find Portable Git for this computer.' 'install Git from https://git-scm.com/download/win, then paste the line again' }
116
+ $exe = Join-Path $env:TEMP $asset.name
117
+ Get-File $asset.browser_download_url $exe
118
+ if (Test-Path $gitDir) { Remove-Item -Recurse -Force $gitDir }
119
+ # A self-extracting 7-Zip archive: -o is the target folder, -y answers yes. No installer, no admin.
120
+ $p = Start-Process -FilePath $exe -ArgumentList "-o`"$gitDir`"", '-y' -Wait -PassThru -WindowStyle Hidden
121
+ Remove-Item -Force $exe -ErrorAction SilentlyContinue
122
+ if ($p.ExitCode -ne 0 -or -not (Test-Path (Join-Path $gitDir 'cmd\git.exe'))) { Fail 'Portable Git did not unpack.' 'paste the line again' }
123
+ Add-Path (Join-Path $gitDir 'cmd')
124
+ Ok (& git --version)
125
+ }
126
+ # Claude Code runs its commands in Git Bash. Tell it where bash is when git came from Portable Git.
127
+ $bash = Join-Path $gitDir 'bin\bash.exe'
128
+ if ((Test-Path $bash) -and -not $env:CLAUDE_CODE_GIT_BASH_PATH) {
129
+ [Environment]::SetEnvironmentVariable('CLAUDE_CODE_GIT_BASH_PATH', $bash, 'User')
130
+ $env:CLAUDE_CODE_GIT_BASH_PATH = $bash
131
+ }
132
+
133
+ # --- 3. Claude Code ---
134
+ Say 'Checking Claude Code'
135
+ $claudeBin = Join-Path $env:USERPROFILE '.local\bin'
136
+ if (Test-Path (Join-Path $claudeBin 'claude.exe')) { Add-Path $claudeBin }
137
+ if ($env:PMOS_SKIP_CLAUDE -eq '1') {
138
+ Ok 'skipped (PMOS_SKIP_CLAUDE=1)'
139
+ } elseif (Have claude) {
140
+ Ok 'Claude Code is installed'
141
+ } else {
142
+ Say 'Installing Claude Code'
143
+ # A child PowerShell: the Claude Code installer may end its own session, which must not end this one.
144
+ & powershell -NoProfile -ExecutionPolicy Bypass -Command "irm https://claude.ai/install.ps1 | iex"
145
+ if (Test-Path (Join-Path $claudeBin 'claude.exe')) { Add-Path $claudeBin }
146
+ if (Have claude) { Ok 'Claude Code installed' }
147
+ else { Fail 'Claude Code did not install.' 'run: irm https://claude.ai/install.ps1 | iex then paste the line again' }
148
+ }
149
+
150
+ # --- 4. The PMOS folder and kit ---
151
+ Say "Setting up the PMOS folder: $PmosDir"
152
+ New-Item -ItemType Directory -Force -Path $PmosDir | Out-Null
153
+ if (-not (Test-Path (Join-Path $PmosDir '.git'))) {
154
+ & git -C $PmosDir init -q
155
+ Ok 'git folder created'
156
+ }
157
+ if (Test-Path (Join-Path $PmosDir 'pmos\AGENTS.md')) {
158
+ Ok 'the PMOS kit is already in this folder'
159
+ } else {
160
+ & npx.cmd -y -p $PmosPkg create-pmos $PmosDir
161
+ if ($LASTEXITCODE -ne 0) { Fail 'Could not add the PMOS kit.' 'paste the line again; if it fails again, send the text above to your PMOS contact' }
162
+ Ok 'PMOS kit added'
163
+ }
164
+
165
+ # --- 5. The PMOS app ---
166
+ Say 'Starting the PMOS app'
167
+ & npx.cmd -y -p $PmosPkg create-pmos workspace $PmosDir
168
+ if ($LASTEXITCODE -ne 0) { Fail 'The PMOS app did not start.' 'run: pmos doctor and send what it prints to your PMOS contact' }
169
+
170
+ Write-Host ""
171
+ Ok 'PMOS is ready. Next time, open PowerShell and type: pmos'
package/install.sh ADDED
@@ -0,0 +1,184 @@
1
+ #!/usr/bin/env bash
2
+ # PMOS one-line setup for macOS and Linux (initiative one-line-setup).
3
+ #
4
+ # curl -fsSL https://unpkg.com/create-pmos/install.sh | bash
5
+ #
6
+ # Installs what is missing, makes a PMOS folder and opens the PMOS app:
7
+ # 1. Node.js (latest LTS from nodejs.org, into ~/.pmos/node; no Homebrew, no admin)
8
+ # 2. git (macOS: Apple's Command Line Tools, one system dialog; Linux: the package manager)
9
+ # 3. Claude Code (its own installer, claude.ai/install.sh)
10
+ # 4. the PMOS kit in ~/PMOS (npx create-pmos)
11
+ # 5. the PMOS app (npx create-pmos workspace), which opens in the browser
12
+ # Every step checks first and skips what is already there, so running the line again is safe.
13
+ #
14
+ # Settings (environment variables):
15
+ # PMOS_DIR=<folder> the PMOS folder (default: ~/PMOS)
16
+ # PMOS_NO_OPEN=1 do not open the browser at the end
17
+ # PMOS_SKIP_CLAUDE=1 do not install Claude Code
18
+ # PMOS_PKG=<spec> the create-pmos package to run (default: create-pmos@latest)
19
+ set -euo pipefail
20
+
21
+ PMOS_HOME="${PMOS_HOME:-$HOME/.pmos}"
22
+ PMOS_DIR="${PMOS_DIR:-$HOME/PMOS}"
23
+ PMOS_PKG="${PMOS_PKG:-create-pmos@latest}"
24
+ NODE_MIN=18
25
+ MARK="# added by PMOS setup"
26
+
27
+ say() { printf '\033[1m==>\033[0m %s\n' "$*"; }
28
+ ok() { printf ' \033[32m✓\033[0m %s\n' "$*"; }
29
+ fail() { printf '\n\033[31m✗ %s\033[0m\n' "$1" >&2; [ -n "${2:-}" ] && printf ' Try: %s\n' "$2" >&2; exit 1; }
30
+ have() { command -v "$1" >/dev/null 2>&1; }
31
+
32
+ OS="$(uname -s)"
33
+ case "$OS" in
34
+ Darwin) PLAT=darwin ;;
35
+ Linux) PLAT=linux ;;
36
+ *) fail "This setup is for macOS and Linux. On Windows, paste the PowerShell line instead." \
37
+ "powershell -ExecutionPolicy Bypass -c \"iex ((New-Object Net.WebClient).DownloadString('https://unpkg.com/create-pmos/install.ps1'))\"" ;;
38
+ esac
39
+ case "$(uname -m)" in
40
+ arm64|aarch64) ARCH=arm64 ;;
41
+ x86_64|amd64) ARCH=x64 ;;
42
+ *) fail "This computer's processor ($(uname -m)) is not supported." ;;
43
+ esac
44
+
45
+ # The shell start-up files that new Terminal windows read, so PATH changes last. On Linux a new window
46
+ # reads ~/.bashrc and a login reads ~/.bash_profile or ~/.profile, so both get the line.
47
+ profile_files() {
48
+ case "${SHELL:-}" in
49
+ */zsh) echo "$HOME/.zprofile" ;;
50
+ */bash) if [ "$PLAT" = darwin ]; then echo "$HOME/.bash_profile"
51
+ else
52
+ echo "$HOME/.bashrc"
53
+ # A login shell reads ~/.bash_profile instead of ~/.profile when it exists.
54
+ if [ -f "$HOME/.bash_profile" ]; then echo "$HOME/.bash_profile"; else echo "$HOME/.profile"; fi
55
+ fi ;;
56
+ *) echo "$HOME/.profile" ;;
57
+ esac
58
+ }
59
+ # Put a folder on PATH now and in new Terminal windows. Adds the line once per file.
60
+ add_path() {
61
+ local dir="$1" f
62
+ case ":$PATH:" in *":$dir:"*) ;; *) export PATH="$dir:$PATH" ;; esac
63
+ for f in $(profile_files); do
64
+ touch "$f"
65
+ grep -qsF "$dir" "$f" || printf '\nexport PATH="%s:$PATH" %s\n' "$dir" "$MARK" >> "$f"
66
+ done
67
+ }
68
+
69
+ node_major() { node -p 'process.versions.node.split(".")[0]' 2>/dev/null || echo 0; }
70
+
71
+ echo
72
+ echo "PMOS setup — this installs what is missing and opens PMOS."
73
+ echo
74
+
75
+ # ── 1. Node.js ────────────────────────────────────────────────────────────────────────────────
76
+ say "Checking Node.js"
77
+ [ -x "$PMOS_HOME/node/bin/node" ] && add_path "$PMOS_HOME/node/bin"
78
+ if have node && [ "$(node_major)" -ge "$NODE_MIN" ]; then
79
+ ok "Node.js $(node -v) is installed"
80
+ else
81
+ say "Installing Node.js (latest LTS) into $PMOS_HOME/node"
82
+ have curl || fail "curl is missing." "install curl, then paste the line again"
83
+ # The list is newest first; the first entry whose "lts" is not false is the latest LTS. awk reads to the end
84
+ # (no early exit), so nothing upstream is cut off mid-write under pipefail.
85
+ ver="$(curl -fsSL https://nodejs.org/dist/index.json | tr '{' '\n' \
86
+ | awk '!found && /"version":"v/ && !/"lts":false/ { sub(/.*"version":"/, ""); sub(/".*/, ""); print; found = 1 }')"
87
+ [ -n "$ver" ] || fail "Could not read the Node.js version list from nodejs.org." "check the internet connection, then paste the line again"
88
+ tmp="$(mktemp -d)"
89
+ curl -fsSL "https://nodejs.org/dist/$ver/node-$ver-$PLAT-$ARCH.tar.gz" -o "$tmp/node.tar.gz" \
90
+ || fail "Could not download Node.js $ver." "check the internet connection, then paste the line again"
91
+ rm -rf "$PMOS_HOME/node" && mkdir -p "$PMOS_HOME/node"
92
+ tar -xzf "$tmp/node.tar.gz" -C "$PMOS_HOME/node" --strip-components=1 --no-same-owner
93
+ rm -rf "$tmp"
94
+ add_path "$PMOS_HOME/node/bin"
95
+ hash -r
96
+ ok "Node.js $(node -v) installed"
97
+ fi
98
+ # A system Node whose global folder needs sudo: point npm's global folder at one the user owns.
99
+ npm_prefix="$(npm prefix -g 2>/dev/null || true)"
100
+ if [ -n "$npm_prefix" ] && [ ! -w "$npm_prefix/lib" ] && [ ! -w "$npm_prefix" ]; then
101
+ mkdir -p "$PMOS_HOME/npm-global"
102
+ npm config set prefix "$PMOS_HOME/npm-global"
103
+ add_path "$PMOS_HOME/npm-global/bin"
104
+ ok "npm global folder set to $PMOS_HOME/npm-global (no sudo needed)"
105
+ fi
106
+ # npm's global folder is where `pmos` lands: put its bin folder on PATH, wherever npm is set up to keep it.
107
+ npm_prefix="$(npm prefix -g 2>/dev/null || true)"
108
+ [ -n "$npm_prefix" ] && [ -d "$npm_prefix" ] && add_path "$npm_prefix/bin"
109
+
110
+ # ── 2. git ────────────────────────────────────────────────────────────────────────────────────
111
+ say "Checking git"
112
+ if [ "$PLAT" = darwin ]; then
113
+ if xcode-select -p >/dev/null 2>&1 && git --version >/dev/null 2>&1; then
114
+ ok "$(git --version)"
115
+ else
116
+ say "Installing Apple's Command Line Tools (they include git)"
117
+ echo " A window opens. Click Install and wait for it to finish. This step can take a few minutes."
118
+ xcode-select --install >/dev/null 2>&1 || true
119
+ waited=0
120
+ until xcode-select -p >/dev/null 2>&1; do
121
+ sleep 5; waited=$((waited + 5))
122
+ [ "$waited" -ge 3600 ] && fail "The Command Line Tools did not finish installing." "run: xcode-select --install, then paste the line again"
123
+ done
124
+ ok "$(git --version)"
125
+ fi
126
+ else
127
+ if have git; then
128
+ ok "$(git --version)"
129
+ else
130
+ say "Installing git (your password may be asked)"
131
+ if have apt-get; then sudo apt-get update -qq && sudo apt-get install -y -qq git
132
+ elif have dnf; then sudo dnf install -y -q git
133
+ elif have pacman; then sudo pacman -S --noconfirm git
134
+ else fail "Could not install git automatically." "install git with your package manager, then paste the line again"; fi
135
+ ok "$(git --version)"
136
+ fi
137
+ # On Linux the terminal component (node-pty) is built from source: it needs a compiler and Python.
138
+ if ! have make || ! have g++ || ! have python3; then
139
+ say "Installing build tools for the PMOS app (your password may be asked)"
140
+ if have apt-get; then sudo apt-get install -y -qq build-essential python3
141
+ elif have dnf; then sudo dnf install -y -q gcc-c++ make python3
142
+ elif have pacman; then sudo pacman -S --noconfirm base-devel python
143
+ else fail "Could not install build tools automatically." "install a C++ compiler, make and python3, then paste the line again"; fi
144
+ ok "build tools installed"
145
+ fi
146
+ fi
147
+
148
+ # ── 3. Claude Code ───────────────────────────────────────────────────────────────────────────
149
+ say "Checking Claude Code"
150
+ [ -x "$HOME/.local/bin/claude" ] && add_path "$HOME/.local/bin"
151
+ if [ "${PMOS_SKIP_CLAUDE:-}" = 1 ]; then
152
+ ok "skipped (PMOS_SKIP_CLAUDE=1)"
153
+ elif have claude; then
154
+ ok "Claude Code is installed"
155
+ else
156
+ say "Installing Claude Code"
157
+ curl -fsSL https://claude.ai/install.sh | bash \
158
+ || fail "Could not install Claude Code." "run: curl -fsSL https://claude.ai/install.sh | bash, then paste the line again"
159
+ add_path "$HOME/.local/bin"
160
+ hash -r
161
+ have claude && ok "Claude Code installed" || fail "Claude Code installed, but the claude command is not found." "open a new Terminal window and paste the line again"
162
+ fi
163
+
164
+ # ── 4. The PMOS folder and kit ──────────────────────────────────────────────────────────────
165
+ say "Setting up the PMOS folder: $PMOS_DIR"
166
+ mkdir -p "$PMOS_DIR"
167
+ if [ ! -d "$PMOS_DIR/.git" ] && ! git -C "$PMOS_DIR" rev-parse --git-dir >/dev/null 2>&1; then
168
+ git -C "$PMOS_DIR" init -q
169
+ ok "git folder created"
170
+ fi
171
+ if [ -f "$PMOS_DIR/pmos/AGENTS.md" ]; then
172
+ ok "the PMOS kit is already in this folder"
173
+ else
174
+ npx -y -p "$PMOS_PKG" create-pmos "$PMOS_DIR" || fail "Could not add the PMOS kit." "paste the line again; if it fails again, send the text above to your PMOS contact"
175
+ ok "PMOS kit added"
176
+ fi
177
+
178
+ # ── 5. The PMOS app ─────────────────────────────────────────────────────────────────────────
179
+ say "Starting the PMOS app"
180
+ npx -y -p "$PMOS_PKG" create-pmos workspace "$PMOS_DIR" || fail "The PMOS app did not start." "run: pmos doctor, and send what it prints to your PMOS contact"
181
+
182
+ echo
183
+ ok "PMOS is ready. Next time, open Terminal and type: pmos"
184
+ echo " New Terminal windows find node, claude and pmos by themselves."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-pmos",
3
- "version": "0.155.0",
3
+ "version": "0.157.0",
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": {
@@ -10,6 +10,8 @@
10
10
  "bin/",
11
11
  "payload/",
12
12
  "workspace/",
13
+ "install.sh",
14
+ "install.ps1",
13
15
  "README.md",
14
16
  "LICENSE"
15
17
  ],
@@ -1,3 +1,3 @@
1
- kernel-source: pmos@8593af2
1
+ kernel-source: pmos@078081c
2
2
  assembled: 2026-10-12
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-12T04:46:53Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-12T05:27:25Z"}
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-12T04:46:53Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-12T05:27:25Z"}
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-12T04:46:53Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-12T05:27:25Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -329,6 +329,7 @@ async function up(dir, { remote = false, open = true } = {}) {
329
329
  };
330
330
  const child = spawn(process.execPath, [path.join(WS, 'server.js')], {
331
331
  cwd: WS, detached: true, stdio: ['ignore', log, log], env,
332
+ windowsHide: true, // Windows: no console window for the background server
332
333
  });
333
334
  // A server that exits at once (a refused tailnet bind, a port lost to a race) ends the wait below early.
334
335
  let exited = false;
@@ -33,7 +33,9 @@ function binaryArch(file) {
33
33
  console.log(`pmos workspace doctor — node ${process.version} (${process.platform}/${process.arch})`);
34
34
  console.log(` install dir: ${WSDIR}\n`);
35
35
 
36
- const REINSTALL = `cd "${WSDIR}" && rm -rf node_modules && npm install (reinstalls node-pty for this platform)`;
36
+ const REINSTALL = process.platform === 'win32'
37
+ ? 'npx create-pmos@latest workspace --reinstall (reinstalls the app and node-pty for this computer)'
38
+ : `cd "${WSDIR}" && rm -rf node_modules && npm install (reinstalls node-pty for this platform)`;
37
39
 
38
40
  // 1. Node version
39
41
  const major = Number(process.versions.node.split('.')[0]);
@@ -101,7 +103,7 @@ function binaryArch(file) {
101
103
  bad(`PTY spawn failed: ${e.message}`,
102
104
  /posix_spawnp/.test(e.message)
103
105
  ? `on macOS this is node-pty's spawn-helper lacking +x (check 3 restores it — re-run \`npm run doctor\`); otherwise a wrong-arch binary — ${REINSTALL}`
104
- : process.platform === 'win32' ? 'native Windows needs WSL for a real PTY — run the workspace inside WSL'
106
+ : process.platform === 'win32' ? `Windows 10 version 1809 or later is needed for terminals (ConPTY); otherwise ${REINSTALL}`
105
107
  : 'check that /bin/sh exists and is executable');
106
108
  }
107
109
  }
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  type: Guide
3
3
  title: "Start the workspace without a terminal"
4
- timestamp: 2026-10-06
4
+ timestamp: 2026-10-12
5
5
  ---
6
6
 
7
7
  # Start the workspace without a terminal
@@ -47,5 +47,5 @@ Type `pmos launcher off`. It removes the app and the login item. It removes only
47
47
  ## Limits
48
48
 
49
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.
50
+ - Windows has no launcher yet. The workspace itself runs on Windows with no WSL: open PowerShell and type `pmos`. If PMOS is not installed yet, the one-line setup in the create-pmos README installs it.
51
51
  - The login item runs once, at login. If the server stops, open the app to start it again.
@@ -123,12 +123,18 @@ To set up Qwen3-ASR, Parakeet, speech detection and speakers, open the settings
123
123
 
124
124
  The kept audio holds your microphone and the meeting apart until the final pass is done. When **Keep the audio for replay** is off, both are deleted after the final pass.
125
125
 
126
+ When PMOS restarts (for example after you install a new version) while a note waits for its final pass, its summary or Wispr Flow's notes, PMOS starts that work again when you next open PMOS:
127
+
128
+ - A final pass that was queued or running is queued again. When it can no longer run, for example because the audio is gone, the note says why, and your agent session writes the notes from the live transcript.
129
+ - A summary that was started and never written is started again.
130
+ - A Wispr Flow fetch continues on its own schedule. When its 20 minutes have passed, it stops and says so. Press **Fetch Wispr Flow notes now** to try again.
131
+
126
132
  ## Wispr Flow notes
127
133
 
128
134
  When you also record the meeting with Wispr Flow, PMOS fetches Wispr Flow's notes and shows them under **Wispr Flow notes**, below yours.
129
135
 
130
136
  1. Five minutes after **Stop**, PMOS starts an agent session that looks for this meeting in Wispr Flow, by its time and title.
131
- 2. When Wispr Flow has no notes yet, PMOS asks again every two minutes, for up to 20 minutes.
137
+ 2. When Wispr Flow has no notes yet, PMOS asks again every two minutes, for up to 20 minutes. Each try starts a new agent session, and the previous one is closed when it is idle.
132
138
  3. When it finds them, the summary shows in the note, with **Open in Wispr Flow**. Your agent session also writes your notes again with Wispr Flow's notes added, and the Overview says **merged with Wispr Flow's notes**. What both say is written once, what only one of them has is added, and where they differ the transcript decides. An item that only Wispr Flow has shows **Said (Wispr Flow):** and no meeting time. This runs once for a note. When Wispr Flow's notes were found before your agent session wrote the notes, they are merged at once.
133
139
  4. When the fetch failed or Wispr Flow had no notes, the **Wispr Flow notes** section says so and shows **Fetch Wispr Flow notes now**. Press it: your agent session looks again at once, and every two minutes for up to 20 minutes from the press. It works when the automatic fetch is switched off.
134
140
 
@@ -16,7 +16,7 @@
16
16
  * A file started by the desktop has no shell profile, so the PATH of the terminal that ran `on` is written into the
17
17
  * script; that is how `claude`, `git` and `tmux` resolve. It never starts remote (tailnet) mode and writes no token.
18
18
  * `off` deletes a file only when it carries MARK, so a file of the same name that the PM wrote is left alone.
19
- * Native Windows is refused: the workspace needs WSL there for a real PTY (doctor.js says the same).
19
+ * Windows is refused: no Start-menu or login item is made there yet. Start the workspace with `pmos`.
20
20
  *
21
21
  * plan() is pure: every input is injected, so each platform's files are assertable from any machine.
22
22
  */
@@ -81,7 +81,7 @@ function desktop(entries) {
81
81
  * `login: false` leaves the login item out.
82
82
  */
83
83
  function plan({ platform, home, env = {}, node, bin, repo, login = true }) {
84
- if (platform === 'win32') return { refused: 'native Windows needs WSL for a real PTY. Run `pmos launcher on` inside WSL.' };
84
+ if (platform === 'win32') return { refused: 'no launcher is made on Windows yet. Start the workspace with `pmos` in PowerShell.' };
85
85
  if (platform !== 'darwin' && platform !== 'linux') return { refused: `no launcher is made for ${platform}. Start the workspace with \`pmos\`.` };
86
86
  const log = path.join(stateRoot.defaultRoot({ env, platform, home }), 'launcher.log');
87
87
  const body = script({ node, bin, repo, pathEnv: env.PATH || '/usr/bin:/bin', log });
@@ -549,15 +549,13 @@ function askWispr(ctx, id) {
549
549
  const h = notes.wisprHandoff(dir, { id, until: stopped });
550
550
  if (!h.ok) return;
551
551
  const tries = (w0.tries || 0) + 1;
552
- let session = w0.session || null;
553
- const again = 'Try again: the Wispr Flow notes may be ready now. Read ' + h.handoff + ' again and write ' + h.out + ' as before.';
554
- const sent = session && typeof ctx.sendToSession === 'function' && ctx.sendToSession(session, again + '\r');
555
- if (!sent) {
556
- const ws = cur.note.ws ? ctx.getWs(cur.note.ws) : null;
557
- const s = ctx.spawnSession({ name: 'notes: Wispr Flow', agent: 'claude', ws: ws && ws.id === cur.note.ws ? ws.id : undefined, origin: 'notes-wispr:' + id, initialInput: h.prompt + '\r' });
558
- if (!s || s.state === 'exited') { notes.setWispr(dir, { id, patch: { state: 'failed', reason: (s && s.note) || 'could not start a claude session', tries } }); return; }
559
- session = s.id;
560
- }
552
+ // every try is a fresh session: a try typed into the last one went nowhere when that session sat on a prompt. The last
553
+ // one is ended first when it is idle (server.js endSupersededRuns; a session still working or waiting on the PM stays).
554
+ if (typeof ctx.endSupersededRuns === 'function') { try { ctx.endSupersededRuns('notes-wispr:' + id); } catch { /* best effort */ } }
555
+ const ws = cur.note.ws ? ctx.getWs(cur.note.ws) : null;
556
+ const s = ctx.spawnSession({ name: 'notes: Wispr Flow', agent: 'claude', ws: ws && ws.id === cur.note.ws ? ws.id : undefined, origin: 'notes-wispr:' + id, initialInput: h.prompt + '\r' });
557
+ if (!s || s.state === 'exited') { notes.setWispr(dir, { id, patch: { state: 'failed', reason: (s && s.note) || 'could not start a claude session', tries } }); return; }
558
+ const session = s.id;
561
559
  notes.setWispr(dir, { id, patch: { state: 'asking', tries, session, asked_at: new Date().toISOString() } });
562
560
  wisprLater(ctx, id, wisprRetry(ctx), () => checkWispr(ctx, id));
563
561
  }
@@ -586,7 +584,8 @@ function fetchWisprNow(ctx, id) {
586
584
  if (listening(cur.note)) return { ok: false, status: 409, error: 'stop listening first' };
587
585
  if (typeof ctx.spawnSession !== 'function') return { ok: false, status: 409, error: 'this workspace cannot start sessions' };
588
586
  const w = cur.note.wispr;
589
- if (w && (w.state === 'waiting' || w.state === 'asking')) return { ok: true, wispr: w };
587
+ // a check with its timer in this process is under way; one without (PMOS restarted) is stuck, so it starts again
588
+ if (w && (w.state === 'waiting' || w.state === 'asking') && wisprTimers.has(id)) return { ok: true, wispr: w };
590
589
  const now = new Date().toISOString();
591
590
  notes.setWispr(dir, { id, patch: { state: 'waiting', tries: 0, reason: '', manual_at: now, stopped_at: (w && w.stopped_at) || now, due_at: now } });
592
591
  askWispr(ctx, id);
@@ -607,12 +606,71 @@ function mergeWispr(ctx, id) {
607
606
  return !!(r && r.ok && r.started);
608
607
  }
609
608
 
609
+ /* AFTER A RESTART. The final pass queue, the Wispr Flow timers and a headless summary run live in this process. A restart
610
+ * (an update installs a new version and starts PMOS again) dropped them, and their notes stayed "queued", "waiting" or
611
+ * "writing" for good. The first notes request after a start picks them up (resumeStuck), once per runtime directory:
612
+ * - a final pass that was queued or running is queued again; when it can no longer run, it is marked failed with the
613
+ * reason, and the agent writes the notes from the live transcript, as Stop does without an engine;
614
+ * - a summary run that was started, wrote nothing and is no longer a session is started again;
615
+ * - a Wispr Flow check that was waiting or asking carries on from its own clock, or ends as failed once its deadline
616
+ * after Stop is past (the PM can press Fetch to try again). */
617
+ let resumedFor = null;
618
+ function resumeStuck(ctx) {
619
+ const dir = ctx.runtimeDir;
620
+ if (resumedFor === dir) return [];
621
+ resumedFor = dir;
622
+ const done = [];
623
+ const l = notes.list(dir);
624
+ if (!l.ok) return done;
625
+ let live = null;
626
+ const liveIds = () => {
627
+ if (live) return live;
628
+ try { live = new Set((typeof ctx.sessionList === 'function' ? ctx.sessionList() : []).filter((x) => x.state !== 'exited').map((x) => x.id)); } catch { live = new Set(); }
629
+ return live;
630
+ };
631
+ const wsFor = (n) => { const w = n.ws ? ctx.getWs(n.ws) : null; return w && w.id === n.ws ? w : null; };
632
+ for (const row of l.notes) {
633
+ const cur = notes.read(dir, row.id);
634
+ if (!cur.ok || cur.note.private || listening(cur.note)) continue;
635
+ const n = cur.note;
636
+ const fst = n.final_state && n.final_state.state;
637
+ if ((fst === 'queued' || fst === 'running') && !(finalRunner && finalRunner.state(n.id))) {
638
+ const q = queueFinal(ctx, n.id);
639
+ if (q.queued) done.push({ id: n.id, final: 'queued' });
640
+ else {
641
+ notes.setFinalState(dir, { id: n.id, state: { state: 'failed', reason: 'PMOS restarted before the final pass ended, and it cannot run again: ' + q.reason } });
642
+ const a = n.agent;
643
+ if (!a || (!a.imported_at && !a.started)) { startAgent(ctx, dir, n.id, wsFor(n)); done.push({ id: n.id, final: 'failed', agent: 'started' }); }
644
+ else done.push({ id: n.id, final: 'failed' });
645
+ }
646
+ } else if (n.agent && n.agent.started && !n.agent.imported_at && !(n.agent.out && fs.existsSync(n.agent.out)) && n.agent.session && !liveIds().has(n.agent.session)) {
647
+ startAgent(ctx, dir, n.id, wsFor(n));
648
+ done.push({ id: n.id, agent: 'started' });
649
+ }
650
+ const w = n.wispr;
651
+ if (w && (w.state === 'waiting' || w.state === 'asking') && !wisprTimers.has(n.id)) {
652
+ const stopped = Math.max(Date.parse(w.stopped_at) || 0, Date.parse(w.manual_at) || 0);
653
+ if (Date.now() - stopped >= wisprDeadline(ctx)) {
654
+ notes.setWispr(dir, { id: n.id, patch: { state: 'failed', reason: 'PMOS restarted while it waited for Wispr Flow, and the ' + Math.round(wisprDeadline(ctx) / 60000) + ' minutes after Stop are past. Press Fetch to try again.' } });
655
+ done.push({ id: n.id, wispr: 'failed' });
656
+ } else {
657
+ const id = n.id;
658
+ if (w.state === 'waiting') wisprLater(ctx, id, Math.max(0, (Date.parse(w.due_at) || 0) - Date.now()), () => askWispr(ctx, id));
659
+ else wisprLater(ctx, id, 0, () => checkWispr(ctx, id));
660
+ done.push({ id, wispr: w.state });
661
+ }
662
+ }
663
+ }
664
+ return done;
665
+ }
666
+
610
667
  const send = (ctx, res, r) => ctx.json(res, r, r.ok ? 200 : (r.status || 400));
611
668
 
612
669
  async function handle(req, res, url, ctx) {
613
670
  const p = url.pathname;
614
671
  const dir = ctx.runtimeDir;
615
672
  if (!dir) { ctx.json(res, { ok: false, error: 'the notes store is not available' }, 500); return true; }
673
+ if (resumedFor !== dir) { try { resumeStuck(ctx); } catch { /* a note that cannot be picked up stays as it was */ } }
616
674
  if (p === '/api/notes' || p === '/api/notes/one') {
617
675
  if (req.method !== 'GET') { ctx.json(res, { ok: false, error: 'GET only' }, 405); return true; }
618
676
  if (p === '/api/notes') { const l = notes.list(dir, { resumeMs: resumeMsOf(ctx) }); if (l.ok) sweepExpired(ctx, l.notes); send(ctx, res, l); return true; }
@@ -991,5 +1049,6 @@ function startAgent(ctx, dir, id, w) {
991
1049
  return { ok: true, started, session: s ? s.id : null, reason, handoff: h.handoff, out: h.out, withheld: h.withheld };
992
1050
  }
993
1051
 
994
- module.exports = { prefixes, handle, enrich, runMinute, bulletsState, queueFinal, askWispr, checkWispr, fetchWisprNow, mergeWispr,
1052
+ module.exports = { prefixes, handle, enrich, runMinute, bulletsState, queueFinal, askWispr, checkWispr, fetchWisprNow, mergeWispr, resumeStuck,
1053
+ __resetResume: () => { resumedFor = null; },
995
1054
  __idle: () => Promise.all([transcriber ? transcriber.idle() : null, finalRunner ? finalRunner.idle() : null, ...chains.values()]) };
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.155.0",
3
+ "version": "0.157.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pmos-workspace",
9
- "version": "0.155.0",
9
+ "version": "0.157.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@xterm/addon-fit": "^0.11.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.155.0",
3
+ "version": "0.157.0",
4
4
  "private": true,
5
5
  "description": "PMOS Terminal Workspace — local companion hosting real PTYs beside the library and gates (D62)",
6
6
  "license": "MIT",
@@ -546,7 +546,7 @@ export function mountSettings(host, ctx = {}) {
546
546
  const values = () => Object.fromEntries(F.map(([k]) => [k, inputs[k].value.trim()]));
547
547
  const running = o.on && o.running && o.running.running;
548
548
  return [group('Online', [extRow('online.on', swBtn(o.on, 'Online', () => post({ ...values(), on: !o.on }, o.on ? 'Online is off' : 'Online is on'), { disabled: !o.canChange }), { scope: 'computer',
549
- note: !o.canChange ? 'Only the computer that runs PMOS can change this.' : !o.cloudflared ? 'cloudflared is not installed. Install it with brew install cloudflared, then switch Online on.' : null })], 'phone'),
549
+ note: !o.canChange ? 'Only the computer that runs PMOS can change this.' : !o.cloudflared ? 'cloudflared is not installed. Install it (Mac: brew install cloudflared; Windows: winget install Cloudflare.cloudflared), then switch Online on.' : null })], 'phone'),
550
550
  running ? fold('check', 'Tunnel running · cloudflared found' + (o.lastRefusal ? '' : ' · no refused sign-in'), 'a') : null,
551
551
  sect('Tunnel and sign-in', [list(fieldRows), o.canChange ? el('div', { class: 'iacts' }, btn('Save', { variant: 'primary', onclick: () => post(values()) })) : null,
552
552
  o.lastRefusal ? help('Last refused sign-in: ' + o.lastRefusal.reason) : null], { ic: 'phone' }),
@@ -231,7 +231,7 @@ export const COPY = {
231
231
  onlineKeysWait: 'Cloudflare\'s sign-in keys are not loaded yet. Sign-ins are refused until they are.',
232
232
  onlineKeysError: (e) => `Cloudflare\'s sign-in keys did not load (${e}). Check the team domain. Sign-ins are refused until they load.`,
233
233
  onlineNeedFields: 'Fill in all five fields to turn Online on.',
234
- onlineNoCloudflared: 'cloudflared is not installed. Install it (brew install cloudflared), then turn Online on.',
234
+ onlineNoCloudflared: 'cloudflared is not installed. Install it (Mac: brew install cloudflared; Windows: winget install Cloudflare.cloudflared), then turn Online on.',
235
235
  onlineEnv: (host) => `Online at https://${host}/, set by PMOS_PUBLIC_HOST when the workspace started. It signs in with the token. Restart the workspace without PMOS_PUBLIC_HOST to use this switch.`,
236
236
  onlineRemote: 'Only the computer that runs the workspace can change this.',
237
237
  onlineUnread: 'The Online settings did not load.',
@@ -3023,8 +3023,8 @@ function spawnFailureAdvice(err, cmd, isShell) {
3023
3023
  `in the workspace install dir.`;
3024
3024
  }
3025
3025
  if (process.platform === 'win32') {
3026
- return `could not start '${cmd}': ${msg}. Native Windows needs WSL for a real PTY shell — ` +
3027
- `run the workspace inside WSL.`;
3026
+ return `could not start '${cmd}': ${msg}. Run \`pmos doctor\`: it checks the terminal component and names the fix. ` +
3027
+ `If this is an agent, check that it is installed and that a new PowerShell window finds it.`;
3028
3028
  }
3029
3029
  return isShell
3030
3030
  ? `could not start a shell ('${cmd}'): ${msg}. Set SHELL to a valid shell (e.g. /bin/zsh) and restart.`
@@ -7925,7 +7925,7 @@ async function handleRequest(req, res) {
7925
7925
  const press = pmGuard.requirePagePress(req, body);
7926
7926
  if (!press.ok) return json(res, { ok: false, error: press.error }, press.status || 403);
7927
7927
  if (BIND.publicHost && body.on === true) return json(res, { ok: false, error: `PMOS_PUBLIC_HOST=${BIND.publicHost} was set when the workspace started, so it is already online that way. Restart it without PMOS_PUBLIC_HOST to use the switch.` }, 409);
7928
- if (body.on === true && !ONLINE.find()) return json(res, { ok: false, field: 'on', error: 'cloudflared is not installed. Install it (brew install cloudflared), then switch Online on again.' }, 409);
7928
+ if (body.on === true && !ONLINE.find()) return json(res, { ok: false, field: 'on', error: 'cloudflared is not installed. Install it (Mac: brew install cloudflared; Windows: winget install Cloudflare.cloudflared), then switch Online on again.' }, 409);
7929
7929
  const out = onlineMode.save(STATE, body);
7930
7930
  if (!out.ok) return json(res, { ok: false, field: out.field || null, error: out.error }, out.field ? 400 : 500);
7931
7931
  ONLINE.settings = out.settings;
@@ -8800,7 +8800,7 @@ async function handleRequest(req, res) {
8800
8800
  // pm-os-absorb Runs 2–5 — the planning layer's routes, one file per run (lib/route-modules.js). Last, so none shadows a route above.
8801
8801
  // meeting-notes Run 1: the Notes routes keep notes in the per-user runtime directory and start the agent-summary session.
8802
8802
  if (await routeModules.dispatch(req, res, url, { json, readBody, getWs, invalidateNeedsAgg, boardCache, consoleState, stateDirFor, sessionList, initiativeIndex,
8803
- runtimeDir: STATE, spawnSession, jevModelOpts, sendToSession,
8803
+ runtimeDir: STATE, spawnSession, jevModelOpts, sendToSession, endSupersededRuns,
8804
8804
  // workspace-headless-jobs R6: is this session a headless run that is still working?
8805
8805
  headlessBusy: (id) => { const x = sessions.get(String(id || '')); return !!(x && x.headless && x.child); },
8806
8806
  // initiative my-work: every registered repository, the day runs, and the two presses that start sessions