@haposoft/cafekit 0.14.0 → 0.14.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +27 -3
  2. package/bin/__tests__/installer-safety.test.js +145 -0
  3. package/bin/install.js +9 -1
  4. package/bin/lib/backup.js +60 -16
  5. package/bin/lib/context.js +1 -1
  6. package/bin/phases/claude-runtime.js +57 -4
  7. package/bin/phases/post-install.js +39 -26
  8. package/bin/phases/root-config.js +24 -5
  9. package/bin/phases/select-platform.js +14 -9
  10. package/package.json +1 -1
  11. package/src/claude/CLAUDE.md +1 -1
  12. package/src/claude/agents/code-auditor.md +1 -1
  13. package/src/claude/agents/debugger.md +1 -0
  14. package/src/claude/agents/god-developer.md +10 -1
  15. package/src/claude/agents/inspector.md +1 -0
  16. package/src/claude/agents/spec-maker.md +1 -1
  17. package/src/claude/agents/test-runner.md +2 -2
  18. package/src/claude/gitignore +11 -4
  19. package/src/claude/hooks/__tests__/privacy-block.test.js +50 -10
  20. package/src/claude/hooks/__tests__/spec-gate.test.js +75 -1
  21. package/src/claude/hooks/__tests__/state.test.js +65 -0
  22. package/src/claude/hooks/docs-sync.cjs +15 -21
  23. package/src/claude/hooks/inspect-block.cjs +1 -1
  24. package/src/claude/hooks/lib/config.cjs +0 -5
  25. package/src/claude/hooks/privacy-block.cjs +39 -62
  26. package/src/claude/hooks/spec-gate.cjs +18 -7
  27. package/src/claude/hooks/spec-state.cjs +4 -0
  28. package/src/claude/hooks/state.cjs +13 -3
  29. package/src/claude/hooks/usage.cjs +6 -0
  30. package/src/claude/migration-manifest.json +1 -0
  31. package/src/claude/rules/orchestrator.md +2 -2
  32. package/src/claude/rules/skill-domain-routing.md +27 -86
  33. package/src/claude/rules/skill-workflow-routing.md +11 -74
  34. package/src/claude/rules/state-sync.md +1 -1
  35. package/src/claude/runtime.json +7 -7
  36. package/src/claude/scripts/validate-spec-output.cjs +6 -0
  37. package/src/claude/skills/code-review/references/spec-compliance-review.md +1 -1
  38. package/src/claude/skills/delegate/SKILL.md +124 -0
  39. package/src/claude/skills/delegate/references/codex-delegation.md +71 -0
  40. package/src/claude/skills/delegate/references/grok-delegation.md +84 -0
  41. package/src/claude/skills/develop/SKILL.md +26 -5
  42. package/src/claude/skills/develop/references/parallel-waves.md +112 -0
  43. package/src/claude/skills/develop/references/quality-gate.md +10 -2
  44. package/src/claude/skills/hotfix/references/parallel-patterns.md +1 -1
  45. package/src/claude/skills/inspect/SKILL.md +42 -113
  46. package/src/claude/skills/inspect/references/internal-inspection.md +16 -135
  47. package/src/claude/skills/specs/SKILL.md +134 -386
  48. package/src/claude/skills/specs/rules/tasks-generation.md +1 -1
  49. package/src/claude/skills/specs/templates/task.md +3 -1
  50. package/src/claude/skills/sync/SKILL.md +2 -2
  51. package/src/claude/skills/sync/references/sync-protocols.md +4 -4
  52. package/src/claude/skills/test/SKILL.md +1 -1
  53. package/src/claude/status.cjs +9 -6
  54. package/src/opencode/plugins/docs-sync.ts +10 -16
  55. package/src/claude/archive-command/code/SKILL.md +0 -55
  56. package/src/claude/archive-command/code/references/execution-loop.md +0 -21
  57. package/src/claude/archive-command/code.md +0 -17
  58. package/src/claude/archive-command/docs.md +0 -609
  59. package/src/claude/archive-command/review/codebase/parallel.md +0 -76
  60. package/src/claude/archive-command/spec-design.md +0 -247
  61. package/src/claude/archive-command/spec-init.md +0 -120
  62. package/src/claude/archive-command/spec-requirements.md +0 -138
  63. package/src/claude/archive-command/spec-status.md +0 -98
  64. package/src/claude/archive-command/spec-tasks.md +0 -173
  65. package/src/claude/archive-command/spec-validate.md +0 -118
  66. package/src/claude/archive-command/test.md +0 -8
