claude-code-session-manager 0.56.0 → 0.57.1

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.
@@ -42,13 +42,14 @@ const PERSONA_NAME_RE = /^[a-z][a-z0-9-]*$/;
42
42
  * assign/remove the relationship from either side. Stored the same
43
43
  * comma-list way as `tools` for one parser to cover both.
44
44
  */
45
- function serializePersona({ name, description, tools, model, color, tags, body }) {
45
+ function serializePersona({ name, description, tools, model, color, tags, title, body }) {
46
46
  const lines = ['---', `name: ${name}`];
47
47
  if (description) lines.push(`description: ${description}`);
48
48
  if (tools && tools.length) lines.push(`tools: ${tools.join(', ')}`);
49
49
  if (model && model !== 'inherit') lines.push(`model: ${model}`);
50
50
  if (color) lines.push(`color: ${color}`);
51
51
  if (tags && tags.length) lines.push(`tags: ${tags.join(', ')}`);
52
+ if (title) lines.push(`title: ${title}`);
52
53
  lines.push('---', '');
53
54
  return lines.join('\n') + (body || '').trim() + '\n';
54
55
  }
@@ -66,6 +67,7 @@ async function savePersona({
66
67
  model,
67
68
  color,
68
69
  tags,
70
+ title,
69
71
  body,
70
72
  globalDir = path.join(os.homedir(), '.claude', 'agents'),
71
73
  validatePath = configMgr.validatePath,
@@ -75,7 +77,7 @@ async function savePersona({
75
77
  throw new Error('agent name must be lowercase, hyphenated (e.g. "my-agent")');
76
78
  }
77
79
  const target = validatePath(path.join(globalDir, `${name}.md`));
78
- const text = serializePersona({ name, description, tools, model, color, tags, body });
80
+ const text = serializePersona({ name, description, tools, model, color, tags, title, body });
79
81
  await writeTextAtomic(target, text);
80
82
  if (originalName && originalName !== name) {
81
83
  const oldReal = validatePath(path.join(globalDir, `${originalName}.md`));
@@ -201,6 +203,7 @@ async function listPersonas({
201
203
  model: fm.model || null,
202
204
  color: fm.color || null,
203
205
  tags: parseTools(fm.tags),
206
+ title: fm.title || null,
204
207
  path: real,
205
208
  body: body.trim(),
206
209
  overridingProjects,
@@ -136,14 +136,6 @@ function validateWrite(realAbs) {
136
136
  if (realAbs === browserSub || realAbs.startsWith(browserSub + path.sep)) {
137
137
  return;
138
138
  }
139
- // Feedback inbox writes (scheduler RCA hook — rcaFeedbackHook.cjs — and
140
- // any interactive feedback filing): narrowly scoped to
141
- // session-manager-operations/feedback/, this repo's existing
142
- // per-project intake convention.
143
- const feedbackSub = path.join(realRoot, 'session-manager-operations', 'feedback');
144
- if (realAbs === feedbackSub || realAbs.startsWith(feedbackSub + path.sep)) {
145
- return;
146
- }
147
139
  // PromptSession persistence (active-index.json + per-session archives,
148
140
  // promptSessions.ts) and the scheduler's own read-modify-write of that
149
141
  // same active index (promptSessionEvents.cjs, PRD 814) — narrowly
@@ -215,6 +215,12 @@ function withPathLock(lockPath, task) {
215
215
  */
216
216
  function ensureEpic(cwd, { goalText, tag, reuseByGoal = false, epicId: explicitEpicId, status = 'proposed', openingPrompt = null, mintIfMissing = true, source = null, forceNewEpic = false } = {}) {
217
217
  if (!cwd || typeof cwd !== 'string') throw new Error('ensureEpic: cwd is required');
218
+ // A relative cwd (e.g. a caller passing '.') would otherwise get stored
219
+ // verbatim on the minted Epic's `cwd` field — the renderer's EpicsWorkspace
220
+ // matches Epics to a tab by exact `s.cwd === effectiveCwd` string equality
221
+ // against the tab's absolute path, so a relative value silently produces an
222
+ // Epic that exists on disk but never renders in any project's queue.
223
+ cwd = path.resolve(cwd);
218
224
  return withPathLock(activeIndexPath(cwd), () => {
219
225
  const index = readActiveIndex(cwd);
220
226
 
@@ -48,8 +48,6 @@ const OWNERS = Object.freeze({
48
48
  'scheduler': 'scheduler',
49
49
  // Project Home owns the synthesized Brief (generate + hand-edit).
50
50
  'project-brief': 'project-home',
51
- // The feedback intake folder — written by the RCA hook and feedback filing.
52
- 'feedback': 'feedback',
53
51
  // Browser tab scratch saves (DOM captures, screenshots, recorded flows).
54
52
  'browser': 'browser',
55
53
  // Structured per-tab error log lines (JSONL), tagged for tracing/analysis.
@@ -21,10 +21,22 @@ const { projectHistoryPath, stateCwds } = require('./queueStore.cjs');
21
21
  // resolved via queueStore.cjs). Old rows stay readable from here; new appends
22
22
  // go to the owning project's shard (entries without a cwd still land here so
23
23
  // nothing is ever dropped).
24
- const HISTORY_PATH = path.join(os.homedir(), '.claude', 'session-manager', 'scheduled-plans', 'history.jsonl');
24
+ //
25
+ // Overridable via SM_HISTORY_PATH_OVERRIDE so queueHistory.test.cjs can point
26
+ // this at an isolated tmpdir path instead of racing a live Electron
27
+ // instance's scheduler.cjs, which writes this same machine-global file.
28
+ const HISTORY_PATH = process.env.SM_HISTORY_PATH_OVERRIDE
29
+ || path.join(os.homedir(), '.claude', 'session-manager', 'scheduled-plans', 'history.jsonl');
25
30
 
26
- /** Every history file currently in play: each project's shard + the legacy global. */
31
+ /**
32
+ * Every history file currently in play: each project's shard + the legacy
33
+ * global. Under SM_HISTORY_PATH_OVERRIDE (queueHistory.test.cjs), skip the
34
+ * federated per-project shards entirely — they're real files under whatever
35
+ * projects exist on this machine's ~/.claude/projects, so including them
36
+ * would leak real production history rows into an otherwise-isolated test.
37
+ */
27
38
  function historyPaths() {
39
+ if (process.env.SM_HISTORY_PATH_OVERRIDE) return [HISTORY_PATH];
28
40
  const paths = [];
29
41
  for (const cwd of stateCwds()) {
30
42
  try { paths.push(projectHistoryPath(cwd)); } catch { /* unusable cwd */ }
@@ -1138,11 +1138,26 @@ async function reconcile(state) {
1138
1138
 
1139
1139
  const next = [];
1140
1140
  const seen = new Set();
1141
+ // Terminal (completed/failed) jobs whose .md vanished from disk BEFORE
1142
+ // their row ever crossed HISTORY_RETENTION_MS in partitionJobs — i.e. the
1143
+ // common case, since archiveCompletedPrd() (called right when a job
1144
+ // finishes, scheduler.cjs's `newlyCompletedPrds` handling) moves the PRD
1145
+ // file immediately, while partitionJobs' appendHistory only fires after 7
1146
+ // days. Bug found 2026-08-02: the old code below dropped these rows on the
1147
+ // very next reconcile() pass with NO history write at all — the file-gone
1148
+ // check couldn't tell "already recorded to history.jsonl on a prior pass"
1149
+ // apart from "never recorded anywhere", so a job that completed and had
1150
+ // its file auto-archived within minutes lost its history entirely. Both
1151
+ // Queue and History tabs then read as empty even for projects with real,
1152
+ // successfully-completed work. Collected here; reconciled against
1153
+ // historyTerminalBySlug() below and backfilled before being dropped.
1154
+ const terminalDroppedNeedingHistoryCheck = [];
1141
1155
  for (const job of state.jobs) {
1142
1156
  const p = onDisk.get(job.slug);
1143
1157
  if (!p) {
1144
1158
  // A terminal job whose .md is gone was archived on purpose — dropping
1145
- // its row is the intended end of the auto-archive flow.
1159
+ // its row is the intended end of the auto-archive flow, PROVIDED it's
1160
+ // already durably recorded in history.jsonl (checked below).
1146
1161
  //
1147
1162
  // A PENDING or RUNNING job whose .md is merely not VISIBLE is a
1148
1163
  // different thing entirely, and dropping it destroys queued work: the
@@ -1163,6 +1178,8 @@ async function reconcile(state) {
1163
1178
  seen.add(job.slug);
1164
1179
  next.push({ ...job });
1165
1180
  console.warn(`[scheduler] reconcile: keeping ${job.status} job ${job.slug} — PRD source not visible in any candidate dir`);
1181
+ } else if (job.status === 'completed' || job.status === 'failed') {
1182
+ terminalDroppedNeedingHistoryCheck.push(job);
1166
1183
  }
1167
1184
  continue;
1168
1185
  }
@@ -1198,10 +1215,24 @@ async function reconcile(state) {
1198
1215
  for (const [slug] of onDisk) {
1199
1216
  if (!seen.has(slug)) unmatchedSlugs.push(slug);
1200
1217
  }
1201
- const historyBySlug = unmatchedSlugs.length > 0
1218
+ const historyBySlug = (unmatchedSlugs.length > 0 || terminalDroppedNeedingHistoryCheck.length > 0)
1202
1219
  ? await queueHistory.historyTerminalBySlug()
1203
1220
  : new Map();
1204
1221
 
1222
+ // Backfill: any terminal job dropped above whose slug isn't already in
1223
+ // history.jsonl gets written now, before its row is gone for good. This is
1224
+ // the fix for the bug described where the loop collects
1225
+ // terminalDroppedNeedingHistoryCheck — without it, a job whose PRD file was
1226
+ // auto-archived immediately on completion (the normal case) is lost from
1227
+ // both jobs[] and history.jsonl in the same reconcile pass.
1228
+ if (terminalDroppedNeedingHistoryCheck.length > 0) {
1229
+ const missing = terminalDroppedNeedingHistoryCheck.filter((j) => !historyBySlug.has(j.slug));
1230
+ if (missing.length > 0) {
1231
+ await queueHistory.appendHistory(missing);
1232
+ console.log(`[scheduler] reconcile: backfilled ${missing.length} completed/failed job(s) into history.jsonl (PRD file already archived, row about to drop)`);
1233
+ }
1234
+ }
1235
+
1205
1236
  // Terminal-in-history slugs whose .md file is still on disk: fed into the
1206
1237
  // auto-archive selection pass below (as synthetic completed entries) so
1207
1238
  // their file can still be swept, without ever creating a live job row
@@ -133,7 +133,7 @@ export interface ImportRef {
133
133
  /** Declared owner ids for the single-writer law over a project's
134
134
  * session-manager-operations/ root. Mirrors OWNERS in
135
135
  * src/main/lib/opsOwnership.cjs — one writer per namespace; everyone reads. */
136
- export type OpsWriter = 'epics' | 'scheduler' | 'project-home' | 'feedback' | 'browser';
136
+ export type OpsWriter = 'epics' | 'scheduler' | 'project-home' | 'browser';
137
137
 
138
138
  export interface WriteResult {
139
139
  ok: boolean;
@@ -267,6 +267,9 @@ export interface AgentPersona {
267
267
  color: string | null;
268
268
  /** Epic intent tags (tagLibrary.ts's TAG_LIBRARY) this persona is associated with. */
269
269
  tags: string[];
270
+ /** "<Department> — <Human title>" (e.g. "Engineering — Architect"), free text — drives
271
+ * department grouping in the New Epic Agent picker. Optional/null when not set. */
272
+ title?: string | null;
270
273
  path: string;
271
274
  body: string;
272
275
  /** Project names (basename of cwd) whose currently-open tab overlays this agent. */
@@ -283,6 +286,7 @@ export interface AgentPersonaSaveInput {
283
286
  model: string;
284
287
  color: string;
285
288
  tags: string[];
289
+ title?: string;
286
290
  body: string;
287
291
  }
288
292