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.
Files changed (70) hide show
  1. htmlgraph/.htmlgraph/.session-warning-state.json +1 -1
  2. htmlgraph/__init__.py +1 -1
  3. htmlgraph/api/main.py +50 -10
  4. htmlgraph/api/templates/dashboard-redesign.html +608 -54
  5. htmlgraph/api/templates/partials/activity-feed.html +21 -0
  6. htmlgraph/api/templates/partials/features.html +81 -12
  7. htmlgraph/api/templates/partials/orchestration.html +35 -0
  8. htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
  9. htmlgraph/cli/.htmlgraph/agents.json +72 -0
  10. htmlgraph/cli/__init__.py +42 -0
  11. htmlgraph/cli/__main__.py +6 -0
  12. htmlgraph/cli/analytics.py +939 -0
  13. htmlgraph/cli/base.py +660 -0
  14. htmlgraph/cli/constants.py +206 -0
  15. htmlgraph/cli/core.py +856 -0
  16. htmlgraph/cli/main.py +143 -0
  17. htmlgraph/cli/models.py +462 -0
  18. htmlgraph/cli/templates/__init__.py +1 -0
  19. htmlgraph/cli/templates/cost_dashboard.py +398 -0
  20. htmlgraph/cli/work/__init__.py +159 -0
  21. htmlgraph/cli/work/features.py +567 -0
  22. htmlgraph/cli/work/orchestration.py +675 -0
  23. htmlgraph/cli/work/sessions.py +465 -0
  24. htmlgraph/cli/work/tracks.py +485 -0
  25. htmlgraph/dashboard.html +6414 -634
  26. htmlgraph/db/schema.py +8 -3
  27. htmlgraph/docs/ORCHESTRATION_PATTERNS.md +20 -13
  28. htmlgraph/docs/README.md +2 -3
  29. htmlgraph/hooks/event_tracker.py +355 -26
  30. htmlgraph/hooks/git_commands.py +175 -0
  31. htmlgraph/hooks/orchestrator.py +137 -71
  32. htmlgraph/hooks/orchestrator_reflector.py +23 -0
  33. htmlgraph/hooks/pretooluse.py +29 -6
  34. htmlgraph/hooks/session_handler.py +28 -0
  35. htmlgraph/hooks/session_summary.py +391 -0
  36. htmlgraph/hooks/subagent_detection.py +202 -0
  37. htmlgraph/hooks/subagent_stop.py +71 -12
  38. htmlgraph/hooks/validator.py +192 -79
  39. htmlgraph/operations/__init__.py +18 -0
  40. htmlgraph/operations/initialization.py +596 -0
  41. htmlgraph/operations/initialization.py.backup +228 -0
  42. htmlgraph/orchestration/__init__.py +16 -1
  43. htmlgraph/orchestration/claude_launcher.py +185 -0
  44. htmlgraph/orchestration/command_builder.py +71 -0
  45. htmlgraph/orchestration/headless_spawner.py +72 -1332
  46. htmlgraph/orchestration/plugin_manager.py +136 -0
  47. htmlgraph/orchestration/prompts.py +137 -0
  48. htmlgraph/orchestration/spawners/__init__.py +16 -0
  49. htmlgraph/orchestration/spawners/base.py +194 -0
  50. htmlgraph/orchestration/spawners/claude.py +170 -0
  51. htmlgraph/orchestration/spawners/codex.py +442 -0
  52. htmlgraph/orchestration/spawners/copilot.py +299 -0
  53. htmlgraph/orchestration/spawners/gemini.py +478 -0
  54. htmlgraph/orchestration/subprocess_runner.py +33 -0
  55. htmlgraph/orchestration.md +563 -0
  56. htmlgraph/orchestrator-system-prompt-optimized.txt +620 -55
  57. htmlgraph/orchestrator_config.py +357 -0
  58. htmlgraph/orchestrator_mode.py +45 -12
  59. htmlgraph/transcript.py +16 -4
  60. htmlgraph-0.26.7.data/data/htmlgraph/dashboard.html +6592 -0
  61. {htmlgraph-0.26.5.dist-info → htmlgraph-0.26.7.dist-info}/METADATA +1 -1
  62. {htmlgraph-0.26.5.dist-info → htmlgraph-0.26.7.dist-info}/RECORD +68 -34
  63. {htmlgraph-0.26.5.dist-info → htmlgraph-0.26.7.dist-info}/entry_points.txt +1 -1
  64. htmlgraph/cli.py +0 -7256
  65. htmlgraph-0.26.5.data/data/htmlgraph/dashboard.html +0 -812
  66. {htmlgraph-0.26.5.data → htmlgraph-0.26.7.data}/data/htmlgraph/styles.css +0 -0
  67. {htmlgraph-0.26.5.data → htmlgraph-0.26.7.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  68. {htmlgraph-0.26.5.data → htmlgraph-0.26.7.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  69. {htmlgraph-0.26.5.data → htmlgraph-0.26.7.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  70. {htmlgraph-0.26.5.dist-info → htmlgraph-0.26.7.dist-info}/WHEEL +0 -0
@@ -0,0 +1,675 @@
1
+ """HtmlGraph CLI - Orchestration commands (Archive, Orchestrator, Claude)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+ from typing import TYPE_CHECKING
9
+
10
+ from rich.console import Console
11
+
12
+ from htmlgraph.cli.base import BaseCommand, CommandError, CommandResult
13
+ from htmlgraph.cli.constants import DEFAULT_GRAPH_DIR
14
+
15
+ if TYPE_CHECKING:
16
+ from argparse import _SubParsersAction
17
+
18
+ console = Console()
19
+
20
+
21
+ def register_archive_commands(subparsers: _SubParsersAction) -> None:
22
+ """Register archive management commands."""
23
+ archive_parser = subparsers.add_parser("archive", help="Archive management")
24
+ archive_subparsers = archive_parser.add_subparsers(
25
+ dest="archive_command", help="Archive command"
26
+ )
27
+
28
+ # archive create
29
+ archive_create = archive_subparsers.add_parser("create", help="Create archive")
30
+ archive_create.add_argument("entity_id", help="Entity ID to archive")
31
+ archive_create.add_argument(
32
+ "--graph-dir", "-g", default=DEFAULT_GRAPH_DIR, help="Graph directory"
33
+ )
34
+ archive_create.add_argument(
35
+ "--format", choices=["json", "text"], default="text", help="Output format"
36
+ )
37
+ archive_create.set_defaults(func=ArchiveCreateCommand.from_args)
38
+
39
+ # archive list
40
+ archive_list = archive_subparsers.add_parser("list", help="List archives")
41
+ archive_list.add_argument(
42
+ "--graph-dir", "-g", default=DEFAULT_GRAPH_DIR, help="Graph directory"
43
+ )
44
+ archive_list.add_argument(
45
+ "--format", choices=["json", "text"], default="text", help="Output format"
46
+ )
47
+ archive_list.set_defaults(func=ArchiveListCommand.from_args)
48
+
49
+
50
+ def register_orchestrator_commands(subparsers: _SubParsersAction) -> None:
51
+ """Register orchestrator commands."""
52
+ orchestrator_parser = subparsers.add_parser(
53
+ "orchestrator", help="Orchestrator management"
54
+ )
55
+ orchestrator_subparsers = orchestrator_parser.add_subparsers(
56
+ dest="orchestrator_command", help="Orchestrator command"
57
+ )
58
+
59
+ # orchestrator enable
60
+ orch_enable = orchestrator_subparsers.add_parser(
61
+ "enable", help="Enable orchestrator mode"
62
+ )
63
+ orch_enable.add_argument(
64
+ "--level",
65
+ "-l",
66
+ choices=["strict", "guidance"],
67
+ default="strict",
68
+ help="Enforcement level (default: strict)",
69
+ )
70
+ orch_enable.add_argument(
71
+ "--graph-dir", "-g", default=DEFAULT_GRAPH_DIR, help="Graph directory"
72
+ )
73
+ orch_enable.set_defaults(func=OrchestratorEnableCommand.from_args)
74
+
75
+ # orchestrator disable
76
+ orch_disable = orchestrator_subparsers.add_parser(
77
+ "disable", help="Disable orchestrator mode"
78
+ )
79
+ orch_disable.add_argument(
80
+ "--graph-dir", "-g", default=DEFAULT_GRAPH_DIR, help="Graph directory"
81
+ )
82
+ orch_disable.set_defaults(func=OrchestratorDisableCommand.from_args)
83
+
84
+ # orchestrator status
85
+ orch_status = orchestrator_subparsers.add_parser(
86
+ "status", help="Show orchestrator status"
87
+ )
88
+ orch_status.add_argument(
89
+ "--graph-dir", "-g", default=DEFAULT_GRAPH_DIR, help="Graph directory"
90
+ )
91
+ orch_status.add_argument(
92
+ "--format", choices=["json", "text"], default="text", help="Output format"
93
+ )
94
+ orch_status.set_defaults(func=OrchestratorStatusCommand.from_args)
95
+
96
+ # orchestrator config show
97
+ config_show = orchestrator_subparsers.add_parser(
98
+ "config-show", help="Show orchestrator configuration"
99
+ )
100
+ config_show.add_argument(
101
+ "--format", choices=["json", "text"], default="text", help="Output format"
102
+ )
103
+ config_show.set_defaults(func=OrchestratorConfigShowCommand.from_args)
104
+
105
+ # orchestrator config set
106
+ config_set = orchestrator_subparsers.add_parser(
107
+ "config-set", help="Set a configuration value"
108
+ )
109
+ config_set.add_argument(
110
+ "key", help="Config key (e.g., thresholds.exploration_calls)"
111
+ )
112
+ config_set.add_argument("value", type=int, help="New value")
113
+ config_set.add_argument(
114
+ "--format", choices=["json", "text"], default="text", help="Output format"
115
+ )
116
+ config_set.set_defaults(func=OrchestratorConfigSetCommand.from_args)
117
+
118
+ # orchestrator config reset
119
+ config_reset = orchestrator_subparsers.add_parser(
120
+ "config-reset", help="Reset configuration to defaults"
121
+ )
122
+ config_reset.add_argument(
123
+ "--format", choices=["json", "text"], default="text", help="Output format"
124
+ )
125
+ config_reset.set_defaults(func=OrchestratorConfigResetCommand.from_args)
126
+
127
+ # orchestrator reset-violations
128
+ reset_violations = orchestrator_subparsers.add_parser(
129
+ "reset-violations", help="Reset violation counter"
130
+ )
131
+ reset_violations.add_argument(
132
+ "--graph-dir", "-g", default=DEFAULT_GRAPH_DIR, help="Graph directory"
133
+ )
134
+ reset_violations.set_defaults(func=OrchestratorResetViolationsCommand.from_args)
135
+
136
+ # orchestrator set-level
137
+ set_level = orchestrator_subparsers.add_parser(
138
+ "set-level", help="Set enforcement level"
139
+ )
140
+ set_level.add_argument(
141
+ "level", choices=["strict", "guidance"], help="Enforcement level to set"
142
+ )
143
+ set_level.add_argument(
144
+ "--graph-dir", "-g", default=DEFAULT_GRAPH_DIR, help="Graph directory"
145
+ )
146
+ set_level.set_defaults(func=OrchestratorSetLevelCommand.from_args)
147
+
148
+
149
+ def register_claude_commands(subparsers: _SubParsersAction) -> None:
150
+ """Register Claude Code launcher commands."""
151
+ claude_parser = subparsers.add_parser(
152
+ "claude", help="Launch Claude Code with HtmlGraph integration"
153
+ )
154
+ claude_parser.add_argument(
155
+ "--init",
156
+ action="store_true",
157
+ help="Launch with orchestrator prompt and plugin installation",
158
+ )
159
+ claude_parser.add_argument(
160
+ "--continue",
161
+ dest="continue_session",
162
+ action="store_true",
163
+ help="Resume last session with orchestrator rules",
164
+ )
165
+ claude_parser.add_argument(
166
+ "--dev",
167
+ action="store_true",
168
+ help="Launch with local plugin for development",
169
+ )
170
+ claude_parser.set_defaults(func=ClaudeCommand.from_args)
171
+
172
+
173
+ # ============================================================================
174
+ # Archive Commands
175
+ # ============================================================================
176
+
177
+
178
+ class ArchiveCreateCommand(BaseCommand):
179
+ """Create an archive."""
180
+
181
+ def __init__(self, *, entity_id: str) -> None:
182
+ super().__init__()
183
+ self.entity_id = entity_id
184
+
185
+ @classmethod
186
+ def from_args(cls, args: argparse.Namespace) -> ArchiveCreateCommand:
187
+ return cls(entity_id=args.entity_id)
188
+
189
+ def execute(self) -> CommandResult:
190
+ """Create an archive."""
191
+ from htmlgraph.archive import ArchiveManager
192
+
193
+ if self.graph_dir is None:
194
+ raise CommandError("Missing graph directory")
195
+
196
+ htmlgraph_dir = Path(self.graph_dir).resolve()
197
+
198
+ if not htmlgraph_dir.exists():
199
+ raise CommandError(f"Directory not found: {htmlgraph_dir}")
200
+
201
+ with console.status("[blue]Initializing archive manager...", spinner="dots"):
202
+ manager = ArchiveManager(htmlgraph_dir)
203
+
204
+ try:
205
+ # Archive the entity
206
+ with console.status(f"[blue]Archiving {self.entity_id}...", spinner="dots"):
207
+ # For now, we'll use the older_than_days parameter with 0 to archive immediately
208
+ result = manager.archive_entities(older_than_days=0, dry_run=False)
209
+
210
+ from htmlgraph.cli.base import TextOutputBuilder
211
+
212
+ output = TextOutputBuilder()
213
+ output.add_success(f"Archived: {self.entity_id}")
214
+ output.add_field(
215
+ "Created", f"{len(result['archive_files'])} archive file(s)"
216
+ )
217
+ output.add_field("Total entities archived", result["archived_count"])
218
+
219
+ json_data = {
220
+ "entity_id": self.entity_id,
221
+ "archived": True,
222
+ "archive_files": result["archive_files"],
223
+ "count": result["archived_count"],
224
+ }
225
+
226
+ return CommandResult(
227
+ text=output.build(),
228
+ json_data=json_data,
229
+ )
230
+ finally:
231
+ manager.close()
232
+
233
+
234
+ class ArchiveListCommand(BaseCommand):
235
+ """List all archives."""
236
+
237
+ @classmethod
238
+ def from_args(cls, args: argparse.Namespace) -> ArchiveListCommand:
239
+ return cls()
240
+
241
+ def execute(self) -> CommandResult:
242
+ """List all archives."""
243
+ if self.graph_dir is None:
244
+ raise CommandError("Missing graph directory")
245
+
246
+ htmlgraph_dir = Path(self.graph_dir).resolve()
247
+
248
+ if not htmlgraph_dir.exists():
249
+ raise CommandError(f"Directory not found: {htmlgraph_dir}")
250
+
251
+ archive_dir = htmlgraph_dir / "archives"
252
+
253
+ if not archive_dir.exists():
254
+ from htmlgraph.cli.base import TextOutputBuilder
255
+
256
+ output = TextOutputBuilder()
257
+ output.add_warning("No archives found.")
258
+ return CommandResult(
259
+ text=output.build(),
260
+ json_data={"archives": []},
261
+ )
262
+
263
+ archive_files = sorted(archive_dir.glob("*.html"))
264
+
265
+ if not archive_files:
266
+ from htmlgraph.cli.base import TextOutputBuilder
267
+
268
+ output = TextOutputBuilder()
269
+ output.add_warning("No archives found.")
270
+ return CommandResult(
271
+ text=output.build(),
272
+ json_data={"archives": []},
273
+ )
274
+
275
+ # Create Rich table
276
+ from htmlgraph.cli.base import TableBuilder
277
+
278
+ builder = TableBuilder.create_list_table(
279
+ f"Archive Files ({len(archive_files)})"
280
+ )
281
+ builder.add_column("Filename", style="cyan", no_wrap=False)
282
+ builder.add_numeric_column("Size (KB)", style="yellow", width=12)
283
+ builder.add_timestamp_column("Modified", width=16)
284
+
285
+ file_list = []
286
+ for f in archive_files:
287
+ size_kb = f.stat().st_size / 1024
288
+ modified = datetime.fromtimestamp(f.stat().st_mtime)
289
+ modified_str = modified.strftime("%Y-%m-%d %H:%M")
290
+
291
+ builder.add_row(f.name, f"{size_kb:.1f}", modified_str)
292
+
293
+ file_list.append(
294
+ {
295
+ "filename": f.name,
296
+ "size_kb": size_kb,
297
+ "modified": modified.isoformat(),
298
+ }
299
+ )
300
+
301
+ # Return table object directly - TextFormatter will print it properly
302
+ return CommandResult(
303
+ data=builder.table,
304
+ json_data={"archives": file_list},
305
+ )
306
+
307
+
308
+ # ============================================================================
309
+ # Orchestrator Commands
310
+ # ============================================================================
311
+
312
+
313
+ class OrchestratorEnableCommand(BaseCommand):
314
+ """Enable orchestrator mode."""
315
+
316
+ def __init__(self, *, level: str = "strict") -> None:
317
+ super().__init__()
318
+ self.level: str = level
319
+
320
+ @classmethod
321
+ def from_args(cls, args: argparse.Namespace) -> OrchestratorEnableCommand:
322
+ return cls(level=getattr(args, "level", "strict"))
323
+
324
+ def execute(self) -> CommandResult:
325
+ """Enable orchestrator mode."""
326
+
327
+ from htmlgraph.orchestrator_mode import OrchestratorModeManager
328
+
329
+ if self.graph_dir is None:
330
+ raise CommandError("Missing graph directory")
331
+
332
+ manager = OrchestratorModeManager(self.graph_dir)
333
+ manager.enable(level=self.level) # type: ignore[arg-type]
334
+ status = manager.status()
335
+
336
+ from htmlgraph.cli.base import TextOutputBuilder
337
+
338
+ output = TextOutputBuilder()
339
+ if self.level == "strict":
340
+ output.add_success("Orchestrator mode enabled (strict enforcement)")
341
+ else:
342
+ output.add_success("Orchestrator mode enabled (guidance mode)")
343
+ output.add_field("Level", self.level)
344
+ if status.get("activated_at"):
345
+ output.add_field("Activated at", status["activated_at"])
346
+
347
+ return CommandResult(
348
+ text=output.build(),
349
+ json_data=status,
350
+ )
351
+
352
+
353
+ class OrchestratorDisableCommand(BaseCommand):
354
+ """Disable orchestrator mode."""
355
+
356
+ @classmethod
357
+ def from_args(cls, args: argparse.Namespace) -> OrchestratorDisableCommand:
358
+ return cls()
359
+
360
+ def execute(self) -> CommandResult:
361
+ """Disable orchestrator mode."""
362
+ from htmlgraph.orchestrator_mode import OrchestratorModeManager
363
+
364
+ if self.graph_dir is None:
365
+ raise CommandError("Missing graph directory")
366
+
367
+ manager = OrchestratorModeManager(self.graph_dir)
368
+ manager.disable(by_user=True)
369
+ status = manager.status()
370
+
371
+ from htmlgraph.cli.base import TextOutputBuilder
372
+
373
+ output = TextOutputBuilder()
374
+ output.add_success("Orchestrator mode disabled")
375
+ output.add_field("Status", "Disabled by user (auto-activation prevented)")
376
+
377
+ return CommandResult(
378
+ text=output.build(),
379
+ json_data=status,
380
+ )
381
+
382
+
383
+ class OrchestratorStatusCommand(BaseCommand):
384
+ """Show orchestrator status."""
385
+
386
+ @classmethod
387
+ def from_args(cls, args: argparse.Namespace) -> OrchestratorStatusCommand:
388
+ return cls()
389
+
390
+ def execute(self) -> CommandResult:
391
+ """Show orchestrator status."""
392
+ from htmlgraph.orchestrator_mode import OrchestratorModeManager
393
+
394
+ if self.graph_dir is None:
395
+ raise CommandError("Missing graph directory")
396
+
397
+ manager = OrchestratorModeManager(self.graph_dir)
398
+ mode = manager.load()
399
+ status = manager.status()
400
+
401
+ from htmlgraph.cli.base import TextOutputBuilder
402
+
403
+ output = TextOutputBuilder()
404
+ if status.get("enabled"):
405
+ if status.get("enforcement_level") == "strict":
406
+ output.add_line("Orchestrator mode: enabled (strict enforcement)")
407
+ else:
408
+ output.add_line("Orchestrator mode: enabled (guidance mode)")
409
+ else:
410
+ output.add_line("Orchestrator mode: disabled")
411
+ if mode.disabled_by_user:
412
+ output.add_field(
413
+ "Status", "Disabled by user (auto-activation prevented)"
414
+ )
415
+
416
+ if status.get("activated_at"):
417
+ output.add_field("Activated at", status["activated_at"])
418
+ if status.get("violations") is not None:
419
+ output.add_field("Violations", f"{status['violations']}/3")
420
+ if status.get("circuit_breaker_triggered"):
421
+ output.add_field("Circuit breaker", "TRIGGERED")
422
+
423
+ return CommandResult(
424
+ data=status,
425
+ text=output.build(),
426
+ json_data=status,
427
+ )
428
+
429
+
430
+ class OrchestratorConfigShowCommand(BaseCommand):
431
+ """Show orchestrator configuration."""
432
+
433
+ @classmethod
434
+ def from_args(cls, args: argparse.Namespace) -> OrchestratorConfigShowCommand:
435
+ return cls()
436
+
437
+ def execute(self) -> CommandResult:
438
+ """Show orchestrator configuration."""
439
+ from htmlgraph.orchestrator_config import (
440
+ format_config_display,
441
+ load_orchestrator_config,
442
+ )
443
+
444
+ config = load_orchestrator_config()
445
+ text_output = format_config_display(config)
446
+
447
+ return CommandResult(
448
+ text=text_output,
449
+ json_data=config.model_dump(),
450
+ )
451
+
452
+
453
+ class OrchestratorConfigSetCommand(BaseCommand):
454
+ """Set a configuration value."""
455
+
456
+ def __init__(self, *, key: str, value: int) -> None:
457
+ super().__init__()
458
+ self.key = key
459
+ self.value = value
460
+
461
+ @classmethod
462
+ def from_args(cls, args: argparse.Namespace) -> OrchestratorConfigSetCommand:
463
+ return cls(key=args.key, value=args.value)
464
+
465
+ def execute(self) -> CommandResult:
466
+ """Set a configuration value."""
467
+ from htmlgraph.orchestrator_config import (
468
+ get_config_paths,
469
+ load_orchestrator_config,
470
+ save_orchestrator_config,
471
+ set_config_value,
472
+ )
473
+
474
+ # Load current config
475
+ config = load_orchestrator_config()
476
+
477
+ try:
478
+ # Set the value
479
+ set_config_value(config, self.key, self.value)
480
+
481
+ # Save to first config path (project-specific)
482
+ config_path = get_config_paths()[0]
483
+ save_orchestrator_config(config, config_path)
484
+
485
+ from htmlgraph.cli.base import TextOutputBuilder
486
+
487
+ output = TextOutputBuilder()
488
+ output.add_success(f"Configuration updated: {self.key} = {self.value}")
489
+ output.add_field("Config file", str(config_path))
490
+
491
+ return CommandResult(
492
+ text=output.build(),
493
+ json_data={
494
+ "key": self.key,
495
+ "value": self.value,
496
+ "path": str(config_path),
497
+ },
498
+ )
499
+ except KeyError as e:
500
+ raise CommandError(f"Invalid config key: {e}")
501
+
502
+
503
+ class OrchestratorConfigResetCommand(BaseCommand):
504
+ """Reset configuration to defaults."""
505
+
506
+ @classmethod
507
+ def from_args(cls, args: argparse.Namespace) -> OrchestratorConfigResetCommand:
508
+ return cls()
509
+
510
+ def execute(self) -> CommandResult:
511
+ """Reset configuration to defaults."""
512
+ from htmlgraph.orchestrator_config import (
513
+ OrchestratorConfig,
514
+ get_config_paths,
515
+ save_orchestrator_config,
516
+ )
517
+
518
+ # Create default config
519
+ config = OrchestratorConfig()
520
+
521
+ # Save to first config path (project-specific)
522
+ config_path = get_config_paths()[0]
523
+ save_orchestrator_config(config, config_path)
524
+
525
+ from htmlgraph.cli.base import TextOutputBuilder
526
+
527
+ output = TextOutputBuilder()
528
+ output.add_success("Configuration reset to defaults")
529
+ output.add_field("Config file", str(config_path))
530
+ output.add_field("Exploration calls", config.thresholds.exploration_calls)
531
+ output.add_field(
532
+ "Circuit breaker", config.thresholds.circuit_breaker_violations
533
+ )
534
+ output.add_field(
535
+ "Violation decay", f"{config.thresholds.violation_decay_seconds}s"
536
+ )
537
+
538
+ return CommandResult(
539
+ text=output.build(),
540
+ json_data=config.model_dump(),
541
+ )
542
+
543
+
544
+ class OrchestratorResetViolationsCommand(BaseCommand):
545
+ """Reset violation counter."""
546
+
547
+ @classmethod
548
+ def from_args(cls, args: argparse.Namespace) -> OrchestratorResetViolationsCommand:
549
+ return cls()
550
+
551
+ def execute(self) -> CommandResult:
552
+ """Reset violation counter."""
553
+ from htmlgraph.orchestrator_mode import OrchestratorModeManager
554
+
555
+ if self.graph_dir is None:
556
+ raise CommandError("Missing graph directory")
557
+
558
+ manager = OrchestratorModeManager(self.graph_dir)
559
+
560
+ if not manager.status().get("enabled"):
561
+ console.print("[yellow]Orchestrator mode is not enabled[/yellow]")
562
+ return CommandResult(
563
+ text="Orchestrator mode is not enabled",
564
+ json_data={"success": False, "message": "not enabled"},
565
+ )
566
+
567
+ manager.reset_violations()
568
+ status = manager.status()
569
+
570
+ from htmlgraph.cli.base import TextOutputBuilder
571
+
572
+ output = TextOutputBuilder()
573
+ output.add_success("Violations reset")
574
+ output.add_field("Violation count", status.get("violations", 0))
575
+ output.add_field(
576
+ "Circuit breaker",
577
+ "Normal" if not status.get("circuit_breaker_triggered") else "TRIGGERED",
578
+ )
579
+
580
+ return CommandResult(
581
+ text=output.build(),
582
+ json_data=status,
583
+ )
584
+
585
+
586
+ class OrchestratorSetLevelCommand(BaseCommand):
587
+ """Set enforcement level."""
588
+
589
+ def __init__(self, *, level: str) -> None:
590
+ super().__init__()
591
+ self.level: str = level
592
+
593
+ @classmethod
594
+ def from_args(cls, args: argparse.Namespace) -> OrchestratorSetLevelCommand:
595
+ return cls(level=args.level)
596
+
597
+ def execute(self) -> CommandResult:
598
+ """Set enforcement level."""
599
+ from htmlgraph.orchestrator_mode import OrchestratorModeManager
600
+
601
+ if self.graph_dir is None:
602
+ raise CommandError("Missing graph directory")
603
+
604
+ manager = OrchestratorModeManager(self.graph_dir)
605
+ manager.set_level(self.level) # type: ignore[arg-type]
606
+ status = manager.status()
607
+
608
+ from htmlgraph.cli.base import TextOutputBuilder
609
+
610
+ output = TextOutputBuilder()
611
+ output.add_success(f"Enforcement level changed to '{self.level}'")
612
+ if self.level == "strict":
613
+ output.add_field("Mode", "Strict enforcement")
614
+ else:
615
+ output.add_field("Mode", "Guidance mode")
616
+
617
+ return CommandResult(
618
+ text=output.build(),
619
+ json_data=status,
620
+ )
621
+
622
+
623
+ # ============================================================================
624
+ # Claude Code Launcher Commands
625
+ # ============================================================================
626
+
627
+
628
+ class ClaudeCommand(BaseCommand):
629
+ """Launch Claude Code with HtmlGraph integration."""
630
+
631
+ def __init__(
632
+ self,
633
+ *,
634
+ init: bool,
635
+ continue_session: bool,
636
+ dev: bool,
637
+ quiet: bool,
638
+ format: str,
639
+ ) -> None:
640
+ super().__init__()
641
+ self.init = init
642
+ self.continue_session = continue_session
643
+ self.dev = dev
644
+ self.quiet = quiet
645
+ self.format = format
646
+
647
+ @classmethod
648
+ def from_args(cls, args: argparse.Namespace) -> ClaudeCommand:
649
+ return cls(
650
+ init=getattr(args, "init", False),
651
+ continue_session=getattr(args, "continue_session", False),
652
+ dev=getattr(args, "dev", False),
653
+ quiet=getattr(args, "quiet", False),
654
+ format=getattr(args, "format", "text"),
655
+ )
656
+
657
+ def execute(self) -> CommandResult:
658
+ """Launch Claude Code."""
659
+ from htmlgraph.orchestration.claude_launcher import ClaudeLauncher
660
+
661
+ # Create args namespace for launcher
662
+ launcher_args = argparse.Namespace(
663
+ init=self.init,
664
+ continue_session=self.continue_session,
665
+ dev=self.dev,
666
+ quiet=self.quiet,
667
+ format=self.format,
668
+ )
669
+
670
+ # Launch Claude Code
671
+ launcher = ClaudeLauncher(launcher_args)
672
+ launcher.launch()
673
+
674
+ # This won't be reached because launcher.launch() calls subprocess
675
+ return CommandResult(text="Claude Code launched")