tillerhand 3.42.0 → 3.43.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/test-writer.md +1 -0
- package/.claude/hooks/commit-gate.mjs +16 -1
- package/.claude/hooks/destructive-guard.mjs +6 -1
- package/.claude/hooks/test-guard.mjs +49 -6
- package/.claude/settings.json +4 -0
- package/README.en.md +1 -1
- package/README.md +1 -1
- package/kit/CHANGELOG.md +10 -0
- package/kit/VERSION +1 -1
- package/kit/guide.html +3 -3
- package/package.json +1 -1
|
@@ -20,6 +20,7 @@ Uygulama planını, kod rolüne verilen talimatları ve yarım kalmış uygulama
|
|
|
20
20
|
- Her kabul kriteri için en az bir test; kriter kimliği test adında (`Ö-01.1`). Yetki, ödeme, kişisel veri varsa saldırgan testi (başka kullanıcı, anonim, bozuk girdi).
|
|
21
21
|
- Uç durumlar: boş, sıfır, çok büyük girdi, Türkçe karakter (`I/İ/ı/i`), tarih ve saat dilimi, eşzamanlı istek; kritere uyanları ekle.
|
|
22
22
|
- Beklentiler sıkı olsun: `toBeTruthy`, `toBeDefined`, matcher'sız `expect` gibi gevşek beklenti yazma. `.skip`/`.only` bırakma.
|
|
23
|
+
- Test dosyalarını Write/Edit ile yaz. Bash, betik veya patch ile yazılan test dosyası commit kapısında durur: kapı yalnız senin Write/Edit ile bıraktığın içeriği tanır.
|
|
23
24
|
- Uygulama kodu yazma; yalnız test dosyaları ve test yardımcıları (fixture, sahte veri). Uygulama dosyası gerekiyorsa (boş fonksiyon iskeleti bile) yazma, raporda söyle.
|
|
24
25
|
- Testleri çalıştır ve **kırmızı** olduklarını göster: her testin neden başarısız olduğu (yok, yanlış değer) beklenen sebep olmalı. Sözdizimi veya içe aktarma hatasıyla kırmızı olan test geçerli değildir; düzelt.
|
|
25
26
|
- Kriter belirsizse veya iki şekilde okunabiliyorsa tahmin etme: soruyu ve seçenekleri raporla, testi yazmadan dur.
|
|
@@ -11,6 +11,8 @@ import crypto from 'node:crypto';
|
|
|
11
11
|
import { execSync } from 'node:child_process';
|
|
12
12
|
import { findRoot, isMain } from './shared.mjs';
|
|
13
13
|
import { unquote } from './destructive-guard.mjs';
|
|
14
|
+
import { isTestFile, isTestConfig, digest, readRecord } from './test-guard.mjs';
|
|
15
|
+
import { isCodeFile } from './edit-rounds.mjs';
|
|
14
16
|
|
|
15
17
|
// Project root (the script's location) even when called from a subfolder: approval and gate must not be written elsewhere
|
|
16
18
|
const root = findRoot();
|
|
@@ -40,7 +42,7 @@ export function changes() {
|
|
|
40
42
|
let body = null;
|
|
41
43
|
try { body = fs.readFileSync(path.join(root, f)); } catch { /* deleted */ }
|
|
42
44
|
h.update(body ?? '\0silindi');
|
|
43
|
-
hashes.set(f, body ?
|
|
45
|
+
hashes.set(f, body ? digest(body) : 'silindi');
|
|
44
46
|
}
|
|
45
47
|
for (const f of partial) {
|
|
46
48
|
h.update(`\0index:${f}:${blobs.get(f)}`);
|
|
@@ -228,6 +230,19 @@ async function hook() {
|
|
|
228
230
|
// The deterministic gate (gate.mjs) must be green for the same changes; not required if the user said "atlandı"
|
|
229
231
|
const gate = readGate();
|
|
230
232
|
const skipped = SKIPPED.test(String(approval?.note ?? ''));
|
|
233
|
+
// Test files only test-writer writes (AGENTS.md rule 7): one changed past the PreToolUse guards stops the commit.
|
|
234
|
+
// Deleted files are left to the guards that ask before a delete. Only code files: snapshots, images and fixtures are
|
|
235
|
+
// written by the test runner, not by Write/Edit. A partially staged test (git apply --cached, skip-worktree) commits an
|
|
236
|
+
// index blob the record does not describe, so it always stops.
|
|
237
|
+
const written = readRecord(root);
|
|
238
|
+
const foreign = d.files.filter((f) => isTestFile(f) && isCodeFile(f) && !isTestConfig(f) && d.hashes[f] !== 'silindi' && written[f] !== d.hashes[f]);
|
|
239
|
+
if (foreign.length && !(skipped && approval.signature === d.signature)) {
|
|
240
|
+
process.stderr.write(`Commit durduruldu: şu test dosyalarını test-writer yazmadı, yazdıktan sonra değişti ya da kısmen hazırlandı: ${foreign.join(', ')}.\n` +
|
|
241
|
+
'Test dosyalarına yalnız test-writer ajanı Write/Edit ile yazar (AGENTS.md kural 7); patch, betik, kabuk, git mv veya Codex/Antigravity ile yazılan test sayılmaz.\n' +
|
|
242
|
+
'Değişiklik gerekiyorsa test-writer\'a dosyayı gözden geçirtip Write/Edit ile yeniden yazdır. Değişikliği kullanıcı kendisi yaptıysa ' +
|
|
243
|
+
've bu commit için denetimi atlamayı açıkça isterse --skip "kullanıcı isteği" kullan.\n');
|
|
244
|
+
process.exit(2);
|
|
245
|
+
}
|
|
231
246
|
if (approval?.signature === d.signature && (skipped || (gate?.signature === d.signature && gate.passed === true))) process.exit(0);
|
|
232
247
|
if (approval?.signature === d.signature) {
|
|
233
248
|
const failed = gate?.signature === d.signature ? gate.checks.filter((k) => k.status === 'failed').map((k) => k.name) : null;
|
|
@@ -51,7 +51,12 @@ const NEEDS_APPROVAL = [
|
|
|
51
51
|
// Each part of a command chain is checked alone: "rm -r node_modules && npx vitest run a.test.ts" does not change a test.
|
|
52
52
|
const WRITER = /\b(rm|del|erase|rmdir|rd|unlink|mv|move|cp|copy|tee|truncate|touch|ln|dd|Set-Content|Add-Content|Out-File|Remove-Item|Move-Item|Copy-Item|Clear-Content)\b|\bcurl\b[^;&|]*\s(-o|--output)\b|\bwget\b[^;&|]*\s(-O|--output-document)\b|\bsed\b[^;&|]*\s(-[a-zA-Z]*i\S*|--in-place\b)|\bperl\b[^;&|]*\s-[a-zA-Z]*i|\bgit\s+(checkout|restore|rm|mv)\b/i;
|
|
53
53
|
// Dependency and git internals are not the project's tests; a URL (curl … https://x/tests/a) or a dd input is not a target
|
|
54
|
-
|
|
54
|
+
// Normalized first: node_modules/../tests/a.test.ts is a test path; only a path that stays under them is skipped
|
|
55
|
+
const isTestCode = (t) => {
|
|
56
|
+
if (/:\/\/|^if=/i.test(t)) return false;
|
|
57
|
+
const n = path.posix.normalize(t.replace(/\\/g, '/'));
|
|
58
|
+
return !/(^|\/)(node_modules|\.git)(\/|$)/i.test(n) && (isTestFile(n) || /(^|\/)(tests?|__tests__|e2e|spec)\/?$/i.test(n));
|
|
59
|
+
};
|
|
55
60
|
const isTestPath = (t) => isTestCode(t) || isTestConfig(t);
|
|
56
61
|
function writesTest(command, isPath = isTestPath) {
|
|
57
62
|
return command.split(/&&|\|\||[;|\n]/).some((part) => {
|
|
@@ -7,8 +7,10 @@
|
|
|
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';
|
|
13
|
+
import crypto from 'node:crypto';
|
|
12
14
|
import { readInput, decide, isMain } from './shared.mjs';
|
|
13
15
|
|
|
14
16
|
const TEST = /(^|\/)(tests?|__tests__|e2e|spec)\/|\.(test|spec|e2e-spec)\.[cm]?[jt]sx?$|(^|\/)test_[^/]*\.py$|_test\.(py|go)$/i;
|
|
@@ -48,6 +50,14 @@ export function configRisk(before, after) {
|
|
|
48
50
|
return changed.some((s) => RISKY.test(s)) ? 'testlerin kapsamını belirleyen ayar değişiyor (include/exclude, testMatch, passWithNoTests, eşik, test script\'i)' : null;
|
|
49
51
|
}
|
|
50
52
|
|
|
53
|
+
// The real path too: a link or junction into the test folder, a Windows short name (TESTS~1)
|
|
54
|
+
// A new nested path (lnk/new/sub/x.ts): resolve the nearest folder that exists and append the rest
|
|
55
|
+
const realOf = (p) => {
|
|
56
|
+
for (let dir = p, rest = ''; ; rest = path.join(path.basename(dir), rest), dir = path.dirname(dir)) {
|
|
57
|
+
try { return path.join(fs.realpathSync.native(dir), rest); } catch { if (path.dirname(dir) === dir) return p; }
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
51
61
|
function hook() {
|
|
52
62
|
const g = readInput();
|
|
53
63
|
if (!g) process.exit(0);
|
|
@@ -56,11 +66,6 @@ function hook() {
|
|
|
56
66
|
const rel = path.relative(g.root, full);
|
|
57
67
|
return [full, rel.startsWith('..') || path.isAbsolute(rel) ? full : rel];
|
|
58
68
|
};
|
|
59
|
-
// The real path too: a link or junction into the test folder, a Windows short name (TESTS~1)
|
|
60
|
-
const realOf = (p) => {
|
|
61
|
-
try { return fs.realpathSync.native(p); } catch { /* a new file */ }
|
|
62
|
-
try { return path.join(fs.realpathSync.native(path.dirname(p)), path.basename(p)); } catch { return p; }
|
|
63
|
-
};
|
|
64
69
|
for (const f of g.files) {
|
|
65
70
|
if (!f.path) continue;
|
|
66
71
|
const [full, name] = resolveName(f.path);
|
|
@@ -91,4 +96,42 @@ function hook() {
|
|
|
91
96
|
process.exit(0);
|
|
92
97
|
}
|
|
93
98
|
|
|
94
|
-
|
|
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);
|
|
125
|
+
}
|
|
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 */ }
|
|
134
|
+
process.exit(0);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (isMain(import.meta.url)) (process.argv.includes('--record') ? record : hook)();
|
package/.claude/settings.json
CHANGED
package/README.en.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
</p>
|
|
7
7
|
|
|
8
8
|
<p align="center">
|
|
9
|
-
<a href="kit/CHANGELOG.md"><img alt="Version" src="https://img.shields.io/badge/version-3.
|
|
9
|
+
<a href="kit/CHANGELOG.md"><img alt="Version" src="https://img.shields.io/badge/version-3.43.0-2a78d6?style=flat-square"></a>
|
|
10
10
|
<a href="LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/license-MIT-2a78d6?style=flat-square"></a>
|
|
11
11
|
<img alt="Claude Code" src="https://img.shields.io/badge/Claude%20Code-ready-d97757?style=flat-square">
|
|
12
12
|
<img alt="Also Codex and Antigravity" src="https://img.shields.io/badge/also-Codex%20%C2%B7%20Antigravity-5b6577?style=flat-square">
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
</p>
|
|
7
7
|
|
|
8
8
|
<p align="center">
|
|
9
|
-
<a href="kit/CHANGELOG.md"><img alt="Sürüm" src="https://img.shields.io/badge/s%C3%BCr%C3%BCm-3.
|
|
9
|
+
<a href="kit/CHANGELOG.md"><img alt="Sürüm" src="https://img.shields.io/badge/s%C3%BCr%C3%BCm-3.43.0-2a78d6?style=flat-square"></a>
|
|
10
10
|
<a href="LICENSE"><img alt="Lisans: MIT" src="https://img.shields.io/badge/lisans-MIT-2a78d6?style=flat-square"></a>
|
|
11
11
|
<img alt="Claude Code" src="https://img.shields.io/badge/Claude%20Code-haz%C4%B1r-d97757?style=flat-square">
|
|
12
12
|
<img alt="Codex ve Antigravity" src="https://img.shields.io/badge/ayr%C4%B1ca-Codex%20%C2%B7%20Antigravity-5b6577?style=flat-square">
|
package/kit/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,16 @@
|
|
|
3
3
|
Kit sürümü `kit/VERSION` dosyasında. Bir projeyi güncellemek: projede `/kit-update`.
|
|
4
4
|
"Elle uyarla" notu olan maddeler projeye ait dosyalardadır; güncelleme bunlara dokunmaz.
|
|
5
5
|
|
|
6
|
+
## [3.43.0] - 2026-10-27
|
|
7
|
+
|
|
8
|
+
### Eklenen
|
|
9
|
+
- **Commit kapısı test dosyasının kaynağını denetler.** `test-writer` bir test dosyasını Write/Edit ile yazınca içeriğin özeti kaydedilir (`.claude/state/test-writer.json`). Commit'e giren bir test dosyası bu kayıtla eşleşmiyorsa commit durur: `git apply`, `patch`, bir betik ya da kabukla yazılan test artık kurul onayıyla da geçmez. Değişikliği kullanıcı kendisi yaptıysa o commit için `--skip` kullanılır. Kısmen hazırlanmış bir test dosyası (`git apply --cached`, `skip-worktree`) her zaman durur. Test klasöründeki kod olmayan dosyalar (snapshot, görsel, fixture) denetlenmez; silinen test dosyaları için silmeden önce soran korumalar geçerli. Yalnız Claude Code'da (ajan kimliği orada var).
|
|
10
|
+
|
|
11
|
+
## [3.42.1] - 2026-10-27
|
|
12
|
+
|
|
13
|
+
### Düzeltilen
|
|
14
|
+
- **Test ayrımında iki yol kaçağı kapandı.** `node_modules/../tests/a.test.ts` gibi `..` içeren yollar artık normalleştirilip test yolu sayılıyor; test klasörüne giden bir bağın altında henüz var olmayan iç içe bir yola yazım da yakalanıyor.
|
|
15
|
+
|
|
6
16
|
## [3.42.0] - 2026-10-27
|
|
7
17
|
|
|
8
18
|
### Eklenen
|
package/kit/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.43.0
|
package/kit/guide.html
CHANGED
|
@@ -287,7 +287,7 @@ footer .container { display: flex; flex-wrap: wrap; gap: 16px; justify-content:
|
|
|
287
287
|
<section class="hero">
|
|
288
288
|
<div class="container">
|
|
289
289
|
<div>
|
|
290
|
-
<span class="pill"><b>v3.
|
|
290
|
+
<span class="pill"><b>v3.43.0</b> Claude Code için başlangıç kiti · MIT</span>
|
|
291
291
|
<h1>Kodu AI yazar.<br><em>Dümen sende.</em></h1>
|
|
292
292
|
<p class="sub">TillerHand, Claude Code ile bir ürünü fikirden canlıya ve bakıma kadar götürür. Her önemli kararda AI sana seçenekleri, önerisini ve her seçeneğin bedelini sunar. Bir uzman kurulu işi denetler, korumalar hatayı daha olmadan durdurur.</p>
|
|
293
293
|
<div class="install"><span id="cmd">npx tillerhand init</span><button class="copy" type="button" data-copy="cmd">Kopyala</button></div>
|
|
@@ -776,7 +776,7 @@ node vibe-kit/kit/kit.mjs init</pre>
|
|
|
776
776
|
<tr><td>Paket kapısı</td><td>Kurulan ya da <code>npx</code>, <code>uvx</code> ile indirilip çalıştırılan paket kayıt defterinden doğrulanır: yoksa engellenir; çok yeni, az indirilmiş veya popüler bir pakete benziyorsa sorulur</td></tr>
|
|
777
777
|
<tr><td>Yapı koruyucu</td><td>Harita dışı yeni dosya sorulur; <code>*.bak</code>, <code>*-old.*</code>, kopya ve <code>temp/</code> gibi dağınıklıklar engellenir</td></tr>
|
|
778
778
|
<tr><td>Yapılandırma onayı</td><td><code>CLAUDE.md</code>, <code>AGENTS.md</code>, <code>.claude/</code>, <code>.codex/</code>, <code>.mcp.json</code> ve iş akışı dosyaları onayın olmadan değişmez</td></tr>
|
|
779
|
-
<tr><td>Test ayrımı</td><td>Testleri kodu yazan değil, ayrı bir ajan (<code>test-writer</code>, farklı model önerilir) koddan önce yazar. Test dosyasına bu ajan dışında kimse yazamaz: ana oturum ve kod ajanı denerse engellenir. Böylece kodu yazan testi kendi koduna göre şekillendiremez</td></tr>
|
|
779
|
+
<tr><td>Test ayrımı</td><td>Testleri kodu yazan değil, ayrı bir ajan (<code>test-writer</code>, farklı model önerilir) koddan önce yazar. Test dosyasına bu ajan dışında kimse yazamaz: ana oturum ve kod ajanı denerse engellenir. Patch veya betikle dolaylı değişen test dosyası da commit kapısında durur. Böylece kodu yazan testi kendi koduna göre şekillendiremez</td></tr>
|
|
780
780
|
<tr><td>Test koruması</td><td>Beklenti silmek veya yoruma almak, <code>.skip</code>/<code>.only</code>, gevşek beklenti, test dosyasını silmek veya taşımak ve test ayarında kapsamı daraltmak sorulur; kabukla test dosyasına yazmak da</td></tr>
|
|
781
781
|
<tr><td>Deterministik kapı</td><td><code>gate.mjs --run</code>: lint, tip kontrolü, test ve Semgrep çalışır; çıktı kurula girdi olur, kırmızı kontrol RET sayılır. Spec'teki "değişmemesi gereken" davranışların testi yoksa veya kırmızıysa kapı kırmızıdır. Eklenen bir satırda bu makineye özel bir yol (proje klasörünün ya da ev klasörünün tam yolu) varsa da kırmızıdır. Yeni veya değişen komut önce sana gösterilip onaylatılır</td></tr>
|
|
782
782
|
<tr><td>Commit kapısı</td><td>Kurul denetimi ve aynı değişiklikler için yeşil kapı olmadan commit atılmaz; görev sonu onayı commit için de geçerli, aynı değişiklik iki kez denetlenmez; denetimi atlamak (<code>--skip</code>) her seferinde sorulur</td></tr>
|
|
@@ -1070,7 +1070,7 @@ Sıradaki: G-013 şifre sıfırlama · bekleyen karar: e-posta servisi</pre>
|
|
|
1070
1070
|
<footer>
|
|
1071
1071
|
<div class="container">
|
|
1072
1072
|
<a class="brand" href="#top" style="font-size:16px"><svg class="mark" style="width:22px;height:22px"><use href="#mark"/></svg>TillerHand</a>
|
|
1073
|
-
<span>Sürüm 3.
|
|
1073
|
+
<span>Sürüm 3.43.0 · <a href="https://github.com/burakdogrul/TillerHand">GitHub</a> · <a href="https://github.com/burakdogrul/TillerHand/blob/main/LICENSE">MIT lisansı</a> · KVKK bölümü kontrol listesidir, hukuki görüş değildir.</span>
|
|
1074
1074
|
</div>
|
|
1075
1075
|
</footer>
|
|
1076
1076
|
|
package/package.json
CHANGED