monoco-toolkit 0.3.10__py3-none-any.whl → 0.3.11__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 (100) 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/config.py +31 -4
  6. monoco/core/git.py +23 -0
  7. monoco/core/ingestion/__init__.py +20 -0
  8. monoco/core/ingestion/discovery.py +248 -0
  9. monoco/core/ingestion/watcher.py +343 -0
  10. monoco/core/ingestion/worker.py +436 -0
  11. monoco/core/loader.py +633 -0
  12. monoco/core/registry.py +34 -25
  13. monoco/core/skills.py +119 -80
  14. monoco/daemon/app.py +77 -1
  15. monoco/daemon/commands.py +10 -0
  16. monoco/daemon/mailroom_service.py +196 -0
  17. monoco/daemon/models.py +1 -0
  18. monoco/daemon/scheduler.py +236 -0
  19. monoco/daemon/services.py +185 -0
  20. monoco/daemon/triggers.py +55 -0
  21. monoco/features/agent/adapter.py +17 -7
  22. monoco/features/agent/apoptosis.py +4 -4
  23. monoco/features/agent/manager.py +41 -5
  24. monoco/{core/resources/en/skills/monoco_core → features/agent/resources/en/skills/monoco_atom_core}/SKILL.md +2 -2
  25. monoco/features/agent/resources/en/skills/{flow_engineer → monoco_workflow_agent_engineer}/SKILL.md +2 -2
  26. monoco/features/agent/resources/en/skills/{flow_manager → monoco_workflow_agent_manager}/SKILL.md +2 -2
  27. monoco/features/agent/resources/en/skills/{flow_planner → monoco_workflow_agent_planner}/SKILL.md +2 -2
  28. monoco/features/agent/resources/en/skills/{flow_reviewer → monoco_workflow_agent_reviewer}/SKILL.md +2 -2
  29. monoco/features/agent/resources/{roles/role-engineer.yaml → zh/roles/monoco_role_engineer.yaml} +3 -3
  30. monoco/features/agent/resources/{roles/role-manager.yaml → zh/roles/monoco_role_manager.yaml} +8 -8
  31. monoco/features/agent/resources/{roles/role-planner.yaml → zh/roles/monoco_role_planner.yaml} +8 -8
  32. monoco/features/agent/resources/{roles/role-reviewer.yaml → zh/roles/monoco_role_reviewer.yaml} +8 -8
  33. monoco/{core/resources/zh/skills/monoco_core → features/agent/resources/zh/skills/monoco_atom_core}/SKILL.md +2 -2
  34. monoco/features/agent/resources/zh/skills/{flow_engineer → monoco_workflow_agent_engineer}/SKILL.md +2 -2
  35. monoco/features/agent/resources/zh/skills/{flow_manager → monoco_workflow_agent_manager}/SKILL.md +2 -2
  36. monoco/features/agent/resources/zh/skills/{flow_planner → monoco_workflow_agent_planner}/SKILL.md +2 -2
  37. monoco/features/agent/resources/zh/skills/{flow_reviewer → monoco_workflow_agent_reviewer}/SKILL.md +2 -2
  38. monoco/features/agent/session.py +59 -11
  39. monoco/features/artifact/__init__.py +0 -0
  40. monoco/features/artifact/adapter.py +33 -0
  41. monoco/features/artifact/resources/zh/AGENTS.md +14 -0
  42. monoco/features/artifact/resources/zh/skills/monoco_atom_artifact/SKILL.md +278 -0
  43. monoco/features/glossary/adapter.py +18 -7
  44. monoco/features/glossary/resources/en/skills/{monoco_glossary → monoco_atom_glossary}/SKILL.md +2 -2
  45. monoco/features/glossary/resources/zh/skills/{monoco_glossary → monoco_atom_glossary}/SKILL.md +2 -2
  46. monoco/features/hooks/__init__.py +11 -0
  47. monoco/features/hooks/adapter.py +67 -0
  48. monoco/features/hooks/commands.py +309 -0
  49. monoco/features/hooks/core.py +441 -0
  50. monoco/features/hooks/resources/ADDING_HOOKS.md +234 -0
  51. monoco/features/i18n/adapter.py +18 -5
  52. monoco/features/i18n/core.py +482 -17
  53. monoco/features/i18n/resources/en/skills/{monoco_i18n → monoco_atom_i18n}/SKILL.md +2 -2
  54. monoco/features/i18n/resources/en/skills/{i18n_scan_workflow → monoco_workflow_i18n_scan}/SKILL.md +2 -2
  55. monoco/features/i18n/resources/zh/skills/{monoco_i18n → monoco_atom_i18n}/SKILL.md +2 -2
  56. monoco/features/i18n/resources/zh/skills/{i18n_scan_workflow → monoco_workflow_i18n_scan}/SKILL.md +2 -2
  57. monoco/features/issue/adapter.py +19 -6
  58. monoco/features/issue/commands.py +281 -7
  59. monoco/features/issue/core.py +227 -13
  60. monoco/features/issue/engine/machine.py +114 -4
  61. monoco/features/issue/linter.py +60 -5
  62. monoco/features/issue/models.py +2 -2
  63. monoco/features/issue/resources/en/AGENTS.md +109 -0
  64. monoco/features/issue/resources/en/skills/{monoco_issue → monoco_atom_issue}/SKILL.md +2 -2
  65. monoco/features/issue/resources/en/skills/{issue_create_workflow → monoco_workflow_issue_creation}/SKILL.md +2 -2
  66. monoco/features/issue/resources/en/skills/{issue_develop_workflow → monoco_workflow_issue_development}/SKILL.md +2 -2
  67. monoco/features/issue/resources/en/skills/{issue_lifecycle_workflow → monoco_workflow_issue_management}/SKILL.md +2 -2
  68. monoco/features/issue/resources/en/skills/{issue_refine_workflow → monoco_workflow_issue_refinement}/SKILL.md +2 -2
  69. monoco/features/issue/resources/hooks/post-checkout.sh +39 -0
  70. monoco/features/issue/resources/hooks/pre-commit.sh +41 -0
  71. monoco/features/issue/resources/hooks/pre-push.sh +35 -0
  72. monoco/features/issue/resources/zh/AGENTS.md +109 -0
  73. monoco/features/issue/resources/zh/skills/{monoco_issue → monoco_atom_issue_lifecycle}/SKILL.md +2 -2
  74. monoco/features/issue/resources/zh/skills/{issue_create_workflow → monoco_workflow_issue_creation}/SKILL.md +2 -2
  75. monoco/features/issue/resources/zh/skills/{issue_develop_workflow → monoco_workflow_issue_development}/SKILL.md +2 -2
  76. monoco/features/issue/resources/zh/skills/{issue_lifecycle_workflow → monoco_workflow_issue_management}/SKILL.md +2 -2
  77. monoco/features/issue/resources/zh/skills/{issue_refine_workflow → monoco_workflow_issue_refinement}/SKILL.md +2 -2
  78. monoco/features/issue/validator.py +101 -1
  79. monoco/features/memo/adapter.py +21 -8
  80. monoco/features/memo/cli.py +103 -10
  81. monoco/features/memo/core.py +178 -92
  82. monoco/features/memo/models.py +53 -0
  83. monoco/features/memo/resources/en/skills/{monoco_memo → monoco_atom_memo}/SKILL.md +2 -2
  84. monoco/features/memo/resources/en/skills/{note_processing_workflow → monoco_workflow_note_processing}/SKILL.md +2 -2
  85. monoco/features/memo/resources/zh/skills/{monoco_memo → monoco_atom_memo}/SKILL.md +2 -2
  86. monoco/features/memo/resources/zh/skills/{note_processing_workflow → monoco_workflow_note_processing}/SKILL.md +2 -2
  87. monoco/features/spike/adapter.py +18 -5
  88. monoco/features/spike/resources/en/skills/{monoco_spike → monoco_atom_spike}/SKILL.md +2 -2
  89. monoco/features/spike/resources/en/skills/{research_workflow → monoco_workflow_research}/SKILL.md +2 -2
  90. monoco/features/spike/resources/zh/skills/{monoco_spike → monoco_atom_spike}/SKILL.md +2 -2
  91. monoco/features/spike/resources/zh/skills/{research_workflow → monoco_workflow_research}/SKILL.md +2 -2
  92. monoco/main.py +38 -1
  93. {monoco_toolkit-0.3.10.dist-info → monoco_toolkit-0.3.11.dist-info}/METADATA +7 -1
  94. monoco_toolkit-0.3.11.dist-info/RECORD +181 -0
  95. monoco_toolkit-0.3.10.dist-info/RECORD +0 -156
  96. /monoco/{core → features/agent}/resources/en/AGENTS.md +0 -0
  97. /monoco/{core → features/agent}/resources/zh/AGENTS.md +0 -0
  98. {monoco_toolkit-0.3.10.dist-info → monoco_toolkit-0.3.11.dist-info}/WHEEL +0 -0
  99. {monoco_toolkit-0.3.10.dist-info → monoco_toolkit-0.3.11.dist-info}/entry_points.txt +0 -0
  100. {monoco_toolkit-0.3.10.dist-info → monoco_toolkit-0.3.11.dist-info}/licenses/LICENSE +0 -0
