@haposoft/cafekit 0.8.12 → 0.8.14
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/README.md +9 -0
- package/package.json +1 -1
- package/src/claude/hooks/docs-sync.cjs +3 -1
- package/src/claude/hooks/lib/color.cjs +12 -5
- package/src/claude/hooks/lib/config.cjs +31 -23
- package/src/claude/hooks/lib/skill-router-routes.cjs +14 -8
- package/src/claude/hooks/skill-router.cjs +2 -2
- package/src/claude/hooks/usage.cjs +15 -8
- package/src/claude/migration-manifest.json +2 -0
- package/src/claude/scripts/validate-docs-reconstruct.cjs +176 -0
- package/src/claude/scripts/validate-docs.cjs +2 -1
- package/src/claude/skills/ai-multimodal/SKILL.md +1 -1
- package/src/claude/skills/docs/SKILL.md +262 -0
- package/src/claude/skills/docs/references/init-workflow.md +132 -0
- package/src/claude/skills/docs/references/reconstruct-workflow.md +407 -0
- package/src/claude/skills/docs/references/standard-docs-workflow.md +124 -0
- package/src/claude/skills/docs/references/summarize-workflow.md +89 -0
- package/src/claude/skills/docs/references/update-workflow.md +123 -0
- package/src/claude/skills/docs/templates/evidence-map.md +14 -0
- package/src/claude/skills/docs/templates/reconstruct-overview.html +205 -0
- package/src/claude/skills/docs/templates/reconstruction.json +40 -0
- package/src/claude/skills/docs/templates/requirements-as-is.md +34 -0
- package/src/claude/skills/docs/templates/unknowns-and-assumptions.md +19 -0
- package/src/claude/status.cjs +4 -2
package/README.md
CHANGED
|
@@ -80,6 +80,7 @@ CafeKit ships many skills, but the main release surface is:
|
|
|
80
80
|
- `/hapo:develop <feature-name>`: implement from approved spec artifacts
|
|
81
81
|
- `/hapo:debug <issue>`: diagnose bugs, incidents, CI failures, flaky tests, UI regressions, and performance issues before fixing
|
|
82
82
|
- `/hapo:hotfix <issue>`: fix diagnosed bugs with root-cause, verification, prevention, and side-effect gates
|
|
83
|
+
- `/hapo:docs [init|update|summarize|reconstruct]`: create project docs or reconstruct as-is system documentation from source code
|
|
83
84
|
- `/hapo:test [scope|--full]`: run verification and return a structured verdict
|
|
84
85
|
- `/hapo:code-review [scope|--pending]`: adversarial review focused on correctness, regressions, and security
|
|
85
86
|
- `/hapo:generate-graph <diagram request>`: generate technical SVG/PNG diagrams
|
|
@@ -125,6 +126,14 @@ Generate a diagram:
|
|
|
125
126
|
/hapo:generate-graph Draw a sequence diagram for auth flow between browser, API, and database
|
|
126
127
|
```
|
|
127
128
|
|
|
129
|
+
Reconstruct current-state docs for an existing or legacy system:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
/hapo:docs reconstruct apps/legacy-admin
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
The reconstruct bundle includes as-is markdown/JSON evidence and a self-contained `overview.html` review dashboard before the approved docs are handed to `/hapo:specs`.
|
|
136
|
+
|
|
128
137
|
## Spec Artifacts
|
|
129
138
|
|
|
130
139
|
CafeKit's current spec workflow writes artifacts under:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@haposoft/cafekit",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.14",
|
|
4
4
|
"description": "Claude Code-first spec-driven workflow for AI coding assistants. Bundles CafeKit hapo: skills, runtime hooks, agents, and installer scaffolding.",
|
|
5
5
|
"author": "Haposoft <nghialt@haposoft.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -15,13 +15,15 @@ try {
|
|
|
15
15
|
const fs = require('fs');
|
|
16
16
|
const path = require('path');
|
|
17
17
|
const { execSync } = require('child_process');
|
|
18
|
+
const { loadConfig } = require('./lib/config.cjs');
|
|
18
19
|
|
|
19
20
|
// Đọc stdin theo chuẩn hook
|
|
20
21
|
const stdin = fs.readFileSync(0, 'utf8').trim();
|
|
21
22
|
const payload = stdin ? JSON.parse(stdin) : {};
|
|
22
23
|
const cwd = payload.cwd || process.cwd();
|
|
24
|
+
const config = loadConfig({ cwd, includeProject: false, includeAssertions: false, includeLocale: false });
|
|
23
25
|
|
|
24
|
-
const docsDir = path.join(cwd, 'docs');
|
|
26
|
+
const docsDir = path.join(cwd, config.paths?.docs || 'docs');
|
|
25
27
|
|
|
26
28
|
// Xác định dự án đã có cốt lõi code hay chưa?
|
|
27
29
|
const hasCode = fs.existsSync(path.join(cwd, 'src')) ||
|
|
@@ -16,14 +16,20 @@ const YELLOW = '\x1b[33m';
|
|
|
16
16
|
const MAGENTA = '\x1b[35m';
|
|
17
17
|
const CYAN = '\x1b[36m';
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
// Claude Code statusline runs via pipe but output displays in TTY - default to true
|
|
21
|
-
const shouldUseColor = (() => {
|
|
19
|
+
function detectColorDefault() {
|
|
22
20
|
if (process.env.NO_COLOR) return false;
|
|
23
21
|
if (process.env.FORCE_COLOR) return true;
|
|
24
22
|
// Default true for statusline context (Claude Code handles TTY display)
|
|
25
23
|
return true;
|
|
26
|
-
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Detect color support at module load (cached)
|
|
27
|
+
// Claude Code statusline runs via pipe but output displays in TTY - default to true
|
|
28
|
+
let shouldUseColor = detectColorDefault();
|
|
29
|
+
|
|
30
|
+
function setColorEnabled(enabled) {
|
|
31
|
+
shouldUseColor = enabled === false ? false : detectColorDefault();
|
|
32
|
+
}
|
|
27
33
|
|
|
28
34
|
// Detect 256-color support via COLORTERM
|
|
29
35
|
const has256Color = (() => {
|
|
@@ -90,6 +96,7 @@ module.exports = {
|
|
|
90
96
|
dim,
|
|
91
97
|
getContextColor,
|
|
92
98
|
coloredBar,
|
|
93
|
-
shouldUseColor,
|
|
99
|
+
get shouldUseColor() { return shouldUseColor; },
|
|
100
|
+
setColorEnabled,
|
|
94
101
|
has256Color
|
|
95
102
|
};
|
|
@@ -9,11 +9,9 @@ const fs = require('fs');
|
|
|
9
9
|
const path = require('path');
|
|
10
10
|
const os = require('os');
|
|
11
11
|
|
|
12
|
-
const
|
|
13
|
-
const GLOBAL_CONFIG_PATH = path.join(os.homedir(), '.claude', '.ck.json');
|
|
12
|
+
const RUNTIME_CONFIG_PATH = '.claude/runtime.json';
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
const CONFIG_PATH = LOCAL_CONFIG_PATH;
|
|
14
|
+
const CONFIG_PATH = RUNTIME_CONFIG_PATH;
|
|
17
15
|
|
|
18
16
|
const DEFAULT_CONFIG = {
|
|
19
17
|
plan: {
|
|
@@ -61,6 +59,7 @@ const DEFAULT_CONFIG = {
|
|
|
61
59
|
},
|
|
62
60
|
assertions: [],
|
|
63
61
|
statusline: 'full',
|
|
62
|
+
statuslineColors: true,
|
|
64
63
|
hooks: {
|
|
65
64
|
'session-init': true,
|
|
66
65
|
'subagent-init': true,
|
|
@@ -81,8 +80,8 @@ const DEFAULT_CONFIG = {
|
|
|
81
80
|
* Arrays are replaced entirely (not concatenated) to avoid duplicate entries
|
|
82
81
|
*
|
|
83
82
|
* IMPORTANT: Empty objects {} are treated as "inherit from parent", not "replace with empty".
|
|
84
|
-
* This allows
|
|
85
|
-
*
|
|
83
|
+
* This allows runtime config to leave hooks as {} (inherit defaults)
|
|
84
|
+
* without resetting all default hook toggles.
|
|
86
85
|
*
|
|
87
86
|
* @param {Object} target - Base object
|
|
88
87
|
* @param {Object} source - Object to merge (takes precedence)
|
|
@@ -464,36 +463,39 @@ function sanitizeConfig(config, projectRoot) {
|
|
|
464
463
|
}
|
|
465
464
|
|
|
466
465
|
/**
|
|
467
|
-
* Load config with cascading resolution: DEFAULT →
|
|
466
|
+
* Load config with cascading resolution: DEFAULT → runtime
|
|
468
467
|
*
|
|
469
468
|
* Resolution order (each layer overrides the previous):
|
|
470
469
|
* 1. DEFAULT_CONFIG (hardcoded defaults)
|
|
471
|
-
* 2.
|
|
472
|
-
* 3. Local config (./.claude/.ck.json) - project-specific overrides
|
|
470
|
+
* 2. Runtime config (./.claude/runtime.json) - installed CafeKit runtime config
|
|
473
471
|
*
|
|
474
472
|
* @param {Object} options - Options for config loading
|
|
475
473
|
* @param {boolean} options.includeProject - Include project section (default: true)
|
|
476
474
|
* @param {boolean} options.includeAssertions - Include assertions (default: true)
|
|
477
475
|
* @param {boolean} options.includeLocale - Include locale section (default: true)
|
|
476
|
+
* @param {string} options.cwd - Project root for local config lookup (default: process.cwd())
|
|
478
477
|
*/
|
|
479
478
|
function loadConfig(options = {}) {
|
|
480
|
-
const {
|
|
481
|
-
|
|
479
|
+
const {
|
|
480
|
+
includeProject = true,
|
|
481
|
+
includeAssertions = true,
|
|
482
|
+
includeLocale = true,
|
|
483
|
+
cwd = process.cwd()
|
|
484
|
+
} = options;
|
|
485
|
+
const projectRoot = cwd;
|
|
482
486
|
|
|
483
|
-
// Load
|
|
484
|
-
const
|
|
485
|
-
const localConfig = loadConfigFromPath(LOCAL_CONFIG_PATH);
|
|
487
|
+
// Load config from the installed CafeKit runtime config.
|
|
488
|
+
const runtimeConfig = loadConfigFromPath(path.join(projectRoot, RUNTIME_CONFIG_PATH));
|
|
486
489
|
|
|
487
490
|
// No config files found - use defaults
|
|
488
|
-
if (!
|
|
491
|
+
if (!runtimeConfig) {
|
|
489
492
|
return getDefaultConfig(includeProject, includeAssertions, includeLocale);
|
|
490
493
|
}
|
|
491
494
|
|
|
492
495
|
try {
|
|
493
|
-
// Deep merge: DEFAULT →
|
|
496
|
+
// Deep merge: DEFAULT → runtime (runtime wins)
|
|
494
497
|
let merged = deepMerge({}, DEFAULT_CONFIG);
|
|
495
|
-
if (
|
|
496
|
-
if (localConfig) merged = deepMerge(merged, localConfig);
|
|
498
|
+
if (runtimeConfig) merged = deepMerge(merged, runtimeConfig);
|
|
497
499
|
|
|
498
500
|
// Build result with optional sections
|
|
499
501
|
const result = {
|
|
@@ -523,6 +525,7 @@ function loadConfig(options = {}) {
|
|
|
523
525
|
result.hooks = merged.hooks || DEFAULT_CONFIG.hooks;
|
|
524
526
|
// Statusline mode
|
|
525
527
|
result.statusline = merged.statusline || 'full';
|
|
528
|
+
result.statuslineColors = merged.statuslineColors !== false;
|
|
526
529
|
|
|
527
530
|
return sanitizeConfig(result, projectRoot);
|
|
528
531
|
} catch (e) {
|
|
@@ -541,7 +544,8 @@ function getDefaultConfig(includeProject = true, includeAssertions = true, inclu
|
|
|
541
544
|
codingLevel: -1, // Default: disabled (no injection, saves tokens)
|
|
542
545
|
skills: { ...DEFAULT_CONFIG.skills },
|
|
543
546
|
hooks: { ...DEFAULT_CONFIG.hooks },
|
|
544
|
-
statusline: 'full'
|
|
547
|
+
statusline: 'full',
|
|
548
|
+
statuslineColors: true
|
|
545
549
|
};
|
|
546
550
|
if (includeLocale) {
|
|
547
551
|
result.locale = { ...DEFAULT_CONFIG.locale };
|
|
@@ -790,8 +794,13 @@ function extractTaskListId(resolved) {
|
|
|
790
794
|
* @param {string} hookName - Hook name (script basename without .cjs)
|
|
791
795
|
* @returns {boolean} Whether hook is enabled
|
|
792
796
|
*/
|
|
793
|
-
function isHookEnabled(hookName) {
|
|
794
|
-
const config = loadConfig({
|
|
797
|
+
function isHookEnabled(hookName, options = {}) {
|
|
798
|
+
const config = loadConfig({
|
|
799
|
+
includeProject: false,
|
|
800
|
+
includeAssertions: false,
|
|
801
|
+
includeLocale: false,
|
|
802
|
+
cwd: options.cwd
|
|
803
|
+
});
|
|
795
804
|
const hooks = config.hooks || {};
|
|
796
805
|
// Return true if undefined (default enabled), otherwise return the boolean value
|
|
797
806
|
return hooks[hookName] !== false;
|
|
@@ -799,8 +808,7 @@ function isHookEnabled(hookName) {
|
|
|
799
808
|
|
|
800
809
|
module.exports = {
|
|
801
810
|
CONFIG_PATH,
|
|
802
|
-
|
|
803
|
-
GLOBAL_CONFIG_PATH,
|
|
811
|
+
RUNTIME_CONFIG_PATH,
|
|
804
812
|
DEFAULT_CONFIG,
|
|
805
813
|
INVALID_FILENAME_CHARS,
|
|
806
814
|
deepMerge,
|
|
@@ -18,7 +18,7 @@ const ROUTES = [
|
|
|
18
18
|
strong: ['spec', 'specs', 'requirements', 'acceptance criteria', 'task breakdown', 'đặc tả', 'dac ta', '仕様', '仕様書', '要件', '受け入れ条件', 'タスク分解', '仕様を作って', '仕様を作成'],
|
|
19
19
|
medium: ['requirement', 'ears', 'design doc', 'scope', '--validate', 'yêu cầu', 'yeu cau', 'phạm vi', 'pham vi', 'validate spec', 'kiểm tra spec', 'kiem tra spec', '要求', '設計書', 'スコープ', '検証', '仕様を確認'],
|
|
20
20
|
weak: ['tính năng mới', 'tinh nang moi', 'feature idea', 'user story', 'criteria', 'task list', '新機能', 'ユーザーストーリー', '基準', 'タスクリスト'],
|
|
21
|
-
negative: ['commit', 'push', 'bug', 'error', 'production', 'pptx', 'pdf'],
|
|
21
|
+
negative: ['commit', 'push', 'bug', 'error', 'production', 'pptx', 'pdf', 'reconstruct requirements', 'as-is requirements', 'legacy system documentation', 'documentation from source code', 'docs from source code', 'ソースコードから'],
|
|
22
22
|
}),
|
|
23
23
|
route('hapo:develop', 'implementation from an approved spec or task list', 75, {
|
|
24
24
|
strong: ['develop', 'implement', 'implementation', 'theo spec', 'theo specs', 'approved spec', 'làm theo spec', 'lam theo spec', '実装', '開発', '仕様に沿って', '仕様どおり', '承認済み仕様'],
|
|
@@ -27,8 +27,8 @@ const ROUTES = [
|
|
|
27
27
|
negative: ['bug', 'debug', 'review', 'test only', 'commit'],
|
|
28
28
|
}),
|
|
29
29
|
route('hapo:test', 'test, verification, QA, or runtime validation', 70, {
|
|
30
|
-
strong: ['unit test', 'integration test', 'e2e', 'playwright', 'coverage', 'kiểm thử', 'kiem thu', '単体テスト', '結合テスト', 'カバレッジ', 'テストして'],
|
|
31
|
-
medium: ['test', 'tests', 'testing', 'qa', 'verify', 'verification', 'kiểm tra chạy', 'kiem tra chay', 'xác minh', 'xac minh', 'テスト', '検証', '確認', '動作確認'],
|
|
30
|
+
strong: ['unit test', 'integration test', 'e2e', 'playwright', 'coverage', 'test all', 'run all tests', 'test toàn bộ', 'test toan bo', 'kiểm thử', 'kiem thu', 'kiểm tra toàn bộ', 'kiem tra toan bo', '単体テスト', '結合テスト', '全体テスト', 'カバレッジ', 'テストして'],
|
|
31
|
+
medium: ['test', 'tests', 'testing', 'qa', 'verify', 'verification', 'kiểm tra chạy', 'kiem tra chay', 'xác minh', 'xac minh', 'テスト', '検証', '確認', '動作確認', '全体確認'],
|
|
32
32
|
weak: ['assert', 'runtime proof', 'manual qa', 'end to end', 'smoke test', 'アサート', 'スモークテスト'],
|
|
33
33
|
negative: ['spec', 'requirements', 'commit', 'push'],
|
|
34
34
|
}),
|
|
@@ -45,8 +45,8 @@ const ROUTES = [
|
|
|
45
45
|
negative: ['deploy', 'test', 'review only'],
|
|
46
46
|
}),
|
|
47
47
|
route('hapo:inspect', 'codebase discovery, file search, structure scan, or locating implementation areas', 64, {
|
|
48
|
-
strong: ['inspect', 'codebase scan', 'scan codebase', 'scan source', 'file discovery', 'find files', 'locate files', 'xem source', 'xem codebase', 'quét source', 'quet source', 'quét codebase', 'quet codebase', 'kiểm tra cấu trúc', 'kiem tra cau truc', 'コード構造', 'ソース確認', 'コードベース確認', 'ファイル探索', '構造を確認'],
|
|
49
|
-
medium: ['search files', 'find where', 'where is', 'project structure', 'code structure', 'repo structure', 'tìm file', 'tim file', 'tìm trong source', 'tim trong source', 'cấu trúc project', 'cau truc project', 'ở đâu', 'o dau', '関連ファイル', 'どこにある', 'プロジェクト構造', 'リポジトリ構造', '探して'],
|
|
48
|
+
strong: ['inspect', 'codebase scan', 'scan codebase', 'scan source', 'file discovery', 'find files', 'locate files', 'xem source', 'xem codebase', 'kiểm tra source', 'kiem tra source', 'kiểm tra source code', 'kiem tra source code', 'quét source', 'quet source', 'quét codebase', 'quet codebase', 'kiểm tra cấu trúc', 'kiem tra cau truc', 'コード構造', 'ソース確認', 'コードベース確認', 'ファイル探索', '構造を確認'],
|
|
49
|
+
medium: ['search files', 'find where', 'where is', 'project structure', 'code structure', 'repo structure', 'source code', 'tìm file', 'tim file', 'tìm trong source', 'tim trong source', 'cấu trúc project', 'cau truc project', 'ở đâu', 'o dau', 'nằm đâu', 'nam dau', '関連ファイル', 'どこにある', 'プロジェクト構造', 'リポジトリ構造', '探して'],
|
|
50
50
|
weak: ['scan', 'inspect code', 'explore code', 'xem qua', 'xem giúp', '調べて', '確認して'],
|
|
51
51
|
negative: ['bug', 'error', 'lỗi', 'loi', 'fail', 'failure', 'production', 'hotfix', 'fix', 'sửa', 'sua', 'debug', 'develop', 'implement', 'test', 'commit', 'push', 'slide', 'pptx'],
|
|
52
52
|
}),
|
|
@@ -63,8 +63,8 @@ const ROUTES = [
|
|
|
63
63
|
negative: ['backend', 'api', 'database'],
|
|
64
64
|
}),
|
|
65
65
|
route('hapo:react-best-practices', 'React and Next.js performance patterns, rerender optimization, and Vercel best practices', 60, {
|
|
66
|
-
strong: ['react best practices', 'next.js best practices', 'vercel react best practices', 'react performance', 'next.js performance', 'optimize react', 'optimize next.js', 'tối ưu react', 'toi uu react', 'tối ưu next.js', 'toi uu next.js', 'reactベストプラクティス', 'next.jsベストプラクティス', 'react最適化', 'next.js最適化', 'react性能'],
|
|
67
|
-
medium: ['bundle optimization', 'bundle size', 'rerender optimization', 're-render optimization', 'data fetching', 'server component', 'client component', 'suspense', 'hydration', 'waterfall', 'usememo', 'usecallback', 'react cache', 'tối ưu rerender', 'toi uu rerender', 'tối ưu bundle', 'toi uu bundle', 'バンドル最適化', '再レンダー最適化', 'データ取得', 'サーバーコンポーネント', 'クライアントコンポーネント', 'ハイドレーション'],
|
|
66
|
+
strong: ['react best practices', 'next.js best practices', 'vercel react best practices', 'react performance', 'next.js performance', 'optimize react', 'optimize next.js', 'rerender nhiều', 'rerender nhieu', 'nhiều rerender', 'nhieu rerender', 'tối ưu react', 'toi uu react', 'tối ưu next.js', 'toi uu next.js', 'reactベストプラクティス', 'next.jsベストプラクティス', 'react最適化', 'next.js最適化', 'react性能'],
|
|
67
|
+
medium: ['bundle optimization', 'bundle size', 'rerender optimization', 're-render optimization', 'data fetching', 'server component', 'client component', 'suspense', 'hydration', 'waterfall', 'usememo', 'usecallback', 'react cache', 'optimize', 'optimization', 'tối ưu', 'toi uu', 'tối ưu rerender', 'toi uu rerender', 'tối ưu bundle', 'toi uu bundle', 'バンドル最適化', '再レンダー最適化', '最適化', 'データ取得', 'サーバーコンポーネント', 'クライアントコンポーネント', 'ハイドレーション'],
|
|
68
68
|
weak: ['react', 'next.js', 'memo', 'rerender', 're-render', 'render performance', 'component performance', 'waterfalls', 'lazy state', 'dynamic import', 'react pattern', 'next.js pattern', 'レンダー性能', 'コンポーネント性能', '動的インポート'],
|
|
69
69
|
negative: ['backend', 'api', 'database', 'slide', 'pptx', 'commit', 'push'],
|
|
70
70
|
}),
|
|
@@ -98,6 +98,12 @@ const ROUTES = [
|
|
|
98
98
|
weak: ['mind map', 'dependency map', 'system map', 'マインドマップ', '依存関係図', 'システム図'],
|
|
99
99
|
negative: ['pptx', 'slide deck'],
|
|
100
100
|
}),
|
|
101
|
+
route('hapo:docs', 'project documentation, codebase docs, or source-backed as-is reconstruction', 51, {
|
|
102
|
+
strong: ['hapo:docs', 'project docs', 'system docs', 'codebase docs', 'codebase documentation', 'documentation from source code', 'docs from source code', 'generate docs from codebase', 'dựng tài liệu từ source code', 'dung tai lieu tu source code', 'tài liệu hệ thống', 'tai lieu he thong', 'tài liệu hiện trạng', 'tai lieu hien trang', 'tạo tài liệu project', 'tao tai lieu project', 'legacy documentation', 'legacy system documentation', 'as-is documentation', 'as-is requirements', 'requirement reconstruction', 'reconstruct requirements', 'reconstruct requirements from legacy system', 'ソースコードから仕様書', 'ソースコードから仕様書を作成', 'ソースコードからドキュメント', '既存システムのドキュメント', '現行仕様書', 'as-isドキュメント'],
|
|
103
|
+
medium: ['create docs', 'update docs', 'refresh docs', 'summarize codebase', 'system documentation', 'source to docs', 'reverse documentation', 'current-state docs', 'existing system docs', 'legacy docs', 'legacy system', 'dựng docs', 'dung docs', 'tạo docs', 'tao docs', 'cập nhật docs', 'cap nhat docs', 'tóm tắt codebase', 'tom tat codebase', 'dựng lại tài liệu', 'dung lai tai lieu', '仕様書を作成', 'ドキュメント作成', 'ドキュメント更新', 'コードベース要約', '既存システム', 'レガシーシステム'],
|
|
104
|
+
weak: ['docs', 'documentation', 'document project', 'document codebase', 'as-is', 'reconstruct', 'hiện trạng', 'hien trang', 'tài liệu', 'tai lieu', 'ドキュメント', '資料化', '仕様化', '現状'],
|
|
105
|
+
negative: ['official docs', 'latest docs', 'library docs', 'framework docs', 'api docs lookup', 'context7', 'best practice', 'research', 'pptx', 'slide'],
|
|
106
|
+
}),
|
|
101
107
|
route('hapo:brainstorm', 'early ideation or unclear solution direction', 50, {
|
|
102
108
|
strong: ['brainstorm', 'ý tưởng', 'y tuong', 'phương án', 'phuong an', 'gợi ý', 'goi y', 'ブレスト', 'アイデア', '案', '提案して', '相談'],
|
|
103
109
|
medium: ['idea', 'ideas', 'approach', 'options', 'tradeoff', 'chủ đề', 'chu de', 'cần làm gì', 'can lam gi', 'アプローチ', '選択肢', 'トレードオフ', 'テーマ', '何をすれば'],
|
|
@@ -118,7 +124,7 @@ const ROUTES = [
|
|
|
118
124
|
}),
|
|
119
125
|
route('hapo:agent-browser', 'browser automation with snapshot refs, web interaction, recording, or Browserbase cloud browser workflows', 45, {
|
|
120
126
|
strong: ['agent-browser', 'browser automation', 'web automation', 'browserbase', 'cloud browser', 'snapshot refs', 'browser snapshot', 'automate browser', 'tự động trình duyệt', 'tu dong trinh duyet', 'tự động thao tác trình duyệt', 'tu dong thao tac trinh duyet', 'ブラウザ自動化', 'クラウドブラウザ', 'ブラウザ操作', 'ブラウザスナップショット'],
|
|
121
|
-
medium: ['open url', 'navigate site', 'click in browser', 'fill form in browser', 'record browser', 'browser session', 'multi tab', 'browser test session', 'mở website', 'mo website', 'truy cập website', 'truy cap website', 'click trên web', 'click tren web', 'điền form web', 'dien form web', 'サイトを開く', 'ブラウザで開く', 'フォーム入力', 'クリック操作', '録画'],
|
|
127
|
+
medium: ['open url', 'navigate site', 'click in browser', 'fill form in browser', 'record browser', 'browser session', 'multi tab', 'browser test session', 'mở website', 'mo website', 'truy cập website', 'truy cap website', 'click trên web', 'click tren web', 'điền form web', 'dien form web', 'サイトを開く', 'ブラウザで開く', 'ブラウザで', 'フォーム入力', 'フォーム入力を自動化', '自動化して', 'クリック操作', '録画'],
|
|
122
128
|
weak: ['click button', 'fill form', 'open site', 'web session', 'browser ref', 'viewport', 'cookies', 'localstorage', 'ボタンをクリック', 'ビューポート', 'クッキー'],
|
|
123
129
|
negative: ['attached screenshot', 'ảnh đính kèm', 'anh dinh kem', '画像添付', 'source code', 'codebase', 'commit', 'push', 'pptx', 'pdf'],
|
|
124
130
|
}),
|
|
@@ -38,13 +38,13 @@ try {
|
|
|
38
38
|
return trimmed.startsWith('/') || /^hapo:[a-z-]+/i.test(trimmed);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
if (!isHookEnabled('skill-router')) process.exit(0);
|
|
42
|
-
|
|
43
41
|
const stdin = fs.readFileSync(0, 'utf8').trim();
|
|
44
42
|
if (!stdin) process.exit(0);
|
|
45
43
|
|
|
46
44
|
const payload = JSON.parse(stdin);
|
|
45
|
+
const cwd = payload.cwd || process.cwd();
|
|
47
46
|
const prompt = payload.prompt || '';
|
|
47
|
+
if (!isHookEnabled('skill-router', { cwd })) process.exit(0);
|
|
48
48
|
if (!prompt || isExplicitCommand(prompt)) process.exit(0);
|
|
49
49
|
|
|
50
50
|
const route = findRoute(prompt);
|
|
@@ -21,14 +21,15 @@ try {
|
|
|
21
21
|
const os = require("os");
|
|
22
22
|
const { execSync } = require("child_process");
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
24
|
+
function readRuntime(cwd) {
|
|
25
|
+
try {
|
|
26
|
+
const runtimePath = path.join(cwd, '.claude', 'runtime.json');
|
|
27
|
+
if (fs.existsSync(runtimePath)) {
|
|
28
|
+
return JSON.parse(fs.readFileSync(runtimePath, 'utf-8'));
|
|
29
|
+
}
|
|
30
|
+
} catch { /* fail-open */ }
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
32
33
|
|
|
33
34
|
// Cache configuration
|
|
34
35
|
const USAGE_CACHE_FILE = path.join(os.tmpdir(), "ck-usage-limits-cache.json");
|
|
@@ -151,6 +152,12 @@ async function main() {
|
|
|
151
152
|
} catch {}
|
|
152
153
|
|
|
153
154
|
const input = JSON.parse(inputStr || "{}");
|
|
155
|
+
const cwd = input.cwd || process.cwd();
|
|
156
|
+
const runtime = readRuntime(cwd);
|
|
157
|
+
if (runtime.usage?.enabled === false) {
|
|
158
|
+
console.log(JSON.stringify(result));
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
154
161
|
|
|
155
162
|
// Detect hook type
|
|
156
163
|
const isUserPrompt = typeof input.prompt === "string";
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"debug",
|
|
16
16
|
"develop",
|
|
17
17
|
"devops",
|
|
18
|
+
"docs",
|
|
18
19
|
"docx",
|
|
19
20
|
"frontend-design",
|
|
20
21
|
"frontend-development",
|
|
@@ -56,6 +57,7 @@
|
|
|
56
57
|
"scripts": {
|
|
57
58
|
"required": [
|
|
58
59
|
"validate-docs.cjs",
|
|
60
|
+
"validate-docs-reconstruct.cjs",
|
|
59
61
|
"browser-tool.cjs",
|
|
60
62
|
"validate-spec-output.cjs"
|
|
61
63
|
]
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CafeKit as-is reconstruction bundle validator.
|
|
4
|
+
*
|
|
5
|
+
* The docs workflow is LLM-authored, so the bundle shape and evidence links
|
|
6
|
+
* must be checked deterministically before it is handed to human review.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
const DOCUMENT_FILES = [
|
|
13
|
+
'overview.html',
|
|
14
|
+
'system-overview.md',
|
|
15
|
+
'requirements-as-is.md',
|
|
16
|
+
'roles-and-permissions.md',
|
|
17
|
+
'entities-and-statuses.md',
|
|
18
|
+
'business-rules.md',
|
|
19
|
+
'integrations.md',
|
|
20
|
+
'architecture-c4.md',
|
|
21
|
+
'constraints-risks-and-decisions.md',
|
|
22
|
+
'glossary.md',
|
|
23
|
+
'evidence-map.md',
|
|
24
|
+
'unknowns-and-assumptions.md',
|
|
25
|
+
];
|
|
26
|
+
const REQUIRED_FILES = ['reconstruction.json', ...DOCUMENT_FILES];
|
|
27
|
+
const EVIDENCE_ID_RE = /\bE-[A-Z]+-\d{3}\b/g;
|
|
28
|
+
|
|
29
|
+
function usage() {
|
|
30
|
+
console.error('Usage: node .claude/scripts/validate-docs-reconstruct.cjs docs/as-is/<scope>');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function readJson(filePath, errors) {
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
36
|
+
} catch (error) {
|
|
37
|
+
errors.push(`reconstruction.json: invalid JSON (${error.message})`);
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function readText(filePath) {
|
|
43
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function uniqueMatches(text, pattern) {
|
|
47
|
+
return [...new Set(text.match(pattern) || [])].sort();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function validateMeta(bundle, meta, errors) {
|
|
51
|
+
for (const key of [
|
|
52
|
+
'scope',
|
|
53
|
+
'generated_at',
|
|
54
|
+
'status',
|
|
55
|
+
'docs_root',
|
|
56
|
+
'source_revision',
|
|
57
|
+
'source_branch',
|
|
58
|
+
'evidence_policy',
|
|
59
|
+
'review_gate',
|
|
60
|
+
'review_status',
|
|
61
|
+
'approved_for_specs',
|
|
62
|
+
'documents',
|
|
63
|
+
'counts',
|
|
64
|
+
'next_recommended_step',
|
|
65
|
+
]) {
|
|
66
|
+
if (!(key in meta)) errors.push(`reconstruction.json.${key}: missing`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
for (const key of ['scope', 'generated_at', 'docs_root', 'source_revision', 'source_branch']) {
|
|
70
|
+
if (key in meta && (typeof meta[key] !== 'string' || meta[key].trim() === '')) {
|
|
71
|
+
errors.push(`reconstruction.json.${key}: must be a non-empty string`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (meta.approved_for_specs !== false && meta.review_status !== 'approved') {
|
|
75
|
+
errors.push('reconstruction.json.approved_for_specs: only approved review may set true');
|
|
76
|
+
}
|
|
77
|
+
if (!Array.isArray(meta.documents)) {
|
|
78
|
+
errors.push('reconstruction.json.documents: must be an array');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const declared = [...meta.documents].sort();
|
|
83
|
+
const expected = [...DOCUMENT_FILES].sort();
|
|
84
|
+
if (JSON.stringify(declared) !== JSON.stringify(expected)) {
|
|
85
|
+
errors.push('reconstruction.json.documents: must exactly list the as-is document bundle');
|
|
86
|
+
}
|
|
87
|
+
if (typeof meta.docs_root === 'string' && !meta.docs_root.includes(path.basename(bundle))) {
|
|
88
|
+
errors.push('reconstruction.json.docs_root: must point at this scope bundle');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function requirementBlocks(text) {
|
|
93
|
+
const matches = [...text.matchAll(/^##\s+(R-ASIS-\d{3})\b[^\n]*$/gm)];
|
|
94
|
+
return matches.map((match, index) => ({
|
|
95
|
+
id: match[1],
|
|
96
|
+
text: text.slice(match.index, matches[index + 1]?.index ?? text.length),
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function validateRequirements(bundle, evidenceText, errors) {
|
|
101
|
+
const requirementPath = path.join(bundle, 'requirements-as-is.md');
|
|
102
|
+
const blocks = requirementBlocks(readText(requirementPath));
|
|
103
|
+
if (blocks.length === 0) {
|
|
104
|
+
errors.push('requirements-as-is.md: must contain at least one ## R-ASIS-### requirement');
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const ledgerIds = new Set(uniqueMatches(evidenceText, EVIDENCE_ID_RE));
|
|
109
|
+
for (const block of blocks) {
|
|
110
|
+
if (!/- Type:\s*(Observed|Inferred|Unknown)\b/.test(block.text)) {
|
|
111
|
+
errors.push(`requirements-as-is.md:${block.id}: missing Type`);
|
|
112
|
+
}
|
|
113
|
+
if (!/- Confidence:\s*(High|Medium|Low)\b/.test(block.text)) {
|
|
114
|
+
errors.push(`requirements-as-is.md:${block.id}: missing Confidence`);
|
|
115
|
+
}
|
|
116
|
+
if (!/^- Evidence:\s*$/m.test(block.text)) {
|
|
117
|
+
errors.push(`requirements-as-is.md:${block.id}: missing Evidence section`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const evidenceIds = uniqueMatches(block.text, EVIDENCE_ID_RE);
|
|
121
|
+
if (evidenceIds.length === 0) {
|
|
122
|
+
errors.push(`requirements-as-is.md:${block.id}: must reference evidence IDs`);
|
|
123
|
+
}
|
|
124
|
+
for (const evidenceId of evidenceIds) {
|
|
125
|
+
if (!ledgerIds.has(evidenceId)) {
|
|
126
|
+
errors.push(`requirements-as-is.md:${block.id}: unknown evidence ID ${evidenceId}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function validateBundle(bundle) {
|
|
133
|
+
const errors = [];
|
|
134
|
+
if (!fs.existsSync(bundle)) return { errors: [`${bundle}: bundle directory does not exist`] };
|
|
135
|
+
|
|
136
|
+
for (const file of REQUIRED_FILES) {
|
|
137
|
+
if (!fs.existsSync(path.join(bundle, file))) errors.push(`${file}: missing`);
|
|
138
|
+
}
|
|
139
|
+
if (errors.length > 0) return { errors };
|
|
140
|
+
|
|
141
|
+
const meta = readJson(path.join(bundle, 'reconstruction.json'), errors);
|
|
142
|
+
if (meta) validateMeta(bundle, meta, errors);
|
|
143
|
+
|
|
144
|
+
const evidence = readText(path.join(bundle, 'evidence-map.md'));
|
|
145
|
+
if (uniqueMatches(evidence, EVIDENCE_ID_RE).length === 0) {
|
|
146
|
+
errors.push('evidence-map.md: must define evidence IDs');
|
|
147
|
+
}
|
|
148
|
+
validateRequirements(bundle, evidence, errors);
|
|
149
|
+
|
|
150
|
+
const overview = readText(path.join(bundle, 'overview.html'));
|
|
151
|
+
if (!overview.includes('data-reconstruct-overview')) {
|
|
152
|
+
errors.push('overview.html: must keep reconstruct overview marker');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return { errors };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function main() {
|
|
159
|
+
const input = process.argv[2];
|
|
160
|
+
if (!input) {
|
|
161
|
+
usage();
|
|
162
|
+
process.exit(2);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const bundle = path.resolve(process.cwd(), input);
|
|
166
|
+
const { errors } = validateBundle(bundle);
|
|
167
|
+
if (errors.length > 0) {
|
|
168
|
+
console.error(`FAIL ${path.relative(process.cwd(), bundle) || bundle}`);
|
|
169
|
+
for (const error of errors) console.error(`- ${error}`);
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
console.log(`PASS ${path.relative(process.cwd(), bundle) || bundle}`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
main();
|
|
@@ -42,7 +42,8 @@ function checkBrokenLinks(docsDir) {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
function main() {
|
|
45
|
-
const
|
|
45
|
+
const docsArg = process.argv[2] || 'docs';
|
|
46
|
+
const docsDir = path.resolve(process.cwd(), docsArg);
|
|
46
47
|
console.log(`[Docs Validator] Auditing bounds directory: ${docsDir}`);
|
|
47
48
|
|
|
48
49
|
if (!fs.existsSync(docsDir)) {
|
|
@@ -35,7 +35,7 @@ export GEMINI_API_KEY_2="key2" # auto-rotates on rate limit
|
|
|
35
35
|
|
|
36
36
|
**Verify setup**: `python scripts/check_setup.py`
|
|
37
37
|
**Analyze media**: `python scripts/gemini_batch_process.py --files <file> --task <analyze|transcribe|extract>`
|
|
38
|
-
- TIP: When you're asked to analyze an image, check if `gemini` command is available, then use `echo "<prompt to analyze image>" | gemini -y -m <gemini.model>` command (read model from
|
|
38
|
+
- TIP: When you're asked to analyze an image, check if `gemini` command is available, then use `echo "<prompt to analyze image>" | gemini -y -m <gemini.model>` command (read model from `.claude/runtime.json`: `gemini.model`). If `gemini` command is not available, use `python scripts/gemini_batch_process.py --files <file> --task analyze` command.
|
|
39
39
|
|
|
40
40
|
> **Stdin support**: Pipe files via stdin for Gemini analysis (auto-detects PNG/JPG/PDF/WAV/MP3).
|
|
41
41
|
|