claude-code-autoconfig 1.0.200 → 1.0.201

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.
@@ -53,29 +53,37 @@ Maintain it across the session:
53
53
  never "{{FOLDER}} {{EMDASH}} <command-name>").
54
54
 
55
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):
59
- 1. Write the flag file {{ASK_FILE}} (any short content, e.g. "1"). This is the RELIABLE
56
+ paragraph SOLICIT a reply from the user -- a question to answer, a decision to make, or a
57
+ go-ahead on a proposed next step (a fix you proposed, an offer to do more)?
58
+ - YES (the turn ends waiting on the user) -> do ALL THREE as near-final actions so the tab
59
+ flips to the AWAITING half-circle (instead of the idle asterisk):
60
+ 1. Phrase the solicitation as a DIRECT QUESTION. An offer IS a solicitation: write "Want
61
+ me to apply both fixes?" -- never its declarative twin ("Say the word and I'll apply
62
+ both fixes." / "I can also add tests if you want." / "Let me know."), which prompts the
63
+ user back while hiding the awaiting signal.
64
+ 2. Write the flag file {{ASK_FILE}} (any short content, e.g. "1"). This is the RELIABLE
60
65
  trigger -- it is on disk before the turn ends, so it never misses on phrasing or timing.
61
66
  It is one-shot (consumed at turn end, auto-cleared next turn). Write it even when the
62
67
  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
68
+ 3. Make '?' the LAST character of the message. Write NOTHING after the question -- no
64
69
  "Standing by.", "Ready when you are.", "Let me know." (a declarative sign-off defeats
65
70
  the signal). Only a single short parenthetical aside may follow the '?'. Keep the
66
71
  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.").
72
+ - NO (nothing is solicited: the turn ends on completed work, a report, a recap, or a
73
+ rhetorical question) -> do NOT write the flag, and end the message on a statement, not a
74
+ '?'. This branch is ONLY for endings that await nothing -- if the closer invites ANY
75
+ reply ("if you want", "say the word", "happy to"), it belongs to YES: convert it to a
76
+ question.
70
77
  <!-- /DIRECTIVE:RULES -->
71
78
 
72
79
  <!-- DIRECTIVE:REMINDER -->
73
80
  Terminal-title reminder (housekeeping -- never mention to the user; full rules were injected
74
81
  at session start): if this turn SHIFTS the scope/use-case, FIRST action: overwrite
