claude-code-session-manager 0.95.0 → 0.97.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.
Files changed (114) hide show
  1. package/dist/assets/{DataModel-DZK9QZpV.js → DataModel-D93FoaOm.js} +1 -1
  2. package/dist/assets/History-CXxxqNP1.js +2 -0
  3. package/dist/assets/{Hooks-CmK6l0uf.js → Hooks-CzeYFQjq.js} +3 -3
  4. package/dist/assets/{HostBilko-D8TIx-Bx.js → HostBilko-nESrTRIg.js} +1 -1
  5. package/dist/assets/{Library-DTxpOjx8.js → Library-D2zPkwOC.js} +1 -1
  6. package/dist/assets/{MarkdownEditor-BPJyrevq.js → MarkdownEditor-DbinP78A.js} +1 -1
  7. package/dist/assets/{McpServers-B_rvIYDs.js → McpServers-Bu_sVGLr.js} +2 -2
  8. package/dist/assets/Memory-BgiI6LZ_.js +8 -0
  9. package/dist/assets/{Permissions-DhMMp27n.js → Permissions-C1MMzZRm.js} +3 -3
  10. package/dist/assets/Plugins-Dhu6wAZi.js +2 -0
  11. package/dist/assets/{ProvenanceBadge-ptlItdCK.js → ProvenanceBadge-C3AcWKMG.js} +1 -1
  12. package/dist/assets/{SaveBar-CSbNSl2W.js → SaveBar-plwYm0iO.js} +1 -1
  13. package/dist/assets/Scheduler-BVYTeh39.js +16 -0
  14. package/dist/assets/{ScopeSwitcher-BJof-GWH.js → ScopeSwitcher-DFy0vv_o.js} +1 -1
  15. package/dist/assets/Settings-DIhGgdfs.js +3 -0
  16. package/dist/assets/{SkillReferenceGraph-CGl4B89-.js → SkillReferenceGraph-DsI7hRPB.js} +1 -1
  17. package/dist/assets/Skills-3E748UfB.js +3 -0
  18. package/dist/assets/{SystemPrompt-DKj-q39J.js → SystemPrompt-BqmfUJOd.js} +1 -1
  19. package/dist/assets/TagLibrary-CPz6fO8Q.js +1 -0
  20. package/dist/assets/{TiptapBody-BbcJL9az.js → TiptapBody-D9EoBQ1P.js} +1 -1
  21. package/dist/assets/{Toggle-DAslvnnv.js → Toggle-BHAmZNDn.js} +1 -1
  22. package/dist/assets/index-tn5JIVPj.js +3079 -0
  23. package/dist/assets/{settingsSchema-pzp0TgvN.js → settingsSchema-Crqy_aNz.js} +1 -1
  24. package/dist/index.html +1 -1
  25. package/package.json +8 -2
  26. package/plugins/CLAUDE.md +3 -3
  27. package/plugins/session-manager-dev/skills/develop/SKILL.md +90 -67
  28. package/plugins/session-manager-dev/skills/develop/standards.md +5 -11
  29. package/plugins/session-manager-dev/skills/requesting-code-review/SKILL.md +5 -15
  30. package/scripts/README.md +1 -0
  31. package/scripts/hooks/guard-destructive-git.cjs +9 -374
  32. package/scripts/hooks/guard-inline-implementation.cjs +9 -128
  33. package/scripts/hooks/guard-prd-writes.cjs +9 -74
  34. package/scripts/hooks/guard-self-schedule.cjs +9 -61
  35. package/scripts/hooks/lib/guard-destructive-git-policy.cjs +410 -0
  36. package/scripts/hooks/lib/guard-inline-implementation-policy.cjs +159 -0
  37. package/scripts/hooks/lib/guard-prd-writes-policy.cjs +100 -0
  38. package/scripts/hooks/lib/guard-self-schedule-policy.cjs +89 -0
  39. package/scripts/scheduler-mcp-server.cjs +8 -1
  40. package/src/main/__tests__/configWriteBoundaryOwners.test.cjs +59 -0
  41. package/src/main/__tests__/exchanges.test.cjs +41 -96
  42. package/src/main/__tests__/exchangesPromptId.test.cjs +16 -25
  43. package/src/main/__tests__/files-reject-credentials.test.cjs +1 -1
  44. package/src/main/__tests__/health-credentials.test.cjs +81 -0
  45. package/src/main/__tests__/ipcSchemas-dependsOn.test.cjs +39 -0
  46. package/src/main/__tests__/memoryAggregate.test.cjs +40 -1
  47. package/src/main/__tests__/opsErrorLogTelemetryTap.test.cjs +1 -1
  48. package/src/main/__tests__/planValidator.test.cjs +125 -0
  49. package/src/main/__tests__/prdCreate.test.cjs +3 -1
  50. package/src/main/__tests__/prdSizing.test.cjs +106 -0
  51. package/src/main/__tests__/queue-starvation-dispatch-driver.test.cjs +46 -0
  52. package/src/main/__tests__/reconcileFlatPrdSweep.test.cjs +10 -0
  53. package/src/main/__tests__/scheduler-looks-done.test.cjs +40 -0
  54. package/src/main/__tests__/scheduler-notify-originating-tab.test.cjs +81 -0
  55. package/src/main/__tests__/scheduler-shard-quarantine.test.cjs +6 -1
  56. package/src/main/__tests__/schedulerStateSidecarRestore.test.cjs +110 -0
  57. package/src/main/__tests__/seedAgentPersonas.test.cjs +19 -1
  58. package/src/main/__tests__/seedValidatorPersona.test.cjs +116 -0
  59. package/src/main/__tests__/telemetryClient.test.cjs +2 -2
  60. package/src/main/__tests__/telemetryContract.test.cjs +2 -2
  61. package/src/main/__tests__/usageSingleFlight.test.cjs +10 -0
  62. package/src/main/__tests__/validationSentinels.test.cjs +84 -0
  63. package/src/main/build-info.json +4 -4
  64. package/src/main/config.cjs +26 -14
  65. package/src/main/exchanges.cjs +26 -21
  66. package/src/main/health.cjs +66 -4
  67. package/src/main/index.cjs +1 -1
  68. package/src/main/ipcSchemas.cjs +15 -7
  69. package/src/main/lib/__tests__/crashTelemetry.test.cjs +1 -1
  70. package/src/main/lib/__tests__/credentials-futile-refresh.test.cjs +115 -0
  71. package/src/main/lib/__tests__/epicWorktreeProjectConfig.test.cjs +85 -27
  72. package/src/main/lib/__tests__/gitWorktree.test.cjs +21 -27
  73. package/src/main/lib/__tests__/guardShims.test.cjs +10 -3
  74. package/src/main/lib/__tests__/loadGate.test.cjs +44 -1
  75. package/src/main/lib/__tests__/opsOwnership.test.cjs +14 -0
  76. package/src/main/lib/__tests__/telemetryBacklog.test.cjs +4 -4
  77. package/src/main/lib/__tests__/telemetryConsent.test.cjs +2 -2
  78. package/src/main/lib/__tests__/telemetryCountersMetadataColumn.test.cjs +1 -1
  79. package/src/main/lib/agentModelResolve.cjs +1 -0
  80. package/src/main/lib/credentials.cjs +62 -0
  81. package/src/main/lib/cwdClassify.cjs +15 -0
  82. package/src/main/lib/definitionOfDone.cjs +9 -13
  83. package/src/main/lib/effectiveModelInfo.cjs +2 -9
  84. package/src/main/lib/epicWorktreeProjectConfig.cjs +75 -31
  85. package/src/main/lib/loadGate.cjs +29 -5
  86. package/src/main/lib/opsOwnership.cjs +23 -0
  87. package/src/main/lib/planValidator.cjs +80 -0
  88. package/src/main/lib/prdCreate.cjs +8 -4
  89. package/src/main/lib/prdSizing.cjs +83 -0
  90. package/src/main/lib/runLogRetention.cjs +2 -8
  91. package/src/main/lib/telemetryBacklog.cjs +9 -3
  92. package/src/main/lib/telemetryClient.cjs +12 -3
  93. package/src/main/lib/telemetrySettings.cjs +8 -2
  94. package/src/main/lib/validationSentinels.cjs +29 -0
  95. package/src/main/memoryAggregate.cjs +36 -11
  96. package/src/main/otelSettings.cjs +8 -2
  97. package/src/main/scheduler.cjs +215 -31
  98. package/src/main/seedAgentPersonas.cjs +1 -1
  99. package/src/main/sessionsStore.cjs +8 -2
  100. package/src/main/supervisor.cjs +3 -3
  101. package/src/main/templates/PRD_AUTHORING.md +9 -9
  102. package/src/main/voiceSettings.cjs +8 -2
  103. package/src/preload/api.d.ts +3 -3
  104. package/src/preload/index.cjs +5 -5
  105. package/src/seed/agents/dev-lead.md +24 -15
  106. package/src/seed/agents/validator.md +33 -0
  107. package/dist/assets/History-BDyJUGtR.js +0 -2
  108. package/dist/assets/Memory-B-9GlHqe.js +0 -8
  109. package/dist/assets/Plugins-DKfAlmMY.js +0 -2
  110. package/dist/assets/Scheduler--rn1QBYc.js +0 -16
  111. package/dist/assets/Settings-Dsx6XZP5.js +0 -3
  112. package/dist/assets/Skills-D522Zfol.js +0 -3
  113. package/dist/assets/TagLibrary-BH7CG7-6.js +0 -1
  114. package/dist/assets/index-Yf8e7VCB.js +0 -3079
