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.
- monoco/cli/workspace.py +1 -1
- monoco/core/config.py +58 -0
- monoco/core/hooks/__init__.py +19 -0
- monoco/core/hooks/base.py +104 -0
- monoco/core/hooks/builtin/__init__.py +11 -0
- monoco/core/hooks/builtin/git_cleanup.py +266 -0
- monoco/core/hooks/builtin/logging_hook.py +78 -0
- monoco/core/hooks/context.py +131 -0
- monoco/core/hooks/registry.py +222 -0
- monoco/core/injection.py +63 -29
- monoco/core/integrations.py +8 -2
- monoco/core/output.py +5 -5
- monoco/core/registry.py +9 -1
- monoco/core/resource/__init__.py +5 -0
- monoco/core/resource/finder.py +98 -0
- monoco/core/resource/manager.py +91 -0
- monoco/core/resource/models.py +35 -0
- monoco/core/resources/en/{SKILL.md → skills/monoco_core/SKILL.md} +2 -0
- monoco/core/resources/zh/{SKILL.md → skills/monoco_core/SKILL.md} +2 -0
- monoco/core/setup.py +1 -1
- monoco/core/skill_framework.py +292 -0
- monoco/core/skills.py +538 -254
- monoco/core/sync.py +73 -1
- monoco/core/workflow_converter.py +420 -0
- monoco/features/{scheduler → agent}/__init__.py +5 -3
- monoco/features/agent/adapter.py +31 -0
- monoco/features/agent/apoptosis.py +44 -0
- monoco/features/agent/cli.py +296 -0
- monoco/features/agent/config.py +96 -0
- monoco/features/agent/defaults.py +12 -0
- monoco/features/{scheduler → agent}/engines.py +32 -6
- monoco/features/agent/flow_skills.py +281 -0
- monoco/features/agent/manager.py +91 -0
- monoco/features/{scheduler → agent}/models.py +6 -3
- monoco/features/agent/resources/atoms/atom-code-dev.yaml +61 -0
- monoco/features/agent/resources/atoms/atom-issue-lifecycle.yaml +73 -0
- monoco/features/agent/resources/atoms/atom-knowledge.yaml +55 -0
- monoco/features/agent/resources/atoms/atom-review.yaml +60 -0
- monoco/features/agent/resources/en/skills/flow_engineer/SKILL.md +94 -0
- monoco/features/agent/resources/en/skills/flow_manager/SKILL.md +93 -0
- monoco/features/agent/resources/en/skills/flow_planner/SKILL.md +85 -0
- monoco/features/agent/resources/en/skills/flow_reviewer/SKILL.md +114 -0
- monoco/features/agent/resources/roles/role-engineer.yaml +49 -0
- monoco/features/agent/resources/roles/role-manager.yaml +46 -0
- monoco/features/agent/resources/roles/role-planner.yaml +46 -0
- monoco/features/agent/resources/roles/role-reviewer.yaml +47 -0
- monoco/features/agent/resources/workflows/workflow-dev.yaml +83 -0
- monoco/features/agent/resources/workflows/workflow-issue-create.yaml +72 -0
- monoco/features/agent/resources/workflows/workflow-review.yaml +94 -0
- monoco/features/agent/resources/zh/skills/flow_engineer/SKILL.md +94 -0
- monoco/features/agent/resources/zh/skills/flow_manager/SKILL.md +88 -0
- monoco/features/agent/resources/zh/skills/flow_planner/SKILL.md +259 -0
- monoco/features/agent/resources/zh/skills/flow_reviewer/SKILL.md +137 -0
- monoco/features/{scheduler → agent}/session.py +36 -1
- monoco/features/{scheduler → agent}/worker.py +40 -4
- monoco/features/glossary/adapter.py +31 -0
- monoco/features/glossary/config.py +5 -0
- monoco/features/glossary/resources/en/AGENTS.md +29 -0
- monoco/features/glossary/resources/en/skills/monoco_glossary/SKILL.md +35 -0
- monoco/features/glossary/resources/zh/AGENTS.md +29 -0
- monoco/features/glossary/resources/zh/skills/monoco_glossary/SKILL.md +35 -0
- monoco/features/i18n/resources/en/skills/i18n_scan_workflow/SKILL.md +105 -0
- monoco/features/i18n/resources/en/{SKILL.md → skills/monoco_i18n/SKILL.md} +2 -0
- monoco/features/i18n/resources/zh/skills/i18n_scan_workflow/SKILL.md +105 -0
- monoco/features/i18n/resources/zh/{SKILL.md → skills/monoco_i18n/SKILL.md} +2 -0
- monoco/features/issue/commands.py +427 -21
- monoco/features/issue/core.py +140 -1
- monoco/features/issue/criticality.py +553 -0
- monoco/features/issue/domain/models.py +28 -2
- monoco/features/issue/engine/machine.py +75 -15
- monoco/features/issue/git_service.py +185 -0
- monoco/features/issue/linter.py +291 -62
- monoco/features/issue/models.py +50 -2
- monoco/features/issue/resources/en/skills/issue_create_workflow/SKILL.md +167 -0
- monoco/features/issue/resources/en/skills/issue_develop_workflow/SKILL.md +224 -0
- monoco/features/issue/resources/en/skills/issue_lifecycle_workflow/SKILL.md +159 -0
- monoco/features/issue/resources/en/skills/issue_refine_workflow/SKILL.md +203 -0
- monoco/features/issue/resources/en/{SKILL.md → skills/monoco_issue/SKILL.md} +50 -0
- monoco/features/issue/resources/zh/skills/issue_create_workflow/SKILL.md +167 -0
- monoco/features/issue/resources/zh/skills/issue_develop_workflow/SKILL.md +224 -0
- monoco/features/issue/resources/zh/skills/issue_lifecycle_workflow/SKILL.md +159 -0
- monoco/features/issue/resources/zh/skills/issue_refine_workflow/SKILL.md +203 -0
- monoco/features/issue/resources/zh/{SKILL.md → skills/monoco_issue/SKILL.md} +52 -0
- monoco/features/issue/validator.py +185 -65
- monoco/features/memo/__init__.py +2 -1
- monoco/features/memo/adapter.py +32 -0
- monoco/features/memo/cli.py +36 -14
- monoco/features/memo/core.py +59 -0
- monoco/features/memo/resources/en/skills/monoco_memo/SKILL.md +77 -0
- monoco/features/memo/resources/en/skills/note_processing_workflow/SKILL.md +140 -0
- monoco/features/memo/resources/zh/AGENTS.md +8 -0
- monoco/features/memo/resources/zh/skills/monoco_memo/SKILL.md +77 -0
- monoco/features/memo/resources/zh/skills/note_processing_workflow/SKILL.md +140 -0
- monoco/features/spike/resources/en/{SKILL.md → skills/monoco_spike/SKILL.md} +2 -0
- monoco/features/spike/resources/en/skills/research_workflow/SKILL.md +121 -0
- monoco/features/spike/resources/zh/{SKILL.md → skills/monoco_spike/SKILL.md} +2 -0
- monoco/features/spike/resources/zh/skills/research_workflow/SKILL.md +121 -0
- monoco/main.py +2 -3
- monoco_toolkit-0.3.10.dist-info/METADATA +124 -0
- monoco_toolkit-0.3.10.dist-info/RECORD +156 -0
- monoco/features/scheduler/cli.py +0 -285
- monoco/features/scheduler/config.py +0 -68
- monoco/features/scheduler/defaults.py +0 -54
- monoco/features/scheduler/manager.py +0 -49
- monoco/features/scheduler/reliability.py +0 -106
- monoco/features/skills/core.py +0 -102
- monoco_toolkit-0.3.6.dist-info/METADATA +0 -127
- monoco_toolkit-0.3.6.dist-info/RECORD +0 -97
- /monoco/core/{hooks.py → githooks.py} +0 -0
- /monoco/features/{skills → glossary}/__init__.py +0 -0
- {monoco_toolkit-0.3.6.dist-info → monoco_toolkit-0.3.10.dist-info}/WHEEL +0 -0
- {monoco_toolkit-0.3.6.dist-info → monoco_toolkit-0.3.10.dist-info}/entry_points.txt +0 -0
- {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(
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
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
|
-
|
|
669
|
-
|
|
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}'
|
|
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
|
-
|
|
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"
|
|
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
|
monoco/features/memo/__init__.py
CHANGED
|
@@ -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})
|
monoco/features/memo/cli.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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)
|
monoco/features/memo/core.py
CHANGED
|
@@ -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
|