claude-code-session-manager 0.85.0 → 0.87.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 (147) hide show
  1. package/dist/assets/AgentLibrary-DyLWzZDf.js +3 -0
  2. package/dist/assets/{DataModel-DRH-Ty20.js → DataModel--mISIJ6h.js} +1 -1
  3. package/dist/assets/{History-CfRhT1Im.js → History-C2ahUXTg.js} +2 -2
  4. package/dist/assets/{Hooks-wEmh_U6c.js → Hooks-BiC6oyR2.js} +3 -3
  5. package/dist/assets/{HostBilko-D_t7Rbi7.js → HostBilko-BPleEOld.js} +1 -1
  6. package/dist/assets/{Library-CpArQ-OJ.js → Library-Dc8Qst1R.js} +1 -1
  7. package/dist/assets/{ListDetail-pjaKYs84.js → ListDetail-DIXh-OLX.js} +1 -1
  8. package/dist/assets/MarkdownEditor-C90bkLXK.js +1 -0
  9. package/dist/assets/{McpServers-ftqaV3kn.js → McpServers-DqcbLOLZ.js} +2 -2
  10. package/dist/assets/{Memory-ChMWkNd0.js → Memory-CW62MXlh.js} +4 -4
  11. package/dist/assets/{Panel-D9Kr40Ai.js → Panel-Bw1FhRuF.js} +1 -1
  12. package/dist/assets/Permissions-BcUC-5y8.js +3 -0
  13. package/dist/assets/{Plugins-BtChISho.js → Plugins-BnKx9flD.js} +2 -2
  14. package/dist/assets/{ProvenanceBadge-DBA5EcYy.js → ProvenanceBadge-Bw5vNVPT.js} +1 -1
  15. package/dist/assets/{SaveBar-I0_dWNTX.js → SaveBar-CWr0O_w-.js} +1 -1
  16. package/dist/assets/Scheduler-DYdLuUqq.js +14 -0
  17. package/dist/assets/{ScopeSwitcher-5GTEveb2.js → ScopeSwitcher-CrBLbg8s.js} +1 -1
  18. package/dist/assets/Settings-DluB-vN1.js +3 -0
  19. package/dist/assets/{SkillReferenceGraph-DNBFGrYE.js → SkillReferenceGraph-CHLSseay.js} +1 -1
  20. package/dist/assets/{Skills-DJB6-bBM.js → Skills-gNdo_HNK.js} +2 -2
  21. package/dist/assets/{SystemPrompt-BiDDrJUA.js → SystemPrompt-Cru05-Ia.js} +1 -1
  22. package/dist/assets/{TagLibrary-_Wrevtop.js → TagLibrary-DNHY0xou.js} +1 -1
  23. package/dist/assets/{TiptapBody-OWWXdLRy.js → TiptapBody-I4lmbCgP.js} +1 -1
  24. package/dist/assets/{Toggle-B122N0HL.js → Toggle-bWMHjmRh.js} +1 -1
  25. package/dist/assets/{index-CDo9xBR9.css → index-DV3PorRY.css} +1 -1
  26. package/dist/assets/{index-DhvuQL4C.js → index-fc_JjdxL.js} +724 -724
  27. package/dist/assets/settingsSchema-BfhtZnGD.js +3 -0
  28. package/dist/index.html +2 -2
  29. package/package.json +15 -14
  30. package/plugins/CLAUDE.md +61 -0
  31. package/plugins/session-manager-dev/.claude-plugin/plugin.json +1 -1
  32. package/plugins/session-manager-dev/skills/builder/4-manual/SKILL.md +1 -1
  33. package/plugins/session-manager-dev/skills/ops-sweep/SKILL.md +1 -1
  34. package/scripts/scheduler-mcp-server.cjs +7 -0
  35. package/src/main/__tests__/agentModelResolve.test.cjs +100 -9
  36. package/src/main/__tests__/broadcastCoalescer.test.cjs +18 -0
  37. package/src/main/__tests__/epicMint.test.cjs +2 -2
  38. package/src/main/__tests__/health-delegation-chain.test.cjs +2 -1
  39. package/src/main/__tests__/health-starve-escalation.test.cjs +94 -0
  40. package/src/main/__tests__/loadGateDetailTick.test.cjs +31 -0
  41. package/src/main/__tests__/machineProfile.test.cjs +19 -1
  42. package/src/main/__tests__/needsReviewLedger.test.cjs +162 -0
  43. package/src/main/__tests__/opsErrorLogTelemetryTap.test.cjs +3 -3
  44. package/src/main/__tests__/pollLoop-dispatch-on-failure.test.cjs +15 -1
  45. package/src/main/__tests__/prdCreateDisposition.test.cjs +201 -0
  46. package/src/main/__tests__/prdFrontmatterDisposition.test.cjs +125 -0
  47. package/src/main/__tests__/prdLocations.test.cjs +100 -2
  48. package/src/main/__tests__/prdLocationsArchived.test.cjs +43 -1
  49. package/src/main/__tests__/prdSetDisposition.test.cjs +222 -0
  50. package/src/main/__tests__/pty-session-open-telemetry.test.cjs +96 -0
  51. package/src/main/__tests__/queue-health-verdict.test.cjs +170 -0
  52. package/src/main/__tests__/queue-starvation-per-project.test.cjs +147 -0
  53. package/src/main/__tests__/queueHistory.test.cjs +63 -0
  54. package/src/main/__tests__/reconcileTiming.test.cjs +135 -0
  55. package/src/main/__tests__/scheduleJobTransitions.test.cjs +101 -1
  56. package/src/main/__tests__/scheduler-boot-orphans.test.cjs +2 -2
  57. package/src/main/__tests__/scheduler-broadcast-reconcile.test.cjs +121 -0
  58. package/src/main/__tests__/scheduler-cross-project-batch.test.cjs +43 -0
  59. package/src/main/__tests__/scheduler-failed-autoreset.test.cjs +121 -0
  60. package/src/main/__tests__/scheduler-guard-verdict-autoresolve.test.cjs +344 -0
  61. package/src/main/__tests__/scheduler-job-budget.test.cjs +172 -0
  62. package/src/main/__tests__/scheduler-looks-done.test.cjs +93 -3
  63. package/src/main/__tests__/scheduler-needs-review-autoresolve.test.cjs +189 -0
  64. package/src/main/__tests__/scheduler-no-dead-end-status.test.cjs +152 -0
  65. package/src/main/__tests__/scheduler-porcelain-rename.test.cjs +164 -0
  66. package/src/main/__tests__/scheduler-quarantine-autoresolve.test.cjs +165 -0
  67. package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +160 -0
  68. package/src/main/__tests__/scheduler-reaper-helpers-basics.test.cjs +87 -0
  69. package/src/main/__tests__/scheduler-shared-tree-guard.test.cjs +88 -0
  70. package/src/main/__tests__/scheduler-starve-escalation.test.cjs +154 -0
  71. package/src/main/__tests__/scheduler-worktree-cap-defer.test.cjs +14 -0
  72. package/src/main/__tests__/telemetryClient.test.cjs +75 -5
  73. package/src/main/__tests__/telemetrySettings.test.cjs +32 -0
  74. package/src/main/chatRunner.cjs +8 -5
  75. package/src/main/health.cjs +76 -3
  76. package/src/main/historyAggregator.cjs +5 -0
  77. package/src/main/index.cjs +95 -44
  78. package/src/main/ipcSchemas.cjs +47 -0
  79. package/src/main/lib/__tests__/active-sessions.test.cjs +251 -0
  80. package/src/main/lib/__tests__/bootSelfHeal.test.cjs +107 -0
  81. package/src/main/lib/__tests__/delegationReadiness.test.cjs +322 -43
  82. package/src/main/lib/__tests__/effectiveModelInfo.test.cjs +239 -0
  83. package/src/main/lib/__tests__/gitWorktree.test.cjs +89 -0
  84. package/src/main/lib/__tests__/guardShims.test.cjs +151 -0
  85. package/src/main/lib/__tests__/loadGate.test.cjs +103 -2
  86. package/src/main/lib/__tests__/opsRootAbsoluteCwd.test.cjs +5 -5
  87. package/src/main/lib/__tests__/prdDisposition.test.cjs +224 -0
  88. package/src/main/lib/__tests__/reaperHelpers.test.cjs +179 -1
  89. package/src/main/lib/__tests__/telemetryBoot.test.cjs +11 -0
  90. package/src/main/lib/__tests__/usageCircuit.test.cjs +224 -0
  91. package/src/main/lib/__tests__/watchdog-helpers.test.cjs +312 -0
  92. package/src/main/lib/__tests__/watchdog-relaunch.test.cjs +193 -0
  93. package/{scripts → src/main}/lib/activeSessions.cjs +50 -4
  94. package/src/main/lib/agentModelResolve.cjs +65 -27
  95. package/src/main/lib/bootSelfHeal.cjs +88 -0
  96. package/src/main/lib/delegationReadiness.cjs +290 -225
  97. package/src/main/lib/effectiveModelInfo.cjs +333 -0
  98. package/src/main/lib/ephemeralCwd.cjs +1 -1
  99. package/src/main/lib/epicMint.cjs +3 -3
  100. package/src/main/lib/gitWorktree.cjs +42 -12
  101. package/src/main/lib/guardShims.cjs +156 -0
  102. package/src/main/lib/jobDirtFilter.cjs +7 -2
  103. package/src/main/lib/launchFailure.cjs +2 -1
  104. package/src/main/lib/loadGate.cjs +23 -1
  105. package/src/main/lib/machineProfile.cjs +15 -0
  106. package/src/main/lib/mcpToolCatalog.cjs +4 -1
  107. package/src/main/lib/needsReviewLedger.cjs +205 -0
  108. package/src/main/lib/opsErrorLog.cjs +1 -1
  109. package/src/main/lib/opsOwnership.cjs +1 -1
  110. package/src/main/lib/prdCreate.cjs +56 -1
  111. package/src/main/lib/prdDisposition.cjs +199 -0
  112. package/src/main/lib/prdFrontmatter.cjs +8 -2
  113. package/src/main/lib/prdLocations.cjs +167 -45
  114. package/src/main/lib/projectHomeAdminRoutes.cjs +4 -4
  115. package/src/main/lib/projectPageSummarySchema.cjs +1 -1
  116. package/src/main/lib/projectRootResolve.cjs +1 -1
  117. package/src/main/lib/queueHistory.cjs +19 -1
  118. package/src/main/lib/queueStore.cjs +6 -1
  119. package/src/main/lib/reaperHelpers.cjs +181 -15
  120. package/src/main/lib/scheduleJobSchema.cjs +8 -0
  121. package/src/main/lib/scheduleJobTransitions.cjs +33 -0
  122. package/src/main/lib/schedulerBatch.cjs +12 -1
  123. package/src/main/lib/schedulerConfig.cjs +37 -0
  124. package/src/main/lib/telemetryBoot.cjs +11 -8
  125. package/src/main/lib/telemetryClient.cjs +44 -2
  126. package/src/main/lib/telemetrySettings.cjs +20 -3
  127. package/src/main/lib/usageCircuit.cjs +159 -0
  128. package/{scripts → src/main}/lib/watchdogHelpers.cjs +1 -1
  129. package/src/main/pty.cjs +9 -0
  130. package/src/main/scheduler/prdParser.cjs +13 -0
  131. package/src/main/scheduler.cjs +1869 -193
  132. package/src/main/templates/PRD_AUTHORING.md +50 -0
  133. package/src/main/templates/project-pages-catalog.json +1 -1
  134. package/src/main/usage.cjs +21 -3
  135. package/src/preload/api.d.ts +92 -1
  136. package/src/preload/index.cjs +10 -0
  137. package/web/README.md +41 -0
  138. package/{scripts/render-project-pages.cjs → web/project-pages/render.cjs} +4 -4
  139. package/{scripts/render-project-pages → web/project-pages/renderer}/dist/renderer.cjs +1 -1
  140. package/{scripts/validate-project-pages-summary.cjs → web/project-pages/validate-summary.cjs} +5 -5
  141. package/dist/assets/AgentLibrary-Bkv-HcP1.js +0 -3
  142. package/dist/assets/MarkdownEditor-Xc141kjj.js +0 -1
  143. package/dist/assets/Permissions-DKoNVgzj.js +0 -3
  144. package/dist/assets/Scheduler-CbES7MC8.js +0 -14
  145. package/dist/assets/Settings-BX3FElXk.js +0 -3
  146. package/dist/assets/settingsSchema-sGoCTd7J.js +0 -3
  147. /package/{scripts/project-pages-logic → web/project-pages/logic}/dist/logic.cjs +0 -0
