create-pmos 0.521.0 → 0.523.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/bin/create-pmos.js +106 -42
- package/package.json +39 -47
- 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 +2 -1
- package/workspace/board-publish-ui-test.js +1 -1
- package/workspace/changelog.json +38 -0
- package/workspace/guide/home.md +2 -2
- package/workspace/guide/published-board.md +15 -3
- package/workspace/lib/board-pack.js +44 -1
- package/workspace/lib/board-publish-choices.js +22 -6
- package/workspace/lib/board-publish-parts.js +95 -0
- package/workspace/lib/boards-host.js +3 -3
- package/workspace/lib/boards-received.js +105 -0
- package/workspace/lib/connector-read.js +19 -0
- package/workspace/lib/headless.js +2 -2
- package/workspace/lib/kit-update.js +51 -21
- package/workspace/lib/project-onboard.js +7 -16
- package/workspace/lib/refresh-run.js +2 -1
- package/workspace/lib/routes-board-publish.js +42 -12
- package/workspace/lib/routes-onboard.js +17 -0
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/bd-publish.css +3 -0
- package/workspace/public/pm/bd-publish.js +39 -9
- package/workspace/public/pm/bd-shared.css +5 -0
- package/workspace/public/pm/bd-shared.js +72 -10
- package/workspace/public/pm/chat-term.js +22 -0
- package/workspace/public/pm/cozy-board.css +10 -0
- package/workspace/public/pm/cozy-home.css +3 -0
- package/workspace/public/pm/home-chat.js +3 -0
- package/workspace/public/pm/premium-board.js +8 -2
- package/workspace/public/pm/premium-shell.js +2 -1
- package/workspace/public/pm/readonly-boot.js +4 -1
- package/workspace/public/pm/readonly.js +40 -5
- package/workspace/public/pm/term-view.js +39 -8
package/bin/create-pmos.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
const os = require('os');
|
|
22
22
|
const fs = require('fs');
|
|
23
23
|
const path = require('path');
|
|
24
|
-
const
|
|
24
|
+
const cp = require('child_process'); // cp.spawnSync, so a test can stand in for it
|
|
25
25
|
|
|
26
26
|
const PAYLOAD = path.join(__dirname, '..', 'payload'); // the assembled pmos/ kit (file-mode)
|
|
27
27
|
const WORKSPACE = path.join(__dirname, '..', 'workspace'); // the bundled Terminal Workspace app (D62)
|
|
@@ -173,13 +173,18 @@ function update(dirArg, opts) {
|
|
|
173
173
|
// global install can't run (locked-down npm prefix), we DON'T fail — we launch straight from the
|
|
174
174
|
// bundled copy (its own bin/pmos.js installs runtime deps on first run) and print the one manual
|
|
175
175
|
// line to get the bare `pmos` word for next time.
|
|
176
|
+
// No console window on Windows when nobody watches a terminal (batch 27 item 7). The in-app update (Settings > Updates)
|
|
177
|
+
// runs this with its output in a log file; then each child here starts hidden. In a terminal the children share it, as
|
|
178
|
+
// before: windowsHide there would give each one a console of its own, away from the one the person reads.
|
|
179
|
+
const hide = () => ({ windowsHide: !process.stdout.isTTY });
|
|
180
|
+
|
|
176
181
|
function onPath(cmd) {
|
|
177
182
|
// `command -v` (POSIX) and `where` (Windows) are shell BUILTINS — neither has a standalone binary on
|
|
178
183
|
// Linux, so the probe MUST run through a shell or it ENOENTs and always reports "not found" (which,
|
|
179
184
|
// after a successful install, prints a false PATH warning and defeats the idempotent fast path). cmd
|
|
180
185
|
// is always a literal, so the string form is injection-safe.
|
|
181
186
|
const probe = process.platform === 'win32' ? `where ${cmd}` : `command -v ${cmd}`;
|
|
182
|
-
try { return spawnSync(probe, { stdio: 'ignore', shell: true }).status === 0; }
|
|
187
|
+
try { return cp.spawnSync(probe, { stdio: 'ignore', shell: true, ...hide() }).status === 0; }
|
|
183
188
|
catch { return false; }
|
|
184
189
|
}
|
|
185
190
|
|
|
@@ -204,14 +209,41 @@ function launchStatus(r) {
|
|
|
204
209
|
|
|
205
210
|
// --no-open: the in-app update (Settings > Updates) reinstalls through here while the PM's tab waits and reloads itself;
|
|
206
211
|
// the restarted server then opens no second tab (batch 18 item 5). It is pmos's own flag, so the server does not inherit it.
|
|
207
|
-
|
|
208
|
-
|
|
212
|
+
// { capture: true } returns the output in r.stdout instead of printing it.
|
|
213
|
+
function runInstalledPmos(args, { capture = false } = {}) {
|
|
214
|
+
const io = capture ? { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] } : { stdio: 'inherit' };
|
|
215
|
+
if (process.platform !== 'win32') return cp.spawnSync('pmos', args, { ...io, ...hide() });
|
|
209
216
|
// cmd.exe does not preserve spawnSync argument boundaries. Run Node directly so a
|
|
210
217
|
// repository under a Windows user name with spaces still reaches the app intact.
|
|
211
|
-
const root =
|
|
218
|
+
const root = globalRoot();
|
|
212
219
|
if (root.error || root.status !== 0) return root;
|
|
213
|
-
if (!root.
|
|
214
|
-
return spawnSync(process.execPath, [path.join(root.
|
|
220
|
+
if (!root.dir) return { status: 1, error: new Error('npm did not report its global install folder') };
|
|
221
|
+
return cp.spawnSync(process.execPath, [path.join(root.dir, 'pmos-workspace', 'bin', 'pmos.js'), ...args], { ...io, ...hide() });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// `npm root -g`, asked once per run: every Windows launch step needs it, and each ask starts npm again.
|
|
225
|
+
let rootMemo = null;
|
|
226
|
+
function globalRoot() {
|
|
227
|
+
if (rootMemo) return rootMemo;
|
|
228
|
+
const win = process.platform === 'win32';
|
|
229
|
+
const r = cp.spawnSync(win ? 'npm.cmd' : 'npm', ['root', '-g'], { encoding: 'utf8', shell: win, ...hide() });
|
|
230
|
+
const res = { status: r.status, error: r.error, dir: r.status === 0 && r.stdout ? r.stdout.trim() : '' };
|
|
231
|
+
if (res.status === 0 && !res.error) rootMemo = res;
|
|
232
|
+
return res;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Is the launcher already set up for this repository? `pmos launcher status` says so with exit 0 when the app and its
|
|
236
|
+
// node and pmos.js are in place, and names the repository. Then `launcher on` would write the same files again, and on
|
|
237
|
+
// Windows run PowerShell twice, so it is skipped. Any other answer (no launcher, a moved node, another repository, an
|
|
238
|
+
// older pmos without the command) runs `launcher on` as before.
|
|
239
|
+
function launcherCurrent(targetDir) {
|
|
240
|
+
const r = runInstalledPmos(['launcher', 'status'], { capture: true });
|
|
241
|
+
if (r.error || r.status !== 0) return false;
|
|
242
|
+
const m = /^\s*repository: (.+?)\s*$/m.exec(String(r.stdout || ''));
|
|
243
|
+
if (!m || /✗/.test(m[1])) return false;
|
|
244
|
+
const win = process.platform === 'win32';
|
|
245
|
+
const norm = (p) => (win ? path.win32.resolve(p).toLowerCase() : path.resolve(p));
|
|
246
|
+
return norm(m[1]) === norm(targetDir);
|
|
215
247
|
}
|
|
216
248
|
|
|
217
249
|
function launchWorkspace(targetDir, viaGlobal) {
|
|
@@ -223,8 +255,11 @@ function launchWorkspace(targetDir, viaGlobal) {
|
|
|
223
255
|
console.error('create-pmos: launcher setup needs the installed pmos command. Fix the global install and run workspace again.');
|
|
224
256
|
return 1;
|
|
225
257
|
}
|
|
226
|
-
|
|
227
|
-
|
|
258
|
+
if (launcherCurrent(targetDir)) console.log('✓ PMOS app and login item already set up — kept as they are.');
|
|
259
|
+
else {
|
|
260
|
+
const launcher = runInstalledPmos(['launcher', 'on', targetDir]);
|
|
261
|
+
if (launchStatus(launcher) !== 0) return 1;
|
|
262
|
+
}
|
|
228
263
|
}
|
|
229
264
|
|
|
230
265
|
if (viaGlobal && onPath('pmos')) {
|
|
@@ -233,8 +268,8 @@ function launchWorkspace(targetDir, viaGlobal) {
|
|
|
233
268
|
}
|
|
234
269
|
// Fallback: run the bundled launcher directly. Its ensureInstalled() pulls runtime deps on first run.
|
|
235
270
|
console.log('Launching from the bundled copy (the `pmos` command is not on PATH).');
|
|
236
|
-
return launchStatus(spawnSync(process.execPath, [path.join(WORKSPACE, 'bin', 'pmos.js'), targetDir, ...NO_OPEN],
|
|
237
|
-
{ stdio: 'inherit', env: process.env }));
|
|
271
|
+
return launchStatus(cp.spawnSync(process.execPath, [path.join(WORKSPACE, 'bin', 'pmos.js'), targetDir, ...NO_OPEN],
|
|
272
|
+
{ stdio: 'inherit', env: process.env, ...hide() }));
|
|
238
273
|
}
|
|
239
274
|
|
|
240
275
|
// Compare two dotted numeric versions. Returns -1 if a<b, 0 if equal, 1 if a>b. Missing/non-numeric
|
|
@@ -257,10 +292,9 @@ function bundledWorkspaceVersion() {
|
|
|
257
292
|
// Version of the globally-installed `pmos-workspace`, or null if absent/unreadable. Read from the
|
|
258
293
|
// npm global root rather than the app's CLI — no dependency on the old binary supporting --version.
|
|
259
294
|
function installedWorkspaceVersion() {
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
const dir = path.join(r.stdout.trim(), 'pmos-workspace');
|
|
295
|
+
const r = globalRoot();
|
|
296
|
+
if (r.status !== 0 || !r.dir) return null;
|
|
297
|
+
const dir = path.join(r.dir, 'pmos-workspace');
|
|
264
298
|
try {
|
|
265
299
|
// A SYMLINK is not an install. `npm install -g <folder>` links to the folder and installs no
|
|
266
300
|
// dependencies, so the link reads as the bundled version (it IS the bundled folder, here the npx
|
|
@@ -278,20 +312,45 @@ function installWorkspaceGlobal() {
|
|
|
278
312
|
const shell = process.platform === 'win32';
|
|
279
313
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'pmos-ws-'));
|
|
280
314
|
try {
|
|
281
|
-
const pack = spawnSync(npmCmd, ['pack', WORKSPACE, '--pack-destination', tmp, '--silent'],
|
|
282
|
-
{ encoding: 'utf8', shell });
|
|
315
|
+
const pack = cp.spawnSync(npmCmd, ['pack', WORKSPACE, '--pack-destination', tmp, '--silent'],
|
|
316
|
+
{ encoding: 'utf8', shell, ...hide() });
|
|
283
317
|
const tgz = pack.status === 0 ? (pack.stdout || '').trim().split('\n').pop() : '';
|
|
284
318
|
if (!tgz) { console.error(`create-pmos: could not pack the workspace app${pack.stderr ? ' — ' + pack.stderr.trim() : ''}`); return 1; }
|
|
285
|
-
|
|
286
|
-
{ stdio: 'inherit', shell }).status;
|
|
319
|
+
const status = cp.spawnSync(npmCmd, ['install', '-g', path.join(tmp, tgz), '--no-audit', '--no-fund'],
|
|
320
|
+
{ stdio: 'inherit', shell, ...hide() }).status;
|
|
321
|
+
if (status === 0) shipKitWithApp(npmCmd, shell);
|
|
322
|
+
return status;
|
|
287
323
|
} finally {
|
|
288
324
|
try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {}
|
|
289
325
|
}
|
|
290
326
|
}
|
|
327
|
+
// The installed app sets up new projects ("Set up PMOS in <folder>"), so it needs the kit. Only this temporary npx bundle has
|
|
328
|
+
// payload/ beside workspace/; the global copy got workspace/ alone and every setup failed "This PMOS install has no kit to add"
|
|
329
|
+
// (onboarding grill, 2026-11-04). Copy the kit into the installed app as <app>/payload, where lib/project-onboard.js looks first.
|
|
330
|
+
// It is copied after `npm install -g` and not packed into the tarball: the workspace's .gitignore patterns (state/acceptances.jsonl)
|
|
331
|
+
// would drop kit files from an `npm pack`. Best-effort: without it setup falls back to `npx create-pmos`.
|
|
332
|
+
function shipKitWithApp(npmCmd, shell) {
|
|
333
|
+
if (!fs.existsSync(path.join(PAYLOAD, 'AGENTS.md'))) return false;
|
|
334
|
+
const root = cp.spawnSync(npmCmd, ['root', '-g'], { encoding: 'utf8', shell, ...hide() });
|
|
335
|
+
const app = path.join(String(root.stdout || '').trim(), 'pmos-workspace');
|
|
336
|
+
if (root.status !== 0 || !fs.existsSync(path.join(app, 'package.json'))) {
|
|
337
|
+
console.log(' (the kit was not copied into the installed app; project setup will download it when needed)');
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
try {
|
|
341
|
+
const dest = path.join(app, 'payload');
|
|
342
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
343
|
+
fs.cpSync(PAYLOAD, dest, { recursive: true });
|
|
344
|
+
return true;
|
|
345
|
+
} catch (e) {
|
|
346
|
+
console.log(` (the kit was not copied into the installed app: ${e.message}; project setup will download it when needed)`);
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
291
350
|
// A persistent server started by the OLD code keeps serving OLD assets after an upgrade — stop it so
|
|
292
351
|
// the next launch brings the server up on the freshly installed code. Best-effort; ignore failures.
|
|
293
352
|
function stopWorkspaceServer() {
|
|
294
|
-
try { spawnSync('pmos', ['stop'], { stdio: 'ignore', shell: process.platform === 'win32' }); }
|
|
353
|
+
try { cp.spawnSync('pmos', ['stop'], { stdio: 'ignore', shell: process.platform === 'win32', ...hide() }); }
|
|
295
354
|
catch { /* no server running, or pmos not launchable — nothing to stop */ }
|
|
296
355
|
}
|
|
297
356
|
|
|
@@ -347,26 +406,31 @@ function workspace(targetArg, opts) {
|
|
|
347
406
|
process.exit(launchWorkspace(targetDir, false));
|
|
348
407
|
}
|
|
349
408
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
} else if (
|
|
369
|
-
|
|
370
|
-
} else {
|
|
371
|
-
|
|
409
|
+
// Run as the create-pmos command; required (by workspace/kit-update.test.js), it only lends its launch steps.
|
|
410
|
+
function main() {
|
|
411
|
+
const args = process.argv.slice(2);
|
|
412
|
+
const positional = args.filter((a) => !a.startsWith('-'));
|
|
413
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
414
|
+
console.log('usage:');
|
|
415
|
+
console.log(' create-pmos [target-dir] vendor the file-mode kit into a repo (pm-artifacts profile, D68)');
|
|
416
|
+
console.log(' create-pmos [target-dir] --code …as a product-code repository instead (code lanes and ladder)');
|
|
417
|
+
console.log(' create-pmos [target-dir] --no-mod …without offering the pmos-cockpit mod in .claude/settings.json');
|
|
418
|
+
console.log(' create-pmos --update [kit-or-repo] refresh only the kit kernel (add --offer-mod to offer the mod in .claude/settings.json)');
|
|
419
|
+
console.log(' create-pmos workspace [repo-dir] install/upgrade + launch the Terminal Workspace GUI');
|
|
420
|
+
console.log(' create-pmos workspace --reinstall force-reinstall the workspace app (drop a stale global copy)');
|
|
421
|
+
console.log(' create-pmos workspace --no-launcher skip the default Mac/Windows app and login item');
|
|
422
|
+
console.log(' create-pmos --version print version + bundled kernel provenance');
|
|
423
|
+
} else if (args.includes('--version') || args.includes('-v')) {
|
|
424
|
+
console.log(`create-pmos ${PKG.version}`);
|
|
425
|
+
console.log(kitVersion());
|
|
426
|
+
console.log(`workspace bundled: ${fs.existsSync(path.join(WORKSPACE, 'bin', 'pmos.js')) ? 'yes' : 'no'}`);
|
|
427
|
+
} else if (positional[0] === 'workspace') {
|
|
428
|
+
workspace(positional[1], { reinstall: args.includes('--reinstall') });
|
|
429
|
+
} else if (args.includes('--update')) {
|
|
430
|
+
update(positional[0], { offerMod: args.includes('--offer-mod') });
|
|
431
|
+
} else {
|
|
432
|
+
vendor(path.resolve(positional[0] || '.'), { code: args.includes('--code'), noMod: args.includes('--no-mod') });
|
|
433
|
+
}
|
|
372
434
|
}
|
|
435
|
+
if (require.main === module) main();
|
|
436
|
+
else module.exports = { hide, globalRoot, runInstalledPmos, launcherCurrent, launchWorkspace, installedWorkspaceVersion, installWorkspaceGlobal, stopWorkspaceServer, resetRoot: () => { rootMemo = null; } };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.523.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": {
|
|
@@ -32,6 +32,44 @@
|
|
|
32
32
|
"audit"
|
|
33
33
|
],
|
|
34
34
|
"pmosChanges": [
|
|
35
|
+
{
|
|
36
|
+
"v": "0.523.0",
|
|
37
|
+
"at": "2026-11-04T08:06:51.586Z",
|
|
38
|
+
"f": 4,
|
|
39
|
+
"prs": [
|
|
40
|
+
{
|
|
41
|
+
"n": 1221,
|
|
42
|
+
"t": "The installed app carries the kit, and ~/PMOS holds one product",
|
|
43
|
+
"s": "The installed app carries the kit, and ~/PMOS holds one product",
|
|
44
|
+
"k": "fix"
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"v": "0.522.0",
|
|
50
|
+
"at": "2026-11-04T07:22:01.639Z",
|
|
51
|
+
"f": 4,
|
|
52
|
+
"prs": [
|
|
53
|
+
{
|
|
54
|
+
"n": 1218,
|
|
55
|
+
"t": "The in-app update on Windows is fast and opens no console window",
|
|
56
|
+
"s": "The in-app update on Windows is fast and opens no console window",
|
|
57
|
+
"k": "fix"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"n": 1219,
|
|
61
|
+
"t": "Chat keeps its answers when the terminal opens; terminal fills the Viewer",
|
|
62
|
+
"s": "Chat keeps its answers when the terminal opens; terminal fills the Viewer",
|
|
63
|
+
"k": "fix"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"n": 1220,
|
|
67
|
+
"t": "Share several products or programs in one link, and a way back to PMOS",
|
|
68
|
+
"s": "Share several products or programs in one link, and a way back to PMOS",
|
|
69
|
+
"k": "new"
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
},
|
|
35
73
|
{
|
|
36
74
|
"v": "0.521.0",
|
|
37
75
|
"at": "2026-11-04T02:17:43.880Z",
|
|
@@ -348,52 +386,6 @@
|
|
|
348
386
|
"d": "The Working card could keep counting after Claude had answered. Now it goes away when the answer ends."
|
|
349
387
|
}
|
|
350
388
|
]
|
|
351
|
-
},
|
|
352
|
-
{
|
|
353
|
-
"v": "0.503.0",
|
|
354
|
-
"at": "2026-11-03T05:48:42.979Z",
|
|
355
|
-
"f": 4,
|
|
356
|
-
"prs": [
|
|
357
|
-
{
|
|
358
|
-
"n": 1186,
|
|
359
|
-
"t": "A timeline end ◇ names the initiative's own workflow, never a fixed Deliver",
|
|
360
|
-
"s": "A timeline end ◇ names the initiative's own workflow, never a fixed Deliver",
|
|
361
|
-
"k": "fix",
|
|
362
|
-
"h": "Timelines end at the right step",
|
|
363
|
-
"d": "A timeline always ended at Deliver, whatever the workflow. Now it ends at the initiative's own last step."
|
|
364
|
-
},
|
|
365
|
-
{
|
|
366
|
-
"n": 1187,
|
|
367
|
-
"t": "Working card leads with the todo or turn goal, not the raw command",
|
|
368
|
-
"s": "Working card leads with the todo or turn goal, not the raw command",
|
|
369
|
-
"k": "new",
|
|
370
|
-
"h": "A calmer Working card",
|
|
371
|
-
"d": "While Claude worked, a card showed the raw command. Now it leads with the to-do or goal it is working on."
|
|
372
|
-
},
|
|
373
|
-
{
|
|
374
|
-
"n": 1188,
|
|
375
|
-
"t": "New meeting recording view, word fix in place",
|
|
376
|
-
"s": "New meeting recording view, word fix in place",
|
|
377
|
-
"k": "new",
|
|
378
|
-
"h": "A new meeting recording view",
|
|
379
|
-
"d": "A recording showed everything at once. Now it opens on a short summary, each point has one check, and you can fix a misheard word in place."
|
|
380
|
-
}
|
|
381
|
-
]
|
|
382
|
-
},
|
|
383
|
-
{
|
|
384
|
-
"v": "0.502.0",
|
|
385
|
-
"at": "2026-11-03T04:57:38.850Z",
|
|
386
|
-
"f": 4,
|
|
387
|
-
"prs": [
|
|
388
|
-
{
|
|
389
|
-
"n": 1185,
|
|
390
|
-
"t": "Full task names in the Timeline too",
|
|
391
|
-
"s": "Full task names in the Timeline too",
|
|
392
|
-
"k": "fix",
|
|
393
|
-
"h": "Full task names in the Timeline",
|
|
394
|
-
"d": "The Timeline still cut long task names short. Now they wrap in full there too."
|
|
395
|
-
}
|
|
396
|
-
]
|
|
397
389
|
}
|
|
398
390
|
]
|
|
399
391
|
}
|
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-11-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-11-04T08:06:47Z"}
|
|
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-11-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-11-04T08:06:47Z"}
|
|
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-11-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-11-04T08:06:47Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
package/workspace/bin/pmos.js
CHANGED
|
@@ -200,7 +200,8 @@ function openBrowser(url) {
|
|
|
200
200
|
const cmd = process.platform === 'darwin' ? ['open', [url]]
|
|
201
201
|
: process.platform === 'win32' ? ['cmd', ['/c', 'start', '', url]]
|
|
202
202
|
: ['xdg-open', [url]];
|
|
203
|
-
|
|
203
|
+
// windowsHide: `cmd /c start` from a launcher with no console would otherwise flash a console window (batch 27 item 7).
|
|
204
|
+
try { spawn(cmd[0], cmd[1], { detached: true, stdio: 'ignore', windowsHide: true }).unref(); } catch {}
|
|
204
205
|
}
|
|
205
206
|
|
|
206
207
|
function npm(args) {
|
|
@@ -113,7 +113,7 @@ const ok = (name, cond, extra = '') => { n++; if (!cond) bad++; console.log(`${c
|
|
|
113
113
|
await s.p.click('[data-bp-sel="program:project-ascent"]'); await s.p.fill('#bpTeam', 'Product'); await s.p.click('[data-bp-days="30"]'); await s.p.fill('#bpPass', 'my-own-password-1');
|
|
114
114
|
await s.p.click('[data-bp="make"]'); await s.p.waitForSelector('[data-bp="copy"]');
|
|
115
115
|
const sent = W.posts[0] && W.posts[0].body;
|
|
116
|
-
ok('D1 a press sends what was picked, the team, the typed password and the days', sent && sent.ws === 'idpf' && sent.team === 'Product' && sent.password === 'my-own-password-1' && sent.days === 30 && sent.
|
|
116
|
+
ok('D1 a press sends what was picked, the team, the typed password and the days', sent && sent.ws === 'idpf' && sent.team === 'Product' && sent.password === 'my-own-password-1' && sent.days === 30 && sent.parts.some((x) => x.kind === 'program' && x.id === 'project-ascent') && sent.parts.some((x) => x.kind === 'initiative' && x.id === first && x.ws === 'idpf'), JSON.stringify(sent));
|
|
117
117
|
const t = await pop(s.p).textContent();
|
|
118
118
|
ok('D2 the link is ready: the link, the password once, one Copy button, and the note says it is shown once', /boards\.test\/b\/link00000003/.test(t) && /my-own-password-1/.test(t) && (await s.p.locator('[data-bp="copy"]').getAttribute('aria-label')) === 'Copy link and password' && /It is shown once/.test(t) && /for the Product team/.test(t));
|
|
119
119
|
await s.ctx.grantPermissions(['clipboard-read', 'clipboard-write']).catch(() => {});
|
package/workspace/changelog.json
CHANGED
|
@@ -1,4 +1,42 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"v": "0.523.0",
|
|
4
|
+
"at": "2026-11-04T08:06:51.586Z",
|
|
5
|
+
"f": 4,
|
|
6
|
+
"prs": [
|
|
7
|
+
{
|
|
8
|
+
"n": 1221,
|
|
9
|
+
"t": "The installed app carries the kit, and ~/PMOS holds one product",
|
|
10
|
+
"s": "The installed app carries the kit, and ~/PMOS holds one product",
|
|
11
|
+
"k": "fix"
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"v": "0.522.0",
|
|
17
|
+
"at": "2026-11-04T07:22:01.639Z",
|
|
18
|
+
"f": 4,
|
|
19
|
+
"prs": [
|
|
20
|
+
{
|
|
21
|
+
"n": 1218,
|
|
22
|
+
"t": "The in-app update on Windows is fast and opens no console window",
|
|
23
|
+
"s": "The in-app update on Windows is fast and opens no console window",
|
|
24
|
+
"k": "fix"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"n": 1219,
|
|
28
|
+
"t": "Chat keeps its answers when the terminal opens; terminal fills the Viewer",
|
|
29
|
+
"s": "Chat keeps its answers when the terminal opens; terminal fills the Viewer",
|
|
30
|
+
"k": "fix"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"n": 1220,
|
|
34
|
+
"t": "Share several products or programs in one link, and a way back to PMOS",
|
|
35
|
+
"s": "Share several products or programs in one link, and a way back to PMOS",
|
|
36
|
+
"k": "new"
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
},
|
|
2
40
|
{
|
|
3
41
|
"v": "0.521.0",
|
|
4
42
|
"at": "2026-11-04T02:17:43.880Z",
|
package/workspace/guide/home.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Guide
|
|
3
3
|
title: "Home: your list, the chat and the Viewer"
|
|
4
4
|
description: "Home: the list grouped by product, the Viewer, the place menu, Your day and its New for you fold, Search, and the initiative, meeting, report, Board and Insights features designed after it."
|
|
5
|
-
timestamp: 2026-11-
|
|
5
|
+
timestamp: 2026-11-04
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Home
|
|
@@ -193,7 +193,7 @@ There is no My work page. The list on Home is your list:
|
|
|
193
193
|
- **Sessions.** A session row names its agent. The coloured dot beside it is its state: blue waits on you, amber may be blocked, green is running. A session PMOS started itself (a skill or a background job) shows on Home only while it works or asks you something. Once it finishes its prompt, it leaves the list. On an initiative's page, a session that waits on you shows once, under **Needs you**, not again under **Sessions**. **Finished sessions** does not list an old run of a chat that is open again.
|
|
194
194
|
- **One chat box.** Every conversation has the same box: **+** (attach a file, notes, the terminal, and **Link to an initiative** for a loose chat), **/** for commands, **@** inside an initiative, the To or In chip, the mic, and Send. A chat opens at its newest message.
|
|
195
195
|
- **While Claude works.** A line at the top of the chat shows what Claude is trying to do. If Claude keeps a to-do list, the line shows the to-do in progress and how many are done, for example **2 of 5 done**. Tap the line to see the full list. If Claude has no to-do list, the line shows Claude's newest one-line status and the time since your message. The line never shows raw commands. To see the commands, tap the terminal icon.
|
|
196
|
-
- **A session's terminal.** **Watch its terminal** in **+**, or a session row such as **Claude sign-in**, opens the session's terminal in the Viewer. While the session runs, it is a real terminal: type in it to answer Claude or paste a code. When the session has ended, the Viewer shows it read only and says so.
|
|
196
|
+
- **A session's terminal.** **Watch its terminal** in **+**, or a session row such as **Claude sign-in**, opens the session's terminal in the Viewer. While the session runs, it is a real terminal: type in it to answer Claude or paste a code. It fills the Viewer and fits its lines again when you resize the Viewer or the window. The chat goes on showing every answer while the terminal is open and after you close it: the terminal is a second view of the same session. When the session has ended, the Viewer shows it read only and says so.
|
|
197
197
|
- **Setting up your first project opens by itself** until a registered folder holds an initiative or setup has finished. The `PMOS` folder that the one-line installer makes holds no initiative yet, so a new user who opens PMOS from the Start menu or the Dock still gets the setup conversation, not an empty Home.
|
|
198
198
|
- **Signing in to Claude when you set up your first project.** If the login you choose is signed in already, PMOS does not open a terminal. It tests the login at once. Otherwise PMOS runs `claude auth login`: Claude opens your browser to sign in, and the terminal under the question shows only Claude's request for a code, if it needs one. Claude's theme and trust-this-folder screens do not appear. When the sign-in ends, PMOS tests the login by itself. **The Claude login on this computer** is the login your usual terminal uses (`~/.claude.json`); PMOS no longer points it at a different file. You choose between two logins only: **The Claude login on this computer** and **A separate login just for PMOS**. The test passes for a login that works in your terminal. Claude updates its own files when it runs, and the test does not count that as a changed setup.
|
|
199
199
|
- **Documents not written yet.** A document that does not exist yet shows **Draft it**, in its stage or ahead of it. The agent writes a draft. Nothing is issued until you read it.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Guide
|
|
3
3
|
title: "Published board: a read-only copy of a Board"
|
|
4
4
|
description: "What a published board is, how to publish one from the Board, what a reader sees and cannot do, and how to preview one."
|
|
5
|
-
timestamp: 2026-11-
|
|
5
|
+
timestamp: 2026-11-04
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Published board: a read-only copy of a Board
|
|
@@ -18,7 +18,10 @@ Press the share icon on the Board's toolbar. A popover opens under it, inside th
|
|
|
18
18
|
The button is not on a narrow screen (720 px or less) and not on the All products page. Open one product to publish from it.
|
|
19
19
|
|
|
20
20
|
1. **Set the host.** The first time, the popover asks for the address of the board host. Ask the person who runs it. Write it as `https://boards.example.com`. The **Change** button on the form sets it again. If the computer has the `PMOS_BOARDS_HOST` variable, the variable is used.
|
|
21
|
-
2. **What to share.** The product of this board is picked. Press a
|
|
21
|
+
2. **What to share.** The product of this board is picked. Your other products and your programs are chips under **Products** and **Programs**. A program chip names the products it reaches. Press a chip to add it. Press **Initiative** to pick one initiative of this board. Press a picked chip again to remove it. **Publish** stays off while nothing is picked.
|
|
22
|
+
- **Several picks are one link.** The reader sees one tab for each pick, in the order you picked them. A program tab opens the program's page with all its products. One pick shows no tabs.
|
|
23
|
+
- **The name.** With two or more picks, the link's name is made from them: three names, then "+ N more". Press the pencil to change it. **Republish** keeps every pick and the name.
|
|
24
|
+
- **The size.** The whole link can be at most 2 MB. When your picks are more, PMOS says so and sends nothing. Pick less.
|
|
22
25
|
3. **Team.** Type the name of a team, or leave it empty. The host tells you when there is no team with that name.
|
|
23
26
|
4. **Password.** It is on, with a password that PMOS made. Press **New** for another one, or write your own. Turn the switch off for a link with no password.
|
|
24
27
|
5. **Ends.** The link ends after 1, 7 or 30 days.
|
|
@@ -54,6 +57,14 @@ The chips are **All**, **Yours** and one for each team. **Yours** shows only whe
|
|
|
54
57
|
- **Yours.** Your own links have **Republish** and **Turn off**, the same as in the Publish popover. **Turn off** asks you to press a second time.
|
|
55
58
|
- **A date in red.** The copy ends within a day and a half.
|
|
56
59
|
|
|
60
|
+
**Shared with you.** Paste a shared board link in the field at the top of **Shared** and press **+**. PMOS asks the host first. When the board has a password, PMOS asks for it once. PMOS keeps the link and its password on this computer only, the same as a team password. The board then shows under **Shared with you** and on the **With you** chip.
|
|
61
|
+
|
|
62
|
+
- Press the name of a board to open it in a new browser tab. You do not type its password again.
|
|
63
|
+
- A board that ends within 7 days shows how many days are left.
|
|
64
|
+
- A board that has ended, was turned off, or has a new password stays as one dimmed line with the reason. Press the name of a board with a new password to type it. Press **×** to forget a board.
|
|
65
|
+
|
|
66
|
+
**Open in my PMOS.** A shared board has two icons at the end of its "shared by" line. **Open in my PMOS** opens Board › Shared in the PMOS on the reader's own computer, with the link already in the field. Nothing is added until the reader presses **+**. The link never carries the password. It works when PMOS runs at its usual address, `http://127.0.0.1:7433`. **Get PMOS** opens pm-os.app/welcome in a new tab.
|
|
67
|
+
|
|
57
68
|
**A team that cannot be read.** One line shows in its place. It says if the team password was changed, if the host locked the team after too many wrong tries, or if the host did not answer. Press **Paste a new invite link** or **Refresh**. The other teams still show. PMOS shows no old rows for a team it cannot read.
|
|
58
69
|
|
|
59
70
|
**Leave a team.** Press the chip of the team. Then press **Leave team**, and press it a second time. PMOS forgets the team and its password on this computer. The copies that you published stay on the host. Join again with the invite link.
|
|
@@ -66,7 +77,8 @@ The tab is not in the read-only Board. A reader sees no Shared tab.
|
|
|
66
77
|
- On the Timeline, the view opens on today.
|
|
67
78
|
- On the Timeline, a milestone ◇ sits on its date, the same as on your own Board. A reader cannot press it.
|
|
68
79
|
- The span picker, the Deliveries lane, and the folds of a program and a product.
|
|
69
|
-
- One line above the Board. It names the selection, the date of the copy, who shared it and when the link ends.
|
|
80
|
+
- One line above the Board. It names the selection, the date of the copy, who shared it and when the link ends. It ends with **Open in my PMOS** and **Get PMOS**.
|
|
81
|
+
- One tab for each pick, when the link holds two or more products or programs.
|
|
70
82
|
- A viewer for each initiative. Press the name of an initiative to open it, on the stage track or on the Timeline. The viewer shows the stage card, About and Tasks. Tasks show for every initiative, also when a stage has a note such as "The rest, by work package". Tasks show as a List or as a Timeline, grouped by stage, owner or status.
|
|
71
83
|
- The panel divider and the panel layout: full screen, split or slide over.
|
|
72
84
|
|
|
@@ -190,4 +190,47 @@ function buildPack({ ws, board, selection, answers, meta }) {
|
|
|
190
190
|
return pack;
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
-
|
|
193
|
+
/** The name of a link of several picks (batch 27 item 1, ShA): the first three names, then "+ N more". */
|
|
194
|
+
function nameOf(labels) {
|
|
195
|
+
const l = (labels || []).map((x) => String(x || '').trim()).filter(Boolean);
|
|
196
|
+
return l.length > 3 ? l.slice(0, 3).join(' + ') + ' + ' + (l.length - 3) + ' more' : l.join(' + ');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** One pack for several products and programs (batch 27 item 1, ShA): one link, one tab per pick.
|
|
200
|
+
* packs [pack] one per product, each made by buildPack for that product's picks, so each is swept as it is today
|
|
201
|
+
* parts [{ label, ws, board, program?, kind? }] the tabs, in pick order: a product's Board, or a program's page opened on its home product
|
|
202
|
+
* name the link's name (nameOf the parts' labels when the PM did not rename it)
|
|
203
|
+
* The answers are joined: each product's keys name its ws, so they do not meet. The programs list is the one answer every product carries:
|
|
204
|
+
* a program is joined by id, its members being the union of what each product's pack kept. The 2 MB check runs on the whole. */
|
|
205
|
+
function combinePacks(packs, parts, name) {
|
|
206
|
+
const list = (packs || []).filter((p) => p && p.schema === SCHEMA && p.responses);
|
|
207
|
+
if (!list.length) plain('There is nothing to publish.');
|
|
208
|
+
const first = list[0];
|
|
209
|
+
const responses = {}; const progs = new Map();
|
|
210
|
+
for (const pk of list) {
|
|
211
|
+
for (const k of Object.keys(pk.responses)) {
|
|
212
|
+
if (k === '/api/programs') {
|
|
213
|
+
for (const p of (pk.responses[k] && pk.responses[k].programs) || []) {
|
|
214
|
+
if (!p || !p.id) continue;
|
|
215
|
+
const had = progs.get(p.id);
|
|
216
|
+
if (!had) { progs.set(p.id, clone(p)); continue; }
|
|
217
|
+
const seen = new Set((had.members || []).map((m) => m.ws + '|' + m.initiative));
|
|
218
|
+
for (const m of p.members || []) if (m && !seen.has(m.ws + '|' + m.initiative)) { had.members.push(clone(m)); seen.add(m.ws + '|' + m.initiative); }
|
|
219
|
+
}
|
|
220
|
+
} else if (!(k in responses)) responses[k] = pk.responses[k];
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
responses['/api/programs'] = { ok: true, programs: [...progs.values()] };
|
|
224
|
+
const sorted = {}; for (const k of Object.keys(responses).sort()) sorted[k] = responses[k];
|
|
225
|
+
const tabs = (parts || []).filter((t) => t && t.ws).map((t) => Object.assign({ label: String(t.label || t.ws), ws: String(t.ws), board: String(t.board || t.ws) }, t.program ? { program: String(t.program) } : {}));
|
|
226
|
+
const label = String(name || '').trim() || nameOf(tabs.map((t) => t.label));
|
|
227
|
+
const pack = { schema: SCHEMA, viewer: first.viewer || '', scope: { kind: 'multi', id: tabs.map((t) => t.ws + (t.program ? ':' + t.program : '')).join(','), label }, by: first.by || '', at: first.at || '', ends: first.ends || '' };
|
|
228
|
+
if (first.hub) pack.hub = first.hub;
|
|
229
|
+
if (first.view) pack.view = first.view;
|
|
230
|
+
if (tabs.length) pack.parts = tabs; // the read-only Board opens on the first, and draws the tab row only for two or more
|
|
231
|
+
pack.responses = sorted;
|
|
232
|
+
assertSize(pack);
|
|
233
|
+
return pack;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
module.exports = { VIEWS, SCHEMA, MAX_BYTES, KINDS, MODEL_KEYS, buildPack, assertSize, teammateDash, combinePacks, nameOf };
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
/* board-publish-choices.js — what can be published from a board (initiative board-publish, slice 2, page side; PRD P1): its product, the programs that hold
|
|
3
3
|
* one of its initiatives, and its initiatives. Read from the Board's own routes, the way lib/board-pack-route.js reads them.
|
|
4
|
-
* choicesOf(req, ctx, ws, board) -> { products, programs, initiatives } | null while the board is still being read
|
|
4
|
+
* choicesOf(req, ctx, ws, board) -> { products, programs, initiatives } | null while the board is still being read
|
|
5
|
+
* contextOf(req, ctx, ws, board) -> { model, programs, boards } | null what a publish of several picks needs (lib/board-publish-parts.js) */
|
|
5
6
|
const BoardPremium = require('./routes-board-premium.js');
|
|
6
7
|
const Programs = require('./routes-programs.js');
|
|
7
8
|
|
|
@@ -13,13 +14,28 @@ async function via(mod, pathAndQuery, req, ctx) {
|
|
|
13
14
|
return handled === false ? null : out;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
/** What a publish needs to know besides the picks: every program (with its members), and the products (boards) this workspace has, from the
|
|
18
|
+
* board on screen's own switcher. { model, programs, boards } | null while the board is still being read. */
|
|
19
|
+
async function contextOf(req, ctx, ws, board) {
|
|
17
20
|
const b = await via(BoardPremium, '/api/board-premium?ws=' + encodeURIComponent(ws) + '&board=' + encodeURIComponent(board), req, ctx);
|
|
18
21
|
if (!b || b.ok === false || !b.model) return null;
|
|
19
|
-
const rows = (b.model.rows || []).filter((r) => r && r.id); const have = new Set(rows.map((r) => r.id));
|
|
20
22
|
const pg = await via(Programs, '/api/programs', req, ctx);
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
+
const here = { id: board, ws, name: (b.model.board && b.model.board.name) || board };
|
|
24
|
+
const boards = [here].concat(((b.model.board && b.model.board.switcher) || []).filter((x) => x && x.ws && !(x.ws === ws && x.id === board)));
|
|
25
|
+
return { model: b.model, programs: ((pg && pg.programs) || []).filter((p) => p && p.id && !p.archived), boards };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The Share sheet's chips (batch 27 item 1, ShA): the board on screen first, then the other products; every program that holds an initiative of a
|
|
29
|
+
* product here, with the products it reaches; and the initiatives of the board on screen. A program says which products it reaches (`reach`). */
|
|
30
|
+
async function choicesOf(req, ctx, ws, board) {
|
|
31
|
+
const c = await contextOf(req, ctx, ws, board);
|
|
32
|
+
if (!c) return null;
|
|
33
|
+
const rows = (c.model.rows || []).filter((r) => r && r.id); const have = new Set(rows.map((r) => r.id));
|
|
34
|
+
const nameOf = new Map(c.boards.map((x) => [x.ws, x.name]));
|
|
35
|
+
const programs = c.programs.filter((p) => (p.members || []).some((m) => m && (m.ws === ws ? have.has(m.initiative) : nameOf.has(m.ws))))
|
|
36
|
+
.map((p) => ({ id: p.id, label: p.title || p.id, reach: [...new Set((p.members || []).map((m) => m && nameOf.get(m.ws)).filter(Boolean))].join(', ') }));
|
|
37
|
+
const products = c.boards.map((x, i) => Object.assign({ id: x.id, ws: x.ws, label: x.name || x.id }, i === 0 ? { n: rows.length } : {}));
|
|
38
|
+
return { products, programs, initiatives: rows.map((r) => ({ id: r.id, label: r.title || r.id })) };
|
|
23
39
|
}
|
|
24
40
|
|
|
25
|
-
module.exports = { choicesOf };
|
|
41
|
+
module.exports = { choicesOf, contextOf };
|