claude-code-session-manager 0.47.0 → 0.48.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.
@@ -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.
@@ -313,7 +313,7 @@ const schedulerCreatePrd = z.object({
313
313
  originClaudeSessionId: z.string().min(1).max(128).regex(NO_NEWLINE_RE, 'must not contain newlines').optional(),
314
314
  // User-selected Feature/Bug tag (PRD 774) carried from the originating
315
315
  // PromptTicket — deterministic, never LLM-classified.
316
- tag: z.enum(['feature', 'bug', 'discussion']).optional(),
316
+ tag: z.enum(['feature', 'bug', 'discussion', 'build']).optional(),
317
317
  });
318
318
 
319
319
  // Bulk archive: slug list, capped to limit unbounded retag/archive payloads.
@@ -687,6 +687,12 @@ const appGitBranch = z.object({
687
687
  cwd: z.string().min(1).max(4096),
688
688
  }).passthrough();
689
689
 
690
+ // build:resolve-target — see src/main/lib/buildTarget.cjs. cwd is a trusted
691
+ // TAB cwd, not validatePath'd here (same invariant as appGitBranch above).
692
+ const buildResolveTarget = z.object({
693
+ cwd: z.string().min(1).max(4096),
694
+ }).passthrough();
695
+
690
696
  // git:status / git:file-status — see src/main/git.cjs. cwd is validatePath'd
691
697
  // inside the handler (allowedRoots = home), so the schema only enforces shape.
