create-pmos 0.134.0 → 0.135.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 (209) hide show
  1. package/package.json +1 -1
  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/bin/notes-replay.js +235 -0
  7. package/workspace/c3-briefing-test.js +130 -0
  8. package/workspace/c3-foundation-test.js +159 -0
  9. package/workspace/c3-ib-items-test.js +105 -0
  10. package/workspace/c3-ib-more-test.js +120 -0
  11. package/workspace/c3-ib-triage-backlog-test.js +119 -0
  12. package/workspace/c3-ini-harness.js +66 -0
  13. package/workspace/c3-initiatives-test.js +140 -0
  14. package/workspace/c3-one-home-test.js +14 -1
  15. package/workspace/c3-reports-test.js +57 -18
  16. package/workspace/c3-settings-test.js +139 -0
  17. package/workspace/c3-work-test.js +42 -10
  18. package/workspace/fixtures/one-home-baseline.json +1 -9
  19. package/workspace/guide/briefing-cards.md +15 -3
  20. package/workspace/guide/briefing.md +12 -8
  21. package/workspace/guide/calendar-popup.md +2 -2
  22. package/workspace/guide/console-calendar.md +39 -10
  23. package/workspace/guide/index.md +1 -0
  24. package/workspace/guide/initiative-boards.md +65 -0
  25. package/workspace/guide/jira.md +3 -1
  26. package/workspace/guide/layout.md +2 -0
  27. package/workspace/guide/my-work.md +5 -3
  28. package/workspace/guide/notes.md +16 -1
  29. package/workspace/guide/reports-dashboards.md +29 -5
  30. package/workspace/guide/settings.md +68 -0
  31. package/workspace/guide/welcome.md +10 -10
  32. package/workspace/lib/calendar-cli.js +3 -2
  33. package/workspace/lib/calendar-layers.js +21 -10
  34. package/workspace/lib/calendar-tidy.js +230 -0
  35. package/workspace/lib/engine/boards.js +542 -0
  36. package/workspace/lib/engine/breakdown-ops.js +180 -0
  37. package/workspace/lib/engine/calendar-import.js +84 -0
  38. package/workspace/lib/engine/calendar-model.js +185 -0
  39. package/workspace/lib/engine/card-state.js +92 -0
  40. package/workspace/lib/engine/derived/derive.js +92 -0
  41. package/workspace/lib/engine/derived/plan.js +3 -0
  42. package/workspace/lib/engine/derived/trust.js +3 -0
  43. package/workspace/lib/engine/doc-health.js +142 -0
  44. package/workspace/lib/engine/doc-sheet.js +75 -0
  45. package/workspace/lib/engine/intake.js +78 -0
  46. package/workspace/lib/engine/outbox.js +86 -0
  47. package/workspace/lib/engine/plan-forecast.js +342 -0
  48. package/workspace/lib/engine/promises.js +29 -0
  49. package/workspace/lib/engine/records/index.js +114 -0
  50. package/workspace/lib/engine/releases.js +96 -0
  51. package/workspace/lib/engine/replies.js +99 -0
  52. package/workspace/lib/engine/reply-contract/reply-contract.js +372 -0
  53. package/workspace/lib/engine/report-families.js +175 -0
  54. package/workspace/lib/engine/roadmap-ops.js +150 -0
  55. package/workspace/lib/engine/sessions-list.js +27 -0
  56. package/workspace/lib/engine/signal-rules.js +95 -0
  57. package/workspace/lib/engine/signals/cards.js +57 -0
  58. package/workspace/lib/engine/signals/detectors.js +370 -0
  59. package/workspace/lib/engine/signals/facts-cli.js +8 -0
  60. package/workspace/lib/engine/signals/facts.js +197 -0
  61. package/workspace/lib/engine/signals/index.js +33 -0
  62. package/workspace/lib/engine/signals/kinds.js +110 -0
  63. package/workspace/lib/engine/signals/layout-paths.js +44 -0
  64. package/workspace/lib/engine/signals/options.js +24 -0
  65. package/workspace/lib/engine/signals/score.js +72 -0
  66. package/workspace/lib/engine/signals/select.js +47 -0
  67. package/workspace/lib/engine/step-planner.js +65 -0
  68. package/workspace/lib/engine/trust/claim-rules.json +93 -0
  69. package/workspace/lib/engine/trust/import-calendar.js +21 -0
  70. package/workspace/lib/engine/trust/import-guard.js +36 -0
  71. package/workspace/lib/engine/trust/ledger.js +53 -0
  72. package/workspace/lib/engine/trust/outbox.js +45 -0
  73. package/workspace/lib/engine/trust/plan.js +5 -0
  74. package/workspace/lib/engine/trust/trust.js +21 -0
  75. package/workspace/lib/engine/truth-table.js +57 -0
  76. package/workspace/lib/engine/workspace-lib.js +24 -0
  77. package/workspace/lib/factory-join.js +1 -1
  78. package/workspace/lib/facts-async.js +4 -4
  79. package/workspace/lib/headless.js +95 -0
  80. package/workspace/lib/import-overlay.js +2 -1
  81. package/workspace/lib/jira-status.js +30 -3
  82. package/workspace/lib/notes-capture.js +9 -1
  83. package/workspace/lib/notes-final.js +38 -13
  84. package/workspace/lib/notes-replay.js +125 -0
  85. package/workspace/lib/notes-screen.js +180 -0
  86. package/workspace/lib/notes-whisper.js +10 -3
  87. package/workspace/lib/outbox-store.js +10 -6
  88. package/workspace/lib/pmos-verbs.js +3 -1
  89. package/workspace/lib/proactive-cli.js +7 -0
  90. package/workspace/lib/proactive.js +15 -2
  91. package/workspace/lib/repo-script.js +36 -0
  92. package/workspace/lib/report-dashboards.js +5 -2
  93. package/workspace/lib/report-frame.js +122 -0
  94. package/workspace/lib/report-schedules.js +113 -0
  95. package/workspace/lib/route-modules.js +9 -2
  96. package/workspace/lib/routes-boards.js +734 -0
  97. package/workspace/lib/routes-brief-page.js +13 -4
  98. package/workspace/lib/routes-briefing.js +22 -11
  99. package/workspace/lib/routes-c3-init.js +4 -3
  100. package/workspace/lib/routes-c3-life.js +2 -1
  101. package/workspace/lib/routes-c3-reports.js +96 -13
  102. package/workspace/lib/routes-c3-strategy.js +4 -3
  103. package/workspace/lib/routes-c3-timeline.js +3 -2
  104. package/workspace/lib/routes-calendar-import.js +3 -2
  105. package/workspace/lib/routes-calendar-views.js +9 -4
  106. package/workspace/lib/routes-doc-health.js +2 -1
  107. package/workspace/lib/routes-doc-sheet.js +2 -1
  108. package/workspace/lib/routes-forecast.js +2 -1
  109. package/workspace/lib/routes-hooks.js +3 -2
  110. package/workspace/lib/routes-initiatives.js +2 -1
  111. package/workspace/lib/routes-intake.js +2 -1
  112. package/workspace/lib/routes-notes.js +55 -9
  113. package/workspace/lib/routes-outbox.js +23 -3
  114. package/workspace/lib/routes-pm-items.js +19 -6
  115. package/workspace/lib/routes-records.js +3 -2
  116. package/workspace/lib/routes-releases.js +2 -1
  117. package/workspace/lib/routes-report-facts.js +4 -3
  118. package/workspace/lib/routes-report-families.js +2 -1
  119. package/workspace/lib/routes-roadmap-ops.js +5 -4
  120. package/workspace/lib/routes-runs.js +6 -5
  121. package/workspace/lib/routes-settings.js +51 -0
  122. package/workspace/lib/routes-signals.js +3 -2
  123. package/workspace/lib/routes-stage-tools.js +220 -0
  124. package/workspace/lib/run-answers.js +2 -1
  125. package/workspace/lib/settings-store.js +226 -0
  126. package/workspace/lib/stage-tools.js +208 -0
  127. package/workspace/lib/vendor/scripts/signals/layout-paths.js +2 -1
  128. package/workspace/lib/vendor/scripts/workspace-lib.js +24 -0
  129. package/workspace/lib/work-evidence.js +73 -0
  130. package/workspace/package-lock.json +2 -2
  131. package/workspace/package.json +1 -1
  132. package/workspace/public/pm/briefing-deck.js +63 -17
  133. package/workspace/public/pm/briefing-model.js +36 -7
  134. package/workspace/public/pm/c3-ambient.js +100 -0
  135. package/workspace/public/pm/c3-brief-needs.js +26 -6
  136. package/workspace/public/pm/c3-brief-pulse.js +12 -4
  137. package/workspace/public/pm/c3-brief-today.js +56 -21
  138. package/workspace/public/pm/c3-brief.js +2 -1
  139. package/workspace/public/pm/c3-calendar.js +161 -42
  140. package/workspace/public/pm/c3-calpop.js +5 -5
  141. package/workspace/public/pm/c3-demo-boards.js +649 -0
  142. package/workspace/public/pm/c3-demo-brief-page.js +1 -1
  143. package/workspace/public/pm/c3-demo-cal4.js +3 -2
  144. package/workspace/public/pm/c3-demo-cal5.js +52 -0
  145. package/workspace/public/pm/c3-demo-data.js +17 -27
  146. package/workspace/public/pm/c3-demo-rep3.js +46 -5
  147. package/workspace/public/pm/c3-demo.js +2 -1
  148. package/workspace/public/pm/c3-dock.js +1 -1
  149. package/workspace/public/pm/c3-ib-docs.js +80 -0
  150. package/workspace/public/pm/c3-ib-initiatives.js +134 -0
  151. package/workspace/public/pm/c3-ib-items.js +201 -0
  152. package/workspace/public/pm/c3-ib-releases.js +51 -0
  153. package/workspace/public/pm/c3-ib-sprints.js +55 -0
  154. package/workspace/public/pm/c3-ib-triage.js +178 -0
  155. package/workspace/public/pm/c3-ii-tabs.js +240 -0
  156. package/workspace/public/pm/c3-ini-records.js +197 -0
  157. package/workspace/public/pm/c3-ini-timeline.js +278 -0
  158. package/workspace/public/pm/c3-ini-ui.js +229 -0
  159. package/workspace/public/pm/c3-ini-work.js +139 -0
  160. package/workspace/public/pm/c3-initiatives.css +218 -0
  161. package/workspace/public/pm/c3-initiatives.js +741 -0
  162. package/workspace/public/pm/c3-plan.js +2 -2
  163. package/workspace/public/pm/c3-reports.css +47 -47
  164. package/workspace/public/pm/c3-reports.js +223 -185
  165. package/workspace/public/pm/c3-settings.css +74 -0
  166. package/workspace/public/pm/c3-settings.js +703 -0
  167. package/workspace/public/pm/c3-shell.js +102 -36
  168. package/workspace/public/pm/c3-terminal-icons.js +2 -2
  169. package/workspace/public/pm/c3-terminal.js +2 -2
  170. package/workspace/public/pm/calendar-views.js +1 -1
  171. package/workspace/public/pm/components.css +319 -213
  172. package/workspace/public/pm/console3.css +99 -23
  173. package/workspace/public/pm/dest-notes.js +26 -4
  174. package/workspace/public/pm/notes-screen-capture.js +165 -0
  175. package/workspace/public/pm/notes-screen-view.js +98 -0
  176. package/workspace/public/pm/panel-trees.js +5 -2
  177. package/workspace/public/pm/pm-work-view.js +233 -146
  178. package/workspace/public/pm/shell-bell.js +3 -2
  179. package/workspace/public/pm/shell-recording.js +29 -8
  180. package/workspace/public/pm/tokens.css +2 -2
  181. package/workspace/public/pm/view-notes.js +46 -9
  182. package/workspace/public/pm/welcome.js +197 -89
  183. package/workspace/public/welcome.html +6 -4
  184. package/workspace/server.js +105 -6
  185. package/workspace/skills/day-check.skill +1 -1
  186. package/workspace/skills/jira-sync.skill +6 -4
  187. package/workspace/skills/morning-brief.skill +1 -1
  188. package/workspace/test-files.json +12 -3
  189. package/workspace/test-lists/c3-ib-items.json +9 -0
  190. package/workspace/test-lists/c3-ib-more.json +9 -0
  191. package/workspace/test-lists/c3-ib-triage-backlog.json +9 -0
  192. package/workspace/test-lists/c3-initiative-boards.json +6 -0
  193. package/workspace/test-lists/calendar-tidy-up.json +6 -0
  194. package/workspace/test-lists/console3-briefing.json +9 -0
  195. package/workspace/test-lists/console3-foundation.json +9 -0
  196. package/workspace/test-lists/console3-reports.json +3 -3
  197. package/workspace/test-lists/meeting-notes.json +10 -4
  198. package/workspace/test-lists/workspace-headless-jobs.json +5 -0
  199. package/workspace/test-lists/workspace-package-script-closure.json +6 -0
  200. package/workspace/lib/vendor/workspace/lib/actor.js +0 -91
  201. package/workspace/lib/vendor/workspace/lib/board-cache.js +0 -156
  202. package/workspace/lib/vendor/workspace/lib/console-state.js +0 -295
  203. package/workspace/lib/vendor/workspace/lib/factory-join.js +0 -158
  204. package/workspace/lib/vendor/workspace/lib/git-info.js +0 -306
  205. package/workspace/lib/vendor/workspace/lib/plan-items.js +0 -922
  206. package/workspace/lib/vendor/workspace/lib/plan-tree.js +0 -419
  207. package/workspace/lib/vendor/workspace/lib/review-gate.js +0 -39
  208. package/workspace/lib/vendor/workspace/lib/sink.js +0 -321
  209. package/workspace/lib/vendor/workspace/lib/state-root.js +0 -587
