htmlgraph 0.24.2__py3-none-any.whl → 0.26.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.
- htmlgraph/__init__.py +20 -1
- htmlgraph/agent_detection.py +26 -10
- htmlgraph/analytics/cross_session.py +4 -3
- htmlgraph/analytics/work_type.py +52 -16
- htmlgraph/analytics_index.py +51 -19
- htmlgraph/api/__init__.py +3 -0
- htmlgraph/api/main.py +2263 -0
- htmlgraph/api/static/htmx.min.js +1 -0
- htmlgraph/api/static/style-redesign.css +1344 -0
- htmlgraph/api/static/style.css +1079 -0
- htmlgraph/api/templates/dashboard-redesign.html +812 -0
- htmlgraph/api/templates/dashboard.html +794 -0
- htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
- htmlgraph/api/templates/partials/activity-feed.html +1020 -0
- htmlgraph/api/templates/partials/agents-redesign.html +317 -0
- htmlgraph/api/templates/partials/agents.html +317 -0
- htmlgraph/api/templates/partials/event-traces.html +373 -0
- htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
- htmlgraph/api/templates/partials/features.html +509 -0
- htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
- htmlgraph/api/templates/partials/metrics.html +346 -0
- htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
- htmlgraph/api/templates/partials/orchestration.html +163 -0
- htmlgraph/api/templates/partials/spawners.html +375 -0
- htmlgraph/atomic_ops.py +560 -0
- htmlgraph/builders/base.py +55 -1
- htmlgraph/builders/bug.py +17 -2
- htmlgraph/builders/chore.py +17 -2
- htmlgraph/builders/epic.py +17 -2
- htmlgraph/builders/feature.py +25 -2
- htmlgraph/builders/phase.py +17 -2
- htmlgraph/builders/spike.py +27 -2
- htmlgraph/builders/track.py +14 -0
- htmlgraph/cigs/__init__.py +4 -0
- htmlgraph/cigs/reporter.py +818 -0
- htmlgraph/cli.py +1427 -401
- htmlgraph/cli_commands/__init__.py +1 -0
- htmlgraph/cli_commands/feature.py +195 -0
- htmlgraph/cli_framework.py +115 -0
- htmlgraph/collections/__init__.py +2 -0
- htmlgraph/collections/base.py +21 -0
- htmlgraph/collections/session.py +189 -0
- htmlgraph/collections/spike.py +7 -1
- htmlgraph/collections/task_delegation.py +236 -0
- htmlgraph/collections/traces.py +482 -0
- htmlgraph/config.py +113 -0
- htmlgraph/converter.py +41 -0
- htmlgraph/cost_analysis/__init__.py +5 -0
- htmlgraph/cost_analysis/analyzer.py +438 -0
- htmlgraph/dashboard.html +3356 -492
- htmlgraph-0.24.2.data/data/htmlgraph/dashboard.html → htmlgraph/dashboard.html.backup +2246 -248
- htmlgraph/dashboard.html.bak +7181 -0
- htmlgraph/dashboard.html.bak2 +7231 -0
- htmlgraph/dashboard.html.bak3 +7232 -0
- htmlgraph/db/__init__.py +38 -0
- htmlgraph/db/queries.py +790 -0
- htmlgraph/db/schema.py +1584 -0
- htmlgraph/deploy.py +26 -27
- htmlgraph/docs/API_REFERENCE.md +841 -0
- htmlgraph/docs/HTTP_API.md +750 -0
- htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
- htmlgraph/docs/ORCHESTRATION_PATTERNS.md +710 -0
- htmlgraph/docs/README.md +533 -0
- htmlgraph/docs/version_check.py +3 -1
- htmlgraph/error_handler.py +544 -0
- htmlgraph/event_log.py +2 -0
- htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/hooks/.htmlgraph/agents.json +72 -0
- htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
- htmlgraph/hooks/__init__.py +8 -0
- htmlgraph/hooks/bootstrap.py +169 -0
- htmlgraph/hooks/cigs_pretool_enforcer.py +2 -2
- htmlgraph/hooks/concurrent_sessions.py +208 -0
- htmlgraph/hooks/context.py +318 -0
- htmlgraph/hooks/drift_handler.py +525 -0
- htmlgraph/hooks/event_tracker.py +496 -79
- htmlgraph/hooks/orchestrator.py +6 -4
- htmlgraph/hooks/orchestrator_reflector.py +4 -4
- htmlgraph/hooks/post_tool_use_handler.py +257 -0
- htmlgraph/hooks/pretooluse.py +473 -6
- htmlgraph/hooks/prompt_analyzer.py +637 -0
- htmlgraph/hooks/session_handler.py +637 -0
- htmlgraph/hooks/state_manager.py +504 -0
- htmlgraph/hooks/subagent_stop.py +309 -0
- htmlgraph/hooks/task_enforcer.py +39 -0
- htmlgraph/hooks/validator.py +15 -11
- htmlgraph/models.py +111 -15
- htmlgraph/operations/fastapi_server.py +230 -0
- htmlgraph/orchestration/headless_spawner.py +344 -29
- htmlgraph/orchestration/live_events.py +377 -0
- htmlgraph/pydantic_models.py +476 -0
- htmlgraph/quality_gates.py +350 -0
- htmlgraph/repo_hash.py +511 -0
- htmlgraph/sdk.py +348 -10
- htmlgraph/server.py +194 -0
- htmlgraph/session_hooks.py +300 -0
- htmlgraph/session_manager.py +131 -1
- htmlgraph/session_registry.py +587 -0
- htmlgraph/session_state.py +436 -0
- htmlgraph/system_prompts.py +449 -0
- htmlgraph/templates/orchestration-view.html +350 -0
- htmlgraph/track_builder.py +19 -0
- htmlgraph/validation.py +115 -0
- htmlgraph-0.26.1.data/data/htmlgraph/dashboard.html +7458 -0
- {htmlgraph-0.24.2.dist-info → htmlgraph-0.26.1.dist-info}/METADATA +91 -64
- {htmlgraph-0.24.2.dist-info → htmlgraph-0.26.1.dist-info}/RECORD +112 -46
- {htmlgraph-0.24.2.data → htmlgraph-0.26.1.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.24.2.data → htmlgraph-0.26.1.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
- {htmlgraph-0.24.2.data → htmlgraph-0.26.1.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
- {htmlgraph-0.24.2.data → htmlgraph-0.26.1.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
- {htmlgraph-0.24.2.dist-info → htmlgraph-0.26.1.dist-info}/WHEEL +0 -0
- {htmlgraph-0.24.2.dist-info → htmlgraph-0.26.1.dist-info}/entry_points.txt +0 -0
htmlgraph/hooks/orchestrator.py
CHANGED
|
@@ -21,7 +21,7 @@ Enforcement Levels:
|
|
|
21
21
|
- guidance: ALLOWS but provides warnings and suggestions
|
|
22
22
|
|
|
23
23
|
Public API:
|
|
24
|
-
- enforce_orchestrator_mode(tool: str, params: dict) -> dict
|
|
24
|
+
- enforce_orchestrator_mode(tool: str, params: dict[str, Any]) -> dict
|
|
25
25
|
Main entry point for hook scripts. Returns hook response dict.
|
|
26
26
|
"""
|
|
27
27
|
|
|
@@ -93,7 +93,9 @@ def add_to_tool_history(tool: str) -> None:
|
|
|
93
93
|
save_tool_history(history)
|
|
94
94
|
|
|
95
95
|
|
|
96
|
-
def is_allowed_orchestrator_operation(
|
|
96
|
+
def is_allowed_orchestrator_operation(
|
|
97
|
+
tool: str, params: dict[str, Any]
|
|
98
|
+
) -> tuple[bool, str, str]:
|
|
97
99
|
"""
|
|
98
100
|
Check if operation is allowed for orchestrators.
|
|
99
101
|
|
|
@@ -268,7 +270,7 @@ def is_allowed_orchestrator_operation(tool: str, params: dict) -> tuple[bool, st
|
|
|
268
270
|
return True, "Allowed in guidance mode", "guidance-allowed"
|
|
269
271
|
|
|
270
272
|
|
|
271
|
-
def create_task_suggestion(tool: str, params: dict) -> str:
|
|
273
|
+
def create_task_suggestion(tool: str, params: dict[str, Any]) -> str:
|
|
272
274
|
"""
|
|
273
275
|
Create Task tool suggestion based on blocked operation.
|
|
274
276
|
|
|
@@ -370,7 +372,7 @@ def create_task_suggestion(tool: str, params: dict) -> str:
|
|
|
370
372
|
)
|
|
371
373
|
|
|
372
374
|
|
|
373
|
-
def enforce_orchestrator_mode(tool: str, params: dict) -> dict:
|
|
375
|
+
def enforce_orchestrator_mode(tool: str, params: dict[str, Any]) -> dict[str, Any]:
|
|
374
376
|
"""
|
|
375
377
|
Enforce orchestrator mode rules.
|
|
376
378
|
|
|
@@ -22,7 +22,7 @@ Usage:
|
|
|
22
22
|
"""
|
|
23
23
|
|
|
24
24
|
import re
|
|
25
|
-
from typing import TypedDict
|
|
25
|
+
from typing import Any, TypedDict
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
class HookSpecificOutput(TypedDict):
|
|
@@ -93,7 +93,7 @@ def is_python_execution(command: str) -> bool:
|
|
|
93
93
|
return False
|
|
94
94
|
|
|
95
95
|
|
|
96
|
-
def should_reflect(hook_input: dict) -> tuple[bool, str]:
|
|
96
|
+
def should_reflect(hook_input: dict[str, Any]) -> tuple[bool, str]:
|
|
97
97
|
"""
|
|
98
98
|
Check if we should show reflection prompt.
|
|
99
99
|
|
|
@@ -156,7 +156,7 @@ Ask yourself:
|
|
|
156
156
|
Continue, but consider delegation for similar future tasks."""
|
|
157
157
|
|
|
158
158
|
|
|
159
|
-
def orchestrator_reflect(tool_input: dict) -> dict:
|
|
159
|
+
def orchestrator_reflect(tool_input: dict[str, Any]) -> dict[str, Any]:
|
|
160
160
|
"""
|
|
161
161
|
Main API function for orchestrator reflection.
|
|
162
162
|
|
|
@@ -184,7 +184,7 @@ def orchestrator_reflect(tool_input: dict) -> dict:
|
|
|
184
184
|
should_show, command_preview = should_reflect(tool_input)
|
|
185
185
|
|
|
186
186
|
# Build response
|
|
187
|
-
response: dict = {"continue": True}
|
|
187
|
+
response: dict[str, Any] = {"continue": True}
|
|
188
188
|
|
|
189
189
|
if should_show:
|
|
190
190
|
reflection = build_reflection_message(command_preview)
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PostToolUse Enhancement - Duration Calculation and Tool Trace Updates
|
|
3
|
+
|
|
4
|
+
This module handles the PostToolUse hook event and updates tool traces with:
|
|
5
|
+
1. Execution end time (when the tool completed)
|
|
6
|
+
2. Duration in milliseconds (end_time - start_time)
|
|
7
|
+
3. Tool output (result of the tool execution)
|
|
8
|
+
4. Status (Ok or Error)
|
|
9
|
+
5. Error message (if status is Error)
|
|
10
|
+
|
|
11
|
+
The module correlates with PreToolUse via tool_use_id environment variable
|
|
12
|
+
and gracefully handles missing pre-events (logs warning, continues).
|
|
13
|
+
|
|
14
|
+
Design:
|
|
15
|
+
- Query tool_traces for matching tool_use_id
|
|
16
|
+
- Get start_time from pre-event
|
|
17
|
+
- Calculate duration_ms (end_time - start_time)
|
|
18
|
+
- Update tool_traces with: end_time, duration_ms, tool_output, status, error_message
|
|
19
|
+
- Handle missing pre-event gracefully (log warning, continue)
|
|
20
|
+
- Non-blocking - errors don't prevent tool execution continuation
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import logging
|
|
25
|
+
import os
|
|
26
|
+
from datetime import datetime, timezone
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
from htmlgraph.db.schema import HtmlGraphDB
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def calculate_duration(start_time_iso: str, end_time_iso: str) -> int:
|
|
35
|
+
"""
|
|
36
|
+
Calculate duration in milliseconds between two ISO8601 UTC timestamps.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
start_time_iso: ISO8601 UTC timestamp from PreToolUse (e.g., "2025-01-07T12:34:56.789000+00:00")
|
|
40
|
+
end_time_iso: ISO8601 UTC timestamp (now, e.g., "2025-01-07T12:34:57.123000+00:00")
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
duration_ms: Integer milliseconds between timestamps (accurate within 1ms)
|
|
44
|
+
|
|
45
|
+
Raises:
|
|
46
|
+
ValueError: If timestamps cannot be parsed
|
|
47
|
+
TypeError: If inputs are not strings
|
|
48
|
+
"""
|
|
49
|
+
try:
|
|
50
|
+
# Parse ISO8601 timestamps (handles timezone-aware datetimes)
|
|
51
|
+
start_dt = datetime.fromisoformat(start_time_iso.replace("Z", "+00:00"))
|
|
52
|
+
end_dt = datetime.fromisoformat(end_time_iso.replace("Z", "+00:00"))
|
|
53
|
+
|
|
54
|
+
# Calculate difference and convert to milliseconds
|
|
55
|
+
delta = end_dt - start_dt
|
|
56
|
+
duration_ms = int(delta.total_seconds() * 1000)
|
|
57
|
+
|
|
58
|
+
return duration_ms
|
|
59
|
+
except (ValueError, AttributeError, TypeError) as e:
|
|
60
|
+
logger.warning(f"Error calculating duration: {e}")
|
|
61
|
+
raise
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def update_tool_trace(
|
|
65
|
+
tool_use_id: str,
|
|
66
|
+
tool_output: dict[str, Any] | None,
|
|
67
|
+
status: str,
|
|
68
|
+
error_message: str | None = None,
|
|
69
|
+
) -> bool:
|
|
70
|
+
"""
|
|
71
|
+
Update tool_traces table with execution end event.
|
|
72
|
+
|
|
73
|
+
Updates an existing tool trace (created by PreToolUse) with:
|
|
74
|
+
- end_time: Current UTC timestamp
|
|
75
|
+
- duration_ms: Milliseconds between start and end
|
|
76
|
+
- tool_output: Result of tool execution (JSON)
|
|
77
|
+
- status: 'Ok' or 'Error'
|
|
78
|
+
- error_message: Error details if status='Error'
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
tool_use_id: Correlation ID from PreToolUse event (from environment)
|
|
82
|
+
tool_output: Tool execution result (dict, will be JSON serialized)
|
|
83
|
+
status: 'Ok' or 'Error'
|
|
84
|
+
error_message: Error details if status='Error'
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
True if update successful, False otherwise
|
|
88
|
+
|
|
89
|
+
Workflow:
|
|
90
|
+
1. Query tool_traces for matching tool_use_id
|
|
91
|
+
2. Get start_time from pre-event
|
|
92
|
+
3. Calculate duration_ms (end_time - start_time)
|
|
93
|
+
4. Update tool_traces with: end_time, duration_ms, tool_output, status, error_message
|
|
94
|
+
5. Handle missing pre-event gracefully (log warning, continue)
|
|
95
|
+
"""
|
|
96
|
+
try:
|
|
97
|
+
# Connect to database
|
|
98
|
+
db = HtmlGraphDB()
|
|
99
|
+
|
|
100
|
+
if not db.connection:
|
|
101
|
+
db.connect()
|
|
102
|
+
|
|
103
|
+
cursor = db.connection.cursor() # type: ignore[union-attr]
|
|
104
|
+
|
|
105
|
+
# Query tool_traces for matching tool_use_id
|
|
106
|
+
cursor.execute(
|
|
107
|
+
"""
|
|
108
|
+
SELECT tool_use_id, start_time FROM tool_traces
|
|
109
|
+
WHERE tool_use_id = ?
|
|
110
|
+
""",
|
|
111
|
+
(tool_use_id,),
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
row = cursor.fetchone()
|
|
115
|
+
|
|
116
|
+
if not row:
|
|
117
|
+
# Missing pre-event - log warning but continue (graceful degradation)
|
|
118
|
+
logger.warning(
|
|
119
|
+
f"Could not find start event for tool_use_id={tool_use_id}. "
|
|
120
|
+
f"PreToolUse event may not have completed. Skipping duration update."
|
|
121
|
+
)
|
|
122
|
+
db.disconnect()
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
# Get start_time from pre-event
|
|
126
|
+
start_time_iso = row[1]
|
|
127
|
+
|
|
128
|
+
# Calculate end_time (now in UTC)
|
|
129
|
+
end_time_iso = datetime.now(timezone.utc).isoformat()
|
|
130
|
+
|
|
131
|
+
# Calculate duration_ms
|
|
132
|
+
try:
|
|
133
|
+
duration_ms = calculate_duration(start_time_iso, end_time_iso)
|
|
134
|
+
except (ValueError, TypeError) as e:
|
|
135
|
+
logger.warning(
|
|
136
|
+
f"Could not calculate duration for tool_use_id={tool_use_id}: {e}. "
|
|
137
|
+
f"Using None for duration."
|
|
138
|
+
)
|
|
139
|
+
duration_ms = None
|
|
140
|
+
|
|
141
|
+
# Validate status
|
|
142
|
+
valid_statuses = {"Ok", "Error", "completed", "failed", "timeout"}
|
|
143
|
+
if status not in valid_statuses:
|
|
144
|
+
logger.warning(
|
|
145
|
+
f"Invalid status '{status}' for tool_use_id={tool_use_id}. "
|
|
146
|
+
f"Using 'Ok' as default."
|
|
147
|
+
)
|
|
148
|
+
status = "Ok"
|
|
149
|
+
|
|
150
|
+
# JSON serialize tool_output
|
|
151
|
+
tool_output_json = None
|
|
152
|
+
if tool_output:
|
|
153
|
+
try:
|
|
154
|
+
tool_output_json = json.dumps(tool_output)
|
|
155
|
+
except (TypeError, ValueError) as e:
|
|
156
|
+
logger.warning(
|
|
157
|
+
f"Could not JSON serialize tool_output for "
|
|
158
|
+
f"tool_use_id={tool_use_id}: {e}"
|
|
159
|
+
)
|
|
160
|
+
tool_output_json = json.dumps(
|
|
161
|
+
{"error": str(e), "output": str(tool_output)}
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Update tool_traces with: end_time, duration_ms, tool_output, status, error_message
|
|
165
|
+
cursor.execute(
|
|
166
|
+
"""
|
|
167
|
+
UPDATE tool_traces
|
|
168
|
+
SET end_time = ?, duration_ms = ?, tool_output = ?,
|
|
169
|
+
status = ?, error_message = ?
|
|
170
|
+
WHERE tool_use_id = ?
|
|
171
|
+
""",
|
|
172
|
+
(
|
|
173
|
+
end_time_iso,
|
|
174
|
+
duration_ms,
|
|
175
|
+
tool_output_json,
|
|
176
|
+
status,
|
|
177
|
+
error_message,
|
|
178
|
+
tool_use_id,
|
|
179
|
+
),
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
if not db.connection:
|
|
183
|
+
db.connect()
|
|
184
|
+
|
|
185
|
+
db.connection.commit() # type: ignore[union-attr]
|
|
186
|
+
|
|
187
|
+
logger.debug(
|
|
188
|
+
f"Updated tool trace: tool_use_id={tool_use_id}, "
|
|
189
|
+
f"duration_ms={duration_ms}, status={status}"
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
db.disconnect()
|
|
193
|
+
return True
|
|
194
|
+
|
|
195
|
+
except Exception as e:
|
|
196
|
+
# Log error but don't block
|
|
197
|
+
logger.error(f"Error updating tool trace for tool_use_id={tool_use_id}: {e}")
|
|
198
|
+
return False
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def get_tool_use_id_from_context() -> str | None:
|
|
202
|
+
"""
|
|
203
|
+
Get tool_use_id from environment (set by PreToolUse hook).
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
tool_use_id string or None if not set
|
|
207
|
+
"""
|
|
208
|
+
return os.environ.get("HTMLGRAPH_TOOL_USE_ID")
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def determine_status_from_response(
|
|
212
|
+
tool_response: dict[str, Any] | None,
|
|
213
|
+
) -> tuple[str, str | None]:
|
|
214
|
+
"""
|
|
215
|
+
Determine status (Ok/Error) and error message from tool response.
|
|
216
|
+
|
|
217
|
+
Analyzes tool response to determine if execution was successful.
|
|
218
|
+
Returns (status, error_message) tuple.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
tool_response: Tool execution response (dict)
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
(status, error_message) where:
|
|
225
|
+
- status: 'Ok' or 'Error'
|
|
226
|
+
- error_message: Error details if Error, else None
|
|
227
|
+
"""
|
|
228
|
+
if not tool_response:
|
|
229
|
+
return ("Ok", None)
|
|
230
|
+
|
|
231
|
+
if not isinstance(tool_response, dict):
|
|
232
|
+
return ("Ok", None)
|
|
233
|
+
|
|
234
|
+
# Check for explicit error indicators
|
|
235
|
+
# Bash tool: non-empty stderr
|
|
236
|
+
stderr = tool_response.get("stderr", "")
|
|
237
|
+
if stderr and isinstance(stderr, str) and stderr.strip():
|
|
238
|
+
return ("Error", f"stderr: {stderr[:500]}")
|
|
239
|
+
|
|
240
|
+
# Explicit error field
|
|
241
|
+
error_field = tool_response.get("error")
|
|
242
|
+
if error_field and str(error_field).strip():
|
|
243
|
+
return ("Error", str(error_field)[:500])
|
|
244
|
+
|
|
245
|
+
# success=false flag
|
|
246
|
+
if tool_response.get("success") is False:
|
|
247
|
+
reason = tool_response.get("reason", "Unknown error")
|
|
248
|
+
return ("Error", str(reason)[:500])
|
|
249
|
+
|
|
250
|
+
# status field indicating failure
|
|
251
|
+
status_field = tool_response.get("status")
|
|
252
|
+
if status_field and status_field.lower() in {"error", "failed", "failed"}:
|
|
253
|
+
reason = tool_response.get("message", "Unknown error")
|
|
254
|
+
return ("Error", str(reason)[:500])
|
|
255
|
+
|
|
256
|
+
# Default to success
|
|
257
|
+
return ("Ok", None)
|