tillerhand 3.43.0 → 3.47.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/agents/board-chair.md +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 +4 -3
- 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 +882 -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
|
@@ -7,21 +7,26 @@
|
|
|
7
7
|
import fs from 'node:fs';
|
|
8
8
|
import os from 'node:os';
|
|
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
|
import { isTestFile, isTestConfig } from './test-guard.mjs';
|
|
12
13
|
import { readLock, readPlan, allowedInLock, inScope, realPath, relPath, protectedRule } from './freeze-guard.mjs';
|
|
13
14
|
|
|
15
|
+
// Texts of ask decisions go to the user (message keys); texts of deny decisions are plain English for the AI.
|
|
16
|
+
// Helpers here have no root argument: the project root comes from findRoot().
|
|
17
|
+
const tr = (k, vars) => t(findRoot(), k, vars);
|
|
18
|
+
|
|
14
19
|
const DESTRUCTIVE = [
|
|
15
|
-
[/\bdrop\s+(table|database|schema|index|view|collection)\b/i, 'DROP
|
|
16
|
-
[/\btruncate\s+(table\s+)?\w/i, 'TRUNCATE
|
|
17
|
-
[/\bdelete\s+from\s+[\w."]+\s*(;|"|'|$)/im, 'WHERE
|
|
18
|
-
[/\balter\s+table\b[^;]*\bdrop\s+column\b/i, 'ALTER TABLE ... DROP COLUMN
|
|
19
|
-
[/\b(supabase\s+db\s+reset|prisma\s+migrate\s+reset|prisma\s+db\s+push\b[^;]*--force-reset|drizzle-kit\s+drop)\b/i, '
|
|
20
|
-
[/\b(alembic\s+downgrade\s+base|dropdb\b|rails\s+db:(drop|reset)|django-admin\s+flush|manage\.py\s+flush)\b/i, '
|
|
21
|
-
[/\bdocker\s+(compose\s+down\b[^;]*\s-v\b|volume\s+(rm|prune)\b|system\s+prune\b[^;]*--volumes)/i, 'Docker volume (
|
|
22
|
-
[/\bgit\s+clean\b(?=[^;]*\s--?[a-z]*f)(?=[^;]*\s-[a-z]*d)/i, 'git clean
|
|
23
|
-
[/\bredis-cli\b[^;]*\b(flushall|flushdb)\b/i, 'Redis
|
|
24
|
-
[/\bfirebase\s+firestore:delete\b/i, 'Firestore
|
|
20
|
+
[/\bdrop\s+(table|database|schema|index|view|collection)\b/i, 'DROP deletes a data structure'],
|
|
21
|
+
[/\btruncate\s+(table\s+)?\w/i, 'TRUNCATE empties a table'],
|
|
22
|
+
[/\bdelete\s+from\s+[\w."]+\s*(;|"|'|$)/im, 'DELETE without WHERE: every row in the table is deleted'],
|
|
23
|
+
[/\balter\s+table\b[^;]*\bdrop\s+column\b/i, 'ALTER TABLE ... DROP COLUMN deletes the column and its data'],
|
|
24
|
+
[/\b(supabase\s+db\s+reset|prisma\s+migrate\s+reset|prisma\s+db\s+push\b[^;]*--force-reset|drizzle-kit\s+drop)\b/i, 'Database is being reset'],
|
|
25
|
+
[/\b(alembic\s+downgrade\s+base|dropdb\b|rails\s+db:(drop|reset)|django-admin\s+flush|manage\.py\s+flush)\b/i, 'Database or schema is being deleted'],
|
|
26
|
+
[/\bdocker\s+(compose\s+down\b[^;]*\s-v\b|volume\s+(rm|prune)\b|system\s+prune\b[^;]*--volumes)/i, 'Docker volume (persistent data) is being deleted'],
|
|
27
|
+
[/\bgit\s+clean\b(?=[^;]*\s--?[a-z]*f)(?=[^;]*\s-[a-z]*d)/i, 'git clean permanently deletes untracked files'],
|
|
28
|
+
[/\bredis-cli\b[^;]*\b(flushall|flushdb)\b/i, 'Redis data is being deleted'],
|
|
29
|
+
[/\bfirebase\s+firestore:delete\b/i, 'Firestore data is being deleted'],
|
|
25
30
|
];
|
|
26
31
|
|
|
27
32
|
// Database addresses other than localhost count as prod (or a shared environment)
|
|
@@ -36,15 +41,15 @@ const UNCLEAR_ENV = /\b(vercel|vc)(@[\w.-]+)?\b(?=[^;&|]*\s(env\s+pull|pull)\b)(
|
|
|
36
41
|
|
|
37
42
|
const NEEDS_APPROVAL = [
|
|
38
43
|
// unquote() opens quoted spellings; here only what the shell sees
|
|
39
|
-
[/\b(vercel|vc)\b[^;&|]*(--prod\b|--target(=|\s+)prod(uction)?\b)/i, '
|
|
44
|
+
[/\b(vercel|vc)\b[^;&|]*(--prod\b|--target(=|\s+)prod(uction)?\b)/i, 'guard.destructive.prodDeploy'],
|
|
40
45
|
// The subcommand can also come after flags: vercel --scope team promote <url>
|
|
41
|
-
[/\b(vercel|vc)(@[\w.-]+)?\b[^;&|]*\s(promote|rollback)\b/i, '
|
|
42
|
-
[UNCLEAR_ENV, '
|
|
43
|
-
[/\b(fly|flyctl)\s+deploy\b|\brailway\s+up\b|\bnetlify\s+deploy\b[^;]*--prod/i, '
|
|
44
|
-
[/\beas(-cli(@[\w.-]+)?)?\s+(submit\b|update:(rollback|republish)\b|update\b(?=[^;\n]*--(branch|channel|environment)(=|\s+)['"]?prod(uction)?\b)|deploy\b(?=[^;\n]*--prod\b))/i, '
|
|
45
|
-
[/\bgit\s+push\b[^;]*\b(main|master|production)\b/i, '
|
|
46
|
-
[/\bgit\s+(rebase|filter-branch|filter-repo)\b/i, '
|
|
47
|
-
[/\b(alembic\s+upgrade|prisma\s+migrate\s+deploy|supabase\s+db\s+push)\b/i, '
|
|
46
|
+
[/\b(vercel|vc)(@[\w.-]+)?\b[^;&|]*\s(promote|rollback)\b/i, 'guard.destructive.promoteRollback'],
|
|
47
|
+
[UNCLEAR_ENV, 'guard.destructive.unclearEnv'],
|
|
48
|
+
[/\b(fly|flyctl)\s+deploy\b|\brailway\s+up\b|\bnetlify\s+deploy\b[^;]*--prod/i, 'guard.destructive.serverDeploy'],
|
|
49
|
+
[/\beas(-cli(@[\w.-]+)?)?\s+(submit\b|update:(rollback|republish)\b|update\b(?=[^;\n]*--(branch|channel|environment)(=|\s+)['"]?prod(uction)?\b)|deploy\b(?=[^;\n]*--prod\b))/i, 'guard.destructive.storeSubmit'],
|
|
50
|
+
[/\bgit\s+push\b[^;]*\b(main|master|production)\b/i, 'guard.destructive.pushMain'],
|
|
51
|
+
[/\bgit\s+(rebase|filter-branch|filter-repo)\b/i, 'guard.destructive.rewriteHistory'],
|
|
52
|
+
[/\b(alembic\s+upgrade|prisma\s+migrate\s+deploy|supabase\s+db\s+push)\b/i, 'guard.destructive.migration'],
|
|
48
53
|
];
|
|
49
54
|
|
|
50
55
|
// The test guard runs on file tools; changing a test or test config file from the shell (sed -i, rm, >, mv, git checkout) is asked.
|
|
@@ -106,10 +111,10 @@ export function shellTargets(command, dir) {
|
|
|
106
111
|
// \\?\ and \\.\ drop only before a drive letter; \\?\UNC\ turns back into plain UNC; GLOBALROOT, pipe etc. stay unclear
|
|
107
112
|
let h = t.replace(/^(\\\\|\/\/)[?.][\\/](?=[a-zA-Z]:)/, '').replace(/^(\\\\|\/\/)[?.][\\/]UNC[\\/]/i, '\\\\')
|
|
108
113
|
.replace(/^(\\\\|\/\/)(localhost|127\.0\.0\.1)[\\/]([a-zA-Z])\$(?=[\\/]|$)/i, '$3:');
|
|
109
|
-
if (/^(\\\\|\/\/)/.test(h)) return markUnclear(
|
|
114
|
+
if (/^(\\\\|\/\/)/.test(h)) return markUnclear(tr('guard.destructive.unclearNetPath', { path: t })), null;
|
|
110
115
|
h = h.replace(/\$\{?PWD\}?/g, d).replace(/^~(?=\/|\\|$)/, os.homedir());
|
|
111
|
-
if (/[$`]/.test(h)) return markUnclear(
|
|
112
|
-
if (/\{[^}]*(,|\.\.)[^}]*\}/.test(h)) return markUnclear(
|
|
116
|
+
if (/[$`]/.test(h)) return markUnclear(tr('guard.destructive.unclearVar', { path: t })), null;
|
|
117
|
+
if (/\{[^}]*(,|\.\.)[^}]*\}/.test(h)) return markUnclear(tr('guard.destructive.unclearBrace', { path: t })), null;
|
|
113
118
|
if (process.platform === 'win32') h = h.replace(/^\/([a-zA-Z])(?=\/|$)/, '$1:');
|
|
114
119
|
let p = path.resolve(d, h);
|
|
115
120
|
// Glob: if missing on disk, the folder before the first wildcard is the target (src/*.ts → src, * → current folder)
|
|
@@ -139,7 +144,7 @@ export function shellTargets(command, dir) {
|
|
|
139
144
|
const target = name === 'popd' ? stack.pop() : resolve(a.filter((x) => !/^-/.test(x) && !/^\/d$/i.test(x)).pop() ?? '$HOME');
|
|
140
145
|
if (name === 'pushd') stack.push(dir);
|
|
141
146
|
if (target && fs.existsSync(target)) dir = target;
|
|
142
|
-
else markUnclear(
|
|
147
|
+
else markUnclear(tr('guard.destructive.unclearCd', { name, target: a.join(' ') || tr('guard.destructive.homeDir') }));
|
|
143
148
|
close();
|
|
144
149
|
continue;
|
|
145
150
|
}
|
|
@@ -156,19 +161,19 @@ export function shellTargets(command, dir) {
|
|
|
156
161
|
}
|
|
157
162
|
if (name === 'xargs') {
|
|
158
163
|
const inner = a.filter((x) => !x.startsWith('-'));
|
|
159
|
-
if (writerKind(path.basename(inner[0] ?? '').toLowerCase(), inner.slice(1))) markUnclear(
|
|
164
|
+
if (writerKind(path.basename(inner[0] ?? '').toLowerCase(), inner.slice(1))) markUnclear(tr('guard.destructive.unclearXargs', { command: inner[0] }));
|
|
160
165
|
close();
|
|
161
166
|
continue;
|
|
162
167
|
}
|
|
163
168
|
const kind = writerKind(name, a);
|
|
164
|
-
if (kind === 'unclear') markUnclear(`${name} ${a.find((x) => !x.startsWith('-')) ?? ''}`.trim()
|
|
169
|
+
if (kind === 'unclear') markUnclear(tr('guard.destructive.unclearNoTarget', { command: `${name} ${a.find((x) => !x.startsWith('-')) ?? ''}`.trim() }));
|
|
165
170
|
if (kind === 'target') {
|
|
166
171
|
for (const x of fileArgs(name, a)) {
|
|
167
172
|
const p = resolve(x);
|
|
168
173
|
if (!p) continue;
|
|
169
174
|
targets.push(p);
|
|
170
175
|
// git checkout <branch>: if it is not a path it is a branch switch, the whole working tree may change
|
|
171
|
-
if (name === 'git' && !fs.existsSync(p) && !a.slice(0, a.indexOf(x)).includes('--')) markUnclear(
|
|
176
|
+
if (name === 'git' && !fs.existsSync(p) && !a.slice(0, a.indexOf(x)).includes('--')) markUnclear(tr('guard.destructive.unclearBranch', { sub: a[0], arg: x }));
|
|
172
177
|
}
|
|
173
178
|
}
|
|
174
179
|
close();
|
|
@@ -214,34 +219,32 @@ export function scopeIssue(command, dir, root) {
|
|
|
214
219
|
const touchesApproval = (y) => VIBE_KIT.test(y) || !/^\.\.|^$/.test(path.relative(y, vk)) && !path.isAbsolute(path.relative(y, vk));
|
|
215
220
|
for (const h of targets) {
|
|
216
221
|
if (touchesApproval(h) || touchesApproval(realPath(h))) return { decision: 'ask', reason: /tercihler\.md$/i.test(h)
|
|
217
|
-
?
|
|
218
|
-
:
|
|
222
|
+
? t(root, 'guard.destructive.prefsFile', { path: h })
|
|
223
|
+
: t(root, 'guard.destructive.gateApproval', { path: h }) };
|
|
219
224
|
const rel = relPath(realPath(h), groot);
|
|
220
|
-
const k = protectedRule(rel) ?? protectedRule(`${rel}
|
|
221
|
-
if (k) return { decision: k[0], reason: k[0] === 'deny' ? `${rel}: ${k[2]}`
|
|
222
|
-
: `${rel}: ${k[2]} Kabuk komutuyla değiştiriliyor; kilit, plan ve onay durumları için ilgili kit komutunu kullan. Kullanıcı onayı gerekli.` };
|
|
225
|
+
const k = protectedRule(rel, root) ?? protectedRule(`${rel}/`, root);
|
|
226
|
+
if (k) return { decision: k[0], reason: k[0] === 'deny' ? `${rel}: ${k[2]}` : t(root, 'guard.destructive.protectedShell', { rel, text: k[2] }) };
|
|
223
227
|
}
|
|
224
228
|
// The target could not be resolved but the command names a rule/guard file ($D/AGENTS.md, {AGENTS,X}.md): asked even without a lock/plan
|
|
225
229
|
if (unclear && /AGENTS|CLAUDE|\.(claude|git|github|codex|agents|env|mcp)\b|secrets|vibe-kit/i.test(command)) {
|
|
226
|
-
return { decision: 'ask', reason:
|
|
230
|
+
return { decision: 'ask', reason: t(root, 'guard.destructive.unclearRule', { unclear }) };
|
|
227
231
|
}
|
|
228
232
|
const lock = readLock(root), plan = readPlan(root);
|
|
229
233
|
if (!lock && !plan) return null;
|
|
230
|
-
const label = lock ?
|
|
231
|
-
if (unclear) return { decision: 'ask', reason:
|
|
234
|
+
const label = lock ? t(root, 'guard.destructive.labelLock', { lock }) : t(root, 'guard.destructive.labelPlan', { task: plan.task });
|
|
235
|
+
if (unclear) return { decision: 'ask', reason: t(root, 'guard.destructive.scopeUnclear', { label, unclear }) };
|
|
232
236
|
for (const h of targets) {
|
|
233
237
|
if (!fs.existsSync(h)) continue;
|
|
234
238
|
const p = realPath(h), up = path.relative(p, groot), rel = relPath(p, groot);
|
|
235
239
|
if (!up.startsWith('..') && !path.isAbsolute(up)) {
|
|
236
|
-
return { decision: 'ask', reason:
|
|
240
|
+
return { decision: 'ask', reason: t(root, 'guard.destructive.scopeRoot', { label, rel: rel || '.' }) };
|
|
237
241
|
}
|
|
238
242
|
if (rel.startsWith('..') || path.isAbsolute(rel)) continue;
|
|
239
243
|
if (lock && !allowedInLock(p, lock, groot)) {
|
|
240
|
-
return { decision: 'ask', reason:
|
|
244
|
+
return { decision: 'ask', reason: t(root, 'guard.destructive.lockOutside', { rel, lock }) };
|
|
241
245
|
}
|
|
242
246
|
if (plan && !inScope(fs.statSync(p).isDirectory() ? `${rel}/` : rel, plan.paths)) {
|
|
243
|
-
return { decision: 'ask', reason:
|
|
244
|
-
'onaylarsa node .claude/hooks/freeze-guard.mjs --plan-add <yol>.' };
|
|
247
|
+
return { decision: 'ask', reason: t(root, 'guard.destructive.planOutside', { rel, task: plan.task }) };
|
|
245
248
|
}
|
|
246
249
|
}
|
|
247
250
|
return null;
|
|
@@ -252,22 +255,22 @@ export function scopeIssue(command, dir, root) {
|
|
|
252
255
|
// Only the real .claude/hooks/commit-gate.mjs --approve is kept out of the gate rule (separators optional: unquote drops backslashes;
|
|
253
256
|
// anchored at the start of the path, so sandbox/.claude/hooks/commit-gate.mjs, a renamed gate.mjs, is not exempt).
|
|
254
257
|
const PERMISSION_EQUIVALENT = [
|
|
255
|
-
['deny', /\brm\b(?=[^;&|]*\s--?[a-z]*r)(?=[^;&|]*\s--?[a-z]*f)/i, 'rm -rf
|
|
256
|
-
['deny', /\b(Remove-Item|rm|ri|del|erase)\b[^;&|]*\s-Recurse\b|\b(rmdir|rd|del)\s+[^;&|]*\/s\b/i, '
|
|
257
|
-
['deny', /\bgit\s+reset\s+[^;&|]*--hard\b/i, 'git reset --hard
|
|
258
|
-
['deny', /\bgit\s+push\b[^;&|]*\s(--force\S*|-\w*f\w*|\+\S+)(?=\s|$)/i, 'git push --force
|
|
259
|
-
['deny', /(^|[\s'"=<>/\\])\.env(?!\.example\b)(\.[\w.-]+|\*)?(?=$|[\s'";|&)<>])|(^|[\s'"/\\])secrets[/\\]/i, '.env
|
|
260
|
-
['ask', /freeze-guard\.mjs['"]?\s+['"]?--off\b/i, '
|
|
261
|
-
['ask', /tools\.mjs['"]?\s+['"]?--commands\b/i, '
|
|
262
|
-
['ask', /freeze-guard\.mjs['"]?\s+['"]?--plan-add\b/i, '
|
|
263
|
-
['ask', /freeze-guard\.mjs['"]?\s+['"]?--plan-save\b/i, '
|
|
264
|
-
['ask', /freeze-guard\.mjs['"]?\s+['"]?--plan-end\b/i, '
|
|
265
|
-
['ask', /commit-gate\.mjs\b[^;&|\n]*--skip\b/i, '
|
|
266
|
-
['ask', /prefs\.mjs['"]?\s+['"]?--(add|remove)\b/i, '
|
|
267
|
-
['ask', /(?<!(?:^|[\s'"=])(?:\.[\\/]|\$\{?CLAUDE_PROJECT_DIR\}?[\\/]?)?\.claude[\\/]?hooks[\\/]?commit-)gate\.mjs['"]?\s+['"]?--approve\b/i, '
|
|
258
|
+
['deny', /\brm\b(?=[^;&|]*\s--?[a-z]*r)(?=[^;&|]*\s--?[a-z]*f)/i, 'Bulk deletion with rm -rf'],
|
|
259
|
+
['deny', /\b(Remove-Item|rm|ri|del|erase)\b[^;&|]*\s-Recurse\b|\b(rmdir|rd|del)\s+[^;&|]*\/s\b/i, 'A folder is being bulk deleted'],
|
|
260
|
+
['deny', /\bgit\s+reset\s+[^;&|]*--hard\b/i, 'git reset --hard deletes uncommitted work'],
|
|
261
|
+
['deny', /\bgit\s+push\b[^;&|]*\s(--force\S*|-\w*f\w*|\+\S+)(?=\s|$)/i, 'git push --force overwrites the remote history'],
|
|
262
|
+
['deny', /(^|[\s'"=<>/\\])\.env(?!\.example\b)(\.[\w.-]+|\*)?(?=$|[\s'";|&)<>])|(^|[\s'"/\\])secrets[/\\]/i, 'Access to .env or secrets/ (the AI does not read or write them)'],
|
|
263
|
+
['ask', /freeze-guard\.mjs['"]?\s+['"]?--off\b/i, 'guard.destructive.perm.freezeOff'],
|
|
264
|
+
['ask', /tools\.mjs['"]?\s+['"]?--commands\b/i, 'guard.destructive.perm.toolsCommands'],
|
|
265
|
+
['ask', /freeze-guard\.mjs['"]?\s+['"]?--plan-add\b/i, 'guard.destructive.perm.planAdd'],
|
|
266
|
+
['ask', /freeze-guard\.mjs['"]?\s+['"]?--plan-save\b/i, 'guard.destructive.perm.planSave'],
|
|
267
|
+
['ask', /freeze-guard\.mjs['"]?\s+['"]?--plan-end\b/i, 'guard.destructive.perm.planEnd'],
|
|
268
|
+
['ask', /commit-gate\.mjs\b[^;&|\n]*--skip\b/i, 'guard.destructive.perm.commitSkip'],
|
|
269
|
+
['ask', /prefs\.mjs['"]?\s+['"]?--(add|remove)\b/i, 'guard.destructive.perm.prefsEdit'],
|
|
270
|
+
['ask', /(?<!(?:^|[\s'"=])(?:\.[\\/]|\$\{?CLAUDE_PROJECT_DIR\}?[\\/]?)?\.claude[\\/]?hooks[\\/]?commit-)gate\.mjs['"]?\s+['"]?--approve\b/i, 'guard.destructive.perm.gateApprove'],
|
|
268
271
|
// Shell counterpart of the PROTECTED rule on file tools: a command that names a rule or guard file and writes/deletes is asked
|
|
269
272
|
// (.git: the gate command approval and git hooks live there)
|
|
270
|
-
['ask', /(?=.*((^|[\s'"/\\])\.(claude|codex|agents|git)(?=$|[\s/\\'"])|\bAGENTS\.md|\bCLAUDE\.md))(?=.*(\b(rm|del|erase|rmdir|rd|mv|move|cp|copy|tee|truncate|checkout|restore|Set-Content|Add-Content|Out-File|Remove-Item|Move-Item|Copy-Item)\b|\bsed\s+-i|>|\bnode\s+-e|\bpython\d?\s+-c))/i, '
|
|
273
|
+
['ask', /(?=.*((^|[\s'"/\\])\.(claude|codex|agents|git)(?=$|[\s/\\'"])|\bAGENTS\.md|\bCLAUDE\.md))(?=.*(\b(rm|del|erase|rmdir|rd|mv|move|cp|copy|tee|truncate|checkout|restore|Set-Content|Add-Content|Out-File|Remove-Item|Move-Item|Copy-Item)\b|\bsed\s+-i|>|\bnode\s+-e|\bpython\d?\s+-c))/i, 'guard.destructive.perm.protectedShell'],
|
|
271
274
|
];
|
|
272
275
|
|
|
273
276
|
// What the shell will see: $'..' is opened, quotes, \ escapes and PowerShell ` escapes drop. For matching only.
|
|
@@ -292,7 +295,10 @@ export function permissionEquivalent(command) {
|
|
|
292
295
|
function permissionOne(command) {
|
|
293
296
|
const k = command.replace(/\s+/g, ' ').replace(/\b2>&1|\b2>\s*(\/dev\/null|nul)\b/gi, ''); // an error redirect is not a write
|
|
294
297
|
const e = PERMISSION_EQUIVALENT.find(([, re]) => re.test(k));
|
|
295
|
-
|
|
298
|
+
if (!e) return null;
|
|
299
|
+
return e[0] === 'deny'
|
|
300
|
+
? { decision: 'deny', reason: `${e[2]}. The AI cannot run this command; if needed, the user runs it themselves.` }
|
|
301
|
+
: { decision: 'ask', reason: tr('guard.destructive.approvalSuffix', { reason: tr(e[2]) }) };
|
|
296
302
|
}
|
|
297
303
|
|
|
298
304
|
// When in doubt, ask: instead of emulating the shell, if a protected word shows up while an obfuscation mark is present, the decision is "ask".
|
|
@@ -310,7 +316,7 @@ function obfuscationOne(command) {
|
|
|
310
316
|
if (!OBFUSCATION.test(command)) return null;
|
|
311
317
|
const g = loosen(command);
|
|
312
318
|
return GUARDED.some((re) => re.test(g))
|
|
313
|
-
? { decision: 'ask', reason:
|
|
319
|
+
? { decision: 'ask', reason: tr('guard.destructive.obfuscated') } : null;
|
|
314
320
|
}
|
|
315
321
|
|
|
316
322
|
// A kit guard script (.claude/hooks) run with an indirect spelling: $VAR, $(..), `..`, ${..}, $'..', a glob * ?, xargs, a pipe or < (commit-gate.mjs $S,
|
|
@@ -322,7 +328,7 @@ export function hookScriptIssue(command) {
|
|
|
322
328
|
if (!/\bnode\b/i.test(k) || !/\.claude|hooks|commit-|gate|freeze/i.test(command)) return null;
|
|
323
329
|
// Arguments from elsewhere count too: xargs, a pipe into node, an input redirect or <(...)
|
|
324
330
|
return /\$\(|`|\$\{|\$'|\$[A-Za-z_]|[*?]|\bxargs\b|\|[^|;&]*\bnode\b|<(?!<)/.test(k)
|
|
325
|
-
? { decision: 'ask', reason: '
|
|
331
|
+
? { decision: 'ask', reason: tr('guard.destructive.indirectHook') } : null;
|
|
326
332
|
}
|
|
327
333
|
|
|
328
334
|
// Separators inside quotes turned into spaces: a quoted & or | ("a & b") must not split a command so its later flags escape the rules
|
|
@@ -331,7 +337,7 @@ const blankQuoted = (s) => s.replace(/"(?:[^"\\]|\\.)*"|'[^']*'/g, (q) => q.repl
|
|
|
331
337
|
// path ("src/a.test.ts") is kept. Such commands are still asked through evaluateOne.
|
|
332
338
|
const quotedTextOff = (s) => s.replace(/"(?:[^"\\]|\\.)*"|'[^']*'/g, (q) => (/\s/.test(q) ? ' ' : q.slice(1, -1)));
|
|
333
339
|
const INLINE = /\b(node|deno|bun)\s+(-e|--eval|-p|--print)\b|\bpython[\d.]*\s+-c\b|\b(ruby|perl)\s+-e\b|\b(ba|z|da)?sh\s+-c\b|\b(pwsh|powershell)(\.exe)?\s+(-c|-Command)\b|\bcmd(\.exe)?\s+\/c\b/i;
|
|
334
|
-
const TEST_WRITER_ONLY = '
|
|
340
|
+
const TEST_WRITER_ONLY = 'Only the test-writer agent writes to test files: whoever writes the code cannot change tests (AGENTS.md rule 7). If a test must be added or changed, call the test-writer agent (continue it with SendMessage if it exists).';
|
|
335
341
|
// ctx: { tool, agent } of the call; only Claude Code tells which subagent runs it. Test files are written by the
|
|
336
342
|
// test-writer agent only; test config files stay a question (evaluateOne).
|
|
337
343
|
export function evaluate(command, ctx = {}) {
|
|
@@ -339,28 +345,32 @@ export function evaluate(command, ctx = {}) {
|
|
|
339
345
|
? { decision: 'deny', reason: TEST_WRITER_ONLY } : null;
|
|
340
346
|
// An inline script (node -e, python -c, bash -c) that names a test path may write it; the shell rules cannot see inside
|
|
341
347
|
const inline = INLINE.test(command) && command.split(/[\s'"`(),;=<>]+/).some(isTestCode)
|
|
342
|
-
? { decision: 'ask', reason: '
|
|
348
|
+
? { decision: 'ask', reason: tr('guard.destructive.inlineTest') } : null;
|
|
343
349
|
return strictest(testWrite, inline, evaluateOne(command), evaluateOne(unquote(command)), evaluateOne(blankQuoted(command)), obfuscationOne(command), hookScriptIssue(command));
|
|
344
350
|
}
|
|
345
351
|
function evaluateOne(command) {
|
|
346
352
|
const k = command.replace(/\s+/g, ' ');
|
|
347
353
|
const prodDb = PROD_DB.test(k) || PROD_MARK.test(k);
|
|
348
354
|
if (PROD_SECRET.test(k)) {
|
|
349
|
-
return { decision: 'deny', reason: 'Prod
|
|
355
|
+
return { decision: 'deny', reason: 'Prod environment variables (secrets) are being pulled onto this machine. The local environment and the AI connect to the dev project only; prod values stay in the hosting dashboard. If needed, the user runs it themselves.' };
|
|
350
356
|
}
|
|
351
357
|
for (const [re, reason] of DESTRUCTIVE) {
|
|
352
358
|
if (re.test(k)) {
|
|
353
|
-
return { decision: 'deny', reason: `${reason}${prodDb ? ' — PROD/
|
|
359
|
+
return { decision: 'deny', reason: `${reason}${prodDb ? ' — a PROD/remote database is targeted' : ''}. The AI cannot run this command. If needed, the user takes a backup first and runs it themselves (in Claude Code with "! command" or in their own terminal).` };
|
|
354
360
|
}
|
|
355
361
|
}
|
|
356
|
-
for (const [re,
|
|
357
|
-
if (re.test(k))
|
|
362
|
+
for (const [re, key] of NEEDS_APPROVAL) {
|
|
363
|
+
if (re.test(k)) {
|
|
364
|
+
return prodDb
|
|
365
|
+
? { decision: 'deny', reason: `${tr(key)}. User approval required. The command contains a prod database address; the AI cannot run it.` }
|
|
366
|
+
: { decision: 'ask', reason: tr('guard.destructive.approvalSuffix', { reason: tr(key) }) };
|
|
367
|
+
}
|
|
358
368
|
}
|
|
359
369
|
if (prodDb && /\b(psql|mysql|mongosh|redis-cli|prisma|drizzle-kit|alembic|supabase)\b/i.test(k)) {
|
|
360
|
-
return { decision: 'ask', reason: '
|
|
370
|
+
return { decision: 'ask', reason: tr('guard.destructive.remoteDb') };
|
|
361
371
|
}
|
|
362
372
|
if (writesTest(k)) {
|
|
363
|
-
return { decision: 'ask', reason: '
|
|
373
|
+
return { decision: 'ask', reason: tr('guard.destructive.testShell') };
|
|
364
374
|
}
|
|
365
375
|
return null;
|
|
366
376
|
}
|
|
@@ -370,6 +380,6 @@ if (isMain(import.meta.url)) {
|
|
|
370
380
|
// Of the lists, the stricter wins (deny > ask)
|
|
371
381
|
const results = g ? [evaluate(g.command, g), g.tool !== 'claude' ? permissionEquivalent(g.command) : null, scopeIssue(g.command, g.dir, g.root)].filter(Boolean) : [];
|
|
372
382
|
const s = results.find((x) => x.decision === 'deny') ?? results[0];
|
|
373
|
-
if (s) decide(g.tool, s.decision, `
|
|
383
|
+
if (s) decide(g.tool, s.decision, s.decision === 'ask' ? tr('guard.destructive.prefix', { reason: s.reason }) : `Shield: ${s.reason}`);
|
|
374
384
|
process.exit(0);
|
|
375
385
|
}
|
|
@@ -30,9 +30,9 @@ function writeState(root, d) {
|
|
|
30
30
|
|
|
31
31
|
// The AI runs the command in the note: $( ) or ` in a file name must not get into it, so the name is written only if it uses safe characters
|
|
32
32
|
const SAFE = /^[\p{L}\p{N}_@./+-]+$/u;
|
|
33
|
-
const noteText = (rel, n) => `${SAFE.test(rel) ? rel : '
|
|
34
|
-
`
|
|
35
|
-
'
|
|
33
|
+
const noteText = (rel, n) => `${SAFE.test(rel) ? rel : 'A file whose name has special characters'} has been edited ${n} times in this session. Repeated AI edits can pile up security holes: ` +
|
|
34
|
+
`before going on, run Semgrep on this file (\`semgrep scan --config p/default --config p/owasp-top-ten -- "${SAFE.test(rel) ? rel : '<file path>'}"\`; if it is not installed, see the semgrep job in CI) ` +
|
|
35
|
+
'and review the change with security-auditor. If you cannot fix the same error in 2 attempts, stop and ask the user for direction.';
|
|
36
36
|
|
|
37
37
|
function hook() {
|
|
38
38
|
const g = readInput();
|
package/.claude/hooks/flow.mjs
CHANGED
|
@@ -28,11 +28,11 @@ const SENSITIVE = /auth|login|session|passw|payment|ödeme|checkout|billing|webh
|
|
|
28
28
|
// Same bands as the cost table in docs/workflow/usage-limits.md §3 (1-2, 3-6, more files)
|
|
29
29
|
export function flow({ files = 0, paths, installed = [], context, level: lvl }) {
|
|
30
30
|
if (paths) files = paths.length;
|
|
31
|
-
const size = files <= 2 ? '
|
|
31
|
+
const size = files <= 2 ? 'small' : files <= 6 ? 'medium' : 'large';
|
|
32
32
|
const clear = context != null && context >= 50;
|
|
33
33
|
// A full main context is paid again on every turn: the code goes out of it
|
|
34
|
-
const code = size === '
|
|
35
|
-
const base = {
|
|
34
|
+
const code = size === 'small' && !clear ? 'main' : 'subagent';
|
|
35
|
+
const base = { small: 1, medium: 2, large: 3 }[size];
|
|
36
36
|
// Yellow halves the members (rules.md §4); red runs one, so a RET stops before the rest is paid for
|
|
37
37
|
const members = lvl === 'red' ? 1 : lvl === 'yellow' ? Math.ceil(base / 2) : base;
|
|
38
38
|
// Most matching files first; only members this project's board has
|
|
@@ -44,13 +44,13 @@ export function flow({ files = 0, paths, installed = [], context, level: lvl })
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
export function flowText(f) {
|
|
47
|
-
const where = (w) => (w === '
|
|
47
|
+
const where = (w) => (w === 'main' ? 'main session' : 'subagent');
|
|
48
48
|
return [
|
|
49
|
-
`
|
|
50
|
-
`
|
|
51
|
-
`
|
|
52
|
-
`
|
|
53
|
-
...(f.clear ? [
|
|
49
|
+
`Flow: ${f.files} files · ${f.size} · context ${f.context == null ? 'unknown' : `${f.context}%`} · limit ${LEVEL_LABEL[f.level]}`,
|
|
50
|
+
`Code: ${where(f.code)}${f.code === 'main' ? '' : ' (code role)'}`,
|
|
51
|
+
`Docs work (roadmap, progress and the cost log go through progress.mjs; this line is for the rest): ${where(f.docs)}${f.docs === 'main' ? '' : ' (docs role)'}`,
|
|
52
|
+
`Board: ${f.board?.length ? f.board.join(', ') : f.board ? 'no matching member' : `${f.members} matching members`} + code-reviewer${f.security ? ' + security-auditor (sensitive path)' : ''}, ${f.parallel ? 'in parallel' : 'in sequence'}${f.board ? '' : ' (plus security-auditor on a sensitive path)'}`,
|
|
53
|
+
...(f.clear ? [`Suggest /clear first: context ${f.context}%, every turn rereads it.`] : []),
|
|
54
54
|
].join('\n');
|
|
55
55
|
}
|
|
56
56
|
|
|
@@ -64,7 +64,7 @@ if (isMain(import.meta.url)) {
|
|
|
64
64
|
const i = process.argv.indexOf(name);
|
|
65
65
|
if (i < 0) return undefined;
|
|
66
66
|
const n = Number(process.argv[i + 1]);
|
|
67
|
-
if (!Number.isFinite(n) || n < 0) { console.error('
|
|
67
|
+
if (!Number.isFinite(n) || n < 0) { console.error('Usage: node .claude/hooks/flow.mjs [--files N] [--context N] [--limit N] [--json]'); process.exit(1); }
|
|
68
68
|
return n;
|
|
69
69
|
};
|
|
70
70
|
const files = arg('--files');
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import fs from 'node:fs';
|
|
18
18
|
import path from 'node:path';
|
|
19
19
|
import { readInput, decide, findRoot, isMain } from './shared.mjs';
|
|
20
|
+
import { t } from './lang.mjs';
|
|
20
21
|
|
|
21
22
|
// Project root even when called from a subfolder: lock and plan are written where the hook reads them; paths are root-relative
|
|
22
23
|
const root = findRoot();
|
|
@@ -27,10 +28,14 @@ const FREE = ['docs/workflow/progress.md', '.claude/state/'];
|
|
|
27
28
|
|
|
28
29
|
// Counterpart of .claude/settings.json → permissions (deny: .env and secrets/, ask: rule and guard files)
|
|
29
30
|
const PROTECTED = [
|
|
30
|
-
['deny', /^(\.env(\.local|\.production|\.[^/]+\.local)?|secrets(\/.*)?)$/i, '
|
|
31
|
-
['ask', /^(CLAUDE\.md|AGENTS\.md|\.mcp\.json|\.claude\/.*|\.codex\/.*|\.agents\/hooks\.json|\.github\/workflows\/.*|\.git(\/.*)?)$/i, '
|
|
31
|
+
['deny', /^(\.env(\.local|\.production|\.[^/]+\.local)?|secrets(\/.*)?)$/i, 'Environment and secret files are not read or written by the AI.'],
|
|
32
|
+
['ask', /^(CLAUDE\.md|AGENTS\.md|\.mcp\.json|\.claude\/.*|\.codex\/.*|\.agents\/hooks\.json|\.github\/workflows\/.*|\.git(\/.*)?)$/i, 'guard.freeze.protectedAsk'],
|
|
32
33
|
];
|
|
33
|
-
|
|
34
|
+
// Returns [decision, pattern, text]; the text of an ask rule is in the user's language (message key), a deny rule's is for the AI
|
|
35
|
+
export const protectedRule = (rel, r = root) => {
|
|
36
|
+
const e = PROTECTED.find(([, re]) => re.test(rel));
|
|
37
|
+
return e ? [e[0], e[1], e[0] === 'ask' ? t(r, e[2]) : e[2]] : null;
|
|
38
|
+
};
|
|
34
39
|
|
|
35
40
|
export const relPath = (file, rootDir) => path.relative(rootDir, path.resolve(rootDir, file)).split(path.sep).join('/');
|
|
36
41
|
|
|
@@ -79,18 +84,18 @@ function setPlan(mode, args) {
|
|
|
79
84
|
const current = readPlan();
|
|
80
85
|
if (mode === '--plan-end') {
|
|
81
86
|
fs.rmSync(planPath(), { force: true });
|
|
82
|
-
return console.log(current ? `
|
|
87
|
+
return console.log(current ? `Scope shield closed (${current.task}).` : 'No active plan.');
|
|
83
88
|
}
|
|
84
89
|
const [task, ...paths] = mode === '--plan-save' ? args : [null, ...args];
|
|
85
90
|
const fail = (m) => { console.error(m); process.exit(1); };
|
|
86
|
-
if (mode === '--plan-save' && current) fail(`
|
|
87
|
-
if (mode === '--plan-add' && !current) fail('
|
|
88
|
-
if ((mode === '--plan-save' && !task) || !paths.length) fail(`
|
|
91
|
+
if (mode === '--plan-save' && current) fail(`A plan is active (${current.task}). To widen the scope, use --plan-add with user approval; if the task is done, run --plan-end first.`);
|
|
92
|
+
if (mode === '--plan-add' && !current) fail('No active plan; run --plan-save <task> <path...> first.');
|
|
93
|
+
if ((mode === '--plan-save' && !task) || !paths.length) fail(`Usage: ${mode === '--plan-save' ? '--plan-save <task> <path...>' : '--plan-add <path...>'}`);
|
|
89
94
|
const normal = paths.map(normalPlanPath);
|
|
90
|
-
if (normal.includes(null)) fail(`
|
|
95
|
+
if (normal.includes(null)) fail(`A path outside the project is not added to the plan: ${paths[normal.indexOf(null)]}`);
|
|
91
96
|
const plan = current ? { task: current.task, paths: [...new Set([...current.paths, ...normal])] } : { task, paths: [...new Set(normal)] };
|
|
92
97
|
writePlan(plan);
|
|
93
|
-
console.log(`
|
|
98
|
+
console.log(`Scope shield: ${plan.task} · ${plan.paths.length} paths. Writing to an existing file outside the list is asked.`);
|
|
94
99
|
}
|
|
95
100
|
|
|
96
101
|
const MODES = ['--set', '--off', '--status', '--plan-save', '--plan-add', '--plan-end'];
|
|
@@ -100,25 +105,25 @@ const MODES = ['--set', '--off', '--status', '--plan-save', '--plan-add', '--pla
|
|
|
100
105
|
function configure(mode, value) {
|
|
101
106
|
if (mode === '--off') {
|
|
102
107
|
fs.rmSync(LOCK, { force: true });
|
|
103
|
-
console.log('
|
|
108
|
+
console.log('Edit lock removed.');
|
|
104
109
|
} else if (mode === '--set' && value) {
|
|
105
110
|
const dir = relPath(value, root).replace(/\/+$/, '');
|
|
106
|
-
if (!dir || dir.startsWith('..')) { console.error('
|
|
111
|
+
if (!dir || dir.startsWith('..')) { console.error('The folder must be inside the project.'); process.exit(1); }
|
|
107
112
|
const current = readLock();
|
|
108
113
|
if (current && !allowedInLock(`${dir}/x`, current)) {
|
|
109
|
-
console.error(`
|
|
114
|
+
console.error(`A lock is on (${current}/). It can only be narrowed; to widen it, first run /freeze off with user approval.`);
|
|
110
115
|
process.exit(1);
|
|
111
116
|
}
|
|
112
117
|
fs.mkdirSync(path.dirname(LOCK), { recursive: true });
|
|
113
118
|
fs.writeFileSync(LOCK, JSON.stringify({ dir, date: new Date().toISOString() }, null, 2));
|
|
114
|
-
console.log(`
|
|
119
|
+
console.log(`Edit lock: only ${dir}/ (to remove it: /freeze off)`);
|
|
115
120
|
} else if (mode.startsWith('--plan-')) {
|
|
116
121
|
setPlan(mode, process.argv.slice(3));
|
|
117
122
|
} else {
|
|
118
123
|
const k = readLock();
|
|
119
124
|
const p = readPlan();
|
|
120
|
-
console.log(k ? `
|
|
121
|
-
console.log(p ? `
|
|
125
|
+
console.log(k ? `Edit lock is on: ${k}/` : 'No edit lock.');
|
|
126
|
+
console.log(p ? `Scope shield: ${p.task} · ${p.paths.length} paths (${p.paths.join(', ')})` : 'No active plan.');
|
|
122
127
|
}
|
|
123
128
|
}
|
|
124
129
|
|
|
@@ -130,30 +135,29 @@ function hook() {
|
|
|
130
135
|
// Gate command approval record (gate.mjs, in the user folder ~/.vibe-kit/): the AI must not write it by hand
|
|
131
136
|
const approval = paths.find((p) => /[\\/]\.vibe-kit([\\/]|$)/i.test(p));
|
|
132
137
|
if (approval) decide(g.tool, 'ask', /tercihler\.md$/i.test(approval)
|
|
133
|
-
?
|
|
134
|
-
:
|
|
138
|
+
? t(g.root, 'guard.freeze.prefsFile', { path: approval })
|
|
139
|
+
: t(g.root, 'guard.freeze.gateApproval', { path: approval }));
|
|
135
140
|
if (g.tool !== 'claude') {
|
|
136
141
|
for (const p of paths) {
|
|
137
|
-
const k = protectedRule(relPath(p, groot));
|
|
142
|
+
const k = protectedRule(relPath(p, groot), g.root);
|
|
138
143
|
if (k) decide(g.tool, k[0], `${relPath(p, groot)}: ${k[2]}`);
|
|
139
144
|
}
|
|
140
145
|
// Reads (view_file etc.): only .env and secrets/ are denied (the counterpart of Read(./.env) in Claude)
|
|
141
146
|
for (const p of g.reads) {
|
|
142
147
|
const rel = relPath(realPath(path.resolve(g.dir, p)), groot);
|
|
143
|
-
const k = protectedRule(rel);
|
|
148
|
+
const k = protectedRule(rel, g.root);
|
|
144
149
|
if (k?.[0] === 'deny') decide(g.tool, 'deny', `${rel}: ${k[2]}`);
|
|
145
150
|
}
|
|
146
151
|
}
|
|
147
152
|
const lock = readLock(g.root);
|
|
148
153
|
const outside = lock && paths.find((p) => !allowedInLock(p, lock, groot));
|
|
149
|
-
if (outside) decide(g.tool, 'deny', `
|
|
154
|
+
if (outside) decide(g.tool, 'deny', `Edit lock: only ${lock}/ may be changed. If you need this file, ask the user; to remove the lock use /freeze off.`);
|
|
150
155
|
const plan = readPlan(g.root);
|
|
151
156
|
for (const [i, p] of (plan ? paths : []).entries()) {
|
|
152
157
|
const rel = relPath(p, groot);
|
|
153
158
|
// A new file is not asked (the structure guard looks at it); deleting and overwriting an existing one is
|
|
154
159
|
if (inScope(rel, plan.paths) || !(g.files[i].deleted || fs.existsSync(p))) continue;
|
|
155
|
-
decide(g.tool, 'ask',
|
|
156
|
-
`onaylarsa node .claude/hooks/freeze-guard.mjs --plan-add <yol>. Görev bittiyse --plan-end.`);
|
|
160
|
+
decide(g.tool, 'ask', t(g.root, 'guard.freeze.scopeAsk', { rel: rel || t(g.root, 'guard.freeze.unknownPath'), task: plan.task }));
|
|
157
161
|
}
|
|
158
162
|
process.exit(0);
|
|
159
163
|
}
|
|
@@ -162,7 +166,7 @@ if (isMain(import.meta.url)) {
|
|
|
162
166
|
const [mode, value] = process.argv.slice(2);
|
|
163
167
|
// A mistyped or removed flag must fail loudly: run as the hook it would pass silently and the AI would think the plan is saved
|
|
164
168
|
if (mode?.startsWith('--') && !MODES.includes(mode)) {
|
|
165
|
-
console.error(`
|
|
169
|
+
console.error(`Unknown flag: ${mode}. Usage: freeze-guard.mjs ${MODES.join(' | ')}`);
|
|
166
170
|
process.exit(1);
|
|
167
171
|
}
|
|
168
172
|
MODES.includes(mode) ? configure(mode, value) : hook();
|