claude-code-session-manager 0.47.1 → 0.49.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/dist/index.html CHANGED
@@ -7,10 +7,10 @@
7
7
  <link rel="preconnect" href="https://fonts.googleapis.com">
8
8
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
9
  <link href="https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,500;0,6..72,600;0,6..72,700;1,6..72,400&family=Geist:wght@300;400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
10
- <script type="module" crossorigin src="./assets/index--6k5kE62.js"></script>
10
+ <script type="module" crossorigin src="./assets/index-DkdVY_yL.js"></script>
11
11
  <link rel="modulepreload" crossorigin href="./assets/monaco-editor-BW5C4Iv1.js">
12
12
  <link rel="stylesheet" crossorigin href="./assets/monaco-editor-BTnBOi8r.css">
13
- <link rel="stylesheet" crossorigin href="./assets/index-B6DLwpeA.css">
13
+ <link rel="stylesheet" crossorigin href="./assets/index-CBdP0BTs.css">
14
14
  </head>
15
15
  <body class="bg-bg text-fg font-sans antialiased">
16
16
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-session-manager",
3
- "version": "0.47.1",
3
+ "version": "0.49.0",
4
4
  "description": "Local cockpit for the Claude Code CLI — multi-tab terminal, full config surface, scheduler, voice dictation, and live observability.",
5
5
  "type": "module",
6
6
  "main": "src/main/index.cjs",
