claude-code-session-manager 0.35.14 → 0.35.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.html CHANGED
@@ -7,7 +7,7 @@
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-DqWSSMkp.js"></script>
10
+ <script type="module" crossorigin src="./assets/index-DRSHENEg.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
13
  <link rel="stylesheet" crossorigin href="./assets/index-B39Qiq0n.css">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-session-manager",
3
- "version": "0.35.14",
3
+ "version": "0.35.15",
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",
@@ -1,16 +1,20 @@
1
1
  ---
2
2
  name: process-feedback
3
3
  description: >-
4
- Process the current project's inbound feedback folder end-to-end: read every
5
- open item, evaluate it against real code, and for work that belongs to this
4
+ Process the current project's inbound feedback folder end-to-end: first sync
5
+ any open GitHub issues on the project's repo into the intake as new feedback
6
+ files (deduped against already-tracked issues), then read every open item,
7
+ evaluate it against real code, and for work that belongs to this
6
8
  project queue it as scheduled PRDs via /develop (never implement inline),
7
9
  archive the item as processed **the moment it is queued** (the scheduler owns
8
10
  execution from there), and fold lessons back into the folder's README so
9
- future feedback (written by other agents/projects) gets better. Use whenever the user says "/process-feedback",
11
+ future feedback (written by other agents/projects, or GitHub issue filers)
12
+ gets better. Use whenever the user says "/process-feedback",
10
13
  "review the feedback folder", "work through the feedback", "any open
11
- feedback?", or drops new files into session-manager-operations/feedback/.
14
+ feedback?", "check the GitHub issues", "triage the GitHub repo", or drops
15
+ new files into session-manager-operations/feedback/.
12
16
  Keywords: feedback, intake, cross-project requests, process feedback, triage
13
- feedback.
17
+ feedback, GitHub issues.
14
18
  model: opus
15
19
  ---
16
20
 
@@ -68,7 +72,49 @@ Before anything else, check for the pre-migration layout: if a legacy
68
72
  `git mv feedback session-manager-operations/feedback` when the repo is
69
73
  git-tracked, else a plain `mv`. This converges any project this skill runs in
70
74
  to the new layout even if its bulk-relocation PRD hasn't landed yet. Only then
