@cullumco/cadence 0.1.3 → 0.1.5

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.
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { getMusicSignal } from "./providers/music.js";
3
- import { getSelfReportSignal } from "./providers/selfreport.js";
3
+ import { getSelfReportSignal, STALE_AFTER_MS, REFRESH_SOON_MS } from "./providers/selfreport.js";
4
4
  import { loadOverrides } from "./cadence.js";
5
+ import { isPaused } from "./config.js";
5
6
  import { debug } from "./debug.js";
6
7
  /* ─────────────────────────────────────────────────────────────────────────
7
8
  * Claude Code SessionStart adapter: one short, human-facing line when a
@@ -16,24 +17,50 @@ import { debug } from "./debug.js";
16
17
  const BUDGET_MS = 700;
17
18
  // The voice of the product's first impression. Return null to stay silent.
18
19
  // Default policy: always one line on startup — say what's seen when there
19
- // are signals, point at the inputs when there's nothing yet.
20
+ // are signals, point at the inputs when there's nothing yet, and invite a
21
+ // refresh when the self-report is about to go stale ("inquire about updating").
20
22
  export function composeHint(info) {
23
+ // Paused: the model-facing hooks are silent, but this line is for the USER —
24
+ // say so once per session so "off" never reads as "broken".
25
+ if (info.paused) {
26
+ return "cadence: paused — prompts go through untouched (`cadence resume` or /cadence:resume to turn it back on)";
27
+ }
21
28
  if (info.firstRun) {
22
- return 'cadence: on, but it hasn\'t heard from you — try `cadence start` (or just `cadence state "deep work"`)';
29
+ return 'cadence: on, but it hasn\'t heard from you — try `cadence start` (or just `cadence report "deep work"`)';
23
30
  }
31
+ const expiringSoon = info.selfReport != null &&
32
+ info.selfReportRemainingMs != null &&
33
+ info.selfReportRemainingMs <= REFRESH_SOON_MS;
24
34
  const seen = [];
25
- if (info.selfReport)
26
- seen.push(`state "${info.selfReport}"`);
35
+ if (info.selfReport) {
36
+ seen.push(`state "${info.selfReport}"${expiringSoon ? " (expiring)" : ""}`);
37
+ }
27
38
  if (info.nowPlaying)
28
39
  seen.push(`${info.nowPlaying.player}: ${info.nowPlaying.artist}`);
29
40
  if (info.pinned.length)
30
41
  seen.push(`pinned ${info.pinned.join(", ")}`);
31
42
  if (seen.length === 0) {
32
- return 'cadence: live, no signals right now — `cadence state "..."` to give it one';
43
+ return 'cadence: live, no signals right now — `cadence report "..."` to give it one';
33
44
  }
34
- return `cadence: live ${seen.join(" · ")} (inputs: cadence state | dials)`;
45
+ // When state is about to expire, the inputs hint becomes an explicit nudge to
46
+ // re-declare — so a long session keeps the cadence honest as the room shifts.
47
+ const tail = expiringSoon
48
+ ? "still in this cadence? `cadence report \"...\"` to refresh"
49
+ : "inputs: cadence report | dials";
50
+ return `cadence: live — ${seen.join(" · ")} (${tail})`;
35
51
  }
36
52
  async function collectInfo() {
53
+ // Paused short-circuits everything — don't even probe for music.
54
+ if (await isPaused()) {
55
+ return {
56
+ selfReport: null,
57
+ selfReportRemainingMs: null,
58
+ pinned: [],
59
+ nowPlaying: null,
60
+ firstRun: false,
61
+ paused: true,
62
+ };
63
+ }
37
64
  // Race music against the budget — MusicBrainz on a brand-new artist can
38
65
  // be slow, and a session greeting must never delay the session.
39
66
  const [report, overrides, music] = await Promise.all([
@@ -41,15 +68,18 @@ async function collectInfo() {
41
68
  loadOverrides(),
42
69
  Promise.race([
43
70
  getMusicSignal().catch(() => null),
44
- new Promise((resolve) => setTimeout(() => resolve(null), BUDGET_MS)),
71
+ new Promise((resolve) => setTimeout(() => resolve(null), BUDGET_MS).unref()),
45
72
  ]),
46
73
  ]);
47
74
  const pinned = Object.keys(overrides);
75
+ const remaining = report ? Math.max(0, STALE_AFTER_MS - (Date.now() - report.setAt)) : null;
48
76
  return {
49
77
  selfReport: report?.text ?? null,
78
+ selfReportRemainingMs: remaining,
50
79
  pinned,
51
80
  nowPlaying: music?.artist ? { artist: music.artist, player: music.player ?? "music" } : null,
52
81
  firstRun: !report && pinned.length === 0,
82
+ paused: false,
53
83
  };
54
84
  }
55
85
  async function main() {
@@ -1,4 +1,5 @@
1
1
  import { STALE_AFTER_MS } from "./providers/selfreport.js";
2
+ import { providerEnabled, providerSetting } from "./config.js";
2
3
  const LABEL_W = 12; // sub-row label column
3
4
  const VALUE_W = 18; // value column, before a "(hidden: …)" note
4
5
  function row(label, value, note) {
@@ -14,12 +15,12 @@ function ttlLeft(setAt, now) {
14
15
  const m = Math.floor((rem % 3_600_000) / 60_000);
15
16
  return h > 0 ? `${h}h${String(m).padStart(2, "0")}m left` : `${m}m left`;
16
17
  }
17
- function ambientRows(a, platform) {
18
+ function environmentRows(a, platform) {
18
19
  if (!a)
19
- return [top("ambient", "— unavailable")];
20
+ return [top("environment", "— unavailable")];
20
21
  const mac = platform === "darwin";
21
22
  const macNote = "— macOS only";
22
- const lines = [" ambient"];
23
+ const lines = [" environment"];
23
24
  lines.push(row("time", `${a.partOfDay} (${a.dayOfWeek})`));
24
25
  lines.push(row("weather", a.weather ?? "— off (run: cadence set-location <lat> <lon>)"));
25
26
  lines.push(!mac
@@ -65,17 +66,21 @@ function ambientRows(a, platform) {
65
66
  : row("focus", "off", "(hidden: only shows on)"));
66
67
  return lines;
67
68
  }
68
- function musicRows(m) {
69
+ function musicRows(m, providers) {
70
+ const spotify = providerEnabled(providers, "spotify")
71
+ ? row("source", "macOS apps + Spotify (cross-platform, linked)")
72
+ : row("source", "macOS apps only", "(cross-platform: cadence spotify)");
69
73
  if (!m?.track)
70
- return [top("music", "— nothing playing")];
74
+ return [top("music", "— nothing playing"), spotify];
71
75
  const lines = [" music"];
72
76
  lines.push(row("track", `${JSON.stringify(m.track)}${m.artist ? ` — ${m.artist}` : ""}${m.player ? ` (${m.player})` : ""}`));
73
77
  lines.push(m.vibe ? row("vibe", m.vibe) : row("vibe", "— no tags yet (looked up once per artist)"));
78
+ lines.push(spotify);
74
79
  return lines;
75
80
  }
76
81
  function reportRow(r, now) {
77
82
  if (!r)
78
- return top("self_report", '— none set (run: cadence state "...")');
83
+ return top("self_report", '— none set (run: cadence report "...")');
79
84
  const text = r.text.length > 44 ? `${r.text.slice(0, 43)}…` : r.text;
80
85
  return top("self_report", `${JSON.stringify(text)} (${ttlLeft(r.setAt, now)})`);
81
86
  }
@@ -92,12 +97,39 @@ function gitRow(g) {
92
97
  ].filter(Boolean);
93
98
  return top("git", parts.join(", "));
94
99
  }
100
+ function intentRow() {
101
+ return top("intent", "— reads your prompt (the hook infers ship/think/debug per-prompt)");
102
+ }
103
+ function tempoRow(providers) {
104
+ return providerEnabled(providers, "typingTempo")
105
+ ? top("typing tempo", "on (opt-in) — rapid vs. considered prompt rhythm → pace")
106
+ : top("typing tempo", "— off (opt-in: cadence enable typingTempo)");
107
+ }
108
+ function optInFlavorRows(providers, environment) {
109
+ const sign = providerSetting(providers, "horoscope");
110
+ return [
111
+ " opt-in flavor",
112
+ row("focused app", providerEnabled(providers, "focusedApp")
113
+ ? environment?.focusedApp ?? "on — nothing non-terminal in front"
114
+ : "— off (cadence enable focusedApp)"),
115
+ row("moon", providerEnabled(providers, "moon")
116
+ ? "on — phase shows in the block"
117
+ : "— off (cadence enable moon)"),
118
+ row("horoscope", typeof sign === "string"
119
+ ? `on (${sign}) — daily text shows in the block`
120
+ : "— off (cadence enable horoscope <sign>)"),
121
+ ];
122
+ }
95
123
  export function renderSignalsTable(raw) {
124
+ const providers = raw.providers ?? {};
96
125
  return [
97
- ...ambientRows(raw.ambient, raw.platform),
98
- ...musicRows(raw.music),
126
+ ...environmentRows(raw.environment, raw.platform),
127
+ ...musicRows(raw.music, providers),
99
128
  reportRow(raw.report, raw.now),
129
+ intentRow(),
100
130
  gitRow(raw.git),
101
131
  top("activity", "— session-only (the hook injects it per-prompt)"),
132
+ tempoRow(providers),
133
+ ...optInFlavorRows(providers, raw.environment),
102
134
  ].join("\n");
103
135
  }
@@ -0,0 +1,136 @@
1
+ import { createServer } from "node:http";
2
+ import { createHash, randomBytes } from "node:crypto";
3
+ import { exec } from "node:child_process";
4
+ /* ─────────────────────────────────────────────────────────────────────────
5
+ * Spotify connect — Authorization Code + PKCE, run from the INTERACTIVE CLI.
6
+ *
7
+ * A distributed CLI can't keep a client secret, so we use PKCE (public client,
8
+ * no secret). The flow: spin up a one-shot loopback server, open the browser to
9
+ * Spotify's consent page, catch the redirect with the auth code, exchange it for
10
+ * a refresh token, and hand that back to the CLI to store in `providers.spotify`
11
+ * — the exact shape the fail-silent hook-side provider already reads.
12
+ *
13
+ * This NEVER runs in the hook (no browser, no server in a 1.5s budget). The hook
14
+ * only ever reads the cached refresh token. Keep it that way.
15
+ * ───────────────────────────────────────────────────────────────────────── */
16
+ // Must be registered verbatim in the Spotify app's redirect URIs. Spotify
17
+ // requires the explicit loopback IP (not "localhost") for native/CLI apps.
18
+ export const REDIRECT_PORT = 8888;
19
+ export const REDIRECT_URI = `http://127.0.0.1:${REDIRECT_PORT}/callback`;
20
+ export const SCOPE = "user-read-currently-playing";
21
+ const AUTH_TIMEOUT_MS = 120_000;
22
+ /** PKCE pair: a high-entropy verifier and its S256 challenge. Pure. */
23
+ export function generatePkce() {
24
+ const verifier = randomBytes(32).toString("base64url"); // 43 chars, within 43–128
25
+ const challenge = createHash("sha256").update(verifier).digest("base64url");
26
+ return { verifier, challenge };
27
+ }
28
+ /** Build the Spotify consent URL. Pure + exported so the params are testable. */
29
+ export function buildAuthorizeUrl(opts) {
30
+ const q = new URLSearchParams({
31
+ response_type: "code",
32
+ client_id: opts.clientId,
33
+ redirect_uri: REDIRECT_URI,
34
+ scope: SCOPE,
35
+ code_challenge_method: "S256",
36
+ code_challenge: opts.challenge,
37
+ state: opts.state,
38
+ });
39
+ return `https://accounts.spotify.com/authorize?${q.toString()}`;
40
+ }
41
+ /** Pull the refresh token out of Spotify's token response, or null. Pure. */
42
+ export function parseTokenResponse(json) {
43
+ const data = json;
44
+ return typeof data?.refresh_token === "string" && data.refresh_token ? data.refresh_token : null;
45
+ }
46
+ // Best-effort browser open; if it fails we've already printed the URL to paste.
47
+ function openBrowser(url) {
48
+ const cmd = process.platform === "darwin"
49
+ ? "open"
50
+ : process.platform === "win32"
51
+ ? "start \"\""
52
+ : "xdg-open";
53
+ const child = exec(`${cmd} "${url}"`, () => { });
54
+ child.on("error", () => { });
55
+ }
56
+ // Wait for Spotify to redirect back with ?code=&state=, validating state.
57
+ function waitForCode(expectedState) {
58
+ return new Promise((resolve, reject) => {
59
+ const server = createServer((req, res) => {
60
+ const url = new URL(req.url ?? "/", REDIRECT_URI);
61
+ if (url.pathname !== "/callback") {
62
+ res.writeHead(404).end();
63
+ return;
64
+ }
65
+ const code = url.searchParams.get("code");
66
+ const state = url.searchParams.get("state");
67
+ const err = url.searchParams.get("error");
68
+ res.writeHead(200, { "Content-Type": "text/html" });
69
+ const done = (msg) => res.end(`<!doctype html><meta charset=utf-8><body style="font:16px system-ui;padding:3rem">${msg} — you can close this tab.</body>`);
70
+ if (err) {
71
+ done(`Spotify returned "${err}"`);
72
+ cleanup();
73
+ reject(new Error(`authorization denied: ${err}`));
74
+ }
75
+ else if (!code || state !== expectedState) {
76
+ done("Something didn't line up");
77
+ cleanup();
78
+ reject(new Error("missing code or state mismatch"));
79
+ }
80
+ else {
81
+ done("Spotify linked ✓");
82
+ cleanup();
83
+ resolve(code);
84
+ }
85
+ });
86
+ const timer = setTimeout(() => {
87
+ cleanup();
88
+ reject(new Error("timed out waiting for Spotify authorization"));
89
+ }, AUTH_TIMEOUT_MS);
90
+ function cleanup() {
91
+ clearTimeout(timer);
92
+ server.close();
93
+ }
94
+ server.on("error", (e) => {
95
+ clearTimeout(timer);
96
+ reject(e.code === "EADDRINUSE"
97
+ ? new Error(`port ${REDIRECT_PORT} is in use — close whatever's on it and retry`)
98
+ : e);
99
+ });
100
+ server.listen(REDIRECT_PORT, "127.0.0.1");
101
+ });
102
+ }
103
+ async function exchangeCode(clientId, code, verifier) {
104
+ const res = await fetch("https://accounts.spotify.com/api/token", {
105
+ method: "POST",
106
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
107
+ body: new URLSearchParams({
108
+ grant_type: "authorization_code",
109
+ code,
110
+ redirect_uri: REDIRECT_URI,
111
+ client_id: clientId,
112
+ code_verifier: verifier,
113
+ }),
114
+ });
115
+ if (!res.ok)
116
+ throw new Error(`token exchange failed: ${res.status} ${await res.text()}`);
117
+ const refreshToken = parseTokenResponse(await res.json());
118
+ if (!refreshToken)
119
+ throw new Error("Spotify did not return a refresh token");
120
+ return refreshToken;
121
+ }
122
+ /** Run the full browser flow. Resolves with a refresh token to store, or
123
+ * throws with a human-readable reason. Logs progress via the passed printer so
124
+ * the CLI owns all stdout. */
125
+ export async function connectSpotify(clientId, log) {
126
+ const { verifier, challenge } = generatePkce();
127
+ const state = randomBytes(16).toString("hex");
128
+ const url = buildAuthorizeUrl({ clientId, challenge, state });
129
+ log(" Opening Spotify in your browser to authorize…");
130
+ log(" If it doesn't open, paste this:\n");
131
+ log(` ${url}\n`);
132
+ openBrowser(url);
133
+ const code = await waitForCode(state);
134
+ log(" Got it — exchanging for a refresh token…");
135
+ return exchangeCode(clientId, code, verifier);
136
+ }
package/dist/stop.js CHANGED
@@ -2,9 +2,10 @@
2
2
  import { pathToFileURL } from "node:url";
