@haposoft/cafekit 0.8.17 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haposoft/cafekit",
3
- "version": "0.8.17",
3
+ "version": "0.9.1",
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",
@@ -43,5 +43,8 @@
43
43
  ],
44
44
  "engines": {
45
45
  "node": ">=18.0.0"
46
+ },
47
+ "devDependencies": {
48
+ "@opencode-ai/plugin": "1.15.11"
46
49
  }
47
50
  }
@@ -15,7 +15,7 @@ These rules reduce common agent coding failures: hidden assumptions, overbuilt s
15
15
  - Do not assume silently. State assumptions when they affect the work.
16
16
  - If multiple interpretations are plausible, surface them before implementation.
17
17
  - If the simpler option is likely better, say so and push back.
18
- - If the task/spec is too vague to verify, stop and ask or route back to spec correction.
18
+ - If the user asks a question about the project, use `/hapo:question` to answer from source evidence before planning.
19
19
  - Before feature planning or coding, read `./README.md` for project context.
20
20
 
21
21
  ### 2. Simplicity First
@@ -46,7 +46,7 @@ These rules reduce common agent coding failures: hidden assumptions, overbuilt s
46
46
  Use this loop for non-trivial work:
47
47
 
48
48
  1. **Understand** — read README, relevant docs, active spec/task, and existing code.
49
- 2. **Plan** — choose the smallest coherent path; use `/hapo:specs` for feature specs when needed.
49
+ 2. **Plan** — choose the smallest coherent path; use `/hapo:question` for evidence-backed project questions and `/hapo:specs` for feature specs when ready.
50
50
  3. **Execute** — implement only the active task/scope; no placeholder completion.
51
51
  4. **Verify** — run exact task commands first, then repo-level lint/test/build as needed.
52
52
  5. **Sync** — mark task state only after proof exists.
@@ -27,6 +27,7 @@
27
27
  "mobile-development",
28
28
  "pdf",
29
29
  "pptx",
30
+ "question",
30
31
  "react-best-practices",
31
32
  "research",
32
33
  "specs",
@@ -29,6 +29,7 @@ User wants to...
29
29
 
30
30
  ```text
31
31
  User wants to...
32
+ ├── answer questions from source code/docs/specs/config -> /hapo:question
32
33
  ├── find files, locate code, scan project structure -> /hapo:inspect
33
34
  ├── diagnose an error or failing check -> /hapo:debug
34
35
  ├── understand possible side effects -> /hapo:impact-analysis
@@ -41,6 +42,7 @@ Local source truth comes from `/hapo:inspect`; external/current knowledge comes
41
42
 
42
43
  ```text
43
44
  User wants to...
45
+ ├── ask/answer documentation questions from source -> /hapo:question
44
46
  ├── create missing project docs -> /hapo:docs --init
45
47
  ├── update docs after source changes -> /hapo:docs --update
46
48
  ├── summarize codebase quickly -> /hapo:docs --summarize
@@ -7,11 +7,13 @@ Do not inject or force a skill when the user explicitly asks for a direct answer
7
7
  ## Core Specification Workflow
8
8
 
9
9
  ```text
10
- /hapo:brainstorm -> /hapo:specs -> /hapo:develop -> /hapo:test -> /hapo:code-review -> /hapo:git
10
+ /hapo:question -> /hapo:brainstorm -> /hapo:specs -> /hapo:develop -> /hapo:test -> /hapo:code-review -> /hapo:git
11
11
  ```
12
12
 
13
13
  | User intent | Suggested start |
14
14
  |---|---|
15
+ | ask about source code, docs, specs, config, dependencies, or external/current technical facts | `/hapo:question` |
16
+ | vague request, missing acceptance criteria, unclear scope, stakeholder questions | ask one clarification or use `/hapo:brainstorm` |
15
17
  | unclear idea, possible approaches, scope tradeoffs | `/hapo:brainstorm` |
16
18
  | create requirements, design, task breakdown, validate spec | `/hapo:specs` |
17
19
  | implement an approved spec or a specific task file | `/hapo:develop` |
