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
package/dist/index.html CHANGED
@@ -7,10 +7,10 @@
7
7
  <link rel="preconnect" href="https://fonts.googleapis.com">
8
8
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
9
  <link href="https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,500;0,6..72,600;0,6..72,700;1,6..72,400&family=Geist:wght@300;400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
10
- <script type="module" crossorigin src="./assets/index-DhvuQL4C.js"></script>
10
+ <script type="module" crossorigin src="./assets/index-fc_JjdxL.js"></script>
11
11
  <link rel="modulepreload" crossorigin href="./assets/monaco-editor-BW5C4Iv1.js">
12
12
  <link rel="stylesheet" crossorigin href="./assets/monaco-editor-BTnBOi8r.css">
13
- <link rel="stylesheet" crossorigin href="./assets/index-CDo9xBR9.css">
13
+ <link rel="stylesheet" crossorigin href="./assets/index-DV3PorRY.css">
14
14
  </head>
15
15
  <body class="bg-bg text-fg font-sans antialiased">
16
16
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-session-manager",
3
- "version": "0.85.0",
3
+ "version": "0.87.0",
4
4
  "description": "Local cockpit for the Claude Code CLI — multi-tab terminal, full config surface, scheduler, voice dictation, and live observability.",
5
5
  "type": "module",
6
6
  "main": "src/main/index.cjs",
@@ -12,13 +12,12 @@
12
12
  ".claude-plugin/",
13
13
  "plugins/",
14
14
  "scripts/postinstall.cjs",
15
- "scripts/lib/",
16
15
  "scripts/mint-epic.cjs",
17
16
  "scripts/scheduler-mcp-server.cjs",
18
- "scripts/render-project-pages.cjs",
19
- "scripts/render-project-pages/dist/",
20
- "scripts/project-pages-logic/dist/",
21
- "scripts/validate-project-pages-summary.cjs",
17
+ "web/project-pages/render.cjs",
18
+ "web/project-pages/renderer/dist/",
19
+ "web/project-pages/logic/dist/",
20
+ "web/project-pages/validate-summary.cjs",
22
21
  "scripts/hooks/guard-prd-writes.cjs",
23
22
  "scripts/hooks/guard-destructive-git.cjs",
24
23
  "scripts/hooks/guard-inline-implementation.cjs",
@@ -36,10 +35,10 @@
36
35
  "dev:renderer": "vite",
37
36
  "dev:electron": "wait-on http://localhost:5173 && SM_DEV=1 electron .",
38
37
  "build": "vite build",
39
- "build:project-pages": "node scripts/build-project-pages-renderer.mjs",
40
- "build:project-pages-logic": "node scripts/build-project-pages-logic.mjs",
41
- "build:project-pages-assets": "node scripts/project-pages-assets.cjs",
42
- "project-pages:gate": "node scripts/project-pages-publish-gate.cjs",
38
+ "build:project-pages": "node web/project-pages/build-renderer.mjs",
39
+ "build:project-pages-logic": "node web/project-pages/build-logic.mjs",
40
+ "build:project-pages-assets": "node web/project-pages/assets.cjs",
41
+ "project-pages:gate": "node web/project-pages/publish-gate.cjs",
43
42
  "typecheck": "tsc --noEmit",
44
43
  "start": "electron .",
45
44
  "postinstall": "node scripts/postinstall.cjs",
@@ -51,12 +50,14 @@
51
50
  "test:e2e:gen-fixture": "espeak-ng -w /tmp/sm-raw.wav -s 140 'testing one two three four five' && ffmpeg -y -i /tmp/sm-raw.wav -ar 48000 -ac 2 -sample_fmt s16 e2e/fixtures/speech.wav",
52
51
  "refresh:vad-assets": "cp node_modules/@ricky0123/vad-web/dist/silero_vad_v5.onnx node_modules/@ricky0123/vad-web/dist/vad.worklet.bundle.min.js src/renderer/public/vad/ && cp node_modules/onnxruntime-web/dist/ort-wasm-simd-threaded.wasm node_modules/onnxruntime-web/dist/ort-wasm-simd-threaded.mjs node_modules/onnxruntime-web/dist/ort-wasm-simd-threaded.jsep.wasm node_modules/onnxruntime-web/dist/ort-wasm-simd-threaded.jsep.mjs src/renderer/public/vad/",
53
52
  "health": "node src/main/health.cjs",
54
- "manual:check": "node scripts/build-manual.mjs --check",
55
- "manual:build": "node scripts/build-manual.mjs",
56
- "manual:figures": "xvfb-run -a node scripts/capture-manual-figures.mjs",
53
+ "manual:check": "node web/manual/build.mjs --check",
54
+ "manual:build": "node web/manual/build.mjs",
55
+ "manual:figures": "xvfb-run -a node web/manual/capture-figures.mjs",
57
56
  "lint:selectors": "node scripts/check-unstable-selectors.cjs",