3
3
  import { getMusicSignal } from "./providers/music.js";
4
4
  import { getSelfReportSignal } from "./providers/selfreport.js";
5
- import { getAmbientSignal } from "./providers/ambient.js";
5
+ import { getEnvironmentSignal } from "./providers/environment.js";
6
6
  import { getGitSignal } from "./providers/git.js";
7
7
  import { deriveCadence, loadOverrides, applyOverrides } from "./cadence.js";
8
+ import { isPaused } from "./config.js";
8
9
  const TOTAL_BUDGET_MS = 1500;
9
10
  async function readStdin() {
10
11
  if (process.stdin.isTTY)
@@ -20,10 +21,10 @@ async function readStdin() {
20
21
  }
21
22
  }
22
23
  async function collectSignals(cwd) {
23
- const [music, report, ambient, git] = await Promise.allSettled([
24
+ const [music, report, environment, git] = await Promise.allSettled([
24
25
  getMusicSignal(),
25
26
  getSelfReportSignal(),
26
- getAmbientSignal(new Date()),
27
+ getEnvironmentSignal(new Date()),
27
28
  getGitSignal(cwd),
28
29
  ]);
29
30
  const signals = [];
@@ -31,8 +32,8 @@ async function collectSignals(cwd) {
31
32
  signals.push(music.value);
32
33
  if (report.status === "fulfilled" && report.value)
33
34
  signals.push(report.value);
34
- if (ambient.status === "fulfilled" && ambient.value)
35
- signals.push(ambient.value);
35
+ if (environment.status === "fulfilled" && environment.value)
36
+ signals.push(environment.value);
36
37
  if (git.status === "fulfilled" && git.value)
37
38
  signals.push(git.value);
38
39
  return signals;
@@ -75,12 +76,14 @@ export function decideStop(input, signals, cadence, pinned) {
75
76
  };
76
77
  }
77
78
  async function main() {
79
+ if (await isPaused())
80
+ return; // user asked for silence — never block while paused
78
81
  const input = await readStdin();
79
82
  const projectDir = input.cwd ?? process.cwd();
80
83
  const [signals, overrides] = await Promise.all([
81
84
  Promise.race([
82
85
  collectSignals(projectDir),
83
- new Promise((resolve) => setTimeout(() => resolve([]), TOTAL_BUDGET_MS)),
86
+ new Promise((resolve) => setTimeout(() => resolve([]), TOTAL_BUDGET_MS).unref()),
84
87
  ]),
85
88
  loadOverrides(),
86
89
  ]);
package/dist/types.js CHANGED
@@ -9,6 +9,7 @@
9
9
  * The four embodied dimensions, each a provider emitting one Signal:
10
10
  * - MusicSignal → what's playing (music)
11
11
  * - SelfReportSignal → what you told us (mood, ground truth)
12
+ * - IntentSignal → what your prompt implies (mood, inferred from words)
12
13
  * - ActivitySignal → your motor/typing tempo (mood, inferred)
13
14
  * - GitSignal → your work state (context)
14
15
  * - PlaceSignal → where & in what setting (place)
package/dist/vibe.js CHANGED
@@ -20,7 +20,7 @@
20
20
  * (energy: death-metal high → Bach low; valence: euphoric high → depressed low).
21
21
  * Order doesn't matter — every matching row is averaged. */
22
22
  const GENRE_AFFECT = {
23
- // ── high energy → ship ───────────────────────────────────────────────
23
+ // ── high energy → pace high ──────────────────────────────────────────
24
24
  punk: { energy: 0.9, valence: 0.6, acoustic: 0.05, moods: ["aggressive", "energetic"] },
25
25
  metal: { energy: 0.95, valence: 0.4, acoustic: 0.02, moods: ["aggressive", "dark"] },
26
26
  hardcore: { energy: 0.95, valence: 0.45, acoustic: 0.02, moods: ["aggressive", "energetic"] },
@@ -66,7 +66,7 @@ const GENRE_AFFECT = {
66
66
  ska: { energy: 0.8, valence: 0.75, acoustic: 0.15, moods: ["happy", "energetic"] },
67
67
  surf: { energy: 0.7, valence: 0.7, acoustic: 0.2, moods: ["happy", "energetic"] },
68
68
  "math rock": { energy: 0.75, valence: 0.55, acoustic: 0.2, moods: ["energetic"] },
69
- // ── mid / groovy → ship-or-think depending on energy ─────────────────
69
+ // ── mid / groovy → pace dead zone (0.4 < energy < 0.7 = no nudge) ────
70
70
  "r&b": { energy: 0.55, valence: 0.6, acoustic: 0.2, moods: ["sexy", "chilled"] },
71
71
  soul: { energy: 0.55, valence: 0.65, acoustic: 0.25, moods: ["romantic", "uplifting"] },
72
72
  reggae: { energy: 0.6, valence: 0.75, acoustic: 0.2, moods: ["chilled", "happy"] },
@@ -86,7 +86,7 @@ const GENRE_AFFECT = {
86
86
  "big band": { energy: 0.65, valence: 0.7, acoustic: 0.6, moods: ["happy", "epic"] },
87
87
  samba: { energy: 0.7, valence: 0.8, acoustic: 0.5, moods: ["happy"] },
88
88
  flamenco: { energy: 0.6, valence: 0.5, acoustic: 0.85, moods: ["romantic", "epic"] },
89
- // ── low energy, organic → think ──────────────────────────────────────
89
+ // ── low energy, organic → pace low, tone warm ────────────────────────
90
90
  ambient: { energy: 0.2, valence: 0.5, acoustic: 0.6, moods: ["ethereal", "calm"] },
91
91
  "lo-fi": { energy: 0.3, valence: 0.5, acoustic: 0.4, moods: ["chilled", "calm"] },
92
92
  lofi: { energy: 0.3, valence: 0.5, acoustic: 0.4, moods: ["chilled", "calm"] },
@@ -112,7 +112,8 @@ const GENRE_AFFECT = {
112
112
  soundtrack: { energy: 0.45, valence: 0.45, acoustic: 0.6, moods: ["epic"] },
113
113
  opera: { energy: 0.5, valence: 0.45, acoustic: 0.9, moods: ["epic"] },
114
114
  choral: { energy: 0.3, valence: 0.5, acoustic: 0.95, moods: ["ethereal", "epic"] },
115
- // ── low energy, low valence → debug-leaning ──────────────────────────
115
+ // ── low energy, low valence → pace low; dark moods are render-only ───
116
+ // (valence currently moves NO dial — see BACKLOG "energyToMode boundary")
116
117
  blues: { energy: 0.45, valence: 0.35, acoustic: 0.5, moods: ["sad", "dark"] },
117
118
  goth: { energy: 0.55, valence: 0.3, acoustic: 0.2, moods: ["dark"] },
118
119
  gothic: { energy: 0.55, valence: 0.3, acoustic: 0.2, moods: ["dark"] },
@@ -143,7 +144,10 @@ export function tagsToVibe(tags) {
143
144
  }
144
145
  if (hits.length === 0)
145
146
  return null;
146
- const energy = hits.reduce((s, a) => s + a.energy, 0) / hits.length;
147
+ const avg = (pick) => hits.reduce((s, a) => s + pick(a), 0) / hits.length;
148
+ const energy = avg((a) => a.energy);
149
+ const valence = avg((a) => a.valence);
150
+ const acoustic = avg((a) => a.acoustic);
147
151
  // Mood words: collect from all hits, dedupe, keep most frequent first.
148
152
  const moodCounts = new Map();
149
153
  for (const h of hits)
@@ -154,5 +158,5 @@ export function tagsToVibe(tags) {
154
158
  .sort((a, b) => b[1] - a[1])
155
159
  .slice(0, 4)
156
160
  .map(([m]) => m);
157
- return { moods, energy };
161
+ return { moods, energy, valence, acoustic };
158
162
  }
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@cullumco/cadence",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Agents that read the room. Ambient context, cadence dials, and finish-line guardrails for Claude Code. macOS-only alpha.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "cadence": "bin/cadence"
8
8
  },
9
- "homepage": "https://cullumco.github.io/cadence/",
9
+ "homepage": "https://cadence.cullum.co/",
10
10
  "repository": {
11
11
  "type": "git",
12
12
  "url": "git+https://github.com/cullumco/cadence.git"
@@ -0,0 +1,16 @@
1
+ ---
2
+ description: Pause Cadence — silence all hooks instantly; prompts go through untouched until resumed.
3
+ disable-model-invocation: true
4
+ ---
5
+
6
+ # Pause Cadence
7
+
8
+ Run `cadence pause` with Bash.
9
+
10
+ Then confirm in one sentence: Cadence is paused, prompts go through untouched,
11
+ and `/cadence:resume` turns it back on. Their state (self-report, pins,
12
+ opt-ins) is preserved exactly as-is.
13
+
14
+ Note: the current session may already have `<user_state>` blocks in context
15
+ from earlier prompts — ignore them from here on; the user has asked for
16
+ silence.
@@ -0,0 +1,12 @@
1
+ ---
2
+ description: Resume Cadence — start reading the room again after a pause.
3
+ disable-model-invocation: true
4
+ ---
5
+
6
+ # Resume Cadence
7
+
8
+ Run `cadence resume` with Bash, then `cadence test`.
9
+
10
+ Confirm in one sentence that Cadence is live again, and summarize what it
11
+ currently sees from the `cadence test` output (or note that no signals are
12
+ present yet). Keep it to two sentences total.
@@ -0,0 +1,43 @@
1
+ ---
2
+ description: Conversational Cadence setup — shape how the agent reads your prompts, choose which signals to share, all through a short conversation.
3
+ disable-model-invocation: true
4
+ ---
5
+
6
+ # Cadence Setup
7
+
8
+ Walk the user through shaping Cadence conversationally. You orchestrate; the
9
+ `cadence` CLI is the source of truth — run commands with Bash, never edit
10
+ `~/.cadence/` files directly.
11
+
12
+ Have a short conversation, not a form. Adapt to their answers; skip anything
13
+ they don't care about. The arc:
14
+
15
+ 1. **State** — ask how they're working right now (shipping? thinking through
16
+ something? debugging? just vibing?). Phrase it naturally. Turn their answer
17
+ into `cadence report "<their words>"`. Tell them it expires after 2 hours and
18
+ they can refresh with `/cadence:state`.
19
+
20
+ 2. **Dials** — ask if there's anything they ALWAYS want, regardless of signals
21
+ (e.g. "always be terse", "never act without asking"). Map to pins:
22
+ `cadence set pace|tone|posture|proactivity <low|medium|high>`. Most people
23
+ should pin nothing — say so. Pins override inference until unset.
24
+
25
+ 3. **Opt-in signals** — offer, one line each, that these are off until asked:
26
+ - typing tempo (`cadence enable typingTempo`) — prompt rhythm → pace
27
+ - focused app (`cadence enable focusedApp`, macOS) — frontmost app as flavor
28
+ - weather (`cadence set-location <lat> <lon>`) — needs a location
29
+ - moon / horoscope (`cadence enable moon`, `cadence enable horoscope <sign>`)
30
+ - Spotify off-Mac (`cadence spotify connect <clientId>` — needs a terminal,
31
+ opens a browser; just point them at it, don't run it)
32
+ Enable only what they ask for.
33
+
34
+ 4. **Show the result** — run `cadence test` and summarize the injected block in
35
+ one or two plain-English sentences: what the agent now sees, and one way it
36
+ will change responses.
37
+
38
+ Close by mentioning: `/cadence:pause` silences everything instantly,
39
+ `/cadence:resume` brings it back, and `cadence signals` shows every signal and
40
+ why it is or isn't firing.
41
+
42
+ Keep the whole exchange warm and fast — under a minute of their time. Do not
43
+ explain the architecture.
@@ -8,12 +8,12 @@ disable-model-invocation: true
8
8
  Use this skill when the user invokes `/cadence:state`.
9
9
 
10
10
  If `$ARGUMENTS` is non-empty:
11
- - Run `cadence state "$ARGUMENTS"` with Bash.
12
- - Then tell the user the state is set and will expire after four hours.
11
+ - Run `cadence report "$ARGUMENTS"` with Bash.
12
+ - Then tell the user the state is set and will expire after two hours.
13
13
  - Keep it to one short sentence.
14
14
 
15
15
  If `$ARGUMENTS` is empty:
16
- - Run `cadence state` with Bash.
16
+ - Run `cadence report` with Bash.
17
17
  - Tell the user the current state, or that none is set.
18
18
 
19
19
  Do not explain the whole product.