openmoneta-dev-kit 1.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.
Files changed (46) hide show
  1. package/README.md +103 -0
  2. package/agents/qa-autonomous.md +131 -0
  3. package/agents/requirement-analyst.md +98 -0
  4. package/agents/security-auditor.md +120 -0
  5. package/agents/ui-tester.md +186 -0
  6. package/bin/openmoneta.js +11 -0
  7. package/hooks/check-plan-exists.sh +154 -0
  8. package/hooks/enforce-docs-first.sh +169 -0
  9. package/hooks/inject-process-context.sh +117 -0
  10. package/hooks/track-changes.sh +46 -0
  11. package/hooks/verify-completion.sh +165 -0
  12. package/hooks.json +30 -0
  13. package/opencode/AGENTS.md.tpl +38 -0
  14. package/opencode/agents/qa-autonomous.md +42 -0
  15. package/opencode/agents/requirement-analyst.md +51 -0
  16. package/opencode/agents/security-auditor.md +46 -0
  17. package/opencode/agents/ui-tester.md +43 -0
  18. package/opencode/plugins/openmoneta-guard.ts +389 -0
  19. package/package.json +41 -0
  20. package/scripts/debug-hooks.sh +54 -0
  21. package/scripts/init-project.sh +438 -0
  22. package/scripts/list-affected-modules.sh +74 -0
  23. package/skills/auth-bypass-testing/SKILL.md +236 -0
  24. package/skills/automated-testing/SKILL.md +162 -0
  25. package/skills/automated-testing/scripts/install-playwright.sh +134 -0
  26. package/skills/module-architect/SKILL.md +256 -0
  27. package/skills/plan-writer/SKILL.md +229 -0
  28. package/skills/requirement-analysis/SKILL.md +163 -0
  29. package/skills/safe-push/SKILL.md +182 -0
  30. package/skills/security-checklist/SKILL.md +116 -0
  31. package/skills/test-strategy/SKILL.md +135 -0
  32. package/skills/ui-test-loop/SKILL.md +161 -0
  33. package/src/cli.js +63 -0
  34. package/src/commands/check.js +30 -0
  35. package/src/commands/init.js +43 -0
  36. package/src/commands/install.js +50 -0
  37. package/src/commands/uninstall.js +74 -0
  38. package/src/commands/update.js +81 -0
  39. package/src/lib/paths.js +46 -0
  40. package/src/lib/version.js +45 -0
  41. package/templates/AGENTS.md.tpl +106 -0
  42. package/templates/docs-INDEX.md.tpl +62 -0
  43. package/templates/env.test.tpl +16 -0
  44. package/templates/karpathy-reference.md +49 -0
  45. package/templates/plans-INDEX.md.tpl +38 -0
  46. package/templates/playwright.config.ts.tpl +44 -0
