monoco-toolkit 0.3.10__py3-none-any.whl → 0.3.12__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 (130) hide show
  1. monoco/__main__.py +8 -0
  2. monoco/core/artifacts/__init__.py +16 -0
  3. monoco/core/artifacts/manager.py +575 -0
  4. monoco/core/artifacts/models.py +161 -0
  5. monoco/core/automation/__init__.py +51 -0
  6. monoco/core/automation/config.py +338 -0
  7. monoco/core/automation/field_watcher.py +296 -0
  8. monoco/core/automation/handlers.py +723 -0
  9. monoco/core/config.py +31 -4
  10. monoco/core/executor/__init__.py +38 -0
  11. monoco/core/executor/agent_action.py +254 -0
  12. monoco/core/executor/git_action.py +303 -0
  13. monoco/core/executor/im_action.py +309 -0
  14. monoco/core/executor/pytest_action.py +218 -0
  15. monoco/core/git.py +38 -0
  16. monoco/core/hooks/context.py +74 -13
  17. monoco/core/ingestion/__init__.py +20 -0
  18. monoco/core/ingestion/discovery.py +248 -0
  19. monoco/core/ingestion/watcher.py +343 -0
  20. monoco/core/ingestion/worker.py +436 -0
  21. monoco/core/loader.py +633 -0
  22. monoco/core/registry.py +34 -25
  23. monoco/core/router/__init__.py +55 -0
  24. monoco/core/router/action.py +341 -0
  25. monoco/core/router/router.py +392 -0
  26. monoco/core/scheduler/__init__.py +63 -0
  27. monoco/core/scheduler/base.py +152 -0
  28. monoco/core/scheduler/engines.py +175 -0
  29. monoco/core/scheduler/events.py +171 -0
  30. monoco/core/scheduler/local.py +377 -0
  31. monoco/core/skills.py +119 -80
  32. monoco/core/watcher/__init__.py +57 -0
  33. monoco/core/watcher/base.py +365 -0
  34. monoco/core/watcher/dropzone.py +152 -0
  35. monoco/core/watcher/issue.py +303 -0
  36. monoco/core/watcher/memo.py +200 -0
  37. monoco/core/watcher/task.py +238 -0
  38. monoco/daemon/app.py +77 -1
  39. monoco/daemon/commands.py +10 -0
  40. monoco/daemon/events.py +34 -0
  41. monoco/daemon/mailroom_service.py +196 -0
  42. monoco/daemon/models.py +1 -0
  43. monoco/daemon/scheduler.py +207 -0
  44. monoco/daemon/services.py +27 -58
  45. monoco/daemon/triggers.py +55 -0
  46. monoco/features/agent/__init__.py +25 -7
  47. monoco/features/agent/adapter.py +17 -7
  48. monoco/features/agent/cli.py +91 -57
  49. monoco/features/agent/engines.py +31 -170
  50. monoco/{core/resources/en/skills/monoco_core → features/agent/resources/en/skills/monoco_atom_core}/SKILL.md +2 -2
  51. monoco/features/agent/resources/en/skills/{flow_engineer → monoco_workflow_agent_engineer}/SKILL.md +2 -2
  52. monoco/features/agent/resources/en/skills/{flow_manager → monoco_workflow_agent_manager}/SKILL.md +2 -2
  53. monoco/features/agent/resources/en/skills/{flow_planner → monoco_workflow_agent_planner}/SKILL.md +2 -2
  54. monoco/features/agent/resources/en/skills/{flow_reviewer → monoco_workflow_agent_reviewer}/SKILL.md +2 -2
  55. monoco/features/agent/resources/{roles/role-engineer.yaml → zh/roles/monoco_role_engineer.yaml} +3 -3
  56. monoco/features/agent/resources/{roles/role-manager.yaml → zh/roles/monoco_role_manager.yaml} +8 -8
  57. monoco/features/agent/resources/{roles/role-planner.yaml → zh/roles/monoco_role_planner.yaml} +8 -8
  58. monoco/features/agent/resources/{roles/role-reviewer.yaml → zh/roles/monoco_role_reviewer.yaml} +8 -8
  59. monoco/{core/resources/zh/skills/monoco_core → features/agent/resources/zh/skills/monoco_atom_core}/SKILL.md +2 -2
  60. monoco/features/agent/resources/zh/skills/{flow_engineer → monoco_workflow_agent_engineer}/SKILL.md +2 -2
  61. monoco/features/agent/resources/zh/skills/{flow_manager → monoco_workflow_agent_manager}/SKILL.md +2 -2
  62. monoco/features/agent/resources/zh/skills/{flow_planner → monoco_workflow_agent_planner}/SKILL.md +2 -2
  63. monoco/features/agent/resources/zh/skills/{flow_reviewer → monoco_workflow_agent_reviewer}/SKILL.md +2 -2
  64. monoco/features/agent/worker.py +1 -1
  65. monoco/features/artifact/__init__.py +0 -0
  66. monoco/features/artifact/adapter.py +33 -0
  67. monoco/features/artifact/resources/zh/AGENTS.md +14 -0
  68. monoco/features/artifact/resources/zh/skills/monoco_atom_artifact/SKILL.md +278 -0
  69. monoco/features/glossary/adapter.py +18 -7
  70. monoco/features/glossary/resources/en/skills/{monoco_glossary → monoco_atom_glossary}/SKILL.md +2 -2
  71. monoco/features/glossary/resources/zh/skills/{monoco_glossary → monoco_atom_glossary}/SKILL.md +2 -2
  72. monoco/features/hooks/__init__.py +11 -0
  73. monoco/features/hooks/adapter.py +67 -0
  74. monoco/features/hooks/commands.py +309 -0
  75. monoco/features/hooks/core.py +441 -0
  76. monoco/features/hooks/resources/ADDING_HOOKS.md +234 -0
  77. monoco/features/i18n/adapter.py +18 -5
  78. monoco/features/i18n/core.py +482 -17
  79. monoco/features/i18n/resources/en/skills/{monoco_i18n → monoco_atom_i18n}/SKILL.md +2 -2
  80. monoco/features/i18n/resources/en/skills/{i18n_scan_workflow → monoco_workflow_i18n_scan}/SKILL.md +2 -2
  81. monoco/features/i18n/resources/zh/skills/{monoco_i18n → monoco_atom_i18n}/SKILL.md +2 -2
  82. monoco/features/i18n/resources/zh/skills/{i18n_scan_workflow → monoco_workflow_i18n_scan}/SKILL.md +2 -2
  83. monoco/features/issue/adapter.py +19 -6
  84. monoco/features/issue/commands.py +352 -20
  85. monoco/features/issue/core.py +475 -16
  86. monoco/features/issue/engine/machine.py +114 -4
  87. monoco/features/issue/linter.py +60 -5
  88. monoco/features/issue/models.py +2 -2
  89. monoco/features/issue/resources/en/AGENTS.md +109 -0
  90. monoco/features/issue/resources/en/skills/{monoco_issue → monoco_atom_issue}/SKILL.md +2 -2
  91. monoco/features/issue/resources/en/skills/{issue_create_workflow → monoco_workflow_issue_creation}/SKILL.md +2 -2
  92. monoco/features/issue/resources/en/skills/{issue_develop_workflow → monoco_workflow_issue_development}/SKILL.md +2 -2
  93. monoco/features/issue/resources/en/skills/{issue_lifecycle_workflow → monoco_workflow_issue_management}/SKILL.md +2 -2
  94. monoco/features/issue/resources/en/skills/{issue_refine_workflow → monoco_workflow_issue_refinement}/SKILL.md +2 -2
  95. monoco/features/issue/resources/hooks/post-checkout.sh +39 -0
  96. monoco/features/issue/resources/hooks/pre-commit.sh +41 -0
  97. monoco/features/issue/resources/hooks/pre-push.sh +35 -0
  98. monoco/features/issue/resources/zh/AGENTS.md +109 -0
  99. monoco/features/issue/resources/zh/skills/{monoco_issue → monoco_atom_issue_lifecycle}/SKILL.md +2 -2
  100. monoco/features/issue/resources/zh/skills/{issue_create_workflow → monoco_workflow_issue_creation}/SKILL.md +2 -2
  101. monoco/features/issue/resources/zh/skills/{issue_develop_workflow → monoco_workflow_issue_development}/SKILL.md +2 -2
  102. monoco/features/issue/resources/zh/skills/{issue_lifecycle_workflow → monoco_workflow_issue_management}/SKILL.md +2 -2
  103. monoco/features/issue/resources/zh/skills/{issue_refine_workflow → monoco_workflow_issue_refinement}/SKILL.md +2 -2
  104. monoco/features/issue/validator.py +101 -1
  105. monoco/features/memo/adapter.py +21 -8
  106. monoco/features/memo/cli.py +103 -10
  107. monoco/features/memo/core.py +178 -92
  108. monoco/features/memo/models.py +53 -0
  109. monoco/features/memo/resources/en/skills/{monoco_memo → monoco_atom_memo}/SKILL.md +2 -2
  110. monoco/features/memo/resources/en/skills/{note_processing_workflow → monoco_workflow_note_processing}/SKILL.md +2 -2
  111. monoco/features/memo/resources/zh/skills/{monoco_memo → monoco_atom_memo}/SKILL.md +2 -2
  112. monoco/features/memo/resources/zh/skills/{note_processing_workflow → monoco_workflow_note_processing}/SKILL.md +2 -2
  113. monoco/features/spike/adapter.py +18 -5
  114. monoco/features/spike/commands.py +5 -3
  115. monoco/features/spike/resources/en/skills/{monoco_spike → monoco_atom_spike}/SKILL.md +2 -2
  116. monoco/features/spike/resources/en/skills/{research_workflow → monoco_workflow_research}/SKILL.md +2 -2
  117. monoco/features/spike/resources/zh/skills/{monoco_spike → monoco_atom_spike}/SKILL.md +2 -2
  118. monoco/features/spike/resources/zh/skills/{research_workflow → monoco_workflow_research}/SKILL.md +2 -2
  119. monoco/main.py +38 -1
  120. {monoco_toolkit-0.3.10.dist-info → monoco_toolkit-0.3.12.dist-info}/METADATA +7 -1
  121. monoco_toolkit-0.3.12.dist-info/RECORD +202 -0
  122. monoco/features/agent/apoptosis.py +0 -44
  123. monoco/features/agent/manager.py +0 -91
  124. monoco/features/agent/session.py +0 -121
  125. monoco_toolkit-0.3.10.dist-info/RECORD +0 -156
  126. /monoco/{core → features/agent}/resources/en/AGENTS.md +0 -0
  127. /monoco/{core → features/agent}/resources/zh/AGENTS.md +0 -0
  128. {monoco_toolkit-0.3.10.dist-info → monoco_toolkit-0.3.12.dist-info}/WHEEL +0 -0
  129. {monoco_toolkit-0.3.10.dist-info → monoco_toolkit-0.3.12.dist-info}/entry_points.txt +0 -0
  130. {monoco_toolkit-0.3.10.dist-info → monoco_toolkit-0.3.12.dist-info}/licenses/LICENSE +0 -0