@@ -2,6 +2,7 @@
2
2
  name: debugger
3
3
  description: "Investigates bugs, incidents, CI/log/DB/performance/frontend failures, traces exact root causes with evidence, and hands off a verification-ready fix plan. Edits code only when explicitly requested by a fix workflow."
4
4
  model: sonnet
5
+ memory: user
5
6
  tools: Glob, Grep, Read, Bash, WebFetch, WebSearch
6
7
  ---
7
8
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: god-developer
3
- description: "Primary code execution agent. Receives specifications (spec) from hapo:specs or task files and transforms them into production-grade source code. Operates on a Single-Track principle (linear, non-parallel)."
3
+ description: "Primary code execution agent. Receives specifications (spec) from hapo:specs or task files and transforms them into production-grade source code. Operates on a Single-Track principle within its workspace (linear inside one working tree; multiple instances may run concurrently in separate isolated worktrees)."
4
4
  model: sonnet
5
5
  tools: Glob, Grep, Read, Edit, Write, NotebookEdit, Bash, WebFetch, WebSearch
6
6
  ---
@@ -94,3 +94,12 @@ Upon completion, output a concise report in this format:
94
94
  - Do not add AI attribution to code or commit messages.
95
95
  - Prioritize security (validate input, never hardcode secrets).
96
96
  - Code should be self-documenting — only add comments for complex logic.
97
+
98
+ ## Worktree Conduct (Parallel Wave dispatch)
99
+
100
+ When dispatched into an isolated git worktree by `hapo:develop --parallel`:
101
+
102
+ - Work ONLY inside this worktree; Single-Track discipline applies unchanged within it.
103
+ - Do NOT edit `spec.json` or any `tasks/*.md` — the orchestrator is the single writer of spec state.
104
+ - Do NOT touch files outside this task's `Related Files`.
105
+ - Commit your completed work: `git commit -m "task(<id>): <title>"` — an uncommitted worktree cannot be merged back.
@@ -3,6 +3,7 @@ name: inspector
3
3
  tools: Glob, Grep, Read, Bash
4
4
  description: "Codebase structure scanner. Use this agent when you need to quickly scout/inspect the codebase architecture, files, and directories. Specializes in finding relevant files for a given work scope before implementation begins."
5
5
  model: haiku
6
+ memory: user
6
7
  ---
7
8
 
8
9
  # Inspect — Codebase Scout
@@ -127,7 +127,7 @@ Before writing `design.md`, select a discovery mode and record the reason:
127
127
  - When requirement coverage format: list numeric IDs only, no descriptive suffixes
128
128
  - Apply `(P)` parallel markers when applicable (load `.claude/skills/specs/rules/tasks-parallel-analysis.md`)
129
129
  - Every task MUST use the compact implementation-ready shape: `Context`, `Steps`, `Requirements`, `Related Files`, `Completion Criteria`, `Evidence`, `Risk Assessment`.
130
- - `Evidence` MUST include exact commands, artifacts/runtime surfaces, runtime reachability proof, and negative-path checks. Existing specs may use `Task Test Plan & Verification Evidence` or legacy `Verification & Evidence`.
130
+ - `## Evidence` (legacy heading aliases still parse) MUST include exact commands, artifacts/runtime surfaces, runtime reachability proof, and negative-path checks.
131
131
  - Completion criteria MUST be objective enough that a downstream quality gate can prove them without guesswork.
132
132
  - UI/app/runtime workflows MUST include a final integration/reachability task or final integration section that names the real entrypoint and proves all scoped user-facing surfaces are wired.
133
133
  - Do not allow orphan task outputs: components, services, hooks, routes, commands, workers, providers, reducers, data loaders, and generated artifacts must be reachable now or assigned to a named later integration task.
@@ -11,7 +11,7 @@ You are a battle-hardened QA engineer who has been burned by production incident
11
11
 
12
12
  ## Task-Aware Inputs
13
13
 
14
- If the prompt includes task file paths, Completion Criteria, Evidence, Task Test Plan & Verification Evidence, or legacy Verification & Evidence instructions, treat them as authoritative.
14
+ If the prompt includes task file paths, Completion Criteria, or `## Evidence` (legacy heading aliases still parse) instructions, treat them as authoritative.
15
15
  Diff-aware test selection does NOT replace task-specific verification.
16
16
  If the task/spec names a specific framework, auth system, transport, or shared-state boundary, keep that contract visible while evaluating evidence.
17
17
  If the prompt includes a feature name or `specs/<feature>`, load `spec.json`, `requirements.md`, `design.md`, and the active/recent task files. Treat `scope_lock`, Completion Criteria, and Evidence as the test contract.
