monoco-toolkit 0.3.6__py3-none-any.whl → 0.3.10__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 (113) hide show
  1. monoco/cli/workspace.py +1 -1
  2. monoco/core/config.py +58 -0
  3. monoco/core/hooks/__init__.py +19 -0
  4. monoco/core/hooks/base.py +104 -0
  5. monoco/core/hooks/builtin/__init__.py +11 -0
  6. monoco/core/hooks/builtin/git_cleanup.py +266 -0
  7. monoco/core/hooks/builtin/logging_hook.py +78 -0
  8. monoco/core/hooks/context.py +131 -0
  9. monoco/core/hooks/registry.py +222 -0
  10. monoco/core/injection.py +63 -29
  11. monoco/core/integrations.py +8 -2
  12. monoco/core/output.py +5 -5
  13. monoco/core/registry.py +9 -1
  14. monoco/core/resource/__init__.py +5 -0
  15. monoco/core/resource/finder.py +98 -0
  16. monoco/core/resource/manager.py +91 -0
  17. monoco/core/resource/models.py +35 -0
  18. monoco/core/resources/en/{SKILL.md → skills/monoco_core/SKILL.md} +2 -0
  19. monoco/core/resources/zh/{SKILL.md → skills/monoco_core/SKILL.md} +2 -0
  20. monoco/core/setup.py +1 -1
  21. monoco/core/skill_framework.py +292 -0
  22. monoco/core/skills.py +538 -254
  23. monoco/core/sync.py +73 -1
  24. monoco/core/workflow_converter.py +420 -0
  25. monoco/features/{scheduler → agent}/__init__.py +5 -3
  26. monoco/features/agent/adapter.py +31 -0
  27. monoco/features/agent/apoptosis.py +44 -0
  28. monoco/features/agent/cli.py +296 -0
  29. monoco/features/agent/config.py +96 -0
  30. monoco/features/agent/defaults.py +12 -0
  31. monoco/features/{scheduler → agent}/engines.py +32 -6
  32. monoco/features/agent/flow_skills.py +281 -0
  33. monoco/features/agent/manager.py +91 -0
  34. monoco/features/{scheduler → agent}/models.py +6 -3
  35. monoco/features/agent/resources/atoms/atom-code-dev.yaml +61 -0
  36. monoco/features/agent/resources/atoms/atom-issue-lifecycle.yaml +73 -0
  37. monoco/features/agent/resources/atoms/atom-knowledge.yaml +55 -0
  38. monoco/features/agent/resources/atoms/atom-review.yaml +60 -0
  39. monoco/features/agent/resources/en/skills/flow_engineer/SKILL.md +94 -0
  40. monoco/features/agent/resources/en/skills/flow_manager/SKILL.md +93 -0
  41. monoco/features/agent/resources/en/skills/flow_planner/SKILL.md +85 -0
  42. monoco/features/agent/resources/en/skills/flow_reviewer/SKILL.md +114 -0
  43. monoco/features/agent/resources/roles/role-engineer.yaml +49 -0
  44. monoco/features/agent/resources/roles/role-manager.yaml +46 -0
  45. monoco/features/agent/resources/roles/role-planner.yaml +46 -0
  46. monoco/features/agent/resources/roles/role-reviewer.yaml +47 -0
  47. monoco/features/agent/resources/workflows/workflow-dev.yaml +83 -0
  48. monoco/features/agent/resources/workflows/workflow-issue-create.yaml +72 -0
  49. monoco/features/agent/resources/workflows/workflow-review.yaml +94 -0
  50. monoco/features/agent/resources/zh/skills/flow_engineer/SKILL.md +94 -0
  51. monoco/features/agent/resources/zh/skills/flow_manager/SKILL.md +88 -0
  52. monoco/features/agent/resources/zh/skills/flow_planner/SKILL.md +259 -0
  53. monoco/features/agent/resources/zh/skills/flow_reviewer/SKILL.md +137 -0
  54. monoco/features/{scheduler → agent}/session.py +36 -1
  55. monoco/features/{scheduler → agent}/worker.py +40 -4
  56. monoco/features/glossary/adapter.py +31 -0
  57. monoco/features/glossary/config.py +5 -0
  58. monoco/features/glossary/resources/en/AGENTS.md +29 -0
  59. monoco/features/glossary/resources/en/skills/monoco_glossary/SKILL.md +35 -0
  60. monoco/features/glossary/resources/zh/AGENTS.md +29 -0
  61. monoco/features/glossary/resources/zh/skills/monoco_glossary/SKILL.md +35 -0
  62. monoco/features/i18n/resources/en/skills/i18n_scan_workflow/SKILL.md +105 -0
  63. monoco/features/i18n/resources/en/{SKILL.md → skills/monoco_i18n/SKILL.md} +2 -0
  64. monoco/features/i18n/resources/zh/skills/i18n_scan_workflow/SKILL.md +105 -0
  65. monoco/features/i18n/resources/zh/{SKILL.md → skills/monoco_i18n/SKILL.md} +2 -0
  66. monoco/features/issue/commands.py +427 -21
  67. monoco/features/issue/core.py +140 -1
  68. monoco/features/issue/criticality.py +553 -0
  69. monoco/features/issue/domain/models.py +28 -2
  70. monoco/features/issue/engine/machine.py +75 -15
  71. monoco/features/issue/git_service.py +185 -0
  72. monoco/features/issue/linter.py +291 -62
  73. monoco/features/issue/models.py +50 -2
  74. monoco/features/issue/resources/en/skills/issue_create_workflow/SKILL.md +167 -0
  75. monoco/features/issue/resources/en/skills/issue_develop_workflow/SKILL.md +224 -0
  76. monoco/features/issue/resources/en/skills/issue_lifecycle_workflow/SKILL.md +159 -0
  77. monoco/features/issue/resources/en/skills/issue_refine_workflow/SKILL.md +203 -0
  78. monoco/features/issue/resources/en/{SKILL.md → skills/monoco_issue/SKILL.md} +50 -0
  79. monoco/features/issue/resources/zh/skills/issue_create_workflow/SKILL.md +167 -0
  80. monoco/features/issue/resources/zh/skills/issue_develop_workflow/SKILL.md +224 -0
  81. monoco/features/issue/resources/zh/skills/issue_lifecycle_workflow/SKILL.md +159 -0
  82. monoco/features/issue/resources/zh/skills/issue_refine_workflow/SKILL.md +203 -0
  83. monoco/features/issue/resources/zh/{SKILL.md → skills/monoco_issue/SKILL.md} +52 -0
  84. monoco/features/issue/validator.py +185 -65
  85. monoco/features/memo/__init__.py +2 -1
  86. monoco/features/memo/adapter.py +32 -0
  87. monoco/features/memo/cli.py +36 -14
  88. monoco/features/memo/core.py +59 -0
  89. monoco/features/memo/resources/en/skills/monoco_memo/SKILL.md +77 -0
  90. monoco/features/memo/resources/en/skills/note_processing_workflow/SKILL.md +140 -0
  91. monoco/features/memo/resources/zh/AGENTS.md +8 -0
  92. monoco/features/memo/resources/zh/skills/monoco_memo/SKILL.md +77 -0
  93. monoco/features/memo/resources/zh/skills/note_processing_workflow/SKILL.md +140 -0
  94. monoco/features/spike/resources/en/{SKILL.md → skills/monoco_spike/SKILL.md} +2 -0
  95. monoco/features/spike/resources/en/skills/research_workflow/SKILL.md +121 -0
  96. monoco/features/spike/resources/zh/{SKILL.md → skills/monoco_spike/SKILL.md} +2 -0
  97. monoco/features/spike/resources/zh/skills/research_workflow/SKILL.md +121 -0
  98. monoco/main.py +2 -3
  99. monoco_toolkit-0.3.10.dist-info/METADATA +124 -0
  100. monoco_toolkit-0.3.10.dist-info/RECORD +156 -0
  101. monoco/features/scheduler/cli.py +0 -285
  102. monoco/features/scheduler/config.py +0 -68
  103. monoco/features/scheduler/defaults.py +0 -54
  104. monoco/features/scheduler/manager.py +0 -49
  105. monoco/features/scheduler/reliability.py +0 -106
  106. monoco/features/skills/core.py +0 -102
  107. monoco_toolkit-0.3.6.dist-info/METADATA +0 -127
  108. monoco_toolkit-0.3.6.dist-info/RECORD +0 -97
  109. /monoco/core/{hooks.py → githooks.py} +0 -0
  110. /monoco/features/{skills → glossary}/__init__.py +0 -0
  111. {monoco_toolkit-0.3.6.dist-info → monoco_toolkit-0.3.10.dist-info}/WHEEL +0 -0
  112. {monoco_toolkit-0.3.6.dist-info → monoco_toolkit-0.3.10.dist-info}/entry_points.txt +0 -0
  113. {monoco_toolkit-0.3.6.dist-info → monoco_toolkit-0.3.10.dist-info}/licenses/LICENSE +0 -0
