create-pmos 0.92.0 → 0.94.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/first-paint-audit.js +2 -1
- package/workspace/guide/notes.md +1 -1
- package/workspace/guide/today.md +1 -1
- package/workspace/lib/kit-update.js +8 -0
- package/workspace/lib/report-needs.js +8 -2
- package/workspace/lib/routes-notes.js +12 -1
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/app.js +2 -1
- package/workspace/public/pm/components.css +172 -1
- package/workspace/public/pm/dest-reports.js +80 -28
- package/workspace/public/pm/mobile.js +1 -1
- package/workspace/public/pm/note-dock.js +1 -0
- package/workspace/public/pm/panel-cockpit.js +3 -1
- package/workspace/public/pm/panel-tabs.js +10 -3
- package/workspace/public/pm/primitives.js +2 -2
- package/workspace/public/pm/report-tab.js +33 -0
- package/workspace/public/pm/reports-copy.js +12 -0
- package/workspace/public/pm/today-copy.js +2 -0
- package/workspace/public/pm/today-day.js +43 -2
- package/workspace/public/pm/topbar.js +16 -2
- package/workspace/public/pm/view-notes-menu.js +190 -0
- package/workspace/public/pm/view-notes.js +5 -17
- package/workspace/skills/report-new.skill +27 -0
- package/workspace/skills/report-template.skill +34 -0
- package/workspace/test-files.json +2 -0
|
@@ -244,7 +244,7 @@ function go(screen) {
|
|
|
244
244
|
}
|
|
245
245
|
|
|
246
246
|
const frameOn = () => !!(ctx && ctx.frame);
|
|
247
|
-
const tabScreen = (tab, id) => ({ kind: (tab && (tab.kind === 'doc' || tab.kind === 'breakdown' || tab.kind === 'dest')) ? 'doc' : 'terminal', id, title: (tab && (tab.title || tab.name)) || String(id) });
|
|
247
|
+
const tabScreen = (tab, id) => ({ kind: (tab && (tab.kind === 'doc' || tab.kind === 'breakdown' || tab.kind === 'dest' || tab.kind === 'report')) ? 'doc' : 'terminal', id, title: (tab && (tab.title || tab.name)) || String(id) });
|
|
248
248
|
|
|
249
249
|
/* The frame on a phone: the PM's own moves through the panels, the palette or Settings, read back as screens. A place
|
|
250
250
|
* opened from elsewhere (⌘K, Settings › Show its initiatives) is a tab switch; Settings is pushed on top; an open
|
|
@@ -42,6 +42,7 @@ const liveId = () => (V ? V.listeningId() : null);
|
|
|
42
42
|
const titleOf = () => { if (!active) return ''; const r = noteById(active.id); return (V && V.titleOf(active.id)) || (r && r.title) || active.title || 'Untitled note'; };
|
|
43
43
|
|
|
44
44
|
export const activeId = () => (active ? active.id : null);
|
|
45
|
+
export const isRecording = () => !!(active && V && liveId() === active.id);
|
|
45
46
|
|
|
46
47
|
export function mountDock() {
|
|
47
48
|
if (host || FIXTURE || typeof document === 'undefined') return;
|
|
@@ -46,6 +46,8 @@ export const VIEWS = {
|
|
|
46
46
|
livenotes: { label: 'live notes', mode: null, scoped: true, palette: true, hint: 'confirm talking points' },
|
|
47
47
|
// meeting-notes Run 1: typed notes, dictation, meeting transcripts and summaries; the PM's own, the same in every repository
|
|
48
48
|
notes: { label: 'notes', mode: null, scoped: false, palette: true, hint: 'type, dictate or record a meeting' },
|
|
49
|
+
// meeting-notes Run 11: notes menu with two-column view grouped by initiative (R47)
|
|
50
|
+
notesmenu: { label: 'notes menu', mode: null, scoped: false, hint: 'view and organize all notes by initiative' },
|
|
49
51
|
};
|
|
50
52
|
|
|
51
53
|
/** The view a list mode opens as, or null when the mode is not a view. `product` is the list itself. */
|
|
@@ -95,7 +97,7 @@ function modes() {
|
|
|
95
97
|
* every `data-*` hook the suites use, and only adds the board's frame around it. A view with no board module draws as before. */
|
|
96
98
|
export const BOARD_MODULES = {
|
|
97
99
|
today: './view-today.js', validate: './view-validate.js', addrepo: './view-addrepo.js', livenotes: './view-livenotes.js',
|
|
98
|
-
notes: './view-notes.js',
|
|
100
|
+
notes: './view-notes.js', notesmenu: './view-notes-menu.js',
|
|
99
101
|
};
|
|
100
102
|
const boardMods = {}; // view -> undefined (not tried) · null (loading or absent) · module
|
|
101
103
|
function boardOf(view) {
|
|
@@ -63,6 +63,11 @@ import('./panel-breakdown.js')
|
|
|
63
63
|
.then((m) => { breakdownMod = m; if ((state.tabs || []).some((t) => t.kind === 'breakdown')) setState({}); })
|
|
64
64
|
.catch(() => { breakdownMod = null; });
|
|
65
65
|
|
|
66
|
+
let reportTabMod = null;
|
|
67
|
+
import('./report-tab.js')
|
|
68
|
+
.then((m) => { reportTabMod = m; if ((state.tabs || []).some((t) => t.kind === 'report')) setState({}); })
|
|
69
|
+
.catch(() => { reportTabMod = null; });
|
|
70
|
+
|
|
66
71
|
/** `{{ expr }}` — one interpolated value in its own span, the way the prototype's runtime emits it. */
|
|
67
72
|
/* A dock control's face: a lucide icon from the vendored sprite, then its word (C6: no Unicode glyph controls). */
|
|
68
73
|
const iconLabel = (name, text) => (text ? [icon(name), el('span', null, text)] : [icon(name)]);
|
|
@@ -1043,6 +1048,8 @@ function paneFor(tab) {
|
|
|
1043
1048
|
if (cockpitMod && typeof cockpitMod.mountView === 'function') cockpitMod.mountView(host, tab);
|
|
1044
1049
|
} else if (tab.kind === 'web') {
|
|
1045
1050
|
mountWeb(host, tab); // overhaul-r5 — never falls through to an empty doc tab
|
|
1051
|
+
} else if (tab.kind === 'report') {
|
|
1052
|
+
if (reportTabMod && typeof reportTabMod.mountReportTab === 'function') reportTabMod.mountReportTab(host, tab);
|
|
1046
1053
|
} else if (tab.kind === 'breakdown') {
|
|
1047
1054
|
// pm-os-absorb Run 1 — an initiative's items (Tree, Board, item sheet); an empty pane until the module lands
|
|
1048
1055
|
if (breakdownMod && typeof breakdownMod.mountBreakdown === 'function') breakdownMod.mountBreakdown(host, tab);
|
|
@@ -1205,7 +1212,7 @@ function cockpitTabNode(t, act, splitTab, vertical) {
|
|
|
1205
1212
|
const dotColor = FX.TAB_COLORS.dot[t.kind] || FX.TAB_COLORS.dot.doc;
|
|
1206
1213
|
const meta = t.kind === 'term'
|
|
1207
1214
|
? ((!FIXTURE && t.sessionId) ? 'session ' + t.sessionId : 'session')
|
|
1208
|
-
: (t.kind === 'view' ? 'view' : t.kind === 'web' ? 'web' : t.kind === 'skill' ? 'skill' : t.kind === 'transcript' ? 'conversation' : t.role);
|
|
1215
|
+
: (t.kind === 'view' ? 'view' : t.kind === 'web' ? 'web' : t.kind === 'skill' ? 'skill' : t.kind === 'transcript' ? 'conversation' : t.kind === 'report' ? 'report' : t.role);
|
|
1209
1216
|
const br = tabBranch(t);
|
|
1210
1217
|
return el('div', {
|
|
1211
1218
|
'data-tab': t.id, 'data-tab-active': front ? '1' : '0',
|
|
@@ -1242,7 +1249,7 @@ function cockpitTabRow(t, front) {
|
|
|
1242
1249
|
const dotColor = FX.TAB_COLORS.dot[t.kind] || FX.TAB_COLORS.dot.doc;
|
|
1243
1250
|
const meta = t.kind === 'term'
|
|
1244
1251
|
? ((!FIXTURE && t.sessionId) ? 'session ' + t.sessionId : 'session')
|
|
1245
|
-
: (t.kind === 'view' ? 'view' : t.kind === 'web' ? 'web' : t.kind === 'skill' ? 'skill' : t.kind === 'transcript' ? 'conversation' : t.role);
|
|
1252
|
+
: (t.kind === 'view' ? 'view' : t.kind === 'web' ? 'web' : t.kind === 'skill' ? 'skill' : t.kind === 'transcript' ? 'conversation' : t.kind === 'report' ? 'report' : t.role);
|
|
1246
1253
|
const br = tabBranch(t);
|
|
1247
1254
|
return el('div', {
|
|
1248
1255
|
'data-tab': t.id, 'data-tab-active': front ? '1' : '0', 'data-tab-row': '',
|
|
@@ -1319,7 +1326,7 @@ function tabNode(t, act, splitTab) {
|
|
|
1319
1326
|
* fixture render keeps the design's literal — the comparator reads this text node. */
|
|
1320
1327
|
const meta = t.kind === 'term'
|
|
1321
1328
|
? ((!FIXTURE && t.sessionId) ? 'session ' + t.sessionId : 'session')
|
|
1322
|
-
: (t.kind === 'view' ? 'view' : t.kind === 'web' ? 'web' : t.kind === 'skill' ? 'skill' : t.kind === 'transcript' ? 'conversation' : t.role);
|
|
1329
|
+
: (t.kind === 'view' ? 'view' : t.kind === 'web' ? 'web' : t.kind === 'skill' ? 'skill' : t.kind === 'transcript' ? 'conversation' : t.kind === 'report' ? 'report' : t.role);
|
|
1323
1330
|
|
|
1324
1331
|
return el('div', {
|
|
1325
1332
|
style: 'flex:none;display:flex;align-items:center;gap:8px;padding:0 10px;background:' + bg
|
|
@@ -289,7 +289,7 @@ export function card({ title, titleIcon, meta, signals, lead, trail, onOpen, ope
|
|
|
289
289
|
/* ── tabs ────────────────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
290
290
|
|
|
291
291
|
/**
|
|
292
|
-
* tabs({ variant: 'segmented'|'line', role: 'tablist'|'radiogroup', items: [{ id, label, count? }], value, onChange(id),
|
|
292
|
+
* tabs({ variant: 'segmented'|'line', role: 'tablist'|'radiogroup', items: [{ id, label, count?, icon? }], value, onChange(id),
|
|
293
293
|
* ariaLabel, full? })
|
|
294
294
|
* Arrow keys move focus between tabs. In a tablist the focused tab is also selected; in a radiogroup (the item sheet's
|
|
295
295
|
* state buttons, where a press writes) selection needs Space or Enter, so moving focus writes nothing.
|
|
@@ -303,7 +303,7 @@ export function tabs({ variant = 'segmented', role = 'tablist', items, value, on
|
|
|
303
303
|
'aria-selected': radio ? null : (on ? 'true' : 'false'), 'aria-checked': radio ? (on ? 'true' : 'false') : null,
|
|
304
304
|
tabindex: on || (value == null && it === items[0]) ? '0' : '-1',
|
|
305
305
|
onClick: () => { if (onChange) onChange(it.id); },
|
|
306
|
-
}, el('span', null, it.label), it.count !== undefined && it.count !== null ? el('span', { class: 'pmc-tabs__count' }, String(it.count)) : null);
|
|
306
|
+
}, it.icon ? icon(it.icon) : null, el('span', null, it.label), it.count !== undefined && it.count !== null ? el('span', { class: 'pmc-tabs__count' }, String(it.count)) : null);
|
|
307
307
|
});
|
|
308
308
|
const box = el('div', { role, 'aria-label': ariaLabel, ...withClass(cx('pmc-tabs', 'pmc-tabs--' + variant, full && 'pmc-tabs--full'), attrs) }, btns);
|
|
309
309
|
box.addEventListener('keydown', (e) => {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/* pm/report-tab.js — a full report, opened as a tab in the dock (reports-redesign follow-up).
|
|
2
|
+
*
|
|
3
|
+
* The dock tab reads the same draft the Reports viewer reads, and draws it whole: the report as a document,
|
|
4
|
+
* not its slide cards. Print and Copy are the viewer's own actions. The tab writes nothing; a report is read here
|
|
5
|
+
* and changed only from the Reports page, where the lock applies.
|
|
6
|
+
*
|
|
7
|
+
* mountReportTab(host, tab) tab: { id, ws, cadence, slot, title }
|
|
8
|
+
*/
|
|
9
|
+
import { el } from './dom.js';
|
|
10
|
+
import { button, ensureComponentsCss } from './primitives.js';
|
|
11
|
+
import { pmGet } from './http.js';
|
|
12
|
+
import { COPY } from './reports-copy.js';
|
|
13
|
+
import { renderReport, printReport, copyMarkdown } from './report-view.js';
|
|
14
|
+
|
|
15
|
+
export function mountReportTab(host, tab) {
|
|
16
|
+
const root = host.firstElementChild;
|
|
17
|
+
if (root && root.getAttribute('data-report-tab') === tab.id) return;
|
|
18
|
+
ensureComponentsCss();
|
|
19
|
+
const page = el('div', { class: 'rp-page rp-tab', 'data-report-tab': tab.id, style: 'flex:1;min-height:0;overflow:auto;padding:12px 16px' });
|
|
20
|
+
host.replaceChildren(page);
|
|
21
|
+
page.appendChild(el('p', { class: 'rp-section__empty' }, COPY.loading));
|
|
22
|
+
const q = '/api/report/draft?ws=' + encodeURIComponent(tab.ws) + '&cadence=' + encodeURIComponent(tab.cadence) + '&slot=' + encodeURIComponent(tab.slot);
|
|
23
|
+
pmGet(q).then((out) => {
|
|
24
|
+
if (!page.isConnected) return;
|
|
25
|
+
if (!out || !out.ok || !out.exists) { page.replaceChildren(el('p', { class: 'rp-section__empty rp-section__empty--bad', role: 'alert' }, COPY.readError)); return; }
|
|
26
|
+
const acts = el('div', { class: 'rp-toolbar__acts' },
|
|
27
|
+
button({ label: COPY.print, variant: 'outline', size: 'sm', onClick: () => printReport(out.markdown) }),
|
|
28
|
+
button({ label: COPY.copyMd, variant: 'outline', size: 'sm', onClick: () => copyMarkdown(out.markdown) }));
|
|
29
|
+
page.replaceChildren(el('div', { class: 'rp-toolbar' }, el('h3', { class: 'rp-section__title rp-toolbar__title' }, tab.title || ''), acts), renderReport(out.markdown));
|
|
30
|
+
}).catch(() => {
|
|
31
|
+
if (page.isConnected) page.replaceChildren(el('p', { class: 'rp-section__empty rp-section__empty--bad', role: 'alert' }, COPY.readError));
|
|
32
|
+
});
|
|
33
|
+
}
|
|
@@ -48,6 +48,8 @@ export const COPY = {
|
|
|
48
48
|
reportName: (cadence, slot) => cadenceName(cadence) + ' report, ' + shortDate(slot),
|
|
49
49
|
rowTitle: (cadence, slot) => cadenceName(cadence) + ' · ' + shortDate(slot),
|
|
50
50
|
draft: 'Draft',
|
|
51
|
+
issuedState: 'Issued',
|
|
52
|
+
typeSummary: (cadence, state) => 'Slides · ' + cadenceName(cadence) + ' · ' + state,
|
|
51
53
|
issued: (at, by) => 'Issued ' + (dayOf(at) || '') + (by ? ' by ' + by : ''),
|
|
52
54
|
edited: 'Edited after issue',
|
|
53
55
|
editedWhy: 'This file changed after it was issued. The issued bytes are in the issues ledger.',
|
|
@@ -94,6 +96,10 @@ export const COPY = {
|
|
|
94
96
|
schedulesFailed: 'The schedules were not changed.',
|
|
95
97
|
pause: 'Pause',
|
|
96
98
|
resume: 'Resume',
|
|
99
|
+
runNow: 'Run now',
|
|
100
|
+
nextRun: (iso) => (dayOf(iso) ? 'next ' + dayOf(iso) : ''),
|
|
101
|
+
oneOff: (cadence) => 'No schedule drafts this ' + cadence + ' report. Make it recurring to draft it on a schedule.',
|
|
102
|
+
makeRecurring: 'Make recurring',
|
|
97
103
|
paused: 'Paused',
|
|
98
104
|
pausedMsg: (name) => name + ' is paused.',
|
|
99
105
|
resumed: (name) => name + ' runs again.',
|
|
@@ -105,7 +111,13 @@ export const COPY = {
|
|
|
105
111
|
|
|
106
112
|
flagTitle: 'The issues ledger has a line that is not read',
|
|
107
113
|
|
|
114
|
+
openTab: 'Open as tab',
|
|
115
|
+
newReport: 'New report',
|
|
108
116
|
present: 'Present',
|
|
117
|
+
lock: 'Lock',
|
|
118
|
+
unlock: 'Unlock',
|
|
119
|
+
locked: 'Locked. Edit notes, Redraft and Issue are off until you unlock.',
|
|
120
|
+
fullScreen: 'Full screen',
|
|
109
121
|
};
|
|
110
122
|
|
|
111
123
|
export const GLOSSARY = {
|
|
@@ -138,6 +138,8 @@ export const COPY = {
|
|
|
138
138
|
unreached: 'The workspace did not answer.',
|
|
139
139
|
refused: (why) => `Nothing changed. ${why}`,
|
|
140
140
|
oldServer: 'This workspace server has no day run yet. Only your goals show.',
|
|
141
|
+
sections: { label: 'Agenda sections', agenda: 'Agenda', goals: 'Goals', jobs: 'Jobs', mentions: 'Mentions', summary: 'Result' },
|
|
142
|
+
noResult: 'No agent results yet.',
|
|
141
143
|
panels: { summary: 'Agent results', calendar: 'Calendar', agenda: 'Agenda', jobs: 'Jobs', goals: 'Goals for today', mentions: 'Mentions since yesterday', detail: 'Detail' },
|
|
142
144
|
summary: (c) => [c.review ? `${c.review} ${c.review === 1 ? 'draft waits' : 'drafts wait'} for you` : null, c.done ? `${c.done} done` : null,
|
|
143
145
|
c.working ? `${c.working} working` : null, c.failed ? `${c.failed} failed` : null].filter(Boolean).join(' · ') || 'No agent job yet.',
|
|
@@ -50,6 +50,7 @@ const store = {
|
|
|
50
50
|
// A new key: a fold stored by the old Today page must not hide the Agenda month.
|
|
51
51
|
const OPEN_KEY = 'pmos.agenda.day.open';
|
|
52
52
|
const SEEN_KEY = 'pmos.today.day.seen';
|
|
53
|
+
const SECTION_KEY = 'pm.today.section';
|
|
53
54
|
let seen = new Set();
|
|
54
55
|
try { seen = new Set(JSON.parse(store.get(SEEN_KEY) || '[]')); } catch { seen = new Set(); }
|
|
55
56
|
|
|
@@ -57,6 +58,7 @@ const cache = new Map(); // ws -> { data, error }
|
|
|
57
58
|
const runs = new Map(); // ws -> the last "Prepare my day" press: { state: 'busy'|'started'|'running'|'failed', at, session, error }
|
|
58
59
|
const ui = {
|
|
59
60
|
open: (() => { const v = store.get(OPEN_KEY); return v === 'true' ? true : v === 'false' ? false : null; })(),
|
|
61
|
+
section: (() => { const v = store.get(SECTION_KEY); return ['agenda', 'goals', 'jobs', 'mentions', 'summary'].includes(v) ? v : 'agenda'; })(),
|
|
60
62
|
sel: null, mtab: 'all', openSrc: null, showIgn: {}, showIgnDigests: false,
|
|
61
63
|
revising: new Set(), notes: new Map(), siteDraft: '', placeDraft: {}, busy: new Set(), scroll: false,
|
|
62
64
|
// today-act-prepare: the folded groups the PM opened, the item whose hold form is open, and its draft
|
|
@@ -380,6 +382,10 @@ async function setSettings(ws, patch) {
|
|
|
380
382
|
function select(type, id) {
|
|
381
383
|
ui.sel = type ? { type, id: String(id) } : null;
|
|
382
384
|
if (type === 'job' && !seen.has(id)) { seen.add(id); store.set(SEEN_KEY, JSON.stringify([...seen].slice(-200))); }
|
|
385
|
+
// Reveal the appropriate section when selecting an item
|
|
386
|
+
if (type === 'job') { ui.section = 'jobs'; }
|
|
387
|
+
else if (type === 'goal') { ui.section = 'goals'; }
|
|
388
|
+
else if (type === 'mention' || type === 'channel') { ui.section = 'mentions'; }
|
|
383
389
|
ui.scroll = true;
|
|
384
390
|
rerender();
|
|
385
391
|
}
|
|
@@ -1188,6 +1194,41 @@ function fold(open) {
|
|
|
1188
1194
|
rerender();
|
|
1189
1195
|
}
|
|
1190
1196
|
|
|
1197
|
+
/** Render the section selector tabs. */
|
|
1198
|
+
function sectionSelector(ctx) {
|
|
1199
|
+
const { m } = ctx;
|
|
1200
|
+
const openGoals = m.allGoals.filter((g) => g.status === 'open').length;
|
|
1201
|
+
const items = [
|
|
1202
|
+
{ id: 'agenda', icon: 'calendar', label: D.sections.agenda, count: null },
|
|
1203
|
+
{ id: 'goals', icon: 'target', label: D.sections.goals, count: openGoals },
|
|
1204
|
+
{ id: 'jobs', icon: 'list-checks', label: D.sections.jobs, count: m.jobs.length },
|
|
1205
|
+
{ id: 'mentions', icon: 'text-quote', label: D.sections.mentions, count: m.mentions.length },
|
|
1206
|
+
{ id: 'summary', icon: 'bot', label: D.sections.summary, count: null },
|
|
1207
|
+
];
|
|
1208
|
+
return tabs({ variant: 'segmented', role: 'tablist', ariaLabel: D.sections.label, value: ui.section, full: true, attrs: { 'data-day': 'section-tabs' },
|
|
1209
|
+
items,
|
|
1210
|
+
onChange: (v) => { ui.section = v; store.set(SECTION_KEY, v); rerender(); } });
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
/** Render the chosen section's content. */
|
|
1214
|
+
function sectionContent(ctx) {
|
|
1215
|
+
const { m } = ctx;
|
|
1216
|
+
switch (ui.section) {
|
|
1217
|
+
case 'agenda':
|
|
1218
|
+
return agenda(ctx);
|
|
1219
|
+
case 'goals':
|
|
1220
|
+
return goalsPanel(ctx);
|
|
1221
|
+
case 'jobs':
|
|
1222
|
+
return jobsPanel(ctx);
|
|
1223
|
+
case 'mentions':
|
|
1224
|
+
return mentionsPanel(ctx);
|
|
1225
|
+
case 'summary':
|
|
1226
|
+
return summaryPanel(ctx) || note(D.noResult, { 'data-day': 'no-result' });
|
|
1227
|
+
default:
|
|
1228
|
+
return agenda(ctx);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1191
1232
|
/** The section. `ws` is the repository the day run belongs to; `openIni(id)` opens an initiative of it. */
|
|
1192
1233
|
export function daySection({ ws, setState, openIni, owed, here, all } = {}) {
|
|
1193
1234
|
if (FIXTURE) return null;
|
|
@@ -1230,12 +1271,12 @@ export function daySection({ ws, setState, openIni, owed, here, all } = {}) {
|
|
|
1230
1271
|
c.error ? el('p', { class: 'td-day__run td-day__run--failed', role: 'status', 'data-day': 'error' }, D.refused(c.error)) : null,
|
|
1231
1272
|
m.d && m.d.legacy ? note(D.oldServer) : null,
|
|
1232
1273
|
el('div', { class: 'td-day__cols' },
|
|
1233
|
-
el('div', { class: 'td-day__left' },
|
|
1274
|
+
el('div', { class: 'td-day__left' }, sectionSelector(ctx), sectionContent(ctx)),
|
|
1234
1275
|
detail(ctx)));
|
|
1235
1276
|
}
|
|
1236
1277
|
|
|
1237
1278
|
export function daySig(ws) {
|
|
1238
1279
|
const c = cache.get(ws) || {};
|
|
1239
|
-
return JSON.stringify([forced, ui.open, ui.sel, ui.mtab, ui.openSrc, ui.showIgn, ui.showIgnDigests, [...ui.revising], [...ui.busy], [...ui.groupsOpen], ui.holding, ui.holdDraft.when, [...ui.verdict], c.error, runs.get(ws) || null, [...seen].length])
|
|
1280
|
+
return JSON.stringify([forced, ui.open, ui.section, ui.sel, ui.mtab, ui.openSrc, ui.showIgn, ui.showIgnDigests, [...ui.revising], [...ui.busy], [...ui.groupsOpen], ui.holding, ui.holdDraft.when, [...ui.verdict], c.error, runs.get(ws) || null, [...seen].length])
|
|
1240
1281
|
+ dataSig(c.data) + goalsSig() + agendaSig();
|
|
1241
1282
|
}
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
import { el, setChildren } from './dom.js';
|
|
22
22
|
import { button, needsDot, needsCount } from './primitives.js';
|
|
23
23
|
import { COPY } from './frame-copy.js';
|
|
24
|
+
import { isRecording } from './note-dock.js';
|
|
24
25
|
|
|
25
26
|
/** The destinations the bar names, in order. Settings sits at the end of the bar, after Sync. Docs joined the bar at the
|
|
26
27
|
* PM's word (2026-10-06, docs-three-way-sync: "I thought Docs is one of the main menu"). People joined after Docs when the
|
|
@@ -39,13 +40,19 @@ export function unsyncedOf(st) {
|
|
|
39
40
|
/** The lucide symbol each place wears in the bar (UI comfort, 2026-10-07: a word alone is slow to find; the phone's tab bar already wears these). */
|
|
40
41
|
const NAV_ICON = Object.freeze({ today: 'house', mywork: 'list-checks', initiatives: 'target', board: 'square-kanban', reports: 'chart-column', docs: 'file-text', people: 'users', settings: 'settings' });
|
|
41
42
|
|
|
42
|
-
export function createTopbar({ go, openPalette, repoMenu, sync } = {}) {
|
|
43
|
+
export function createTopbar({ go, openPalette, openView, repoMenu, sync } = {}) {
|
|
43
44
|
const node = el('header', { class: 'fr-bar', 'data-shell': 'top', 'data-fr': 'bar' });
|
|
44
45
|
|
|
45
46
|
let repoBtn = null;
|
|
46
47
|
let repoLabel = null;
|
|
47
48
|
const repoSlot = el('div', { class: 'fr-bar__repo' });
|
|
48
49
|
|
|
50
|
+
const recordingIndicator = el('span', {
|
|
51
|
+
class: 'fr-recording-dot', 'data-fr-recording': '',
|
|
52
|
+
title: 'Notes are being recorded',
|
|
53
|
+
'aria-hidden': 'true',
|
|
54
|
+
});
|
|
55
|
+
|
|
49
56
|
const count = needsCount('0', { 'data-today-badge': '' });
|
|
50
57
|
const dot = needsDot();
|
|
51
58
|
const links = {};
|
|
@@ -80,6 +87,12 @@ export function createTopbar({ go, openPalette, repoMenu, sync } = {}) {
|
|
|
80
87
|
});
|
|
81
88
|
search.appendChild(el('kbd', { class: 'fr-kbd' }, COPY.searchKey));
|
|
82
89
|
|
|
90
|
+
const notes = button({
|
|
91
|
+
label: 'Notes', variant: 'outline', icon: 'notebook-pen',
|
|
92
|
+
attrs: { class: 'fr-notes', 'data-fr-view': 'notes', title: 'Open notes menu (all notes by initiative)' },
|
|
93
|
+
onClick: () => openView && openView('notesmenu'),
|
|
94
|
+
});
|
|
95
|
+
|
|
83
96
|
let syncBtn = null;
|
|
84
97
|
let syncLabel = null;
|
|
85
98
|
|
|
@@ -116,7 +129,8 @@ export function createTopbar({ go, openPalette, repoMenu, sync } = {}) {
|
|
|
116
129
|
syncLabel = want;
|
|
117
130
|
syncBtn = want ? syncButton(want, !!model.syncing) : null;
|
|
118
131
|
}
|
|
119
|
-
|
|
132
|
+
recordingIndicator.classList.toggle('fr-recording-dot--active', isRecording());
|
|
133
|
+
setChildren(node, [repoSlot, recordingIndicator, nav, search, notes, syncBtn, links.settings]);
|
|
120
134
|
}
|
|
121
135
|
|
|
122
136
|
update({});
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/* pm/view-notes-menu.js — the Notes menu: all notes grouped by initiative (Run 11, R47).
|
|
2
|
+
*
|
|
3
|
+
* Two-column layout:
|
|
4
|
+
* left all notes grouped by initiative; click an initiative title to collapse/expand;
|
|
5
|
+
* click a note to open its detail on the right
|
|
6
|
+
* right the note detail (same single column as the full-screen note, Run 7)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { el, setChildren } from './dom.js';
|
|
10
|
+
import { askConfirm } from './dialogs.js';
|
|
11
|
+
import { pmGet, pmPost } from './http.js';
|
|
12
|
+
import { icon } from './icons.js';
|
|
13
|
+
import { button, iconButton, empty, toast, ensureComponentsCss } from './primitives.js';
|
|
14
|
+
import * as viewNotes from './view-notes.js';
|
|
15
|
+
|
|
16
|
+
ensureComponentsCss();
|
|
17
|
+
|
|
18
|
+
let cache = new Map();
|
|
19
|
+
let list = null;
|
|
20
|
+
let selectedId = null;
|
|
21
|
+
let expandedInitiatives = new Set();
|
|
22
|
+
|
|
23
|
+
async function loadList() {
|
|
24
|
+
const r = await pmGet('/api/notes');
|
|
25
|
+
if (r && r.ok) {
|
|
26
|
+
list = { notes: r.notes || [] };
|
|
27
|
+
} else {
|
|
28
|
+
list = { notes: [], error: (r && r.error) || 'the notes could not be read' };
|
|
29
|
+
}
|
|
30
|
+
return list;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function loadNote(id) {
|
|
34
|
+
const r = await pmGet('/api/notes/one?id=' + encodeURIComponent(id));
|
|
35
|
+
if (r && r.ok) {
|
|
36
|
+
cache.set(id, r.note);
|
|
37
|
+
return r.note;
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function groupByInitiative(notes) {
|
|
43
|
+
const groups = {};
|
|
44
|
+
notes.forEach((n) => {
|
|
45
|
+
const key = n.initiative || '(no initiative)';
|
|
46
|
+
if (!groups[key]) groups[key] = [];
|
|
47
|
+
groups[key].push(n);
|
|
48
|
+
});
|
|
49
|
+
return Object.entries(groups).sort((a, b) => {
|
|
50
|
+
if (a[0] === '(no initiative)') return 1;
|
|
51
|
+
if (b[0] === '(no initiative)') return -1;
|
|
52
|
+
return a[0].localeCompare(b[0]);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function renderNoteRow(note, isSelected, onClick) {
|
|
57
|
+
const summary = note.summary || '';
|
|
58
|
+
const summaryText = summary
|
|
59
|
+
.split('\n')
|
|
60
|
+
.slice(0, 2)
|
|
61
|
+
.map((line) => line.trim())
|
|
62
|
+
.filter(Boolean)
|
|
63
|
+
.join(' ')
|
|
64
|
+
.substring(0, 60);
|
|
65
|
+
|
|
66
|
+
return el('div', {
|
|
67
|
+
class: 'nt-menu-row' + (isSelected ? ' nt-menu-row--selected' : ''),
|
|
68
|
+
'data-nt-menu-row': note.id,
|
|
69
|
+
onClick,
|
|
70
|
+
},
|
|
71
|
+
el('div', { class: 'nt-menu-row-title' }, note.title || 'Untitled note'),
|
|
72
|
+
el('div', { class: 'nt-menu-row-date' }, new Date(note.updated_at).toLocaleDateString()),
|
|
73
|
+
summaryText && el('div', { class: 'nt-menu-row-summary' }, summaryText),
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function renderLeftPanel(notes, selectedId, onSelectNote, onToggleInitiative) {
|
|
78
|
+
const groups = groupByInitiative(notes);
|
|
79
|
+
|
|
80
|
+
return el('div', { class: 'nt-menu-left' },
|
|
81
|
+
...groups.map(([initiative, initiativeNotes]) => {
|
|
82
|
+
const isExpanded = expandedInitiatives.has(initiative);
|
|
83
|
+
return el('div', { class: 'nt-menu-group' },
|
|
84
|
+
el('div', {
|
|
85
|
+
class: 'nt-menu-group-title',
|
|
86
|
+
onClick: () => onToggleInitiative(initiative),
|
|
87
|
+
},
|
|
88
|
+
icon(isExpanded ? 'chevron-down' : 'chevron-right', { class: 'nt-menu-group-icon' }),
|
|
89
|
+
el('span', {}, initiative),
|
|
90
|
+
),
|
|
91
|
+
isExpanded && el('div', { class: 'nt-menu-group-items' },
|
|
92
|
+
...initiativeNotes.map((note) =>
|
|
93
|
+
renderNoteRow(note, note.id === selectedId, () => onSelectNote(note.id)),
|
|
94
|
+
),
|
|
95
|
+
),
|
|
96
|
+
);
|
|
97
|
+
}),
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function renderBoard(inner, ctx) {
|
|
102
|
+
ensureComponentsCss();
|
|
103
|
+
const root = el('div', { class: 'nt-menu' });
|
|
104
|
+
|
|
105
|
+
async function render() {
|
|
106
|
+
if (!list) await loadList();
|
|
107
|
+
const note = selectedId ? cache.get(selectedId) : null;
|
|
108
|
+
|
|
109
|
+
const leftPanel = renderLeftPanel(
|
|
110
|
+
list.notes || [],
|
|
111
|
+
selectedId,
|
|
112
|
+
async (id) => {
|
|
113
|
+
selectedId = id;
|
|
114
|
+
await loadNote(id);
|
|
115
|
+
render();
|
|
116
|
+
},
|
|
117
|
+
(initiative) => {
|
|
118
|
+
if (expandedInitiatives.has(initiative)) {
|
|
119
|
+
expandedInitiatives.delete(initiative);
|
|
120
|
+
} else {
|
|
121
|
+
expandedInitiatives.add(initiative);
|
|
122
|
+
}
|
|
123
|
+
render();
|
|
124
|
+
},
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const rightPanel = note
|
|
128
|
+
? await (async () => {
|
|
129
|
+
// Load and render the full note detail using the note view's render logic
|
|
130
|
+
try {
|
|
131
|
+
// Import the note view's rendering for inline display
|
|
132
|
+
const noteModule = await import('./view-notes.js');
|
|
133
|
+
// Parse next steps/actions from the note's final state
|
|
134
|
+
let actions = [];
|
|
135
|
+
if (note.final_state && note.final_state.actions) {
|
|
136
|
+
actions = note.final_state.actions;
|
|
137
|
+
}
|
|
138
|
+
// Use the note's renderInline if available, otherwise render basic detail
|
|
139
|
+
return el('div', { class: 'nt-menu-right' },
|
|
140
|
+
el('div', { class: 'nt-detail-header' },
|
|
141
|
+
el('div', { class: 'nt-detail-title' }, note.title || 'Untitled note'),
|
|
142
|
+
el('div', { class: 'nt-detail-date' }, new Date(note.updated_at).toLocaleDateString()),
|
|
143
|
+
),
|
|
144
|
+
el('div', { class: 'nt-detail-summary' },
|
|
145
|
+
note.summary ? el('pre', {}, note.summary) : el('p', { class: 'nt-hint' }, 'No summary yet'),
|
|
146
|
+
),
|
|
147
|
+
actions.length > 0 && el('div', { class: 'nt-detail-actions' },
|
|
148
|
+
el('div', { class: 'nt-detail-actions-title' }, 'Next steps'),
|
|
149
|
+
...actions.map((action, idx) =>
|
|
150
|
+
el('div', { class: 'nt-detail-action-row' },
|
|
151
|
+
el('div', { class: 'nt-detail-action-text' }, action.text || ''),
|
|
152
|
+
action.owner && el('div', { class: 'nt-detail-action-owner' }, `Owner: ${action.owner}`),
|
|
153
|
+
action.due && el('div', { class: 'nt-detail-action-due' }, `Due: ${action.due}`),
|
|
154
|
+
iconButton({ icon: 'plus', class: 'nt-detail-make-task', title: 'Create task from this step' },
|
|
155
|
+
async () => {
|
|
156
|
+
try {
|
|
157
|
+
const result = await pmPost('/api/notes/create-task', {
|
|
158
|
+
id: note.id,
|
|
159
|
+
text: action.text,
|
|
160
|
+
owner: action.owner,
|
|
161
|
+
due: action.due,
|
|
162
|
+
});
|
|
163
|
+
if (result && result.ok) {
|
|
164
|
+
toast({ text: 'Task created' });
|
|
165
|
+
} else {
|
|
166
|
+
toast({ text: 'Could not create task' });
|
|
167
|
+
}
|
|
168
|
+
} catch (e) {
|
|
169
|
+
console.error('Task creation error:', e);
|
|
170
|
+
toast({ text: 'Error creating task' });
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
),
|
|
174
|
+
),
|
|
175
|
+
),
|
|
176
|
+
),
|
|
177
|
+
);
|
|
178
|
+
} catch {
|
|
179
|
+
return el('div', { class: 'nt-menu-right' }, empty({ text: 'Could not load note detail' }));
|
|
180
|
+
}
|
|
181
|
+
})()
|
|
182
|
+
: el('div', { class: 'nt-menu-right' }, empty({ text: 'Select a note to view details' }));
|
|
183
|
+
|
|
184
|
+
setChildren(root, [leftPanel, rightPanel]);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
await render();
|
|
188
|
+
ctx.setState = (state) => { render(); };
|
|
189
|
+
return root;
|
|
190
|
+
}
|
|
@@ -571,7 +571,8 @@ async function deleteAudio(ctx, n) {
|
|
|
571
571
|
async function openSource(kind) {
|
|
572
572
|
const md = navigator.mediaDevices;
|
|
573
573
|
if (kind === 'mic') return md.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true, channelCount: 1 } });
|
|
574
|
-
|
|
574
|
+
// preferCurrentTab + selfBrowserSurface defaults to the current tab with tab audio, reducing friction for the common case
|
|
575
|
+
const s = await md.getDisplayMedia({ video: true, audio: { echoCancellation: false, noiseSuppression: false, autoGainControl: false }, systemAudio: 'include', preferCurrentTab: true, selfBrowserSurface: 'include' });
|
|
575
576
|
const tracks = s.getAudioTracks();
|
|
576
577
|
for (const t of s.getVideoTracks()) t.stop(); // only the sound is used
|
|
577
578
|
if (!tracks.length) throw Object.assign(new Error('The shared tab or window has no audio. Share it again and tick "Share tab audio".'), { noAudio: true });
|
|
@@ -908,22 +909,9 @@ async function stopRecording(ctx) {
|
|
|
908
909
|
async function chooseSources(ctx, n) {
|
|
909
910
|
const eng = engine && engine.ws === wsOf(ctx) && engine.whisper ? engine : await loadEngine(ctx);
|
|
910
911
|
const local = !!(eng.whisper && eng.whisper.ready);
|
|
911
|
-
|
|
912
|
-
const
|
|
913
|
-
|
|
914
|
-
el('p', { class: 'nt-hint' }, local ? 'Pick the meeting\'s tab or window next, and tick "Share tab audio".' : 'The meeting can be heard once local transcription is set up in Settings. Until then the microphone is recognised by the browser on this device.'),
|
|
915
|
-
el('p', { class: 'nt-consent', 'data-nt-consent': '' }, local
|
|
916
|
-
? 'Speech is transcribed on this machine' + (eng.whisper.keep_audio === false ? ', and no audio is kept' : ', and the audio is kept on this machine so you can replay the meeting') + '. Let people know you\'re taking notes.'
|
|
917
|
-
: 'Speech is recognised by the browser on this device, and no audio is kept. Let people know you\'re taking notes.')];
|
|
918
|
-
let h = null;
|
|
919
|
-
const go = () => {
|
|
920
|
-
const want = [mic.control.checked && 'mic', local && meet.control.checked && 'meeting'].filter(Boolean);
|
|
921
|
-
h.close();
|
|
922
|
-
if (!want.length) return;
|
|
923
|
-
if (local) startAudio(ctx, n, want); else startSpeech(ctx, n, 'record');
|
|
924
|
-
};
|
|
925
|
-
h = sheet({ title: 'Listen to', body, attrs: { 'data-nt-chooser': '' },
|
|
926
|
-
actions: [button({ label: 'Cancel', variant: 'ghost', onClick: () => h.close() }), button({ label: 'Start', variant: 'primary', onClick: go, attrs: { 'data-nt-start': '' } })] });
|
|
912
|
+
// Default to both mic and meeting (skip the chooser dialog entirely per Wawan's streamlining request)
|
|
913
|
+
const want = local ? ['mic', 'meeting'] : ['speech'];
|
|
914
|
+
if (local) startAudio(ctx, n, want); else startSpeech(ctx, n, 'record');
|
|
927
915
|
}
|
|
928
916
|
async function setSwitch(ctx, patch) {
|
|
929
917
|
const ws = wsOf(ctx);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: report-new
|
|
3
|
+
label: "New report (brainstorm, then save as a template)"
|
|
4
|
+
description: "Brainstorm one new recurring or ad hoc report with the PM: its purpose, audience, period, sections and format. Saves only the chosen structure as a template and registers its schedule. Never saves the sample artifacts."
|
|
5
|
+
run: agent
|
|
6
|
+
agent: claude
|
|
7
|
+
output:
|
|
8
|
+
- Template
|
|
9
|
+
- Schedule
|
|
10
|
+
- First draft
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
/report-new — help the PM design one new report, then save its structure as a template. You brainstorm, then write one definition file and register one schedule. The sample text, the slides and any other draft from the session are not saved.
|
|
14
|
+
|
|
15
|
+
Rules that always hold (D71):
|
|
16
|
+
1. DRAFT ONLY. A report is issued by the PM from the Reports page. Never send, post, share, email, publish or upload a report.
|
|
17
|
+
2. READ-ONLY on every source. Never change mail, chat, Jira, Confluence, calendar or any other source.
|
|
18
|
+
3. SAVE ONLY THE STRUCTURE. The template is the chosen structure: id, cadence, audience, sections, purpose and tone. Never save a sample, a slide or a draft as a template.
|
|
19
|
+
|
|
20
|
+
Procedure:
|
|
21
|
+
1. Ask, one question at a time: what the report is for; who reads it; the sources it should draw on; the sections it needs; and the format (slides, a document, or a message). Offer a sensible default for each and keep going unless the PM corrects it.
|
|
22
|
+
2. Ask for the period. Accept a cadence word (daily, weekly, bi-weekly, monthly), a single date, a range, or a phrase such as "every Tuesday", "every 2 weeks on Monday" or "the first Monday of the month". Map the phrase to the nearest schedule cadence and say what you chose. For a one-off report, ask for its date and set no recurring schedule.
|
|
23
|
+
3. Propose a template id: lowercase, starting with a letter, up to 40 characters from a-z, 0-9 and hyphens. Confirm it with the PM.
|
|
24
|
+
4. Write `<state>/reports/definitions/<id>.md`, where <state> is `pmos/state/` if it exists, otherwise `planning/`. Use this frontmatter, then the prose: `type: Reference`, `title`, `timestamp` (today), `cadence: <id>`, `audience`, and `sections:` as a list. Put the purpose, sources, length and tone in the body.
|
|
25
|
+
5. If the period is recurring, register the schedule: `pmos schedule add --skill report-template --cadence <daily|weekly|bi-weekly|monthly> --args <id> --name "<Name>"`. Use the cadence from step 2. The schedule drafts the report; it never issues one.
|
|
26
|
+
6. Run the first draft now: `pmos report draft <id>`. Then say where it is, and that the PM issues it from Reports.
|
|
27
|
+
7. Report in one short message: the template path, the schedule (or the one-off date), and the first draft path.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: report-template
|
|
3
|
+
label: "Saved report (from its template)"
|
|
4
|
+
description: "Prepare a saved report as a draft. The schedule's args name the template; the script writes every number, then this skill writes the notes between the markers. Never sends or posts."
|
|
5
|
+
run: agent
|
|
6
|
+
agent: claude
|
|
7
|
+
output:
|
|
8
|
+
- Headline
|
|
9
|
+
- Sections from the template
|
|
10
|
+
- Needs your call
|
|
11
|
+
- Sources reached and not reached
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
/report-template — prepare a saved report for THIS repository as a draft. The template id is the schedule's args, for example `quarterly-ops`. A script with no model writes the draft and every number in it; you write the words between the note markers.
|
|
15
|
+
|
|
16
|
+
Rules that always hold (D71):
|
|
17
|
+
1. DRAFT ONLY. You write a file in this repository. Never send, post, share, email, publish or upload the report anywhere; the PM does that.
|
|
18
|
+
2. READ-ONLY on every source. Never send, reply, forward, post, comment, create, edit, transition, label, archive or delete anything in mail, chat, Jira, Confluence, calendar or any other source.
|
|
19
|
+
3. SOURCED. Every claim carries a pointer the reader can open: an initiative id, a file path in this repository, or an acceptance. A claim you cannot point to is left out or marked "unverified".
|
|
20
|
+
4. NOTHING INVENTED. The script lists the sources it reached and did not reach under "Sources reached and not reached": that section is not yours to edit.
|
|
21
|
+
5. THE TEMPLATE'S RULES. The script shapes the file from the template: its sections, their order and its blocks. The template's purpose, audience, length and tone govern your words. Do not substitute your own format.
|
|
22
|
+
6. THE SCRIPT WRITES EVERY NUMBER. In a note you type no count, ratio, percentage or date.
|
|
23
|
+
7. PROSE ONLY BETWEEN NOTE MARKERS. Write only between a `<!-- pmos:note <id> -->` line and its `<!-- /pmos:note -->` line. Never edit, move or delete a marker or any line outside one. Never add a section to the file.
|
|
24
|
+
|
|
25
|
+
Procedure:
|
|
26
|
+
1. The template id is the schedule's `args`. It must match `^[a-z][a-z0-9-]{0,39}$`. If it is missing or does not match, stop and say which schedule has no valid template id.
|
|
27
|
+
2. Find the state directory: `pmos/state/` if it exists, otherwise `planning/`. Call it <state>.
|
|
28
|
+
3. Read the template: `<state>/reports/definitions/<id>.md`. If it does not exist, stop and say the template is missing. Do not invent one.
|
|
29
|
+
4. Run `pmos report draft <id>`. It writes `<state>/reports/<id>/YYYY-MM-DD.md`, with one empty note marker for each block.
|
|
30
|
+
- If it refuses because a draft for the slot exists, do not overwrite it. Write your notes into the file that exists.
|
|
31
|
+
- If it refuses because the slot is issued, stop. An issued report is frozen. Say so in one line.
|
|
32
|
+
- If it refuses for any other reason, stop and quote the refusal. Do not write the file yourself.
|
|
33
|
+
5. Read the draft, then the sources it names. Write one or two sentences for each `<!-- pmos:note <block-id> -->`. Start with "Warning:" when a block needs attention. Leave any note that already has text.
|
|
34
|
+
6. Run `pmos report draft <id> --redraft` so the script rewrites the file and keeps every note. Read the file once more.
|
|
@@ -103,6 +103,7 @@
|
|
|
103
103
|
"pm-r29-filespad-test.js",
|
|
104
104
|
"pm-r29-newsession-alignment-test.js",
|
|
105
105
|
"pm-live-notes-test.js",
|
|
106
|
+
"pm-notes-menu-test.js",
|
|
106
107
|
"pm-r30-test.js",
|
|
107
108
|
"pm-breakdown-test.js",
|
|
108
109
|
"pm-breakdown-phone-test.js",
|
|
@@ -201,6 +202,7 @@
|
|
|
201
202
|
"pm-r29-filespad-test.js": 5,
|
|
202
203
|
"pm-r29-newsession-alignment-test.js": 8,
|
|
203
204
|
"pm-live-notes-test.js": 24,
|
|
205
|
+
"pm-notes-menu-test.js": 8,
|
|
204
206
|
"pm-r30-test.js": 4,
|
|
205
207
|
"ui-test.js": 30,
|
|
206
208
|
"plan-items.test.js": 1,
|