@@ -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
+
@@ -1,9 +1,10 @@
1
1
  import re
2
2
  from pathlib import Path
3
- from datetime import datetime
4
- from typing import List, Dict, Optional
3
+ from typing import List, Optional, Any
5
4
  import secrets
5
+ from datetime import datetime
6
6
 
7
+ from .models import Memo
7
8
 
8
9
  def is_chinese(text: str) -> bool:
9
10
  """Check if the text contains at least one Chinese character."""
@@ -18,7 +19,6 @@ def validate_content_language(content: str, source_lang: str) -> bool:
18
19
  if source_lang == "zh":
19
20
  return is_chinese(content)
20
21
  # For 'en', we generally allow everything but could be more strict.
21
- # Requirement is mainly about enforcing 'zh' when configured.
22
22
  return True
23
23
 
24
24
 
@@ -27,120 +27,206 @@ def get_memos_dir(issues_root: Path) -> Path:
27
27
  Get the directory for memos.
28
28
  Convention: Sibling of Issues directory.
29
29
  """
30
- # issues_root is usually ".../Issues"
31
30
  return issues_root.parent / "Memos"
32
31
 
33
-
34
32
  def get_inbox_path(issues_root: Path) -> Path:
35
33
  return get_memos_dir(issues_root) / "inbox.md"
36
34
 
37
-
38
35
  def generate_memo_id() -> str:
39
36
  """Generate a short 6-char ID."""
40
37
  return secrets.token_hex(3)
41
38
 
42
-
43
- def format_memo(uid: str, content: str, context: Optional[str] = None) -> str:
44
- timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
45
- header = f"## [{uid}] {timestamp}"
46
-
47
- body = content.strip()
48
-
49
- if context:
50
- body = f"> **Context**: `{context}`\n\n{body}"
51
-
52
- return f"\n{header}\n{body}\n"
53
-
54
-
55
- def add_memo(issues_root: Path, content: str, context: Optional[str] = None) -> str:
39
+ def parse_memo_block(block: str) -> Optional[Memo]:
56
40
  """
