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
monoco/core/sync.py
CHANGED
|
@@ -46,6 +46,12 @@ def sync_command(
|
|
|
46
46
|
help="Specific file to update (default: auto-detect from config or standard files)",
|
|
47
47
|
),
|
|
48
48
|
check: bool = typer.Option(False, "--check", help="Dry run check mode"),
|
|
49
|
+
workflows: bool = typer.Option(
|
|
50
|
+
False,
|
|
51
|
+
"--workflows",
|
|
52
|
+
"-w",
|
|
53
|
+
help="Also distribute Flow Skills as Antigravity Workflows to .agent/workflows/",
|
|
54
|
+
),
|
|
49
55
|
):
|
|
50
56
|
"""
|
|
51
57
|
Synchronize Agent Environment (System Prompts & Skills).
|
|
@@ -84,7 +90,39 @@ def sync_command(
|
|
|
84
90
|
f"[blue]Collected {len(collected_prompts)} prompts from {len(active_features)} features.[/blue]"
|
|
85
91
|
)
|
|
86
92
|
|
|
87
|
-
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# 3. Distribute Roles
|
|
96
|
+
console.print("[bold blue]Distributing agent roles...[/bold blue]")
|
|
97
|
+
|
|
98
|
+
# Source: Builtin Resource Dir
|
|
99
|
+
# monoco/core/sync.py -> monoco/core -> monoco -> features/agent/resources/roles
|
|
100
|
+
resource_dir = Path(__file__).parent.parent / "features" / "agent" / "resources" / "roles"
|
|
101
|
+
|
|
102
|
+
# Target: .monoco/roles
|
|
103
|
+
target_roles_dir = root / ".monoco" / "roles"
|
|
104
|
+
# Only create if we have sources
|
|
105
|
+
if resource_dir.exists():
|
|
106
|
+
target_roles_dir.mkdir(parents=True, exist_ok=True)
|
|
107
|
+
import shutil
|
|
108
|
+
|
|
109
|
+
count = 0
|
|
110
|
+
for yaml_file in resource_dir.glob("*.yaml"):
|
|
111
|
+
target_file = target_roles_dir / yaml_file.name
|
|
112
|
+
try:
|
|
113
|
+
# Copy only if different or new? For now, nice and simple overwrite.
|
|
114
|
+
shutil.copy2(yaml_file, target_file)
|
|
115
|
+
console.print(f"[dim] ✓ Synced role {yaml_file.name}[/dim]")
|
|
116
|
+
count += 1
|
|
117
|
+
except Exception as e:
|
|
118
|
+
console.print(f"[red] Failed to sync role {yaml_file.name}: {e}[/red]")
|
|
119
|
+
|
|
120
|
+
if count > 0:
|
|
121
|
+
console.print(f"[green] ✓ Updated {count} roles in .monoco/roles/[/green]")
|
|
122
|
+
else:
|
|
123
|
+
console.print("[yellow] No builtin roles found to sync.[/yellow]")
|
|
124
|
+
|
|
125
|
+
# 4. Distribute Skills
|
|
88
126
|
console.print("[bold blue]Distributing skills to agent frameworks...[/bold blue]")
|
|
89
127
|
|
|
90
128
|
# Determine language from config
|
|
@@ -124,6 +162,26 @@ def sync_command(
|
|
|
124
162
|
"[yellow]No agent frameworks detected. Skipping skill distribution.[/yellow]"
|
|
125
163
|
)
|
|
126
164
|
|
|
165
|
+
# 5. Distribute Workflows (if --workflows flag is set)
|
|
166
|
+
if workflows:
|
|
167
|
+
console.print("[bold blue]Distributing Flow Skills as Workflows...[/bold blue]")
|
|
168
|
+
|
|
169
|
+
try:
|
|
170
|
+
workflow_results = skill_manager.distribute_workflows(force=False, lang=skill_lang)
|
|
171
|
+
success_count = sum(1 for v in workflow_results.values() if v)
|
|
172
|
+
if workflow_results:
|
|
173
|
+
console.print(
|
|
174
|
+
f"[green] ✓ Distributed {success_count}/{len(workflow_results)} workflows to .agent/workflows/[/green]"
|
|
175
|
+
)
|
|
176
|
+
else:
|
|
177
|
+
console.print(
|
|
178
|
+
"[yellow] No Flow Skills found to convert[/yellow]"
|
|
179
|
+
)
|
|
180
|
+
except Exception as e:
|
|
181
|
+
console.print(
|
|
182
|
+
f"[red] Failed to distribute workflows: {e}[/red]"
|
|
183
|
+
)
|
|
184
|
+
|
|
127
185
|
# 4. Determine Targets
|
|
128
186
|
targets = _get_targets(root, config, target)
|
|
129
187
|
|
|
@@ -238,3 +296,17 @@ def uninstall_command(
|
|
|
238
296
|
console.print(
|
|
239
297
|
"[yellow]No agent frameworks detected. Skipping skill cleanup.[/yellow]"
|
|
240
298
|
)
|
|
299
|
+
|
|
300
|
+
# 3. Clean up Workflows
|
|
301
|
+
console.print("[bold blue]Cleaning up distributed workflows...[/bold blue]")
|
|
302
|
+
|
|
303
|
+
try:
|
|
304
|
+
removed_count = skill_manager.cleanup_workflows()
|
|
305
|
+
if removed_count > 0:
|
|
306
|
+
console.print(
|
|
307
|
+
f"[green] ✓ Removed {removed_count} workflows from .agent/workflows/[/green]"
|
|
308
|
+
)
|
|
309
|
+
except Exception as e:
|
|
310
|
+
console.print(
|
|
311
|
+
f"[red] Failed to clean workflows: {e}[/red]"
|
|
312
|
+
)
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Flow Skill to Antigravity Workflow Converter.
|
|
3
|
+
|
|
4
|
+
This module converts Monoco Flow Skills to Antigravity Workflow format.
|
|
5
|
+
|
|
6
|
+
Conversion Rules:
|
|
7
|
+
1. Frontmatter: Only keep 'description', discard other fields (name, type, role, version, author)
|
|
8
|
+
2. Filename: monoco_flow_engineer/SKILL.md -> flow-engineer.md
|
|
9
|
+
3. Content: Remove Mermaid state diagrams, convert to simple step lists
|
|
10
|
+
4. Output: .agent/workflows/ directory
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Optional, Tuple
|
|
16
|
+
import yaml
|
|
17
|
+
from rich.console import Console
|
|
18
|
+
|
|
19
|
+
console = Console()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class FlowSkillConverter:
|
|
23
|
+
"""Converts Flow Skill files to Antigravity Workflow format."""
|
|
24
|
+
|
|
25
|
+
# Source directories to search for Flow Skills
|
|
26
|
+
SOURCE_PATTERNS = [
|
|
27
|
+
"monoco/features/agent/resources/{lang}/skills/flow_*/SKILL.md",
|
|
28
|
+
"monoco/features/*/resources/{lang}/skills/flow_*/SKILL.md",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
def __init__(self, root_dir: Path):
|
|
32
|
+
"""
|
|
33
|
+
Initialize converter with project root directory.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
root_dir: Project root directory
|
|
37
|
+
"""
|
|
38
|
+
self.root_dir = root_dir
|
|
39
|
+
|
|
40
|
+
def _is_flow_skill(self, skill_file: Path) -> bool:
|
|
41
|
+
"""
|
|
42
|
+
Check if a SKILL.md file is a Flow Skill by reading its frontmatter.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
skill_file: Path to SKILL.md file
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
True if the skill is a Flow Skill (type == "flow")
|
|
49
|
+
"""
|
|
50
|
+
try:
|
|
51
|
+
content = skill_file.read_text(encoding="utf-8")
|
|
52
|
+
if not content.startswith("---"):
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
parts = content.split("---", 2)
|
|
56
|
+
if len(parts) < 3:
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
frontmatter = yaml.safe_load(parts[1].strip()) or {}
|
|
60
|
+
return frontmatter.get("type") == "flow"
|
|
61
|
+
except Exception:
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
def discover_flow_skills(self, lang: str = "zh") -> list[Path]:
|
|
65
|
+
"""
|
|
66
|
+
Discover all Flow Skill files in the source directories.
|
|
67
|
+
|
|
68
|
+
Flow Skills are discovered from monoco/features/*/resources/ directories,
|
|
69
|
+
not from the distributed .claude/skills/ directory.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
lang: Language code (default: "zh")
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
List of paths to Flow Skill SKILL.md files
|
|
76
|
+
"""
|
|
77
|
+
flow_skills = []
|
|
78
|
+
seen_names = set()
|
|
79
|
+
|
|
80
|
+
# Search in source directories
|
|
81
|
+
features_dir = self.root_dir / "monoco" / "features"
|
|
82
|
+
if features_dir.exists():
|
|
83
|
+
for feature_dir in features_dir.iterdir():
|
|
84
|
+
if not feature_dir.is_dir():
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
resources_dir = feature_dir / "resources" / lang / "skills"
|
|
88
|
+
if not resources_dir.exists():
|
|
89
|
+
continue
|
|
90
|
+
|
|
91
|
+
for skill_dir in resources_dir.iterdir():
|
|
92
|
+
if not skill_dir.is_dir():
|
|
93
|
+
continue
|
|
94
|
+
|
|
95
|
+
skill_file = skill_dir / "SKILL.md"
|
|
96
|
+
if skill_file.exists() and skill_dir.name not in seen_names:
|
|
97
|
+
# Check if it's a flow skill by reading frontmatter
|
|
98
|
+
if self._is_flow_skill(skill_file):
|
|
99
|
+
flow_skills.append(skill_file)
|
|
100
|
+
seen_names.add(skill_dir.name)
|
|
101
|
+
|
|
102
|
+
return sorted(flow_skills)
|
|
103
|
+
|
|
104
|
+
def convert_skill(self, skill_file: Path) -> Tuple[str, str]:
|
|
105
|
+
"""
|
|
106
|
+
Convert a Flow Skill file to Antigravity Workflow format.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
skill_file: Path to the Flow Skill SKILL.md file
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Tuple of (workflow_filename, workflow_content)
|
|
113
|
+
"""
|
|
114
|
+
content = skill_file.read_text(encoding="utf-8")
|
|
115
|
+
|
|
116
|
+
# Parse frontmatter
|
|
117
|
+
frontmatter, body = self._extract_frontmatter(content)
|
|
118
|
+
|
|
119
|
+
# Convert frontmatter (keep only description)
|
|
120
|
+
new_frontmatter = self._convert_frontmatter(frontmatter)
|
|
121
|
+
|
|
122
|
+
# Convert body content
|
|
123
|
+
new_body = self._convert_body(body)
|
|
124
|
+
|
|
125
|
+
# Generate workflow filename
|
|
126
|
+
workflow_filename = self._generate_filename(skill_file)
|
|
127
|
+
|
|
128
|
+
# Combine
|
|
129
|
+
if new_frontmatter:
|
|
130
|
+
workflow_content = f"---\n{new_frontmatter}---\n\n{new_body}"
|
|
131
|
+
else:
|
|
132
|
+
workflow_content = new_body
|
|
133
|
+
|
|
134
|
+
return workflow_filename, workflow_content
|
|
135
|
+
|
|
136
|
+
def _extract_frontmatter(self, content: str) -> Tuple[dict, str]:
|
|
137
|
+
"""
|
|
138
|
+
Extract YAML frontmatter from markdown content.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
content: Full markdown content
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Tuple of (frontmatter_dict, body_content)
|
|
145
|
+
"""
|
|
146
|
+
if not content.startswith("---"):
|
|
147
|
+
return {}, content
|
|
148
|
+
|
|
149
|
+
parts = content.split("---", 2)
|
|
150
|
+
if len(parts) < 3:
|
|
151
|
+
return {}, content
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
frontmatter = yaml.safe_load(parts[1].strip()) or {}
|
|
155
|
+
except yaml.YAMLError:
|
|
156
|
+
frontmatter = {}
|
|
157
|
+
|
|
158
|
+
body = parts[2].strip()
|
|
159
|
+
return frontmatter, body
|
|
160
|
+
|
|
161
|
+
def _convert_frontmatter(self, frontmatter: dict) -> str:
|
|
162
|
+
"""
|
|
163
|
+
Convert frontmatter to Antigravity Workflow format.
|
|
164
|
+
|
|
165
|
+
Only keeps 'description' field.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
frontmatter: Original frontmatter dictionary
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
New frontmatter as YAML string
|
|
172
|
+
"""
|
|
173
|
+
description = frontmatter.get("description", "")
|
|
174
|
+
|
|
175
|
+
if not description:
|
|
176
|
+
return ""
|
|
177
|
+
|
|
178
|
+
# Create minimal frontmatter with only description
|
|
179
|
+
new_frontmatter = {"description": description}
|
|
180
|
+
|
|
181
|
+
return yaml.dump(new_frontmatter, allow_unicode=True, sort_keys=False)
|
|
182
|
+
|
|
183
|
+
def _convert_body(self, body: str) -> str:
|
|
184
|
+
"""
|
|
185
|
+
Convert body content to Antigravity Workflow format.
|
|
186
|
+
|
|
187
|
+
- Remove Mermaid state diagrams
|
|
188
|
+
- Keep step sections but simplify
|
|
189
|
+
- Remove complex formatting
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
body: Original body content
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
Converted body content
|
|
196
|
+
"""
|
|
197
|
+
lines = body.split("\n")
|
|
198
|
+
result_lines = []
|
|
199
|
+
in_mermaid = False
|
|
200
|
+
skip_section = False
|
|
201
|
+
|
|
202
|
+
for line in lines:
|
|
203
|
+
# Detect Mermaid code block start
|
|
204
|
+
if line.strip().startswith("```mermaid"):
|
|
205
|
+
in_mermaid = True
|
|
206
|
+
continue
|
|
207
|
+
|
|
208
|
+
# Detect Mermaid code block end
|
|
209
|
+
if in_mermaid and line.strip() == "```":
|
|
210
|
+
in_mermaid = False
|
|
211
|
+
continue
|
|
212
|
+
|
|
213
|
+
# Skip lines inside Mermaid block
|
|
214
|
+
if in_mermaid:
|
|
215
|
+
continue
|
|
216
|
+
|
|
217
|
+
# Skip "工作流状态机" section header
|
|
218
|
+
if "工作流状态机" in line or "Workflow State Machine" in line:
|
|
219
|
+
skip_section = True
|
|
220
|
+
continue
|
|
221
|
+
|
|
222
|
+
# Detect new section (level 2 header)
|
|
223
|
+
if line.strip().startswith("## ") and skip_section:
|
|
224
|
+
skip_section = False
|
|
225
|
+
|
|
226
|
+
if skip_section:
|
|
227
|
+
continue
|
|
228
|
+
|
|
229
|
+
# Keep the line
|
|
230
|
+
result_lines.append(line)
|
|
231
|
+
|
|
232
|
+
# Clean up excessive blank lines
|
|
233
|
+
cleaned_lines = self._cleanup_blank_lines(result_lines)
|
|
234
|
+
|
|
235
|
+
return "\n".join(cleaned_lines)
|
|
236
|
+
|
|
237
|
+
def _cleanup_blank_lines(self, lines: list[str]) -> list[str]:
|
|
238
|
+
"""
|
|
239
|
+
Clean up excessive blank lines while preserving structure.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
lines: List of content lines
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
Cleaned list of lines
|
|
246
|
+
"""
|
|
247
|
+
result = []
|
|
248
|
+
prev_blank = False
|
|
249
|
+
|
|
250
|
+
for line in lines:
|
|
251
|
+
is_blank = not line.strip()
|
|
252
|
+
|
|
253
|
+
# Skip consecutive blank lines
|
|
254
|
+
if is_blank and prev_blank:
|
|
255
|
+
continue
|
|
256
|
+
|
|
257
|
+
result.append(line)
|
|
258
|
+
prev_blank = is_blank
|
|
259
|
+
|
|
260
|
+
# Remove trailing blank lines
|
|
261
|
+
while result and not result[-1].strip():
|
|
262
|
+
result.pop()
|
|
263
|
+
|
|
264
|
+
return result
|
|
265
|
+
|
|
266
|
+
def _generate_filename(self, skill_file: Path) -> str:
|
|
267
|
+
"""
|
|
268
|
+
Generate workflow filename from skill file path.
|
|
269
|
+
|
|
270
|
+
Conversion: flow_engineer/SKILL.md -> flow-engineer.md
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
skill_file: Path to the Flow Skill SKILL.md file
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
Workflow filename
|
|
277
|
+
"""
|
|
278
|
+
# Get parent directory name (e.g., "flow_engineer")
|
|
279
|
+
skill_dir_name = skill_file.parent.name
|
|
280
|
+
|
|
281
|
+
# Remove "flow_" prefix
|
|
282
|
+
if skill_dir_name.startswith("flow_"):
|
|
283
|
+
role_name = skill_dir_name[len("flow_"):]
|
|
284
|
+
else:
|
|
285
|
+
role_name = skill_dir_name
|
|
286
|
+
|
|
287
|
+
# Convert to workflow filename
|
|
288
|
+
workflow_filename = f"flow-{role_name}.md"
|
|
289
|
+
|
|
290
|
+
return workflow_filename
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
class WorkflowDistributor:
|
|
294
|
+
"""Distributes converted workflows to target directory."""
|
|
295
|
+
|
|
296
|
+
def __init__(self, root_dir: Path):
|
|
297
|
+
"""
|
|
298
|
+
Initialize distributor.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
root_dir: Project root directory
|
|
302
|
+
"""
|
|
303
|
+
self.root_dir = root_dir
|
|
304
|
+
self.converter = FlowSkillConverter(root_dir)
|
|
305
|
+
|
|
306
|
+
def distribute(self, force: bool = False, lang: str = "zh") -> dict[str, bool]:
|
|
307
|
+
"""
|
|
308
|
+
Convert and distribute all Flow Skills to .agent/workflows/.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
force: Overwrite existing files even if unchanged
|
|
312
|
+
lang: Language code for Flow Skills (default: "zh")
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
Dictionary mapping workflow filenames to success status
|
|
316
|
+
"""
|
|
317
|
+
results = {}
|
|
318
|
+
|
|
319
|
+
# Discover flow skills
|
|
320
|
+
flow_skills = self.converter.discover_flow_skills(lang=lang)
|
|
321
|
+
|
|
322
|
+
if not flow_skills:
|
|
323
|
+
console.print("[yellow]No Flow Skills found to convert[/yellow]")
|
|
324
|
+
return results
|
|
325
|
+
|
|
326
|
+
# Target directory
|
|
327
|
+
workflows_dir = self.root_dir / ".agent" / "workflows"
|
|
328
|
+
workflows_dir.mkdir(parents=True, exist_ok=True)
|
|
329
|
+
|
|
330
|
+
console.print(f"[dim]Found {len(flow_skills)} Flow Skills to convert[/dim]")
|
|
331
|
+
|
|
332
|
+
for skill_file in flow_skills:
|
|
333
|
+
try:
|
|
334
|
+
workflow_filename, workflow_content = self.converter.convert_skill(skill_file)
|
|
335
|
+
target_file = workflows_dir / workflow_filename
|
|
336
|
+
|
|
337
|
+
# Check if update is needed
|
|
338
|
+
if target_file.exists() and not force:
|
|
339
|
+
existing_content = target_file.read_text(encoding="utf-8")
|
|
340
|
+
if existing_content == workflow_content:
|
|
341
|
+
console.print(f"[dim] = {workflow_filename} is up to date[/dim]")
|
|
342
|
+
results[workflow_filename] = True
|
|
343
|
+
continue
|
|
344
|
+
|
|
345
|
+
# Write workflow file
|
|
346
|
+
target_file.write_text(workflow_content, encoding="utf-8")
|
|
347
|
+
console.print(f"[green] ✓ Created {workflow_filename}[/green]")
|
|
348
|
+
results[workflow_filename] = True
|
|
349
|
+
|
|
350
|
+
except Exception as e:
|
|
351
|
+
console.print(f"[red] ✗ Failed to convert {skill_file.name}: {e}[/red]")
|
|
352
|
+
results[skill_file.name] = False
|
|
353
|
+
|
|
354
|
+
return results
|
|
355
|
+
|
|
356
|
+
def cleanup(self, lang: str = "zh") -> int:
|
|
357
|
+
"""
|
|
358
|
+
Remove all distributed workflows from .agent/workflows/.
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
lang: Language code for Flow Skills (default: "zh")
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
Number of files removed
|
|
365
|
+
"""
|
|
366
|
+
workflows_dir = self.root_dir / ".agent" / "workflows"
|
|
367
|
+
|
|
368
|
+
if not workflows_dir.exists():
|
|
369
|
+
return 0
|
|
370
|
+
|
|
371
|
+
removed_count = 0
|
|
372
|
+
|
|
373
|
+
# Discover flow skills to know which files to remove
|
|
374
|
+
flow_skills = self.converter.discover_flow_skills(lang=lang)
|
|
375
|
+
workflow_filenames = set()
|
|
376
|
+
|
|
377
|
+
for skill_file in flow_skills:
|
|
378
|
+
workflow_filename = self.converter._generate_filename(skill_file)
|
|
379
|
+
workflow_filenames.add(workflow_filename)
|
|
380
|
+
|
|
381
|
+
# Remove workflow files
|
|
382
|
+
for workflow_file in workflows_dir.glob("flow-*.md"):
|
|
383
|
+
if workflow_file.name in workflow_filenames:
|
|
384
|
+
workflow_file.unlink()
|
|
385
|
+
console.print(f"[green] ✓ Removed {workflow_file.name}[/green]")
|
|
386
|
+
removed_count += 1
|
|
387
|
+
|
|
388
|
+
# Remove empty directory
|
|
389
|
+
if workflows_dir.exists() and not any(workflows_dir.iterdir()):
|
|
390
|
+
workflows_dir.rmdir()
|
|
391
|
+
console.print(f"[dim] Removed empty directory: {workflows_dir}[/dim]")
|
|
392
|
+
|
|
393
|
+
if removed_count == 0:
|
|
394
|
+
console.print(f"[dim]No workflows to remove from {workflows_dir}[/dim]")
|
|
395
|
+
|
|
396
|
+
return removed_count
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def convert_flow_skill_to_workflow(skill_content: str) -> str:
|
|
400
|
+
"""
|
|
401
|
+
Convert Flow Skill content to Antigravity Workflow format.
|
|
402
|
+
|
|
403
|
+
This is a standalone utility function for direct content conversion.
|
|
404
|
+
|
|
405
|
+
Args:
|
|
406
|
+
skill_content: Original Flow Skill markdown content
|
|
407
|
+
|
|
408
|
+
Returns:
|
|
409
|
+
Converted Workflow markdown content
|
|
410
|
+
"""
|
|
411
|
+
converter = FlowSkillConverter(Path("."))
|
|
412
|
+
|
|
413
|
+
frontmatter, body = converter._extract_frontmatter(skill_content)
|
|
414
|
+
new_frontmatter = converter._convert_frontmatter(frontmatter)
|
|
415
|
+
new_body = converter._convert_body(body)
|
|
416
|
+
|
|
417
|
+
if new_frontmatter:
|
|
418
|
+
return f"---\n{new_frontmatter}---\n\n{new_body}"
|
|
419
|
+
else:
|
|
420
|
+
return new_body
|
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
from .models import RoleTemplate, SchedulerConfig
|
|
1
|
+
from .models import RoleTemplate, AgentRoleConfig as AgentConfig, SchedulerConfig
|
|
2
2
|
from .worker import Worker
|
|
3
|
-
from .config import load_scheduler_config
|
|
3
|
+
from .config import load_scheduler_config, load_agent_config
|
|
4
4
|
from .defaults import DEFAULT_ROLES
|
|
5
5
|
from .session import Session, RuntimeSession
|
|
6
6
|
from .manager import SessionManager
|
|
7
|
-
from .
|
|
7
|
+
from .apoptosis import ApoptosisManager
|
|
8
8
|
|
|
9
9
|
__all__ = [
|
|
10
10
|
"RoleTemplate",
|
|
11
|
+
"AgentConfig",
|
|
11
12
|
"SchedulerConfig",
|
|
13
|
+
"load_agent_config",
|
|
12
14
|
"Worker",
|
|
13
15
|
"load_scheduler_config",
|
|
14
16
|
"DEFAULT_ROLES",
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Dict
|
|
3
|
+
from monoco.core.feature import MonocoFeature, IntegrationData
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AgentFeature(MonocoFeature):
|
|
7
|
+
@property
|
|
8
|
+
def name(self) -> str:
|
|
9
|
+
return "agent"
|
|
10
|
+
|
|
11
|
+
def initialize(self, root: Path, config: Dict) -> None:
|
|
12
|
+
# Agent feature doesn't require special initialization
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
def integrate(self, root: Path, config: Dict) -> IntegrationData:
|
|
16
|
+
# Determine language from config, default to 'en'
|
|
17
|
+
lang = config.get("i18n", {}).get("source_lang", "en")
|
|
18
|
+
|
|
19
|
+
# Resource path: monoco/features/agent/resources/{lang}/AGENTS.md
|
|
20
|
+
base_dir = Path(__file__).parent / "resources"
|
|
21
|
+
|
|
22
|
+
# Try specific language, fallback to 'en'
|
|
23
|
+
prompt_file = base_dir / lang / "AGENTS.md"
|
|
24
|
+
if not prompt_file.exists():
|
|
25
|
+
prompt_file = base_dir / "en" / "AGENTS.md"
|
|
26
|
+
|
|
27
|
+
content = ""
|
|
28
|
+
if prompt_file.exists():
|
|
29
|
+
content = prompt_file.read_text(encoding="utf-8").strip()
|
|
30
|
+
|
|
31
|
+
return IntegrationData(system_prompts={"Agent": content})
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from .manager import SessionManager
|
|
2
|
+
from .models import RoleTemplate
|
|
3
|
+
|
|
4
|
+
class ApoptosisManager:
|
|
5
|
+
"""
|
|
6
|
+
Manages the controlled shutdown (Apoptosis) and investigation (Autopsy)
|
|
7
|
+
of failed agent sessions.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
def __init__(self, session_manager: SessionManager):
|
|
11
|
+
self.session_manager = session_manager
|
|
12
|
+
|
|
13
|
+
def trigger_apoptosis(self, session_id: str) -> None:
|
|
14
|
+
"""
|
|
15
|
+
Trigger the apoptosis process for a given session.
|
|
16
|
+
1. Mark session as crashed.
|
|
17
|
+
2. Spin up a Coroner agent to diagnose.
|
|
18
|
+
"""
|
|
19
|
+
session = self.session_manager.get_session(session_id)
|
|
20
|
+
if not session:
|
|
21
|
+
return
|
|
22
|
+
|
|
23
|
+
# 1. Mark as crashed
|
|
24
|
+
session.model.status = "crashed"
|
|
25
|
+
|
|
26
|
+
# 2. Start Coroner
|
|
27
|
+
self._perform_autopsy(session)
|
|
28
|
+
|
|
29
|
+
def _perform_autopsy(self, victim_session):
|
|
30
|
+
coroner_role = RoleTemplate(
|
|
31
|
+
name="Coroner",
|
|
32
|
+
description="Investigates cause of death for failed agents.",
|
|
33
|
+
trigger="system.crash",
|
|
34
|
+
goal="Determine why the previous agent failed.",
|
|
35
|
+
system_prompt="You are the Coroner. Analyze the logs.",
|
|
36
|
+
engine="gemini"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
coroner_session = self.session_manager.create_session(
|
|
40
|
+
issue_id=victim_session.model.issue_id,
|
|
41
|
+
role=coroner_role
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
coroner_session.start()
|