claude-code-session-manager 0.95.0 → 0.96.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/{DataModel-DZK9QZpV.js → DataModel-T0D00BpK.js} +1 -1
- package/dist/assets/History-DuAovTr7.js +2 -0
- package/dist/assets/{Hooks-CmK6l0uf.js → Hooks-CgP9jsAN.js} +3 -3
- package/dist/assets/{HostBilko-D8TIx-Bx.js → HostBilko-DHfz3Mxa.js} +1 -1
- package/dist/assets/{Library-DTxpOjx8.js → Library--poVP-d1.js} +1 -1
- package/dist/assets/{MarkdownEditor-BPJyrevq.js → MarkdownEditor-C56-Pc4c.js} +1 -1
- package/dist/assets/{McpServers-B_rvIYDs.js → McpServers-D-ykP4Vi.js} +2 -2
- package/dist/assets/Memory-BrkJjRL8.js +8 -0
- package/dist/assets/{Permissions-DhMMp27n.js → Permissions-BMpAaxJu.js} +3 -3
- package/dist/assets/Plugins-OuEe5S_T.js +2 -0
- package/dist/assets/{ProvenanceBadge-ptlItdCK.js → ProvenanceBadge-4eNbDYe8.js} +1 -1
- package/dist/assets/{SaveBar-CSbNSl2W.js → SaveBar-BQ8y9Lkv.js} +1 -1
- package/dist/assets/Scheduler-DcMT5sT_.js +16 -0
- package/dist/assets/{ScopeSwitcher-BJof-GWH.js → ScopeSwitcher-XdxtVKP-.js} +1 -1
- package/dist/assets/Settings-FdaPUq2t.js +3 -0
- package/dist/assets/{SkillReferenceGraph-CGl4B89-.js → SkillReferenceGraph-CJzvvc0_.js} +1 -1
- package/dist/assets/Skills-Wvm9OE1e.js +3 -0
- package/dist/assets/{SystemPrompt-DKj-q39J.js → SystemPrompt--LYw_69j.js} +1 -1
- package/dist/assets/TagLibrary-Be6HbMMx.js +1 -0
- package/dist/assets/{TiptapBody-BbcJL9az.js → TiptapBody-DzFlWMgO.js} +1 -1
- package/dist/assets/{Toggle-DAslvnnv.js → Toggle-BxtAFE1i.js} +1 -1
- package/dist/assets/index-XkrLB67D.js +3079 -0
- package/dist/assets/{settingsSchema-pzp0TgvN.js → settingsSchema-BIvDZ1Cj.js} +1 -1
- package/dist/index.html +1 -1
- package/package.json +8 -2
- package/plugins/CLAUDE.md +3 -3
- package/plugins/session-manager-dev/skills/develop/SKILL.md +83 -67
- package/plugins/session-manager-dev/skills/develop/standards.md +5 -11
- package/plugins/session-manager-dev/skills/requesting-code-review/SKILL.md +5 -15
- package/scripts/README.md +1 -0
- package/scripts/hooks/guard-destructive-git.cjs +9 -374
- package/scripts/hooks/guard-inline-implementation.cjs +9 -128
- package/scripts/hooks/guard-prd-writes.cjs +9 -74
- package/scripts/hooks/guard-self-schedule.cjs +9 -61
- package/scripts/hooks/lib/guard-destructive-git-policy.cjs +410 -0
- package/scripts/hooks/lib/guard-inline-implementation-policy.cjs +159 -0
- package/scripts/hooks/lib/guard-prd-writes-policy.cjs +100 -0
- package/scripts/hooks/lib/guard-self-schedule-policy.cjs +89 -0
- package/scripts/scheduler-mcp-server.cjs +8 -1
- package/src/main/__tests__/configWriteBoundaryOwners.test.cjs +59 -0
- package/src/main/__tests__/exchanges.test.cjs +41 -96
- package/src/main/__tests__/exchangesPromptId.test.cjs +16 -25
- package/src/main/__tests__/files-reject-credentials.test.cjs +1 -1
- package/src/main/__tests__/health-credentials.test.cjs +81 -0
- package/src/main/__tests__/memoryAggregate.test.cjs +40 -1
- package/src/main/__tests__/opsErrorLogTelemetryTap.test.cjs +1 -1
- package/src/main/__tests__/planValidator.test.cjs +71 -0
- package/src/main/__tests__/prdCreate.test.cjs +3 -1
- package/src/main/__tests__/prdSizing.test.cjs +106 -0
- package/src/main/__tests__/queue-starvation-dispatch-driver.test.cjs +46 -0
- package/src/main/__tests__/reconcileFlatPrdSweep.test.cjs +10 -0
- package/src/main/__tests__/scheduler-looks-done.test.cjs +40 -0
- package/src/main/__tests__/scheduler-notify-originating-tab.test.cjs +81 -0
- package/src/main/__tests__/scheduler-shard-quarantine.test.cjs +6 -1
- package/src/main/__tests__/schedulerStateSidecarRestore.test.cjs +110 -0
- package/src/main/__tests__/seedAgentPersonas.test.cjs +19 -1
- package/src/main/__tests__/seedValidatorPersona.test.cjs +116 -0
- package/src/main/__tests__/telemetryClient.test.cjs +2 -2
- package/src/main/__tests__/telemetryContract.test.cjs +2 -2
- package/src/main/__tests__/usageSingleFlight.test.cjs +10 -0
- package/src/main/__tests__/validationSentinels.test.cjs +84 -0
- package/src/main/build-info.json +4 -4
- package/src/main/config.cjs +26 -14
- package/src/main/exchanges.cjs +26 -21
- package/src/main/health.cjs +66 -4
- package/src/main/index.cjs +1 -1
- package/src/main/ipcSchemas.cjs +5 -1
- package/src/main/lib/__tests__/crashTelemetry.test.cjs +1 -1
- package/src/main/lib/__tests__/credentials-futile-refresh.test.cjs +115 -0
- package/src/main/lib/__tests__/epicWorktreeProjectConfig.test.cjs +85 -27
- package/src/main/lib/__tests__/gitWorktree.test.cjs +21 -27
- package/src/main/lib/__tests__/guardShims.test.cjs +10 -3
- package/src/main/lib/__tests__/loadGate.test.cjs +44 -1
- package/src/main/lib/__tests__/opsOwnership.test.cjs +14 -0
- package/src/main/lib/__tests__/telemetryBacklog.test.cjs +4 -4
- package/src/main/lib/__tests__/telemetryConsent.test.cjs +2 -2
- package/src/main/lib/__tests__/telemetryCountersMetadataColumn.test.cjs +1 -1
- package/src/main/lib/agentModelResolve.cjs +1 -0
- package/src/main/lib/credentials.cjs +62 -0
- package/src/main/lib/cwdClassify.cjs +15 -0
- package/src/main/lib/definitionOfDone.cjs +9 -13
- package/src/main/lib/effectiveModelInfo.cjs +2 -9
- package/src/main/lib/epicWorktreeProjectConfig.cjs +75 -31
- package/src/main/lib/loadGate.cjs +29 -5
- package/src/main/lib/opsOwnership.cjs +23 -0
- package/src/main/lib/planValidator.cjs +28 -0
- package/src/main/lib/prdCreate.cjs +8 -4
- package/src/main/lib/prdSizing.cjs +83 -0
- package/src/main/lib/runLogRetention.cjs +2 -8
- package/src/main/lib/telemetryBacklog.cjs +9 -3
- package/src/main/lib/telemetryClient.cjs +12 -3
- package/src/main/lib/telemetrySettings.cjs +8 -2
- package/src/main/lib/validationSentinels.cjs +29 -0
- package/src/main/memoryAggregate.cjs +36 -11
- package/src/main/otelSettings.cjs +8 -2
- package/src/main/scheduler.cjs +215 -31
- package/src/main/seedAgentPersonas.cjs +1 -1
- package/src/main/sessionsStore.cjs +8 -2
- package/src/main/supervisor.cjs +3 -3
- package/src/main/templates/PRD_AUTHORING.md +9 -9
- package/src/main/voiceSettings.cjs +8 -2
- package/src/preload/api.d.ts +3 -3
- package/src/preload/index.cjs +5 -5
- package/src/seed/agents/dev-lead.md +24 -15
- package/src/seed/agents/validator.md +33 -0
- package/dist/assets/History-BDyJUGtR.js +0 -2
- package/dist/assets/Memory-B-9GlHqe.js +0 -8
- package/dist/assets/Plugins-DKfAlmMY.js +0 -2
- package/dist/assets/Scheduler--rn1QBYc.js +0 -16
- package/dist/assets/Settings-Dsx6XZP5.js +0 -3
- package/dist/assets/Skills-D522Zfol.js +0 -3
- package/dist/assets/TagLibrary-BH7CG7-6.js +0 -1
- package/dist/assets/index-Yf8e7VCB.js +0 -3079
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* seedValidatorPersona.test.cjs — covers the `validator` persona's delivery
|
|
3
|
+
* through seedAgentPersonas.cjs's existing "names not yet in `seeded`" path.
|
|
4
|
+
*
|
|
5
|
+
* Run: timeout 120 npx vitest run src/main/__tests__/seedValidatorPersona.test.cjs
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
import { test, expect, beforeEach, afterEach } from 'vitest';
|
|
11
|
+
const fs = require('node:fs');
|
|
12
|
+
const os = require('node:os');
|
|
13
|
+
const path = require('node:path');
|
|
14
|
+
|
|
15
|
+
let tmpHome;
|
|
16
|
+
let realHome;
|
|
17
|
+
let seedAgentPersonas;
|
|
18
|
+
|
|
19
|
+
// config.cjs bakes os.homedir() into top-level consts at first require
|
|
20
|
+
// (allowedRoots/WRITE_PREFIXES, used by writeJsonSync) — must be purged
|
|
21
|
+
// alongside seedAgentPersonas.cjs so each test's tmpHome takes effect.
|
|
22
|
+
const MODULES_TO_RELOAD = ['../seedAgentPersonas.cjs', '../config.cjs'];
|
|
23
|
+
|
|
24
|
+
function purgeRequireCache() {
|
|
25
|
+
for (const m of MODULES_TO_RELOAD) {
|
|
26
|
+
try { delete require.cache[require.resolve(m)]; } catch { /* not loaded yet */ }
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
realHome = process.env.HOME;
|
|
32
|
+
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'seed-validator-persona-test-'));
|
|
33
|
+
process.env.HOME = tmpHome;
|
|
34
|
+
delete process.env.SM_SEED_AGENT_PERSONAS_DISABLE;
|
|
35
|
+
purgeRequireCache();
|
|
36
|
+
({ seedAgentPersonas } = require('../seedAgentPersonas.cjs'));
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
process.env.HOME = realHome;
|
|
41
|
+
delete process.env.SM_SEED_AGENT_PERSONAS_DISABLE;
|
|
42
|
+
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
43
|
+
purgeRequireCache();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
function agentsDir() {
|
|
47
|
+
return path.join(tmpHome, '.claude', 'agents');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function markerPath() {
|
|
51
|
+
return path.join(tmpHome, '.claude', 'session-manager', '.agent-personas-seeded');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function silentLogger() {
|
|
55
|
+
return { log: () => {}, warn: () => {} };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const ALL_PERSONAS = ['architect', 'dev-lead', 'project-home-builder', 'validator'];
|
|
59
|
+
const seedSrc = (name) => path.join(__dirname, '..', '..', 'seed', 'agents', `${name}.md`);
|
|
60
|
+
|
|
61
|
+
test('a fresh homedir gets all four persona files', async () => {
|
|
62
|
+
await seedAgentPersonas({ logger: silentLogger() });
|
|
63
|
+
|
|
64
|
+
for (const name of ALL_PERSONAS) {
|
|
65
|
+
const dest = path.join(agentsDir(), `${name}.md`);
|
|
66
|
+
expect(fs.existsSync(dest)).toBe(true);
|
|
67
|
+
expect(fs.readFileSync(dest, 'utf8')).toBe(fs.readFileSync(seedSrc(name), 'utf8'));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const marker = JSON.parse(fs.readFileSync(markerPath(), 'utf8'));
|
|
71
|
+
expect(marker.seeded.sort()).toEqual([...ALL_PERSONAS].sort());
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('a homedir whose marker already lists the first three receives validator.md, and the other three files are byte-identical before and after', async () => {
|
|
75
|
+
const mPath = markerPath();
|
|
76
|
+
fs.mkdirSync(path.dirname(mPath), { recursive: true });
|
|
77
|
+
fs.writeFileSync(
|
|
78
|
+
mPath,
|
|
79
|
+
JSON.stringify({ seeded: ['architect', 'dev-lead', 'project-home-builder'], attempts: 0, ts: '2026-01-01T00:00:00.000Z' })
|
|
80
|
+
);
|
|
81
|
+
fs.mkdirSync(agentsDir(), { recursive: true });
|
|
82
|
+
const preExisting = {};
|
|
83
|
+
for (const name of ['architect', 'dev-lead', 'project-home-builder']) {
|
|
84
|
+
const content = `existing ${name} content\n`;
|
|
85
|
+
fs.writeFileSync(path.join(agentsDir(), `${name}.md`), content);
|
|
86
|
+
preExisting[name] = content;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
expect(fs.existsSync(path.join(agentsDir(), 'validator.md'))).toBe(false);
|
|
90
|
+
|
|
91
|
+
await seedAgentPersonas({ logger: silentLogger() });
|
|
92
|
+
|
|
93
|
+
expect(fs.readFileSync(path.join(agentsDir(), 'validator.md'), 'utf8')).toBe(fs.readFileSync(seedSrc('validator'), 'utf8'));
|
|
94
|
+
|
|
95
|
+
for (const name of ['architect', 'dev-lead', 'project-home-builder']) {
|
|
96
|
+
expect(fs.readFileSync(path.join(agentsDir(), `${name}.md`), 'utf8')).toBe(preExisting[name]);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const marker = JSON.parse(fs.readFileSync(mPath, 'utf8'));
|
|
100
|
+
expect(marker.seeded.sort()).toEqual([...ALL_PERSONAS].sort());
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('the seeded validator body is under AGENT_BODY_CHAR_CAP and contains both required sentinel lines', () => {
|
|
104
|
+
const { AGENT_BODY_CHAR_CAP } = require('../lib/agentModelResolve.cjs');
|
|
105
|
+
const { splitFrontmatter } = require('../lib/prdFrontmatter.cjs');
|
|
106
|
+
const raw = fs.readFileSync(seedSrc('validator'), 'utf8');
|
|
107
|
+
const { body } = splitFrontmatter(raw);
|
|
108
|
+
const trimmed = body.trim();
|
|
109
|
+
|
|
110
|
+
expect(trimmed.length).toBeLessThan(AGENT_BODY_CHAR_CAP);
|
|
111
|
+
expect(trimmed).toContain('VALIDATION: <slug> VERIFIED');
|
|
112
|
+
expect(trimmed).toContain('SCHEDULER_VERDICT: PASS');
|
|
113
|
+
expect(trimmed).not.toContain('Do not touch:');
|
|
114
|
+
expect(trimmed).not.toContain('Report explicitly:');
|
|
115
|
+
expect(trimmed.endsWith('report them.')).toBe(true);
|
|
116
|
+
});
|
|
@@ -78,8 +78,8 @@ function freshClient(home, profileOverrides) {
|
|
|
78
78
|
// Explicit opt-in: this suite deliberately exercises telemetryClient's real
|
|
79
79
|
// queue/sent-file I/O, so it must override the test-environment no-op guard
|
|
80
80
|
// by pointing the spool at this test's own isolated tmp dir (never a real
|
|
81
|
-
// user's ~/.
|
|
82
|
-
process.env.SM_TELEMETRY_SPOOL = path.join(home, '.
|
|
81
|
+
// user's ~/.claude/session-manager).
|
|
82
|
+
process.env.SM_TELEMETRY_SPOOL = path.join(home, '.claude', 'session-manager');
|
|
83
83
|
for (const p of ['../lib/telemetryClient.cjs', '../config.cjs', '../lib/telemetrySettings.cjs', '../lib/machineProfile.cjs']) {
|
|
84
84
|
const resolved = require.resolve(p);
|
|
85
85
|
delete require.cache[resolved];
|
|
@@ -348,7 +348,7 @@ function freshClient(home, profileOverrides) {
|
|
|
348
348
|
process.env.HOME = home;
|
|
349
349
|
// Explicit opt-in: overrides the test-environment no-op guard so this
|
|
350
350
|
// suite's real telemetryClient calls actually persist into an isolated dir.
|
|
351
|
-
process.env.SM_TELEMETRY_SPOOL = path.join(home, '.
|
|
351
|
+
process.env.SM_TELEMETRY_SPOOL = path.join(home, '.claude', 'session-manager');
|
|
352
352
|
for (const p of HOME_DEPENDENT_MODULES) {
|
|
353
353
|
const resolved = require.resolve(p);
|
|
354
354
|
delete require.cache[resolved];
|
|
@@ -695,7 +695,7 @@ test('records queued, a 503 mid-drain, then a fresh client flushing successfully
|
|
|
695
695
|
|
|
696
696
|
expect(stubServer.state.errors.length).toBe(before); // nothing landed during the failure
|
|
697
697
|
|
|
698
|
-
const wmPath = path.join(home, '.
|
|
698
|
+
const wmPath = path.join(home, '.claude', 'session-manager', 'telemetry-watermarks.json');
|
|
699
699
|
const wmAfterFailure = JSON.parse(await fsp.readFile(wmPath, 'utf8'));
|
|
700
700
|
const entryAfterFailure = Object.values(wmAfterFailure).find((e) => e && typeof e === 'object' && 'bytesConfirmed' in e);
|
|
701
701
|
expect(entryAfterFailure.bytesConfirmed).toBe(0); // bytesConfirmed frozen during the failed phase
|
|
@@ -88,6 +88,7 @@ afterEach(() => {
|
|
|
88
88
|
process.env.HOME = realHome;
|
|
89
89
|
vi.unstubAllGlobals();
|
|
90
90
|
vi.restoreAllMocks();
|
|
91
|
+
vi.useRealTimers();
|
|
91
92
|
purgeRequireCache();
|
|
92
93
|
// maxRetries: a fire-and-forget cache write inside usage.cjs can land in .claude/ mid-rm
|
|
93
94
|
// (ENOTEMPTY) when the machine is loaded; this is cleanup robustness, not a test retry.
|
|
@@ -112,6 +113,15 @@ test('two concurrent fetchUsage() calls produce exactly one HTTP fetch', async (
|
|
|
112
113
|
});
|
|
113
114
|
|
|
114
115
|
test('an ok result resets the shared circuit streak built up by prior failures', async () => {
|
|
116
|
+
// The breaker's open->half_open transition (usageCircuit.cjs's state())
|
|
117
|
+
// compares real Date.now() against a jittered 15-30s backoff deadline. On
|
|
118
|
+
// a loaded machine, real wall-clock time can drift far enough between the
|
|
119
|
+
// 3rd failure and the assertions below for that deadline to pass, flipping
|
|
120
|
+
// 'open' to 'half_open' out from under this test. Freeze time so the
|
|
121
|
+
// open/closed assertions are decided by call sequence, not the real clock.
|
|
122
|
+
vi.useFakeTimers();
|
|
123
|
+
vi.setSystemTime(Date.now());
|
|
124
|
+
|
|
115
125
|
const usage = require('../usage.cjs');
|
|
116
126
|
const circuit = usage.__usageCircuitForTest;
|
|
117
127
|
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* validationSentinels.test.cjs — unit tests for parseValidationSentinels
|
|
3
|
+
* (PRD 1407): a plan-level validator job ends its run with one
|
|
4
|
+
* `VALIDATION: <slug> VERIFIED` or `VALIDATION: <slug> REFUTED — <reason>`
|
|
5
|
+
* line per PRD; this module extracts those lines from the job's result text.
|
|
6
|
+
*
|
|
7
|
+
* Run: timeout 300 npx vitest run src/main/__tests__/validationSentinels.test.cjs
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
import { test, expect } from 'vitest';
|
|
13
|
+
const { parseValidationSentinels } = require('../lib/validationSentinels.cjs');
|
|
14
|
+
|
|
15
|
+
test('parses a VERIFIED line with no reason', () => {
|
|
16
|
+
expect(parseValidationSentinels('VALIDATION: 123-foo VERIFIED')).toEqual([
|
|
17
|
+
{ slug: '123-foo', verdict: 'verified', reason: '' },
|
|
18
|
+
]);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('parses a REFUTED line with an em-dash reason', () => {
|
|
22
|
+
expect(parseValidationSentinels('VALIDATION: 123-foo REFUTED — missing test coverage')).toEqual([
|
|
23
|
+
{ slug: '123-foo', verdict: 'refuted', reason: 'missing test coverage' },
|
|
24
|
+
]);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('parses a REFUTED line with an en-dash reason', () => {
|
|
28
|
+
expect(parseValidationSentinels('VALIDATION: 123-foo REFUTED – bad wiring')).toEqual([
|
|
29
|
+
{ slug: '123-foo', verdict: 'refuted', reason: 'bad wiring' },
|
|
30
|
+
]);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('parses a REFUTED line with a hyphen reason', () => {
|
|
34
|
+
expect(parseValidationSentinels('VALIDATION: 123-foo REFUTED - wrong file touched')).toEqual([
|
|
35
|
+
{ slug: '123-foo', verdict: 'refuted', reason: 'wrong file touched' },
|
|
36
|
+
]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('parses a REFUTED line with no reason', () => {
|
|
40
|
+
expect(parseValidationSentinels('VALIDATION: 123-foo REFUTED')).toEqual([
|
|
41
|
+
{ slug: '123-foo', verdict: 'refuted', reason: '' },
|
|
42
|
+
]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('ignores lowercase verified/refuted', () => {
|
|
46
|
+
expect(parseValidationSentinels('VALIDATION: 123-foo verified')).toEqual([]);
|
|
47
|
+
expect(parseValidationSentinels('VALIDATION: 123-foo refuted')).toEqual([]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('ignores every other line in a multi-line transcript', () => {
|
|
51
|
+
const text = [
|
|
52
|
+
'Some preamble text',
|
|
53
|
+
'Running gate...',
|
|
54
|
+
'VALIDATION: 111-alpha VERIFIED',
|
|
55
|
+
'random noise mentioning VALIDATION: not-a-real-line',
|
|
56
|
+
'VALIDATION: 222-beta REFUTED — the fix was incomplete',
|
|
57
|
+
'trailing notes',
|
|
58
|
+
].join('\n');
|
|
59
|
+
|
|
60
|
+
expect(parseValidationSentinels(text)).toEqual([
|
|
61
|
+
{ slug: '111-alpha', verdict: 'verified', reason: '' },
|
|
62
|
+
{ slug: '222-beta', verdict: 'refuted', reason: 'the fix was incomplete' },
|
|
63
|
+
]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('last line per slug wins, order of first appearance kept', () => {
|
|
67
|
+
const text = [
|
|
68
|
+
'VALIDATION: 111-alpha VERIFIED',
|
|
69
|
+
'VALIDATION: 222-beta REFUTED — first reason',
|
|
70
|
+
'VALIDATION: 111-alpha REFUTED — actually broken',
|
|
71
|
+
'VALIDATION: 222-beta VERIFIED',
|
|
72
|
+
].join('\n');
|
|
73
|
+
|
|
74
|
+
expect(parseValidationSentinels(text)).toEqual([
|
|
75
|
+
{ slug: '111-alpha', verdict: 'refuted', reason: 'actually broken' },
|
|
76
|
+
{ slug: '222-beta', verdict: 'verified', reason: '' },
|
|
77
|
+
]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('returns [] for null, undefined, and empty input', () => {
|
|
81
|
+
expect(parseValidationSentinels(null)).toEqual([]);
|
|
82
|
+
expect(parseValidationSentinels(undefined)).toEqual([]);
|
|
83
|
+
expect(parseValidationSentinels('')).toEqual([]);
|
|
84
|
+
});
|
package/src/main/build-info.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.
|
|
3
|
-
"gitSha": "
|
|
4
|
-
"gitShortSha": "
|
|
2
|
+
"version": "0.96.0",
|
|
3
|
+
"gitSha": "32cc4ebad387727ee0d691ffcf524d938efdcfca",
|
|
4
|
+
"gitShortSha": "32cc4eba",
|
|
5
5
|
"gitBranch": "HEAD",
|
|
6
|
-
"builtAt": "2026-09-
|
|
6
|
+
"builtAt": "2026-09-24T19:16:51.123Z",
|
|
7
7
|
"dirty": false
|
|
8
8
|
}
|
package/src/main/config.cjs
CHANGED
|
@@ -19,7 +19,7 @@ const fs = require('node:fs');
|
|
|
19
19
|
const fsp = require('node:fs/promises');
|
|
20
20
|
const path = require('node:path');
|
|
21
21
|
const os = require('node:os');
|
|
22
|
-
const { assertOpsWrite, opsPath } = require('./lib/opsOwnership.cjs');
|
|
22
|
+
const { assertOpsWrite, opsPath, OWNERS } = require('./lib/opsOwnership.cjs');
|
|
23
23
|
const chokidar = require('chokidar');
|
|
24
24
|
const logs = require('./logs.cjs');
|
|
25
25
|
const { sendIfAlive } = require('./lib/sendToRenderer.cjs');
|
|
@@ -113,7 +113,6 @@ const WRITE_PREFIXES = [
|
|
|
113
113
|
path.join(os.homedir(), '.claude'),
|
|
114
114
|
path.join(os.homedir(), '.claude.json'), // global MCP servers config
|
|
115
115
|
path.join(os.homedir(), '.config', 'claude-code'),
|
|
116
|
-
path.join(os.homedir(), '.config', 'session-manager'),
|
|
117
116
|
];
|
|
118
117
|
|
|
119
118
|
/**
|
|
@@ -144,17 +143,15 @@ function validateWrite(realAbs) {
|
|
|
144
143
|
if (realAbs === claudeSub || realAbs.startsWith(claudeSub + path.sep)) {
|
|
145
144
|
return;
|
|
146
145
|
}
|
|
147
|
-
// Writable ops namespaces under a registered project root —
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
// project-pages projectHomeAdminRoutes.cjs's project_home_write
|
|
157
|
-
// path (home.html)
|
|
146
|
+
// Writable ops namespaces under a registered project root — every
|
|
147
|
+
// namespace declared in opsOwnership.cjs's OWNERS map is writable here
|
|
148
|
+
// by construction (PRD 1410): OWNERS is the single source of truth for
|
|
149
|
+
// "this namespace exists and has an owner", so a namespace can never
|
|
150
|
+
// again be declared owned yet unwritable (incident: PRD 1398 added
|
|
151
|
+
// 'ui-prefs' to OWNERS but this list was hand-maintained separately and
|
|
152
|
+
// never updated, so every ui-prefs write threw here before
|
|
153
|
+
// assertOpsWrite's per-writer check ever ran). Which WRITER may use a
|
|
154
|
+
// given namespace is still assertOpsWrite's job below, not this list's.
|
|
158
155
|
// Paths come from opsOwnership.opsPath (PRD 1082) — the one ops-root
|
|
159
156
|
// resolver — which THROWS for an ephemeral root (a linked worktree that
|
|
160
157
|
// pty.cjs registered as an allowed root for its spawn cwd, or
|
|
@@ -162,7 +159,7 @@ function validateWrite(realAbs) {
|
|
|
162
159
|
// never a writable destination, so its exemptions simply do not exist.
|
|
163
160
|
let opsSubs;
|
|
164
161
|
try {
|
|
165
|
-
opsSubs =
|
|
162
|
+
opsSubs = Object.keys(OWNERS).map((ns) => opsPath(realRoot, ns));
|
|
166
163
|
} catch {
|
|
167
164
|
opsSubs = [];
|
|
168
165
|
}
|
|
@@ -313,6 +310,20 @@ function writeJsonSync(abs, data, opts = {}) {
|
|
|
313
310
|
return { ok: true, mtimeMs: stat.mtimeMs };
|
|
314
311
|
}
|
|
315
312
|
|
|
313
|
+
/**
|
|
314
|
+
* One-shot best-effort move of a legacy config file to its new home. No-op
|
|
315
|
+
* (silent) when the new path already exists or the old path never existed —
|
|
316
|
+
* either way there is nothing to migrate. Same-filesystem rename is atomic,
|
|
317
|
+
* so there is no partial-write window to worry about.
|
|
318
|
+
*/
|
|
319
|
+
function migrateLegacyHomeFile(oldPath, newPath) {
|
|
320
|
+
try {
|
|
321
|
+
if (fs.existsSync(newPath) || !fs.existsSync(oldPath)) return;
|
|
322
|
+
fs.mkdirSync(path.dirname(newPath), { recursive: true });
|
|
323
|
+
fs.renameSync(oldPath, newPath);
|
|
324
|
+
} catch { /* best-effort; a stale/missing legacy file must never block boot */ }
|
|
325
|
+
}
|
|
326
|
+
|
|
316
327
|
async function listDir(abs, { filesOnly = false, dirsOnly = false, includeHidden = false } = {}) {
|
|
317
328
|
try {
|
|
318
329
|
abs = validatePath(expandHome(abs));
|
|
@@ -490,6 +501,7 @@ module.exports = {
|
|
|
490
501
|
readText,
|
|
491
502
|
writeJson,
|
|
492
503
|
writeJsonSync,
|
|
504
|
+
migrateLegacyHomeFile,
|
|
493
505
|
writeTextAtomic,
|
|
494
506
|
writeBinaryAtomic,
|
|
495
507
|
listDir,
|
package/src/main/exchanges.cjs
CHANGED
|
@@ -3,8 +3,15 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* exchanges.cjs — durable append-only log of completed terminal-chat exchanges.
|
|
5
5
|
*
|
|
6
|
-
* Appends one NDJSON record per successful chat run to
|
|
7
|
-
*
|
|
6
|
+
* Appends one NDJSON record per successful chat run to the per-project ops
|
|
7
|
+
* root (single-writer law, lib/opsOwnership.cjs — 'prompt-sessions' is owned
|
|
8
|
+
* by 'epics'):
|
|
9
|
+
* <cwd>/session-manager-operations/prompt-sessions/exchanges.jsonl
|
|
10
|
+
*
|
|
11
|
+
* Historical rows from before this PRD live on, untouched, in the old global
|
|
12
|
+
* store (~/.claude/knowledge-log/exchanges/<encodeCwd(cwd)>.jsonl) — migrating
|
|
13
|
+
* them would require the same unsafe cwd-decoding this store is meant to
|
|
14
|
+
* avoid, so they are simply left behind; only new exchanges land here.
|
|
8
15
|
*
|
|
9
16
|
* Record shape (contract for PRDs 324 + 325):
|
|
10
17
|
* { ts, sessionId, cwd, prompt, result, summary, degraded?, promptId? }
|
|
@@ -20,31 +27,30 @@
|
|
|
20
27
|
*/
|
|
21
28
|
|
|
22
29
|
const fsp = require('node:fs/promises');
|
|
23
|
-
const fs = require('node:fs');
|
|
24
30
|
const path = require('node:path');
|
|
25
|
-
const
|
|
26
|
-
const { encodeCwd } = require('./lib/encodeCwd.cjs');
|
|
27
|
-
const { validatePath } = require('./config.cjs');
|
|
31
|
+
const { opsPath, assertOpsWrite } = require('./lib/opsOwnership.cjs');
|
|
28
32
|
const { summarize } = require('./lib/summarize.cjs');
|
|
29
33
|
|
|
30
|
-
const
|
|
31
|
-
|
|
34
|
+
const EXCHANGES_WRITER = 'epics';
|
|
35
|
+
|
|
36
|
+
function exchangesFilePath(cwd) {
|
|
37
|
+
return opsPath(cwd, 'prompt-sessions', 'exchanges.jsonl');
|
|
38
|
+
}
|
|
32
39
|
|
|
33
40
|
/**
|
|
34
|
-
* Record a completed exchange. Creates the
|
|
35
|
-
*
|
|
36
|
-
* processes
|
|
37
|
-
* pipe-sized payloads
|
|
41
|
+
* Record a completed exchange. Creates the ops directory if needed. Appends
|
|
42
|
+
* one JSON line via raw fs O_APPEND (not config.cjs's writeJson) so
|
|
43
|
+
* concurrent writes from separate processes stay safe — each line is a
|
|
44
|
+
* single write, POSIX O_APPEND atomic for pipe-sized payloads. Because this
|
|
45
|
+
* bypasses config.cjs's normal write boundary, the single-writer law is
|
|
46
|
+
* asserted explicitly here.
|
|
38
47
|
*
|
|
39
48
|
* @param {{ sessionId: string, cwd: string, prompt: string, result: string, promptId?: string }} opts
|
|
40
49
|
* @returns {Promise<void>}
|
|
41
50
|
*/
|
|
42
51
|
async function recordExchange({ sessionId, cwd, prompt, result, promptId }) {
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
// Security: validate that the target path stays within home dir
|
|
47
|
-
validatePath(EXCHANGES_DIR);
|
|
52
|
+
const filePath = exchangesFilePath(cwd);
|
|
53
|
+
assertOpsWrite(filePath, EXCHANGES_WRITER);
|
|
48
54
|
|
|
49
55
|
// Summarize — always resolves; never throws
|
|
50
56
|
const { summary, model, degraded } = await summarize(result);
|
|
@@ -63,7 +69,7 @@ async function recordExchange({ sessionId, cwd, prompt, result, promptId }) {
|
|
|
63
69
|
|
|
64
70
|
const line = JSON.stringify(record) + '\n';
|
|
65
71
|
|
|
66
|
-
await fsp.mkdir(
|
|
72
|
+
await fsp.mkdir(path.dirname(filePath), { recursive: true });
|
|
67
73
|
await fsp.appendFile(filePath, line, { encoding: 'utf8' });
|
|
68
74
|
}
|
|
69
75
|
|
|
@@ -79,8 +85,7 @@ const DEFAULT_LIMIT = 100;
|
|
|
79
85
|
* @returns {Promise<object[]>}
|
|
80
86
|
*/
|
|
81
87
|
async function listExchanges({ cwd, sessionId, limit = DEFAULT_LIMIT, offset = 0 }) {
|
|
82
|
-
const
|
|
83
|
-
const filePath = path.join(EXCHANGES_DIR, `${encoded}.jsonl`);
|
|
88
|
+
const filePath = exchangesFilePath(cwd);
|
|
84
89
|
|
|
85
90
|
let stat;
|
|
86
91
|
try { stat = await fsp.stat(filePath); } catch { return []; }
|
|
@@ -128,4 +133,4 @@ async function listExchanges({ cwd, sessionId, limit = DEFAULT_LIMIT, offset = 0
|
|
|
128
133
|
return filtered.slice(offset, offset + limit);
|
|
129
134
|
}
|
|
130
135
|
|
|
131
|
-
module.exports = { recordExchange, listExchanges
|
|
136
|
+
module.exports = { recordExchange, listExchanges };
|
package/src/main/health.cjs
CHANGED
|
@@ -23,7 +23,7 @@ const { computeStallSummary, computeDepHistorySatisfaction, FAILURE_STREAK_ESCAL
|
|
|
23
23
|
const { findStarvedProjects, findUnresolvableDepRoots, DEFAULT_PROJECT_CWD, DEP_HISTORY_FAIL_OPEN } = require('./lib/schedulerBatch.cjs');
|
|
24
24
|
const { auditLogPath, readTail } = require('./lib/auditLog.cjs');
|
|
25
25
|
const { resolveBuildIdentity } = require('./lib/buildIdentity.cjs');
|
|
26
|
-
const {
|
|
26
|
+
const { computeReport, isRetentionEnabled, liveKeysFromJobs } = require('./lib/runLogRetention.cjs');
|
|
27
27
|
const { allProjectCwds } = require('./lib/activeSessions.cjs');
|
|
28
28
|
const { scanEpicTranscripts } = require('./lib/epicTranscriptDiagnostic.cjs');
|
|
29
29
|
|
|
@@ -627,6 +627,50 @@ function evaluateBuildFreshness({ running = null, installedOnDisk = null, repoHe
|
|
|
627
627
|
return { ok: true, running, installedOnDisk, repoHead, restartNeeded, publishNeeded, ...(notes.length ? { note: notes.join('; ') } : {}) };
|
|
628
628
|
}
|
|
629
629
|
|
|
630
|
+
/**
|
|
631
|
+
* evaluateCredentialHealth({ credState, now }) → { component, issues }
|
|
632
|
+
*
|
|
633
|
+
* Pure over its inputs — `credState` is the shape lib/credentials.cjs's
|
|
634
|
+
* refreshIfNeeded()/readCredentials() return ({ kind, message?, expiredAt? }),
|
|
635
|
+
* injected so this is testable without touching the Keychain. Surfaces the ONE
|
|
636
|
+
* failure that silently voids the whole scheduler on a user's machine: the
|
|
637
|
+
* OAuth token is expired (or missing) so every headless `claude -p` job fails
|
|
638
|
+
* to authenticate. The scheduler already pauses on a live 'auth' poll
|
|
639
|
+
* (scheduler.cjs setPaused('auth')), but nothing told the OPERATOR why the
|
|
640
|
+
* queue went quiet — `npm run health` read GREEN while no job could run. This
|
|
641
|
+
* names the cause and the fix (`claude login`).
|
|
642
|
+
*
|
|
643
|
+
* NON-CRITICAL by design: reported in components.credentials + issues so
|
|
644
|
+
* /local-project-health shows it, but it does not flip status.ok — health must
|
|
645
|
+
* not hard-fail a repo checkout just because THIS machine's token lapsed, and
|
|
646
|
+
* an expired token is an operator action (re-login), not a code defect. Maps:
|
|
647
|
+
* kind 'auth' → not ok, actionable `claude login` message (the real block)
|
|
648
|
+
* kind 'config' → not ok, credentials unreadable/absent
|
|
649
|
+
* kind 'ok'/'unsupported' → ok (token usable, or too-fresh-to-refresh)
|
|
650
|
+
*/
|
|
651
|
+
function evaluateCredentialHealth({ credState, now = Date.now() } = {}) {
|
|
652
|
+
const issues = [];
|
|
653
|
+
if (!credState || typeof credState !== 'object') {
|
|
654
|
+
return { component: { ok: true, state: 'unknown' }, issues };
|
|
655
|
+
}
|
|
656
|
+
const { kind } = credState;
|
|
657
|
+
if (kind === 'auth') {
|
|
658
|
+
const expiredAt = typeof credState.expiredAt === 'number' ? credState.expiredAt : null;
|
|
659
|
+
const agoMin = expiredAt ? Math.max(0, Math.round((now - expiredAt) / 60_000)) : null;
|
|
660
|
+
const when = expiredAt ? new Date(expiredAt).toISOString() : 'unknown';
|
|
661
|
+
const msg = `Claude credentials expired${agoMin !== null ? ` (${agoMin}m ago, at ${when})` : ''} — headless \`claude -p\` scheduler jobs cannot authenticate. Run \`claude login\` in a terminal to refresh.`;
|
|
662
|
+
issues.push(msg);
|
|
663
|
+
return { component: { ok: false, state: 'expired', expiredAt, message: msg }, issues };
|
|
664
|
+
}
|
|
665
|
+
if (kind === 'config') {
|
|
666
|
+
const msg = `Claude credentials unreadable: ${credState.message ?? 'unknown'} — scheduler jobs cannot authenticate. Run \`claude login\`.`;
|
|
667
|
+
issues.push(msg);
|
|
668
|
+
return { component: { ok: false, state: 'unreadable', message: msg }, issues };
|
|
669
|
+
}
|
|
670
|
+
// 'ok' (fresh) or 'unsupported' (auto-refresh unavailable but token still valid)
|
|
671
|
+
return { component: { ok: true, state: kind === 'unsupported' ? 'valid-no-refresh' : 'valid' }, issues };
|
|
672
|
+
}
|
|
673
|
+
|
|
630
674
|
/**
|
|
631
675
|
* evaluateStarveEscalationHealth(jobs, now, thresholdMs, escalationReasons)
|
|
632
676
|
* → { ok, projects?, message? }
|
|
@@ -1138,15 +1182,16 @@ async function check(opts = {}) {
|
|
|
1138
1182
|
// `config`, same home as every other scheduler machine setting.
|
|
1139
1183
|
try {
|
|
1140
1184
|
const retentionCfg = queueState?.config?.schedulerRunLogRetention;
|
|
1141
|
-
const
|
|
1185
|
+
const runsDir = schedulerPaths.runsDir();
|
|
1186
|
+
const liveKeys = liveKeysFromJobs(queueState?.jobs || [], { runsDir });
|
|
1142
1187
|
const report = computeReport(
|
|
1143
|
-
|
|
1188
|
+
runsDir,
|
|
1144
1189
|
retentionCfg?.policy || {},
|
|
1145
1190
|
{ liveKeys }
|
|
1146
1191
|
);
|
|
1147
1192
|
status.components.run_log_retention = {
|
|
1148
1193
|
ok: true,
|
|
1149
|
-
path:
|
|
1194
|
+
path: runsDir,
|
|
1150
1195
|
totalBytes: report.usage.totalBytes,
|
|
1151
1196
|
dirCount: report.usage.dirCount,
|
|
1152
1197
|
runCount: report.usage.runCount,
|
|
@@ -1205,6 +1250,22 @@ async function check(opts = {}) {
|
|
|
1205
1250
|
status.components.build = { ok: true, error: e.message };
|
|
1206
1251
|
}
|
|
1207
1252
|
|
|
1253
|
+
// 6.8. Credential health (informational-but-loud): is the Claude OAuth token
|
|
1254
|
+
// usable? An expired/absent token silently voids every headless `claude -p`
|
|
1255
|
+
// job — the queue "runs" but nothing authenticates. refreshIfNeeded() reads
|
|
1256
|
+
// the Keychain/file and (post futile-refresh fix) never spawns a doomed CLI
|
|
1257
|
+
// fallback for an expired-no-refresh-token creds, so this is a cheap read.
|
|
1258
|
+
// Non-critical: names the cause + `claude login` fix without flipping ok.
|
|
1259
|
+
try {
|
|
1260
|
+
const { refreshIfNeeded } = require('./lib/credentials.cjs');
|
|
1261
|
+
const credState = await refreshIfNeeded();
|
|
1262
|
+
const { component, issues } = evaluateCredentialHealth({ credState, now: Date.now() });
|
|
1263
|
+
status.components.credentials = component;
|
|
1264
|
+
for (const i of issues) status.issues.push(i);
|
|
1265
|
+
} catch (e) {
|
|
1266
|
+
status.components.credentials = { ok: true, state: 'unknown', error: e.message };
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1208
1269
|
// 7. Summary scoring: ok if all critical components pass.
|
|
1209
1270
|
// Critical: nodejs, config dir, typescript, build artifact, test infrastructure.
|
|
1210
1271
|
// Non-fatal: scheduler/transcripts dirs may not exist on fresh install.
|
|
@@ -1231,6 +1292,7 @@ module.exports = {
|
|
|
1231
1292
|
readFreshHeartbeat,
|
|
1232
1293
|
evaluatePrdMigrationHealth,
|
|
1233
1294
|
evaluateDelegationChainHealth,
|
|
1295
|
+
evaluateCredentialHealth,
|
|
1234
1296
|
computeEpicIndexDrift,
|
|
1235
1297
|
evaluateEpicIndexHealth,
|
|
1236
1298
|
computeProjectProblemCounts,
|
package/src/main/index.cjs
CHANGED
|
@@ -1027,7 +1027,7 @@ ipcMain.handle('exchanges:list', validated(schemas.exchangesList, async (payload
|
|
|
1027
1027
|
}
|
|
1028
1028
|
}));
|
|
1029
1029
|
|
|
1030
|
-
// OTEL telemetry export (opt-in via ~/.
|
|
1030
|
+
// OTEL telemetry export (opt-in via ~/.claude/session-manager/otel.json).
|
|
1031
1031
|
ipcMain.handle('otel:get-config', async () => otelSettings.load());
|
|
1032
1032
|
ipcMain.handle('otel:set-config', async (_e, cfg) => {
|
|
1033
1033
|
if (!otelSettings.isValid(cfg)) {
|
package/src/main/ipcSchemas.cjs
CHANGED
|
@@ -632,10 +632,14 @@ const memoryStale = z.object({
|
|
|
632
632
|
// the encoded cwd slug (memoryAggregate.cjs reads directly from
|
|
633
633
|
// ~/.claude/projects/<workspace>/memory/), same regex as the other memory:*
|
|
634
634
|
// handlers. `refresh: true` is the cost gate that fires the single claude -p
|
|
635
|
-
// clustering pass; falsy returns the cached result.
|
|
635
|
+
// clustering pass; falsy returns the cached result. `cwd` (PRD 1389,
|
|
636
|
+
// optional — same shape as memory:stale's) addresses the per-project cache
|
|
637
|
+
// at <cwd>/session-manager-operations/memory-clusters/clusters.json;
|
|
638
|
+
// absent/invalid cwd just disables caching for that call.
|
|
636
639
|
const memoryAggregate = z.object({
|
|
637
640
|
workspace: z.string().regex(MEMORY_WORKSPACE_RE),
|
|
638
641
|
refresh: z.boolean().optional(),
|
|
642
|
+
cwd: z.string().max(4096).optional(),
|
|
639
643
|
}).strict();
|
|
640
644
|
|
|
641
645
|
// ──────────────────────────────────────────── Project Brief (PRD 837)
|
|
@@ -35,7 +35,7 @@ async function mkHome() {
|
|
|
35
35
|
process.env.HOME = dir;
|
|
36
36
|
// Explicit opt-in: overrides the test-environment no-op guard so this
|
|
37
37
|
// suite's real telemetryClient calls actually persist into an isolated dir.
|
|
38
|
-
process.env.SM_TELEMETRY_SPOOL = path.join(dir, '.
|
|
38
|
+
process.env.SM_TELEMETRY_SPOOL = path.join(dir, '.claude', 'session-manager');
|
|
39
39
|
return dir;
|
|
40
40
|
}
|
|
41
41
|
|