57
- Append a memo to the inbox.
58
- Returns the generated UID.
41
+ Parse a text block into a Memo object.
42
+ Block format:
43
+ ## [uid] YYYY-MM-DD HH:MM:SS
44
+ - **Key**: Value
45
+ ...
46
+ Content
59
47
  """
60
- inbox_path = get_inbox_path(issues_root)
61
-
62
- if not inbox_path.exists():
63
- inbox_path.parent.mkdir(parents=True, exist_ok=True)
64
- inbox_path.write_text("# Monoco Memos Inbox\n", encoding="utf-8")
65
-
66
- uid = generate_memo_id()
67
- entry = format_memo(uid, content, context)
68
-
69
- with inbox_path.open("a", encoding="utf-8") as f:
70
- f.write(entry)
71
-
72
- return uid
73
-
74
-
75
- def list_memos(issues_root: Path) -> List[Dict[str, str]]:
48
+ lines = block.strip().split("\n")
49
+ if not lines:
50
+ return None
51
+
52
+ header = lines[0]
53
+ match = re.match(r"^## \[([a-f0-9]+)\] (.*?)$", header)
54
+ if not match:
55
+ return None
56
+
57
+ uid = match.group(1)
58
+ ts_str = match.group(2)
59
+ try:
60
+ timestamp = datetime.strptime(ts_str, "%Y-%m-%d %H:%M:%S")
61
+ except ValueError:
62
+ timestamp = datetime.now() # Fallback
63
+
64
+ content_lines = []
65
+ metadata = {}
66
+
67
+ # Simple state machine
68
+ # 0: Header (done)
69
+ # 1: Metadata
70
+ # 2: Content
71
+
72
+ state = 1
73
+
74
+ for line in lines[1:]:
75
+ stripped = line.strip()
76
+ if state == 1:
77
+ if not stripped:
78
+ continue
79
+ # Check for metadata line: - **Key**: Value
80
+ meta_match = re.match(r"^\- \*\*([a-zA-Z]+)\*\*: (.*)$", stripped)
81
+ if meta_match:
82
+ key = meta_match.group(1).lower()
83
+ val = meta_match.group(2).strip()
84
+ metadata[key] = val
85
+ else:
86
+ # First non-metadata line marks start of content
87
+ state = 2
88
+ content_lines.append(line)
89
+ elif state == 2:
90
+ content_lines.append(line)
91
+
92
+ content = "\n".join(content_lines).strip()
93
+
94
+ # Map metadata to model fields
95
+ # Status map reverse
96
+ status_raw = metadata.get("status", "[ ] Pending")
97
+ status = "pending"
98
+ if "[x] Tracked" in status_raw:
99
+ status = "tracked"
100
+ elif "[x] Resolved" in status_raw:
101
+ status = "resolved"
102
+ elif "[-] Dismissed" in status_raw:
103
+ status = "dismissed"
104
+
105
+ return Memo(
106
+ uid=uid,
107
+ timestamp=timestamp,
108
+ content=content,
109
+ author=metadata.get("from", "User"),
110
+ source=metadata.get("source", "cli"),
111
+ type=metadata.get("type", "insight"),
112
+ status=status,
113
+ ref=metadata.get("ref"),
114
+ context=metadata.get("context") # Note: context might need cleanup if it was wrapped in code blocks
115
+ )
116
+
117
+ def load_memos(issues_root: Path) -> List[Memo]:
76
118
  """
