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.
- package/README.md +103 -0
- package/agents/qa-autonomous.md +131 -0
- package/agents/requirement-analyst.md +98 -0
- package/agents/security-auditor.md +120 -0
- package/agents/ui-tester.md +186 -0
- package/bin/openmoneta.js +11 -0
- package/hooks/check-plan-exists.sh +154 -0
- package/hooks/enforce-docs-first.sh +169 -0
- package/hooks/inject-process-context.sh +117 -0
- package/hooks/track-changes.sh +46 -0
- package/hooks/verify-completion.sh +165 -0
- package/hooks.json +30 -0
- package/opencode/AGENTS.md.tpl +38 -0
- package/opencode/agents/qa-autonomous.md +42 -0
- package/opencode/agents/requirement-analyst.md +51 -0
- package/opencode/agents/security-auditor.md +46 -0
- package/opencode/agents/ui-tester.md +43 -0
- package/opencode/plugins/openmoneta-guard.ts +389 -0
- package/package.json +41 -0
- package/scripts/debug-hooks.sh +54 -0
- package/scripts/init-project.sh +438 -0
- package/scripts/list-affected-modules.sh +74 -0
- package/skills/auth-bypass-testing/SKILL.md +236 -0
- package/skills/automated-testing/SKILL.md +162 -0
- package/skills/automated-testing/scripts/install-playwright.sh +134 -0
- package/skills/module-architect/SKILL.md +256 -0
- package/skills/plan-writer/SKILL.md +229 -0
- package/skills/requirement-analysis/SKILL.md +163 -0
- package/skills/safe-push/SKILL.md +182 -0
- package/skills/security-checklist/SKILL.md +116 -0
- package/skills/test-strategy/SKILL.md +135 -0
- package/skills/ui-test-loop/SKILL.md +161 -0
- package/src/cli.js +63 -0
- package/src/commands/check.js +30 -0
- package/src/commands/init.js +43 -0
- package/src/commands/install.js +50 -0
- package/src/commands/uninstall.js +74 -0
- package/src/commands/update.js +81 -0
- package/src/lib/paths.js +46 -0
- package/src/lib/version.js +45 -0
- package/templates/AGENTS.md.tpl +106 -0
- package/templates/docs-INDEX.md.tpl +62 -0
- package/templates/env.test.tpl +16 -0
- package/templates/karpathy-reference.md +49 -0
- package/templates/plans-INDEX.md.tpl +38 -0
- 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
|
+
}
|