claude-code-session-manager 0.68.0 → 0.70.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.
Files changed (46) hide show
  1. package/dist/assets/AgentLibrary-ClxN8f2w.js +3 -0
  2. package/dist/assets/{History-BySQczDf.js → History-EDAc-ZMb.js} +2 -2
  3. package/dist/assets/{Hooks-CG8jWh3-.js → Hooks-aRH7nMJd.js} +3 -3
  4. package/dist/assets/HostBilko-SlCefAAH.js +1 -0
  5. package/dist/assets/{Library-BA8cOckT.js → Library-BCIBTN21.js} +1 -1
  6. package/dist/assets/{ListDetail-CVIhWPWb.js → ListDetail-CJZGbxwd.js} +1 -1
  7. package/dist/assets/{MarkdownEditor-DUoKh1Qr.js → MarkdownEditor-Due9TO9x.js} +1 -1
  8. package/dist/assets/{McpServers-CkowlPR7.js → McpServers-BYKw9iDf.js} +2 -2
  9. package/dist/assets/{Memory-C5Vv5Wim.js → Memory-CIqDISVo.js} +4 -4
  10. package/dist/assets/{Panel-CVgA2-Y3.js → Panel-C6JXCGRu.js} +1 -1
  11. package/dist/assets/{Permissions-u43I9j0-.js → Permissions-C1gnBLDo.js} +3 -3
  12. package/dist/assets/Plugins-BysI_znb.js +2 -0
  13. package/dist/assets/ProvenanceBadge-BxphD65P.js +1 -0
  14. package/dist/assets/{SaveBar-R1ZZ9WH5.js → SaveBar-4DXRUXpo.js} +1 -1
  15. package/dist/assets/{Scheduler-D4Gzz6iD.js → Scheduler-BmbVLNFH.js} +6 -6
  16. package/dist/assets/{ScopeSwitcher-DBcYhNZF.js → ScopeSwitcher-Dlbq0DLs.js} +1 -1
  17. package/dist/assets/Settings-BZtTqlKA.js +3 -0
  18. package/dist/assets/{SkillReferenceGraph-DTfBzZ7O.js → SkillReferenceGraph-CQ-0ZVDl.js} +1 -1
  19. package/dist/assets/Skills-BGBKWxNl.js +3 -0
  20. package/dist/assets/SystemPrompt-Bc715ZOf.js +1 -0
  21. package/dist/assets/TagLibrary-CAnB2jV8.js +1 -0
  22. package/dist/assets/{TiptapBody-DxzA0zb5.js → TiptapBody-CxLrfTQ5.js} +1 -1
  23. package/dist/assets/{Toggle-CPlUEMUg.js → Toggle-CLcPpQX-.js} +1 -1
  24. package/dist/assets/{index-CEnMgeQU.css → index-CFtzZuq7.css} +1 -1
  25. package/dist/assets/{index-Ceid3k5X.js → index-DVVeB50r.js} +356 -355
  26. package/dist/assets/{settingsSchema-CgmSslES.js → settingsSchema-DS2QMkuB.js} +1 -1
  27. package/dist/index.html +2 -2
  28. package/package.json +1 -1
  29. package/plugins/session-manager-dev/.claude-plugin/plugin.json +1 -1
  30. package/plugins/session-manager-dev/skills/send-feedback/SKILL.md +149 -0
  31. package/src/main/__tests__/crossProjectFeedback.test.cjs +327 -0
  32. package/src/main/__tests__/crossProjectFeedbackRoutes.test.cjs +154 -0
  33. package/src/main/index.cjs +5 -0
  34. package/src/main/lib/buildTarget.cjs +15 -0
  35. package/src/main/lib/crossProjectFeedback.cjs +545 -0
  36. package/src/main/lib/epicMint.cjs +38 -7
  37. package/src/main/lib/promptSessionSchema.cjs +7 -1
  38. package/dist/assets/AgentLibrary-D1SHQWCQ.js +0 -3
  39. package/dist/assets/HostBilko-BxjPY1PW.js +0 -1
  40. package/dist/assets/Plugins-DComjLBQ.js +0 -2
  41. package/dist/assets/ProvenanceBadge-DNTnbu6f.js +0 -1
  42. package/dist/assets/Settings-CXcfro2y.js +0 -3
  43. package/dist/assets/Skills-CVlcQjVO.js +0 -3
  44. package/dist/assets/SystemPrompt-COLmPm10.js +0 -1
  45. package/dist/assets/TagLibrary-BN6bvsjU.js +0 -1
  46. package/plugins/session-manager-dev/skills/builder/session-manager-operations/scheduler/state/queue.json +0 -3
