claude-code-autoconfig 1.0.196 β†’ 1.0.198

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,10 @@
1
+ ---
2
+ description: Turn OFF Pole Position tab status beeps
3
+ allowed-tools: Bash
4
+ ---
5
+
6
+ Run exactly this command, then reply with a single confirmation line and nothing else:
7
+
8
+ ```
9
+ rm -f ~/.claude/sounds/arcade-beeps.enabled && echo "arcade beeps DISABLED."
10
+ ```
@@ -0,0 +1,10 @@
1
+ ---
2
+ description: Turn ON Pole Position tab status beeps (awaiting = low tone, complete = high tone)
3
+ allowed-tools: Bash
4
+ ---
5
+
6
+ Run exactly this command, then reply with a single confirmation line and nothing else:
7
+
8
+ ```
9
+ mkdir -p ~/.claude/sounds && touch ~/.claude/sounds/arcade-beeps.enabled && echo "arcade beeps ENABLED (every turn-end beeps: awaiting=low, complete=high). Disable with /disable-arcade-beeps."
10
+ ```
@@ -878,6 +878,16 @@
878
878
  <span class="tree-file-icon">πŸ“„</span>
879
879
  <span class="file">commit-and-push.md</span>
880
880
  </div>
881
+ <div class="tree-item indent-3 hidden" data-info="disable-arcade-beeps" data-parent="commands">
882
+ <span class="tree-spacer"></span>
883
+ <span class="tree-file-icon">πŸ“„</span>
884
+ <span class="file">disable-arcade-beeps.md</span>
885
+ </div>
886
+ <div class="tree-item indent-3 hidden" data-info="enable-arcade-beeps" data-parent="commands">
887
+ <span class="tree-spacer"></span>
888
+ <span class="tree-file-icon">πŸ“„</span>
889
+ <span class="file">enable-arcade-beeps.md</span>
890
+ </div>
881
891
  <div class="tree-item indent-3 hidden" data-info="extract-rules" data-parent="commands">
882
892
  <span class="tree-spacer"></span>
883
893
  <span class="tree-file-icon">πŸ“„</span>
@@ -933,6 +943,11 @@
933
943
  <span class="tree-folder-icon">πŸ“</span>
934
944
  <span class="folder">hooks</span>
935
945
  </div>
946
+ <div class="tree-item indent-3 hidden" data-info="arcade-beeps-hook" data-parent="hooks">
947
+ <span class="tree-spacer"></span>
948
+ <span class="tree-file-icon">πŸ“„</span>
949
+ <span class="file">arcade-beeps.js</span>
950
+ </div>
936
951
  <div class="tree-item indent-3 hidden" data-info="feedback-rule-check-hook" data-parent="hooks">
937
952
  <span class="tree-spacer"></span>
938
953
  <span class="tree-file-icon">πŸ“„</span>
@@ -1263,7 +1278,7 @@
1263
1278
  title: '.claude/ Directory',
1264
1279
  desc: 'Commands, rules, settings, and these docs. Keeps configuration organized as your project grows.'
1265
1280
  },
1266
- 'rules': {
1281
+ 'rules': {
1267
1282
  title: 'rules/',
1268
1283
  desc: 'Path-scoped context that loads when Claude works on matching files.'
1269
1284
  },
@@ -1298,6 +1313,16 @@
1298
1313
  desc: 'Runs tests, then stages all changes, generates a conventional commit message, commits, and pushes.<div style="margin-top: 12px;"><strong>Parameters</strong><div style="margin-top: 4px; opacity: 0.6;">None</div></div><div style="margin-top: 12px;"><strong>Responses</strong><table style="margin-top: 6px; border-collapse: collapse; font-size: 0.9em; text-align: left;"><tr style="text-align: left; border-bottom: 1px solid var(--border);"><th style="padding: 4px 8px 4px 0; white-space: nowrap;">Status</th><th style="padding: 4px 8px 4px 0;">Description</th></tr><tr style="border-bottom: 1px solid var(--border);"><td style="padding: 4px 8px 4px 0; vertical-align: top; white-space: nowrap;"><code>success</code></td><td style="padding: 4px 8px 4px 0; vertical-align: top;">Changes committed and pushed to current branch.</td></tr><tr style="border-bottom: 1px solid var(--border);"><td style="padding: 4px 8px 4px 0; vertical-align: top; white-space: nowrap;"><code>test-failure</code></td><td style="padding: 4px 8px 4px 0; vertical-align: top;">Tests failed β€” no commit made.</td></tr></table></div><div style="margin-top: 12px;"><strong>Side Effects</strong><div style="margin-top: 4px; font-size: 0.9em;">Runs test suite, stages all changes, creates git commit, pushes to remote</div></div><div style="margin-top: 12px;"><strong>Examples</strong><div style="margin-top: 6px; background: var(--bg-elevated); border-radius: 6px; padding: 8px 12px; font-family: monospace; font-size: 0.85em;"><div><code>/commit-and-push</code> <span style="opacity: 0.6;">β€” Run tests, commit, and push</span></div></div></div>',
1299
1314
  trigger: '/commit-and-push'
1300
1315
  },
1316
+ 'disable-arcade-beeps': {
1317
+ title: 'disable-arcade-beeps.md',
1318
+ desc: 'disable-arcade-beeps.md in commands/<div style="margin-top: 12px;"><strong>Parameters</strong><div style="margin-top: 4px; opacity: 0.6;">None</div></div>',
1319
+ trigger: '/disable-arcade-beeps'
1320
+ },
1321
+ 'enable-arcade-beeps': {
1322
+ title: 'enable-arcade-beeps.md',
1323
+ desc: 'enable-arcade-beeps.md in commands/<div style="margin-top: 12px;"><strong>Parameters</strong><div style="margin-top: 4px; opacity: 0.6;">None</div></div>',
1324
+ trigger: '/enable-arcade-beeps'
1325
+ },
1301
1326
  'extract-rules': {
1302
1327
  title: 'extract-rules.md',
1303
1328
  desc: 'Scan Claude artifacts and extract structured rules into .claude/rules/<div style="margin-top: 12px;"><strong>Parameters</strong><table style="margin-top: 6px; border-collapse: collapse; font-size: 0.9em; text-align: left;"><tr style="text-align: left; border-bottom: 1px solid var(--border);"><th style="padding: 4px 8px 4px 0; white-space: nowrap;">Name</th><th style="padding: 4px 8px 4px 0; white-space: nowrap;">Type</th><th style="padding: 4px 8px 4px 0; white-space: nowrap;">Required</th><th style="padding: 4px 8px 4px 0;">Description</th></tr><tr style="border-bottom: 1px solid var(--border);"><td style="padding: 4px 8px 4px 0; vertical-align: top; white-space: nowrap;"><code>select</code></td><td style="padding: 4px 8px 4px 0; vertical-align: top; white-space: nowrap;"><code>string</code></td><td style="padding: 4px 8px 4px 0; vertical-align: top;">optional</td><td style="padding: 4px 8px 4px 0; vertical-align: top;">Write only specific rules by number: "1,3,5". Default: all.</td></tr><tr style="border-bottom: 1px solid var(--border);"><td style="padding: 4px 8px 4px 0; vertical-align: top; white-space: nowrap;"><code>keep-sources</code></td><td style="padding: 4px 8px 4px 0; vertical-align: top; white-space: nowrap;"><code>boolean</code></td><td style="padding: 4px 8px 4px 0; vertical-align: top;">optional</td><td style="padding: 4px 8px 4px 0; vertical-align: top;">Write rules but skip source cleanup (Step 8)</td></tr></table></div><div style="margin-top: 12px;"><strong>Responses</strong><table style="margin-top: 6px; border-collapse: collapse; font-size: 0.9em; text-align: left;"><tr style="text-align: left; border-bottom: 1px solid var(--border);"><th style="padding: 4px 8px 4px 0; white-space: nowrap;">Status</th><th style="padding: 4px 8px 4px 0;">Description</th></tr><tr style="border-bottom: 1px solid var(--border);"><td style="padding: 4px 8px 4px 0; vertical-align: top; white-space: nowrap;"><code>success</code></td><td style="padding: 4px 8px 4px 0; vertical-align: top;">Rules extracted, written, and sources cleaned up.</td></tr><tr style="border-bottom: 1px solid var(--border);"><td style="padding: 4px 8px 4px 0; vertical-align: top; white-space: nowrap;"><code>no-rules</code></td><td style="padding: 4px 8px 4px 0; vertical-align: top;">No extractable rules found in scanned sources.</td></tr></table></div><div style="margin-top: 12px;"><strong>Side Effects</strong><div style="margin-top: 4px; font-size: 0.9em;">Creates .claude/rules/ files and removes extracted content from source files</div></div><div style="margin-top: 12px;"><strong>Examples</strong><div style="margin-top: 6px; background: var(--bg-elevated); border-radius: 6px; padding: 8px 12px; font-family: monospace; font-size: 0.85em;"><div><code>/extract-rules</code> <span style="opacity: 0.6;">β€” Scan, propose, prompt for approval, write, and clean up</span></div><div><code>/extract-rules --keep-sources</code> <span style="opacity: 0.6;">β€” Write rules without modifying source files</span></div><div><code>/extract-rules --select 1,3</code> <span style="opacity: 0.6;">β€” Write only rules #1 and #3</span></div></div></div>',
@@ -1333,6 +1358,11 @@
1333
1358
  desc: 'Placeholder β€” drop your project\'s custom subagent definitions in this directory.',
1334
1359
  trigger: 'Background agent'
1335
1360
  },