77
- Parse memos from inbox.
119
+ Parse all memos from inbox.
78
120
  """
79
121
  inbox_path = get_inbox_path(issues_root)
80
122
  if not inbox_path.exists():
81
123
  return []
82
124
 
83
125
  content = inbox_path.read_text(encoding="utf-8")
84
-
85
- # Regex to find headers: ## [uid] timestamp
86
- # We split by headers
87
-
88
- pattern = re.compile(r"^## \[([a-f0-9]+)\] (.*?)$", re.MULTILINE)
89
-
126
+
127
+ # Split by headers: ## [uid]
128
+ # We use a lookahead or just standard split carefully
129
+ parts = re.split(r"(^## \[)", content, flags=re.MULTILINE)[1:] # Skip preamble
130
+
131
+ # parts will be like: ['## [', 'abc] 2023...\n...', '## [', 'def] ...']
132
+ # Reassemble pairs
133
+ blocks = []
134
+ for i in range(0, len(parts), 2):
135
+ if i+1 < len(parts):
136
+ blocks.append(parts[i] + parts[i+1])
137
+
90
138
  memos = []
91
- matches = list(pattern.finditer(content))
92
-
93
- for i, match in enumerate(matches):
94
- uid = match.group(1)
95
- timestamp = match.group(2)
96
-
97
- start = match.end()
98
- end = matches[i + 1].start() if i + 1 < len(matches) else len(content)
99
-
100
- body = content[start:end].strip()
101
-
102
- memos.append({"id": uid, "timestamp": timestamp, "content": body})
103
-
139
+ for block in blocks:
140
+ memo = parse_memo_block(block)
141
+ if memo:
142
+ memos.append(memo)
143
+
144
+ # Sort by timestamp desc? Or keep file order? File order is usually append (time asc).
104
145
  return memos
105
146
 
106
-
107
- def delete_memo(issues_root: Path, memo_id: str) -> bool:
147
+ def save_memos(issues_root: Path, memos: List[Memo]) -> None:
108
148
  """
