claude-code-session-manager 0.73.2 → 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-Dnx86P4n.js → History-92FYrZKS.js} +2 -2
- package/dist/assets/{Hooks-CAYlpqBp.js → Hooks-DlnavO1l.js} +2 -2
- package/dist/assets/HostBilko-CKI7jt1z.js +1 -0
- package/dist/assets/{Library-DVbXlboP.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-BxUk1aR3.js → McpServers-DWXQGzVc.js} +2 -2
- package/dist/assets/{Memory-DVbPQFZw.js → Memory-HCx9Arao.js} +4 -4
- package/dist/assets/Panel-B20C6kN8.js +1 -0
- package/dist/assets/{Permissions-CSLxAoTL.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-CrBVVXN1.js → Scheduler-xUD8pQ6q.js} +7 -7
- package/dist/assets/ScopeSwitcher-DVlKMck5.js +1 -0
- package/dist/assets/Settings-C457mxOK.js +3 -0
- package/dist/assets/{SkillReferenceGraph-B1DaeT2R.js → SkillReferenceGraph-D3dDc5X7.js} +1 -1
- package/dist/assets/{Skills-CSdxEC-f.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-4hzD0jjv.js → TiptapBody-B1Ox6D6E.js} +1 -1
- package/dist/assets/Toggle-CaYw6Lrj.js +1 -0
- package/dist/assets/{index-DJ6xdB0r.js → index-CEeLxNCz.js} +659 -655
- package/dist/assets/{index-D9C8N3pn.css → index-Dbo9R4e0.css} +1 -1
- package/dist/assets/{settingsSchema-DVblsXFp.js → settingsSchema-DUkd45M5.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +3 -1
- package/plugins/session-manager-dev/skills/memory-sanitation/SKILL.md +21 -1
- package/scripts/scheduler-mcp-server.cjs +471 -0
- package/src/main/__tests__/agentModelResolve.test.cjs +19 -0
- package/src/main/__tests__/crossProjectFeedback.test.cjs +7 -0
- package/src/main/__tests__/crossProjectFeedbackRoutes.test.cjs +7 -0
- package/src/main/__tests__/epicMint.test.cjs +50 -2
- package/src/main/__tests__/health-claude-md-budget.test.cjs +57 -0
- package/src/main/__tests__/prdCreate.test.cjs +17 -0
- package/src/main/__tests__/promptSessionsCreateEpicHandler.test.cjs +7 -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/__tests__/workTypeLibrary.test.cjs +89 -0
- package/src/main/agentLibrary.cjs +43 -0
- package/src/main/health.cjs +51 -1
- package/src/main/index.cjs +44 -3
- package/src/main/ipcSchemas.cjs +46 -9
- package/src/main/lib/__tests__/agentPersonaSchema.test.cjs +67 -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/agentModelResolve.cjs +32 -1
- package/src/main/lib/agentPersonaSchema.cjs +82 -0
- package/src/main/lib/delegationReadiness.cjs +141 -0
- package/src/main/lib/epicDelegationStats.cjs +124 -0
- package/src/main/lib/epicMint.cjs +74 -1
- package/src/main/lib/prdFrontmatter.cjs +6 -5
- package/src/main/lib/promptSessionSchema.cjs +7 -15
- package/src/main/lib/workTypeLibrary.cjs +48 -0
- 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 +47 -3
- 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-D0frFrHG.js +0 -3
- package/dist/assets/HostBilko-CXykS2ap.js +0 -1
- package/dist/assets/ListDetail-DXd5rB2F.js +0 -1
- package/dist/assets/MarkdownEditor-D0mO1TUZ.js +0 -1
- package/dist/assets/Panel-DigZB9Yd.js +0 -1
- package/dist/assets/Plugins-DOTiP9sQ.js +0 -2
- package/dist/assets/ProvenanceBadge-PsNd-CBq.js +0 -1
- package/dist/assets/SaveBar-C4bgDklX.js +0 -1
- package/dist/assets/ScopeSwitcher-RMAwgmj4.js +0 -1
- package/dist/assets/Settings-CM4lZxnn.js +0 -3
- package/dist/assets/SystemPrompt-j-WYOC5f.js +0 -1
- package/dist/assets/TagLibrary-Fj0zrPru.js +0 -1
- package/dist/assets/Toggle-H6pNcv3l.js +0 -1
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* health-claude-md-budget.test.cjs — CLAUDE.md SIZE BUDGET enforcement
|
|
3
|
+
* (PRD 1039, following CLAUDE.md's growth from 3,871 to 65,768 chars between
|
|
4
|
+
* 2026-05-09 and 2026-08-13 with zero of 359 PRDs ever pruning it). Exercises
|
|
5
|
+
* parseClaudeMdBudget()/evaluateClaudeMdBudget() directly since they're pure —
|
|
6
|
+
* no need to spawn the full check() to cover this.
|
|
7
|
+
*
|
|
8
|
+
* Run: timeout 120 npx vitest run src/main/__tests__/health-claude-md-budget.test.cjs
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
import { test, expect } from 'vitest';
|
|
14
|
+
const { parseClaudeMdBudget, evaluateClaudeMdBudget } = require('../health.cjs');
|
|
15
|
+
|
|
16
|
+
test('under budget reports ok with no issue', () => {
|
|
17
|
+
const result = evaluateClaudeMdBudget(11873, 12000);
|
|
18
|
+
expect(result.ok).toBe(true);
|
|
19
|
+
expect(result.applicable).toBe(true);
|
|
20
|
+
expect(result.message).toBeUndefined();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('exactly at budget reports ok with no issue', () => {
|
|
24
|
+
const result = evaluateClaudeMdBudget(12000, 12000);
|
|
25
|
+
expect(result.ok).toBe(true);
|
|
26
|
+
expect(result.applicable).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('over budget reports one issue with actual size and budget in the message', () => {
|
|
30
|
+
const result = evaluateClaudeMdBudget(13210, 12000);
|
|
31
|
+
expect(result.ok).toBe(false);
|
|
32
|
+
expect(result.overage).toBe(1210);
|
|
33
|
+
expect(result.message).toContain('13210');
|
|
34
|
+
expect(result.message).toContain('12000');
|
|
35
|
+
expect(result.message).toContain('1210');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('no SIZE BUDGET line present -> parses to null, evaluator skips silently', () => {
|
|
39
|
+
const text = '# Some Project\n\nJust a normal CLAUDE.md with no budget declared.\n';
|
|
40
|
+
const budget = parseClaudeMdBudget(text);
|
|
41
|
+
expect(budget).toBeNull();
|
|
42
|
+
const result = evaluateClaudeMdBudget(50000, budget);
|
|
43
|
+
expect(result.ok).toBe(true);
|
|
44
|
+
expect(result.applicable).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('malformed/unparseable budget line -> null, no throw', () => {
|
|
48
|
+
const text = '> **SIZE BUDGET — a lot of chars**, checked before every commit.';
|
|
49
|
+
expect(() => parseClaudeMdBudget(text)).not.toThrow();
|
|
50
|
+
const budget = parseClaudeMdBudget(text);
|
|
51
|
+
expect(budget).toBeNull();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('comma-separated form parses to a plain integer', () => {
|
|
55
|
+
const text = '> **SIZE BUDGET — 12,000 chars**, checked with `wc -c CLAUDE.md` before every commit.';
|
|
56
|
+
expect(parseClaudeMdBudget(text)).toBe(12000);
|
|
57
|
+
});
|
|
@@ -512,6 +512,23 @@ test('POST /admin/scheduler/create-prd with sourcePromptId writes it into the cr
|
|
|
512
512
|
}
|
|
513
513
|
});
|
|
514
514
|
|
|
515
|
+
test('POST /admin/scheduler/create-prd with tag writes it into the created PRD frontmatter (PRD 1041)', async () => {
|
|
516
|
+
const prdsDir = await mkTmpPrdsDir();
|
|
517
|
+
const remote = makeFakeRemoteWithPrdsDir(prdsDir);
|
|
518
|
+
const { admin, port, token } = await startWithRemote(remote);
|
|
519
|
+
try {
|
|
520
|
+
const res = await request(port, {
|
|
521
|
+
method: 'POST', path: '/admin/scheduler/create-prd', token,
|
|
522
|
+
body: validCreateBody({ slug: 'with-a-tag', tag: 'bug' }),
|
|
523
|
+
});
|
|
524
|
+
expect(res.status).toBe(200);
|
|
525
|
+
const written = fs.readFileSync(path.join(prdsDir, `${res.json.nn}-with-a-tag.md`), 'utf8');
|
|
526
|
+
expect(written).toMatch(/^tag: bug$/m);
|
|
527
|
+
} finally {
|
|
528
|
+
await admin.stop();
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
|
|
515
532
|
// ──────────────────────────────────────────── originClaudeSessionId fallback
|
|
516
533
|
// (2026-08-01 fix: a PRD queued via the scheduler_create_prd MCP tool from
|
|
517
534
|
// inside an Epic's own chat session, without an explicit sourcePromptId,
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
'use strict';
|
|
17
17
|
|
|
18
18
|
import { test, expect, afterEach } from 'vitest';
|
|
19
|
+
const fs = require('node:fs');
|
|
19
20
|
const fsp = require('node:fs/promises');
|
|
20
21
|
const os = require('node:os');
|
|
21
22
|
const path = require('node:path');
|
|
@@ -40,6 +41,12 @@ async function mkCwd() {
|
|
|
40
41
|
const cwd = await fsp.mkdtemp(path.join(os.tmpdir(), 'sm-promptsessions-create-epic-'));
|
|
41
42
|
config.addAllowedRoot(cwd);
|
|
42
43
|
tmpDirs.push(cwd);
|
|
44
|
+
// Project-overlay fixture so the 'architect' agentType used below resolves
|
|
45
|
+
// via ensureEpic's write-time persona-existence check without depending on
|
|
46
|
+
// the host's ~/.claude/agents/ (epicMint.cjs).
|
|
47
|
+
const agentsDir = path.join(cwd, '.claude', 'agents');
|
|
48
|
+
fs.mkdirSync(agentsDir, { recursive: true });
|
|
49
|
+
fs.writeFileSync(path.join(agentsDir, 'architect.md'), '---\nname: architect\n---\nFixture persona for tests.\n');
|
|
43
50
|
return cwd;
|
|
44
51
|
}
|
|
45
52
|
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* seedAgentPersonas.test.cjs — unit tests for src/main/seedAgentPersonas.cjs.
|
|
3
|
+
*
|
|
4
|
+
* Run: timeout 120 npx vitest run src/main/__tests__/seedAgentPersonas.test.cjs
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
import { test, expect, beforeEach, afterEach } from 'vitest';
|
|
10
|
+
const fs = require('node:fs');
|
|
11
|
+
const os = require('node:os');
|
|
12
|
+
const path = require('node:path');
|
|
13
|
+
|
|
14
|
+
let tmpHome;
|
|
15
|
+
let realHome;
|
|
16
|
+
let seedAgentPersonas;
|
|
17
|
+
|
|
18
|
+
// config.cjs bakes os.homedir() into top-level consts at first require
|
|
19
|
+
// (allowedRoots/WRITE_PREFIXES, used by writeJsonSync) — must be purged
|
|
20
|
+
// alongside seedAgentPersonas.cjs so each test's tmpHome takes effect.
|
|
21
|
+
const MODULES_TO_RELOAD = ['../seedAgentPersonas.cjs', '../config.cjs'];
|
|
22
|
+
|
|
23
|
+
function purgeRequireCache() {
|
|
24
|
+
for (const m of MODULES_TO_RELOAD) {
|
|
25
|
+
try { delete require.cache[require.resolve(m)]; } catch { /* not loaded yet */ }
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
realHome = process.env.HOME;
|
|
31
|
+
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'seed-agent-personas-test-'));
|
|
32
|
+
process.env.HOME = tmpHome;
|
|
33
|
+
delete process.env.SM_SEED_AGENT_PERSONAS_DISABLE;
|
|
34
|
+
purgeRequireCache();
|
|
35
|
+
({ seedAgentPersonas } = require('../seedAgentPersonas.cjs'));
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
process.env.HOME = realHome;
|
|
40
|
+
delete process.env.SM_SEED_AGENT_PERSONAS_DISABLE;
|
|
41
|
+
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
42
|
+
purgeRequireCache();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
function agentsDir() {
|
|
46
|
+
return path.join(tmpHome, '.claude', 'agents');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function markerPath() {
|
|
50
|
+
return path.join(tmpHome, '.claude', 'session-manager', '.agent-personas-seeded');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function silentLogger() {
|
|
54
|
+
return { log: () => {}, warn: () => {} };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
test('fresh seed writes both persona files', async () => {
|
|
58
|
+
await seedAgentPersonas({ logger: silentLogger() });
|
|
59
|
+
|
|
60
|
+
const architect = path.join(agentsDir(), 'architect.md');
|
|
61
|
+
const devLead = path.join(agentsDir(), 'dev-lead.md');
|
|
62
|
+
expect(fs.existsSync(architect)).toBe(true);
|
|
63
|
+
expect(fs.existsSync(devLead)).toBe(true);
|
|
64
|
+
|
|
65
|
+
const bundledArchitect = fs.readFileSync(
|
|
66
|
+
path.join(__dirname, '..', '..', 'seed', 'agents', 'architect.md'),
|
|
67
|
+
'utf8'
|
|
68
|
+
);
|
|
69
|
+
expect(fs.readFileSync(architect, 'utf8')).toBe(bundledArchitect);
|
|
70
|
+
|
|
71
|
+
const marker = JSON.parse(fs.readFileSync(markerPath(), 'utf8'));
|
|
72
|
+
expect(marker.done).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('a pre-existing persona file is left byte-identical', async () => {
|
|
76
|
+
fs.mkdirSync(agentsDir(), { recursive: true });
|
|
77
|
+
const architect = path.join(agentsDir(), 'architect.md');
|
|
78
|
+
const customContent = '# My custom architect\nnever overwrite me\n';
|
|
79
|
+
fs.writeFileSync(architect, customContent);
|
|
80
|
+
|
|
81
|
+
await seedAgentPersonas({ logger: silentLogger() });
|
|
82
|
+
|
|
83
|
+
expect(fs.readFileSync(architect, 'utf8')).toBe(customContent);
|
|
84
|
+
// dev-lead had no pre-existing file, so it should still be seeded.
|
|
85
|
+
expect(fs.existsSync(path.join(agentsDir(), 'dev-lead.md'))).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('done:true marker short-circuits without touching agents dir', async () => {
|
|
89
|
+
const mPath = markerPath();
|
|
90
|
+
fs.mkdirSync(path.dirname(mPath), { recursive: true });
|
|
91
|
+
fs.writeFileSync(mPath, JSON.stringify({ done: true, attempts: 1, ts: '2026-01-01T00:00:00.000Z' }));
|
|
92
|
+
|
|
93
|
+
await seedAgentPersonas({ logger: silentLogger() });
|
|
94
|
+
|
|
95
|
+
expect(fs.existsSync(agentsDir())).toBe(false);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('SM_SEED_AGENT_PERSONAS_DISABLE=1 short-circuits', async () => {
|
|
99
|
+
process.env.SM_SEED_AGENT_PERSONAS_DISABLE = '1';
|
|
100
|
+
|
|
101
|
+
await seedAgentPersonas({ logger: silentLogger() });
|
|
102
|
+
|
|
103
|
+
expect(fs.existsSync(agentsDir())).toBe(false);
|
|
104
|
+
expect(fs.existsSync(markerPath())).toBe(false);
|
|
105
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* seedSchedulerMcp.test.cjs — unit tests for src/main/seedSchedulerMcp.cjs.
|
|
3
|
+
*
|
|
4
|
+
* Run: timeout 120 npx vitest run src/main/__tests__/seedSchedulerMcp.test.cjs
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
import { test, expect, beforeEach, afterEach } from 'vitest';
|
|
10
|
+
const fs = require('node:fs');
|
|
11
|
+
const os = require('node:os');
|
|
12
|
+
const path = require('node:path');
|
|
13
|
+
|
|
14
|
+
let tmpHome;
|
|
15
|
+
let realHome;
|
|
16
|
+
let seedSchedulerMcp;
|
|
17
|
+
|
|
18
|
+
// config.cjs bakes os.homedir() into top-level consts at first require
|
|
19
|
+
// (allowedRoots/WRITE_PREFIXES, used by writeJsonSync) — must be purged
|
|
20
|
+
// alongside seedSchedulerMcp.cjs so each test's tmpHome takes effect.
|
|
21
|
+
const MODULES_TO_RELOAD = ['../seedSchedulerMcp.cjs', '../config.cjs'];
|
|
22
|
+
|
|
23
|
+
function purgeRequireCache() {
|
|
24
|
+
for (const m of MODULES_TO_RELOAD) {
|
|
25
|
+
try { delete require.cache[require.resolve(m)]; } catch { /* not loaded yet */ }
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
realHome = process.env.HOME;
|
|
31
|
+
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'seed-scheduler-mcp-test-'));
|
|
32
|
+
process.env.HOME = tmpHome;
|
|
33
|
+
delete process.env.SM_SEED_SCHEDULER_MCP_DISABLE;
|
|
34
|
+
purgeRequireCache();
|
|
35
|
+
({ seedSchedulerMcp } = require('../seedSchedulerMcp.cjs'));
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
process.env.HOME = realHome;
|
|
40
|
+
delete process.env.SM_SEED_SCHEDULER_MCP_DISABLE;
|
|
41
|
+
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
42
|
+
purgeRequireCache();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
function markerPath() {
|
|
46
|
+
return path.join(tmpHome, '.claude', 'session-manager', '.scheduler-mcp-seeded');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function claudeJsonPath() {
|
|
50
|
+
return path.join(tmpHome, '.claude.json');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function silentLogger() {
|
|
54
|
+
return { log: () => {}, warn: () => {} };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function readMarker() {
|
|
58
|
+
return JSON.parse(fs.readFileSync(markerPath(), 'utf8'));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
test('fresh registration calls addFn with the absolute server path and writes done marker', async () => {
|
|
62
|
+
const calls = [];
|
|
63
|
+
const addFn = async (serverPath) => {
|
|
64
|
+
calls.push(serverPath);
|
|
65
|
+
return { ok: true, exitCode: 0 };
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
await seedSchedulerMcp({ logger: silentLogger(), addFn });
|
|
69
|
+
|
|
70
|
+
expect(calls).toHaveLength(1);
|
|
71
|
+
expect(path.isAbsolute(calls[0])).toBe(true);
|
|
72
|
+
expect(calls[0].endsWith(path.join('scripts', 'scheduler-mcp-server.cjs'))).toBe(true);
|
|
73
|
+
|
|
74
|
+
const marker = readMarker();
|
|
75
|
+
expect(marker.done).toBe(true);
|
|
76
|
+
expect(marker.attempts).toBe(0);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('already-registered at user scope short-circuits without calling addFn', async () => {
|
|
80
|
+
fs.writeFileSync(
|
|
81
|
+
claudeJsonPath(),
|
|
82
|
+
JSON.stringify({
|
|
83
|
+
mcpServers: {
|
|
84
|
+
'session-manager-scheduler': { type: 'stdio', command: 'node', args: ['/some/path/scheduler-mcp-server.cjs'] },
|
|
85
|
+
fetch: { type: 'stdio', command: 'uvx', args: ['mcp-server-fetch'] },
|
|
86
|
+
},
|
|
87
|
+
})
|
|
88
|
+
);
|
|
89
|
+
let called = false;
|
|
90
|
+
const addFn = async () => { called = true; return { ok: true }; };
|
|
91
|
+
|
|
92
|
+
await seedSchedulerMcp({ logger: silentLogger(), addFn });
|
|
93
|
+
|
|
94
|
+
expect(called).toBe(false);
|
|
95
|
+
const marker = readMarker();
|
|
96
|
+
expect(marker.done).toBe(true);
|
|
97
|
+
|
|
98
|
+
// Untouched — the pre-existing registration (and sibling entries) stay as-is.
|
|
99
|
+
const claudeJson = JSON.parse(fs.readFileSync(claudeJsonPath(), 'utf8'));
|
|
100
|
+
expect(claudeJson.mcpServers['session-manager-scheduler'].args).toEqual(['/some/path/scheduler-mcp-server.cjs']);
|
|
101
|
+
expect(claudeJson.mcpServers.fetch).toBeDefined();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('done:true marker short-circuits without calling addFn', async () => {
|
|
105
|
+
fs.mkdirSync(path.dirname(markerPath()), { recursive: true });
|
|
106
|
+
fs.writeFileSync(markerPath(), JSON.stringify({ done: true, attempts: 1, ts: '2026-01-01T00:00:00.000Z' }));
|
|
107
|
+
let called = false;
|
|
108
|
+
const addFn = async () => { called = true; return { ok: true }; };
|
|
109
|
+
|
|
110
|
+
await seedSchedulerMcp({ logger: silentLogger(), addFn });
|
|
111
|
+
|
|
112
|
+
expect(called).toBe(false);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('SM_SEED_SCHEDULER_MCP_DISABLE=1 short-circuits', async () => {
|
|
116
|
+
process.env.SM_SEED_SCHEDULER_MCP_DISABLE = '1';
|
|
117
|
+
let called = false;
|
|
118
|
+
const addFn = async () => { called = true; return { ok: true }; };
|
|
119
|
+
|
|
120
|
+
await seedSchedulerMcp({ logger: silentLogger(), addFn });
|
|
121
|
+
|
|
122
|
+
expect(called).toBe(false);
|
|
123
|
+
expect(fs.existsSync(markerPath())).toBe(false);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('a failed attempt bumps the counter and stops after MAX_ATTEMPTS', async () => {
|
|
127
|
+
const addFn = async () => ({ ok: false, error: 'boom' });
|
|
128
|
+
|
|
129
|
+
await seedSchedulerMcp({ logger: silentLogger(), addFn });
|
|
130
|
+
expect(readMarker()).toMatchObject({ done: false, attempts: 1 });
|
|
131
|
+
|
|
132
|
+
await seedSchedulerMcp({ logger: silentLogger(), addFn });
|
|
133
|
+
expect(readMarker()).toMatchObject({ done: false, attempts: 2 });
|
|
134
|
+
|
|
135
|
+
await seedSchedulerMcp({ logger: silentLogger(), addFn });
|
|
136
|
+
expect(readMarker()).toMatchObject({ done: false, attempts: 3 });
|
|
137
|
+
|
|
138
|
+
// MAX_ATTEMPTS reached — a 4th run must not call addFn again or bump further.
|
|
139
|
+
let called = false;
|
|
140
|
+
const trackedAddFn = async () => { called = true; return { ok: false, error: 'boom' }; };
|
|
141
|
+
await seedSchedulerMcp({ logger: silentLogger(), addFn: trackedAddFn });
|
|
142
|
+
|
|
143
|
+
expect(called).toBe(false);
|
|
144
|
+
expect(readMarker()).toMatchObject({ done: false, attempts: 3 });
|
|
145
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* seedStatus.test.cjs — unit tests for src/main/seedStatus.cjs.
|
|
3
|
+
*
|
|
4
|
+
* Run: timeout 120 npx vitest run src/main/__tests__/seedStatus.test.cjs
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
import { test, expect, beforeEach, afterEach } from 'vitest';
|
|
10
|
+
const fs = require('node:fs');
|
|
11
|
+
const os = require('node:os');
|
|
12
|
+
const path = require('node:path');
|
|
13
|
+
|
|
14
|
+
let tmpHome;
|
|
15
|
+
let realHome;
|
|
16
|
+
let getSeedStatus;
|
|
17
|
+
|
|
18
|
+
// config.cjs (via seedSchedulerMcp/seedAgentPersonas) and each seed*.cjs
|
|
19
|
+
// module bake os.homedir() into top-level consts at first require — purge
|
|
20
|
+
// all of them alongside seedStatus.cjs so each test's tmpHome takes effect.
|
|
21
|
+
const MODULES_TO_RELOAD = [
|
|
22
|
+
'../seedStatus.cjs',
|
|
23
|
+
'../seedDevPlugin.cjs',
|
|
24
|
+
'../seedSchedulerMcp.cjs',
|
|
25
|
+
'../seedAgentPersonas.cjs',
|
|
26
|
+
'../config.cjs',
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
function purgeRequireCache() {
|
|
30
|
+
for (const m of MODULES_TO_RELOAD) {
|
|
31
|
+
try { delete require.cache[require.resolve(m)]; } catch { /* not loaded yet */ }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
realHome = process.env.HOME;
|
|
37
|
+
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'seed-status-test-'));
|
|
38
|
+
process.env.HOME = tmpHome;
|
|
39
|
+
purgeRequireCache();
|
|
40
|
+
({ getSeedStatus } = require('../seedStatus.cjs'));
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
process.env.HOME = realHome;
|
|
45
|
+
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
46
|
+
purgeRequireCache();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const MARKER_NAMES = {
|
|
50
|
+
'dev-plugin': '.dev-plugin-seeded',
|
|
51
|
+
'scheduler-mcp': '.scheduler-mcp-seeded',
|
|
52
|
+
'agent-personas': '.agent-personas-seeded',
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
function markerPath(id) {
|
|
56
|
+
return path.join(tmpHome, '.claude', 'session-manager', MARKER_NAMES[id]);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function writeMarker(id, contents) {
|
|
60
|
+
const p = markerPath(id);
|
|
61
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
62
|
+
fs.writeFileSync(p, contents);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
test('no marker files at all → every seeder reports pending with a fix string', () => {
|
|
66
|
+
const status = getSeedStatus();
|
|
67
|
+
for (const id of Object.keys(MARKER_NAMES)) {
|
|
68
|
+
expect(status[id].status).toBe('pending');
|
|
69
|
+
expect(typeof status[id].fix).toBe('string');
|
|
70
|
+
expect(status[id].fix.length).toBeGreaterThan(0);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
for (const id of Object.keys(MARKER_NAMES)) {
|
|
75
|
+
test(`${id}: done:true marker → done`, () => {
|
|
76
|
+
writeMarker(id, JSON.stringify({ done: true, attempts: 1, ts: '2026-01-01T00:00:00.000Z' }));
|
|
77
|
+
expect(getSeedStatus()[id].status).toBe('done');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test(`${id}: done:false, attempts below max → pending`, () => {
|
|
81
|
+
writeMarker(id, JSON.stringify({ done: false, attempts: 1, ts: '2026-01-01T00:00:00.000Z' }));
|
|
82
|
+
expect(getSeedStatus()[id].status).toBe('pending');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test(`${id}: done:false, attempts >= max → exhausted`, () => {
|
|
86
|
+
writeMarker(id, JSON.stringify({ done: false, attempts: 3, ts: '2026-01-01T00:00:00.000Z' }));
|
|
87
|
+
expect(getSeedStatus()[id].status).toBe('exhausted');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test(`${id}: unparseable marker → pending, never throws`, () => {
|
|
91
|
+
writeMarker(id, 'not json at all {{{');
|
|
92
|
+
expect(() => getSeedStatus()).not.toThrow();
|
|
93
|
+
expect(getSeedStatus()[id].status).toBe('pending');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test(`${id}: missing marker file → pending`, () => {
|
|
97
|
+
// No file written for this id — directory may not even exist.
|
|
98
|
+
expect(getSeedStatus()[id].status).toBe('pending');
|
|
99
|
+
});
|
|
100
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* workTypeLibrary.test.cjs — cross-file consistency tests for the WorkType
|
|
3
|
+
* union. Epic tag and PRD tag are one concept (WorkType); this test enforces
|
|
4
|
+
* that every declaration site agrees, since the two module systems involved
|
|
5
|
+
* (renderer .ts, main .cjs, and the preload .d.ts that can import neither)
|
|
6
|
+
* can't enforce it via a shared import. Modelled on scheduleJobSchema.test.cjs's
|
|
7
|
+
* JOB_STATUSES cross-file assertions.
|
|
8
|
+
*
|
|
9
|
+
* Run: timeout 300 npx vitest run src/main/__tests__/workTypeLibrary.test.cjs
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
import { test, expect } from 'vitest';
|
|
15
|
+
const fs = require('node:fs');
|
|
16
|
+
const path = require('node:path');
|
|
17
|
+
const { WORK_TYPES, WorkTypeSchema } = require('../lib/workTypeLibrary.cjs');
|
|
18
|
+
|
|
19
|
+
const REPO_ROOT = path.resolve(__dirname, '../../..');
|
|
20
|
+
const TAG_LIBRARY_PATH = path.join(REPO_ROOT, 'src/renderer/lib/tagLibrary.ts');
|
|
21
|
+
const API_D_TS_PATH = path.join(REPO_ROOT, 'src/preload/api.d.ts');
|
|
22
|
+
const IPC_SCHEMAS_PATH = path.join(REPO_ROOT, 'src/main/ipcSchemas.cjs');
|
|
23
|
+
const SCHEDULER_MCP_SERVER_PATH = path.join(REPO_ROOT, 'scripts/scheduler-mcp-server.cjs');
|
|
24
|
+
|
|
25
|
+
test('WORK_TYPES matches the expected 6 canonical ids in order', () => {
|
|
26
|
+
expect(WORK_TYPES).toEqual([
|
|
27
|
+
'feature',
|
|
28
|
+
'bug',
|
|
29
|
+
'discussion',
|
|
30
|
+
'build',
|
|
31
|
+
'project-home-builder',
|
|
32
|
+
'bilko-host-publisher',
|
|
33
|
+
]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('WorkTypeSchema accepts every WORK_TYPES value and rejects an unknown one', () => {
|
|
37
|
+
for (const value of WORK_TYPES) {
|
|
38
|
+
expect(WorkTypeSchema.safeParse(value).success).toBe(true);
|
|
39
|
+
}
|
|
40
|
+
expect(WorkTypeSchema.safeParse('not-a-real-tag').success).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('WORK_TYPES matches the tag ids declared in tagLibrary.ts TAG_LIBRARY, in order', () => {
|
|
44
|
+
const src = fs.readFileSync(TAG_LIBRARY_PATH, 'utf8');
|
|
45
|
+
const tagMatches = [...src.matchAll(/^\s*tag:\s*'([^']+)',/gm)].map((m) => m[1]);
|
|
46
|
+
expect(tagMatches).toEqual(WORK_TYPES);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('api.d.ts tag unions (line ~1166 and ~1687) both contain exactly the same 6 values', () => {
|
|
50
|
+
const src = fs.readFileSync(API_D_TS_PATH, 'utf8');
|
|
51
|
+
const unionMatches = [...src.matchAll(/tag\?:\s*((?:'[^']+'\s*\|?\s*)+);/g)].map((m) =>
|
|
52
|
+
m[1]
|
|
53
|
+
.split('|')
|
|
54
|
+
.map((s) => s.trim().replace(/^'|'$/g, ''))
|
|
55
|
+
.filter(Boolean)
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
expect(unionMatches.length).toBeGreaterThanOrEqual(2);
|
|
59
|
+
for (const union of unionMatches) {
|
|
60
|
+
expect(union).toEqual(WORK_TYPES);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('ipcSchemas.cjs no longer hardcodes the full-taxonomy tag z.enum literal', () => {
|
|
65
|
+
const src = fs.readFileSync(IPC_SCHEMAS_PATH, 'utf8');
|
|
66
|
+
// The old scheduleRetagPrd literal was missing 'bilko-host-publisher'
|
|
67
|
+
// (5 of the 6 WORK_TYPES values) — assert that exact stale shape is gone.
|
|
68
|
+
expect(src).not.toMatch(/z\.enum\(\['feature',\s*'bug',\s*'discussion',\s*'build',\s*'project-home-builder'\]\)/);
|
|
69
|
+
// Epic tag (schedulerCreateEpic-style routes) draws on the full 6-value
|
|
70
|
+
// WorkType union via EpicTagSchema, which is itself WorkTypeSchema
|
|
71
|
+
// (promptSessionSchema.cjs). PRD tag fields (schedulerCreatePrd,
|
|
72
|
+
// adminPrdFrontmatterPatch) draw on the narrower 5-value PrdWorkTypeSchema
|
|
73
|
+
// — independent values, same shared vocabulary (PRD 1041).
|
|
74
|
+
expect(src).toMatch(/tag:\s*EpicTagSchema/);
|
|
75
|
+
const prdTagMatches = [...src.matchAll(/tag:\s*PrdWorkTypeSchema/g)];
|
|
76
|
+
expect(prdTagMatches.length).toBeGreaterThanOrEqual(2);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('scheduler-mcp-server.cjs requires PRD_WORK_TYPES rather than hardcoding a tag enum literal, and imports it exactly twice (scheduler_create_prd + scheduler_update_prd)', () => {
|
|
80
|
+
const { PRD_WORK_TYPES } = require('../lib/workTypeLibrary.cjs');
|
|
81
|
+
const src = fs.readFileSync(SCHEDULER_MCP_SERVER_PATH, 'utf8');
|
|
82
|
+
expect(src).toMatch(/require\(['"].*workTypeLibrary\.cjs['"]\)/);
|
|
83
|
+
const occurrences = [...src.matchAll(/enum:\s*PRD_WORK_TYPES/g)].length;
|
|
84
|
+
// scheduler_create_prd's inputSchema.tag.enum and scheduler_update_prd's
|
|
85
|
+
// frontmatter.tag.enum — exactly 2 sites, not the Epic-tag (6-value)
|
|
86
|
+
// enum on feedback_open_session's tag property.
|
|
87
|
+
expect(occurrences).toBe(2);
|
|
88
|
+
expect(PRD_WORK_TYPES).toEqual(['feature', 'bug', 'build', 'project-home-builder', 'bilko-host-publisher']);
|
|
89
|
+
});
|
|
@@ -22,6 +22,7 @@ const fsSync = require('node:fs');
|
|
|
22
22
|
const path = require('node:path');
|
|
23
23
|
const os = require('node:os');
|
|
24
24
|
const { splitFrontmatter } = require('./lib/prdFrontmatter.cjs');
|
|
25
|
+
const { resolvePersonaPaths } = require('./lib/epicMint.cjs');
|
|
25
26
|
const configMgr = require('./config.cjs');
|
|
26
27
|
const sessionsStore = require('./sessionsStore.cjs');
|
|
27
28
|
|
|
@@ -249,6 +250,47 @@ async function listPersonas({
|
|
|
249
250
|
return personas;
|
|
250
251
|
}
|
|
251
252
|
|
|
253
|
+
/**
|
|
254
|
+
* Reads a persona `.md` file's raw text for `epicIntake.ts`'s `agentBody`
|
|
255
|
+
* input, honoring the same project-overlay-then-global precedence as
|
|
256
|
+
* `epicMint.cjs`'s `resolvePersonaPaths` (a project's `<cwd>/.claude/agents/
|
|
257
|
+
* <name>.md` wins over `~/.claude/agents/<name>.md`) — the same precedence
|
|
258
|
+
* `listPersonas` deliberately does NOT apply, since that lists only the
|
|
259
|
+
* global directory for the Agent Library nav page. Returns `null` when
|
|
260
|
+
* neither location has the file; frontmatter stripping and the 6000-char cap
|
|
261
|
+
* are epicIntake.ts's job, not this reader's.
|
|
262
|
+
*/
|
|
263
|
+
async function getPersonaBody({
|
|
264
|
+
cwd,
|
|
265
|
+
name,
|
|
266
|
+
validatePath = configMgr.validatePath,
|
|
267
|
+
} = {}) {
|
|
268
|
+
// Same gate savePersona/deletePersona apply before touching a path built
|
|
269
|
+
// from `name` (agentLibrary.cjs's PERSONA_NAME_RE check) — without it a
|
|
270
|
+
// `name` like "../../other-project/CLAUDE" survives resolvePersonaPaths'
|
|
271
|
+
// plain path.join and validatePath's allowed-roots check (which only
|
|
272
|
+
// confirms the resolved path stays under the home dir / an opened
|
|
273
|
+
// project), letting any readable .md under those roots be read and
|
|
274
|
+
// forwarded into the Epic's opening prompt.
|
|
275
|
+
if (!PERSONA_NAME_RE.test(name || '')) return null;
|
|
276
|
+
const { projectPath, globalPath } = resolvePersonaPaths(cwd, name);
|
|
277
|
+
for (const candidate of [projectPath, globalPath]) {
|
|
278
|
+
let real;
|
|
279
|
+
try {
|
|
280
|
+
real = validatePath(candidate);
|
|
281
|
+
} catch {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
try {
|
|
285
|
+
const text = await fsp.readFile(real, 'utf8');
|
|
286
|
+
return { path: real, text };
|
|
287
|
+
} catch {
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
|
|
252
294
|
module.exports = {
|
|
253
295
|
listPersonas,
|
|
254
296
|
openProjects,
|
|
@@ -257,6 +299,7 @@ module.exports = {
|
|
|
257
299
|
deletePersona,
|
|
258
300
|
removeOverride,
|
|
259
301
|
serializePersona,
|
|
302
|
+
getPersonaBody,
|
|
260
303
|
PERSONA_NAME_RE,
|
|
261
304
|
ALL_PROJECTS,
|
|
262
305
|
};
|
package/src/main/health.cjs
CHANGED
|
@@ -181,6 +181,37 @@ function evaluatePerProjectStall(stallSummary, lastRunAtIso, now, thresholdMs) {
|
|
|
181
181
|
return results;
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
+
// Parses the "SIZE BUDGET — 12,000 chars" header line out of a project's
|
|
185
|
+
// CLAUDE.md text. Tolerant of the em-dash, bold markers, the leading
|
|
186
|
+
// blockquote '>', and the thousands comma. Returns null (not a throw, not a
|
|
187
|
+
// default) when the line is absent or unparseable — a CLAUDE.md that
|
|
188
|
+
// predates this convention has nothing to enforce, and that must stay a
|
|
189
|
+
// silent skip rather than a manufactured RED.
|
|
190
|
+
function parseClaudeMdBudget(text) {
|
|
191
|
+
if (typeof text !== 'string') return null;
|
|
192
|
+
const match = text.match(/SIZE BUDGET\s*[—-]+\s*([\d,]+)\s*chars/i);
|
|
193
|
+
if (!match) return null;
|
|
194
|
+
const n = Number(match[1].replace(/,/g, ''));
|
|
195
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Pure comparison of CLAUDE.md's actual byte size against its self-declared
|
|
199
|
+
// budget (see parseClaudeMdBudget). budget === null means "no budget
|
|
200
|
+
// declared" — skip silently rather than treating it as 0 or infinite.
|
|
201
|
+
function evaluateClaudeMdBudget(chars, budget) {
|
|
202
|
+
if (budget == null) return { ok: true, applicable: false };
|
|
203
|
+
const overage = chars - budget;
|
|
204
|
+
if (overage <= 0) return { ok: true, applicable: true, chars, budget };
|
|
205
|
+
return {
|
|
206
|
+
ok: false,
|
|
207
|
+
applicable: true,
|
|
208
|
+
chars,
|
|
209
|
+
budget,
|
|
210
|
+
overage,
|
|
211
|
+
message: `CLAUDE.md is ${chars} chars, over its ${budget}-char budget by ${overage} (see the SIZE BUDGET note in CLAUDE.md)`,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
184
215
|
// Pure evaluator over migratePrds()'s { moved, skipped, unresolved } result —
|
|
185
216
|
// kept separate from the fs-touching check() call site so it's directly
|
|
186
217
|
// unit-testable, matching evaluateTickLiveness's pattern.
|
|
@@ -443,6 +474,23 @@ async function check() {
|
|
|
443
474
|
};
|
|
444
475
|
}
|
|
445
476
|
|
|
477
|
+
// 6.4. Check project CLAUDE.md against its own self-declared SIZE BUDGET
|
|
478
|
+
// header (see the CLAUDE.md size-budget note). Skips silently when no
|
|
479
|
+
// budget line is present — this is not required for every project.
|
|
480
|
+
try {
|
|
481
|
+
const claudeMdPath = path.join(PROJECT_ROOT, 'CLAUDE.md');
|
|
482
|
+
const claudeMdText = fs.readFileSync(claudeMdPath, 'utf8');
|
|
483
|
+
const chars = Buffer.byteLength(claudeMdText, 'utf8');
|
|
484
|
+
const budget = parseClaudeMdBudget(claudeMdText);
|
|
485
|
+
const budgetResult = evaluateClaudeMdBudget(chars, budget);
|
|
486
|
+
status.components.claude_md_budget = budgetResult;
|
|
487
|
+
if (!budgetResult.ok) {
|
|
488
|
+
status.issues.push(budgetResult.message);
|
|
489
|
+
}
|
|
490
|
+
} catch (e) {
|
|
491
|
+
status.components.claude_md_budget = { ok: true, applicable: false };
|
|
492
|
+
}
|
|
493
|
+
|
|
446
494
|
// 6.5. Check ~/.claude/CLAUDE.md's @import chain resolves cleanly.
|
|
447
495
|
// Informational only — a broken/stale persona import degrades instruction
|
|
448
496
|
// fidelity, not app health, so it never flips status.ok to false. See
|
|
@@ -492,7 +540,7 @@ async function check() {
|
|
|
492
540
|
// Critical: nodejs, config dir, typescript, build artifact, test infrastructure.
|
|
493
541
|
// Non-fatal: scheduler/transcripts dirs may not exist on fresh install.
|
|
494
542
|
// Informational: app log age (shows if app is running, but not blocking).
|
|
495
|
-
const criticalComponents = ['nodejs', 'config_dir', 'typescript', 'build_artifact', 'test_infrastructure', 'scheduler_queue', 'prd_migration'];
|
|
543
|
+
const criticalComponents = ['nodejs', 'config_dir', 'typescript', 'build_artifact', 'test_infrastructure', 'scheduler_queue', 'prd_migration', 'claude_md_budget'];
|
|
496
544
|
status.ok = criticalComponents.every((c) => status.components[c]?.ok !== false);
|
|
497
545
|
|
|
498
546
|
status.elapsedMs = Date.now() - start;
|
|
@@ -515,6 +563,8 @@ module.exports = {
|
|
|
515
563
|
evaluatePrdMigrationHealth,
|
|
516
564
|
computeProjectProblemCounts,
|
|
517
565
|
evaluatePerProjectStall,
|
|
566
|
+
parseClaudeMdBudget,
|
|
567
|
+
evaluateClaudeMdBudget,
|
|
518
568
|
TICK_STALL_THRESHOLD_MS,
|
|
519
569
|
HEARTBEAT_STALE_MS,
|
|
520
570
|
};
|