diff --git a/src/claude/shim-template.ts b/src/claude/shim-template.ts index 3e9e2cb7..efdcd3f5 100644 --- a/src/claude/shim-template.ts +++ b/src/claude/shim-template.ts @@ -167,7 +167,9 @@ if (fs.existsSync(target)) import(pathToFileURL(target).href).then((m) => ${call // where their learnings are. In the order of the 13 Oct onboarding plan: when // one shapes a reply, Claude credits it, and only after the first credit does // it offer, once, to set trail up. Plain Node, no package: it's the one part of -// trail a teammate without it still runs. +// trail a teammate without it still runs. A title may be quoted either way: +// trail writes JSON's double quotes, and a repo's Prettier may rewrite them to +// single ones. const OFFER_TRAIL = ` else if (process.argv[2] === 'session-start') offerTrail(); @@ -178,7 +180,10 @@ function offerTrail() { if (files.length === 0) return; const field = (text, key) => { const line = text.split('\\n').find((l) => l.startsWith(key + ':')); - return line ? line.slice(key.length + 1).trim().replace(/^"|"$/g, '') : ''; + const v = line ? line.slice(key.length + 1).trim() : ''; + if (/^'.*'$/.test(v)) return v.slice(1, -1).replace(/''/g, "'"); + if (/^".*"$/.test(v)) { try { return JSON.parse(v); } catch { return v.slice(1, -1); } } + return v; }; const lines = []; for (const f of files.slice(0, 12)) { diff --git a/test/claude-shim-resolve.test.ts b/test/claude-shim-resolve.test.ts index 41cd5695..96084c19 100644 --- a/test/claude-shim-resolve.test.ts +++ b/test/claude-shim-resolve.test.ts @@ -252,6 +252,18 @@ test('each learning is listed with its author, newest first, so the credit can n assert.ok(sam >= 0 && anirudh > sam, ctx); }); +test('a learning title reads clean whichever quotes it has, as trail wrote it or as Prettier left it', () => { + const root = tmpRepo('shim-offer-quotes'); + const repo = clonedWithLearnings(root, 'Kumar', ['Anirudh']); + const dir = join(repo, '.trail', 'learnings'); + writeFileSync(join(dir, '2026-10-15-single.md'), `---\ntitle: 'assign: one PR into dev, it''s the rule'\nauthor: Priya\n---\n`); + writeFileSync(join(dir, '2026-10-16-double.md'), `---\ntitle: ${JSON.stringify('graft: say "trail" now')}\nauthor: Sam\n---\n`); + const ctx: string = JSON.parse(runBare(root, repo, 'session-start')).hookSpecificOutput.additionalContext; + assert.ok(ctx.includes("- assign: one PR into dev, it's the rule (Priya) ·"), ctx); + assert.ok(ctx.includes('- graft: say "trail" now (Sam) ·'), ctx); + assert.ok(ctx.includes('- Retries decision 0 (Anirudh) ·'), ctx); +}); + test('without trail, other events and a repo with no learnings stay silent', () => { const root = tmpRepo('shim-offer-quiet'); const repo = clonedWithLearnings(root, 'Kumar', ['Anirudh']);