create-pmos 0.525.0 → 0.526.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.
Files changed (71) hide show
  1. package/package.json +38 -41
  2. package/payload/KIT-VERSION +1 -1
  3. package/payload/planning/evals/eval-calibration.md +1 -1
  4. package/payload/planning/okrs/health-budgets.md +1 -1
  5. package/payload/state/discoveries.md +1 -1
  6. package/workspace/briefing-premium-board-test.js +1 -1
  7. package/workspace/changelog.json +37 -0
  8. package/workspace/ev-everywhere-test.js +2 -2
  9. package/workspace/guide/agent-capabilities.md +28 -2
  10. package/workspace/guide/board.md +14 -1
  11. package/workspace/guide/claude-proposes.md +3 -1
  12. package/workspace/guide/home.md +11 -5
  13. package/workspace/guide/idle-lab.md +2 -2
  14. package/workspace/guide/lifecycle.md +16 -3
  15. package/workspace/guide/today.md +11 -3
  16. package/workspace/lib/bg-queue.js +7 -3
  17. package/workspace/lib/board-premium.js +4 -1
  18. package/workspace/lib/engine/boards.js +3 -2
  19. package/workspace/lib/headless.js +19 -4
  20. package/workspace/lib/idle-lab-engine.js +531 -0
  21. package/workspace/lib/initiative-chat.js +12 -2
  22. package/workspace/lib/initiative-cli.js +1 -1
  23. package/workspace/lib/initiative-premium.js +11 -3
  24. package/workspace/lib/lab-cli.js +220 -0
  25. package/workspace/lib/lifecycle-stage.js +17 -3
  26. package/workspace/lib/one-stage.js +131 -0
  27. package/workspace/lib/pmos-verbs.js +3 -0
  28. package/workspace/lib/proposals.js +5 -2
  29. package/workspace/lib/report-draft.js +1 -1
  30. package/workspace/lib/route-modules.js +1 -0
  31. package/workspace/lib/routes-board-premium.js +22 -0
  32. package/workspace/lib/routes-boards.js +61 -12
  33. package/workspace/lib/routes-notes.js +1 -1
  34. package/workspace/lib/routes-your-move.js +64 -0
  35. package/workspace/lib/status-signal.js +123 -0
  36. package/workspace/lib/workflow-cli.js +3 -1
  37. package/workspace/lib/your-move.js +61 -0
  38. package/workspace/package-lock.json +2 -2
  39. package/workspace/package.json +1 -1
  40. package/workspace/public/pm/bd-acts.js +2 -1
  41. package/workspace/public/pm/bd-cols.js +8 -5
  42. package/workspace/public/pm/bd-prog-panel.js +3 -2
  43. package/workspace/public/pm/bd-span.js +2 -1
  44. package/workspace/public/pm/bd-views.js +8 -6
  45. package/workspace/public/pm/cozy-home.css +3 -0
  46. package/workspace/public/pm/cozy-today.css +151 -0
  47. package/workspace/public/pm/cozy.css +9 -0
  48. package/workspace/public/pm/good-morning.js +327 -0
  49. package/workspace/public/pm/grill-words.js +16 -1
  50. package/workspace/public/pm/home-app.js +187 -18
  51. package/workspace/public/pm/home-data.js +45 -13
  52. package/workspace/public/pm/home-day.js +66 -14
  53. package/workspace/public/pm/home-ini-model.js +1 -1
  54. package/workspace/public/pm/home-stage-card.js +7 -0
  55. package/workspace/public/pm/ini-tasks.js +2 -2
  56. package/workspace/public/pm/ini-view-model.js +10 -3
  57. package/workspace/public/pm/ini-view.css +9 -0
  58. package/workspace/public/pm/ini-view.js +19 -5
  59. package/workspace/public/pm/move-rank.js +155 -0
  60. package/workspace/public/pm/premium-board.js +7 -4
  61. package/workspace/public/pm/premium-initiative-head.js +39 -4
  62. package/workspace/public/pm/stage-chip.js +44 -0
  63. package/workspace/run-tests.js +1 -0
  64. package/workspace/server.js +43 -1
  65. package/workspace/skills/idle-lab-prototype.skill +36 -0
  66. package/workspace/skills/idle-lab-scan.skill +32 -0
  67. package/workspace/test-lists/good-morning.json +9 -0
  68. package/workspace/test-lists/idle-lab-engine.json +9 -0
  69. package/workspace/test-lists/one-stage.json +5 -0
  70. package/workspace/test-lists/status-signal.json +5 -0
  71. package/workspace/test-lists/your-move.json +9 -0
@@ -3,9 +3,12 @@
3
3
  * "Good morning" opens Your day as an item in the Viewer (a phone only since batch 14 item 4). Batch 15 item 3 (design E, frames YdE1Ready,
4
4
  * YdE2AllClear, YdE3Evening, YdE6BigDay, YdE7Rough): the opening card is Claude's small mood face and one first-person line, with chips for
5
5
  * the work Claude really did; Your move holds only the calls that need the PM; the rest folds:
