tillerhand 3.43.0 → 3.47.0

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.
Files changed (185) hide show
  1. package/.claude/agents/board-chair.md +20 -20
  2. package/.claude/agents/code-reviewer.md +20 -20
  3. package/.claude/agents/council-advisor.md +14 -14
  4. package/.claude/agents/security-auditor.md +30 -30
  5. package/.claude/agents/test-writer.md +22 -22
  6. package/.claude/board/board-ai.md +25 -25
  7. package/.claude/board/board-api.md +25 -25
  8. package/.claude/board/board-architect.md +28 -28
  9. package/.claude/board/board-devops.md +25 -25
  10. package/.claude/board/board-mobile.md +25 -25
  11. package/.claude/board/board-performance.md +24 -24
  12. package/.claude/board/board-privacy.md +25 -25
  13. package/.claude/board/board-product.md +26 -26
  14. package/.claude/board/board-qa.md +28 -28
  15. package/.claude/board/board-ux.md +26 -26
  16. package/.claude/board/council.md +76 -76
  17. package/.claude/board/rules.md +61 -61
  18. package/.claude/claude-security-guidance.md +10 -10
  19. package/.claude/commands/adopt.md +70 -69
  20. package/.claude/commands/ask-mode.md +9 -9
  21. package/.claude/commands/board.md +15 -15
  22. package/.claude/commands/build.md +47 -47
  23. package/.claude/commands/dashboard.md +17 -17
  24. package/.claude/commands/design.md +31 -31
  25. package/.claude/commands/findings-all.md +14 -14
  26. package/.claude/commands/findings-business.md +7 -7
  27. package/.claude/commands/findings-legal.md +7 -7
  28. package/.claude/commands/findings-perf.md +7 -7
  29. package/.claude/commands/findings-security.md +9 -9
  30. package/.claude/commands/findings-tech.md +7 -7
  31. package/.claude/commands/findings-ux.md +7 -7
  32. package/.claude/commands/fix-bug.md +15 -15
  33. package/.claude/commands/freeze.md +8 -8
  34. package/.claude/commands/handoff.md +38 -38
  35. package/.claude/commands/kit-update.md +17 -14
  36. package/.claude/commands/language.md +13 -0
  37. package/.claude/commands/maintain.md +19 -19
  38. package/.claude/commands/plan-task.md +32 -32
  39. package/.claude/commands/qa.md +17 -17
  40. package/.claude/commands/release.md +40 -40
  41. package/.claude/commands/research.md +31 -31
  42. package/.claude/commands/retro.md +32 -32
  43. package/.claude/commands/review-changes.md +15 -15
  44. package/.claude/commands/rollback.md +24 -24
  45. package/.claude/commands/start.md +116 -113
  46. package/.claude/commands/tools.md +11 -11
  47. package/.claude/commands/user-feedback.md +11 -11
  48. package/.claude/commands/wrap-up.md +18 -18
  49. package/.claude/findings/lenses/business.md +24 -24
  50. package/.claude/findings/lenses/legal.md +29 -29
  51. package/.claude/findings/lenses/perf.md +19 -19
  52. package/.claude/findings/lenses/security.md +30 -30
  53. package/.claude/findings/lenses/tech.md +23 -23
  54. package/.claude/findings/lenses/ux.md +19 -19
  55. package/.claude/findings/protocol.md +54 -54
  56. package/.claude/findings/report-template.md +30 -30
  57. package/.claude/hooks/budget.mjs +7 -7
  58. package/.claude/hooks/checkpoint.mjs +12 -12
  59. package/.claude/hooks/commit-gate.mjs +38 -37
  60. package/.claude/hooks/cost.mjs +2 -2
  61. package/.claude/hooks/council.mjs +3 -3
  62. package/.claude/hooks/dashboard-data.mjs +3 -2
  63. package/.claude/hooks/dashboard.mjs +164 -149
  64. package/.claude/hooks/destructive-guard.mjs +74 -64
  65. package/.claude/hooks/edit-rounds.mjs +3 -3
  66. package/.claude/hooks/flow.mjs +10 -10
  67. package/.claude/hooks/freeze-guard.mjs +27 -23
  68. package/.claude/hooks/gate.mjs +25 -25
  69. package/.claude/hooks/guard-secrets.mjs +11 -11
  70. package/.claude/hooks/lang.mjs +114 -0
  71. package/.claude/hooks/messages.mjs +495 -0
  72. package/.claude/hooks/models.mjs +14 -11
  73. package/.claude/hooks/notify.mjs +3 -2
  74. package/.claude/hooks/package-gate.mjs +17 -15
  75. package/.claude/hooks/plugins-sync.mjs +9 -9
  76. package/.claude/hooks/prefs.mjs +19 -19
  77. package/.claude/hooks/preview.mjs +11 -11
  78. package/.claude/hooks/progress.mjs +14 -14
  79. package/.claude/hooks/qa-report.mjs +11 -10
  80. package/.claude/hooks/reminder.mjs +6 -3
  81. package/.claude/hooks/session-start.mjs +23 -17
  82. package/.claude/hooks/shared.mjs +1 -1
  83. package/.claude/hooks/status-read.mjs +5 -4
  84. package/.claude/hooks/statusline.mjs +14 -11
  85. package/.claude/hooks/structure-guard.mjs +9 -9
  86. package/.claude/hooks/test-guard.mjs +12 -12
  87. package/.claude/hooks/tools.mjs +28 -29
  88. package/.claude/hooks/usage.mjs +18 -15
  89. package/.claude/output-styles/anlatici.md +28 -28
  90. package/.claude/security-patterns.json +3 -3
  91. package/.claude/settings.json +2 -0
  92. package/.claude/skills/logo-design/SKILL.md +42 -42
  93. package/.claude/skills/motion-video/SKILL.md +52 -52
  94. package/.claude/skills/source-research/SKILL.md +35 -35
  95. package/.claude/skills/ui-motion/SKILL.md +38 -38
  96. package/.github/workflows/kit-tests.yml +7 -7
  97. package/.github/workflows/npm-publish.yml +16 -16
  98. package/AGENTS.md +56 -56
  99. package/CHANGELOG.md +7 -7
  100. package/CLAUDE.md +5 -5
  101. package/README.md +199 -172
  102. package/README.tr.md +292 -0
  103. package/docs/architecture/overview.md +31 -31
  104. package/docs/architecture/stack.md +2 -2
  105. package/docs/archive/README.md +4 -4
  106. package/docs/decisions/adr-template.md +14 -14
  107. package/docs/decisions/log.md +10 -10
  108. package/docs/findings/README.md +13 -13
  109. package/docs/findings/config.md +13 -13
  110. package/docs/legal/privacy-kvkk.md +1 -1
  111. package/docs/operations/accounts.md +25 -25
  112. package/docs/operations/deploy-setup.md +64 -64
  113. package/docs/operations/feature-flags.md +17 -17
  114. package/docs/operations/maintenance.md +48 -48
  115. package/docs/operations/release-checklist.md +61 -61
  116. package/docs/operations/spend-alarms.md +28 -28
  117. package/docs/product/feedback.md +9 -8
  118. package/docs/product/glossary.md +16 -16
  119. package/docs/product/launch.md +28 -28
  120. package/docs/product/prd.md +43 -42
  121. package/docs/product/roadmap.md +31 -30
  122. package/docs/product/specs/_template.md +13 -12
  123. package/docs/product/unit-economics.md +23 -22
  124. package/docs/quality/metrics.md +14 -14
  125. package/docs/quality/retros.md +11 -11
  126. package/docs/quality/test-plan.md +48 -48
  127. package/docs/security/checklist.md +57 -57
  128. package/docs/workflow/board.md +7 -7
  129. package/docs/workflow/cost-log.md +4 -4
  130. package/docs/workflow/decision-protocol.md +70 -70
  131. package/docs/workflow/learning.md +4 -4
  132. package/docs/workflow/mcp-catalog.md +31 -31
  133. package/docs/workflow/models.md +35 -35
  134. package/docs/workflow/progress.md +7 -7
  135. package/docs/workflow/skills.md +108 -108
  136. package/docs/workflow/usage-limits.md +62 -62
  137. package/kit/CHANGELOG.md +876 -841
  138. package/kit/VERSION +1 -1
  139. package/kit/guide.html +713 -477
  140. package/kit/guide.tr.html +1328 -0
  141. package/kit/kit.mjs +56 -54
  142. package/kit/project/README.md +29 -29
  143. package/kit/project/gitignore +12 -12
  144. package/kit/templates/addons/ai/.env.example.append +5 -5
  145. package/kit/templates/addons/ai/ai/evals/README.md +11 -11
  146. package/kit/templates/addons/ai/ai/evals/cases.jsonl +2 -2
  147. package/kit/templates/addons/ai/ai/prompts/README.md +8 -8
  148. package/kit/templates/addons/ai/docs/ai/guide.md +56 -56
  149. package/kit/templates/backend/.claude/hooks/format.mjs +1 -1
  150. package/kit/templates/backend/.claude/structure.json +1 -1
  151. package/kit/templates/backend/.env.example +6 -6
  152. package/kit/templates/backend/.github/dependabot.yml +1 -1
  153. package/kit/templates/backend/.github/workflows/ci.yml +13 -13
  154. package/kit/templates/backend/.github/workflows/claude-ci-fix.yml +19 -19
  155. package/kit/templates/backend/.github/workflows/claude-maintenance.yml +18 -18
  156. package/kit/templates/backend/.github/workflows/live-monitoring.yml +14 -14
  157. package/kit/templates/backend/.pre-commit-config.yaml +3 -3
  158. package/kit/templates/backend/docs/architecture/stack.md +98 -98
  159. package/kit/templates/backend/docs/architecture/structure.md +34 -34
  160. package/kit/templates/mobile/.claude/structure.json +1 -1
  161. package/kit/templates/mobile/.env.example +6 -6
  162. package/kit/templates/mobile/.github/dependabot.yml +2 -2
  163. package/kit/templates/mobile/.github/workflows/ci.yml +18 -18
  164. package/kit/templates/mobile/.github/workflows/claude-ci-fix.yml +19 -19
  165. package/kit/templates/mobile/.github/workflows/claude-maintenance.yml +18 -18
  166. package/kit/templates/mobile/.gitignore.append +2 -2
  167. package/kit/templates/mobile/docs/architecture/stack.md +113 -113
  168. package/kit/templates/mobile/docs/architecture/structure.md +33 -33
  169. package/kit/templates/mobile/docs/design/design-guide.md +37 -37
  170. package/kit/templates/mobile/tests/security/rls.mjs +22 -22
  171. package/kit/templates/web-saas/.claude/structure.json +1 -1
  172. package/kit/templates/web-saas/.env.example +11 -11
  173. package/kit/templates/web-saas/.github/dependabot.yml +3 -3
  174. package/kit/templates/web-saas/.github/workflows/ci.yml +20 -20
  175. package/kit/templates/web-saas/.github/workflows/claude-ci-fix.yml +19 -19
  176. package/kit/templates/web-saas/.github/workflows/claude-maintenance.yml +18 -18
  177. package/kit/templates/web-saas/.github/workflows/live-monitoring.yml +12 -12
  178. package/kit/templates/web-saas/docs/architecture/stack.md +116 -116
  179. package/kit/templates/web-saas/docs/architecture/structure.md +44 -44
  180. package/kit/templates/web-saas/docs/design/design-guide.md +38 -38
  181. package/kit/templates/web-saas/tests/live/smoke.spec.ts +4 -4
  182. package/kit/templates/web-saas/tests/security/rls.mjs +22 -22
  183. package/kit/tools/codex/.codex/hooks.json +1 -1
  184. package/package.json +2 -2
  185. package/README.en.md +0 -267
