claude-code-session-manager 0.47.1 → 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.
- package/dist/assets/{TiptapBody-3cBKCLGZ.js → TiptapBody-w8iZE-hl.js} +1 -1
- package/dist/assets/{index--6k5kE62.js → index-B610Xk3D.js} +469 -467
- package/dist/assets/{index-B6DLwpeA.css → index-DewHqVRE.css} +1 -1
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/plugins/session-manager-dev/skills/builder/0-diff/SKILL.md +44 -0
- package/plugins/session-manager-dev/skills/builder/1-classify-and-bump/SKILL.md +37 -0
- package/plugins/session-manager-dev/skills/builder/2-gate/SKILL.md +37 -0
- package/plugins/session-manager-dev/skills/builder/3-publish/SKILL.md +97 -0
- package/plugins/session-manager-dev/skills/builder/4-report/SKILL.md +35 -0
- package/plugins/session-manager-dev/skills/builder/SKILL.md +86 -0
- package/src/main/__tests__/agentLibrary.test.cjs +133 -0
- package/src/main/agentLibrary.cjs +115 -0
- package/src/main/index.cjs +14 -0
- package/src/main/ipcSchemas.cjs +8 -1
- package/src/main/lib/__tests__/buildTarget.test.cjs +52 -0
- package/src/main/lib/buildTarget.cjs +56 -0
- package/src/preload/api.d.ts +25 -1
- package/src/preload/index.cjs +4 -0
|
@@ -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 };
|
package/src/main/index.cjs
CHANGED
|
@@ -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.
|
package/src/main/ipcSchemas.cjs
CHANGED
|
@@ -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 };
|
package/src/preload/api.d.ts
CHANGED
|
@@ -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,
|
package/src/preload/index.cjs
CHANGED
|
@@ -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
|