tillerhand 3.47.1 → 3.48.1

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 (126) hide show
  1. package/.claude/agents/board-chair.md +9 -9
  2. package/.claude/agents/code-reviewer.md +2 -2
  3. package/.claude/agents/council-advisor.md +2 -2
  4. package/.claude/agents/security-auditor.md +6 -6
  5. package/.claude/agents/test-writer.md +1 -1
  6. package/.claude/board/board-ai.md +6 -6
  7. package/.claude/board/board-api.md +6 -6
  8. package/.claude/board/board-architect.md +7 -7
  9. package/.claude/board/board-devops.md +6 -6
  10. package/.claude/board/board-mobile.md +6 -6
  11. package/.claude/board/board-performance.md +6 -6
  12. package/.claude/board/board-privacy.md +6 -6
  13. package/.claude/board/board-product.md +7 -7
  14. package/.claude/board/board-qa.md +7 -7
  15. package/.claude/board/board-ux.md +6 -6
  16. package/.claude/board/council.md +32 -32
  17. package/.claude/board/rules.md +8 -8
  18. package/.claude/commands/adopt.md +10 -10
  19. package/.claude/commands/ask-mode.md +6 -5
  20. package/.claude/commands/board.md +1 -1
  21. package/.claude/commands/build.md +8 -8
  22. package/.claude/commands/design.md +8 -8
  23. package/.claude/commands/findings-all.md +2 -2
  24. package/.claude/commands/findings-security.md +1 -1
  25. package/.claude/commands/fix-bug.md +2 -2
  26. package/.claude/commands/handoff.md +12 -12
  27. package/.claude/commands/language.md +1 -1
  28. package/.claude/commands/maintain.md +2 -2
  29. package/.claude/commands/plan-task.md +3 -3
  30. package/.claude/commands/qa.md +5 -5
  31. package/.claude/commands/release.md +3 -3
  32. package/.claude/commands/research.md +2 -2
  33. package/.claude/commands/retro.md +2 -2
  34. package/.claude/commands/review-changes.md +2 -2
  35. package/.claude/commands/rollback.md +1 -1
  36. package/.claude/commands/start.md +20 -20
  37. package/.claude/commands/tools.md +8 -6
  38. package/.claude/commands/user-feedback.md +3 -3
  39. package/.claude/commands/wrap-up.md +3 -3
  40. package/.claude/findings/protocol.md +2 -2
  41. package/.claude/findings/report-template.md +1 -1
  42. package/.claude/hooks/commit-gate.mjs +11 -9
  43. package/.claude/hooks/council.mjs +51 -24
  44. package/.claude/hooks/dashboard-data.mjs +73 -53
  45. package/.claude/hooks/dashboard.mjs +80 -71
  46. package/.claude/hooks/destructive-guard.mjs +1 -1
  47. package/.claude/hooks/freeze-guard.mjs +1 -1
  48. package/.claude/hooks/gate.mjs +14 -11
  49. package/.claude/hooks/labels.mjs +177 -0
  50. package/.claude/hooks/messages.mjs +8 -7
  51. package/.claude/hooks/notify.mjs +2 -1
  52. package/.claude/hooks/prefs.mjs +24 -10
  53. package/.claude/hooks/preview.mjs +6 -6
  54. package/.claude/hooks/progress.mjs +27 -15
  55. package/.claude/hooks/qa-report.mjs +12 -10
  56. package/.claude/hooks/session-start.mjs +9 -7
  57. package/.claude/hooks/status-read.mjs +22 -18
  58. package/.claude/hooks/statusline.mjs +10 -8
  59. package/.claude/hooks/tools.mjs +13 -8
  60. package/.claude/output-styles/{anlatici.md → narrator.md} +2 -2
  61. package/.claude/skills/logo-design/SKILL.md +2 -2
  62. package/.claude/skills/source-research/SKILL.md +1 -1
  63. package/.claude/skills/ui-motion/SKILL.md +2 -2
  64. package/AGENTS.md +12 -12
  65. package/CLAUDE.md +1 -1
  66. package/README.md +2 -2
  67. package/README.tr.md +3 -3
  68. package/docs/decisions/adr-template.md +1 -1
  69. package/docs/decisions/log.md +2 -2
  70. package/docs/findings/README.md +1 -1
  71. package/docs/operations/accounts.md +7 -7
  72. package/docs/operations/deploy-setup.md +3 -3
  73. package/docs/operations/feature-flags.md +2 -2
  74. package/docs/operations/maintenance.md +1 -1
  75. package/docs/operations/release-checklist.md +2 -2
  76. package/docs/operations/spend-alarms.md +2 -2
  77. package/docs/product/feedback.md +4 -4
  78. package/docs/product/launch.md +1 -1
  79. package/docs/product/prd.md +10 -10
  80. package/docs/product/roadmap.md +8 -8
  81. package/docs/product/specs/_template.md +8 -8
  82. package/docs/product/unit-economics.md +10 -10
  83. package/docs/quality/metrics.md +2 -2
  84. package/docs/quality/test-plan.md +5 -5
  85. package/docs/security/checklist.md +2 -2
  86. package/docs/workflow/board.md +5 -5
  87. package/docs/workflow/cost-log.md +1 -1
  88. package/docs/workflow/decision-protocol.md +18 -16
  89. package/docs/workflow/learning.md +2 -2
  90. package/docs/workflow/mcp-catalog.md +1 -1
  91. package/docs/workflow/models.md +1 -1
  92. package/docs/workflow/progress.md +14 -14
  93. package/docs/workflow/skills.md +9 -9
  94. package/docs/workflow/usage-limits.md +5 -5
  95. package/kit/CHANGELOG.md +28 -0
  96. package/kit/VERSION +1 -1
  97. package/kit/guide.html +21 -21
  98. package/kit/guide.tr.html +21 -21
  99. package/kit/kit.mjs +29 -0
  100. package/kit/project/README.md +1 -1
  101. package/kit/templates/addons/ai/ai/evals/README.md +5 -5
  102. package/kit/templates/addons/ai/ai/evals/cases.jsonl +3 -3
  103. package/kit/templates/addons/ai/docs/ai/guide.md +1 -1
  104. package/kit/templates/backend/.github/workflows/ci.yml +4 -4
  105. package/kit/templates/backend/.github/workflows/claude-ci-fix.yml +10 -10
  106. package/kit/templates/backend/.github/workflows/claude-maintenance.yml +1 -1
  107. package/kit/templates/backend/.github/workflows/live-monitoring.yml +1 -1
  108. package/kit/templates/backend/.pre-commit-config.yaml +1 -1
  109. package/kit/templates/backend/docs/architecture/stack.md +7 -7
  110. package/kit/templates/backend/docs/architecture/structure.md +2 -2
  111. package/kit/templates/mobile/.github/workflows/ci.yml +4 -4
  112. package/kit/templates/mobile/.github/workflows/claude-ci-fix.yml +10 -10
  113. package/kit/templates/mobile/.github/workflows/claude-maintenance.yml +1 -1
  114. package/kit/templates/mobile/docs/architecture/stack.md +5 -5
  115. package/kit/templates/mobile/docs/architecture/structure.md +1 -1
  116. package/kit/templates/mobile/docs/design/design-guide.md +1 -1
  117. package/kit/templates/mobile/tests/security/rls.mjs +4 -4
  118. package/kit/templates/web-saas/.github/workflows/ci.yml +4 -4
  119. package/kit/templates/web-saas/.github/workflows/claude-ci-fix.yml +10 -10
  120. package/kit/templates/web-saas/.github/workflows/claude-maintenance.yml +1 -1
  121. package/kit/templates/web-saas/.github/workflows/live-monitoring.yml +1 -1
  122. package/kit/templates/web-saas/docs/architecture/stack.md +12 -12
  123. package/kit/templates/web-saas/docs/architecture/structure.md +2 -2
  124. package/kit/templates/web-saas/docs/design/design-guide.md +1 -1
  125. package/kit/templates/web-saas/tests/security/rls.mjs +4 -4
  126. package/package.json +1 -1
