create-pmos 0.498.0 → 0.500.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.498.0",
3
+ "version": "0.500.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": {
@@ -32,6 +32,44 @@
32
32
  "audit"
33
33
  ],
34
34
  "pmosChanges": [
35
+ {
36
+ "v": "0.500.0",
37
+ "at": "2026-11-03T04:17:03.191Z",
38
+ "f": 3,
39
+ "prs": [
40
+ {
41
+ "n": 1183,
42
+ "t": "Delete a captured meeting-note point from its edit card",
43
+ "s": "Delete a captured meeting-note point from its edit card",
44
+ "k": "new"
45
+ }
46
+ ]
47
+ },
48
+ {
49
+ "v": "0.499.0",
50
+ "at": "2026-11-03T03:25:28.533Z",
51
+ "f": 3,
52
+ "prs": [
53
+ {
54
+ "n": 1180,
55
+ "t": "The context meter knows the default model is 1M",
56
+ "s": "The context meter knows the default model is 1M",
57
+ "k": "fix"
58
+ },
59
+ {
60
+ "n": 1181,
61
+ "t": "`pmos initiative list` names records on a document lane",
62
+ "s": "Pmos initiative list names records on a document lane",
63
+ "k": "fix"
64
+ },
65
+ {
66
+ "n": 1182,
67
+ "t": "Claude login test passes for a signed-in new user; picker shows each login once",
68
+ "s": "Claude login test passes for a signed-in new user; picker shows each login once",
69
+ "k": "fix"
70
+ }
71
+ ]
72
+ },
35
73
  {
36
74
  "v": "0.498.0",
37
75
  "at": "2026-11-03T02:50:09.805Z",
@@ -384,31 +422,6 @@
384
422
  "s": "Batch 22: feedback fixes (held for one merge)"
385
423
  }
386
424
  ]
387
- },
388
- {
389
- "v": "0.480.0",
390
- "at": "2026-11-01T11:08:09.778Z",
391
- "f": 3,
392
- "prs": [
393
- {
394
- "n": 1139,
395
- "t": "Set up a custom report from chat in one press (held)",
396
- "s": "Set up a custom report from chat in one press (held)",
397
- "k": "fix"
398
- }
399
- ]
400
- },
401
- {
402
- "v": "0.479.0",
403
- "at": "2026-11-01T10:19:06.142Z",
404
- "f": 3,
405
- "prs": [
406
- {
407
- "n": 1137,
408
- "t": "Batch 21: feedback fixes (held for one merge)",
409
- "s": "Batch 21: feedback fixes (held for one merge)"
410
- }
411
- ]
412
425
  }
413
426
  ]
414
427
  }