@@ -30,7 +30,7 @@ Select tests by the task's touched surface:
30
30
  ## Command Resolution Order
31
31
 
32
32
  When the task file names exact commands, use this order:
33
- 1. Run every exact executable command from `Evidence` (or `Task Test Plan & Verification Evidence` / legacy `Verification & Evidence`) in declaration order.
33
+ 1. Run every exact executable command from `## Evidence` (legacy heading aliases still parse) in declaration order.
34
34
  2. Run repo-default typecheck/test/build commands only to fill gaps not already covered above.
35
35
  3. Apply diff-aware test selection only after task-mandated commands are satisfied.
36
36
 
@@ -1,19 +1,26 @@
1
- # Environment and Sandbox credentials within .claude
1
+ # CafeKit local runtime (.claude/ or .opencode/)
2
+ # Also ignored at project root by the installer. This file protects
3
+ # force-adds (`git add -f`) and partial un-ignores of the runtime folder.
4
+
5
+ # Secrets and local credentials
2
6
  .env
3
7
  .env.*
4
8
  !.env.example
5
9
 
6
- # Virtual environments for python scripts (skills)
10
+ # Skill dependency installs (Python venv + npm)
7
11
  skills/.venv/
12
+ skills/**/.venv/
13
+ skills/**/node_modules/
8
14
  .venv/
9
15
  venv/
10
- skills/**/node_modules/
11
16
  __pycache__/
17
+ **/__pycache__/
12
18
  *.pyc
13
19
 
14
- # System generated state, caches, and logs
20
+ # Generated session state, caches, and logs
15
21
  session-state/
16
22
  hooks/.logs/
23
+ plugins/.logs/
17
24
  agent-memory/
18
25
  settings.bak.json
19
26
  .mcp.json
@@ -1,8 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  // Behavioral tests for privacy-block.cjs. The hook is run as a real subprocess:
4
- // a PreToolUse payload is piped to stdin and the exit code is asserted
5
- // (0 = allow, 2 = block). Covers the symlink-bypass hardening.
4
+ // a PreToolUse payload is piped to stdin and native permission output asserted.
6
5
 
7
6
  const { test } = require('node:test');
8
7
  const assert = require('node:assert');
@@ -13,40 +12,47 @@ const path = require('node:path');
13
12
 
14
13
  const HOOK = path.join(__dirname, '..', 'privacy-block.cjs');
15
14
 
16
- /** Run the hook with a tool payload; return { code, stderr }. */
15
+ /** Run the hook with a tool payload; return process output. */
17
16
  function runHook(payload, cwd) {
18
17
  const res = spawnSync(process.execPath, [HOOK], {
19
18
  input: JSON.stringify({ ...payload, cwd: cwd || payload.cwd }),
20
19
  encoding: 'utf8',
21
20
  });
22
- return { code: res.status, stderr: res.stderr || '' };
21
+ return { code: res.status, stdout: res.stdout || '', stderr: res.stderr || '' };
23
22
  }
24
23
 
25
24
  function tmpDir() {
26
25
  return fs.mkdtempSync(path.join(os.tmpdir(), 'ck-privacy-'));
27
26
  }
28
27
 