6
- * Your move the calls only the PM can make (homeList's move rows with call: true), open by default
6
+ * Your move one ranked list (workspace grill W3, frame WgW3Move; pm/move-rank.js): every open ✦ of the products Home reads, in
7
+ * order of consequence, each with its age and one press, then the calls only the PM can make (homeList's move rows
8
+ * with call: true). Proposals older than two weeks fold under "Older", never gone. Open by default
7
9
  * New for you (feedback batch 16 item 5b, canvas FmA2Day) the FYI items of GET /api/news-for-you: tasks given to the PM and work
8
- * finished while away. The 3 newest, then "N more" (the whole list in the Viewer, cozy-news.js); ✓✓ in the fold's header
10
+ * finished while away. The tasks given to the PM are one row, "32 tasks given to you" (W3), that opens the list; the 3
11
+ * newest other items, then "N more" (the whole list in the Viewer, cozy-news.js); ✓✓ in the fold's header
9
12
  * marks all seen; today's digests of the morning brief are one row. No item and no digest, no fold. It replaces the New
10
13
  * for you page of the place menu.
11
14
  * Drafted for you the drafts of today's morning prep run (GET /api/phone-day finished) and the weekly update; a draft the PM is done
@@ -32,13 +35,14 @@
32
35
  import { todayOf, hm } from './home-data.js';
33
36
  import { adler, adlerScene, asleep, firstOpenToday, dress, dressDay } from './cozy-adler.js'; // Adler, Claude's face (batch 16 item 2): AdB1-3, AdP1, AdP4; dressed for the season (batch 18 item 8-B)
34
37
  import { initials, ago } from './cozy-evmodel.js';
38
+ import { rankMove } from './move-rank.js'; // Your move is one ranked list (workspace grill W3)
35
39
 
36
40
  const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
37
41
  const plural = (n, one, many) => n + ' ' + (n === 1 ? one : many);
38
42
  const clip = (s, n) => { const t = String(s || '').replace(/\s+/g, ' ').trim(); return t.length > n ? t.slice(0, n - 1) + '…' : t; };
39
43
  export const DAY_ICON = 'M12 3l1.8 4.6L18 9l-4.2 1.4L12 15l-1.8-4.6L6 9l4.2-1.4z';
40
44
  const CLOCK = 'M12 4a8 8 0 1 0 0 16a8 8 0 1 0 0-16M12 8v4l3 2', CAL = 'M4 5h16v15H4zM8 3v4M16 3v4M4 10h16', CHEV = 'M10 7l5 5-5 5';
41
- const CHECK = 'M5 12.5l4.5 4.5L19 7', OPEN = 'M14 4h6v6M20 4l-9 9M18 14v6H4V6h6';
45
+ const CHECK = 'M5 12.5l4.5 4.5L19 7', OPEN = 'M14 4h6v6M20 4l-9 9M18 14v6H4V6h6', PEN = 'M4 20h4L19 9l-4-4L4 16zM13.5 6.5l4 4', XX = 'M6 6l12 12M18 6L6 18';
42
46
  const AGAIN = 'M4 12a8 8 0 0 1 14-5M20 12a8 8 0 0 1-14 5M18 3v4h-4M6 21v-4h4';
43
47
  const svg = (d, w = 15, sw = 1.8) => `<svg aria-hidden="true" width="${w}" height="${w}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="${sw}" stroke-linecap="round" stroke-linejoin="round"><path d="${d}"/></svg>`;
44
48
 
@@ -96,7 +100,7 @@ export function tomorrowOf(D, now) {
96
100
 
97
101
  /** Your day's words. M: homeList's model (move); D: Home's data (today's meetings, phoneDay, the brief page's me and mentions);
98
102
  * R: reviewModel. o: { delightOk: false when the delighted face showed on another day this week, news: the GET /api/news-for-you answer,
99
- * ask: home-mentions dayAskOf (the one waiting ask the opening card names) }.
103
+ * ask: home-mentions dayAskOf (the one waiting ask the opening card names), rank: { products, programs, move } for pm/move-rank.js (W3) }.
100
104
  * → { mood, line, sub, chips, misses, offer, calls, drafts, wait, done, next, week, thanks, news, tomorrow } */
101
105
  export function dayModel(M, D, R, now = Date.now(), o = {}) {
102
106
  D = D || {};
@@ -104,7 +108,8 @@ export function dayModel(M, D, R, now = Date.now(), o = {}) {
104
108
  const t = todayOf(D).filter((m) => m && m.start && !Number.isNaN(Date.parse(m.start))).sort((x, y) => Date.parse(x.start) - Date.parse(y.start));
105
109
  const mins = t.reduce((n, m) => { const s = span(m); return n + Math.max(0, Math.round((s.b - s.a) / 60000)); }, 0);
106
110
  const next = t.find((m) => m.state === 'now') || t.find((m) => m.state !== 'past' && span(m).b > now) || null;
107
- const meetText = !t.length ? 'No meeting today.' : plural(t.length, 'meeting', 'meetings') + ', ' + dur(mins) + '. '
111
+ // no meeting, no meeting line (W3): the day says nothing about meetings it does not have
112
+ const meetText = !t.length ? '' : plural(t.length, 'meeting', 'meetings') + ', ' + dur(mins) + '. '
108
113
  + (!next ? 'They are all over.' : next.state === 'now' ? (next.title || 'A meeting') + ' is on now.' : 'The ' + (next === t[0] ? 'first' : 'next') + ' is at ' + hm(next.start) + '.');
109
114
  const un = R && R.started && !R.done ? 'Step ' + (R.at + 1) + ' of 4' : '';
110
115
 
@@ -134,7 +139,7 @@ export function dayModel(M, D, R, now = Date.now(), o = {}) {
134
139
  mood = 'concerned';
135
140
  line = 'Straight answer: ' + (misses.length ? (misses.length === 1 ? 'I couldn’t finish one of today’s drafts.' : 'I couldn’t finish ' + misses.length + ' of today’s drafts.')
136
141
  : (blocked.length === 1 ? clip(blocked[0].name, 80) + ' is stuck.' : blocked.length + ' pieces of work are stuck.'));
137
- sub = misses.length ? 'That one’s on me. I can run it again.' : 'It’s your call, so it’s first in Your move.';
142
+ sub = misses.length ? 'That one’s on me. I can run it again.' : 'It’s your call, and it’s in Your move.';
138
143
  } else if (!calls.length && done > 0 && o.delightOk !== false) {
139
144
  // delighted: earned, at most once a week (YdE2AllClear)
140
145
  mood = 'delighted'; line = 'That was everything that needed you today.';
@@ -156,10 +161,11 @@ export function dayModel(M, D, R, now = Date.now(), o = {}) {
156
161
  const fail = syncFailOf(o.sources, now);
157
162
  let failLead = false;
158
163
  if (fail && mood !== 'concerned') { const w = failWords(fail); mood = 'concerned'; line = w.line; sub = w.sub; failLead = true; }
159
- // no prep run today: say so, and offer it (Prepare my day); never claim work that did not happen
160
- if (!run && h < 17) offer = { kind: 'prep', text: 'I haven’t prepared today yet.', sub: 'Shall I prep your meetings and draft what I can?' };
164
+ // no prep run today: say so, and offer it (Prepare my day); never claim work that did not happen. No meeting, no prep line (W3)
165
+ if (!run && h < 17 && t.length) offer = { kind: 'prep', text: 'I haven’t prepared today yet.', sub: 'Shall I prep your meetings and draft what I can?' };
161
166
  return {
162
167
  mood, face: run && run.working && mood === 'calm' ? 'working' : mood, line, sub, chips, misses, offer, calls, drafts, wait, done, thanks, fail, failLead, ask: o.ask || null,
168
+ rank: o.rank ? rankMove({ ...o.rank, calls, now }) : null,
163
169
  news: newsOf(o.news), digests: digestsOf(D), tomorrow: tomorrowOf(D, now),
164
170
  next: next ? { meeting: next, now: next.state === 'now', time: hm(next.start), title: next.title || 'A meeting' } : null,
165
171
  week: { sub: un || 'Weekly review' },
@@ -179,7 +185,8 @@ export function openHtml(Y, o = {}) {
179
185
  if (rain) o = { ...o, scene: true };
180
186
  // a dressed day's scene is wider (460 x 170), so the card's 150 px shows its garland or lights, not only the ground (AsPlaces)
181
187
  const head = rain ? `<div class="yd-scene">${rain}</div>` : o.scene ? `<div class="yd-scene"${dress() ? ' data-ad-dress role="button" tabindex="0" aria-label="Why Adler is dressed"' : ''}>${adlerScene('morning', 'handled', { label: false, dress: true, ...(dress() ? { w: 460, h: 170 } : {}) })}</div>` : o.big ? '' : faceHtml(Y.face || Y.mood);
182
- const chips = Y.chips.length ? `<div class="yd-chips">${Y.chips.map((c) => `<span class="yd-chip">${svg(CHECK, 14, 2.6)}${esc(c)}</span>`).join('')}</div>` : '';
188
+ // on Good morning (o.gm) the chips are one quiet line, "Done for you: …" (G1)
189
+ const chips = o.gm && Y.did ? `<div class="gm-did">${svg(CHECK, 14, 2.6)}${esc(Y.did)}</div>` : Y.chips.length ? `<div class="yd-chips">${Y.chips.map((c) => `<span class="yd-chip">${svg(CHECK, 14, 2.6)}${esc(c)}</span>`).join('')}</div>` : '';
183
190
  const miss = Y.misses.map((m) => `<div class="yd-miss" data-id="${esc(m.id)}"><div class="yd-mt"><b>My miss: ${esc(m.title)}</b>${m.why ? `<em>${esc(m.why)}</em>` : ''}</div>${ok('retry', 'Run it again', m.id)}</div>`).join('');
184
191
  // the failed source under Claude's own miss: one card, no second Adler; its icon reads the source again (Settings › Connectors' ↻)
185
192
  const sync = Y.fail ? (Y.failLead ? `<div class="yd-sync">${alt('sync', 'Read ' + Y.fail.name + ' again', Y.fail.id, AGAIN)}</div>`
@@ -195,6 +202,23 @@ function callHtml(r) {
195
202
  const why = r.pick ? 'I’d ' + r.pick.charAt(0).toLowerCase() + r.pick.slice(1).replace(/\.?$/, '.') + ' It’s your call.' : r.when ? 'Goes out ' + r.when + '.' : r.sub || '';
196
203
  return `<div class="yd-card yd-call${r.state === 'blocked' ? ' am' : ''}" data-id="${esc(r.id)}"><div class="yd-mt"><b>${esc(r.name)}</b>${why ? `<em>${esc(why)}</em>` : ''}</div>${alt('sel', 'Open: ' + r.name, '', CHEV)}</div>`;
197
204
  }
205
+ /* one row of the ranked Your move (W3, WgW3Move): ✦, the sentence and what it rests on, its age, and its presses, icon only. The text opens what
206
+ * the row is about (the initiative's ✦, the program, Intelligence); ✓ does the one thing it says; the second icon picks, decides or drops. */
207
+ const ALT_ICON = { verdict: PEN, meeting: PEN, grill: XX, other: CHEV, kr: CHEV };
208
+ function moveRowHtml(r) {
209
+ if (r.kind === 'call') return callHtml(r.call);
210
+ const late = r.rank <= 2 && r.days != null && r.days > 0;
211
+ const yes = r.yes ? `<button type="button" class="yd-ok sm" data-yd="mv-yes" data-key="${esc(r.key)}" aria-label="${esc(r.yes + ': ' + r.title)}" title="${esc(r.yes)}">${svg(CHECK, 18, 2.6)}</button>` : '';
212
+ const alt = r.alt ? `<button type="button" class="yd-alt sm" data-yd="mv-alt" data-key="${esc(r.key)}" aria-label="${esc(r.alt + ': ' + r.title)}" title="${esc(r.alt)}">${svg(ALT_ICON[r.kind] || CHEV, 16, 2)}</button>` : '';
213
+ return `<div class="yd-mv" data-key="${esc(r.key)}"><span class="yd-mvs" aria-hidden="true">✦</span><button type="button" class="yd-mvt" data-yd="mv-open" data-key="${esc(r.key)}"><b>${esc(r.title)}</b>${r.sub ? `<em>${esc(r.sub)}</em>` : ''}</button>`
214
+ + `${r.age ? `<span class="yd-age${late ? ' late' : ''}">${esc(r.age)}</span>` : ''}<span class="yd-mva">${yes}${alt}</span></div>`;
215
+ }
216
+ /** Your move's fold from the ranked list: the rows, then "Older than two weeks · N" folded (never gone). null when nothing is there. */
217
+ export function moveSection(R) {
218
+ if (!R || (!R.rows.length && !R.older.length)) return null;
219
+ const older = R.older.length ? `<details class="yd-older"><summary>Older than two weeks · ${R.older.length}</summary>${R.older.map(moveRowHtml).join('')}</details>` : '';
220
+ return { id: 'move', title: 'Your move', count: R.rows.length + (R.older.length ? ' · ' + R.older.length + ' older' : ''), open: true, html: `<div class="yd-mvl">${R.rows.map(moveRowHtml).join('')}${older}</div>` };
221
+ }
198
222
  /* one row that can wait: the list's own row, so a press opens what the list row opens; no "late" words (here and now) */
199
223
  function waitRow(r) {
200
224
  const mark = r.check ? '<span class="hv-ck" role="img" aria-label="Your task"></span>' : '<span class="hv-d" role="img" aria-label="Waits on you"></span>';
@@ -221,6 +245,7 @@ export function nightHtml(Y) {
221
245
 
222
246
  /* one New for you item (FmA2Day): the blue dot, who, the words and where it came from; a press opens it in the Viewer (cozy-news.js) */
223
247
  const MARK2 = 'M2 12.5l4.5 4.5L15 8M9 15l2 2L22 7';
248
+ const listOf = (a) => (a.length < 2 ? a.join('') : a.slice(0, -1).join(', ') + ' and ' + a[a.length - 1]);
224
249
  const SPARK = 'M12 3l1.8 4.7L18.5 9.5l-4.7 1.8L12 16l-1.8-4.7L5.5 9.5l4.7-1.8z';
225
250
  export const NEWS_SHOWN = 3;
226
251
  function newsRow(it, now) {
@@ -241,11 +266,15 @@ function newsRow(it, now) {
241
266
  export function newsSection(items, now = Date.now(), digests = 0) {
242
267
  items = items || [];
243
268
  if (!items.length && !(digests > 0)) return null;
244
- const more = items.length - NEWS_SHOWN;
269
+ // the tasks given to the PM are one row (W3: "32 tasks given to you"), which opens the whole list in the Viewer; one task stays itself
270
+ const tasks = items.filter((x) => x.kind === 'task'), froms = [...new Set(tasks.map((x) => x.from).filter(Boolean))], rest = tasks.length > 1 ? items.filter((x) => x.kind !== 'task') : items;
271
+ const trow = tasks.length > 1 ? `<button type="button" class="yd-nw" data-yd="news-all"><i class="yd-nd" aria-hidden="true"></i><span class="yd-av sp" aria-hidden="true">${tasks.length}</span><span class="yd-mt"><b>${esc(tasks.length + ' tasks given to you')}</b><em>${esc(froms.length ? 'From ' + listOf(froms.slice(0, 2)) + (froms.length > 2 ? ' and ' + (froms.length - 2) + ' more' : '') : 'Open the list')}</em></span></button>` : '';
272
+ const shown = Math.max(0, NEWS_SHOWN - (trow ? 1 : 0));
273
+ const more = rest.length - shown;
245
274
  const dig = digests > 0 ? `<button type="button" class="yd-more" data-yd="news-all">${digests === 1 ? 'Today’s digest' : 'Today’s ' + digests + ' digests'}</button>` : '';
246
275
  return { id: 'news', title: 'New for you', count: items.length + (digests > 0 ? digests : 0), open: true,
247
276
  tail: items.length ? `<button type="button" class="yd-all" data-yd="seen" aria-label="Mark all ${items.length} seen" title="Mark all seen">${svg(MARK2, 20, 1.9)}</button>` : '',
248
- html: `<div class="yd-news">${items.slice(0, NEWS_SHOWN).map((x) => newsRow(x, now)).join('')}${more > 0 ? `<button type="button" class="yd-more" data-yd="news-all">${more} more</button>` : ''}${dig}</div>` };
277
+ html: `<div class="yd-news">${trow}${rest.slice(0, shown).map((x) => newsRow(x, now)).join('')}${more > 0 ? `<button type="button" class="yd-more" data-yd="news-all">${more} more</button>` : ''}${dig}</div>` };
249
278
  }
250
279
  /** The morning brief's digests of today (GET /api/briefing-premium model.today), which New for you's list shows. */
251
280
  export function digestsOf(D) {
@@ -263,8 +292,11 @@ export function dayParts(Y, o = {}) {
263
292
  const sections = [], now = o.now || Date.now();
264
293
  // the tea figure only after a real day (something done, drafted or waiting): nothing at all stays silent (zero is silence)
265
294
  const real = Y.done > 0 || Y.chips.length || Y.drafts.length || Y.wait.length;
266
- const big = Y.calls.length || Y.mood === 'concerned' ? null : Y.mood === 'delighted' ? 'win' : real ? 'tea' : null;
267
- if (Y.calls.length) sections.push({ id: 'move', title: 'Your move', count: Y.calls.length, open: true, html: Y.calls.map(callHtml).join('') });
295
+ const ranked = Y.rank && (Y.rank.rows.length || Y.rank.older.length);
296
+ const big = Y.calls.length || ranked || Y.mood === 'concerned' ? null : Y.mood === 'delighted' ? 'win' : real ? 'tea' : null;
297
+ const mv = Y.rank ? moveSection(Y.rank) : null;
298
+ if (mv) sections.push(mv);
299
+ else if (Y.calls.length) sections.push({ id: 'move', title: 'Your move', count: Y.calls.length, open: true, html: Y.calls.map(callHtml).join('') });
268
300
  else if (big) sections.push({ id: 'move', title: 'Your move', open: true, html: bigHtml(big) });
269
301
  { const nw = newsSection(Y.news || [], o.now, Y.digests || 0); if (nw) sections.push(nw); }
270
302
  if (Y.drafts.length) sections.push({ id: 'drafts', title: 'Drafted for you', count: Y.drafts.length, html: `<div class="hv-card">${Y.drafts.map((d, i) => draftRow(d, i)).join('')}</div>` });
@@ -278,6 +310,26 @@ export function dayParts(Y, o = {}) {
278
310
  return { open: openHtml(Y, { big: !!big, scene }), sections, night };
279
311
  }
280
312
 
313
+ /** Good morning (nyt-ui GM2, pm/good-morning.js): the page replaces the top of Your day. G: gmModel's answer. The opening card becomes its
314
+ * lead, Your move its top 3, New for you its Since yesterday; what stays folds under it: the rest of Your move ("N more in Your move"),
315
+ * Drafted for you, Can wait, Now (a phone's moment card) and This week. Nothing is dropped. → { gm, sections, night } */
316
+ export function gmParts(Y, G, o = {}) {
317
+ const now = o.now || Date.now(), sections = [];
318
+ const rest = Y.rank ? Y.rank.rows.filter((r) => !G.move.some((m) => m.key === r.key) && !(G.call && r.kind === 'call' && r.call && r.call.id === G.call.id)) : [];
319
+ if (G.state !== 'evening' && (rest.length || (Y.rank && Y.rank.older.length))) {
320
+ const mv = moveSection({ rows: rest, older: Y.rank.older });
321
+ if (mv) sections.push({ ...mv, id: 'more', title: 'More in Your move', open: false });
322
+ }
323
+ if (Y.drafts.length) sections.push({ id: 'drafts', title: 'Drafted for you', count: Y.drafts.length, html: `<div class="hv-card">${Y.drafts.map((d, i) => draftRow(d, i)).join('')}</div>` });
324
+ if (Y.wait.length) sections.push({ id: 'wait', title: 'Can wait', count: Y.wait.length, html: `<div class="hv-card hd-move">${Y.wait.map(waitRow).join('')}</div>` });
325
+ if (o.moment) sections.push({ id: 'now', title: 'Now', open: true, html: '<div class="hd-now"></div>' });
326
+ sections.push({ id: 'week', title: 'This week', count: Y.week.sub, html: `<div class="hv-card"><div class="hm-r hv-r two" data-id="review"><button type="button" class="hm-rm" data-hm-review aria-label="Weekly review: ${esc(Y.week.sub)}. Open"><span class="hv-mk">${svg(CAL, 16)}</span><span class="hm-rt"><b>Weekly review</b><em>${esc(Y.week.sub)}</em></span><span class="hv-cv">${svg(CHEV, 16)}</span></button></div></div>` });
327
+ // one Adler per screen: the morning scene on the day's first open before noon, else his face on the lead
328
+ const scene = !!o.first && Y.mood !== 'concerned' && G.state === 'morning' && new Date(now).getHours() < 12;
329
+ const night = Y.mood !== 'concerned' && asleep(now) ? nightHtml(Y) : '';
330
+ return { gm: o.html(G, openHtml, { scene }), sections, night };
331
+ }
332
+
281
333
  /** Draws Your day into the Viewer's body: the opening card, then the folds; moment(el) puts the phone's moment card in Now. */
282
334
  export { firstOpenToday };
283
335
  export function drawDay(body, P, VW, moment) {
@@ -286,6 +338,6 @@ export function drawDay(body, P, VW, moment) {
286
338
  const b = body.querySelector('[data-yd="awake"]'); if (b) b.addEventListener('click', (e) => { e.stopPropagation(); drawDay(body, { ...P, night: '' }, VW, moment); });
287
339
  return;
288
340
  }
289
- body.innerHTML = `<div class="yd">${P.open}${VW.czSections(P.sections, { key: 'day' })}</div>`;
341
+ body.innerHTML = P.gm ? `<div class="yd gm-wrap">${P.gm}${VW.czSections(P.sections, { key: 'day' })}</div>` : `<div class="yd">${P.open}${VW.czSections(P.sections, { key: 'day' })}</div>`;
290
342
  const now = body.querySelector('.hd-now'); if (now && moment) moment(now);
291
343
  }
@@ -109,7 +109,7 @@ const PILL = { draft: ['Draft', ''], ok: ['Up to date', 'ok'], live: ['Live', 'o
109
109
  export function docPhases(M) {
110
110
  const D = (M || {}).docs || {};
111
111
  if (Array.isArray(D.phases) && D.phases.length) return { phases: D.phases, cur: D.cur || D.phases[0] };
112
- return { phases: PHASES, cur: phaseOf(M && M.acts && M.acts.stage) };
112
+ return { phases: PHASES, cur: phaseOf((M && M.oneStage && M.oneStage.step) || (M && M.acts && M.acts.stage)) }; // W1: the one stage's step
113
113
  }
114
114
  export function docRows(M) {
115
115
  const D = (M || {}).docs || {}, { phases } = docPhases(M);
@@ -99,5 +99,12 @@ export function stageCardHtml(h, o = {}) {
99
99
  return `<section class="hm-card hm-pcard hm-sc" aria-label="${esc(o.title || '')}">`
100
100
  + `<div class="hm-box hm-scb ${esc(turn)}"><div class="hm-sch"><b>${esc(h.cur || 'No stage')}</b>${R.done.length + R.open.length ? `<span class="of">${esc(h.doneOf)}</span>` : ''}`
101
101
  + (o.wf || '') + (clock ? `<span class="hm-sct${h.clock.over ? ' over' : ''}">${CLOCK}${esc(clock)}</span>` : '') + '</div>'
102
+ + (h.source ? `<p class="hm-scsrc">${esc(h.source)}</p>` : '') + gateHtml(h.gate)
102
103
  + bars + none + list + '</div></section>';
103
104
  }
105
+ /** W1: the gate line under the stage ("Gate to Build · PRD ✓ · rubric ✓ · contract not approved"): the spec ladder, which no longer sets the
106
+ * stage. '' when the engine gave no gate check. */
107
+ export function gateHtml(g) {
108
+ if (!g || !Array.isArray(g.parts) || !g.parts.length) return '';
109
+ return `<p class="hm-gate"><span class="lbl">${esc(g.label)}</span>${g.parts.map((p) => `<span class="g${p.ok ? '' : ' no'}"${p.ok ? ` aria-label="${esc(p.text + ' done')}"` : ''}${p.note ? ` title="${esc(p.note)}"` : ''}>${esc(p.text)}${p.ok ? '<svg class="cz-ck" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12l4.5 4.5L19 7"/></svg>' : ''}</span>`).join('')}</p>`;
110
+ }
@@ -464,7 +464,7 @@ export function allHtml(v, o, isOpen) {
464
464
  else if (D <= 1100) { if (m % 3 === 0) ticks.push({ n, t: 'Q' + (m / 3 + 1) + yr, b: m === 0 }); }
465
465
  else if (m === 0) ticks.push({ n, t: String(d.getUTCFullYear()), b: true });
466
466
  }
467
- const stageTone = (sec) => (sec.status === 'done' || sec.kind === 'past' ? 'dn' : sec.status === 'now' ? 'now' : '');
467
+ const stageTone = (sec) => ((sec.status === 'done' || sec.kind === 'past') && !(sec.openN > 0) ? 'dn' : sec.status === 'now' ? 'now' : ''); // W1: done only when every task is
468
468
  const msMark = (x) => { const b = dn(x.end); if (b == null) return '';
469
469
  return `<i class="iva-md${x.done ? ' dn' : x.late ? ' lt' : ''}" data-tlx="${pc(b)}" role="img" aria-label="${esc('Milestone ' + x.title + ', ' + md(b))}" title="${esc(x.title + ' · ' + md(b))}"></i>`; };
470
470
  const name = (sec) => sec.label || sec.name || 'Not in a stage';
@@ -475,7 +475,7 @@ export function allHtml(v, o, isOpen) {
475
475
  const has = rs.length || dn_.length || jr.length || tk.length;
476
476
  const mk = o.mark(sec);
477
477
  let mark = '';
478
- if (sp && sec.ms) mark = `<i class="iva-md st${sec.status === 'done' ? ' dn' : ''}" data-tlx="${pc(sp[1])}"></i>`;
478
+ if (sp && sec.ms) mark = `<i class="iva-md st${sec.status === 'done' && !(sec.openN > 0) ? ' dn' : ''}" data-tlx="${pc(sp[1])}"></i>`;
479
479
  else if (sp) mark = `<i class="iva-b ${stageTone(sec)}" data-tlx="${pc(sp[0])}" data-tlw="${pw(sp[0], sp[1])}"></i>` + rowsOf(sec).filter((x) => x.ms && !x.wont).map(msMark).join('');
480
480
  const when = sp ? (sec.ms ? md(sp[1]) : rng(sp[0], sp[1])) : '';
481
481
  const say = name(sec) + (sec.count ? ', ' + sec.count : '') + (sp ? ', ' + rng(sp[0], sp[1]) : ', no dates yet') + (has ? '. ' + (op ? 'Hide' : 'Show') + ' its tasks' : '');
@@ -36,6 +36,8 @@
36
36
  * 19 a big section shows the 3 most urgent first, then folds the rest by the next WBS level, each with its count; done folds last. */
37
37
  import { ticketGroups } from './tk-model.js'; // the drafted Jira tickets by epic (kit 2, T1): By epic's groups
38
38
 
39
+ import { stageSource } from './stage-chip.js'; // W1: what set the one stage, and when it was read
40
+ const endWord = (r) => (r && r.oneStage && ['shipped', 'on-hold', 'cancelled'].includes(r.oneStage.stage) ? r.oneStage.label : '');
39
41
  import { lineOf } from './stage-line.js'; // the stage card's one line (batch 11 item 21)
40
42
 
41
43
  const MON = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
@@ -244,7 +246,9 @@ export function iniView(r, ctx = {}) {
244
246
  };
245
247
  const sTotal = jira ? tk.length : curRows.length, sDone = jira ? tk.filter((k) => k.status === 'Done').length : curRows.filter((x) => x.closed).length;
246
248
  const stage = {
247
- n: cur >= 0 ? cur + 1 : null, of: list.length, name: st ? st.name : (t.label || r.stage || 'No stage on record'), ms: !!(st && st.ms), key: st ? st.key : '',
249
+ n: cur >= 0 ? cur + 1 : null, of: list.length, name: (endWord(r) || (st ? st.name : (t.label || r.stage || 'No stage on record'))), ms: !!(st && st.ms), key: st ? st.key : '',
250
+ // W1: what set the stage and when it was read, and the gate line (the spec ladder) the page model sends
251
+ source: stageSource(r), gate: ctx.gate || null,
248
252
  doneOf, nDone: sDone, nTotal: sTotal, nLate: curRows.filter((x) => !x.closed && x.late).length, gates, day: days != null ? 'day ' + days + (usual != null ? ' of ~' + usual : '') : '', dayOver: days != null && usual != null && days > usual,
249
253
  clock: days != null && !line.chip ? (usual != null ? days + ' of ~' + usual + ' days' : plural(days, 'day', 'days')) : '', line, ready,
250
254
  ladder: list.map((s) => ({ name: s.name, ms: s.ms, cls: s.status })), dates, next: nx,
@@ -265,8 +269,8 @@ export function iniView(r, ctx = {}) {
265
269
  else if (s.status === 'done' || s.status === 'skip') sub = s.status === 'skip' ? 'Skipped' + (s.why ? ': ' + s.why : '') : span(ran[i].from, ran[i].to, today);
266
270
  else { const p = planOf(rs); if (p) { sub = 'Plan ' + dateY(p, today); if (dayOf(p) < today && op.length) { sub += ', missed'; subWarn = true; } } }
267
271
  const h = headOf(rs); if (h) sub = [sub, h].filter(Boolean).join(' · ');
268
- return { kind: 'stage', key: s.key, n: i + 1, name: s.name, ms: s.ms, status: s.status, sub, subWarn, count: countOf(op.length, lt.length, dn), warn: lt.length > 0,
269
- open: s.status === 'now' || lt.length > 0, ...bodyOf(rs, archOf([s.key])), jira: null, why, ...(s.status === 'skip' ? { skipWhy: s.why || '' } : {}) };
272
+ return { kind: 'stage', key: s.key, n: i + 1, name: s.name, ms: s.ms, status: s.status, sub, subWarn, count: countOf(op.length, lt.length, dn), warn: lt.length > 0, openN: op.length,
273
+ open: s.status === 'now' || lt.length > 0 || (s.status === 'done' && op.length > 0), ...bodyOf(rs, archOf([s.key])), jira: null, why, ...(s.status === 'skip' ? { skipWhy: s.why || '' } : {}) };
270
274
  };
271
275
  const anyTasks = rows.length > 0 || jiraIn;
272
276
  // one row per stage, in workflow order (batch 11 item 5, Wawan: "one row/group per stage. Do not combine"): past and later stages are no
@@ -281,6 +285,7 @@ export function iniView(r, ctx = {}) {
281
285
  x.count = countOf(opn.length + x.top.length + x.groups.reduce((n, g) => n + g.n, 0), x.top.filter((y) => y.late).length, 0);
282
286
  x.sub = x.sub + (x.sub ? ' · ' : '') + 'Jira tickets, by epic';
283
287
  if (opn.length) x.open = true; // passed but still open in Jira (Deliver): the tickets stay in view, not folded under a done stage
288
+ x.openN = (x.openN || 0) + opn.length;
284
289
  }
285
290
  sections.push(x);
286
291
  });
@@ -350,6 +355,8 @@ export function linkedOf(r) {
350
355
  * group it again; each group is a section of kind 'group' (or 'epic'), so the three views draw it with no code of their own. Links,
351
356
  * the ◇ and the trash belong to the task, so they come with it. Jira's own tickets (Build's section) have no owner or epic here: in any
352
357
  * grouping but Stage they sit in one "In Jira" section. */
358
+ /** W1: a task group shows ✓ only when every task in it is done: a passed stage with open tasks (Plan 1 open, Build 21 open) reads open. */
359
+ export const ticks = (sec) => !!sec && (sec.status === 'done' || sec.kind === 'past') && !(sec.openN > 0);
353
360
  export const GROUP_BY = { stage: 'Stage', epic: 'Epic', mine: 'Your groups', owner: 'Owner', status: 'Status' };
354
361
  /** An old layout's 'stage' (from before tasks were the WBS) means what Stage means again; 'none' is no longer offered and reads as Stage. */
355
362
  export function groupBy(L, T) {
@@ -1301,3 +1301,12 @@ body.iv-dragging { user-select: none; -webkit-user-select: none; cursor: grabbin
1301
1301
  .tkc.ivq .ive-m .ck { color: var(--cz-ink2); }
1302
1302
  /* the open row has its own status chip: the box's caret stays out */
1303
1303
  .iv-tr.iv-ed .iv-car { display: none !important; }
1304
+ /* the grill findings on the header (workspace grill W3, WgW3Move): the two best, each with its angle and day, and one ✦ for all of them */
1305
+ .iv-gf { position: relative; display: flex; flex-direction: column; gap: 6px; background: var(--cz-card); border: 1px solid var(--cz-hair); border-radius: 16px; padding: 10px 52px 10px 12px; margin-top: 8px; }
1306
+ .iv-gfr { margin: 0; display: flex; gap: 8px; align-items: flex-start; font-size: var(--fs-row); line-height: var(--lh-row); color: var(--cz-ink); }
1307
+ .iv-gfr b { font-weight: var(--fw-strong); }
1308
+ .iv-gfd { flex: none; width: 8px; height: 8px; border-radius: 4px; margin-top: 6px; background: var(--cz-risk); }
1309
+ .iv-gfd.late { background: var(--cz-late); }
1310
+ .iv-gfm { color: var(--cz-ink3); font-size: var(--fs-label); }
1311
+ .iv-gfo { position: absolute; top: 6px; right: 6px; width: 44px; height: 44px; border-radius: 22px; border: 0; background: var(--cz-bluebg); color: var(--cz-blue); display: inline-flex; align-items: center; justify-content: center; cursor: pointer; padding: 0; }
1312
+ .iv-gfo:focus-visible { outline: 2px solid var(--cz-blue); outline-offset: 2px; }
@@ -44,7 +44,9 @@ import * as MI from './mission.js'; // the initiative as a mission, the stage
44
44
  import * as AD from './cozy-adler.js'; // Adler on the card: his face for the ask, his full body for a real win
45
45
  import * as MD from './metric-drafts.js'; // the success measure Claude drafts from the PRD (batch 15 item 1, canvas Iq9Initiative)
46
46
  import * as AB from './ini-about.js'; // About: the description and the success measure, folded to one line (initiative-mission-control R6)
47
+ import { headerFlags } from './grill-words.js'; // the grill findings on the header (workspace grill W3)
47
48
  import * as ED from './ini-edit.js'; // a task changed where it sits in the List: open in place, ⋯, + per stage, drag (batch 18 item 16)
49
+ import { ticks } from './ini-view-model.js'; // W1: a task group ticks only when every task in it is done
48
50
 
49
51
  const CSS = new URL('./ini-view.css', import.meta.url).href;
50
52
  if (typeof document !== 'undefined' && document.head && !document.querySelector(`link[href="${CSS}"]`)) {
@@ -133,6 +135,12 @@ const pipsOf = (st, cls) => {
133
135
  const d = Math.min(n, st.nDone || 0), l = Math.min(n - d, st.nLate || 0);
134
136
  return `<span class="iv-pips${cls ? ' ' + cls : ''}" aria-hidden="true">${Array.from({ length: n }, (_, i) => `<i class="${i < d ? 'd' : i < d + l ? 'l' : 'o'}"></i>`).join('')}</span>`;
135
137
  };
138
+ /** W1: under the stage, what set it ("Jira IDPF-2815 · Design Discovery · read 07:16") and the gate line ("Gate to Build · PRD ✓ · contract not
139
+ * approved"): the spec ladder no longer sets the stage. '' when neither is known. */
140
+ function srcGateHtml(st) {
141
+ const g = st.gate && Array.isArray(st.gate.parts) && st.gate.parts.length ? `<p class="hm-gate iv-gate"><span class="lbl">${esc(st.gate.label)}</span>${st.gate.parts.map((x) => `<span class="g${x.ok ? '' : ' no'}"${x.ok ? ` aria-label="${esc(x.text + ' done')}"` : ''}${x.note ? ` title="${esc(x.note)}"` : ''}>${esc(x.text)}${x.ok ? '<svg class="cz-ck" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12l4.5 4.5L19 7"/></svg>' : ''}</span>`).join('')}</p>` : '';
142
+ return (st.source ? `<p class="hm-scsrc iv-src">${esc(st.source)}</p>` : '') + g;
143
+ }
136
144
  /** The telemetry card, and with it the banner it folds into (R2, R3). */
137
145
  function telHtml(v, o, pri, mc) {
138
146
  const st = v.stage, L = st.line, id = v.id || v.key || '', M = MI.missionOf(L), D = st.dates;
@@ -145,11 +153,11 @@ function telHtml(v, o, pri, mc) {
145
153
  // the banner (hidden): the stage, the dates, the days left, the count, the measure, the next move
146
154
  const left = D && D.left ? `<span class="iv-chp2${D.over ? ' late' : ''}">${esc(D.left)}</span>` : L && L.chip ? `<span class="iv-chp2">${esc(L.chip.text)}</span>` : '';
147
155
  const count = st.nTotal ? `<span class="iv-chp2${st.nLate ? ' late' : ''}">${pipsOf(st, 'sm')}${esc(st.doneOf || st.nDone + ' of ' + st.nTotal + ' done')}</span>` : '';
148
- const bn = mc ? '' : `<div class="iv-bn"><div class="iv-bt"><span class="iv-shn">${tileOf(st)}<b>${esc(st.name)}</b></span>${dt ? `<span class="s">${esc(dt)}</span>` : ''}</div><div class="iv-chips">${left}${count}${AB.chipHtml(v, o)}</div>${st.before ? `<p class="iv-bf s">${esc(st.before)}</p>` : ''}${nextHtml(st, o, pri)}</div>`;
156
+ const bn = mc ? '' : `<div class="iv-bn"><div class="iv-bt"><span class="iv-shn">${tileOf(st)}<b>${esc(st.name)}</b></span>${dt ? `<span class="s">${esc(dt)}</span>` : ''}</div><div class="iv-chips">${left}${count}${AB.chipHtml(v, o)}</div>${srcGateHtml(st)}${st.before ? `<p class="iv-bf s">${esc(st.before)}</p>` : ''}${nextHtml(st, o, pri)}</div>`;
149
157
  const tg = `<button type="button" class="iv-mctg" data-iv-mc="mc" aria-expanded="${!!mc}" aria-label="${mc ? 'Hide the mission' : 'Show the mission'}" title="${mc ? 'Hide' : 'Show'}">${SVG(CHEV, 16, 2)}</button>`;
150
158
  return `<div class="iv-tel" data-iv-tel aria-label="${esc(st.aria || 'Stage · ' + st.name)}">`
151
159
  + (mc ? `<div class="iv-tj" data-iv-tj="${esc(id)}">${svg}</div>`
152
- + `<div class="iv-tel-ct"><div class="iv-tel-t"><span class="iv-shn">${tileOf(st)}${nameOf(st, o)}</span>${holdChip(L, o)}${set}${dates}</div>${readoutsHtml(st)}</div>` : bn)
160
+ + `<div class="iv-tel-ct"><div class="iv-tel-t"><span class="iv-shn">${tileOf(st)}${nameOf(st, o)}</span>${holdChip(L, o)}${set}${dates}</div>${srcGateHtml(st)}${readoutsHtml(st)}</div>` : bn)
153
161
  + `${tg}<span class="iv-mi-tip" role="tooltip" hidden></span></div>`;
154
162
  }
155
163
  /** The way to finish this stage (R5): the count with its marks, the next move, the open milestone tasks, and the people. */
@@ -171,6 +179,12 @@ function gatesHtml(v, o, pri, open) {
171
179
  return `<section class="iv-gx"${ph ? ' data-ph' : ''} aria-label="${esc(title)}"><button type="button" class="iv-gx-t" data-iv-mc="gx" aria-expanded="${!!open}"><span class="chev" aria-hidden="true">${SVG(CHEV, 16, 2)}</span><span class="tx dk"><b>${title}</b>${sub ? `<span class="s">${esc(sub)}</span>` : ''}</span>${ph}</button>`
172
180
  + `${st.nTotal ? `<span class="iv-gx-c">${pipsOf(st)}<small>${esc(st.doneOf || st.nDone + ' of ' + st.nTotal + ' done')}</small></span>` : ''}<div class="iv-gx-a">${nextHtml(st, o, pri)}</div>${open ? rows : ''}<div class="iv-gx-ft">${metaHtml(s, o)}</div></section>`;
173
181
  }
182
+ /* the grill findings on the header (workspace grill W3, WgW3Move): pm/grill-words.js headerFlags; not behind a pill, zero is silence */
183
+ function flagsHtml(v) {
184
+ const F = headerFlags(v && v.props); if (!F) return '';
185
+ return `<div class="iv-gf" role="group" aria-label="What Claude found when it grilled it">${F.rows.map((r) => `<p class="iv-gfr"><i class="iv-gfd${r.hot ? ' late' : ''}" aria-hidden="true"></i><span><b>${esc(r.title)}</b> <span class="iv-gfm">${esc([r.label, r.day].filter(Boolean).join(' · '))}</span></span></p>`).join('')}`
186
+ + `<button type="button" class="iv-gfo" data-iv-gf aria-label="${esc('Open the ' + plural(F.n, 'finding', 'findings'))}" title="Open the findings">${SVG('M12 3l1.8 4.7L18.5 9.5l-4.7 1.8L12 16l-1.8-4.7L5.5 9.5l4.7-1.8z', 16, 2)}</button></div>`;
187
+ }
174
188
  function ctrlHtml(v, o = {}) {
175
189
  const s = v.summary, st = v.stage, id = v.id || v.key || '';
176
190
  const on = AD.prefs()['adler.amount'] !== 'off';
@@ -187,7 +201,7 @@ function ctrlHtml(v, o = {}) {
187
201
  top = `<div class="iv-mc-top adl"><div class="iv-ask">${AD.adler('needs-you', 'm', { label: false })}<p class="iv-why">${esc(s.say || s.text || s.primary.label)}</p>${n}`
188
202
  + `<button type="button" class="iv-go" data-iv-primary="${esc(s.primary.act)}" aria-label="${esc(s.primary.label)}" title="${esc(s.primary.label)}">${SVG(P.go, 16, 2.2)}</button></div></div>`;
189
203
  } else if (claude) top = `<div class="iv-mc-top adl"><div class="iv-ask cl">${AD.adler('thinking', 'm', { label: false })}<p class="iv-why">${esc(s.claude)}</p></div></div>`;
190
- return `<section class="iv-sum iv-mc${S.mc ? '' : ' shut-mc'}${S.gx ? '' : ' shut-gx'}${S.ab ? '' : ' shut-ab'}${top ? ' has-top' : ''}" aria-label="Mission">${top}`
204
+ return `<section class="iv-sum iv-mc${S.mc ? '' : ' shut-mc'}${S.gx ? '' : ' shut-gx'}${S.ab ? '' : ' shut-ab'}${top ? ' has-top' : ''}" aria-label="Mission">${top}${flagsHtml(v)}`
191
205
  + `<div class="iv-mc-row">${telHtml(v, o, pri, S.mc)}${S.mc ? gatesHtml(v, o, pri, S.gx) : ''}</div>${S.mc ? AB.aboutHtml(v, o, S.ab) : ''}</section>`;
192
206
  }
193
207
  /** The date of an ISO day, as the model says dates ("Oct 30"). */
@@ -292,10 +306,9 @@ function bodyHtml(v, sec, o) {
292
306
  return h;
293
307
  }
294
308
  function markOf(sec) {
295
- if (sec.kind === 'past') return `<span class="iv-no ok" aria-hidden="true">${SVG(P.check, 12, 3)}</span>`;
309
+ if (ticks(sec)) return `<span class="iv-no ok" aria-hidden="true">${SVG(P.check, 12, 3)}</span>`; // W1: only when every task is done
296
310
  if (sec.kind === 'later' || sec.kind === 'none') return '<span class="iv-no dash" aria-hidden="true"></span>';
297
311
  if (sec.status === 'skip') return `<span class="iv-no skip" aria-hidden="true">${SVG(P.skip, 12, 2)}</span>`;
298
- if (sec.status === 'done') return `<span class="iv-no ok" aria-hidden="true">${SVG(P.check, 12, 3)}</span>`;
299
312
  if (sec.kind === 'group' || sec.kind === 'epic') return ''; // a group of another grouping has no stage number (batch 11 item 20)
300
313
  if (sec.ms) return '<span class="iv-no ms" aria-hidden="true"></span>';
301
314
  return `<span class="iv-no${sec.status === 'now' ? ' now' : ''}" aria-hidden="true">${esc(sec.n)}</span>`;
@@ -470,6 +483,7 @@ export function wireView(body, h) {
470
483
  if (d.sm && H.sm) { e.preventDefault(); H.sm(d.sm, b); return; }
471
484
  if (d.smOpen && H.openIni) { e.preventDefault(); H.openIni(d.smOpen, b); return; }
472
485
  if (d.ivPrimary && H.primary) { e.preventDefault(); H.primary(d.ivPrimary, b); return; }
486
+ if ('ivGf' in d) { e.preventDefault(); const v = VIEWED.get(id); if (v) PP.open(v, body.__iv || H); return; }
473
487
  if ('ivAsks' in d) { e.preventDefault(); if (H.asks) H.asks(b); else if (H.primary) { const p = root.querySelector('[data-iv-primary]'); if (p) H.primary(p.dataset.ivPrimary, p); } return; }
474
488
  if ('ivWf' in d && H.wf) { e.preventDefault(); H.wf(b); return; }
475
489
  if ('ivPerson' in d && H.person) { e.preventDefault(); H.person(b); return; }
@@ -0,0 +1,155 @@
1
+ /* pm/move-rank.js — Your move is one ranked list (workspace grill W3, frame WgW3Move on the main canvas, In development; brief
2
+ * /mnt/project-files/workspace-grill/brief-W1-W4.md). Pure: Node imports it (home-day.test.js).
3
+ *
4
+ * rankMove(o) → { rows, older, n } every open ✦ of the products Home reads, in order of consequence, each with its age
5
+ * ageWord(days) → 'today' · '1 day' · '86 days'
6
+ *
7
+ * THE ORDER (frame WgW3Move, "The order"):
8
+ * 1 verdict an outcome check past its date, with Claude's drafted Keep or Iterate (GET /api/your-move verdicts); ✓ records it, ✎ opens
9
+ * the full verdict on Intelligence. With nothing read: "I could not read enough to judge" and ✎ only.
10
+ * 2 grill a grill finding (lib/grill.js: red team, premortem, …) on an initiative that shipped (Deliver or later, or done) or is late
11
+ * 3 meeting a risk or date change a meeting named, grouped per program (else per product): Claude's milestone changes of the program
12
+ * (GET /api/programs review.open) and the date and status proposals whose source is a meeting. One ✓ confirms all; ✎ picks.
13
+ * 4 ahead a grill finding on work still ahead
14
+ * 5 kr the first reading for a key result with none (GET /api/your-move kr); the press opens Intelligence
15
+ * 6 other the rest Claude proposes on an initiative (descriptions, outcomes, names, found elsewhere, other dates, links): one row per
16
+ * initiative that opens its ✦; folded under Older once its newest proposal is more than 14 days old, never gone
17
+ * The calls only the PM can make that are not ✦ (a session that asks, a decision, blocked work) follow rank 5, as before.
18
+ *
19
+ * ONE FINDING, ONE ROW. Findings of one angle on one initiative are one row with their parts (Inline login's three red-team ✦), and two
20
+ * proposals that say the same words on one initiative count once. Each row names its age: the day Claude filed it (a verdict: the days since
21
+ * its check date; a late task's dates: the days since the date it missed).
22
+ *
23
+ * o: { products: [{ ws, name, board, rows }] (Board rows: r.props from lib/proposals.js viewOf, r.track, r.status),
24
+ * programs: GET /api/programs' programs, move: GET /api/your-move answers by ws ({ [ws]: { verdicts, kr } }), calls: homeList's call
25
+ * rows, now }
26
+ * A row: { key, rank, kind, title, sub, tag, at, age, older, ws, board, ini, program, props: [{ ws, board, ini, ids, sigs }], ms: { program,
27
+ * keys } | null, verdict: { ini, board, verdict, text } | null, yes: label | '', alt: label | '', call } */
28
+ import { findingOf } from './grill-words.js';
29
+
30
+ const DAY = 864e5;
31
+ export const OLDER_DAYS = 14;
32
+ const MON = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
33
+ const plural = (n, a, b) => n + ' ' + (n === 1 ? a : b);
34
+ const clip = (s, n) => { const t = String(s || '').replace(/\s+/g, ' ').trim(); return t.length > n ? t.slice(0, n - 1).replace(/\s+\S*$/, '') + '…' : t; };
35
+ const list = (a) => (a.length < 2 ? a.join('') : a.slice(0, -1).join(', ') + ' and ' + a[a.length - 1]);
36
+ const tsOf = (v) => { const t = Date.parse(v || ''); return Number.isFinite(t) ? t : null; };
37
+ const dayOf = (iso) => { const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(iso || '')); return m ? new Date(+m[1], +m[2] - 1, +m[3]).getTime() : null; };
38
+ /** "10 Aug", or "10 Aug 2027" outside this year. */
39
+ const dm = (iso, now) => { const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(iso || '')); return m ? +m[3] + ' ' + MON[+m[2] - 1] + (+m[1] !== new Date(now).getFullYear() ? ' ' + m[1] : '') : ''; };
40
+ const daysSince = (t, now) => (t == null ? null : Math.max(0, Math.floor((now - t) / DAY)));
41
+ export const ageWord = (d) => (d == null ? '' : d === 0 ? 'today' : plural(d, 'day', 'days'));
42
+ const norm = (s) => String(s || '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
43
+ const MEET = /\b(meeting|notes?|calendar|sync|standup|stand-up|review|steering|weekly)\b/i;
44
+ const OTHER_WORD = { description: 'a description', outcome: 'an expected outcome', found: 'what it found elsewhere', dates: 'new dates', status: 'work that looks done', names: 'tidy names', links: 'task links', grill: 'grill findings' };
45
+
46
+ /** Shipped (Deliver or later, or done) or late: a grill finding on it is rank 2. */
47
+ const shippedOrLate = (r) => !!r && (r.late || r.status === 'late' || r.status === 'done' || (r.acts && r.acts.life === 'done') || (r.track && Number.isFinite(r.track.k) && r.track.k >= 5));
48
+ const live = (r) => !!r && r.id && !(r.acts && (r.acts.archived || r.acts.life === 'cancelled'));
49
+ /** Every open proposal of a row, the grill findings folded behind the ✦'s first three included. */
50
+ function propsOf(r) {
51
+ const out = [];
52
+ for (const g of (r.props && r.props.groups) || []) {
53
+ for (const p of g.props || []) out.push(p);
54
+ for (const p of g.hidden || []) out.push(p);
55
+ }
56
+ return out;
57
+ }
58
+ /** When a proposal was filed; a computed date proposal: the earliest date its tasks missed. */
59
+ function atOf(p) {
60
+ const t = tsOf(p.at); if (t != null) return t;
61
+ if (p.group === 'dates') { const ds = (p.items || []).map((x) => dayOf(x.from && x.from.due)).filter((x) => x != null); return ds.length ? Math.min(...ds) : null; }
62
+ return null;
63
+ }
64
+ const fromMeeting = (p) => (p.group === 'dates' || p.group === 'status') && (p.sources || []).some((s) => MEET.test(String((s && s.label) || '')));
65
+ const programOf = (programs, ws, ini) => (programs || []).find((p) => p && !p.archived && (p.members || []).some((m) => m.initiative === ini && (!m.ws || !ws || m.ws === ws))) || null;
66
+
67
+ export function rankMove(o = {}) {
68
+ const now = o.now || Date.now(), rows = [];
69
+ const add = (r) => { const d = daysSince(r.at, now); rows.push({ props: [], ms: null, verdict: null, yes: '', alt: '', ...r, age: r.age != null ? r.age : ageWord(d), days: d }); };
70
+ const meet = new Map(); // program id or 'p:' + ws → the meeting row being built
71
+ const meetRow = (key, title, extra) => { if (!meet.has(key)) meet.set(key, { key: 'mt:' + key, rank: 3, kind: 'meeting', title, parts: [], at: null, props: [], ms: null, ...extra }); return meet.get(key); };
72
+ for (const P of o.products || []) {
73
+ const ws = P.ws || '';
74
+ // 1 the verdicts past their check date (GET /api/your-move)
75
+ const M = (o.move && o.move[ws]) || null;
76
+ for (const v of (M && M.verdicts) || []) {
77
+ const d = v.draft;
78
+ add({ key: 'vd:' + ws + ':' + v.ini, rank: 1, kind: 'verdict', ws, board: v.board || P.board || '', ini: v.ini, at: dayOf(v.horizon), tag: 'Did it work?',
79
+ title: v.title + ': did it work?' + (d ? ' Verdict drafted: ' + d.verdict : ''),
80
+ sub: (d ? d.text + ' ' : 'I could not read enough to judge. ') + 'The check was due ' + dm(v.horizon, now) + '.',
81
+ verdict: d ? { ini: v.ini, board: v.board || P.board || '', verdict: d.verdict, text: d.text } : null, yes: d ? 'Record ' + d.verdict : '', alt: 'Decide on Intelligence' });
82
+ }
83
+ for (const r of (P.rows || []).filter(live)) {
84
+ const name = r.title || r.id, hot = shippedOrLate(r), prog = programOf(o.programs, ws, r.id);
85
+ const grill = new Map(), other = [], seen = new Set();
86
+ for (const p of propsOf(r)) {
87
+ const said = norm(p.group + ' ' + (p.title || '') + ' ' + (p.group === 'grill' ? '' : p.why || ''));
88
+ if (seen.has(said)) continue; // the same words twice on one initiative are one proposal
89
+ seen.add(said);
90
+ if (p.group === 'grill') { const f = findingOf(p); if (!grill.has(f.angle)) grill.set(f.angle, []); grill.get(f.angle).push({ p, f }); continue; }
91
+ if (fromMeeting(p)) {
92
+ const m = prog ? meetRow(prog.id, prog.title, { program: prog.id }) : meetRow('p:' + ws, P.name, { ws });
93
+ m.parts.push(name + ': ' + clip(p.title, 60));
94
+ let e = m.props.find((x) => x.ws === ws && x.ini === r.id); if (!e) m.props.push(e = { ws, board: P.board || '', ini: r.id, ids: [], sigs: [] });
95
+ e.ids.push(p.id); if (p.sig) e.sigs.push(p.sig);
96
+ const t = atOf(p); if (t != null && (m.at == null || t < m.at)) m.at = t;
97
+ continue;
98
+ }
99
+ other.push(p);
100
+ }
101
+ // 2 and 4: one row per angle on one initiative, its parts counted
102
+ for (const [, L] of grill) {
103
+ L.sort((a, b) => ((a.p.data && a.p.data.rank) || 5) - ((b.p.data && b.p.data.rank) || 5));
104
+ const top = L[0], ats = L.map((x) => atOf(x.p)).filter((t) => t != null);
105
+ add({ key: 'gr:' + ws + ':' + r.id + ':' + top.f.angle, rank: hot ? 2 : 4, kind: 'grill', ws, board: P.board || '', ini: r.id, tag: top.f.label, at: ats.length ? Math.min(...ats) : null,
106
+ title: name + ': ' + clip(top.f.title, 120),
107
+ sub: [top.f.label + (L.length > 1 ? ' · ' + plural(L.length - 1, 'more finding', 'more findings') + ' like it' : ''), top.f.next ? 'Next: ' + clip(top.f.next, 110) : clip(top.f.why, 110)].filter(Boolean).join(' · '),
108
+ props: [{ ws, board: P.board || '', ini: r.id, ids: L.map((x) => x.p.id), sigs: L.map((x) => x.p.sig).filter(Boolean) }], yes: top.f.next ? 'Add the next step as a task' : 'Keep', alt: 'Not this' });
109
+ }
110
+ // 6 the rest: one row per initiative, opening its ✦
111
+ if (other.length) {
112
+ const ats = other.map(atOf).filter((t) => t != null), newest = ats.length ? Math.max(...ats) : null;
113
+ const kinds = [...new Set(other.map((p) => p.group))].map((g) => OTHER_WORD[g] || g);
114
+ add({ key: 'ot:' + ws + ':' + r.id, rank: 6, kind: 'other', ws, board: P.board || '', ini: r.id, tag: '', at: ats.length ? Math.min(...ats) : null,
115
+ title: name + ': ' + plural(other.length, 'proposal', 'proposals'), sub: 'Claude proposes ' + list(kinds) + '.',
116
+ older: newest != null && daysSince(newest, now) > OLDER_DAYS, alt: 'Open what Claude proposes' });
117
+ }
118
+ }
119
+ // 5 the first key result with no reading
120
+ const K = M && M.kr;
121
+ // products that share one OKR file wait on the same key result: it is one row, the products named once
122
+ const kk = K && K.first ? norm(K.first.title + ' ' + K.first.need) : '';
123
+ const seenK = kk && rows.find((x) => x.kind === 'kr' && x.krKey === kk);
124
+ if (seenK) { seenK.shared.push(P.name || ws); seenK.sub = seenK.sub.replace(/ Shared by .*$/, '') + ' Shared by ' + list(seenK.shared) + '.'; }
125
+ else if (K && K.first) add({ key: 'kr:' + ws, krKey: kk, shared: [P.name || ws], rank: 5, kind: 'kr', ws, tag: 'Key result', at: null, age: '',
126
+ title: K.first.token + ' ' + K.first.title + ': it waits on ' + K.first.need,
127
+ sub: (K.waiting > 1 ? 'The first of ' + K.waiting + ' key results waiting on a source. ' : 'One key result waits on a source. ') + (K.read ? K.read + ' of ' + K.total + ' have a reading.' : 'None has a reading.'),
128
+ alt: 'Open Intelligence' });
129
+ }
130
+ // 3 the program's milestone changes join its meeting row (one row per program, never one per change)
131
+ for (const pr of (o.programs || []).filter((p) => p && !p.archived)) {
132
+ const L = (pr.review && Array.isArray(pr.review.open)) ? pr.review.open : [];
133
+ if (!L.length) continue;
134
+ const m = meetRow(pr.id, pr.title, { program: pr.id });
135
+ m.ms = { program: pr.id, keys: L.map((i) => i.key).filter(Boolean) };
136
+ // a change names its milestone by id; the program's milestones carry the name (as ms-suggest.js says it)
137
+ const msName = (i) => { const x = i.milestone ? (pr.milestones || []).find((y) => y && y.id === i.milestone) : null; return (x && x.name) || i.name || 'A milestone'; };
138
+ // honest words: a change read from a record, not a meeting, makes the row "to confirm"
139
+ if (L.some((i) => i.src && i.src.kind && i.src.kind !== 'meeting')) m.notMeet = true;
140
+ for (const i of L) m.parts.unshift(i.op === 'risk' ? msName(i) + ' at risk' : i.op === 'move' ? msName(i) + ' to ' + dm(i.date, now) : i.op === 'rename' && i.name ? msName({ milestone: i.milestone }) + ' renamed ' + i.name : msName(i));
141
+ const t = tsOf(pr.review.checked); if (t != null && (m.at == null || t < m.at)) m.at = t;
142
+ }
143
+ for (const m of meet.values()) {
144
+ const n = m.parts.length, msN = m.ms ? m.ms.keys.length : 0;
145
+ const said = [...new Set(m.parts)]; // three risks on one milestone are said once
146
+ add({ ...m, title: m.title + ': ' + (msN === n ? plural(n, 'milestone change', 'milestone changes') : plural(n, 'change', 'changes')) + (m.notMeet ? ' to confirm' : ' from your meetings'),
147
+ sub: clip(list(said.slice(0, 4)) + (said.length > 4 ? ' and ' + (said.length - 4) + ' more' : ''), 160) + (n > 1 ? '. One press confirms all ' + n + '; ✎ to pick.' : '.'),
148
+ yes: n > 1 ? 'Confirm all ' + n : 'Confirm', alt: 'Pick one by one' });
149
+ }
150
+ // the calls only the PM can make that are not ✦ (Claude's milestone changes are rank 3 now: said once)
151
+ for (const c of (o.calls || []).filter((x) => x && x.kind !== 'msreview')) add({ key: 'call:' + c.id, rank: 5.5, kind: 'call', call: c, title: c.name, sub: '', at: null, age: '' });
152
+ rows.sort((a, b) => a.rank - b.rank || (a.at == null) - (b.at == null) || (a.at || 0) - (b.at || 0) || a.key.localeCompare(b.key));
153
+ const older = rows.filter((r) => r.older), shown = rows.filter((r) => !r.older);
154
+ return { rows: shown, older, n: rows.length };
155
+ }
@@ -264,14 +264,16 @@ function needHtml(C, open) {
264
264
  const rows = [];
265
265
  if (C.asks) rows.push({ k: 'asks', dot: 'you m-info', n: C.asks, name: plural(C.asks, 'New ask', 'New asks'), say: C.asks + ' new ' + plural(C.asks, 'ask', 'asks') });
266
266
  for (const [k, tone, mo, name] of CHIP_F) if (C[k]) rows.push({ k, dot: tone + (mo ? ' ' + mo : ''), n: C[k], name, say: C[k] + ' ' + name.toLowerCase() });
267
- if (!rows.length) return '';
267
+ // workspace grill W2: the open initiatives nothing read says anything about. One ✦ reads Jira for them; they are never counted as on track
268
+ const nosig = C.none ? `<button type="button" class="bd-nr" id="chNoSig"><i class="cz-dot q"></i>✦ Read Jira now for the ${C.none} with no signal</button>` : '';
269
+ if (!rows.length && !nosig) return '';
268
270
  const on = rows.some((r) => F.chips.has(r.k));
269
271
  const item = (r) => (r.k === 'asks'
270
272
  ? `<button type="button" class="bd-nr" id="chAsks" aria-haspopup="dialog"><i class="cz-dot ${r.dot}"></i>${esc(r.name)}<span class="k">${r.n}</span></button>`
271
273
  : `<button type="button" class="bd-nr${F.chips.has(r.k) ? ' on' : ''}" data-f="${r.k}" aria-pressed="${F.chips.has(r.k)}"><i class="cz-dot ${r.dot}"></i>${esc(r.name)}<span class="k">${r.n}</span></button>`);
272
- return `<span class="bd-grpw2 bd-needw" id="bdNeedw"><button type="button" class="bd-chip bd-need${on ? ' on' : ''}" id="chNeed" aria-haspopup="true" aria-expanded="${open}" aria-controls="bdNeedMenu" aria-label="${esc(rows.map((r) => r.say).join(', '))}. Show them" title="${esc(rows.map((r) => r.say).join(' · '))}">`
273
- + rows.map((r) => `<span><i class="cz-dot ${r.dot}"></i>${r.n}</span>`).join('') + SV(I.chev) + '</button>'
274
- + `<span class="bd-grpm bd-needm" id="bdNeedMenu" role="group" aria-label="Needs a look"${open ? '' : ' hidden'}><span class="bd-mlbl">Needs a look</span>${rows.map(item).join('')}</span></span>`;
274
+ return `<span class="bd-grpw2 bd-needw" id="bdNeedw"><button type="button" class="bd-chip bd-need${on ? ' on' : ''}" id="chNeed" aria-haspopup="true" aria-expanded="${open}" aria-controls="bdNeedMenu" aria-label="${esc(rows.map((r) => r.say).concat(C.none ? [C.none + ' no signal'] : []).join(', '))}. Show them" title="${esc(rows.map((r) => r.say).join(' · '))}">`
275
+ + rows.map((r) => `<span><i class="cz-dot ${r.dot}"></i>${r.n}</span>`).join('') + (C.none ? `<span><i class="cz-dot q"></i>${C.none}</span>` : '') + SV(I.chev) + '</button>'
276
+ + `<span class="bd-grpm bd-needm" id="bdNeedMenu" role="group" aria-label="Needs a look"${open ? '' : ' hidden'}><span class="bd-mlbl">Needs a look</span>${rows.map(item).join('')}${nosig}</span></span>`;
275
277
  }
276
278
  const menuOpen = (id) => !!$('#' + id) && !$('#' + id).hidden;
277
279
  function drawChips() {
@@ -314,6 +316,7 @@ $('#bdChipset').addEventListener('click', (e) => {
314
316
  }
315
317
  const b = e.target.closest('button'); if (!b) return;
316
318
  if (b.id === 'chJira') refreshJira(b);
319
+ else if (b.id === 'chNoSig') { closeMenu('bdNeedMenu', 'chNeed'); refreshJira($('#chJira') || b); }
317
320
  else if (b.id === 'chSprint') Acts.open.sprint(b);
318
321
  else if (b.id === 'chAsks') { closeMenu('bdNeedMenu', 'chNeed'); Acts.open.asks($('#chNeed')); }
319
322
  else if (b.id === 'chShared') setTab('shared');