create-pmos 0.128.0 → 0.129.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.128.0",
3
+ "version": "0.129.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@337d695
1
+ kernel-source: pmos@64a9920
2
2
  assembled: 2026-10-11
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-11T03:06:09Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-11T03:14:46Z"}
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-11T03:06:09Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-11T03:14:46Z"}
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-11T03:06:09Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-11T03:14:46Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -2,7 +2,7 @@
2
2
  type: Guide
3
3
  title: "Guide: Today and the proactive engine"
4
4
  description: "What the Today item shows, how your goals for the day are prepared, how the morning brief is made, and how the workspace works out what is waiting for you."
5
- timestamp: 2026-10-06
5
+ timestamp: 2026-10-11
6
6
  ---
7
7
 
8
8
  # Guide: Today and the proactive engine
@@ -92,7 +92,7 @@ A check is due for one of three reasons.
92
92
 
93
93
  **Backoff.** A check that finds nothing new and resolves nothing is an empty check. Each empty check in a row doubles the wait, up to 120 minutes. A check that finds or resolves something sets the wait back to the interval. For that reason the session submits an empty merge when nothing changed, so the check counts.
94
94
 
95
- No check is due when no run exists for the day, when a check is running, when checks are off, or outside the start and end times. **Check now** starts a check at once for the run of the day. It is refused when a check is running or when there is no run.
95
+ No check is due when no run exists for the day, when a check is running, when checks are off, or outside the start and end times. **Check now** starts a check at once for the run of the day. It is refused when a check is running or when there is no run. A new check uses the session of the last check again when that session is still open and quiet. Only the new run's line is typed into it, so the day keeps one check session.
96
96
 
97
97
  The page shows when the last check ran, when the next is due, and a log of the last 20 checks with the reason, how many items they found and how many they resolved.
98
98
 
@@ -284,6 +284,7 @@ function sessionEnded(run, sessionId, now) {
284
284
  }
285
285
  if (next.brief_session && next.brief_session.id === sessionId && !next.brief_session.ended_at) { next.brief_session.ended_at = iso(now); changed = true; }
286
286
  if (next.checks.session_id === sessionId) { next.checks.session_id = null; changed = true; }
287
+ if (sessionId && next.checks.prev_session === sessionId) { next.checks.prev_session = null; changed = true; }
287
288
  return { ok: true, run: next, changed };
288
289
  }
289
290
 