@@ -1,7 +1,7 @@
1
1
  ---
2
- name: issue-create-workflow
2
+ name: monoco_workflow_issue_creation
3
3
  description: 从 Memo 提取改进线索,分类并创建 Issue Ticket (Copilot 模式)
4
- type: flow
4
+ type: workflow
5
5
  domain: issue
6
6
  version: 1.0.0
7
7
  ---
@@ -1,7 +1,7 @@
1
1
  ---
2
- name: issue-develop-workflow
2
+ name: monoco_workflow_issue_development
3
3
  description: 执行 Issue 开发、测试、提交和评审的完整工作流 (Copilot 模式)
4
- type: flow
4
+ type: workflow
5
5
  domain: issue
6
6
  version: 1.0.0
7
7
  ---
@@ -1,7 +1,7 @@
1
1
  ---
2
- name: issue-lifecycle-workflow
2
+ name: monoco_workflow_issue_management
3
3
  description: Issue 生命周期工作流 (Flow Skill)。定义从创建到关闭的完整 Issue 管理流程,确保任务追踪和流程合规。
4
- type: flow
4
+ type: workflow
5
5
  domain: issue
6
6
  version: 1.0.0
7
7
  ---
@@ -1,7 +1,7 @@
1
1
  ---
2
- name: issue-refine-workflow
2
+ name: monoco_workflow_issue_refinement
3
3
  description: 调查并细化 Issue,补充技术细节和实现方案 (Copilot 模式)