29
- test('blocks a direct Read of .env (exit 2)', () => {
28
+ function permissionDecision(stdout) {
29
+ if (!stdout.trim()) return null;
30
+ return JSON.parse(stdout).hookSpecificOutput?.permissionDecision || null;
31
+ }
32
+
33
+ test('asks natively for a direct Read of .env', () => {
30
34
  const dir = tmpDir();
31
35
  try {
32
36
  const env = path.join(dir, '.env');
33
37
  fs.writeFileSync(env, 'SECRET=1');
34
- const { code } = runHook({ tool_name: 'Read', tool_input: { file_path: env } }, dir);
35
- assert.strictEqual(code, 2);
38
+ const { code, stdout } = runHook({ tool_name: 'Read', tool_input: { file_path: env } }, dir);
39
+ assert.strictEqual(code, 0);
40
+ assert.strictEqual(permissionDecision(stdout), 'ask');
36
41
  } finally {
37
42
  fs.rmSync(dir, { recursive: true, force: true });
38
43
  }
39
44
  });
40
45
 
41
- test('blocks a symlink whose target is .env (regression: symlink bypass)', () => {
46
+ test('asks for a symlink whose target is .env (regression: symlink bypass)', () => {
42
47
  const dir = tmpDir();
43
48
  try {
44
49
  const env = path.join(dir, '.env');
45
50
  fs.writeFileSync(env, 'SECRET=1');
46
51
  const link = path.join(dir, 'notes.txt'); // harmless-looking name
47
52
  fs.symlinkSync(env, link);
48
- const { code } = runHook({ tool_name: 'Read', tool_input: { file_path: link } }, dir);
49
- assert.strictEqual(code, 2, 'symlink to .env must be blocked, not allowed');
53
+ const { code, stdout } = runHook({ tool_name: 'Read', tool_input: { file_path: link } }, dir);
54
+ assert.strictEqual(code, 0);
55
+ assert.strictEqual(permissionDecision(stdout), 'ask');
50
56
  } finally {
51
57
  fs.rmSync(dir, { recursive: true, force: true });
52
58
  }
@@ -77,3 +83,37 @@ test('allows a symlink to an exempt .env.example (exemption still wins)', () =>
77
83
  fs.rmSync(dir, { recursive: true, force: true });
78
84
  }
79
85
  });
86
+
87
+ test('an exempt-looking symlink to .env still asks based on its target', () => {
88
+ const dir = tmpDir();
89
+ try {
90
+ const env = path.join(dir, '.env');
91
+ fs.writeFileSync(env, 'SECRET=1');
92
+ const link = path.join(dir, '.env.example');
93
+ fs.symlinkSync(env, link);
94
+ const { stdout } = runHook({ tool_name: 'Read', tool_input: { file_path: link } }, dir);
95
+ assert.strictEqual(permissionDecision(stdout), 'ask');
96
+ } finally {
97
+ fs.rmSync(dir, { recursive: true, force: true });
98
+ }
99
+ });
100
+
101
+ for (const command of [
102
+ 'cat ".env"',
103
+ 'cat .env',
104
+ 'cat .env*',
105
+ 'sed -n 1p .env',
106
+ "bash -c 'sed -n 1p .env'",
107
+ ]) {
108
+ test(`asks natively for Bash sensitive reference: ${command}`, () => {
109
+ const dir = tmpDir();
110
+ try {
111
+ fs.writeFileSync(path.join(dir, '.env'), 'SECRET=1');
112
+ const { code, stdout } = runHook({ tool_name: 'Bash', tool_input: { command } }, dir);
113
+ assert.strictEqual(code, 0);
114
+ assert.strictEqual(permissionDecision(stdout), 'ask');
115
+ } finally {
116
+ fs.rmSync(dir, { recursive: true, force: true });
117
+ }
118
+ });
119
+ }
@@ -64,7 +64,7 @@ function readCache() {
64
64
  * @param {string} [opts.taskStatus='done']
65
65
  * @param {string|null} [opts.completed_at='2026-07-01T00:00:00Z']
66
66
  * @param {string} [opts.mdStatus='done']
67
- * @param {'valid'|'missing-evidence'|'placeholder'|'none'} [opts.evidence='valid']
67
+ * @param {'valid'|'legacy-valid'|'failed'|'fence-only'|'missing-evidence'|'placeholder'|'none'} [opts.evidence='valid']
68
68
  * @param {object|null} [opts.runtime] — if set, write .claude/runtime.json
69
69
  */
70
70
  function makeFixture(opts = {}) {
@@ -97,12 +97,19 @@ function makeFixture(opts = {}) {
97
97
  evidenceBlock = [
98
98
  '## Evidence',
99
99
  '',
100
+ 'Verification: PASS',
100
101
  '```',
101
102
  'npm test',
102
103
  'PASS: 10 tests',
103
104
  '```',
104
105
  '',
105
106
  ].join('\n');
107
+ } else if (evidence === 'legacy-valid') {
108
+ evidenceBlock = '## Evidence\n\nnpm test — passed, exit code 0\n';
109
+ } else if (evidence === 'failed') {
110
+ evidenceBlock = '## Evidence\n\nVerification: PASS\n\nFAIL: tests failed, exit code 1\n';
111
+ } else if (evidence === 'fence-only') {
112
+ evidenceBlock = '## Evidence\n\n```\nnpm test\n```\n';
106
113
  } else if (evidence === 'placeholder') {
107
114
  evidenceBlock = [
108
115
  '## Evidence',
@@ -268,3 +275,70 @@ test('7. Evidence with {{...}} placeholder → blocked (check c)', () => {
268
275
  fs.rmSync(dir, { recursive: true, force: true });
269
276
  }
270
277
  });
278
+
279
+ test('8. explicit failure cannot pass even when PASS text is present', () => {
280
+ const dir = makeFixture({ evidence: 'failed' });
281
+ try {
282
+ seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
283
+ const body = parseBlock(runHook({}, dir).stdout);
284
+ assert.ok(body);
285
+ assert.match(body.reason, /\bc\b/);
286
+ } finally {
287
+ fs.rmSync(dir, { recursive: true, force: true });
288
+ }
289
+ });
290
+
291
+ test('9. a code fence alone is not verification proof', () => {
292
+ const dir = makeFixture({ evidence: 'fence-only' });
293
+ try {
294
+ seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
295
+ assert.ok(parseBlock(runHook({}, dir).stdout));
296
+ } finally {
297
+ fs.rmSync(dir, { recursive: true, force: true });
298
+ }
299
+ });
300
+
301
+ test('10. completed_at must be a valid ISO timestamp', () => {
302
+ const dir = makeFixture({ completed_at: 'yesterday' });
303
+ try {
304
+ seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
305
+ const body = parseBlock(runHook({}, dir).stdout);
306
+ assert.ok(body);
307
+ assert.match(body.reason, /\bd\b/);
308
+ } finally {
309
+ fs.rmSync(dir, { recursive: true, force: true });
310
+ }
311
+ });
312
+
313
+ test('11. done → pending cache transition is persisted and can be gated again', () => {
314
+ const dir = makeFixture({ taskStatus: 'pending', mdStatus: 'pending' });
315
+ try {
316
+ seedCache({ [FEATURE]: { [TASK_REL]: 'done' } });
317
+ assert.strictEqual(runHook({}, dir).stdout, '');
318
+ assert.strictEqual(readCache()[FEATURE][TASK_REL], 'pending');
319
+
320
+ const specFile = path.join(dir, 'specs', FEATURE, 'spec.json');
321
+ const spec = JSON.parse(fs.readFileSync(specFile, 'utf8'));
322
+ spec.task_registry[TASK_REL] = {
323
+ status: 'done',
324
+ completed_at: '2026-07-01T00:00:00Z',
325
+ };
326
+ fs.writeFileSync(specFile, JSON.stringify(spec));
327
+ const taskFile = path.join(dir, 'specs', FEATURE, TASK_REL);
328
+ fs.writeFileSync(taskFile, '# Task\n\n**Status:** done\n\n## Evidence\n\nFAIL: tests failed\n');
329
+
330
+ assert.ok(parseBlock(runHook({}, dir).stdout), 're-completed failing task must be gated');
331
+ } finally {
332
+ fs.rmSync(dir, { recursive: true, force: true });
333
+ }
334
+ });
335
+
336
+ test('12. legacy successful receipt remains read-compatible', () => {
337
+ const dir = makeFixture({ evidence: 'legacy-valid' });
338
+ try {
339
+ seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
340
+ assert.strictEqual(runHook({}, dir).stdout, '');
341
+ } finally {
342
+ fs.rmSync(dir, { recursive: true, force: true });
343
+ }
344
+ });
@@ -0,0 +1,65 @@
1
+ 'use strict';
2
+
3
+ const { test } = require('node:test');
4
+ const assert = require('node:assert');
5
+ const { spawnSync } = require('node:child_process');
6
+ const fs = require('node:fs');
7
+ const os = require('node:os');
8
+ const path = require('node:path');
9
+
10
+ const HOOK = path.join(__dirname, '..', 'state.cjs');
11
+
12
+ test('Stop snapshot includes tracked modifications and untracked files', () => {
13
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ck-state-'));
14
+ try {
15
+ fs.mkdirSync(path.join(dir, '.claude'));
16
+ spawnSync('git', ['init', '-q'], { cwd: dir });
17
+ spawnSync('git', ['config', 'user.email', 'test@example.com'], { cwd: dir });
18
+ spawnSync('git', ['config', 'user.name', 'Test'], { cwd: dir });
19
+ fs.writeFileSync(path.join(dir, 'tracked.txt'), 'before\n');
20
+ spawnSync('git', ['add', 'tracked.txt'], { cwd: dir });
21
+ spawnSync('git', ['commit', '-qm', 'initial'], { cwd: dir });
22
+
23
+ fs.writeFileSync(path.join(dir, 'tracked.txt'), 'after\n');
24
+ fs.writeFileSync(path.join(dir, 'new-untracked.json'), '{}\n');
25
+ const result = spawnSync(process.execPath, [HOOK], {
26
+ cwd: dir,
27
+ input: JSON.stringify({ hook_event_name: 'Stop', cwd: dir }),
28
+ encoding: 'utf8',
29
+ });
30
+
31
+ assert.strictEqual(result.status, 0);
32
+ const state = fs.readFileSync(
33
+ path.join(dir, '.claude', 'session-state', 'latest.md'),
34
+ 'utf8',
35
+ );
36
+ assert.match(state, /- tracked\.txt/);
37
+ assert.match(state, /- new-untracked\.json/);
38
+ } finally {
39
+ fs.rmSync(dir, { recursive: true, force: true });
40
+ }
41
+ });
42
+
43
+ test('Stop snapshot includes untracked files before the first commit', () => {
44
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ck-state-no-head-'));
45
+ try {
46
+ fs.mkdirSync(path.join(dir, '.claude'));
47
+ spawnSync('git', ['init', '-q'], { cwd: dir });
48
+ fs.writeFileSync(path.join(dir, 'first-untracked.txt'), 'new\n');
49
+
50
+ const result = spawnSync(process.execPath, [HOOK], {
51
+ cwd: dir,
52
+ input: JSON.stringify({ hook_event_name: 'Stop', cwd: dir }),
53
+ encoding: 'utf8',
54
+ });
55
+
56
+ assert.strictEqual(result.status, 0);
57
+ const state = fs.readFileSync(
58
+ path.join(dir, '.claude', 'session-state', 'latest.md'),
59
+ 'utf8',
60
+ );
61
+ assert.match(state, /- first-untracked\.txt/);
62
+ } finally {
63
+ fs.rmSync(dir, { recursive: true, force: true });
64
+ }
65
+ });
@@ -39,24 +39,20 @@ try {
39
39
 
40
40
  const lines = [];
41
41
 
42
- // Trường hợp 1: Có Code nhưng chưa có thư mục docs
42
+ // Case 1: source present but docs/ missing
43
43
  if (!fs.existsSync(docsDir)) {
44
44
  lines.push('');
45
- lines.push('### 🔴 URGENT SYSTEM ACTION: MISSING DOCUMENTATION 🔴');
46
- lines.push(`> BẮT BUỘC (MANDATORY): Hệ thống phát hiện dự án đã có Source Code nhưng thư mục \`docs/\` chưa tồn tại.`);
47
- lines.push(`> Nhiệm vụ ĐẦU TIÊN của bạn trong session này là đọc hiểu mã nguồn hiện tại và tạo ra các tài liệu tiêu chuẩn:`);
48
- lines.push(`> 1. \`docs/system-architecture.md\` (Kiến trúc hệ thống, phân tích flow)`);
49
- lines.push(`> 2. \`docs/project-overview-pdr.md\` (Tổng quan, mục tiêu, tính năng)`);
50
- lines.push(`> 3. Khởi tạo file hidden \`docs/.sync_hash\` để theo dõi cập nhật.`);
51
- lines.push(`> `);
52
- lines.push(`> LƯU Ý BẮT BUỘC: Bạn phải chạy lệnh Git này trong Terminal để lấy Hash Code mới nhất: \`git log -1 --format="%H" -- . ":(exclude)docs"\``);
53
- lines.push(`> Sau đó ghi chuẩn giá trị Hash đó vào file \`docs/.sync_hash\`.`);
45
+ lines.push('### Missing docs/');
46
+ lines.push('> Source exists but `docs/` does not. Create baseline docs first:');
47
+ lines.push('> 1. `docs/system-architecture.md` — system architecture and flows');
48
+ lines.push('> 2. `docs/project-overview-pdr.md` — overview, goals, features');
49
+ lines.push('> 3. Write current hash to `docs/.sync_hash` via `git log -1 --format="%H" -- . ":(exclude)docs"`');
54
50
  lines.push('');
55
- }
56
- // Trường hợp 2: Có Code, đã có Docs. Kiểm tra Continuous Sync
51
+ }
52
+ // Case 2: docs exist — check continuous sync vs source-only git hash
57
53
  else {
58
54
  try {
59
- // Lấy hash của lần thay đổi SOURCE CODE gần nhất (Bỏ qua những commit chỉ sửa docs)
55
+ // Latest SOURCE-only hash (ignore docs-only commits)
60
56
  const currentHash = execSync('git log -1 --format="%H" -- . ":(exclude)docs"', {
61
57
  cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore']
62
58
  }).trim();
@@ -69,18 +65,16 @@ try {
69
65
 
70
66
  if (lastSyncHash !== currentHash) {
71
67
  lines.push('');
72
- lines.push('### 🔄 CONTINUOUS DOCS SYNC REQUIRED 🔄');
73
- lines.push(`> Source Code vừa có sự thay đổi mới (Git Hash: \`${currentHash}\`) kể từ lần đồng bộ Docs cuối cùng (\`${lastSyncHash || 'Chưa gán'}\`).`);
74
- lines.push(`> YÊU CẦU TRƯỚC KHI LÀM THÊM TÍNH NĂNG:`);
75
- lines.push(`> 1. Rà soát file bị thay đổi gần đây (dùng \`git diff ${lastSyncHash} ${currentHash}\` hoặc \`git log\`).`);
76
- lines.push(`> 2. Cập nhật lại \`docs/system-architecture.md\` hoặc các chuẩn code nếu cần thiết.`);
77
- lines.push(`> 3. Cập nhật Changelog (nhật ký thay đổi).`);
78
- lines.push(`> 4. KHI HOÀN TẤT, BẠN PHẢI GHI ĐÈ GIÁ TRỊ SAU: \`${currentHash}\` VÀO FILE \`docs/.sync_hash\` ĐỂ CHỐT TRẠNG THÁI HIỆN TẠI.`);
68
+ lines.push('### Docs sync needed');
69
+ lines.push(`> Source changed (\`${currentHash}\`) since last docs sync (\`${lastSyncHash || 'none'}\`).`);
70
+ lines.push(`> 1. Review recent changes (\`git diff ${lastSyncHash} ${currentHash}\` or \`git log\`).`);
71
+ lines.push('> 2. Update architecture/code-standards docs and changelog as needed.');
72
+ lines.push(`> 3. Write \`${currentHash}\` to \`docs/.sync_hash\` when done.`);
79
73
  lines.push('');
80
74
  }
81
75
  }
82
76
  } catch (e) {
83
- // Git chưa init hoặc chưa có commit nào thì im lặng fail-open
77
+ // Git not initialized or no commits — fail-open
84
78
  }
85
79
  }
86
80
 
@@ -21,7 +21,7 @@ try {
21
21
  // Directories that should never be read (too large / irrelevant to LLM)
22
22
  const BLOCKED_DIRS = [
23
23
  'node_modules', 'dist', 'build', '.next', '.nuxt', '.output',
24
- '__pycache__', '.venv', 'venv', '.env',
24
+ '__pycache__', '.venv', 'venv',
25
25
  'vendor', 'target',
26
26
  '.git', 'coverage', '.nyc_output',
27
27
  ];
@@ -52,11 +52,6 @@ const DEFAULT_CONFIG = {
52
52
  packageManager: 'auto',
53
53
  framework: 'auto'
54
54
  },
55
- skills: {
56
- research: {
57
- useGemini: true // Toggle Gemini CLI usage in research skill
58
- }
59
- },
60
55
  assertions: [],
61
56
  statusline: 'full',
62
57
  statuslineColors: true,
@@ -7,12 +7,11 @@
7
7
  * Claude Code CLI privacy gate for sensitive files.
8
8
  *
9
9
  * Runtime contract:
10
- * - Non-bash file access to sensitive files is blocked with a JSON marker
11
- * - Assistant must use AskUserQuestion with that JSON payload
12
- * - If user approves, assistant should read via `bash cat "file"`
13
- * - Bash access is allowed with a warning to enable the approved follow-up path
10
+ * - Sensitive access returns Claude Code's native PreToolUse "ask" decision
11
+ * - The same decision applies to direct tools and Bash commands
12
+ * - Symlinks are classified by their resolved target before their alias
14
13
  *
15
- * Exit: 0 = allow, 2 = block
14
+ * Exit: 0 (the JSON hook output carries the permission decision)
16
15
  */
17
16
 
18
17
  try {
@@ -20,7 +19,7 @@ try {
20
19
  const path = require('path');
21
20
 
22
21
  const RESTRICTED_PATTERNS = [
23
- /^\.env(\.|$)/i,
22
+ /^\.env(?:[.\[*?{]|$)/i,
24
23
  /^credentials/i,
25
24
  /secrets?\.(ya?ml|json)$/i,
26
25
  /\.pem$/i,
@@ -66,22 +65,28 @@ try {
66
65
  * when the path cannot be resolved (missing file, broken link) — fail-open to
67
66
  * the original-path check in that case.
68
67
  */
69
- function resolveTarget(filePath) {
68
+ function resolveTarget(filePath, cwd) {
70
69
  try {
71
- return fs.realpathSync(filePath);
70
+ const requested = path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath);
71
+ return fs.realpathSync(requested);
72
72
  } catch {
73
73
  return null;
74
74
  }
75
75
  }
76
76
 
77
77
  function extractBashPaths(command) {
78
- const paths = [];
79
- const regex = /(?:cat|less|more|head|tail|source|\.)\s+(?:"([^"]+)"|'([^']+)'|([^\s]+))/g;
80
- let match;
81
- while ((match = regex.exec(command)) !== null) {
82
- paths.push(match[1] || match[2] || match[3]);
78
+ const paths = new Set();
79
+ // Quotes are delimiters rather than grouped tokens so nested shell
80
+ // commands such as `bash -c 'cat .env'` cannot hide the sensitive path.
81
+ const tokens = command.split(/[\s|;&<>"']+/).filter(Boolean);
82
+ for (const token of tokens) {
83
+ const cleaned = token.replace(/^["'()`]+|["'(),]+$/g, '');
84
+ for (const candidate of cleaned.split('=')) {
85
+ const value = candidate.replace(/^["'()`]+|["'(),]+$/g, '');
86
+ if (value && (isSensitive(value) || isSafe(value))) paths.add(value);
87
+ }
83
88
  }
84
- return paths;
89
+ return [...paths];
85
90
  }
86
91
 
87
92
  function extractPaths(toolName, input) {
@@ -107,42 +112,15 @@ try {
107
112
  return paths.filter(Boolean);
108
113
  }
109
114
 
110
- function formatBlockMessage(filePath) {
115
+ function askForPermission(filePath) {
111
116
  const basename = path.basename(filePath);
112
- const promptData = {
113
- type: 'PRIVACY_PROMPT',
114
- file: filePath,
115
- basename,
116
- question: {
117
- header: 'File Access',
118
- text: `I need to read "${basename}" which may contain sensitive data (API keys, passwords, tokens). Do you approve?`,
119
- options: [
120
- {
121
- label: 'Yes, approve access',
122
- description: `Allow reading ${basename} this time`
123
- },
124
- {
125
- label: 'No, skip this file',
126
- description: 'Continue without accessing this file'
127
- }
128
- ]
117
+ process.stdout.write(JSON.stringify({
118
+ hookSpecificOutput: {
119
+ hookEventName: 'PreToolUse',
120
+ permissionDecision: 'ask',
121
+ permissionDecisionReason: `Sensitive file access requires approval: ${basename}`
129
122
  }
130
- };
131
-
132
- return [
133
- 'NOTE: This is not an error. This block protects sensitive data.',
134
- '',
135
- `PRIVACY BLOCK: Sensitive file access requires user approval`,
136
- `File: ${filePath}`,
137
- '',
138
- '@@PRIVACY_PROMPT_START@@',
139
- JSON.stringify(promptData, null, 2),
140
- '@@PRIVACY_PROMPT_END@@',
141
- '',
142
- 'Claude Code follow-up:',
143
- `- If approved: use bash to read: cat "${filePath}"`,
144
- '- If denied: continue without this file'
145
- ].join('\n');
123
+ }) + '\n');
146
124
  }
147
125
 
148
126
  const stdin = fs.readFileSync(0, 'utf8').trim();
@@ -160,22 +138,21 @@ try {
160
138
  if (!paths.length) process.exit(0);
161
139
 
162
140
  for (const filePath of paths) {
163
- // Check the requested path and, when it is a symlink, its real target.
164
- const target = resolveTarget(filePath);
165
- const candidates = target && target !== filePath ? [filePath, target] : [filePath];
166
-
167
- // An exemption on either name wins (e.g. a symlink to .env.example).
168
- if (candidates.some(isSafe)) continue;
169
- // Sensitive if the requested path OR its symlink target is sensitive.
170
- if (!candidates.some(isSensitive)) continue;
171
-
172
- if (toolName === 'Bash') {
173
- console.error(`WARN: Privacy-sensitive file access via bash allowed for approved follow-up: ${path.basename(filePath)}`);
174
- process.exit(0);
141
+ const target = resolveTarget(filePath, cwd);
142
+
143
+ // The real target is authoritative: an exempt-looking alias must not hide
144
+ // a sensitive target, while a real .env.example target remains safe.
145
+ if (target) {
146
+ if (isSensitive(target) && !isSafe(target)) {
147
+ askForPermission(filePath);
148
+ process.exit(0);
149
+ }
150
+ if (isSafe(target)) continue;
175
151
  }
176
152
 
177
- console.error(formatBlockMessage(filePath));
178
- process.exit(2);
153
+ if (isSafe(filePath) || !isSensitive(filePath)) continue;
154
+ askForPermission(filePath);
155
+ process.exit(0);
179
156
  }
180
157
 
181
158
  process.exit(0);