@trac3er/oh-my-god 1.0.2
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.
- package/.claude-plugin/marketplace.json +36 -0
- package/.claude-plugin/plugin.json +23 -0
- package/.claude-plugin/scripts/install.sh +49 -0
- package/.claude-plugin/scripts/uninstall.sh +80 -0
- package/.claude-plugin/scripts/update.sh +84 -0
- package/.mcp.json +20 -0
- package/LICENSE +21 -0
- package/OMG-setup.sh +1093 -0
- package/README.md +335 -0
- package/THIRD_PARTY_NOTICES.md +24 -0
- package/UPSTREAM_DIFF.md +20 -0
- package/agents/__init__.py +1 -0
- package/agents/_model_roles.yaml +26 -0
- package/agents/designer.md +67 -0
- package/agents/explore.md +60 -0
- package/agents/model_roles.py +196 -0
- package/agents/omg-api-builder.md +23 -0
- package/agents/omg-architect-mode.md +43 -0
- package/agents/omg-architect.md +13 -0
- package/agents/omg-backend-engineer.md +43 -0
- package/agents/omg-critic.md +16 -0
- package/agents/omg-database-engineer.md +43 -0
- package/agents/omg-escalation-router.md +17 -0
- package/agents/omg-executor.md +12 -0
- package/agents/omg-frontend-designer.md +42 -0
- package/agents/omg-implement-mode.md +50 -0
- package/agents/omg-infra-engineer.md +43 -0
- package/agents/omg-qa-tester.md +16 -0
- package/agents/omg-research-mode.md +43 -0
- package/agents/omg-security-auditor.md +43 -0
- package/agents/omg-testing-engineer.md +43 -0
- package/agents/plan.md +80 -0
- package/agents/quick_task.md +64 -0
- package/agents/reviewer.md +83 -0
- package/agents/task.md +71 -0
- package/commands/OMG:ccg.md +22 -0
- package/commands/OMG:compat.md +57 -0
- package/commands/OMG:crazy.md +125 -0
- package/commands/OMG:domain-init.md +11 -0
- package/commands/OMG:escalate.md +52 -0
- package/commands/OMG:health-check.md +45 -0
- package/commands/OMG:init.md +134 -0
- package/commands/OMG:mode.md +44 -0
- package/commands/OMG:project-init.md +11 -0
- package/commands/OMG:ralph-start.md +43 -0
- package/commands/OMG:ralph-stop.md +23 -0
- package/commands/OMG:teams.md +39 -0
- package/commands/ai-commit.md +113 -0
- package/commands/ccg.md +9 -0
- package/commands/create-agent.md +183 -0
- package/commands/omc-teams.md +9 -0
- package/commands/session-branch.md +85 -0
- package/commands/session-fork.md +53 -0
- package/commands/session-merge.md +134 -0
- package/commands/theme.md +44 -0
- package/config/lsp_languages.yaml +324 -0
- package/config/themes/catppuccin-frappe.yaml +14 -0
- package/config/themes/catppuccin-latte.yaml +14 -0
- package/config/themes/catppuccin-macchiato.yaml +14 -0
- package/config/themes/catppuccin-mocha.yaml +14 -0
- package/config/themes/dracula.yaml +14 -0
- package/config/themes/gruvbox-dark.yaml +14 -0
- package/config/themes/nord.yaml +14 -0
- package/config/themes/one-dark.yaml +14 -0
- package/config/themes/solarized-dark.yaml +14 -0
- package/config/themes/tokyo-night.yaml +14 -0
- package/control_plane/__init__.py +2 -0
- package/control_plane/openapi.yaml +109 -0
- package/control_plane/server.py +107 -0
- package/control_plane/service.py +148 -0
- package/crates/omg-natives/Cargo.toml +17 -0
- package/crates/omg-natives/src/clipboard.rs +5 -0
- package/crates/omg-natives/src/glob.rs +15 -0
- package/crates/omg-natives/src/grep.rs +15 -0
- package/crates/omg-natives/src/highlight.rs +15 -0
- package/crates/omg-natives/src/html.rs +14 -0
- package/crates/omg-natives/src/image.rs +5 -0
- package/crates/omg-natives/src/keys.rs +5 -0
- package/crates/omg-natives/src/lib.rs +36 -0
- package/crates/omg-natives/src/prof.rs +5 -0
- package/crates/omg-natives/src/ps.rs +5 -0
- package/crates/omg-natives/src/shell.rs +5 -0
- package/crates/omg-natives/src/task.rs +5 -0
- package/crates/omg-natives/src/text.rs +14 -0
- package/hooks/_agent_registry.py +421 -0
- package/hooks/_budget.py +31 -0
- package/hooks/_common.py +476 -0
- package/hooks/_learnings.py +126 -0
- package/hooks/_memory.py +103 -0
- package/hooks/circuit-breaker.py +270 -0
- package/hooks/config-guard.py +163 -0
- package/hooks/context_pressure.py +53 -0
- package/hooks/credential_store.py +801 -0
- package/hooks/fetch-rate-limits.py +212 -0
- package/hooks/firewall.py +48 -0
- package/hooks/hashline-formatter-bridge.py +224 -0
- package/hooks/hashline-injector.py +273 -0
- package/hooks/hashline-validator.py +216 -0
- package/hooks/idle-detector.py +95 -0
- package/hooks/intentgate-keyword-detector.py +188 -0
- package/hooks/magic-keyword-router.py +195 -0
- package/hooks/policy_engine.py +310 -0
- package/hooks/post-tool-failure.py +19 -0
- package/hooks/post-write.py +199 -0
- package/hooks/pre-compact.py +204 -0
- package/hooks/pre-tool-inject.py +98 -0
- package/hooks/prompt-enhancer.py +672 -0
- package/hooks/quality-runner.py +191 -0
- package/hooks/secret-guard.py +47 -0
- package/hooks/session-end-capture.py +137 -0
- package/hooks/session-start.py +275 -0
- package/hooks/shadow_manager.py +297 -0
- package/hooks/state_migration.py +209 -0
- package/hooks/stop-gate.py +7 -0
- package/hooks/stop_dispatcher.py +929 -0
- package/hooks/test-validator.py +138 -0
- package/hooks/todo-state-tracker.py +114 -0
- package/hooks/tool-ledger.py +126 -0
- package/hooks/trust_review.py +524 -0
- package/install.sh +9 -0
- package/omg_natives/__init__.py +186 -0
- package/omg_natives/_bindings.py +165 -0
- package/omg_natives/clipboard.py +36 -0
- package/omg_natives/glob.py +42 -0
- package/omg_natives/grep.py +61 -0
- package/omg_natives/highlight.py +54 -0
- package/omg_natives/html.py +157 -0
- package/omg_natives/image.py +51 -0
- package/omg_natives/keys.py +46 -0
- package/omg_natives/prof.py +39 -0
- package/omg_natives/ps.py +93 -0
- package/omg_natives/shell.py +58 -0
- package/omg_natives/task.py +41 -0
- package/omg_natives/text.py +50 -0
- package/package.json +26 -0
- package/plugins/README.md +82 -0
- package/plugins/advanced/commands/OMG:code-review.md +114 -0
- package/plugins/advanced/commands/OMG:deep-plan.md +221 -0
- package/plugins/advanced/commands/OMG:handoff.md +115 -0
- package/plugins/advanced/commands/OMG:learn.md +110 -0
- package/plugins/advanced/commands/OMG:maintainer.md +31 -0
- package/plugins/advanced/commands/OMG:ralph-start.md +43 -0
- package/plugins/advanced/commands/OMG:ralph-stop.md +23 -0
- package/plugins/advanced/commands/OMG:security-review.md +119 -0
- package/plugins/advanced/commands/OMG:sequential-thinking.md +20 -0
- package/plugins/advanced/commands/OMG:ship.md +46 -0
- package/plugins/advanced/plugin.json +96 -0
- package/plugins/core/plugin.json +82 -0
- package/pytest.ini +5 -0
- package/registry/__init__.py +1 -0
- package/registry/verify_artifact.py +90 -0
- package/rules/contextual/architect-mode.md +9 -0
- package/rules/contextual/big-picture.md +20 -0
- package/rules/contextual/code-hygiene.md +26 -0
- package/rules/contextual/context-management.md +19 -0
- package/rules/contextual/context-minimization.md +32 -0
- package/rules/contextual/ddd-sdd.md +28 -0
- package/rules/contextual/dependency-safety.md +16 -0
- package/rules/contextual/doc-check.md +13 -0
- package/rules/contextual/implement-mode.md +9 -0
- package/rules/contextual/infra-safety.md +14 -0
- package/rules/contextual/outside-in.md +13 -0
- package/rules/contextual/persistent-mode.md +24 -0
- package/rules/contextual/research-mode.md +9 -0
- package/rules/contextual/security-domains.md +25 -0
- package/rules/contextual/vision-detection.md +27 -0
- package/rules/contextual/web-search.md +25 -0
- package/rules/contextual/write-verify.md +23 -0
- package/rules/core/00-truth.md +20 -0
- package/rules/core/01-surgical.md +19 -0
- package/rules/core/02-circuit-breaker.md +22 -0
- package/rules/core/03-ensemble.md +28 -0
- package/rules/core/04-testing.md +30 -0
- package/runtime/__init__.py +32 -0
- package/runtime/adapters/__init__.py +13 -0
- package/runtime/adapters/claude.py +60 -0
- package/runtime/adapters/gpt.py +53 -0
- package/runtime/adapters/local.py +53 -0
- package/runtime/business_workflow.py +220 -0
- package/runtime/compat.py +1299 -0
- package/runtime/custom_agent_loader.py +366 -0
- package/runtime/dispatcher.py +47 -0
- package/runtime/ecosystem.py +371 -0
- package/runtime/legacy_compat.py +7 -0
- package/runtime/omc_compat.py +7 -0
- package/runtime/omc_contract_snapshot.json +916 -0
- package/runtime/omg_compat_contract_snapshot.json +916 -0
- package/runtime/subagent_dispatcher.py +362 -0
- package/runtime/team_router.py +838 -0
- package/scripts/check-omc-contract-snapshot.py +12 -0
- package/scripts/check-omg-compat-contract-snapshot.py +137 -0
- package/scripts/check-omg-standalone-clean.py +102 -0
- package/scripts/legacy_to_omg_migrate.py +29 -0
- package/scripts/migrate-omc.py +464 -0
- package/scripts/omc_to_omg_migrate.py +12 -0
- package/scripts/omg.py +493 -0
- package/scripts/settings-merge.py +224 -0
- package/scripts/verify-no-omc.sh +5 -0
- package/scripts/verify-standalone.sh +21 -0
- package/templates/idea.yml +30 -0
- package/templates/policy.yaml +15 -0
- package/templates/profile.yaml +25 -0
- package/templates/runtime.yaml +12 -0
- package/templates/working-memory.md +17 -0
- package/tools/__init__.py +2 -0
- package/tools/browser_consent.py +289 -0
- package/tools/browser_stealth.py +481 -0
- package/tools/browser_tool.py +448 -0
- package/tools/changelog_generator.py +268 -0
- package/tools/commit_splitter.py +361 -0
- package/tools/config_discovery.py +151 -0
- package/tools/config_merger.py +449 -0
- package/tools/git_inspector.py +298 -0
- package/tools/lsp_client.py +275 -0
- package/tools/lsp_discovery.py +231 -0
- package/tools/lsp_operations.py +392 -0
- package/tools/python_repl.py +656 -0
- package/tools/python_sandbox.py +609 -0
- package/tools/search_providers/__init__.py +77 -0
- package/tools/search_providers/brave.py +115 -0
- package/tools/search_providers/exa.py +116 -0
- package/tools/search_providers/jina.py +104 -0
- package/tools/search_providers/perplexity.py +139 -0
- package/tools/search_providers/synthetic.py +74 -0
- package/tools/session_snapshot.py +736 -0
- package/tools/ssh_manager.py +912 -0
- package/tools/theme_engine.py +294 -0
- package/tools/theme_selector.py +137 -0
- package/tools/web_search.py +622 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
"""Parallel Execution Backend — concurrent subagent job management.
|
|
2
|
+
|
|
3
|
+
Manages concurrent subagent jobs with isolation, artifact streaming,
|
|
4
|
+
and a 100-job limit using stdlib ThreadPoolExecutor.
|
|
5
|
+
|
|
6
|
+
Feature flag: OMG_PARALLEL_SUBAGENTS_ENABLED (default: False)
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
import time
|
|
13
|
+
import uuid
|
|
14
|
+
import threading
|
|
15
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
16
|
+
from datetime import datetime, timezone
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
# --- Path resolution (never relies on CWD) ---
|
|
20
|
+
_DISPATCHER_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
21
|
+
_OMG_ROOT = os.path.dirname(_DISPATCHER_DIR)
|
|
22
|
+
|
|
23
|
+
# --- Constants ---
|
|
24
|
+
MAX_JOBS = 100
|
|
25
|
+
|
|
26
|
+
# --- Module-level singletons ---
|
|
27
|
+
_executor: ThreadPoolExecutor | None = None
|
|
28
|
+
_jobs: dict[str, dict[str, Any]] = {}
|
|
29
|
+
_lock = threading.Lock()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _get_feature_flag() -> Any:
|
|
33
|
+
"""Lazy-import get_feature_flag from hooks/_common.py."""
|
|
34
|
+
hooks_dir = os.path.join(_OMG_ROOT, "hooks")
|
|
35
|
+
if hooks_dir not in sys.path:
|
|
36
|
+
sys.path.insert(0, hooks_dir)
|
|
37
|
+
try:
|
|
38
|
+
from _common import get_feature_flag # pyright: ignore[reportMissingImports]
|
|
39
|
+
return get_feature_flag
|
|
40
|
+
except ImportError:
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _get_atomic_json_write() -> Any:
|
|
45
|
+
"""Lazy-import atomic_json_write from hooks/_common.py."""
|
|
46
|
+
hooks_dir = os.path.join(_OMG_ROOT, "hooks")
|
|
47
|
+
if hooks_dir not in sys.path:
|
|
48
|
+
sys.path.insert(0, hooks_dir)
|
|
49
|
+
try:
|
|
50
|
+
from _common import atomic_json_write # pyright: ignore[reportMissingImports]
|
|
51
|
+
return atomic_json_write
|
|
52
|
+
except ImportError:
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _is_enabled() -> bool:
|
|
57
|
+
"""Check if parallel subagents feature is enabled.
|
|
58
|
+
|
|
59
|
+
Resolution: env var OMG_PARALLEL_SUBAGENTS_ENABLED → settings.json → default False.
|
|
60
|
+
"""
|
|
61
|
+
# Fast path: check env var directly
|
|
62
|
+
env_val = os.environ.get("OMG_PARALLEL_SUBAGENTS_ENABLED", "").lower()
|
|
63
|
+
if env_val in ("0", "false", "no"):
|
|
64
|
+
return False
|
|
65
|
+
if env_val in ("1", "true", "yes"):
|
|
66
|
+
return True
|
|
67
|
+
|
|
68
|
+
# Slow path: check via get_feature_flag
|
|
69
|
+
get_flag = _get_feature_flag()
|
|
70
|
+
if get_flag is not None:
|
|
71
|
+
return get_flag("PARALLEL_SUBAGENTS", default=False)
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _get_project_dir() -> str:
|
|
76
|
+
"""Get project directory from env or cwd."""
|
|
77
|
+
return os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _jobs_dir() -> str:
|
|
81
|
+
"""Return the jobs state directory path."""
|
|
82
|
+
return os.path.join(_get_project_dir(), ".omg", "state", "jobs")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _job_path(job_id: str) -> str:
|
|
86
|
+
"""Return the file path for a specific job."""
|
|
87
|
+
return os.path.join(_jobs_dir(), f"{job_id}.json")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _persist_job(job_id: str, record: dict[str, Any]) -> None:
|
|
91
|
+
"""Persist job record to disk via atomic_json_write."""
|
|
92
|
+
writer = _get_atomic_json_write()
|
|
93
|
+
if writer is not None:
|
|
94
|
+
writer(_job_path(job_id), record)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _load_job_from_disk(job_id: str) -> dict[str, Any] | None:
|
|
98
|
+
"""Load a job record from disk if it exists."""
|
|
99
|
+
path = _job_path(job_id)
|
|
100
|
+
if not os.path.exists(path):
|
|
101
|
+
return None
|
|
102
|
+
try:
|
|
103
|
+
import json
|
|
104
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
105
|
+
return json.load(f)
|
|
106
|
+
except (OSError, ValueError):
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def get_executor() -> ThreadPoolExecutor:
|
|
111
|
+
"""Lazy-init and return the module-level ThreadPoolExecutor singleton."""
|
|
112
|
+
global _executor
|
|
113
|
+
if _executor is None:
|
|
114
|
+
_executor = ThreadPoolExecutor(max_workers=MAX_JOBS)
|
|
115
|
+
return _executor
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _running_count() -> int:
|
|
119
|
+
"""Count jobs with status 'running'."""
|
|
120
|
+
return len([j for j in _jobs.values() if j["status"] == "running"])
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def submit_job(
|
|
124
|
+
agent_name: str,
|
|
125
|
+
task_text: str,
|
|
126
|
+
isolation: str = "none",
|
|
127
|
+
) -> str:
|
|
128
|
+
"""Submit a subagent job for concurrent execution.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
agent_name: Name of the agent to dispatch.
|
|
132
|
+
task_text: Task description / prompt for the agent.
|
|
133
|
+
isolation: Isolation backend — "none" (default) or "worktree".
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
job_id (8-char hex string).
|
|
137
|
+
|
|
138
|
+
Raises:
|
|
139
|
+
RuntimeError: If feature is disabled or job limit reached.
|
|
140
|
+
"""
|
|
141
|
+
if not _is_enabled():
|
|
142
|
+
raise RuntimeError("feature disabled")
|
|
143
|
+
|
|
144
|
+
with _lock:
|
|
145
|
+
if _running_count() >= MAX_JOBS:
|
|
146
|
+
raise RuntimeError("job limit reached")
|
|
147
|
+
|
|
148
|
+
job_id = uuid.uuid4().hex[:8]
|
|
149
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
150
|
+
record: dict[str, Any] = {
|
|
151
|
+
"job_id": job_id,
|
|
152
|
+
"agent_name": agent_name,
|
|
153
|
+
"task_text": task_text,
|
|
154
|
+
"isolation": isolation,
|
|
155
|
+
"status": "queued",
|
|
156
|
+
"created_at": now,
|
|
157
|
+
"artifacts": [],
|
|
158
|
+
"error": None,
|
|
159
|
+
}
|
|
160
|
+
_jobs[job_id] = record
|
|
161
|
+
|
|
162
|
+
# Persist initial state
|
|
163
|
+
_persist_job(job_id, record)
|
|
164
|
+
|
|
165
|
+
# Submit to thread pool — returns immediately
|
|
166
|
+
executor = get_executor()
|
|
167
|
+
executor.submit(_run_job, job_id)
|
|
168
|
+
|
|
169
|
+
return job_id
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _check_git_available() -> bool:
|
|
173
|
+
"""Return True if git is available on PATH."""
|
|
174
|
+
import shutil
|
|
175
|
+
return shutil.which("git") is not None
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _setup_worktree(job_id: str) -> str | None:
|
|
179
|
+
"""Attempt to create a git worktree for job isolation.
|
|
180
|
+
|
|
181
|
+
Returns worktree path on success, None on failure.
|
|
182
|
+
"""
|
|
183
|
+
if not _check_git_available():
|
|
184
|
+
return None
|
|
185
|
+
|
|
186
|
+
import subprocess
|
|
187
|
+
|
|
188
|
+
project_dir = _get_project_dir()
|
|
189
|
+
worktree_dir = os.path.join(project_dir, ".omg", "worktrees", job_id)
|
|
190
|
+
|
|
191
|
+
try:
|
|
192
|
+
os.makedirs(os.path.dirname(worktree_dir), exist_ok=True)
|
|
193
|
+
subprocess.run(
|
|
194
|
+
["git", "worktree", "add", "--detach", worktree_dir],
|
|
195
|
+
capture_output=True,
|
|
196
|
+
text=True,
|
|
197
|
+
check=True,
|
|
198
|
+
timeout=30,
|
|
199
|
+
cwd=project_dir,
|
|
200
|
+
)
|
|
201
|
+
return worktree_dir
|
|
202
|
+
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, OSError):
|
|
203
|
+
return None
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _cleanup_worktree(worktree_dir: str) -> None:
|
|
207
|
+
"""Remove a git worktree (best-effort)."""
|
|
208
|
+
import subprocess
|
|
209
|
+
import shutil
|
|
210
|
+
|
|
211
|
+
project_dir = _get_project_dir()
|
|
212
|
+
try:
|
|
213
|
+
subprocess.run(
|
|
214
|
+
["git", "worktree", "remove", "--force", worktree_dir],
|
|
215
|
+
capture_output=True,
|
|
216
|
+
text=True,
|
|
217
|
+
check=False,
|
|
218
|
+
timeout=15,
|
|
219
|
+
cwd=project_dir,
|
|
220
|
+
)
|
|
221
|
+
except (subprocess.TimeoutExpired, OSError):
|
|
222
|
+
pass
|
|
223
|
+
|
|
224
|
+
# Fallback: remove directory if still exists
|
|
225
|
+
try:
|
|
226
|
+
if os.path.isdir(worktree_dir):
|
|
227
|
+
shutil.rmtree(worktree_dir, ignore_errors=True)
|
|
228
|
+
except OSError:
|
|
229
|
+
pass
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _run_job(job_id: str) -> None:
|
|
233
|
+
"""Execute a subagent job in the thread pool.
|
|
234
|
+
|
|
235
|
+
Updates job status and persists artifacts as they are produced.
|
|
236
|
+
NOTE: Does NOT actually spawn Claude — simulates execution for now.
|
|
237
|
+
"""
|
|
238
|
+
with _lock:
|
|
239
|
+
record = _jobs.get(job_id)
|
|
240
|
+
if record is None:
|
|
241
|
+
return
|
|
242
|
+
if record["status"] == "cancelled":
|
|
243
|
+
return
|
|
244
|
+
record["status"] = "running"
|
|
245
|
+
record["started_at"] = datetime.now(timezone.utc).isoformat()
|
|
246
|
+
|
|
247
|
+
_persist_job(job_id, record)
|
|
248
|
+
|
|
249
|
+
worktree_dir: str | None = None
|
|
250
|
+
try:
|
|
251
|
+
# Setup isolation if requested
|
|
252
|
+
if record.get("isolation") == "worktree":
|
|
253
|
+
worktree_dir = _setup_worktree(job_id)
|
|
254
|
+
if worktree_dir:
|
|
255
|
+
with _lock:
|
|
256
|
+
record["worktree"] = worktree_dir
|
|
257
|
+
_persist_job(job_id, record)
|
|
258
|
+
|
|
259
|
+
# --- Simulated execution ---
|
|
260
|
+
# Real implementation would dispatch to an agent here.
|
|
261
|
+
# For now, simulate work + produce an artifact.
|
|
262
|
+
time.sleep(0.05) # Simulate brief work
|
|
263
|
+
|
|
264
|
+
artifact = {
|
|
265
|
+
"type": "result",
|
|
266
|
+
"agent": record["agent_name"],
|
|
267
|
+
"content": f"Simulated output for: {record['task_text'][:100]}",
|
|
268
|
+
"produced_at": datetime.now(timezone.utc).isoformat(),
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
with _lock:
|
|
272
|
+
# Check for cancellation mid-execution
|
|
273
|
+
if record["status"] == "cancelled":
|
|
274
|
+
return
|
|
275
|
+
record["artifacts"].append(artifact)
|
|
276
|
+
record["status"] = "completed"
|
|
277
|
+
record["completed_at"] = datetime.now(timezone.utc).isoformat()
|
|
278
|
+
|
|
279
|
+
_persist_job(job_id, record)
|
|
280
|
+
|
|
281
|
+
except Exception as exc:
|
|
282
|
+
with _lock:
|
|
283
|
+
record["status"] = "failed"
|
|
284
|
+
record["error"] = str(exc)
|
|
285
|
+
record["completed_at"] = datetime.now(timezone.utc).isoformat()
|
|
286
|
+
_persist_job(job_id, record)
|
|
287
|
+
|
|
288
|
+
finally:
|
|
289
|
+
# Cleanup worktree if created
|
|
290
|
+
if worktree_dir:
|
|
291
|
+
_cleanup_worktree(worktree_dir)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def get_job_status(job_id: str) -> dict[str, Any]:
|
|
295
|
+
"""Get the current status and artifacts for a job.
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
job_id: Job identifier returned by submit_job().
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
Job record dict, or {"error": "not found"} if job doesn't exist.
|
|
302
|
+
"""
|
|
303
|
+
with _lock:
|
|
304
|
+
record = _jobs.get(job_id)
|
|
305
|
+
if record is not None:
|
|
306
|
+
return dict(record)
|
|
307
|
+
|
|
308
|
+
# Not in memory — try loading from disk
|
|
309
|
+
disk_record = _load_job_from_disk(job_id)
|
|
310
|
+
if disk_record is not None:
|
|
311
|
+
return disk_record
|
|
312
|
+
|
|
313
|
+
return {"error": "not found"}
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def cancel_job(job_id: str) -> bool:
|
|
317
|
+
"""Cancel a queued or running job.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
job_id: Job identifier to cancel.
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
True if the job was cancelled, False if not found or already terminal.
|
|
324
|
+
"""
|
|
325
|
+
with _lock:
|
|
326
|
+
record = _jobs.get(job_id)
|
|
327
|
+
if record is None:
|
|
328
|
+
return False
|
|
329
|
+
if record["status"] in ("completed", "failed", "cancelled"):
|
|
330
|
+
return False
|
|
331
|
+
record["status"] = "cancelled"
|
|
332
|
+
record["completed_at"] = datetime.now(timezone.utc).isoformat()
|
|
333
|
+
|
|
334
|
+
_persist_job(job_id, record)
|
|
335
|
+
return True
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def list_jobs(status_filter: str | None = None) -> list[dict[str, Any]]:
|
|
339
|
+
"""List all jobs, optionally filtered by status.
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
status_filter: If provided, only return jobs matching this status.
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
List of job record dicts.
|
|
346
|
+
"""
|
|
347
|
+
with _lock:
|
|
348
|
+
if status_filter is None:
|
|
349
|
+
return [dict(j) for j in _jobs.values()]
|
|
350
|
+
return [dict(j) for j in _jobs.values() if j["status"] == status_filter]
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def shutdown(wait: bool = True) -> None:
|
|
354
|
+
"""Shut down the executor gracefully.
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
wait: If True, wait for running jobs to complete before returning.
|
|
358
|
+
"""
|
|
359
|
+
global _executor
|
|
360
|
+
if _executor is not None:
|
|
361
|
+
_executor.shutdown(wait=wait)
|
|
362
|
+
_executor = None
|