create-pmos 0.25.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -45,8 +45,21 @@
45
45
  * Three rules hold it honest. Rows are DUPLICATED, never moved — an initiative keeps its place under
46
46
  * its own anchor, because the anchor is the truth (D59) and a shortcut that re-homes work is a second
47
47
  * filing system. The group is ABSENT, not empty, when nothing is live. And the join is
48
- * lib/working-set.js's, which is the server's own `initiative/<id>` two-hopper — so a session the
49
- * join cannot reach contributes nothing rather than being bucketed somewhere plausible.
48
+ * lib/working-set.js's — so a session the join cannot reach never lands on a row it does not belong
49
+ * to.
50
+ *
51
+ * ── What that first cut got wrong (workspace-session-attribution) ────────────────────────────
52
+ * It joined on the `initiative/<id>` branch convention ALONE, which holds in one of four ordinary
53
+ * situations — and the proof was this file's own history: the initiative that shipped this group was
54
+ * built on a `claude/*` branch, so the group could not see the session that wrote it. A session now
55
+ * RECORDS what it is for, and `attributeSessions` resolves recorded → branch → unattributed, saying
56
+ * which path answered.
57
+ *
58
+ * The other half was worse than a narrow join. A session the board could not place was DROPPED, so a
59
+ * running agent was invisible on the surface whose job is to say what is running. Refusing to guess
60
+ * and refusing to mention are different things, and only the first is honesty — hence `orphanRows`,
61
+ * which lists them under the group, claims no initiative for them, and offers the bind that turns
62
+ * the PM's knowledge into a recorded declaration.
50
63
  *
51
64
  * Fixture mode is structurally unaffected, not conditionally suppressed: `pollSessions` returns early
52
65
  * on FIXTURE and `foldWorkspaces` never runs, so `state.sessions` is empty and there are no branches.
