htmlgraph 0.26.5__py3-none-any.whl → 0.26.7__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.
- htmlgraph/.htmlgraph/.session-warning-state.json +1 -1
- htmlgraph/__init__.py +1 -1
- htmlgraph/api/main.py +50 -10
- htmlgraph/api/templates/dashboard-redesign.html +608 -54
- htmlgraph/api/templates/partials/activity-feed.html +21 -0
- htmlgraph/api/templates/partials/features.html +81 -12
- htmlgraph/api/templates/partials/orchestration.html +35 -0
- htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/cli/.htmlgraph/agents.json +72 -0
- htmlgraph/cli/__init__.py +42 -0
- htmlgraph/cli/__main__.py +6 -0
- htmlgraph/cli/analytics.py +939 -0
- htmlgraph/cli/base.py +660 -0
- htmlgraph/cli/constants.py +206 -0
- htmlgraph/cli/core.py +856 -0
- htmlgraph/cli/main.py +143 -0
- htmlgraph/cli/models.py +462 -0
- htmlgraph/cli/templates/__init__.py +1 -0
- htmlgraph/cli/templates/cost_dashboard.py +398 -0
- htmlgraph/cli/work/__init__.py +159 -0
- htmlgraph/cli/work/features.py +567 -0
- htmlgraph/cli/work/orchestration.py +675 -0
- htmlgraph/cli/work/sessions.py +465 -0
- htmlgraph/cli/work/tracks.py +485 -0
- htmlgraph/dashboard.html +6414 -634
- htmlgraph/db/schema.py +8 -3
- htmlgraph/docs/ORCHESTRATION_PATTERNS.md +20 -13
- htmlgraph/docs/README.md +2 -3
- htmlgraph/hooks/event_tracker.py +355 -26
- htmlgraph/hooks/git_commands.py +175 -0
- htmlgraph/hooks/orchestrator.py +137 -71
- htmlgraph/hooks/orchestrator_reflector.py +23 -0
- htmlgraph/hooks/pretooluse.py +29 -6
- htmlgraph/hooks/session_handler.py +28 -0
- htmlgraph/hooks/session_summary.py +391 -0
- htmlgraph/hooks/subagent_detection.py +202 -0
- htmlgraph/hooks/subagent_stop.py +71 -12
- htmlgraph/hooks/validator.py +192 -79
- htmlgraph/operations/__init__.py +18 -0
- htmlgraph/operations/initialization.py +596 -0
- htmlgraph/operations/initialization.py.backup +228 -0
- htmlgraph/orchestration/__init__.py +16 -1
- htmlgraph/orchestration/claude_launcher.py +185 -0
- htmlgraph/orchestration/command_builder.py +71 -0
- htmlgraph/orchestration/headless_spawner.py +72 -1332
- htmlgraph/orchestration/plugin_manager.py +136 -0
- htmlgraph/orchestration/prompts.py +137 -0
- htmlgraph/orchestration/spawners/__init__.py +16 -0
- htmlgraph/orchestration/spawners/base.py +194 -0
- htmlgraph/orchestration/spawners/claude.py +170 -0
- htmlgraph/orchestration/spawners/codex.py +442 -0
- htmlgraph/orchestration/spawners/copilot.py +299 -0
- htmlgraph/orchestration/spawners/gemini.py +478 -0
- htmlgraph/orchestration/subprocess_runner.py +33 -0
- htmlgraph/orchestration.md +563 -0
- htmlgraph/orchestrator-system-prompt-optimized.txt +620 -55
- htmlgraph/orchestrator_config.py +357 -0
- htmlgraph/orchestrator_mode.py +45 -12
- htmlgraph/transcript.py +16 -4
- htmlgraph-0.26.7.data/data/htmlgraph/dashboard.html +6592 -0
- {htmlgraph-0.26.5.dist-info → htmlgraph-0.26.7.dist-info}/METADATA +1 -1
- {htmlgraph-0.26.5.dist-info → htmlgraph-0.26.7.dist-info}/RECORD +68 -34
- {htmlgraph-0.26.5.dist-info → htmlgraph-0.26.7.dist-info}/entry_points.txt +1 -1
- htmlgraph/cli.py +0 -7256
- htmlgraph-0.26.5.data/data/htmlgraph/dashboard.html +0 -812
- {htmlgraph-0.26.5.data → htmlgraph-0.26.7.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.26.5.data → htmlgraph-0.26.7.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
- {htmlgraph-0.26.5.data → htmlgraph-0.26.7.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
- {htmlgraph-0.26.5.data → htmlgraph-0.26.7.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
- {htmlgraph-0.26.5.dist-info → htmlgraph-0.26.7.dist-info}/WHEEL +0 -0
htmlgraph/hooks/validator.py
CHANGED
|
@@ -6,6 +6,9 @@ Provides intelligent guidance for HtmlGraph workflow based on:
|
|
|
6
6
|
2. Recent tool usage patterns (anti-pattern detection)
|
|
7
7
|
3. Learned patterns from transcript analytics
|
|
8
8
|
|
|
9
|
+
Subagents spawned via Task() have unrestricted tool access.
|
|
10
|
+
Detection uses 5-level strategy: env vars, session state, database.
|
|
11
|
+
|
|
9
12
|
This module can be used by hook scripts or imported directly for validation logic.
|
|
10
13
|
|
|
11
14
|
Main API:
|
|
@@ -26,35 +29,50 @@ Example:
|
|
|
26
29
|
|
|
27
30
|
import json
|
|
28
31
|
import re
|
|
29
|
-
from datetime import datetime, timezone
|
|
30
32
|
from pathlib import Path
|
|
31
33
|
from typing import Any, cast
|
|
32
34
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
"
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
35
|
+
from htmlgraph.hooks.subagent_detection import is_subagent_context
|
|
36
|
+
from htmlgraph.orchestrator_config import load_orchestrator_config
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_anti_patterns(config: Any | None = None) -> dict[tuple[str, ...], str]:
|
|
40
|
+
"""
|
|
41
|
+
Build anti-pattern rules from configuration.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
config: Optional OrchestratorConfig. If None, loads from file.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Dict mapping tool sequences to warning messages
|
|
48
|
+
"""
|
|
49
|
+
if config is None:
|
|
50
|
+
config = load_orchestrator_config()
|
|
51
|
+
|
|
52
|
+
patterns = config.anti_patterns
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
tuple(["Bash"] * patterns.consecutive_bash): (
|
|
56
|
+
f"{patterns.consecutive_bash} consecutive Bash commands. "
|
|
57
|
+
"Check for errors or consider a different approach."
|
|
58
|
+
),
|
|
59
|
+
tuple(["Edit"] * patterns.consecutive_edit): (
|
|
60
|
+
f"{patterns.consecutive_edit} consecutive Edits. "
|
|
61
|
+
"Consider batching changes or reading file first."
|
|
62
|
+
),
|
|
63
|
+
tuple(["Grep"] * patterns.consecutive_grep): (
|
|
64
|
+
f"{patterns.consecutive_grep} consecutive Greps. "
|
|
65
|
+
"Consider reading results before searching more."
|
|
66
|
+
),
|
|
67
|
+
tuple(["Read"] * patterns.consecutive_read): (
|
|
68
|
+
f"{patterns.consecutive_read} consecutive Reads. "
|
|
69
|
+
"Consider caching file content."
|
|
70
|
+
),
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# Legacy constant for backwards compatibility (now uses config)
|
|
75
|
+
ANTI_PATTERNS = get_anti_patterns()
|
|
58
76
|
|
|
59
77
|
# Tools that indicate exploration/implementation (require work item in strict mode)
|
|
60
78
|
EXPLORATION_TOOLS = {"Grep", "Glob", "Task"}
|
|
@@ -67,68 +85,89 @@ OPTIMAL_PATTERNS = {
|
|
|
67
85
|
("Edit", "Bash"): "Good: Edit then test - verify changes.",
|
|
68
86
|
}
|
|
69
87
|
|
|
70
|
-
#
|
|
71
|
-
TOOL_HISTORY_FILE = Path("/tmp/htmlgraph-tool-history.json")
|
|
88
|
+
# Maximum number of recent tool calls to consider for pattern detection
|
|
72
89
|
MAX_HISTORY = 20
|
|
73
90
|
|
|
74
91
|
|
|
75
|
-
def load_tool_history() -> list[dict]:
|
|
76
|
-
"""
|
|
77
|
-
|
|
78
|
-
try:
|
|
79
|
-
data = json.loads(TOOL_HISTORY_FILE.read_text())
|
|
80
|
-
|
|
81
|
-
# Handle both formats: {"history": [...]} and [...] (legacy)
|
|
82
|
-
if isinstance(data, dict): # type: ignore[arg-type]
|
|
83
|
-
data = data.get("history", [])
|
|
84
|
-
|
|
85
|
-
# Filter to last hour only
|
|
86
|
-
cutoff = datetime.now(timezone.utc).timestamp() - 3600
|
|
87
|
-
|
|
88
|
-
# Handle both "ts" (old) and "timestamp" (new) formats
|
|
89
|
-
filtered = []
|
|
90
|
-
for t in data:
|
|
91
|
-
ts = t.get("ts", 0)
|
|
92
|
-
if not ts and "timestamp" in t:
|
|
93
|
-
# Parse ISO format timestamp
|
|
94
|
-
try:
|
|
95
|
-
ts = datetime.fromisoformat(
|
|
96
|
-
t["timestamp"].replace("Z", "+00:00")
|
|
97
|
-
).timestamp()
|
|
98
|
-
except Exception:
|
|
99
|
-
ts = 0
|
|
100
|
-
if ts > cutoff:
|
|
101
|
-
filtered.append(t)
|
|
102
|
-
|
|
103
|
-
return filtered[-MAX_HISTORY:]
|
|
104
|
-
except Exception:
|
|
105
|
-
pass
|
|
106
|
-
return []
|
|
92
|
+
def load_tool_history(session_id: str) -> list[dict]:
|
|
93
|
+
"""
|
|
94
|
+
Load recent tool history from database (session-isolated).
|
|
107
95
|
|
|
96
|
+
Args:
|
|
97
|
+
session_id: Session identifier to filter tool history
|
|
108
98
|
|
|
109
|
-
|
|
110
|
-
|
|
99
|
+
Returns:
|
|
100
|
+
List of recent tool calls with tool name and timestamp
|
|
101
|
+
"""
|
|
111
102
|
try:
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
103
|
+
from htmlgraph.db.schema import HtmlGraphDB
|
|
104
|
+
|
|
105
|
+
# Find database path
|
|
106
|
+
cwd = Path.cwd()
|
|
107
|
+
graph_dir = cwd / ".htmlgraph"
|
|
108
|
+
if not graph_dir.exists():
|
|
109
|
+
for parent in [cwd.parent, cwd.parent.parent, cwd.parent.parent.parent]:
|
|
110
|
+
candidate = parent / ".htmlgraph"
|
|
111
|
+
if candidate.exists():
|
|
112
|
+
graph_dir = candidate
|
|
113
|
+
break
|
|
114
|
+
|
|
115
|
+
db_path = graph_dir / "htmlgraph.db"
|
|
116
|
+
if not db_path.exists():
|
|
117
|
+
return []
|
|
118
|
+
|
|
119
|
+
db = HtmlGraphDB(str(db_path))
|
|
120
|
+
if db.connection is None:
|
|
121
|
+
return []
|
|
122
|
+
|
|
123
|
+
cursor = db.connection.cursor()
|
|
124
|
+
cursor.execute(
|
|
125
|
+
"""
|
|
126
|
+
SELECT tool_name, timestamp
|
|
127
|
+
FROM agent_events
|
|
128
|
+
WHERE session_id = ?
|
|
129
|
+
ORDER BY timestamp DESC
|
|
130
|
+
LIMIT ?
|
|
131
|
+
""",
|
|
132
|
+
(session_id, MAX_HISTORY),
|
|
115
133
|
)
|
|
134
|
+
|
|
135
|
+
# Return in chronological order (oldest first) for pattern detection
|
|
136
|
+
rows = cursor.fetchall()
|
|
137
|
+
db.disconnect()
|
|
138
|
+
|
|
139
|
+
return [{"tool": row[0], "timestamp": row[1]} for row in reversed(rows)]
|
|
116
140
|
except Exception:
|
|
117
|
-
|
|
141
|
+
# Graceful degradation - return empty history on error
|
|
142
|
+
return []
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def record_tool(tool: str, session_id: str) -> None:
|
|
146
|
+
"""
|
|
147
|
+
Record a tool use in database.
|
|
118
148
|
|
|
149
|
+
Note: This is now handled by track-event.py hook, so this function
|
|
150
|
+
is kept for backward compatibility but does nothing.
|
|
119
151
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
152
|
+
Args:
|
|
153
|
+
tool: Tool name being called
|
|
154
|
+
session_id: Session identifier for isolation
|
|
155
|
+
"""
|
|
156
|
+
# Tool recording is now handled by track-event.py PostToolUse hook
|
|
157
|
+
# This function is kept for backward compatibility but does nothing
|
|
158
|
+
pass
|
|
125
159
|
|
|
126
160
|
|
|
127
161
|
def detect_anti_pattern(tool: str, history: list[dict]) -> str | None:
|
|
128
|
-
"""Check if adding this tool creates an anti-pattern."""
|
|
129
|
-
|
|
162
|
+
"""Check if adding this tool creates an anti-pattern (uses configurable thresholds)."""
|
|
163
|
+
# Load fresh anti-patterns from config
|
|
164
|
+
anti_patterns = get_anti_patterns()
|
|
165
|
+
|
|
166
|
+
# Get max pattern length to know how far to look back
|
|
167
|
+
max_pattern_len = max(len(p) for p in anti_patterns.keys()) if anti_patterns else 5
|
|
168
|
+
recent_tools = [h["tool"] for h in history[-max_pattern_len:]] + [tool]
|
|
130
169
|
|
|
131
|
-
for pattern, message in
|
|
170
|
+
for pattern, message in anti_patterns.items():
|
|
132
171
|
pattern_len = len(pattern)
|
|
133
172
|
if len(recent_tools) >= pattern_len:
|
|
134
173
|
# Check if recent tools end with this pattern
|
|
@@ -229,6 +268,15 @@ def is_always_allowed(
|
|
|
229
268
|
# Read-only Bash patterns
|
|
230
269
|
if tool == "Bash":
|
|
231
270
|
command = params.get("command", "")
|
|
271
|
+
|
|
272
|
+
# Check git commands using shared classification
|
|
273
|
+
if command.strip().startswith("git"):
|
|
274
|
+
from htmlgraph.hooks.git_commands import should_allow_git_command
|
|
275
|
+
|
|
276
|
+
if should_allow_git_command(command):
|
|
277
|
+
return True
|
|
278
|
+
|
|
279
|
+
# Check other bash patterns
|
|
232
280
|
for pattern in config.get("always_allow", {}).get("bash_patterns", []):
|
|
233
281
|
if re.match(pattern, command):
|
|
234
282
|
return True
|
|
@@ -292,7 +340,9 @@ def get_active_work_item() -> dict | None:
|
|
|
292
340
|
return None
|
|
293
341
|
|
|
294
342
|
|
|
295
|
-
def check_orchestrator_violation(
|
|
343
|
+
def check_orchestrator_violation(
|
|
344
|
+
tool: str, params: dict[str, Any], session_id: str = "unknown"
|
|
345
|
+
) -> dict | None:
|
|
296
346
|
"""
|
|
297
347
|
Check if operation violates orchestrator mode rules.
|
|
298
348
|
|
|
@@ -302,6 +352,7 @@ def check_orchestrator_violation(tool: str, params: dict[str, Any]) -> dict | No
|
|
|
302
352
|
Args:
|
|
303
353
|
tool: Tool name
|
|
304
354
|
params: Tool parameters
|
|
355
|
+
session_id: Session identifier for loading tool history
|
|
305
356
|
|
|
306
357
|
Returns:
|
|
307
358
|
Blocking response dict if violation detected in strict mode, None otherwise
|
|
@@ -339,7 +390,9 @@ def check_orchestrator_violation(tool: str, params: dict[str, Any]) -> dict | No
|
|
|
339
390
|
is_allowed_orchestrator_operation,
|
|
340
391
|
)
|
|
341
392
|
|
|
342
|
-
is_allowed, reason, category = is_allowed_orchestrator_operation(
|
|
393
|
+
is_allowed, reason, category = is_allowed_orchestrator_operation(
|
|
394
|
+
tool, params, session_id
|
|
395
|
+
)
|
|
343
396
|
|
|
344
397
|
# If orchestrator would block (but returns continue=True), we block here
|
|
345
398
|
if not is_allowed:
|
|
@@ -367,33 +420,49 @@ def check_orchestrator_violation(tool: str, params: dict[str, Any]) -> dict | No
|
|
|
367
420
|
|
|
368
421
|
|
|
369
422
|
def validate_tool_call(
|
|
370
|
-
tool: str,
|
|
423
|
+
tool: str,
|
|
424
|
+
params: dict[str, Any],
|
|
425
|
+
config: dict[str, Any],
|
|
426
|
+
history: list[dict],
|
|
427
|
+
session_id: str | None = None,
|
|
371
428
|
) -> dict[str, Any]:
|
|
372
429
|
"""
|
|
373
430
|
Validate tool call and return GUIDANCE with active learning.
|
|
374
431
|
|
|
432
|
+
Subagents spawned via Task() have unrestricted tool access.
|
|
433
|
+
Detection uses 5-level strategy: env vars, session state, database.
|
|
434
|
+
|
|
375
435
|
Args:
|
|
376
436
|
tool: Tool name (e.g., "Edit", "Bash", "Read")
|
|
377
437
|
params: Tool parameters (e.g., {"file_path": "test.py"})
|
|
378
438
|
config: Validation configuration (from load_validation_config())
|
|
379
|
-
history: Tool usage history (from load_tool_history())
|
|
439
|
+
history: Tool usage history (from load_tool_history(session_id))
|
|
440
|
+
session_id: Optional session ID for loading history if not provided
|
|
380
441
|
|
|
381
442
|
Returns:
|
|
382
443
|
dict[str, Any]: {"decision": "allow" | "block", "guidance": "...", "suggestion": "...", ...}
|
|
383
444
|
All operations are ALLOWED unless blocked for safety reasons.
|
|
384
445
|
|
|
385
446
|
Example:
|
|
447
|
+
session_id = tool_input.get("session_id", "unknown")
|
|
448
|
+
history = load_tool_history(session_id)
|
|
386
449
|
result = validate_tool_call("Edit", {"file_path": "test.py"}, config, history)
|
|
387
450
|
if result["decision"] == "block":
|
|
388
451
|
print(result["reason"])
|
|
389
452
|
elif "guidance" in result:
|
|
390
453
|
print(result["guidance"])
|
|
391
454
|
"""
|
|
455
|
+
# Check if this is a subagent context - subagents have unrestricted tool access
|
|
456
|
+
if is_subagent_context():
|
|
457
|
+
return {"decision": "allow"}
|
|
458
|
+
|
|
392
459
|
result = {"decision": "allow"}
|
|
393
460
|
guidance_parts = []
|
|
394
461
|
|
|
395
462
|
# Step 0a: Check orchestrator mode violations (if enabled)
|
|
396
|
-
orchestrator_violation = check_orchestrator_violation(
|
|
463
|
+
orchestrator_violation = check_orchestrator_violation(
|
|
464
|
+
tool, params, session_id or "unknown"
|
|
465
|
+
)
|
|
397
466
|
if orchestrator_violation:
|
|
398
467
|
# BLOCK orchestrator violations in strict mode
|
|
399
468
|
return orchestrator_violation
|
|
@@ -509,3 +578,47 @@ def validate_tool_call(
|
|
|
509
578
|
result["guidance"] = " | ".join(guidance_parts)
|
|
510
579
|
|
|
511
580
|
return result
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def main() -> None:
|
|
584
|
+
"""Hook entry point for script wrapper."""
|
|
585
|
+
import sys
|
|
586
|
+
|
|
587
|
+
try:
|
|
588
|
+
# Read tool input from stdin
|
|
589
|
+
tool_input = json.load(sys.stdin)
|
|
590
|
+
|
|
591
|
+
# Claude Code uses "name" and "input", fallback to "tool" and "params"
|
|
592
|
+
tool = tool_input.get("name", "") or tool_input.get("tool", "")
|
|
593
|
+
params = tool_input.get("input", {}) or tool_input.get("params", {})
|
|
594
|
+
|
|
595
|
+
# Get session_id from hook_input (NEW: required for session-isolated history)
|
|
596
|
+
session_id = tool_input.get("session_id", "unknown")
|
|
597
|
+
|
|
598
|
+
# Load config
|
|
599
|
+
config = load_validation_config()
|
|
600
|
+
|
|
601
|
+
# Load session-isolated tool history (NEW: from database, not file)
|
|
602
|
+
history = load_tool_history(session_id)
|
|
603
|
+
|
|
604
|
+
# Get guidance with pattern awareness
|
|
605
|
+
result = validate_tool_call(tool, params, config, history)
|
|
606
|
+
|
|
607
|
+
# Note: Tool recording is now handled by track-event.py PostToolUse hook
|
|
608
|
+
# No need to call record_tool() or save_tool_history() here
|
|
609
|
+
|
|
610
|
+
# Output JSON with guidance/block message
|
|
611
|
+
print(json.dumps(result))
|
|
612
|
+
|
|
613
|
+
# Exit 1 to BLOCK if decision is "block", otherwise allow
|
|
614
|
+
if result.get("decision") == "block":
|
|
615
|
+
sys.exit(1)
|
|
616
|
+
else:
|
|
617
|
+
sys.exit(0)
|
|
618
|
+
|
|
619
|
+
except Exception as e:
|
|
620
|
+
# Graceful degradation - allow on error
|
|
621
|
+
print(
|
|
622
|
+
json.dumps({"decision": "allow", "guidance": f"Validation hook error: {e}"})
|
|
623
|
+
)
|
|
624
|
+
sys.exit(0)
|
htmlgraph/operations/__init__.py
CHANGED
|
@@ -24,6 +24,16 @@ from .hooks import (
|
|
|
24
24
|
list_hooks,
|
|
25
25
|
validate_hook_config,
|
|
26
26
|
)
|
|
27
|
+
from .initialization import (
|
|
28
|
+
create_analytics_index,
|
|
29
|
+
create_config_files,
|
|
30
|
+
create_database,
|
|
31
|
+
create_directory_structure,
|
|
32
|
+
initialize_htmlgraph,
|
|
33
|
+
install_git_hooks,
|
|
34
|
+
update_gitignore,
|
|
35
|
+
validate_directory,
|
|
36
|
+
)
|
|
27
37
|
from .server import (
|
|
28
38
|
ServerHandle,
|
|
29
39
|
ServerStartResult,
|
|
@@ -58,4 +68,12 @@ __all__ = [
|
|
|
58
68
|
"start_server",
|
|
59
69
|
"stop_server",
|
|
60
70
|
"get_server_status",
|
|
71
|
+
"initialize_htmlgraph",
|
|
72
|
+
"validate_directory",
|
|
73
|
+
"create_directory_structure",
|
|
74
|
+
"create_database",
|
|
75
|
+
"create_analytics_index",
|
|
76
|
+
"create_config_files",
|
|
77
|
+
"update_gitignore",
|
|
78
|
+
"install_git_hooks",
|
|
61
79
|
]
|