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.
Files changed (62) hide show
  1. package/README.md +2 -2
  2. package/THE-ARKAOS-GUIDE.md +2 -2
  3. package/VERSION +1 -1
  4. package/arka/SKILL.md +1 -1
  5. package/arka/skills/costs/SKILL.md +3 -3
  6. package/bin/arka-doctor +14 -8
  7. package/config/cognition/schedules.yaml +2 -2
  8. package/core/harness/drift.py +21 -10
  9. package/core/harness/manager.py +25 -5
  10. package/core/hooks/session_start.py +0 -26
  11. package/core/jobs/auto_doc_worker.py +34 -127
  12. package/core/runtime/claude_code.py +13 -1
  13. package/core/runtime/gateway/litellm_config.py +4 -3
  14. package/core/runtime/model_router.py +16 -10
  15. package/core/runtime/pricing.py +27 -5
  16. package/core/runtime/runtime_models.py +9 -7
  17. package/core/synapse/layers.py +92 -2
  18. package/dashboard/app/pages/models.vue +1 -1
  19. package/departments/content/SKILL.md +1 -0
  20. package/departments/content/skills/hyperframes/SKILL.md +95 -0
  21. package/departments/content/skills/video-produce/SKILL.md +27 -17
  22. package/departments/content/skills/video-setup/SKILL.md +26 -13
  23. package/harness/codex/AGENTS.md +2 -2
  24. package/harness/copilot/copilot-instructions.md +2 -2
  25. package/harness/cursor/rules/arkaos.mdc +3 -3
  26. package/harness/gemini/GEMINI.md +2 -2
  27. package/harness/opencode/AGENTS.md +2 -2
  28. package/harness/opencode/agents/arka-architect-gabriel.md +1 -1
  29. package/harness/opencode/agents/arka-brand-director-valentina.md +1 -1
  30. package/harness/opencode/agents/arka-cfo-helena.md +1 -1
  31. package/harness/opencode/agents/arka-chief-of-staff-afonso.md +1 -1
  32. package/harness/opencode/agents/arka-community-strategist-beatriz.md +1 -1
  33. package/harness/opencode/agents/arka-content-strategist-rafael.md +1 -1
  34. package/harness/opencode/agents/arka-conversion-strategist-ines.md +1 -1
  35. package/harness/opencode/agents/arka-coo-sofia.md +1 -1
  36. package/harness/opencode/agents/arka-copy-director-eduardo.md +1 -1
  37. package/harness/opencode/agents/arka-cqo-marta.md +1 -1
  38. package/harness/opencode/agents/arka-cto-marco.md +1 -1
  39. package/harness/opencode/agents/arka-design-ops-lead-iris.md +1 -1
  40. package/harness/opencode/agents/arka-ecom-director-ricardo.md +1 -1
  41. package/harness/opencode/agents/arka-knowledge-director-clara.md +1 -1
  42. package/harness/opencode/agents/arka-leadership-director-rodrigo.md +1 -1
  43. package/harness/opencode/agents/arka-marketing-director-luna.md +1 -1
  44. package/harness/opencode/agents/arka-ops-lead-daniel.md +1 -1
  45. package/harness/opencode/agents/arka-pm-director-carolina.md +1 -1
  46. package/harness/opencode/agents/arka-revops-lead-vicente.md +1 -1
  47. package/harness/opencode/agents/arka-saas-strategist-tiago.md +1 -1
  48. package/harness/opencode/agents/arka-sales-director-miguel.md +1 -1
  49. package/harness/opencode/agents/arka-strategy-director-tomas.md +1 -1
  50. package/harness/opencode/agents/arka-tech-director-francisca.md +1 -1
  51. package/harness/opencode/agents/arka-tech-lead-paulo.md +1 -1
  52. package/harness/opencode/agents/arka-video-producer-simao.md +1 -1
  53. package/harness/zed/.rules +2 -2
  54. package/installer/doctor.js +18 -6
  55. package/installer/fallback-model.js +80 -29
  56. package/installer/index.js +7 -4
  57. package/installer/update.js +5 -2
  58. package/knowledge/commands-keywords.json +29 -2
  59. package/knowledge/commands-registry.json +41 -5
  60. package/knowledge/skills-manifest.json +14 -1
  61. package/package.json +1 -1
  62. 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. 340 skills. Enterprise frameworks. Multi-runtime. One install.
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 266 skills, generated straight from the same sources the core uses. Inside Claude Code:
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
@@ -1,6 +1,6 @@
1
1
  # The ArkaOS Guide