@@ -81,4 +83,6 @@ Use `/hapo:research` for external knowledge. Use `/hapo:inspect` for local sourc
81
83
  - Add a secondary skill only when it materially changes execution.
82
84
  - Prefer explicit user commands over this table.
83
85
  - If two workflows conflict, ask one concise clarification.
86
+ - If the user asks a factual or project question before workflow selection, use `/hapo:question`.
87
+ - If missing information blocks workflow selection, ask one concise clarification or use `/hapo:brainstorm`.
84
88
  - If the user asks to "fix" and root cause is unknown, start with `/hapo:debug` unless the issue is trivial and local.
@@ -0,0 +1,162 @@
1
+ ---
2
+ name: hapo:question
3
+ description: "Answer questions with evidence. Use when the user asks about project behavior, source code, specs, docs, configuration, dependencies, or external technical information; inspect the repo first, use internet/current docs when repo evidence is insufficient, and ask back only when the question cannot be answered safely."
4
+ argument-hint: "<question> [--repo|--web|--both|--brief|--deep]"
5
+ version: 1.0.0
6
+ ---
7
+
8
+ # Question Skill
9
+
10
+ `hapo:question` is an evidence-backed question-answering skill. It answers user questions by checking the local project first, then external/current sources when the repository cannot answer the question.
11
+
12
+ ## Core Stance
13
+
14
+ - Answer the question; do not turn every question into a spec or planning session.
15
+ - Source-first: prefer README, docs, specs, code, config, tests, logs, and package metadata before assumptions.
16
+ - Use external/current sources when the local repo is missing the answer, the topic is version-sensitive, or the user asks about outside tools, libraries, APIs, laws, pricing, or best practices.
17
+ - Ask back only when the question is too broad, has no target, needs credentials/private context, or any answer would be misleading.
18
+ - Do not edit files, implement code, create specs, or change project state.
19
+
20
+ <ANSWER-ONLY-GATE>
21
+ Do NOT implement, scaffold, refactor, modify files, generate full specs, or make final architecture decisions.
22
+ The output is an answer, evidence, confidence, and a follow-up only when needed.
23
+ </ANSWER-ONLY-GATE>
24
+
25
+ ## When To Use
26
+
27
+ Use `hapo:question` when the user asks:
28
+ - "Trong hệ thống này X làm gì?"
29
+ - "Luồng này đang xử lý ở đâu?"
30
+ - "Config/version/package hiện tại là gì?"
31
+ - "Có dùng thư viện/công nghệ/cách làm nào không?"
32
+ - "Requirement/spec này có khớp code không?"
33
+ - "Tool/library/framework này nên dùng thế nào?"
34
+ - "Theo best practice/current docs thì sao?"
35
+ - "Tôi nên hiểu phần này như thế nào?"
36
+
37
+ Do not use it when:
38
+ - The user asks to implement, fix, debug, test, commit, or publish.
39
+ - The user wants ideation/tradeoff exploration; use `hapo:brainstorm`.
40
+ - The user wants a formal spec; use `hapo:specs`.
41
+ - The user wants full legacy documentation reconstruction; use `hapo:docs --reconstruct`.
42
+ - The user wants root-cause diagnosis for a failure; use `hapo:debug`.
43
+
44
+ ## Modes
45
+
46
+ - Default: repo-first answer. Use external sources only if local evidence is insufficient.
47
+ - `--repo`: answer from local source only. If not enough evidence, say so and do not use web.
48
+ - `--web`: answer from external/current sources. Still read local context if the question mentions this project.
49
+ - `--both`: explicitly combine local source evidence and external/current evidence.
50
+ - `--brief`: short answer, minimal evidence.
51
+ - `--deep`: broader evidence scan, include trace and tradeoffs.
52
+
53
+ If modes conflict, prefer the most explicit source mode in this order: `--both`, `--repo`, `--web`, then default. Apply `--brief` or `--deep` as output depth modifiers.
54
+
55
+ ## Workflow
56
+
57
+ ### 1. Classify The Question
58
+
59
+ Pick one primary answer type:
60
+
61
+ ```text
62
+ source-code behavior | config/version | spec/docs consistency | dependency/library | workflow/process | external/current knowledge | unclear
63
+ ```
64
+
65
+ If `unclear`, ask one focused follow-up with the minimum choices needed.
66
+
67
+ ### 2. Gather Evidence
68
+
69
+ Repo evidence priority:
70
+ 1. `README.md`, `AGENTS.md`, `CLAUDE.md`
71
+ 2. relevant `docs/`, `specs/`, `.claude/`, `.opencode/`
72
+ 3. source files, tests, scripts, package manifests, config files
73
+ 4. git history only when the question asks about changes or provenance
74
+
75
+ Use focused search (`rg`, targeted file reads) instead of broad scans. Use `hapo:inspect` when the user asks where something lives or the source surface is unclear.
76
+
77
+ External evidence:
78
+ - Use external/current docs when repo evidence is absent, stale, or not authoritative.
79
+ - Prefer official docs, standards, source repositories, or primary vendor docs.
80
+ - State clearly when an answer is inferred from external docs rather than confirmed in the repo.
81
+
82
+ ### 3. Decide Whether To Answer Or Ask Back
83
+
84
+ Answer directly when at least one of these is true:
85
+ - repo evidence answers the question,
86
+ - external/current evidence answers the non-project part,
87
+ - a cautious answer can separate known facts from inference.
88
+
89
+ Ask back when:
90
+ - the target system/module is not named and multiple targets are plausible,
91
+ - the needed file/source is blocked or private,
92
+ - the question requires business/domain judgment not present in source,
93
+ - the answer would require guessing customer intent,
94
+ - the question bundles multiple unrelated topics.
95
+
96
+ Ask only one follow-up question. Include 2-3 concrete options if helpful.
97
+
98
+ ### 4. Output Contract
99
+
100
+ Default output:
101
+
102
+ ```markdown
103
+ **Answer**
104
+ ...
105
+
106
+ **Evidence**
107
+ - `path/to/file`: what it proves
108
+ - External: source name/link, what it proves
109
+
110
+ **Confidence**
111
+ high | medium | low
112
+
113
+ **Gaps**
114
+ - ...
115
+
116
+ **Next**
117
+ ...
118
+ ```
119
+
120
+ Use `templates/question.md` when the user asks to save or document the answer.
121
+
122
+ ## Answer Style
123
+
124
+ - Lead with the answer, not the research process.
125
+ - Use file paths and line references when local evidence is used.
126
+ - Separate "confirmed by source" from "inferred".
127
+ - Keep the answer proportional: concise for simple questions, structured for complex ones.
128
+ - If external sources were used, include source links or names.
129
+ - If no evidence exists, say "không thấy bằng chứng trong source hiện tại" and explain what was checked.
130
+
131
+ ## Examples
132
+
133
+ ### Source Question
134
+
135
+ ```text
136
+ /hapo:question "Trong hệ thống này cafekit dùng file nào để bật/tắt skill routing?"
137
+ ```
138
+
139
+ Expected behavior:
140
+ - read runtime/config/install files,
141
+ - answer with exact config file and relevant keys,
142
+ - say whether it is runtime-driven or hard-coded.
143
+
144
+ ### Mixed Source + Web Question
145
+
146
+ ```text
147
+ /hapo:question "Dự án này đang dùng React version nào, và version đó có còn phù hợp theo best practice hiện tại không?" --both
148
+ ```
149
+
150
+ Expected behavior:
151
+ - read `package.json` or lockfile,
152
+ - check current React docs/release info if needed,
153
+ - separate local version from external recommendation.
154
+
155
+ ### Ask Back
156
+
157
+ ```text
158
+ /hapo:question "Hệ thống này có ổn không?"
159
+ ```
160
+
161
+ Expected behavior:
162
+ - ask one narrowing question because "ổn" could mean architecture, security, performance, UX, tests, or deploy readiness.
@@ -0,0 +1,32 @@
1
+ # Question Answer: {{TITLE}}
2
+
3
+ ## Question
4
+ {{QUESTION}}
5
+
6
+ ## Answer
7
+ {{ANSWER}}
8
+
9
+ ## Evidence
10
+ | Source | Finding | Confidence |
11
+ |---|---|---|
12
+ | {{SOURCE}} | {{FINDING}} | {{CONFIDENCE}} |
13
+
14
+ ## Source Trace
15
+ - Repo files checked: {{REPO_FILES}}
16
+ - External/current sources checked: {{EXTERNAL_SOURCES}}
17
+ - Search terms or commands: {{SEARCH_TRACE}}
18
+
19
+ ## Confirmed Facts
20
+ - {{FACT}}
21
+
22
+ ## Inferences
23
+ - {{INFERENCE}}
24
+
25
+ ## Gaps / Unknowns
26
+ - {{GAP}}
27
+
28
+ ## Follow-up Question
29
+ {{FOLLOW_UP_OR_NONE}}
30
+
31
+ ## Next
32
+ {{NEXT_STEP}}
@@ -7,8 +7,8 @@ Primary operating instructions for OpenCode using the CafeKit runtime.
7
7
  - Project instructions live in `AGENTS.md`.
