create-pmos 0.232.0 → 0.233.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/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/bin/pmos.js +46 -6
- package/workspace/doctor.js +5 -1
- package/workspace/lib/server-discovery.js +16 -1
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/test-lists/one-line-setup.json +3 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.233.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": {
|
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-14T09:17:36Z"}
|
|
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-14T09:17:36Z"}
|
|
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-14T09:17:36Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
package/workspace/bin/pmos.js
CHANGED
|
@@ -57,9 +57,9 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
57
57
|
// old 900ms was tight enough that a server under load read as "not running" — the launcher would
|
|
58
58
|
// then start a second one and collide with the first.
|
|
59
59
|
const PROBE_MS = 2500;
|
|
60
|
-
function probe(port) {
|
|
60
|
+
function probe(port, ms = PROBE_MS) {
|
|
61
61
|
return new Promise((res) => {
|
|
62
|
-
const req = http.get(`${urlOf(port)}/api/sessions`, { timeout:
|
|
62
|
+
const req = http.get(`${urlOf(port)}/api/sessions`, { timeout: ms }, (r) => {
|
|
63
63
|
let b = ''; r.on('data', (c) => (b += c));
|
|
64
64
|
r.on('end', () => {
|
|
65
65
|
let j = null; try { j = JSON.parse(b); } catch {}
|
|
@@ -94,6 +94,31 @@ async function discover() {
|
|
|
94
94
|
return hits.filter(Boolean);
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Our own servers that are running but did not answer the probe (lib/server-discovery.js busyServers).
|
|
99
|
+
* Pinned launches only look at the pin, the same as discover().
|
|
100
|
+
*/
|
|
101
|
+
async function busy(livePorts = []) {
|
|
102
|
+
const held = [];
|
|
103
|
+
for (const p of D.candidatePorts({ stateDir: STATE, pinned: PINNED })) if (!livePorts.includes(p) && !(await portFree(p))) held.push(p);
|
|
104
|
+
return D.busyServers(STATE, { livePorts, heldPorts: held });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// How long to wait for a busy server before giving up on it. A server that is only slow answers well
|
|
108
|
+
// inside this; one that is stuck does not, and starting a second server beside it does not help.
|
|
109
|
+
const BUSY_WAIT_MS = 60000;
|
|
110
|
+
async function waitForBusy(servers) {
|
|
111
|
+
process.stdout.write(`the workspace on port ${servers[0].port} is busy; waiting for it to answer `);
|
|
112
|
+
const until = Date.now() + BUSY_WAIT_MS;
|
|
113
|
+
while (Date.now() < until) {
|
|
114
|
+
for (const b of servers) if (await probe(b.port, 10000)) { say(''); return b.port; }
|
|
115
|
+
process.stdout.write('.');
|
|
116
|
+
await sleep(1000);
|
|
117
|
+
}
|
|
118
|
+
say('');
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
97
122
|
/** Can we bind it ourselves? The only honest test of "free" — lsof answers a different question. */
|
|
98
123
|
function portFree(port) {
|
|
99
124
|
return new Promise((res) => {
|
|
@@ -257,7 +282,7 @@ async function servesTailnet(port) {
|
|
|
257
282
|
|
|
258
283
|
// `remote` starts a NEW server in tailnet mode (pmos phone); a server that is already running is reused as it is.
|
|
259
284
|
// `open: false` registers the repo without opening the desktop browser.
|
|
260
|
-
async function up(dir, { remote = false, open = true } = {}) {
|
|
285
|
+
async function up(dir, { remote = false, open = true, waited = false } = {}) {
|
|
261
286
|
// 1. Is a workspace already running — on ANY port it recorded, not just the one this shell guesses?
|
|
262
287
|
let live = await discover();
|
|
263
288
|
// A server that predates this install is running OLD code under NEW files (or the reverse). Reusing
|
|
@@ -289,6 +314,15 @@ async function up(dir, { remote = false, open = true } = {}) {
|
|
|
289
314
|
}
|
|
290
315
|
return openWorkspace(dir, port, { open });
|
|
291
316
|
}
|
|
317
|
+
// 2. Is one of OUR servers running but too busy to answer? Then wait for it. Starting a second
|
|
318
|
+
// server on the next port is what left a Windows PM with two servers and neither loading.
|
|
319
|
+
const slow = waited ? [] : await busy();
|
|
320
|
+
if (slow.length) {
|
|
321
|
+
if (await waitForBusy(slow)) return up(dir, { remote, open, waited: true }); // re-run: it may be an older version
|
|
322
|
+
say(`✗ the workspace server on port ${slow[0].port} (pid ${slow[0].pid}) is running but has not answered for a minute.`);
|
|
323
|
+
say(' Run `pmos stop`, then `pmos` to restart it. If it happens again, run `pmos doctor` and send its output.');
|
|
324
|
+
return 1;
|
|
325
|
+
}
|
|
292
326
|
if (!ensureInstalled()) return 1;
|
|
293
327
|
fs.mkdirSync(STATE, { recursive: true });
|
|
294
328
|
cleanStalePids([]);
|
|
@@ -337,7 +371,10 @@ async function up(dir, { remote = false, open = true } = {}) {
|
|
|
337
371
|
child.unref();
|
|
338
372
|
try { fs.writeFileSync(pidFile(port), String(child.pid)); } catch {}
|
|
339
373
|
process.stdout.write('starting the workspace ');
|
|
340
|
-
|
|
374
|
+
// Up to ~60s while the server process is alive. A first start on Windows (antivirus scanning every
|
|
375
|
+
// new file) can take longer than 10s, and giving up early left a server starting with nobody waiting.
|
|
376
|
+
for (let i = 0; i < 240 && !exited; i++) {
|
|
377
|
+
if (i === 40) process.stdout.write('\n still starting — a first start can take up to a minute ');
|
|
341
378
|
if (await probe(port)) {
|
|
342
379
|
say('');
|
|
343
380
|
say(' sessions keep running after you close this terminal · `pmos stop` shuts it down');
|
|
@@ -404,7 +441,8 @@ async function stopOne(port) {
|
|
|
404
441
|
if (!pid) { say(`✗ could not locate the server on port ${port} (no pid file, and nothing I can see owns it). Stop it manually.`); return 1; }
|
|
405
442
|
try { process.kill(pid); } catch (e) { say(`✗ could not signal pid ${pid}: ${e.message}`); return 1; }
|
|
406
443
|
for (let i = 0; i < 20; i++) {
|
|
407
|
-
|
|
444
|
+
// Gone means the process ended or the port is free. "Not answering" is not enough: a busy server does not answer either.
|
|
445
|
+
if (!D.pidAlive(pid) || await portFree(port)) { say(`✓ stopped — ${urlOf(port)}`); try { fs.unlinkSync(pidFile(port)); } catch {} return 0; }
|
|
408
446
|
await sleep(250);
|
|
409
447
|
}
|
|
410
448
|
say(`✗ port ${port} still responding after 5s — kill it manually (pid ${pid}).`);
|
|
@@ -412,7 +450,9 @@ async function stopOne(port) {
|
|
|
412
450
|
}
|
|
413
451
|
|
|
414
452
|
async function stop() {
|
|
415
|
-
|
|
453
|
+
// A busy server does not answer the probe but is still ours, and still holds its port. Stop it too.
|
|
454
|
+
const answering = await discover();
|
|
455
|
+
const live = [...answering, ...await busy(answering.map((l) => l.port))];
|
|
416
456
|
if (!live.length) { say('○ not running.'); cleanStalePids([]); return 0; }
|
|
417
457
|
// sessions are tmux-backed: stopping the server leaves them running; the next `pmos` re-attaches.
|
|
418
458
|
say('stopping the workspace — sessions keep running in tmux and re-attach on the next launch…');
|
package/workspace/doctor.js
CHANGED
|
@@ -167,7 +167,11 @@ function binaryArch(file) {
|
|
|
167
167
|
} else {
|
|
168
168
|
const want = pinned || D.DEFAULT_PORT;
|
|
169
169
|
if (await free(want)) ok(`no server running; port ${want} is free (start it with: pmos)`);
|
|
170
|
-
else {
|
|
170
|
+
else if (D.busyServers(STATE, { heldPorts: [want] }).length) {
|
|
171
|
+
const b = D.busyServers(STATE, { heldPorts: [want] })[0];
|
|
172
|
+
bad(`the workspace server on port ${want} (pid ${b.pid}) is running but did not answer within 2s — it is busy`,
|
|
173
|
+
'wait a minute and run `pmos` again; if it still does not answer, run `pmos stop`, then `pmos`');
|
|
174
|
+
} else {
|
|
171
175
|
const who = owner(want) || 'another program';
|
|
172
176
|
bad(`no workspace server is answering, but port ${want} is held by ${who}`,
|
|
173
177
|
pinned ? `stop that process, or pin a free port: PMOS_WS_PORT=7500 pmos`
|
|
@@ -102,6 +102,21 @@ function stalePidFiles(stateDir, { livePorts = [], alive = pidAlive } = {}) {
|
|
|
102
102
|
.map((r) => r.file);
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
/**
|
|
106
|
+
* The workspace servers that are running but did not answer: a record whose port is not among the
|
|
107
|
+
* live ones, whose port is still held, and whose pid is alive. A busy server (a long synchronous job,
|
|
108
|
+
* or a slow first start on Windows) looks exactly like this. Reading it as "something else holds the
|
|
109
|
+
* port" is how `pmos` started a second server beside a first one that was only slow. `heldPorts` is
|
|
110
|
+
* injected (the caller owns the socket), so this stays pure.
|
|
111
|
+
*/
|
|
112
|
+
function busyServers(stateDir, { livePorts = [], heldPorts = [], alive = pidAlive } = {}) {
|
|
113
|
+
const live = new Set(livePorts.map(Number));
|
|
114
|
+
const held = new Set(heldPorts.map(Number));
|
|
115
|
+
return pidRecords(stateDir)
|
|
116
|
+
.filter((r) => !live.has(r.port) && held.has(r.port) && alive(r.pid))
|
|
117
|
+
.map((r) => ({ port: r.port, pid: r.pid }));
|
|
118
|
+
}
|
|
119
|
+
|
|
105
120
|
/**
|
|
106
121
|
* The first port at or after `start` that `isFree` accepts, within the bounded span. `isFree` is
|
|
107
122
|
* injected (the caller owns the socket), so this stays pure and the span is assertable.
|
|
@@ -123,5 +138,5 @@ function looksLikeWorkspace(body) {
|
|
|
123
138
|
|
|
124
139
|
module.exports = {
|
|
125
140
|
DEFAULT_PORT, FALLBACK_SPAN, PID_RE,
|
|
126
|
-
pidFileName, pidRecords, candidatePorts, pidAlive, stalePidFiles, nextFreePort, looksLikeWorkspace,
|
|
141
|
+
pidFileName, pidRecords, candidatePorts, pidAlive, stalePidFiles, busyServers, nextFreePort, looksLikeWorkspace,
|
|
127
142
|
};
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.233.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "pmos-workspace",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.233.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/workspace/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_comment": "one-line-setup: on Windows the background server hides the console window of every command it runs (lib/win-hide.js).",
|
|
3
|
-
"modules": ["win-hide.test.js"],
|
|
4
|
-
"seconds": { "win-hide.test.js": 1 }
|
|
2
|
+
"_comment": "one-line-setup: on Windows the background server hides the console window of every command it runs (lib/win-hide.js); `pmos` waits for its own busy server instead of starting a second one (busy-server.test.js).",
|
|
3
|
+
"modules": ["win-hide.test.js", "busy-server.test.js"],
|
|
4
|
+
"seconds": { "win-hide.test.js": 1, "busy-server.test.js": 12 }
|
|
5
5
|
}
|