create-pmos 0.54.1 → 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.1",
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.1",
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",
@@ -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. */
@@ -89,6 +89,188 @@ export const COPY = {
89
89
  refused: (why) => `Nothing changed. ${why}`,
90
90
  unreached: 'The workspace did not answer.',
91
91
  },
92
+ /** The day section (initiative today-goals, PRD G9 to G22): the top bar, the four lists on the left and the detail on the
93
+ * right. A status word is short: one pill shows it. */
94
+ day: {
95
+ title: 'Your day',
96
+ toggle: (need) => (need ? `Your day · ${need} ${need === 1 ? 'needs' : 'need'} you` : 'Your day: calendar, jobs, goals and mentions'),
97
+ fold: 'Fold your day',
98
+ hello: { morning: 'Good morning.', afternoon: 'Good afternoon.', evening: 'Good evening.' },
99
+ free: (min) => `${Math.floor(min / 60)} h ${min % 60} min free`,
100
+ steps: ['Read', 'Sort', 'Agents work', 'You approve'],
101
+ stepTitle: ['Read your sources', 'Make the jobs', 'Agents do the work', 'You approve'],
102
+ stepHint: ['The session reads your calendar and your mentions.', 'Each job is tied to a goal or a meeting.', 'One session per job, at most 5 at once.', 'Only what sends or decides waits for you.'],
103
+ stepOf: (n) => `Prepare my day · step ${n} of 4`,
104
+ status: {
105
+ idle: 'One press: the agent reads, makes the jobs and does them. You approve only what sends or decides.',
106
+ reading: 'Reading your calendar and mentions…',
107
+ working: (w, n) => `${w} ${w === 1 ? 'agent' : 'agents'} working · ${n} ${n === 1 ? 'needs' : 'need'} you`,
108
+ review: (n, d) => `${n} ${n === 1 ? 'needs' : 'need'} you · ${d} done`,
109
+ done: (d) => `All jobs done · ${d} done`,
110
+ },
111
+ checked: (last, next) => (last ? `Checked ${last}${next ? ` · next ${next}` : ''}` : next ? `First check at ${next}` : 'No check yet'),
112
+ checking: 'Checking…',
113
+ checkNow: 'Check now',
114
+ checkOff: 'Daytime check off',
115
+ checkStarted: 'A check started. It reads only what changed.',
116
+ checkRefused: (why) => `No check started. ${why}`,
117
+ sources: 'Sources',
118
+ prepare: 'Prepare my day',
119
+ again: 'Prepare again',
120
+ starting: 'Starting…',
121
+ stillReading: 'Reading…',
122
+ prepareHint: 'Starts a session that reads your sources. When it is done, one session starts for each job the agent can do.',
123
+ runStarted: (at) => `Started at ${at}. The session reads your sources; the jobs show here when it is done.`,
124
+ runRunning: 'The read is still running. Nothing new started.',
125
+ runFailed: (why) => `Not started. ${why}`,
126
+ openSession: 'Open the session',
127
+ unreached: 'The workspace did not answer.',
128
+ refused: (why) => `Nothing changed. ${why}`,
129
+ oldServer: 'This workspace server has no day run yet. Only your goals show.',
130
+ panels: { calendar: 'Calendar', jobs: 'Jobs', goals: 'Goals for today', mentions: 'Mentions since yesterday', detail: 'Detail' },
131
+ now: (at) => `now ${at}`,
132
+ noCalendar: 'No events read for today.',
133
+ noJobs: 'No jobs yet. Press Prepare my day. Each job shows here; select one to see what it needs.',
134
+ readingJobs: 'Reading your sources…',
135
+ noMentions: 'No mentions read today.',
136
+ groups: { needs: 'Needs you', working: 'Agent working', done: 'Done', worth: 'Worth asking' },
137
+ jobsCount: (n, d) => `${n} ${n === 1 ? 'needs' : 'need'} you · ${d} done`,
138
+ goalsCount: (n, max) => `${n} of ${max}`,
139
+ mentionsCount: (n, need) => `${n} · ${need} ${need === 1 ? 'needs' : 'need'} you`,
140
+ all: 'All',
141
+ status2: {
142
+ waiting: 'waiting', working: 'agent working', review: 'approve', reviewAuto: 'check it', sending: 'sending', sent: 'sent',
143
+ done: 'done', failed: 'failed', you: 'your act', notStarted: 'not started', handled: 'handled', replyNeeded: 'reply needed',
144
+ },
145
+ newItem: 'new',
146
+ pill: { prepare: 'prepare', ready: 'ready', ended: 'ended', notes: 'notes', goal: (n) => `goal ${n}` },
147
+ watched: 'Watched channels, projects and spaces',
148
+ showIgnored: (n) => `Show ${n} ignored`,
149
+ hideIgnored: 'Hide ignored',
150
+ digestLine: (lvl, d, o) => `${lvl} · ${d} ${d === 1 ? 'decision' : 'decisions'} · ${o} open ${o === 1 ? 'question' : 'questions'}`,
151
+ levels: { watch: 'Watch', search: 'Search', ignore: 'Ignore' },
152
+ levelWord: { watch: 'watched', search: 'searched', ignore: 'ignored' },
153
+ levelHelp: 'Watch: read every message and sum it up. Search: read only mentions and words from your goals. Ignore: do not read.',
154
+ place: { slack: 'channel', jira: 'project', confluence: 'space' },
155
+ placeAdd: { slack: 'Add a channel', jira: 'Add a project', confluence: 'Add a space' },
156
+ placeHint: 'Type a name and press Enter. It is watched.',
157
+ readNote: { slack: 'Mentions, DMs and your threads are always read.', jira: 'Issues assigned to you and mentions of you are always read.', confluence: 'Pages and comments that mention you are always read.' },
158
+ decisions: 'Decisions',
159
+ open: 'Open questions',
160
+ forGoals: 'For your goals',
161
+ openIn: (svc) => `Open in ${svc} ↗`,
162
+ changeLevel: 'Level',
163
+ kind: {
164
+ meeting: 'Meeting', job: 'Job', jobGoal: (n) => `Job · goal ${n}`, goal: (n) => `Goal ${n}`, mention: (svc) => `Mention · ${svc}`,
165
+ settings: 'Settings', place: (svc, what, lvl) => `${svc} ${what} · ${lvl}`,
166
+ },
167
+ ended: 'ended',
168
+ purpose: 'Goal of the meeting',
169
+ prep: 'Prepare',
170
+ phases: { before: 'Before', during: 'During', after: 'After' },
171
+ who: { pm: 'you', agent: 'agent' },
172
+ needPrep: 'You need to prepare',
173
+ nothingToPrep: 'Nothing to prepare',
174
+ initiative: 'Initiative',
175
+ suggested: (why) => `Suggested. ${why}`,
176
+ openInitiative: 'Open the initiative',
177
+ countsToward: (n, text) => `Counts toward goal ${n}: ${text}`,
178
+ tieHow: 'To tie this meeting to a goal, add it as a goal. A goal with the same initiative takes its jobs.',
179
+ addAsGoal: 'Add as a goal for today',
180
+ goalAdded: 'The meeting is a goal for today.',
181
+ notesTitle: 'After the meeting: what was found',
182
+ wispr: 'Wispr Flow notes',
183
+ actions: 'Action items',
184
+ mail: 'Related mail',
185
+ noWispr: 'No Wispr Flow notes for this meeting.',
186
+ noMail: 'No related mail.',
187
+ openLink: 'open ↗',
188
+ about: 'About',
189
+ ask: 'The ask',
190
+ reply: 'Reply needed',
191
+ how: 'How',
192
+ why: 'Why now',
193
+ where: 'From',
194
+ yes: 'Yes',
195
+ no: 'No',
196
+ fromWho: (who, where) => (who && where ? `${who} in ${where}` : who || where || ''),
197
+ draft: 'The draft',
198
+ approve: 'Approve and let the agent send it',
199
+ approveAuto: 'Approve',
200
+ revise: 'Revise',
201
+ retry: 'Run it again',
202
+ noteLabel: 'What should change',
203
+ noteHint: 'What should change? For example: shorter, and lead with the decision.',
204
+ sendNote: 'Run it again with this note',
205
+ cancel: 'Cancel',
206
+ noteNeeded: 'Write what should change, in 1 to 500 characters.',
207
+ revisions: (n, max) => `${n} of ${max} revisions used`,
208
+ noMoreRevisions: 'This job used all its revisions. Do the rest yourself.',
209
+ openConversation: 'Open the conversation ↗',
210
+ openDraft: 'Open the draft ↗',
211
+ openSent: 'Open what was sent ↗',
212
+ decided: { approve: 'Your approval is recorded. The agent sends it now.', revise: 'The note is recorded. A new session starts for this job.', retry: 'The job waits for a session.' },
213
+ sentAt: (at) => `Sent at ${at}.`,
214
+ failedWhy: (why) => `Failed: ${why}`,
215
+ waitingNote: 'The agent has not started this job yet. At most 5 sessions run at once.',
216
+ workingNote: 'An agent session is doing this job now.',
217
+ sendingNote: 'You said yes to this draft. An agent session is sending it now.',
218
+ youNote: 'Only you can do this one. The link opens the conversation.',
219
+ autoNote: 'The agent did this alone. Nothing went outside this machine.',
220
+ notTask: 'The agent starts this job when you press Prepare my day.',
221
+ related: 'Related',
222
+ theJob: 'The job for it',
223
+ goalJobs: (d, n) => `${d} of ${n} jobs done`,
224
+ goalNeed: (n) => `${n} ${n === 1 ? 'needs' : 'need'} you`,
225
+ goalNone: 'No job yet',
226
+ meetings: 'Meetings',
227
+ jobs: 'Jobs',
228
+ stepsTitle: 'Steps found',
229
+ readNone: 'Press Prepare my day. A session starts and reads your sources by itself.',
230
+ readNow: 'The morning-brief session is reading your sources now.',
231
+ readDone: (ev, it) => `Read ${ev} ${ev === 1 ? 'event' : 'events'} and ${it} ${it === 1 ? 'item' : 'items'}.`,
232
+ notYet: 'Not started yet.',
233
+ madeJobs: (n, you, agent) => `${n} ${n === 1 ? 'job' : 'jobs'}: ${you} for you, ${agent} for the agent.`,
234
+ agentsLine: (w, q, d) => `${w} working, ${q} waiting, ${d} done. At most 5 sessions run at once.`,
235
+ approveLine: (n) => `${n} ${n === 1 ? 'job needs' : 'jobs need'} you. Nothing goes out before you approve it.`,
236
+ manageSources: 'Manage sources',
237
+ sourcesIntro: 'Read only. The agent asks you before it sends anything. Select a name for its settings.',
238
+ dayCheck: 'Daytime check',
239
+ every: 'How often',
240
+ everyOpts: [[30, '30 min'], [60, '60 min'], [0, 'Off']],
241
+ hours: 'Working hours',
242
+ beforeMeeting: 'Before a meeting',
243
+ beforeMeetingWhat: 'reads again 15 min before',
244
+ afterMeeting: 'After a meeting',
245
+ afterMeetingWhat: 'reads the notes and mail once',
246
+ checkHelp: 'Each check reads only what changed. After two empty checks, the wait doubles, up to 2 hours.',
247
+ checkRunning: (at) => `${at} · checking now`,
248
+ checkLog: (at, found, resolved) => `${at} · ${found} new, ${resolved} taken off`,
249
+ srcName: { calendar: 'Calendar', wispr: 'Wispr Flow', mail: 'Mail', slack: 'Slack', jira: 'Jira', confluence: 'Confluence', github: 'GitHub', drive: 'Google Drive', figma: 'Figma', website: 'Your websites' },
250
+ srcWhat: {
251
+ calendar: 'Today\'s events, who attends, agenda links',
252
+ wispr: 'Notes of meetings that ended',
253
+ mail: 'Threads tied to today\'s meetings: subjects and senders only',
254
+ slack: 'Mentions and DMs since yesterday, and the channels you watch',
255
+ jira: 'Issues and comments that mention you, reviews that wait',
256
+ confluence: 'Pages and comments that mention you',
257
+ github: 'Pull requests and issues that wait on you or mention you',
258
+ drive: 'Docs shared with you, and comments to you',
259
+ figma: 'Comments that mention you, files changed since yesterday',
260
+ website: 'Your pages: changes and broken links',
261
+ },
262
+ srcState: { off: 'off', 'not-reached': 'not reached', read: 'read', none: 'not read yet' },
263
+ gave: (n) => `gave ${n}`,
264
+ readSwitch: (name) => `Read ${name}`,
265
+ read: 'Read',
266
+ sites: 'Website addresses',
267
+ siteAdd: 'Add a website',
268
+ siteHint: 'https://… then press Enter',
269
+ siteBad: 'A website address starts with https or http.',
270
+ siteFull: 'At most 10 websites.',
271
+ remove: 'Remove',
272
+ noSources: 'The sources could not be read.',
273
+ },
92
274
  /** The Acceptance Gate card (D12): the PM's own act on a run. */
93
275
  accept: {
94
276
  review: 'Review',