@haposoft/cafekit 0.8.17 → 0.9.0
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 +47 -84
- package/bin/install.js +90 -653
- package/bin/lib/copy-utils.js +61 -0
- package/bin/lib/opencode-install.js +866 -0
- package/package.json +4 -1
- package/src/claude/CLAUDE.md +2 -2
- package/src/claude/migration-manifest.json +1 -0
- package/src/claude/rules/skill-domain-routing.md +2 -0
- package/src/claude/rules/skill-workflow-routing.md +3 -1
- package/src/claude/skills/qs/SKILL.md +177 -0
- package/src/claude/skills/qs/templates/qs.md +30 -0
- package/src/opencode/AGENTS.md +6 -6
- package/src/opencode/plugins/docs-sync.ts +181 -0
- package/src/opencode/plugins/inspect-block.ts +164 -0
- package/src/opencode/plugins/privacy-block.ts +182 -0
- package/src/opencode/plugins/rules.ts +163 -0
- package/src/opencode/plugins/session.ts +179 -0
- package/src/opencode/plugins/state.ts +253 -0
- package/src/opencode/plugins/usage.ts +178 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@haposoft/cafekit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
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
|
}
|
package/src/claude/CLAUDE.md
CHANGED
|
@@ -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,
|
|
18
|
+
- If the task/spec is too vague to verify, use `/hapo:qs` or ask one grounded clarification 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
|
|
49
|
+
2. **Plan** — choose the smallest coherent path; use `/hapo:qs` when requirements are unclear 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.
|
|
@@ -29,6 +29,7 @@ User wants to...
|
|
|
29
29
|
|
|
30
30
|
```text
|
|
31
31
|
User wants to...
|
|
32
|
+
├── clarify missing requirements or acceptance criteria -> /hapo:qs
|
|
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
|
+
├── prepare stakeholder/customer questions before docs -> /hapo:qs --batch
|
|
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,12 @@ 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:qs -> /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
|
+
| vague request, missing acceptance criteria, unclear scope, stakeholder questions | `/hapo:qs` |
|
|
15
16
|
| unclear idea, possible approaches, scope tradeoffs | `/hapo:brainstorm` |
|
|
16
17
|
| create requirements, design, task breakdown, validate spec | `/hapo:specs` |
|
|
17
18
|
| implement an approved spec or a specific task file | `/hapo:develop` |
|
|
@@ -81,4 +82,5 @@ Use `/hapo:research` for external knowledge. Use `/hapo:inspect` for local sourc
|
|
|
81
82
|
- Add a secondary skill only when it materially changes execution.
|
|
82
83
|
- Prefer explicit user commands over this table.
|
|
83
84
|
- If two workflows conflict, ask one concise clarification.
|
|
85
|
+
- If missing information blocks workflow selection, use `/hapo:qs` before choosing brainstorm/specs/develop.
|
|
84
86
|
- 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,177 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: hapo:qs
|
|
3
|
+
description: "Clarify vague requests before planning or implementation. Use when requirements, acceptance criteria, scope, constraints, stakeholder decisions, or next workflow readiness are unclear."
|
|
4
|
+
argument-hint: "<request_or_context> [--batch|--spec-ready|--stakeholder]"
|
|
5
|
+
version: 1.0.0
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# QS Skill
|
|
9
|
+
|
|
10
|
+
`hapo:qs` turns unclear intent into answerable questions and a readiness verdict. It is a clarification gate before `hapo:brainstorm`, `hapo:specs`, `hapo:docs --reconstruct`, `hapo:debug`, or `hapo:develop`.
|
|
11
|
+
|
|
12
|
+
## Core Stance
|
|
13
|
+
|
|
14
|
+
- Context before questions. Read the useful project/spec/docs/source context before asking.
|
|
15
|
+
- Questions are work products. Make them specific, prioritized, and easy to answer.
|
|
16
|
+
- One thing at a time in interactive mode.
|
|
17
|
+
- Prefer multiple-choice options with a recommended default when the context supports it.
|
|
18
|
+
- Do not design the solution unless the user explicitly moves to `hapo:brainstorm`.
|
|
19
|
+
- Do not create specs or write code from this skill.
|
|
20
|
+
|
|
21
|
+
<CLARIFICATION-ONLY-GATE>
|
|
22
|
+
Do NOT implement, scaffold, edit product code, generate full specs, or make architecture decisions as final.
|
|
23
|
+
The output is questions, assumptions, readiness, and next-step guidance.
|
|
24
|
+
</CLARIFICATION-ONLY-GATE>
|
|
25
|
+
|
|
26
|
+
## When To Use
|
|
27
|
+
|
|
28
|
+
Use `hapo:qs` when:
|
|
29
|
+
- The request is too vague to verify.
|
|
30
|
+
- Acceptance criteria are missing.
|
|
31
|
+
- Scope boundary is unclear.
|
|
32
|
+
- Stakeholder/customer input is needed before specs.
|
|
33
|
+
- The user asks "what should I ask?", "what is missing?", "can this go to specs?", or "is this clear enough?"
|
|
34
|
+
- A spec/task has ambiguity that would make `hapo:develop` risky.
|
|
35
|
+
- A legacy reconstruction or bug report lacks enough scope to start the correct workflow.
|
|
36
|
+
|
|
37
|
+
Do not use it when:
|
|
38
|
+
- The user asks for a direct factual answer that can be answered safely.
|
|
39
|
+
- The user already gave exact output, acceptance criteria, scope, constraints, and target files.
|
|
40
|
+
- The right action is clearly `hapo:debug`, `hapo:inspect`, `hapo:research`, or `hapo:docs --reconstruct`.
|
|
41
|
+
|
|
42
|
+
## Modes
|
|
43
|
+
|
|
44
|
+
- Default interactive mode: ask one highest-value question, then wait.
|
|
45
|
+
- `--batch`: produce a grouped question list for a stakeholder/customer/team.
|
|
46
|
+
- `--spec-ready`: audit whether the request can move to `hapo:specs`.
|
|
47
|
+
- `--stakeholder`: write business-friendly questions with minimal implementation jargon.
|
|
48
|
+
|
|
49
|
+
If multiple modes appear, use the most restrictive mode in this order: `--spec-ready`, `--batch`, `--stakeholder`, default.
|
|
50
|
+
|
|
51
|
+
## Workflow
|
|
52
|
+
|
|
53
|
+
### 1. Context Scout
|
|
54
|
+
|
|
55
|
+
Read only enough context to make the questions concrete:
|
|
56
|
+
- `README.md`
|
|
57
|
+
- relevant `docs/`
|
|
58
|
+
- relevant `specs/`
|
|
59
|
+
- relevant source files when the request names a feature, module, API, UI, or bug
|
|
60
|
+
|
|
61
|
+
Use `hapo:inspect` or focused search when the location is unclear. Do not scan the whole repo unless the question is about the whole repo.
|
|
62
|
+
|
|
63
|
+
### 2. Build The Clarification Map
|
|
64
|
+
|
|
65
|
+
Create a private map with these fields:
|
|
66
|
+
|
|
67
|
+
```text
|
|
68
|
+
Known facts:
|
|
69
|
+
Assumptions:
|
|
70
|
+
Missing decisions:
|
|
71
|
+
Blocking unknowns:
|
|
72
|
+
Non-blocking unknowns:
|
|
73
|
+
Possible next workflow:
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Classify each unknown:
|
|
77
|
+
- `P0`: blocks correct next action.
|
|
78
|
+
- `P1`: high-risk ambiguity; should answer before implementation.
|
|
79
|
+
- `P2`: useful detail; can defer.
|
|
80
|
+
|
|
81
|
+
### 3. Ask Or Report
|
|
82
|
+
|
|
83
|
+
Default interactive mode:
|
|
84
|
+
- Ask exactly one `P0` question.
|
|
85
|
+
- Make it concrete and grounded in observed context.
|
|
86
|
+
- If possible, provide 2-3 options.
|
|
87
|
+
- Put the recommended option first and explain the tradeoff in one sentence.
|
|
88
|
+
|
|
89
|
+
`--batch` or `--stakeholder`:
|
|
90
|
+
- Produce grouped questions by priority.
|
|
91
|
+
- Keep each question answerable.
|
|
92
|
+
- Include recommended options only when they are justified by context.
|
|
93
|
+
|
|
94
|
+
`--spec-ready`:
|
|
95
|
+
- Return a readiness checklist and verdict.
|
|
96
|
+
- Do not ask a long question list unless verdict is `not-ready`.
|
|
97
|
+
|
|
98
|
+
### 4. Readiness Verdict
|
|
99
|
+
|
|
100
|
+
End with one of:
|
|
101
|
+
- `not-ready`: missing P0 decisions.
|
|
102
|
+
- `brainstorm-ready`: enough to explore solution options, but not enough for specs.
|
|
103
|
+
- `specs-ready`: enough to run `/hapo:specs <request>`.
|
|
104
|
+
- `develop-ready`: an approved spec/task exists and no blocking ambiguity remains.
|
|
105
|
+
|
|
106
|
+
### 5. Next Step
|
|
107
|
+
|
|
108
|
+
Suggest exactly one next command:
|
|
109
|
+
- `/hapo:qs ...` when another clarification round is required.
|
|
110
|
+
- `/hapo:brainstorm ...` when design/options are still open.
|
|
111
|
+
- `/hapo:specs ...` when requirements are clear enough.
|
|
112
|
+
- `/hapo:debug ...` when the issue is a bug with unknown root cause.
|
|
113
|
+
- `/hapo:docs --reconstruct <scope>` when the task is legacy/as-is docs.
|
|
114
|
+
- `/hapo:develop <feature>` only when an approved spec/task is already clear.
|
|
115
|
+
|
|
116
|
+
## Output Formats
|
|
117
|
+
|
|
118
|
+
### Default Interactive Output
|
|
119
|
+
|
|
120
|
+
```markdown
|
|
121
|
+
**Context Read**
|
|
122
|
+
- ...
|
|
123
|
+
|
|
124
|
+
**Known**
|
|
125
|
+
- ...
|
|
126
|
+
|
|
127
|
+
**Blocking Unknown**
|
|
128
|
+
- P0: ...
|
|
129
|
+
|
|
130
|
+
**Question**
|
|
131
|
+
...
|
|
132
|
+
|
|
133
|
+
**Options**
|
|
134
|
+
- A. ... (Recommended) - ...
|
|
135
|
+
- B. ... - ...
|
|
136
|
+
- C. ... - ...
|
|
137
|
+
|
|
138
|
+
**Readiness**
|
|
139
|
+
not-ready | brainstorm-ready | specs-ready | develop-ready
|
|
140
|
+
|
|
141
|
+
**Next**
|
|
142
|
+
/hapo:qs ...
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Batch Output
|
|
146
|
+
|
|
147
|
+
Use `templates/qs.md` when the user asks to save or document the question set.
|
|
148
|
+
|
|
149
|
+
```markdown
|
|
150
|
+
**Question Set**
|
|
151
|
+
|
|
152
|
+
**P0 - Must Answer**
|
|
153
|
+
1. ...
|
|
154
|
+
|
|
155
|
+
**P1 - Should Answer**
|
|
156
|
+
1. ...
|
|
157
|
+
|
|
158
|
+
**P2 - Can Defer**
|
|
159
|
+
1. ...
|
|
160
|
+
|
|
161
|
+
**Assumptions To Confirm**
|
|
162
|
+
- ...
|
|
163
|
+
|
|
164
|
+
**Readiness**
|
|
165
|
+
...
|
|
166
|
+
|
|
167
|
+
**Next**
|
|
168
|
+
...
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Quality Bar
|
|
172
|
+
|
|
173
|
+
- Every P0 question must explain what decision it unlocks.
|
|
174
|
+
- No generic "please clarify" questions.
|
|
175
|
+
- No more than one interactive question per response.
|
|
176
|
+
- No implementation plan unless the next command is `hapo:brainstorm` or `hapo:specs`.
|
|
177
|
+
- If no P0/P1 unknown exists, do not invent questions. Say it is ready and name the next command.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Question Set: {{TITLE}}
|
|
2
|
+
|
|
3
|
+
## Context Read
|
|
4
|
+
- {{CONTEXT_ITEM}}
|
|
5
|
+
|
|
6
|
+
## Known Facts
|
|
7
|
+
- {{KNOWN_FACT}}
|
|
8
|
+
|
|
9
|
+
## Assumptions
|
|
10
|
+
- {{ASSUMPTION}}
|
|
11
|
+
|
|
12
|
+
## P0 - Must Answer
|
|
13
|
+
1. **Question:** {{P0_QUESTION}}
|
|
14
|
+
- **Why it matters:** {{WHY_IT_MATTERS}}
|
|
15
|
+
- **Recommended answer:** {{RECOMMENDED_ANSWER}}
|
|
16
|
+
- **Other options:** {{OTHER_OPTIONS}}
|
|
17
|
+
|
|
18
|
+
## P1 - Should Answer
|
|
19
|
+
1. **Question:** {{P1_QUESTION}}
|
|
20
|
+
- **Risk if unanswered:** {{RISK}}
|
|
21
|
+
|
|
22
|
+
## P2 - Can Defer
|
|
23
|
+
1. **Question:** {{P2_QUESTION}}
|
|
24
|
+
- **Can defer until:** {{DEFER_POINT}}
|
|
25
|
+
|
|
26
|
+
## Readiness Verdict
|
|
27
|
+
`not-ready | brainstorm-ready | specs-ready | develop-ready`
|
|
28
|
+
|
|
29
|
+
## Next Command
|
|
30
|
+
`/hapo:qs ... | /hapo:brainstorm ... | /hapo:specs ... | /hapo:debug ... | /hapo:docs --reconstruct ... | /hapo:develop ...`
|
package/src/opencode/AGENTS.md
CHANGED
|
@@ -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
|
|
11
|
-
- Shared CafeKit support files live in `.
|
|
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
|
+
- `/qs <request-or-context> [--batch|--spec-ready|--stakeholder]`: clarify requirements, missing decisions, and next workflow readiness.
|
|
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 `.
|
|
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,
|
|
45
|
+
- If the task/spec is too vague to verify, use `/qs` or ask one grounded clarification 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
|
|
76
|
+
2. Plan: choose the smallest coherent path; use `/qs` when requirements are unclear 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
|
+
});
|