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.
Files changed (43) hide show
  1. package/package.json +1 -1
  2. package/payload/KIT-VERSION +1 -1
  3. package/payload/planning/evals/eval-calibration.md +1 -1
  4. package/payload/planning/okrs/health-budgets.md +1 -1
  5. package/payload/scripts/delivery-index.js +85 -7
  6. package/payload/scripts/workflow-state.js +3 -3
  7. package/payload/state/discoveries.md +1 -1
  8. package/workspace/fixtures/initiatives-api/sheet.json +29 -7
  9. package/workspace/guide/index.md +3 -3
  10. package/workspace/guide/initiatives-screen.md +25 -1
  11. package/workspace/guide/layout.md +6 -6
  12. package/workspace/guide/proactive-engine.md +1 -0
  13. package/workspace/guide/today.md +23 -1
  14. package/workspace/lib/calendar-memory.js +257 -0
  15. package/workspace/lib/console-state.js +1 -1
  16. package/workspace/lib/doc-category.js +3 -1
  17. package/workspace/lib/engine/delivery-index.js +85 -7
  18. package/workspace/lib/engine/workflow-state.js +3 -3
  19. package/workspace/lib/initiative-files.js +59 -1
  20. package/workspace/lib/initiative-rows.js +14 -2
  21. package/workspace/lib/mobile-nav.js +1 -1
  22. package/workspace/lib/plan-tree.js +44 -5
  23. package/workspace/lib/proactive-cli.js +16 -1
  24. package/workspace/lib/routes-initiatives.js +165 -9
  25. package/workspace/package-lock.json +1 -1
  26. package/workspace/package.json +1 -1
  27. package/workspace/public/index.html +1 -1
  28. package/workspace/public/pm/components.css +70 -0
  29. package/workspace/public/pm/dest-initiatives.js +7 -1
  30. package/workspace/public/pm/frame-copy.js +2 -2
  31. package/workspace/public/pm/initiative-sheet.js +42 -10
  32. package/workspace/public/pm/initiatives-api.js +15 -0
  33. package/workspace/public/pm/initiatives-copy.js +13 -0
  34. package/workspace/public/pm/initiatives-ui.js +14 -0
  35. package/workspace/public/pm/registry.js +1 -1
  36. package/workspace/public/pm/today-agenda.js +305 -0
  37. package/workspace/public/pm/today-copy.js +54 -2
  38. package/workspace/public/pm/today-day.js +23 -3
  39. package/workspace/public/pm/today-goals.js +7 -1
  40. package/workspace/server.js +53 -2
  41. package/workspace/skills/morning-brief.skill +6 -1
  42. package/workspace/test-files.json +2 -0
  43. 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 panel('calendar', null, el('ul', { class: 'td-day__rows', 'data-day-list': 'calendar' }, ...rows));
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), calendar(ctx), jobsPanel(ctx), mentionsPanel(ctx), waitingPanel(owed)),
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;
@@ -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,
@@ -0,0 +1,5 @@
1
+ {
2
+ "_comment": "scan-consistency: delivery attribution by change, stage frontier, Files sources and the breakdown read from the spec.",
3
+ "modules": ["scan-consistency.test.js"],
4
+ "seconds": { "scan-consistency.test.js": 10 }
5
+ }