create-pmos 0.510.0 → 0.512.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 (43) hide show
  1. package/package.json +239 -134
  2. package/payload/KIT-VERSION +1 -1
  3. package/payload/planning/evals/eval-calibration.md +1 -1
  4. package/payload/planning/okrs/health-budgets.md +1 -1
  5. package/payload/state/discoveries.md +1 -1
  6. package/workspace/briefing-premium-board-test.js +6 -7
  7. package/workspace/changelog.json +349 -912
  8. package/workspace/ev-everywhere-test.js +4 -4
  9. package/workspace/fixtures/next-prompt-replay.json +40 -0
  10. package/workspace/guide/home.md +2 -1
  11. package/workspace/guide/index.md +1 -0
  12. package/workspace/guide/initiative-boards.md +3 -1
  13. package/workspace/guide/next-prompt.md +62 -0
  14. package/workspace/guide/settings.md +11 -5
  15. package/workspace/lib/board-premium.js +3 -2
  16. package/workspace/lib/kit-update.js +5 -2
  17. package/workspace/lib/next-prompt.js +199 -0
  18. package/workspace/lib/route-modules.js +1 -0
  19. package/workspace/lib/routes-boards.js +15 -1
  20. package/workspace/lib/routes-next-prompt.js +248 -0
  21. package/workspace/package-lock.json +2 -2
  22. package/workspace/package.json +1 -1
  23. package/workspace/panels-test.js +68 -1
  24. package/workspace/public/pm/bd-cols.js +2 -1
  25. package/workspace/public/pm/bd-deliveries.js +1 -1
  26. package/workspace/public/pm/bd-span.js +15 -10
  27. package/workspace/public/pm/cozy-ev.css +26 -10
  28. package/workspace/public/pm/cozy-home.css +45 -1
  29. package/workspace/public/pm/cozy-live-pill.css +9 -0
  30. package/workspace/public/pm/cozy-live-pill.js +50 -3
  31. package/workspace/public/pm/cozy-panels.css +9 -0
  32. package/workspace/public/pm/cozy-panels.js +99 -16
  33. package/workspace/public/pm/cozy-settings.js +70 -16
  34. package/workspace/public/pm/cozy-viewer.js +15 -3
  35. package/workspace/public/pm/home-chat.js +25 -4
  36. package/workspace/public/pm/next-prompt-ui.js +117 -0
  37. package/workspace/public/pm/page-chat.css +9 -0
  38. package/workspace/public/pm/page-chat.js +90 -0
  39. package/workspace/public/pm/premium-board.js +5 -0
  40. package/workspace/public/pm/upd-weeks.js +21 -7
  41. package/workspace/public/premium-board.html +2 -2
  42. package/workspace/test-lists/closed-not-late.json +5 -0
  43. package/workspace/test-lists/next-prompt.json +5 -0
@@ -150,20 +150,20 @@ const put = (root, rel, text) => { fs.mkdirSync(path.dirname(path.join(root, rel
150
150
  const hits = { check: 0, update: 0, upgrade: 0 };
151
151
  const day = (n) => new Date(Date.now() - n * 864e5).toISOString();
152
152
  await pg.route('**/api/kit/status*', (r) => r.fulfill({ contentType: 'application/json', body: JSON.stringify({ ok: true, app: { version: '0.442.0' }, kit: { present: true, version: '0.442.0' }, latest: '0.451.0', appBehind: true, kitBehind: true, checkDaily: true,
153
- changes: [{ v: '0.451.0', at: day(0), prs: [{ n: 1098, t: 'Claude settings', s: 'Settings has a Claude section.', k: 'new' }, { n: 1100, t: 'Hide linked worktrees', s: 'They no longer show as repositories.', k: 'fix' }] }, { v: '0.441.0', at: day(8), prs: [{ n: 1078, t: 'Ready-made dashboards', s: 'Every program has one.', k: 'new' }] }] }) }));
153
+ changes: [{ v: '0.451.0', at: day(0), prs: [{ n: 1098, t: 'Claude settings', s: 'Settings has a Claude section.', k: 'imp', h: 'Claude in Settings', d: 'Claude had no settings of its own. Now Settings has a Claude section.', top: 'Claude has its own place in Settings.' }, { n: 1100, t: 'Hide linked worktrees', s: 'They no longer show as repositories.', k: 'fix' }] }, { v: '0.441.0', at: day(8), prs: [{ n: 1078, t: 'Ready-made dashboards', s: 'Every program has one.', k: 'new' }] }] }) }));
154
154
  await pg.route('**/api/kit/check', (r) => { hits.check++; r.fulfill({ contentType: 'application/json', body: JSON.stringify({ ok: true, latest: '0.451.0', appBehind: true, kitBehind: true, latestChanges: [] }) }); });
155
155
  await pg.route('**/api/kit/update', (r) => { hits.update++; r.fulfill({ contentType: 'application/json', body: JSON.stringify({ ok: true }) }); });
156
156
  await pg.route('**/api/kit/upgrade-app', (r) => { hits.upgrade++; r.fulfill({ contentType: 'application/json', body: JSON.stringify({ ok: false, error: 'stubbed: not installing' }) }); });
157
157
  await pg.keyboard.press('Escape'); await until(() => pg.evaluate(() => !document.querySelector('#evSettings:not([hidden])')));
158
158
  await menu(pg); await pg.click('#czMenu [data-cz-settings]'); await pg.waitForSelector('#evSettings:not([hidden])'); // reopen: the routes above answer the status read
159
159
  await pg.click('#evSettings [data-ev-sec="updates"]');
160
- const wk = await until(() => pg.evaluate(() => { const d = document.querySelector('#evSettings'); const h = d.querySelector('.ev-wkh'); return h ? { head: h.textContent, groups: [...d.querySelectorAll('.ev-wk > .ev-wkm .ev-wgl')].map((x) => x.textContent), t: d.querySelector('.ev-wk > .ev-wkm').textContent, arc: !!d.querySelector('[data-ev-arc][aria-expanded="false"]'), rows: d.querySelectorAll('[data-ev-wk]').length } : null; }), 20000);
160
+ const wk = await until(() => pg.evaluate(() => { const d = document.querySelector('#evSettings'); const h = d.querySelector('.ev-wkh'); return h ? { head: h.textContent, sum: [...d.querySelectorAll('.ev-wk > .ev-sum p')].map((x) => x.textContent), kit: (d.querySelector('.ev-wk .ev-rv') || {}).textContent || '', dots: [...d.querySelectorAll('.ev-wk > .ev-rel .ev-dot')].map((x) => x.title), t: d.querySelector('.ev-wk').textContent, arc: !!d.querySelector('[data-ev-arc][aria-expanded="false"]'), rows: d.querySelectorAll('[data-ev-wk]').length } : null; }), 20000);
161
161
  const first = await (async () => { await sleep(800); return hits.check; })();
