create-pmos 0.59.2 → 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/REMOTE.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 +7 -9
- 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/dest-settings.js +1 -0
- package/workspace/public/pm/frame-copy.js +14 -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/mobile.js +4 -5
- package/workspace/public/pm/registry.js +1 -1
- package/workspace/public/pm/tab-term.js +6 -50
- 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 +71 -63
- package/workspace/skills/morning-brief.skill +6 -1
- package/workspace/test-files.json +2 -0
- package/workspace/test-lists/scan-consistency.json +5 -0
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
export const COPY = {
|
|
12
|
-
title: '
|
|
12
|
+
title: 'Agenda',
|
|
13
13
|
/** "3 need you", "1 needs you". */
|
|
14
14
|
count: (n) => `${n} ${n === 1 ? 'needs' : 'need'} you`,
|
|
15
15
|
countIn: (n, repo) => `${n} ${n === 1 ? 'needs' : 'need'} you in ${repo || 'this repository'}`,
|
|
@@ -128,7 +128,7 @@ export const COPY = {
|
|
|
128
128
|
unreached: 'The workspace did not answer.',
|
|
129
129
|
refused: (why) => `Nothing changed. ${why}`,
|
|
130
130
|
oldServer: 'This workspace server has no day run yet. Only your goals show.',
|
|
131
|
-
panels: { summary: 'Agent results', calendar: 'Calendar', jobs: 'Jobs', goals: 'Goals for today', mentions: 'Mentions since yesterday', waiting: 'Also waiting for you', detail: 'Detail' },
|
|
131
|
+
panels: { summary: 'Agent results', calendar: 'Calendar', agenda: 'Agenda', jobs: 'Jobs', goals: 'Goals for today', mentions: 'Mentions since yesterday', waiting: 'Also waiting for you', detail: 'Detail' },
|
|
132
132
|
summary: (c) => [c.review ? `${c.review} ${c.review === 1 ? 'draft waits' : 'drafts wait'} for you` : null, c.done ? `${c.done} done` : null,
|
|
133
133
|
c.working ? `${c.working} working` : null, c.failed ? `${c.failed} failed` : null].filter(Boolean).join(' · ') || 'No agent job yet.',
|
|
134
134
|
summaryAll: (n) => `All ${n} agent ${n === 1 ? 'job has' : 'jobs have'} a result.`,
|
|
@@ -282,6 +282,58 @@ export const COPY = {
|
|
|
282
282
|
siteFull: 'At most 10 websites.',
|
|
283
283
|
remove: 'Remove',
|
|
284
284
|
noSources: 'The sources could not be read.',
|
|
285
|
+
/** The Agenda month (G25 to G27): the month on top, the agenda of a day or a week under it, the meeting's memory. */
|
|
286
|
+
ag: {
|
|
287
|
+
title: 'Agenda',
|
|
288
|
+
loading: 'Reading the month…',
|
|
289
|
+
prev: 'Earlier month',
|
|
290
|
+
later: 'Later month',
|
|
291
|
+
thisMonth: 'This month',
|
|
292
|
+
weekNo: (n) => `W${n}`,
|
|
293
|
+
weekOf: (n) => `Week ${n}`,
|
|
294
|
+
weekHint: (n) => `Show the agenda of week ${n}`,
|
|
295
|
+
dayHint: (label, key) => `Show the agenda of ${label}${key ? '. Matters most: ' + key : ''}`,
|
|
296
|
+
lens: { overview: 'Overview', full: 'Full', key: 'Key', ready: 'Prep', free: 'Free' },
|
|
297
|
+
lensHelp: {
|
|
298
|
+
overview: 'Each day: how full it is, the meeting that matters and your prep.',
|
|
299
|
+
full: 'The bar is 09:00 to 18:00. Each block is a meeting. Dark blocks are important.',
|
|
300
|
+
key: 'The one meeting of the day that matters, when there is one.',
|
|
301
|
+
ready: 'Prep done out of prep needed. Red: too little free time before the meeting.',
|
|
302
|
+
free: 'Free blocks of 90 minutes or more.',
|
|
303
|
+
},
|
|
304
|
+
busy: (min) => (min >= 60 ? `${Math.floor(min / 60)} h${min % 60 ? ' ' + (min % 60) + ' min' : ''}` : `${min} min`),
|
|
305
|
+
freeBlocks: (n) => `${n} free ${n === 1 ? 'block' : 'blocks'}`,
|
|
306
|
+
noFree: 'No long free block',
|
|
307
|
+
readyBadge: (done, need) => `${done}/${need}`,
|
|
308
|
+
readyWord: { ok: 'prep done', warn: 'prep left', risk: 'prep at risk' },
|
|
309
|
+
noNotes: (n) => `${n} no notes`,
|
|
310
|
+
star: '★',
|
|
311
|
+
lit: (title, n) => `${title}: ${n} ${n === 1 ? 'day' : 'days'} marked on the month.`,
|
|
312
|
+
empty: 'Nothing is remembered yet. Press Prepare my day: the session reads your calendar 90 days back and 31 days ahead.',
|
|
313
|
+
emptyMonth: 'No meetings remembered for this month.',
|
|
314
|
+
unread: 'The month could not be read.',
|
|
315
|
+
tiles: { busy: 'In meetings', key: 'Matters most', ready: 'Prep' },
|
|
316
|
+
noKey: 'Nothing stands out',
|
|
317
|
+
noPrep: 'Nothing to prepare',
|
|
318
|
+
noMeetings: 'No meetings.',
|
|
319
|
+
routine: (n) => `+${n} repeating, nothing for you`,
|
|
320
|
+
hideRoutine: 'Hide the repeating ones',
|
|
321
|
+
kindWord: { one: 'One-to-one', onboard: 'Onboarding', group: 'Group meeting' },
|
|
322
|
+
why: { present: 'You present.', decide: 'You decide.', external: 'People from outside the company join.', exec: 'Leadership joins.', many: (n) => `${n} people join.`, routine: 'It repeats, and nothing in it is for you.' },
|
|
323
|
+
means: 'What it means',
|
|
324
|
+
before: 'Before this meeting',
|
|
325
|
+
prepLine: (done, need) => `${done} of ${need} prep steps done.`,
|
|
326
|
+
readyNote: { ok: 'Your prep is done.', warn: 'Some prep is left, and there is time for it.', risk: 'The free time before it is short for the prep left.' },
|
|
327
|
+
series: (past, coming) => `Held ${past} ${past === 1 ? 'time' : 'times'} so far · ${coming} to come.`,
|
|
328
|
+
once: 'It happens once.',
|
|
329
|
+
lastTime: (date) => `Last time · ${date}`,
|
|
330
|
+
noLast: 'No notes from the last time.',
|
|
331
|
+
afterNotes: 'Notes',
|
|
332
|
+
openLines: 'Still open',
|
|
333
|
+
noNotesYet: 'No notes for this meeting.',
|
|
334
|
+
people: (n) => `${n} people`,
|
|
335
|
+
today: 'Today',
|
|
336
|
+
},
|
|
285
337
|
},
|
|
286
338
|
/** The Acceptance Gate card (D12): the PM's own act on a run. */
|
|
287
339
|
accept: {
|
|
@@ -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');
|
|
@@ -302,7 +303,8 @@ const TOKEN = BIND.token || null; // non-loopback peers must
|
|
|
302
303
|
const onlineMode = require('./lib/online-mode');
|
|
303
304
|
const accessJwt = require('./lib/access-jwt');
|
|
304
305
|
const accessKeys = require('./lib/access-keys');
|
|
305
|
-
const ONLINE = { settings: onlineMode.read(STATE), keys: null, keysTeam: null, fetchKeys: accessKeys.fetchCerts, find: onlineMode.findCloudflared
|
|
306
|
+
const ONLINE = { settings: onlineMode.read(STATE), keys: null, keysTeam: null, fetchKeys: accessKeys.fetchCerts, find: onlineMode.findCloudflared,
|
|
307
|
+
lastRefusal: null }; // { reason, at } of the last refused sign-in: a fixed reason word only, never the assertion or a claim (D08)
|
|
306
308
|
const tunnel = onlineMode.createTunnel({ stateDir: STATE, find: () => ONLINE.find() });
|
|
307
309
|
/** The public hostname the guards admit right now: the environment's, else the Online switch's while it is on. */
|
|
308
310
|
function publicHost() { return BIND.publicHost || (ONLINE.settings.on ? ONLINE.settings.host : null); }
|
|
@@ -331,21 +333,26 @@ function applyOnline() {
|
|
|
331
333
|
/** The D89 decision for one request: { ok: true } or { ok: false, reason }. The assertion is never logged (D08). */
|
|
332
334
|
async function accessVerdict(headers) {
|
|
333
335
|
const s = ONLINE.settings; const store = ONLINE.keys;
|
|
334
|
-
if (!store)
|
|
336
|
+
if (!store) { const v = { ok: false, reason: 'no keys' }; noteVerdict(v); return v; }
|
|
335
337
|
try { await store.ensure(); } catch { /* the store records its own error */ }
|
|
336
338
|
const token = (headers || {})['cf-access-jwt-assertion'];
|
|
337
339
|
const opts = { keyFor: store.keyFor, teamDomain: s.team, audience: s.audience, email: s.email, now: Math.floor(Date.now() / 1000) };
|
|
338
340
|
let v = accessJwt.verify(token, opts);
|
|
339
341
|
if (!v.ok && v.reason === 'unknown key' && v.kid && await store.refetchFor(v.kid)) v = accessJwt.verify(token, opts);
|
|
342
|
+
noteVerdict(v);
|
|
340
343
|
return v;
|
|
341
344
|
}
|
|
345
|
+
/** Keep the reason of the last refusal for Settings › Online, so the PM can see which check failed. A pass clears it. */
|
|
346
|
+
function noteVerdict(v) {
|
|
347
|
+
ONLINE.lastRefusal = v && v.ok ? null : { reason: String((v && v.reason) || 'unknown'), at: new Date().toISOString() };
|
|
348
|
+
}
|
|
342
349
|
/** The Online record and state, as GET /api/online returns it. Nothing secret is in it: the fields are not credentials. */
|
|
343
350
|
function onlineView(req) {
|
|
344
351
|
const s = ONLINE.settings;
|
|
345
352
|
return {
|
|
346
353
|
ok: true, on: s.on, host: s.host, tunnel: s.tunnel, team: s.team, audience: s.audience, email: s.email,
|
|
347
354
|
complete: onlineMode.complete(s), envHost: BIND.publicHost || null, cloudflared: !!ONLINE.find(),
|
|
348
|
-
running: tunnel.status(), keys: ONLINE.keys ? ONLINE.keys.status() : null,
|
|
355
|
+
running: tunnel.status(), keys: ONLINE.keys ? ONLINE.keys.status() : null, lastRefusal: ONLINE.lastRefusal,
|
|
349
356
|
canChange: onlineChangeAllowed(req),
|
|
350
357
|
};
|
|
351
358
|
}
|
|
@@ -453,13 +460,6 @@ function profileOverlay(profile) {
|
|
|
453
460
|
}
|
|
454
461
|
|
|
455
462
|
const SCROLLBACK = 200_000; // bytes of ring buffer per session
|
|
456
|
-
const SCROLLBACK_SLACK = 32_768; // how far the ring may run over before a trim (appendBuf)
|
|
457
|
-
// PTY output goes on the wire at most once per this window per session (queueOutput). node-pty
|
|
458
|
-
// hands over whatever one read() returned, which for an agent CLI repainting its screen is many
|
|
459
|
-
// small chunks — each one a JSON frame, a WebSocket message and a parse in the browser. The first
|
|
460
|
-
// chunk after a quiet spell is sent at once, so a keystroke's echo is never delayed by it.
|
|
461
|
-
const OUTPUT_COALESCE_MS = 4;
|
|
462
|
-
const OUTPUT_COALESCE_MAX = 64 * 1024; // …and flushed at once past this, so a flood stays bounded
|
|
463
463
|
// How often the server re-broadcasts the session list while anything is alive (see heartbeat()).
|
|
464
464
|
// 2s is chosen against what it drives: the state dot and the quiet clock. QUIET_AFTER_MS is 30s, so
|
|
465
465
|
// a 2s cadence puts the worst-case error in "quiet 31s" at one tick — under the rounding the UI
|
|
@@ -1456,7 +1456,13 @@ function parseHead(full) {
|
|
|
1456
1456
|
}
|
|
1457
1457
|
}
|
|
1458
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('..')) : [];
|
|
1459
1464
|
return {
|
|
1465
|
+
named,
|
|
1460
1466
|
initiative: id ? id[1] : (alsoIds[0] || null),
|
|
1461
1467
|
also: alsoIds,
|
|
1462
1468
|
recordId: rid ? rid[1] : null,
|
|
@@ -1522,6 +1528,7 @@ function docMeta(root, full) {
|
|
|
1522
1528
|
// frontmatter wins; the filename convention is the fallback, never the override
|
|
1523
1529
|
initiative: head.initiative || (role === 'record' ? head.recordId : null) || null,
|
|
1524
1530
|
also: role === 'record' ? [] : (head.also || []),
|
|
1531
|
+
named: role === 'record' ? (head.named || []) : [],
|
|
1525
1532
|
stem, title: head.title || null,
|
|
1526
1533
|
kr: head.kr || null, lane: head.lane || null,
|
|
1527
1534
|
dest: head.dest || null,
|
|
@@ -1575,6 +1582,19 @@ function initiativeIndex(root) {
|
|
|
1575
1582
|
// a filename stem is promoted to a group only when it matches a declared id or when
|
|
1576
1583
|
// several artifacts share it, so a lone research note never invents an initiative.
|
|
1577
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('/');
|
|
1578
1598
|
const byStem = new Map();
|
|
1579
1599
|
for (const m of metas) if (!m.initiative) byStem.set(m.stem, (byStem.get(m.stem) || 0) + 1);
|
|
1580
1600
|
const groups = new Map();
|
|
@@ -1603,6 +1623,7 @@ function initiativeIndex(root) {
|
|
|
1603
1623
|
const prefixOwner = (stem) => capturing.find((id) => stem.length > id.length + 1 && stem.startsWith(id + '-')) || null;
|
|
1604
1624
|
const keyFor = (m) => {
|
|
1605
1625
|
if (m.initiative) return m.initiative;
|
|
1626
|
+
if (namedBy.has(relOf(m))) return namedBy.get(relOf(m));
|
|
1606
1627
|
if (declared.has(m.stem)) return m.stem;
|
|
1607
1628
|
// the convention join: `jira-sync-prd.md` + `jira-sync-eval-rubric.md` are one unit
|
|
1608
1629
|
if ((byStem.get(m.stem) || 0) > 1) return m.stem;
|
|
@@ -1617,7 +1638,7 @@ function initiativeIndex(root) {
|
|
|
1617
1638
|
g.artifacts.push(art);
|
|
1618
1639
|
// overhaul-r2 C1 — listed under every OTHER initiative it names too (it does not lend them a
|
|
1619
1640
|
// title, an anchor or a lane: those stay the record's and its primary owner's)
|
|
1620
|
-
for (const other of m.also || []) {
|
|
1641
|
+
for (const other of [...(m.also || []), ...(alsoNamed.get(relOf(m)) || [])]) {
|
|
1621
1642
|
if (other === k) continue;
|
|
1622
1643
|
if (!groups.has(other)) groups.set(other, { id: other, title: null, kr: null, lane: null, artifacts: [] });
|
|
1623
1644
|
groups.get(other).artifacts.push({ ...art, shared: true });
|
|
@@ -2843,11 +2864,7 @@ function trimBuf(buf, limit) {
|
|
|
2843
2864
|
|
|
2844
2865
|
function appendBuf(s, data) {
|
|
2845
2866
|
s.buf += data;
|
|
2846
|
-
|
|
2847
|
-
// per chunk flattens and scans the whole 200 KB string for each few bytes an agent CLI repaints —
|
|
2848
|
-
// a spinner alone is dozens of chunks a second. Letting the ring run SCROLLBACK_SLACK over and then
|
|
2849
|
-
// cutting it back to SCROLLBACK does the same work once per 32 KB of output.
|
|
2850
|
-
if (s.buf.length > SCROLLBACK + SCROLLBACK_SLACK) s.buf = trimBuf(s.buf, SCROLLBACK);
|
|
2867
|
+
if (s.buf.length > SCROLLBACK) s.buf = trimBuf(s.buf, SCROLLBACK);
|
|
2851
2868
|
s.bufDirty = true; // the periodic writer skips a session that is not dirty
|
|
2852
2869
|
// Parser 2's whole input: the moment output last arrived. `quiet` is derived from this, never
|
|
2853
2870
|
// from reading the bytes — which is why it is the only one of the five that cannot misread.
|
|
@@ -2855,46 +2872,6 @@ function appendBuf(s, data) {
|
|
|
2855
2872
|
s.parsed = null; // buffer moved; the cached parse is stale
|
|
2856
2873
|
}
|
|
2857
2874
|
|
|
2858
|
-
/**
|
|
2859
|
-
* Put PTY output on the wire as at most ONE frame per OUTPUT_COALESCE_MS per session.
|
|
2860
|
-
*
|
|
2861
|
-
* Leading edge, then a window: a chunk that arrives when nothing has been sent recently goes out at
|
|
2862
|
-
* once — a keystroke's echo is never held back — and opens a window in which further chunks are
|
|
2863
|
-
* gathered and sent together when it closes. A burst (an agent CLI repainting its screen) therefore
|
|
2864
|
-
* costs one frame per window instead of one per node-pty read, and a lone echo costs nothing.
|
|
2865
|
-
*
|
|
2866
|
-
* The bytes reach the ring (`appendBuf`) at the same moment they reach the clients, never before:
|
|
2867
|
-
* a client that attaches in between gets `buf` without the pending bytes and then the flush, so it
|
|
2868
|
-
* sees every byte exactly once and in order. Anything that writes to `buf` directly while output may
|
|
2869
|
-
* be pending (the exit note) calls `flushOutput` first for the same reason.
|
|
2870
|
-
*/
|
|
2871
|
-
function queueOutput(s, data) {
|
|
2872
|
-
if (!data) return;
|
|
2873
|
-
if (s._out) s._out += data; else s._out = data;
|
|
2874
|
-
if (!s._outTimer) { sendOutput(s); s._outTimer = setTimeout(() => closeOutputWindow(s), OUTPUT_COALESCE_MS); return; }
|
|
2875
|
-
if (s._out.length >= OUTPUT_COALESCE_MAX) sendOutput(s);
|
|
2876
|
-
}
|
|
2877
|
-
function closeOutputWindow(s) {
|
|
2878
|
-
s._outTimer = null;
|
|
2879
|
-
if (!s._out) return;
|
|
2880
|
-
// Output kept arriving through the window: send it, and keep gathering for one more window.
|
|
2881
|
-
sendOutput(s);
|
|
2882
|
-
s._outTimer = setTimeout(() => closeOutputWindow(s), OUTPUT_COALESCE_MS);
|
|
2883
|
-
}
|
|
2884
|
-
function flushOutput(s) {
|
|
2885
|
-
if (s._outTimer) { clearTimeout(s._outTimer); s._outTimer = null; }
|
|
2886
|
-
sendOutput(s);
|
|
2887
|
-
}
|
|
2888
|
-
function sendOutput(s) {
|
|
2889
|
-
const data = s._out;
|
|
2890
|
-
if (!data) return;
|
|
2891
|
-
s._out = '';
|
|
2892
|
-
appendBuf(s, data);
|
|
2893
|
-
if (!s.clients || !s.clients.size) return;
|
|
2894
|
-
const frame = JSON.stringify({ t: 'data', id: s.id, data });
|
|
2895
|
-
for (const c of s.clients) if (c.readyState === 1) c.send(frame);
|
|
2896
|
-
}
|
|
2897
|
-
|
|
2898
2875
|
// Platform-correct guidance when a spawn fails. On macOS `posix_spawnp failed` is almost always
|
|
2899
2876
|
// node-pty's prebuilt `spawn-helper` missing its executable bit (prebuild extraction drops it) —
|
|
2900
2877
|
// the binary is fine, just not +x. The server auto-restores it at startup and `npm run doctor`
|
|
@@ -3128,7 +3105,8 @@ function attachPty(s, { seed = false } = {}) {
|
|
|
3128
3105
|
onOutput: (pane, text) => {
|
|
3129
3106
|
if (paneId && pane !== paneId) return; // only this session's pane
|
|
3130
3107
|
if (s._reattempts) s._reattempts = 0; // a working attach clears the retry budget
|
|
3131
|
-
|
|
3108
|
+
appendBuf(s, text);
|
|
3109
|
+
for (const c of s.clients) if (c.readyState === 1) c.send(JSON.stringify({ t: 'data', id: s.id, data: text }));
|
|
3132
3110
|
},
|
|
3133
3111
|
});
|
|
3134
3112
|
p.onData((d) => parser.feed(d));
|
|
@@ -3136,7 +3114,6 @@ function attachPty(s, { seed = false } = {}) {
|
|
|
3136
3114
|
// on the client PTY's own window size.
|
|
3137
3115
|
client.resize(s.cols || 80, s.rows || 24);
|
|
3138
3116
|
p.onExit(({ exitCode }) => {
|
|
3139
|
-
flushOutput(s); // whatever the pane said last lands before a reconnect or the exit note
|
|
3140
3117
|
s.pty = null;
|
|
3141
3118
|
// Our PTY is the tmux CLIENT: it exits when the pane process ends AND on a mere detach. If
|
|
3142
3119
|
// the tmux session is still alive, the process lives on — reconnect (bounded, to never spin).
|
|
@@ -3378,9 +3355,11 @@ function spawnSession({ name, agent, cwd, ws, origin, initialInput, profile, ini
|
|
|
3378
3355
|
announceStateHealth(s);
|
|
3379
3356
|
announceTree(s); // registered first: a session's co-tenancy question includes itself in the map
|
|
3380
3357
|
announceModel(s);
|
|
3381
|
-
p.onData((d) =>
|
|
3358
|
+
p.onData((d) => {
|
|
3359
|
+
appendBuf(s, d);
|
|
3360
|
+
for (const c of s.clients) if (c.readyState === 1) c.send(JSON.stringify({ t: 'data', id, data: d }));
|
|
3361
|
+
});
|
|
3382
3362
|
p.onExit(({ exitCode }) => {
|
|
3383
|
-
flushOutput(s); // the last bytes the process wrote come before the exit note, in the ring and on the wire
|
|
3384
3363
|
s.state = 'exited'; s.pty = null;
|
|
3385
3364
|
const fast = Date.now() - startedMs < 800; // exited almost immediately → surface a hint
|
|
3386
3365
|
s.note = fast
|
|
@@ -3879,6 +3858,15 @@ function dayTick() {
|
|
|
3879
3858
|
if (JSON.stringify(run) !== before) writeDayRun(run);
|
|
3880
3859
|
}
|
|
3881
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
|
+
}
|
|
3882
3870
|
|
|
3883
3871
|
// ── Background prepare, slice D3 (initiative proactive-engine) ──────────────────────────────────────────────────
|
|
3884
3872
|
// The PM approves ONE plan hash from Today. The 60-second tick later starts that session through the SAME path a next-step
|
|
@@ -7637,6 +7625,13 @@ async function handleRequest(req, res) {
|
|
|
7637
7625
|
item_repos: goalsLib.itemRepos({ day: readGoalsDay(date), brief, placeRepos: st.placeRepos || {} }),
|
|
7638
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 : [] } });
|
|
7639
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
|
+
}
|
|
7640
7635
|
if (p.startsWith('/api/proactive/day/') && req.method === 'POST') {
|
|
7641
7636
|
const verb = p.slice('/api/proactive/day/'.length);
|
|
7642
7637
|
if (!['deliver', 'sent', 'fail', 'decide', 'check'].includes(verb)) return json(res, { ok: false, error: 'unknown day-run action' }, 404);
|
|
@@ -7740,6 +7735,14 @@ async function handleRequest(req, res) {
|
|
|
7740
7735
|
const bday = body && typeof body.date === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(body.date) ? body.date : briefLib.localDate(Date.now());
|
|
7741
7736
|
const v = briefLib.validateBrief(body, { now: Date.now(), settings: proSettings(), goalIds: readGoalsDay(bday).goals.map((g) => g.id) });
|
|
7742
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
|
+
}
|
|
7743
7746
|
// `merge: true` (a daytime check, G22) merges into the stored brief of that day; with none stored it is stored as it is.
|
|
7744
7747
|
let out = { ...v.brief, resolved: [] }; let merged = null;
|
|
7745
7748
|
let stored = null; try { stored = JSON.parse(fs.readFileSync(path.join(dir, v.brief.date + '.json'), 'utf8')); } catch { /* none yet */ }
|
|
@@ -7752,10 +7755,15 @@ async function handleRequest(req, res) {
|
|
|
7752
7755
|
}
|
|
7753
7756
|
try { fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(path.join(dir, out.date + '.json'), JSON.stringify(out) + '\n'); }
|
|
7754
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
|
+
}
|
|
7755
7763
|
// A brief for a day the PM pressed "Prepare my day": its agent priorities become tasks and start (G13), and a merge
|
|
7756
7764
|
// closes the check that sent it (G22).
|
|
7757
7765
|
try { dayAfterBrief(out.date, merged); } catch (e) { warnWriteFail('day run after the brief', e); }
|
|
7758
|
-
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 } : {}) });
|
|
7759
7767
|
}
|
|
7760
7768
|
if (req.method === 'GET') {
|
|
7761
7769
|
const now = Date.now(); const st = proSettings();
|
|
@@ -8241,7 +8249,7 @@ function httpGuard(req, res) {
|
|
|
8241
8249
|
accessVerdict(req.headers).then((v) => {
|
|
8242
8250
|
if (v.ok) return handleRequest(req, res);
|
|
8243
8251
|
res.writeHead(403, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
|
|
8244
|
-
res.end(JSON.stringify({ ok: false, error: 'your Cloudflare sign-in could not be verified for this workspace' }));
|
|
8252
|
+
res.end(JSON.stringify({ ok: false, error: 'your Cloudflare sign-in could not be verified for this workspace. Settings › Online on the computer that runs it says which check failed' }));
|
|
8245
8253
|
}, () => {
|
|
8246
8254
|
res.writeHead(403, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
|
|
8247
8255
|
res.end(JSON.stringify({ ok: false, error: 'your Cloudflare sign-in could not be verified for this workspace' }));
|
|
@@ -8544,7 +8552,7 @@ module.exports = { server, online: { ONLINE, applyOnline, tunnel, publicHost },
|
|
|
8544
8552
|
stripLegacyClientScreen,
|
|
8545
8553
|
// the scrollback ring's front trim, exported pure so the suite can assert that a cut never lands
|
|
8546
8554
|
// inside an escape sequence or between the halves of a surrogate pair.
|
|
8547
|
-
trimBuf, SCROLLBACK,
|
|
8555
|
+
trimBuf, SCROLLBACK,
|
|
8548
8556
|
// the liveness heartbeat's guard, exported pure so the "idle machine stays idle" half can be
|
|
8549
8557
|
// asserted without a server that has no clients (the suite always has both).
|
|
8550
8558
|
shouldHeartbeat, LIVENESS_MS, TX_REREAD_MS,
|
|
@@ -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,
|