create-pmos 0.55.1 → 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.
@@ -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
- newSession: () => newSession(),
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
- return { session: dockParts.session, fold: dockParts.fold, focus: dockParts.focus };
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
- if (ck && act && act.id !== lastFrontId) {
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 = act ? act.id : null;
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
- msg: {
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,
@@ -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"];
@@ -2391,6 +2391,7 @@ function persist(s) {
2391
2391
  meta.initiative = s.initiative == null ? null : s.initiative;
2392
2392
  meta.attributedBy = s.attributedBy || null;
2393
2393
  }
2394
+ if (s.workType) meta.workType = s.workType;
2394
2395
  try {
2395
2396
  fs.writeFileSync(path.join(SESS_DIR, s.id + '.json'), JSON.stringify(meta));
2396
2397
  fs.writeFileSync(path.join(SESS_DIR, s.id + '.scrollback'), s.buf);
@@ -2703,6 +2704,7 @@ function sessionList() {
2703
2704
  // make an inference indistinguishable from a record — the exact confusion this field removes.
2704
2705
  initiative: 'initiative' in s ? (s.initiative == null ? null : s.initiative) : null,
2705
2706
  attributed_by: 'initiative' in s ? (s.attributedBy || null) : null,
2707
+ work_type: s.workType || null,
2706
2708
  profile_id: 'profile' in s ? (s.profile == null ? null : s.profile) : null,
2707
2709
  profile_line: profilesLib.profileLine(
2708
2710
  !('profile' in s) ? undefined
@@ -3095,7 +3097,27 @@ function resolveProfileForSpawn(id) {
3095
3097
  : `profile "${id}" is not among the descriptors in ${read.dir} — PMOS will not substitute another` };
3096
3098
  }
3097
3099
 
3098
- function spawnSession({ name, agent, cwd, ws, origin, initialInput, profile, initiative, modelAsk, planItem }) {
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 }) {
3099
3121
  const id = nextSessionId();
3100
3122
  /* pm-os-absorb Run 1 — every AGENT session gets a token, `<id>.<8 hex>`, in its environment as PMOS_SESSION, and
3101
3123
  * PMOS_BIN, this workspace's own bin/pmos.js. `pmos item` run inside it is then attributed `agent:<token>` (the token
@@ -3141,6 +3163,8 @@ function spawnSession({ name, agent, cwd, ws, origin, initialInput, profile, ini
3141
3163
  // Hand to agent: the breakdown item this session was started for, or null. Set only by /api/plan-items/handoff/go.
3142
3164
  base.plan_item = planItem || null;
3143
3165
  base.token = sessionToken;
3166
+ const wt = workTypeOrNull(workType);
3167
+ if (wt) base.workType = wt;
3144
3168
 
3145
3169
  /* WHICH MODEL (D75). Resolved once, here, with the profile, so the record can say only what was applied.
3146
3170
  * Three states are kept apart — not asked, applied, and "this binary has no model control" — and a choice that
@@ -3259,7 +3283,7 @@ function spawnSession({ name, agent, cwd, ws, origin, initialInput, profile, ini
3259
3283
  // A FAILED git operation in a real repo must never spawn a session on the wrong branch (rubric
3260
3284
  // hard floor): we return {ok:false} and spawn nothing. Kept as its own function so the
3261
3285
  // git-then-maybe-spawn contract is unit-testable without a WS client.
3262
- 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 }) {
3263
3287
  const w = getWs(ws);
3264
3288
  if (!w) return { ok: false, error: 'no workspace' };
3265
3289
  // The profile is checked BEFORE any git operation. A composer that picked a profile PMOS cannot
@@ -3306,7 +3330,7 @@ function spawnWithBranch({ name, agent, cwd, ws, branch, createBranch, isolate,
3306
3330
  broadcast({ t: 'branch', ws: w.id, ...gitInfo.branchInfo(w.root) }); // let every client refresh the chip
3307
3331
  }
3308
3332
  }
3309
- 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 });
3310
3334
  // The co-tenancy fact, on the ordinary path too. Note where this sits: OUTSIDE the `if (branch)`
3311
3335
  // block above. The isolate default already computes that other sessions would be disturbed — and
3312
3336
  // then the whole git block is skipped when no branch was picked, which is the common path and the
@@ -3409,7 +3433,7 @@ function startPreview({ initiative, repoKey, ws }) {
3409
3433
  // One click. Resolves the policy, then hands the SAME seam spawnWithBranch already exposes the
3410
3434
  // branch/isolate decision it made — so the inherited hard floor (a failed git operation spawns
3411
3435
  // nothing) is enforced in exactly one place and is not re-implemented here.
3412
- 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 }) {
3413
3437
  const key = repoKeyFor({ repoKey, ws });
3414
3438
  if (!key) return { ok: false, error: 'no repository selected' };
3415
3439
  const trees = treesOfRepo(key);
@@ -3437,6 +3461,8 @@ function startWork({ initiative, repoKey, ws, agent, name, profile, origin, init
3437
3461
  origin, initialInput,
3438
3462
  // Hand to agent (pm-os-absorb Run 1): only /api/plan-items/handoff/go passes it.
3439
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,
3440
3466
  });
3441
3467
  if (!r.ok) return r;
3442
3468
  return { ok: true, session: r.session, notice: r.notice || plan.why, plan: plan.plan };
@@ -6280,6 +6306,25 @@ async function handleRequest(req, res) {
6280
6306
  boardCache.invalidate(root);
6281
6307
  return json(res, { ok: true, id, lane, previous: before, changed: true });
6282
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
+ }
6283
6328
  if (p === '/api/session/initiative' && req.method === 'POST') {
6284
6329
  /* Bind a RUNNING session to an initiative — the PM's own word about work already in flight
6285
6330
  * (workspace-session-attribution R3/R8).
@@ -7902,9 +7947,13 @@ wss.on('connection', (ws) => {
7902
7947
  // `initiative` is what the client has always held and never sent: `+ session` is clicked on
7903
7948
  // an initiative row, the tab carries its `slug`, and the spawn message named three other
7904
7949
  // fields. An absent or unusable value leaves the session unattributed — never a refused spawn.
7905
- const r = spawnWithBranch({ name: m.name, 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),
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),
7906
7954
  // D75 — what the composer chose (a tier, or a model), carried like the profile. Absent = not asked.
7907
- 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 });
7908
7957
  // A refused profile is its own sentence, not "branch: …" — the composer's error line names
7909
7958
  // what PMOS could not read, and nothing was started.
7910
7959
  if (!r.ok) { ws.send(JSON.stringify({ t: 'error', error: (/^(profile|model choice) /.test(r.error || '') ? '' : 'branch: ') + r.error })); return; }
@@ -7916,8 +7965,13 @@ wss.on('connection', (ws) => {
7916
7965
  // repo-first R1 — the one-click path. It carries an INITIATIVE (or none) and a repository,
7917
7966
  // never a branch and never an isolate flag: those are the policy's to decide, and a client
7918
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; }
7919
7970
  const r = startWork({ initiative: wsBindable(ws, m.initiative), repoKey: m.repoKey, ws: m.ws,
7920
- 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) });
7921
7975
  if (!r.ok) { ws.send(JSON.stringify({ t: 'error', error: (/^profile /.test(r.error || '') ? '' : 'start: ') + r.error })); return; }
7922
7976
  if (r.notice) ws.send(JSON.stringify({ t: 'notice', notice: r.notice }));
7923
7977
  ws.send(JSON.stringify({ t: 'created', id: r.session.id }));
@@ -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` (11 930 bytes, sha256 `692b332b79456b0fa8a47ef813a2ce54d6f5f47d00e41e85277d6ecd39e6a38b`). It holds 29 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) |
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