58
57
  "lint:hooks": "node scripts/check-conditional-hooks.cjs",
59
- "lint": "npm run lint:selectors && npm run lint:hooks"
58
+ "lint:unregistered-tests": "node scripts/check-unregistered-tests.cjs",
59
+ "lint": "npm run lint:selectors && npm run lint:hooks && npm run lint:unregistered-tests",
60
+ "sync:settings-schema": "node scripts/sync-settings-schema.cjs"
60
61
  },
61
62
  "keywords": [
62
63
  "claude",
@@ -0,0 +1,61 @@
1
+ # plugins/ — scoped context
2
+
3
+ Part of the **AGENT LAYER** partition — see
4
+ [`project-partition.md`](../session-manager-operations/architecture/project-partition.md).
5
+
6
+ ## What's here
7
+
8
+ `session-manager-dev/skills/` — 15 skills (`blog-for-project-feature`, `builder`, `develop`,
9
+ `discover-features`, `explain-to-me`, `find-opportunity`, `issue-address`,
10
+ `local-project-health`, `memory-sanitation`, `ops-sweep`, `project-status`, `pr-review-sweep`,
11
+ `pr-signal`, `requesting-code-review`, `send-feedback`) — verified against `ls
12
+ plugins/session-manager-dev/skills` 2026-09-13; keep in sync with `project-partition.md` and
13
+ `seedDevPlugin.cjs`'s header count.
14
+
15
+ Two manifests: root [`.claude-plugin/marketplace.json`](../.claude-plugin/marketplace.json)
16
+ declares the one plugin (`source: "./plugins/session-manager-dev"`); the plugin's own
17
+ [`session-manager-dev/.claude-plugin/plugin.json`](session-manager-dev/.claude-plugin/plugin.json)
18
+ carries the `description` shown in the marketplace UI — list only shipped skills.
19
+
20
+ `src/main/seedDevPlugin.cjs` is the first-boot offline installer — installs this plugin from its
21
+ bundled marketplace (no GitHub/registry call) as a default on a fresh machine, then marks
22
+ `~/.claude/session-manager/.dev-plugin-seeded`.
23
+
24
+ **`skills/develop/standards.md` is a frozen absolute-path contract.** Every scheduler PRD, in
25
+ every consuming repo, points its headless executor at it by resolved absolute path, baked
26
+ in at authoring time — including PRDs already archived in OTHER repos. Renaming or moving it
27
+ breaks them all, undetectably from here.
28
+
29
+ **Path literals inside shipped skills.** A skill runs in OTHER repos, so a
30
+ session-manager-internal path (`src/main/...`, `src/renderer/...`, bare `scripts/...`) inside one
31
+ is a portability defect unless `$SM_ROOT`-qualified (see `ops-sweep/SKILL.md`'s resolution) or
32
+ removed. Current offenders, not precedent: `memory-sanitation/SKILL.md`, `builder/SKILL.md` +
33
+ `builder/0-diff/SKILL.md`, `develop/SKILL.md`, `develop/standards.md`, `ops-sweep/SKILL.md`.
34
+
35
+ ## Who consumes this
36
+
37
+ **Other repos**, not (only) this one. A project installs `claude-code-session-manager` from npm,
38
+ or references its skills/hooks directly, for the same dev workflow session-manager uses on itself.
39
+ Editing a skill here changes behavior in every consuming repo. See
40
+ [`../session-manager-operations/reviews/2026-09-12-agent-layer-consumers.md`](../session-manager-operations/reviews/2026-09-12-agent-layer-consumers.md)
41
+ for live sibling consumers.
42
+
43
+ ## What must NOT be assumed
44
+
45
+ - Treat every skill edit here as a cross-repo API change, not a local-only one.
46
+ - Do not vendor `scripts/hooks/guard-*.cjs` into a skill/repo — adopt **by reference** via the
47
+ stable shim (`src/main/lib/guardShims.cjs`) at `~/.claude/session-manager/hooks/guard-*.cjs`,
48
+ never this repo's own absolute path or a copy.
49
+ - Do not confuse this with `.claude/` at the repo root — this repo's own unshipped local dev
50
+ config. `.claude/agents/*.md` are per-project OVERLAYS that win over
51
+ `~/.claude/agents/*.md` (Claude Code's own precedence, per `src/main/agentLibrary.cjs`);
52
+ `src/seed/agents/` is the shipped source for exactly the three personas
53
+ `src/main/seedAgentPersonas.cjs` copies into `~/.claude/agents/` on first boot (`architect`,
54
+ `dev-lead`, `project-home-builder`); `builder` and `bilko-host-publisher` have no seed source
55
+ and exist only as this repo's overlays. Tracked on purpose: `.claude/settings.json` (guard
56
+ hooks) plus the `builder.md`/`project-home-builder.md` overlays (auditable edits) —
57
+ `bilko-host-publisher.md` is untracked despite being in use; nothing else under `.claude/` is.
58
+ - `.mcp.json` at the repo root registers only `bilko-host` — the scheduler MCP server
59
+ (`session-manager-scheduler`) is registered at USER scope in `~/.claude.json`, not here.
60
+ - A skill may run headless via a scheduled PRD in another project — don't rely on
61
+ interactive-session-only features; check `/develop`'s scheduler flow first.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "session-manager-dev",
3
- "description": "The default end-to-end development skillset for the Claude Code Session Manager: decompose work into scheduler PRDs (/develop), hand a finding to another project on this machine (/send-feedback), rank what to work on next in a shared repo (/find-opportunity), roll up operational status and optimize the North-Star KPI (/project-status, /optimize-kpi, /local-project-health), gate with review (/requesting-code-review, /security-review), document how the project works (/explain-to-me), and keep the auto-memory store clean (/memory-sanitation). Together with the session-manager scheduler these form a closed develop → queue → track → review → status → improve loop.",
3
+ "description": "The default end-to-end development skillset for the Claude Code Session Manager: decompose work into scheduler PRDs (/develop), hand a finding to another project on this machine (/send-feedback), rank what to work on next in a shared repo (/find-opportunity), roll up operational status (/project-status, /local-project-health), gate with review (/requesting-code-review), document how the project works (/explain-to-me), and keep the auto-memory store clean (/memory-sanitation). Together with the session-manager scheduler these form a closed develop → queue → track → review → status → improve loop.",
4
4
  "version": "0.1.0",
5
5
  "author": {
6
6
  "name": "Stanislav Georgiev",
@@ -54,7 +54,7 @@ Hard rules — these are the ones that protect a paying customer:
54
54
  the one thing people get wrong, the workflow that pays off. If a paragraph would be equally
55
55
  at home in `CLAUDE.md`, cut it.
56
56
  - **Preserve the free chapter.** At least one chapter must stay `free: true` — it is the
57
- marketing sample, and `scripts/build-manual.mjs` refuses to build without one.
57
+ marketing sample, and `web/manual/build.mjs` refuses to build without one.
58
58
 
59
59
  ### 3. Bump the manual's own version
60
60
 
@@ -133,5 +133,5 @@ id(s) or PRD(s) filed.
133
133
  directly; don't run the full sweep for a question with a known answer.
134
134
  - Cron/scheduled sweeping across every active project cwd is an explicit
135
135
  stretch goal, not implemented here — this skill is manual, single-project,
136
- invoked per run. `scripts/lib/watchdogHelpers.cjs`'s `activeProjectCwds()`
136
+ invoked per run. `src/main/lib/watchdogHelpers.cjs`'s `activeProjectCwds()`
137
137
  is the reusable piece if that's ever built.
@@ -164,6 +164,11 @@ const TOOLS = [
164
164
  slug: { type: 'string', description: 'Optional kebab-case slug; derived from title if omitted' },
165
165
  parallelGroup: { type: 'number', description: 'DEPRECATED and ignored (PRD 832): numbers are strictly unique per project; use dependsOn for ordering' },
166
166
  dependsOn: { type: 'array', items: { type: 'string' }, description: 'Optional: PRD slugs that must complete before this one becomes eligible (replaces the retired shared-NN-parallel convention)' },
167
+ disposition: {
168
+ type: 'string',
169
+ enum: ['append', 'new-head'],
170
+ description: 'Required when this PRD would land as a new root (no dependsOn given) in an Epic that already has incomplete PRDs: "append" depends this wave on the existing chain\'s current terminal PRD(s); "new-head" leaves it independent, eligible to run in parallel. Omit when the Epic has no incomplete PRDs yet, or when dependsOn already places this PRD explicitly.',
171
+ },
167
172
  sourcePromptId: { type: 'string', description: "Effectively required: an EXISTING, already-human-approved Epic's promptSessionId (the id shown in the Epics list / active-index.json sessions key) to join — NOT a PromptTicket.id (those are distinct fields). The server never mints a new Epic; if this is omitted and no Epic can be resolved (via the SM_CHAT_SESSION_ID fallback below), the write is refused with an error telling the caller to create/approve an Epic first." },
168
173
  sourceTabId: { type: 'string', description: 'Optional: tab id (claudeSessionId) this PRD was queued from, so the scheduler can route a completion status prompt back to it' },
169
174
  tag: {
@@ -231,6 +236,8 @@ const TOOLS = [
231
236
  tag: { type: 'string', enum: PRD_WORK_TYPES },
232
237
  agentType: { type: 'string', description: 'WHO executes this PRD (persona name) — patchable after creation too, see scheduler_create_prd' },
233
238
  quietMachine: { type: 'boolean', description: 'Optional: the exclusive machine-wide quiet lease flag (see scheduler_create_prd) — patchable after creation too' },
239
+ dependsOn: { type: 'array', items: { type: 'string' }, description: 'Optional: replaces the dependsOn list; an explicit empty array CLEARS it — see scheduler_create_prd' },
240
+ disposition: { type: 'string', enum: ['append', 'new-head'], description: 'Optional: patches the recorded wave-authoring decision — see scheduler_create_prd' },
234
241
  },
235
242
  },
236
243
  body: { type: 'string', description: 'Optional: full replacement body (everything after the frontmatter)' },
@@ -17,7 +17,7 @@ const {
17
17
  FALLBACK_MODEL,
18
18
  resolveEpicModel,
19
19
  findAgentTypeByClaudeSessionId,
20
- readPersonaModel,
20
+ readOverlayAwarePersonaModel,
21
21
  resolvePrdPersonaForSpawn,
22
22
  } = require('../lib/agentModelResolve.cjs');
23
23
 
@@ -103,37 +103,39 @@ test('findAgentTypeByClaudeSessionId returns null for no match', async () => {
103
103
  expect(findAgentTypeByClaudeSessionId(cwd, 'missing')).toBeNull();
104
104
  });
105
105
 
106
- test('readPersonaModel returns null for a persona with no model field', async () => {
106
+ test('readOverlayAwarePersonaModel returns null for a persona with no model field', async () => {
107
+ const cwd = await mkTmpDir('sm-agentmodel-cwd-');
107
108
  const globalDir = await mkTmpDir('sm-agentmodel-agents-');
108
109
  writePersona(globalDir, 'no-model-persona', []);
109
- expect(readPersonaModel('no-model-persona', { globalDir, validatePath: noopValidatePath })).toBeNull();
110
+ expect(readOverlayAwarePersonaModel('no-model-persona', { cwd, globalDir, validatePath: noopValidatePath })).toBeNull();
110
111
  });
111
112
 
112
- test('readPersonaModel returns null (never throws) for a path-traversal agentType rejected by validatePath', async () => {
113
+ test('readOverlayAwarePersonaModel returns null (never throws) for a path-traversal agentType rejected by validatePath', async () => {
114
+ const cwd = await mkTmpDir('sm-agentmodel-cwd-');
113
115
  const globalDir = await mkTmpDir('sm-agentmodel-agents-');
114
116
  const realValidatePath = (p) => {
115
117
  const real = path.resolve(p);
116
118
  if (!real.startsWith(path.resolve(globalDir) + path.sep)) throw new Error('outside allowed boundaries');
117
119
  return real;
118
120
  };
119
- expect(readPersonaModel('../../../etc/passwd', { globalDir, validatePath: realValidatePath })).toBeNull();
121
+ expect(readOverlayAwarePersonaModel('../../../etc/passwd', { cwd, globalDir, validatePath: realValidatePath })).toBeNull();
120
122
  });
121
123
 
122
124
  // READ side of the agentType FK: a persona that was valid when the Epic was
123
125
  // created but got deleted afterward (rename/deletion in Agent Library) must
124
126
  // still resolve to null, not throw — the WRITE side (epicMint.cjs's
125
127
  // ensureEpic) is what refuses a bad reference at creation time.
126
- test('readPersonaModel returns null (does not throw) for a persona deleted after the Epic was created, and logs it once via opsErrorLog', async () => {
128
+ test('readOverlayAwarePersonaModel returns null (does not throw) for a persona deleted after the Epic was created, and logs it once via opsErrorLog', async () => {
127
129
  const cwd = await mkTmpDir('sm-agentmodel-cwd-');
128
130
  const globalDir = await mkTmpDir('sm-agentmodel-agents-'); // never written to — simulates a deleted persona
129
131
 
130
- expect(() => readPersonaModel('deleted-persona', { globalDir, validatePath: noopValidatePath, cwd })).not.toThrow();
131
- expect(readPersonaModel('deleted-persona', { globalDir, validatePath: noopValidatePath, cwd })).toBeNull();
132
+ expect(() => readOverlayAwarePersonaModel('deleted-persona', { globalDir, validatePath: noopValidatePath, cwd })).not.toThrow();
133
+ expect(readOverlayAwarePersonaModel('deleted-persona', { globalDir, validatePath: noopValidatePath, cwd })).toBeNull();
132
134
 
133
135
  const { todayFile } = require('../lib/opsErrorLog.cjs');
134
136
  const lines = fs.readFileSync(todayFile(cwd), 'utf8').split('\n').filter(Boolean).map((l) => JSON.parse(l));
135
137
  const matches = lines.filter((l) => l.message.includes('deleted-persona'));
136
- // Logged once despite two readPersonaModel calls above — dedup key is (cwd, agentType).
138
+ // Logged once despite two readOverlayAwarePersonaModel calls above — dedup key is (cwd, agentType).
137
139
  expect(matches).toHaveLength(1);
138
140
  expect(matches[0].level).toBe('warn');
139
141
  });
@@ -190,6 +192,95 @@ test('resolvePrdPersonaForSpawn returns the fallback (no persona applied) when a
190
192
  expect(result).toEqual({ model: FALLBACK_MODEL, systemPrompt: null, personaPath: null });
191
193
  });
192
194
 
195
+ // Overlay-aware resolution (unify-epic-model-resolution): resolveEpicModel
196
+ // must use the SAME project-overlay-then-global precedence
197
+ // resolvePrdPersonaForSpawn already uses — so a project's
198
+ // `.claude/agents/<name>.md` wins for a launched Chat/Terminal session
199
+ // exactly like it already does for a scheduled PRD.
200
+
201
+ test('resolveEpicModel prefers a project-overlay persona over the global one with the same name', async () => {
202
+ const cwd = await mkTmpDir('sm-agentmodel-cwd-');
203
+ const globalDir = await mkTmpDir('sm-agentmodel-agents-');
204
+ writeIndex(cwd, { 'epic-1': { id: 'epic-1', claudeSessionId: 'sess-overlay', agentType: 'shared-persona' } });
205
+ writePersona(globalDir, 'shared-persona', ['model: opus']);
206
+ const projectAgentsDir = path.join(cwd, '.claude', 'agents');
207
+ writePersona(projectAgentsDir, 'shared-persona', ['model: haiku']);
208
+
209
+ const model = resolveEpicModel({
210
+ cwd,
211
+ claudeSessionId: 'sess-overlay',
212
+ deps: { globalDir, validatePath: noopValidatePath },
213
+ });
214
+
215
+ expect(model).toBe('haiku');
216
+ });
217
+
218
+ test('resolveEpicModel falls through to the global persona when no project overlay exists', async () => {
219
+ const cwd = await mkTmpDir('sm-agentmodel-cwd-');
220
+ const globalDir = await mkTmpDir('sm-agentmodel-agents-');
221
+ writeIndex(cwd, { 'epic-1': { id: 'epic-1', claudeSessionId: 'sess-global-only', agentType: 'global-only-persona' } });
222
+ writePersona(globalDir, 'global-only-persona', ['model: opus']);
223
+
224
+ const model = resolveEpicModel({
225
+ cwd,
226
+ claudeSessionId: 'sess-global-only',
227
+ deps: { globalDir, validatePath: noopValidatePath },
228
+ });
229
+
230
+ expect(model).toBe('opus');
231
+ });
232
+
233
+ test('resolveEpicModel falls back to FALLBACK_MODEL (never throws) for a dangling agentType with no persona file anywhere, and logs it once', async () => {
234
+ const cwd = await mkTmpDir('sm-agentmodel-cwd-');
235
+ const globalDir = await mkTmpDir('sm-agentmodel-agents-'); // never written to
236
+ writeIndex(cwd, { 'epic-1': { id: 'epic-1', claudeSessionId: 'sess-dangling', agentType: 'ghost-persona' } });
237
+
238
+ const call = () => resolveEpicModel({
239
+ cwd,
240
+ claudeSessionId: 'sess-dangling',
241
+ deps: { globalDir, validatePath: noopValidatePath },
242
+ });
243
+
244
+ expect(call).not.toThrow();
245
+ expect(call()).toBe(FALLBACK_MODEL);
246
+ call(); // second call must not log a second line (dedup per (cwd, agentType))
247
+
248
+ const { todayFile } = require('../lib/opsErrorLog.cjs');
249
+ const lines = fs.readFileSync(todayFile(cwd), 'utf8').split('\n').filter(Boolean).map((l) => JSON.parse(l));
250
+ const matches = lines.filter((l) => l.message.includes('ghost-persona'));
251
+ expect(matches).toHaveLength(1);
252
+ });
253
+
254
+ // Chat (chatRunner.cjs, in-process) and Terminal (EpicTerminalPane.tsx, via
255
+ // the agents:resolve-epic-model IPC added in this change) both call this
256
+ // exact function with the same { cwd, claudeSessionId } shape — so "the same
257
+ // Epic resolves identically in both views" is structural (one function, one
258
+ // call signature), not a coincidence of two implementations agreeing. This
259
+ // asserts the function is a pure, deterministic read of that Epic's overlay-
260
+ // resolved persona model, which is what makes that guarantee hold.
261
+ test('resolveEpicModel resolves identically for the same Epic across repeated calls (the guarantee Chat and Terminal both rely on)', async () => {
262
+ const cwd = await mkTmpDir('sm-agentmodel-cwd-');
263
+ const globalDir = await mkTmpDir('sm-agentmodel-agents-');
264
+ writeIndex(cwd, { 'epic-1': { id: 'epic-1', claudeSessionId: 'sess-shared', agentType: 'shared-epic-persona' } });
265
+ writePersona(globalDir, 'shared-epic-persona', ['model: haiku']);
266
+
267
+ const callArgs = { cwd, claudeSessionId: 'sess-shared', deps: { globalDir, validatePath: noopValidatePath } };
268
+ const chatResult = resolveEpicModel(callArgs);
269
+ const terminalResult = resolveEpicModel(callArgs); // simulates the IPC-forwarded call Terminal makes
270
+
271
+ expect(chatResult).toBe('haiku');
272
+ expect(terminalResult).toBe(chatResult);
273
+ });
274
+
275
+ // --model must never be left unpinned (CLAUDE.md model-pinning rule) on any
276
+ // of the three launch paths. resolvePrdPersonaForSpawn's own miss-path tests
277
+ // above already cover the scheduler call site; this covers the shared
278
+ // Chat/Terminal resolver.
279
+ test('resolveEpicModel never returns an empty/falsy --model value, even on every miss path', async () => {
280
+ expect(resolveEpicModel({})).toBeTruthy();
281
+ expect(resolveEpicModel({ cwd: '/nonexistent-cwd-xyz', claudeSessionId: 'no-such-session' })).toBeTruthy();
282
+ });
283
+
193
284
  test('resolvePrdPersonaForSpawn caps the persona body at 6000 characters with a truncation notice naming the persona path', async () => {
194
285
  const longBody = 'x'.repeat(6500);
195
286
  const result = await resolvePrdPersonaForSpawn({
@@ -101,4 +101,22 @@ test('flush() with no pending timer still sends (no-arm case)', async () => {
101
101
  expect(sent).toEqual(['state-1']);
102
102
  });
103
103
 
104
+ test('a getPayload() that throws does not wedge the coalescer — the next schedule() still sends', async () => {
105
+ const sent = [];
106
+ const send = vi.fn((payload) => sent.push(payload));
107
+ const getPayload = vi.fn()
108
+ .mockRejectedValueOnce(new Error('boom'))
109
+ .mockResolvedValue('state-recovered');
110
+ const coalescer = createBroadcastCoalescer({ delayMs: 200, send, getPayload });
111
+
112
+ coalescer.schedule();
113
+ await vi.advanceTimersByTimeAsync(200);
114
+ expect(sent).toEqual([]);
115
+
116
+ coalescer.schedule();
117
+ await vi.advanceTimersByTimeAsync(200);
118
+ expect(sent).toEqual(['state-recovered']);
119
+ expect(getPayload).toHaveBeenCalledTimes(2);
120
+ });
121
+
104
122
  console.log('broadcastCoalescer tests: PASS');
@@ -287,8 +287,8 @@ test('ensureEpic refuses the mint and appends an epic_mint_refused audit event w
287
287
  // "throw on write, report on read" — a mint whose agentType names a persona
288
288
  // that doesn't resolve is refused outright; a persona that goes dangling
289
289
  // LATER (renamed/deleted after the Epic was minted) is legitimate history,
290
- // left to agentModelResolve.cjs's readPersonaModel (stays permissive) and the
291
- // ops-hygiene sweep to report.
290
+ // left to agentModelResolve.cjs's readOverlayAwarePersonaModel (stays
291
+ // permissive) and the ops-hygiene sweep to report.
292
292
 
293
293
  test('minting with an unknown agentType throws and names the two paths checked', async () => {
294
294
  const cwd = await mkCwd();
@@ -86,7 +86,7 @@ test('failing check with no fix omits the "(fix: ...)" suffix', () => {
86
86
  test('end-to-end: check() reports delegation_chain for this repo, expected to pass here', async () => {
87
87
  const status = await check();
88
88
  expect(status.components.delegation_chain).toBeDefined();
89
- expect(status.components.delegation_chain.checks).toHaveLength(8);
89
+ expect(status.components.delegation_chain.checks).toHaveLength(9);
90
90
  const ids = status.components.delegation_chain.checks.map((c) => c.id);
91
91
  expect(ids).toEqual([
92
92
  'scheduler-mcp',
@@ -97,6 +97,7 @@ test('end-to-end: check() reports delegation_chain for this repo, expected to pa
97
97
  'prd-write-guard',
98
98
  'destructive-git-guard',
99
99
  'inline-implementation-guard',
100
+ 'self-schedule-guard',
100
101
  ]);
101
102
  // This repo registers the scheduler MCP at user scope (~/.claude.json) and
102
103
  // wires guard-prd-writes in .claude/settings.json, and no longer carries a
@@ -0,0 +1,94 @@
1
+ /**
2
+ * health-starve-escalation.test.cjs — `npm run health` must report
3
+ * non-GREEN when any project has been starved (findStarvedProjects) past
4
+ * the LATER escalation threshold (STARVE_ESCALATION_MS), naming the project
5
+ * and the hold reason scheduler.cjs's runStarveEscalationSweep already
6
+ * recorded on the matching 'project_starve_escalated' audit event — health
7
+ * runs as its own cold process with no access to the live scheduler's
8
+ * in-memory `lastTick`, so the durable audit-log.jsonl trail is the only
9
+ * place that reason survives to be read from here.
10
+ *
11
+ * Exercises evaluateStarveEscalationHealth() and
12
+ * latestStarveEscalationReasons() directly — pure/fs-scoped, matching every
13
+ * other evaluate* helper's test pattern in this file (see
14
+ * health-queue-dispatch.test.cjs's header). `project_starve_escalation` is
15
+ * wired into health.cjs's `criticalComponents` gate, so `ok: false` here is
16
+ * exactly what flips `npm run health`'s exit code from 0 to 1.
17
+ *
18
+ * Run: timeout 120 npx vitest run src/main/__tests__/health-starve-escalation.test.cjs
19
+ */
20
+
21
+ 'use strict';
22
+
23
+ import { test, expect } from 'vitest';
24
+ const fs = require('node:fs');
25
+ const os = require('node:os');
26
+ const path = require('node:path');
27
+
28
+ const {
29
+ evaluateStarveEscalationHealth,
30
+ latestStarveEscalationReasons,
31
+ } = require('../health.cjs');
32
+ const { STARVE_ESCALATION_MS } = require('../scheduler.cjs');
33
+
34
+ const CWD = '/home/bilko/Projects/Bilko';
35
+ const NOW = Date.parse('2026-09-12T20:00:00.000Z');
36
+ const idleFor = (ms) => NOW - ms;
37
+
38
+ test('a project starved past the escalation threshold is non-GREEN and names the project + hold reason', () => {
39
+ const jobs = [{ slug: 'a', cwd: CWD, status: 'pending', createdAt: new Date(idleFor(STARVE_ESCALATION_MS + 60_000)).toISOString() }];
40
+ // findStarvedProjects requires some OTHER project to be running — an idle
41
+ // machine is not starvation, only a passed-over project is.
42
+ const allJobs = [...jobs, { slug: 'other', cwd: '/home/bilko/Projects/other', status: 'running' }];
43
+ const result = evaluateStarveEscalationHealth(allJobs, NOW, STARVE_ESCALATION_MS, { [CWD]: 'slots-exhausted' });
44
+ expect(result.ok).toBe(false);
45
+ expect(result.projects).toEqual([
46
+ expect.objectContaining({ cwd: CWD, pendingCount: 1, holdReason: 'slots-exhausted' }),
47
+ ]);
48
+ expect(result.message).toMatch(new RegExp(CWD.replace(/\//g, '\\/')));
49
+ expect(result.message).toMatch(/slots-exhausted/);
50
+ });
51
+
52
+ test('a starved project with no recorded escalation reason yet reports "unknown", not a throw', () => {
53
+ const allJobs = [
54
+ { slug: 'a', cwd: CWD, status: 'pending', createdAt: new Date(idleFor(STARVE_ESCALATION_MS + 1)).toISOString() },
55
+ { slug: 'other', cwd: '/home/bilko/Projects/other', status: 'running' },
56
+ ];
57
+ const result = evaluateStarveEscalationHealth(allJobs, NOW, STARVE_ESCALATION_MS, {});
58
+ expect(result.ok).toBe(false);
59
+ expect(result.projects[0].holdReason).toBe('unknown');
60
+ });
61
+
62
+ test('a starve below the escalation threshold stays GREEN', () => {
63
+ const allJobs = [
64
+ { slug: 'a', cwd: CWD, status: 'pending', createdAt: new Date(idleFor(STARVE_ESCALATION_MS - 60_000)).toISOString() },
65
+ { slug: 'other', cwd: '/home/bilko/Projects/other', status: 'running' },
66
+ ];
67
+ const result = evaluateStarveEscalationHealth(allJobs, NOW, STARVE_ESCALATION_MS, {});
68
+ expect(result).toEqual({ ok: true });
69
+ });
70
+
71
+ test('nothing pending anywhere stays GREEN', () => {
72
+ const result = evaluateStarveEscalationHealth([], NOW, STARVE_ESCALATION_MS, {});
73
+ expect(result).toEqual({ ok: true });
74
+ });
75
+
76
+ test('latestStarveEscalationReasons reads the LAST recorded holdReason per cwd from the audit log', () => {
77
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'starve-health-audit-'));
78
+ const auditPath = path.join(dir, 'audit-log.jsonl');
79
+ const lines = [
80
+ { kind: 'project_starved', cwd: CWD, ageMs: 1000 }, // different kind — must be ignored
81
+ { kind: 'project_starve_escalated', cwd: CWD, holdReason: 'memory-deferred' },
82
+ { kind: 'project_starve_escalated', cwd: CWD, holdReason: 'load-deferred' }, // latest wins
83
+ { kind: 'project_starve_escalated', cwd: '/home/bilko/Projects/other', holdReason: 'held' },
84
+ ];
85
+ fs.writeFileSync(auditPath, lines.map((l) => JSON.stringify(l)).join('\n') + '\n');
86
+
87
+ const reasons = latestStarveEscalationReasons(auditPath);
88
+ expect(reasons[CWD]).toBe('load-deferred');
89
+ expect(reasons['/home/bilko/Projects/other']).toBe('held');
90
+ });
91
+
92
+ test('latestStarveEscalationReasons returns {} for a missing audit log, never a throw', () => {
93
+ expect(latestStarveEscalationReasons('/nonexistent/path/audit-log.jsonl')).toEqual({});
94
+ });
@@ -0,0 +1,31 @@
1
+ /**
2
+ * loadGateDetailTick.test.cjs — unit test for the human-readable detail
3
+ * string a `reason: 'load-deferred'` tick surfaces to the renderer via
4
+ * lastTick.detail (buildScheduleStatePayload). Extracted as a pure function
5
+ * (formatLoadGateDetail) so this is testable without driving tickQueue's
6
+ * full fs/worktree machinery or launching the app.
7
+ *
8
+ * Run: timeout 300 npx vitest run src/main/__tests__/loadGateDetailTick.test.cjs
9
+ */
10
+
11
+ 'use strict';
12
+
13
+ import { test, expect } from 'vitest';
14
+ const { formatLoadGateDetail } = require('../scheduler.cjs');
15
+
16
+ test('names the load gate, the measured ratio, the threshold and how long it has been held', () => {
17
+ const detail = formatLoadGateDetail({
18
+ loadavg1: 13.31,
19
+ cores: 14,
20
+ ratio: 0.951,
21
+ threshold: 0.85,
22
+ gatedSinceMs: 83 * 60_000,
23
+ });
24
+
25
+ expect(detail).toMatch(/load gate/i);
26
+ expect(detail).toContain('13.31');
27
+ expect(detail).toContain('14');
28
+ expect(detail).toContain('0.951');
29
+ expect(detail).toContain('0.85');
30
+ expect(detail).toMatch(/83m/);
31
+ });
@@ -7,7 +7,7 @@
7
7
 
8
8
  import { test, expect } from 'vitest';
9
9
  const os = require('node:os');
10
- const { buildMachineProfile, computeMachineDigest, resolveInstallChannel } = require('../lib/machineProfile.cjs');
10
+ const { buildMachineProfile, computeMachineDigest, resolveInstallChannel, resolveEnv } = require('../lib/machineProfile.cjs');
11
11
 
12
12
  function fakeOs(overrides = {}) {
13
13
  return {
@@ -132,3 +132,21 @@ test('resolveInstallChannel: dev when SM_DEV flag is set, regardless of app path
132
132
  test('resolveInstallChannel: unknown otherwise', () => {
133
133
  expect(resolveInstallChannel({ appPath: '/opt/some/app', devFlag: false })).toBe('unknown');
134
134
  });
135
+
136
+ // ─── resolveEnv (wire-level env discriminator) ──────────────────────────
137
+
138
+ test('resolveEnv: test when running under a test runner, regardless of installChannel', () => {
139
+ expect(resolveEnv({ isTestRunner: true, installChannel: 'dev' })).toBe('test');
140
+ expect(resolveEnv({ isTestRunner: true, installChannel: 'npx' })).toBe('test');
141
+ expect(resolveEnv({ isTestRunner: true, installChannel: 'unknown' })).toBe('test');
142
+ });
143
+
144
+ test('resolveEnv: dev when installChannel is dev and not under a test runner', () => {
145
+ expect(resolveEnv({ isTestRunner: false, installChannel: 'dev' })).toBe('dev');
146
+ });
147
+
148
+ test('resolveEnv: prod for every other installChannel (npx, unknown) when not under a test runner', () => {
149
+ expect(resolveEnv({ isTestRunner: false, installChannel: 'npx' })).toBe('prod');
150
+ expect(resolveEnv({ isTestRunner: false, installChannel: 'unknown' })).toBe('prod');
151
+ expect(resolveEnv({ isTestRunner: false, installChannel: undefined })).toBe('prod');
152
+ });