tillerhand 3.42.1 → 3.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/agents/board-chair.md +20 -20
- package/.claude/agents/code-reviewer.md +20 -20
- package/.claude/agents/council-advisor.md +14 -14
- package/.claude/agents/security-auditor.md +30 -30
- package/.claude/agents/test-writer.md +22 -21
- package/.claude/board/board-ai.md +25 -25
- package/.claude/board/board-api.md +25 -25
- package/.claude/board/board-architect.md +28 -28
- package/.claude/board/board-devops.md +25 -25
- package/.claude/board/board-mobile.md +25 -25
- package/.claude/board/board-performance.md +24 -24
- package/.claude/board/board-privacy.md +25 -25
- package/.claude/board/board-product.md +26 -26
- package/.claude/board/board-qa.md +28 -28
- package/.claude/board/board-ux.md +26 -26
- package/.claude/board/council.md +76 -76
- package/.claude/board/rules.md +61 -61
- package/.claude/claude-security-guidance.md +10 -10
- package/.claude/commands/adopt.md +70 -69
- package/.claude/commands/ask-mode.md +9 -9
- package/.claude/commands/board.md +15 -15
- package/.claude/commands/build.md +47 -47
- package/.claude/commands/dashboard.md +17 -17
- package/.claude/commands/design.md +31 -31
- package/.claude/commands/findings-all.md +14 -14
- package/.claude/commands/findings-business.md +7 -7
- package/.claude/commands/findings-legal.md +7 -7
- package/.claude/commands/findings-perf.md +7 -7
- package/.claude/commands/findings-security.md +9 -9
- package/.claude/commands/findings-tech.md +7 -7
- package/.claude/commands/findings-ux.md +7 -7
- package/.claude/commands/fix-bug.md +15 -15
- package/.claude/commands/freeze.md +8 -8
- package/.claude/commands/handoff.md +38 -38
- package/.claude/commands/kit-update.md +17 -14
- package/.claude/commands/language.md +13 -0
- package/.claude/commands/maintain.md +19 -19
- package/.claude/commands/plan-task.md +32 -32
- package/.claude/commands/qa.md +17 -17
- package/.claude/commands/release.md +40 -40
- package/.claude/commands/research.md +31 -31
- package/.claude/commands/retro.md +32 -32
- package/.claude/commands/review-changes.md +15 -15
- package/.claude/commands/rollback.md +24 -24
- package/.claude/commands/start.md +116 -113
- package/.claude/commands/tools.md +11 -11
- package/.claude/commands/user-feedback.md +11 -11
- package/.claude/commands/wrap-up.md +18 -18
- package/.claude/findings/lenses/business.md +24 -24
- package/.claude/findings/lenses/legal.md +29 -29
- package/.claude/findings/lenses/perf.md +19 -19
- package/.claude/findings/lenses/security.md +30 -30
- package/.claude/findings/lenses/tech.md +23 -23
- package/.claude/findings/lenses/ux.md +19 -19
- package/.claude/findings/protocol.md +54 -54
- package/.claude/findings/report-template.md +30 -30
- package/.claude/hooks/budget.mjs +7 -7
- package/.claude/hooks/checkpoint.mjs +12 -12
- package/.claude/hooks/commit-gate.mjs +50 -34
- package/.claude/hooks/cost.mjs +2 -2
- package/.claude/hooks/council.mjs +3 -3
- package/.claude/hooks/dashboard-data.mjs +3 -2
- package/.claude/hooks/dashboard.mjs +164 -149
- package/.claude/hooks/destructive-guard.mjs +74 -64
- package/.claude/hooks/edit-rounds.mjs +3 -3
- package/.claude/hooks/flow.mjs +10 -10
- package/.claude/hooks/freeze-guard.mjs +27 -23
- package/.claude/hooks/gate.mjs +25 -25
- package/.claude/hooks/guard-secrets.mjs +11 -11
- package/.claude/hooks/lang.mjs +114 -0
- package/.claude/hooks/messages.mjs +495 -0
- package/.claude/hooks/models.mjs +14 -11
- package/.claude/hooks/notify.mjs +3 -2
- package/.claude/hooks/package-gate.mjs +17 -15
- package/.claude/hooks/plugins-sync.mjs +9 -9
- package/.claude/hooks/prefs.mjs +19 -19
- package/.claude/hooks/preview.mjs +11 -11
- package/.claude/hooks/progress.mjs +14 -14
- package/.claude/hooks/qa-report.mjs +11 -10
- package/.claude/hooks/reminder.mjs +6 -3
- package/.claude/hooks/session-start.mjs +23 -17
- package/.claude/hooks/shared.mjs +1 -1
- package/.claude/hooks/status-read.mjs +5 -4
- package/.claude/hooks/statusline.mjs +14 -11
- package/.claude/hooks/structure-guard.mjs +9 -9
- package/.claude/hooks/test-guard.mjs +61 -20
- package/.claude/hooks/tools.mjs +28 -29
- package/.claude/hooks/usage.mjs +18 -15
- package/.claude/output-styles/anlatici.md +28 -28
- package/.claude/security-patterns.json +3 -3
- package/.claude/settings.json +6 -0
- package/.claude/skills/logo-design/SKILL.md +42 -42
- package/.claude/skills/motion-video/SKILL.md +52 -52
- package/.claude/skills/source-research/SKILL.md +35 -35
- package/.claude/skills/ui-motion/SKILL.md +38 -38
- package/.github/workflows/kit-tests.yml +7 -7
- package/.github/workflows/npm-publish.yml +16 -16
- package/AGENTS.md +56 -56
- package/CHANGELOG.md +7 -7
- package/CLAUDE.md +5 -5
- package/README.md +199 -172
- package/README.tr.md +292 -0
- package/docs/architecture/overview.md +31 -31
- package/docs/architecture/stack.md +2 -2
- package/docs/archive/README.md +4 -4
- package/docs/decisions/adr-template.md +14 -14
- package/docs/decisions/log.md +10 -10
- package/docs/findings/README.md +13 -13
- package/docs/findings/config.md +13 -13
- package/docs/legal/privacy-kvkk.md +1 -1
- package/docs/operations/accounts.md +25 -25
- package/docs/operations/deploy-setup.md +64 -64
- package/docs/operations/feature-flags.md +17 -17
- package/docs/operations/maintenance.md +48 -48
- package/docs/operations/release-checklist.md +61 -61
- package/docs/operations/spend-alarms.md +28 -28
- package/docs/product/feedback.md +9 -8
- package/docs/product/glossary.md +16 -16
- package/docs/product/launch.md +28 -28
- package/docs/product/prd.md +43 -42
- package/docs/product/roadmap.md +31 -30
- package/docs/product/specs/_template.md +13 -12
- package/docs/product/unit-economics.md +23 -22
- package/docs/quality/metrics.md +14 -14
- package/docs/quality/retros.md +11 -11
- package/docs/quality/test-plan.md +48 -48
- package/docs/security/checklist.md +57 -57
- package/docs/workflow/board.md +7 -7
- package/docs/workflow/cost-log.md +4 -4
- package/docs/workflow/decision-protocol.md +70 -70
- package/docs/workflow/learning.md +4 -4
- package/docs/workflow/mcp-catalog.md +31 -31
- package/docs/workflow/models.md +35 -35
- package/docs/workflow/progress.md +7 -7
- package/docs/workflow/skills.md +108 -108
- package/docs/workflow/usage-limits.md +62 -62
- package/kit/CHANGELOG.md +879 -839
- package/kit/VERSION +1 -1
- package/kit/guide.html +713 -477
- package/kit/guide.tr.html +1328 -0
- package/kit/kit.mjs +56 -54
- package/kit/project/README.md +29 -29
- package/kit/project/gitignore +12 -12
- package/kit/templates/addons/ai/.env.example.append +5 -5
- package/kit/templates/addons/ai/ai/evals/README.md +11 -11
- package/kit/templates/addons/ai/ai/evals/cases.jsonl +2 -2
- package/kit/templates/addons/ai/ai/prompts/README.md +8 -8
- package/kit/templates/addons/ai/docs/ai/guide.md +56 -56
- package/kit/templates/backend/.claude/hooks/format.mjs +1 -1
- package/kit/templates/backend/.claude/structure.json +1 -1
- package/kit/templates/backend/.env.example +6 -6
- package/kit/templates/backend/.github/dependabot.yml +1 -1
- package/kit/templates/backend/.github/workflows/ci.yml +13 -13
- package/kit/templates/backend/.github/workflows/claude-ci-fix.yml +19 -19
- package/kit/templates/backend/.github/workflows/claude-maintenance.yml +18 -18
- package/kit/templates/backend/.github/workflows/live-monitoring.yml +14 -14
- package/kit/templates/backend/.pre-commit-config.yaml +3 -3
- package/kit/templates/backend/docs/architecture/stack.md +98 -98
- package/kit/templates/backend/docs/architecture/structure.md +34 -34
- package/kit/templates/mobile/.claude/structure.json +1 -1
- package/kit/templates/mobile/.env.example +6 -6
- package/kit/templates/mobile/.github/dependabot.yml +2 -2
- package/kit/templates/mobile/.github/workflows/ci.yml +18 -18
- package/kit/templates/mobile/.github/workflows/claude-ci-fix.yml +19 -19
- package/kit/templates/mobile/.github/workflows/claude-maintenance.yml +18 -18
- package/kit/templates/mobile/.gitignore.append +2 -2
- package/kit/templates/mobile/docs/architecture/stack.md +113 -113
- package/kit/templates/mobile/docs/architecture/structure.md +33 -33
- package/kit/templates/mobile/docs/design/design-guide.md +37 -37
- package/kit/templates/mobile/tests/security/rls.mjs +22 -22
- package/kit/templates/web-saas/.claude/structure.json +1 -1
- package/kit/templates/web-saas/.env.example +11 -11
- package/kit/templates/web-saas/.github/dependabot.yml +3 -3
- package/kit/templates/web-saas/.github/workflows/ci.yml +20 -20
- package/kit/templates/web-saas/.github/workflows/claude-ci-fix.yml +19 -19
- package/kit/templates/web-saas/.github/workflows/claude-maintenance.yml +18 -18
- package/kit/templates/web-saas/.github/workflows/live-monitoring.yml +12 -12
- package/kit/templates/web-saas/docs/architecture/stack.md +116 -116
- package/kit/templates/web-saas/docs/architecture/structure.md +44 -44
- package/kit/templates/web-saas/docs/design/design-guide.md +38 -38
- package/kit/templates/web-saas/tests/live/smoke.spec.ts +4 -4
- package/kit/templates/web-saas/tests/security/rls.mjs +22 -22
- package/kit/tools/codex/.codex/hooks.json +1 -1
- package/package.json +2 -2
- package/README.en.md +0 -267
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// Must stay fast: the status line calls it on every refresh (git and heavy reads are in dashboard.mjs).
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
7
|
+
import { t } from './lang.mjs';
|
|
7
8
|
|
|
8
9
|
// Only a regular file: a folder or a link to a folder named like a test or report file must not throw (EISDIR)
|
|
9
10
|
export const readText = (p) => { try { return fs.statSync(p).isFile() ? fs.readFileSync(p, 'utf8') : ''; } catch { return ''; } };
|
|
@@ -70,9 +71,9 @@ export function readStatus(root) {
|
|
|
70
71
|
.map(([no, date, stage, topic, question, chosen, door, decidedBy]) => ({ no, date, stage, topic, question, chosen, door, decidedBy }));
|
|
71
72
|
|
|
72
73
|
const checklists = [
|
|
73
|
-
['
|
|
74
|
-
['
|
|
75
|
-
].filter(([, f]) => fs.existsSync(d(f))).map(([name, f]) => ({ name, file: f, ...tally(readText(d(f))) }));
|
|
74
|
+
['statusRead.checklist.security', 'docs/security/checklist.md'], ['statusRead.checklist.release', 'docs/operations/release-checklist.md'],
|
|
75
|
+
['statusRead.checklist.legal', 'docs/legal/privacy-kvkk.md'], ['statusRead.checklist.ai', 'docs/ai/guide.md'], ['statusRead.checklist.launch', 'docs/product/launch.md'],
|
|
76
|
+
].filter(([, f]) => fs.existsSync(d(f))).map(([name, f]) => ({ name: t(root, name), file: f, ...tally(readText(d(f))) }));
|
|
76
77
|
|
|
77
78
|
let kit = null;
|
|
78
79
|
try { kit = JSON.parse(readText(d('.kit.json'))); } catch { /* not set up */ }
|
|
@@ -91,7 +92,7 @@ export function readStatus(root) {
|
|
|
91
92
|
const title = (f) => readText(d(f)).match(/^# (.+)$/m)?.[1].trim();
|
|
92
93
|
return {
|
|
93
94
|
// The project name is in AGENTS.md; in projects before 3.9.0 in CLAUDE.md (the update adds AGENTS.md with a placeholder)
|
|
94
|
-
project: [title('AGENTS.md'), title('CLAUDE.md')].find((a) => a && a !== '[PROJE ADI]') ?? title('AGENTS.md') ?? '
|
|
95
|
+
project: [title('AGENTS.md'), title('CLAUDE.md')].find((a) => a && a !== '[PROJE ADI]') ?? title('AGENTS.md') ?? t(root, 'statusRead.defaultProject'),
|
|
95
96
|
// type: null = set up by init, no type chosen yet (waiting for /start or /adopt)
|
|
96
97
|
installed: !!kit && kit.type !== null,
|
|
97
98
|
kitPending: kit ? kit.type === null : fs.existsSync(d('kit/kit.mjs')),
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Claude Code bottom status line: phase, task progress, question mode and usage limits.
|
|
3
|
-
// Example: ▶ Faz 1 · G-012 ·
|
|
3
|
+
// Example: ▶ Faz 1 · G-012 · task 7/23 · questions: open (closed: tasarim) · 5h 34% · weekly 5%
|
|
4
4
|
// Also writes the rate_limits Claude Code sends into .claude/state/usage.json (usage.mjs reads it).
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import { readStatus } from './status-read.mjs';
|
|
7
7
|
import { writeLimits, readLimits, level } from './usage.mjs';
|
|
8
|
+
import { t } from './lang.mjs';
|
|
8
9
|
|
|
9
10
|
let root = process.cwd();
|
|
10
11
|
let input = {};
|
|
@@ -34,8 +35,8 @@ function limitPart() {
|
|
|
34
35
|
const l = readLimits(root);
|
|
35
36
|
if (!l) return null;
|
|
36
37
|
const p = [];
|
|
37
|
-
if (l.five_hour) p.push(
|
|
38
|
-
if (l.seven_day) p.push(
|
|
38
|
+
if (l.five_hour) p.push(t(root, 'statusline.limit5h', { percent: Math.round(l.five_hour.percent) }));
|
|
39
|
+
if (l.seven_day) p.push(t(root, 'statusline.limitWeek', { percent: Math.round(l.seven_day.percent) }));
|
|
39
40
|
const s = level(l);
|
|
40
41
|
return p.join(' · ') + (s === 'yellow' ? ' ⚠' : s === 'red' ? ' ⛔' : '');
|
|
41
42
|
}
|
|
@@ -44,22 +45,24 @@ try {
|
|
|
44
45
|
const d = readStatus(root);
|
|
45
46
|
const parts = [];
|
|
46
47
|
if (d.kitPending) {
|
|
47
|
-
parts.push(
|
|
48
|
+
parts.push(t(root, 'statusline.notSetUp'));
|
|
48
49
|
} else {
|
|
49
50
|
const a = d.fields;
|
|
51
|
+
// The values in progress.md stay Turkish (açık / kapalı); only what the line shows is localized
|
|
50
52
|
const question = (a['Soru modu'] ?? 'açık')
|
|
53
|
+
.replace(/^(açık|kapalı)(?=$|\s)/i, (m) => t(root, m.toLowerCase() === 'açık' ? 'statusline.modeOpen' : 'statusline.modeOff'))
|
|
51
54
|
.replace(/\s*·\s*kapalı konular:\s*yok/i, '')
|
|
52
|
-
.replace(/\s*·\s*kapalı konular:\s*(.*)$/i,
|
|
55
|
+
.replace(/\s*·\s*kapalı konular:\s*(.*)$/i, (_, topics) => ` ${t(root, 'statusline.closed', { topics })}`);
|
|
53
56
|
parts.push(`▶ ${a['Aktif faz'] ?? '-'}`);
|
|
54
|
-
if (/başlangıç/i.test(a['Aktif faz'] ?? '')) parts.push(
|
|
55
|
-
else parts.push(a['Aktif görev'] ?? '-',
|
|
56
|
-
parts.push(
|
|
57
|
-
if (d.pending.length) parts.push(
|
|
57
|
+
if (/başlangıç/i.test(a['Aktif faz'] ?? '')) parts.push(t(root, 'statusline.step', { step: a['Başlangıç adımı'] ?? '-' }));
|
|
58
|
+
else parts.push(a['Aktif görev'] ?? '-', t(root, 'statusline.task', { done: d.taskCount.done, total: d.taskCount.total }));
|
|
59
|
+
parts.push(t(root, 'statusline.question', { mode: question }));
|
|
60
|
+
if (d.pending.length) parts.push(t(root, 'statusline.pending', { count: d.pending.length }));
|
|
58
61
|
}
|
|
59
62
|
const limit = limitPart();
|
|
60
63
|
if (limit) parts.push(limit);
|
|
61
|
-
if (context != null) parts.push(
|
|
64
|
+
if (context != null) parts.push(t(root, 'statusline.context', { percent: context }) + (context >= 70 ? ' ⛔' : context >= 50 ? ' ⚠' : ''));
|
|
62
65
|
console.log(parts.join(' · '));
|
|
63
66
|
} catch {
|
|
64
|
-
console.log(
|
|
67
|
+
console.log(t(root, 'statusline.unreadable'));
|
|
65
68
|
}
|
|
@@ -8,6 +8,7 @@ import fs from 'node:fs';
|
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import { execFileSync } from 'node:child_process';
|
|
10
10
|
import { readInput, decide, findRoot, isMain } from './shared.mjs';
|
|
11
|
+
import { t } from './lang.mjs';
|
|
11
12
|
|
|
12
13
|
const root = findRoot();
|
|
13
14
|
|
|
@@ -41,14 +42,13 @@ export function check(rel, map) {
|
|
|
41
42
|
if (rel.startsWith('sandbox/')) return null;
|
|
42
43
|
const denied = map.deny.find((re) => re.test(rel));
|
|
43
44
|
if (denied) {
|
|
44
|
-
return { decision: 'deny', reason: `"${rel}"
|
|
45
|
+
return { decision: 'deny', reason: `"${rel}" matches a clutter pattern (backup, old version, copy or temp file). Git history is already the backup; use sandbox/ for experiments.` };
|
|
45
46
|
}
|
|
46
47
|
if (map.allow.some((re) => re.test(rel))) return null;
|
|
47
48
|
const top = [...new Set((map.raw.allow ?? []).map((d) => d.split('/')[0]).filter((d) => !d.includes('.') || d.startsWith('.')))].slice(0, 12);
|
|
48
49
|
return {
|
|
49
50
|
decision: 'ask',
|
|
50
|
-
reason:
|
|
51
|
-
'Geçici veya deneme ise sandbox/ altına yaz. Kalıcı yeni bir yer gerekiyorsa bunu teknik karar olarak sor ve haritayı güncelle (docs/architecture/structure.md).',
|
|
51
|
+
reason: t(root, 'guard.structure.offMap', { rel, top: top.join(', ') }),
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
54
|
|
|
@@ -62,20 +62,20 @@ function hook() {
|
|
|
62
62
|
const rel = path.relative(g.root, full);
|
|
63
63
|
if (rel.startsWith('..') || path.isAbsolute(rel)) continue; // outside the project
|
|
64
64
|
const s = check(rel, map);
|
|
65
|
-
if (s) decide(g.tool, s.decision, `
|
|
65
|
+
if (s) decide(g.tool, s.decision, s.decision === 'ask' ? t(g.root, 'guard.structure.prefix', { reason: s.reason }) : `Structure: ${s.reason}`);
|
|
66
66
|
}
|
|
67
67
|
process.exit(0);
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
function checkAll() {
|
|
71
71
|
const map = readMap();
|
|
72
|
-
if (!map) { console.log('
|
|
72
|
+
if (!map) { console.log('No structure map (.claude/structure.json); check skipped.'); return; }
|
|
73
73
|
const files = execFileSync('git', ['ls-files'], { cwd: root, encoding: 'utf8' }).split('\n').filter(Boolean);
|
|
74
74
|
const violations = files.map((f) => [f, check(f, map)]).filter(([, s]) => s);
|
|
75
|
-
if (!violations.length) { console.log(`
|
|
76
|
-
console.error(`
|
|
77
|
-
for (const [f, s] of violations) console.error(` ${s.decision === 'deny' ? '
|
|
78
|
-
console.error('
|
|
75
|
+
if (!violations.length) { console.log(`Structure OK: ${files.length} files.`); return; }
|
|
76
|
+
console.error(`Structure violations (${violations.length}):`);
|
|
77
|
+
for (const [f, s] of violations) console.error(` ${s.decision === 'deny' ? 'FORBIDDEN' : 'OFF MAP'} ${f}`);
|
|
78
|
+
console.error('Move the file to the right place, put it under sandbox/, or update the map (as a technical decision).');
|
|
79
79
|
process.exit(1);
|
|
80
80
|
}
|
|
81
81
|
|
|
@@ -7,9 +7,12 @@
|
|
|
7
7
|
// test script) changes a field that could narrow the scope. A commented-out expectation counts as deleted.
|
|
8
8
|
// It does not block: the user approves a justified change.
|
|
9
9
|
// In Claude Code a test file can only be written by the test-writer subagent (agent_type); anyone else is denied.
|
|
10
|
+
// --record (PostToolUse): stores the digest of each test file test-writer wrote; commit-gate.mjs checks it.
|
|
10
11
|
import fs from 'node:fs';
|
|
11
12
|
import path from 'node:path';
|
|
12
|
-
import
|
|
13
|
+
import crypto from 'node:crypto';
|
|
14
|
+
import { readInput, decide, isMain, findRoot } from './shared.mjs';
|
|
15
|
+
import { t } from './lang.mjs';
|
|
13
16
|
|
|
14
17
|
const TEST = /(^|\/)(tests?|__tests__|e2e|spec)\/|\.(test|spec|e2e-spec)\.[cm]?[jt]sx?$|(^|\/)test_[^/]*\.py$|_test\.(py|go)$/i;
|
|
15
18
|
// Windows resolves "a.test.ts.", "a.test.ts " and "a.test.ts::$DATA" (alternate stream) to the same file
|
|
@@ -35,9 +38,9 @@ const count = (s, re) => (stripComments(s).match(re) ?? []).length;
|
|
|
35
38
|
|
|
36
39
|
// Returns: null (no problem) | a description of the weakening
|
|
37
40
|
export function weakening(before, after) {
|
|
38
|
-
if (count(after, SKIP) > count(before, SKIP)) return 'test
|
|
39
|
-
if (count(after, EXPECT) < count(before, EXPECT)) return
|
|
40
|
-
if (count(after, WEAK) > count(before, WEAK)) return
|
|
41
|
+
if (count(after, SKIP) > count(before, SKIP)) return t(findRoot(), 'guard.test.skip');
|
|
42
|
+
if (count(after, EXPECT) < count(before, EXPECT)) return t(findRoot(), 'guard.test.expectRemoved');
|
|
43
|
+
if (count(after, WEAK) > count(before, WEAK)) return t(findRoot(), 'guard.test.expectWeak');
|
|
41
44
|
return null;
|
|
42
45
|
}
|
|
43
46
|
|
|
@@ -45,9 +48,17 @@ export function weakening(before, after) {
|
|
|
45
48
|
export function configRisk(before, after) {
|
|
46
49
|
const a = new Set(before.split('\n')), b = new Set(after.split('\n'));
|
|
47
50
|
const changed = [...a].filter((s) => !b.has(s)).concat([...b].filter((s) => !a.has(s)));
|
|
48
|
-
return changed.some((s) => RISKY.test(s)) ?
|
|
51
|
+
return changed.some((s) => RISKY.test(s)) ? t(findRoot(), 'guard.test.configRisk') : null;
|
|
49
52
|
}
|
|
50
53
|
|
|
54
|
+
// The real path too: a link or junction into the test folder, a Windows short name (TESTS~1)
|
|
55
|
+
// A new nested path (lnk/new/sub/x.ts): resolve the nearest folder that exists and append the rest
|
|
56
|
+
const realOf = (p) => {
|
|
57
|
+
for (let dir = p, rest = ''; ; rest = path.join(path.basename(dir), rest), dir = path.dirname(dir)) {
|
|
58
|
+
try { return path.join(fs.realpathSync.native(dir), rest); } catch { if (path.dirname(dir) === dir) return p; }
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
51
62
|
function hook() {
|
|
52
63
|
const g = readInput();
|
|
53
64
|
if (!g) process.exit(0);
|
|
@@ -56,13 +67,6 @@ function hook() {
|
|
|
56
67
|
const rel = path.relative(g.root, full);
|
|
57
68
|
return [full, rel.startsWith('..') || path.isAbsolute(rel) ? full : rel];
|
|
58
69
|
};
|
|
59
|
-
// The real path too: a link or junction into the test folder, a Windows short name (TESTS~1)
|
|
60
|
-
// A new nested path (lnk/new/sub/x.ts): resolve the nearest folder that exists and append the rest
|
|
61
|
-
const realOf = (p) => {
|
|
62
|
-
for (let dir = p, rest = ''; ; rest = path.join(path.basename(dir), rest), dir = path.dirname(dir)) {
|
|
63
|
-
try { return path.join(fs.realpathSync.native(dir), rest); } catch { if (path.dirname(dir) === dir) return p; }
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
70
|
for (const f of g.files) {
|
|
67
71
|
if (!f.path) continue;
|
|
68
72
|
const [full, name] = resolveName(f.path);
|
|
@@ -73,24 +77,61 @@ function hook() {
|
|
|
73
77
|
// Tests are written by the test-writer agent only (AGENTS.md rule 7): whoever writes the code must not shape the test
|
|
74
78
|
// to it. Only Claude Code reports the subagent; Codex and Antigravity keep the rule in text. Config files stay a question.
|
|
75
79
|
if (g.tool === 'claude' && g.agent !== 'test-writer' && !config) {
|
|
76
|
-
decide(g.tool, 'deny', `Test
|
|
77
|
-
'
|
|
80
|
+
decide(g.tool, 'deny', `Test separation: ${name.split(path.sep).join('/')} is a test file; only the test-writer agent writes test files, and whoever writes the code cannot change tests (AGENTS.md rule 7). ` +
|
|
81
|
+
'If a test must be added or changed, call the test-writer agent (continue it with SendMessage if it exists); if the criterion changes, ask the user.');
|
|
78
82
|
}
|
|
79
83
|
let before = f.before;
|
|
80
84
|
if (before === undefined) { try { before = fs.readFileSync(full, 'utf8'); } catch { before = ''; } }
|
|
81
85
|
const after = f.after ?? f.text;
|
|
82
86
|
let reason;
|
|
83
|
-
if (f.deleted) reason = config ? 'test
|
|
87
|
+
if (f.deleted) reason = t(g.root, config ? 'guard.test.configDeleted' : 'guard.test.fileDeleted');
|
|
84
88
|
// Codex Move: a test moved to a path that does not run is as good as deleted; moving onto an existing file overwrites it
|
|
85
|
-
else if (source && (!isTestFile(name) || (TEST_NAME.test(path.basename(source)) && !TEST_NAME.test(path.basename(name))))) reason =
|
|
86
|
-
else if (f.source && fs.existsSync(full)) reason =
|
|
89
|
+
else if (source && (!isTestFile(name) || (TEST_NAME.test(path.basename(source)) && !TEST_NAME.test(path.basename(name))))) reason = t(g.root, 'guard.test.movedAway', { source: f.source });
|
|
90
|
+
else if (f.source && fs.existsSync(full)) reason = t(g.root, 'guard.test.moveOverwrite', { source: f.source, path: f.path });
|
|
87
91
|
else reason = config ? configRisk(before, after) : weakening(before, after);
|
|
88
92
|
if (reason) {
|
|
89
|
-
decide(g.tool, 'ask',
|
|
90
|
-
|
|
93
|
+
decide(g.tool, 'ask', t(g.root, 'guard.test.ask', { name: name.split(path.sep).join('/'), reason }));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
process.exit(0);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// What test-writer wrote: the commit gate stops a changed test file whose content on disk is not one of these
|
|
100
|
+
// (git apply, patch or a script writes past the PreToolUse guards). State: .claude/state/test-writer.json → { files: { path: digest } }
|
|
101
|
+
export const digest = (buf) => crypto.createHash('sha256').update(buf).digest('hex').slice(0, 16);
|
|
102
|
+
export const recordPath = (root) => path.join(root, '.claude', 'state', 'test-writer.json');
|
|
103
|
+
export function readRecord(root) {
|
|
104
|
+
try {
|
|
105
|
+
const d = JSON.parse(fs.readFileSync(recordPath(root), 'utf8'));
|
|
106
|
+
return d?.files && typeof d.files === 'object' && !Array.isArray(d.files) ? d.files : {};
|
|
107
|
+
} catch { return {}; }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// PostToolUse: the file is on disk now, so its digest is what test-writer left
|
|
111
|
+
function record() {
|
|
112
|
+
const g = readInput();
|
|
113
|
+
if (!g || g.tool !== 'claude' || g.agent !== 'test-writer') process.exit(0);
|
|
114
|
+
const files = readRecord(g.root);
|
|
115
|
+
const rootReal = realOf(g.root);
|
|
116
|
+
for (const f of g.files) {
|
|
117
|
+
if (!f.path) continue;
|
|
118
|
+
const full = path.resolve(g.dir, f.path);
|
|
119
|
+
let body;
|
|
120
|
+
try { body = fs.readFileSync(full); } catch { continue; }
|
|
121
|
+
// The written path and its real path (a link into the test folder): git lists the one it tracks
|
|
122
|
+
for (const rel of [path.relative(g.root, full), path.relative(rootReal, realOf(full))]) {
|
|
123
|
+
const key = rel.split(path.sep).join('/');
|
|
124
|
+
if (!key.startsWith('..') && !path.isAbsolute(rel) && isTestFile(key) && !isTestConfig(key)) files[key] = digest(body);
|
|
91
125
|
}
|
|
92
126
|
}
|
|
127
|
+
// Temp file + rename: a reader never sees half a JSON (it would read {} and drop every record)
|
|
128
|
+
const tmp = `${recordPath(g.root)}.${process.pid}.tmp`;
|
|
129
|
+
try {
|
|
130
|
+
fs.mkdirSync(path.dirname(tmp), { recursive: true });
|
|
131
|
+
fs.writeFileSync(tmp, JSON.stringify({ files }, null, 2));
|
|
132
|
+
fs.renameSync(tmp, recordPath(g.root));
|
|
133
|
+
} catch { fs.rmSync(tmp, { force: true }); /* the commit gate then asks for test-writer again */ }
|
|
93
134
|
process.exit(0);
|
|
94
135
|
}
|
|
95
136
|
|
|
96
|
-
if (isMain(import.meta.url)) hook();
|
|
137
|
+
if (isMain(import.meta.url)) (process.argv.includes('--record') ? record : hook)();
|
package/.claude/hooks/tools.mjs
CHANGED
|
@@ -17,41 +17,41 @@ import { findRoot, isMain } from './shared.mjs';
|
|
|
17
17
|
// Keys are the package names users type (/tools ac tasarim): they stay Turkish
|
|
18
18
|
export const PACKAGES = {
|
|
19
19
|
tasarim: {
|
|
20
|
-
description: '
|
|
20
|
+
description: 'Style directions, palette and typefaces, GSAP',
|
|
21
21
|
command: '/design',
|
|
22
22
|
plugins: ['taste-skill@taste-skill', 'ui-ux-pro-max@ui-ux-pro-max-skill', 'gsap-skills@gsap-skills'],
|
|
23
23
|
},
|
|
24
24
|
guvenlik: {
|
|
25
|
-
description: '
|
|
25
|
+
description: 'Deep security scan',
|
|
26
26
|
command: '/findings-security, /release',
|
|
27
27
|
plugins: ['differential-review', 'insecure-defaults', 'sharp-edges', 'static-analysis', 'supply-chain-risk-auditor', 'agentic-actions-auditor', 'fp-check']
|
|
28
28
|
.map((e) => `${e}@trailofbits`).concat('claude-security@claude-plugins-official'),
|
|
29
29
|
},
|
|
30
30
|
yayin: {
|
|
31
|
-
description: '
|
|
31
|
+
description: 'Code map for humans, session report',
|
|
32
32
|
command: '/release, /handoff, /adopt',
|
|
33
33
|
plugins: ['understand-anything@understand-anything', 'session-report@claude-plugins-official'],
|
|
34
34
|
},
|
|
35
|
-
video: { description: '
|
|
35
|
+
video: { description: 'Video with Remotion', command: 'motion-video', plugins: ['remotion@remotion'] },
|
|
36
36
|
};
|
|
37
37
|
|
|
38
38
|
// Command descriptions are also in the skill list. Light: non-core commands are listed by name only (skillOverrides "name-only"),
|
|
39
39
|
// still in the / menu and callable by name. Kit settings.json ships this list; a test keeps the two in sync.
|
|
40
40
|
export const CORE_COMMANDS = ['start', 'adopt', 'build', 'fix-bug', 'plan-task', 'design', 'tools', 'review-changes', 'wrap-up', 'release'];
|
|
41
41
|
export const LIGHT_COMMANDS = ['ask-mode', 'board', 'dashboard', 'findings-all', 'findings-business', 'findings-legal', 'findings-perf',
|
|
42
|
-
'findings-security', 'findings-tech', 'findings-ux', 'freeze', 'handoff', 'kit-update', 'maintain', 'qa', 'research', 'retro', 'rollback', 'user-feedback'];
|
|
42
|
+
'findings-security', 'findings-tech', 'findings-ux', 'freeze', 'handoff', 'kit-update', 'language', 'maintain', 'qa', 'research', 'retro', 'rollback', 'user-feedback'];
|
|
43
43
|
|
|
44
44
|
const settingsPaths = (root) => ({ project: path.join(root, '.claude', 'settings.json'), local: path.join(root, '.claude', 'settings.local.json') });
|
|
45
45
|
|
|
46
46
|
function readJson(p) {
|
|
47
47
|
if (!fs.existsSync(p)) return {};
|
|
48
48
|
try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch {
|
|
49
|
-
throw new Error(`${p}
|
|
49
|
+
throw new Error(`${p} is not valid JSON; fix it before changing a package.`);
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
function pkg(name) {
|
|
54
|
-
if (!Object.hasOwn(PACKAGES, name ?? '')) throw new Error(`
|
|
54
|
+
if (!Object.hasOwn(PACKAGES, name ?? '')) throw new Error(`Unknown package: ${name}. Options: ${Object.keys(PACKAGES).join(', ')}`);
|
|
55
55
|
return PACKAGES[name];
|
|
56
56
|
}
|
|
57
57
|
|
|
@@ -84,12 +84,12 @@ export function setPackage(root, name, enabled) {
|
|
|
84
84
|
// Writes the shared project settings (the profile is team-wide). Only kit light commands without a value change; the user's
|
|
85
85
|
// own values (another skill, or "on"/"off" on a kit command) stay. full keeps an empty key so the update does not add the list back.
|
|
86
86
|
export function setCommandMode(root, mode) {
|
|
87
|
-
if (mode !== 'light' && mode !== 'full') throw new Error(`
|
|
87
|
+
if (mode !== 'light' && mode !== 'full') throw new Error(`Unknown mode: ${mode}. Options: light, full`);
|
|
88
88
|
const file = settingsPaths(root).project;
|
|
89
|
-
if (!fs.existsSync(file)) throw new Error(`${file}
|
|
89
|
+
if (!fs.existsSync(file)) throw new Error(`${file} does not exist; the command list was not written.`);
|
|
90
90
|
const s = readJson(file);
|
|
91
91
|
const isObject = (x) => x && typeof x === 'object' && !Array.isArray(x);
|
|
92
|
-
if (!isObject(s) || (s.skillOverrides !== undefined && !isObject(s.skillOverrides))) throw new Error(`${file} → skillOverrides
|
|
92
|
+
if (!isObject(s) || (s.skillOverrides !== undefined && !isObject(s.skillOverrides))) throw new Error(`${file} → skillOverrides is not an object; fix it before writing.`);
|
|
93
93
|
const o = { ...(s.skillOverrides ?? {}) };
|
|
94
94
|
for (const c of LIGHT_COMMANDS) {
|
|
95
95
|
if (mode === 'light' && o[c] === undefined) o[c] = 'name-only';
|
|
@@ -139,57 +139,56 @@ export function measure() {
|
|
|
139
139
|
const installedList = () => Object.entries(installRecords()).flatMap(([id, k]) => [k].flat().map((e) => ({ id, scope: e?.scope, projectPath: e?.projectPath })));
|
|
140
140
|
const tokenOf = (measurement, ids) => {
|
|
141
141
|
const found = measurement.filter((o) => ids.includes(o.id));
|
|
142
|
-
return found.length ? `~${found.reduce((s, o) => s + o.skillTokens + o.agentTokens, 0)} token` : '? token (
|
|
142
|
+
return found.length ? `~${found.reduce((s, o) => s + o.skillTokens + o.agentTokens, 0)} token` : '? token (not installed)';
|
|
143
143
|
};
|
|
144
|
-
const NOTE = '
|
|
144
|
+
const NOTE = 'After enabling, run /reload-plugins. Enabling mid-session breaks the prompt cache; best at a phase boundary, together with /clear.';
|
|
145
145
|
if (isMain(import.meta.url)) {
|
|
146
146
|
const root = findRoot();
|
|
147
147
|
const [flag, name] = process.argv.slice(2);
|
|
148
148
|
try {
|
|
149
149
|
if (flag === '--enable' || flag === '--disable') {
|
|
150
150
|
const ids = setPackage(root, name, flag === '--enable');
|
|
151
|
-
if (!ids.length) { console.log(
|
|
152
|
-
console.log(
|
|
151
|
+
if (!ids.length) { console.log(`The plugins of package ${name} are not defined in this project; no change.`); process.exit(0); }
|
|
152
|
+
console.log(`Package ${name} ${flag === '--enable' ? 'enabled' : 'back to the project default'} (.claude/settings.local.json): ${ids.join(', ')}`);
|
|
153
153
|
const missing = missingPlugins(Object.fromEntries(ids.map((id) => [id, true])), installedList(), root).map((e) => e.id);
|
|
154
|
-
if (flag === '--enable' && missing.length) console.log(`
|
|
154
|
+
if (flag === '--enable' && missing.length) console.log(`Not installed: ${missing.join(', ')}. Ask the user; if they approve: node .claude/hooks/plugins-sync.mjs --install`);
|
|
155
155
|
console.log(NOTE);
|
|
156
156
|
} else if (flag === '--required') {
|
|
157
157
|
pkg(name);
|
|
158
158
|
const d = packageStatus(root).find((x) => x.name === name);
|
|
159
|
-
if (!d.plugins.length) { console.log(
|
|
160
|
-
if (d.enabled) { console.log(
|
|
161
|
-
console.log(
|
|
162
|
-
`
|
|
159
|
+
if (!d.plugins.length) { console.log(`Package ${name} is not in this project; skip the step.`); process.exit(0); }
|
|
160
|
+
if (d.enabled) { console.log(`Package ${name} is enabled.`); process.exit(0); }
|
|
161
|
+
console.log(`Package ${name} is disabled (${tokenOf(measure(), d.plugins)}/session: ${d.plugins.join(', ')}). ` +
|
|
162
|
+
`Ask the user; if they approve: node .claude/hooks/tools.mjs --enable ${name}, then /reload-plugins. If they decline, do the step without the package.`);
|
|
163
163
|
process.exit(2);
|
|
164
164
|
} else if (flag === '--measure') {
|
|
165
165
|
const project = readJson(settingsPaths(root).project).enabledPlugins ?? {};
|
|
166
166
|
const effective = { ...project, ...(readJson(settingsPaths(root).local).enabledPlugins ?? {}) };
|
|
167
|
-
const packageOf = (id) => Object.keys(PACKAGES).find((a) => PACKAGES[a].plugins.includes(id)) ?? (id in project ? '
|
|
167
|
+
const packageOf = (id) => Object.keys(PACKAGES).find((a) => PACKAGES[a].plugins.includes(id)) ?? (id in project ? 'core' : '-');
|
|
168
168
|
const rows = measure().map((o) => ({ plugin: o.id, pkg: packageOf(o.id), enabled: effective[o.id] === true, skill: o.skill, descTokens: o.skillTokens, agentTokens: o.agentTokens }))
|
|
169
169
|
.sort((a, b) => b.descTokens + b.agentTokens - a.descTokens - a.agentTokens);
|
|
170
|
-
|
|
171
|
-
console.table(rows.map((r) => ({ eklenti: r.plugin, paket: r.pkg, açık: r.enabled, skill: r.skill, açıklamaTok: r.descTokens, ajanTok: r.agentTokens })));
|
|
170
|
+
console.table(rows.map((r) => ({ plugin: r.plugin, package: r.pkg, enabled: r.enabled, skill: r.skill, descTokens: r.descTokens, agentTokens: r.agentTokens })));
|
|
172
171
|
const top = (l) => l.reduce((s, r) => s + r.descTokens + r.agentTokens, 0);
|
|
173
|
-
console.log(`
|
|
172
|
+
console.log(`Enabled plugins: ~${top(rows.filter((r) => r.enabled))} tokens · if all were enabled: ~${top(rows)} tokens. Exact value: /context, /doctor.`);
|
|
174
173
|
} else if (flag === '--commands') {
|
|
175
174
|
setCommandMode(root, name);
|
|
176
175
|
console.log(name === 'light'
|
|
177
|
-
? `
|
|
178
|
-
: '
|
|
176
|
+
? `Command list is light: ${LIGHT_COMMANDS.length} commands are listed by name only (they stay in the / menu). Takes effect in a new session.`
|
|
177
|
+
: 'Command list is full: all commands are listed with their descriptions. Takes effect in a new session.');
|
|
179
178
|
} else if (!flag) {
|
|
180
179
|
const state = packageStatus(root);
|
|
181
180
|
const project = readJson(settingsPaths(root).project).enabledPlugins ?? {};
|
|
182
181
|
const packaged = new Set(Object.values(PACKAGES).flatMap((p) => p.plugins));
|
|
183
|
-
console.log(
|
|
182
|
+
console.log(`Core (always on): ${Object.keys(project).filter((id) => project[id] !== false && !packaged.has(id)).join(', ') || '-'}`);
|
|
184
183
|
const measurement = measure();
|
|
185
184
|
for (const d of state.filter((x) => x.plugins.length)) {
|
|
186
|
-
console.log(` ${d.name.padEnd(9)} ${d.enabled ? '
|
|
185
|
+
console.log(` ${d.name.padEnd(9)} ${d.enabled ? 'on ' : 'off'} ${tokenOf(measurement, d.plugins).padEnd(24)} ${d.command} · ${d.description}`);
|
|
187
186
|
}
|
|
188
187
|
const cm = commandMode(root);
|
|
189
|
-
console.log(`
|
|
188
|
+
console.log(`Command list: ${cm.mode} (${cm.named}/${LIGHT_COMMANDS.length} commands by name only; to change: --commands light|full)`);
|
|
190
189
|
console.log(NOTE);
|
|
191
190
|
} else {
|
|
192
|
-
console.error('
|
|
191
|
+
console.error('Usage: tools.mjs [--enable <package> | --disable <package> | --required <package> | --measure | --commands light|full]');
|
|
193
192
|
process.exit(1);
|
|
194
193
|
}
|
|
195
194
|
} catch (h) {
|
package/.claude/hooks/usage.mjs
CHANGED
|
@@ -7,11 +7,13 @@
|
|
|
7
7
|
import fs from 'node:fs';
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import { findRoot, isMain } from './shared.mjs';
|
|
10
|
+
import { t, readLang } from './lang.mjs';
|
|
10
11
|
|
|
11
12
|
export const THRESHOLDS = { five_hour: { yellow: 60, red: 85 }, seven_day: { yellow: 70, red: 90 } };
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
// Window → message key of its name
|
|
14
|
+
const WINDOWS = { five_hour: 'usage.window5h', seven_day: 'usage.windowWeek' };
|
|
15
|
+
// Plain level names (flow.mjs, read by the AI); summary() shows them in the project language through usage.level.* keys
|
|
16
|
+
export const LEVEL_LABEL = { green: 'green', yellow: 'yellow', red: 'red', unknown: 'unknown' };
|
|
15
17
|
const file = (root) => path.join(root, '.claude', 'state', 'usage.json');
|
|
16
18
|
|
|
17
19
|
export function writeLimits(root, rateLimits, now = Date.now()) {
|
|
@@ -50,7 +52,7 @@ export function readLimits(root, now = Date.now()) {
|
|
|
50
52
|
return data;
|
|
51
53
|
}
|
|
52
54
|
|
|
53
|
-
// green | yellow | red | unknown (LEVEL_LABEL has the
|
|
55
|
+
// green | yellow | red | unknown (LEVEL_LABEL has the plain label)
|
|
54
56
|
export function level(data) {
|
|
55
57
|
if (!data) return 'unknown';
|
|
56
58
|
let s = 'green';
|
|
@@ -63,23 +65,24 @@ export function level(data) {
|
|
|
63
65
|
return s;
|
|
64
66
|
}
|
|
65
67
|
|
|
66
|
-
function timeLeft(resetsAt, now) {
|
|
68
|
+
function timeLeft(resetsAt, now, root) {
|
|
67
69
|
if (!resetsAt) return '';
|
|
68
70
|
const min = Math.max(0, Math.round((resetsAt * 1000 - now) / 60000));
|
|
69
|
-
const clock = new Date(resetsAt * 1000).toLocaleString('tr-TR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' });
|
|
70
|
-
const left = min >= 1440
|
|
71
|
-
|
|
71
|
+
const clock = new Date(resetsAt * 1000).toLocaleString(readLang(root) === 'tr' ? 'tr-TR' : 'en-GB', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' });
|
|
72
|
+
const left = min >= 1440
|
|
73
|
+
? t(root, 'usage.leftDays', { days: Math.floor(min / 1440), hours: Math.floor((min % 1440) / 60) })
|
|
74
|
+
: t(root, 'usage.leftHours', { hours: Math.floor(min / 60), minutes: min % 60 });
|
|
75
|
+
return t(root, 'usage.resets', { clock, left });
|
|
72
76
|
}
|
|
73
77
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
+
// The dashboard shows this text to the user, so it follows the project language (root)
|
|
79
|
+
export function summary(data, now = Date.now(), root = findRoot()) {
|
|
80
|
+
if (!data) return t(root, 'usage.none');
|
|
78
81
|
const parts = Object.entries(WINDOWS)
|
|
79
82
|
.filter(([k]) => data[k])
|
|
80
|
-
.map(([k, name]) =>
|
|
81
|
-
const stale = data.ageMin > 30 ?
|
|
82
|
-
return
|
|
83
|
+
.map(([k, name]) => t(root, 'usage.windowPart', { name: t(root, name), percent: Math.round(data[k].percent), tail: data[k].renewed ? ` ${t(root, 'usage.renewed')}` : ` (${timeLeft(data[k].resetsAt, now, root)})` }));
|
|
84
|
+
const stale = data.ageMin > 30 ? t(root, 'usage.stale') : '';
|
|
85
|
+
return t(root, 'usage.summary', { parts: parts.join(' · '), level: t(root, `usage.level.${level(data)}`), age: data.ageMin, stale });
|
|
83
86
|
}
|
|
84
87
|
|
|
85
88
|
if (isMain(import.meta.url)) {
|
|
@@ -1,47 +1,47 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: anlatici
|
|
3
|
-
description:
|
|
3
|
+
description: Narrator mode. Explains every change and error in plain language in the conversation language, as if to someone who knows no software. Coding behavior does not change.
|
|
4
4
|
keep-coding-instructions: true
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
#
|
|
7
|
+
# Narrator mode
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
The user knows no software or is just learning. Do the work as usual, but explain what you did so the user understands it and can explain it to someone else if needed. Write explanations in plain language in the conversation language, in full sentences. The report language shortening rules (caveman) do not apply to these explanations; they apply only to the status block.
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## After every work step
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
If you changed code or settings, write this block at the end of your answer (before the status block):
|
|
14
14
|
|
|
15
15
|
```
|
|
16
|
-
──
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
── What happened? ───────────────
|
|
17
|
+
What I did: <1-2 sentences in everyday language; say what a file is for, not its name: "sign-up form", "login page">
|
|
18
|
+
Why: <what problem the user would have had without it>
|
|
19
|
+
How you see it: <the place to click, the page to open or the single command to run>
|
|
20
|
+
New concept: <a technical term that appears for the first time in this step: "term: one-sentence explanation"; leave the line out if there is none>
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
##
|
|
23
|
+
## Understanding check at the end of a task
|
|
24
24
|
|
|
25
|
-
`/build`
|
|
25
|
+
After the `/build` or `/fix-bug` report (before the status block), ask at most 2 questions about the change in this task. The goal is not to give an exam but for the user to understand what was done and why.
|
|
26
26
|
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
27
|
+
- Ask with AskUserQuestion. Give 3 options: one correct, two plausible but wrong. Shuffle the position of the correct option and do not write "(Recommended)". Make the last option "Not now".
|
|
28
|
+
- Make the question of the type "Why was this change needed?" or "What would happen without it?". Do not ask about file names or code details.
|
|
29
|
+
- Respond to a correct answer in one sentence. For a wrong answer, explain the correct one in 1-2 sentences and do not make a big deal of the mistake.
|
|
30
|
+
- Add every answered question as one row to the "Kayıtlar" table in `docs/workflow/learning.md`: `| YYYY-AA-GG | <task id> | <concept, 1-3 words> | doğru |` (or `yanlış`). Do not write "Not now" and unanswered questions. If the file does not exist, create it with the same heading and columns.
|
|
31
31
|
|
|
32
|
-
##
|
|
32
|
+
## When an error occurs
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
Do not copy the error message as it is. Explain it like this:
|
|
35
35
|
|
|
36
|
-
1. **
|
|
37
|
-
2. **
|
|
38
|
-
3. **
|
|
36
|
+
1. **What happened:** The everyday-language equivalent of the error. Example: "The app could not connect to the database because the connection address is missing."
|
|
37
|
+
2. **Is it your fault:** Does the user need to do something, or will you make the fix?
|
|
38
|
+
3. **What we do:** The fix step. If the user has to do something (a setting in a panel, entering a password), write it step by step, one at a time.
|
|
39
39
|
|
|
40
|
-
##
|
|
40
|
+
## Rules
|
|
41
41
|
|
|
42
|
-
-
|
|
43
|
-
-
|
|
44
|
-
-
|
|
45
|
-
-
|
|
46
|
-
-
|
|
47
|
-
-
|
|
42
|
+
- Explain a technical term in one sentence the first time you use it. Example: "a commit is a saved snapshot of the changes we made". Do not explain it again on later uses.
|
|
43
|
+
- You may use analogies, but do not simplify so much that it leads to misunderstanding.
|
|
44
|
+
- When asking for a decision (AskUserQuestion), write the "what changes if you choose this" part of each option's description in non-technical language.
|
|
45
|
+
- Do not explain at length: each block is 4-5 lines at most. If the user wants more, go into detail.
|
|
46
|
+
- Do not present not knowing something as a shortcoming. Do not use phrases like "you probably know this already".
|
|
47
|
+
- If something breaks, remind the user of the way back to the last working state: `/rollback`.
|
|
@@ -4,17 +4,17 @@
|
|
|
4
4
|
"rule_name": "supabase_service_role_client",
|
|
5
5
|
"regex": "(service_role|SUPABASE_SERVICE_ROLE|SUPABASE_SECRET_KEY|sb_secret_)",
|
|
6
6
|
"paths": ["**/src/app/**/*.tsx", "**/src/components/**", "**/src/features/**/components/**", "**/app/**/*.tsx"],
|
|
7
|
-
"reminder": "Supabase
|
|
7
|
+
"reminder": "The Supabase secret key (sb_secret_…, the old service_role) must not go into client code; it bypasses RLS. Move it to the server side (server action, route handler, edge function)."
|
|
8
8
|
},
|
|
9
9
|
{
|
|
10
10
|
"rule_name": "public_env_secret",
|
|
11
11
|
"regex": "(NEXT_PUBLIC|EXPO_PUBLIC)_[A-Z_]*(SECRET|SERVICE_ROLE|PRIVATE)",
|
|
12
|
-
"reminder": "NEXT_PUBLIC_ / EXPO_PUBLIC_
|
|
12
|
+
"reminder": "NEXT_PUBLIC_ / EXPO_PUBLIC_ variables are embedded in the browser and the app bundle. Never define a secret value with this prefix."
|
|
13
13
|
},
|
|
14
14
|
{
|
|
15
15
|
"rule_name": "personal_data_log",
|
|
16
16
|
"regex": "console\\.(log|info)\\([^)]*(email|phone|tckn|tc_kimlik|password|token)",
|
|
17
|
-
"reminder": "
|
|
17
|
+
"reminder": "Do not log personal data or credentials (KVKK, the Turkish data protection law). Mask the identity or log only the record id."
|
|
18
18
|
}
|
|
19
19
|
]
|
|
20
20
|
}
|