4
- type: flow
4
+ type: workflow
5
5
  domain: issue
6
6
  version: 1.0.0
7
7
  ---
@@ -28,9 +28,19 @@ class IssueValidator:
28
28
  current_project: Optional[str] = None,
29
29
  workspace_root: Optional[str] = None,
30
30
  valid_domains: Set[str] = set(),
31
+ all_issues: List[IssueMetadata] = None,
31
32
  ) -> List[Diagnostic]:
32
33
  """
33
34
  Validate an issue and return diagnostics.
35
+
36
+ Args:
37
+ meta: Issue metadata to validate
38
+ content: Full content of the issue file
39
+ all_issue_ids: Set of all issue IDs in the project (for reference validation)
40
+ current_project: Current project name
41
+ workspace_root: Workspace root project name
42
+ valid_domains: Set of valid domain names
43
+ all_issues: List of all IssueMetadata objects (for domain governance checks, FEAT-0136)
34
44
  """
35
45
  diagnostics = []
36
46
  self._current_project = current_project
@@ -65,6 +75,9 @@ class IssueValidator:
65
75
  # 1. State Matrix Validation
66
76
  diagnostics.extend(self._validate_state_matrix(meta, content))
67
77
 
78
+ # 1.5 Directory Consistency (FEAT-0144)
79
+ diagnostics.extend(self._validate_directory_consistency(meta, content))
80
+
68
81
  # 2. State Requirements (Strict Verification)
