create-pmos 0.130.0 → 0.132.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 (51) 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/state/discoveries.md +1 -1
  6. package/workspace/c3-one-home-test.js +4 -2
  7. package/workspace/c3-work-test.js +200 -0
  8. package/workspace/fixtures/one-home-baseline.json +0 -91
  9. package/workspace/guide/console-calendar.md +65 -0
  10. package/workspace/guide/data-freshness.md +40 -0
  11. package/workspace/guide/index.md +2 -0
  12. package/workspace/guide/my-work.md +76 -1
  13. package/workspace/lib/calendar-cli.js +84 -0
  14. package/workspace/lib/calendar-layers.js +372 -0
  15. package/workspace/lib/facts-async.js +144 -35
  16. package/workspace/lib/freshness.js +167 -0
  17. package/workspace/lib/offloop.js +94 -0
  18. package/workspace/lib/pm-items.js +71 -6
  19. package/workspace/lib/pmos-verbs.js +3 -0
  20. package/workspace/lib/report-needs.js +1 -1
  21. package/workspace/lib/route-modules.js +1 -0
  22. package/workspace/lib/routes-board.js +3 -1
  23. package/workspace/lib/routes-calendar-views.js +112 -3
  24. package/workspace/lib/routes-my-work.js +27 -9
  25. package/workspace/lib/routes-pm-items.js +123 -13
  26. package/workspace/lib/routes-sources.js +64 -0
  27. package/workspace/lib/work-focus.js +106 -0
  28. package/workspace/package-lock.json +2 -2
  29. package/workspace/package.json +1 -1
  30. package/workspace/public/pm/c3-calendar.js +392 -0
  31. package/workspace/public/pm/c3-demo-cal4.js +114 -0
  32. package/workspace/public/pm/c3-demo-data.js +79 -77
  33. package/workspace/public/pm/c3-demo-misc2.js +4 -16
  34. package/workspace/public/pm/c3-demo-work-ix.js +19 -67
  35. package/workspace/public/pm/c3-demo.js +10 -3
  36. package/workspace/public/pm/c3-shell.js +5 -2
  37. package/workspace/public/pm/calendar-views.js +10 -3
  38. package/workspace/public/pm/components.css +34 -0
  39. package/workspace/public/pm/console3.css +155 -0
  40. package/workspace/public/pm/dest-settings.js +10 -1
  41. package/workspace/public/pm/freshness-line.js +123 -0
  42. package/workspace/public/pm/pm-work-view.js +364 -172
  43. package/workspace/public/pm/work-place.js +58 -0
  44. package/workspace/server.js +39 -2
  45. package/workspace/skills/calendar-sync.skill +30 -0
  46. package/workspace/test-files.json +2 -0
  47. package/workspace/test-lists/calendar-layers.json +5 -0
  48. package/workspace/test-lists/calendar-time-page.json +5 -0
  49. package/workspace/test-lists/console3-work-page.json +9 -0
  50. package/workspace/test-lists/workspace-data-freshness.json +6 -0
  51. package/workspace/public/pm/c3-demo-work2.js +0 -13
