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 +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/c3-foundation-test.js +5 -2
- package/workspace/lib/calendar-events.js +64 -0
- package/workspace/lib/four-panel.js +4 -2
- package/workspace/lib/profile-discovery.js +27 -3
- package/workspace/lib/routes-brief-page.js +2 -2
- package/workspace/lib/routes-calendar-views.js +3 -3
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/test-files.json +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "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": {
|
package/payload/KIT-VERSION
CHANGED
|
@@ -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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
96
|
-
const
|
|
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
|
-
|
|
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
|
-
*
|
|
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:
|
|
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
|
|
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('./
|
|
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-
|
|
14
|
-
*
|
|
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('./
|
|
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.
|
|
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.
|
|
9
|
+
"version": "0.140.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/workspace/package.json
CHANGED
|
@@ -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,
|