69
82
  diagnostics.extend(self._validate_state_requirements(meta, blocks))
70
83
 
@@ -80,7 +93,7 @@ class IssueValidator:
80
93
  # 5.5 Domain Integrity
81
94
  diagnostics.extend(
82
95
  self._validate_domains(
83
- meta, content, all_issue_ids, valid_domains=valid_domains
96
+ meta, content, all_issue_ids, valid_domains=valid_domains, all_issues=all_issues
84
97
  )
85
98
  )
86
99
 
@@ -151,6 +164,62 @@ class IssueValidator:
151
164
  return i
152
165
  return 0
153
166
 
167
+ def _validate_directory_consistency(
168
+ self, meta: IssueMetadata, content: str
169
+ ) -> List[Diagnostic]:
170
+ """
171
+ Check if the issue status matches its physical directory.
172
+ Checks for illegal directory names (like 'done' or 'freezed').
173
+ """
174
+ diagnostics = []
175
+ if not meta.path:
176
+ return diagnostics
177
+
178
+ path = Path(meta.path)
179
+ parent_dir_name = path.parent.name.lower()
180
+
181
+ # 1. Status/Directory Mismatch
182
+ if meta.status == "open" and parent_dir_name == "closed":
183
+ diagnostics.append(
184
+ self._create_diagnostic(
185
+ f"Status/Directory Mismatch: Issue '{meta.id}' has status 'open' but is in 'closed/' directory.",
186
+ DiagnosticSeverity.Error,
187
+ )
188
+ )
189
+ elif meta.status == "closed" and parent_dir_name == "open":
190
+ diagnostics.append(
191
+ self._create_diagnostic(
192
+ f"Status/Directory Mismatch: Issue '{meta.id}' has status 'closed' but is in 'open/' directory.",
193
+ DiagnosticSeverity.Error,
194
+ )
195
+ )
196
+
197
+ # 2. Illegal Directory Names
198
+ if parent_dir_name == "done":
199
+ diagnostics.append(
200
+ self._create_diagnostic(
201
+ "Illegal Directory: Issues should be in 'closed/' directory, not 'done/'.",
202
+ DiagnosticSeverity.Error,
203
+ )
204
+ )
205
+ elif parent_dir_name == "freezed":
206
+ diagnostics.append(
207
+ self._create_diagnostic(
208
+ "Illegal Directory: Issues should be in 'backlog/' directory, not 'freezed/'.",
209
+ DiagnosticSeverity.Error,
210
+ )
211
+ )
212
+
213
+ return diagnostics
214
+
215
+ def _validate_status_enum(self, meta: IssueMetadata, content: str) -> List[Diagnostic]:
216
+ """Legacy helper for enum validation (mostly handled by Pydantic now)."""
217
+ return []
218
+
219
+ def _validate_stage_enum(self, meta: IssueMetadata, content: str) -> List[Diagnostic]:
220
+ """Legacy helper for enum validation (mostly handled by Pydantic now)."""
221
+ return []
222
+
154
223
  def _validate_state_matrix(
155
224
  self, meta: IssueMetadata, content: str
156
225
  ) -> List[Diagnostic]:
@@ -629,6 +698,7 @@ class IssueValidator:
629
698
  content: str,
630
699
  all_ids: Set[str] = set(),
631
700
  valid_domains: Set[str] = set(),
701
+ all_issues: List[IssueMetadata] = None,
632
702
  ) -> List[Diagnostic]:
633
703
  diagnostics = []
634
704
  # Check if 'domains' field exists in frontmatter text
@@ -674,6 +744,36 @@ class IssueValidator:
674
744
  )
675
745
  )
676
746
 
