aisef 0.1.0__py3-none-any.whl

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 (89) hide show
  1. aisdlc/__init__.py +0 -0
  2. aisdlc/cli/__init__.py +101 -0
  3. aisdlc/cli/__main__.py +9 -0
  4. aisdlc/cli/_common.py +72 -0
  5. aisdlc/cli/doctor.py +286 -0
  6. aisdlc/cli/harness.py +270 -0
  7. aisdlc/cli/implement.py +552 -0
  8. aisdlc/cli/parser.py +248 -0
  9. aisdlc/cli/plan.py +280 -0
  10. aisdlc/clients/__init__.py +0 -0
  11. aisdlc/clients/base.py +156 -0
  12. aisdlc/clients/claude_code.py +151 -0
  13. aisdlc/clients/compile.py +250 -0
  14. aisdlc/clients/opencode.py +162 -0
  15. aisdlc/clients/stream.py +340 -0
  16. aisdlc/config.py +363 -0
  17. aisdlc/control/__init__.py +0 -0
  18. aisdlc/control/acceptance.py +41 -0
  19. aisdlc/control/approvals.py +367 -0
  20. aisdlc/control/bmad_status.py +181 -0
  21. aisdlc/control/change.py +138 -0
  22. aisdlc/control/complexity.py +533 -0
  23. aisdlc/control/conformance.py +178 -0
  24. aisdlc/control/design_contract.py +218 -0
  25. aisdlc/control/experience.py +214 -0
  26. aisdlc/control/gate.py +800 -0
  27. aisdlc/control/impact.py +364 -0
  28. aisdlc/control/journal.py +356 -0
  29. aisdlc/control/ledger.py +686 -0
  30. aisdlc/control/machine_gate.py +318 -0
  31. aisdlc/control/normalize.py +696 -0
  32. aisdlc/control/outcome.py +116 -0
  33. aisdlc/control/preflight.py +741 -0
  34. aisdlc/control/replay.py +121 -0
  35. aisdlc/control/scheduler.py +199 -0
  36. aisdlc/control/security.py +133 -0
  37. aisdlc/control/state.py +332 -0
  38. aisdlc/control/tdd.py +84 -0
  39. aisdlc/control/worktree.py +392 -0
  40. aisdlc/harness/__init__.py +0 -0
  41. aisdlc/harness/aria.py +159 -0
  42. aisdlc/harness/assets/render.mjs +122 -0
  43. aisdlc/harness/browser.py +152 -0
  44. aisdlc/harness/context.py +306 -0
  45. aisdlc/harness/guardrails.py +821 -0
  46. aisdlc/harness/mockup_map.py +164 -0
  47. aisdlc/harness/mockup_verify.py +297 -0
  48. aisdlc/harness/observe.py +320 -0
  49. aisdlc/harness/prompts.py +116 -0
  50. aisdlc/harness/routing.py +146 -0
  51. aisdlc/harness/sandbox.py +489 -0
  52. aisdlc/harness/testlog.py +245 -0
  53. aisdlc/harness/tools.py +373 -0
  54. aisdlc/kit/__init__.py +0 -0
  55. aisdlc/kit/catalog.json +89 -0
  56. aisdlc/kit/catalog.py +122 -0
  57. aisdlc/kit/constitution.py +144 -0
  58. aisdlc/kit/detect_stack.py +160 -0
  59. aisdlc/kit/docs.py +102 -0
  60. aisdlc/kit/fetch.py +128 -0
  61. aisdlc/kit/install.py +218 -0
  62. aisdlc/kit/prompts/devsecops.md +50 -0
  63. aisdlc/kit/prompts/mockup-screen.md +27 -0
  64. aisdlc/kit/prompts/story-implement.md +112 -0
  65. aisdlc/kit/prompts/story-review.md +120 -0
  66. aisdlc/kit/prompts/story-security-review.md +127 -0
  67. aisdlc/kit/registry.py +434 -0
  68. aisdlc/kit/router.py +204 -0
  69. aisdlc/kit/rules/must_never.md +40 -0
  70. aisdlc/kit/rules/principles.md +64 -0
  71. aisdlc/kit/security_filter.py +264 -0
  72. aisdlc/kit/skill_scan.py +225 -0
  73. aisdlc/kit/skills/aisdlc-mockup-html/SKILL.md +125 -0
  74. aisdlc/kit/skills.py +132 -0
  75. aisdlc/phases/__init__.py +1 -0
  76. aisdlc/phases/deploy.py +486 -0
  77. aisdlc/phases/implement.py +2020 -0
  78. aisdlc/phases/improve.py +524 -0
  79. aisdlc/phases/mockup.py +302 -0
  80. aisdlc/phases/plan.py +437 -0
  81. aisdlc/phases/qa.py +455 -0
  82. aisdlc/phases/report.py +430 -0
  83. aisdlc/phases/run.py +525 -0
  84. aisdlc/phases/story_split.py +362 -0
  85. aisef-0.1.0.dist-info/METADATA +282 -0
  86. aisef-0.1.0.dist-info/RECORD +89 -0
  87. aisef-0.1.0.dist-info/WHEEL +5 -0
  88. aisef-0.1.0.dist-info/entry_points.txt +2 -0
  89. aisef-0.1.0.dist-info/top_level.txt +1 -0