71
- continue with step 0.
75
+ continue with step 0b.
76
+
77
+ ### 0b. Sync open GitHub issues into the intake (source sync)
78
+
79
+ For a project with a GitHub remote, pull open issues and materialize any not already tracked as a
80
+ new feedback file — **before** the quick-exit check below evaluates what's open, since this step
81
+ can create new open items that check needs to see. This lets a repo's GitHub issue tracker feed
82
+ the same triage → queue pipeline without a human manually copying issues into the feedback folder.
83
+
84
+ - Skip silently (no error, no report) if `gh repo view --json nameWithOwner -q .nameWithOwner`
85
+ fails — no GitHub remote or `gh` unauthenticated. This step is opportunistic, not required.
86
+ - Fetch open issues via `gh api repos/<owner>/<repo>/issues --paginate --jq '...'` (per-issue
87
+ `number,title,body,labels`) rather than `gh issue list`/`gh issue view` — the latter two can hit
88
+ a known `gh` CLI bug on repos with legacy GitHub Projects (classic) boards
89
+ (`GraphQL: Projects (classic) is being deprecated ... (repository.issue.projectCards)`,
90
+ documented in `standards.md`'s Execution discipline section); `gh api` sidesteps it outright by
91
+ never touching the deprecated field. Note `gh api .../issues` also returns pull requests (they
92
+ have an `issue.pull_request` key) — filter those out, this step is issues only.
93
+ - **Dedup before creating anything.** An issue already tracked has a feedback file (open OR
94
+ already in `processed/`) containing the token `gh-issue-<N>` — `grep -rl "gh-issue-<N>"
95
+ session-manager-operations/feedback/` (recursive, covers both the inbox and `processed/`) before
96
+ materializing issue `N`. Skip any issue that already has a match.
97
+ - For each untracked open issue, create
98
+ `session-manager-operations/feedback/<yyyy-mm-dd>-gh-issue-<N>-<slug>.md` following the folder's
99
+ normal frontmatter convention (see `README.md`):
100
+ - `title`: the issue title, verbatim.
101
+ - `source`: `GitHub issue gh-issue-<N> (<issue URL>)` — the `gh-issue-<N>` token here is what
102
+ the dedup grep matches on next run; don't reword it out.
103
+ - `type`: infer from labels (`bug` label → `bug`; `enhancement`/no matching label → `enhancement`;
104
+ `security` → `security`; `performance` → `performance`).
105
+ - `severity`: `normal` unless the issue's own labels/title clearly indicate `blocker`/`high`
106
+ (data loss, crash, security hole, or an unusable feature with no workaround — same bar as the
107
+ README's own severity guidance; don't inflate).
108
+ - Body: `# What happens / what's missing` = the issue body (verbatim, or a faithful trim if very
109
+ long — don't paraphrase away specifics); `# Evidence` = the issue URL + number, plus any
110
+ screenshot/repro references already in the issue body; `# Suggested direction (optional)` =
111
+ any proposed-solution section already present in the issue body.
112
+ - Synced files then flow through steps 1–6 below exactly like any other feedback file — no
113
+ special-casing after creation, same evaluate → queue → disposition → archive pipeline.
114
+ - When step 4 dispositions a synced issue by queuing a PRD, the `## RESOLUTION` must also state
115
+ the originating issue number/URL. This skill does **not** auto-close or auto-comment on the
116
+ GitHub issue — that stays a human (or a later, deliberate step) decision once the fix actually
117
+ ships, not something this sync step does on queue.
72
118
 
73
119
  ### 0. Quick-exit — bail in milliseconds if there's nothing to do
74
120
 
@@ -0,0 +1,46 @@
1
+ /**
2
+ * scheduler-force-tick-outcome.test.cjs — unit tests for forceTickOutcome().
3
+ *
4
+ * Covers the "No pending jobs" vs "Already running" ambiguity: when
5
+ * pickNextBatch() returns an empty batch with no hold reason, that's only
6
+ * genuinely "no pending jobs" if nothing is running either — if a job
7
+ * already transitioned to running (e.g. the when-available poll won a race
8
+ * against a manual force-tick click), the message must say so instead of
9
+ * flatly claiming nothing is pending.
10
+ *
11
+ * Run: timeout 120 node --test src/main/__tests__/scheduler-force-tick-outcome.test.cjs
12
+ */
13
+
14
+ 'use strict';
15
+
16
+ const { test } = require('node:test');
17
+ const assert = require('node:assert/strict');
18
+ const { forceTickOutcome } = require('../scheduler.cjs');
19
+
20
+ test('forceTickOutcome: null result → No pending jobs', () => {
21
+ const outcome = forceTickOutcome(null);
22
+ assert.strictEqual(outcome.message, 'No pending jobs');
23
+ });
24
+
25
+ test('forceTickOutcome: fired → Fired N job(s)', () => {
26
+ const outcome = forceTickOutcome({ fired: true, count: 2, group: 0 });
27
+ assert.strictEqual(outcome.message, 'Fired 2 job(s) (group g0)');
28
+ });
29
+
30
+ test('forceTickOutcome: reason=drained (genuinely nothing pending or running) → No pending jobs', () => {
31
+ const outcome = forceTickOutcome({ fired: false, reason: 'drained' });
32
+ assert.strictEqual(outcome.message, 'No pending jobs');
33
+ });
34
+
35
+ test('forceTickOutcome: reason=already-running (race: poll fired it first) → Already running message, not "No pending jobs"', () => {
36
+ const outcome = forceTickOutcome({ fired: false, reason: 'already-running', runningCount: 1 });
37
+ assert.strictEqual(outcome.message, 'Already running — 1 job(s) in flight');
38
+ assert.notStrictEqual(outcome.message, 'No pending jobs');
39
+ });
40
+
41
+ test('forceTickOutcome: reason=held → detail message surfaces', () => {
42
+ const outcome = forceTickOutcome({ fired: false, reason: 'held', detail: '[scheduler] concurrency [/a]: cap reached, no slots' });
43
+ assert.strictEqual(outcome.message, 'cap reached, no slots');
44
+ });
45
+
46
+ console.log('scheduler-force-tick-outcome tests: PASS');
@@ -1697,6 +1697,20 @@ function tickQueue() {
1697
1697
  console.log(`[scheduler] dod-drain: ${err?.message ?? String(err)}`);
1698
1698
  });
1699
1699
  if (holdReason) return { fired: false, reason: 'held', detail: holdReason };
1700
+ // Distinguish "genuinely nothing to do" from "the batch was already
1701
+ // fired by a concurrent tickQueue() call" (e.g. the periodic
1702
+ // when-available poll winning a race against a manual force-tick —
1703
+ // pickNextBatch correctly returns reason:null in both cases since
1704
+ // neither is a hold; the caller-facing message needs to tell them
1705
+ // apart so "No pending jobs" doesn't read as wrong next to a job
1706
+ // that is visibly running).
1707
+ const runningCount = Math.max(
1708
+ runningSet.size,
1709
+ state.jobs.filter((j) => j.status === 'running').length,
1710
+ );
1711
+ if (runningCount > 0) {
1712
+ return { fired: false, reason: 'already-running', runningCount };
1713
+ }
1700
1714
  return { fired: false, reason: 'drained' };
1701
1715
  }
