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.
- package/package.json +1 -1
- package/payload/KIT-VERSION +2 -2
- 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/payload/state/log.md +1 -1
- package/workspace/briefing-premium-products-test.js +164 -0
- package/workspace/briefing-premium-sources-test.js +153 -0
- package/workspace/guide/briefing-premium.md +29 -2
- package/workspace/lib/briefing-premium-agent.js +1 -1
- package/workspace/lib/briefing-premium-products.js +121 -0
- package/workspace/lib/briefing-premium-sources.js +140 -0
- package/workspace/lib/briefing-premium.js +97 -20
- package/workspace/lib/routes-briefing-premium.js +39 -5
- package/workspace/lib/settings-store.js +3 -0
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/premium.css +15 -0
- package/workspace/public/pm/premium.js +53 -7
- package/workspace/public/premium.html +1 -0
- package/workspace/server.js +1 -1
- package/workspace/test-files.json +4 -0
- package/workspace/test-lists/briefing-premium.json +4 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* lib/briefing-premium-sources.js — mail, Jira and Confluence on the premium Briefing (initiative briefing-premium-sources, S1 to S9).
|
|
3
|
+
*
|
|
4
|
+
* build({ brief, myWork, itemLines, today, products }) → { rows, digests, state, notRead } PURE.
|
|
5
|
+
* readSources(ctx, now) → { brief, myWork, itemLines, today } what the route reads.
|
|
6
|
+
*
|
|
7
|
+
* brief = { state: 'ok' | 'absent' | 'stale' | 'bad', brief? }: today's morning brief (lib/brief.js, <state>/briefs/<date>.json).
|
|
8
|
+
* products = (initiative id) → { ws, name } | null: the included products' initiatives. A row names a product only through it.
|
|
9
|
+
*
|
|
10
|
+
* THE RULE: a sentence, chip, name, count or link is made only from a field the brief or the ticket holds. A field it does not hold is left out.
|
|
11
|
+
* A row has no write: nothing here sends, replies, posts or records. PURE except readSources. */
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const Real = require('./briefing-real.js');
|
|
15
|
+
const { localDate } = require('./brief.js');
|
|
16
|
+
|
|
17
|
+
const SOURCE_WORD = { mail: 'Mail', jira: 'Jira', confluence: 'Confluence', slack: 'Slack' };
|
|
18
|
+
const ROW_SOURCES = ['mail', 'jira', 'confluence'];
|
|
19
|
+
const VERB_WORD = { reply: 'Reply', review: 'Review', decide: 'Decide', 'sign-off': 'Sign off', read: 'Read', do: 'Do' };
|
|
20
|
+
const EFFORT_WORD = { minutes: 'minutes', 'an-hour': 'an hour', 'half-day': 'half a day', day: 'a day' };
|
|
21
|
+
const ACROSS = 'Across products';
|
|
22
|
+
const clip = (s, n) => { s = String(s == null ? '' : s).replace(/\s+/g, ' ').trim(); return s.length > n ? s.slice(0, n - 1).trimEnd() + '…' : s; };
|
|
23
|
+
const text = (v, n) => (typeof v === 'string' && v.trim() ? clip(v, n) : '');
|
|
24
|
+
const https = (v) => { const s = String(v || ''); try { return s.length <= 500 && new URL(s).protocol === 'https:' ? s : null; } catch { return null; } };
|
|
25
|
+
const list = (v) => (Array.isArray(v) ? v : []);
|
|
26
|
+
const cap = (s) => s.charAt(0).toUpperCase() + s.slice(1);
|
|
27
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
28
|
+
const hhmm = (ms) => { const d = new Date(ms); return pad(d.getHours()) + ':' + pad(d.getMinutes()); };
|
|
29
|
+
|
|
30
|
+
/** The items that become rows: a mail, Jira or Confluence item the PM owns, or that mentions the PM, or that is worth asking. */
|
|
31
|
+
function rowItems(brief) {
|
|
32
|
+
const keep = (x) => x && typeof x === 'object' && x.id && text(x.title, 140) && ROW_SOURCES.includes(x.source);
|
|
33
|
+
const asks = list(brief.worth_asking).filter((x) => keep(x));
|
|
34
|
+
const mine = list(brief.priorities).filter((x) => keep(x) && (x.owner === 'pm' || x.mention === true));
|
|
35
|
+
return [...mine.map((x) => ({ x, ask: false })), ...asks.map((x) => ({ x, ask: true }))];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function itemRow(x, ask, brief, products) {
|
|
39
|
+
const src = SOURCE_WORD[x.source];
|
|
40
|
+
const verb = VERB_WORD[x.verb] || '';
|
|
41
|
+
const todo = text(x.todo, 200);
|
|
42
|
+
const hit = x.initiative_id && products ? products(x.initiative_id) : null;
|
|
43
|
+
const url = https(x.link);
|
|
44
|
+
const facts = [['Source', src], ['From', text(x.who, 120)], ['Where', text(x.where, 120)], ['About', text(x.about, 480)], ['Asked', text(x.ask, 240)], ['Quote', text(x.quote, 280)], ['Evidence', text(x.evidence, 240)],
|
|
45
|
+
['Confidence', typeof x.confidence === 'number' ? Math.round(x.confidence * 100) + '%' + (ask ? ', below the bar: ask before acting' : '') : ''], ['Effort', EFFORT_WORD[x.effort] || ''],
|
|
46
|
+
['Reply needed', typeof x.reply_needed === 'boolean' ? (x.reply_needed ? 'Yes' : 'No') : ''], ['Initiative', hit ? hit.name + ' · ' + x.initiative_id : ''], ['Due', text(x.due, 40)]].filter((f) => f[1]);
|
|
47
|
+
const why = text(x.why_now, 240);
|
|
48
|
+
return {
|
|
49
|
+
kind: 'source', key: 'src:' + x.id, title: text(x.title, 140),
|
|
50
|
+
subject: [src, text(x.who, 120), x.mention ? 'mentions you' : '', ask ? 'worth asking' : ''].filter(Boolean).join(' · '),
|
|
51
|
+
prepared: verb && todo ? verb + ': ' + todo : todo || verb || 'No prepared line is on record.',
|
|
52
|
+
skip: why ? 'Why now: ' + why : 'No reason is on record.',
|
|
53
|
+
product: hit ? hit.name : ACROSS, from: ['brief:' + x.id], detail: { source: x.source },
|
|
54
|
+
dialog: {
|
|
55
|
+
eye: src, logTitle: 'Read from the morning brief', title: text(x.title, 140), sub: [src, text(x.who, 120)].filter(Boolean).join(' · '),
|
|
56
|
+
log: ['Read the morning brief of ' + brief.date, 'Found this ' + src + ' item', hit ? 'It names the initiative ' + x.initiative_id + ' of ' + hit.name : 'It names no initiative of an included product, so it is not tied to a product'],
|
|
57
|
+
say: text(x.about, 480) || why || 'The brief holds no more on this item.',
|
|
58
|
+
head: 'The item', facts, opts: [], sel: null, nodef: '', link: url, linkLabel: 'Open in ' + src,
|
|
59
|
+
pane: { label: src, file: 'briefs/' + brief.date + '.json', h: text(x.title, 140), intro: why, rows: facts, paras: [], parasTitle: '' },
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function ticketRow(n, today) {
|
|
65
|
+
const url = https(n.url);
|
|
66
|
+
const label = { clock: 'Due', alert: 'Blocked', reply: 'Asked', inbox: 'Status' };
|
|
67
|
+
const fact = (f) => { const t = String(f.text || ''); return [label[f.icon] || '', f.icon === 'clock' ? t.replace(/^Due /, '') : f.icon === 'inbox' ? t.split(' · ').slice(1).join(' · ') : t]; };
|
|
68
|
+
const facts = [['Source', 'Jira'], ['Ticket', n.src || ''], ...list(n.facts).map(fact)].filter((f) => f[0] && f[1]);
|
|
69
|
+
return {
|
|
70
|
+
kind: 'source', key: 'src:' + n.itemId, title: text(n.headline, 140),
|
|
71
|
+
subject: ['Jira', n.src, 'assigned to you'].filter(Boolean).join(' · '),
|
|
72
|
+
prepared: text(n.why, 200) || 'No prepared line is on record.',
|
|
73
|
+
skip: n.dueText ? cap(n.dueText) + '.' : 'No due date is on record.',
|
|
74
|
+
product: ACROSS, from: ['jira:' + n.itemId], detail: { source: 'jira' },
|
|
75
|
+
dialog: {
|
|
76
|
+
eye: 'Jira', logTitle: 'Read from your Jira tickets', title: text(n.headline, 140), sub: 'Jira · ' + (n.src || ''),
|
|
77
|
+
log: ['Read the tickets assigned to you', 'Checked your Work list for a done or put-off copy', 'Found none, so it stays'],
|
|
78
|
+
say: text(n.why, 200), head: 'The ticket', facts, opts: [], sel: null, nodef: '', link: url, linkLabel: 'Open ' + (n.src || 'the ticket') + ' in Jira',
|
|
79
|
+
pane: { label: 'Jira', file: 'Jira · assigned to you', h: text(n.headline, 140), intro: text(n.why, 200), rows: facts, paras: [], parasTitle: '' },
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function digestItem(d, brief) {
|
|
85
|
+
const at = Date.parse(brief.generated_at);
|
|
86
|
+
if (!Number.isFinite(at) || !SOURCE_WORD[d.source] || !text(d.name, 120)) return null;
|
|
87
|
+
const dec = list(d.decisions).filter((s) => text(s, 140)), open = list(d.open).filter((s) => text(s, 140)), goals = list(d.goals).filter((s) => text(s, 140));
|
|
88
|
+
const src = SOURCE_WORD[d.source];
|
|
89
|
+
const sub = [dec.length ? dec.length + (dec.length === 1 ? ' decision' : ' decisions') : '', open.length ? open.length + ' open' : '', goals.length ? goals.length + (goals.length === 1 ? ' goal' : ' goals') : ''].filter(Boolean).join(' · ') || 'No lines on record';
|
|
90
|
+
const facts = [['Source', src], ['Name', text(d.name, 120)], ['Decisions', String(dec.length)], ['Open', String(open.length)], ['Goals', goals.length ? String(goals.length) : '']].filter((f) => f[1]);
|
|
91
|
+
return {
|
|
92
|
+
at, t: hhmm(at), kind: 'digest', title: src + ' · ' + text(d.name, 120), sub, src: 'Digests',
|
|
93
|
+
dialog: {
|
|
94
|
+
eye: 'Digest', logTitle: 'Read from the morning brief', title: src + ' · ' + text(d.name, 120), sub: sub,
|
|
95
|
+
log: ['Read the morning brief of ' + brief.date, 'Found the ' + src + ' digest of ' + text(d.name, 120)],
|
|
96
|
+
say: 'This is what the brief holds for ' + text(d.name, 120) + '. It is a summary, not a copy of the source.',
|
|
97
|
+
head: 'The digest', facts, opts: [], sel: null, nodef: '', link: https(d.link), linkLabel: 'Open in ' + src,
|
|
98
|
+
pane: { label: 'Digest', file: 'briefs/' + brief.date + '.json', h: src + ' · ' + text(d.name, 120), intro: '', rows: facts, parasTitle: 'What the digest holds',
|
|
99
|
+
paras: [...dec.map((s) => 'Decision: ' + text(s, 140)), ...open.map((s) => 'Open: ' + text(s, 140)), ...goals.map((s) => 'Goal: ' + text(s, 140))] },
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The sources the brief says it did not read, named for the header: only the four that rows and digests come from. */
|
|
105
|
+
function notReadOf(brief) {
|
|
106
|
+
const st = brief.sources && typeof brief.sources === 'object' ? brief.sources : {};
|
|
107
|
+
return ['mail', 'jira', 'confluence', 'slack'].filter((k) => st[k] === 'not-reached' || st[k] === 'off')
|
|
108
|
+
.map((k) => SOURCE_WORD[k].toLowerCase() + (st[k] === 'off' ? ' (switched off)' : ' (the brief says it was not reached)'));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function build({ brief, myWork, itemLines, today, products } = {}) {
|
|
112
|
+
const out = { rows: [], digests: [], state: 'absent', notRead: [], from: [] };
|
|
113
|
+
const b = brief && brief.state === 'ok' && brief.brief && brief.brief.date === today ? brief.brief : null;
|
|
114
|
+
out.state = b ? 'ok' : brief && brief.state === 'ok' ? 'stale' : (brief && brief.state) || 'absent';
|
|
115
|
+
const tickets = (Real.build({ brief: null, myWork, itemLines, today }).needs || []).filter((n) => n.origin === 'jira' && n.itemId)
|
|
116
|
+
.sort((a, c) => a._rank - c._rank || (c._late || 0) - (a._late || 0));
|
|
117
|
+
out.rows.push(...tickets.map((n) => ticketRow(n, today)));
|
|
118
|
+
if (b) {
|
|
119
|
+
out.rows.push(...rowItems(b).map(({ x, ask }) => itemRow(x, ask, b, products)));
|
|
120
|
+
out.digests = list(b.digests).map((d) => digestItem(d, b)).filter(Boolean);
|
|
121
|
+
out.notRead = notReadOf(b);
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** What the route reads each request. A source that fails gives nothing. */
|
|
127
|
+
async function readSources(ctx, now) {
|
|
128
|
+
const today = localDate(now);
|
|
129
|
+
const dir = ctx && ctx.runtimeDir;
|
|
130
|
+
const out = { brief: { state: 'absent' }, myWork: [], itemLines: [], today };
|
|
131
|
+
if (!dir) return out;
|
|
132
|
+
let raw = null;
|
|
133
|
+
try { raw = fs.readFileSync(path.join(dir, 'briefs', today + '.json'), 'utf8'); } catch { /* no brief for today */ }
|
|
134
|
+
if (raw !== null) { try { const j = JSON.parse(raw); out.brief = j && typeof j === 'object' && !Array.isArray(j) ? { state: 'ok', brief: j } : { state: 'bad' }; } catch { out.brief = { state: 'bad' }; } }
|
|
135
|
+
const real = await Real.readSources(ctx, new Date(now), today).catch(() => null);
|
|
136
|
+
if (real) { out.myWork = real.myWork; out.itemLines = real.itemLines; }
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = { build, readSources, ACROSS, SOURCE_WORD };
|
|
@@ -21,6 +21,7 @@ const fs = require('fs');
|
|
|
21
21
|
const path = require('path');
|
|
22
22
|
const { execFile } = require('child_process');
|
|
23
23
|
const W = require('./briefing-premium-writes.js');
|
|
24
|
+
const Src = require('./briefing-premium-sources.js');
|
|
24
25
|
|
|
25
26
|
const DAY = 86400000;
|
|
26
27
|
const MAX_ROWS = 7;
|
|
@@ -35,6 +36,7 @@ const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July',
|
|
|
35
36
|
const WEEKDAYS = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
|
36
37
|
const dateLine = (ms) => { const d = new Date(ms); return WEEKDAYS[d.getDay()] + ' ' + d.getDate() + ' ' + MONTHS[d.getMonth()]; };
|
|
37
38
|
/* A short name for a sentence: the title up to its first dash, colon or bracket, without a leading article. */
|
|
39
|
+
const joinNames = (a) => (a.length < 2 ? a.join('') : a.slice(0, -1).join(', ') + ' and ' + a[a.length - 1]);
|
|
38
40
|
const shortName = (t) => clip(String(t || '').split(/\s[—–-]\s|:|\(/)[0].replace(/^(the|a|an)\s+/i, ''), 48);
|
|
39
41
|
const MON = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
40
42
|
const shortDate = (ms) => { const d = new Date(ms); return d.getDate() + ' ' + MON[d.getMonth()]; };
|
|
@@ -75,6 +77,8 @@ const START_HOLD = 'Starting an agent from here arrives with the live conversati
|
|
|
75
77
|
const tryD = (fn) => { try { return fn(); } catch { return null; } };
|
|
76
78
|
const skipped = { n: 0 };
|
|
77
79
|
|
|
80
|
+
/** Two products can hold an initiative of the same name, so a lookup by id also names the workspace (none in a one-workspace read). */
|
|
81
|
+
const idk = (x, id) => (x.ws || '') + '|' + id;
|
|
78
82
|
const live = (i) => i && !i.accepted && !i.stopped && !i.legacy;
|
|
79
83
|
|
|
80
84
|
/* ── rows ─────────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
@@ -206,15 +210,23 @@ function waitedRows(f) {
|
|
|
206
210
|
}];
|
|
207
211
|
}
|
|
208
212
|
|
|
209
|
-
|
|
210
|
-
|
|
213
|
+
/** A row of one product in a merged list: its key names the workspace (kind:id:ws) so two products never share a key, and it carries the product's name. */
|
|
214
|
+
const tagRow = (r, p) => Object.assign({}, r, { key: r.key + ':' + p.ws, product: p.name, ws: p.ws });
|
|
215
|
+
/** Every row of one set of facts, in the order the rules give, with those the PM put off split out. Not yet cut to the page's cap. */
|
|
216
|
+
function allRowsOf(f) {
|
|
217
|
+
let every = [].concat(blockedRows(f), decisionRows(f), waitedRows(f));
|
|
218
|
+
if (f.product) every = every.map((r) => tagRow(r, f.product));
|
|
211
219
|
const off = (r) => (f.defers && f.defers[r.key] > f.now); // put off by the PM until a time that has not come
|
|
212
220
|
const all = every.filter((r) => !off(r));
|
|
213
|
-
const putOff = every.filter(off).map((r) => ({ key: r.key, title: r.title, until: f.defers[r.key] }));
|
|
221
|
+
const putOff = every.filter(off).map((r) => ({ key: r.key, title: r.title, until: f.defers[r.key], ...(r.product ? { product: r.product } : {}) }));
|
|
222
|
+
return { all, putOff };
|
|
223
|
+
}
|
|
224
|
+
const cut = (all, putOff) => {
|
|
214
225
|
const rows = all.slice(0, MAX_ROWS).map((r, k) => Object.assign({ n: k + 1, first: k === 0 }, r));
|
|
215
226
|
rows.forEach((r) => delete r.order);
|
|
216
227
|
return { rows, more: Math.max(0, all.length - rows.length), putOff };
|
|
217
|
-
}
|
|
228
|
+
};
|
|
229
|
+
function rowsOf(f) { const a = allRowsOf(f); return cut(a.all, a.putOff); }
|
|
218
230
|
|
|
219
231
|
/* ── hero ─────────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
220
232
|
function correctionRate(runs, from, to) {
|
|
@@ -244,9 +256,10 @@ function heroOf(f, rows) {
|
|
|
244
256
|
const clauses = [];
|
|
245
257
|
if (blocked.length) {
|
|
246
258
|
const ordering = blocked.every((r) => r.stepKey === 'spec' || r.stepKey === 'prototype');
|
|
247
|
-
|
|
259
|
+
const where = [...new Set(blocked.map((r) => r.product).filter(Boolean))];
|
|
260
|
+
clauses.push({ t: num(blocked.length) + ' ' + plural(blocked.length, 'initiative is', 'initiatives are') + ' blocked' + (ordering ? ' on ordering' : '') + (where.length ? ' in ' + joinNames(where) : ''), from: blocked.map((r) => r.from[0]) });
|
|
248
261
|
}
|
|
249
|
-
rest.forEach((r) => clauses.push({ t: r.hero, from: r.from }));
|
|
262
|
+
rest.forEach((r) => clauses.push({ t: r.hero + (r.product ? ' in ' + r.product : ''), from: r.from }));
|
|
250
263
|
if (clauses.length) {
|
|
251
264
|
const p = [['What is stuck is yours: ', 0]];
|
|
252
265
|
clauses.forEach((c, k) => {
|
|
@@ -286,13 +299,13 @@ function mergeDialog(m, f) {
|
|
|
286
299
|
};
|
|
287
300
|
}
|
|
288
301
|
function runDialog(r, t, byId) {
|
|
289
|
-
const i = byId.get(r.initiative_id), ap = r.pre_run_contract && r.pre_run_contract.PM_approved === true;
|
|
302
|
+
const i = byId.get(idk(r, r.initiative_id)), ap = r.pre_run_contract && r.pre_run_contract.PM_approved === true;
|
|
290
303
|
const facts = [['Initiative', (i && i.title) ? clip(i.title, 70) : r.initiative_id], ['Run', r.run_id], ['Lane', r.lane || (i && i.lane)], ['Corrections', Number.isFinite(Number(r.human_corrections)) ? String(r.human_corrections) : ''], ['Contract approved', r.pre_run_contract ? (ap ? 'Yes' : 'No') : ''], ['Recorded', longDate(t) + ' ' + hhmm(t)]].filter((x) => x[1]);
|
|
291
304
|
return {
|
|
292
305
|
eye: 'Agent run', logTitle: 'Read from the run log', title: i ? clip(i.title, 90) : r.initiative_id, sub: hhmm(t) + ' · agent run',
|
|
293
306
|
log: ['Read planning/runs/agent-runs.jsonl', 'Matched the run to its initiative' + (i ? '' : ' (no record found)'), 'Read its recorded fields'],
|
|
294
307
|
say: 'This run is logged. Nothing needs you unless you disagree with it.',
|
|
295
|
-
head: 'The run', facts, opts: [], sel: null, nodef: '', action: 'Return this run to the agent', write: { kind: 'return', run: r.run_id },
|
|
308
|
+
head: 'The run', facts, opts: [], sel: null, nodef: '', action: 'Return this run to the agent', write: { kind: 'return', run: r.run_id, ...(r.ws ? { ws: r.ws } : {}) },
|
|
296
309
|
pane: { label: 'Run record', file: 'planning/runs/agent-runs.jsonl', h: i ? clip(i.title, 90) : r.initiative_id, intro: '', rows: facts, paras: r.note ? [clip(r.note, 600)] : [], parasTitle: 'Its note' },
|
|
297
310
|
};
|
|
298
311
|
}
|
|
@@ -314,13 +327,14 @@ function todayOf(f) {
|
|
|
314
327
|
for (const m of f.meetings || []) {
|
|
315
328
|
items.push({ at: m.start, t: hhmm(m.start), kind: 'meeting', title: m.title, sub: [Math.round((m.end - m.start) / 60000) + ' min', m.with].filter(Boolean).join(' · '), src: 'Calendar', dialog: tryD(() => meetingDialog(m)) });
|
|
316
329
|
}
|
|
317
|
-
for (const m of f.merges || []) if (m.at >= midnight && m.at <= f.now) items.push({ at: m.at, t: hhmm(m.at), kind: 'merge', title: clip(m.title, 110), sub: 'Merged to main', src: 'Merges', dialog: tryD(() => mergeDialog(m, f)) });
|
|
330
|
+
for (const m of f.merges || []) if (m.at >= midnight && m.at <= f.now) items.push({ at: m.at, t: hhmm(m.at), kind: 'merge', ...(m.product ? { product: m.product } : {}), title: clip(m.title, 110), sub: 'Merged to main', src: 'Merges', dialog: tryD(() => mergeDialog(m, f)) });
|
|
318
331
|
const runs = (f.runs || []).filter((r) => { const t = Date.parse(r.created_at); return t >= midnight && t <= f.now; });
|
|
319
|
-
const byId = new Map(f.initiatives.map((i) => [i.id, i]));
|
|
320
|
-
for (const r of runs) { const t = Date.parse(r.created_at); items.push({ at: t, t: hhmm(t), kind: 'run', title: clip((byId.get(r.initiative_id) || {}).title || r.initiative_id, 110), sub: 'Agent run' + (Number.isFinite(Number(r.human_corrections)) ? ' · ' + r.human_corrections + ' ' + plural(Number(r.human_corrections), 'correction', 'corrections') : ''), src: 'Agent runs', dialog: tryD(() => runDialog(r, t, byId)) }); }
|
|
332
|
+
const byId = new Map(f.initiatives.map((i) => [idk(i, i.id), i]));
|
|
333
|
+
for (const r of runs) { const t = Date.parse(r.created_at); items.push({ at: t, t: hhmm(t), kind: 'run', ...(r.product ? { product: r.product } : {}), title: clip((byId.get(idk(r, r.initiative_id)) || {}).title || r.initiative_id, 110), sub: 'Agent run' + (Number.isFinite(Number(r.human_corrections)) ? ' · ' + r.human_corrections + ' ' + plural(Number(r.human_corrections), 'correction', 'corrections') : ''), src: 'Agent runs', dialog: tryD(() => runDialog(r, t, byId)) }); }
|
|
334
|
+
for (const d of f.digests || []) items.push(d);
|
|
321
335
|
const ahead = items.filter((x) => x.at > f.asOf).sort((a, b) => a.at - b.at), earlier = items.filter((x) => x.at <= f.asOf).sort((a, b) => b.at - a.at);
|
|
322
336
|
const count = (k) => items.filter((x) => x.kind === k).length;
|
|
323
|
-
return { ahead, earlier, now: { t: hhmm(f.asOf), label: 'snapshot' }, counts: { meeting: count('meeting'), merge: count('merge'), run: count('run') }, note: ahead.length + earlier.length ? null : 'Nothing is recorded for today yet.' };
|
|
337
|
+
return { ahead, earlier, now: { t: hhmm(f.asOf), label: 'snapshot' }, counts: { meeting: count('meeting'), merge: count('merge'), run: count('run'), digest: count('digest') }, note: ahead.length + earlier.length ? null : 'Nothing is recorded for today yet.' };
|
|
324
338
|
}
|
|
325
339
|
|
|
326
340
|
/* ── the model ────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
@@ -348,6 +362,64 @@ function build(f) {
|
|
|
348
362
|
};
|
|
349
363
|
}
|
|
350
364
|
|
|
365
|
+
|
|
366
|
+
/* ── several products (initiative briefing-premium-products) ──────────────────────────────────────────────── */
|
|
367
|
+
const KIND_RANK = { blocked: 0, decision: 1, waited: 2 };
|
|
368
|
+
const longest = (r) => { const d = (r.detail && r.detail.days) || []; return d.length ? d[d.length - 1] : 0; };
|
|
369
|
+
|
|
370
|
+
/** Several products' facts → ONE model, in the same shape build() makes.
|
|
371
|
+
* g = { now, asOf, meetings (shared, or null), defers { key: until }, available: [{ ws, name, home, on }],
|
|
372
|
+
* parts: [{ product: { ws, name }, f (that product's facts) | null, state: 'ready' | 'reading' | 'error', error? }] }
|
|
373
|
+
* A row belongs to one product (its key ends with the workspace id) and the rows are put in one order: blocked first, then decisions, then reviews.
|
|
374
|
+
* A count is never summed across products: each product gets its own failed-review row. The hero and Today read the merged facts. */
|
|
375
|
+
function buildMany(g) {
|
|
376
|
+
skipped.n = 0;
|
|
377
|
+
const now = g.now, asOf = g.asOf || g.now, meetings = g.meetings === undefined ? null : g.meetings;
|
|
378
|
+
const parts = (g.parts || []).map((p) => ({ product: p.product, state: p.state || 'ready', error: p.error, f: p.f ? Object.assign({ initiatives: [], runs: [], merges: [], evals: [], tripwires: null, missing: [], protoDecided: [] }, p.f, { now, asOf, meetings, defers: g.defers || {}, product: p.product }) : null }));
|
|
379
|
+
const read = parts.filter((p) => p.f && p.state !== 'error');
|
|
380
|
+
const every = [], putOff = [];
|
|
381
|
+
for (const p of read) { const a = allRowsOf(p.f); every.push(...a.all); putOff.push(...a.putOff); }
|
|
382
|
+
every.sort((a, b) => (KIND_RANK[a.kind] - KIND_RANK[b.kind]) || (a.kind === 'blocked' ? (a.order[0] - b.order[0]) || (a.order[1] < b.order[1] ? -1 : a.order[1] > b.order[1] ? 1 : 0) : a.kind === 'waited' ? longest(b) - longest(a) : 0));
|
|
383
|
+
// mail, Jira and Confluence (initiative briefing-premium-sources): their rows follow the repositories' rows, and a row names a product only when exactly one included product holds its initiative
|
|
384
|
+
const owner = new Map();
|
|
385
|
+
for (const p of read) for (const i of p.f.initiatives) owner.set(i.id, owner.has(i.id) ? null : { ws: p.product.ws, name: p.product.name });
|
|
386
|
+
const sx = g.sources ? Src.build(Object.assign({}, g.sources, { products: (id) => owner.get(id) || null })) : null;
|
|
387
|
+
const { rows, more } = cut(every.concat(sx ? sx.rows : []), putOff);
|
|
388
|
+
const tag = (list, p) => list.map((x) => Object.assign({}, x, { ws: p.product.ws, product: p.product.name }));
|
|
389
|
+
const cf = {
|
|
390
|
+
now, asOf, meetings, defers: g.defers || {},
|
|
391
|
+
initiatives: [].concat(...read.map((p) => p.f.initiatives.map((i) => Object.assign({}, i, { ws: p.product.ws })))),
|
|
392
|
+
runs: [].concat(...read.map((p) => tag(p.f.runs, p))),
|
|
393
|
+
merges: [].concat(...read.map((p) => tag(p.f.merges, p))),
|
|
394
|
+
evals: [].concat(...read.map((p) => tag(p.f.evals, p))),
|
|
395
|
+
digests: sx ? sx.digests : [],
|
|
396
|
+
};
|
|
397
|
+
const available = g.available || [], inc = available.filter((a) => a.on), out = available.filter((a) => !a.on);
|
|
398
|
+
const waiting = parts.filter((p) => p.state === 'reading' || !p.f).filter((p) => p.state !== 'error'), failed = parts.filter((p) => p.state === 'error');
|
|
399
|
+
const pending = parts.some((p) => p.state === 'reading' || (p.f && p.f.pending));
|
|
400
|
+
const missing = [];
|
|
401
|
+
if (meetings === null) missing.push('calendar');
|
|
402
|
+
for (const p of read) for (const k of p.f.missing || []) missing.push(k + ' (' + p.product.name + ')');
|
|
403
|
+
for (const p of failed) missing.push(p.product.name);
|
|
404
|
+
const missText = [].concat(meetings === null ? ['the calendar'] : [], ...read.map((p) => (p.f.missing || []).map((k) => (SOURCE_NAME[k] || k) + ' of ' + p.product.name)), failed.map((p) => p.product.name + ' could not be read'));
|
|
405
|
+
const none = !inc.length;
|
|
406
|
+
const hero = none ? [{ parts: [{ t: available.length ? 'No product is included, so there is nothing to read. Pick one under Products.' : 'No product workspace is registered, so there is nothing to read.', s: false }], from: [] }]
|
|
407
|
+
: heroOf(cf, rows);
|
|
408
|
+
const srcRows = rows.filter((r) => r.kind === 'source'), srcSeen = [...new Set(srcRows.map((r) => r.detail.source))];
|
|
409
|
+
if (srcRows.length) hero.push({ parts: [{ t: num(srcRows.length) + ' ' + plural(srcRows.length, 'item', 'items') + ' from ' + joinNames(srcSeen.map((k) => Src.SOURCE_WORD[k])), s: true }, { t: ' ' + plural(srcRows.length, 'needs', 'need') + ' you.', s: false }], from: srcRows.map((r) => r.from[0]) });
|
|
410
|
+
const sxNote = !sx ? '' : sx.state === 'ok' ? (sx.notRead.length ? ' · not read by the morning brief: ' + sx.notRead.join(', ') : '') : sx.state === 'bad' ? ' · the morning brief could not be read' : ' · no morning brief for today';
|
|
411
|
+
return {
|
|
412
|
+
date: dateLine(now),
|
|
413
|
+
pending,
|
|
414
|
+
header: { text: 'Real data · as of ' + hhmm(asOf) + ' · ' + (none ? 'no product included' : joinNames(inc.map((a) => a.name))) + (out.length ? ' · ' + joinNames(out.map((a) => a.name)) + ' not included' : '') + (missText.length ? ' · not read: ' + missText.join(', ') : '') + sxNote + (waiting.length ? ' · reading ' + joinNames(waiting.map((p) => p.product.name)) : '') + (skipped.n ? ' · ' + skipped.n + (skipped.n === 1 ? ' record' : ' records') + ' could not be read and ' + (skipped.n === 1 ? 'was' : 'were') + ' left out' : ''), missing },
|
|
415
|
+
hero: hero.length ? hero : [{ parts: [{ t: pending ? 'Reading the initiative state. What needs you is not known yet.' : 'Nothing needs you and nothing moved today that the records show.', s: false }], from: [] }],
|
|
416
|
+
list: { title: 'What you need to do', rows, more, count: rows.length + more, pendingNote: pending ? 'Blocked work, prototypes waiting for you and failed reviews appear when the initiative state has been read.' : null, putOff: putOff.map((x) => ({ key: x.key, title: x.title, ...(x.product ? { product: x.product } : {}), until: x.until, text: (x.product ? x.product + ': ' : '') + x.title + ' · back ' + backAt(x.until, now) })), order: 'In this order: blocked work first, then decisions only you can make, then what has waited longest.' },
|
|
417
|
+
today: todayOf(cf),
|
|
418
|
+
dock: { needs: pending ? null : rows.length + more, label: plural(rows.length + more, 'needs you', 'need you'), next: rows[0] ? rows[0].title : null },
|
|
419
|
+
products: { available: available.map((a) => ({ ws: a.ws, name: a.name, home: !!a.home, on: !!a.on })), none },
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
351
423
|
/* ── I/O ──────────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
352
424
|
const readJsonl = (file) => { try { return fs.readFileSync(file, 'utf8').split('\n').map((l) => l.trim()).filter(Boolean).map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean); } catch { return null; } };
|
|
353
425
|
const run = (cmd, args, opts) => new Promise((res) => {
|
|
@@ -406,8 +478,18 @@ async function repoKey(root) {
|
|
|
406
478
|
return head.trim() + ':' + require('crypto').createHash('sha1').update(st || '').digest('hex').slice(0, 12);
|
|
407
479
|
}
|
|
408
480
|
|
|
481
|
+
/** Today's calendar rows, from the calendar PMOS holds (one for the whole computer, not one per workspace). null when it cannot be read. */
|
|
482
|
+
function readMeetings(runtimeDir, now) {
|
|
483
|
+
try {
|
|
484
|
+
const CE = require('./calendar-events.js');
|
|
485
|
+
if (!runtimeDir) throw new Error('no runtime dir');
|
|
486
|
+
const all = CE.readAll(runtimeDir, { now });
|
|
487
|
+
return CE.eventsOn(all, dayKey(now)).map((e) => ({ start: Date.parse(e.start), end: Date.parse(e.end), title: e.title, with: e.with || '', initiative: e.initiative || null })).filter((e) => Number.isFinite(e.start));
|
|
488
|
+
} catch { return null; }
|
|
489
|
+
}
|
|
490
|
+
|
|
409
491
|
/** The quick read: runs, reviews, merges (with their files), the calendar and the kill conditions. No initiative state. */
|
|
410
|
-
async function readFast({ root, now, runtimeDir }) {
|
|
492
|
+
async function readFast({ root, now, runtimeDir, calendar = true }) {
|
|
411
493
|
const f = { now: now || Date.now(), asOf: now || Date.now(), initiatives: [], runs: [], merges: [], evals: [], meetings: null, tripwires: null, missing: [] };
|
|
412
494
|
const runs = readJsonl(path.join(root, 'planning', 'runs', 'agent-runs.jsonl')); if (runs) f.runs = runs; else f.missing.push('runs');
|
|
413
495
|
const evals = readJsonl(path.join(root, 'planning', 'runs', 'evals.jsonl')); if (evals) f.evals = evals; else f.missing.push('evals');
|
|
@@ -423,12 +505,7 @@ async function readFast({ root, now, runtimeDir }) {
|
|
|
423
505
|
m.files = nums.length; m.ins = ins; m.del = del; m.dirs = Object.entries(top).sort((a, b) => b[1] - a[1]).slice(0, 3).map((e) => e[0]);
|
|
424
506
|
m.body = lines.filter((x) => !/^(\d+|-)\t(\d+|-)\t/.test(x)).join('\n').trim();
|
|
425
507
|
}
|
|
426
|
-
|
|
427
|
-
const CE = require('./calendar-events.js');
|
|
428
|
-
if (!runtimeDir) throw new Error('no runtime dir');
|
|
429
|
-
const all = CE.readAll(runtimeDir, { now: f.now });
|
|
430
|
-
f.meetings = CE.eventsOn(all, dayKey(f.now)).map((e) => ({ start: Date.parse(e.start), end: Date.parse(e.end), title: e.title, with: e.with || '', initiative: e.initiative || null })).filter((e) => Number.isFinite(e.start));
|
|
431
|
-
} catch { f.meetings = null; }
|
|
508
|
+
if (calendar) f.meetings = readMeetings(runtimeDir, f.now);
|
|
432
509
|
f.tripwires = readTripwires(root);
|
|
433
510
|
f.protoDecided = [...W.decidedPrototypes(root)];
|
|
434
511
|
f.defers = Object.fromEntries(W.readDefers(runtimeDir, f.now));
|
|
@@ -511,4 +588,4 @@ const peekModel = (root) => (kept && kept.root === root ? kept.model : partial &
|
|
|
511
588
|
const settle = () => inflight || Promise.resolve();
|
|
512
589
|
const forget = () => { kept = null; partial = null; inflight = null; lastError = null; startedAt = 0; };
|
|
513
590
|
|
|
514
|
-
module.exports = { build, hooks, refresh, readSources, readFast, readInitiatives, repoKey, readFm, readTripwires, modelOf, settle, forget, rebuild, peekModel, rowsOf, heroOf, todayOf, failedReviews, num };
|
|
591
|
+
module.exports = { build, buildMany, allRowsOf, readMeetings, hooks, refresh, readSources, readFast, readInitiatives, repoKey, readFm, readTripwires, modelOf, settle, forget, rebuild, peekModel, rowsOf, heroOf, todayOf, failedReviews, num };
|
|
@@ -20,6 +20,24 @@ const { repoScript } = require('./repo-script.js');
|
|
|
20
20
|
const { correctionEvent } = require(repoScript('card-state.js'));
|
|
21
21
|
const { appendOutbox } = require(repoScript('outbox.js'));
|
|
22
22
|
|
|
23
|
+
const Prod = require('./briefing-premium-products.js');
|
|
24
|
+
const Src = require('./briefing-premium-sources.js');
|
|
25
|
+
const settings = require('./settings-store.js');
|
|
26
|
+
|
|
27
|
+
/** The workspaces the Briefing may read right now (initiative briefing-premium-products): choose()'s list, from the server's workspace list and the PM's setting. */
|
|
28
|
+
function products(ctx, only) {
|
|
29
|
+
const home = ctx.homeRoot || ctx.selfRoot || null;
|
|
30
|
+
const list = (typeof ctx.workspaceList === 'function' ? ctx.workspaceList() : []).map((w) => ({ id: w.id, name: w.label || w.name, root: w.root, home: !!home && path.resolve(w.root) === path.resolve(home) }));
|
|
31
|
+
return Prod.choose({ workspaces: list, include: settings.get(ctx.runtimeDir, 'ws.briefing.include', null), only });
|
|
32
|
+
}
|
|
33
|
+
/** The included product a merged row (or a returned run) belongs to: { root, ws } | { error }. null when the key is the one-workspace page's. */
|
|
34
|
+
function ownerOf(ctx, ws) {
|
|
35
|
+
const a = products(ctx).available.find((x) => x.ws === ws);
|
|
36
|
+
if (!a) return { error: 'that product is not registered' };
|
|
37
|
+
if (!a.on) return { error: a.name + ' is not included in the Briefing' };
|
|
38
|
+
return { root: a.root, ws: a.ws };
|
|
39
|
+
}
|
|
40
|
+
|
|
23
41
|
const enginePath = () => ENGINE_PATHS.find((p) => fs.existsSync(p)) || null;
|
|
24
42
|
|
|
25
43
|
const POSTS = ['decide', 'put-off', 'return', 'ask'];
|
|
@@ -37,21 +55,29 @@ async function post(req, res, url, ctx) {
|
|
|
37
55
|
const b = got.body;
|
|
38
56
|
const g = guard.requirePagePress(req, b);
|
|
39
57
|
if (!g.ok) { ctx.json(res, { ok: false, error: g.error }, g.status); return true; }
|
|
40
|
-
const
|
|
58
|
+
const runtimeDir = ctx.runtimeDir;
|
|
59
|
+
// A merged row's key ends with its workspace (kind:id:ws); the one-workspace page's key does not and keeps the old path.
|
|
60
|
+
const sk = Prod.splitKey(b.key);
|
|
61
|
+
let root = rootOf(url, ctx), merged = false;
|
|
62
|
+
if (sk || (act === 'return' && b.ws)) {
|
|
63
|
+
const o = ownerOf(ctx, sk ? sk.ws : String(b.ws));
|
|
64
|
+
if (o.error) { ctx.json(res, { ok: false, error: o.error }, 400); return true; }
|
|
65
|
+
root = o.root; merged = true;
|
|
66
|
+
}
|
|
41
67
|
if (act === 'ask') {
|
|
42
|
-
let wsId; try { const w = ctx.getWs && ctx.getWs(url.searchParams.get('ws') || null); wsId = w && w.id; } catch { wsId = undefined; }
|
|
43
|
-
const a = Ag.ask({ runtimeDir, key: b.key, text: b.text, model: /^work:/.test(String(b.key || '')) ? WP.peekModel() : /^board:/.test(String(b.key || '')) ? BoardP.peekModel() : /^(init|doc):/.test(String(b.key || '')) ? InitP.peekModel() : BP.peekModel(root), spawnSession: ctx.spawnSession, wsId });
|
|
68
|
+
let wsId; try { const w = merged && sk ? { id: sk.ws } : ctx.getWs && ctx.getWs(url.searchParams.get('ws') || null); wsId = w && w.id; } catch { wsId = undefined; }
|
|
69
|
+
const a = Ag.ask({ runtimeDir, key: b.key, text: b.text, model: /^work:/.test(String(b.key || '')) ? WP.peekModel() : /^board:/.test(String(b.key || '')) ? BoardP.peekModel() : /^(init|doc):/.test(String(b.key || '')) ? InitP.peekModel() : sk || /^src:/.test(String(b.key || '')) ? Prod.peekModel() : BP.peekModel(root), spawnSession: ctx.spawnSession, wsId });
|
|
44
70
|
ctx.json(res, a.body, a.status);
|
|
45
71
|
return true;
|
|
46
72
|
}
|
|
47
73
|
let r;
|
|
48
74
|
try {
|
|
49
|
-
if (act === 'decide') r = Wr.decide({ root, key: b.key, choice: b.choice, why: b.why });
|
|
75
|
+
if (act === 'decide') r = Wr.decide({ root, key: sk ? sk.base : b.key, choice: b.choice, why: b.why });
|
|
50
76
|
else if (act === 'put-off') r = Wr.putOff({ runtimeDir, key: b.key, until: b.until });
|
|
51
77
|
else r = Wr.returnRun({ root, runtimeDir, run: b.run, reason: b.reason, correctionEvent, appendOutbox });
|
|
52
78
|
} catch (e) { r = { ok: false, status: 500, error: 'the record could not be written: ' + String((e && e.message) || e).slice(0, 160) }; }
|
|
53
79
|
if (!r.ok) { ctx.json(res, { ok: false, error: r.error }, r.status || 400); return true; }
|
|
54
|
-
try { await BP.rebuild({ root, runtimeDir }); } catch { /* the page re-reads on its next poll */ }
|
|
80
|
+
try { if (merged) await Prod.rebuildOne({ root, runtimeDir }); else await BP.rebuild({ root, runtimeDir }); } catch { /* the page re-reads on its next poll */ }
|
|
55
81
|
ctx.json(res, { ok: true, wrote: act, ...(r.heading ? { heading: r.heading } : {}) });
|
|
56
82
|
return true;
|
|
57
83
|
}
|
|
@@ -72,6 +98,14 @@ module.exports = {
|
|
|
72
98
|
}
|
|
73
99
|
if (url.pathname !== '/api/briefing-premium') return false;
|
|
74
100
|
if (req.method !== 'GET') { ctx.json(res, { ok: false, error: 'GET only' }, 405); return true; }
|
|
101
|
+
// No ws: the Briefing across the products the PM picked. A ws: that one workspace, read as before.
|
|
102
|
+
if (!url.searchParams.get('ws')) {
|
|
103
|
+
const sel = products(ctx);
|
|
104
|
+
const sources = await Src.readSources(ctx, Date.now()).catch(() => null);
|
|
105
|
+
const r = await Prod.modelOfMany({ available: sel.available, engine: enginePath(), runtimeDir: ctx.runtimeDir, sources });
|
|
106
|
+
ctx.json(res, r.state === 'error' ? { ok: false, ...r } : { ok: true, ...r }, r.state === 'ready' ? 200 : r.state === 'reading' ? 202 : 500);
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
75
109
|
let root;
|
|
76
110
|
try { const w = ctx.getWs && ctx.getWs(url.searchParams.get('ws') || null); root = w && w.root; } catch { root = null; }
|
|
77
111
|
root = root || path.resolve(__dirname, '..', '..');
|
|
@@ -33,6 +33,8 @@ const bool = (section, scope, def) => ({ section, scope, type: 'bool', def });
|
|
|
33
33
|
const DEFS = Object.freeze({
|
|
34
34
|
'ws.start': one('workspaces', 'computer', ['last', 'ask'], 'last'),
|
|
35
35
|
'ws.briefing': one('workspaces', 'computer', ['one', 'all'], 'one'),
|
|
36
|
+
// the premium Briefing: which workspaces it reads (initiative briefing-premium-products). null = every one except the home workspace.
|
|
37
|
+
'ws.briefing.include': { section: 'workspaces', scope: 'computer', type: 'ids', max: 50, def: null },
|
|
36
38
|
'board.template': one('boards', 'ws', ['product', 'project', 'simple'], 'product'),
|
|
37
39
|
'board.rules': one('boards', 'ws', ['warn', 'off'], 'warn'),
|
|
38
40
|
'board.templates': { section: 'boards', scope: 'computer', type: 'templates', def: TEMPLATES },
|
|
@@ -121,6 +123,7 @@ function validate(key, value) {
|
|
|
121
123
|
if (typeof value !== 'string' || value.length > 64) return `${key} must be a time zone such as Asia/Jakarta, or empty for this computer's`;
|
|
122
124
|
try { new Intl.DateTimeFormat('en', { timeZone: value }); return null; } catch { return `${value} is not a time zone this computer knows`; }
|
|
123
125
|
}
|
|
126
|
+
case 'ids': return value === null || Array.isArray(value) && value.length <= d.max && value.every((x) => typeof x === 'string' && WS_ID.test(x)) && new Set(value).size === value.length ? null : `${key} must list at most ${d.max} workspace ids, each once`;
|
|
124
127
|
case 'templates': {
|
|
125
128
|
if (!value || typeof value !== 'object' || Array.isArray(value)) return `${key} must name the templates`;
|
|
126
129
|
const ids = Object.keys(value);
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.255.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.255.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/workspace/package.json
CHANGED
|
@@ -151,6 +151,7 @@ input[type=range]:active::-webkit-slider-thumb{transform:scale(1.15)}
|
|
|
151
151
|
.dbody .kv{display:grid;grid-template-columns:110px minmax(0,1fr);gap:6px 14px;font-size:15px}
|
|
152
152
|
.dbody .kv span:nth-child(odd){color:var(--ink-3)}
|
|
153
153
|
.cta{height:48px;padding:0 24px;border-radius:var(--r-chip);background:var(--solid);color:var(--on-solid);font-weight:650;display:inline-flex;align-items:center;gap:8px;transition:transform .35s var(--spring)}
|
|
154
|
+
a.cta{text-decoration:none}
|
|
154
155
|
.cta:hover{transform:translateY(-2px)}
|
|
155
156
|
.cta:active{transform:scale(.97)}
|
|
156
157
|
.cta svg{width:18px;height:18px}
|
|
@@ -1196,3 +1197,17 @@ button.dsn:hover{box-shadow:inset 0 0 0 1.5px rgb(79,109,245)}
|
|
|
1196
1197
|
.crmenu{max-height:min(60vh,420px);overflow:auto;overscroll-behavior:contain}
|
|
1197
1198
|
.crmenu button{flex:none}
|
|
1198
1199
|
.crmenu button span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
1200
|
+
|
|
1201
|
+
/* the Briefing across products (initiative briefing-premium-products): the product name on a row, and the picker built from the mockup's chip and menu */
|
|
1202
|
+
.pn{font:600 12.5px var(--mono);letter-spacing:.04em;color:var(--ink-2);margin-right:8px;padding:2px 8px;border-radius:999px;background:var(--well);white-space:nowrap}
|
|
1203
|
+
.pkw{position:relative;display:inline-flex;align-items:center}
|
|
1204
|
+
.pkw[hidden]{display:none}
|
|
1205
|
+
.pkw .fchip{height:44px}
|
|
1206
|
+
.pkmenu{left:auto;right:0;min-width:240px}
|
|
1207
|
+
.pkmenu .bx{flex:none;width:20px;height:20px;border-radius:6px;border:2px solid var(--ink-3);display:grid;place-items:center}
|
|
1208
|
+
.pkmenu button[aria-checked="true"] .bx{background:var(--solid);border-color:var(--solid)}
|
|
1209
|
+
.pkmenu button[aria-checked="true"] .bx::after{content:"";width:9px;height:5px;border-left:2px solid var(--on-solid);border-bottom:2px solid var(--on-solid);transform:rotate(-45deg) translate(1px,-1px)}
|
|
1210
|
+
.pkmenu .pkreset{margin-top:4px;border-top:1px solid var(--hair);border-radius:0 0 14px 14px;color:var(--ink-2)}
|
|
1211
|
+
.pkempty{padding:10px 14px}
|
|
1212
|
+
.top{position:relative;z-index:6}
|
|
1213
|
+
.pkw{z-index:40}
|