@@ -7,6 +7,7 @@ import fs from 'node:fs';
7
7
  import path from 'node:path';
8
8
  import { spawn } from 'node:child_process';
9
9
  import { findRoot, isMain } from './shared.mjs';
10
+ import { t } from './lang.mjs';
10
11
 
11
12
  export const THRESHOLD_SEC = 90;
12
13
 
@@ -39,9 +40,9 @@ function main() {
39
40
  try { const r = JSON.parse(fs.readFileSync(path.join(root, '.claude', 'state', 'reminder.json'), 'utf8')); startedAt = r.turnAt ?? r.tur ?? 0; } catch { /* */ } // tur: key before 3.15.0
40
41
  const duration = (Date.now() - startedAt) / 1000;
41
42
  if (!startedAt || duration < THRESHOLD_SEC) process.exit(0);
42
- message = `İş bitti (${Math.round(duration / 60)} dk). Sonuca bakabilirsin.`;
43
+ message = t(root, 'notify.done', { minutes: Math.round(duration / 60) });
43
44
  } else {
44
- message = input.message || 'Claude cevabını bekliyor.';
45
+ message = input.message || t(root, 'notify.waiting');
45
46
  }
46
47
 
47
48
  const title = `Claude Code · ${project}`.slice(0, 60);
@@ -7,7 +7,8 @@
7
7
 
8
8
  import fs from 'node:fs';
9
9
  import path from 'node:path';
10
- import { readInput, decide, isMain } from './shared.mjs';
10
+ import { readInput, decide, isMain, findRoot } from './shared.mjs';
11
+ import { t } from './lang.mjs';
11
12
 
12
13
  export const THRESHOLD = { newDays: 30, npmWeeklyDownloads: 500 };
13
14
 