692
698
  const gitStatus = z.object({
@@ -857,6 +863,7 @@ module.exports = {
857
863
  voiceSetRecording,
858
864
  appTestFireHook,
859
865
  appGitBranch,
866
+ buildResolveTarget,
860
867
  gitStatus,
861
868
  gitFileStatus,
862
869
  repoAnalyze,
@@ -0,0 +1,52 @@
1
+ // buildTarget.cjs — resolves a project's publish destination for 'build'-tagged Epics.
2
+ // vitest globals (test/beforeEach/afterEach) — same convention as the other .cjs tests.
3
+ const assert = require('node:assert');
4
+ const fs = require('node:fs');
5
+ const os = require('node:os');
6
+ const path = require('node:path');
7
+ const { resolveBuildTarget } = require('../buildTarget.cjs');
8
+
9
+ let tmpDir;
10
+
11
+ beforeEach(() => {
12
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-build-target-'));
13
+ });
14
+ afterEach(() => {
15
+ fs.rmSync(tmpDir, { recursive: true, force: true });
16
+ });
17
+
18
+ test('explicit config file present is returned verbatim', () => {
19
+ const archDir = path.join(tmpDir, 'session-manager-operations', 'architecture');
20
+ fs.mkdirSync(archDir, { recursive: true });
21
+ const config = {
22
+ registry: 'npm',
23
+ packageName: 'my-pkg',
24
+ versionBumpPolicy: 'conventional-commits',
25
+ gates: ['typecheck'],
26
+ };
27
+ fs.writeFileSync(path.join(archDir, 'build-target.json'), JSON.stringify(config));
28
+
29
+ assert.deepEqual(resolveBuildTarget(tmpDir), config);
30
+ });
31
+
32
+ test('no config but package.json has name and is not private auto-discovers', () => {
33
+ fs.writeFileSync(path.join(tmpDir, 'package.json'), JSON.stringify({ name: 'auto-pkg' }));
34
+
35
+ assert.deepEqual(resolveBuildTarget(tmpDir), {
36
+ registry: 'npm',
37
+ packageName: 'auto-pkg',
38
+ versionBumpPolicy: 'conventional-commits',
39
+ gates: [],
40
+ discovered: true,
41
+ });
42
+ });
43
+
44
+ test('no config and package.json is private returns null', () => {
45
+ fs.writeFileSync(path.join(tmpDir, 'package.json'), JSON.stringify({ name: 'priv-pkg', private: true }));
46
+
47
+ assert.equal(resolveBuildTarget(tmpDir), null);
48
+ });
49
+
50
+ test('no config and no package.json returns null', () => {
51
+ assert.equal(resolveBuildTarget(tmpDir), null);
52
+ });
@@ -0,0 +1,56 @@
1
+ /**
2
+ * buildTarget.cjs — resolves a project's publish destination for
3
+ * 'build'-tagged Epics.
4
+ *
5
+ * Reads `<cwd>/session-manager-operations/architecture/build-target.json`
6
+ * when present (a skill-authored doc, not an OWNERS-governed runtime-write
7
+ * namespace — see this repo's CLAUDE.md). Falls back to auto-discovery from
8
+ * `<cwd>/package.json` when no explicit config exists. Pure local-file read;
9
+ * no npm-registry network call here. `cwd` is not validated against
10
+ * `config.cjs`'s `validatePath` allowedRoots — callers must pass a trusted
11
+ * TAB cwd (the same invariant `prdLocations.cjs` relies on), not a raw
12
+ * renderer-supplied string.
13
+ */
14
+ 'use strict';
15
+
16
+ const fs = require('node:fs');
17
+ const path = require('node:path');
18
+
19
+ const BUILD_TARGET_SUBPATH = ['session-manager-operations', 'architecture', 'build-target.json'];
20
+
21
+ function readJson(filePath) {
22
+ try {
23
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+
29
+ /**
30
+ * resolveBuildTarget(cwd) → the project's build target config, or null if
31
+ * neither an explicit config nor an auto-discoverable package.json exists.
32
+ */
33
+ function resolveBuildTarget(cwd) {
34
+ if (!cwd || typeof cwd !== 'string') return null;
35
+
36
+ const configPath = path.join(cwd, ...BUILD_TARGET_SUBPATH);
37
+ const explicit = readJson(configPath);
38
+ if (explicit && typeof explicit === 'object' && !Array.isArray(explicit) && typeof explicit.packageName === 'string') {
39
+ return explicit;
40
+ }
41
+
42
+ const pkg = readJson(path.join(cwd, 'package.json'));
43
+ if (pkg && typeof pkg.name === 'string' && pkg.name.length > 0 && pkg.private !== true) {
44
+ return {
45
+ registry: 'npm',
46
+ packageName: pkg.name,
47
+ versionBumpPolicy: 'conventional-commits',
48
+ gates: [],
49
+ discovered: true,
50
+ };
51
+ }
52
+
53
+ return null;
54
+ }
55
+
56
+ module.exports = { resolveBuildTarget };
@@ -1,3 +1,11 @@
1
+ export interface BuildTarget {
2
+ registry: string;
3
+ packageName: string;
4
+ versionBumpPolicy: string;
5
+ gates: string[];
6
+ discovered?: boolean;
7
+ }
8
+
1
9
  export interface ActionOutcome {
2
10
  ok: boolean;
3
11
  kind?: 'info' | 'warn' | 'error';
@@ -250,6 +258,17 @@ export interface McpStatusResult {
250
258
  checkedAt: number;
251
259
  }
252
260
 
261
+ /** One global `~/.claude/agents/<name>.md` persona, per the Agent Library nav page. */
262
+ export interface AgentPersona {
263
+ name: string;
264
+ description: string | null;
265
+ tools: string[];
266
+ path: string;
267
+ body: string;
268
+ /** Project names (basename of cwd) whose currently-open tab overlays this agent. */
269
+ overridingProjects: string[];
270
+ }
271
+
253
272
  export interface VoiceHotkeyConfig {
254
273
  accelerator: string;
255
274
  mode: 'hold' | 'toggle';
@@ -1090,7 +1109,7 @@ export interface ChatCreatePrdPayload {
1090
1109
  /** Originating tab id — used at job completion to route a status prompt back into the tab. */
1091
1110
  sourceTabId?: string;
1092
1111
  /** User-selected Feature/Bug tag (PRD 774) carried from the originating PromptTicket. */
1093
- tag?: 'feature' | 'bug' | 'discussion';
1112
+ tag?: 'feature' | 'bug' | 'discussion' | 'build';
1094
1113
  }
1095
1114
 
1096
1115
  export type ChatCreatePrdResult =
@@ -1170,6 +1189,8 @@ export interface SessionManagerAPI {
1170
1189
  engageRulesPath: () => Promise<string | null>;
1171
1190
  pickDirectory: () => Promise<string | null>;
1172
1191
  gitBranch: (cwd: string) => Promise<string | null>;
1192
+ /** Publish target for 'build'-tagged Epics — null disables the Build toolbar button. */
1193
+ resolveBuildTarget: (cwd: string) => Promise<BuildTarget | null>;
1173
1194
  rebootApp: () => void;
1174
1195
  testFireHook: (args: TestFireHookArgs) => Promise<TestFireHookResult>;
1175
1196
  /** F7 — true under SM_E2E=1; renderer uses this to suppress wizard auto-trigger. */
@@ -1284,6 +1305,9 @@ export interface SessionManagerAPI {
1284
1305
  mcp: {
1285
1306
  status: () => Promise<McpStatusResult>;
1286
1307
  };
1308
+ agents: {
1309
+ listPersonas: () => Promise<AgentPersona[]>;
1310
+ };
1287
1311
  logs: {
1288
1312
  write: (
1289
1313
  scope: string,
@@ -8,6 +8,7 @@ contextBridge.exposeInMainWorld('api', {
8
8
  engageRulesPath: () => ipcRenderer.invoke('app:engage-rules-path'),
9
9
  pickDirectory: () => ipcRenderer.invoke('app:pick-directory'),
10
10
  gitBranch: (cwd) => ipcRenderer.invoke('app:git-branch', { cwd }),
11
+ resolveBuildTarget: (cwd) => ipcRenderer.invoke('build:resolve-target', { cwd }),
11
12
  rebootApp: () => ipcRenderer.send('app:reboot-app'),
12
13
  archiveProject: (encoded) => ipcRenderer.invoke('app:archive-project', { encoded }),
13
14
  testFireHook: (args) => ipcRenderer.invoke('app:test-fire-hook', args),
@@ -145,6 +146,9 @@ contextBridge.exposeInMainWorld('api', {
145
146
  mcp: {
146
147
  status: () => ipcRenderer.invoke('mcp:status'),
147
148
  },
149
+ agents: {
150
+ listPersonas: () => ipcRenderer.invoke('agents:list-personas'),
151
+ },
148
152
  logs: {
149
153
  // ctx is optional: { cwd, tabId, epicId, tags } — when present and
150
154
  // level is 'error', main also appends a tagged line to that project's