create-pmos 0.144.0 → 0.145.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-pmos",
3
- "version": "0.144.0",
3
+ "version": "0.145.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": {
@@ -1,3 +1,3 @@
1
- kernel-source: pmos@9f8623c
1
+ kernel-source: pmos@e7517fc
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-12T01:54:19Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-12T01:59:40Z"}
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-12T01:54:19Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-12T01:59:40Z"}
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-12T01:54:19Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-12T01:59:40Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -37,6 +37,7 @@ const { harness } = require('./c3-ini-harness');
37
37
  await page.fill('[data-ibt="f-more"]', 'Which trials need SSO first?');
38
38
  await page.click('[data-ibt="more-go"]');
39
39
  await until(() => store(() => window.__c3boards.asks.find((a) => a.id === 'ask-1').needMore === true));
40
+ await until(async () => /prompted for more detail/.test(await toast()));
40
41
  ok('T6 Ask for more records the question; the ask stays open', await store(() => { const a = window.__c3boards.asks.find((x) => x.id === 'ask-1'); return a.state === 'open' && a.moreNote === 'Which trials need SSO first?'; }) && /prompted for more detail/.test(await toast()));
41
42
 
42
43
  await page.click('[data-ibt-ask="ask-1"] .tt').catch(() => {});
@@ -0,0 +1,131 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ /* c3-updates-test.js — console3 Settings › Updates runs the app update itself, and the kit Update sends the checked version
4
+ * (initiative c3-app-update).
5
+ *
6
+ * The page used to say "run it from the main app" and link to /, and its kit Update posted no version, which the server
7
+ * refuses with "Check for updates first". Against a real server; the kit routes and /api/sessions are stood in by the
8
+ * page's route table, so nothing is installed and npm is never asked. Hooks only (data-c3set-*).
9
+ */
10
+ const path = require('path');
11
+ const { spawn } = require('child_process');
12
+ const { findBrowser } = require('./lib/find-browser');
13
+ const T = require('./test-run').configure({ name: 'c3-updates' });
14
+
15
+ const BASE = T.BASE;
16
+ const results = [];
17
+ const ok = (name, cond, extra = '') => { results.push({ name, cond: !!cond }); console.log(`${cond ? '✓' : '✗'} ${name}${!cond && extra !== '' ? ' — ' + extra : ''}`); };
18
+ let srv = null; let browser = null;
19
+ const finish = async (code) => {
20
+ if (browser) await browser.close().catch(() => {});
21
+ if (srv) { try { srv.kill('SIGKILL'); } catch { /* gone */ } }
22
+ T.killTmux();
23
+ const bad = results.filter((x) => !x.cond).length;
24
+ console.log(`\n${results.length - bad}/${results.length} c3-updates assertions passed`);
25
+ process.exit(code !== undefined ? code : (bad ? 1 : 0));
26
+ };
27
+ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
28
+
29
+ async function waitFor(page, fn, arg, ms = 8000) {
30
+ try { await page.waitForFunction(fn, arg, { timeout: ms }); return true; } catch { return false; }
31
+ }
32
+
33
+ (async () => {
34
+ const exe = findBrowser();
35
+ if (!exe) { console.log('✗ no Chrome/Chromium on this machine — the c3-updates suite cannot run'); return finish(1); }
36
+ let chromium;
37
+ try { ({ chromium } = require('playwright-core')); } catch { console.log('✗ playwright-core is not installed — the c3-updates suite cannot run'); return finish(1); }
38
+
39
+ srv = spawn(process.execPath, [path.join(__dirname, 'server.js')], { cwd: __dirname, stdio: 'ignore', env: T.env({ PMOS_WS_PORT: String(T.PORT) }) });
40
+ await T.waitForServer(BASE);
41
+ browser = await chromium.launch({ executablePath: exe });
42
+ const ctx = await browser.newContext({ viewport: { width: 1440, height: 1000 } });
43
+ const page = await ctx.newPage();
44
+ const errors = [];
45
+ page.on('pageerror', (e) => errors.push(String(e && e.message || e)));
46
+
47
+ // The stand-ins: an installed app 0.1.0 and a kit 0.1.0 in every workspace, both behind 0.2.0.
48
+ const S = { app: '0.1.0', kit: '0.1.0', checkout: false, upgrade: { ok: true }, down: 0 };
49
+ const calls = [];
50
+ const body = (ws) => ({ ok: true, ws, app: { version: S.app, fromCheckout: S.checkout }, kit: { present: true, version: S.kit }, source: { kind: 'npm' } });
51
+ const wsOf = (route) => { try { return JSON.parse(route.request().postData() || '{}'); } catch { return {}; } };
52
+ await page.route((u) => u.pathname === '/api/kit/status', (route) => route.fulfill({ json: { ...body(new URL(route.request().url()).searchParams.get('ws')), busy: null } }));
53
+ await page.route((u) => u.pathname === '/api/kit/check', (route) => { const b = wsOf(route); calls.push(['check', b]); route.fulfill({ json: { ...body(b.ws), latest: '0.2.0', appBehind: S.app !== '0.2.0', kitBehind: S.kit !== '0.2.0' } }); });
54
+ await page.route((u) => u.pathname === '/api/kit/update', (route) => {
55
+ const b = wsOf(route); calls.push(['kit', b]);
56
+ if (b.version !== '0.2.0') return route.fulfill({ status: 502, json: { ok: false, error: 'Check for updates first' } });
57
+ route.fulfill({ json: { ...body(b.ws), kit: { present: true, version: '0.2.0' }, output: 'kit updated' } });
58
+ });
59
+ await page.route((u) => u.pathname === '/api/kit/upgrade-app', (route) => {
60
+ const b = wsOf(route); calls.push(['app', b]);
61
+ if (!S.upgrade.ok) return route.fulfill({ status: 403, json: S.upgrade });
62
+ S.down = 2; // the server is away for two polls, then answers on the new version
63
+ route.fulfill({ json: { ok: true, ws: b.ws, from: '0.1.0', to: b.version, log: '/tmp/app-update.log' } });
64
+ });
65
+ let reloaded = false;
66
+ await page.route((u) => u.pathname === '/api/sessions', async (route) => {
67
+ if (!calls.some((c) => c[0] === 'app' && S.upgrade.ok)) return route.continue();
68
+ if (S.down > 0) { S.down -= 1; return route.abort('connectionrefused'); }
69
+ S.app = '0.2.0';
70
+ const r = await route.fetch(); const j = await r.json().catch(() => ({}));
71
+ return route.fulfill({ json: { ...j, version: S.app } });
72
+ });
73
+ page.on('framenavigated', (f) => { if (f === page.mainFrame() && calls.some((c) => c[0] === 'app')) reloaded = true; });
74
+ const named = (kind) => calls.filter((c) => c[0] === kind);
75
+
76
+ const open = async () => {
77
+ await page.goto(BASE + '/c3.html?tab=settings');
78
+ await waitFor(page, () => !!document.querySelector('[data-c3set-nav] [data-c3set-sec="updates"]'));
79
+ await page.click('[data-c3set-nav] [data-c3set-sec="updates"]');
80
+ return waitFor(page, () => !!document.querySelector('[data-c3set-section="updates"] [data-c3set-check]:not([disabled])'));
81
+ };
82
+
83
+ /* ── A: before a check, nothing is offered and npm is not asked ── */
84
+ ok('A1 Updates opens with Check now', await open());
85
+ ok('A2 no app Update and no Open the main app before a check', await page.evaluate(() => !document.querySelector('[data-c3set-app-update]') && !/Open the main app/.test(document.querySelector('[data-c3set-section="updates"]').textContent)));
86
+ ok('A3 nothing was asked of npm before the press', calls.length === 0, JSON.stringify(calls));
87
+
88
+ /* ── B: the kit Update sends the version the check found ── */
89
+ await page.click('[data-c3set-check]');
90
+ ok('B1 after the check, the app row offers Update to 0.2.0', await waitFor(page, () => { const b = document.querySelector('[data-c3set-app-update]'); return !!b && /Update to 0\.2\.0/.test(b.textContent); }));
91
+ ok('B2 the page no longer sends you to the main app', await page.evaluate(() => !/Open the main app/.test(document.querySelector('[data-c3set-section="updates"]').textContent)));
92
+ ok('B3 each workspace row offers a kit Update', await waitFor(page, () => !!document.querySelector('[data-c3set-kit-update]')));
93
+ const kitWs = await page.getAttribute('[data-c3set-kit-update]', 'data-c3set-kit-update');
94
+ await page.click(`[data-c3set-kit-update="${kitWs}"]`);
95
+ ok('B4 kit Update posts that workspace and the checked version', await waitFor(page, () => !!document.querySelector('[data-c3-toast]') && /Updated the kit/.test(document.querySelector('[data-c3-toast]').textContent)) && named('kit').length === 1 && named('kit')[0][1].ws === kitWs && named('kit')[0][1].version === '0.2.0', JSON.stringify(named('kit')));
96
+ ok('B5 that row no longer offers Update', await waitFor(page, (w) => !document.querySelector(`[data-c3set-kit-update="${w}"]`), kitWs));
97
+
98
+ /* ── C: the app update asks first, then runs, waits for the restart and reloads ── */
99
+ await page.click('[data-c3set-app-update]');
100
+ ok('C1 it asks first, and says PMOS stops while the new version installs', await waitFor(page, () => { const c = document.querySelector('[data-c3set-app-confirm]'); return !!c && /stops/.test(c.textContent) && !!document.querySelector('[data-c3set-app-go]'); }));
101
+ ok('C2 nothing runs before the answer', named('app').length === 0);
102
+ await page.click('[data-c3set-app-cancel]');
103
+ ok('C3 Cancel puts the Update button back', await waitFor(page, () => !document.querySelector('[data-c3set-app-confirm]') && !!document.querySelector('[data-c3set-app-update]')) && named('app').length === 0);
104
+ await page.click('[data-c3set-app-update]');
105
+ await page.click('[data-c3set-app-go]');
106
+ for (let i = 0; i < 40 && !named('app').length; i++) await new Promise((r) => setTimeout(r, 100));
107
+ ok('C4 the update posts the checked version', named('app').length === 1 && named('app')[0][1].version === '0.2.0', JSON.stringify(named('app')));
108
+ ok('C5 the row says it is updating and that the page reloads', await waitFor(page, () => { const b = document.querySelector('[data-c3set-app-busy]'); return !!b && /reloads/.test(b.textContent); }, null, 3000) || reloaded);
109
+ for (let i = 0; i < 200 && !reloaded; i++) await new Promise((r) => setTimeout(r, 100));
110
+ ok('C6 the page reloads by itself when the new version answers', reloaded);
111
+
112
+ /* ── D: a refusal is shown on the row ── */
113
+ S.app = '0.1.0'; S.upgrade = { ok: false, error: 'This page signed in with a token that changes when the workspace restarts' };
114
+ calls.length = 0; reloaded = false;
115
+ await open();
116
+ await page.click('[data-c3set-check]');
117
+ await waitFor(page, () => !!document.querySelector('[data-c3set-app-update]'));
118
+ await page.click('[data-c3set-app-update]');
119
+ await page.click('[data-c3set-app-go]');
120
+ ok('D1 a refused update says why, on the app row', await waitFor(page, () => { const r = document.querySelector('[data-c3set-row="upd.app"]'); return !!r && /token that changes/.test(r.textContent); }));
121
+ ok('D2 a refused update does not reload the page', !reloaded);
122
+
123
+ /* ── E: a checkout updates with git ── */
124
+ S.checkout = true; S.upgrade = { ok: true }; calls.length = 0;
125
+ await open();
126
+ await page.click('[data-c3set-check]');
127
+ ok('E1 a checkout says git pull and offers no app Update', await waitFor(page, () => /git pull/.test(document.querySelector('[data-c3set-row="upd.app"]').textContent)) && !(await page.$('[data-c3set-app-update]')));
128
+
129
+ ok('F1 the page throws no error', errors.length === 0, errors.slice(0, 3).join(' | '));
130
+ finish();
131
+ })();
@@ -2,7 +2,7 @@
2
2
  type: Guide
3
3
  title: "Settings in console3: one home for each setting"
4
4
  description: "How to open Settings in console3, what each section holds, what the scope tags and the environment notes mean, and where the settings are kept."
5
- timestamp: 2026-10-11
5
+ timestamp: 2026-10-12
6
6
  ---
7
7
 
8
8
  # Settings in console3: one home for each setting
@@ -55,6 +55,14 @@ When a section has changed settings, **Reset section** at the top of the section
55
55
 
56
56
  Some rows show a feature that PMOS does not have yet, such as choosing calendars, mapping Jira projects, pairing a phone, the device list, and export or reset of every setting. The control on these rows is off, and the row says why.
57
57
 
58
+ ## Update PMOS and the kit
59
+
60
+ Open **Updates** and press **Check now**. PMOS asks the npm registry for the latest version only when you press it.
61
+
62
+ - **Update to** a version on the **PMOS app** row updates the app. The row asks first: press **Restart and update**, or **Cancel**. PMOS stops, installs the new version, and starts again. Open terminals keep running. The page reloads when the new version starts. If the update is refused, or PMOS starts again on the old version, or PMOS does not come back, the row says so and names the update log.
63
+ - When PMOS runs from a PMOS checkout, the row says to update it with `git pull`. There is no Update button.
64
+ - **Update** on a workspace row updates the kit in that repository. It replaces the kit files in its `pmos/` folder (AGENTS.md, skills, scripts, templates, `okf/core`, the mod and KIT-VERSION). It does not change `state/`, `planning/` or `okf/product/`, and it does not commit. Review the change in the repository as a normal pull request.
65
+
58
66
  ## Where the settings are kept
59
67
 
60
68
  The settings that had a home before this page keep it. For example, the recording engine is kept by the notes engine, Validate consent by the validate route, and the theme and the new-session defaults in this browser.
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.144.0",
3
+ "version": "0.145.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pmos-workspace",
9
- "version": "0.144.0",
9
+ "version": "0.145.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.144.0",
3
+ "version": "0.145.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",
@@ -15,7 +15,7 @@
15
15
  * TypeSafe key GET /api/typesafe (present or not; the key is never read)
16
16
  * about you GET and POST /api/proactive/settings {name, about:{role, work, duties}} (the Welcome page writes the same)
17
17
  * running sessions GET /api/sessions calendar import POST /api/sources/refresh {source:'calendar'}
18
- * versions GET /api/kit/status · POST /api/kit/check, /api/kit/update
18
+ * versions GET /api/kit/status · POST /api/kit/check, /api/kit/update, /api/kit/upgrade-app (then GET /api/sessions until the new version)
19
19
  * Every other setting is in the settings store: GET /api/settings and POST /api/settings (lib/settings-store.js).
20
20
  * A board's own workflow is edited on the board; here are only the templates new boards start from.
21
21
  * Nothing here asks for or shows a key or a password. */
@@ -161,7 +161,7 @@ export function mountSettings(host, ctx = {}) {
161
161
  ensureCss();
162
162
  const M = {
163
163
  sec: SECTIONS.some((s) => s[0] === ctx.section) ? ctx.section : 'about', q: '', ws: ctx.ws || '', data: null, err: {},
164
- engine: null, people: null, consent: null, online: null, profiles: null, typesafe: null, kits: {}, about: null, running: null,
164
+ engine: null, people: null, consent: null, online: null, profiles: null, typesafe: null, kits: {}, kitBusy: null, app: { phase: null, error: '' }, about: null, running: null,
165
165
  tplEdit: null, wsEdit: null, wsDrop: null, addPath: '',
166
166
  };
167
167
  const root = el('div', { class: 'brief c3set', 'data-c3set': '' });
@@ -607,6 +607,28 @@ export function mountSettings(host, ctx = {}) {
607
607
  lockedRow('look.icons', 'Card icons', 'Chosen from the built-in icon set by the card kind (D7).', 'Chosen by code rules', 'computer')], 'eye')];