@@ -0,0 +1,58 @@
1
+ /* pm/work-place.js — the Work page's placement rule, as the client needs it (initiative console3-work-page, PRD R2, R5, R7).
2
+ *
3
+ * The SERVER places every item (lib/pm-items.js workPlacement) and answers GET /api/pm-items with `work`, `review` and `counts`.
4
+ * The client never copies an item from one view to another. It uses this copy of the rule for two things only:
5
+ * 1. the capture line's preview chip, which says where a new line lands before it is saved;
6
+ * 2. demo mode (/c3.html?demo=1), where nothing reaches the server, so the fixture and the in-memory overlay are placed here.
7
+ * The rule, in order: parked is Someday; waiting on someone is Waiting; a "not before" (defer) date after today is Upcoming;
8
+ * flagged is Today; due today or earlier is Today; a later due date is Upcoming; no date is Inbox. Dates are YYYY-MM-DD strings.
9
+ * Pure: no DOM, no fetch. */
10
+
11
+ export const VIEWS = Object.freeze(['inbox', 'today', 'upcoming', 'waiting', 'someday']);
12
+ export const REVIEW_DAYS = 7;
13
+
14
+ const day = (s) => (s ? String(s).slice(0, 10) : '');
15
+ export function addIso(s, n) { const d = new Date(day(s) + 'T00:00:00Z'); d.setUTCDate(d.getUTCDate() + n); return d.toISOString().slice(0, 10); }
16
+ export function daysBetween(a, b) { return Math.round((Date.parse(day(b) + 'T00:00:00Z') - Date.parse(day(a) + 'T00:00:00Z')) / 86400000); }
17
+
18
+ /** The view an open item belongs in. */
19
+ export function workPlace(item, today) {
20
+ const i = item || {};
21
+ if (i.parked) return 'someday';
22
+ if (i.waiting_on) return 'waiting';
23
+ if (i.defer && day(i.defer) > today) return 'upcoming';
24
+ if (i.flagged) return 'today';
25
+ if (i.due && day(i.due) <= today) return 'today';
26
+ if (i.due) return 'upcoming';
27
+ return 'inbox';
28
+ }
29
+
30
+ /** The day an Upcoming item arrives: its "not before" date when that is still ahead, else its due date. */
31
+ export function arrives(item, today) { return item.defer && day(item.defer) > today ? day(item.defer) : day(item.due); }
32
+
33
+ /** The days since the item was last touched (any event), from touched_at, else created_at. */
34
+ export function untouchedDays(item, today) { const t = day(item.touched_at || item.created_at); return t ? daysBetween(t, today) : 0; }
35
+
36
+ const byStr = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
37
+ /** Each view in the order the PRD's contract gives: Today overdue first, then flagged, then by due; Upcoming by arrival; Inbox newest first. */
38
+ export function sortView(view, list, today) {
39
+ const l = list.slice();
40
+ if (view === 'today') {
41
+ const rank = (i) => (i.due && day(i.due) < today ? 0 : i.flagged ? 1 : 2);
42
+ l.sort((a, b) => rank(a) - rank(b) || byStr(day(a.due) || '9999', day(b.due) || '9999') || byStr(String(a.time || ''), String(b.time || '')));
43
+ } else if (view === 'upcoming') l.sort((a, b) => byStr(arrives(a, today), arrives(b, today)));
44
+ else if (view === 'inbox') l.sort((a, b) => byStr(String(b.created_at || ''), String(a.created_at || '')));
45
+ return l;
46
+ }
47
+
48
+ /** { work, review, counts } for a set of items (done ones are left out), as the server's answer has them. */
49
+ export function buildWork(items, today, reviewDays = REVIEW_DAYS) {
50
+ const work = Object.fromEntries(VIEWS.map((v) => [v, []]));
51
+ const open = (items || []).filter((i) => i && !i.done);
52
+ open.forEach((i) => work[workPlace(i, today)].push(i));
53
+ VIEWS.forEach((v) => { work[v] = sortView(v, work[v], today); });
54
+ const review = open.filter((i) => ['inbox', 'someday', 'waiting'].includes(workPlace(i, today)) && untouchedDays(i, today) >= reviewDays)
55
+ .sort((a, b) => untouchedDays(b, today) - untouchedDays(a, today));
56
+ const counts = { ...Object.fromEntries(VIEWS.map((v) => [v, work[v].length])), review: review.length };
57
+ return { work, review, counts };
58
+ }
@@ -1942,6 +1942,21 @@ const autoSync = require('./lib/auto-sync').createAutoSync({
1942
1942
  onDone: (root) => boardCache.invalidate(root),
1943
1943
  label: `${os.hostname()} (auto-sync)`,
1944
1944
  });
