create-pmos 0.38.8 → 0.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/payload/KIT-VERSION +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/README.md +37 -1
- package/workspace/bin/pmos.js +8 -1
- package/workspace/guide/index.md +40 -0
- package/workspace/guide/initiatives-and-next-steps.md +72 -0
- package/workspace/guide/layout.md +73 -0
- package/workspace/guide/proactive-engine.md +75 -0
- package/workspace/guide/scheduled-tasks-and-cli.md +41 -0
- package/workspace/guide/statuses.md +53 -0
- package/workspace/guide/validate-with-typesafe.md +80 -0
- package/workspace/lib/brief.js +113 -0
- package/workspace/lib/claims.js +605 -0
- package/workspace/lib/doc-profiles.js +74 -0
- package/workspace/lib/initiative-files.js +37 -6
- package/workspace/lib/judgment.js +65 -0
- package/workspace/lib/mobile-nav.js +1 -1
- package/workspace/lib/next-step-actions.js +139 -0
- package/workspace/lib/next-steps.js +27 -7
- package/workspace/lib/preview-kind.js +17 -2
- package/workspace/lib/proactive-cli.js +137 -0
- package/workspace/lib/proactive.js +208 -0
- package/workspace/lib/repo-watch.js +120 -0
- package/workspace/lib/schedule-cli.js +5 -2
- package/workspace/lib/secret-scan.js +28 -0
- package/workspace/lib/session-budget.js +35 -0
- package/workspace/lib/status-groups.js +53 -10
- package/workspace/lib/typesafe-settings.js +122 -0
- package/workspace/lib/typesafe.js +124 -0
- package/workspace/lib/validate.js +304 -0
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +3 -3
- package/workspace/public/pm/app.js +53 -18
- package/workspace/public/pm/data.js +18 -3
- package/workspace/public/pm/mobile.js +1 -1
- package/workspace/public/pm/modes-live.js +373 -0
- package/workspace/public/pm/modes.js +26 -0
- package/workspace/public/pm/next-step-ui.js +185 -0
- package/workspace/public/pm/panel-detail.js +47 -11
- package/workspace/public/pm/panel-initiatives.js +26 -4
- package/workspace/public/pm/panel-repos.js +2 -2
- package/workspace/public/pm/state.js +3 -3
- package/workspace/public/pm/status-banner.js +87 -0
- package/workspace/public/pm/tab-doc.js +66 -8
- package/workspace/server.js +562 -18
- package/workspace/skills/morning-brief.skill +43 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* brief.js — the morning brief: what a session submits, checked once, and how it is ordered.
|
|
3
|
+
*
|
|
4
|
+
* The workspace server makes no model or network call and reaches no connector. A scheduled skill runs in an agent session,
|
|
5
|
+
* reads the sources the profile declares and the settings allow, and submits ONE brief. This module owns the shape of that
|
|
6
|
+
* brief and the check of it. A brief in any other shape is refused with the reason, never repaired. Every text field has a
|
|
7
|
+
* short limit, so a message body cannot be stored here by accident.
|
|
8
|
+
*
|
|
9
|
+
* validateBrief(raw, {now, settings}) → {ok:true, brief} | {ok:false, error}
|
|
10
|
+
* greeting(now, name) → the one line Today opens with
|
|
11
|
+
* localDate(now) → YYYY-MM-DD on this machine's clock
|
|
12
|
+
*
|
|
13
|
+
* The bucket is derived: an item under WORTH_ASKING confidence is "worth asking", whatever the session said. The order is
|
|
14
|
+
* fixed and stated: higher confidence first, then PM-owned before agent-owned, then by title.
|
|
15
|
+
*/
|
|
16
|
+
const crypto = require('crypto');
|
|
17
|
+
|
|
18
|
+
const SOURCES = ['calendar', 'wispr', 'mail', 'jira', 'confluence'];
|
|
19
|
+
const ITEM_SOURCES = [...SOURCES, 'derived'];
|
|
20
|
+
const SOURCE_STATES = ['read', 'not-reached', 'off'];
|
|
21
|
+
const OWNERS = ['pm', 'agent'];
|
|
22
|
+
const EFFORTS = ['minutes', 'an-hour', 'half-day', 'day'];
|
|
23
|
+
const WORTH_ASKING = 0.5;
|
|
24
|
+
const MAX_ITEMS = 12;
|
|
25
|
+
const MAX_EVENTS = 20;
|
|
26
|
+
const INI_RE = /^[a-z0-9][a-z0-9._-]*$/i;
|
|
27
|
+
const clip = (s, n) => String(s == null ? '' : s).replace(/\s+/g, ' ').trim().slice(0, n);
|
|
28
|
+
|
|
29
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
30
|
+
const localDate = (now) => { const d = new Date(now); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; };
|
|
31
|
+
function greeting(now, name) {
|
|
32
|
+
const h = new Date(now).getHours();
|
|
33
|
+
const part = h < 12 ? 'Good morning' : h < 18 ? 'Good afternoon' : 'Good evening';
|
|
34
|
+
return part + (name ? ', ' + name : '') + '.';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const tooLong = (v, n) => typeof v === 'string' && v.trim().length > n;
|
|
38
|
+
|
|
39
|
+
/** @returns {{ok:true, brief:object}|{ok:false, error:string}} */
|
|
40
|
+
function validateBrief(raw, ctx = {}) {
|
|
41
|
+
const now = ctx.now || Date.now();
|
|
42
|
+
const settings = ctx.settings || {};
|
|
43
|
+
const on = (src) => !settings.briefSources || settings.briefSources[src] !== false;
|
|
44
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return { ok: false, error: 'a brief is a JSON object' };
|
|
45
|
+
const date = raw.date === undefined ? localDate(now) : raw.date;
|
|
46
|
+
if (typeof date !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(date) || Number.isNaN(Date.parse(date))) return { ok: false, error: 'date must be YYYY-MM-DD' };
|
|
47
|
+
|
|
48
|
+
const sources = {};
|
|
49
|
+
const given = raw.sources === undefined ? {} : raw.sources;
|
|
50
|
+
if (!given || typeof given !== 'object' || Array.isArray(given)) return { ok: false, error: `sources must be an object of ${SOURCES.join(', ')} → ${SOURCE_STATES.join(' | ')}` };
|
|
51
|
+
for (const k of Object.keys(given)) if (!SOURCES.includes(k)) return { ok: false, error: `unknown source "${k}" — one of ${SOURCES.join(', ')}` };
|
|
52
|
+
for (const k of SOURCES) {
|
|
53
|
+
const v = given[k] === undefined ? 'not-reached' : given[k];
|
|
54
|
+
if (!SOURCE_STATES.includes(v)) return { ok: false, error: `source "${k}" must be one of ${SOURCE_STATES.join(', ')}` };
|
|
55
|
+
if (!on(k) && v === 'read') return { ok: false, error: `"${k}" is switched off in the settings, so it cannot be reported as read` };
|
|
56
|
+
sources[k] = !on(k) ? 'off' : v;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const ev = raw.calendar === undefined ? [] : raw.calendar;
|
|
60
|
+
if (!Array.isArray(ev)) return { ok: false, error: 'calendar must be a list of events' };
|
|
61
|
+
if (ev.length > MAX_EVENTS) return { ok: false, error: `calendar has ${ev.length} events — at most ${MAX_EVENTS}` };
|
|
62
|
+
if (ev.length && sources.calendar !== 'read') return { ok: false, error: 'calendar events were given but calendar is not reported as read' };
|
|
63
|
+
const calendar = [];
|
|
64
|
+
for (const [i, e] of ev.entries()) {
|
|
65
|
+
if (!e || typeof e !== 'object' || Array.isArray(e)) return { ok: false, error: `calendar event ${i + 1} is not an object` };
|
|
66
|
+
if (typeof e.title !== 'string' || !e.title.trim()) return { ok: false, error: `calendar event ${i + 1} needs a title` };
|
|
67
|
+
if (tooLong(e.title, 120)) return { ok: false, error: `calendar event ${i + 1}: title is over 120 characters` };
|
|
68
|
+
if (typeof e.start !== 'string' || Number.isNaN(Date.parse(e.start))) return { ok: false, error: `calendar event ${i + 1}: start must be a date and time` };
|
|
69
|
+
if (e.end !== undefined && (typeof e.end !== 'string' || Number.isNaN(Date.parse(e.end)))) return { ok: false, error: `calendar event ${i + 1}: end must be a date and time` };
|
|
70
|
+
if (e.with !== undefined && tooLong(e.with, 120)) return { ok: false, error: `calendar event ${i + 1}: "with" is over 120 characters` };
|
|
71
|
+
calendar.push({ start: e.start, end: e.end || null, title: clip(e.title, 120), with: e.with ? clip(e.with, 120) : null });
|
|
72
|
+
}
|
|
73
|
+
calendar.sort((a, b) => Date.parse(a.start) - Date.parse(b.start));
|
|
74
|
+
|
|
75
|
+
const it = raw.items === undefined ? [] : raw.items;
|
|
76
|
+
if (!Array.isArray(it)) return { ok: false, error: 'items must be a list' };
|
|
77
|
+
if (it.length > MAX_ITEMS) return { ok: false, error: `items has ${it.length} entries — at most ${MAX_ITEMS}` };
|
|
78
|
+
const items = [];
|
|
79
|
+
for (const [i, x] of it.entries()) {
|
|
80
|
+
const at = `item ${i + 1}`;
|
|
81
|
+
if (!x || typeof x !== 'object' || Array.isArray(x)) return { ok: false, error: `${at} is not an object` };
|
|
82
|
+
if (typeof x.title !== 'string' || !x.title.trim()) return { ok: false, error: `${at} needs a title` };
|
|
83
|
+
if (tooLong(x.title, 140)) return { ok: false, error: `${at}: title is over 140 characters` };
|
|
84
|
+
if (typeof x.why_now !== 'string' || !x.why_now.trim()) return { ok: false, error: `${at} needs why_now` };
|
|
85
|
+
if (tooLong(x.why_now, 240)) return { ok: false, error: `${at}: why_now is over 240 characters` };
|
|
86
|
+
if (x.evidence !== undefined && tooLong(x.evidence, 240)) return { ok: false, error: `${at}: evidence is over 240 characters` };
|
|
87
|
+
if (!ITEM_SOURCES.includes(x.source)) return { ok: false, error: `${at}: source must be one of ${ITEM_SOURCES.join(', ')}` };
|
|
88
|
+
if (x.source !== 'derived' && sources[x.source] !== 'read') return { ok: false, error: `${at}: source "${x.source}" is not reported as read, so it cannot support an item` };
|
|
89
|
+
if (!OWNERS.includes(x.owner)) return { ok: false, error: `${at}: owner must be one of ${OWNERS.join(', ')}` };
|
|
90
|
+
if (!EFFORTS.includes(x.effort)) return { ok: false, error: `${at}: effort must be one of ${EFFORTS.join(', ')}` };
|
|
91
|
+
if (typeof x.confidence !== 'number' || !Number.isFinite(x.confidence) || x.confidence < 0 || x.confidence > 1) return { ok: false, error: `${at}: confidence must be a number from 0 to 1` };
|
|
92
|
+
if (x.initiative_id !== undefined && x.initiative_id !== null && (typeof x.initiative_id !== 'string' || !INI_RE.test(x.initiative_id))) return { ok: false, error: `${at}: initiative_id is not an initiative id` };
|
|
93
|
+
const title = clip(x.title, 140);
|
|
94
|
+
items.push({
|
|
95
|
+
id: crypto.createHash('sha1').update(`${date}|${x.source}|${title}`).digest('hex').slice(0, 12),
|
|
96
|
+
title, why_now: clip(x.why_now, 240), evidence: clip(x.evidence, 240), source: x.source, initiative_id: x.initiative_id || null,
|
|
97
|
+
owner: x.owner, effort: x.effort, confidence: x.confidence, worth_asking: x.confidence < WORTH_ASKING,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
items.sort((a, b) => b.confidence - a.confidence || (a.owner === b.owner ? 0 : a.owner === 'pm' ? -1 : 1) || a.title.localeCompare(b.title));
|
|
101
|
+
const ids = new Set(items.map((x) => x.id));
|
|
102
|
+
if (ids.size !== items.length) return { ok: false, error: 'two items have the same source and title' };
|
|
103
|
+
|
|
104
|
+
let judgment = { used: false };
|
|
105
|
+
if (raw.judgment !== undefined) {
|
|
106
|
+
if (!raw.judgment || typeof raw.judgment !== 'object' || typeof raw.judgment.used !== 'boolean') return { ok: false, error: 'judgment must be {"used": true|false}' };
|
|
107
|
+
if (raw.judgment.used && !settings.briefScoring) return { ok: false, error: 'typed scoring is not switched on in the settings, so a brief cannot say it used it' };
|
|
108
|
+
judgment = { used: raw.judgment.used };
|
|
109
|
+
}
|
|
110
|
+
return { ok: true, brief: { date, generated_at: new Date(now).toISOString(), sources, calendar, priorities: items.filter((x) => !x.worth_asking), worth_asking: items.filter((x) => x.worth_asking), judgment } };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = { SOURCES, ITEM_SOURCES, SOURCE_STATES, OWNERS, EFFORTS, WORTH_ASKING, MAX_ITEMS, MAX_EVENTS, validateBrief, greeting, localDate };
|