@@ -148,12 +149,13 @@ const daysAgo = (date, now) => Math.floor((now - new Date(date).getTime()) / 864
148
149
  // get: a fetch-like function (mocked in tests)
149
150
  // status: missing | unknown | similar | new | low | ok
150
151
  export async function check({ eco, name }, get = fetch, now = Date.now()) {
152
+ const tr = (k, vars) => t(findRoot(), k, vars);
151
153
  const similar = similarPopular(eco, name);
152
154
  try {
153
155
  if (eco === 'npm') {
154
156
  const r = await get(`https://registry.npmjs.org/${name.replace('/', '%2f')}`, { signal: AbortSignal.timeout(6000) });
155
- if (r.status === 404) return { status: 'missing', detail: 'npm kayıt defterinde böyle bir paket yok' };
156
- if (!r.ok) return { status: 'unknown', detail: `npm yanıtı ${r.status}` };
157
+ if (r.status === 404) return { status: 'missing', detail: tr('guard.package.npmMissing') };
158
+ if (!r.ok) return { status: 'unknown', detail: tr('guard.package.npmStatus', { status: r.status }) };
157
159
  const j = await r.json();
158
160
  const age = j.time?.created ? daysAgo(j.time.created, now) : null;
159
161
  let downloads = null;
@@ -161,22 +163,22 @@ export async function check({ eco, name }, get = fetch, now = Date.now()) {
161
163
  const dr = await get(`https://api.npmjs.org/downloads/point/last-week/${name}`, { signal: AbortSignal.timeout(6000) });
162
164
  if (dr.ok) downloads = (await dr.json()).downloads ?? null;
163
165
  } catch { /* download count is optional */ }
164
- if (similar) return { status: 'similar', detail: `popüler "${similar}" paketine çok benziyor (yazım hatası tuzağı olabilir)` };
165
- if (age != null && age < THRESHOLD.newDays) return { status: 'new', detail: `${age} gün önce yayımlanmış` };
166
- if (downloads != null && downloads < THRESHOLD.npmWeeklyDownloads) return { status: 'low', detail: `haftalık indirme ${downloads}` };
167
- return { status: 'ok', detail: `${age ?? '?'} gün, haftalık ${downloads ?? '?'} indirme` };
166
+ if (similar) return { status: 'similar', detail: tr('guard.package.similar', { similar }) };
167
+ if (age != null && age < THRESHOLD.newDays) return { status: 'new', detail: tr('guard.package.newNpm', { age }) };
168
+ if (downloads != null && downloads < THRESHOLD.npmWeeklyDownloads) return { status: 'low', detail: tr('guard.package.lowDownloads', { downloads }) };
169
+ return { status: 'ok', detail: `${age ?? '?'} days, ${downloads ?? '?'} weekly downloads` };
168
170
  }
169
171
  const r = await get(`https://pypi.org/pypi/${encodeURIComponent(name)}/json`, { signal: AbortSignal.timeout(6000) });
170
- if (r.status === 404) return { status: 'missing', detail: 'PyPI\'da böyle bir paket yok' };
171
- if (!r.ok) return { status: 'unknown', detail: `PyPI yanıtı ${r.status}` };
172
+ if (r.status === 404) return { status: 'missing', detail: tr('guard.package.pypiMissing') };
173
+ if (!r.ok) return { status: 'unknown', detail: tr('guard.package.pypiStatus', { status: r.status }) };
172
174
  const j = await r.json();
173
175
  const dates = Object.values(j.releases ?? {}).flat().map((f) => f.upload_time_iso_8601).filter(Boolean).sort();
174
176
  const age = dates.length ? daysAgo(dates[0], now) : null;
175
- if (similar) return { status: 'similar', detail: `popüler "${similar}" paketine çok benziyor (yazım hatası tuzağı olabilir)` };
176
- if (age != null && age < THRESHOLD.newDays) return { status: 'new', detail: `ilk sürümü ${age} gün önce yayımlanmış` };
177
- return { status: 'ok', detail: `${age ?? '?'} gün` };
177
+ if (similar) return { status: 'similar', detail: tr('guard.package.similar', { similar }) };
178
+ if (age != null && age < THRESHOLD.newDays) return { status: 'new', detail: tr('guard.package.newPypi', { age }) };
179
+ return { status: 'ok', detail: `${age ?? '?'} days` };
178
180
  } catch (e) {
179
- return { status: 'unknown', detail: `doğrulanamadı (${e.name === 'TimeoutError' ? 'zaman aşımı' : 'ağ hatası'})` };
181
+ return { status: 'unknown', detail: tr(e.name === 'TimeoutError' ? 'guard.package.timeout' : 'guard.package.network') };
180
182
  }
181
183
  }
182
184
 
@@ -192,11 +194,11 @@ async function hook() {
192
194
  const results = await Promise.all(packages.map(async (p) => ({ ...p, ...(await check(p)) })));
193
195
  const missing = results.filter((s) => s.status === 'missing' && !s.unsure);
194
196
  if (missing.length) {
195
- decide(g.tool, 'deny', `Paket kapısı: ${missing.map((s) => `${s.name} (${s.detail})`).join(', ')}. Bu ad uydurulmuş olabilir. Doğru paket adını resmi dokümandan bul; kullanıcıya bildir.`);
197
+ decide(g.tool, 'deny', `Package gate: ${missing.map((s) => `${s.name} (${s.detail})`).join(', ')}. This name may be made up. Find the correct package name in the official docs; tell the user.`);
196
198
  }
197
199
  const suspect = results.filter((s) => s.status !== 'ok');
198
200
  if (suspect.length) {
199
- decide(g.tool, 'ask', `Paket kapısı uyarısı: ${suspect.map((s) => `${s.eco}:${s.name} — ${s.detail}`).join('; ')}. Kurmadan önce paketin gerçekten istenen paket olduğundan emin ol.`);
201
+ decide(g.tool, 'ask', t(g.root, 'guard.package.ask', { list: suspect.map((s) => `${s.eco}:${s.name} — ${s.detail}`).join('; ') }));
200
202
  }
201
203
  process.exit(0);
202
204
  }
@@ -41,31 +41,31 @@ if (isMain(import.meta.url)) {
41
41
  const install = process.argv.includes('--install');
42
42
  let wanted;
43
43
  try { wanted = JSON.parse(fs.readFileSync(path.join(root, '.claude', 'settings.json'), 'utf8')).enabledPlugins; } catch (h) {
44
- console.error(`.claude/settings.json okunamadı: ${h.message}`);
44
+ console.error(`Could not read .claude/settings.json: ${h.message}`);
45
45
  process.exit(1);
46
46
  }
47
47
  let installed;
48
48
  try { installed = JSON.parse(run('plugin', 'list', '--json')); } catch (h) {
49
- console.error(`claude CLI çalışmadı (${reason(h)}). Eklentileri /plugin ile elle kur.`);
49
+ console.error(`The claude CLI failed (${reason(h)}). Install the plugins by hand with /plugin.`);
50
50
  process.exit(1);
51
51
  }
52
52
  const off = disabledHere(wanted, installed, root);
53
- if (off.length) console.log(`Bilgi: kurulu ama bu makinede kapalı (kişisel tercih, değiştirilmedi): ${off.join(', ')}. Açmak için /plugin → Installed.`);
53
+ if (off.length) console.log(`Info: installed but disabled on this machine (personal choice, left unchanged): ${off.join(', ')}. To enable: /plugin → Installed.`);
54
54
  const list = missingPlugins(wanted, installed, root);
55
- if (!list.length) { console.log('Eklentiler eşit: proje ayarındaki her eklenti kurulu.'); process.exit(0); }
55
+ if (!list.length) { console.log('Plugins in sync: every plugin in the project settings is installed.'); process.exit(0); }
56
56
  if (!install) {
57
- console.log(`Kurulu olmayan ${list.length} eklenti:\n${list.map((e) => ` ${e.id}${e.enabled ? '' : ' (kurulup kapatılacak)'}`).join('\n')}\nKurmak için kullanıcıya sor, onaylarsa: node .claude/hooks/plugins-sync.mjs --install`);
57
+ console.log(`${list.length} plugins not installed:\n${list.map((e) => ` ${e.id}${e.enabled ? '' : ' (will be installed, then disabled)'}`).join('\n')}\nAsk the user first; if they approve: node .claude/hooks/plugins-sync.mjs --install`);
58
58
  process.exit(0);
59
59
  }
60
60
  let failures = 0;
61
61
  for (const { id, enabled } of list) {
62
- if (!/^[\w.-]+@[\w.-]+$/.test(id)) { console.error(`atlandı (geçersiz ad): ${id}`); failures++; continue; }
62
+ if (!/^[\w.-]+@[\w.-]+$/.test(id)) { console.error(`skipped (invalid name): ${id}`); failures++; continue; }
63
63
  try {
64
64
  run('plugin', 'install', id, '--scope', 'project');
65
65
  if (!enabled) run('plugin', 'disable', id, '--scope', 'project');
66
- console.log(`kuruldu${enabled ? '' : ' (kapalı)'}: ${id}`);
67
- } catch (h) { console.error(`kurulamadı: ${id} · ${reason(h)}`); failures++; }
66
+ console.log(`installed${enabled ? '' : ' (disabled)'}: ${id}`);
67
+ } catch (h) { console.error(`install failed: ${id} · ${reason(h)}`); failures++; }
68
68
  }
69
- console.log(`${list.length - failures}/${list.length} eklenti kuruldu. Bu oturumda etkinleştirmek için /reload-plugins.`);
69
+ console.log(`${list.length - failures}/${list.length} plugins installed. To enable them in this session: /reload-plugins.`);
70
70
  process.exit(failures ? 1 : 0);
71
71
  }
@@ -2,7 +2,7 @@
2
2
  // Personal preferences: how this user likes to work (talk, questions, reports, code style, things to avoid).
