create-pmos 0.211.0 → 0.213.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-pmos",
3
- "version": "0.211.0",
3
+ "version": "0.213.0",
4
4
  "description": "Scaffold the PMOS portable kit — run the PMOS product-management workflow (initiative → lane-fit spec → contract → build → three gates, with work-type lanes for features, research, maintenance, incidents, and content) entirely inside your own repo, with all state as git-versioned files. Includes `workflow-sweep`, which audits the state every board derives from and separates real defects from work you simply have not done. `create-pmos workspace` also installs + launches the local Terminal Workspace GUI. No database, no network, no telemetry.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -1,3 +1,3 @@
1
- kernel-source: pmos@d5e2b69
1
+ kernel-source: pmos@4425c43
2
2
  assembled: 2026-10-14
3
3
  mode: vendor
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Eval calibration log"
4
4
  description: "Append-only record of every PM override of an evaluator verdict."
5
- generated: {by: process:create-pmos, at: "2026-10-14T02:17:42Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-14T03:05:22Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Health budgets"
4
4
  description: "Closed registry of health-budget anchors (D59), edited only by the PM."
5
- generated: {by: process:create-pmos, at: "2026-10-14T02:17:42Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-14T03:05:22Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Discoveries and friction log"
4
4
  description: "Append-only record of what runs discovered and where the process rubbed."
5
- generated: {by: process:create-pmos, at: "2026-10-14T02:17:42Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-14T03:05:22Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -155,7 +155,7 @@ const { harness } = require('./c3-ini-harness');
155
155
  ok('P1 an on-hold initiative: the banner with the reason and the check-back day, Resume and Cancel', /On hold since/.test(ban) && /Q1 budget/.test(ban) && /Check back/.test(ban) && /Resume/.test(ban), ban);
156
156
  await page.click('[data-il-menu="IDPF-152"]');
157
157
  const pm = await st(() => [...document.querySelectorAll('[data-ini="menu"] .ini-mlbl')].map((n) => n.textContent).join(','));
158
- ok('P2 the page "…" menu holds Status and Tidy up', pm === 'Status,Tidy up' && await st(() => !!document.querySelector('[data-ini="menu"] [data-il-resume]')), pm);
158
+ ok('P2 the page "…" menu holds This initiative, Status and Tidy up', pm === 'This initiative,Status,Tidy up' && await st(() => !!document.querySelector('[data-ini="menu"] [data-il-resume]')), pm);
159
159
  await page.keyboard.press('Escape');
160
160
  await page.click('[data-il="banner"] [data-il-resume]');
161
161
  await until(async () => ((await row('IDPF-152')) || {}).status === 'on track');
@@ -121,11 +121,15 @@ const { harness } = require('./c3-ini-harness');
121
121
 
122
122
  /* ── an initiative: tools, stepper, workflow editor ── */
123
123
  await open('ini:idpf:IDPF-07:overview');
124
- const strip = await st(() => [...document.querySelectorAll('[data-ini="tools"] [data-ini-tool]')].map((b) => b.dataset.iniTool).join(','));
125
- ok('I1 the tools strip shows the tools of the current stage (Plan: RICE, PRD, Done-criteria, ...)', /^rice,prd,rubric/.test(strip), strip);
126
- await page.click('[data-ini="more-tools"]');
127
- const more = await until(() => st(() => document.querySelectorAll('[data-ini="menu"] [data-ini-moretool]').length));
128
- ok('I2 More tools lists the tools of the other stages, grouped by stage', more > 3 && /Discover/.test(await st(() => document.querySelector('[data-ini="menu"]').textContent)), String(more));
124
+ const head = await st(() => { const p = document.querySelector('[data-ini-page]'); const c = document.querySelector('[data-ini="crumbs"]').parentElement;
125
+ return { title: !!p.querySelector(':scope > .lh'), dash: [...c.querySelectorAll('button')].some((b) => b.textContent.trim() === 'Dashboard'), tag: /Jira not set|Jira · |PMOS tasks only/.test(p.textContent.split('Overview')[0]),
126
+ status: !!c.querySelector('[data-ini="ini-chips"]'), gear: !!document.querySelector('[data-ini="stepper"]').parentElement.querySelector('[data-ini="edit-workflow"]') }; });
127
+ ok('H1 the header is the breadcrumb row with the status, and the stepper row with the gear; no title row, Dashboard button or Jira tag', !head.title && !head.dash && !head.tag && head.status && head.gear, JSON.stringify(head));
128
+ await page.click('[data-ini="tools"]');
129
+ const strip = await until(() => st(() => { const m = document.querySelector('[data-ini="menu"]'); return m && [...m.querySelectorAll('[data-ini-tool]')].map((b) => b.dataset.iniTool).join(','); }));
130
+ ok('I1 Tools lists the tools of the current stage first (Plan: RICE, PRD, Done-criteria, ...)', /^rice,prd,rubric/.test(strip) && (await st(() => document.querySelector('[data-ini="menu"] .ini-mlbl').textContent)) === 'For Plan', strip);
131
+ const more = await st(() => document.querySelectorAll('[data-ini="menu"] [data-ini-moretool]').length);
132
+ ok('I2 then the tools of the other stages, grouped by stage', more > 3 && /Discover/.test(await st(() => document.querySelector('[data-ini="menu"]').textContent)), String(more));
129
133
  await page.keyboard.press('Escape');
130
134
  ok('I3 Escape closes the menu', await until(() => st(() => !document.querySelector('[data-ini="menu"]'))));
131
135
  await page.click('[data-ini-step="Build & Deliver"]');
@@ -150,8 +154,9 @@ const { harness } = require('./c3-ini-harness');
150
154
  ok('W4 the initiative now runs Research only and moved to Plan; other initiatives keep theirs', r07.stage === 'Plan' && r07.wf === 'Discover,Plan,Evaluate'
151
155
  && await store(() => !window.__c3boards.rows.find((r) => r.id === 'IDPF-12').template), JSON.stringify(r07));
152
156
 
153
- /* ── Jira epics of an initiative (jira-epic-items) ── */
154
- await page.click('[data-ini="jira-epics"]');
157
+ /* ── Jira epics of an initiative (jira-epic-items): in the page "…" menu ── */
158
+ await page.click('[data-il-menu="IDPF-07"]');
159
+ await page.click('[data-ini="menu"] [data-ini="jira-epics"]');
155
160
  await until(() => st(() => !!document.querySelector('[data-ini="epics-input"]')));
156
161
  await page.fill('[data-ini="epics-input"]', 'IDPF-900, XX-1');
157
162
  await page.click('[data-ini="epics-save"]');
@@ -160,8 +165,11 @@ const { harness } = require('./c3-ini-harness');
160
165
  await page.fill('[data-ini="epics-input"]', 'idpf-900, idpf-901');
161
166
  await page.click('[data-ini="epics-save"]');
162
167
  await until(() => store(() => (window.__c3boards.rows.find((r) => r.id === 'IDPF-07').epics || []).length === 2));
163
- const epChip = await until(() => st(() => { const c = document.querySelector('[data-ini="jira-epics"]'); return c && /Jira epics IDPF-900, IDPF-901/.test(c.textContent) && c.textContent; }));
164
- ok('E2 the epics are saved upper-cased, and the initiative header names them', !!epChip, epChip);
168
+ await until(() => st(() => !document.querySelector('[data-ini="epics-input"]')));
169
+ await page.click('[data-il-menu="IDPF-07"]');
170
+ const epChip = await until(() => st(() => { const c = document.querySelector('[data-ini="menu"] [data-ini="jira-epics"]'); return c && /Jira epics IDPF-900, IDPF-901/.test(c.textContent) && c.textContent; }));
171
+ ok('E2 the epics are saved upper-cased, and the page menu names them', !!epChip, epChip);
172
+ await page.keyboard.press('Escape');
165
173
 
166
174
  /* ── phone width ── */
167
175
  await page.setViewportSize({ width: 400, height: 860 });
@@ -27,7 +27,7 @@ const finish = async (code) => {
27
27
  };
28
28
  process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
29
29
 
30
- const SECTIONS = ['about', 'workspaces', 'boards', 'connectors', 'recording', 'time', 'sharing', 'access', 'sessions', 'privacy', 'appearance', 'updates'];
30
+ const SECTIONS = ['about', 'workspaces', 'boards', 'connectors', 'refresh', 'recording', 'time', 'sharing', 'access', 'sessions', 'privacy', 'appearance', 'updates'];
31
31
 
32
32
  async function waitFor(page, fn, arg, ms = 8000) {
33
33
  try { await page.waitForFunction(fn, arg, { timeout: ms }); return true; } catch { return false; }
@@ -66,7 +66,7 @@ async function waitFor(page, fn, arg, ms = 8000) {
66
66
  /* ── B: every section draws ── */
67
67
  await waitFor(page, () => !!document.querySelector('[data-c3set-section] .st-row'));
68
68
  ok('B0 Settings opens on About you, drawn as console3 section cards', await waitFor(page, () => !!document.querySelector('[data-c3set-section="about"] .card.sect .list .nrow.st-row #c3set-you-name')));
69
- ok('B1 the nav lists twelve sections', (await page.$$eval('[data-c3set-nav] [data-c3set-sec]', (b) => b.length)) === SECTIONS.length);
69
+ ok('B1 the nav lists every section', (await page.$$eval('[data-c3set-nav] [data-c3set-sec]', (b) => b.length)) === SECTIONS.length);
70
70
  for (const id of SECTIONS) {
71
71
  await page.click(`[data-c3set-nav] [data-c3set-sec="${id}"]`);
72
72
  const drew = await waitFor(page, (s) => { const x = document.querySelector('[data-c3set-section="' + s + '"]'); return x && x.querySelectorAll('.st-row, .sect, .fold').length > 0; }, id, 4000);
@@ -107,6 +107,57 @@ async function waitFor(page, fn, arg, ms = 8000) {
107
107
  await page.click('[data-c3set-nav] [data-c3set-sec="access"]');
108
108
  ok('D7 phone pairing shows, turned off, with the reason', await waitFor(page, () => { const r = document.querySelector('[data-c3set-row="access.pair"]'); return !!r && !!r.querySelector('button:disabled') && !!r.querySelector('.fold'); }));
109
109
 
110
+
111
+ /* ── R: Data refresh (data-refresh-settings R9) ── */
112
+ await page.goto(BASE + '/c3.html?tab=settings');
113
+ await waitFor(page, () => !!document.querySelector('[data-c3set-nav] [data-c3set-sec="connectors"]'));
114
+ await page.click('[data-c3set-nav] [data-c3set-sec="connectors"]');
115
+ ok('R1 Connectors links to Data refresh instead of holding its schedule', await waitFor(page, () => { const a = document.querySelector('[data-c3set-row="conn.cal.refresh"]'); const b = document.querySelector('[data-c3set-row="conn.jira.refresh"]'); return !!a && !!b && !!a.querySelector('button.lk') && /Data refresh/.test(a.textContent) && !a.querySelector('select') && !b.querySelector('select'); }));
116
+ await page.click('[data-c3set-row="conn.cal.refresh"] button.lk');
117
+ ok('R2 the link opens the Data refresh section', await waitFor(page, () => !!document.querySelector('[data-c3set-section="refresh"] [data-c3set-rf-card]')));
118
+ ok('R3 the section draws 13 source rows in three cards', await waitFor(page, () => document.querySelectorAll('[data-c3set-section="refresh"] [data-c3set-rf]').length === 13 && document.querySelectorAll('[data-c3set-rf-card]').length === 3));
119
+ ok('R3b the overview, the rules and the log are there', await page.evaluate(() => !!document.querySelector('[data-c3set-rf-sum]') && !!document.querySelector('[data-c3set-rf-all]') && !!document.querySelector('[data-c3set-rf-rules]') && !!document.querySelector('[data-c3set-rf-log]')));
120
+ ok('R3c a source with its own home says where, and has no editor button', await page.evaluate(() => { const r = document.querySelector('[data-c3set-rf="scan"]'); return !!r && /Set in Initiatives/.test(r.textContent) && !r.querySelector('[data-c3set-rf-open]'); }));
121
+ ok('R3d the filter Off lists the sources that are off', await (async () => { await page.click('[data-c3set-section="refresh"] .seg [data-v="off"]'); const n = await page.evaluate(() => document.querySelectorAll('[data-c3set-rf]').length); await page.click('[data-c3set-section="refresh"] .seg [data-v="all"]'); return n > 0 && n < 13; })());
122
+
123
+ // turning on needs no question
124
+ await page.click('[data-c3set-rf-on="calendar"]');
125
+ ok('R4 turning a source on posts on:true and shows a toast with Undo', await waitFor(page, () => { const t = document.querySelector('[data-c3-toast]'); return !!t && /Turned on: Calendar/.test(t.textContent) && !!t.querySelector('[data-c3-toast-action]'); }));
126
+ ok('R4b the server has it on', await waitFor(page, async () => { const j = await (await fetch('/api/refresh')).json(); return j.rows.find((x) => x.id === 'calendar').on === true; }));
127
+
128
+ // the schedule editor posts the schedule
129
+ await page.click('[data-c3set-rf-open="calendar"]');
130
+ ok('R5 Schedule opens the editor under the row', await waitFor(page, () => !!document.querySelector('[data-c3set-rf="calendar"] [data-c3set-rf-editor]')));
131
+ await page.selectOption('#c3set-rf-time-0', '07:00');
132
+ const posted = page.waitForRequest((q) => q.url().endsWith('/api/refresh/source') && q.method() === 'POST' && /schedule/.test(q.postData() || ''), { timeout: 8000 }).catch(() => null);
133
+ await page.click('[data-c3set-rf-save="calendar"]');
134
+ const req = await posted; const body = req ? JSON.parse(req.postData()) : null;
135
+ ok('R6 Save posts {id, schedule} with the new time', body && body.id === 'calendar' && body.schedule.kind === 'daily' && body.schedule.times[0] === '07:00', JSON.stringify(body));
136
+ ok('R6b the save says so in a toast with Undo', await waitFor(page, () => { const t = document.querySelector('[data-c3-toast]'); return !!t && /Saved: Calendar/.test(t.textContent) && /07:00/.test(t.textContent) && !!t.querySelector('[data-c3-toast-action]'); }));
137
+ ok('R6c the row shows the new schedule', await waitFor(page, () => /07:00/.test((document.querySelector('[data-c3set-rf="calendar"] .chips') || {}).textContent || '')));
138
+
139
+ // turning a source off asks first and names what it affects
140
+ await page.click('[data-c3set-rf-on="jira"]');
141
+ await waitFor(page, async () => (await (await fetch('/api/refresh')).json()).rows.find((x) => x.id === 'jira').on === true);
142
+ await page.click('[data-c3set-rf-on="jira"]');
143
+ ok('R7 turning off asks first and names the impact and what uses it', await waitFor(page, () => { const a = document.querySelector('[data-c3set-rf-ask="jira"]'); return !!a && /Sprint summaries stop/.test(a.textContent) && /Core documents/.test(a.textContent) && /Initiative driver/.test(a.textContent); }));
144
+ ok('R7b nothing changed yet', await page.evaluate(async () => (await (await fetch('/api/refresh')).json()).rows.find((x) => x.id === 'jira').on === true));
145
+ await page.click('[data-c3set-rf-off-go="jira"]');
146
+ ok('R7c confirming posts off and shows a toast with Undo', await waitFor(page, async () => { const j = await (await fetch('/api/refresh')).json(); const t = document.querySelector('[data-c3-toast]'); return j.rows.find((x) => x.id === 'jira').on === false && !!t && /Turned off: Jira/.test(t.textContent) && !!t.querySelector('[data-c3-toast-action]'); }));
147
+
148
+ // the rules
149
+ await page.selectOption('#c3set-rf-max', '2');
150
+ ok('R8 a rule is saved through POST /api/refresh/rules', await waitFor(page, async () => (await (await fetch('/api/refresh')).json()).rules.max === '2'));
151
+ ok('R8b the rules card shows quiet and working hours read-only', await page.evaluate(() => { const r = document.querySelector('[data-c3set-row="rf.quiet"]'); const w = document.querySelector('[data-c3set-row="rf.window"]'); return !!r && !!w && !r.querySelector('select, input') && /09:00 to 18:00/.test(w.textContent); }));
152
+
153
+ // the log
154
+ const ran = await page.evaluate(async () => { const { pmPost } = await import('/pm/http.js'); const w = (await (await fetch('/api/refresh')).json()).ws; return pmPost('/api/refresh/run', { id: 'signals', ws: w }); });
155
+ ok('R9 a refresh can be pressed', ran && ran.ok, JSON.stringify(ran));
156
+ ok('R9b the log view lists the run, newest first, with Export', await waitFor(page, () => { const l = document.querySelector('[data-c3set-rf-log]'); return !!l && l.querySelectorAll('[data-c3set-rfrun]').length > 0 && /Signals/.test(l.textContent) && /\/api\/refresh\/log\.csv/.test((l.querySelector('[data-c3set-rf-export]') || {}).href || ''); }, null, 20000));
157
+ await page.selectOption('#c3set-rf-log-result', 'failed');
158
+ ok('R9c filtering by result puts it in the Export link', await waitFor(page, () => /result=failed/.test((document.querySelector('[data-c3set-rf-export]') || {}).href || '')));
159
+ await page.selectOption('#c3set-rf-log-result', '');
160
+
110
161
  /* ── E: theme ── */
111
162
  await page.click('[data-c3set-nav] [data-c3set-sec="appearance"]');
112
163
  await waitFor(page, () => !!document.querySelector('[data-c3set-row="look.theme"] [data-v="dark"]'));
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  type: Guide
3
3
  title: "Data freshness: how the workspace keeps up to date, and how old each source is"
4
- timestamp: 2026-10-12
4
+ timestamp: 2026-10-14
5
5
  ---
6
6
 
7
7
  # Data freshness
@@ -15,18 +15,18 @@ The workspace checks each registered repository every 10 seconds for a change: a
15
15
  - **Initiatives** (Briefing, Calendar and the reports) are read in a separate process, so the workspace keeps answering while it reads. While a read runs, the pages show the last result. The result is kept on this machine, so after a restart the pages open at once with the last result and a new read starts only if the repository changed.
16
16
  - **Board and My work** are folded in a separate thread for the same reason.
17
17
 
18
- To stop the checks for one repository, press **Stop watching this repository** in Settings › Data freshness. The pages then show what was read last, and **Refresh** reads it again. Press **Watch this repository for changes** to start the checks again.
18
+ To stop the checks, turn off **Repository and initiatives** in Settings › Data refresh. This stops them for every registered repository. The pages then show what was read last, and **Refresh now** reads it again. Turn it on to start the checks again.
19
19
 
20
20
  ## Jira, calendar, My work and tracker items
21
21
 
22
- The workspace holds no password or token, and it never calls these services. An agent session reads each one with your own connection, on a schedule or when you press **Refresh**:
22
+ The workspace holds no password or token, and it never calls these services. An agent session reads each one with your own connection, on the schedule set in **Settings › Data refresh**, or when you press **Refresh**:
23
23
 
24
24
  | Source | Skill | Default schedule | Shown as old after |
25
25
  |---|---|---|---|
26
- | Calendar | calendar-sync | 06:30 and 12:30 every day (set `calendar`) | 8 hours |
27
- | Jira status | jira-sync | 06:30 every day (set `jira`) | 26 hours |
28
- | My work | my-work-read | when you press Read again | 26 hours |
29
- | Tracker items | tracker-refresh | Friday 15:30 (set `tracker`) | 8 days |
26
+ | Calendar | calendar-sync | 06:30 and 12:30 every day | 8 hours |
27
+ | Jira status | jira-sync | 06:30 every day | 26 hours |
28
+ | My work | my-work-read | when you press Refresh | 26 hours |
29
+ | Tracker items | tracker-refresh | Friday 15:30 | 8 days |
30
30
 
31
31
  The calendar-sync skill reads today and the next 14 days, read-only, and saves the events with this command:
32
32
 
@@ -52,4 +52,31 @@ Each page shows a short line that says when each source was last read, for examp
52
52
  - A watched repository is not named, because the workspace checks it every few seconds.
53
53
  - The refresh icon at the end of the line reads every source again: the initiatives on this machine, and an agent read for Calendar, Jira status, My work and tracker items. A source that is switched off or is being read already is left alone. A short note says how many reads started, or which could not start.
54
54
 
55
- Settings › Data freshness shows every source in one table: when it was read, its state, its schedule, and **Refresh**.
55
+ ## Settings › Data refresh
56
+
57
+ **Settings › Data refresh** is the one place that says what PMOS refreshes and when. It lists 13 sources in three groups: from your tools (Calendar, Jira, work assigned to you, tracker files), written by PMOS (the morning briefing, the day check, the horizon scan, core documents, the initiative driver, the report finder, signals) and on change (the repository and Git sync).
58
+
59
+ For each source you can:
60
+
61
+ - Turn it on or off. Before a source goes off, PMOS lists what it affects, for example "The initiative driver cannot see ticket moves".
62
+ - Set its schedule (the tool sources and the briefing): only when you press Refresh, every 15 minutes to 4 hours, daily or on working days at up to four times, or weekly. The day check runs every 30 or 60 minutes. The horizon scan and the report finder keep their schedules where they are set today, and the row says where.
63
+ - Press **Refresh now**. A source whose inputs are old offers **Refresh inputs first**.
64
+ - See when it last refreshed, the result, what it got or the error, the next run, what it uses and what uses it.
65
+
66
+ The schedules that the recommended sets made in schedules.json for these five skills move here once, on the first start. Each old entry is switched off with the reason "Moved to Settings › Data refresh". A recommended set or **Prepare my day** now turns the source on here instead of adding a schedule.
67
+
68
+ ### The order of refreshes
69
+
70
+ A refresh waits for the sources it uses. The briefing waits for work assigned to you; core documents wait for the repository, Jira and the horizon scan; the driver waits for the repository, Jira and core documents. While an input is running or due, the refresh waits for up to 30 minutes, then it runs anyway and its log line says which input it did not wait for. An input that failed or is off does not hold anything: the refresh uses the last good data, and the row shows it under "Built on old data".
71
+
72
+ ### The rules
73
+
74
+ - **Catch up**: a refresh missed while PMOS was closed runs once when PMOS starts. Off, the missed slot is skipped and written to the log.
75
+ - **Retry**: a failed scheduled refresh is tried once more after 15 or 60 minutes.
76
+ - **Tell me**: a failed scheduled refresh adds one item to Needs you on the Briefing.
77
+ - **Runs at once**: at most this many scheduled refreshes run together. A refresh you press is never held.
78
+ - No scheduled refresh starts in your quiet hours; the background jobs keep to the day check's working hours.
79
+
80
+ ### The refresh log
81
+
82
+ Every background run is written to `refresh-log.jsonl` in your runtime folder on this computer: a start line and an end line with the result, the time it took, what it got and the error. A skipped slot writes a line with the reason. The start line records each input's state and when it last refreshed. A done refresh of Jira or of work assigned to you keeps a copy of what it read, so you can see what PMOS knew at any time. Lines and copies older than the keep setting (30, 90 or 365 days, or forever; 90 by default) are removed once a day. The log view filters by source and result, and **Export** downloads it as CSV.
@@ -44,7 +44,7 @@ On a desktop the workspace is one frame. [Layout](layout.md) explains each part.
44
44
  - [Start the workspace without a terminal](launcher.md): the PMOS app, the login item and `pmos launcher`.
45
45
  - [Jira tickets](jira.md): Jira tickets drafted from the PRD, pushed when you select them, and their status.
46
46
  - [My work](my-work.md): the work assigned to you, its order, the brief to an agent, and `pmos my-work`.
47
- - [Data freshness](data-freshness.md): when each source is read again, how old it is, and `pmos calendar`.
47
+ - [Data freshness](data-freshness.md): when each source is read again, how old it is, Settings › Data refresh and its log, and `pmos calendar`.
48
48
  - [People](people.md): the people you work with in a repository, how they get there, and `pmos people`.
49
49
  - [Docs](docs.md): whether each linked document is the same as, ahead of or behind its Google Docs and Confluence copy.
50
50
  - [Calendar popup](calendar-popup.md): open the full calendar over any page of the console with the calendar button or the C key, and close it.
@@ -2,7 +2,7 @@
2
2
  type: Guide
3
3
  title: "Initiatives on the console: Portfolio, boards and workflows"
4
4
  description: "How to use the Initiatives tab of the console page (/): the Portfolio of boards, a board and its nine tabs, an initiative with its own workflow and stage records, and the Build and deliver setting."
5
- timestamp: 2026-10-13
5
+ timestamp: 2026-10-14
6
6
  ---
7
7
 
8
8
  # Initiatives on the console: Portfolio, boards and workflows
@@ -86,9 +86,9 @@ Press the gear in the board header to open the board settings.
86
86
 
87
87
  ## An initiative
88
88
 
89
- Each initiative has its own workflow, picked from a template: for example Product, Quick fix, Research only, or Content and launch. The stepper shows the stages of that workflow. Press a stage to move the initiative there. The move is your decision, so nothing blocks it. An order rule shows as a warning chip, for example "Skipped Plan" or "No spec yet". Press **Workflow** to change the template or edit the stages. When you remove a stage that holds the initiative, the editor asks where it goes.
89
+ Each initiative has its own workflow, picked from a template: for example Product, Quick fix, Research only, or Content and launch. The stepper shows the stages of that workflow. Press a stage to move the initiative there. The move is your decision, so nothing blocks it. An order rule shows as a warning chip next to the status, at the right of the breadcrumb, for example "Skipped Plan" or "No spec yet". Press the gear next to the stepper to change the template or edit the stages. When you remove a stage that holds the initiative, the editor asks where it goes.
90
90
 
91
- Under the stepper are the tools of the current stage. **More tools** lists the tools of the other stages. The tabs are **Overview**, **Breakdown**, **Docs**, **Tickets**, **Timeline** and **History**.
91
+ Press **Tools**, at the right of the stepper, to open a tool. The menu lists the tools of the current stage first, then the tools of the other stages. The **…** menu at the right of the breadcrumb opens the Dashboard and the Jira epics of the initiative, and holds Status and Tidy up. The tabs are **Overview**, **Breakdown**, **Docs**, **Tickets**, **Timeline** and **History**.
92
92
 
93
93
  The **Overview** shows the stage records of the current stage:
94
94
 
@@ -128,7 +128,8 @@ function create(deps) {
128
128
  } catch (e) { warn('driver run end', e); }
129
129
  }
130
130
 
131
- function tick({ now = Date.now(), env } = {}) {
131
+ /* `sweepNow` (Refresh now, data-refresh-settings R8) runs the unblock sweep at once, inside or outside the window. */
132
+ function tick({ now = Date.now(), env, sweepNow = false } = {}) {
132
133
  if (off(env || deps.env)) return { off: true };
133
134
  const summary = { epoch: 0, woke: 0, swept: 0, started: 0, refused: 0 };
134
135
  const leads = [];
@@ -155,9 +156,9 @@ function create(deps) {
155
156
  for (const l of newWakes) { D.append(file, l, { now }); summary.woke++; }
156
157
  state = readState(file);
157
158
  // The unblock sweep: deterministic, every 30 minutes inside the day-check window.
158
- const due = !lastSweep.has(file) || now - lastSweep.get(file) >= D.SWEEP_EVERY_MS;
159
+ const due = sweepNow || !lastSweep.has(file) || now - lastSweep.get(file) >= D.SWEEP_EVERY_MS;
159
160
  let factsNow = null;
160
- if (due && dayCheckOpen(settings, now, deps.inQuiet)) {
161
+ if (due && (sweepNow || dayCheckOpen(settings, now, deps.inQuiet))) {
161
162
  const g = gather(w, deps.ctx, { now, includeWork: w.id === home, runtimeDir: deps.runtimeDir, driverState: state });
162
163
  if (g.ok) {
163
164
  factsNow = g.facts;
@@ -112,6 +112,8 @@ function create(o) {
112
112
  });
113
113
 
114
114
  function scheduleOf(skill) {
115
+ // data-refresh-settings: a skill source's schedule lives in Settings › Data refresh, which answers for it here.
116
+ if (skill && o.scheduleOf) { try { const r = o.scheduleOf(skill); if (r !== undefined) return r; } catch { /* fall back to schedules.json */ } }
115
117
  if (!skill || !o.schedules) return null;
116
118
  let list = [];
117
119
  try { list = o.schedules() || []; } catch { return null; }