create-pmos 0.54.0 → 0.55.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.
@@ -10,7 +10,9 @@
10
10
  * newSignals(...) which candidates have not been appended to signals.jsonl inside the dedupe window
11
11
  * dismissedIds(...) which candidates the PM dismissed within the last seven days
12
12
  * validateFeedback(body) the four verdicts, the implicit events, an optional one-line note
13
- * normalizeSettings(raw) off, per-trigger off, quiet hours, the stall threshold, pause learning
13
+ * normalizeSettings(raw) off, per-trigger off, quiet hours, the stall threshold, pause learning; and for Today's
14
+ * day run the brief sources, the level per channel/project/space (scopes), the daytime
15
+ * check (dayCheck) and the PM's website addresses
14
16
  * inQuietHours(now, quiet) whether automatic scans are held
15
17
  *
16
18
  * A candidate is never a status. Nothing here changes an initiative, a lifecycle line or an acceptance.
@@ -29,10 +31,20 @@ const DISMISS_MS = 7 * DAY;
29
31
  const BASE = { needs: 100, returned: 100, review: 80, schedule: 60, stalled: 40 };
30
32
  const OWNER = { needs: 'pm', returned: 'pm', review: 'pm', schedule: 'pm', stalled: 'agent' };
31
33
 
32
- const BRIEF_SOURCES = ['calendar', 'wispr', 'mail', 'slack', 'jira', 'confluence'];
33
- // Mail is off by default: a brief reads titles and metadata of the other sources, and the PM turns mail on.
34
- const BRIEF_DEFAULT = Object.freeze({ calendar: true, wispr: true, mail: false, slack: true, jira: true, confluence: true });
35
- const DEFAULTS = Object.freeze({ name: '', briefSources: BRIEF_DEFAULT, briefScoring: false, backgroundPrepare: false, enabled: true, triggers: Object.freeze(Object.fromEntries(TRIGGERS.map((t) => [t, true]))), stalledDays: 5, quiet: null, pauseLearning: false });
34
+ const BRIEF_SOURCES = ['calendar', 'wispr', 'mail', 'slack', 'jira', 'confluence', 'github', 'drive', 'figma', 'website'];
35
+ // Mail is off by default: a brief reads titles and metadata of the other sources, and the PM turns mail on. The four
36
+ // sources added for the day run (today-goals G19) are off too: the PM switches each on.
37
+ const BRIEF_DEFAULT = Object.freeze({ calendar: true, wispr: true, mail: false, slack: true, jira: true, confluence: true, github: false, drive: false, figma: false, website: false });
38
+ // G20: a level per Slack channel, Jira project and Confluence space. A place with no level is read as `search`.
39
+ const SCOPE_SERVICES = ['slack', 'jira', 'confluence'];
40
+ const SCOPE_LEVELS = ['watch', 'search', 'ignore'];
41
+ const MAX_SCOPE_NAMES = 50;
42
+ const MAX_WEBSITES = 10;
43
+ // G22: the daytime check — every 30 or 60 minutes, or off (0), between start and end of the working day.
44
+ const DAY_CHECK_EVERY = [0, 30, 60];
45
+ const DAY_CHECK_DEFAULT = Object.freeze({ every: 60, start: '09:00', end: '18:00' });
46
+ const DEFAULTS = Object.freeze({ name: '', briefSources: BRIEF_DEFAULT, briefScoring: false, backgroundPrepare: false, enabled: true, triggers: Object.freeze(Object.fromEntries(TRIGGERS.map((t) => [t, true]))), stalledDays: 5, quiet: null, pauseLearning: false,
47
+ scopes: Object.freeze({ slack: Object.freeze({}), jira: Object.freeze({}), confluence: Object.freeze({}) }), dayCheck: DAY_CHECK_DEFAULT, websites: Object.freeze([]) });
36
48
 
37
49
  const idOf = (key) => crypto.createHash('sha1').update(String(key)).digest('hex').slice(0, 12);
38
50
  const clip = (s, n) => String(s == null ? '' : s).replace(/\s+/g, ' ').trim().slice(0, n);
@@ -165,7 +177,8 @@ const HHMM = /^([01]\d|2[0-3]):([0-5]\d)$/;
165
177
  /** @returns {{ok:true, settings:object}|{ok:false, error:string}} — `raw` is merged over the defaults; nothing is partly applied. */