@@ -0,0 +1,83 @@
1
+ /**
2
+ * prdSizing.cjs — pure PRD-size warning rules (PRD 1403).
3
+ *
4
+ * The measured actual/estimate ratio across scheduler runs sits at 0.24 (p50) with 60% of
5
+ * jobs finishing in <=10 min, yet nothing at the authoring gate flags an oversized PRD before
6
+ * it lands. `sizingWarnings` is the single rule set; prdCreate.cjs's createPrd() surfaces its
7
+ * output in the create-prd response, and scheduler-mcp-server.cjs's scheduler_create_prd
8
+ * handler prints it as a "Sizing warnings:" block. Warnings never block a write.
9
+ */
10
+ 'use strict';
11
+
12
+ const SIZING_LIMITS = {
13
+ estimateMinutes: 15,
14
+ acLines: 8,
15
+ acLineChars: 400,
16
+ bodyChars: 7000,
17
+ };
18
+
19
+ // Open-ended search-and-fix shape, e.g. "grep the codebase and update every call site." Matches
20
+ // across sentence boundaries within one AC line ([\s\S]*, not [^.]*) — a natural two-sentence
21
+ // phrasing like "Search the codebase for X. Update every call site." is exactly the shape this
22
+ // rule exists to catch, and a period must not be enough to dodge it.
23
+ const OPEN_ENDED_RE = /\b(grep|search|scan|audit|find)\b[\s\S]*\b(and|then)\b[\s\S]*\b(update|fix|adjust|migrate|change)\b/i;
24
+ const TIMEOUT_CMD_RE = /\btimeout\s+\d+/gi;
25
+
26
+ /**
27
+ * Pure — no I/O. Returns one human-readable warning per triggered rule, `[]` when nothing
28
+ * is triggered. `input` mirrors createPrd()'s own input shape; missing/non-array fields are
29
+ * treated as empty/absent rather than thrown on, since this is advisory only.
30
+ */
31
+ function sizingWarnings(input) {
32
+ const { estimateMinutes, goal, implementationNotes } = input || {};
33
+ const acceptanceCriteria = Array.isArray(input?.acceptanceCriteria) ? input.acceptanceCriteria : [];
34
+ const acLines = acceptanceCriteria.filter((line) => typeof line === 'string');
35
+
36
+ const warnings = [];
37
+
38
+ if (typeof estimateMinutes === 'number' && estimateMinutes > SIZING_LIMITS.estimateMinutes) {
39
+ warnings.push(
40
+ `estimateMinutes (${estimateMinutes}) exceeds the ${SIZING_LIMITS.estimateMinutes}-minute sizing limit — split this PRD into smaller work items.`,
41
+ );
42
+ }
43
+
44
+ if (acceptanceCriteria.length > SIZING_LIMITS.acLines) {
45
+ warnings.push(
46
+ `acceptanceCriteria has ${acceptanceCriteria.length} lines, exceeding the ${SIZING_LIMITS.acLines}-line sizing limit — split into smaller, single-purpose PRDs.`,
47
+ );
48
+ }
49
+
50
+ if (acLines.some((line) => line.length > SIZING_LIMITS.acLineChars)) {
51
+ warnings.push(
52
+ `One or more acceptance criteria lines exceed ${SIZING_LIMITS.acLineChars} characters — break the line up into smaller, single-purpose criteria.`,
53
+ );
54
+ }
55
+
56
+ if (acLines.some((line) => OPEN_ENDED_RE.test(line))) {
57
+ warnings.push(
58
+ 'One or more acceptance criteria lines describe an open-ended search-and-fix task (e.g. "grep ... and update ...") — scope it to a concrete, bounded change instead.',
59
+ );
60
+ }
61
+
62
+ if (acLines.some((line) => {
63
+ const matches = line.match(TIMEOUT_CMD_RE);
64
+ return matches && matches.length >= 2;
65
+ })) {
66
+ warnings.push(
67
+ 'One or more acceptance criteria lines run two or more `timeout <n>` commands — split into separate criteria, one command each.',
68
+ );
69
+ }
70
+
71
+ const bodyChars = String(goal || '').length
72
+ + String(implementationNotes || '').length
73
+ + acLines.join('\n').length;
74
+ if (bodyChars > SIZING_LIMITS.bodyChars) {
75
+ warnings.push(
76
+ `Combined goal + implementation notes + acceptance criteria is ${bodyChars} characters, exceeding the ${SIZING_LIMITS.bodyChars}-character sizing limit — split into smaller PRDs.`,
77
+ );
78
+ }
79
+
80
+ return warnings;
81
+ }
82
+
83
+ module.exports = { SIZING_LIMITS, sizingWarnings };
@@ -43,13 +43,8 @@
43
43
  */
