claude-code-session-manager 0.74.0 → 0.75.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/AgentLibrary-SpzptNyq.js +3 -0
- package/dist/assets/DataModel-C4M9xVLC.js +1 -0
- package/dist/assets/{History-ShwkUTny.js → History-92FYrZKS.js} +2 -2
- package/dist/assets/{Hooks-Dc5y1rZH.js → Hooks-DlnavO1l.js} +2 -2
- package/dist/assets/HostBilko-CKI7jt1z.js +1 -0
- package/dist/assets/{Library-BOL6GqFX.js → Library-Brhxe7xa.js} +1 -1
- package/dist/assets/ListDetail-DmIweYTN.js +1 -0
- package/dist/assets/MarkdownEditor-DA4xqWWi.js +1 -0
- package/dist/assets/{McpServers-DuOTxmc4.js → McpServers-DWXQGzVc.js} +2 -2
- package/dist/assets/{Memory-BYJHDz7W.js → Memory-HCx9Arao.js} +4 -4
- package/dist/assets/Panel-B20C6kN8.js +1 -0
- package/dist/assets/{Permissions-DhYERDVR.js → Permissions-C4sPgLpA.js} +3 -3
- package/dist/assets/Plugins-CaRenCBg.js +2 -0
- package/dist/assets/ProvenanceBadge-DfrXzTeh.js +1 -0
- package/dist/assets/SaveBar-BmpUx2Yh.js +1 -0
- package/dist/assets/{Scheduler-Bu6kSkl3.js → Scheduler-xUD8pQ6q.js} +6 -6
- package/dist/assets/ScopeSwitcher-DVlKMck5.js +1 -0
- package/dist/assets/Settings-C457mxOK.js +3 -0
- package/dist/assets/{SkillReferenceGraph-rKaTHmOg.js → SkillReferenceGraph-D3dDc5X7.js} +1 -1
- package/dist/assets/{Skills-BHlH2uG5.js → Skills-BNDlbftC.js} +2 -2
- package/dist/assets/SystemPrompt-d-ykNRec.js +1 -0
- package/dist/assets/TagLibrary-D3JWkQK-.js +1 -0
- package/dist/assets/{TiptapBody-XoQHiqfF.js → TiptapBody-B1Ox6D6E.js} +1 -1
- package/dist/assets/Toggle-CaYw6Lrj.js +1 -0
- package/dist/assets/{index-Cs_Lq9-w.js → index-CEeLxNCz.js} +659 -655
- package/dist/assets/{index-D9C8N3pn.css → index-Dbo9R4e0.css} +1 -1
- package/dist/assets/{settingsSchema-BTDJcu2x.js → settingsSchema-DUkd45M5.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +3 -1
- package/scripts/scheduler-mcp-server.cjs +471 -0
- package/src/main/__tests__/seedAgentPersonas.test.cjs +105 -0
- package/src/main/__tests__/seedSchedulerMcp.test.cjs +145 -0
- package/src/main/__tests__/seedStatus.test.cjs +100 -0
- package/src/main/agentLibrary.cjs +43 -0
- package/src/main/index.cjs +42 -1
- package/src/main/ipcSchemas.cjs +25 -0
- package/src/main/lib/__tests__/delegationReadiness.test.cjs +185 -0
- package/src/main/lib/__tests__/epicDelegationStats.test.cjs +137 -0
- package/src/main/lib/delegationReadiness.cjs +141 -0
- package/src/main/lib/epicDelegationStats.cjs +124 -0
- package/src/main/lib/promptSessionSchema.cjs +1 -1
- package/src/main/seedAgentPersonas.cjs +92 -0
- package/src/main/seedDevPlugin.cjs +14 -2
- package/src/main/seedSchedulerMcp.cjs +148 -0
- package/src/main/seedStatus.cjs +59 -0
- package/src/preload/api.d.ts +40 -1
- package/src/preload/index.cjs +7 -0
- package/src/seed/agents/architect.md +64 -0
- package/src/seed/agents/dev-lead.md +42 -0
- package/src/seed/prompts/code-review/ac-coverage-check.md +8 -0
- package/src/seed/prompts/code-review/correctness-only.md +8 -0
- package/src/seed/prompts/code-review/full-spectrum-high.md +8 -0
- package/src/seed/prompts/code-review/hallucination-check.md +8 -0
- package/src/seed/prompts/code-review/public-api-compat.md +8 -0
- package/src/seed/prompts/code-review/readability-naming.md +8 -0
- package/src/seed/prompts/debugging/bug-as-failing-test.md +8 -0
- package/src/seed/prompts/debugging/git-bisect-regression.md +8 -0
- package/src/seed/prompts/debugging/instrument-intermittent-bug.md +8 -0
- package/src/seed/prompts/debugging/localize-pipeline-failure.md +8 -0
- package/src/seed/prompts/debugging/reproduce-then-diagnose.md +8 -0
- package/src/seed/prompts/documentation/adr-from-change.md +8 -0
- package/src/seed/prompts/documentation/module-readme.md +8 -0
- package/src/seed/prompts/documentation/onboarding-plan.md +8 -0
- package/src/seed/prompts/documentation/refresh-claude-md.md +8 -0
- package/src/seed/prompts/documentation/tsdoc-public-exports.md +8 -0
- package/src/seed/prompts/git-pr/conventional-commit.md +8 -0
- package/src/seed/prompts/git-pr/draft-pr-title-body.md +8 -0
- package/src/seed/prompts/git-pr/pre-commit-safety-sweep.md +8 -0
- package/src/seed/prompts/git-pr/release-notes-block.md +8 -0
- package/src/seed/prompts/git-pr/split-large-pr.md +8 -0
- package/src/seed/prompts/performance/bundle-startup-audit.md +8 -0
- package/src/seed/prompts/performance/complexity-audit.md +8 -0
- package/src/seed/prompts/performance/cpu-profile-hot-path.md +8 -0
- package/src/seed/prompts/performance/db-query-plan-review.md +8 -0
- package/src/seed/prompts/performance/memory-leak-hunt.md +8 -0
- package/src/seed/prompts/qa/api-contract-tests.md +8 -0
- package/src/seed/prompts/qa/e2e-critical-path.md +8 -0
- package/src/seed/prompts/qa/failing-test-for-bug.md +8 -0
- package/src/seed/prompts/qa/find-missing-test-coverage.md +8 -0
- package/src/seed/prompts/qa/stabilize-flaky-test.md +8 -0
- package/src/seed/prompts/qa/tdd-red-first.md +8 -0
- package/src/seed/prompts/qa/visual-regression-review.md +8 -0
- package/src/seed/prompts/qa/wcag-axe-scan.md +8 -0
- package/src/seed/prompts/refactoring/dead-code-sweep.md +8 -0
- package/src/seed/prompts/refactoring/extract-duplicated-pattern.md +8 -0
- package/src/seed/prompts/refactoring/modernize-legacy-file.md +8 -0
- package/src/seed/prompts/refactoring/reduce-cyclomatic-complexity.md +8 -0
- package/src/seed/prompts/refactoring/tighten-module-boundaries.md +8 -0
- package/src/seed/prompts/security/authz-audit.md +8 -0
- package/src/seed/prompts/security/crypto-correctness.md +8 -0
- package/src/seed/prompts/security/cwe-top-25-hunt.md +8 -0
- package/src/seed/prompts/security/dependency-audit.md +8 -0
- package/src/seed/prompts/security/ipc-boundary-hardening.md +8 -0
- package/src/seed/prompts/security/owasp-top-10-staged-diff.md +8 -0
- package/src/seed/prompts/security/secret-credential-scan.md +8 -0
- package/dist/assets/AgentLibrary-CSnWV6uv.js +0 -3
- package/dist/assets/HostBilko-evd1hD8q.js +0 -1
- package/dist/assets/ListDetail-BPJb2et3.js +0 -1
- package/dist/assets/MarkdownEditor-CACsHVuf.js +0 -1
- package/dist/assets/Panel-n-p8OQLc.js +0 -1
- package/dist/assets/Plugins-D1ckVgo3.js +0 -2
- package/dist/assets/ProvenanceBadge-ByTjDwAX.js +0 -1
- package/dist/assets/SaveBar-_ORUCmZw.js +0 -1
- package/dist/assets/ScopeSwitcher-Css6Sg7Y.js +0 -1
- package/dist/assets/Settings-DlyJjexf.js +0 -3
- package/dist/assets/SystemPrompt-DsgF8xsl.js +0 -1
- package/dist/assets/TagLibrary-Q5QsKMes.js +0 -1
- package/dist/assets/Toggle-BJHhHQOq.js +0 -1
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* epicDelegationStats.test.cjs — unit tests for the derived-at-read-time
|
|
3
|
+
* "did this Epic delegate?" counters (prdsQueued from the Epic's own live
|
|
4
|
+
* PRD dir, inlineEdits from its interactive claude session transcript).
|
|
5
|
+
*
|
|
6
|
+
* Run: timeout 120 npx vitest run src/main/lib/__tests__/epicDelegationStats.test.cjs
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
import { test, expect, afterEach } from 'vitest';
|
|
12
|
+
const fs = require('node:fs');
|
|
13
|
+
const os = require('node:os');
|
|
14
|
+
const path = require('node:path');
|
|
15
|
+
const {
|
|
16
|
+
computeEpicDelegationStats,
|
|
17
|
+
countPrdsQueued,
|
|
18
|
+
countInlineEdits,
|
|
19
|
+
isApplicationSourcePath,
|
|
20
|
+
} = require('../epicDelegationStats.cjs');
|
|
21
|
+
const { encodeCwd } = require('../encodeCwd.cjs');
|
|
22
|
+
|
|
23
|
+
const tmpDirs = [];
|
|
24
|
+
function makeTmpCwd() {
|
|
25
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'epic-delegation-'));
|
|
26
|
+
tmpDirs.push(dir);
|
|
27
|
+
return dir;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
while (tmpDirs.length) {
|
|
32
|
+
fs.rmSync(tmpDirs.pop(), { recursive: true, force: true });
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
function makeTmpHome() {
|
|
37
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'epic-delegation-home-'));
|
|
38
|
+
tmpDirs.push(dir);
|
|
39
|
+
return dir;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function writeTranscript(homeDir, cwd, sessionId, lines) {
|
|
43
|
+
const dir = path.join(homeDir, '.claude', 'projects', encodeCwd(cwd));
|
|
44
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
45
|
+
const filePath = path.join(dir, `${sessionId}.jsonl`);
|
|
46
|
+
fs.writeFileSync(filePath, lines.map((l) => JSON.stringify(l)).join('\n') + '\n');
|
|
47
|
+
return filePath;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function toolUseLine(name, filePath) {
|
|
51
|
+
return {
|
|
52
|
+
type: 'assistant',
|
|
53
|
+
message: { content: [{ type: 'tool_use', name, id: 't1', input: { file_path: filePath } }] },
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
test('countPrdsQueued counts only .md files in the Epic own live prds dir', () => {
|
|
58
|
+
const cwd = makeTmpCwd();
|
|
59
|
+
const prdsDir = path.join(cwd, 'session-manager-operations', 'scheduler', 'epics', 'epic-1', 'prds');
|
|
60
|
+
fs.mkdirSync(prdsDir, { recursive: true });
|
|
61
|
+
fs.writeFileSync(path.join(prdsDir, '01-foo.md'), '# foo');
|
|
62
|
+
fs.writeFileSync(path.join(prdsDir, '02-bar.md'), '# bar');
|
|
63
|
+
fs.writeFileSync(path.join(prdsDir, '.reserved-03'), '');
|
|
64
|
+
expect(countPrdsQueued(cwd, 'epic-1')).toBe(2);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('countPrdsQueued returns 0 when the prds dir does not exist', () => {
|
|
68
|
+
const cwd = makeTmpCwd();
|
|
69
|
+
expect(countPrdsQueued(cwd, 'missing-epic')).toBe(0);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('isApplicationSourcePath matches src/scripts/plugins/bin, rejects everything else', () => {
|
|
73
|
+
const cwd = '/repo';
|
|
74
|
+
expect(isApplicationSourcePath(cwd, '/repo/src/main/index.cjs')).toBe(true);
|
|
75
|
+
expect(isApplicationSourcePath(cwd, '/repo/scripts/hooks/foo.cjs')).toBe(true);
|
|
76
|
+
expect(isApplicationSourcePath(cwd, '/repo/plugins/dev/skill.md')).toBe(true);
|
|
77
|
+
expect(isApplicationSourcePath(cwd, '/repo/bin/cli.cjs')).toBe(true);
|
|
78
|
+
expect(isApplicationSourcePath(cwd, '/repo/session-manager-operations/scheduler/epics/e1/prds/01.md')).toBe(false);
|
|
79
|
+
expect(isApplicationSourcePath(cwd, '/repo/README.md')).toBe(false);
|
|
80
|
+
expect(isApplicationSourcePath(cwd, '/outside/src/index.cjs')).toBe(false);
|
|
81
|
+
expect(isApplicationSourcePath(cwd, undefined)).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('countInlineEdits counts Write/Edit/NotebookEdit tool_use blocks under application source only', () => {
|
|
85
|
+
const cwd = makeTmpCwd();
|
|
86
|
+
const homeDir = makeTmpHome();
|
|
87
|
+
writeTranscript(homeDir, cwd, 'sess-1', [
|
|
88
|
+
toolUseLine('Write', path.join(cwd, 'src', 'main', 'foo.cjs')),
|
|
89
|
+
toolUseLine('Edit', path.join(cwd, 'src', 'main', 'foo.cjs')),
|
|
90
|
+
toolUseLine('Read', path.join(cwd, 'src', 'main', 'foo.cjs')),
|
|
91
|
+
toolUseLine('Write', path.join(cwd, 'session-manager-operations', 'scheduler', 'epics', 'e1', 'prds', '01.md')),
|
|
92
|
+
toolUseLine('NotebookEdit', path.join(cwd, 'scripts', 'nb.ipynb')),
|
|
93
|
+
]);
|
|
94
|
+
expect(countInlineEdits(cwd, 'sess-1', { homeDir })).toBe(3);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('countInlineEdits returns 0 when the transcript file does not exist', () => {
|
|
98
|
+
const cwd = makeTmpCwd();
|
|
99
|
+
const homeDir = makeTmpHome();
|
|
100
|
+
expect(countInlineEdits(cwd, 'no-such-session', { homeDir })).toBe(0);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('countInlineEdits returns 0 when claudeSessionId is missing', () => {
|
|
104
|
+
const cwd = makeTmpCwd();
|
|
105
|
+
expect(countInlineEdits(cwd, null)).toBe(0);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('countInlineEdits tolerates a malformed trailing line', () => {
|
|
109
|
+
const cwd = makeTmpCwd();
|
|
110
|
+
const homeDir = makeTmpHome();
|
|
111
|
+
const dir = path.join(homeDir, '.claude', 'projects', encodeCwd(cwd));
|
|
112
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
113
|
+
const filePath = path.join(dir, 'sess-2.jsonl');
|
|
114
|
+
fs.writeFileSync(
|
|
115
|
+
filePath,
|
|
116
|
+
JSON.stringify(toolUseLine('Write', path.join(cwd, 'src', 'a.ts'))) + '\n{not json\n',
|
|
117
|
+
);
|
|
118
|
+
expect(countInlineEdits(cwd, 'sess-2', { homeDir })).toBe(1);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('computeEpicDelegationStats combines both counters and never throws on missing state', () => {
|
|
122
|
+
const cwd = makeTmpCwd();
|
|
123
|
+
const homeDir = makeTmpHome();
|
|
124
|
+
expect(computeEpicDelegationStats(cwd, 'ghost-epic', 'ghost-session', { homeDir })).toEqual({
|
|
125
|
+
prdsQueued: 0,
|
|
126
|
+
inlineEdits: 0,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const prdsDir = path.join(cwd, 'session-manager-operations', 'scheduler', 'epics', 'epic-2', 'prds');
|
|
130
|
+
fs.mkdirSync(prdsDir, { recursive: true });
|
|
131
|
+
fs.writeFileSync(path.join(prdsDir, '01-foo.md'), '# foo');
|
|
132
|
+
writeTranscript(homeDir, cwd, 'sess-3', [toolUseLine('Edit', path.join(cwd, 'src', 'x.ts'))]);
|
|
133
|
+
expect(computeEpicDelegationStats(cwd, 'epic-2', 'sess-3', { homeDir })).toEqual({
|
|
134
|
+
prdsQueued: 1,
|
|
135
|
+
inlineEdits: 1,
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delegation-readiness probe — answers "can this project actually hand work
|
|
3
|
+
* to the scheduler?" as structured data instead of a silent tool-list gap.
|
|
4
|
+
*
|
|
5
|
+
* Why this exists: when the session-manager-scheduler MCP server isn't
|
|
6
|
+
* registered, `scheduler_create_prd` is simply absent from the agent's tool
|
|
7
|
+
* list — there's no error to catch, so an agent asked to delegate just
|
|
8
|
+
* implements inline instead. See scripts/install-scheduler-mcp-user-scope.sh's
|
|
9
|
+
* header for the incident this repeat-guards against (PRD 1024-1030).
|
|
10
|
+
*
|
|
11
|
+
* This module only computes the four checks below; surfacing them in the UI
|
|
12
|
+
* or the Epic's grounding line is a dependent PRD (delegation-readiness-ui).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('node:fs');
|
|
16
|
+
const path = require('node:path');
|
|
17
|
+
const os = require('node:os');
|
|
18
|
+
|
|
19
|
+
const SCHEDULER_MCP_NAME = 'session-manager-scheduler';
|
|
20
|
+
const DEV_PLUGIN_ENABLED_KEY = 'session-manager-dev@session-manager';
|
|
21
|
+
const SCHEDULER_MCP_SERVER_SCRIPT = path.resolve(__dirname, '..', '..', '..', 'scripts', 'scheduler-mcp-server.cjs');
|
|
22
|
+
|
|
23
|
+
/** Read + JSON.parse a file, returning `fallback` on any read/parse failure. */
|
|
24
|
+
function readJsonSafe(absPath, fallback) {
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(fs.readFileSync(absPath, 'utf8'));
|
|
27
|
+
} catch {
|
|
28
|
+
return fallback;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function checkSchedulerMcp({ cwd, homeDir }) {
|
|
33
|
+
const userConfig = readJsonSafe(path.join(homeDir, '.claude.json'), null);
|
|
34
|
+
const userHas = !!userConfig?.mcpServers?.[SCHEDULER_MCP_NAME];
|
|
35
|
+
|
|
36
|
+
const projectConfig = readJsonSafe(path.join(cwd, '.mcp.json'), null);
|
|
37
|
+
const projectHas = !!projectConfig?.mcpServers?.[SCHEDULER_MCP_NAME];
|
|
38
|
+
|
|
39
|
+
const ok = userHas || projectHas;
|
|
40
|
+
let detail;
|
|
41
|
+
if (userHas) detail = `${SCHEDULER_MCP_NAME} registered at user scope (~/.claude.json)`;
|
|
42
|
+
else if (projectHas) detail = `${SCHEDULER_MCP_NAME} registered at project scope (${cwd}/.mcp.json)`;
|
|
43
|
+
else detail = `no ${SCHEDULER_MCP_NAME} entry in ~/.claude.json or ${cwd}/.mcp.json`;
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
id: 'scheduler-mcp',
|
|
47
|
+
label: 'Scheduler MCP server registered',
|
|
48
|
+
ok,
|
|
49
|
+
detail,
|
|
50
|
+
fix: ok
|
|
51
|
+
? null
|
|
52
|
+
: `claude mcp add ${SCHEDULER_MCP_NAME} --scope user -- node ${SCHEDULER_MCP_SERVER_SCRIPT}`,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function checkDevPlugin({ homeDir }) {
|
|
57
|
+
const settings = readJsonSafe(path.join(homeDir, '.claude', 'settings.json'), null);
|
|
58
|
+
const ok = settings?.enabledPlugins?.[DEV_PLUGIN_ENABLED_KEY] === true;
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
id: 'dev-plugin',
|
|
62
|
+
label: 'session-manager-dev plugin enabled',
|
|
63
|
+
ok,
|
|
64
|
+
detail: ok
|
|
65
|
+
? `${DEV_PLUGIN_ENABLED_KEY} enabled in ~/.claude/settings.json`
|
|
66
|
+
: `${DEV_PLUGIN_ENABLED_KEY} missing from ~/.claude/settings.json enabledPlugins`,
|
|
67
|
+
fix: ok ? null : 'Enable in Plugins tab: session-manager-dev (Session Manager)',
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Personas resolve from BOTH scopes (agentLibrary.cjs: "project cwd wins
|
|
72
|
+
// over the global" one) — a project that only ever defines project-scoped
|
|
73
|
+
// overrides in `<cwd>/.claude/agents/` (this repo's own `.claude/agents/`
|
|
74
|
+
// is exactly that case) is a legitimately delegation-ready project, not a
|
|
75
|
+
// failing one, so both directories must be counted.
|
|
76
|
+
function countMdFiles(dir) {
|
|
77
|
+
try {
|
|
78
|
+
return fs.readdirSync(dir).filter((f) => f.endsWith('.md')).length;
|
|
79
|
+
} catch {
|
|
80
|
+
return 0;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function checkAgentPersonas({ cwd, homeDir }) {
|
|
85
|
+
const globalCount = countMdFiles(path.join(homeDir, '.claude', 'agents'));
|
|
86
|
+
const projectCount = countMdFiles(path.join(cwd, '.claude', 'agents'));
|
|
87
|
+
const count = globalCount + projectCount;
|
|
88
|
+
const ok = count > 0;
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
id: 'agent-personas',
|
|
92
|
+
label: 'At least one agent persona defined',
|
|
93
|
+
ok,
|
|
94
|
+
detail: ok
|
|
95
|
+
? `${count} persona(s) across ~/.claude/agents/ and ${cwd}/.claude/agents/`
|
|
96
|
+
: `no .md personas in ~/.claude/agents/ or ${cwd}/.claude/agents/`,
|
|
97
|
+
fix: ok ? null : 'Create a persona in the Agent Library tab',
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function checkPrdWriteGuard({ cwd }) {
|
|
102
|
+
const settings = readJsonSafe(path.join(cwd, '.claude', 'settings.json'), null);
|
|
103
|
+
const preToolUse = Array.isArray(settings?.hooks?.PreToolUse) ? settings.hooks.PreToolUse : [];
|
|
104
|
+
const ok = preToolUse.some((matcher) =>
|
|
105
|
+
Array.isArray(matcher?.hooks) &&
|
|
106
|
+
matcher.hooks.some((h) => typeof h?.command === 'string' && h.command.includes('guard-prd-writes'))
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
id: 'prd-write-guard',
|
|
111
|
+
label: 'PRD-write guard hook installed',
|
|
112
|
+
ok,
|
|
113
|
+
detail: ok
|
|
114
|
+
? `guard-prd-writes PreToolUse hook found in ${cwd}/.claude/settings.json`
|
|
115
|
+
: `no guard-prd-writes PreToolUse hook in ${cwd}/.claude/settings.json`,
|
|
116
|
+
fix: ok
|
|
117
|
+
? null
|
|
118
|
+
: `Add a PreToolUse hook to ${cwd}/.claude/settings.json matching Write|Edit|NotebookEdit that runs node scripts/hooks/guard-prd-writes.cjs`,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Runs all four delegation-readiness checks for `cwd`. Every filesystem read
|
|
124
|
+
* is wrapped (readJsonSafe / try-catch) so a missing or unparseable file
|
|
125
|
+
* yields ok:false with a detail, never a thrown exception.
|
|
126
|
+
*/
|
|
127
|
+
function checkDelegationReadiness({ cwd, homeDir = os.homedir() }) {
|
|
128
|
+
const checks = [
|
|
129
|
+
checkSchedulerMcp({ cwd, homeDir }),
|
|
130
|
+
checkDevPlugin({ homeDir }),
|
|
131
|
+
checkAgentPersonas({ cwd, homeDir }),
|
|
132
|
+
checkPrdWriteGuard({ cwd }),
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
ok: checks.every((c) => c.ok),
|
|
137
|
+
checks,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
module.exports = { checkDelegationReadiness };
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Derived-at-read-time counters answering "did this Epic delegate work into
|
|
5
|
+
* PRDs, or did the interactive session just edit application source
|
|
6
|
+
* inline?" — no new writer, no new namespace; both numbers are computed
|
|
7
|
+
* from state that already exists on disk (single-writer law,
|
|
8
|
+
* opsOwnership.cjs).
|
|
9
|
+
*
|
|
10
|
+
* prdsQueued reads the Epic's own live PRD dir
|
|
11
|
+
* (session-manager-operations/scheduler/epics/<epicId>/prds/) — the
|
|
12
|
+
* "prds-archived" sibling deliberately does NOT count here: this is a
|
|
13
|
+
* live-delegation signal, not a lifetime PRD count (that's epicPrds() in
|
|
14
|
+
* the renderer, sourced from the scheduler's own snapshot).
|
|
15
|
+
*
|
|
16
|
+
* inlineEdits reads the Epic's OWN interactive claude session transcript —
|
|
17
|
+
* ~/.claude/projects/<encodeCwd(cwd)>/<claudeSessionId>.jsonl, the same file
|
|
18
|
+
* transcripts.cjs already tails for the live feed — and counts Write/Edit/
|
|
19
|
+
* NotebookEdit tool_use blocks whose file_path resolves under this repo's
|
|
20
|
+
* src/, scripts/, plugins/, or bin/. That transcript already carries real
|
|
21
|
+
* tool_use blocks (classifyTranscriptLine.cjs); the durable
|
|
22
|
+
* session-manager-operations/prompt-sessions/transcripts/<epicId>.jsonl
|
|
23
|
+
* store does NOT (it holds only rendered {role, text} turns, verified by
|
|
24
|
+
* reading one), so it is not a usable source for this counter.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const fs = require('node:fs');
|
|
28
|
+
const os = require('node:os');
|
|
29
|
+
const path = require('node:path');
|
|
30
|
+
const { encodeCwd } = require('./encodeCwd.cjs');
|
|
31
|
+
const { classifyLine } = require('./classifyTranscriptLine.cjs');
|
|
32
|
+
|
|
33
|
+
// Same cap transcripts.cjs's readDelta uses for a single pass — bounds
|
|
34
|
+
// memory/time on a very large transcript instead of reading it whole.
|
|
35
|
+
const MAX_TRANSCRIPT_BYTES = 8 * 1024 * 1024;
|
|
36
|
+
|
|
37
|
+
const INLINE_EDIT_TOOL_NAMES = new Set(['Write', 'Edit', 'NotebookEdit']);
|
|
38
|
+
const APPLICATION_SOURCE_PREFIXES = ['src/', 'scripts/', 'plugins/', 'bin/'];
|
|
39
|
+
|
|
40
|
+
function countPrdsQueued(cwd, epicId, deps = {}) {
|
|
41
|
+
const readdirSync = deps.readdirSync || fs.readdirSync;
|
|
42
|
+
try {
|
|
43
|
+
const dir = path.join(cwd, 'session-manager-operations', 'scheduler', 'epics', epicId, 'prds');
|
|
44
|
+
return readdirSync(dir).filter((name) => name.endsWith('.md')).length;
|
|
45
|
+
} catch {
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** True when `filePath` (absolute or relative) resolves to a path under
|
|
51
|
+
* cwd's src/, scripts/, plugins/, or bin/ — never a dotfile/root config,
|
|
52
|
+
* never a path outside the repo entirely. */
|
|
53
|
+
function isApplicationSourcePath(cwd, filePath) {
|
|
54
|
+
if (typeof filePath !== 'string' || !filePath) return false;
|
|
55
|
+
const rel = path.relative(cwd, path.resolve(cwd, filePath)).split(path.sep).join('/');
|
|
56
|
+
if (rel.startsWith('..')) return false;
|
|
57
|
+
return APPLICATION_SOURCE_PREFIXES.some((prefix) => rel === prefix.slice(0, -1) || rel.startsWith(prefix));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Reads at most the trailing MAX_TRANSCRIPT_BYTES of `filePath`, dropping
|
|
61
|
+
* a truncated leading partial line when the read didn't start at byte 0. */
|
|
62
|
+
function readTranscriptTail(filePath, deps = {}) {
|
|
63
|
+
const statSync = deps.statSync || fs.statSync;
|
|
64
|
+
const openSync = deps.openSync || fs.openSync;
|
|
65
|
+
const readSync = deps.readSync || fs.readSync;
|
|
66
|
+
const closeSync = deps.closeSync || fs.closeSync;
|
|
67
|
+
|
|
68
|
+
const size = statSync(filePath).size;
|
|
69
|
+
const start = Math.max(0, size - MAX_TRANSCRIPT_BYTES);
|
|
70
|
+
const length = size - start;
|
|
71
|
+
const buffer = Buffer.alloc(length);
|
|
72
|
+
const fd = openSync(filePath, 'r');
|
|
73
|
+
try {
|
|
74
|
+
readSync(fd, buffer, 0, length, start);
|
|
75
|
+
} finally {
|
|
76
|
+
closeSync(fd);
|
|
77
|
+
}
|
|
78
|
+
const text = buffer.toString('utf8');
|
|
79
|
+
return start > 0 ? text.slice(text.indexOf('\n') + 1) : text;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function countInlineEdits(cwd, claudeSessionId, deps = {}) {
|
|
83
|
+
if (!claudeSessionId) return 0;
|
|
84
|
+
const readFn = deps.readTranscriptTail || readTranscriptTail;
|
|
85
|
+
const homeDir = deps.homeDir || os.homedir();
|
|
86
|
+
try {
|
|
87
|
+
const filePath = path.join(homeDir, '.claude', 'projects', encodeCwd(cwd), `${claudeSessionId}.jsonl`);
|
|
88
|
+
const text = readFn(filePath, deps);
|
|
89
|
+
let count = 0;
|
|
90
|
+
for (const line of text.split('\n')) {
|
|
91
|
+
const trimmed = line.trim();
|
|
92
|
+
if (!trimmed) continue;
|
|
93
|
+
let obj;
|
|
94
|
+
try {
|
|
95
|
+
obj = JSON.parse(trimmed);
|
|
96
|
+
} catch {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
for (const event of classifyLine(obj)) {
|
|
100
|
+
if (event.kind !== 'tool_use') continue;
|
|
101
|
+
const { name, input } = event.data || {};
|
|
102
|
+
if (!INLINE_EDIT_TOOL_NAMES.has(name)) continue;
|
|
103
|
+
if (isApplicationSourcePath(cwd, input?.file_path)) count += 1;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return count;
|
|
107
|
+
} catch {
|
|
108
|
+
return 0;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function computeEpicDelegationStats(cwd, epicId, claudeSessionId, deps = {}) {
|
|
113
|
+
return {
|
|
114
|
+
prdsQueued: countPrdsQueued(cwd, epicId, deps),
|
|
115
|
+
inlineEdits: countInlineEdits(cwd, claudeSessionId, deps),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = {
|
|
120
|
+
computeEpicDelegationStats,
|
|
121
|
+
countPrdsQueued,
|
|
122
|
+
countInlineEdits,
|
|
123
|
+
isApplicationSourcePath,
|
|
124
|
+
};
|
|
@@ -41,7 +41,7 @@ const EpicSourceSchema = z.object({
|
|
|
41
41
|
// data so the Epic's first turn can render a structured AIM briefing card
|
|
42
42
|
// instead of regex-parsing the flat string back apart.
|
|
43
43
|
const EpicIntakeSectionSchema = z.object({
|
|
44
|
-
kind: z.enum(['actor', 'injection', 'input', 'mission', 'goal', 'reference']),
|
|
44
|
+
kind: z.enum(['actor', 'persona-body', 'injection', 'input', 'mission', 'goal', 'reference']),
|
|
45
45
|
label: z.string(),
|
|
46
46
|
text: z.string(),
|
|
47
47
|
source: z.string().optional(),
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* First-boot seeder for the bundled Agent personas (Architect, Dev Lead).
|
|
3
|
+
*
|
|
4
|
+
* On a fresh install `~/.claude/agents/` is empty, so the Agent Library has
|
|
5
|
+
* nothing to offer — no persona to select for an Epic's Actor. This seeder
|
|
6
|
+
* copies the personas bundled at `src/seed/agents/*.md` into `~/.claude/agents/`
|
|
7
|
+
* on first boot, one file per persona, and NEVER overwrites a file that
|
|
8
|
+
* already exists there (hand-edited or user-authored personas win).
|
|
9
|
+
*
|
|
10
|
+
* Idempotent the same way as seedDevPlugin.cjs:
|
|
11
|
+
* 1. A marker file (`~/.claude/session-manager/.agent-personas-seeded`)
|
|
12
|
+
* records seed state. Once seeding SUCCEEDS we write `done` and never
|
|
13
|
+
* touch it again. A FAILED attempt only bumps an attempt counter and
|
|
14
|
+
* retries on the next few boots (bounded by MAX_ATTEMPTS), so a
|
|
15
|
+
* transient first-boot failure doesn't permanently skip the personas.
|
|
16
|
+
*
|
|
17
|
+
* Fire-and-forget: called post-window from index.cjs. Errors are logged,
|
|
18
|
+
* never thrown. Kill-switch: SM_SEED_AGENT_PERSONAS_DISABLE=1.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const fs = require('node:fs');
|
|
22
|
+
const path = require('node:path');
|
|
23
|
+
const os = require('node:os');
|
|
24
|
+
const { writeJsonSync } = require('./config.cjs');
|
|
25
|
+
|
|
26
|
+
const PERSONAS = ['architect', 'dev-lead'];
|
|
27
|
+
const MAX_ATTEMPTS = 3; // give a transient first-boot failure a few chances
|
|
28
|
+
|
|
29
|
+
function bundledSourceDir() {
|
|
30
|
+
return path.join(__dirname, '..', 'seed', 'agents');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function destAgentsDir() {
|
|
34
|
+
return path.join(os.homedir(), '.claude', 'agents');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function markerPath() {
|
|
38
|
+
return path.join(os.homedir(), '.claude', 'session-manager', '.agent-personas-seeded');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Read the marker → { done:boolean, attempts:number }. Absent = fresh. */
|
|
42
|
+
function readMarker() {
|
|
43
|
+
try {
|
|
44
|
+
const raw = fs.readFileSync(markerPath(), 'utf8').trim();
|
|
45
|
+
const m = JSON.parse(raw);
|
|
46
|
+
return { done: !!m.done, attempts: Number(m.attempts) || 0 };
|
|
47
|
+
} catch {
|
|
48
|
+
return { done: false, attempts: 0 };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function writeMarker(state) {
|
|
53
|
+
try {
|
|
54
|
+
writeJsonSync(markerPath(), { ...state, ts: new Date().toISOString() });
|
|
55
|
+
} catch (err) {
|
|
56
|
+
console.warn('[seedAgentPersonas] could not write marker:', err?.message ?? err);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function seedAgentPersonas({ logger = console, writeLog = () => {} } = {}) {
|
|
61
|
+
if (process.env.SM_SEED_AGENT_PERSONAS_DISABLE === '1') return;
|
|
62
|
+
const marker = readMarker();
|
|
63
|
+
if (marker.done) return; // succeeded before — leave it alone.
|
|
64
|
+
if (marker.attempts >= MAX_ATTEMPTS) return; // gave up — manual copy only.
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const srcDir = bundledSourceDir();
|
|
68
|
+
const destDir = destAgentsDir();
|
|
69
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
70
|
+
|
|
71
|
+
for (const name of PERSONAS) {
|
|
72
|
+
const src = path.join(srcDir, `${name}.md`);
|
|
73
|
+
const dest = path.join(destDir, `${name}.md`);
|
|
74
|
+
if (fs.existsSync(dest)) continue; // never overwrite an existing persona
|
|
75
|
+
fs.copyFileSync(src, dest);
|
|
76
|
+
logger.log?.(`[seedAgentPersonas] seeded ${name}.md`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
writeMarker({ done: true, attempts: marker.attempts });
|
|
80
|
+
} catch (err) {
|
|
81
|
+
logger.warn?.('[seedAgentPersonas] error:', err?.message ?? err);
|
|
82
|
+
writeLog({
|
|
83
|
+
scope: 'seed-agent-personas',
|
|
84
|
+
level: 'error',
|
|
85
|
+
message: 'seed error',
|
|
86
|
+
meta: { error: err?.message ?? String(err), attempt: marker.attempts + 1, maxAttempts: MAX_ATTEMPTS },
|
|
87
|
+
});
|
|
88
|
+
writeMarker({ done: false, attempts: marker.attempts + 1 });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = { seedAgentPersonas, markerPath, MAX_ATTEMPTS };
|
|
@@ -65,7 +65,7 @@ function writeMarker(state) {
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
async function seedDevPlugin({ logger = console } = {}) {
|
|
68
|
+
async function seedDevPlugin({ logger = console, writeLog = () => {} } = {}) {
|
|
69
69
|
if (process.env.SM_SEED_DEV_PLUGIN_DISABLE === '1') return;
|
|
70
70
|
const marker = readMarker();
|
|
71
71
|
if (marker.done) return; // succeeded before — leave it alone.
|
|
@@ -84,12 +84,24 @@ async function seedDevPlugin({ logger = console } = {}) {
|
|
|
84
84
|
} else {
|
|
85
85
|
// Failure: bump the attempt counter so the next boot can retry (bounded).
|
|
86
86
|
logger.warn?.(`[seedDevPlugin] install failed (exit ${r.exitCode})${r.error ? ` — ${r.error}` : ''}; attempt ${marker.attempts + 1}/${MAX_ATTEMPTS}`);
|
|
87
|
+
writeLog({
|
|
88
|
+
scope: 'seed-dev-plugin',
|
|
89
|
+
level: 'error',
|
|
90
|
+
message: 'install failed',
|
|
91
|
+
meta: { exitCode: r.exitCode, error: r.error, attempt: marker.attempts + 1, maxAttempts: MAX_ATTEMPTS },
|
|
92
|
+
});
|
|
87
93
|
writeMarker({ done: false, attempts: marker.attempts + 1 });
|
|
88
94
|
}
|
|
89
95
|
} catch (err) {
|
|
90
96
|
logger.warn?.('[seedDevPlugin] error:', err?.message ?? err);
|
|
97
|
+
writeLog({
|
|
98
|
+
scope: 'seed-dev-plugin',
|
|
99
|
+
level: 'error',
|
|
100
|
+
message: 'seed error',
|
|
101
|
+
meta: { error: err?.message ?? String(err), attempt: marker.attempts + 1, maxAttempts: MAX_ATTEMPTS },
|
|
102
|
+
});
|
|
91
103
|
writeMarker({ done: false, attempts: marker.attempts + 1 });
|
|
92
104
|
}
|
|
93
105
|
}
|
|
94
106
|
|
|
95
|
-
module.exports = { seedDevPlugin };
|
|
107
|
+
module.exports = { seedDevPlugin, markerPath, MAX_ATTEMPTS };
|