arkaos 2.20.1 → 2.22.0
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/VERSION +1 -1
- package/arka/SKILL.md +16 -1
- package/arka/skills/costs/SKILL.md +62 -0
- package/arka/skills/flow/SKILL.md +14 -0
- package/arka/skills/forge/SKILL.md +14 -0
- package/config/hooks/post-tool-use.ps1 +80 -0
- package/config/hooks/post-tool-use.sh +47 -0
- package/config/hooks/pre-tool-use.ps1 +81 -8
- package/config/hooks/pre-tool-use.sh +81 -7
- package/config/hooks/stop.ps1 +79 -0
- package/config/hooks/stop.sh +88 -0
- package/config/hooks/user-prompt-submit.ps1 +50 -0
- package/config/hooks/user-prompt-submit.sh +24 -0
- package/core/cognition/__pycache__/auto_documentor.cpython-313.pyc +0 -0
- package/core/cognition/auto_documentor.py +433 -0
- package/core/jobs/__init__.py +14 -3
- package/core/jobs/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/jobs/__pycache__/auto_doc_worker.cpython-313.pyc +0 -0
- package/core/jobs/auto_doc_worker.py +257 -0
- package/core/obsidian/__init__.py +30 -3
- package/core/obsidian/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/obsidian/__pycache__/cataloger.cpython-313.pyc +0 -0
- package/core/obsidian/__pycache__/relator.cpython-313.pyc +0 -0
- package/core/obsidian/__pycache__/taxonomy.cpython-313.pyc +0 -0
- package/core/obsidian/cataloger.py +251 -0
- package/core/obsidian/relator.py +241 -0
- package/core/obsidian/taxonomy.py +100 -0
- package/core/runtime/__init__.py +22 -1
- package/core/runtime/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/base.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/claude_code.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/codex_cli.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/cursor.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/gemini_cli.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/llm_cost_telemetry.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/llm_cost_telemetry_cli.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/llm_provider.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/pricing.cpython-313.pyc +0 -0
- package/core/runtime/base.py +30 -1
- package/core/runtime/claude_code.py +68 -0
- package/core/runtime/codex_cli.py +33 -0
- package/core/runtime/cursor.py +19 -0
- package/core/runtime/gemini_cli.py +33 -0
- package/core/runtime/llm_cost_telemetry.py +306 -0
- package/core/runtime/llm_cost_telemetry_cli.py +138 -0
- package/core/runtime/llm_provider.py +382 -0
- package/core/runtime/pricing.py +85 -0
- package/core/synapse/__init__.py +8 -1
- package/core/synapse/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/synapse/__pycache__/engine.cpython-313.pyc +0 -0
- package/core/synapse/__pycache__/kb_cache.cpython-313.pyc +0 -0
- package/core/synapse/__pycache__/layers.cpython-313.pyc +0 -0
- package/core/synapse/engine.py +15 -1
- package/core/synapse/kb_cache.py +125 -6
- package/core/synapse/layers.py +296 -0
- package/core/workflow/__pycache__/flow_enforcer.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/kb_first_decider.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/marker_cache.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/research_gate.cpython-313.pyc +0 -0
- package/core/workflow/flow_enforcer.py +14 -3
- package/core/workflow/kb_first_decider.py +63 -0
- package/core/workflow/marker_cache.py +147 -0
- package/core/workflow/research_gate.py +301 -0
- package/departments/brand/SKILL.md +14 -0
- package/departments/brand/skills/archetype-finder/SKILL.md +14 -0
- package/departments/brand/skills/colors/SKILL.md +14 -0
- package/departments/brand/skills/design-system/SKILL.md +14 -0
- package/departments/brand/skills/identity-system/SKILL.md +14 -0
- package/departments/brand/skills/logo-brief/SKILL.md +14 -0
- package/departments/brand/skills/mockup-generate/SKILL.md +14 -0
- package/departments/brand/skills/naming-evaluate/SKILL.md +14 -0
- package/departments/brand/skills/positioning-statement/SKILL.md +14 -0
- package/departments/brand/skills/primal-audit/SKILL.md +14 -0
- package/departments/brand/skills/ux-audit/SKILL.md +14 -0
- package/departments/brand/skills/voice-guide/SKILL.md +14 -0
- package/departments/brand/skills/wireframe/SKILL.md +14 -0
- package/departments/community/SKILL.md +14 -0
- package/departments/community/skills/ai-community/SKILL.md +14 -0
- package/departments/community/skills/betting-setup/SKILL.md +14 -0
- package/departments/community/skills/business-model/SKILL.md +14 -0
- package/departments/community/skills/content-calendar/SKILL.md +14 -0
- package/departments/community/skills/events-plan/SKILL.md +14 -0
- package/departments/community/skills/gamification-design/SKILL.md +14 -0
- package/departments/community/skills/growth-plan/SKILL.md +14 -0
- package/departments/community/skills/metrics-track/SKILL.md +14 -0
- package/departments/community/skills/moderation/SKILL.md +14 -0
- package/departments/community/skills/monetize-plan/SKILL.md +14 -0
- package/departments/community/skills/niche-setup/SKILL.md +14 -0
- package/departments/community/skills/onboarding-flow/SKILL.md +14 -0
- package/departments/community/skills/platform-select/SKILL.md +14 -0
- package/departments/content/SKILL.md +14 -0
- package/departments/content/skills/ai-workflow/SKILL.md +14 -0
- package/departments/content/skills/analytics/SKILL.md +14 -0
- package/departments/content/skills/calendar/SKILL.md +14 -0
- package/departments/content/skills/content-system/SKILL.md +14 -0
- package/departments/content/skills/monetization-plan/SKILL.md +14 -0
- package/departments/content/skills/newsletter-write/SKILL.md +14 -0
- package/departments/content/skills/platform-optimize/SKILL.md +14 -0
- package/departments/content/skills/repurpose-plan/SKILL.md +14 -0
- package/departments/content/skills/script-structure/SKILL.md +14 -0
- package/departments/content/skills/short-form/SKILL.md +14 -0
- package/departments/content/skills/thumbnail-package/SKILL.md +14 -0
- package/departments/content/skills/viral-design/SKILL.md +14 -0
- package/departments/content/skills/youtube-strategy/SKILL.md +14 -0
- package/departments/dev/SKILL.md +14 -0
- package/departments/dev/skills/ai-assisted-dev/SKILL.md +14 -0
- package/departments/dev/skills/architecture-design/SKILL.md +14 -0
- package/departments/dev/skills/code-review/SKILL.md +14 -0
- package/departments/dev/skills/db-design/SKILL.md +14 -0
- package/departments/dev/skills/ddd-model/SKILL.md +14 -0
- package/departments/dev/skills/demo-gif/SKILL.md +14 -0
- package/departments/dev/skills/deploy/SKILL.md +14 -0
- package/departments/dev/skills/devops-pipeline/SKILL.md +14 -0
- package/departments/dev/skills/docs/SKILL.md +14 -0
- package/departments/dev/skills/mcp/SKILL.md +14 -0
- package/departments/dev/skills/performance-audit/SKILL.md +14 -0
- package/departments/dev/skills/refactor-plan/SKILL.md +14 -0
- package/departments/dev/skills/research/SKILL.md +14 -0
- package/departments/dev/skills/security-compliance/SKILL.md +14 -0
- package/departments/dev/skills/stack-check/SKILL.md +14 -0
- package/departments/ecom/SKILL.md +14 -0
- package/departments/ecom/skills/analytics/SKILL.md +14 -0
- package/departments/ecom/skills/browse-competitor/SKILL.md +14 -0
- package/departments/ecom/skills/cart-recovery/SKILL.md +14 -0
- package/departments/ecom/skills/cro-optimize/SKILL.md +14 -0
- package/departments/ecom/skills/customer-journey/SKILL.md +14 -0
- package/departments/ecom/skills/fulfillment-plan/SKILL.md +14 -0
- package/departments/ecom/skills/marketplace-manage/SKILL.md +14 -0
- package/departments/ecom/skills/pricing-strategy/SKILL.md +14 -0
- package/departments/ecom/skills/product-launch/SKILL.md +14 -0
- package/departments/ecom/skills/rfm-segment/SKILL.md +14 -0
- package/departments/ecom/skills/social-commerce/SKILL.md +14 -0
- package/departments/ecom/skills/store-audit/SKILL.md +14 -0
- package/departments/ecom/skills/subscription-model/SKILL.md +14 -0
- package/departments/finance/SKILL.md +14 -0
- package/departments/finance/skills/budget-plan/SKILL.md +14 -0
- package/departments/finance/skills/cashflow-forecast/SKILL.md +14 -0
- package/departments/finance/skills/ciso-advisor/SKILL.md +14 -0
- package/departments/finance/skills/financial-model/SKILL.md +14 -0
- package/departments/finance/skills/pitch-deck/SKILL.md +14 -0
- package/departments/finance/skills/scenario-analysis/SKILL.md +14 -0
- package/departments/finance/skills/unit-economics/SKILL.md +14 -0
- package/departments/finance/skills/valuation-model/SKILL.md +14 -0
- package/departments/kb/SKILL.md +14 -0
- package/departments/kb/skills/ai-research/SKILL.md +14 -0
- package/departments/kb/skills/competitive-intel/SKILL.md +14 -0
- package/departments/kb/skills/knowledge-review/SKILL.md +14 -0
- package/departments/kb/skills/learn-content/SKILL.md +14 -0
- package/departments/kb/skills/moc-create/SKILL.md +14 -0
- package/departments/kb/skills/persona-build/SKILL.md +14 -0
- package/departments/kb/skills/research-plan/SKILL.md +14 -0
- package/departments/kb/skills/search-kb/SKILL.md +14 -0
- package/departments/kb/skills/source-evaluate/SKILL.md +14 -0
- package/departments/kb/skills/taxonomy-manage/SKILL.md +14 -0
- package/departments/kb/skills/write-as-persona/SKILL.md +14 -0
- package/departments/landing/SKILL.md +14 -0
- package/departments/landing/skills/ab-test/SKILL.md +14 -0
- package/departments/landing/skills/affiliate-bridge/SKILL.md +14 -0
- package/departments/landing/skills/awareness-diagnose/SKILL.md +14 -0
- package/departments/landing/skills/email-sequence/SKILL.md +14 -0
- package/departments/landing/skills/funnel-metrics/SKILL.md +14 -0
- package/departments/landing/skills/headline-write/SKILL.md +14 -0
- package/departments/landing/skills/launch-sequence/SKILL.md +14 -0
- package/departments/landing/skills/offer-create/SKILL.md +14 -0
- package/departments/landing/skills/optimize-page/SKILL.md +14 -0
- package/departments/landing/skills/page-architect/SKILL.md +14 -0
- package/departments/landing/skills/persuasion-apply/SKILL.md +14 -0
- package/departments/landing/skills/webinar-funnel/SKILL.md +14 -0
- package/departments/leadership/SKILL.md +14 -0
- package/departments/leadership/skills/change-manage/SKILL.md +14 -0
- package/departments/leadership/skills/conflict-resolve/SKILL.md +14 -0
- package/departments/leadership/skills/culture-audit/SKILL.md +14 -0
- package/departments/leadership/skills/delegation-matrix/SKILL.md +14 -0
- package/departments/leadership/skills/disc-assess/SKILL.md +14 -0
- package/departments/leadership/skills/feedback-give/SKILL.md +14 -0
- package/departments/leadership/skills/hiring-plan/SKILL.md +14 -0
- package/departments/leadership/skills/performance-review/SKILL.md +14 -0
- package/departments/marketing/SKILL.md +14 -0
- package/departments/marketing/skills/ab-test/SKILL.md +14 -0
- package/departments/marketing/skills/analytics-report/SKILL.md +14 -0
- package/departments/marketing/skills/audience-segment/SKILL.md +14 -0
- package/departments/marketing/skills/calendar-plan/SKILL.md +14 -0
- package/departments/marketing/skills/competitor-analysis/SKILL.md +14 -0
- package/departments/marketing/skills/content-audit/SKILL.md +14 -0
- package/departments/marketing/skills/email-sequence/SKILL.md +14 -0
- package/departments/marketing/skills/growth-loop/SKILL.md +14 -0
- package/departments/marketing/skills/marketing-automation/SKILL.md +14 -0
- package/departments/marketing/skills/paid-campaign/SKILL.md +14 -0
- package/departments/marketing/skills/programmatic-seo/SKILL.md +14 -0
- package/departments/marketing/skills/seo-audit/SKILL.md +14 -0
- package/departments/marketing/skills/social-strategy/SKILL.md +14 -0
- package/departments/ops/SKILL.md +14 -0
- package/departments/ops/skills/bottleneck-find/SKILL.md +14 -0
- package/departments/ops/skills/dashboard-build/SKILL.md +14 -0
- package/departments/ops/skills/gdpr-compliance/SKILL.md +14 -0
- package/departments/ops/skills/gtd-setup/SKILL.md +14 -0
- package/departments/ops/skills/integration-design/SKILL.md +14 -0
- package/departments/ops/skills/iso27001/SKILL.md +14 -0
- package/departments/ops/skills/lean-audit/SKILL.md +14 -0
- package/departments/ops/skills/metrics-dashboard/SKILL.md +14 -0
- package/departments/ops/skills/n8n-flow/SKILL.md +14 -0
- package/departments/ops/skills/quality-management/SKILL.md +14 -0
- package/departments/ops/skills/risk-management/SKILL.md +14 -0
- package/departments/ops/skills/soc2-compliance/SKILL.md +14 -0
- package/departments/ops/skills/sop-create/SKILL.md +14 -0
- package/departments/ops/skills/workflow-automate/SKILL.md +14 -0
- package/departments/ops/skills/zapier-flow/SKILL.md +14 -0
- package/departments/org/SKILL.md +14 -0
- package/departments/org/skills/compensation-plan/SKILL.md +14 -0
- package/departments/org/skills/culture-define/SKILL.md +14 -0
- package/departments/org/skills/decision-framework/SKILL.md +14 -0
- package/departments/org/skills/hiring-plan/SKILL.md +14 -0
- package/departments/org/skills/meeting-optimize/SKILL.md +14 -0
- package/departments/org/skills/onboarding-design/SKILL.md +14 -0
- package/departments/org/skills/org-design/SKILL.md +14 -0
- package/departments/org/skills/remote-setup/SKILL.md +14 -0
- package/departments/org/skills/sop-process/SKILL.md +14 -0
- package/departments/org/skills/team-assess/SKILL.md +14 -0
- package/departments/pm/SKILL.md +14 -0
- package/departments/pm/skills/backlog-groom/SKILL.md +14 -0
- package/departments/pm/skills/discovery-plan/SKILL.md +14 -0
- package/departments/pm/skills/estimate-forecast/SKILL.md +14 -0
- package/departments/pm/skills/impact-map/SKILL.md +14 -0
- package/departments/pm/skills/kanban-setup/SKILL.md +14 -0
- package/departments/pm/skills/risk-register/SKILL.md +14 -0
- package/departments/pm/skills/roadmap-build/SKILL.md +14 -0
- package/departments/pm/skills/sprint-plan/SKILL.md +14 -0
- package/departments/pm/skills/stakeholder-map/SKILL.md +14 -0
- package/departments/pm/skills/standup-run/SKILL.md +14 -0
- package/departments/pm/skills/story-write/SKILL.md +14 -0
- package/departments/saas/SKILL.md +14 -0
- package/departments/saas/skills/benchmark-compare/SKILL.md +14 -0
- package/departments/saas/skills/churn-analysis/SKILL.md +14 -0
- package/departments/saas/skills/customer-success/SKILL.md +14 -0
- package/departments/saas/skills/growth-plan/SKILL.md +14 -0
- package/departments/saas/skills/gtm-strategy/SKILL.md +14 -0
- package/departments/saas/skills/launch-execute/SKILL.md +14 -0
- package/departments/saas/skills/metrics-dashboard/SKILL.md +14 -0
- package/departments/saas/skills/micro-saas-stack/SKILL.md +14 -0
- package/departments/saas/skills/mvp-build/SKILL.md +14 -0
- package/departments/saas/skills/niche-evaluate/SKILL.md +14 -0
- package/departments/saas/skills/onboarding-optimize/SKILL.md +14 -0
- package/departments/saas/skills/plg-setup/SKILL.md +14 -0
- package/departments/saas/skills/pricing-strategy/SKILL.md +14 -0
- package/departments/saas/skills/validate-idea/SKILL.md +14 -0
- package/departments/sales/SKILL.md +14 -0
- package/departments/sales/skills/challenger-sell/SKILL.md +14 -0
- package/departments/sales/skills/deal-qualify/SKILL.md +14 -0
- package/departments/sales/skills/discovery-call/SKILL.md +14 -0
- package/departments/sales/skills/forecast-revenue/SKILL.md +14 -0
- package/departments/sales/skills/negotiate-plan/SKILL.md +14 -0
- package/departments/sales/skills/objection-handle/SKILL.md +14 -0
- package/departments/sales/skills/pipeline-manage/SKILL.md +14 -0
- package/departments/sales/skills/pricing-negotiate/SKILL.md +14 -0
- package/departments/strategy/SKILL.md +14 -0
- package/departments/strategy/skills/blue-ocean/SKILL.md +14 -0
- package/departments/strategy/skills/bmc/SKILL.md +14 -0
- package/departments/strategy/skills/board-advisor/SKILL.md +14 -0
- package/departments/strategy/skills/cto-advisor/SKILL.md +14 -0
- package/departments/strategy/skills/extract-data/SKILL.md +14 -0
- package/departments/strategy/skills/five-forces/SKILL.md +14 -0
- package/departments/strategy/skills/growth-strategy/SKILL.md +14 -0
- package/departments/strategy/skills/moat-analysis/SKILL.md +14 -0
- package/departments/strategy/skills/position/SKILL.md +14 -0
- package/departments/strategy/skills/scenario-plan/SKILL.md +14 -0
- package/package.json +3 -3
- package/pyproject.toml +1 -1
- package/scripts/migrate_skills_kb_first.py +137 -0
|
@@ -63,6 +63,56 @@ if (-not $v2Installed) {
|
|
|
63
63
|
# --- Performance timing ----------------------------------------------------
|
|
64
64
|
$sw = [System.Diagnostics.Stopwatch]::StartNew()
|
|
65
65
|
|
|
66
|
+
# --- Flow marker cache invalidation (v2 — new turn resets ALLOW cache) ---
|
|
67
|
+
# Runs before the Synapse bridge so a stuck Python call cannot leave a
|
|
68
|
+
# stale marker across turns. Non-blocking — all failures swallowed.
|
|
69
|
+
try {
|
|
70
|
+
$sessionIdUps = ''
|
|
71
|
+
if ($payload -and $payload.session_id) {
|
|
72
|
+
$sessionIdUps = [string]$payload.session_id
|
|
73
|
+
}
|
|
74
|
+
if ($sessionIdUps) {
|
|
75
|
+
$arkaosRootUps = $env:ARKAOS_ROOT
|
|
76
|
+
if (-not $arkaosRootUps) {
|
|
77
|
+
$repoPathFileUps = Join-Path $env:USERPROFILE '.arkaos\.repo-path'
|
|
78
|
+
if (Test-Path -LiteralPath $repoPathFileUps) {
|
|
79
|
+
try {
|
|
80
|
+
$arkaosRootUps = (Get-Content -Raw -LiteralPath $repoPathFileUps -Encoding UTF8).Trim()
|
|
81
|
+
} catch { }
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (-not $arkaosRootUps) {
|
|
85
|
+
$arkaosRootUps = Join-Path $env:USERPROFILE '.arkaos'
|
|
86
|
+
}
|
|
87
|
+
$pythonForInvalidate = $null
|
|
88
|
+
$venvPyUps = Join-Path $env:USERPROFILE '.arkaos\venv\Scripts\python.exe'
|
|
89
|
+
if (Test-Path -LiteralPath $venvPyUps) {
|
|
90
|
+
$pythonForInvalidate = $venvPyUps
|
|
91
|
+
} else {
|
|
92
|
+
foreach ($cmd in 'python3','python','py') {
|
|
93
|
+
$resolvedUps = Get-Command $cmd -ErrorAction SilentlyContinue
|
|
94
|
+
if ($resolvedUps) { $pythonForInvalidate = $resolvedUps.Source; break }
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if ($pythonForInvalidate) {
|
|
98
|
+
$pyCode = "import os`ntry:`n from core.workflow.marker_cache import invalidate_marker`n invalidate_marker(os.environ.get('SESSION_ID_UPS',''))`nexcept Exception:`n pass`ntry:`n from core.synapse.kb_cache import invalidate_obsidian_query`n invalidate_obsidian_query(os.environ.get('SESSION_ID_UPS',''))`nexcept Exception:`n pass"
|
|
99
|
+
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
|
100
|
+
$psi.FileName = $pythonForInvalidate
|
|
101
|
+
$psi.Arguments = "-c `"$($pyCode -replace '"','\"' -replace "`r?`n",'; ')`""
|
|
102
|
+
$psi.UseShellExecute = $false
|
|
103
|
+
$psi.CreateNoWindow = $true
|
|
104
|
+
$psi.RedirectStandardOutput = $true
|
|
105
|
+
$psi.RedirectStandardError = $true
|
|
106
|
+
[void]$psi.EnvironmentVariables.Add('SESSION_ID_UPS', $sessionIdUps)
|
|
107
|
+
[void]$psi.EnvironmentVariables.Add('PYTHONPATH', $arkaosRootUps)
|
|
108
|
+
try {
|
|
109
|
+
$procUps = [System.Diagnostics.Process]::Start($psi)
|
|
110
|
+
if (-not $procUps.WaitForExit(1500)) { try { $procUps.Kill() } catch { } }
|
|
111
|
+
} catch { }
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
} catch { }
|
|
115
|
+
|
|
66
116
|
# --- Sync version detection ------------------------------------------------
|
|
67
117
|
$syncNotice = ''
|
|
68
118
|
$arkaosHome = Join-Path $env:USERPROFILE '.arkaos'
|
|
@@ -88,6 +88,30 @@ if command -v jq &>/dev/null; then
|
|
|
88
88
|
user_input=$(echo "$input" | jq -r '.userInput // .message // ""' 2>/dev/null)
|
|
89
89
|
SESSION_ID=$(echo "$input" | jq -r '.session_id // ""' 2>/dev/null)
|
|
90
90
|
fi
|
|
91
|
+
|
|
92
|
+
# ─── Flow marker cache invalidation (v2 — new turn, reset ALLOW cache) ──
|
|
93
|
+
# Cheap, non-blocking, runs before Synapse so a stuck Python later cannot
|
|
94
|
+
# leave a stale marker alive across turns.
|
|
95
|
+
if [ -n "$SESSION_ID" ] && command -v python3 &>/dev/null; then
|
|
96
|
+
_INVALIDATE_ROOT="${ARKAOS_ROOT:-}"
|
|
97
|
+
if [ -z "$_INVALIDATE_ROOT" ] && [ -f "$HOME/.arkaos/.repo-path" ]; then
|
|
98
|
+
_INVALIDATE_ROOT=$(cat "$HOME/.arkaos/.repo-path" 2>/dev/null)
|
|
99
|
+
fi
|
|
100
|
+
[ -z "$_INVALIDATE_ROOT" ] && _INVALIDATE_ROOT="$HOME/.arkaos"
|
|
101
|
+
SESSION_ID="$SESSION_ID" PYTHONPATH="$_INVALIDATE_ROOT" python3 -c "
|
|
102
|
+
import os
|
|
103
|
+
try:
|
|
104
|
+
from core.workflow.marker_cache import invalidate_marker
|
|
105
|
+
invalidate_marker(os.environ.get('SESSION_ID', ''))
|
|
106
|
+
except Exception:
|
|
107
|
+
pass
|
|
108
|
+
try:
|
|
109
|
+
from core.synapse.kb_cache import invalidate_obsidian_query
|
|
110
|
+
invalidate_obsidian_query(os.environ.get('SESSION_ID', ''))
|
|
111
|
+
except Exception:
|
|
112
|
+
pass
|
|
113
|
+
" 2>/dev/null || true
|
|
114
|
+
fi
|
|
91
115
|
# Fallback: try to get the raw text
|
|
92
116
|
if [ -z "$user_input" ]; then
|
|
93
117
|
user_input=$(echo "$input" | head -c 2000)
|
|
Binary file
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
"""Auto-documentor — extracts session learnings and writes to Obsidian.
|
|
2
|
+
|
|
3
|
+
Parses the Claude Code transcript JSONL after Quality Gate approval,
|
|
4
|
+
synthesises learnings about external sources consulted, decisions made,
|
|
5
|
+
and deliverables produced, then invokes the Obsidian cataloger + relator
|
|
6
|
+
(Task #4 modules) to file structured, wikilinked notes into the vault.
|
|
7
|
+
|
|
8
|
+
The synthesis step is runtime- and model-agnostic: it delegates to the
|
|
9
|
+
active `LLMProvider` (see `core.runtime.llm_provider`). This module
|
|
10
|
+
NEVER picks a model — the provider / runtime / env does. When no
|
|
11
|
+
provider is available or the call fails, it falls through to a
|
|
12
|
+
deterministic template synthesiser that preserves every extracted fact.
|
|
13
|
+
|
|
14
|
+
ADR/Plan references:
|
|
15
|
+
- ~/.arkaos/plans/2026-04-20-intelligence-v2.md (Task #7 — Épico B)
|
|
16
|
+
- ~/.arkaos/plans/2026-04-20-llm-agnostic.md (Task #12/#13 — LLMProvider)
|
|
17
|
+
- core/obsidian/cataloger.py, core/obsidian/relator.py (Task #4)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
import re
|
|
24
|
+
from dataclasses import dataclass, field
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Iterable
|
|
27
|
+
|
|
28
|
+
from core.obsidian import cataloger as _cataloger
|
|
29
|
+
from core.obsidian import relator as _relator
|
|
30
|
+
from core.obsidian.writer import ObsidianWriter
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
SAFE_SESSION_ID_RE = re.compile(r"^[A-Za-z0-9._-]{1,128}$")
|
|
34
|
+
|
|
35
|
+
_URL_RE = re.compile(r"https?://[^\s\)\]\"']+")
|
|
36
|
+
_FILE_PATH_RE = re.compile(r"(?:^|[\s`'])(/[A-Za-z0-9_./\-]+\.[A-Za-z0-9]+)")
|
|
37
|
+
_ROUTING_MARKER_RE = re.compile(
|
|
38
|
+
r"\[arka:routing\]\s*([\w-]+)\s*->\s*(\w+)", re.IGNORECASE
|
|
39
|
+
)
|
|
40
|
+
_DECISION_CUES = (
|
|
41
|
+
"decided to", "chose ", "selected ", "going with", "i'll use",
|
|
42
|
+
"we'll use", "opted for", "the approach is", "rationale:",
|
|
43
|
+
)
|
|
44
|
+
_EXTERNAL_RESEARCH_TOOLS = frozenset({
|
|
45
|
+
"WebFetch", "WebSearch",
|
|
46
|
+
"mcp__context7__query-docs", "mcp__context7__resolve-library-id",
|
|
47
|
+
"mcp__firecrawl__firecrawl_search",
|
|
48
|
+
"mcp__firecrawl__firecrawl_scrape",
|
|
49
|
+
"mcp__firecrawl__firecrawl_crawl",
|
|
50
|
+
"mcp__firecrawl__firecrawl_extract",
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
_AUTO_DOC_SUFFIX = "Auto-documented by ArkaOS"
|
|
54
|
+
_LLM_MAX_TOKENS = 1500
|
|
55
|
+
|
|
56
|
+
_SYSTEM_PROMPT = (
|
|
57
|
+
"You are ArkaOS's auto-documentor. Produce a concise knowledge note "
|
|
58
|
+
"(150-300 words) summarising the session. Structure: short intro, "
|
|
59
|
+
"then markdown sections for Key Facts, Decisions, and Sources. "
|
|
60
|
+
"Preserve every URL and file path verbatim. Use Obsidian wikilinks "
|
|
61
|
+
"([[Topic]]) for reusable concepts. No preamble, no sign-off, no "
|
|
62
|
+
"meta commentary about the model or prompt. Output only markdown."
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class Learning:
|
|
68
|
+
"""One synthesisable learning extracted from a session transcript."""
|
|
69
|
+
|
|
70
|
+
topic: str
|
|
71
|
+
content: str
|
|
72
|
+
sources: list[str] = field(default_factory=list)
|
|
73
|
+
metadata: dict = field(default_factory=dict)
|
|
74
|
+
decisions: list[str] = field(default_factory=list)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ─── Extraction ────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def extract_learnings(transcript_path: Path) -> list[Learning]:
|
|
81
|
+
"""Parse a transcript and return structured Learning records."""
|
|
82
|
+
records = _load_transcript(transcript_path)
|
|
83
|
+
if not records:
|
|
84
|
+
return []
|
|
85
|
+
sources = _extract_sources(records)
|
|
86
|
+
decisions = _extract_decisions(records)
|
|
87
|
+
deliverables = _extract_deliverables(records)
|
|
88
|
+
content = _build_content_blob(records, decisions, deliverables)
|
|
89
|
+
if not content.strip() and not sources and not decisions:
|
|
90
|
+
return []
|
|
91
|
+
metadata = _detect_metadata(content, records)
|
|
92
|
+
topic = _derive_topic(records, metadata)
|
|
93
|
+
return [Learning(
|
|
94
|
+
topic=topic,
|
|
95
|
+
content=content,
|
|
96
|
+
sources=sources,
|
|
97
|
+
metadata=metadata,
|
|
98
|
+
decisions=decisions,
|
|
99
|
+
)]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _load_transcript(path: Path) -> list[dict]:
|
|
103
|
+
if path is None or not path.exists():
|
|
104
|
+
return []
|
|
105
|
+
out: list[dict] = []
|
|
106
|
+
try:
|
|
107
|
+
raw = path.read_text(encoding="utf-8", errors="replace")
|
|
108
|
+
except OSError:
|
|
109
|
+
return []
|
|
110
|
+
for line in raw.splitlines():
|
|
111
|
+
if not line.strip():
|
|
112
|
+
continue
|
|
113
|
+
try:
|
|
114
|
+
out.append(json.loads(line))
|
|
115
|
+
except json.JSONDecodeError:
|
|
116
|
+
continue
|
|
117
|
+
return out
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _iter_content_items(record: dict) -> Iterable[dict]:
|
|
121
|
+
content = record.get("content")
|
|
122
|
+
if content is None:
|
|
123
|
+
content = record.get("message", {}).get("content")
|
|
124
|
+
if isinstance(content, list):
|
|
125
|
+
for item in content:
|
|
126
|
+
if isinstance(item, dict):
|
|
127
|
+
yield item
|
|
128
|
+
elif isinstance(content, str):
|
|
129
|
+
yield {"type": "text", "text": content}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _role_of(record: dict) -> str:
|
|
133
|
+
return record.get("role") or record.get("message", {}).get("role") or ""
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _text_of(record: dict) -> str:
|
|
137
|
+
parts: list[str] = []
|
|
138
|
+
for item in _iter_content_items(record):
|
|
139
|
+
if "text" in item:
|
|
140
|
+
parts.append(str(item["text"]))
|
|
141
|
+
return "\n".join(parts)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _extract_sources(records: list[dict]) -> list[str]:
|
|
145
|
+
seen: set[str] = set()
|
|
146
|
+
sources: list[str] = []
|
|
147
|
+
for rec in records:
|
|
148
|
+
for item in _iter_content_items(rec):
|
|
149
|
+
if item.get("type") == "tool_use":
|
|
150
|
+
_collect_tool_use_sources(item, sources, seen)
|
|
151
|
+
text = _text_of(rec)
|
|
152
|
+
for url in _URL_RE.findall(text):
|
|
153
|
+
if url not in seen:
|
|
154
|
+
seen.add(url)
|
|
155
|
+
sources.append(url)
|
|
156
|
+
return sources
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _collect_tool_use_sources(item: dict, sources: list[str], seen: set) -> None:
|
|
160
|
+
tool = item.get("name", "")
|
|
161
|
+
if tool not in _EXTERNAL_RESEARCH_TOOLS:
|
|
162
|
+
return
|
|
163
|
+
params = item.get("input") or {}
|
|
164
|
+
for key in ("url", "query", "libraryName", "topic"):
|
|
165
|
+
val = params.get(key)
|
|
166
|
+
if isinstance(val, str) and val and val not in seen:
|
|
167
|
+
seen.add(val)
|
|
168
|
+
sources.append(val)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _extract_decisions(records: list[dict]) -> list[str]:
|
|
172
|
+
decisions: list[str] = []
|
|
173
|
+
for rec in records:
|
|
174
|
+
if _role_of(rec) != "assistant":
|
|
175
|
+
continue
|
|
176
|
+
text = _text_of(rec)
|
|
177
|
+
for match in _ROUTING_MARKER_RE.finditer(text):
|
|
178
|
+
decisions.append(f"routed: {match.group(1)} -> {match.group(2)}")
|
|
179
|
+
for line in text.splitlines():
|
|
180
|
+
low = line.lower().strip()
|
|
181
|
+
if any(cue in low for cue in _DECISION_CUES) and len(line) < 400:
|
|
182
|
+
decisions.append(line.strip())
|
|
183
|
+
return _dedupe_keep_order(decisions)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _extract_deliverables(records: list[dict]) -> list[str]:
|
|
187
|
+
seen: set[str] = set()
|
|
188
|
+
out: list[str] = []
|
|
189
|
+
for rec in records:
|
|
190
|
+
for item in _iter_content_items(rec):
|
|
191
|
+
if item.get("type") != "tool_use":
|
|
192
|
+
continue
|
|
193
|
+
if item.get("name") not in ("Write", "Edit", "MultiEdit"):
|
|
194
|
+
continue
|
|
195
|
+
path = (item.get("input") or {}).get("file_path", "")
|
|
196
|
+
if path and path not in seen:
|
|
197
|
+
seen.add(path)
|
|
198
|
+
out.append(path)
|
|
199
|
+
return out
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _build_content_blob(
|
|
203
|
+
records: list[dict], decisions: list[str], deliverables: list[str]
|
|
204
|
+
) -> str:
|
|
205
|
+
user_prompts = [
|
|
206
|
+
_text_of(r) for r in records if _role_of(r) == "user"
|
|
207
|
+
]
|
|
208
|
+
assistant_finals = [
|
|
209
|
+
_text_of(r) for r in records if _role_of(r) == "assistant"
|
|
210
|
+
][-3:]
|
|
211
|
+
sections: list[str] = []
|
|
212
|
+
if user_prompts:
|
|
213
|
+
sections.append("## Request\n\n" + user_prompts[0].strip()[:800])
|
|
214
|
+
if decisions:
|
|
215
|
+
sections.append("## Decisions\n\n" + "\n".join(f"- {d}" for d in decisions[:10]))
|
|
216
|
+
if deliverables:
|
|
217
|
+
sections.append("## Deliverables\n\n" + "\n".join(f"- `{p}`" for p in deliverables[:20]))
|
|
218
|
+
if assistant_finals:
|
|
219
|
+
sections.append("## Summary\n\n" + assistant_finals[-1].strip()[:1200])
|
|
220
|
+
return "\n\n".join(sections)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _detect_metadata(content: str, records: list[dict]) -> dict:
|
|
224
|
+
low = content.lower()
|
|
225
|
+
meta: dict = {"source": "session-transcript"}
|
|
226
|
+
for rec in records:
|
|
227
|
+
if _role_of(rec) != "assistant":
|
|
228
|
+
continue
|
|
229
|
+
text = _text_of(rec)
|
|
230
|
+
for match in _ROUTING_MARKER_RE.finditer(text):
|
|
231
|
+
meta["dept"] = match.group(1).lower()
|
|
232
|
+
meta["agent"] = match.group(2).lower()
|
|
233
|
+
break
|
|
234
|
+
if "dept" in meta:
|
|
235
|
+
break
|
|
236
|
+
if "laravel" in low or "php" in low:
|
|
237
|
+
meta["stack"] = "Laravel"
|
|
238
|
+
elif "nuxt" in low or "vue" in low:
|
|
239
|
+
meta["stack"] = "Vue"
|
|
240
|
+
elif "next.js" in low or "react" in low:
|
|
241
|
+
meta["stack"] = "React"
|
|
242
|
+
elif "python" in low or "pydantic" in low:
|
|
243
|
+
meta["stack"] = "Python"
|
|
244
|
+
return meta
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _derive_topic(records: list[dict], metadata: dict) -> str:
|
|
248
|
+
for rec in records:
|
|
249
|
+
if _role_of(rec) != "user":
|
|
250
|
+
continue
|
|
251
|
+
text = _text_of(rec).strip()
|
|
252
|
+
if text:
|
|
253
|
+
first_line = text.splitlines()[0][:120]
|
|
254
|
+
return first_line or "Session Learning"
|
|
255
|
+
return metadata.get("dept", "Session Learning").title()
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _dedupe_keep_order(items: Iterable[str]) -> list[str]:
|
|
259
|
+
seen: set[str] = set()
|
|
260
|
+
out: list[str] = []
|
|
261
|
+
for item in items:
|
|
262
|
+
if item and item not in seen:
|
|
263
|
+
seen.add(item)
|
|
264
|
+
out.append(item)
|
|
265
|
+
return out
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
# ─── Synthesis ─────────────────────────────────────────────────────────
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def synthesize(learning: Learning) -> str:
|
|
272
|
+
"""Produce a markdown body for the learning.
|
|
273
|
+
|
|
274
|
+
Delegates to the active `LLMProvider` via `_call_llm`. If the
|
|
275
|
+
provider is unavailable, returns empty text, or raises, falls
|
|
276
|
+
through to a deterministic template that preserves every extracted
|
|
277
|
+
fact. No model name ever crosses this boundary.
|
|
278
|
+
"""
|
|
279
|
+
llm_out = _call_llm(learning)
|
|
280
|
+
if llm_out:
|
|
281
|
+
return llm_out
|
|
282
|
+
return _template_synthesize(learning)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _call_llm(learning: Learning) -> str:
|
|
286
|
+
from core.runtime import get_llm_provider
|
|
287
|
+
from core.runtime.llm_provider import LLMUnavailable
|
|
288
|
+
|
|
289
|
+
try:
|
|
290
|
+
provider = get_llm_provider()
|
|
291
|
+
if not provider.is_available():
|
|
292
|
+
return ""
|
|
293
|
+
prompt = _build_synthesis_prompt(learning)
|
|
294
|
+
response = provider.complete(
|
|
295
|
+
prompt, max_tokens=_LLM_MAX_TOKENS, system=_SYSTEM_PROMPT
|
|
296
|
+
)
|
|
297
|
+
return response.text.strip()
|
|
298
|
+
except LLMUnavailable:
|
|
299
|
+
return ""
|
|
300
|
+
except Exception: # noqa: BLE001 — LLM path must never crash the doc job
|
|
301
|
+
return ""
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _build_synthesis_prompt(learning: Learning) -> str:
|
|
305
|
+
lines = [f"Topic: {learning.topic}", ""]
|
|
306
|
+
if learning.content.strip():
|
|
307
|
+
lines.append("Session blob:")
|
|
308
|
+
lines.append(learning.content.strip())
|
|
309
|
+
lines.append("")
|
|
310
|
+
if learning.sources:
|
|
311
|
+
lines.append("Sources consulted:")
|
|
312
|
+
for src in learning.sources[:20]:
|
|
313
|
+
lines.append(f"- {src}")
|
|
314
|
+
lines.append("")
|
|
315
|
+
if learning.decisions:
|
|
316
|
+
lines.append("Decisions recorded:")
|
|
317
|
+
for dec in learning.decisions[:10]:
|
|
318
|
+
lines.append(f"- {dec}")
|
|
319
|
+
lines.append("")
|
|
320
|
+
if learning.metadata:
|
|
321
|
+
meta_pairs = sorted(learning.metadata.items())
|
|
322
|
+
lines.append("Metadata:")
|
|
323
|
+
for key, value in meta_pairs:
|
|
324
|
+
lines.append(f"- {key}: {value}")
|
|
325
|
+
lines.append("")
|
|
326
|
+
lines.append(
|
|
327
|
+
"Write the note now. Obey the system prompt. Output only markdown."
|
|
328
|
+
)
|
|
329
|
+
return "\n".join(lines)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _template_synthesize(learning: Learning) -> str:
|
|
333
|
+
parts = [f"# {learning.topic}", ""]
|
|
334
|
+
parts.append(f"> {_AUTO_DOC_SUFFIX}.")
|
|
335
|
+
parts.append("")
|
|
336
|
+
if learning.content.strip():
|
|
337
|
+
parts.append(learning.content.strip())
|
|
338
|
+
parts.append("")
|
|
339
|
+
if learning.sources:
|
|
340
|
+
parts.append("## Sources")
|
|
341
|
+
parts.append("")
|
|
342
|
+
for src in learning.sources[:20]:
|
|
343
|
+
parts.append(f"- {src}")
|
|
344
|
+
parts.append("")
|
|
345
|
+
if learning.decisions:
|
|
346
|
+
parts.append("## Decisions")
|
|
347
|
+
parts.append("")
|
|
348
|
+
for dec in learning.decisions[:10]:
|
|
349
|
+
parts.append(f"- {dec}")
|
|
350
|
+
parts.append("")
|
|
351
|
+
return "\n".join(parts).rstrip() + "\n"
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
# ─── Orchestration ─────────────────────────────────────────────────────
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def document_session(
|
|
358
|
+
transcript_path: Path,
|
|
359
|
+
session_id: str,
|
|
360
|
+
vault_path: Path,
|
|
361
|
+
qg_verdict: str,
|
|
362
|
+
) -> list[Path]:
|
|
363
|
+
"""Run the full pipeline. Returns the paths of every note written."""
|
|
364
|
+
if qg_verdict != "APPROVED":
|
|
365
|
+
return []
|
|
366
|
+
if not _safe_session_id(session_id):
|
|
367
|
+
return []
|
|
368
|
+
transcript_path = Path(transcript_path)
|
|
369
|
+
learnings = extract_learnings(transcript_path)
|
|
370
|
+
if not learnings:
|
|
371
|
+
return []
|
|
372
|
+
writer = ObsidianWriter(vault_path=vault_path)
|
|
373
|
+
written: list[Path] = []
|
|
374
|
+
for learning in learnings:
|
|
375
|
+
note_path = _document_one(learning, writer, vault_path, session_id)
|
|
376
|
+
if note_path:
|
|
377
|
+
written.append(note_path)
|
|
378
|
+
return written
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _document_one(
|
|
382
|
+
learning: Learning,
|
|
383
|
+
writer: ObsidianWriter,
|
|
384
|
+
vault_path: Path,
|
|
385
|
+
session_id: str,
|
|
386
|
+
) -> Path | None:
|
|
387
|
+
body = synthesize(learning)
|
|
388
|
+
meta = dict(learning.metadata)
|
|
389
|
+
meta.setdefault("title", learning.topic[:80])
|
|
390
|
+
meta.setdefault("session", session_id)
|
|
391
|
+
meta.setdefault("auto_documented", True)
|
|
392
|
+
try:
|
|
393
|
+
plan = _cataloger.plan(body, meta)
|
|
394
|
+
except ValueError:
|
|
395
|
+
return None
|
|
396
|
+
note_path = _cataloger.execute(plan, body, writer)
|
|
397
|
+
_relate_note(note_path, body, vault_path, plan)
|
|
398
|
+
return note_path
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _relate_note(note_path: Path, body: str, vault_path: Path, plan) -> None:
|
|
402
|
+
try:
|
|
403
|
+
related = _relator.find_related(
|
|
404
|
+
content=body, vault=vault_path, exclude=note_path
|
|
405
|
+
)
|
|
406
|
+
except Exception:
|
|
407
|
+
return
|
|
408
|
+
if related:
|
|
409
|
+
_append_related_block(note_path, related)
|
|
410
|
+
_relator.update_back_references(note_path, related, note_path.stem)
|
|
411
|
+
if plan.applicable_mocs:
|
|
412
|
+
_relator.update_mocs(note_path.stem, list(plan.applicable_mocs), vault_path)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def _append_related_block(note_path: Path, related) -> None:
|
|
416
|
+
try:
|
|
417
|
+
current = note_path.read_text(encoding="utf-8")
|
|
418
|
+
except OSError:
|
|
419
|
+
return
|
|
420
|
+
block = _relator.generate_wikilinks_block(related)
|
|
421
|
+
if not block or "## Related" in current:
|
|
422
|
+
return
|
|
423
|
+
sep = "" if current.endswith("\n") else "\n"
|
|
424
|
+
try:
|
|
425
|
+
note_path.write_text(current + sep + "\n" + block, encoding="utf-8")
|
|
426
|
+
except OSError:
|
|
427
|
+
return
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def _safe_session_id(session_id: str) -> bool:
|
|
431
|
+
if not isinstance(session_id, str) or not session_id:
|
|
432
|
+
return False
|
|
433
|
+
return bool(SAFE_SESSION_ID_RE.match(session_id))
|
package/core/jobs/__init__.py
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
|
-
"""Job queue — SQLite-
|
|
1
|
+
"""Job queue — SQLite tracker + filesystem-backed auto-doc worker."""
|
|
2
2
|
|
|
3
|
-
from core.jobs.manager import
|
|
3
|
+
from core.jobs.manager import Job, JobManager
|
|
4
|
+
from core.jobs.auto_doc_worker import (
|
|
5
|
+
enqueue_job,
|
|
6
|
+
process_pending_jobs,
|
|
7
|
+
run_single_job,
|
|
8
|
+
)
|
|
4
9
|
|
|
5
|
-
__all__ = [
|
|
10
|
+
__all__ = [
|
|
11
|
+
"Job",
|
|
12
|
+
"JobManager",
|
|
13
|
+
"enqueue_job",
|
|
14
|
+
"process_pending_jobs",
|
|
15
|
+
"run_single_job",
|
|
16
|
+
]
|
|
Binary file
|
|
Binary file
|