openmoneta-dev-kit 1.10.4 → 1.11.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/VERSION CHANGED
@@ -1 +1 @@
1
- 1.10.4
1
+ 1.11.0
@@ -6,18 +6,31 @@
6
6
 
7
7
  1. Luôn trả lời bằng tiếng Việt.
8
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.
9
+ 3. Dùng OpenMoneta workflow 6 bước (xem bên dưới).
10
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
11
  5. Nếu có plan `In Progress`, chỉ sửa file trong `## Files ảnh hưởng` / `## Files thay đổi`.
12
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
13
 
14
+ ## Quy trình 6 bước
15
+
16
+ - **B1 Phân tích** (skill `requirement-analysis`): Read `docs/INDEX.md` → match Token Routing → đọc README module liên quan → đọc source → hỏi clarify nếu yêu cầu chưa rõ (ghi vào `## Hiểu yêu cầu` của plan).
17
+ - **B2 Thiết kế** (skill `module-architect`): SRP 1 module 1 trách nhiệm. Feature có concept riêng → module mới (KHÔNG nhét utils/common/shared). **Module có source nhưng chưa có doc → BẮT BUỘC backfill `docs/modules/<slug>/README.md` 3 sections (Trách nhiệm + Public API + Dependencies)** trước khi sang B4. Module mới → thêm keyword vào Token Routing của `docs/INDEX.md`.
18
+ - **B3 Adaptive Plan** (skill `plan-writer`): task nhỏ/rõ → không cần plan. Task lớn/rủi ro/mơ hồ → tạo repo plan `Status: Draft`, trình user review, chỉ code sau khi user approve và đổi `Status: In Progress`.
19
+ - **B4 Triển khai**: có plan active → chỉ edit file trong scope plan; không plan → chỉ sửa đúng yêu cầu. Mỗi sub-task xong → tick `- [x]`.
20
+ - **B5 Update doc + close plan** — **mandatory closing checklist** (plugin guard `event:session.idle` sẽ re-prompt nếu thiếu):
21
+ - [ ] Sync `docs/modules/<slug>/README.md` cho mọi module bị sửa: **Trách nhiệm + Public API + Dependencies**.
22
+ - [ ] Module mới đã có trong bảng "Modules hiện có" + "Token Routing" của `docs/INDEX.md`.
23
+ - [ ] Plan → `Status: Done` + tick mọi checkbox.
24
+ - [ ] **AUTO-ARCHIVE**: `git mv plans/<file>.md plans/archive/<file>.md` + update `plans/INDEX.md` (Active → Archived).
25
+ - **B6 Pre-push Sync + Safe Push** (skill `safe-push`, CONDITIONAL): chỉ khi user yêu cầu push. `git fetch` + rebase trước push, conflict logic → hỏi user, `git push` thường, KHÔNG `--force` shared branch.
26
+
14
27
  ## OpenCode Notes
15
28
 
16
29
  - OpenCode đọc global rules từ `~/.config/opencode/AGENTS.md`.
17
30
  - OpenCode đọc project rules từ `AGENTS.md` ở project root. Project rules là source of truth cho từng repo.
18
31
  - 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
32
  - 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.
33
+ - OpenCode plugin guard trong `~/.config/opencode/plugins/openmoneta-guard.ts` enforce workflow tương đương Cursor hooks: `tool.execute.before` (docs-first + plan gate), `tool.execute.after` (track changes), và `event:session.idle` (verify B2/B5: module README + close plan, re-prompt nhắc hoàn tất với loop guard ≤4 lần).
21
34
 
22
35
  ## Khi Bắt Đầu Task
23
36
 
@@ -253,11 +253,174 @@ function fail(message: string): never {
253
253
  throw new Error(`[OpenMoneta Dev Kit]\n${message}`)
254
254
  }
255
255
 