44
44
 
45
45
  const fs = require('node:fs');
46
- const os = require('node:os');
47
46
  const path = require('node:path');
48
-
49
- const DEFAULT_RUNS_DIR = path.join(
50
- os.homedir(),
51
- '.claude', 'session-manager', 'scheduled-plans', 'runs'
52
- );
47
+ const schedulerPaths = require('./schedulerPaths.cjs');
53
48
 
54
49
  // Any status that is not yet a terminal outcome. Mirrors the status literals
55
50
  // used throughout scheduler.cjs (see e.g. its DOD_SLUG_RE-adjacent status
@@ -436,14 +431,13 @@ function applyRetention(runsDir, settings, opts) {
436
431
  * startup timer, alongside finalizeClosedDays) — never on its own timer.
437
432
  */
438
433
  function runBootSweep(opts) {
439
- const runsDir = (opts && opts.runsDir) || DEFAULT_RUNS_DIR;
434
+ const runsDir = (opts && opts.runsDir) || schedulerPaths.runsDir();
440
435
  const queueStore = require('./queueStore.cjs');
441
436
  const state = queueStore.readMergedSync();
442
437
  return applyRetention(runsDir, state.config || {}, { jobs: state.jobs || [] });
443
438
  }
444
439
 
445
440
  module.exports = {
446
- DEFAULT_RUNS_DIR,
447
441
  LIVE_STATUSES,
448
442
  isLiveJob,
449
443
  liveKeysFromJobs,
@@ -21,7 +21,7 @@
21
21
  * a conservative, delivery-confirmed offset) at once — see the two-phase