1945
+ // workspace-data-freshness (R2 to R4): the facts cache is kept in the runtime state folder, and ONE change detector per
1946
+ // workspace starts the board and facts rebuilds when the repository changes, so nothing is re-read on a clock. The board
1947
+ // keeps its last value while it rebuilds (stale while revalidate): dropping it would show a blank Board on every change.
1948
+ const factsAsync = require('./lib/facts-async.js');
1949
+ factsAsync.configure({ stateDir: STATE });
1950
+ const freshness = require('./lib/freshness.js').create({
1951
+ stateDir: STATE,
1952
+ workspaces: () => [...workspaces.values()],
1953
+ schedules: () => readSchedules(),
1954
+ factsStatus: (root) => factsAsync.status(root),
1955
+ onChange: (w) => { boardCache.board(w.root, { refreshNow: true }); factsAsync.refresh(w.root); },
1956
+ pollMs: Math.max(250, Number(process.env.PMOS_FRESH_POLL_MS) || 10000),
1957
+ stableMs: Math.max(0, Number(process.env.PMOS_FRESH_STABLE_MS) || 3000),
1958
+ });
1959
+ if (!REQUIRED_AS_LIBRARY) freshness.start();
1945
1960
  const STOPPED_STATES = ['closed', 'on-hold', 'cancelled'];
