secondshift 0.1.0b12__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.
- secondshift/__init__.py +1 -0
- secondshift/__main__.py +3 -0
- secondshift/cli.py +17 -0
- secondshift/config/__init__.py +0 -0
- secondshift/config/api.py +23 -0
- secondshift/config/app.py +62 -0
- secondshift/config/constants.py +279 -0
- secondshift/config/container_env.py +118 -0
- secondshift/config/first_run.py +40 -0
- secondshift/config/formatting.py +5 -0
- secondshift/config/gunicorn.py +31 -0
- secondshift/config/logging.py +40 -0
- secondshift/config/settings/__init__.py +0 -0
- secondshift/config/settings/base.py +524 -0
- secondshift/config/settings/dev.py +14 -0
- secondshift/config/settings/local.py +23 -0
- secondshift/config/settings/production.py +113 -0
- secondshift/config/settings/runner.py +140 -0
- secondshift/config/settings/test.py +47 -0
- secondshift/config/static_storage.py +45 -0
- secondshift/config/urls.py +42 -0
- secondshift/connections/__init__.py +0 -0
- secondshift/connections/access.py +27 -0
- secondshift/connections/admin.py +25 -0
- secondshift/connections/agents.py +224 -0
- secondshift/connections/api.py +747 -0
- secondshift/connections/apps.py +6 -0
- secondshift/connections/catalog.py +96 -0
- secondshift/connections/checks.py +22 -0
- secondshift/connections/dao.py +305 -0
- secondshift/connections/errors.py +13 -0
- secondshift/connections/execution.py +262 -0
- secondshift/connections/external.py +258 -0
- secondshift/connections/github.py +123 -0
- secondshift/connections/http_api/__init__.py +1 -0
- secondshift/connections/http_api/client.py +161 -0
- secondshift/connections/http_api/connections.py +187 -0
- secondshift/connections/http_api/spec.py +382 -0
- secondshift/connections/mcp/__init__.py +1 -0
- secondshift/connections/mcp/base.py +139 -0
- secondshift/connections/mcp/client.py +353 -0
- secondshift/connections/mcp/custom.py +86 -0
- secondshift/connections/mcp/github.py +256 -0
- secondshift/connections/mcp/oauth.py +352 -0
- secondshift/connections/mcp/providers.py +15 -0
- secondshift/connections/mcp/schemas.py +77 -0
- secondshift/connections/mcp/sentry.py +135 -0
- secondshift/connections/mcp/signin.py +206 -0
- secondshift/connections/migration_helpers.py +28 -0
- secondshift/connections/migrations/0001_initial.py +101 -0
- secondshift/connections/migrations/0002_alter_connection_kind.py +18 -0
- secondshift/connections/migrations/0003_require_approval.py +23 -0
- secondshift/connections/migrations/0004_new_connections_skip_approval.py +18 -0
- secondshift/connections/migrations/0005_agentconnection.py +36 -0
- secondshift/connections/migrations/0006_remove_is_default.py +23 -0
- secondshift/connections/migrations/0007_agent_last_check.py +18 -0
- secondshift/connections/migrations/0008_agentcredentialcleanup.py +25 -0
- secondshift/connections/migrations/0009_release_0_1_0b11.py +52 -0
- secondshift/connections/migrations/0010_connection_attribution.py +18 -0
- secondshift/connections/migrations/__init__.py +0 -0
- secondshift/connections/model_providers.py +441 -0
- secondshift/connections/models.py +246 -0
- secondshift/connections/outbound.py +317 -0
- secondshift/connections/repository_ref.py +19 -0
- secondshift/connections/schemas.py +313 -0
- secondshift/connections/serialization.py +100 -0
- secondshift/connections/services.py +94 -0
- secondshift/connections/tools.py +38 -0
- secondshift/connections/webhooks.py +107 -0
- secondshift/core/__init__.py +0 -0
- secondshift/core/adapters.py +38 -0
- secondshift/core/admin.py +5 -0
- secondshift/core/api.py +104 -0
- secondshift/core/apps.py +20 -0
- secondshift/core/launcher.py +311 -0
- secondshift/core/local_agents.py +90 -0
- secondshift/core/local_output.py +120 -0
- secondshift/core/management/__init__.py +0 -0
- secondshift/core/management/commands/__init__.py +0 -0
- secondshift/core/management/commands/bootstrap_dev.py +33 -0
- secondshift/core/management/commands/dev_reset_db.py +204 -0
- secondshift/core/management/commands/login.py +23 -0
- secondshift/core/management/commands/logs.py +58 -0
- secondshift/core/management/commands/status.py +46 -0
- secondshift/core/management/commands/up.py +244 -0
- secondshift/core/middleware.py +21 -0
- secondshift/core/migrations/__init__.py +0 -0
- secondshift/core/schemas.py +71 -0
- secondshift/core/signin.py +58 -0
- secondshift/core/static/core/account.css +149 -0
- secondshift/core/static/core/admin.css +135 -0
- secondshift/core/templatetags/__init__.py +0 -0
- secondshift/core/templatetags/accounts.py +20 -0
- secondshift/core/terminal.py +27 -0
- secondshift/core/views.py +10 -0
- secondshift/gateway/__init__.py +0 -0
- secondshift/gateway/apps.py +6 -0
- secondshift/gateway/dao.py +137 -0
- secondshift/gateway/metering.py +171 -0
- secondshift/gateway/migrations/0001_initial.py +54 -0
- secondshift/gateway/migrations/0002_modelusage_gateway_usage_created_at_idx.py +19 -0
- secondshift/gateway/migrations/__init__.py +0 -0
- secondshift/gateway/models.py +61 -0
- secondshift/gateway/pricing.py +39 -0
- secondshift/gateway/proxy.py +369 -0
- secondshift/gateway/tokens.py +68 -0
- secondshift/gateway/urls.py +17 -0
- secondshift/gateway/views.py +11 -0
- secondshift/github/__init__.py +6 -0
- secondshift/github/app.py +109 -0
- secondshift/github/authors.py +56 -0
- secondshift/github/credentials.py +59 -0
- secondshift/github/errors.py +2 -0
- secondshift/github/git.py +56 -0
- secondshift/github/identity.py +35 -0
- secondshift/github/installations.py +74 -0
- secondshift/github/issues.py +75 -0
- secondshift/github/permissions.py +49 -0
- secondshift/github/pulls.py +49 -0
- secondshift/github/repos.py +52 -0
- secondshift/github/rest.py +93 -0
- secondshift/github/ruff.toml +16 -0
- secondshift/github/tokens.py +52 -0
- secondshift/jobs/__init__.py +0 -0
- secondshift/jobs/access.py +12 -0
- secondshift/jobs/admin.py +83 -0
- secondshift/jobs/api.py +184 -0
- secondshift/jobs/apps.py +6 -0
- secondshift/jobs/automatic_grants.py +115 -0
- secondshift/jobs/autosync.py +78 -0
- secondshift/jobs/capabilities.py +43 -0
- secondshift/jobs/dao.py +258 -0
- secondshift/jobs/drafts.py +112 -0
- secondshift/jobs/grants.py +116 -0
- secondshift/jobs/limits.py +26 -0
- secondshift/jobs/management/__init__.py +0 -0
- secondshift/jobs/management/commands/__init__.py +0 -0
- secondshift/jobs/manifest.py +364 -0
- secondshift/jobs/migrations/0001_initial.py +87 -0
- secondshift/jobs/migrations/0002_job_history.py +18 -0
- secondshift/jobs/migrations/0003_job_skill_text.py +23 -0
- secondshift/jobs/migrations/0004_jobgrant_tool_rules.py +18 -0
- secondshift/jobs/migrations/0005_job_triggers.py +18 -0
- secondshift/jobs/migrations/0006_job_webhook.py +18 -0
- secondshift/jobs/migrations/0007_job_deliveries.py +23 -0
- secondshift/jobs/migrations/0008_job_removed_at.py +18 -0
- secondshift/jobs/migrations/0009_job_subagents.py +18 -0
- secondshift/jobs/migrations/0010_job_skill_source.py +18 -0
- secondshift/jobs/migrations/__init__.py +0 -0
- secondshift/jobs/models.py +199 -0
- secondshift/jobs/needs.py +154 -0
- secondshift/jobs/plan.py +176 -0
- secondshift/jobs/planning.py +386 -0
- secondshift/jobs/readiness.py +46 -0
- secondshift/jobs/repository_sync.py +199 -0
- secondshift/jobs/scheduling.py +26 -0
- secondshift/jobs/schemas.py +207 -0
- secondshift/jobs/serialization.py +218 -0
- secondshift/jobs/skill_needs.py +189 -0
- secondshift/jobs/sync.py +252 -0
- secondshift/jobs/tool_policy.py +82 -0
- secondshift/jobs/tool_rules.py +200 -0
- secondshift/mcp_server/__init__.py +6 -0
- secondshift/mcp_server/api.py +79 -0
- secondshift/mcp_server/apps.py +7 -0
- secondshift/mcp_server/audit.py +22 -0
- secondshift/mcp_server/core.py +12 -0
- secondshift/mcp_server/dao.py +23 -0
- secondshift/mcp_server/migrations/0001_initial.py +33 -0
- secondshift/mcp_server/migrations/__init__.py +0 -0
- secondshift/mcp_server/models.py +40 -0
- secondshift/mcp_server/schemas.py +204 -0
- secondshift/mcp_server/tools/__init__.py +9 -0
- secondshift/mcp_server/tools/jobs.py +106 -0
- secondshift/mcp_server/tools/manifests.py +127 -0
- secondshift/mcp_server/tools/repositories.py +42 -0
- secondshift/mcp_server/tools/runs.py +86 -0
- secondshift/mcp_server/tools/tasks.py +103 -0
- secondshift/mcp_server/tools_registry.py +81 -0
- secondshift/repositories/__init__.py +0 -0
- secondshift/repositories/access.py +13 -0
- secondshift/repositories/admin.py +19 -0
- secondshift/repositories/api.py +296 -0
- secondshift/repositories/apps.py +6 -0
- secondshift/repositories/dao.py +114 -0
- secondshift/repositories/external_skills.py +224 -0
- secondshift/repositories/github.py +27 -0
- secondshift/repositories/github_access.py +57 -0
- secondshift/repositories/github_check.py +279 -0
- secondshift/repositories/github_sync.py +181 -0
- secondshift/repositories/github_views.py +90 -0
- secondshift/repositories/local.py +198 -0
- secondshift/repositories/management/__init__.py +0 -0
- secondshift/repositories/management/commands/__init__.py +0 -0
- secondshift/repositories/management/commands/prune_source_cache.py +15 -0
- secondshift/repositories/migrations/0001_initial.py +45 -0
- secondshift/repositories/migrations/0002_repository_last_sync_error.py +18 -0
- secondshift/repositories/migrations/0003_repository_checked_sha.py +18 -0
- secondshift/repositories/migrations/0004_pull_request_settings.py +23 -0
- secondshift/repositories/migrations/0005_remove_repository_installation_id.py +17 -0
- secondshift/repositories/migrations/0006_allow_issues.py +18 -0
- secondshift/repositories/migrations/__init__.py +0 -0
- secondshift/repositories/mirrors.py +50 -0
- secondshift/repositories/models.py +104 -0
- secondshift/repositories/pull_requests.py +199 -0
- secondshift/repositories/schemas.py +102 -0
- secondshift/repositories/serialization.py +133 -0
- secondshift/repositories/skill_source.py +64 -0
- secondshift/repositories/snapshots.py +238 -0
- secondshift/repositories/sources.py +256 -0
- secondshift/repositories/status.py +67 -0
- secondshift/runner/README.md +65 -0
- secondshift/runner/__init__.py +1 -0
- secondshift/runner/agent_credentials.py +89 -0
- secondshift/runner/agent_image/Dockerfile +16 -0
- secondshift/runner/agent_image/agent.py +132 -0
- secondshift/runner/agent_image/claude.py +254 -0
- secondshift/runner/agent_image/codex.py +84 -0
- secondshift/runner/agent_image/outcomes.py +120 -0
- secondshift/runner/agent_image/subagent_hook.py +71 -0
- secondshift/runner/agent_protocol.py +41 -0
- secondshift/runner/agents.py +254 -0
- secondshift/runner/api.py +123 -0
- secondshift/runner/apps.py +6 -0
- secondshift/runner/client.py +168 -0
- secondshift/runner/collection.py +276 -0
- secondshift/runner/dao.py +64 -0
- secondshift/runner/dev.py +19 -0
- secondshift/runner/images.py +57 -0
- secondshift/runner/migrations/0001_initial.py +48 -0
- secondshift/runner/migrations/0002_session_source_sha256.py +18 -0
- secondshift/runner/migrations/0003_session_collect_changes.py +18 -0
- secondshift/runner/migrations/0004_session_history_sha256.py +18 -0
- secondshift/runner/migrations/0005_require_source_sha256.py +18 -0
- secondshift/runner/migrations/0006_session_skill_sha256.py +18 -0
- secondshift/runner/migrations/__init__.py +0 -0
- secondshift/runner/models.py +53 -0
- secondshift/runner/protocol.py +97 -0
- secondshift/runner/schemas.py +36 -0
- secondshift/runner/services.py +465 -0
- secondshift/runner/sources.py +130 -0
- secondshift/runner/urls.py +7 -0
- secondshift/runs/__init__.py +0 -0
- secondshift/runs/access.py +13 -0
- secondshift/runs/admin.py +89 -0
- secondshift/runs/api.py +211 -0
- secondshift/runs/apps.py +11 -0
- secondshift/runs/artifacts.py +261 -0
- secondshift/runs/dao.py +640 -0
- secondshift/runs/deliveries.py +80 -0
- secondshift/runs/execution/__init__.py +0 -0
- secondshift/runs/execution/access_needs.py +89 -0
- secondshift/runs/execution/agent.py +91 -0
- secondshift/runs/execution/agent_broker.py +198 -0
- secondshift/runs/execution/budgets.py +177 -0
- secondshift/runs/execution/cli_agent.py +167 -0
- secondshift/runs/execution/consent.py +58 -0
- secondshift/runs/execution/deadlines.py +47 -0
- secondshift/runs/execution/external_tools.py +84 -0
- secondshift/runs/execution/issues.py +99 -0
- secondshift/runs/execution/model_gateway.py +90 -0
- secondshift/runs/execution/payload.py +63 -0
- secondshift/runs/execution/result.py +59 -0
- secondshift/runs/execution/run.py +345 -0
- secondshift/runs/execution/subagents.py +246 -0
- secondshift/runs/execution/tool_broker.py +123 -0
- secondshift/runs/execution/workspace_tools.py +248 -0
- secondshift/runs/history.py +182 -0
- secondshift/runs/inputs.py +49 -0
- secondshift/runs/issue_delivery.py +144 -0
- secondshift/runs/management/__init__.py +0 -0
- secondshift/runs/management/commands/__init__.py +0 -0
- secondshift/runs/management/commands/prune_runs.py +12 -0
- secondshift/runs/management/commands/run_dbos_worker.py +129 -0
- secondshift/runs/migration_helpers.py +313 -0
- secondshift/runs/migrations/0001_initial.py +69 -0
- secondshift/runs/migrations/0002_run_started_by.py +18 -0
- secondshift/runs/migrations/0003_agentlease.py +25 -0
- secondshift/runs/migrations/0004_task_assignment.py +39 -0
- secondshift/runs/migrations/0005_backfill_tasks.py +19 -0
- secondshift/runs/migrations/0006_standalone_tasks.py +50 -0
- secondshift/runs/migrations/0007_run_delivery.py +35 -0
- secondshift/runs/migrations/0008_run_outputs.py +18 -0
- secondshift/runs/migrations/0009_pull_request_delivery.py +28 -0
- secondshift/runs/migrations/0010_run_issue.py +32 -0
- secondshift/runs/migrations/0011_issues_delivery.py +23 -0
- secondshift/runs/migrations/0012_alter_rundelivery_status.py +18 -0
- secondshift/runs/migrations/0013_release_0_1_0b11.py +113 -0
- secondshift/runs/migrations/0014_run_access_need.py +30 -0
- secondshift/runs/migrations/__init__.py +0 -0
- secondshift/runs/models.py +340 -0
- secondshift/runs/orchestration/__init__.py +0 -0
- secondshift/runs/orchestration/admission.py +15 -0
- secondshift/runs/orchestration/dbos_runtime.py +43 -0
- secondshift/runs/orchestration/dispatch.py +130 -0
- secondshift/runs/orchestration/periodic.py +39 -0
- secondshift/runs/orchestration/reconciliation.py +98 -0
- secondshift/runs/orchestration/schedules.py +51 -0
- secondshift/runs/orchestration/workflows.py +480 -0
- secondshift/runs/outputs.py +52 -0
- secondshift/runs/publishing.py +162 -0
- secondshift/runs/queue.py +26 -0
- secondshift/runs/reporting/__init__.py +0 -0
- secondshift/runs/reporting/collection.py +26 -0
- secondshift/runs/reporting/connection_usage.py +45 -0
- secondshift/runs/reporting/failures.py +150 -0
- secondshift/runs/reporting/health.py +158 -0
- secondshift/runs/reporting/notifications.py +258 -0
- secondshift/runs/reporting/queries.py +60 -0
- secondshift/runs/retention.py +35 -0
- secondshift/runs/schemas.py +192 -0
- secondshift/runs/serialization.py +245 -0
- secondshift/runs/services.py +176 -0
- secondshift/runs/signals.py +25 -0
- secondshift/runs/submissions.py +236 -0
- secondshift/runs/transitions.py +84 -0
- secondshift/templates/account/password_reset_help_text.html +1 -0
- secondshift/templates/admin/base_site.html +24 -0
- secondshift/templates/allauth/elements/provider.html +9 -0
- secondshift/templates/allauth/layouts/base.html +67 -0
- secondshift/templates/socialaccount/snippets/login.html +9 -0
- secondshift/templates/workspaces/invite_unusable.html +10 -0
- secondshift/triggers/__init__.py +0 -0
- secondshift/triggers/admin.py +19 -0
- secondshift/triggers/api.py +116 -0
- secondshift/triggers/apps.py +6 -0
- secondshift/triggers/dao.py +66 -0
- secondshift/triggers/filters.py +48 -0
- secondshift/triggers/migrations/0001_initial.py +55 -0
- secondshift/triggers/migrations/0002_ignored_events_and_hourly_limit.py +23 -0
- secondshift/triggers/migrations/__init__.py +0 -0
- secondshift/triggers/models.py +83 -0
- secondshift/triggers/schemas.py +58 -0
- secondshift/triggers/serialization.py +91 -0
- secondshift/triggers/services.py +246 -0
- secondshift/triggers/verification.py +84 -0
- secondshift/web/assets/index-Bwy0hvk_.js +2419 -0
- secondshift/web/assets/index-D27gB9_L.css +1 -0
- secondshift/web/favicon.svg +19 -0
- secondshift/web/index.html +16 -0
- secondshift/web/secondshift-mark.svg +16 -0
- secondshift/workspaces/__init__.py +0 -0
- secondshift/workspaces/access.py +181 -0
- secondshift/workspaces/admin.py +37 -0
- secondshift/workspaces/api.py +264 -0
- secondshift/workspaces/apps.py +6 -0
- secondshift/workspaces/context_processors.py +11 -0
- secondshift/workspaces/dao.py +281 -0
- secondshift/workspaces/invites.py +150 -0
- secondshift/workspaces/limits.py +197 -0
- secondshift/workspaces/management/__init__.py +0 -0
- secondshift/workspaces/management/commands/__init__.py +0 -0
- secondshift/workspaces/management/commands/create_workspace.py +39 -0
- secondshift/workspaces/migrations/0001_initial.py +67 -0
- secondshift/workspaces/migrations/0002_api_token.py +34 -0
- secondshift/workspaces/migrations/0003_execution_default.py +29 -0
- secondshift/workspaces/migrations/0004_copy_default_model_key.py +26 -0
- secondshift/workspaces/migrations/__init__.py +0 -0
- secondshift/workspaces/models.py +169 -0
- secondshift/workspaces/owners.py +58 -0
- secondshift/workspaces/schemas.py +149 -0
- secondshift/workspaces/serialization.py +138 -0
- secondshift/workspaces/server_settings.py +140 -0
- secondshift/workspaces/services.py +94 -0
- secondshift/workspaces/tokens.py +108 -0
- secondshift/workspaces/views.py +32 -0
- secondshift-0.1.0b12.dist-info/METADATA +270 -0
- secondshift-0.1.0b12.dist-info/RECORD +371 -0
- secondshift-0.1.0b12.dist-info/WHEEL +4 -0
- secondshift-0.1.0b12.dist-info/entry_points.txt +2 -0
- secondshift-0.1.0b12.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import json
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path, PurePosixPath
|
|
5
|
+
from typing import Annotated, ClassVar, Self
|
|
6
|
+
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
from croniter import croniter
|
|
10
|
+
from django.conf import settings
|
|
11
|
+
from pydantic import (
|
|
12
|
+
BaseModel,
|
|
13
|
+
ConfigDict,
|
|
14
|
+
Field,
|
|
15
|
+
ValidationError,
|
|
16
|
+
field_validator,
|
|
17
|
+
model_validator,
|
|
18
|
+
)
|
|
19
|
+
from pydantic_core import ErrorDetails
|
|
20
|
+
|
|
21
|
+
from secondshift.config.constants import (
|
|
22
|
+
COMMIT_SHA_PATTERN,
|
|
23
|
+
GITHUB_REPOSITORY_PATTERN,
|
|
24
|
+
MAX_RUN_ISSUES,
|
|
25
|
+
TriggerKind,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CapabilityManifest(BaseModel):
|
|
30
|
+
model_config = ConfigDict(extra="forbid")
|
|
31
|
+
|
|
32
|
+
binding: str = Field(pattern=r"^[a-z0-9]+(?:-[a-z0-9]+)*$", max_length=120)
|
|
33
|
+
tools: list[str] = Field(min_length=1)
|
|
34
|
+
required: bool = True
|
|
35
|
+
|
|
36
|
+
@field_validator("tools")
|
|
37
|
+
@classmethod
|
|
38
|
+
def validate_tools(cls, values: list[str]) -> list[str]:
|
|
39
|
+
if any(not value.strip() for value in values):
|
|
40
|
+
raise ValueError("tool names cannot be empty")
|
|
41
|
+
if len(values) != len(set(values)):
|
|
42
|
+
raise ValueError("tool names must be unique within a binding")
|
|
43
|
+
return values
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class RequirementsManifest(BaseModel):
|
|
47
|
+
model_config = ConfigDict(extra="forbid")
|
|
48
|
+
|
|
49
|
+
mcps: list[CapabilityManifest] = Field(default_factory=list)
|
|
50
|
+
apis: list[CapabilityManifest] = Field(default_factory=list)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class NotificationManifest(BaseModel):
|
|
54
|
+
model_config = ConfigDict(extra="forbid")
|
|
55
|
+
|
|
56
|
+
binding: str = Field(pattern=r"^[a-z0-9]+(?:-[a-z0-9]+)*$", max_length=120)
|
|
57
|
+
required: bool = True
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# A dotted path to a field in a JSON body, such as "status" or "labels.team".
|
|
61
|
+
FieldPath = Annotated[
|
|
62
|
+
str,
|
|
63
|
+
Field(pattern=r"^[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*$", max_length=200),
|
|
64
|
+
]
|
|
65
|
+
DURATION = re.compile(r"^(\d+)([mhd])$")
|
|
66
|
+
DURATION_UNITS = {"m": 60, "h": 3600, "d": 86400}
|
|
67
|
+
REPEAT_WINDOW_MAX_SECONDS = 7 * 86400
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def duration_seconds(value: str) -> int:
|
|
71
|
+
"""Seconds in a duration such as "30m", "1h" or "2d"."""
|
|
72
|
+
match = DURATION.match(value)
|
|
73
|
+
if match is None:
|
|
74
|
+
raise ValueError("must be a number followed by m, h or d, such as 1h")
|
|
75
|
+
return int(match.group(1)) * DURATION_UNITS[match.group(2)]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# Used when a job sets repeat_key without repeat_window.
|
|
79
|
+
REPEAT_WINDOW_DEFAULT = "1h"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class WebhookManifest(BaseModel):
|
|
83
|
+
"""Which webhook requests start a run. Field names are the sender's."""
|
|
84
|
+
|
|
85
|
+
model_config = ConfigDict(extra="forbid")
|
|
86
|
+
|
|
87
|
+
# Skip a request when every listed field has the listed value.
|
|
88
|
+
skip_when: dict[FieldPath, str | int | float | bool] = Field(default_factory=dict)
|
|
89
|
+
# Requests with the same value in this field are the same event.
|
|
90
|
+
repeat_key: FieldPath | None = None
|
|
91
|
+
# How long after its run a repeat is still skipped.
|
|
92
|
+
repeat_window: str | None = None
|
|
93
|
+
|
|
94
|
+
@field_validator("repeat_window")
|
|
95
|
+
@classmethod
|
|
96
|
+
def validate_repeat_window(cls, value: str | None) -> str | None:
|
|
97
|
+
if value is None:
|
|
98
|
+
return value
|
|
99
|
+
seconds = duration_seconds(value)
|
|
100
|
+
if not 0 < seconds <= REPEAT_WINDOW_MAX_SECONDS:
|
|
101
|
+
raise ValueError("must be between 1m and 7d")
|
|
102
|
+
return value
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def window(self) -> str:
|
|
106
|
+
"""How long repeats are skipped, as written in the manifest."""
|
|
107
|
+
return self.repeat_window or REPEAT_WINDOW_DEFAULT
|
|
108
|
+
|
|
109
|
+
@model_validator(mode="after")
|
|
110
|
+
def validate_window_has_key(self) -> Self:
|
|
111
|
+
if self.repeat_window is not None and self.repeat_key is None:
|
|
112
|
+
raise ValueError("repeat_window needs a repeat_key")
|
|
113
|
+
return self
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class SkillSourceManifest(BaseModel):
|
|
117
|
+
"""Another public GitHub repository that holds the job's skill.
|
|
118
|
+
|
|
119
|
+
The job's `skill` is then a path in that repository. Only the skill's
|
|
120
|
+
folder reaches the run, read-only at /skill.
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
model_config = ConfigDict(extra="forbid")
|
|
124
|
+
|
|
125
|
+
repository: str = Field(pattern=rf"^{GITHUB_REPOSITORY_PATTERN}$", max_length=200)
|
|
126
|
+
# A job pins a full commit SHA. A one-off task may name a branch or tag,
|
|
127
|
+
# or leave it out for the default branch.
|
|
128
|
+
ref: str | None = Field(default=None, min_length=1, max_length=200)
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def pinned(self) -> bool:
|
|
132
|
+
return self.ref is not None and bool(re.fullmatch(COMMIT_SHA_PATTERN, self.ref))
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
IssueLabel = Annotated[
|
|
136
|
+
str, Field(min_length=1, max_length=50, pattern=r"^\S(?:.*\S)?$")
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class OpenIssuesManifest(BaseModel):
|
|
141
|
+
"""Where SecondShift opens the issues a job's agent proposes, and how many."""
|
|
142
|
+
|
|
143
|
+
model_config = ConfigDict(extra="forbid")
|
|
144
|
+
|
|
145
|
+
# Added to every issue. The agent cannot choose labels.
|
|
146
|
+
labels: list[IssueLabel] = Field(default_factory=list, max_length=10)
|
|
147
|
+
max: int = Field(default=MAX_RUN_ISSUES, ge=1, le=MAX_RUN_ISSUES)
|
|
148
|
+
|
|
149
|
+
@field_validator("labels")
|
|
150
|
+
@classmethod
|
|
151
|
+
def distinct_labels(cls, value: list[str]) -> list[str]:
|
|
152
|
+
if len({label.casefold() for label in value}) != len(value):
|
|
153
|
+
raise ValueError("list each label once")
|
|
154
|
+
return value
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class JobManifest(BaseModel):
|
|
158
|
+
model_config = ConfigDict(extra="forbid")
|
|
159
|
+
# A saved job runs only a skill someone reviewed, so its source is pinned.
|
|
160
|
+
pinned_sources: ClassVar[bool] = True
|
|
161
|
+
|
|
162
|
+
name: str = Field(min_length=1, max_length=200)
|
|
163
|
+
skill: str = Field(min_length=1, max_length=500)
|
|
164
|
+
# Where the skill comes from, when it is not in this repository.
|
|
165
|
+
skill_source: SkillSourceManifest | None = None
|
|
166
|
+
# Without a schedule, the job runs only when started with Run now.
|
|
167
|
+
schedule: str | None = Field(default=None, min_length=1, max_length=100)
|
|
168
|
+
timezone: str = "UTC"
|
|
169
|
+
# How many of the job's own earlier reports each run starts with.
|
|
170
|
+
history: int = Field(default=0, ge=0)
|
|
171
|
+
# How many sub-agents one run may start. They share the run's limits.
|
|
172
|
+
subagents: int = Field(default=0, ge=0)
|
|
173
|
+
requires: RequirementsManifest = Field(default_factory=RequirementsManifest)
|
|
174
|
+
notify: list[NotificationManifest] = Field(default_factory=list)
|
|
175
|
+
# Events that may start the job. An owner still turns each one on.
|
|
176
|
+
triggers: list[TriggerKind] = Field(default_factory=list)
|
|
177
|
+
webhook: WebhookManifest | None = None
|
|
178
|
+
# Open a draft pull request with the agent's changes. An owner still
|
|
179
|
+
# allows pull requests for the repository.
|
|
180
|
+
open_pull_request: bool = False
|
|
181
|
+
# Open the issues the agent proposes. true uses the defaults. An owner
|
|
182
|
+
# still allows issues for the repository.
|
|
183
|
+
open_issues: OpenIssuesManifest | None = None
|
|
184
|
+
|
|
185
|
+
@field_validator("open_issues", mode="before")
|
|
186
|
+
@classmethod
|
|
187
|
+
def open_issues_short_form(cls, value: object) -> object:
|
|
188
|
+
if value is True:
|
|
189
|
+
return {}
|
|
190
|
+
if value is False:
|
|
191
|
+
return None
|
|
192
|
+
return value
|
|
193
|
+
|
|
194
|
+
@field_validator("skill")
|
|
195
|
+
@classmethod
|
|
196
|
+
def validate_skill_path(cls, value: str) -> str:
|
|
197
|
+
path = PurePosixPath(value)
|
|
198
|
+
if path.is_absolute() or ".." in path.parts:
|
|
199
|
+
raise ValueError("skill must be a repository-relative path")
|
|
200
|
+
if path.name != "SKILL.md":
|
|
201
|
+
raise ValueError("skill must point to a SKILL.md file")
|
|
202
|
+
return value
|
|
203
|
+
|
|
204
|
+
@field_validator("triggers")
|
|
205
|
+
@classmethod
|
|
206
|
+
def validate_triggers(cls, values: list[TriggerKind]) -> list[TriggerKind]:
|
|
207
|
+
if len(values) != len(set(values)):
|
|
208
|
+
raise ValueError("triggers must be unique within a job")
|
|
209
|
+
return values
|
|
210
|
+
|
|
211
|
+
@field_validator("history")
|
|
212
|
+
@classmethod
|
|
213
|
+
def validate_history(cls, value: int) -> int:
|
|
214
|
+
if value > settings.JOB_HISTORY_MAX:
|
|
215
|
+
raise ValueError(
|
|
216
|
+
f"history cannot be above {settings.JOB_HISTORY_MAX} on this server"
|
|
217
|
+
)
|
|
218
|
+
return value
|
|
219
|
+
|
|
220
|
+
@field_validator("subagents")
|
|
221
|
+
@classmethod
|
|
222
|
+
def validate_subagents(cls, value: int) -> int:
|
|
223
|
+
if value > settings.RUN_SUBAGENTS_MAX:
|
|
224
|
+
raise ValueError(
|
|
225
|
+
f"subagents cannot be above {settings.RUN_SUBAGENTS_MAX} on this server"
|
|
226
|
+
)
|
|
227
|
+
return value
|
|
228
|
+
|
|
229
|
+
@field_validator("schedule")
|
|
230
|
+
@classmethod
|
|
231
|
+
def validate_schedule(cls, value: str | None) -> str | None:
|
|
232
|
+
if value is not None and not croniter.is_valid(value):
|
|
233
|
+
raise ValueError("schedule must be a valid five-field cron expression")
|
|
234
|
+
return value
|
|
235
|
+
|
|
236
|
+
@field_validator("timezone")
|
|
237
|
+
@classmethod
|
|
238
|
+
def validate_timezone(cls, value: str) -> str:
|
|
239
|
+
try:
|
|
240
|
+
ZoneInfo(value)
|
|
241
|
+
except ZoneInfoNotFoundError as error:
|
|
242
|
+
raise ValueError("timezone must be an IANA timezone") from error
|
|
243
|
+
return value
|
|
244
|
+
|
|
245
|
+
@model_validator(mode="after")
|
|
246
|
+
def validate_skill_source(self) -> Self:
|
|
247
|
+
if self.skill_source is None:
|
|
248
|
+
return self
|
|
249
|
+
if self.skill is None:
|
|
250
|
+
raise ValueError("skill_source needs a skill path in that repository")
|
|
251
|
+
if self.pinned_sources and not self.skill_source.pinned:
|
|
252
|
+
raise ValueError(
|
|
253
|
+
"skill_source.ref must be a full commit SHA, so a sync never "
|
|
254
|
+
"runs a skill nobody reviewed"
|
|
255
|
+
)
|
|
256
|
+
return self
|
|
257
|
+
|
|
258
|
+
@model_validator(mode="after")
|
|
259
|
+
def validate_webhook_is_a_trigger(self) -> Self:
|
|
260
|
+
if self.webhook is not None and TriggerKind.WEBHOOK not in self.triggers:
|
|
261
|
+
raise ValueError("webhook settings need triggers: [webhook]")
|
|
262
|
+
return self
|
|
263
|
+
|
|
264
|
+
@model_validator(mode="after")
|
|
265
|
+
def validate_unique_bindings(self) -> Self:
|
|
266
|
+
bindings = [
|
|
267
|
+
item.binding
|
|
268
|
+
for item in [*self.requires.mcps, *self.requires.apis, *self.notify]
|
|
269
|
+
]
|
|
270
|
+
if len(bindings) != len(set(bindings)):
|
|
271
|
+
raise ValueError("bindings must be unique within a job")
|
|
272
|
+
return self
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
class TaskManifest(JobManifest):
|
|
276
|
+
"""One-off work may supply instructions instead of a committed skill."""
|
|
277
|
+
|
|
278
|
+
pinned_sources: ClassVar[bool] = False
|
|
279
|
+
|
|
280
|
+
skill: str | None = Field(default=None, min_length=1, max_length=500) # type: ignore[assignment]
|
|
281
|
+
|
|
282
|
+
@field_validator("skill")
|
|
283
|
+
@classmethod
|
|
284
|
+
def validate_skill_path(cls, value: str | None) -> str | None:
|
|
285
|
+
return JobManifest.validate_skill_path(value) if value is not None else None
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
class RepositoryManifest(BaseModel):
|
|
289
|
+
model_config = ConfigDict(extra="forbid")
|
|
290
|
+
|
|
291
|
+
version: int = 1
|
|
292
|
+
jobs: dict[str, JobManifest]
|
|
293
|
+
|
|
294
|
+
@field_validator("version")
|
|
295
|
+
@classmethod
|
|
296
|
+
def validate_version(cls, value: int) -> int:
|
|
297
|
+
if value != 1:
|
|
298
|
+
raise ValueError("only manifest version 1 is supported")
|
|
299
|
+
return value
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def load_manifest(path: Path) -> RepositoryManifest:
|
|
303
|
+
return parse_manifest(path.read_text())
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def parse_manifest(text: str) -> RepositoryManifest:
|
|
307
|
+
raw = yaml.safe_load(text)
|
|
308
|
+
if not isinstance(raw, dict):
|
|
309
|
+
raise ManifestError("The file must be a mapping.")
|
|
310
|
+
try:
|
|
311
|
+
return RepositoryManifest.model_validate(raw)
|
|
312
|
+
except ValidationError as error:
|
|
313
|
+
raise ManifestError(describe_errors(error)) from error
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
class ManifestError(ValueError):
|
|
317
|
+
"""A manifest problem, worded for the person who wrote the file."""
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def describe_errors(error: ValidationError) -> str:
|
|
321
|
+
unknown = [
|
|
322
|
+
field_path(item) for item in error.errors() if item["type"] == "extra_forbidden"
|
|
323
|
+
]
|
|
324
|
+
problems = [
|
|
325
|
+
describe_error(item)
|
|
326
|
+
for item in error.errors()
|
|
327
|
+
if item["type"] != "extra_forbidden"
|
|
328
|
+
]
|
|
329
|
+
if unknown:
|
|
330
|
+
label = "Unknown field" if len(unknown) == 1 else "Unknown fields"
|
|
331
|
+
problems.append(
|
|
332
|
+
f"{label}: {', '.join(unknown)}. Check the spelling, or update "
|
|
333
|
+
"SecondShift if the file was written for a newer version."
|
|
334
|
+
)
|
|
335
|
+
return " ".join(problems)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def describe_error(item: ErrorDetails) -> str:
|
|
339
|
+
path = field_path(item)
|
|
340
|
+
if item["type"] == "missing":
|
|
341
|
+
return f"{path} is required."
|
|
342
|
+
message = item["msg"].removeprefix("Value error, ").rstrip(".")
|
|
343
|
+
message = message[:1].lower() + message[1:]
|
|
344
|
+
return f"{path}: {message}." if path else f"{message[:1].upper()}{message[1:]}."
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def field_path(item: ErrorDetails) -> str:
|
|
348
|
+
return ".".join(str(part) for part in item["loc"])
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def skill_key(job: JobManifest) -> str:
|
|
352
|
+
"""Names the job's skill file among those a sync reads."""
|
|
353
|
+
if source := job.skill_source:
|
|
354
|
+
return f"{source.repository}@{source.ref}:{job.skill}"
|
|
355
|
+
return job.skill
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def manifest_hash(job: JobManifest) -> str:
|
|
359
|
+
data = job.model_dump()
|
|
360
|
+
if data["skill_source"] is None:
|
|
361
|
+
# Jobs written before skill sources keep the hash they had.
|
|
362
|
+
del data["skill_source"]
|
|
363
|
+
canonical = json.dumps(data, sort_keys=True, separators=(",", ":"))
|
|
364
|
+
return hashlib.sha256(canonical.encode()).hexdigest()
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Generated by Django 6.0.9 on 2026-10-26 14:22
|
|
2
|
+
|
|
3
|
+
import django.db.models.deletion
|
|
4
|
+
from django.conf import settings
|
|
5
|
+
from django.db import migrations, models
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Migration(migrations.Migration):
|
|
9
|
+
|
|
10
|
+
initial = True
|
|
11
|
+
|
|
12
|
+
dependencies = [
|
|
13
|
+
('connections', '0001_initial'),
|
|
14
|
+
('repositories', '0001_initial'),
|
|
15
|
+
('workspaces', '0001_initial'),
|
|
16
|
+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
operations = [
|
|
20
|
+
migrations.CreateModel(
|
|
21
|
+
name='Job',
|
|
22
|
+
fields=[
|
|
23
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
24
|
+
('max_turns', models.PositiveIntegerField(blank=True, null=True)),
|
|
25
|
+
('max_tool_calls', models.PositiveIntegerField(blank=True, null=True)),
|
|
26
|
+
('token_budget', models.PositiveBigIntegerField(blank=True, null=True)),
|
|
27
|
+
('timeout_seconds', models.PositiveIntegerField(blank=True, null=True)),
|
|
28
|
+
('key', models.SlugField(max_length=120)),
|
|
29
|
+
('name', models.CharField(max_length=200)),
|
|
30
|
+
('skill_path', models.CharField(max_length=500)),
|
|
31
|
+
('cron', models.CharField(blank=True, default='', max_length=100)),
|
|
32
|
+
('timezone', models.CharField(default='UTC', max_length=64)),
|
|
33
|
+
('enabled', models.BooleanField(default=False)),
|
|
34
|
+
('next_run_at', models.DateTimeField(blank=True, null=True)),
|
|
35
|
+
('config_hash', models.CharField(max_length=64)),
|
|
36
|
+
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
37
|
+
('updated_at', models.DateTimeField(auto_now=True)),
|
|
38
|
+
('repository', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='jobs', to='repositories.repository')),
|
|
39
|
+
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='jobs', to='workspaces.workspace')),
|
|
40
|
+
],
|
|
41
|
+
options={
|
|
42
|
+
'ordering': ['repository__full_name', 'name'],
|
|
43
|
+
'abstract': False,
|
|
44
|
+
},
|
|
45
|
+
),
|
|
46
|
+
migrations.CreateModel(
|
|
47
|
+
name='CapabilityRequest',
|
|
48
|
+
fields=[
|
|
49
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
50
|
+
('binding', models.SlugField(max_length=120)),
|
|
51
|
+
('kind', models.CharField(choices=[('mcp', 'MCP'), ('api', 'API'), ('notification', 'Notification')], max_length=24)),
|
|
52
|
+
('requested_tools', models.JSONField(blank=True, default=list)),
|
|
53
|
+
('required', models.BooleanField(default=True)),
|
|
54
|
+
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
55
|
+
('updated_at', models.DateTimeField(auto_now=True)),
|
|
56
|
+
('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='capability_requests', to='jobs.job')),
|
|
57
|
+
],
|
|
58
|
+
options={
|
|
59
|
+
'ordering': ['job', 'binding'],
|
|
60
|
+
},
|
|
61
|
+
),
|
|
62
|
+
migrations.CreateModel(
|
|
63
|
+
name='JobGrant',
|
|
64
|
+
fields=[
|
|
65
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
66
|
+
('status', models.CharField(choices=[('pending', 'Pending'), ('approved', 'Approved'), ('denied', 'Denied')], default='pending', max_length=16)),
|
|
67
|
+
('allowed_tools', models.JSONField(blank=True, default=list)),
|
|
68
|
+
('reviewed_at', models.DateTimeField(blank=True, null=True)),
|
|
69
|
+
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
70
|
+
('updated_at', models.DateTimeField(auto_now=True)),
|
|
71
|
+
('capability_request', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='grant', to='jobs.capabilityrequest')),
|
|
72
|
+
('connection', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='job_grants', to='connections.connection')),
|
|
73
|
+
('reviewed_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='reviewed_job_grants', to=settings.AUTH_USER_MODEL)),
|
|
74
|
+
],
|
|
75
|
+
options={
|
|
76
|
+
'ordering': ['capability_request__job', 'capability_request__binding'],
|
|
77
|
+
},
|
|
78
|
+
),
|
|
79
|
+
migrations.AddConstraint(
|
|
80
|
+
model_name='job',
|
|
81
|
+
constraint=models.UniqueConstraint(fields=('repository', 'key'), name='unique_repository_job_key'),
|
|
82
|
+
),
|
|
83
|
+
migrations.AddConstraint(
|
|
84
|
+
model_name='capabilityrequest',
|
|
85
|
+
constraint=models.UniqueConstraint(fields=('job', 'binding'), name='unique_job_capability_binding'),
|
|
86
|
+
),
|
|
87
|
+
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Generated by Django 6.0.9 on 2026-10-27 12:35
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
('jobs', '0001_initial'),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.AddField(
|
|
14
|
+
model_name='job',
|
|
15
|
+
name='history',
|
|
16
|
+
field=models.PositiveSmallIntegerField(default=0),
|
|
17
|
+
),
|
|
18
|
+
]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Generated by Django 6.0.9 on 2026-10-29 09:13
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
('jobs', '0002_job_history'),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.AddField(
|
|
14
|
+
model_name='job',
|
|
15
|
+
name='skill_text',
|
|
16
|
+
field=models.TextField(blank=True, default=''),
|
|
17
|
+
),
|
|
18
|
+
migrations.AddField(
|
|
19
|
+
model_name='job',
|
|
20
|
+
name='skill_truncated',
|
|
21
|
+
field=models.BooleanField(default=False),
|
|
22
|
+
),
|
|
23
|
+
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Generated by Django 6.0.9 on 2026-11-01 11:51
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
('jobs', '0003_job_skill_text'),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.AddField(
|
|
14
|
+
model_name='jobgrant',
|
|
15
|
+
name='tool_rules',
|
|
16
|
+
field=models.JSONField(blank=True, default=dict),
|
|
17
|
+
),
|
|
18
|
+
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Generated by Django 6.0.9 on 2026-11-01 17:20
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
('jobs', '0004_jobgrant_tool_rules'),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.AddField(
|
|
14
|
+
model_name='job',
|
|
15
|
+
name='triggers',
|
|
16
|
+
field=models.JSONField(blank=True, default=list),
|
|
17
|
+
),
|
|
18
|
+
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Generated by Django 6.0.9 on 2026-11-01 18:05
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
('jobs', '0005_job_triggers'),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.AddField(
|
|
14
|
+
model_name='job',
|
|
15
|
+
name='webhook',
|
|
16
|
+
field=models.JSONField(blank=True, default=dict),
|
|
17
|
+
),
|
|
18
|
+
]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Generated by Django 6.0.9 on 2026-11-04 11:50
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
('jobs', '0006_job_webhook'),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.AddField(
|
|
14
|
+
model_name='job',
|
|
15
|
+
name='open_issues',
|
|
16
|
+
field=models.JSONField(blank=True, null=True),
|
|
17
|
+
),
|
|
18
|
+
migrations.AddField(
|
|
19
|
+
model_name='job',
|
|
20
|
+
name='open_pull_request',
|
|
21
|
+
field=models.BooleanField(default=False),
|
|
22
|
+
),
|
|
23
|
+
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Generated by Django 6.0.9 on 2026-11-07 14:45
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
('jobs', '0007_job_deliveries'),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.AddField(
|
|
14
|
+
model_name='job',
|
|
15
|
+
name='removed_at',
|
|
16
|
+
field=models.DateTimeField(blank=True, null=True),
|
|
17
|
+
),
|
|
18
|
+
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Generated by Django 6.0.9 on 2026-11-07 16:01
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
('jobs', '0008_job_removed_at'),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.AddField(
|
|
14
|
+
model_name='job',
|
|
15
|
+
name='subagents',
|
|
16
|
+
field=models.PositiveSmallIntegerField(default=0),
|
|
17
|
+
),
|
|
18
|
+
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Generated by Django 6.0.9 on 2026-11-11 19:20
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
('jobs', '0009_job_subagents'),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.AddField(
|
|
14
|
+
model_name='job',
|
|
15
|
+
name='skill_source',
|
|
16
|
+
field=models.JSONField(blank=True, null=True),
|
|
17
|
+
),
|
|
18
|
+
]
|
|
File without changes
|