@@ -1,28 +1,39 @@
1
1
  /* pm/welcome.js — the first-run page (/welcome). One primary button per screen.
2
2
  *
3
- * Welcome → First look → Your portfolio → Questions (only when the scan left any) → First Briefing → More sources
3
+ * About you → Your sources → First look → Your portfolio → Questions (only when the scan left any) → First Briefing
4
4
  *
5
5
  * Routes (lib/routes-onboarding.js): GET /api/onboarding/status|structure|briefing, POST /api/onboarding/scan|apply|undo.
6
- * More sources uses POST /api/sources/refresh (lib/routes-sources.js): it starts the source's read-only skill as a session in
7
- * the workspace with the PM's own connection. Nothing is read until the PM presses Bring in; the server calls nothing outside.
6
+ * About you saves the name, role, work and job description to the proactive settings (POST /api/proactive/settings
7
+ * {name, about}); the brief and the day check read them from `pmos proactive settings`.
8
+ * Your sources lists the folders and the tools. A ticked tool starts when the PM presses Scan: Calendar and Jira through
9
+ * POST /api/sources/refresh (lib/routes-sources.js), which starts the source's read-only skill as a session with the PM's
10
+ * own connection; Inbox and Slack are sources of the daily brief, so they are turned on (briefSources) and the day is
11
+ * prepared once (POST /api/proactive/goals/plan). Nothing is read before the press; the server calls nothing outside.
8
12
  * Presses carry the launch nonce through pm/http.js. The page writes nothing itself; text taken from the scanned folder is
9
13
  * untrusted, so every value goes through esc() and no quoted text from a file is shown.
10
14
  * data-testid attributes are the browser suite's handles.
11
15
  */
12
16
  import { pmGet, pmPost } from './http.js';