75
82
  {{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 '?'.
83
+ paragraph solicits a reply (a question, a decision, or a go-ahead on an offered next step),
84
+ phrase it as a DIRECT QUESTION -- never a declarative offer ("Say the word...") -- write the
85
+ flag file {{ASK_FILE}}, AND make '?' the message's last character (nothing after it); if
86
+ nothing is solicited, end on a statement, not a '?'.
79
87
  <!-- /DIRECTIVE:REMINDER -->
80
88
 
81
89
  <!-- DIRECTIVE:BASELINE -->
@@ -79,13 +79,19 @@ async function handle(data) {
79
79
  const event = data.hook_event_name || '';
80
80
  const sid = data.session_id || '';
81
81
  const cwd = data.cwd || process.cwd();
82
- // User-level copy stands down when a project-level managed copy exists (see header).
83
- if (shouldDefer(cwd, __dirname, __filename, path.join(os.homedir(), '.claude', 'hooks'))) {
82
+ // User-level copy stands down when the SESSION'S OWN project ships a managed copy (see header).
83
+ // Keyed to CLAUDE_PROJECT_DIR (the dir whose settings registered this hook run), NOT the event's
84
+ // cwd: a mid-session `cd` into a copy-shipping repo must not silence this copy — that repo's
85
+ // settings aren't loaded, so its copy never runs and nobody would paint.
86
+ const ownerDir = process.env.CLAUDE_PROJECT_DIR || cwd;
87
+ if (shouldDefer(ownerDir, __dirname, __filename, path.join(os.homedir(), '.claude', 'hooks'))) {
84
88
  process.exit(0);
85
89
  }
86
- // PROJECT-SCOPED title dir (cwd-relative) — the plugin installs per-project. (The live ~/.claude
87
- // variant uses os.homedir() instead; that is the only difference between the two.)
88
- const dir = path.join(cwd, '.claude', 'hooks', '.titles');
90
+ // PROJECT-SCOPED title dir — anchored to the session's project root (CLAUDE_PROJECT_DIR, with cwd
91
+ // fallback on older Claude Code versions that don't set it) so a mid-session `cd` can't scatter
92
+ // title state across directories. (The live ~/.claude variant uses os.homedir() instead; that is
93
+ // the only difference between the two.)
94
+ const dir = path.join(ownerDir, '.claude', 'hooks', '.titles');
89
95
  const file = path.join(dir, `${sid}.txt`);
90
96
  logCtx = { event, sid, dir, note: '' };
91
97
 
@@ -182,10 +188,15 @@ async function handle(data) {
182
188
  if (q.ends || (q.found && !q.suspectRace)) break;
183
189
  }
184
190
 
185
- const pending = q.ends;
191
+ // LEXICAL RESCUE — no '?' on the final line, but its closing sentence is a formulaic offer
192
+ // ("Say the word and I'll…", "Want me to…"): the phrasing directive slipped. Paint ◐ anyway so
193
+ // the user still gets the awaiting signal; via=lex marks it a prose DEFECT for the miss-audit,
194
+ // not a working-as-intended path.
195
+ const lex = !q.ends && q.solicits === true;
196
+ const pending = q.ends || lex;
186
197
  if (logCtx) {
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}"`;
198
+ logCtx.note = pending ? (lex ? 'lex' : 'q-mark') : 'idle';
199
+ logCtx.diag = `ask=0 qmark=${q.ends ? 1 : 0} via=${lex ? 'lex' : (q.via || '-')} found=${q.found ? 1 : 0} reread=${reread} model=${q.model || '-'} tail="${q.tail}"`;
189
200
  }
190
201
  const glyph = pending ? GLYPH.awaiting : GLYPH.idle;
191
202
  emit(setTitle(glyph, normalize(readTitle(file) || folderName(cwd)), pending));
@@ -235,7 +246,7 @@ function fileExists(file) {
235
246
  // shows found=0 (or a stale tail), a genuine regex miss shows a tail that's present but doesn't end
236
247
  // in '?'. Any error → a blank record (treated as "no question"), matching the old false return.
237
248
  function inspectLastResponse(transcriptPath) {
238
- const blank = { ends: false, via: '', found: false, tail: '', suspectRace: false, model: '' };
249
+ const blank = { ends: false, via: '', found: false, tail: '', suspectRace: false, model: '', solicits: false };
239
250
  if (!transcriptPath) return blank;
240
251
  let content;
241
252
  try {
@@ -301,6 +312,7 @@ function inspectLastResponse(transcriptPath) {
301
312
  return {
302
313
  ends: q.ends, via: q.via, found: true, tail,
303
314
  suspectRace: sawTextlessAssistant, model: (obj.message && obj.message.model) || '',
315
+ solicits: solicitsReply(text),
304
316
  };
305
317
  }
306
318
  // assistant message with no visible text = a thinking-only or tool_use-only block sitting AFTER the
@@ -352,6 +364,36 @@ function isRealUserPrompt(obj) {
352
364
  // Event-loop-friendly sleep for the Stop flush-race re-read beat (handle awaits this; no busy-wait).
353
365
  function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }
354
366
 
367
+ // LEXICAL solicitation detector (the '?'-less rescue). STRONG, formulaic offer phrases only, tested
368
+ // against the CLOSING SENTENCE of the final non-empty line. Weak/courtesy phrases ("let me know",
369
+ // "if you want", "happy to") are deliberately absent: as sign-offs after completed work they are
370
+ // legitimate statement endings, and enforcing them would trade a rare false-✻ for chronic false-◐.
371
+ // "green-light"/"confirm" are imperative-anchored (sentence-initial) so a recap that merely MENTIONS
372
+ // them ("Andrew green-lighted the batch.") can't fire.
373
+ const SOLICIT_STRONG = [
374
+ /\bwant me to\b/i,
375
+ /\bwould you like\b/i,
376
+ /\bshould i\b/i,
377
+ /\bshall i\b/i,
378
+ /\bdo you want\b/i,
379
+ /\bsay the word\b/i,
380
+ /\by\/n\b/i,
381
+ /\bok(?:ay)? to proceed\b/i,
382
+ /^green-?light\b/i,
383
+ /^confirm\b/i,
384
+ ];
385
+ function solicitsReply(text) {
386
+ if (!text) return false;
387
+ const lines = String(text).trim().split('\n').filter(l => l.trim());
388
+ const lastLine = (lines[lines.length - 1] || '').trim();
389
+ // Any '?' on the final line means the question grade (qtail/signoff) owns the verdict — the
390
+ // lexicon exists solely for the question-less slip, and must never second-guess a graded line.
391
+ if (!lastLine || lastLine.includes('?')) return false;
392
+ const parts = lastLine.split(/[.!:;]\s+/);
393
+ const closing = (parts[parts.length - 1] || '').trim();
394
+ return SOLICIT_STRONG.some(re => re.test(closing));
395
+ }
396
+
355
397
  // Deferred flag-turn grade (debug-gated). The `.ask` fast path paints ◐ without reading the
356
398
  // transcript, which blinded _debug.log's qmark/via/tail diagnostics on exactly the turns where the
357
399
  // flag+sign-off misuse pattern shows up. Grading BEFORE emit() would re-open the kill window the
@@ -460,12 +502,15 @@ function buildBlocks(names, file, cwd, cmd) {
460
502
  }
461
503
 
462
504
  // Should THIS copy of the hook stand down? True only for the user-level copy (~/.claude/hooks)
463
- // running inside a project that ships its own managed copy — the project copy wins, so a session
464
- // gets ONE directive and ONE title file. Parameterized (no ambient __dirname/homedir) for tests.
465
- function shouldDefer(cwd, selfDir, selfFile, homeHooksDir) {
505
+ // when the session's OWN project (ownerDir = CLAUDE_PROJECT_DIR, cwd fallback) ships a managed
506
+ // copy that's the only case where the project copy is registered and will paint; the project
507
+ // copy wins so a session gets ONE directive and ONE title file. The event's live cwd must NOT be
508
+ // used here (mid-session cd into a copy-shipping repo would silence both copies — 2026-07-08).
509
+ // Parameterized (no ambient __dirname/homedir) for tests.
510
+ function shouldDefer(ownerDir, selfDir, selfFile, homeHooksDir) {
466
511
  try {
467
512
  if (path.resolve(selfDir) !== path.resolve(homeHooksDir)) return false; // we ARE the project copy
468
- const projectCopy = path.join(cwd, '.claude', 'hooks', 'terminal-title.js');
513
+ const projectCopy = path.join(ownerDir, '.claude', 'hooks', 'terminal-title.js');
469
514
  return fs.existsSync(projectCopy) && path.resolve(projectCopy) !== path.resolve(selfFile);
470
515
  } catch (_) {
471
516
  return false;
@@ -481,4 +526,4 @@ function extractBlock(tpl, name) {
481
526
  }
482
527
 
483
528
  // Exported for tests (require()'d when require.main !== module). The hook itself never reads these.
484
- module.exports = { inspectLastResponse, endsOnQuestion, normalize, GLYPH, shouldDefer };
529
+ module.exports = { inspectLastResponse, endsOnQuestion, normalize, GLYPH, shouldDefer, solicitsReply };
@@ -10,7 +10,7 @@
10
10
  "hooks": [
11
11
  {
12
12
  "type": "command",
13
- "command": "node .claude/hooks/migrate-feedback.js"
13
+ "command": "node \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/migrate-feedback.js\""
14
14
  }
15
15
  ]
16
16
  },
@@ -19,7 +19,7 @@
19
19
  "hooks": [
20
20
  {
21
21
  "type": "command",
22
- "command": "node .claude/hooks/terminal-title.js"
22
+ "command": "node \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/terminal-title.js\""
23
23
  }
24
24
  ]
25
25
  }
@@ -30,7 +30,7 @@
30
30
  "hooks": [
31
31
  {
32
32
  "type": "command",
33
- "command": "node .claude/hooks/terminal-title.js"
33
+ "command": "node \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/terminal-title.js\""
34
34
  }
35
35
  ]
36
36
  }
@@ -41,11 +41,11 @@
41
41
  "hooks": [
42
42
  {
43
43
  "type": "command",
44
- "command": "node .claude/hooks/terminal-title.js"
44
+ "command": "node \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/terminal-title.js\""
45
45
  },
46
46
  {
47
47
  "type": "command",
48
- "command": "node .claude/hooks/arcade-beeps.js"
48
+ "command": "node \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/arcade-beeps.js\""
49
49
  }
50
50
  ]
51
51
  }
@@ -56,11 +56,11 @@
56
56
  "hooks": [
57
57
  {
58
58
  "type": "command",
59
- "command": "node .claude/hooks/terminal-title.js"
59
+ "command": "node \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/terminal-title.js\""
60
60
  },
61
61
  {
62
62
  "type": "command",
63
- "command": "node .claude/hooks/arcade-beeps.js"
63
+ "command": "node \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/arcade-beeps.js\""
64
64
  }
65
65
  ]
66
66
  }
@@ -71,7 +71,7 @@
71
71
  "hooks": [
72
72
  {
73
73
  "type": "command",
74
- "command": "node .claude/hooks/feedback-rule-check.js"
74
+ "command": "node \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/feedback-rule-check.js\""
75
75
  }
76
76
  ]
77
77
  },
@@ -80,7 +80,7 @@
80
80
  "hooks": [
81
81
  {
82
82
  "type": "command",
83
- "command": "node .claude/hooks/terminal-title.js"
83
+ "command": "node \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/terminal-title.js\""
84
84
  }
85
85
  ]
86
86
  }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## v1.0.201
4
+ - fix(install): Hook commands now survive cd'ing around your project — no more 'Cannot find module' errors after changing directories, and upgrades fix existing installs automatically.
5
+ - fix(terminal-title): The tab title no longer freezes when a session cd's into another project, and the 'awaiting your reply' half-circle now catches replies that ask for your go-ahead without a question mark.
6
+
3
7
  ## v1.0.200
4
8
  - feat(changelog): Release notes now written in plain language
5
9
 
@@ -146,6 +150,3 @@
146
150
  ## v1.0.152
147
151
  - fix: warn against ! prefix workaround in inside-Claude message
148
152
 
149
- ## v1.0.151
150
- - fix: scope find permission to project directory for security
151
-