claude-code-session-manager 0.26.0 → 0.28.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/.claude-plugin/marketplace.json +21 -0
- package/dist/assets/{TiptapBody-BsI_35c5.js → TiptapBody-CJ6GK5CM.js} +1 -1
- package/dist/assets/index-DtQ4LzuV.js +3534 -0
- package/dist/assets/index-Dwb94Uxm.css +32 -0
- package/dist/index.html +2 -2
- package/package.json +3 -1
- package/plugins/session-manager-dev/.claude-plugin/plugin.json +19 -0
- package/plugins/session-manager-dev/skills/develop/SKILL.md +112 -0
- package/plugins/session-manager-dev/skills/develop/standards.md +67 -0
- package/plugins/session-manager-dev/skills/explain-to-me/SKILL.md +192 -0
- package/plugins/session-manager-dev/skills/explain-to-me/assets/style-reference.html +163 -0
- package/plugins/session-manager-dev/skills/local-project-health/SKILL.md +58 -0
- package/plugins/session-manager-dev/skills/my-feedback/SKILL.md +132 -0
- package/plugins/session-manager-dev/skills/optimize-kpi/SKILL.md +289 -0
- package/plugins/session-manager-dev/skills/prd/SKILL.md +134 -0
- package/plugins/session-manager-dev/skills/process-feedback/SKILL.md +183 -0
- package/plugins/session-manager-dev/skills/project-status/SKILL.md +244 -0
- package/plugins/session-manager-dev/skills/requesting-code-review/SKILL.md +105 -0
- package/plugins/session-manager-dev/skills/requesting-code-review/code-reviewer.md +146 -0
- package/plugins/session-manager-dev/skills/security-review/SKILL.md +105 -0
- package/src/main/__tests__/scheduler-autofix-select.test.cjs +95 -0
- package/src/main/__tests__/scheduler-autopromote.test.cjs +33 -0
- package/src/main/hives.cjs +96 -36
- package/src/main/scheduler.cjs +135 -11
- package/src/main/templates/PRD_AUTHORING.md +316 -0
- package/src/preload/api.d.ts +8 -7
- package/dist/assets/index-BKkf6gtA.css +0 -32
- package/dist/assets/index-C61eFtxs.js +0 -3525
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# Code Review Agent
|
|
2
|
+
|
|
3
|
+
You are reviewing code changes for production readiness.
|
|
4
|
+
|
|
5
|
+
**Your task:**
|
|
6
|
+
1. Review {WHAT_WAS_IMPLEMENTED}
|
|
7
|
+
2. Compare against {PLAN_OR_REQUIREMENTS}
|
|
8
|
+
3. Check code quality, architecture, testing
|
|
9
|
+
4. Categorize issues by severity
|
|
10
|
+
5. Assess production readiness
|
|
11
|
+
|
|
12
|
+
## What Was Implemented
|
|
13
|
+
|
|
14
|
+
{DESCRIPTION}
|
|
15
|
+
|
|
16
|
+
## Requirements/Plan
|
|
17
|
+
|
|
18
|
+
{PLAN_REFERENCE}
|
|
19
|
+
|
|
20
|
+
## Git Range to Review
|
|
21
|
+
|
|
22
|
+
**Base:** {BASE_SHA}
|
|
23
|
+
**Head:** {HEAD_SHA}
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
git diff --stat {BASE_SHA}..{HEAD_SHA}
|
|
27
|
+
git diff {BASE_SHA}..{HEAD_SHA}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Review Checklist
|
|
31
|
+
|
|
32
|
+
**Code Quality:**
|
|
33
|
+
- Clean separation of concerns?
|
|
34
|
+
- Proper error handling?
|
|
35
|
+
- Type safety (if applicable)?
|
|
36
|
+
- DRY principle followed?
|
|
37
|
+
- Edge cases handled?
|
|
38
|
+
|
|
39
|
+
**Architecture:**
|
|
40
|
+
- Sound design decisions?
|
|
41
|
+
- Scalability considerations?
|
|
42
|
+
- Performance implications?
|
|
43
|
+
- Security concerns?
|
|
44
|
+
|
|
45
|
+
**Testing:**
|
|
46
|
+
- Tests actually test logic (not mocks)?
|
|
47
|
+
- Edge cases covered?
|
|
48
|
+
- Integration tests where needed?
|
|
49
|
+
- All tests passing?
|
|
50
|
+
|
|
51
|
+
**Requirements:**
|
|
52
|
+
- All plan requirements met?
|
|
53
|
+
- Implementation matches spec?
|
|
54
|
+
- No scope creep?
|
|
55
|
+
- Breaking changes documented?
|
|
56
|
+
|
|
57
|
+
**Production Readiness:**
|
|
58
|
+
- Migration strategy (if schema changes)?
|
|
59
|
+
- Backward compatibility considered?
|
|
60
|
+
- Documentation complete?
|
|
61
|
+
- No obvious bugs?
|
|
62
|
+
|
|
63
|
+
## Output Format
|
|
64
|
+
|
|
65
|
+
### Strengths
|
|
66
|
+
[What's well done? Be specific.]
|
|
67
|
+
|
|
68
|
+
### Issues
|
|
69
|
+
|
|
70
|
+
#### Critical (Must Fix)
|
|
71
|
+
[Bugs, security issues, data loss risks, broken functionality]
|
|
72
|
+
|
|
73
|
+
#### Important (Should Fix)
|
|
74
|
+
[Architecture problems, missing features, poor error handling, test gaps]
|
|
75
|
+
|
|
76
|
+
#### Minor (Nice to Have)
|
|
77
|
+
[Code style, optimization opportunities, documentation improvements]
|
|
78
|
+
|
|
79
|
+
**For each issue:**
|
|
80
|
+
- File:line reference
|
|
81
|
+
- What's wrong
|
|
82
|
+
- Why it matters
|
|
83
|
+
- How to fix (if not obvious)
|
|
84
|
+
|
|
85
|
+
### Recommendations
|
|
86
|
+
[Improvements for code quality, architecture, or process]
|
|
87
|
+
|
|
88
|
+
### Assessment
|
|
89
|
+
|
|
90
|
+
**Ready to merge?** [Yes/No/With fixes]
|
|
91
|
+
|
|
92
|
+
**Reasoning:** [Technical assessment in 1-2 sentences]
|
|
93
|
+
|
|
94
|
+
## Critical Rules
|
|
95
|
+
|
|
96
|
+
**DO:**
|
|
97
|
+
- Categorize by actual severity (not everything is Critical)
|
|
98
|
+
- Be specific (file:line, not vague)
|
|
99
|
+
- Explain WHY issues matter
|
|
100
|
+
- Acknowledge strengths
|
|
101
|
+
- Give clear verdict
|
|
102
|
+
|
|
103
|
+
**DON'T:**
|
|
104
|
+
- Say "looks good" without checking
|
|
105
|
+
- Mark nitpicks as Critical
|
|
106
|
+
- Give feedback on code you didn't review
|
|
107
|
+
- Be vague ("improve error handling")
|
|
108
|
+
- Avoid giving a clear verdict
|
|
109
|
+
|
|
110
|
+
## Example Output
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
### Strengths
|
|
114
|
+
- Clean database schema with proper migrations (db.ts:15-42)
|
|
115
|
+
- Comprehensive test coverage (18 tests, all edge cases)
|
|
116
|
+
- Good error handling with fallbacks (summarizer.ts:85-92)
|
|
117
|
+
|
|
118
|
+
### Issues
|
|
119
|
+
|
|
120
|
+
#### Important
|
|
121
|
+
1. **Missing help text in CLI wrapper**
|
|
122
|
+
- File: index-conversations:1-31
|
|
123
|
+
- Issue: No --help flag, users won't discover --concurrency
|
|
124
|
+
- Fix: Add --help case with usage examples
|
|
125
|
+
|
|
126
|
+
2. **Date validation missing**
|
|
127
|
+
- File: search.ts:25-27
|
|
128
|
+
- Issue: Invalid dates silently return no results
|
|
129
|
+
- Fix: Validate ISO format, throw error with example
|
|
130
|
+
|
|
131
|
+
#### Minor
|
|
132
|
+
1. **Progress indicators**
|
|
133
|
+
- File: indexer.ts:130
|
|
134
|
+
- Issue: No "X of Y" counter for long operations
|
|
135
|
+
- Impact: Users don't know how long to wait
|
|
136
|
+
|
|
137
|
+
### Recommendations
|
|
138
|
+
- Add progress reporting for user experience
|
|
139
|
+
- Consider config file for excluded projects (portability)
|
|
140
|
+
|
|
141
|
+
### Assessment
|
|
142
|
+
|
|
143
|
+
**Ready to merge: With fixes**
|
|
144
|
+
|
|
145
|
+
**Reasoning:** Core implementation is solid with good architecture and tests. Important issues (help text, date validation) are easily fixed and don't affect core functionality.
|
|
146
|
+
```
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: security-review
|
|
3
|
+
description: >-
|
|
4
|
+
Check for common security vulnerabilities before shipping code — focus on what
|
|
5
|
+
actually gets exploited (OWASP Top 10: injection, auth, secrets, access control),
|
|
6
|
+
not compliance theater. Use before committing code that handles user input, auth,
|
|
7
|
+
or data storage; when adding API endpoints or external integrations; when
|
|
8
|
+
reviewing dependencies; or when asked to do a security review. Keywords: security,
|
|
9
|
+
vulnerability, OWASP, injection, auth, secrets, review.
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Security Review
|
|
13
|
+
|
|
14
|
+
## Overview
|
|
15
|
+
|
|
16
|
+
Check for common security vulnerabilities before shipping code. Focus on what actually gets exploited, not compliance theater.
|
|
17
|
+
|
|
18
|
+
## When to Use
|
|
19
|
+
|
|
20
|
+
- Before committing code that handles user input, auth, or data storage
|
|
21
|
+
- When adding new API endpoints or external integrations
|
|
22
|
+
- When reviewing dependencies or updating packages
|
|
23
|
+
- When asked to do a security review
|
|
24
|
+
|
|
25
|
+
## OWASP Top 10 Checklist
|
|
26
|
+
|
|
27
|
+
### Injection (SQL, NoSQL, OS Command, LDAP)
|
|
28
|
+
- All user input parameterized or escaped before use in queries
|
|
29
|
+
- No string concatenation in SQL/NoSQL queries
|
|
30
|
+
- No `eval()`, `exec()`, `shell=True`, or `child_process.exec()` with user input
|
|
31
|
+
- ORM/query builder used instead of raw queries where possible
|
|
32
|
+
|
|
33
|
+
### Broken Authentication
|
|
34
|
+
- Passwords hashed with bcrypt/argon2 (never MD5/SHA1)
|
|
35
|
+
- Session tokens are random, long, and expire
|
|
36
|
+
- MFA available for sensitive operations
|
|
37
|
+
- No credentials in URLs, logs, or error messages
|
|
38
|
+
|
|
39
|
+
### Sensitive Data Exposure
|
|
40
|
+
- No secrets in code, config files, or git history (.env, API keys, tokens)
|
|
41
|
+
- Data encrypted at rest (AES-256) and in transit (TLS)
|
|
42
|
+
- Sensitive fields excluded from logs and API responses
|
|
43
|
+
- PII handled according to data classification
|
|
44
|
+
|
|
45
|
+
### XSS (Cross-Site Scripting)
|
|
46
|
+
- All output HTML-encoded by default (React JSX handles this)
|
|
47
|
+
- No `dangerouslySetInnerHTML` or `v-html` without sanitization
|
|
48
|
+
- Content-Security-Policy headers set
|
|
49
|
+
- User input never inserted into `<script>` tags or event handlers
|
|
50
|
+
|
|
51
|
+
### Broken Access Control
|
|
52
|
+
- Every API endpoint checks authentication AND authorization
|
|
53
|
+
- No direct object references without ownership verification
|
|
54
|
+
- CORS configured to specific allowed origins (not `*`)
|
|
55
|
+
- Rate limiting on auth and sensitive endpoints
|
|
56
|
+
|
|
57
|
+
### Security Misconfiguration
|
|
58
|
+
- No default credentials or debug modes in production
|
|
59
|
+
- Error messages don't leak stack traces or internal details
|
|
60
|
+
- HTTP security headers set (HSTS, X-Frame-Options, X-Content-Type-Options)
|
|
61
|
+
- Unnecessary features and endpoints disabled
|
|
62
|
+
|
|
63
|
+
### CSRF (Cross-Site Request Forgery)
|
|
64
|
+
- State-changing requests use CSRF tokens or SameSite cookies
|
|
65
|
+
- POST/PUT/DELETE endpoints reject requests without valid tokens
|
|
66
|
+
|
|
67
|
+
## Dependency Check
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
# Node.js
|
|
71
|
+
npm audit
|
|
72
|
+
# Check for known vulnerabilities
|
|
73
|
+
npx audit-ci --moderate
|
|
74
|
+
|
|
75
|
+
# Python
|
|
76
|
+
pip-audit
|
|
77
|
+
safety check
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Flag: outdated packages with known CVEs, unmaintained dependencies, packages with suspiciously few downloads or recent ownership transfers.
|
|
81
|
+
|
|
82
|
+
## Secrets Scan
|
|
83
|
+
|
|
84
|
+
Look for accidentally committed secrets:
|
|
85
|
+
- API keys, tokens, passwords in source files
|
|
86
|
+
- `.env` files not in `.gitignore`
|
|
87
|
+
- Hardcoded connection strings
|
|
88
|
+
- Private keys or certificates
|
|
89
|
+
|
|
90
|
+
## Input Validation Rules
|
|
91
|
+
|
|
92
|
+
- Validate at system boundaries (API endpoints, form handlers, file uploads)
|
|
93
|
+
- Reject unexpected input types and sizes
|
|
94
|
+
- Whitelist over blacklist
|
|
95
|
+
- Validate on the server, never trust client-only validation
|
|
96
|
+
|
|
97
|
+
## When Reporting Issues
|
|
98
|
+
|
|
99
|
+
Classify findings:
|
|
100
|
+
- **Critical**: Exploitable now, data at risk (injection, auth bypass, exposed secrets)
|
|
101
|
+
- **High**: Exploitable with effort (XSS, CSRF, broken access control)
|
|
102
|
+
- **Medium**: Defense-in-depth gap (missing headers, weak config)
|
|
103
|
+
- **Low**: Best practice deviation (could become a problem later)
|
|
104
|
+
|
|
105
|
+
Always provide the fix, not just the finding.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scheduler-autofix-select.test.cjs — unit tests for selectAutoFixTargets.
|
|
3
|
+
*
|
|
4
|
+
* Run: timeout 120 node --test src/main/__tests__/scheduler-autofix-select.test.cjs
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const { test } = require('node:test');
|
|
10
|
+
const assert = require('node:assert/strict');
|
|
11
|
+
const { selectAutoFixTargets } = require('../scheduler.cjs');
|
|
12
|
+
|
|
13
|
+
const noSiblingOnDisk = () => false;
|
|
14
|
+
|
|
15
|
+
function makeJob(overrides = {}) {
|
|
16
|
+
return {
|
|
17
|
+
slug: '05-my-feature',
|
|
18
|
+
status: 'needs_review',
|
|
19
|
+
runId: '2026-06-16T10-00-00-000Z',
|
|
20
|
+
parallelGroup: 5,
|
|
21
|
+
...overrides,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test('selects a fresh needs_review job', () => {
|
|
26
|
+
const jobs = [makeJob()];
|
|
27
|
+
const result = selectAutoFixTargets(jobs, { fixSlugExists: noSiblingOnDisk });
|
|
28
|
+
assert.strictEqual(result.length, 1);
|
|
29
|
+
assert.strictEqual(result[0].slug, '05-my-feature');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('excludes job with autoFixAttempted: true', () => {
|
|
33
|
+
const jobs = [makeJob({ autoFixAttempted: true })];
|
|
34
|
+
const result = selectAutoFixTargets(jobs, { fixSlugExists: noSiblingOnDisk });
|
|
35
|
+
assert.strictEqual(result.length, 0);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('excludes a fix-plan slug (05-fix-foo)', () => {
|
|
39
|
+
const jobs = [makeJob({ slug: '05-fix-foo' })];
|
|
40
|
+
const result = selectAutoFixTargets(jobs, { fixSlugExists: noSiblingOnDisk });
|
|
41
|
+
assert.strictEqual(result.length, 0);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('excludes a failed job', () => {
|
|
45
|
+
const jobs = [makeJob({ status: 'failed' })];
|
|
46
|
+
const result = selectAutoFixTargets(jobs, { fixSlugExists: noSiblingOnDisk });
|
|
47
|
+
assert.strictEqual(result.length, 0);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('excludes a completed job', () => {
|
|
51
|
+
const jobs = [makeJob({ status: 'completed' })];
|
|
52
|
+
const result = selectAutoFixTargets(jobs, { fixSlugExists: noSiblingOnDisk });
|
|
53
|
+
assert.strictEqual(result.length, 0);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('excludes a job missing runId', () => {
|
|
57
|
+
const jobs = [makeJob({ runId: null })];
|
|
58
|
+
const result = selectAutoFixTargets(jobs, { fixSlugExists: noSiblingOnDisk });
|
|
59
|
+
assert.strictEqual(result.length, 0);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('excludes when fix sibling exists on disk', () => {
|
|
63
|
+
const jobs = [makeJob()];
|
|
64
|
+
// fixSlug = '05-fix-my-feature'
|
|
65
|
+
const result = selectAutoFixTargets(jobs, { fixSlugExists: (s) => s === '05-fix-my-feature' });
|
|
66
|
+
assert.strictEqual(result.length, 0);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('excludes when fix sibling already in the queue', () => {
|
|
70
|
+
const sibling = { slug: '05-fix-my-feature', status: 'pending', runId: null };
|
|
71
|
+
const jobs = [makeJob(), sibling];
|
|
72
|
+
const result = selectAutoFixTargets(jobs, { fixSlugExists: noSiblingOnDisk });
|
|
73
|
+
assert.strictEqual(result.length, 0);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('fixSlug uses padded parallelGroup and strips leading digits from slug', () => {
|
|
77
|
+
const jobs = [makeJob({ slug: '07-some-task', parallelGroup: 7 })];
|
|
78
|
+
// Expected fixSlug: '07-fix-some-task'
|
|
79
|
+
const seen = [];
|
|
80
|
+
selectAutoFixTargets(jobs, {
|
|
81
|
+
fixSlugExists: (s) => { seen.push(s); return false; },
|
|
82
|
+
});
|
|
83
|
+
assert.strictEqual(seen[0], '07-fix-some-task');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('defaults parallelGroup to 99 when absent', () => {
|
|
87
|
+
const jobs = [makeJob({ slug: '05-my-feature', parallelGroup: undefined })];
|
|
88
|
+
const seen = [];
|
|
89
|
+
selectAutoFixTargets(jobs, {
|
|
90
|
+
fixSlugExists: (s) => { seen.push(s); return false; },
|
|
91
|
+
});
|
|
92
|
+
assert.strictEqual(seen[0], '99-fix-my-feature');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
console.log('scheduler-autofix-select tests: PASS');
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scheduler-autopromote.test.cjs — unit tests for isPromotableOriginal.
|
|
3
|
+
*
|
|
4
|
+
* Run: timeout 120 node --test src/main/__tests__/scheduler-autopromote.test.cjs
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const { test } = require('node:test');
|
|
10
|
+
const assert = require('node:assert/strict');
|
|
11
|
+
const { isPromotableOriginal } = require('../scheduler.cjs');
|
|
12
|
+
|
|
13
|
+
test('isPromotableOriginal: failed → true', () => {
|
|
14
|
+
assert.strictEqual(isPromotableOriginal('failed'), true);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('isPromotableOriginal: needs_review → true', () => {
|
|
18
|
+
assert.strictEqual(isPromotableOriginal('needs_review'), true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('isPromotableOriginal: completed → false', () => {
|
|
22
|
+
assert.strictEqual(isPromotableOriginal('completed'), false);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('isPromotableOriginal: running → false', () => {
|
|
26
|
+
assert.strictEqual(isPromotableOriginal('running'), false);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('isPromotableOriginal: pending → false', () => {
|
|
30
|
+
assert.strictEqual(isPromotableOriginal('pending'), false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
console.log('scheduler-autopromote tests: PASS');
|
package/src/main/hives.cjs
CHANGED
|
@@ -1,29 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* hives.cjs —
|
|
2
|
+
* hives.cjs — subagent recipe templates ("Hives").
|
|
3
3
|
*
|
|
4
|
-
* A
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* A Recipe is a named sequence of steps, each referencing an agent by name
|
|
5
|
+
* from `~/.claude/agents/<agentName>.md`. Shape:
|
|
6
|
+
* { slug, name, description, brief?, steps: [{ agentName, note? }] }
|
|
7
|
+
*
|
|
8
|
+
* Legacy hives (old shape: `roles: [{ label, prompt }]`) are migrated on
|
|
9
|
+
* first read: each role is materialized as an agent .md and replaced by a
|
|
10
|
+
* step referencing it. Migration is idempotent and permanent (re-saved).
|
|
8
11
|
*
|
|
9
12
|
* Storage: `~/.claude/session-manager/hives/<slug>.json`
|
|
10
13
|
* - slug must match SLUG_RE: /^[a-z0-9-_]{1,64}$/
|
|
11
|
-
* - up to 32
|
|
14
|
+
* - up to 32 steps per recipe
|
|
12
15
|
* - per-field byte caps mirrored in the inline zod schemas below
|
|
13
16
|
*
|
|
14
|
-
* IPC namespace:
|
|
15
|
-
* - hives:list -> { hives:
|
|
16
|
-
* - hives:get -> { hive:
|
|
17
|
+
* IPC namespace (channel names kept for backwards compat):
|
|
18
|
+
* - hives:list -> { hives: Recipe[], error: string | null }
|
|
19
|
+
* - hives:get -> { hive: Recipe | null, error: string | null }
|
|
17
20
|
* - hives:save -> { ok: boolean, error: string | null }
|
|
18
21
|
* - hives:delete -> { ok: boolean, error: string | null }
|
|
19
22
|
*
|
|
20
|
-
* All mutations go through config.cjs::writeJson (atomic
|
|
21
|
-
* config.cjs::validatePath (allowedRoots = home dir).
|
|
22
|
-
*
|
|
23
|
-
* Default hives (Code review / Build feature / Bug hunt) ship in the renderer
|
|
24
|
-
* (src/renderer/lib/defaultHives.ts) and are NOT writable to disk — they exist
|
|
25
|
-
* only as in-memory starter examples so a fresh install has content. The IPC
|
|
26
|
-
* layer only sees user-saved hives.
|
|
23
|
+
* All mutations go through config.cjs::writeJson / writeTextAtomic (atomic
|
|
24
|
+
* tmp+rename) and config.cjs::validatePath (allowedRoots = home dir).
|
|
25
|
+
* Never raw fs.writeFile.
|
|
27
26
|
*/
|
|
28
27
|
|
|
29
28
|
'use strict';
|
|
@@ -37,34 +36,35 @@ const config = require('./config.cjs');
|
|
|
37
36
|
|
|
38
37
|
// ──────────────────────────────────────────── caps
|
|
39
38
|
const SLUG_RE = /^[a-z0-9-_]{1,64}$/;
|
|
39
|
+
const AGENT_NAME_RE = /^[a-z0-9-_]{1,64}$/;
|
|
40
40
|
const MAX_NAME_LEN = 128;
|
|
41
41
|
const MAX_DESC_LEN = 2048;
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
const MAX_ROLES = 32;
|
|
42
|
+
const MAX_BRIEF_LEN = 8 * 1024;
|
|
43
|
+
const MAX_NOTE_LEN = 2048;
|
|
44
|
+
const MAX_STEPS = 32;
|
|
46
45
|
|
|
47
46
|
// ──────────────────────────────────────────── inline zod schemas
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
const recipeStepSchema = z.object({
|
|
48
|
+
agentName: z.string().regex(AGENT_NAME_RE),
|
|
49
|
+
note: z.string().max(MAX_NOTE_LEN).optional(),
|
|
51
50
|
}).strict();
|
|
52
51
|
|
|
53
|
-
const
|
|
52
|
+
const recipeSchema = z.object({
|
|
54
53
|
slug: z.string().regex(SLUG_RE),
|
|
55
54
|
name: z.string().min(1).max(MAX_NAME_LEN),
|
|
56
55
|
description: z.string().max(MAX_DESC_LEN).default(''),
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
brief: z.string().max(MAX_BRIEF_LEN).optional(),
|
|
57
|
+
steps: z.array(recipeStepSchema).min(1).max(MAX_STEPS),
|
|
59
58
|
}).strict();
|
|
60
59
|
|
|
61
60
|
const slugPayload = z.object({
|
|
62
61
|
slug: z.string().regex(SLUG_RE),
|
|
63
62
|
}).strict();
|
|
64
63
|
|
|
64
|
+
// Field name "hive" kept for IPC channel backwards compatibility.
|
|
65
65
|
const savePayload = z.object({
|
|
66
66
|
slug: z.string().regex(SLUG_RE),
|
|
67
|
-
hive:
|
|
67
|
+
hive: recipeSchema,
|
|
68
68
|
}).strict();
|
|
69
69
|
|
|
70
70
|
// ──────────────────────────────────────────── paths
|
|
@@ -72,6 +72,10 @@ function rootDir() {
|
|
|
72
72
|
return path.join(os.homedir(), '.claude', 'session-manager', 'hives');
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
function agentsDir() {
|
|
76
|
+
return path.join(os.homedir(), '.claude', 'agents');
|
|
77
|
+
}
|
|
78
|
+
|
|
75
79
|
function hivePath(slug) {
|
|
76
80
|
if (!SLUG_RE.test(slug)) {
|
|
77
81
|
throw new Error(`invalid hive slug (must match ${SLUG_RE.source})`);
|
|
@@ -83,6 +87,58 @@ async function ensureRoot() {
|
|
|
83
87
|
await fsp.mkdir(rootDir(), { recursive: true });
|
|
84
88
|
}
|
|
85
89
|
|
|
90
|
+
// ──────────────────────────────────────────── helpers
|
|
91
|
+
function kebabCase(s) {
|
|
92
|
+
return s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 64) || 'agent';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ──────────────────────────────────────────── legacy migration
|
|
96
|
+
async function migrateLegacyHive(slug, raw) {
|
|
97
|
+
const adir = agentsDir();
|
|
98
|
+
await fsp.mkdir(adir, { recursive: true });
|
|
99
|
+
|
|
100
|
+
const usedNames = new Set();
|
|
101
|
+
const steps = [];
|
|
102
|
+
|
|
103
|
+
for (const role of (raw.roles ?? [])) {
|
|
104
|
+
let base = kebabCase(role.label ?? 'agent');
|
|
105
|
+
let agentName = base;
|
|
106
|
+
let suffix = 2;
|
|
107
|
+
while (usedNames.has(agentName)) {
|
|
108
|
+
agentName = `${base.slice(0, 62)}-${suffix++}`;
|
|
109
|
+
}
|
|
110
|
+
usedNames.add(agentName);
|
|
111
|
+
|
|
112
|
+
const agentFilePath = path.join(adir, `${agentName}.md`);
|
|
113
|
+
// validatePath ensures we stay within allowedRoots (home dir).
|
|
114
|
+
const validPath = config.validatePath(agentFilePath);
|
|
115
|
+
|
|
116
|
+
let exists = false;
|
|
117
|
+
try { await fsp.stat(validPath); exists = true; } catch { /* ENOENT = not yet created */ }
|
|
118
|
+
|
|
119
|
+
if (!exists) {
|
|
120
|
+
const descLine = (role.prompt ?? '').replace(/[\r\n]+/g, ' ').slice(0, 200);
|
|
121
|
+
const content = `---\nname: ${role.label}\ndescription: ${descLine}\n---\n\n${role.prompt ?? ''}\n`;
|
|
122
|
+
await config.writeTextAtomic(validPath, content);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
steps.push({ agentName });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const recipe = {
|
|
129
|
+
slug,
|
|
130
|
+
name: raw.name,
|
|
131
|
+
description: raw.description ?? '',
|
|
132
|
+
steps,
|
|
133
|
+
...(raw.defaultPlan ? { brief: raw.defaultPlan } : {}),
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// Persist migrated shape permanently so subsequent reads skip migration.
|
|
137
|
+
await saveHive(slug, recipe);
|
|
138
|
+
|
|
139
|
+
return { hive: recipe, error: null };
|
|
140
|
+
}
|
|
141
|
+
|
|
86
142
|
// ──────────────────────────────────────────── core ops
|
|
87
143
|
async function listHives() {
|
|
88
144
|
try {
|
|
@@ -118,13 +174,20 @@ async function readHive(slug) {
|
|
|
118
174
|
const r = await config.readJson(abs);
|
|
119
175
|
if (!r || !r.exists) return { hive: null, error: null };
|
|
120
176
|
if (r.parseError) return { hive: null, error: `parse error: ${r.parseError}` };
|
|
177
|
+
|
|
178
|
+
const raw = r.data;
|
|
179
|
+
|
|
180
|
+
// Legacy migration: old shape has `roles` array instead of `steps`.
|
|
181
|
+
if (raw && Array.isArray(raw.roles) && !Array.isArray(raw.steps)) {
|
|
182
|
+
return migrateLegacyHive(slug, raw);
|
|
183
|
+
}
|
|
184
|
+
|
|
121
185
|
// Re-validate stored shape so a hand-edited file can't smuggle in bad data.
|
|
122
|
-
const parsed =
|
|
186
|
+
const parsed = recipeSchema.safeParse(raw);
|
|
123
187
|
if (!parsed.success) {
|
|
124
|
-
return { hive: null, error: `invalid
|
|
188
|
+
return { hive: null, error: `invalid recipe on disk: ${parsed.error.issues[0]?.message ?? 'unknown'}` };
|
|
125
189
|
}
|
|
126
|
-
// Trust the file's own slug only if it matches the filename;
|
|
127
|
-
// them to agree (filename wins — that's the storage key).
|
|
190
|
+
// Trust the file's own slug only if it matches the filename; filename wins.
|
|
128
191
|
const hive = { ...parsed.data, slug };
|
|
129
192
|
return { hive, error: null };
|
|
130
193
|
}
|
|
@@ -139,8 +202,7 @@ async function getHive(slug) {
|
|
|
139
202
|
|
|
140
203
|
async function saveHive(slug, hive) {
|
|
141
204
|
try {
|
|
142
|
-
// Body slug must match path slug.
|
|
143
|
-
// can't accidentally overwrite the wrong file by tampering with `slug`.
|
|
205
|
+
// Body slug must match path slug.
|
|
144
206
|
if (hive.slug !== slug) {
|
|
145
207
|
return { ok: false, error: `slug mismatch: payload slug "${hive.slug}" != path "${slug}"` };
|
|
146
208
|
}
|
|
@@ -148,10 +210,9 @@ async function saveHive(slug, hive) {
|
|
|
148
210
|
const abs = hivePath(slug);
|
|
149
211
|
const out = {
|
|
150
212
|
...hive,
|
|
151
|
-
// Strip undefined for clean JSON on disk.
|
|
152
213
|
description: hive.description ?? '',
|
|
153
214
|
};
|
|
154
|
-
if (out.
|
|
215
|
+
if (out.brief === undefined || out.brief === '') delete out.brief;
|
|
155
216
|
const result = await config.writeJson(abs, out);
|
|
156
217
|
if (!result || !result.ok) {
|
|
157
218
|
return { ok: false, error: result?.error ?? 'write failed' };
|
|
@@ -178,7 +239,6 @@ async function deleteHive(slug) {
|
|
|
178
239
|
await fsp.unlink(real);
|
|
179
240
|
} catch (e) {
|
|
180
241
|
if (e.code === 'ENOENT') {
|
|
181
|
-
// Treat missing as success — idempotent delete, same as fs unlink ENOENT.
|
|
182
242
|
return { ok: true, error: null };
|
|
183
243
|
}
|
|
184
244
|
return { ok: false, error: e.message };
|