arkaos 5.17.0 → 5.17.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/README.md +2 -2
- package/THE-ARKAOS-GUIDE.md +2 -2
- package/VERSION +1 -1
- package/arka/SKILL.md +1 -1
- package/arka/skills/costs/SKILL.md +3 -3
- package/bin/arka-doctor +14 -8
- package/config/cognition/schedules.yaml +2 -2
- package/core/harness/drift.py +21 -10
- package/core/harness/manager.py +25 -5
- package/core/hooks/session_start.py +0 -26
- package/core/jobs/auto_doc_worker.py +34 -127
- package/core/runtime/claude_code.py +13 -1
- package/core/runtime/gateway/litellm_config.py +4 -3
- package/core/runtime/model_router.py +16 -10
- package/core/runtime/pricing.py +27 -5
- package/core/runtime/runtime_models.py +9 -7
- package/core/synapse/layers.py +92 -2
- package/dashboard/app/pages/models.vue +1 -1
- package/departments/content/SKILL.md +1 -0
- package/departments/content/skills/hyperframes/SKILL.md +95 -0
- package/departments/content/skills/video-produce/SKILL.md +27 -17
- package/departments/content/skills/video-setup/SKILL.md +26 -13
- package/harness/codex/AGENTS.md +2 -2
- package/harness/copilot/copilot-instructions.md +2 -2
- package/harness/cursor/rules/arkaos.mdc +3 -3
- package/harness/gemini/GEMINI.md +2 -2
- package/harness/opencode/AGENTS.md +2 -2
- package/harness/opencode/agents/arka-architect-gabriel.md +1 -1
- package/harness/opencode/agents/arka-brand-director-valentina.md +1 -1
- package/harness/opencode/agents/arka-cfo-helena.md +1 -1
- package/harness/opencode/agents/arka-chief-of-staff-afonso.md +1 -1
- package/harness/opencode/agents/arka-community-strategist-beatriz.md +1 -1
- package/harness/opencode/agents/arka-content-strategist-rafael.md +1 -1
- package/harness/opencode/agents/arka-conversion-strategist-ines.md +1 -1
- package/harness/opencode/agents/arka-coo-sofia.md +1 -1
- package/harness/opencode/agents/arka-copy-director-eduardo.md +1 -1
- package/harness/opencode/agents/arka-cqo-marta.md +1 -1
- package/harness/opencode/agents/arka-cto-marco.md +1 -1
- package/harness/opencode/agents/arka-design-ops-lead-iris.md +1 -1
- package/harness/opencode/agents/arka-ecom-director-ricardo.md +1 -1
- package/harness/opencode/agents/arka-knowledge-director-clara.md +1 -1
- package/harness/opencode/agents/arka-leadership-director-rodrigo.md +1 -1
- package/harness/opencode/agents/arka-marketing-director-luna.md +1 -1
- package/harness/opencode/agents/arka-ops-lead-daniel.md +1 -1
- package/harness/opencode/agents/arka-pm-director-carolina.md +1 -1
- package/harness/opencode/agents/arka-revops-lead-vicente.md +1 -1
- package/harness/opencode/agents/arka-saas-strategist-tiago.md +1 -1
- package/harness/opencode/agents/arka-sales-director-miguel.md +1 -1
- package/harness/opencode/agents/arka-strategy-director-tomas.md +1 -1
- package/harness/opencode/agents/arka-tech-director-francisca.md +1 -1
- package/harness/opencode/agents/arka-tech-lead-paulo.md +1 -1
- package/harness/opencode/agents/arka-video-producer-simao.md +1 -1
- package/harness/zed/.rules +2 -2
- package/installer/doctor.js +18 -6
- package/installer/fallback-model.js +80 -29
- package/installer/index.js +7 -4
- package/installer/update.js +5 -2
- package/knowledge/commands-keywords.json +29 -2
- package/knowledge/commands-registry.json +41 -5
- package/knowledge/skills-manifest.json +14 -1
- package/package.json +1 -1
- package/pyproject.toml +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
**The Operating System for AI Agent Teams.**
|
|
6
6
|
|
|
7
|
-
89 agents. 17 departments.
|
|
7
|
+
89 agents. 17 departments. 341 skills. Enterprise frameworks. Multi-runtime. One install.
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
10
|
npx arkaos install
|
|
@@ -97,7 +97,7 @@ npx arkaos doctor # Health check
|
|
|
97
97
|
|
|
98
98
|
### Skill packs, à la carte
|
|
99
99
|
|
|
100
|
-
The default install ships a curated core so your context window stays lean. Everything else lives in the ArkaOS plugin marketplace: 16 department packs with
|
|
100
|
+
The default install ships a curated core so your context window stays lean. Everything else lives in the ArkaOS plugin marketplace: 16 department packs with 267 skills, generated straight from the same sources the core uses. Inside Claude Code:
|
|
101
101
|
|
|
102
102
|
```
|
|
103
103
|
/plugin marketplace add andreagroferreira/arka-os
|
package/THE-ARKAOS-GUIDE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# The ArkaOS Guide
|
|
2
2
|
|
|
3
|
-
> v5.
|
|
3
|
+
> v5.17.1 — 89 agents, 17 departments, 341 skills, 307 commands, 20 ADRs.
|
|
4
4
|
> One file, everything you need to start. Generated by `scripts/guide_gen.py` — never hand-edited.
|
|
5
5
|
|
|
6
6
|
## What it is
|
|
@@ -40,7 +40,7 @@ answers as a generic assistant.
|
|
|
40
40
|
| `/arka` | 22 |
|
|
41
41
|
| `/brand` | 14 |
|
|
42
42
|
| `/community` | 15 |
|
|
43
|
-
| `/content` |
|
|
43
|
+
| `/content` | 21 |
|
|
44
44
|
| `/dev` | 42 |
|
|
45
45
|
| `/do` | 1 |
|
|
46
46
|
| `/ecom` | 18 |
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
5.17.
|
|
1
|
+
5.17.2
|
package/arka/SKILL.md
CHANGED
|
@@ -63,7 +63,7 @@ complementary, not alternatives.
|
|
|
63
63
|
# ArkaOS — Main Orchestrator
|
|
64
64
|
|
|
65
65
|
> **The Operating System for AI Agent Teams**
|
|
66
|
-
> 89 agents. 17 departments.
|
|
66
|
+
> 89 agents. 17 departments. 341 skills. Multi-runtime. Dashboard. Knowledge RAG.
|
|
67
67
|
|
|
68
68
|
## ⛔ Evidence flow — 4 gates (NON-NEGOTIABLE)
|
|
69
69
|
|
|
@@ -70,10 +70,10 @@ Outside this skill's scope but worth surfacing alongside the summary:
|
|
|
70
70
|
| Lever | Where | Effect |
|
|
71
71
|
| --- | --- | --- |
|
|
72
72
|
| `ARKAOS_LLM_PROVIDER` | env | Switch between Anthropic, OpenAI, Gemini adapters per the multi-LLM router. |
|
|
73
|
-
| `fallbackModel` | `~/.claude/settings.json` (Claude Code 2.1.166+) | Up to three models tried in order when the primary is overloaded or unavailable. `npx arkaos install` / `update` seed `["claude-opus-5", "claude-sonnet-5"]` when the key is absent and never overwrite an operator chain; `/arka status` shows the configured chain. |
|
|
74
|
-
| Schedule `model:` / `fallback_models:` | `~/.arkaos/schedules.yaml` | A pinned `model` goes to `claude --model`; without one the Model Fabric `strategy` role becomes `ANTHROPIC_DEFAULT_MODEL` (Claude Code 2.1.236+). `fallback_models` goes to `--fallback-model` (default `claude-opus-5 → claude-sonnet-5`) so one overload or model 404 no longer ends a nightly cycle. |
|
|
73
|
+
| `fallbackModel` | `~/.claude/settings.json` (Claude Code 2.1.166+) | Up to three models tried in order when the primary is overloaded or unavailable. `npx arkaos install` / `update` seed `["claude-opus-5-5", "claude-sonnet-5"]` when the key is absent, upgrade a chain that is exactly a previous ArkaOS default, and never overwrite an operator chain; `/arka status` shows the configured chain. |
|
|
74
|
+
| Schedule `model:` / `fallback_models:` | `~/.arkaos/schedules.yaml` | A pinned `model` goes to `claude --model`; without one the Model Fabric `strategy` role becomes `ANTHROPIC_DEFAULT_MODEL` (Claude Code 2.1.236+). `fallback_models` goes to `--fallback-model` (default `claude-opus-5-5 → claude-sonnet-5`) so one overload or model 404 no longer ends a nightly cycle. |
|
|
75
75
|
| `ANTHROPIC_BEDROCK_SERVICE_TIER` | env (Claude Code 2.1.122+) | `default` / `flex` / `priority`. Only relevant when routing through AWS Bedrock — `flex` cuts cost ~50 % on non-urgent workloads at the price of higher tail latency. |
|
|
76
|
-
| Agent `model:` field | `departments/*/agents/*.yaml` | Per-agent override per the model-routing matrix in CLAUDE.md. Mechanical roles default to `sonnet` at low effort, C-suite to `opus` (Opus 5); the Quality Gate row below lifts Marta / Eduardo / Francisca to the frontier tier. |
|
|
76
|
+
| Agent `model:` field | `departments/*/agents/*.yaml` | Per-agent override per the model-routing matrix in CLAUDE.md. Mechanical roles default to `sonnet` at low effort, C-suite to `opus` (Opus 5.5); the Quality Gate row below lifts Marta / Eduardo / Francisca to the frontier tier. |
|
|
77
77
|
| Quality Gate model | constitution `quality_gate.model_policy` | Marta / Eduardo / Francisca run on the best model available (frontier tier) regardless of the cost ceiling — review quality is never the place to save tokens. |
|
|
78
78
|
|
|
79
79
|
## Non-negotiables
|
package/bin/arka-doctor
CHANGED
|
@@ -370,17 +370,23 @@ run_check "plugins" "warn" "$PLUGINS_DESC" "$PLUGINS_OK" "Run: bash install.sh"
|
|
|
370
370
|
|
|
371
371
|
# ─── Check 17: Hyperframes Skills (video production) ──────────────────────
|
|
372
372
|
# Parity with installer/doctor.js `hyperframes-skills` (PR-C2): same name,
|
|
373
|
-
# same
|
|
373
|
+
# same sentinels, same two roots (~/.claude/skills and ~/.agents/skills),
|
|
374
|
+
# warn-only — video production is opt-in.
|
|
374
375
|
HF_OK=false
|
|
375
376
|
HF_DESC="Hyperframes skills AUSENTE (video-as-code editing for /content video)"
|
|
376
|
-
for
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
377
|
+
for hf_root in "$HOME/.claude/skills" "$HOME/.agents/skills"; do
|
|
378
|
+
for hf_name in hyperframes hyperframes-core; do
|
|
379
|
+
if [ -f "$hf_root/$hf_name/SKILL.md" ]; then
|
|
380
|
+
HF_OK=true
|
|
381
|
+
# Static, matching installer/doctor.js `description` exactly: run_check
|
|
382
|
+
# splices $desc unescaped into the --json payload, so no path or
|
|
383
|
+
# name interpolation may reach it.
|
|
384
|
+
HF_DESC="Hyperframes skills installed (video-as-code editing for /content video)"
|
|
385
|
+
break 2
|
|
386
|
+
fi
|
|
387
|
+
done
|
|
382
388
|
done
|
|
383
|
-
run_check "hyperframes-skills" "warn" "$HF_DESC" "$HF_OK" "Run /content video-setup in Claude Code"
|
|
389
|
+
run_check "hyperframes-skills" "warn" "$HF_DESC" "$HF_OK" "Run /content video-setup in Claude Code, or: npx hyperframes skills update (core set; npx hyperframes skills check reports staleness)"
|
|
384
390
|
|
|
385
391
|
# ─── Check 18: SQLite corrupt-store backups (self-heal F1-D1) ──────────────
|
|
386
392
|
# Parity with installer/doctor.js `sqlite-corrupt-backups`: same name,
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
#
|
|
6
6
|
# Model pin and fallback chain (Runtime Sync PR3) — per schedule, optional:
|
|
7
7
|
# model: claude-sonnet-5 # pinned: goes to `claude --model`
|
|
8
|
-
# fallback_models: [claude-opus-5] # `--fallback-model a,b`; [] disables
|
|
8
|
+
# fallback_models: [claude-opus-5-5] # `--fallback-model a,b`; [] disables
|
|
9
9
|
# Without `model`, the Model Fabric `strategy` role (~/.arkaos/models.yaml)
|
|
10
10
|
# becomes ANTHROPIC_DEFAULT_MODEL for the run. Without `fallback_models`,
|
|
11
|
-
# the chain is claude-opus-5 → claude-sonnet-5. The runtime caps chains at
|
|
11
|
+
# the chain is claude-opus-5-5 → claude-sonnet-5. The runtime caps chains at
|
|
12
12
|
# three models after de-duplication, removes ids outside `availableModels`
|
|
13
13
|
# before the walk and fails over past an id it cannot reach (a retired
|
|
14
14
|
# model); the ArkaOS gateway plays no part in the chain.
|
package/core/harness/drift.py
CHANGED
|
@@ -462,25 +462,36 @@ def _check_worktree(report: DriftReport, settings: dict[str, Any]) -> None:
|
|
|
462
462
|
)
|
|
463
463
|
|
|
464
464
|
|
|
465
|
+
def _fallback_finding(status: DriftStatus, detail: str) -> DriftFinding:
|
|
466
|
+
return DriftFinding("settings:fallbackModel", status, "fallbackModel", detail)
|
|
467
|
+
|
|
468
|
+
|
|
465
469
|
def _check_fallback_model(report: DriftReport, settings: dict[str, Any]) -> None:
|
|
466
470
|
"""MISSING when the key is absent or JSON null (the seeder writes the
|
|
467
|
-
default in both cases);
|
|
468
|
-
|
|
469
|
-
|
|
471
|
+
default in both cases); DIVERGED when the chain is exactly a default
|
|
472
|
+
ArkaOS seeded in an earlier release (assert upgrades it); ADOPTED for
|
|
473
|
+
any other value that is not the default — an array, an empty array,
|
|
474
|
+
or the legacy string form."""
|
|
475
|
+
from core.runtime.claude_code import (
|
|
476
|
+
DEFAULT_FALLBACK_MODELS,
|
|
477
|
+
is_previous_fallback_default,
|
|
478
|
+
)
|
|
470
479
|
|
|
471
480
|
chain = settings.get("fallbackModel")
|
|
472
481
|
if chain is None:
|
|
473
482
|
report.findings.append(
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
483
|
+
_fallback_finding(DriftStatus.MISSING, "fallbackModel chain default not seeded")
|
|
484
|
+
)
|
|
485
|
+
elif is_previous_fallback_default(chain):
|
|
486
|
+
detail = (
|
|
487
|
+
f"previous ArkaOS default chain ({' → '.join(chain)}); "
|
|
488
|
+
f"`arka harness assert` upgrades it to {' → '.join(DEFAULT_FALLBACK_MODELS)}"
|
|
478
489
|
)
|
|
490
|
+
report.findings.append(_fallback_finding(DriftStatus.DIVERGED, detail))
|
|
479
491
|
elif chain != list(DEFAULT_FALLBACK_MODELS):
|
|
480
492
|
report.findings.append(
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
"operator-configured fallback chain; seed policy adopts it",
|
|
493
|
+
_fallback_finding(
|
|
494
|
+
DriftStatus.ADOPTED, "operator-configured fallback chain; seed policy adopts it"
|
|
484
495
|
)
|
|
485
496
|
)
|
|
486
497
|
|
package/core/harness/manager.py
CHANGED
|
@@ -41,7 +41,10 @@ from pathlib import Path
|
|
|
41
41
|
from core.harness import drift, json_store, paths
|
|
42
42
|
from core.harness.manifest import OwnershipManifest
|
|
43
43
|
from core.harness.spec import HookRegistration, spec_for
|
|
44
|
-
from core.runtime.claude_code import
|
|
44
|
+
from core.runtime.claude_code import (
|
|
45
|
+
DEFAULT_FALLBACK_MODELS,
|
|
46
|
+
is_previous_fallback_default,
|
|
47
|
+
)
|
|
45
48
|
|
|
46
49
|
|
|
47
50
|
@dataclass
|
|
@@ -49,8 +52,8 @@ class SurfaceAction:
|
|
|
49
52
|
"""One thing assert did (or deliberately did not do) to a surface."""
|
|
50
53
|
|
|
51
54
|
surface: str
|
|
52
|
-
# created | repaired | unrepaired | reseeded |
|
|
53
|
-
# | refused | noop
|
|
55
|
+
# created | repaired | unrepaired | reseeded | upgraded
|
|
56
|
+
# | adopted-skip | refused | noop
|
|
54
57
|
action: str
|
|
55
58
|
detail: str = ""
|
|
56
59
|
|
|
@@ -353,13 +356,23 @@ class ClaudeConfigManager:
|
|
|
353
356
|
def _assert_seed(
|
|
354
357
|
self, settings: dict, report: AssertReport, reseed: bool
|
|
355
358
|
) -> None:
|
|
356
|
-
for surface, key, default, is_ours in _seed_surfaces(self):
|
|
359
|
+
for surface, key, default, is_ours, is_previous in _seed_surfaces(self):
|
|
357
360
|
current = settings.get(key)
|
|
358
361
|
if current is None:
|
|
359
362
|
settings[key] = default
|
|
360
363
|
report.actions.append(SurfaceAction(surface, "created"))
|
|
361
364
|
elif is_ours(current):
|
|
362
365
|
report.actions.append(SurfaceAction(surface, "noop"))
|
|
366
|
+
elif is_previous(current):
|
|
367
|
+
# A chain ArkaOS itself seeded before is not an operator
|
|
368
|
+
# decision: move it to the current default.
|
|
369
|
+
settings[key] = default
|
|
370
|
+
report.actions.append(
|
|
371
|
+
SurfaceAction(
|
|
372
|
+
surface, "upgraded",
|
|
373
|
+
"previous ArkaOS default; moved to the current default",
|
|
374
|
+
)
|
|
375
|
+
)
|
|
363
376
|
elif reseed:
|
|
364
377
|
settings[key] = default
|
|
365
378
|
report.actions.append(SurfaceAction(surface, "reseeded"))
|
|
@@ -531,20 +544,27 @@ def _user_deny_extensions(home: Path | None) -> list[str]:
|
|
|
531
544
|
return [r for r in raw if isinstance(r, str) and r]
|
|
532
545
|
|
|
533
546
|
|
|
547
|
+
def _never_previous(_value: object) -> bool:
|
|
548
|
+
"""Surfaces with no history of ArkaOS-authored defaults to upgrade."""
|
|
549
|
+
return False
|
|
550
|
+
|
|
551
|
+
|
|
534
552
|
def _seed_surfaces(manager: ClaudeConfigManager):
|
|
535
553
|
statusline = _statusline_default(manager)
|
|
536
554
|
return (
|
|
537
555
|
(
|
|
538
556
|
"settings:statusLine", "statusLine", statusline,
|
|
539
|
-
drift._is_arkaos_statusline,
|
|
557
|
+
drift._is_arkaos_statusline, _never_previous,
|
|
540
558
|
),
|
|
541
559
|
(
|
|
542
560
|
"settings:worktree", "worktree", {"baseRef": "head"},
|
|
543
561
|
lambda v: isinstance(v, dict) and v.get("baseRef") == "head",
|
|
562
|
+
_never_previous,
|
|
544
563
|
),
|
|
545
564
|
(
|
|
546
565
|
"settings:fallbackModel", "fallbackModel", list(DEFAULT_FALLBACK_MODELS),
|
|
547
566
|
lambda v: v == list(DEFAULT_FALLBACK_MODELS),
|
|
567
|
+
is_previous_fallback_default,
|
|
548
568
|
),
|
|
549
569
|
)
|
|
550
570
|
|
|
@@ -318,31 +318,6 @@ def _trigger_reorganizer(repo: str, config: dict) -> None:
|
|
|
318
318
|
_spawn_detached([sys.executable, "-m", "core.cognition.reorganizer_cli"], repo)
|
|
319
319
|
|
|
320
320
|
|
|
321
|
-
def _trigger_auto_doc_drain(repo: str, config: dict) -> None:
|
|
322
|
-
"""Queue-aware AND config-gated: ``cognition.auto_doc_drain_on_session``
|
|
323
|
-
(default True). The Stop hook enqueues auto-doc jobs; sessions drain
|
|
324
|
-
them — the consumer follows the producer's cadence, so a machine with
|
|
325
|
-
no sessions never wakes a worker for nothing (knowledge-flow PR1)."""
|
|
326
|
-
cognition = config.get("cognition") or {}
|
|
327
|
-
if not cognition.get("auto_doc_drain_on_session", True):
|
|
328
|
-
return
|
|
329
|
-
if not repo:
|
|
330
|
-
return
|
|
331
|
-
try:
|
|
332
|
-
from core.jobs.auto_doc_worker import _queue_root
|
|
333
|
-
if not any((_queue_root() / "pending").glob("*.json")):
|
|
334
|
-
return
|
|
335
|
-
except Exception:
|
|
336
|
-
return
|
|
337
|
-
max_age = cognition.get("auto_doc_max_age_days", 14)
|
|
338
|
-
_spawn_detached(
|
|
339
|
-
[sys.executable, "-m", "core.jobs.auto_doc_worker", "--once",
|
|
340
|
-
"--max-jobs", "10", "--max-age-days", str(max_age)],
|
|
341
|
-
repo,
|
|
342
|
-
log_path=Path.home() / ".arkaos" / "logs" / "auto-doc-drain.log",
|
|
343
|
-
)
|
|
344
|
-
|
|
345
|
-
|
|
346
321
|
def _ensure_dashboard(repo: str, config: dict) -> None:
|
|
347
322
|
dashboard = config.get("dashboard") or {}
|
|
348
323
|
if not dashboard.get("ensure_on_session", True):
|
|
@@ -409,7 +384,6 @@ def build_visible(cwd: str) -> str:
|
|
|
409
384
|
msg += _forge_line()
|
|
410
385
|
msg += _drift(version)
|
|
411
386
|
_trigger_reorganizer(repo, config)
|
|
412
|
-
_trigger_auto_doc_drain(repo, config)
|
|
413
387
|
_ensure_dashboard(repo, config)
|
|
414
388
|
return msg
|
|
415
389
|
|
|
@@ -6,8 +6,6 @@ Queue layout under `~/.arkaos/jobs/auto-doc/`:
|
|
|
6
6
|
processing/ — jobs currently being handled
|
|
7
7
|
completed/ — jobs finished successfully
|
|
8
8
|
failed/ — jobs that exhausted retries or could not be parsed
|
|
9
|
-
expired/ — jobs older than --max-age-days, moved aside unprocessed
|
|
10
|
-
(never deleted: the operator can audit or re-queue them)
|
|
11
9
|
|
|
12
10
|
Each job is a JSON file:
|
|
13
11
|
|
|
@@ -28,31 +26,22 @@ second worker that tries to claim the same job will raise and skip it.
|
|
|
28
26
|
from __future__ import annotations
|
|
29
27
|
|
|
30
28
|
import argparse
|
|
31
|
-
import contextlib
|
|
32
29
|
import json
|
|
33
30
|
import os
|
|
34
31
|
import sys
|
|
35
32
|
import time
|
|
36
33
|
import uuid
|
|
37
|
-
from datetime import
|
|
34
|
+
from datetime import datetime, timezone
|
|
38
35
|
from pathlib import Path
|
|
36
|
+
from typing import Optional
|
|
39
37
|
|
|
40
|
-
from core.knowledge.vault import resolve_vault_path
|
|
41
38
|
from core.shared import safe_session_id as _safe_session_id_module
|
|
42
39
|
|
|
40
|
+
|
|
43
41
|
MAX_ATTEMPTS = 3
|
|
44
42
|
# Re-export for backward compatibility with any external importers.
|
|
45
43
|
SAFE_SESSION_ID_RE = _safe_session_id_module.SAFE_SESSION_ID_RE
|
|
46
|
-
_QUEUE_SUBDIRS = ("pending", "processing", "completed", "failed"
|
|
47
|
-
|
|
48
|
-
_VAULT_REMEDIATION = (
|
|
49
|
-
"No vault configured. Set `knowledge.vaultPath` in ~/.arkaos/config.json "
|
|
50
|
-
"(or export ARKAOS_VAULT). Jobs were left untouched in pending/."
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
class VaultNotConfiguredError(RuntimeError):
|
|
55
|
-
"""No vault answered; pending jobs must not be claimed or retried."""
|
|
44
|
+
_QUEUE_SUBDIRS = ("pending", "processing", "completed", "failed")
|
|
56
45
|
|
|
57
46
|
|
|
58
47
|
def _queue_root() -> Path:
|
|
@@ -62,17 +51,11 @@ def _queue_root() -> Path:
|
|
|
62
51
|
return Path.home() / ".arkaos" / "jobs" / "auto-doc"
|
|
63
52
|
|
|
64
53
|
|
|
65
|
-
def _vault_path() -> Path
|
|
66
|
-
""
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
``ARKAOS_VAULT`` env var, both required to exist on disk. This
|
|
71
|
-
replaces a hardcoded ``~/Documents/Personal`` fallback that silently
|
|
72
|
-
sent every note to one developer's personal layout (the exact defect
|
|
73
|
-
``core.knowledge.vault`` was written to end — the worker predates it).
|
|
74
|
-
"""
|
|
75
|
-
return resolve_vault_path()
|
|
54
|
+
def _vault_path() -> Path:
|
|
55
|
+
override = os.environ.get("ARKAOS_VAULT", "").strip()
|
|
56
|
+
if override:
|
|
57
|
+
return Path(override)
|
|
58
|
+
return Path.home() / "Documents" / "Personal"
|
|
76
59
|
|
|
77
60
|
|
|
78
61
|
def _ensure_queue(root: Path) -> None:
|
|
@@ -81,7 +64,7 @@ def _ensure_queue(root: Path) -> None:
|
|
|
81
64
|
|
|
82
65
|
|
|
83
66
|
def _now_iso() -> str:
|
|
84
|
-
return datetime.now(
|
|
67
|
+
return datetime.now(timezone.utc).isoformat()
|
|
85
68
|
|
|
86
69
|
|
|
87
70
|
# ─── Enqueue ───────────────────────────────────────────────────────────
|
|
@@ -91,7 +74,7 @@ def enqueue_job(
|
|
|
91
74
|
session_id: str,
|
|
92
75
|
transcript_path: str,
|
|
93
76
|
qg_verdict: str,
|
|
94
|
-
queue_root: Path
|
|
77
|
+
queue_root: Optional[Path] = None,
|
|
95
78
|
) -> str:
|
|
96
79
|
"""Write a pending job file. Returns the job id."""
|
|
97
80
|
root = queue_root or _queue_root()
|
|
@@ -118,71 +101,22 @@ def enqueue_job(
|
|
|
118
101
|
|
|
119
102
|
def process_pending_jobs(
|
|
120
103
|
max_jobs: int = 10,
|
|
121
|
-
queue_root: Path
|
|
122
|
-
max_age_days: float | None = None,
|
|
104
|
+
queue_root: Optional[Path] = None,
|
|
123
105
|
) -> list[dict]:
|
|
124
|
-
"""Process up to `max_jobs` pending jobs. Returns per-job result dicts.
|
|
125
|
-
|
|
126
|
-
Raises :class:`VaultNotConfiguredError` BEFORE claiming anything when
|
|
127
|
-
no vault is configured — a missing vault is an environment failure
|
|
128
|
-
and must never consume job attempts or move files out of pending/.
|
|
129
|
-
"""
|
|
106
|
+
"""Process up to `max_jobs` pending jobs. Returns per-job result dicts."""
|
|
130
107
|
root = queue_root or _queue_root()
|
|
131
108
|
_ensure_queue(root)
|
|
132
|
-
vault = _vault_path()
|
|
133
|
-
if vault is None:
|
|
134
|
-
raise VaultNotConfiguredError(_VAULT_REMEDIATION)
|
|
135
|
-
pending = sorted((root / "pending").glob("*.json"))
|
|
136
109
|
results: list[dict] = []
|
|
137
|
-
|
|
138
|
-
pending, expired = _partition_expired(pending, root, max_age_days)
|
|
139
|
-
results.extend(expired)
|
|
110
|
+
pending = sorted((root / "pending").glob("*.json"))
|
|
140
111
|
for job_file in pending[:max_jobs]:
|
|
141
112
|
claimed = _claim(job_file, root)
|
|
142
113
|
if claimed is None:
|
|
143
114
|
continue
|
|
144
|
-
results.append(run_single_job(claimed, root
|
|
115
|
+
results.append(run_single_job(claimed, root))
|
|
145
116
|
return results
|
|
146
117
|
|
|
147
118
|
|
|
148
|
-
def
|
|
149
|
-
prefix = job_file.name.split("-", 1)[0]
|
|
150
|
-
try:
|
|
151
|
-
return float(prefix)
|
|
152
|
-
except ValueError:
|
|
153
|
-
return None
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
def _partition_expired(
|
|
157
|
-
pending: list[Path], root: Path, max_age_days: float
|
|
158
|
-
) -> tuple[list[Path], list[dict]]:
|
|
159
|
-
"""Split by job age, read from the unix prefix of the job filename.
|
|
160
|
-
|
|
161
|
-
A name without a parsable prefix is kept for processing — unknown
|
|
162
|
-
age must never silently shelve a job.
|
|
163
|
-
"""
|
|
164
|
-
cutoff = time.time() - max_age_days * 86400.0
|
|
165
|
-
keep: list[Path] = []
|
|
166
|
-
expired: list[dict] = []
|
|
167
|
-
for job_file in pending:
|
|
168
|
-
stamp = _job_timestamp(job_file)
|
|
169
|
-
if stamp is not None and stamp < cutoff:
|
|
170
|
-
expired.append(_move_to_expired(job_file, root))
|
|
171
|
-
else:
|
|
172
|
-
keep.append(job_file)
|
|
173
|
-
return keep, expired
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
def _move_to_expired(job_file: Path, root: Path) -> dict:
|
|
177
|
-
target = root / "expired" / job_file.name
|
|
178
|
-
try:
|
|
179
|
-
os.replace(job_file, target)
|
|
180
|
-
except OSError:
|
|
181
|
-
return {"job_id": job_file.stem, "status": "expire-failed"}
|
|
182
|
-
return {"job_id": job_file.stem, "status": "expired"}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
def _claim(job_file: Path, root: Path) -> Path | None:
|
|
119
|
+
def _claim(job_file: Path, root: Path) -> Optional[Path]:
|
|
186
120
|
target = root / "processing" / job_file.name
|
|
187
121
|
try:
|
|
188
122
|
os.replace(job_file, target)
|
|
@@ -193,17 +127,10 @@ def _claim(job_file: Path, root: Path) -> Path | None:
|
|
|
193
127
|
return None
|
|
194
128
|
|
|
195
129
|
|
|
196
|
-
def run_single_job(
|
|
197
|
-
job_path: Path,
|
|
198
|
-
queue_root: Path | None = None,
|
|
199
|
-
vault: Path | None = None,
|
|
200
|
-
) -> dict:
|
|
130
|
+
def run_single_job(job_path: Path, queue_root: Optional[Path] = None) -> dict:
|
|
201
131
|
"""Process one job end-to-end. Moves to completed/ or failed/."""
|
|
202
132
|
root = queue_root or _queue_root()
|
|
203
133
|
_ensure_queue(root)
|
|
204
|
-
vault = vault or _vault_path()
|
|
205
|
-
if vault is None:
|
|
206
|
-
return _release_claim(job_path, root)
|
|
207
134
|
try:
|
|
208
135
|
payload = json.loads(job_path.read_text(encoding="utf-8"))
|
|
209
136
|
if not isinstance(payload, dict):
|
|
@@ -213,26 +140,18 @@ def run_single_job(
|
|
|
213
140
|
payload["attempts"] = int(payload.get("attempts") or 0) + 1
|
|
214
141
|
job_path.write_text(json.dumps(payload), encoding="utf-8")
|
|
215
142
|
try:
|
|
216
|
-
written = _execute(payload
|
|
143
|
+
written = _execute(payload)
|
|
217
144
|
except Exception as exc:
|
|
218
145
|
return _handle_retry(job_path, payload, root, str(exc))
|
|
219
146
|
return _move_to_completed(job_path, root, payload, written)
|
|
220
147
|
|
|
221
148
|
|
|
222
|
-
def
|
|
223
|
-
"""Vault vanished between pre-flight and execution: put the job back
|
|
224
|
-
WITHOUT touching attempts — environment failure, not job failure."""
|
|
225
|
-
target = root / "pending" / job_path.name
|
|
226
|
-
with contextlib.suppress(OSError):
|
|
227
|
-
os.replace(job_path, target)
|
|
228
|
-
return {"job_id": job_path.stem, "status": "vault-unconfigured"}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
def _execute(payload: dict, vault: Path) -> list[str]:
|
|
149
|
+
def _execute(payload: dict) -> list[str]:
|
|
232
150
|
from core.cognition.auto_documentor import document_session
|
|
233
151
|
transcript = Path(payload.get("transcript_path") or "")
|
|
234
152
|
session_id = str(payload.get("session_id") or "")
|
|
235
153
|
verdict = str(payload.get("qg_verdict") or "")
|
|
154
|
+
vault = _vault_path()
|
|
236
155
|
paths = document_session(transcript, session_id, vault, verdict)
|
|
237
156
|
return [str(p) for p in paths]
|
|
238
157
|
|
|
@@ -264,8 +183,10 @@ def _move_to_completed(
|
|
|
264
183
|
payload["status"] = "completed"
|
|
265
184
|
target = root / "completed" / job_path.name
|
|
266
185
|
job_path.write_text(json.dumps(payload), encoding="utf-8")
|
|
267
|
-
|
|
186
|
+
try:
|
|
268
187
|
os.replace(job_path, target)
|
|
188
|
+
except OSError:
|
|
189
|
+
pass
|
|
269
190
|
return {
|
|
270
191
|
"job_id": payload.get("job_id"),
|
|
271
192
|
"status": "completed",
|
|
@@ -279,13 +200,17 @@ def _move_to_failed(job_path: Path, root: Path, error: str) -> dict:
|
|
|
279
200
|
info["status"] = "failed"
|
|
280
201
|
info["error"] = error
|
|
281
202
|
info["failed_at"] = _now_iso()
|
|
282
|
-
|
|
203
|
+
try:
|
|
283
204
|
job_path.write_text(json.dumps(info), encoding="utf-8")
|
|
205
|
+
except OSError:
|
|
206
|
+
pass
|
|
284
207
|
try:
|
|
285
208
|
os.replace(job_path, target)
|
|
286
209
|
except OSError:
|
|
287
|
-
|
|
210
|
+
try:
|
|
288
211
|
job_path.unlink()
|
|
212
|
+
except OSError:
|
|
213
|
+
pass
|
|
289
214
|
return {
|
|
290
215
|
"job_id": info.get("job_id"),
|
|
291
216
|
"status": "failed",
|
|
@@ -314,38 +239,20 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
314
239
|
help="Cap on jobs per pass.")
|
|
315
240
|
parser.add_argument("--interval", type=float, default=5.0,
|
|
316
241
|
help="Seconds to sleep between passes when daemonised.")
|
|
317
|
-
parser.add_argument("--max-age-days", type=float, default=None,
|
|
318
|
-
help="Move pending jobs older than this to expired/ "
|
|
319
|
-
"instead of processing them.")
|
|
320
242
|
return parser
|
|
321
243
|
|
|
322
244
|
|
|
323
|
-
def
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
results = process_pending_jobs(
|
|
327
|
-
max_jobs=max_jobs, max_age_days=max_age_days
|
|
328
|
-
)
|
|
329
|
-
except VaultNotConfiguredError as exc:
|
|
330
|
-
print(str(exc), file=sys.stderr)
|
|
331
|
-
return 2
|
|
245
|
+
def main(argv: Optional[list[str]] = None) -> int:
|
|
246
|
+
args = _build_parser().parse_args(argv)
|
|
247
|
+
results = process_pending_jobs(max_jobs=args.max_jobs)
|
|
332
248
|
for r in results:
|
|
333
249
|
print(json.dumps(r), flush=True)
|
|
334
|
-
return None
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
def main(argv: list[str] | None = None) -> int:
|
|
338
|
-
args = _build_parser().parse_args(argv)
|
|
339
|
-
rc = _run_pass(args.max_jobs, args.max_age_days)
|
|
340
|
-
if rc is not None:
|
|
341
|
-
return rc
|
|
342
250
|
if args.once:
|
|
343
251
|
return 0
|
|
344
252
|
while True:
|
|
345
253
|
time.sleep(max(0.1, float(args.interval)))
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
return rc
|
|
254
|
+
for r in process_pending_jobs(max_jobs=args.max_jobs):
|
|
255
|
+
print(json.dumps(r), flush=True)
|
|
349
256
|
|
|
350
257
|
|
|
351
258
|
if __name__ == "__main__":
|
|
@@ -108,7 +108,19 @@ _PROBE_TIMEOUT_SECONDS = 10
|
|
|
108
108
|
# model is overloaded or unavailable; the runtime caps chains at three after
|
|
109
109
|
# de-duplication. installer/fallback-model.js carries the same list —
|
|
110
110
|
# tests/python/test_scheduler_daemon.py pins the two to each other.
|
|
111
|
-
DEFAULT_FALLBACK_MODELS: tuple[str, ...] = ("claude-opus-5", "claude-sonnet-5")
|
|
111
|
+
DEFAULT_FALLBACK_MODELS: tuple[str, ...] = ("claude-opus-5-5", "claude-sonnet-5")
|
|
112
|
+
# Chains ArkaOS ITSELF seeded in earlier releases. A settings.json holding one
|
|
113
|
+
# of these byte for byte is not an operator decision, so the seeder and
|
|
114
|
+
# `arka harness assert` upgrade it to DEFAULT_FALLBACK_MODELS; any other
|
|
115
|
+
# present value is the operator's and stays. Append, never edit — the JS twin
|
|
116
|
+
# in installer/fallback-model.js is pinned to this list by the same test.
|
|
117
|
+
PREVIOUS_FALLBACK_DEFAULTS: tuple[tuple[str, ...], ...] = (("claude-opus-5", "claude-sonnet-5"),)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def is_previous_fallback_default(chain: object) -> bool:
|
|
121
|
+
"""True when ``chain`` is exactly a chain ArkaOS seeded in the past."""
|
|
122
|
+
return isinstance(chain, list) and tuple(chain) in PREVIOUS_FALLBACK_DEFAULTS
|
|
123
|
+
|
|
112
124
|
|
|
113
125
|
# Cache for the `claude --version` probe, keyed by the binary probed ("" =
|
|
114
126
|
# the PATH lookup). In-memory only: a disk cache would let one session
|
|
@@ -51,10 +51,11 @@ _SLOT_ROLE: dict[str, str] = {
|
|
|
51
51
|
|
|
52
52
|
# runtime-tier tokens -> real Anthropic model ids the proxy calls upstream.
|
|
53
53
|
# Real ids (claude-fable-5-1, claude-*) pass straight through.
|
|
54
|
-
# Runtime Sync 2026-09-03: `best` is Fable 5.1,
|
|
55
|
-
#
|
|
54
|
+
# Runtime Sync 2026-09-03 (+ Opus 5.5 sweep 2026-09-22): `best` is Fable 5.1,
|
|
55
|
+
# `opus` is Opus 5.5 (the id the Claude Code 2.1.280 alias resolves to), and
|
|
56
|
+
# the weakest lane (`haiku`/`fast`) is Sonnet 5 — ArkaOS never routes to Haiku.
|
|
56
57
|
_RUNTIME_TO_ANTHROPIC: dict[str, str] = {
|
|
57
|
-
"opus": "claude-opus-5",
|
|
58
|
+
"opus": "claude-opus-5-5",
|
|
58
59
|
"best": "claude-fable-5-1",
|
|
59
60
|
"fable": "claude-fable-5-1",
|
|
60
61
|
"sonnet": "claude-sonnet-5",
|
|
@@ -55,18 +55,23 @@ KNOWN_EFFORTS = ("low", "medium", "high", "max")
|
|
|
55
55
|
|
|
56
56
|
_ALIAS_NAMES = frozenset({"best", "default", "fast"})
|
|
57
57
|
|
|
58
|
-
# Runtime Sync 2026-09-03
|
|
59
|
-
#
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
# visible without
|
|
58
|
+
# Runtime Sync 2026-09-03 (Opus 4.x → Opus 5, Fable 5 → Fable 5.1, Haiku →
|
|
59
|
+
# Sonnet 5) and the Opus 5.5 sweep 2026-09-22 (Opus 5 → Opus 5.5) — ids that
|
|
60
|
+
# no longer exist, or lanes ArkaOS no longer routes to. Normalised when a
|
|
61
|
+
# role is RESOLVED; the operator's models.yaml is never rewritten. One
|
|
62
|
+
# stderr notice per process per id, so a stale pin is visible without
|
|
63
|
+
# becoming noise. Legacy ids map straight to the CURRENT lane — the lookup
|
|
64
|
+
# is a single dict read, not a transitive walk, so no value may be a key.
|
|
65
|
+
LEGACY_SYNC_DATE = "2026-09-22"
|
|
63
66
|
LEGACY_MODEL_IDS: dict[str, str] = {
|
|
64
67
|
"claude-fable-5": "claude-fable-5-1",
|
|
65
68
|
"claude-fable-5[1m]": "claude-fable-5-1[1m]",
|
|
66
|
-
"claude-opus-
|
|
67
|
-
"claude-opus-
|
|
68
|
-
"claude-opus-4-
|
|
69
|
-
"claude-opus-4-
|
|
69
|
+
"claude-opus-5": "claude-opus-5-5",
|
|
70
|
+
"claude-opus-5[1m]": "claude-opus-5-5[1m]",
|
|
71
|
+
"claude-opus-4-8": "claude-opus-5-5",
|
|
72
|
+
"claude-opus-4-8[1m]": "claude-opus-5-5[1m]",
|
|
73
|
+
"claude-opus-4-7": "claude-opus-5-5",
|
|
74
|
+
"claude-opus-4-6": "claude-opus-5-5",
|
|
70
75
|
"claude-haiku-4-5-20251001": "claude-sonnet-5",
|
|
71
76
|
"claude-haiku-4-5": "claude-sonnet-5",
|
|
72
77
|
"haiku": "sonnet",
|
|
@@ -89,7 +94,8 @@ def normalise_model_id(model: str, source: str = "models.yaml") -> str:
|
|
|
89
94
|
_LEGACY_NOTICED.add((source, model))
|
|
90
95
|
print(
|
|
91
96
|
f"[arka:warn] {source} pins legacy model {model!r}; resolving as "
|
|
92
|
-
f"{current!r} (
|
|
97
|
+
f"{current!r} (retired lane; last sweep {LEGACY_SYNC_DATE}). "
|
|
98
|
+
"Update the pin to silence this.",
|
|
93
99
|
file=sys.stderr,
|
|
94
100
|
)
|
|
95
101
|
return current
|