608
608
  }
609
609
 
610
+ /* ── the app update (workspace-kit-update): the server stops, reinstalls and starts again, so the page waits for it ── */
611
+ const pause = (ms) => new Promise((r) => setTimeout(r, ms));
612
+ async function updateApp(ws, from, to) {
613
+ M.app = { phase: 'busy', error: '', to }; draw();
614
+ const r = await pmPost('/api/kit/upgrade-app', { ws, version: to });
615
+ if (!r || r.ok === false) { M.app = { phase: null, error: (r && r.error) || 'The update did not start.' }; draw(); return; }
616
+ await waitForRestart(r.from || from, r.log || '');
617
+ }
618
+ /** Reload when the server is back on a new version; otherwise say what happened and name the log. */
619
+ async function waitForRestart(from, log) {
620
+ let sawDown = false;
621
+ const until = Date.now() + 5 * 60000;
622
+ while (Date.now() < until) {
623
+ await pause(2000);
624
+ const j = await pmGet('/api/sessions');
625
+ if (!j || j.ok === false || !j.version) { sawDown = true; continue; }
626
+ if (j.version !== from) { location.reload(); return; }
627
+ if (sawDown) { M.app = { phase: null, error: 'PMOS started again on ' + j.version + ', so the update did not finish. The update log is ' + log + '.' }; draw(); return; }
628
+ }
629
+ M.app = { phase: null, error: 'PMOS did not come back on this address. Type pmos in a terminal to open it. The update log is ' + log + '.' }; draw();
630
+ }
631
+
610
632
  function updatesSec() {
611
633
  const rows = (ctx.workspaces || []).map((w) => {
612
634
  const k = M.kits[w.id];
@@ -616,20 +638,31 @@ export function mountSettings(host, ctx = {}) {
616
638
  el('div', { class: 'chips' }, chip(!k ? 'Reading' : k.error ? k.error : kit && kit.present ? 'kit ' + (kit.version || 'vendored by hand') : 'no kit in this workspace', 'mono'),
617
639
  k && k.latest ? (behind ? tag(k.latest + ' available', 'warn') : tag('Up to date', 'ok')) : null),
618
640
  M.err['kit:' + w.id] ? fold('alert', M.err['kit:' + w.id], 'w') : null),
619
- el('div', { class: 'acts' }, behind ? btn('Update', { variant: 'primary', onclick: async () => { const r = await pmPost('/api/kit/update', { ws: w.id }); if (r && r.ok) { M.kits[w.id] = { ...M.kits[w.id], ...r, kitBehind: false }; toast('Updated the kit in ' + (w.label || w.name || w.id)); } else M.err['kit:' + w.id] = (r && r.error) || 'Not updated.'; draw(); } }) : null));
641
+ el('div', { class: 'acts' }, behind ? btn('Update', { variant: 'primary', disabled: !!M.kitBusy, attrs: { 'data-c3set-kit-update': w.id }, onclick: async () => { M.kitBusy = w.id; delete M.err['kit:' + w.id]; draw(); const r = await pmPost('/api/kit/update', { ws: w.id, version: k.latest }); M.kitBusy = null; if (r && r.ok) { M.kits[w.id] = { ...M.kits[w.id], ...r, kitBehind: false }; toast('Updated the kit in ' + (w.label || w.name || w.id)); } else M.err['kit:' + w.id] = (r && r.error) || 'Not updated.'; draw(); } }) : null));
620
642
  });