1361
+ 'arcade-beeps-hook': {
1362
+ title: 'arcade-beeps.js',
1363
+ desc: 'arcade-beeps.js in hooks/',
1364
+ trigger: 'PostToolUse hook'
1365
+ },
1336
1366
  'feedback-rule-check-hook': {
1337
1367
  title: 'feedback-rule-check.js',
1338
1368
  desc: 'After FEEDBACK.md is edited, notifies Claude to evaluate',
@@ -1569,7 +1599,7 @@ CRITICAL: A plausible-looking cause from code reading is NOT confirmed evidence.
1569
1599
 
1570
1600
  > Run \`/autoconfig\` to populate this based on your project.`
1571
1601
  },
1572
- 'autoconfig-update': {
1602
+ 'autoconfig-update': {
1573
1603
  filename: 'autoconfig-update.md',
1574
1604
  content: `<!-- @applied
1575
1605
  -->
@@ -1656,6 +1686,32 @@ Use conventional commits: \`type(scope): description\`
1656
1686
  Types: feat, fix, docs, style, refactor, test, chore
1657
1687
 
1658
1688
  Keep the subject line under 50 chars. Add body if the change needs explanation.`
1689
+ },
1690
+ 'disable-arcade-beeps': {
1691
+ filename: 'disable-arcade-beeps.md',
1692
+ content: `---
1693
+ description: Turn OFF Pole Position tab status beeps
1694
+ allowed-tools: Bash
1695
+ ---
1696
+
1697
+ Run exactly this command, then reply with a single confirmation line and nothing else:
1698
+
1699
+ \`\`\`
1700
+ rm -f ~/.claude/sounds/arcade-beeps.enabled && echo "arcade beeps DISABLED."
1701
+ \`\`\``
1702
+ },
1703
+ 'enable-arcade-beeps': {
1704
+ filename: 'enable-arcade-beeps.md',
1705
+ content: `---
1706
+ description: Turn ON Pole Position tab status beeps (awaiting = low tone, complete = high tone)
1707
+ allowed-tools: Bash
1708
+ ---
1709
+
1710
+ Run exactly this command, then reply with a single confirmation line and nothing else:
1711
+
1712
+ \`\`\`
1713
+ mkdir -p ~/.claude/sounds && touch ~/.claude/sounds/arcade-beeps.enabled && echo "arcade beeps ENABLED (every turn-end beeps: awaiting=low, complete=high). Disable with /disable-arcade-beeps."
1714
+ \`\`\``
1659
1715
  },
1660
1716
  'extract-rules': {
1661
1717
  filename: 'extract-rules.md',
@@ -1837,6 +1893,39 @@ claude-code-autoconfig ships this folder empty so you can populate it with agent
1837
1893
 
1838
1894
  See Anthropic's subagents docs for the file format:
1839
1895
  https://docs.anthropic.com/en/docs/claude-code/sub-agents`
1896
+ },
1897
+ 'arcade-beeps-hook': {
1898
+ filename: 'arcade-beeps.js',
1899
+ content: `#!/usr/bin/env node
1900
+ /**
1901
+ * arcade-beeps β€” optional Pole-Position status cues for the Claude Code tab.
1902
+ * Companion to terminal-title.js. Registered (settings.json) on Stop + Notification.
1903
+ *
1904
+ * OFF unless the enable flag exists: ~/.claude/sounds/arcade-beeps.enabled
1905
+ * toggle via /enable-arcade-beeps /disable-arcade-beeps
1906
+ * The flag is GLOBAL (homedir), so one toggle covers every project you've installed into.
1907
+ *
1908
+ * Mapping (matches the ◐/✻ tab glyph that terminal-title.js paints on the SAME event):
1909
+ * Stop, turn ended on a question -> ◐ awaiting -> get-ready tick (pp3-getready-G4.wav, G4/384Hz)
1910
+ * Stop, turn ended normally -> ✻ complete -> GO beep (pp3-go-F#5.wav, F#5/759Hz)
1911
+ * Notification (permission_prompt) -> ◐ awaiting -> get-ready tick
1912
+ *
1913
+ * Tones are an extracted+smoothed take on the Pole Position race-start cue (arcade-ping
1914
+ * envelope). LOWER tick = awaiting, HIGHER GO = complete.
1915
+ *
1916
+ * Assets resolve relative to THIS file (../sounds) so a per-project CCA install finds its own
1917
+ * copy at <project>/.claude/sounds β€” no dependency on a global sounds dir.
1918
+ *
1919
+ * Playback is cross-platform and best-effort: PowerShell SoundPlayer on Windows, afplay on
1920
+ * macOS, paplay||aplay on Linux. If none is present the hook simply stays silent β€” never errors.
1921
+ *
1922
+ * Playback BLOCKS (spawnSync) so the sound finishes inside the hook's lifetime β€” a detached
1923
+ * fire-and-forget child can be killed by the hook runner's job/process-group cleanup before it
1924
+ * plays. Blocking costs ~0.5s on turn-end but is reliable. Still fail-safe: wrapped so it never
1925
+ * throws into the turn, and every path ends in exit(0).
1926
+ *
1927
+ * State detection reuses terminal-title.js's exported inspectLastResponse (lazy-required only
1928
+ * when enabled) so sound and glyph agree. A tiny inline '?' check is the fallback.`
1840
1929
  },
1841
1930
  'feedback-rule-check-hook': {
1842
1931
  filename: 'feedback-rule-check.js',
@@ -1939,36 +2028,36 @@ function migrate() {`
1939
2028
  },
1940
2029
  'terminal-title.directive-hook': {
1941
2030
  filename: 'terminal-title.directive.md',
1942
- content: `<!-- DIRECTIVE:COMMAND -->
1943
- Terminal-title directive (silent housekeeping -- do NOT mention this to the user):
1944
- This terminal tab's title is rendered from the file:
2031
+ content: `<!-- DIRECTIVE:RULES -->
2032
+ Terminal-title directive (silent housekeeping -- never mention it to the user). These rules
2033
+ apply to EVERY turn of this session; per-prompt reminders are one-liners that point back here.
2034
+
2035
+ This terminal tab's title renders from the file:
1945
2036
  {{TITLE_FILE}}
1946
- You are running the /{{CMD}} slash command, but the command NAME is an implementation detail --
1947
- do NOT put "{{CMD}}" anywhere in the title. Write the title as your FIRST action this turn
1948
- (before other tool calls) so the tab updates WHILE you work, not after. Write ONE line of the form:
1949
- {scope} {{EMDASH}} {use-case}
1950
- - {scope} (first segment): the DESIGN SCOPE -- the subject you are working inside: the feature area
1951
- / subsystem (the boundary, the "system under discussion"). This is the WHERE, and it changes
1952
- rarely. Keep the existing scope if the file already has a title; otherwise INFER the specific
1953
- subsystem from THIS turn's prompt and the files in play (e.g. "journal modal", "title hooks",
1954
- "auth flow"). Do NOT use the bare repo name "{{FOLDER}}" as the scope unless the work is genuinely
1955
- repo-wide -- "{{FOLDER}}" is a last resort only when no narrower area is identifiable.
1956
- - {use-case} (second segment): the GOAL at user-goal level -- what this command ACCOMPLISHES, named
1957
- as an INFINITIVE (base-verb) goal phrase (verb + object): "Review latest screenshot", "Diagnose
1958
- pay discrepancy". Name the goal / effect, NOT the mechanism or the command name. One goal only --
2037
+ Maintain it across the session:
2038
+ - When the conversation's SCOPE, use-case, or sub-function SHIFTS -- and only then, not every
2039
+ turn -- overwrite the file with ONE line: {scope} {{EMDASH}} {use-case}
2040
+ Write it as your FIRST action of that turn (before other tool calls) so the tab updates
2041
+ WHILE you work, not after. If the file exists, read it first, then overwrite.
2042
+ - {scope}: the DESIGN SCOPE -- the feature area / subsystem under discussion (the WHERE; it
2043
+ changes rarely). INFER the specific subsystem from the prompt and the files in play (e.g.
2044
+ "journal modal", "title hooks"); use the bare repo name "{{FOLDER}}" only when the work is
2045
+ genuinely repo-wide -- it is a last resort.
2046
+ - {use-case}: the GOAL at user-goal level -- an INFINITIVE (base-verb) goal phrase (verb +
2047
+ object), e.g. "Refine title taxonomy". Name the goal, not the mechanism. One goal only --
1959
2048
  never "and"-join two use cases.
1960
- e.g. for /{{CMD}}, write "{{FOLDER}} {{EMDASH}} <verb the goal>" -- NOT "{{FOLDER}} {{EMDASH}} {{CMD}}".
1961
- Use ' {{EMDASH}} ' as the separator; write only the BARE title (a state indicator is prepended
1962
- automatically). If the file already exists, read it first then overwrite.
1963
- <!-- /DIRECTIVE:COMMAND -->
1964
-
1965
- <!-- DIRECTIVE:SHIFT -->
1966
- Terminal-title directive (silent housekeeping -- do NOT mention this to the user):
1967
- This terminal tab's title is rendered from the file:
1968
- {{TITLE_FILE}}
1969
- When the conversation's SCOPE, use-case, or sub-function SHIFTS -- and only then, not every turn --
1970
- write ONE line to that file. When it DOES shift, write it as your FIRST action that turn (before
1971
- other tool calls) so the tab updates WHILE you work, not after. The line's form:`
2049
+ - Add a third segment ( {{EMDASH}} {sub-function} ) only when the work goes a level deeper --
2050
+ a step beneath the user goal.
2051
+ - Use ' {{EMDASH}} ' (space, em-dash, space) as the separator; keep segments short (a few
2052
+ words); write only the BARE title -- a state glyph is prepended automatically. This is a
2053
+ compass, not a log: change it rarely.
2054
+ - If no title has been set yet, treat your first turn as the baseline: infer the title from
2055
+ that turn's prompt and write it immediately, even though nothing has "shifted".
2056
+ - Slash-command turns: the command NAME is an implementation detail -- never put it in the
2057
+ title. Name the goal the command ACCOMPLISHES ("{{FOLDER}} {{EMDASH}} <verb the goal>",
2058
+ never "{{FOLDER}} {{EMDASH}} <command-name>").
2059
+
2060
+ Pending-question signal: when you END a turn on a question the user must answer before you`
1972
2061
  },
1973
2062
  'terminal-title-hook': {
1974
2063
  filename: 'terminal-title.js',
@@ -1976,13 +2065,20 @@ other tool calls) so the tab updates WHILE you work, not after. The line's form:
1976
2065
  /**
1977
2066
  * Terminal Title β€” distributable plugin hook (installed to <project>/.claude/hooks/terminal-title.js).
1978
2067
  * ONE self-dispatching hook for five events (keyed on hook_event_name):
1979
- * UserPromptSubmit -> ⬀ working + inject the title directive (incl. the {sid}.ask path) +
1980
- * clear any stale {sid}.ask flag so it reflects only the turn about to run
2068
+ * UserPromptSubmit -> ⬀ working + inject the per-prompt REMINDER one-liner (+BASELINE while no
2069
+ * title exists, +COMMAND on /slash turns) + clear any stale {sid}.ask flag
1981
2070
  * PostToolUse -> ⬀ working (refresh, so a mid-turn title flip shows live + clears a stale ◐)
1982
2071
  * Notification -> ◐ awaiting your approval (permission_prompt matcher only)
1983
2072
  * Stop -> ✻ idle / done β€” OR ◐ awaiting (+ a 2nd BEL = gold tab) when the turn ended
1984
2073
  * on a question (last visible response text ends in '?', or a {sid}.ask flag)
1985
2074
  * SessionStart -> ✻ idle "Claude Code β€” New session" (or an existing title on resume/compact)
2075
+ * + inject the FULL RULES block β€” once per session instead of every prompt
2076
+ * (~90% less directive overhead); resume/compact re-inject so a squeezed
2077
+ * context re-learns the rules.
2078
+ *
2079
+ * Dedupe: when this file is the USER-LEVEL copy (~/.claude/hooks) and the project ships its own
2080
+ * managed copy (<cwd>/.claude/hooks/terminal-title.js), this copy stands down entirely β€” otherwise
2081
+ * both hooks inject directives naming two different title files (double tokens, double Writes).
1986
2082
  *
1987
2083
  * The title is set TWO ways on every event: \`process.title\` (= SetConsoleTitleW on Windows β€” the ONLY
1988
2084
  * mechanism that flips the tab on UserPromptSubmit, where Claude Code drops \`terminalSequence\`) AND
@@ -1994,13 +2090,7 @@ other tool calls) so the tab updates WHILE you work, not after. The line's form:
1994
2090
  * ~512KB-capped, written to .titles/_debug.log β€” so it never ships a growing log.
1995
2091
  *
1996
2092
  * Requires \`env.CLAUDE_CODE_DISABLE_TERMINAL_TITLE = "1"\` (set by plugin.json), or CC's own writer races.
1997
- */
1998
- const fs = require('fs');
1999
- const path = require('path');
2000
-
2001
- const ESC = '\\x1b';
2002
- const BEL = '\\x07';
2003
- const EMDASH = String.fromCodePoint(0x2014);`
2093
+ */`
2004
2094
  },
2005
2095
  'feedback-feedback': {
2006
2096
  filename: 'FEEDBACK.md',
@@ -2081,6 +2171,10 @@ Claude reads this file but must never write to it. When Claude discovers project
2081
2171
  {
2082
2172
  "type": "command",
2083
2173
  "command": "node .claude/hooks/terminal-title.js"
2174
+ },
2175
+ {
2176
+ "type": "command",
2177
+ "command": "node .claude/hooks/arcade-beeps.js"
2084
2178
  }
2085
2179
  ]
2086
2180
  }
@@ -2092,6 +2186,10 @@ Claude reads this file but must never write to it. When Claude discovers project
2092
2186
  {
2093
2187
  "type": "command",
2094
2188
  "command": "node .claude/hooks/terminal-title.js"
2189
+ },
2190
+ {
2191
+ "type": "command",
2192
+ "command": "node .claude/hooks/arcade-beeps.js"
2095
2193
  }
2096
2194
  ]
2097
2195
  }
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * arcade-beeps β€” optional Pole-Position status cues for the Claude Code tab.
4
+ * Companion to terminal-title.js. Registered (settings.json) on Stop + Notification.
5
+ *
6
+ * OFF unless the enable flag exists: ~/.claude/sounds/arcade-beeps.enabled
7
+ * toggle via /enable-arcade-beeps /disable-arcade-beeps
8
+ * The flag is GLOBAL (homedir), so one toggle covers every project you've installed into.
9
+ *
10
+ * Mapping (matches the ◐/✻ tab glyph that terminal-title.js paints on the SAME event):
11
+ * Stop, turn ended on a question -> ◐ awaiting -> get-ready tick (pp3-getready-G4.wav, G4/384Hz)
12
+ * Stop, turn ended normally -> ✻ complete -> GO beep (pp3-go-F#5.wav, F#5/759Hz)
13
+ * Notification (permission_prompt) -> ◐ awaiting -> get-ready tick
14
+ *
15
+ * Tones are an extracted+smoothed take on the Pole Position race-start cue (arcade-ping
16
+ * envelope). LOWER tick = awaiting, HIGHER GO = complete.
17
+ *
18
+ * Assets resolve relative to THIS file (../sounds) so a per-project CCA install finds its own
19
+ * copy at <project>/.claude/sounds β€” no dependency on a global sounds dir.
20
+ *
21
+ * Playback is cross-platform and best-effort: PowerShell SoundPlayer on Windows, afplay on
22
+ * macOS, paplay||aplay on Linux. If none is present the hook simply stays silent β€” never errors.
23
+ *
24
+ * Playback BLOCKS (spawnSync) so the sound finishes inside the hook's lifetime β€” a detached
25
+ * fire-and-forget child can be killed by the hook runner's job/process-group cleanup before it
26
+ * plays. Blocking costs ~0.5s on turn-end but is reliable. Still fail-safe: wrapped so it never
27
+ * throws into the turn, and every path ends in exit(0).
28
+ *
29
+ * State detection reuses terminal-title.js's exported inspectLastResponse (lazy-required only
30
+ * when enabled) so sound and glyph agree. A tiny inline '?' check is the fallback.
31
+ *
32
+ * Diagnostic log (bounded ~64KB) at ~/.claude/hooks/.titles/arcade-beeps.log records every
33
+ * invocation + choice, so we can prove whether the hook fires on a real Stop.
34
+ */
35
+ const fs = require('fs');
36
+ const os = require('os');
37
+ const path = require('path');
38
+ const { spawnSync } = require('child_process');
39
+
40
+ const ASSET_DIR = path.join(__dirname, '..', 'sounds'); // wavs ship beside the hook
41
+ const FLAG = path.join(os.homedir(), '.claude', 'sounds', 'arcade-beeps.enabled'); // global toggle
42
+ const LOG = path.join(os.homedir(), '.claude', 'hooks', '.titles', 'arcade-beeps.log');
43
+ const DEBUG = process.env.ARCADE_BEEPS_DEBUG === '1';
44
+
45
+ function logLine(msg) {
46
+ try {
47
+ try { if (fs.statSync(LOG).size > 64 * 1024) fs.renameSync(LOG, `${LOG}.1`); } catch (_) { /* none yet */ }
48
+ fs.appendFileSync(LOG, `${new Date().toISOString()} ${msg}\n`);
49
+ } catch (_) { /* logging must never throw */ }
50
+ }
51
+
52
+ function enabled() { try { return fs.existsSync(FLAG); } catch (_) { return false; } }
53
+ function delay(ms) { return new Promise(r => setTimeout(r, ms)); }
54
+
55
+ // Pick a blocking audio player for the current OS. Returns [cmd, args].
56
+ function playerFor(wav) {
57
+ if (process.platform === 'win32') {
58
+ return ['powershell', ['-NoProfile', '-Command', `(New-Object Media.SoundPlayer '${wav}').PlaySync()`]];
59
+ }
60
+ if (process.platform === 'darwin') {
61
+ return ['afplay', [wav]];
62
+ }
63
+ // linux / other: prefer paplay (PulseAudio/PipeWire), fall back to aplay (ALSA)
64
+ return ['sh', ['-c', `paplay "${wav}" 2>/dev/null || aplay "${wav}" 2>/dev/null`]];
65
+ }
66
+
67
+ function play(wavName, why) {
68
+ if (DEBUG) process.stderr.write(`arcade-beeps: chose ${wavName} (${why})\n`);
69
+ logLine(`play ${wavName} (${why})`);
70
+ try {
71
+ const wav = path.join(ASSET_DIR, wavName);
72
+ if (!fs.existsSync(wav)) { logLine(`MISSING ${wav}`); return; }
73
+ // Block until the sound finishes so it can't be reaped with the hook process.
74
+ const [cmd, cmdArgs] = playerFor(wav);
75
+ spawnSync(cmd, cmdArgs, { stdio: 'ignore', windowsHide: true });
76
+ } catch (e) { logLine(`play-error ${e && e.message}`); }
77
+ }
78
+
79
+ // Fallback: does the last visible assistant text end on a question? (Mirrors terminal-title.js's regex.)
80
+ function endsOnQuestionInline(transcriptPath) {
81
+ try {
82
+ const lines = fs.readFileSync(transcriptPath, 'utf8').split('\n');
83
+ for (let i = lines.length - 1; i >= 0; i--) {
84
+ const l = lines[i].trim(); if (!l) continue;
85
+ let o; try { o = JSON.parse(l); } catch (_) { continue; }
86
+ if (!o || o.type !== 'assistant' || !o.message) continue;
87
+ const c = o.message.content; let t = '';
88
+ if (typeof c === 'string') t = c;
89
+ else if (Array.isArray(c)) t = c.filter(b => b && b.type === 'text' && typeof b.text === 'string').map(b => b.text).join('\n');
90
+ if (t.trim()) return /\?[\s)*_"]*(\([^()]*\)[\s.*_"]*)?$/.test(t);
91
+ }
92
+ } catch (_) { /* ignore */ }
93
+ return false;
94
+ }
95
+
96
+ async function main(input) {
97
+ let data = {};
98
+ try { data = JSON.parse(input); } catch (_) { /* ignore */ }
99
+ const event = data.hook_event_name || '?';
100
+ const en = enabled();
101
+ logLine(`invoked event=${event} enabled=${en ? 1 : 0}`);
102
+ if (!en) return; // opt-in only
103
+
104
+ if (event === 'Notification') { play('pp3-getready-G4.wav', 'notification'); return; }
105
+ if (event !== 'Stop') return;
106
+
107
+ // awaiting (question) vs complete β€” same signals terminal-title.js uses, so the tone matches the glyph.
108
+ const sid = data.session_id || '';
109
+ const askFile = path.join(os.homedir(), '.claude', 'hooks', '.titles', `${sid}.ask`);
110
+ let pending = false;
111
+ try { pending = fs.existsSync(askFile); } catch (_) { /* ignore */ }
112
+
113
+ if (!pending) {
114
+ let inspect = null;
115
+ try { ({ inspectLastResponse: inspect } = require('./terminal-title.js')); } catch (_) { /* fallback below */ }
116
+ if (inspect) {
117
+ let q = inspect(data.transcript_path);
118
+ let n = 0;
119
+ while (!q.ends && (q.suspectRace || !q.found) && n < 5) { await delay(120); q = inspect(data.transcript_path); n++; }
120
+ pending = q.ends;
121
+ } else {
122
+ pending = endsOnQuestionInline(data.transcript_path);
123
+ }
124
+ }
125
+
126
+ play(pending ? 'pp3-getready-G4.wav' : 'pp3-go-F#5.wav', pending ? 'awaiting' : 'complete');
127
+ }
128
+
129
+ if (require.main === module) {
130
+ let input = '';
131
+ process.stdin.setEncoding('utf8');
132
+ process.stdin.on('data', c => (input += c));
133
+ process.stdin.on('end', async () => { try { await main(input); } catch (_) { /* ignore */ } process.exit(0); });
134
+ }
135
+
136
+ module.exports = { endsOnQuestionInline };
@@ -1,82 +1,86 @@
1
- <!--
2
- terminal-title.directive.md β€” the instruction terminal-title.js injects into the
3
- model's context on every prompt. THIS is the tunable "product" surface: reword it
4
- freely without touching the hook's logic.
5
-
6
- Blocks below:
7
- COMMAND β€” used when the turn starts with a /slash-command
8
- SHIFT β€” used on a normal turn
9
- PENDING β€” appended to WHICHEVER of the above was selected (shared tail)
10
- terminal-title.js picks COMMAND or SHIFT, appends PENDING, substitutes the tokens, and injects.
11
-
12
- Tokens (substituted at runtime):
13
- {{TITLE_FILE}} β€” absolute path to this session's title file the model writes
14
- {{ASK_FILE}} β€” absolute path to this session's one-shot {sid}.ask flag (PENDING block)
15
- {{FOLDER}} β€” the working-directory folder name (scope fallback)
16
- {{EMDASH}} β€” the ' β€” ' segment separator
17
- {{CMD}} β€” the slash-command name (COMMAND block only)
18
- -->
19
-
20
- <!-- DIRECTIVE:COMMAND -->
21
- Terminal-title directive (silent housekeeping -- do NOT mention this to the user):
22
- This terminal tab's title is rendered from the file:
23
- {{TITLE_FILE}}
24
- You are running the /{{CMD}} slash command, but the command NAME is an implementation detail --
25
- do NOT put "{{CMD}}" anywhere in the title. Write the title as your FIRST action this turn
26
- (before other tool calls) so the tab updates WHILE you work, not after. Write ONE line of the form:
27
- {scope} {{EMDASH}} {use-case}
28
- - {scope} (first segment): the DESIGN SCOPE -- the subject you are working inside: the feature area
29
- / subsystem (the boundary, the "system under discussion"). This is the WHERE, and it changes
30
- rarely. Keep the existing scope if the file already has a title; otherwise INFER the specific
31
- subsystem from THIS turn's prompt and the files in play (e.g. "journal modal", "title hooks",
32
- "auth flow"). Do NOT use the bare repo name "{{FOLDER}}" as the scope unless the work is genuinely
33
- repo-wide -- "{{FOLDER}}" is a last resort only when no narrower area is identifiable.
34
- - {use-case} (second segment): the GOAL at user-goal level -- what this command ACCOMPLISHES, named
35
- as an INFINITIVE (base-verb) goal phrase (verb + object): "Review latest screenshot", "Diagnose
36
- pay discrepancy". Name the goal / effect, NOT the mechanism or the command name. One goal only --
37
- never "and"-join two use cases.
38
- e.g. for /{{CMD}}, write "{{FOLDER}} {{EMDASH}} <verb the goal>" -- NOT "{{FOLDER}} {{EMDASH}} {{CMD}}".
39
- Use ' {{EMDASH}} ' as the separator; write only the BARE title (a state indicator is prepended
40
- automatically). If the file already exists, read it first then overwrite.
41
- <!-- /DIRECTIVE:COMMAND -->
42
-
43
- <!-- DIRECTIVE:SHIFT -->
44
- Terminal-title directive (silent housekeeping -- do NOT mention this to the user):
45
- This terminal tab's title is rendered from the file:
46
- {{TITLE_FILE}}
47
- When the conversation's SCOPE, use-case, or sub-function SHIFTS -- and only then, not every turn --
48
- write ONE line to that file. When it DOES shift, write it as your FIRST action that turn (before
49
- other tool calls) so the tab updates WHILE you work, not after. The line's form:
50
- {scope} {{EMDASH}} {use-case}
51
- - {scope}: the DESIGN SCOPE -- the subject under discussion: the feature area / subsystem (the
52
- boundary). This is the WHERE; it changes rarely. INFER the specific subsystem from the prompt and
53
- the files in play (e.g. "journal modal", "title hooks"); do NOT default to the bare repo name --
54
- use it only when the work is genuinely repo-wide.
55
- - {use-case}: the GOAL at user-goal level -- what you are accomplishing, as an INFINITIVE (base-verb)
56
- goal phrase (verb + object), e.g. "Refine title taxonomy". Name the goal, not the
57
- mechanism. One goal only.
58
- Add a third segment ( {{EMDASH}} {sub-function} ) only when the work goes a level deeper -- this is the
59
- SUBFUNCTION goal level, a step beneath the user goal.
60
- Rules: use ' {{EMDASH}} ' (space, em-dash, space) as the separator; keep each segment short (a few
61
- words); write only the BARE title -- a state indicator is prepended automatically; leave the
62
- file unchanged when the scope has not moved; this is a compass, so change it rarely. If the file
63
- already exists, read it first then overwrite.
64
- <!-- /DIRECTIVE:SHIFT -->
65
-
66
- <!-- DIRECTIVE:PENDING -->
67
- Pending-question signal: when you END this turn on a question the user must answer before you can
68
- proceed, do BOTH of these as near-final actions so the tab flips to the AWAITING half-circle
69
- (instead of the idle asterisk):
70
- 1. Write the flag file {{ASK_FILE}} (any short content, e.g. "1"). This is the RELIABLE trigger --
71
- it is on disk before the turn ends, so it never misses (the transcript-text check in 2 can race
72
- the turn-end write and silently miss, or miss on phrasing -- see below). The flag is one-shot: it
73
- is consumed at turn end and auto-cleared next turn, so write it ONLY on a turn you are genuinely
74
- blocked on an answer. Write it WHENEVER the turn genuinely needs the user's answer to proceed --
75
- the flag doesn't parse your text, so it is correct even when your closing question is wrapped in
76
- parens, phrased as a parenthetical aside, or is not the literal final character of the message.
77
- 2. Phrase your FINAL line so it ends with a question mark ('?') -- a backup signal, and good UX. A
78
- single trailing parenthetical aside after the '?' is fine ("...option A or B? (I lean B.)"), but
79
- do not rely on phrasing for the signal -- the flag in step 1 is what reliably flips the tab.
80
- Make the closing question self-contained: answerable from the question alone, without re-reading
81
- the response above it. Only do this for a genuine blocking question, never a rhetorical one or a recap.
82
- <!-- /DIRECTIVE:PENDING -->
1
+ <!--
2
+ terminal-title.directive.md β€” the instruction text terminal-title.js injects into the
3
+ model's context. THIS is the tunable "product" surface: reword it freely without touching
4
+ the hook's logic.
5
+
6
+ Token-lean delivery (v2): the full rulebook is injected ONCE per session (SessionStart,
7
+ including resume/compact so a squeezed context re-learns it); every prompt gets only a
8
+ short reminder, plus state-specific addenda the hook selects when they apply.
9
+
10
+ Blocks below:
11
+ RULES β€” the full directive, injected at SessionStart (all sources)
12
+ REMINDER β€” the per-prompt one-liner (every UserPromptSubmit)
13
+ BASELINE β€” appended to REMINDER while no title file exists yet (first turn)
14
+ COMMAND β€” appended to REMINDER when the turn starts with a /slash-command
15
+ terminal-title.js picks REMINDER (+BASELINE/+COMMAND as applicable), substitutes the
16
+ tokens, and injects. SessionStart injects RULES.
17
+
18
+ Tokens (substituted at runtime):
19
+ {{TITLE_FILE}} β€” absolute path to this session's title file the model writes
20
+ {{ASK_FILE}} β€” absolute path to this session's one-shot {sid}.ask flag
21
+ {{FOLDER}} β€” the working-directory folder name (scope fallback)
22
+ {{EMDASH}} β€” the ' β€” ' segment separator
23
+ {{CMD}} β€” the slash-command name (COMMAND block only)
24
+ -->
25
+
26
+ <!-- DIRECTIVE:RULES -->
27
+ Terminal-title directive (silent housekeeping -- never mention it to the user). These rules
28
+ apply to EVERY turn of this session; per-prompt reminders are one-liners that point back here.
29
+
30
+ This terminal tab's title renders from the file:
31
+ {{TITLE_FILE}}
32
+ Maintain it across the session:
33
+ - When the conversation's SCOPE, use-case, or sub-function SHIFTS -- and only then, not every
34
+ turn -- overwrite the file with ONE line: {scope} {{EMDASH}} {use-case}
35
+ Write it as your FIRST action of that turn (before other tool calls) so the tab updates
36
+ WHILE you work, not after. If the file exists, read it first, then overwrite.
37
+ - {scope}: the DESIGN SCOPE -- the feature area / subsystem under discussion (the WHERE; it
38
+ changes rarely). INFER the specific subsystem from the prompt and the files in play (e.g.
39
+ "journal modal", "title hooks"); use the bare repo name "{{FOLDER}}" only when the work is
40
+ genuinely repo-wide -- it is a last resort.
41
+ - {use-case}: the GOAL at user-goal level -- an INFINITIVE (base-verb) goal phrase (verb +
42
+ object), e.g. "Refine title taxonomy". Name the goal, not the mechanism. One goal only --
43
+ never "and"-join two use cases.
44
+ - Add a third segment ( {{EMDASH}} {sub-function} ) only when the work goes a level deeper --
45
+ a step beneath the user goal.
46
+ - Use ' {{EMDASH}} ' (space, em-dash, space) as the separator; keep segments short (a few
47
+ words); write only the BARE title -- a state glyph is prepended automatically. This is a
48
+ compass, not a log: change it rarely.
49
+ - If no title has been set yet, treat your first turn as the baseline: infer the title from
50
+ that turn's prompt and write it immediately, even though nothing has "shifted".
51
+ - Slash-command turns: the command NAME is an implementation detail -- never put it in the
52
+ title. Name the goal the command ACCOMPLISHES ("{{FOLDER}} {{EMDASH}} <verb the goal>",
53
+ never "{{FOLDER}} {{EMDASH}} <command-name>").
54
+
55
+ Pending-question signal: when you END a turn on a question the user must answer before you
56
+ can proceed, do BOTH as near-final actions so the tab flips to the AWAITING half-circle
57
+ (instead of the idle asterisk):
58
+ 1. Write the flag file {{ASK_FILE}} (any short content, e.g. "1"). This is the RELIABLE
59
+ trigger -- it is on disk before the turn ends, so it never misses on phrasing or timing.
60
+ The flag is one-shot (consumed at turn end, auto-cleared next turn), so write it ONLY on
61
+ a turn genuinely blocked on an answer -- but ALWAYS then, even when the closing question
62
+ is wrapped in parens or is not the literal final character of the message.
63
+ 2. Phrase your FINAL line to end with a question mark ('?') -- the backup signal, and good
64
+ UX. A single trailing parenthetical aside after the '?' is fine.
65
+ Make the closing question self-contained: answerable without re-reading the response above
66
+ it. Never signal for a rhetorical question or a recap.
67
+ <!-- /DIRECTIVE:RULES -->
68
+
69
+ <!-- DIRECTIVE:REMINDER -->
70
+ Terminal-title reminder (housekeeping -- never mention to the user; full rules were injected
71
+ at session start): if this turn SHIFTS the scope/use-case, FIRST action: overwrite
72
+ {{TITLE_FILE}} with "{scope} {{EMDASH}} {use-case}". If you END this turn blocked on a
73
+ question, write the flag file {{ASK_FILE}} and end your final line with '?'.
74
+ <!-- /DIRECTIVE:REMINDER -->
75
+
76
+ <!-- DIRECTIVE:BASELINE -->
77
+ No title is set yet -- treat THIS turn as the baseline: infer {scope} {{EMDASH}} {use-case}
78
+ from this prompt and the files in play, and write {{TITLE_FILE}} NOW (first action). Do not
79
+ default to the bare folder name "{{FOLDER}}" unless the work is genuinely repo-wide.
80
+ <!-- /DIRECTIVE:BASELINE -->
81
+
82
+ <!-- DIRECTIVE:COMMAND -->
83
+ This turn runs the /{{CMD}} slash command -- the command NAME is an implementation detail:
84
+ never put "{{CMD}}" in the title. If the scope shifted, name the goal the command
85
+ ACCOMPLISHES (e.g. "<area> {{EMDASH}} <verb the goal>").
86
+ <!-- /DIRECTIVE:COMMAND -->
@@ -2,13 +2,20 @@
2
2
  /**
3
3
  * Terminal Title β€” distributable plugin hook (installed to <project>/.claude/hooks/terminal-title.js).
4
4
  * ONE self-dispatching hook for five events (keyed on hook_event_name):
5
- * UserPromptSubmit -> ⬀ working + inject the title directive (incl. the {sid}.ask path) +
6
- * clear any stale {sid}.ask flag so it reflects only the turn about to run
5
+ * UserPromptSubmit -> ⬀ working + inject the per-prompt REMINDER one-liner (+BASELINE while no
6
+ * title exists, +COMMAND on /slash turns) + clear any stale {sid}.ask flag
7
7
  * PostToolUse -> ⬀ working (refresh, so a mid-turn title flip shows live + clears a stale ◐)
8
8
  * Notification -> ◐ awaiting your approval (permission_prompt matcher only)
9
9
  * Stop -> ✻ idle / done β€” OR ◐ awaiting (+ a 2nd BEL = gold tab) when the turn ended
10
10
  * on a question (last visible response text ends in '?', or a {sid}.ask flag)
11
11
  * SessionStart -> ✻ idle "Claude Code β€” New session" (or an existing title on resume/compact)
12
+ * + inject the FULL RULES block β€” once per session instead of every prompt
13
+ * (~90% less directive overhead); resume/compact re-inject so a squeezed
14
+ * context re-learns the rules.
15
+ *
16
+ * Dedupe: when this file is the USER-LEVEL copy (~/.claude/hooks) and the project ships its own
17
+ * managed copy (<cwd>/.claude/hooks/terminal-title.js), this copy stands down entirely β€” otherwise
18
+ * both hooks inject directives naming two different title files (double tokens, double Writes).
12
19
  *
13
20
  * The title is set TWO ways on every event: `process.title` (= SetConsoleTitleW on Windows β€” the ONLY
14
21
  * mechanism that flips the tab on UserPromptSubmit, where Claude Code drops `terminalSequence`) AND
@@ -23,6 +30,7 @@
23
30
  */
24
31
  const fs = require('fs');
25
32
  const path = require('path');
33
+ const os = require('os');
26
34
 
27
35
  const ESC = '\x1b';
28
36
  const BEL = '\x07';
@@ -61,6 +69,10 @@ async function handle(data) {
61
69
  const event = data.hook_event_name || '';
62
70
  const sid = data.session_id || '';
63
71
  const cwd = data.cwd || process.cwd();
72
+ // User-level copy stands down when a project-level managed copy exists (see header).
73
+ if (shouldDefer(cwd, __dirname, __filename, path.join(os.homedir(), '.claude', 'hooks'))) {
74
+ process.exit(0);
75
+ }
64
76
  // PROJECT-SCOPED title dir (cwd-relative) β€” the plugin installs per-project. (The live ~/.claude
65
77
  // variant uses os.homedir() instead; that is the only difference between the two.)
66
78
  const dir = path.join(cwd, '.claude', 'hooks', '.titles');
@@ -96,7 +108,14 @@ async function handle(data) {
96
108
  if (event === 'SessionStart') {
97
109
  // Fresh-session placeholder; on resume/compact an existing model-authored title is preferred.
98
110
  const title = normalize(readTitle(file) || 'Claude Code - New session');
99
- emit(setTitle(GLYPH.idle, title));
111
+ const out = setTitle(GLYPH.idle, title);
112
+ // Inject the FULL rulebook here β€” once per session β€” instead of on every prompt. All sources
113
+ // get it: startup/clear teach a fresh context, resume/compact re-teach a squeezed one.
114
+ const rules = buildBlocks(['RULES'], file, cwd, '');
115
+ if (rules) {
116
+ out.hookSpecificOutput = { hookEventName: 'SessionStart', additionalContext: rules };
117
+ }
118
+ emit(out);
100
119
  return;
101
120
  }
102
121
 
@@ -281,28 +300,47 @@ function emit(obj) {
281
300
  process.exit(0);
282
301
  }
283
302
 
284
- // Build the injected directive from terminal-title.directive.md (COMMAND block for a /slash-command
285
- // turn, SHIFT otherwise; PENDING tail appended). Wording lives in the .md so it tunes without code edits.
303
+ // Build the per-prompt injection: the REMINDER one-liner, plus BASELINE while no title file
304
+ // exists yet (first turn), plus COMMAND on a /slash-command turn. The full rulebook (RULES) is
305
+ // injected by SessionStart, not here. Wording lives in the .md so it tunes without code edits.
286
306
  function buildDirective(data, file, cwd) {
307
+ const prompt = typeof data.prompt === 'string' ? data.prompt : '';
308
+ const m = prompt.match(/^\s*\/([A-Za-z0-9][\w:.-]*)/);
309
+ const blocks = ['REMINDER'];
310
+ if (!fileExists(file)) blocks.push('BASELINE');
311
+ if (m) blocks.push('COMMAND');
312
+ return buildBlocks(blocks, file, cwd, m ? m[1] : '');
313
+ }
314
+
315
+ // Extract the named blocks from terminal-title.directive.md, join them, substitute the tokens.
316
+ function buildBlocks(names, file, cwd, cmd) {
287
317
  let tpl = '';
288
318
  try {
289
319
  tpl = fs.readFileSync(path.join(__dirname, 'terminal-title.directive.md'), 'utf8');
290
320
  } catch (_) {
291
321
  return '';
292
322
  }
293
- const prompt = typeof data.prompt === 'string' ? data.prompt : '';
294
- const m = prompt.match(/^\s*\/([A-Za-z0-9][\w:.-]*)/);
295
- const cmd = m ? m[1] : '';
296
- const block = extractBlock(tpl, cmd ? 'COMMAND' : 'SHIFT');
297
- if (!block) return '';
298
- const pending = extractBlock(tpl, 'PENDING');
299
- const combined = pending ? `${block}\n\n${pending}` : block;
300
- return combined
323
+ const parts = names.map(n => extractBlock(tpl, n)).filter(Boolean);
324
+ if (!parts.length) return '';
325
+ return parts.join('\n')
301
326
  .split('{{TITLE_FILE}}').join(file)
302
327
  .split('{{ASK_FILE}}').join(file.replace(/\.txt$/, '.ask'))
303
328
  .split('{{FOLDER}}').join(folderName(cwd))
304
329
  .split('{{EMDASH}}').join(EMDASH)
305
- .split('{{CMD}}').join(cmd);
330
+ .split('{{CMD}}').join(cmd || '');
331
+ }
332
+
333
+ // Should THIS copy of the hook stand down? True only for the user-level copy (~/.claude/hooks)
334
+ // running inside a project that ships its own managed copy β€” the project copy wins, so a session
335
+ // gets ONE directive and ONE title file. Parameterized (no ambient __dirname/homedir) for tests.
336
+ function shouldDefer(cwd, selfDir, selfFile, homeHooksDir) {
337
+ try {
338
+ if (path.resolve(selfDir) !== path.resolve(homeHooksDir)) return false; // we ARE the project copy
339
+ const projectCopy = path.join(cwd, '.claude', 'hooks', 'terminal-title.js');
340
+ return fs.existsSync(projectCopy) && path.resolve(projectCopy) !== path.resolve(selfFile);
341
+ } catch (_) {
342
+ return false;
343
+ }
306
344
  }
307
345
 
308
346
  function extractBlock(tpl, name) {
@@ -314,4 +352,4 @@ function extractBlock(tpl, name) {
314
352
  }
315
353
 
316
354
  // Exported for tests (require()'d when require.main !== module). The hook itself never reads these.
317
- module.exports = { inspectLastResponse, normalize, GLYPH };
355
+ module.exports = { inspectLastResponse, normalize, GLYPH, shouldDefer };
@@ -42,6 +42,10 @@
42
42
  {
43
43
  "type": "command",
44
44
  "command": "node .claude/hooks/terminal-title.js"
45
+ },
46
+ {
47
+ "type": "command",
48
+ "command": "node .claude/hooks/arcade-beeps.js"
45
49
  }
46
50
  ]
47
51
  }
@@ -53,6 +57,10 @@
53
57
  {
54
58
  "type": "command",
55
59
  "command": "node .claude/hooks/terminal-title.js"
60
+ },
61
+ {
62
+ "type": "command",
63
+ "command": "node .claude/hooks/arcade-beeps.js"
56
64
  }
57
65
  ]
58
66
  }
Binary file
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## v1.0.198
4
+ - feat(arcade-beeps): ship opt-in Pole Position status beeps via CCA
5
+
6
+ ## v1.0.197
7
+ - perf(terminal-title): deliver the directive once per session; dedupe user/project hooks
8
+
3
9
  ## v1.0.196
4
10
  - fix(terminal-title): stop at the turn boundary when grading the closing '?' (stale-awaiting race)
5
11
  - fix(cli): require non-TTY stdout for inside-Claude block (env var alone false-positives in inherited terminals)
@@ -142,9 +148,3 @@
142
148
  ## v1.0.149
143
149
  - style: consolidate inside-Claude message to two lines
144
150
 
145
- ## v1.0.148
146
- - fix: refine inside-Claude error wording
147
-
148
- ## v1.0.147
149
- - fix: make inside-Claude error message more specific
150
-
package/CLAUDE.md CHANGED
@@ -42,17 +42,13 @@ See `.claude/feedback/` for corrections and guidance from the team.
42
42
  ## Discoveries
43
43
  <!-- Claude: append project-specific learnings, gotchas, and context below. This section persists across /autoconfig runs. -->
44
44
 
45
- ### Publishing to npm β€” web-auth (passkey) flow, run by the human
45
+ ### Publishing to npm β€” bypass-2FA token flow (web-auth publish is a DEAD END)
46
46
 
47
- `npm publish` for this package is a **two-actor** flow; a bare in-agent `npm publish` will FAIL. (Supersedes the auto-generated "## Publishing" one-liner above.)
47
+ (Supersedes the auto-generated "## Publishing" one-liner above.)
48
48
 
49
- - The npm account has **2FA**. A sandboxed/agent shell can't open a browser, so `npm publish` there dies with `npm error code EOTP`. Don't retry it in-agent; don't default to asking for a typed OTP.
50
- - **Working flow:** the agent runs `npm test` + `npm version patch` (commit + tag), then the **human runs the publish in their own terminal** (needs a browser):
51
- ```
52
- cd C:\CODE\claude-code-autoconfig && npm login --auth-type=web && npm publish
53
- ```
54
- β†’ browser opens β†’ pick the Google passkey β†’ close it β†’ `+ claude-code-autoconfig@<version>`.
55
- - **After** the success line, the agent pushes: `git push origin main --follow-tags`. Never double-bump if the version is already bumped. Full procedure: `.claude/commands/deploy-to-npmjs.md` (`/deploy-to-npmjs`).
49
+ - **Primary flow (agent can do it end-to-end):** `npm test` + `npm version patch` (commit + tag) β†’ `npm whoami` to verify the Bypass-2FA granular token in `~/.npmrc` still authenticates β†’ plain `npm publish` (works headless, no OTP) β†’ after the `+ claude-code-autoconfig@<version>` line, `git push origin main --follow-tags`. Never double-bump if the version is already bumped.
50
+ - **If `npm whoami` returns E401**, the token is dead/expired (90-day cap). The human rotates it (npmjs.com β†’ Access Tokens β†’ Granular β†’ "Bypass 2FA", read-write, scoped to this package only) and pastes it into `~/.npmrc` in an editor β€” never via a command. Stopgap without a token: `npm publish --otp=<recovery-code>` in the human's own terminal.
51
+ - **Do NOT use `npm login --auth-type=web && npm publish`:** login succeeds but publish still dies `EOTP` β€” the npm CLI can't route publish-time 2FA to the browser on this webauthn-only account (confirmed 2026-07-03, npm 10.9.2). Don't retry it in-agent, and don't ask for a typed TOTP (none exists). Full procedure: `.claude/commands/deploy-to-npmjs.md` (`/deploy-to-npmjs`).
56
52
 
57
53
  ## Debugging Methodology β€” Evidence Before Solutions
58
54
 
package/README.md CHANGED
@@ -61,7 +61,8 @@ your-project/
61
61
  β”œβ”€β”€ hooks/ # Hook scripts
62
62
  β”‚ β”œβ”€β”€ format.js # Auto-format on Write/Edit
63
63
  β”‚ β”œβ”€β”€ terminal-title.js # Use-case terminal tab titles + live state
64
- β”‚ └── terminal-title.directive.md # Injected title directive (tunable wording)
64
+ β”‚ β”œβ”€β”€ terminal-title.directive.md # Injected title directive (tunable wording)
65
+ β”‚ └── arcade-beeps.js # Optional Pole Position status beeps (opt-in)
65
66
  β”œβ”€β”€ docs/ # Interactive documentation
66
67
  β”‚ └── autoconfig.docs.html # Open with /show-docs
67
68
  β”œβ”€β”€ updates/ # Pending config updates
@@ -70,6 +71,9 @@ your-project/
70
71
  β”‚ └── readme-sync.md # Sync README & docs before publish
71
72
  β”œβ”€β”€ scripts/ # Utility scripts
72
73
  β”‚ └── sync-docs.js # Regenerate interactive HTML docs
74
+ β”œβ”€β”€ sounds/ # Status-cue audio for arcade-beeps
75
+ β”‚ β”œβ”€β”€ pp3-getready-G4.wav # Awaiting tone (get-ready tick)
76
+ β”‚ └── pp3-go-F#5.wav # Complete tone (GO beep)
73
77
  β”œβ”€β”€ .mcp.json # MCP server configs (empty placeholder)
74
78
  └── settings.json # Permissions & security
75
79
  ```
@@ -110,6 +114,8 @@ Autoconfig is **self-configuring**. Run `/autoconfig` and Claude:
110
114
  | `/gls` | Views latest screenshot for visual context |
111
115
  | `/validate-cca-install` | Validates installation against latest published version |
112
116
  | `/extract-rules` | Scan Claude artifacts and extract structured rules |
117
+ | `/enable-arcade-beeps` | Turn on opt-in Pole Position status beeps |
118
+ | `/disable-arcade-beeps` | Turn off the status beeps |
113
119
 
114
120
  ### Updates
115
121
 
@@ -135,6 +141,10 @@ Autoconfig writes a debug methodology to Claude's persistent memory (`MEMORY.md`
135
141
 
136
142
  Run several Claude Code sessions at once and the tabs all look alike. Autoconfig retitles each tab as `{scope} β€” {what you're working on}` with a live state indicator β€” ⬀ working, ◐ waiting on you, ✻ idle β€” so you can tell sessions apart at a glance. Claude updates the title itself as the work shifts; nothing to configure.
137
143
 
144
+ ### Status Beeps (opt-in)
145
+
146
+ Prefer your ears to your eyes? Run `/enable-arcade-beeps` for Pole Position–style tab cues that mirror the title glyph: a low get-ready tick when a session is **waiting on you** and a higher GO tone when it **finishes**. Cross-platform (Windows / macOS / Linux) and off by default β€” toggle back off any time with `/disable-arcade-beeps`.
147
+
138
148
  ### Team Feedback
139
149
 
140
150
  When Claude makes a mistake, add an entry to `.claude/feedback/FEEDBACK.md`:
package/bin/cli.js CHANGED
@@ -27,7 +27,7 @@ const WINDOWS_RESERVED = ['CON', 'PRN', 'AUX', 'NUL', 'COM1', 'COM2', 'COM3', 'C
27
27
  'LPT6', 'LPT7', 'LPT8', 'LPT9'];
28
28
 
29
29
  // Files/folders installed by autoconfig - don't backup these
30
- const AUTOCONFIG_FILES = ['commands', 'docs', 'agents', 'migration', 'hooks', 'scripts', 'rules', 'feedback', 'settings.json', 'settings.local.json', '.mcp.json', '.autoconfig-version', '.autoconfig-plugins.json'];
30
+ const AUTOCONFIG_FILES = ['commands', 'docs', 'agents', 'migration', 'hooks', 'scripts', 'sounds', 'rules', 'feedback', 'settings.json', 'settings.local.json', '.mcp.json', '.autoconfig-version', '.autoconfig-plugins.json'];
31
31
 
32
32
  function isReservedName(name) {
33
33
  const baseName = name.replace(/\.[^.]*$/, '').toUpperCase();
@@ -775,7 +775,7 @@ if (fs.existsSync(feedbackSrc)) {
775
775
  // (copyDirIfMissing), BUT the cca-managed title-hook files are ALWAYS refreshed so bug-fixes
776
776
  // reach existing installs β€” without this, copyDirIfMissing leaves stale hooks in place forever
777
777
  // (same always-overwrite rationale as scripts/ below). --force already overwrites everything.
778
- const MANAGED_HOOKS = ['terminal-title.js', 'terminal-title.directive.md'];
778
+ const MANAGED_HOOKS = ['terminal-title.js', 'terminal-title.directive.md', 'arcade-beeps.js'];
779
779
  if (fs.existsSync(hooksSrc)) {
780
780
  const copyFn = forceMode ? copyDir : copyDirIfMissing;
781
781
  copyFn(hooksSrc, path.join(claudeDest, 'hooks'));
@@ -793,6 +793,12 @@ if (fs.existsSync(scriptsSrc)) {
793
793
  copyDir(scriptsSrc, path.join(claudeDest, 'scripts'));
794
794
  }
795
795
 
796
+ // Copy sounds directory (binary status-cue assets for arcade-beeps; always overwrite)
797
+ const soundsSrc = path.join(packageDir, '.claude', 'sounds');
798
+ if (fs.existsSync(soundsSrc)) {
799
+ copyDir(soundsSrc, path.join(claudeDest, 'sounds'));
800
+ }
801
+
796
802
  // Note: updates directory is no longer copied to user projects.
797
803
  // Update files are only used by --pull-updates (for /autoconfig-update).
798
804
  // On fresh install, all updates are pre-marked as applied and the content
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-autoconfig",
3
- "version": "1.0.196",
3
+ "version": "1.0.198",
4
4
  "description": "Intelligent, self-configuring setup for Claude Code. One command analyzes your project, configures Claude, and shows you what it did.",
5
5
  "author": "ADAC 1001 <info@adac1001.com>",
6
6
  "license": "MIT",
@@ -23,14 +23,14 @@
23
23
  ],
24
24
  "scripts": {
25
25
  "test": "node test/box-alignment.test.js && node test/cli-install.test.js && node test/update-system.test.js && node test/plugin-system.test.js && node test/terminal-title.test.js",
26
- "pub": "npm login --auth-type=web && npm publish",
26
+ "pub": "npm whoami && npm publish",
27
27
  "test:box": "node test/box-alignment.test.js",
28
28
  "test:install": "node test/cli-install.test.js",
29
29
  "test:update": "node test/update-system.test.js",
30
30
  "postversion": "node scripts/generate-changelog.js --postversion"
31
31
  },
32
32
  "bin": {
33
- "claude-code-autoconfig": "./bin/cli.js"
33
+ "claude-code-autoconfig": "bin/cli.js"
34
34
  },
35
35
  "files": [
36
36
  "bin",