create-pmos 0.33.0 → 0.36.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 +10 -0
- package/bin/create-pmos.js +15 -3
- package/package.json +1 -1
- package/payload/KIT-VERSION +2 -2
- package/payload/okf/core/concepts/agent-harness.md +2 -2
- package/payload/okf/core/concepts/design-loop.md +5 -5
- package/payload/okf/core/concepts/eval-driven-pm.md +1 -1
- package/payload/okf/core/concepts/index.md +2 -2
- package/payload/okf/core/concepts/okr-anti-optimism.md +1 -1
- package/payload/okf/core/concepts/okr-tree.md +2 -2
- package/payload/okf/core/concepts/output-eval.md +2 -2
- package/payload/okf/core/concepts/sdlc-loop.md +1 -1
- package/payload/okf/core/concepts/self-refining-harness.md +3 -3
- package/payload/okf/core/concepts/simplified-technical-english.md +74 -0
- package/payload/okf/core/templates/content-brief-template.md +1 -1
- package/payload/okf/core/templates/design-brief-template.md +1 -1
- package/payload/okf/core/templates/eval-rubric-template.md +1 -1
- package/payload/okf/core/templates/log.md +1 -1
- package/payload/okf/core/templates/prd-template.md +1 -1
- package/payload/okf/core/templates/spike-brief-template.md +1 -1
- package/payload/scripts/frontier.sh +6 -2
- package/payload/scripts/gate-initiative.sh +24 -3
- package/payload/scripts/metrics.py +20 -4
- package/payload/scripts/read-jsonl.js +53 -0
- package/payload/scripts/workflow-state.js +144 -35
- package/payload/scripts/workflow-sweep.js +39 -22
- package/payload/skills/SKILL-FORMAT.md +1 -1
- package/payload/skills/copywriting.skill +1 -1
- package/payload/skills/correction-signal-derive.skill +1 -1
- package/payload/skills/design-brief.skill +4 -4
- package/payload/skills/design-reconcile.skill +1 -1
- package/payload/skills/discovery.skill +8 -6
- package/payload/skills/eval-rubric.skill +4 -4
- package/payload/skills/evaluator.skill +1 -1
- package/payload/skills/explain-run.skill +1 -1
- package/payload/skills/explain-surface.skill +6 -6
- package/payload/skills/harness-refine.skill +1 -1
- package/payload/skills/ingest-repo.skill +6 -6
- package/payload/skills/okr.skill +2 -2
- package/payload/skills/pmos-router.skill +8 -8
- package/payload/skills/prd.skill +5 -5
- package/payload/skills/prototype-grill.skill +8 -8
- package/payload/skills/prototype.skill +7 -7
- package/payload/skills/skill-creator.skill +1 -1
- package/payload/skills/source-to-concept.skill +1 -1
- package/payload/skills/ste-term-map.md +69 -0
- package/payload/skills/strategic-brief.skill +4 -4
- package/payload/skills/technical-writing-ste.skill +176 -0
- package/payload/skills-native/copywriting/SKILL.md +1 -1
- package/payload/skills-native/correction-signal-derive/SKILL.md +1 -1
- package/payload/skills-native/design-brief/SKILL.md +4 -4
- package/payload/skills-native/design-reconcile/SKILL.md +1 -1
- package/payload/skills-native/discovery/SKILL.md +8 -6
- package/payload/skills-native/eval-rubric/SKILL.md +4 -4
- package/payload/skills-native/evaluator/SKILL.md +1 -1
- package/payload/skills-native/explain-run/SKILL.md +1 -1
- package/payload/skills-native/explain-surface/SKILL.md +6 -6
- package/payload/skills-native/harness-refine/SKILL.md +1 -1
- package/payload/skills-native/ingest-repo/SKILL.md +6 -6
- package/payload/skills-native/okr/SKILL.md +2 -2
- package/payload/skills-native/pmos-router/SKILL.md +8 -8
- package/payload/skills-native/prd/SKILL.md +5 -5
- package/payload/skills-native/prototype/SKILL.md +7 -7
- package/payload/skills-native/prototype-grill/SKILL.md +8 -8
- package/payload/skills-native/skill-creator/SKILL.md +1 -1
- package/payload/skills-native/source-to-concept/SKILL.md +1 -1
- package/payload/skills-native/strategic-brief/SKILL.md +4 -4
- package/payload/skills-native/technical-writing-ste/SKILL.md +178 -0
- package/workspace/bin/pmos.js +9 -3
- package/workspace/lib/engine/read-jsonl.js +53 -0
- package/workspace/lib/engine/workflow-state.js +144 -35
- package/workspace/lib/file-mode-parse.js +19 -4
- package/workspace/lib/tree-share.js +84 -2
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/board-read.js +38 -0
- package/workspace/public/pm/data.js +102 -1
- package/workspace/public/pm/modes.js +50 -28
- package/workspace/public/pm/panel-detail.js +2 -17
- package/workspace/public/pm/panel-initiatives.js +3 -20
- package/workspace/public/pm/panel-newsess.js +14 -1
- package/workspace/public/pm/panel-tabs.js +53 -14
- package/workspace/public/pm/tab-term.js +30 -2
- package/workspace/server.js +95 -70
- package/workspace/test-run.js +51 -9
|
@@ -706,6 +706,101 @@ function currentProductName() {
|
|
|
706
706
|
return productOrder[0] || null;
|
|
707
707
|
}
|
|
708
708
|
|
|
709
|
+
/* ── the Focus queue ─────────────────────────────────────────────────────────────────────────
|
|
710
|
+
* `/api/console?name=focus` is cross-product (the server accepts and ignores `ws`) and it runs the
|
|
711
|
+
* board and ladder derivations for every registered workspace — a cold derive the boot path
|
|
712
|
+
* deliberately avoids (see `lightRecord`). So it is fetched LAZILY, on entry to focus mode, never
|
|
713
|
+
* at load.
|
|
714
|
+
*
|
|
715
|
+
* The server's view (lib/focus.js buildFocus) is `{queue, handoffs, delegated, upkeep, badge, …}`;
|
|
716
|
+
* modes.js renders the fixture rows' shape `{first, waiting, syn, saw}`. The reshape happens here
|
|
717
|
+
* — one adapter, not a second renderer — and a failed read is published as `{readError}`, which
|
|
718
|
+
* the mode renders as a failed read, never as "a successful read of nothing".
|
|
719
|
+
*/
|
|
720
|
+
|
|
721
|
+
/** One focus row in the shape the mode's queue renderer reads. The `read:` label is the caller's
|
|
722
|
+
* to add (lib/focus.js R4) — the server's string is the content after it. */
|
|
723
|
+
function focusRow(r) {
|
|
724
|
+
return {
|
|
725
|
+
n: r.rank == null ? '' : String(r.rank),
|
|
726
|
+
slug: r.subject || '',
|
|
727
|
+
// `productName` is the board's own key (the repo name data.js registered), which is what
|
|
728
|
+
// `jump` needs; `product` is the server's repo-key path and matches no board.
|
|
729
|
+
prod2: r.productName || '',
|
|
730
|
+
why: r.why || '',
|
|
731
|
+
read: r.read ? 'read: ' + r.read : '',
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/** The server's focus view, reshaped to what focusList renders. Every row is the payload's — no
|
|
736
|
+
* fixture constant is reachable from here. */
|
|
737
|
+
function focusFrom(view) {
|
|
738
|
+
const h = view.handoffs || {};
|
|
739
|
+
const d = view.delegated || {};
|
|
740
|
+
const u = view.upkeep || {};
|
|
741
|
+
|
|
742
|
+
const syn = [];
|
|
743
|
+
for (const s of d.running || []) {
|
|
744
|
+
syn.push({
|
|
745
|
+
t: (s.name || s.id) + ' · ' + (s.word || s.shown) + (s.productName ? ' — ' + s.productName : ''),
|
|
746
|
+
hasBtn: false,
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
/* `starts` are the engine's own delegate OFFERS. They render as rows, not buttons: this surface
|
|
750
|
+
* has no runner that can feed `initialInput` into a spawn, and a ▶ button PMOS cannot honour is
|
|
751
|
+
* worse than no button (the server's own starts_note rule). */
|
|
752
|
+
for (const s of d.starts || []) {
|
|
753
|
+
syn.push({ t: (s.initiative || '') + ' — ' + (s.action || s.skill || ''), hasBtn: false });
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
const saw = [];
|
|
757
|
+
if (u.memory && u.memory.heading) {
|
|
758
|
+
saw.push({ t: u.memory.heading + ' — ' + (u.memory.body || u.memory.state || '') });
|
|
759
|
+
}
|
|
760
|
+
const bud = u.budgets || {};
|
|
761
|
+
if (bud.state === 'error') {
|
|
762
|
+
saw.push({ t: 'health budgets — read failed: ' + (bud.error || 'no reason given') });
|
|
763
|
+
}
|
|
764
|
+
for (const r of bud.rows || []) {
|
|
765
|
+
saw.push({
|
|
766
|
+
t: r.id + ' — cap ' + (r.cap || 'not declared') + ' · ' + (r.total || 0)
|
|
767
|
+
+ ' anchored initiative' + (r.total === 1 ? '' : 's'),
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
// The server's caveat travels with the counts it qualifies: without it the totals above read as
|
|
771
|
+
// the cap test, which is exactly the claim the view refuses to make.
|
|
772
|
+
if ((bud.rows || []).length && bud.caveat) saw.push({ t: bud.caveat });
|
|
773
|
+
|
|
774
|
+
return {
|
|
775
|
+
first: (view.queue || []).map(focusRow),
|
|
776
|
+
waiting: (h.items || []).map(focusRow),
|
|
777
|
+
// The handoffs body for a tracker that is NOT CONNECTED — a different sentence from an empty
|
|
778
|
+
// list, rendered by the mode in place of the calm empty line.
|
|
779
|
+
waitingNote: (h.items || []).length ? null : (h.body || null),
|
|
780
|
+
syn,
|
|
781
|
+
saw,
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
let focusInFlight = false;
|
|
786
|
+
|
|
787
|
+
async function loadFocus() {
|
|
788
|
+
if (FIXTURE || focusInFlight) return;
|
|
789
|
+
focusInFlight = true;
|
|
790
|
+
// The in-flight read is STATED. With nothing published yet, the mode would otherwise render its
|
|
791
|
+
// calm empty line — a claim of a successful read of nothing while the read is still out. A
|
|
792
|
+
// usable earlier payload stays on screen for the refresh instead.
|
|
793
|
+
if (!state.focus || state.focus.readError) setState({ focus: { reading: true } });
|
|
794
|
+
try {
|
|
795
|
+
const view = await getJson('/api/console', { name: 'focus' });
|
|
796
|
+
setState({
|
|
797
|
+
focus: view && view.ok !== false
|
|
798
|
+
? focusFrom(view)
|
|
799
|
+
: { readError: (view && view.error) || 'the focus queue could not be read' },
|
|
800
|
+
});
|
|
801
|
+
} finally { focusInFlight = false; }
|
|
802
|
+
}
|
|
803
|
+
|
|
709
804
|
/* ── sessions ───────────────────────────────────────────────────────────────────────────────
|
|
710
805
|
* Polled rather than pushed. The WebSocket that carries `sessions` frames belongs to panel 4's
|
|
711
806
|
* terminal module, which owns the attach/input protocol; opening a SECOND socket here to read a
|
|
@@ -1214,7 +1309,7 @@ export function runningCount(s) {
|
|
|
1214
1309
|
* newly selected initiative, and a repaint when it lands.
|
|
1215
1310
|
*/
|
|
1216
1311
|
if (!FIXTURE) {
|
|
1217
|
-
let lastSel = null, lastProduct = null;
|
|
1312
|
+
let lastSel = null, lastProduct = null, lastMode = null;
|
|
1218
1313
|
subscribe(() => {
|
|
1219
1314
|
if (state.product !== lastProduct) {
|
|
1220
1315
|
lastProduct = state.product;
|
|
@@ -1222,6 +1317,12 @@ if (!FIXTURE) {
|
|
|
1222
1317
|
if (p && p.ws && state.wsId !== p.ws) setState({ wsId: p.ws });
|
|
1223
1318
|
if (p && !p.board) loadInitiatives(state.product);
|
|
1224
1319
|
}
|
|
1320
|
+
// The focus view is fetched on ENTRY to focus mode, and re-fetched on every re-entry — the
|
|
1321
|
+
// queue is a statement about now, and the mode switch is the moment the PM asks for it.
|
|
1322
|
+
if (state.mode !== lastMode) {
|
|
1323
|
+
lastMode = state.mode;
|
|
1324
|
+
if (state.mode === 'focus') loadFocus();
|
|
1325
|
+
}
|
|
1225
1326
|
const sel = state.sel;
|
|
1226
1327
|
if (sel && sel !== lastSel) {
|
|
1227
1328
|
lastSel = sel;
|
|
@@ -47,6 +47,7 @@ import { el, setChildren } from './dom.js';
|
|
|
47
47
|
import { state, setState, FIXTURE } from './state.js';
|
|
48
48
|
import * as FX from './fixture.js';
|
|
49
49
|
import { openDoc, openTerm } from './panel-initiatives.js';
|
|
50
|
+
import { boardOf, records, currentProduct } from './board-read.js';
|
|
50
51
|
|
|
51
52
|
/* ── the prototype's small helpers ─────────────────────────────────────────────────────────── */
|
|
52
53
|
|
|
@@ -127,23 +128,7 @@ const knowMap = () => ({ ...(state.know || seedOr(() => FX.KNOW, {})), ...(state
|
|
|
127
128
|
const profileList = () => state.profiles || seedOr(() => FX.PROFILES, []);
|
|
128
129
|
const intgList = () => state.intgs || seedOr(() => FX.INTG, []);
|
|
129
130
|
|
|
130
|
-
/* ── the board, read the way panels 2 and 3 read it
|
|
131
|
-
|
|
132
|
-
const boardOf = () =>
|
|
133
|
-
(state.board && typeof state.board === 'object' ? state.board : (FIXTURE ? FX.ALLGROUPS : {}));
|
|
134
|
-
|
|
135
|
-
function records() {
|
|
136
|
-
const base = state.records && typeof state.records === 'object'
|
|
137
|
-
? state.records
|
|
138
|
-
: (FIXTURE ? FX.DATA : {});
|
|
139
|
-
return { ...base, ...state.userInits };
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function currentProduct() {
|
|
143
|
-
if (state.product) return state.product;
|
|
144
|
-
const keys = Object.keys(boardOf());
|
|
145
|
-
return keys.length ? keys[0] : 'workspace';
|
|
146
|
-
}
|
|
131
|
+
/* ── the board, read the way panels 2 and 3 read it (board-read.js — one derivation) ───────── */
|
|
147
132
|
|
|
148
133
|
/** The initiative a mode acts on when it starts a session (line 1397). */
|
|
149
134
|
function selectedSlug() {
|
|
@@ -207,12 +192,12 @@ function focusList() {
|
|
|
207
192
|
});
|
|
208
193
|
/* The queue is a JUDGEMENT about what is blocked on the PM, so it is never invented here. In
|
|
209
194
|
* fixture mode it is the prototype's four ranked items; in real mode it is whatever the data
|
|
210
|
-
* layer published on `state.focus` (
|
|
211
|
-
*
|
|
195
|
+
* layer published on `state.focus` (loadFocus's adapted `/api/console?name=focus` view —
|
|
196
|
+
* {first, waiting, syn, saw}, or {readError} for a failed read); with neither, each section
|
|
197
|
+
* renders its label and one honest empty line. */
|
|
212
198
|
const F = state.focus || (FIXTURE
|
|
213
199
|
? { first: FX.FOCUS_FIRST, waiting: FX.FOCUS_WAITING, syn: FX.FOCUS_SYN }
|
|
214
200
|
: { first: [], waiting: [], syn: [] });
|
|
215
|
-
const live = FIXTURE || !!state.focus;
|
|
216
201
|
const first = (F.first || []).map(fRow);
|
|
217
202
|
const waiting = (F.waiting || []).map(fRow);
|
|
218
203
|
const MEM = memoryOf();
|
|
@@ -220,14 +205,20 @@ function focusList() {
|
|
|
220
205
|
const syn = (F.syn || []).map((f) => ({
|
|
221
206
|
...f,
|
|
222
207
|
on: () => {
|
|
223
|
-
|
|
208
|
+
/* The delegate spawn is the FIXTURE's own seed session. A real-mode row never carries this
|
|
209
|
+
* action (data.js renders the server's `starts` without a button — a button would claim a
|
|
210
|
+
* spawn this surface has no runner for), so the gate is stated, not assumed. */
|
|
211
|
+
if (FIXTURE && f.action === 'delegate-billing-stories') {
|
|
224
212
|
const d = FX.DELEGATE_SESSION;
|
|
225
213
|
startSession(d.slug, { ...d.sess });
|
|
226
214
|
}
|
|
227
215
|
},
|
|
228
216
|
}));
|
|
229
217
|
|
|
230
|
-
|
|
218
|
+
/* Upkeep is data-driven like the other sections: the fixture seed renders ONLY in fixture mode;
|
|
219
|
+
* in real mode the rows are the server's own upkeep view, adapted by data.js. Seed rows behind a
|
|
220
|
+
* published `state.focus` were "the beautiful shell" waiting to activate. */
|
|
221
|
+
const saw = (FIXTURE ? FX.focusSaw(MEM) : (F.saw || [])).map((f) => ({
|
|
231
222
|
...f,
|
|
232
223
|
on: () => {
|
|
233
224
|
if (f.action === 'mode:memory') { setState({ mode: 'memory' }); return; }
|
|
@@ -236,7 +227,7 @@ function focusList() {
|
|
|
236
227
|
jump(prod2, slug);
|
|
237
228
|
}
|
|
238
229
|
},
|
|
239
|
-
}))
|
|
230
|
+
}));
|
|
240
231
|
|
|
241
232
|
const board = boardOf();
|
|
242
233
|
const ends = productTuples().map(([id]) => {
|
|
@@ -244,6 +235,21 @@ function focusList() {
|
|
|
244
235
|
return g0 ? { prod: id, obj: g0.gid + ' · ' + g0.gname } : null;
|
|
245
236
|
}).filter(Boolean);
|
|
246
237
|
|
|
238
|
+
/* A failed read renders as a failed read, and an in-flight one says so — never "a successful
|
|
239
|
+
* read of nothing" for either. Used by the four sections the focus payload feeds; the objectives
|
|
240
|
+
* section derives from the board and keeps its own empty line. */
|
|
241
|
+
const emptyOr = () => {
|
|
242
|
+
if (F.readError) {
|
|
243
|
+
return el('div', { style: 'padding:5px 14px 6px;font-size:11px;color:var(--destructive)' },
|
|
244
|
+
iv('read failed — ' + F.readError));
|
|
245
|
+
}
|
|
246
|
+
if (F.reading) {
|
|
247
|
+
return el('div', { style: 'padding:5px 14px 6px;font-size:11px;color:var(--muted-foreground);font-style:italic' },
|
|
248
|
+
'reading the focus queue…');
|
|
249
|
+
}
|
|
250
|
+
return emptyLine();
|
|
251
|
+
};
|
|
252
|
+
|
|
247
253
|
const out = [];
|
|
248
254
|
out.push(el('div', { style: 'padding:8px 14px 4px;font-size:11.5px;color:var(--muted-foreground);line-height:1.5' },
|
|
249
255
|
'This list holds only items where ',
|
|
@@ -270,7 +276,7 @@ function focusList() {
|
|
|
270
276
|
el('span', { style: 'display:block;font-size:11px;margin-top:1px' }, iv(ff.why)),
|
|
271
277
|
el('span', { style: 'display:block;font-family:var(--mono);font-size:10px;color:var(--muted-foreground)' }, iv(ff.read))));
|
|
272
278
|
}
|
|
273
|
-
if (!first.length) out.push(
|
|
279
|
+
if (!first.length) out.push(emptyOr());
|
|
274
280
|
|
|
275
281
|
out.push(el('div', { style: LABEL9 }, 'handoffs · waiting on other teams'));
|
|
276
282
|
for (const fw of waiting) {
|
|
@@ -281,7 +287,15 @@ function focusList() {
|
|
|
281
287
|
el('span', { style: 'display:block;font-size:11px;color:var(--muted-foreground);margin-top:1px' }, iv(fw.why)),
|
|
282
288
|
el('span', { style: 'display:block;font-family:var(--mono);font-size:10px;color:var(--muted-foreground)' }, iv(fw.read))));
|
|
283
289
|
}
|
|
284
|
-
|
|
290
|
+
/* `waitingNote` is the server's own sentence for a tracker that is NOT CONNECTED — a different
|
|
291
|
+
* fact from a successful read that found nothing, so it gets the server's words, not the calm
|
|
292
|
+
* empty line. */
|
|
293
|
+
if (!waiting.length) {
|
|
294
|
+
out.push(F.waitingNote
|
|
295
|
+
? el('div', { style: 'padding:5px 14px 6px;font-size:11px;color:var(--muted-foreground);font-style:italic' },
|
|
296
|
+
iv(F.waitingNote))
|
|
297
|
+
: emptyOr());
|
|
298
|
+
}
|
|
285
299
|
|
|
286
300
|
out.push(el('div', { style: LABEL9 }, 'delegated · agents in parallel'));
|
|
287
301
|
for (const fy of syn) {
|
|
@@ -292,7 +306,7 @@ function focusList() {
|
|
|
292
306
|
style: 'flex:none;font-family:var(--mono);font-size:10.5px;padding:4px 9px;min-height:26px;background:var(--brand);color:oklch(0.987 0.002 197.1);font-weight:500',
|
|
293
307
|
}, iv(fy.btn))));
|
|
294
308
|
}
|
|
295
|
-
if (!syn.length) out.push(
|
|
309
|
+
if (!syn.length) out.push(emptyOr());
|
|
296
310
|
|
|
297
311
|
out.push(el('div', { style: LABEL9 }, 'upkeep · keep the system sharp'));
|
|
298
312
|
for (const fs2 of saw) {
|
|
@@ -302,7 +316,7 @@ function focusList() {
|
|
|
302
316
|
hover: ROW_HOVER,
|
|
303
317
|
}, el('span', { style: 'display:block;font-size:11.5px;color:var(--muted-foreground);line-height:1.45' }, iv(fs2.t))));
|
|
304
318
|
}
|
|
305
|
-
if (!saw.length) out.push(
|
|
319
|
+
if (!saw.length) out.push(emptyOr());
|
|
306
320
|
return out;
|
|
307
321
|
}
|
|
308
322
|
|
|
@@ -668,12 +682,20 @@ function schedBody(det) {
|
|
|
668
682
|
onWhen: (e) => save({ ...t, when: e.target.value }),
|
|
669
683
|
onRunCmd: (e) => save({ ...t, runCmd: e.target.value }),
|
|
670
684
|
onScope: (e) => save({ ...t, scope: e.target.value }),
|
|
685
|
+
/* The green "ok — ran … 0 failures" line is the FIXTURE's seed log. In real mode nothing
|
|
686
|
+
* executes on this press — no runner is wired to this surface — so writing it there would be
|
|
687
|
+
* a fabricated execution success (the watermelon the honesty contract forbids). The real-mode
|
|
688
|
+
* line says exactly what happened: nothing. */
|
|
671
689
|
onRun: () => save({
|
|
672
690
|
...t,
|
|
673
|
-
log: [{
|
|
691
|
+
log: [FIXTURE ? {
|
|
674
692
|
ts: '12:41 today',
|
|
675
693
|
t: t.runCmd ? 'ok — ran `' + t.runCmd + '` manually, 0 failures' : 'nothing ran — no command defined yet',
|
|
676
694
|
c: t.runCmd ? 'var(--success)' : 'var(--warning)',
|
|
695
|
+
} : {
|
|
696
|
+
ts: '—',
|
|
697
|
+
t: 'nothing ran — no runner is wired to this surface yet',
|
|
698
|
+
c: 'var(--warning)',
|
|
677
699
|
}, ...(t.log || [])],
|
|
678
700
|
}),
|
|
679
701
|
onToggle: () => save({ ...t, enabled: !t.enabled }),
|
|
@@ -54,6 +54,7 @@ import { state, setState, FIXTURE } from './state.js';
|
|
|
54
54
|
import * as FX from './fixture.js';
|
|
55
55
|
import { openDoc, openTab, openTerm, fileNewModel } from './panel-initiatives.js';
|
|
56
56
|
import { addProductBody } from './panel-repos.js';
|
|
57
|
+
import { boardOf, records, currentProduct } from './board-read.js';
|
|
57
58
|
|
|
58
59
|
/* ── the prototype's small helpers ─────────────────────────────────────────────────────────── */
|
|
59
60
|
|
|
@@ -102,23 +103,7 @@ function declare(fn, payload) {
|
|
|
102
103
|
if (typeof write === 'function') { try { write(payload); } catch { /* the UI still shows it */ } }
|
|
103
104
|
}
|
|
104
105
|
|
|
105
|
-
/* ── the board, read the same way panel 2 reads it
|
|
106
|
-
|
|
107
|
-
const boardOf = () =>
|
|
108
|
-
(state.board && typeof state.board === 'object' ? state.board : (FIXTURE ? FX.ALLGROUPS : {}));
|
|
109
|
-
|
|
110
|
-
function records() {
|
|
111
|
-
const base = state.records && typeof state.records === 'object'
|
|
112
|
-
? state.records
|
|
113
|
-
: (FIXTURE ? FX.DATA : {});
|
|
114
|
-
return { ...base, ...state.userInits };
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function currentProduct() {
|
|
118
|
-
if (state.product) return state.product;
|
|
119
|
-
const keys = Object.keys(boardOf());
|
|
120
|
-
return keys.length ? keys[0] : 'workspace';
|
|
121
|
-
}
|
|
106
|
+
/* ── the board, read the same way panel 2 reads it (board-read.js — one derivation) ────────── */
|
|
122
107
|
|
|
123
108
|
function baseGroups(product) {
|
|
124
109
|
const board = boardOf();
|
|
@@ -70,6 +70,7 @@
|
|
|
70
70
|
import { el, setChildren } from './dom.js';
|
|
71
71
|
import { state, setState, FIXTURE } from './state.js';
|
|
72
72
|
import * as FX from './fixture.js';
|
|
73
|
+
import { boardOf, records, currentProduct } from './board-read.js';
|
|
73
74
|
|
|
74
75
|
/* ── the prototype's small helpers, verbatim in behaviour ──────────────────────────────────── */
|
|
75
76
|
|
|
@@ -82,20 +83,8 @@ const selRow = (on) => ({
|
|
|
82
83
|
bl: on ? 'var(--brand)' : 'transparent',
|
|
83
84
|
});
|
|
84
85
|
|
|
85
|
-
/*
|
|
86
|
-
*
|
|
87
|
-
* "The beautiful shell" in the PRD's failure-mode list is exactly this: fixture rows rendering in
|
|
88
|
-
* the real app, so the diff looks clean while nothing is connected. */
|
|
89
|
-
const seedBoard = () => (FIXTURE ? FX.ALLGROUPS : {});
|
|
90
|
-
const seedRecords = () => (FIXTURE ? FX.DATA : {});
|
|
91
|
-
const boardOf = () => (state.board && typeof state.board === 'object' ? state.board : seedBoard());
|
|
92
|
-
|
|
93
|
-
/** The repository whose board is showing (line 1395). */
|
|
94
|
-
function currentProduct() {
|
|
95
|
-
if (state.product) return state.product;
|
|
96
|
-
const keys = Object.keys(boardOf());
|
|
97
|
-
return keys.length ? keys[0] : 'workspace';
|
|
98
|
-
}
|
|
86
|
+
/* `boardOf` / `records` / `currentProduct` live in board-read.js — one derivation, four readers.
|
|
87
|
+
* The seed-board honesty rule ("the beautiful shell") is stated and enforced there. */
|
|
99
88
|
|
|
100
89
|
/** The Objective/KR groups a repository starts from, before user-filed additions. */
|
|
101
90
|
function baseGroups(product) {
|
|
@@ -106,12 +95,6 @@ function baseGroups(product) {
|
|
|
106
95
|
return FIXTURE && !state.board ? (FX.ALLGROUPS[product] || FX.ALLGROUPS.workspace) : [];
|
|
107
96
|
}
|
|
108
97
|
|
|
109
|
-
/** Every initiative record: the board's own records, with user-filed ones layered on (line 1392). */
|
|
110
|
-
function records() {
|
|
111
|
-
const base = state.records && typeof state.records === 'object' ? state.records : seedRecords();
|
|
112
|
-
return { ...base, ...state.userInits };
|
|
113
|
-
}
|
|
114
|
-
|
|
115
98
|
/** GROUPS (lines 1425–1430) — user groups appended, user KRs and user initiatives folded in. */
|
|
116
99
|
function groupsFor(product) {
|
|
117
100
|
const extraGroups = (state.userGroups && state.userGroups[product]) || [];
|
|
@@ -97,6 +97,9 @@ export function openNewSession(slug, seed) {
|
|
|
97
97
|
newSessAgent: s.agent || 'shell',
|
|
98
98
|
newSessBranch: '', newSessBranchNew: '', newSessBranchMode: false,
|
|
99
99
|
newSessIsolate: null, newSessError: null, newSessBusy: false,
|
|
100
|
+
// Cleared so a form opened on a DIFFERENT repository never shows the previous repo's branch
|
|
101
|
+
// list while its own read is out — "reading the repository…" is true; a stale list is not.
|
|
102
|
+
newSessBranches: null,
|
|
100
103
|
// Cleared on the way IN as well as on the way out: a form that opened still showing the profile
|
|
101
104
|
// a previous, abandoned question had chosen would be proposing an answer nobody gave.
|
|
102
105
|
preflightProfile: s.profile === undefined ? null : s.profile,
|
|
@@ -135,9 +138,17 @@ function proposeName(slug) {
|
|
|
135
138
|
|
|
136
139
|
/* Fired when the form opens, and again when the server says a branch moved. Both reads are
|
|
137
140
|
* independent and each writes only its own key, so a repository with no git still gets its profile
|
|
138
|
-
* chips and a machine with no profile directory still gets its branch picker.
|
|
141
|
+
* chips and a machine with no profile directory still gets its branch picker.
|
|
142
|
+
*
|
|
143
|
+
* `readsFor` records WHICH open the reads were actually issued for. The lazy data.js import above
|
|
144
|
+
* can land after the form opened — refreshReads then returned early and nothing ever retried, so
|
|
145
|
+
* the form sat on "reading the repository…" for ever. The render path re-issues the reads once per
|
|
146
|
+
* open (newSessionBlock); the token is what stops it re-fetching on every repaint. */
|
|
147
|
+
let readsFor = null;
|
|
148
|
+
|
|
139
149
|
function refreshReads() {
|
|
140
150
|
if (FIXTURE || !dataMod) return;
|
|
151
|
+
readsFor = state.newSess;
|
|
141
152
|
const ws = state.wsId || null;
|
|
142
153
|
if (typeof dataMod.loadBranches === 'function') {
|
|
143
154
|
dataMod.loadBranches(ws).then((r) => setState({ newSessBranches: r || null })).catch(() => {});
|
|
@@ -451,6 +462,8 @@ async function onStart() {
|
|
|
451
462
|
/** The pre-flight block, or null when it is closed. Called from panel-tabs.js's render. */
|
|
452
463
|
export function newSessionBlock() {
|
|
453
464
|
if (!state.newSess) return null;
|
|
465
|
+
// The reads this open still owes — issued here when data.js resolved after openNewSession ran.
|
|
466
|
+
if (!FIXTURE && dataMod && readsFor !== state.newSess) refreshReads();
|
|
454
467
|
absorbBranchBroadcast();
|
|
455
468
|
const slug = state.newSess.slug;
|
|
456
469
|
const busy = !!state.newSessBusy;
|
|
@@ -33,6 +33,7 @@ import { openTab, openTerm as openTermTab, openDoc as openDocTab } from './panel
|
|
|
33
33
|
import { mountDoc, disposeDoc, docText } from './tab-doc.js';
|
|
34
34
|
import { mountTask } from './tab-task.js';
|
|
35
35
|
import { newSessionBlock, openNewSession } from './panel-newsess.js';
|
|
36
|
+
import { boardOf, currentProduct } from './board-read.js';
|
|
36
37
|
|
|
37
38
|
/* tab-term.js is loaded LAZILY and on purpose: it is another module's file, a static import of a
|
|
38
39
|
* file that is not on disk yet takes the whole panel down with it, and an empty terminal pane is a
|
|
@@ -45,16 +46,7 @@ import('./tab-term.js')
|
|
|
45
46
|
/** `{{ expr }}` — one interpolated value in its own span, the way the prototype's runtime emits it. */
|
|
46
47
|
const iv = (v) => el('span', null, v === null || v === undefined ? '' : String(v));
|
|
47
48
|
|
|
48
|
-
/* ── the board, read exactly the way panels 2 and 3 read it
|
|
49
|
-
|
|
50
|
-
const boardOf = () =>
|
|
51
|
-
(state.board && typeof state.board === 'object' ? state.board : (FIXTURE ? FX.ALLGROUPS : {}));
|
|
52
|
-
|
|
53
|
-
function currentProduct() {
|
|
54
|
-
if (state.product) return state.product;
|
|
55
|
-
const keys = Object.keys(boardOf());
|
|
56
|
-
return keys.length ? keys[0] : 'workspace';
|
|
57
|
-
}
|
|
49
|
+
/* ── the board, read exactly the way panels 2 and 3 read it (board-read.js) ─────────────────── */
|
|
58
50
|
|
|
59
51
|
/** The selected initiative (prototype line 1397) — the slug `+ session` starts a session on. */
|
|
60
52
|
function selSlug() {
|
|
@@ -85,7 +77,11 @@ function visibleTabs() {
|
|
|
85
77
|
const map = slugProductOf();
|
|
86
78
|
return (state.tabs || []).filter((t) => {
|
|
87
79
|
const sp = map[t.slug] !== undefined ? map[t.slug] : t.repo;
|
|
88
|
-
|
|
80
|
+
/* `== null` on purpose: an unattributed-session tab carries `slug: null, repo: null`
|
|
81
|
+
* (panel-initiatives openTerm), and a strict undefined check excluded it from EVERY
|
|
82
|
+
* repository's bar — an open, PTY-attached tab with no visible chip and no close button. A
|
|
83
|
+
* tab on no board is visible everywhere, whichever null-ish shape says so. */
|
|
84
|
+
return sp == null || sp === product;
|
|
89
85
|
});
|
|
90
86
|
}
|
|
91
87
|
|
|
@@ -645,6 +641,8 @@ const CH_BTN = 'flex:none;font-family:var(--mono);font-size:10.5px;padding:4px 1
|
|
|
645
641
|
|
|
646
642
|
function peerRow(prompt, p) {
|
|
647
643
|
const armed = state.endConfirm === p.id;
|
|
644
|
+
const refusal = state.endRefusal && state.endRefusal.id === p.id ? state.endRefusal.why : null;
|
|
645
|
+
const asked = state.endAsked === p.id;
|
|
648
646
|
return el('div', {
|
|
649
647
|
style: 'display:flex;gap:10px;align-items:flex-start;padding:8px 10px;border:1px solid oklch(1 0 0 / 10%);background:oklch(1 0 0 / 3%)',
|
|
650
648
|
},
|
|
@@ -664,6 +662,17 @@ function peerRow(prompt, p) {
|
|
|
664
662
|
style: 'font-family:var(--mono);font-size:10.5px;color:oklch(0.62 0.02 213);overflow:hidden;text-overflow:ellipsis;white-space:nowrap',
|
|
665
663
|
}, iv(p.doing)) : null,
|
|
666
664
|
el('div', { style: 'font-size:11px;color:' + RISK_COLOR[p.risk.level] + ';line-height:1.45' }, iv(p.risk.why)),
|
|
665
|
+
/* WHY THE END DID NOT HAPPEN, in the row the PM was looking at. A refusal recorded in state
|
|
666
|
+
* and rendered nowhere would be the same silence in a new place — the whole defect here was a
|
|
667
|
+
* failure that looked like a success. */
|
|
668
|
+
refusal ? el('div', {
|
|
669
|
+
style: 'font-size:11px;color:var(--destructive);line-height:1.45',
|
|
670
|
+
}, iv('not ended — ' + refusal)) : null,
|
|
671
|
+
/* And the honest reading of a SENT end: asked, not done. The row leaves this list when the
|
|
672
|
+
* server stops reporting the session, and that is the confirmation. */
|
|
673
|
+
asked ? el('div', {
|
|
674
|
+
style: 'font-size:11px;color:oklch(0.62 0.02 213);line-height:1.45',
|
|
675
|
+
}, iv('asked it to end — this row disappears when the session actually stops')) : null,
|
|
667
676
|
),
|
|
668
677
|
el('div', { style: 'flex:none;display:flex;gap:6px;align-items:center' },
|
|
669
678
|
el('button', {
|
|
@@ -672,9 +681,39 @@ function peerRow(prompt, p) {
|
|
|
672
681
|
}, 'open it'),
|
|
673
682
|
el('button', {
|
|
674
683
|
onClick: () => {
|
|
675
|
-
if (!armed) { setState({ endConfirm: p.id }); return; }
|
|
676
|
-
|
|
677
|
-
|
|
684
|
+
if (!armed) { setState({ endConfirm: p.id, endRefusal: null }); return; }
|
|
685
|
+
/* THE ANSWER IS READ, NOT DISCARDED. This was an unconditional `setState({endConfirm:null})`
|
|
686
|
+
* after a call whose return value was thrown away, so three separate paths — no module, a
|
|
687
|
+
* `FIXTURE || !id` refusal, and a closed socket — disarmed the button on a request that
|
|
688
|
+
* never left the page. The third is the one an ordinary PM meets: in a fresh window with
|
|
689
|
+
* no terminal tab the chooser is populated over HTTP while this socket has never opened.
|
|
690
|
+
*
|
|
691
|
+
* The decision itself is in lib/tree-share so the BLOCKING suite can assert it; a rule
|
|
692
|
+
* that lived here would only ever be tested by the browser suites, which CI runs under
|
|
693
|
+
* `continue-on-error: true` — they report and never block. */
|
|
694
|
+
const TS = typeof window !== 'undefined' ? window.PMOSTreeShare : null;
|
|
695
|
+
const hasModule = !!(termMod && typeof termMod.killSession === 'function');
|
|
696
|
+
const at = TS && typeof TS.endAttempt === 'function'
|
|
697
|
+
? TS.endAttempt({
|
|
698
|
+
id: p.id,
|
|
699
|
+
hasModule,
|
|
700
|
+
socketOpen: !!(termMod && typeof termMod.socketOpen === 'function' && termMod.socketOpen()),
|
|
701
|
+
fixture: FIXTURE,
|
|
702
|
+
})
|
|
703
|
+
/* The module that decides is the same module that built this row, so it cannot be
|
|
704
|
+
* missing here. If it ever is, refuse — a destructive control has no safe default. */
|
|
705
|
+
: { send: false, why: 'the workspace could not decide whether this could be sent, so it was not' };
|
|
706
|
+
if (!at.send) {
|
|
707
|
+
/* STAYS ARMED. Re-arming is the only signal the PM has that nothing happened, so a
|
|
708
|
+
* refusal must not clear `endConfirm` — the disarm was itself the false report. */
|
|
709
|
+
setState({ endRefusal: { id: p.id, why: at.why } });
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
termMod.killSession(p.id);
|
|
713
|
+
/* `asked`, never `ended`. All this page knows is that a frame was written to a socket;
|
|
714
|
+
* whether the session died is the server's to report, and the row leaving the list is
|
|
715
|
+
* what says so. */
|
|
716
|
+
setState({ endConfirm: null, endRefusal: null, endAsked: p.id });
|
|
678
717
|
},
|
|
679
718
|
style: CH_BTN + (armed ? ';color:var(--destructive);border-color:var(--destructive)' : ''),
|
|
680
719
|
hover: CHIP_HOVER,
|
|
@@ -256,9 +256,15 @@ export function requestSpawn(opts) {
|
|
|
256
256
|
};
|
|
257
257
|
SOCK.spawnQueue.push(req);
|
|
258
258
|
/* Armed BEFORE the flush, and armed whether or not the message goes out now: a request waiting
|
|
259
|
-
* for a socket that never opens must end in a sentence, not in a form that spins for ever.
|
|
259
|
+
* for a socket that never opens must end in a sentence, not in a form that spins for ever.
|
|
260
|
+
*
|
|
261
|
+
* Only an UNSENT ask leaves the queue here — the server never heard it, so no answer is coming.
|
|
262
|
+
* A SENT one keeps its slot, settled: the queue's ORDER is the correlation, and removing a slot
|
|
263
|
+
* the server will still answer (spawnWithBranch can legitimately outlive this timer on a large
|
|
264
|
+
* repo) would hand its late `created` to the next request in line — a different click's PTY.
|
|
265
|
+
* The `created` handler detaches the session a dead slot's answer names. */
|
|
260
266
|
req.timer = setTimeout(() => {
|
|
261
|
-
SOCK.spawnQueue = SOCK.spawnQueue.filter((x) => x !== req);
|
|
267
|
+
if (!req.sent) SOCK.spawnQueue = SOCK.spawnQueue.filter((x) => x !== req);
|
|
262
268
|
settleAsk(req, {
|
|
263
269
|
ok: false,
|
|
264
270
|
error: req.sent
|
|
@@ -369,6 +375,11 @@ function handle(m) {
|
|
|
369
375
|
const req = takeSent(true);
|
|
370
376
|
if (!req) return;
|
|
371
377
|
if (req.kind === 'ask') {
|
|
378
|
+
// A slot the timeout already settled has no caller left: the form reported the failure and
|
|
379
|
+
// moved on, so the session this answer names is unowned — detach it rather than leak a
|
|
380
|
+
// running PTY nobody asked for. Consuming the slot (above) is what keeps the NEXT answer
|
|
381
|
+
// correlated with the next request.
|
|
382
|
+
if (req.settled) { send({ t: 'detach', id: m.id }); return; }
|
|
372
383
|
// Nobody is on screen waiting for this. Hand the caller the id and whatever non-fatal truth
|
|
373
384
|
// arrived just before it; the caller opens the tab, and the tab carries the id so bind()
|
|
374
385
|
// ATTACHES rather than starting a second session for the same click.
|
|
@@ -925,6 +936,23 @@ export function killSession(id) {
|
|
|
925
936
|
return send({ t: 'kill', id });
|
|
926
937
|
}
|
|
927
938
|
|
|
939
|
+
/**
|
|
940
|
+
* Is the terminal socket open right now?
|
|
941
|
+
*
|
|
942
|
+
* Exported so a caller can decide BEFORE acting rather than discover afterwards. `killSession`
|
|
943
|
+
* already returned `false` on a closed socket and its own docstring said a caller should use that
|
|
944
|
+
* to say "not sent" — the chooser discarded it, and a destructive control disarmed on a request
|
|
945
|
+
* that never left the page. The pre-flight is what lets the chooser refuse and SAY WHY, and the
|
|
946
|
+
* reason has to be distinguishable from "the module is not loaded", which the return value alone
|
|
947
|
+
* cannot express.
|
|
948
|
+
*
|
|
949
|
+
* The readiness constant is `1` (`OPEN`) and nothing else: `CONNECTING` is not open, and a frame
|
|
950
|
+
* written to a `CLOSING` socket is a frame nobody receives.
|
|
951
|
+
*/
|
|
952
|
+
export function socketOpen() {
|
|
953
|
+
return !!(SOCK.ws && SOCK.ws.readyState === 1);
|
|
954
|
+
}
|
|
955
|
+
|
|
928
956
|
/**
|
|
929
957
|
* Write a status line into a tab's terminal, by tab id — for a caller that holds a fact about a
|
|
930
958
|
* session but not the pane showing it.
|