@@ -0,0 +1,44 @@
1
+ ---
2
+ name: builder:diff
3
+ description: Step 0 of builder — determine everything on HEAD that hasn't been published yet. Prefer the project's actual npm registry state over git tags, since a tag can exist without a matching publish (or vice versa); fall back to the last git tag when there's no resolvable package name or no network.
4
+ ---
5
+
6
+ # builder:diff
7
+
8
+ Resolve what "already published" means for this project, then list every commit on `HEAD`
9
+ since that point.
10
+
11
+ ## 1. Resolve the build target
12
+
13
+ Read `session-manager-operations/architecture/build-target.json` via
14
+ `resolveBuildTarget(cwd)` in `src/main/lib/buildTarget.cjs` (falls back to `package.json`'s
15
+ `name` if no explicit config exists). If `buildTarget.cjs` isn't present yet in this repo,
16
+ read `package.json`'s `name` field directly and note in the final report that the config
17
+ reader was missing — this does not block the pipeline.
18
+
19
+ ## 2. Determine the last published point
20
+
21
+ Preferred: `npm view <packageName> version` — the actual registry state, not just a local
22
+ tag. This catches the case where a tag was pushed but the publish itself failed partway
23
+ (the tag would lie; the registry won't).
24
+
25
+ Fallback (no network, or `npm view` 404s because the package was never published): use the
26
+ last git tag instead — `git describe --tags --abbrev=0`.
27
+
28
+ ## 3. Diff
29
+
30
+ ```
31
+ git log <last-published-version-or-tag>..HEAD --oneline
32
+ ```
33
+
34
+ If `npm view` returned a version not tagged locally, resolve its tag first (`git tag -l
35
+ 'v<version>'`); if no matching local tag exists, fall back to comparing against
36
+ `git describe --tags --abbrev=0` and note the discrepancy in the report.
37
+
38
+ ## Output
39
+
40
+ - The resolved package name and last-published version/tag (and which method resolved it —
41
+ registry or git tag).
42
+ - The full commit list (subject lines) since that point.
43
+ - If the list is empty: stop here and report "nothing to publish" — do not proceed to
44
+ `builder:classify-and-bump`.
@@ -0,0 +1,37 @@
1
+ ---
2
+ name: builder:classify-and-bump
3
+ description: Step 1 of builder — parse conventional-commit prefixes from the commit list produced by builder:diff and decide the semver bump. Never guess when the history doesn't follow the convention — ask the user instead.
4
+ ---
5
+
6
+ # builder:classify-and-bump
7
+
8
+ Classify every commit subject from `builder:diff`'s output and pick the highest-severity
9
+ bump that applies.
10
+
11
+ ## Rules (conventional commits)
12
+
13
+ - Any commit containing `BREAKING CHANGE:` in its body, or a `!:` right after the type/scope
14
+ (e.g. `feat(api)!:`) → **major**.
15
+ - Any `feat:`/`feat(scope):` commit (and no breaking marker above) → **minor**.
16
+ - Any `fix:`/`fix(scope):` commit (and nothing higher) → **patch**.
17
+ - `chore:`, `docs:`, `refactor:`, `test:`, `ci:` etc. with no `fix`/`feat`/breaking commit
18
+ alongside them → **patch** (matches this project's own convention — see recent history,
19
+ e.g. `chore(release): bump to v0.47.1` following a patch-only commit set).
20
+
21
+ Take the **highest** bump implied by the whole commit list (one `feat:` among ten `fix:`
22
+ commits still means minor; one breaking marker anywhere means major).
23
+
24
+ ## When commits don't follow the convention
25
+
26
+ If a meaningful fraction of the commit list has no recognizable `type:` prefix at all (not
27
+ just an unconventional scope — genuinely no prefix, e.g. a bare "wip" or "updates"), **do
28
+ not guess**. Say so explicitly and ask the user which bump to apply, listing the
29
+ unclassifiable commits. This mirrors `pr-review-sweep:classify`'s needs-decision carve-out —
30
+ guessing wrong here ships an incorrect semver bump that's hard to walk back once published.
31
+
32
+ ## Output
33
+
34
+ - The bump kind: `patch` | `minor` | `major`.
35
+ - The commit list grouped by which rule matched each one (for the eventual report step).
36
+ - If ambiguous: the question posed to the user instead of a bump kind, and the pipeline
37
+ pauses here until answered.
@@ -0,0 +1,37 @@
1
+ ---
2
+ name: builder:gate
3
+ description: Step 2 of builder — run typecheck and unit tests, both time-bounded, as the hard pre-publish gate. A failing gate stops the pipeline; builder:publish never runs on a red gate.
4
+ ---
5
+
6
+ # builder:gate
7
+
8
+ Run this project's own check commands against `HEAD` exactly as they run in CI/manually —
9
+ no shortcuts, no `--skip` flags.
10
+
11
+ ```
12
+ timeout 180 npm run typecheck
13
+ timeout 300 npm run test:unit
14
+ ```
15
+
16
+ (Adjust bounds per project if `CLAUDE.md` documents different timing expectations — these
17
+ values match session-manager's own commands: `tsc --noEmit` and `vitest run`.) Always wrap
18
+ in `timeout` — an unbounded gate command is exactly the kind of stuck-job failure mode
19
+ `PRD_AUTHORING.md` warns about elsewhere in this repo; a hang here should surface as a
20
+ timeout, not run forever.
21
+
22
+ ## On failure
23
+
24
+ Stop immediately. Do not proceed to `builder:publish`. Report:
25
+ - which command failed (typecheck or test:unit)
26
+ - the actual failure output (not just "gate failed")
27
+ - that no version bump, tag, or publish happened
28
+
29
+ ## On success
30
+
31
+ Proceed to `builder:publish` with no pause for reconfirmation — see the orchestrator's hard
32
+ rules on this.
33
+
34
+ ## Output
35
+
36
+ - `PASS` or `FAIL` per command.
37
+ - Full failure output on `FAIL`, empty on `PASS`.
@@ -0,0 +1,97 @@
1
+ ---
2
+ name: builder:publish
3
+ description: Step 3 of builder — the ISOLATED WORKTREE publish technique. Bumps the version, tags, then does the actual build+publish from a clean git worktree checked out at that tag so the live/dirty working directory is never touched by prepublishOnly's build step. Every command below was run for real 4 times in the session that originated this skill (session-manager v0.45.1 → v0.47.1).
4
+ ---
5
+
6
+ # builder:publish
7
+
8
+ A dirty working tree in the main repo is **not** a blocker for this step — that's the whole
9
+ point of the isolated-worktree technique. Run the sequence below in order, without pausing
10
+ to ask for reconfirmation once `builder:gate` has passed.
11
+
12
+ ## Procedure (exact order — verified across 4 real publishes)
13
+
14
+ 1. **Bump the version, package.json only:**
15
+ ```
16
+ npm version <bump> --no-git-tag-version
17
+ ```
18
+ (`<bump>` is the kind decided by `builder:classify-and-bump` — `patch`/`minor`/`major`.)
19
+ `--no-git-tag-version` matters: it updates `package.json`/`package-lock.json` without
20
+ creating a git tag yet, so the tag step below can carry a proper message.
21
+
22
+ 2. **Stage only the two version files — never `-A`:**
23
+ ```
24
+ git add package.json package-lock.json
25
+ ```
26
+
27
+ 3. **Commit, with a message listing the commits this release covers:**
28
+ ```
29
+ git commit -m "chore(release): bump to v<version>
30
+
31
+ - <commit 1 subject>
32
+ - <commit 2 subject>
33
+ - ..."
34
+ ```
35
+
36
+ 4. **Tag the release commit:**
37
+ ```
38
+ git tag v<version>
39
+ ```
40
+
41
+ 5. **Create the isolated worktree from that tag:**
42
+ ```
43
+ git worktree add /tmp/sm-publish-v<version> v<version>
44
+ ```
45
+
46
+ 6. **Move into the worktree and build clean:**
47
+ ```
48
+ cd /tmp/sm-publish-v<version>
49
+ npm ci
50
+ npm run typecheck
51
+ ```
52
+ `npm ci` gives a fully clean `node_modules` from the lockfile — no leftover local state
53
+ from the dirty main repo can leak into the published artifact.
54
+
55
+ 7. **Publish from inside the worktree:**
56
+ ```
57
+ npm publish
58
+ ```
59
+ This project's `prepublishOnly` script runs `vite build` — because step 6 already `cd`'d
60
+ into `/tmp/sm-publish-v<version>`, that build runs **inside the clean worktree**, never
61
+ touching (or being affected by) the live/dirty working directory back in the main repo.
62
+ This is the entire reason the worktree exists: it decouples "what's uncommitted in my
63
+ editor right now" from "what gets built and shipped."
64
+
65
+ 8. **Back in the main repo, push:**
66
+ ```
67
+ cd <main repo path>
68
+ git push origin main
69
+ git push origin v<version>
70
+ ```
71
+
72
+ 9. **Clean up the worktree:**
73
+ ```
74
+ git worktree remove /tmp/sm-publish-v<version> --force
75
+ ```
76
+
77
+ 10. **Verify the publish actually landed on the registry:**
78
+ ```
79
+ npm view <packageName> version
80
+ npm view <packageName> dist-tags
81
+ ```
82
+ Confirm the reported version matches `<version>` and `latest` points to it (unless the
83
+ project intentionally publishes to a different dist-tag).
84
+
85
+ ## On any step failing
86
+
87
+ Stop at that step. Do not proceed. Report which step failed and its output. If the worktree
88
+ was already created (step 5+), leave it in place if its state is relevant evidence for
89
+ diagnosing the failure; otherwise remove it (step 9) before reporting, so a retry doesn't
90
+ collide with a stale `/tmp/sm-publish-v<version>` path.
91
+
92
+ ## Output
93
+
94
+ - Version published.
95
+ - Confirmation the worktree was removed.
96
+ - `npm view` output proving the registry has the new version and correct dist-tag.
97
+ - Confirmation both `git push` calls succeeded (main + tag).
@@ -0,0 +1,35 @@
1
+ ---
2
+ name: builder:report
3
+ description: Step 4 of builder — summarize the completed (or stopped) publish run in one report — version bumped, commits covered, npm dist-tag verified, git push confirmed.
4
+ ---
5
+
6
+ # builder:report
7
+
8
+ Produce one concise summary of what happened, whether the pipeline reached publish or
9
+ stopped early.
10
+
11
+ ## On a successful publish
12
+
13
+ Report:
14
+ - **Version**: previous → new (and bump kind: patch/minor/major).
15
+ - **Commits covered**: the full list from `builder:diff`, grouped by the classification
16
+ `builder:classify-and-bump` assigned.
17
+ - **Gate result**: typecheck + test:unit, both PASS.
18
+ - **npm verification**: `npm view <packageName> version` and `dist-tags` output, confirming
19
+ the registry matches.
20
+ - **Git state**: both `git push origin main` and `git push origin v<version>` confirmed;
21
+ worktree removed.
22
+
23
+ ## On an early stop (no diff, failed gate, ambiguous bump, or a publish-step failure)
24
+
25
+ Report exactly which step stopped the pipeline and why:
26
+ - `builder:diff` — "nothing to publish" (no commits since last release).
27
+ - `builder:classify-and-bump` — the question posed to the user, still unanswered.
28
+ - `builder:gate` — which command failed and its output.
29
+ - `builder:publish` — which numbered step failed, its output, and whether a worktree or
30
+ local tag was left behind that a retry needs to account for.
31
+
32
+ ## Output
33
+
34
+ One report, plain text, suitable for pasting into an Epic/PRD completion note or relaying
35
+ directly to the user — no separate file is written by this step.
@@ -0,0 +1,86 @@
1
+ ---
2
+ name: builder
3
+ description: Watch the current project's git history against its published npm package and drive the next publish — diff HEAD against the last release, classify + bump the version from conventional-commit prefixes, gate on typecheck/tests, publish from an isolated git worktree (never the live working directory), then report. Orchestrates 5 nested sub-skills (builder:diff, :classify-and-bump, :gate, :publish, :report). Use when the user says "/builder", "publish", "release", "cut a release", "bump the version", "ship to npm", or asks whether there's anything unpublished.
4
+ ---
5
+
6
+ # builder (orchestrator)
7
+
8
+ Global, project-agnostic release pipeline — works against whatever npm package the current
9
+ project publishes (resolved via `src/main/lib/buildTarget.cjs`'s `resolveBuildTarget()`, or
10
+ by reading `package.json` directly if that file isn't present yet). Codified from a real
11
+ session that ran this exact sequence 4 times by hand, publishing session-manager
12
+ v0.45.1 → v0.47.1 to npm — this skill turns that proven manual procedure into a reusable one.
13
+
14
+ **Naming convention** (same as `pr-review-sweep` and `issue-address`): sub-skill directories
15
+ are prefixed `0-`, `1-`, ... in execution order, so a plain directory listing sorts in DAG
16
+ order without opening any file. The invocable `name:` field stays a clean colon-scoped
17
+ identifier (`builder:diff`) without the numeric prefix.
18
+
19
+ ## Pipeline DAG
20
+
21
+ ```
22
+ ┌───────────────────────┐
23
+ │ 0. builder:diff │ HEAD vs. last published version (npm view, else last git tag)
24
+ └───────────────────────┘
25
+ │ commit list since last release (empty → stop, nothing to do)
26
+ ▼
27
+ ┌───────────────────────────┐
28
+ │ 1. builder:classify-and-bump │ conventional-commit prefixes → patch/minor/major
29
+ └───────────────────────────┘ (non-conventional history → ask, don't guess)
30
+ │ bump kind decided
31
+ ▼
32
+ ┌───────────────────────┐
33
+ │ 2. builder:gate │ npm run typecheck + npm run test:unit, both bounded
34
+ └───────────────────────┘
35
+ PASS │ │ FAIL
36
+ ▼ ▼
37
+ ┌───────────────────────┐ STOP — report failing gate, do not publish
38
+ │ 3. builder:publish │ ISOLATED WORKTREE technique (see that step's own file)
39
+ └───────────────────────┘
40
+ │ version bumped, tagged, pushed, published, dist-tag verified
41
+ ▼
42
+ ┌───────────────────────┐
43
+ │ 4. builder:report │ version, commits covered, dist-tag, push confirmation
44
+ └───────────────────────┘
45
+ ```
46
+
47
+ | Step | Input | Output | On failure/empty |
48
+ |---|---|---|---|
49
+ | 0. `builder:diff` | current project cwd | commit list since last published version (or last git tag) | no commits since last release → stop, report "nothing to publish" |
50
+ | 1. `builder:classify-and-bump` | commit list | bump kind (`patch`/`minor`/`major`) | commits don't follow conventional-commit style → say so, ask the user rather than guessing |
51
+ | 2. `builder:gate` | working tree at HEAD | typecheck + unit test result | either fails → stop, report the failure, do not publish |
52
+ | 3. `builder:publish` | bump kind, gate-passed HEAD | published npm package, pushed tag + main | any step fails → stop, report which step and the worktree's state (don't leave it behind uncleaned unless it's evidence of the failure) |
53
+ | 4. `builder:report` | publish result | one summary: version bumped, commits covered, npm dist-tag verified, git push confirmed | n/a |
54
+
55
+ ## Hard rules — read before running any step
56
+
57
+ - **A dirty working tree in the main repo is NOT a blocker.** The isolated-worktree
58
+ technique in `builder:publish` sidesteps it entirely — `npm ci` + `vite build` +
59
+ `npm publish` all run inside a clean worktree checked out from the just-created tag, never
60
+ touching the live/dirty repo. Don't hold the pipeline waiting for a clean tree; that
61
+ guidance is superseded by this skill.
62
+ - **Once a diff is found and the gate passes, run the full sequence through to publish
63
+ without pausing for reconfirmation.** This was confirmed by the user across 4 consecutive
64
+ manual runs in the session that originated this skill — asking "should I publish now?"
65
+ after the gate is green is exactly the friction this skill exists to remove.
66
+ - **The gate is a hard stop.** A failing `typecheck` or `test:unit` run means step 3 never
67
+ executes — no publish, no version bump, no tag. Report the failure and stop.
68
+ - **Never guess a version bump from unconventional commit messages.** If the commit list
69
+ doesn't cleanly map to `fix:`/`feat:`/`BREAKING CHANGE:` prefixes, `builder:classify-and-bump`
70
+ asks the user which bump to apply rather than inferring one.
71
+
72
+ ## Why nested skills instead of one inline sequence
73
+
74
+ Each step is its own file so it shows up as its own invocation in whatever surface tracks
75
+ skill/tool calls, and so a step can be changed (a different gate command, a new registry
76
+ target) without touching the others — same rationale as `pr-review-sweep` and
77
+ `issue-address`.
78
+
79
+ ## What this skill is not
80
+
81
+ - Not a decision-maker on *whether* to release — it assumes the user (or an Epic) already
82
+ wants the next diff shipped. If there's genuine ambiguity about scope, `builder:classify-and-bump`
83
+ is the one step that pauses to ask.
84
+ - Not a registry-agnostic tool — `builder:publish` is npm-specific (see `buildTarget.cjs`'s
85
+ `registry` field for future non-npm targets; out of scope for this version).
86
+ - Not wired to any UI button — invoking this skill is the only entry point today.
@@ -0,0 +1,133 @@
1
+ /**
2
+ * agentLibrary.test.cjs — unit tests for the Agent Library nav page's
3
+ * backend: enumerating global `~/.claude/agents/*.md` personas and
4
+ * detecting per-project `.claude/agents/<name>.md` overlays among
5
+ * currently-open tabs.
6
+ *
7
+ * Run: timeout 120 npx vitest run src/main/__tests__/agentLibrary.test.cjs
8
+ */
9
+
10
+ import { test, expect, afterEach } from 'vitest';
11
+ const fs = require('node:fs');
12
+ const fsp = require('node:fs/promises');
13
+ const os = require('node:os');
14
+ const path = require('node:path');
15
+ const { listPersonas, openProjects, parseTools } = require('../agentLibrary.cjs');
16
+
17
+ const tmpDirs = [];
18
+ afterEach(async () => {
19
+ while (tmpDirs.length) {
20
+ const d = tmpDirs.pop();
21
+ await fsp.rm(d, { recursive: true, force: true });
22
+ }
23
+ });
24
+
25
+ async function mkTmp(prefix) {
26
+ const d = await fsp.mkdtemp(path.join(os.tmpdir(), prefix));
27
+ tmpDirs.push(d);
28
+ return d;
29
+ }
30
+
31
+ // listPersonas/openProjects take injectable deps; tests pass an identity
32
+ // validatePath since the fixture dirs live under os.tmpdir(), not the real
33
+ // home directory config.cjs's validatePath would otherwise enforce.
34
+ const identityValidatePath = (p) => p;
35
+
36
+ test('parseTools splits + trims a comma-separated tools frontmatter value', () => {
37
+ expect(parseTools('Read, Grep, Glob, Bash')).toEqual(['Read', 'Grep', 'Glob', 'Bash']);
38
+ expect(parseTools('')).toEqual([]);
39
+ expect(parseTools(undefined)).toEqual([]);
40
+ });
41
+
42
+ test('openProjects dedups by cwd and uses the last path segment as name', async () => {
43
+ const loadSessions = async () => ({
44
+ tabs: [
45
+ { cwd: '/home/u/Projects/alpha' },
46
+ { cwd: '/home/u/Projects/beta/' },
47
+ { cwd: '/home/u/Projects/alpha' }, // duplicate — should collapse
48
+ { cwd: null }, // malformed — should be skipped
49
+ ],
50
+ });
51
+ const projects = await openProjects({ loadSessions });
52
+ expect(projects).toEqual([
53
+ { cwd: '/home/u/Projects/alpha', name: 'alpha' },
54
+ { cwd: '/home/u/Projects/beta/', name: 'beta' },
55
+ ]);
56
+ });
57
+
58
+ test('listPersonas returns [] when the global agents dir does not exist', async () => {
59
+ const missingDir = path.join(os.tmpdir(), 'sm-agent-library-does-not-exist-' + Date.now());
60
+ const personas = await listPersonas({
61
+ globalDir: missingDir,
62
+ loadSessions: async () => ({ tabs: [] }),
63
+ validatePath: identityValidatePath,
64
+ });
65
+ expect(personas).toEqual([]);
66
+ });
67
+
68
+ test('listPersonas parses frontmatter and reports overridingProjects for open tabs with a local overlay', async () => {
69
+ const globalDir = await mkTmp('sm-agent-library-global-');
70
+ await fsp.writeFile(
71
+ path.join(globalDir, 'builder.md'),
72
+ [
73
+ '---',
74
+ 'name: builder',
75
+ 'description: Watch git history and drive the next publish.',
76
+ 'tools: Read, Grep, Glob, Bash',
77
+ '---',
78
+ '',
79
+ 'You are the Builder agent.',
80
+ '',
81
+ ].join('\n'),
82
+ );
83
+ await fsp.writeFile(
84
+ path.join(globalDir, 'debugger.md'),
85
+ ['---', 'name: debugger', 'description: Diagnose a failing test.', '---', '', 'Body.', ''].join('\n'),
86
+ );
87
+
88
+ const projectWithOverlay = await mkTmp('sm-agent-library-project-a-');
89
+ await fsp.mkdir(path.join(projectWithOverlay, '.claude', 'agents'), { recursive: true });
90
+ await fsp.writeFile(
91
+ path.join(projectWithOverlay, '.claude', 'agents', 'builder.md'),
92
+ '---\nname: builder\n---\nProject-specific overlay.\n',
93
+ );
94
+
95
+ const projectWithoutOverlay = await mkTmp('sm-agent-library-project-b-');
96
+
97
+ const loadSessions = async () => ({
98
+ tabs: [{ cwd: projectWithOverlay }, { cwd: projectWithoutOverlay }],
99
+ });
100
+
101
+ const personas = await listPersonas({ globalDir, loadSessions, validatePath: identityValidatePath });
102
+ expect(personas).toHaveLength(2);
103
+
104
+ const byName = Object.fromEntries(personas.map((p) => [p.name, p]));
105
+ expect(byName.builder.description).toBe('Watch git history and drive the next publish.');
106
+ expect(byName.builder.tools).toEqual(['Read', 'Grep', 'Glob', 'Bash']);
107
+ expect(byName.builder.body).toContain('You are the Builder agent.');
108
+ expect(byName.builder.overridingProjects).toEqual([path.basename(projectWithOverlay)]);
109
+
110
+ expect(byName.debugger.overridingProjects).toEqual([]);
111
+ });
112
+
113
+ test('listPersonas skips a project cwd that validatePath rejects, rather than throwing', async () => {
114
+ const globalDir = await mkTmp('sm-agent-library-global-');
115
+ await fsp.writeFile(path.join(globalDir, 'builder.md'), '---\nname: builder\n---\nBody.\n');
116
+
117
+ const rejectedProject = await mkTmp('sm-agent-library-project-rejected-');
118
+ await fsp.mkdir(path.join(rejectedProject, '.claude', 'agents'), { recursive: true });
119
+ await fsp.writeFile(path.join(rejectedProject, '.claude', 'agents', 'builder.md'), 'overlay\n');
120
+
121
+ const loadSessions = async () => ({ tabs: [{ cwd: rejectedProject }] });
122
+ // Simulate config.cjs's validatePath throwing for anything under the
123
+ // rejected project (as it would for a cwd outside the allowed-roots set)
124
+ // while still allowing the global agents dir itself to resolve.
125
+ const selectiveValidatePath = (p) => {
126
+ if (p.startsWith(rejectedProject)) throw new Error('outside allowed boundaries');
127
+ return p;
128
+ };
129
+
130
+ const personas = await listPersonas({ globalDir, loadSessions, validatePath: selectiveValidatePath });
131
+ expect(personas).toHaveLength(1);
132
+ expect(personas[0].overridingProjects).toEqual([]);
133
+ });
@@ -0,0 +1,115 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * agentLibrary.cjs — read-only enumeration of agent personas for the
5
+ * "Agent Library" nav page (Home face only).
6
+ *
7
+ * Global agent definitions live at `~/.claude/agents/*.md`. Per Claude Code's
8
+ * own precedence rules, a project can overlay a same-named agent at
9
+ * `<project-cwd>/.claude/agents/<name>.md`, which wins over the global
10
+ * definition when both exist. This module reports, for every global agent,
11
+ * which of the currently-open TABs (per sessionsStore's persisted tab list —
12
+ * the same source of truth the renderer keeps in sync on every open/close)
13
+ * has such an overlay.
14
+ *
15
+ * All filesystem paths are routed through config.cjs's validatePath so this
16
+ * feature can't be used to read outside the home-dir boundary already
17
+ * enforced everywhere else in the app.
18
+ */
19
+
20
+ const fsp = require('node:fs/promises');
21
+ const fsSync = require('node:fs');
22
+ const path = require('node:path');
23
+ const os = require('node:os');
24
+ const { splitFrontmatter } = require('./lib/prdFrontmatter.cjs');
25
+ const configMgr = require('./config.cjs');
26
+ const sessionsStore = require('./sessionsStore.cjs');
27
+
28
+ /** Parses the frontmatter `tools` field ("Read, Grep, Glob, Bash") into a list. */
29
+ function parseTools(raw) {
30
+ if (!raw) return [];
31
+ return raw.split(',').map((t) => t.trim()).filter(Boolean);
32
+ }
33
+
34
+ /**
35
+ * Currently-open project tabs, deduped by cwd, as `{ cwd, name }` — `name`
36
+ * is the last path segment (matches the `projectNameFromCwd` convention used
37
+ * by the Scheduler's ProjectTag primitive). `loadSessions` is injectable
38
+ * (mirrors scheduler.cjs's notifyOriginatingTab) so tests can exercise the
39
+ * dedup/naming logic without touching the real persisted tabs.json.
40
+ */
41
+ async function openProjects({ loadSessions = sessionsStore.load } = {}) {
42
+ const { tabs } = await loadSessions();
43
+ const seen = new Set();
44
+ const projects = [];
45
+ for (const t of tabs ?? []) {
46
+ if (!t || typeof t.cwd !== 'string' || !t.cwd || seen.has(t.cwd)) continue;
47
+ seen.add(t.cwd);
48
+ projects.push({ cwd: t.cwd, name: path.basename(t.cwd.replace(/\/+$/, '')) || t.cwd });
49
+ }
50
+ return projects;
51
+ }
52
+
53
+ /**
54
+ * Lists every global agent persona plus which open projects overlay it.
55
+ * Deps are injectable so tests can point `globalDir` at a fixture directory
56
+ * and `loadSessions` at a fake tab list without touching the real
57
+ * `~/.claude/agents` or `tabs.json`.
58
+ */
59
+ async function listPersonas({
60
+ globalDir = path.join(os.homedir(), '.claude', 'agents'),
61
+ loadSessions = sessionsStore.load,
62
+ validatePath = configMgr.validatePath,
63
+ } = {}) {
64
+ let files;
65
+ try {
66
+ files = (await fsp.readdir(globalDir)).filter((f) => f.endsWith('.md'));
67
+ } catch (e) {
68
+ if (e.code === 'ENOENT') return [];
69
+ throw e;
70
+ }
71
+
72
+ const projects = await openProjects({ loadSessions });
73
+ const personas = [];
74
+
75
+ for (const file of files.sort()) {
76
+ const fallbackName = file.replace(/\.md$/, '');
77
+ let real;
78
+ try {
79
+ real = validatePath(path.join(globalDir, file));
80
+ } catch {
81
+ continue; // out of bounds — shouldn't happen for a home-relative path
82
+ }
83
+ let text;
84
+ try {
85
+ text = await fsp.readFile(real, 'utf8');
86
+ } catch {
87
+ continue;
88
+ }
89
+ const { fm, body } = splitFrontmatter(text);
90
+
91
+ const overridingProjects = [];
92
+ for (const p of projects) {
93
+ let overlayReal;
94
+ try {
95
+ overlayReal = validatePath(path.join(p.cwd, '.claude', 'agents', file));
96
+ } catch {
97
+ continue; // project cwd outside allowed roots — skip rather than throw
98
+ }
99
+ if (fsSync.existsSync(overlayReal)) overridingProjects.push(p.name);
100
+ }
101
+
102
+ personas.push({
103
+ name: fm.name || fallbackName,
104
+ description: fm.description || null,
105
+ tools: parseTools(fm.tools),
106
+ path: real,
107
+ body: body.trim(),
108
+ overridingProjects,
109
+ });
110
+ }
111
+
112
+ return personas;
113
+ }
114
+
115
+ module.exports = { listPersonas, openProjects, parseTools };
@@ -30,6 +30,8 @@ const { createAdminHttp } = require('./lib/localAdminHttp.cjs');
30
30
  const prdCreate = require('./lib/prdCreate.cjs');
31
31
  const chatRunner = require('./chatRunner.cjs');
32
32
  const promptSessionEvents = require('./promptSessionEvents.cjs');
33
+ const agentLibrary = require('./agentLibrary.cjs');
34
+ const { resolveBuildTarget } = require('./lib/buildTarget.cjs');
33
35
  const adminHttp = createAdminHttp();
34
36
  scheduler.registerAdminRoutes(adminHttp);
35
37
  prdCreate.registerAdminRoute(adminHttp, scheduler.remote);
@@ -445,6 +447,11 @@ ipcMain.handle('app:launch-mode', () => ({
445
447
  // `claude mcp list`. Read-only, single in-flight call — no polling.
446
448
  ipcMain.handle('mcp:status', () => probeMcpStatus());
447
449
 
450
+ // Agent Library nav page (Home face only): global `~/.claude/agents/*.md`
451
+ // personas plus, per currently-open project tab, whether that project
452
+ // overlays the same agent name at `<cwd>/.claude/agents/<name>.md`. Read-only.
453
+ ipcMain.handle('agents:list-personas', () => agentLibrary.listPersonas());
454
+
448
455
  ipcMain.handle('app:engage-rules-path', () => process.env.SESSION_MANAGER_ENGAGE_RULES || null);
449
456
 
450
457
  // Boot diagnostics — renderer polls these to surface toasts when `claude` isn't
@@ -681,6 +688,13 @@ ipcMain.handle('app:git-branch', validated(schemas.appGitBranch, async ({ cwd })
681
688
  });
682
689
  }));
683
690
 
691
+ // Resolves a project's publish target for 'build'-tagged Epics — see
692
+ // lib/buildTarget.cjs. Null means the Build toolbar button should be disabled
693
+ // (no explicit config, no auto-discoverable publishable package.json).
694
+ ipcMain.handle('build:resolve-target', validated(schemas.buildResolveTarget, ({ cwd }) => {
695
+ return resolveBuildTarget(cwd);
696
+ }));
697
+
684
698
  // Containment check for the open-in-{editor,finder,terminal} handlers lives
685
699
  // in lib/insideHome.cjs — single chokepoint for the /home/bilkoEVIL prefix-trap.
686
700
  // Editor / finder / terminal logic lives in lib/openExternalApp.cjs.