3
3
  // One file for all projects: ~/.vibe-kit/tercihler.md. Git never sees it; the session-start hook loads it every session.
4
4
  // node .claude/hooks/prefs.mjs --list numbered list
5
- // node .claude/hooks/prefs.mjs --add <kategori> "<tercih>" only with the user's approval (asked)
5
+ // node .claude/hooks/prefs.mjs --add <category> "<preference>" only with the user's approval (asked)
6
6
  // node .claude/hooks/prefs.mjs --remove <no> (asked)
7
7
  // Categories: konusma, sorular, raporlar, kod, kacin. A project rule goes to AGENTS.md → Öğrenilen kurallar, not here.
8
8
  // Why a script: the AI never writes this file directly; one line per preference, no secrets, a size cap.
@@ -33,7 +33,7 @@ const valid = (t) => t && t.length <= MAX_LENGTH && !findSecrets(t).length;
33
33
  export function parsePrefs(file = prefsFile()) {
34
34
  const items = [], unknown = [];
35
35
  let text = '';
36
- try { if (fs.statSync(file).size > MAX_BYTES) return { items, unknown: ['(dosya 64 KB\'tan büyük)'] }; text = fs.readFileSync(file, 'utf8'); } catch { return { items, unknown }; }
36
+ try { if (fs.statSync(file).size > MAX_BYTES) return { items, unknown: ['(file larger than 64 KB)'] }; text = fs.readFileSync(file, 'utf8'); } catch { return { items, unknown }; }
37
37
  let category = null;
38
38
  for (const line of text.split(/\r?\n/)) {
39
39
  const h = line.match(/^##\s+(.+)$/);
@@ -47,8 +47,8 @@ export function parsePrefs(file = prefsFile()) {
47
47
  export const readPrefs = (file = prefsFile()) => parsePrefs(file).items;
48
48
 
49
49
  function writePrefs(items, file) {
50
- const lines = ['# Kişisel tercihler (TillerHand)', '',
51
- '> Her oturumda AI\'ya yüklenir, tüm projelerinde geçerlidir. `node .claude/hooks/prefs.mjs --add` ve `--remove` ile değiştir; elle düzenlersen başlıkları ve "- " satırlarını koru.', ''];
50
+ const lines = ['# Personal preferences (TillerHand)', '',
51
+ '> Loaded into the AI every session, applies to all your projects. Change it with `node .claude/hooks/prefs.mjs --add` and `--remove`; if you edit by hand, keep the headings and the "- " lines.', ''];
52
52
  for (const [key, title] of Object.entries(CATEGORIES)) {
53
53
  const list = items.filter((p) => p.category === key);
54
54
  if (list.length) lines.push(`## ${title}`, '', ...list.map((p) => `- ${p.text}`), '');
@@ -57,32 +57,32 @@ function writePrefs(items, file) {
57
57
  fs.mkdirSync(path.dirname(file), { recursive: true });
58
58
  fs.writeFileSync(file, lines.join('\n'));
59
59
  } catch (e) {
60
- return `Dosya yazılamadı (${e.code ?? e.message}): ${file}. Araç ev klasörüne yazamıyorsa satırı elle ekle.`;
60
+ return `Could not write the file (${e.code ?? e.message}): ${file}. If the tool cannot write to the home folder, add the line by hand.`;
61
61
  }
62
62
  return null;
63
63
  }
64
64
 
65
65
  // Returns { error } | { existing: <category> } | {}
66
66
  export function addPref(category, text, file = prefsFile()) {
67
- if (!Object.hasOwn(CATEGORIES, category ?? '')) return { error: `Kategori şunlardan biri olmalı: ${Object.keys(CATEGORIES).join(', ')}` };
67
+ if (!Object.hasOwn(CATEGORIES, category ?? '')) return { error: `Category must be one of: ${Object.keys(CATEGORIES).join(', ')}` };
68
68
  const t = clean(text);
69
- if (!t) return { error: 'Tercih metni boş.' };
70
- if (t.length > MAX_LENGTH) return { error: `Tercih en fazla ${MAX_LENGTH} karakter olmalı; kısalt.` };
71
- if (findSecrets(t).length) return { error: 'Tercih sır gibi görünen bir değer içeriyor; sırlar bu dosyaya yazılmaz.' };
69
+ if (!t) return { error: 'The preference text is empty.' };
70
+ if (t.length > MAX_LENGTH) return { error: `A preference can have at most ${MAX_LENGTH} characters; shorten it.` };
71
+ if (findSecrets(t).length) return { error: 'The preference contains a value that looks like a secret; secrets are not written to this file.' };
72
72
  const { items, unknown } = parsePrefs(file);
73
- if (unknown.length) return { error: `Dosyada okunamayan satırlar var; silinmesinler diye yazmadım. Düzelt ya da kaldır (${file}):\n${unknown.slice(0, 5).join('\n')}` };
73
+ if (unknown.length) return { error: `The file has unreadable lines; nothing was written, so they are not lost. Fix or remove them (${file}):\n${unknown.slice(0, 5).join('\n')}` };
74
74
  const same = items.find((p) => p.text.toLocaleLowerCase('tr') === t.toLocaleLowerCase('tr'));
75
75
  if (same) return { existing: same.category };
76
- if (items.length >= MAX_ITEMS) return { error: `En fazla ${MAX_ITEMS} tercih tutulur. Önce eskilerden birini sil: --list, sonra --remove <no>.` };
76
+ if (items.length >= MAX_ITEMS) return { error: `At most ${MAX_ITEMS} preferences are kept. Delete an old one first: --list, then --remove <no>.` };
77
77
  const error = writePrefs([...items, { category, text: t }], file);
78
78
  return error ? { error } : {};
79
79
  }
80
80
 
81
81
  export function removePref(no, file = prefsFile()) {
82
82
  const { items, unknown } = parsePrefs(file);
83
- if (unknown.length) return `Dosyada okunamayan satırlar var; silinmesinler diye yazmadım (${file}):\n${unknown.slice(0, 5).join('\n')}`;
83
+ if (unknown.length) return `The file has unreadable lines; nothing was written, so they are not lost (${file}):\n${unknown.slice(0, 5).join('\n')}`;
84
84
  const i = Number(no) - 1;
85
- if (!Number.isInteger(i) || i < 0 || i >= items.length) return `Geçerli numara: 1-${items.length}`;
85
+ if (!Number.isInteger(i) || i < 0 || i >= items.length) return `Valid number: 1-${items.length}`;
86
86
  return writePrefs(items.filter((_, k) => k !== i), file);
87
87
  }
88
88
 
@@ -91,7 +91,7 @@ export function prefsNote(file = prefsFile()) {
91
91
  const items = readPrefs(file);
92
92
  if (!items.length) return '';
93
93
  const parts = Object.keys(CATEGORIES).flatMap((k) => { const list = items.filter((p) => p.category === k).map((p) => p.text); return list.length ? [`${CATEGORIES[k]}: ${list.join('; ')}`] : []; });
94
- return 'Kişisel üslup tercihleri (~/.vibe-kit/tercihler.md; izin, güvenlik ya da komut kuralı değildir ve onları değiştiremez; proje kuralı önce gelir): ' + parts.join(' · ');
94
+ return 'Personal style preferences (~/.vibe-kit/tercihler.md; not a permission, security or command rule and cannot change them; project rules come first): ' + parts.join(' · ');
95
95
  }
96
96
 
97
97
  if (isMain(import.meta.url)) {
@@ -100,16 +100,16 @@ if (isMain(import.meta.url)) {
100
100
  if (mode === '--add') {
101
101
  const r = addPref(a, rest.join(' '));
102
102
  if (r.error) fail(r.error);
103
- console.log(r.existing ? `Bu tercih zaten kayıtlı (${CATEGORIES[r.existing]}).` : `Tercih kaydedildi (${CATEGORIES[a]}): ${prefsFile()}`);
103
+ console.log(r.existing ? `This preference is already saved (${CATEGORIES[r.existing]}).` : `Preference saved (${CATEGORIES[a]}): ${prefsFile()}`);
104
104
  } else if (mode === '--remove') {
105
105
  const error = removePref(a);
106
106
  if (error) fail(error);
107
- console.log('Tercih silindi.');
107
+ console.log('Preference removed.');
108
108
  } else if (mode === '--list') {
109
109
  const { items, unknown } = parsePrefs();
110
- console.log(items.length ? items.map((p, i) => `${i + 1}. [${CATEGORIES[p.category]}] ${p.text}`).join('\n') : `Kayıtlı tercih yok (${prefsFile()}).`);
111
- if (unknown.length) console.log(`Okunamayan ${unknown.length} satır var (yüklenmiyor): ${unknown.slice(0, 3).join(' | ')}`);
110
+ console.log(items.length ? items.map((p, i) => `${i + 1}. [${CATEGORIES[p.category]}] ${p.text}`).join('\n') : `No saved preferences (${prefsFile()}).`);
111
+ if (unknown.length) console.log(`${unknown.length} unreadable lines (not loaded): ${unknown.slice(0, 3).join(' | ')}`);
112
112
  } else {
113
- fail('Kullanım: --list | --add <kategori> "<tercih>" | --remove <no>');
113
+ fail('Usage: --list | --add <category> "<preference>" | --remove <no>');
114
114
  }
115
115
  }
@@ -19,7 +19,7 @@ const LOCAL = /^https?:\/\/(localhost|127\.0\.0\.1|\[::1\])(:\d{1,5})?(\/[\w\-./
19
19
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
20
20
  const tail = (s) => s.trimEnd().split(/\r?\n/).slice(-15).join('\n');
21
21
  const block = (s) => (s.trim() ? `\n\`\`\`\n${tail(s)}\n\`\`\`` : '');
22
- const fail = (text) => ({ code: 1, text: `Önizleme yapılamadı: ${text}` });
22
+ const fail = (text) => ({ code: 1, text: `Preview failed: ${text}` });
23
23
 
24
24
  async function answers(url) {
25
25
  try { await fetch(url, { signal: AbortSignal.timeout(2000) }); return true; } catch { return false; }
@@ -50,17 +50,17 @@ function browser(tmp, args) {
50
50
  export async function preview(root, task) {
51
51
  let kit = {};
52
52
  try { kit = JSON.parse(fs.readFileSync(path.join(root, '.kit.json'), 'utf8')); } catch { /* not set up */ }
53
- if (kit.type === 'mobile') return { code: 0, text: 'Önizleme atlandı: mobil projede web görüntüsü telefondakini göstermez. Ekranı telefonda veya Expo Go\'da açıp kontrol etmesini kullanıcıdan iste.' };
53
+ if (kit.type === 'mobile') return { code: 0, text: 'Preview skipped: in a mobile project a web screenshot does not show what the phone shows. Ask the user to open the screen on the phone or in Expo Go and check it.' };
54
54
  const list = approvedCommands(root);
55
55
  const command = (name) => list.find((k) => k.name === name).command;
56
56
  const url = command('önizleme'), dev = command('geliştirme');
57
- if (!url) return fail('AGENTS.md → Komutlar\'da `önizleme:` satırı yok veya boş. Ekle (örnek: `önizleme: http://localhost:3000`) ya da kanıtı bu görevde elle üret.');
58
- if (!LOCAL.test(url)) return fail(`önizleme adresi yerel olmalı (http://localhost:<port>, 127.0.0.1 veya [::1]). Şu an: ${url}`);
59
- if (!isApproved(root, list)) return { code: 1, text: `Önizleme · ${approvalNeeded(list)}` };
57
+ if (!url) return fail('AGENTS.md → Komutlar has no `önizleme:` line, or it is empty. Add it (example: `önizleme: http://localhost:3000`) or produce the evidence by hand in this task.');
58
+ if (!LOCAL.test(url)) return fail(`the önizleme address must be local (http://localhost:<port>, 127.0.0.1 or [::1]). Now: ${url}`);
59
+ if (!isApproved(root, list)) return { code: 1, text: `Preview · ${approvalNeeded(list)}` };
60
60
  const dir = `.previews/${String(task || 'son').replace(/[^\w-]/g, '-')}`;
61
61
  // A linked folder from the repo would send the files elsewhere
62
62
  for (const d of ['.previews', dir]) {
63
- try { if (fs.lstatSync(path.join(root, d)).isSymbolicLink()) return fail(`${d} bir bağlantı (symlink/junction); gerçek klasör olmalı.`); } catch { /* missing: created below */ }
63
+ try { if (fs.lstatSync(path.join(root, d)).isSymbolicLink()) return fail(`${d} is a link (symlink/junction); it must be a real folder.`); } catch { /* missing: created below */ }
64
64
  }
65
65
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'kit-preview-'));
66
66
  const logFile = path.join(tmp, 'dev.log');
@@ -77,7 +77,7 @@ export async function preview(root, task) {
77
77
  process.once('SIGTERM', onSignal);
78
78
  try {
79
79
  if (!(await answers(url))) {
80
- if (!dev) return fail(`${url} açık değil ve AGENTS.md → Komutlar'da \`geliştirme:\` komutu yok.`);
80
+ if (!dev) return fail(`${url} is not up and AGENTS.md → Komutlar has no \`geliştirme:\` command.`);
81
81
  // Output goes to a file, not a pipe: the screenshot steps block the event loop and a full pipe would freeze the server
82
82
  const fd = fs.openSync(logFile, 'w');
83
83
  // A process group on POSIX so the whole tree can be stopped; on Windows detached would open a console window
@@ -90,8 +90,8 @@ export async function preview(root, task) {
90
90
  while (!up && running() && Date.now() < deadline) { await sleep(500); up = await answers(url); }
91
91
  if (!up) {
92
92
  return fail(running()
93
- ? `${url} ${Math.round(wait / 1000)} sn içinde açılmadı. Adres ve port AGENTS.md → Komutlar'daki \`önizleme:\` ile aynı mı?${block(devLog())}`
94
- : `dev sunucu açılmadan kapandı (${child.exitCode ?? child.signalCode}): ${dev}${block(devLog())}`);
93
+ ? `${url} did not come up within ${Math.round(wait / 1000)} s. Are the address and port the same as \`önizleme:\` in AGENTS.md → Komutlar?${block(devLog())}`
94
+ : `the dev server exited before it came up (${child.exitCode ?? child.signalCode}): ${dev}${block(devLog())}`);
95
95
  }
96
96
  }
97
97
  opened = true;
@@ -100,7 +100,7 @@ export async function preview(root, task) {
100
100
  const r = browser(tmp, step.map(String));
101
101
  const shot = step[0] === 'screenshot' ? path.join(tmp, step[1].slice(11)) : null;
102
102
  if (r.status !== 0 || (shot && !fs.existsSync(shot))) {
103
- return fail(`ekran görüntüsü alınamadı (${step.join(' ')}).${block(`${r.error?.message ?? ''}\n${r.stdout ?? ''}${r.stderr ?? ''}`)}`);
103
+ return fail(`could not take the screenshot (${step.join(' ')}).${block(`${r.error?.message ?? ''}\n${r.stdout ?? ''}${r.stderr ?? ''}`)}`);
104
104
  }
105
105
  }
106
106
  fs.mkdirSync(path.join(root, dir), { recursive: true });
@@ -109,7 +109,7 @@ export async function preview(root, task) {
109
109
  fs.rmSync(dest, { force: true }); // a link left in its place is removed, not written through
110
110
  fs.copyFileSync(path.join(tmp, `${name}.png`), dest);
111
111
  }
112
- return { code: 0, text: `Önizleme hazır (${url}):\n${SIZES.map(([name, w]) => `- ${dir}/${name}.png (${w} px)`).join('\n')}\nGörüntüleri aç ve kullanıcıya göster.` };
112
+ return { code: 0, text: `Preview ready (${url}):\n${SIZES.map(([name, w]) => `- ${dir}/${name}.png (${w} px)`).join('\n')}\nOpen the images and show them to the user.` };
113
113
  } finally {
114
114
  if (opened) browser(tmp, ['close']);
115
115
  if (child) await stop(child);
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  // Mechanical doc writes at task end, by script instead of a model call (0 tokens, the format cannot drift).
3
3
  // node .claude/hooks/progress.mjs --done G-012 roadmap: [ ] → [x]
4
- // node .claude/hooks/progress.mjs --cost "<iş>" <K|O|B|BOARD|DESIGN|FINDINGS> <başlangıç %> <bitiş %> <dk> ["<not>"]
4
+ // node .claude/hooks/progress.mjs --cost "<job>" <K|O|B|BOARD|DESIGN|FINDINGS> <start %> <end %> <min> ["<note>"]
5
5
  // node .claude/hooks/progress.mjs --start [--limit N] at task start: keeps the 5-hour % and the time
6
- // node .claude/hooks/progress.mjs --cost "<iş>" <boyut> ["<not>"] [--limit N] start from --start, end now (once)
7
- // node .claude/hooks/progress.mjs --task "<aktif görev>" --did "<yapılan>" --next "<sıradaki adım>"
6
+ // node .claude/hooks/progress.mjs --cost "<job>" <size> ["<note>"] [--limit N] start from --start, end now (once)
7
+ // node .claude/hooks/progress.mjs --task "<active task>" --did "<done>" --next "<next step>"
8
8
  // node .claude/hooks/progress.mjs --check progress.md labels the status line and dashboard read
9
9
  // Every progress write also sets **Son güncelleme:** to today. Flags can be combined in one call.
10
10
  import fs from 'node:fs';
@@ -15,7 +15,7 @@ import { readLimits } from './usage.mjs';
15
15
 
16
16
  const LABELS = ['Son güncelleme', 'Aktif faz', 'Aktif görev', 'Başlangıç adımı', 'Soru modu', 'Profil'];
17
17
  const SIZES = ['K', 'O', 'B', 'BOARD', 'DESIGN', 'FINDINGS'];
18
- const USAGE = 'Kullanım: node .claude/hooks/progress.mjs [--done G-NNN] [--start] [--cost "<iş>" <K|O|B|BOARD|DESIGN|FINDINGS> [<başlangıç %> <bitiş %> <dk>] ["<not>"]] [--limit N] [--task "<metin>"] [--did "<metin>"] [--next "<metin>"] [--check]';
18
+ const USAGE = 'Usage: node .claude/hooks/progress.mjs [--done G-NNN] [--start] [--cost "<job>" <K|O|B|BOARD|DESIGN|FINDINGS> [<start %> <end %> <min>] ["<note>"]] [--limit N] [--task "<text>"] [--did "<text>"] [--next "<text>"] [--check]';
19
19
  // One line per value: a newline would break a label, a list item or a table row
20
20
  const line = (s) => clean(String(s).replace(/\s*\r?\n\s*/g, ' ')).trim();
21
21
  const today = () => new Date().toLocaleDateString('sv-SE');
@@ -26,9 +26,9 @@ const section = (text, title) => text.match(new RegExp(`^## ${title}[^\\n]*\\n([
26
26
  export function markDone(text, id) {
27
27
  const re = new RegExp(`^(\\s*- )\\[( |x)\\](\\s*${id}:)`, 'mi');
28
28
  const m = text.match(re);
29
- if (!m) return { error: `${id} yol haritasında yok.` };
30
- if (m[2].toLowerCase() === 'x') return { text, note: `${id} zaten işaretli.` };
31
- return { text: text.replace(re, '$1[x]$3'), note: `${id} işaretlendi.` };
29
+ if (!m) return { error: `${id} is not in the roadmap.` };
30
+ if (m[2].toLowerCase() === 'x') return { text, note: `${id} is already marked.` };
31
+ return { text: text.replace(re, '$1[x]$3'), note: `${id} marked done.` };
32
32
  }
33
33
 
34
34
  export function costRow(args, date = today()) {
@@ -76,10 +76,10 @@ if (isMain(import.meta.url)) {
76
76
  const limit = () => { const v = value('--limit'); const n = v != null ? Number(v) : readLimits(root)?.five_hour?.percent; return Number.isFinite(n) ? n : null; };
77
77
  const startFile = file('.claude/state/task-start.json');
78
78
  if (argv.includes('--start')) {
79
- const percent = limit() ?? fail('Limit okunamadı: --limit <5 saatlik %> ver (masaüstünde get_usage).');
79
+ const percent = limit() ?? fail('Could not read the limit: pass --limit <5-hour %> (on desktop, get_usage).');
80
80
  fs.mkdirSync(path.dirname(startFile), { recursive: true });
81
81
  fs.writeFileSync(startFile, JSON.stringify({ percent, at: Date.now() }));
82
- console.log(`Görev başlangıcı kaydedildi: 5s %${percent}.`);
82
+ console.log(`Task start saved: 5h %${percent}.`);
83
83
  }
84
84
 
85
85
  const c = argv.indexOf('--cost');
@@ -90,26 +90,26 @@ if (isMain(import.meta.url)) {
90
90
  if (auto) {
91
91
  // Only job and size (and a note): the start comes from --start, used once so an old start never leaks into the next task
92
92
  let start;
93
- try { start = JSON.parse(fs.readFileSync(startFile, 'utf8')); } catch { fail('Görev başlangıcı yok: görev başında progress.mjs --start çalıştır ya da yüzdeleri ver.'); }
94
- const end = limit() ?? fail('Limit okunamadı: --limit <5 saatlik %> ver (masaüstünde get_usage).');
93
+ try { start = JSON.parse(fs.readFileSync(startFile, 'utf8')); } catch { fail('No task start: run progress.mjs --start at the start of the task or pass the percentages.'); }
94
+ const end = limit() ?? fail('Could not read the limit: pass --limit <5-hour %> (on desktop, get_usage).');
95
95
  args.splice(2, 0, String(start.percent), String(end), String(Math.round((Date.now() - start.at) / 60000)));
96
96
  }
97
97
  const row = costRow(args) ?? fail(USAGE);
98
98
  const log = file('docs/workflow/cost-log.md');
99
99
  fs.writeFileSync(log, `${fs.readFileSync(log, 'utf8').trimEnd()}\n${row}\n`);
100
100
  if (auto) fs.rmSync(startFile, { force: true });
101
- console.log(`Maliyet satırı eklendi: ${row}`);
101
+ console.log(`Cost row added: ${row}`);
102
102
  }
103
103
 
104
104
  const [task, did, next] = ['--task', '--did', '--next'].map(value);
105
105
  if ([task, did, next].some((v) => v != null)) {
106
106
  fs.writeFileSync(file('docs/workflow/progress.md'), updateProgress(fs.readFileSync(file('docs/workflow/progress.md'), 'utf8'), { task, did, next }));
107
- console.log('progress.md güncellendi.');
107
+ console.log('progress.md updated.');
108
108
  }
109
109
 
110
110
  if (argv.includes('--check')) {
111
111
  const missing = missingLabels(fs.readFileSync(file('docs/workflow/progress.md'), 'utf8'));
112
- console.log(missing.length ? `progress.md'de eksik ya da bozuk kalın etiket: ${missing.join(', ')}. Biçim: **Etiket:** değer` : 'progress.md etiketleri sağlam.');
112
+ console.log(missing.length ? `progress.md has missing or broken bold labels: ${missing.join(', ')}. Format: **Label:** value` : 'progress.md labels are intact.');
113
113
  if (missing.length) code = 1;
114
114
  }
115
115
  process.exit(code);
@@ -9,6 +9,7 @@ import fs from 'node:fs';
9
9
  import path from 'node:path';
10
10
  import { prdCriteria, junitCases } from './dashboard-data.mjs';
11
11
  import { findRoot, isMain } from './shared.mjs';
12
+ import { t } from './lang.mjs';
12
13
 
13
14
  // A pipe would split the table cell (the dashboard's table reader does not know \|)
14
15
  const cell = (s) => String(s ?? '').split(/\r?\n/)[0].trim().slice(0, 160).replace(/\|/g, '/');
@@ -18,15 +19,15 @@ const reasonLine = (s) => (s ?? '').split(/\r?\n/).find((l) => /^\s*Error\b/.tes
18
19
  const STALE_MIN = 30;
19
20
 
20
21
  export function qaReport(root, junit = 'test-results-qa/junit.xml', now = new Date()) {
21
- const none = (why) => ({ code: 2, text: `QA raporu yazılmadı: ${why}` });
22
+ const none = (why) => ({ code: 2, text: `QA report not written: ${why}` });
22
23
  let xml, age;
23
24
  try { xml = fs.readFileSync(path.resolve(root, junit), 'utf8'); age = (now - fs.statSync(path.resolve(root, junit)).mtimeMs) / 60000; } catch {
24
- return none(`${junit} yok. Testleri JUnit raporuyla çalıştır (qa.md adım 4).`);
25
+ return none(`${junit} not found. Run the tests with a JUnit report (qa.md step 4).`);
25
26
  }
26
27
  let prd;
27
- try { prd = fs.readFileSync(path.join(root, 'docs/product/prd.md'), 'utf8'); } catch { return none('docs/product/prd.md yok.'); }
28
+ try { prd = fs.readFileSync(path.join(root, 'docs/product/prd.md'), 'utf8'); } catch { return none('docs/product/prd.md not found.'); }
28
29
  const criteria = prdCriteria(prd);
29
- if (!criteria.length) return none('PRD\'de kimlikli kabul kriteri yok (`- [ ] Ö-01.1: ...`).');
30
+ if (!criteria.length) return none('the PRD has no acceptance criterion with an id (`- [ ] Ö-01.1: ...`).');
30
31
  const results = new Map();
31
32
  for (const c of junitCases(xml)) {
32
33
  for (const k of c.keys) {
@@ -53,15 +54,15 @@ export function qaReport(root, junit = 'test-results-qa/junit.xml', now = new Da
53
54
  return { criterion: `${k.id} ${k.text}`, result: !r ? 'koşulmadı' : r.failed ? 'kaldı' : 'geçti', evidence, note: r?.failed ? reasonLine(r.reason) : '' };
54
55
  });
55
56
  const count = (x) => rows.filter((r) => r.result === x).length;
56
- if (!count('geçti') && !count('kaldı')) return none(`${junit} içinde PRD kriterine bağlı test sonucu yok (test adı kimlikle başlamalı: \`Ö-01.1 ...\`; sunucu açıldı mı?).`);
57
- const summary = `${count('geçti')} geçti · ${count('kaldı')} kaldı · ${count('koşulmadı')} koşulmadı`;
57
+ if (!count('geçti') && !count('kaldı')) return none(`no test result tied to a PRD criterion in ${junit} (the test name must start with the id: \`Ö-01.1 ...\`; did the server start?).`);
58
+ const counts = { passed: count('geçti'), failed: count('kaldı'), notRun: count('koşulmadı') };
58
59
  const file = `docs/quality/qa/${stamp}.md`;
59
- const md = `# QA raporu · ${stamp}\n\n**Sonuç:** ${summary} (PRD'de ${rows.length} kriter, kaynak: \`${junit}\`)\n\n` +
60
- `| Kriter | Sonuç | Kanıt | Not |\n|---|---|---|---|\n${rows.map((r) => `| ${cell(r.criterion)} | ${r.result} | ${r.evidence} | ${cell(r.note)} |`).join('\n')}\n`;
60
+ const md = `${t(root, 'qaReport.title', { stamp })}\n\n${t(root, 'qaReport.result', { summary: t(root, 'qaReport.summary', counts), count: rows.length, junit })}\n\n` +
61
+ `${t(root, 'qaReport.tableHeader')}\n|---|---|---|---|\n${rows.map((r) => `| ${cell(r.criterion)} | ${r.result} | ${r.evidence} | ${cell(r.note)} |`).join('\n')}\n`;
61
62
  fs.mkdirSync(path.join(root, 'docs/quality/qa'), { recursive: true });
62
63
  fs.writeFileSync(path.join(root, file), md);
63
- const stale = age > STALE_MIN ? `\nUyarı: ${junit} ${Math.round(age)} dk önce yazılmış; bu koşunun sonucu olmayabilir, testleri yeniden çalıştır.` : '';
64
- return { code: count('kaldı') ? 1 : 0, text: `QA: ${summary} → ${file}${stale}` };
64
+ const stale = age > STALE_MIN ? `\nWarning: ${junit} was written ${Math.round(age)} min ago; it may not be the result of this run, rerun the tests.` : '';
65
+ return { code: count('kaldı') ? 1 : 0, text: `QA: ${counts.passed} passed · ${counts.failed} failed · ${counts.notRun} not run → ${file}${stale}` };
65
66
  }
66
67
 
67
68
  if (isMain(import.meta.url)) {
@@ -5,6 +5,7 @@
5
5
  import fs from 'node:fs';
6
6
  import path from 'node:path';
7
7
  import { findRoot } from './shared.mjs';
8
+ import { readLang } from './lang.mjs';
8
9
 
9
10
  export const INTERVAL = 12; // every 12 messages
10
11
  const root = findRoot();
@@ -20,14 +21,16 @@ data.turnAt = Date.now(); // notify.mjs computes the length of long jobs from th
20
21
  try { fs.mkdirSync(path.dirname(counterFile), { recursive: true }); fs.writeFileSync(counterFile, JSON.stringify(data)); } catch { /* not writable: skip silently */ }
21
22
 
22
23
  const notes = [];
24
+ const lang = readLang(root);
25
+ if (lang) notes.push(`[Kit] Reply in ${lang}.`); // every message: the language rule must not fade in long sessions
23
26
  if (data.count % INTERVAL === 0) {
24
- notes.push('[Kit hatırlatma] Kararları AskUserQuestion ile sor (önerilen ilk sırada). Kapsamı genişletme. Kurul ve limit kurallarına uy. Her cevabın sonuna durum bloğu ekle.');
27
+ notes.push('[Kit reminder] Ask decisions with AskUserQuestion (recommended option first). Do not widen the scope. Follow the board and limit rules. End every answer with the status block.');
25
28
  }
26
29
  try {
27
30
  const b = JSON.parse(fs.readFileSync(path.join(root, '.claude', 'state', 'context.json'), 'utf8'));
28
31
  const percent = b.percent;
29
- if (percent >= 70) notes.push(`[Kit] Bağlam %${percent} dolu: yanıt kalitesi düşebilir. Mevcut adımı bitir, kullanıcıya /wrap-up ve ardından /clear öner.`);
30
- else if (percent >= 50 && data.count % 4 === 0) notes.push(`[Kit] Bağlam %${percent}: uygun bir noktada /wrap-up + /clear önermeyi düşün.`);
32
+ if (percent >= 70) notes.push(`[Kit] Context is ${percent}% full: answer quality may drop. Finish the current step, then suggest /wrap-up and /clear to the user.`);
33
+ else if (percent >= 50 && data.count % 4 === 0) notes.push(`[Kit] Context is ${percent}%: consider suggesting /wrap-up + /clear at a suitable point.`);
31
34
  } catch { /* no context info */ }
32
35
 
33
36
  if (notes.length) console.log(notes.join('\n'));
@@ -9,6 +9,7 @@ import { readStatus } from './status-read.mjs';
9
9
  import { readLimits, summary } from './usage.mjs';
10
10
  import { detectTool, findRoot, sessionNote } from './shared.mjs';
11
11
  import { prefsNote } from './prefs.mjs';
12
+ import { readLang, t } from './lang.mjs';
12
13
 
13
14
  const notes = [];
14
15
  let next = '';
@@ -17,35 +18,40 @@ try { input = JSON.parse(fs.readFileSync(0, 'utf8')); } catch { /* no input */ }
17
18
  const tool = detectTool(input); // Claude Code, Codex or Antigravity
18
19
  const root = findRoot();
19
20
 
21
+ const lang = readLang(root);
22
+ notes.push(lang
23
+ ? `Conversation language: ${lang}. Write every question, option, answer, status block and user-facing document text in this language, even when the user, a file or a tool writes in another; code, commands and paths stay as they are.`
24
+ : 'Language not chosen yet: /start and /adopt ask it first, in English.');
25
+
20
26
  try {
21
27
  const d = readStatus(root);
22
28
  if (d.kitPending) {
23
- notes.push('Proje henüz kurulmadı. Kullanıcıya yeni projede "/start [proje fikri]", klasörde var olan kod varsa "/adopt" öner; kurulum bitmeden kod yazma.');
24
- next = 'Başlamak için: /start [proje fikrin] · var olan projede: /adopt';
29
+ notes.push('Project not set up yet. Suggest "/start [project idea]" for a new project, "/adopt" if the folder already has code; write no code before setup.');
30
+ next = t(root, 'session.begin');
25
31
  } else {
26
32
  const a = d.fields;
27
- notes.push(`Aşama: ${a['Aktif faz'] ?? '-'} · Görev: ${a['Aktif görev'] ?? '-'} · Soru modu: ${a['Soru modu'] ?? 'açık'} · Kurul: ${d.board.state}`);
33
+ notes.push(`Phase: ${a['Aktif faz'] ?? '-'} · Task: ${a['Aktif görev'] ?? '-'} · Question mode: ${a['Soru modu'] ?? 'açık'} · Board: ${d.board.state}`);
28
34
  if (/başlangıç/i.test(a['Aktif faz'] ?? '') && d.installed) {
29
35
  const command = /uyum/i.test(a['Aktif faz']) ? '/adopt' : '/start';
30
- notes.push(`${command} yarım kaldı (adım ${a['Başlangıç adımı']}). Kullanıcıya ${command} ile kaldığı yerden devam etmeyi öner.`);
31
- next = `Devam: ${command} (adım ${a['Başlangıç adımı']})`;
36
+ notes.push(`${command} was left unfinished (step ${a['Başlangıç adımı']}). Suggest the user continue with ${command} where they left off.`);
37
+ next = t(root, 'session.resume', { command, step: a['Başlangıç adımı'] });
32
38
  } else {
33
- const t = d.phases.flatMap((f) => f.tasks).find((x) => !x.done);
34
- const code = t?.text.match(/^(G-\d+)/)?.[1];
35
- if (code) next = `Sıradaki: /build ${code} · ${t.text.replace(/^G-\d+:\s*/, '').split(' — ')[0].replace(/\*\*/g, '').slice(0, 70)}`;
39
+ const task = d.phases.flatMap((f) => f.tasks).find((x) => !x.done);
40
+ const code = task?.text.match(/^(G-\d+)/)?.[1];
41
+ if (code) next = t(root, 'session.next', { code, title: task.text.replace(/^G-\d+:\s*/, '').split(' — ')[0].replace(/\*\*/g, '').slice(0, 70) });
36
42
  }
37
43
  if (d.handoffPending) {
38
- notes.push('Devir notu bekliyor (docs/workflow/handoff.md): proje başka kişiden veya hesaptan devralınıyor. İşe başlamadan /handoff devral öner.');
39
- next = `Devir notu var: /handoff devral${next ? ` · sonra ${next}` : ''}`;
44
+ notes.push('Handoff note waiting (docs/workflow/handoff.md): the project is being taken over from another person or account. Suggest /handoff devral before starting work.');
45
+ next = next ? t(root, 'session.handoffThen', { next }) : t(root, 'session.handoff');
40
46
  }
41
47
  if (next) notes.push(next);
42
- if (d.leftOff.length) notes.push(`Kaldığı yer: ${d.leftOff.join(' | ')}. Kullanıcıya buradan devam etmeyi öner.`);
43
- if (d.pending.length) notes.push(`Bekleyen kararlar: ${d.pending.join(' | ')}. Önce bunları sor.`);
48
+ if (d.leftOff.length) notes.push(`Where we left off: ${d.leftOff.join(' | ')}. Suggest the user continue from here.`);
49
+ if (d.pending.length) notes.push(`Pending decisions: ${d.pending.join(' | ')}. Ask these first.`);
44
50
  }
45
51
  if (tool === 'claude') { // limit data and the caveman plugin are Claude Code only
46
52
  const l = readLimits(root);
47
- notes.push(l ? `Limit (son bilinen): ${summary(l)}` : 'Limit: bilinmiyor. İş planlamadan önce oku (get_usage veya node .claude/hooks/usage.mjs).');
48
- notes.push('Rapor dili için caveman:caveman skill\'ini yükle (yüklü değilse atla).');
53
+ notes.push(l ? `Limit (last known): ${summary(l)}` : 'Limit: unknown. Read it before planning work (get_usage or node .claude/hooks/usage.mjs).');
54
+ notes.push('Load the caveman:caveman skill for the report style (skip if it is not installed).');
49
55
  }
50
56
  } catch {
51
57
  // docs could not be read: give git info only
@@ -54,13 +60,13 @@ try {
54
60
  const git = (c) => execSync(`git ${c}`, { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
55
61
  try {
56
62
  git('rev-parse --is-inside-work-tree');
57
- const branch = git('branch --show-current') || '(ayrık HEAD)';
63
+ const branch = git('branch --show-current') || '(detached HEAD)';
58
64
  const changed = git('status --porcelain').split('\n').filter(Boolean).length;
59
- notes.push(`Dal: ${branch} · commit edilmemiş dosya: ${changed}`);
65
+ notes.push(`Branch: ${branch} · uncommitted files: ${changed}`);
60
66
  const lastProgress = git('log -1 --format=%H -- docs/workflow/progress.md');
61
67
  if (lastProgress) {
62
68
  const since = Number(git(`rev-list --count ${lastProgress}..HEAD`));
63
- if (since >= 3) notes.push(`UYARI: docs/workflow/progress.md son ${since} commit'tir güncellenmedi. Oturum sonunda /wrap-up hatırlat.`);
69
+ if (since >= 3) notes.push(`WARNING: docs/workflow/progress.md has not been updated for the last ${since} commits. Remind the user to run /wrap-up at the end of the session.`);
64
70
  }
65
71
  } catch {
66
72
  // not a git repo, or before the first commit
@@ -120,7 +120,7 @@ export function decisionText(tool, decision, reason) {
120
120
  if (tool === 'antigravity') return JSON.stringify({ decision, reason });
121
121
  if (tool === 'codex' && decision === 'ask') {
122
122
  decision = 'deny';
123
- reason = `Kullanıcı onayı gerekli. ${reason} Codex'te onay penceresi olmadığı için engellendi: kullanıcıya sor; onaylarsa işlemi kendisi yapsın.`;
123
+ reason = `User approval required. ${reason} Blocked because Codex has no approval prompt: ask the user; if they approve, they should do the action themselves.`;
124
124
  }
125
125
  return JSON.stringify({ hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: decision, permissionDecisionReason: reason } });
126
126
  }