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/assets/{TiptapBody-3cBKCLGZ.js → TiptapBody-Raw_Is9m.js} +1 -1
- package/dist/assets/{index-B6DLwpeA.css → index-CBdP0BTs.css} +1 -1
- package/dist/assets/{index--6k5kE62.js → index-DkdVY_yL.js} +526 -524
- 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
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
|
|
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-
|
|
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.
|
|
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 };
|
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.
|