create-pmos 0.55.0 → 0.55.1
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/bin/create-pmos.js +3 -1
- 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/guide/proactive-engine.md +1 -0
- package/workspace/guide/today.md +2 -0
- package/workspace/lib/brief.js +28 -4
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +1 -1
- package/workspace/public/pm/components.css +1 -0
- package/workspace/public/pm/today-copy.js +3 -0
- package/workspace/public/pm/today-day.js +11 -5
- package/workspace/server.js +25 -3
- package/workspace/skills/morning-brief.skill +3 -1
package/bin/create-pmos.js
CHANGED
|
@@ -292,9 +292,11 @@ function workspace(targetArg, opts) {
|
|
|
292
292
|
console.log(forceReinstall
|
|
293
293
|
? 'Reinstalling the Terminal Workspace app (--reinstall)…'
|
|
294
294
|
: `Upgrading the Terminal Workspace app (installed ${installed || 'unknown'} → bundled ${bundled})…`);
|
|
295
|
+
// Stop the running server FIRST. `npm install -g` deletes and rewrites the folder that server runs from; a server
|
|
296
|
+
// still running there afterwards keeps its old code and starts every new session in a deleted folder.
|
|
297
|
+
stopWorkspaceServer();
|
|
295
298
|
const status = installWorkspaceGlobal();
|
|
296
299
|
if (status === 0) {
|
|
297
|
-
stopWorkspaceServer(); // restart the persistent server on the new code
|
|
298
300
|
console.log(`\n✅ pmos-workspace is now ${bundled}. Launching — if the UI still looks unchanged, hard-refresh the browser (Cmd/Ctrl+Shift+R) to drop the cached stylesheet.`);
|
|
299
301
|
} else {
|
|
300
302
|
console.log('\n⚠ Reinstall did not complete cleanly — launching the previously installed copy.');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.55.
|
|
3
|
+
"version": "0.55.1",
|
|
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-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-06T07:02:27Z"}
|
|
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-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-06T07:02:27Z"}
|
|
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-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-06T07:02:27Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -32,6 +32,7 @@ The top of Today is the morning brief: a greeting, the day's calendar, the prior
|
|
|
32
32
|
- Press **schedule the morning brief** in Today to make a daily schedule at 06:45. The schedule is an ordinary scheduled task, so it runs only while the workspace server runs. A sleeping computer gets no brief. A schedule that is made after its time of day for the first time runs within a minute, so pressing the button at 10:00 starts one brief session straight away.
|
|
33
33
|
- Each source is a setting: calendar, wispr, mail, slack, jira, confluence, github, drive, figma and website. Calendar, wispr, slack, jira and confluence are on by default. Mail, github, drive, figma and website are off by default. The skill reads only subjects and senders of mail, never bodies. A source that is off cannot be reported as read.
|
|
34
34
|
- **Scopes.** For Slack, Jira and Confluence you give each channel, project or space one scope. At most 50 names are kept for each service, and a name has at most 120 characters. **watch** means the session reads every message since the last read and writes a digest: the decisions, the open questions, and what touches a goal. **search** means the session reads nothing there and looks only for mentions of you and the words of your goals. **ignore** means the session does not read, search or report the place.
|
|
35
|
+
- **Places.** The morning brief lists every Slack channel you are a member of, every Jira project you work in and every Confluence space you are in. It reads only their names and links, not their messages. Sources shows these places, so you can give each one a scope. Before the first brief that reads a service, Sources says that its places are listed after that read. At most 200 places are kept in a brief.
|
|
35
36
|
- **Websites.** Under the website source you list at most 10 addresses. The session reads each one for changes that matter to a goal or a meeting.
|
|
36
37
|
- **Mentions.** In every place that is not ignored, the skill finds the mentions of you: where, who, the link to the conversation, and a short paraphrase. It never copies a message.
|
|
37
38
|
- **Meetings.** Each calendar event can carry a purpose and a prep note (at most 240 characters each), up to 6 steps before, during and after the meeting (each step says whether you or an agent does it), a suggested initiative with the reason, and up to 5 links.
|
package/workspace/guide/today.md
CHANGED
|
@@ -16,6 +16,8 @@ Above the cards, Today shows the day in two columns.
|
|
|
16
16
|
|
|
17
17
|
Press **Prepare my day** to start the day. It runs from the brief to the delivery of each agent job, and you do not press anything else until a draft is ready for you. The page [Guide: Today and the proactive engine](proactive-engine.md) describes each step.
|
|
18
18
|
|
|
19
|
+
If the morning-brief session stops before it submits a brief, the Read step says so and gives the reason. **Prepare my day** is then ready again, so press it to try once more. If the session says that its folder is not there, open the repository again from its current folder first.
|
|
20
|
+
|
|
19
21
|
Each job has one of three kinds.
|
|
20
22
|
|
|
21
23
|
| Kind | Who does it | What you do |
|
package/workspace/lib/brief.js
CHANGED
|
@@ -45,6 +45,7 @@ const WORTH_ASKING = 0.5;
|
|
|
45
45
|
const MAX_ITEMS = 30;
|
|
46
46
|
const MAX_EVENTS = 20;
|
|
47
47
|
const MAX_DIGESTS = 30;
|
|
48
|
+
const MAX_PLACES = 200; // the Slack channels, Jira projects and Confluence spaces the PM is in: names only, so Sources can list them
|
|
48
49
|
const MAX_RESOLVED = 200;
|
|
49
50
|
const INI_RE = /^[a-z0-9][a-z0-9._-]*$/i;
|
|
50
51
|
const GOAL_RE = /^g-[0-9a-f]{8}$/;
|
|
@@ -289,6 +290,24 @@ function validateBrief(raw, ctx = {}) {
|
|
|
289
290
|
digests.push({ source: d.source, name, link: d.link || null, decisions: lines(d.decisions, 140), open: lines(d.open, 140), goals: lines(d.goals, 140) });
|
|
290
291
|
}
|
|
291
292
|
|
|
293
|
+
// `places`: every channel, project and space the PM is in, by name, so the PM can set a level for each (G20). Names
|
|
294
|
+
// and links only: nothing of what is said there.
|
|
295
|
+
const pl = raw.places === undefined || raw.places === null ? [] : raw.places;
|
|
296
|
+
if (!Array.isArray(pl)) return { ok: false, error: 'places must be a list' };
|
|
297
|
+
if (pl.length > MAX_PLACES) return { ok: false, error: `places has ${pl.length} entries — at most ${MAX_PLACES}` };
|
|
298
|
+
const places = [];
|
|
299
|
+
for (const [i, x] of pl.entries()) {
|
|
300
|
+
const at = `place ${i + 1}`;
|
|
301
|
+
if (!x || typeof x !== 'object' || Array.isArray(x)) return { ok: false, error: `${at} is not an object` };
|
|
302
|
+
if (!DIGEST_SOURCES.includes(x.source)) return { ok: false, error: `${at}: source must be one of ${DIGEST_SOURCES.join(', ')}` };
|
|
303
|
+
if (sources[x.source] !== 'read') return { ok: false, error: `${at}: source "${x.source}" is not reported as read, so it cannot list places` };
|
|
304
|
+
if (typeof x.name !== 'string' || !x.name.trim()) return { ok: false, error: `${at} needs a name (the channel, project or space)` };
|
|
305
|
+
const err = optText(x.name, 120, `${at}: name`) || optLink(x.link, `${at}: link`);
|
|
306
|
+
if (err) return { ok: false, error: err };
|
|
307
|
+
const name = clip(x.name, 120);
|
|
308
|
+
if (!places.some((y) => y.source === x.source && y.name === name)) places.push({ source: x.source, name, link: x.link || null });
|
|
309
|
+
}
|
|
310
|
+
|
|
292
311
|
// `resolved` is read by a merge only: the ids of stored items a check found done or answered.
|
|
293
312
|
const rs = raw.resolved === undefined || raw.resolved === null ? [] : raw.resolved;
|
|
294
313
|
if (!Array.isArray(rs) || rs.length > MAX_RESOLVED || rs.some((x) => typeof x !== 'string' || !ITEM_ID_RE.test(x))) return { ok: false, error: `resolved must be a list of at most ${MAX_RESOLVED} item ids` };
|
|
@@ -300,7 +319,7 @@ function validateBrief(raw, ctx = {}) {
|
|
|
300
319
|
judgment = { used: raw.judgment.used };
|
|
301
320
|
}
|
|
302
321
|
return { ok: true, brief: { date, generated_at: new Date(now).toISOString(), sources, calendar, priorities: items.filter((x) => !x.worth_asking), worth_asking: items.filter((x) => x.worth_asking), judgment,
|
|
303
|
-
digests, resolved: [...new Set(rs)] } };
|
|
322
|
+
digests, places, resolved: [...new Set(rs)] } };
|
|
304
323
|
}
|
|
305
324
|
|
|
306
325
|
/**
|
|
@@ -310,12 +329,13 @@ function validateBrief(raw, ctx = {}) {
|
|
|
310
329
|
* An incoming item with a stored id replaces it and keeps its first_seen and new. More than 30 is refused.
|
|
311
330
|
* events by id: the fields given overwrite, notes merge by kind (wispr, mail), new events are added.
|
|
312
331
|
* digests replaced by (source, name). sources 'read' wins, otherwise the stored state is kept.
|
|
332
|
+
* places a service's list is replaced when the incoming brief lists that service.
|
|
313
333
|
* @returns {{ok:true, brief:object, added:number, removed:number}|{ok:false, error:string}}
|
|
314
334
|
*/
|
|
315
335
|
function mergeBrief(stored, incoming, { now = Date.now() } = {}) {
|
|
316
336
|
if (!incoming || typeof incoming !== 'object') return { ok: false, error: 'nothing to merge' };
|
|
317
337
|
const inItems = [...(incoming.priorities || []), ...(incoming.worth_asking || [])];
|
|
318
|
-
if (!stored || typeof stored !== 'object') return { ok: true, brief: { ...incoming, digests: incoming.digests || [], resolved: [] }, added: inItems.length, removed: 0 };
|
|
338
|
+
if (!stored || typeof stored !== 'object') return { ok: true, brief: { ...incoming, digests: incoming.digests || [], places: incoming.places || [], resolved: [] }, added: inItems.length, removed: 0 };
|
|
319
339
|
if (stored.date !== incoming.date) return { ok: false, error: `a merge is for the stored brief of the same day — the stored brief is for ${stored.date}` };
|
|
320
340
|
const at = new Date(now).toISOString();
|
|
321
341
|
const gone = new Set(incoming.resolved || []);
|
|
@@ -348,6 +368,10 @@ function mergeBrief(stored, incoming, { now = Date.now() } = {}) {
|
|
|
348
368
|
for (const d of incoming.digests || []) dg.set(d.source + '|' + d.name, d);
|
|
349
369
|
if (dg.size > MAX_DIGESTS) return { ok: false, error: `the brief would hold ${dg.size} digests — at most ${MAX_DIGESTS}` };
|
|
350
370
|
|
|
371
|
+
// places: a merge that lists places for a service replaces that service's list; a service it does not list keeps its own.
|
|
372
|
+
const listed = new Set((incoming.places || []).map((x) => x.source));
|
|
373
|
+
const places = [...(stored.places || []).filter((x) => !listed.has(x.source)), ...(incoming.places || [])];
|
|
374
|
+
|
|
351
375
|
const sources = {};
|
|
352
376
|
for (const k of SOURCES) {
|
|
353
377
|
const a = (stored.sources || {})[k]; const b = (incoming.sources || {})[k];
|
|
@@ -359,9 +383,9 @@ function mergeBrief(stored, incoming, { now = Date.now() } = {}) {
|
|
|
359
383
|
date: stored.date, generated_at: stored.generated_at || at, updated_at: at, sources, calendar,
|
|
360
384
|
priorities: items.filter((x) => !x.worth_asking), worth_asking: items.filter((x) => x.worth_asking),
|
|
361
385
|
judgment: { used: !!((stored.judgment && stored.judgment.used) || (incoming.judgment && incoming.judgment.used)) },
|
|
362
|
-
digests: [...dg.values()], resolved: [...new Set([...(stored.resolved || []), ...taken])].slice(-MAX_RESOLVED),
|
|
386
|
+
digests: [...dg.values()], places, resolved: [...new Set([...(stored.resolved || []), ...taken])].slice(-MAX_RESOLVED),
|
|
363
387
|
} };
|
|
364
388
|
}
|
|
365
389
|
|
|
366
|
-
module.exports = { SOURCES, ITEM_SOURCES, SOURCE_STATES, OWNERS, EFFORTS, KINDS, DIGEST_SOURCES, WORTH_ASKING, MAX_ITEMS, MAX_EVENTS, MAX_DIGESTS,
|
|
390
|
+
module.exports = { SOURCES, ITEM_SOURCES, SOURCE_STATES, OWNERS, EFFORTS, KINDS, DIGEST_SOURCES, WORTH_ASKING, MAX_ITEMS, MAX_EVENTS, MAX_DIGESTS, MAX_PLACES,
|
|
367
391
|
isUrl, validateBrief, mergeBrief, greeting, localDate };
|
package/workspace/package.json
CHANGED
|
@@ -2172,6 +2172,7 @@
|
|
|
2172
2172
|
}
|
|
2173
2173
|
.td-day__hello { flex: 0 1 220px; min-width: 0; overflow: hidden; text-overflow: ellipsis; font-size: 14px; color: var(--foreground); white-space: nowrap; }
|
|
2174
2174
|
.td-day__dim { color: var(--muted-foreground); }
|
|
2175
|
+
.td-day__warn { color: var(--warning); }
|
|
2175
2176
|
.td-day__mono { font-family: var(--mono); font-variant-numeric: tabular-nums; font-size: 12px; }
|
|
2176
2177
|
.td-day__steps { display: flex; flex-wrap: wrap; gap: 2px; list-style: none; margin: 0; padding: 0; }
|
|
2177
2178
|
.td-day__ts { gap: 6px; padding: 0 6px; letter-spacing: 0.04em; color: var(--muted-foreground); }
|
|
@@ -104,6 +104,7 @@ export const COPY = {
|
|
|
104
104
|
status: {
|
|
105
105
|
idle: 'One press: the agent reads, makes the jobs and does them. You approve only what sends or decides.',
|
|
106
106
|
reading: 'Reading your calendar and mentions…',
|
|
107
|
+
readFailed: (why) => `The read stopped: ${why}`,
|
|
107
108
|
working: (w, n) => `${w} ${w === 1 ? 'agent' : 'agents'} working · ${n} ${n === 1 ? 'needs' : 'need'} you`,
|
|
108
109
|
review: (n, d) => `${n} ${n === 1 ? 'needs' : 'need'} you · ${d} done`,
|
|
109
110
|
done: (d) => `All jobs done · ${d} done`,
|
|
@@ -154,6 +155,7 @@ export const COPY = {
|
|
|
154
155
|
place: { slack: 'channel', jira: 'project', confluence: 'space' },
|
|
155
156
|
placeAdd: { slack: 'Add a channel', jira: 'Add a project', confluence: 'Add a space' },
|
|
156
157
|
placeHint: 'Type a name and press Enter. It is watched.',
|
|
158
|
+
noPlaces: { slack: 'Your channels are listed after the first read of Slack. Press Prepare my day.', jira: 'Your projects are listed after the first read of Jira. Press Prepare my day.', confluence: 'Your spaces are listed after the first read of Confluence. Press Prepare my day.' },
|
|
157
159
|
readNote: { slack: 'Mentions, DMs and your threads are always read.', jira: 'Issues assigned to you and mentions of you are always read.', confluence: 'Pages and comments that mention you are always read.' },
|
|
158
160
|
decisions: 'Decisions',
|
|
159
161
|
open: 'Open questions',
|
|
@@ -228,6 +230,7 @@ export const COPY = {
|
|
|
228
230
|
stepsTitle: 'Steps found',
|
|
229
231
|
readNone: 'Press Prepare my day. A session starts and reads your sources by itself.',
|
|
230
232
|
readNow: 'The morning-brief session is reading your sources now.',
|
|
233
|
+
readFailed: (why) => `The morning-brief session did not finish: ${why} Press Prepare again to try again.`,
|
|
231
234
|
readDone: (ev, it) => `Read ${ev} ${ev === 1 ? 'event' : 'events'} and ${it} ${it === 1 ? 'item' : 'items'}.`,
|
|
232
235
|
notYet: 'Not started yet.',
|
|
233
236
|
madeJobs: (n, you, agent) => `${n} ${n === 1 ? 'job' : 'jobs'}: ${you} for you, ${agent} for the agent.`,
|
|
@@ -155,12 +155,14 @@ function model(d) {
|
|
|
155
155
|
const scopes = (settings && settings.scopes) || {};
|
|
156
156
|
const level = (svc, name) => (scopes[svc] && scopes[svc][name]) || 'search';
|
|
157
157
|
const digests = ((brief && Array.isArray(brief.digests)) ? brief.digests : []).map((g) => ({ ...g, id: g.source + '|' + g.name, level: level(g.source, g.name) }));
|
|
158
|
+
const places = (brief && Array.isArray(brief.places)) ? brief.places : [];
|
|
158
159
|
return {
|
|
159
|
-
d, brief, plan, goals, goalNo, run, tasks, jobs, events, now, settings, scopes, level, digests,
|
|
160
|
+
d, brief, plan, goals, goalNo, run, tasks, jobs, events, now, settings, scopes, level, digests, places,
|
|
160
161
|
stage: run ? (d.stage || deriveStage(run, brief)) : null,
|
|
161
162
|
mentions: jobs.filter((j) => j.item && j.item.mention),
|
|
162
163
|
sources: (d && Array.isArray(d.sources)) ? d.sources : [],
|
|
163
164
|
check: (d && d.check) || null,
|
|
165
|
+
readFailed: (d && d.read_failed) || null,
|
|
164
166
|
free: plan && plan.time ? plan.time.free_minutes : null,
|
|
165
167
|
maxGoals: (d && d.max_goals) || 7,
|
|
166
168
|
needs: jobs.filter((j) => j.group === 'needs').length,
|
|
@@ -287,7 +289,7 @@ function topBar(ctx) {
|
|
|
287
289
|
attrs: { class: 'td-day__ts', 'data-step-state': st, 'data-day-step': String(i), 'data-day-sel': 'step:' + i, 'aria-current': ctx.cur('step', String(i)), title: D.stepHint[i] } }));
|
|
288
290
|
}));
|
|
289
291
|
const working = m.tasks.filter((t) => t.status === 'working' || t.status === 'sending').length;
|
|
290
|
-
const status = !m.run ? D.status.idle : m.stage === 'reading' ? D.status.reading : m.stage === 'working' ? D.status.working(working, m.needs)
|
|
292
|
+
const status = !m.run ? D.status.idle : m.readFailed ? D.status.readFailed(m.readFailed.why) : m.stage === 'reading' ? D.status.reading : m.stage === 'working' ? D.status.working(working, m.needs)
|
|
291
293
|
: m.stage === 'review' ? D.status.review(m.needs, m.done) : D.status.done(m.done);
|
|
292
294
|
let check = null;
|
|
293
295
|
if (m.run && m.check) {
|
|
@@ -300,7 +302,7 @@ function topBar(ctx) {
|
|
|
300
302
|
const press = runs.get(ws);
|
|
301
303
|
const busy = !!press && press.state === 'busy';
|
|
302
304
|
const bs = m.run && m.run.brief_session;
|
|
303
|
-
const reading = !!bs && !bs.ended_at && m.stage === 'reading';
|
|
305
|
+
const reading = !!bs && !bs.ended_at && m.stage === 'reading' && !m.readFailed;
|
|
304
306
|
const label = busy ? D.starting : reading ? D.stillReading : m.run ? D.again : D.prepare;
|
|
305
307
|
return el('header', { class: 'td-day__bar' },
|
|
306
308
|
(() => { const when = [dateLabel(m.now), m.free !== null && m.free !== undefined ? D.free(m.free) : null].filter(Boolean).join(' · ');
|
|
@@ -576,7 +578,8 @@ function stepDetail(ctx, i) {
|
|
|
576
578
|
if (i === 0) {
|
|
577
579
|
const bs = m.run.brief_session;
|
|
578
580
|
const items = m.brief ? (m.brief.priorities || []).length + (m.brief.worth_asking || []).length : 0;
|
|
579
|
-
return [head, el('p',
|
|
581
|
+
return [head, el('p', m.readFailed ? { class: 'td-day__warn', 'data-day': 'read-failed' } : null,
|
|
582
|
+
m.brief ? D.readDone(m.events.length, items) : m.readFailed ? D.readFailed(m.readFailed.why) : D.readNow),
|
|
580
583
|
el('ul', { class: 'td-day__chk' }, ...m.sources.map((s) => el('li', { class: s.on && s.state === 'read' ? 'td-day__ok' : '' }, el('span', { class: s.on ? '' : 'td-day__dim' }, `${srcName(s.k)}: ${srcLine(s)}`)))),
|
|
581
584
|
el('div', { class: 'td-day__acts' }, button({ label: D.manageSources, size: 'sm', onClick: () => select('sources', 'all') }),
|
|
582
585
|
bs && bs.id ? button({ label: D.openSession, size: 'sm', variant: 'ghost', attrs: { 'data-day-act': 'session' }, onClick: () => openSession({ id: bs.id, name: 'morning-brief' }) }) : null)];
|
|
@@ -603,7 +606,9 @@ function srcLine(s) {
|
|
|
603
606
|
|
|
604
607
|
function placeControls(ctx, svc) {
|
|
605
608
|
const { m } = ctx;
|
|
606
|
-
|
|
609
|
+
// every place the last read found you in, then any you set a level for or that has a digest
|
|
610
|
+
const names = [...new Set([...m.places.filter((x) => x.source === svc).map((x) => x.name), ...Object.keys(m.scopes[svc] || {}),
|
|
611
|
+
...m.digests.filter((g) => g.source === svc).map((g) => g.name)])];
|
|
607
612
|
const row = (name) => el('div', { class: 'td-day__ch', 'data-day-place': svc + ':' + name }, el('span', null, name), levelSeg(ctx, svc, name));
|
|
608
613
|
const live = names.filter((x) => m.level(svc, x) !== 'ignore');
|
|
609
614
|
const ign = names.filter((x) => m.level(svc, x) === 'ignore');
|
|
@@ -619,6 +624,7 @@ function placeControls(ctx, svc) {
|
|
|
619
624
|
input.control.addEventListener('input', () => { ui.placeDraft[svc] = input.control.value; });
|
|
620
625
|
return [
|
|
621
626
|
note(D.readNote[svc]),
|
|
627
|
+
m.places.some((x) => x.source === svc) ? null : note(D.noPlaces[svc]),
|
|
622
628
|
...live.map(row),
|
|
623
629
|
ign.length ? button({ label: ui.showIgn[svc] ? D.hideIgnored : D.showIgnored(ign.length), variant: 'ghost', size: 'sm',
|
|
624
630
|
attrs: { 'data-day-ignored': svc, 'aria-expanded': ui.showIgn[svc] ? 'true' : 'false' }, onClick: () => { ui.showIgn[svc] = !ui.showIgn[svc]; rerender(); } }) : null,
|
package/workspace/server.js
CHANGED
|
@@ -156,6 +156,13 @@ const EMBEDDED_SELF = !INSTALLED_COPY && SELF_ROOT === PARENT_DIR &&
|
|
|
156
156
|
// launcher named a real repo via PMOS_SELF_ROOT. An installed copy launched WITHOUT that env
|
|
157
157
|
// (unsupported direct run) has no home — the client must open a repo to do anything.
|
|
158
158
|
const HOME_ENABLED = EMBEDDED_SELF || (!!process.env.PMOS_SELF_ROOT && dirExists(SELF_ROOT));
|
|
159
|
+
// An installed copy runs from the global npm folder, and the launcher starts it there. An upgrade (`npm install -g`)
|
|
160
|
+
// deletes and rewrites that folder, so a process that inherits it as its working directory is left in a deleted
|
|
161
|
+
// directory: the tmux daemon this server starts, and every agent CLI that daemon or a PTY starts, inherit it, and an
|
|
162
|
+
// agent CLI that reads its working directory at start exits ("the current working directory was deleted"). Every path
|
|
163
|
+
// in this server is absolute (__dirname, STATE, a workspace root), so an installed copy moves to the home folder,
|
|
164
|
+
// which no upgrade removes.
|
|
165
|
+
if (INSTALLED_COPY) { try { process.chdir(os.homedir()); } catch { /* keep the launch directory */ } }
|
|
159
166
|
const PUB = path.join(__dirname, 'public');
|
|
160
167
|
const PM_DIR = path.join(PUB, 'pm'); // the four-panel front end — served as a directory, guarded below
|
|
161
168
|
const XROOT = path.join(__dirname, 'node_modules', '@xterm');
|
|
@@ -2849,7 +2856,10 @@ function writeTmuxConf() {
|
|
|
2849
2856
|
// `env` is an OPT-IN override and only `new-session` ever passes one (profiles R3). has-session /
|
|
2850
2857
|
// kill-session / list-sessions / display-message have no session context, so handing them a
|
|
2851
2858
|
// profile's overlay would apply it to whichever pane the command happened to touch — a silent bug.
|
|
2852
|
-
|
|
2859
|
+
// The tmux daemon is born from the first of these calls and keeps that working directory for its whole life, across
|
|
2860
|
+
// workspace upgrades. It is started in the home folder, never in this server's install folder (see the chdir above).
|
|
2861
|
+
function tmuxCtl(args, env) { return spawnSync(TMUX, ['-L', TMUX_SOCK, ...args], { env: env || sessionEnv(), encoding: 'utf8', cwd: tmuxCwd() }); }
|
|
2862
|
+
function tmuxCwd() { const h = os.homedir(); return dirExists(h) ? h : undefined; }
|
|
2853
2863
|
function tmuxHasSession(name) { return !!TMUX && tmuxCtl(['has-session', '-t', name]).status === 0; }
|
|
2854
2864
|
function tmuxKillSession(name) { if (TMUX && name) tmuxCtl(['kill-session', '-t', name]); }
|
|
2855
2865
|
// Create the tmux session a FRESH spawn runs in. It REFUSES a name that already exists rather
|
|
@@ -3109,6 +3119,11 @@ function spawnSession({ name, agent, cwd, ws, origin, initialInput, profile, ini
|
|
|
3109
3119
|
const cmd = isShell ? (commandExists(SHELL) ? SHELL : 'sh') : agent;
|
|
3110
3120
|
// a session's working directory is confined to its OWN workspace root (R3)
|
|
3111
3121
|
const workdir = cwd && insideRoot(root, path.resolve(root, cwd)) ? path.resolve(root, cwd) : root;
|
|
3122
|
+
// A repository folder that was moved or deleted after it was registered: tmux would start the agent in its own folder
|
|
3123
|
+
// instead, and the agent would run in the wrong place or not at all. Refuse here and name the folder.
|
|
3124
|
+
if (!dirExists(workdir)) return exitedSession({ id, ws: w ? w.id : wsId(SELF_ROOT), root, name: (name && String(name).trim()) || agent, agent,
|
|
3125
|
+
origin: tag, profile: null, profileRefusal: null, plan_item: planItem || null, token: sessionToken },
|
|
3126
|
+
workdir, `the folder ${workdir} is not there. Open the repository again from its current folder, then try again.`);
|
|
3112
3127
|
// Resolved once, here, so every path below spawns under the SAME environment the record claims.
|
|
3113
3128
|
// `pres.id` is null whenever the overlay was not applied — including when a profile was asked for
|
|
3114
3129
|
// and could not be read — so the pane line can never name a profile that did nothing (R3/R9).
|
|
@@ -7122,7 +7137,8 @@ async function handleRequest(req, res) {
|
|
|
7122
7137
|
const list = readSchedules(); list.unshift(sched); writeSchedules(list); seeded = true;
|
|
7123
7138
|
}
|
|
7124
7139
|
const result = fireSchedule(sched);
|
|
7125
|
-
|
|
7140
|
+
// A session that could not start is recorded as already ended, so the page says why instead of waiting on a read.
|
|
7141
|
+
if (result.sessionId) run.brief_session = { id: result.sessionId, started_at: new Date(pnow).toISOString(), ended_at: result.ok ? null : new Date(pnow).toISOString() };
|
|
7126
7142
|
writeDayRun(run);
|
|
7127
7143
|
return json(res, { ok: !!result.ok, started: !!result.ok, session: result.sessionId || null, run, seeded, schedule: { id: sched.id, time: sched.time }, result, goals: open.length,
|
|
7128
7144
|
...(result.ok ? {} : { error: result.reason || 'the morning-brief session did not start' }) }, result.ok ? 200 : 500);
|
|
@@ -7154,7 +7170,13 @@ async function handleRequest(req, res) {
|
|
|
7154
7170
|
const due = dayRunLib.checkDue(run, { settings: st, now, events: brief ? brief.calendar || [] : [] });
|
|
7155
7171
|
const items = brief ? [...(brief.priorities || []), ...(brief.worth_asking || [])] : [];
|
|
7156
7172
|
const sources = proactiveLib.BRIEF_SOURCES.map((k) => ({ k, on: st.briefSources[k] === true, state: brief ? brief.sources[k] || null : null, got: items.filter((x) => x.source === k).length }));
|
|
7157
|
-
|
|
7173
|
+
// The brief session ended and no brief arrived: say so, with the session's own reason, so the page does not wait on a
|
|
7174
|
+
// read that will never finish (it reads as "stuck on Read" otherwise).
|
|
7175
|
+
const bs = run && run.brief_session; const bsess = bs ? sessions.get(bs.id) : null;
|
|
7176
|
+
const readFailed = !run || brief ? null
|
|
7177
|
+
: !bs ? { session: null, why: 'the morning-brief session did not start.' }
|
|
7178
|
+
: (!!bs.ended_at || !bsess || bsess.state === 'exited') ? { session: bs.id, why: (bsess && bsess.note) || 'the session ended without submitting a brief.' } : null;
|
|
7179
|
+
return json(res, { ok: true, ws: w ? w.id : null, today: date, now: new Date(now).toISOString(), run, stage: dayRunLib.stage(run, brief), brief, plan, cards_error: cardsError, read_failed: readFailed,
|
|
7158
7180
|
settings: { briefSources: st.briefSources, scopes: st.scopes, dayCheck: st.dayCheck, websites: st.websites }, sources,
|
|
7159
7181
|
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 : [] } });
|
|
7160
7182
|
}
|
|
@@ -34,6 +34,7 @@ Procedure:
|
|
|
34
34
|
- 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
35
|
- 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.
|
|
36
36
|
- Website, if on: read each address in the websites list once, only to see what changed that matters to a goal or a meeting. Report what changed in one line. Do not follow links to other sites.
|
|
37
|
+
- LIST THE PLACES THE PM IS IN, for each of Slack, Jira and Confluence that you read: the Slack channels the PM is a member of (public and private, not direct messages), the Jira projects the PM works in (projects with issues assigned to, reported by or watched by the PM), and the Confluence spaces the PM is a member of or has written in. Keep only the name (for example "#pm-team", "PRICE", "Product") and the link. Report them under "places" (see step 4). This list is how the PM sets a scope for each place, so list every place, not only the busy ones. Read only the names: do not read messages to make this list.
|
|
37
38
|
- Slack, Jira and Confluence take a scope for each name. Read the scopes from `pmos proactive settings`. A name with no scope is "search".
|
|
38
39
|
- watch: read EVERY message or comment since the last read (yesterday morning if there is none). Then write one digest (see step 4) with the decisions made, the open questions, and what touches a goal. Write the digest, not the messages.
|
|
39
40
|
- search: do not read the place. Look only for MENTIONS of the PM and for the words of the PM's goals and initiatives.
|
|
@@ -70,9 +71,10 @@ Procedure:
|
|
|
70
71
|
"items": [{"title": "…", "why_now": "…", "source": "calendar|wispr|mail|slack|jira|confluence|github|drive|figma|website|derived", "initiative_id": "<id or omit>", "goal": "<goal id or omit>", "mention": false, "owner": "pm|agent", "effort": "minutes|an-hour|half-day|day", "confidence": 0.0, "evidence": "…",
|
|
71
72
|
"link": "https://…", "where": "#pm-team", "who": "…", "about": "…", "ask": "…", "reply_needed": true, "how": "…", "kind": "you|approve|auto"}],
|
|
72
73
|
"digests": [{"source": "slack|jira|confluence", "name": "#pm-team", "link": "https://…", "decisions": ["…"], "open": ["…"], "goals": ["…"]}],
|
|
74
|
+
"places": [{"source": "slack|jira|confluence", "name": "#pm-team", "link": "https://…"}],
|
|
73
75
|
"judgment": {"used": false}}
|
|
74
76
|
```
|
|
75
|
-
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.
|
|
77
|
+
"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.
|
|
76
78
|
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.
|
|
77
79
|
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.
|
|
78
80
|
|