166
178
  function normalizeSettings(raw) {
167
179
  const r = raw && typeof raw === 'object' ? raw : {};
168
- const s = { name: DEFAULTS.name, briefSources: { ...BRIEF_DEFAULT }, briefScoring: DEFAULTS.briefScoring, backgroundPrepare: DEFAULTS.backgroundPrepare, enabled: DEFAULTS.enabled, triggers: { ...DEFAULTS.triggers }, stalledDays: DEFAULTS.stalledDays, quiet: DEFAULTS.quiet, pauseLearning: DEFAULTS.pauseLearning };
180
+ const s = { name: DEFAULTS.name, briefSources: { ...BRIEF_DEFAULT }, briefScoring: DEFAULTS.briefScoring, backgroundPrepare: DEFAULTS.backgroundPrepare, enabled: DEFAULTS.enabled, triggers: { ...DEFAULTS.triggers }, stalledDays: DEFAULTS.stalledDays, quiet: DEFAULTS.quiet, pauseLearning: DEFAULTS.pauseLearning,
181
+ scopes: { slack: {}, jira: {}, confluence: {} }, dayCheck: { ...DAY_CHECK_DEFAULT }, websites: [] };
169
182
  if (r.enabled !== undefined) { if (typeof r.enabled !== 'boolean') return { ok: false, error: 'enabled must be true or false' }; s.enabled = r.enabled; }
170
183
  if (r.name !== undefined) { if (typeof r.name !== 'string' || r.name.length > 40) return { ok: false, error: 'name must be text of at most 40 characters' }; s.name = r.name.replace(/\s+/g, ' ').trim(); }
171
184
  if (r.briefScoring !== undefined) { if (typeof r.briefScoring !== 'boolean') return { ok: false, error: 'briefScoring must be true or false' }; s.briefScoring = r.briefScoring; }
@@ -198,6 +211,39 @@ function normalizeSettings(raw) {
198
211
  return { ok: false, error: 'quiet must be null, or {start, end} as HH:MM (24-hour, local), different from each other' };
199
212
  } else s.quiet = { start: r.quiet.start, end: r.quiet.end };
200
213
  }
214
+ if (r.scopes !== undefined) {
215
+ if (!r.scopes || typeof r.scopes !== 'object' || Array.isArray(r.scopes)) return { ok: false, error: `scopes must be an object of ${SCOPE_SERVICES.join(', ')} → {name: level}` };
216
+ for (const [svc, names] of Object.entries(r.scopes)) {
217
+ if (!SCOPE_SERVICES.includes(svc)) return { ok: false, error: `unknown scope service "${svc}" — one of ${SCOPE_SERVICES.join(', ')}` };
218
+ if (!names || typeof names !== 'object' || Array.isArray(names)) return { ok: false, error: `scopes.${svc} must be an object of name → ${SCOPE_LEVELS.join(' | ')}` };
219
+ const out = {};
220
+ for (const [name, level] of Object.entries(names)) {
221
+ // null clears a level: the place is read as `search` again. The route merges a patch over the stored levels.
222
+ if (level === null) continue;
223
+ if (!name.trim() || name.length > 120) return { ok: false, error: `a ${svc} name is 1 to 120 characters` };
224
+ if (!SCOPE_LEVELS.includes(level)) return { ok: false, error: `the level of ${svc} "${name}" must be one of ${SCOPE_LEVELS.join(', ')}` };
225
+ out[name] = level;
226
+ }
227
+ if (Object.keys(out).length > MAX_SCOPE_NAMES) return { ok: false, error: `scopes.${svc} has ${Object.keys(out).length} names — at most ${MAX_SCOPE_NAMES}` };
228
+ s.scopes[svc] = out;
229
+ }
230
+ }
231
+ if (r.dayCheck !== undefined) {
232
+ const d = r.dayCheck;
233
+ if (!d || typeof d !== 'object' || Array.isArray(d)) return { ok: false, error: 'dayCheck must be {every, start, end}' };
234
+ const every = d.every === undefined ? DAY_CHECK_DEFAULT.every : d.every;
235
+ const start = d.start === undefined ? DAY_CHECK_DEFAULT.start : d.start;
236
+ const end = d.end === undefined ? DAY_CHECK_DEFAULT.end : d.end;
237
+ if (!DAY_CHECK_EVERY.includes(every)) return { ok: false, error: 'dayCheck.every must be 30 or 60 (minutes), or 0 for off' };
238
+ if (!HHMM.test(String(start)) || !HHMM.test(String(end)) || start >= end) return { ok: false, error: 'dayCheck start and end must be HH:MM (24-hour, local), with start before end' };
239
+ s.dayCheck = { every, start, end };
240
+ }
241
+ if (r.websites !== undefined) {
242
+ if (!Array.isArray(r.websites)) return { ok: false, error: 'websites must be a list of http(s) addresses' };
243
+ if (r.websites.length > MAX_WEBSITES) return { ok: false, error: `websites has ${r.websites.length} addresses — at most ${MAX_WEBSITES}` };
244
+ for (const u of r.websites) if (typeof u !== 'string' || u.length > 500 || !/^https?:\/\/\S+$/.test(u)) return { ok: false, error: 'a website must be an http(s) address of at most 500 characters' };
245
+ s.websites = [...new Set(r.websites)];
246
+ }
201
247
  return { ok: true, settings: s };