8
8
  - CafeKit commands live in `.opencode/commands/` and use OpenCode-native slash names without the `hapo:` prefix.
9
9
  - CafeKit subagents live in `.opencode/agents/` using OpenCode frontmatter.
10
- - CafeKit skills intentionally live in `.claude/skills/` because OpenCode reads Claude-compatible skills natively.
11
- - Shared CafeKit support files live in `.claude/rules/`, `.claude/scripts/`, `.claude/references/`, and `.claude/runtime.json`.
10
+ - CafeKit skills live in `.opencode/skills/` and are read natively by OpenCode.
11
+ - Shared CafeKit support files live in `.opencode/rules/`, `.opencode/scripts/`, `.opencode/references/`, and `.opencode/runtime.json`.
12
12
  - OpenCode config is merged into `opencode.json`; keep project-specific model/provider choices there.
13
13
 
14
14
  ## Command Surface
@@ -16,6 +16,7 @@ Primary operating instructions for OpenCode using the CafeKit runtime.
16
16
  Use these OpenCode commands:
17
17
 
18
18
  - `/brainstorm <idea-or-problem>`: scout and narrow an unclear idea before specs.
19
+ - `/question <question> [--repo|--web|--both|--brief|--deep]`: answer questions using repo evidence first, then external/current sources when needed.
19
20
  - `/specs <feature-or-spec-command>`: create, update, validate, or approve specs.
