claude-code-autoconfig 1.0.197 ā 1.0.199
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/commands/disable-arcade-beeps.md +10 -0
- package/.claude/commands/enable-arcade-beeps.md +10 -0
- package/.claude/docs/autoconfig.docs.html +99 -2
- package/.claude/hooks/arcade-beeps.js +136 -0
- package/.claude/hooks/terminal-title.directive.md +17 -12
- package/.claude/hooks/terminal-title.js +165 -36
- package/.claude/settings.json +8 -0
- package/.claude/sounds/pp3-getready-G4.wav +0 -0
- package/.claude/sounds/pp3-go-F#5.wav +0 -0
- package/CHANGELOG.md +8 -6
- package/README.md +11 -1
- package/bin/cli.js +23 -37
- package/bin/update-summary.js +122 -0
- package/package.json +2 -2
|
@@ -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
|
-
|
|
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
|
-
|
|
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',
|
|
@@ -2082,6 +2171,10 @@ Claude reads this file but must never write to it. When Claude discovers project
|
|
|
2082
2171
|
{
|
|
2083
2172
|
"type": "command",
|
|
2084
2173
|
"command": "node .claude/hooks/terminal-title.js"
|
|
2174
|
+
},
|
|
2175
|
+
{
|
|
2176
|
+
"type": "command",
|
|
2177
|
+
"command": "node .claude/hooks/arcade-beeps.js"
|
|
2085
2178
|
}
|
|
2086
2179
|
]
|
|
2087
2180
|
}
|
|
@@ -2093,6 +2186,10 @@ Claude reads this file but must never write to it. When Claude discovers project
|
|
|
2093
2186
|
{
|
|
2094
2187
|
"type": "command",
|
|
2095
2188
|
"command": "node .claude/hooks/terminal-title.js"
|
|
2189
|
+
},
|
|
2190
|
+
{
|
|
2191
|
+
"type": "command",
|
|
2192
|
+
"command": "node .claude/hooks/arcade-beeps.js"
|
|
2096
2193
|
}
|
|
2097
2194
|
]
|
|
2098
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 };
|
|
@@ -52,25 +52,30 @@ Maintain it across the session:
|
|
|
52
52
|
title. Name the goal the command ACCOMPLISHES ("{{FOLDER}} {{EMDASH}} <verb the goal>",
|
|
53
53
|
never "{{FOLDER}} {{EMDASH}} <command-name>").
|
|
54
54
|
|
|
55
|
-
Pending-question signal
|
|
56
|
-
|
|
57
|
-
(
|
|
55
|
+
Pending-question signal -- END-OF-TURN TEST, apply it on every turn: does your final
|
|
56
|
+
paragraph ask the user something you need answered before the work can continue?
|
|
57
|
+
- YES (you are blocked on the user) -> do BOTH as near-final actions so the tab flips to the
|
|
58
|
+
AWAITING half-circle (instead of the idle asterisk):
|
|
58
59
|
1. Write the flag file {{ASK_FILE}} (any short content, e.g. "1"). This is the RELIABLE
|
|
59
60
|
trigger -- it is on disk before the turn ends, so it never misses on phrasing or timing.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
61
|
+
It is one-shot (consumed at turn end, auto-cleared next turn). Write it even when the
|
|
62
|
+
closing question is wrapped in parens or is not the literal final character.
|
|
63
|
+
2. Make '?' the LAST character of the message. Write NOTHING after the question -- no
|
|
64
|
+
"Standing by.", "Ready when you are.", "Let me know." (a declarative sign-off defeats
|
|
65
|
+
the signal). Only a single short parenthetical aside may follow the '?'. Keep the
|
|
66
|
+
question self-contained: answerable without re-reading the response above it.
|
|
67
|
+
- NO (the question is rhetorical, a recap, or an optional follow-up offer the user is free
|
|
68
|
+
to ignore) -> do NOT write the flag, and end the message on a statement, not a '?' --
|
|
69
|
+
phrase optional offers declaratively ("I can also add tests if you want.").
|
|
67
70
|
<!-- /DIRECTIVE:RULES -->
|
|
68
71
|
|
|
69
72
|
<!-- DIRECTIVE:REMINDER -->
|
|
70
73
|
Terminal-title reminder (housekeeping -- never mention to the user; full rules were injected
|
|
71
74
|
at session start): if this turn SHIFTS the scope/use-case, FIRST action: overwrite
|
|
72
|
-
{{TITLE_FILE}} with "{scope} {{EMDASH}} {use-case}".
|
|
73
|
-
|
|
75
|
+
{{TITLE_FILE}} with "{scope} {{EMDASH}} {use-case}". End-of-turn test: if your final
|
|
76
|
+
paragraph asks something you need answered to continue, write the flag file {{ASK_FILE}} AND
|
|
77
|
+
make '?' the message's last character (nothing after it); otherwise end on a statement, not
|
|
78
|
+
a '?'.
|
|
74
79
|
<!-- /DIRECTIVE:REMINDER -->
|
|
75
80
|
|
|
76
81
|
<!-- DIRECTIVE:BASELINE -->
|
|
@@ -7,7 +7,9 @@
|
|
|
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
|
-
* on a question (last visible response text ends in '?', or a {sid}.ask flag
|
|
10
|
+
* on a question (last visible response text ends in '?', or a {sid}.ask flag;
|
|
11
|
+
* flag turns hand the skipped grade to a detached --post-grade child that logs
|
|
12
|
+
* a StopDiag line when CLAUDE_TITLE_DEBUG=1 ā paint-first, diagnostics after)
|
|
11
13
|
* SessionStart -> ā» idle "Claude Code ā New session" (or an existing title on resume/compact)
|
|
12
14
|
* + inject the FULL RULES block ā once per session instead of every prompt
|
|
13
15
|
* (~90% less directive overhead); resume/compact re-inject so a squeezed
|
|
@@ -53,16 +55,24 @@ let logCtx = null;
|
|
|
53
55
|
// functions below are exported instead, so the stdin read doesn't hang the runner. async because the
|
|
54
56
|
// Stop branch may await a short re-read beat (see the flush-race guard in handle).
|
|
55
57
|
if (require.main === module) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
58
|
+
if (process.argv[2] === '--post-grade') {
|
|
59
|
+
// Detached child mode: grade a flag-turn transcript purely for the debug log (see postGrade).
|
|
60
|
+
// No stdin ā the payload rides argv so the parent never waits on this process. setImmediate
|
|
61
|
+
// defers the run until module evaluation has finished, so no declaration below this block can
|
|
62
|
+
// be hit while still in its temporal dead zone.
|
|
63
|
+
setImmediate(() => postGrade(process.argv[3]).then(() => process.exit(0), () => process.exit(0)));
|
|
64
|
+
} else {
|
|
65
|
+
let input = '';
|
|
66
|
+
process.stdin.setEncoding('utf8');
|
|
67
|
+
process.stdin.on('data', chunk => (input += chunk));
|
|
68
|
+
process.stdin.on('end', async () => {
|
|
69
|
+
try {
|
|
70
|
+
await handle(JSON.parse(input));
|
|
71
|
+
} catch (err) {
|
|
72
|
+
process.exit(0); // never break the turn on a title error ā emit nothing
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
66
76
|
}
|
|
67
77
|
|
|
68
78
|
async function handle(data) {
|
|
@@ -128,16 +138,41 @@ async function handle(data) {
|
|
|
128
138
|
// Stop: idle, UNLESS the turn ended on a question the user must answer ā then awaiting + a 2nd BEL
|
|
129
139
|
// so VS Code paints the (otherwise bell-less) tab gold. "Ended on a question" = last visible
|
|
130
140
|
// assistant text ends in '?' (transcript heuristic) OR an explicit {sid}.ask flag (consumed here).
|
|
141
|
+
|
|
142
|
+
// FAILSAFE PRE-PAINT ā flip to ā» idle SYNCHRONOUSLY now, before the async grade below. That grade
|
|
143
|
+
// reads/re-reads the transcript to dodge the flush race and can either throw (caught ā exit 0, emits
|
|
144
|
+
// nothing) or be killed on a huge, slow-to-flush final message ā either way it would otherwise leave
|
|
145
|
+
// the tab stuck on the last ⬤. process.title (SetConsoleTitleW) takes effect immediately and persists
|
|
146
|
+
// after exit, so the tab is correct even if we die below. Idle is the default Stop outcome; the grade
|
|
147
|
+
// only ever UPGRADES it to ā awaiting (worst case: a <1s ā» flash before ā on a question turn, and the
|
|
148
|
+
// {sid}.ask flag already backstops that case).
|
|
149
|
+
try { process.title = `${GLYPH.idle} ${normalize(readTitle(file) || folderName(cwd))}`; } catch (_) { /* ignore */ }
|
|
150
|
+
|
|
131
151
|
const askFile = path.join(dir, `${sid}.ask`);
|
|
132
152
|
const askPresent = fileExists(askFile);
|
|
153
|
+
if (askPresent) { try { fs.unlinkSync(askFile); } catch (_) { /* ignore */ } }
|
|
133
154
|
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
// is
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
|
|
155
|
+
// FAST PATH ā the {sid}.ask flag is the race-proof "ended on a question" signal, written to disk BEFORE
|
|
156
|
+
// Stop fires. When present, paint ā awaiting and emit() IMMEDIATELY, skipping the transcript grade below.
|
|
157
|
+
// This is the stuck-⬤ fix: emit() (the terminalSequence CC applies on clean exit) is the ONLY paint VS
|
|
158
|
+
// Code honors, and the async grade can be KILLED on a huge / slow-to-flush transcript before it reaches
|
|
159
|
+
// emit() ā which froze the tab on the last ⬤ working. Reaching emit() synchronously here closes that
|
|
160
|
+
// window for every flagged question turn (the common case).
|
|
161
|
+
if (askPresent) {
|
|
162
|
+
const deferred = spawnDeferredGrade(data, dir, sid, file, cwd);
|
|
163
|
+
if (logCtx) {
|
|
164
|
+
logCtx.note = 'ask-flag';
|
|
165
|
+
logCtx.diag = `ask=1 fast-path (${deferred ? 'grade deferred to StopDiag' : 'grade skipped'})`;
|
|
166
|
+
}
|
|
167
|
+
emit(setTitle(GLYPH.awaiting, normalize(readTitle(file) || folderName(cwd)), true));
|
|
168
|
+
return; // emit() exits; the return keeps control flow honest if that ever changes
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// No flag ā default idle, but the turn may have ended on '?' without one. Grade the transcript, guarding
|
|
172
|
+
// the flush race: the final assistant text can land in the JSONL a beat AFTER Stop fires (~200ms append
|
|
173
|
+
// lag). `suspectRace` (freshest on-disk assistant block is text-less, or none found) means the real final
|
|
174
|
+
// message is still flushing ā re-read a few times before grading. Each pass reads only the transcript
|
|
175
|
+
// TAIL, so the loop stays fast on a multi-MB transcript and always reaches emit().
|
|
141
176
|
let q = inspectLastResponse(data.transcript_path);
|
|
142
177
|
let reread = 0;
|
|
143
178
|
while (!q.ends && (q.suspectRace || !q.found) && reread < 7) {
|
|
@@ -147,13 +182,10 @@ async function handle(data) {
|
|
|
147
182
|
if (q.ends || (q.found && !q.suspectRace)) break;
|
|
148
183
|
}
|
|
149
184
|
|
|
150
|
-
|
|
151
|
-
let note = pending ? 'q-mark' : 'idle';
|
|
152
|
-
if (!pending && askPresent) { pending = true; note = 'ask-flag'; }
|
|
153
|
-
if (askPresent) { try { fs.unlinkSync(askFile); } catch (_) { /* ignore */ } }
|
|
185
|
+
const pending = q.ends;
|
|
154
186
|
if (logCtx) {
|
|
155
|
-
logCtx.note =
|
|
156
|
-
logCtx.diag = `ask=${
|
|
187
|
+
logCtx.note = pending ? 'q-mark' : 'idle';
|
|
188
|
+
logCtx.diag = `ask=0 qmark=${q.ends ? 1 : 0} via=${q.via || '-'} found=${q.found ? 1 : 0} reread=${reread} model=${q.model || '-'} tail="${q.tail}"`;
|
|
157
189
|
}
|
|
158
190
|
const glyph = pending ? GLYPH.awaiting : GLYPH.idle;
|
|
159
191
|
emit(setTitle(glyph, normalize(readTitle(file) || folderName(cwd)), pending));
|
|
@@ -203,11 +235,30 @@ function fileExists(file) {
|
|
|
203
235
|
// shows found=0 (or a stale tail), a genuine regex miss shows a tail that's present but doesn't end
|
|
204
236
|
// in '?'. Any error ā a blank record (treated as "no question"), matching the old false return.
|
|
205
237
|
function inspectLastResponse(transcriptPath) {
|
|
206
|
-
const blank = { ends: false, found: false, tail: '', suspectRace: false };
|
|
238
|
+
const blank = { ends: false, via: '', found: false, tail: '', suspectRace: false, model: '' };
|
|
207
239
|
if (!transcriptPath) return blank;
|
|
208
240
|
let content;
|
|
209
241
|
try {
|
|
210
|
-
|
|
242
|
+
// Read only the TAIL of the transcript. The current turn's final message sits at the very end, and
|
|
243
|
+
// reading the whole multi-MB JSONL of a long session ā then re-reading it up to 7Ć in the flush-race
|
|
244
|
+
// loop ā is what let a Stop grade run long enough to be killed before it painted (the stuck-⬤ bug).
|
|
245
|
+
// A fixed tail keeps every pass fast regardless of session length; the leading partial line is dropped.
|
|
246
|
+
const TAIL_BYTES = 1024 * 1024;
|
|
247
|
+
const size = fs.statSync(transcriptPath).size;
|
|
248
|
+
if (size <= TAIL_BYTES) {
|
|
249
|
+
content = fs.readFileSync(transcriptPath, 'utf8');
|
|
250
|
+
} else {
|
|
251
|
+
const fd = fs.openSync(transcriptPath, 'r');
|
|
252
|
+
try {
|
|
253
|
+
const buf = Buffer.alloc(TAIL_BYTES);
|
|
254
|
+
fs.readSync(fd, buf, 0, TAIL_BYTES, size - TAIL_BYTES);
|
|
255
|
+
const tail = buf.toString('utf8');
|
|
256
|
+
const nl = tail.indexOf('\n');
|
|
257
|
+
content = nl >= 0 ? tail.slice(nl + 1) : tail; // drop the partial first line
|
|
258
|
+
} finally {
|
|
259
|
+
fs.closeSync(fd);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
211
262
|
} catch (_) {
|
|
212
263
|
return blank;
|
|
213
264
|
}
|
|
@@ -244,15 +295,13 @@ function inspectLastResponse(transcriptPath) {
|
|
|
244
295
|
if (text.trim()) {
|
|
245
296
|
// last ~60 chars, collapsed to one line and quote-stripped so it can't break the log framing
|
|
246
297
|
const tail = text.trim().slice(-60).replace(/\s+/g, ' ').replace(/"/g, "'");
|
|
247
|
-
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const endsOnQuestion = /\?[\s)*_"]*(\([^()]*\)[\s.*_"]*)?$/.test(text);
|
|
255
|
-
return { ends: endsOnQuestion, found: true, tail, suspectRace: sawTextlessAssistant };
|
|
298
|
+
const q = endsOnQuestion(text);
|
|
299
|
+
// `model` (e.g. "claude-fable-5") rides into the debug diag so per-model miss rates can be
|
|
300
|
+
// compared straight from _debug.log.
|
|
301
|
+
return {
|
|
302
|
+
ends: q.ends, via: q.via, found: true, tail,
|
|
303
|
+
suspectRace: sawTextlessAssistant, model: (obj.message && obj.message.model) || '',
|
|
304
|
+
};
|
|
256
305
|
}
|
|
257
306
|
// assistant message with no visible text = a thinking-only or tool_use-only block sitting AFTER the
|
|
258
307
|
// last text we'll grade ā a strong hint the final text line hasn't flushed yet.
|
|
@@ -261,6 +310,36 @@ function inspectLastResponse(transcriptPath) {
|
|
|
261
310
|
return blank;
|
|
262
311
|
}
|
|
263
312
|
|
|
313
|
+
// Did the text end on a question the user must answer? Two tiers (via names the matched one):
|
|
314
|
+
// 'qtail' ā the text itself ends on '?', tolerating trailing whitespace / ) * _ " AND one
|
|
315
|
+
// trailing parenthetical aside ("How should we handle it? (I lean option 2.)").
|
|
316
|
+
// A mid-message rhetorical '?' or a plain statement ending in ')' won't match.
|
|
317
|
+
// 'signoff' ā the text ends on ONE short declarative sign-off line BELOW a question-ending
|
|
318
|
+
// line ("ā¦which do you prefer?\n\nLet me know."). The directive forbids the
|
|
319
|
+
// sign-off, but a model that forgets it shouldn't cost the user the ā ā so
|
|
320
|
+
// tolerate exactly one trailing line, and only when it is short (ā¤48 chars),
|
|
321
|
+
// '?'-free, and not list/heading/quote/table/fence content, so a question
|
|
322
|
+
// followed by real elaboration (options list, explanation) still grades idle.
|
|
323
|
+
// Same-line trailing statements ("Want me to proceed? Done.") stay non-questions
|
|
324
|
+
// ā a mid-line '?' is exactly the rhetorical shape the grade must not fire on.
|
|
325
|
+
// Belt to the {sid}.ask flag's suspenders: the flag is the primary, parse-free path; this only
|
|
326
|
+
// hardens the transcript fallback for turns that didn't write one.
|
|
327
|
+
// QTAIL lives INSIDE the function (not module-level const) ā the --post-grade child calls into
|
|
328
|
+
// this during module evaluation, and a module-level const below the require.main block is still
|
|
329
|
+
// in its temporal dead zone at that point.
|
|
330
|
+
function endsOnQuestion(text) {
|
|
331
|
+
const QTAIL = /\?[\s)*_"]*(\([^()]*\)[\s.*_"]*)?$/;
|
|
332
|
+
if (QTAIL.test(text)) return { ends: true, via: 'qtail' };
|
|
333
|
+
const lines = text.split('\n').map(l => l.trim()).filter(Boolean);
|
|
334
|
+
if (lines.length >= 2) {
|
|
335
|
+
const last = lines[lines.length - 1];
|
|
336
|
+
const isSignoff = last.length <= 48 && !last.includes('?')
|
|
337
|
+
&& !/^(?:[-*ā¢>]\s|#{1,6}\s|\||\d+[.)]\s|`{3})/.test(last);
|
|
338
|
+
if (isSignoff && QTAIL.test(lines[lines.length - 2])) return { ends: true, via: 'signoff' };
|
|
339
|
+
}
|
|
340
|
+
return { ends: false, via: '' };
|
|
341
|
+
}
|
|
342
|
+
|
|
264
343
|
// A genuine human prompt (real text) vs a tool_result-carrier user message (content is only
|
|
265
344
|
// tool_result blocks). Marks the boundary of the current turn's response while walking the transcript.
|
|
266
345
|
function isRealUserPrompt(obj) {
|
|
@@ -273,6 +352,56 @@ function isRealUserPrompt(obj) {
|
|
|
273
352
|
// Event-loop-friendly sleep for the Stop flush-race re-read beat (handle awaits this; no busy-wait).
|
|
274
353
|
function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }
|
|
275
354
|
|
|
355
|
+
// Deferred flag-turn grade (debug-gated). The `.ask` fast path paints ā without reading the
|
|
356
|
+
// transcript, which blinded _debug.log's qmark/via/tail diagnostics on exactly the turns where the
|
|
357
|
+
// flag+sign-off misuse pattern shows up. Grading BEFORE emit() would re-open the kill window the
|
|
358
|
+
// fast path exists to close, and nothing runs after emit() (it exits) ā so hand the grade to a
|
|
359
|
+
// DETACHED child (this same file, --post-grade) that logs on its own time. unref() + ignored stdio
|
|
360
|
+
// mean the parent's exit ā and therefore the paint ā is never delayed by even one re-read beat.
|
|
361
|
+
function spawnDeferredGrade(data, dir, sid, file, cwd) {
|
|
362
|
+
if (process.env.CLAUDE_TITLE_DEBUG !== '1') return false;
|
|
363
|
+
try {
|
|
364
|
+
const payload = JSON.stringify({
|
|
365
|
+
sid, dir,
|
|
366
|
+
transcriptPath: data.transcript_path || '',
|
|
367
|
+
title: normalize(readTitle(file) || folderName(cwd)),
|
|
368
|
+
});
|
|
369
|
+
const { spawn } = require('child_process');
|
|
370
|
+
spawn(process.execPath, [__filename, '--post-grade', payload], {
|
|
371
|
+
detached: true, stdio: 'ignore', windowsHide: true,
|
|
372
|
+
env: Object.assign({}, process.env, { CLAUDE_TITLE_DEBUG: '1' }),
|
|
373
|
+
}).unref();
|
|
374
|
+
return true;
|
|
375
|
+
} catch (_) {
|
|
376
|
+
return false; // diagnostics are best-effort; the paint path never depends on this
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Child mode (--post-grade): grade the transcript purely for the debug log, after the paint already
|
|
381
|
+
// happened. Same flush-race re-read loop as the main Stop grade (and by child-start the final message
|
|
382
|
+
// has usually flushed, so this data is CLEANER than an inline grade would have been). Logs as
|
|
383
|
+
// event=StopDiag, so flag-turn protocol compliance reads straight out of _debug.log:
|
|
384
|
+
// qmark=1 via=qtail -> full compliance (flag AND '?'-last)
|
|
385
|
+
// qmark=1 via=signoff -> flag + banned closer appended after the question (the either/or pattern)
|
|
386
|
+
// qmark=0 -> flag-only; the message ended on a statement
|
|
387
|
+
async function postGrade(payloadJson) {
|
|
388
|
+
const p = JSON.parse(payloadJson || '{}');
|
|
389
|
+
let q = inspectLastResponse(p.transcriptPath);
|
|
390
|
+
let reread = 0;
|
|
391
|
+
while (!q.ends && (q.suspectRace || !q.found) && reread < 7) {
|
|
392
|
+
await delay(120);
|
|
393
|
+
reread++;
|
|
394
|
+
q = inspectLastResponse(p.transcriptPath);
|
|
395
|
+
if (q.ends || (q.found && !q.suspectRace)) break;
|
|
396
|
+
}
|
|
397
|
+
logCtx = {
|
|
398
|
+
event: 'StopDiag', sid: p.sid || '', dir: p.dir || '', note: 'ask-flag',
|
|
399
|
+
diag: `ask=1 qmark=${q.ends ? 1 : 0} via=${q.via || '-'} found=${q.found ? 1 : 0} reread=${reread} model=${q.model || '-'} tail="${q.tail}"`,
|
|
400
|
+
};
|
|
401
|
+
// Reuses titleLog's capped append; glyph/ring mirror what the fast path actually painted.
|
|
402
|
+
titleLog(GLYPH.awaiting, p.title || '', true);
|
|
403
|
+
}
|
|
404
|
+
|
|
276
405
|
function readTitle(file) {
|
|
277
406
|
try {
|
|
278
407
|
if (fs.existsSync(file)) return fs.readFileSync(file, 'utf8').trim();
|
|
@@ -352,4 +481,4 @@ function extractBlock(tpl, name) {
|
|
|
352
481
|
}
|
|
353
482
|
|
|
354
483
|
// Exported for tests (require()'d when require.main !== module). The hook itself never reads these.
|
|
355
|
-
module.exports = { inspectLastResponse, normalize, GLYPH, shouldDefer };
|
|
484
|
+
module.exports = { inspectLastResponse, endsOnQuestion, normalize, GLYPH, shouldDefer };
|
package/.claude/settings.json
CHANGED
|
@@ -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
|
|
Binary file
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v1.0.199
|
|
4
|
+
- feat(cli): grouped update summary on upgrade
|
|
5
|
+
- feat(terminal-title): awaiting-signal hardening ā sign-off-tolerant grade, deferred flag-turn diagnostics, failsafe pre-paint
|
|
6
|
+
- fix(terminal-title): require the '?' to be the last character of a pending-question turn
|
|
7
|
+
|
|
8
|
+
## v1.0.198
|
|
9
|
+
- feat(arcade-beeps): ship opt-in Pole Position status beeps via CCA
|
|
10
|
+
|
|
3
11
|
## v1.0.197
|
|
4
12
|
- perf(terminal-title): deliver the directive once per session; dedupe user/project hooks
|
|
5
13
|
|
|
@@ -142,9 +150,3 @@
|
|
|
142
150
|
## v1.0.150
|
|
143
151
|
- style: use pointing emoji in inside-Claude message
|
|
144
152
|
|
|
145
|
-
## v1.0.149
|
|
146
|
-
- style: consolidate inside-Claude message to two lines
|
|
147
|
-
|
|
148
|
-
## v1.0.148
|
|
149
|
-
- fix: refine inside-Claude error wording
|
|
150
|
-
|
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
|
-
ā
|
|
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
|
@@ -4,6 +4,7 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const readline = require('readline');
|
|
6
6
|
const { execSync, spawn } = require('child_process');
|
|
7
|
+
const { formatUpdateSummary } = require('./update-summary.js');
|
|
7
8
|
|
|
8
9
|
const cwd = process.cwd();
|
|
9
10
|
const packageDir = path.dirname(__dirname);
|
|
@@ -27,7 +28,7 @@ const WINDOWS_RESERVED = ['CON', 'PRN', 'AUX', 'NUL', 'COM1', 'COM2', 'COM3', 'C
|
|
|
27
28
|
'LPT6', 'LPT7', 'LPT8', 'LPT9'];
|
|
28
29
|
|
|
29
30
|
// 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'];
|
|
31
|
+
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
32
|
|
|
32
33
|
function isReservedName(name) {
|
|
33
34
|
const baseName = name.replace(/\.[^.]*$/, '').toUpperCase();
|
|
@@ -775,7 +776,7 @@ if (fs.existsSync(feedbackSrc)) {
|
|
|
775
776
|
// (copyDirIfMissing), BUT the cca-managed title-hook files are ALWAYS refreshed so bug-fixes
|
|
776
777
|
// reach existing installs ā without this, copyDirIfMissing leaves stale hooks in place forever
|
|
777
778
|
// (same always-overwrite rationale as scripts/ below). --force already overwrites everything.
|
|
778
|
-
const MANAGED_HOOKS = ['terminal-title.js', 'terminal-title.directive.md'];
|
|
779
|
+
const MANAGED_HOOKS = ['terminal-title.js', 'terminal-title.directive.md', 'arcade-beeps.js'];
|
|
779
780
|
if (fs.existsSync(hooksSrc)) {
|
|
780
781
|
const copyFn = forceMode ? copyDir : copyDirIfMissing;
|
|
781
782
|
copyFn(hooksSrc, path.join(claudeDest, 'hooks'));
|
|
@@ -793,6 +794,12 @@ if (fs.existsSync(scriptsSrc)) {
|
|
|
793
794
|
copyDir(scriptsSrc, path.join(claudeDest, 'scripts'));
|
|
794
795
|
}
|
|
795
796
|
|
|
797
|
+
// Copy sounds directory (binary status-cue assets for arcade-beeps; always overwrite)
|
|
798
|
+
const soundsSrc = path.join(packageDir, '.claude', 'sounds');
|
|
799
|
+
if (fs.existsSync(soundsSrc)) {
|
|
800
|
+
copyDir(soundsSrc, path.join(claudeDest, 'sounds'));
|
|
801
|
+
}
|
|
802
|
+
|
|
796
803
|
// Note: updates directory is no longer copied to user projects.
|
|
797
804
|
// Update files are only used by --pull-updates (for /autoconfig-update).
|
|
798
805
|
// On fresh install, all updates are pre-marked as applied and the content
|
|
@@ -950,43 +957,22 @@ if (isUpgrade) {
|
|
|
950
957
|
console.log('\x1b[33mā ā\x1b[0m');
|
|
951
958
|
console.log('\x1b[33māāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\x1b[0m');
|
|
952
959
|
}
|
|
953
|
-
// Show
|
|
954
|
-
|
|
960
|
+
// Show what changed on the upgrade path so a re-run never looks like "nothing came down":
|
|
961
|
+
// grouped features/fixes since the installed version, or a single confirmation line when
|
|
962
|
+
// already on the latest. Rendered here so it lands right before the ENTER prompt.
|
|
963
|
+
// Logic lives in update-summary.js (pure + unit-tested).
|
|
964
|
+
if (isUpgrade) {
|
|
955
965
|
const changelogPath = path.join(packageDir, 'CHANGELOG.md');
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
const patch = parseInt(ver.split('.').pop(), 10);
|
|
965
|
-
currentEntry = patch > prevPatch ? { ver, items: [] } : null;
|
|
966
|
-
} else if (currentEntry && line.startsWith('- ')) {
|
|
967
|
-
currentEntry.items.push(line.slice(2));
|
|
968
|
-
} else if (currentEntry && line === '' && currentEntry.items.length > 0) {
|
|
969
|
-
entries.push(currentEntry);
|
|
970
|
-
currentEntry = null;
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
|
-
if (currentEntry && currentEntry.items.length > 0) entries.push(currentEntry);
|
|
974
|
-
if (entries.length > 0) {
|
|
975
|
-
console.log(`\x1b[90m What's new since v${previousVersion}:\x1b[0m`);
|
|
976
|
-
console.log();
|
|
977
|
-
const show = entries.slice(0, 10);
|
|
978
|
-
for (const e of show) {
|
|
979
|
-
for (const item of e.items) {
|
|
980
|
-
console.log(`\x1b[90m ${e.ver} ā ${item}\x1b[0m`);
|
|
981
|
-
}
|
|
982
|
-
}
|
|
983
|
-
const remaining = entries.length - show.length;
|
|
984
|
-
if (remaining > 0) {
|
|
985
|
-
console.log(`\x1b[90m ... and ${remaining} more (see CHANGELOG.md)\x1b[0m`);
|
|
986
|
-
}
|
|
987
|
-
console.log();
|
|
988
|
-
}
|
|
966
|
+
const changelogText = fs.existsSync(changelogPath) ? fs.readFileSync(changelogPath, 'utf8') : '';
|
|
967
|
+
console.log();
|
|
968
|
+
for (const seg of formatUpdateSummary(previousVersion, currentVersion, changelogText)) {
|
|
969
|
+
if (seg.kind === 'latest') console.log(`\x1b[32m ā ${seg.text}\x1b[0m`);
|
|
970
|
+
else if (seg.kind === 'heading') console.log(`\x1b[36m ${seg.text}\x1b[0m`);
|
|
971
|
+
else if (seg.kind === 'group') console.log(`\x1b[33m ${seg.text}:\x1b[0m`);
|
|
972
|
+
else if (seg.kind === 'item') console.log(`\x1b[90m ⢠${seg.text}\x1b[0m`);
|
|
973
|
+
else if (seg.kind === 'more') console.log(`\x1b[90m ${seg.text}\x1b[0m`);
|
|
989
974
|
}
|
|
975
|
+
console.log();
|
|
990
976
|
}
|
|
991
977
|
if (!isUpgrade) {
|
|
992
978
|
console.log('\x1b[90m%s\x1b[0m', "You'll need to approve a few file prompts to complete the installation.");
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* update-summary.js ā renders the "what changed" summary shown on the installer's
|
|
5
|
+
* upgrade path (right before the ENTER prompt).
|
|
6
|
+
*
|
|
7
|
+
* Pure + side-effect free so it can be unit-tested without executing the installer
|
|
8
|
+
* (cli.js runs its whole flow on require, so its inline logic can't be imported).
|
|
9
|
+
*
|
|
10
|
+
* formatUpdateSummary() returns an array of { kind, text } segments; the caller
|
|
11
|
+
* (cli.js) maps kind -> color + indent:
|
|
12
|
+
* 'latest' -> already on the newest version (a single line, no feature relist)
|
|
13
|
+
* 'heading' -> "What's new since your last update (vā¦):"
|
|
14
|
+
* 'group' -> "New features" / "Fixes & improvements"
|
|
15
|
+
* 'item' -> one change bullet
|
|
16
|
+
* 'more' -> "⦠and N more" overflow line
|
|
17
|
+
*
|
|
18
|
+
* Source of truth is CHANGELOG.md ("## vX.Y.Z" headers, "- type(scope): summary" bullets).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const MAX_ITEMS = 12;
|
|
22
|
+
// Conventional-commit types that are housekeeping, not user-facing features/fixes.
|
|
23
|
+
const SKIP_TYPES = new Set(['chore', 'docs', 'test', 'ci', 'build', 'style']);
|
|
24
|
+
|
|
25
|
+
function parseVersion(v) {
|
|
26
|
+
return String(v || '').replace(/^v/, '').split('.').map(n => parseInt(n, 10) || 0);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// -1 if a < b, 0 if equal, 1 if a > b (segment-wise numeric compare).
|
|
30
|
+
function compareVersions(a, b) {
|
|
31
|
+
const pa = parseVersion(a), pb = parseVersion(b);
|
|
32
|
+
const len = Math.max(pa.length, pb.length);
|
|
33
|
+
for (let i = 0; i < len; i++) {
|
|
34
|
+
const d = (pa[i] || 0) - (pb[i] || 0);
|
|
35
|
+
if (d !== 0) return d < 0 ? -1 : 1;
|
|
36
|
+
}
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Parse "## vX.Y.Z" sections -> [{ ver, items: [rawBullet, ...] }] in file order (newest first).
|
|
41
|
+
function parseChangelog(text) {
|
|
42
|
+
const entries = [];
|
|
43
|
+
let cur = null;
|
|
44
|
+
for (const line of String(text || '').split(/\r?\n/)) {
|
|
45
|
+
if (line.startsWith('## v')) {
|
|
46
|
+
cur = { ver: line.slice(3).trim(), items: [] };
|
|
47
|
+
entries.push(cur);
|
|
48
|
+
} else if (cur && line.startsWith('- ')) {
|
|
49
|
+
cur.items.push(line.slice(2).trim());
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return entries;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function capitalize(s) {
|
|
56
|
+
s = String(s || '').trim();
|
|
57
|
+
return s ? s[0].toUpperCase() + s.slice(1) : s;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// "feat(scope): do a thing" -> { type: 'feat', text: 'Do a thing' }
|
|
61
|
+
function classifyBullet(raw) {
|
|
62
|
+
const m = String(raw).match(/^(\w+)(?:\([^)]*\))?:\s*(.*)$/);
|
|
63
|
+
if (!m) return { type: 'other', text: capitalize(raw) };
|
|
64
|
+
return { type: m[1].toLowerCase(), text: capitalize(m[2]) };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function formatUpdateSummary(previousVersion, currentVersion, changelogText) {
|
|
68
|
+
// Already current ā a re-run. One confirmation line, no feature relist.
|
|
69
|
+
if (previousVersion && compareVersions(previousVersion, currentVersion) === 0) {
|
|
70
|
+
return [{ kind: 'latest', text: `You're already on the latest version (v${currentVersion})` }];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const entries = parseChangelog(changelogText);
|
|
74
|
+
|
|
75
|
+
// Which versions to describe:
|
|
76
|
+
// known older prev -> everything newer than prev (since your last update)
|
|
77
|
+
// unknown prev -> just the current version's own entry
|
|
78
|
+
let relevant, heading;
|
|
79
|
+
if (previousVersion) {
|
|
80
|
+
relevant = entries.filter(e => compareVersions(e.ver, previousVersion) > 0);
|
|
81
|
+
heading = `What's new since your last update (v${previousVersion}):`;
|
|
82
|
+
} else {
|
|
83
|
+
relevant = entries.filter(e => compareVersions(e.ver, currentVersion) === 0);
|
|
84
|
+
heading = `This install includes (v${currentVersion}):`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Split bullets into two buckets, dropping housekeeping types.
|
|
88
|
+
const features = [], fixes = [];
|
|
89
|
+
for (const e of relevant) {
|
|
90
|
+
for (const raw of e.items) {
|
|
91
|
+
const c = classifyBullet(raw);
|
|
92
|
+
if (SKIP_TYPES.has(c.type)) continue;
|
|
93
|
+
(c.type === 'feat' ? features : fixes).push(c.text);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Upgrade with nothing user-facing (e.g. all housekeeping) ā just confirm the bump.
|
|
98
|
+
if (features.length === 0 && fixes.length === 0) {
|
|
99
|
+
return [{ kind: 'latest', text: `Updated to v${currentVersion}` }];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const out = [{ kind: 'heading', text: heading }];
|
|
103
|
+
let shown = 0, overflow = 0;
|
|
104
|
+
|
|
105
|
+
function emitGroup(label, items) {
|
|
106
|
+
if (items.length === 0) return;
|
|
107
|
+
const take = items.slice(0, Math.max(0, MAX_ITEMS - shown));
|
|
108
|
+
overflow += items.length - take.length;
|
|
109
|
+
if (take.length === 0) return; // no room left; counted toward overflow
|
|
110
|
+
out.push({ kind: 'group', text: label });
|
|
111
|
+
for (const t of take) { out.push({ kind: 'item', text: t }); shown++; }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
emitGroup('New features', features);
|
|
115
|
+
emitGroup('Fixes & improvements', fixes);
|
|
116
|
+
|
|
117
|
+
if (overflow > 0) out.push({ kind: 'more', text: `⦠and ${overflow} more (see CHANGELOG.md)` });
|
|
118
|
+
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
module.exports = { formatUpdateSummary, compareVersions, parseChangelog, classifyBullet };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-autoconfig",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.199",
|
|
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",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"cli"
|
|
23
23
|
],
|
|
24
24
|
"scripts": {
|
|
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",
|
|
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 && node test/update-summary.test.js",
|
|
26
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",
|