2
2
 
3
- > v5.16.1 — 89 agents, 17 departments, 340 skills, 306 commands, 20 ADRs.
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` | 20 |
43
+ | `/content` | 21 |
44
44
  | `/dev` | 42 |
45
45
  | `/do` | 1 |
46
46
  | `/ecom` | 18 |
package/VERSION CHANGED
@@ -1 +1 @@
1
- 5.17.0
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. 340 skills. Multi-runtime. Dashboard. Knowledge RAG.
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 sentinel, warn-only — video production is opt-in.
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 hf_name in hyperframes hyperframes-core; do
377
- if [ -f "$HOME/.claude/skills/$hf_name/SKILL.md" ]; then
378
- HF_OK=true
379
- HF_DESC="Hyperframes skills installed ($hf_name)"
380
- break
381
- fi
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.
@@ -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); ADOPTED for any other value that is not the
468
- default — an array, an empty array, or the legacy string form."""
469
- from core.runtime.claude_code import DEFAULT_FALLBACK_MODELS
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
- DriftFinding(
475
- "settings:fallbackModel", DriftStatus.MISSING, "fallbackModel",
476
- "fallbackModel chain default not seeded",
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
- DriftFinding(
482
- "settings:fallbackModel", DriftStatus.ADOPTED, "fallbackModel",
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
 
@@ -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 DEFAULT_FALLBACK_MODELS
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 | adopted-skip
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 UTC, datetime
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", "expired")
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 | None:
66
- """The operator's vault via the canonical resolver, or ``None``.
67
-
68
- Resolution order is the canonical one from ``core.knowledge.vault``:
69
- ``knowledge.vaultPath`` in ``~/.arkaos/config.json`` first, then the
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(UTC).isoformat()
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 | None = None,
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 | None = None,
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
- if max_age_days is not None:
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, vault=vault))
115
+ results.append(run_single_job(claimed, root))
145
116
  return results
146
117
 
147
118
 
148
- def _job_timestamp(job_file: Path) -> float | None:
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, vault)
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 _release_claim(job_path: Path, root: Path) -> dict:
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
- with contextlib.suppress(OSError):
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
- with contextlib.suppress(OSError):
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
- with contextlib.suppress(OSError):
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 _run_pass(max_jobs: int, max_age_days: float | None) -> int | None:
324
- """One drain pass; prints results. Returns an exit code on fatal error."""
325
- try:
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
- rc = _run_pass(args.max_jobs, args.max_age_days)
347
- if rc is not None:
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, `opus` is Opus 5, and the
55
- # weakest lane (`haiku`/`fast`) is Sonnet 5 — ArkaOS never routes to Haiku.
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 — ids that no longer exist, or lanes ArkaOS no
59
- # longer routes to (Opus 4.x → Opus 5, Fable 5 → Fable 5.1, Haiku → Sonnet 5).
60
- # Normalised when a role is RESOLVED; the operator's models.yaml is never
61
- # rewritten. One stderr notice per process per id, so a stale pin is
62
- # visible without becoming noise.
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-4-8": "claude-opus-5",
67
- "claude-opus-4-8[1m]": "claude-opus-5[1m]",
68
- "claude-opus-4-7": "claude-opus-5",
69
- "claude-opus-4-6": "claude-opus-5",
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} (Runtime Sync 2026-09-03). Update the pin to silence this.",
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