anvil-dev-framework 0.1.6 → 0.1.8

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 (87) hide show
  1. package/README.md +33 -13
  2. package/VERSION +1 -1
  3. package/docs/ANV-263-hook-logging-investigation.md +116 -0
  4. package/docs/INSTALLATION.md +18 -0
  5. package/docs/command-reference.md +302 -2
  6. package/docs/session-workflow.md +62 -9
  7. package/docs/system-architecture.md +569 -0
  8. package/global/commands/anvil-settings.md +3 -1
  9. package/global/commands/audit.md +163 -0
  10. package/global/commands/checklist.md +180 -0
  11. package/global/commands/efficiency.md +356 -0
  12. package/global/commands/evidence.md +99 -32
  13. package/global/commands/insights.md +101 -3
  14. package/global/commands/orient.md +29 -0
  15. package/global/commands/patterns.md +115 -0
  16. package/global/commands/ralph.md +47 -1
  17. package/global/commands/token-budget.md +214 -0
  18. package/global/lib/__pycache__/agent_registry.cpython-314.pyc +0 -0
  19. package/global/lib/__pycache__/claim_service.cpython-314.pyc +0 -0
  20. package/global/lib/__pycache__/coderabbit_service.cpython-314.pyc +0 -0
  21. package/global/lib/__pycache__/context_optimizer.cpython-314.pyc +0 -0
  22. package/global/lib/__pycache__/coordination_service.cpython-314.pyc +0 -0
  23. package/global/lib/__pycache__/doc_coverage_service.cpython-314.pyc +0 -0
  24. package/global/lib/__pycache__/gate_logger.cpython-314.pyc +0 -0
  25. package/global/lib/__pycache__/git_utils.cpython-314.pyc +0 -0
  26. package/global/lib/__pycache__/github_service.cpython-314.pyc +0 -0
  27. package/global/lib/__pycache__/handoff_generator.cpython-314.pyc +0 -0
  28. package/global/lib/__pycache__/hygiene_service.cpython-314.pyc +0 -0
  29. package/global/lib/__pycache__/issue_models.cpython-314.pyc +0 -0
  30. package/global/lib/__pycache__/issue_provider.cpython-314.pyc +0 -0
  31. package/global/lib/__pycache__/linear_data_service.cpython-314.pyc +0 -0
  32. package/global/lib/__pycache__/linear_provider.cpython-314.pyc +0 -0
  33. package/global/lib/__pycache__/local_provider.cpython-314.pyc +0 -0
  34. package/global/lib/__pycache__/optimization_applier.cpython-314.pyc +0 -0
  35. package/global/lib/__pycache__/orient_fast.cpython-314.pyc +0 -0
  36. package/global/lib/__pycache__/quality_service.cpython-314.pyc +0 -0
  37. package/global/lib/__pycache__/ralph_prompt_generator.cpython-314.pyc +0 -0
  38. package/global/lib/__pycache__/ralph_state.cpython-314.pyc +0 -0
  39. package/global/lib/__pycache__/state_manager.cpython-314.pyc +0 -0
  40. package/global/lib/__pycache__/token_analyzer.cpython-314.pyc +0 -0
  41. package/global/lib/__pycache__/token_metrics.cpython-314.pyc +0 -0
  42. package/global/lib/__pycache__/transcript_parser.cpython-314.pyc +0 -0
  43. package/global/lib/__pycache__/verification_runner.cpython-314.pyc +0 -0
  44. package/global/lib/__pycache__/verify_iteration.cpython-314.pyc +0 -0
  45. package/global/lib/__pycache__/verify_subagent.cpython-314.pyc +0 -0
  46. package/global/lib/context_optimizer.py +323 -0
  47. package/global/lib/git_utils.py +267 -0
  48. package/global/lib/issue_models.py +28 -0
  49. package/global/lib/linear_provider.py +217 -16
  50. package/global/lib/optimization_applier.py +582 -0
  51. package/global/lib/orient_fast.py +24 -1
  52. package/global/lib/ralph_state.py +264 -24
  53. package/global/lib/token_analyzer.py +1357 -0
  54. package/global/lib/token_metrics.py +873 -0
  55. package/global/tests/__pycache__/test_context_optimizer.cpython-314-pytest-9.0.2.pyc +0 -0
  56. package/global/tests/__pycache__/test_doc_coverage.cpython-314-pytest-9.0.2.pyc +0 -0
  57. package/global/tests/__pycache__/test_git_utils.cpython-314-pytest-9.0.2.pyc +0 -0
  58. package/global/tests/__pycache__/test_issue_models.cpython-314-pytest-9.0.2.pyc +0 -0
  59. package/global/tests/__pycache__/test_linear_filtering.cpython-314-pytest-9.0.2.pyc +0 -0
  60. package/global/tests/__pycache__/test_linear_provider.cpython-314-pytest-9.0.2.pyc +0 -0
  61. package/global/tests/__pycache__/test_local_provider.cpython-314-pytest-9.0.2.pyc +0 -0
  62. package/global/tests/__pycache__/test_optimization_applier.cpython-314-pytest-9.0.2.pyc +0 -0
  63. package/global/tests/__pycache__/test_token_analyzer.cpython-314-pytest-9.0.2.pyc +0 -0
  64. package/global/tests/__pycache__/test_token_analyzer_phase6.cpython-314-pytest-9.0.2.pyc +0 -0
  65. package/global/tests/__pycache__/test_token_metrics.cpython-314-pytest-9.0.2.pyc +0 -0
  66. package/global/tests/test_context_optimizer.py +321 -0
  67. package/global/tests/test_git_utils.py +160 -0
  68. package/global/tests/test_issue_models.py +40 -0
  69. package/global/tests/test_linear_filtering.py +319 -0
  70. package/global/tests/test_linear_provider.py +125 -0
  71. package/global/tests/test_optimization_applier.py +508 -0
  72. package/global/tests/test_token_analyzer.py +735 -0
  73. package/global/tests/test_token_analyzer_phase6.py +537 -0
  74. package/global/tests/test_token_metrics.py +791 -0
  75. package/global/tools/anvil-memory/src/__tests__/commands.test.ts +238 -1
  76. package/global/tools/anvil-memory/src/commands/ralph-iteration.ts +249 -0
  77. package/global/tools/anvil-memory/src/index.ts +2 -8
  78. package/package.json +1 -1
  79. package/scripts/anvil +7 -2
  80. package/global/tools/anvil-memory/src/__tests__/ccs/context-monitor.test.ts +0 -535
  81. package/global/tools/anvil-memory/src/__tests__/ccs/edge-cases.test.ts +0 -645
  82. package/global/tools/anvil-memory/src/__tests__/ccs/fixtures.ts +0 -363
  83. package/global/tools/anvil-memory/src/__tests__/ccs/index.ts +0 -8
  84. package/global/tools/anvil-memory/src/__tests__/ccs/integration.test.ts +0 -417
  85. package/global/tools/anvil-memory/src/__tests__/ccs/prompt-generator.test.ts +0 -571
  86. package/global/tools/anvil-memory/src/__tests__/ccs/ralph-stop.test.ts +0 -440
  87. package/global/tools/anvil-memory/src/__tests__/ccs/test-utils.ts +0 -252