1946
1961
  function readLifecycle(w) {
1947
1962
  const latest = new Map();
@@ -2362,6 +2377,12 @@ const SEED_SETS = {
2362
2377
  // jira-wbs (J14): the agent step that reads the status of every pushed Jira ticket into each initiative's snapshot,
2363
2378
  // read-only. Daily at 06:30, so the morning brief (06:45) and every report read a status at most a day old.
2364
2379
  jira: [{ name: 'Jira status check', skill: 'jira-sync', cadence: 'daily', time: '06:30' }],
2380
+ // workspace-data-freshness (R8): the read-only calendar pull, before the morning brief (06:45) and again at midday, so
2381
+ // Briefing and Calendar never work from yesterday's events.
2382
+ calendar: [
2383
+ { name: 'Calendar pull (morning)', skill: 'calendar-sync', cadence: 'daily', time: '06:30' },
2384
+ { name: 'Calendar pull (midday)', skill: 'calendar-sync', cadence: 'daily', time: '12:30' },
2385
+ ],
2365
2386
  };
2366
2387
  function readSchedules() {
2367
2388
  try { const a = JSON.parse(fs.readFileSync(schedFile(), 'utf8')); return Array.isArray(a) ? a : []; } catch { return []; }
@@ -4065,6 +4086,18 @@ function myWorkRead(wsId) {
4065
4086
  if (!s || s.state === 'exited') return { ok: false, status: 500, error: (s && s.note) || 'the read session could not start' };
4066
4087
  return { ok: true, session: s.id };
4067
4088
  }
4089
+ /** Refresh now (workspace-data-freshness R7): start skill `skillId` in a session of repository `wsId`, as myWorkRead does. */
4090
+ function startSkillSession(wsId, skillId, name) {
4091
+ const w = getWs(wsId);
4092
+ if (!w) return { ok: false, status: 400, error: 'no workspace' };
4093
+ const task = taskById(skillId, w.root);
4094
+ if (!task || task.kind !== 'agent' || !task.prompt) return { ok: false, status: 409, error: `the ${skillId} skill is not installed for this repository` };
4095
+ const prompt = skillStore.retargetFiling(task.prompt, filingContract(w));
4096
+ const s = spawnSession({ name, agent: task.agent || 'claude', ws: w.id, origin: 'source-refresh', initialInput: prompt + '\r',
4097
+ modelAsk: { scheduleTier: null, skillTier: task.tier || null, skill: task.skill } });
4098
+ if (!s || s.state === 'exited') return { ok: false, status: 500, error: (s && s.note) || 'the refresh session could not start' };
4099
+ return { ok: true, session: s.id };
4100
+ }
4068
4101
  /* The calendar the Agenda remembers (G23): one file beside the briefs, written only by the brief route. */
4069
4102
  const CAL_MEMORY = () => path.join(STATE, 'calendar-memory.json');
4070
4103
  // the PM's key-meeting marks (lib/calendar-memory.js setMark): a small file beside the calendar memory, written only by
@@ -6457,7 +6490,9 @@ async function handleRequest(req, res) {
6457
6490
  // `ledgers` (pm-os-absorb Run 1) — the plan-items signature of every registered workspace, so an open Breakdown
6458
6491
  // knows to refetch: `items` is one stat per workspace; `evidence` is computed only for the ids in ?ledgers=
6459
6492
  // (at most 4). Computed on this request and never stored (D33).
6460
- if (p === '/api/sessions') return json(res, { sessions: sessionList(), version: APP_VERSION,
6493
+ // `data_versions` (workspace-data-freshness R4): a number per workspace that moves when its files change, so a page
6494
+ // refetches only then.
6495
+ if (p === '/api/sessions') return json(res, { sessions: sessionList(), version: APP_VERSION, data_versions: freshness.versions(),
6461
6496
  ledgers: planItems.ledgerSigs([...workspaces.values()].map((w) => ({ id: w.id, root: w.root })), url.searchParams.get('ledgers')) });
6462
6497
  if (p === '/api/git/branches') {
6463
6498
  // Read-only branch awareness for a workspace. A non-repo returns {ok:false} (never a fake
@@ -8654,7 +8689,9 @@ async function handleRequest(req, res) {
8654
8689
  if (await routeModules.dispatch(req, res, url, { json, readBody, getWs, invalidateNeedsAgg, boardCache, consoleState, stateDirFor, sessionList, initiativeIndex,
8655
8690
  runtimeDir: STATE, spawnSession, jevModelOpts, sendToSession,
8656
8691
  // initiative my-work: every registered repository, the day runs, and the two presses that start sessions
8657
- workspaceList: () => [...workspaces.values()], readDayRun, myWorkBrief, myWorkRead })) return;
8692
+ workspaceList: () => [...workspaces.values()], readDayRun, myWorkBrief, myWorkRead,
8693
+ // workspace-data-freshness: the source table, the facts rebuild and Refresh now's skill sessions
8694
+ freshness, factsRefresh: (root) => factsAsync.refresh(root, { force: true }), startSkill: startSkillSession })) return;
8658
8695
  res.writeHead(404); res.end('not found');
8659
8696
  }
8660
8697
 
@@ -0,0 +1,30 @@
1
+ ---
2
+ name: calendar-sync
3
+ label: "Calendar pull"
4
+ description: "Read, read-only, the PM's calendar events for today and the next 14 days, and hand them to PMOS so Briefing and Calendar work from today's events. It writes nothing to the calendar."
5
+ run: agent
6
+ agent: claude
7
+ output:
8
+ - Imported
9
+ - Not reached
10
+ - Needs you
11
+ ---
12
+
13
+ /calendar-sync — bring the PM's calendar up to date in PMOS. You read the calendar with the PM's own calendar connection. PMOS saves what you read and shows it on Briefing and Calendar. You change nothing in the calendar.
14
+
15
+ Rules that always hold:
16
+ 1. READ-ONLY on the calendar. Use only the tools that list or read events. Never create, edit, move, accept, decline, delete or respond to an event, and never send an invitation or a message.
17
+ 2. THE ONLY WRITE is `pmos calendar import --from <file> --sensitivity <label>`, once, at the end. Never edit a repository file. Never commit.
18
+ 3. LIGHT ON CONTENT. For each event, give only its id, title, start, end, the attendees' display names and, when you can tell, the initiative id it is for. Never copy a description, a meeting link, a dial-in number, an attachment, an email address or a token.
19
+ 4. SENSITIVITY. Use `internal` unless the PM said otherwise. Leave out any event marked private, personal, confidential or HR, and count it.
20
+ 5. TEMPORARY FILES OUTSIDE THE REPOSITORY. Write the events file in this session's scratch folder, and delete it after the import.
21
+ 6. NOTHING INVENTED. If the calendar cannot be reached, list it under "Not reached" and import nothing: an empty import is not a calendar with no meetings.
22
+
23
+ Procedure:
24
+ 1. List the PM's events from the start of today to the end of the 14th day after today, from the primary calendar. Fetch every page. Skip an event the PM declined.
25
+ 2. Write the events as JSON in the scratch folder, for example `<scratch>/calendar.json`, as a list with one object per event:
26
+ `{"id", "title", "start", "end", "attendees": ["Display Name", …], "initiative": "<initiative id or null>"}`
27
+ `start` and `end` are ISO times with the offset, for example `2026-10-12T09:00:00+07:00`. An all-day event has the date only.
28
+ 3. Run `pmos calendar import --from <scratch>/calendar.json --sensitivity internal`. If it refuses the file, fix what it names and run it once more.
29
+ 4. Delete the scratch file.
30
+ 5. Report in four short lists: Imported (the count, and the first and last day), Left out (the count of private events), Not reached, and Needs you (only when something blocked the pull).
@@ -25,6 +25,7 @@
25
25
  "pm-items.test.js",
26
26
  "jobs-to-be-done.test.js",
27
27
  "pm-items-routes.test.js",
28
+ "work-focus.test.js",
28
29
  "brief-page.test.js",
29
30
  "item-steps.test.js",
30
31
  "status-facts-panel.test.js",
@@ -147,6 +148,7 @@
147
148
  "pm-items.test.js": 1,
148
149
  "jobs-to-be-done.test.js": 1,
149
150
  "pm-items-routes.test.js": 1,
151
+ "work-focus.test.js": 1,
150
152
  "brief-page.test.js": 1,
151
153
  "pm-brief-page-test.js": 30,
152
154
  "item-steps.test.js": 1,
@@ -0,0 +1,5 @@
1
+ {
2
+ "_comment": "calendar-layers: GET /api/calendar-views layers, free blocks and event context (initiative calendar-time-page, R2 to R5, R8).",
3
+ "modules": ["calendar-layers.test.js"],
4
+ "seconds": { "calendar-layers.test.js": 2 }
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "_comment": "Initiative calendar-time-page: the console3 Calendar tab as a page about time (layers, meeting context, free time). Demo fixture /c3.html?demo=1.",
3
+ "panels": ["pm-calendar-page-test.js"],
4
+ "seconds": { "pm-calendar-page-test.js": 40 }
5
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "_comment": "Initiative console3-work-page: the Work page browser suite.",
3
+ "panels": [
4
+ "c3-work-test.js"
5
+ ],
6
+ "seconds": {
7
+ "c3-work-test.js": 30
8
+ }
9
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "_comment": "workspace-data-freshness: the shared commit range, the change-keyed facts cache, the change detector and data version, the source table and Refresh now, the worker-thread folds, and pmos calendar.",
3
+ "modules": ["data-freshness.test.js"],
4
+ "panels": [],
5
+ "seconds": { "data-freshness.test.js": 25 }
6
+ }
@@ -1,13 +0,0 @@
1
- /* pm/c3-demo-work2.js — demo-only fixtures of the Work tab's Jobs card (/c3.html?demo=1). Sets window.__c3work2, which pm-work-view.js reads in demo mode only.
2
- * jobAll: for each job, the items the mockup lists in its case file under "All N items, across every list" (console3.html c3jall, line 4653),
3
- * grouped by list in the order triage, today, week, waiting, later, done. Each group is [list label, [titles], struck-through?]. The titles are the
4
- * mockup's own S.items (line 1070); the demo list data holds a few more items under job 1, so the count shown here is the mockup's. */
5
- window.__c3work2 = {
6
- jobAll: {
7
- j1: [['Triage', ['Assign owners to 2 unowned workshop actions', 'Approve pricing-page copy before handoff', 'Confirm the growth assumption in the Q4 forecast']],
8
- ['Today', ['Cut Checkout v2 to 3 core flows to hold 24 Oct?']], ['Waiting', ['Payments sandbox keys', 'Annual-plan label']], ['Done', ['Send revised PRD to Dara'], true]],
9
- j2: [['Triage', ['Confirm the 1 Oct numbers before the 10:30 update']]],
10
- j3: [['Triage', ['Review the 13:00 interview guide', 'Pick interview slots with 3 customers', 'Accept the 15 Oct interview invite']], ['Week', ['Share returns insights with sales']]],
11
- j4: [['Week', ['Confirm the SSO date to Marta']]],
12
- },
13
- };