tillerhand 3.48.1 → 3.49.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 (108) hide show
  1. package/.claude/agents/board-chair.md +1 -1
  2. package/.claude/agents/security-auditor.md +1 -1
  3. package/.claude/agents/test-writer.md +1 -1
  4. package/.claude/board/board-product.md +1 -1
  5. package/.claude/board/council.md +1 -1
  6. package/.claude/board/rules.md +6 -6
  7. package/.claude/commands/adopt.md +3 -3
  8. package/.claude/commands/ask-mode.md +4 -4
  9. package/.claude/commands/board.md +1 -1
  10. package/.claude/commands/build.md +1 -1
  11. package/.claude/commands/design.md +4 -4
  12. package/.claude/commands/findings-business.md +1 -1
  13. package/.claude/commands/findings-legal.md +1 -1
  14. package/.claude/commands/findings-perf.md +1 -1
  15. package/.claude/commands/findings-security.md +2 -2
  16. package/.claude/commands/findings-tech.md +1 -1
  17. package/.claude/commands/findings-ux.md +1 -1
  18. package/.claude/commands/fix-bug.md +2 -2
  19. package/.claude/commands/handoff.md +2 -2
  20. package/.claude/commands/kit-update.md +2 -2
  21. package/.claude/commands/qa.md +1 -1
  22. package/.claude/commands/release.md +2 -2
  23. package/.claude/commands/research.md +3 -3
  24. package/.claude/commands/retro.md +1 -1
  25. package/.claude/commands/rollback.md +3 -3
  26. package/.claude/commands/start.md +4 -4
  27. package/.claude/commands/tools.md +1 -1
  28. package/.claude/commands/user-feedback.md +1 -1
  29. package/.claude/findings/lenses/business.md +1 -1
  30. package/.claude/findings/protocol.md +2 -2
  31. package/.claude/hooks/budget.mjs +1 -1
  32. package/.claude/hooks/checkpoint.mjs +2 -2
  33. package/.claude/hooks/commit-gate.mjs +18 -17
  34. package/.claude/hooks/council.mjs +37 -52
  35. package/.claude/hooks/dashboard-data.mjs +34 -42
  36. package/.claude/hooks/dashboard.mjs +18 -20
  37. package/.claude/hooks/destructive-guard.mjs +2 -2
  38. package/.claude/hooks/flow.mjs +1 -1
  39. package/.claude/hooks/freeze-guard.mjs +2 -2
  40. package/.claude/hooks/gate.mjs +21 -13
  41. package/.claude/hooks/labels.mjs +92 -152
  42. package/.claude/hooks/lang.mjs +25 -9
  43. package/.claude/hooks/messages.mjs +497 -494
  44. package/.claude/hooks/notify.mjs +1 -1
  45. package/.claude/hooks/prefs.mjs +10 -18
  46. package/.claude/hooks/preview.mjs +1 -1
  47. package/.claude/hooks/progress.mjs +7 -9
  48. package/.claude/hooks/qa-report.mjs +1 -3
  49. package/.claude/hooks/session-start.mjs +2 -3
  50. package/.claude/hooks/status-read.mjs +5 -5
  51. package/.claude/hooks/statusline.mjs +4 -4
  52. package/.claude/hooks/tools.mjs +3 -7
  53. package/.claude/hooks/usage.mjs +2 -2
  54. package/.claude/i18n/tr.json +496 -0
  55. package/.claude/security-patterns.json +1 -1
  56. package/.claude/skills/logo-design/SKILL.md +2 -2
  57. package/.claude/skills/motion-video/SKILL.md +1 -1
  58. package/.claude/skills/source-research/SKILL.md +1 -1
  59. package/.claude/skills/ui-motion/SKILL.md +1 -1
  60. package/AGENTS.md +1 -1
  61. package/CHANGELOG.md +1 -1
  62. package/README.md +2 -2
  63. package/README.tr.md +2 -2
  64. package/docs/architecture/overview.md +1 -1
  65. package/docs/decisions/log.md +1 -1
  66. package/docs/legal/privacy-kvkk.md +35 -35
  67. package/docs/operations/deploy-setup.md +1 -1
  68. package/docs/operations/feature-flags.md +1 -1
  69. package/docs/product/glossary.md +3 -3
  70. package/docs/product/launch.md +1 -1
  71. package/docs/product/prd.md +2 -2
  72. package/docs/product/roadmap.md +1 -1
  73. package/docs/product/specs/_template.md +1 -1
  74. package/docs/product/unit-economics.md +1 -1
  75. package/docs/quality/test-plan.md +5 -5
  76. package/docs/workflow/decision-protocol.md +1 -2
  77. package/docs/workflow/mcp-catalog.md +1 -1
  78. package/docs/workflow/skills.md +2 -2
  79. package/docs/workflow/usage-limits.md +1 -1
  80. package/kit/CHANGELOG.md +34 -0
  81. package/kit/VERSION +1 -1
  82. package/kit/guide.html +56 -56
  83. package/kit/guide.tr.html +10 -10
  84. package/kit/kit.mjs +7 -42
  85. package/kit/templates/addons/ai/.env.example.append +3 -3
  86. package/kit/templates/addons/tr/docs/legal/turkiye.md +55 -55
  87. package/kit/templates/addons/tr/types/backend/.env.example.append +2 -2
  88. package/kit/templates/addons/tr/types/backend/app/shared/payments.py +1 -1
  89. package/kit/templates/addons/tr/types/backend/tests/security/test_payment_webhook.py +3 -3
  90. package/kit/templates/addons/tr/types/web-saas/.env.example.append +2 -2
  91. package/kit/templates/addons/tr/types/web-saas/.github/workflows/payment-webhook.yml +3 -3
  92. package/kit/templates/addons/tr/types/web-saas/src/lib/payments/webhook-signature.ts +1 -1
  93. package/kit/templates/addons/tr/types/web-saas/tests/security/payment-webhook.mjs +4 -4
  94. package/kit/templates/backend/.github/dependabot.yml +1 -1
  95. package/kit/templates/backend/.github/workflows/live-monitoring.yml +1 -1
  96. package/kit/templates/backend/docs/architecture/stack.md +3 -3
  97. package/kit/templates/mobile/.github/dependabot.yml +1 -1
  98. package/kit/templates/mobile/.github/workflows/ci.yml +2 -2
  99. package/kit/templates/mobile/docs/architecture/stack.md +4 -4
  100. package/kit/templates/mobile/docs/architecture/structure.md +1 -1
  101. package/kit/templates/mobile/docs/design/design-guide.md +1 -1
  102. package/kit/templates/web-saas/.github/dependabot.yml +1 -1
  103. package/kit/templates/web-saas/.github/workflows/ci.yml +2 -2
  104. package/kit/templates/web-saas/.github/workflows/live-monitoring.yml +1 -1
  105. package/kit/templates/web-saas/docs/architecture/stack.md +4 -4
  106. package/kit/templates/web-saas/docs/design/design-guide.md +1 -1
  107. package/kit/tools/antigravity/.agents/hooks.json +5 -5
  108. package/package.json +1 -1