747
+ # FEAT-0136: Scale-Aware Domain Governance
748
+ # Rule: If Total Issues > 128 or Total Epics > 32, enforce strict Domain coverage
749
+ is_large_scale = num_issues > 128 or num_epics > 32
750
+
751
+ # FEAT-0136: Domain Auto-Inheritance Logic
752
+ # If child issue has no domains but parent has, treat as inherited (logical inheritance)
753
+ if meta.type != "epic" and not meta.domains and meta.parent and all_issues and is_large_scale:
754
+ parent_issue = None
755
+ for issue in all_issues:
756
+ if issue.id == meta.parent:
757
+ parent_issue = issue
758
+ break
759
+
760
+ if parent_issue and parent_issue.domains:
761
+ # Parent has domains, child inherits them logically
762
+ # This is not an error - it's valid inheritance
763
+ pass
764
+ elif parent_issue and not parent_issue.domains:
765
+ # Parent also has no domains in large scale mode - this is an error
766
+ line = self._get_field_line(content, "domains")
767
+ diagnostics.append(
768
+ self._create_diagnostic(
769
+ f"Domain Governance: Issue '{meta.id}' has no domains assigned. "
770
+ f"Parent '{meta.parent}' also has no domains. In large-scale projects, "
771
+ f"at least 75% of Epics must have domains for children to inherit.",
772
+ DiagnosticSeverity.Error,
773
+ line=line if line > 0 else field_line,
774
+ )
775
+ )
776
+
677
777
  # Domain Content Validation
678
778
  # If valid_domains is provided (from file scan), use it as strict source of truth
679
779
  if hasattr(meta, "domains") and meta.domains:
@@ -1,19 +1,32 @@
1
1
  from pathlib import Path
2
2
  from typing import Dict
3
- from monoco.core.feature import MonocoFeature, IntegrationData
3
+ from monoco.core.loader import FeatureModule, FeatureMetadata
4
+ from monoco.core.feature import IntegrationData
4
5
 
5
6
 
6
- class MemoFeature(MonocoFeature):
7
- @property
8
- def name(self) -> str:
9
- return "memo"
7
+ class MemoFeature(FeatureModule):
8
+ """Memo (fleeting notes) feature module with unified lifecycle support."""
10
9
 
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
10
+ @property
11
+ def metadata(self) -> FeatureMetadata:
12
+ return FeatureMetadata(
13
+ name="memo",
14
+ version="1.0.0",
15
+ description="Fleeting notes and quick idea capture",
16
+ dependencies=["core"],
17
+ priority=40,
18
+ lazy=True, # Can be lazy loaded
19
+ )
20
+
21
+ def _on_mount(self, context: "FeatureContext") -> None: # type: ignore
22
+ """Memo feature doesn't require explicit initialization.
23
+
24
+ The inbox is created on first use.
25
+ """
14
26
  pass
15
27
 
16
28
  def integrate(self, root: Path, config: Dict) -> IntegrationData:
29
+ """Provide integration data for agent environment."""
17
30
  # Determine language from config, default to 'en'
18
31
  lang = config.get("i18n", {}).get("source_lang", "en")
19
32
 
@@ -4,7 +4,7 @@ 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, delete_memo, get_inbox_path, validate_content_language
7
+ from .core import add_memo, load_memos, delete_memo, update_memo, get_inbox_path, validate_content_language
8
8
 
9
9
  app = typer.Typer(help="Manage memos (fleeting notes).")
10
10
  console = Console()
