@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 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.12",
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
- // Detect color support at module load (cached)
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 LOCAL_CONFIG_PATH = '.claude/.ck.json';
13
- const GLOBAL_CONFIG_PATH = path.join(os.homedir(), '.claude', '.ck.json');
12
+ const RUNTIME_CONFIG_PATH = '.claude/runtime.json';
14
13
 
15
- // Legacy export for backward compatibility
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 global config to set hooks.foo: false and have it persist even when
85
- * local config has hooks: {} (empty = inherit, not reset to defaults).
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 → global → local
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. Global config (~/.claude/.ck.json) - user preferences
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 { includeProject = true, includeAssertions = true, includeLocale = true } = options;
481
- const projectRoot = process.cwd();
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 configs from both locations
484
- const globalConfig = loadConfigFromPath(GLOBAL_CONFIG_PATH);
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 (!globalConfig && !localConfig) {
491
+ if (!runtimeConfig) {
489
492
  return getDefaultConfig(includeProject, includeAssertions, includeLocale);
490
493
  }
491
494
 
492
495
  try {
493
- // Deep merge: DEFAULT → global → local (local wins)
496
+ // Deep merge: DEFAULT → runtime (runtime wins)
494
497
  let merged = deepMerge({}, DEFAULT_CONFIG);
495
- if (globalConfig) merged = deepMerge(merged, globalConfig);
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({ includeProject: false, includeAssertions: false, includeLocale: false });
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
- LOCAL_CONFIG_PATH,
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
- // Check if usage tracking is disabled in runtime.json
25
- try {
26
- const runtimePath = path.join(process.cwd(), '.claude', 'runtime.json');
27
- if (fs.existsSync(runtimePath)) {
28
- const runtime = JSON.parse(fs.readFileSync(runtimePath, 'utf-8'));
29
- if (runtime.usage?.enabled === false) process.exit(0);
30
- }
31
- } catch { /* fail-open */ }
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 docsDir = path.resolve(process.cwd(), 'docs');
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 `$HOME/packages/spec/src/claude/.ck.json`: `gemini.model`). If `gemini` command is not available, use `python scripts/gemini_batch_process.py --files <file> --task analyze` command.
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