202
248
  }
203
249
 
@@ -210,4 +256,4 @@ function inQuietHours(nowMs, quiet) {
210
256
  return a < b ? m >= a && m < b : m >= a || m < b;
211
257
  }
212
258
 
213
- module.exports = { BRIEF_SOURCES, TRIGGERS, VERDICTS, EVENTS, DEFAULTS, DEDUPE_MS, DISMISS_MS, BASE, detect, rank, newSignals, dismissedIds, validateFeedback, normalizeSettings, inQuietHours };
259
+ module.exports = { BRIEF_SOURCES, SCOPE_SERVICES, SCOPE_LEVELS, MAX_SCOPE_NAMES, MAX_WEBSITES, DAY_CHECK_EVERY, TRIGGERS, VERDICTS, EVENTS, DEFAULTS, DEDUPE_MS, DISMISS_MS, BASE, detect, rank, newSignals, dismissedIds, validateFeedback, normalizeSettings, inQuietHours };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.54.0",
3
+ "version": "0.55.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.54.0",
3
+ "version": "0.55.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",
@@ -42,6 +42,7 @@ import { createDockRail } from './dock.js';
42
42
  import { SEL, ensureComponentsCss, iconButton, button, menu, toast } from './primitives.js';
43
43
  import { ensureSprite } from './icons.js';
44
44
  import { COPY as FC } from './frame-copy.js';
45
+ import { registerCommand } from './registry.js';
45
46
 
46
47
  /* The LEGACY layout is the three-column shell of column-layout: repositories with the list under them, the detail, the tabs, and
47
48
  * a list that a mode (skills, memory …) replaces. A phone still draws it until Run 4 of workspace-cockpit-nav, and `?layout=legacy`
@@ -482,9 +483,20 @@ function repoMenu(anchor) {
482
483
  const items = tuples.map((t) => ({ label: t[0], icon: t[0] === cur ? 'check' : null, onSelect: () => chooseRepo(t[0]) }));
483
484
  if (items.length) items.push({ sep: true });
484
485
  items.push({ label: FC.repo.all, icon: fr.scope === 'all' ? 'check' : null, onSelect: chooseAll });
486
+ if (tuples.length) items.push({ sep: true }, { label: FC.repo.branches, onSelect: openBranchesSheet });
485
487
  menu({ anchor, items, ariaLabel: FC.repo.menu });
486
488
  }
487
489
 
490
+ /* Branches and sessions (0.54.1): the legacy panel 1's working trees, which the frame never drew. The current repository,
491
+ * or every repository when the scope is All. */
492
+ function openBranchesSheet() {
493
+ const tuples = mods.repos && typeof mods.repos.repoTuples === 'function' ? mods.repos.repoTuples() : [];
494
+ const cur = fr.scope === 'all' ? null : activeRepo();
495
+ const repos = cur ? [cur] : tuples.map((t) => t[0]);
496
+ import('./branches-sheet.js').then((m) => m.openBranches(repos)).catch((e) => console.error('[pm] could not open the branches:', e));
497
+ }
498
+ if (FRAME) registerCommand({ id: 'branches-open', label: FC.repo.branches, hint: FC.branches.hint, cli: 'git worktree list', run: () => openBranchesSheet() });
499
+
488
500
  /* Sync: the bar shows it only while the current repository is not in sync. The reading is local git (GET
489
501
  * /api/sync/status), re-read a minute after the last one and at once after a Sync. */
490
502
  const syncSeen = new Map(); // repo -> { at, st, pending }
