create-pmos 0.85.0 → 0.86.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-pmos",
3
- "version": "0.85.0",
3
+ "version": "0.86.0",
4
4
  "description": "Scaffold the PMOS portable kit — run the PMOS product-management workflow (initiative → lane-fit spec → contract → build → three gates, with work-type lanes for features, research, maintenance, incidents, and content) entirely inside your own repo, with all state as git-versioned files. Includes `workflow-sweep`, which audits the state every board derives from and separates real defects from work you simply have not done. `create-pmos workspace` also installs + launches the local Terminal Workspace GUI. No database, no network, no telemetry.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -1,3 +1,3 @@
1
- kernel-source: pmos@2d5583a
1
+ kernel-source: pmos@f91d32d
2
2
  assembled: 2026-10-07
3
3
  mode: vendor
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Eval calibration log"
4
4
  description: "Append-only record of every PM override of an evaluator verdict."
5
- generated: {by: process:create-pmos, at: "2026-10-07T16:18:36Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-07T16:27:16Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Health budgets"
4
4
  description: "Closed registry of health-budget anchors (D59), edited only by the PM."
5
- generated: {by: process:create-pmos, at: "2026-10-07T16:18:36Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-07T16:27:16Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Discoveries and friction log"
4
4
  description: "Append-only record of what runs discovered and where the process rubbed."
5
- generated: {by: process:create-pmos, at: "2026-10-07T16:18:36Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-07T16:27:16Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -10,6 +10,12 @@ Open My work from the top bar, from ⌘K (**My work**), or press `g w`.
10
10
 
11
11
  My work lists every Jira ticket and every breakdown (WBS) item assigned to you, in all your registered repositories. It ranks them, shows what each one asks, and lets you give an agent an instruction for it. The agent's draft then waits for you in Agenda.
12
12
 
13
+ ## The screen
14
+
15
+ The page has the same layout as Agenda. The title says how many items wait for a brief. At the right of the title, four buttons switch the list: **To brief**, **Agents**, **Ready** and **All**, each with its count. A strip below the title shows when Jira was last read and how many tickets and plan items the read holds. **Read again** is at the right of the strip.
16
+
17
+ Two cards fill the rest of the page. The left card lists the items. Each row has a dot for its tier (red for P1, amber for P2, grey for the rest). A badge shows when the item is due, and a **Decision** badge marks an item that looks like a decision only you can make. The right card, **Brief**, shows the open item. On a phone, the two cards stack.
18
+
13
19
  ## Where the items come from
14
20
 
15
21
  - **Jira tickets.** The workspace does not read Jira. The **my-work-read** skill reads it in an agent session, with your own Atlassian connection. It reads the open issues assigned to you, with their comments and links, and writes a context card for each: who asked, what they ask, a short summary of the thread, the questions still open, and a suggested instruction for an agent. It does not copy the comments. Press **Read again** in My work to run it.
@@ -38,7 +44,7 @@ Being assigned a ticket does not mean an agent must resolve it. Every new item s
38
44
  2. Change the instruction. It starts as the agent's suggestion.
39
45
  3. Press **Run agent**. Or press **I'll handle it** to keep the item yourself.
40
46
 
41
- Run agent adds one job to today's day run, with your instruction. The job runs like every Agenda job: at most 5 at once, one draft, and nothing is sent. A hand icon marks an item that looks like a decision only you can make. An agent can still prepare it.
47
+ Run agent adds one job to today's day run, with your instruction. The job runs like every Agenda job: at most 5 at once, one draft, and nothing is sent. The **Decision** badge marks an item that looks like a decision only you can make. An agent can still prepare it.
42
48
 
43
49
  **Back to brief** returns an item to To brief, unless an agent is working on it.
44
50
 
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.85.0",
3
+ "version": "0.86.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pmos-workspace",
9
- "version": "0.85.0",
9
+ "version": "0.86.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@xterm/addon-fit": "^0.11.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.85.0",
3
+ "version": "0.86.0",
4
4
  "private": true,
5
5
  "description": "PMOS Terminal Workspace — local companion hosting real PTYs beside the library and gates (D62)",
6
6
  "license": "MIT",
@@ -5928,12 +5928,51 @@
5928
5928
  }
5929
5929
  .rp-mywork-top {
5930
5930
  display: flex;
5931
- flex-wrap: wrap;
5931
+ flex-direction: column;
5932
+ gap: 12px;
5933
+ }
5934
+ .rp-mywork-header {
5935
+ display: flex;
5936
+ justify-content: space-between;
5932
5937
  align-items: center;
5933
5938
  gap: 12px;
5934
5939
  }
5935
- .rp-mywork-top > .rp-head__title {
5936
- margin-right: auto;
5940
+ .rp-mywork-header h2 {
5941
+ flex: 1;
5942
+ }
5943
+ .rp-mywork-strip {
5944
+ display: flex;
5945
+ align-items: center;
5946
+ gap: 10px;
5947
+ padding: 8px 12px;
5948
+ border: 1px solid var(--border);
5949
+ border-radius: 8px;
5950
+ background: var(--card);
5951
+ }
5952
+ .rp-mywork-title {
5953
+ flex: 1;
5954
+ margin: 0;
5955
+ font-size: 20px;
5956
+ font-weight: 600;
5957
+ }
5958
+ .rp-mywork-strip > .rp-mywork-hint:first-child {
5959
+ color: var(--foreground);
5960
+ }
5961
+ .rp-mywork-row {
5962
+ --background: transparent;
5963
+ }
5964
+ .rp-mywork-spacer {
5965
+ flex: 1;
5966
+ }
5967
+ .rp-mywork-dot--p1 {
5968
+ background: var(--destructive);
5969
+ }
5970
+ .rp-mywork-dot--p2 {
5971
+ background: var(--warning);
5972
+ }
5973
+ .rp-mywork-dot--p3,
5974
+ .rp-mywork-dot--p4 {
5975
+ background: var(--muted-foreground);
5937
5976
  }
5938
5977
  .rp-mywork-body {
5939
5978
  display: flex;
@@ -5942,32 +5981,69 @@
5942
5981
  }
5943
5982
  .rp-mywork-grid {
5944
5983
  display: grid;
5945
- grid-template-columns: minmax(280px, 3fr) 2fr;
5946
- gap: 24px;
5947
- align-items: start;
5984
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1.25fr);
5985
+ gap: 12px;
5986
+ align-items: stretch;
5948
5987
  }
5949
5988
  .rp-mywork-list {
5950
5989
  display: flex;
5951
5990
  flex-direction: column;
5952
- gap: 8px;
5991
+ gap: 0;
5953
5992
  min-width: 0;
5954
5993
  }
5955
5994
  .rp-mywork-views {
5956
5995
  border-bottom: 1px solid var(--border);
5996
+ margin-bottom: 8px;
5957
5997
  }
5958
5998
  .rp-mywork-row {
5959
- border-top: 1px solid var(--border);
5999
+ border: 1px solid transparent;
6000
+ border-radius: 6px;
5960
6001
  }
5961
- .rp-mywork-row:last-child {
5962
- border-bottom: 1px solid var(--border);
6002
+ .rp-mywork-grid > .td-day__panel,
6003
+ .rp-mywork-grid > .td-day__detail {
6004
+ align-content: start;
6005
+ }
6006
+ .rp-mywork-badge {
6007
+ gap: 4px;
6008
+ }
6009
+ .rp-mywork-row__inner {
6010
+ display: flex;
6011
+ align-items: center;
6012
+ gap: 10px;
6013
+ flex: 1;
6014
+ min-width: 0;
6015
+ }
6016
+ .rp-mywork-row__content {
6017
+ flex: 1;
6018
+ min-width: 0;
6019
+ }
6020
+ .rp-mywork-row__title {
6021
+ font-weight: 500;
6022
+ font-size: 14px;
6023
+ }
6024
+ .rp-mywork-row__meta {
6025
+ font-size: 12px;
6026
+ color: var(--muted-foreground);
5963
6027
  }
5964
6028
  .rp-mywork-row--open {
5965
- box-shadow: inset 2px 0 0 var(--foreground);
6029
+ background: var(--accent);
6030
+ border-color: var(--ring);
6031
+ }
6032
+ .rp-mywork-dot {
6033
+ width: 8px;
6034
+ height: 8px;
6035
+ border-radius: 50%;
6036
+ flex: none;
6037
+ display: inline-block;
6038
+ }
6039
+ .rp-mywork-badge {
6040
+ flex: none;
5966
6041
  }
5967
6042
  .rp-mywork-state {
5968
6043
  display: inline-flex;
5969
6044
  align-items: center;
5970
6045
  color: var(--muted-foreground);
6046
+ flex: none;
5971
6047
  }
5972
6048
  .rp-mywork-page {
5973
6049
  display: flex;
@@ -6035,6 +6111,10 @@
6035
6111
  .rp-mywork-grid {
6036
6112
  grid-template-columns: 1fr;
6037
6113
  }
6114
+ .rp-mywork-header {
6115
+ flex-direction: column;
6116
+ align-items: flex-start;
6117
+ }
6038
6118
  }
6039
6119
 
6040
6120
  /* -- layouts · .nt- Notes: one meeting on one screen — notes and what was caught beside a quiet transcript (meeting-notes Runs 1 and 2) -- */
@@ -4,15 +4,13 @@
4
4
  * A DESTINATION MODULE (registry.js): mountDestination(host, ctx) on every render with the same host, disposeDestination(host).
5
5
  * It keeps ONE persistent root per host and rebuilds the body only when its own data or a choice changed.
6
6
  *
7
- * What it shows:
8
- * - the tabs To brief (the default) · Agents · Ready · All, each with its count, and Read again;
9
- * - the list, in WSJF order: the tier, the title, a meta line (Jira or WBS, the key, who asked, the due date) and a
10
- * state icon;
11
- * - the chosen item: What the agent found (the ask, the thread, the open questions), the instruction box prefilled
12
- * with the agent's suggestion, Run agent and I'll handle it; Back to brief when the item is the PM's or an agent's;
13
- * Open Agenda when its draft waits there; Details with each part of the score.
14
- * Every press is a page press to POST /api/my-work/act or /api/my-work/refresh. Approve, revise and retry are Agenda's
15
- * (R7): there is no approve button here. In fixture mode it reads nothing.
7
+ * Layout (matching Agenda's two-column design):
8
+ * - header: "N to brief in <repo>" with segmented control (To brief/Agents/Ready/All with counts);
9
+ * - strip: Jira read line and Read again button;
10
+ * - body: two-column grid (desktop) or single column (mobile)
11
+ * - left: .td-day__panel card with list items (status dot, title, meta, trailing badge);
12
+ * - right: .td-day__detail card with the chosen item's full view.
13
+ * Every press is a page press to POST /api/my-work/act or /api/my-work/refresh. In fixture mode it reads nothing.
16
14
  */
17
15
 
18
16
  import { el } from './dom.js';
@@ -71,6 +69,27 @@ const stateIcon = (x) => {
71
69
  return el('span', { class: 'rp-mywork-state', role: 'img', 'aria-label': stateWord(x), title: stateWord(x), 'data-my-work-state': x.state }, node);
72
70
  };
73
71
 
72
+ // Helper: create status dot colored by tier
73
+ const statusDot = (tier) => {
74
+ return el('span', { class: 'rp-mywork-dot rp-mywork-dot--' + String(tier).toLowerCase(), 'data-my-work-tier': tier, title: COPY.score({ tier }) });
75
+ };
76
+
77
+ // Trailing badges: a decision marker and a due date when one is set (soon = warning tone).
78
+ const dueLabel = (due) => {
79
+ const days = Math.round((new Date(due + 'T00:00:00') - new Date(new Date().toDateString())) / 86400000);
80
+ if (Number.isNaN(days)) return { text: 'Due ' + due, soon: false };
81
+ if (days < 0) return { text: 'Overdue', soon: true };
82
+ if (days === 0) return { text: 'Due today', soon: true };
83
+ if (days === 1) return { text: 'Due tomorrow', soon: true };
84
+ return { text: 'Due ' + new Date(due + 'T00:00:00').toLocaleDateString('en-US', { month: 'short', day: 'numeric' }), soon: false };
85
+ };
86
+ const trailingBadges = (x) => {
87
+ const out = [];
88
+ if (x.kind === 'decide' && x.state === 'brief') out.push(badge({ text: 'Decision', tone: 'outline', attrs: { title: COPY.decide, class: 'rp-mywork-badge' } }));
89
+ if (x.due) { const d = dueLabel(x.due); out.push(badge({ text: d.text, tone: d.soon ? 'warning' : 'outline', attrs: { class: 'rp-mywork-badge' } })); }
90
+ return out;
91
+ };
92
+
74
93
  /* ── mount ───────────────────────────────────────────────────────────────────────────────────────────────────────── */
75
94
 
76
95
  const insts = new WeakMap();
@@ -128,12 +147,12 @@ function itemRow(inst, x) {
128
147
  const meta = [x.source === 'jira' ? 'Jira ' + x.key : 'WBS ' + (x.repo ? x.repo + ' · ' : '') + x.key];
129
148
  if (x.from) meta.push(COPY.from(x.from));
130
149
  if (x.due) meta.push(COPY.due(x.due));
131
- return el('div', { class: 'rp-mywork-row' + (open ? ' rp-mywork-row--open' : ''), role: 'listitem', 'data-my-work-item': x.id },
150
+ return el('div', { class: 'rp-mywork-row td-day__r' + (open ? ' rp-mywork-row--open' : ''), role: 'listitem', 'data-my-work-item': x.id, 'data-cur': open ? 'true' : null },
132
151
  row({
133
152
  title: x.title,
134
153
  meta,
135
- lead: [badge({ text: x.wsjf.tier, tone: x.wsjf.tier === 'P1' ? 'warning' : 'outline', attrs: { 'data-my-work-tier': x.wsjf.tier, title: COPY.score(x.wsjf) } })],
136
- trail: [x.kind === 'decide' && x.state === 'brief' ? el('span', { class: 'rp-mywork-state', role: 'img', 'aria-label': COPY.decide, title: COPY.decide }, icon('diamond')) : null, stateIcon(x)].filter(Boolean),
154
+ lead: [statusDot(x.wsjf.tier)],
155
+ trail: [...trailingBadges(x), x.state !== 'brief' ? stateIcon(x) : el('span', { 'data-my-work-state': x.state, hidden: '' })].filter(Boolean),
137
156
  onOpen: () => { inst.open = x.id; inst.details = false; inst.last = ''; draw(inst); },
138
157
  openLabel: `${x.title}, ${x.wsjf.tier}, ${stateWord(x)}`,
139
158
  openAttrs: { 'aria-current': open ? 'true' : null, 'data-my-work-open': x.id },
@@ -143,14 +162,19 @@ function itemRow(inst, x) {
143
162
  function listPane(inst, d) {
144
163
  const shown = d.items.filter((x) => TAB_STATES[inst.tab].includes(x.state));
145
164
  const counts = { brief: d.counts.brief, agents: d.counts.agents, ready: d.counts.ready, all: d.counts.all };
146
- const controls = el('div', { class: 'rp-mywork-views' },
147
- tabs({ variant: 'line', value: inst.tab, ariaLabel: 'Show', attrs: { 'data-my-work-tabs': '' },
148
- items: Object.keys(COPY.tabs).map((id) => ({ id, label: COPY.tabs[id], count: counts[id] })),
149
- onChange: (v) => { inst.tab = v; inst.open = null; inst.last = ''; draw(inst); } }));
150
- const rows = shown.length
151
- ? el('div', { role: 'list', 'data-my-work-list': inst.tab }, shown.map((x) => itemRow(inst, x)))
152
- : el('p', { class: 'rp-section__empty' }, COPY.none[inst.tab]);
153
- return el('div', { class: 'rp-mywork-list' }, controls, rows);
165
+ const panelChildren = [
166
+ el('h2', { class: 'td-day__h2' }, COPY.tabs[inst.tab], el('span', null, counts[inst.tab] + (counts[inst.tab] === 1 ? ' item' : ' items')))
167
+ ];
168
+
169
+ if (shown.length) {
170
+ const rowsContainer = el('div', { role: 'list', 'data-my-work-list': inst.tab });
171
+ shown.forEach((x) => rowsContainer.appendChild(itemRow(inst, x)));
172
+ panelChildren.push(rowsContainer);
173
+ } else {
174
+ panelChildren.push(el('p', { class: 'rp-section__empty' }, COPY.none[inst.tab]));
175
+ }
176
+
177
+ return el('section', { class: 'td-day__panel' }, ...panelChildren);
154
178
  }
155
179
 
156
180
  /* ── the item ────────────────────────────────────────────────────────────────────────────────────────────────────── */
@@ -178,7 +202,7 @@ function briefSec(inst, x) {
178
202
  const r = await act(inst, { action: 'brief', item: x.id, instruction }, COPY.started);
179
203
  if (r) inst.drafts.delete(x.id);
180
204
  } });
181
- const mine = button({ label: COPY.handle, variant: 'outline', size: 'sm', disabled: inst.busy, attrs: { 'data-my-work-act': 'handle' },
205
+ const mine = button({ label: COPY.handle, variant: 'outline', size: 'sm', icon: 'check', disabled: inst.busy, attrs: { 'data-my-work-act': 'handle' },
182
206
  onClick: () => act(inst, { action: 'handle', item: x.id }, COPY.taken) });
183
207
  return el('div', { class: 'rp-mywork-brief' }, f, el('div', { class: 'rp-mywork-acts' }, run, mine));
184
208
  }
@@ -198,14 +222,18 @@ function statusSec(inst, x) {
198
222
 
199
223
  function itemPane(inst, d) {
200
224
  const x = d.items.find((y) => y.id === inst.open);
201
- if (!x) return el('div', { class: 'rp-mywork-page' }, el('p', { class: 'rp-section__empty' }, COPY.pick));
225
+ if (!x) return el('section', { class: 'td-day__detail' }, el('p', { class: 'rp-section__empty' }, COPY.pick));
226
+
202
227
  const head = el('header', { class: 'rp-mywork-who' },
203
228
  el('h3', { class: 'rp-mywork-who__title' }, x.title),
204
229
  el('p', { class: 'rp-mywork-hint' }, [x.source === 'jira' ? 'Jira' : 'WBS', x.key, x.repo, x.status, x.due ? COPY.due(x.due) : null, x.from ? COPY.from(x.from) : null].filter(Boolean).join(' · ')),
205
230
  x.url ? el('a', { class: 'rp-mywork-link', href: x.url, target: '_blank', rel: 'noopener noreferrer' }, icon('link'), ' ', COPY.source) : null);
231
+
206
232
  const details = button({ label: inst.details ? COPY.hide : COPY.details, variant: 'ghost', size: 'sm', iconEnd: inst.details ? 'chevron-down' : 'chevron-right',
207
233
  attrs: { 'aria-expanded': inst.details ? 'true' : 'false', 'data-my-work-details': '' }, onClick: () => { inst.details = !inst.details; inst.last = ''; draw(inst); } });
208
- return el('article', { class: 'rp-mywork-page', 'data-my-work-page': x.id },
234
+
235
+ return el('article', { class: 'rp-mywork-page td-day__detail', 'data-my-work-page': x.id },
236
+ el('h2', { class: 'td-day__h2' }, 'Brief', el('span', null, x.key)),
209
237
  head, foundSec(x),
210
238
  x.state === 'brief' ? section(COPY.instruction, [briefSec(inst, x)], { 'data-my-work-sec': 'brief' }) : statusSec(inst, x),
211
239
  el('div', { class: 'rp-mywork-acts' }, details), inst.details ? el('p', { class: 'rp-mywork-hint', 'data-my-work-score': '' }, COPY.score(x.wsjf)) : null);
@@ -213,18 +241,42 @@ function itemPane(inst, d) {
213
241
 
214
242
  /* ── the page ────────────────────────────────────────────────────────────────────────────────────────────────────── */
215
243
 
216
- function header(inst) {
217
- if (inst.head) return inst.head;
244
+ function header(inst, d) {
245
+ const key = JSON.stringify([d && d.counts, inst.tab]);
246
+ if (inst.head && inst.headData === key) return inst.head;
247
+ inst.headData = key;
248
+
249
+ const counts = d && d.counts ? d.counts : { brief: 0, agents: 0, ready: 0, all: 0 };
250
+ const briefCount = counts.brief || 0;
251
+ const nounPhrase = briefCount === 0 ? 'Nothing to brief' : `${briefCount} to brief`;
252
+
253
+ const segmentedTabs = tabs({ variant: 'segmented', role: 'radiogroup', value: inst.tab, ariaLabel: 'Filter', attrs: { 'data-my-work-tabs': '' },
254
+ items: Object.keys(COPY.tabs).map((id) => ({ id, label: COPY.tabs[id], count: counts[id] })),
255
+ onChange: (v) => { inst.tab = v; inst.open = null; inst.last = ''; draw(inst); } });
256
+ const btns = segmentedTabs.querySelectorAll('button');
257
+ Object.keys(COPY.tabs).forEach((id, i) => { if (btns[i]) btns[i].setAttribute('data-tab', id); });
258
+
218
259
  inst.readLine = el('p', { class: 'rp-mywork-hint', 'data-my-work-read': '' });
219
- inst.head = el('div', { class: 'rp-mywork-top' }, el('h2', { class: 'rp-head__title' }, COPY.title), inst.readLine,
220
- button({ label: COPY.readAgain, variant: 'outline', size: 'sm', icon: 'refresh-cw', attrs: { 'data-my-work-act': 'refresh' }, onClick: () => readAgain(inst) }));
260
+ const readAgainBtn = button({ label: COPY.readAgain, variant: 'outline', size: 'sm', icon: 'refresh-cw', attrs: { 'data-my-work-act': 'refresh', 'data-my-work-read': '' }, onClick: () => readAgain(inst) });
261
+
262
+ const headerRow = el('div', { class: 'rp-mywork-header' },
263
+ el('h2', { class: 'rp-mywork-title' }, nounPhrase),
264
+ segmentedTabs);
265
+
266
+ const stripCard = el('div', { class: 'rp-mywork-strip' },
267
+ inst.readLine,
268
+ el('span', { class: 'rp-mywork-spacer' }),
269
+ inst.countLine = el('span', { class: 'rp-mywork-hint' }),
270
+ readAgainBtn);
271
+
272
+ inst.head = el('div', { class: 'rp-mywork-top' }, headerRow, stripCard);
221
273
  return inst.head;
222
274
  }
223
275
 
224
276
  function draw(inst) {
225
277
  const root = inst.root;
226
278
  const d = myWork();
227
- const head = header(inst);
279
+ const head = header(inst, d);
228
280
  if (head.parentNode !== root || inst.body.parentNode !== root) root.replaceChildren(head, inst.body);
229
281
  const fp = JSON.stringify([d, inst.tab, inst.open, inst.details, inst.busy]);
230
282
  if (fp === inst.last) return;
@@ -235,8 +287,14 @@ function draw(inst) {
235
287
  if (!d) { inst.body.replaceChildren(el('p', { class: 'rp-section__empty' }, COPY.loading)); return; }
236
288
  if (!d.ok) { inst.body.replaceChildren(el('p', { class: 'rp-section__empty rp-section__empty--bad', role: 'alert' }, d.error || COPY.readError)); return; }
237
289
  inst.readLine.textContent = COPY.readAt(d.read_at ? new Date(d.read_at).toLocaleString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }) : null);
290
+ const nJira = d.items.filter((x) => x.source === 'jira').length;
291
+ const nPlan = d.items.length - nJira;
292
+ inst.countLine.textContent = `${nJira} ticket${nJira === 1 ? '' : 's'} · ${nPlan} plan item${nPlan === 1 ? '' : 's'}`;
238
293
  const shown = d.items.filter((x) => TAB_STATES[inst.tab].includes(x.state));
239
294
  if (!shown.some((x) => x.id === inst.open)) inst.open = shown.length ? shown[0].id : null;
240
295
  const banner = (d.banner || []).length ? el('p', { class: 'rp-mywork-hint', role: 'status', 'data-my-work-banner': '' }, d.banner.map((b) => b.source + ': ' + b.error).join(' · ')) : null;
241
- inst.body.replaceChildren(...[banner, el('div', { class: 'rp-mywork-grid' }, listPane(inst, d), itemPane(inst, d))].filter(Boolean));
296
+ const grid = el('div', { class: 'rp-mywork-grid td-day__cols' },
297
+ listPane(inst, d),
298
+ itemPane(inst, d));
299
+ inst.body.replaceChildren(...[banner, grid].filter(Boolean));
242
300
  }