rakit-cli 2.0.1__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.
- rakit_cli/__init__.py +3 -0
- rakit_cli/branding.py +21 -0
- rakit_cli/config.py +267 -0
- rakit_cli/main.py +1397 -0
- rakit_cli/mcp_server.py +325 -0
- rakit_cli/security.py +90 -0
- rakit_cli/skill_installer.py +218 -0
- rakit_cli/tasks_parser.py +59 -0
- rakit_cli/templates/SKILL.md +125 -0
- rakit_cli-2.0.1.dist-info/METADATA +108 -0
- rakit_cli-2.0.1.dist-info/RECORD +13 -0
- rakit_cli-2.0.1.dist-info/WHEEL +4 -0
- rakit_cli-2.0.1.dist-info/entry_points.txt +3 -0
rakit_cli/mcp_server.py
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
"""Server MCP `stdio` JSON-RPC 2.0 ringan untuk asisten AI editor lokal (ADR-002)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from rakit_cli.config import (
|
|
11
|
+
WorkspaceConfig,
|
|
12
|
+
get_effective_api_token,
|
|
13
|
+
load_workspace_config,
|
|
14
|
+
save_workspace_config,
|
|
15
|
+
write_active_task_prompt,
|
|
16
|
+
)
|
|
17
|
+
from rakit_cli.security import redact_sensitive_text
|
|
18
|
+
from rakit_cli.tasks_parser import parse_tasks_markdown
|
|
19
|
+
|
|
20
|
+
MCP_TOOLS_CATALOG: list[dict[str, Any]] = [
|
|
21
|
+
{
|
|
22
|
+
"name": "rakit_status",
|
|
23
|
+
"description": "Membaca ikatan proyek aktif dan progres TASKS.md di folder kerja notebook.",
|
|
24
|
+
"inputSchema": {
|
|
25
|
+
"type": "object",
|
|
26
|
+
"properties": {
|
|
27
|
+
"workspace_path": {
|
|
28
|
+
"type": "string",
|
|
29
|
+
"description": "Jalur folder proyek aktif (opsional)",
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"name": "rakit_claim_ticket",
|
|
36
|
+
"description": (
|
|
37
|
+
"Mengikat kunci tiket aktif pada workspace lokal untuk dikerjakan asisten AI."
|
|
38
|
+
),
|
|
39
|
+
"inputSchema": {
|
|
40
|
+
"type": "object",
|
|
41
|
+
"properties": {
|
|
42
|
+
"ticket_key": {"type": "string"},
|
|
43
|
+
"workspace_path": {
|
|
44
|
+
"type": "string",
|
|
45
|
+
"description": "Jalur folder proyek aktif (opsional)",
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
"required": ["ticket_key"],
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"name": "rakit_sync_progress",
|
|
53
|
+
"description": "Menyusun muatan progres sub-tugas TASKS.md yang tersanitasi dari rahasia.",
|
|
54
|
+
"inputSchema": {
|
|
55
|
+
"type": "object",
|
|
56
|
+
"properties": {
|
|
57
|
+
"summary": {"type": "string"},
|
|
58
|
+
"workspace_path": {
|
|
59
|
+
"type": "string",
|
|
60
|
+
"description": "Jalur folder proyek aktif (opsional)",
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"name": "rakit_ask_human",
|
|
67
|
+
"description": (
|
|
68
|
+
"Menyiapkan pertanyaan klarifikasi ask_human untuk dikirim ke HP pengembang."
|
|
69
|
+
),
|
|
70
|
+
"inputSchema": {
|
|
71
|
+
"type": "object",
|
|
72
|
+
"properties": {
|
|
73
|
+
"question": {"type": "string"},
|
|
74
|
+
"workspace_path": {
|
|
75
|
+
"type": "string",
|
|
76
|
+
"description": "Jalur folder proyek aktif (opsional)",
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
"required": ["question"],
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def find_workspace_root(start_dir: Path | None = None) -> Path:
|
|
86
|
+
"""
|
|
87
|
+
TUJUAN: Menemukan folder workspace yang memuat berkas `.rakit/workspace.json`.
|
|
88
|
+
|
|
89
|
+
ALUR:
|
|
90
|
+
1. Memeriksa environment variable `RAKIT_WORKSPACE` bila disetel.
|
|
91
|
+
2. Melakukan traversal ke direktori induk (parent traversal) dari `start_dir`.
|
|
92
|
+
3. Bila tidak ditemukan, kembali ke `start_dir` atau direktori aktif.
|
|
93
|
+
"""
|
|
94
|
+
import os
|
|
95
|
+
|
|
96
|
+
env_root = os.environ.get("RAKIT_WORKSPACE", "").strip()
|
|
97
|
+
if env_root:
|
|
98
|
+
candidate = Path(env_root).resolve()
|
|
99
|
+
if (candidate / ".rakit" / "workspace.json").exists():
|
|
100
|
+
return candidate
|
|
101
|
+
|
|
102
|
+
cur = (start_dir or Path.cwd()).resolve()
|
|
103
|
+
for candidate in [cur, *cur.parents]:
|
|
104
|
+
if (candidate / ".rakit" / "workspace.json").exists():
|
|
105
|
+
return candidate
|
|
106
|
+
return cur
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def handle_jsonrpc_message(
|
|
110
|
+
request: dict[str, Any],
|
|
111
|
+
*,
|
|
112
|
+
workspace_root: Path,
|
|
113
|
+
) -> dict[str, Any] | None:
|
|
114
|
+
"""
|
|
115
|
+
TUJUAN: Menangani satu pesan JSON-RPC 2.0 MCP (`initialize`, `tools/list`, `tools/call`).
|
|
116
|
+
|
|
117
|
+
ALUR:
|
|
118
|
+
1. Mengabaikan notifikasi satu arah yang tidak memiliki `id`.
|
|
119
|
+
2. Mengembalikan katalog 4 perkakas pada `tools/list`.
|
|
120
|
+
3. Mengeksekusi logika workspace lokal pada `tools/call` dengan penyensoran rahasia.
|
|
121
|
+
|
|
122
|
+
DIPANGGIL DARI: Loop `stdio` di `main()` dan pengujian unit MCP
|
|
123
|
+
DEPENDENSI: config, security, tasks_parser
|
|
124
|
+
"""
|
|
125
|
+
method = str(request.get("method") or "")
|
|
126
|
+
request_id = request.get("id")
|
|
127
|
+
if request_id is None:
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
if method == "initialize":
|
|
131
|
+
return {
|
|
132
|
+
"jsonrpc": "2.0",
|
|
133
|
+
"id": request_id,
|
|
134
|
+
"result": {
|
|
135
|
+
"protocolVersion": "2024-11-05",
|
|
136
|
+
"serverInfo": {"name": "rakit-mcp", "version": "2.0.0"},
|
|
137
|
+
"capabilities": {"tools": {}},
|
|
138
|
+
},
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if method == "tools/list":
|
|
142
|
+
return {
|
|
143
|
+
"jsonrpc": "2.0",
|
|
144
|
+
"id": request_id,
|
|
145
|
+
"result": {"tools": MCP_TOOLS_CATALOG},
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if method == "tools/call":
|
|
149
|
+
params = request.get("params") or {}
|
|
150
|
+
tool_name = str(params.get("name") or "")
|
|
151
|
+
arguments = params.get("arguments") or {}
|
|
152
|
+
result_payload = _execute_mcp_tool(
|
|
153
|
+
tool_name=tool_name,
|
|
154
|
+
arguments=arguments,
|
|
155
|
+
workspace_root=workspace_root,
|
|
156
|
+
)
|
|
157
|
+
return {
|
|
158
|
+
"jsonrpc": "2.0",
|
|
159
|
+
"id": request_id,
|
|
160
|
+
"result": {
|
|
161
|
+
"content": [
|
|
162
|
+
{
|
|
163
|
+
"type": "text",
|
|
164
|
+
"text": json.dumps(result_payload, ensure_ascii=False),
|
|
165
|
+
}
|
|
166
|
+
]
|
|
167
|
+
},
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
"jsonrpc": "2.0",
|
|
172
|
+
"id": request_id,
|
|
173
|
+
"error": {"code": -32601, "message": f"Method tidak dikenali: {method}"},
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _execute_mcp_tool(
|
|
178
|
+
*,
|
|
179
|
+
tool_name: str,
|
|
180
|
+
arguments: dict[str, Any],
|
|
181
|
+
workspace_root: Path,
|
|
182
|
+
) -> dict[str, Any]:
|
|
183
|
+
"""Menjalankan salah satu dari 4 perkakas MCP lokal."""
|
|
184
|
+
target_root = workspace_root
|
|
185
|
+
arg_ws = arguments.get("workspace_path")
|
|
186
|
+
if isinstance(arg_ws, str) and arg_ws.strip():
|
|
187
|
+
target_root = Path(arg_ws).resolve()
|
|
188
|
+
else:
|
|
189
|
+
target_root = find_workspace_root(workspace_root)
|
|
190
|
+
|
|
191
|
+
cfg = load_workspace_config(target_root)
|
|
192
|
+
if cfg is None:
|
|
193
|
+
return {"ok": False, "code": "WORKSPACE_NOT_INITIALIZED"}
|
|
194
|
+
|
|
195
|
+
tasks_path = target_root / "TASKS.md"
|
|
196
|
+
tasks_summary = (
|
|
197
|
+
parse_tasks_markdown(tasks_path.read_text(encoding="utf-8"))
|
|
198
|
+
if tasks_path.exists()
|
|
199
|
+
else parse_tasks_markdown("")
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
if tool_name == "rakit_status":
|
|
203
|
+
return {
|
|
204
|
+
"ok": True,
|
|
205
|
+
"project_slug": cfg.project_slug,
|
|
206
|
+
"active_ticket_key": cfg.active_ticket_key,
|
|
207
|
+
"tasks_completed": tasks_summary.completed_count,
|
|
208
|
+
"tasks_total": tasks_summary.total_count,
|
|
209
|
+
"next_open_task": tasks_summary.next_open_task,
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if tool_name == "rakit_claim_ticket":
|
|
213
|
+
ticket_key = str(arguments.get("ticket_key") or "").strip()
|
|
214
|
+
if not ticket_key:
|
|
215
|
+
return {"ok": False, "code": "TICKET_KEY_REQUIRED"}
|
|
216
|
+
updated = WorkspaceConfig(
|
|
217
|
+
project_slug=cfg.project_slug,
|
|
218
|
+
api_base_url=cfg.api_base_url,
|
|
219
|
+
machine_label=cfg.machine_label,
|
|
220
|
+
root_fingerprint=cfg.root_fingerprint,
|
|
221
|
+
active_ticket_key=ticket_key,
|
|
222
|
+
api_token=cfg.api_token,
|
|
223
|
+
secret_blocklist=cfg.secret_blocklist,
|
|
224
|
+
)
|
|
225
|
+
save_workspace_config(target_root, updated)
|
|
226
|
+
|
|
227
|
+
server_status = "saved_locally_offline"
|
|
228
|
+
auth_hint = ""
|
|
229
|
+
token = get_effective_api_token(cfg)
|
|
230
|
+
if cfg.api_base_url:
|
|
231
|
+
try:
|
|
232
|
+
import urllib.error
|
|
233
|
+
import urllib.request
|
|
234
|
+
|
|
235
|
+
claim_url = f"{cfg.api_base_url}/tickets/{ticket_key}/claim/"
|
|
236
|
+
claim_body = json.dumps(
|
|
237
|
+
{
|
|
238
|
+
"holder_id": cfg.machine_label or "mcp-agent",
|
|
239
|
+
"holder_kind": "NOTEBOOK_CLI",
|
|
240
|
+
"ttl_minutes": 30,
|
|
241
|
+
"project_slug": cfg.project_slug,
|
|
242
|
+
}
|
|
243
|
+
).encode("utf-8")
|
|
244
|
+
headers = {"Content-Type": "application/json"}
|
|
245
|
+
if token:
|
|
246
|
+
headers["Authorization"] = f"Token {token}"
|
|
247
|
+
req = urllib.request.Request(
|
|
248
|
+
claim_url, data=claim_body, headers=headers, method="POST"
|
|
249
|
+
)
|
|
250
|
+
with urllib.request.urlopen(req, timeout=4) as resp:
|
|
251
|
+
if resp.status in {200, 201}:
|
|
252
|
+
server_status = "claimed_on_server"
|
|
253
|
+
raw_claim = json.loads(resp.read().decode("utf-8"))
|
|
254
|
+
ticket_obj = (raw_claim.get("data") or {}).get("ticket") or {}
|
|
255
|
+
if ticket_obj:
|
|
256
|
+
write_active_task_prompt(target_root, ticket_obj)
|
|
257
|
+
except urllib.error.HTTPError as err:
|
|
258
|
+
if err.code in {401, 403}:
|
|
259
|
+
server_status = "unauthorized_401"
|
|
260
|
+
auth_hint = (
|
|
261
|
+
"Token API RAKIT belum disetel atau tidak valid. "
|
|
262
|
+
"Salin token dari Web Console: http://localhost:8000/accounts/profile/ "
|
|
263
|
+
"lalu simpan dengan: rakit token <TOKEN>"
|
|
264
|
+
)
|
|
265
|
+
else:
|
|
266
|
+
server_status = f"http_{err.code}"
|
|
267
|
+
except Exception as e:
|
|
268
|
+
server_status = f"offline_fallback: {e}"
|
|
269
|
+
|
|
270
|
+
res: dict[str, Any] = {
|
|
271
|
+
"ok": True,
|
|
272
|
+
"active_ticket_key": ticket_key,
|
|
273
|
+
"server_claim_status": server_status,
|
|
274
|
+
}
|
|
275
|
+
if auth_hint:
|
|
276
|
+
res["auth_hint"] = auth_hint
|
|
277
|
+
return res
|
|
278
|
+
|
|
279
|
+
if tool_name == "rakit_sync_progress":
|
|
280
|
+
raw_summary = str(arguments.get("summary") or tasks_summary.next_open_task)
|
|
281
|
+
return {
|
|
282
|
+
"ok": True,
|
|
283
|
+
"active_ticket_key": cfg.active_ticket_key,
|
|
284
|
+
"tasks_completed": tasks_summary.completed_count,
|
|
285
|
+
"tasks_total": tasks_summary.total_count,
|
|
286
|
+
"summary": redact_sensitive_text(raw_summary),
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if tool_name == "rakit_ask_human":
|
|
290
|
+
question = redact_sensitive_text(str(arguments.get("question") or "").strip())
|
|
291
|
+
if not question:
|
|
292
|
+
return {"ok": False, "code": "QUESTION_REQUIRED"}
|
|
293
|
+
return {
|
|
294
|
+
"ok": True,
|
|
295
|
+
"active_ticket_key": cfg.active_ticket_key,
|
|
296
|
+
"paused_status": "NEEDS_HUMAN",
|
|
297
|
+
"question": question,
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return {"ok": False, "code": "UNKNOWN_TOOL"}
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def main() -> int:
|
|
304
|
+
"""Menjalankan loop pembacaan baris JSON-RPC 2.0 melalui `sys.stdin` dan `sys.stdout`."""
|
|
305
|
+
import argparse
|
|
306
|
+
|
|
307
|
+
parser = argparse.ArgumentParser(description="RAKIT MCP stdio server")
|
|
308
|
+
parser.add_argument("--workspace", "-w", type=Path, default=None, help="Jalur workspace")
|
|
309
|
+
args, _ = parser.parse_known_args()
|
|
310
|
+
|
|
311
|
+
workspace_root = find_workspace_root(args.workspace.resolve() if args.workspace else None)
|
|
312
|
+
for raw_line in sys.stdin:
|
|
313
|
+
stripped = raw_line.strip()
|
|
314
|
+
if not stripped:
|
|
315
|
+
continue
|
|
316
|
+
request_obj = json.loads(stripped)
|
|
317
|
+
response_obj = handle_jsonrpc_message(request_obj, workspace_root=workspace_root)
|
|
318
|
+
if response_obj is not None:
|
|
319
|
+
sys.stdout.write(json.dumps(response_obj, ensure_ascii=False) + "\n")
|
|
320
|
+
sys.stdout.flush()
|
|
321
|
+
return 0
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
if __name__ == "__main__":
|
|
325
|
+
raise SystemExit(main())
|
rakit_cli/security.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Saringan keamanan pemindaian lokal & penyensoran rahasia sebelum dikirim ke server RAKIT."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path, PurePosixPath
|
|
7
|
+
|
|
8
|
+
BLOCKED_DIRECTORY_NAMES: frozenset[str] = frozenset(
|
|
9
|
+
{
|
|
10
|
+
".git",
|
|
11
|
+
".venv",
|
|
12
|
+
"venv",
|
|
13
|
+
"node_modules",
|
|
14
|
+
"__pycache__",
|
|
15
|
+
".pytest_cache",
|
|
16
|
+
".dart_tool",
|
|
17
|
+
"build",
|
|
18
|
+
"secrets",
|
|
19
|
+
".secrets",
|
|
20
|
+
".rakit",
|
|
21
|
+
}
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
BLOCKED_SUFFIXES: tuple[str, ...] = (
|
|
25
|
+
".pem",
|
|
26
|
+
".key",
|
|
27
|
+
".p12",
|
|
28
|
+
".pfx",
|
|
29
|
+
".keystore",
|
|
30
|
+
".jks",
|
|
31
|
+
".sqlite3",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
BLOCKED_FILE_PREFIXES: tuple[str, ...] = (
|
|
35
|
+
"id_rsa",
|
|
36
|
+
"id_ed25519",
|
|
37
|
+
"id_ecdsa",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
_SECRET_PATTERNS: tuple[re.Pattern[str], ...] = (
|
|
41
|
+
re.compile(
|
|
42
|
+
r"(?i)(token|api[_-]?key|secret|password)\s*[:=]\s*['\"]?[A-Za-z0-9_\-\.]{8,}['\"]?"
|
|
43
|
+
),
|
|
44
|
+
re.compile(r"gh[pousr]_[A-Za-z0-9_]{20,}"),
|
|
45
|
+
re.compile(r"sk-[A-Za-z0-9_\-]{20,}"),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def is_safe_workspace_path(relative_path: str | Path) -> bool:
|
|
50
|
+
"""
|
|
51
|
+
TUJUAN: Memastikan berkas lokal aman dipindai tanpa menyentuh `.env` atau kunci privat (NFR-05).
|
|
52
|
+
|
|
53
|
+
ALUR:
|
|
54
|
+
1. Menolak segmen direktori terlarang / berawalan titik (`.git`, `.venv`, dll.).
|
|
55
|
+
2. Menolak berkas dot/tersembunyi (`.gitignore`, `.coverage`, `.python-version`, `.env*`).
|
|
56
|
+
3. Menolak sertifikat/kunci privat (`*.pem`, `*.key`, `id_rsa*`).
|
|
57
|
+
|
|
58
|
+
DIPANGGIL DARI: Pemindai pohon direktori `rakit scan`
|
|
59
|
+
DEPENDENSI: pathlib
|
|
60
|
+
"""
|
|
61
|
+
posix_path = PurePosixPath(str(relative_path).replace("\\", "/"))
|
|
62
|
+
for part in posix_path.parts[:-1]:
|
|
63
|
+
if part.startswith(".") or part in BLOCKED_DIRECTORY_NAMES:
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
filename = posix_path.name
|
|
67
|
+
lower_name = filename.lower()
|
|
68
|
+
if lower_name.startswith(".") or lower_name in BLOCKED_DIRECTORY_NAMES:
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
if lower_name.endswith(BLOCKED_SUFFIXES):
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
if lower_name.startswith(BLOCKED_FILE_PREFIXES):
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
return True
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def redact_sensitive_text(raw_text: str) -> str:
|
|
81
|
+
"""
|
|
82
|
+
TUJUAN: Menyensor pola token, password, dan kunci API dari ringkasan teks sebelum dikirim.
|
|
83
|
+
|
|
84
|
+
DIPANGGIL DARI: `rakit sync` dan perkakas MCP `rakit_ask_human`
|
|
85
|
+
DEPENDENSI: re
|
|
86
|
+
"""
|
|
87
|
+
sanitized = raw_text
|
|
88
|
+
for pattern in _SECRET_PATTERNS:
|
|
89
|
+
sanitized = pattern.sub("[REDACTED_SECRET]", sanitized)
|
|
90
|
+
return sanitized
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""Modul pemasang & pengelola AI Skill RAKIT v2.0 untuk asisten coding."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
_FALLBACK_SKILL_CONTENT = """---
|
|
8
|
+
name: rakit
|
|
9
|
+
description: >-
|
|
10
|
+
Skill orkestrator alur kerja AI & pengembang untuk ekosistem RAKIT v2.0.
|
|
11
|
+
Membimbing siklus hidup pengembangan dari ide (PRD, User Story, ADR, DB Design, Prototipe),
|
|
12
|
+
interaksi tiket backlog via MCP/CLI, hingga quality gates.
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# RAKIT v2.0 AI Assistant Skill
|
|
16
|
+
Skill ini memandu AI mengeksekusi tiket backlog dan quality gates RAKIT v2.0 secara riil.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
MODULAR_COMPANION_SKILLS: dict[str, str] = {
|
|
20
|
+
"rakit-status": """---
|
|
21
|
+
name: rakit-status
|
|
22
|
+
description: Membaca status koneksi proyek RAKIT, kunci tiket aktif, dan progres TASKS.md.
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
# /rakit-status — Status Proyek & Progres
|
|
26
|
+
1. Panggil MCP `rakit_status` atau jalankan `rakit status`.
|
|
27
|
+
2. Sajikan laporan status proyek, tiket aktif, progres TASKS.md, dan tugas terbuka berikutnya.
|
|
28
|
+
""",
|
|
29
|
+
"rakit-claim-ticket": """---
|
|
30
|
+
name: rakit-claim-ticket
|
|
31
|
+
description: Mengklaim tiket aktif dari backlog ke workspace lokal & buat .rakit/PROMPT.md.
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
# /rakit-claim-ticket — Klaim Tiket Aktif
|
|
35
|
+
1. Panggil MCP `rakit_claim_ticket` dengan ticket_key atau jalankan `rakit start <KEY>`.
|
|
36
|
+
2. Baca `.rakit/PROMPT.md` untuk memahami allowed_paths dan kriteria GWT.
|
|
37
|
+
3. Mulai pengerjaan TDD secara nyata di repositori!
|
|
38
|
+
""",
|
|
39
|
+
"rakit-sync-progress": """---
|
|
40
|
+
name: rakit-sync-progress
|
|
41
|
+
description: Menyaring rahasia & sinkronkan progres to-do TASKS.md ke Web Console RAKIT.
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
# /rakit-sync-progress — Sinkronisasi Progres
|
|
45
|
+
1. Pastikan to-do selesai di `TASKS.md` sudah bertanda `[x]`.
|
|
46
|
+
2. Panggil MCP `rakit_sync_progress` atau jalankan `rakit sync --note "<CATATAN>"`.
|
|
47
|
+
3. Tampilkan konfirmasi progres dan persentase terbaru.
|
|
48
|
+
""",
|
|
49
|
+
"rakit-ask-human": """---
|
|
50
|
+
name: rakit-ask-human
|
|
51
|
+
description: Eskalasi blocker/keraguan bisnis ke Web Console & HP pengembang (NEEDS_HUMAN).
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
# /rakit-ask-human — Eskalasi Blocker ke Pengembang
|
|
55
|
+
1. Rumuskan pertanyaan ringkas tanpa data rahasia/token.
|
|
56
|
+
2. Panggil tool MCP `rakit_ask_human` dengan argumen pertanyaan.
|
|
57
|
+
3. Status tiket berubah menjadi `NEEDS_HUMAN` di Web Console & HP pengembang.
|
|
58
|
+
4. Tunggu respon manusia sebelum melanjutkan kode.
|
|
59
|
+
""",
|
|
60
|
+
"rakit-new-task": """---
|
|
61
|
+
name: rakit-new-task
|
|
62
|
+
description: Formulasi tiket baru berformat GWT, tentukan allowed_paths, & daftar ke backlog.
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
# /rakit-new-task — Buat Tiket Baru
|
|
66
|
+
1. Tentukan sejak awal bersama pengguna:
|
|
67
|
+
- **Kategori Alur** (mis. `Manajemen Aset`, `Auth & Login`, `Inti Sistem`),
|
|
68
|
+
- **Story Points** (mis. `3` atau `5`), **Allowed Paths**, **Given-When-Then**, dan **Cakupan**.
|
|
69
|
+
2. Tambahkan butir `- [ ]` terstruktur ke `TASKS.md` dengan format metadata berikut
|
|
70
|
+
agar langsung terbaca oleh `rakit scan --persist`:
|
|
71
|
+
- `- **Kategori:** <Kategori Alur>`
|
|
72
|
+
- `- **Story Points:** <angka>`
|
|
73
|
+
- `- **Allowed Paths:** <daftar folder/berkas dipisah koma>`
|
|
74
|
+
- `- **Given-When-Then (GWT):**` (`- **Given:**`, `- **When:**`, `- **Then:**`)
|
|
75
|
+
- `- **Cakupan:** <rincian poin pekerjaan & unit test>`
|
|
76
|
+
3. Jalankan `rakit scan --persist` untuk mendaftarkan tiket lengkap ke BACKLOG Web Console.
|
|
77
|
+
""",
|
|
78
|
+
"rakit-prd": """---
|
|
79
|
+
name: rakit-prd
|
|
80
|
+
description: Menyusun dokumen Product Requirement Document di docs/prd/<fitur>.md (G1 Gate).
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
# /rakit-prd — Penyusunan PRD
|
|
84
|
+
1. Buat dokumen PRD di `docs/prd/<nama_fitur>.md`.
|
|
85
|
+
2. Susun Problem Statement, Goals, Non-Goals, FR (GWT), Security, dan allowed_paths.
|
|
86
|
+
3. Periksa kelengkapan Gerbang G1 (Spec Gate).
|
|
87
|
+
""",
|
|
88
|
+
"rakit-user-story": """---
|
|
89
|
+
name: rakit-user-story
|
|
90
|
+
description: Memecah PRD menjadi User Stories berformat GWT dan estimasi Story Points.
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
# /rakit-user-story — Pecah User Story
|
|
94
|
+
1. Baca PRD dan buat berkas `docs/prd/user-stories/<fitur>.md`.
|
|
95
|
+
2. Format: As a... I want to... So that..., Acceptance Criteria GWT, estimasi.
|
|
96
|
+
3. Daftarkan checklist sub-tugas ke `TASKS.md`.
|
|
97
|
+
""",
|
|
98
|
+
"rakit-adr": """---
|
|
99
|
+
name: rakit-adr
|
|
100
|
+
description: Membuat Architectural Decision Record di docs/adr/ format Michael Nygard.
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
# /rakit-adr — Architectural Decision Record
|
|
104
|
+
1. Cari nomor urut ADR berikutnya di `docs/adr/`.
|
|
105
|
+
2. Tulis berkas `docs/adr/ADR-XXX-<judul>.md`.
|
|
106
|
+
3. Cantumkan Status, Konteks, Keputusan, Konsekuensi, dan Compliance.
|
|
107
|
+
""",
|
|
108
|
+
"rakit-db-design": """---
|
|
109
|
+
name: rakit-db-design
|
|
110
|
+
description: Merancang skema database relasional, ERD mermaid, tenant isolation, & migrasi.
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
# /rakit-db-design — Desain Database Relasional
|
|
114
|
+
1. Rancang entitas, relasi, UUID/BigInt PK, FK cascade, dan composite index.
|
|
115
|
+
2. Terapkan multi-tenant (`tenant_id`), audit trail, dan soft-delete.
|
|
116
|
+
3. Buat diagram Mermaid ERD dan deklarasi model Django lengkap.
|
|
117
|
+
""",
|
|
118
|
+
"rakit-proto": """---
|
|
119
|
+
name: rakit-proto
|
|
120
|
+
description: Merancang prototipe antarmuka UI (Dasa UI v2, Cotton, HTMX, Flutter Pastel).
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
# /rakit-proto — Prototipe Antarmuka
|
|
124
|
+
1. Desain komponen UI menggunakan sistem desain yang berlaku:
|
|
125
|
+
- Web: Dasa UI v2 + Django-Cotton + HTMX parsial.
|
|
126
|
+
- Mobile: Flutter M3 Pastel (`useMaterial3: true`, RakitSemanticColors).
|
|
127
|
+
2. Dilarang memakai inline CSS liar atau warna hex hardcoded.
|
|
128
|
+
""",
|
|
129
|
+
"rakit-verify": """---
|
|
130
|
+
name: rakit-verify
|
|
131
|
+
description: Audit mutu lokal (linter, pytest, coverage, TASKS.md) menuju Gerbang G2 (QA).
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
# /rakit-verify — Verifikasi Mutu Gerbang G2
|
|
135
|
+
1. Pastikan to-do terkait di `TASKS.md` sudah `[x]`.
|
|
136
|
+
2. Jalankan linter (mis. `uv run ruff check .`) dan unit test (`uv run pytest`)
|
|
137
|
+
sesuai konfigurasi `verification` di `rakit.yaml`.
|
|
138
|
+
3. Jalankan `rakit verify`.
|
|
139
|
+
4. Panggil `rakit_sync_progress` untuk melaporkan kesiapan tiket ke QA.
|
|
140
|
+
""",
|
|
141
|
+
"rakit-uat": """---
|
|
142
|
+
name: rakit-uat
|
|
143
|
+
description: Menyusun dokumen UAT Acceptance Test Plan dan skenario Playwright E2E.
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
# /rakit-uat — User Acceptance Test (UAT)
|
|
147
|
+
1. Buat dokumen skenario acceptance di `docs/uat/<fitur>.md`.
|
|
148
|
+
2. Tulis skenario E2E pada direktori proyek target sesuai `e2e_dir` di `rakit.yaml`
|
|
149
|
+
(mis. `tests/e2e/test_<fitur>.py` atau `e2e/<fitur>.spec.ts`).
|
|
150
|
+
3. Pastikan bukti uji/video H.264 `.mp4` (jika tersedia) dikirim ke API RAKIT.
|
|
151
|
+
""",
|
|
152
|
+
"rakit-release": """---
|
|
153
|
+
name: rakit-release
|
|
154
|
+
description: Checklist Gerbang G3 (Rilis Produksi), bump SemVer, CHANGELOG, & artefak.
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
# /rakit-release — Gerbang G3 & Rilis Produksi
|
|
158
|
+
1. Periksa kelulusan seluruh tiket dan audit keamanan.
|
|
159
|
+
2. Naikkan versi SemVer di `pyproject.toml` dan `pubspec.yaml`.
|
|
160
|
+
3. Perbarui `CHANGELOG.md` dan verifikasi artefak rilis.
|
|
161
|
+
""",
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def get_rakit_skill_content() -> str:
|
|
166
|
+
"""Mengembalikan konten Markdown lengkap dari berkas SKILL.md RAKIT v2.0."""
|
|
167
|
+
template_path = Path(__file__).parent / "templates" / "SKILL.md"
|
|
168
|
+
if template_path.exists():
|
|
169
|
+
return template_path.read_text(encoding="utf-8")
|
|
170
|
+
return _FALLBACK_SKILL_CONTENT.strip() + "\n"
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def install_skill(
|
|
174
|
+
destination_dir: Path,
|
|
175
|
+
*,
|
|
176
|
+
force: bool = False,
|
|
177
|
+
) -> tuple[Path, str]:
|
|
178
|
+
"""
|
|
179
|
+
TUJUAN: Memasang berkas SKILL.md ke folder target beserta skill pendamping.
|
|
180
|
+
|
|
181
|
+
PARAMETER:
|
|
182
|
+
- destination_dir: Folder tujuan (misal .agents/skills/rakit)
|
|
183
|
+
- force: Bila True, timpa berkas jika sudah ada.
|
|
184
|
+
|
|
185
|
+
KEMBALIAN:
|
|
186
|
+
Tuple (jalur_berkas_skill, status: 'created' | 'updated' | 'existing_preserved')
|
|
187
|
+
"""
|
|
188
|
+
destination_dir.mkdir(parents=True, exist_ok=True)
|
|
189
|
+
skill_file = destination_dir / "SKILL.md"
|
|
190
|
+
|
|
191
|
+
content = get_rakit_skill_content()
|
|
192
|
+
is_update = skill_file.exists()
|
|
193
|
+
status = "existing_preserved"
|
|
194
|
+
if not skill_file.exists() or force:
|
|
195
|
+
skill_file.write_text(content, encoding="utf-8")
|
|
196
|
+
status = "updated" if is_update else "created"
|
|
197
|
+
|
|
198
|
+
# Jika destination_dir bernama 'rakit', otomatis pasang skill pendamping di folder induknya
|
|
199
|
+
if destination_dir.name == "rakit":
|
|
200
|
+
base_skills_dir = destination_dir.parent
|
|
201
|
+
for skill_name, skill_text in MODULAR_COMPANION_SKILLS.items():
|
|
202
|
+
child_dir = base_skills_dir / skill_name
|
|
203
|
+
child_dir.mkdir(parents=True, exist_ok=True)
|
|
204
|
+
child_file = child_dir / "SKILL.md"
|
|
205
|
+
if not child_file.exists() or force:
|
|
206
|
+
child_file.write_text(skill_text.strip() + "\n", encoding="utf-8")
|
|
207
|
+
|
|
208
|
+
return skill_file, status
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def get_default_workspace_skill_dir(workspace_root: Path) -> Path:
|
|
212
|
+
"""Mengembalikan jalur standar direktori skill workspace lokal (.agents/skills/rakit)."""
|
|
213
|
+
return workspace_root / ".agents" / "skills" / "rakit"
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def get_default_global_skill_dir() -> Path:
|
|
217
|
+
"""Mengembalikan jalur standar direktori skill global pengguna (~/.agents/skills/rakit)."""
|
|
218
|
+
return Path.home() / ".agents" / "skills" / "rakit"
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Pengurai berkas `TASKS.md` untuk menghitung progres sub-tugas secara deterministik."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
_CHECKBOX_PATTERN = re.compile(r"^\s*-\s*\[(?P<mark>[ xX])\]\s+(?P<label>.+?)\s*$")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class TasksProgressSummary:
|
|
13
|
+
"""
|
|
14
|
+
TUJUAN: Menyimpan ringkasan tugas selesai, total tugas, persentase, dan item terbuka berikutnya.
|
|
15
|
+
|
|
16
|
+
DIPANGGIL DARI: `rakit sync`, `rakit scan`, dan perkakas MCP `rakit_status`
|
|
17
|
+
DEPENDENSI: dataclasses
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
completed_count: int
|
|
21
|
+
total_count: int
|
|
22
|
+
progress_pct: int
|
|
23
|
+
next_open_task: str
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def parse_tasks_markdown(markdown_text: str) -> TasksProgressSummary:
|
|
27
|
+
"""
|
|
28
|
+
TUJUAN: Menghitung kotak centang `- [x]` dan `- [ ]` dari isi berkas `TASKS.md`.
|
|
29
|
+
|
|
30
|
+
ALUR:
|
|
31
|
+
1. Memindai baris demi baris memakai ekspresi reguler kotak centang Markdown.
|
|
32
|
+
2. Mencatat butir `- [ ]` pertama sebagai `next_open_task` (milestone aktif).
|
|
33
|
+
|
|
34
|
+
DIPANGGIL DARI: CLI `rakit` dan server MCP `rakit-mcp`
|
|
35
|
+
DEPENDENSI: re
|
|
36
|
+
"""
|
|
37
|
+
completed = 0
|
|
38
|
+
total = 0
|
|
39
|
+
next_open = ""
|
|
40
|
+
|
|
41
|
+
for line in markdown_text.splitlines():
|
|
42
|
+
match = _CHECKBOX_PATTERN.match(line)
|
|
43
|
+
if match is None:
|
|
44
|
+
continue
|
|
45
|
+
total += 1
|
|
46
|
+
mark = match.group("mark")
|
|
47
|
+
label = match.group("label").strip()
|
|
48
|
+
if mark.lower() == "x":
|
|
49
|
+
completed += 1
|
|
50
|
+
elif not next_open:
|
|
51
|
+
next_open = label
|
|
52
|
+
|
|
53
|
+
pct = int((completed / total) * 100) if total > 0 else 0
|
|
54
|
+
return TasksProgressSummary(
|
|
55
|
+
completed_count=completed,
|
|
56
|
+
total_count=total,
|
|
57
|
+
progress_pct=min(pct, 100),
|
|
58
|
+
next_open_task=next_open,
|
|
59
|
+
)
|