create-pmos 0.59.1 → 0.59.3
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/REMOTE.md +1 -1
- package/workspace/guide/layout.md +1 -1
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +1 -1
- package/workspace/public/pm/dest-settings.js +1 -0
- package/workspace/public/pm/frame-copy.js +12 -0
- package/workspace/server.js +10 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.59.
|
|
3
|
+
"version": "0.59.3",
|
|
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-06T14:43:28Z"}
|
|
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-06T14:43:28Z"}
|
|
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-06T14:43:28Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
package/workspace/REMOTE.md
CHANGED
|
@@ -217,7 +217,7 @@ Run these from a machine that is **not** signed in to Access:
|
|
|
217
217
|
- **`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).
|
|
218
218
|
- **`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.
|
|
219
219
|
- **A local reverse proxy now gets `401`** — it adds a forwarding header, so its requests need the token. This is intended.
|
|
220
|
-
- **`403` "your Cloudflare sign-in could not be verified for this workspace"** (Online switch) — check the AUD tag, the team domain and the email in **Settings › Online**. The email must be the one you sign in with.
|
|
220
|
+
- **`403` "your Cloudflare sign-in could not be verified for this workspace"** (Online switch) — check the AUD tag, the team domain and the email in **Settings › Online**. The email must be the one you sign in with. Settings › Online on the desktop names the check that refused the last sign-in. To see which email Cloudflare signed you in with, open `https://<your address>/cdn-cgi/access/get-identity` and read `email`.
|
|
221
221
|
- **Cloudflare error 1033** — the tunnel is not running. In **Settings › Online**, check the status line, and read `online-tunnel.log` in the workspace state folder. Another `cloudflared` for the same tunnel (for example a service you installed) also serves the hostname.
|
|
222
222
|
|
|
223
223
|
- **"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.
|
|
@@ -31,7 +31,7 @@ Settings holds settings only, in seven sections:
|
|
|
31
31
|
- **Profiles**: the Claude profiles a session can start with. They open in the detail.
|
|
32
32
|
- **New sessions**: the default CLI and the default model for a new session in the dock.
|
|
33
33
|
- **Validate consent**: the consent of the current repository. Turn it on or off here. **Open Validate** opens the Validate page as a tab.
|
|
34
|
-
- **Online**: puts this workspace at your own address, such as `pmos.pm-os.app`, through a Cloudflare tunnel. Enter the public address, the tunnel name, your Cloudflare team domain, the Access audience (AUD) tag and your email, then press **Turn on**. The workspace runs the tunnel and remembers the switch when it restarts. On your phone, open the address and sign in with Cloudflare Access (Google or an email code). The workspace does not ask for its token there. Only the computer that runs the workspace can change this section. A phone sees whether it is on. [REMOTE.md](../REMOTE.md) has the one-time Cloudflare steps.
|
|
34
|
+
- **Online**: puts this workspace at your own address, such as `pmos.pm-os.app`, through a Cloudflare tunnel. Enter the public address, the tunnel name, your Cloudflare team domain, the Access audience (AUD) tag and your email, then press **Turn on**. The workspace runs the tunnel and remembers the switch when it restarts. On your phone, open the address and sign in with Cloudflare Access (Google or an email code). The workspace does not ask for its token there. Only the computer that runs the workspace can change this section. A phone sees whether it is on. When a sign-in at the address is refused, this section names the check that failed, such as the email or the AUD tag. [REMOTE.md](../REMOTE.md) has the one-time Cloudflare steps.
|
|
35
35
|
- **Theme**: Dark or Light.
|
|
36
36
|
- **Updates**: the version of the workspace app and the version of the kit in the current repository. Press **Check for updates** to compare them with the latest published version of PMOS. This is the only time the workspace asks the internet for a version, and it asks only when you press. When a newer version is available, a button shows for each part that is older:
|
|
37
37
|
- **Update the kit** does what `npx create-pmos@latest --update` does in a terminal. It changes only the kit's own files under `pmos/` (skills, scripts, templates and guides). Your records in `state/`, `planning/` and `okf/product/` do not change. The last lines of the output show under the button. The changed files are under `pmos/`, which Sync treats as records, so Sync (or automatic sync) sends them with your other records. To read them first, look at `git diff pmos/` before you sync.
|
package/workspace/package.json
CHANGED
|
@@ -204,6 +204,7 @@ function onlineSection() {
|
|
|
204
204
|
lines.push(el('p', { class: 'fr-set__note', 'data-fr': 'online-tunnel' },
|
|
205
205
|
r.running ? P.onlineRunning : r.lastExit ? P.onlineExited(r.lastExit.code) : P.onlineStarting));
|
|
206
206
|
if (d.keys && !d.keys.keys) lines.push(el('p', { class: 'fr-set__note', 'data-fr': 'online-keys' }, d.keys.lastError ? P.onlineKeysError(d.keys.lastError) : P.onlineKeysWait));
|
|
207
|
+
if (d.lastRefusal) lines.push(el('p', { class: 'fr-set__note', 'data-fr': 'online-refused', role: 'status' }, P.onlineRefused(d.lastRefusal.reason, d.lastRefusal.at)));
|
|
207
208
|
}
|
|
208
209
|
if (!d.canChange) return section('online', P.online, [...lines, el('p', { class: 'fr-set__note' }, P.onlineRemote)]);
|
|
209
210
|
const f = (key, label, placeholder) => field({
|
|
@@ -170,6 +170,18 @@ export const COPY = {
|
|
|
170
170
|
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.`,
|
|
171
171
|
onlineRemote: 'Only the computer that runs the workspace can change this.',
|
|
172
172
|
onlineUnread: 'The Online settings did not load.',
|
|
173
|
+
onlineRefused: (reason, at) => `The last sign-in at the public address was refused${at ? ` (${at.slice(11, 16)} UTC)` : ''}: ${({
|
|
174
|
+
malformed: 'Cloudflare sent no sign-in, or one PMOS cannot read. Check that the Access application covers the public address.',
|
|
175
|
+
algorithm: 'the sign-in was not signed the way Cloudflare Access signs.',
|
|
176
|
+
'unknown key': 'it was signed with a key this team does not publish. Check the team domain.',
|
|
177
|
+
'no keys': 'Cloudflare\'s sign-in keys are not loaded. Check the team domain.',
|
|
178
|
+
signature: 'its signature did not match. Check the team domain.',
|
|
179
|
+
issuer: 'it came from another Cloudflare team. Check the team domain.',
|
|
180
|
+
audience: 'it was for another Access application. Check the audience (AUD) tag: copy it from the application for the public address.',
|
|
181
|
+
expired: 'it had expired, or this computer\'s clock is wrong. Sign in again.',
|
|
182
|
+
'not yet valid': 'it is not valid yet, so this computer\'s clock is probably wrong.',
|
|
183
|
+
email: 'you signed in with another email. The email field must be the address you sign in to Cloudflare with.',
|
|
184
|
+
})[reason] || reason}`,
|
|
173
185
|
updates: 'Updates',
|
|
174
186
|
updatesCheck: 'Check for updates',
|
|
175
187
|
updatesChecking: 'Checking for updates…',
|
package/workspace/server.js
CHANGED
|
@@ -302,7 +302,8 @@ const TOKEN = BIND.token || null; // non-loopback peers must
|
|
|
302
302
|
const onlineMode = require('./lib/online-mode');
|
|
303
303
|
const accessJwt = require('./lib/access-jwt');
|
|
304
304
|
const accessKeys = require('./lib/access-keys');
|
|
305
|
-
const ONLINE = { settings: onlineMode.read(STATE), keys: null, keysTeam: null, fetchKeys: accessKeys.fetchCerts, find: onlineMode.findCloudflared
|
|
305
|
+
const ONLINE = { settings: onlineMode.read(STATE), keys: null, keysTeam: null, fetchKeys: accessKeys.fetchCerts, find: onlineMode.findCloudflared,
|
|
306
|
+
lastRefusal: null }; // { reason, at } of the last refused sign-in: a fixed reason word only, never the assertion or a claim (D08)
|
|
306
307
|
const tunnel = onlineMode.createTunnel({ stateDir: STATE, find: () => ONLINE.find() });
|
|
307
308
|
/** The public hostname the guards admit right now: the environment's, else the Online switch's while it is on. */
|
|
308
309
|
function publicHost() { return BIND.publicHost || (ONLINE.settings.on ? ONLINE.settings.host : null); }
|
|
@@ -331,21 +332,26 @@ function applyOnline() {
|
|
|
331
332
|
/** The D89 decision for one request: { ok: true } or { ok: false, reason }. The assertion is never logged (D08). */
|
|
332
333
|
async function accessVerdict(headers) {
|
|
333
334
|
const s = ONLINE.settings; const store = ONLINE.keys;
|
|
334
|
-
if (!store)
|
|
335
|
+
if (!store) { const v = { ok: false, reason: 'no keys' }; noteVerdict(v); return v; }
|
|
335
336
|
try { await store.ensure(); } catch { /* the store records its own error */ }
|
|
336
337
|
const token = (headers || {})['cf-access-jwt-assertion'];
|
|
337
338
|
const opts = { keyFor: store.keyFor, teamDomain: s.team, audience: s.audience, email: s.email, now: Math.floor(Date.now() / 1000) };
|
|
338
339
|
let v = accessJwt.verify(token, opts);
|
|
339
340
|
if (!v.ok && v.reason === 'unknown key' && v.kid && await store.refetchFor(v.kid)) v = accessJwt.verify(token, opts);
|
|
341
|
+
noteVerdict(v);
|
|
340
342
|
return v;
|
|
341
343
|
}
|
|
344
|
+
/** Keep the reason of the last refusal for Settings › Online, so the PM can see which check failed. A pass clears it. */
|
|
345
|
+
function noteVerdict(v) {
|
|
346
|
+
ONLINE.lastRefusal = v && v.ok ? null : { reason: String((v && v.reason) || 'unknown'), at: new Date().toISOString() };
|
|
347
|
+
}
|
|
342
348
|
/** The Online record and state, as GET /api/online returns it. Nothing secret is in it: the fields are not credentials. */
|
|
343
349
|
function onlineView(req) {
|
|
344
350
|
const s = ONLINE.settings;
|
|
345
351
|
return {
|
|
346
352
|
ok: true, on: s.on, host: s.host, tunnel: s.tunnel, team: s.team, audience: s.audience, email: s.email,
|
|
347
353
|
complete: onlineMode.complete(s), envHost: BIND.publicHost || null, cloudflared: !!ONLINE.find(),
|
|
348
|
-
running: tunnel.status(), keys: ONLINE.keys ? ONLINE.keys.status() : null,
|
|
354
|
+
running: tunnel.status(), keys: ONLINE.keys ? ONLINE.keys.status() : null, lastRefusal: ONLINE.lastRefusal,
|
|
349
355
|
canChange: onlineChangeAllowed(req),
|
|
350
356
|
};
|
|
351
357
|
}
|
|
@@ -8192,7 +8198,7 @@ function httpGuard(req, res) {
|
|
|
8192
8198
|
accessVerdict(req.headers).then((v) => {
|
|
8193
8199
|
if (v.ok) return handleRequest(req, res);
|
|
8194
8200
|
res.writeHead(403, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
|
|
8195
|
-
res.end(JSON.stringify({ ok: false, error: 'your Cloudflare sign-in could not be verified for this workspace' }));
|
|
8201
|
+
res.end(JSON.stringify({ ok: false, error: 'your Cloudflare sign-in could not be verified for this workspace. Settings › Online on the computer that runs it says which check failed' }));
|
|
8196
8202
|
}, () => {
|
|
8197
8203
|
res.writeHead(403, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
|
|
8198
8204
|
res.end(JSON.stringify({ ok: false, error: 'your Cloudflare sign-in could not be verified for this workspace' }));
|