@@ -148,11 +161,26 @@ const branches = () => (state.wsBranches && typeof state.wsBranches === 'object'
148
161
  * `return` two modules away.
149
162
  */
150
163
  function liveSessions() {
151
- if (FIXTURE) return {};
164
+ return attribution().byInitiative;
165
+ }
166
+
167
+ /**
168
+ * The full attribution — `{byInitiative, via, unattributed}` — computed ONCE per render and shared
169
+ * by the row markers, the working-set group, the unattributed row and the search. Deriving it three
170
+ * times would be three chances to disagree about one fact.
171
+ *
172
+ * The join now prefers what a session RECORDED over what its branch is called; `via` says which
173
+ * answered, and `unattributed` is every live session neither path could place. That last list is
174
+ * why this returns the whole shape rather than just the map: a running agent PMOS cannot attribute
175
+ * still has to be visible somewhere, and dropping it was half the defect this fixes.
176
+ */
177
+ function attribution() {
178
+ const empty = { byInitiative: {}, via: {}, unattributed: [] };
179
+ if (FIXTURE) return empty;
152
180
  const ws = WS();
153
- if (!ws) return {};
181
+ if (!ws || typeof ws.attributeSessions !== 'function') return empty;
154
182
  const map = branches();
155
- return ws.sessionsByInitiative(state.sessions, (id) => map[id] || null);
183
+ return ws.attributeSessions(state.sessions, (id) => map[id] || null);
156
184
  }
157
185
 
158
186
  /**
@@ -161,13 +189,14 @@ function liveSessions() {
161
189
  * `onBoard` is what stops a tab or a working tree belonging to ANOTHER repository putting a row on
162
190
  * this repository's board — `slugProduct()` is the same map panel 4 scopes its tabs by.
163
191
  */
164
- function workingSet(live, product) {
192
+ function workingSet(attr, product) {
165
193
  if (FIXTURE) return [];
166
194
  const ws = WS();
167
195
  if (!ws) return [];
168
196
  const owner = slugProduct();
169
197
  return ws.workingSet({
170
- live,
198
+ live: attr.byInitiative,
199
+ via: attr.via,
171
200
  trees: ws.initiativesWithTree(Object.values(branches())),
172
201
  tabs: ws.initiativesWithTab(state.tabs),
173
202
  onBoard: (slug) => owner[slug] === product,
@@ -224,9 +253,25 @@ export function openTab(tab) {
224
253
  }
225
254
 
226
255
  export function openTerm(slug, sess) {
256
+ /* THE TAB ID, and why a session with no initiative gets a different one.
257
+ *
258
+ * `term:<slug>:<name>` is the id every initiative-opened terminal has always had, and it is what
259
+ * makes `openTab` focus an already-open tab instead of duplicating it. A session PMOS cannot place
260
+ * on an initiative has no slug — and interpolating one anyway produced `term:null:agent 1`, where
261
+ * every unattributed session with the same name would collide on one tab. Keying those by the
262
+ * SERVER's own session id instead is both collision-free and more honest: the tab is a view of
263
+ * that process, not of an initiative it does not belong to. */
264
+ const key = slug ? 'term:' + slug + ':' + sess.name : 'term:sess:' + ((sess && sess.id) || sess.name);
227
265
  openTab({
228
- id: 'term:' + slug + ':' + sess.name, kind: 'term', title: sess.name, slug,
229
- repo: slugProduct()[slug], sess: sess.name, state: sess.st, profile: sess.profile || 'claude-work',
266
+ id: key, kind: 'term', title: sess.name, slug: slug || null,
267
+ /* `profile` is the session's OWN, or null — never the literal 'claude-work'. It used to default
268
+ * to that string, which was a claim about a descriptor this client had never read: nothing in
269
+ * data.js reads /api/profiles, so the name was invented here and then filed on the session. It
270
+ * also became load-bearing the moment the tab's profile started travelling to the server on a
271
+ * spawn, because `resolveProfileForSpawn` REFUSES a profile it cannot find — a fabricated
272
+ * default would have turned "no profile chosen" into "session refused". Every display reader
273
+ * (fixture.js:853, modes.js:658/808) already supplies its own fallback for null. */
274
+ repo: slug ? slugProduct()[slug] : null, sess: sess.name, state: sess.st, profile: sess.profile || null,
230
275
  /* The SERVER's session id, when the row that was clicked carries one. This is the exact join:
231
276
  * with it, the terminal attaches to precisely the PTY the PM pointed at, and a reload attaches
232
277
  * to the same one again. Without it the tab falls back to matching by NAME, and names repeat
@@ -333,11 +378,67 @@ function groupNode(g, product) {
333
378
  body.push(el('div', { style: 'padding:5px 12px 6px 27px;font-size:11px;color:var(--muted-foreground);font-style:italic' },
334
379
  'nothing filed yet — a successful read of nothing'));
335
380
  }
381
+ // Rows this group carries that are NOT initiatives — today, the live sessions PMOS cannot place.
382
+ // Appended by the caller so `groupNode` stays the one renderer and does not learn a second model.
383
+ if (g.after) body.push(g.after);
336
384
  }
337
385
 
338
386
  return el('div', { style: 'margin-top:4px' }, groupHeader, body);
339
387
  }
340
388
 
389
+ /* ── the live sessions PMOS cannot place ──────────────────────────────────────────────────────
390
+ *
391
+ * One row per unattributable live session, under the working-set group, in the same row shape the
392
+ * initiatives use so the eye reads one list. It deliberately does NOT look like an initiative row:
393
+ * the id slot carries the session's own name and its glyph is the session state, because this row
394
+ * stands for a process, not for a record.
395
+ *
396
+ * Clicking it opens that session's terminal — the same `openTerm` join every other session click
397
+ * uses, by the server's id, with no `spawn`. `+ bind` is the second control: it turns the PM's
398
+ * knowledge of what the session is for into a recorded declaration, which is the only way a session
399
+ * started before this field existed can ever join an initiative.
400
+ */
401
+ function orphanRows(rows) {
402
+ const ss = SS();
403
+ const ws = WS();
404
+ const head = el('div', {
405
+ style: 'display:flex;align-items:baseline;gap:6px;padding:6px 12px 3px 27px',
406
+ title: ws ? ws.unattributedTitle(rows) : null,
407
+ },
408
+ el('span', { style: 'flex:none;font-family:var(--mono);font-size:10px;color:var(--warning)' }, iv('⌁')),
409
+ el('span', { style: 'font-size:10.5px;color:var(--muted-foreground);overflow:hidden;text-overflow:ellipsis;white-space:nowrap' },
410
+ iv('not on an initiative — ' + rows.length + ' live session' + (rows.length === 1 ? '' : 's'))),
411
+ );
412
+
413
+ const body = rows.map((s) => {
414
+ const st = ss && ss.GLYPH[s.shown] ? s.shown : null;
415
+ return el('button', {
416
+ onClick: () => openTerm(null, { name: s.name, st: s.shown, label: s.label, id: s.id, profile: s.profile_id }),
417
+ style: 'display:flex;align-items:center;gap:7px;width:100%;padding:5px 12px 5px 27px;min-height:32px;border-left:2px solid transparent;background:transparent',
418
+ hover: 'background:var(--accent)',
419
+ title: (s.name || s.id) + ' — ' + (s.label || s.shown || '')
420
+ + '\nPMOS was never told what this session is for. + bind records it.',
421
+ },
422
+ el('span', { style: 'flex:none;font-size:12px;color:' + (st && ss ? ss.COLOR[st] : 'var(--muted-foreground)') },
423
+ st && ss ? ss.GLYPH[st] : '·'),
424
+ el('span', { style: 'font-family:var(--mono);font-size:12.5px;color:var(--foreground);overflow:hidden;text-overflow:ellipsis;white-space:nowrap' },
425
+ iv(s.name || s.id)),
426
+ el('span', { style: 'margin-left:auto;flex:none;font-size:9px;color:var(--muted-foreground)' }, iv(s.label || s.shown || '')),
427
+ );
428
+ });
429
+
430
+ const bind = el('button', {
431
+ onClick: () => setState({ bindSess: rows.length === 1 ? rows[0].id : null, bindResult: null, detail: { kind: 'bind' }, col2: true }),
432
+ style: 'display:flex;align-items:center;gap:7px;width:100%;padding:6px 12px 6px 27px;color:var(--muted-foreground)',
433
+ hover: 'color:var(--foreground);background:var(--accent)',
434
+ },
435
+ el('span', { style: 'flex:none;width:9px;text-align:center;font-size:14px' }, '+'),
436
+ el('span', { style: 'font-size:11px' }, 'bind a session to an initiative'),
437
+ );
438
+
439
+ return el('div', null, head, body, bind);
440
+ }
441
+
341
442
  /* ── "matches elsewhere" — artifacts and sessions that the group list cannot show ──────────── */
342
443
 
343
444
  /**
@@ -522,9 +623,10 @@ export function renderInitiatives(host) {
522
623
  const DATA = records();
523
624
  const selSlug = selectedSlug();
524
625
  const detKind = (state.detail && state.detail.kind) || '';
525
- // ONE join per render, shared by the row markers, the working-set group and the search. Computing
526
- // it three times would be three chances to disagree about one fact.
527
- const live = liveSessions();
626
+ // ONE attribution per render, shared by the row markers, the working-set group, the unattributed
627
+ // row and the search. Computing it four times would be four chances to disagree about one fact.
628
+ const attr = attribution();
629
+ const live = attr.byInitiative;
528
630
 
529
631
  /** One row's view model. `why` is null everywhere except in the working-set group. */
530
632
  const rowOf = (slug, why) => {
@@ -561,13 +663,26 @@ export function renderInitiatives(host) {
561
663
  *
562
664
  * Absent when empty. Not a "0", not a placeholder: a heading over nothing is a heading the PM
563
665
  * learns to skip. */
564
- const now = workingSet(live, product).filter((e) => !q || e.slug.includes(q));
565
- if (now.length) {
666
+ const now = workingSet(attr, product).filter((e) => !q || e.slug.includes(q));
667
+ /* EVERY LIVE SESSION PMOS CANNOT PLACE (workspace-session-attribution R5).
668
+ *
669
+ * The board shipped joining on the `initiative/<id>` branch alone, so a session on `main`, on a
670
+ * `claude/*` branch, or in a repo with no git simply vanished from it. Refusing to guess which
671
+ * initiative it belongs to is right; refusing to MENTION it is not, and only the first is honesty.
672
+ *
673
+ * It is rendered as a row inside the same group rather than as a group of its own, because it is
674
+ * not an initiative and must not sit in a list keyed by one. `+ bind` opens the chooser that
675
+ * turns it into the PM's own declaration. */
676
+ const orphans = q ? [] : (attr.unattributed || []);
677
+ if (now.length || orphans.length) {
678
+ const rows = now.map((e) => rowOf(e.slug, WS() ? WS().reasonTitle(e) : null));
566
679
  kids.push(groupNode({
567
- gid: 'NOW', dyn: true, idc: FX.C.brand, gname: 'working on', gct: String(now.length),
680
+ gid: 'NOW', dyn: true, idc: FX.C.brand, gname: 'working on',
681
+ gct: String(now.length + orphans.length),
568
682
  hasKr: true, krid: '⑂', krname: 'live sessions · checked-out trees · open tabs',
569
- inits: now.map((e) => rowOf(e.slug, WS() ? WS().reasonTitle(e) : null)),
683
+ inits: rows,
570
684
  isEmpty: false,
685
+ after: orphans.length ? orphanRows(orphans) : null,
571
686
  }, product));
572
687
  }
573
688
  for (const g of groups) kids.push(groupNode(g, product));