109
- Delete a memo by its ID.
110
- Returns True if deleted, False if not found.
149
+ Rewrite the inbox file with the given list of memos.
150
+ """
151
+ inbox_path = get_inbox_path(issues_root)
152
+
153
+ # Header
154
+ lines = ["# Monoco Memos Inbox", ""]
155
+
156
+ for memo in memos:
157
+ lines.append(memo.to_markdown().strip())
158
+ lines.append("") # Spacer
159
+
160
+ inbox_path.write_text("\n".join(lines), encoding="utf-8")
161
+
162
+
163
+ def add_memo(
164
+ issues_root: Path,
165
+ content: str,
166
+ context: Optional[str] = None,
167
+ author: str = "User",
168
+ source: str = "cli",
169
+ memo_type: str = "insight"
170
+ ) -> str:
171
+ """
172
+ Append a memo to the inbox.
173
+ Returns the generated UID.
111
174
  """
175
+ uid = generate_memo_id()
176
+ memo = Memo(
177
+ uid=uid,
178
+ content=content,
179
+ context=context,
180
+ author=author,
181
+ source=source,
182
+ type=memo_type
183
+ )
184
+
185
+ # Append mode is more robust against concurrent reads than rewrite,
186
+ # but for consistent formatting we might want to just append string.
112
187
  inbox_path = get_inbox_path(issues_root)
188
+
113
189
  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)
190
+ inbox_path.parent.mkdir(parents=True, exist_ok=True)
191
+ inbox_path.write_text("# Monoco Memos Inbox\n\n", encoding="utf-8")
192
+
193
+ with inbox_path.open("a", encoding="utf-8") as f:
194
+ f.write("\n" + memo.to_markdown().strip() + "\n")
195
+
196
+ return uid
118
197
 
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
198
+ def update_memo(issues_root: Path, memo_id: str, updates: dict) -> bool:
199
+ """
200
+ Update a memo's fields.
201
+ """
202
+ memos = load_memos(issues_root)
203
+ found = False
204
+ for i, m in enumerate(memos):
205
+ if m.uid == memo_id:
206
+ # Apply updates
207
+ updated_data = m.model_dump()
208
+ updated_data.update(updates)
209
+ memos[i] = Memo(**updated_data) # Re-validate
210
+ found = True
124
211
  break
212
+
213
+ if found:
214
+ save_memos(issues_root, memos)
215
+
216
+ return found
125
217
 
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
218
+ def delete_memo(issues_root: Path, memo_id: str) -> bool:
219
+ """
220
+ Delete a memo by its ID.
221
+ """
222
+ memos = load_memos(issues_root)
223
+ initial_count = len(memos)
224
+ memos = [m for m in memos if m.uid != memo_id]
225
+
226
+ if len(memos) < initial_count:
227
+ save_memos(issues_root, memos)
228
+ return True
229
+ return False
230
+
231
+ # Compatibility shim
232
+ list_memos = load_memos
@@ -0,0 +1,53 @@
1
+ from datetime import datetime
2
+ from typing import Optional, Literal
3
+ from pydantic import BaseModel, Field
4
+
5
+ class Memo(BaseModel):
6
+ uid: str
7
+ content: str
8
+ timestamp: datetime = Field(default_factory=datetime.now)
9
+
10
+ # Optional Context
11
+ context: Optional[str] = None
12
+
13
+ # New Metadata Fields
14
+ author: str = "User" # User, Assistant, or specific Agent Name
15
+ source: str = "cli" # cli, agent, mailroom, etc.
16
+ status: Literal["pending", "tracked", "resolved", "dismissed"] = "pending"
17
+ ref: Optional[str] = None # Linked Issue ID or other reference
18
+ type: Literal["insight", "bug", "feature", "task"] = "insight"
19
+
20
+ def to_markdown(self) -> str:
21
+ """
22
+ Render the memo to Markdown format.
23
+ """
24
+ ts_str = self.timestamp.strftime("%Y-%m-%d %H:%M:%S")
25
+ header = f"## [{self.uid}] {ts_str}"
26
+
27
+ # Metadata block
28
+ meta = []
29
+ if self.author != "User":
30
+ meta.append(f"- **From**: {self.author}")
31
+ if self.source != "cli":
32
+ meta.append(f"- **Source**: {self.source}")
33
+ if self.type != "insight":
34
+ meta.append(f"- **Type**: {self.type}")
35
+
36
+ # Status line with checkbox simulation
37
+ status_map = {
38
+ "pending": "[ ] Pending",
39
+ "tracked": "[x] Tracked",
40
+ "resolved": "[x] Resolved",
41
+ "dismissed": "[-] Dismissed"
42
+ }
43
+ meta.append(f"- **Status**: {status_map.get(self.status, '[ ] Pending')}")
44
+
45
+ if self.ref:
46
+ meta.append(f"- **Ref**: {self.ref}")
47
+
48
+ if self.context:
49
+ meta.append(f"- **Context**: `{self.context}`")
50
+
51
+ meta_block = "\n".join(meta)
52
+
53
+ return f"\n{header}\n{meta_block}\n\n{self.content.strip()}\n"
@@ -1,7 +1,7 @@
1
1
  ---
2
- name: monoco-memo
2
+ name: monoco_atom_memo
3
3
  description: Lightweight memo system for quickly recording ideas, inspirations, and temporary notes. Distinguished from the formal Issue system.
4
- type: standard
4
+ type: atom
5
5
  version: 1.0.0
6
6
  ---
7
7
 
@@ -1,7 +1,7 @@
1
1
  ---
2
- name: note-processing-workflow
2
+ name: monoco_workflow_note_processing
3
3
  description: Memo Note Processing Workflow (Flow Skill). Defines the standard operational process from capturing fleeting notes to organizing and archiving, ensuring effective management of ideas.
4
- type: flow
4
+ type: workflow
5
5
  domain: memo
6
6
  version: 1.0.0
7
7
  ---
@@ -1,7 +1,7 @@
1
1
  ---
2
- name: monoco-memo
2
+ name: monoco_atom_memo
3
3
  description: 轻量级备忘录系统,用于快速记录想法、灵感和临时笔记。与正式的 Issue 系统区分开来。
4
- type: standard
4
+ type: atom
5
5
  version: 1.0.0
6
6
  ---
7
7
 
@@ -1,7 +1,7 @@
1
1
  ---
2
- name: note-processing-workflow
2
+ name: monoco_workflow_note_processing
3
3
  description: Memo 笔记处理工作流 (Flow Skill)。定义从捕获 fleeting notes 到组织归档的标准操作流程,确保想法有效管理。
4
- type: flow
4
+ type: workflow
5
5
  domain: memo
6
6
  version: 1.0.0
7
7
  ---
@@ -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
  from monoco.features.spike import core
5
6
 
6
7
 
7
- class SpikeFeature(MonocoFeature):
8
+ class SpikeFeature(FeatureModule):
9
+ """Spike (research) feature module with unified lifecycle support."""
10
+
8
11
  @property
9
- def name(self) -> str:
10
- return "spike"
12
+ def metadata(self) -> FeatureMetadata:
13
+ return FeatureMetadata(
14
+ name="spike",
15
+ version="1.0.0",
16
+ description="Research spike management for external references",
17
+ dependencies=["core"],
18
+ priority=30,
19
+ )
11
20
 
12
- def initialize(self, root: Path, config: Dict) -> None:
21
+ def _on_mount(self, context: "FeatureContext") -> None: # type: ignore
22
+ """Initialize spike feature with workspace context."""
23
+ root = context.root
24
+ config = context.config
13
25
  spikes_name = config.get("paths", {}).get("spikes", ".references")
14
26
  core.init(root, spikes_name)
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
  base_dir = Path(__file__).parent / "resources"
@@ -1,7 +1,7 @@
1
1
  ---
2
- name: monoco-spike
2
+ name: monoco_atom_spike
3
3
  description: Manage external reference repositories for research and learning. Provides read-only access to curated codebases.
4
- type: standard
4
+ type: atom
5
5
  version: 1.0.0
6
6
  ---
7
7
 
@@ -1,7 +1,7 @@
1
1
  ---
2
- name: research-workflow
2
+ name: monoco_workflow_research
3
3
  description: Spike Research Workflow (Flow Skill). Defines the standard operational process from adding external repositories to knowledge extraction and archiving, ensuring effective management of external knowledge.
4
- type: flow
4
+ type: workflow
5
5
  domain: spike
6
6
  version: 1.0.0
7
7
  ---
@@ -1,7 +1,7 @@
1
1
  ---
2
- name: monoco-spike
2
+ name: monoco_atom_spike
3
3
  description: 管理用于研究和学习的外部参考仓库。提供对精选代码库的只读访问。
4
- type: standard
4
+ type: atom
5
5
  version: 1.0.0
6
6
  ---
7
7
 
@@ -1,7 +1,7 @@
1
1
  ---
2
- name: research-workflow
2
+ name: monoco_workflow_research
3
3
  description: Spike 研究工作流 (Flow Skill)。定义从添加外部仓库到知识提取和归档的标准操作流程,确保外部知识有效管理。
4
- type: flow
4
+ type: workflow
5
5
  domain: spike
6
6
  version: 1.0.0
7
7
  ---