create-pmos 0.54.1 → 0.55.1
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/bin/create-pmos.js +3 -1
- package/package.json +1 -1
- package/payload/KIT-VERSION +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/guide/proactive-engine.md +73 -9
- package/workspace/guide/today.md +27 -2
- package/workspace/lib/brief.js +268 -8
- package/workspace/lib/day-run.js +297 -0
- package/workspace/lib/goals.js +25 -3
- package/workspace/lib/proactive-cli.js +119 -6
- package/workspace/lib/proactive.js +53 -7
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +1 -1
- package/workspace/public/pm/components.css +135 -23
- package/workspace/public/pm/dest-today.js +10 -8
- package/workspace/public/pm/today-copy.js +185 -0
- package/workspace/public/pm/today-day.js +766 -0
- package/workspace/public/pm/today-goals.js +96 -161
- package/workspace/replaced-tests.json +10 -0
- package/workspace/server.js +213 -10
- package/workspace/skills/day-check.skill +56 -0
- package/workspace/skills/day-task.skill +47 -0
- package/workspace/skills/morning-brief.skill +47 -10
- package/workspace/test-files.json +2 -0
|
@@ -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
|
-
|
|
35
|
-
const
|
|
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 };
|
package/workspace/package.json
CHANGED
|
@@ -2148,45 +2148,157 @@
|
|
|
2148
2148
|
flex-direction: column;
|
|
2149
2149
|
gap: 8px;
|
|
2150
2150
|
}
|
|
2151
|
-
/* today-goals (
|
|
2152
|
-
.
|
|
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:
|
|
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-
|
|
2161
|
-
.td-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
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__warn { color: var(--warning); }
|
|
2176
|
+
.td-day__mono { font-family: var(--mono); font-variant-numeric: tabular-nums; font-size: 12px; }
|
|
2177
|
+
.td-day__steps { display: flex; flex-wrap: wrap; gap: 2px; list-style: none; margin: 0; padding: 0; }
|
|
2178
|
+
.td-day__ts { gap: 6px; padding: 0 6px; letter-spacing: 0.04em; color: var(--muted-foreground); }
|
|
2179
|
+
.td-day__ts[data-step-state="done"], .td-day__ts[data-step-state="now"] { color: var(--foreground); }
|
|
2180
|
+
.td-day__ts[data-step-state="now"] { background: var(--muted); }
|
|
2181
|
+
.td-day__ts[aria-current="true"] { border-color: var(--ring); }
|
|
2182
|
+
.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; }
|
|
2183
|
+
.td-day__ts[data-step-state="done"] .td-day__tsdot { background: currentColor; }
|
|
2184
|
+
.td-day__ts[data-step-state="now"] .td-day__tsdot { background: var(--foreground); animation: td-day-pulse 1.2s infinite; }
|
|
2185
|
+
@keyframes td-day-pulse { 50% { opacity: 0.3; } }
|
|
2186
|
+
@media (prefers-reduced-motion: reduce) { .td-day__ts[data-step-state="now"] .td-day__tsdot { animation: none; } }
|
|
2187
|
+
.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; }
|
|
2188
|
+
.td-day__check { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--muted-foreground); }
|
|
2189
|
+
.td-day__run { margin: 0; font-size: 12px; line-height: 16px; color: var(--foreground); }
|
|
2190
|
+
.td-day__run--failed { color: var(--destructive); }
|
|
2191
|
+
.td-day__note { margin: 0; font-size: 12px; line-height: 16px; color: var(--muted-foreground); }
|
|
2192
|
+
|
|
2193
|
+
.td-day__cols { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.25fr); gap: 12px; align-items: start; }
|
|
2194
|
+
.td-day__left { display: grid; gap: 12px; min-width: 0; }
|
|
2195
|
+
.td-day__panel, .td-day__detail {
|
|
2196
|
+
display: grid;
|
|
2197
|
+
gap: 8px;
|
|
2198
|
+
min-width: 0;
|
|
2199
|
+
padding: 10px 12px;
|
|
2200
|
+
border: 1px solid var(--border);
|
|
2201
|
+
background: var(--card);
|
|
2202
|
+
}
|
|
2203
|
+
.td-day__detail { position: sticky; top: 8px; max-height: calc(100vh - 16px); overflow: auto; align-content: start; }
|
|
2204
|
+
@container td-day (max-width: 859px) {
|
|
2205
|
+
.td-day__cols { grid-template-columns: minmax(0, 1fr); }
|
|
2206
|
+
.td-day__detail { position: static; max-height: none; }
|
|
2207
|
+
}
|
|
2208
|
+
.td-day__h2 {
|
|
2165
2209
|
display: flex;
|
|
2166
|
-
|
|
2167
|
-
gap:
|
|
2210
|
+
justify-content: space-between;
|
|
2211
|
+
gap: 8px;
|
|
2212
|
+
margin: 0;
|
|
2213
|
+
font-size: 12px;
|
|
2214
|
+
font-weight: 600;
|
|
2215
|
+
letter-spacing: 0.06em;
|
|
2216
|
+
text-transform: uppercase;
|
|
2217
|
+
color: var(--muted-foreground);
|
|
2218
|
+
}
|
|
2219
|
+
.td-day__h2 span { font-weight: 400; letter-spacing: 0; text-transform: none; }
|
|
2220
|
+
.td-day__rows { list-style: none; margin: 0; padding: 0; display: grid; gap: 2px; }
|
|
2221
|
+
.td-day__r { border: 1px solid transparent; }
|
|
2222
|
+
.td-day__r[data-cur="true"] { background: var(--accent); border-color: var(--ring); }
|
|
2223
|
+
.td-day__r[data-past="true"] { opacity: 0.62; }
|
|
2224
|
+
.td-day__r[data-goal-tie="true"] { border-left: 3px solid var(--foreground); }
|
|
2225
|
+
.td-day__lead { min-width: 44px; font: 12px var(--mono); color: var(--muted-foreground); font-variant-numeric: tabular-nums; }
|
|
2226
|
+
.td-day__chips { display: inline-flex; gap: 4px; }
|
|
2227
|
+
.td-day__nowrow { display: flex; align-items: center; gap: 8px; padding: 0 8px; font: 11px var(--mono); color: var(--destructive); }
|
|
2228
|
+
.td-day__nowrow::after { content: ""; flex: 1; border-top: 2px solid var(--destructive); }
|
|
2229
|
+
.td-day__gh { display: flex; align-items: center; gap: 6px; margin: 4px 8px 2px; font-size: 12px; font-weight: 600; color: var(--muted-foreground); }
|
|
2230
|
+
.td-day__group { display: grid; gap: 2px; }
|
|
2231
|
+
.td-day__newdot { display: inline-block; flex: none; width: 7px; height: 7px; border-radius: 50%; background: var(--warning); }
|
|
2232
|
+
.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); }
|
|
2233
|
+
|
|
2234
|
+
/* the detail */
|
|
2235
|
+
.td-day__dhead { display: grid; gap: 4px; }
|
|
2236
|
+
.td-day__kind { font: 11px var(--mono); letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted-foreground); }
|
|
2237
|
+
.td-day__dhead h3 { margin: 0; font-size: 16px; line-height: 24px; color: var(--foreground); }
|
|
2238
|
+
.td-day__detail p { margin: 0; font-size: 14px; line-height: 20px; color: var(--foreground); }
|
|
2239
|
+
.td-day__detail p.td-day__note { font-size: 12px; line-height: 16px; color: var(--muted-foreground); }
|
|
2240
|
+
.td-day__acts { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
|
|
2241
|
+
.td-day__a {
|
|
2242
|
+
display: inline-flex;
|
|
2243
|
+
align-items: center;
|
|
2244
|
+
height: 28px;
|
|
2245
|
+
padding: 0 10px;
|
|
2246
|
+
border: 1px solid var(--border);
|
|
2247
|
+
color: var(--foreground);
|
|
2248
|
+
font-size: 12px;
|
|
2249
|
+
font-weight: 600;
|
|
2250
|
+
letter-spacing: 0.1em;
|
|
2251
|
+
text-transform: uppercase;
|
|
2252
|
+
text-decoration: none;
|
|
2253
|
+
white-space: nowrap;
|
|
2168
2254
|
}
|
|
2169
|
-
.td-
|
|
2170
|
-
.td-
|
|
2171
|
-
.td-
|
|
2172
|
-
.td-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
.td-
|
|
2176
|
-
.td-
|
|
2255
|
+
.td-day__a:focus-visible { outline: 2px solid color-mix(in oklab, var(--ring) 50%, var(--foreground)); outline-offset: 2px; }
|
|
2256
|
+
@media (pointer: coarse) { .td-day__a { min-height: 44px; } }
|
|
2257
|
+
.td-day__phase, .td-day__rel { display: grid; gap: 4px; }
|
|
2258
|
+
.td-day__phase h4, .td-day__rel h4, .td-day__notes h4, .td-day__draftbox h4, .td-day__more h4 {
|
|
2259
|
+
margin: 0; font-size: 11px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted-foreground);
|
|
2260
|
+
}
|
|
2261
|
+
.td-day__chk { list-style: none; margin: 0; padding: 0; display: grid; gap: 4px; font-size: 14px; color: var(--foreground); }
|
|
2262
|
+
.td-day__chk li { display: grid; grid-template-columns: 16px minmax(0, 1fr); gap: 6px; }
|
|
2263
|
+
.td-day__chk li::before { content: "○"; color: var(--muted-foreground); }
|
|
2264
|
+
.td-day__chk--ok li::before, .td-day__chk li.td-day__ok::before { content: "✓"; color: var(--foreground); }
|
|
2265
|
+
.td-day__chk li:has(.td-day__stepline--done)::before { content: "✓"; color: var(--foreground); }
|
|
2266
|
+
.td-day__chk li:has(.td-day__stepline--you)::before { content: "●"; color: var(--warning); }
|
|
2267
|
+
.td-day__who { margin-left: 6px; font-size: 11px; color: var(--muted-foreground); }
|
|
2268
|
+
.td-day__notes { display: grid; gap: 8px; padding: 10px; background: var(--muted); }
|
|
2269
|
+
.td-day__src { font: 11px var(--mono); color: var(--muted-foreground); }
|
|
2270
|
+
.td-day__ini { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; font-size: 14px; color: var(--foreground); }
|
|
2271
|
+
.td-day__job { display: grid; gap: 6px; padding: 10px 12px; border: 1px solid var(--border); background: var(--background); }
|
|
2272
|
+
.td-day__jobtop { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 8px; }
|
|
2273
|
+
.td-day__jobtitle { font-weight: 600; min-width: 0; color: var(--foreground); }
|
|
2274
|
+
.td-day__ctx { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 3px 10px; margin: 0; font-size: 14px; }
|
|
2275
|
+
.td-day__ctx dt { padding-top: 1px; font-size: 12px; color: var(--muted-foreground); }
|
|
2276
|
+
.td-day__ctx dd { margin: 0; color: var(--foreground); }
|
|
2277
|
+
.td-day__draftbox { display: grid; gap: 4px; }
|
|
2278
|
+
.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); }
|
|
2279
|
+
.td-day__revise { display: grid; gap: 6px; }
|
|
2280
|
+
.td-day__log { font-size: 12px; color: var(--muted-foreground); }
|
|
2281
|
+
.td-day__ch { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 8px; font-size: 14px; color: var(--foreground); }
|
|
2282
|
+
.td-day__slist { display: grid; }
|
|
2283
|
+
.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); }
|
|
2284
|
+
.td-day__sr:first-child { border-top: 0; }
|
|
2285
|
+
.td-day__ic { display: grid; place-items: center; width: 28px; height: 28px; font: 600 11px var(--mono); background: var(--muted); color: var(--foreground); }
|
|
2286
|
+
.td-day__st { font-size: 12px; color: var(--muted-foreground); white-space: nowrap; }
|
|
2287
|
+
.td-day__more { grid-column: 2 / -1; display: grid; gap: 8px; min-width: 0; padding: 4px 0 6px; }
|
|
2288
|
+
.td-day__sites { list-style: none; margin: 0; padding: 0; display: grid; gap: 4px; }
|
|
2289
|
+
.td-day__sites li { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-width: 0; }
|
|
2290
|
+
.td-day__sites li span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2291
|
+
/* the goal detail keeps the steps the goals section showed (G6): Today cards and the agent steps that can be prepared */
|
|
2292
|
+
.td-goal__steps { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
|
|
2177
2293
|
.td-goal__step { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px; }
|
|
2178
2294
|
.td-goal__step-title { color: var(--foreground); }
|
|
2179
|
-
.td-goal__meta, .td-
|
|
2295
|
+
.td-goal__meta, .td-goal__prep-text {
|
|
2180
2296
|
color: var(--muted-foreground);
|
|
2181
2297
|
font-size: 12px;
|
|
2182
2298
|
line-height: 16px;
|
|
2183
2299
|
margin: 0;
|
|
2184
2300
|
}
|
|
2185
2301
|
.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
2302
|
.td-card {
|
|
2191
2303
|
display: flex;
|
|
2192
2304
|
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 {
|
|
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
|
|
122
|
-
|
|
123
|
-
const
|
|
124
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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,191 @@ 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
|
+
readFailed: (why) => `The read stopped: ${why}`,
|
|
108
|
+
working: (w, n) => `${w} ${w === 1 ? 'agent' : 'agents'} working · ${n} ${n === 1 ? 'needs' : 'need'} you`,
|
|
109
|
+
review: (n, d) => `${n} ${n === 1 ? 'needs' : 'need'} you · ${d} done`,
|
|
110
|
+
done: (d) => `All jobs done · ${d} done`,
|
|
111
|
+
},
|
|
112
|
+
checked: (last, next) => (last ? `Checked ${last}${next ? ` · next ${next}` : ''}` : next ? `First check at ${next}` : 'No check yet'),
|
|
113
|
+
checking: 'Checking…',
|
|
114
|
+
checkNow: 'Check now',
|
|
115
|
+
checkOff: 'Daytime check off',
|
|
116
|
+
checkStarted: 'A check started. It reads only what changed.',
|
|
117
|
+
checkRefused: (why) => `No check started. ${why}`,
|
|
118
|
+
sources: 'Sources',
|
|
119
|
+
prepare: 'Prepare my day',
|
|
120
|
+
again: 'Prepare again',
|
|
121
|
+
starting: 'Starting…',
|
|
122
|
+
stillReading: 'Reading…',
|
|
123
|
+
prepareHint: 'Starts a session that reads your sources. When it is done, one session starts for each job the agent can do.',
|
|
124
|
+
runStarted: (at) => `Started at ${at}. The session reads your sources; the jobs show here when it is done.`,
|
|
125
|
+
runRunning: 'The read is still running. Nothing new started.',
|
|
126
|
+
runFailed: (why) => `Not started. ${why}`,
|
|
127
|
+
openSession: 'Open the session',
|
|
128
|
+
unreached: 'The workspace did not answer.',
|
|
129
|
+
refused: (why) => `Nothing changed. ${why}`,
|
|
130
|
+
oldServer: 'This workspace server has no day run yet. Only your goals show.',
|
|
131
|
+
panels: { calendar: 'Calendar', jobs: 'Jobs', goals: 'Goals for today', mentions: 'Mentions since yesterday', detail: 'Detail' },
|
|
132
|
+
now: (at) => `now ${at}`,
|
|
133
|
+
noCalendar: 'No events read for today.',
|
|
134
|
+
noJobs: 'No jobs yet. Press Prepare my day. Each job shows here; select one to see what it needs.',
|
|
135
|
+
readingJobs: 'Reading your sources…',
|
|
136
|
+
noMentions: 'No mentions read today.',
|
|
137
|
+
groups: { needs: 'Needs you', working: 'Agent working', done: 'Done', worth: 'Worth asking' },
|
|
138
|
+
jobsCount: (n, d) => `${n} ${n === 1 ? 'needs' : 'need'} you · ${d} done`,
|
|
139
|
+
goalsCount: (n, max) => `${n} of ${max}`,
|
|
140
|
+
mentionsCount: (n, need) => `${n} · ${need} ${need === 1 ? 'needs' : 'need'} you`,
|
|
141
|
+
all: 'All',
|
|
142
|
+
status2: {
|
|
143
|
+
waiting: 'waiting', working: 'agent working', review: 'approve', reviewAuto: 'check it', sending: 'sending', sent: 'sent',
|
|
144
|
+
done: 'done', failed: 'failed', you: 'your act', notStarted: 'not started', handled: 'handled', replyNeeded: 'reply needed',
|
|
145
|
+
},
|
|
146
|
+
newItem: 'new',
|
|
147
|
+
pill: { prepare: 'prepare', ready: 'ready', ended: 'ended', notes: 'notes', goal: (n) => `goal ${n}` },
|
|
148
|
+
watched: 'Watched channels, projects and spaces',
|
|
149
|
+
showIgnored: (n) => `Show ${n} ignored`,
|
|
150
|
+
hideIgnored: 'Hide ignored',
|
|
151
|
+
digestLine: (lvl, d, o) => `${lvl} · ${d} ${d === 1 ? 'decision' : 'decisions'} · ${o} open ${o === 1 ? 'question' : 'questions'}`,
|
|
152
|
+
levels: { watch: 'Watch', search: 'Search', ignore: 'Ignore' },
|
|
153
|
+
levelWord: { watch: 'watched', search: 'searched', ignore: 'ignored' },
|
|
154
|
+
levelHelp: 'Watch: read every message and sum it up. Search: read only mentions and words from your goals. Ignore: do not read.',
|
|
155
|
+
place: { slack: 'channel', jira: 'project', confluence: 'space' },
|
|
156
|
+
placeAdd: { slack: 'Add a channel', jira: 'Add a project', confluence: 'Add a space' },
|
|
157
|
+
placeHint: 'Type a name and press Enter. It is watched.',
|
|
158
|
+
noPlaces: { slack: 'Your channels are listed after the first read of Slack. Press Prepare my day.', jira: 'Your projects are listed after the first read of Jira. Press Prepare my day.', confluence: 'Your spaces are listed after the first read of Confluence. Press Prepare my day.' },
|
|
159
|
+
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.' },
|
|
160
|
+
decisions: 'Decisions',
|
|
161
|
+
open: 'Open questions',
|
|
162
|
+
forGoals: 'For your goals',
|
|
163
|
+
openIn: (svc) => `Open in ${svc} ↗`,
|
|
164
|
+
changeLevel: 'Level',
|
|
165
|
+
kind: {
|
|
166
|
+
meeting: 'Meeting', job: 'Job', jobGoal: (n) => `Job · goal ${n}`, goal: (n) => `Goal ${n}`, mention: (svc) => `Mention · ${svc}`,
|
|
167
|
+
settings: 'Settings', place: (svc, what, lvl) => `${svc} ${what} · ${lvl}`,
|
|
168
|
+
},
|
|
169
|
+
ended: 'ended',
|
|
170
|
+
purpose: 'Goal of the meeting',
|
|
171
|
+
prep: 'Prepare',
|
|
172
|
+
phases: { before: 'Before', during: 'During', after: 'After' },
|
|
173
|
+
who: { pm: 'you', agent: 'agent' },
|
|
174
|
+
needPrep: 'You need to prepare',
|
|
175
|
+
nothingToPrep: 'Nothing to prepare',
|
|
176
|
+
initiative: 'Initiative',
|
|
177
|
+
suggested: (why) => `Suggested. ${why}`,
|
|
178
|
+
openInitiative: 'Open the initiative',
|
|
179
|
+
countsToward: (n, text) => `Counts toward goal ${n}: ${text}`,
|
|
180
|
+
tieHow: 'To tie this meeting to a goal, add it as a goal. A goal with the same initiative takes its jobs.',
|
|
181
|
+
addAsGoal: 'Add as a goal for today',
|
|
182
|
+
goalAdded: 'The meeting is a goal for today.',
|
|
183
|
+
notesTitle: 'After the meeting: what was found',
|
|
184
|
+
wispr: 'Wispr Flow notes',
|
|
185
|
+
actions: 'Action items',
|
|
186
|
+
mail: 'Related mail',
|
|
187
|
+
noWispr: 'No Wispr Flow notes for this meeting.',
|
|
188
|
+
noMail: 'No related mail.',
|
|
189
|
+
openLink: 'open ↗',
|
|
190
|
+
about: 'About',
|
|
191
|
+
ask: 'The ask',
|
|
192
|
+
reply: 'Reply needed',
|
|
193
|
+
how: 'How',
|
|
194
|
+
why: 'Why now',
|
|
195
|
+
where: 'From',
|
|
196
|
+
yes: 'Yes',
|
|
197
|
+
no: 'No',
|
|
198
|
+
fromWho: (who, where) => (who && where ? `${who} in ${where}` : who || where || ''),
|
|
199
|
+
draft: 'The draft',
|
|
200
|
+
approve: 'Approve and let the agent send it',
|
|
201
|
+
approveAuto: 'Approve',
|
|
202
|
+
revise: 'Revise',
|
|
203
|
+
retry: 'Run it again',
|
|
204
|
+
noteLabel: 'What should change',
|
|
205
|
+
noteHint: 'What should change? For example: shorter, and lead with the decision.',
|
|
206
|
+
sendNote: 'Run it again with this note',
|
|
207
|
+
cancel: 'Cancel',
|
|
208
|
+
noteNeeded: 'Write what should change, in 1 to 500 characters.',
|
|
209
|
+
revisions: (n, max) => `${n} of ${max} revisions used`,
|
|
210
|
+
noMoreRevisions: 'This job used all its revisions. Do the rest yourself.',
|
|
211
|
+
openConversation: 'Open the conversation ↗',
|
|
212
|
+
openDraft: 'Open the draft ↗',
|
|
213
|
+
openSent: 'Open what was sent ↗',
|
|
214
|
+
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.' },
|
|
215
|
+
sentAt: (at) => `Sent at ${at}.`,
|
|
216
|
+
failedWhy: (why) => `Failed: ${why}`,
|
|
217
|
+
waitingNote: 'The agent has not started this job yet. At most 5 sessions run at once.',
|
|
218
|
+
workingNote: 'An agent session is doing this job now.',
|
|
219
|
+
sendingNote: 'You said yes to this draft. An agent session is sending it now.',
|
|
220
|
+
youNote: 'Only you can do this one. The link opens the conversation.',
|
|
221
|
+
autoNote: 'The agent did this alone. Nothing went outside this machine.',
|
|
222
|
+
notTask: 'The agent starts this job when you press Prepare my day.',
|
|
223
|
+
related: 'Related',
|
|
224
|
+
theJob: 'The job for it',
|
|
225
|
+
goalJobs: (d, n) => `${d} of ${n} jobs done`,
|
|
226
|
+
goalNeed: (n) => `${n} ${n === 1 ? 'needs' : 'need'} you`,
|
|
227
|
+
goalNone: 'No job yet',
|
|
228
|
+
meetings: 'Meetings',
|
|
229
|
+
jobs: 'Jobs',
|
|
230
|
+
stepsTitle: 'Steps found',
|
|
231
|
+
readNone: 'Press Prepare my day. A session starts and reads your sources by itself.',
|
|
232
|
+
readNow: 'The morning-brief session is reading your sources now.',
|
|
233
|
+
readFailed: (why) => `The morning-brief session did not finish: ${why} Press Prepare again to try again.`,
|
|
234
|
+
readDone: (ev, it) => `Read ${ev} ${ev === 1 ? 'event' : 'events'} and ${it} ${it === 1 ? 'item' : 'items'}.`,
|
|
235
|
+
notYet: 'Not started yet.',
|
|
236
|
+
madeJobs: (n, you, agent) => `${n} ${n === 1 ? 'job' : 'jobs'}: ${you} for you, ${agent} for the agent.`,
|
|
237
|
+
agentsLine: (w, q, d) => `${w} working, ${q} waiting, ${d} done. At most 5 sessions run at once.`,
|
|
238
|
+
approveLine: (n) => `${n} ${n === 1 ? 'job needs' : 'jobs need'} you. Nothing goes out before you approve it.`,
|
|
239
|
+
manageSources: 'Manage sources',
|
|
240
|
+
sourcesIntro: 'Read only. The agent asks you before it sends anything. Select a name for its settings.',
|
|
241
|
+
dayCheck: 'Daytime check',
|
|
242
|
+
every: 'How often',
|
|
243
|
+
everyOpts: [[30, '30 min'], [60, '60 min'], [0, 'Off']],
|
|
244
|
+
hours: 'Working hours',
|
|
245
|
+
beforeMeeting: 'Before a meeting',
|
|
246
|
+
beforeMeetingWhat: 'reads again 15 min before',
|
|
247
|
+
afterMeeting: 'After a meeting',
|
|
248
|
+
afterMeetingWhat: 'reads the notes and mail once',
|
|
249
|
+
checkHelp: 'Each check reads only what changed. After two empty checks, the wait doubles, up to 2 hours.',
|
|
250
|
+
checkRunning: (at) => `${at} · checking now`,
|
|
251
|
+
checkLog: (at, found, resolved) => `${at} · ${found} new, ${resolved} taken off`,
|
|
252
|
+
srcName: { calendar: 'Calendar', wispr: 'Wispr Flow', mail: 'Mail', slack: 'Slack', jira: 'Jira', confluence: 'Confluence', github: 'GitHub', drive: 'Google Drive', figma: 'Figma', website: 'Your websites' },
|
|
253
|
+
srcWhat: {
|
|
254
|
+
calendar: 'Today\'s events, who attends, agenda links',
|
|
255
|
+
wispr: 'Notes of meetings that ended',
|
|
256
|
+
mail: 'Threads tied to today\'s meetings: subjects and senders only',
|
|
257
|
+
slack: 'Mentions and DMs since yesterday, and the channels you watch',
|
|
258
|
+
jira: 'Issues and comments that mention you, reviews that wait',
|
|
259
|
+
confluence: 'Pages and comments that mention you',
|
|
260
|
+
github: 'Pull requests and issues that wait on you or mention you',
|
|
261
|
+
drive: 'Docs shared with you, and comments to you',
|
|
262
|
+
figma: 'Comments that mention you, files changed since yesterday',
|
|
263
|
+
website: 'Your pages: changes and broken links',
|
|
264
|
+
},
|
|
265
|
+
srcState: { off: 'off', 'not-reached': 'not reached', read: 'read', none: 'not read yet' },
|
|
266
|
+
gave: (n) => `gave ${n}`,
|
|
267
|
+
readSwitch: (name) => `Read ${name}`,
|
|
268
|
+
read: 'Read',
|
|
269
|
+
sites: 'Website addresses',
|
|
270
|
+
siteAdd: 'Add a website',
|
|
271
|
+
siteHint: 'https://… then press Enter',
|
|
272
|
+
siteBad: 'A website address starts with https or http.',
|
|
273
|
+
siteFull: 'At most 10 websites.',
|
|
274
|
+
remove: 'Remove',
|
|
275
|
+
noSources: 'The sources could not be read.',
|
|
276
|
+
},
|
|
92
277
|
/** The Acceptance Gate card (D12): the PM's own act on a run. */
|
|
93
278
|
accept: {
|
|
94
279
|
review: 'Review',
|