@@ -580,9 +580,92 @@ To resume later, run: /ralph start "{self.objective}"
580
580
 
581
581
 
582
582
  # =========================================================================
583
- # Linear Integration (ANV-211)
583
+ # Linear Integration (ANV-211, ANV-214)
584
584
  # =========================================================================
585
585
 
586
+ @classmethod
587
+ def initialize_from_project(
588
+ cls,
589
+ project_name: str,
590
+ no_sync: bool = False,
591
+ include_done: bool = False,
592
+ state_file: str = DEFAULT_STATE_FILE,
593
+ ) -> "RalphState":
594
+ """Initialize Ralph session from a Linear project (ANV-214).
595
+
596
+ Args:
597
+ project_name: Linear project name (e.g., "HUD Development")
598
+ no_sync: If True, don't sync status updates back to Linear
599
+ include_done: If True, include already-completed issues
600
+ state_file: Path to state file
601
+
602
+ Returns:
603
+ Initialized RalphState with Linear integration enabled in project mode
604
+ """
605
+ provider = _create_linear_provider()
606
+
607
+ try:
608
+ issues = provider.get_project_issues(
609
+ project_name=project_name, include_done=include_done
610
+ )
611
+ except (KeyError, AttributeError):
612
+ raise ValueError(f"Project '{project_name}' not found in Linear") from None
613
+
614
+ if not issues:
615
+ raise ValueError(f"Project '{project_name}' has no issues to process")
616
+
617
+ # Build LinearSubtask objects from Issue instances
618
+ subtasks = []
619
+ todo_items = []
620
+ for issue in issues:
621
+ issue_status = issue.status.value.lower() if issue.status else ""
622
+ status = "completed" if issue_status in ("done", "completed", "closed") else "todo"
623
+ subtask = LinearSubtask(
624
+ id=issue.id,
625
+ identifier=issue.identifier,
626
+ title=issue.title,
627
+ status=status,
628
+ )
629
+ subtasks.append(subtask)
630
+ if status == "todo":
631
+ todo_items.append(f"[{issue.identifier}] {issue.title}")
632
+
633
+ # Create LinearIntegration in project mode
634
+ linear_integration = LinearIntegration(
635
+ enabled=True,
636
+ mode="project",
637
+ parent_issue=None,
638
+ parent_id=None,
639
+ project_name=project_name,
640
+ subtasks=subtasks,
641
+ last_sync=datetime.now(timezone.utc).isoformat(),
642
+ no_sync=no_sync,
643
+ )
644
+
645
+ # Initialize state
646
+ state = cls(
647
+ task_name=f"Project: {project_name}",
648
+ objective=f"Complete all issues in project '{project_name}'",
649
+ iteration=0,
650
+ started_at=datetime.now(timezone.utc).isoformat(),
651
+ status="running",
652
+ no_change_count=0,
653
+ last_diff_hash="",
654
+ error_hashes=[],
655
+ max_iterations=DEFAULT_MAX_ITERATIONS,
656
+ completion_promise=DEFAULT_COMPLETION_PROMISE,
657
+ todo_items=todo_items,
658
+ completed_items=[],
659
+ context_checkpoint=None,
660
+ context_history=[],
661
+ linear_integration=linear_integration,
662
+ state_file=state_file,
663
+ )
664
+
665
+ Path(state_file).parent.mkdir(parents=True, exist_ok=True)
666
+ state.save()
667
+ return state
668
+
586
669
  @classmethod
