create-pmos 0.253.0 → 0.255.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.
@@ -8,8 +8,9 @@ const ICON = {
8
8
  meeting: '<rect x="4" y="5" width="16" height="15" rx="3"/><path d="M4 10h16M9 3v4M15 3v4"/>',
9
9
  merge: '<circle cx="6" cy="6" r="2"/><circle cx="6" cy="18" r="2"/><circle cx="18" cy="9" r="2"/><path d="M6 8v8M18 11c0 4-6 3-12 5"/>',
10
10
  run: '<path d="M13 3 5 14h6l-1 7 8-11h-6z"/>',
11
+ digest: '<path d="M5 7h14M5 12h14M5 17h9"/>',
11
12
  };
12
- const SRC = { meeting: 'Calendar', merge: 'Merges', run: 'Agent runs' };
13
+ const SRC = { meeting: 'Calendar', merge: 'Merges', run: 'Agent runs', digest: 'Digests' };
13
14
  let M = null; // the model
14
15
  const on = new Set(Object.keys(SRC));
15
16
  const pinned = new Set();
@@ -42,7 +43,7 @@ function drawRows() {
42
43
  const keys = rows.map((r) => r.key), off = (M.list.putOff || []).map((x) => x.key); // a row put off is not done: it leaves both counts and is named below the list
43
44
  const base = (M.pending ? keys : baseline(keys)).filter((k) => !off.includes(k)), done = base.filter((k) => !keys.includes(k)).length, total = base.length;
44
45
  const order = rows.slice().sort((a, b) => (pinned.has(b.key) - pinned.has(a.key)) || (a.n - b.n));
45
- L.innerHTML = order.map((r, i) => `<div class="rr${i < 3 ? ' lead' : ''}${i === 0 ? ' first' : ''}" data-key="${esc(r.key)}"${linkOf(r) ? ` data-link="${esc(linkOf(r))}"` : ''}><button class="rhd" aria-haspopup="dialog"><span class="rn">${STAMP(i + 1)}</span><span><span class="rt">${esc(r.title)}${i === 0 ? '<em class="tgf">Do this first</em>' : ''}</span><span class="rs">${esc(r.subject)}</span><span class="sug"><span>${esc(r.prepared)}</span></span><span class="skip">${esc(r.skip)}</span></span></button><button class="rpin" data-pin="${esc(r.key)}" aria-pressed="${pinned.has(r.key)}" aria-label="Pin ${esc(r.title)} to the top" title="Pin to the top"><svg class="i" viewBox="0 0 24 24"><path d="M9 4h6l-1 6 3 3H7l3-3zM12 13v7"/></svg></button></div>`).join('')
46
+ L.innerHTML = order.map((r, i) => `<div class="rr${i < 3 ? ' lead' : ''}${i === 0 ? ' first' : ''}" data-key="${esc(r.key)}"${linkOf(r) ? ` data-link="${esc(linkOf(r))}"` : ''}><button class="rhd" aria-haspopup="dialog"><span class="rn">${STAMP(i + 1)}</span><span><span class="rt">${esc(r.title)}${i === 0 ? '<em class="tgf">Do this first</em>' : ''}</span><span class="rs">${r.product ? `<b class="pn">${esc(r.product)}</b>` : ''}${esc(r.subject)}</span><span class="sug"><span>${esc(r.prepared)}</span></span><span class="skip">${esc(r.skip)}</span></span></button><button class="rpin" data-pin="${esc(r.key)}" aria-pressed="${pinned.has(r.key)}" aria-label="Pin ${esc(r.title)} to the top" title="Pin to the top"><svg class="i" viewBox="0 0 24 24"><path d="M9 4h6l-1 6 3 3H7l3-3zM12 13v7"/></svg></button></div>`).join('')
46
47
  + (M.list.more ? `<p class="note">${M.list.more} more ${M.list.more === 1 ? 'row is' : 'rows are'} not shown here.</p>` : '');
47
48
  const n = M.list.count;
48
49
  $('#movesCap').innerHTML = M.pending ? `${n} ${n === 1 ? 'row' : 'rows'} so far · still reading` : n ? `<b>${done} of ${total}</b> done · ${n} ${n === 1 ? 'row' : 'rows'} left` : 'All clear';
@@ -66,11 +67,11 @@ $('#rank').addEventListener('click', (e) => {
66
67
  const linkOf = (r) => ((r.from || []).find((x) => /^meeting:/.test(x)) || '').replace(/^meeting:/, '');
67
68
  function evHtml(e, i) {
68
69
  const lnk = e.kind === 'meeting' && (M.list.rows || []).some((r) => linkOf(r) === e.title) ? e.title : '';
69
- return `<button class="ev src-${e.kind}"${lnk ? ` data-link="${esc(lnk)}"` : ''} data-i="${i}" style="animation:rise .6s var(--ease) both"><time>${esc(e.t)}</time><span class="gl-t"><svg class="i" viewBox="0 0 24 24">${ICON[e.kind]}</svg></span><span><b>${esc(e.title)}</b><span class="d">${esc(e.sub)}</span></span></button>`;
70
+ return `<button class="ev src-${e.kind}"${lnk ? ` data-link="${esc(lnk)}"` : ''} data-i="${i}" style="animation:rise .6s var(--ease) both"><time>${esc(e.t)}</time><span class="gl-t"><svg class="i" viewBox="0 0 24 24">${ICON[e.kind]}</svg></span><span><b>${esc(e.title)}</b><span class="d">${esc(e.sub)}${e.product ? ' · ' + esc(e.product) : ''}</span></span></button>`;
70
71
  }
71
72
  const ALL = () => M.today.ahead.concat(M.today.earlier);
72
73
  function drawChips() {
73
- $('#srcChips').innerHTML = Object.entries(SRC).map(([k, n]) => `<button class="fchip" data-k="${k}" aria-pressed="${on.has(k)}">${n}<em>${M.today.counts[k] || 0}</em></button>`).join('');
74
+ $('#srcChips').innerHTML = Object.entries(SRC).filter(([k]) => k !== 'digest' || M.today.counts.digest).map(([k, n]) => `<button class="fchip" data-k="${k}" aria-pressed="${on.has(k)}">${n}<em>${M.today.counts[k] || 0}</em></button>`).join('');
74
75
  }
75
76
  function drawTl() {
76
77
  const T = M.today, all = ALL(), ahead = T.ahead.filter((e) => on.has(e.kind)), early = T.earlier.filter((e) => on.has(e.kind));
@@ -126,6 +127,7 @@ function cardHtml(r) {
126
127
  return `${head}<p class="ct">Return this run: why</p><div class="seg2" id="retWhy">${RET_REASONS.map((x) => `<button data-ret="${esc(x)}" aria-pressed="${RET[r.key] === x}">${esc(x)}</button>`).join('')}</div>${err}<div class="cact"><button class="cta" id="recBtn" data-rec="return" disabled>${CHECK}<span>${esc(d.action)}</span></button></div></div>`;
127
128
  }
128
129
  if (d.opts.length) return `${head}${opts}${d.nodef ? `<p class="nodef">${esc(d.nodef)}</p>` : ''}<div class="cact" id="cardAct"><button class="cta" disabled title="${esc(d.hold || 'Nothing here can be recorded yet.')}">${o ? esc(d.prim[o.k]) : 'Your call: choose one'}</button></div>${d.hold ? holdNote(d.hold) : ''}</div>`;
130
+ if (d.link) return `${head}<div class="cact"><a class="cta" id="srcLink" href="${esc(d.link)}" target="_blank" rel="noopener noreferrer">${esc(d.linkLabel || 'Open the source')}</a></div></div>`;
129
131
  return `${head}${d.nodef ? `<p class="nodef">${esc(d.nodef)}</p>` : ''}${d.action ? `<div class="cact"><button class="cta" disabled title="${esc(d.hold || 'Nothing here can be recorded.')}">${CHECK}${esc(d.action)}</button></div>${d.hold ? holdNote(d.hold) : ''}` : ''}</div>`;
130
132
  }
131
133
  function paneHtml(r) {
@@ -180,7 +182,7 @@ $('#dBody').addEventListener('click', (e) => {
180
182
  const w = openR.dialog.write, why = ($('#why') || {}).value || '', key = openR.key;
181
183
  WHY[key] = why;
182
184
  if (w.kind === 'decision') write('decide', { key, choice: SEL[key] || openR.dialog.sel, why }, async (res) => { delete WHY[key]; toast('Recorded in pm-decisions.md.'); closeDlg(); await load(); });
183
- else write('return', { run: w.run, reason: RET[key] }, async () => { toast('Returned to the agent. It counts as a correction.'); closeDlg(); await load(); });
185
+ else write('return', { run: w.run, reason: RET[key], ...(w.ws ? { ws: w.ws } : {}) }, async () => { toast('Returned to the agent. It counts as a correction.'); closeDlg(); await load(); });
184
186
  }
185
187
  });
186
188
  $('#dBody').addEventListener('input', (e) => { if (e.target.id === 'why' && openR) { WHY[openR.key] = e.target.value; ctaState(openR); } });
@@ -197,6 +199,50 @@ function openEvent(e) {
197
199
  ['pointerleave', 'focusout'].forEach((ev) => document.addEventListener(ev, (e) => { const t = e.target.closest && e.target.closest('[data-link]'); if (t) lk(t, false); }, true));
198
200
  }
199
201
 
202
+
203
+ /* ── the products the Briefing reads (initiative briefing-premium-products) ──────────────────────────────────────────────────────────── */
204
+ const wsParam = () => new URLSearchParams(location.search).get('ws');
205
+ function drawPick() {
206
+ const w = $('#pkw'), P = M.products, one = wsParam();
207
+ if (!w) return;
208
+ if (one) { w.hidden = false; $('#pick').hidden = true; $('#pickMenu').hidden = true; let a = $('#pickAll'); if (!a) { a = document.createElement('a'); a.id = 'pickAll'; a.className = 'fchip'; a.href = '/premium.html'; a.textContent = 'All picked products'; w.appendChild(a); } return; }
209
+ if ($('#pickAll')) $('#pickAll').remove();
210
+ $('#pick').hidden = false;
211
+ if (!P) { w.hidden = true; return; }
212
+ w.hidden = false;
213
+ const n = P.available.filter((x) => x.on).length;
214
+ $('#pickN').textContent = n + ' of ' + P.available.length;
215
+ $('#pickMenu').innerHTML = P.available.length
216
+ ? P.available.map((x) => `<button role="menuitemcheckbox" aria-checked="${x.on}" data-ws="${esc(x.ws)}"><i class="bx"></i><span>${esc(x.name)}</span>${x.home ? '<em>home</em>' : ''}</button>`).join('') + '<button role="menuitem" data-reset="1" class="pkreset"><span>Back to the default</span><em>all but home</em></button>'
217
+ : '<p class="note pkempty">No product workspace is registered.</p>';
218
+ }
219
+ function closePick() { $('#pickMenu').hidden = true; $('#pick').setAttribute('aria-expanded', 'false'); }
220
+ $('#pick').addEventListener('click', (e) => {
221
+ e.stopPropagation();
222
+ const m = $('#pickMenu'), open = m.hidden;
223
+ m.hidden = !open; $('#pick').setAttribute('aria-expanded', open);
224
+ if (open) { const f = $('button', m); if (f) f.focus(); }
225
+ });
226
+ document.addEventListener('click', (e) => { if (!e.target.closest('#pkw')) closePick(); });
227
+ $('#pkw').addEventListener('keydown', (e) => {
228
+ if (e.key === 'Escape') { closePick(); $('#pick').focus(); return; }
229
+ if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp') return;
230
+ const L = $$('#pickMenu button'), i = L.indexOf(document.activeElement); if (i < 0) return;
231
+ e.preventDefault(); L[(i + (e.key === 'ArrowDown' ? 1 : L.length - 1)) % L.length].focus();
232
+ });
233
+ $('#pickMenu').addEventListener('click', async (e) => {
234
+ const b = e.target.closest('button'); if (!b || !M || !M.products) return;
235
+ const on = M.products.available.filter((x) => x.on).map((x) => x.ws);
236
+ let body;
237
+ if (b.dataset.reset) body = { key: 'ws.briefing.include', reset: true };
238
+ else { const ws = b.dataset.ws, next = on.includes(ws) ? on.filter((x) => x !== ws) : on.concat(ws); body = { key: 'ws.briefing.include', value: next }; }
239
+ const res = await pmPost('/api/settings', body);
240
+ if (!res || res.ok !== true) { toast((res && res.error) || 'The choice was not saved.'); return; }
241
+ toast(b.dataset.reset ? 'Back to the default: every product except the home one.' : 'Saved. The Briefing now reads the products you ticked.');
242
+ await load();
243
+ const again = b.dataset.ws ? $(`#pickMenu [data-ws="${CSS.escape(b.dataset.ws)}"]`) : null; if (again) again.focus();
244
+ });
245
+
200
246
  /* ── load ───────────────────────────────────────────────────────────────────────────────────────────────── */
201
247
  let readTimer = null;
202
248
  const ago = (ms) => { const m = Math.round(ms / 60000); return m < 1 ? 'just now' : m < 90 ? m + ' min ago' : Math.round(m / 60) + ' h ago'; };
@@ -207,7 +253,7 @@ async function load() {
207
253
  clearInterval(readTimer);
208
254
  if (r && r.state === 'reading') {
209
255
  // the first read has no end I can promise: say how long it has run, and ask again
210
- const t0 = Date.now() - (r.elapsed_ms || 0), tick = () => { $('#brief').textContent = 'Reading the repository for the first time. ' + Math.round((Date.now() - t0) / 1000) + ' s so far. This page shows nothing until it has read, rather than guess.'; };
256
+ const t0 = Date.now() - (r.elapsed_ms || 0), tick = () => { $('#brief').textContent = 'Reading the products for the first time. ' + Math.round((Date.now() - t0) / 1000) + ' s so far. This page shows nothing until it has read, rather than guess.'; };
211
257
  tick(); readTimer = setInterval(tick, 1000); setTimeout(load, 3000);
212
258
  return;
213
259
  }
@@ -216,7 +262,7 @@ async function load() {
216
262
  $('#brief').textContent = 'The Briefing could not read the repository, so it shows nothing rather than guess.' + (r && r.error ? ' The reason: ' + r.error : status ? ' (the server answered ' + status + ')' : ' The server did not answer.');
217
263
  return;
218
264
  }
219
- M = r.model; drawHero(); drawRows(); drawChips(); drawTl(); drawDock(); onScroll();
265
+ M = r.model; drawHero(); drawRows(); drawChips(); drawTl(); drawDock(); drawPick(); onScroll();
220
266
  const age = Date.now() - r.as_of;
221
267
  if (age > 5 * 60000) $('#snap').textContent += ' · read ' + ago(age) + (r.refreshing ? ', refreshing' : '');
222
268
  if (r.error) $('#snap').textContent += ' · the latest read failed: ' + r.error.slice(0, 120);
@@ -35,6 +35,7 @@
35
35
  <header class="top rise" style="--d:0">
36
36
  <h1 class="vh">Briefing</h1><span class="glass name" aria-hidden="true">Briefing</span>
37
37
  <div class="tools tools-top"><span class="snap" id="snap">Reading the repository</span>
38
+ <span class="pkw" id="pkw" hidden><button class="fchip" id="pick" aria-haspopup="menu" aria-expanded="false" aria-controls="pickMenu">Products<em id="pickN"></em></button><div class="crmenu pkmenu" id="pickMenu" role="menu" hidden></div></span>
38
39
  <button class="glass round" id="theme" aria-label="Switch light or dark"><svg class="i" viewBox="0 0 24 24"><path d="M20 14.5A8 8 0 0 1 9.5 4 8 8 0 1 0 20 14.5z"/></svg></button></div>
39
40
  </header>
40
41
  <section class="hero">
@@ -9360,7 +9360,7 @@ async function handleRequest(req, res) {
9360
9360
  // pm-os-absorb Runs 2–5 — the planning layer's routes, one file per run (lib/route-modules.js). Last, so none shadows a route above.
9361
9361
  // meeting-notes Run 1: the Notes routes keep notes in the per-user runtime directory and start the agent-summary session.
9362
9362
  if (await routeModules.dispatch(req, res, url, { json, readBody, getWs, invalidateNeedsAgg, boardCache, consoleState, stateDirFor, sessionList, initiativeIndex,
9363
- runtimeDir: STATE, spawnSession, jevModelOpts, sendToSession, endSupersededRuns, proSettings,
9363
+ runtimeDir: STATE, homeRoot: HOME_ENABLED ? SELF_ROOT : null, spawnSession, jevModelOpts, sendToSession, endSupersededRuns, proSettings,
9364
9364
  // report-workspace: the schedule store a recurring report registers its draft-only schedule in, a skill's body, and the stop of a headless run
9365
9365
  scheduleStore: { read: readSchedules, write: writeSchedules },
9366
9366
  // report-structure-gaps: Draft it now on a report with a saved structure starts the draft session the schedule would, and the row reads its state
@@ -41,6 +41,8 @@
41
41
  "briefing-premium-work.test.js",
42
42
  "briefing-premium-board.test.js",
43
43
  "briefing-premium-initiative.test.js",
44
+ "briefing-premium-products.test.js",
45
+ "briefing-premium-sources.test.js",
44
46
  "calendar-events.test.js",
45
47
  "calendar-supersede.test.js",
46
48
  "tracker-read.test.js",
@@ -200,6 +202,8 @@
200
202
  "briefing-premium-work.test.js": 1,
201
203
  "briefing-premium-board.test.js": 1,
202
204
  "briefing-premium-initiative.test.js": 1,
205
+ "briefing-premium-products.test.js": 1,
206
+ "briefing-premium-sources.test.js": 1,
203
207
  "calendar-events.test.js": 1,
204
208
  "calendar-supersede.test.js": 1,
205
209
  "tracker-read.test.js": 1,
@@ -5,6 +5,8 @@
5
5
  "briefing-premium-work-test.js",
6
6
  "briefing-premium-board-test.js",
7
7
  "briefing-premium-initiative-test.js",
8
+ "briefing-premium-products-test.js",
9
+ "briefing-premium-sources-test.js",
8
10
  "briefing-premium-agent-server.test.js"
9
11
  ],
10
12
  "seconds": {
@@ -12,6 +14,8 @@
12
14
  "briefing-premium-work-test.js": 80,
13
15
  "briefing-premium-board-test.js": 120,
14
16
  "briefing-premium-initiative-test.js": 120,
17
+ "briefing-premium-products-test.js": 90,
18
+ "briefing-premium-sources-test.js": 90,
15
19
  "briefing-premium-agent-server.test.js": 40
16
20
  }
17
21
  }