@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.
- package/.claude-plugin/marketplace.json +29 -0
- package/.claude-plugin/plugin.json +20 -0
- package/ALPHA.md +84 -0
- package/README.md +204 -0
- package/bin/cadence +5 -0
- package/bin/cadence.js +5 -0
- package/dist/cadence.js +187 -0
- package/dist/cli.js +205 -0
- package/dist/hook.js +80 -0
- package/dist/inject.js +106 -0
- package/dist/providers/activity.js +35 -0
- package/dist/providers/ambient.js +158 -0
- package/dist/providers/git.js +52 -0
- package/dist/providers/music.js +151 -0
- package/dist/providers/selfreport.js +19 -0
- package/dist/stop.js +99 -0
- package/dist/types.js +16 -0
- package/dist/vibe.js +96 -0
- package/hooks/hooks.json +26 -0
- package/package.json +60 -0
- package/skills/state/SKILL.md +19 -0
- package/skills/try/SKILL.md +19 -0
|
@@ -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
package/bin/cadence.js
ADDED
package/dist/cadence.js
ADDED
|
@@ -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
|
+
}
|