claude-code-session-manager 0.92.0 → 0.93.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-DDGx3qrn.js +1 -0
- package/dist/assets/{History-BB6wpf02.js → History-CHBjYZyD.js} +2 -2
- package/dist/assets/{Hooks-zQZH56Sv.js → Hooks-DG4c-Rzz.js} +3 -3
- package/dist/assets/{HostBilko-CW0pHoPS.js → HostBilko-DHXcpV2c.js} +1 -1
- package/dist/assets/{Library-DrGdaz9N.js → Library-CN1h5rAE.js} +1 -1
- package/dist/assets/{MarkdownEditor-H1ytN4Tk.js → MarkdownEditor-DmBxrhdn.js} +1 -1
- package/dist/assets/{McpServers-CYSRCI-j.js → McpServers-Beb_HD6I.js} +2 -2
- package/dist/assets/{Memory-zMNpJyRU.js → Memory-Dk4uDCjg.js} +4 -4
- package/dist/assets/{Permissions-Be6cIkPg.js → Permissions-BovobeZw.js} +3 -3
- package/dist/assets/Plugins-DvOf_B62.js +2 -0
- package/dist/assets/ProvenanceBadge-Dutz_4OY.js +1 -0
- package/dist/assets/{SaveBar-xQZ2EBOn.js → SaveBar-CT5u6ZCJ.js} +1 -1
- package/dist/assets/Scheduler-B7GSnZj5.js +16 -0
- package/dist/assets/{ScopeSwitcher-BAfquN6Y.js → ScopeSwitcher-BUYDpz2v.js} +1 -1
- package/dist/assets/Settings-DIRmr3tL.js +3 -0
- package/dist/assets/{SkillReferenceGraph-BI1YtCbB.js → SkillReferenceGraph-CdGr6d3_.js} +1 -1
- package/dist/assets/Skills-BQOheQv4.js +3 -0
- package/dist/assets/SystemPrompt-5LsimKbX.js +1 -0
- package/dist/assets/TagLibrary-DreLsT3n.js +1 -0
- package/dist/assets/{TiptapBody-BptXAVR9.js → TiptapBody-C9kmlBnI.js} +1 -1
- package/dist/assets/{Toggle-BwiClCSI.js → Toggle-BKLCBu9A.js} +1 -1
- package/dist/assets/{index-D3P_jldk.css → index-CKH5Uxik.css} +1 -1
- package/dist/assets/index-Db_SK9rW.js +3079 -0
- package/dist/assets/settingsSchema-B5jBhSUP.js +1 -0
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/main/__tests__/agentEffortResolve.test.cjs +117 -0
- package/src/main/__tests__/agentLibrary.test.cjs +21 -0
- package/src/main/__tests__/agentModelResolve.test.cjs +16 -0
- package/src/main/__tests__/agentOverlayWrite.test.cjs +95 -0
- package/src/main/__tests__/chatRunner-session-flag-retry.test.cjs +44 -0
- package/src/main/__tests__/personaMerge.test.cjs +169 -0
- package/src/main/__tests__/prdCreatePlanId.test.cjs +132 -0
- package/src/main/__tests__/promptSessionsCreateEpicHandler.test.cjs +13 -0
- package/src/main/__tests__/pty-epic-worktree-spawn-cwd.test.cjs +75 -10
- package/src/main/__tests__/scheduler-prd-persona-spawn.test.cjs +47 -2
- package/src/main/agentLibrary.cjs +86 -40
- package/src/main/build-info.json +4 -4
- package/src/main/chatRunner.cjs +5 -0
- package/src/main/index.cjs +10 -0
- package/src/main/ipcSchemas.cjs +12 -0
- package/src/main/lib/__tests__/effectiveModelInfo.test.cjs +10 -5
- package/src/main/lib/__tests__/modelCatalog.test.cjs +202 -0
- package/src/main/lib/agentEffortResolve.cjs +74 -0
- package/src/main/lib/agentModelResolve.cjs +52 -59
- package/src/main/lib/agentPersonaSchema.cjs +3 -0
- package/src/main/lib/effectiveModelInfo.cjs +38 -20
- package/src/main/lib/epicMint.cjs +5 -2
- package/src/main/lib/epicWorktreeMerge.cjs +7 -6
- package/src/main/lib/modelCatalog.cjs +243 -0
- package/src/main/lib/personaMerge.cjs +166 -0
- package/src/main/lib/prdCreate.cjs +23 -5
- package/src/main/lib/prdDisposition.cjs +46 -0
- package/src/main/lib/prdFrontmatter.cjs +6 -2
- package/src/main/lib/promptSessionSchema.cjs +3 -0
- package/src/main/lib/promptSessionsCreateEpic.cjs +5 -3
- package/src/main/pty.cjs +16 -2
- package/src/main/scheduler/prdParser.cjs +2 -0
- package/src/main/scheduler.cjs +21 -5
- package/src/main/templates/PRD_AUTHORING.md +4 -0
- package/src/preload/api.d.ts +53 -2
- package/src/preload/index.cjs +4 -0
- package/dist/assets/AgentLibrary-Bu-1eT_n.js +0 -3
- package/dist/assets/DataModel-Bk9rkJ8x.js +0 -1
- package/dist/assets/ListDetail-D4bxU_-2.js +0 -1
- package/dist/assets/Panel-RMz3Acpu.js +0 -1
- package/dist/assets/Plugins-B8sTWf2K.js +0 -2
- package/dist/assets/ProvenanceBadge-CO-DZN38.js +0 -1
- package/dist/assets/Scheduler-CF3zsJBw.js +0 -16
- package/dist/assets/Settings-uPjmqutA.js +0 -3
- package/dist/assets/Skills-BEy9FDYd.js +0 -3
- package/dist/assets/SystemPrompt-BYkkw-LL.js +0 -1
- package/dist/assets/TagLibrary-Kg3yOC5b.js +0 -1
- package/dist/assets/index-a2MVk8qh.js +0 -3074
- package/dist/assets/settingsSchema-DgxlStF5.js +0 -3
|
@@ -18,7 +18,7 @@ const fs = require('node:fs');
|
|
|
18
18
|
const path = require('node:path');
|
|
19
19
|
const os = require('node:os');
|
|
20
20
|
const { encodeCwd } = require('../encodeCwd.cjs');
|
|
21
|
-
const { resolveEffectiveModelInfo, isConcreteModelId,
|
|
21
|
+
const { resolveEffectiveModelInfo, isConcreteModelId, computeEffortEnvReachable } = require('../effectiveModelInfo.cjs');
|
|
22
22
|
// config.cjs's `allowedRoots` is a module-level singleton seeded from
|
|
23
23
|
// os.homedir() at FIRST require — since this test's HOME changes every test
|
|
24
24
|
// (fresh mkdtemp), a persona lookup (getPersonaBody -> validatePath) would
|
|
@@ -223,17 +223,22 @@ test('isConcreteModelId treats known CLI aliases as non-concrete and everything
|
|
|
223
223
|
expect(isConcreteModelId('opus')).toBe(false);
|
|
224
224
|
expect(isConcreteModelId('sonnet')).toBe(false);
|
|
225
225
|
expect(isConcreteModelId('haiku')).toBe(false);
|
|
226
|
+
expect(isConcreteModelId('opus[1m]')).toBe(false);
|
|
227
|
+
expect(isConcreteModelId('best')).toBe(false);
|
|
228
|
+
expect(isConcreteModelId('opusplan')).toBe(false);
|
|
229
|
+
expect(isConcreteModelId('default')).toBe(false);
|
|
230
|
+
expect(isConcreteModelId('claude-opus-4-7[1m]')).toBe(true);
|
|
226
231
|
expect(isConcreteModelId('fable')).toBe(false);
|
|
227
232
|
expect(isConcreteModelId(null)).toBe(false);
|
|
228
233
|
expect(isConcreteModelId('claude-opus-5')).toBe(true);
|
|
229
234
|
});
|
|
230
235
|
|
|
231
|
-
test('
|
|
232
|
-
expect(
|
|
236
|
+
test('effortEnvReachable is always false for an app-launched session — cleanChildEnv strips CLAUDE_EFFORT and CLAUDE_CODE_* before a child ever sees them', () => {
|
|
237
|
+
expect(computeEffortEnvReachable({})).toBe(false);
|
|
233
238
|
});
|
|
234
239
|
|
|
235
|
-
test('resolveEffectiveModelInfo surfaces
|
|
240
|
+
test('resolveEffectiveModelInfo surfaces effortEnvReachable alongside the model fields', async () => {
|
|
236
241
|
writeGlobalPersona('any', ['model: sonnet']);
|
|
237
242
|
const info = await resolveEffectiveModelInfo({ cwd, agentType: 'any' });
|
|
238
|
-
expect(info.
|
|
243
|
+
expect(info.effortEnvReachable).toBe(false);
|
|
239
244
|
});
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
3
|
+
const { resolveModelCatalog, CATALOG_FLOOR, parseAliases, parseEffortLevels, scanModelIds } = require('../modelCatalog.cjs');
|
|
4
|
+
|
|
5
|
+
const MODEL_OUT = 'Usage: /model <name>. Available: sonnet, opus, haiku, fable, best, sonnet[1m], opus[1m], fable[1m], opusplan, default, or a full model ID.';
|
|
6
|
+
const EFFORT_OUT = 'Usage: /effort <low|medium|high|xhigh|max|auto>';
|
|
7
|
+
const NOW = Date.parse('2026-09-20T00:00:00Z');
|
|
8
|
+
|
|
9
|
+
function mk(over = {}) {
|
|
10
|
+
const files = over.files || {};
|
|
11
|
+
const fsImpl = {
|
|
12
|
+
readFileSync: (f) => { if (f in files) return files[f]; if (String(f).endsWith('claude-settings-schema.json')) return JSON.stringify({ properties: { effortLevel: { enum: ['low', 'medium', 'high', 'xhigh'] } } }); throw new Error('ENOENT'); },
|
|
13
|
+
realpathSync: (p) => p,
|
|
14
|
+
statSync: () => ({ isFile: () => true, size: 200 * 1024 * 1024 }),
|
|
15
|
+
openSync: () => 1, readSync: (fd, buf) => { buf.write('\x7fELF'); return 4; }, closeSync: () => {},
|
|
16
|
+
...over.fs,
|
|
17
|
+
};
|
|
18
|
+
const runClaudeP = vi.fn(async (prompt) => ({ ok: true, out: prompt === '/model' ? MODEL_OUT : EFFORT_OUT }));
|
|
19
|
+
const writeJson = vi.fn(async () => {});
|
|
20
|
+
const deps = {
|
|
21
|
+
env: {}, now: () => NOW, homeDir: () => '/home/u', projectRootOf: (c) => c,
|
|
22
|
+
probeClaudeVersion: async () => '2.1.277', resolveClaudeBin: () => '/bin/claude',
|
|
23
|
+
scanModelIds: () => ['claude-opus-5', 'claude-sonnet-5'], fs: fsImpl, runClaudeP, writeJson, ...over.deps,
|
|
24
|
+
};
|
|
25
|
+
return { deps, runClaudeP, writeJson };
|
|
26
|
+
}
|
|
27
|
+
const CACHE = '/home/u/.claude/session-manager/model-catalog.json';
|
|
28
|
+
const cacheEntry = (o = {}) => JSON.stringify({ aliases: ['sonnet'], models: ['claude-old-1'], effortLevels: ['low'], settingsEffortLevels: ['low'], claudeVersion: '2.1.277', probedAt: new Date(NOW - 1000).toISOString(), degraded: false, ...o });
|
|
29
|
+
|
|
30
|
+
describe('parsers', () => {
|
|
31
|
+
it('parses aliases, dropping the "or a full model ID" clause and keeping [1m]', () => {
|
|
32
|
+
expect(parseAliases(`Current model: x\n${MODEL_OUT}`)).toEqual(['sonnet', 'opus', 'haiku', 'fable', 'best', 'sonnet[1m]', 'opus[1m]', 'fable[1m]', 'opusplan', 'default']);
|
|
33
|
+
});
|
|
34
|
+
it('parses effort levels', () => {
|
|
35
|
+
expect(parseEffortLevels(EFFORT_OUT)).toEqual(['low', 'medium', 'high', 'xhigh', 'max', 'auto']);
|
|
36
|
+
});
|
|
37
|
+
it('returns null on garbage', () => {
|
|
38
|
+
expect(parseAliases('nope')).toBeNull();
|
|
39
|
+
expect(parseEffortLevels('nope')).toBeNull();
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('scanModelIds', () => {
|
|
44
|
+
const scanBuf = (content, opts) => {
|
|
45
|
+
const data = Buffer.from(content, 'latin1');
|
|
46
|
+
let pos = 0;
|
|
47
|
+
const fsImpl = {
|
|
48
|
+
openSync: () => 1, closeSync: () => {},
|
|
49
|
+
readSync: (fd, buf, off, len) => { const n = Math.min(len, data.length - pos); data.copy(buf, off, pos, pos + n); pos += n; return n; },
|
|
50
|
+
};
|
|
51
|
+
return scanModelIds('/x', { fsImpl, ...opts });
|
|
52
|
+
};
|
|
53
|
+
it('finds an id split across a chunk boundary', () => {
|
|
54
|
+
const content = 'x'.repeat(13) + 'claude-opus-4-8' + '\0' + 'y'.repeat(20) + 'claude-haiku-4-5-20251001\0';
|
|
55
|
+
// chunk 20 cuts the first id after "claude-opus" (offset 13..28)
|
|
56
|
+
expect(scanBuf(content, { chunkSize: 20, overlap: 64 })).toEqual(['claude-haiku-4-5-20251001', 'claude-opus-4-8']);
|
|
57
|
+
});
|
|
58
|
+
it('does not report a truncated id when the tail is cut mid-number', () => {
|
|
59
|
+
expect(scanBuf('aaaaclaude-opus-4-8zz', { chunkSize: 14, overlap: 64 })).toEqual(['claude-opus-4-8']);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('resolveModelCatalog', () => {
|
|
64
|
+
it('happy path: merges probes, binary, schema, allowlist and writes cache', async () => {
|
|
65
|
+
const t = mk({ files: { '/home/u/.claude/settings.json': JSON.stringify({ availableModels: ['opus', 'haiku'] }), '/p/.claude/settings.local.json': JSON.stringify({ availableModels: ['haiku', 'sonnet'] }) } });
|
|
66
|
+
const c = await resolveModelCatalog({ cwd: '/p', force: true, deps: t.deps });
|
|
67
|
+
expect(c.aliases).toContain('opus[1m]');
|
|
68
|
+
expect(c.effortLevels).toEqual(['low', 'medium', 'high', 'xhigh', 'max']); // `auto` is a reset verb, not selectable
|
|
69
|
+
expect(c.models).toEqual(['claude-opus-5', 'claude-sonnet-5']);
|
|
70
|
+
expect(c.settingsEffortLevels).toEqual(['low', 'medium', 'high', 'xhigh']);
|
|
71
|
+
expect(c.availableModels).toEqual(['opus', 'haiku', 'sonnet']);
|
|
72
|
+
expect(c.sources).toEqual({ aliases: 'probe', models: 'binary', effortLevels: 'probe' });
|
|
73
|
+
expect(c.degraded).toBe(false);
|
|
74
|
+
expect(c.claudeVersion).toBe('2.1.277');
|
|
75
|
+
expect(t.runClaudeP).toHaveBeenCalledWith('/model', { model: 'haiku', timeoutMs: 45000 });
|
|
76
|
+
expect(t.runClaudeP).toHaveBeenCalledWith('/effort', { model: 'haiku', timeoutMs: 45000 });
|
|
77
|
+
expect(t.writeJson).toHaveBeenCalledWith(CACHE, c);
|
|
78
|
+
});
|
|
79
|
+
it('availableModels is null when no scope defines it', async () => {
|
|
80
|
+
const c = await resolveModelCatalog({ cwd: '/p', force: true, deps: mk().deps });
|
|
81
|
+
expect(c.availableModels).toBeNull();
|
|
82
|
+
});
|
|
83
|
+
it('fresh matching cache is returned without spawning; force bypasses it', async () => {
|
|
84
|
+
const t = mk({ files: { [CACHE]: cacheEntry() } });
|
|
85
|
+
const c = await resolveModelCatalog({ deps: t.deps });
|
|
86
|
+
expect(c.aliases).toEqual(['sonnet']);
|
|
87
|
+
expect(c.sources.aliases).toBe('cache');
|
|
88
|
+
expect(t.runClaudeP).not.toHaveBeenCalled();
|
|
89
|
+
await resolveModelCatalog({ force: true, deps: t.deps });
|
|
90
|
+
expect(t.runClaudeP).toHaveBeenCalledTimes(2);
|
|
91
|
+
});
|
|
92
|
+
it('probe timeout falls back to stale cache and marks degraded', async () => {
|
|
93
|
+
const t = mk({ files: { [CACHE]: cacheEntry({ probedAt: new Date(NOW - 48 * 3600e3).toISOString() }) }, deps: { runClaudeP: vi.fn(async () => ({ ok: false, error: 'timeout' })) } });
|
|
94
|
+
const c = await resolveModelCatalog({ deps: t.deps });
|
|
95
|
+
expect(c.aliases).toEqual(['sonnet']);
|
|
96
|
+
expect(c.effortLevels).toEqual(['low']);
|
|
97
|
+
expect(c.sources.aliases).toBe('cache');
|
|
98
|
+
expect(c.sources.effortLevels).toBe('cache');
|
|
99
|
+
expect(c.sources.models).toBe('binary');
|
|
100
|
+
expect(c.degraded).toBe(true);
|
|
101
|
+
expect(t.writeJson).not.toHaveBeenCalled();
|
|
102
|
+
});
|
|
103
|
+
it('stale cache from a different CLI version is used before the floor; no cache → floor', async () => {
|
|
104
|
+
const fail = { runClaudeP: vi.fn(async () => ({ ok: false })), scanModelIds: () => { throw new Error('ENOENT'); } };
|
|
105
|
+
const stale = mk({ files: { [CACHE]: cacheEntry({ claudeVersion: '1.0.0' }) }, deps: fail });
|
|
106
|
+
const c1 = await resolveModelCatalog({ deps: stale.deps });
|
|
107
|
+
expect(c1.aliases).toEqual(['sonnet']);
|
|
108
|
+
expect(c1.models).toEqual(['claude-old-1']);
|
|
109
|
+
const none = mk({ deps: fail });
|
|
110
|
+
const c2 = await resolveModelCatalog({ deps: none.deps });
|
|
111
|
+
expect(c2.aliases).toEqual(CATALOG_FLOOR.aliases);
|
|
112
|
+
expect(c2.effortLevels).toEqual(CATALOG_FLOOR.effortLevels);
|
|
113
|
+
expect(c2.models).toEqual([]);
|
|
114
|
+
expect(c2.sources).toEqual({ aliases: 'floor', models: 'floor', effortLevels: 'floor' });
|
|
115
|
+
expect(c2.degraded).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
it('corrupt cache degrades to floor without throwing', async () => {
|
|
118
|
+
const t = mk({ files: { [CACHE]: '{not json' }, deps: { runClaudeP: vi.fn(async () => ({ ok: false })) } });
|
|
119
|
+
const c = await resolveModelCatalog({ deps: t.deps });
|
|
120
|
+
expect(c.aliases).toEqual(CATALOG_FLOOR.aliases);
|
|
121
|
+
});
|
|
122
|
+
it('single-flight collapses concurrent calls into one probe pair', async () => {
|
|
123
|
+
const t = mk();
|
|
124
|
+
const [a, b] = await Promise.all([resolveModelCatalog({ force: true, deps: t.deps }), resolveModelCatalog({ force: true, deps: t.deps })]);
|
|
125
|
+
expect(a).toBe(b);
|
|
126
|
+
expect(t.runClaudeP).toHaveBeenCalledTimes(2);
|
|
127
|
+
});
|
|
128
|
+
it('SM_E2E=1 never spawns: floor, or a cache when present', async () => {
|
|
129
|
+
const t = mk({ deps: { env: { SM_E2E: '1' } } });
|
|
130
|
+
const c = await resolveModelCatalog({ force: true, deps: t.deps });
|
|
131
|
+
expect(c.aliases).toEqual(CATALOG_FLOOR.aliases);
|
|
132
|
+
expect(t.runClaudeP).not.toHaveBeenCalled();
|
|
133
|
+
const t2 = mk({ files: { [CACHE]: cacheEntry() }, deps: { env: { SM_E2E: '1' } } });
|
|
134
|
+
expect((await resolveModelCatalog({ deps: t2.deps })).aliases).toEqual(['sonnet']);
|
|
135
|
+
expect(t2.runClaudeP).not.toHaveBeenCalled();
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('catalog cache file (real write boundary)', () => {
|
|
140
|
+
it('is written under ~/.claude, a path the real validateWrite accepts', async () => {
|
|
141
|
+
const os = require('node:os'); const pth = require('node:path');
|
|
142
|
+
const config = require('../../config.cjs');
|
|
143
|
+
const realHome = os.homedir();
|
|
144
|
+
const file = pth.join(realHome, '.claude', 'session-manager', 'model-catalog.json');
|
|
145
|
+
const t = mk({ deps: { homeDir: () => realHome, writeJson: undefined } });
|
|
146
|
+
const written = [];
|
|
147
|
+
t.deps.writeJson = async (f, d) => { written.push(f); config.validateWrite(f); };
|
|
148
|
+
await resolveModelCatalog({ force: true, deps: t.deps });
|
|
149
|
+
expect(written).toEqual([file]);
|
|
150
|
+
});
|
|
151
|
+
it('a userData path is rejected by validateWrite (why the cache must not live there)', () => {
|
|
152
|
+
const config = require('../../config.cjs');
|
|
153
|
+
expect(() => config.validateWrite('/somewhere/userData/model-catalog.json')).toThrow(/outside allowed write boundaries/);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe('binary scan guard', () => {
|
|
158
|
+
it('never scans a small shell-script stub; degrades to the floor', async () => {
|
|
159
|
+
const scan = vi.fn(() => ['claude-x-1']);
|
|
160
|
+
const t = mk({ fs: { statSync: () => ({ isFile: () => true, size: 300 }) }, deps: { scanModelIds: scan } });
|
|
161
|
+
const c = await resolveModelCatalog({ force: true, deps: t.deps });
|
|
162
|
+
expect(scan).not.toHaveBeenCalled();
|
|
163
|
+
expect(c.sources.models).toBe('floor');
|
|
164
|
+
expect(c.degraded).toBe(true);
|
|
165
|
+
});
|
|
166
|
+
it('never scans a large file that is a shell script', async () => {
|
|
167
|
+
const scan = vi.fn(() => ['claude-x-1']);
|
|
168
|
+
const t = mk({ fs: { readSync: (fd, buf) => { buf.write('#!/bin/bash\n'); return 12; } }, deps: { scanModelIds: scan } });
|
|
169
|
+
await resolveModelCatalog({ force: true, deps: t.deps });
|
|
170
|
+
expect(scan).not.toHaveBeenCalled();
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe('null CLI version key', () => {
|
|
175
|
+
it('neither serves a cache as fresh nor writes one', async () => {
|
|
176
|
+
const t = mk({ files: { [CACHE]: cacheEntry({ claudeVersion: null }) }, deps: { probeClaudeVersion: async () => null } });
|
|
177
|
+
const c = await resolveModelCatalog({ deps: t.deps });
|
|
178
|
+
expect(t.runClaudeP).toHaveBeenCalled();
|
|
179
|
+
expect(c.claudeVersion).toBeNull();
|
|
180
|
+
expect(t.writeJson).not.toHaveBeenCalled();
|
|
181
|
+
});
|
|
182
|
+
it('a null-version cache entry is never a fresh hit for a real version', async () => {
|
|
183
|
+
const t = mk({ files: { [CACHE]: cacheEntry({ claudeVersion: null }) } });
|
|
184
|
+
await resolveModelCatalog({ deps: t.deps });
|
|
185
|
+
expect(t.runClaudeP).toHaveBeenCalled();
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe('effort auto is never a flag value', () => {
|
|
190
|
+
it('drops `auto` from live and cached effort levels', async () => {
|
|
191
|
+
const t = mk({ files: { [CACHE]: cacheEntry({ effortLevels: ['low', 'auto'], probedAt: new Date(NOW - 48 * 3600e3).toISOString() }) }, deps: { runClaudeP: vi.fn(async () => ({ ok: false })) } });
|
|
192
|
+
const c = await resolveModelCatalog({ deps: t.deps });
|
|
193
|
+
expect(c.effortLevels).toEqual(['low']);
|
|
194
|
+
});
|
|
195
|
+
it('effortArgs and resolveEpicEffort never emit --effort auto', () => {
|
|
196
|
+
const { effortArgs } = require('../agentEffortResolve.cjs');
|
|
197
|
+
expect(effortArgs('auto')).toEqual([]);
|
|
198
|
+
expect(effortArgs('AUTO')).toEqual([]);
|
|
199
|
+
expect(effortArgs('inherit')).toEqual([]);
|
|
200
|
+
expect(effortArgs('high')).toEqual(['--effort', 'high']);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* agentEffortResolve.cjs — the effort twin of agentModelResolve.cjs: resolves
|
|
5
|
+
* the `--effort <level>` an Epic-/PRD-backed `claude` launch should carry,
|
|
6
|
+
* from the agentType persona's own `effort:` frontmatter field (written by
|
|
7
|
+
* agentLibrary.cjs's serializePersona; the key is literally `effort`).
|
|
8
|
+
*
|
|
9
|
+
* PRECEDENCE (explicit):
|
|
10
|
+
* 0. Epic-level `effort` (the New Session card's override, stored on the Epic
|
|
11
|
+
* record; source 'epic') beats everything below. Only reachable on the
|
|
12
|
+
* Chat/Terminal path (claudeSessionId) — a scheduled PRD has no Epic record
|
|
13
|
+
* of its own to carry it. `inherit`/`auto` there means "no override".
|
|
14
|
+
* 1. persona `effort:` — project overlay (`<cwd>/.claude/agents/<name>.md`)
|
|
15
|
+
* beats the global `~/.claude/agents/<name>.md` (same path resolver and
|
|
16
|
+
* same reader as the model path: agentModelResolve.cjs's
|
|
17
|
+
* readOverlayAwarePersona, i.e. personaMerge.cjs's field-level merge — a
|
|
18
|
+
* frontmatter-only overlay can pin just `effort`; source is
|
|
19
|
+
* 'persona-overlay' only when the overlay itself supplied the value).
|
|
20
|
+
* 2. `inherit`, absent, dangling agentType, unreadable persona → `effort:
|
|
21
|
+
* null`, meaning "pass NO --effort flag at all" so the CLI applies its
|
|
22
|
+
* own settings.json / default resolution.
|
|
23
|
+
*
|
|
24
|
+
* This module NEVER reads settings.json and NEVER invents a fallback level.
|
|
25
|
+
* A caller must append the flag only when `effort` is a non-null string —
|
|
26
|
+
* never `--effort inherit`, `--effort null`, or an empty value.
|
|
27
|
+
*
|
|
28
|
+
* The persona value is passed through UNCHANGED (only trimmed): the CLI
|
|
29
|
+
* accepts low/medium/high/xhigh/max and, for an unknown level, merely warns
|
|
30
|
+
* (`Unknown --effort value 'x' — ignoring it and using the default effort.`)
|
|
31
|
+
* and proceeds at the default — so a wrong value degrades gracefully instead
|
|
32
|
+
* of failing the run, and rewriting it here would hide the mistake.
|
|
33
|
+
*
|
|
34
|
+
* Plain sync Node module, never throws (same contract as resolveEpicModel).
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
const { findEpicByClaudeSessionId, epicOverrideValue, readOverlayAwarePersona } = require('./agentModelResolve.cjs');
|
|
38
|
+
|
|
39
|
+
// `auto` is a `/effort` RESET verb, not a `--effort` value; it must never reach argv.
|
|
40
|
+
const NON_FLAG_EFFORT = new Set(['inherit', 'auto']);
|
|
41
|
+
|
|
42
|
+
const NO_EFFORT = Object.freeze({ effort: null, source: null });
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {{ cwd: string, claudeSessionId?: string, agentType?: string|null, deps?: object }} opts
|
|
46
|
+
* `agentType` wins when given (scheduler PRD path); otherwise it is joined
|
|
47
|
+
* from the Epic's `claudeSessionId` (Chat/Terminal path).
|
|
48
|
+
* @returns {{ effort: string|null, source: 'epic'|'persona'|'persona-overlay'|'inherit'|null }}
|
|
49
|
+
* source null = no agentType/persona resolved; 'inherit' = persona present
|
|
50
|
+
* but sets no level.
|
|
51
|
+
*/
|
|
52
|
+
function resolveEpicEffort({ cwd, claudeSessionId, agentType, deps = {} } = {}) {
|
|
53
|
+
try {
|
|
54
|
+
const epic = claudeSessionId ? findEpicByClaudeSessionId(cwd, claudeSessionId, deps) : null;
|
|
55
|
+
const override = epicOverrideValue(epic, 'effort');
|
|
56
|
+
if (override && !NON_FLAG_EFFORT.has(override.toLowerCase())) return { effort: override, source: 'epic' };
|
|
57
|
+
const type = agentType || epic?.agentType || null;
|
|
58
|
+
if (!type) return { ...NO_EFFORT };
|
|
59
|
+
const persona = readOverlayAwarePersona(type, { ...deps, cwd });
|
|
60
|
+
if (!persona) return { ...NO_EFFORT };
|
|
61
|
+
const raw = typeof persona.fm.effort === 'string' ? persona.fm.effort.trim() : '';
|
|
62
|
+
if (!raw || NON_FLAG_EFFORT.has(raw.toLowerCase())) return { effort: null, source: 'inherit' };
|
|
63
|
+
return { effort: raw, source: persona.provenance.effort === 'overlay' ? 'persona-overlay' : 'persona' };
|
|
64
|
+
} catch {
|
|
65
|
+
return { ...NO_EFFORT };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** argv fragment for a resolved effort: `['--effort', level]` or `[]`. One place decides the "no flag" rule. */
|
|
70
|
+
function effortArgs(effort) {
|
|
71
|
+
return typeof effort === 'string' && effort && !NON_FLAG_EFFORT.has(effort.toLowerCase()) ? ['--effort', effort] : [];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = { resolveEpicEffort, effortArgs };
|
|
@@ -17,17 +17,21 @@
|
|
|
17
17
|
* views literally call the same function instead of two functions that are
|
|
18
18
|
* merely supposed to agree.
|
|
19
19
|
*
|
|
20
|
+
* PRECEDENCE (explicit): Epic-level `model` (the New Session card's override,
|
|
21
|
+
* stored on the Epic record) > persona frontmatter `model` > `fallbackModel`.
|
|
22
|
+
* An Epic with no `model` key (every Epic minted before the override existed)
|
|
23
|
+
* resolves exactly as it always did.
|
|
24
|
+
*
|
|
20
25
|
* Plain Node module (sync fs) — no Electron deps, mirrors epicMint.cjs's
|
|
21
26
|
* readActiveIndex so this can be called from chatRunner.cjs's synchronous
|
|
22
27
|
* executeRun() without restructuring it into an async flow.
|
|
23
28
|
*/
|
|
24
29
|
|
|
25
|
-
const fs = require('node:fs');
|
|
26
30
|
const path = require('node:path');
|
|
27
31
|
const os = require('node:os');
|
|
28
32
|
const { splitFrontmatter } = require('./prdFrontmatter.cjs');
|
|
29
33
|
const { readActiveIndex, resolvePersonaPaths } = require('./epicMint.cjs');
|
|
30
|
-
const { PERSONA_NAME_RE } = require('
|
|
34
|
+
const { resolveMergedPersona, PERSONA_NAME_RE } = require('./personaMerge.cjs');
|
|
31
35
|
const configMgr = require('../config.cjs');
|
|
32
36
|
|
|
33
37
|
/** The hardcoded floor every branch of resolveEpicModel falls back to — --model must never be omitted (CLAUDE.md "Automation model pinning"). */
|
|
@@ -56,7 +60,7 @@ function logDanglingPersonaOnce(cwd, agentType) {
|
|
|
56
60
|
cwd,
|
|
57
61
|
scope: 'agentModelResolve',
|
|
58
62
|
level: 'warn',
|
|
59
|
-
message: `agentType '${agentType}' has no resolvable persona file — model resolution fell back to '${FALLBACK_MODEL}'`,
|
|
63
|
+
message: `agentType '${agentType}' has no resolvable persona file — model resolution fell back to '${FALLBACK_MODEL}' (and no persona effort applies)`,
|
|
60
64
|
meta: { agentType },
|
|
61
65
|
});
|
|
62
66
|
} catch { /* logging must never break model resolution */ }
|
|
@@ -68,74 +72,55 @@ function logDanglingPersonaOnce(cwd, agentType) {
|
|
|
68
72
|
* resolveSourcePromptIdFromClaudeSession uses (Epic:claude-session is 1:1).
|
|
69
73
|
* Returns null if no matching Epic is found or it has no agentType set.
|
|
70
74
|
*/
|
|
71
|
-
function
|
|
75
|
+
function findEpicByClaudeSessionId(cwd, claudeSessionId, deps = {}) {
|
|
72
76
|
if (!cwd || !claudeSessionId) return null;
|
|
73
77
|
const load = deps.readActiveIndex || readActiveIndex;
|
|
74
78
|
const { sessions } = load(cwd);
|
|
75
79
|
for (const session of Object.values(sessions)) {
|
|
76
|
-
if (session && session.claudeSessionId === claudeSessionId) return session
|
|
80
|
+
if (session && session.claudeSessionId === claudeSessionId) return session;
|
|
77
81
|
}
|
|
78
82
|
return null;
|
|
79
83
|
}
|
|
80
84
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
* Never throws.
|
|
90
|
-
*/
|
|
91
|
-
function readModelFromCandidatePaths(candidates, agentType, deps) {
|
|
92
|
-
const validatePath = deps.validatePath || configMgr.validatePath;
|
|
93
|
-
for (const candidate of candidates) {
|
|
94
|
-
let real;
|
|
95
|
-
try {
|
|
96
|
-
real = validatePath(candidate);
|
|
97
|
-
} catch {
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
try {
|
|
101
|
-
const text = fs.readFileSync(real, 'utf8');
|
|
102
|
-
const { fm } = splitFrontmatter(text);
|
|
103
|
-
return fm.model || null;
|
|
104
|
-
} catch {
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
logDanglingPersonaOnce(deps.cwd, agentType);
|
|
109
|
-
return null;
|
|
85
|
+
function findAgentTypeByClaudeSessionId(cwd, claudeSessionId, deps = {}) {
|
|
86
|
+
return findEpicByClaudeSessionId(cwd, claudeSessionId, deps)?.agentType || null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** A non-empty trimmed string from an Epic's optional `model`/`effort` override field, else null. */
|
|
90
|
+
function epicOverrideValue(epic, field) {
|
|
91
|
+
const raw = epic && typeof epic[field] === 'string' ? epic[field].trim() : '';
|
|
92
|
+
return raw || null;
|
|
110
93
|
}
|
|
111
94
|
|
|
112
95
|
/**
|
|
113
|
-
* Reads an Epic's agentType persona's
|
|
114
|
-
*
|
|
115
|
-
* (
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
96
|
+
* Reads an Epic's agentType persona through personaMerge.cjs's
|
|
97
|
+
* `resolveMergedPersona` — the ONE overlay-over-global merge every reader
|
|
98
|
+
* shares (overlay frontmatter keys win, absent keys and an empty overlay body
|
|
99
|
+
* fall back to the global; see that module's header for the `inherit` and
|
|
100
|
+
* malformed-overlay rules). Returns `{ fm, body, path, fromOverlay,
|
|
101
|
+
* provenance, ... }`, or null on a total miss / invalid name — never throws,
|
|
102
|
+
* and logs the dangling-persona warning at most once per (cwd, agentType).
|
|
119
103
|
*
|
|
120
|
-
* Sync (not `getPersonaBody`'s async
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
* — never throws.
|
|
104
|
+
* Sync (not `getPersonaBody`'s async form) because `resolveEpicModel` must
|
|
105
|
+
* stay synchronous: `chatRunner.cjs`'s `executeRun()` registers its cancel
|
|
106
|
+
* handle into `inFlight` synchronously, and its caller (`pump()`) reads that
|
|
107
|
+
* entry back immediately after invoking the executor — an `await` inserted
|
|
108
|
+
* ahead of that registration would run it a tick late and silently break
|
|
109
|
+
* `cancel()`. The PERSONA_NAME_RE traversal guard lives in the merge module.
|
|
127
110
|
*/
|
|
111
|
+
function readOverlayAwarePersona(agentType, deps = {}) {
|
|
112
|
+
const persona = resolveMergedPersona(deps.cwd, agentType, deps);
|
|
113
|
+
if (!persona) {
|
|
114
|
+
if (agentType && PERSONA_NAME_RE.test(agentType)) logDanglingPersonaOnce(deps.cwd, agentType);
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
return persona;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The persona's `model` frontmatter field (overlay-then-global), or null. Never throws. */
|
|
128
121
|
function readOverlayAwarePersonaModel(agentType, deps = {}) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
// "../../other-project/CLAUDE" survives resolvePersonaPaths' plain
|
|
132
|
-
// path.join and validatePath's allowed-roots check (which only confirms
|
|
133
|
-
// the resolved path stays under the home dir / an opened project), letting
|
|
134
|
-
// any readable .md under those roots be read on this --model spawn path.
|
|
135
|
-
if (!agentType || !PERSONA_NAME_RE.test(agentType)) return null;
|
|
136
|
-
const resolvePaths = deps.resolvePersonaPaths || resolvePersonaPaths;
|
|
137
|
-
const { projectPath, globalPath } = resolvePaths(deps.cwd, agentType, deps);
|
|
138
|
-
return readModelFromCandidatePaths([projectPath, globalPath], agentType, deps);
|
|
122
|
+
const persona = readOverlayAwarePersona(agentType, deps);
|
|
123
|
+
return (persona && persona.fm.model) || null;
|
|
139
124
|
}
|
|
140
125
|
|
|
141
126
|
/**
|
|
@@ -179,7 +164,10 @@ function isProjectOverlayPersonaPath(cwd, agentType, personaPath, deps = {}) {
|
|
|
179
164
|
*/
|
|
180
165
|
function resolveEpicModel({ cwd, claudeSessionId, fallbackModel = FALLBACK_MODEL, deps = {} } = {}) {
|
|
181
166
|
try {
|
|
182
|
-
const
|
|
167
|
+
const epic = findEpicByClaudeSessionId(cwd, claudeSessionId, deps);
|
|
168
|
+
const override = epicOverrideValue(epic, 'model');
|
|
169
|
+
if (override && override !== 'inherit') return override;
|
|
170
|
+
const agentType = epic?.agentType || null;
|
|
183
171
|
if (!agentType) return fallbackModel;
|
|
184
172
|
const model = readOverlayAwarePersonaModel(agentType, { ...deps, cwd });
|
|
185
173
|
if (!model || model === 'inherit') return fallbackModel;
|
|
@@ -237,7 +225,9 @@ async function resolvePrdPersonaForSpawn({ cwd, agentType, fallbackModel = FALLB
|
|
|
237
225
|
reportOnce(cwd, agentType, deps);
|
|
238
226
|
return miss;
|
|
239
227
|
}
|
|
240
|
-
|
|
228
|
+
// getPersonaBody returns the MERGED persona (fm + body); a bare `{ path, text }`
|
|
229
|
+
// (older injected reader) is parsed here.
|
|
230
|
+
const { fm, body } = persona.fm ? persona : splitFrontmatter(persona.text);
|
|
241
231
|
const model = fm.model && fm.model !== 'inherit' ? fm.model : fallbackModel;
|
|
242
232
|
const systemPrompt = buildPersonaBodyText(body, persona.path);
|
|
243
233
|
return { model, systemPrompt, personaPath: persona.path };
|
|
@@ -250,7 +240,10 @@ module.exports = {
|
|
|
250
240
|
FALLBACK_MODEL,
|
|
251
241
|
resolveEpicModel,
|
|
252
242
|
findAgentTypeByClaudeSessionId,
|
|
243
|
+
findEpicByClaudeSessionId,
|
|
244
|
+
epicOverrideValue,
|
|
253
245
|
readOverlayAwarePersonaModel,
|
|
246
|
+
readOverlayAwarePersona,
|
|
254
247
|
isProjectOverlayPersonaPath,
|
|
255
248
|
resolvePrdPersonaForSpawn,
|
|
256
249
|
};
|
|
@@ -50,12 +50,15 @@ const AgentPersonaSaveSchema = z.object({
|
|
|
50
50
|
description: BoundedString,
|
|
51
51
|
tools: z.array(BoundedString),
|
|
52
52
|
model: BoundedString,
|
|
53
|
+
effort: BoundedString.optional(),
|
|
53
54
|
color: BoundedString,
|
|
54
55
|
tags: z.array(WorkTypeSchema),
|
|
55
56
|
projects: z.array(ProjectEntrySchema).optional(),
|
|
56
57
|
action: z.string().max(20000).optional(),
|
|
57
58
|
actionLabel: BoundedString.optional(),
|
|
58
59
|
title: BoundedString.optional(),
|
|
60
|
+
// Set = write this project's frontmatter-only override (model/effort only) instead of the global file.
|
|
61
|
+
projectName: BoundedString.optional(),
|
|
59
62
|
// Unbounded — a persona's body is arbitrary system-prompt prose.
|
|
60
63
|
body: z.string(),
|
|
61
64
|
});
|
|
@@ -13,11 +13,11 @@
|
|
|
13
13
|
* "we don't know; the CLI resolves this alias at launch." The two must
|
|
14
14
|
* never collapse into one optimistic string.
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
16
|
+
* PERSONA effort IS here now (agentEffortResolve.cjs): it needs the same
|
|
17
|
+
* main-process, overlay-aware persona read as the persona model, and a launch
|
|
18
|
+
* now actually passes it as `--effort`. The settings.json effortLevel
|
|
19
|
+
* scope-chain read stays in the renderer (`useEffectiveSettings.ts`) as the
|
|
20
|
+
* FALLBACK when the persona sets no level — this module never reads settings.
|
|
21
21
|
*
|
|
22
22
|
* Never throws — every lookup failure degrades a field to null, same
|
|
23
23
|
* contract as agentModelResolve.cjs's resolveEpicModel.
|
|
@@ -29,12 +29,17 @@ const os = require('node:os');
|
|
|
29
29
|
const { splitFrontmatter } = require('./prdFrontmatter.cjs');
|
|
30
30
|
const { resolveEpicTranscriptPath } = require('./epicTranscriptPath.cjs');
|
|
31
31
|
|
|
32
|
-
//
|
|
32
|
+
// Deliberately stays a FIXED alias set (reviewed against the catalog-driven raw picker): it answers
|
|
33
|
+
// "alias vs concrete id", not "which options to offer", so live catalog ids must not enter it.
|
|
34
|
+
// Mirrors rawSessionModel.ts's RAW_MODELS fallback — duplicated rather than imported
|
|
33
35
|
// because that file is a renderer ES module and this is a main-process CJS
|
|
34
36
|
// module (CLAUDE.md: "No CommonJS in renderer, no ES modules in main").
|
|
35
37
|
// Anything NOT in this set is treated as an already-concrete pinned model id
|
|
36
38
|
// (e.g. 'claude-opus-5'), never as an alias needing evidence.
|
|
37
|
-
|
|
39
|
+
// Also the non-family CLI aliases the Agent Library picker can now write
|
|
40
|
+
// (best / opusplan / default); a trailing `[1m]`-style suffix is stripped
|
|
41
|
+
// before the lookup (see isConcreteModelId), so 'opus[1m]' is an alias too.
|
|
42
|
+
const KNOWN_MODEL_ALIASES = new Set(['opus', 'sonnet', 'haiku', 'fable', 'best', 'opusplan', 'default']);
|
|
38
43
|
|
|
39
44
|
// Run-log evidence (scheduler.cjs's executeJob, ~line 4473) is written
|
|
40
45
|
// essentially at dispatch time, before any of the run's own stdout streams
|
|
@@ -54,10 +59,14 @@ function runsDir(homeDir) {
|
|
|
54
59
|
}
|
|
55
60
|
|
|
56
61
|
function isConcreteModelId(modelAlias) {
|
|
57
|
-
|
|
62
|
+
if (!modelAlias) return false;
|
|
63
|
+
// 'opus[1m]' is an alias with a context suffix; 'claude-opus-4-7[1m]' keeps its id.
|
|
64
|
+
return !KNOWN_MODEL_ALIASES.has(modelAlias.replace(/\[[^\]]*\]$/, ''));
|
|
58
65
|
}
|
|
59
66
|
|
|
60
67
|
/**
|
|
68
|
+
* ENV-VAR question only (`effortEnvReachable`): persona effort now travels on argv as `--effort`,
|
|
69
|
+
* which cleanChildEnv cannot strip — so this says nothing about whether effort reaches the CLI.
|
|
61
70
|
* True when this app's spawn path (cleanEnv.cjs's cleanChildEnv) would strip
|
|
62
71
|
* an effort env var before a child ever saw it — i.e. whether the settings
|
|
63
72
|
* schema's env-var effort configuration path is actually reachable for an
|
|
@@ -65,7 +74,7 @@ function isConcreteModelId(modelAlias) {
|
|
|
65
74
|
* hardcoding a boolean, so this self-corrects if that deletion list ever
|
|
66
75
|
* changes instead of silently drifting stale.
|
|
67
76
|
*/
|
|
68
|
-
function
|
|
77
|
+
function computeEffortEnvReachable(deps) {
|
|
69
78
|
try {
|
|
70
79
|
const { cleanChildEnv } = deps.cleanEnv || require('./cleanEnv.cjs');
|
|
71
80
|
const probe = cleanChildEnv({ CLAUDE_EFFORT: 'high', CLAUDE_CODE_EFFORT_LEVEL: 'high' });
|
|
@@ -106,15 +115,18 @@ async function resolvePersonaModelAlias(cwd, agentType, deps) {
|
|
|
106
115
|
} catch {
|
|
107
116
|
persona = null;
|
|
108
117
|
}
|
|
109
|
-
if (!persona) return { modelAlias: null, modelSource: 'fallback' };
|
|
118
|
+
if (!persona) return { modelAlias: null, modelSource: 'fallback', provenance: {} };
|
|
110
119
|
|
|
111
|
-
|
|
112
|
-
|
|
120
|
+
// getPersonaBody returns the MERGED persona with per-field provenance; a bare
|
|
121
|
+
// `{ path, text }` (older injected reader) falls back to whole-file provenance.
|
|
122
|
+
const fm = persona.fm || splitFrontmatter(persona.text).fm;
|
|
123
|
+
const provenance = persona.provenance
|
|
124
|
+
|| Object.fromEntries(Object.keys(fm).map((k) => [k, isProjectOverlayPersonaPath(cwd, agentType, persona.path, deps) ? 'overlay' : 'global']));
|
|
113
125
|
|
|
114
126
|
if (fm.model && fm.model !== 'inherit') {
|
|
115
|
-
return { modelAlias: fm.model, modelSource:
|
|
127
|
+
return { modelAlias: fm.model, modelSource: provenance.model === 'overlay' ? 'persona-overlay' : 'persona', provenance };
|
|
116
128
|
}
|
|
117
|
-
return { modelAlias: null, modelSource: 'inherit' };
|
|
129
|
+
return { modelAlias: null, modelSource: 'inherit', provenance };
|
|
118
130
|
}
|
|
119
131
|
|
|
120
132
|
/** Best-effort read of `<cwd>/session-manager-operations/scheduler/state/queue.json`'s hot jobs[]. */
|
|
@@ -268,21 +280,25 @@ function findLatestTranscriptModel(cwd, agentType, deps) {
|
|
|
268
280
|
|
|
269
281
|
/**
|
|
270
282
|
* resolveEffectiveModelInfo({ cwd, agentType, deps? }) →
|
|
271
|
-
* { agentType, modelAlias, modelSource, resolvedModelId, resolvedFrom,
|
|
283
|
+
* { agentType, modelAlias, modelSource, resolvedModelId, resolvedFrom, effortEnvReachable,
|
|
284
|
+
* personaEffort, personaEffortSource }
|
|
272
285
|
*
|
|
273
286
|
* Never throws. `cwd` is normalized through the same worktree/ops-internal
|
|
274
287
|
* -aware resolver every ops-root reader goes through, so a job worktree cwd
|
|
275
288
|
* resolves to its main project tree before anything is looked up.
|
|
276
289
|
*/
|
|
277
290
|
async function resolveEffectiveModelInfo({ cwd, agentType, deps = {} } = {}) {
|
|
278
|
-
const
|
|
291
|
+
const effortEnvReachable = computeEffortEnvReachable(deps);
|
|
279
292
|
const miss = {
|
|
280
293
|
agentType: agentType || null,
|
|
281
294
|
modelAlias: null,
|
|
282
295
|
modelSource: 'fallback',
|
|
296
|
+
personaProvenance: {},
|
|
283
297
|
resolvedModelId: null,
|
|
284
298
|
resolvedFrom: null,
|
|
285
|
-
|
|
299
|
+
effortEnvReachable,
|
|
300
|
+
personaEffort: null,
|
|
301
|
+
personaEffortSource: null,
|
|
286
302
|
};
|
|
287
303
|
if (!cwd || !agentType) return miss;
|
|
288
304
|
|
|
@@ -291,8 +307,10 @@ async function resolveEffectiveModelInfo({ cwd, agentType, deps = {} } = {}) {
|
|
|
291
307
|
const normalizedCwd = projectRootOf(path.resolve(cwd));
|
|
292
308
|
if (!normalizedCwd) return miss;
|
|
293
309
|
|
|
294
|
-
const { modelAlias, modelSource } = await resolvePersonaModelAlias(normalizedCwd, agentType, deps);
|
|
295
|
-
const
|
|
310
|
+
const { modelAlias, modelSource, provenance: personaProvenance } = await resolvePersonaModelAlias(normalizedCwd, agentType, deps);
|
|
311
|
+
const { effort: personaEffort, source: personaEffortSource } = (deps.agentEffortResolve || require('./agentEffortResolve.cjs'))
|
|
312
|
+
.resolveEpicEffort({ cwd: normalizedCwd, agentType, deps });
|
|
313
|
+
const result = { agentType, modelAlias, modelSource, personaProvenance, resolvedModelId: null, resolvedFrom: null, effortEnvReachable, personaEffort, personaEffortSource };
|
|
296
314
|
|
|
297
315
|
if (isConcreteModelId(modelAlias)) {
|
|
298
316
|
// Already a concrete pinned id — echo it back, fabricate no evidence.
|
|
@@ -327,6 +345,6 @@ async function resolveEffectiveModelInfo({ cwd, agentType, deps = {} } = {}) {
|
|
|
327
345
|
module.exports = {
|
|
328
346
|
resolveEffectiveModelInfo,
|
|
329
347
|
isConcreteModelId,
|
|
330
|
-
|
|
348
|
+
computeEffortEnvReachable,
|
|
331
349
|
KNOWN_MODEL_ALIASES,
|
|
332
350
|
};
|