1702
1716
 
@@ -1748,6 +1762,8 @@ function forceTickOutcome(result) {
1748
1762
  switch (result.reason) {
1749
1763
  case 'drained':
1750
1764
  return { ok: true, kind: 'info', message: 'No pending jobs' };
1765
+ case 'already-running':
1766
+ return { ok: true, kind: 'info', message: `Already running — ${result.runningCount} job(s) in flight` };
1751
1767
  case 'paused':
1752
1768
  return { ok: true, kind: 'warn', message: 'Scheduler is paused' };
1753
1769
  case 'cancelled':
@@ -2403,10 +2419,10 @@ function registerScheduleHandlers() {
2403
2419
  });
2404
2420
  await broadcast();
2405
2421
  let message;
2406
- if (added === 0 && removed === 0) message = 'Rescanned — no changes';
2407
- else if (added > 0 && removed === 0) message = `Rescanned — ${added} new PRD(s) picked up`;
2408
- else if (added === 0 && removed > 0) message = `Rescanned — ${removed} PRD(s) removed from disk`;
2409
- else message = `Rescanned — ${added} new PRD(s) picked up, ${removed} removed from disk`;
2422
+ if (added === 0 && removed === 0) message = 'Rescanned — no new/removed PRD files';
2423
+ else if (added > 0 && removed === 0) message = `Rescanned — ${added} new PRD file(s) picked up`;
2424
+ else if (added === 0 && removed > 0) message = `Rescanned — ${removed} PRD file(s) removed from disk`;
2425
+ else message = `Rescanned — ${added} new PRD file(s) picked up, ${removed} removed from disk`;
2410
2426
  return { ok: true, kind: 'info', message };
2411
2427
  });
2412
2428
 
@@ -2825,4 +2841,4 @@ const remote = {
2825
2841
  },
2826
2842
  };
2827
2843
 
2828
- module.exports = { registerScheduleHandlers, attachWindow, init, ROOT, PRDS_DIR, selectHistoryJobs, parsePorcelain, FINISH_PROTOCOL, remote, pickNextBatch, pickForProject, reapDeadRunningJobs, pollRecoveryClearSource, memoryLimitedBatchSize, availableForJobs, reverifyNeedsReview, isRescanCandidate, isPromotableOriginal, selectAutoFixTargets, isEligibleForImmediateAutoFix, resolveRunId, isUnresolvableNeedsReview, healTargetForFix, buildInvestigationPrompt, committedInWindow, isFixPlanSlug, isFixPlanBeyondDepthCap, MAX_INVESTIGATION_DEPTH };
2844
+ module.exports = { registerScheduleHandlers, attachWindow, init, ROOT, PRDS_DIR, selectHistoryJobs, parsePorcelain, FINISH_PROTOCOL, remote, pickNextBatch, pickForProject, reapDeadRunningJobs, pollRecoveryClearSource, memoryLimitedBatchSize, availableForJobs, reverifyNeedsReview, isRescanCandidate, isPromotableOriginal, selectAutoFixTargets, isEligibleForImmediateAutoFix, resolveRunId, isUnresolvableNeedsReview, healTargetForFix, buildInvestigationPrompt, committedInWindow, isFixPlanSlug, isFixPlanBeyondDepthCap, MAX_INVESTIGATION_DEPTH, forceTickOutcome };