create-pmos 0.104.0 → 0.106.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 +1 -1
- 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/guide/layout.md +1 -1
- package/workspace/lib/profile-discovery.js +28 -2
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/dest-settings.js +53 -18
- package/workspace/public/pm/frame-copy.js +14 -1
- package/workspace/public/pm/modes.js +15 -3
- package/workspace/public/pm/view-notes-menu.js +168 -43
- package/workspace/server.js +5 -1
- package/workspace/test-lists/workspace-profile-detection.json +6 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.106.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": {
|
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-10-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-08T01:32:06Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Health budgets"
|
|
4
4
|
description: "Closed registry of health-budget anchors (D59), edited only by the PM."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-08T01:32:06Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Discoveries and friction log"
|
|
4
4
|
description: "Append-only record of what runs discovered and where the process rubbed."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-08T01:32:06Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -30,7 +30,7 @@ Each place is drawn when its run is built. Until then the place says which run b
|
|
|
30
30
|
Settings holds settings only, in eight sections. On a wide screen they are cards in two columns, like Agenda: Repositories, New sessions, Profiles and Theme on the left, and Validate consent, People, Online and Updates on the right. On a narrow screen the cards stack in one column. Each switch (Validate consent, People, Online) turns on when you press it. **Check for updates** is at the top right of the page.
|
|
31
31
|
|
|
32
32
|
- **Repositories**: every registered repository. Press a repository to make it the current one. Press its ⋯ for **Rename** or **Remove from PMOS**. Each asks in a sheet, and a remove asks you to confirm. **Add repository** shows the scan of a folder before you add it.
|
|
33
|
-
- **Profiles**: the Claude profiles a session can start with. These are the descriptors in `~/.claude/profiles`, and also the profiles you already run: a shell command that sets `CLAUDE_CONFIG_DIR` (for example `alias claude-work='CLAUDE_CONFIG_DIR=~/.claude-work claude'`), a launcher script named `claude-*`, or a config folder such as `~/.claude-work`. A profile found this way says where it was found, and a session under it sets only `CLAUDE_CONFIG_DIR`. Press **New profile** to make one. This writes `~/.claude/profiles/<id>.json`. Account secrets are never shown in the browser.
|
|
33
|
+
- **Profiles**: the Claude profiles a session can start with. These are the descriptors in `~/.claude/profiles`, and also the profiles you already run: a shell command that sets `CLAUDE_CONFIG_DIR` (for example `alias claude-work='CLAUDE_CONFIG_DIR=~/.claude-work claude'`), a launcher script named `claude-*`, or a config folder such as `~/.claude-work`. Each profile is a row with a **Detected** badge when PMOS found it rather than wrote it, and a **Default** badge when it is the default. Its ⋯ menu has **Use as default**. A profile found this way says where it was found, and a session under it sets only `CLAUDE_CONFIG_DIR`. The list reads again every few seconds while Settings is open, and **Look again** in the Claude profiles menu reads it at once. When nothing is found, the note names the start-up files and folders PMOS looked at. Press **New profile** to make one. This writes `~/.claude/profiles/<id>.json`. Account secrets are never shown in the browser.
|
|
34
34
|
- **New sessions**: the default CLI, the default model, the default Claude profile and the default launch flags for a new session. The flags come from `claude --help`, for example `--chrome`. A flag that needs a value lists the values it accepts.
|
|
35
35
|
- **Validate consent**: the consent of the current repository. Use its switch to turn it on or off. **Open Validate** opens the Validate page as a tab.
|
|
36
36
|
- **People**: whether the morning brief keeps attendee names from the calendar for the current repository. It is off until you turn it on. Turning it off also hides the names recorded before.
|
|
@@ -31,7 +31,8 @@ const path = require('path');
|
|
|
31
31
|
const ID_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/;
|
|
32
32
|
/** Something only Claude Code writes into its config directory. A folder with none of these is not offered. */
|
|
33
33
|
const CONFIG_MARKERS = ['settings.json', '.claude.json', '.credentials.json', 'projects', 'CLAUDE.md', 'commands', 'agents', 'todos', 'statsig', 'shell-snapshots'];
|
|
34
|
-
const RC_FILES = ['.zshrc', '.zprofile', '.zshenv', '.zlogin', '.bashrc', '.bash_profile', '.bash_login', '.profile', '.aliases', '.bash_aliases', '.
|
|
34
|
+
const RC_FILES = ['.zshrc', '.zprofile', '.zshenv', '.zlogin', '.bashrc', '.bash_profile', '.bash_login', '.profile', '.aliases', '.bash_aliases', '.bash_functions', '.zshrc.local', '.zsh/aliases.zsh', '.zsh_aliases',
|
|
35
|
+
'.config/zsh/.zshrc', '.config/zsh/aliases.zsh', '.config/bash/aliases', '.config/shell/aliases', '.oh-my-zsh/custom/aliases.zsh', '.config/fish/config.fish'];
|
|
35
36
|
const FISH_FUNCTIONS = '.config/fish/functions';
|
|
36
37
|
const BIN_DIRS = ['.local/bin', 'bin', '.bin', '.config/claude/bin'];
|
|
37
38
|
const MAX_TEXT = 256 * 1024; // a start-up file larger than this is not read
|
|
@@ -191,4 +192,29 @@ function detectProfiles(homeDir) {
|
|
|
191
192
|
}));
|
|
192
193
|
}
|
|
193
194
|
|
|
194
|
-
|
|
195
|
+
/**
|
|
196
|
+
* searched(homeDir) → what detectProfiles looked at, for the surface to say when it found nothing: the start-up files that
|
|
197
|
+
* exist (and so were read), the launcher scripts named claude-*, and the `~/.claude-*` folders with whether each holds a file
|
|
198
|
+
* only Claude writes. Names only, under the home folder — no contents, no paths outside it.
|
|
199
|
+
*/
|
|
200
|
+
function searched(homeDir) {
|
|
201
|
+
const out = { files: [], launchers: [], folders: [] };
|
|
202
|
+
if (typeof homeDir !== 'string' || !homeDir) return out;
|
|
203
|
+
for (const rel of RC_FILES) if (safeRead(path.join(homeDir, rel), MAX_TEXT) != null) out.files.push('~/' + rel);
|
|
204
|
+
for (const rel of BIN_DIRS) {
|
|
205
|
+
let names = [];
|
|
206
|
+
try { names = fs.readdirSync(path.join(homeDir, rel)); } catch { continue; }
|
|
207
|
+
for (const n of names.slice().sort()) if (/^claude[-_.]/.test(n)) out.launchers.push('~/' + rel + '/' + n);
|
|
208
|
+
}
|
|
209
|
+
for (const [parent, re] of [[homeDir, /^\.claude[-_.][A-Za-z0-9]/], [path.join(homeDir, '.config'), /^claude[-_][A-Za-z0-9]/]]) {
|
|
210
|
+
let names = [];
|
|
211
|
+
try { names = fs.readdirSync(parent); } catch { continue; }
|
|
212
|
+
for (const n of names.slice().sort()) {
|
|
213
|
+
const d = path.join(parent, n);
|
|
214
|
+
if (re.test(n) && isDir(d)) out.folders.push({ name: '~/' + path.relative(homeDir, d), claude: looksLikeConfigDir(d) });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return out;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
module.exports = { detectProfiles, searched, resolveDir, configDirOf, fromShellText, ID_RE };
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.106.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.106.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/workspace/package.json
CHANGED
|
@@ -173,30 +173,58 @@ function addBoard() {
|
|
|
173
173
|
}
|
|
174
174
|
|
|
175
175
|
function profilesSection() {
|
|
176
|
-
const open = () => setState({ detail: { kind: 'prof' }, col2: true });
|
|
176
|
+
const open = (id) => setState({ detail: { kind: 'prof', id: id || null }, col2: true });
|
|
177
|
+
loadSessionReads();
|
|
178
|
+
const found = sess.profiles && Array.isArray(sess.profiles.profiles) ? sess.profiles.profiles : [];
|
|
179
|
+
const def = sessionDefaults().profile;
|
|
177
180
|
// every row has its ⋯, here as on the repositories: on a phone it is the one control a row is known by
|
|
178
181
|
const more = iconButton({
|
|
179
182
|
icon: 'ellipsis', ariaLabel: P.more(P.profilesRow), attrs: { 'data-fr': 'profiles-more' },
|
|
180
|
-
onClick: () => menu({ anchor: more, ariaLabel: P.more(P.profilesRow), items: [
|
|
183
|
+
onClick: () => menu({ anchor: more, ariaLabel: P.more(P.profilesRow), items: [
|
|
184
|
+
{ label: P.profilesOpen, onSelect: () => open(found[0] && found[0].id) },
|
|
185
|
+
{ label: P.profilesLookAgain, onSelect: () => readProfilesAgain() },
|
|
186
|
+
] }),
|
|
181
187
|
});
|
|
182
|
-
loadSessionReads();
|
|
183
|
-
const found = sess.profiles && Array.isArray(sess.profiles.profiles) ? sess.profiles.profiles : [];
|
|
184
188
|
const kids = [row({
|
|
185
|
-
title: P.profilesRow, meta: [P.profilesMeta], onOpen: open, openLabel: P.profilesOpen, trail: [more],
|
|
189
|
+
title: P.profilesRow, meta: [P.profilesMeta], onOpen: () => open(found[0] && found[0].id), openLabel: P.profilesOpen, trail: [more],
|
|
186
190
|
attrs: { 'data-fr-row': 'profiles' },
|
|
187
191
|
})];
|
|
192
|
+
// one row per profile the reader returned, descriptors and detected alike: the list the default select and the New session form offer
|
|
193
|
+
for (const x of found) {
|
|
194
|
+
const isDef = def === x.id;
|
|
195
|
+
const rm = iconButton({
|
|
196
|
+
icon: 'ellipsis', ariaLabel: P.more(x.id), attrs: { 'data-fr': 'profile-more' },
|
|
197
|
+
onClick: () => menu({ anchor: rm, ariaLabel: P.more(x.id), items: [
|
|
198
|
+
{ label: isDef ? P.profileNoDefault : P.profileUseDefault, onSelect: () => chooseDefaultProfile(isDef ? null : x.id) },
|
|
199
|
+
{ label: P.profilesOpen, onSelect: () => open(x.id) },
|
|
200
|
+
] }),
|
|
201
|
+
});
|
|
202
|
+
kids.push(row({
|
|
203
|
+
title: x.id, meta: [P.profileMeta(x)],
|
|
204
|
+
signals: [isDef ? badge({ text: P.profileDefaultBadge, tone: 'outline' }) : null, x.detected ? badge({ text: P.profileDetectedBadge, tone: 'outline' }) : null].filter(Boolean),
|
|
205
|
+
onOpen: () => open(x.id), openLabel: P.profilesOpen + ': ' + x.id, trail: [rm],
|
|
206
|
+
attrs: { 'data-fr-row': 'profile', 'data-profile': x.id, 'data-default': isDef ? 'true' : 'false', 'data-detected': x.detected ? 'true' : 'false' },
|
|
207
|
+
}));
|
|
208
|
+
}
|
|
188
209
|
if (sess.formOpen) kids.push(profileForm());
|
|
189
210
|
if (sess.msg) kids.push(el('p', { class: 'fr-set__note', role: 'status', 'data-fr': 'profiles-msg' }, sess.msg));
|
|
190
211
|
kids.push(el('p', { class: 'fr-set__note', 'data-fr': 'profiles-found' }, !sess.profiles ? P.profilesReading
|
|
191
212
|
: sess.profiles.ok === false ? P.profilesUnread(sess.profiles.error)
|
|
192
|
-
: found.length ? P.profilesFound(found.map((x) => x.id), found.filter((x) => x.detected).map((x) => x.id)) : P.profilesNone(sess.profiles.dir)));
|
|
213
|
+
: found.length ? P.profilesFound(found.map((x) => x.id), found.filter((x) => x.detected).map((x) => x.id)) : P.profilesNone(sess.profiles.dir, sess.profiles.searched)));
|
|
193
214
|
const add = button({ label: P.profileNew, variant: 'outline', size: 'sm', icon: 'plus', attrs: { 'data-fr': 'profile-new', 'aria-expanded': sess.formOpen ? 'true' : 'false' },
|
|
194
215
|
onClick: () => { sess.formOpen = !sess.formOpen; sess.msg = ''; setState({}); } });
|
|
195
216
|
return section('profiles', P.profiles, kids, add, 'bot');
|
|
196
217
|
}
|
|
197
218
|
|
|
219
|
+
/** The default Claude profile, from a row or the select: kept for the dock, and remembered by the server for the New session form. */
|
|
220
|
+
function chooseDefaultProfile(id) {
|
|
221
|
+
setSessionDefaults({ profile: id || null });
|
|
222
|
+
pmPost('/api/profiles/prefer', { agent: 'claude', profile: id || null });
|
|
223
|
+
setState({});
|
|
224
|
+
}
|
|
225
|
+
|
|
198
226
|
/* ── the Claude profile and launch flags a new session starts with (workspace-launch-flags) ───────────────────────── */
|
|
199
|
-
const sess = { profiles: null, flags: null, asked: false, flagsOpen: false, filter: '', formOpen: false, msg: '' };
|
|
227
|
+
const sess = { readAt: 0, profiles: null, flags: null, asked: false, flagsOpen: false, filter: '', formOpen: false, msg: '' };
|
|
200
228
|
|
|
201
229
|
/** Both reads once per page load; a created profile asks for the profile read again. */
|
|
202
230
|
function loadSessionReads() {
|
|
@@ -205,10 +233,21 @@ function loadSessionReads() {
|
|
|
205
233
|
sess.asked = true;
|
|
206
234
|
pmGet('/api/claude-flags').then((j) => { sess.flags = j || { ok: false, error: 'no answer', flags: [] }; setState({}); });
|
|
207
235
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
236
|
+
// read again when it is a while old, so a profile added since (a folder, an alias) shows without a reload
|
|
237
|
+
if (!sess.reading && (!sess.profiles || Date.now() - sess.readAt > 15000)) readProfilesAgain();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Ask for the profiles again. The list on screen stays until the answer comes. */
|
|
241
|
+
function readProfilesAgain() {
|
|
242
|
+
if (FIXTURE || sess.reading) return;
|
|
243
|
+
sess.reading = true;
|
|
244
|
+
pmGet('/api/profiles').then((j) => {
|
|
245
|
+
sess.reading = false; sess.readAt = Date.now();
|
|
246
|
+
sess.profiles = j || { ok: false, error: 'no answer', profiles: [] };
|
|
247
|
+
// the detail column and the New session form read the same answer
|
|
248
|
+
state.profiles = sess.profiles;
|
|
249
|
+
setState({});
|
|
250
|
+
});
|
|
212
251
|
}
|
|
213
252
|
|
|
214
253
|
let profFormNode = null;
|
|
@@ -239,7 +278,7 @@ function profileForm() {
|
|
|
239
278
|
mcp: mcpF.control.value.split(/[\s,]+/).filter(Boolean), env };
|
|
240
279
|
const j = await pmPost('/api/profiles/create', body);
|
|
241
280
|
if (!j || j.ok === false) { err.textContent = (j && j.error) || P.pfFailed; return; }
|
|
242
|
-
reset(); sess.profiles = null; sess.msg = P.pfCreated(j.profile.id, j.profile.env_count);
|
|
281
|
+
reset(); sess.profiles = null; sess.readAt = 0; sess.msg = P.pfCreated(j.profile.id, j.profile.env_count);
|
|
243
282
|
setState({});
|
|
244
283
|
};
|
|
245
284
|
profFormNode = el('form', { class: 'fr-set__form', 'data-fr': 'profile-form', onSubmit: (e) => { e.preventDefault(); submit(); } },
|
|
@@ -277,12 +316,8 @@ function sessionsSection() {
|
|
|
277
316
|
label: P.profileDefault, kind: 'select', value: d.profile || '',
|
|
278
317
|
options: [{ value: '', label: P.noProfile }, ...(stale ? [{ value: d.profile, label: P.profileMissing(d.profile) }] : []),
|
|
279
318
|
...live.map((x) => ({ value: x.id, label: (x.label && x.label !== x.id ? x.label + ' (' + x.id + ')' : x.id) }))],
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
// the form's own per-agent memory follows, so both starts agree; a refusal is shown by the form itself
|
|
283
|
-
pmPost('/api/profiles/prefer', { agent: 'claude', profile: v || null });
|
|
284
|
-
setState({});
|
|
285
|
-
},
|
|
319
|
+
// the form's own per-agent memory follows, so both starts agree; a refusal is shown by the form itself
|
|
320
|
+
onCommit: (v) => chooseDefaultProfile(v),
|
|
286
321
|
attrs: { 'data-fr': 'default-profile' },
|
|
287
322
|
});
|
|
288
323
|
const flags = d.flags;
|
|
@@ -5,6 +5,14 @@
|
|
|
5
5
|
* rules. The words follow the PMOS-STE instructional register: short sentences, one instruction each, the active voice.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
/** One sentence naming where detection looked, built from /api/profiles `searched` (names only). */
|
|
9
|
+
function P_searched(sr) {
|
|
10
|
+
const files = sr.files && sr.files.length ? 'read ' + sr.files.join(', ') : 'found no shell start-up file';
|
|
11
|
+
const dirs = sr.folders && sr.folders.length ? '; folders ' + sr.folders.map((f) => f.name + (f.claude ? '' : ' (no Claude files in it)')).join(', ') : '; no ~/.claude-<name> folder';
|
|
12
|
+
const lau = sr.launchers && sr.launchers.length ? '; launchers ' + sr.launchers.join(', ') : '';
|
|
13
|
+
return 'Looked: ' + files + dirs + lau + '. Press Look again after you add one.';
|
|
14
|
+
}
|
|
15
|
+
|
|
8
16
|
export const COPY = {
|
|
9
17
|
repo: {
|
|
10
18
|
all: 'All repositories',
|
|
@@ -177,7 +185,12 @@ export const COPY = {
|
|
|
177
185
|
profilesReading: 'Reading ~/.claude/profiles…',
|
|
178
186
|
profilesUnread: (e) => 'The profiles could not be read — ' + (e || 'no reason given'),
|
|
179
187
|
profilesFound: (ids, seen) => 'Found ' + ids.length + ': ' + ids.join(', ') + '.' + (seen && seen.length ? ' ' + seen.join(', ') + (seen.length === 1 ? ' was' : ' were') + ' detected from your shell commands or config folders; a session under ' + (seen.length === 1 ? 'it' : 'one') + ' sets only CLAUDE_CONFIG_DIR.' : ''),
|
|
180
|
-
profilesNone: (dir) => 'No profile in ' + (dir || '~/.claude/profiles') + ' yet.',
|
|
188
|
+
profilesNone: (dir, srch) => 'No profile in ' + (dir || '~/.claude/profiles') + ' yet.' + (srch ? ' ' + P_searched(srch) : ''),
|
|
189
|
+
profileDefaultBadge: 'Default', profileDetectedBadge: 'Detected', profileUseDefault: 'Use as default', profileNoDefault: 'Stop using as default',
|
|
190
|
+
profilesLookAgain: 'Look again',
|
|
191
|
+
profileMeta: (x) => x.detected
|
|
192
|
+
? 'Detected from ' + ({ shell: 'a shell command', launcher: 'a launcher script', 'config-dir': 'a config folder' }[x.detected.source] || 'your machine') + ' · sets CLAUDE_CONFIG_DIR'
|
|
193
|
+
: [x.label && x.label !== x.id ? x.label : null, x.account, x.env_count ? x.env_count + ' variable' + (x.env_count === 1 ? '' : 's') : null].filter(Boolean).join(' · ') || 'Profile descriptor',
|
|
181
194
|
pfId: 'Id (the file name)', pfLabel: 'Label', pfAccount: 'Account, shown as written and not checked', pfMcp: 'MCP server names, declared only',
|
|
182
195
|
pfEnvHead: 'Environment variables applied to the session',
|
|
183
196
|
pfEnvName: 'Variable name', pfEnvValue: 'Value', pfEnvValuePh: 'value (hidden)', pfAddVar: 'Add variable',
|
|
@@ -42,12 +42,24 @@ const PILL = 'font-family:var(--mono);font-size:10px;padding:2px 6px;background:
|
|
|
42
42
|
/* ── the Claude profiles (the detail Settings opens) ─────────────────────────────────────────── */
|
|
43
43
|
|
|
44
44
|
/** The registered profiles: the live data layer's, else (fixture mode only) the seed's. */
|
|
45
|
-
const profileList = () =>
|
|
45
|
+
const profileList = () => {
|
|
46
|
+
const p = state.profiles; // the /api/profiles answer ({ profiles: [...] }), or a bare list
|
|
47
|
+
return Array.isArray(p) ? p : p && Array.isArray(p.profiles) ? p.profiles : (FIXTURE ? FX.PROFILES : []);
|
|
48
|
+
};
|
|
46
49
|
|
|
47
50
|
function profBody(det) {
|
|
48
|
-
let pf = { id: '', account: '', mcp: [], noMcp: false, note: '' };
|
|
51
|
+
let pf = { id: '', account: '', mcp: [], noMcp: false, note: '', detected: null };
|
|
49
52
|
const x = profileList().find((z) => z.id === det.id);
|
|
50
|
-
if (x) pf = { ...x, noMcp: x.mcp.length
|
|
53
|
+
if (x) pf = { ...pf, ...x, account: x.account || '', mcp: x.mcp || [], note: x.note || (x.detected ? 'Found on this machine, not written by PMOS. A session started under it sets only CLAUDE_CONFIG_DIR.' : ''), noMcp: !(x.mcp && x.mcp.length) };
|
|
54
|
+
|
|
55
|
+
// a profile PMOS found rather than wrote has no descriptor file: say how it was found, in tokens-and-classes only
|
|
56
|
+
if (pf.detected) {
|
|
57
|
+
const how = { shell: 'a shell command', launcher: 'a launcher script', 'config-dir': 'a config folder' }[pf.detected.source] || 'this machine';
|
|
58
|
+
return [el('div', { class: 'fr-fp-stack', 'data-fr': 'profile-detected' },
|
|
59
|
+
el('span', { class: 'fr-fp-label' }, iv(pf.id)),
|
|
60
|
+
el('p', { class: 'fr-fp-sub' }, 'Detected from ' + how + '. ' + pf.note),
|
|
61
|
+
el('p', { class: 'fr-fp-sub' }, iv(pf.detected.from)))];
|
|
62
|
+
}
|
|
51
63
|
|
|
52
64
|
const out = [];
|
|
53
65
|
out.push(el('div', { style: HEAD_BLOCK },
|
|
@@ -130,56 +130,181 @@ export function renderBoard(inner, ctx) {
|
|
|
130
130
|
|
|
131
131
|
const rightPanel = note
|
|
132
132
|
? await (async () => {
|
|
133
|
-
// Load and render the full note detail using the note view's render logic
|
|
134
133
|
try {
|
|
135
|
-
// Import the note view's rendering for inline display
|
|
136
134
|
const noteModule = await import('./view-notes.js');
|
|
137
|
-
// Parse
|
|
135
|
+
// Parse sections from the note's data
|
|
138
136
|
let actions = [];
|
|
139
|
-
|
|
140
|
-
|
|
137
|
+
let decisions = [];
|
|
138
|
+
let questions = [];
|
|
139
|
+
let highlights = [];
|
|
140
|
+
let conflicts = [];
|
|
141
|
+
if (note.final_state) {
|
|
142
|
+
if (note.final_state.actions) actions = note.final_state.actions;
|
|
143
|
+
if (note.final_state.decisions) decisions = note.final_state.decisions;
|
|
144
|
+
if (note.final_state.questions) questions = note.final_state.questions;
|
|
141
145
|
}
|
|
142
|
-
|
|
143
|
-
|
|
146
|
+
if (note.highlights) highlights = note.highlights;
|
|
147
|
+
if (note.conflicts) conflicts = note.conflicts;
|
|
148
|
+
|
|
149
|
+
const parts = [];
|
|
150
|
+
|
|
151
|
+
// Header with title, date, initiative, and action buttons
|
|
152
|
+
parts.push(
|
|
144
153
|
el('div', { class: 'nt-detail-header' },
|
|
145
|
-
el('div', { class: 'nt-detail-title' },
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
...actions.map((action, idx) =>
|
|
154
|
-
el('div', { class: 'nt-detail-action-row' },
|
|
155
|
-
el('div', { class: 'nt-detail-action-text' }, action.text || ''),
|
|
156
|
-
action.owner && el('div', { class: 'nt-detail-action-owner' }, `Owner: ${action.owner}`),
|
|
157
|
-
action.due && el('div', { class: 'nt-detail-action-due' }, `Due: ${action.due}`),
|
|
158
|
-
iconButton({ icon: 'plus', class: 'nt-detail-make-task', title: 'Create task from this step' },
|
|
159
|
-
async () => {
|
|
160
|
-
try {
|
|
161
|
-
const result = await pmPost('/api/notes/create-task', {
|
|
162
|
-
id: note.id,
|
|
163
|
-
text: action.text,
|
|
164
|
-
owner: action.owner,
|
|
165
|
-
due: action.due,
|
|
166
|
-
});
|
|
167
|
-
if (result && result.ok) {
|
|
168
|
-
toast({ text: 'Task created' });
|
|
169
|
-
} else {
|
|
170
|
-
toast({ text: 'Could not create task' });
|
|
171
|
-
}
|
|
172
|
-
} catch (e) {
|
|
173
|
-
console.error('Task creation error:', e);
|
|
174
|
-
toast({ text: 'Error creating task' });
|
|
175
|
-
}
|
|
176
|
-
},
|
|
177
|
-
),
|
|
178
|
-
),
|
|
154
|
+
el('div', { class: 'nt-detail-title-bar' },
|
|
155
|
+
el('div', { class: 'nt-detail-title' }, note.title || 'Untitled note'),
|
|
156
|
+
el('div', { class: 'nt-detail-actions-bar' },
|
|
157
|
+
iconButton({ icon: 'play', ariaLabel: 'Resume recording', attrs: { class: 'nt-detail-resume' } }),
|
|
158
|
+
el('div', { class: 'nt-menu-dots' },
|
|
159
|
+
iconButton({ icon: 'more-vertical', ariaLabel: 'More options', attrs: { class: 'nt-detail-more' } })
|
|
160
|
+
)
|
|
161
|
+
)
|
|
179
162
|
),
|
|
180
|
-
|
|
163
|
+
el('div', { class: 'nt-detail-meta' },
|
|
164
|
+
el('span', { class: 'nt-detail-date' }, new Date(note.updated_at).toLocaleDateString()),
|
|
165
|
+
note.initiative && el('span', { class: 'nt-detail-initiative' }, typeof note.initiative === 'string' ? note.initiative : (note.initiative.title || note.initiative.id)),
|
|
166
|
+
),
|
|
167
|
+
)
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
// Summary/Overview
|
|
171
|
+
parts.push(
|
|
172
|
+
el('div', { class: 'nt-detail-summary' },
|
|
173
|
+
el('h3', { class: 'nt-detail-section-title' }, 'Overview'),
|
|
174
|
+
note.summary
|
|
175
|
+
? el('div', { class: 'nt-detail-summary-text' }, note.summary)
|
|
176
|
+
: el('p', { class: 'nt-hint' }, 'No summary yet'),
|
|
177
|
+
)
|
|
181
178
|
);
|
|
182
|
-
|
|
179
|
+
|
|
180
|
+
// Conflicts section
|
|
181
|
+
if (conflicts && conflicts.length > 0) {
|
|
182
|
+
parts.push(
|
|
183
|
+
el('div', { class: 'nt-detail-conflicts' },
|
|
184
|
+
el('h3', { class: 'nt-detail-section-title' }, `Conflicts (${conflicts.length})`),
|
|
185
|
+
el('div', { class: 'nt-detail-conflicts-list' },
|
|
186
|
+
...conflicts.map((conflict) =>
|
|
187
|
+
el('div', { class: 'nt-detail-conflict-item' },
|
|
188
|
+
el('div', { class: 'nt-conflict-text' }, conflict.text || ''),
|
|
189
|
+
el('div', { class: 'nt-conflict-details' },
|
|
190
|
+
conflict.file && el('span', { class: 'nt-conflict-file' }, conflict.file),
|
|
191
|
+
iconButton({ icon: 'eye', ariaLabel: 'Review conflict', attrs: { class: 'nt-conflict-review' } })
|
|
192
|
+
)
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
)
|
|
196
|
+
)
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Highlights section
|
|
201
|
+
if (highlights && highlights.length > 0) {
|
|
202
|
+
parts.push(
|
|
203
|
+
el('div', { class: 'nt-detail-highlights' },
|
|
204
|
+
el('h3', { class: 'nt-detail-section-title' }, `Highlights (${highlights.length})`),
|
|
205
|
+
el('div', { class: 'nt-detail-highlights-list' },
|
|
206
|
+
...highlights.map((h) =>
|
|
207
|
+
el('div', { class: 'nt-detail-highlight-item' },
|
|
208
|
+
el('div', { class: 'nt-highlight-text' }, h.text || ''),
|
|
209
|
+
el('div', { class: 'nt-highlight-meta' },
|
|
210
|
+
el('span', { class: 'nt-highlight-label' }, h.label || ''),
|
|
211
|
+
h.timestamp && el('span', { class: 'nt-highlight-time' }, h.timestamp),
|
|
212
|
+
iconButton({ icon: 'play', ariaLabel: 'Play audio', attrs: { class: 'nt-highlight-play' } })
|
|
213
|
+
)
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
)
|
|
217
|
+
)
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Decisions section
|
|
222
|
+
if (decisions && decisions.length > 0) {
|
|
223
|
+
parts.push(
|
|
224
|
+
el('div', { class: 'nt-detail-decisions' },
|
|
225
|
+
el('h3', { class: 'nt-detail-section-title' }, `Decisions (${decisions.length})`),
|
|
226
|
+
el('div', { class: 'nt-detail-decisions-list' },
|
|
227
|
+
...decisions.map((decision, idx) =>
|
|
228
|
+
el('div', { class: 'nt-detail-decision-item' },
|
|
229
|
+
el('div', { class: 'nt-decision-text' }, decision.text || decision),
|
|
230
|
+
el('div', { class: 'nt-decision-actions' },
|
|
231
|
+
iconButton({ icon: 'edit', ariaLabel: 'Edit decision', attrs: { class: 'nt-decision-edit' } })
|
|
232
|
+
)
|
|
233
|
+
)
|
|
234
|
+
)
|
|
235
|
+
)
|
|
236
|
+
)
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Open questions section
|
|
241
|
+
if (questions && questions.length > 0) {
|
|
242
|
+
parts.push(
|
|
243
|
+
el('div', { class: 'nt-detail-questions' },
|
|
244
|
+
el('h3', { class: 'nt-detail-section-title' }, `Open questions (${questions.length})`),
|
|
245
|
+
el('div', { class: 'nt-detail-questions-list' },
|
|
246
|
+
...questions.map((question) =>
|
|
247
|
+
el('div', { class: 'nt-detail-question-item' },
|
|
248
|
+
el('div', { class: 'nt-question-text' }, question.text || question),
|
|
249
|
+
el('div', { class: 'nt-question-actions' },
|
|
250
|
+
button({ label: 'Draft answer', variant: 'ghost', size: 'sm' })
|
|
251
|
+
)
|
|
252
|
+
)
|
|
253
|
+
)
|
|
254
|
+
)
|
|
255
|
+
)
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Actions/Next steps section
|
|
260
|
+
if (actions && actions.length > 0) {
|
|
261
|
+
parts.push(
|
|
262
|
+
el('div', { class: 'nt-detail-actions' },
|
|
263
|
+
el('h3', { class: 'nt-detail-section-title' }, `Next steps (${actions.length})`),
|
|
264
|
+
el('div', { class: 'nt-detail-actions-list' },
|
|
265
|
+
...actions.map((action) =>
|
|
266
|
+
el('div', { class: 'nt-detail-action-row' },
|
|
267
|
+
el('div', { class: 'nt-detail-action-text' }, action.text || ''),
|
|
268
|
+
el('div', { class: 'nt-action-meta' },
|
|
269
|
+
action.owner && el('span', { class: 'nt-action-owner' }, `Owner: ${action.owner}`),
|
|
270
|
+
action.due && el('span', { class: 'nt-action-due' }, `Due: ${action.due}`),
|
|
271
|
+
),
|
|
272
|
+
iconButton({
|
|
273
|
+
icon: 'plus',
|
|
274
|
+
ariaLabel: 'Create task',
|
|
275
|
+
attrs: { class: 'nt-detail-make-task' },
|
|
276
|
+
onClick: async () => {
|
|
277
|
+
try {
|
|
278
|
+
const result = await pmPost('/api/notes/create-task', {
|
|
279
|
+
id: note.id,
|
|
280
|
+
text: action.text,
|
|
281
|
+
owner: action.owner,
|
|
282
|
+
due: action.due,
|
|
283
|
+
});
|
|
284
|
+
if (result && result.ok) {
|
|
285
|
+
toast({ text: 'Task created' });
|
|
286
|
+
} else {
|
|
287
|
+
toast({ text: 'Could not create task' });
|
|
288
|
+
}
|
|
289
|
+
} catch (e) {
|
|
290
|
+
console.error('Task creation error:', e);
|
|
291
|
+
toast({ text: 'Error creating task' });
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
}),
|
|
295
|
+
)
|
|
296
|
+
)
|
|
297
|
+
)
|
|
298
|
+
)
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Files section (placeholder for future)
|
|
303
|
+
// Discussion/Transcript section (collapsible placeholder)
|
|
304
|
+
|
|
305
|
+
return el('div', { class: 'nt-menu-right' }, ...parts);
|
|
306
|
+
} catch (e) {
|
|
307
|
+
console.error('Note detail error:', e);
|
|
183
308
|
return el('div', { class: 'nt-menu-right' }, empty({ text: 'Could not load note detail' }));
|
|
184
309
|
}
|
|
185
310
|
})()
|
package/workspace/server.js
CHANGED
|
@@ -436,6 +436,8 @@ function profileById(id) {
|
|
|
436
436
|
// `env` VALUES NEVER CROSS THE WIRE (contract §1.3). An environment overlay is exactly where a
|
|
437
437
|
// credential lives, and D08's never-log-headers discipline covers anything the browser receives.
|
|
438
438
|
// The names are shipped so the pane can say WHAT was applied; the values are not.
|
|
439
|
+
// a path shown with the home folder as ~, so a config folder (which is also the CLAUDE_CONFIG_DIR value) is not sent whole
|
|
440
|
+
function tildePath(f) { const h = os.homedir(); return typeof f === 'string' && h && (f === h || f.startsWith(h + path.sep)) ? '~' + f.slice(h.length) : f; }
|
|
439
441
|
function profileWire(p, read) {
|
|
440
442
|
const env = p && p.env && typeof p.env === 'object' ? p.env : {};
|
|
441
443
|
const keys = Object.keys(env);
|
|
@@ -443,7 +445,7 @@ function profileWire(p, read) {
|
|
|
443
445
|
id: p.id, label: p.label == null ? null : p.label, account: p.account == null ? null : p.account,
|
|
444
446
|
mcp: Array.isArray(p.mcp) ? p.mcp.slice() : [],
|
|
445
447
|
// where a profile PMOS found (rather than was given) came from: a shell file, a launcher script or a config folder
|
|
446
|
-
detected: p.detected ? { source: p.detected.source, from: p.detected.from, line: p.detected.line } : null,
|
|
448
|
+
detected: p.detected ? { source: p.detected.source, from: tildePath(p.detected.from), line: p.detected.line } : null,
|
|
447
449
|
env_count: keys.length, env_keys: keys,
|
|
448
450
|
// Computed HERE, once, so no surface can re-word R6's five sentences.
|
|
449
451
|
line: profilesLib.profileLine(p, read),
|
|
@@ -6108,6 +6110,8 @@ async function handleRequest(req, res) {
|
|
|
6108
6110
|
dir: read.dir,
|
|
6109
6111
|
profiles: (read.profiles || []).map((x) => profileWire(x, read)),
|
|
6110
6112
|
broken: (read.broken || []).map((b) => ({ file: b.file, error: b.error })),
|
|
6113
|
+
// what detection looked at, so an empty list can say where it looked (names only, under the home folder)
|
|
6114
|
+
searched: (() => { try { return require('./lib/profile-discovery').searched(os.homedir()); } catch { return null; } })(),
|
|
6111
6115
|
prefs: readPrefs(),
|
|
6112
6116
|
error: read.error || null,
|
|
6113
6117
|
});
|