162
162
  await pg.click('#evSettings [data-ev-sec="general"]'); await pg.click('#evSettings [data-ev-sec="updates"]'); await sleep(500);
163
163
  ok('W2 opening Updates asks for the latest kit by itself (one read for each workspace), and only once while Settings stays open', first >= 1 && hits.check === first, JSON.stringify(hits));
164
- ok('W2 what\'s new shows this week open: New and Fixed groups, no empty Improved group, the earlier week folded in a closed Archive', !!wk && /^This week/.test(wk.head) && wk.groups.join() === 'New,Fixed' && /Claude settings\./.test(wk.t) && /#1100/.test(wk.t) && wk.arc && wk.rows === 0, JSON.stringify(wk));
164
+ ok('W2 design E: this week opens with its summary line, the newest kit at version level, each change a story with a New or Fixed dot, no PR numbers, the earlier week folded in a closed Archive', !!wk && /^This week/.test(wk.head) && /2 changes/.test(wk.head) && wk.sum.join() === 'Claude has its own place in Settings.' && /^0\.451\.0latest/.test(wk.kit) && wk.dots.join() === 'New,Fixed' && /Claude in Settings/.test(wk.t) && /Now Settings has a Claude section/.test(wk.t) && !/#1100|#1098/.test(wk.t) && wk.arc && wk.rows === 0, JSON.stringify(wk));
165
165
  await pg.click('#evSettings [data-ev-arc]'); await pg.click('#evSettings [data-ev-wk]');
166
- ok('W2 the Archive opens to the earlier weeks, and a week opens on press', !!(await until(() => pg.evaluate(() => /Ready-made dashboards\./.test(document.querySelector('#evSettings').textContent)))));
166
+ ok('W2 the Archive opens to the earlier weeks, and a week opens on press', !!(await until(() => pg.evaluate(() => /Ready-made dashboards/.test(document.querySelector('#evSettings').textContent)))));
167
167
  await snap(pg, 'updates-1440-light');
168
168
  await pg.click('#evSettings [data-ev-upd="update"]');
169
169
  await until(() => hits.upgrade > 0, 8000);
@@ -0,0 +1,40 @@
1
+ {
2
+ "about": "Recorded answers for the next-prompt and sharpen-prompt replay gates (lib/routes-next-prompt.js). Each answer was given by claude-sonnet-5-5 on a real reply from the PMOS project threads on 2026-11-03, except the cases named as made up, which test a drop. The replay never calls a model.",
3
+ "next": [
4
+ { "name": "a reply that asks a question gets the answer as a follow card, citing the line it answers",
5
+ "reply": "#1198 (no initiative without an expected outcome) is merged, and kit-publish is publishing it.\nAll five approved features are now merged except the hindsight engine, which stays unbuilt because there are only 1 of the 10 needed prediction-and-outcome pairs. Do you want to swap it for another idea, such as a period-end KR debrief, or stay at four?",
6
+ "answer": { "cards": [{ "move": "follow", "text": "Yes, swap the hindsight engine for the period-end KR debrief.", "from": "Do you want to swap it for another idea, such as a period-end KR debrief, or stay at four?" }] },
7
+ "expect": { "filed": 1 } },
8
+ { "name": "a retired thing in an initiative chat gets an ask-the-past card, citing the verb that answers it",
9
+ "place": { "initiative": "Fix OOH Digital missing from Board" },
10
+ "reply": "Found the cause: the Board hid initiatives on the Later horizon. Retiring Now/Next/Later everywhere is a bigger change, so I asked for a separate thread for it.",
11
+ "answer": { "cards": [{ "move": "past", "text": "Did we ever decide to retire the Now/Next/Later stage? Answer from the records with file:line sources, or say No record found." }] },
12
+ "expect": { "filed": 1, "cites": ["lib/pmos-verbs.js"] } },
13
+ { "name": "a reply that needs nothing gets no card",
14
+ "reply": "First I'll find why OOH Digital is ticked in the product filter but has no rows on the Board.",
15
+ "answer": { "cards": [] },
16
+ "expect": { "none": true } },
17
+ { "name": "made up: a move PMOS does not have is dropped",
18
+ "reply": "PR #1171 is merged.",
19
+ "answer": { "cards": [{ "move": "deploy", "text": "Deploy PR #1171 to production now." }] },
20
+ "expect": { "none": true } },
21
+ { "name": "made up: a follow card that quotes a line the reply never said is dropped",
22
+ "reply": "PR #1171 is merged. Tell me if any task still sits under \"Not in a stage\".",
23
+ "answer": { "cards": [{ "move": "follow", "text": "Yes, merge it.", "from": "Should I merge PR #1171 now or wait?" }] },
24
+ "expect": { "none": true } },
25
+ { "name": "made up: ask the past outside an initiative chat is dropped",
26
+ "reply": "Retiring Now/Next/Later everywhere is a bigger change.",
27
+ "answer": { "cards": [{ "move": "past", "text": "Why did we retire Now/Next/Later?" }] },
28
+ "expect": { "none": true } }
29
+ ],
30
+ "sharpen": [
31
+ { "name": "a rough date move becomes a rehearsal, citing what the PM typed",
32
+ "text": "move the legal review 3 days later",
33
+ "answer": { "text": "Rehearse moving the legal review task in Ooh-digital 3 days later: show what it pushes back and which dates or tasks shift, then wait for my yes before changing anything." },
34
+ "expect": { "filed": 1 } },
35
+ { "name": "a sharper prompt that says the same is not shown",
36
+ "text": "Hotfix, why OOH digital product isn't shown here",
37
+ "answer": { "text": "Hotfix: why isn't the OOH digital product shown here?" },
38
+ "expect": { "none": true } }
39
+ ]
40
+ }
@@ -160,7 +160,8 @@ Some features are still not designed. Home keeps each one usable as a plain card
160
160
 
161
161
  ## The other pages
162
162
 
163
- - **Board.** New initiative takes links and files, starts in Triage and asks only the questions that the material does not answer. **Later** keeps the questions as the next step. The Board also has List, By stage and a span picker. Documents are in each initiative's Documents fold and in Search. On a phone, **Metrics** and **Cancelled and archived** are in every view.
163
+ - **Board.** New initiative takes links and files, starts in Triage and asks only the questions that the material does not answer. **Later** keeps the questions as the next step. The Board also has List, By stage and a span picker. Documents are in each initiative's Documents fold and in Search. On a phone, **Metrics** and **Cancelled and archived** are in every view. On a computer, press **Discuss in chat** on an initiative's or a program's panel to open the chat about it in a panel on the Board. It is the same conversation as on Home. At 1600 px and wider the chat opens beside the panel; on a laptop it slides over it.
164
+ - **Dock a panel left or right (Board and Intelligence).** Drag a panel's header to the left or the right half of the window, and drop it when that half turns blue. With the keyboard, press **Arrange** in the panel's header, then **Dock on the left** or **Dock on the right**. Each kind of panel stays on its side after a reload. The divider resizes the panel on either side. The Live pill's list has a grab bar on top: drag it to the left or the right side, or press it to move the list to the other side.
164
165
  - **Meetings.** Replay with screen captures and kept points, Focus, a presenter view, Add material, the After view, a Day, Week and Month calendar, and Notes.
165
166
  - **Reports.** The Brief, the report states, a document reader and a live page reader.
166
167
  - **Insights.** A goal's detail with its chart and readings, targets on each tile, Library flows and a Portfolio view.
@@ -81,6 +81,7 @@ On a desktop the workspace is one frame. [Layout](layout.md) explains each part.
81
81
  - [Initiatives › Timeline](initiatives-timeline.md): when each initiative runs, one row per initiative, and how to reorder rows by their grip.
82
82
  - [Workflows](workflows.md): Settings › Workflows, the stages each kind of initiative goes through, New workflow in the chat, and `pmos workflow`.
83
83
  - [Expected outcomes](outcomes.md): every initiative says what should change once it ships and when to check it. Claude drafts each one from the PRD or the KR, you keep it with ✓, and `pmos outcomes`.
84
+ - [The next prompt, suggested and sharpened](next-prompt.md): after a reply, up to two prompts to send next; while you type a rough prompt, a sharper version in your style (Polish, Prompt engineer, Your own).
84
85
  - [Rehearse a plan](rehearse.md): before you approve a date change or a cut, Claude shows what it breaks or helps in other initiatives, goals and program milestones, with the record each effect comes from, and `pmos rehearse`.
85
86
  - [Forecasts](forecast.md): how a goal, a delivery, a program and a health budget are forecast, the states, the kept baseline, and `pmos forecast`.
86
87
  - [Programs on the Board](programs.md): All products, programs first then products, By product, the program panel and its milestones, and the 60 % side panels.
@@ -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-11-02
5
+ timestamp: 2026-11-03
6
6
  ---
7
7
 
8
8
  # Initiatives on the console: Portfolio, boards and workflows
@@ -58,6 +58,8 @@ Each initiative has a **stage** and a **status**. The stage is the step in its o
58
58
  | Done | It reached the end of its workflow, or it was accepted or closed. |
59
59
  | Cancelled | You stopped it. It is counted as cancelled, not done. |
60
60
 
61
+ An initiative in the last stage of its workflow (for example **Closed**, or a stage in the Done category) is never Late or At risk. On the Timeline its bar ends at its own end date. It has no line to Today, no late dot and no late milestone.
62
+
61
63
  A cancelled initiative is hidden from every view. The list footer says how many are hidden, for example "1 cancelled hidden". Press it to see them, or pick Cancelled in the Status filter.
62
64
 
63
65
  Press **…** on a row to open its menu. It is the initiative's short menu: Split, Merge into, Program, Copy, The record and **Delete** (see [Home](home.md#an-initiative)). Press the stage name on the initiative's stage card for the stage picker:
@@ -0,0 +1,62 @@
1
+ ---
2
+ type: Guide
3
+ title: "The next prompt, suggested and sharpened"
4
+ description: "After a reply, the chat suggests what to say next. While you type a rough prompt, Claude offers a sharper version. What each card means, the styles, the settings, and the guardrails."
5
+ timestamp: 2026-11-03
6
+ ---
7
+
8
+ # The next prompt, suggested and sharpened
9
+
10
+ You do not have to remember phrases such as "grill this". The chat suggests them when they fit.
11
+
12
+ ## After a reply
13
+
14
+ When Claude finishes a reply, up to two cards can show above the box. Usually there is one.
15
+
16
+ - The card Claude recommends is white, with a blue ring and a blue ✦. Another card is grey.
17
+ - Tap a card to put its text in the box. Nothing is sent. Change the text if you want to, then send it with ↑.
18
+ - The cards go away when you type, or when Claude starts a new reply. They do not time out.
19
+ - When there is no clear next step, no card shows.
20
+
21
+ A card suggests only something that works in PMOS today:
22
+
23
+ - an answer to the question or the offer at the end of the reply;
24
+ - "Grill this …" in an initiative's chat ([Claude proposes](claude-proposes.md));
25
+ - "Why did we …?" in an initiative's chat ([Ask about the past](ask-the-past.md));
26
+ - "Rehearse moving …" before a date change or a cut ([Rehearse a plan](rehearse.md));
27
+ - "Run the drift replay" when the weekly check waits for it ([What you might have missed](what-you-missed.md)).
28
+
29
+ Each card names only the initiatives, tasks and PRs that the reply or your message names.
30
+
31
+ ## While you type
32
+
33
+ When you stop typing for about 1.5 seconds on a rough prompt of three or more words, a small spinner card shows while Claude sharpens it. Then one lit **Sharper** card shows. Tap it to put the sharper text in the box. ↶ in the box puts back what you wrote. When the sharper version says the same as yours, no card shows.
34
+
35
+ The sliders icon on the Sharper card sets how Claude sharpens:
36
+
37
+ - **Polish**: clearer and shorter. Your meaning is kept.
38
+ - **Prompt engineer** (the default): the best prompt for the job. It names the real initiative or task and says what a good answer holds. A date move or a cut becomes a rehearsal that waits for your yes.
39
+ - **Your own**: your own wording rules. You write them once in Settings.
40
+
41
+ The same menu has the **Sharpen as I type** switch. Off means no spinner and no Sharper card.
42
+
43
+ ## Settings › Claude › Sharpen prompts
44
+
45
+ | Row | What it does |
46
+ |---|---|
47
+ | Suggest the next prompt | On by default. Off: no cards after a reply. |
48
+ | Sharpen as I type | On when Claude's runs use your API key (Settings › Claude › API key), because a sharper version then shows in about 2 seconds. Without a key, each one is a background Claude run that takes 10 seconds or more, so it starts off. You can turn it on anyway. |
49
+ | Sharpen as | Polish, Prompt engineer or Your own. |
50
+ | Your own rules | The rules for Your own, up to 1000 characters. |
51
+
52
+ ## The guardrails
53
+
54
+ The suggestions and the sharper prompt go through the same guardrail as every AI job in PMOS:
55
+
56
+ - **Propose only.** A card is text you can put in the box. Nothing is sent, and nothing is written to a record, a session or a repository.
57
+ - **Proven only.** Each suggested move points to the line of PMOS code that makes it work, and PMOS checks that line before the card shows. An answer to the reply points to the line of the reply it answers. A card that PMOS cannot check is not shown.
58
+ - **Replay-tested.** A new version shows nothing until its recorded cases pass, and those include cases that must be dropped.
59
+ - **Audited.** Each run writes a line to the AI audit, with no text of your conversation in it.
60
+ - **Off switch.** `pmos ai off next-prompt` stops the suggestions after a reply. `pmos ai off sharpen-prompt` stops the Sharper card. `pmos ai off` stops every AI job.
61
+
62
+ The reply and the words a card refers to are kept in your PMOS state folder (`next-prompt/`, the newest 30 files), outside every repository. Your choices for these rows are kept there too, in `next-prompt/prefs.json`.
@@ -35,7 +35,7 @@ Settings is not a tab. While it is open, no tab is selected. Press a tab to go b
35
35
  | Claude sessions | What a new session starts with: the CLI, the model, the Claude profile and the launch flags for sessions you start, and the flags for automatic sessions. The session limits. Validate consent for this workspace. |
36
36
  | Privacy and data | A list of what leaves this computer and when. Whether a TypeSafe key is saved, the local model that answers when there is no key (Off, Winnow or Laya), and the data folder. |
37
37
  | Appearance | The theme (Same as system, Light or Dark) and the page that opens first. |
38
- | Claude | The Claude profile that new Claude sessions start with, and the Anthropic API key for Claude's own runs on the API (see below). |
38
+ | Claude | The Claude profile that new Claude sessions start with, the Anthropic API key for Claude's own runs on the API, and Sharpen prompts (see below). |
39
39
  | Claude's checks | When Claude grills an initiative by itself (Full, Only at stage exits, or Off) and the monthly credit for its checks ($5, $10, $20 or $50), with what this month used. See below. |
40
40
  | Updates | The version of the app and of each workspace's kit, the check for updates, and what changed in each week's kits (what's new). See below. |
41
41
 
@@ -67,12 +67,16 @@ Open **Updates**. PMOS asks the npm registry for the latest version at once, and
67
67
 
68
68
  ### What's new
69
69
 
70
- Under the update card, **Updates** shows what changed in the published kits. **This week** is open: it is the one you need. A week runs Monday to Sunday. Every earlier week, last week included, is folded into **Archive**. Press **Archive** to list the earlier weeks (**Week of** a date), each with its counts, and press a week to read it. When there is nothing for this week yet, the newest week with changes is the open one.
70
+ Under the update card, **Updates** shows what changed in the published kits. **This week** is open: it is the one you need. A week runs Monday to Sunday. The heading gives the dates and the number of changes. When there is nothing for this week yet, the newest week with changes is the open one.
71
71
 
72
- - Each week has up to three groups: **New** (a feature), **Improved** (a faster or better thing that already existed) and **Fixed** (a problem that is gone). A group with nothing in it is not shown, and a week with nothing to show is not listed.
73
- - Each line is a bold name and one sentence, copied from the pull request that made the change, with its number at the end. When the name and the sentence say the same thing, the line shows it once. Work that you cannot see in PMOS (notes, tests, run logs) is left out, and so are notes for the people who build PMOS, such as "[skip publish]" or "(batch 25 item 11)".
72
+ - **Summary.** Up to three lines on top say the most important changes of the week. Claude writes them when it makes the change.
73
+ - **The newest kit** comes next, with its version and **latest** when it is the newest published kit.
74
+ - **Earlier this week** lists the other changes of the week as one list, the most important ones first. The list shows seven changes; press **N more** to show the rest.
75
+ - Each change is a short story: a plain name, then what was missing and what is different now. Claude writes it for you. A blue dot marks **New** and an amber dot marks **Fixed**. A change with no dot is **Improved**. The most important changes always show as **New**.
76
+ - There are no pull request numbers and no kit ranges. Work that you cannot see in PMOS (notes, tests, run logs) is left out.
77
+ - **Archive** holds every earlier week, last week included. Press **Archive** to list them, then press a week to read it.
74
78
  - A week shows **Ready to install** when one of its kits is newer than the version you run.
75
- - A kit published before PMOS kept the kind of each change has no kind. Its title decides: a word such as "fix" or "no longer" puts it under **Fixed**, and anything else under **Improved**.
79
+ - A change published before Claude wrote stories shows the pull request's title and its sentence instead.
76
80
  - **What's new** is not a section of its own any more. The button on the update card in **General** opens **Updates**.
77
81
 
78
82
  ## Claude: the profile and the API key
@@ -81,6 +85,8 @@ Under the update card, **Updates** shows what changed in the published kits. **T
81
85
 
82
86
  **API key.** Some runs of Claude, for example Grill, can use the Claude API and not a Claude session. Paste an Anthropic API key and press **Save** to turn this on. The key stays on this computer, in a file only you can read, outside every repository. PMOS never shows the key again: the row only says that a key is set. Press **Remove** to delete it. A key saved here is used before `ANTHROPIC_API_KEY`. A command that a run starts does not get the key. Only the computer that runs the workspace can change it. Set `PMOS_GRILL_API=off` to stop runs on the API.
83
87
 
88
+ **Sharpen prompts.** Suggest the next prompt (on by default) shows up to two prompts above the chat box after a reply. Sharpen as I type offers a sharper version of a rough prompt. It is on when an API key is set, and off without one, because a background run is too slow to follow your typing. Sharpen as picks Polish, Prompt engineer (the default) or Your own, and Your own rules holds the rules for Your own. The same style and switch are in the menu on the chat's Sharper card. See [The next prompt, suggested and sharpened](next-prompt.md).
89
+
84
90
  ## Claude's checks: when Claude grills, and what it may spend
85
91
 
86
92
  Claude grills an initiative by itself and files what it finds under the initiative's ✦ (see the Claude proposes guide). There is no button to start it. This section is the only control.
@@ -275,10 +275,11 @@ function rowOf(r, wf, today) {
275
275
  ['Progress', pctOf(r.progress) != null ? pctOf(r.progress) + '%' : ''], ['Starts', r.start ? shortDate(r.start) : ''], ['Ends', r.end ? shortDate(r.end) : ''],
276
276
  ['Last changed', r.updated ? shortDate(String(r.updated).slice(0, 10)) : ''], ['Possible duplicate of', r.dupOf], ['Warnings', (r.warnings || []).map((w) => (typeof w === 'string' ? w : w && (w.text || w.message || w.id)) || '').filter(Boolean).join('; ')],
277
277
  ].filter((x) => x[1]);
278
- const over = r.end && today && days(r.end, today) > 0 && r.status !== 'done';
278
+ // a closed initiative (its workflow's terminal stage, lib/routes-boards.js closedOf) is never over: no line to Today, no days over (batch 26 item 5)
279
+ const over = r.end && today && days(r.end, today) > 0 && r.status !== 'done' && !r.closed;
279
280
  return {
280
281
  key: 'board:' + r.id, id: r.id, title: clip(r.title, 140), sub: [r.owner || 'No owner', r.lane].filter(Boolean).join(' · '),
281
- status: r.status || null, statusLabel: r.status && r.status !== 'on track' ? (STATUS_LABEL[r.status] || r.status) : null, bad, late: r.status === 'late',
282
+ status: r.status || null, statusLabel: r.status && r.status !== 'on track' ? (STATUS_LABEL[r.status] || r.status) : null, bad, late: r.status === 'late', closed: !!r.closed,
282
283
  stage: r.stage || null, cat: r.cat || null, stageNote: r.stageSource === 'derived' ? 'derived' : null,
283
284
  seg: names.map((n, j) => (j < cur ? 'p' : j === cur ? 'n' : '')), progress: pctOf(r.progress),
284
285
  finish: r.end ? shortDate(r.end) : 'Not planned', over: !!over, start: r.start || null, end: r.end || null,
@@ -125,7 +125,8 @@ const npxArgs = (src, version, rest) => (src && src.kind === 'local'
125
125
  /** One changelog entry as the page may show it, or null: a version, a time, and PRs with a number, a summary, from
126
126
  * kit-changelog format 2 a title, and from format 3 a kind (k: new, imp, fix or x), which decides New, Improved or Fixed.
127
127
  * Kits published before kit-changelog dropped "[skip publish]" kept it in the text and lost the PR number to an inline
128
- * "(#123)"; both are repaired here, so every line ends in one muted number. */
128
+ * "(#123)"; both are repaired here, so every line ends in one muted number. From format 4 a PR may carry Claude's story for the
129
+ * PM (h: a plain title, d: what was missing and what is different now, top: a summary line that marks a highlight). */
129
130
  const KINDS = new Set(['new', 'imp', 'fix', 'x']);
130
131
  const SKIP_TAG = /\s*\[(?:skip|no)[ -][a-z]+\]/gi;
131
132
  const PR_REF = /\s*\(#(\d+)\)/;
@@ -135,7 +136,9 @@ function cleanPr(p) {
135
136
  const t = typeof p.t === 'string' ? p.t : '', ref = PR_REF.exec(t) || PR_REF.exec(p.s);
136
137
  const n = Number.isInteger(p.n) ? p.n : ref ? Number(ref[1]) : null;
137
138
  const tt = tidy(t).slice(0, 200), s = tidy(p.s).slice(0, 400);
138
- return { n, ...(tt ? { t: tt } : {}), s: s || tt, ...(KINDS.has(p.k) ? { k: p.k } : {}) };
139
+ const str = (x, max) => (typeof x === 'string' ? x.replace(/\s+/g, ' ').trim().slice(0, max) : '');
140
+ const h = str(p.h, 120), d = h ? str(p.d, 400) : '', top = h ? str(p.top, 200) : '';
141
+ return { n, ...(tt ? { t: tt } : {}), s: s || tt, ...(KINDS.has(p.k) ? { k: p.k } : {}), ...(h ? { h } : {}), ...(d ? { d } : {}), ...(top ? { top } : {}) };
139
142
  }
140
143
  function cleanEntry(e) {
141
144
  if (!e || !validVersion(e.v)) return null;
@@ -0,0 +1,199 @@
1
+ 'use strict';
2
+ /* next-prompt.js — Claude suggests the PM's next prompt, already sharpened (batch 26 item 2, design B on the main canvas: NpB, NpBw).
3
+ *
4
+ * Two jobs, both short Claude calls whose answer is a suggestion and never an act:
5
+ * next after a reply ends: up to two prompts the PM could send next, the first one recommended. A tap puts the text in the box; nothing sends.
6
+ * sharpen while the PM types a rough prompt: one sharper version in the PM's chosen style (Polish, Prompt engineer, Your own).
7
+ *
8
+ * PROVEN ONLY. A `next` card is one of MOVES, and a move is offered only while the code that makes it work is on disk: each move names the file
9
+ * and the text that prove it (lib/pmos-verbs.js for rehearse, ask the past and the drift replay; lib/routes-briefing-premium.js for "grill
10
+ * this"). movesOf(root) finds that line, and the card cites it, so lib/ai-guard.js check() verifies the quote at its line before the card shows.
11
+ * A `follow` card answers the question or the offer the reply ends with; it cites the line of the reply it answers (state:next-prompt/…).
12
+ * A sharpened prompt cites the PM's own words. A card with no verified source is dropped and never shown.
13
+ *
14
+ * MOVES · STYLES · JOB_NEXT · JOB_SHARPEN
15
+ * movesOf(root) → [{ id, where, use, ref, quote }] the moves whose proving line is on disk
16
+ * nextPrompt({ pm, reply, place, style, rules, moves }) → string the prompt for the `next` call
17
+ * sharpenPrompt({ text, reply, place, style, rules }) → string the prompt for the `sharpen` call
18
+ * parseNext(text, { moves, place }) → [{ move, text, from? }] the cards the answer holds, unknown moves and bad shapes left out (≤ 2)
19
+ * parseSharpen(text, rough) → string | null the sharper prompt, or null (no answer, or it says the same)
20
+ * same(a, b) → bool two prompts that say the same thing (≥ 90% the same words)
21
+ * proposalsOfNext(cards, { moves, replyRef }) → [ai-guard proposal] replyRef(from) → { ref, quote } | null
22
+ * proposalOfSharpen(text, roughRef) → ai-guard proposal
23
+ * rough(text) → bool 3 or more words and 12 or more characters, not a slash command
24
+ *
25
+ * Pure: no I/O apart from movesOf reading the proving files. lib/routes-next-prompt.js makes the call and files the cards through the guard. */
26
+ const fs = require('fs');
27
+ const path = require('path');
28
+ const crypto = require('crypto');
29
+
30
+ const JOB_NEXT = 'next-prompt';
31
+ const JOB_SHARPEN = 'sharpen-prompt';
32
+ const MAX_CARDS = 2;
33
+ const MAX_TEXT = 400;
34
+ const REPLY_MAX = 3000;
35
+ const GRILL_RE = /^\s*(please\s+)?(grill|re-?grill)\b/i; // the same test lib/routes-briefing-premium.js grillAsk makes
36
+
37
+ /* where: 'any' chat, or only an initiative's chat (its key starts init:). `find` is the text that proves the move works today. */
38
+ const MOVES = Object.freeze([
39
+ { id: 'follow', where: 'any', file: null,
40
+ use: 'Answer the question the reply asks, or take up the offer it makes, in the PM\'s words (for example "Yes, swap it for the period-end KR debrief."). Only when the reply asks or offers something. Put the exact line of the reply you answer in "from".' },
41
+ { id: 'grill', where: 'initiative', file: 'lib/routes-briefing-premium.js', find: 'const GRILL_RE = ',
42
+ use: 'Ask Claude to grill this initiative\'s plan. The text MUST start with "Grill this" and then say what to test it against (earlier decisions, scope, dates). Only when the reply is about this initiative\'s plan, scope, dates or a decision on it.' },
43
+ { id: 'past', where: 'initiative', file: 'lib/pmos-verbs.js', find: "past: Object.freeze({ module: 'past-cli.js'",
44
+ use: 'Ask why something was decided or dropped: "Why did we …?" or "Did we ever decide …?", answered from the records with file:line sources or "No record found". Only when the reply names an earlier decision, or something dropped, retired or replaced.' },
45
+ { id: 'rehearse', where: 'any', file: 'lib/pmos-verbs.js', find: "rehearse: Object.freeze({ module: 'rehearse-cli.js'",
46
+ use: 'Ask Claude to rehearse a change before making it: moving a task, changing an initiative\'s end date, or cutting a task or an initiative. It shows what breaks, helps and moves, then waits for the PM\'s yes. Only when the reply or the PM\'s message talks about such a change, naming the real task or initiative.' },
47
+ { id: 'drift', where: 'any', file: 'lib/pmos-verbs.js', find: "drift: Object.freeze({ module: 'drift-cli.js'",
48
+ use: 'Ask Claude to run the weekly drift check\'s replay on the PM\'s records ("Run the drift replay"). Only when the reply says the weekly "what you might have missed" check waits for its replay.' },
49
+ ]);
50
+
51
+ const STYLES = Object.freeze({
52
+ polish: 'Polish: make it clearer and shorter. Keep the PM\'s meaning, words and order where they work. Add nothing the PM did not ask for.',
53
+ engineer: 'Prompt engineer: build the best prompt for the job. Say the goal in the first words, name the real things (initiative, task, product, PR) from the context, and say what a good answer holds. Add no new request, step or condition the PM did not ask for. One or two short sentences, plain words, no role-play preamble, no markdown, no lists.',
54
+ own: 'The PM\'s own rules, below. Follow them; where they say nothing, keep the PM\'s meaning and make it clear.',
55
+ });
56
+
57
+ const one = (s, n) => String(s == null ? '' : s).replace(/\s+/g, ' ').trim().slice(0, n);
58
+ const norm = (s) => String(s == null ? '' : s).toLowerCase().replace(/[^\p{L}\p{N}\s]/gu, ' ').replace(/\s+/g, ' ').trim();
59
+ const words = (s) => norm(s).split(' ').filter(Boolean);
60
+ const rough = (t) => { const s = String(t || '').trim(); return !s.startsWith('/') && s.length >= 12 && words(s).length >= 3; };
61
+
62
+ /** The moves whose proving line is on disk now, each with its ref (path:line under root) and the line as its quote. */
63
+ function movesOf(root) {
64
+ const out = [], cache = new Map();
65
+ for (const m of MOVES) {
66
+ if (!m.file) { out.push({ id: m.id, where: m.where, use: m.use, ref: null, quote: null }); continue; }
67
+ if (!cache.has(m.file)) { let L = null; try { L = fs.readFileSync(path.join(root, m.file), 'utf8').split('\n'); } catch { L = null; } cache.set(m.file, L); }
68
+ const L = cache.get(m.file); if (!L) continue;
69
+ const i = L.findIndex((l) => l.includes(m.find)); if (i < 0) continue;
70
+ out.push({ id: m.id, where: m.where, use: m.use, ref: `${m.file}:${i + 1}`, quote: one(L[i], 200) });
71
+ }
72
+ return out;
73
+ }
74
+ const inInitiative = (place) => !!(place && place.initiative);
75
+ const usable = (moves, place) => (moves || []).filter((m) => m.where === 'any' || inInitiative(place));
76
+
77
+ function placeLines(place) {
78
+ if (!place) return 'The chat: Home (not tied to one initiative).';
79
+ const bits = [];
80
+ if (place.initiative) bits.push(`The chat: the initiative "${one(place.initiative, 120)}"${place.stage ? `, stage ${one(place.stage, 40)}` : ''}${place.product ? `, product ${one(place.product, 80)}` : ''}.`);
81
+ else bits.push(`The chat: ${one(place.title || 'Home', 120)} (not tied to one initiative).`);
82
+ return bits.join('\n');
83
+ }
84
+ const styleLines = (style, rules) => (STYLES[style] || STYLES.engineer) + (style === 'own' && rules ? '\nThe PM\'s rules:\n' + one(rules, 1000) : '');
85
+ const clip = (s, n) => { const t = String(s == null ? '' : s).trim(); return t.length > n ? t.slice(0, n) + ' …' : t; };
86
+
87
+ function nextPrompt({ pm, reply, place, style = 'engineer', rules = '', moves = [] } = {}) {
88
+ const ok = usable(moves, place);
89
+ return [
90
+ 'You suggest the next message a product manager (the PM) could send in a PMOS chat with Claude. You write it in the PM\'s voice, ready to send. You do not answer the PM, and you run nothing.',
91
+ '',
92
+ placeLines(place),
93
+ '',
94
+ 'The PM\'s last message:', '<pm>', clip(pm, 1200) || '(none)', '</pm>',
95
+ '',
96
+ 'Claude\'s reply:', '<reply>', clip(reply, REPLY_MAX), '</reply>',
97
+ '',
98
+ 'You may suggest only these moves. Each one works in PMOS today:',
99
+ ...ok.map((m) => `- ${m.id}: ${m.use}`),
100
+ '',
101
+ 'How to write each suggestion:',
102
+ styleLines(style, rules),
103
+ '',
104
+ 'Rules:',
105
+ '- At most 2 suggestions, the best first. Usually 1. Return none when the reply needs nothing from the PM or no move clearly fits: an empty list is a good answer.',
106
+ '- Use only names, numbers and dates that appear above. Never invent them.',
107
+ '- Never suggest something the reply says is already done, or that only repeats the reply.',
108
+ `- Each text is at most ${MAX_TEXT} characters.`,
109
+ '',
110
+ 'Answer with JSON only, no other text: {"cards":[{"move":"<id>","text":"<the prompt>","from":"<follow only: the exact line of the reply it answers>"}]}',
111
+ ].join('\n');
112
+ }
113
+
114
+ function sharpenPrompt({ text, reply, place, style = 'engineer', rules = '' } = {}) {
115
+ return [
116
+ 'A product manager (the PM) is typing a message to Claude in a PMOS chat. Rewrite it into a sharper prompt, ready to send. You do not answer it, and you run nothing.',
117
+ '',
118
+ placeLines(place),
119
+ '',
120
+ reply ? 'Claude\'s last reply, for context:\n<reply>\n' + clip(reply, 1500) + '\n</reply>\n' : '',
121
+ 'What the PM typed:', '<draft>', clip(text, 1500), '</draft>',
122
+ '',
123
+ 'Style:',
124
+ styleLines(style, rules),
125
+ '',
126
+ 'Rules:',
127
+ '- Keep what the PM asks for. Do not add a new request or step, and do not answer it. Say nothing about how Claude should read or check data.',
128
+ '- When the draft asks to move a task, change an end date or cut work, ask Claude to rehearse it first ("Rehearse moving …") and wait for the PM\'s yes: PMOS does that today.',
129
+ '- Use only names, numbers and dates from the draft and the context above. Never invent them.',
130
+ '- Write in the PM\'s voice, as the message the PM sends. Do not guess what "here" or "this" points at when the context does not say.',
131
+ `- At most ${MAX_TEXT} characters.`,
132
+ '- If the draft is already as sharp as it can be in this style, say so.',
133
+ '',
134
+ 'Answer with JSON only, no other text: {"text":"<the sharper prompt>"} or {"same":true}',
135
+ ].join('\n');
136
+ }
137
+
138
+ /** The first JSON object in a model's answer (it may wrap it in a code fence or a sentence). */
139
+ function jsonOf(text) {
140
+ const s = String(text == null ? '' : text);
141
+ const a = s.indexOf('{'), b = s.lastIndexOf('}');
142
+ if (a < 0 || b <= a) return null;
143
+ try { return JSON.parse(s.slice(a, b + 1)); } catch { return null; }
144
+ }
145
+
146
+ function parseNext(text, { moves = [], place = null } = {}) {
147
+ const o = jsonOf(text); if (!o || !Array.isArray(o.cards)) return [];
148
+ const ok = new Map(usable(moves, place).map((m) => [m.id, m]));
149
+ const out = [];
150
+ for (const c of o.cards) {
151
+ if (out.length >= MAX_CARDS) break;
152
+ if (!c || typeof c !== 'object') continue;
153
+ const move = String(c.move || ''), t = one(c.text, MAX_TEXT + 1);
154
+ if (!ok.has(move) || !t || t.length > MAX_TEXT) continue;
155
+ if (move === 'grill' && !GRILL_RE.test(t)) continue; // the chat starts a grill run only on a message that starts with grill
156
+ if (move === 'follow' && !one(c.from, 400)) continue;
157
+ if (out.some((x) => same(x.text, t))) continue;
158
+ out.push({ move, text: t, ...(move === 'follow' ? { from: one(c.from, 400) } : {}) });
159
+ }
160
+ return out;
161
+ }
162
+
163
+ /** Two prompts that say the same: the same words, or at least 90% of the words of both shared. */
164
+ function same(a, b) {
165
+ const x = words(a), y = words(b);
166
+ if (!x.length || !y.length) return x.length === y.length;
167
+ if (x.join(' ') === y.join(' ')) return true;
168
+ const X = new Set(x), Y = new Set(y);
169
+ let both = 0; for (const w of X) if (Y.has(w)) both++;
170
+ return both / Math.max(X.size, Y.size) >= 0.9;
171
+ }
172
+
173
+ function parseSharpen(text, roughText) {
174
+ const o = jsonOf(text); if (!o || o.same === true) return null;
175
+ const t = one(o.text, MAX_TEXT + 1);
176
+ if (!t || t.length > MAX_TEXT || same(t, roughText)) return null;
177
+ return t;
178
+ }
179
+
180
+ const idOf = (prefix, s) => prefix + ':' + crypto.createHash('sha1').update(String(s)).digest('hex').slice(0, 12);
181
+
182
+ function proposalsOfNext(cards, { moves = [], replyRef = () => null } = {}) {
183
+ const by = new Map(moves.map((m) => [m.id, m]));
184
+ return (cards || []).map((c, i) => {
185
+ const m = by.get(c.move) || {};
186
+ const src = c.move === 'follow' ? replyRef(c.from) : (m.ref ? { ref: m.ref, quote: m.quote } : null);
187
+ return {
188
+ id: idOf('np', c.move + '\n' + c.text), title: c.move, say: c.text, confidence: 'medium',
189
+ claims: [{ text: c.move === 'follow' ? 'It answers the reply' : 'This move works in PMOS today', sources: src ? [{ label: c.move === 'follow' ? 'the reply' : m.file || c.move, ref: src.ref, quote: src.quote }] : [] }],
190
+ data: { move: c.move, rec: i === 0 },
191
+ };
192
+ });
193
+ }
194
+ function proposalOfSharpen(text, src) {
195
+ return { id: idOf('sh', text), title: 'sharper', say: text, confidence: 'medium',
196
+ claims: [{ text: 'It keeps what the PM typed', sources: src ? [{ label: 'what you typed', ref: src.ref, quote: src.quote }] : [] }], data: { kind: 'sharper' } };
197
+ }
198
+
199
+ module.exports = { JOB_NEXT, JOB_SHARPEN, MAX_CARDS, MAX_TEXT, MOVES, STYLES, GRILL_RE, movesOf, nextPrompt, sharpenPrompt, parseNext, parseSharpen, same, rough, jsonOf, proposalsOfNext, proposalOfSharpen };
@@ -114,6 +114,7 @@ const MODULES = Object.freeze([
114
114
  'routes-goal-status', // batch 16 item 6: the goals' one status from readings (product page), notify choice, recipe, Read now, test read, CSV readings
115
115
  'routes-refresh', // data-refresh-settings: Settings › Data refresh: what refreshes, when, the refresh log, Refresh now
116
116
  'routes-impact-views', // batch 16 item 7 (Your impact, Phase 4): the chapter card's read, a stage that closed this week (boards.jsonl; read-only)
117
+ 'routes-next-prompt', // batch 26 item 2: the chat's suggested next prompt and Sharper card (lib/next-prompt.js, through lib/ai-guard.js)
117
118
  'routes-drift', // weekly-drift-check: What would I have missed, Home's weekly ✦ and its ⋯ drop and Undo (runtime drift/drift.jsonl; page-press POST)
118
119
  ]);
119
120
  const PM_DIR = path.join(__dirname, '..', 'public', 'pm');
@@ -477,6 +477,16 @@ const gapFields = (spans) => ({ gaps: holdSpans.planGaps(spans, 14), stageSpans:
477
477
  const minDay = (a, b) => (!a ? b || null : !b ? a : a < b ? a : b);
478
478
  const maxDay = (a, b) => (!a ? b || null : !b ? a : a > b ? a : b);
479
479
 
480
+ /** batch 26 item 5: whether an initiative stands in its workflow's terminal stage, read from the workflow, never from a list of names. On a
481
+ * D99 workflow: the stage is in the Done category, or it is the workflow's last stage. On a board's own column workflow: its last column. */
482
+ function closedOf(row, wx) {
483
+ if (wx.own) {
484
+ const st = wx.stages.find((x) => x.key === wx.stage);
485
+ return !!st && (st.cat === 'done' || (st.of > 1 && st.n === st.of));
486
+ }
487
+ const wf = Array.isArray(row.workflow) ? row.workflow : [];
488
+ return wf.length > 1 && wf.findIndex((x) => x.name === row.stage) === wf.length - 1;
489
+ }
480
490
  function boardRow(board, base, folded, lib) {
481
491
  const s = B.stageOf(board, base.id, base.reading, folded);
482
492
  const ts = taskSpanOf(folded, base.id); // the bar covers the breakdown items and the tasks
@@ -496,6 +506,10 @@ function boardRow(board, base, folded, lib) {
496
506
  const wx = workflowRowOf(board, base, folded, lib || libOf(folded), s.stage);
497
507
  Object.assign(row, wfRowFields(wx));
498
508
  if (wx.own) { row.stage = (wx.stages.find((x) => x.key === wx.stage) || {}).name || row.stage; row.cat = null; row.stageSource = 'move'; }
509
+ // batch 26 item 5: an initiative in its workflow's terminal stage is finished, so it is never late or at risk. Every surface (the Board's
510
+ // tone, dot, late line and edge tabs, Home, Kanban, the program rollups) reads the status this leaves and row.closed.
511
+ row.closed = closedOf(row, wx);
512
+ if (row.closed && (row.status === 'late' || row.status === 'at risk')) row.status = 'on track';
499
513
  const w = [];
500
514
  if (s.source === 'move') {
501
515
  const last = [...folded.moves].reverse().find((m) => m.board === board.id && m.id === base.id);
@@ -1822,4 +1836,4 @@ function meetingBoards(ctx) {
1822
1836
  return { list, detail };
1823
1837
  }
1824
1838
 
1825
- module.exports = { prefixes: ['/api/boards'], handle, tasksOfIni, postFrame, needRows, itemsOf, readLedger, foldOf, epicsOf, rowsOfBoard, invalidateRows: (w) => { rowsLib.invalidate(w.root); rowCache.delete(w.root); }, move, switchWorkflow, taskState, taskAdd, taskEdit, writeAs, taskDelete, taskRestore, initiativeWorkflow, seedInitiative, stageTasks, boardIndex, appendScanAsk, scanDeclineSignal, createAndPlace, startOnDefault, ledgerFile, append, frameFor, boardIdOf, meetingBoards, boardsOf, baseRows, boardRow, taskSpanOf, workflowRowOf, foldOf, portfolio, SELF_REFUSAL, NO_STATE };
1839
+ module.exports = { prefixes: ['/api/boards'], handle, tasksOfIni, postFrame, needRows, itemsOf, readLedger, foldOf, epicsOf, rowsOfBoard, invalidateRows: (w) => { rowsLib.invalidate(w.root); rowCache.delete(w.root); }, move, switchWorkflow, taskState, taskAdd, taskEdit, writeAs, taskDelete, taskRestore, initiativeWorkflow, seedInitiative, stageTasks, boardIndex, appendScanAsk, scanDeclineSignal, createAndPlace, startOnDefault, ledgerFile, append, frameFor, boardIdOf, meetingBoards, boardsOf, baseRows, boardRow, taskSpanOf, workflowRowOf, closedOf, foldOf, portfolio, SELF_REFUSAL, NO_STATE };