621
643
  const first = (ctx.workspaces || [])[0];
622
644
  const st = first && M.kits[first.id];
623
645
  const app = st && st.app; const appBehind = st && st.appBehind;
624
- const appRow = frame({ key: 'upd.app', label: 'PMOS app ' + ((app && app.version) || ''),
625
- help: appBehind ? 'The app update restarts PMOS. Run it from the main app\'s Settings, Updates, which waits for the restart.' : 'Press Check now to ask the npm registry for the latest version.',
626
- control: [st && st.latest ? tag(appBehind ? st.latest + ' available' : 'Up to date', appBehind ? 'acc' : 'ok') : null,
627
- btn('Check now', { ic: 'undo', disabled: !first, attrs: { 'data-c3set-check': '' }, onclick: async () => {
646
+ // A checkout updates with git, unless PMOS_UPDATE_SOURCE names a local package (server.js /api/kit/upgrade-app).
647
+ const gitOnly = !!(app && app.fromCheckout && !(st.source && st.source.kind === 'local'));
648
+ const U = M.app; const busy = U.phase === 'busy';
649
+ const appHelp = !appBehind ? 'Press Check now to ask the npm registry for the latest version.'
650
+ : gitOnly ? 'This workspace runs from a PMOS checkout. Update it with git pull.'
651
+ : 'The update stops PMOS, installs ' + st.latest + ' in place of ' + ((app && app.version) || '?') + ', and starts it again. Sessions keep running in tmux. This page reloads when the new version starts.';
652
+ const asking = appBehind && !gitOnly && U.phase === 'confirm';
653
+ const appRow = frame({ key: 'upd.app', label: 'PMOS app ' + ((app && app.version) || ''), help: appHelp, err: U.error || null,
654
+ note: asking ? el('span', { 'data-c3set-app-confirm': '' }, fold('alert', 'PMOS stops for about a minute while ' + st.latest + ' installs. Open terminals keep running.', 'w'))
655
+ : busy ? el('span', { 'data-c3set-app-busy': '', role: 'status', 'aria-live': 'polite' }, fold('cloud', 'Updating PMOS to ' + U.to + '. This page reloads when the new version starts.')) : null,
656
+ control: asking ? [btn('Cancel', { variant: 'ghost', attrs: { 'data-c3set-app-cancel': '' }, onclick: () => { M.app = { phase: null, error: '' }; draw(); } }),
657
+ btn('Restart and update', { variant: 'primary', attrs: { 'data-c3set-app-go': '' }, onclick: () => updateApp(first.id, (app && app.version) || '', st.latest) })]
658
+ : [st && st.latest ? tag(appBehind ? st.latest + ' available' : 'Up to date', appBehind ? 'acc' : 'ok') : null,
659
+ btn('Check now', { ic: 'undo', disabled: !first || busy, attrs: { 'data-c3set-check': '' }, onclick: async () => {
660
+ M.app = { phase: null, error: '' };
628
661
  for (const w of ctx.workspaces || []) { const r = await pmPost('/api/kit/check', { ws: w.id }); M.kits[w.id] = r && r.ok ? r : { ...(M.kits[w.id] || {}), error: (r && r.error) || 'The check failed.' }; }
629
662
  const s2 = first && M.kits[first.id]; toast(s2 && s2.appBehind ? 'PMOS ' + s2.latest + ' is available' : 'Checked. PMOS is up to date'); draw();
630
663
  } }),
631
- appBehind ? btn('Open the main app', { variant: 'primary', onclick: () => { location.href = '/'; } }) : null] });
632
- return [sect('PMOS app', [list([appRow])], { ic: 'cloud' }), sect('Kit in each workspace', [list(rows)], { ic: 'repo', count: rows.length }), groupOf('updates', 'When', 'cloud')];
664
+ appBehind && !gitOnly && !busy ? btn('Update to ' + st.latest, { variant: 'primary', attrs: { 'data-c3set-app-update': '' }, onclick: () => { M.app = { phase: 'confirm', error: '' }; draw(); } }) : null] });
665
+ return [sect('PMOS app', [list([appRow])], { ic: 'cloud', attrs: { 'data-c3set-app': '' } }), sect('Kit in each workspace', [list(rows)], { ic: 'repo', count: rows.length }), groupOf('updates', 'When', 'cloud')];
633
666
  }
634
667
 
635
668
  const BUILD = { about: aboutSec, workspaces: workspacesSec, boards: boardsSec, connectors: connectorsSec, recording: recordingSec, time: timeSec, sharing: sharingSec, access: accessSec, sessions: sessionsSec, privacy: privacySec, appearance: appearanceSec, updates: updatesSec };
@@ -133,7 +133,8 @@
133
133
  "pm-card-layout-test.js",
134
134
  "pm-portfolio-inline-test.js",
135
135
  "pm-brief-page-test.js",
136
- "c3-settings-test.js"
136
+ "c3-settings-test.js",
137
+ "c3-updates-test.js"
137
138
  ],
138
139
  "ui": [
139
140
  "ui-test.js"
@@ -263,7 +264,8 @@
263
264
  "pm-card-layout-test.js": 20,
264
265
  "pm-portfolio-inline-test.js": 15,
265
266
  "settings-store.test.js": 1,
266
- "c3-settings-test.js": 25
267
+ "c3-settings-test.js": 25,
268
+ "c3-updates-test.js": 20
267
269
  },
268
270
  "_comment_seconds": "Measured wall time of each suite run alone (4-CPU sandbox, 2026-10-05). Only the ORDER uses it: the slowest suites start first, and --shard deals suites to the least-loaded shard so the shards finish together. A wrong or missing number never skips a suite; a missing one counts as 10."
269
271
  }