20
21
  - `/develop <feature> [task-file] [--flash]`: implement approved spec tasks.
21
22
  - `/test [scope|--full]`: verify implementation and collect evidence.
@@ -26,7 +27,7 @@ Use these OpenCode commands:
26
27
  - `/inspect <target>`: inspect source, artifacts, or external context.
27
28
  - `/generate-graph <request>`: generate technical diagrams.
28
29
 
29
- The underlying CafeKit skills are still named `hapo:*` in their documentation. In OpenCode, use the commands above; each command reads the matching skill from `.claude/skills/<skill>/SKILL.md`.
30
+ The underlying CafeKit skills are still named `hapo:*` in their documentation. In OpenCode, use the commands above; each command reads the matching skill from `.opencode/skills/<skill>/SKILL.md`.
30
31
 
31
32
  ## Core Objective
32
33
 
@@ -41,7 +42,7 @@ These rules reduce common agent coding failures: hidden assumptions, overbuilt s
41
42
  - Do not assume silently. State assumptions when they affect the work.
42
43
  - If multiple interpretations are plausible, surface them before implementation.
43
44
  - If the simpler option is likely better, say so and push back.
44
- - If the task/spec is too vague to verify, stop and ask or route back to spec correction.
45
+ - If the user asks a question about the project, use `/question` to answer from source evidence before planning.
45
46
  - Before feature planning or coding, read `./README.md` for project context.