@@ -1,3 +1,4 @@
1
+ <!-- PRD_AUTHORING.md v2 -->
1
2
  # PRD Authoring Guide — Scheduler Safety Rules
2
3
 
3
4
  This guide codifies lessons from two stuck-job incidents (fizzpop poll-hang, etch-engine post-AC overrun) into enforceable rules every PRD MUST follow. Violating these rules costs real money and wastes hours waiting at a terminal.
@@ -148,6 +149,8 @@ estimateMinutes: 60
148
149
 
149
150
  **Rule:** Include exact file paths, function signatures if they save a Read, library versions, and the name of any sibling PRD the executor must NOT duplicate. Do not reference "the conversation", "what we discussed", "the design doc", or any other external context. If the executor would need to search for something, include the answer.
150
151
 
152
+ **Epic context digest is additive, not a dependency (PRD 958):** when a job's `epicId` resolves to a known Epic in that project's `active-index.json`, the scheduler prepends a short digest of the Epic's own session (goal text + recent turns, built by `src/main/lib/epicContextDigest.cjs`'s `buildContextDigest`) to the `-p` prompt sent to the executor — the on-disk PRD `.md` file itself is never rewritten. This exists purely to orient the executor faster; it is never load-bearing. The digest is a silent no-op when the Epic doesn't resolve, and any failure building it is caught and logged, never blocking dispatch. Every PRD body must still stand on its own per the rule above — write it as if the digest will not be there.
153
+
151
154
  ---