@@ -1,3 +1,3 @@
1
- kernel-source: pmos@1a05518b
1
+ kernel-source: pmos@74d94d5f
2
2
  assembled: 2026-11-03
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-11-03T02:50:05Z"}
5
+ generated: {by: process:create-pmos, at: "2026-11-03T04:17:00Z"}
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-11-03T02:50:05Z"}
5
+ generated: {by: process:create-pmos, at: "2026-11-03T04:17:00Z"}
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-11-03T02:50:05Z"}
5
+ generated: {by: process:create-pmos, at: "2026-11-03T04:17:00Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -1,4 +1,42 @@
1
1
  [
2
+ {
3
+ "v": "0.500.0",
4
+ "at": "2026-11-03T04:17:03.191Z",
5
+ "f": 3,
6
+ "prs": [
7
+ {
8
+ "n": 1183,
9
+ "t": "Delete a captured meeting-note point from its edit card",
10
+ "s": "Delete a captured meeting-note point from its edit card",
11
+ "k": "new"
12
+ }
13
+ ]
14
+ },
15
+ {
16
+ "v": "0.499.0",
17
+ "at": "2026-11-03T03:25:28.533Z",
18
+ "f": 3,
19
+ "prs": [
20
+ {
21
+ "n": 1180,
22
+ "t": "The context meter knows the default model is 1M",
23
+ "s": "The context meter knows the default model is 1M",
24
+ "k": "fix"
25
+ },
26
+ {
27
+ "n": 1181,
28
+ "t": "`pmos initiative list` names records on a document lane",
29
+ "s": "Pmos initiative list names records on a document lane",
30
+ "k": "fix"
31
+ },
32
+ {
33
+ "n": 1182,
34
+ "t": "Claude login test passes for a signed-in new user; picker shows each login once",
35
+ "s": "Claude login test passes for a signed-in new user; picker shows each login once",
36
+ "k": "fix"
37
+ }
38
+ ]
39
+ },
2
40
  {
3
41
  "v": "0.498.0",
4
42
  "at": "2026-11-03T02:50:09.805Z",
@@ -53,7 +53,7 @@ Press the ✦ to open "Claude proposes" in the Viewer. The groups come in this o
53
53
 
54
54
  In the pill menu on an initiative's row, a session that is idle and safe to close is light blue. Its line starts with ✦ and says why, and its ✓ closes it. An idle session that is not safe has no ✦ and no ✓. Its line says what holds it, such as "3 files not committed".
55
55
 
56
- Each session shows how full its context window is, as a small pie in the pill list, the chat header, the phone top bar and the Live pill. The pie shows from 50% (grey). It turns amber from 70% and red from 90%. Hover it to see the tokens used, such as "156k of 200k used · last turn 10:12". The window is 1M when the session runs a 1M model, else 200k. From 70%, when it is your turn, Claude proposes Compact: the row's ✦ line says so, and its ✓ types /compact into the session. The chat shows the same offer as a chip. A session that is safe to close shows that ✦ instead. ✕ hides the Compact offer until the context is 10 points fuller.
56
+ Each session shows how full its context window is, as a small pie in the pill list, the chat header, the phone top bar and the Live pill. The pie shows from 50% (grey). It turns amber from 70% and red from 90%. Hover it to see the tokens used, such as "156k of 200k used · last turn 10:12". The window is 1M when the session runs a 1M model, else 200k. Claude Code's default model, Opus 5.5, is a 1M model, so a session you start without choosing a model has a 1M window. If CLAUDE_CODE_DISABLE_1M_CONTEXT is set, the window is 200k. From 70%, when it is your turn, Claude proposes Compact: the row's ✦ line says so, and its ✓ types /compact into the session. The chat shows the same offer as a chip. A session that is safe to close shows that ✦ instead. ✕ hides the Compact offer until the context is 10 points fuller.
57
57
 
58
58
  ✓ in the header keeps everything shown. Each proposal has its own ✓ and ✕, and ⌄ shows its rows. ✕ on a row leaves it out of that group for good, with Undo on the row. One toast says what was placed, with Undo. History lists each approval with its own Undo. An initiative that was started stays when you undo: close it on the Board.
59
59
 
@@ -182,7 +182,7 @@ There is no My work page. The list on Home is your list:
182
182
  - **One chat box.** Every conversation has the same box: **+** (attach a file, notes, the terminal, and **Link to an initiative** for a loose chat), **/** for commands, **@** inside an initiative, the To or In chip, the mic, and Send. A chat opens at its newest message.
183
183
  - **A session's terminal.** **Watch its terminal** in **+**, or a session row such as **Claude sign-in**, opens the session's terminal in the Viewer. While the session runs, it is a real terminal: type in it to answer Claude or paste a code. When the session has ended, the Viewer shows it read only and says so.
184
184
  - **Setting up your first project opens by itself** until a registered folder holds an initiative or setup has finished. The `PMOS` folder that the one-line installer makes holds no initiative yet, so a new user who opens PMOS from the Start menu or the Dock still gets the setup conversation, not an empty Home.
185
- - **Signing in to Claude when you set up your first project.** If the login you choose is signed in already, PMOS does not open a terminal. It tests the login at once. Otherwise PMOS runs `claude auth login`: Claude opens your browser to sign in, and the terminal under the question shows only Claude's request for a code, if it needs one. Claude's theme and trust-this-folder screens do not appear. When the sign-in ends, PMOS tests the login by itself. **The Claude login on this computer** is the login your usual terminal uses (`~/.claude.json`); PMOS no longer points it at a different file.
185
+ - **Signing in to Claude when you set up your first project.** If the login you choose is signed in already, PMOS does not open a terminal. It tests the login at once. Otherwise PMOS runs `claude auth login`: Claude opens your browser to sign in, and the terminal under the question shows only Claude's request for a code, if it needs one. Claude's theme and trust-this-folder screens do not appear. When the sign-in ends, PMOS tests the login by itself. **The Claude login on this computer** is the login your usual terminal uses (`~/.claude.json`); PMOS no longer points it at a different file. You choose between two logins only: **The Claude login on this computer** and **A separate login just for PMOS**. The test passes for a login that works in your terminal. Claude updates its own files when it runs, and the test does not count that as a changed setup.
186
186
  - **Documents not written yet.** A document that does not exist yet shows **Draft it**, in its stage or ahead of it. The agent writes a draft. Nothing is issued until you read it.
187
187
  - **Meeting notes.** A decision, to-do or question that the notes caught is not a fact until you confirm it in the meeting's Replay. Home, Your day and the evening recap do not count unconfirmed points.
188
188
 
@@ -2,7 +2,7 @@
2
2
  type: Guide
3
3
  title: "Guide: notes"
4
4
  description: "How to take notes in a meeting: one note top to bottom, typed notes as timed lines, key points written each minute and labelled by Jev, delete a note; final notes written after Stop from the whole recording, with speakers and a quote for each item, decisions with their why and who, next steps with an open question, a discussion paragraph, and Wispr Flow's notes merged in and fetched by hand when the fetch fails; listen to your microphone and the meeting on this machine, see decisions, actions and questions caught as they are said, check them against the repository for conflicts, capture highlights during the meeting, create tasks from next steps, correct and tag lines, see what changed in your notes, replay the meeting, see screenshots of the shared screen beside the transcript, link a note to its meeting and its initiative, and get a summary."
5
- timestamp: 2026-11-02
5
+ timestamp: 2026-11-03
6
6
  ---
7
7
 
8
8
  # Guide: notes
@@ -238,6 +238,12 @@ Press **Replay** beside the player (in Calendar › Notes, or on the meeting's c
238
238
 
239
239
  The page reads only records on this machine: the note, the file Focus kept for the meeting, and the screen shots.
240
240
 
241
+ ### Fix or delete a key point
242
+
243
+ In a meeting's Viewer, press a decision, a question or an action to open its edit card. Change the kind or the words, then press **Save**. PMOS remembers what you meant for the next notes, unless you press **Only fix this one**.
244
+
245
+ To delete the point, press the trash on the edit card. The point leaves at once, with no question first. **Undo** shows for 8 seconds and puts it back. A deleted point stays deleted after a reload. It is not in **Left out** and it is not counted. A delete does not teach PMOS anything, and nothing is sent outside PMOS.
246
+
241
247
  The replay page was part of the console, which is deleted. A replay link (for example `/c3.html?replay=n-abcd-1234`) now opens Home.
242
248
 
243
249
  ### Clean up the replay sound
@@ -8,6 +8,7 @@ const cp = require('child_process');
8
8
  const win = require('./win-command');
9
9
  const claudeHome = require('./claude-home');
10
10
  const DEFAULT = '__default__';
11
+ const OWN_ID = require('./own-profile').ID; // PMOS's separate login (lib/own-profile.js)
11
12
  const PROMPT = 'Reply with exactly PMOS_READY. Do not use tools.';
12
13
  const FLAGS = ['--safe-mode', '--tools', '--strict-mcp-config', '--mcp-config', '--disable-slash-commands', '--no-session-persistence', '--output-format'];
13
14
  const MESSAGES = {
@@ -38,7 +39,15 @@ function executable(env) {
38
39
  const file = String(env.PATH || '').split(path.delimiter).map(d => path.join(d, 'claude')).find(f => { try { fs.accessSync(f, fs.constants.X_OK); return fs.statSync(f).isFile(); } catch { return false; } });
39
40
  return { file: file || 'claude', prefix: [] };
40
41
  }
41
- function stamp(f) { try { const s = fs.statSync(f); return [s.mtimeMs, s.size]; } catch { return null; } }
42
+ // The parts of settings.json that decide which account Claude uses: only these are fingerprinted, as a hash, never returned.
43
+ const AUTH_ENV = ['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_BASE_URL', 'CLAUDE_CODE_USE_BEDROCK', 'CLAUDE_CODE_USE_VERTEX', 'CLAUDE_CODE_USE_FOUNDRY'];
44
+ function settingsAuth(f) {
45
+ let j; try { j = JSON.parse(fs.readFileSync(f, 'utf8')); } catch (e) { return e && e.code === 'ENOENT' ? null : 'unreadable'; }
46
+ if (!j || typeof j !== 'object') return 'unreadable';
47
+ const env = j.env && typeof j.env === 'object' ? j.env : {};
48
+ const picked = { apiKeyHelper: j.apiKeyHelper || '', forceLoginMethod: j.forceLoginMethod || '', forceLoginOrgUUID: j.forceLoginOrgUUID || '', env: AUTH_ENV.map(k => [k, env[k] || '']) };
49
+ return crypto.createHash('sha256').update(JSON.stringify(picked)).digest('hex');
50
+ }
42
51
  function atomic(file, value) {
43
52
  fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
44
53
  if (fs.existsSync(file) && fs.lstatSync(file).isSymbolicLink()) throw new Error('symlink');
@@ -56,18 +65,20 @@ function create({ stateDir, home = os.homedir(), readProfiles, readPrefs, savePr
56
65
  if (typeof id !== 'string' || !/^[a-zA-Z0-9_.-]{1,80}$/.test(id)) return null;
57
66
  // The usual login is Claude with no CLAUDE_CONFIG_DIR at all, unless the PM's own environment sets one. Naming ~/.claude
58
67
  // here made Claude read ~/.claude/.claude.json and miss the login in ~/.claude.json (lib/claude-home.js).
59
- if (id === DEFAULT) { const inherited = envFor(null).CLAUDE_CONFIG_DIR; return { id, label: 'My usual Claude login', env: inherited ? { CLAUDE_CONFIG_DIR: path.resolve(inherited) } : {} }; }
68
+ if (id === DEFAULT) { const inherited = envFor(null).CLAUDE_CONFIG_DIR; return { id, label: 'The Claude login on this computer', env: inherited ? { CLAUDE_CONFIG_DIR: path.resolve(inherited) } : {} }; }
60
69
  return (readProfiles().profiles || []).find(p => p.id === id) || null;
61
70
  }
62
71
  function binding(p) {
63
72
  const env = envFor(p);
64
73
  const command = resolveExecutable(env);
65
74
  const dir = path.resolve(env.CLAUDE_CONFIG_DIR || path.join(home, '.claude'));
66
- const authNames = ['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_BASE_URL', 'CLAUDE_CODE_USE_BEDROCK', 'CLAUDE_CODE_USE_VERTEX', 'CLAUDE_CODE_USE_FOUNDRY'];
67
75
  // the env as it is applied (a CLAUDE_CONFIG_DIR that names only ~/.claude sets nothing), so an old descriptor and a new one match
68
76
  const applied = claudeHome.overlay(p.env, { home, inherited: envFor(null).CLAUDE_CONFIG_DIR || '' });
69
- const input = { command, executable: stamp(command.file), env: Object.entries(applied).sort(), dir,
70
- auth: authNames.map(k => [k, env[k] || '']), config: [path.join(dir, 'settings.json'), claudeHome.configFile(env, home), path.join(dir, '.credentials.json')].map(stamp) };
77
+ // Only what decides the login: the command, the env, the folder and the auth settings. NOT the timestamps of ~/.claude.json,
78
+ // .credentials.json or the executable: every `claude -p` rewrites ~/.claude.json and may refresh its token, and Claude Code
79
+ // updates itself, so those made every check end "The Claude setup changed during the check" (bug 2026-11-03, new user).
80
+ const input = { command: { file: command.file, prefix: command.prefix }, env: Object.entries(applied).sort(), dir,
81
+ auth: AUTH_ENV.map(k => [k, env[k] || '']), settings: settingsAuth(path.join(dir, 'settings.json')) };
71
82
  return { env, command, fingerprint: crypto.createHash('sha256').update(JSON.stringify(input)).digest('hex') };
72
83
  }
73
84
  // The sentence for a state. A signed-out or unverified login names the login it checked and its folder, because signing in to another
@@ -88,9 +99,21 @@ function create({ stateDir, home = os.homedir(), readProfiles, readPrefs, savePr
88
99
  const state = valid ? r.state : 'unchecked';
89
100
  return { state, message: say(state, p), profile: id, label: p.label || id, checked_at: r && r.checked_at || null, ready: state === 'ready' && selected() === id };
90
101
  }
102
+ /** The logins to choose from, one row per login that really differs (bug 2026-11-03: "My usual Claude login" and "PMOS"
103
+ * showed beside the two options they repeat). A profile with the same binding as the login on this computer is that login,
104
+ * so it folds into it; PMOS's own profile is the "separate login" option, so it is named `own` and not listed again.
105
+ * `alias` maps a folded id to the row that shows it. */
91
106
  function list() {
92
107
  const read = readProfiles();
93
- return { ok: true, selected: selected(), status: status(), profiles: [candidate(DEFAULT), ...(read.profiles || [])].map(p => ({ id: p.id, label: p.label || p.id })), broken: (read.broken || []).map(b => ({ file: b.file })), discovery_failed: read.ok === false };
108
+ const usual = candidate(DEFAULT), usualPrint = binding(usual).fingerprint;
109
+ const alias = {}, rows = [{ id: DEFAULT, label: usual.label }];
110
+ let own = null;
111
+ for (const p of read.profiles || []) {
112
+ if (p.id === OWN_ID) { own = p.id; continue; }
113
+ let same = false; try { same = binding(p).fingerprint === usualPrint; } catch {}
114
+ if (same) alias[p.id] = DEFAULT; else rows.push({ id: p.id, label: p.label || p.id });
115
+ }
116
+ return { ok: true, selected: selected(), status: status(), profiles: rows, alias, own, broken: (read.broken || []).map(b => ({ file: b.file })), discovery_failed: read.ok === false };
94
117
  }
95
118
  function pinDefault(p) {
96
119
  if (p.id !== DEFAULT) return p.id;
@@ -5,26 +5,55 @@ const path = require('path');
5
5
  /* context-use.js — how full a session's context window is, and whether Claude proposes to compact it (batch 13 item 8, canvas XctxA1Desk).
6
6
  *
7
7
  * of(seen, hints) → { used, window, pct, level, at } | null
8
- * seen = transcript-read's `context`: { used, peak, at } of the newest real (not subagent) model turn, or null
9
- * hints = model ids that can name the window: the model PMOS started the session with, the CLI's own setting
8
+ * seen = transcript-read's `context`: { used, peak, at, model } of the newest real (not subagent) model turn, or null
9
+ * hints = model ids that can name the window: the model PMOS started the session with, the CLI's own setting (cliHints, which
10
+ * also carries OFF_HINT when CLAUDE_CODE_DISABLE_1M_CONTEXT is set)
11
+ * native1M(model) → true for a model whose window is 1M with no [1m] suffix
10
12
  * advise(ctx, shown) → 'compact' | null
11
- * cliHints(root, home?) → the model the CLI itself is set to: `model` in ~/.claude and <root>/.claude settings, and ANTHROPIC_MODEL
13
+ * cliHints(root, home?) → the model the CLI itself is set to: `model` in ~/.claude and <root>/.claude settings, and ANTHROPIC_MODEL;
14
+ * OFF_HINT last when CLAUDE_CODE_DISABLE_1M_CONTEXT is set in the environment or in a settings file's `env`
12
15
  * The words under the wedge ("116k of 200k used · last turn 10:12") are drawn by pm/ctx-wedge.js, in the PM's own clock.
13
16
  *
14
17
  * THE NUMBER is what the next turn starts from: the newest reply's input + cache-read + cache-write tokens (what the model was sent) plus
15
- * its output. It is not a running total, so it drops after a compact. THE WINDOW is 1M when a hint names a [1m] model or a turn already
16
- * went past 200k, else 200k (the CLI's default). With no turn read, or an agent that is not Claude, there is nothing: no %, no advice.
18
+ * its output. It is not a running total, so it drops after a compact. THE WINDOW is 1M when a turn already went past 200k, when a hint
19
+ * names a [1m] model, or when the model is one the CLI runs with 1M by itself (batch 25 item 8: Claude Code's model docs list Fable,
20
+ * Opus 4.7 and later, Sonnet 5 and later and Haiku 5.5; the CLI's default model, Opus 5.5, is one of them). The model of the newest turn
21
+ * in the transcript decides; with none read, the first hint does. CLAUDE_CODE_DISABLE_1M_CONTEXT makes the CLI treat every model as 200k, so it does
22
+ * here too. An unknown model is 200k. With no turn read, or an agent that is not Claude, there is nothing: no %, no advice.
17
23
  * Advice only on the PM's turn (a session that works is never told to compact), and only from 70%. */
18
24
 
19
25
  const BASE = 200000;
20
26
  const LONG = 1000000;
21
27
  const LEVELS = [[90, 'crit'], [70, 'warn'], [50, 'quiet']];
22
28
 
29
+ const OFF_HINT = '[no-1m]'; // not a model: cliHints' word for CLAUDE_CODE_DISABLE_1M_CONTEXT, so a caller passes one list
23
30
  const isLong = (m) => /\[1m\]/i.test(String(m || ''));
24
31
 
32
+ /* The release a model id names: claude-opus-4-8 → [4, 8], claude-sonnet-5-20260101 → [5, 0], opus → null (an alias: the latest). */
33
+ function versionOf(model, family) {
34
+ const m = String(model).match(new RegExp(`${family}-(\\d+)(?:[-.](\\d{1,2}))?(?!\\d)`, 'i'));
35
+ return m ? [Number(m[1]), Number(m[2] || 0)] : null;
36
+ }
37
+ const atLeast = (v, major, minor) => v[0] > major || (v[0] === major && v[1] >= minor);
38
+ function native1M(model) {
39
+ const s = String(model || '').toLowerCase();
40
+ if (!s) return false;
41
+ if (isLong(s)) return true;
42
+ if (/claude-\d/.test(s)) return false; // the old naming (claude-3-5-sonnet): all 200k
43
+ if (/fable/.test(s)) return true;
44
+ for (const [family, major, minor] of [['opus', 4, 7], ['sonnet', 5, 0], ['haiku', 5, 5]]) {
45
+ if (!new RegExp(`(^|[^a-z])${family}([^a-z]|$)`).test(s)) continue;
46
+ const v = versionOf(s, family);
47
+ return v ? atLeast(v, major, minor) : true; // a bare alias names the latest release
48
+ }
49
+ return false;
50
+ }
51
+
25
52
  function of(seen, hints = []) {
26
53
  if (!seen || !(seen.used > 0)) return null;
27
- const window = hints.some(isLong) || (seen.peak || 0) > BASE ? LONG : BASE;
54
+ const models = hints.filter((h) => h && h !== OFF_HINT);
55
+ const long = !hints.includes(OFF_HINT) && (models.some(isLong) || native1M(seen.model || models[0]));
56
+ const window = long || (seen.peak || 0) > BASE ? LONG : BASE;
28
57
  const pct = Math.min(100, Math.floor((seen.used / window) * 100)); // floor: 69.6% is not yet 70%
29
58
  const lv = LEVELS.find(([n]) => pct >= n);
30
59
  return { used: seen.used, window, pct, level: lv ? lv[1] : null, at: seen.at || null };
@@ -37,18 +66,31 @@ function advise(ctx, shown) {
37
66
 
38
67
  /* The CLI's own model setting. A session PMOS started with no model choice runs the CLI's default, and that setting is what can make it a
39
68
  * [1m] model. Read again only when a settings file changes (sessionList runs on every broadcast). */
40
- const memo = new Map(); // file → { stamp, model }
41
- function modelIn(file) {
69
+ const memo = new Map(); // file → { stamp, model, off }
70
+ const OFF = 'CLAUDE_CODE_DISABLE_1M_CONTEXT';
71
+ const isOn = (v) => v != null && v !== '' && !/^(0|false|no)$/i.test(String(v));
72
+ function settingsIn(file) {
42
73
  let stamp = null; try { const st = fs.statSync(file); stamp = st.size + ':' + st.mtimeMs; } catch { return null; }
43
- const m = memo.get(file); if (m && m.stamp === stamp) return m.model;
44
- let model = null; try { const j = JSON.parse(fs.readFileSync(file, 'utf8')); model = typeof j.model === 'string' ? j.model : null; } catch { model = null; }
45
- memo.set(file, { stamp, model });
46
- return model;
74
+ const m = memo.get(file); if (m && m.stamp === stamp) return m;
75
+ let model = null, off = false;
76
+ try {
77
+ const j = JSON.parse(fs.readFileSync(file, 'utf8'));
78
+ model = typeof j.model === 'string' ? j.model : null;
79
+ off = !!(j.env && typeof j.env === 'object' && isOn(j.env[OFF]));
80
+ } catch { model = null; off = false; }
81
+ const out = { stamp, model, off };
82
+ memo.set(file, out);
83
+ return out;
47
84
  }
48
- function cliHints(root, home = os.homedir()) {
85
+ function settingsFiles(root, home) {
49
86
  const files = [path.join(home, '.claude', 'settings.json')];
50
87
  if (root) files.push(path.join(String(root), '.claude', 'settings.json'), path.join(String(root), '.claude', 'settings.local.json'));
51
- return files.map(modelIn).concat(process.env.ANTHROPIC_MODEL || null).filter(Boolean);
88
+ return files;
89
+ }
90
+ function cliHints(root, home = os.homedir()) {
91
+ const read = settingsFiles(root, home).map((f) => settingsIn(f) || {});
92
+ const off = isOn(process.env[OFF]) || read.some((x) => x.off);
93
+ return read.map((x) => x.model || null).concat(process.env.ANTHROPIC_MODEL || null, off ? OFF_HINT : null).filter(Boolean);
52
94
  }
53
95
 
54
- module.exports = { of, advise, cliHints, BASE, LONG };
96
+ module.exports = { of, advise, cliHints, native1M, OFF_HINT, BASE, LONG };
@@ -18,7 +18,9 @@
18
18
  * PM types the confirmation at a terminal, and an agent session passes with its own actor (D98); every other command
19
19
  * takes lib/actor.js fromCli: the PM at a terminal, the bound agent session from
20
20
  * PMOS_SESSION, `agent:unknown` otherwise. An agent's placement applies at once and its line names the agent (D98).
21
- * Work lanes only (D86 point 4): `list` and `export` leave document-lane initiatives out.
21
+ * Work lanes only (D86 point 4): `list` and `export` leave document-lane initiatives out. `list` names them in one closing
22
+ * line, because a record on a document lane is never on Home, the Board or the Roadmap: a feature record written by hand on
23
+ * the `prd` lane otherwise disappears with no word (batch 25 item 7). `new` refuses a document lane already (the writer).
22
24
  * D105 retired Now / Next / Later: `now`, `next` and `later` answer with what to use instead and change nothing.
23
25
  * Exit codes: 0 done, 1 refused, 2 usage.
24
26
  */
@@ -47,6 +49,16 @@ function startOnDefault(root, r, actor) {
47
49
  } catch { /* the initiative is made; it runs Product feature */ }
48
50
  }
49
51
 
52
+ /** The records on a document lane (D86 point 4): never on the Roadmap, Home or the Board. Stopped and accepted ones are left out. */
53
+ const documentsOf = (rows) => (rows || []).filter((r) => r && r.ok !== false && r.group && r.group !== 'work' && !r.accepted && !r.stopped)
54
+ .map((r) => ({ id: r.id, lane: r.lane || null }));
55
+ const WORK_LANES = 'new-feature, improvement, bug-fix, discovery or chore';
56
+ /** One line: what is not on the Roadmap, and what to do when it is work and not a document. */
57
+ function docNote(docs) {
58
+ const names = docs.map((d) => `${d.id} (${d.lane || 'document'})`).join(', ');
59
+ return `Not on the Roadmap · ${docs.length} on a document lane: ${names}. A document lane does not show on Home or the Board. If it is work, set lane: in its record to ${WORK_LANES}.`;
60
+ }
61
+
50
62
  const COMMANDS = { initiative: ['new', 'top', 'up', 'down', 'bottom', 'after', 'check-by', 'adopt', 'drop', 'list', 'export'] };
51
63
  /** The horizon verbs D105 retired: they answer with what to use instead. */
52
64
  const RETIRED = Object.freeze(['now', 'next', 'later']);
@@ -147,7 +159,7 @@ async function run(argv, { say = (s) => console.log(s), cwd = process.cwd(), isT
147
159
  const stage = (id) => lifecycle.labelOf(byRow.get(id));
148
160
  if (json) {
149
161
  say(JSON.stringify({ ok: true, ranked: o.placed.ranked, byInit: o.placed.byInit, stages: Object.fromEntries(o.placed.ranked.map((id) => [id, stage(id)])),
150
- proposals: o.placed.proposals.map((p) => ({ ...p, by: actorLib.pageName(p.by) })) }));
162
+ proposals: o.placed.proposals.map((p) => ({ ...p, by: actorLib.pageName(p.by) })), documents: documentsOf(o.rows) }));
151
163
  return 0;
152
164
  }
153
165
  const ids = o.placed.ranked;
@@ -163,6 +175,8 @@ async function run(argv, { say = (s) => console.log(s), cwd = process.cwd(), isT
163
175
  say(` ${p.rank + 1}. ${id} — ${(byRow.get(id) || {}).title || id}${bits.length ? ' · ' + bits.join(' · ') : ''}`);
164
176
  }
165
177
  for (const p of o.placed.proposals) say(`proposal ${p.eid}: ${p.init} ${p.after ? `after ${p.after}` : 'to the top'} · ${actorLib.pageName(p.by)}`);
178
+ const docs = documentsOf(o.rows);
179
+ if (docs.length) say(docNote(docs));
166
180
  return 0;
167
181
  }
168
182
 
@@ -2,8 +2,9 @@
2
2
  /* meeting-points.js — what the PM said about the key points of a meeting's Replay (initiative premium-uncovered-features, area meetings;
3
3
  * MtReplay.dc.html: the bulk bar's Confirm and Not right). The Replay keeps the points that were agreed, owned or left open; the PM ticks
4
4
  * some and confirms them, or marks them not right. A point marked not right leaves the kept list and goes to "Left out".
5
+ * A point the PM deleted (the trash on its edit card, batch 25 item 10) is gone: not kept, not Left out, not counted. Undo is verdict null.
5
6
  *
6
- * pointsOf(dir, note) → { [pointId]: 'confirmed' | 'wrong' } a later line wins; verdict null forgets the point
7
+ * pointsOf(dir, note) → { [pointId]: 'confirmed' | 'wrong' | 'deleted' } a later line wins; verdict null forgets the point
7
8
  * setPoints(dir, { note, ids, verdict }) → { ok, points } | { ok: false, status, error }
8
9
  *
9
10
  * A point's id is the id lib/meeting-replay.js gives it (d1, a2, q1). One line of <runtime>/meeting-points.jsonl per press, file mode,
@@ -14,7 +15,7 @@ const path = require('path');
14
15
  const FILE = 'meeting-points.jsonl';
15
16
  const NOTE_ID = /^[A-Za-z0-9_-]{1,80}$/;
16
17
  const POINT_ID = /^[A-Za-z0-9_-]{1,40}$/;
17
- const VERDICTS = ['confirmed', 'wrong'];
18
+ const VERDICTS = ['confirmed', 'wrong', 'deleted'];
18
19
 
19
20
  function readLines(file) {
20
21
  let text = '';
@@ -43,7 +44,7 @@ function setPoints(dir, body = {}) {
43
44
  const ids = Array.isArray(body.ids) ? [...new Set(body.ids.map(String))] : [];
44
45
  if (!ids.length || ids.length > 200 || !ids.every((x) => POINT_ID.test(x))) return { ok: false, status: 400, error: 'ids are the key points to mark, 1 to 200 of them' };
45
46
  const verdict = body.verdict == null ? null : String(body.verdict);
46
- if (verdict !== null && !VERDICTS.includes(verdict)) return { ok: false, status: 400, error: 'verdict is confirmed, wrong or null' };
47
+ if (verdict !== null && !VERDICTS.includes(verdict)) return { ok: false, status: 400, error: 'verdict is confirmed, wrong, deleted or null' };
47
48
  fs.mkdirSync(dir, { recursive: true });
48
49
  fs.appendFileSync(path.join(dir, FILE), JSON.stringify({ v: 1, note, ids, verdict, at: new Date().toISOString() }) + '\n');
49
50
  return { ok: true, points: pointsOf(dir, note) };
@@ -186,7 +186,7 @@ function reportSchedules(RN, range) {
186
186
  }
187
187
 
188
188
  /** The decisions and next steps of a note, each with confirmed: true when the PM confirmed or corrected it, else false. A corrected
189
- * point carries the PM's words. A point marked not right is left out. */
189
+ * point carries the PM's words. A point marked not right, or deleted, is left out. */
190
190
  function confirmedPoints(runtimeDir, noteId, decisions, actions) {
191
191
  let pts = {}, eds = {};
192
192
  try { pts = require('./meeting-points.js').pointsOf(runtimeDir, noteId); } catch { pts = {}; }
@@ -194,7 +194,7 @@ function confirmedPoints(runtimeDir, noteId, decisions, actions) {
194
194
  const out = { decisions: [], actions: [] };
195
195
  const put = (list, k) => (list || []).forEach((x, i) => {
196
196
  const id = k[0] + (i + 1), e = eds[id];
197
- if (pts[id] === 'wrong') return;
197
+ if (pts[id] === 'wrong' || pts[id] === 'deleted') return;
198
198
  const kind = e && ['decision', 'action'].includes(e.kind) ? e.kind : e ? null : k;
199
199
  if (!kind) return; // corrected into a question: not a decision or a step any more
200
200
  const row = { text: e ? e.text : x && x.text, confirmed: !!e || pts[id] === 'confirmed' };
@@ -2,8 +2,8 @@
2
2
  /* routes-meeting-points.js — the Replay's Confirm and Not right (initiative premium-uncovered-features, area meetings; lib/route-modules.js).
3
3
  * The data is kept by lib/meeting-points.js.
4
4
  *
5
- * GET /api/meeting-points?note=<id> → { ok, points: { [pointId]: 'confirmed' | 'wrong' } }
6
- * POST /api/meeting-points { note, ids: [pointId], verdict } → { ok, points } verdict confirmed | wrong | null (Undo)
5
+ * GET /api/meeting-points?note=<id> → { ok, points: { [pointId]: 'confirmed' | 'wrong' | 'deleted' } }
6
+ * POST /api/meeting-points { note, ids: [pointId], verdict } → { ok, points } verdict confirmed | wrong | deleted | null (Undo)
7
7
  * The GET also answers { edits, lessons }: the PM's corrections to the note's points and what PMOS learned (lib/meeting-learn.js).
8
8
  * POST /api/meeting-points/edit { note, id, kind, text, orig: { kind, text }, learn, items: [{ id, text }] } → { ok, edits, lesson, check }
9
9
  * a corrected point (XvAReplayEdit). learn false is "Only fix this one". check: the other points of the note that use the learned words.
@@ -100,7 +100,7 @@ async function handle(req, res, url, ctx) {
100
100
  const bound = r.ok && ctx.claudeSetup.bound(r.profile);
101
101
  // A new check of another login, or a failed check, drops everything bound to the old one.
102
102
  const keep = bound && st.profile === r.profile && st.fingerprint === bound.fingerprint;
103
- write(ctx, { ...(keep ? st : { project: st.project, folder: st.folder }), profile: r.profile || b.profile, fingerprint: bound ? bound.fingerprint : null, checked: { state: r.state, at: r.checked_at || null } });
103
+ write(ctx, { ...(keep ? st : { project: st.project, folder: st.folder }), profile: r.profile || b.profile, fingerprint: bound ? bound.fingerprint : null, checked: { state: r.state, at: r.checked_at || null, message: r.message || null } });
104
104
  return send({ ...view(ctx), ...r });
105
105
  }
106
106
  const bound = verified(ctx, st);
@@ -58,7 +58,7 @@ function runOf(run) {
58
58
  failed: T.filter((t) => t.status === 'failed').slice(0, 3).map((t) => ({ id: t.id, title: clip(t.title, 200), error: clip(t.error, 200) })) };
59
59
  }
60
60
 
61
- /** deps: { events, items, run, notes: [note with final_notes], adapter, points: (noteId) → { [pointId]: 'confirmed' | 'wrong' }, seen: Set of seen ids }
61
+ /** deps: { events, items, run, notes: [note with final_notes], adapter, points: (noteId) → { [pointId]: 'confirmed' | 'wrong' | 'deleted' }, seen: Set of seen ids }
62
62
  * notes are today's non-private notes, read in full. A decision's point id is the Replay's (lib/meeting-replay.js): d1, d2, … in caughtOf order. */
63
63
  function build(query, deps = {}) {
64
64
  const q = query || {};
@@ -76,7 +76,7 @@ function build(query, deps = {}) {
76
76
  caughtOf(n).decision.forEach((x, i) => {
77
77
  const v = pts['d' + (i + 1)];
78
78
  if (v === 'confirmed') decisions.push({ kind: 'decision', text: clip(x.text, 200), sub: clip(n.title || (n.meeting && n.meeting.title) || 'A meeting', 80) });
79
- else if (v !== 'wrong') toConfirm += 1;
79
+ else if (v !== 'wrong' && v !== 'deleted') toConfirm += 1;
80
80
  });
81
81
  }
82
82
  const done = items.filter((i) => i && i.done && i.done_at && localDate(new Date(i.done_at)) === today);
@@ -290,7 +290,7 @@ function feedHarness(acc, raw, clean) {
290
290
  // context use (batch 13 item 8): the NEWEST real turn's size, not the sum. A subagent's turn has its own context and is left out.
291
291
  if (m.model && m.model !== SYNTHETIC && !r.isSidechain) {
292
292
  const used = (u.input_tokens || 0) + (u.cache_read_input_tokens || 0) + (u.cache_creation_input_tokens || 0) + (u.output_tokens || 0);
293
- if (used > 0) { acc.ctx = { used, at: r.timestamp || null }; if (used > acc.peak) acc.peak = used; }
293
+ if (used > 0) { acc.ctx = { used, at: r.timestamp || null, model: clean(m.model) }; if (used > acc.peak) acc.peak = used; } // model: lib/context-use.js names the window from it
294
294
  }
295
295
  }
296
296
  for (const c of (m.content || [])) {
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.498.0",
3
+ "version": "0.500.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pmos-workspace",
9
- "version": "0.498.0",
9
+ "version": "0.500.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@anthropic-ai/sdk": "^1.3.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.498.0",
3
+ "version": "0.500.0",
4
4
  "private": true,
5
5
  "description": "PMOS Terminal Workspace — local companion hosting real PTYs beside the library and gates (D62)",
6
6
  "license": "MIT",
@@ -43,21 +43,26 @@ export function mountOnboard(host, { ws = '', done = false } = {}) {
43
43
  function turns() {
44
44
  const p = P(), c = (S.data && S.data.claude) || { profiles: [] }, out = [];
45
45
  // 1. connect and test
46
- const profiles = c.profiles || [];
47
- const pick = S.profile || p.profile || c.selected || (profiles[0] && profiles[0].id);
46
+ // One row per login that really differs: a pinned copy of the login on this computer shows as that login, and PMOS's own
47
+ // profile is the "separate login" row (bug 2026-11-03: "My usual Claude login" and "PMOS" repeated the other two rows).
48
+ const profiles = c.profiles || [], ownId = c.own || '__own__';
49
+ const row = (id) => (id === '__own__' || (id && id === c.own) ? ownId : (c.alias && c.alias[id]) || id);
50
+ const pick = row(S.profile || p.profile || c.selected || (profiles[0] && profiles[0].id));
48
51
  if (!p.verified) {
49
- const failed = p.checked && p.checked.state && p.checked.state !== 'ready' && p.profile === pick;
50
- const opts = profiles.map((x) => `<label class="ob-opt${x.id === pick ? ' on' : ''}"><input type="radio" name="ob-login" value="${esc(x.id)}"${x.id === pick ? ' checked' : ''}><span><b>${esc(x.id === '__default__' ? 'The Claude login on this computer' : x.label)}</b>${x.id === '__default__' ? '<em>Whatever account Claude Code uses in your usual terminal</em>' : ''}</span></label>`).join('');
52
+ const failed = p.checked && p.checked.state && p.checked.state !== 'ready' && row(p.profile) === pick;
53
+ const opt = (id, label, note) => `<label class="ob-opt${id === pick ? ' on' : ''}"><input type="radio" name="ob-login" value="${esc(id)}"${id === pick ? ' checked' : ''}><span><b>${esc(label)}</b>${note ? `<em>${esc(note)}</em>` : ''}</span></label>`;
54
+ const opts = profiles.map((x) => opt(x.id, x.id === '__default__' ? 'The Claude login on this computer' : x.label, x.id === '__default__' ? 'Whatever account Claude Code uses in your usual terminal' : '')).join('')
55
+ + opt(ownId, 'A separate login just for PMOS', 'Keeps PMOS on its own account. You sign in once.');
51
56
  out.push({ mood: failed ? 'honest' : 'needs-you', body: `<p>${failed ? '' : 'Let’s set up a product or project. First, which Claude login should PMOS use? I’ll run one short check on exactly that login.'}</p>`
52
- + (failed ? `<div class="ob-bad" role="alert"><b>${esc(S.data.claude.status && S.data.claude.status.profile === p.profile ? S.data.claude.status.message : (p.checked.state === 'signed_out' ? 'This login needs signing in again.' : 'This login did not pass the check.'))}</b><span>I won’t switch to a different account on my own. Sign this one in, or pick another login.</span></div>` : '')
53
- + `<div class="ut-card ob-card" role="radiogroup" aria-label="Claude login">${opts}<label class="ob-opt${pick === '__own__' ? ' on' : ''}"><input type="radio" name="ob-login" value="__own__"${pick === '__own__' ? ' checked' : ''}><span><b>A separate login just for PMOS</b><em>Keeps PMOS on its own account. You sign in once.</em></span></label>`
57
+ + (failed ? `<div class="ob-bad" role="alert"><b>${esc(p.checked.message || (p.checked.state === 'signed_out' ? 'This login needs signing in again.' : 'This login did not pass the check.'))}</b><span>I won’t switch to a different account on my own. Sign this one in, or pick another login.</span></div>` : '')
58
+ + `<div class="ut-card ob-card" role="radiogroup" aria-label="Claude login">${opts}`
54
59
  + `<div class="ob-row">${btn('check', 'Test this login', 'pri')}${btn('login', 'Sign in to it', '')}</div>`
55
60
  + `<p class="ob-note">The test sends one short message to Claude with no tools. It uses a little of this account’s usage.</p>`
56
61
  + (S.session ? '<p class="ob-note">Finish signing in in the browser window Claude opened. If it asks for a code, paste it below. I test the login as soon as you are done.</p><div class="cs-term ob-term" aria-label="Claude sign-in terminal"></div>' : '')
57
62
  + `</div>` });
58
63
  return out;
59
64
  }
60
- const label = (profiles.find((x) => x.id === p.profile) || {}).label || p.profile;
65
+ const label = row(p.profile) === ownId ? 'A separate login just for PMOS' : (profiles.find((x) => x.id === row(p.profile)) || {}).label || p.profile;
61
66
  out.push({ mood: 'handled', body: `<div class="ob-good">${CHECK}<b>Login works · ${esc(label)}</b><span>Tested ${p.checked && p.checked.at ? new Date(p.checked.at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : 'just now'} · one test message, no tools</span></div>`
62
67
  + (p.project ? '' : `<p>What product or project is this for? Its name is enough to start.</p><p class="ob-sub">If you have them, add the team, a Jira key, a link or the folder where it lives. They help me find the right sources, not a same-name one.</p><div class="ob-row"><button type="button" class="hm-chip loose" data-ob="relogin">Use another login</button></div>`) });
63
68
  if (!p.project) return out;
@@ -151,7 +156,9 @@ export function mountOnboard(host, { ws = '', done = false } = {}) {
151
156
  S.busy = a; S.note = ''; draw();
152
157
  try {
153
158
  if (a === 'check' || a === 'login') {
154
- let profile = S.profile || p.profile || (S.data.claude && S.data.claude.selected);
159
+ const cl = S.data.claude || {};
160
+ let profile = S.profile || p.profile || cl.selected;
161
+ if (cl.alias && cl.alias[profile]) profile = cl.alias[profile]; // a pinned copy of the usual login is tested as that login
155
162
  if (profile === '__own__') { const own = await post('own'); if (!own.ok) throw new Error(own.error || 'The PMOS login could not be made.'); profile = own.id; S.profile = profile; await load(); }
156
163
  const r = await post(a, { profile });
157
164
  if (a === 'login') {
@@ -225,6 +225,7 @@
225
225
  .vzp-learn b:empty { display: none; }
226
226
  .vzp-ef { display: flex; align-items: center; gap: 10px; }
227
227
  .vzp-sp { flex: 1; }
228
+ .vzp-del:hover { color: var(--cz-ink); }
228
229
  .vzp-gbtn { min-height: 34px; padding: 0 12px !important; border-radius: 999px; font-size: var(--fs-3); font-weight: 700; }
229
230
 
230
231
  /* one Replay at a time: the phone's below 640px, else the desk's */
@@ -63,7 +63,8 @@ export function replayView(rep, { n = null, points = {}, edits = {}, lessons = [
63
63
  const raw = Array.isArray(rep.raw) ? rep.raw : [];
64
64
  const rawOf = (k) => raw.find((x) => x && (x.id === k.id || (x.k === k.k && x.text === k.text))) || k;
65
65
  const lineAt = (t) => { if (t == null) return null; let best = null; for (const l of rep.lines) if (best === null || Math.abs(l.t - t) < Math.abs(best.t - t)) best = l; return best && Math.abs(best.t - t) <= NEAR ? best : null; };
66
- const all = rep.keys.map((k) => {
66
+ /* a point the PM deleted is gone: not kept, not Left out, not counted (its line still keeps the remark it was caught from out of Left out) */
67
+ const all = rep.keys.filter((k) => points[k.id] !== 'deleted').map((k) => {
67
68
  const r = rawOf(k), ed = edits[k.id] || null, state = ed ? '' : r.state || k.state || '', owner = clip(r.owner || k.owner, 40), line = lineAt(k.t);
68
69
  const quote = line ? clip(line.who, 30) + ': “' + clip(line.text, 90) + '”' : '';
69
70
  const kind0 = k.k === 'action' ? 'action' : k.k === 'question' ? 'question' : 'decision';
@@ -109,7 +110,7 @@ export function replayView(rep, { n = null, points = {}, edits = {}, lessons = [
109
110
  }
110
111
 
111
112
  /* ── html (pure) ── */
112
- const P = { play: 'M8 5v14l11-7z', pause: 'M8 5h3v14H8zM13 5h3v14h-3z', big: 'M14 4h6v6M10 20H4v-6M20 4l-7 7M4 20l7-7', chev: 'M10 7l5 5-5 5', chk: 'M5 12l5 5 9-10', task: 'M5 12l4 4L19 6', x: 'M6 6l12 12M18 6L6 18' };
113
+ const P = { play: 'M8 5v14l11-7z', pause: 'M8 5h3v14H8zM13 5h3v14h-3z', big: 'M14 4h6v6M10 20H4v-6M20 4l-7 7M4 20l7-7', chev: 'M10 7l5 5-5 5', chk: 'M5 12l5 5 9-10', task: 'M5 12l4 4L19 6', x: 'M6 6l12 12M18 6L6 18', bin: 'M5 7h14M10 7V4h4v3M7 7l1 13h8l1-13' };
113
114
  const ic = (k, w = 16, cls = '') => `<svg aria-hidden="true" width="${w}" height="${w}" viewBox="0 0 24 24"${cls ? ` class="${cls}"` : ''}><path d="${P[k]}"/></svg>`;
114
115
  const pct = (x) => Math.min(100, Math.max(0, x)).toFixed(2);
115
116
 
@@ -139,7 +140,8 @@ function pointRow(p, ui) {
139
140
  + (p.check ? `<small class="vzp-chk2">Check: you said “${esc(p.check.phrase)}” means ${esc(p.check.meaning)}</small>` : '') + '</span></button>';
140
141
  }
141
142
  const stop = (t) => (/[.!?…”]$/.test(t) ? t : t + '.');
142
- /** The correction (XvAReplayEdit): the kind, the old words struck, the new words, what PMOS will remember, Cancel and Save.
143
+ /** The correction (XvAReplayEdit): the kind, the old words struck, the new words, what PMOS will remember, the trash (Delete: no confirm,
144
+ * Undo in the message, like a task's Delete), Cancel and Save.
143
145
  * d = { kind, text, learn }; also = how many other points of this note use the learned words. */
144
146
  export function editHtml(p, d = {}, also = 0) {
145
147
  const kind = d.kind || p.kind, text = d.text == null ? p.text : d.text, learn = d.learn !== false;
@@ -152,7 +154,7 @@ export function editHtml(p, d = {}, also = 0) {
152
154
  + (changed ? `<p class="vzp-was"><s>${esc(KIND_OF[p.orig.kind] + ' · ' + p.orig.text)}</s></p>` : '')
153
155
  + `<textarea class="vzp-eta" data-a="etext" rows="3" aria-label="What was really said">${esc(text)}</textarea>`
154
156
  + `<div class="vzp-learn"><b>${learn && changed ? 'I will remember this.' : ''}</b> <span data-pl="lesson">${esc(lessonText)}</span>${changed ? ` <button type="button" class="vzp-link" data-a="elearn" aria-pressed="${!learn}">${learn ? 'Only fix this one' : 'Remember it'}</button>` : ''}</div>`
155
- + `<div class="vzp-ef"><span class="vzp-mut vzp-sm">${esc([p.time, p.by].filter(Boolean).join(' · '))}</span><span class="vzp-sp"></span><button type="button" class="vzp-gbtn" data-a="ecancel">Cancel</button><button type="button" class="vzp-dark" data-a="esave"${changed && text.trim() ? '' : ' disabled'}>Save</button></div></div>`;
157
+ + `<div class="vzp-ef"><span class="vzp-mut vzp-sm">${esc([p.time, p.by].filter(Boolean).join(' · '))}</span><span class="vzp-sp"></span><button type="button" class="vzp-ib vzp-del" data-a="edel" data-id="${esc(p.id)}" aria-label="Delete" title="Delete">${ic('bin', 16)}</button><button type="button" class="vzp-gbtn" data-a="ecancel">Cancel</button><button type="button" class="vzp-dark" data-a="esave"${changed && text.trim() ? '' : ' disabled'}>Save</button></div></div>`;
156
158
  }
157
159
  /** "In this part · <topic>": the kept points said in the stretch the replay is in. */
158
160
  export function partHtml(V, ui = {}) {
@@ -700,7 +700,7 @@ function keydown(me, e, body) {
700
700
  }
701
701
  const bodyEl = () => (MT && vCur() && vCur().id === MT.vid ? document.querySelector('#czViewer .cz-vw-body') : null); // null while a preview covers the meeting
702
702
  const repaint = (me) => { const b = bodyEl(); if (b && MT === me) paint(me, b); };
703
- function say(me, text, tone, undo) { me.ui.msg = { text, tone, undo }; repaint(me); clearTimeout(me.msgT); me.msgT = setTimeout(() => { if (me.ui.msg && me.ui.msg.text === text) { me.ui.msg = null; repaint(me); } }, 9000); }
703
+ function say(me, text, tone, undo, ms = 9000) { me.ui.msg = { text, tone, undo }; repaint(me); clearTimeout(me.msgT); me.msgT = setTimeout(() => { if (me.ui.msg && me.ui.msg.text === text) { me.ui.msg = null; repaint(me); } }, ms); }
704
704
  function tick(me) {
705
705
  if (MT !== me || !me.model) return;
706
706
  const b = bodyEl(); if (!b) return;
@@ -783,6 +783,7 @@ async function press(me, e) {
783
783
  case 'elearn': ui.draft = { ...ui.draft, learn: ui.draft.learn === false, text: draftText() }; return repaint(me);
784
784
  case 'ecancel': ui.edit = null; ui.draft = null; return repaint(me);
785
785
  case 'esave': return saveEdit(me);
786
+ case 'edel': return delPoint(me, t.dataset.id);
786
787
  /* the summary's quiet line and the lines under it */
787
788
  case 'm-about': if (ui.pop === 'about') { ui.pop = ''; return repaint(me); } return openAbout(me);
788
789
  case 'aboutpick': return saveAbout(me, t.dataset.v);
@@ -941,6 +942,15 @@ async function bulk(me, k, ids) {
941
942
  }
942
943
  return verdict(me, ids, k, k === 'confirmed' ? plural(ids.length, 'point') + ' confirmed.' : plural(ids.length, 'point') + ' moved to Left out.', k === 'wrong');
943
944
  }
945
+ /** Delete on the edit card: the point is gone, here and after a reload (verdict deleted). No confirm: Undo stays 8 s, as a task's Delete. */
946
+ async function delPoint(me, id) {
947
+ if (!me.n || !id) return;
948
+ const r = await pmPost('/api/meeting-points', { note: me.n.id, ids: [id], verdict: 'deleted' });
949
+ if (!r || !r.ok) return say(me, 'Not deleted: ' + ((r && r.error) || 'no answer'), 'bad');
950
+ me.ui.edit = null; me.ui.draft = null; if (me.ui.sel) me.ui.sel.delete(id);
951
+ me.D.points = r.points || {}; draw(me);
952
+ say(me, 'Point deleted.', '', async () => verdict(me, [id], null, 'Put back.'), 8000);
953
+ }
944
954
  async function verdict(me, ids, v, text, undo) {
945
955
  const r = await pmPost('/api/meeting-points', { note: me.n.id, ids, verdict: v });
946
956
  if (!r || !r.ok) return say(me, 'Not saved: ' + ((r && r.error) || 'no answer'), 'bad');