@@ -0,0 +1,545 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * crossProjectFeedback.cjs — THE PROJECT-TO-PROJECT CONDUIT.
5
+ *
6
+ * Problem this solves. `/process-feedback`, `/my-feedback` and the
7
+ * `session-manager-operations/feedback/` intake folder were retired
8
+ * (2026-08-02), and the scheduler was correctly locked down so a PRD can only
9
+ * ever JOIN an Epic that a human already approved. Both changes were right on
10
+ * their own, and together they removed the last channel an agent working in
11
+ * Project-A had for handing a finding to Project-B. The finding either died in
12
+ * a transcript or the human relayed it by hand.
13
+ *
14
+ * What this restores, and what it deliberately does NOT restore. Session
15
+ * Manager's main process is the ONE process that legitimately holds several
16
+ * projects' operations roots open at once, so it — and only it — can perform
17
+ * the cross-folder write. A session in Project-A calls `feedback_open_session`
18
+ * (MCP) → this module mints a `proposed` Epic in **Project-B's own**
19
+ * `session-manager-operations/prompt-sessions/active-index.json`, carrying the
20
+ * report as its opening prompt and stamped with where it came from. It appears
21
+ * in Project-B's Sessions queue exactly like a hand-filed proposal.
22
+ *
23
+ * It does NOT re-open the agent-facing proposal channel that was removed. That
24
+ * channel let an agent file work into the project it was ALREADY running in,
25
+ * which is what "run /develop inside the Epic you're already in" replaced and
26
+ * which stays refused — `sameProject` below rejects toCwd === fromCwd outright
27
+ * and names /develop in the error. The only new power is depositing a proposal
28
+ * into a DIFFERENT project's inbox.
29
+ *
30
+ * Why that is safe. The part of the SINGLE-CREATOR LAW that actually guards
31
+ * spend is not "a human typed it" — it's "every Epic is born 'proposed' and
32
+ * nothing runs until a human presses Approve & start". A cross-project
33
+ * feedback Epic is born 'proposed' like every other Epic (epicMint.cjs's
34
+ * BORN-PROPOSED check applies to this mint authority verbatim), so the human
35
+ * in the RECEIVING project is still the one who decides whether a single token
36
+ * gets spent. An agent can ask; it cannot start.
37
+ *
38
+ * Guards, all fail-closed:
39
+ * - both cwds go through config.cjs's validatePath (allowedRoots = home)
40
+ * - both must be real, existing directories
41
+ * - toCwd !== fromCwd (see above)
42
+ * - toCwd must ALREADY be a Session Manager project — its
43
+ * `session-manager-operations/` must exist. This module never conjures an
44
+ * operations root in an arbitrary directory, so it can't be used to
45
+ * scatter app state across the filesystem.
46
+ * - title/body length caps, so one call can't write an unbounded blob into
47
+ * another project's active-index.json.
48
+ *
49
+ * Single-writer law: the actual write is `ensureEpic`'s, which already
50
+ * declares writer 'scheduler' against prompt-sessions/'s narrow
51
+ * active-index.json delegation (opsOwnership.cjs). No new namespace, no new
52
+ * writer, no new delegation.
53
+ */
54
+
55
+ const fs = require('node:fs');
56
+ const fsp = require('node:fs/promises');
57
+ const os = require('node:os');
58
+ const path = require('node:path');
59
+
60
+ const config = require('../config.cjs');
61
+ const { readBody, sendJson } = require('./localAdminHttp.cjs');
62
+ const {
63
+ ensureEpic,
64
+ readActiveIndex,
65
+ MINT_AUTHORITY_CROSS_PROJECT_FEEDBACK,
66
+ } = require('./epicMint.cjs');
67
+ const { appendAuditEvent } = require('./auditLog.cjs');
68
+ const { OPS_ROOT_DIR } = require('./opsOwnership.cjs');
69
+
70
+ /** Only these three Epic tags make sense for an inbound report — they are
71
+ * exactly the tags that route through /develop (tagLibrary.ts). 'build',
72
+ * 'project-home-builder' and 'bilko-host-publisher' name dedicated local
73
+ * pipelines and are meaningless as a request from another project. */
74
+ const FEEDBACK_TAGS = Object.freeze(['bug', 'feature', 'discussion']);
75
+ const DEFAULT_FEEDBACK_TAG = 'discussion';
76
+
77
+ /** Default persona for the receiving session. 'architect' owns scope
78
+ * clarification and decomposition, which is precisely what a receiving human
79
+ * wants pointed at an incoming report. */
80
+ const DEFAULT_FEEDBACK_AGENT = 'architect';
81
+
82
+ const MAX_TITLE_CHARS = 200;
83
+ const MAX_BODY_CHARS = 20000;
84
+ const MAX_REFERENCES = 20;
85
+
86
+ /** Bound on how many `~/.claude/projects/` folders listFeedbackTargets will
87
+ * resolve. That directory accumulates one folder per path the CLI was ever
88
+ * launched from (thousands here — see CLAUDE.md's knownProjectAggregate
89
+ * note), so the scan is capped at the most recently active folders and
90
+ * reports the truncation rather than silently covering a subset. */
91
+ const TARGET_SCAN_LIMIT = 300;
92
+
93
+ const PROJECTS_DIR = path.join(os.homedir(), '.claude', 'projects');
94
+
95
+ /**
96
+ * Where listFeedbackTargets scans for transcript folders. Injectable rather
97
+ * than hardcoded to `os.homedir()` for a reason that bit immediately: the
98
+ * transcripts tests seed and then delete folders under the REAL
99
+ * `~/.claude/projects/` in their own afterEach, so a test that scanned the
100
+ * live directory raced their teardown and failed intermittently under the
101
+ * full suite while passing in isolation. A module whose only I/O root is
102
+ * baked into a constant can't be tested without touching the developer's
103
+ * actual home directory — so the root is a parameter, defaulted.
104
+ */
105
+ function resolveProjectsDir(override) {
106
+ return override || process.env.SM_PROJECTS_DIR || PROJECTS_DIR;
107
+ }
108
+
109
+ function singleLine(s) {
110
+ return String(s ?? '').replace(/\r?\n/g, ' ').trim();
111
+ }
112
+
113
+ function isDirectory(p) {
114
+ try {
115
+ return fs.statSync(p).isDirectory();
116
+ } catch {
117
+ return false;
118
+ }
119
+ }
120
+
121
+ /** True when `cwd` is a Session Manager project — i.e. it already has an
122
+ * operations root. Deliberately the ONLY definition of "can receive
123
+ * feedback": it needs no registry, can't go stale, and refuses to create the
124
+ * folder it is checking for. */
125
+ function isSessionManagerProject(cwd) {
126
+ return isDirectory(path.join(cwd, OPS_ROOT_DIR));
127
+ }
128
+
129
+ /**
130
+ * composeFeedbackIntake({ fromCwd, fromEpicId, title, body, referencePaths })
131
+ * → { goalText, openingPrompt, sections }
132
+ *
133
+ * Pure. Mirrors the renderer's `composeEpicIntake` (lib/epicIntake.ts) —
134
+ * same `EpicIntakeSection` shape, same emission order, same '\n\n' joins — so
135
+ * the receiving Epic's first turn renders the identical AIM briefing card as a
136
+ * hand-created one. It cannot import that module (TS, renderer-side), so it
137
+ * deliberately emits only the section kinds whose text it OWNS:
138
+ *
139
+ * actor — the persona line, same sentence template as composeEpicIntake
140
+ * input — AIM's Input axis: what grounds this session. For an inbound
141
+ * report that is literally "an external report from Project-A",
142
+ * which is this module's own fact to state.
143
+ * goal — the report itself.
144
+ * reference — attached paths.
145
+ *
146
+ * There is NO `mission` section, on purpose. A mission section's text is the
147
+ * tag's `initialPromptTemplate`, which lives in agentTagDefs.ts; re-typing it
148
+ * here would be a second, drifting copy of a tag's meaning — the exact thing
149
+ * CLAUDE.md forbids. The Epic still carries its `tag` field, so every surface
150
+ * that renders a tag's meaning keeps reading it through `agentTagDef(tag)`.
151
+ *
152
+ * Complexity: O(n) in the number of references.
153
+ */
154
+ function composeFeedbackIntake({
155
+ fromCwd,
156
+ fromEpicId = null,
157
+ title,
158
+ body,
159
+ agentName = DEFAULT_FEEDBACK_AGENT,
160
+ agentDescription = null,
161
+ referencePaths = [],
162
+ } = {}) {
163
+ const trimmedTitle = singleLine(title);
164
+ const trimmedBody = String(body ?? '').trim();
165
+ const sections = [];
166
+
167
+ if (agentName && agentDescription) {
168
+ sections.push({
169
+ kind: 'actor',
170
+ label: 'Actor',
171
+ text: `You are acting as the "${singleLine(agentName)}" agent: ${singleLine(agentDescription)}`,
172
+ source: agentName,
173
+ });
174
+ }
175
+
176
+ const origin = fromEpicId
177
+ ? `${singleLine(fromCwd)} (session ${singleLine(fromEpicId)})`
178
+ : singleLine(fromCwd);
179
+ sections.push({
180
+ kind: 'input',
181
+ label: 'Input',
182
+ text:
183
+ `This session is INBOUND FEEDBACK from another project: ${origin}. `
184
+ + 'Nobody in this project wrote the report below — an agent working in that project did, and it '
185
+ + 'may be wrong about this codebase. Verify the claim against the code here before acting on it, '
186
+ + 'and if it does not hold, say so and close the session rather than building against it.',
187
+ source: fromCwd,
188
+ });
189
+
190
+ const goalBody = trimmedTitle ? `Goal: ${trimmedTitle}\n\n${trimmedBody}` : trimmedBody;
191
+ sections.push({ kind: 'goal', label: 'Goal', text: goalBody });
192
+
193
+ for (const p of referencePaths) {
194
+ sections.push({ kind: 'reference', label: 'Reference', text: `Reference: ${singleLine(p)}`, source: p });
195
+ }
196
+
197
+ // Same join rule as epicIntake.ts's joinSections: '\n\n' everywhere except
198
+ // between two consecutive reference lines, which stay one visual block.
199
+ const openingPrompt = sections.reduce((out, s, i) => {
200
+ if (i === 0) return s.text;
201
+ const sep = s.kind === 'reference' && sections[i - 1].kind === 'reference' ? '\n' : '\n\n';
202
+ return out + sep + s.text;
203
+ }, '');
204
+
205
+ const bodyText = trimmedTitle ? `${trimmedTitle}\n\n${trimmedBody}` : trimmedBody;
206
+ const refLines = referencePaths.map((p) => `Reference: ${singleLine(p)}`);
207
+ const goalText = refLines.length ? `${bodyText}\n\n${refLines.join('\n')}` : bodyText;
208
+
209
+ return { goalText, openingPrompt, sections };
210
+ }
211
+
212
+ /**
213
+ * validateFeedbackInput(input) → { ok: true, value } | { ok: false, status, error }
214
+ *
215
+ * Pure apart from filesystem existence checks. Split out from
216
+ * openFeedbackSession so every refusal is unit-testable without minting
217
+ * anything.
218
+ */
219
+ function validateFeedbackInput(input = {}) {
220
+ const bad = (status, error) => ({ ok: false, status, error });
221
+
222
+ const title = singleLine(input.title);
223
+ if (!title) return bad(400, 'title is required');
224
+ if (title.length > MAX_TITLE_CHARS) {
225
+ return bad(400, `title is ${title.length} chars; cap is ${MAX_TITLE_CHARS}`);
226
+ }
227
+
228
+ const body = String(input.body ?? '').trim();
229
+ if (!body) return bad(400, 'body is required — the feedback report itself');
230
+ if (body.length > MAX_BODY_CHARS) {
231
+ return bad(400, `body is ${body.length} chars; cap is ${MAX_BODY_CHARS}. Summarize, and attach detail as a reference path.`);
232
+ }
233
+
234
+ const tag = input.tag ?? DEFAULT_FEEDBACK_TAG;
235
+ if (!FEEDBACK_TAGS.includes(tag)) {
236
+ return bad(400, `tag must be one of ${FEEDBACK_TAGS.join(' | ')} (got ${JSON.stringify(tag)})`);
237
+ }
238
+
239
+ const referencePaths = Array.isArray(input.referencePaths) ? input.referencePaths : [];
240
+ if (referencePaths.length > MAX_REFERENCES) {
241
+ return bad(400, `too many referencePaths (${referencePaths.length}); cap is ${MAX_REFERENCES}`);
242
+ }
243
+
244
+ if (!input.toCwd || typeof input.toCwd !== 'string') return bad(400, 'toCwd is required');
245
+ if (!input.fromCwd || typeof input.fromCwd !== 'string') return bad(400, 'fromCwd is required');
246
+
247
+ let toCwd;
248
+ let fromCwd;
249
+ try {
250
+ toCwd = config.validatePath(path.resolve(input.toCwd));
251
+ } catch (e) {
252
+ return bad(400, `toCwd rejected: ${e.message}`);
253
+ }
254
+ try {
255
+ fromCwd = config.validatePath(path.resolve(input.fromCwd));
256
+ } catch (e) {
257
+ return bad(400, `fromCwd rejected: ${e.message}`);
258
+ }
259
+
260
+ if (!isDirectory(toCwd)) return bad(400, `toCwd is not an existing directory: ${toCwd}`);
261
+ if (!isDirectory(fromCwd)) return bad(400, `fromCwd is not an existing directory: ${fromCwd}`);
262
+
263
+ if (toCwd === fromCwd) {
264
+ return bad(
265
+ 400,
266
+ 'toCwd and fromCwd are the same project — this tool is only for handing a finding to a DIFFERENT '
267
+ + 'project. Work you want done in the project you are already in goes through /develop inside the '
268
+ + 'Epic you are already in, which is what replaced the retired feedback folder.',
269
+ );
270
+ }
271
+
272
+ if (!isSessionManagerProject(toCwd)) {
273
+ return bad(
274
+ 400,
275
+ `${toCwd} is not a Session Manager project — it has no ${OPS_ROOT_DIR}/ directory, so it has no `
276
+ + 'Sessions queue to deliver a proposal into. Open it as a project in Session Manager once (which '
277
+ + 'creates that directory), then send the feedback again.',
278
+ );
279
+ }
280
+
281
+ return {
282
+ ok: true,
283
+ value: {
284
+ toCwd,
285
+ fromCwd,
286
+ title,
287
+ body,
288
+ tag,
289
+ referencePaths: referencePaths.map((p) => String(p)),
290
+ fromEpicId: input.fromEpicId ? String(input.fromEpicId) : null,
291
+ agentType: input.agentType ? String(input.agentType) : DEFAULT_FEEDBACK_AGENT,
292
+ agentDescription: input.agentDescription ? String(input.agentDescription) : null,
293
+ },
294
+ };
295
+ }
296
+
297
+ /**
298
+ * openFeedbackSession(input, deps?) → { ok, status?, error?, epicId, toCwd, ... }
299
+ *
300
+ * Mints the `proposed` Epic in the RECEIVING project and, when the sending
301
+ * Epic is known and still active, chains a receipt onto its event chain so the
302
+ * sender's own transcript records where the finding went. The receipt is
303
+ * best-effort: a failure there never invalidates a delivered proposal, it just
304
+ * reports `receiptOnOriginEpic: false`.
305
+ *
306
+ * `deps` exists so tests can inject the two side-effecting collaborators
307
+ * without an Electron/main-process boot.
308
+ */
309
+ async function openFeedbackSession(input, deps = {}) {
310
+ const mint = deps.ensureEpic || ensureEpic;
311
+ const appendReceipt =
312
+ deps.appendResponseEventIfKnown
313
+ // Required lazily: promptSessionEvents.cjs pulls in config.cjs's watcher
314
+ // machinery, and this module is also required from unit tests that never
315
+ // boot the main process.
316
+ || ((...args) => require('../promptSessionEvents.cjs').appendResponseEventIfKnown(...args));
317
+
318
+ const checked = validateFeedbackInput(input);
319
+ if (!checked.ok) return checked;
320
+ const v = checked.value;
321
+
322
+ // The MCP client can't know its own Epic id — it only inherits the claude
323
+ // session id chatRunner.cjs stamps on the `claude -p` child. Resolve it
324
+ // through prdCreate.cjs's existing resolver rather than a second copy of
325
+ // the same lookup (required lazily: prdCreate pulls in ipcSchemas/config).
326
+ if (!v.fromEpicId && input.originClaudeSessionId) {
327
+ try {
328
+ const { resolveSourcePromptIdFromClaudeSession } = require('./prdCreate.cjs');
329
+ v.fromEpicId = resolveSourcePromptIdFromClaudeSession(v.fromCwd, String(input.originClaudeSessionId)) || null;
330
+ } catch { /* unresolvable — the proposal still delivers, just without a receipt */ }
331
+ }
332
+
333
+ const { goalText, openingPrompt, sections } = composeFeedbackIntake({
334
+ fromCwd: v.fromCwd,
335
+ fromEpicId: v.fromEpicId,
336
+ title: v.title,
337
+ body: v.body,
338
+ agentName: v.agentType,
339
+ agentDescription: v.agentDescription,
340
+ referencePaths: v.referencePaths,
341
+ });
342
+
343
+ // The Epic's stored identity is the one-line title, not the whole report —
344
+ // that's what the receiving queue row shows. The report travels in
345
+ // openingPrompt, which is what Approve & start sends.
346
+ let minted;
347
+ try {
348
+ minted = await mint(v.toCwd, {
349
+ goalText: v.title,
350
+ tag: v.tag,
351
+ agentType: v.agentType,
352
+ status: 'proposed',
353
+ openingPrompt,
354
+ sections,
355
+ source: {
356
+ producer: 'cross-project-feedback',
357
+ fromCwd: v.fromCwd,
358
+ ...(v.fromEpicId ? { fromEpicId: v.fromEpicId } : {}),
359
+ },
360
+ mintAuthority: MINT_AUTHORITY_CROSS_PROJECT_FEEDBACK,
361
+ });
362
+ } catch (e) {
363
+ return { ok: false, status: 500, error: `could not open the feedback session: ${e.message}` };
364
+ }
365
+
366
+ appendAuditEvent('epic_cross_project_feedback', {
367
+ cwd: v.toCwd,
368
+ epicId: minted.epicId,
369
+ fromCwd: v.fromCwd,
370
+ fromEpicId: v.fromEpicId,
371
+ tag: v.tag,
372
+ title: v.title,
373
+ });
374
+
375
+ let receiptOnOriginEpic = false;
376
+ if (v.fromEpicId) {
377
+ try {
378
+ receiptOnOriginEpic = await appendReceipt(
379
+ v.fromCwd,
380
+ v.fromEpicId,
381
+ `Feedback delivered to ${v.toCwd} as a proposed session (${minted.epicId}): "${v.title}". `
382
+ + 'It will not run until a human in that project approves it.',
383
+ );
384
+ } catch {
385
+ receiptOnOriginEpic = false;
386
+ }
387
+ }
388
+
389
+ return {
390
+ ok: true,
391
+ epicId: minted.epicId,
392
+ toCwd: v.toCwd,
393
+ fromCwd: v.fromCwd,
394
+ tag: v.tag,
395
+ status: 'proposed',
396
+ receiptOnOriginEpic,
397
+ // Say plainly what did NOT happen, same posture as create-prd's
398
+ // `enqueued: false` — never let a caller report this as "work started".
399
+ note:
400
+ 'Delivered as a PROPOSED session in the receiving project\'s Sessions queue. Nothing runs and no '
401
+ + 'tokens are spent until a human in that project presses Approve & start. There is no callback: '
402
+ + 'if you need an answer, ask the human to follow up.',
403
+ goalText,
404
+ };
405
+ }
406
+
407
+ /**
408
+ * listFeedbackTargets() → { projects: [{ cwd, lastActivity }], scanned, truncated }
409
+ *
410
+ * Which projects can receive feedback. Resolved the same way CLAUDE.md
411
+ * requires everywhere else: a PROJECT IS A CWD, resolved from transcript
412
+ * CONTENT, never by naively decoding a `~/.claude/projects/` folder name back
413
+ * into a path. A folder whose cwd cannot be resolved is DROPPED, not guessed.
414
+ * Results are then filtered to directories that still exist AND already carry
415
+ * an operations root — exactly `openFeedbackSession`'s own precondition, so
416
+ * this list can't offer a target that would then be refused.
417
+ */
418
+ async function listFeedbackTargets({ limit = TARGET_SCAN_LIMIT, projectsDir } = {}) {
419
+ const root = resolveProjectsDir(projectsDir);
420
+ let entries;
421
+ try {
422
+ entries = await fsp.readdir(root, { withFileTypes: true });
423
+ } catch {
424
+ return { projects: [], scanned: 0, truncated: 0 };
425
+ }
426
+ const dirs = entries.filter((e) => e.isDirectory());
427
+
428
+ // Newest-first by folder mtime, so the cap keeps the projects actually in
429
+ // use rather than an arbitrary alphabetical slice.
430
+ const stamped = [];
431
+ for (const d of dirs) {
432
+ const full = path.join(root, d.name);
433
+ try {
434
+ stamped.push({ full, mtimeMs: (await fsp.stat(full)).mtimeMs });
435
+ } catch { /* raced away */ }
436
+ }
437
+ stamped.sort((a, b) => b.mtimeMs - a.mtimeMs);
438
+ const scanned = stamped.slice(0, limit);
439
+
440
+ const byCwd = new Map();
441
+ for (const { full, mtimeMs } of scanned) {
442
+ const cwd = await resolveCwdFromTranscriptDir(full);
443
+ if (!cwd) continue;
444
+ const prior = byCwd.get(cwd);
445
+ if (!prior || prior.lastActivity < mtimeMs) byCwd.set(cwd, { cwd, lastActivity: mtimeMs });
446
+ }
447
+
448
+ const projects = [...byCwd.values()]
449
+ .filter((p) => isDirectory(p.cwd) && isSessionManagerProject(p.cwd))
450
+ .sort((a, b) => b.lastActivity - a.lastActivity)
451
+ .map((p) => ({ cwd: p.cwd, lastActivity: new Date(p.lastActivity).toISOString() }));
452
+
453
+ return { projects, scanned: scanned.length, truncated: Math.max(0, stamped.length - scanned.length) };
454
+ }
455
+
456
+ /** Read the `cwd` field off the first JSON line of the newest .jsonl in a
457
+ * `~/.claude/projects/<encoded>/` folder. Returns null when the folder holds
458
+ * no transcript or no line carries a cwd — the caller drops it rather than
459
+ * falling back to decoding the folder name. */
460
+ async function resolveCwdFromTranscriptDir(dir) {
461
+ let files;
462
+ try {
463
+ files = (await fsp.readdir(dir)).filter((f) => f.endsWith('.jsonl'));
464
+ } catch {
465
+ return null;
466
+ }
467
+ if (!files.length) return null;
468
+ const stamped = [];
469
+ for (const f of files) {
470
+ try {
471
+ stamped.push({ f, mtimeMs: (await fsp.stat(path.join(dir, f))).mtimeMs });
472
+ } catch { /* raced away */ }
473
+ }
474
+ stamped.sort((a, b) => b.mtimeMs - a.mtimeMs);
475
+ for (const { f } of stamped.slice(0, 3)) {
476
+ try {
477
+ // Transcripts can be large; only the head is needed — the cwd is
478
+ // stamped on the very first records.
479
+ const handle = await fsp.open(path.join(dir, f), 'r');
480
+ try {
481
+ const buf = Buffer.alloc(64 * 1024);
482
+ const { bytesRead } = await handle.read(buf, 0, buf.length, 0);
483
+ const head = buf.subarray(0, bytesRead).toString('utf8');
484
+ for (const line of head.split('\n')) {
485
+ if (!line.trim()) continue;
486
+ try {
487
+ const parsed = JSON.parse(line);
488
+ if (parsed && typeof parsed.cwd === 'string' && parsed.cwd) return path.resolve(parsed.cwd);
489
+ } catch { /* partial trailing line */ }
490
+ }
491
+ } finally {
492
+ await handle.close();
493
+ }
494
+ } catch { /* unreadable */ }
495
+ }
496
+ return null;
497
+ }
498
+
499
+ /** Registers both routes on the injected loopback admin transport. Mirrors
500
+ * prdCreate.cjs's registerAdminRoute — auth is the transport's job. */
501
+ function registerAdminRoute(adminHttp) {
502
+ adminHttp.registerRoute('POST', '/admin/feedback/open-session', async (req, res) => {
503
+ const raw = await readBody(req);
504
+ let parsed;
505
+ try {
506
+ parsed = raw ? JSON.parse(raw) : {};
507
+ } catch {
508
+ sendJson(res, 400, { ok: false, error: 'invalid JSON body' });
509
+ return;
510
+ }
511
+ const result = await openFeedbackSession(parsed);
512
+ if (!result.ok) {
513
+ sendJson(res, result.status ?? 400, { ok: false, error: result.error });
514
+ return;
515
+ }
516
+ sendJson(res, 200, result);
517
+ });
518
+
519
+ adminHttp.registerRoute('GET', '/admin/feedback/targets', async (req, res, query) => {
520
+ // `projectsDir` is a test seam only — production callers never pass it,
521
+ // and it can only narrow WHICH transcript folders are scanned, never
522
+ // where a feedback session may be written (that stays governed by
523
+ // openFeedbackSession's own validatePath + ops-root checks).
524
+ const projectsDir = query?.get('projectsDir') || undefined;
525
+ const result = await listFeedbackTargets({ projectsDir });
526
+ sendJson(res, 200, { ok: true, ...result });
527
+ });
528
+ }
529
+
530
+ module.exports = {
531
+ FEEDBACK_TAGS,
532
+ DEFAULT_FEEDBACK_TAG,
533
+ DEFAULT_FEEDBACK_AGENT,
534
+ MAX_TITLE_CHARS,
535
+ MAX_BODY_CHARS,
536
+ MAX_REFERENCES,
537
+ PROJECTS_DIR,
538
+ resolveProjectsDir,
539
+ isSessionManagerProject,
540
+ composeFeedbackIntake,
541
+ validateFeedbackInput,
542
+ openFeedbackSession,
543
+ listFeedbackTargets,
544
+ registerAdminRoute,
545
+ };
@@ -45,6 +45,33 @@ const promptSessionSchema = require('./promptSessionSchema.cjs');
45
45
  // deliberate domain-model change, not a convenience.
