claude-code-session-manager 0.120.0 → 0.122.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-LBg3mVL8.js → AgentLibrary-BmJ8hIPw.js} +1 -1
- package/dist/assets/{Button-C4BN5ISR.js → Button-BS4kmz4p.js} +1 -1
- package/dist/assets/{DataModel-CHhjk61s.js → DataModel-DZHBZrfL.js} +1 -1
- package/dist/assets/History-pg00b2Gd.js +2 -0
- package/dist/assets/{Hooks-ByWVgXIf.js → Hooks-BTEs0UZY.js} +1 -1
- package/dist/assets/{Library-QvYVAEmY.js → Library-DuaJ-VSn.js} +1 -1
- package/dist/assets/{ListDetail-FvNe6fo5.js → ListDetail-Cud72s-O.js} +1 -1
- package/dist/assets/{McpServers-C7EmPm8B.js → McpServers-Dy-wnG_X.js} +1 -1
- package/dist/assets/{Memory-nyLR4Yw4.js → Memory-Bs1T1Jo7.js} +1 -1
- package/dist/assets/{Panel-thElCbLX.js → Panel-8UhLXEOW.js} +1 -1
- package/dist/assets/{Permissions-FQ9GM6Ca.js → Permissions-BfwzvmBS.js} +1 -1
- package/dist/assets/Plugins-McjyvvKh.js +2 -0
- package/dist/assets/{ProvenanceBadge-lAViG8yV.js → ProvenanceBadge-D0Ov_6WJ.js} +1 -1
- package/dist/assets/{RemoteMachines-AWpARk7N.js → RemoteMachines-gOlFTvWq.js} +1 -1
- package/dist/assets/{SaveBar-c15EEGB_.js → SaveBar-Cd6hJR6K.js} +1 -1
- package/dist/assets/Scheduler-BZXKqYJP.js +16 -0
- package/dist/assets/{ScopeSwitcher-BLep1rtB.js → ScopeSwitcher-C24ysV1K.js} +1 -1
- package/dist/assets/{Settings-D1kTUIDB.js → Settings-DWnN2qxn.js} +1 -1
- package/dist/assets/{SkillReferenceGraph-_40_f0t3.js → SkillReferenceGraph-BsWuiTbw.js} +1 -1
- package/dist/assets/{Skills-DaTkSOuU.js → Skills-6pHn6glu.js} +1 -1
- package/dist/assets/{SystemPrompt-DxNf-soM.js → SystemPrompt-DT0LBH-K.js} +1 -1
- package/dist/assets/{TagLibrary-L6KpBV-x.js → TagLibrary-DbJ0zbYL.js} +1 -1
- package/dist/assets/{TiptapBody-C_XjtJ82.js → TiptapBody-C4kIvIMH.js} +1 -1
- package/dist/assets/{Toggle-H0rERKNI.js → Toggle-pP19zlyk.js} +1 -1
- package/dist/assets/{ViewTabs-ZUWphDat.js → ViewTabs-CpzN8e5I.js} +1 -1
- package/dist/assets/{historyTotals-BxFKOztb.js → historyTotals-Caq78bb5.js} +1 -1
- package/dist/assets/{index-B5wj0DEX.js → index-Bjtguv6I.js} +434 -434
- package/dist/assets/{settingsSchema-BRcYkXJN.js → settingsSchema-CC2ySu3d.js} +1 -1
- package/dist/assets/{skillFrontmatter-DFGp4DST.js → skillFrontmatter-hDhNo8jE.js} +1 -1
- package/dist/assets/{useAgentsDirWatch-VvrgalfC.js → useAgentsDirWatch-BuOtY25U.js} +1 -1
- package/dist/assets/{useRemotePeers-mvXKNEE3.js → useRemotePeers-CNQmRBpt.js} +1 -1
- package/dist/index.html +1 -1
- package/package.json +1 -1
- package/plugins/session-manager-dev/skills/develop/SKILL.md +7 -7
- package/plugins/session-manager-dev/skills/develop/standards.md +4 -10
- package/scripts/scheduler-mcp-server.cjs +2 -2
- package/src/main/build-info.json +4 -4
- package/src/main/chatRunner.cjs +56 -43
- package/src/main/docEdit.cjs +1 -0
- package/src/main/index.cjs +12 -13
- package/src/main/ipcSchemas.cjs +17 -3
- package/src/main/lib/classifyPromptTicket.cjs +1 -0
- package/src/main/lib/claude/effectiveModelInfo.cjs +9 -21
- package/src/main/lib/claude/personaToolScope.cjs +7 -1
- package/src/main/lib/claude/runClaudeP.cjs +1 -0
- package/src/main/lib/epic/delegationReadiness.cjs +75 -8
- package/src/main/lib/mcpToolCatalog.cjs +0 -1
- package/src/main/lib/persona-macro/macroLibrary.cjs +75 -0
- package/src/main/lib/persona-macro/shippedPersonaSeeds.cjs +11 -0
- package/src/main/lib/prd/prdCreate.cjs +28 -1
- package/src/main/lib/prd/prdSizing.cjs +1 -8
- package/src/main/lib/remoteTranscriptTail.cjs +69 -2
- package/src/main/lib/runSeeders.cjs +21 -0
- package/src/main/lib/scheduler/loopEfficiency.cjs +109 -0
- package/src/main/lib/scheduler/queueHistory.cjs +44 -1
- package/src/main/lib/scheduler/queueStore.cjs +4 -72
- package/src/main/lib/scheduler/rcaReport.cjs +1 -1
- package/src/main/lib/scheduler/refireCap.cjs +40 -0
- package/src/main/lib/scheduler/reviewNotice.cjs +1 -1
- package/src/main/lib/scheduler/schedulerConfig.cjs +1 -2
- package/src/main/lib/scheduler/schedulerPaths.cjs +0 -2
- package/src/main/lib/scheduler/sessionSlots.cjs +1 -1
- package/src/main/lib/telemetry/auditLog.cjs +65 -14
- package/src/main/loopEfficiencyHandlers.cjs +160 -0
- package/src/main/queueOps.cjs +4 -9
- package/src/main/runVerify.cjs +22 -454
- package/src/main/scheduler/reviewLadder.cjs +12 -2
- package/src/main/scheduler.cjs +103 -177
- package/src/main/seedDevPlugin.cjs +49 -22
- package/src/main/seedStatus.cjs +3 -2
- package/src/main/supervisor.cjs +1 -0
- package/src/main/templates/PRD_AUTHORING.md +16 -31
- package/src/main/webRemote.cjs +82 -91
- package/src/preload/api.d.ts +39 -6
- package/src/preload/index.cjs +3 -0
- package/src/seed/agents/dev-lead.md +2 -9
- package/dist/assets/History-CL-T6Lv3.js +0 -2
- package/dist/assets/Plugins-D9KE7XTI.js +0 -2
- package/dist/assets/Scheduler-FvduP1FG.js +0 -16
|
@@ -137,28 +137,16 @@ function readHotJobsForCwd(cwd, deps) {
|
|
|
137
137
|
/** Best-effort read of `<cwd>/session-manager-operations/scheduler/state/history.jsonl`'s archived rows. */
|
|
138
138
|
function readHistoryEntriesForCwd(cwd, deps) {
|
|
139
139
|
const { projectHistoryPath } = deps.queueStore || require('../scheduler/queueStore.cjs');
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
return [];
|
|
145
|
-
}
|
|
140
|
+
const { readHistoryFileTailSync } = deps.queueHistory || require('../scheduler/queueHistory.cjs');
|
|
141
|
+
// Bounded tail (<=500 rows / <=1 MB): only the most recent matching row matters.
|
|
142
|
+
const rows = readHistoryFileTailSync(projectHistoryPath(cwd), { maxRows: 500, maxBytes: 1024 * 1024 });
|
|
143
|
+
// Tail reader is newest-first; callers sort by timestamp anyway.
|
|
146
144
|
const entries = [];
|
|
147
|
-
for (const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
// share history.jsonl with terminal job rows but carry no `agentType` —
|
|
153
|
-
// findLatestSchedulerModel's own `j.agentType === agentType` filter
|
|
154
|
-
// already excludes them, but skip explicitly here too so this reader
|
|
155
|
-
// stays correct even if a future ledger line shape happens to pick up
|
|
156
|
-
// an agentType-shaped field.
|
|
157
|
-
if (j?.kind && j.kind !== 'terminal') continue;
|
|
158
|
-
entries.push(j);
|
|
159
|
-
} catch {
|
|
160
|
-
// corrupt/partial line — skip
|
|
161
|
-
}
|
|
145
|
+
for (const j of rows) {
|
|
146
|
+
// needs_review_entry/needs_review_resolution lines (needsReviewLedger.cjs)
|
|
147
|
+
// share history.jsonl with terminal job rows; skip non-terminal kinds.
|
|
148
|
+
if (j?.kind && j.kind !== 'terminal') continue;
|
|
149
|
+
entries.push(j);
|
|
162
150
|
}
|
|
163
151
|
return entries;
|
|
164
152
|
}
|
|
@@ -54,10 +54,16 @@ function personaToolScopeArgs(scope) {
|
|
|
54
54
|
prompt: 'Tool scope is enforced by the scheduler.',
|
|
55
55
|
tools: scope.tools,
|
|
56
56
|
};
|
|
57
|
-
|
|
57
|
+
const args = [
|
|
58
58
|
'--agents', JSON.stringify({ [HEADLESS_SCOPE_AGENT_NAME]: def }),
|
|
59
59
|
'--agent', HEADLESS_SCOPE_AGENT_NAME,
|
|
60
60
|
];
|
|
61
|
+
// A scope naming no MCP tool never uses a server: with --strict-mcp-config and
|
|
62
|
+
// no --mcp-config (variadic; would swallow later args) the CLI boots none.
|
|
63
|
+
if (!scope.tools.some((t) => String(t).startsWith('mcp__'))) {
|
|
64
|
+
args.push('--strict-mcp-config');
|
|
65
|
+
}
|
|
66
|
+
return args;
|
|
61
67
|
}
|
|
62
68
|
|
|
63
69
|
module.exports = {
|
|
@@ -32,6 +32,7 @@ async function runClaudeP(prompt, { model = 'sonnet', timeoutMs = 180_000, syste
|
|
|
32
32
|
'--model', model,
|
|
33
33
|
'--dangerously-skip-permissions',
|
|
34
34
|
...headlessPermissionArgs(),
|
|
35
|
+
'--strict-mcp-config',
|
|
35
36
|
'--output-format', 'text',
|
|
36
37
|
];
|
|
37
38
|
if (systemPrompt) args.push('--append-system-prompt', systemPrompt);
|
|
@@ -303,18 +303,84 @@ async function checkSchedulerMcpLive({ cwd, homeDir, schedulerMcpCheck }) {
|
|
|
303
303
|
};
|
|
304
304
|
}
|
|
305
305
|
|
|
306
|
+
const DEV_PLUGIN_MARKETPLACE = 'session-manager';
|
|
307
|
+
const DEV_PLUGIN_REINSTALL_FIX = 'Restart Session Manager to reinstall the session-manager-dev plugin';
|
|
308
|
+
|
|
309
|
+
/** installPath of the dev plugin from installed_plugins.json, or null. */
|
|
310
|
+
function readDevPluginInstallPath(homeDir) {
|
|
311
|
+
const data = readJsonOrSync(path.join(homeDir, '.claude', 'plugins', 'installed_plugins.json'), null);
|
|
312
|
+
const entries = data?.plugins?.[DEV_PLUGIN_ENABLED_KEY];
|
|
313
|
+
if (!Array.isArray(entries)) return null;
|
|
314
|
+
for (const e of entries) {
|
|
315
|
+
if (e && typeof e.installPath === 'string' && e.installPath) return e.installPath;
|
|
316
|
+
}
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Source path of the 'session-manager' marketplace from known_marketplaces.json, or null. */
|
|
321
|
+
function readDevMarketplacePath(homeDir) {
|
|
322
|
+
const data = readJsonOrSync(path.join(homeDir, '.claude', 'plugins', 'known_marketplaces.json'), null);
|
|
323
|
+
const m = data?.[DEV_PLUGIN_MARKETPLACE];
|
|
324
|
+
const p = m?.source?.path ?? m?.installLocation;
|
|
325
|
+
return typeof p === 'string' && p ? p : null;
|
|
326
|
+
}
|
|
327
|
+
|
|
306
328
|
function checkDevPlugin({ homeDir }) {
|
|
307
329
|
const settings = readJsonOrSync(path.join(homeDir, '.claude', 'settings.json'), null);
|
|
308
|
-
const
|
|
330
|
+
const enabled = settings?.enabledPlugins?.[DEV_PLUGIN_ENABLED_KEY] === true;
|
|
331
|
+
const base = { id: 'dev-plugin', label: 'session-manager-dev plugin enabled' };
|
|
332
|
+
|
|
333
|
+
if (!enabled) {
|
|
334
|
+
return {
|
|
335
|
+
...base,
|
|
336
|
+
ok: false,
|
|
337
|
+
detail: `${DEV_PLUGIN_ENABLED_KEY} missing from ~/.claude/settings.json enabledPlugins`,
|
|
338
|
+
fix: 'Enable in Plugins tab: session-manager-dev (Session Manager)',
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const installPath = readDevPluginInstallPath(homeDir);
|
|
343
|
+
if (!installPath) {
|
|
344
|
+
return {
|
|
345
|
+
...base,
|
|
346
|
+
ok: false,
|
|
347
|
+
detail: `${DEV_PLUGIN_ENABLED_KEY} enabled but not recorded in ~/.claude/plugins/installed_plugins.json`,
|
|
348
|
+
fix: DEV_PLUGIN_REINSTALL_FIX,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
const skillFile = path.join(installPath, 'skills', 'develop', 'SKILL.md');
|
|
352
|
+
if (!fs.existsSync(skillFile)) {
|
|
353
|
+
return {
|
|
354
|
+
...base,
|
|
355
|
+
ok: false,
|
|
356
|
+
detail: `${DEV_PLUGIN_ENABLED_KEY} enabled but ${skillFile} is missing`,
|
|
357
|
+
fix: DEV_PLUGIN_REINSTALL_FIX,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const marketplacePath = readDevMarketplacePath(homeDir);
|
|
362
|
+
if (!marketplacePath) {
|
|
363
|
+
return {
|
|
364
|
+
...base,
|
|
365
|
+
ok: false,
|
|
366
|
+
detail: `'${DEV_PLUGIN_MARKETPLACE}' marketplace not registered in ~/.claude/plugins/known_marketplaces.json`,
|
|
367
|
+
fix: DEV_PLUGIN_REINSTALL_FIX,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
if (!fs.existsSync(marketplacePath)) {
|
|
371
|
+
return {
|
|
372
|
+
...base,
|
|
373
|
+
ok: false,
|
|
374
|
+
detail: `'${DEV_PLUGIN_MARKETPLACE}' marketplace folder ${marketplacePath} no longer exists`,
|
|
375
|
+
fix: DEV_PLUGIN_REINSTALL_FIX,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
309
378
|
|
|
310
379
|
return {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
? `${DEV_PLUGIN_ENABLED_KEY} enabled in ~/.claude/settings.json`
|
|
316
|
-
: `${DEV_PLUGIN_ENABLED_KEY} missing from ~/.claude/settings.json enabledPlugins`,
|
|
317
|
-
fix: ok ? null : 'Enable in Plugins tab: session-manager-dev (Session Manager)',
|
|
380
|
+
...base,
|
|
381
|
+
ok: true,
|
|
382
|
+
detail: `${DEV_PLUGIN_ENABLED_KEY} enabled in ~/.claude/settings.json and installed on disk`,
|
|
383
|
+
fix: null,
|
|
318
384
|
};
|
|
319
385
|
}
|
|
320
386
|
|
|
@@ -795,6 +861,7 @@ async function ensureGuardsInstalled(cwd, { homeDir = os.homedir() } = {}) {
|
|
|
795
861
|
}
|
|
796
862
|
|
|
797
863
|
module.exports = {
|
|
864
|
+
checkDevPlugin,
|
|
798
865
|
checkDelegationReadiness,
|
|
799
866
|
ensureGuardsInstalled,
|
|
800
867
|
logAutoInstallEvent,
|
|
@@ -127,7 +127,6 @@ const MCP_TOOL_CATALOG = [
|
|
|
127
127
|
exampleArgs: {
|
|
128
128
|
title: 'Add unit tests for the retry backoff helper',
|
|
129
129
|
cwd: '/home/bilko/Projects/session-manager',
|
|
130
|
-
estimateMinutes: 10,
|
|
131
130
|
goal: 'Cover retryWithBackoff.cjs edge cases (zero retries, max-delay clamp) that currently have no test.',
|
|
132
131
|
acceptanceCriteria: ['New test file exercises zero-retry and max-delay-clamp cases', 'Typecheck passes', 'Unit tests pass'],
|
|
133
132
|
implementationNotes: 'See src/main/lib/retryWithBackoff.cjs and its existing __tests__ sibling for the pattern to extend.',
|
|
@@ -93,6 +93,61 @@ function refineInputs(value, ctx) {
|
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
/** Distinct `{{name}}` placeholder names in first-seen order. */
|
|
97
|
+
function macroPlaceholders(prompt) {
|
|
98
|
+
const names = [];
|
|
99
|
+
for (const m of String(prompt).matchAll(new RegExp(MACRO_PLACEHOLDER_RE.source, 'g'))) {
|
|
100
|
+
if (!names.includes(m[1])) names.push(m[1]);
|
|
101
|
+
}
|
|
102
|
+
return names;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** `{ [name]: default ?? '' }` for every declared input. */
|
|
106
|
+
function defaultMacroValues(inputs) {
|
|
107
|
+
const out = {};
|
|
108
|
+
for (const i of inputs || []) out[i.name] = i.default ?? '';
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function sanitizeMacroValue(v) {
|
|
113
|
+
// eslint-disable-next-line no-control-regex
|
|
114
|
+
return v.replace(/[\r\n\t]+/g, ' ').replace(/[\u0000-\u001f\u007f]/g, '').trim();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const MACRO_PROMPT_RENDERED_MAX_LEN = 8000;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Mirrors `src/renderer/lib/macroPrompt.ts` renderMacroPrompt, but strict for macros
|
|
121
|
+
* without inputs: a placeholder with no declared input is always an error.
|
|
122
|
+
* @returns {{ ok: true, prompt: string } | { ok: false, error: string }}
|
|
123
|
+
*/
|
|
124
|
+
function renderMacroPrompt(prompt, inputs, values) {
|
|
125
|
+
const vals = values && typeof values === 'object' ? values : {};
|
|
126
|
+
const clean = Object.create(null);
|
|
127
|
+
for (const input of inputs || []) {
|
|
128
|
+
const own = Object.prototype.hasOwnProperty.call(vals, input.name) && typeof vals[input.name] === 'string';
|
|
129
|
+
const v = sanitizeMacroValue(own ? vals[input.name] : (input.default ?? ''));
|
|
130
|
+
if (v === '') {
|
|
131
|
+
if (input.required) return { ok: false, error: `"${input.label}" is required` };
|
|
132
|
+
} else if (v.length > MACRO_INPUT_MAX_LEN) {
|
|
133
|
+
return { ok: false, error: `"${input.label}" is longer than ${MACRO_INPUT_MAX_LEN} characters` };
|
|
134
|
+
} else if (input.options && input.options.length > 0 && !input.options.includes(v)) {
|
|
135
|
+
return { ok: false, error: `"${input.label}" must be one of: ${input.options.join(', ')}` };
|
|
136
|
+
}
|
|
137
|
+
clean[input.name] = v;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
for (const name of macroPlaceholders(prompt)) {
|
|
141
|
+
if (!(name in clean)) return { ok: false, error: `prompt uses undeclared input {{${name}}}` };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const rendered = String(prompt).replace(new RegExp(MACRO_PLACEHOLDER_RE.source, 'g'), (_m, n) => clean[n]);
|
|
145
|
+
if (rendered.length > MACRO_PROMPT_RENDERED_MAX_LEN) {
|
|
146
|
+
return { ok: false, error: `rendered prompt is longer than ${MACRO_PROMPT_RENDERED_MAX_LEN} characters` };
|
|
147
|
+
}
|
|
148
|
+
return { ok: true, prompt: rendered };
|
|
149
|
+
}
|
|
150
|
+
|
|
96
151
|
const MacroSchema = z.object({
|
|
97
152
|
id: MacroIdSchema,
|
|
98
153
|
label: MacroLabelSchema,
|
|
@@ -151,6 +206,12 @@ function normalizeCwd(cwd) {
|
|
|
151
206
|
return collapsed.replace(/\/+$/, '');
|
|
152
207
|
}
|
|
153
208
|
|
|
209
|
+
/** True iff the macro is shown for `cwd`: its projects include '*' or the normalized cwd. */
|
|
210
|
+
function macroVisibleIn(macro, cwd) {
|
|
211
|
+
const projects = (macro && macro.projects) || [];
|
|
212
|
+
return projects.includes(ALL_PROJECTS) || projects.includes(normalizeCwd(cwd));
|
|
213
|
+
}
|
|
214
|
+
|
|
154
215
|
function normalizeProjects(projects) {
|
|
155
216
|
const out = [];
|
|
156
217
|
for (const p of projects || []) {
|
|
@@ -414,6 +475,14 @@ async function loadForWrite(filePath, opts) {
|
|
|
414
475
|
* Create (no id) or update (id) a macro. Returns the saved Macro.
|
|
415
476
|
* Throws on invalid input or unknown id.
|
|
416
477
|
*/
|
|
478
|
+
/** Merged-macro check: every `{{name}}` in the prompt must be a declared input. */
|
|
479
|
+
function assertPlaceholdersDeclared(macro) {
|
|
480
|
+
const declared = new Set((macro.inputs || []).map((i) => i.name));
|
|
481
|
+
for (const name of macroPlaceholders(macro.prompt)) {
|
|
482
|
+
if (!declared.has(name)) throw new Error(`prompt placeholder {{${name}}} is not declared in inputs`);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
417
486
|
async function saveMacro(input, opts = {}) {
|
|
418
487
|
const parsed = MacroSaveSchema.parse(input);
|
|
419
488
|
const filePath = resolvePath(opts);
|
|
@@ -442,6 +511,7 @@ async function saveMacro(input, opts = {}) {
|
|
|
442
511
|
} else {
|
|
443
512
|
saved.inputs = parsed.inputs;
|
|
444
513
|
}
|
|
514
|
+
assertPlaceholdersDeclared(saved);
|
|
445
515
|
macros[idx] = saved;
|
|
446
516
|
} else {
|
|
447
517
|
const projects = parsed.projects !== undefined ? normalizeProjects(parsed.projects) : [];
|
|
@@ -457,6 +527,7 @@ async function saveMacro(input, opts = {}) {
|
|
|
457
527
|
updatedAt: now,
|
|
458
528
|
};
|
|
459
529
|
if (parsed.inputs && parsed.inputs.length > 0) saved.inputs = parsed.inputs;
|
|
530
|
+
assertPlaceholdersDeclared(saved);
|
|
460
531
|
macros.push(saved);
|
|
461
532
|
}
|
|
462
533
|
await writeStore(filePath, macros);
|
|
@@ -518,6 +589,10 @@ module.exports = {
|
|
|
518
589
|
MacroSaveSchema,
|
|
519
590
|
MacroDeleteSchema,
|
|
520
591
|
MacroSetProjectSchema,
|
|
592
|
+
macroPlaceholders,
|
|
593
|
+
defaultMacroValues,
|
|
594
|
+
renderMacroPrompt,
|
|
595
|
+
macroVisibleIn,
|
|
521
596
|
listMacros,
|
|
522
597
|
saveMacro,
|
|
523
598
|
deleteMacro,
|
|
@@ -124,6 +124,17 @@ const SHIPPED_PERSONA_SEEDS = deepFreeze({
|
|
|
124
124
|
tools: "Read, Grep, Glob, Bash, Edit, Write",
|
|
125
125
|
},
|
|
126
126
|
},
|
|
127
|
+
{
|
|
128
|
+
commit: "2129verify",
|
|
129
|
+
bodySha256: "82b93c1da9a5e3b7dfcfadd1b4689d988a4bd19de2d897ebd3f3158969d741fc",
|
|
130
|
+
fm: {
|
|
131
|
+
description: "Executes exactly one already-scoped PRD at a time, headless, start to finish — reads the PRD's Goal/Acceptance Criteria/Implementation notes and standards.md, implements it, verifies against its own AC, and reports. Has no visibility into the overall plan — that's architect's job. This is the default persona a scheduled PRD runs as: the scheduler resolves a PRD's `agentType` frontmatter field (default `dev-lead`) to this file and launches the headless executor AS this persona via `--append-system-prompt`.",
|
|
132
|
+
model: "sonnet",
|
|
133
|
+
effort: "medium",
|
|
134
|
+
title: "Engineering — Software Engineer",
|
|
135
|
+
tools: "Read, Grep, Glob, Bash, Edit, Write",
|
|
136
|
+
},
|
|
137
|
+
},
|
|
127
138
|
],
|
|
128
139
|
"project-home-builder": [
|
|
129
140
|
{
|
|
@@ -29,6 +29,7 @@ const { resolveProjectContext } = require('../projectRootResolve.cjs');
|
|
|
29
29
|
const { fixChainDepthOf, baseSlugOf } = require('../scheduler/fixChainDepth.cjs');
|
|
30
30
|
const { DEFAULT_PRD_AGENT_TYPE, assertAgentTypeWritable } = require('./prdAgentType.cjs');
|
|
31
31
|
const { resolveDepSlug, findNearMatches } = require('./depSlugResolve.cjs');
|
|
32
|
+
const { resolveEpicPrdWriteDir } = require('./prdLocations.cjs');
|
|
32
33
|
const { isFixPlanSlug } = require('../scheduler/fixPlanSlug.cjs');
|
|
33
34
|
const { isIncomplete, resolveChainTerminals, isValidPlanId, mintPlanId, resolveInheritedPlanId } = require('./prdDisposition.cjs');
|
|
34
35
|
const { sizingWarnings } = require('./prdSizing.cjs');
|
|
@@ -87,7 +88,9 @@ function buildPrdBody(input) {
|
|
|
87
88
|
// No `parallelGroup` frontmatter key (PRD_AUTHORING.md §5) — the NN-
|
|
88
89
|
// filename prefix is the single source of truth for grouping; adding a
|
|
89
90
|
// second one here would let the two drift out of sync.
|
|
90
|
-
const fmLines = ['---', `title: ${title}`, `cwd: ${cwd}
|
|
91
|
+
const fmLines = ['---', `title: ${title}`, `cwd: ${cwd}`];
|
|
92
|
+
// Optional: only a finite positive integer is written; absent => budget's 45-min floor.
|
|
93
|
+
if (Number.isInteger(estimateMinutes) && estimateMinutes > 0) fmLines.push(`estimateMinutes: ${estimateMinutes}`);
|
|
91
94
|
// Provenance stamp (PRD-authoring lockdown): every PRD written through this
|
|
92
95
|
// function — the sole in-process implementation behind both the admin
|
|
93
96
|
// HTTP create-prd route and the renderer's chat:create-prd IPC handler —
|
|
@@ -443,6 +446,30 @@ async function createPrd(input, remote) {
|
|
|
443
446
|
}
|
|
444
447
|
extraWarnings.push(...await worktreeReachAdvisory(input, cwd));
|
|
445
448
|
|
|
449
|
+
// Duplicate-slug guard (2110/2111 phone-drop-summary-state: duplicate re-issue after archive).
|
|
450
|
+
// Finished rows leave queue.json, so an author can re-issue a landed PRD under a fresh NN.
|
|
451
|
+
// Refused before NN allocation so it burns no number. Fix plans are exempt.
|
|
452
|
+
if (input.sourcePromptId && !isFixPlanSlug(`0-${slug}`)) {
|
|
453
|
+
let epicDirs = [];
|
|
454
|
+
try {
|
|
455
|
+
const prdsDir = resolveEpicPrdWriteDir(cwd, input.sourcePromptId);
|
|
456
|
+
epicDirs = [prdsDir, path.join(path.dirname(prdsDir), 'prds-archived')];
|
|
457
|
+
} catch { /* unsafe epic id — no dirs to scan */ }
|
|
458
|
+
for (const dir of epicDirs) {
|
|
459
|
+
let names = [];
|
|
460
|
+
try { names = await fsp.readdir(dir); } catch { /* missing dir counts as empty */ }
|
|
461
|
+
const dup = names.find((n) => n.endsWith('.md') && n.slice(0, -3).replace(/^\d+-/, '') === slug);
|
|
462
|
+
if (dup) {
|
|
463
|
+
return {
|
|
464
|
+
ok: false,
|
|
465
|
+
status: 409,
|
|
466
|
+
error: `A PRD with slug "${slug}" already exists in this Epic as "${dup}". Its work may already have landed. ` +
|
|
467
|
+
'Use scheduler_update_prd / scheduler_reset_job on it, or pick a distinct slug.',
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
446
473
|
const nn = await remote.allocateParallelGroup(input.cwd);
|
|
447
474
|
const filenameSlug = `${nn}-${slug}`;
|
|
448
475
|
|
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
'use strict';
|
|
12
12
|
|
|
13
13
|
const SIZING_LIMITS = {
|
|
14
|
-
estimateMinutes: 15,
|
|
15
14
|
acLines: 8,
|
|
16
15
|
acLineChars: 400,
|
|
17
16
|
bodyChars: 7000,
|
|
@@ -30,18 +29,12 @@ const TIMEOUT_CMD_RE = /\btimeout\s+\d+/gi;
|
|
|
30
29
|
* treated as empty/absent rather than thrown on, since this is advisory only.
|
|
31
30
|
*/
|
|
32
31
|
function sizingWarnings(input) {
|
|
33
|
-
const {
|
|
32
|
+
const { goal, implementationNotes } = input || {};
|
|
34
33
|
const acceptanceCriteria = Array.isArray(input?.acceptanceCriteria) ? input.acceptanceCriteria : [];
|
|
35
34
|
const acLines = acceptanceCriteria.filter((line) => typeof line === 'string');
|
|
36
35
|
|
|
37
36
|
const warnings = [];
|
|
38
37
|
|
|
39
|
-
if (typeof estimateMinutes === 'number' && estimateMinutes > SIZING_LIMITS.estimateMinutes) {
|
|
40
|
-
warnings.push(
|
|
41
|
-
`estimateMinutes (${estimateMinutes}) exceeds the ${SIZING_LIMITS.estimateMinutes}-minute sizing limit — split this PRD into smaller work items.`,
|
|
42
|
-
);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
38
|
if (acceptanceCriteria.length > SIZING_LIMITS.acLines) {
|
|
46
39
|
warnings.push(
|
|
47
40
|
`acceptanceCriteria has ${acceptanceCriteria.length} lines, exceeding the ${SIZING_LIMITS.acLines}-line sizing limit — split into smaller, single-purpose PRDs.`,
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// Bounded-tail reader for Claude Code JSONL transcripts: the last N human-readable
|
|
4
4
|
// chat messages (user prompts + assistant text) without reading the whole file.
|
|
5
|
+
// Also builds the phone's conversation page (exchanges + live tail) in pure code.
|
|
5
6
|
// Top-level (no domain fits: epic/ transcripts are path-only; util/ is domain-free).
|
|
6
7
|
// Transcripts reach hundreds of MB, so cost is O(maxBytes), never O(file).
|
|
7
8
|
|
|
@@ -11,6 +12,10 @@ const DEFAULT_LIMIT = 30;
|
|
|
11
12
|
const MAX_LIMIT = 100;
|
|
12
13
|
const DEFAULT_MAX_BYTES = 524288;
|
|
13
14
|
const MAX_TEXT_CHARS = 4000;
|
|
15
|
+
const MAX_PROMPT_CHARS = 2000;
|
|
16
|
+
const MAX_RESULT_CHARS = 16000;
|
|
17
|
+
const MAX_LIVE_MESSAGES = 20;
|
|
18
|
+
const MAX_PAGE_TEXT_BYTES = 100000; // relay closes any message over 256 KB
|
|
14
19
|
|
|
15
20
|
/** Join the plain-text parts of a message `content` (string or block array). */
|
|
16
21
|
function textOf(content) {
|
|
@@ -22,8 +27,12 @@ function textOf(content) {
|
|
|
22
27
|
.join('\n');
|
|
23
28
|
}
|
|
24
29
|
|
|
30
|
+
function cap(text, max) {
|
|
31
|
+
return text.length > max ? text.slice(0, max - 1) + '…' : text;
|
|
32
|
+
}
|
|
33
|
+
|
|
25
34
|
function truncate(text) {
|
|
26
|
-
return text
|
|
35
|
+
return cap(text, MAX_TEXT_CHARS);
|
|
27
36
|
}
|
|
28
37
|
|
|
29
38
|
/** Parse one JSONL line into a chat message, or null when it is not one. */
|
|
@@ -87,4 +96,62 @@ async function readRecentChatMessages(filePath, { limit = DEFAULT_LIMIT, maxByte
|
|
|
87
96
|
}
|
|
88
97
|
}
|
|
89
98
|
|
|
90
|
-
|
|
99
|
+
/**
|
|
100
|
+
* Pure: build one phone-ready conversation page from exchanges (NEWEST-FIRST,
|
|
101
|
+
* callers fetch `limit + 1` so `hasMore` is derivable) plus the live transcript tail.
|
|
102
|
+
* @param {{ exchanges?: any[], tail?: any[], limit?: number, before?: number }} [opts]
|
|
103
|
+
* @returns {{ messages: Array<{ role: string, text: string, ts: string|null, uuid: string|null, live?: true }>, hasMore: boolean, nextBefore: number|null }}
|
|
104
|
+
* messages oldest-first: history, then live.
|
|
105
|
+
*/
|
|
106
|
+
function buildRemoteConversation({ exchanges = [], tail = [], limit = 10, before = 0 } = {}) {
|
|
107
|
+
const n = Math.floor(Number(limit));
|
|
108
|
+
const lim = Number.isFinite(n) && n >= 1 ? n : 10;
|
|
109
|
+
const b = Math.max(0, Math.floor(Number(before)) || 0);
|
|
110
|
+
const valid = (Array.isArray(exchanges) ? exchanges : []).filter((e) => e && typeof e === 'object' && typeof e.ts === 'string');
|
|
111
|
+
const candidates = valid.slice(0, lim);
|
|
112
|
+
let hasMore = valid.length > lim;
|
|
113
|
+
let budget = MAX_PAGE_TEXT_BYTES;
|
|
114
|
+
const size = (text) => Buffer.byteLength(text, 'utf8');
|
|
115
|
+
|
|
116
|
+
// Live tail (first page only): newest-first until the budget is hit.
|
|
117
|
+
const live = [];
|
|
118
|
+
if (b === 0 && Array.isArray(tail)) {
|
|
119
|
+
const newest = valid.reduce((m, e) => Math.max(m, Date.parse(e.ts) || 0), 0);
|
|
120
|
+
const fresh = tail.filter((m) => {
|
|
121
|
+
if (!m || typeof m !== 'object' || typeof m.text !== 'string') return false;
|
|
122
|
+
if (!valid.length) return true;
|
|
123
|
+
const t = typeof m.ts === 'string' ? Date.parse(m.ts) : NaN;
|
|
124
|
+
return Number.isFinite(t) && t > newest;
|
|
125
|
+
});
|
|
126
|
+
for (const m of fresh.slice(-MAX_LIVE_MESSAGES).reverse()) {
|
|
127
|
+
const bytes = size(m.text);
|
|
128
|
+
if (bytes > budget) break;
|
|
129
|
+
budget -= bytes;
|
|
130
|
+
live.push({ role: m.role, text: m.text, ts: m.ts ?? null, uuid: m.uuid ?? null, live: true });
|
|
131
|
+
}
|
|
132
|
+
live.reverse();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// History: whole exchanges newest-first, oldest dropped when over budget.
|
|
136
|
+
const history = [];
|
|
137
|
+
let returned = 0;
|
|
138
|
+
for (const e of candidates) {
|
|
139
|
+
const pair = [];
|
|
140
|
+
const prompt = typeof e.prompt === 'string' ? e.prompt.trim() : '';
|
|
141
|
+
const result = typeof e.result === 'string' ? e.result.trim() : '';
|
|
142
|
+
if (prompt) pair.push({ role: 'user', text: cap(prompt, MAX_PROMPT_CHARS), ts: e.ts, uuid: `ex:${e.ts}:u` });
|
|
143
|
+
if (result) pair.push({ role: 'assistant', text: cap(result, MAX_RESULT_CHARS), ts: e.ts, uuid: `ex:${e.ts}:a` });
|
|
144
|
+
const bytes = pair.reduce((s, m) => s + size(m.text), 0);
|
|
145
|
+
if (bytes > budget) {
|
|
146
|
+
hasMore = true;
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
budget -= bytes;
|
|
150
|
+
history.unshift(...pair);
|
|
151
|
+
returned++;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return { messages: [...history, ...live], hasMore, nextBefore: hasMore ? b + returned : null };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
module.exports = { readRecentChatMessages, buildRemoteConversation };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Runs first-boot seeders one at a time. `claude plugin install` and
|
|
2
|
+
// `claude mcp add --scope user` both rewrite Claude's config files; run
|
|
3
|
+
// concurrently, one write can drop the other's entry after its marker says done.
|
|
4
|
+
// A failing step is logged and never skips the steps after it.
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {Array<{ name: string, run: () => Promise<unknown> }>} steps
|
|
8
|
+
* @returns {Promise<void>}
|
|
9
|
+
*/
|
|
10
|
+
async function runSeedersSerially(steps) {
|
|
11
|
+
for (const step of steps) {
|
|
12
|
+
try {
|
|
13
|
+
await step.run();
|
|
14
|
+
} catch (e) {
|
|
15
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
16
|
+
console.error(`[seed] ${step.name} failed: ${message}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = { runSeedersSerially };
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* loopEfficiency.cjs — pure calculator for the scheduler's real throughput
|
|
3
|
+
* metric: validated PRDs per unit of usage, per project.
|
|
4
|
+
*
|
|
5
|
+
* Why: the scheduler is bound by the account's usage window, not by slots, so
|
|
6
|
+
* "verified PRDs per usage %" (and $ per verified PRD) is what matters. Also
|
|
7
|
+
* makes the "wasted runs <= 5%" target in scheduler-operations.md computable.
|
|
8
|
+
*
|
|
9
|
+
* Pure: no I/O, no Date.now(), no mutation of inputs.
|
|
10
|
+
* Complexity O(R + V + P log P): one pass over runs, one over validations,
|
|
11
|
+
* then a sort of the P project rows.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const { COMPLETED_EQUIVALENT_VERDICTS } = require('./terminalRunOutcome.cjs');
|
|
18
|
+
|
|
19
|
+
/** @param {unknown} t @param {number} fromMs @param {number} toMs */
|
|
20
|
+
function inWindow(t, fromMs, toMs) {
|
|
21
|
+
return typeof t === 'number' && Number.isFinite(t) && t >= fromMs && t < toMs;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** @param {unknown} slug */
|
|
25
|
+
function normSlug(slug) {
|
|
26
|
+
return String(slug == null ? '' : slug).replace(/^\d+-/, '');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {{
|
|
31
|
+
* runs?: Array<{ cwd: string, slug: string, startedAtMs: number, costUsd?: number, verdict: string | null }>,
|
|
32
|
+
* validations?: Array<{ cwd: string, slug: string, verdict: 'VERIFIED' | 'REFUTED', atMs: number }>,
|
|
33
|
+
* fromMs: number,
|
|
34
|
+
* toMs: number,
|
|
35
|
+
* weeklyUtilizationPct?: number | null,
|
|
36
|
+
* }} input
|
|
37
|
+
*/
|
|
38
|
+
function computeLoopEfficiency({ runs, validations, fromMs, toMs, weeklyUtilizationPct }) {
|
|
39
|
+
/** @type {Map<string, { cwd: string, runs: number, wastedRuns: number, executorUsd: number, last: Map<string, { atMs: number, verdict: string }> }>} */
|
|
40
|
+
const byCwd = new Map();
|
|
41
|
+
const bucket = (/** @type {string} */ cwd) => {
|
|
42
|
+
let b = byCwd.get(cwd);
|
|
43
|
+
if (!b) {
|
|
44
|
+
b = { cwd, runs: 0, wastedRuns: 0, executorUsd: 0, last: new Map() };
|
|
45
|
+
byCwd.set(cwd, b);
|
|
46
|
+
}
|
|
47
|
+
return b;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
for (const r of runs || []) {
|
|
51
|
+
if (!r || !inWindow(r.startedAtMs, fromMs, toMs)) continue;
|
|
52
|
+
const b = bucket(String(r.cwd));
|
|
53
|
+
b.runs += 1;
|
|
54
|
+
const v = r.verdict;
|
|
55
|
+
if (v != null && v !== 'clean' && !COMPLETED_EQUIVALENT_VERDICTS.has(v)) b.wastedRuns += 1;
|
|
56
|
+
const cost = Number(r.costUsd);
|
|
57
|
+
if (Number.isFinite(cost)) b.executorUsd += cost;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
for (const v of validations || []) {
|
|
61
|
+
if (!v || !inWindow(v.atMs, fromMs, toMs)) continue;
|
|
62
|
+
const b = bucket(String(v.cwd));
|
|
63
|
+
const key = normSlug(v.slug);
|
|
64
|
+
const prev = b.last.get(key);
|
|
65
|
+
if (!prev || v.atMs >= prev.atMs) b.last.set(key, { atMs: v.atMs, verdict: v.verdict });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const rows = [];
|
|
69
|
+
const totals = { runs: 0, wastedRuns: 0, verified: 0, refuted: 0, executorUsd: 0, wastedRunRate: 0 };
|
|
70
|
+
for (const b of byCwd.values()) {
|
|
71
|
+
let verified = 0;
|
|
72
|
+
let refuted = 0;
|
|
73
|
+
for (const { verdict } of b.last.values()) {
|
|
74
|
+
if (verdict === 'VERIFIED') verified += 1;
|
|
75
|
+
else if (verdict === 'REFUTED') refuted += 1;
|
|
76
|
+
}
|
|
77
|
+
rows.push({
|
|
78
|
+
cwd: b.cwd,
|
|
79
|
+
project: path.basename(b.cwd),
|
|
80
|
+
runs: b.runs,
|
|
81
|
+
wastedRuns: b.wastedRuns,
|
|
82
|
+
verified,
|
|
83
|
+
refuted,
|
|
84
|
+
executorUsd: b.executorUsd,
|
|
85
|
+
usdPerVerified: verified > 0 ? b.executorUsd / verified : null,
|
|
86
|
+
});
|
|
87
|
+
totals.runs += b.runs;
|
|
88
|
+
totals.wastedRuns += b.wastedRuns;
|
|
89
|
+
totals.verified += verified;
|
|
90
|
+
totals.refuted += refuted;
|
|
91
|
+
totals.executorUsd += b.executorUsd;
|
|
92
|
+
}
|
|
93
|
+
totals.wastedRunRate = totals.runs > 0 ? totals.wastedRuns / totals.runs : 0;
|
|
94
|
+
|
|
95
|
+
rows.sort((a, b) => b.executorUsd - a.executorUsd || (a.project < b.project ? -1 : a.project > b.project ? 1 : 0));
|
|
96
|
+
|
|
97
|
+
const pct = typeof weeklyUtilizationPct === 'number' ? weeklyUtilizationPct : NaN;
|
|
98
|
+
const hasPct = Number.isFinite(pct) && pct > 0;
|
|
99
|
+
return {
|
|
100
|
+
fromMs,
|
|
101
|
+
toMs,
|
|
102
|
+
rows,
|
|
103
|
+
totals,
|
|
104
|
+
weeklyUtilizationPct: weeklyUtilizationPct == null ? null : weeklyUtilizationPct,
|
|
105
|
+
verifiedPerUsagePct: hasPct ? totals.verified / pct : null,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = { computeLoopEfficiency };
|