@@ -376,6 +377,9 @@ function checkDone(run, { found = 0, resolved = 0 } = {}, now) {
376
377
  const next = clone(run);
377
378
  const f = Math.max(0, Number(found) || 0); const r = Math.max(0, Number(resolved) || 0);
378
379
  next.checks.empty_streak = f === 0 && r === 0 ? next.checks.empty_streak + 1 : 0;
380
+ // The check's session stays open after its merge; prev_session keeps its id so the next check is typed into it
381
+ // instead of starting another session (workspace-session-lifecycle), the way a day task reuses its own.
382
+ if (next.checks.session_id) next.checks.prev_session = next.checks.session_id;
379
383
  next.checks.session_id = null;
380
384
  const open = [...next.checks.log].reverse().find((x) => x.found === null);
381
385
  if (open) { open.found = f; open.resolved = r; open.done_at = iso(now); }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.128.0",
3
+ "version": "0.129.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pmos-workspace",
9
- "version": "0.128.0",
9
+ "version": "0.129.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.128.0",
3
+ "version": "0.129.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",
@@ -2669,10 +2669,14 @@ function adoptOrphanTmuxSessions() {
2669
2669
  for (const t of tmuxList().sort((a, b) => a.created - b.created)) {
2670
2670
  if (!t.name.startsWith('pmos-') || claimed.has(t.name)) continue;
2671
2671
  const dir = t.dir && isDir(t.dir) ? path.resolve(t.dir) : null;
2672
- // Where it belongs: the workspace containing its pane directory; else — because a pane the PM
2673
- // can see must be a pane the PM can reach — that directory is registered as one, exactly as
2674
- // `pmos <path>` would. Only the home workspace is assumed when there is no readable directory.
2675
- const w = workspaceForPath(dir) || (dir && registerWorkspace(dir)) || homeWorkspace();
2672
+ const start = t.start && isDir(t.start) ? path.resolve(t.start) : null;
2673
+ // Where it belongs: the workspace containing the directory the pane STARTED in, then the one containing where it is
2674
+ // now. Only when neither is registered is a directory registered — the start directory first, because that is a
2675
+ // folder a session was opened in, not wherever the PM last `cd`-ed (registering that made ~ or /tmp a permanent
2676
+ // workspace). The current directory is the last resort, kept because a pane the PM can see must be one they can
2677
+ // reach; the home workspace is assumed only when there is no readable directory at all.
2678
+ const w = workspaceForPath(start) || workspaceForPath(dir) || (start && registerWorkspace(start))
2679
+ || (dir && registerWorkspace(dir)) || homeWorkspace();
2676
2680
  if (!w) continue;
2677
2681
  const id = nextSessionId();
2678
2682
  // The agent is the pane's own start command, not a guess: tmux spawned it directly (see
@@ -2680,7 +2684,7 @@ function adoptOrphanTmuxSessions() {
2680
2684
  const agent = adoptedAgent(t.cmd);
2681
2685
  const s = {
2682
2686
  id, ws: w.id, root: w.root, name: `recovered ${agent}`, agent,
2683
- origin: null, cwd: dir && insideRoot(w.root, dir) ? dir : w.root,
2687
+ origin: t.origin || null, cwd: dir && insideRoot(w.root, dir) ? dir : w.root,
2684
2688
  state: 'working', pty: null, tmux: t.name, note: '', buf: '',
2685
2689
  cols: 80, rows: 24, clients: new Set(), created: nowISO(), recovered: true,
2686
2690
  };
@@ -3115,6 +3119,10 @@ function tmuxList() {
3115
3119
  .map((name) => ({
3116
3120
  name,
3117
3121
  dir: tmuxField(name, '#{pane_current_path}'),
3122
+ // Where the pane STARTED (the -c of new-session): the workspace it was opened in. The current path follows every
3123
+ // `cd`, so a pane the PM moved to ~ or /tmp would otherwise file itself under that folder.
3124
+ start: tmuxField(name, '#{pane_start_path}'),
3125
+ origin: tmuxField(name, '#{@pmos_origin}'),
3118
3126
  created: Number(tmuxField(name, '#{session_created}')) || 0,
3119
3127
  cmd: tmuxField(name, '#{pane_start_command}'),
3120
3128
  }));
@@ -3461,6 +3469,9 @@ function spawnSession({ name, agent, cwd, ws, origin, initialInput, profile, ini
3461
3469
  s.buf = `\x1b[38;5;203m✗ ${why}\x1b[0m\r\n\x1b[38;5;244mFix the above, then ↻ respawn.\x1b[0m\r\n`;
3462
3470
  persist(s); pushSessions(); pushWorkspaces(); return s;
3463
3471
  }
3472
+ // The origin rides on the tmux session itself (a user option), so a pane whose record is lost is adopted with it:
3473
+ // a recovered automatic job stays automatic and the sweep can still end it. One cheap call, only when there is one.
3474
+ if (origin) tmuxCtl(['set-option', '-t', tname, '@pmos_origin', String(origin)]);
3464
3475
  announceStateHealth(s); // the unmigrated notice comes FIRST — it explains an empty skills list
3465
3476
  announceTree(s); // before attach, so the advisory is the first line of the pane's record
3466
3477
  announceModel(s);
@@ -3990,9 +4001,19 @@ function startDayCheck(run, reason, now) {
3990
4001
  if (run.checks.session_id) run = dayRunLib.sessionEnded(run, run.checks.session_id, now).run;
3991
4002
  const sk = daySkillPrompt(run, 'day-check');
3992
4003
  if (sk.error) return { ok: false, error: sk.error };
3993
- const prompt = `${sk.prompt}\n\nThis run: /day-check ${reason} — the last check was at ${run.checks.last || 'none yet'}; read what changed since ${run.checks.last || run.started_at}.`;
3994
- const s = spawnSession({ name: 'Daytime check', agent: sk.task.agent || 'claude', ws: sk.w ? sk.w.id : run.ws, origin: 'day-check', initialInput: prompt + '\r',
3995
- modelAsk: { scheduleTier: null, skillTier: sk.task.tier || null, skill: sk.task.skill } });
4004
+ const line = `This run: /day-check ${reason} — the last check was at ${run.checks.last || 'none yet'}; read what changed since ${run.checks.last || run.started_at}.`;
4005
+ // The last check's session is used again while it is open and quiet: it has the skill in its context, so only this
4006
+ // run's line is typed into it. Before, every check started a new session and the old one sat idle until the sweep
4007
+ // ended it, about one more process per interval (workspace-session-lifecycle).
4008
+ const prev = run.checks.prev_session && sessions.get(run.checks.prev_session);
4009
+ let s = null;
4010
+ if (prev && prev.pty && prev.state !== 'exited' && harness.deriveState(prev.state, prev.lastOutputAt, now).state !== 'working') {
4011
+ try { firstInput.deliver(prev, line + '\r'); s = prev; } catch { s = null; }
4012
+ }
4013
+ if (!s) {
4014
+ s = spawnSession({ name: 'Daytime check', agent: sk.task.agent || 'claude', ws: sk.w ? sk.w.id : run.ws, origin: 'day-check', initialInput: `${sk.prompt}\n\n${line}\r`,
4015
+ modelAsk: { scheduleTier: null, skillTier: sk.task.tier || null, skill: sk.task.skill } });
4016
+ }
3996
4017
  if (s.state === 'exited') return { ok: false, error: s.note || 'the check session could not start' };
3997
4018
  const c = dayRunLib.checkStarted(run, s.id, reason, now);
3998
4019
  return c.ok ? { ok: true, run: c.run, session: s.id } : c;
@@ -9013,7 +9034,7 @@ if (require.main === module) {
9013
9034
  });
9014
9035
  });
9015
9036
  }
9016
- module.exports = { server, online: { ONLINE, applyOnline, tunnel, publicHost }, spawnSession, removeSession, sweepAutomaticSessions, endSupersededRuns, renameSession, sessions, libraryTree, readTextDoc,
9037
+ module.exports = { server, online: { ONLINE, applyOnline, tunnel, publicHost }, spawnSession, removeSession, sweepAutomaticSessions, endSupersededRuns, adoptOrphanTmuxSessions, renameSession, sessions, libraryTree, readTextDoc,
9017
9038
  registerWorkspace, forgetWorkspace, workspaces, library, initiativeIndex, fsList,
9018
9039
  // R1 — exported so the suite can assert the CARRY-THROUGH: branchInfo separates an ordinary
9019
9040
  // non-repo folder from a real git fault, and the whole legibility defect was that distinction