@@ -11,8 +11,7 @@ import { verdict, isValue } from './labels.mjs';
11
11
  import { readLimits, summary, level, THRESHOLDS } from './usage.mjs';
12
12
  import { packageStatus, commandMode } from './tools.mjs';
13
13
  import { readModels, modelsFile, modelsNote, roleLabel } from './models.mjs';
14
- import { OLD_SIZES } from './progress.mjs';
15
- import { readLang, t } from './lang.mjs';
14
+ import { readLang, t, locale } from './lang.mjs';
16
15
  import { acceptanceStatus, accounts, flags, unitEconomics, feedback, learning, qaSummary, metrics, githubStatus, readContext, findingReports, FINDING_KINDS, producedAssets, costSummary, councilMinutes, spendAlarms, envWarnings, repeatedBugClasses } from './dashboard-data.mjs';
17
16
 
18
17
  const args = process.argv.slice(2);
@@ -28,8 +27,8 @@ const kindName = (k) => (Object.hasOwn(FINDING_KINDS, k) ? L(`dashboard.findingK
28
27
  // The dashboard is made in the user's language: none before /start or /adopt asks it
29
28
  const lang = readLang(root);
30
29
  if (!lang) { if (!quiet) console.log('No language chosen yet: the dashboard is made after /start or /adopt asks it.'); process.exit(0); }
31
- // Dates follow the conversation language (English when the runtime does not know the code)
32
- const loc = (() => { try { return Intl.DateTimeFormat.supportedLocalesOf(lang)[0] ?? 'en'; } catch { return 'en'; } })();
30
+ // Dates follow the conversation language
31
+ const loc = locale(lang);
33
32
 
34
33
  // ---------- data ----------
35
34
  const d = readStatus(root);
@@ -119,6 +118,8 @@ if (d.kitPending) addAlert('info', L('dashboard.alert.kitPending'), L('dashboard
119
118
  if (d.handoffPending) addAlert('warn', L('dashboard.alert.handoffPending'), '/handoff take');
120
119
  const lastReject = [...d.board.meetings].reverse().find((t) => verdict(t.result) === 'reject');
121
120
  if (lastReject && !lastReject.decision) addAlert('critical', L('dashboard.alert.boardReject', { kind: lastReject.kind, scope: lastReject.scope, critical: lastReject.critical || L('dashboard.criticalFinding') }), L('dashboard.act.askClaudeFix'));
121
+ // A result verdict() cannot read is not an approval: the user checks it (fail-closed)
122
+ for (const t of d.board.meetings.filter((m) => m.result && verdict(m.result) === 'unknown')) addAlert('warn', L('dashboard.alert.unknownVerdict', { kind: t.kind, scope: t.scope, result: t.result }), 'docs/workflow/board.md');
122
123
  const limitLevel = level(limit);
123
124
  const limitShort = limit ? [[L('dashboard.limit5h'), limit.five_hour], [L('dashboard.limitWeeklyLower'), limit.seven_day]].filter(([, p]) => p)
124
125
  .map(([name, p]) => `${name} %${Math.round(p.percent)}${p.resetsAt ? ` ${L('dashboard.resetsParen', { when: new Date(p.resetsAt * 1000).toLocaleString(loc, { weekday: 'short', hour: '2-digit', minute: '2-digit' }) })}` : ''}`).join(', ') : '';
@@ -130,7 +131,7 @@ for (const s of d.issues.slice(0, 5)) addAlert('warn', L('dashboard.alert.openIs
130
131
  for (const r of repeatedBugClasses(root)) addAlert('info', L('dashboard.alert.repeatedBug', { count: r.specs.length, cls: r.cls, specs: r.specs.join(', ') }), '/retro');
131
132
  if (git?.changed.length > 15) addAlert('info', L('dashboard.alert.uncommitted', { count: git.changed.length }), L('dashboard.act.commitOrWrapUp'));
132
133
  if (git?.progressStale >= 3) addAlert('info', L('dashboard.alert.progressStale', { count: git.progressStale }), '/wrap-up');
133
- const off = (a['Question mode'] ?? '').match(/(?:kapalı konular|closed topics):\s*(.+)$/i)?.[1];
134
+ const off = (a['Question mode'] ?? '').match(/closed topics:\s*(.+)$/i)?.[1];
134
135
  if (off && !isValue(off, 'none')) addAlert('info', L('dashboard.alert.closedTopics', { topics: off }), L('dashboard.act.askModeOpen'));
135
136
  if (d.installed && isValue(d.board.state, 'notSetUp') && !isValue(a['Active phase'], 'setup')) addAlert('info', L('dashboard.alert.boardNotSetUp'), '/board setup');
136
137
  if (github?.liveMonitoring?.length) addAlert('critical', L('dashboard.alert.liveMonitoring', { title: github.liveMonitoring[0].title }), L('dashboard.act.fixLive', { number: github.liveMonitoring[0].number }));
@@ -186,7 +187,7 @@ const autoDecisions = d.decisions.filter((k) => isValue(k.decidedBy, 'automatic'
186
187
  const rejectCount = d.board.meetings.filter((t) => verdict(t.result) === 'reject').length;
187
188
  const mvpCriteria = mvp.reduce((t, o) => ({ done: t.done + o.done, total: t.total + o.total }), { done: 0, total: 0 });
188
189
  const avgCost = Object.entries(cost.reduce((m, r) => {
189
- if (!Number.isNaN(r.diff)) (m[OLD_SIZES[r.size] ?? r.size] ??= []).push(r.diff);
190
+ if (!Number.isNaN(r.diff)) (m[r.size] ??= []).push(r.diff);
190
191
  return m;
191
192
  }, Object.create(null))).map(([size, l]) => ({ size, avg: l.reduce((t, x) => t + x, 0) / l.length, n: l.length }));
192
193
 
@@ -202,25 +203,22 @@ const feedLabel = (k) => (Object.hasOwn(FEED_KEYS, k) ? L(FEED_KEYS[k]) : k);
202
203
  const onOff = (v) => (isValue(v, 'on') ? L('dashboard.on') : isValue(v, 'off') ? L('dashboard.off') : v);
203
204
  // A text for the page's own script: a single-quoted JS string, no tag can end the script block
204
205
  const jsStr = (key) => `'${L(key).replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/</g, '\\u003c')}'`;
205
- // A board verdict in either language; an option name (decision council) or an unreadable result is neutral
206
- const verdictBadge = (k) => badge({ approve: 'good', conditional: 'warn', reject: 'critical' }[verdict(k)] ?? 'info', k);
206
+ // A council decision: an option name or an unreadable result is neutral. A meeting result verdict() cannot read is a warning.
207
+ const verdictBadge = (k, other = 'info') => badge({ approve: 'good', conditional: 'warn', reject: 'critical' }[verdict(k)] ?? other, k);
207
208
  const badge = (sev, caption) => `<span class="badge ${sev}"><span class="ic" aria-hidden="true">${SEV[sev][0]}</span> ${e(caption ?? L(SEV[sev][1]))}</span>`;
208
209
  const fmtDate = (t) => { const z = toDate(t); return Number.isNaN(z.getTime()) ? e(t) : z.toLocaleString(loc, { day: '2-digit', month: 'short', hour: t.length > 10 ? '2-digit' : undefined, minute: t.length > 10 ? '2-digit' : undefined }); };
209
210
  const limitSeverity = (p, k) => (p == null ? 'info' : p >= THRESHOLDS[k].red ? 'critical' : p >= THRESHOLDS[k].yellow ? 'warn' : 'good');
210
211
  const TOPICS = ['product', 'tech', 'design', 'content', 'data', 'security', 'cost'];
211
- // Decision log topics before 3.48, in the same order; they count under the English topic
212
- const TOPICS_TR = ['urun', 'teknik', 'tasarim', 'icerik', 'veri', 'guvenlik', 'maliyet'];
213
- const topicKey = (topic = '') => { const k = topic.trim().toLowerCase(); return TOPICS[TOPICS_TR.indexOf(k)] ?? k; };
212
+ const topicKey = (topic = '') => topic.trim().toLowerCase();
214
213
 
215
- // The command to paste into Claude: "`/build G-013` …" or "/wrap-up ve /clear" → first command, without its <placeholder> or note
216
- const cmdOf = (s = '') => { const m = String(s).match(/`(\/[^`\n]+)`/) ?? String(s).match(/^(\/.*)$/); return m ? m[1].split(/ ve | ile | and | to |\s*\(/)[0].replace(/\s*[[<].*$/, '').trim() : ''; };
214
+ // The command to paste into Claude: "`/build G-013` …" or "/wrap-up and /clear" → first command, without its <placeholder> or note
215
+ const cmdOf = (s = '') => { const m = String(s).match(/`(\/[^`\n]+)`/) ?? String(s).match(/^(\/.*)$/); return m ? m[1].split(/\s+\S+\s+\/[\w:-]+(?![\w:/-]|\.\w)| and | to |\s*\(/)[0].replace(/\s*[[<].*$/, '').trim() : ''; };
217
216
  const copyBtn = (s) => { const c = cmdOf(s); return c ? ` <button class="copy-cmd" type="button" data-cmd="${e(c)}" aria-label="${e(L('dashboard.copyAria', { command: c }))}">${e(L('dashboard.copy'))}</button>` : ''; };
218
217
  const alertRow = (x) => `<li class="${x.sev}"><span class="ic" aria-hidden="true">${SEV[x.sev][0]}</span><span><span class="sr-only">${L(SEV[x.sev][1])}: </span>${md(x.text)}</span>${x.action ? `<span class="action">→ ${md(x.action)}${copyBtn(x.action)}</span>` : ''}</li>`;
219
218
  const kpi = (label, value, sub = '', bar = '') => `<div class="kpi"><div class="kpi-l">${e(label)}</div><div class="kpi-v">${value}</div>${bar}<div class="kpi-s">${sub}</div></div>`;
220
219
 
221
220
  // ---------- sections ----------
222
- // A pre-3.48 value says "Faz 1"; the roadmap headings are read as "Phase 1"
223
- const activePhase = (a['Active phase'] ?? '').replace(/^faz\b/i, 'Phase').replace(/^başlangıç(?![\p{L}\p{N}])/iu, 'Setup');
221
+ const activePhase = a['Active phase'] ?? '';
224
222
  const activeIndex = Math.max(0, [{ name: 'Setup' }, ...d.phases].findIndex((f) => activePhase.toLowerCase().startsWith(f.name.toLowerCase().split(' ').slice(0, 2).join(' '))));
225
223
  const start = (a['Setup step'] ?? '0/9').split('/').map(Number);
226
224
  const stageStrip = `<ol class="steps">${[{ name: 'Setup', done: start[0] || 0, total: start[1] || 9 }, ...d.phases].map((f, i) =>
@@ -264,7 +262,7 @@ const boardSection = `
264
262
  <div class="tw"><table><thead><tr><th>${L('dashboard.th.date')}</th><th>${L('dashboard.th.kind')}</th><th>${L('dashboard.th.topic')}</th><th>${L('dashboard.th.decision')}</th><th>${L('dashboard.th.consensus')}</th><th>${L('dashboard.th.minutes')}</th></tr></thead><tbody>${councils.slice(0, 10).map((k) => `<tr><td class="num nw">${e(k.date)}</td><td>${e(k.kind)}</td><td>${md(k.topic)}</td><td>${decisionBadge(k.decision)}</td><td class="num">${k.consensus == null ? '-' : `%${k.consensus}`}</td><td><a href="${k.file.split('/').map(encodeURIComponent).join('/')}" target="_blank" rel="noopener">${L('dashboard.open')}</a></td></tr>`).join('')}</tbody></table></div>`;
265
263
  })() : empty(L('dashboard.noCouncil'), '/board council <question>')}</section>
266
264
  <section class="card"><h3>${L('dashboard.meetingLog')}</h3>${d.board.meetings.length ? `<div class="tw"><table><thead><tr><th>#</th><th>${L('dashboard.th.date')}</th><th>${L('dashboard.th.kind')}</th><th>${L('dashboard.th.scope')}</th><th>${L('dashboard.th.attendees')}</th><th>${L('dashboard.th.result')}</th><th>${L('dashboard.th.critical')}</th><th>${L('dashboard.th.userDecision')}</th></tr></thead><tbody>
267
- ${[...d.board.meetings].reverse().map((t) => `<tr><td class="num">${e(t.no)}</td><td class="num nw">${e(t.date)}</td><td>${e(t.kind)}</td><td>${md(t.scope)}</td><td>${e(t.attendees)}</td><td>${verdictBadge(t.result)}</td><td>${md(t.critical)}</td><td>${md(t.decision)}</td></tr>`).join('')}
265
+ ${[...d.board.meetings].reverse().map((t) => `<tr><td class="num">${e(t.no)}</td><td class="num nw">${e(t.date)}</td><td>${e(t.kind)}</td><td>${md(t.scope)}</td><td>${e(t.attendees)}</td><td>${verdictBadge(t.result, 'warn')}</td><td>${md(t.critical)}</td><td>${md(t.decision)}</td></tr>`).join('')}
268
266
  </tbody></table></div>` : empty(L('dashboard.noMeetings'))}</section>`;
269
267
 
270
268
  const quality = `
@@ -278,7 +276,7 @@ const quality = `
278
276
  ${qa ? `<section class="card"><h3>${L('dashboard.lastQa', { name: e(qa.name) })}</h3><div class="kpis mini">${kpi(L('dashboard.passed'), qa.passed)}${kpi(L('dashboard.failed'), qa.failed, qa.failed ? badge('critical', L('dashboard.withEvidence')) : '')}${kpi(L('dashboard.notRun'), qa.notRun)}</div>${qa.failures.length ? `<ul>${qa.failures.slice(0, 5).map((x) => `<li>${md(x.criterion)}${x.note ? ` <span class="muted small">${e(x.note)}</span>` : ''}</li>`).join('')}</ul>` : ''}<p class="muted small"><a href="${e(qa.file)}" target="_blank" rel="noopener">${L('dashboard.openReport')}</a></p></section>` : ''}
279
277
  <section class="card"><h3>${L('dashboard.acceptanceTests')}</h3>${acceptance.total ? `<div class="kpis mini">${kpi(L('dashboard.th.criterion'), acceptance.total)}${kpi(L('dashboard.kpiTested'), `${acceptance.tested}/${acceptance.total}`, '', progressBar(acceptance.tested, acceptance.total))}${kpi(L('dashboard.kpiPassing'), `${acceptance.passing}/${acceptance.total}`, acceptance.reportTime ? L('dashboard.reportTime', { when: new Date(acceptance.reportTime).toLocaleString(loc, { day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit' }) }) : L('dashboard.noJunit'), progressBar(acceptance.passing, acceptance.total, 'good'))}</div>` : empty(L('dashboard.noAcceptanceIds'))}</section>
280
278
  <section class="card"><h3>${L('dashboard.metricsMonthly')}</h3>${metricRecords.length ? `<div class="tw"><table><thead><tr><th>${L('dashboard.th.period')}</th><th>${L('dashboard.th.task')}</th><th>${L('dashboard.th.avgMinutes')}</th><th>${L('dashboard.th.avgLimit')}</th><th>${L('dashboard.th.fixes')}</th><th>${L('dashboard.th.reverts')}</th><th>${L('dashboard.th.duplicateCode')}</th><th>${L('dashboard.th.openCritical')}</th><th>${L('dashboard.th.acceptance')}</th></tr></thead><tbody>${[...metricRecords].reverse().slice(0, 8).map((m) => `<tr><td class="nw">${e(m.period)}</td><td class="num">${e(m.taskCount)}</td><td class="num">${e(m.duration)}</td><td class="num">${e(m.limit)}</td><td class="num">${e(m.fixes)}</td><td class="num">${e(m.reverts)}</td><td class="num">${e(m.duplicates)}</td><td class="num">${e(m.critical)}</td><td class="num">${e(m.acceptance)}</td></tr>`).join('')}</tbody></table></div>` : empty(L('dashboard.noMetrics'))}</section>
281
- <section class="card"><h3>${L('dashboard.criticalFlowTests')}</h3>${criticalFlows.length ? `<div class="tw"><table><thead><tr><th>#</th><th>${L('dashboard.th.flow')}</th><th>${L('dashboard.th.testKind')}</th><th>${L('dashboard.th.file')}</th><th>${L('dashboard.status')}</th></tr></thead><tbody>${criticalFlows.map((k) => `<tr><td class="num">${e(k.no)}</td><td>${md(k.feed)}</td><td>${e(k.kind)}</td><td>${md(k.fileName)}</td><td>${/✅|yeşil|var/i.test(k.state) ? badge('good', L('dashboard.present')) : badge('warn', L('dashboard.missing'))}</td></tr>`).join('')}</tbody></table></div>` : empty(L('dashboard.noTestPlan'))}</section>
279
+ <section class="card"><h3>${L('dashboard.criticalFlowTests')}</h3>${criticalFlows.length ? `<div class="tw"><table><thead><tr><th>#</th><th>${L('dashboard.th.flow')}</th><th>${L('dashboard.th.testKind')}</th><th>${L('dashboard.th.file')}</th><th>${L('dashboard.status')}</th></tr></thead><tbody>${criticalFlows.map((k) => `<tr><td class="num">${e(k.no)}</td><td>${md(k.feed)}</td><td>${e(k.kind)}</td><td>${md(k.fileName)}</td><td>${/✅|green|present/i.test(k.state) ? badge('good', L('dashboard.present')) : badge('warn', L('dashboard.missing'))}</td></tr>`).join('')}</tbody></table></div>` : empty(L('dashboard.noTestPlan'))}</section>
282
280
  <section class="card"><h3>${L('dashboard.maintenanceLog')}</h3>${maintenance.length ? `<ul class="feed compact">${[...maintenance].reverse().slice(0, 8).map((b) => feedRow({ time: b.date, kind: 'maintenance', title: `${b.kind}: ${b.findings}`, extra: b.work })).join('')}</ul>` : empty(L('dashboard.noMaintenance'), '/maintain weekly')}
283
281
  ${lastRetro ? `<p class="muted small">${L('dashboard.lastRetro', { value: e(lastRetro) })}</p>` : ''}</section>`;
284
282
 
@@ -302,7 +300,7 @@ const product = `
302
300
  </div>
303
301
  <div class="grid2">
304
302
  <section class="card"><h3>${L('dashboard.technology')}</h3>${stack.length && !stack.every((s) => !s.choice) ? `<dl class="dl">${stack.map((s) => `<dt>${e(s.layer)}</dt><dd>${md(s.choice || '-')}</dd>`).join('')}</dl>` : empty(L('dashboard.noStack'))}</section>
305
- <section class="card"><h3>${L('dashboard.design')}</h3>${designDirection.length ? `<dl class="dl">${designDirection.map((t) => `<dt>${e(t.area)}</dt><dd>${md(t.choice)}</dd>`).join('')}</dl>` : empty(L('dashboard.noDesign'), '/design genel')}
303
+ <section class="card"><h3>${L('dashboard.design')}</h3>${designDirection.length ? `<dl class="dl">${designDirection.map((t) => `<dt>${e(t.area)}</dt><dd>${md(t.choice)}</dd>`).join('')}</dl>` : empty(L('dashboard.noDesign'), '/design general')}
306
304
  ${previews.length ? `<h4>${L('dashboard.previews')}</h4><ul class="links">${previews.map((o) => `<li><b>${e(o.name)}</b> ${o.files.map((f) => `<a href=".previews/${encodeURIComponent(o.name)}/${encodeURIComponent(f)}" target="_blank" rel="noopener">${e(f.replace(/\.html$/, ''))}</a>`).join(' ')}</li>`).join('')}</ul>` : ''}</section>
307
305
  </div>
308
306
  <div class="grid2">
@@ -553,9 +551,9 @@ ${TABS.map(([id, , content], i) => `<div class="panel" role="tabpanel" id="p-${i
553
551
  });
554
552
 
555
553
  var auto = document.getElementById('auto-refresh'), timer;
556
- try { auto.checked = localStorage.getItem('durum-oto') !== '0'; } catch (er) {}
554
+ try { auto.checked = localStorage.getItem('status-auto') !== '0'; } catch (er) {}
557
555
  function schedule() { clearTimeout(timer); if (auto.checked) timer = setTimeout(function () { location.reload(); }, 30000); }
558
- auto.addEventListener('change', function () { try { localStorage.setItem('durum-oto', auto.checked ? '1' : '0'); } catch (er) {} schedule(); });
556
+ auto.addEventListener('change', function () { try { localStorage.setItem('status-auto', auto.checked ? '1' : '0'); } catch (er) {} schedule(); });
559
557
  schedule();
560
558
  })();
561
559
  </script>
@@ -218,7 +218,7 @@ export function scopeIssue(command, dir, root) {
218
218
  const vk = path.join(os.userInfo().homedir, '.vibe-kit');
219
219
  const touchesApproval = (y) => VIBE_KIT.test(y) || !/^\.\.|^$/.test(path.relative(y, vk)) && !path.isAbsolute(path.relative(y, vk));
220
220
  for (const h of targets) {
221
- if (touchesApproval(h) || touchesApproval(realPath(h))) return { decision: 'ask', reason: /(preferences|tercihler)\.md$/i.test(h)
221
+ if (touchesApproval(h) || touchesApproval(realPath(h))) return { decision: 'ask', reason: /preferences[.]md$/i.test(h)
222
222
  ? t(root, 'guard.destructive.prefsFile', { path: h })
223
223
  : t(root, 'guard.destructive.gateApproval', { path: h }) };
224
224
  const rel = relPath(realPath(h), groot);
@@ -281,7 +281,7 @@ export const unquote = (s) => s.replace(/\$'((?:[^'\\]|\\.)*)'/g, '$1').replace(
281
281
  const RANK = { deny: 2, ask: 1 };
282
282
  const strictest = (...s) => s.filter(Boolean).sort((a, b) => RANK[b.decision] - RANK[a.decision])[0] ?? null;
283
283
 
284
- // A commit message that only names a command (git commit -m "commit-gate.mjs --skip anlatıldı") is text, not a call.
284
+ // A commit message that only names a command (git commit -m "commit-gate.mjs --skip explained") is text, not a call.
285
285
  // Emptied only when the WHOLE command is one plain commit (optionally after "git add … &&") and the message is its last
286
286
  // word: then nothing else can hide in it. No quote, $, `, (, #, <, > before the message: a quote opened earlier
287
287
  // would make a real command look like the message. The message must be safe in bash, PowerShell and cmd alike: no
@@ -23,7 +23,7 @@ const MEMBERS = [
23
23
  ['board-product', /^docs\/product\//i],
24
24
  ];
25
25
  // Auth, payment, webhook, upload, secrets: security-auditor joins on top of the member count (rules.md §3)
26
- const SENSITIVE = /auth|login|session|passw|payment|ödeme|checkout|billing|webhook|upload|secret|token|crypto|\.env|iyzico|paytr|stripe/i;
26
+ const SENSITIVE = /auth|login|session|passw|payment|checkout|billing|webhook|upload|secret|token|crypto|\.env|iyzico|paytr|stripe/i;
27
27
 
28
28
  // Same bands as the cost table in docs/workflow/usage-limits.md §3 (1-2, 3-6, more files)
29
29
  export function flow({ files = 0, paths, installed = [], context, level: lvl }) {
@@ -10,7 +10,7 @@
10
10
  // node .claude/hooks/freeze-guard.mjs --status print state (lock and plan)
11
11
  // Scope shield (F-06): after /plan-task approval the plan's file list is saved; while a plan is active a write to an
12
12
  // EXISTING file not on the list is asked. New files are the structure guard's job; docs/, CHANGELOG.md are free. No plan: silent.
13
- // State: .claude/state/plan.json → { "task": "G-012", "paths": ["src/a.ts", "src/features/fatura/"] } (ending in /: folder)
13
+ // State: .claude/state/plan.json → { "task": "G-012", "paths": ["src/a.ts", "src/features/invoice/"] } (ending in /: folder)
14
14
  // node .claude/hooks/freeze-guard.mjs --plan-save <task> <path...> save when no plan is active (asked: the user sees the list)
15
15
  // node .claude/hooks/freeze-guard.mjs --plan-add <path...> widen the list (with user approval)
16
16
  // node .claude/hooks/freeze-guard.mjs --plan-end close when the task is done (asked)
@@ -134,7 +134,7 @@ function hook() {
134
134
  const paths = g.files.map(({ path: p }) => realPath(path.resolve(g.dir, p)));
135
135
  // Gate command approval record (gate.mjs, in the user folder ~/.vibe-kit/): the AI must not write it by hand
136
136
  const approval = paths.find((p) => /[\\/]\.vibe-kit([\\/]|$)/i.test(p));
137
- if (approval) decide(g.tool, 'ask', /(preferences|tercihler)\.md$/i.test(approval)
137
+ if (approval) decide(g.tool, 'ask', /preferences[.]md$/i.test(approval)
138
138
  ? t(g.root, 'guard.freeze.prefsFile', { path: approval })
139
139
  : t(g.root, 'guard.freeze.gateApproval', { path: approval }));
140
140
  if (g.tool !== 'claude') {
@@ -18,15 +18,12 @@ import { spawnSync } from 'node:child_process';
18
18
  import { changes } from './commit-gate.mjs';
19
19
  import { isCodeFile } from './edit-rounds.mjs';
20
20
  import { findRoot, isMain } from './shared.mjs';
21
- import { normalize } from './labels.mjs';
22
21
  import { specInvariants } from './dashboard-data.mjs';
23
22
 
24
- // Keys of the AGENTS.md → Commands block (Turkish keys of pre-3.48 projects are read through normalize)
23
+ // Keys of the AGENTS.md → Commands block
25
24
  const CHECKS = ['lint', 'type check', 'test'];
26
25
  // The approval also covers the dev command and the preview address: preview.mjs runs the one and opens the other
27
26
  const APPROVED = [...CHECKS, 'dev', 'preview'];
28
- // The digest keeps the pre-3.48 names, so an approval given before the English labels stays valid
29
- const DIGEST_NAME = { 'type check': 'tip kontrol', dev: 'geliştirme', preview: 'önizleme' };
30
27
  const TIMEOUT = 10 * 60 * 1000;
31
28
  const BUFFER = 64 * 1024 * 1024;
32
29
  // A red check shows its last 30 lines; a longer output is kept whole in a file, read only when the tail is not enough
@@ -42,13 +39,21 @@ function failed(root, name, r) {
42
39
 
43
40
  // Only the "## Commands" section is read: a "test:" line elsewhere must not count as a command
44
41
  export function readCommands(md, names = CHECKS) {
45
- const section = normalize(md).match(/^## Commands[^\n]*\n([\s\S]*?)(?=^## |(?![\s\S]))/m)?.[1] ?? '';
42
+ const section = md.match(/^## Commands[^\n]*\n([\s\S]*?)(?=^## |(?![\s\S]))/m)?.[1] ?? '';
46
43
  return names.map((name) => {
47
44
  const value = section.match(new RegExp(`^${name}:[ \\t]*(.*?)[ \\t\\r]*$`, 'm'))?.[1];
48
45
  return { name, command: value && !/^\[.*\]$/.test(value) ? value : null };
49
46
  });
50
47
  }
51
48
 
49
+ // Keys under "## Commands" the kit does not know (a label written in another language): named, not silently ignored
50
+ const KNOWN_KEYS = ['dev', 'preview', 'test', 'lint', 'type check', 'build'];
51
+ export function unknownCommandKeys(md) {
52
+ const section = md.match(/^## Commands[^\n]*\n([\s\S]*?)(?=^## |(?![\s\S]))/m)?.[1] ?? '';
53
+ const fenced = [...section.matchAll(/^```[^\n]*\n([\s\S]*?)^```/gm)].map((m) => m[1]).join('\n'); // prose around the block is not a key
54
+ return [...fenced.matchAll(/^([\p{L}][\p{L}\p{N} _-]*?)[ \t]*:(?!\/\/)/gmu)].map((m) => m[1]).filter((k) => !KNOWN_KEYS.includes(k));
55
+ }
56
+
52
57
  // The approval record lives outside the project, in the user folder (keyed by a digest of the project's real path): no repo,
53
58
  // archive or .git can carry an approval. The home folder comes from the OS (os.userInfo); HOME/USERPROFILE cannot redirect it.
54
59
  export function approvalPath(root) {
@@ -57,7 +62,7 @@ export function approvalPath(root) {
57
62
  const key = crypto.createHash('sha256').update(realRoot.toLowerCase()).digest('hex').slice(0, 32);
58
63
  return path.join(os.userInfo().homedir, '.vibe-kit', 'gate-approval', `${key}.json`);
59
64
  }
60
- const digest = (list) => crypto.createHash('sha256').update(JSON.stringify(list.map((k) => [DIGEST_NAME[k.name] ?? k.name, k.command]))).digest('hex');
65
+ const digest = (list) => crypto.createHash('sha256').update(JSON.stringify(list.map((k) => [k.name, k.command]))).digest('hex');
61
66
  export const commandList = (list) => list.filter((k) => k.command).map((k) => `- ${k.name}: ${k.command}`).join('\n');
62
67
  export const readAgents = (root) => { try { return fs.readFileSync(path.join(root, 'AGENTS.md'), 'utf8'); } catch { return ''; } };
63
68
  export const approvedCommands = (root) => readCommands(readAgents(root), APPROVED);
@@ -113,7 +118,7 @@ function semgrep(root, files) {
113
118
  // boundary after it; any slash style, JSON-escaped too; case-insensitive only on Windows).
114
119
  // Note only: a generic C:\Users\<name>, /home/<name>/, /Users/<name>/ (a container path like /home/node/app or a route
115
120
  // is not a machine path, so it never blocks).
116
- // "kit:yol-izinli" on the line or the line above allows a deliberate one (Dockerfile and JSON take no inline comment).
121
+ // "kit:path-allowed" on the line or the line above allows a deliberate one (Dockerfile and JSON take no inline comment).
117
122
  // Only added lines of tracked files are read (an old path elsewhere in the file is not this change's); a new file is
118
123
  // read whole. Paths are read from the git top level, so a project in a monorepo subfolder is checked too.
119
124
  const ESCAPE = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
@@ -146,18 +151,19 @@ export function absPathCheck(root, files) {
146
151
  } catch { continue; }
147
152
  lines.forEach((l, i) => {
148
153
  if (added.has(f) && !added.get(f).has(i + 1)) return;
149
- if (l.includes('kit:yol-izinli') || lines[i - 1]?.includes('kit:yol-izinli')) return;
154
+ if (l.includes('kit:path-allowed') || lines[i - 1]?.includes('kit:path-allowed')) return;
150
155
  const at = `${f}:${i + 1}: ${l.trim().slice(0, 120)}`;
151
156
  if (mine?.test(l)) red.push(at); else if (generic.test(l)) notes.push(at);
152
157
  });
153
158
  }
154
159
  const list = (a) => [...a.slice(0, 15), a.length > 15 ? `… and ${a.length - 15} more lines` : ''].filter(Boolean).join('\n');
155
- if (red.length) return { name, status: 'failed', note: 'path specific to this machine; use import.meta.url, __dirname, path.join(root, …), os.tmpdir(). If deliberate, write kit:yol-izinli on the line or the line above',
160
+ if (red.length) return { name, status: 'failed', note: 'path specific to this machine; use import.meta.url, __dirname, path.join(root, …), os.tmpdir(). If deliberate, write kit:path-allowed on the line or the line above',
156
161
  output: list(red) };
157
162
  return { name, status: 'passed', ...(notes.length && { note: `${notes.length} lines have a path that looks like a user folder; fix it if it is machine-specific (does not block)`, output: list(notes) }) };
158
163
  }
159
164
 
160
- export const approvalNeeded = (list) => `command approval needed\nAGENTS.md → Commands is new or changed; these commands do not run unapproved:\n${commandList(list)}\n` +
165
+ export const approvalNeeded = (list, unknown = []) => `command approval needed\nAGENTS.md → Commands is new or changed; these commands do not run unapproved:\n${commandList(list)}\n` +
166
+ (unknown.length ? `Keys the gate does not read (they never run; use dev, preview, test, lint, type check, build): ${unknown.join(', ')}\n` : '') +
161
167
  'Show the commands to the user and ask. If they approve: node .claude/hooks/gate.mjs --approve, then run the command again.';
162
168
 
163
169
  // Spec drift (F-23): a "must not change" behavior whose test is missing or red stops the gate; no result is only a note
@@ -185,9 +191,9 @@ export function runGate(root) {
185
191
  let signature = null, files = [];
186
192
  try { ({ signature, files } = changes()); } catch { /* not a git repo: the commands still run */ }
187
193
  const list = approvedCommands(root);
188
- if (!isApproved(root, list)) return { passed: false, text: `## Gate · ${approvalNeeded(list)}` };
194
+ if (!isApproved(root, list)) return { passed: false, text: `## Gate · ${approvalNeeded(list, unknownCommandKeys(readAgents(root)))}` };
189
195
  const checks = [...list.filter((k) => CHECKS.includes(k.name)).map((k) => check(root, k)), semgrep(root, files)];
190
- const empty = !checks.some((k) => k.status === 'passed');
196
+ const empty = !checks.some((k) => CHECKS.includes(k.name) && k.status === 'passed'); // Semgrep alone does not fill a gate
191
197
  const spec = specCheck(root, start); // after the test command, which may have refreshed the JUnit report; it does not fill an empty gate
192
198
  if (spec) checks.push(spec);
193
199
  checks.push(absPathCheck(root, files)); // after "empty": it does not fill a gate with no lint/test either
@@ -198,7 +204,9 @@ export function runGate(root) {
198
204
  const result = checks.some((k) => k.status === 'failed') ? 'Result: RED. The commit gate stays closed: fix it and run the gate again.'
199
205
  : empty ? 'Result: EMPTY, no check ran. Fill in AGENTS.md → Commands; the commit gate stays closed (if the user wants, commit-gate.mjs --skip).'
200
206
  : 'Result: GREEN.';
201
- return { passed, text: [`## Gate · ${files.length} files changed`, ...checks.map(line), result].join('\n') };
207
+ const unknown = unknownCommandKeys(readAgents(root));
208
+ const warning = unknown.length ? [`⚠ AGENTS.md → Commands has keys the gate does not read: ${unknown.join(', ')}. Use dev, preview, test, lint, type check, build.`] : [];
209
+ return { passed, text: [`## Gate · ${files.length} files changed`, ...checks.map(line), ...warning, result].join('\n') };
202
210
  }
203
211
 
204
212
  if (isMain(import.meta.url)) {
@@ -1,174 +1,114 @@
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.
1
+ // Labels and values hooks read from project files. They stay English in every project language.
3
2
 
4
3
  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)' },
4
+ lastUpdated: 'Last updated',
5
+ activePhase: 'Active phase',
6
+ activeTask: 'Active task',
7
+ setupStep: 'Setup step',
8
+ questionMode: 'Question mode',
9
+ profile: 'Profile',
10
+ currentState: 'Current state',
11
+ doneLastSession: 'Done last session',
12
+ nextStep: 'Next step',
13
+ pendingDecisions: 'Pending decisions',
14
+ whereWeLeftOff: 'Where we left off',
15
+ openIssues: 'Open issues / blockers',
16
+ status: 'Status',
17
+ commitReview: 'Commit review',
18
+ members: 'Members',
19
+ meetingLog: 'Meeting log',
20
+ decisionLog: 'Decision log',
21
+ phase: 'Phase',
22
+ commands: 'Commands',
23
+ dev: 'dev',
24
+ preview: 'preview',
25
+ typeCheck: 'type check',
26
+ type: 'Type',
27
+ class: 'Class',
28
+ mustNotChange: 'Behavior that must not change',
29
+ priority: 'Priority',
30
+ outOfScope: 'Out of scope',
31
+ chosenTech: 'Chosen technologies',
32
+ maintenanceLog: 'Maintenance log',
33
+ flags: 'Flags',
34
+ records: 'Records',
35
+ unreleased: 'Unreleased',
36
+ currency: 'Currency',
37
+ price: 'Price (per user, monthly)',
38
+ paymentFee: 'Payment fee (%)',
39
+ fixedCost: 'Fixed monthly cost',
40
+ variableCost: 'Variable cost per user (monthly)',
41
+ targetUsers: 'Target active users',
42
+ freeShare: 'Free user share (%)',
43
+ accounts: 'Accounts',
44
+ inbox: 'Inbox',
45
+ themes: 'Themes',
46
+ reports: 'Reports',
47
+ alarms: 'Alarms',
48
+ chosenDirection: 'Chosen direction',
49
+ valueProposition: 'Value proposition',
50
+ criticalFlows: 'Critical flows',
51
+ learnedRules: 'Learned rules',
52
+ rule: 'Rule',
53
+ environments: 'Environments (dev / prod)',
55
54
  };
56
55
 
57
- // Values hooks read at known label or column positions. Readers compare with isValue(); never normalized in place.
56
+ // Values hooks read at known label or column positions. Readers compare with isValue().
58
57
  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' },
58
+ on: 'on',
59
+ off: 'off',
60
+ none: 'none',
61
+ setup: 'Setup',
62
+ notSetUp: 'not set up',
63
+ setUp: 'set up',
64
+ full: 'Full',
65
+ light: 'Light',
66
+ standard: 'Standard',
67
+ waiting: 'waiting',
68
+ automatic: 'automatic',
69
+ user: 'user',
70
+ oneWay: 'one-way',
71
+ twoWay: 'two-way',
72
+ yes: 'yes',
73
+ no: 'no',
74
+ right: 'right',
75
+ wrong: 'wrong',
76
+ passed: 'passed',
77
+ failed: 'failed',
78
+ notRun: 'not run',
79
+ bug: 'bug',
80
+ feature: 'feature',
81
+ request: 'request',
82
+ complaint: 'complaint',
83
+ praise: 'praise',
84
+ question: 'question',
85
+ new: 'new',
86
+ planned: 'planned',
87
+ done: 'done',
88
+ rejected: 'rejected',
89
+ merged: 'merged',
91
90
  };
92
91
 
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.
92
+ // The value, case-insensitive, whole or followed by a non-letter: "on · closed topics: none" is on, "online" is not.
95
93
  export function isValue(text, key) {
96
94
  if (!Object.hasOwn(VALUES, key)) throw new Error(`unknown value: ${key}`);
97
95
  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))));
96
+ const s = text.trim().toLowerCase();
97
+ const w = VALUES[key].toLowerCase();
98
+ return s.startsWith(w) && !/^[\p{L}\p{N}]/u.test(s.slice(w.length));
103
99
  }
104
100
 
105
101
  export function label(key) {
106
102
  if (!Object.hasOwn(LABELS, key)) throw new Error(`unknown label: ${key}`);
107
- return LABELS[key].en;
103
+ return LABELS[key];
108
104
  }
109
105
 
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;
106
+ const VERDICTS = [['CONDITIONAL', 'conditional'], ['APPROVE', 'approve'], ['REJECT', 'reject']];
125
107
 
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'.
108
+ // Board result. Fail-closed: anything that is not one clean verdict is 'unknown'.
169
109
  export function verdict(text) {
170
110
  if (typeof text !== 'string') return 'unknown';
171
- // "**Result:** RET", "**RET**"; after the word only the end, a dash, a bracket or ": , ; ." ("RET: güvenlik")
111
+ // "**Result:** REJECT", "**REJECT**"; after the word only the end, a dash, a bracket or ": , ; ." ("REJECT: security")
172
112
  const s = text.trim().replace(/^\*\*[^*]+:\*\*\s*/, '').replace(/^\*\*([^*]+)\*\*/, '$1').toUpperCase();
173
113
  for (const [word, result] of VERDICTS) {
174
114
  if (s.startsWith(word) && /^(?:\s*(?:$|[—–(-])|[:,;.])/.test(s.slice(word.length))) return result;