claude-code-autoconfig 1.0.188 → 1.0.191

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.
@@ -270,12 +270,7 @@ Run `/show-docs` to view the interactive documentation (requires full install vi
270
270
 
271
271
  **IMPORTANT**: Do not add any additional text, summaries, or details after this message. The finale should be clean and punchy - end here.
272
272
 
273
- ### Windows Cleanup
274
-
275
- On Windows, if a `nul` file was accidentally created in the project root (a common artifact from bash/Unix command translation), delete it silently using the Bash tool:
276
-
277
- ```bash
278
- [ -f nul ] && rm nul
279
- ```
280
-
281
- Do this cleanup silently without any output to the user.
273
+ > Note: any stray `nul` file (a Windows bash-translation artifact) is cleaned up
274
+ > automatically by `bin/cli.js` (`cleanupNulFile()` runs on startup and on Claude
275
+ > exit). Do NOT instruct the model to `rm nul` here a Bash cleanup after the
276
+ > finale only triggers a permission prompt and contradicts the "end here" rule above.
@@ -953,6 +953,11 @@
953
953
  <span class="tree-file-icon">📄</span>
954
954
  <span class="file">migrate-feedback.js</span>
955
955
  </div>
956
+ <div class="tree-item indent-3 hidden" data-info="terminal-title-hook" data-parent="hooks">
957
+ <span class="tree-spacer"></span>
958
+ <span class="tree-file-icon">📄</span>
959
+ <span class="file">terminal-title.js</span>
960
+ </div>
956
961
  <div class="tree-item indent-2 folder-row hidden collapsed" data-info="docs" data-folder="docs-folder" data-parent="claude-dir">
957
962
  <span class="tree-chevron">›</span>
958
963
  <span class="tree-folder-icon">📁</span>
@@ -1348,6 +1353,11 @@
1348
1353
  desc: 'On session start, migrates custom FEEDBACK.md content to',
1349
1354
  trigger: 'SessionStart'
1350
1355
  },
