create-pmos 0.245.0 → 0.247.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-pmos",
3
- "version": "0.245.0",
3
+ "version": "0.247.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": {
@@ -1,3 +1,3 @@
1
- kernel-source: pmos@df74e9d
1
+ kernel-source: pmos@47bc3dc
2
2
  assembled: 2026-10-15
3
3
  mode: vendor
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Eval calibration log"
4
4
  description: "Append-only record of every PM override of an evaluator verdict."
5
- generated: {by: process:create-pmos, at: "2026-10-15T03:51:47Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-15T07:42:51Z"}
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-15T03:51:47Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-15T07:42:51Z"}
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-15T03:51:47Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-15T07:42:51Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -34,7 +34,7 @@ Settings holds settings only, in eight sections. On a wide screen they are cards
34
34
  - **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.
35
35
  - **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.
36
36
  - **PMOS's own Claude login** (Claude sessions): a Claude login that only PMOS uses. A `/login` typed in a terminal replaces the account in `~/.claude`, and PMOS's background runs then fail with "OAuth session expired". Press **Set up**. PMOS makes the profile `pmos` (the folder `~/.claude-pmos`) and opens a Claude session under it. Type `/login` there and sign in once. Then press **Use for PMOS**. The row shows **In use**. Terminals keep working as before: sign in or switch there whenever you like, and PMOS's login is not touched. **Sign in again** opens the session again. PMOS never makes this the default by itself, and it holds no password.
37
- - **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. The default profile is one setting for the whole workspace. PMOS keeps it on the server, so every browser, port and device starts a Claude session with the same profile. Choose it here or with "make … the default" in the New session form. Both change the same setting.
37
+ - **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. **Flags for automatic sessions** is a second list for the sessions PMOS starts for you, such as a scheduled brief. Such a session cannot answer a permission question, so it waits. Until you save a choice it starts with `--permission-mode=acceptEdits`, which ends the file-edit questions. A tool from a connector can still ask; allow it in your Claude settings or choose a looser mode here. The default profile is one setting for the whole workspace. PMOS keeps it on the server, so every browser, port and device starts a Claude session with the same profile. Choose it here or with "make … the default" in the New session form. Both change the same setting.
38
38
  - **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.
39
39
  - **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.
40
40
  - **Online**: puts this workspace at your own address, such as `pmos.pm-os.app`, through a Cloudflare tunnel. Enter the public address, the tunnel name, your Cloudflare team domain, the Access audience (AUD) tag and your email, then turn on the **Online** switch. The workspace runs the tunnel and remembers the switch when it restarts. On your phone, open the address and sign in with Cloudflare Access (Google or an email code). The workspace does not ask for its token there. Only the computer that runs the workspace can change this section. A phone sees whether it is on. When a sign-in at the address is refused, this section names the check that failed, such as the email or the AUD tag. [REMOTE.md](../REMOTE.md) has the one-time Cloudflare steps.
@@ -32,7 +32,7 @@ Settings is not a tab. While it is open, no tab is selected. Press a tab to go b
32
32
  | Day and time | The time zone, the first day of the week, your working hours, the free time before a meeting with prep, the buffer between meetings, and the time-of-day tint and daylight strip. |
33
33
  | Sharing | How a report is shared: off, a link, or sent by the sender. The audiences of the workspace. Schedules are not here. Each report has its own schedule, in Reports, under Set up. |
34
34
  | Access and devices | Use from a phone or another computer: the host, the tunnel and the people who may open it. |
35
- | Claude sessions | What a new session starts with: the CLI, the model, the Claude profile and the launch flags. The session limits. Validate consent for this workspace. |
35
+ | Claude sessions | What a new session starts with: the CLI, the model, the Claude profile and the launch flags for sessions you start, and the flags for automatic sessions. The session limits. Validate consent for this workspace. |
36
36
  | Privacy and data | A list of what leaves this computer and when. Whether a TypeSafe key is saved, the local model that answers when there is no key (Off, Winnow or Laya), and the data folder. |
37
37
  | Appearance | The theme (Same as system, Light or Dark) and the page that opens first. |
38
38
  | Updates | The version of the app and of each workspace's kit. Check for updates, and how updates are found and applied. |
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
  /* calendar-cli.js — `pmos calendar …`: the calendar events an agent session read, saved for Briefing and Calendar
3
3
  * (workspace-data-freshness R8).
4
- * pmos calendar import --from <file|-> --sensitivity public|internal|confidential|private [--dry-run] [--json]
4
+ * pmos calendar import --from <file|-> --sensitivity public|internal|confidential|private [--window <from>..<to>] [--dry-run] [--json]
5
5
  * pmos calendar status [--json]
6
6
  * This command never talks to a calendar. The calendar-sync skill reads it in an agent session, with the PM's own
7
7
  * connection, and hands the events to `import`. The import goes through scripts/calendar-import.js unchanged: hidden text
@@ -18,13 +18,14 @@ const L = require(repoScript('trust/ledger.js'));
18
18
  const COMMANDS = { calendar: ['import', 'status'] };
19
19
  const USAGE = [
20
20
  'usage: pmos calendar import|status — the calendar events an agent session read for Briefing and Calendar',
21
- ' pmos calendar import --from <file|-> --sensitivity <label> [--dry-run] save the events (the calendar-sync skill runs this)',
21
+ ' pmos calendar import --from <file|-> --sensitivity <label> [--window YYYY-MM-DD..YYYY-MM-DD] [--dry-run] save the events (the calendar-sync skill runs this)',
22
+ ' --window: the days the events cover. Saved events in those days that are not in the file are removed (moved, declined, cancelled).',
22
23
  ' pmos calendar status how many events are saved, and when they were read',
23
24
  ' every command: [--json]',
24
25
  'This command never calls a calendar. Your agent reads it with your own connection.',
25
26
  ];
26
27
  const BOOL = new Set(['json', 'dry-run', 'help']);
27
- const VALUE = new Set(['from', 'sensitivity']);
28
+ const VALUE = new Set(['from', 'sensitivity', 'window']);
28
29
 
29
30
  function parseArgs(argv) {
30
31
  const out = { _: [] };
@@ -69,16 +70,25 @@ async function run(argv, { say = console.log, env = process.env, stateDir, stdin
69
70
  try { raw = JSON.parse(readInput(a.from, stdin)); } catch (e) { say(`✗ the events file is not JSON: ${String(e.message).slice(0, 160)}`); return 1; }
70
71
  const list = Array.isArray(raw) ? raw : raw && Array.isArray(raw.events) ? raw.events : null;
71
72
  if (!list) { say('✗ the file must hold a list of events, or { "events": [ … ] }'); return 1; }
73
+ let window;
74
+ if (a.window) {
75
+ const m = /^(\d{4}-\d{2}-\d{2})\.\.(\d{4}-\d{2}-\d{2})$/.exec(a.window);
76
+ if (!m) { say('✗ --window must look like 2026-10-15..2026-10-29'); return 2; }
77
+ window = { from: m[1], to: m[2] };
78
+ try { CI.checkWindow(window); } catch (e) { say(`✗ ${e.message}`); return 2; }
79
+ }
72
80
  const at = now().toISOString();
73
81
  let evs;
74
82
  try { evs = CI.importEvents(list, { fetchedAt: at, sensitivity: a.sensitivity }); }
75
83
  catch (e) { say(`✗ ${e.message}`); return 1; }
76
84
  if (a['dry-run']) { out({ ok: true, dryRun: true, count: evs.length }, `would save ${evs.length} events (dry run, nothing written)`); return 0; }
77
85
  const actor = L.actorFrom({ env, isTTY: isTTY == null ? !!(process.stdin.isTTY && process.stdout.isTTY) : isTTY });
78
- try { CI.appendImport(file, evs, actor, at); }
86
+ let lines;
87
+ try { lines = CI.appendImport(file, evs, actor, at, { window }); }
79
88
  catch (e) { say(`✗ the events could not be saved: ${String(e.message).slice(0, 160)}`); return 1; }
89
+ const removed = lines.length - evs.length;
80
90
  const flagged = evs.filter((e) => e.findings && e.findings.length).length;
81
- out({ ok: true, count: evs.length, flagged, read_at: at }, `✓ saved ${evs.length} calendar events${flagged ? ` (${flagged} carried hidden or suspicious text, marked)` : ''}`);
91
+ out({ ok: true, count: evs.length, removed, flagged, read_at: at }, `✓ saved ${evs.length} calendar events${removed ? `, removed ${removed} that the calendar no longer holds in those days` : ''}${flagged ? ` (${flagged} carried hidden or suspicious text, marked)` : ''}`);
82
92
  return 0;
83
93
  }
84
94
 
@@ -24,6 +24,12 @@ const D = require('./driver.js');
24
24
  const facts = require('./driver-facts.js');
25
25
  const proactive = require('./proactive.js');
26
26
 
27
+ // A run that fails because the Claude login has expired fails again for every other target. Holding the next runs for this long (their
28
+ // wakes stay queued) stops one expired login from filing hundreds of failed runs; the first run after the wait is the probe.
29
+ const LOGIN_HOLD_MS = 30 * 60 * 1000;
30
+ const LOGIN_RE = /failed to authenticate|oauth (session|token)|session expired|could not be refreshed|not logged in|invalid api key|authentication_error|\b401\b/i;
31
+ const loginFailure = (r) => LOGIN_RE.test([r && r.note, r && r.result && r.result.text, r && r.result && r.result.error].filter(Boolean).join(' '));
32
+
27
33
  const off = (env) => /^(off|0|false|no)$/i.test(String((env || process.env).PMOS_DRIVER || '').trim());
28
34
  const hhmm = (ms) => { const d = new Date(ms); return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`; };
29
35
  const dayCheckOpen = (settings, now, inQuiet) => {
@@ -44,6 +50,7 @@ function readJsonl(f) {
44
50
  function create(deps) {
45
51
  const active = new Map(); // run id → { target, ledger, session }
46
52
  const lastSweep = new Map(); // ledger file → ms
53
+ let loginFailedAt = 0; // ms of the last run that failed on the Claude login; new runs wait LOGIN_HOLD_MS after it
47
54
  const warn = deps.warn || (() => {});
48
55
  const gather = deps.gather || facts.gather; // tests pass their own facts
49
56
 
@@ -122,6 +129,8 @@ function create(deps) {
122
129
  const res = (r && r.result) || {};
123
130
  const ok = !!(r && !r.timedOut && !r.stopped && r.code === 0 && res.ok && !res.isError);
124
131
  const reason = ok ? null : (r && r.note) || 'the run failed';
132
+ if (ok) loginFailedAt = 0;
133
+ else if (loginFailure(r)) loginFailedAt = Date.now();
125
134
  try {
126
135
  D.append(file, { ev: 'run-end', run: runId, ok, ...(reason ? { reason } : {}), ...(res.denied && res.denied.length ? { denied: res.denied } : {}),
127
136
  agent_run: agentRun(runId, target, ok) });
@@ -170,6 +179,7 @@ function create(deps) {
170
179
  }
171
180
  // Start what the rules allow.
172
181
  if (deps.headlessOff()) continue; // a driver never runs in a pane
182
+ if (loginFailedAt && now - loginFailedAt < LOGIN_HOLD_MS) { summary.held = 'login'; continue; } // wakes stay queued
173
183
  const running = [...active.values()];
174
184
  const p = D.plan(state, { now, running: active.size, activeTargets: running.map((r) => D.targetKey(r.target)) });
175
185
  for (const r of p.refused) { D.append(file, { ev: 'refused', target: r.target, wakes: r.wakes, reason: r.reason }, { now }); summary.refused++; }
@@ -191,4 +201,4 @@ function create(deps) {
191
201
  return { tick, active: () => [...active.keys()] };
192
202
  }
193
203
 
194
- module.exports = { create, off, dayCheckOpen };
204
+ module.exports = { create, off, dayCheckOpen, loginFailure, LOGIN_HOLD_MS };
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * importEvents(rawEvents, { fetchedAt, sensitivity }) → [event] pure; a missing or unknown sensitivity is refused
9
9
  * foldEvents(lines) → [event] current events; the PM's notes survive a refresh
10
- * appendImport(file, events, actor, now) → lines ledger append (scripts/trust/ledger.js)
10
+ * appendImport(file, events, actor, now, {window}) → lines ledger append (scripts/trust/ledger.js); a window removes the saved events of its days the import no longer holds
11
11
  * appendNote(file, eventId, text, actor, now) → line a PM note on one event
12
12
  * readLines(file) · cacheMarkdown(events, dir, now) the Markdown cache is the PM's trust/import-calendar.js, unchanged
13
13
  */
@@ -52,6 +52,7 @@ function foldEvents(lines) {
52
52
  const by = new Map();
53
53
  for (const l of lines || []) {
54
54
  if (!l) continue;
55
+ if (l.kind === 'calendar_event_removed') { by.delete(l.event_id); continue; }
55
56
  if (l.kind === 'calendar_event' && l.event && l.event.id) {
56
57
  const prev = by.get(l.event.id);
57
58
  by.set(l.event.id, { ...l.event, agent_prep_notes: prev ? prev.agent_prep_notes : l.event.agent_prep_notes || '', pm_notes: prev ? prev.pm_notes : [] });
@@ -64,9 +65,33 @@ function foldEvents(lines) {
64
65
 
65
66
  const readLines = (file) => L.read(file);
66
67
 
67
- function appendImport(file, events, actor, now) {
68
+ const DAY = /^\d{4}-\d{2}-\d{2}$/;
69
+ /** A window is { from, to }, local days YYYY-MM-DD, from <= to. Throws on anything else. */
70
+ function checkWindow(w) {
71
+ if (!w || !DAY.test(String(w.from)) || !DAY.test(String(w.to)) || String(w.from) > String(w.to)) throw new Error('window needs from and to as YYYY-MM-DD, from not after to');
72
+ return { from: String(w.from), to: String(w.to) };
73
+ }
74
+ /** The saved events a windowed import replaces: those that start inside the window (the day written in their `at`) and are not in the import. Pure. */
75
+ function supersededIds(saved, imported, window) {
76
+ const w = checkWindow(window);
77
+ const keep = new Set((imported || []).map((e) => e.id));
78
+ return (saved || []).filter((e) => { const d = String(e.at || '').slice(0, 10); return d >= w.from && d <= w.to && !keep.has(e.id); }).map((e) => e.id);
79
+ }
80
+
81
+ // opts.window: the days this import covers. Saved events in those days that the import does not hold are removed (calendar_event_removed),
82
+ // so a rescheduled, declined or cancelled meeting leaves the ledger. The ledger itself stays append only.
83
+ function appendImport(file, events, actor, now, opts = {}) {
84
+ let gone = [];
85
+ if (opts.window) {
86
+ const w = checkWindow(opts.window);
87
+ if (!events.length) throw new Error('a window import needs at least one event: an empty read is not a calendar with no meetings');
88
+ gone = supersededIds(foldEvents(readLines(file)), events, w);
89
+ }
68
90
  fs.mkdirSync(path.dirname(file), { recursive: true });
69
- return events.map((event) => L.append(file, { id: L.newId(), kind: 'calendar_event', why: 'calendar import (reported, untrusted)', at: now || new Date().toISOString(), event }, actor));
91
+ const at = now || new Date().toISOString();
92
+ const lines = events.map((event) => L.append(file, { id: L.newId(), kind: 'calendar_event', why: 'calendar import (reported, untrusted)', at, event }, actor));
93
+ for (const event_id of gone) lines.push(L.append(file, { id: L.newId(), kind: 'calendar_event_removed', why: 'not in the import for its days (rescheduled, declined or cancelled)', at, event_id }, actor));
94
+ return lines;
70
95
  }
71
96
 
72
97
  function appendNote(file, eventId, text, actor, now) {
@@ -81,4 +106,4 @@ function cacheMarkdown(events, dir, now) {
81
106
  return sampleCache.importEvents(events.map((e) => ({ id: e.id, title: e.title, start: e.at, end: e.end || e.at, attendees: e.attendees, initiative: e.initiative })), dir, now);
82
107
  }
83
108
 
84
- module.exports = { SENSITIVITY, importEvents, foldEvents, appendImport, appendNote, readLines, cacheMarkdown };
109
+ module.exports = { SENSITIVITY, importEvents, foldEvents, appendImport, supersededIds, checkWindow, appendNote, readLines, cacheMarkdown };
@@ -2,7 +2,8 @@
2
2
  /* routes-calendar-import.js — cached calendar events, supplied by a session (console3 wave E2a).
3
3
  *
4
4
  * GET /api/calendar-import/events → { ok, events } the folded cache
5
- * POST /api/calendar-import/events { events, sensitivity } → { ok, count } page press; sensitivity is required
5
+ * POST /api/calendar-import/events { events, sensitivity, window? } → { ok, count, removed } page press; sensitivity is required;
6
+ * window { from, to } (local days) removes the saved events of those days the import does not hold
6
7
  * POST /api/calendar-import/note { event_id, text } → { ok } the PM's note on an event
7
8
  *
8
9
  * Nothing here fetches a calendar: PMOS holds no credentials. Events come in the body. Imports are untrusted data
@@ -28,9 +29,11 @@ function importRoute(req, body, runtimeDir, now = new Date()) {
28
29
  let evs;
29
30
  try { evs = CI.importEvents(body.events, { fetchedAt: now.toISOString(), sensitivity: body.sensitivity }); }
30
31
  catch (e) { return fail(400, e.message); }
31
- try { CI.appendImport(fileOf(runtimeDir), evs, L.actorFrom({ ui: true }), now.toISOString()); }
32
+ let removed = 0;
33
+ if (body.window) { try { CI.checkWindow(body.window); } catch (e) { return fail(400, e.message); } if (!evs.length) return fail(400, 'a window import needs at least one event'); }
34
+ try { removed = CI.appendImport(fileOf(runtimeDir), evs, L.actorFrom({ ui: true }), now.toISOString(), { window: body.window }).length - evs.length; }
32
35
  catch (e) { return fail(500, e.message); }
33
- return { status: 200, body: { ok: true, count: evs.length } };
36
+ return { status: 200, body: { ok: true, count: evs.length, removed } };
34
37
  }
35
38
 
36
39
  function noteRoute(req, body, runtimeDir, now = new Date()) {
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.245.0",
3
+ "version": "0.247.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pmos-workspace",
9
- "version": "0.245.0",
9
+ "version": "0.247.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@xterm/addon-fit": "^0.11.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.245.0",
3
+ "version": "0.247.0",
4
4
  "private": true,
5
5
  "description": "PMOS Terminal Workspace — local companion hosting real PTYs beside the library and gates (D62)",
6
6
  "license": "MIT",
@@ -25,6 +25,7 @@ import { button, SEL } from './primitives.js';
25
25
  import { pmGet, pmPost } from './http.js';
26
26
  import { c3ic } from './pm-work-view.js';
27
27
  import { sessionDefaults, setSessionDefaults } from './dock.js';
28
+ import { flagPicker, flagsLine, flagsMissingValue } from './flag-picker.js';
28
29
  import { toast } from './c3-toast.js';
29
30
  import { dayPartOf } from './c3-daypart.js';
30
31
  import { finderBlock } from './c3-report-store.js';
@@ -147,7 +148,7 @@ export function mountSettings(host, ctx = {}) {
147
148
  ensureCss();
148
149
  const M = {
149
150
  sec: SECTIONS.some((s) => s[0] === ctx.section) ? ctx.section : 'about', q: '', ws: ctx.ws || '', data: null, err: {},
150
- engine: null, people: null, consent: null, online: null, profiles: null, typesafe: null, kits: {}, kitBusy: null, app: { phase: null, error: '' }, about: null, running: null,
151
+ engine: null, flagList: null, autoFlags: null, flagFilter: '', flagsOpen: { mine: false, auto: false }, people: null, consent: null, online: null, profiles: null, typesafe: null, kits: {}, kitBusy: null, app: { phase: null, error: '' }, about: null, running: null,
151
152
  tplEdit: null, wsEdit: null, wsDrop: null, addPath: '',
152
153
  rf: null, rfLog: null, rfSig: '', rfFilter: 'all', rfEdit: null, rfAsk: null, rfErr: {}, rfAllBusy: false, rfLoading: false, logSource: '', logResult: '',
153
154
  };
@@ -191,6 +192,8 @@ export function mountSettings(host, ctx = {}) {
191
192
  function loadOnce() {
192
193
  pmGet('/api/online').then((r) => { M.online = r && r.ok !== false ? r : { error: (r && r.error) || 'not read' }; draw(); });
193
194
  pmGet('/api/profiles').then((r) => { M.profiles = r && r.ok !== false ? r : { error: (r && r.error) || 'not read', profiles: [] }; draw(); });
195
+ pmGet('/api/claude-flags').then((r) => { M.flagList = r || { ok: false, error: 'no answer', flags: [] }; draw(); });
196
+ pmGet('/api/auto-session-flags').then((r) => { M.autoFlags = r && r.ok !== false ? r : { error: (r && r.error) || 'not read', flags: [] }; draw(); });
194
197
  pmGet('/api/typesafe').then((r) => { M.typesafe = r && r.ok ? r.key : null; draw(); });
195
198
  pmGet('/api/proactive/settings').then((r) => { const s = r && r.ok && r.settings; M.about = s ? { name: s.name || '', role: (s.about || {}).role || '', work: (s.about || {}).work || '', duties: (s.about || {}).duties || '' } : { error: (r && r.error) || 'not read' }; draw(); });
196
199
  pmGet('/api/sessions').then((r) => { M.running = r && Array.isArray(r.sessions) ? r.sessions : null; draw(); });
@@ -541,6 +544,36 @@ export function mountSettings(host, ctx = {}) {
541
544
  ...later];
542
545
  }
543
546
 
547
+ /* Launch flags. 'mine': what a session you start begins with (kept in this browser, as the other defaults are).
548
+ * 'auto': what a session PMOS starts for you begins with (a schedule, a day run); kept on this computer, because no
549
+ * browser is open to answer a permission question when one fires. */
550
+ function flagsRow(which) {
551
+ const auto = which === 'auto';
552
+ const chosen = auto ? ((M.autoFlags && M.autoFlags.flags) || []) : sessionDefaults().flags;
553
+ const open = M.flagsOpen[which];
554
+ const save = async (next) => {
555
+ if (auto) {
556
+ const r = await pmPost('/api/auto-session-flags', { flags: next });
557
+ if (r && r.ok !== false) { M.autoFlags = r; delete M.err['sess.flags.auto']; toast('Saved: flags for automatic sessions'); }
558
+ else M.err['sess.flags.auto'] = (r && r.error) || 'Not saved.';
559
+ } else { setSessionDefaults({ flags: next }); toast('Saved: launch flags'); }
560
+ draw();
561
+ };
562
+ const missing = M.flagList ? flagsMissingValue(M.flagList, chosen) : [];
563
+ const control = el('div', { class: 'c3set-flags', 'data-c3set-flags': which },
564
+ el('p', { class: 'dim mono' }, chosen.length ? 'claude ' + flagsLine(chosen) : 'No flags.'),
565
+ missing.length ? el('p', { class: 'dim', role: 'alert' }, missing[0] + ' needs a value before it can be saved.') : null,
566
+ flagPicker({ list: M.flagList, chosen, filter: M.flagFilter, compact: !open, onFilter: (q) => { M.flagFilter = q; draw(); },
567
+ onChange: (next) => { if (auto && flagsMissingValue(M.flagList, next).length) { M.autoFlags = { ...M.autoFlags, flags: next }; draw(); } else save(next); } }),
568
+ btn(open ? 'Hide the full list' : 'Show every flag', { variant: 'ghost', onclick: () => { M.flagsOpen[which] = !open; draw(); }, attrs: { 'data-c3set-flags-all': which } }));
569
+ return frame(auto
570
+ ? { key: 'sess.flags.auto', label: 'Flags for automatic sessions', scope: 'computer', wide: true, control, err: M.err['sess.flags.auto'],
571
+ help: 'Sessions PMOS starts for you, such as a scheduled brief, cannot answer a permission question, so they wait. Set a permission mode here. Until you save a choice they start with --permission-mode=acceptEdits.',
572
+ note: M.autoFlags && M.autoFlags.saved === false ? 'Using the default. Change a flag to save your own.' : null }
573
+ : { key: 'sess.flags', label: 'Launch flags', scope: 'computer', wide: true, control,
574
+ help: 'Flags for the claude command when you start a session, such as a permission mode. You can change them for one session in the New session form.', note: 'Kept in this browser, as the dock keeps it.' });
575
+ }
576
+
544
577
  function sessionsSec() {
545
578
  const d = sessionDefaults(); const p = M.profiles;
546
579
  const profs = (p && p.profiles) || [];
@@ -559,7 +592,7 @@ export function mountSettings(host, ctx = {}) {
559
592
  extRow('sess.cli', seg('Start with', [['claude', 'Claude Code'], ['shell', 'A shell']], d.cli, (v) => setDef({ cli: v }, 'sess.cli', { cli: d.cli })), { scope: 'computer', note: 'Kept in this browser, as the dock keeps it.' }),
560
593
  extRow('sess.tier', seg('Model', [['auto', 'Auto'], ['fast', 'Fast'], ['balanced', 'Balanced'], ['deep', 'Deep']], d.tier, (v) => setDef({ tier: v }, 'sess.tier', { tier: d.tier })), { scope: 'computer' }),
561
594
  extRow('sess.profile', profileSelect('c3set-sess-profile'), { scope: 'computer', forId: 'c3set-sess-profile', note: profs.length ? null : p ? 'No profile was found. Make one in the classic app\'s Settings, Profiles (/classic.html).' : 'Reading profiles.' }),
562
- lockedRow('sess.flags', 'Launch flags', 'Flags for the claude command, such as a permission mode.', 'Set when you start a session', 'computer')], 'term'),
595
+ flagsRow('mine'), flagsRow('auto')], 'term'),
563
596
  ownLoginSec(profs, def),
564
597
  profs.length ? sect('Profiles', [list(profRows), help('A new profile is made in the classic app\'s Settings, Profiles (/classic.html).')], { ic: 'user', count: profs.length }) : null,
565
598
  groupOf('sessions', 'Housekeeping', 'term'),
@@ -473,6 +473,22 @@ function writePrefs(prefs) {
473
473
  try { fs.writeFileSync(PREFS_FILE, JSON.stringify(prefs)); return null; }
474
474
  catch (e) { warnWriteFail('prefs.json (profile preselection)', e); return `the preference could not be saved — ${String((e && e.code) || 'write failed')}`; }
475
475
  }
476
+ // Launch flags for AUTOMATIC Claude panes (a schedule, a day run, a refresh): the ones the PM did not press. They cannot answer a
477
+ // permission question, so a pane that asks stays "Starting" for ever. The PM sets the flags here (Settings, New sessions);
478
+ // until then a pane starts in acceptEdits, which ends the file-edit prompts. Headless runs have their own fixed tool lists.
479
+ const AUTO_FLAGS_FILE = path.join(STATE, 'auto-session-flags.json');
480
+ const AUTO_FLAGS_DEFAULT = Object.freeze([{ flag: '--permission-mode', value: 'acceptEdits' }]);
481
+ function readAutoFlags() {
482
+ try {
483
+ const o = JSON.parse(fs.readFileSync(AUTO_FLAGS_FILE, 'utf8'));
484
+ if (o && Array.isArray(o.flags) && claudeFlags.resolve(o.flags, 'claude').ok) return { flags: o.flags.map((x) => ({ flag: x.flag, value: x.value || '' })), saved: true };
485
+ } catch { /* none saved, or unreadable: the default stands */ }
486
+ return { flags: AUTO_FLAGS_DEFAULT.map((x) => ({ ...x })), saved: false };
487
+ }
488
+ function writeAutoFlags(flags) {
489
+ try { fs.writeFileSync(AUTO_FLAGS_FILE, JSON.stringify({ flags })); return null; }
490
+ catch (e) { warnWriteFail('auto-session-flags.json', e); return `the flags could not be saved — ${String((e && e.code) || 'write failed')}`; }
491
+ }
476
492
  // The default profile for an agent, for a start the PM pressed that named no profile at all (`undefined`). An explicit
477
493
  // null means "no profile" and is never replaced. A remembered id that no longer reads yields undefined: the start then
478
494
  // runs with no profile and says nothing it did not do, rather than failing on a preference the PM did not just choose.
@@ -3826,6 +3842,10 @@ function spawnSessionInner({ name, agent, cwd, ws, origin, initialInput, profile
3826
3842
  /* LAUNCH FLAGS (workspace-launch-flags). Checked against the generated claude flag list, and refused like a model
3827
3843
  * choice: a flag that cannot be honoured starts nothing. The tokens go AFTER the model's, and a flag the model
3828
3844
  * chips already own is refused by the list itself, so the two never both speak. */
3845
+ // An automatic PANE (not a headless run) that was handed no flags takes the PM's automatic-session flags, so it never waits on a prompt nobody sees.
3846
+ if (launchFlags === undefined && agent === 'claude' && sessionOrigin.isAutomatic(tag) && !(initialInput && headlessLib.kindFor({ agent, origin: tag, skill: modelAsk && modelAsk.skill }))) {
3847
+ launchFlags = readAutoFlags().flags;
3848
+ }
3829
3849
  const fres = claudeFlags.resolve(launchFlags, agent);
3830
3850
  if (!fres.ok) return exitedSession(base, workdir, `launch flag refused — ${fres.error}`);
3831
3851
  if (launchFlags !== undefined) base.launchFlags = fres.applied;
@@ -6660,6 +6680,19 @@ async function handleRequest(req, res) {
6660
6680
  }
6661
6681
  // Every flag `claude --help` lists, with whether the form may offer it (workspace-launch-flags). A static file.
6662
6682
  if (p === '/api/claude-flags' && req.method === 'GET') return json(res, claudeFlags.list());
6683
+ // The flags automatic Claude panes start with (a schedule, a day run). Checked against the same list as every other launch.
6684
+ if (p === '/api/auto-session-flags' && req.method === 'GET') { const a = readAutoFlags(); return json(res, { ok: true, flags: a.flags, saved: a.saved }); }
6685
+ if (p === '/api/auto-session-flags' && req.method === 'POST') {
6686
+ let body = {};
6687
+ try { body = JSON.parse((await readBody(req)) || '{}'); }
6688
+ catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
6689
+ const flags = Array.isArray(body.flags) ? body.flags.map((x) => ({ flag: x && x.flag, value: x && x.value ? String(x.value) : '' })) : null;
6690
+ if (!flags) return json(res, { ok: false, error: 'flags must be a list' }, 400);
6691
+ const chk = claudeFlags.resolve(flags, 'claude');
6692
+ if (!chk.ok) return json(res, { ok: false, error: chk.error }, 400);
6693
+ const err = writeAutoFlags(flags);
6694
+ return err ? json(res, { ok: false, error: err }, 500) : json(res, { ok: true, flags, saved: true });
6695
+ }
6663
6696
  /* Create one descriptor from the Settings form (workspace-launch-flags). It writes ONE file, ~/.claude/profiles/<id>.json,
6664
6697
  * and only when none by that name exists: PMOS never overwrites a descriptor the PM keeps. The file is read back through
6665
6698
  * the same reader every other surface uses, so a descriptor this route wrote that the reader would refuse is removed
@@ -24,8 +24,8 @@ Procedure:
24
24
  1. List the PM's events from the start of today to the end of the 14th day after today, from the primary calendar. Fetch every page. Skip an event the PM declined.
25
25
  2. Write the events as JSON in the scratch folder, for example `<scratch>/calendar.json`, as a list with one object per event:
26
26
  `{"id", "title", "start", "end", "attendees": ["Display Name", …], "initiative": "<initiative id or null>"}`
27
- `id` is the calendar's own id for that event (for a repeating meeting, the id of that one occurrence). Use the same id on every run, never a made-up one: PMOS keeps one copy per id.
27
+ `id` is the calendar's own id for that event (for a repeating meeting, the id of that one occurrence). Use the same id on every run, never a made-up one: PMOS keeps one copy per id. If an id changes anyway, the window import removes the old copy.
28
28
  `start` and `end` are ISO times with the offset, for example `2026-10-12T09:00:00+07:00`. An all-day event has the date only.
29
- 3. Run `pmos calendar import --from <scratch>/calendar.json --sensitivity internal`. If it refuses the file, fix what it names and run it once more.
29
+ 3. Run `pmos calendar import --from <scratch>/calendar.json --sensitivity internal --window <today>..<day 14>`, with both days as `YYYY-MM-DD`. The window makes PMOS drop a saved event in those days that is not in the file, so a meeting that moved, was declined or was cancelled leaves its old slot. If it refuses the file, fix what it names and run it once more.
30
30
  4. Delete the scratch file.
31
31
  5. Report in four short lists: Imported (the count, and the first and last day), Left out (the count of private events), Not reached, and Needs you (only when something blocked the pull).
@@ -35,6 +35,7 @@
35
35
  "work-assigned.test.js",
36
36
  "briefing-real-data.test.js",
37
37
  "calendar-events.test.js",
38
+ "calendar-supersede.test.js",
38
39
  "tracker-read.test.js",
39
40
  "item-steps.test.js",
40
41
  "status-facts-panel.test.js",
@@ -186,6 +187,7 @@
186
187
  "work-assigned.test.js": 1,
187
188
  "briefing-real-data.test.js": 1,
188
189
  "calendar-events.test.js": 1,
190
+ "calendar-supersede.test.js": 1,
189
191
  "tracker-read.test.js": 1,
190
192
  "pm-brief-page-test.js": 30,
191
193
  "item-steps.test.js": 1,