create-pmos 0.497.0 → 0.499.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 (40) hide show
  1. package/package.json +57 -26
  2. package/payload/AGENTS.md +1 -0
  3. package/payload/KIT-VERSION +1 -1
  4. package/payload/planning/evals/eval-calibration.md +1 -1
  5. package/payload/planning/okrs/health-budgets.md +1 -1
  6. package/payload/scripts/workflow-state.js +28 -0
  7. package/payload/state/discoveries.md +1 -1
  8. package/workspace/briefing-premium-test.js +2 -2
  9. package/workspace/changelog.json +56 -0
  10. package/workspace/guide/briefing-premium.md +4 -2
  11. package/workspace/guide/claude-proposes.md +1 -1
  12. package/workspace/guide/home.md +4 -4
  13. package/workspace/guide/launcher.md +7 -1
  14. package/workspace/lib/briefing-premium.js +38 -12
  15. package/workspace/lib/claude-readiness.js +29 -6
  16. package/workspace/lib/context-use.js +57 -15
  17. package/workspace/lib/engine/workflow-state.js +28 -0
  18. package/workspace/lib/git-identity-cli.js +67 -0
  19. package/workspace/lib/git-identity.js +93 -0
  20. package/workspace/lib/initiative-cli.js +16 -2
  21. package/workspace/lib/pmos-verbs.js +3 -0
  22. package/workspace/lib/routes-boards.js +8 -3
  23. package/workspace/lib/routes-onboard.js +1 -1
  24. package/workspace/lib/transcript-read.js +1 -1
  25. package/workspace/lib/vendor/scripts/workflow-state.js +28 -0
  26. package/workspace/package-lock.json +2 -2
  27. package/workspace/package.json +1 -1
  28. package/workspace/public/pm/bd-ini-panel.js +1 -1
  29. package/workspace/public/pm/cozy-onboard.js +15 -8
  30. package/workspace/public/pm/home-app.js +4 -2
  31. package/workspace/public/pm/home-chat.js +2 -2
  32. package/workspace/public/pm/home-ini.js +1 -1
  33. package/workspace/public/pm/home-need.js +1 -1
  34. package/workspace/public/pm/ini-edit.js +82 -19
  35. package/workspace/public/pm/ini-tasks.js +6 -1
  36. package/workspace/public/pm/ini-view-model.js +3 -1
  37. package/workspace/public/pm/ini-view.css +31 -7
  38. package/workspace/public/pm/premium-board.js +2 -2
  39. package/workspace/stage-card-tasks-test.js +4 -3
  40. package/workspace/test-lists/git-identity.json +5 -0
@@ -16,7 +16,7 @@
16
16
  * A loose session the host can place (host.suggestLink(target, firstText) → { id, title }) shows one chip: Link it (host.linkTo) or × (host.dismissLink).
17
17
  * Every write is the PM's press. mountChat(el, host) returns { open(target), refresh(), focus(), send(text) }.