152
155
 
153
156
  ## §8 Scope sizing — target ~15 min, ceiling 30 (data-driven, 2026-06)
@@ -448,3 +451,50 @@ intake`, commit `352b89c`). It syncs open GitHub issues into `session-manager-op
448
451
  → queue path the rest of this guide documents. An external project building a Slack (or any
449
452
  other) adapter should follow the same source → triage → queue shape, ending at
450
453
  `scheduler_create_prd` (app running) or a hand-written PRD file (app closed) as described above.
454
+
455
+ ## §15 Resolving a `failed`/`needs_review` job whose target work is already done
456
+
457
+ **Symptom:** a job lands in `failed` or `needs_review` even though its actual objective was
458
+ already satisfied — either by a sibling/concurrent job that finished the same work first (a
459
+ duplicate PRD racing another one), or by an out-of-band actor (a human, another agent) reaching
460
+ the same target state before the scheduler's run even started. The job's own transcript may be
461
+ completely accurate (`SCHEDULER_VERDICT: PASS but no commit landed during the run window`) — the
462
+ "failure" is a stale queue entry, not broken work.
463
+
464
+ **Do NOT hand-edit `queue.json` to fix the status.** It's a live file the running Electron
465
+ scheduler process (and the external watchdog) both read and write; a manual edit races the app's
466
+ own save cycle and risks a torn write. There is also no supported IPC/CLI surface today to flip a
467
+ single job's `status` field directly.
468
+
469
+ **The safe, supported remediation — confirmed working live (2026-07-18):** archive the job's PRD
470
+ *source file*, not the queue entry. `reconcile()` (`src/main/scheduler.cjs`) runs on every queue
471
+ read/tick and drops any `queue.json` job entry whose PRD `.md` no longer exists in `prds/` — so
472
+ archiving the file is sufficient; you never touch `queue.json` yourself.
473
+
474
+ ```js
475
+ // From this repo's root (session-manager), or anywhere queueOps.cjs is reachable:
476
+ const q = require('./src/main/queueOps.cjs');
477
+ await q.archiveMany(['<slug-of-the-stale-job>']);
478
+ // Atomic rename to prds-archived/<ISO>/<slug>.md — reversible, path-contained,
479
+ // no queue.json write. The next reconcile() (within one scheduler tick, ~10-15s
480
+ // observed) drops the matching queue.json job entry automatically.
481
+ ```
482
+
483
+ This is the exact mechanism `schedule:clear-queue`'s IPC handler uses internally, just scoped to
484
+ one slug instead of "every non-running job" — safe to call from outside Electron (no admin API
485
+ needed) since `queueOps.cjs`'s `archiveMany` is a plain exported function, not IPC-gated.
486
+
487
+ **When to use this vs. requeueing a retry:** only when you've confirmed the target state is
488
+ already correct (read the failed job's own transcript/log — did it conclude "nothing left to
489
+ do"? did a sibling job's commit already land the same objective? does an independent check like
490
+ `gh pr view <n> --json mergeable` already show the desired end state?). If the target state is
491
+ genuinely NOT yet reached, fix and requeue instead — archiving does not fix an actual bug, it
492
+ only clears a stale status label for already-completed work.
493
+
494
+ **Known gap this doesn't cover:** the verifier's `pass_no_commit` classification (a run that
495
+ concludes "PASS, no code change needed" gets flagged `needs_review` as if it were suspicious)
496
+ does not yet special-case "another actor already satisfied this PRD's postcondition" for
497
+ externally-checkable targets (e.g. a `gh pr view`-mergeable branch). `RESCANNABLE_VERDICTS`
498
+ already includes `pass_no_commit` for one narrow case (fix-plan jobs, exempted 2026-07-12) but
499
+ not the general case. See `session-manager-operations/feedback/processed/` for the tracked
500
+ follow-up on softening this classification for merge-style PRDs with a checkable postcondition.
@@ -1,5 +1,5 @@
1
1
  {
2
- "$comment": "GENERATED by scripts/project-pages-assets.cjs from src/renderer/lib/projectPages/library/*.tsx — do not edit; run `npm run build:project-pages-assets`.",
2
+ "$comment": "GENERATED by web/project-pages/assets.cjs from src/renderer/lib/projectPages/library/*.tsx — do not edit; run `npm run build:project-pages-assets`.",
3
3
  "lenses": [
4
4
  {
5
5
  "id": "home",
@@ -116,6 +116,19 @@ const OK_CACHE_TTL_MS = 30_000;
116
116
  *
117
117
  * Returns one of: 'ok' | 'auth' | 'transient' | 'meter_rate_limited'
118
118
  */
119
+ /**
120
+ * Parses a `Retry-After` header value (delta-seconds or an HTTP-date) into a
121
+ * millisecond duration from now, or null if absent/unparseable.
122
+ */
123
+ function parseRetryAfterMs(headerValue) {
124
+ if (!headerValue) return null;
125
+ const seconds = Number(headerValue);
126
+ if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1000;
127
+ const dateMs = Date.parse(headerValue);
128
+ if (Number.isFinite(dateMs)) return Math.max(0, dateMs - Date.now());
129
+ return null;
130
+ }
131
+
119
132
  function classifyUsageResponse(status, bodyText) {
120
133
  if (status === 401 || status === 403) return { kind: 'auth', httpStatus: status };
121
134
  if (status === 429) {
@@ -195,7 +208,12 @@ async function fetchUsage() {
195
208
  let parsed = null;
196
209
  try { parsed = JSON.parse(body); } catch { /* */ }
197
210
  if (parsed?.error?.type === 'rate_limit_error') {
198
- return { kind: 'meter_rate_limited', message: body.slice(0, 200), httpStatus: 429 };
211
+ return {
212
+ kind: 'meter_rate_limited',
213
+ message: body.slice(0, 200),
214
+ httpStatus: 429,
215
+ retryAfterMs: parseRetryAfterMs(r.headers.get('retry-after')),
216
+ };
199
217
  }
200
218
  return { kind: 'transient', message: body.slice(0, 200) || 'HTTP 429', httpStatus: 429 };
201
219
  }
@@ -241,11 +259,11 @@ function registerBillingHandlers() {
241
259
  return r;
242
260
  }
243
261
  if (r.kind === 'meter_rate_limited') {
244
- if (cache) return { kind: 'meter_rate_limited', message: r.message, httpStatus: r.httpStatus, cached: cache.data, staleSince: cache.fetchedAt };
262
+ if (cache) return { kind: 'meter_rate_limited', message: r.message, httpStatus: r.httpStatus, retryAfterMs: r.retryAfterMs, cached: cache.data, staleSince: cache.fetchedAt };
245
263
  return r;
246
264
  }
247
265
  return r; // config
248
266
  });
249
267
  }
250
268
 
251
- module.exports = { registerBillingHandlers, fetchUsage, classifyUsageResponse, usageMeterApplicable, readClaudeSettingsAuth };
269
+ module.exports = { registerBillingHandlers, fetchUsage, classifyUsageResponse, parseRetryAfterMs, usageMeterApplicable, readClaudeSettingsAuth };
@@ -181,7 +181,7 @@ export type BillingFetchResult =
181
181
  | { kind: 'ok-stale'; data: BillingData; staleSince: number; lastError: string }
182
182
  | { kind: 'auth'; message: string; httpStatus: number; expiredAt?: number | null; cached?: BillingData; staleSince?: number }
183
183
  | { kind: 'transient'; message: string; httpStatus: number | null }
184
- | { kind: 'meter_rate_limited'; message: string; httpStatus: number; cached?: BillingData; staleSince?: number }
184
+ | { kind: 'meter_rate_limited'; message: string; httpStatus: number; retryAfterMs?: number | null; cached?: BillingData; staleSince?: number }
185
185
  | { kind: 'config'; message: string };
186
186
 
187
187
  // ── MCP server live connection probe (`claude mcp list`)
@@ -550,6 +550,11 @@ export interface ScheduleJob {
550
550
  /** Explicit cross-PRD ordering (PRD 832): slugs that must complete before
551
551
  * this job is eligible. Replaces the retired shared-NN-parallel convention. */
552
552
  dependsOn?: string[];
553
+ /** Wave-authoring decision (scheduler wave-disposition PRD): 'append' (this
554
+ * row's dependsOn extends the Epic's existing chain) or 'new-head' (an
555
+ * independent root). Refreshed from the PRD file on every reconcile, like
556
+ * dependsOn above; null when this row never needed the decision. */
557
+ disposition?: 'append' | 'new-head' | null;
553
558
  /** The originating claude session — the Epic's claudeSessionId, resolved
554
559
  * from active-index.json at ingest (PRD 832). An Epic IS a tagged session. */
555
560
  originSessionId?: string | null;
@@ -697,6 +702,13 @@ export interface PrdListItem {
697
702
  sourcePromptId?: string | null;
698
703
  /** Owning Epic id, derived from the PRD's directory. See ScheduleJob.epicId. */
699
704
  epicId?: string | null;
705
+ /** PRD frontmatter `dependsOn` — slugs that must complete before this PRD
706
+ * is eligible. Mirrors ScheduleJob.dependsOn; parsed by parsePrd but
707
+ * previously dropped by listPrdsInternal before reaching this type, the
708
+ * same class of bug this field's sibling comment (epicId) warns about. */
709
+ dependsOn?: string[] | null;
710
+ /** PRD frontmatter `disposition` — see ScheduleJob.disposition. */
711
+ disposition?: 'append' | 'new-head' | null;
700
712
  /** True when this PRD's source .md was found in a `prds-archived/` dir
701
713
  * (its scheduler job already ran to completion) rather than the live
702
714
  * `prds/` dir. Archived PRDs have no matching queue.json job row (the
@@ -824,6 +836,10 @@ export interface ScheduleStateSnapshot {
824
836
  jobs: ScheduleJob[];
825
837
  scheduledFor: string | null;
826
838
  lastRunAt: string | null;
839
+ /** Stamped every time tickQueue reaches the picker at all, whether or not
840
+ * that pass ends in a launch — distinct from lastRunAt (only stamped on an
841
+ * actual launch). See classifyQueueHealth's header comment. */
842
+ lastDispatchAttemptAt?: string | null;
827
843
  nextReset: string | null;
828
844
  /** Set when scheduler self-paused (rate-limit detected). null when running normally. */
829
845
  paused: SchedulePauseInfo | null;
@@ -862,6 +878,48 @@ export interface ScheduleLoadGate {
862
878
  at: string;
863
879
  }
864
880
 
881
+ /** One dependency-chain group blocked on a terminal (failed/skipped) row — computeBlockedChains' shape. */
882
+ export interface ScheduleBlockedChain {
883
+ cwd: string;
884
+ blockedBy: string[];
885
+ blocked: number;
886
+ }
887
+
888
+ /** classifyQueueHealth's verdict — the one honest cause behind a stale-looking queue. */
889
+ export interface ScheduleQueueHealthVerdict {
890
+ kind: 'paused' | 'launch-blocked' | 'idle' | 'saturated' | 'blocked' | 'stalled' | 'running';
891
+ cwd: string | null;
892
+ pending: number;
893
+ needsReviewCount: number;
894
+ runningCount: number;
895
+ /** Pending rows whose dependsOn chain is fully satisfied right now — always computed, regardless of kind. */
896
+ dispatchable: number;
897
+ blockedChains: ScheduleBlockedChain[];
898
+ /** Present on 'blocked' / 'stalled' — ms since lastDispatchAttemptAt. */
899
+ idleMs?: number;
900
+ /** Present on 'paused'. */
901
+ reason?: string | null;
902
+ /** Present on 'launch-blocked'. */
903
+ agentType?: string;
904
+ block?: ScheduleLaunchBlock;
905
+ /** Present on 'saturated'. */
906
+ totalSlots?: number | null;
907
+ }
908
+
909
+ export interface ScheduleQueueHealthResult {
910
+ /** True when queue.json was unreadable — every other field is absent; render "unknown", never a zero. */
911
+ unknown: boolean;
912
+ reason?: string;
913
+ now?: number;
914
+ verdict?: ScheduleQueueHealthVerdict;
915
+ /** Machine-wide session slot pool — not scoped to the requested cwd. */
916
+ slots?: { inUse: number; total: number; free: number; source: 'env' | 'pool' };
917
+ /** Oldest currently-running job's age in ms, across every project. null when nothing is running. */
918
+ oldestRunningAgeMs?: number | null;
919
+ lastRunAt?: string | null;
920
+ lastDispatchAttemptAt?: string | null;
921
+ }
922
+
865
923
  export interface HistoryAggregateRequest {
866
924
  fromDate?: string;
867
925
  toDate?: string;
@@ -1621,6 +1679,28 @@ export interface SessionManagerAPI {
1621
1679
  * `listPersonas` above, which only reads the global directory. Null when neither
1622
1680
  * location has the file. */
1623
1681
  getPersonaBody: (payload: { cwd: string; name: string }) => Promise<{ path: string; text: string } | null>;
1682
+ /** "What will this Epic actually run as?" — persona alias, overlay-aware
1683
+ * provenance, and evidence-based concrete model id (scheduler run log or
1684
+ * session transcript). Never throws on the main side; a dangling
1685
+ * agentType or missing evidence degrades individual fields to null. Pair
1686
+ * with the renderer's `useEffectiveSettings.ts` scope-chain reader for
1687
+ * the effortLevel/effortSource half — this call only covers the parts
1688
+ * that need main-process filesystem access. */
1689
+ resolveModelInfo: (payload: { cwd: string; agentType: string }) => Promise<{
1690
+ agentType: string;
1691
+ modelAlias: string | null;
1692
+ modelSource: 'persona' | 'persona-overlay' | 'inherit' | 'fallback';
1693
+ resolvedModelId: string | null;
1694
+ resolvedFrom: 'scheduler-run' | 'transcript' | null;
1695
+ effortReachable: boolean;
1696
+ }>;
1697
+ /** The launch-time authority for an Epic-backed session's `--model`: the SAME
1698
+ * resolver (`agentModelResolve.cjs`'s `resolveEpicModel`) chatRunner.cjs's Chat
1699
+ * path calls in-process, reached here so Terminal (`EpicTerminalPane.tsx`, no
1700
+ * fs access) agrees with Chat by calling the identical function. Overlay-aware
1701
+ * (project `.claude/agents/<name>.md` wins over global), never throws — a
1702
+ * dangling agentType or absent/`inherit` model falls back to `'sonnet'`. */
1703
+ resolveEpicModel: (payload: { cwd: string; claudeSessionId: string }) => Promise<string>;
1624
1704
  /** Fires after any save/delete/removeOverride — subscribers should re-fetch listPersonas(). */
1625
1705
  onChanged: (handler: () => void) => () => void;
1626
1706
  };
@@ -1764,6 +1844,10 @@ export interface SessionManagerAPI {
1764
1844
  writePrd: (slug: string, body: string) => Promise<{ ok: true; bytesWritten: number } | { ok: false; error: string }>;
1765
1845
  listPrds: () => Promise<PrdListItem[]>;
1766
1846
  health: () => Promise<ScheduleHealthSnapshot>;
1847
+ /** Queue-health header verdict — reuses classifyQueueHealth so this can
1848
+ * never disagree with the starvation watchdog. `cwd` null/omitted =
1849
+ * machine-wide scope. */
1850
+ queueHealth: (cwd?: string | null) => Promise<ScheduleQueueHealthResult>;
1767
1851
  onState: (handler: (snapshot: ScheduleStateSnapshot) => void) => () => void;
1768
1852
  /** Heartbeat-driven stall alert: queue holds jobs but 0 running / 0 pending / not paused for a full poll interval. Fires at most once per stall episode. */
1769
1853
  onStall: (handler: (event: ScheduleStallEvent) => void) => () => void;
@@ -1775,6 +1859,13 @@ export interface SessionManagerAPI {
1775
1859
  retagPrds: (items: RetagPrdItem[]) => Promise<RetagPrdResult>;
1776
1860
  /** Stamps a 'quarantined' PRD (no createdVia provenance) as legacy-adopted via the update-prd API, then reconciles it to 'pending'. */
1777
1861
  adoptPrd: (slug: string) => Promise<ActionOutcome>;
1862
+ /** Change a PRD's wave disposition (promote to a new head, or attach behind another chain's terminal PRD(s)) — refuses a running/completed row or a dependsOn cycle. */
1863
+ setPrdDisposition: (args: {
1864
+ slug: string;
1865
+ cwd?: string;
1866
+ disposition: 'append' | 'new-head';
1867
+ dependsOn?: string[];
1868
+ }) => Promise<ActionOutcome>;
1778
1869
  /** Return the last N completed/failed jobs from queue.json (newest first). */
1779
1870
  getHistory: (limit?: number) => Promise<ScheduleHistoryResult>;
1780
1871
  };
@@ -105,6 +105,8 @@ contextBridge.exposeInMainWorld('api', {
105
105
  deletePersona: (payload) => ipcRenderer.invoke('agents:delete-persona', payload),
106
106
  removeOverride: (payload) => ipcRenderer.invoke('agents:remove-override', payload),
107
107
  getPersonaBody: (payload) => ipcRenderer.invoke('agents:get-persona-body', payload),
108
+ resolveModelInfo: (payload) => ipcRenderer.invoke('agents:resolve-model-info', payload),
109
+ resolveEpicModel: (payload) => ipcRenderer.invoke('agents:resolve-epic-model', payload),
108
110
  // Fired after any save/delete/removeOverride so every mounted
109
111
  // subscriber (Agent Library, Tag Library) re-fetches the same file.
110
112
  onChanged: (handler) => {
@@ -243,6 +245,10 @@ contextBridge.exposeInMainWorld('api', {
243
245
  writePrd: (slug, body) => ipcRenderer.invoke('schedule:write-prd', { slug, body }),
244
246
  listPrds: () => ipcRenderer.invoke('schedule:list-prds'),
245
247
  health: () => ipcRenderer.invoke('schedule:health'),
248
+ // Queue-health header: one honest verdict for "why does the queue look
249
+ // stale" (saturated/blocked/idle/stalled/paused/launch-blocked), scoped
250
+ // to `cwd` (null = machine-wide).
251
+ queueHealth: (cwd) => ipcRenderer.invoke('schedule:queue-health', cwd ? { cwd } : {}),
246
252
  onState: (handler) => {
247
253
  const listener = (_e, payload) => handler(payload);
248
254
  ipcRenderer.on('schedule:state', listener);
@@ -264,6 +270,10 @@ contextBridge.exposeInMainWorld('api', {
264
270
  // the same update-prd API path the admin route/MCP tool use, then
265
271
  // reconciles so the row promotes to 'pending' on the very same call.
266
272
  adoptPrd: (slug) => ipcRenderer.invoke('schedule:adopt-prd', { slug }),
273
+ // Change a PRD's wave disposition (promote to a new head, or attach
274
+ // behind another chain's terminal PRD(s)) — see scheduler.cjs's
275
+ // remote.setPrdDisposition for the safety validation this goes through.
276
+ setPrdDisposition: (args) => ipcRenderer.invoke('schedule:set-prd-disposition', args),
267
277
  // History — last N completed/failed jobs from queue.json.
268
278
  getHistory: (limit) => ipcRenderer.invoke('schedule:get-history', limit !== undefined ? { limit } : {}),
269
279
  },
package/web/README.md ADDED
@@ -0,0 +1,41 @@
1
+ # web/ — the WEB PRESENCE partition
2
+
3
+ This directory collects every producer of content displayed on **bilko.run**. It is one of the
4
+ four partitions described in
5
+ [`session-manager-operations/architecture/project-partition.md`](../session-manager-operations/architecture/project-partition.md)
6
+ — PRD 1184 made this partition PHYSICAL so the boundary is visible in the tree, not just on
7
+ paper.
8
+
9
+ **The user-facing pages themselves are NOT here.** They live in `~/Projects/Bilko`, a sibling
10
+ repo. This directory only holds the tooling that *produces* what Bilko serves:
11
+
12
+ - `project-pages/` — renders a project's Home/Marketing/Feature/Architecture/Brief pages from a
13
+ saved component library + a summary JSON. Consumed by the `project-home-builder` agent
14
+ persona and by `src/main/lib/projectHomeAdminRoutes.cjs` (an Electron IPC handler that stays
15
+ in `src/main/` — it's DESKTOP HARNESS runtime that *calls* this WEB PRESENCE toolchain, not
16
+ part of it).
17
+ - `manual/` — builds and captures figures for the Field Manual, the paid product sold at
18
+ bilko.run/manual. Source content lives in `session-manager-operations/manual/` (OPERATIONS
19
+ STATE, unmoved); output lands in `~/Projects/Bilko/data/manual/releases/`.
20
+ - `remote-app/` — the phone-remote PWA, the live source of the bundle published at
21
+ `bilko.run/projects/session-manager/`.
22
+
23
+ ## Staged for extraction
24
+
25
+ This directory is staged for a future `git subtree split --prefix=web` into its own repo. That
26
+ split has NOT happened yet — this PRD only collected the partition under one path so the split
27
+ becomes mechanical later. Before actually running it, the human still needs to decide:
28
+
29
+ - **How the extracted repo reaches `src/main`'s IPC handlers.** `projectHomeAdminRoutes.cjs`
30
+ currently reads the `project-pages/renderer/dist/` and `project-pages/logic/dist/` bundles by
31
+ a relative path assuming both live in the same repo checkout. Once `web/` is a separate repo,
32
+ something has to publish those bundles somewhere the harness can still read them (a build
33
+ step that vendors them back in, a package dependency, a fetched artifact — undecided).
34
+ - **Who then owns the npm `files` array entries.** `package.json`'s `files` array currently
35
+ lists `web/project-pages/render.cjs`, `web/project-pages/renderer/dist/`,
36
+ `web/project-pages/logic/dist/`, and `web/project-pages/validate-summary.cjs` directly. If
37
+ `web/` moves to its own repo, either the harness repo re-vendors those specific build outputs
38
+ before packing, or the split repo publishes them and the harness repo depends on that package
39
+ — undecided.
40
+ - `web-remote/relay/` is dead code that was deliberately NOT moved here — it stays where it is
41
+ pending a separate decision on whether to delete it.
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  // Bash-invocable CLI wrapping the Stage 0 renderer (render.tsx →
3
- // scripts/render-project-pages/dist/renderer.cjs, built by
3
+ // web/project-pages/renderer/dist/renderer.cjs, built by
4
4
  // `npm run build:project-pages`). Requires that build to have already run —
5
5
  // this script does NOT bundle on the fly, since PRD authoring standards
6
6
  // forbid unbounded/slow steps inside a headless job.
7
7
  //
8
8
  // Usage:
9
- // node scripts/render-project-pages.cjs <summary.json> <picks.json> <output dir> <generatedAt>
9
+ // node web/project-pages/render.cjs <summary.json> <picks.json> <output dir> <generatedAt>
10
10
  //
11
11
  // generatedAt (4th arg, required) is an ISO 8601 timestamp string the CALLER
12
12
  // stamps — Date.now() is unavailable in some execution contexts (e.g.
@@ -35,13 +35,13 @@ function fail(message) {
35
35
  function main() {
36
36
  const [summaryPath, picksPath, outDir, generatedAt] = process.argv.slice(2);
37
37
  if (!summaryPath || !picksPath || !outDir || !generatedAt) {
38
- fail('usage: node scripts/render-project-pages.cjs <summary.json> <picks.json> <output dir> <generatedAt>');
38
+ fail('usage: node web/project-pages/render.cjs <summary.json> <picks.json> <output dir> <generatedAt>');
39
39
  }
40
40
  if (Number.isNaN(Date.parse(generatedAt))) {
41
41
  fail(`generatedAt must be an ISO 8601 timestamp, got: ${generatedAt}`);
42
42
  }
43
43
 
44
- const bundlePath = path.join(__dirname, 'render-project-pages', 'dist', 'renderer.cjs');
44
+ const bundlePath = path.join(__dirname, 'renderer', 'dist', 'renderer.cjs');
45
45
  if (!fs.existsSync(bundlePath)) {
46
46
  fail(`build bundle not found at ${bundlePath} — run "npm run build:project-pages" first`);
47
47
  }
@@ -18738,7 +18738,7 @@ var FONT_DATA = {
18738
18738
  var import_jsx_runtime7 = __toESM(require_jsx_runtime(), 1);
18739
18739
  function fontFace(family, weight, filename) {
18740
18740
  const base64 = FONT_DATA[filename];
18741
- if (!base64) throw new Error(`Missing font data for ${filename} \u2014 rerun scripts/generate-project-pages-font-data.mjs`);
18741
+ if (!base64) throw new Error(`Missing font data for ${filename} \u2014 rerun web/project-pages/generate-font-data.mjs`);
18742
18742
  return `@font-face{font-family:'${family}';font-style:normal;font-weight:${weight};font-display:swap;src:url(data:font/woff2;base64,${base64}) format('woff2');}`;
18743
18743
  }
18744
18744
  function buildFontFaces() {
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  // Thin CLI wrapper around validateProjectPageSummary (src/renderer/lib/
3
3
  // projectPages/summaryValidate.ts), compiled into
4
- // scripts/project-pages-logic/dist/logic.cjs by `npm run
4
+ // web/project-pages/logic/dist/logic.cjs by `npm run
5
5
  // build:project-pages-logic`. The project-home-builder Epic runs this
6
6
  // against its own summary.json before proceeding to Stage 2/3 (see
7
7
  // .claude/agents/project-home-builder.md).
8
8
  //
9
- // Usage: node scripts/validate-project-pages-summary.cjs <summary.json path>
9
+ // Usage: node web/project-pages/validate-summary.cjs <summary.json path>
10
10
  // Exits 0 and prints "valid" on success; exits 1 and prints each error on
11
- // failure. Same CLI-wrapper pattern as scripts/render-project-pages.cjs.
11
+ // failure. Same CLI-wrapper pattern as web/project-pages/render.cjs.
12
12
  //
13
13
  // The path argument is used as-is (no allowedRoots validation like
14
14
  // config.cjs's validatePath) — this is a standalone local dev/ops script run
@@ -27,10 +27,10 @@ function fail(message) {
27
27
  function main() {
28
28
  const [summaryPath] = process.argv.slice(2);
29
29
  if (!summaryPath) {
30
- fail('usage: node scripts/validate-project-pages-summary.cjs <summary.json path>');
30
+ fail('usage: node web/project-pages/validate-summary.cjs <summary.json path>');
31
31
  }
32
32
 
33
- const bundlePath = path.join(__dirname, 'project-pages-logic', 'dist', 'logic.cjs');
33
+ const bundlePath = path.join(__dirname, 'logic', 'dist', 'logic.cjs');
34
34
  if (!fs.existsSync(bundlePath)) {
35
35
  fail(`build bundle not found at ${bundlePath} — run "npm run build:project-pages-logic" first`);
36
36
  }
@@ -1,3 +0,0 @@
1
- import{a9 as e,z as V,ai as m,j as z,b as D,as as b,ap as X,T as _,ar as Z,az as ee,A as T}from"./index-DhvuQL4C.js";import{P as te}from"./Panel-D9Kr40Ai.js";import{L as ne}from"./ListDetail-pjaKYs84.js";import"./monaco-editor-BW5C4Iv1.js";function y({children:t,onClick:r,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:r,disabled:x,className:v,children:t});return i?e.jsx(V,{content:i,align:"bottom-center",children:a}):a}const ae=["inherit","haiku","sonnet","opus"],B=["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 R(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 I={name:"new-agent",description:"",tools:["Read","Grep"],model:"inherit",color:"",tags:[],projects:[],action:"",actionLabel:"",body:""},re=new Set(_.map(t=>t.tag));function oe(t){return t.filter(r=>re.has(r))}function ie(){const[t,r]=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;r(d),g(L=>{var P;return n??L??((P=d[0])==null?void 0:P.name)??null})}catch(d){if(!w.current)return;r([]),b.error(d.message||"failed to load agent personas")}};if(m.useEffect(()=>{p()},[]),m.useEffect(()=>window.api.agents.onChanged(()=>p()),[]),m.useEffect(()=>{const n=X();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(z,{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,o=u??(l?R(l):null),F=!!u&&!!l&&JSON.stringify(u)!==JSON.stringify(R(l)),S=v,K=n=>{c(null),a(!1),N(!1),C(null),g(n)},W=n=>c(d=>({...d??(l?R(l):I),...n})),M=()=>{c({...I}),a(!0),N(!1)},J=()=>{o&&(c({...o,name:o.name+"-copy"}),a(!0))},H=()=>{c(null),a(!1)},Y=async()=>{if(o){if(!/^[a-z][a-z0-9-]*$/.test(o.name)){b.error('agent name must be lowercase, hyphenated (e.g. "my-agent")');return}s(!0);try{const n={name:o.name,originalName:S||l==null?void 0:l.name,description:o.description,tools:o.tools,model:o.model,color:o.color,tags:oe(o.tags),projects:o.projects,action:o.action,actionLabel:o.actionLabel,body:o.body};await window.api.agents.savePersona(n),b.info(S?`created ${o.name}`:`saved ${o.name}`),c(null),a(!1),await p(o.name)}catch(n){b.error(n.message||"failed to save agent persona")}finally{s(!1)}}},q=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)}},U=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:M,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:o==null?void 0:o.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:o?e.jsx(le,{obj:o,set:W,saved:l,dirty:F,isNew:S,busy:k,confirmDelete:j,setConfirmDelete:N,onDuplicate:J,onRevert:H,onSave:Y,onDelete:q,onDropOverride:U}):e.jsx(z,{title:"select an agent"})})})}const pe=m.memo(ie);function le({obj:t,set:r,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=>r({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=>r({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(de,{options:ae,value:t.model,onChange:s=>r({model:s}),mono:!0})}),e.jsx(h,{label:"tools",hint:`${t.tools.length} of ${B.length} granted. Anything not ticked is refused at call time.`,children:e.jsx("div",{className:"flex flex-wrap gap-1.5",children:B.map(s=>{const w=t.tools.includes(s);return e.jsx("button",{onClick:()=>r({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:()=>r({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:_.map(s=>{const w=t.tags.includes(s.tag),p=Z(s.tag);return e.jsxs("button",{onClick:()=>r({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(ce,{obj:t,set:r}),e.jsx(h,{label:"definition",hint:"The system prompt this persona runs under.",children:e.jsx("textarea",{rows:9,value:t.body,onChange:s=>r({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 ce({obj:t,set:r}){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!==T&&!i.has(a)),u=t.projects.includes(T),c=a=>r({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(T),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=>r({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=>r({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:r,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,r&&e.jsx("div",{className:"text-[11px] text-fg-faint mt-1 leading-snug",children:r})]})}function de({options:t,value:r,onChange:x,mono:g}){return e.jsx("div",{className:"flex flex-wrap gap-1",children:t.map(i=>{const f=i===r;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{pe as AgentLibrary};
@@ -1 +0,0 @@
1
- import{a9 as i,F as r}from"./index-DhvuQL4C.js";function l({value:e,onChange:n,path:o,readOnly:a}){return i.jsx(r,{path:`file://${o}`,defaultLanguage:"markdown",value:e,onChange:t=>n(t??""),theme:"vs-dark",options:{minimap:{enabled:!1},fontSize:13,fontFamily:"JetBrains Mono, ui-monospace, Menlo, monospace",lineNumbers:"on",renderLineHighlight:"line",scrollBeyondLastLine:!1,tabSize:2,wordWrap:"on",readOnly:a,automaticLayout:!0}})}export{l as M};
@@ -1,3 +0,0 @@
1
- import{ai as d,ay as z,at as H,aA as W,av as N,a9 as e,j as w,P as D,V as q}from"./index-DhvuQL4C.js";import{P as A}from"./Panel-D9Kr40Ai.js";import{S as G}from"./ScopeSwitcher-5GTEveb2.js";import{S as Q}from"./SaveBar-I0_dWNTX.js";import{P as U}from"./Library-CpArQ-OJ.js";import{u as X,m as Y,p as Z,g as ee,E as te,a as se,s as re}from"./settingsSchema-sGoCTd7J.js";import"./monaco-editor-BW5C4Iv1.js";const ne=["allow","deny","ask"];function ae(r){if(r.trim()==="")return{perms:{},full:{},err:null};try{const n=JSON.parse(r);return{perms:n.permissions??{},full:n,err:null}}catch(n){return{perms:{},full:{},err:n.message}}}function oe(r,n){const a={...r,permissions:n};return JSON.stringify(a,null,2)+`
2
- `}function ie(){const r=z(),n=H(),a=(n==null?void 0:n.cwd)??null,s=W(t=>t.navFace),i=d.useRef(!1),u=d.useRef(s),[p,c]=d.useState(()=>s==="project"&&a?"project":"user"),[m,P]=d.useState("effective");d.useEffect(()=>{if(u.current!==s){if(u.current=s,i.current){i.current=!1;return}c(s==="project"&&a?"project":"user")}},[s,a]),d.useEffect(()=>{s==="project"&&m==="presets"&&P("effective")},[s,m]);const J=t=>{i.current=!0,c(t)},b=X(D,r,a),f=b[p]??null,j=N(t=>t.files),E=N(t=>t.setDraft),O=N(t=>t.saveJson),I=N(t=>t.revert),[M,y]=d.useState(null),k=e.jsx(q,{options:[{key:"effective",label:"Effective"},{key:"rules",label:"Rules"},...s==="home"?[{key:"presets",label:"Presets"}]:[]],active:m,onChange:P}),$=d.useMemo(()=>{const t=Y(Z(j,b));return ee(t,["permissions"])??{kind:"object",children:{}}},[j,b]),F=(t,l)=>{var R;const o=b[p];if(!o)return;y(null);const h=((R=j[o])==null?void 0:R.draftRaw)??"";let v={};if(h.trim())try{v=JSON.parse(h)}catch{v={}}const B=re(v.permissions??{},t,l),K={...v,permissions:B};E(o,JSON.stringify(K,null,2)+`
3
- `)};if(!r)return e.jsx(w,{title:"loading…"});if(m==="presets")return e.jsx(A,{toolbar:k,children:e.jsx(U,{})});const C=t=>t!=="user"&&!a,g=f?j[f]:null,{perms:x,full:L,err:T}=g?ae(g.draftRaw):{perms:{},full:{},err:null},S=t=>{f&&(y(null),E(f,oe(L,t)))},_=(t,l)=>{const o=l.trim();if(!o)return;const h=x[t]??[];h.includes(o)||S({...x,[t]:[...h,o]})},V=(t,l)=>{const o=x[t]??[];S({...x,[t]:o.filter((h,v)=>v!==l)})};return e.jsx(A,{toolbar:e.jsxs(e.Fragment,{children:[k,e.jsx("span",{className:"mx-2 text-fg-faint",children:"·"}),e.jsx(G,{scopes:D.scopes,active:p,onChange:J,annotate:t=>{if(C(t))return{exists:!1};const l=b[t];if(!l)return null;const o=j[l];return{exists:(o==null?void 0:o.exists)??!1,dirty:(o==null?void 0:o.dirty)??!1}}}),e.jsx("span",{className:"ml-3 text-fg-faint truncate",children:f??"—"}),e.jsx("div",{className:"flex-1"}),e.jsx("a",{href:"https://code.claude.com/docs/en/settings#permissions",target:"_blank",rel:"noreferrer",className:"text-fg-faint hover:text-fg-dim underline-offset-2 hover:underline",children:"rule syntax ↗"})]}),footer:f&&g?e.jsx(Q,{dirty:g.dirty,busy:g.busy,parseError:M||T||g.parseError,lastSavedAt:g.lastSavedAt,onSave:async()=>{y(null);const t=await O(f);t.ok||y(t.error??"save failed")},onRevert:()=>{y(null),I(f)}}):null,children:C(p)?e.jsx(w,{title:"no active project",hint:"open a tab in a project directory to edit project or local permissions"}):m==="effective"?e.jsx(te,{node:$,targetScope:p,onOverride:F,schema:se().rootedAt(["permissions"]),rootLabel:"permissions"}):f&&g?e.jsxs("div",{className:"p-4 space-y-5 max-w-3xl",children:[e.jsx(le,{value:x.defaultMode??"default",onChange:t=>S({...x,defaultMode:t})}),ne.map(t=>e.jsx(ce,{list:t,rules:x[t]??[],onAdd:l=>_(t,l),onRemove:l=>V(t,l)},t)),e.jsx(de,{dirs:x.additionalDirectories??[],onChange:t=>S({...x,additionalDirectories:t})})]}):e.jsx(w,{title:"resolving…"})})}const ve=d.memo(ie);function le({value:r,onChange:n}){const a=["default","acceptEdits","plan","bypassPermissions"];return e.jsxs("section",{children:[e.jsx("h3",{className:"text-xs text-fg uppercase tracking-wider mb-2",children:"Default Mode"}),e.jsx("div",{className:"inline-flex rounded border border-line overflow-hidden",children:a.map(s=>e.jsx("button",{onClick:()=>n(s),className:`px-2.5 py-1 text-xs transition-colors ${r===s?"bg-bg-hi text-fg":"bg-bg-elev text-fg-dim hover:text-fg hover:bg-bg-hi"}`,children:s},s))})]})}function ce({list:r,rules:n,onAdd:a,onRemove:s}){const[i,u]=d.useState(""),p=r==="allow"?"text-green-400":r==="deny"?"text-red-400":"text-yellow-400";return e.jsxs("section",{children:[e.jsx("h3",{className:`text-xs uppercase tracking-wider mb-2 ${p}`,children:r}),e.jsxs("div",{className:"flex gap-2 mb-2",children:[e.jsx("input",{value:i,onChange:c=>u(c.target.value),onKeyDown:c=>{c.key==="Enter"&&(a(i),u(""))},placeholder:r==="allow"?"e.g. Bash(ls:*) or Read(~/.claude/**)":"e.g. Write(/etc/**)",className:"flex-1 bg-bg-elev border border-line rounded px-2 py-1 text-xs text-fg placeholder-fg-faint"}),e.jsx("button",{onClick:()=>{a(i),u("")},disabled:!i.trim(),className:"px-2.5 py-1 border border-line rounded text-xs text-fg-dim hover:text-fg hover:bg-bg-hi disabled:opacity-40",children:"add"})]}),n.length===0?e.jsx("div",{className:"text-fg-faint text-xs",children:"no rules"}):e.jsx("ul",{className:"space-y-0.5",children:n.map((c,m)=>e.jsxs("li",{className:"flex items-center gap-2 text-xs font-mono bg-bg-elev border border-line rounded px-2 py-1",children:[e.jsx("span",{className:"flex-1 text-fg-dim truncate",children:c}),e.jsx("button",{onClick:()=>s(m),className:"text-fg-faint hover:text-red-400",title:"remove",children:"×"})]},`${m}-${c}`))})]})}function de({dirs:r,onChange:n}){const[a,s]=d.useState("");return e.jsxs("section",{children:[e.jsx("h3",{className:"text-xs text-fg uppercase tracking-wider mb-2",children:"Additional Directories"}),e.jsx("div",{className:"flex gap-2 mb-2",children:e.jsx("input",{value:a,onChange:i=>s(i.target.value),onKeyDown:i=>{i.key==="Enter"&&a.trim()&&(n([...r,a.trim()]),s(""))},placeholder:"/absolute/path/to/dir",className:"flex-1 bg-bg-elev border border-line rounded px-2 py-1 text-xs text-fg placeholder-fg-faint"})}),e.jsx("ul",{className:"space-y-0.5",children:r.map((i,u)=>e.jsxs("li",{className:"flex items-center gap-2 text-xs font-mono bg-bg-elev border border-line rounded px-2 py-1",children:[e.jsx("span",{className:"flex-1 text-fg-dim truncate",children:i}),e.jsx("button",{onClick:()=>n(r.filter((p,c)=>c!==u)),className:"text-fg-faint hover:text-red-400",children:"×"})]},`${u}-${i}`))})]})}export{ve as Permissions};