package/hooks.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "version": 1,
3
+ "hooks": {
4
+ "sessionStart": [
5
+ {
6
+ "command": "bash $HOME/.cursor/hooks/inject-process-context.sh"
7
+ }
8
+ ],
9
+ "preToolUse": [
10
+ {
11
+ "matcher": "Write|StrReplace|EditNotebook|Delete",
12
+ "command": "bash $HOME/.cursor/hooks/check-plan-exists.sh"
13
+ },
14
+ {
15
+ "matcher": "Read|Glob|Grep",
16
+ "command": "bash $HOME/.cursor/hooks/enforce-docs-first.sh"
17
+ }
18
+ ],
19
+ "afterFileEdit": [
20
+ {
21
+ "command": "bash $HOME/.cursor/hooks/track-changes.sh"
22
+ }
23
+ ],
24
+ "stop": [
25
+ {
26
+ "command": "bash $HOME/.cursor/hooks/verify-completion.sh"
27
+ }
28
+ ]
29
+ }
30
+ }
@@ -0,0 +1,38 @@
1
+ # OpenMoneta Dev Kit for OpenCode
2
+
3
+ > Global OpenCode rules installed by OpenMoneta Dev Kit. Project-level `AGENTS.md` still has priority and should be created/synced with `bash ~/.cursor/scripts/init-project.sh .` or the project init workflow documented by this kit.
4
+
5
+ ## Nguyên tắc
6
+
7
+ 1. Luôn trả lời bằng tiếng Việt.
8
+ 2. Khi project có `docs/INDEX.md`, đọc file đó trước khi đọc source code để dùng Token Routing.
9
+ 3. Dùng OpenMoneta workflow 6 bước: phân tích yêu cầu, thiết kế module, adaptive planning, triển khai, update docs/close plan, safe push khi user yêu cầu push.
10
+ 4. Task nhỏ/rõ ràng có thể không cần plan. Task lớn/rủi ro/mơ hồ phải tạo repo plan `Status: Draft`, trình user review, chỉ triển khai sau khi user approve và đổi `Status: In Progress`.
11
+ 5. Nếu có plan `In Progress`, chỉ sửa file trong `## Files ảnh hưởng` / `## Files thay đổi`.
12
+ 6. Khi user yêu cầu push, phải fetch/rebase remote ngay trước push; không dùng `git push --force` trên shared branch.
13
+
14
+ ## OpenCode Notes
15
+
16
+ - OpenCode đọc global rules từ `~/.config/opencode/AGENTS.md`.
17
+ - OpenCode đọc project rules từ `AGENTS.md` ở project root. Project rules là source of truth cho từng repo.
18
+ - OpenMoneta skills được cài vào `~/.config/opencode/skills/*/SKILL.md` và agent nên load bằng skill tool khi cần.
19
+ - OpenMoneta subagents được cài vào `~/.config/opencode/agents/`.
20
+ - OpenCode plugin guard trong `~/.config/opencode/plugins/openmoneta-guard.ts` enforce một phần workflow tương đương Cursor hooks.
21
+
22
+ ## Khi Bắt Đầu Task
23
+
24
+ Nếu project có OpenMoneta files:
25
+
26
+ 1. Read `docs/INDEX.md`.
27
+ 2. Read `plans/INDEX.md` để kiểm tra plan active.
28
+ 3. Chọn 1-3 module liên quan qua Token Routing.
29
+ 4. Read `docs/modules/<module>/README.md` nếu có.
30
+ 5. Chỉ sau đó đọc source liên quan.
31
+
32
+ Nếu project chưa có OpenMoneta files, nhắc user chạy:
33
+
34
+ ```bash
35
+ bash ~/.config/opencode/scripts/init-project.sh .
36
+ ```
37
+
38
+ hoặc hỏi user có muốn khởi tạo project-level files không.
@@ -0,0 +1,42 @@
1
+ ---
2
+ description: ON-DEMAND ONLY. Viết unit/integration test khi user yêu cầu, tự chạy test, debug, fix loop đến khi pass hoặc gặp blocker rõ.
3
+ mode: subagent
4
+ permission:
5
+ edit: ask
6
+ bash: ask
7
+ ---
8
+
9
+ Bạn là **qa-autonomous** của OpenMoneta Dev Kit trong OpenCode.
10
+
11
+ Chỉ dùng khi user yêu cầu viết test hoặc primary agent cần isolation cho test loop.
12
+
13
+ ## Workflow
14
+
15
+ 1. Đọc plan active và Acceptance Criteria/Test Plan nếu có.
16
+ 2. Đọc skill `test-strategy` nếu available.
17
+ 3. Phát hiện stack qua config (`package.json`, `pyproject.toml`, `go.mod`, ...).
18
+ 4. Viết test focused theo behavior, không skip test.
19
+ 5. Chạy test liên quan, đọc lỗi đầy đủ, fix root cause, retest.
20
+ 6. Dừng sau tối đa 5 vòng nếu không fix được, báo root cause và blocker.
21
+
22
+ ## Quy tắc
23
+
24
+ - Fix code khi code sai; chỉ sửa test khi assertion/test setup sai.
25
+ - Không dùng `.skip`, `xit`, comment-out test để pass.
26
+ - Một lần thay đổi, một lần verify.
27
+
28
+ ## Output
29
+
30
+ ```markdown
31
+ ## QA Result
32
+
33
+ PASS | FAIL
34
+
35
+ - Tool:
36
+ - Attempts:
37
+ - Tests added/changed:
38
+ - AC verified:
39
+ - Blocker nếu fail:
40
+ ```
41
+
42
+ Luôn trả lời bằng tiếng Việt.
@@ -0,0 +1,51 @@
1
+ ---
2
+ description: Phân tích yêu cầu phức tạp, đọc tài liệu/codebase, sinh giả thuyết, edge case, rủi ro và đề xuất câu hỏi cho user. Không code.
3
+ mode: subagent
4
+ permission:
5
+ edit: deny
6
+ bash: ask
7
+ ---
8
+
9
+ Bạn là **requirement-analyst** của OpenMoneta Dev Kit trong OpenCode.
10
+
11
+ ## Vai trò
12
+
13
+ Bạn được gọi khi yêu cầu user mơ hồ, lớn, hoặc cần explore rộng. Bạn không edit file. Output là báo cáo phân tích để primary agent quyết định hỏi user hoặc tạo plan.
14
+
15
+ ## Workflow
16
+
17
+ 1. Đọc `docs/INDEX.md` nếu có.
18
+ 2. Đọc `plans/INDEX.md` để kiểm tra plan active/overlap.
19
+ 3. Đọc module README liên quan qua Token Routing.
20
+ 4. Đọc source vừa đủ để hiểu impact, không scan toàn bộ repo.
21
+ 5. Tóm tắt yêu cầu, giả thuyết, edge cases, rủi ro, impact.
22
+ 6. Đề xuất câu hỏi critical để làm rõ yêu cầu. Mục đích: làm rõ yêu cầu người dùng, phân tích tất cả trường hợp có thể xảy ra, phản biện và đề xuất phương án tối ưu hơn nếu có. Đặt đủ câu hỏi cần thiết, không giới hạn số lượng.
23
+
24
+ ## Output
25
+
26
+ ```markdown
27
+ # Requirement Analysis Report
28
+
29
+ ## Yêu cầu
30
+ <diễn đạt lại ngắn>
31
+
32
+ ## Giả thuyết
33
+ - ...
34
+
35
+ ## Edge cases
36
+ - ...
37
+
38
+ ## Rủi ro
39
+ - ...
40
+
41
+ ## Impact
42
+ - `path/module` — High/Medium/Low — ...
43
+
44
+ ## Câu hỏi đề xuất hỏi user
45
+ 1. ...
46
+
47
+ ## Khuyến nghị tiếp theo
48
+ <hỏi user / đủ info để tạo plan / đủ info để code>
49
+ ```
50
+
51
+ Luôn trả lời bằng tiếng Việt, súc tích.
@@ -0,0 +1,46 @@
1
+ ---
2
+ description: ON-DEMAND ONLY. Audit OWASP Top 10, scan secrets, kiểm tra dependency CVE. Read-only, không edit.
3
+ mode: subagent
4
+ permission:
5
+ edit: deny
6
+ bash: ask
7
+ ---
8
+
9
+ Bạn là **security-auditor** của OpenMoneta Dev Kit trong OpenCode.
10
+
11
+ Chỉ dùng khi user yêu cầu security audit hoặc task chạm auth/payment/PII/admin/permission và primary agent muốn audit độc lập.
12
+
13
+ ## Workflow
14
+
15
+ 1. Xác định scope từ `.cursor/.session-changes.json` nếu có, hoặc từ plan active trong `plans/`.
16
+ 2. Audit OWASP Top 10 trong phạm vi file liên quan.
17
+ 3. Scan secrets bằng công cụ read-only (`rg`, `git ls-files`, audit package manager nếu phù hợp).
18
+ 4. Chỉ report HIGH/CRITICAL dependency CVE nếu có.
19
+ 5. Không sửa file.
20
+
21
+ ## Output
22
+
23
+ ```markdown
24
+ # Security Audit Report
25
+
26
+ ## Scope
27
+ - ...
28
+
29
+ ## OWASP Checklist
30
+ - A01 Access Control: Pass/Issue/N/A — ...
31
+ - ...
32
+
33
+ ## Secrets Scan
34
+ - ...
35
+
36
+ ## Dependency CVE
37
+ - ...
38
+
39
+ ## Recommendations
40
+ 1. ...
41
+
42
+ ## Verdict
43
+ PASS | FAIL — <kết luận ngắn>
44
+ ```
45
+
46
+ Luôn trả lời bằng tiếng Việt.
@@ -0,0 +1,43 @@
1
+ ---
2
+ description: ON-DEMAND ONLY. Playwright UI/UX testing multi-viewport, screenshot, loop fix CSS/component khi user yêu cầu UI test.
3
+ mode: subagent
4
+ permission:
5
+ edit: ask
6
+ bash: ask
7
+ ---
8
+
9
+ Bạn là **ui-tester** của OpenMoneta Dev Kit trong OpenCode.
10
+
11
+ Chỉ dùng khi user yêu cầu UI test, visual regression, hoặc test-fix giao diện.
12
+
13
+ ## Workflow
14
+
15
+ 1. Đọc plan active hoặc yêu cầu user để xác định AC UI.
16
+ 2. Kiểm tra Playwright đã cài chưa; nếu chưa, hỏi/đề xuất cài.
17
+ 3. Chạy multi-viewport: mobile, tablet, desktop.
18
+ 4. Lưu screenshot vào `tests/screenshots/<slug>/iter-<n>/`.
19
+ 5. Đọc lỗi/screenshot, fix CSS/component, retest.
20
+ 6. Sau mỗi 2 vòng, tóm tắt thay đổi và hỏi user nếu hướng visual chưa chắc chắn.
21
+ 7. Hard limit 5 vòng trước khi escalate.
22
+
23
+ ## Quy tắc
24
+
25
+ - Screenshot thật, không đoán bằng đọc code.
26
+ - Không sửa test để né lỗi visual thật.
27
+ - Cleanup state tạm khi hoàn tất.
28
+
29
+ ## Output
30
+
31
+ ```markdown
32
+ ## UI Test Result
33
+
34
+ PASS | FAIL
35
+
36
+ - Viewports:
37
+ - Screenshots:
38
+ - Attempts:
39
+ - Issues fixed:
40
+ - Blocker nếu fail:
41
+ ```
42
+
43
+ Luôn trả lời bằng tiếng Việt.
@@ -0,0 +1,389 @@
1
+ import fs from "node:fs"
2
+ import path from "node:path"
3
+
4
+ type ToolArgs = Record<string, unknown>
5
+
6
+ const SOURCE_HINTS = [
7
+ "apps/",
8
+ "packages/",
9
+ "src/",
10
+ "lib/",
11
+ "components/",
12
+ "server/",
13
+ "modules/",
14
+ "pages/",
15
+ "/app/",
16
+ ]
17
+
18
+ const SAFE_READ_HINTS = [
19
+ "docs/",
20
+ "plans/",
21
+ ".cursor/",
22
+ ".opencode/",
23
+ ".github/",
24
+ ".vscode/",
25
+ "AGENTS.md",
26
+ "README",
27
+ "CHANGELOG",
28
+ "LICENSE",
29
+ "package.json",
30
+ "tsconfig",
31
+ "vite.config",
32
+ "playwright.config",
33
+ "biome.config",
34
+ "vitest.config",
35
+ "astro.config",
36
+ ".gitignore",
37
+ "Dockerfile",
38
+ ]
39
+
40
+ function asString(value: unknown): string {
41
+ return typeof value === "string" ? value : ""
42
+ }
43
+
44
+ function projectRoot(ctx: { worktree?: string; directory?: string }): string {
45
+ const wt = ctx.worktree
46
+ if (wt && wt !== "/" && wt !== ".") return wt
47
+ const dir = ctx.directory
48
+ if (dir && dir !== "/" && dir !== ".") return dir
49
+ return process.cwd()
50
+ }
51
+
52
+ function normalize(root: string, filePath: string): string {
53
+ if (!filePath) return ""
54
+ return path.isAbsolute(filePath) ? filePath : path.join(root, filePath)
55
+ }
56
+
57
+ function rel(root: string, filePath: string): string {
58
+ const abs = normalize(root, filePath)
59
+ return path.relative(root, abs).replaceAll(path.sep, "/")
60
+ }
61
+
62
+ function firstPathArg(args: ToolArgs): string {
63
+ return (
64
+ asString(args.path) ||
65
+ asString(args.filePath) ||
66
+ asString(args.file) ||
67
+ asString(args.target) ||
68
+ asString(args.targetFile) ||
69
+ asString(args.target_file)
70
+ )
71
+ }
72
+
73
+ function readTarget(args: ToolArgs): string {
74
+ return [
75
+ firstPathArg(args),
76
+ asString(args.pattern),
77
+ asString(args.glob),
78
+ asString(args.query),
79
+ ]
80
+ .filter(Boolean)
81
+ .join(" ")
82
+ }
83
+
84
+ function bashCommand(args: ToolArgs): string {
85
+ return asString(args.command) || asString(args.cmd) || asString(args.script)
86
+ }
87
+
88
+ function isReadTool(tool: string): boolean {
89
+ return ["read", "glob", "grep", "list"].includes(tool.toLowerCase())
90
+ }
91
+
92
+ function isBashTool(tool: string): boolean {
93
+ return ["bash", "shell", "terminal"].includes(tool.toLowerCase())
94
+ }
95
+
96
+ function isEditTool(tool: string): boolean {
97
+ return ["write", "edit", "apply_patch", "patch"].includes(tool.toLowerCase())
98
+ }
99
+
100
+ function shouldCheckRead(tool: string, target: string): boolean {
101
+ if (!target) return false
102
+ if (SAFE_READ_HINTS.some((hint) => target.includes(hint))) return false
103
+ if (["glob", "grep", "list"].includes(tool.toLowerCase())) return true
104
+ return SOURCE_HINTS.some((hint) => target.includes(hint))
105
+ }
106
+
107
+ function isSourceSearchCommand(command: string): boolean {
108
+ if (!command) return false
109
+ const normalized = command.replaceAll("\\\n", " ")
110
+ if (!/(^|[;&|()\s])(rg|grep|find|fd|ls|tree|cat|sed|awk|head|tail)\b/.test(normalized)) {
111
+ return false
112
+ }
113
+ if (SOURCE_HINTS.some((hint) => normalized.includes(hint))) return true
114
+ return !SAFE_READ_HINTS.some((hint) => normalized.includes(hint))
115
+ }
116
+
117
+ function readsDocsIndexCommand(command: string): boolean {
118
+ if (!command) return false
119
+ return command.includes("docs/INDEX.md")
120
+ }
121
+
122
+ function isSensitivePath(relativePath: string): boolean {
123
+ const lower = relativePath.toLowerCase()
124
+ return (
125
+ lower === ".env" ||
126
+ lower.startsWith(".env.") ||
127
+ [
128
+ "auth",
129
+ "oauth",
130
+ "session",
131
+ "token",
132
+ "secret",
133
+ "credential",
134
+ "security",
135
+ "permission",
136
+ "rbac",
137
+ "role",
138
+ "admin",
139
+ "payment",
140
+ "billing",
141
+ "invoice",
142
+ "subscription",
143
+ "checkout",
144
+ "db",
145
+ "database",
146
+ "schema",
147
+ "migration",
148
+ "prisma",
149
+ "drizzle",
150
+ "sql",
151
+ "deploy",
152
+ "infra",
153
+ ].some((needle) => lower.includes(needle)) ||
154
+ lower.startsWith(".github/workflows/")
155
+ )
156
+ }
157
+
158
+ function isWhitelistedEdit(relativePath: string): boolean {
159
+ if (!relativePath) return true
160
+ if (
161
+ [
162
+ "README.md",
163
+ "README",
164
+ "CHANGELOG.md",
165
+ ".gitignore",
166
+ ".env.example",
167
+ "AGENTS.md",
168
+ "LICENSE",
169
+ "LICENSE.md",
170
+ "LICENSE.txt",
171
+ ].includes(relativePath)
172
+ ) {
173
+ return true
174
+ }
175
+ if (
176
+ relativePath.startsWith("docs/") ||
177
+ relativePath.startsWith("plans/") ||
178
+ relativePath.startsWith(".cursor/") ||
179
+ relativePath.startsWith(".opencode/") ||
180
+ relativePath.startsWith(".vscode/")
181
+ ) {
182
+ return true
183
+ }
184
+ if (relativePath.startsWith(".github/") && !relativePath.startsWith(".github/workflows/")) {
185
+ return true
186
+ }
187
+ if (
188
+ relativePath.endsWith(".md") &&
189
+ !["src/", "lib/", "app/", "pages/", "components/"].some((prefix) =>
190
+ relativePath.startsWith(prefix),
191
+ )
192
+ ) {
193
+ return true
194
+ }
195
+ return false
196
+ }
197
+
198
+ function listPlans(root: string, status: "Draft" | "In Progress"): string[] {
199
+ const plansDir = path.join(root, "plans")
200
+ if (!fs.existsSync(plansDir)) return []
201
+ return fs
202
+ .readdirSync(plansDir)
203
+ .filter((file) => file.endsWith(".md") && file !== "INDEX.md")
204
+ .map((file) => path.join(plansDir, file))
205
+ .filter((file) => {
206
+ const content = fs.readFileSync(file, "utf8")
207
+ return new RegExp(`^\\*{0,2}status\\*{0,2}:?\\s*${status}`, "mi").test(content)
208
+ })
209
+ }
210
+
211
+ function planContainsPath(planPath: string, relativePath: string): boolean {
212
+ const lines = fs.readFileSync(planPath, "utf8").split(/\r?\n/)
213
+ let inFiles = false
214
+ for (const line of lines) {
215
+ if (/^##\s+(Files ảnh hưởng|Files thay đổi|Files changed|Files)\s*$/.test(line)) {
216
+ inFiles = true
217
+ continue
218
+ }
219
+ if (inFiles && /^##\s+/.test(line)) break
220
+ if (inFiles && line.includes(relativePath)) return true
221
+ }
222
+ return false
223
+ }
224
+
225
+ function trackChange(root: string, relativePath: string, tool: string) {
226
+ if (!relativePath) return
227
+ const cursorDir = path.join(root, ".cursor")
228
+ fs.mkdirSync(cursorDir, { recursive: true })
229
+ const trackFile = path.join(cursorDir, ".session-changes.json")
230
+ const payload = fs.existsSync(trackFile)
231
+ ? JSON.parse(fs.readFileSync(trackFile, "utf8"))
232
+ : { started_at: new Date().toISOString(), changes: [] }
233
+ payload.changes.push({ path: relativePath, ts: new Date().toISOString(), tool })
234
+ fs.writeFileSync(trackFile, `${JSON.stringify(payload, null, 2)}\n`)
235
+ }
236
+
237
+ function editedPaths(root: string, args: ToolArgs): string[] {
238
+ const direct = firstPathArg(args)
239
+ if (direct) return [rel(root, direct)]
240
+
241
+ const patch = asString(args.patch) || asString(args.diff)
242
+ if (!patch) return []
243
+
244
+ const paths: string[] = []
245
+ for (const line of patch.split(/\r?\n/)) {
246
+ const match = line.match(/^\*\*\* (?:Add|Update|Delete) File:\s+(.+)$/)
247
+ if (match?.[1]) paths.push(rel(root, match[1].trim()))
248
+ }
249
+ return [...new Set(paths)]
250
+ }
251
+
252
+ function fail(message: string): never {
253
+ throw new Error(`[OpenMoneta Dev Kit]\n${message}`)
254
+ }
255
+
256
+ export const OpenMonetaGuard = async (ctx: { worktree?: string; directory?: string }) => {
257
+ const globalKey = Symbol.for("openmoneta.devkit.guard.loaded")
258
+ const globalState = globalThis as Record<symbol, number>
259
+ globalState[globalKey] = (globalState[globalKey] || 0) + 1
260
+
261
+ const root = projectRoot(ctx)
262
+ const marker = path.join(root, ".cursor", ".docs-index-read")
263
+ const guardLoadedMarker = path.join(root, ".cursor", ".openmoneta-guard-loaded.json")
264
+ const docsIndex = path.join(root, "docs", "INDEX.md")
265
+ const pendingEdits = new Map<string, string[]>()
266
+
267
+ fs.mkdirSync(path.dirname(marker), { recursive: true })
268
+ fs.writeFileSync(
269
+ guardLoadedMarker,
270
+ `${JSON.stringify(
271
+ {
272
+ loaded_at: new Date().toISOString(),
273
+ root,
274
+ version: "1.8.6",
275
+ load_count: globalState[globalKey],
276
+ },
277
+ null,
278
+ 2,
279
+ )}\n`,
280
+ )
281
+
282
+ // OpenCode does not have Cursor's sessionStart hook, so reset the docs-first
283
+ // marker when the plugin is loaded for a new OpenCode process/session.
284
+ if (fs.existsSync(marker)) {
285
+ fs.rmSync(marker, { force: true })
286
+ }
287
+
288
+ return {
289
+ "tool.execute.before": async (
290
+ input: { tool: string; callID?: string },
291
+ output: { args: ToolArgs },
292
+ ) => {
293
+ const tool = input.tool.toLowerCase()
294
+ const args = output.args || {}
295
+
296
+ if (isReadTool(tool) && fs.existsSync(docsIndex)) {
297
+ const target = readTarget(args)
298
+ if (target.includes("docs/INDEX.md") || target.endsWith("docs/INDEX.md")) {
299
+ fs.mkdirSync(path.dirname(marker), { recursive: true })
300
+ fs.writeFileSync(marker, new Date().toISOString())
301
+ return
302
+ }
303
+ if (shouldCheckRead(tool, target) && !fs.existsSync(marker)) {
304
+ fail(
305
+ [
306
+ "Bạn đang đọc/search source code trước khi đọc `docs/INDEX.md`.",
307
+ "",
308
+ "Quy trình đúng:",
309
+ "1. Read `docs/INDEX.md`.",
310
+ "2. Dùng Token Routing để chọn module liên quan.",
311
+ "3. Read module README/source liên quan.",
312
+ ].join("\n"),
313
+ )
314
+ }
315
+ }
316
+
317
+ if (isBashTool(tool) && fs.existsSync(docsIndex)) {
318
+ const command = bashCommand(args)
319
+ if (readsDocsIndexCommand(command)) {
320
+ fs.mkdirSync(path.dirname(marker), { recursive: true })
321
+ fs.writeFileSync(marker, new Date().toISOString())
322
+ return
323
+ }
324
+ if (isSourceSearchCommand(command) && !fs.existsSync(marker)) {
325
+ fail(
326
+ [
327
+ "Bạn đang chạy shell command đọc/search source code trước khi đọc `docs/INDEX.md`.",
328
+ "",
329
+ `Command bị chặn: ${command}`,
330
+ "",
331
+ "Quy trình đúng:",
332
+ "1. Read `docs/INDEX.md` hoặc chạy command đọc `docs/INDEX.md`.",
333
+ "2. Dùng Token Routing để chọn module liên quan.",
334
+ "3. Sau đó mới search source liên quan.",
335
+ ].join("\n"),
336
+ )
337
+ }
338
+ }
339
+
340
+ if (isEditTool(tool)) {
341
+ const paths = editedPaths(root, args)
342
+ for (const relativePath of paths) {
343
+ if (isWhitelistedEdit(relativePath)) continue
344
+
345
+ const draftPlans = listPlans(root, "Draft")
346
+ const activePlans = listPlans(root, "In Progress")
347
+
348
+ if (activePlans.length === 0) {
349
+ if (draftPlans.length > 0) {
350
+ fail(
351
+ `Có plan Draft nhưng chưa được user approve, nên chưa được sửa code \`${relativePath}\`.\n` +
352
+ "Hãy trình plan cho user review, chỉ đổi Status: In Progress sau khi user approve.",
353
+ )
354
+ }
355
+ if (isSensitivePath(relativePath)) {
356
+ fail(
357
+ `File \`${relativePath}\` thuộc nhóm nhạy cảm/rủi ro cao nhưng chưa có plan được user approve.\n` +
358
+ "Tạo repo plan Status: Draft, trình user review, rồi đổi Status: In Progress sau khi approve.",
359
+ )
360
+ }
361
+ continue
362
+ }
363
+
364
+ if (!activePlans.some((plan) => planContainsPath(plan, relativePath))) {
365
+ fail(
366
+ `File \`${relativePath}\` không nằm trong section Files của plan In Progress.\n` +
367
+ "Update plan scope trước khi sửa file mới.",
368
+ )
369
+ }
370
+ }
371
+ if (input.callID && paths.length > 0) {
372
+ pendingEdits.set(input.callID, paths)
373
+ }
374
+ }
375
+ },
376
+
377
+ "tool.execute.after": async (input: { tool: string; callID?: string }) => {
378
+ const tool = input.tool.toLowerCase()
379
+ if (!isEditTool(tool)) return
380
+ const paths = input.callID ? pendingEdits.get(input.callID) || [] : []
381
+ if (input.callID) pendingEdits.delete(input.callID)
382
+ for (const relativePath of paths) {
383
+ trackChange(root, relativePath, input.tool)
384
+ }
385
+ },
386
+ }
387
+ }
388
+
389
+ export default OpenMonetaGuard
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "openmoneta-dev-kit",
3
+ "version": "1.9.0",
4
+ "description": "OpenMoneta Dev Kit — Biến Cursor IDE / OpenCode thành team developer hoàn chỉnh với quy trình 6 bước, adaptive planning, hooks enforcement, và token-aware doc routing",
5
+ "keywords": [
6
+ "cursor",
7
+ "opencode",
8
+ "ai",
9
+ "developer-tools",
10
+ "workflow",
11
+ "vietnamese"
12
+ ],
13
+ "homepage": "https://github.com/rapperkey/OpenMoneta-Dev-Kit",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/rapperkey/OpenMoneta-Dev-Kit.git"
17
+ },
18
+ "license": "MIT",
19
+ "author": "OpenMoneta",
20
+ "bin": {
21
+ "openmoneta": "bin/openmoneta.js"
22
+ },
23
+ "files": [
24
+ "bin/",
25
+ "src/",
26
+ "templates/",
27
+ "skills/",
28
+ "agents/",
29
+ "hooks/",
30
+ "scripts/",
31
+ "opencode/",
32
+ "hooks.json"
33
+ ],
34
+ "engines": {
35
+ "node": ">=16"
36
+ },
37
+ "os": [
38
+ "darwin",
39
+ "linux"
40
+ ]
41
+ }