@pjmendonca/devflow 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/CHANGELOG.md +526 -0
  2. package/LICENSE +21 -0
  3. package/README.md +620 -0
  4. package/bin/devflow-checkpoint.js +10 -0
  5. package/bin/devflow-collab.js +10 -0
  6. package/bin/devflow-cost.js +10 -0
  7. package/bin/devflow-create-persona.js +10 -0
  8. package/bin/devflow-init.js +10 -0
  9. package/bin/devflow-memory.js +10 -0
  10. package/bin/devflow-new-doc.js +10 -0
  11. package/bin/devflow-personalize.js +10 -0
  12. package/bin/devflow-setup-checkpoint.js +10 -0
  13. package/bin/devflow-story.js +10 -0
  14. package/bin/devflow-tech-debt.js +10 -0
  15. package/bin/devflow-validate-overrides.js +10 -0
  16. package/bin/devflow-validate.js +10 -0
  17. package/bin/devflow-version.js +10 -0
  18. package/lib/constants.js +30 -0
  19. package/lib/exec-python.js +78 -0
  20. package/lib/python-check.js +178 -0
  21. package/package.json +64 -0
  22. package/tooling/.automation/agents/architect.md +135 -0
  23. package/tooling/.automation/agents/ba.md +70 -0
  24. package/tooling/.automation/agents/dev.md +79 -0
  25. package/tooling/.automation/agents/maintainer.md +97 -0
  26. package/tooling/.automation/agents/pm.md +116 -0
  27. package/tooling/.automation/agents/reviewer.md +141 -0
  28. package/tooling/.automation/agents/sm.md +61 -0
  29. package/tooling/.automation/agents/writer.md +193 -0
  30. package/tooling/.automation/config.ps1.template +61 -0
  31. package/tooling/.automation/config.sh.template +48 -0
  32. package/tooling/.automation/memory/.gitkeep +6 -0
  33. package/tooling/.automation/memory/knowledge/kg_integration-test.json +94 -0
  34. package/tooling/.automation/memory/knowledge/kg_test-story.json +300 -0
  35. package/tooling/.automation/memory/shared/shared_integration-test.json +30 -0
  36. package/tooling/.automation/memory/shared/shared_test-story.json +78 -0
  37. package/tooling/.automation/overrides/templates/README.md +113 -0
  38. package/tooling/.automation/overrides/templates/architect/README.md +27 -0
  39. package/tooling/.automation/overrides/templates/architect/cloud-native.yaml +92 -0
  40. package/tooling/.automation/overrides/templates/architect/enterprise-architect.yaml +85 -0
  41. package/tooling/.automation/overrides/templates/architect/pragmatic-minimalist.yaml +88 -0
  42. package/tooling/.automation/overrides/templates/ba/README.md +27 -0
  43. package/tooling/.automation/overrides/templates/ba/agile-storyteller.yaml +86 -0
  44. package/tooling/.automation/overrides/templates/ba/domain-expert.yaml +91 -0
  45. package/tooling/.automation/overrides/templates/ba/requirements-engineer.yaml +89 -0
  46. package/tooling/.automation/overrides/templates/dev/README.md +32 -0
  47. package/tooling/.automation/overrides/templates/dev/junior-mentored.yaml +39 -0
  48. package/tooling/.automation/overrides/templates/dev/performance-engineer.yaml +43 -0
  49. package/tooling/.automation/overrides/templates/dev/rapid-prototyper.yaml +52 -0
  50. package/tooling/.automation/overrides/templates/dev/security-focused.yaml +43 -0
  51. package/tooling/.automation/overrides/templates/dev/senior-fullstack.yaml +39 -0
  52. package/tooling/.automation/overrides/templates/maintainer/README.md +27 -0
  53. package/tooling/.automation/overrides/templates/maintainer/devops-maintainer.yaml +113 -0
  54. package/tooling/.automation/overrides/templates/maintainer/legacy-steward.yaml +94 -0
  55. package/tooling/.automation/overrides/templates/maintainer/oss-maintainer.yaml +94 -0
  56. package/tooling/.automation/overrides/templates/pm/README.md +27 -0
  57. package/tooling/.automation/overrides/templates/pm/agile-pm.yaml +91 -0
  58. package/tooling/.automation/overrides/templates/pm/hybrid-delivery.yaml +87 -0
  59. package/tooling/.automation/overrides/templates/pm/traditional-pm.yaml +91 -0
  60. package/tooling/.automation/overrides/templates/reviewer/README.md +11 -0
  61. package/tooling/.automation/overrides/templates/reviewer/mentoring-reviewer.yaml +45 -0
  62. package/tooling/.automation/overrides/templates/reviewer/quick-sanity.yaml +50 -0
  63. package/tooling/.automation/overrides/templates/reviewer/thorough-critic.yaml +48 -0
  64. package/tooling/.automation/overrides/templates/sm/README.md +11 -0
  65. package/tooling/.automation/overrides/templates/sm/agile-coach.yaml +52 -0
  66. package/tooling/.automation/overrides/templates/sm/startup-pm.yaml +50 -0
  67. package/tooling/.automation/overrides/templates/sm/technical-lead.yaml +47 -0
  68. package/tooling/.automation/overrides/templates/user-profile.template.yaml +62 -0
  69. package/tooling/.automation/overrides/templates/writer/README.md +27 -0
  70. package/tooling/.automation/overrides/templates/writer/api-documentarian.yaml +99 -0
  71. package/tooling/.automation/overrides/templates/writer/docs-as-code.yaml +108 -0
  72. package/tooling/.automation/overrides/templates/writer/user-guide-author.yaml +100 -0
  73. package/tooling/completions/DevflowCompletion.ps1 +213 -0
  74. package/tooling/completions/_run-story +116 -0
  75. package/tooling/completions/run-story-completion.bash +136 -0
  76. package/tooling/docs/DOC-STANDARD.md +717 -0
  77. package/tooling/docs/sprint-status.yaml.template +24 -0
  78. package/tooling/docs/templates/bug-report.md +234 -0
  79. package/tooling/docs/templates/migration-spec.md +274 -0
  80. package/tooling/docs/templates/refactor-spec.md +86 -0
  81. package/tooling/docs/templates/tech-debt.md +86 -0
  82. package/tooling/scripts/context_checkpoint.py +556 -0
  83. package/tooling/scripts/cost_dashboard.py +617 -0
  84. package/tooling/scripts/create-persona.py +690 -0
  85. package/tooling/scripts/create-persona.sh +435 -0
  86. package/tooling/scripts/init-project-workflow.ps1 +651 -0
  87. package/tooling/scripts/init-project-workflow.py +70 -0
  88. package/tooling/scripts/init-project-workflow.sh +746 -0
  89. package/tooling/scripts/lib/__init__.py +35 -0
  90. package/tooling/scripts/lib/agent_handoff.py +526 -0
  91. package/tooling/scripts/lib/agent_router.py +698 -0
  92. package/tooling/scripts/lib/checkpoint-integration.ps1 +245 -0
  93. package/tooling/scripts/lib/checkpoint-integration.sh +191 -0
  94. package/tooling/scripts/lib/claude-cli.ps1 +952 -0
  95. package/tooling/scripts/lib/claude-cli.sh +1293 -0
  96. package/tooling/scripts/lib/cost_config.py +222 -0
  97. package/tooling/scripts/lib/cost_display.py +443 -0
  98. package/tooling/scripts/lib/cost_tracker.py +710 -0
  99. package/tooling/scripts/lib/currency_converter.py +328 -0
  100. package/tooling/scripts/lib/errors.py +438 -0
  101. package/tooling/scripts/lib/override-loader.sh +286 -0
  102. package/tooling/scripts/lib/pair_programming.py +589 -0
  103. package/tooling/scripts/lib/shared_memory.py +637 -0
  104. package/tooling/scripts/lib/swarm_orchestrator.py +689 -0
  105. package/tooling/scripts/memory_summarize.py +324 -0
  106. package/tooling/scripts/new-doc.ps1 +405 -0
  107. package/tooling/scripts/new-doc.py +93 -0
  108. package/tooling/scripts/new-doc.sh +534 -0
  109. package/tooling/scripts/personalize_agent.py +385 -0
  110. package/tooling/scripts/rollback-migration.sh +540 -0
  111. package/tooling/scripts/run-collab.ps1 +251 -0
  112. package/tooling/scripts/run-collab.py +605 -0
  113. package/tooling/scripts/run-collab.sh +110 -0
  114. package/tooling/scripts/run-story.ps1 +490 -0
  115. package/tooling/scripts/run-story.py +387 -0
  116. package/tooling/scripts/run-story.sh +467 -0
  117. package/tooling/scripts/setup-checkpoint-service.ps1 +219 -0
  118. package/tooling/scripts/setup-checkpoint-service.py +87 -0
  119. package/tooling/scripts/setup-checkpoint-service.sh +236 -0
  120. package/tooling/scripts/tech-debt-tracker.py +608 -0
  121. package/tooling/scripts/update_version.py +244 -0
  122. package/tooling/scripts/validate-overrides.py +511 -0
  123. package/tooling/scripts/validate-overrides.sh +432 -0
  124. package/tooling/scripts/validate_setup.py +539 -0