18
18
  * target: { key?, sid?, title, scope: { text, icon, loose }, role: { name, initial }, intro?: { role, text, at, mood? (Adler as the avatar while the log is empty), pills: [{ label, primary, go }] },
19
- * ini?, rich, also?: { text, go } }
19
+ * ini?, rich, also?: { text, go, verb? ('Answer' when absent; 'Review' for Claude's ✦ proposal) } }
20
20
  * host: { esc, toast, pmGet, pmPost, roles(), seen(key), markSeen(key), onScope(btn), onLink(btn), onTo(btn), workItem(text, target),
21
21
  * viewerClosed(), reopenViewer(), head(target), touch(sid) (the PM opened, typed in or answered that session: the Live pill) } */
22
22
  import { mdHtml, userHtml, toolLine, webKind } from './chat-md.js';
@@ -400,7 +400,7 @@ export function mountChat(el, host) {
400
400
  const chipsOf = (items) => (chipsHtmlFn ? chipsHtmlFn(items) : '');
401
401
  function drawFoot(R) {
402
402
  const t = C.t, goal = C.th && C.th.goal;
403
- const also = t.also && !(C.conv && C.conv.ask) ? `<button type="button" class="hm-also" data-hm-also><i aria-hidden="true"></i><span><b>Also waiting on you:</b> ${esc(t.also.text)}</span><em>Answer</em></button>` : '';
403
+ const also = t.also && !(C.conv && C.conv.ask) ? `<button type="button" class="hm-also" data-hm-also><i aria-hidden="true"></i><span><b>Also waiting on you:</b> ${esc(t.also.text)}</span><em>${esc(t.also.verb || 'Answer')}</em></button>` : '';
404
404
  // while it works the PM talks as usual (Claude reads it after its tool call); a goal that stopped says how to go on (principle 8)
405
405
  const ph = C.conv && C.conv.ask ? 'Or tell ' + R.role.name + ' what to do instead' : goal && goal.state === 'stopped' && !running() ? 'Reply to go on, or /goal for a new goal' : t.placeholder || 'Message Claude';
406
406
  const sc = t.scope || { text: 'All work', icon: 'M5 21V4M5 4h12l-2 4 2 4H5' };
@@ -294,7 +294,7 @@ function wirePage(b, id) {
294
294
  const M = curM(); if (!M) return;
295
295
  inp.disabled = true;
296
296
  const D = opt ? opt : ADD_DATES || {};
297
- const r = await C.pmPost('/api/boards/tasks/add', { ...bw(M), initiative: id, title, ...(D.start ? { start: D.start } : {}), ...(D.due ? { due: D.due } : {}), ...(opt && opt.section != null ? { section: opt.section } : {}) });
297
+ const r = await C.pmPost('/api/boards/tasks/add', { ...bw(M), initiative: id, title, ...(D.start ? { start: D.start } : {}), ...(D.due ? { due: D.due } : {}), ...(opt && opt.section != null ? { section: opt.section } : {}), ...(opt && opt.milestone ? { milestone: true } : {}), ...(opt && opt.owner ? { owner: opt.owner } : {}) });
298
298
  inp.disabled = false;
299
299
  if (!r || r.ok === false) { say((r && r.error) || 'The task was not added.'); return; }
300
300
  const uid = r.item && r.item.uid;
@@ -74,7 +74,7 @@ function whyBlocked(step, rule, lane) {
74
74
  if (step === 'prototype') return 'This idea needs a prototype people can look at before a PRD says what to build.';
75
75
  if (step === 'contract') return 'The run needs its done-criteria agreed before it starts, so there is something to check it against.';
76
76
  if (step === 'anchor') return 'Every initiative names the goal it moves. Work with no goal is turned away.';
77
- if (step === 'route') return 'The initiative needs its lane, so the right rules run on it.';
77
+ if (step === 'route') return 'The initiative needs a workflow, so its stages can run.';
78
78
  return 'The ' + step + ' step is not done yet.';
79
79
  }
80
80
  const lineOf = (step, rule) => (/^O[12]$/.test(rule || '') ? 'Blocked: ' + (STEP_WORD[step] || 'the ' + step) + ' came after the build'
@@ -9,8 +9,11 @@
9
9
  * dashed Dates chip) and ⋯ right after the dates (StA8Desk)
10
10
  * ticketHtml(k, o) an unlinked Jira ticket's open row: the same, with Open the ticket in place of ⋯ (its place is Jira's)
11
11
  * plusHtml(sec, id, o) the + on an open stage's header: "New task in <stage>" (StA5Add)
12
- * addHtml(sec, id) the field at the top of that stage's list, with an optional Dates chip
13
- * press(b, d, root, H, e) a press this file owns → true: a title (opens the row), ⋯ and its menu, the +, the field's Dates chip
12
+ * addHtml(sec, id) the field at the top of that stage's list, and one chip row under it (batch 25 item 5, canvas
13
+ * TaskAddChips): Stage (the stage the + opened in; a picker of the initiative's workflow stages), Type (Task
14
+ * or Milestone: a milestone's box is ◇ and its Dates chip takes one date), Dates, Owner. Only what the task
15
+ * model stores: stage, milestone, start, due, owner
16
+ * press(b, d, root, H, e) a press this file owns → true: a title (opens the row), ⋯ and its menu, the +, the field's chips
14
17
  * wire(body) once per body: the name field (Enter or blur saves, Esc closes), the add field (Enter adds and stays open),
15
18
  * a press outside the open row closes it, and the drag (StA10Drag): a grip on hover on a desktop, a long press
16
19
  * on the title on a phone; the target stage lights up and a blue line shows where the task lands
@@ -29,14 +32,15 @@ const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&':
29
32
  const SVG = (d, w = 16, 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>`;
30
33
  const P = {
31
34
  more: 'M5 12h.01M12 12h.01M19 12h.01', plus: 'M12 5v14M5 12h14', check: 'M5 12l5 5 9-10', down: 'M7 10l5 5 5-5', right: 'M10 7l5 5-5 5', left: 'M14 7l-5 5 5 5',
32
- dia: 'M12 4l8 8-8 8-8-8z', cal: 'M4 5h16v15H4zM8 3v4M16 3v4M4 10h16', grip: 'M9 6h.01M15 6h.01M9 12h.01M15 12h.01M9 18h.01M15 18h.01',
35
+ dia: 'M12 4l8 8-8 8-8-8z', stage: 'M12 3l9 5-9 5-9-5zM3 13l9 5 9-5', box: 'M6 5h12a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1z',
36
+ who: 'M12 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM4 21c1-4 4-6 8-6s7 2 8 6', cal: 'M4 5h16v15H4zM8 3v4M16 3v4M4 10h16', grip: 'M9 6h.01M15 6h.01M9 12h.01M15 12h.01M9 18h.01M15 18h.01',
33
37
  open: 'M14 4h6v6M20 4l-9 9M18 14v6H4V6h6', bin: 'M5 7h14M10 7V4h4v3M7 7l1 13h8l1-13',
34
38
  };
35
39
 
36
40
  /* ── what is open, per initiative ── */
37
41
  const OPEN = new Map(); // initiative id → the uid of the row open in place
38
42
  const ADD = new Map(); // initiative id → the stage key whose + field is open
39
- const ADD_D = new Map(); // initiative id → { start, due } the add field's Dates chip picked
43
+ const ADD_D = new Map(); // initiative id → what the add field's chips picked: { start, due, section, ms, owner } (batch 25 item 5)
40
44
  const STAGES = new Map(); // initiative id → [{ key, name }] the stages a task can move to
41
45
  const VIEW = new Map(); // initiative id → the view the List drew last (the drag reads its stages' tasks)
42
46
  export const openOf = (id) => OPEN.get(id) || '';
@@ -94,14 +98,41 @@ export function plusHtml(sec, id, o) {
94
98
  return `<button type="button" class="iv-ib iv-plus${on ? ' on' : ''}" data-iv-plus="${esc(sec.key || '')}" data-nm="${esc(nm)}" aria-pressed="${on}" aria-label="${esc('New task in ' + nm)}" title="${esc('New task in ' + nm)}">${SVG(P.plus, 16, 2.2)}</button>`;
95
99
  }
96
100
  const mdIso = (iso) => { const n = TV.dn(iso); return n == null ? '' : TV.md(n); };
101
+ const initials = (n) => String(n || '').split(/\s+/).filter(Boolean).slice(0, 2).map((w) => w[0].toUpperCase()).join('') || '?';
102
+ /** The add field's picks: the stage it adds into (the + it opened from, unless the Stage chip changed it) and that stage's name. */
103
+ function addTo(id, own, nm) {
104
+ const D = ADD_D.get(id) || {};
105
+ return D.section != null && D.section !== own ? { key: D.section, name: stageName(id, D.section) } : { key: own, name: nm };
106
+ }
107
+ /** The chip row under the add field (TaskAddChips): Stage, Type, Dates, Owner. A chip with nothing picked is dashed. */
108
+ function chipsHtml(id, D, to) {
109
+ const ms = !!D.ms;
110
+ const dt = ms ? mdIso(D.due) : D.start || D.due ? [mdIso(D.start), mdIso(D.due)].filter(Boolean).join(' – ') : '';
111
+ const car = SVG(P.down, 12, 2.2);
112
+ const stage = `<button type="button" class="iv-edc iv-adc on" data-iv-addst aria-haspopup="menu" aria-expanded="false" aria-label="${esc('Stage: ' + to.name + '. Change')}" title="Stage">${SVG(P.stage, 14, 2)}<b>${esc(to.name)}</b>${car}</button>`;
113
+ const type = `<button type="button" class="iv-edc iv-adc${ms ? ' ms' : ''}" data-iv-addty aria-haspopup="menu" aria-expanded="false" aria-label="${esc('Type: ' + (ms ? 'Milestone' : 'Task') + '. Change')}" title="Task or milestone">${SVG(ms ? P.dia : P.box, 14, 2)}${ms ? 'Milestone' : 'Task'}${car}</button>`;
114
+ const dates = `<button type="button" class="iv-edc iv-edd${dt ? '' : ' dash'}" data-iv-adddt data-s="${esc(ms ? '' : D.start || '')}" data-e="${esc(D.due || '')}" data-f="${ms ? 'due' : 'start'}"${ms ? ' data-one="1"' : ''} aria-haspopup="dialog" aria-label="${esc(dt ? (ms ? 'Date: ' : 'Dates: ') + dt + '. Change' : ms ? 'Date, optional' : 'Dates, optional')}" title="${ms ? 'Date, optional' : 'Dates, optional'}">${SVG(P.cal, 14, 2)}${esc(dt || (ms ? 'Date' : 'Dates'))}</button>`;
115
+ const owner = D.owner
116
+ ? `<button type="button" class="iv-edc iv-adc" data-iv-addow aria-haspopup="menu" aria-expanded="false" aria-label="${esc('Owner: ' + D.owner + '. Change')}" title="Owner"><span class="iv-av" aria-hidden="true">${esc(initials(D.owner))}</span>${esc(D.owner)}</button>`
117
+ : `<button type="button" class="iv-edc iv-adc dash" data-iv-addow aria-haspopup="menu" aria-expanded="false" aria-label="Owner, optional: you when none is picked" title="Owner, optional">${SVG(P.who, 14, 2)}Owner</button>`;
118
+ return stage + type + dates + owner;
119
+ }
120
+ const addBox = (ms) => (ms ? `<span class="iv-adms" aria-hidden="true">${SVG(P.dia, 20, 2)}</span>` : '<span class="iv-cb" aria-hidden="true"></span>');
97
121
  export function addHtml(sec, id) {
98
122
  if (addOf(id) !== (sec.key || '')) return '';
99
- const nm = sec.name || 'Not in a stage', D = ADD_D.get(id) || {};
100
- const dt = D.start || D.due ? [mdIso(D.start), mdIso(D.due)].filter(Boolean).join(' – ') : '';
101
- return `<form class="iv-addf iv-addin" data-esc-layer data-iv-addin="${esc(sec.key || '')}" data-nm="${esc(nm)}"><span class="iv-cb" aria-hidden="true"></span>`
102
- + `<input type="text" data-iv-addinp maxlength="200" autocomplete="off" placeholder="${esc('New task in ' + nm)}" aria-label="${esc('New task in ' + nm)}">`
123
+ const own = sec.key || '', nm = sec.name || 'Not in a stage', D = ADD_D.get(id) || {}, to = addTo(id, own, nm);
124
+ return `<form class="iv-addf iv-addin" data-esc-layer data-iv-addin="${esc(own)}" data-nm="${esc(nm)}">${addBox(D.ms)}`
125
+ + `<input type="text" data-iv-addinp maxlength="200" autocomplete="off" placeholder="${esc('New task in ' + to.name)}" aria-label="${esc('New task in ' + to.name)}">`
103
126
  + `<button type="submit" class="iv-go" aria-label="Add the task" title="Add (Enter). Esc closes.">${SVG(P.check, 16, 2.2)}</button></form>`
104
- + `<div class="iv-addd"><button type="button" class="iv-edc iv-edd${dt ? '' : ' dash'}" data-iv-adddt data-s="${esc(D.start || '')}" data-e="${esc(D.due || '')}" data-f="start" aria-label="${esc(dt ? 'Dates: ' + dt + '. Change' : 'Dates, optional')}" title="Dates, optional">${SVG(P.cal, 14, 2)}${esc(dt || 'Dates')}</button></div>`;
127
+ + `<div class="iv-addd">${chipsHtml(id, D, to)}</div>`;
128
+ }
129
+ /** The names an owner can be: You first, then everyone who owns a task of this initiative. */
130
+ function ownersOf(id) {
131
+ const v = VIEW.get(id), out = new Set(['You']);
132
+ for (const s of (v && v.tasks && v.tasks.sections) || []) for (const c of s.stages || [s]) {
133
+ for (const x of [...(c.top || []), ...(c.groups || []).flatMap((g) => g.rows), ...(c.done || [])]) if (x.owner && !x.jkey) out.add(x.owner);
134
+ }
135
+ return [...out];
105
136
  }
106
137
 
107
138
  /* ── the writes (Undo in the host's toast) ── */
@@ -173,9 +204,11 @@ export function press(b, d, root, H, e) {
173
204
  }
174
205
  if ('ivAdddt' in d) {
175
206
  e.preventDefault(); TOUCHED = Date.now();
176
- TV.datePop(b, '', { dates: (u, p) => { ADD_D.set(id, { ...(ADD_D.get(id) || {}), ...p }); const f = b.closest('.iv-wl'); redrawAdd(f, id); } });
207
+ const list = b.closest('.iv-wl');
208
+ TV.datePop(b, '', { dates: (u, p) => { ADD_D.set(id, { ...(ADD_D.get(id) || {}), ...p }); redrawAdd(list, id); } });
177
209
  return true;
178
210
  }
211
+ if ('ivAddst' in d || 'ivAddty' in d || 'ivAddow' in d) { e.preventDefault(); TOUCHED = Date.now(); addMenu(b, d, id); return true; }
179
212
  if (d.ivMore) { e.preventDefault(); TOUCHED = Date.now(); moreMenu(b, d.ivMore, id, v, H); return true; }
180
213
  if (b.closest('.iv-ed')) TOUCHED = Date.now();
181
214
  return false;
@@ -192,14 +225,43 @@ function focusLater(root, sel, select) {
192
225
  };
193
226
  if (typeof requestAnimationFrame === 'function') requestAnimationFrame(go); else go();
194
227
  }
195
- /** The add field's Dates chip again, after a pick, without a redraw (the typed words stay). */
228
+ /** The add field's chips, box and placeholder again after a pick, without a redraw (the typed words stay). */
196
229
  function redrawAdd(list, id) {
197
- const f = list && list.querySelector('.iv-addd'); if (!f) return;
198
- const D = ADD_D.get(id) || {}, dt = [mdIso(D.start), mdIso(D.due)].filter(Boolean).join(' – '), c = f.querySelector('[data-iv-adddt]');
199
- if (!c) return;
200
- c.dataset.s = D.start || ''; c.dataset.e = D.due || ''; c.classList.toggle('dash', !dt);
201
- c.setAttribute('aria-label', dt ? 'Dates: ' + dt + '. Change' : 'Dates, optional');
202
- c.innerHTML = SVG(P.cal, 14, 2) + esc(dt || 'Dates');
230
+ const f = list && list.querySelector('[data-iv-addin]'), row = list && list.querySelector('.iv-addd'); if (!f || !row) return;
231
+ const D = ADD_D.get(id) || {}, to = addTo(id, f.dataset.ivAddin, f.dataset.nm);
232
+ row.innerHTML = chipsHtml(id, D, to);
233
+ const box = f.firstElementChild; if (box && box.tagName === 'SPAN') box.outerHTML = addBox(D.ms);
234
+ const inp = f.querySelector('[data-iv-addinp]');
235
+ if (inp) { inp.placeholder = 'New task in ' + to.name; inp.setAttribute('aria-label', 'New task in ' + to.name); }
236
+ }
237
+ /** The Stage, Type and Owner chips' pickers, right under the chip. A pick changes only the add field; nothing is saved until ✓. */
238
+ function addMenu(b, d, id) {
239
+ const list = b.closest('.iv-wl'), f = list && list.querySelector('[data-iv-addin]'); if (!f) return;
240
+ const D = ADD_D.get(id) || {}, set = (p) => { ADD_D.set(id, { ...(ADD_D.get(id) || {}), ...p }); redrawAdd(list, id); const n = list.querySelector('[data-iv-addinp]'); if (n) n.focus({ preventScroll: true }); };
241
+ const item = (attr, on, inner) => `<button type="button" role="menuitemradio" class="o${on ? ' hl' : ''}" aria-checked="${on}" ${attr}><span class="ck" aria-hidden="true">${on ? SVG(P.check, 14, 2.4) : ''}</span>${inner}</button>`;
242
+ if ('ivAddst' in d) {
243
+ const to = addTo(id, f.dataset.ivAddin, f.dataset.nm).key;
244
+ let n = 0;
245
+ const html = `<div class="tkc-pop ivq-m ive-m" role="menu" aria-label="Stage">` + (STAGES.get(id) || []).map((s) => item(`data-ive-to="${esc(s.key)}"`, s.key === to, `${s.key ? `<span class="nn" aria-hidden="true">${++n}</span>` : ''}<span>${esc(s.name)}</span>`)).join('') + '</div>';
246
+ TV.openPop(b, html, (o) => { if (o.dataset.iveTo == null) return; TV.closePop(); set({ section: o.dataset.iveTo }); });
247
+ return;
248
+ }
249
+ if ('ivAddty' in d) {
250
+ const html = `<div class="tkc-pop ivq-m ive-m" role="menu" aria-label="Task or milestone">` + item('data-ive-ty="task"', !D.ms, `<span class="nn ic" aria-hidden="true">${SVG(P.box, 14, 2)}</span><span>Task</span>`)
251
+ + item('data-ive-ty="ms"', !!D.ms, `<span class="nn ic" aria-hidden="true">${SVG(P.dia, 14, 2)}</span><span>Milestone</span><span class="k">one date</span>`) + '</div>';
252
+ TV.openPop(b, html, (o) => {
253
+ const t = o.dataset.iveTy; if (!t) return; TV.closePop();
254
+ // a milestone has one date: the due day (or the start, when only a start was picked)
255
+ if (t === 'ms') set({ ms: true, due: D.due || D.start || '', start: '' }); else set({ ms: false });
256
+ });
257
+ return;
258
+ }
259
+ const names = ownersOf(id), cur = D.owner || '';
260
+ const html = `<div class="tkc-pop ivq-m ive-m" role="menu" aria-label="Owner">` + names.map((nm) => item(`data-ive-ow="${esc(nm === 'You' ? '' : nm)}"`, nm === 'You' ? !cur : nm === cur, `<span class="iv-av" aria-hidden="true">${esc(nm === 'You' ? 'Y' : initials(nm))}</span><span>${esc(nm)}</span>`)).join('')
261
+ + `<hr><form class="tkc-new ive-own"><input type="text" maxlength="80" placeholder="Someone else" aria-label="Owner's name" autocomplete="off"></form></div>`;
262
+ const el = TV.openPop(b, html, (o) => { if (o.dataset.iveOw == null) return; TV.closePop(); set({ owner: o.dataset.iveOw }); });
263
+ const form = el.querySelector('.ive-own');
264
+ if (form) form.addEventListener('submit', (e) => { e.preventDefault(); const v = form.querySelector('input').value.trim(); if (!v) return; TV.closePop(); set({ owner: v }); });
203
265
  }
204
266
  /** ⋯ of the open row (StA2bMore): Milestone (or Make it a plain task), Move to a stage, Open the task card (Open the ticket on a Jira row),
205
267
  * Delete (only where the task can be deleted; never a Jira row). Move to a stage turns the menu into the workflow's stages, ← goes back. */
@@ -256,9 +318,10 @@ export function wire(body) {
256
318
  if (f.dataset.ivRenf != null) { const inp = f.querySelector('[data-iv-ren]'), id = idOf(); OPEN.delete(id); if (!save(inp) && H().redraw) H().redraw(); return; }
257
319
  const inp = f.querySelector('[data-iv-addinp]'), t = inp ? inp.value.trim() : '', id = idOf();
258
320
  if (!t || !H().addTask) return;
259
- const D = ADD_D.get(id) || {};
321
+ // the chips' picks (batch 25 item 5): the stage, a milestone with its one date, the owner (none: you)
322
+ const D = ADD_D.get(id) || {}, to = addTo(id, f.dataset.ivAddin, f.dataset.nm);
260
323
  inp.disabled = true;
261
- Promise.resolve(H().addTask(t, inp, { section: f.dataset.ivAddin, name: f.dataset.nm, ...(D.start ? { start: D.start } : {}), ...(D.due ? { due: D.due } : {}) })).then(() => {
324
+ Promise.resolve(H().addTask(t, inp, { section: to.key, name: to.name, ...(D.start && !D.ms ? { start: D.start } : {}), ...(D.due ? { due: D.due } : {}), ...(D.ms ? { milestone: true } : {}), ...(D.owner ? { owner: D.owner } : {}) })).then(() => {
262
325
  // Enter keeps the field open for the next one (StA5Add)
263
326
  ADD_D.delete(id);
264
327
  const n = body.querySelector('[data-iv-addinp]'); if (n) { if (n === inp) n.value = ''; n.disabled = false; n.focus({ preventScroll: true }); }
@@ -157,10 +157,15 @@ const tkTitle = (t) => `<button type="button" class="iv-tt" data-ig-tk="${esc(t.
157
157
  /** A task linked to a Jira ticket it names (ini-view-model.js linkOf): the key once, in blue, then the rest of its title, the same in the List,
158
158
  * the Timeline and the Kanban (class lk). A key PMOS has not read stays plain text in the title (0.442.0 hotfix). */
159
159
  export function keyName(x) {
160
- if (!x.jkey) return esc(x.title);
160
+ if (!x.jkey) return prefixName(x.title);
161
161
  const rest = String(x.title || '').replace(new RegExp('[\\[(]?\\b' + x.jkey + '\\b[\\])]?\\s*[:\u2013\u2014-]?\\s*'), '').trim();
162
162
  return `<em class="lk">${esc(x.jkey)}</em>${rest ? ' ' + esc(rest) : ''}`;
163
163
  }
164
+ /** A title that starts with a short prefix ("Bus shelter: Confirm QR placement") keeps it, quieter (batch 25 item 4, canvas TaskNamesWrap). */
165
+ export function prefixName(t) {
166
+ const m = /^([^:]{1,32}):\s+(\S[\s\S]*)$/.exec(String(t == null ? '' : t));
167
+ return m && m[1].trim().split(/\s+/).length <= 4 && !/\/\/|\d{1,2}$/.test(m[1]) ? `<span class="iv-pf">${esc(m[1])}: </span>${esc(m[2])}` : esc(t);
168
+ }
164
169
  /** The Jira mark of a linked task, in place of its box: the status square the ticket rows draw (To do, In progress, Done). Jira owns the
165
170
  * status, so it marks nothing here. */
166
171
  export const jqMark = (x) => `<span class="iv-jq ig-sq ${x.closed ? 'done' : x.prog ? 'doing' : 'todo'}${mvCls(x.jmove)}" role="img" aria-label="${esc('Jira ' + x.jkey + ': ' + (movingOf(x.jmove) ? 'moving to ' + x.status : x.jst || x.status))}" title="${esc('Jira · ' + (movingOf(x.jmove) ? 'moving to ' + x.status : x.jst || x.status))}"></span>`;
@@ -302,7 +302,9 @@ export function iniView(r, ctx = {}) {
302
302
  // what waits on the PM: the track's, else the trail's (the Board merges the trail into the track; Home reads the row as sent)
303
303
  const you = t.you || (it.you && it.you.text) || null, youKind = t.youKind || (it.you && it.you.kind) || null, youWhy = t.youWhy || (it.you && it.you.why) || '';
304
304
  let wait = null;
305
- if (ctx.ask && ctx.ask.title) wait = { text: '“' + ctx.ask.title + '” needs your answer.', say: 'Can you answer “' + ctx.ask.title + '”? I’ll carry on once you do.', primary: { act: 'ask', label: 'Answer: ' + ctx.ask.title } };
305
+ // Claude's ✦ proposal for a blocked step (batch 25 item 6): what it will do, for the PM to approve; never a bare question
306
+ if (ctx.ask && ctx.ask.propose && ctx.ask.propose.say) wait = { text: '✦ ' + ctx.ask.propose.say, say: '✦ ' + ctx.ask.propose.say + ' Shall I go ahead?', primary: { act: 'ask', label: '✦ ' + ctx.ask.propose.label } };
307
+ else if (ctx.ask && ctx.ask.title) wait = { text: '“' + ctx.ask.title + '” needs your answer.', say: 'Can you answer “' + ctx.ask.title + '”? I’ll carry on once you do.', primary: { act: 'ask', label: 'Answer: ' + ctx.ask.title } };
306
308
  else if (you && youKind) wait = { text: youWhy, primary: youKind === 'drift' && it.links && it.links.prd ? { act: 'doc:' + it.links.prd, label: 'Open the PRD' } : youKind === 'commit' ? { act: 'commit', label: 'Send to Jira' } : { act: 'page', label: you } };
307
309
  else if (you) { const opn = tk.filter((k) => k.status !== 'Done').length; wait = { text: opn ? plural(opn, 'ticket is', 'tickets are') + ' still open, so the sign-off can wait until they close.' : 'Every release ticket is done.', primary: { act: 'page', label: you } }; }
308
310
  else if (ctx.grill && ctx.grill.open && !ctx.grill.later) wait = { text: 'PMOS asks what the BRD needs that the Why does not say yet.', primary: { act: 'grill', label: 'Answer the questions' } };
@@ -1193,12 +1193,24 @@ html.ad-still .mi-svg * { animation: none !important; }
1193
1193
  /* ── a task changed where it sits in the List (batch 18 item 16, agreed A; canvas In development St*) ──
1194
1194
  * One line per task (StA1Rows): the box, the title, its days at the right (a phone: the due day only), the owner's face on a desktop. A title
1195
1195
  * press opens the row in place (StA2Edit): the name field, then one quiet line: status, Start – Due, ⋯ right after the dates (StA8Desk). */
1196
- .iv-tr.q { align-items: center; position: relative; box-sizing: border-box; }
1197
- .iv-tr.q .iv-bxw { align-self: center; }
1198
- .iv-tr.q > .w { flex-direction: row; align-items: center; gap: 10px; min-width: 0; }
1199
- .iv-tr.q .iv-tn { flex: 1; min-width: 0; display: flex; align-items: center; gap: 6px; }
1200
- .iv-tr.q .iv-tt { display: block !important; flex: 0 1 auto; text-align: left; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; font-weight: 600; }
1201
- .iv-tr.q .iv-rm { flex: none; display: inline-flex; align-items: center; gap: 0; min-height: 0; max-width: 55%; white-space: nowrap; }
1196
+ /* batch 25 item 4 (canvas TaskNamesWrap, agreed A): a task's name is never cut. It wraps onto as many lines as it needs; its days stay at the
1197
+ * right of the first line in a narrower column; a narrow panel puts them under the name. The prefix ("Bus shelter:") stays, quieter. */
1198
+ .iv-tr.q { align-items: flex-start; position: relative; box-sizing: border-box; padding: 11px 0 !important; }
1199
+ .iv-tr.q .iv-bxw { align-self: flex-start; margin-top: 2px; }
1200
+ .iv-tr.q > .w { flex-direction: row; align-items: flex-start; gap: 10px; min-width: 0; }
1201
+ .iv-tr.q .iv-tn { flex: 1; min-width: 0; display: flex; align-items: flex-start; gap: 6px; }
1202
+ .iv-tr.q .iv-tt { display: block !important; flex: 0 1 auto; text-align: left; white-space: normal; overflow-wrap: anywhere; min-width: 0; font-weight: 600; line-height: 22px; }
1203
+ .iv-tr.q .iv-tn > :not(.iv-tt) { margin-top: 4px; }
1204
+ .iv-tr.q .iv-rm { flex: none; display: inline-flex; align-items: center; gap: 0; min-height: 22px; max-width: 40%; white-space: nowrap; }
1205
+ .iv-pf { color: var(--cz-ink3); font-weight: 700; }
1206
+ @container iv (max-width: 520px) {
1207
+ .iv-tr.q > .w { flex-direction: column; gap: 0; }
1208
+ .iv-tr.q .iv-rm { max-width: 100%; flex-wrap: wrap; white-space: normal; }
1209
+ .iv-tr.q .iv-rm .iv-dc { margin-left: 0 !important; padding-left: 0 !important; }
1210
+ .iv-tr.q .iv-dst { display: inline; }
1211
+ /* the days sit under the name: their wider press area (a phone) reaches down, not up over the name */
1212
+ .iv-tr.q .iv-rm .iv-dc::after { top: 0; }
1213
+ }
1202
1214
  .iv-tr.q .iv-dc { margin: 0 !important; white-space: nowrap; }
1203
1215
  .iv-dms { display: inline-flex; margin-right: 4px; color: var(--cz-ink2); }
1204
1216
  .iv-rav { width: 24px; height: 24px; margin-left: 10px; background: var(--cz-well); color: var(--cz-ink2); }
@@ -1246,7 +1258,19 @@ html.ad-still .mi-svg * { animation: none !important; }
1246
1258
  .iv-addin { margin: 0 -14px 0 -46px; padding: 10px 14px 4px 46px; background: var(--cz-bluebg); gap: 10px; }
1247
1259
  .iv-addin .iv-cb { margin-top: 0 !important; border-style: solid; }
1248
1260
  .iv-addin input { border: 1.5px solid var(--cz-blue); }
1249
- .iv-addd { margin: 0 -14px 0 -46px; padding: 4px 14px 12px 46px; background: var(--cz-bluebg); }
1261
+ .iv-addd { margin: 0 -14px 0 -46px; padding: 4px 14px 12px 46px; background: var(--cz-bluebg); display: flex; flex-wrap: wrap; gap: 8px; }
1262
+ /* the add field's chips (batch 25 item 5, canvas TaskAddChips): Stage (picked, blue), Type, Dates, Owner; a milestone's box is ◇ */
1263
+ .iv .iv-adc { gap: 5px; }
1264
+ .iv .iv-adc.on { color: var(--cz-blueink) !important; box-shadow: 0 0 0 1.5px var(--cz-blue); }
1265
+ .iv .iv-adc.on b { font-weight: 700; max-width: 180px; overflow: hidden; text-overflow: ellipsis; }
1266
+ .iv .iv-adc.ms { color: var(--cz-blueink) !important; }
1267
+ .iv .iv-adc .iv-av { width: 20px; height: 20px; font-size: 11px; }
1268
+ .iv-adms { width: 20px; height: 20px; flex: none; display: inline-flex; align-items: center; justify-content: center; color: var(--cz-blue); }
1269
+ .tkc.ivq .ive-m .nn { width: 22px; height: 20px; border-radius: 7px; background: var(--cz-well); font-size: 11px; font-weight: 800; color: var(--cz-ink3); display: inline-flex; align-items: center; justify-content: center; flex: none; }
1270
+ .tkc.ivq .ive-m .nn.ic { background: transparent; color: var(--cz-ink2); }
1271
+ .tkc.ivq .ive-m .o.hl .nn:not(.ic) { background: var(--cz-bluebg); color: var(--cz-blueink); }
1272
+ .tkc.ivq .ive-m .iv-av { width: 22px; height: 22px; border-radius: 50%; background: var(--cz-well); color: var(--cz-ink2); font-size: 11px; font-weight: 800; display: inline-flex; align-items: center; justify-content: center; flex: none; }
1273
+ .tkc.ivq .ive-own { padding: 4px 6px 6px; }
1250
1274
  /* the drag (StA10Drag): the row lifts and follows the pointer; its slot stays as a dashed ghost; the target stage lights up; a blue line marks the drop */
1251
1275
  .iv-tr.iv-ghost { outline: 1.5px dashed color-mix(in srgb, var(--cz-ink3) 50%, var(--cz-card)); outline-offset: -2px; opacity: .55; }
1252
1276
  .iv-fly { position: fixed; z-index: 1001; left: var(--fly-x, 0); top: var(--fly-y, 0); width: var(--fly-w, 320px); pointer-events: none; background: var(--cz-card); border-radius: 12px; padding: 7px 12px !important;
@@ -1142,8 +1142,8 @@ function openTask(r, k) {
1142
1142
  async function addTask(r, title, section, dates, name) {
1143
1143
  const body = { ws: r.ws || M.board.ws, board: r.boardId || M.board.id };
1144
1144
  // dates: a gap's + (batch 11 item 13) fills the task's start and due with the gap's days ahead; section: the stage it goes under (the
1145
- // add route places it, batch 18 item 16)
1146
- const res = await pmPost('/api/boards/tasks/add', { ...body, initiative: r.id, title, ...(dates && dates.start ? { start: dates.start } : {}), ...(dates && dates.due ? { due: dates.due } : {}), ...(section ? { section } : {}) }).catch(() => null);
1145
+ // add route places it, batch 18 item 16); the add field's chips also send milestone and owner in it (batch 25 item 5)
1146
+ const res = await pmPost('/api/boards/tasks/add', { ...body, initiative: r.id, title, ...(dates && dates.start ? { start: dates.start } : {}), ...(dates && dates.due ? { due: dates.due } : {}), ...(dates && dates.milestone ? { milestone: true } : {}), ...(dates && dates.owner ? { owner: dates.owner } : {}), ...(section ? { section } : {}) }).catch(() => null);
1147
1147
  if (!res || res.ok === false) { say((res && res.error) || 'The task was not added.'); return false; }
1148
1148
  const uid = res.item && res.item.uid;
1149
1149
  // the + on a stage (batch 18 item 16, StA5Add): "Added to Build" with Undo
@@ -135,15 +135,16 @@ const TICKETS = [{ key: 'IDPF-3387', title: 'Refactor the AppAuthService', statu
135
135
  const b1 = row('task-b1'), b4 = row('task-b4'), d1 = row('task-d1'), b2 = row('task-b2');
136
136
  const tt = b1 && b1.querySelector('.iv-tt').getBoundingClientRect(), dc = b1 && b1.querySelector('.iv-dc').getBoundingClientRect();
137
137
  return {
138
- oneLine: !!(tt && dc && Math.abs((tt.top + tt.height / 2) - (dc.top + dc.height / 2)) < 6 && b1.getBoundingClientRect().height <= 52),
139
- phoneDue: b1 && b1.querySelector('.iv-dc').innerText.trim(), startHidden: !vis(b1 && b1.querySelector('.iv-dst')),
138
+ // batch 25 item 4 (TaskNamesWrap, agreed A): a narrow panel puts the days under the full name, start and due
139
+ under: !!(tt && dc && dc.top >= tt.bottom - 2 && Math.abs(dc.left - tt.left) < 8), wraps: !!(b1 && getComputedStyle(b1.querySelector('.iv-tt')).whiteSpace === 'normal'),
140
+ phoneDue: b1 && b1.querySelector('.iv-dc').innerText.trim(), startShown: vis(b1 && b1.querySelector('.iv-dst')),
140
141
  undated: b4 && !b4.querySelector('.iv-dc'), ms: d1 && d1.querySelector('.iv-dc').innerText.trim(), msTitleGlyph: !!(d1 && d1.querySelector('.iv-tn .iv-ms')), msIcon: !!(d1 && d1.querySelector('.iv-dc .iv-dms svg')),
141
142
  plus: [...document.querySelectorAll('#czViewer .iv-plus')].map((b) => b.getAttribute('aria-label')), headPlus: !!document.querySelector('#czViewer .iv-th [data-iv-add]'),
142
143
  jira: b2 && !!b2.querySelector('.iv-jq.ig-sq') && !b2.querySelector('[data-iv-check]'),
143
144
  grips: [...document.querySelectorAll('#czViewer .iv-grip')].filter(vis).length,
144
145
  };
145
146
  });
146
- ok('S1 a phone row is one line: the title, its due day at the right (the start hidden)', R1.oneLine && R1.phoneDue === 'Nov 6' && R1.startHidden, JSON.stringify(R1));
147
+ ok('S1 a phone row: the whole title (it wraps, never cut), its days under it, start and due (batch 25 item 4)', R1.under && R1.wraps && /Nov 6$/.test(R1.phoneDue || '') && R1.startShown, JSON.stringify(R1));
147
148
  ok('S1 a row with no dates shows none (zero is silence)', R1.undated, JSON.stringify(R1));
148
149
  ok('S1 a milestone row says "◇ Nov 14", its ◇ once (not after the title)', R1.ms === 'Nov 14' && R1.msIcon && !R1.msTitleGlyph, JSON.stringify(R1));
149
150
  ok('S1 each open stage has its own icon-only + ("New task in …"); the Tasks header has none', R1.plus.join('|') === 'New task in Build|New task in Deliver' && !R1.headPlus, JSON.stringify(R1));
@@ -0,0 +1,5 @@
1
+ {
2
+ "_comment": "feedback batch 25 item 1: a hosted product repository with no git author identity gets the PM's from the other hosted repositories (lib/git-identity.js).",
3
+ "modules": ["git-identity.test.js"],
4
+ "seconds": { "git-identity.test.js": 2 }
5
+ }