587
670
  def initialize_from_linear(
588
671
  cls,
@@ -761,43 +844,102 @@ To resume later, run: /ralph start "{self.objective}"
761
844
  return True
762
845
  return False
763
846
 
764
- def sync_to_linear(self, identifier: str, state_name: str) -> bool:
765
- """Sync subtask status to Linear.
847
+ def sync_to_linear(
848
+ self,
849
+ identifier: str,
850
+ state_name: str,
851
+ max_retries: int = 3,
852
+ retry_delay: float = 1.0,
853
+ ) -> bool:
854
+ """Sync subtask status to Linear with retry logic (ANV-215).
766
855
 
767
856
  Args:
768
857
  identifier: Linear issue identifier
769
858
  state_name: Target state name ("done", "in_progress", etc.)
859
+ max_retries: Maximum number of retry attempts
860
+ retry_delay: Initial delay between retries (exponential backoff)
770
861
 
771
862
  Returns:
772
863
  True if sync succeeded
773
864
  """
865
+ import time
866
+
774
867
  if not self.linear_integration or self.linear_integration.no_sync:
775
868
  return False
776
869
 
777
- try:
778
- # ANV-120: Use _create_linear_provider() to ensure proper team config
779
- provider = _create_linear_provider()
780
-
781
- # Import IssueStatus for state conversion
870
+ last_error = None
871
+ for attempt in range(max_retries):
782
872
  try:
783
- from .issue_models import IssueStatus
784
- except ImportError:
785
- from issue_models import IssueStatus
873
+ # ANV-120: Use _create_linear_provider() to ensure proper team config
874
+ provider = _create_linear_provider()
786
875
 
787
- # Convert state name to IssueStatus enum
788
- target_status = IssueStatus.from_linear_state(state_name)
876
+ # Import IssueStatus for state conversion
877
+ try:
878
+ from .issue_models import IssueStatus
879
+ except ImportError:
880
+ from issue_models import IssueStatus
789
881
 
790
- # Update issue with the converted status
791
- provider.update_issue(identifier, status=target_status)
792
- self.linear_integration.last_sync = datetime.now(timezone.utc).isoformat()
793
- self.save()
794
- return True
795
- except (ImportError, AttributeError, ValueError, OSError) as e:
796
- # Log the error for debugging but don't crash
797
- # Catch: ImportError (module issues), AttributeError (API mismatch),
798
- # ValueError (bad data), OSError (network/auth failures)
799
- print(f"Warning: Failed to sync {identifier} to Linear: {e}")
800
- return False
882
+ # Convert state name to IssueStatus enum
883
+ target_status = IssueStatus.from_linear_state(state_name)
884
+
885
+ # Update issue with the converted status
886
+ provider.update_issue(identifier, status=target_status)
887
+ self.linear_integration.last_sync = datetime.now(timezone.utc).isoformat()
888
+ self.save()
889
+ return True
890
+ except (ImportError, AttributeError, ValueError) as e:
891
+ # Non-retryable errors - fail immediately
892
+ print(f"Warning: Failed to sync {identifier} to Linear: {e}")
893
+ return False
894
+ except (OSError, Exception) as e:
895
+ # Retryable error (network/timeout/rate limit/API error)
896
+ # LinearProvider raises generic Exception for HTTP errors
897
+ last_error = e
898
+ if attempt < max_retries - 1:
899
+ delay = retry_delay * (2 ** attempt) # Exponential backoff
900
+ print(f"Warning: Sync failed, retrying in {delay}s: {e}")
901
+ time.sleep(delay)
902
+
903
+ # All retries exhausted
904
+ print(f"Warning: Failed to sync {identifier} after {max_retries} attempts: {last_error}")
905
+ return False
906
+
907
+ def mark_subtask_skipped(self, identifier: str, reason: str) -> bool:
908
+ """Mark a Linear subtask as skipped (ANV-215).
909
+
910
+ Args:
911
+ identifier: Linear issue identifier (e.g., "ANV-215")
912
+ reason: Reason for skipping
913
+
914
+ Returns:
915
+ True if subtask was found and updated
916
+ """
917
+ return self.mark_subtask_complete(identifier, skip_reason=reason)
918
+
919
+ def handle_missing_subtask(self, identifier: str) -> None:
920
+ """Handle a missing subtask gracefully (ANV-215).
921
+
922
+ Logs a warning and marks the subtask as skipped in local state
923
+ without attempting to sync to Linear.
924
+
925
+ Args:
926
+ identifier: Linear issue identifier
927
+ """
928
+ print(f"Warning: Subtask {identifier} not found in Linear, marking as skipped")
929
+ if self.linear_integration and self.linear_integration.enabled:
930
+ for subtask in self.linear_integration.subtasks:
931
+ if subtask.identifier == identifier:
932
+ subtask.status = "skipped"
933
+ subtask.skip_reason = "Not found in Linear"
934
+ subtask.completed_at = datetime.now(timezone.utc).isoformat()
935
+
936
+ # Remove from todo_items
937
+ item_prefix = f"[{identifier}]"
938
+ self.todo_items = [t for t in self.todo_items if not t.startswith(item_prefix)]
939
+ self.save()
940
+ return
941
+ # Subtask not found in local state
942
+ print(f"Warning: {identifier} not found in local state, no action taken")
801
943
 
802
944
  def get_linear_progress(self) -> Tuple[int, int, int]:
803
945
  """Get Linear subtask progress counts.
@@ -813,6 +955,49 @@ To resume later, run: /ralph start "{self.objective}"
813
955
  remaining = sum(1 for s in self.linear_integration.subtasks if s.status == "todo")
814
956
  return (completed, skipped, remaining)
815
957
 
958
+ def is_project_complete(self) -> bool:
959
+ """Check if all issues in project are complete (ANV-214).
960
+
961
+ Returns:
962
+ True if Linear integration is enabled in project mode and all
963
+ issues are either completed or skipped.
964
+ """
965
+ if not self.linear_integration or not self.linear_integration.enabled:
966
+ return False
967
+ if self.linear_integration.mode != "project":
968
+ return False
969
+ if not self.linear_integration.subtasks:
970
+ return False
971
+ return all(s.status != "todo" for s in self.linear_integration.subtasks)
972
+
973
+ def get_current_issue_context(self) -> Optional[Dict[str, Any]]:
974
+ """Get context for the current issue being worked on (ANV-214).
975
+
976
+ Returns:
977
+ Dictionary with current issue details, or None if no current issue.
978
+ """
979
+ next_subtask = self.get_next_subtask()
980
+ if not next_subtask:
981
+ return None
982
+
983
+ return {
984
+ "id": next_subtask.id,
985
+ "identifier": next_subtask.identifier,
986
+ "title": next_subtask.title,
987
+ "status": next_subtask.status,
988
+ "mode": self.linear_integration.mode if self.linear_integration else "issue",
989
+ "project_name": (
990
+ self.linear_integration.project_name
991
+ if self.linear_integration
992
+ else None
993
+ ),
994
+ "parent_issue": (
995
+ self.linear_integration.parent_issue
996
+ if self.linear_integration
997
+ else None
998
+ ),
999
+ }
1000
+
816
1001
 
817
1002
  # =============================================================================
818
1003
  # File Generation
@@ -1056,6 +1241,20 @@ def main():
1056
1241
  "--no-sync", action="store_true", help="Don't sync status back to Linear"
1057
1242
  )