@@ -27,6 +27,7 @@ class IssueValidator:
27
27
  all_issue_ids: Set[str] = set(),
28
28
  current_project: Optional[str] = None,
29
29
  workspace_root: Optional[str] = None,
30
+ valid_domains: Set[str] = set(),
30
31
  ) -> List[Diagnostic]:
31
32
  """
32
33
  Validate an issue and return diagnostics.
@@ -77,7 +78,11 @@ class IssueValidator:
77
78
  diagnostics.extend(self._validate_references(meta, content, all_issue_ids))
78
79
 
79
80
  # 5.5 Domain Integrity
80
- diagnostics.extend(self._validate_domains(meta, content, all_issue_ids))
81
+ diagnostics.extend(
82
+ self._validate_domains(
83
+ meta, content, all_issue_ids, valid_domains=valid_domains
84
+ )
85
+ )
81
86
 
82
87
  # 6. Time Consistency
83
88
  diagnostics.extend(self._validate_time_consistency(meta, content))
@@ -88,6 +93,9 @@ class IssueValidator:
88
93
  # 8. Language Consistency
89
94
  diagnostics.extend(self._validate_language_consistency(meta, content))
90
95
 
96
+ # 9. Placeholder Detection
97
+ diagnostics.extend(self._validate_placeholders(meta, content))
98
+
91
99
  return diagnostics
92
100
 
93
101
  def _validate_language_consistency(
@@ -431,7 +439,7 @@ class IssueValidator:
431
439
  )
432
440
  resolver = ReferenceResolver(context)
433
441
 
434
- # Malformed ID Check
442
+ # 1. Malformed ID Check (Syntax)
435
443
  if meta.parent and meta.parent.startswith("#"):
436
444
  line = self._get_field_line(content, "parent")
437
445
  diagnostics.append(
@@ -466,7 +474,63 @@ class IssueValidator:
466
474
  )
467
475
  )
468
476
 
469
- if not all_ids or not resolver:
477
+ # 2. Body Reference Check (Format and Existence)
478
+ lines = content.split("\n")
479
+ in_fm = False
480
+ fm_end = 0
481
+ for i, line in enumerate(lines):
482
+ if line.strip() == "---":
483
+ if not in_fm:
484
+ in_fm = True
485
+ else:
486
+ fm_end = i
487
+ break
488
+
489
+ for i, line in enumerate(lines):
490
+ if i <= fm_end:
491
+ continue # Skip frontmatter
492
+
493
+ # Find all matches, including those with invalid suffixes to report them properly
494
+ matches = re.finditer(r"\b((?:EPIC|FEAT|CHORE|FIX)-\d{4}(?:-\d+)?)\b", line)
495
+ for match in matches:
496
+ full_raw_id = match.group(1)
497
+
498
+ # Check if it has an invalid suffix (e.g. FEAT-0099-1)
499
+ if len(full_raw_id.split("-")) > 2:
500
+ diagnostics.append(
501
+ self._create_diagnostic(
502
+ f"Invalid ID Format: '{full_raw_id}' has an invalid suffix. Use 'parent' field for hierarchy.",
503
+ DiagnosticSeverity.Warning,
504
+ line=i,
505
+ )
506
+ )
507
+ continue
508
+
509
+ ref_id = full_raw_id
510
+
511
+ # Knowledge Check (Only if resolver is available)
512
+ if resolver:
513
+ # Check for namespaced ID before this match?
514
+ full_match = re.search(
515
+ r"\b(?:([a-z0-9_-]+)::)?(" + re.escape(ref_id) + r")\b",
516
+ line[max(0, match.start() - 50) : match.end()],
517
+ )
518
+
519
+ check_id = ref_id
520
+ if full_match and full_match.group(1):
521
+ check_id = f"{full_match.group(1)}::{ref_id}"
522
+
523
+ if ref_id != meta.id and not resolver.is_valid_reference(check_id):
524
+ diagnostics.append(
525
+ self._create_diagnostic(
526
+ f"Broken Reference: Issue '{check_id}' not found.",
527
+ DiagnosticSeverity.Warning,
528
+ line=i,
529
+ )
530
+ )
531
+
532
+ # 3. Hierarchy and Graph Integrity (Requires Resolver)
533
+ if not resolver:
470
534
  return diagnostics
471
535
 
472
536
  # Logic: Epics must have a parent (unless it is the Sink Root EPIC-0000)
@@ -506,49 +570,6 @@ class IssueValidator:
506
570
  )
507
571
  )
508
572
 
509
- # Body Reference Check
510
- # Regex for generic issue ID: (EPIC|FEAT|CHORE|FIX)-\d{4}
511
- # We scan line by line to get line numbers
512
- lines = content.split("\n")
513
- # Skip frontmatter for body check to avoid double counting (handled above)
514
- in_fm = False
515
- fm_end = 0
516
- for i, line in enumerate(lines):
517
- if line.strip() == "---":
518
- if not in_fm:
519
- in_fm = True
520
- else:
521
- fm_end = i
522
- break
523
-
524
- for i, line in enumerate(lines):
525
- if i <= fm_end:
526
- continue # Skip frontmatter
527
-
528
- # Find all matches
529
- matches = re.finditer(r"\b((?:EPIC|FEAT|CHORE|FIX)-\d{4})\b", line)
530
- for match in matches:
531
- ref_id = match.group(1)
532
- # Check for namespaced ID before this match?
533
- # The regex above only catches the ID part.
534
- # Let's adjust regex to optionally catch namespace::
535
- full_match = re.search(
536
- r"\b(?:([a-z0-9_-]+)::)?(" + re.escape(ref_id) + r")\b",
537
- line[max(0, match.start() - 50) : match.end()],
538
- )
539
-
540
- check_id = ref_id
541
- if full_match and full_match.group(1):
542
- check_id = f"{full_match.group(1)}::{ref_id}"
543
-
544
- if ref_id != meta.id and not resolver.is_valid_reference(check_id):
545
- diagnostics.append(
546
- self._create_diagnostic(
547
- f"Broken Reference: Issue '{check_id}' not found.",
548
- DiagnosticSeverity.Warning,
549
- line=i,
550
- )
551
- )
552
573
  return diagnostics
553
574
  return diagnostics
554
575
 
@@ -603,7 +624,11 @@ class IssueValidator:
603
624
  return diagnostics
604
625
 
605
626
  def _validate_domains(
606
- self, meta: IssueMetadata, content: str, all_ids: Set[str] = set()
627
+ self,
628
+ meta: IssueMetadata,
629
+ content: str,
630
+ all_ids: Set[str] = set(),
631
+ valid_domains: Set[str] = set(),
607
632
  ) -> List[Diagnostic]:
608
633
  diagnostics = []
609
634
  # Check if 'domains' field exists in frontmatter text
@@ -650,38 +675,79 @@ class IssueValidator:
650
675
  )
651
676
 
652
677
  # Domain Content Validation
653
- from .domain_service import DomainService
654
-
655
- service = DomainService()
656
-
678
+ # If valid_domains is provided (from file scan), use it as strict source of truth
657
679
  if hasattr(meta, "domains") and meta.domains:
658
- for domain in meta.domains:
659
- if service.is_alias(domain):
660
- canonical = service.get_canonical(domain)
661
- diagnostics.append(
662
- self._create_diagnostic(
663
- f"Domain Alias: '{domain}' is an alias for '{canonical}'. Preference: Canonical.",
664
- DiagnosticSeverity.Warning,
665
- line=field_line,
680
+ if valid_domains:
681
+ # Use File-based validation
682
+ for domain in meta.domains:
683
+ # 1. Format Check: PascalCase
684
+ is_pascal = re.match(r"^[A-Z][a-zA-Z0-9]+$", domain) is not None
685
+
686
+ if not is_pascal:
687
+ # Suggest conversion
688
+ normalized = "".join(
689
+ word.capitalize()
690
+ for word in re.findall(r"[a-zA-Z0-9]+", domain)
666
691
  )
667
- )
668
- elif not service.is_defined(domain):
669
- if service.config.strict:
692
+ if normalized in valid_domains:
693
+ diagnostics.append(
694
+ self._create_diagnostic(
695
+ f"Domain Format Error: '{domain}' must be PascalCase (e.g., '{normalized}').",
696
+ DiagnosticSeverity.Error,
697
+ line=field_line,
698
+ )
699
+ )
700
+ else:
701
+ diagnostics.append(
702
+ self._create_diagnostic(
703
+ f"Domain Format Error: '{domain}' must be PascalCase (no spaces/symbols).",
704
+ DiagnosticSeverity.Error,
705
+ line=field_line,
706
+ )
707
+ )
708
+ continue
709
+
710
+ # 2. Existence Check
711
+ if domain not in valid_domains:
670
712
  diagnostics.append(
671
713
  self._create_diagnostic(
672
- f"Unknown Domain: '{domain}' is not defined in domain ontology.",
714
+ f"Unknown Domain: '{domain}' not found. Available: {', '.join(sorted(valid_domains))}",
673
715
  DiagnosticSeverity.Error,
674
716
  line=field_line,
675
717
  )
676
718
  )
677
- else:
719
+ else:
720
+ # Fallback to legacy DomainService (hardcoded list)
721
+ from .domain_service import DomainService
722
+
723
+ service = DomainService()
724
+ for domain in meta.domains:
725
+ if service.is_alias(domain):
726
+ canonical = service.get_canonical(domain)
678
727
  diagnostics.append(
679
728
  self._create_diagnostic(
680
- f"Unknown Domain: '{domain}' is not defined in domain ontology.",
729
+ f"Domain Alias: '{domain}' is an alias for '{canonical}'. Preference: Canonical.",
681
730
  DiagnosticSeverity.Warning,
682
731
  line=field_line,
683
732
  )
684
733
  )
734
+ elif not service.is_defined(domain):
735
+ if service.config.strict:
736
+ diagnostics.append(
737
+ self._create_diagnostic(
738
+ f"Unknown Domain: '{domain}' is not defined in domain ontology.",
739
+ DiagnosticSeverity.Error,
740
+ line=field_line,
741
+ )
742
+ )
743
+ else:
744
+ diagnostics.append(
745
+ self._create_diagnostic(
746
+ f"Unknown Domain: '{domain}' is not defined in domain ontology.",
747
+ DiagnosticSeverity.Warning,
748
+ line=field_line,
749
+ )
750
+ )
685
751
 
686
752
  return diagnostics
687
753
 
@@ -716,3 +782,57 @@ class IssueValidator:
716
782
  )
717
783
 
718
784
  return diagnostics
785
+
786
+ def _validate_placeholders(
787
+ self, meta: IssueMetadata, content: str
788
+ ) -> List[Diagnostic]:
789
+ """
790
+ Detect uncleared placeholders in issue content.
791
+
792
+ Placeholders are template hints that should be removed before submission.
793
+ Examples:
794
+ - <!-- Required for Review/Done stage. Record review feedback here. -->
795
+ - <!-- TODO: Add implementation details -->
796
+ - <!-- Placeholder: ... -->
797
+
798
+ Severity depends on stage:
799
+ - review/done: ERROR (must be cleared before submission)
800
+ - draft/open/doing: WARNING (should be cleared)
801
+ """
802
+ diagnostics = []
803
+
804
+ # Define placeholder patterns
805
+ placeholder_patterns = [
806
+ # HTML comments with common placeholder keywords
807
+ (r"<!--\s*Required for Review/Done stage.*?-->", "Review/Done placeholder"),
808
+ (r"<!--\s*TODO:.*?-->", "TODO placeholder"),
809
+ (r"<!--\s*FIXME:.*?-->", "FIXME placeholder"),
810
+ (r"<!--\s*Placeholder:.*?-->", "Generic placeholder"),
811
+ (r"<!--\s*Template:.*?-->", "Template placeholder"),
812
+ (r"<!--\s*Example:.*?-->", "Example placeholder"),
813
+ # Generic instruction patterns (English and Chinese)
814
+ (r"<!--\s*Record review feedback here.*?-->", "Review placeholder"),
815
+ (r"<!--\s*在此记录评审反馈.*?-->", "Review placeholder (Chinese)"),
816
+ ]
817
+
818
+ lines = content.splitlines()
819
+
820
+ # Determine severity based on stage
821
+ if meta.stage in ["review", "done"]:
822
+ severity = DiagnosticSeverity.Error
823
+ else:
824
+ severity = DiagnosticSeverity.Warning
825
+
826
+ for line_idx, line in enumerate(lines):
827
+ for pattern, desc in placeholder_patterns:
828
+ if re.search(pattern, line, re.IGNORECASE):
829
+ diagnostics.append(
830
+ self._create_diagnostic(
831
+ f"Uncleared Placeholder: {desc} found. Remove template hints before submission.",
832
+ severity,
833
+ line_idx,
834
+ )
835
+ )
836
+ break # Only report once per line
837
+
838
+ return diagnostics
@@ -1,3 +1,4 @@
1
1
  from .cli import app
2
+ from .adapter import MemoFeature
2
3
 
3
- __all__ = ["app"]
4
+ __all__ = ["app", "MemoFeature"]
@@ -0,0 +1,32 @@
1
+ from pathlib import Path
2
+ from typing import Dict
3
+ from monoco.core.feature import MonocoFeature, IntegrationData
4
+
5
+
6
+ class MemoFeature(MonocoFeature):
7
+ @property
8
+ def name(self) -> str:
9
+ return "memo"
10
+
11
+ def initialize(self, root: Path, config: Dict) -> None:
12
+ # Memo feature doesn't require explicit initialization
13
+ # The inbox is created on first use
14
+ pass
15
+
16
+ def integrate(self, root: Path, config: Dict) -> IntegrationData:
17
+ # Determine language from config, default to 'en'
18
+ lang = config.get("i18n", {}).get("source_lang", "en")
19
+
20
+ # Resource path: monoco/features/memo/resources/{lang}/AGENTS.md
21
+ base_dir = Path(__file__).parent / "resources"
22
+
23
+ # Try specific language, fallback to 'en'
24
+ prompt_file = base_dir / lang / "AGENTS.md"
25
+ if not prompt_file.exists():
26
+ prompt_file = base_dir / "en" / "AGENTS.md"
27
+
28
+ content = ""
29
+ if prompt_file.exists():
30
+ content = prompt_file.read_text(encoding="utf-8").strip()
31
+
32
+ return IntegrationData(system_prompts={"Memo (Fleeting Notes)": content})
@@ -4,25 +4,16 @@ from typing import Optional
4
4
  from rich.console import Console
5
5
  from rich.table import Table
6
6
  from monoco.core.config import get_config
7
- from .core import add_memo, list_memos, get_inbox_path
7
+ from .core import add_memo, list_memos, delete_memo, get_inbox_path, validate_content_language
8
8
 
9
9
  app = typer.Typer(help="Manage memos (fleeting notes).")
10
10
  console = Console()
11
11
 
12
12
 
13
- def get_issues_root() -> Path:
14
- config = get_config()
13
+ def get_issues_root(config=None) -> Path:
14
+ if config is None:
15
+ config = get_config()
15
16
  # Resolve absolute path for issues
16
- root = Path(config.paths.root).resolve()
17
- # If config.paths.root is '.', it means current or discovered root.
18
- # We should trust get_config's loading mechanism, but find_monoco_root might be safer to base off.
19
- # Update: config is loaded relative to where it was found.
20
- # Let's rely on config.paths.root if it's absolute, or relative to CWD?
21
- # Actually, the ConfigLoader doesn't mutate paths.root based on location.
22
- # It defaults to "."
23
-
24
- # Better approach:
25
- # Use find_monoco_root() to get base, then append config.paths.issues
26
17
  from monoco.core.config import find_monoco_root
27
18
 
28
19
  project_root = find_monoco_root()
@@ -35,11 +26,26 @@ def add_command(
35
26
  context: Optional[str] = typer.Option(
36
27
  None, "--context", "-c", help="Context reference (e.g. file:line)."
37
28
  ),
29
+ force: bool = typer.Option(
30
+ False, "--force", "-f", help="Bypass i18n language validation."
31
+ ),
38
32
  ):
39
33
  """
