create-pmos 0.126.0 → 0.128.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.
@@ -639,7 +639,9 @@ function repoIdentity(root) {
639
639
  // a fault: a folder registered by path legitimately has no origin, and null is the honest answer.
640
640
  try { remote = gitInfo.originUrl(root); } catch { remote = null; }
641
641
  try {
642
- const bi = gitInfo.branchInfo(root);
642
+ // headInfo, not branchInfo: only the branch name is used here, and branchInfo's branch list,
643
+ // `git status` and worktree list cost ~5 more shell-outs per workspace per TTL (session lifecycle).
644
+ const bi = gitInfo.headInfo(root, tree || null);
643
645
  if (bi.ok) branch = bi.detached ? null : bi.current;
644
646
  // R1/A2 — carry the CODE and the MESSAGE, not a boolean. branchInfo already separates the two
645
647
  // failures and says so in its own comment: `no-git` is an ordinary folder and "deserves no
@@ -2387,6 +2389,23 @@ function reportToMarkdown(surface) {
2387
2389
  }
2388
2390
  // Fire one schedule: run its skill → contract → deliverable → stamp → file to needs-you. Same path
2389
2391
  // for a scheduled tick and for run-now (R7). Pure of PTYs in v1 (deterministic report skill).
2392
+ // A schedule that fires again while its previous run is still open ends that run first, unless it is still producing
2393
+ // output or holds a call waiting for the PM. Before, every fire added a session beside the last one, and each stayed
2394
+ // up to the idle limit (workspace-session-lifecycle). Only automatic origins are ever touched.
2395
+ function endSupersededRuns(origin, now = Date.now()) {
2396
+ const ended = [];
2397
+ if (!sessionOrigin.isAutomatic(origin)) return ended;
2398
+ for (const s of sessions.values()) {
2399
+ if (s.origin !== origin || !s.pty || s.autoDelete === 'never') continue;
2400
+ if (harness.deriveState(s.state, s.lastOutputAt, now).state === 'working') continue;
2401
+ if (sessionShown(s, now) === 'blocked') continue;
2402
+ s.autoEnded = true; s.note = 'ended: the schedule started a new run';
2403
+ if (s.tmux) tmuxKillSession(s.tmux);
2404
+ try { s.pty.kill(); } catch {}
2405
+ ended.push(s.id);
2406
+ }
2407
+ return ended;
2408
+ }
2390
2409
  function fireSchedule(sched) {
2391
2410
  const w = workspaces.get(sched.ws) || homeWorkspace();
2392
2411
  const root = w ? w.root : SELF_ROOT;
@@ -2412,6 +2431,7 @@ function fireSchedule(sched) {
2412
2431
  // The schedule's recorded profile travels with the process it creates (profiles R8.3). Only an
2413
2432
  // AGENT-kind task has a PTY, which is why `profile_applies` is false for the skill-kind rows
2414
2433
  // below rather than the control simply being hidden.
2434
+ endSupersededRuns('schedule:' + sched.id);
2415
2435
  const s = spawnSession({ name: sched.name, agent: task.agent || 'claude', ws: sched.ws,
2416
2436
  origin: 'schedule:' + sched.id, initialInput: prompt ? prompt + '\r' : null,
2417
2437
  profile: sched.profile == null ? null : sched.profile,
@@ -2502,6 +2522,8 @@ function recordConversation(s) {
2502
2522
  }
2503
2523
  function persist(s) {
2504
2524
  if (s.state === 'exited' && !s._txSaved) { s._txSaved = true; try { recordConversation(s); } catch {} }
2525
+ // When it ended: the clock the sweep's keepExitedDays reads. A live session has none (a respawn clears it).
2526
+ if (s.state === 'exited') { if (!s.exitedAt) s.exitedAt = nowISO(); } else if (s.exitedAt) delete s.exitedAt;
2505
2527
  else if (s.state !== 'exited') s._txSaved = false;
2506
2528
  // note IS persisted — a restored session must still say why it exited. `root` is persisted
2507
2529
  // so a session survives even if the workspace registry is lost (it re-registers its root).
@@ -2509,6 +2531,7 @@ function persist(s) {
2509
2531
  state: s.state, note: s.note || '', created: s.created, tmux: s.tmux || null, origin: s.origin || null };
2510
2532
  if (s.autoDelete) meta.autoDelete = s.autoDelete;
2511
2533
  if (s.autoEnded) meta.autoEnded = true;
2534
+ if (s.exitedAt) meta.exitedAt = s.exitedAt;
2512
2535
  // The breakdown item a Hand to agent session is bound to, so a restart keeps the binding (pm-os-absorb Run 1).
2513
2536
  if (s.plan_item) meta.plan_item = s.plan_item;
2514
2537
  // pm-os-absorb Run 2 (R25): the SHA-256 of the agent's PMOS_SESSION token, never the token (D08). A restored session
@@ -2601,6 +2624,8 @@ function restorePersisted() {
2601
2624
  } else {
2602
2625
  s.state = 'exited';
2603
2626
  s.note = meta.note || 'restored after server restart — scrollback preserved; ↻ respawn to start a new process';
2627
+ // A record from before exitedAt existed: its last write is when it last changed, the closest truthful clock.
2628
+ if (!s.exitedAt) { try { s.exitedAt = new Date(fs.statSync(path.join(SESS_DIR, f)).mtimeMs).toISOString(); } catch {} }
2604
2629
  }
2605
2630
  const n = Number(String(meta.id).split('-')[1]); if (Number.isFinite(n) && n >= seq) seq = n + 1;
2606
2631
  } catch {}
@@ -2897,7 +2922,9 @@ function sweepAutomaticSessions(now = Date.now()) {
2897
2922
  const policy = readSessionPolicy();
2898
2923
  const done = { ended: [], removed: [] };
2899
2924
  for (const s of [...sessions.values()]) {
2900
- const act = sessionOrigin.decide({ ...s, shown: sessionShown(s, now) }, policy, now);
2925
+ // `shown` is a function: decide() asks for it only for a live automatic session past its idle limit, so the sweep no
2926
+ // longer resolves a transcript (a `ps` walk for a shell pane) for every session every 30 seconds.
2927
+ const act = sessionOrigin.decide({ ...s, shown: () => sessionShown(s, now) }, policy, now);
2901
2928
  if (act === 'end' && s.pty) {
2902
2929
  s.autoEnded = true; s.note = 'ended: idle too long (automatic session)';
2903
2930
  if (s.tmux) tmuxKillSession(s.tmux);
@@ -8986,7 +9013,7 @@ if (require.main === module) {
8986
9013
  });
8987
9014
  });
8988
9015
  }
8989
- module.exports = { server, online: { ONLINE, applyOnline, tunnel, publicHost }, spawnSession, removeSession, renameSession, sessions, libraryTree, readTextDoc,
9016
+ module.exports = { server, online: { ONLINE, applyOnline, tunnel, publicHost }, spawnSession, removeSession, sweepAutomaticSessions, endSupersededRuns, renameSession, sessions, libraryTree, readTextDoc,
8990
9017
  registerWorkspace, forgetWorkspace, workspaces, library, initiativeIndex, fsList,
8991
9018
  // R1 — exported so the suite can assert the CARRY-THROUGH: branchInfo separates an ordinary
8992
9019
  // non-repo folder from a real git fault, and the whole legibility defect was that distinction
@@ -53,6 +53,7 @@
53
53
  "learn.test.js",
54
54
  "session-flag.test.js",
55
55
  "session-origin.test.js",
56
+ "session-lifecycle.test.js",
56
57
  "tab-gone.test.js",
57
58
  "live-notes.test.js",
58
59
  "plan-items.test.js",
@@ -172,6 +173,7 @@
172
173
  "learn.test.js": 3,
173
174
  "session-flag.test.js": 1,
174
175
  "session-origin.test.js": 1,
176
+ "session-lifecycle.test.js": 2,
175
177
  "tab-gone.test.js": 1,
176
178
  "live-notes.test.js": 3,
177
179
  "pm-shell-test.js": 6,
@@ -0,0 +1,6 @@
1
+ {
2
+ "_comment": "console3 Reports dashboards: the server half (library, defaults, scope filter, layout store, page-press layout route) and the browser suite (scopes, edit, share, library, Drafts tab).",
3
+ "modules": ["report-dashboards.test.js"],
4
+ "panels": ["c3-reports-test.js"],
5
+ "seconds": { "report-dashboards.test.js": 3, "c3-reports-test.js": 30 }
6
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "_comment": "local-test-kit: the demo-data builder and the launcher script of scripts/local-kit/. Starts no server and binds no port.",
3
+ "modules": ["local-kit.test.js"],
4
+ "seconds": { "local-kit.test.js": 6 }
5
+ }