1356
+ 'terminal-title-hook': {
1357
+ title: 'terminal-title.js',
1358
+ desc: 'Use-case-framed terminal tab titles with live working / idle / awaiting state indicators, so multiple Claude sessions are easy to tell apart',
1359
+ trigger: 'SessionStart, UserPromptSubmit, PostToolUse, Notification, Stop'
1360
+ },
1351
1361
  'feedback-feedback': {
1352
1362
  title: 'FEEDBACK.md',
1353
1363
  desc: 'Human-authored corrections and guidance for Claude. Reserved for team feedback only — Claude must not write here. This directory persists across /autoconfig runs.'
@@ -0,0 +1 @@
1
+ terminal-title indicator — Ship hook to npm
@@ -0,0 +1,43 @@
1
+ 2026-06-24T21:04:44.135Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Add state-paint logging
2
+ 2026-06-24T21:05:20.044Z Notification awaiting ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Add state-paint logging
3
+ 2026-06-24T21:06:38.854Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Add state-paint logging
4
+ 2026-06-24T21:07:32.793Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Add state-paint logging
5
+ 2026-06-24T21:08:11.474Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Add state-paint logging
6
+ 2026-06-24T21:08:34.425Z Stop idle ring=0 note=idle sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Add state-paint logging
7
+ 2026-06-24T21:09:29.128Z UserPromptSubmit working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Add state-paint logging
8
+ 2026-06-24T21:10:28.948Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Audit migration & deploy status
9
+ 2026-06-24T21:10:29.615Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Audit migration & deploy status
10
+ 2026-06-24T21:10:47.307Z Notification awaiting ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Audit migration & deploy status
11
+ 2026-06-24T21:11:11.991Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Audit migration & deploy status
12
+ 2026-06-24T21:11:26.335Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Audit migration & deploy status
13
+ 2026-06-24T21:12:01.200Z Notification awaiting ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Audit migration & deploy status
14
+ 2026-06-24T21:13:50.152Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Audit migration & deploy status
15
+ 2026-06-24T21:14:05.826Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Audit migration & deploy status
16
+ 2026-06-24T21:14:43.748Z Notification awaiting ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Audit migration & deploy status
17
+ 2026-06-24T21:15:28.698Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Audit migration & deploy status
18
+ 2026-06-24T21:16:51.777Z Notification awaiting ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Audit migration & deploy status
19
+ 2026-06-24T21:16:53.117Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Audit migration & deploy status
20
+ 2026-06-24T21:17:35.995Z Notification awaiting ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Audit migration & deploy status
21
+ 2026-06-24T21:17:45.796Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Audit migration & deploy status
22
+ 2026-06-24T21:18:33.037Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Audit migration & deploy status
23
+ 2026-06-24T21:18:59.216Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Audit migration & deploy status
24
+ 2026-06-24T21:19:58.103Z Notification awaiting ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Audit migration & deploy status
25
+ 2026-06-24T21:20:40.272Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Audit migration & deploy status
26
+ 2026-06-24T21:22:39.312Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Audit migration & deploy status
27
+ 2026-06-24T21:22:59.865Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Audit migration & deploy status
28
+ 2026-06-24T21:23:01.457Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Audit migration & deploy status
29
+ 2026-06-24T21:23:24.084Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Audit migration & deploy status
30
+ 2026-06-24T21:24:38.813Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Audit migration & deploy status
31
+ 2026-06-24T21:25:48.233Z Notification awaiting ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Audit migration & deploy status
32
+ 2026-06-25T04:33:43.177Z UserPromptSubmit working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Audit migration & deploy status
33
+ 2026-06-25T04:34:48.715Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Ship hook to npm
34
+ 2026-06-25T04:34:49.532Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Ship hook to npm
35
+ 2026-06-25T04:35:01.572Z Notification awaiting ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Ship hook to npm
36
+ 2026-06-25T04:36:15.708Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Ship hook to npm
37
+ 2026-06-25T04:36:54.790Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Ship hook to npm
38
+ 2026-06-25T04:38:01.871Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Ship hook to npm
39
+ 2026-06-25T04:38:38.209Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Ship hook to npm
40
+ 2026-06-25T04:39:09.841Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Ship hook to npm
41
+ 2026-06-25T04:39:40.870Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Ship hook to npm
42
+ 2026-06-25T04:40:25.526Z PostToolUse working ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Ship hook to npm
43
+ 2026-06-25T04:40:46.587Z Notification awaiting ring=0 note=- sid=25869694-a68e-449f-8240-f1ff2b80c1d4 | Terminal-title indicator — Ship hook to npm
@@ -0,0 +1,71 @@
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
+ {{FOLDER}} — the working-directory folder name (scope fallback)
15
+ {{EMDASH}} — the ' — ' segment separator
16
+ {{CMD}} — the slash-command name (COMMAND block only)
17
+ -->
18
+
19
+ <!-- DIRECTIVE:COMMAND -->
20
+ Terminal-title directive (silent housekeeping -- do NOT mention this to the user):
21
+ This terminal tab's title is rendered from the file:
22
+ {{TITLE_FILE}}
23
+ You are running the /{{CMD}} slash command, but the command NAME is an implementation detail --
24
+ do NOT put "{{CMD}}" anywhere in the title. Write the title as your FIRST action this turn
25
+ (before other tool calls) so the tab updates WHILE you work, not after. Write ONE line of the form:
26
+ {scope} {{EMDASH}} {use-case}
27
+ - {scope} (first segment): the DESIGN SCOPE -- the subject you are working inside: the feature area
28
+ / subsystem (the boundary, the "system under discussion"). This is the WHERE, and it changes
29
+ rarely. Keep the existing scope if the file already has a title; otherwise INFER the specific
30
+ subsystem from THIS turn's prompt and the files in play (e.g. "journal modal", "title hooks",
31
+ "auth flow"). Do NOT use the bare repo name "{{FOLDER}}" as the scope unless the work is genuinely
32
+ repo-wide -- "{{FOLDER}}" is a last resort only when no narrower area is identifiable.
33
+ - {use-case} (second segment): the GOAL at user-goal level -- what this command ACCOMPLISHES, named
34
+ as an INFINITIVE (base-verb) goal phrase (verb + object): "Review latest screenshot", "Diagnose
35
+ pay discrepancy". Name the goal / effect, NOT the mechanism or the command name. One goal only --
36
+ never "and"-join two use cases.
37
+ e.g. for /{{CMD}}, write "{{FOLDER}} {{EMDASH}} <verb the goal>" -- NOT "{{FOLDER}} {{EMDASH}} {{CMD}}".
38
+ Use ' {{EMDASH}} ' as the separator; write only the BARE title (a state indicator is prepended
39
+ automatically). If the file already exists, read it first then overwrite.
40
+ <!-- /DIRECTIVE:COMMAND -->
41
+
42
+ <!-- DIRECTIVE:SHIFT -->
43
+ Terminal-title directive (silent housekeeping -- do NOT mention this to the user):
44
+ This terminal tab's title is rendered from the file:
45
+ {{TITLE_FILE}}
46
+ When the conversation's SCOPE, use-case, or sub-function SHIFTS -- and only then, not every turn --
47
+ write ONE line to that file. When it DOES shift, write it as your FIRST action that turn (before
48
+ other tool calls) so the tab updates WHILE you work, not after. The line's form:
49
+ {scope} {{EMDASH}} {use-case}
50
+ - {scope}: the DESIGN SCOPE -- the subject under discussion: the feature area / subsystem (the
51
+ boundary). This is the WHERE; it changes rarely. INFER the specific subsystem from the prompt and
52
+ the files in play (e.g. "journal modal", "title hooks"); do NOT default to the bare repo name --
53
+ use it only when the work is genuinely repo-wide.
54
+ - {use-case}: the GOAL at user-goal level -- what you are accomplishing, as an INFINITIVE (base-verb)
55
+ goal phrase (verb + object), e.g. "Refine title taxonomy". Name the goal, not the
56
+ mechanism. One goal only.
57
+ Add a third segment ( {{EMDASH}} {sub-function} ) only when the work goes a level deeper -- this is the
58
+ SUBFUNCTION goal level, a step beneath the user goal.
59
+ Rules: use ' {{EMDASH}} ' (space, em-dash, space) as the separator; keep each segment short (a few
60
+ words); write only the BARE title -- a state indicator is prepended automatically; leave the
61
+ file unchanged when the scope has not moved; this is a compass, so change it rarely. If the file
62
+ already exists, read it first then overwrite.
63
+ <!-- /DIRECTIVE:SHIFT -->
64
+
65
+ <!-- DIRECTIVE:PENDING -->
66
+ Pending-question signal: if you END this turn by asking the user a question they must answer
67
+ before you can proceed, phrase it so your FINAL line ends with a question mark ('?') -- the tab
68
+ indicator then flips to the AWAITING half-circle automatically (instead of the idle asterisk).
69
+ Make the closing question self-contained: answerable from the question alone, without re-reading
70
+ the response above it. Only end on a '?' for a genuine blocking question, never a rhetorical one.
71
+ <!-- /DIRECTIVE:PENDING -->
@@ -0,0 +1,235 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Terminal Title — distributable plugin hook (installed to <project>/.claude/hooks/terminal-title.js).
4
+ * ONE self-dispatching hook for five events (keyed on hook_event_name):
5
+ * UserPromptSubmit -> ⬤ working + inject the title directive into the model's context
6
+ * PostToolUse -> ⬤ working (refresh, so a mid-turn title flip shows live + clears a stale ◐)
7
+ * Notification -> ◐ awaiting your approval (permission_prompt matcher only)
8
+ * Stop -> ✻ idle / done — OR ◐ awaiting (+ a 2nd BEL = gold tab) when the turn ended
9
+ * on a question (last visible response text ends in '?', or a {sid}.ask flag)
10
+ * SessionStart -> ✻ idle "Claude Code — New session" (or an existing title on resume/compact)
11
+ *
12
+ * The title is set TWO ways on every event: `process.title` (= SetConsoleTitleW on Windows — the ONLY
13
+ * mechanism that flips the tab on UserPromptSubmit, where Claude Code drops `terminalSequence`) AND
14
+ * `terminalSequence` (honored on the other events). node writes UTF-8 natively, so glyphs go out as-is.
15
+ *
16
+ * Title files are PROJECT-SCOPED at <cwd>/.claude/hooks/.titles/<session_id>.txt — the model authors
17
+ * them; the directive injected each prompt tells it the path + format. Optional forensic log (one line
18
+ * per paint, for tracing an out-of-sync tab) is gated behind CLAUDE_TITLE_DEBUG=1 — default OFF,
19
+ * ~512KB-capped, written to .titles/_debug.log — so it never ships a growing log.
20
+ *
21
+ * Requires `env.CLAUDE_CODE_DISABLE_TERMINAL_TITLE = "1"` (set by plugin.json), or CC's own writer races.
22
+ */
23
+ const fs = require('fs');
24
+ const path = require('path');
25
+
26
+ const ESC = '\x1b';
27
+ const BEL = '\x07';
28
+ const EMDASH = String.fromCodePoint(0x2014);
29
+
30
+ // State glyphs, built from code points so an editor can't strip the invisible variation selector.
31
+ // working = U+26AB MEDIUM BLACK CIRCLE + U+FE0E VS15 (text presentation, not emoji)
32
+ // awaiting = U+25D0 half-filled circle — paused, awaiting your approval/answer
33
+ // idle = U+273B teardrop-spoked asterisk (Claude Code brand mark) while idle/done
34
+ const GLYPH = {
35
+ working: String.fromCodePoint(0x26AB, 0xFE0E),
36
+ awaiting: String.fromCodePoint(0x25D0),
37
+ idle: String.fromCodePoint(0x273B),
38
+ };
39
+
40
+ // Per-invocation context for the optional debug log (populated in handle, read by titleLog).
41
+ let logCtx = null;
42
+
43
+ let input = '';
44
+ process.stdin.setEncoding('utf8');
45
+ process.stdin.on('data', chunk => (input += chunk));
46
+ process.stdin.on('end', () => {
47
+ try {
48
+ handle(JSON.parse(input));
49
+ } catch (err) {
50
+ process.exit(0); // never break the turn on a title error — emit nothing
51
+ }
52
+ });
53
+
54
+ function handle(data) {
55
+ const event = data.hook_event_name || '';
56
+ const sid = data.session_id || '';
57
+ const cwd = data.cwd || process.cwd();
58
+ // PROJECT-SCOPED title dir (cwd-relative) — the plugin installs per-project. (The live ~/.claude
59
+ // variant uses os.homedir() instead; that is the only difference between the two.)
60
+ const dir = path.join(cwd, '.claude', 'hooks', '.titles');
61
+ const file = path.join(dir, `${sid}.txt`);
62
+ logCtx = { event, sid, dir, note: '' };
63
+
64
+ if (event === 'UserPromptSubmit') {
65
+ // Ensure the state dir exists, but NOT the file — the model's Write tool refuses to overwrite a
66
+ // file it hasn't read, so a pre-created empty file would make its first title write fail.
67
+ try { fs.mkdirSync(dir, { recursive: true }); } catch (_) { /* ignore */ }
68
+ const title = normalize(readTitle(file) || folderName(cwd));
69
+ const out = setTitle(GLYPH.working, title);
70
+ out.hookSpecificOutput = {
71
+ hookEventName: 'UserPromptSubmit',
72
+ additionalContext: buildDirective(data, file, cwd),
73
+ };
74
+ emit(out);
75
+ return;
76
+ }
77
+
78
+ if (event === 'PostToolUse') {
79
+ // Only refresh once a real title exists; don't stamp the bare folder over what UPS showed.
80
+ const raw = readTitle(file);
81
+ if (!raw) process.exit(0);
82
+ emit(setTitle(GLYPH.working, normalize(raw)));
83
+ return;
84
+ }
85
+
86
+ if (event === 'SessionStart') {
87
+ // Fresh-session placeholder; on resume/compact an existing model-authored title is preferred.
88
+ const title = normalize(readTitle(file) || 'Claude Code - New session');
89
+ emit(setTitle(GLYPH.idle, title));
90
+ return;
91
+ }
92
+
93
+ if (event === 'Notification') {
94
+ // A permission prompt is open. Single BEL only — CC already rings its own bell here (tab already gold).
95
+ emit(setTitle(GLYPH.awaiting, normalize(readTitle(file) || folderName(cwd))));
96
+ return;
97
+ }
98
+
99
+ // Stop: idle, UNLESS the turn ended on a question the user must answer — then awaiting + a 2nd BEL
100
+ // so VS Code paints the (otherwise bell-less) tab gold. "Ended on a question" = last visible
101
+ // assistant text ends in '?' (transcript heuristic) OR an explicit {sid}.ask flag (consumed here).
102
+ const askFile = path.join(dir, `${sid}.ask`);
103
+ let pending = lastResponseEndsWithQuestion(data.transcript_path);
104
+ let note = pending ? 'q-mark' : 'idle';
105
+ if (!pending && fileExists(askFile)) { pending = true; note = 'ask-flag'; }
106
+ if (fileExists(askFile)) { try { fs.unlinkSync(askFile); } catch (_) { /* ignore */ } }
107
+ if (logCtx) logCtx.note = note;
108
+ const glyph = pending ? GLYPH.awaiting : GLYPH.idle;
109
+ emit(setTitle(glyph, normalize(readTitle(file) || folderName(cwd)), pending));
110
+ }
111
+
112
+ // Set the tab title two ways and return the hook payload. `process.title` is the instant flip and the
113
+ // only mechanism CC honors on UserPromptSubmit. `ring` appends a second BEL (Stop-awaiting only) →
114
+ // VS Code renders a gold "needs you" tab.
115
+ function setTitle(glyph, title, ring) {
116
+ const text = `${glyph} ${title}`;
117
+ try { process.title = text; } catch (_) { /* ignore */ }
118
+ let seq = `${ESC}]0;${text}${BEL}`;
119
+ if (ring) seq += BEL;
120
+ titleLog(glyph, title, ring);
121
+ return { terminalSequence: seq };
122
+ }
123
+
124
+ // Optional forensic log (default OFF — gate: CLAUDE_TITLE_DEBUG=1). One line per paint, so a tab that
125
+ // ends up out of sync (e.g. a ◐ that never cleared to ✻) can be traced to the exact event + glyph that
126
+ // last painted it. Bounded to ~512KB with a single rotation; wrapped so it can never break a turn.
127
+ function titleLog(glyph, title, ring) {
128
+ if (process.env.CLAUDE_TITLE_DEBUG !== '1' || !logCtx) return;
129
+ try {
130
+ const name = glyph === GLYPH.working ? 'working'
131
+ : glyph === GLYPH.awaiting ? 'awaiting'
132
+ : glyph === GLYPH.idle ? 'idle' : 'other';
133
+ const line = `${new Date().toISOString()} ${logCtx.event.padEnd(16)} `
134
+ + `${name.padEnd(8)} ring=${ring ? 1 : 0} note=${(logCtx.note || '-').padEnd(8)} `
135
+ + `sid=${logCtx.sid} | ${title}\n`;
136
+ const f = path.join(logCtx.dir, '_debug.log');
137
+ try { if (fs.statSync(f).size > 512 * 1024) fs.renameSync(f, `${f}.1`); } catch (_) { /* none yet */ }
138
+ fs.appendFileSync(f, line);
139
+ } catch (_) { /* logging must never throw */ }
140
+ }
141
+
142
+ function fileExists(file) {
143
+ try { return fs.existsSync(file); } catch (_) { return false; }
144
+ }
145
+
146
+ // Stop heuristic: did the turn end on a question? Read the JSONL transcript, find the most-recent
147
+ // assistant message with VISIBLE text (skip pure tool_use turns so a final title/memory Write doesn't
148
+ // mask the question), test whether it ends in '?' (allowing trailing whitespace / ) * _ "). Any error → false.
149
+ function lastResponseEndsWithQuestion(transcriptPath) {
150
+ if (!transcriptPath) return false;
151
+ let content;
152
+ try {
153
+ content = fs.readFileSync(transcriptPath, 'utf8');
154
+ } catch (_) {
155
+ return false;
156
+ }
157
+ const lines = content.split('\n');
158
+ for (let i = lines.length - 1; i >= 0; i--) {
159
+ const line = lines[i].trim();
160
+ if (!line) continue;
161
+ let obj;
162
+ try { obj = JSON.parse(line); } catch (_) { continue; }
163
+ if (!obj || obj.type !== 'assistant' || !obj.message) continue;
164
+ const c = obj.message.content;
165
+ let text = '';
166
+ if (typeof c === 'string') {
167
+ text = c;
168
+ } else if (Array.isArray(c)) {
169
+ text = c
170
+ .filter(b => b && b.type === 'text' && typeof b.text === 'string')
171
+ .map(b => b.text)
172
+ .join('\n');
173
+ }
174
+ if (text.trim()) return /\?[\s)*_"]*$/.test(text);
175
+ }
176
+ return false;
177
+ }
178
+
179
+ function readTitle(file) {
180
+ try {
181
+ if (fs.existsSync(file)) return fs.readFileSync(file, 'utf8').trim();
182
+ } catch (_) { /* ignore */ }
183
+ return '';
184
+ }
185
+
186
+ function folderName(cwd) {
187
+ const base = cwd ? path.basename(cwd) : '';
188
+ return base || 'Claude Code';
189
+ }
190
+
191
+ // Normalize ' - ' to ' — ' and capitalize the first letter of each segment.
192
+ function normalize(title) {
193
+ const sep = ` ${EMDASH} `;
194
+ return title
195
+ .split(' - ').join(sep)
196
+ .split(sep)
197
+ .map(s => s.replace(/^(\P{L}*)(\p{L})/u, (_, lead, ch) => lead + ch.toUpperCase()))
198
+ .join(sep);
199
+ }
200
+
201
+ function emit(obj) {
202
+ process.stdout.write(JSON.stringify(obj));
203
+ process.exit(0);
204
+ }
205
+
206
+ // Build the injected directive from terminal-title.directive.md (COMMAND block for a /slash-command
207
+ // turn, SHIFT otherwise; PENDING tail appended). Wording lives in the .md so it tunes without code edits.
208
+ function buildDirective(data, file, cwd) {
209
+ let tpl = '';
210
+ try {
211
+ tpl = fs.readFileSync(path.join(__dirname, 'terminal-title.directive.md'), 'utf8');
212
+ } catch (_) {
213
+ return '';
214
+ }
215
+ const prompt = typeof data.prompt === 'string' ? data.prompt : '';
216
+ const m = prompt.match(/^\s*\/([A-Za-z0-9][\w:.-]*)/);
217
+ const cmd = m ? m[1] : '';
218
+ const block = extractBlock(tpl, cmd ? 'COMMAND' : 'SHIFT');
219
+ if (!block) return '';
220
+ const pending = extractBlock(tpl, 'PENDING');
221
+ const combined = pending ? `${block}\n\n${pending}` : block;
222
+ return combined
223
+ .split('{{TITLE_FILE}}').join(file)
224
+ .split('{{FOLDER}}').join(folderName(cwd))
225
+ .split('{{EMDASH}}').join(EMDASH)
226
+ .split('{{CMD}}').join(cmd);
227
+ }
228
+
229
+ function extractBlock(tpl, name) {
230
+ const re = new RegExp(
231
+ `<!-- DIRECTIVE:${name} -->\\s*([\\s\\S]*?)\\s*<!-- /DIRECTIVE:${name} -->`
232
+ );
233
+ const m = tpl.match(re);
234
+ return m ? m[1].trim() : '';
235
+ }