46
46
  const MINT_AUTHORITY_NEW_EPIC_UI = 'new-epic-ui';
47
47
 
48
+ // The SECOND (and only other) mint authority — a deliberate domain-model
49
+ // change, argued for here rather than discovered later.
50
+ //
51
+ // PROJECT-TO-PROJECT FEEDBACK. With `/process-feedback`, `/my-feedback` and
52
+ // the `feedback/` folder retired (2026-08-02) and the scheduler correctly
53
+ // locked down to join-only, an agent working in Project-A had NO way to hand
54
+ // a finding to Project-B — the finding either died in a transcript or the
55
+ // human had to relay it by hand. `lib/crossProjectFeedback.cjs` restores that
56
+ // conduit, and it is the sole holder of this token.
57
+ //
58
+ // What makes it safe is the part of the SINGLE-CREATOR LAW that actually
59
+ // guards spend: every Epic is born 'proposed', and NOTHING runs until a human
60
+ // in the RECEIVING project presses "Approve & start". This authority cannot
61
+ // change that — the BORN-PROPOSED check below still applies to it verbatim,
62
+ // and `crossProjectFeedback.cjs` additionally pins tag/source before minting.
63
+ // So this is not a re-opening of the agent-facing proposal channel that was
64
+ // removed: an agent may deposit a proposal into another project's inbox, and
65
+ // that is all it may do. The removed channel let an agent file work into the
66
+ // project it was already running in, which is what /develop-inside-your-own-
67
+ // Epic replaced and which stays refused.
68
+ const MINT_AUTHORITY_CROSS_PROJECT_FEEDBACK = 'cross-project-feedback';
69
+
70
+ const MINT_AUTHORITIES = Object.freeze([
71
+ MINT_AUTHORITY_NEW_EPIC_UI,
72
+ MINT_AUTHORITY_CROSS_PROJECT_FEEDBACK,
73
+ ]);
74
+
48
75
  function activeIndexPath(cwd) {
49
76
  return path.join(cwd, 'session-manager-operations', 'prompt-sessions', 'active-index.json');
50
77
  }