@@ -0,0 +1,438 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Error Handling - Enhanced error messages for better debugging.
4
+
5
+ Provides user-friendly error messages with context and suggested fixes.
6
+
7
+ Usage:
8
+ from lib.errors import CostTrackingError, handle_error
9
+
10
+ try:
11
+ # ... code ...
12
+ except Exception as e:
13
+ handle_error(e, context="loading session")
14
+ """
15
+
16
+ import json
17
+ import sys
18
+ import traceback
19
+ from dataclasses import dataclass
20
+ from enum import Enum
21
+ from pathlib import Path
22
+ from typing import Any, Optional
23
+
24
+
25
+ class ErrorCode(Enum):
26
+ """Error codes for categorizing issues."""
27
+
28
+ # Configuration errors (1xx)
29
+ CONFIG_NOT_FOUND = 101
30
+ CONFIG_PARSE_ERROR = 102
31
+ CONFIG_INVALID_VALUE = 103
32
+
33
+ # File/storage errors (2xx)
34
+ FILE_NOT_FOUND = 201
35
+ FILE_READ_ERROR = 202
36
+ FILE_WRITE_ERROR = 203
37
+ DIRECTORY_NOT_FOUND = 204
38
+ PERMISSION_DENIED = 205
39
+
40
+ # Session errors (3xx)
41
+ SESSION_NOT_FOUND = 301
42
+ SESSION_CORRUPTED = 302
43
+ SESSION_SAVE_FAILED = 303
44
+
45
+ # Budget errors (4xx)
46
+ BUDGET_EXCEEDED = 401
47
+ BUDGET_INVALID = 402
48
+
49
+ # Calculation errors (5xx)
50
+ UNKNOWN_MODEL = 501
51
+ INVALID_TOKENS = 502
52
+ CALCULATION_ERROR = 503
53
+
54
+ # General errors (9xx)
55
+ UNKNOWN_ERROR = 999
56
+
57
+
58
+ @dataclass
59
+ class ErrorContext:
60
+ """Context information for an error."""
61
+
62
+ operation: str
63
+ file_path: Optional[Path] = None
64
+ model: Optional[str] = None
65
+ agent: Optional[str] = None
66
+ tokens: Optional[dict[str, int]] = None
67
+ budget: Optional[float] = None
68
+ additional: Optional[dict[str, Any]] = None
69
+
70
+
71
+ class CostTrackingError(Exception):
72
+ """Base exception for cost tracking errors."""
73
+
74
+ def __init__(
75
+ self,
76
+ message: str,
77
+ code: ErrorCode = ErrorCode.UNKNOWN_ERROR,
78
+ context: Optional[ErrorContext] = None,
79
+ suggestion: Optional[str] = None,
80
+ cause: Optional[Exception] = None,
81
+ ):
82
+ self.message = message
83
+ self.code = code
84
+ self.context = context
85
+ self.suggestion = suggestion
86
+ self.cause = cause
87
+ super().__init__(self.format_message())
88
+
89
+ def format_message(self) -> str:
90
+ """Format the error message with context."""
91
+ parts = [f"[{self.code.name}] {self.message}"]
92
+
93
+ if self.context:
94
+ parts.append(f"\n Operation: {self.context.operation}")
95
+ if self.context.file_path:
96
+ parts.append(f" File: {self.context.file_path}")
97
+ if self.context.model:
98
+ parts.append(f" Model: {self.context.model}")
99
+ if self.context.agent:
100
+ parts.append(f" Agent: {self.context.agent}")
101
+
102
+ if self.suggestion:
103
+ parts.append(f"\n 💡 Suggestion: {self.suggestion}")
104
+
105
+ if self.cause:
106
+ parts.append(f"\n Caused by: {type(self.cause).__name__}: {self.cause}")
107
+
108
+ return "\n".join(parts)
109
+
110
+
111
+ class ConfigurationError(CostTrackingError):
112
+ """Configuration-related errors."""
113
+
114
+ pass
115
+
116
+
117
+ class SessionError(CostTrackingError):
118
+ """Session management errors."""
119
+
120
+ pass
121
+
122
+
123
+ class BudgetError(CostTrackingError):
124
+ """Budget-related errors."""
125
+
126
+ pass
127
+
128
+
129
+ class CalculationError(CostTrackingError):
130
+ """Calculation-related errors."""
131
+
132
+ pass
133
+
134
+
135
+ # Error message templates with suggestions
136
+ ERROR_MESSAGES = {
137
+ ErrorCode.CONFIG_NOT_FOUND: {
138
+ "message": "Configuration file not found",
139
+ "suggestion": "Create a config.json file or set environment variables. Run 'python validate_setup.py' for guidance.",
140
+ },
141
+ ErrorCode.CONFIG_PARSE_ERROR: {
142
+ "message": "Failed to parse configuration file",
143
+ "suggestion": "Check that your config.json is valid JSON. Use a JSON validator to find syntax errors.",
144
+ },
145
+ ErrorCode.CONFIG_INVALID_VALUE: {
146
+ "message": "Invalid configuration value",
147
+ "suggestion": "Check the configuration documentation for valid values and ranges.",
148
+ },
149
+ ErrorCode.FILE_NOT_FOUND: {
150
+ "message": "Required file not found",
151
+ "suggestion": "Ensure the file exists and the path is correct. Run 'python validate_setup.py' to check your setup.",
152
+ },
153
+ ErrorCode.FILE_READ_ERROR: {
154
+ "message": "Failed to read file",
155
+ "suggestion": "Check file permissions and ensure the file is not corrupted or locked by another process.",
156
+ },
157
+ ErrorCode.FILE_WRITE_ERROR: {
158
+ "message": "Failed to write file",
159
+ "suggestion": "Check directory permissions and ensure there's sufficient disk space.",
160
+ },
161
+ ErrorCode.DIRECTORY_NOT_FOUND: {
162
+ "message": "Required directory not found",
163
+ "suggestion": "The directory will be created automatically. If this persists, check your file system permissions.",
164
+ },
165
+ ErrorCode.PERMISSION_DENIED: {
166
+ "message": "Permission denied",
167
+ "suggestion": "Check file/directory permissions. On Unix, try: chmod 755 <path>",
168
+ },
169
+ ErrorCode.SESSION_NOT_FOUND: {
170
+ "message": "Session not found",
171
+ "suggestion": "The session may have expired or been deleted. Start a new tracking session.",
172
+ },
173
+ ErrorCode.SESSION_CORRUPTED: {
174
+ "message": "Session data is corrupted",
175
+ "suggestion": "The session file may be damaged. Remove the corrupted file and start a new session.",
176
+ },
177
+ ErrorCode.SESSION_SAVE_FAILED: {
178
+ "message": "Failed to save session",
179
+ "suggestion": "Check disk space and permissions. The session data is preserved in memory.",
180
+ },
181
+ ErrorCode.BUDGET_EXCEEDED: {
182
+ "message": "Budget limit exceeded",
183
+ "suggestion": "Increase the budget limit or optimize token usage. Consider using a cheaper model.",
184
+ },
185
+ ErrorCode.BUDGET_INVALID: {
186
+ "message": "Invalid budget value",
187
+ "suggestion": "Budget must be a positive number. Set to 0 for no limit.",
188
+ },
189
+ ErrorCode.UNKNOWN_MODEL: {
190
+ "message": "Unknown model specified",
191
+ "suggestion": "Use a supported model: opus, sonnet, haiku. Defaulting to sonnet pricing.",
192
+ },
193
+ ErrorCode.INVALID_TOKENS: {
194
+ "message": "Invalid token count",
195
+ "suggestion": "Token counts must be non-negative integers.",
196
+ },
197
+ ErrorCode.CALCULATION_ERROR: {
198
+ "message": "Error calculating cost",
199
+ "suggestion": "Check input values. Ensure token counts are valid numbers.",
200
+ },
201
+ }
202
+
203
+
204
+ def create_error(
205
+ code: ErrorCode,
206
+ context: Optional[ErrorContext] = None,
207
+ custom_message: Optional[str] = None,
208
+ cause: Optional[Exception] = None,
209
+ ) -> CostTrackingError:
210
+ """Create an error with appropriate type and message."""
211
+ template = ERROR_MESSAGES.get(code, {"message": "Unknown error", "suggestion": None})
212
+ message = custom_message or template["message"]
213
+ suggestion = template.get("suggestion")
214
+
215
+ # Select appropriate error class
216
+ if code.value < 200:
217
+ error_class = ConfigurationError
218
+ elif code.value < 400:
219
+ error_class = SessionError
220
+ elif code.value < 500:
221
+ error_class = BudgetError
222
+ elif code.value < 600:
223
+ error_class = CalculationError
224
+ else:
225
+ error_class = CostTrackingError
226
+
227
+ return error_class(
228
+ message=message, code=code, context=context, suggestion=suggestion, cause=cause
229
+ )
230
+
231
+
232
+ def format_error_for_user(error: Exception, verbose: bool = False) -> str:
233
+ """Format an error for user-friendly display."""
234
+ lines = []
235
+
236
+ # Header
237
+ lines.append("━" * 60)
238
+ lines.append("❌ Error Occurred")
239
+ lines.append("━" * 60)
240
+
241
+ if isinstance(error, CostTrackingError):
242
+ lines.append(f"\n{error.format_message()}")
243
+ else:
244
+ lines.append(f"\n{type(error).__name__}: {error}")
245
+
246
+ if verbose:
247
+ lines.append("\n📋 Stack Trace:")
248
+ lines.append(traceback.format_exc())
249
+
250
+ lines.append("\n" + "━" * 60)
251
+
252
+ return "\n".join(lines)
253
+
254
+
255
+ def handle_error(
256
+ error: Exception,
257
+ context: str = "unknown operation",
258
+ exit_on_error: bool = False,
259
+ verbose: bool = False,
260
+ ) -> None:
261
+ """
262
+ Handle an error with user-friendly output.
263
+
264
+ Args:
265
+ error: The exception that occurred
266
+ context: Description of what was being done
267
+ exit_on_error: Whether to exit the program
268
+ verbose: Whether to show stack trace
269
+ """
270
+ # Create context if not a CostTrackingError
271
+ if not isinstance(error, CostTrackingError):
272
+ error_context = ErrorContext(operation=context)
273
+
274
+ # Try to determine error type
275
+ if isinstance(error, FileNotFoundError):
276
+ error = create_error(ErrorCode.FILE_NOT_FOUND, context=error_context, cause=error)
277
+ elif isinstance(error, PermissionError):
278
+ error = create_error(ErrorCode.PERMISSION_DENIED, context=error_context, cause=error)
279
+ elif isinstance(error, json.JSONDecodeError):
280
+ error = create_error(ErrorCode.CONFIG_PARSE_ERROR, context=error_context, cause=error)
281
+
282
+ # Output error
283
+ print(format_error_for_user(error, verbose=verbose), file=sys.stderr)
284
+
285
+ if exit_on_error:
286
+ sys.exit(1)
287
+
288
+
289
+ def wrap_errors(operation: str):
290
+ """
291
+ Decorator to wrap function errors with context.
292
+
293
+ Usage:
294
+ @wrap_errors("loading session")
295
+ def load_session(path):
296
+ ...
297
+ """
298
+
299
+ def decorator(func):
300
+ def wrapper(*args, **kwargs):
301
+ try:
302
+ return func(*args, **kwargs)
303
+ except CostTrackingError:
304
+ raise
305
+ except Exception as e:
306
+ context = ErrorContext(operation=operation)
307
+ raise create_error(
308
+ ErrorCode.UNKNOWN_ERROR, context=context, custom_message=str(e), cause=e
309
+ ) from e
310
+
311
+ return wrapper
312
+
313
+ return decorator
314
+
315
+
316
+ class ErrorReporter:
317
+ """Collect and report multiple errors."""
318
+
319
+ def __init__(self):
320
+ self.errors: list = []
321
+ self.warnings: list = []
322
+
323
+ def add_error(self, error: Exception, context: str = ""):
324
+ """Add an error to the collection."""
325
+ self.errors.append((error, context))
326
+
327
+ def add_warning(self, message: str, context: str = ""):
328
+ """Add a warning to the collection."""
329
+ self.warnings.append((message, context))
330
+
331
+ def has_errors(self) -> bool:
332
+ """Check if any errors were recorded."""
333
+ return len(self.errors) > 0
334
+
335
+ def has_warnings(self) -> bool:
336
+ """Check if any warnings were recorded."""
337
+ return len(self.warnings) > 0
338
+
339
+ def format_report(self) -> str:
340
+ """Format all errors and warnings as a report."""
341
+ lines = []
342
+
343
+ if self.errors:
344
+ lines.append(f"\n❌ {len(self.errors)} Error(s):")
345
+ for i, (error, context) in enumerate(self.errors, 1):
346
+ lines.append(f" {i}. [{context}] {error}")
347
+
348
+ if self.warnings:
349
+ lines.append(f"\n⚠️ {len(self.warnings)} Warning(s):")
350
+ for i, (message, context) in enumerate(self.warnings, 1):
351
+ lines.append(f" {i}. [{context}] {message}")
352
+
353
+ return "\n".join(lines)
354
+
355
+ def print_report(self):
356
+ """Print the error report."""
357
+ if self.errors or self.warnings:
358
+ print(self.format_report(), file=sys.stderr)
359
+
360
+ def clear(self):
361
+ """Clear all errors and warnings."""
362
+ self.errors.clear()
363
+ self.warnings.clear()
364
+
365
+
366
+ # Logging helpers
367
+ def log_debug(message: str, **kwargs):
368
+ """Log a debug message (only in verbose mode)."""
369
+ if _verbose_mode:
370
+ extra = " ".join(f"{k}={v}" for k, v in kwargs.items())
371
+ print(f"[DEBUG] {message} {extra}", file=sys.stderr)
372
+
373
+
374
+ def log_info(message: str):
375
+ """Log an info message."""
376
+ print(f"ℹ️ {message}")
377
+
378
+
379
+ def log_warning(message: str):
380
+ """Log a warning message."""
381
+ print(f"⚠️ {message}", file=sys.stderr)
382
+
383
+
384
+ def log_error(message: str):
385
+ """Log an error message."""
386
+ print(f"❌ {message}", file=sys.stderr)
387
+
388
+
389
+ def log_success(message: str):
390
+ """Log a success message."""
391
+ print(f"✅ {message}")
392
+
393
+
394
+ # Verbose mode flag
395
+ _verbose_mode = False
396
+
397
+
398
+ def set_verbose(enabled: bool):
399
+ """Enable or disable verbose mode."""
400
+ global _verbose_mode
401
+ _verbose_mode = enabled
402
+
403
+
404
+ def is_verbose() -> bool:
405
+ """Check if verbose mode is enabled."""
406
+ return _verbose_mode
407
+
408
+
409
+ if __name__ == "__main__":
410
+ # Demo error handling
411
+ print("Error Handling Demo\n")
412
+
413
+ # Create and display different error types
414
+ errors = [
415
+ create_error(
416
+ ErrorCode.BUDGET_EXCEEDED,
417
+ context=ErrorContext(
418
+ operation="logging usage", model="opus", agent="DEV", budget=15.00
419
+ ),
420
+ ),
421
+ create_error(
422
+ ErrorCode.SESSION_CORRUPTED,
423
+ context=ErrorContext(
424
+ operation="loading session", file_path=Path("/path/to/session.json")
425
+ ),
426
+ ),
427
+ create_error(
428
+ ErrorCode.UNKNOWN_MODEL,
429
+ context=ErrorContext(
430
+ operation="calculating cost",
431
+ model="gpt-4", # Wrong model
432
+ ),
433
+ ),
434
+ ]
435
+
436
+ for error in errors:
437
+ print(format_error_for_user(error))
438
+ print()