@@ -0,0 +1,177 @@
1
+ // Label dictionary (3.48.0): the labels hooks read from project files, Turkish (pre-3.48 projects) and English.
2
+ // Readers call normalize() first and then only know the English labels. Turkish reading goes away in 4.0.
3
+
4
+ export const LABELS = {
5
+ lastUpdated: { en: 'Last updated', tr: 'Son güncelleme' },
6
+ activePhase: { en: 'Active phase', tr: 'Aktif faz' },
7
+ activeTask: { en: 'Active task', tr: 'Aktif görev' },
8
+ setupStep: { en: 'Setup step', tr: 'Başlangıç adımı' },
9
+ questionMode: { en: 'Question mode', tr: 'Soru modu' },
10
+ profile: { en: 'Profile', tr: 'Profil' },
11
+ currentState: { en: 'Current state', tr: 'Şu anki durum' },
12
+ doneLastSession: { en: 'Done last session', tr: 'Son oturumda yapılanlar' },
13
+ nextStep: { en: 'Next step', tr: 'Sıradaki adım' },
14
+ pendingDecisions: { en: 'Pending decisions', tr: 'Bekleyen kararlar' },
15
+ whereWeLeftOff: { en: 'Where we left off', tr: 'Kaldığı yer' },
16
+ openIssues: { en: 'Open issues / blockers', tr: 'Açık sorunlar / bloklayıcılar' },
17
+ status: { en: 'Status', tr: 'Durum' },
18
+ commitReview: { en: 'Commit review', tr: 'Commit denetimi' },
19
+ members: { en: 'Members', tr: 'Üyeler' },
20
+ meetingLog: { en: 'Meeting log', tr: 'Toplantı günlüğü' },
21
+ decisionLog: { en: 'Decision log', tr: 'Karar günlüğü' },
22
+ phase: { en: 'Phase', tr: 'Faz' },
23
+ commands: { en: 'Commands', tr: 'Komutlar' },
24
+ dev: { en: 'dev', tr: 'geliştirme' },
25
+ preview: { en: 'preview', tr: 'önizleme' },
26
+ typeCheck: { en: 'type check', tr: 'tip kontrol' },
27
+ type: { en: 'Type', tr: 'Tür' },
28
+ class: { en: 'Class', tr: 'Sınıf' },
29
+ mustNotChange: { en: 'Behavior that must not change', tr: 'Değişmemesi gereken davranış' },
30
+ priority: { en: 'Priority', tr: 'Öncelik' },
31
+ outOfScope: { en: 'Out of scope', tr: 'Kapsam dışı' },
32
+ chosenTech: { en: 'Chosen technologies', tr: 'Seçilen teknolojiler' },
33
+ maintenanceLog: { en: 'Maintenance log', tr: 'Bakım kaydı' },
34
+ flags: { en: 'Flags', tr: 'Bayraklar' },
35
+ records: { en: 'Records', tr: 'Kayıtlar' },
36
+ unreleased: { en: 'Unreleased', tr: 'Yayınlanmamış' },
37
+ currency: { en: 'Currency', tr: 'Para birimi' },
38
+ price: { en: 'Price (per user, monthly)', tr: 'Fiyat (kullanıcı başı aylık)' },
39
+ paymentFee: { en: 'Payment fee (%)', tr: 'Ödeme komisyonu (%)' },
40
+ fixedCost: { en: 'Fixed monthly cost', tr: 'Sabit aylık maliyet' },
41
+ variableCost: { en: 'Variable cost per user (monthly)', tr: 'Kullanıcı başı değişken maliyet (aylık)' },
42
+ targetUsers: { en: 'Target active users', tr: 'Hedef aktif kullanıcı' },
43
+ freeShare: { en: 'Free user share (%)', tr: 'Ücretsiz kullanıcı oranı (%)' },
44
+ accounts: { en: 'Accounts', tr: 'Hesaplar' },
45
+ inbox: { en: 'Inbox', tr: 'Gelen kutusu' },
46
+ themes: { en: 'Themes', tr: 'Temalar' },
47
+ reports: { en: 'Reports', tr: 'Raporlar' },
48
+ alarms: { en: 'Alarms', tr: 'Alarmlar' },
49
+ chosenDirection: { en: 'Chosen direction', tr: 'Seçilen yön' },
50
+ valueProposition: { en: 'Value proposition', tr: 'Değer önerisi' },
51
+ criticalFlows: { en: 'Critical flows', tr: 'Kritik akışlar' },
52
+ learnedRules: { en: 'Learned rules', tr: 'Öğrenilen kurallar' },
53
+ rule: { en: 'Rule', tr: 'Kural' },
54
+ environments: { en: 'Environments (dev / prod)', tr: 'Ortamlar (dev / prod)' },
55
+ };
56
+
57
+ // Values hooks read at known label or column positions. Readers compare with isValue(); never normalized in place.
58
+ export const VALUES = {
59
+ on: { en: 'on', tr: 'açık' },
60
+ off: { en: 'off', tr: 'kapalı' },
61
+ none: { en: 'none', tr: 'yok' },
62
+ setup: { en: 'Setup', tr: 'Başlangıç' },
63
+ notSetUp: { en: 'not set up', tr: 'kurulmadı' },
64
+ setUp: { en: 'set up', tr: 'kuruldu' },
65
+ full: { en: 'Full', tr: 'Tam' },
66
+ light: { en: 'Light', tr: 'Hafif' },
67
+ standard: { en: 'Standard', tr: 'Standart' },
68
+ waiting: { en: 'waiting', tr: 'bekliyor' },
69
+ automatic: { en: 'automatic', tr: 'otomatik' },
70
+ user: { en: 'user', tr: 'kullanıcı' },
71
+ oneWay: { en: 'one-way', tr: 'tek' },
72
+ twoWay: { en: 'two-way', tr: 'çift' },
73
+ yes: { en: 'yes', tr: 'evet' },
74
+ no: { en: 'no', tr: 'hayır' },
75
+ right: { en: 'right', tr: 'doğru' },
76
+ wrong: { en: 'wrong', tr: 'yanlış' },
77
+ passed: { en: 'passed', tr: 'geçti' },
78
+ failed: { en: 'failed', tr: 'kaldı' },
79
+ notRun: { en: 'not run', tr: 'koşulmadı' },
80
+ bug: { en: 'bug', tr: 'hata' },
81
+ feature: { en: 'feature', tr: 'özellik' },
82
+ request: { en: 'request', tr: 'istek' },
83
+ complaint: { en: 'complaint', tr: 'şikayet' },
84
+ praise: { en: 'praise', tr: 'övgü' },
85
+ question: { en: 'question', tr: 'soru' },
86
+ new: { en: 'new', tr: 'yeni' },
87
+ planned: { en: 'planned', tr: 'planlandı' },
88
+ done: { en: 'done', tr: 'yapıldı' },
89
+ rejected: { en: 'rejected', tr: 'reddedildi' },
90
+ merged: { en: 'merged', tr: 'birleşti' },
91
+ };
92
+
93
+ // The value in either language, case-insensitive (Turkish-aware), whole or followed by a non-letter:
94
+ // "açık · kapalı konular: yok" is on, "açıklama" is not.
95
+ export function isValue(text, key) {
96
+ if (!Object.hasOwn(VALUES, key)) throw new Error(`unknown value: ${key}`);
97
+ if (typeof text !== 'string') return false;
98
+ // Turkish lowercase for "AÇIK", plain lowercase for "FAILED" (Turkish would give "faıled")
99
+ const lower = (x) => [x.toLocaleLowerCase('tr'), x.toLowerCase()];
100
+ const texts = lower(text.trim());
101
+ return Object.values(VALUES[key]).flatMap(lower).some((w) =>
102
+ texts.some((s) => s.startsWith(w) && !/^[\p{L}\p{N}]/u.test(s.slice(w.length))));
103
+ }
104
+
105
+ export function label(key) {
106
+ if (!Object.hasOwn(LABELS, key)) throw new Error(`unknown label: ${key}`);
107
+ return LABELS[key].en;
108
+ }
109
+
110
+ const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
111
+ const COMMAND_KEYS = ['dev', 'preview', 'typeCheck'];
112
+ // Longest first so a shorter label never wins over a longer one that starts the same way.
113
+ const PAIRS = Object.entries(LABELS).filter(([k]) => !COMMAND_KEYS.includes(k)).map(([, v]) => v)
114
+ .sort((a, b) => b.tr.length - a.tr.length);
115
+ const BOLD = PAIRS.map(({ tr, en }) => [new RegExp(`^\\*\\*${esc(tr)}:\\*\\*`), `**${en}:**`]);
116
+ // The label must end at a non-letter: "## Faz 1" changes, "## Fazla mesai" does not. A PRD heading may be numbered
117
+ // ("## 6. Kapsam dışı"), the CHANGELOG one is bracketed ("## [Yayınlanmamış]").
118
+ const HEADING = [
119
+ [new RegExp(`^## \\[${esc(LABELS.unreleased.tr)}\\]`), `## [${LABELS.unreleased.en}]`],
120
+ ...PAIRS.map(({ tr, en }) => [new RegExp(`^## (\\d+\\. )?${esc(tr)}(?![\\p{L}\\p{N}])`, 'u'), `## $1${en}`]),
121
+ ];
122
+ const COMMAND = COMMAND_KEYS.map((k) => [new RegExp(`^${esc(LABELS[k].tr)}:`), `${LABELS[k].en}:`]);
123
+ const FENCE = /^ {0,3}(`{3,}|~{3,})/;
124
+ const COMMANDS_HEADING = /^## (Komutlar|Commands)(?![\p{L}\p{N}])/u;
125
+
126
+ const swap = (line, rules) => {
127
+ for (const [re, to] of rules) if (re.test(line)) return line.replace(re, to);
128
+ return line;
129
+ };
130
+
131
+ // Rewrites Turkish labels to English only at label positions: a line-start **Label:**, a "## Label" heading,
132
+ // and the keys of the AGENTS.md Commands block. Prose, tables, other code blocks and values stay byte-identical.
133
+ export function normalize(text) {
134
+ const bom = text.startsWith('') ? '' : '';
135
+ const parts = text.slice(bom.length).split(/(\r?\n)/);
136
+ let fence = false;
137
+ let commands = false;
138
+ for (let i = 0; i < parts.length; i += 2) {
139
+ const line = parts[i];
140
+ if (FENCE.test(line)) { fence = !fence; continue; }
141
+ if (!fence && /^#{1,2} /.test(line)) commands = COMMANDS_HEADING.test(line);
142
+ if (commands) parts[i] = swap(line, [...COMMAND, ...HEADING]);
143
+ else if (!fence) parts[i] = swap(line, [...BOLD, ...HEADING]);
144
+ }
145
+ return bom + parts.join('');
146
+ }
147
+
148
+ // The label a writer uses for key in this file: the language the file already has for it (English if both, or neither).
149
+ // Same positions as normalize: a line-start **Label:** or a "## Label" heading outside code blocks.
150
+ export function labelIn(text, key) {
151
+ const { en, tr } = LABELS[key] ?? {};
152
+ if (!Object.hasOwn(LABELS, key)) throw new Error(`unknown label: ${key}`);
153
+ const at = (word) => new RegExp(`^(\\*\\*${esc(word)}:\\*\\*|## (\\d+\\. )?\\[?${esc(word)}(?![\\p{L}\\p{N}]))`, 'u');
154
+ const [isEn, isTr] = [at(en), at(tr)];
155
+ let fence = false, found = en;
156
+ for (const line of text.replace(/^/, '').split(/\r?\n/)) {
157
+ if (FENCE.test(line)) { fence = !fence; continue; }
158
+ if (fence) continue;
159
+ if (isEn.test(line)) return en;
160
+ if (isTr.test(line)) found = tr;
161
+ }
162
+ return found;
163
+ }
164
+
165
+ const VERDICTS = [['KOŞULLU ONAY', 'conditional'], ['CONDITIONAL', 'conditional'], ['KOŞULLU', 'conditional'],
166
+ ['APPROVE', 'approve'], ['ONAY', 'approve'], ['REJECT', 'reject'], ['RET', 'reject']];
167
+
168
+ // Board result in either language. Fail-closed: anything that is not one clean verdict is 'unknown'.
169
+ export function verdict(text) {
170
+ if (typeof text !== 'string') return 'unknown';
171
+ // "**Result:** RET", "**RET**"; after the word only the end, a dash, a bracket or ": , ; ." ("RET: güvenlik")
172
+ const s = text.trim().replace(/^\*\*[^*]+:\*\*\s*/, '').replace(/^\*\*([^*]+)\*\*/, '$1').toUpperCase();
173
+ for (const [word, result] of VERDICTS) {
174
+ if (s.startsWith(word) && /^(?:\s*(?:$|[—–(-])|[:,;.])/.test(s.slice(word.length))) return result;
175
+ }
176
+ return 'unknown';
177
+ }
@@ -4,8 +4,8 @@ export const MESSAGES = {
4
4
  'session.begin': { tr: 'Başlamak için: /start [proje fikrin] · var olan projede: /adopt', en: 'To begin: /start [your project idea] · existing project: /adopt' },
5
5
  'session.resume': { tr: 'Devam: {command} (adım {step})', en: 'Continue: {command} (step {step})' },
6
6
  'session.next': { tr: 'Sıradaki: /build {code} · {title}', en: 'Next: /build {code} · {title}' },
7
- 'session.handoff': { tr: 'Devir notu var: /handoff devral', en: 'Handoff note waiting: /handoff devral' },
8
- 'session.handoffThen': { tr: 'Devir notu var: /handoff devral · sonra {next}', en: 'Handoff note waiting: /handoff devral · then {next}' },
7
+ 'session.handoff': { tr: 'Devir notu var: /handoff take', en: 'Handoff note waiting: /handoff take' },
8
+ 'session.handoffThen': { tr: 'Devir notu var: /handoff take · sonra {next}', en: 'Handoff note waiting: /handoff take · then {next}' },
9
9
  'dashboard.title': { tr: 'Kontrol paneli', en: 'Dashboard' },
10
10
  'dashboard.sevCritical': { tr: 'Kritik', en: 'Critical' },
11
11
  'dashboard.sevWarn': { tr: 'Dikkat', en: 'Attention' },
@@ -42,7 +42,8 @@ export const MESSAGES = {
42
42
  'dashboard.kpiDecisions': { tr: 'Kararlar', en: 'Decisions' },
43
43
  'dashboard.usersAuto': { tr: '{user} kullanıcı · {auto} otomatik', en: '{user} user · {auto} auto' },
44
44
  'dashboard.board': { tr: 'Kurul', en: 'Board' },
45
- 'dashboard.rejects': { tr: '{count} RET', en: '{count} RET' },
45
+ 'dashboard.rejects': { tr: '{count} ret', en: '{count} rejected' },
46
+ 'dashboard.kpiRejects': { tr: 'Ret', en: 'Rejected' },
46
47
  'dashboard.membersCount': { tr: '{count} üye', en: '{count} members' },
47
48
  'dashboard.kpiCommits7': { tr: 'Commit · 7 gün', en: 'Commits · 7 days' },
48
49
  'dashboard.filesWaiting': { tr: '{count} dosya bekliyor', en: '{count} files waiting' },
@@ -122,7 +123,7 @@ export const MESSAGES = {
122
123
  'dashboard.kpiPassing': { tr: 'Geçen', en: 'Passing' },
123
124
  'dashboard.reportTime': { tr: 'rapor: {when}', en: 'report: {when}' },
124
125
  'dashboard.noJunit': { tr: 'JUnit raporu yok', en: 'No JUnit report' },
125
- 'dashboard.noAcceptanceIds': { tr: 'PRD\'de kimlikli kabul kriteri yok (`Ö-01.1` biçimi).', en: 'The PRD has no acceptance criteria with an id (`Ö-01.1` format).' },
126
+ 'dashboard.noAcceptanceIds': { tr: 'PRD\'de kimlikli kabul kriteri yok (`AC-01.1` biçimi).', en: 'The PRD has no acceptance criteria with an id (`AC-01.1` format).' },
126
127
  'dashboard.metricsMonthly': { tr: 'Metrikler · aylık', en: 'Metrics · monthly' },
127
128
  'dashboard.noMetrics': { tr: 'Henüz metrik kaydı yok. `/maintain monthly` ve `/retro` ekler.', en: 'No metrics recorded yet. `/maintain monthly` and `/retro` add them.' },
128
129
  'dashboard.criticalFlowTests': { tr: 'Kritik akış testleri', en: 'Critical flow tests' },
@@ -336,7 +337,7 @@ export const MESSAGES = {
336
337
  'dashboard.act.commitOrWrapUp': { tr: 'Commit veya /wrap-up', en: 'Commit or /wrap-up' },
337
338
  'dashboard.alert.progressStale': { tr: 'docs/workflow/progress.md {count} commit\'tir güncellenmedi.', en: 'docs/workflow/progress.md has not been updated for {count} commits.' },
338
339
  'dashboard.alert.closedTopics': { tr: 'Soru modu kapalı konular: {topics}. Bu konulardaki kararlar otomatik veriliyor.', en: 'Question mode closed topics: {topics}. Decisions on these topics are made automatically.' },
339
- 'dashboard.act.askModeOpen': { tr: '/ask-mode ac <konu>', en: '/ask-mode ac <topic>' },
340
+ 'dashboard.act.askModeOpen': { tr: '/ask-mode on <konu>', en: '/ask-mode on <topic>' },
340
341
  'dashboard.alert.boardNotSetUp': { tr: 'Uzman kurulu kurulmadı.', en: 'The expert board is not set up.' },
341
342
  'dashboard.alert.liveMonitoring': { tr: 'Canlı izleme: {title}', en: 'Live monitoring: {title}' },
342
343
  'dashboard.act.fixLive': { tr: '/fix-bug canlı izleme #{number}', en: '/fix-bug live monitoring #{number}' },
@@ -372,7 +373,7 @@ export const MESSAGES = {
372
373
  'dashboard.act.wrapUpClear': { tr: '/wrap-up ve /clear', en: '/wrap-up and /clear' },
373
374
  'dashboard.alert.findingsCritical': { tr: '{kind} bulgu raporunda {critical} kritik bulgu var (sağlık {health}/100).', en: 'The {kind} finding report has {critical} critical findings (health {health}/100).' },
374
375
  'dashboardData.prodRefInMcp': { tr: 'Prod projesinin ref\'i ({ref}) .mcp.json\'da: AI prod\'a bağlanabiliyor.', en: 'The prod project\'s ref ({ref}) is in .mcp.json: the AI can connect to prod.' },
375
- 'dashboardData.prodRefAction': { tr: '.mcp.json → dev ref (docs/operations/accounts.md → Ortamlar)', en: '.mcp.json → dev ref (docs/operations/accounts.md → Ortamlar)' },
376
+ 'dashboardData.prodRefAction': { tr: '.mcp.json → dev ref (docs/operations/accounts.md → Environments)', en: '.mcp.json → dev ref (docs/operations/accounts.md → Environments)' },
376
377
  'dashboardData.rlsMissing': { tr: 'Supabase kullanılıyor ama RLS saldırgan testi yok (tests/security/rls.mjs).', en: 'Supabase is in use but there is no RLS attacker test (tests/security/rls.mjs).' },
377
378
  'dashboardData.rlsAction': { tr: '/kit-update ile şablonu al, tabloları TABLES\'a ekle', en: 'Get the template with /kit-update, add the tables to TABLES' },
378
379
  'guard.commit.skipAsk': { tr: 'Commit kapısı: --skip kurul denetimini ve deterministik kapıyı aşar. Sadece kullanıcı açıkça isterse onayla.', en: 'Commit gate: --skip bypasses the board review and the deterministic gate. Approve only if the user explicitly asks.' },
@@ -398,7 +399,7 @@ export const MESSAGES = {
398
399
  'guard.destructive.perm.planSave': { tr: 'Kapsam kalkanının dosya listesi yazılıyor', en: 'The scope shield\'s file list is being written' },
399
400
  'guard.destructive.perm.planEnd': { tr: 'Kapsam kalkanı kapatılıyor', en: 'The scope shield is being closed' },
400
401
  'guard.destructive.perm.commitSkip': { tr: 'Commit kapısı ve kurul denetimi atlanıyor', en: 'The commit gate and the board review are being skipped' },
401
- 'guard.destructive.perm.prefsEdit': { tr: 'Kişisel tercih dosyası (~/.vibe-kit/tercihler.md) değişiyor', en: 'The personal preferences file (~/.vibe-kit/tercihler.md) is changing' },
402
+ 'guard.destructive.perm.prefsEdit': { tr: 'Kişisel tercih dosyası (~/.vibe-kit/preferences.md) değişiyor', en: 'The personal preferences file (~/.vibe-kit/preferences.md) is changing' },
402
403
  'guard.destructive.perm.gateApprove': { tr: 'Deterministik kapının çalıştıracağı komutlar onaylanıyor', en: 'The commands the deterministic gate will run are being approved' },
403
404
  'guard.destructive.perm.protectedShell': { tr: 'Kural veya koruma dosyası kabuk komutuyla değiştiriliyor', en: 'A rule or guard file is being changed with a shell command' },
404
405
  'guard.destructive.unclearNetPath': { tr: 'hedef ağ yolu ({path})', en: 'target is a network path ({path})' },
@@ -8,6 +8,7 @@ import path from 'node:path';
8
8
  import { spawn } from 'node:child_process';
9
9
  import { findRoot, isMain } from './shared.mjs';
10
10
  import { t } from './lang.mjs';
11
+ import { PLACEHOLDER_NAMES } from './status-read.mjs';
11
12
 
12
13
  export const THRESHOLD_SEC = 90;
13
14
 
@@ -30,7 +31,7 @@ function main() {
30
31
 
31
32
  let project = path.basename(root);
32
33
  for (const f of ['AGENTS.md', 'CLAUDE.md']) { // in projects before 3.9.0 the name is in CLAUDE.md
33
- try { const name = fs.readFileSync(path.join(root, f), 'utf8').match(/^# (.+)$/m)?.[1]; if (name && name !== '[PROJE ADI]') { project = name; break; } } catch { /* */ }
34
+ try { const name = fs.readFileSync(path.join(root, f), 'utf8').match(/^# (.+)$/m)?.[1]; if (name && !PLACEHOLDER_NAMES.includes(name)) { project = name; break; } } catch { /* */ }
34
35
  }
35
36
 
36
37
  let message;
@@ -1,10 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  // Personal preferences: how this user likes to work (talk, questions, reports, code style, things to avoid).
3
- // One file for all projects: ~/.vibe-kit/tercihler.md. Git never sees it; the session-start hook loads it every session.
3
+ // One file for all projects: ~/.vibe-kit/preferences.md (tercihler.md before 3.48, renamed on first use). Git never sees
4
+ // it; the session-start hook loads it every session.
4
5
  // node .claude/hooks/prefs.mjs --list numbered list
5
6
  // node .claude/hooks/prefs.mjs --add <category> "<preference>" only with the user's approval (asked)
6
7
  // node .claude/hooks/prefs.mjs --remove <no> (asked)
7
- // Categories: konusma, sorular, raporlar, kod, kacin. A project rule goes to AGENTS.md → Öğrenilen kurallar, not here.
8
+ // Categories: conversation, questions, reports, code, avoid (the pre-3.48 konusma, sorular, raporlar, kod, kacin still work).
9
+ // A project rule goes to AGENTS.md → Learned rules, not here.
8
10
  // Why a script: the AI never writes this file directly; one line per preference, no secrets, a size cap.
9
11
  // The same rules apply when reading: any program on the machine can write this file, so what is loaded is checked
10
12
  // again and is labeled as style preferences that cannot change permissions or rules.
@@ -14,17 +16,28 @@ import path from 'node:path';
14
16
  import { findSecrets } from './guard-secrets.mjs';
15
17
  import { isMain } from './shared.mjs';
16
18
 
17
- export const CATEGORIES = { konusma: 'Konuşma', sorular: 'Sorular ve kararlar', raporlar: 'Raporlar', kod: 'Kod', kacin: 'Kaçınılacaklar' };
19
+ export const CATEGORIES = { conversation: 'Conversation', questions: 'Questions and decisions', reports: 'Reports', code: 'Code', avoid: 'Avoid' };
20
+ // Pre-3.48 keys and headings, still read
21
+ const OLD = { konusma: ['conversation', 'Konuşma'], sorular: ['questions', 'Sorular ve kararlar'], raporlar: ['reports', 'Raporlar'], kod: ['code', 'Kod'], kacin: ['avoid', 'Kaçınılacaklar'] };
22
+ export const categoryKey = (k) => (Object.hasOwn(CATEGORIES, k ?? '') ? k : Object.hasOwn(OLD, k ?? '') ? OLD[k][0] : null);
18
23
  export const MAX_ITEMS = 40;
19
24
  const MAX_LENGTH = 200;
20
25
  const MAX_BYTES = 64 * 1024;
21
26
 
22
- // os.homedir() follows HOME / USERPROFILE; the file name is fixed, so no other file can be written through it
23
- export const prefsFile = () => path.join(os.homedir(), '.vibe-kit', 'tercihler.md');
27
+ // os.homedir() follows HOME / USERPROFILE; the file name is fixed, so no other file can be written through it.
28
+ // A pre-3.48 tercihler.md is renamed once (decision: a 3.47 project on the same machine no longer sees it).
29
+ export const prefsFile = () => {
30
+ const file = path.join(os.homedir(), '.vibe-kit', 'preferences.md');
31
+ const old = path.join(os.homedir(), '.vibe-kit', 'tercihler.md');
32
+ if (fs.existsSync(file) || !fs.existsSync(old)) return file;
33
+ try { fs.renameSync(old, file); return file; } catch { return old; }
34
+ };
24
35
 
25
36
  // A heading is matched by key or title, ignoring case and Turkish letters (## kod, ## Kacinilacaklar)
26
37
  const fold = (s) => s.toLocaleLowerCase('tr').normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/ı/g, 'i').trim();
27
- const categoryOf = (heading) => Object.keys(CATEGORIES).find((k) => fold(k) === fold(heading) || fold(CATEGORIES[k]) === fold(heading)) ?? null;
38
+ const categoryOf = (heading) => Object.keys(CATEGORIES).find((k) => fold(k) === fold(heading) || fold(CATEGORIES[k]) === fold(heading))
39
+ ?? Object.values(OLD).find(([, title]) => fold(title) === fold(heading))?.[0]
40
+ ?? categoryKey(Object.keys(OLD).find((k) => fold(k) === fold(heading)));
28
41
  const clean = (s) => String(s ?? '').replace(/[\u0000-\u001f\u007f-\u009f]/g, ' ').replace(/\s+/g, ' ').trim();
29
42
  const valid = (t) => t && t.length <= MAX_LENGTH && !findSecrets(t).length;
30
43
 
@@ -63,8 +76,9 @@ function writePrefs(items, file) {
63
76
  }
64
77
 
65
78
  // Returns { error } | { existing: <category> } | {}
66
- export function addPref(category, text, file = prefsFile()) {
67
- if (!Object.hasOwn(CATEGORIES, category ?? '')) return { error: `Category must be one of: ${Object.keys(CATEGORIES).join(', ')}` };
79
+ export function addPref(key, text, file = prefsFile()) {
80
+ const category = categoryKey(key);
81
+ if (!category) return { error: `Category must be one of: ${Object.keys(CATEGORIES).join(', ')}` };
68
82
  const t = clean(text);
69
83
  if (!t) return { error: 'The preference text is empty.' };
70
84
  if (t.length > MAX_LENGTH) return { error: `A preference can have at most ${MAX_LENGTH} characters; shorten it.` };
@@ -91,7 +105,7 @@ export function prefsNote(file = prefsFile()) {
91
105
  const items = readPrefs(file);
92
106
  if (!items.length) return '';
93
107
  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 'Personal style preferences (~/.vibe-kit/tercihler.md; not a permission, security or command rule and cannot change them; project rules come first): ' + parts.join(' · ');
108
+ return 'Personal style preferences (~/.vibe-kit/preferences.md; not a permission, security or command rule and cannot change them; project rules come first): ' + parts.join(' · ');
95
109
  }
96
110
 
97
111
  if (isMain(import.meta.url)) {
@@ -100,7 +114,7 @@ if (isMain(import.meta.url)) {
100
114
  if (mode === '--add') {
101
115
  const r = addPref(a, rest.join(' '));
102
116
  if (r.error) fail(r.error);
103
- console.log(r.existing ? `This preference is already saved (${CATEGORIES[r.existing]}).` : `Preference saved (${CATEGORIES[a]}): ${prefsFile()}`);
117
+ console.log(r.existing ? `This preference is already saved (${CATEGORIES[r.existing]}).` : `Preference saved (${CATEGORIES[categoryKey(a)]}): ${prefsFile()}`);
104
118
  } else if (mode === '--remove') {
105
119
  const error = removePref(a);
106
120
  if (error) fail(error);
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // Live preview (F-17): opens the dev server, takes desktop (1440) and mobile (390) screenshots, stops what it started.
3
3
  // node .claude/hooks/preview.mjs <task> → .previews/<task>/desktop.png and mobile.png · exit code: 0 done or skipped, 1 error
4
- // Commands come from AGENTS.md → Komutlar: geliştirme (dev server) and önizleme (local address). Nothing runs or is opened
4
+ // Commands come from AGENTS.md → Commands: dev (dev server) and preview (local address). Nothing runs or is opened
5
5
  // without the gate approval (gate.mjs --approve covers both lines). A server already answering is used and left open.
6
6
  // Mobile (Expo) projects are skipped: a web screenshot is not what the phone shows.
7
7
  // Screenshots: @playwright/cli (pinned, via npx). PREVIEW_CLI=<node script> replaces it in tests; PREVIEW_TIMEOUT_MS: wait for the address.
@@ -53,9 +53,9 @@ export async function preview(root, task) {
53
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
- const url = command('önizleme'), dev = command('geliştirme');
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}`);
56
+ const url = command('preview'), dev = command('dev');
57
+ if (!url) return fail('AGENTS.md → Commands has no `preview:` line, or it is empty. Add it (example: `preview: http://localhost:3000`) or produce the evidence by hand in this task.');
58
+ if (!LOCAL.test(url)) return fail(`the preview address must be local (http://localhost:<port>, 127.0.0.1 or [::1]). Now: ${url}`);
59
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
@@ -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} is not up and AGENTS.md → Komutlar has no \`geliştirme:\` command.`);
80
+ if (!dev) return fail(`${url} is not up and AGENTS.md → Commands has no \`dev:\` 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,7 +90,7 @@ 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} 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())}`
93
+ ? `${url} did not come up within ${Math.round(wait / 1000)} s. Are the address and port the same as \`preview:\` in AGENTS.md → Commands?${block(devLog())}`
94
94
  : `the dev server exited before it came up (${child.exitCode ?? child.signalCode}): ${dev}${block(devLog())}`);
95
95
  }
96
96
  }
@@ -1,27 +1,34 @@
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 "<job>" <K|O|B|BOARD|DESIGN|FINDINGS> <start %> <end %> <min> ["<note>"]
4
+ // node .claude/hooks/progress.mjs --cost "<job>" <S|M|L|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
6
  // node .claude/hooks/progress.mjs --cost "<job>" <size> ["<note>"] [--limit N] start from --start, end now (once)
7
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
- // Every progress write also sets **Son güncelleme:** to today. Flags can be combined in one call.
9
+ // Every progress write also sets **Last updated:** (**Son güncelleme:** in a Turkish file) to today. Flags can be combined in one call.
10
10
  import fs from 'node:fs';
11
11
  import path from 'node:path';
12
12
  import { findRoot, isMain } from './shared.mjs';
13
13
  import { clean, labels } from './status-read.mjs';
14
+ import { label, labelIn, normalize } from './labels.mjs';
14
15
  import { readLimits } from './usage.mjs';
15
16
 
16
- const LABELS = ['Son güncelleme', 'Aktif faz', 'Aktif görev', 'Başlangıç adımı', 'Soru modu', 'Profil'];
17
- const SIZES = ['K', 'O', 'B', 'BOARD', 'DESIGN', 'FINDINGS'];
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]';
17
+ const REQUIRED = ['lastUpdated', 'activePhase', 'activeTask', 'setupStep', 'questionMode', 'profile'];
18
+ const SIZES = ['S', 'M', 'L', 'BOARD', 'DESIGN', 'FINDINGS'];
19
+ // Sizes before 3.48 (küçük / orta / büyük); still accepted, written as the English one
20
+ export const OLD_SIZES = { K: 'S', O: 'M', B: 'L' };
21
+ const USAGE = 'Usage: node .claude/hooks/progress.mjs [--done G-NNN] [--start] [--cost "<job>" <S|M|L|BOARD|DESIGN|FINDINGS> [<start %> <end %> <min>] ["<note>"]] [--limit N] [--task "<text>"] [--did "<text>"] [--next "<text>"] [--check]';
19
22
  // One line per value: a newline would break a label, a list item or a table row
20
23
  const line = (s) => clean(String(s).replace(/\s*\r?\n\s*/g, ' ')).trim();
21
24
  const today = () => new Date().toLocaleDateString('sv-SE');
22
- const setLabel = (text, k, v) => text.replace(new RegExp(`^(\\*\\*${k}:\\*\\*).*$`, 'm'), `$1 ${v}`);
23
- const setSection = (text, title, body) => text.replace(new RegExp(`^(## ${title}[^\\n]*\\n)[\\s\\S]*?(?=^## |(?![\\s\\S]))`, 'm'), `$1\n${body}\n\n`);
24
- const section = (text, title) => text.match(new RegExp(`^## ${title}[^\\n]*\\n([\\s\\S]*?)(?=^## |(?![\\s\\S]))`, 'm'))?.[1] ?? '';
25
+ // Labels and headings are written in the language the file already uses (labelIn). Function replacements: a value
26
+ // with $& or $' is text, not a replacement pattern.
27
+ const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
28
+ const setLabel = (text, key, v) => text.replace(new RegExp(`^(\\*\\*${esc(labelIn(text, key))}:\\*\\*).*$`, 'm'), (_, l) => `${l} ${v}`);
29
+ const sectionRe = (text, key) => new RegExp(`^(## ${esc(labelIn(text, key))}[^\\n]*\\n)([\\s\\S]*?)(?=^## |(?![\\s\\S]))`, 'm');
30
+ const setSection = (text, key, body) => text.replace(sectionRe(text, key), (_, h) => `${h}\n${body}\n\n`);
31
+ const section = (text, key) => text.match(sectionRe(text, key))?.[2] ?? '';
25
32
 
26
33
  export function markDone(text, id) {
27
34
  const re = new RegExp(`^(\\s*- )\\[( |x)\\](\\s*${id}:)`, 'mi');
@@ -32,7 +39,8 @@ export function markDone(text, id) {
32
39
  }
33
40
 
34
41
  export function costRow(args, date = today()) {
35
- const [job, size, start, end, min, note = ''] = args;
42
+ const [job, typed, start, end, min, note = ''] = args;
43
+ const size = OLD_SIZES[typed] ?? typed;
36
44
  const nums = [start, end, min].map(Number);
37
45
  if (!job || !SIZES.includes(size) || nums.some((n) => !Number.isFinite(n) || n < 0) || args.length > 6) return null;
38
46
  // The end below the start means the 5-hour window renewed in between: the difference is unknown
@@ -42,17 +50,21 @@ export function costRow(args, date = today()) {
42
50
  }
43
51
 
44
52
  export function updateProgress(text, { task, did, next }) {
45
- let t = setLabel(text, 'Son güncelleme', today());
46
- if (task != null) t = setLabel(t, 'Aktif görev', line(task));
53
+ let t = setLabel(text, 'lastUpdated', today());
54
+ if (task != null) t = setLabel(t, 'activeTask', line(task));
47
55
  if (did != null) {
48
- const items = section(t, 'Son oturumda yapılanlar').split('\n').map((s) => s.trim()).filter((s) => /^- \S/.test(s));
49
- t = setSection(t, 'Son oturumda yapılanlar', [...items, `- ${line(did)}`].join('\n'));
56
+ const items = section(t, 'doneLastSession').split('\n').map((s) => s.trim()).filter((s) => /^- \S/.test(s));
57
+ t = setSection(t, 'doneLastSession', [...items, `- ${line(did)}`].join('\n'));
50
58
  }
51
- if (next != null) t = setSection(t, 'Sıradaki adım', `1. ${line(next)}`);
59
+ if (next != null) t = setSection(t, 'nextStep', `1. ${line(next)}`);
52
60
  return t;
53
61
  }
54
62
 
55
- export const missingLabels = (text) => LABELS.filter((k) => !(k in labels(text)));
63
+ // By their English names, whichever language the file uses; an example inside a code block does not count
64
+ export const missingLabels = (text) => {
65
+ const found = labels(normalize(text).replace(/^```[\s\S]*?^```.*$/gm, ''));
66
+ return REQUIRED.map((k) => label(k)).filter((k) => !(k in found));
67
+ };
56
68
 
57
69
  if (isMain(import.meta.url)) {
58
70
  const root = findRoot();
@@ -2,8 +2,8 @@
2
2
  // Browser QA report (F-18): written from the JUnit result of the /qa run, so "it works" needs a green test behind it.
3
3
  // node .claude/hooks/qa-report.mjs [junit file] default: test-results-qa/junit.xml (the /qa run has its own output folder:
4
4
  // Playwright empties its output folder at every run, a shared one would delete the acceptance report and the evidence)
5
- // → docs/quality/qa/<YYYY-MM-DD-HHMMSS>.md: one row per PRD criterion (geçti / kaldı / koşulmadı), the first failure line,
6
- // evidence test-results-qa/shots/<Ö-id>.png copied to .previews/qa-<stamp>/ (kept out of git, outlives the next run).
5
+ // → docs/quality/qa/<YYYY-MM-DD-HHMMSS>.md: one row per PRD criterion (passed / failed / not run), the first failure line,
6
+ // evidence test-results-qa/shots/<AC-id>.png copied to .previews/qa-<stamp>/ (kept out of git, outlives the next run).
7
7
  // Exit code: 0 nothing failed, 1 a criterion failed, 2 nothing to report (no JUnit file, no PRD criterion, no test ran).
8
8
  import fs from 'node:fs';
9
9
  import path from 'node:path';
@@ -27,7 +27,7 @@ export function qaReport(root, junit = 'test-results-qa/junit.xml', now = new Da
27
27
  let prd;
28
28
  try { prd = fs.readFileSync(path.join(root, 'docs/product/prd.md'), 'utf8'); } catch { return none('docs/product/prd.md not found.'); }
29
29
  const criteria = prdCriteria(prd);
30
- if (!criteria.length) return none('the PRD has no acceptance criterion with an id (`- [ ] Ö-01.1: ...`).');
30
+ if (!criteria.length) return none('the PRD has no acceptance criterion with an id (`- [ ] AC-01.1: ...`).');
31
31
  const results = new Map();
32
32
  for (const c of junitCases(xml)) {
33
33
  for (const k of c.keys) {
@@ -42,27 +42,29 @@ export function qaReport(root, junit = 'test-results-qa/junit.xml', now = new Da
42
42
  const rows = criteria.map((k) => {
43
43
  const r = results.get(k.key);
44
44
  let evidence = '';
45
- const shot = path.join(root, path.dirname(junit), 'shots', `${k.id}.png`);
45
+ // The test may name the screenshot in the other id form (PRD "Ö-01.1", test "AC-01.1", or the other way round)
46
+ const names = [k.id, k.id.replace(/^Ö-/, 'AC-'), k.id.replace(/^AC-/, 'Ö-')];
47
+ const shot = [...new Set(names)].map((n) => path.join(root, path.dirname(junit), 'shots', `${n}.png`))
48
+ .find((p) => { try { return fs.lstatSync(p).isFile(); } catch { return false; } });
46
49
  // Only a real screenshot file: a shots/<id>.png link would copy any local file into .previews/
47
- let real = false;
48
- try { real = fs.lstatSync(shot).isFile(); } catch { /* no screenshot */ }
50
+ const real = !!shot;
49
51
  if (r && !linked && real) {
50
52
  fs.mkdirSync(shots, { recursive: true });
51
53
  fs.copyFileSync(shot, path.join(shots, `${k.id}.png`));
52
54
  evidence = `.previews/qa-${stamp}/${k.id}.png`;
53
55
  }
54
- return { criterion: `${k.id} ${k.text}`, result: !r ? 'koşulmadı' : r.failed ? 'kaldı' : 'geçti', evidence, note: r?.failed ? reasonLine(r.reason) : '' };
56
+ return { criterion: `${k.id} ${k.text}`, result: !r ? 'not run' : r.failed ? 'failed' : 'passed', evidence, note: r?.failed ? reasonLine(r.reason) : '' };
55
57
  });
56
58
  const count = (x) => rows.filter((r) => r.result === x).length;
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ı') };
59
+ if (!count('passed') && !count('failed')) return none(`no test result tied to a PRD criterion in ${junit} (the test name must start with the id: \`AC-01.1 ...\`; did the server start?).`);
60
+ const counts = { passed: count('passed'), failed: count('failed'), notRun: count('not run') };
59
61
  const file = `docs/quality/qa/${stamp}.md`;
60
62
  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
63
  `${t(root, 'qaReport.tableHeader')}\n|---|---|---|---|\n${rows.map((r) => `| ${cell(r.criterion)} | ${r.result} | ${r.evidence} | ${cell(r.note)} |`).join('\n')}\n`;
62
64
  fs.mkdirSync(path.join(root, 'docs/quality/qa'), { recursive: true });
63
65
  fs.writeFileSync(path.join(root, file), md);
64
66
  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}` };
67
+ return { code: count('failed') ? 1 : 0, text: `QA: ${counts.passed} passed · ${counts.failed} failed · ${counts.notRun} not run → ${file}${stale}` };
66
68
  }
67
69
 
68
70
  if (isMain(import.meta.url)) {
@@ -10,6 +10,7 @@ import { readLimits, summary } from './usage.mjs';
10
10
  import { detectTool, findRoot, sessionNote } from './shared.mjs';
11
11
  import { prefsNote } from './prefs.mjs';
12
12
  import { readLang, t } from './lang.mjs';
13
+ import { isValue } from './labels.mjs';
13
14
 
14
15
  const notes = [];
15
16
  let next = '';
@@ -30,18 +31,19 @@ try {
30
31
  next = t(root, 'session.begin');
31
32
  } else {
32
33
  const a = d.fields;
33
- notes.push(`Phase: ${a['Aktif faz'] ?? '-'} · Task: ${a['Aktif görev'] ?? '-'} · Question mode: ${a['Soru modu'] ?? 'açık'} · Board: ${d.board.state}`);
34
- if (/başlangıç/i.test(a['Aktif faz'] ?? '') && d.installed) {
35
- const command = /uyum/i.test(a['Aktif faz']) ? '/adopt' : '/start';
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ı'] });
34
+ notes.push(`Phase: ${a['Active phase'] ?? '-'} · Task: ${a['Active task'] ?? '-'} · Question mode: ${a['Question mode'] ?? 'on'} · Board: ${d.board.state}`);
35
+ if (isValue(a['Active phase'], 'setup') && d.installed) {
36
+ // "Setup (adopt)", before 3.48 "Başlangıç (uyum)"
37
+ const command = /\((adopt|uyum)\)/i.test(a['Active phase']) ? '/adopt' : '/start';
38
+ notes.push(`${command} was left unfinished (step ${a['Setup step']}). Suggest the user continue with ${command} where they left off.`);
39
+ next = t(root, 'session.resume', { command, step: a['Setup step'] });
38
40
  } else {
39
41
  const task = d.phases.flatMap((f) => f.tasks).find((x) => !x.done);
40
42
  const code = task?.text.match(/^(G-\d+)/)?.[1];
41
43
  if (code) next = t(root, 'session.next', { code, title: task.text.replace(/^G-\d+:\s*/, '').split(' — ')[0].replace(/\*\*/g, '').slice(0, 70) });
42
44
  }
43
45
  if (d.handoffPending) {
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.');
46
+ notes.push('Handoff note waiting (docs/workflow/handoff.md): the project is being taken over from another person or account. Suggest /handoff take before starting work.');
45
47
  next = next ? t(root, 'session.handoffThen', { next }) : t(root, 'session.handoff');
46
48
  }
47
49
  if (next) notes.push(next);
@@ -72,7 +74,7 @@ try {
72
74
  // not a git repo, or before the first commit
73
75
  }
74
76
 
75
- // The user's preferences from every project (~/.vibe-kit/tercihler.md), short; a broken file must not stop the session
77
+ // The user's preferences from every project (~/.vibe-kit/preferences.md), short; a broken file must not stop the session
76
78
  try { const p = prefsNote(); if (p) notes.push(p.length > 1500 ? `${p.slice(0, 1500)}…` : p); } catch { /* no preferences */ }
77
79
 
78
80
  if (notes.length) console.log(sessionNote(tool, '[Kit] ' + notes.join('\n[Kit] '), next && `[Kit] ${next}`));