create-pmos 0.55.0 → 0.55.2
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/REMOTE.md +19 -0
- package/workspace/bin/pmos.js +90 -9
- package/workspace/guide/index.md +1 -1
- package/workspace/guide/layout.md +9 -5
- package/workspace/guide/proactive-engine.md +1 -0
- package/workspace/guide/today.md +2 -0
- package/workspace/lib/brief.js +28 -4
- package/workspace/lib/phone-cli.js +94 -0
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +1 -1
- package/workspace/public/pm/app.js +1 -1
- package/workspace/public/pm/components.css +139 -0
- package/workspace/public/pm/data.js +8 -0
- package/workspace/public/pm/dock-sessions.js +373 -0
- package/workspace/public/pm/dock.js +21 -37
- package/workspace/public/pm/frame-copy.js +50 -0
- package/workspace/public/pm/icons.js +1 -1
- package/workspace/public/pm/panel-tabs.js +46 -11
- package/workspace/public/pm/tab-term.js +13 -1
- package/workspace/public/pm/today-copy.js +3 -0
- package/workspace/public/pm/today-day.js +11 -5
- package/workspace/public/vendor/lucide-sprite.js +8 -2
- package/workspace/server.js +86 -10
- package/workspace/skills/morning-brief.skill +3 -1
- package/workspace/test-lists/workspace-phone-command.json +5 -0
- package/workspace/third-party/README.md +1 -1
|
@@ -56,6 +56,56 @@ export const COPY = {
|
|
|
56
56
|
skillsHead: 'Start from a skill',
|
|
57
57
|
noSkills: 'No skills to start from.',
|
|
58
58
|
skillsUnread: 'The skills list did not load.',
|
|
59
|
+
sideTabs: 'Show the sessions as a list at the side',
|
|
60
|
+
topTabs: 'Show the sessions as a strip on top',
|
|
61
|
+
// 0.55.1 — the dock's session list (dock-sessions.js)
|
|
62
|
+
ds: {
|
|
63
|
+
notLinked: 'Not linked',
|
|
64
|
+
scheduled: 'Scheduled',
|
|
65
|
+
openTabs: 'Open',
|
|
66
|
+
none: 'No sessions yet. Press Session to start one.',
|
|
67
|
+
group: 'Show the sessions of',
|
|
68
|
+
ended: 'ended',
|
|
69
|
+
open: (n) => 'Open ' + n,
|
|
70
|
+
more: (n) => 'Name, initiative and type of work of ' + n,
|
|
71
|
+
end: (n) => 'End ' + n,
|
|
72
|
+
endFront: 'End the session in front, or close the tab in front',
|
|
73
|
+
endAsk: (n) => 'End “' + n + '”?',
|
|
74
|
+
endYes: 'End',
|
|
75
|
+
cancel: 'Cancel',
|
|
76
|
+
endFailed: 'The session could not be ended.',
|
|
77
|
+
endedOne: (n) => 'Ended “' + n + '”.',
|
|
78
|
+
endedTree: (n, b) => 'Ended “' + n + '”. Its working copy on ' + b + ' is still there.',
|
|
79
|
+
removeCopy: 'Remove working copy',
|
|
80
|
+
copyRemoved: (b) => 'Removed the working copy of ' + b + '. The branch is kept.',
|
|
81
|
+
copyKept: (why) => 'The working copy stays: ' + why,
|
|
82
|
+
newFor: (t) => 'New session for ' + t,
|
|
83
|
+
newTitle: 'New session',
|
|
84
|
+
newSub: 'Name it, pick where it runs, then pick how it starts.',
|
|
85
|
+
name: 'Name',
|
|
86
|
+
namePh: 'Empty: named after the skill and initiative',
|
|
87
|
+
forLabel: 'For',
|
|
88
|
+
noInitiative: 'No initiative',
|
|
89
|
+
on: 'On',
|
|
90
|
+
auto: (where) => 'Automatic: ' + where,
|
|
91
|
+
mainTree: (b) => 'main tree' + (b ? ' (' + b + ')' : ''),
|
|
92
|
+
newFolder: (b) => 'new working copy on ' + b,
|
|
93
|
+
newBranch: 'New branch…',
|
|
94
|
+
branchName: 'Branch name',
|
|
95
|
+
needBranch: 'Type a name for the new branch.',
|
|
96
|
+
startHead: 'Start',
|
|
97
|
+
blank: 'Blank session',
|
|
98
|
+
blankMeta: (cli, tier) => (cli === 'claude' ? 'Claude' : 'Shell') + (cli === 'claude' && tier !== 'auto' ? ' · ' + tier : '') + ' · from Settings',
|
|
99
|
+
skillsLoading: 'Reading the skills…',
|
|
100
|
+
refused: 'The workspace server refused, without saying why.',
|
|
101
|
+
metaTitle: 'Session',
|
|
102
|
+
type: 'Type of work',
|
|
103
|
+
noType: 'No type',
|
|
104
|
+
initiative: 'Initiative',
|
|
105
|
+
save: 'Save',
|
|
106
|
+
saveFailed: 'Nothing was saved.',
|
|
107
|
+
types: { research: 'Research', prototype: 'Prototype', prd: 'PRD', build: 'Build', review: 'Review', report: 'Report', general: 'General' },
|
|
108
|
+
},
|
|
59
109
|
},
|
|
60
110
|
detail: { close: 'Close the detail' },
|
|
61
111
|
phone: {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* pm/icons.js — the lucide icons the Breakdown uses (D86 point 6).
|
|
2
2
|
*
|
|
3
|
-
* The
|
|
3
|
+
* The 35 symbols live in one vendored sprite, /vendor/lucide-sprite.js, generated from lucide-static 1.52.0 (see
|
|
4
4
|
* workspace/third-party/README.md). `ensureSprite()` loads it once and puts it in the overlay layer; `icon(name)` returns
|
|
5
5
|
* an <svg> that points at one symbol. Icons are decoration only: every one is aria-hidden, and a control that shows only
|
|
6
6
|
* an icon carries its own aria-label (primitives.js `iconButton` refuses to build one without).
|
|
@@ -35,7 +35,8 @@ import { mountDoc, disposeDoc, docText } from './tab-doc.js';
|
|
|
35
35
|
import { mountWeb, promptWeb } from './tab-web.js';
|
|
36
36
|
import { newSessionBlock, openNewSession } from './panel-newsess.js';
|
|
37
37
|
import { boardOf, currentProduct } from './board-read.js';
|
|
38
|
-
import { field, button, toast, ensureComponentsCss } from './primitives.js';
|
|
38
|
+
import { field, button, iconButton, toast, ensureComponentsCss } from './primitives.js';
|
|
39
|
+
import { COPY } from './frame-copy.js';
|
|
39
40
|
import { COPY as PAL } from './palette-copy.js';
|
|
40
41
|
import { askChoice } from './dialogs.js';
|
|
41
42
|
import { icon, ensureSprite } from './icons.js';
|
|
@@ -1084,7 +1085,11 @@ function keepSessionsLive(shown) {
|
|
|
1084
1085
|
let dockMod = null;
|
|
1085
1086
|
const inFrame = () => !!(window.pmApp && window.pmApp.frame && window.pmApp.frame.on);
|
|
1086
1087
|
import('./dock.js').then((m) => { dockMod = m; if (inFrame()) setState({}); }).catch(() => { dockMod = null; });
|
|
1088
|
+
/* 0.55.1 — the dock lists SESSIONS grouped by initiative (dock-sessions.js), in the frame and live mode only. */
|
|
1089
|
+
let dsMod = null;
|
|
1090
|
+
if (!FIXTURE) import('./dock-sessions.js').then((m) => { dsMod = m; if (inFrame()) setState({}); }).catch(() => { dsMod = null; });
|
|
1087
1091
|
let dockParts = null;
|
|
1092
|
+
let dockOrient = null;
|
|
1088
1093
|
function frameDock() {
|
|
1089
1094
|
if (!dockMod || !inFrame()) return null;
|
|
1090
1095
|
const repo = currentProduct();
|
|
@@ -1094,17 +1099,28 @@ function frameDock() {
|
|
|
1094
1099
|
ws,
|
|
1095
1100
|
session: dockMod.sessionMenuButton({
|
|
1096
1101
|
ws,
|
|
1097
|
-
|
|
1098
|
-
withOptions: (d) => { openNewSession(selSlug(), { agent: d.cli }); setState({ newSessTier: d.tier }); },
|
|
1099
|
-
openSkill: (sk) => openTab({ id: 'skill:' + sk.id, kind: 'skill', title: sk.label || sk.skill || sk.id, skillId: sk.id, repo: state.product || null, slug: null, role: 'SKILL' }),
|
|
1102
|
+
open: () => (dsMod ? dsMod.openNewSessionSheet(currentProduct(), { initiative: null }) : newSession()),
|
|
1100
1103
|
}),
|
|
1101
1104
|
fold: dockParts ? dockParts.fold : dockMod.foldButton({ fold: () => window.pmApp.frame.foldDock() }),
|
|
1102
1105
|
focus: dockParts ? dockParts.focus : dockMod.focusButton({ toggle: () => window.pmApp.frame.toggleFocus() }),
|
|
1103
1106
|
};
|
|
1104
1107
|
}
|
|
1105
|
-
if (isMobile()) return { session: dockParts.session, fold: null, focus: null };
|
|
1108
|
+
if (isMobile()) return { session: dockParts.session, fold: null, focus: null, orient: null };
|
|
1106
1109
|
dockMod.paintFocus(dockParts.focus, !!window.pmApp.frame.dockMax);
|
|
1107
|
-
|
|
1110
|
+
// the tabs-on-top / tabs-at-side switch, icon only in the frame (0.55.1); drawn per paint because its icon follows the mode
|
|
1111
|
+
// one element across paints, so the keyboard focus stays on it; only its icon and name follow the mode
|
|
1112
|
+
const v = !!state.tabsVertical;
|
|
1113
|
+
if (!dockOrient) {
|
|
1114
|
+
dockOrient = iconButton({ icon: 'panel-left', ariaLabel: COPY.dock.sideTabs,
|
|
1115
|
+
onClick: () => setState({ tabsVertical: !state.tabsVertical, dsEnding: null }), attrs: { 'data-tabs': 'orient' } });
|
|
1116
|
+
}
|
|
1117
|
+
const oname = v ? 'panel-top' : 'panel-left';
|
|
1118
|
+
const cur = dockOrient.querySelector('[data-icon]');
|
|
1119
|
+
if (!cur || cur.getAttribute('data-icon') !== oname) dockOrient.replaceChildren(icon(oname));
|
|
1120
|
+
const olabel = v ? COPY.dock.topTabs : COPY.dock.sideTabs;
|
|
1121
|
+
dockOrient.setAttribute('aria-label', olabel);
|
|
1122
|
+
dockOrient.title = olabel;
|
|
1123
|
+
return { session: dockParts.session, fold: dockParts.fold, focus: dockParts.focus, orient: dockOrient };
|
|
1108
1124
|
}
|
|
1109
1125
|
|
|
1110
1126
|
/**
|
|
@@ -1277,6 +1293,8 @@ export function renderTabs(host) {
|
|
|
1277
1293
|
const oi = vertical ? 'arrow-left-right' : 'arrow-up-down';
|
|
1278
1294
|
if (!orientIcon.firstChild || orientIcon.firstChild.getAttribute('data-icon') !== oi) orientIcon.replaceChildren(icon(oi));
|
|
1279
1295
|
orientBtn.title = vertical ? 'show the tabs as a strip above the panes' : 'show the tabs as a list at the left of the panes';
|
|
1296
|
+
const ds = fr && dsMod ? currentProduct() : null; // the frame's session list (0.55.1); null = the tab list as before
|
|
1297
|
+
const notListed = (listed) => visTabs.filter((t) => !(t.kind === 'term' && t.sessionId && listed.has(t.sessionId)));
|
|
1280
1298
|
if (vertical) {
|
|
1281
1299
|
tabBar.setAttribute('style', CK_VBAR);
|
|
1282
1300
|
const V_BTN = CK_BTN.replace('margin:0 0 4px', 'margin:0') + ';justify-content:start';
|
|
@@ -1314,10 +1332,25 @@ export function renderTabs(host) {
|
|
|
1314
1332
|
setChildren(tabBar, [
|
|
1315
1333
|
...visTabs.map((t) => tabNode(t, act, splitTab)),
|
|
1316
1334
|
fr ? fr.session : newSessionBtn,
|
|
1317
|
-
FIXTURE ? null : newWebBtn,
|
|
1335
|
+
FIXTURE || fr ? null : newWebBtn,
|
|
1318
1336
|
!FIXTURE && state.webNotice ? el('span', { 'data-r5': 'notice', style: 'align-self:center;font-size:11px;color:var(--destructive);padding:0 6px' }, state.webNotice) : null,
|
|
1319
|
-
newSessionOptsBtn,
|
|
1320
|
-
canSplit ? splitBtn : null,
|
|
1337
|
+
fr ? null : newSessionOptsBtn,
|
|
1338
|
+
canSplit && (!fr || state.splitId) ? splitBtn : null,
|
|
1339
|
+
]);
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
/* 0.55.1 — in the frame the bar lists SESSIONS by initiative (dock-sessions.js); the tabs that are not sessions follow
|
|
1343
|
+
* under "Open". The bar keeps the style the branch above gave it; only its children change. Web, options and split left
|
|
1344
|
+
* the frame's dock (split stays only to undo one already made). */
|
|
1345
|
+
if (ck && ds) {
|
|
1346
|
+
const others = notListed(dsMod.listedSessionIds(ds));
|
|
1347
|
+
setChildren(tabBar, vertical ? [
|
|
1348
|
+
fr.session,
|
|
1349
|
+
...dsMod.railNodes(ds, others.map((t) => cockpitTabNode(t, act, splitTab, true))),
|
|
1350
|
+
el('div', { 'data-tabs': 'vcontrols', class: 'fr-ds-foot' }, state.splitId ? splitBtn : null, fr.orient, fr.focus, fr.fold),
|
|
1351
|
+
] : [
|
|
1352
|
+
...dsMod.stripNodes(ds, others.map((t) => cockpitTabNode(t, act, splitTab))),
|
|
1353
|
+
state.splitId ? splitBtn : null, fr.orient, fr.focus, fr.fold,
|
|
1321
1354
|
]);
|
|
1322
1355
|
}
|
|
1323
1356
|
|
|
@@ -1325,7 +1358,9 @@ export function renderTabs(host) {
|
|
|
1325
1358
|
if (tabBar.scrollTop !== keepY) tabBar.scrollTop = keepY;
|
|
1326
1359
|
// When the tab in front changes, the bar scrolls just enough to show it, so a tab opened at the far end of a crowded strip or a
|
|
1327
1360
|
// long list is seen. Only the bar's own offset is moved (not scrollIntoView, which would also scroll the panels around it).
|
|
1328
|
-
|
|
1361
|
+
// …and when the bar itself changed (the session list landed, or the orientation flipped): the tab may be off screen
|
|
1362
|
+
const frontKey = act ? act.id + '|' + (ds ? 'ds' : '') + (vertical ? 'v' : 'h') : null;
|
|
1363
|
+
if (ck && act && frontKey !== lastFrontId) {
|
|
1329
1364
|
const n = [...tabBar.querySelectorAll('[data-tab]')].find((x) => x.getAttribute('data-tab') === act.id);
|
|
1330
1365
|
if (n) {
|
|
1331
1366
|
const b = tabBar.getBoundingClientRect(), r = n.getBoundingClientRect();
|
|
@@ -1336,7 +1371,7 @@ export function renderTabs(host) {
|
|
|
1336
1371
|
else if (r.right > b.right) tabBar.scrollLeft += r.right - b.right;
|
|
1337
1372
|
}
|
|
1338
1373
|
}
|
|
1339
|
-
lastFrontId =
|
|
1374
|
+
lastFrontId = frontKey;
|
|
1340
1375
|
|
|
1341
1376
|
// ── panes ──
|
|
1342
1377
|
reapClosedPanes();
|
|
@@ -241,8 +241,20 @@ export function requestSpawn(opts) {
|
|
|
241
241
|
return new Promise((resolve) => {
|
|
242
242
|
const req = {
|
|
243
243
|
kind: 'ask', resolve, notice: null, settled: false, timer: null, sent: false,
|
|
244
|
-
|
|
244
|
+
/* `start: true` (0.55.1, the dock's new-session sheet) sends the one-click `start` message instead: the server's
|
|
245
|
+
* start policy then picks the tree — the initiative's own branch, made the first time, or the main tree when
|
|
246
|
+
* there is no initiative — and nothing about branches is decided here. */
|
|
247
|
+
msg: o.start ? {
|
|
248
|
+
t: 'start', name: o.name, agent: o.agent || 'shell', ws: o.ws,
|
|
249
|
+
...(o.initiative ? { initiative: o.initiative } : {}),
|
|
250
|
+
profile: o.profile === undefined ? null : o.profile,
|
|
251
|
+
...(o.skill ? { skill: o.skill } : {}),
|
|
252
|
+
...(o.workType ? { workType: o.workType } : {}),
|
|
253
|
+
...(o.tier ? { tier: o.tier } : {}),
|
|
254
|
+
} : {
|
|
245
255
|
t: 'new',
|
|
256
|
+
...(o.skill ? { skill: o.skill } : {}),
|
|
257
|
+
...(o.workType ? { workType: o.workType } : {}),
|
|
246
258
|
name: o.name,
|
|
247
259
|
agent: o.agent || 'shell',
|
|
248
260
|
ws: o.ws,
|
|
@@ -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,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* lucide-sprite.js — GENERATED by workspace/third-party/build-lucide-sprite.js from lucide-static 1.52.0.
|
|
2
|
-
* Do not edit by hand. The icons: grip-vertical, plus, check, square, square-check-big, x, undo-2, flag, bot, chevron-right, chevron-down, ellipsis, circle-alert, history, arrow-up, arrow-down, diamond, settings, search, refresh-cw, chevron-left, sun, moon, arrow-left, arrow-right, globe, columns-2, arrow-up-down, arrow-left-right.
|
|
2
|
+
* Do not edit by hand. The icons: grip-vertical, plus, check, square, square-check-big, x, undo-2, flag, bot, chevron-right, chevron-down, ellipsis, circle-alert, history, arrow-up, arrow-down, diamond, settings, search, refresh-cw, chevron-left, sun, moon, arrow-left, arrow-right, globe, columns-2, arrow-up-down, arrow-left-right, panel-top, panel-left, maximize-2, minimize-2, git-branch, clock.
|
|
3
3
|
*
|
|
4
4
|
* ISC License
|
|
5
5
|
*
|
|
@@ -75,5 +75,11 @@ export const SPRITE = '<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true
|
|
|
75
75
|
+ '<symbol id="lucide-columns-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" /><path d="M12 3v18" /></symbol>'
|
|
76
76
|
+ '<symbol id="lucide-arrow-up-down" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21 16-4 4-4-4" /><path d="M17 20V4" /><path d="m3 8 4-4 4 4" /><path d="M7 4v16" /></symbol>'
|
|
77
77
|
+ '<symbol id="lucide-arrow-left-right" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3 4 7l4 4" /><path d="M4 7h16" /><path d="m16 21 4-4-4-4" /><path d="M20 17H4" /></symbol>'
|
|
78
|
+
+ '<symbol id="lucide-panel-top" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" /><path d="M3 9h18" /></symbol>'
|
|
79
|
+
+ '<symbol id="lucide-panel-left" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" /><path d="M9 3v18" /></symbol>'
|
|
80
|
+
+ '<symbol id="lucide-maximize-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6" /><path d="m21 3-7 7" /><path d="m3 21 7-7" /><path d="M9 21H3v-6" /></symbol>'
|
|
81
|
+
+ '<symbol id="lucide-minimize-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m14 10 7-7" /><path d="M20 10h-6V4" /><path d="m3 21 7-7" /><path d="M4 14h6v6" /></symbol>'
|
|
82
|
+
+ '<symbol id="lucide-git-branch" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 6a9 9 0 0 0-9 9V3" /><circle cx="18" cy="6" r="3" /><circle cx="6" cy="18" r="3" /></symbol>'
|
|
83
|
+
+ '<symbol id="lucide-clock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /><path d="M12 6v6l4 2" /></symbol>'
|
|
78
84
|
+ '</svg>';
|
|
79
|
-
export const ICONS = ["grip-vertical","plus","check","square","square-check-big","x","undo-2","flag","bot","chevron-right","chevron-down","ellipsis","circle-alert","history","arrow-up","arrow-down","diamond","settings","search","refresh-cw","chevron-left","sun","moon","arrow-left","arrow-right","globe","columns-2","arrow-up-down","arrow-left-right"];
|
|
85
|
+
export const ICONS = ["grip-vertical","plus","check","square","square-check-big","x","undo-2","flag","bot","chevron-right","chevron-down","ellipsis","circle-alert","history","arrow-up","arrow-down","diamond","settings","search","refresh-cw","chevron-left","sun","moon","arrow-left","arrow-right","globe","columns-2","arrow-up-down","arrow-left-right","panel-top","panel-left","maximize-2","minimize-2","git-branch","clock"];
|
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');
|
|
@@ -2384,6 +2391,7 @@ function persist(s) {
|
|
|
2384
2391
|
meta.initiative = s.initiative == null ? null : s.initiative;
|
|
2385
2392
|
meta.attributedBy = s.attributedBy || null;
|
|
2386
2393
|
}
|
|
2394
|
+
if (s.workType) meta.workType = s.workType;
|
|
2387
2395
|
try {
|
|
2388
2396
|
fs.writeFileSync(path.join(SESS_DIR, s.id + '.json'), JSON.stringify(meta));
|
|
2389
2397
|
fs.writeFileSync(path.join(SESS_DIR, s.id + '.scrollback'), s.buf);
|
|
@@ -2696,6 +2704,7 @@ function sessionList() {
|
|
|
2696
2704
|
// make an inference indistinguishable from a record — the exact confusion this field removes.
|
|
2697
2705
|
initiative: 'initiative' in s ? (s.initiative == null ? null : s.initiative) : null,
|
|
2698
2706
|
attributed_by: 'initiative' in s ? (s.attributedBy || null) : null,
|
|
2707
|
+
work_type: s.workType || null,
|
|
2699
2708
|
profile_id: 'profile' in s ? (s.profile == null ? null : s.profile) : null,
|
|
2700
2709
|
profile_line: profilesLib.profileLine(
|
|
2701
2710
|
!('profile' in s) ? undefined
|
|
@@ -2849,7 +2858,10 @@ function writeTmuxConf() {
|
|
|
2849
2858
|
// `env` is an OPT-IN override and only `new-session` ever passes one (profiles R3). has-session /
|
|
2850
2859
|
// kill-session / list-sessions / display-message have no session context, so handing them a
|
|
2851
2860
|
// profile's overlay would apply it to whichever pane the command happened to touch — a silent bug.
|
|
2852
|
-
|
|
2861
|
+
// The tmux daemon is born from the first of these calls and keeps that working directory for its whole life, across
|
|
2862
|
+
// workspace upgrades. It is started in the home folder, never in this server's install folder (see the chdir above).
|
|
2863
|
+
function tmuxCtl(args, env) { return spawnSync(TMUX, ['-L', TMUX_SOCK, ...args], { env: env || sessionEnv(), encoding: 'utf8', cwd: tmuxCwd() }); }
|
|
2864
|
+
function tmuxCwd() { const h = os.homedir(); return dirExists(h) ? h : undefined; }
|
|
2853
2865
|
function tmuxHasSession(name) { return !!TMUX && tmuxCtl(['has-session', '-t', name]).status === 0; }
|
|
2854
2866
|
function tmuxKillSession(name) { if (TMUX && name) tmuxCtl(['kill-session', '-t', name]); }
|
|
2855
2867
|
// Create the tmux session a FRESH spawn runs in. It REFUSES a name that already exists rather
|
|
@@ -3085,7 +3097,27 @@ function resolveProfileForSpawn(id) {
|
|
|
3085
3097
|
: `profile "${id}" is not among the descriptors in ${read.dir} — PMOS will not substitute another` };
|
|
3086
3098
|
}
|
|
3087
3099
|
|
|
3088
|
-
|
|
3100
|
+
/* The type of work a session is doing (0.55.1): the dock shows it under the session's name. Set by what started the
|
|
3101
|
+
* session (a skill, a schedule) or chosen by the PM; changed later from the session's ⋯ sheet. Optional: a session
|
|
3102
|
+
* with none shows nothing, and a session started before this field existed restores with none. */
|
|
3103
|
+
const WORK_TYPES = Object.freeze(['research', 'prototype', 'prd', 'build', 'review', 'report', 'general']);
|
|
3104
|
+
const workTypeOrNull = (v) => (WORK_TYPES.includes(String(v || '').toLowerCase()) ? String(v).toLowerCase() : null);
|
|
3105
|
+
|
|
3106
|
+
/* Starting a session FROM a skill (0.55.1): the same launch a scheduled run makes (fireSchedule), on the PM's press —
|
|
3107
|
+
* the skill's agent, its prompt (filing contract retargeted for this workspace) as the first input, and its tier. */
|
|
3108
|
+
function skillLaunch(wsIdArg, skillId) {
|
|
3109
|
+
const w = getWs(wsIdArg) || homeWorkspace();
|
|
3110
|
+
const root = w ? w.root : SELF_ROOT;
|
|
3111
|
+
const tasks = loadTasks(root);
|
|
3112
|
+
const id = String(skillId || '');
|
|
3113
|
+
const task = tasks.find((t) => t.id === id) || tasks.find((t) => t.skill === id) || null;
|
|
3114
|
+
if (!task) return { ok: false, error: `no skill named ${id.slice(0, 60)} in this repository` };
|
|
3115
|
+
const prompt = task.prompt ? skillStore.retargetFiling(task.prompt, filingContract(w)) : null;
|
|
3116
|
+
return { ok: true, agent: task.agent || 'claude', initialInput: prompt ? prompt + '\r' : null,
|
|
3117
|
+
modelAsk: { skillTier: task.tier || null, skill: task.skill } };
|
|
3118
|
+
}
|
|
3119
|
+
|
|
3120
|
+
function spawnSession({ name, agent, cwd, ws, origin, initialInput, profile, initiative, modelAsk, planItem, workType }) {
|
|
3089
3121
|
const id = nextSessionId();
|
|
3090
3122
|
/* pm-os-absorb Run 1 — every AGENT session gets a token, `<id>.<8 hex>`, in its environment as PMOS_SESSION, and
|
|
3091
3123
|
* PMOS_BIN, this workspace's own bin/pmos.js. `pmos item` run inside it is then attributed `agent:<token>` (the token
|
|
@@ -3109,6 +3141,11 @@ function spawnSession({ name, agent, cwd, ws, origin, initialInput, profile, ini
|
|
|
3109
3141
|
const cmd = isShell ? (commandExists(SHELL) ? SHELL : 'sh') : agent;
|
|
3110
3142
|
// a session's working directory is confined to its OWN workspace root (R3)
|
|
3111
3143
|
const workdir = cwd && insideRoot(root, path.resolve(root, cwd)) ? path.resolve(root, cwd) : root;
|
|
3144
|
+
// A repository folder that was moved or deleted after it was registered: tmux would start the agent in its own folder
|
|
3145
|
+
// instead, and the agent would run in the wrong place or not at all. Refuse here and name the folder.
|
|
3146
|
+
if (!dirExists(workdir)) return exitedSession({ id, ws: w ? w.id : wsId(SELF_ROOT), root, name: (name && String(name).trim()) || agent, agent,
|
|
3147
|
+
origin: tag, profile: null, profileRefusal: null, plan_item: planItem || null, token: sessionToken },
|
|
3148
|
+
workdir, `the folder ${workdir} is not there. Open the repository again from its current folder, then try again.`);
|
|
3112
3149
|
// Resolved once, here, so every path below spawns under the SAME environment the record claims.
|
|
3113
3150
|
// `pres.id` is null whenever the overlay was not applied — including when a profile was asked for
|
|
3114
3151
|
// and could not be read — so the pane line can never name a profile that did nothing (R3/R9).
|
|
@@ -3126,6 +3163,8 @@ function spawnSession({ name, agent, cwd, ws, origin, initialInput, profile, ini
|
|
|
3126
3163
|
// Hand to agent: the breakdown item this session was started for, or null. Set only by /api/plan-items/handoff/go.
|
|
3127
3164
|
base.plan_item = planItem || null;
|
|
3128
3165
|
base.token = sessionToken;
|
|
3166
|
+
const wt = workTypeOrNull(workType);
|
|
3167
|
+
if (wt) base.workType = wt;
|
|
3129
3168
|
|
|
3130
3169
|
/* WHICH MODEL (D75). Resolved once, here, with the profile, so the record can say only what was applied.
|
|
3131
3170
|
* Three states are kept apart — not asked, applied, and "this binary has no model control" — and a choice that
|
|
@@ -3244,7 +3283,7 @@ function spawnSession({ name, agent, cwd, ws, origin, initialInput, profile, ini
|
|
|
3244
3283
|
// A FAILED git operation in a real repo must never spawn a session on the wrong branch (rubric
|
|
3245
3284
|
// hard floor): we return {ok:false} and spawn nothing. Kept as its own function so the
|
|
3246
3285
|
// git-then-maybe-spawn contract is unit-testable without a WS client.
|
|
3247
|
-
function spawnWithBranch({ name, agent, cwd, ws, branch, createBranch, isolate, profile, initiative, modelAsk, origin, initialInput, planItem }) {
|
|
3286
|
+
function spawnWithBranch({ name, agent, cwd, ws, branch, createBranch, isolate, profile, initiative, modelAsk, origin, initialInput, planItem, workType }) {
|
|
3248
3287
|
const w = getWs(ws);
|
|
3249
3288
|
if (!w) return { ok: false, error: 'no workspace' };
|
|
3250
3289
|
// The profile is checked BEFORE any git operation. A composer that picked a profile PMOS cannot
|
|
@@ -3291,7 +3330,7 @@ function spawnWithBranch({ name, agent, cwd, ws, branch, createBranch, isolate,
|
|
|
3291
3330
|
broadcast({ t: 'branch', ws: w.id, ...gitInfo.branchInfo(w.root) }); // let every client refresh the chip
|
|
3292
3331
|
}
|
|
3293
3332
|
}
|
|
3294
|
-
const s = spawnSession({ name, agent: agent || 'shell', cwd, ws: targetWs, profile: pres.id, initiative, modelAsk, origin, initialInput, planItem });
|
|
3333
|
+
const s = spawnSession({ name, agent: agent || 'shell', cwd, ws: targetWs, profile: pres.id, initiative, modelAsk, origin, initialInput, planItem, workType });
|
|
3295
3334
|
// The co-tenancy fact, on the ordinary path too. Note where this sits: OUTSIDE the `if (branch)`
|
|
3296
3335
|
// block above. The isolate default already computes that other sessions would be disturbed — and
|
|
3297
3336
|
// then the whole git block is skipped when no branch was picked, which is the common path and the
|
|
@@ -3394,7 +3433,7 @@ function startPreview({ initiative, repoKey, ws }) {
|
|
|
3394
3433
|
// One click. Resolves the policy, then hands the SAME seam spawnWithBranch already exposes the
|
|
3395
3434
|
// branch/isolate decision it made — so the inherited hard floor (a failed git operation spawns
|
|
3396
3435
|
// nothing) is enforced in exactly one place and is not re-implemented here.
|
|
3397
|
-
function startWork({ initiative, repoKey, ws, agent, name, profile, origin, initialInput, planItem }) {
|
|
3436
|
+
function startWork({ initiative, repoKey, ws, agent, name, profile, origin, initialInput, planItem, workType, modelAsk }) {
|
|
3398
3437
|
const key = repoKeyFor({ repoKey, ws });
|
|
3399
3438
|
if (!key) return { ok: false, error: 'no repository selected' };
|
|
3400
3439
|
const trees = treesOfRepo(key);
|
|
@@ -3422,6 +3461,8 @@ function startWork({ initiative, repoKey, ws, agent, name, profile, origin, init
|
|
|
3422
3461
|
origin, initialInput,
|
|
3423
3462
|
// Hand to agent (pm-os-absorb Run 1): only /api/plan-items/handoff/go passes it.
|
|
3424
3463
|
planItem,
|
|
3464
|
+
// 0.55.1 — the dock's new-session sheet: the type of work, and a skill's model ask.
|
|
3465
|
+
workType, modelAsk: modelAsk || null,
|
|
3425
3466
|
});
|
|
3426
3467
|
if (!r.ok) return r;
|
|
3427
3468
|
return { ok: true, session: r.session, notice: r.notice || plan.why, plan: plan.plan };
|
|
@@ -6265,6 +6306,25 @@ async function handleRequest(req, res) {
|
|
|
6265
6306
|
boardCache.invalidate(root);
|
|
6266
6307
|
return json(res, { ok: true, id, lane, previous: before, changed: true });
|
|
6267
6308
|
}
|
|
6309
|
+
if (p === '/api/session/meta' && req.method === 'POST') {
|
|
6310
|
+
// 0.55.1 — the dock's ⋯ sheet: a session's NAME and TYPE OF WORK. Labels only: no checkout, no respawn. The
|
|
6311
|
+
// initiative has its own route (below) because it carries provenance; these two are just what the PM calls it.
|
|
6312
|
+
let body = {};
|
|
6313
|
+
try { body = JSON.parse((await readBody(req)) || '{}'); }
|
|
6314
|
+
catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
|
|
6315
|
+
const g = pmGuard.requirePmPress(req, body, { act: 'bind' });
|
|
6316
|
+
if (!g.ok) return json(res, { ok: false, error: g.error }, g.status);
|
|
6317
|
+
const s = sessions.get(String(body.id || ''));
|
|
6318
|
+
if (!s) return json(res, { ok: false, error: 'no such session' }, 404);
|
|
6319
|
+
if ('workType' in body) {
|
|
6320
|
+
const wt = body.workType === null || body.workType === '' ? null : workTypeOrNull(body.workType);
|
|
6321
|
+
if (body.workType && !wt) return json(res, { ok: false, error: 'type of work must be one of ' + WORK_TYPES.join(', ') }, 400);
|
|
6322
|
+
if (wt) s.workType = wt; else delete s.workType;
|
|
6323
|
+
}
|
|
6324
|
+
if (typeof body.name === 'string' && body.name.trim()) renameSession(s.id, body.name);
|
|
6325
|
+
persist(s); pushSessions();
|
|
6326
|
+
return json(res, { ok: true, id: s.id, name: s.name, work_type: s.workType || null });
|
|
6327
|
+
}
|
|
6268
6328
|
if (p === '/api/session/initiative' && req.method === 'POST') {
|
|
6269
6329
|
/* Bind a RUNNING session to an initiative — the PM's own word about work already in flight
|
|
6270
6330
|
* (workspace-session-attribution R3/R8).
|
|
@@ -7122,7 +7182,8 @@ async function handleRequest(req, res) {
|
|
|
7122
7182
|
const list = readSchedules(); list.unshift(sched); writeSchedules(list); seeded = true;
|
|
7123
7183
|
}
|
|
7124
7184
|
const result = fireSchedule(sched);
|
|
7125
|
-
|
|
7185
|
+
// A session that could not start is recorded as already ended, so the page says why instead of waiting on a read.
|
|
7186
|
+
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
7187
|
writeDayRun(run);
|
|
7127
7188
|
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
7189
|
...(result.ok ? {} : { error: result.reason || 'the morning-brief session did not start' }) }, result.ok ? 200 : 500);
|
|
@@ -7154,7 +7215,13 @@ async function handleRequest(req, res) {
|
|
|
7154
7215
|
const due = dayRunLib.checkDue(run, { settings: st, now, events: brief ? brief.calendar || [] : [] });
|
|
7155
7216
|
const items = brief ? [...(brief.priorities || []), ...(brief.worth_asking || [])] : [];
|
|
7156
7217
|
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
|
-
|
|
7218
|
+
// The brief session ended and no brief arrived: say so, with the session's own reason, so the page does not wait on a
|
|
7219
|
+
// read that will never finish (it reads as "stuck on Read" otherwise).
|
|
7220
|
+
const bs = run && run.brief_session; const bsess = bs ? sessions.get(bs.id) : null;
|
|
7221
|
+
const readFailed = !run || brief ? null
|
|
7222
|
+
: !bs ? { session: null, why: 'the morning-brief session did not start.' }
|
|
7223
|
+
: (!!bs.ended_at || !bsess || bsess.state === 'exited') ? { session: bs.id, why: (bsess && bsess.note) || 'the session ended without submitting a brief.' } : null;
|
|
7224
|
+
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
7225
|
settings: { briefSources: st.briefSources, scopes: st.scopes, dayCheck: st.dayCheck, websites: st.websites }, sources,
|
|
7159
7226
|
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
7227
|
}
|
|
@@ -7880,9 +7947,13 @@ wss.on('connection', (ws) => {
|
|
|
7880
7947
|
// `initiative` is what the client has always held and never sent: `+ session` is clicked on
|
|
7881
7948
|
// an initiative row, the tab carries its `slug`, and the spawn message named three other
|
|
7882
7949
|
// fields. An absent or unusable value leaves the session unattributed — never a refused spawn.
|
|
7883
|
-
|
|
7950
|
+
// 0.55.1 — a session started FROM a skill: the skill's agent, prompt and tier, resolved here and never sent.
|
|
7951
|
+
const sk = m.skill ? skillLaunch(m.ws, m.skill) : null;
|
|
7952
|
+
if (sk && !sk.ok) { ws.send(JSON.stringify({ t: 'error', error: 'skill: ' + sk.error })); return; }
|
|
7953
|
+
const r = spawnWithBranch({ name: m.name, agent: sk ? sk.agent : m.agent, cwd: m.cwd, ws: m.ws, branch: m.branch, createBranch: m.createBranch, isolate: m.isolate, profile: m.profile, initiative: wsBindable(ws, m.initiative),
|
|
7884
7954
|
// D75 — what the composer chose (a tier, or a model), carried like the profile. Absent = not asked.
|
|
7885
|
-
modelAsk: (m.tier || m.model) ? { explicit: { tier: m.tier, model: m.model, effort: m.effort } } : null
|
|
7955
|
+
modelAsk: sk ? sk.modelAsk : ((m.tier || m.model) ? { explicit: { tier: m.tier, model: m.model, effort: m.effort } } : null),
|
|
7956
|
+
initialInput: sk ? sk.initialInput : null, workType: m.workType });
|
|
7886
7957
|
// A refused profile is its own sentence, not "branch: …" — the composer's error line names
|
|
7887
7958
|
// what PMOS could not read, and nothing was started.
|
|
7888
7959
|
if (!r.ok) { ws.send(JSON.stringify({ t: 'error', error: (/^(profile|model choice) /.test(r.error || '') ? '' : 'branch: ') + r.error })); return; }
|
|
@@ -7894,8 +7965,13 @@ wss.on('connection', (ws) => {
|
|
|
7894
7965
|
// repo-first R1 — the one-click path. It carries an INITIATIVE (or none) and a repository,
|
|
7895
7966
|
// never a branch and never an isolate flag: those are the policy's to decide, and a client
|
|
7896
7967
|
// that could still send them would be a second place the rule lives.
|
|
7968
|
+
const sk = m.skill ? skillLaunch(m.ws, m.skill) : null;
|
|
7969
|
+
if (sk && !sk.ok) { ws.send(JSON.stringify({ t: 'error', error: 'skill: ' + sk.error })); return; }
|
|
7897
7970
|
const r = startWork({ initiative: wsBindable(ws, m.initiative), repoKey: m.repoKey, ws: m.ws,
|
|
7898
|
-
agent: m.agent, name: m.name, profile: m.profile
|
|
7971
|
+
agent: sk ? sk.agent : m.agent, name: m.name, profile: m.profile,
|
|
7972
|
+
initialInput: sk ? sk.initialInput : null, workType: m.workType,
|
|
7973
|
+
// a blank session from the dock carries the Settings tier (D75); a skill's own tier wins
|
|
7974
|
+
modelAsk: sk ? sk.modelAsk : (m.tier ? { explicit: { tier: m.tier } } : null) });
|
|
7899
7975
|
if (!r.ok) { ws.send(JSON.stringify({ t: 'error', error: (/^profile /.test(r.error || '') ? '' : 'start: ') + r.error })); return; }
|
|
7900
7976
|
if (r.notice) ws.send(JSON.stringify({ t: 'notice', notice: r.notice }));
|
|
7901
7977
|
ws.send(JSON.stringify({ t: 'created', id: r.session.id }));
|
|
@@ -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
|
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "workspace-phone-command: `pmos phone` — lib/phone-cli.js reading the startup banner back, and the real launcher's reuse, restart and Tailscale-refused paths on a scratch port.",
|
|
3
|
+
"modules": ["phone-command.test.js"],
|
|
4
|
+
"seconds": { "phone-command.test.js": 4 }
|
|
5
|
+
}
|
|
@@ -12,7 +12,7 @@ The workspace runs with no network (D62). Two third-party browser files are comm
|
|
|
12
12
|
| Package | Version | Licence | Tarball | Tarball sha256 | Shipped file |
|
|
13
13
|
|---|---|---|---|---|---|
|
|
14
14
|
| sortablejs | 1.15.7 | MIT | https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.7.tgz | `66cbfc8fa18860783584fe0c346baf6cbf3399da72d23d21053425ba232abed4` | `public/vendor/Sortable.min.js`: an unmodified copy of `package/Sortable.min.js` (45 478 bytes, sha256 `bf4241bc73fef7f11c59a283a69fe8051cdd31c6d8ff5a2b9ba219e7831fcf76`; npm integrity `sha512-Kk8wLQPlS+yi1ZEf48a4+fzHa4yxjC30M/Sr2AnQu+f/MPwvvX9XjZ6OWejiz8crBsLwSq8GHqaxaET7u6ux0A==`) |
|
|
15
|
-
| lucide-static | 1.52.0 | ISC, plus the Feather MIT notice | https://registry.npmjs.org/lucide-static/-/lucide-static-1.52.0.tgz | `4d2aa079171e4eba4c704844b8b0c9edb6e4cfb2fb8bd86f7b14353994f04a87` | `public/vendor/lucide-sprite.js`: generated by `build-lucide-sprite.js` (
|
|
15
|
+
| lucide-static | 1.52.0 | ISC, plus the Feather MIT notice | https://registry.npmjs.org/lucide-static/-/lucide-static-1.52.0.tgz | `4d2aa079171e4eba4c704844b8b0c9edb6e4cfb2fb8bd86f7b14353994f04a87` | `public/vendor/lucide-sprite.js`: generated by `build-lucide-sprite.js` (13 525 bytes, sha256 `3bb7ec3369206ff1bc8c6ae5dfd91c5d99e5ec89c4357d18b744b594ea366447`). It holds 35 icons: grip-vertical, plus, check, square, square-check-big, x, undo-2, flag, bot, chevron-right, chevron-down, ellipsis, circle-alert, history, arrow-up, arrow-down, diamond, then the eight the frame's chrome draws (Run 3): settings, search, refresh-cw, chevron-left, sun, moon, arrow-left, arrow-right, then globe, columns-2, arrow-up-down and arrow-left-right for the dock strip's web, split and orientation chips (Run 3, C6), then panel-top, panel-left, maximize-2, minimize-2, git-branch and clock for the dock's icon controls and session rows (0.54.2) |
|
|
16
16
|
|
|
17
17
|
The licences are in this folder: [sortablejs-1.15.7.LICENSE](sortablejs-1.15.7.LICENSE) and [lucide-static-1.52.0.LICENSE](lucide-static-1.52.0.LICENSE). The sprite also carries the lucide licence text as its first comment.
|
|
18
18
|
|