claude-code-session-manager 0.73.2 → 0.75.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/AgentLibrary-SpzptNyq.js +3 -0
- package/dist/assets/DataModel-C4M9xVLC.js +1 -0
- package/dist/assets/{History-Dnx86P4n.js → History-92FYrZKS.js} +2 -2
- package/dist/assets/{Hooks-CAYlpqBp.js → Hooks-DlnavO1l.js} +2 -2
- package/dist/assets/HostBilko-CKI7jt1z.js +1 -0
- package/dist/assets/{Library-DVbXlboP.js → Library-Brhxe7xa.js} +1 -1
- package/dist/assets/ListDetail-DmIweYTN.js +1 -0
- package/dist/assets/MarkdownEditor-DA4xqWWi.js +1 -0
- package/dist/assets/{McpServers-BxUk1aR3.js → McpServers-DWXQGzVc.js} +2 -2
- package/dist/assets/{Memory-DVbPQFZw.js → Memory-HCx9Arao.js} +4 -4
- package/dist/assets/Panel-B20C6kN8.js +1 -0
- package/dist/assets/{Permissions-CSLxAoTL.js → Permissions-C4sPgLpA.js} +3 -3
- package/dist/assets/Plugins-CaRenCBg.js +2 -0
- package/dist/assets/ProvenanceBadge-DfrXzTeh.js +1 -0
- package/dist/assets/SaveBar-BmpUx2Yh.js +1 -0
- package/dist/assets/{Scheduler-CrBVVXN1.js → Scheduler-xUD8pQ6q.js} +7 -7
- package/dist/assets/ScopeSwitcher-DVlKMck5.js +1 -0
- package/dist/assets/Settings-C457mxOK.js +3 -0
- package/dist/assets/{SkillReferenceGraph-B1DaeT2R.js → SkillReferenceGraph-D3dDc5X7.js} +1 -1
- package/dist/assets/{Skills-CSdxEC-f.js → Skills-BNDlbftC.js} +2 -2
- package/dist/assets/SystemPrompt-d-ykNRec.js +1 -0
- package/dist/assets/TagLibrary-D3JWkQK-.js +1 -0
- package/dist/assets/{TiptapBody-4hzD0jjv.js → TiptapBody-B1Ox6D6E.js} +1 -1
- package/dist/assets/Toggle-CaYw6Lrj.js +1 -0
- package/dist/assets/{index-DJ6xdB0r.js → index-CEeLxNCz.js} +659 -655
- package/dist/assets/{index-D9C8N3pn.css → index-Dbo9R4e0.css} +1 -1
- package/dist/assets/{settingsSchema-DVblsXFp.js → settingsSchema-DUkd45M5.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +3 -1
- package/plugins/session-manager-dev/skills/memory-sanitation/SKILL.md +21 -1
- package/scripts/scheduler-mcp-server.cjs +471 -0
- package/src/main/__tests__/agentModelResolve.test.cjs +19 -0
- package/src/main/__tests__/crossProjectFeedback.test.cjs +7 -0
- package/src/main/__tests__/crossProjectFeedbackRoutes.test.cjs +7 -0
- package/src/main/__tests__/epicMint.test.cjs +50 -2
- package/src/main/__tests__/health-claude-md-budget.test.cjs +57 -0
- package/src/main/__tests__/prdCreate.test.cjs +17 -0
- package/src/main/__tests__/promptSessionsCreateEpicHandler.test.cjs +7 -0
- package/src/main/__tests__/seedAgentPersonas.test.cjs +105 -0
- package/src/main/__tests__/seedSchedulerMcp.test.cjs +145 -0
- package/src/main/__tests__/seedStatus.test.cjs +100 -0
- package/src/main/__tests__/workTypeLibrary.test.cjs +89 -0
- package/src/main/agentLibrary.cjs +43 -0
- package/src/main/health.cjs +51 -1
- package/src/main/index.cjs +44 -3
- package/src/main/ipcSchemas.cjs +46 -9
- package/src/main/lib/__tests__/agentPersonaSchema.test.cjs +67 -0
- package/src/main/lib/__tests__/delegationReadiness.test.cjs +185 -0
- package/src/main/lib/__tests__/epicDelegationStats.test.cjs +137 -0
- package/src/main/lib/agentModelResolve.cjs +32 -1
- package/src/main/lib/agentPersonaSchema.cjs +82 -0
- package/src/main/lib/delegationReadiness.cjs +141 -0
- package/src/main/lib/epicDelegationStats.cjs +124 -0
- package/src/main/lib/epicMint.cjs +74 -1
- package/src/main/lib/prdFrontmatter.cjs +6 -5
- package/src/main/lib/promptSessionSchema.cjs +7 -15
- package/src/main/lib/workTypeLibrary.cjs +48 -0
- package/src/main/seedAgentPersonas.cjs +92 -0
- package/src/main/seedDevPlugin.cjs +14 -2
- package/src/main/seedSchedulerMcp.cjs +148 -0
- package/src/main/seedStatus.cjs +59 -0
- package/src/preload/api.d.ts +47 -3
- package/src/preload/index.cjs +7 -0
- package/src/seed/agents/architect.md +64 -0
- package/src/seed/agents/dev-lead.md +42 -0
- package/src/seed/prompts/code-review/ac-coverage-check.md +8 -0
- package/src/seed/prompts/code-review/correctness-only.md +8 -0
- package/src/seed/prompts/code-review/full-spectrum-high.md +8 -0
- package/src/seed/prompts/code-review/hallucination-check.md +8 -0
- package/src/seed/prompts/code-review/public-api-compat.md +8 -0
- package/src/seed/prompts/code-review/readability-naming.md +8 -0
- package/src/seed/prompts/debugging/bug-as-failing-test.md +8 -0
- package/src/seed/prompts/debugging/git-bisect-regression.md +8 -0
- package/src/seed/prompts/debugging/instrument-intermittent-bug.md +8 -0
- package/src/seed/prompts/debugging/localize-pipeline-failure.md +8 -0
- package/src/seed/prompts/debugging/reproduce-then-diagnose.md +8 -0
- package/src/seed/prompts/documentation/adr-from-change.md +8 -0
- package/src/seed/prompts/documentation/module-readme.md +8 -0
- package/src/seed/prompts/documentation/onboarding-plan.md +8 -0
- package/src/seed/prompts/documentation/refresh-claude-md.md +8 -0
- package/src/seed/prompts/documentation/tsdoc-public-exports.md +8 -0
- package/src/seed/prompts/git-pr/conventional-commit.md +8 -0
- package/src/seed/prompts/git-pr/draft-pr-title-body.md +8 -0
- package/src/seed/prompts/git-pr/pre-commit-safety-sweep.md +8 -0
- package/src/seed/prompts/git-pr/release-notes-block.md +8 -0
- package/src/seed/prompts/git-pr/split-large-pr.md +8 -0
- package/src/seed/prompts/performance/bundle-startup-audit.md +8 -0
- package/src/seed/prompts/performance/complexity-audit.md +8 -0
- package/src/seed/prompts/performance/cpu-profile-hot-path.md +8 -0
- package/src/seed/prompts/performance/db-query-plan-review.md +8 -0
- package/src/seed/prompts/performance/memory-leak-hunt.md +8 -0
- package/src/seed/prompts/qa/api-contract-tests.md +8 -0
- package/src/seed/prompts/qa/e2e-critical-path.md +8 -0
- package/src/seed/prompts/qa/failing-test-for-bug.md +8 -0
- package/src/seed/prompts/qa/find-missing-test-coverage.md +8 -0
- package/src/seed/prompts/qa/stabilize-flaky-test.md +8 -0
- package/src/seed/prompts/qa/tdd-red-first.md +8 -0
- package/src/seed/prompts/qa/visual-regression-review.md +8 -0
- package/src/seed/prompts/qa/wcag-axe-scan.md +8 -0
- package/src/seed/prompts/refactoring/dead-code-sweep.md +8 -0
- package/src/seed/prompts/refactoring/extract-duplicated-pattern.md +8 -0
- package/src/seed/prompts/refactoring/modernize-legacy-file.md +8 -0
- package/src/seed/prompts/refactoring/reduce-cyclomatic-complexity.md +8 -0
- package/src/seed/prompts/refactoring/tighten-module-boundaries.md +8 -0
- package/src/seed/prompts/security/authz-audit.md +8 -0
- package/src/seed/prompts/security/crypto-correctness.md +8 -0
- package/src/seed/prompts/security/cwe-top-25-hunt.md +8 -0
- package/src/seed/prompts/security/dependency-audit.md +8 -0
- package/src/seed/prompts/security/ipc-boundary-hardening.md +8 -0
- package/src/seed/prompts/security/owasp-top-10-staged-diff.md +8 -0
- package/src/seed/prompts/security/secret-credential-scan.md +8 -0
- package/dist/assets/AgentLibrary-D0frFrHG.js +0 -3
- package/dist/assets/HostBilko-CXykS2ap.js +0 -1
- package/dist/assets/ListDetail-DXd5rB2F.js +0 -1
- package/dist/assets/MarkdownEditor-D0mO1TUZ.js +0 -1
- package/dist/assets/Panel-DigZB9Yd.js +0 -1
- package/dist/assets/Plugins-DOTiP9sQ.js +0 -2
- package/dist/assets/ProvenanceBadge-PsNd-CBq.js +0 -1
- package/dist/assets/SaveBar-C4bgDklX.js +0 -1
- package/dist/assets/ScopeSwitcher-RMAwgmj4.js +0 -1
- package/dist/assets/Settings-CM4lZxnn.js +0 -3
- package/dist/assets/SystemPrompt-j-WYOC5f.js +0 -1
- package/dist/assets/TagLibrary-Fj0zrPru.js +0 -1
- package/dist/assets/Toggle-H6pNcv3l.js +0 -1
package/src/main/index.cjs
CHANGED
|
@@ -36,6 +36,7 @@ const epicWorktreeMint = require('./lib/epicWorktreeMint.cjs');
|
|
|
36
36
|
const epicWorktreeMerge = require('./lib/epicWorktreeMerge.cjs');
|
|
37
37
|
const epicWorktreeProjectConfig = require('./lib/epicWorktreeProjectConfig.cjs');
|
|
38
38
|
const agentLibrary = require('./agentLibrary.cjs');
|
|
39
|
+
const { checkDelegationReadiness } = require('./lib/delegationReadiness.cjs');
|
|
39
40
|
const { sendIfAlive } = require('./lib/sendToRenderer.cjs');
|
|
40
41
|
const { resolveBuildTarget } = require('./lib/buildTarget.cjs');
|
|
41
42
|
const crossProjectFeedback = require('./lib/crossProjectFeedback.cjs');
|
|
@@ -54,6 +55,9 @@ const teams = require('./teams.cjs');
|
|
|
54
55
|
const queueOps = require('./queueOps.cjs');
|
|
55
56
|
const pluginInstall = require('./pluginInstall.cjs');
|
|
56
57
|
const { seedDevPlugin } = require('./seedDevPlugin.cjs');
|
|
58
|
+
const { seedAgentPersonas } = require('./seedAgentPersonas.cjs');
|
|
59
|
+
const { seedSchedulerMcp } = require('./seedSchedulerMcp.cjs');
|
|
60
|
+
const { getSeedStatus } = require('./seedStatus.cjs');
|
|
57
61
|
const otel = require('./otel.cjs');
|
|
58
62
|
const otelSettings = require('./otelSettings.cjs');
|
|
59
63
|
const { registerHistoryAggregatorHandlers, finalizeClosedDays, refreshIntradayToday } = require('./historyAggregator.cjs');
|
|
@@ -67,6 +71,7 @@ const { registerProjectBriefIpc } = require('./projectBrief.cjs');
|
|
|
67
71
|
const { registerProjectPagesIpc } = require('./projectPages.cjs');
|
|
68
72
|
const { registerBilkoHostIpc } = require('./bilkoHost.cjs');
|
|
69
73
|
const promptSessionTranscript = require('./promptSessionTranscript.cjs');
|
|
74
|
+
const { computeEpicDelegationStats } = require('./lib/epicDelegationStats.cjs');
|
|
70
75
|
const agentMemory = require('./agentMemory.cjs');
|
|
71
76
|
const git = require('./git.cjs');
|
|
72
77
|
const heapSnapshot = require('./heapSnapshot.cjs');
|
|
@@ -466,11 +471,11 @@ ipcMain.handle('agents:list-personas', () => agentLibrary.listPersonas());
|
|
|
466
471
|
function broadcastAgentsChanged() {
|
|
467
472
|
sendIfAlive(mainWindow, 'agents:changed', {});
|
|
468
473
|
}
|
|
469
|
-
ipcMain.handle('agents:save-persona', async (
|
|
474
|
+
ipcMain.handle('agents:save-persona', validated(schemas.agentsSavePersona, async (payload) => {
|
|
470
475
|
const result = await agentLibrary.savePersona(payload);
|
|
471
476
|
broadcastAgentsChanged();
|
|
472
477
|
return result;
|
|
473
|
-
});
|
|
478
|
+
}));
|
|
474
479
|
ipcMain.handle('agents:delete-persona', async (_e, payload) => {
|
|
475
480
|
const result = await agentLibrary.deletePersona(payload);
|
|
476
481
|
broadcastAgentsChanged();
|
|
@@ -481,6 +486,19 @@ ipcMain.handle('agents:remove-override', async (_e, payload) => {
|
|
|
481
486
|
broadcastAgentsChanged();
|
|
482
487
|
return result;
|
|
483
488
|
});
|
|
489
|
+
// New Epic AIM composer (epicIntake.ts's `agentBody`/`agentPath`): unlike
|
|
490
|
+
// list-personas above (global dir only), this honors the project-overlay-
|
|
491
|
+
// then-global precedence so an Epic's opening prompt carries the persona
|
|
492
|
+
// body that actually applies to its cwd. Read-only.
|
|
493
|
+
ipcMain.handle('agents:get-persona-body', validated(schemas.agentsGetPersonaBody, (payload) =>
|
|
494
|
+
agentLibrary.getPersonaBody(payload)));
|
|
495
|
+
|
|
496
|
+
// "Can this project actually delegate?" probe (PRD: delegation-readiness).
|
|
497
|
+
// Structured data over the four preconditions for scheduler_create_prd being
|
|
498
|
+
// in an agent's tool list at all — surfacing it in the UI is a sibling PRD.
|
|
499
|
+
ipcMain.handle('app:delegation-readiness', validated(schemas.delegationReadinessCwd, (payload) =>
|
|
500
|
+
checkDelegationReadiness(payload)
|
|
501
|
+
));
|
|
484
502
|
|
|
485
503
|
ipcMain.handle('app:engage-rules-path', () => process.env.SESSION_MANAGER_ENGAGE_RULES || null);
|
|
486
504
|
|
|
@@ -488,6 +506,9 @@ ipcMain.handle('app:engage-rules-path', () => process.env.SESSION_MANAGER_ENGAGE
|
|
|
488
506
|
// on disk or the home self-check failed (e.g. macOS /Users symlink mismatch).
|
|
489
507
|
ipcMain.handle('app:claude-bin-status', () => bootClaudeBin);
|
|
490
508
|
ipcMain.handle('app:home-self-check', () => bootHomeSelfCheck);
|
|
509
|
+
// First-boot seeder outcomes ('done' | 'pending' | 'exhausted' per seeder),
|
|
510
|
+
// read live from each seeder's own marker file — see seedStatus.cjs.
|
|
511
|
+
ipcMain.handle('app:seed-status', () => getSeedStatus());
|
|
491
512
|
|
|
492
513
|
ipcMain.handle('app:pick-directory', async () => {
|
|
493
514
|
console.log('[main] pick-directory invoked');
|
|
@@ -802,6 +823,11 @@ ipcMain.handle('promptSessionTranscript:read', validated(schemas.promptSessionTr
|
|
|
802
823
|
const turns = await promptSessionTranscript.readTurns(cwd, epicId, { limit });
|
|
803
824
|
return { turns };
|
|
804
825
|
}));
|
|
826
|
+
// Derived-at-read-time "PRDs queued vs inline edits" counter (no writer —
|
|
827
|
+
// reads the Epic's own live prds/ dir + its own claude session transcript).
|
|
828
|
+
ipcMain.handle('epicDelegationStats:get', validated(schemas.epicDelegationStatsGet, async ({ cwd, epicId, claudeSessionId }) => {
|
|
829
|
+
return computeEpicDelegationStats(cwd, epicId, claudeSessionId || null);
|
|
830
|
+
}));
|
|
805
831
|
// Renderer Epic lifecycle trace (PRD 940) — append-only, no read/list IPC.
|
|
806
832
|
// Kind allowlist enforced by schemas.auditLogAppend, not here.
|
|
807
833
|
ipcMain.handle('auditLog:append', validated(schemas.auditLogAppend, async ({ kind, cwd, epicId, source }) => {
|
|
@@ -1149,9 +1175,24 @@ app.whenReady().then(async () => {
|
|
|
1149
1175
|
// First-boot default: install the bundled session-manager-dev plugin (its 10
|
|
1150
1176
|
// dev skills) from the app's own marketplace. One-shot + idempotent; never
|
|
1151
1177
|
// throws. SM_SEED_DEV_PLUGIN_DISABLE=1 to opt out.
|
|
1152
|
-
seedDevPlugin({ logger: console }).catch((e) => {
|
|
1178
|
+
seedDevPlugin({ logger: console, writeLog: logs.writeLine }).catch((e) => {
|
|
1153
1179
|
logs.writeLine({ scope: 'seed-dev-plugin', level: 'error', message: 'seed failed', meta: { error: e?.message } });
|
|
1154
1180
|
});
|
|
1181
|
+
// First-boot default: seed the bundled Architect + Dev Lead Agent personas
|
|
1182
|
+
// into ~/.claude/agents/ so the Agent Library isn't empty on a fresh
|
|
1183
|
+
// install. One-shot + idempotent; never overwrites an existing persona;
|
|
1184
|
+
// never throws. SM_SEED_AGENT_PERSONAS_DISABLE=1 to opt out.
|
|
1185
|
+
seedAgentPersonas({ logger: console, writeLog: logs.writeLine }).catch((e) => {
|
|
1186
|
+
logs.writeLine({ scope: 'seed-agent-personas', level: 'error', message: 'seed failed', meta: { error: e?.message } });
|
|
1187
|
+
});
|
|
1188
|
+
// First-boot default: register the session-manager-scheduler MCP server at
|
|
1189
|
+
// USER scope so scheduler_create_prd is available in every project, not
|
|
1190
|
+
// just this repo's own .mcp.json. One-shot + idempotent; never overwrites
|
|
1191
|
+
// an existing registration; never throws. SM_SEED_SCHEDULER_MCP_DISABLE=1
|
|
1192
|
+
// to opt out.
|
|
1193
|
+
seedSchedulerMcp({ logger: console, writeLog: logs.writeLine }).catch((e) => {
|
|
1194
|
+
logs.writeLine({ scope: 'seed-scheduler-mcp', level: 'error', message: 'seed failed', meta: { error: e?.message } });
|
|
1195
|
+
});
|
|
1155
1196
|
// History rollup finalize pass: deferred 30s past boot so it never competes
|
|
1156
1197
|
// with first-paint, fire-and-forget (cron/offline refresh is PRD 651 — this
|
|
1157
1198
|
// is just the one-shot in-app pass).
|
package/src/main/ipcSchemas.cjs
CHANGED
|
@@ -9,6 +9,8 @@ const { z } = require('zod');
|
|
|
9
9
|
const os = require('node:os');
|
|
10
10
|
const path = require('node:path');
|
|
11
11
|
const { PromptSessionSchema, EpicTagSchema, EpicSourceSchema, EpicIntakeSectionSchema } = require('./lib/promptSessionSchema.cjs');
|
|
12
|
+
const { PrdWorkTypeSchema } = require('./lib/workTypeLibrary.cjs');
|
|
13
|
+
const { AgentPersonaSaveSchema } = require('./lib/agentPersonaSchema.cjs');
|
|
12
14
|
|
|
13
15
|
// ──────────────────────────────────────────── PTY
|
|
14
16
|
const ptySpawn = z.object({
|
|
@@ -331,13 +333,13 @@ const schedulerCreatePrd = z.object({
|
|
|
331
333
|
// within an Epic's own chat session joins that Epic even if the tool call
|
|
332
334
|
// forgot to pass sourcePromptId explicitly.
|
|
333
335
|
originClaudeSessionId: z.string().min(1).max(128).regex(NO_NEWLINE_RE, 'must not contain newlines').optional(),
|
|
334
|
-
// User-selected
|
|
335
|
-
//
|
|
336
|
-
//
|
|
337
|
-
//
|
|
338
|
-
//
|
|
339
|
-
//
|
|
340
|
-
tag:
|
|
336
|
+
// User-selected work-type tag (PRD 774; independence from the parent
|
|
337
|
+
// Epic's own tag confirmed PRD 1041) — deterministic, never LLM-classified.
|
|
338
|
+
// Epic tag and PRD tag draw on the same WorkType vocabulary
|
|
339
|
+
// (lib/workTypeLibrary.cjs) but are independent values; 'discussion' is
|
|
340
|
+
// excluded here for the same reason adminPrdFrontmatterPatch excludes it
|
|
341
|
+
// below — a discussion-tagged Epic never reaches PRD authoring.
|
|
342
|
+
tag: PrdWorkTypeSchema.optional(),
|
|
341
343
|
});
|
|
342
344
|
|
|
343
345
|
// Bulk archive: slug list, capped to limit unbounded retag/archive payloads.
|
|
@@ -387,8 +389,9 @@ const adminPrdFrontmatterPatch = z.object({
|
|
|
387
389
|
sourceTabId: z.string().min(1).max(128).regex(NO_NEWLINE_RE, 'must not contain newlines').optional(),
|
|
388
390
|
// 'discussion' deliberately excluded: matches prdFrontmatter.cjs's
|
|
389
391
|
// parsePrdFile, which never parses that value for a PRD's tag key (a
|
|
390
|
-
// discussion-tagged Epic never reaches PRD authoring)
|
|
391
|
-
|
|
392
|
+
// discussion-tagged Epic never reaches PRD authoring) — see
|
|
393
|
+
// workTypeLibrary.cjs's PrdWorkTypeSchema.
|
|
394
|
+
tag: PrdWorkTypeSchema.optional(),
|
|
392
395
|
// Provenance stamp (PRD-authoring lockdown). Free-text rather than an enum
|
|
393
396
|
// — 'scheduler-api' (create-time) and 'legacy-adopted' (migration/manual
|
|
394
397
|
// adopt) are the two values this codebase writes today, but the field
|
|
@@ -464,6 +467,14 @@ const setSessionSlotsSchema = z.object({
|
|
|
464
467
|
cap: z.number().int().min(0).max(10),
|
|
465
468
|
}).strict();
|
|
466
469
|
|
|
470
|
+
// ──────────────────────────────────────────── Agent Library
|
|
471
|
+
// agents:save-persona — the Agent entity (Agent/WorkType/Epic/PRD/Job ERD)
|
|
472
|
+
// had no boundary validation until this schema; Epic and Job each got theirs
|
|
473
|
+
// only after a production incident (see scheduleJobSchema.cjs's header).
|
|
474
|
+
// Full field-level rules live in lib/agentPersonaSchema.cjs, mirroring
|
|
475
|
+
// AgentPersonaSaveInput (src/preload/api.d.ts) field-for-field.
|
|
476
|
+
const agentsSavePersona = AgentPersonaSaveSchema;
|
|
477
|
+
|
|
467
478
|
// ──────────────────────────────────────────── Memory tool (Bundle C, cycle 3)
|
|
468
479
|
// Workspace-scoped markdown store at ~/.claude/projects/<ws>/memory/.
|
|
469
480
|
// Slug regex is shared with memoryTool.cjs/memoryAggregate.cjs via lib/memorySlug.cjs;
|
|
@@ -526,6 +537,17 @@ const projectBriefCwd = z.object({
|
|
|
526
537
|
cwd: z.string().min(1).max(4096),
|
|
527
538
|
}).strict();
|
|
528
539
|
|
|
540
|
+
// ──────────────────────────────────────────── Delegation readiness
|
|
541
|
+
const delegationReadinessCwd = z.object({
|
|
542
|
+
cwd: z.string().min(1).max(4096),
|
|
543
|
+
}).strict();
|
|
544
|
+
|
|
545
|
+
// ──────────────────────────────────────────── Agent persona body (New Epic AIM)
|
|
546
|
+
const agentsGetPersonaBody = z.object({
|
|
547
|
+
cwd: z.string().min(1).max(4096),
|
|
548
|
+
name: z.string().min(1).max(200),
|
|
549
|
+
}).strict();
|
|
550
|
+
|
|
529
551
|
// ──────────────────────────────────────────── Project Pages (PRD 932)
|
|
530
552
|
// Same validation split as projectBriefCwd above: real path validation is
|
|
531
553
|
// config.cjs's validatePath at first fs access in projectPages.cjs.
|
|
@@ -597,6 +619,17 @@ const promptSessionTranscriptRead = z.object({
|
|
|
597
619
|
limit: z.number().int().positive().max(10_000).optional(),
|
|
598
620
|
}).strict();
|
|
599
621
|
|
|
622
|
+
// ──────────────────────────────────────────── Epic delegation stats
|
|
623
|
+
// (derived-at-read-time "PRDs queued vs inline edits" counter surfaced on
|
|
624
|
+
// the Epic card). claudeSessionId is optional — a proposed Epic that never
|
|
625
|
+
// started chatting has none, and the computation just reports 0 inline
|
|
626
|
+
// edits rather than throwing.
|
|
627
|
+
const epicDelegationStatsGet = z.object({
|
|
628
|
+
cwd: z.string().min(1).max(4096),
|
|
629
|
+
epicId: z.string().regex(PROMPT_SESSION_EPIC_ID_RE),
|
|
630
|
+
claudeSessionId: z.string().max(128).optional().nullable(),
|
|
631
|
+
}).strict();
|
|
632
|
+
|
|
600
633
|
// ──────────────────────────────────────────── Audit log (renderer Epic
|
|
601
634
|
// lifecycle trace, PRD 940). Kind allowlist is enforced HERE, main-side — the
|
|
602
635
|
// renderer cannot write arbitrary kinds through this channel, only the six
|
|
@@ -938,6 +971,7 @@ module.exports = {
|
|
|
938
971
|
projectBriefUpdate,
|
|
939
972
|
promptSessionTranscriptAppend,
|
|
940
973
|
promptSessionTranscriptRead,
|
|
974
|
+
epicDelegationStatsGet,
|
|
941
975
|
promptSessionsMergeActiveIndex,
|
|
942
976
|
promptSessionsCreateEpic,
|
|
943
977
|
promptSessionsCreateWorktree,
|
|
@@ -962,6 +996,9 @@ module.exports = {
|
|
|
962
996
|
chatClassifyTicket,
|
|
963
997
|
chatExternalSend,
|
|
964
998
|
exchangesList,
|
|
999
|
+
agentsSavePersona,
|
|
1000
|
+
agentsGetPersonaBody,
|
|
1001
|
+
delegationReadinessCwd,
|
|
965
1002
|
},
|
|
966
1003
|
validated,
|
|
967
1004
|
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agentPersonaSchema.test.cjs — unit tests for the canonical Agent persona
|
|
3
|
+
* WRITE-path zod schema and its assertValidAgentPersonaSave fail-closed
|
|
4
|
+
* helper.
|
|
5
|
+
*
|
|
6
|
+
* Agent is the third entity in the Agent/WorkType/Epic/PRD/Job ERD to get a
|
|
7
|
+
* boundary schema — Epic (promptSessionSchema.cjs) and Job
|
|
8
|
+
* (scheduleJobSchema.cjs) each got theirs only after a production incident;
|
|
9
|
+
* this one closes the gap proactively before Agent has had its own.
|
|
10
|
+
*
|
|
11
|
+
* Run: timeout 120 npx vitest run src/main/lib/__tests__/agentPersonaSchema.test.cjs
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
import { test, expect } from 'vitest';
|
|
17
|
+
const {
|
|
18
|
+
AgentPersonaSaveSchema,
|
|
19
|
+
assertValidAgentPersonaSave,
|
|
20
|
+
} = require('../agentPersonaSchema.cjs');
|
|
21
|
+
|
|
22
|
+
function validPersona(overrides = {}) {
|
|
23
|
+
return {
|
|
24
|
+
name: 'code-reviewer',
|
|
25
|
+
description: 'Reviews diffs for correctness and style.',
|
|
26
|
+
tools: ['Read', 'Grep', 'Glob'],
|
|
27
|
+
model: 'inherit',
|
|
28
|
+
color: 'blue',
|
|
29
|
+
tags: ['feature', 'bug'],
|
|
30
|
+
projects: ['*'],
|
|
31
|
+
action: '/develop\n\nReview the current diff.',
|
|
32
|
+
actionLabel: 'Review',
|
|
33
|
+
title: 'Engineering — Code Reviewer',
|
|
34
|
+
body: 'You are a meticulous code reviewer.',
|
|
35
|
+
...overrides,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
test('a valid full persona parses', () => {
|
|
40
|
+
const full = validPersona();
|
|
41
|
+
expect(() => assertValidAgentPersonaSave(full)).not.toThrow();
|
|
42
|
+
expect(AgentPersonaSaveSchema.safeParse(full).success).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('a persona with an unknown tag is rejected', () => {
|
|
46
|
+
const badTag = validPersona({ tags: ['feature', 'not-a-real-worktype'] });
|
|
47
|
+
expect(() => assertValidAgentPersonaSave(badTag)).toThrow(/tags/);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('a persona with an uppercase/spaced name is rejected', () => {
|
|
51
|
+
const badName = validPersona({ name: 'Code Reviewer' });
|
|
52
|
+
expect(() => assertValidAgentPersonaSave(badName)).toThrow(/name/);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('a persona with only the required fields parses', () => {
|
|
56
|
+
const minimal = {
|
|
57
|
+
name: 'minimal-agent',
|
|
58
|
+
description: '',
|
|
59
|
+
tools: [],
|
|
60
|
+
model: 'inherit',
|
|
61
|
+
color: '',
|
|
62
|
+
tags: [],
|
|
63
|
+
body: '',
|
|
64
|
+
};
|
|
65
|
+
expect(() => assertValidAgentPersonaSave(minimal)).not.toThrow();
|
|
66
|
+
expect(AgentPersonaSaveSchema.safeParse(minimal).success).toBe(true);
|
|
67
|
+
});
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* delegationReadiness.test.cjs — unit tests for the "can this project
|
|
3
|
+
* actually delegate?" probe. Uses a temp HOME and a temp cwd so the real
|
|
4
|
+
* ~/.claude and this repo's own config are never touched.
|
|
5
|
+
*
|
|
6
|
+
* Run: timeout 300 npx vitest run src/main/lib/__tests__/delegationReadiness.test.cjs
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { test, expect, afterEach } from 'vitest';
|
|
10
|
+
const fs = require('node:fs');
|
|
11
|
+
const fsp = require('node:fs/promises');
|
|
12
|
+
const os = require('node:os');
|
|
13
|
+
const path = require('node:path');
|
|
14
|
+
const { checkDelegationReadiness } = require('../delegationReadiness.cjs');
|
|
15
|
+
|
|
16
|
+
const tmpDirs = [];
|
|
17
|
+
afterEach(async () => {
|
|
18
|
+
while (tmpDirs.length) {
|
|
19
|
+
const d = tmpDirs.pop();
|
|
20
|
+
await fsp.rm(d, { recursive: true, force: true });
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
async function mkTmp(prefix) {
|
|
25
|
+
const d = await fsp.mkdtemp(path.join(os.tmpdir(), prefix));
|
|
26
|
+
tmpDirs.push(d);
|
|
27
|
+
return d;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function writeJson(absPath, value) {
|
|
31
|
+
await fsp.mkdir(path.dirname(absPath), { recursive: true });
|
|
32
|
+
await fsp.writeFile(absPath, JSON.stringify(value), 'utf8');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function makeGreenFixtures() {
|
|
36
|
+
const homeDir = await mkTmp('sm-delegation-home-');
|
|
37
|
+
const cwd = await mkTmp('sm-delegation-cwd-');
|
|
38
|
+
|
|
39
|
+
await writeJson(path.join(homeDir, '.claude.json'), {
|
|
40
|
+
mcpServers: { 'session-manager-scheduler': { type: 'stdio', command: 'node', args: [] } },
|
|
41
|
+
});
|
|
42
|
+
await writeJson(path.join(homeDir, '.claude', 'settings.json'), {
|
|
43
|
+
enabledPlugins: { 'session-manager-dev@session-manager': true },
|
|
44
|
+
});
|
|
45
|
+
await fsp.mkdir(path.join(homeDir, '.claude', 'agents'), { recursive: true });
|
|
46
|
+
await fsp.writeFile(path.join(homeDir, '.claude', 'agents', 'dev-lead.md'), '# dev-lead', 'utf8');
|
|
47
|
+
await writeJson(path.join(cwd, '.claude', 'settings.json'), {
|
|
48
|
+
hooks: {
|
|
49
|
+
PreToolUse: [
|
|
50
|
+
{
|
|
51
|
+
matcher: 'Write|Edit|NotebookEdit',
|
|
52
|
+
hooks: [{ type: 'command', command: 'node scripts/hooks/guard-prd-writes.cjs' }],
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return { homeDir, cwd };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
test('all four checks pass on a fully-configured project', async () => {
|
|
62
|
+
const { homeDir, cwd } = await makeGreenFixtures();
|
|
63
|
+
const result = checkDelegationReadiness({ cwd, homeDir });
|
|
64
|
+
|
|
65
|
+
expect(result.ok).toBe(true);
|
|
66
|
+
expect(result.checks).toHaveLength(4);
|
|
67
|
+
expect(result.checks.every((c) => c.ok)).toBe(true);
|
|
68
|
+
expect(result.checks.map((c) => c.id)).toEqual([
|
|
69
|
+
'scheduler-mcp',
|
|
70
|
+
'dev-plugin',
|
|
71
|
+
'agent-personas',
|
|
72
|
+
'prd-write-guard',
|
|
73
|
+
]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('scheduler-mcp: passes via project-scope .mcp.json even without user scope', async () => {
|
|
77
|
+
const { homeDir, cwd } = await makeGreenFixtures();
|
|
78
|
+
await writeJson(path.join(homeDir, '.claude.json'), { mcpServers: {} });
|
|
79
|
+
await writeJson(path.join(cwd, '.mcp.json'), {
|
|
80
|
+
mcpServers: { 'session-manager-scheduler': { command: 'node', args: [] } },
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const result = checkDelegationReadiness({ cwd, homeDir });
|
|
84
|
+
const check = result.checks.find((c) => c.id === 'scheduler-mcp');
|
|
85
|
+
expect(check.ok).toBe(true);
|
|
86
|
+
expect(result.ok).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('scheduler-mcp: fails with a runnable fix when absent from both scopes', async () => {
|
|
90
|
+
const { homeDir, cwd } = await makeGreenFixtures();
|
|
91
|
+
await writeJson(path.join(homeDir, '.claude.json'), { mcpServers: {} });
|
|
92
|
+
|
|
93
|
+
const result = checkDelegationReadiness({ cwd, homeDir });
|
|
94
|
+
const check = result.checks.find((c) => c.id === 'scheduler-mcp');
|
|
95
|
+
expect(check.ok).toBe(false);
|
|
96
|
+
expect(result.ok).toBe(false);
|
|
97
|
+
expect(check.fix).toMatch(/^claude mcp add session-manager-scheduler --scope user/);
|
|
98
|
+
expect(check.fix).toContain('scheduler-mcp-server.cjs');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('dev-plugin: fails independently when enabledPlugins is missing the key', async () => {
|
|
102
|
+
const { homeDir, cwd } = await makeGreenFixtures();
|
|
103
|
+
await writeJson(path.join(homeDir, '.claude', 'settings.json'), { enabledPlugins: {} });
|
|
104
|
+
|
|
105
|
+
const result = checkDelegationReadiness({ cwd, homeDir });
|
|
106
|
+
const check = result.checks.find((c) => c.id === 'dev-plugin');
|
|
107
|
+
expect(check.ok).toBe(false);
|
|
108
|
+
expect(result.ok).toBe(false);
|
|
109
|
+
expect(check.fix).toBeTruthy();
|
|
110
|
+
// the other three checks still pass independently
|
|
111
|
+
expect(result.checks.filter((c) => c.id !== 'dev-plugin').every((c) => c.ok)).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('agent-personas: fails independently when ~/.claude/agents has no personas', async () => {
|
|
115
|
+
const { homeDir, cwd } = await makeGreenFixtures();
|
|
116
|
+
await fsp.rm(path.join(homeDir, '.claude', 'agents', 'dev-lead.md'));
|
|
117
|
+
|
|
118
|
+
const result = checkDelegationReadiness({ cwd, homeDir });
|
|
119
|
+
const check = result.checks.find((c) => c.id === 'agent-personas');
|
|
120
|
+
expect(check.ok).toBe(false);
|
|
121
|
+
expect(result.ok).toBe(false);
|
|
122
|
+
expect(result.checks.filter((c) => c.id !== 'agent-personas').every((c) => c.ok)).toBe(true);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('agent-personas: passes via project-scope .claude/agents/ even without global personas', async () => {
|
|
126
|
+
const { homeDir, cwd } = await makeGreenFixtures();
|
|
127
|
+
await fsp.rm(path.join(homeDir, '.claude', 'agents'), { recursive: true, force: true });
|
|
128
|
+
await fsp.mkdir(path.join(cwd, '.claude', 'agents'), { recursive: true });
|
|
129
|
+
await fsp.writeFile(path.join(cwd, '.claude', 'agents', 'builder.md'), '# builder', 'utf8');
|
|
130
|
+
|
|
131
|
+
const result = checkDelegationReadiness({ cwd, homeDir });
|
|
132
|
+
const check = result.checks.find((c) => c.id === 'agent-personas');
|
|
133
|
+
expect(check.ok).toBe(true);
|
|
134
|
+
expect(result.ok).toBe(true);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('agent-personas: fails when the directory does not exist at all', async () => {
|
|
138
|
+
const { homeDir, cwd } = await makeGreenFixtures();
|
|
139
|
+
await fsp.rm(path.join(homeDir, '.claude', 'agents'), { recursive: true, force: true });
|
|
140
|
+
|
|
141
|
+
const result = checkDelegationReadiness({ cwd, homeDir });
|
|
142
|
+
const check = result.checks.find((c) => c.id === 'agent-personas');
|
|
143
|
+
expect(check.ok).toBe(false);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('prd-write-guard: fails independently when the hook is missing', async () => {
|
|
147
|
+
const { homeDir, cwd } = await makeGreenFixtures();
|
|
148
|
+
await writeJson(path.join(cwd, '.claude', 'settings.json'), { hooks: { PreToolUse: [] } });
|
|
149
|
+
|
|
150
|
+
const result = checkDelegationReadiness({ cwd, homeDir });
|
|
151
|
+
const check = result.checks.find((c) => c.id === 'prd-write-guard');
|
|
152
|
+
expect(check.ok).toBe(false);
|
|
153
|
+
expect(result.ok).toBe(false);
|
|
154
|
+
expect(result.checks.filter((c) => c.id !== 'prd-write-guard').every((c) => c.ok)).toBe(true);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('unparseable JSON files yield ok:false with a detail, never a throw', async () => {
|
|
158
|
+
const { homeDir, cwd } = await makeGreenFixtures();
|
|
159
|
+
await fs.promises.writeFile(path.join(homeDir, '.claude.json'), '{ not valid json', 'utf8');
|
|
160
|
+
await fs.promises.writeFile(path.join(homeDir, '.claude', 'settings.json'), '{ not valid json', 'utf8');
|
|
161
|
+
await fs.promises.writeFile(path.join(cwd, '.claude', 'settings.json'), '{ not valid json', 'utf8');
|
|
162
|
+
|
|
163
|
+
expect(() => checkDelegationReadiness({ cwd, homeDir })).not.toThrow();
|
|
164
|
+
const result = checkDelegationReadiness({ cwd, homeDir });
|
|
165
|
+
|
|
166
|
+
const scheduler = result.checks.find((c) => c.id === 'scheduler-mcp');
|
|
167
|
+
const devPlugin = result.checks.find((c) => c.id === 'dev-plugin');
|
|
168
|
+
const guard = result.checks.find((c) => c.id === 'prd-write-guard');
|
|
169
|
+
expect(scheduler.ok).toBe(false);
|
|
170
|
+
expect(scheduler.detail).toBeTruthy();
|
|
171
|
+
expect(devPlugin.ok).toBe(false);
|
|
172
|
+
expect(devPlugin.detail).toBeTruthy();
|
|
173
|
+
expect(guard.ok).toBe(false);
|
|
174
|
+
expect(guard.detail).toBeTruthy();
|
|
175
|
+
expect(result.ok).toBe(false);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('missing cwd/.claude/settings.json entirely is treated as guard-absent, not a throw', async () => {
|
|
179
|
+
const { homeDir, cwd } = await makeGreenFixtures();
|
|
180
|
+
await fsp.rm(path.join(cwd, '.claude', 'settings.json'));
|
|
181
|
+
|
|
182
|
+
expect(() => checkDelegationReadiness({ cwd, homeDir })).not.toThrow();
|
|
183
|
+
const result = checkDelegationReadiness({ cwd, homeDir });
|
|
184
|
+
expect(result.checks.find((c) => c.id === 'prd-write-guard').ok).toBe(false);
|
|
185
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* epicDelegationStats.test.cjs — unit tests for the derived-at-read-time
|
|
3
|
+
* "did this Epic delegate?" counters (prdsQueued from the Epic's own live
|
|
4
|
+
* PRD dir, inlineEdits from its interactive claude session transcript).
|
|
5
|
+
*
|
|
6
|
+
* Run: timeout 120 npx vitest run src/main/lib/__tests__/epicDelegationStats.test.cjs
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
import { test, expect, afterEach } from 'vitest';
|
|
12
|
+
const fs = require('node:fs');
|
|
13
|
+
const os = require('node:os');
|
|
14
|
+
const path = require('node:path');
|
|
15
|
+
const {
|
|
16
|
+
computeEpicDelegationStats,
|
|
17
|
+
countPrdsQueued,
|
|
18
|
+
countInlineEdits,
|
|
19
|
+
isApplicationSourcePath,
|
|
20
|
+
} = require('../epicDelegationStats.cjs');
|
|
21
|
+
const { encodeCwd } = require('../encodeCwd.cjs');
|
|
22
|
+
|
|
23
|
+
const tmpDirs = [];
|
|
24
|
+
function makeTmpCwd() {
|
|
25
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'epic-delegation-'));
|
|
26
|
+
tmpDirs.push(dir);
|
|
27
|
+
return dir;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
while (tmpDirs.length) {
|
|
32
|
+
fs.rmSync(tmpDirs.pop(), { recursive: true, force: true });
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
function makeTmpHome() {
|
|
37
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'epic-delegation-home-'));
|
|
38
|
+
tmpDirs.push(dir);
|
|
39
|
+
return dir;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function writeTranscript(homeDir, cwd, sessionId, lines) {
|
|
43
|
+
const dir = path.join(homeDir, '.claude', 'projects', encodeCwd(cwd));
|
|
44
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
45
|
+
const filePath = path.join(dir, `${sessionId}.jsonl`);
|
|
46
|
+
fs.writeFileSync(filePath, lines.map((l) => JSON.stringify(l)).join('\n') + '\n');
|
|
47
|
+
return filePath;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function toolUseLine(name, filePath) {
|
|
51
|
+
return {
|
|
52
|
+
type: 'assistant',
|
|
53
|
+
message: { content: [{ type: 'tool_use', name, id: 't1', input: { file_path: filePath } }] },
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
test('countPrdsQueued counts only .md files in the Epic own live prds dir', () => {
|
|
58
|
+
const cwd = makeTmpCwd();
|
|
59
|
+
const prdsDir = path.join(cwd, 'session-manager-operations', 'scheduler', 'epics', 'epic-1', 'prds');
|
|
60
|
+
fs.mkdirSync(prdsDir, { recursive: true });
|
|
61
|
+
fs.writeFileSync(path.join(prdsDir, '01-foo.md'), '# foo');
|
|
62
|
+
fs.writeFileSync(path.join(prdsDir, '02-bar.md'), '# bar');
|
|
63
|
+
fs.writeFileSync(path.join(prdsDir, '.reserved-03'), '');
|
|
64
|
+
expect(countPrdsQueued(cwd, 'epic-1')).toBe(2);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('countPrdsQueued returns 0 when the prds dir does not exist', () => {
|
|
68
|
+
const cwd = makeTmpCwd();
|
|
69
|
+
expect(countPrdsQueued(cwd, 'missing-epic')).toBe(0);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('isApplicationSourcePath matches src/scripts/plugins/bin, rejects everything else', () => {
|
|
73
|
+
const cwd = '/repo';
|
|
74
|
+
expect(isApplicationSourcePath(cwd, '/repo/src/main/index.cjs')).toBe(true);
|
|
75
|
+
expect(isApplicationSourcePath(cwd, '/repo/scripts/hooks/foo.cjs')).toBe(true);
|
|
76
|
+
expect(isApplicationSourcePath(cwd, '/repo/plugins/dev/skill.md')).toBe(true);
|
|
77
|
+
expect(isApplicationSourcePath(cwd, '/repo/bin/cli.cjs')).toBe(true);
|
|
78
|
+
expect(isApplicationSourcePath(cwd, '/repo/session-manager-operations/scheduler/epics/e1/prds/01.md')).toBe(false);
|
|
79
|
+
expect(isApplicationSourcePath(cwd, '/repo/README.md')).toBe(false);
|
|
80
|
+
expect(isApplicationSourcePath(cwd, '/outside/src/index.cjs')).toBe(false);
|
|
81
|
+
expect(isApplicationSourcePath(cwd, undefined)).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('countInlineEdits counts Write/Edit/NotebookEdit tool_use blocks under application source only', () => {
|
|
85
|
+
const cwd = makeTmpCwd();
|
|
86
|
+
const homeDir = makeTmpHome();
|
|
87
|
+
writeTranscript(homeDir, cwd, 'sess-1', [
|
|
88
|
+
toolUseLine('Write', path.join(cwd, 'src', 'main', 'foo.cjs')),
|
|
89
|
+
toolUseLine('Edit', path.join(cwd, 'src', 'main', 'foo.cjs')),
|
|
90
|
+
toolUseLine('Read', path.join(cwd, 'src', 'main', 'foo.cjs')),
|
|
91
|
+
toolUseLine('Write', path.join(cwd, 'session-manager-operations', 'scheduler', 'epics', 'e1', 'prds', '01.md')),
|
|
92
|
+
toolUseLine('NotebookEdit', path.join(cwd, 'scripts', 'nb.ipynb')),
|
|
93
|
+
]);
|
|
94
|
+
expect(countInlineEdits(cwd, 'sess-1', { homeDir })).toBe(3);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('countInlineEdits returns 0 when the transcript file does not exist', () => {
|
|
98
|
+
const cwd = makeTmpCwd();
|
|
99
|
+
const homeDir = makeTmpHome();
|
|
100
|
+
expect(countInlineEdits(cwd, 'no-such-session', { homeDir })).toBe(0);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('countInlineEdits returns 0 when claudeSessionId is missing', () => {
|
|
104
|
+
const cwd = makeTmpCwd();
|
|
105
|
+
expect(countInlineEdits(cwd, null)).toBe(0);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('countInlineEdits tolerates a malformed trailing line', () => {
|
|
109
|
+
const cwd = makeTmpCwd();
|
|
110
|
+
const homeDir = makeTmpHome();
|
|
111
|
+
const dir = path.join(homeDir, '.claude', 'projects', encodeCwd(cwd));
|
|
112
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
113
|
+
const filePath = path.join(dir, 'sess-2.jsonl');
|
|
114
|
+
fs.writeFileSync(
|
|
115
|
+
filePath,
|
|
116
|
+
JSON.stringify(toolUseLine('Write', path.join(cwd, 'src', 'a.ts'))) + '\n{not json\n',
|
|
117
|
+
);
|
|
118
|
+
expect(countInlineEdits(cwd, 'sess-2', { homeDir })).toBe(1);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('computeEpicDelegationStats combines both counters and never throws on missing state', () => {
|
|
122
|
+
const cwd = makeTmpCwd();
|
|
123
|
+
const homeDir = makeTmpHome();
|
|
124
|
+
expect(computeEpicDelegationStats(cwd, 'ghost-epic', 'ghost-session', { homeDir })).toEqual({
|
|
125
|
+
prdsQueued: 0,
|
|
126
|
+
inlineEdits: 0,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const prdsDir = path.join(cwd, 'session-manager-operations', 'scheduler', 'epics', 'epic-2', 'prds');
|
|
130
|
+
fs.mkdirSync(prdsDir, { recursive: true });
|
|
131
|
+
fs.writeFileSync(path.join(prdsDir, '01-foo.md'), '# foo');
|
|
132
|
+
writeTranscript(homeDir, cwd, 'sess-3', [toolUseLine('Edit', path.join(cwd, 'src', 'x.ts'))]);
|
|
133
|
+
expect(computeEpicDelegationStats(cwd, 'epic-2', 'sess-3', { homeDir })).toEqual({
|
|
134
|
+
prdsQueued: 1,
|
|
135
|
+
inlineEdits: 1,
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -27,6 +27,35 @@ const configMgr = require('../config.cjs');
|
|
|
27
27
|
/** The hardcoded floor every branch of resolveEpicModel falls back to — --model must never be omitted (CLAUDE.md "Automation model pinning"). */
|
|
28
28
|
const FALLBACK_MODEL = 'sonnet';
|
|
29
29
|
|
|
30
|
+
// READ side of the agentType FK (write side: epicMint.cjs's ensureEpic —
|
|
31
|
+
// "throw on write, report on read"). A persona can legitimately be renamed
|
|
32
|
+
// or deleted after an Epic was created against it; that Epic must keep
|
|
33
|
+
// loading (this module's whole contract is "never throws"), but the
|
|
34
|
+
// dangling reference should still be visible somewhere. Logged at most once
|
|
35
|
+
// per (cwd, agentType) so a headless launch that resolves the same Epic's
|
|
36
|
+
// model on every turn doesn't flood opsErrorLog's daily file.
|
|
37
|
+
const loggedDanglingPersonas = new Set();
|
|
38
|
+
|
|
39
|
+
function logDanglingPersonaOnce(cwd, agentType) {
|
|
40
|
+
if (!cwd || !agentType) return;
|
|
41
|
+
const key = `${cwd}::${agentType}`;
|
|
42
|
+
if (loggedDanglingPersonas.has(key)) return;
|
|
43
|
+
loggedDanglingPersonas.add(key);
|
|
44
|
+
try {
|
|
45
|
+
// Required lazily: opsErrorLog.cjs is a leaf module with no Electron
|
|
46
|
+
// dependency, but keeping this require inside the miss path avoids
|
|
47
|
+
// paying for it on the (overwhelmingly common) hit path.
|
|
48
|
+
const { appendError } = require('./opsErrorLog.cjs');
|
|
49
|
+
appendError({
|
|
50
|
+
cwd,
|
|
51
|
+
scope: 'agentModelResolve',
|
|
52
|
+
level: 'warn',
|
|
53
|
+
message: `agentType '${agentType}' has no resolvable persona file — model resolution fell back to '${FALLBACK_MODEL}'`,
|
|
54
|
+
meta: { agentType },
|
|
55
|
+
});
|
|
56
|
+
} catch { /* logging must never break model resolution */ }
|
|
57
|
+
}
|
|
58
|
+
|
|
30
59
|
/**
|
|
31
60
|
* Looks up the agentType an Epic (PromptSession) was created with, by the
|
|
32
61
|
* Epic's own claudeSessionId — the same domain-model join `prdCreate.cjs`'s
|
|
@@ -56,12 +85,14 @@ function readPersonaModel(agentType, deps = {}) {
|
|
|
56
85
|
try {
|
|
57
86
|
real = validatePath(path.join(globalDir, `${agentType}.md`));
|
|
58
87
|
} catch {
|
|
88
|
+
logDanglingPersonaOnce(deps.cwd, agentType);
|
|
59
89
|
return null;
|
|
60
90
|
}
|
|
61
91
|
let text;
|
|
62
92
|
try {
|
|
63
93
|
text = fs.readFileSync(real, 'utf8');
|
|
64
94
|
} catch {
|
|
95
|
+
logDanglingPersonaOnce(deps.cwd, agentType);
|
|
65
96
|
return null;
|
|
66
97
|
}
|
|
67
98
|
const { fm } = splitFrontmatter(text);
|
|
@@ -81,7 +112,7 @@ function resolveEpicModel({ cwd, claudeSessionId, fallbackModel = FALLBACK_MODEL
|
|
|
81
112
|
try {
|
|
82
113
|
const agentType = findAgentTypeByClaudeSessionId(cwd, claudeSessionId, deps);
|
|
83
114
|
if (!agentType) return fallbackModel;
|
|
84
|
-
const model = readPersonaModel(agentType, deps);
|
|
115
|
+
const model = readPersonaModel(agentType, { ...deps, cwd });
|
|
85
116
|
if (!model || model === 'inherit') return fallbackModel;
|
|
86
117
|
return model;
|
|
87
118
|
} catch {
|