@@ -175,13 +202,15 @@ function ensureEpic(cwd, { goalText, tag, epicId: explicitEpicId, status = 'prop
175
202
  });
176
203
  }
177
204
 
178
- // SINGLE-CREATOR LAW (see this file's header). Only the New Epic UI may
179
- // mint; every other caller reaching this line was trying to JOIN an Epic
180
- // that isn't there, which is a bug in the caller, not a cue to create one.
181
- if (mintAuthority !== MINT_AUTHORITY_NEW_EPIC_UI) {
182
- const reason = 'no open Epic to join — an Epic is created in exactly one place, the New Epic UI '
183
- + '(mintAuthority). Agents that are sure the work is needed run /develop inside the Epic they are '
184
- + 'already in; a PRD-authoring path may never conjure one';
205
+ // SINGLE-CREATOR LAW (see this file's header). Only the New Epic UI —
206
+ // or, for an inbound cross-project feedback proposal, lib/
207
+ // crossProjectFeedback.cjs — may mint; every other caller reaching this
208
+ // line was trying to JOIN an Epic that isn't there, which is a bug in the
209
+ // caller, not a cue to create one.
210
+ if (!MINT_AUTHORITIES.includes(mintAuthority)) {
211
+ const reason = 'no open Epic to join — an Epic is created in exactly two places, the New Epic UI and an '
212
+ + 'inbound cross-project feedback proposal (mintAuthority). Agents that are sure the work is needed run '
213
+ + '/develop inside the Epic they are already in; a PRD-authoring path may never conjure one';
185
214
  appendAuditEvent('epic_mint_refused', { cwd, epicId: explicitEpicId ?? null, status, reason });
186
215
  throw new Error(`ensureEpic: ${reason} (epicId=${explicitEpicId ?? 'none'})`);
187
216
  }
@@ -323,6 +352,8 @@ function removeEpic(cwd, epicId) {
323
352
  module.exports = {
324
353
  ensureEpic,
325
354
  MINT_AUTHORITY_NEW_EPIC_UI,
355
+ MINT_AUTHORITY_CROSS_PROJECT_FEEDBACK,
356
+ MINT_AUTHORITIES,
326
357
  appendPrdCreatedEvent,
327
358
  removeEpic,
328
359
  activeIndexPath,
@@ -32,10 +32,16 @@ const EpicTagSchema = z.enum([
32
32
 
33
33
  // Mirrors EpicSource (src/renderer/state/promptSessions.ts:22-27).
34
34
  const EpicSourceSchema = z.object({
35
- producer: z.enum(['new-epic-ui', 'scheduler-dispatch']),
35
+ producer: z.enum(['new-epic-ui', 'scheduler-dispatch', 'cross-project-feedback']),
36
36
  prdSlug: z.string().optional(),
37
37
  runId: z.string().optional(),
38
38
  sourceTabId: z.string().optional(),
39
+ // Cross-project provenance ('cross-project-feedback' only): which project,
40
+ // and which Epic inside it, sent this proposal. Kept on the Epic itself so
41
+ // the receiving human can see who is asking without a second lookup table —
42
+ // there is no cross-project registry anywhere else.
43
+ fromCwd: z.string().optional(),
44
+ fromEpicId: z.string().optional(),
39
45
  });
40
46
 
41
47
  // Mirrors EpicIntakeSection (src/renderer/lib/epicIntake.ts) — the labeled
@@ -1,3 +0,0 @@
1
- import{K as e,p as U,Y as m,d as B,b as D,a5 as b,a2 as V,T as X,a4 as Z,ab as ee,A as R}from"./index-Ceid3k5X.js";import{P as te}from"./Panel-CVgA2-Y3.js";import{L as ne}from"./ListDetail-CVIhWPWb.js";import"./monaco-editor-BW5C4Iv1.js";function y({children:t,onClick:o,disabled:x,variant:g="default",tooltip:i,type:f="button",className:u}){const v=`px-2 py-0.5 text-xs rounded border transition-colors disabled:opacity-40 disabled:cursor-not-allowed ${g==="primary"?"text-accent border-accent/50 hover:bg-bg-hi":g==="danger"?"text-red-400 hover:text-red-300 border-line hover:border-red-400/50":"text-fg-dim border-line hover:text-fg hover:bg-bg-hi"} ${u??""}`,a=e.jsx("button",{type:f,title:i,onClick:o,disabled:x,className:v,children:t});return i?e.jsx(U,{content:i,align:"bottom-center",children:a}):a}const ae=["inherit","haiku","sonnet","opus"],I=["Read","Grep","Glob","Bash","Write","Edit","WebFetch","WebSearch","Task"],se=["","red","orange","yellow","green","cyan","blue","purple","pink"],G={red:"#c0392b",orange:"#b85c34",yellow:"#c9a227",green:"#6f7d52",cyan:"#3f8f8a",blue:"#4a6fa5",purple:"#7a6a8a",pink:"#b45a80"};function T(t){return{name:t.name,description:t.description??"",tools:t.tools,model:t.model??"inherit",color:t.color??"",tags:t.tags,projects:t.projects??[],action:t.action??"",actionLabel:t.actionLabel??"",body:t.body}}const z={name:"new-agent",description:"",tools:["Read","Grep"],model:"inherit",color:"",tags:[],projects:[],action:"",actionLabel:"",body:""};function re(){const[t,o]=m.useState(null),[x,g]=m.useState(null),[i,f]=m.useState(""),[u,c]=m.useState(null),[v,a]=m.useState(!1),[j,N]=m.useState(!1),[O,C]=m.useState(null),[k,s]=m.useState(!1),w=m.useRef(!0);m.useEffect(()=>()=>{w.current=!1},[]);const p=async n=>{try{const d=await window.api.agents.listPersonas();if(!w.current)return;o(d),g(L=>{var P;return n??L??((P=d[0])==null?void 0:P.name)??null})}catch(d){if(!w.current)return;o([]),b.error(d.message||"failed to load agent personas")}};if(m.useEffect(()=>{p()},[]),m.useEffect(()=>window.api.agents.onChanged(()=>p()),[]),m.useEffect(()=>{const n=V();n&&g(n);const d=L=>g(L.detail);return window.addEventListener("sm:select-persona",d),()=>window.removeEventListener("sm:select-persona",d)},[]),t===null)return e.jsx(B,{title:"loading…"});const $=i.toLowerCase(),E=t.filter(n=>!$||n.name.toLowerCase().includes($)||(n.description??"").toLowerCase().includes($)),l=t.find(n=>n.name===x)??t[0]??null,r=u??(l?T(l):null),F=!!u&&!!l&&JSON.stringify(u)!==JSON.stringify(T(l)),S=v,K=n=>{c(null),a(!1),N(!1),C(null),g(n)},M=n=>c(d=>({...d??(l?T(l):z),...n})),W=()=>{c({...z}),a(!0),N(!1)},_=()=>{r&&(c({...r,name:r.name+"-copy"}),a(!0))},J=()=>{c(null),a(!1)},Y=async()=>{if(r){if(!/^[a-z][a-z0-9-]*$/.test(r.name)){b.error('agent name must be lowercase, hyphenated (e.g. "my-agent")');return}s(!0);try{const n={name:r.name,originalName:S||l==null?void 0:l.name,description:r.description,tools:r.tools,model:r.model,color:r.color,tags:r.tags,projects:r.projects,action:r.action,actionLabel:r.actionLabel,body:r.body};await window.api.agents.savePersona(n),b.info(S?`created ${r.name}`:`saved ${r.name}`),c(null),a(!1),await p(r.name)}catch(n){b.error(n.message||"failed to save agent persona")}finally{s(!1)}}},H=async()=>{if(l){s(!0);try{await window.api.agents.deletePersona({name:l.name}),b.info(`deleted ${l.name}`),N(!1),c(null),a(!1),await p()}catch(n){b.error(n.message||"failed to delete agent persona")}finally{s(!1)}}},q=async n=>{s(!0);try{await window.api.agents.deletePersona({name:n}),b.info(`deleted ${n}`),C(null),x===n&&(c(null),a(!1)),await p()}catch(d){b.error(d.message||"failed to delete agent persona")}finally{s(!1)}},Q=async n=>{if(l)try{await window.api.agents.removeOverride({name:l.name,projectName:n}),b.info(`removed ${n}'s override of ${l.name}`),await p(l.name)}catch(d){b.error(d.message||"failed to remove override")}};return e.jsx(te,{toolbar:e.jsxs(e.Fragment,{children:[e.jsxs("span",{className:"text-fg-faint",children:[t.length," persona",t.length===1?"":"s"]}),e.jsx("input",{value:i,onChange:n=>f(n.target.value),placeholder:"filter…",className:"bg-bg border border-line rounded px-2 py-0.5 text-xs text-fg w-40"}),e.jsx("div",{className:"flex-1"}),e.jsx(y,{variant:"primary",onClick:W,children:"+ New agent"})]}),children:e.jsx(ne,{sidebar:e.jsxs("div",{className:"py-1",children:[S&&e.jsx("div",{className:"w-full text-left px-3 py-1 text-xs flex flex-col gap-0.5 bg-bg-hi text-fg border-l-2 border-accent",children:e.jsxs("span",{className:"flex items-center gap-2 justify-between w-full",children:[e.jsx("span",{className:"truncate font-mono",children:r==null?void 0:r.name}),e.jsx("span",{className:"shrink-0 text-[10px] font-semibold text-accent",children:"new"})]})}),E.length===0&&!S?e.jsx("div",{className:"px-3 py-1 text-xs text-fg-faint italic",children:"no agent personas found"}):E.map(n=>{const d=!S&&n.name===x,L=d&&F,P=O===n.name;return e.jsxs("div",{"data-agent-row":n.name,className:`group w-full text-left px-3 py-1 text-xs flex flex-col gap-0.5 border-l-2 cursor-pointer ${d?"bg-bg-hi text-fg border-accent":"text-fg-dim hover:text-fg hover:bg-bg-hi border-transparent"}`,onClick:()=>K(n.name),children:[e.jsxs("span",{className:"flex items-center gap-2 justify-between w-full",children:[e.jsxs("span",{className:"flex items-center gap-1.5 min-w-0",children:[e.jsx("span",{className:"w-2 h-2 rounded-sm shrink-0",style:{background:n.color?G[n.color]??n.color:"var(--fg-faint, #999)",opacity:n.color?1:.35}}),e.jsx("span",{className:"truncate font-mono",children:n.name})]}),e.jsxs("span",{className:"shrink-0 flex items-center gap-1.5",children:[L&&e.jsx("span",{className:"text-[10px] font-semibold text-accent",children:"unsaved"}),!L&&n.overridingProjects.length>0&&e.jsxs(D,{tone:"dim",children:[n.overridingProjects.length," override",n.overridingProjects.length>1?"s":""]}),P?e.jsxs("span",{className:"flex items-center gap-1",onClick:A=>A.stopPropagation(),children:[e.jsx("button",{onClick:()=>q(n.name),disabled:k,title:`Delete ${n.name}`,className:"text-[10px] font-semibold text-red-400 hover:text-red-300",children:"confirm"}),e.jsx("button",{onClick:()=>C(null),title:"Cancel",className:"text-[10px] text-fg-faint hover:text-fg",children:"cancel"})]}):e.jsx("button",{onClick:A=>{A.stopPropagation(),C(n.name)},title:`Delete ${n.name}`,className:"opacity-0 group-hover:opacity-100 text-fg-faint hover:text-red-400 transition-opacity",children:"×"})]})]}),n.description&&e.jsx("span",{className:"truncate text-fg-faint",children:n.description})]},n.name)})]}),detail:r?e.jsx(oe,{obj:r,set:M,saved:l,dirty:F,isNew:S,busy:k,confirmDelete:j,setConfirmDelete:N,onDuplicate:_,onRevert:J,onSave:Y,onDelete:H,onDropOverride:Q}):e.jsx(B,{title:"select an agent"})})})}const fe=m.memo(re);function oe({obj:t,set:o,saved:x,dirty:g,isNew:i,busy:f,confirmDelete:u,setConfirmDelete:c,onDuplicate:v,onRevert:a,onSave:j,onDelete:N,onDropOverride:O}){const C=`~/.claude/agents/${t.name}.md`,k=g||i;return e.jsxs("div",{className:"max-w-3xl",children:[e.jsxs("div",{className:"px-4 py-3 border-b border-line flex items-center gap-2 flex-wrap",children:[e.jsx("span",{className:"text-sm font-semibold text-fg font-mono",children:t.name}),i?e.jsx(D,{tone:"accent",children:"unsaved draft"}):g?e.jsx(D,{tone:"warn",children:"edited"}):e.jsx(D,{tone:"good",children:"saved"}),e.jsx("span",{className:"ml-auto text-[11px] text-fg-faint font-mono",children:C})]}),e.jsxs("div",{className:"p-4 space-y-4",children:[e.jsx(h,{label:"name",hint:"Filename on disk — lowercase, hyphenated.",children:e.jsx("input",{value:t.name,onChange:s=>o({name:s.target.value.toLowerCase()}),className:"w-full bg-bg border border-line rounded px-2 py-1 text-xs text-fg font-mono"})}),e.jsx(h,{label:"description",hint:"Shown in the list and in agent pickers.",children:e.jsx("input",{value:t.description,onChange:s=>o({description:s.target.value}),className:"w-full bg-bg border border-line rounded px-2 py-1 text-xs text-fg"})}),e.jsx(h,{label:"default model",hint:"This persona's default model for its Epics — sets the --model an Epic launches with (Terminal + Chat both honor it). 'inherit' falls back to each view's own default, not this Settings page. Cheap models for read-only personas; opus for judgement calls.",children:e.jsx(le,{options:ae,value:t.model,onChange:s=>o({model:s}),mono:!0})}),e.jsx(h,{label:"tools",hint:`${t.tools.length} of ${I.length} granted. Anything not ticked is refused at call time.`,children:e.jsx("div",{className:"flex flex-wrap gap-1.5",children:I.map(s=>{const w=t.tools.includes(s);return e.jsx("button",{onClick:()=>o({tools:w?t.tools.filter(p=>p!==s):[...t.tools,s]}),className:`px-2 py-0.5 rounded-full text-[11px] font-mono border ${w?"bg-sage/15 text-sage-dark border-sage/30":"bg-bg-hi text-fg-faint border-line"}`,children:s},s)})})}),e.jsx(h,{label:"color",children:e.jsx("div",{className:"flex gap-1.5",children:se.map(s=>e.jsx("button",{title:s||"none",onClick:()=>o({color:s}),className:"w-5 h-5 rounded",style:{background:s?G[s]:"transparent",border:t.color===s?"2px solid var(--accent, #b85c34)":"1px solid var(--line, #555)"}},s||"none"))})}),e.jsx(h,{label:"tags",hint:"Session mission tags this persona is associated with — shown as the default agent on that tag's own page. The first one (in Tag Library order) is the MISSION an Action button's session opens with.",children:e.jsx("div",{className:"flex flex-wrap gap-1.5",children:X.map(s=>{const w=t.tags.includes(s.tag),p=Z(s.tag);return e.jsxs("button",{onClick:()=>o({tags:w?t.tags.filter($=>$!==s.tag):[...t.tags,s.tag]}),className:`px-2 py-0.5 rounded-full text-[11px] font-mono border ${w?`${p.bg} ${p.text} ${p.border?"border-current/30":"border-transparent"}`:"bg-bg-hi text-fg-faint border-line"}`,children:["#",s.tag]},s.tag)})})}),e.jsx(ie,{obj:t,set:o}),e.jsx(h,{label:"definition",hint:"The system prompt this persona runs under.",children:e.jsx("textarea",{rows:9,value:t.body,onChange:s=>o({body:s.target.value}),className:"w-full bg-bg border border-line rounded px-2 py-1.5 text-xs text-fg font-mono leading-relaxed resize-y"})}),e.jsx(h,{label:"project overrides",hint:"Open projects that shadow this definition with a local copy.",children:x&&x.overridingProjects.length>0?e.jsx("div",{className:"flex flex-wrap gap-1.5",children:x.overridingProjects.map(s=>e.jsxs("span",{className:"inline-flex items-center gap-1.5 bg-bg-elev border border-line rounded px-2 py-0.5 text-[11px] font-mono text-fg-dim",children:[s,e.jsx("button",{onClick:()=>O(s),title:"Drop the local copy",className:"text-fg-faint hover:text-fg",children:"×"})]},s))}):e.jsx("div",{className:"text-xs text-fg-faint italic",children:i?"save first to see project overrides.":"no open project overlays this agent — the global definition applies everywhere."})})]}),e.jsx("div",{className:"px-4 py-2.5 border-t border-line flex items-center gap-2 flex-wrap",children:u?e.jsxs(e.Fragment,{children:[e.jsxs("span",{className:"text-xs text-fg-dim",children:["Delete ",e.jsx("strong",{className:"font-mono",children:t.name})," and its file?"]}),e.jsx(y,{variant:"danger",onClick:N,disabled:f,children:"Delete"}),e.jsx(y,{onClick:()=>c(!1),children:"Keep"})]}):e.jsxs(e.Fragment,{children:[e.jsx(y,{onClick:v,disabled:f,children:"Duplicate"}),!i&&e.jsx(y,{variant:"danger",onClick:()=>c(!0),disabled:f,children:"Delete"}),e.jsx("div",{className:"flex-1"}),k&&e.jsx("span",{className:"text-[11px] text-fg-faint",children:"unsaved changes"}),e.jsx(y,{onClick:a,disabled:!k||f,children:"Revert"}),e.jsx(y,{variant:"primary",onClick:j,disabled:!k||f,children:f?"Saving…":i?"Create":"Save"})]})})]})}function ie({obj:t,set:o}){const{projects:x}=ee(),g=x.map(a=>({cwd:a.cwd,name:a.name})),i=new Set(g.map(a=>a.cwd)),f=t.projects.filter(a=>a!==R&&!i.has(a)),u=t.projects.includes(R),c=a=>o({projects:t.projects.includes(a)?t.projects.filter(j=>j!==a):[...t.projects,a]}),v=t.projects.length>0;return e.jsxs(e.Fragment,{children:[e.jsx(h,{label:"action — appears in",hint:"Projects whose Sessions toolbar gets a one-click button for this agent. Pressing it starts a normal session: this persona is the ACTOR, its first tag is the MISSION, and the action text below is the opening goal. Leave empty for an agent that is only picked by hand in the New Session card.",children:e.jsxs("div",{className:"flex flex-wrap gap-1.5",children:[e.jsx("button",{onClick:()=>c(R),className:`px-2 py-0.5 rounded-full text-[11px] font-mono border ${u?"bg-accent/15 text-accent border-accent/40 font-semibold":"bg-bg-hi text-fg-faint border-line"}`,children:"every project"}),g.map(a=>{const j=t.projects.includes(a.cwd);return e.jsx("button",{title:a.cwd,onClick:()=>c(a.cwd),className:`px-2 py-0.5 rounded-full text-[11px] font-mono border ${j?"bg-sage/15 text-sage-dark border-sage/30":"bg-bg-hi text-fg-faint border-line"} ${u&&!j?"opacity-40":""}`,children:a.name},a.cwd)}),f.map(a=>e.jsxs("button",{title:`${a} — not a known project on this machine`,onClick:()=>c(a),className:"px-2 py-0.5 rounded-full text-[11px] font-mono border bg-bg-hi text-fg-faint border-dashed border-line",children:[a," ×"]},a))]})}),v&&e.jsxs(e.Fragment,{children:[e.jsx(h,{label:"action — button label",hint:"Caption on the Sessions toolbar button. Defaults to the agent name.",children:e.jsx("input",{value:t.actionLabel,onChange:a=>o({actionLabel:a.target.value}),placeholder:t.name,className:"w-full bg-bg border border-line rounded px-2 py-1 text-xs text-fg"})}),e.jsx(h,{label:"action — opening instruction",hint:"Sent as the session's first message, under the AIM framing. Falls back to this persona's description when empty; with neither, no button is shown.",children:e.jsx("textarea",{rows:3,value:t.action,onChange:a=>o({action:a.target.value}),placeholder:t.description||`e.g. /builder
2
-
3
- Check git vs the published package and publish anything new.`,className:"w-full bg-bg border border-line rounded px-2 py-1.5 text-xs text-fg font-mono leading-relaxed resize-y"})})]})]})}function h({label:t,hint:o,children:x}){return e.jsxs("div",{children:[e.jsx("div",{className:"text-[10.5px] font-semibold uppercase tracking-wide text-fg-faint mb-1.5 font-mono",children:t}),x,o&&e.jsx("div",{className:"text-[11px] text-fg-faint mt-1 leading-snug",children:o})]})}function le({options:t,value:o,onChange:x,mono:g}){return e.jsx("div",{className:"flex flex-wrap gap-1",children:t.map(i=>{const f=i===o;return e.jsx("button",{onClick:()=>x(i),className:`px-2.5 py-1 rounded text-xs border ${g?"font-mono":""} ${f?"bg-accent/15 text-accent border-accent/40 font-semibold":"bg-bg-hi text-fg-dim border-line"}`,children:i},i)})})}export{fe as AgentLibrary};