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.
- package/.claude/agents/board-chair.md +9 -9
- package/.claude/agents/code-reviewer.md +2 -2
- package/.claude/agents/council-advisor.md +2 -2
- package/.claude/agents/security-auditor.md +6 -6
- package/.claude/agents/test-writer.md +1 -1
- package/.claude/board/board-ai.md +6 -6
- package/.claude/board/board-api.md +6 -6
- package/.claude/board/board-architect.md +7 -7
- package/.claude/board/board-devops.md +6 -6
- package/.claude/board/board-mobile.md +6 -6
- package/.claude/board/board-performance.md +6 -6
- package/.claude/board/board-privacy.md +6 -6
- package/.claude/board/board-product.md +7 -7
- package/.claude/board/board-qa.md +7 -7
- package/.claude/board/board-ux.md +6 -6
- package/.claude/board/council.md +32 -32
- package/.claude/board/rules.md +8 -8
- package/.claude/commands/adopt.md +10 -10
- package/.claude/commands/ask-mode.md +6 -5
- package/.claude/commands/board.md +1 -1
- package/.claude/commands/build.md +8 -8
- package/.claude/commands/design.md +8 -8
- package/.claude/commands/findings-all.md +2 -2
- package/.claude/commands/findings-security.md +1 -1
- package/.claude/commands/fix-bug.md +2 -2
- package/.claude/commands/handoff.md +12 -12
- package/.claude/commands/language.md +1 -1
- package/.claude/commands/maintain.md +2 -2
- package/.claude/commands/plan-task.md +3 -3
- package/.claude/commands/qa.md +5 -5
- package/.claude/commands/release.md +3 -3
- package/.claude/commands/research.md +2 -2
- package/.claude/commands/retro.md +2 -2
- package/.claude/commands/review-changes.md +2 -2
- package/.claude/commands/rollback.md +1 -1
- package/.claude/commands/start.md +20 -20
- package/.claude/commands/tools.md +8 -6
- package/.claude/commands/user-feedback.md +3 -3
- package/.claude/commands/wrap-up.md +3 -3
- package/.claude/findings/protocol.md +2 -2
- package/.claude/findings/report-template.md +1 -1
- package/.claude/hooks/commit-gate.mjs +11 -9
- package/.claude/hooks/council.mjs +51 -24
- package/.claude/hooks/dashboard-data.mjs +73 -53
- package/.claude/hooks/dashboard.mjs +80 -71
- package/.claude/hooks/destructive-guard.mjs +1 -1
- package/.claude/hooks/freeze-guard.mjs +1 -1
- package/.claude/hooks/gate.mjs +14 -11
- package/.claude/hooks/labels.mjs +177 -0
- package/.claude/hooks/messages.mjs +8 -7
- package/.claude/hooks/notify.mjs +2 -1
- package/.claude/hooks/prefs.mjs +24 -10
- package/.claude/hooks/preview.mjs +6 -6
- package/.claude/hooks/progress.mjs +27 -15
- package/.claude/hooks/qa-report.mjs +12 -10
- package/.claude/hooks/session-start.mjs +9 -7
- package/.claude/hooks/status-read.mjs +22 -18
- package/.claude/hooks/statusline.mjs +10 -8
- package/.claude/hooks/tools.mjs +13 -8
- package/.claude/output-styles/{anlatici.md → narrator.md} +2 -2
- package/.claude/skills/logo-design/SKILL.md +2 -2
- package/.claude/skills/source-research/SKILL.md +1 -1
- package/.claude/skills/ui-motion/SKILL.md +2 -2
- package/AGENTS.md +12 -12
- package/CLAUDE.md +1 -1
- package/README.md +2 -2
- package/README.tr.md +3 -3
- package/docs/decisions/adr-template.md +1 -1
- package/docs/decisions/log.md +2 -2
- package/docs/findings/README.md +1 -1
- package/docs/operations/accounts.md +7 -7
- package/docs/operations/deploy-setup.md +3 -3
- package/docs/operations/feature-flags.md +2 -2
- package/docs/operations/maintenance.md +1 -1
- package/docs/operations/release-checklist.md +2 -2
- package/docs/operations/spend-alarms.md +2 -2
- package/docs/product/feedback.md +4 -4
- package/docs/product/launch.md +1 -1
- package/docs/product/prd.md +10 -10
- package/docs/product/roadmap.md +8 -8
- package/docs/product/specs/_template.md +8 -8
- package/docs/product/unit-economics.md +10 -10
- package/docs/quality/metrics.md +2 -2
- package/docs/quality/test-plan.md +5 -5
- package/docs/security/checklist.md +2 -2
- package/docs/workflow/board.md +5 -5
- package/docs/workflow/cost-log.md +1 -1
- package/docs/workflow/decision-protocol.md +18 -16
- package/docs/workflow/learning.md +2 -2
- package/docs/workflow/mcp-catalog.md +1 -1
- package/docs/workflow/models.md +1 -1
- package/docs/workflow/progress.md +14 -14
- package/docs/workflow/skills.md +9 -9
- package/docs/workflow/usage-limits.md +5 -5
- package/kit/CHANGELOG.md +28 -0
- package/kit/VERSION +1 -1
- package/kit/guide.html +21 -21
- package/kit/guide.tr.html +21 -21
- package/kit/kit.mjs +29 -0
- package/kit/project/README.md +1 -1
- package/kit/templates/addons/ai/ai/evals/README.md +5 -5
- package/kit/templates/addons/ai/ai/evals/cases.jsonl +3 -3
- package/kit/templates/addons/ai/docs/ai/guide.md +1 -1
- package/kit/templates/backend/.github/workflows/ci.yml +4 -4
- package/kit/templates/backend/.github/workflows/claude-ci-fix.yml +10 -10
- package/kit/templates/backend/.github/workflows/claude-maintenance.yml +1 -1
- package/kit/templates/backend/.github/workflows/live-monitoring.yml +1 -1
- package/kit/templates/backend/.pre-commit-config.yaml +1 -1
- package/kit/templates/backend/docs/architecture/stack.md +7 -7
- package/kit/templates/backend/docs/architecture/structure.md +2 -2
- package/kit/templates/mobile/.github/workflows/ci.yml +4 -4
- package/kit/templates/mobile/.github/workflows/claude-ci-fix.yml +10 -10
- package/kit/templates/mobile/.github/workflows/claude-maintenance.yml +1 -1
- package/kit/templates/mobile/docs/architecture/stack.md +5 -5
- package/kit/templates/mobile/docs/architecture/structure.md +1 -1
- package/kit/templates/mobile/docs/design/design-guide.md +1 -1
- package/kit/templates/mobile/tests/security/rls.mjs +4 -4
- package/kit/templates/web-saas/.github/workflows/ci.yml +4 -4
- package/kit/templates/web-saas/.github/workflows/claude-ci-fix.yml +10 -10
- package/kit/templates/web-saas/.github/workflows/claude-maintenance.yml +1 -1
- package/kit/templates/web-saas/.github/workflows/live-monitoring.yml +1 -1
- package/kit/templates/web-saas/docs/architecture/stack.md +12 -12
- package/kit/templates/web-saas/docs/architecture/structure.md +2 -2
- package/kit/templates/web-saas/docs/design/design-guide.md +1 -1
- package/kit/templates/web-saas/tests/security/rls.mjs +4 -4
- 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
|
|
8
|
-
'session.handoffThen': { tr: 'Devir notu var: /handoff
|
|
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}
|
|
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 (
|
|
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
|
|
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 →
|
|
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/
|
|
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})' },
|
package/.claude/hooks/notify.mjs
CHANGED
|
@@ -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
|
|
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;
|
package/.claude/hooks/prefs.mjs
CHANGED
|
@@ -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
|
|
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:
|
|
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 = {
|
|
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
|
-
|
|
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))
|
|
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(
|
|
67
|
-
|
|
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/
|
|
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 →
|
|
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('
|
|
57
|
-
if (!url) return fail('AGENTS.md →
|
|
58
|
-
if (!LOCAL.test(url)) return fail(`the
|
|
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 →
|
|
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
|
|
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>" <
|
|
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
|
|
17
|
-
const SIZES = ['
|
|
18
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
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,
|
|
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, '
|
|
46
|
-
if (task != null) t = setLabel(t, '
|
|
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, '
|
|
49
|
-
t = setSection(t, '
|
|
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, '
|
|
59
|
+
if (next != null) t = setSection(t, 'nextStep', `1. ${line(next)}`);
|
|
52
60
|
return t;
|
|
53
61
|
}
|
|
54
62
|
|
|
55
|
-
|
|
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 (
|
|
6
|
-
// evidence test-results-qa/shots
|
|
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 (`- [ ]
|
|
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
|
-
|
|
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
|
-
|
|
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 ? '
|
|
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('
|
|
58
|
-
const counts = { passed: count('
|
|
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('
|
|
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['
|
|
34
|
-
if (
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
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/
|
|
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}`));
|