create-pmos 0.498.0 → 0.499.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 +26 -13
- 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 +25 -0
- package/workspace/guide/claude-proposes.md +1 -1
- package/workspace/guide/home.md +1 -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/routes-onboard.js +1 -1
- 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.499.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,31 @@
|
|
|
32
32
|
"audit"
|
|
33
33
|
],
|
|
34
34
|
"pmosChanges": [
|
|
35
|
+
{
|
|
36
|
+
"v": "0.499.0",
|
|
37
|
+
"at": "2026-11-03T03:25:28.533Z",
|
|
38
|
+
"f": 3,
|
|
39
|
+
"prs": [
|
|
40
|
+
{
|
|
41
|
+
"n": 1180,
|
|
42
|
+
"t": "The context meter knows the default model is 1M",
|
|
43
|
+
"s": "The context meter knows the default model is 1M",
|
|
44
|
+
"k": "fix"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"n": 1181,
|
|
48
|
+
"t": "`pmos initiative list` names records on a document lane",
|
|
49
|
+
"s": "Pmos initiative list names records on a document lane",
|
|
50
|
+
"k": "fix"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"n": 1182,
|
|
54
|
+
"t": "Claude login test passes for a signed-in new user; picker shows each login once",
|
|
55
|
+
"s": "Claude login test passes for a signed-in new user; picker shows each login once",
|
|
56
|
+
"k": "fix"
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
},
|
|
35
60
|
{
|
|
36
61
|
"v": "0.498.0",
|
|
37
62
|
"at": "2026-11-03T02:50:09.805Z",
|
|
@@ -397,18 +422,6 @@
|
|
|
397
422
|
"k": "fix"
|
|
398
423
|
}
|
|
399
424
|
]
|
|
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-03T03:25:24Z"}
|
|
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-03T03:25:24Z"}
|
|
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-03T03:25:24Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
package/workspace/changelog.json
CHANGED
|
@@ -1,4 +1,29 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"v": "0.499.0",
|
|
4
|
+
"at": "2026-11-03T03:25:28.533Z",
|
|
5
|
+
"f": 3,
|
|
6
|
+
"prs": [
|
|
7
|
+
{
|
|
8
|
+
"n": 1180,
|
|
9
|
+
"t": "The context meter knows the default model is 1M",
|
|
10
|
+
"s": "The context meter knows the default model is 1M",
|
|
11
|
+
"k": "fix"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"n": 1181,
|
|
15
|
+
"t": "`pmos initiative list` names records on a document lane",
|
|
16
|
+
"s": "Pmos initiative list names records on a document lane",
|
|
17
|
+
"k": "fix"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"n": 1182,
|
|
21
|
+
"t": "Claude login test passes for a signed-in new user; picker shows each login once",
|
|
22
|
+
"s": "Claude login test passes for a signed-in new user; picker shows each login once",
|
|
23
|
+
"k": "fix"
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
},
|
|
2
27
|
{
|
|
3
28
|
"v": "0.498.0",
|
|
4
29
|
"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
|
|
|
@@ -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
|
|
|
@@ -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);
|
|
@@ -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.499.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.499.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') {
|