256
- export const OpenMonetaGuard = async (ctx: { worktree?: string; directory?: string }) => {
256
+ // === verify-completion parity (port từ Cursor hook verify-completion.sh) ===
257
+
258
+ const VERIFY_LOOP_LIMIT = 4
259
+ const NON_CODE_CHANGE = /^(docs\/|plans\/|README|\.gitignore|\.env\.example|AGENTS\.md|\.cursor\/)/
260
+
261
+ function readSessionChanges(root: string): string[] {
262
+ const file = path.join(root, ".cursor", ".session-changes.json")
263
+ if (!fs.existsSync(file)) return []
264
+ try {
265
+ const data = JSON.parse(fs.readFileSync(file, "utf8")) as { changes?: { path?: string }[] }
266
+ return (data.changes || []).map((c) => asString(c.path)).filter(Boolean)
267
+ } catch {
268
+ return []
269
+ }
270
+ }
271
+
272
+ function clearSessionChanges(root: string) {
273
+ fs.rmSync(path.join(root, ".cursor", ".session-changes.json"), { force: true })
274
+ }
275
+
276
+ // Port logic infer module slug từ scripts/list-affected-modules.sh
277
+ function inferModules(paths: string[]): { slug: string; docsPath: string }[] {
278
+ const out = new Map<string, string>()
279
+ for (const p of paths) {
280
+ if (!p) continue
281
+ if (/^(docs|plans|tests|scripts|infra|logs|\.cursor|node_modules)\//.test(p)) continue
282
+ if (["AGENTS.md", "CHANGELOG.md", "README.md", ".gitignore", ".env.example"].includes(p)) continue
283
+ const seg = p.split("/")
284
+ if (p.startsWith("apps/") && seg.length >= 2) {
285
+ out.set(`apps-${seg[1]}`, `docs/modules/apps-${seg[1]}`)
286
+ } else if (p.startsWith("packages/") && seg.length >= 2) {
287
+ out.set(`packages-${seg[1]}`, `docs/modules/packages-${seg[1]}`)
288
+ } else if (p.startsWith("src/modules/") && seg.length >= 3) {
289
+ out.set(seg[2], `docs/modules/${seg[2]}`)
290
+ } else if (p.startsWith("src/") && seg.length >= 2 && seg[1] !== "modules") {
291
+ out.set(seg[1], `docs/modules/${seg[1]}`)
292
+ }
293
+ }
294
+ return [...out.entries()].map(([slug, docsPath]) => ({ slug, docsPath }))
295
+ }
296
+
297
+ function planUntickedCount(planPath: string): number {
298
+ const match = fs.readFileSync(planPath, "utf8").match(/^- \[ \]/gm)
299
+ return match ? match.length : 0
300
+ }
301
+
302
+ // Check 9: có heading "## Hiểu yêu cầu" + nội dung không trống (bỏ dòng blockquote `>`)
303
+ function planHasUnderstanding(planPath: string): boolean {
304
+ const lines = fs.readFileSync(planPath, "utf8").split(/\r?\n/)
305
+ let inSection = false
306
+ let foundHeading = false
307
+ let hasContent = false
308
+ for (const line of lines) {
309
+ if (/^##\s+Hiểu yêu cầu/.test(line)) {
310
+ foundHeading = true
311
+ inSection = true
312
+ continue
313
+ }
314
+ if (inSection && /^##\s+/.test(line)) break
315
+ if (inSection) {
316
+ const trimmed = line.trim()
317
+ if (trimmed && !trimmed.startsWith(">")) hasContent = true
318
+ }
319
+ }
320
+ return foundHeading && hasContent
321
+ }
322
+
323
+ // Build danh sách issue tương đương verify-completion Check 5/6/9.
324
+ function buildVerifyIssues(root: string): string[] {
325
+ const paths = readSessionChanges(root)
326
+ if (paths.length === 0) return []
327
+ if (!paths.some((p) => !NON_CODE_CHANGE.test(p))) return []
328
+
329
+ const issues: string[] = []
330
+
331
+ const draftPlans = listPlans(root, "Draft")
332
+ const inProgressPlans = listPlans(root, "In Progress")
333
+ const activePlans = [...draftPlans, ...inProgressPlans]
334
+
335
+ // Check 5: plan active phải Done + hết checkbox
336
+ for (const plan of activePlans) {
337
+ const name = path.basename(plan)
338
+ const unticked = planUntickedCount(plan)
339
+ if (unticked > 0) {
340
+ issues.push(`❌ Plan '${name}' còn ${unticked} checkbox '- [ ]' chưa tick. Hoàn thành các task (Bước 5).`)
341
+ }
342
+ if (draftPlans.includes(plan)) {
343
+ issues.push(
344
+ `❌ Plan '${name}' vẫn ở trạng thái Draft (chưa được user approve) nhưng có code change gắn vào.`,
345
+ )
346
+ } else {
347
+ issues.push(
348
+ `❌ Plan '${name}' vẫn In Progress. Đổi Status: Done + git mv plans/${name} sang plans/archive/ (Bước 5).`,
349
+ )
350
+ }
351
+ }
352
+
353
+ // Check 9: plan active + plan sửa trong session phải có "## Hiểu yêu cầu" không trống
354
+ const planSet = new Set<string>(activePlans)
355
+ for (const rel of paths) {
356
+ if (/^plans\/.*\.md$/.test(rel) && rel !== "plans/INDEX.md") {
357
+ const abs = path.join(root, rel)
358
+ if (fs.existsSync(abs)) planSet.add(abs)
359
+ }
360
+ }
361
+ for (const plan of planSet) {
362
+ if (!planHasUnderstanding(plan)) {
363
+ issues.push(
364
+ `❌ Plan '${path.basename(plan)}' thiếu/trống section '## Hiểu yêu cầu' (audit clarify Bước 1).`,
365
+ )
366
+ }
367
+ }
368
+
369
+ // Check 6: module bị sửa phải có docs/modules/<slug>/README.md
370
+ for (const { slug, docsPath } of inferModules(paths)) {
371
+ if (!fs.existsSync(path.join(root, docsPath, "README.md"))) {
372
+ issues.push(
373
+ `❌ Module '${slug}' bị sửa nhưng thiếu '${docsPath}/README.md'. Tạo README 3 sections (Trách nhiệm + Public API + Dependencies) + cập nhật docs/INDEX.md (Bước 2/5).`,
374
+ )
375
+ }
376
+ }
377
+
378
+ return issues
379
+ }
380
+
381
+ type VerifyLoopState = { sessionID: string; count: number }
382
+
383
+ function readVerifyLoop(root: string): VerifyLoopState {
384
+ const file = path.join(root, ".cursor", ".openmoneta-verify-loop.json")
385
+ if (!fs.existsSync(file)) return { sessionID: "", count: 0 }
386
+ try {
387
+ return JSON.parse(fs.readFileSync(file, "utf8")) as VerifyLoopState
388
+ } catch {
389
+ return { sessionID: "", count: 0 }
390
+ }
391
+ }
392
+
393
+ function writeVerifyLoop(root: string, state: VerifyLoopState) {
394
+ const file = path.join(root, ".cursor", ".openmoneta-verify-loop.json")
395
+ fs.mkdirSync(path.dirname(file), { recursive: true })
396
+ fs.writeFileSync(file, `${JSON.stringify(state, null, 2)}\n`)
397
+ }
398
+
399
+ function clearVerifyLoop(root: string) {
400
+ fs.rmSync(path.join(root, ".cursor", ".openmoneta-verify-loop.json"), { force: true })
401
+ }
402
+
403
+ type SessionPromptClient = {
404
+ session?: {
405
+ prompt?: (input: {
406
+ path: { id: string }
407
+ body: { parts: { type: string; text: string }[] }
408
+ }) => Promise<unknown>
409
+ }
410
+ }
411
+
412
+ type GuardContext = {
413
+ worktree?: string
414
+ directory?: string
415
+ client?: SessionPromptClient
416
+ }
417
+
418
+ export const OpenMonetaGuard = async (ctx: GuardContext) => {
257
419
  const globalKey = Symbol.for("openmoneta.devkit.guard.loaded")
258
420
  const globalState = globalThis as Record<symbol, number>
259
421
  globalState[globalKey] = (globalState[globalKey] || 0) + 1
260
422
 
423
+ const client = ctx.client
261
424
  const root = projectRoot(ctx)
262
425
  const marker = path.join(root, ".cursor", ".docs-index-read")
263
426
  const guardLoadedMarker = path.join(root, ".cursor", ".openmoneta-guard-loaded.json")
@@ -271,7 +434,7 @@ export const OpenMonetaGuard = async (ctx: { worktree?: string; directory?: stri
271
434
  {
272
435
  loaded_at: new Date().toISOString(),
273
436
  root,
274
- version: "1.8.6",
437
+ version: "1.11.0",
275
438
  load_count: globalState[globalKey],
276
439
  },
277
440
  null,
@@ -383,6 +546,62 @@ export const OpenMonetaGuard = async (ctx: { worktree?: string; directory?: stri
383
546
  trackChange(root, relativePath, input.tool)
384
547
  }
385
548
  },
549
+
550
+ // Parity với Cursor hook `stop`/verify-completion. OpenCode `event` không block
551
+ // được như Cursor, nên khi phiên idle mà còn thiếu B2/B5 ta re-prompt lại session
552
+ // để buộc Agent hoàn tất, kèm loop guard chống lặp vô hạn.
553
+ event: async ({
554
+ event,
555
+ }: {
556
+ event: { type: string; properties?: Record<string, unknown> }
557
+ }) => {
558
+ if (event.type !== "session.idle") return
559
+ try {
560
+ const issues = buildVerifyIssues(root)
561
+ const sessionID = asString(event.properties?.sessionID)
562
+
563
+ if (issues.length === 0) {
564
+ clearSessionChanges(root)
565
+ clearVerifyLoop(root)
566
+ return
567
+ }
568
+
569
+ const loop = readVerifyLoop(root)
570
+ const count = loop.sessionID === sessionID ? loop.count : 0
571
+
572
+ if (count >= VERIFY_LOOP_LIMIT) {
573
+ console.warn(
574
+ `[OpenMoneta Dev Kit] verify-completion: đã nhắc ${count} lần, graceful exit. ` +
575
+ `Hãy review thủ công:\n${issues.join("\n")}`,
576
+ )
577
+ return
578
+ }
579
+
580
+ if (!client?.session?.prompt || !sessionID) {
581
+ console.warn(
582
+ `[OpenMoneta Dev Kit] verify-completion còn ${issues.length} việc nhưng không re-prompt được ` +
583
+ `(thiếu client/sessionID):\n${issues.join("\n")}`,
584
+ )
585
+ return
586
+ }
587
+
588
+ const message = [
589
+ "[OpenMoneta Dev Kit — verify-completion] Phiên chưa nên kết thúc, còn thiếu Bước 2/5:",
590
+ "",
591
+ ...issues,
592
+ "",
593
+ `Hoàn tất các mục trên rồi mới kết thúc. (Còn ${VERIFY_LOOP_LIMIT - count - 1} lần nhắc nữa.)`,
594
+ ].join("\n")
595
+
596
+ await client.session.prompt({
597
+ path: { id: sessionID },
598
+ body: { parts: [{ type: "text", text: message }] },
599
+ })
600
+ writeVerifyLoop(root, { sessionID, count: count + 1 })
601
+ } catch (err) {
602
+ console.warn(`[OpenMoneta Dev Kit] verify-completion event error: ${String(err)}`)
603
+ }
604
+ },
386
605
  }
387
606
  }
388
607
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openmoneta-dev-kit",
3
- "version": "1.10.4",
3
+ "version": "1.11.0",
4
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
5
  "keywords": [
6
6
  "cursor",
@@ -40,16 +40,17 @@ async function run(args) {
40
40
  const pkgRoot = getPkgRoot()
41
41
 
42
42
  try {
43
- if (!isWindows()) {
44
- execSync("git rev-parse --is-inside-work-tree", { stdio: "pipe", cwd: pkgRoot })
45
- const branch = execSync("git rev-parse --abbrev-ref HEAD", { encoding: "utf8", cwd: pkgRoot }).trim()
46
- console.log(`\n ▶ Git pull (branch: ${branch})...`)
47
- execSync(`git pull origin "${branch}"`, { stdio: "inherit", cwd: pkgRoot })
48
- } else {
49
- execSync("powershell.exe -Command \"& { git pull }\"", { stdio: "inherit", cwd: pkgRoot, shell: true })
50
- }
43
+ execSync("git rev-parse --is-inside-work-tree", { stdio: "pipe", cwd: pkgRoot })
44
+ const branch = execSync("git rev-parse --abbrev-ref HEAD", { encoding: "utf8", cwd: pkgRoot }).trim()
45
+ console.log(`\n ▶ Git pull (branch: ${branch})...`)
46
+ execSync(`git pull origin "${branch}"`, { stdio: "inherit", cwd: pkgRoot })
51
47
  } catch {
52
- console.warn(` ⚠ Không thể git pull, dùng source hiện tại.`)
48
+ try {
49
+ console.log(`\n ▶ NPM update: openmoneta-dev-kit@latest...`)
50
+ execSync(`npm install -g openmoneta-dev-kit@latest`, { stdio: "inherit" })
51
+ } catch {
52
+ console.warn(` ⚠ Không thể cập nhật source (git/npm). Dùng source hiện tại.`)
53
+ }
53
54
  }
54
55
 
55
56
  if (isInstalled("cursor")) {
@@ -54,7 +54,7 @@
54
54
 
55
55
  ### Bước 5 — Update doc + close plan
56
56
 
57
- > Skill: `module-architect` (đã merge `doc-maintainer`). Hook `verify-completion` Check 6 chặn nếu module README thiếu.
57
+ > Skill: `module-architect` (đã merge `doc-maintainer`). Cursor: hook `verify-completion` Check 6 chặn kết thúc nếu module README thiếu. OpenCode: plugin guard `event:session.idle` re-prompt nhắc hoàn tất (loop guard ≤4 lần).
58
58
 
59
59
  - Sync `docs/modules/<slug>/README.md` (Public API + Dependencies) nếu API đổi.
60
60
  - Module mới → đảm bảo đã có trong bảng "Modules hiện có" + "Token Routing" của `docs/INDEX.md`.
@@ -91,11 +91,13 @@
91
91
 
92
92
  ## Hooks enforce
93
93
 
94
+ > Cursor dùng bash hooks (`~/.cursor/hooks/`). OpenCode dùng plugin guard `openmoneta-guard.ts` — `tool.execute.before/after` tương đương 2 hook đầu, `event:session.idle` tương đương `verify-completion` (re-prompt thay vì block cứng).
95
+
94
96
  | Hook | Khi nào | Hậu quả |
95
97
  |---|---|---|
96
98
  | `enforce-docs-first` | preToolUse Read/Glob/Grep | BLOCK source code đọc nếu chưa Read `docs/INDEX.md` |
97
99
  | `check-plan-exists` | preToolUse Write/StrReplace/EditNotebook/Delete | ALLOW task thường không plan; BLOCK file nhạy cảm không plan, plan Draft, hoặc file ngoài scope plan |
98
- | `verify-completion` | stop | BLOCK kết thúc nếu Check 5/6/9 fail (plan Done + checkbox, module README, "Hiểu yêu cầu") |
100
+ | `verify-completion` | stop (Cursor) / `session.idle` (OpenCode) | Cursor BLOCK kết thúc nếu Check 5/6/9 fail; OpenCode re-prompt nhắc hoàn tất (plan Done + checkbox, module README, "Hiểu yêu cầu"), loop guard ≤4 lần |
99
101
 
100
102
  ## Override cho dự án này
101
103