@@ -26,6 +26,12 @@ def add_command(
26
26
  context: Optional[str] = typer.Option(
27
27
  None, "--context", "-c", help="Context reference (e.g. file:line)."
28
28
  ),
29
+ type: str = typer.Option(
30
+ "insight", "--type", "-t", help="Type of memo (insight, bug, feature, task)."
31
+ ),
32
+ source: str = typer.Option(
33
+ "cli", "--source", "-s", help="Source of the memo."
34
+ ),
29
35
  force: bool = typer.Option(
30
36
  False, "--force", "-f", help="Bypass i18n language validation."
31
37
  ),
@@ -47,36 +53,75 @@ def add_command(
47
53
  )
48
54
  raise typer.Exit(code=1)
49
55
 
50
- uid = add_memo(issues_root, content, context)
56
+ # TODO: Get actual user name if possible
57
+ author = "User"
58
+
59
+ uid = add_memo(
60
+ issues_root,
61
+ content,
62
+ context=context,
63
+ author=author,
64
+ source=source,
65
+ memo_type=type
66
+ )
51
67
 
52
68
  console.print(f"[green]✔ Memo recorded.[/green] ID: [bold]{uid}[/bold]")
53
69
 
54
70
 
55
71
  @app.command("list")
56
- def list_command():
72
+ def list_command(
73
+ status: Optional[str] = typer.Option(None, "--status", help="Filter by status (pending, tracked, resolved)."),
74
+ limit: int = typer.Option(None, "--limit", "-n", help="Limit number of memos shown.")
75
+ ):
57
76
  """
58
77
  List all memos in the inbox.
59
78
  """
60
79
  issues_root = get_issues_root()
61
80
 
62
- memos = list_memos(issues_root)
81
+ memos = load_memos(issues_root)
82
+
83
+ if status:
84
+ memos = [m for m in memos if m.status == status]
63
85
 
64
86
  if not memos:
65
- console.print("No memos found. Use `monoco memo add` to create one.")
87
+ console.print("No memos found.")
66
88
  return
89
+
90
+ # Reverse sort by timestamp (newest first) usually?
91
+ # But file is appended. Let's show newest at bottom (log style) or newest at top?
92
+ # Usually list shows content. Newest at bottom is standard for logs, but for "Inbox" maybe newest top?
93
+ # Let's keep file order (oldest first) unless user asks otherwise, or maybe reverse it for "Inbox" feel?
94
+ # Let's reverse it to see latest first.
95
+ memos.reverse()
96
+
97
+ if limit:
98
+ memos = memos[:limit]
67
99
 
68
100
  table = Table(title="Memo Inbox")
69
101
  table.add_column("ID", style="cyan", no_wrap=True)
70
- table.add_column("Timestamp", style="magenta")
102
+ table.add_column("Stat", style="yellow", width=4)
103
+ table.add_column("Type", style="magenta", width=8)
104
+ table.add_column("Ref", style="blue")
71
105
  table.add_column("Content")
72
106
 
73
107
  for memo in memos:
74
108
  # Truncate content for list view
75
- content_preview = memo["content"].split("\n")[0]
76
- if len(memo["content"]) > 50:
109
+ content_preview = memo.content.split("\n")[0]
110
+ if len(content_preview) > 50:
77
111
  content_preview = content_preview[:47] + "..."
78
-
79
- table.add_row(memo["id"], memo["timestamp"], content_preview)
112
+
113
+ status_icon = " "
114
+ if memo.status == "pending": status_icon = "P"
115
+ elif memo.status == "tracked": status_icon = "T"
116
+ elif memo.status == "resolved": status_icon = "✔"
117
+
118
+ table.add_row(
119
+ memo.uid,
120
+ status_icon,
121
+ memo.type,
122
+ memo.ref or "",
123
+ content_preview
124
+ )
80
125
 
81
126
  console.print(table)
82
127
 
@@ -110,3 +155,51 @@ def delete_command(
110
155
  else:
111
156
  console.print(f"[red]Error: Memo with ID [bold]{memo_id}[/bold] not found.[/red]")
112
157
  raise typer.Exit(code=1)
158
+
159
+
160
+ @app.command("link")
161
+ def link_command(
162
+ memo_id: str = typer.Argument(..., help="Memo ID"),
163
+ issue_id: str = typer.Argument(..., help="Issue ID to link to")
164
+ ):
165
+ """
166
+ Link a memo to an issue (Traceability).
167
+ Sets status to 'tracked'.
168
+ """
169
+ issues_root = get_issues_root()
170
+
171
+ updates = {
172
+ "status": "tracked",
173
+ "ref": issue_id
174
+ }
175
+
176
+ if update_memo(issues_root, memo_id, updates):
177
+ console.print(f"[green]✔ Memo {memo_id} linked to {issue_id}.[/green]")
178
+ else:
179
+ console.print(f"[red]Error: Memo {memo_id} not found.[/red]")
180
+ raise typer.Exit(code=1)
181
+
182
+
183
+ @app.command("resolve")
184
+ def resolve_command(
185
+ memo_id: str = typer.Argument(..., help="Memo ID")
186
+ ):
187
+ """
188
+ Mark a memo as resolved.
189
+ """
190
+ issues_root = get_issues_root()
191
+
192
+ updates = {
193
+ "status": "resolved"
194
+ }
195
+
196
+ if update_memo(issues_root, memo_id, updates):
197
+ console.print(f"[green]✔ Memo {memo_id} resolved.[/green]")
198
+ else:
199
+ console.print(f"[red]Error: Memo {memo_id} not found.[/red]")
200
+ raise typer.Exit(code=1)
201
+
202
+
203
+
204
+
205
+