create-pmos 0.59.3 → 0.60.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/scripts/delivery-index.js +85 -7
- package/payload/scripts/workflow-state.js +3 -3
- package/payload/state/discoveries.md +1 -1
- package/workspace/fixtures/initiatives-api/sheet.json +29 -7
- package/workspace/guide/index.md +3 -3
- package/workspace/guide/initiatives-screen.md +25 -1
- package/workspace/guide/layout.md +6 -6
- package/workspace/guide/proactive-engine.md +1 -0
- package/workspace/guide/today.md +23 -1
- package/workspace/lib/calendar-memory.js +257 -0
- package/workspace/lib/console-state.js +1 -1
- package/workspace/lib/doc-category.js +3 -1
- package/workspace/lib/engine/delivery-index.js +85 -7
- package/workspace/lib/engine/workflow-state.js +3 -3
- package/workspace/lib/initiative-files.js +59 -1
- package/workspace/lib/initiative-rows.js +14 -2
- package/workspace/lib/mobile-nav.js +1 -1
- package/workspace/lib/plan-tree.js +44 -5
- package/workspace/lib/proactive-cli.js +16 -1
- package/workspace/lib/routes-initiatives.js +165 -9
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +1 -1
- package/workspace/public/index.html +1 -1
- package/workspace/public/pm/components.css +70 -0
- package/workspace/public/pm/dest-initiatives.js +7 -1
- package/workspace/public/pm/frame-copy.js +2 -2
- package/workspace/public/pm/initiative-sheet.js +42 -10
- package/workspace/public/pm/initiatives-api.js +15 -0
- package/workspace/public/pm/initiatives-copy.js +13 -0
- package/workspace/public/pm/initiatives-ui.js +14 -0
- package/workspace/public/pm/registry.js +1 -1
- package/workspace/public/pm/today-agenda.js +305 -0
- package/workspace/public/pm/today-copy.js +54 -2
- package/workspace/public/pm/today-day.js +23 -3
- package/workspace/public/pm/today-goals.js +7 -1
- package/workspace/server.js +53 -2
- package/workspace/skills/morning-brief.skill +6 -1
- package/workspace/test-files.json +2 -0
- package/workspace/test-lists/scan-consistency.json +5 -0
|
@@ -28,6 +28,7 @@ import { pmGet, pmPost } from './http.js';
|
|
|
28
28
|
import { FIXTURE } from './state.js';
|
|
29
29
|
import { COPY } from './today-copy.js';
|
|
30
30
|
import { goalRows, goalAdd, goalDetail, addGoal, goalsSig } from './today-goals.js';
|
|
31
|
+
import { agendaPanel, meetingDetail, memoryBlock, refreshAgenda, agendaSig, onAgenda } from './today-agenda.js';
|
|
31
32
|
|
|
32
33
|
const D = COPY.day;
|
|
33
34
|
const SLOW_MS = 30000;
|
|
@@ -73,6 +74,8 @@ async function load(ws) {
|
|
|
73
74
|
const next = out && out.ok ? { data: out, error: null } : { data: prev ? prev.data : null, error: (out && out.error) || D.unreached };
|
|
74
75
|
cache.set(ws, next);
|
|
75
76
|
if (!prev || dataSig(prev.data) !== dataSig(next.data) || prev.error !== next.error) rerender();
|
|
77
|
+
// the month follows the brief and the clock: it is read again with the day
|
|
78
|
+
refreshAgenda();
|
|
76
79
|
schedule();
|
|
77
80
|
}
|
|
78
81
|
|
|
@@ -378,6 +381,13 @@ function eventChip(e) {
|
|
|
378
381
|
function calendar(ctx) {
|
|
379
382
|
const { m } = ctx;
|
|
380
383
|
if (!m.events.length) return panel('calendar', null, note(m.run && m.stage === 'reading' ? D.readingJobs : D.noCalendar));
|
|
384
|
+
return panel('calendar', null, calendarRows(ctx));
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** Today's meetings from the brief, with a line at the current time: the agenda of today, and the calendar panel. */
|
|
388
|
+
function calendarRows(ctx) {
|
|
389
|
+
const { m } = ctx;
|
|
390
|
+
if (!m.events.length) return null;
|
|
381
391
|
const rows = [];
|
|
382
392
|
let nowShown = false;
|
|
383
393
|
const nowRow = () => el('li', { class: 'td-day__nowrow', 'data-day': 'now', 'aria-hidden': 'true' }, D.now(hhmm(m.now)));
|
|
@@ -389,7 +399,14 @@ function calendar(ctx) {
|
|
|
389
399
|
{ 'data-past': e.ended ? 'true' : null, 'data-goal-tie': n ? 'true' : null }));
|
|
390
400
|
}
|
|
391
401
|
if (!nowShown) rows.push(nowRow());
|
|
392
|
-
return
|
|
402
|
+
return el('ul', { class: 'td-day__rows', 'data-day-list': 'calendar' }, ...rows);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/* The Agenda month (G25 to G27): today-agenda.js draws it with the helpers of this section. An older workspace server
|
|
406
|
+
* has no agenda route: then the day's calendar panel shows, as before. */
|
|
407
|
+
function agenda(ctx) {
|
|
408
|
+
const h = { panel, sel: ui.sel, todayRows: () => calendarRows(ctx), rowBtn: (type, id, parts, mods) => rowBtn(ctx, type, id, parts, mods) };
|
|
409
|
+
return agendaPanel(ctx, h) || calendar(ctx);
|
|
393
410
|
}
|
|
394
411
|
|
|
395
412
|
function jobRow(ctx, j) {
|
|
@@ -525,6 +542,7 @@ function eventDetail(ctx, e) {
|
|
|
525
542
|
ini ? el('div', { class: 'td-day__ini', 'data-day': 'initiative' }, el('span', null, D.initiative + ': ', el('b', { class: 'td-day__mono' }, ini.id), ini.why ? el('span', { class: 'td-day__dim' }, ' (' + D.suggested(ini.why) + ')') : null),
|
|
526
543
|
ctx.openIni ? button({ label: D.openInitiative, size: 'sm', variant: 'ghost', attrs: { 'data-day-act': 'open-initiative' }, onClick: () => ctx.openIni(ini.id) }) : null) : null,
|
|
527
544
|
tie,
|
|
545
|
+
memoryBlock(e.id),
|
|
528
546
|
!e.ended ? el('div', { class: 'td-day__acts' }, e.prep ? pill(D.needPrep, 'warn') : steps.before && steps.before.length ? pill(D.nothingToPrep, 'ok') : null) : null,
|
|
529
547
|
after,
|
|
530
548
|
!e.ended ? [list(D.phases.before, (steps.before || []).map(stepItem)), list(D.phases.during, (steps.during || []).map(stepItem)), list(D.phases.after, (steps.after || []).map(stepItem)), mailBlock] : null,
|
|
@@ -777,6 +795,7 @@ function detail(ctx) {
|
|
|
777
795
|
if (s.type === 'channel') return channelDetail(ctx, s.id);
|
|
778
796
|
if (s.type === 'step') return stepDetail(ctx, Math.max(0, Math.min(3, Number(s.id) || 0)));
|
|
779
797
|
if (s.type === 'sources') return sourcesDetail(ctx);
|
|
798
|
+
if (s.type === 'meeting') return meetingDetail(ctx, s.id, { dhead });
|
|
780
799
|
return null;
|
|
781
800
|
};
|
|
782
801
|
kids = find(sel);
|
|
@@ -798,6 +817,7 @@ export function daySection({ ws, setState, openIni, owed, here, all } = {}) {
|
|
|
798
817
|
// like Today's own feeds, a read that lands re-renders only while the section is on the page: a hidden Today must not
|
|
799
818
|
// cause a whole-page render under whatever the PM is doing elsewhere
|
|
800
819
|
if (setState) rerender = () => { if (document.querySelector('[data-today="day"]')) setState({}); };
|
|
820
|
+
onAgenda(() => rerender());
|
|
801
821
|
ensure(ws);
|
|
802
822
|
const c = cache.get(ws) || {};
|
|
803
823
|
const m = model(c.data, { here: here || null, all: !!all });
|
|
@@ -828,12 +848,12 @@ export function daySection({ ws, setState, openIni, owed, here, all } = {}) {
|
|
|
828
848
|
c.error ? el('p', { class: 'td-day__run td-day__run--failed', role: 'status', 'data-day': 'error' }, D.refused(c.error)) : null,
|
|
829
849
|
m.d && m.d.legacy ? note(D.oldServer) : null,
|
|
830
850
|
el('div', { class: 'td-day__cols' },
|
|
831
|
-
el('div', { class: 'td-day__left' }, summaryPanel(ctx), goalsPanel(ctx),
|
|
851
|
+
el('div', { class: 'td-day__left' }, summaryPanel(ctx), goalsPanel(ctx), agenda(ctx), jobsPanel(ctx), mentionsPanel(ctx), waitingPanel(owed)),
|
|
832
852
|
detail(ctx)));
|
|
833
853
|
}
|
|
834
854
|
|
|
835
855
|
export function daySig(ws) {
|
|
836
856
|
const c = cache.get(ws) || {};
|
|
837
857
|
return JSON.stringify([ui.open, ui.sel, ui.mtab, ui.openSrc, ui.showIgn, ui.showIgnDigests, [...ui.revising], [...ui.busy], c.error, runs.get(ws) || null, [...seen].length])
|
|
838
|
-
+ dataSig(c.data) + goalsSig();
|
|
858
|
+
+ dataSig(c.data) + goalsSig() + agendaSig();
|
|
839
859
|
}
|
|
@@ -21,6 +21,7 @@ import { COPY } from './today-copy.js';
|
|
|
21
21
|
const G = COPY.goals;
|
|
22
22
|
const D = COPY.day;
|
|
23
23
|
let draft = '';
|
|
24
|
+
let lastAdded = null; // { key, at }: the text of the last goal sent, so a double commit adds it once
|
|
24
25
|
const prepared = new Map(); // step ref -> { key, branch, item_id, prompt_sha, initiative_id, ws } (a plan shown, not yet queued)
|
|
25
26
|
|
|
26
27
|
async function press(ctx, body) {
|
|
@@ -69,10 +70,15 @@ export function goalAdd(ctx) {
|
|
|
69
70
|
const f = field({ label: G.inputLabel, value: '', placeholder: G.inputHint, hideLabel: true, attrs: { 'data-goals': 'input', 'data-day-input': 'goal', maxlength: '140' },
|
|
70
71
|
onCommit: async (v) => {
|
|
71
72
|
if (!String(v).trim()) { draft = v; return; }
|
|
73
|
+
// a rebuild while the text is typed can commit it twice: once as the old field leaves the page, once on Enter.
|
|
74
|
+
// The second commit of the text just added clears the field and adds nothing.
|
|
75
|
+
const key = String(v).trim().toLowerCase();
|
|
76
|
+
if (lastAdded && lastAdded.key === key && Date.now() - lastAdded.at < 10000) { draft = ''; f.control.value = ''; ctx.rerender(); return; }
|
|
77
|
+
lastAdded = { key, at: Date.now() };
|
|
72
78
|
// clear before the press: its reload re-renders the section, and a rebuild must not bring the text back
|
|
73
79
|
draft = '';
|
|
74
80
|
if (await addGoal(ctx, v)) { if (f.control.resetCommitted) f.control.resetCommitted(''); f.control.value = ''; ctx.rerender(); }
|
|
75
|
-
else { draft = v; ctx.rerender(); }
|
|
81
|
+
else { lastAdded = null; draft = v; ctx.rerender(); }
|
|
76
82
|
} });
|
|
77
83
|
// a rebuild keeps what is typed. The field is made empty and filled after, so Enter still commits the restored text.
|
|
78
84
|
f.control.value = draft;
|
package/workspace/server.js
CHANGED
|
@@ -95,6 +95,7 @@ const judgmentLib = require('./lib/judgment');
|
|
|
95
95
|
const proactiveLib = require('./lib/proactive');
|
|
96
96
|
const learnLib = require('./lib/learn');
|
|
97
97
|
const briefLib = require('./lib/brief');
|
|
98
|
+
const calMemLib = require('./lib/calendar-memory');
|
|
98
99
|
const goalsLib = require('./lib/goals');
|
|
99
100
|
const dayRunLib = require('./lib/day-run');
|
|
100
101
|
const todayLib = require('./lib/today');
|
|
@@ -1455,7 +1456,13 @@ function parseHead(full) {
|
|
|
1455
1456
|
}
|
|
1456
1457
|
}
|
|
1457
1458
|
const alsoIds = [...new Set(also.filter((x) => /^[A-Za-z0-9][\w.-]*$/.test(x)))];
|
|
1459
|
+
// scan-consistency R3 — the files a RECORD names in its frontmatter (`prd_path`, `explainer_path`, `design_path`, …).
|
|
1460
|
+
// Only the record role reads them (see docMeta); they are the strongest tie a document can have to an initiative.
|
|
1461
|
+
const fmBlock = /^---\r?\n([\s\S]*?)(?:\r?\n---|$)/.exec(head);
|
|
1462
|
+
const named = fmBlock ? [...fmBlock[1].matchAll(/^[a-z_]*_path[ \t]*:[ \t]*["']?([^"'\s][^"'\n]*?)["']?[ \t]*$/gm)]
|
|
1463
|
+
.map((m) => m[1].trim().replace(/^\.?\//, '')).filter((x) => x && !x.split('/').includes('..')) : [];
|
|
1458
1464
|
return {
|
|
1465
|
+
named,
|
|
1459
1466
|
initiative: id ? id[1] : (alsoIds[0] || null),
|
|
1460
1467
|
also: alsoIds,
|
|
1461
1468
|
recordId: rid ? rid[1] : null,
|
|
@@ -1521,6 +1528,7 @@ function docMeta(root, full) {
|
|
|
1521
1528
|
// frontmatter wins; the filename convention is the fallback, never the override
|
|
1522
1529
|
initiative: head.initiative || (role === 'record' ? head.recordId : null) || null,
|
|
1523
1530
|
also: role === 'record' ? [] : (head.also || []),
|
|
1531
|
+
named: role === 'record' ? (head.named || []) : [],
|
|
1524
1532
|
stem, title: head.title || null,
|
|
1525
1533
|
kr: head.kr || null, lane: head.lane || null,
|
|
1526
1534
|
dest: head.dest || null,
|
|
@@ -1574,6 +1582,19 @@ function initiativeIndex(root) {
|
|
|
1574
1582
|
// a filename stem is promoted to a group only when it matches a declared id or when
|
|
1575
1583
|
// several artifacts share it, so a lone research note never invents an initiative.
|
|
1576
1584
|
const declared = new Set(metas.map((m) => m.initiative).filter(Boolean));
|
|
1585
|
+
// scan-consistency R3 — a document a record NAMES (`explainer_path: planning/explainers/x.md`) belongs to that record's
|
|
1586
|
+
// initiative, whatever its file name. Before this the join read only declarations and file names, so an explainer or
|
|
1587
|
+
// a design whose name differs from the id was attributed to nothing and the initiative showed its record alone (62 of
|
|
1588
|
+
// 283 here). When two records name one file, the first in path order keeps it; the other still lists it as shared.
|
|
1589
|
+
const namedBy = new Map();
|
|
1590
|
+
const alsoNamed = new Map();
|
|
1591
|
+
for (const m of metas.filter((x) => x.role === 'record' && x.initiative).sort((a, b) => a.path.localeCompare(b.path))) {
|
|
1592
|
+
for (const rel of m.named || []) {
|
|
1593
|
+
if (!namedBy.has(rel)) namedBy.set(rel, m.initiative);
|
|
1594
|
+
else if (namedBy.get(rel) !== m.initiative) { if (!alsoNamed.has(rel)) alsoNamed.set(rel, new Set()); alsoNamed.get(rel).add(m.initiative); }
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
const relOf = (m) => String(m.path).split(path.sep).join('/');
|
|
1577
1598
|
const byStem = new Map();
|
|
1578
1599
|
for (const m of metas) if (!m.initiative) byStem.set(m.stem, (byStem.get(m.stem) || 0) + 1);
|
|
1579
1600
|
const groups = new Map();
|
|
@@ -1602,6 +1623,7 @@ function initiativeIndex(root) {
|
|
|
1602
1623
|
const prefixOwner = (stem) => capturing.find((id) => stem.length > id.length + 1 && stem.startsWith(id + '-')) || null;
|
|
1603
1624
|
const keyFor = (m) => {
|
|
1604
1625
|
if (m.initiative) return m.initiative;
|
|
1626
|
+
if (namedBy.has(relOf(m))) return namedBy.get(relOf(m));
|
|
1605
1627
|
if (declared.has(m.stem)) return m.stem;
|
|
1606
1628
|
// the convention join: `jira-sync-prd.md` + `jira-sync-eval-rubric.md` are one unit
|
|
1607
1629
|
if ((byStem.get(m.stem) || 0) > 1) return m.stem;
|
|
@@ -1616,7 +1638,7 @@ function initiativeIndex(root) {
|
|
|
1616
1638
|
g.artifacts.push(art);
|
|
1617
1639
|
// overhaul-r2 C1 — listed under every OTHER initiative it names too (it does not lend them a
|
|
1618
1640
|
// title, an anchor or a lane: those stay the record's and its primary owner's)
|
|
1619
|
-
for (const other of m.also || []) {
|
|
1641
|
+
for (const other of [...(m.also || []), ...(alsoNamed.get(relOf(m)) || [])]) {
|
|
1620
1642
|
if (other === k) continue;
|
|
1621
1643
|
if (!groups.has(other)) groups.set(other, { id: other, title: null, kr: null, lane: null, artifacts: [] });
|
|
1622
1644
|
groups.get(other).artifacts.push({ ...art, shared: true });
|
|
@@ -3836,6 +3858,15 @@ function dayTick() {
|
|
|
3836
3858
|
if (JSON.stringify(run) !== before) writeDayRun(run);
|
|
3837
3859
|
}
|
|
3838
3860
|
// ── end of the today day run helpers ─────────────────────────────────────────────────────────────────────────────
|
|
3861
|
+
/* The calendar the Agenda remembers (G23): one file beside the briefs, written only by the brief route. */
|
|
3862
|
+
const CAL_MEMORY = () => path.join(STATE, 'calendar-memory.json');
|
|
3863
|
+
function readCalMemory() {
|
|
3864
|
+
try { return JSON.parse(fs.readFileSync(CAL_MEMORY(), 'utf8')); } catch { return null; }
|
|
3865
|
+
}
|
|
3866
|
+
function writeCalMemory(m) {
|
|
3867
|
+
const f = CAL_MEMORY(); const tmp = `${f}.${process.pid}.tmp`;
|
|
3868
|
+
fs.mkdirSync(STATE, { recursive: true }); fs.writeFileSync(tmp, JSON.stringify(m) + '\n'); fs.renameSync(tmp, f);
|
|
3869
|
+
}
|
|
3839
3870
|
|
|
3840
3871
|
// ── Background prepare, slice D3 (initiative proactive-engine) ──────────────────────────────────────────────────
|
|
3841
3872
|
// The PM approves ONE plan hash from Today. The 60-second tick later starts that session through the SAME path a next-step
|
|
@@ -7594,6 +7625,13 @@ async function handleRequest(req, res) {
|
|
|
7594
7625
|
item_repos: goalsLib.itemRepos({ day: readGoalsDay(date), brief, placeRepos: st.placeRepos || {} }),
|
|
7595
7626
|
check: { every: st.dayCheck.every, last: run ? run.checks.last : null, next: due.next, running: !!(run && run.checks.session_id), log: run ? run.checks.log : [] } });
|
|
7596
7627
|
}
|
|
7628
|
+
/* The Agenda month (G25 to G27): read-only. The calendar memory and today's brief, joined by lib/calendar-memory.js. */
|
|
7629
|
+
if (p === '/api/proactive/agenda' && req.method === 'GET') {
|
|
7630
|
+
const now = Date.now();
|
|
7631
|
+
const month = url.searchParams.get('month') || undefined;
|
|
7632
|
+
if (month !== undefined && !/^\d{4}-\d{2}$/.test(month)) return json(res, { ok: false, error: 'month must be YYYY-MM' }, 400);
|
|
7633
|
+
return json(res, calMemLib.agendaMonth({ memory: readCalMemory(), brief: briefOfDay(briefLib.localDate(now)), month, now }));
|
|
7634
|
+
}
|
|
7597
7635
|
if (p.startsWith('/api/proactive/day/') && req.method === 'POST') {
|
|
7598
7636
|
const verb = p.slice('/api/proactive/day/'.length);
|
|
7599
7637
|
if (!['deliver', 'sent', 'fail', 'decide', 'check'].includes(verb)) return json(res, { ok: false, error: 'unknown day-run action' }, 404);
|
|
@@ -7697,6 +7735,14 @@ async function handleRequest(req, res) {
|
|
|
7697
7735
|
const bday = body && typeof body.date === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(body.date) ? body.date : briefLib.localDate(Date.now());
|
|
7698
7736
|
const v = briefLib.validateBrief(body, { now: Date.now(), settings: proSettings(), goalIds: readGoalsDay(bday).goals.map((g) => g.id) });
|
|
7699
7737
|
if (!v.ok) return json(res, { ok: false, error: v.error }, 400);
|
|
7738
|
+
// The calendar memory rides with the brief (G23): checked first, so a refused memory stores nothing at all.
|
|
7739
|
+
let mem = null;
|
|
7740
|
+
if (body.memory !== undefined) {
|
|
7741
|
+
if (v.brief.sources.calendar !== 'read') return json(res, { ok: false, error: 'a calendar memory was given but calendar is not reported as read' }, 400);
|
|
7742
|
+
const mv = calMemLib.validateMemory(body.memory, { now: Date.now() });
|
|
7743
|
+
if (!mv.ok) return json(res, { ok: false, error: mv.error }, 400);
|
|
7744
|
+
mem = mv.memory;
|
|
7745
|
+
}
|
|
7700
7746
|
// `merge: true` (a daytime check, G22) merges into the stored brief of that day; with none stored it is stored as it is.
|
|
7701
7747
|
let out = { ...v.brief, resolved: [] }; let merged = null;
|
|
7702
7748
|
let stored = null; try { stored = JSON.parse(fs.readFileSync(path.join(dir, v.brief.date + '.json'), 'utf8')); } catch { /* none yet */ }
|
|
@@ -7709,10 +7755,15 @@ async function handleRequest(req, res) {
|
|
|
7709
7755
|
}
|
|
7710
7756
|
try { fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(path.join(dir, out.date + '.json'), JSON.stringify(out) + '\n'); }
|
|
7711
7757
|
catch (e) { warnWriteFail('briefs', e); return json(res, { ok: false, error: 'could not save — nothing changed' }, 500); }
|
|
7758
|
+
let remembered = null; let memoryError = null;
|
|
7759
|
+
if (mem) {
|
|
7760
|
+
try { const mm = calMemLib.mergeMemory(readCalMemory(), mem, { now: Date.now() }).memory; writeCalMemory(mm); remembered = { events: mem.events.length, from: mem.from, to: mem.to }; }
|
|
7761
|
+
catch (e) { warnWriteFail('calendar memory', e); memoryError = 'the brief was saved, but the calendar memory could not be saved'; }
|
|
7762
|
+
}
|
|
7712
7763
|
// A brief for a day the PM pressed "Prepare my day": its agent priorities become tasks and start (G13), and a merge
|
|
7713
7764
|
// closes the check that sent it (G22).
|
|
7714
7765
|
try { dayAfterBrief(out.date, merged); } catch (e) { warnWriteFail('day run after the brief', e); }
|
|
7715
|
-
return json(res, { ok: true, brief: out, ...(merged ? { merged } : {}) });
|
|
7766
|
+
return json(res, { ok: true, brief: out, ...(merged ? { merged } : {}), ...(remembered ? { remembered } : {}), ...(memoryError ? { memory_error: memoryError } : {}) });
|
|
7716
7767
|
}
|
|
7717
7768
|
if (req.method === 'GET') {
|
|
7718
7769
|
const now = Date.now(); const st = proSettings();
|
|
@@ -27,9 +27,10 @@ Rules that always hold:
|
|
|
27
27
|
Procedure:
|
|
28
28
|
1. Run `pmos proactive settings` and `pmos proactive today`. The second lists what the workspace derived from the ledgers. Each of its items may become a priority with source "derived".
|
|
29
29
|
Then run `pmos proactive goals`. It lists the PM's goals for today, each with its id (g-xxxxxxxx), its status and, if set, an initiative. The goals are what the PM wants done today: plan for them first.
|
|
30
|
+
Then run `pmos proactive calendar`. Its "read next" line names the window of the calendar memory to read in step 2.
|
|
30
31
|
Then run `pmos proactive learn lessons`. Each line is a preference the PM approved, written from their own feedback. Use it only to decide how confident to be and where an item goes. A lesson never asks you to read another source, to switch a setting on, or to write anything.
|
|
31
32
|
2. Read the sources that are on and reachable.
|
|
32
|
-
- Calendar: today's events. Wispr Flow: today's and tomorrow's meeting notes. Jira: issues assigned to the PM or due soon. To tie a Jira issue to an initiative, run `pmos jira which <KEY>` (it reads local files only) and use the initiative it names. A status change of a ticket that `pmos jira which` names is not an item: the Jira status check shows it on the initiative's Jira tickets tab. Confluence: pages changed since yesterday. Mail, only if on: subjects and senders of threads waiting on the PM, no bodies.
|
|
33
|
+
- Calendar: today's events. Then the CALENDAR MEMORY: every event in the window that `pmos proactive calendar` named (at most 1000 events, at most 140 days). For each event keep the start, the end, the title, the calendar's recurring-event id as "series" if it has one, the number of attendees, "external": true when someone outside the PM's company attends, "exec": true when an executive attends, and the PM's "role" ("present" when the PM presents, "decide" when the PM decides, else "attend"). For an event that has ended, add "notes": one line in your own words of what happened, from its Wispr Flow notes, only when there are notes; and "open": up to 3 points still open from it. For a coming event that needs work first, add "prep": up to 6 steps, and "prep_done": how many of them are already done. Read only titles, times, attendee counts and notes. Never copy an invitation's text. Wispr Flow: today's and tomorrow's meeting notes. Jira: issues assigned to the PM or due soon. To tie a Jira issue to an initiative, run `pmos jira which <KEY>` (it reads local files only) and use the initiative it names. A status change of a ticket that `pmos jira which` names is not an item: the Jira status check shows it on the initiative's Jira tickets tab. Confluence: pages changed since yesterday. Mail, only if on: subjects and senders of threads waiting on the PM, no bodies.
|
|
33
34
|
- GitHub, if on: pull requests that wait on the PM for review, pull requests of the PM that have a review or a failed check, and issues assigned to the PM. Keep the repository, the number, the title and the state.
|
|
34
35
|
- Drive, if on: files shared with the PM or changed since yesterday that a meeting or a goal names. Keep the name, the owner and the link.
|
|
35
36
|
- Figma, if on: files the PM owns or was asked to look at, with new comments since yesterday. Keep the file, the commenter and a short paraphrase of the comment.
|
|
@@ -72,8 +73,12 @@ Procedure:
|
|
|
72
73
|
"link": "https://…", "where": "#pm-team", "who": "…", "about": "…", "ask": "…", "reply_needed": true, "how": "…", "kind": "you|approve|auto"}],
|
|
73
74
|
"digests": [{"source": "slack|jira|confluence", "name": "#pm-team", "link": "https://…", "decisions": ["…"], "open": ["…"], "goals": ["…"]}],
|
|
74
75
|
"places": [{"source": "slack|jira|confluence", "name": "#pm-team", "link": "https://…"}],
|
|
76
|
+
"memory": {"from": "YYYY-MM-DD", "to": "YYYY-MM-DD",
|
|
77
|
+
"events": [{"start": "<ISO>", "end": "<ISO>", "title": "…", "series": "<recurring id or omit>", "attendees": 2, "external": false, "exec": false, "role": "present|decide|attend",
|
|
78
|
+
"notes": "<ended only>", "open": ["…"], "prep": ["<coming only>"], "prep_done": 0}]},
|
|
75
79
|
"judgment": {"used": false}}
|
|
76
80
|
```
|
|
81
|
+
"memory" is optional: give it only when calendar is "read". Its "from" and "to" are the window you read, and every event's start is inside it. The workspace replaces the days of that window in what it remembers.
|
|
77
82
|
"places" holds at most 200 entries, each a name of at most 120 characters, only for a source that is "read". A digest has at most 5 decisions, 5 open questions and 5 goal lines, each at most 140 characters, and at most 30 digests. Write a digest only for a place with scope watch, and only for a source that is "read". Only report calendar events if calendar is "read". Only give an item a source that is "read" (or "derived"). Only use a goal id that `pmos proactive goals` listed today: the workspace refuses any other. Leave out a field you cannot fill: an empty string is worse than no field.
|
|
78
83
|
5. Run `pmos proactive brief submit --file <that file>`. If it is refused, read the reason, fix the file and submit once more. Do not work around a refusal.
|
|
79
84
|
6. Finish by saying in three lines how many goals have a step and which have none, how many priorities and worth-asking items you submitted, and which sources were not reached.
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"modules": [
|
|
11
11
|
"proactive.test.js",
|
|
12
12
|
"goals.test.js",
|
|
13
|
+
"agenda.test.js",
|
|
13
14
|
"day-run.test.js",
|
|
14
15
|
"focus.test.js",
|
|
15
16
|
"handoff.test.js",
|
|
@@ -109,6 +110,7 @@
|
|
|
109
110
|
"test.js": 48,
|
|
110
111
|
"proactive.test.js": 4,
|
|
111
112
|
"goals.test.js": 6,
|
|
113
|
+
"agenda.test.js": 5,
|
|
112
114
|
"day-run.test.js": 6,
|
|
113
115
|
"focus.test.js": 1,
|
|
114
116
|
"handoff.test.js": 1,
|