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.
- package/README.md +27 -0
- package/package.json +1 -1
- package/payload/KIT-VERSION +2 -2
- package/workspace/README.md +33 -18
- package/workspace/bin/pmos.js +5 -1
- package/workspace/doctor.js +5 -1
- package/workspace/lib/skill-store.js +180 -6
- package/workspace/lib/state-root.js +587 -0
- package/workspace/lib/working-set.js +124 -28
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +2 -2
- package/workspace/public/pm/data.js +86 -0
- package/workspace/public/pm/panel-detail.js +174 -2
- package/workspace/public/pm/panel-initiatives.js +131 -16
- package/workspace/public/pm/panel-newsess.js +507 -0
- package/workspace/public/pm/panel-repos.js +31 -0
- package/workspace/public/pm/panel-tabs.js +34 -2
- package/workspace/public/pm/state.js +45 -1
- package/workspace/public/pm/tab-term.js +288 -15
- package/workspace/server.js +278 -24
- package/workspace/test-run.js +84 -0
|
@@ -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
|
|
49
|
-
*
|
|
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
|
-
|
|
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.
|
|
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(
|
|
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:
|
|
229
|
-
|
|
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
|
|
526
|
-
// it
|
|
527
|
-
const
|
|
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(
|
|
565
|
-
|
|
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',
|
|
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:
|
|
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));
|