40
34
  Capture a new idea or thought into the Memo Inbox.
41
35
  """
42
- issues_root = get_issues_root()
36
+ config = get_config()
37
+ issues_root = get_issues_root(config)
38
+
39
+ # Language Validation
40
+ source_lang = config.i18n.source_lang
41
+ if not force and not validate_content_language(content, source_lang):
42
+ console.print(
43
+ f"[red]Error: Content language mismatch.[/red] Content does not match configured source language: [bold]{source_lang}[/bold]."
44
+ )
45
+ console.print(
46
+ "[yellow]Tip: Use --force to bypass this check if you really want to add this content.[/yellow]"
47
+ )
48
+ raise typer.Exit(code=1)
43
49
 
44
50
  uid = add_memo(issues_root, content, context)
45
51
 
@@ -88,3 +94,19 @@ def open_command():
88
94
  return
89
95
 
90
96
  typer.launch(str(inbox_path))
97
+
98
+
99
+ @app.command("delete")
100
+ def delete_command(
101
+ memo_id: str = typer.Argument(..., help="The ID of the memo to delete.")
102
+ ):
103
+ """
104
+ Delete a memo from the inbox by its ID.
105
+ """
106
+ issues_root = get_issues_root()
107
+
108
+ if delete_memo(issues_root, memo_id):
109
+ console.print(f"[green]✔ Memo [bold]{memo_id}[/bold] deleted successfully.[/green]")
110
+ else:
111
+ console.print(f"[red]Error: Memo with ID [bold]{memo_id}[/bold] not found.[/red]")
112
+ raise typer.Exit(code=1)
@@ -5,6 +5,23 @@ from typing import List, Dict, Optional
5
5
  import secrets
6
6
 
7
7
 
8
+ def is_chinese(text: str) -> bool:
9
+ """Check if the text contains at least one Chinese character."""
10
+ return any("\u4e00" <= char <= "\u9fff" for char in text)
11
+
12
+
13
+ def validate_content_language(content: str, source_lang: str) -> bool:
14
+ """
15
+ Check if content matches source language using simple heuristics.
16
+ Returns True if matched or if detection is not supported for the lang.
17
+ """
18
+ if source_lang == "zh":
19
+ return is_chinese(content)
20
+ # For 'en', we generally allow everything but could be more strict.
21
+ # Requirement is mainly about enforcing 'zh' when configured.
22
+ return True
23
+
24
+
8
25
  def get_memos_dir(issues_root: Path) -> Path:
9
26
  """