46
47
 
47
48
  ### 2. Simplicity First
@@ -72,7 +73,7 @@ These rules reduce common agent coding failures: hidden assumptions, overbuilt s
72
73
  Use this loop for non-trivial work:
73
74
 
74
75
  1. Understand: read README, relevant docs, active spec/task, and existing code.
75
- 2. Plan: choose the smallest coherent path; use `/specs` for feature specs when needed.
76
+ 2. Plan: choose the smallest coherent path; use `/question` for evidence-backed project questions and `/specs` for feature specs when ready.
76
77
  3. Execute: implement only the active task/scope; no placeholder completion.
77
78
  4. Verify: run exact task commands first, then repo-level lint/test/build as needed.
78
79
  5. Sync: mark task state only after proof exists.
@@ -137,4 +138,3 @@ Consult these when the task touches the relevant area:
137
138
  ## Language Consistency
138
139
 
139
140
  When generating specs or structured project output, use the user's preferred language consistently across the whole spec workspace. Technical terms, code samples, and file paths may remain English.
140
-
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Copyright (c) 2026 Haposoft. MIT License.
3
+ *
4
+ * OpenCode Plugin — docs-sync.ts
5
+ *
6
+ * Port of .claude/hooks/docs-sync.cjs.
7
+ *
8
+ * Behavior:
9
+ * - On session.created: detect if project has source code but no docs/, or if
10
+ * docs/.sync_hash is stale relative to current source-only git hash.
11
+ * - Write banner to `.opencode/session-banner.md` so AGENTS.md / skills can
12
+ * surface it (OpenCode event console.log does not flow into assistant
13
+ * context, only the server log).
14
+ * - Also print to stderr so the OpenCode server log records it.
15
+ *
16
+ * Disable via `.opencode/runtime.json` { "docsSync": false }.
17
+ */
18
+
19
+ import type { Plugin } from "@opencode-ai/plugin";
20
+ import {
21
+ existsSync,
22
+ readFileSync,
23
+ writeFileSync,
24
+ mkdirSync,
25
+ appendFileSync,
26
+ } from "node:fs";
27
+ import { join, dirname } from "node:path";
28
+ import { fileURLToPath } from "node:url";
29
+ import { execSync } from "node:child_process";
30
+
31
+ const PLUGIN_DIR = dirname(fileURLToPath(import.meta.url));
32
+ const BANNER_SECTION_START = "<!-- CAFEKIT DOCS SYNC START -->";
33
+ const BANNER_SECTION_END = "<!-- CAFEKIT DOCS SYNC END -->";
34
+
35
+ function readRuntime(cwd: string): Record<string, any> {
36
+ try {
37
+ const file = join(cwd, ".opencode", "runtime.json");
38
+ return existsSync(file) ? JSON.parse(readFileSync(file, "utf8")) : {};
39
+ } catch {
40
+ return {};
41
+ }
42
+ }
43
+
44
+ function resolveDocsDir(cwd: string, runtime: Record<string, any>): string {
45
+ const configured = runtime.paths?.docs;
46
+ return join(cwd, typeof configured === "string" && configured ? configured : "docs");
47
+ }
48
+
49
+ function hasSourceCode(cwd: string): boolean {
50
+ return (
51
+ existsSync(join(cwd, "src")) ||
52
+ existsSync(join(cwd, "app")) ||
53
+ existsSync(join(cwd, "lib")) ||
54
+ existsSync(join(cwd, "package.json")) ||
55
+ existsSync(join(cwd, "index.js")) ||
56
+ existsSync(join(cwd, "main.py"))
57
+ );
58
+ }
59
+
60
+ function currentSourceHash(cwd: string): string {
61
+ try {
62
+ return execSync('git log -1 --format="%H" -- . ":(exclude)docs"', {
63
+ cwd,
64
+ encoding: "utf8",
65
+ stdio: ["ignore", "pipe", "ignore"],
66
+ }).trim();
67
+ } catch {
68
+ return "";
69
+ }
70
+ }
71
+
72
+ function buildMissingDocsBanner(): string {
73
+ return [
74
+ "### 🔴 URGENT SYSTEM ACTION: MISSING DOCUMENTATION 🔴",
75
+ "> 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.",
76
+ "> 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:",
77
+ "> 1. `docs/system-architecture.md` (Kiến trúc hệ thống, phân tích flow)",
78
+ "> 2. `docs/project-overview-pdr.md` (Tổng quan, mục tiêu, tính năng)",
79
+ "> 3. Khởi tạo file hidden `docs/.sync_hash` để theo dõi cập nhật.",
80
+ "> ",
81
+ '> 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"`',
82
+ "> Sau đó ghi chuẩn giá trị Hash đó vào file `docs/.sync_hash`.",
83
+ ].join("\n");
84
+ }
85
+
86
+ function buildStaleDocsBanner(currentHash: string, lastSyncHash: string): string {
87
+ return [
88
+ "### 🔄 CONTINUOUS DOCS SYNC REQUIRED 🔄",
89
+ `> 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"}\`).`,
90
+ "> YÊU CẦU TRƯỚC KHI LÀM THÊM TÍNH NĂNG:",
91
+ `> 1. Rà soát file bị thay đổi gần đây (dùng \`git diff ${lastSyncHash} ${currentHash}\` hoặc \`git log\`).`,
92
+ "> 2. Cập nhật lại `docs/system-architecture.md` hoặc các chuẩn code nếu cần thiết.",
93
+ "> 3. Cập nhật Changelog (nhật ký thay đổi).",
94
+ `> 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.`,
95
+ ].join("\n");
96
+ }
97
+
98
+ function writeBanner(cwd: string, banner: string | null): void {
99
+ const target = join(cwd, ".opencode", "session-banner.md");
100
+ const dir = dirname(target);
101
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
102
+
103
+ const existing = existsSync(target) ? readFileSync(target, "utf8") : "";
104
+ const pattern = new RegExp(`${BANNER_SECTION_START}[\\s\\S]*?${BANNER_SECTION_END}\\n?`);
105
+ const cleaned = existing.replace(pattern, "").trimEnd();
106
+
107
+ if (!banner) {
108
+ if (cleaned !== existing.trimEnd()) {
109
+ writeFileSync(target, cleaned ? `${cleaned}\n` : "");
110
+ }
111
+ return;
112
+ }
113
+
114
+ const block = [BANNER_SECTION_START, banner, BANNER_SECTION_END].join("\n");
115
+ const next = cleaned ? `${cleaned}\n\n${block}\n` : `${block}\n`;
116
+ writeFileSync(target, next);
117
+ }
118
+
119
+ function logCrash(error: unknown): void {
120
+ try {
121
+ const logDir = join(PLUGIN_DIR, ".logs");
122
+ if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true });
123
+ appendFileSync(
124
+ join(logDir, "hook-log.jsonl"),
125
+ JSON.stringify({
126
+ ts: new Date().toISOString(),
127
+ hook: "docs-sync",
128
+ status: "crash",
129
+ error: error instanceof Error ? error.message : String(error),
130
+ }) + "\n",
131
+ );
132
+ } catch {
133
+ // fail-open
134
+ }
135
+ }
136
+
137
+ export const DocsSync: Plugin = async ({ directory }) => ({
138
+ event: async ({ event }) => {
139
+ try {
140
+ if (event.type !== "session.created") return;
141
+
142
+ const cwd = directory;
143
+ const runtime = readRuntime(cwd);
144
+ if (runtime.docsSync === false) return;
145
+
146
+ if (!hasSourceCode(cwd)) {
147
+ writeBanner(cwd, null);
148
+ return;
149
+ }
150
+
151
+ const docsDir = resolveDocsDir(cwd, runtime);
152
+
153
+ if (!existsSync(docsDir)) {
154
+ const banner = buildMissingDocsBanner();
155
+ writeBanner(cwd, banner);
156
+ console.error(banner);
157
+ return;
158
+ }
159
+
160
+ const currentHash = currentSourceHash(cwd);
161
+ if (!currentHash) {
162
+ writeBanner(cwd, null);
163
+ return;
164
+ }
165
+
166
+ const syncFile = join(docsDir, ".sync_hash");
167
+ const lastSyncHash = existsSync(syncFile) ? readFileSync(syncFile, "utf8").trim() : "";
168
+
169
+ if (lastSyncHash === currentHash) {
170
+ writeBanner(cwd, null);
171
+ return;
172
+ }
173
+
174
+ const banner = buildStaleDocsBanner(currentHash, lastSyncHash);
175
+ writeBanner(cwd, banner);
176
+ console.error(banner);
177
+ } catch (error) {
178
+ logCrash(error);
179
+ }
180
+ },
181
+ });
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Copyright (c) 2026 Haposoft. MIT License.
3
+ *
4
+ * OpenCode Plugin — inspect-block.ts
5
+ *
6
+ * Port of .claude/hooks/inspect-block.cjs.
7
+ *
8
+ * Behavior:
9
+ * - Block reads/writes/globs/greps that target heavy directories (node_modules,
10
+ * .git, dist, build, .next, .venv, vendor, target, coverage, ...).
11
+ * - Block excessively broad glob patterns (e.g. **\/* alone).
12
+ * - Allow approved build/package-manager bash commands.
13
+ *
14
+ * Disable via `.opencode/runtime.json` { "inspect": { "enabled": false } }
15
+ * or `.opencode/runtime.json` { "scout": { "enabled": false } }.
16
+ */
17
+
18
+ import type { Plugin } from "@opencode-ai/plugin";
19
+ import { existsSync, readFileSync, mkdirSync, appendFileSync } from "node:fs";
20
+ import { join, dirname } from "node:path";
21
+ import { fileURLToPath } from "node:url";
22
+
23
+ const BLOCKED_DIRS = [
24
+ "node_modules",
25
+ "dist",
26
+ "build",
27
+ ".next",
28
+ ".nuxt",
29
+ ".output",
30
+ "__pycache__",
31
+ ".venv",
32
+ "venv",
33
+ ".env",
34
+ "vendor",
35
+ "target",
36
+ ".git",
37
+ "coverage",
38
+ ".nyc_output",
39
+ ];
40
+
41
+ const BROAD_GLOB = [/^\*\*\/\*$/, /^\*\*\.\w+$/, /^\*\*\/\*\.\w+$/];
42
+
43
+ const ALLOWED_CMD =
44
+ /^(npm|pnpm|yarn|bun|npx|pnpx|bunx|tsc|vite|esbuild|webpack|rollup|turbo|nx|jest|vitest|eslint|prettier|go|cargo|make|mvn|gradle|dotnet|docker|kubectl|helm|python3?|pip|uv|deno|bundle|rake|php|composer|ruby|mix)\b/;
45
+ const VENV_EXEC = /(^|[\/\\])\.?venv[\/\\](bin|Scripts)[\/\\]/;
46
+ const VENV_CREATE = /^(python3?|py)\s+.*-m\s+venv\s+|^uv\s+venv(\s|$)/;
47
+
48
+ const GATED_TOOLS = new Set(["read", "write", "edit", "glob", "grep", "bash"]);
49
+
50
+ const PLUGIN_DIR = dirname(fileURLToPath(import.meta.url));
51
+
52
+ function isAllowedCommand(cmd: string): boolean {
53
+ const stripped = cmd
54
+ .trim()
55
+ .replace(/^(\w+=\S+\s+)+/, "")
56
+ .replace(/^(sudo|env|time)\s+/, "")
57
+ .trim();
58
+ return ALLOWED_CMD.test(stripped) || VENV_EXEC.test(stripped) || VENV_CREATE.test(stripped);
59
+ }
60
+
61
+ function isBlockedPath(p: string): string | null {
62
+ const segments = p.replace(/\\/g, "/").split("/");
63
+ return segments.find((seg) => BLOCKED_DIRS.includes(seg)) ?? null;
64
+ }
65
+
66
+ function isBroadGlob(p: string): boolean {
67
+ return BROAD_GLOB.some((rule) => rule.test(p.trim()));
68
+ }
69
+
70
+ function extractPaths(tool: string, args: Record<string, unknown>): string[] {
71
+ const out: string[] = [];
72
+ if (!args) return out;
73
+
74
+ if (typeof args.filePath === "string") out.push(args.filePath);
75
+ if (typeof args.path === "string") out.push(args.path);
76
+ if (typeof args.pattern === "string" && tool === "glob") out.push(args.pattern);
77
+
78
+ if (tool === "bash" && typeof args.command === "string") {
79
+ const matches = args.command.match(/(?:cat|ls|find|head|tail)\s+(\S+)/g);
80
+ if (matches) {
81
+ for (const m of matches) {
82
+ const tail = m.trim().split(/\s+/).pop();
83
+ if (tail) out.push(tail);
84
+ }
85
+ }
86
+ }
87
+
88
+ return out.filter(Boolean);
89
+ }
90
+
91
+ function readRuntime(cwd: string): Record<string, any> {
92
+ try {
93
+ const file = join(cwd, ".opencode", "runtime.json");
94
+ return existsSync(file) ? JSON.parse(readFileSync(file, "utf8")) : {};
95
+ } catch {
96
+ return {};
97
+ }
98
+ }
99
+
100
+ function logCrash(error: unknown): void {
101
+ try {
102
+ const logDir = join(PLUGIN_DIR, ".logs");
103
+ if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true });
104
+ appendFileSync(
105
+ join(logDir, "hook-log.jsonl"),
106
+ JSON.stringify({
107
+ ts: new Date().toISOString(),
108
+ hook: "inspect-block",
109
+ status: "crash",
110
+ error: error instanceof Error ? error.message : String(error),
111
+ }) + "\n",
112
+ );
113
+ } catch {
114
+ // fail-open
115
+ }
116
+ }
117
+
118
+ export const InspectBlock: Plugin = async ({ directory }) => ({
119
+ "tool.execute.before": async (input, output) => {
120
+ try {
121
+ if (!GATED_TOOLS.has(input.tool)) return;
122
+
123
+ const runtime = readRuntime(directory);
124
+ if (runtime.scout?.enabled === false || runtime.inspect?.enabled === false) return;
125
+
126
+ const args = (output.args ?? {}) as Record<string, unknown>;
127
+
128
+ if (input.tool === "bash" && typeof args.command === "string") {
129
+ const cmds = args.command.split(/\s*(?:&&|\|\||;)\s*/).filter(Boolean);
130
+ if (cmds.every((c) => isAllowedCommand(c))) return;
131
+ }
132
+
133
+ if (input.tool === "glob" && typeof args.pattern === "string" && isBroadGlob(args.pattern)) {
134
+ throw new Error(
135
+ [
136
+ "SCOPE LIMIT EXCEEDED: Glob pattern is excessively broad",
137
+ `Requested Pattern: ${args.pattern}`,
138
+ "",
139
+ "Please narrow your scope (e.g., src/**/*.ts rather than **/*.ts).",
140
+ ].join("\n"),
141
+ );
142
+ }
143
+
144
+ const paths = extractPaths(input.tool, args);
145
+ for (const p of paths) {
146
+ const blocked = isBlockedPath(p);
147
+ if (blocked) {
148
+ throw new Error(
149
+ [
150
+ `SCOPE LIMIT EXCEEDED: Directory "${blocked}/" is explicitly forbidden`,
151
+ `Requested Path: ${p}`,
152
+ `Restricted zones: ${BLOCKED_DIRS.join(", ")}`,
153
+ ].join("\n"),
154
+ );
155
+ }
156
+ }
157
+ } catch (error) {
158
+ if (error instanceof Error && error.message.startsWith("SCOPE LIMIT EXCEEDED")) {
159
+ throw error;
160
+ }
161
+ logCrash(error);
162
+ }
163
+ },
164
+ });