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,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
|
+
}
|
|
@@ -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/index.cjs
CHANGED
|
@@ -36,6 +36,7 @@ const epicWorktreeMint = require('./lib/epicWorktreeMint.cjs');
|
|
|
36
36
|
const epicWorktreeMerge = require('./lib/epicWorktreeMerge.cjs');
|
|
37
37
|
const epicWorktreeProjectConfig = require('./lib/epicWorktreeProjectConfig.cjs');
|
|
38
38
|
const agentLibrary = require('./agentLibrary.cjs');
|
|
39
|
+
const { checkDelegationReadiness } = require('./lib/delegationReadiness.cjs');
|
|
39
40
|
const { sendIfAlive } = require('./lib/sendToRenderer.cjs');
|
|
40
41
|
const { resolveBuildTarget } = require('./lib/buildTarget.cjs');
|
|
41
42
|
const crossProjectFeedback = require('./lib/crossProjectFeedback.cjs');
|
|
@@ -54,6 +55,9 @@ const teams = require('./teams.cjs');
|
|
|
54
55
|
const queueOps = require('./queueOps.cjs');
|
|
55
56
|
const pluginInstall = require('./pluginInstall.cjs');
|
|
56
57
|
const { seedDevPlugin } = require('./seedDevPlugin.cjs');
|
|
58
|
+
const { seedAgentPersonas } = require('./seedAgentPersonas.cjs');
|
|
59
|
+
const { seedSchedulerMcp } = require('./seedSchedulerMcp.cjs');
|
|
60
|
+
const { getSeedStatus } = require('./seedStatus.cjs');
|
|
57
61
|
const otel = require('./otel.cjs');
|
|
58
62
|
const otelSettings = require('./otelSettings.cjs');
|
|
59
63
|
const { registerHistoryAggregatorHandlers, finalizeClosedDays, refreshIntradayToday } = require('./historyAggregator.cjs');
|
|
@@ -67,6 +71,7 @@ const { registerProjectBriefIpc } = require('./projectBrief.cjs');
|
|
|
67
71
|
const { registerProjectPagesIpc } = require('./projectPages.cjs');
|
|
68
72
|
const { registerBilkoHostIpc } = require('./bilkoHost.cjs');
|
|
69
73
|
const promptSessionTranscript = require('./promptSessionTranscript.cjs');
|
|
74
|
+
const { computeEpicDelegationStats } = require('./lib/epicDelegationStats.cjs');
|
|
70
75
|
const agentMemory = require('./agentMemory.cjs');
|
|
71
76
|
const git = require('./git.cjs');
|
|
72
77
|
const heapSnapshot = require('./heapSnapshot.cjs');
|
|
@@ -481,6 +486,19 @@ ipcMain.handle('agents:remove-override', async (_e, payload) => {
|
|
|
481
486
|
broadcastAgentsChanged();
|
|
482
487
|
return result;
|
|
483
488
|
});
|
|
489
|
+
// New Epic AIM composer (epicIntake.ts's `agentBody`/`agentPath`): unlike
|
|
490
|
+
// list-personas above (global dir only), this honors the project-overlay-
|
|
491
|
+
// then-global precedence so an Epic's opening prompt carries the persona
|
|
492
|
+
// body that actually applies to its cwd. Read-only.
|
|
493
|
+
ipcMain.handle('agents:get-persona-body', validated(schemas.agentsGetPersonaBody, (payload) =>
|
|
494
|
+
agentLibrary.getPersonaBody(payload)));
|
|
495
|
+
|
|
496
|
+
// "Can this project actually delegate?" probe (PRD: delegation-readiness).
|
|
497
|
+
// Structured data over the four preconditions for scheduler_create_prd being
|
|
498
|
+
// in an agent's tool list at all — surfacing it in the UI is a sibling PRD.
|
|
499
|
+
ipcMain.handle('app:delegation-readiness', validated(schemas.delegationReadinessCwd, (payload) =>
|
|
500
|
+
checkDelegationReadiness(payload)
|
|
501
|
+
));
|
|
484
502
|
|
|
485
503
|
ipcMain.handle('app:engage-rules-path', () => process.env.SESSION_MANAGER_ENGAGE_RULES || null);
|
|
486
504
|
|
|
@@ -488,6 +506,9 @@ ipcMain.handle('app:engage-rules-path', () => process.env.SESSION_MANAGER_ENGAGE
|
|
|
488
506
|
// on disk or the home self-check failed (e.g. macOS /Users symlink mismatch).
|
|
489
507
|
ipcMain.handle('app:claude-bin-status', () => bootClaudeBin);
|
|
490
508
|
ipcMain.handle('app:home-self-check', () => bootHomeSelfCheck);
|
|
509
|
+
// First-boot seeder outcomes ('done' | 'pending' | 'exhausted' per seeder),
|
|
510
|
+
// read live from each seeder's own marker file — see seedStatus.cjs.
|
|
511
|
+
ipcMain.handle('app:seed-status', () => getSeedStatus());
|
|
491
512
|
|
|
492
513
|
ipcMain.handle('app:pick-directory', async () => {
|
|
493
514
|
console.log('[main] pick-directory invoked');
|
|
@@ -802,6 +823,11 @@ ipcMain.handle('promptSessionTranscript:read', validated(schemas.promptSessionTr
|
|
|
802
823
|
const turns = await promptSessionTranscript.readTurns(cwd, epicId, { limit });
|
|
803
824
|
return { turns };
|
|
804
825
|
}));
|
|
826
|
+
// Derived-at-read-time "PRDs queued vs inline edits" counter (no writer —
|
|
827
|
+
// reads the Epic's own live prds/ dir + its own claude session transcript).
|
|
828
|
+
ipcMain.handle('epicDelegationStats:get', validated(schemas.epicDelegationStatsGet, async ({ cwd, epicId, claudeSessionId }) => {
|
|
829
|
+
return computeEpicDelegationStats(cwd, epicId, claudeSessionId || null);
|
|
830
|
+
}));
|
|
805
831
|
// Renderer Epic lifecycle trace (PRD 940) — append-only, no read/list IPC.
|
|
806
832
|
// Kind allowlist enforced by schemas.auditLogAppend, not here.
|
|
807
833
|
ipcMain.handle('auditLog:append', validated(schemas.auditLogAppend, async ({ kind, cwd, epicId, source }) => {
|
|
@@ -1149,9 +1175,24 @@ app.whenReady().then(async () => {
|
|
|
1149
1175
|
// First-boot default: install the bundled session-manager-dev plugin (its 10
|
|
1150
1176
|
// dev skills) from the app's own marketplace. One-shot + idempotent; never
|
|
1151
1177
|
// throws. SM_SEED_DEV_PLUGIN_DISABLE=1 to opt out.
|
|
1152
|
-
seedDevPlugin({ logger: console }).catch((e) => {
|
|
1178
|
+
seedDevPlugin({ logger: console, writeLog: logs.writeLine }).catch((e) => {
|
|
1153
1179
|
logs.writeLine({ scope: 'seed-dev-plugin', level: 'error', message: 'seed failed', meta: { error: e?.message } });
|
|
1154
1180
|
});
|
|
1181
|
+
// First-boot default: seed the bundled Architect + Dev Lead Agent personas
|
|
1182
|
+
// into ~/.claude/agents/ so the Agent Library isn't empty on a fresh
|
|
1183
|
+
// install. One-shot + idempotent; never overwrites an existing persona;
|
|
1184
|
+
// never throws. SM_SEED_AGENT_PERSONAS_DISABLE=1 to opt out.
|
|
1185
|
+
seedAgentPersonas({ logger: console, writeLog: logs.writeLine }).catch((e) => {
|
|
1186
|
+
logs.writeLine({ scope: 'seed-agent-personas', level: 'error', message: 'seed failed', meta: { error: e?.message } });
|
|
1187
|
+
});
|
|
1188
|
+
// First-boot default: register the session-manager-scheduler MCP server at
|
|
1189
|
+
// USER scope so scheduler_create_prd is available in every project, not
|
|
1190
|
+
// just this repo's own .mcp.json. One-shot + idempotent; never overwrites
|
|
1191
|
+
// an existing registration; never throws. SM_SEED_SCHEDULER_MCP_DISABLE=1
|
|
1192
|
+
// to opt out.
|
|
1193
|
+
seedSchedulerMcp({ logger: console, writeLog: logs.writeLine }).catch((e) => {
|
|
1194
|
+
logs.writeLine({ scope: 'seed-scheduler-mcp', level: 'error', message: 'seed failed', meta: { error: e?.message } });
|
|
1195
|
+
});
|
|
1155
1196
|
// History rollup finalize pass: deferred 30s past boot so it never competes
|
|
1156
1197
|
// with first-paint, fire-and-forget (cron/offline refresh is PRD 651 — this
|
|
1157
1198
|
// is just the one-shot in-app pass).
|
package/src/main/ipcSchemas.cjs
CHANGED
|
@@ -537,6 +537,17 @@ const projectBriefCwd = z.object({
|
|
|
537
537
|
cwd: z.string().min(1).max(4096),
|
|
538
538
|
}).strict();
|
|
539
539
|
|
|
540
|
+
// ──────────────────────────────────────────── Delegation readiness
|
|
541
|
+
const delegationReadinessCwd = z.object({
|
|
542
|
+
cwd: z.string().min(1).max(4096),
|
|
543
|
+
}).strict();
|
|
544
|
+
|
|
545
|
+
// ──────────────────────────────────────────── Agent persona body (New Epic AIM)
|
|
546
|
+
const agentsGetPersonaBody = z.object({
|
|
547
|
+
cwd: z.string().min(1).max(4096),
|
|
548
|
+
name: z.string().min(1).max(200),
|
|
549
|
+
}).strict();
|
|
550
|
+
|
|
540
551
|
// ──────────────────────────────────────────── Project Pages (PRD 932)
|
|
541
552
|
// Same validation split as projectBriefCwd above: real path validation is
|
|
542
553
|
// config.cjs's validatePath at first fs access in projectPages.cjs.
|
|
@@ -608,6 +619,17 @@ const promptSessionTranscriptRead = z.object({
|
|
|
608
619
|
limit: z.number().int().positive().max(10_000).optional(),
|
|
609
620
|
}).strict();
|
|
610
621
|
|
|
622
|
+
// ──────────────────────────────────────────── Epic delegation stats
|
|
623
|
+
// (derived-at-read-time "PRDs queued vs inline edits" counter surfaced on
|
|
624
|
+
// the Epic card). claudeSessionId is optional — a proposed Epic that never
|
|
625
|
+
// started chatting has none, and the computation just reports 0 inline
|
|
626
|
+
// edits rather than throwing.
|
|
627
|
+
const epicDelegationStatsGet = z.object({
|
|
628
|
+
cwd: z.string().min(1).max(4096),
|
|
629
|
+
epicId: z.string().regex(PROMPT_SESSION_EPIC_ID_RE),
|
|
630
|
+
claudeSessionId: z.string().max(128).optional().nullable(),
|
|
631
|
+
}).strict();
|
|
632
|
+
|
|
611
633
|
// ──────────────────────────────────────────── Audit log (renderer Epic
|
|
612
634
|
// lifecycle trace, PRD 940). Kind allowlist is enforced HERE, main-side — the
|
|
613
635
|
// renderer cannot write arbitrary kinds through this channel, only the six
|
|
@@ -949,6 +971,7 @@ module.exports = {
|
|
|
949
971
|
projectBriefUpdate,
|
|
950
972
|
promptSessionTranscriptAppend,
|
|
951
973
|
promptSessionTranscriptRead,
|
|
974
|
+
epicDelegationStatsGet,
|
|
952
975
|
promptSessionsMergeActiveIndex,
|
|
953
976
|
promptSessionsCreateEpic,
|
|
954
977
|
promptSessionsCreateWorktree,
|
|
@@ -974,6 +997,8 @@ module.exports = {
|
|
|
974
997
|
chatExternalSend,
|
|
975
998
|
exchangesList,
|
|
976
999
|
agentsSavePersona,
|
|
1000
|
+
agentsGetPersonaBody,
|
|
1001
|
+
delegationReadinessCwd,
|
|
977
1002
|
},
|
|
978
1003
|
validated,
|
|
979
1004
|
};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* delegationReadiness.test.cjs — unit tests for the "can this project
|
|
3
|
+
* actually delegate?" probe. Uses a temp HOME and a temp cwd so the real
|
|
4
|
+
* ~/.claude and this repo's own config are never touched.
|
|
5
|
+
*
|
|
6
|
+
* Run: timeout 300 npx vitest run src/main/lib/__tests__/delegationReadiness.test.cjs
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { test, expect, afterEach } from 'vitest';
|
|
10
|
+
const fs = require('node:fs');
|
|
11
|
+
const fsp = require('node:fs/promises');
|
|
12
|
+
const os = require('node:os');
|
|
13
|
+
const path = require('node:path');
|
|
14
|
+
const { checkDelegationReadiness } = require('../delegationReadiness.cjs');
|
|
15
|
+
|
|
16
|
+
const tmpDirs = [];
|
|
17
|
+
afterEach(async () => {
|
|
18
|
+
while (tmpDirs.length) {
|
|
19
|
+
const d = tmpDirs.pop();
|
|
20
|
+
await fsp.rm(d, { recursive: true, force: true });
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
async function mkTmp(prefix) {
|
|
25
|
+
const d = await fsp.mkdtemp(path.join(os.tmpdir(), prefix));
|
|
26
|
+
tmpDirs.push(d);
|
|
27
|
+
return d;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function writeJson(absPath, value) {
|
|
31
|
+
await fsp.mkdir(path.dirname(absPath), { recursive: true });
|
|
32
|
+
await fsp.writeFile(absPath, JSON.stringify(value), 'utf8');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function makeGreenFixtures() {
|
|
36
|
+
const homeDir = await mkTmp('sm-delegation-home-');
|
|
37
|
+
const cwd = await mkTmp('sm-delegation-cwd-');
|
|
38
|
+
|
|
39
|
+
await writeJson(path.join(homeDir, '.claude.json'), {
|
|
40
|
+
mcpServers: { 'session-manager-scheduler': { type: 'stdio', command: 'node', args: [] } },
|
|
41
|
+
});
|
|
42
|
+
await writeJson(path.join(homeDir, '.claude', 'settings.json'), {
|
|
43
|
+
enabledPlugins: { 'session-manager-dev@session-manager': true },
|
|
44
|
+
});
|
|
45
|
+
await fsp.mkdir(path.join(homeDir, '.claude', 'agents'), { recursive: true });
|
|
46
|
+
await fsp.writeFile(path.join(homeDir, '.claude', 'agents', 'dev-lead.md'), '# dev-lead', 'utf8');
|
|
47
|
+
await writeJson(path.join(cwd, '.claude', 'settings.json'), {
|
|
48
|
+
hooks: {
|
|
49
|
+
PreToolUse: [
|
|
50
|
+
{
|
|
51
|
+
matcher: 'Write|Edit|NotebookEdit',
|
|
52
|
+
hooks: [{ type: 'command', command: 'node scripts/hooks/guard-prd-writes.cjs' }],
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return { homeDir, cwd };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
test('all four checks pass on a fully-configured project', async () => {
|
|
62
|
+
const { homeDir, cwd } = await makeGreenFixtures();
|
|
63
|
+
const result = checkDelegationReadiness({ cwd, homeDir });
|
|
64
|
+
|
|
65
|
+
expect(result.ok).toBe(true);
|
|
66
|
+
expect(result.checks).toHaveLength(4);
|
|
67
|
+
expect(result.checks.every((c) => c.ok)).toBe(true);
|
|
68
|
+
expect(result.checks.map((c) => c.id)).toEqual([
|
|
69
|
+
'scheduler-mcp',
|
|
70
|
+
'dev-plugin',
|
|
71
|
+
'agent-personas',
|
|
72
|
+
'prd-write-guard',
|
|
73
|
+
]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('scheduler-mcp: passes via project-scope .mcp.json even without user scope', async () => {
|
|
77
|
+
const { homeDir, cwd } = await makeGreenFixtures();
|
|
78
|
+
await writeJson(path.join(homeDir, '.claude.json'), { mcpServers: {} });
|
|
79
|
+
await writeJson(path.join(cwd, '.mcp.json'), {
|
|
80
|
+
mcpServers: { 'session-manager-scheduler': { command: 'node', args: [] } },
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const result = checkDelegationReadiness({ cwd, homeDir });
|
|
84
|
+
const check = result.checks.find((c) => c.id === 'scheduler-mcp');
|
|
85
|
+
expect(check.ok).toBe(true);
|
|
86
|
+
expect(result.ok).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('scheduler-mcp: fails with a runnable fix when absent from both scopes', async () => {
|
|
90
|
+
const { homeDir, cwd } = await makeGreenFixtures();
|
|
91
|
+
await writeJson(path.join(homeDir, '.claude.json'), { mcpServers: {} });
|
|
92
|
+
|
|
93
|
+
const result = checkDelegationReadiness({ cwd, homeDir });
|
|
94
|
+
const check = result.checks.find((c) => c.id === 'scheduler-mcp');
|
|
95
|
+
expect(check.ok).toBe(false);
|
|
96
|
+
expect(result.ok).toBe(false);
|
|
97
|
+
expect(check.fix).toMatch(/^claude mcp add session-manager-scheduler --scope user/);
|
|
98
|
+
expect(check.fix).toContain('scheduler-mcp-server.cjs');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('dev-plugin: fails independently when enabledPlugins is missing the key', async () => {
|
|
102
|
+
const { homeDir, cwd } = await makeGreenFixtures();
|
|
103
|
+
await writeJson(path.join(homeDir, '.claude', 'settings.json'), { enabledPlugins: {} });
|
|
104
|
+
|
|
105
|
+
const result = checkDelegationReadiness({ cwd, homeDir });
|
|
106
|
+
const check = result.checks.find((c) => c.id === 'dev-plugin');
|
|
107
|
+
expect(check.ok).toBe(false);
|
|
108
|
+
expect(result.ok).toBe(false);
|
|
109
|
+
expect(check.fix).toBeTruthy();
|
|
110
|
+
// the other three checks still pass independently
|
|
111
|
+
expect(result.checks.filter((c) => c.id !== 'dev-plugin').every((c) => c.ok)).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('agent-personas: fails independently when ~/.claude/agents has no personas', async () => {
|
|
115
|
+
const { homeDir, cwd } = await makeGreenFixtures();
|
|
116
|
+
await fsp.rm(path.join(homeDir, '.claude', 'agents', 'dev-lead.md'));
|
|
117
|
+
|
|
118
|
+
const result = checkDelegationReadiness({ cwd, homeDir });
|
|
119
|
+
const check = result.checks.find((c) => c.id === 'agent-personas');
|
|
120
|
+
expect(check.ok).toBe(false);
|
|
121
|
+
expect(result.ok).toBe(false);
|
|
122
|
+
expect(result.checks.filter((c) => c.id !== 'agent-personas').every((c) => c.ok)).toBe(true);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('agent-personas: passes via project-scope .claude/agents/ even without global personas', async () => {
|
|
126
|
+
const { homeDir, cwd } = await makeGreenFixtures();
|
|
127
|
+
await fsp.rm(path.join(homeDir, '.claude', 'agents'), { recursive: true, force: true });
|
|
128
|
+
await fsp.mkdir(path.join(cwd, '.claude', 'agents'), { recursive: true });
|
|
129
|
+
await fsp.writeFile(path.join(cwd, '.claude', 'agents', 'builder.md'), '# builder', 'utf8');
|
|
130
|
+
|
|
131
|
+
const result = checkDelegationReadiness({ cwd, homeDir });
|
|
132
|
+
const check = result.checks.find((c) => c.id === 'agent-personas');
|
|
133
|
+
expect(check.ok).toBe(true);
|
|
134
|
+
expect(result.ok).toBe(true);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('agent-personas: fails when the directory does not exist at all', async () => {
|
|
138
|
+
const { homeDir, cwd } = await makeGreenFixtures();
|
|
139
|
+
await fsp.rm(path.join(homeDir, '.claude', 'agents'), { recursive: true, force: true });
|
|
140
|
+
|
|
141
|
+
const result = checkDelegationReadiness({ cwd, homeDir });
|
|
142
|
+
const check = result.checks.find((c) => c.id === 'agent-personas');
|
|
143
|
+
expect(check.ok).toBe(false);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('prd-write-guard: fails independently when the hook is missing', async () => {
|
|
147
|
+
const { homeDir, cwd } = await makeGreenFixtures();
|
|
148
|
+
await writeJson(path.join(cwd, '.claude', 'settings.json'), { hooks: { PreToolUse: [] } });
|
|
149
|
+
|
|
150
|
+
const result = checkDelegationReadiness({ cwd, homeDir });
|
|
151
|
+
const check = result.checks.find((c) => c.id === 'prd-write-guard');
|
|
152
|
+
expect(check.ok).toBe(false);
|
|
153
|
+
expect(result.ok).toBe(false);
|
|
154
|
+
expect(result.checks.filter((c) => c.id !== 'prd-write-guard').every((c) => c.ok)).toBe(true);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('unparseable JSON files yield ok:false with a detail, never a throw', async () => {
|
|
158
|
+
const { homeDir, cwd } = await makeGreenFixtures();
|
|
159
|
+
await fs.promises.writeFile(path.join(homeDir, '.claude.json'), '{ not valid json', 'utf8');
|
|
160
|
+
await fs.promises.writeFile(path.join(homeDir, '.claude', 'settings.json'), '{ not valid json', 'utf8');
|
|
161
|
+
await fs.promises.writeFile(path.join(cwd, '.claude', 'settings.json'), '{ not valid json', 'utf8');
|
|
162
|
+
|
|
163
|
+
expect(() => checkDelegationReadiness({ cwd, homeDir })).not.toThrow();
|
|
164
|
+
const result = checkDelegationReadiness({ cwd, homeDir });
|
|
165
|
+
|
|
166
|
+
const scheduler = result.checks.find((c) => c.id === 'scheduler-mcp');
|
|
167
|
+
const devPlugin = result.checks.find((c) => c.id === 'dev-plugin');
|
|
168
|
+
const guard = result.checks.find((c) => c.id === 'prd-write-guard');
|
|
169
|
+
expect(scheduler.ok).toBe(false);
|
|
170
|
+
expect(scheduler.detail).toBeTruthy();
|
|
171
|
+
expect(devPlugin.ok).toBe(false);
|
|
172
|
+
expect(devPlugin.detail).toBeTruthy();
|
|
173
|
+
expect(guard.ok).toBe(false);
|
|
174
|
+
expect(guard.detail).toBeTruthy();
|
|
175
|
+
expect(result.ok).toBe(false);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('missing cwd/.claude/settings.json entirely is treated as guard-absent, not a throw', async () => {
|
|
179
|
+
const { homeDir, cwd } = await makeGreenFixtures();
|
|
180
|
+
await fsp.rm(path.join(cwd, '.claude', 'settings.json'));
|
|
181
|
+
|
|
182
|
+
expect(() => checkDelegationReadiness({ cwd, homeDir })).not.toThrow();
|
|
183
|
+
const result = checkDelegationReadiness({ cwd, homeDir });
|
|
184
|
+
expect(result.checks.find((c) => c.id === 'prd-write-guard').ok).toBe(false);
|
|
185
|
+
});
|