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 +39 -26
- package/payload/KIT-VERSION +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/changelog.json +38 -0
- package/workspace/guide/claude-proposes.md +1 -1
- package/workspace/guide/home.md +1 -1
- package/workspace/guide/notes.md +7 -1
- package/workspace/lib/claude-readiness.js +29 -6
- package/workspace/lib/context-use.js +57 -15
- package/workspace/lib/initiative-cli.js +16 -2
- package/workspace/lib/meeting-points.js +4 -3
- package/workspace/lib/routes-calendar-views.js +2 -2
- package/workspace/lib/routes-meeting-points.js +2 -2
- package/workspace/lib/routes-onboard.js +1 -1
- package/workspace/lib/routes-phone-day.js +2 -2
- package/workspace/lib/transcript-read.js +1 -1
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/cozy-onboard.js +15 -8
- package/workspace/public/pm/cozy-vw-mtg.css +1 -0
- package/workspace/public/pm/premium-mtg-replay.js +6 -4
- package/workspace/public/pm/premium-vw-mtg.js +11 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "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
|
}
|
package/payload/KIT-VERSION
CHANGED
|
@@ -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-
|
|
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-
|
|
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-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-11-03T04:17:00Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
package/workspace/changelog.json
CHANGED
|
@@ -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
|
|
package/workspace/guide/home.md
CHANGED
|
@@ -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
|
|
package/workspace/guide/notes.md
CHANGED
|
@@ -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-
|
|
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
|
-
|
|
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: '
|
|
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
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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
|
|
16
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
|
44
|
-
let model = null
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
9
|
+
"version": "0.500.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@anthropic-ai/sdk": "^1.3.0",
|
package/workspace/package.json
CHANGED
|
@@ -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
|
-
|
|
47
|
-
|
|
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
|
|
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(
|
|
53
|
-
+ `<div class="ut-card ob-card" role="radiogroup" aria-label="Claude login">${opts}
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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); } },
|
|
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');
|