chore(rebar): Pier_T_rebar 작업 폴더 초기 구성 및 외부 의존 파일 통합
- Code\rebar_pipeline, review_pipeline, pier-program을 정션에서 실제 폴더로 전환 - Code\rhino_mcp 및 CLAUDE_rhino배경.md 추가, .mcp.json은 PYTHONPATH로 새 위치 사용 - pier-program 내부 .git 제거(이력은 backup\pier-program_20261028.bundle에 보관) - worktree 가이드 반영(CLAUDE.local.md, HANDOFF.md) - D:\Rhino-MCP 경로 참조를 새 위치로 치환 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
964a841de1
3978 files changed
+58013356
No files matched your search
@@ -0,0 +1,92 @@
|
||||
import re
|
||||
import sys
|
||||
import json
|
||||
|
||||
def calculate_ambiguity_score(user_request, context={}):
|
||||
score = 0
|
||||
factors = []
|
||||
|
||||
# Factor 1: No file path (higher ambiguity)
|
||||
file_path_pattern = r'(\/[\w.-]+\/[\w.-]+)|([A-Z]:\\[\w.-]+\\[\w.-]+)|(\.\/[\w.-]+)|(\.(js|ts|py|go|rs|java|tsx|jsx|vue|svelte|md|json|yaml|yml)$)'
|
||||
if not re.search(file_path_pattern, user_request, re.IGNORECASE):
|
||||
factors.append('no_file_path')
|
||||
score += 0.15
|
||||
|
||||
# Factor 2: No technical terms
|
||||
tech_terms = [
|
||||
'api', 'database', 'server', 'client', 'component', 'module',
|
||||
'function', 'class', 'interface', 'type', 'schema', 'endpoint',
|
||||
'authentication', 'authorization', 'middleware', 'controller',
|
||||
'service', 'repository', 'model', 'view', 'hook', 'context'
|
||||
]
|
||||
if not any(term in user_request.lower() for term in tech_terms):
|
||||
factors.append('no_technical_terms')
|
||||
score += 0.1
|
||||
|
||||
# Factor 3: No specific nouns (PascalCase or camelCase)
|
||||
if not re.search(r'("[^"]+")|(\'[^\']+\')|([A-Z][a-z]+[A-Z][a-z]+)|([a-z]+[A-Z][a-z]+)', user_request):
|
||||
factors.append('no_specific_nouns')
|
||||
score += 0.15
|
||||
|
||||
# Factor 4: No scope definition
|
||||
scope_patterns = [
|
||||
r'only|just|specifically|exactly',
|
||||
r'all|every|entire|whole',
|
||||
r'from\s+\w+\s+to\s+\w+',
|
||||
r'in\s+the\s+\w+\s+(?:file|folder|directory|module)'
|
||||
]
|
||||
if not any(re.search(p, user_request, re.IGNORECASE) for p in scope_patterns):
|
||||
factors.append('no_scope')
|
||||
score += 0.1
|
||||
|
||||
# Factor 5: Multiple interpretations
|
||||
ambiguous_patterns = [
|
||||
r'maybe|perhaps|or|either',
|
||||
r'it|this|that|those|these',
|
||||
r'stuff|things|something',
|
||||
r'fix|update|change|modify'
|
||||
]
|
||||
pronoun_count = len(re.findall(r'\b(it|this|that)\b', user_request, re.IGNORECASE))
|
||||
if pronoun_count >= 2 or any(re.search(p, user_request, re.IGNORECASE) for p in ambiguous_patterns):
|
||||
factors.append('multiple_interpretations')
|
||||
score += 0.2
|
||||
|
||||
# Factor 6: Short request
|
||||
if len(user_request) < 30:
|
||||
factors.append('short_request')
|
||||
score += 0.15
|
||||
|
||||
# Factor 7: Context Conflict (New in v2)
|
||||
# Check if request contradicts current phase
|
||||
current_phase = context.get('current_feature_phase', 'PLAN')
|
||||
phase_keywords = {
|
||||
'PLAN': ['implement', 'code', 'build', 'deploy'],
|
||||
'DESIGN': ['deploy', 'test', 'release'],
|
||||
'DO': ['plan', 'design', 'architecture']
|
||||
}
|
||||
conflict_keywords = phase_keywords.get(current_phase, [])
|
||||
for keyword in conflict_keywords:
|
||||
if keyword in user_request.lower():
|
||||
factors.append(f'context_conflict_with_{current_phase}')
|
||||
score += 0.15
|
||||
break
|
||||
|
||||
score = min(1.0, max(0.0, score))
|
||||
return {"score": score, "factors": factors}
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print(json.dumps({"error": "No request provided"}))
|
||||
sys.exit(1)
|
||||
|
||||
request = sys.argv[1]
|
||||
# Simulate context from arg 2 if present
|
||||
context = {}
|
||||
if len(sys.argv) > 2:
|
||||
try:
|
||||
context = json.loads(sys.argv[2])
|
||||
except:
|
||||
pass
|
||||
|
||||
result = calculate_ambiguity_score(request, context)
|
||||
print(json.dumps(result, indent=2))
|
||||
@@ -0,0 +1,53 @@
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
import glob
|
||||
|
||||
def archive_feature(feature_name):
|
||||
archive_root = "archive"
|
||||
target_dir = os.path.join(archive_root, feature_name)
|
||||
|
||||
print(f"Archiving feature '{feature_name}' to {target_dir}...")
|
||||
|
||||
if not os.path.exists(target_dir):
|
||||
os.makedirs(target_dir)
|
||||
|
||||
# Find all files related to this feature
|
||||
# Convention: {feature}.*.md or inside feature folders
|
||||
|
||||
patterns = [
|
||||
f"docs/**/*{feature_name}*.md",
|
||||
f"src/**/*{feature_name}*"
|
||||
]
|
||||
|
||||
moved_count = 0
|
||||
|
||||
for pattern in patterns:
|
||||
# Recursive glob
|
||||
files = glob.glob(pattern, recursive=True)
|
||||
for src in files:
|
||||
if os.path.isdir(src): continue
|
||||
|
||||
# mirror logic logic is complex, for v1.0 let's just flatten or simple move
|
||||
# Simple move: archive/{feature}/{original_basename}
|
||||
basename = os.path.basename(src)
|
||||
dst = os.path.join(target_dir, basename)
|
||||
|
||||
try:
|
||||
shutil.move(src, dst)
|
||||
print(f" Moved: {src} -> {dst}")
|
||||
moved_count += 1
|
||||
except Exception as e:
|
||||
print(f" Error moving {src}: {e}")
|
||||
|
||||
if moved_count > 0:
|
||||
print(f"✅ Successfully archived {moved_count} files.")
|
||||
else:
|
||||
print("⚠️ No files found to archive.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python archive.py <feature_name>")
|
||||
sys.exit(1)
|
||||
|
||||
archive_feature(sys.argv[1])
|
||||
@@ -0,0 +1,196 @@
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import datetime
|
||||
import shutil
|
||||
import re
|
||||
from state_engine import ContextManager, Guardrails
|
||||
|
||||
def detect_stale_features(ctx):
|
||||
features = ctx.state.get('features', {})
|
||||
stale = []
|
||||
now = datetime.datetime.now()
|
||||
for fname, fdata in features.items():
|
||||
# archive된거 제외하려면 로직 필요하나 일단 history 마지막 날짜 기준
|
||||
if fdata.get('phase') in ['REPORT', 'ARCHIVE']:
|
||||
continue
|
||||
history = fdata.get('history', [])
|
||||
if not history: continue
|
||||
try:
|
||||
last_time = datetime.datetime.fromisoformat(history[-1].get('timestamp', ''))
|
||||
days_idle = (now - last_time).days
|
||||
if days_idle >= 2: # Reduce to 2 for tighter detection in AI contexts
|
||||
stale.append({"feature": fname, "daysIdle": days_idle, "phase": fdata.get('phase')})
|
||||
except:
|
||||
pass
|
||||
return stale
|
||||
|
||||
def print_agent_context_injection(ctx):
|
||||
"""
|
||||
Prints a markdown-formatted context for the agent to consume immediately.
|
||||
"""
|
||||
state = ctx.state
|
||||
current_feature = state.get('current_feature')
|
||||
level = state.get('project_level', 'Starter')
|
||||
|
||||
# Read blueprint for goal
|
||||
blueprint_path = os.path.join(".hmcmkit", "blueprint.md")
|
||||
goal = "Unknown"
|
||||
if os.path.exists(blueprint_path):
|
||||
try:
|
||||
with open(blueprint_path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
# Simple extraction for goal
|
||||
match = re.search(r"- \*\*Goal\*\*: (.*)", content)
|
||||
if match:
|
||||
goal = match.group(1)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("🚀 HMCMkit DYNAMIC CONTEXT INJECTION (Action Items)")
|
||||
print("="*60)
|
||||
print(f"\n### 📍 Current Project Status")
|
||||
print(f"- **Project Name**: {state.get('project_name')}")
|
||||
print(f"- **Project Goal**: {goal}")
|
||||
print(f"- **Project Level**: {level}")
|
||||
print(f"- **Active Feature**: {current_feature or 'None (Ready for new task)'}")
|
||||
|
||||
if current_feature:
|
||||
feature_data = state.get('features', {}).get(current_feature, {})
|
||||
print(f"- **Current Phase**: {feature_data.get('phase', 'PLAN')}")
|
||||
print(f"- **Match Rate**: {feature_data.get('match_rate', 0)}%")
|
||||
|
||||
print(f"\n### 🎯 Recommended Next Action")
|
||||
if not current_feature:
|
||||
print("1. 상단 `blueprint.md`를 읽고 프로젝트의 현황을 파악하세요.")
|
||||
print("2. 사용자에게 다음에 개발할 기능(Feature)이 무엇인지 물어보거나, `blueprint.md`에 명시된 할 일을 제안하세요.")
|
||||
else:
|
||||
phase = state.get('features', {}).get(current_feature, {}).get('phase', 'PLAN')
|
||||
print(f"1. 현재 활성화된 기능 `{current_feature}`의 `{phase}` 단계를 계속 진행하세요.")
|
||||
print(f"2. 만약 `{phase}` 단계가 완료되었다면, `Check` 단계를 거쳐 다음 단계로 전이(Transition)를 제안하세요.")
|
||||
|
||||
guard = Guardrails(ctx)
|
||||
loop_check = guard.check_loop(current_feature)
|
||||
if not loop_check['safe']:
|
||||
print(f"\n 🚨 [루프 차단 가드레일]: {loop_check['reason']}")
|
||||
|
||||
stale = detect_stale_features(ctx)
|
||||
if stale:
|
||||
print("\n### ⚠️ Stale Feature 경고")
|
||||
for s in stale:
|
||||
print(f"- `{s['feature']}`: {s['daysIdle']}일 동안 변경 사항이 없습니다. (현재: {s['phase']})")
|
||||
|
||||
print("\n" + "="*60 + "\n")
|
||||
|
||||
def sync_skills_to_native_ide():
|
||||
"""
|
||||
Syncs HMCMkit skills to the native .agent/skills directory.
|
||||
Uses Symlink first, fallbacks to Copy on error.
|
||||
"""
|
||||
src_dir = os.path.join(".hmcmkit", "knowledge", "skills")
|
||||
dest_dir = os.path.join(".agents", "skills")
|
||||
|
||||
if not os.path.exists(src_dir):
|
||||
return
|
||||
|
||||
if not os.path.exists(dest_dir):
|
||||
os.makedirs(dest_dir, exist_ok=True)
|
||||
|
||||
print(f"🔄 Syncing skills to native IDE: {dest_dir}")
|
||||
|
||||
for item in os.listdir(src_dir):
|
||||
s_path = os.path.abspath(os.path.join(src_dir, item))
|
||||
d_path = os.path.abspath(os.path.join(dest_dir, item))
|
||||
|
||||
# Cleanup existing
|
||||
if os.path.exists(d_path) or os.path.islink(d_path):
|
||||
try:
|
||||
if os.path.islink(d_path) or os.path.isfile(d_path):
|
||||
os.remove(d_path)
|
||||
elif os.path.isdir(d_path):
|
||||
shutil.rmtree(d_path)
|
||||
except Exception as e:
|
||||
print(f" ⚠️ Could not cleanup {item}: {e}")
|
||||
continue
|
||||
|
||||
# Try Symlink first
|
||||
try:
|
||||
os.symlink(s_path, d_path, target_is_directory=os.path.isdir(s_path))
|
||||
print(f" [LINK] {item}")
|
||||
except Exception as e:
|
||||
# Fallback to Copy
|
||||
try:
|
||||
if os.path.isdir(s_path):
|
||||
shutil.copytree(s_path, d_path)
|
||||
else:
|
||||
shutil.copy2(s_path, d_path)
|
||||
print(f" [COPY] {item} (Symlink failed: {e})")
|
||||
except Exception as copy_e:
|
||||
print(f" ⚠️ Failed to sync {item}: {copy_e}")
|
||||
|
||||
def bootup_check():
|
||||
"""
|
||||
Simulates hooks/session-start.js logic.
|
||||
Checks if .hmcmkit environment is valid and updates last_access timestamp.
|
||||
"""
|
||||
hmcmkit_root = ".hmcmkit"
|
||||
if not os.path.exists(hmcmkit_root):
|
||||
print(f"Error: {hmcmkit_root} directory not found. Please initialize first.")
|
||||
return False
|
||||
|
||||
# Check core files
|
||||
required_files = [
|
||||
os.path.join(hmcmkit_root, "config.json"),
|
||||
os.path.join(hmcmkit_root, "state.json")
|
||||
]
|
||||
|
||||
missing = [f for f in required_files if not os.path.exists(f)]
|
||||
if missing:
|
||||
print(f"❌ Error: Missing core files: {missing}")
|
||||
return False
|
||||
|
||||
# v1.0.2: Auto-generate Index if missing
|
||||
index_path = os.path.join(hmcmkit_root, "system", "_GRAPH-INDEX.md")
|
||||
if not os.path.exists(index_path):
|
||||
print("🔍 Knowledge Index missing. Generating...")
|
||||
try:
|
||||
from graph_index import generate_graph_index
|
||||
generate_graph_index(hmcmkit_root)
|
||||
except Exception as e:
|
||||
print(f"⚠️ Warning: Could not generate Knowledge Index: {e}")
|
||||
|
||||
# Update state last_access
|
||||
ctx = ContextManager(os.path.join(hmcmkit_root, "state.json"))
|
||||
ctx.state['last_bootup'] = datetime.datetime.now().isoformat()
|
||||
ctx._save_state()
|
||||
|
||||
# Auto-detect Project Level
|
||||
level = ctx.detect_project_level()
|
||||
ctx._save_state()
|
||||
|
||||
# v1.0.1: Ensure backup dir exists
|
||||
backup_dir = os.path.join(hmcmkit_root, "backups")
|
||||
if not os.path.exists(backup_dir):
|
||||
os.makedirs(backup_dir)
|
||||
|
||||
# NATIVE SKILL SYNC
|
||||
sync_skills_to_native_ide()
|
||||
|
||||
# Check for active feature
|
||||
current_feature = ctx.get_active_feature()
|
||||
|
||||
print(f"\n✅ System Ready (Level: {level})")
|
||||
print(f" Active Feature: {current_feature or 'None'}")
|
||||
|
||||
# DYNAMIC CONTEXT INJECTION
|
||||
print_agent_context_injection(ctx)
|
||||
|
||||
print("\n [Usage Hints]")
|
||||
print(" - Plan : 'I need a feature...'")
|
||||
print(" - Do : 'Implement the feature...'")
|
||||
print(" - Check: 'Verify my work...'")
|
||||
return True
|
||||
|
||||
if __name__ == "__main__":
|
||||
bootup_check()
|
||||
@@ -0,0 +1,88 @@
|
||||
import sys
|
||||
import re
|
||||
import json
|
||||
|
||||
def extract_checklist(markdown_content):
|
||||
checklist = []
|
||||
lines = markdown_content.split('\n')
|
||||
current_section = "General"
|
||||
in_table = False
|
||||
headers = []
|
||||
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
|
||||
# Detect headers as sections
|
||||
header_match = re.match(r'^(#{1,6})\s+(.+)', line)
|
||||
if header_match:
|
||||
current_section = header_match.group(2).strip()
|
||||
in_table = False
|
||||
continue
|
||||
|
||||
# Detect existing checkboxes
|
||||
msg_match = re.match(r'^\s*-\s*\[([ x])\]\s+(.+)', line)
|
||||
if msg_match:
|
||||
status = "completed" if msg_match.group(1) == 'x' else "pending"
|
||||
checklist.append({
|
||||
"section": current_section,
|
||||
"item": msg_match.group(2).strip(),
|
||||
"type": "checkbox",
|
||||
"status": status
|
||||
})
|
||||
continue
|
||||
|
||||
# Detect table start
|
||||
if '|' in line and not in_table:
|
||||
# check if it looks like a header row
|
||||
parts = [p.strip() for p in line.split('|') if p.strip()]
|
||||
if len(parts) > 1:
|
||||
headers = parts
|
||||
in_table = True
|
||||
continue
|
||||
|
||||
# Detect table separator
|
||||
if in_table and set(stripped.replace('|', '').replace('-', '').replace(' ', '')) == set():
|
||||
continue
|
||||
|
||||
# Detect table rows
|
||||
if in_table and '|' in line:
|
||||
parts = [p.strip() for p in line.split('|') if p.strip()]
|
||||
if len(parts) == len(headers):
|
||||
# Heuristic: If first column looks like a File/ID/Method, treat as requirement
|
||||
item_text = f"{parts[0]}: {parts[1]}" if len(parts) >= 2 else parts[0]
|
||||
checklist.append({
|
||||
"section": current_section,
|
||||
"item": item_text,
|
||||
"type": "table_row",
|
||||
"status": "pending"
|
||||
})
|
||||
elif not stripped:
|
||||
in_table = False
|
||||
|
||||
return checklist
|
||||
|
||||
def generate_verification_plan(checklist):
|
||||
output = []
|
||||
output.append("# Generated Gap Analysis Checklist")
|
||||
output.append("\n## Gap Analysis Matrix\n")
|
||||
output.append("| Category | Requirement | Implementation Status | Notes |")
|
||||
output.append("|---|---|---|---|")
|
||||
|
||||
for item in checklist:
|
||||
output.append(f"| {item['section']} | {item['item']} | Pending | |")
|
||||
|
||||
return "\n".join(output)
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python checklist_generator.py <input_md_file>")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
with open(sys.argv[1], 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
checklist = extract_checklist(content)
|
||||
print(generate_verification_plan(checklist))
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
sys.exit(1)
|
||||
@@ -0,0 +1,43 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
def generate_graph_index(root_path=".hmcmkit"):
|
||||
index_content = ["# HMCMkit Knowledge Graph Index", "", "> Automatically generated index of all knowledge assets.", ""]
|
||||
|
||||
# Simple tree walk
|
||||
for root, dirs, files in os.walk(root_path):
|
||||
# Filter hidden
|
||||
dirs[:] = [d for d in dirs if not d.startswith('.')]
|
||||
files = [f for f in files if not f.startswith('.')]
|
||||
|
||||
rel_path = os.path.relpath(root, root_path)
|
||||
if rel_path == ".":
|
||||
level = 0
|
||||
rel_path = ""
|
||||
else:
|
||||
level = rel_path.count(os.sep) + 1
|
||||
|
||||
indent = " " * level
|
||||
folder_name = os.path.basename(root)
|
||||
if folder_name != ".hmcmkit" and rel_path != "":
|
||||
index_content.append(f"{indent}- 📂 **{folder_name}/**")
|
||||
|
||||
sub_indent = " " * (level + 1)
|
||||
for f in files:
|
||||
if f.endswith(".md") or f.endswith(".json") or f.endswith(".py"):
|
||||
link_path = os.path.join(root, f).replace("\\", "/") # POSIX style for links
|
||||
# Make link relative to where _GRAPH-INDEX.md will be (in system/)
|
||||
# Actually, let's just use the path from project root for clarity in Antigravity context
|
||||
index_content.append(f"{sub_indent}- [{f}]({link_path})")
|
||||
|
||||
output_path = os.path.join(root_path, "system", "_GRAPH-INDEX.md")
|
||||
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
||||
|
||||
with open(output_path, 'w', encoding='utf-8') as f:
|
||||
f.write("\n".join(index_content))
|
||||
|
||||
print(f"✅ Generated Graph Index at: {output_path}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
target = sys.argv[1] if len(sys.argv) > 1 else ".hmcmkit"
|
||||
generate_graph_index(target)
|
||||
@@ -0,0 +1,86 @@
|
||||
import os
|
||||
import json
|
||||
|
||||
def init_environment():
|
||||
"""
|
||||
Resets the HMCMkit environment to its initial state.
|
||||
- state.json is reset to defaults.
|
||||
- blueprint.md is reset to a baseline template.
|
||||
"""
|
||||
hmcmkit_root = ".hmcmkit"
|
||||
|
||||
if not os.path.exists(hmcmkit_root):
|
||||
print(f"Error: {hmcmkit_root} directory not found.")
|
||||
return
|
||||
|
||||
state_path = os.path.join(hmcmkit_root, "state.json")
|
||||
blueprint_path = os.path.join(hmcmkit_root, "blueprint.md")
|
||||
|
||||
# 1. Reset state.json
|
||||
default_state = {
|
||||
"project_name": "New HMCMkit Project",
|
||||
"current_feature": None,
|
||||
"features": {},
|
||||
"team_config": {
|
||||
"enabled": False,
|
||||
"max_teammates": 4
|
||||
},
|
||||
"last_bootup": None,
|
||||
"project_level": "Starter"
|
||||
}
|
||||
|
||||
try:
|
||||
with open(state_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(default_state, f, indent=2, ensure_ascii=False)
|
||||
print(f"✅ Reset state: {state_path}")
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to reset state: {e}")
|
||||
|
||||
# 2. Reset blueprint.md
|
||||
blueprint_template = """# Project Blueprint (Domain Knowledge Layer)
|
||||
|
||||
> **Role**: The single source of truth for "What" we are building.
|
||||
> **Usage**: Read this file at the start of every session to understand the project context.
|
||||
|
||||
## 1. Project Overview
|
||||
- **Name**: New HMCMkit Project
|
||||
- **Goal**: [Define your project goal here]
|
||||
- **Tech Stack**: [List your technology stack]
|
||||
|
||||
## 2. Architecture Diagram (Mermaid)
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
User[User] -->|Request| Antigravity[Antigravity Agent]
|
||||
|
||||
subgraph Context_Layer [.hmcmkit/]
|
||||
Blueprint[blueprint.md]
|
||||
Governance[governance.md]
|
||||
State[state.json]
|
||||
end
|
||||
|
||||
Antigravity -->|Read Context| Context_Layer
|
||||
```
|
||||
|
||||
## 3. Data Models
|
||||
[Define your core data models and entities]
|
||||
|
||||
## 4. API Specifications
|
||||
[List your API endpoints if applicable]
|
||||
|
||||
## 5. Conventions & Standards
|
||||
- **Naming**: kebab-case for files, PascalCase for components.
|
||||
- **Language**: English for code, Korean for user communication.
|
||||
"""
|
||||
|
||||
try:
|
||||
with open(blueprint_path, 'w', encoding='utf-8') as f:
|
||||
f.write(blueprint_template)
|
||||
print(f"✅ Reset blueprint: {blueprint_path}")
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to reset blueprint: {e}")
|
||||
|
||||
print("\n🚀 HMCMkit environment has been initialized.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
init_environment()
|
||||
@@ -0,0 +1,65 @@
|
||||
import re
|
||||
import sys
|
||||
import json
|
||||
|
||||
SUPPORTED_LANGUAGES = ['en', 'ko', 'ja', 'zh', 'es', 'fr', 'de', 'it']
|
||||
|
||||
AGENT_TRIGGER_PATTERNS = {
|
||||
'gap-detector': {
|
||||
'en': ['verify', 'check', 'gap', 'compare', 'validate'],
|
||||
'ko': ['검증', '확인', '갭', '비교', '검사', '맞아?', '이거 괜찮아?'],
|
||||
},
|
||||
'pdca-iterator': {
|
||||
'en': ['improve', 'iterate', 'fix', 'auto-fix', 'optimize'],
|
||||
'ko': ['개선', '반복', '수정', '자동 수정', '고쳐줘', '개선해줘'],
|
||||
},
|
||||
'cto-lead': {
|
||||
'en': ['team', 'project lead', 'CTO', 'team mode', 'coordinate team'],
|
||||
'ko': ['팀', '팀장', '프로젝트 리드', 'CTO', '팀 구성', '팀 모드'],
|
||||
},
|
||||
'bkend-expert': {
|
||||
'en': ['bkend', 'BaaS', 'backend service', 'database setup', 'user auth', 'file upload', 'REST API', 'signup feature', 'login feature'],
|
||||
'ko': ['bkend', '백엔드 서비스', '데이터베이스 설정', '사용자 인증', '파일 업로드', '회원가입 기능', '로그인 기능'],
|
||||
},
|
||||
'code-analyzer': {
|
||||
'en': ['analyze', 'quality', 'security', 'code review', 'any issues?'],
|
||||
'ko': ['분석', '품질', '보안', '코드 리뷰', '이상해', '뭔가 이상해'],
|
||||
},
|
||||
'starter-guide': {
|
||||
'en': ['help', 'beginner', 'first time', 'how to', 'explain'],
|
||||
'ko': ['도움', '초보자', '처음', '어떻게', '설명해', '모르겠'],
|
||||
}
|
||||
}
|
||||
|
||||
def detect_language(text):
|
||||
if not text: return 'en'
|
||||
if re.search(r'[\uAC00-\uD7AF]', text): return 'ko'
|
||||
if re.search(r'[\u3040-\u30FF]', text): return 'ja'
|
||||
return 'en'
|
||||
|
||||
def detect_intent(text):
|
||||
lang = detect_language(text)
|
||||
detected_agents = []
|
||||
|
||||
lower_text = text.lower()
|
||||
|
||||
for agent, patterns in AGENT_TRIGGER_PATTERNS.items():
|
||||
# Check specific lang patterns
|
||||
lang_patterns = patterns.get(lang, []) + patterns.get('en', [])
|
||||
for pattern in lang_patterns:
|
||||
if pattern.lower() in lower_text:
|
||||
detected_agents.append(agent)
|
||||
break
|
||||
|
||||
return {
|
||||
"language": lang,
|
||||
"suggested_agents": detected_agents
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print(json.dumps({"error": "No text provided"}))
|
||||
sys.exit(1)
|
||||
|
||||
result = detect_intent(sys.argv[1])
|
||||
print(json.dumps(result, indent=2, ensure_ascii=False))
|
||||
@@ -0,0 +1,96 @@
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
import json
|
||||
import datetime
|
||||
|
||||
# Configuration
|
||||
HMCMKIT_ROOT = ".hmcmkit"
|
||||
CONFIG_PATH = os.path.join(HMCMKIT_ROOT, "config.json")
|
||||
TEMPLATE_DIR = os.path.join(HMCMKIT_ROOT, "knowledge", "templates")
|
||||
|
||||
def load_config():
|
||||
with open(CONFIG_PATH, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
|
||||
def get_target_path(config, phase, feature_name):
|
||||
"""
|
||||
Resolves the target path based on config.json 'pdca' section.
|
||||
"""
|
||||
pdca_config = config.get('pdca', {})
|
||||
path_key = f"{phase.lower()}DocPaths" # e.g. planDocPaths
|
||||
|
||||
paths = pdca_config.get(path_key, [])
|
||||
if not paths:
|
||||
# Fallback defaults
|
||||
defaults = {
|
||||
"plan": f"docs/01-plan/{feature_name}.plan.md",
|
||||
"design": f"docs/02-design/{feature_name}.design.md",
|
||||
"gap-analysis": f"docs/03-check/{feature_name}.gap.md",
|
||||
"report": f"docs/04-act/{feature_name}.report.md"
|
||||
}
|
||||
return defaults.get(phase.lower(), f"docs/{phase}/{feature_name}.md")
|
||||
|
||||
# Use the first configured path
|
||||
first_path = paths[0]
|
||||
return first_path.replace("{feature}", feature_name).replace("{type}", phase)
|
||||
|
||||
def scaffold(phase, feature_name):
|
||||
config = load_config()
|
||||
|
||||
# 1. Identify Template
|
||||
template_types = config.get('templates', {}).get('types', {})
|
||||
template_file = template_types.get(phase.lower())
|
||||
|
||||
if not template_file:
|
||||
print(f"❌ Error: No template defined for phase '{phase}'")
|
||||
return False
|
||||
|
||||
template_path = os.path.join(TEMPLATE_DIR, template_file)
|
||||
if not os.path.exists(template_path):
|
||||
# Try looking for Level Variants (e.g. plan-starter.template.md)
|
||||
# For now, just error
|
||||
print(f"❌ Error: Template file not found at {template_path}")
|
||||
return False
|
||||
|
||||
# 2. Identify Target Path
|
||||
target_path = get_target_path(config, phase, feature_name)
|
||||
|
||||
# 3. Create Directory
|
||||
os.makedirs(os.path.dirname(target_path), exist_ok=True)
|
||||
|
||||
if os.path.exists(target_path):
|
||||
print(f"⚠️ Target file already exists: {target_path}")
|
||||
# v1.0.1: Auto Snapshot
|
||||
try:
|
||||
from snapshot import create_snapshot
|
||||
create_snapshot(target_path)
|
||||
print(f" (Backup created before potential overwrite)")
|
||||
except ImportError:
|
||||
pass
|
||||
return False
|
||||
|
||||
# 4. Read & process Template
|
||||
with open(template_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# Simple Variable Replacement
|
||||
content = content.replace("{{feature}}", feature_name)
|
||||
content = content.replace("{{date}}", datetime.datetime.now().strftime("%Y-%m-%d"))
|
||||
|
||||
# 5. Write File
|
||||
with open(target_path, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
|
||||
print(f"✅ Created {phase.upper()} document: {target_path}")
|
||||
return True
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 3:
|
||||
print("Usage: python scaffold.py <phase> <feature_name>")
|
||||
print("Phases: plan, design, gap-analysis, report")
|
||||
sys.exit(1)
|
||||
|
||||
phase = sys.argv[1]
|
||||
feature = sys.argv[2]
|
||||
scaffold(phase, feature)
|
||||
@@ -0,0 +1,38 @@
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
import datetime
|
||||
|
||||
def create_snapshot(file_path):
|
||||
"""
|
||||
Creates a timestamped backup of the given file in .hmcmkit/backups/
|
||||
"""
|
||||
if not os.path.exists(file_path):
|
||||
print(f"❌ Error: File not found: {file_path}")
|
||||
return None
|
||||
|
||||
# Determine paths
|
||||
hmcmkit_root = ".hmcmkit"
|
||||
backup_dir = os.path.join(hmcmkit_root, "backups")
|
||||
os.makedirs(backup_dir, exist_ok=True)
|
||||
|
||||
# Generate timestamped name
|
||||
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
filename = os.path.basename(file_path)
|
||||
backup_name = f"{timestamp}_{filename}"
|
||||
backup_path = os.path.join(backup_dir, backup_name)
|
||||
|
||||
try:
|
||||
shutil.copy2(file_path, backup_path)
|
||||
print(f"📦 Snapshot created: {backup_path}")
|
||||
return backup_path
|
||||
except Exception as e:
|
||||
print(f"❌ Snapshot failed: {e}")
|
||||
return None
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python snapshot.py <file_path>")
|
||||
sys.exit(1)
|
||||
|
||||
create_snapshot(sys.argv[1])
|
||||
@@ -0,0 +1,283 @@
|
||||
import os
|
||||
import json
|
||||
import datetime
|
||||
import re
|
||||
|
||||
# ==========================================
|
||||
# 1. Constants & Configuration (Ported from lib/core & lib/pdca)
|
||||
# ==========================================
|
||||
|
||||
PDCA_PHASES = {
|
||||
'PLAN': {'order': 1, 'name': 'Plan', 'icon': '📋'},
|
||||
'DESIGN': {'order': 2, 'name': 'Design', 'icon': '📐'},
|
||||
'DO': {'order': 3, 'name': 'Do', 'icon': '🔨'},
|
||||
'CHECK': {'order': 4, 'name': 'Check', 'icon': '🔍'},
|
||||
'ACT': {'order': 5, 'name': 'Act', 'icon': '🔄'},
|
||||
'REPORT': {'order': 6, 'name': 'Report', 'icon': '📊'}
|
||||
}
|
||||
|
||||
LEVEL_PHASE_MAP = {
|
||||
"Starter": {
|
||||
"required": ["PLAN", "DO", "CHECK"],
|
||||
"optional": ["DESIGN"],
|
||||
"skip": ["PHASE-1", "PHASE-2", "PHASE-4", "PHASE-7", "PHASE-9"]
|
||||
},
|
||||
"Dynamic": {
|
||||
"required": ["PLAN", "DESIGN", "DO", "CHECK", "REPORT"],
|
||||
"optional": ["PHASE-3", "PHASE-5", "PHASE-6"],
|
||||
"skip": ["PHASE-9"]
|
||||
},
|
||||
"Enterprise": {
|
||||
"required": ["PLAN", "DESIGN", "DO", "CHECK", "REPORT",
|
||||
"PHASE-1", "PHASE-2", "PHASE-3", "PHASE-4",
|
||||
"PHASE-5", "PHASE-6", "PHASE-7", "PHASE-8", "PHASE-9"],
|
||||
"optional": [],
|
||||
"skip": []
|
||||
}
|
||||
}
|
||||
|
||||
TEAM_STRATEGIES = {
|
||||
"Starter": None,
|
||||
"Dynamic": {
|
||||
"teammates": 3,
|
||||
"cto_agent": "cto-lead",
|
||||
"roles": [
|
||||
{"name": "developer", "agents": ["bkend-expert"], "phases": ["DO", "ACT"]},
|
||||
{"name": "frontend", "agents": ["frontend-architect"], "phases": ["DESIGN", "DO"]},
|
||||
{"name": "qa", "agents": ["qa-monitor", "gap-detector"], "phases": ["CHECK"]}
|
||||
]
|
||||
},
|
||||
"Enterprise": {
|
||||
"teammates": 5,
|
||||
"cto_agent": "cto-lead",
|
||||
"roles": [
|
||||
{"name": "architect", "agents": ["enterprise-expert", "infra-architect"], "phases": ["DESIGN"]},
|
||||
{"name": "developer", "agents": ["bkend-expert", "frontend-architect"], "phases": ["DO", "ACT"]},
|
||||
{"name": "qa", "agents": ["qa-strategist", "qa-monitor", "gap-detector"], "phases": ["CHECK"]},
|
||||
{"name": "reviewer", "agents": ["code-analyzer", "design-validator"], "phases": ["CHECK", "ACT"]},
|
||||
{"name": "security", "agents": ["security-architect"], "phases": ["DESIGN", "CHECK"]}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
# ==========================================
|
||||
# 2. State Engine Logic
|
||||
# ==========================================
|
||||
|
||||
class ContextManager:
|
||||
"""
|
||||
Manages global state, feature context, and project level detection.
|
||||
Ported from lib/task/context.js & lib/pdca/level.js
|
||||
"""
|
||||
def __init__(self, state_file_path):
|
||||
self.state_file_path = state_file_path
|
||||
self.state = self._load_state()
|
||||
|
||||
def _load_state(self):
|
||||
if not os.path.exists(self.state_file_path):
|
||||
return {
|
||||
"project_name": "Antigravity Project",
|
||||
"project_level": "Dynamic", # Default
|
||||
"current_feature": None,
|
||||
"features": {},
|
||||
"team_config": {
|
||||
"enabled": False,
|
||||
"max_teammates": 4
|
||||
},
|
||||
"last_bootup": None
|
||||
}
|
||||
try:
|
||||
with open(self.state_file_path, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
except json.JSONDecodeError:
|
||||
return self._load_state() # Recursive retry with default if corrupt (handled by external reset usually)
|
||||
|
||||
def _save_state(self):
|
||||
with open(self.state_file_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(self.state, f, indent=2, ensure_ascii=False)
|
||||
|
||||
def detect_project_level(self, project_root="."):
|
||||
"""
|
||||
Ported from lib/pdca/level.js: detectLevel()
|
||||
"""
|
||||
try:
|
||||
files = os.listdir(project_root)
|
||||
files_str = " ".join(files).lower()
|
||||
|
||||
# Enterprise Markers
|
||||
if any(m in files_str for m in ['kubernetes', 'k8s', 'terraform', 'microservices', 'docker-compose']):
|
||||
self.state['project_level'] = "Enterprise"
|
||||
return "Enterprise"
|
||||
|
||||
# Dynamic Markers
|
||||
if any(m in files for m in ['package.json', 'requirements.txt', 'go.mod', 'Cargo.toml', 'pom.xml']):
|
||||
self.state['project_level'] = "Dynamic"
|
||||
return "Dynamic"
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.state['project_level'] = "Starter"
|
||||
return "Starter"
|
||||
|
||||
def set_active_feature(self, feature_name):
|
||||
self.state['current_feature'] = feature_name
|
||||
if feature_name and feature_name not in self.state['features']:
|
||||
self.state['features'][feature_name] = {
|
||||
"phase": "PLAN",
|
||||
"match_rate": 0,
|
||||
"history": []
|
||||
}
|
||||
self._save_state()
|
||||
|
||||
def get_active_feature(self):
|
||||
return self.state.get('current_feature')
|
||||
|
||||
def get_feature_status(self, feature_name):
|
||||
return self.state.get('features', {}).get(feature_name)
|
||||
|
||||
|
||||
class PDCAStatus:
|
||||
"""
|
||||
Manages Phase Transitions and Deliverable Checks.
|
||||
Ported from lib/pdca/phase.js
|
||||
"""
|
||||
def __init__(self, context_manager):
|
||||
self.ctx = context_manager
|
||||
|
||||
def validate_transition(self, feature_name, to_phase):
|
||||
"""
|
||||
Ported from lib/pdca/phase.js: validatePdcaTransition()
|
||||
NOW INCLUDES LEVEL-BASED VALIDATION
|
||||
"""
|
||||
feature = self.ctx.get_feature_status(feature_name)
|
||||
if not feature:
|
||||
return {"valid": True, "reason": "New feature"}
|
||||
|
||||
from_phase = feature.get('phase', 'PLAN').upper()
|
||||
to_phase = to_phase.upper()
|
||||
|
||||
from_order = PDCA_PHASES.get(from_phase, {}).get('order', 0)
|
||||
to_order = PDCA_PHASES.get(to_phase, {}).get('order', 0)
|
||||
|
||||
# Allow going back
|
||||
if to_order < from_order:
|
||||
return {"valid": True, "reason": "Returning to earlier phase"}
|
||||
|
||||
# Check Level Requirements
|
||||
level = self.ctx.state.get("project_level", "Dynamic")
|
||||
level_config = LEVEL_PHASE_MAP.get(level, LEVEL_PHASE_MAP["Dynamic"])
|
||||
|
||||
# Iterate phases between from_phase and to_phase
|
||||
# e.g. PLAN(1) -> DO(3). Check if DESIGN(2) is required.
|
||||
for i in range(from_order + 1, to_order):
|
||||
intermediate_phase_name = ""
|
||||
for name, info in PDCA_PHASES.items():
|
||||
if info['order'] == i:
|
||||
intermediate_phase_name = name
|
||||
break
|
||||
|
||||
if intermediate_phase_name and intermediate_phase_name in level_config['required']:
|
||||
return {"valid": False, "reason": f"Cannot skip required phase: {intermediate_phase_name} ({level} Level)"}
|
||||
|
||||
# Cannot skip more than 1 phase generally (sanity check)
|
||||
if to_order > from_order + 1:
|
||||
# But allow if intermediate is NOT required
|
||||
# The loop above handles the "Required" check.
|
||||
# If we are here, it means intermediate phases were optional or skipped.
|
||||
pass
|
||||
|
||||
return {"valid": True, "reason": "Transition allowed"}
|
||||
|
||||
def update_phase(self, feature_name, phase, match_rate=None):
|
||||
phase = phase.upper() # Normalize
|
||||
|
||||
# Validate first
|
||||
validation = self.validate_transition(feature_name, phase)
|
||||
if not validation['valid']:
|
||||
print(f"Warning: Invalid transition - {validation['reason']}")
|
||||
# We enforce it anyway for now as agent override, but log it
|
||||
|
||||
feature = self.ctx.get_feature_status(feature_name)
|
||||
if feature:
|
||||
feature['phase'] = phase
|
||||
if match_rate is not None:
|
||||
feature['match_rate'] = match_rate
|
||||
|
||||
entry = {
|
||||
"timestamp": datetime.datetime.now().isoformat(),
|
||||
"action": "phase_update",
|
||||
"phase": phase,
|
||||
"match_rate": match_rate
|
||||
}
|
||||
feature['history'].append(entry)
|
||||
self.ctx._save_state()
|
||||
|
||||
class Guardrails:
|
||||
"""
|
||||
Implements safety checks like Loop Detection.
|
||||
Ported from hmcmkit (Hybrid JS -> Python approach)
|
||||
"""
|
||||
def __init__(self, context_manager):
|
||||
self.ctx = context_manager
|
||||
|
||||
def check_loop(self, feature_name):
|
||||
feature = self.ctx.get_feature_status(feature_name)
|
||||
if not feature or 'history' not in feature:
|
||||
return {"safe": True}
|
||||
|
||||
recent = feature['history'][-10:] if len(feature['history']) >= 10 else feature['history']
|
||||
check_act_count = sum(1 for h in recent if h.get('phase', '') in ['CHECK', 'ACT'])
|
||||
|
||||
if check_act_count >= 8: # 4 cycles of check-act
|
||||
return {"safe": False, "reason": "무한 루프 감지: CHECK/ACT 반복이 4회를 초과했습니다. 자동 수정을 멈추고 사용자에게 보고하세요."}
|
||||
|
||||
return {"safe": True}
|
||||
|
||||
|
||||
class TeamCoordinator:
|
||||
"""
|
||||
Manages Team Strategy and Roles.
|
||||
Ported from lib/team/strategy.js & lib/team/coordinator.js
|
||||
"""
|
||||
def __init__(self, context_manager):
|
||||
self.ctx = context_manager
|
||||
|
||||
def get_team_strategy(self):
|
||||
level = self.ctx.state.get("project_level", "Dynamic")
|
||||
return TEAM_STRATEGIES.get(level, TEAM_STRATEGIES["Dynamic"])
|
||||
|
||||
def suggest_team_mode(self, user_message):
|
||||
"""
|
||||
Ported from lib/team/coordinator.js: suggestTeamMode()
|
||||
"""
|
||||
level = self.ctx.state.get("project_level", "Dynamic")
|
||||
|
||||
if level == "Starter":
|
||||
return {"suggest": False, "reason": "Starter level does not support teams"}
|
||||
|
||||
# Logic: Message > 1000 chars or Keyword trigger
|
||||
if len(user_message) >= 1000:
|
||||
return {"suggest": True, "reason": "Long message detected (Major Feature)"}
|
||||
|
||||
keywords = ["team", "project lead", "cto", "role", "assign"]
|
||||
if any(k in user_message.lower() for k in keywords):
|
||||
return {"suggest": True, "reason": "Team keywords detected"}
|
||||
|
||||
return {"suggest": False}
|
||||
|
||||
def get_roles_for_phase(self, phase):
|
||||
strategy = self.get_team_strategy()
|
||||
if not strategy: return []
|
||||
|
||||
phase = phase.upper()
|
||||
active_roles = []
|
||||
for role in strategy['roles']:
|
||||
if phase in role['phases']:
|
||||
active_roles.append(role)
|
||||
return active_roles
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Test Suite
|
||||
# mgr = ContextManager(".hmcmkit/state.json")
|
||||
pass
|
||||
Reference in new issue
Block a user