aisdlc/__init__.py ADDED
File without changes
aisdlc/cli/__init__.py ADDED
@@ -0,0 +1,101 @@
1
+ """Giao diện dòng lệnh.
2
+
3
+ Mọi lệnh đều **gọi-một-lần**: đọc trạng thái trên đĩa, làm việc, ghi lại,
4
+ rồi thoát. Không lệnh nào giả định mình là tiến trình chính, không lệnh
5
+ nào chạy nền. Đó là điều kiện để cùng bộ lệnh này dùng được ở hai chế độ
6
+ (quyết định Đ2):
7
+
8
+ * **driver-led** — script hoặc CI gọi ``aisdlc run``;
9
+ * **agent-led** — chính agent gọi ``aisdlc next`` / ``verify`` / ``complete``
10
+ qua Bash, ngay trong phiên chat của Claude Desktop hay OpenCode.
11
+
12
+ Quy ước mã thoát: ``0`` thành công · ``1`` lỗi dùng sai · ``2`` trạng thái
13
+ chưa đạt (cổng chưa duyệt, doctor không đạt) — để CI phân biệt được
14
+ "hỏng" với "chưa xong".
15
+
16
+ Bố cục gói (chia theo pha, không đổi hành vi):
17
+
18
+ * :mod:`._common` — mã thoát, gốc artifact, kho duyệt/trạng thái, adapter;
19
+ * :mod:`.doctor` — lệnh ``doctor``;
20
+ * :mod:`.plan` — cổng người + pha lập kế hoạch;
21
+ * :mod:`.implement` — pha hiện thực và nghiệm thu;
22
+ * :mod:`.harness` — dựng và vận hành harness;
23
+ * :mod:`.parser` — bộ phân tích tham số và ``main``.
24
+
25
+ Mọi tên module-level cũ được xuất lại ở đây, nên ``from aisdlc.cli import
26
+ main`` và bạn bè vẫn chạy y như khi còn là một file.
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ from ._common import (
32
+ ARTIFACT_ROOT,
33
+ _GATE_MARK,
34
+ EXIT_NOT_READY,
35
+ EXIT_OK,
36
+ EXIT_USAGE,
37
+ _approvals,
38
+ _artifact_root,
39
+ _client,
40
+ _gate_arg,
41
+ _state,
42
+ )
43
+ from .doctor import _hook_paths_elsewhere, cmd_doctor
44
+ from .harness import cmd_compile, cmd_doc, cmd_guard, cmd_init, cmd_setup, cmd_skill
45
+ from .implement import (
46
+ _project_has_ui,
47
+ cmd_devsecops,
48
+ cmd_evidence,
49
+ cmd_predeploy,
50
+ cmd_qa,
51
+ cmd_report,
52
+ cmd_run,
53
+ cmd_status,
54
+ cmd_tool,
55
+ cmd_verify,
56
+ )
57
+ from .parser import build_parser, main
58
+ from .plan import (
59
+ _preflight_lines,
60
+ cmd_approve,
61
+ cmd_auto_approve,
62
+ cmd_change,
63
+ cmd_gates,
64
+ cmd_mockup,
65
+ cmd_plan,
66
+ cmd_reject,
67
+ cmd_review,
68
+ )
69
+
70
+ __all__ = [
71
+ "ARTIFACT_ROOT",
72
+ "EXIT_NOT_READY",
73
+ "EXIT_OK",
74
+ "EXIT_USAGE",
75
+ "build_parser",
76
+ "cmd_approve",
77
+ "cmd_auto_approve",
78
+ "cmd_change",
79
+ "cmd_compile",
80
+ "cmd_devsecops",
81
+ "cmd_doc",
82
+ "cmd_doctor",
83
+ "cmd_evidence",
84
+ "cmd_gates",
85
+ "cmd_guard",
86
+ "cmd_init",
87
+ "cmd_mockup",
88
+ "cmd_plan",
89
+ "cmd_predeploy",
90
+ "cmd_qa",
91
+ "cmd_reject",
92
+ "cmd_report",
93
+ "cmd_review",
94
+ "cmd_run",
95
+ "cmd_setup",
96
+ "cmd_skill",
97
+ "cmd_status",
98
+ "cmd_tool",
99
+ "cmd_verify",
100
+ "main",
101
+ ]
aisdlc/cli/__main__.py ADDED
@@ -0,0 +1,9 @@
1
+ """Giữ nguyên `python -m aisdlc.cli` — trước khi tách, `cli.py` có khối
2
+ ``if __name__ == "__main__"`` làm đúng việc này."""
3
+
4
+ import sys
5
+
6
+ from . import main
7
+
8
+ if __name__ == "__main__":
9
+ sys.exit(main())
aisdlc/cli/_common.py ADDED
@@ -0,0 +1,72 @@
1
+ """Mảnh dùng chung của bộ lệnh: mã thoát, gốc artifact, kho duyệt/trạng
2
+ thái, adapter client, và bộ chuyển tham số tên cổng.
3
+
4
+ Mọi module lệnh khác nhập từ đây, nên file này không được nhập ngược lại
5
+ chúng — giữ đồ thị nhập một chiều.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ from ..control.approvals import GATE_ORDER, ApprovalStore, Gate, Status
15
+ from ..control.state import StateStore
16
+
17
+
18
+ ARTIFACT_ROOT = "_bmad-output"
19
+
20
+ EXIT_OK = 0
21
+ EXIT_USAGE = 1
22
+ EXIT_NOT_READY = 2
23
+
24
+ #: Ký hiệu trạng thái cổng, đủ để đọc lướt.
25
+ _GATE_MARK = {
26
+ Status.APPROVED: "✅",
27
+ Status.PENDING: "⏳",
28
+ Status.CHANGES_REQUESTED: "✗",
29
+ Status.STALE: "⚠️",
30
+ }
31
+
32
+
33
+ def _artifact_root(args) -> Path:
34
+ """Gốc artifact — **một** gốc cho cả dự án (bất biến 2).
35
+
36
+ Agent chạy trong worktree của story, nên `--project .` ở đó trỏ vào
37
+ worktree. Ghi bằng chứng vào đấy thì cổng đọc ở gốc chính không thấy
38
+ gì, và story "chưa từng chạy test" dù nó vừa chạy.
39
+ """
40
+ from ..control.worktree import main_repo
41
+
42
+ return main_repo(args.project) / ARTIFACT_ROOT
43
+
44
+
45
+ def _approvals(args) -> ApprovalStore:
46
+ return ApprovalStore(_artifact_root(args))
47
+
48
+
49
+ def _state(args) -> StateStore:
50
+ return StateStore(_artifact_root(args))
51
+
52
+
53
+ def _gate_arg(value: str) -> Gate:
54
+ try:
55
+ return Gate(value)
56
+ except ValueError:
57
+ valid = ", ".join(g.value for g in GATE_ORDER)
58
+ raise argparse.ArgumentTypeError(f"cổng không hợp lệ: {value}. Hợp lệ: {valid}")
59
+
60
+
61
+ def _client(args):
62
+ """Adapter đã kiểm tên và tình trạng cài đặt. None nếu không dùng được."""
63
+ from ..clients.compile import ADAPTERS
64
+
65
+ if args.client not in ADAPTERS:
66
+ print(f"✗ client không hỗ trợ: {args.client}", file=sys.stderr)
67
+ return None, EXIT_USAGE
68
+ adapter = ADAPTERS[args.client]()
69
+ if not adapter.available():
70
+ print(f"✗ chưa cài {args.client} trên máy này", file=sys.stderr)
71
+ return None, EXIT_NOT_READY
72
+ return adapter, EXIT_OK
aisdlc/cli/doctor.py ADDED
@@ -0,0 +1,286 @@
1
+ """Lệnh ``doctor`` — kiểm môi trường, cấu hình, hook và skill đã cài."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import shutil
7
+ import subprocess
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ from ..config import Config, ConfigError
12
+ from ._common import EXIT_NOT_READY, EXIT_OK
13
+
14
+
15
+ def _hook_paths_elsewhere(project: Path) -> list[str] | None:
16
+ """Đường dẫn dự án ghi trong hook/plugin biên dịch mà **khác** dự án này.
17
+ None khi không có hook nào để so."""
18
+ import re as _re
19
+
20
+ goc = project.resolve()
21
+ thay: list[str] = []
22
+ hook = project / ".claude" / "settings.json"
23
+ if hook.is_file():
24
+ thay += _re.findall(r"--project\s+(\S+)\s+guard", hook.read_text(encoding="utf-8", errors="replace"))
25
+ plugin = project / ".opencode" / "plugin" / "aisdlc-guard.ts"
26
+ if plugin.is_file():
27
+ thay += _re.findall(r'const PROJECT = "([^"]+)"', plugin.read_text(encoding="utf-8", errors="replace"))
28
+ if not thay:
29
+ return None
30
+ return sorted({p for p in thay if Path(p).resolve() != goc})
31
+
32
+
33
+ def cmd_doctor(args) -> int:
34
+ """Kiểm tra môi trường có đủ chạy framework không."""
35
+ project = Path(args.project)
36
+ problems: list[str] = []
37
+ lines: list[str] = []
38
+
39
+ def check(label: str, ok: bool, detail: str = "", *, required: bool = True) -> None:
40
+ mark = "✅" if ok else ("✗" if required else "○")
41
+ lines.append(f" {mark} {label}{(' — ' + detail) if detail else ''}")
42
+ if required and not ok:
43
+ problems.append(label)
44
+
45
+ lines.append("Môi trường:")
46
+ check("python >= 3.11", sys.version_info >= (3, 11), sys.version.split()[0])
47
+ check("git", bool(shutil.which("git")))
48
+
49
+ claude = shutil.which("claude")
50
+ check("claude CLI", bool(claude), claude or "không tìm thấy", required=False)
51
+ opencode = shutil.which("opencode")
52
+ check("opencode CLI", bool(opencode), opencode or "không tìm thấy", required=False)
53
+
54
+ docker_ok = False
55
+ if shutil.which("docker"):
56
+ try:
57
+ docker_ok = subprocess.run(
58
+ ["docker", "info"], capture_output=True, timeout=10
59
+ ).returncode == 0
60
+ except (subprocess.TimeoutExpired, OSError):
61
+ docker_ok = False
62
+ check(
63
+ "docker daemon",
64
+ docker_ok,
65
+ "chạy" if docker_ok else "không chạy — sandbox sẽ suy biến, bảo đảm thấp hơn",
66
+ required=False,
67
+ )
68
+
69
+ from ..harness.browser import availability as browser_availability
70
+ from ..harness.tools import image_for
71
+
72
+ browser_why = browser_availability(project)
73
+ check(
74
+ "playwright + chromium",
75
+ not browser_why,
76
+ "sẵn sàng" if not browser_why else f"{browser_why} — không trích được hợp đồng mockup",
77
+ required=False,
78
+ )
79
+
80
+ lines.append("Dự án:")
81
+ check("thư mục dự án", project.is_dir(), str(project))
82
+ from ..harness import sandbox as _sandbox
83
+
84
+ try:
85
+ cfg_for_sandbox = Config.load(project)
86
+ image = image_for(project, cfg_for_sandbox)
87
+ spec = _sandbox.SandboxSpec(
88
+ workspace=project, cmd=["true"],
89
+ use_docker=cfg_for_sandbox["sandbox.use_docker"],
90
+ provider=cfg_for_sandbox["sandbox.provider"],
91
+ )
92
+ except ConfigError:
93
+ image, spec = "?", _sandbox.SandboxSpec(workspace=project, cmd=["true"])
94
+ # Provider đang dùng và bảo đảm nó khai (ADR-005 V5): người đọc biết
95
+ # bằng chứng sắp ghi `degraded` vì **thiếu gì**, trước khi chạy story nào.
96
+ try:
97
+ prov, missing = _sandbox.select_provider(spec)
98
+ except (RuntimeError, ValueError, ImportError, AttributeError) as e:
99
+ prov, missing = None, []
100
+ check("sandbox provider", False, str(e), required=False)
101
+ if prov is not None:
102
+ co = sorted(g.value for g, s in prov.guarantees(spec.level).items() if s.blocks_at_source)
103
+ check(
104
+ "sandbox provider", not missing,
105
+ f"{prov.id} — bảo đảm ở {spec.level.value}: {', '.join(co) or 'không có'}"
106
+ + (f"; THIẾU {', '.join(missing)} — công cụ chạy ngoài cách ly, bằng chứng ghi degraded"
107
+ if missing else ""),
108
+ required=False,
109
+ )
110
+ if prov is not None and prov.id == "docker":
111
+ # Chỉ Docker mới bàn về ảnh; provider khác chạy công cụ của máy.
112
+ check(
113
+ "ảnh sandbox",
114
+ image != "alpine:latest",
115
+ image + (" — không có công cụ của stack nào, test sẽ đỏ vì thiếu công cụ"
116
+ if image == "alpine:latest" else " (hợp stack)"),
117
+ required=False,
118
+ )
119
+ req = project / "docs" / "requirements.md"
120
+ check("docs/requirements.md", req.is_file(), str(req))
121
+
122
+ # Hook sinh ra ≠ hook chạy. Story chạy trong worktree; worktree chỉ có
123
+ # `.claude/` nếu dự án commit nó. Harness nay truyền `--settings` tường
124
+ # minh (G4), nhưng thứ đó chỉ được chứng minh bằng hợp quy — ở đây nói
125
+ # thẳng tình trạng để người đọc biết mình đang dựa vào lớp nào.
126
+ plugin = project / ".opencode" / "plugin" / "aisdlc-guard.ts"
127
+ if plugin.is_file():
128
+ tracked = subprocess.run(
129
+ ["git", "-C", str(project), "ls-files", "--error-unmatch", str(plugin)],
130
+ capture_output=True, text=True,
131
+ ).returncode == 0
132
+ check(
133
+ "plugin OpenCode trong worktree",
134
+ tracked,
135
+ "`.opencode/plugin` đã commit — worktree tự có plugin" if tracked else
136
+ "`.opencode/plugin` chưa commit — worktree KHÔNG có plugin, OpenCode chạy "
137
+ "story với zero guard (đo hợp quy 2026-09-05); commit `.opencode/` hoặc "
138
+ "không dùng OpenCode cho `run`",
139
+ required=False,
140
+ )
141
+ hook = project / ".claude" / "settings.json"
142
+ if hook.is_file():
143
+ tracked = subprocess.run(
144
+ ["git", "-C", str(project), "ls-files", "--error-unmatch", str(hook)],
145
+ capture_output=True, text=True,
146
+ ).returncode == 0
147
+ check(
148
+ "hook trong worktree",
149
+ tracked,
150
+ "`.claude/settings.json` đã commit — worktree tự có hook" if tracked else
151
+ "`.claude/settings.json` chưa commit — worktree không có hook; harness "
152
+ "truyền `--settings` tường minh, cổng story mục \"guard có chạy\" sẽ "
153
+ "trượt nếu hook không tới",
154
+ required=False,
155
+ )
156
+
157
+ # Guard framework thêm sau lần `compile` cuối thì hook cũ không biết —
158
+ # ví dụ `process-ref` (luật 6, 2026-09-05). Không kiểm thì guard mới chỉ
159
+ # có trong mã, không có trong phiên agent nào.
160
+ rep = project / "_bmad-output" / "compile-report.json"
161
+ if rep.is_file():
162
+ from ..harness.guardrails import GUARD_MATCHERS
163
+
164
+ try:
165
+ data = json.loads(rep.read_text(encoding="utf-8"))
166
+ except (OSError, json.JSONDecodeError):
167
+ data = {}
168
+ for c in data.get("clients", []):
169
+ thieu = sorted(set(GUARD_MATCHERS) - set(c.get("guards_wired") or []) - set(c.get("guards_post_hoc") or []))
170
+ check(
171
+ f"hook {c.get('client')} đủ guard", not thieu,
172
+ f"{len(GUARD_MATCHERS)} guard" if not thieu else
173
+ f"thiếu {', '.join(thieu)} — framework có guard mới sau lần biên dịch; chạy `aisdlc compile --client {c.get('client')}`",
174
+ )
175
+
176
+ # Ngưỡng cỡ story chỉ đáng tin chừng nào nó còn khớp dữ liệu thật
177
+ # (ADR-004 R5). Bảng hiệu chuẩn do `run` tự ghi sau mỗi story.
178
+ from ..control import complexity
179
+
180
+ rows = complexity.load_calibration(project / "_bmad-output")
181
+ if rows:
182
+ lech_nguong = complexity.divergence(rows)
183
+ check(
184
+ "ngưỡng cỡ story khớp dữ liệu", not lech_nguong,
185
+ f"{len(rows)} story đã đo, không thấy lệch" if not lech_nguong else
186
+ "; ".join(lech_nguong) + " — chỉnh `story.max_complexity` trong "
187
+ "`.ai/config.json` hoặc xem lại trọng số ở `control/complexity.py`",
188
+ required=False,
189
+ )
190
+
191
+ lech = _hook_paths_elsewhere(project)
192
+ if lech is not None:
193
+ check(
194
+ "hook trỏ đúng dự án", not lech,
195
+ "đường dẫn `--project`/`PROJECT` trong hook khớp thư mục này" if not lech else
196
+ f"hook trỏ sang {', '.join(lech[:2])} — dự án bị chép/di chuyển? guard sẽ ghi bằng "
197
+ "chứng vào dự án ấy; chạy `aisdlc compile` lại",
198
+ )
199
+
200
+ try:
201
+ cfg = Config.load(project)
202
+ check("cấu hình", True, cfg.source)
203
+ lenh = str(cfg.get("tools.test", "") or "")
204
+ if lenh:
205
+ co_cov = any(k in lenh for k in ("--coverage", "--cov", "--experimental-test-coverage", "c8 ", "nyc "))
206
+ check(
207
+ "lệnh test in coverage", co_cov,
208
+ "cổng `coverage.min` đọc được số" if co_cov else
209
+ f"`tools.test` = `{lenh}` không in coverage → mục coverage của cổng sẽ là **chưa cấu hình** "
210
+ "(thêm `--coverage` / `--cov` / `--experimental-test-coverage`)",
211
+ required=False,
212
+ )
213
+ # Tên test đọc được không (ADR-005 V9)? Có bằng chứng thì tin bằng
214
+ # chứng: lần `tool_run test` gần nhất ghi `test_format`; chưa có
215
+ # thì đoán từ cờ lệnh. e9/par: 220/377 lần test không đọc được tên.
216
+ from ..harness.observe import TOOL_RUN, EvidenceStore
217
+
218
+ store = EvidenceStore(project / "_bmad-output")
219
+ lan = [e for sid in store.stories() for e in store.read(sid).of(TOOL_RUN)
220
+ if e.name in ("test", "test:baseline") and not e.detail.get("skipped")]
221
+ if lan:
222
+ doc_ten = bool(lan[-1].detail.get("test_format"))
223
+ vi = f"lần test gần nhất ghi test_format={lan[-1].detail.get('test_format') or ''!r}"
224
+ else:
225
+ doc_ten = any(k in lenh for k in ("-v", "--verbose", "--reporter=verbose",
226
+ "--test-reporter", "node --test", "ctrf"))
227
+ vi = "chưa có bằng chứng — đoán từ cờ lệnh"
228
+ check(
229
+ "lệnh test in tên test", doc_ten,
230
+ f"cổng đọc được tên test ({vi})" if doc_ten else
231
+ f"{vi} → mục `tiêu chí có test`/`không làm đỏ test có sẵn` là **chưa cấu hình**. "
232
+ "Dùng reporter in tên hoặc CTRF: pytest `-v` hay `pip install pytest-json-ctrf` + "
233
+ "`pytest --ctrf /dev/stdout`; vitest `--reporter=verbose` hay `vitest-ctrf-json-reporter` "
234
+ "(in `vitest-ctrf/report.json` ra stdout, giữ mã thoát); node `--test-reporter=spec|tap`",
235
+ required=False,
236
+ )
237
+ except ConfigError as e:
238
+ check("cấu hình", False, str(e))
239
+
240
+ skills_dir = project / ".claude" / "skills"
241
+ if skills_dir.is_dir():
242
+ from ..kit.security_filter import Verdict, classify_all
243
+
244
+ installed = [d for d in skills_dir.iterdir() if d.is_dir()]
245
+ lines.append("Skill đã cài:")
246
+ check("có skill", bool(installed), f"{len(installed)} thư mục")
247
+
248
+ # Bất biến: skill tấn công không bao giờ được có mặt trong dự án.
249
+ offensive = [
250
+ c.skill.name
251
+ for c in classify_all(skills_dir)
252
+ if c.verdict is Verdict.OFFENSIVE
253
+ ]
254
+ # Skill của framework nằm trong kho framework; dự án giữ một bản
255
+ # sao. Sửa skill mà không cài lại thì agent vẫn chạy bản cũ, và
256
+ # cách duy nhất phát hiện là ngồi so từng file.
257
+ from ..kit.install import OWN_SKILLS
258
+
259
+ stale = []
260
+ for own in sorted(OWN_SKILLS.glob("*/SKILL.md")) if OWN_SKILLS.is_dir() else []:
261
+ copied = skills_dir / own.parent.name / "SKILL.md"
262
+ if copied.is_file() and copied.read_bytes() != own.read_bytes():
263
+ stale.append(own.parent.name)
264
+ check(
265
+ "skill framework cập nhật",
266
+ not stale,
267
+ "khớp bản gốc" if not stale
268
+ else f"cũ hơn kho: {', '.join(stale)} — chạy `aisdlc setup`",
269
+ required=False,
270
+ )
271
+
272
+ check(
273
+ "không có skill tấn công",
274
+ not offensive,
275
+ "sạch" if not offensive else f"LỌT: {', '.join(offensive[:5])}",
276
+ )
277
+ else:
278
+ lines.append("Skill đã cài:")
279
+ check("đã chạy setup", False, "chưa — chạy: aisdlc setup", required=False)
280
+
281
+ print("\n".join(lines))
282
+ if problems:
283
+ print(f"\n✗ thiếu: {', '.join(problems)}")
284
+ return EXIT_NOT_READY
285
+ print("\n✅ sẵn sàng")
286
+ return EXIT_OK