17
+ import { el } from './dom.js';
18
+ import { button, field, SEL } from './primitives.js';
19
+ import { ensureC3Css } from './pm-work-view.js';
20
+ import { applyDayPart } from './c3-daypart.js';
13
21
 
14
22
  const root = document.getElementById('wl-root');
15
23
  const params = new URLSearchParams(location.search);
16
24
  const S = {
17
25
  ws: params.get('ws') || '', folder: null, added: [], step: 'welcome', busy: '', error: '', note: '',
18
- look: null, structure: null, checked: {}, titles: {}, boardOf: {}, open: {}, answers: {}, skipped: {}, importId: '', briefing: [], sources: {},
26
+ about: { name: '', role: '', work: '', duties: '' }, pick: {},
27
+ look: null, structure: null, checked: {}, titles: {}, boardOf: {}, open: {}, edit: {}, answers: {}, skipped: {}, importId: '', briefing: [], sources: {},
19
28
  };
20
- const STEPS = [['welcome', 'Welcome'], ['look', 'First look'], ['portfolio', 'Your portfolio'], ['questions', 'Questions'], ['briefing', 'First Briefing'], ['sources', 'More sources']];
21
- /* The sources a first run can bring in. `source` is a lib/freshness.js SOURCES id with a read-only skill. Inbox and Slack
22
- * have no import skill yet, so the page says so instead of offering a button that cannot work. */
23
- const MORE_SOURCES = [
29
+ const STEPS = [['welcome', 'About you'], ['sources', 'Your sources'], ['look', 'First look'], ['portfolio', 'Your portfolio'], ['questions', 'Questions'], ['briefing', 'First Briefing']];
30
+ /* The tools a first run can read besides the folders. `source` is a lib/freshness.js SOURCES id with a read-only skill; `brief` is a
31
+ * lib/proactive.js BRIEF_SOURCES key that the morning brief reads when it is on. */
32
+ const TOOLS = [
24
33
  { source: 'calendar', name: 'Calendar', adds: 'Your meetings today and for the next 14 days, so the Briefing can get you ready for them.' },
25
34
  { source: 'my-work', name: 'Jira', adds: 'The Jira issues assigned to you, so My work starts with them.' },
35
+ { source: 'inbox', brief: 'mail', name: 'Inbox', adds: 'The threads waiting on you, by subject and sender only, in your daily Briefing. No message text is kept.' },
36
+ { source: 'slack', brief: 'slack', name: 'Slack', adds: 'Messages that mention you and your direct messages, as short summaries in your daily Briefing.' },
26
37
  ];
27
38
 
28
39
  const esc = (v) => String(v == null ? '' : v).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
@@ -37,7 +48,7 @@ function counts() {
37
48
  return { n: on.length, m: new Set(on.map(boardOfRow)).size };
38
49
  }
39
50
  function adopt(structure) {
40
- S.structure = structure; S.checked = {}; S.titles = {}; S.boardOf = {}; S.open = {}; S.answers = {}; S.skipped = {};
51
+ S.structure = structure; S.checked = {}; S.titles = {}; S.boardOf = {}; S.open = {}; S.edit = {}; S.answers = {}; S.skipped = {};
41
52
  for (const r of rowsOf()) { S.checked[r.proposal_id] = !!r.checked; S.titles[r.proposal_id] = r.title; }
42
53
  }
43
54
  function revisions() {
@@ -75,7 +86,29 @@ const addFolder = () => run('add', async () => {
75
86
  if (!w || !w.workspace) { fail(w, 'Adding the folder'); return; }
76
87
  if (!S.added.some((a) => a.id === w.workspace.id) && w.workspace.id !== S.ws) S.added.push({ id: w.workspace.id, name: w.workspace.name, root: w.workspace.root });
77
88
  });
89
+ const saveAbout = () => run('about', async () => {
90
+ const a = S.about;
91
+ const r = await pmPost('/api/proactive/settings', { name: a.name.trim().slice(0, 40), about: { role: a.role.trim(), work: a.work.trim(), duties: a.duties.trim() } });
92
+ if (!r || !r.ok) { fail(r, 'Saving what you told PMOS'); return; }
93
+ S.step = 'sources';
94
+ });
95
+ /* Start the ticked tools: each one is its own read-only agent session. A tool that cannot start says why on its row and
96
+ * does not stop the folder scan. */
97
+ async function startTools() {
98
+ const picked = TOOLS.filter((x) => S.pick[x.source]);
99
+ const brief = picked.filter((x) => x.brief);
100
+ if (brief.length) {
101
+ const on = await pmPost('/api/proactive/settings', { briefSources: Object.fromEntries(brief.map((x) => [x.brief, true])) });
102
+ const r = on && on.ok ? await pmPost('/api/proactive/goals/plan', { ws: S.ws }) : on;
103
+ for (const x of brief) S.sources[x.source] = r && r.ok ? { started: true } : { error: (r && r.error) || 'Starting the read did not work.' };
104
+ }
105
+ for (const x of picked.filter((t) => !t.brief)) {
106
+ const r = await pmPost('/api/sources/refresh', { ws: S.ws, source: x.source });
107
+ S.sources[x.source] = r && r.ok ? { started: true } : { error: (r && r.error) || 'Starting the read did not work.' };
108
+ }
109
+ }
78
110
  const scan = () => run('scan', async () => {
111
+ await startTools();
79
112
  const r = await pmPost('/api/onboarding/scan', { ws: S.ws, extra_roots: S.added.map((a) => a.root) });
80
113
  if (!r || !r.ok) { fail(r, 'The scan'); return; }
81
114
  S.look = r.first_look; S.step = 'look';
@@ -101,7 +134,7 @@ const importNow = () => {
101
134
  if ((S.structure.questions || []).length && S.step !== 'questions') { S.step = 'questions'; render(); return; }
102
135
  run('apply', applyNow);
103
136
  };
104
- // "Not now": Welcome stops opening on its own for this folder (`pmos welcome` still opens it), then the console opens
137
+ // "Not now" and "Skip setup": Welcome stops opening on its own for this folder (`pmos welcome` still opens it), then the console opens
105
138
  const notNow = () => run('dismiss', async () => {
106
139
  const r = await pmPost('/api/onboarding/dismiss', { ws: S.ws });
107
140
  if (!r || !r.ok) { fail(r, 'Not now'); return; }
@@ -115,111 +148,172 @@ const undo = () => run('undo', async () => {
115
148
  });
116
149
 
117
150
  /* ── views ───────────────────────────────────────────────────────────────────────────────────────────────────────── */
118
- const spin = (txt) => `<span class="wl-spin" data-testid="loading" aria-hidden="true"></span>${esc(txt)}`;
151
+ /* Drawn with console3 parts (console3.css): .card.sect > .sh2, ul.list > li.nrow (.ki, .nb, .tt, .chips, .acts), .chip2, .tag,
152
+ * .bdg, .fold, .pbar, .xp. Buttons and checkboxes are the primitives (primitives.js button() and field()): a view writes a
153
+ * placeholder through ctl() and render() puts the primitive in its place. The .wl- rules in components.css add only the
154
+ * frame, the progress labels, the footer bar and the field blocks (the approved console3 mockup's Welcome page). Each step
155
+ * view returns [body, footer]. */
156
+ const ICONS = { folder: 'repo', board: 'cols', tools: 'org' };
157
+ const ic = (n, cls) => `<svg class="ic${cls ? ' ' + cls : ''}" aria-hidden="true"><use href="#c3i-${ICONS[n] || n}"></use></svg>`;
158
+ const spin = (txt) => `<span class="wl-spindot" data-testid="loading" aria-hidden="true"></span>${esc(txt)}`;
159
+ const dis = (on) => (on ? 'disabled' : '');
160
+ /* Controls drawn this render: ctl(spec) returns a placeholder, mountControls() swaps each for its primitive.
161
+ * spec: { variant, size, html (the label, already escaped), attrs, href, lead (a console3 icon) } or { check, text, value, attrs }. */
162
+ let CTL = [];
163
+ const ctl = (spec) => { CTL.push(spec); return `<span data-wl-ctl="${CTL.length - 1}"></span>`; };
164
+ function mountControls() {
165
+ for (const ph of root.querySelectorAll('[data-wl-ctl]')) {
166
+ const c = CTL[Number(ph.getAttribute('data-wl-ctl'))];
167
+ let node;
168
+ if (c.check) node = field({ label: c.text, kind: 'check', value: !!c.value, attrs: c.attrs });
169
+ else {
170
+ const { disabled, ...attrs } = c.attrs; // button() takes disabled on its own; a key in attrs would override it
171
+ const b = button({ label: '', variant: c.variant, size: c.size, disabled: !!disabled, attrs });
172
+ b.querySelector(SEL.btnLabel).innerHTML = c.html;
173
+ if (c.lead) b.insertAdjacentHTML('afterbegin', ic(c.lead, 'sm'));
174
+ if (c.href) { node = el('a', { ...attrs, class: b.className, href: c.href }); node.append(...b.childNodes); } else node = b;
175
+ }
176
+ ph.replaceWith(node);
177
+ }
178
+ }
179
+ const busyAttrs = (key) => (S.busy === key ? { 'aria-busy': 'true' } : {});
180
+ const primary = (id, text, busyKey, busyText, off) => ctl({ variant: 'primary', html: S.busy === busyKey ? spin(busyText) : esc(text), attrs: { class: 'wl-primary', 'data-testid': id, disabled: !!(S.busy || off), ...busyAttrs(busyKey) } });
181
+ const ghost = (id, text, more) => ctl({ variant: 'ghost', html: esc(text), attrs: { 'data-testid': id, disabled: !!S.busy, ...(more || {}) } });
182
+ const head = (h, lead, leadId) => `<header class="wl-head"><h1>${esc(h)}</h1>${lead ? `<p class="wl-lead"${leadId ? ` data-testid="${leadId}"` : ''}>${esc(lead)}</p>` : ''}</header>`;
183
+ const sub = (title, icon, right, count) => `<div class="sh2"><h2>${icon ? ic(icon, 'sm') : ''}${title}${count != null ? ` <span class="bdg g">${count}</span>` : ''}</h2>${right || ''}</div>`;
184
+ const pathChip = (t, attrs) => `<span class="chip2"${attrs ? ' ' + attrs : ''}>${ic('doc', 'sm')}${esc(t)}</span>`;
185
+ const foot = (left, right) => `<footer class="wl-foot"><div>${left || ''}</div><div>${right}</div></footer>`;
186
+ const backBtn = () => ghost('back-button', 'Back');
119
187
 
120
188
  function viewWelcome() {
189
+ const a = S.about;
190
+ const field = (k, text, hint, max) => `<div class="wl-fld"><label class="wl-flabel" for="about-${k}">${text}</label><input type="text" id="about-${k}" data-testid="about-${k}" maxlength="${max}" value="${esc(a[k])}" placeholder="${esc(hint)}" autocomplete="off"></div>`;
191
+ return [head('Welcome to PMOS', 'Tell PMOS who you are, so your Briefing knows what matters to you. Every field is optional, and you can change them later in Settings.')
192
+ + field('name', 'Your name', 'For example: Dana', 40)
193
+ + field('role', 'Your role', 'For example: Senior PM, Payments', 80)
194
+ + field('work', 'What you do', 'In one line, for example: I run checkout and the payment methods', 200)
195
+ + `<div class="wl-fld"><label class="wl-flabel" for="about-duties">Your job description</label><textarea id="about-duties" data-testid="about-duties" maxlength="1000" rows="5" placeholder="What you own, decide and deliver. Paste it from your job description if you like.">${esc(a.duties)}</textarea></div>`,
196
+ foot('', primary('about-continue', 'Continue', 'about', 'Saving'))];
197
+ }
198
+
199
+ function toolRow(x) {
200
+ const st = S.sources[x.source] || {};
201
+ return `<div class="wl-tool" data-testid="source-row" data-id="${esc(x.source)}">${ctl({ check: true, text: x.name, value: S.pick[x.source], attrs: { 'data-testid': 'source-check', 'data-id': x.source, disabled: !!S.busy } })}
202
+ <span class="dim">${esc(x.adds)}</span>${st.error ? `<span class="wl-err" role="alert">${esc(st.error)}</span>` : ''}</div>`;
203
+ }
204
+ function viewSources() {
121
205
  const f = S.folder || { name: '', root: '' };
122
- const added = S.added.length ? `<ul class="wl-added" data-testid="added-folders">${S.added.map((a) => `<li data-testid="added-folder">${esc(a.name)} <span class="wl-path">${esc(a.root)}</span></li>`).join('')}</ul>` : '';
123
- return `<h1 class="wl-h1">Welcome to PMOS</h1>
124
- <p class="wl-p wl-muted">PMOS reads your folder and shows you what it found: your initiatives, your documents and what needs you first.</p>
125
- <section class="wl-card"><h2 class="wl-h2" data-testid="folder-name">${esc(f.name || 'This folder')}</h2><div class="wl-path" data-testid="folder-path">${esc(f.root)}</div>${added}
126
- <div class="wl-actions"><button type="button" class="wl-btn" data-testid="add-folder" ${S.busy ? 'disabled' : ''}>${S.busy === 'add' ? spin('Waiting for the chooser') : 'Add another folder'}</button></div>
127
- ${S.note ? `<p class="wl-p wl-muted" data-testid="note">${esc(S.note)}</p>` : ''}</section>
128
- <p class="wl-promise" data-testid="promise">Reads only. Writes nothing in your folder. Nothing leaves this computer.</p>
129
- <div class="wl-actions"><button type="button" class="wl-btn wl-primary" data-testid="scan-button" ${S.busy ? 'disabled' : ''}>${S.busy === 'scan' ? spin('Scanning your folder') : 'Scan'}</button>
130
- <button type="button" class="wl-btn wl-quiet" data-testid="not-now" ${S.busy ? 'disabled' : ''}>Not now</button></div>
131
- ${S.busy === 'scan' ? '<p class="wl-p wl-muted" data-testid="scan-wait">A large folder can take a minute or two. PMOS keeps working while it reads.</p>' : ''}`;
206
+ const n = TOOLS.filter((x) => S.pick[x.source]).length;
207
+ const added = S.added.map((a) => `<li class="nrow" data-testid="added-folder"><span class="ki">${ic('folder')}</span><div class="nb"><span class="tt">${esc(a.name)}</span><div class="chips"><span class="chip2">${esc(a.root)}</span></div></div><span></span></li>`).join('');
208
+ const folders = `<ul class="list" data-testid="added-folders"><li class="nrow"><span class="ki">${ic('folder')}</span><div class="nb"><span class="tt" data-testid="folder-name">${esc(f.name || 'This folder')}</span><div class="chips"><span class="chip2" data-testid="folder-path">${esc(f.root)}</span></div></div><div class="acts"><span class="tag">this folder</span></div></li>${added}</ul>`;
209
+ const addBtn = `<div>${ctl({ variant: 'outline', size: 'sm', lead: S.busy === 'add' ? '' : 'plus', html: S.busy === 'add' ? spin('Waiting for the chooser') : 'Add another folder', attrs: { 'data-testid': 'add-folder', disabled: !!S.busy, ...busyAttrs('add') } })}</div>`;
210
+ return [head('Where does your work live?', 'PMOS reads your folders and the tools you tick, then shows you your initiatives, your documents and what needs you first.')
211
+ + sub('Folders', 'folder', '', 1 + S.added.length) + folders + addBtn
212
+ + (S.note ? `<div class="fold" data-testid="note">${esc(S.note)}</div>` : '')
213
+ + sub('Tools', 'tools') + TOOLS.map(toolRow).join('')
214
+ + `<p class="dim wl-small" data-testid="promise">${ic('lock', 'sm')} Reads only. Writes nothing in your folders or your tools. PMOS sends nothing out: your agent reads the tools you tick with your own connection.</p>`
215
+ + (S.busy === 'scan' ? '<p class="dim wl-small" data-testid="scan-wait">A large folder can take a minute or two. The rest of PMOS keeps working.</p>' : ''),
216
+ foot(backBtn(), primary('scan-button', n ? `Scan the folder and read ${plural(n, 'tool', 'tools')}` : 'Scan', 'scan', 'Scanning your folder'))];
132
217
  }
133
218
 
219
+ /* A numbered list (the number is drawn in the .ki by a CSS counter), one row per finding or Briefing item. */
220
+ const numbered = (rows) => `<ol class="list wl-num" role="list">${rows}</ol>`;
134
221
  function viewLook() {
135
222
  const L = S.look || {};
136
- const items = (L.findings || []).slice(0, 10).map((f) => `<li data-testid="finding"><strong>${esc(f.title)}</strong>${f.why ? `<div class="wl-muted">${esc(f.why)}</div>` : ''}<div class="wl-ref" data-testid="finding-ref">${esc(ref((f.evidence || [])[0]))}</div></li>`).join('');
137
- return `<h1 class="wl-h1">First look</h1>
138
- <p class="wl-p" data-testid="first-look-sentence">${esc(L.sentence)}</p>
139
- <section class="wl-card"><h2 class="wl-h2">What matters most</h2>${items ? `<ol class="wl-findings">${items}</ol>` : '<p class="wl-p wl-muted" data-testid="no-findings">Nothing in this folder needs you urgently.</p>'}</section>
140
- <div class="wl-actions"><button type="button" class="wl-btn wl-primary" data-testid="continue-button" ${S.busy ? 'disabled' : ''}>${S.busy === 'structure' ? spin('Building your portfolio') : 'Continue'}</button></div>`;
223
+ const found = (L.findings || []).slice(0, 10);
224
+ const rows = found.map((f) => `<li class="nrow" data-testid="finding"><span class="ki"></span><div class="nb"><span class="tt"><b>${esc(f.title)}</b></span>${f.why ? `<div class="dim wl-small">${esc(f.why)}</div>` : ''}<div class="chips">${pathChip(ref((f.evidence || [])[0]), 'data-testid="finding-ref"')}</div></div><span></span></li>`).join('');
225
+ return [head('First look', L.sentence, 'first-look-sentence')
226
+ + sub('What matters most', 'spark', '', found.length)
227
+ + (rows ? numbered(rows) : '<div class="fold" data-testid="no-findings">Nothing in this folder needs you urgently.</div>'),
228
+ foot(backBtn(), primary('continue-button', 'Continue', 'structure', 'Building your portfolio'))];
141
229
  }
142
230
 
143
231
  function rowView(r) {
144
- const id = r.proposal_id; const docs = r.docs || []; const open = !!S.open[id];
232
+ const id = r.proposal_id; const docs = r.docs || []; const open = !!S.open[id]; const ed = !!S.edit[id]; const on = !!S.checked[id];
233
+ const t = S.titles[id];
145
234
  const opts = S.structure.boards.map((b) => `<option value="${esc(b.key)}" ${b.key === boardOfRow(r) ? 'selected' : ''}>${esc(b.name)}</option>`).join('');
146
- return `<div class="wl-row" data-testid="row" data-id="${esc(id)}">
147
- <input type="checkbox" class="wl-check" data-testid="row-check" data-id="${esc(id)}" aria-label="Import ${esc(S.titles[id])}" ${S.checked[id] ? 'checked' : ''} />
148
- <div><input type="text" class="wl-input" data-testid="row-title" data-id="${esc(id)}" value="${esc(S.titles[id])}" aria-label="Title" />
149
- <div class="wl-meta"><span class="wl-chip" data-testid="row-lane">${esc(r.lane)}</span><span class="wl-chip" data-testid="row-anchor">${esc(r.anchor || 'no anchor')}</span><span class="wl-chip" data-testid="row-stage">${esc(r.stage)}</span>
150
- <button type="button" class="wl-btn wl-small" data-testid="row-docs-toggle" data-id="${esc(id)}" aria-expanded="${open}">${plural(docs.length, 'document', 'documents')}</button>
151
- <select class="wl-input" data-testid="row-board" data-id="${esc(id)}" aria-label="Board">${opts}</select></div>
152
- ${open ? `<ul class="wl-docs" data-testid="row-docs">${docs.map((d) => `<li>${esc(d.title && d.title !== d.file ? d.title + ' · ' : '')}${esc(d.file)}</li>`).join('')}</ul>` : ''}</div></div>`;
235
+ return `<li class="nrow${on ? '' : ' wl-off'}" data-testid="row" data-id="${esc(id)}"><span class="wl-cb">${ctl({ check: true, text: '', value: on, attrs: { 'data-testid': 'row-check', 'data-id': id, 'aria-label': 'Import ' + t } })}</span>
236
+ <div class="nb"><span class="tt">${esc(t)}</span>
237
+ <div class="chips"><span class="chip2" data-testid="row-lane">${ic('init', 'sm')}${esc(r.lane)}</span><span class="chip2" data-testid="row-anchor">${ic('target', 'sm')}${esc(r.anchor || 'no anchor')}</span><span class="tag" data-testid="row-stage">${esc(r.stage)}</span><button type="button" class="chip2 lk" data-testid="row-docs-toggle" data-id="${esc(id)}" aria-expanded="${open}">${ic('doc', 'sm')}${plural(docs.length, 'document', 'documents')}</button></div>
238
+ ${open ? `<ul class="wl-docs" data-testid="row-docs">${docs.map((d) => `<li>${pathChip((d.title && d.title !== d.file ? d.title + ' · ' : '') + d.file)}</li>`).join('')}</ul>` : ''}
239
+ <div class="xp${ed ? ' open' : ''}"><div class="xi"><div class="wl-edit"><div class="wl-fld"><label class="wl-flabel" for="wt-${esc(id)}">Title</label><input type="text" id="wt-${esc(id)}" data-testid="row-title" data-id="${esc(id)}" value="${esc(t)}"></div>
240
+ <div class="wl-fld"><label class="wl-flabel" for="wb-${esc(id)}">Board</label><select id="wb-${esc(id)}" data-testid="row-board" data-id="${esc(id)}">${opts}</select></div></div></div></div></div>
241
+ <div class="acts">${ctl({ variant: 'ghost', size: 'sm', html: ed ? 'Done' : 'Edit', attrs: { 'data-testid': 'row-edit', 'data-id': id, 'aria-expanded': String(ed), disabled: !on } })}</div></li>`;
153
242
  }
154
-
243
+ const importText = () => { const c = counts(); return `Import ${plural(c.n, 'initiative', 'initiatives')} into ${plural(c.m, 'board', 'boards')}`; };
244
+ const hasQuestions = () => !!(S.structure && (S.structure.questions || []).length);
155
245
  function viewPortfolio() {
156
246
  const all = rowsOf(); const c = counts();
157
247
  const boards = S.structure.boards.map((b) => {
158
248
  const mine = all.filter((r) => boardOfRow(r) === b.key);
159
- return `<section class="wl-card wl-board" data-testid="board" data-board="${esc(b.key)}"><header><h2 class="wl-h2" data-testid="board-name">${esc(b.name)}</h2>
160
- <span class="wl-muted" data-testid="unsorted-count">${plural((b.unsorted || []).length, 'document', 'documents')} unsorted</span></header>
161
- ${mine.length ? mine.map(rowView).join('') : '<p class="wl-p wl-muted">No initiatives here.</p>'}</section>`;
249
+ return `<div class="wl-board" data-testid="board" data-board="${esc(b.key)}">${sub(`<span data-testid="board-name">${esc(b.name)}</span>`, 'board', `<span class="chip2" data-testid="unsorted-count">${plural((b.unsorted || []).length, 'document', 'documents')} unsorted</span>`)}
250
+ ${mine.length ? `<ul class="list">${mine.map(rowView).join('')}</ul>` : '<div class="fold">No initiatives here.</div>'}</div>`;
162
251
  }).join('');
163
- return `<h1 class="wl-h1">Your portfolio</h1>
164
- <p class="wl-p wl-muted">This is how PMOS would set up your work. Untick what you do not want, rename it, or move it to another board.</p>
165
- ${boards}
166
- <div class="wl-actions"><button type="button" class="wl-btn wl-primary" data-testid="import-button" ${S.busy || !c.n ? 'disabled' : ''}>${S.busy === 'apply' ? spin('Importing') : `Import ${plural(c.n, 'initiative', 'initiatives')} into ${plural(c.m, 'board', 'boards')}`}</button></div>`;
252
+ const q = hasQuestions();
253
+ return [head('Your portfolio', 'This is how PMOS would set up your work. Untick what you do not want, rename it, or move it to another board.') + boards
254
+ + (q && c.n ? `<p class="dim wl-small" data-testid="import-summary">${importText()} after a few questions.</p>` : ''),
255
+ foot(backBtn(), primary('import-button', q ? 'Continue' : importText(), 'apply', 'Importing', !c.n))];
167
256
  }
168
257
 
169
258
  function viewQuestions() {
170
- const qs = (S.structure.questions || []).map((q) => {
259
+ const qs = (S.structure.questions || []).map((q, n) => {
171
260
  const skipped = !!S.skipped[q.id];
172
261
  const cur = S.answers[q.id] != null ? S.answers[q.id] : q.default;
173
- const input = (q.options && q.options.length)
174
- ? (q.options.length > 4 && !q.free
175
- ? `<select class="wl-input" data-testid="question-select" data-id="${esc(q.id)}" ${skipped ? 'disabled' : ''}>${q.options.map((o) => { const v = typeof o === 'object' ? o.value : o; const t = typeof o === 'object' ? (o.label || o.value) : o; return `<option value="${esc(v)}" ${String(cur) === String(v) ? 'selected' : ''}>${esc(t)}</option>`; }).join('')}</select>`
176
- : q.options.map((o) => { const v = typeof o === 'object' ? o.value : o; const t = typeof o === 'object' ? (o.label || o.value) : o; return `<label class="wl-label"><input type="radio" name="q-${esc(q.id)}" data-testid="question-option" data-id="${esc(q.id)}" value="${esc(v)}" ${String(cur) === String(v) ? 'checked' : ''} ${skipped ? 'disabled' : ''} /> ${esc(t)}</label>`; }).join('')
177
- + (q.free ? `<label class="wl-label wl-free">${esc(q.free)} <input type="text" class="wl-input" data-testid="question-input" data-id="${esc(q.id)}" placeholder="KR-A1 or health:upkeep" value="${q.options.some((o) => String(typeof o === 'object' ? o.value : o) === String(cur)) ? '' : esc(cur)}" ${skipped ? 'disabled' : ''} /></label>` : ''))
178
- : `<input type="text" class="wl-input" data-testid="question-input" data-id="${esc(q.id)}" value="${esc(cur)}" ${skipped ? 'disabled' : ''} />`;
179
- return `<section class="wl-card wl-q" data-testid="question" data-id="${esc(q.id)}"><p class="wl-p"><strong>${esc(q.text)}</strong></p>${input}
180
- ${q.options && q.options.length ? '' : `<p class="wl-p wl-muted">Default: ${esc(q.default)}</p>`}<button type="button" class="wl-btn wl-small" data-testid="question-skip" data-id="${esc(q.id)}" aria-pressed="${skipped}">${skipped ? 'Skipped. Use the default' : 'Skip'}</button></section>`;
262
+ const off = skipped ? 'disabled' : '';
263
+ const val = (o) => (typeof o === 'object' ? o.value : o); const txt = (o) => (typeof o === 'object' ? (o.label || o.value) : o);
264
+ let input;
265
+ if (q.options && q.options.length > 4 && !q.free) input = `<select aria-label="${esc(q.text)}" data-testid="question-select" data-id="${esc(q.id)}" ${off}>${q.options.map((o) => `<option value="${esc(val(o))}" ${String(cur) === String(val(o)) ? 'selected' : ''}>${esc(txt(o))}</option>`).join('')}</select>`;
266
+ else if (q.options && q.options.length) {
267
+ const other = q.options.some((o) => String(val(o)) === String(cur)) ? '' : cur;
268
+ input = `<div role="radiogroup" aria-label="${esc(q.text)}">${q.options.map((o) => `<label class="wl-opt"><input type="radio" name="q-${esc(q.id)}" data-testid="question-option" data-id="${esc(q.id)}" value="${esc(val(o))}" ${String(cur) === String(val(o)) ? 'checked' : ''} ${off}>${esc(txt(o))}</label>`).join('')}
269
+ ${q.free ? `<label class="wl-opt">${esc(q.free)} <input type="text" data-testid="question-input" data-id="${esc(q.id)}" placeholder="KR-A1 or health:upkeep" value="${esc(other)}" ${off}></label>` : ''}</div>`;
270
+ } else input = `<input type="text" aria-label="${esc(q.text)}" data-testid="question-input" data-id="${esc(q.id)}" value="${esc(cur)}" ${off}><p class="dim wl-small">Default: ${esc(q.default)}</p>`;
271
+ return `<div class="wl-q" data-testid="question" data-id="${esc(q.id)}">${sub(`Question ${n + 1}`, 'info', ctl({ variant: 'ghost', size: 'sm', html: skipped ? 'Skipped. Use the default' : 'Skip', attrs: { 'data-testid': 'question-skip', 'data-id': q.id, 'aria-pressed': String(skipped), disabled: !!S.busy } }))}
272
+ <b class="wl-qt">${esc(q.text)}</b>${input}</div>`;
181
273
  }).join('');
182
- const c = counts();
183
- return `<h1 class="wl-h1">A few questions</h1><p class="wl-p wl-muted">The scan could not decide these. Each has a default, and you can skip any of them.</p>${qs}
184
- <div class="wl-actions"><button type="button" class="wl-btn wl-primary" data-testid="questions-continue" ${S.busy ? 'disabled' : ''}>${S.busy === 'apply' ? spin('Importing') : `Import ${plural(c.n, 'initiative', 'initiatives')} into ${plural(c.m, 'board', 'boards')}`}</button></div>`;
274
+ return [head('A few questions', 'The scan could not decide these. Each has a default, and you can skip any of them.') + qs,
275
+ foot(backBtn(), primary('questions-continue', importText(), 'apply', 'Importing'))];
185
276
  }
186
277
 
187
278
  function viewBriefing() {
188
- const items = S.briefing.map((b) => `<li data-testid="briefing-item"><strong>${esc(b.title)}</strong>${b.why ? `<div class="wl-muted">${esc(b.why)}</div>` : ''}${b.initiative ? `<div class="wl-muted" data-testid="briefing-initiative">${esc(b.initiative.title)} · board ${esc(b.initiative.board)}</div>` : ''}<div class="wl-ref">${esc(ref((b.evidence || [])[0]))}</div></li>`).join('');
189
- return `<h1 class="wl-h1">Your first Briefing</h1>
190
- <p class="wl-p wl-muted">${S.briefing.length ? 'These need you first.' : 'Nothing needs you right now.'}</p>
191
- <section class="wl-card">${items ? `<ol class="wl-findings">${items}</ol>` : '<p class="wl-p wl-muted" data-testid="no-briefing">Your portfolio is in. Open PMOS to see it.</p>'}</section>
192
- <div class="wl-actions"><button type="button" class="wl-btn wl-primary" data-testid="sources-continue">Continue</button>
193
- <button type="button" class="wl-btn wl-quiet" data-testid="undo-link" ${S.busy ? 'disabled' : ''}>${S.busy === 'undo' ? spin('Undoing') : 'Undo this import'}</button></div>`;
194
- }
195
-
196
- function sourceRow(x) {
197
- const st = S.sources[x.source] || {};
198
- const busy = S.busy === 'source:' + x.source;
199
- const state = st.started ? '<span class="wl-muted" data-testid="source-started">Started in an agent session. Follow it in PMOS.</span>'
200
- : st.skipped ? '<span class="wl-muted" data-testid="source-skipped">Skipped. You can bring it in later from Settings.</span>'
201
- : `<button type="button" class="wl-btn" data-testid="source-bring" data-id="${esc(x.source)}" ${S.busy ? 'disabled' : ''}>${busy ? spin('Starting') : 'Bring in'}</button>
202
- <button type="button" class="wl-btn wl-quiet" data-testid="source-skip" data-id="${esc(x.source)}" ${S.busy ? 'disabled' : ''}>Skip</button>`;
203
- return `<li class="wl-source" data-testid="source-row" data-id="${esc(x.source)}"><div class="wl-source-text"><strong>${esc(x.name)}</strong><div class="wl-muted">${esc(x.adds)}</div>${st.error ? `<div class="wl-error" role="alert">${esc(st.error)}</div>` : ''}</div><div class="wl-source-actions">${state}</div></li>`;
204
- }
205
- function viewSources() {
206
- return `<h1 class="wl-h1">Bring in more sources</h1>
207
- <p class="wl-p wl-muted">Your folder is in. These sources are optional. Each one runs in an agent session with your own connection, reads only, and writes nothing to the tool. Nothing is read until you press Bring in.</p>
208
- <section class="wl-card"><ul class="wl-sources">${MORE_SOURCES.map(sourceRow).join('')}</ul>
209
- <p class="wl-p wl-muted" data-testid="sources-not-yet">Inbox and Slack have no import yet.</p></section>
210
- <div class="wl-actions"><a class="wl-btn wl-primary" data-testid="open-pmos" href="/c3.html?ws=${encodeURIComponent(S.ws)}">Open PMOS</a></div>`;
279
+ const items = S.briefing.map((b) => `<li class="nrow" data-testid="briefing-item"><span class="ki"></span><div class="nb"><span class="tt"><b>${esc(b.title)}</b></span>${b.why ? `<div class="dim wl-small">${esc(b.why)}</div>` : ''}<div class="chips">${b.initiative ? `<span class="chip2" data-testid="briefing-initiative">${esc(b.initiative.title)} · board ${esc(b.initiative.board)}</span>` : ''}${pathChip(ref((b.evidence || [])[0]))}</div></div><span></span></li>`).join('');
280
+ const tools = TOOLS.filter((x) => S.sources[x.source]).map((x) => { const st = S.sources[x.source]; const bad = !st.started;
281
+ return `<li class="nrow" data-testid="source-status" data-id="${esc(x.source)}"><span class="ki${bad ? ' hot' : ''}">${ic(bad ? 'alert' : 'clock')}</span><div class="nb"><span class="tt">${esc(x.name)}</span><div class="dim wl-small${bad ? ' wl-bad' : ''}">${bad ? esc(st.error || '') : `${esc(x.name)} is being read in an agent session. Its items join your Briefing in PMOS when it finishes.`}</div></div><span></span></li>`; });
282
+ return [head('Your first Briefing', S.briefing.length ? 'These need you first.' : 'Nothing needs you right now.')
283
+ + sub('Briefing', 'sun', '', S.briefing.length)
284
+ + (items ? numbered(items) : '<div class="fold" data-testid="no-briefing">Your portfolio is in. Open PMOS to see it.</div>')
285
+ + (tools.length ? sub('Your tools', 'tools', '', tools.length) + `<ul class="list">${tools.join('')}</ul>` : ''),
286
+ foot(ctl({ variant: 'ghost', html: S.busy === 'undo' ? spin('Undoing') : 'Undo this import', attrs: { 'data-testid': 'undo-link', disabled: !!S.busy, ...busyAttrs('undo') } }),
287
+ ctl({ variant: 'primary', html: 'Open PMOS', href: '/c3.html?ws=' + encodeURIComponent(S.ws), attrs: { class: 'wl-primary', 'data-testid': 'open-pmos' } }))];
211
288
  }
212
289
 
213
290
  const VIEWS = { welcome: viewWelcome, look: viewLook, portfolio: viewPortfolio, questions: viewQuestions, briefing: viewBriefing, sources: viewSources };
291
+ const shownSteps = () => STEPS.filter(([k]) => k !== 'questions' || S.step === 'questions' || hasQuestions());
292
+
293
+ /* The frame: PMOS, the step count and Not now (About you) or Skip setup (until the import); a progress bar and the step names
294
+ * (a done step goes back to that step, except once the import is made: Undo is the way back then); one step card. */
295
+ function frame(body, footer) {
296
+ const l = shownSteps(); const at = Math.max(0, l.findIndex(([k]) => k === S.step));
297
+ const back = S.step !== 'briefing';
298
+ const steps = l.map(([k, t], n) => `<li><button type="button" class="wl-stepbtn${n < at ? ' wl-done' : ''}" data-testid="step" data-step="${k}" ${n === at ? 'aria-current="step"' : ''} ${n < at && back && !S.busy ? '' : 'disabled'}>${n < at ? ic('check', 'sm') : ''}${esc(t)}</button></li>`).join('');
299
+ const skip = S.step === 'briefing' ? '' : S.step === 'welcome' ? ghost('not-now', 'Not now') : ghost('skip-setup', 'Skip setup');
300
+ return `<div class="wl-top"><div><h1>PMOS</h1><div class="wl-sub" data-testid="step-count">Set up · step ${at + 1} of ${l.length}</div></div><div class="wl-tools">${skip}</div></div>
301
+ <div class="wl-col"><nav class="wl-prog" aria-label="Setup steps"><div class="pbar" aria-hidden="true"><i class="wl-fill"></i></div><ol class="wl-steps" data-testid="steps">${steps}</ol></nav>
302
+ ${S.error ? `<div class="fold wl-e" role="alert" data-testid="error">${ic('alert', 'sm')}${esc(S.error)}</div>` : ''}
303
+ <section class="card sect wl-card">${body}${footer}</section></div>`;
304
+ }
214
305
 
215
306
  function render() {
216
307
  const keepScroll = window.scrollY;
217
- const stepsBar = `<ol class="wl-steps" data-testid="steps">${STEPS.filter(([k]) => k !== 'questions' || S.step === 'questions' || (S.structure && (S.structure.questions || []).length))
218
- .map(([k, t]) => `<li ${k === S.step ? 'aria-current="step"' : ''} data-step="${k}">${t}</li>`).join('')}</ol>`;
219
308
  const a = document.activeElement;
220
309
  const was = a && root.contains(a) ? [a.getAttribute('data-testid'), a.getAttribute('data-id')] : null;
221
310
  root.setAttribute('data-step', S.step);
222
- root.innerHTML = stepsBar + (S.error ? `<div class="wl-error" role="alert" data-testid="error">${esc(S.error)}</div>` : '') + VIEWS[S.step]();
311
+ CTL = [];
312
+ const [body, footer] = VIEWS[S.step]();
313
+ root.innerHTML = frame(body, footer);
314
+ mountControls();
315
+ const l = shownSteps(); const at = Math.max(0, l.findIndex(([k]) => k === S.step));
316
+ const fill = root.querySelector('.wl-fill'); if (fill) fill.style.setProperty('--wl-prog', Math.round(((at + 1) / l.length) * 100) + '%');
223
317
  window.scrollTo(0, keepScroll);
224
318
  if (was && was[0]) { // keep the keyboard where it was after a re-render
225
319
  const again = [...root.querySelectorAll('[data-testid="' + was[0] + '"]')].find((el) => (el.getAttribute('data-id') || null) === (was[1] || null));
@@ -229,10 +323,6 @@ function render() {
229
323
 
230
324
  /* ── events (one delegate; the markup is rebuilt on each render) ─────────────────────────────────────────────────── */
231
325
  const idOf = (el) => el.getAttribute('data-id');
232
- const bringIn = (source) => run('source:' + source, async () => {
233
- const r = await pmPost('/api/sources/refresh', { ws: S.ws, source });
234
- S.sources[source] = r && r.ok ? { started: true } : { error: (r && r.error) || 'Starting the import did not work.' };
235
- });
236
326
  document.addEventListener('click', (e) => {
237
327
  if (!root.contains(e.target)) return;
238
328
  const t = e.target.closest('[data-testid]'); if (!t) return;
@@ -242,22 +332,26 @@ document.addEventListener('click', (e) => {
242
332
  else if (k === 'continue-button') toPortfolio();
243
333
  else if (k === 'import-button' || k === 'questions-continue') { if (k === 'questions-continue') run('apply', applyNow); else importNow(); }
244
334
  else if (k === 'undo-link') undo();
245
- else if (k === 'sources-continue') { S.step = 'sources'; S.error = ''; render(); }
246
- else if (k === 'source-bring') bringIn(idOf(t));
247
- else if (k === 'source-skip') { S.sources[idOf(t)] = { skipped: true }; render(); }
248
- else if (k === 'not-now') notNow();
335
+ else if (k === 'about-continue') saveAbout();
336
+ else if (k === 'back-button') { const l = shownSteps().map(([s]) => s); S.step = l[l.indexOf(S.step) - 1] || 'welcome'; S.error = ''; render(); }
337
+ else if (k === 'step') { if (!t.disabled) { S.step = t.getAttribute('data-step'); S.error = ''; render(); } }
338
+ else if (k === 'not-now' || k === 'skip-setup') notNow();
339
+ else if (k === 'row-edit') { S.edit[idOf(t)] = !S.edit[idOf(t)]; render(); }
249
340
  else if (k === 'row-docs-toggle') { S.open[idOf(t)] = !S.open[idOf(t)]; render(); }
250
341
  else if (k === 'question-skip') { const id = idOf(t); S.skipped[id] = !S.skipped[id]; render(); }
251
342
  });
252
343
  root.addEventListener('change', (e) => {
253
344
  const t = e.target; const k = t.getAttribute && t.getAttribute('data-testid');
254
- if (k === 'row-check') { S.checked[idOf(t)] = t.checked; render(); }
345
+ if (k === 'row-check') { S.checked[idOf(t)] = t.checked; if (!t.checked) S.edit[idOf(t)] = false; render(); }
346
+ else if (k === 'row-title') render();
347
+ else if (k === 'source-check') { S.pick[idOf(t)] = t.checked; render(); }
255
348
  else if (k === 'row-board') { S.boardOf[idOf(t)] = t.value; render(); }
256
349
  else if (k === 'question-option' || k === 'question-select') S.answers[idOf(t)] = t.value;
257
350
  });
258
351
  root.addEventListener('input', (e) => {
259
352
  const t = e.target; const k = t.getAttribute && t.getAttribute('data-testid');
260
353
  if (k === 'row-title') S.titles[idOf(t)] = t.value;
354
+ else if (k && k.startsWith('about-')) S.about[k.slice(6)] = t.value;
261
355
  else if (k === 'question-input') {
262
356
  const id = idOf(t); const q = ((S.structure && S.structure.questions) || []).find((x) => x.id === id);
263
357
  S.answers[id] = t.value.trim() ? t.value : (q ? q.default : t.value); // an emptied free answer goes back to the default
@@ -265,13 +359,27 @@ root.addEventListener('input', (e) => {
265
359
  });
266
360
 
267
361
  /* ── start ───────────────────────────────────────────────────────────────────────────────────────────────────────── */
362
+ /* The console3 look: its stylesheet and icon sprite, the PM's console theme, and the time-of-day tint on the page root. */
363
+ function look() {
364
+ ensureC3Css();
365
+ const page = document.getElementById('wl-c3');
366
+ let theme = params.get('theme');
367
+ try { theme = theme || localStorage.getItem('pmos.c3.theme'); } catch { /* no storage */ }
368
+ theme = theme === 'dark' ? 'dark' : 'light';
369
+ document.documentElement.setAttribute('data-pm-page-theme', theme);
370
+ if (page) { page.setAttribute('data-pm-theme', theme); applyDayPart(page); setInterval(() => applyDayPart(page), 5 * 60 * 1000); }
371
+ }
372
+
268
373
  async function start() {
374
+ look();
269
375
  render();
270
376
  const list = await pmGet('/api/workspaces');
271
377
  const all = (list && list.workspaces) || [];
272
378
  const mine = all.find((w) => w.id === S.ws) || (!S.ws ? all[0] : null);
273
379
  if (!mine) { S.error = 'This folder is not open in PMOS. Start pmos in the folder, then reload.'; render(); return; }
274
380
  S.ws = mine.id; S.folder = { name: mine.name, root: mine.root };
381
+ const ps = await pmGet('/api/proactive/settings');
382
+ if (ps && ps.ok && ps.settings) { const ab = ps.settings.about || {}; S.about = { name: ps.settings.name || '', role: ab.role || '', work: ab.work || '', duties: ab.duties || '' }; }
275
383
  const st = await pmGet(`/api/onboarding/status?ws=${encodeURIComponent(S.ws)}`);
276
384
  if (st && st.ok && st.state === 'imported') {
277
385
  const live = (st.imports || []).filter((i) => !i.retracted).pop();
@@ -1,20 +1,22 @@
1
1
  <!doctype html>
2
- <html lang="en" class="wl-page" data-pm-page-theme="light">
2
+ <html lang="en" class="wl-html" data-pm-page-theme="light">
3
3
  <head>
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
6
6
  <meta name="pmos-nonce" content="" />
7
7
  <title>Welcome to PMOS</title>
8
8
  <!-- The first-run page. The server fills the nonce meta the way it does for /c3.html, so a press (Scan, Import, Undo)
9
- carries it. Tokens and fonts are the console's, so light and dark both resolve. The page does not load the console. -->
9
+ carries it. Tokens and fonts are the console's, so light and dark both resolve. The page does not load the console:
10
+ pm/welcome.js links console3.css and its icon sprite and draws inside the same .c3 .c3pg root as /c3.html. -->
10
11
  <link rel="preload" href="/vendor/ibm-plex-mono-400.woff2" as="font" type="font/woff2" crossorigin />
12
+ <link rel="preload" href="/vendor/ibm-plex-mono-500.woff2" as="font" type="font/woff2" crossorigin />
11
13
  <link rel="stylesheet" href="/pm/tokens.css" />
12
14
  <link rel="stylesheet" href="/pm/components.css" />
13
15
 
14
16
  </head>
15
17
  <body class="wl-body">
16
- <main id="wl-root" class="wl-wrap" data-testid="welcome-root"></main>
17
- <noscript><p class="wl-wrap">This page needs JavaScript.</p></noscript>
18
+ <div id="wl-c3" class="wl-page c3 c3pg" data-pm-theme="light"><main id="wl-root" class="wl-welcome" data-testid="welcome-root"></main></div>
19
+ <noscript><p class="wl-noscript">This page needs JavaScript.</p></noscript>
18
20
  <script type="module" src="/pm/welcome.js"></script>
19
21
  </body>
20
22
  </html>