tillerhand 3.43.0 → 3.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 -22
- 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 +38 -37
- 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 +12 -12
- 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 +2 -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 +876 -841
- 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
package/.claude/hooks/notify.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import fs from 'node:fs';
|
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
import { spawn } from 'node:child_process';
|
|
9
9
|
import { findRoot, isMain } from './shared.mjs';
|
|
10
|
+
import { t } from './lang.mjs';
|
|
10
11
|
|
|
11
12
|
export const THRESHOLD_SEC = 90;
|
|
12
13
|
|
|
@@ -39,9 +40,9 @@ function main() {
|
|
|
39
40
|
try { const r = JSON.parse(fs.readFileSync(path.join(root, '.claude', 'state', 'reminder.json'), 'utf8')); startedAt = r.turnAt ?? r.tur ?? 0; } catch { /* */ } // tur: key before 3.15.0
|
|
40
41
|
const duration = (Date.now() - startedAt) / 1000;
|
|
41
42
|
if (!startedAt || duration < THRESHOLD_SEC) process.exit(0);
|
|
42
|
-
message =
|
|
43
|
+
message = t(root, 'notify.done', { minutes: Math.round(duration / 60) });
|
|
43
44
|
} else {
|
|
44
|
-
message = input.message || '
|
|
45
|
+
message = input.message || t(root, 'notify.waiting');
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
const title = `Claude Code · ${project}`.slice(0, 60);
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
|
|
8
8
|
import fs from 'node:fs';
|
|
9
9
|
import path from 'node:path';
|
|
10
|
-
import { readInput, decide, isMain } from './shared.mjs';
|
|
10
|
+
import { readInput, decide, isMain, findRoot } from './shared.mjs';
|
|
11
|
+
import { t } from './lang.mjs';
|
|
11
12
|
|
|
12
13
|
export const THRESHOLD = { newDays: 30, npmWeeklyDownloads: 500 };
|
|
13
14
|
|
|
@@ -148,12 +149,13 @@ const daysAgo = (date, now) => Math.floor((now - new Date(date).getTime()) / 864
|
|
|
148
149
|
// get: a fetch-like function (mocked in tests)
|
|
149
150
|
// status: missing | unknown | similar | new | low | ok
|
|
150
151
|
export async function check({ eco, name }, get = fetch, now = Date.now()) {
|
|
152
|
+
const tr = (k, vars) => t(findRoot(), k, vars);
|
|
151
153
|
const similar = similarPopular(eco, name);
|
|
152
154
|
try {
|
|
153
155
|
if (eco === 'npm') {
|
|
154
156
|
const r = await get(`https://registry.npmjs.org/${name.replace('/', '%2f')}`, { signal: AbortSignal.timeout(6000) });
|
|
155
|
-
if (r.status === 404) return { status: 'missing', detail: '
|
|
156
|
-
if (!r.ok) return { status: 'unknown', detail:
|
|
157
|
+
if (r.status === 404) return { status: 'missing', detail: tr('guard.package.npmMissing') };
|
|
158
|
+
if (!r.ok) return { status: 'unknown', detail: tr('guard.package.npmStatus', { status: r.status }) };
|
|
157
159
|
const j = await r.json();
|
|
158
160
|
const age = j.time?.created ? daysAgo(j.time.created, now) : null;
|
|
159
161
|
let downloads = null;
|
|
@@ -161,22 +163,22 @@ export async function check({ eco, name }, get = fetch, now = Date.now()) {
|
|
|
161
163
|
const dr = await get(`https://api.npmjs.org/downloads/point/last-week/${name}`, { signal: AbortSignal.timeout(6000) });
|
|
162
164
|
if (dr.ok) downloads = (await dr.json()).downloads ?? null;
|
|
163
165
|
} catch { /* download count is optional */ }
|
|
164
|
-
if (similar) return { status: 'similar', detail:
|
|
165
|
-
if (age != null && age < THRESHOLD.newDays) return { status: 'new', detail:
|
|
166
|
-
if (downloads != null && downloads < THRESHOLD.npmWeeklyDownloads) return { status: 'low', detail:
|
|
167
|
-
return { status: 'ok', detail: `${age ?? '?'}
|
|
166
|
+
if (similar) return { status: 'similar', detail: tr('guard.package.similar', { similar }) };
|
|
167
|
+
if (age != null && age < THRESHOLD.newDays) return { status: 'new', detail: tr('guard.package.newNpm', { age }) };
|
|
168
|
+
if (downloads != null && downloads < THRESHOLD.npmWeeklyDownloads) return { status: 'low', detail: tr('guard.package.lowDownloads', { downloads }) };
|
|
169
|
+
return { status: 'ok', detail: `${age ?? '?'} days, ${downloads ?? '?'} weekly downloads` };
|
|
168
170
|
}
|
|
169
171
|
const r = await get(`https://pypi.org/pypi/${encodeURIComponent(name)}/json`, { signal: AbortSignal.timeout(6000) });
|
|
170
|
-
if (r.status === 404) return { status: 'missing', detail: '
|
|
171
|
-
if (!r.ok) return { status: 'unknown', detail:
|
|
172
|
+
if (r.status === 404) return { status: 'missing', detail: tr('guard.package.pypiMissing') };
|
|
173
|
+
if (!r.ok) return { status: 'unknown', detail: tr('guard.package.pypiStatus', { status: r.status }) };
|
|
172
174
|
const j = await r.json();
|
|
173
175
|
const dates = Object.values(j.releases ?? {}).flat().map((f) => f.upload_time_iso_8601).filter(Boolean).sort();
|
|
174
176
|
const age = dates.length ? daysAgo(dates[0], now) : null;
|
|
175
|
-
if (similar) return { status: 'similar', detail:
|
|
176
|
-
if (age != null && age < THRESHOLD.newDays) return { status: 'new', detail:
|
|
177
|
-
return { status: 'ok', detail: `${age ?? '?'}
|
|
177
|
+
if (similar) return { status: 'similar', detail: tr('guard.package.similar', { similar }) };
|
|
178
|
+
if (age != null && age < THRESHOLD.newDays) return { status: 'new', detail: tr('guard.package.newPypi', { age }) };
|
|
179
|
+
return { status: 'ok', detail: `${age ?? '?'} days` };
|
|
178
180
|
} catch (e) {
|
|
179
|
-
return { status: 'unknown', detail:
|
|
181
|
+
return { status: 'unknown', detail: tr(e.name === 'TimeoutError' ? 'guard.package.timeout' : 'guard.package.network') };
|
|
180
182
|
}
|
|
181
183
|
}
|
|
182
184
|
|
|
@@ -192,11 +194,11 @@ async function hook() {
|
|
|
192
194
|
const results = await Promise.all(packages.map(async (p) => ({ ...p, ...(await check(p)) })));
|
|
193
195
|
const missing = results.filter((s) => s.status === 'missing' && !s.unsure);
|
|
194
196
|
if (missing.length) {
|
|
195
|
-
decide(g.tool, 'deny', `
|
|
197
|
+
decide(g.tool, 'deny', `Package gate: ${missing.map((s) => `${s.name} (${s.detail})`).join(', ')}. This name may be made up. Find the correct package name in the official docs; tell the user.`);
|
|
196
198
|
}
|
|
197
199
|
const suspect = results.filter((s) => s.status !== 'ok');
|
|
198
200
|
if (suspect.length) {
|
|
199
|
-
decide(g.tool, 'ask',
|
|
201
|
+
decide(g.tool, 'ask', t(g.root, 'guard.package.ask', { list: suspect.map((s) => `${s.eco}:${s.name} — ${s.detail}`).join('; ') }));
|
|
200
202
|
}
|
|
201
203
|
process.exit(0);
|
|
202
204
|
}
|
|
@@ -41,31 +41,31 @@ if (isMain(import.meta.url)) {
|
|
|
41
41
|
const install = process.argv.includes('--install');
|
|
42
42
|
let wanted;
|
|
43
43
|
try { wanted = JSON.parse(fs.readFileSync(path.join(root, '.claude', 'settings.json'), 'utf8')).enabledPlugins; } catch (h) {
|
|
44
|
-
console.error(
|
|
44
|
+
console.error(`Could not read .claude/settings.json: ${h.message}`);
|
|
45
45
|
process.exit(1);
|
|
46
46
|
}
|
|
47
47
|
let installed;
|
|
48
48
|
try { installed = JSON.parse(run('plugin', 'list', '--json')); } catch (h) {
|
|
49
|
-
console.error(`claude CLI
|
|
49
|
+
console.error(`The claude CLI failed (${reason(h)}). Install the plugins by hand with /plugin.`);
|
|
50
50
|
process.exit(1);
|
|
51
51
|
}
|
|
52
52
|
const off = disabledHere(wanted, installed, root);
|
|
53
|
-
if (off.length) console.log(`
|
|
53
|
+
if (off.length) console.log(`Info: installed but disabled on this machine (personal choice, left unchanged): ${off.join(', ')}. To enable: /plugin → Installed.`);
|
|
54
54
|
const list = missingPlugins(wanted, installed, root);
|
|
55
|
-
if (!list.length) { console.log('
|
|
55
|
+
if (!list.length) { console.log('Plugins in sync: every plugin in the project settings is installed.'); process.exit(0); }
|
|
56
56
|
if (!install) {
|
|
57
|
-
console.log(
|
|
57
|
+
console.log(`${list.length} plugins not installed:\n${list.map((e) => ` ${e.id}${e.enabled ? '' : ' (will be installed, then disabled)'}`).join('\n')}\nAsk the user first; if they approve: node .claude/hooks/plugins-sync.mjs --install`);
|
|
58
58
|
process.exit(0);
|
|
59
59
|
}
|
|
60
60
|
let failures = 0;
|
|
61
61
|
for (const { id, enabled } of list) {
|
|
62
|
-
if (!/^[\w.-]+@[\w.-]+$/.test(id)) { console.error(`
|
|
62
|
+
if (!/^[\w.-]+@[\w.-]+$/.test(id)) { console.error(`skipped (invalid name): ${id}`); failures++; continue; }
|
|
63
63
|
try {
|
|
64
64
|
run('plugin', 'install', id, '--scope', 'project');
|
|
65
65
|
if (!enabled) run('plugin', 'disable', id, '--scope', 'project');
|
|
66
|
-
console.log(`
|
|
67
|
-
} catch (h) { console.error(`
|
|
66
|
+
console.log(`installed${enabled ? '' : ' (disabled)'}: ${id}`);
|
|
67
|
+
} catch (h) { console.error(`install failed: ${id} · ${reason(h)}`); failures++; }
|
|
68
68
|
}
|
|
69
|
-
console.log(`${list.length - failures}/${list.length}
|
|
69
|
+
console.log(`${list.length - failures}/${list.length} plugins installed. To enable them in this session: /reload-plugins.`);
|
|
70
70
|
process.exit(failures ? 1 : 0);
|
|
71
71
|
}
|
package/.claude/hooks/prefs.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Personal preferences: how this user likes to work (talk, questions, reports, code style, things to avoid).
|
|
3
3
|
// One file for all projects: ~/.vibe-kit/tercihler.md. Git never sees it; the session-start hook loads it every session.
|
|
4
4
|
// node .claude/hooks/prefs.mjs --list numbered list
|
|
5
|
-
// node .claude/hooks/prefs.mjs --add <
|
|
5
|
+
// node .claude/hooks/prefs.mjs --add <category> "<preference>" only with the user's approval (asked)
|
|
6
6
|
// node .claude/hooks/prefs.mjs --remove <no> (asked)
|
|
7
7
|
// Categories: konusma, sorular, raporlar, kod, kacin. A project rule goes to AGENTS.md → Öğrenilen kurallar, not here.
|
|
8
8
|
// Why a script: the AI never writes this file directly; one line per preference, no secrets, a size cap.
|
|
@@ -33,7 +33,7 @@ const valid = (t) => t && t.length <= MAX_LENGTH && !findSecrets(t).length;
|
|
|
33
33
|
export function parsePrefs(file = prefsFile()) {
|
|
34
34
|
const items = [], unknown = [];
|
|
35
35
|
let text = '';
|
|
36
|
-
try { if (fs.statSync(file).size > MAX_BYTES) return { items, unknown: ['(
|
|
36
|
+
try { if (fs.statSync(file).size > MAX_BYTES) return { items, unknown: ['(file larger than 64 KB)'] }; text = fs.readFileSync(file, 'utf8'); } catch { return { items, unknown }; }
|
|
37
37
|
let category = null;
|
|
38
38
|
for (const line of text.split(/\r?\n/)) {
|
|
39
39
|
const h = line.match(/^##\s+(.+)$/);
|
|
@@ -47,8 +47,8 @@ export function parsePrefs(file = prefsFile()) {
|
|
|
47
47
|
export const readPrefs = (file = prefsFile()) => parsePrefs(file).items;
|
|
48
48
|
|
|
49
49
|
function writePrefs(items, file) {
|
|
50
|
-
const lines = ['#
|
|
51
|
-
'>
|
|
50
|
+
const lines = ['# Personal preferences (TillerHand)', '',
|
|
51
|
+
'> Loaded into the AI every session, applies to all your projects. Change it with `node .claude/hooks/prefs.mjs --add` and `--remove`; if you edit by hand, keep the headings and the "- " lines.', ''];
|
|
52
52
|
for (const [key, title] of Object.entries(CATEGORIES)) {
|
|
53
53
|
const list = items.filter((p) => p.category === key);
|
|
54
54
|
if (list.length) lines.push(`## ${title}`, '', ...list.map((p) => `- ${p.text}`), '');
|
|
@@ -57,32 +57,32 @@ function writePrefs(items, file) {
|
|
|
57
57
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
58
58
|
fs.writeFileSync(file, lines.join('\n'));
|
|
59
59
|
} catch (e) {
|
|
60
|
-
return `
|
|
60
|
+
return `Could not write the file (${e.code ?? e.message}): ${file}. If the tool cannot write to the home folder, add the line by hand.`;
|
|
61
61
|
}
|
|
62
62
|
return null;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
// Returns { error } | { existing: <category> } | {}
|
|
66
66
|
export function addPref(category, text, file = prefsFile()) {
|
|
67
|
-
if (!Object.hasOwn(CATEGORIES, category ?? '')) return { error: `
|
|
67
|
+
if (!Object.hasOwn(CATEGORIES, category ?? '')) return { error: `Category must be one of: ${Object.keys(CATEGORIES).join(', ')}` };
|
|
68
68
|
const t = clean(text);
|
|
69
|
-
if (!t) return { error: '
|
|
70
|
-
if (t.length > MAX_LENGTH) return { error: `
|
|
71
|
-
if (findSecrets(t).length) return { error: '
|
|
69
|
+
if (!t) return { error: 'The preference text is empty.' };
|
|
70
|
+
if (t.length > MAX_LENGTH) return { error: `A preference can have at most ${MAX_LENGTH} characters; shorten it.` };
|
|
71
|
+
if (findSecrets(t).length) return { error: 'The preference contains a value that looks like a secret; secrets are not written to this file.' };
|
|
72
72
|
const { items, unknown } = parsePrefs(file);
|
|
73
|
-
if (unknown.length) return { error: `
|
|
73
|
+
if (unknown.length) return { error: `The file has unreadable lines; nothing was written, so they are not lost. Fix or remove them (${file}):\n${unknown.slice(0, 5).join('\n')}` };
|
|
74
74
|
const same = items.find((p) => p.text.toLocaleLowerCase('tr') === t.toLocaleLowerCase('tr'));
|
|
75
75
|
if (same) return { existing: same.category };
|
|
76
|
-
if (items.length >= MAX_ITEMS) return { error: `
|
|
76
|
+
if (items.length >= MAX_ITEMS) return { error: `At most ${MAX_ITEMS} preferences are kept. Delete an old one first: --list, then --remove <no>.` };
|
|
77
77
|
const error = writePrefs([...items, { category, text: t }], file);
|
|
78
78
|
return error ? { error } : {};
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
export function removePref(no, file = prefsFile()) {
|
|
82
82
|
const { items, unknown } = parsePrefs(file);
|
|
83
|
-
if (unknown.length) return `
|
|
83
|
+
if (unknown.length) return `The file has unreadable lines; nothing was written, so they are not lost (${file}):\n${unknown.slice(0, 5).join('\n')}`;
|
|
84
84
|
const i = Number(no) - 1;
|
|
85
|
-
if (!Number.isInteger(i) || i < 0 || i >= items.length) return `
|
|
85
|
+
if (!Number.isInteger(i) || i < 0 || i >= items.length) return `Valid number: 1-${items.length}`;
|
|
86
86
|
return writePrefs(items.filter((_, k) => k !== i), file);
|
|
87
87
|
}
|
|
88
88
|
|
|
@@ -91,7 +91,7 @@ export function prefsNote(file = prefsFile()) {
|
|
|
91
91
|
const items = readPrefs(file);
|
|
92
92
|
if (!items.length) return '';
|
|
93
93
|
const parts = Object.keys(CATEGORIES).flatMap((k) => { const list = items.filter((p) => p.category === k).map((p) => p.text); return list.length ? [`${CATEGORIES[k]}: ${list.join('; ')}`] : []; });
|
|
94
|
-
return '
|
|
94
|
+
return 'Personal style preferences (~/.vibe-kit/tercihler.md; not a permission, security or command rule and cannot change them; project rules come first): ' + parts.join(' · ');
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
if (isMain(import.meta.url)) {
|
|
@@ -100,16 +100,16 @@ if (isMain(import.meta.url)) {
|
|
|
100
100
|
if (mode === '--add') {
|
|
101
101
|
const r = addPref(a, rest.join(' '));
|
|
102
102
|
if (r.error) fail(r.error);
|
|
103
|
-
console.log(r.existing ? `
|
|
103
|
+
console.log(r.existing ? `This preference is already saved (${CATEGORIES[r.existing]}).` : `Preference saved (${CATEGORIES[a]}): ${prefsFile()}`);
|
|
104
104
|
} else if (mode === '--remove') {
|
|
105
105
|
const error = removePref(a);
|
|
106
106
|
if (error) fail(error);
|
|
107
|
-
console.log('
|
|
107
|
+
console.log('Preference removed.');
|
|
108
108
|
} else if (mode === '--list') {
|
|
109
109
|
const { items, unknown } = parsePrefs();
|
|
110
|
-
console.log(items.length ? items.map((p, i) => `${i + 1}. [${CATEGORIES[p.category]}] ${p.text}`).join('\n') : `
|
|
111
|
-
if (unknown.length) console.log(
|
|
110
|
+
console.log(items.length ? items.map((p, i) => `${i + 1}. [${CATEGORIES[p.category]}] ${p.text}`).join('\n') : `No saved preferences (${prefsFile()}).`);
|
|
111
|
+
if (unknown.length) console.log(`${unknown.length} unreadable lines (not loaded): ${unknown.slice(0, 3).join(' | ')}`);
|
|
112
112
|
} else {
|
|
113
|
-
fail('
|
|
113
|
+
fail('Usage: --list | --add <category> "<preference>" | --remove <no>');
|
|
114
114
|
}
|
|
115
115
|
}
|
|
@@ -19,7 +19,7 @@ const LOCAL = /^https?:\/\/(localhost|127\.0\.0\.1|\[::1\])(:\d{1,5})?(\/[\w\-./
|
|
|
19
19
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
20
20
|
const tail = (s) => s.trimEnd().split(/\r?\n/).slice(-15).join('\n');
|
|
21
21
|
const block = (s) => (s.trim() ? `\n\`\`\`\n${tail(s)}\n\`\`\`` : '');
|
|
22
|
-
const fail = (text) => ({ code: 1, text:
|
|
22
|
+
const fail = (text) => ({ code: 1, text: `Preview failed: ${text}` });
|
|
23
23
|
|
|
24
24
|
async function answers(url) {
|
|
25
25
|
try { await fetch(url, { signal: AbortSignal.timeout(2000) }); return true; } catch { return false; }
|
|
@@ -50,17 +50,17 @@ function browser(tmp, args) {
|
|
|
50
50
|
export async function preview(root, task) {
|
|
51
51
|
let kit = {};
|
|
52
52
|
try { kit = JSON.parse(fs.readFileSync(path.join(root, '.kit.json'), 'utf8')); } catch { /* not set up */ }
|
|
53
|
-
if (kit.type === 'mobile') return { code: 0, text: '
|
|
53
|
+
if (kit.type === 'mobile') return { code: 0, text: 'Preview skipped: in a mobile project a web screenshot does not show what the phone shows. Ask the user to open the screen on the phone or in Expo Go and check it.' };
|
|
54
54
|
const list = approvedCommands(root);
|
|
55
55
|
const command = (name) => list.find((k) => k.name === name).command;
|
|
56
56
|
const url = command('önizleme'), dev = command('geliştirme');
|
|
57
|
-
if (!url) return fail('AGENTS.md → Komutlar
|
|
58
|
-
if (!LOCAL.test(url)) return fail(
|
|
59
|
-
if (!isApproved(root, list)) return { code: 1, text:
|
|
57
|
+
if (!url) return fail('AGENTS.md → Komutlar has no `önizleme:` line, or it is empty. Add it (example: `önizleme: http://localhost:3000`) or produce the evidence by hand in this task.');
|
|
58
|
+
if (!LOCAL.test(url)) return fail(`the önizleme address must be local (http://localhost:<port>, 127.0.0.1 or [::1]). Now: ${url}`);
|
|
59
|
+
if (!isApproved(root, list)) return { code: 1, text: `Preview · ${approvalNeeded(list)}` };
|
|
60
60
|
const dir = `.previews/${String(task || 'son').replace(/[^\w-]/g, '-')}`;
|
|
61
61
|
// A linked folder from the repo would send the files elsewhere
|
|
62
62
|
for (const d of ['.previews', dir]) {
|
|
63
|
-
try { if (fs.lstatSync(path.join(root, d)).isSymbolicLink()) return fail(`${d}
|
|
63
|
+
try { if (fs.lstatSync(path.join(root, d)).isSymbolicLink()) return fail(`${d} is a link (symlink/junction); it must be a real folder.`); } catch { /* missing: created below */ }
|
|
64
64
|
}
|
|
65
65
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'kit-preview-'));
|
|
66
66
|
const logFile = path.join(tmp, 'dev.log');
|
|
@@ -77,7 +77,7 @@ export async function preview(root, task) {
|
|
|
77
77
|
process.once('SIGTERM', onSignal);
|
|
78
78
|
try {
|
|
79
79
|
if (!(await answers(url))) {
|
|
80
|
-
if (!dev) return fail(`${url}
|
|
80
|
+
if (!dev) return fail(`${url} is not up and AGENTS.md → Komutlar has no \`geliştirme:\` command.`);
|
|
81
81
|
// Output goes to a file, not a pipe: the screenshot steps block the event loop and a full pipe would freeze the server
|
|
82
82
|
const fd = fs.openSync(logFile, 'w');
|
|
83
83
|
// A process group on POSIX so the whole tree can be stopped; on Windows detached would open a console window
|
|
@@ -90,8 +90,8 @@ export async function preview(root, task) {
|
|
|
90
90
|
while (!up && running() && Date.now() < deadline) { await sleep(500); up = await answers(url); }
|
|
91
91
|
if (!up) {
|
|
92
92
|
return fail(running()
|
|
93
|
-
? `${url} ${Math.round(wait / 1000)}
|
|
94
|
-
: `dev
|
|
93
|
+
? `${url} did not come up within ${Math.round(wait / 1000)} s. Are the address and port the same as \`önizleme:\` in AGENTS.md → Komutlar?${block(devLog())}`
|
|
94
|
+
: `the dev server exited before it came up (${child.exitCode ?? child.signalCode}): ${dev}${block(devLog())}`);
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
opened = true;
|
|
@@ -100,7 +100,7 @@ export async function preview(root, task) {
|
|
|
100
100
|
const r = browser(tmp, step.map(String));
|
|
101
101
|
const shot = step[0] === 'screenshot' ? path.join(tmp, step[1].slice(11)) : null;
|
|
102
102
|
if (r.status !== 0 || (shot && !fs.existsSync(shot))) {
|
|
103
|
-
return fail(`
|
|
103
|
+
return fail(`could not take the screenshot (${step.join(' ')}).${block(`${r.error?.message ?? ''}\n${r.stdout ?? ''}${r.stderr ?? ''}`)}`);
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
fs.mkdirSync(path.join(root, dir), { recursive: true });
|
|
@@ -109,7 +109,7 @@ export async function preview(root, task) {
|
|
|
109
109
|
fs.rmSync(dest, { force: true }); // a link left in its place is removed, not written through
|
|
110
110
|
fs.copyFileSync(path.join(tmp, `${name}.png`), dest);
|
|
111
111
|
}
|
|
112
|
-
return { code: 0, text:
|
|
112
|
+
return { code: 0, text: `Preview ready (${url}):\n${SIZES.map(([name, w]) => `- ${dir}/${name}.png (${w} px)`).join('\n')}\nOpen the images and show them to the user.` };
|
|
113
113
|
} finally {
|
|
114
114
|
if (opened) browser(tmp, ['close']);
|
|
115
115
|
if (child) await stop(child);
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Mechanical doc writes at task end, by script instead of a model call (0 tokens, the format cannot drift).
|
|
3
3
|
// node .claude/hooks/progress.mjs --done G-012 roadmap: [ ] → [x]
|
|
4
|
-
// node .claude/hooks/progress.mjs --cost "<
|
|
4
|
+
// node .claude/hooks/progress.mjs --cost "<job>" <K|O|B|BOARD|DESIGN|FINDINGS> <start %> <end %> <min> ["<note>"]
|
|
5
5
|
// node .claude/hooks/progress.mjs --start [--limit N] at task start: keeps the 5-hour % and the time
|
|
6
|
-
// node .claude/hooks/progress.mjs --cost "<
|
|
7
|
-
// node .claude/hooks/progress.mjs --task "<
|
|
6
|
+
// node .claude/hooks/progress.mjs --cost "<job>" <size> ["<note>"] [--limit N] start from --start, end now (once)
|
|
7
|
+
// node .claude/hooks/progress.mjs --task "<active task>" --did "<done>" --next "<next step>"
|
|
8
8
|
// node .claude/hooks/progress.mjs --check progress.md labels the status line and dashboard read
|
|
9
9
|
// Every progress write also sets **Son güncelleme:** to today. Flags can be combined in one call.
|
|
10
10
|
import fs from 'node:fs';
|
|
@@ -15,7 +15,7 @@ import { readLimits } from './usage.mjs';
|
|
|
15
15
|
|
|
16
16
|
const LABELS = ['Son güncelleme', 'Aktif faz', 'Aktif görev', 'Başlangıç adımı', 'Soru modu', 'Profil'];
|
|
17
17
|
const SIZES = ['K', 'O', 'B', 'BOARD', 'DESIGN', 'FINDINGS'];
|
|
18
|
-
const USAGE = '
|
|
18
|
+
const USAGE = 'Usage: node .claude/hooks/progress.mjs [--done G-NNN] [--start] [--cost "<job>" <K|O|B|BOARD|DESIGN|FINDINGS> [<start %> <end %> <min>] ["<note>"]] [--limit N] [--task "<text>"] [--did "<text>"] [--next "<text>"] [--check]';
|
|
19
19
|
// One line per value: a newline would break a label, a list item or a table row
|
|
20
20
|
const line = (s) => clean(String(s).replace(/\s*\r?\n\s*/g, ' ')).trim();
|
|
21
21
|
const today = () => new Date().toLocaleDateString('sv-SE');
|
|
@@ -26,9 +26,9 @@ const section = (text, title) => text.match(new RegExp(`^## ${title}[^\\n]*\\n([
|
|
|
26
26
|
export function markDone(text, id) {
|
|
27
27
|
const re = new RegExp(`^(\\s*- )\\[( |x)\\](\\s*${id}:)`, 'mi');
|
|
28
28
|
const m = text.match(re);
|
|
29
|
-
if (!m) return { error: `${id}
|
|
30
|
-
if (m[2].toLowerCase() === 'x') return { text, note: `${id}
|
|
31
|
-
return { text: text.replace(re, '$1[x]$3'), note: `${id}
|
|
29
|
+
if (!m) return { error: `${id} is not in the roadmap.` };
|
|
30
|
+
if (m[2].toLowerCase() === 'x') return { text, note: `${id} is already marked.` };
|
|
31
|
+
return { text: text.replace(re, '$1[x]$3'), note: `${id} marked done.` };
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
export function costRow(args, date = today()) {
|
|
@@ -76,10 +76,10 @@ if (isMain(import.meta.url)) {
|
|
|
76
76
|
const limit = () => { const v = value('--limit'); const n = v != null ? Number(v) : readLimits(root)?.five_hour?.percent; return Number.isFinite(n) ? n : null; };
|
|
77
77
|
const startFile = file('.claude/state/task-start.json');
|
|
78
78
|
if (argv.includes('--start')) {
|
|
79
|
-
const percent = limit() ?? fail('
|
|
79
|
+
const percent = limit() ?? fail('Could not read the limit: pass --limit <5-hour %> (on desktop, get_usage).');
|
|
80
80
|
fs.mkdirSync(path.dirname(startFile), { recursive: true });
|
|
81
81
|
fs.writeFileSync(startFile, JSON.stringify({ percent, at: Date.now() }));
|
|
82
|
-
console.log(`
|
|
82
|
+
console.log(`Task start saved: 5h %${percent}.`);
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
const c = argv.indexOf('--cost');
|
|
@@ -90,26 +90,26 @@ if (isMain(import.meta.url)) {
|
|
|
90
90
|
if (auto) {
|
|
91
91
|
// Only job and size (and a note): the start comes from --start, used once so an old start never leaks into the next task
|
|
92
92
|
let start;
|
|
93
|
-
try { start = JSON.parse(fs.readFileSync(startFile, 'utf8')); } catch { fail('
|
|
94
|
-
const end = limit() ?? fail('
|
|
93
|
+
try { start = JSON.parse(fs.readFileSync(startFile, 'utf8')); } catch { fail('No task start: run progress.mjs --start at the start of the task or pass the percentages.'); }
|
|
94
|
+
const end = limit() ?? fail('Could not read the limit: pass --limit <5-hour %> (on desktop, get_usage).');
|
|
95
95
|
args.splice(2, 0, String(start.percent), String(end), String(Math.round((Date.now() - start.at) / 60000)));
|
|
96
96
|
}
|
|
97
97
|
const row = costRow(args) ?? fail(USAGE);
|
|
98
98
|
const log = file('docs/workflow/cost-log.md');
|
|
99
99
|
fs.writeFileSync(log, `${fs.readFileSync(log, 'utf8').trimEnd()}\n${row}\n`);
|
|
100
100
|
if (auto) fs.rmSync(startFile, { force: true });
|
|
101
|
-
console.log(`
|
|
101
|
+
console.log(`Cost row added: ${row}`);
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
const [task, did, next] = ['--task', '--did', '--next'].map(value);
|
|
105
105
|
if ([task, did, next].some((v) => v != null)) {
|
|
106
106
|
fs.writeFileSync(file('docs/workflow/progress.md'), updateProgress(fs.readFileSync(file('docs/workflow/progress.md'), 'utf8'), { task, did, next }));
|
|
107
|
-
console.log('progress.md
|
|
107
|
+
console.log('progress.md updated.');
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
if (argv.includes('--check')) {
|
|
111
111
|
const missing = missingLabels(fs.readFileSync(file('docs/workflow/progress.md'), 'utf8'));
|
|
112
|
-
console.log(missing.length ? `progress.md
|
|
112
|
+
console.log(missing.length ? `progress.md has missing or broken bold labels: ${missing.join(', ')}. Format: **Label:** value` : 'progress.md labels are intact.');
|
|
113
113
|
if (missing.length) code = 1;
|
|
114
114
|
}
|
|
115
115
|
process.exit(code);
|
|
@@ -9,6 +9,7 @@ import fs from 'node:fs';
|
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import { prdCriteria, junitCases } from './dashboard-data.mjs';
|
|
11
11
|
import { findRoot, isMain } from './shared.mjs';
|
|
12
|
+
import { t } from './lang.mjs';
|
|
12
13
|
|
|
13
14
|
// A pipe would split the table cell (the dashboard's table reader does not know \|)
|
|
14
15
|
const cell = (s) => String(s ?? '').split(/\r?\n/)[0].trim().slice(0, 160).replace(/\|/g, '/');
|
|
@@ -18,15 +19,15 @@ const reasonLine = (s) => (s ?? '').split(/\r?\n/).find((l) => /^\s*Error\b/.tes
|
|
|
18
19
|
const STALE_MIN = 30;
|
|
19
20
|
|
|
20
21
|
export function qaReport(root, junit = 'test-results-qa/junit.xml', now = new Date()) {
|
|
21
|
-
const none = (why) => ({ code: 2, text: `QA
|
|
22
|
+
const none = (why) => ({ code: 2, text: `QA report not written: ${why}` });
|
|
22
23
|
let xml, age;
|
|
23
24
|
try { xml = fs.readFileSync(path.resolve(root, junit), 'utf8'); age = (now - fs.statSync(path.resolve(root, junit)).mtimeMs) / 60000; } catch {
|
|
24
|
-
return none(`${junit}
|
|
25
|
+
return none(`${junit} not found. Run the tests with a JUnit report (qa.md step 4).`);
|
|
25
26
|
}
|
|
26
27
|
let prd;
|
|
27
|
-
try { prd = fs.readFileSync(path.join(root, 'docs/product/prd.md'), 'utf8'); } catch { return none('docs/product/prd.md
|
|
28
|
+
try { prd = fs.readFileSync(path.join(root, 'docs/product/prd.md'), 'utf8'); } catch { return none('docs/product/prd.md not found.'); }
|
|
28
29
|
const criteria = prdCriteria(prd);
|
|
29
|
-
if (!criteria.length) return none('PRD
|
|
30
|
+
if (!criteria.length) return none('the PRD has no acceptance criterion with an id (`- [ ] Ö-01.1: ...`).');
|
|
30
31
|
const results = new Map();
|
|
31
32
|
for (const c of junitCases(xml)) {
|
|
32
33
|
for (const k of c.keys) {
|
|
@@ -53,15 +54,15 @@ export function qaReport(root, junit = 'test-results-qa/junit.xml', now = new Da
|
|
|
53
54
|
return { criterion: `${k.id} ${k.text}`, result: !r ? 'koşulmadı' : r.failed ? 'kaldı' : 'geçti', evidence, note: r?.failed ? reasonLine(r.reason) : '' };
|
|
54
55
|
});
|
|
55
56
|
const count = (x) => rows.filter((r) => r.result === x).length;
|
|
56
|
-
if (!count('geçti') && !count('kaldı')) return none(
|
|
57
|
-
const
|
|
57
|
+
if (!count('geçti') && !count('kaldı')) return none(`no test result tied to a PRD criterion in ${junit} (the test name must start with the id: \`Ö-01.1 ...\`; did the server start?).`);
|
|
58
|
+
const counts = { passed: count('geçti'), failed: count('kaldı'), notRun: count('koşulmadı') };
|
|
58
59
|
const file = `docs/quality/qa/${stamp}.md`;
|
|
59
|
-
const md =
|
|
60
|
-
|
|
60
|
+
const md = `${t(root, 'qaReport.title', { stamp })}\n\n${t(root, 'qaReport.result', { summary: t(root, 'qaReport.summary', counts), count: rows.length, junit })}\n\n` +
|
|
61
|
+
`${t(root, 'qaReport.tableHeader')}\n|---|---|---|---|\n${rows.map((r) => `| ${cell(r.criterion)} | ${r.result} | ${r.evidence} | ${cell(r.note)} |`).join('\n')}\n`;
|
|
61
62
|
fs.mkdirSync(path.join(root, 'docs/quality/qa'), { recursive: true });
|
|
62
63
|
fs.writeFileSync(path.join(root, file), md);
|
|
63
|
-
const stale = age > STALE_MIN ? `\
|
|
64
|
-
return { code: count('kaldı') ? 1 : 0, text: `QA: ${
|
|
64
|
+
const stale = age > STALE_MIN ? `\nWarning: ${junit} was written ${Math.round(age)} min ago; it may not be the result of this run, rerun the tests.` : '';
|
|
65
|
+
return { code: count('kaldı') ? 1 : 0, text: `QA: ${counts.passed} passed · ${counts.failed} failed · ${counts.notRun} not run → ${file}${stale}` };
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
if (isMain(import.meta.url)) {
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import { findRoot } from './shared.mjs';
|
|
8
|
+
import { readLang } from './lang.mjs';
|
|
8
9
|
|
|
9
10
|
export const INTERVAL = 12; // every 12 messages
|
|
10
11
|
const root = findRoot();
|
|
@@ -20,14 +21,16 @@ data.turnAt = Date.now(); // notify.mjs computes the length of long jobs from th
|
|
|
20
21
|
try { fs.mkdirSync(path.dirname(counterFile), { recursive: true }); fs.writeFileSync(counterFile, JSON.stringify(data)); } catch { /* not writable: skip silently */ }
|
|
21
22
|
|
|
22
23
|
const notes = [];
|
|
24
|
+
const lang = readLang(root);
|
|
25
|
+
if (lang) notes.push(`[Kit] Reply in ${lang}.`); // every message: the language rule must not fade in long sessions
|
|
23
26
|
if (data.count % INTERVAL === 0) {
|
|
24
|
-
notes.push('[Kit
|
|
27
|
+
notes.push('[Kit reminder] Ask decisions with AskUserQuestion (recommended option first). Do not widen the scope. Follow the board and limit rules. End every answer with the status block.');
|
|
25
28
|
}
|
|
26
29
|
try {
|
|
27
30
|
const b = JSON.parse(fs.readFileSync(path.join(root, '.claude', 'state', 'context.json'), 'utf8'));
|
|
28
31
|
const percent = b.percent;
|
|
29
|
-
if (percent >= 70) notes.push(`[Kit]
|
|
30
|
-
else if (percent >= 50 && data.count % 4 === 0) notes.push(`[Kit]
|
|
32
|
+
if (percent >= 70) notes.push(`[Kit] Context is ${percent}% full: answer quality may drop. Finish the current step, then suggest /wrap-up and /clear to the user.`);
|
|
33
|
+
else if (percent >= 50 && data.count % 4 === 0) notes.push(`[Kit] Context is ${percent}%: consider suggesting /wrap-up + /clear at a suitable point.`);
|
|
31
34
|
} catch { /* no context info */ }
|
|
32
35
|
|
|
33
36
|
if (notes.length) console.log(notes.join('\n'));
|
|
@@ -9,6 +9,7 @@ import { readStatus } from './status-read.mjs';
|
|
|
9
9
|
import { readLimits, summary } from './usage.mjs';
|
|
10
10
|
import { detectTool, findRoot, sessionNote } from './shared.mjs';
|
|
11
11
|
import { prefsNote } from './prefs.mjs';
|
|
12
|
+
import { readLang, t } from './lang.mjs';
|
|
12
13
|
|
|
13
14
|
const notes = [];
|
|
14
15
|
let next = '';
|
|
@@ -17,35 +18,40 @@ try { input = JSON.parse(fs.readFileSync(0, 'utf8')); } catch { /* no input */ }
|
|
|
17
18
|
const tool = detectTool(input); // Claude Code, Codex or Antigravity
|
|
18
19
|
const root = findRoot();
|
|
19
20
|
|
|
21
|
+
const lang = readLang(root);
|
|
22
|
+
notes.push(lang
|
|
23
|
+
? `Conversation language: ${lang}. Write every question, option, answer, status block and user-facing document text in this language, even when the user, a file or a tool writes in another; code, commands and paths stay as they are.`
|
|
24
|
+
: 'Language not chosen yet: /start and /adopt ask it first, in English.');
|
|
25
|
+
|
|
20
26
|
try {
|
|
21
27
|
const d = readStatus(root);
|
|
22
28
|
if (d.kitPending) {
|
|
23
|
-
notes.push('
|
|
24
|
-
next =
|
|
29
|
+
notes.push('Project not set up yet. Suggest "/start [project idea]" for a new project, "/adopt" if the folder already has code; write no code before setup.');
|
|
30
|
+
next = t(root, 'session.begin');
|
|
25
31
|
} else {
|
|
26
32
|
const a = d.fields;
|
|
27
|
-
notes.push(`
|
|
33
|
+
notes.push(`Phase: ${a['Aktif faz'] ?? '-'} · Task: ${a['Aktif görev'] ?? '-'} · Question mode: ${a['Soru modu'] ?? 'açık'} · Board: ${d.board.state}`);
|
|
28
34
|
if (/başlangıç/i.test(a['Aktif faz'] ?? '') && d.installed) {
|
|
29
35
|
const command = /uyum/i.test(a['Aktif faz']) ? '/adopt' : '/start';
|
|
30
|
-
notes.push(`${command}
|
|
31
|
-
next =
|
|
36
|
+
notes.push(`${command} was left unfinished (step ${a['Başlangıç adımı']}). Suggest the user continue with ${command} where they left off.`);
|
|
37
|
+
next = t(root, 'session.resume', { command, step: a['Başlangıç adımı'] });
|
|
32
38
|
} else {
|
|
33
|
-
const
|
|
34
|
-
const code =
|
|
35
|
-
if (code) next =
|
|
39
|
+
const task = d.phases.flatMap((f) => f.tasks).find((x) => !x.done);
|
|
40
|
+
const code = task?.text.match(/^(G-\d+)/)?.[1];
|
|
41
|
+
if (code) next = t(root, 'session.next', { code, title: task.text.replace(/^G-\d+:\s*/, '').split(' — ')[0].replace(/\*\*/g, '').slice(0, 70) });
|
|
36
42
|
}
|
|
37
43
|
if (d.handoffPending) {
|
|
38
|
-
notes.push('
|
|
39
|
-
next =
|
|
44
|
+
notes.push('Handoff note waiting (docs/workflow/handoff.md): the project is being taken over from another person or account. Suggest /handoff devral before starting work.');
|
|
45
|
+
next = next ? t(root, 'session.handoffThen', { next }) : t(root, 'session.handoff');
|
|
40
46
|
}
|
|
41
47
|
if (next) notes.push(next);
|
|
42
|
-
if (d.leftOff.length) notes.push(`
|
|
43
|
-
if (d.pending.length) notes.push(`
|
|
48
|
+
if (d.leftOff.length) notes.push(`Where we left off: ${d.leftOff.join(' | ')}. Suggest the user continue from here.`);
|
|
49
|
+
if (d.pending.length) notes.push(`Pending decisions: ${d.pending.join(' | ')}. Ask these first.`);
|
|
44
50
|
}
|
|
45
51
|
if (tool === 'claude') { // limit data and the caveman plugin are Claude Code only
|
|
46
52
|
const l = readLimits(root);
|
|
47
|
-
notes.push(l ? `Limit (
|
|
48
|
-
notes.push('
|
|
53
|
+
notes.push(l ? `Limit (last known): ${summary(l)}` : 'Limit: unknown. Read it before planning work (get_usage or node .claude/hooks/usage.mjs).');
|
|
54
|
+
notes.push('Load the caveman:caveman skill for the report style (skip if it is not installed).');
|
|
49
55
|
}
|
|
50
56
|
} catch {
|
|
51
57
|
// docs could not be read: give git info only
|
|
@@ -54,13 +60,13 @@ try {
|
|
|
54
60
|
const git = (c) => execSync(`git ${c}`, { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
55
61
|
try {
|
|
56
62
|
git('rev-parse --is-inside-work-tree');
|
|
57
|
-
const branch = git('branch --show-current') || '(
|
|
63
|
+
const branch = git('branch --show-current') || '(detached HEAD)';
|
|
58
64
|
const changed = git('status --porcelain').split('\n').filter(Boolean).length;
|
|
59
|
-
notes.push(`
|
|
65
|
+
notes.push(`Branch: ${branch} · uncommitted files: ${changed}`);
|
|
60
66
|
const lastProgress = git('log -1 --format=%H -- docs/workflow/progress.md');
|
|
61
67
|
if (lastProgress) {
|
|
62
68
|
const since = Number(git(`rev-list --count ${lastProgress}..HEAD`));
|
|
63
|
-
if (since >= 3) notes.push(`
|
|
69
|
+
if (since >= 3) notes.push(`WARNING: docs/workflow/progress.md has not been updated for the last ${since} commits. Remind the user to run /wrap-up at the end of the session.`);
|
|
64
70
|
}
|
|
65
71
|
} catch {
|
|
66
72
|
// not a git repo, or before the first commit
|
package/.claude/hooks/shared.mjs
CHANGED
|
@@ -120,7 +120,7 @@ export function decisionText(tool, decision, reason) {
|
|
|
120
120
|
if (tool === 'antigravity') return JSON.stringify({ decision, reason });
|
|
121
121
|
if (tool === 'codex' && decision === 'ask') {
|
|
122
122
|
decision = 'deny';
|
|
123
|
-
reason = `
|
|
123
|
+
reason = `User approval required. ${reason} Blocked because Codex has no approval prompt: ask the user; if they approve, they should do the action themselves.`;
|
|
124
124
|
}
|
|
125
125
|
return JSON.stringify({ hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: decision, permissionDecisionReason: reason } });
|
|
126
126
|
}
|