22
22
  * design below.
23
23
  *
24
- * TWO-PHASE WATERMARK. Per file, ~/.config/session-manager/telemetry-
24
+ * TWO-PHASE WATERMARK. Per file, ~/.claude/session-manager/telemetry-
25
25
  * watermarks.json tracks:
26
26
  * - bytesEnqueued: how far we've read + handed lines to telemetryClient.
27
27
  * Advances the instant a line has been processed
@@ -85,8 +85,14 @@ function lastRunSummary() {
85
85
  return lastSummary ? { ...lastSummary } : null;
86
86
  }
87
87
 
88
+ let migrated = false;
88
89
  function watermarksPath() {
89
- return path.join(os.homedir(), '.config', 'session-manager', 'telemetry-watermarks.json');
90
+ const p = path.join(os.homedir(), '.claude', 'session-manager', 'telemetry-watermarks.json');
91
+ if (!migrated) {
92
+ migrated = true;
93
+ config.migrateLegacyHomeFile(path.join(os.homedir(), '.config', 'session-manager', 'telemetry-watermarks.json'), p);
94
+ }
95
+ return p;
90
96
  }
91
97
 
92
98
  function resolveDeps(deps = {}) {
@@ -154,7 +160,7 @@ function entryFor(wm, key) {
154
160
 
155
161
  /**
156
162
  * Known projects come from the same source of truth the renderer restores
157
- * tabs from (sessionsStore.cjs's ~/.config/session-manager/tabs.json — TAB =
163
+ * tabs from (sessionsStore.cjs's ~/.claude/session-manager/tabs.json — TAB =
158
164
  * cwd = Main Project), deduped by normalized cwd, with ephemeral (worktree /
159
165
  * tmpdir) cwds excluded before normalization even runs, since a worktree cwd
160
166
  * would otherwise resolve back to its real project and be scanned twice
@@ -7,7 +7,7 @@
7
7
  * through the same queue/dedup/backoff machinery but sent one record at a
8
8
  * time via sendSingle() rather than sendBatch().
9
9
  *
10
- * Records accumulate durably in ~/.config/session-manager/telemetry-queue.jsonl
10
+ * Records accumulate durably in ~/.claude/session-manager/telemetry-queue.jsonl
11
11
  * the instant they're accepted, and are only ever sent by flush(reason) — on
12
12
  * a deliberate cadence (boot / daily / version-change / quit / manual), never
13
13
  * on a short interval. Every record is idempotent by recordId and stamped at
@@ -111,12 +111,21 @@ function logWarn(message, meta) {
111
111
 
112
112
  /**
113
113
  * SM_TELEMETRY_SPOOL, when set, overrides the spool directory in place of
114
- * `~/.config/session-manager` — the explicit opt-in a test that genuinely
114
+ * `~/.claude/session-manager` — the explicit opt-in a test that genuinely
115
115
  * needs to exercise real queue/sent-file I/O uses to prove it isn't about to
116
116
  * write into a real user's spool (see isTestEnvironment() below).
117
117
  */
118
+ let spoolMigrated = false;
118
119
  function spoolDir() {
119
- return process.env.SM_TELEMETRY_SPOOL || path.join(os.homedir(), '.config', 'session-manager');
120
+ if (process.env.SM_TELEMETRY_SPOOL) return process.env.SM_TELEMETRY_SPOOL;
121
+ const dir = path.join(os.homedir(), '.claude', 'session-manager');
122
+ if (!spoolMigrated) {
123
+ spoolMigrated = true;
124
+ const oldDir = path.join(os.homedir(), '.config', 'session-manager');
125
+ config.migrateLegacyHomeFile(path.join(oldDir, 'telemetry-queue.jsonl'), path.join(dir, 'telemetry-queue.jsonl'));
126
+ config.migrateLegacyHomeFile(path.join(oldDir, 'telemetry-sent.json'), path.join(dir, 'telemetry-sent.json'));
127
+ }
128
+ return dir;
120
129
  }
121
130
 
122
131
  function queuePath() {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * telemetrySettings — persists product-telemetry consent/config + install identity.
3
3
  *
4
- * Storage: ~/.config/session-manager/telemetry.json
4
+ * Storage: ~/.claude/session-manager/telemetry.json
5
5
  * Shape: {
6
6
  * enabled: boolean, // ON by default — hard kill switch is SM_TELEMETRY=0
7
7
  * installId: string, // crypto.randomUUID(), minted once, the only identity
@@ -45,8 +45,14 @@ const DEFAULTS = Object.freeze({
45
45
 
46
46
  const KNOWN_KEYS = new Set(Object.keys(DEFAULTS));
47
47
 
48
+ let migrated = false;
48
49
  function storePath() {
49
- return path.join(os.homedir(), '.config', 'session-manager', 'telemetry.json');
50
+ const p = path.join(os.homedir(), '.claude', 'session-manager', 'telemetry.json');
51
+ if (!migrated) {
52
+ migrated = true;
53
+ config.migrateLegacyHomeFile(path.join(os.homedir(), '.config', 'session-manager', 'telemetry.json'), p);
54
+ }
55
+ return p;
50
56
  }
51
57
 
52
58
  function isValid(cfg) {
@@ -0,0 +1,29 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * validationSentinels.cjs — parses the `VALIDATION: <slug> VERIFIED` /
5
+ * `VALIDATION: <slug> REFUTED — <reason>` lines a plan-level validator job
6
+ * (persona `validator`, PRD 1405) emits, one per PRD it checked, at the end
7
+ * of its run. Pure text-in/array-out; no filesystem or scheduler knowledge.
8
+ */
9
+
10
+ const SENTINEL_RE = /^VALIDATION:\s+(\S+)\s+(VERIFIED|REFUTED)(?:\s*[—–-]+\s*(.*))?\s*$/gm;
11
+
12
+ /**
13
+ * parseValidationSentinels(text) → Array<{ slug, verdict, reason }>
14
+ *
15
+ * `verdict` is lowercased ('verified' | 'refuted'); `reason` is '' when
16
+ * absent. Last matching line per slug wins, but the ORDER of the returned
17
+ * array follows each slug's first appearance in `text`, not its last.
18
+ */
19
+ function parseValidationSentinels(text) {
20
+ if (!text) return [];
21
+ const bySlug = new Map();
22
+ for (const match of text.matchAll(SENTINEL_RE)) {
23
+ const [, slug, verdict, reason] = match;
24
+ bySlug.set(slug, { slug, verdict: verdict.toLowerCase(), reason: reason || '' });
25
+ }
26
+ return Array.from(bySlug.values());
27
+ }
28
+
29
+ module.exports = { parseValidationSentinels };
@@ -8,9 +8,10 @@
8
8
  * owns) and, via a single cost-gated `claude -p` pass, organizes them into
9
9
  * named semantic clusters with `[[wikilink]]`-derived connections.
10
10
  *
11
- * Cache: ~/.claude/session-manager/memory-clusters/<workspace>.json
12
- * The `claude -p` call only fires when the caller passes `refresh: true` —
13
- * this is the cost gate; the renderer wires it to an explicit button.
11
+ * Cache: <cwd>/session-manager-operations/memory-clusters/clusters.json (one
12
+ * file per project — see memory-clusters/README.md). The `claude -p` call
13
+ * only fires when the caller passes `refresh: true` — this is the cost gate;
14
+ * the renderer wires it to an explicit button.
14
15
  *
15
16
  * Spawn/capture/timeout pattern mirrors kg.cjs's runClaude (~line 316-351):
16
17
  * stdin closed, model pinned, hard timeout that resolves {ok:false} rather
@@ -26,17 +27,34 @@ const { extractJson } = require('./lib/extractJson.cjs');
26
27
  const { runClaudeP } = require('./lib/runClaudeP.cjs');
27
28
  const { writeJson } = require('./config.cjs');
28
29
  const config = require('./config.cjs');
30
+ const { opsPath } = require('./lib/opsOwnership.cjs');
29
31
  const { MEMORY_SLUG_RE } = require('./lib/memorySlug.cjs');
30
32
 
31
33
  const HOME = os.homedir();
32
- const CLUSTERS_DIR = path.join(HOME, '.claude', 'session-manager', 'memory-clusters');
34
+ const CACHE_WRITER = 'memory-clusters';
33
35
 
34
36
  function memoryDir(workspace) {
35
37
  return path.join(HOME, '.claude', 'projects', workspace, 'memory');
36
38
  }
37
39
 
38
- function cachePath(workspace) {
39
- return path.join(CLUSTERS_DIR, `${workspace}.json`);
40
+ function clustersCachePath(cwd) {
41
+ return opsPath(cwd, 'memory-clusters', 'clusters.json');
42
+ }
43
+
44
+ /**
45
+ * Resolves `cwd` to a real, boundary-checked project root, or null when
46
+ * `cwd` is absent/invalid. Mirrors memoryTool.cjs's stale() cwd handling:
47
+ * an absent/unusable cwd disables the per-project cache entirely rather than
48
+ * throwing — the cache is purely regenerable, so a caller with no cwd (or a
49
+ * cwd this process cannot vouch for) just gets an uncached aggregate.
50
+ */
51
+ function resolveCwd(cwd) {
52
+ if (typeof cwd !== 'string' || !cwd) return null;
53
+ try {
54
+ return config.validatePath(cwd);
55
+ } catch {
56
+ return null;
57
+ }
40
58
  }
41
59
 
42
60
  // System prompt for clustering — sets the role server-side so the CLI treats
@@ -143,17 +161,19 @@ function parseClusters(rawLlmText, slugs) {
143
161
  return { clusters, orphans };
144
162
  }
145
163
 
146
- async function readCache(workspace) {
147
- const r = await config.readText(cachePath(workspace));
164
+ async function readCache(cwd) {
165
+ const realCwd = resolveCwd(cwd);
166
+ if (!realCwd) return null;
167
+ const r = await config.readText(clustersCachePath(realCwd));
148
168
  if (!r.exists) return null;
149
169
  try { return JSON.parse(r.text); } catch { return null; }
150
170
  }
151
171
 
152
- async function aggregate({ workspace, refresh }) {
172
+ async function aggregate({ workspace, refresh, cwd }) {
153
173
  const ws = typeof workspace === 'string' && workspace ? workspace : encodeCwd(null);
154
174
 
155
175
  if (!refresh) {
156
- const cached = await readCache(ws);
176
+ const cached = await readCache(cwd);
157
177
  if (cached) return { ...cached, cached: true };
158
178
  return { workspace: ws, generatedAt: null, clusters: [], orphans: [], cached: false };
159
179
  }
@@ -174,7 +194,11 @@ async function aggregate({ workspace, refresh }) {
174
194
  }
175
195
 
176
196
  const result = { workspace: ws, generatedAt: Date.now(), clusters, orphans };
177
- await writeJson(cachePath(ws), result);
197
+ const realCwd = resolveCwd(cwd);
198
+ if (realCwd) {
199
+ config.addAllowedRoot(realCwd);
200
+ await writeJson(clustersCachePath(realCwd), result, { writer: CACHE_WRITER });
201
+ }
178
202
  return { ...result, cached: false };
179
203
  }
180
204
 
@@ -189,4 +213,5 @@ module.exports = {
189
213
  aggregate,
190
214
  // exported for tests
191
215
  parseFrontmatter,
216
+ clustersCachePath,
192
217
  };
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * otelSettings — persists OTEL telemetry export configuration.
3
3
  *
4
- * Storage: ~/.config/session-manager/otel.json
4
+ * Storage: ~/.claude/session-manager/otel.json
5
5
  * Shape: {
6
6
  * enabled: boolean,
7
7
  * endpoint: string, // OTLP/HTTP traces endpoint
@@ -30,8 +30,14 @@ const DEFAULTS = Object.freeze({
30
30
  schemaVersion: SCHEMA_VERSION,
31
31
  });
32
32
 
33
+ let migrated = false;
33
34
  function storePath() {
34
- return path.join(os.homedir(), '.config', 'session-manager', 'otel.json');
35
+ const p = path.join(os.homedir(), '.claude', 'session-manager', 'otel.json');
36
+ if (!migrated) {
37
+ migrated = true;
38
+ config.migrateLegacyHomeFile(path.join(os.homedir(), '.config', 'session-manager', 'otel.json'), p);
39
+ }
40
+ return p;
35
41
  }
36
42
 
37
43
  function isValid(cfg) {