10
27
  Get the directory for memos.
@@ -85,3 +102,45 @@ def list_memos(issues_root: Path) -> List[Dict[str, str]]:
85
102
  memos.append({"id": uid, "timestamp": timestamp, "content": body})
86
103
 
87
104
  return memos
105
+
106
+
107
+ def delete_memo(issues_root: Path, memo_id: str) -> bool:
108
+ """
109
+ Delete a memo by its ID.
110
+ Returns True if deleted, False if not found.
111
+ """
112
+ inbox_path = get_inbox_path(issues_root)
113
+ if not inbox_path.exists():
114
+ return False
115
+
116
+ content = inbox_path.read_text(encoding="utf-8")
117
+ pattern = re.compile(r"^## \[([a-f0-9]+)\] (.*?)$", re.MULTILINE)
118
+
119
+ matches = list(pattern.finditer(content))
120
+ target_idx = -1
121
+ for i, m in enumerate(matches):
122
+ if m.group(1) == memo_id:
123
+ target_idx = i
124
+ break
125
+
126
+ if target_idx == -1:
127
+ return False
128
+
129
+ # Find boundaries
130
+ start = matches[target_idx].start()
131
+ # Include the potential newline before the header if it exists
132
+ if start > 0 and content[start - 1] == "\n":
133
+ start -= 1
134
+
135
+ if target_idx + 1 < len(matches):
136
+ end = matches[target_idx + 1].start()
137
+ # Back up if there's a newline before the next header that we should keep?
138
+ # Actually, if we delete a memo, we should probably remove one "entry block".
139
+ # Entry blocks are format_memo: \n## header\nbody\n
140
+ # So we want to remove the leading \n and the trailing parts.
141
+ else:
142
+ end = len(content)
143
+
144
+ new_content = content[:start] + content[end:]
145
+ inbox_path.write_text(new_content, encoding="utf-8")
146
+ return True
@@ -0,0 +1,77 @@
1
+ ---
2
+ name: monoco-memo
3
+ description: Lightweight memo system for quickly recording ideas, inspirations, and temporary notes. Distinguished from the formal Issue system.
4
+ type: standard
5
+ version: 1.0.0
6
+ ---
7
+
8
+ # Monoco Memo
9
+
10
+ Use this skill to quickly capture fleeting notes (fleeting ideas) without creating a formal Issue.
11
+
12
+ ## When to Use Memo vs Issue
13
+
14
+ | Scenario | Use | Reason |
15
+ |----------|-----|--------|
16
+ | Temporary ideas, inspirations | **Memo** | No tracking needed, no completion status required |
17
+ | Code snippets, link bookmarks | **Memo** | Quick record, organize later |
18
+ | Meeting notes | **Memo** | Record first, then extract tasks |
19
+ | Actionable work unit | **Issue** | Requires tracking, acceptance criteria, lifecycle |
20
+ | Bug fix | **Issue** | Needs to record reproduction steps, verification results |
21
+ | Feature development | **Issue** | Needs design, decomposition, delivery |
22
+
23
+ > **Core Principle**: Memos record **ideas**; Issues handle **actionable tasks**.
24
+
25
+ ## Commands
26
+
27
+ ### Add Memo
28
+
29
+ ```bash
30
+ monoco memo add "Your memo content"
31
+ ```
32
+
33
+ Optional parameters:
34
+ - `-c, --context`: Add context reference (e.g., `file:line`)
35
+
36
+ Examples:
37
+ ```bash
38
+ # Simple record
39
+ monoco memo add "Consider using Redis cache for user sessions"
40
+
41
+ # Record with context
42
+ monoco memo add "Recursion here may cause stack overflow" -c "src/utils.py:42"
43
+ ```
44
+
45
+ ### View Memo List
46
+
47
+ ```bash
48
+ monoco memo list
49
+ ```
50
+
51
+ Displays all unarchived memos.
52
+
53
+ ### Open Memo File
54
+
55
+ ```bash
56
+ monoco memo open
57
+ ```
58
+
59
+ Opens the memo file in the default editor for organizing or batch editing.
60
+
61
+ ## Workflow
62
+
63
+ ```
64
+ Idea flashes → monoco memo add "..." → Regular organization → Extract into Issue or archive
65
+ ```
66
+
67
+ 1. **Capture**: Use `monoco memo add` immediately when you have an idea
68
+ 2. **Organize**: Regularly (e.g., daily/weekly) run `monoco memo list` to review
69
+ 3. **Convert**: Transform valuable memos into formal Issues
70
+ 4. **Archive**: Remove from memos after processing
71
+
72
+ ## Best Practices
73
+
74
+ 1. **Keep concise**: Memos are quick notes, no detailed description needed
75
+ 2. **Convert timely**: Valuable ideas should be converted to Issue as soon as possible to avoid forgetting
76
+ 3. **Clean up regularly**: Memos are temporary, don't let them accumulate indefinitely
77
+ 4. **Use context**: When recording code-related ideas, use `-c` parameter to mark the location