@@ -681,7 +693,7 @@ function frameKey(e) {
681
693
 
682
694
  /* What the suites and the phone chrome reach the frame by. */
683
695
  const frameApi = {
684
- on: true, go, toggleDock, foldDock, openDock, toggleFocus, dockWidth, chooseRepo, chooseAll, openPalette, repoMenu, closeDetail, sync: runSync,
696
+ on: true, go, toggleDock, foldDock, openDock, toggleFocus, dockWidth, chooseRepo, chooseAll, openPalette, repoMenu, openBranchesSheet, closeDetail, sync: runSync,
685
697
  get unsynced() { return !!fr.unsynced; },
686
698
  get dest() { return fr.dest; }, get scope() { return fr.scope; }, get dockOpen() { return dockOpen(); }, get dockMax() { return !!fr.dockMax; },
687
699
  get detailOpen() { return !!state.col2; }, get syncing() { return fr.syncing; }, get needs() { return fr.needs || 0; },
@@ -0,0 +1,56 @@
1
+ /* pm/branches-sheet.js — the working trees of a repository and the sessions in each, in the frame (0.54.1).
2
+ *
3
+ * The legacy panel 1 listed every tree (main first) with its live sessions, `+ branch` and `+ session` (panel-trees.js).
4
+ * The frame never drew panel 1, so from 0.53.0 the branches and their sessions had no place. This sheet draws the same
5
+ * blocks, unchanged: one repository, or every repository when the scope is All. It repaints on every state change while
6
+ * it is open, so a session that starts or ends elsewhere (a terminal, the CLI) shows here within one poll.
7
+ *
8
+ * openBranches(repos) → the sheet. `repos` is the list of repository names to show, in order.
9
+ */
10
+
11
+ import { el, setChildren } from './dom.js';
12
+ import { sheet } from './primitives.js';
13
+ import { state, subscribe } from './state.js';
14
+ import { treesOf } from './panel-repos.js';
15
+ import { treesBlock, branchForm, treeNoteFor, sessionsIn } from './panel-trees.js';
16
+ import { refreshTrees } from './data.js';
17
+ import { COPY } from './frame-copy.js';
18
+
19
+ const live = (s) => s && s.state !== 'exited' && s.shown !== 'exited';
20
+
21
+ function sub(repos) {
22
+ let trees = 0; let sessions = 0;
23
+ for (const r of repos) for (const t of treesOf(r)) { trees++; sessions += sessionsIn(t.ws).filter(live).length; }
24
+ return COPY.branches.sub(trees, sessions);
25
+ }
26
+
27
+ function body(repos) {
28
+ if (!repos.length) return [el('p', { class: 'fr-branches__empty' }, COPY.branches.none)];
29
+ return repos.map((r) => {
30
+ const trees = treesOf(r);
31
+ return el('section', { class: 'fr-branches__repo', 'data-fr-branches': r },
32
+ repos.length > 1 ? el('h3', { class: 'fr-branches__name' }, r) : null,
33
+ branchForm(r),
34
+ trees.length ? treesBlock(r, trees, true) : el('p', { class: 'fr-branches__empty' }, COPY.branches.noTrees),
35
+ treeNoteFor(r));
36
+ });
37
+ }
38
+
39
+ export function openBranches(repos) {
40
+ const list = (repos || []).filter(Boolean);
41
+ const s = sheet({ title: COPY.branches.title, sub: sub(list), body: body(list), attrs: { 'data-fr': 'branches' }, onClose: () => stop() });
42
+ // The trees can change behind the page (a branch made in a terminal): read them once more on open.
43
+ try { Promise.resolve(refreshTrees()).catch(() => {}); } catch { /* no data layer */ }
44
+ let last = null;
45
+ const stop = subscribe(() => {
46
+ if (!s.open) return;
47
+ // Repaint only when something the sheet shows changed, so an input the PM types in keeps its node between polls.
48
+ const key = JSON.stringify([state.branchForm && { ...state.branchForm, name: null, from: null }, state.treeNote, state.treeSel, state.treeConfirm,
49
+ list.map((r) => treesOf(r).map((t) => [t.ws, t.branch, sessionsIn(t.ws).map((x) => [x.id, x.state, x.shown, x.name, x.initiative])]))]);
50
+ if (key === last) return;
51
+ last = key;
52
+ s.setHead(COPY.branches.title, sub(list));
53
+ setChildren(s.body, body(list));
54
+ });
55
+ return s;
56
+ }
@@ -2148,45 +2148,156 @@
2148
2148
  flex-direction: column;
2149
2149
  gap: 8px;
2150
2150
  }
2151
- /* today-goals (G6): the goals for today, above what is owed */
2152
- .td-goals {
2151
+ /* today-goals (G16 to G22): the day section above what is owed. Folded, it is one toggle; open, a one-line top bar, the
2152
+ lists on the left and the detail of the selected thing on the right. Below 860px of its own width the detail goes under
2153
+ the list. The controls are the primitives (button, row, tabs, field, badge, meter); this block lays them out and
2154
+ marks the selected row. Colour stays neutral, with warning and destructive for what needs the PM (R28-12). */
2155
+ .td-board:has(> .td-day[data-open="true"]) { max-width: 1240px; }
2156
+ .td-day {
2157
+ container: td-day / inline-size;
2153
2158
  display: flex;
2154
2159
  flex-direction: column;
2155
- gap: 8px;
2160
+ gap: 12px;
2161
+ min-width: 0;
2162
+ }
2163
+ .td-day[data-open="false"] { border: 1px solid var(--border); background: var(--card); padding: 4px 8px; }
2164
+ .td-day__bar {
2165
+ display: flex;
2166
+ flex-wrap: wrap;
2167
+ align-items: center;
2168
+ gap: 8px 10px;
2156
2169
  padding: 8px 12px;
2157
2170
  border: 1px solid var(--border);
2158
2171
  background: var(--card);
2159
2172
  }
2160
- .td-goals__head { display: flex; align-items: center; }
2161
- .td-goals__list, .td-goal__steps, .td-goal__ctx-list {
2162
- list-style: none;
2163
- margin: 0;
2164
- padding: 0;
2173
+ .td-day__hello { flex: 0 1 220px; min-width: 0; overflow: hidden; text-overflow: ellipsis; font-size: 14px; color: var(--foreground); white-space: nowrap; }
2174
+ .td-day__dim { color: var(--muted-foreground); }
2175
+ .td-day__mono { font-family: var(--mono); font-variant-numeric: tabular-nums; font-size: 12px; }
2176
+ .td-day__steps { display: flex; flex-wrap: wrap; gap: 2px; list-style: none; margin: 0; padding: 0; }
2177
+ .td-day__ts { gap: 6px; padding: 0 6px; letter-spacing: 0.04em; color: var(--muted-foreground); }
2178
+ .td-day__ts[data-step-state="done"], .td-day__ts[data-step-state="now"] { color: var(--foreground); }
2179
+ .td-day__ts[data-step-state="now"] { background: var(--muted); }
2180
+ .td-day__ts[aria-current="true"] { border-color: var(--ring); }
2181
+ .td-day__tsdot { display: inline-block; flex: none; margin-right: 6px; vertical-align: middle; width: 8px; height: 8px; border-radius: 50%; border: 1.5px solid currentColor; }
2182
+ .td-day__ts[data-step-state="done"] .td-day__tsdot { background: currentColor; }
2183
+ .td-day__ts[data-step-state="now"] .td-day__tsdot { background: var(--foreground); animation: td-day-pulse 1.2s infinite; }
2184
+ @keyframes td-day-pulse { 50% { opacity: 0.3; } }
2185
+ @media (prefers-reduced-motion: reduce) { .td-day__ts[data-step-state="now"] .td-day__tsdot { animation: none; } }
2186
+ .td-day__status { flex: 1 1 0; min-width: 0; font-size: 12px; color: var(--muted-foreground); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
2187
+ .td-day__check { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--muted-foreground); }
2188
+ .td-day__run { margin: 0; font-size: 12px; line-height: 16px; color: var(--foreground); }
2189
+ .td-day__run--failed { color: var(--destructive); }
2190
+ .td-day__note { margin: 0; font-size: 12px; line-height: 16px; color: var(--muted-foreground); }
2191
+
2192
+ .td-day__cols { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.25fr); gap: 12px; align-items: start; }
2193
+ .td-day__left { display: grid; gap: 12px; min-width: 0; }
2194
+ .td-day__panel, .td-day__detail {
2195
+ display: grid;
2196
+ gap: 8px;
2197
+ min-width: 0;
2198
+ padding: 10px 12px;
2199
+ border: 1px solid var(--border);
2200
+ background: var(--card);
2201
+ }
2202
+ .td-day__detail { position: sticky; top: 8px; max-height: calc(100vh - 16px); overflow: auto; align-content: start; }
2203
+ @container td-day (max-width: 859px) {
2204
+ .td-day__cols { grid-template-columns: minmax(0, 1fr); }
2205
+ .td-day__detail { position: static; max-height: none; }
2206
+ }
2207
+ .td-day__h2 {
2165
2208
  display: flex;
2166
- flex-direction: column;
2167
- gap: 6px;
2209
+ justify-content: space-between;
2210
+ gap: 8px;
2211
+ margin: 0;
2212
+ font-size: 12px;
2213
+ font-weight: 600;
2214
+ letter-spacing: 0.06em;
2215
+ text-transform: uppercase;
2216
+ color: var(--muted-foreground);
2217
+ }
2218
+ .td-day__h2 span { font-weight: 400; letter-spacing: 0; text-transform: none; }
2219
+ .td-day__rows { list-style: none; margin: 0; padding: 0; display: grid; gap: 2px; }
2220
+ .td-day__r { border: 1px solid transparent; }
2221
+ .td-day__r[data-cur="true"] { background: var(--accent); border-color: var(--ring); }
2222
+ .td-day__r[data-past="true"] { opacity: 0.62; }
2223
+ .td-day__r[data-goal-tie="true"] { border-left: 3px solid var(--foreground); }
2224
+ .td-day__lead { min-width: 44px; font: 12px var(--mono); color: var(--muted-foreground); font-variant-numeric: tabular-nums; }
2225
+ .td-day__chips { display: inline-flex; gap: 4px; }
2226
+ .td-day__nowrow { display: flex; align-items: center; gap: 8px; padding: 0 8px; font: 11px var(--mono); color: var(--destructive); }
2227
+ .td-day__nowrow::after { content: ""; flex: 1; border-top: 2px solid var(--destructive); }
2228
+ .td-day__gh { display: flex; align-items: center; gap: 6px; margin: 4px 8px 2px; font-size: 12px; font-weight: 600; color: var(--muted-foreground); }
2229
+ .td-day__group { display: grid; gap: 2px; }
2230
+ .td-day__newdot { display: inline-block; flex: none; width: 7px; height: 7px; border-radius: 50%; background: var(--warning); }
2231
+ .td-day__chip { min-width: 84px; padding: 3px 8px; font: 500 11px/1.2 var(--mono); text-align: center; background: var(--muted); color: var(--foreground); }
2232
+
2233
+ /* the detail */
2234
+ .td-day__dhead { display: grid; gap: 4px; }
2235
+ .td-day__kind { font: 11px var(--mono); letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted-foreground); }
2236
+ .td-day__dhead h3 { margin: 0; font-size: 16px; line-height: 24px; color: var(--foreground); }
2237
+ .td-day__detail p { margin: 0; font-size: 14px; line-height: 20px; color: var(--foreground); }
2238
+ .td-day__detail p.td-day__note { font-size: 12px; line-height: 16px; color: var(--muted-foreground); }
2239
+ .td-day__acts { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
2240
+ .td-day__a {
2241
+ display: inline-flex;
2242
+ align-items: center;
2243
+ height: 28px;
2244
+ padding: 0 10px;
2245
+ border: 1px solid var(--border);
2246
+ color: var(--foreground);
2247
+ font-size: 12px;
2248
+ font-weight: 600;
2249
+ letter-spacing: 0.1em;
2250
+ text-transform: uppercase;
2251
+ text-decoration: none;
2252
+ white-space: nowrap;
2168
2253
  }
2169
- .td-goals__list { counter-reset: goal; }
2170
- .td-goal { display: flex; flex-direction: column; gap: 4px; }
2171
- .td-goal__head { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px; }
2172
- .td-goal__text { color: var(--foreground); font-weight: 500; }
2173
- .td-goal__text::before { counter-increment: goal; content: counter(goal) ". "; color: var(--muted-foreground); }
2174
- .td-goal--closed .td-goal__text { color: var(--muted-foreground); text-decoration: line-through; }
2175
- .td-goal__ctrls { display: inline-flex; gap: 4px; margin-left: auto; }
2176
- .td-goal__body { display: flex; flex-direction: column; gap: 4px; padding-left: 16px; }
2254
+ .td-day__a:focus-visible { outline: 2px solid color-mix(in oklab, var(--ring) 50%, var(--foreground)); outline-offset: 2px; }
2255
+ @media (pointer: coarse) { .td-day__a { min-height: 44px; } }
2256
+ .td-day__phase, .td-day__rel { display: grid; gap: 4px; }
2257
+ .td-day__phase h4, .td-day__rel h4, .td-day__notes h4, .td-day__draftbox h4, .td-day__more h4 {
2258
+ margin: 0; font-size: 11px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted-foreground);
2259
+ }
2260
+ .td-day__chk { list-style: none; margin: 0; padding: 0; display: grid; gap: 4px; font-size: 14px; color: var(--foreground); }
2261
+ .td-day__chk li { display: grid; grid-template-columns: 16px minmax(0, 1fr); gap: 6px; }
2262
+ .td-day__chk li::before { content: "○"; color: var(--muted-foreground); }
2263
+ .td-day__chk--ok li::before, .td-day__chk li.td-day__ok::before { content: "✓"; color: var(--foreground); }
2264
+ .td-day__chk li:has(.td-day__stepline--done)::before { content: "✓"; color: var(--foreground); }
2265
+ .td-day__chk li:has(.td-day__stepline--you)::before { content: "●"; color: var(--warning); }
2266
+ .td-day__who { margin-left: 6px; font-size: 11px; color: var(--muted-foreground); }
2267
+ .td-day__notes { display: grid; gap: 8px; padding: 10px; background: var(--muted); }
2268
+ .td-day__src { font: 11px var(--mono); color: var(--muted-foreground); }
2269
+ .td-day__ini { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; font-size: 14px; color: var(--foreground); }
2270
+ .td-day__job { display: grid; gap: 6px; padding: 10px 12px; border: 1px solid var(--border); background: var(--background); }
2271
+ .td-day__jobtop { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 8px; }
2272
+ .td-day__jobtitle { font-weight: 600; min-width: 0; color: var(--foreground); }
2273
+ .td-day__ctx { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 3px 10px; margin: 0; font-size: 14px; }
2274
+ .td-day__ctx dt { padding-top: 1px; font-size: 12px; color: var(--muted-foreground); }
2275
+ .td-day__ctx dd { margin: 0; color: var(--foreground); }
2276
+ .td-day__draftbox { display: grid; gap: 4px; }
2277
+ .td-day__draft { margin: 0; max-height: 220px; overflow: auto; padding: 8px 10px; white-space: pre-wrap; font: 12px/1.5 var(--mono); background: var(--muted); color: var(--foreground); }
2278
+ .td-day__revise { display: grid; gap: 6px; }
2279
+ .td-day__log { font-size: 12px; color: var(--muted-foreground); }
2280
+ .td-day__ch { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 8px; font-size: 14px; color: var(--foreground); }
2281
+ .td-day__slist { display: grid; }
2282
+ .td-day__sr { display: grid; grid-template-columns: 28px minmax(0, 1fr) auto auto; align-items: center; gap: 10px; padding: 7px 0; border-top: 1px solid var(--border); }
2283
+ .td-day__sr:first-child { border-top: 0; }
2284
+ .td-day__ic { display: grid; place-items: center; width: 28px; height: 28px; font: 600 11px var(--mono); background: var(--muted); color: var(--foreground); }
2285
+ .td-day__st { font-size: 12px; color: var(--muted-foreground); white-space: nowrap; }
2286
+ .td-day__more { grid-column: 2 / -1; display: grid; gap: 8px; min-width: 0; padding: 4px 0 6px; }
2287
+ .td-day__sites { list-style: none; margin: 0; padding: 0; display: grid; gap: 4px; }
2288
+ .td-day__sites li { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-width: 0; }
2289
+ .td-day__sites li span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2290
+ /* the goal detail keeps the steps the goals section showed (G6): Today cards and the agent steps that can be prepared */
2291
+ .td-goal__steps { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
2177
2292
  .td-goal__step { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px; }
2178
2293
  .td-goal__step-title { color: var(--foreground); }
2179
- .td-goal__meta, .td-goal__ctx, .td-goal__none, .td-goals__note, .td-goal__prep-text {
2294
+ .td-goal__meta, .td-goal__prep-text {
2180
2295
  color: var(--muted-foreground);
2181
2296
  font-size: 12px;
2182
2297
  line-height: 16px;
2183
2298
  margin: 0;
2184
2299
  }
2185
2300
  .td-goal__prep { display: inline-flex; align-items: center; gap: 8px; }
2186
- .td-goals__bar { display: flex; gap: 8px; }
2187
- .td-goals__run { margin: 0; font-size: 12px; line-height: 16px; color: var(--foreground); }
2188
- .td-goals__run--failed { color: var(--destructive); }
2189
- .td-goals__sub { margin: 4px 0 0; font-size: 12px; font-weight: 600; color: var(--muted-foreground); }
2190
2301
  .td-card {
2191
2302
  display: flex;
2192
2303
  flex-direction: column;
@@ -21,7 +21,7 @@ import { wsOf } from './data.js';
21
21
  import { state, setState, FIXTURE } from './state.js';
22
22
  import { COPY } from './today-copy.js';
23
23
  import { todayCard, acceptCard, rankNodes, scopeCtx, ensureFeed, feedOf, feedErrorOf, bannerOf, onFeed } from './today-cards.js';
24
- import { goalsSection, goalsSig } from './today-goals.js';
24
+ import { daySection, daySig } from './today-day.js';
25
25
 
26
26
  const SHOWN = 3; // cards drawn before "Show more": the first paint holds at most 17 controls (first-paint-today.test.js)
27
27
  let expanded = false;
@@ -118,10 +118,12 @@ function build(ctx0) {
118
118
  ? button({ label: expanded ? COPY.fewer : COPY.more(nodes.length - SHOWN), variant: 'ghost', attrs: { 'data-today': 'more' }, onClick: () => { expanded = !expanded; setState({}); } })
119
119
  : null;
120
120
  nodes.forEach((node) => node.setAttribute('role', 'listitem'));
121
- // today-goals (G6): the PM's goals sit above what is owed; they are joined to the cards of the first repository in scope
122
- const goalsWs = sc.repos.length ? wsOf(sc.repos[0]) : null;
123
- const goals = goalsSection({ ws: goalsWs, setState: ctx0.setState || setState });
124
- return { children: [head, goals, health, list, more], sig: JSON.stringify([sc.all, sc.repo, n, ready, expanded, banner]) + goalsSig(goalsWs) + nodes.map((x) => x.outerHTML).join('') };
121
+ // today-goals (G6, G16): the day section (goals, calendar, jobs, mentions, the day run) sits above what is owed; it is
122
+ // joined to the cards of the first repository in scope, and its initiatives open in that repository
123
+ const dayRepo = sc.repos.length ? sc.repos[0] : null;
124
+ const dayWs = dayRepo ? wsOf(dayRepo) : null;
125
+ const day = daySection({ ws: dayWs, setState: ctx0.setState || setState, openIni: dayRepo ? (id) => openInitiative(dayRepo, id) : null });
126
+ return { children: [head, day, health, list, more], sig: JSON.stringify([sc.all, sc.repo, n, ready, expanded, banner]) + daySig(dayWs) + nodes.map((x) => x.outerHTML).join('') };
125
127
  }
126
128
 
127
129
  /** The key of the control that has the focus inside the board, so it can be focused again after a rebuild. */
@@ -132,7 +134,7 @@ function focusKey(root) {
132
134
  const attr = ['data-today-act', 'data-today-more', 'data-today-add-note', 'data-today-accept', 'data-today-return'].find((k) => a.hasAttribute(k));
133
135
  if (card && attr) return { id: card.getAttribute('data-today-id'), attr };
134
136
  if (a.closest('[data-today-note]')) return { id: card && card.getAttribute('data-today-id'), note: true };
135
- if (a.getAttribute('data-goals') === 'input') return { goalsInput: true, caret: a.selectionStart };
137
+ if (a.hasAttribute('data-day-input')) return { dayInput: a.getAttribute('data-day-input'), caret: a.selectionStart };
136
138
  if (a.hasAttribute('data-today-scope')) return { scope: a.getAttribute('data-today-scope') };
137
139
  if (a.hasAttribute('data-today') && a.getAttribute('data-today') === 'more') return { more: true };
138
140
  return null;
@@ -143,10 +145,10 @@ function refocus(root, k) {
143
145
  let t = null;
144
146
  if (k.note && card) t = card.querySelector('[data-today-note] input, [data-today-note] textarea, input[data-today-note], textarea[data-today-note]');
145
147
  else if (k.attr && card) t = card.querySelector('[' + k.attr + ']');
146
- else if (k.goalsInput) t = root.querySelector('[data-goals="input"]');
148
+ else if (k.dayInput) t = [...root.querySelectorAll('[data-day-input]')].find((x) => x.getAttribute('data-day-input') === k.dayInput) || null;
147
149
  else if (k.scope) t = root.querySelector('[data-today-scope="' + k.scope + '"]');
148
150
  else if (k.more) t = root.querySelector('[data-today="more"]');
149
- if (t && !t.hidden) { try { t.focus({ preventScroll: true }); if (k.goalsInput && typeof k.caret === 'number') t.setSelectionRange(k.caret, k.caret); } catch { /* not focusable */ } }
151
+ if (t && !t.hidden) { try { t.focus({ preventScroll: true }); if (k.dayInput && typeof k.caret === 'number') t.setSelectionRange(k.caret, k.caret); } catch { /* not focusable */ } }
150
152
  }
151
153
 
152
154
  /** Draw Today into `host`. Safe to call on every render: nothing changes unless what Today shows changed. */
@@ -11,6 +11,14 @@ export const COPY = {
11
11
  none: 'No repository',
12
12
  menu: 'Choose a repository',
13
13
  open: (name) => 'Repository: ' + name + '. Choose a repository.',
14
+ branches: 'Branches and sessions',
15
+ },
16
+ branches: {
17
+ title: 'Branches and sessions',
18
+ sub: (trees, live) => trees + (trees === 1 ? ' branch' : ' branches') + ' · ' + live + ' live ' + (live === 1 ? 'session' : 'sessions'),
19
+ none: 'Add a repository to see its branches.',
20
+ noTrees: 'This repository has no working tree the workspace can read.',
21
+ hint: 'Every branch and the sessions running in it',
14
22
  },
15
23
  nav: {
16
24
  label: 'Destinations',