1058
1243
 
1244
+ # Init-project command (ANV-214)
1245
+ init_project_parser = subparsers.add_parser(
1246
+ "init-project", help="Initialize Ralph from Linear project"
1247
+ )
1248
+ init_project_parser.add_argument(
1249
+ "--project", required=True, help="Linear project name (e.g., 'HUD Development')"
1250
+ )
1251
+ init_project_parser.add_argument(
1252
+ "--no-sync", action="store_true", help="Don't sync status back to Linear"
1253
+ )
1254
+ init_project_parser.add_argument(
1255
+ "--include-done", action="store_true", help="Include already-completed issues"
1256
+ )
1257
+
1059
1258
  # Sync command (ANV-211)
1060
1259
  sync_parser = subparsers.add_parser("sync", help="Sync status with Linear")
1061
1260
  # --complete and --skip are mutually exclusive
@@ -1161,6 +1360,47 @@ def main():
1161
1360
  except ImportError as e:
1162
1361
  print(f"Error: Could not load Linear provider - {e}")
1163
1362
 
1363
+ elif args.command == "init-project":
1364
+ try:
1365
+ state = RalphState.initialize_from_project(
1366
+ project_name=args.project,
1367
+ no_sync=args.no_sync,
1368
+ include_done=args.include_done,
1369
+ )
1370
+
1371
+ # Create supporting files
1372
+ create_fix_plan(
1373
+ state.task_name,
1374
+ state.objective,
1375
+ state.todo_items,
1376
+ )
1377
+ create_progress_file(
1378
+ state.task_name,
1379
+ state.objective,
1380
+ len(state.todo_items),
1381
+ )
1382
+ create_prompt_file(
1383
+ state.task_name,
1384
+ state.objective,
1385
+ remaining_count=len(state.todo_items),
1386
+ total_items=len(state.todo_items),
1387
+ started_at=state.started_at,
1388
+ )
1389
+
1390
+ completed, skipped, remaining = state.get_linear_progress()
1391
+ print(f"Ralph session initialized from project: {args.project}")
1392
+ print(f" State file: {state.state_file}")
1393
+ print(f" Issues: {remaining} todo, {completed} already done")
1394
+ if args.include_done:
1395
+ print(" Include done: YES (processing all issues)")
1396
+ if args.no_sync:
1397
+ print(" Sync: DISABLED (changes won't update Linear)")
1398
+
1399
+ except ValueError as e:
1400
+ print(f"Error: {e}")
1401
+ except ImportError as e:
1402
+ print(f"Error: Could not load Linear provider - {e}")
1403
+
1164
1404
  elif args.command == "sync":
1165
1405
  state = RalphState.load()
1166
1406
  if not state: