create-pmos 0.139.0 → 0.140.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-pmos",
3
- "version": "0.139.0",
3
+ "version": "0.140.0",
4
4
  "description": "Scaffold the PMOS portable kit — run the PMOS product-management workflow (initiative → lane-fit spec → contract → build → three gates, with work-type lanes for features, research, maintenance, incidents, and content) entirely inside your own repo, with all state as git-versioned files. Includes `workflow-sweep`, which audits the state every board derives from and separates real defects from work you simply have not done. `create-pmos workspace` also installs + launches the local Terminal Workspace GUI. No database, no network, no telemetry.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -1,3 +1,3 @@
1
- kernel-source: pmos@aa6731f
1
+ kernel-source: pmos@86ccd34
2
2
  assembled: 2026-10-12
3
3
  mode: vendor
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Eval calibration log"
4
4
  description: "Append-only record of every PM override of an evaluator verdict."
5
- generated: {by: process:create-pmos, at: "2026-10-12T01:17:12Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-12T01:22:45Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Health budgets"
4
4
  description: "Closed registry of health-budget anchors (D59), edited only by the PM."
5
- generated: {by: process:create-pmos, at: "2026-10-12T01:17:12Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-12T01:22:45Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Discoveries and friction log"
4
4
  description: "Append-only record of what runs discovered and where the process rubbed."
5
- generated: {by: process:create-pmos, at: "2026-10-12T01:17:12Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-12T01:22:45Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -92,12 +92,15 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
92
92
  const h0 = await p.evaluate((s) => document.querySelector(s).offsetHeight, card);
93
93
  await p.evaluate((s) => document.querySelector(s).click(), card);
94
94
  ok('R4 the card flips', await until(() => p.evaluate((s) => document.querySelector(s).classList.contains('back'), card), 4000));
95
- await sleep(500);
96
- const fit = await p.evaluate((s) => {
95
+ /* The back grows over a transition; a slow runner is still mid-grow after a fixed wait, so poll until it settles. */
96
+ const measure = () => p.evaluate((s) => {
97
97
  const c = document.querySelector(s); const cb = c.getBoundingClientRect(); const fb = c.querySelector('.fb');
98
98
  const inside = (e) => !e || e.getBoundingClientRect().bottom <= cb.bottom + 1;
99
99
  return { grew: c.offsetHeight, noClip: fb.scrollHeight <= fb.clientHeight + 1, opts: inside(c.querySelector('.opts')), conf: inside(c.querySelector('.c3slide, .c3hold')), foot: inside(c.querySelector('.cft')), hasOpts: !!c.querySelector('.opt') };
100
100
  }, card);
101
+ await sleep(500);
102
+ let fit = await measure();
103
+ await until(async () => { fit = await measure(); return fit.grew >= h0 && fit.noClip && fit.opts && fit.conf && fit.foot && fit.hasOpts; }, 4000);
101
104
  ok('R4 the flipped back grows to fit its content', fit.grew >= h0 && fit.noClip && fit.opts && fit.conf && fit.foot && fit.hasOpts, JSON.stringify({ h0, fit }));
102
105
  /* decide: a hold button (fine pointer) or a click; the slide where the pointer is coarse */
103
106
  const act = await p.evaluate((s) => { const c = document.querySelector(s); const hb = c.querySelector('.c3hold .hb'); if (hb) { const r = hb.getBoundingClientRect(); return { x: r.left + r.width / 2, y: r.top + r.height / 2, mode: hb.getAttribute('data-hold') }; } return null; }, card);
@@ -0,0 +1,64 @@
1
+ 'use strict';
2
+ /* lib/calendar-events.js — every calendar event the workspace holds, in one list (incident calendar-one-event-source).
3
+ *
4
+ * readAll(runtimeDir, { now }) → [event] the shape of routes-calendar-import.js readEvents: { id, at, end, title, attendees, ... }
5
+ *
6
+ * The workspace keeps meetings in three places:
7
+ * 1. <runtime>/calendar-import/events.jsonl the import ledger (Settings › Import now, `pmos calendar import`);
8
+ * 2. <runtime>/calendar-memory.json the calendar the morning brief submits (lib/calendar-memory.js), 90 days back and 31 ahead;
9
+ * 3. <runtime>/briefs/<today>.json today's brief, whose calendar is today's meetings with their plan.
10
+ * The Agenda and the Record menu read 2 and 3 (GET /api/proactive/agenda). Calendar and the Briefing's Today read 1 only, so a PM whose
11
+ * meetings came in with the morning brief saw them under Record and nowhere else. This reader joins the three. An imported event wins
12
+ * over a remembered one with the same start minute and title, because it carries the PM's notes; today's brief replaces the remembered
13
+ * events of today, as agendaMonth does.
14
+ *
15
+ * `at` and `end` of a remembered event are local date-times with the offset (2026-10-12T10:45:00+08:00), so a reader that takes the
16
+ * first ten characters as the day (calendar-model.js bucketEvents) gets the local day. READ ONLY; a file that cannot be read gives nothing. */
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+ const calMem = require('./calendar-memory.js');
20
+
21
+ const pad = (n) => String(n).padStart(2, '0');
22
+ const localDate = (ms) => { const d = new Date(ms); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; };
23
+ /** A local date-time with its offset: the same instant, with the local day in its first ten characters. */
24
+ function localIso(ms) {
25
+ const d = new Date(ms);
26
+ const off = -d.getTimezoneOffset();
27
+ const sign = off >= 0 ? '+' : '-';
28
+ const a = Math.abs(off);
29
+ return `${localDate(ms)}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}${sign}${pad(Math.floor(a / 60))}:${pad(a % 60)}`;
30
+ }
31
+ const readJson = (f) => { try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch { return null; } };
32
+ const minuteKey = (ms, title) => Math.floor(ms / 60000) + '|' + calMem.normTitle(title);
33
+
34
+ /** A remembered or briefed event as an imported one. `src` says where it came from. */
35
+ function asEvent(e, src) {
36
+ const s = Date.parse(e.start);
37
+ if (!e || !e.title || Number.isNaN(s)) return null;
38
+ const f = e.end && !Number.isNaN(Date.parse(e.end)) ? Date.parse(e.end) : s + 30 * 60000;
39
+ const people = e.with ? String(e.with).split(/\s*,\s*/).filter(Boolean) : [];
40
+ return { id: String(e.id || calMem.seriesKey(e) + '@' + s), at: localIso(s), end: localIso(f), title: String(e.title), attendees: people.slice(0, 12),
41
+ initiative: e.initiative || null, agent_prep_notes: typeof e.prep === 'string' ? e.prep : '', pm_notes: [], src, label: 'reported', trust: 'untrusted' };
42
+ }
43
+
44
+ function readAll(runtimeDir, { now = Date.now() } = {}) {
45
+ if (!runtimeDir) return [];
46
+ let imported = [];
47
+ try { imported = require('./routes-calendar-import.js').readEvents(runtimeDir).map((e) => ({ ...e, src: e.src || 'import' })); } catch { imported = []; }
48
+ const today = localDate(now);
49
+ const mem = readJson(path.join(runtimeDir, 'calendar-memory.json'));
50
+ const brief = readJson(path.join(runtimeDir, 'briefs', today + '.json'));
51
+ const briefed = brief && brief.date === today && Array.isArray(brief.calendar) ? brief.calendar.map((e) => asEvent(e, 'brief')).filter(Boolean) : null;
52
+ let remembered = (mem && Array.isArray(mem.events) ? mem.events : []).map((e) => asEvent(e, 'memory')).filter(Boolean);
53
+ if (briefed) remembered = remembered.filter((e) => e.at.slice(0, 10) !== today).concat(briefed);
54
+ const seen = new Set(imported.filter((e) => e && e.at && !Number.isNaN(Date.parse(e.at))).map((e) => minuteKey(Date.parse(e.at), e.title)));
55
+ const out = imported.slice();
56
+ for (const e of remembered) {
57
+ const k = minuteKey(Date.parse(e.at), e.title);
58
+ if (seen.has(k)) continue;
59
+ seen.add(k); out.push(e);
60
+ }
61
+ return out.sort((a, b) => (Date.parse(a.at) || 0) - (Date.parse(b.at) || 0));
62
+ }
63
+
64
+ module.exports = { readAll, asEvent, localIso };
@@ -168,7 +168,7 @@ function connectors(homeDir) {
168
168
  }
169
169
 
170
170
  let read;
171
- try { read = require('./profiles').readProfiles(home); }
171
+ try { read = require('./profiles').readProfiles(home, { detect: true }); }
172
172
  catch { return empty('the profile reader is unavailable — PMOS cannot say what is resolvable, so it says nothing.'); }
173
173
 
174
174
  // ABSENT and BROKEN are different facts here for the same reason they are in the profile reader:
@@ -186,7 +186,9 @@ function connectors(homeDir) {
186
186
  const names = MCP_FOR_KIND[kind] || [];
187
187
  for (const prof of read.profiles) {
188
188
  const mcp = Array.isArray(prof.mcp) ? prof.mcp : [];
189
- const hit = mcp.find((s) => names.includes(String(s).toLowerCase()));
189
+ // a claude.ai connector is listed as "claude.ai Google Drive": compare it as "google-drive"
190
+ const key = (s) => String(s).toLowerCase().replace(/^claude\.ai\s+/, '').trim().replace(/\s+/g, '-');
191
+ const hit = mcp.find((s) => names.includes(key(s)));
190
192
  if (!hit) continue;
191
193
  configured.push({ kind, profile: prof.id, server: hit, declared: true });
192
194
  break; // one profile per kind is enough to offer the lookup
@@ -24,6 +24,12 @@
24
24
  * variable an alias sets (a key, a proxy) is left alone, and the surface says only one variable is applied.
25
25
  *
26
26
  * A descriptor under `~/.claude/profiles` always wins over a detected profile of the same id (see profiles.js).
27
+ *
28
+ * A detected profile's connectors come from the config directory's own `.claude.json`: the names of the MCP servers
29
+ * added with `claude mcp add --scope user` (`mcpServers`) and of the claude.ai connectors that account has used
30
+ * (`claudeAiMcpEverConnected`). Only the NAMES are kept. A server's command, URL and environment are never read into
31
+ * the result, so nothing secret leaves this module. Before this, a detected profile always carried `mcp: []`, and
32
+ * console3 Settings said "This profile lists no connectors" for a profile with many.
27
33
  */
28
34
  const fs = require('fs');
29
35
  const path = require('path');
@@ -37,6 +43,7 @@ const FISH_FUNCTIONS = '.config/fish/functions';
37
43
  const BIN_DIRS = ['.local/bin', 'bin', '.bin', '.config/claude/bin'];
38
44
  const MAX_TEXT = 256 * 1024; // a start-up file larger than this is not read
39
45
  const MAX_SCRIPT = 16 * 1024; // a launcher script larger than this is a binary or something else
46
+ const MAX_CONFIG = 32 * 1024 * 1024; // Claude's .claude.json keeps project history and can be large
40
47
 
41
48
  const safeRead = (file, max) => {
42
49
  try {
@@ -142,7 +149,24 @@ function fromConfigDirs(home) {
142
149
  }
143
150
 
144
151
  /**
145
- * detectProfiles(homeDir) → [{ id, label, account:null, mcp:[], env:{CLAUDE_CONFIG_DIR}, path, detected:{source, from, line} }]
152
+ * connectorsIn(dir) → the connector names Claude's config directory `dir` lists, or [] when it lists none or the file
153
+ * cannot be read. User-scope MCP servers first, then claude.ai connectors, each sorted, no repeats. Names only.
154
+ */
155
+ function connectorsIn(dir) {
156
+ if (typeof dir !== 'string' || !dir) return [];
157
+ const text = safeRead(path.join(dir, '.claude.json'), MAX_CONFIG);
158
+ if (!text) return [];
159
+ let cfg;
160
+ try { cfg = JSON.parse(text); } catch { return []; }
161
+ if (!cfg || typeof cfg !== 'object' || Array.isArray(cfg)) return [];
162
+ const servers = cfg.mcpServers && typeof cfg.mcpServers === 'object' && !Array.isArray(cfg.mcpServers) ? Object.keys(cfg.mcpServers) : [];
163
+ const cloud = Array.isArray(cfg.claudeAiMcpEverConnected) ? cfg.claudeAiMcpEverConnected.filter((x) => typeof x === 'string') : [];
164
+ const clean = (xs) => xs.map((x) => x.trim()).filter((x) => x && x.length <= 120).sort((a, b) => a.localeCompare(b));
165
+ return [...new Set([...clean(servers), ...clean(cloud)])];
166
+ }
167
+
168
+ /**
169
+ * detectProfiles(homeDir) → [{ id, label, account:null, mcp:[names from connectorsIn], env:{CLAUDE_CONFIG_DIR}, path, detected:{source, from, line} }]
146
170
  * Sorted by id. When one id turns up in several places the shell definition wins over a launcher, a launcher over a bare
147
171
  * directory: the command is what the PM types, so its directory is the one that is meant. A profile whose directory does
148
172
  * not exist is dropped (the command would start Claude on an empty home, which is a new login, not a profile).
@@ -187,7 +211,7 @@ function detectProfiles(homeDir) {
187
211
  if (!cur || rank[f.source] < rank[cur.source]) byId.set(f.id, f);
188
212
  }
189
213
  return [...byId.values()].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)).map((f) => ({
190
- id: f.id, label: f.id, account: null, mcp: [], env: { CLAUDE_CONFIG_DIR: f.dir },
214
+ id: f.id, label: f.id, account: null, mcp: connectorsIn(f.dir), env: { CLAUDE_CONFIG_DIR: f.dir },
191
215
  path: f.from, detected: { source: f.source, from: f.from, line: f.line },
192
216
  }));
193
217
  }
@@ -217,4 +241,4 @@ function searched(homeDir) {
217
241
  return out;
218
242
  }
219
243
 
220
- module.exports = { detectProfiles, searched, resolveDir, configDirOf, fromShellText, ID_RE };
244
+ module.exports = { detectProfiles, connectorsIn, searched, resolveDir, configDirOf, fromShellText, ID_RE };
@@ -4,7 +4,7 @@
4
4
  * → { ok, date, me?, today: [meeting], needs: [row], decisions_error? } me: the PM's first name from git config user.name, or null
5
5
  *
6
6
  * READ ONLY, and built only from what PMOS already holds. Nothing is invented.
7
- * today one row per meeting of the local day, in time order, from the cached calendar events (routes-calendar-import.js readEvents,
7
+ * today one row per meeting of the local day, in time order, from every calendar event the workspace holds (calendar-events.js readAll,
8
8
  * the same source as /api/calendar-views): { id, title, start, end, minutes, people, initiative, prepared, before, state }.
9
9
  * prepared: [{ kind: 'prep'|'pm-note'|'note'|'last-note', text, id? }] what PMOS holds for the meeting (the agent's prep note,
10
10
  * the PM's notes on the event, the note linked to it, the last note of an earlier occurrence). before: [{ id, title, due,
@@ -173,7 +173,7 @@ module.exports = {
173
173
  if (Number.isNaN(now.getTime())) now = new Date();
174
174
  try {
175
175
  const dir = ctx.runtimeDir;
176
- const events = dir ? require('./routes-calendar-import.js').readEvents(dir) : [];
176
+ const events = dir ? require('./calendar-events.js').readAll(dir, { now: now.getTime() }) : [];
177
177
  const items = dir ? pmItems.fold(pmItems.readLines(path.join(dir, 'pm-items', 'items.jsonl'))) : [];
178
178
  // The decisions come from the signal engine, whose repository read is slow. The page already holds them (GET /api/briefing feeds its deck), so the
179
179
  // read is made only when the caller asks (?decisions=1); the items and the agenda never wait for it.
@@ -10,8 +10,8 @@
10
10
  * the planned end is the promised date, the planned start is that date minus planned_days; no forecast is invented, so an
11
11
  * initiative without one shows as 'no plan' (or 'overdue' past its promise).
12
12
  *
13
- * EVENTS: lib/calendar-memory.js has validate, merge and month functions only; no reader of the stored calendar is exported.
14
- * Until one is, events = [] and events_source is 'none'. KRS: no KR metric series is stored in the repo, so the strategy lens
13
+ * EVENTS: lib/calendar-events.js readAll, the imported events joined with the calendar the morning brief remembers and today's
14
+ * brief (the same meetings the Agenda and the Record menu show). With no reader, events = [] and events_source is 'none'. KRS: no KR metric series is stored in the repo, so the strategy lens
15
15
  * gets none and says so. */
16
16
  const path = require('path');
17
17
  const { repoScript } = require('./repo-script.js');
@@ -166,7 +166,7 @@ module.exports = {
166
166
  catch (e) { ctx.json(res, { ok: false, error: 'the calendar could not read the repository: ' + (e && e.message ? e.message : String(e)) }, 500); return true; }
167
167
  const out = handle({ view: url.searchParams.get('view') || undefined, date: url.searchParams.get('date') || undefined }, {
168
168
  readFacts: () => facts,
169
- readEvents: () => require('./routes-calendar-import.js').readEvents(ctx.runtimeDir),
169
+ readEvents: () => require('./calendar-events.js').readAll(ctx.runtimeDir),
170
170
  ...readers(root, ctx.runtimeDir),
171
171
  });
172
172
  ctx.json(res, out.body, out.status);
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.139.0",
3
+ "version": "0.140.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pmos-workspace",
9
- "version": "0.139.0",
9
+ "version": "0.140.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@xterm/addon-fit": "^0.11.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.139.0",
3
+ "version": "0.140.0",
4
4
  "private": true,
5
5
  "description": "PMOS Terminal Workspace — local companion hosting real PTYs beside the library and gates (D62)",
6
6
  "license": "MIT",
@@ -29,6 +29,7 @@
29
29
  "work-focus.test.js",
30
30
  "work-evidence.test.js",
31
31
  "brief-page.test.js",
32
+ "calendar-events.test.js",
32
33
  "item-steps.test.js",
33
34
  "status-facts-panel.test.js",
34
35
  "jev-claims.test.js",
@@ -157,6 +158,7 @@
157
158
  "work-focus.test.js": 1,
158
159
  "work-evidence.test.js": 1,
159
160
  "brief-page.test.js": 1,
161
+ "calendar-events.test.js": 1,
160
162
  "pm-brief-page-test.js": 30,
161
163
  "item-steps.test.js": 1,
162
164
  "status-facts-panel.test.js": 1,