@cullumco/cadence 0.1.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.
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "cadence",
3
+ "description": "Ambient context plugins from Cullum&Co.",
4
+ "owner": {
5
+ "name": "Cullum&Co",
6
+ "email": "scott@cullum.co"
7
+ },
8
+ "plugins": [
9
+ {
10
+ "name": "cadence",
11
+ "displayName": "Cadence",
12
+ "description": "Agents that read the room: embodied state, cadence dials, and conservative finish-line guardrails for Claude Code.",
13
+ "source": {
14
+ "source": "npm",
15
+ "package": "@cullumco/cadence"
16
+ },
17
+ "category": "Productivity",
18
+ "tags": [
19
+ "context",
20
+ "hooks",
21
+ "agents",
22
+ "claude-code"
23
+ ],
24
+ "homepage": "https://github.com/cullumco/cadence",
25
+ "repository": "https://github.com/cullumco/cadence",
26
+ "license": "MIT"
27
+ }
28
+ ]
29
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "cadence",
3
+ "displayName": "Cadence",
4
+ "version": "0.1.0",
5
+ "description": "Ambient context for Claude Code: embodied signals, cadence dials, and finish-line guardrails.",
6
+ "author": {
7
+ "name": "Cullum&Co",
8
+ "url": "https://cullum.co"
9
+ },
10
+ "homepage": "https://github.com/cullumco/cadence",
11
+ "repository": "https://github.com/cullumco/cadence",
12
+ "license": "MIT",
13
+ "keywords": [
14
+ "context",
15
+ "hooks",
16
+ "cadence",
17
+ "ambient-context",
18
+ "claude-code"
19
+ ]
20
+ }
package/ALPHA.md ADDED
@@ -0,0 +1,84 @@
1
+ # Cadence Alpha
2
+
3
+ Cadence is in Claude Code alpha. The product goal is simple: make the agent feel
4
+ less deaf to the room around the prompt.
5
+
6
+ ## Install
7
+
8
+ The canonical install is the Claude Code marketplace:
9
+
10
+ ```text
11
+ /plugin marketplace add cullumco/cadence
12
+ /plugin install cadence@cadence
13
+ /reload-plugins
14
+ /cadence:try
15
+ ```
16
+
17
+ `marketplace.json` currently sources the plugin from `npm:@cullumco/cadence`.
18
+ `/plugin marketplace add` succeeds today (the repo is live), but
19
+ `/plugin install` will fail to fetch until the npm package is published — use
20
+ "Run from source" below as the alpha fallback in the meantime.
21
+
22
+ After install, set a self-report and try something that would normally trigger
23
+ a soft handoff:
24
+
25
+ ```text
26
+ /cadence:state shipping, locked in
27
+ ```
28
+
29
+ ```text
30
+ clean up the obvious rough edges here
31
+ ```
32
+
33
+ Cadence should add a `<user_state>` block before the prompt and, when you are in
34
+ a shipping cadence, the Stop hook should discourage permission-seeking endings.
35
+
36
+ ## Run from source
37
+
38
+ For alpha testers who want to iterate on the code, or to bypass the npm-publish
39
+ gap on the marketplace path:
40
+
41
+ ```bash
42
+ git clone https://github.com/cullumco/cadence ~/cadence
43
+ cd ~/cadence
44
+ npm install
45
+ npm run verify:alpha
46
+ claude --plugin-dir ~/cadence
47
+ ```
48
+
49
+ Inside Claude Code: `/cadence:try`, then `/cadence:state shipping, locked in`.
50
+
51
+ ## Release Gate
52
+
53
+ ```bash
54
+ npm run verify:alpha
55
+ npm publish --dry-run
56
+ ```
57
+
58
+ `verify:alpha` validates plugin manifests, runs tests, dry-packs the npm package,
59
+ checks required plugin files, installs the packed tarball into a temporary
60
+ consumer project, verifies the installed plugin layout, and smoke-tests the
61
+ installed `cadence` binary plus prompt hook.
62
+
63
+ CI runs the same gate on GitHub Actions via `.github/workflows/alpha.yml`.
64
+
65
+ ## Publish Command
66
+
67
+ Once npm is authenticated with an account that can publish `@cullumco/cadence`:
68
+
69
+ ```bash
70
+ npm run release:alpha
71
+ ```
72
+
73
+ That command runs the full alpha gate, confirms npm auth, and publishes the
74
+ package. `npm publish` also has a `prepublishOnly` gate as a last line of
75
+ defense.
76
+
77
+ ## Current External Requirements
78
+
79
+ - An npm account with access to the `@cullumco` scope must publish the package.
80
+
81
+ Current state:
82
+ - GitHub: https://github.com/cullumco/cadence is live (public, MIT). CI
83
+ runs `npm run verify:alpha` on every push to `main`.
84
+ - npm: `@cullumco/cadence` is not published yet.
package/README.md ADDED
@@ -0,0 +1,204 @@
1
+ # cadence
2
+
3
+ > Agents that read the room.
4
+ > Claude Code has one input channel: text. Cadence is the second.
5
+
6
+ Cadence is an ambient context layer for agents. The current alpha surface is a
7
+ Claude Code hook: it injects your current **embodied state** — what you're
8
+ listening to, what you told it, how you want it to respond — into every prompt,
9
+ then asks Claude to *read your prompt through that lens*. The agent stops being
10
+ deaf to the room.
11
+
12
+ A [Cullum&Co](https://cullum.co) project.
13
+
14
+ ## What it does
15
+
16
+ Before Claude sees your prompt, Cadence injects a `<user_state>` block:
17
+
18
+ ```
19
+ <user_state>
20
+ signals:
21
+ music: "Loose" — Daniel Caesar (Spotify)
22
+ vibe: sexy, chilled
23
+ self_report: "two beers, shipping"
24
+ cadence: # inferred from signals, advisory
25
+ { pace=fast tone=warm posture=decisive proactivity=act-freely }
26
+ reframe: read my prompt as someone in this cadence meant it: keep it fast and
27
+ tight — answer first, trim the preamble; make the call rather than offering a
28
+ menu of options; act without stopping to check in; keep the tone warm and
29
+ casual. If my words clearly mean otherwise, follow my words.
30
+ </user_state>
31
+ ```
32
+
33
+ It doesn't constrain the agent or rewrite your prompt — it gives the model the
34
+ context your words are missing, and a lens for reading them. The lens always
35
+ defers to what you actually typed.
36
+
37
+ ## How it works
38
+
39
+ **Signals → dials → a reframe lens.**
40
+
41
+ 1. **Signals** — what Cadence can sense right now:
42
+ - **ambient** — time of day, day of week, weather (opt-in), battery, machine
43
+ uptime/load, dark mode, displays, wifi. Mostly zero-setup, cross-platform.
44
+ The one signal that's always there: `context: friday afternoon, rainy`.
45
+ - **git** — commits this hour, dirty files, mid-merge/rebase, read from the
46
+ project you're in: `git: 6 dirty, mid-conflict`. Cross-platform.
47
+ - **activity** — prompt length and minutes since your last prompt, read from
48
+ the hook payload: `activity: { min_since_prompt=45 prompt_len=123 }`.
49
+ - **music** — what's playing (via macOS now-playing, any player), turned into
50
+ a clean *vibe* (mood words) via [MusicBrainz](https://musicbrainz.org). No
51
+ Spotify login, no API key, no Premium.
52
+ - **self-report** — what you tell it: `cadence state "two beers, shipping"`.
53
+
54
+ Time/day and self-report move the dials; the rest render as context the agent
55
+ reads (flavor). Git's nudges are built but dormant — see `BACKLOG.md`.
56
+ 2. **Dials** — four independent knobs, each `low | medium | high`, inferred from
57
+ the signals (or pinned by you):
58
+ - **pace** — deliberate ↔ fast
59
+ - **tone** — warm ↔ crisp
60
+ - **posture** — exploratory ↔ decisive
61
+ - **proactivity** — ask-first ↔ act-freely
62
+ 3. **Reframe** — a sentence composed from the dials telling the agent how to
63
+ *read* your prompt. Generated fresh each time; always ends "if my words
64
+ clearly mean otherwise, follow my words."
65
+
66
+ The dials are independent on purpose — high-energy-but-mellow music can read as
67
+ "fast pace, warm tone," something a single ship/think/debug label could never
68
+ express.
69
+
70
+ ## Requirements
71
+
72
+ - **macOS** (the now-playing reader uses AppleScript against Spotify.app /
73
+ Music.app). The self-report and dials work everywhere; only the music signal
74
+ is macOS-only.
75
+ - **Node 20+**
76
+ - Claude Code for the alpha adapter
77
+
78
+ ## Install
79
+
80
+ In Claude Code:
81
+
82
+ ```text
83
+ /plugin marketplace add cullumco/cadence
84
+ /plugin install cadence@cadence
85
+ /reload-plugins
86
+ /cadence:try
87
+ ```
88
+
89
+ Then set a self-report so you can feel the difference:
90
+
91
+ ```text
92
+ /cadence:state shipping, locked in
93
+ ```
94
+
95
+ Alpha testers running from source — while `@cullumco/cadence` is pending npm
96
+ publish — see [`ALPHA.md`](ALPHA.md).
97
+
98
+ The prompt hook has a ~1.5s budget and exits silently when it has nothing to
99
+ say, so it never blocks or slows a prompt. The Stop hook is conservative: it
100
+ only intervenes when you're explicitly in a shipping / act-freely cadence and
101
+ Claude tries to end with a soft handoff like "want me to do that next?"
102
+
103
+ ### Music vibe (optional, macOS only)
104
+
105
+ Nothing to set up. If Spotify.app or Music.app is playing, Cadence reads the
106
+ track, looks the artist's vibe up on MusicBrainz once, and caches it forever at
107
+ `~/.cadence/vibe-cache.json`. If nothing's playing, the music signal is simply
108
+ absent.
109
+
110
+ ## Daily use
111
+
112
+ ```bash
113
+ cadence state "two beers, shipping" # set self-reported state (expires in 4h)
114
+ cadence state # print current self-report
115
+ cadence clear # clear it
116
+ cadence test # preview exactly what the hook would inject
117
+ ```
118
+
119
+ From inside Claude Code, the plugin skill gives the same self-report path:
120
+
121
+ ```text
122
+ /cadence:state two beers, shipping
123
+ /cadence:try
124
+ ```
125
+
126
+ ### Driving the dials by hand
127
+
128
+ The dials are inferred, but you can pin any of them — your pin wins, the rest
129
+ keep inferring:
130
+
131
+ ```bash
132
+ cadence dials # show the mixing board and what's pinned
133
+ cadence set pace fast # pin a dial (accepts words OR low|medium|high)
134
+ cadence set tone warm
135
+ cadence unset pace # back to inferred
136
+ cadence unset all
137
+ ```
138
+
139
+ Pinned dials show with a `*` in the block so Claude knows they're your explicit
140
+ choice, not a guess. You can also pin per-session with env vars:
141
+ `CADENCE_PACE=fast`, `CADENCE_TONE=warm`, etc.
142
+
143
+ ## The file that matters: `src/cadence.ts`
144
+
145
+ `deriveCadence()` maps your signals to the four dials, and `buildReframe()`
146
+ composes the lens. That's where your taste lives — which signal moves which dial,
147
+ and how the lens reads. A working baseline ships so it runs end-to-end
148
+ immediately; the mapping is opinionated and meant to be yours.
149
+
150
+ ## Adapter posture
151
+
152
+ Claude Code is the alpha surface, not the whole product. The agnostic product
153
+ shape is:
154
+
155
+ ```
156
+ signals -> cadence dials -> context envelope -> adapter-specific delivery
157
+ ```
158
+
159
+ Today the adapter-specific delivery is Claude Code's `UserPromptSubmit` and
160
+ `Stop` hooks. The core signal types, cadence derivation, reframe lens, and
161
+ rendering are kept separate so future adapters can deliver the same cadence
162
+ state through other agent surfaces.
163
+
164
+ ## Alpha release checklist
165
+
166
+ Before publishing:
167
+
168
+ ```bash
169
+ npm run verify:alpha
170
+ npm publish --dry-run
171
+ npm run release:alpha
172
+ ```
173
+
174
+ The package is scoped and configured for public npm publish via
175
+ `publishConfig.access = "public"`. The repo already ships
176
+ `.claude-plugin/marketplace.json`, so once `@cullumco/cadence` lands on npm,
177
+ the canonical install at the top of this README starts working end-to-end.
178
+
179
+ A GitHub Actions workflow exists at `.github/workflows/alpha.yml` but is
180
+ currently disabled — re-enable with `gh workflow enable Alpha` when you want
181
+ the gate to run on every push to `main`.
182
+
183
+ ## What's next
184
+
185
+ See [`BACKLOG.md`](BACKLOG.md). Highlights:
186
+
187
+ - **More signals** — stronger `git` nudges, calendar density, wifi/place.
188
+ Git is the highest-value one: it moves the dials from *what you said* to *what
189
+ you're actually doing*.
190
+ - **After-the-fact injection** — refine the cadence mid-task (`PostToolUse`),
191
+ building on the conservative finish-line `Stop` guard that now ships.
192
+ - **Opt-in flavor providers** — horoscope, moon phase, for those who want them.
193
+
194
+ ## Caveats
195
+
196
+ - **macOS-only music.** The now-playing reader is AppleScript. Other platforms
197
+ get self-report + dials, just no music vibe.
198
+ - **Spotify's Web API is not used** and not needed — Spotify deprecated audio
199
+ features for new apps (2024) and gated dev-mode behind Premium (2026). Cadence
200
+ reads what's playing at the OS level instead.
201
+
202
+ ## License
203
+
204
+ MIT
package/bin/cadence ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import("../dist/cli.js").catch((err) => {
3
+ console.error(err);
4
+ process.exit(1);
5
+ });
package/bin/cadence.js ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import("../dist/cli.js").catch((err) => {
3
+ console.error(err);
4
+ process.exit(1);
5
+ });
@@ -0,0 +1,187 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ export const DIALS = ["pace", "tone", "posture", "proactivity"];
5
+ const LEVELS = ["low", "medium", "high"];
6
+ /* ─────────────────────────────────────────────────────────────────────────
7
+ * SCOTT — this is now THE file (it replaced mode.ts).
8
+ *
9
+ * Instead of collapsing everything into ship/think/debug, Cadence drives four
10
+ * INDEPENDENT dials. deriveCadence() is where your taste decides which signal
11
+ * moves which dial. That's the personality of the product.
12
+ *
13
+ * The dials (each low | medium | high):
14
+ * pace how fast/terse vs deliberate/expansive the reply should be
15
+ * tone warm/casual vs crisp/professional voice
16
+ * posture exploratory (options, tradeoffs) vs decisive (make the call)
17
+ * proactivity ask-before-acting vs act-without-checking-in
18
+ *
19
+ * Signals you can read (any subset present):
20
+ * report { text } ← you said it; trust it most
21
+ * music { vibe, energy } ← energy 0–1 drives pace; vibe colors tone
22
+ * git { commitsLastHour, ... } ← work rhythm (when the provider lands)
23
+ * activity { minSinceLastPrompt, promptLength }
24
+ *
25
+ * A working baseline is below so it runs end-to-end. The mapping is yours.
26
+ * ───────────────────────────────────────────────────────────────────────── */
27
+ // Human-facing word for each dial at each level (rendered in the block).
28
+ export const DIAL_WORDS = {
29
+ pace: { low: "deliberate", medium: "steady", high: "fast" },
30
+ tone: { low: "warm", medium: "neutral", high: "crisp" },
31
+ posture: { low: "exploratory", medium: "balanced", high: "decisive" },
32
+ proactivity: { low: "ask-first", medium: "balanced", high: "act-freely" },
33
+ };
34
+ export function deriveCadence(state) {
35
+ const music = state.signals.find((s) => s.source === "music");
36
+ const report = state.signals.find((s) => s.source === "self_report");
37
+ const git = state.signals.find((s) => s.source === "git");
38
+ const activity = state.signals.find((s) => s.source === "activity");
39
+ const ambient = state.signals.find((s) => s.source === "ambient");
40
+ // Start neutral; each signal nudges individual dials.
41
+ const c = {
42
+ pace: "medium",
43
+ tone: "medium",
44
+ posture: "medium",
45
+ proactivity: "medium",
46
+ };
47
+ // ── ambient → soft nudges FIRST (weakest), so stronger signals below win ──
48
+ // Atmosphere, not orders: it colors the default, then music/self-report/git
49
+ // can override. "It's late" shouldn't beat "I'm shipping."
50
+ if (ambient) {
51
+ if (ambient.hour >= 22 || ambient.hour < 6)
52
+ c.pace = "low"; // late → gentler
53
+ if (ambient.partOfDay === "early morning")
54
+ c.pace = "low"; // easing in
55
+ if (ambient.isWeekend)
56
+ c.tone = "low"; // looser on weekends
57
+ if (ambient.weather && /rain|snow|fog|storm|cloud/.test(ambient.weather)) {
58
+ c.tone = "low"; // gloomy out → warmer in
59
+ }
60
+ if (ambient.onBattery)
61
+ c.pace = "high"; // mobile/untethered → quick hits
62
+ }
63
+ // ── music energy → pace (only pace; leave tone/posture to other signals) ──
64
+ if (music?.energy != null) {
65
+ if (music.energy >= 0.7)
66
+ c.pace = "high";
67
+ else if (music.energy <= 0.4)
68
+ c.pace = "low";
69
+ }
70
+ // mellow/organic vibe words warm the tone
71
+ if (music?.vibe && /\b(calm|chilled|ethereal|romantic|warm)\b/.test(music.vibe)) {
72
+ c.tone = "low";
73
+ }
74
+ // ── self-report → posture / proactivity / tone (you know your state) ──────
75
+ if (report) {
76
+ const t = report.text.toLowerCase();
77
+ if (/\b(ship|shipping|jamming|locked.?in|sending|grind|just|send it)\b/.test(t)) {
78
+ c.posture = "high";
79
+ c.proactivity = "high";
80
+ c.pace = "high";
81
+ }
82
+ if (/\b(think|thinking|exploring|planning|deciding|tradeoff|figuring|weigh)\b/.test(t)) {
83
+ c.posture = "low";
84
+ c.pace = "low";
85
+ }
86
+ if (/\b(stuck|broken|confused|debug|wtf|borked|why)\b/.test(t)) {
87
+ c.posture = "low"; // lead with hypotheses, don't take framing at face value
88
+ c.proactivity = "low"; // verify before acting
89
+ }
90
+ if (/\b(beers?|tired|late|chill|relaxed|cozy)\b/.test(t))
91
+ c.tone = "low";
92
+ if (/\b(focused|formal|work|serious|crunch)\b/.test(t))
93
+ c.tone = "high";
94
+ }
95
+ // ── git: FLAVOR ONLY for now (renders as context, no dial nudges yet) ─────
96
+ // Candidate nudges, deliberately dormant until we've watched real output:
97
+ // conflicted → proactivity low (verify, don't barrel)
98
+ // commitsLastHour >= 3 → pace high (flow state)
99
+ // See BACKLOG: turn these on once the flavor proves trustworthy.
100
+ void git;
101
+ // ── activity → pace (returning from a break = slow back down) ─────────────
102
+ if (activity?.minSinceLastPrompt != null && activity.minSinceLastPrompt > 30) {
103
+ c.pace = "low";
104
+ }
105
+ return c;
106
+ }
107
+ /* Compose the interpretation lens from the dials. Reads as second-person
108
+ * "how to read my prompt", and always defers to the literal words — because
109
+ * the cadence is inferred and fires on every prompt, so a wrong guess must
110
+ * be cheap. */
111
+ export function buildReframe(c) {
112
+ const parts = [];
113
+ if (c.pace === "high")
114
+ parts.push("keep it fast and tight — answer first, trim the preamble");
115
+ else if (c.pace === "low")
116
+ parts.push("take it slow and expansive — room to lay things out");
117
+ if (c.posture === "high")
118
+ parts.push("make the call rather than offering a menu of options");
119
+ else if (c.posture === "low")
120
+ parts.push("surface the tradeoffs and options behind what I asked");
121
+ if (c.proactivity === "high")
122
+ parts.push("act without stopping to check in");
123
+ else if (c.proactivity === "low")
124
+ parts.push("verify assumptions and lead with hypotheses before acting");
125
+ if (c.tone === "low")
126
+ parts.push("keep the tone warm and casual");
127
+ else if (c.tone === "high")
128
+ parts.push("keep the tone crisp and professional");
129
+ const body = parts.length === 0
130
+ ? "read my prompt at face value"
131
+ : "read my prompt as someone in this cadence meant it: " + parts.join("; ");
132
+ return body + ". If my words clearly mean otherwise, follow my words.";
133
+ }
134
+ const CONFIG_FILE = join(homedir(), ".cadence", "config.json");
135
+ function isLevel(v) {
136
+ return typeof v === "string" && LEVELS.includes(v);
137
+ }
138
+ // Accept EITHER the internal level ("high") or the human word ("fast").
139
+ // This keeps config/env pins aligned with the CLI and the rendered board.
140
+ export function resolveDialLevel(dial, input) {
141
+ if (typeof input !== "string")
142
+ return null;
143
+ const v = input.toLowerCase();
144
+ if (isLevel(v))
145
+ return v;
146
+ for (const lvl of LEVELS) {
147
+ if (DIAL_WORDS[dial][lvl].toLowerCase() === v)
148
+ return lvl;
149
+ }
150
+ return null;
151
+ }
152
+ export async function loadOverrides() {
153
+ const ov = {};
154
+ // config file first (lowest precedence)
155
+ try {
156
+ const cfg = JSON.parse(await readFile(CONFIG_FILE, "utf-8"));
157
+ for (const dial of DIALS) {
158
+ const level = resolveDialLevel(dial, cfg[dial]);
159
+ if (level)
160
+ ov[dial] = level;
161
+ }
162
+ }
163
+ catch {
164
+ // no config file — fine
165
+ }
166
+ // env vars override the file
167
+ for (const dial of DIALS) {
168
+ const level = resolveDialLevel(dial, process.env[`CADENCE_${dial.toUpperCase()}`]);
169
+ if (level)
170
+ ov[dial] = level;
171
+ }
172
+ return ov;
173
+ }
174
+ /** Merge pinned dials over the inferred cadence. Returns the final board and
175
+ * the list of dials that were user-set (so the renderer can mark them). */
176
+ export function applyOverrides(inferred, overrides) {
177
+ const cadence = { ...inferred };
178
+ const pinned = [];
179
+ for (const dial of DIALS) {
180
+ const v = overrides[dial];
181
+ if (v) {
182
+ cadence[dial] = v;
183
+ pinned.push(dial);
184
+ }
185
+ }
186
+ return { cadence, pinned };
187
+ }