cdx-manager 0.12.4 → 0.14.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # CDX Manager
2
2
 
3
- [![License](https://img.shields.io/badge/license-MIT-4C8BF5)](LICENSE) ![Version](https://img.shields.io/badge/version-v0.12.4-4C8BF5) ![Python](https://img.shields.io/badge/python-3.9%2B-3776AB?logo=python&logoColor=white)
3
+ [![License](https://img.shields.io/badge/license-MIT-4C8BF5)](LICENSE) ![Version](https://img.shields.io/badge/version-v0.14.0-4C8BF5) ![Python](https://img.shields.io/badge/python-3.9%2B-3776AB?logo=python&logoColor=white)
4
4
 
5
5
  **Run multiple Codex, Claude, Antigravity, and Ollama sessions from one terminal. Switch between accounts instantly.**
6
6
 
@@ -374,7 +374,7 @@ cdx history --summary --from 2026-05-01 --to 2026-05-28
374
374
  | `cdx reset <name> [--yes] [--json]` | Explicitly consume one available banked Codex rate-limit reset; confirmation is required unless `--yes` is supplied |
375
375
  | `cdx export <file> [--include-auth] [--sessions a,b] [--passphrase-env VAR\|--passphrase-stdin] [--force] [--json]` | Export sessions to a portable bundle; `--include-auth` encrypts auth data with a passphrase |
376
376
  | `cdx import <file> [--sessions a,b] [--passphrase-env VAR\|--passphrase-stdin] [--force\|--merge] [--allow-authless-force] [--json]` | Import sessions from a bundle into the current `CDX_HOME` |
377
- | `cdx doctor [--severity OK|WARN|FAIL[,OK|WARN|FAIL...]] [--json]` | Inspect CLI dependencies, provider CLI versions/capability hints, CDX_HOME permissions, missing state, orphan profiles, and pending quarantines; optionally filter issue severities |
377
+ | `cdx doctor [--severity OK|WARN|FAIL[,OK|WARN|FAIL...]] [--check-provider-flags] [--json]` | Inspect CLI dependencies, provider CLI versions/capability hints, CDX_HOME permissions, missing state, orphan profiles, and pending quarantines; optionally filter issue severities. `--check-provider-flags` additionally verifies that the CLI flags cdx maps for each permission are actually accepted by each provider's installed CLI |
378
378
  | `cdx repair [--dry-run] [--force] [--json]` | Plan or apply safe repairs for missing state files, quarantines, and orphan profiles |
379
379
  | `cdx view [--json] [--lan] [--lan-rw] [--focus <ref>] [--read] [--port <port>] [--host <host>] [--refresh-interval <s>] [--tls] [--tls-cert <path>] [--tls-key <path>] [--open] [--no-open]` | Open the Logics browser/focus viewer by delegating to `logics-manager view`; all viewer flags are forwarded; JSON mode reports diagnostics without launching it |
380
380
  | `cdx update [--check] [--yes] [--json] [--version TAG]` | Update cdx-manager using the installer that matches how it was installed |
@@ -384,7 +384,12 @@ cdx history --summary --from 2026-05-01 --to 2026-05-28
384
384
  | `cdx notify --next-ready [--poll seconds] [--once] [--schedule] [--refresh] [--json]` | Wait until the recommended session is usable, or schedule the next known reset notification |
385
385
  | `cdx next [--json] [--refresh]` | Select the best next assistant using the same priority logic as `cdx status` |
386
386
  | `cdx select --provider PROVIDER [--min-reasoning-effort minimal\|low\|medium\|high\|xhigh] [--min-power minimal\|low\|medium\|high\|xhigh] [--require-ready] [--refresh] --json` | Select a suitable session for headless automation |
387
- | `cdx run [session] --cwd PATH (--prompt-file PATH\|--prompt TEXT) [--provider PROVIDER] [--model MODEL] [--reasoning-effort minimal\|low\|medium\|high\|xhigh] [--power minimal\|low\|medium\|high\|xhigh] [--permission MODE] [--timeout-seconds N] --json` | Run one headless task and return a stable JSON result |
387
+ | `cdx run [session] --cwd PATH (--prompt-file PATH\|--prompt TEXT\|--prompt-file -) [--provider PROVIDER] [--model MODEL] [--reasoning-effort minimal\|low\|medium\|high\|xhigh] [--power minimal\|low\|medium\|high\|xhigh] [--permission MODE] [--timeout-seconds N] [--detach] [--refresh] --json` | Run one headless task and return a stable JSON result; `--detach` returns the `run_id` at launch without waiting, `--prompt-file -` reads the prompt from stdin |
388
+ | `cdx run-report <run_id> --json` | Full report, transcript metadata, and final payload for a run |
389
+ | `cdx run-status <run_id> --json` | Status of one run by id |
390
+ | `cdx run-tail <run_id> [--lines N] --json` | Last lines of a run's own output, while it is still running or after it finished |
391
+ | `cdx runs [--limit N] [--since 7d\|today\|DATE] --json` | List recent runs; `--since` returns every run completed after the cursor and ignores `--limit` |
392
+ | `cdx schema --json` | Publish the enums, mutually-exclusive argument groups, and error codes programmatic callers should validate against |
388
393
  | `cdx stats [name] [--since 7d\|today\|DATE] [--from DATE] [--to DATE] [--json]` | Aggregate launch counts, duration, and known headless token usage by session |
389
394
  | `cdx status [--json] [--refresh\|--cached] [--timeout SECONDS]` | Show token usage table for all sessions; `--cached` skips live provider probes and returns only stored status |
390
395
  | `cdx status --small [--refresh\|--cached] [--timeout SECONDS]` / `cdx status -s [--refresh\|--cached] [--timeout SECONDS]` | Show compact token usage table without provider, blocking quota, credits, and updated columns |
@@ -506,6 +511,130 @@ cdx stats work
506
511
  cdx select --provider codex --min-reasoning-effort low --require-ready --json
507
512
  ```
508
513
 
514
+ ### How cdx picks a session
515
+
516
+ `cdx select`, `cdx run --provider`, `cdx next`, the `cdx status` recommendation, and `cdx ready` all use one ranking. They used to use two that disagreed, so the answer depended on which command you asked.
517
+
518
+ **Candidate filters** exclude sessions before any ordering happens: disabled sessions, logged-out sessions (they cannot serve work), sessions of another provider when one is requested, sessions below a requested `--min-reasoning-effort`, and — when readiness is required — sessions with no availability left.
519
+
520
+ **Ordering** compares the remaining candidates factor by factor:
521
+
522
+ 1. **Usability** — usable now, then blocked with a known upcoming reset, then reset known, then nothing known.
523
+ 2. **Priority** — the `--priority` you set with `cdx set <name> --priority 0..100`, highest first.
524
+ 3. Then, for a session you can use now: **credits** (sessions *without* credits first, so included quota is spent before paid credits), **availability**, **reset time**. For a session you cannot use now the order is **reset time** first, then credits and availability — when you cannot use it, what matters is when it comes back.
525
+ 4. **Reasoning effort** — the lowest configured effort that still clears any requested floor, leaving stronger sessions free for work that needs them.
526
+ 5. **Session name**, so the order is deterministic.
527
+
528
+ `--priority` ranks sessions *within* a usability class, never across one. A high-priority session with no quota left does not outrank a usable one: priority expresses which usable session to prefer, not a way to route work somewhere it will fail.
529
+
530
+ `cdx select --json` reports `selection_policy` (built from the ranking itself, so it cannot describe an order the code does not apply) and `deciding_factor`/`reason`, naming the factor that actually separated the winner from the runner-up for that call — or reporting that it was the only candidate. Do not treat `selection_policy` as a stable identifier; it changes when the ranking changes, which is the point.
531
+
532
+ `cdx run --provider` selects from cached status by default. Pass `--refresh` to fetch status first. When a session is auto-selected without any recorded availability, the run payload carries a `session_selected_without_status` warning — that is different from a session known to be low, and on a freshly imported or long-idle set of sessions it is the ordinary case.
533
+
534
+ ### Verifying provider flag mappings
535
+
536
+ cdx translates each `--permission` value into concrete provider CLI flags. Those mappings are declarations about someone else's CLI, and a provider can drop or rename a flag without cdx noticing — which is exactly what happened with an `--experimental-yolo` flag mapped for ollama that the ollama CLI never had.
537
+
538
+ `cdx doctor --check-provider-flags` verifies them against the installed provider CLIs:
539
+
540
+ ```bash
541
+ cdx doctor --check-provider-flags --json
542
+ ```
543
+
544
+ For each configured provider it reports, per permission, whether every mapped flag is accepted. A rejected flag is a `FAIL` naming the provider, the permission, and the flag. A provider whose CLI is not installed, or whose help cannot be read, is a `WARN` — never an `OK`: "could not verify" is precisely the state a stale mapping hides in. A provider that maps no flags at all, such as ollama, reports `OK` with an empty mapping, so having nothing to check is distinguishable from having failed to check.
545
+
546
+ It is opt-in because it costs one provider CLI invocation per configured provider. When it is not requested, the default `cdx doctor` report says so with a `provider_permission_flags_unchecked` warning rather than omitting it, so a green report never implies the mappings were verified.
547
+
548
+ ### Programmatic Callers
549
+
550
+ `cdx run` blocks until the provider finishes, which suits a supervisor that can wait. Agents, MCP servers, and watchdogs usually cannot, so the surface below exists specifically for them: launch without waiting, watch progress, react to failures by code, and discover valid values instead of hard-coding them.
551
+
552
+ **Launch without waiting.** `--detach` returns as soon as the run is registered, with the `run_id` already assigned:
553
+
554
+ ```bash
555
+ cdx run work --cwd /path/to/repo --prompt-file task.md --detach --json
556
+ ```
557
+
558
+ The payload carries `detached: true`, `run_id`, `pid`, and the artifact paths — but no `exit_code`, `duration_seconds`, or `usage`, since the run has not finished. The detached process is put in its own session, so it survives the launcher exiting, including when cdx was invoked over an SSH command that returns immediately. It transitions its own registry entry to a terminal status, so `cdx runs` and `cdx run-status` stay accurate with nobody supervising.
559
+
560
+ Authentication is still resolved before the launch returns, so a login problem surfaces synchronously rather than silently in the background.
561
+
562
+ **Watch a run in progress.** `cdx run-status` reports only running/succeeded/failed; `run-tail` shows what the run is actually doing:
563
+
564
+ ```bash
565
+ cdx run-tail <run_id> --lines 50 --json
566
+ ```
567
+
568
+ It returns the last lines of the run's own `stdout_path` plus its current status, whether the run is still going or already finished. Lines are raw provider output, not summarized. Undecodable bytes come back with replacement characters rather than failing the call.
569
+
570
+ **Pipe an untrusted prompt.** `--prompt-file -` reads the prompt from standard input, so arbitrary text never has to reach a command line or a temporary file the caller must clean up:
571
+
572
+ ```bash
573
+ echo "$UNTRUSTED_PROMPT" | cdx run work --cwd /path/to/repo --prompt-file - --json
574
+ ```
575
+
576
+ It fails immediately if standard input is a terminal, rather than blocking at a silent prompt.
577
+
578
+ **Poll for completions with a cursor.** `cdx runs --since` returns every run that completed after the cursor, using the same cursor forms as `cdx history --since`:
579
+
580
+ ```bash
581
+ cdx runs --since 15m --json
582
+ cdx runs --since 2026-08-07T09:00:00Z --json
583
+ ```
584
+
585
+ A cursor is bounded by time, not by row count: `--limit` is ignored when `--since` is given (and says so in `warnings`). That is deliberate — capping a cursor query by row count would silently drop completions a caller had not yet seen, which is exactly the miss the cursor removes. Runs still in flight are not returned; they appear once they complete. Callers do not need to keep their own set of already-reported run ids.
586
+
587
+ **React to failures by code, not by message.** Argument failures report a specific `error.code` and name the offending arguments as data:
588
+
589
+ | `error.code` | Raised when |
590
+ | --- | --- |
591
+ | `missing_required_argument` | A required argument is absent |
592
+ | `mutually_exclusive_arguments` | Two arguments that cannot be combined were both given |
593
+ | `invalid_argument_value` | A constrained option got a value outside its accepted set |
594
+ | `argument_value_out_of_range` | A numeric argument fell outside its accepted range |
595
+ | `unknown_argument` | An unrecognized flag |
596
+
597
+ ```json
598
+ {
599
+ "ok": false,
600
+ "error": {
601
+ "code": "mutually_exclusive_arguments",
602
+ "message": "cdx run: cannot specify both a session name and --provider.",
603
+ "arguments": ["session", "--provider"],
604
+ "allowed_values": null
605
+ }
606
+ }
607
+ ```
608
+
609
+ Never branch on `error.message`; it is written for a human reading a terminal and may be reworded.
610
+
611
+ **Discover valid values.** `cdx schema --json` publishes the enums and argument constraints the parser itself validates against:
612
+
613
+ ```bash
614
+ cdx schema --json
615
+ ```
616
+
617
+ It returns accepted values for `permission` (with its aliases and canonical forms), `reasoning_effort`/`power`, `kind`, and `provider`, plus the declared mutually-exclusive argument groups and the error codes above. Validate against this rather than copying the lists into your own code — a hand-maintained copy drifts, and cdx will not tell you when it does.
618
+
619
+ **Surface run warnings.** The run payload's `warnings` list reports degradations a zero exit code would otherwise hide, on the successful path as much as the failing one:
620
+
621
+ ```json
622
+ {
623
+ "ok": true,
624
+ "exit_code": 0,
625
+ "warnings": [
626
+ {
627
+ "code": "network_disabled_by_permission",
628
+ "message": "codex runs at permission 'review' inside a sandbox with no network access, ...",
629
+ "provider": "codex",
630
+ "permission": "review"
631
+ }
632
+ ]
633
+ }
634
+ ```
635
+
636
+ Codex ties network access to its sandbox, so any permission below `full` leaves the run unable to resolve DNS — `gh`, `curl`, and package installs fail inside an otherwise successful run. Show these warnings to whoever reads your output rather than discarding them; a run that succeeds while quietly doing less is the case they exist for.
637
+
509
638
  ---
510
639
 
511
640
  ## Backup And Restore
@@ -0,0 +1,146 @@
1
+ # CDX Manager 0.14.0
2
+
3
+ > 0.13.0 was prepared but never published. Everything it contained ships here,
4
+ > so this release covers two versions' worth of work: the programmatic CLI
5
+ > surface for non-interactive callers, and the unification of session selection
6
+ > and accepted values.
7
+
8
+ ## Highlights
9
+
10
+ - `cdx run --detach` launches a run without waiting and returns its `run_id` immediately; `cdx run-tail` shows a run's live output.
11
+ - `cdx schema --json` publishes the enums, constraints, and error codes callers should validate against, instead of forcing them to copy lists that drift.
12
+ - Every command that picks a session now uses one ranking, and `--priority` finally counts in all of them.
13
+ - `cdx set --permission workspace-write` works, matching what `cdx run --permission` always accepted.
14
+ - New `cdx doctor --check-provider-flags` verifies that the CLI flags cdx maps for each permission actually exist in the provider CLIs.
15
+
16
+ ## Changes
17
+
18
+ ### Detached runs
19
+
20
+ `cdx run --detach --json` registers the run, starts the provider detached from the invoking process, and returns straight away with `detached: true`, the assigned `run_id`, `pid`, and the artifact paths. The child is placed in its own session so it outlives the launcher, including when cdx was invoked over an SSH command that returns immediately, and it transitions its own registry entry to a terminal status so `cdx runs` and `cdx run-status` stay accurate with nobody supervising. Authentication is still resolved before the launch returns, so a login problem surfaces synchronously rather than in the background.
21
+
22
+ Callers no longer have to poll `cdx runs` and guess which row belongs to the run they just started.
23
+
24
+ ### Live run output
25
+
26
+ `cdx run-tail <run_id> [--lines N] --json` returns the last lines of a run's recorded `stdout_path` plus its current status, while the run is still going or after it finished. Undecodable provider bytes are returned with replacement characters rather than failing the call. A run with no recorded output path reports `run_output_unavailable`, distinct from an unreadable one.
27
+
28
+ ### Specific argument error codes
29
+
30
+ Argument and usage failures previously collapsed into `invalid_request` carrying the full human usage line, so a missing `--cwd` was indistinguishable from passing a session name and `--provider` together. They now report `missing_required_argument`, `mutually_exclusive_arguments`, `invalid_argument_value`, `argument_value_out_of_range`, or `unknown_argument`, and the error object gained `arguments` and `allowed_values` fields naming the offending arguments as data. The human-readable message and the exit codes are unchanged.
31
+
32
+ `invalid_reasoning_effort` is kept for an unsupported `--reasoning-effort`/`--power` value; supplying two conflicting values now reports `mutually_exclusive_arguments`, since the two flags are aliases of one setting.
33
+
34
+ ### Schema discovery
35
+
36
+ `cdx schema --json` publishes the accepted values for `permission` (with aliases and canonical forms), `reasoning_effort`/`power`, `kind`, and `provider`, the declared mutually-exclusive argument groups, and the argument error codes. Values are read from the same definitions the parser validates against, and a test fails if the two disagree, so downstream callers can stop maintaining copies that drift.
37
+
38
+ ### Prompts on standard input
39
+
40
+ `cdx run --prompt-file -` reads the prompt from standard input, so a caller relaying arbitrary text no longer has to stage and clean up a temporary file or place the text on a command line. It fails immediately with `invalid_argument_value` if standard input is a terminal.
41
+
42
+ ### Completion cursor for runs
43
+
44
+ `cdx runs --since <cursor> --json` returns every run that completed after the cursor, accepting the same cursor forms as `cdx history --since`. The cursor bounds the result by time rather than row count: `--limit` is ignored when `--since` is given, and the payload says so in `warnings`. Capping a cursor query by row count would silently drop completions a polling caller had not yet seen. Runs still in flight are excluded until they complete.
45
+
46
+ ### Run warnings
47
+
48
+ The run payload's `warnings` list was declared in the contract but hardcoded empty. It is now populated, on the successful path as much as the failing one. The first warning, `network_disabled_by_permission`, fires when a Codex-backed run uses a permission below `full`: Codex ties network access to its sandbox, so the run cannot resolve DNS and network-dependent tooling fails inside a run that still exits zero. The condition is read from the same permission-to-flag mapping the launch spec uses, so adding a permission cannot leave it behind.
49
+
50
+ ### One session selection rule
51
+
52
+ `cdx select`, `cdx run --provider`, `cdx next`, the `cdx status` recommendation, and `cdx ready` used two independent rankings that disagreed. Each knew something the other did not: the headless one filtered by provider and honored `--priority`, the recommendation one understood credits and reset scheduling. Which session you got depended on which command you asked.
53
+
54
+ They are now one function in `src/session_ranking.py`, parameterized by what each caller needs. Candidate filters (disabled, logged out, provider, effort floor, readiness) are kept separate from ordering, because describing a filter as a sort stage is part of what made the old published policy wrong.
55
+
56
+ The merged ordering is: usability class, then `--priority`, then — for a usable session — credits, availability and reset; for a session that is not usable now, reset first, since when it comes back is what matters. Reasoning effort and session name break the remaining ties.
57
+
58
+ ### The priority setting counts everywhere
59
+
60
+ `cdx set <name> --priority 90` previously affected only `cdx select` and `cdx run --provider`. Status rows now carry the configured priority and effort, so `cdx next`, `cdx status`, and `cdx ready` honor it too.
61
+
62
+ Priority ranks sessions *within* a usability class, never across one: a high-priority session with no quota left does not outrank a usable one. Priority says which usable session to prefer, not where to send work that will fail there.
63
+
64
+ The `cdx status` recommendation line is now labelled `Recommended:` rather than `Priority:`. The old label named the whole recommendation after one of its inputs, so tuning `--priority` and seeing no change read as the setting being broken when availability or resets had decided instead.
65
+
66
+ ### Truthful selection reporting
67
+
68
+ `cdx select --json` built its `selection_policy` from a hardcoded string that omitted the reasoning-effort tie-break and described the readiness filter as a sort stage, and a `reason` that claimed availability decided even when priority or the alphabetical fallback did.
69
+
70
+ Both are now derived. `selection_policy` is built from the ranking itself and reports factors and filters separately; `deciding_factor` and `reason` name the factor that actually separated the winner from the runner-up for that call, or report that it was the only candidate. Do not treat `selection_policy` as a stable identifier — it changes when the ranking does, which is the point.
71
+
72
+ ### Selecting on fresh or missing status
73
+
74
+ `cdx run --provider` now accepts `--refresh` to fetch status before auto-selecting. Cached status remains the default, so ordinary runs do not each pay for a provider probe.
75
+
76
+ When auto-selection picks a session with no recorded availability, the run payload carries a `session_selected_without_status` warning. That is distinct from a session known to be low: on a freshly imported or long-idle set of sessions, choosing with no data at all is the ordinary case, and it was previously invisible.
77
+
78
+ ### One definition of the values cdx accepts
79
+
80
+ The reasoning-effort enum was defined in four places (two of them identical, one line apart in the same module) and the permission enum in two, with `cdx set` validating against one pair and `cdx run` against another. `config.py` now owns both, and every validator, normalizer, and `cdx schema --json` derives from them. One test asserts identity rather than equality; another fails if any module restates one of these sets.
81
+
82
+ ### Permission aliases work in cdx set
83
+
84
+ `cdx run --permission workspace-write` was accepted and normalized to `default`, while `cdx set <name> --permission workspace-write` failed with `Unsupported permission`. The same applied to `read-only` and `danger-full-access`, which are the provider-native spellings a user is most likely to have in hand, and `cdx schema --json` published the aliases without saying they applied to one command only.
85
+
86
+ `cdx set` and the `perm` alias command now accept them, storing the canonical value so nothing downstream has to learn aliases exist. They were added to `set` rather than removed from `run`, since removing them would break callers passing provider-native spellings today.
87
+
88
+ ### Verifying provider flag mappings
89
+
90
+ cdx maps each permission to concrete provider CLI flags, and nothing verified those mappings. GitHub issue #8 was exactly that: an `--experimental-yolo` flag mapped for ollama that the ollama CLI never had, undetected until a user hit it.
91
+
92
+ `cdx doctor --check-provider-flags` checks each configured provider's installed CLI for the flags cdx maps. A rejected flag is a `FAIL` naming the provider, the permission, and the flag. An uninstalled CLI or unreadable help is a `WARN`, never an `OK` — "could not verify" is the state that bug lived in. A provider that maps nothing, such as ollama, reports `OK` with an empty mapping, so having nothing to check is distinguishable from having failed to check.
93
+
94
+ It is opt-in because it costs a provider CLI invocation per provider, and the default report carries a `provider_permission_flags_unchecked` warning rather than omitting it, so a green doctor never implies the mappings were verified.
95
+
96
+ Antigravity and ollama permission mappings also moved out of inline branches into `LAUNCH_PERMISSION_ARGS`, so every provider's mapping is declared in one table the check can read. Ollama's entry is deliberately empty.
97
+
98
+ ## Internal structure
99
+
100
+ No behavior change, listed because the source layout is different if you have a patch in flight.
101
+
102
+ `cli_commands.py` (3313 lines) became nine per-domain modules under `src/commands/`, and `session_service.py` (1690 lines) became `session_service`, `session_helpers`, `session_status`, and `session_backup`. `create_session_service` went from a 1153-line factory holding 46 closures to 56 lines of assembly, with dependencies passed explicitly rather than captured.
103
+
104
+ Both original modules remain the import location for everything they exposed, so no caller changed. The split followed a coupling measurement in each case, and stopped where the measurement stopped: the session service's runtime and auth groups have no helpers of their own, so they were left in place rather than turned into files that would only re-import from elsewhere. That reasoning is recorded in `session_service`'s docstring, with a test that fails if someone finishes the job by instinct.
105
+
106
+ ## Behavior changes
107
+
108
+ - **`--priority` now outranks availability.** It previously sat below availability in the headless sort, so it only broke exact ties and was effectively inert. A session with lower availability but higher priority now wins within the same usability class. This is the change that makes the setting mean anything.
109
+ - **Ties on session name now sort ascending everywhere.** The recommendation ranking sorted names descending as a side effect of reversing its whole sort tuple, disagreeing with the headless ranking. The ascending order is kept.
110
+ - **Logged-out sessions are never selection candidates.** `cdx select` without `--require-ready` could previously return one. `--require-ready` remains stricter still: it requires a confirmed `authenticated` state, so a session whose auth has never been checked is not offered to `cdx run --provider`.
111
+ - `cdx status`'s recommendation line is labelled `Recommended:` instead of `Priority:`.
112
+ - `cdx select --json`'s `selection_policy` is now a structured object rather than an underscore-joined string.
113
+
114
+ ## Fixes
115
+
116
+ - `cdx --version` reported a stale release. `src/cli.py` restated the version as a fourth hardcoded copy alongside `VERSION`, `package.json`, and `pyproject.toml`, and nothing kept the four in step — it had been left at 0.12.4. The version is now resolved from the `VERSION` file in a checkout and from installed package metadata otherwise, and a test fails if any of the four (plus the README badge) disagree.
117
+ - The release validator was left scraping a `VERSION = "..."` literal out of `src/cli.py`, which the fix above had removed. It resolved an empty string and rejected every release with "Missing release version in: src/cli.py" — inside the publish workflow, after the tag was already public. It now reads the resolved version, and a test fails if any declaration site stops yielding one.
118
+ - Two tests still asserted the `--experimental-yolo` flag removed from ollama launches in 0.12.4 (GitHub issue #8); the launch behavior was already correct, the stale expectations are now aligned.
119
+ - `format_json_error` honors an error's declared code instead of always re-deriving one from the message prefix, so structured argument errors keep their code when they reach the CLI entry point.
120
+ - The detached-run test asserted the POSIX `start_new_session` unconditionally, so the Windows CI job failed on a code path that was correct. The assertion is now platform-aware, and Windows CI exercises the `creationflags` branch for the first time.
121
+
122
+ ## Review fixes
123
+
124
+ Found by a review pass over the programmatic CLI work, before release:
125
+
126
+ - The detached child's `CDX_RUN_ID` is consumed rather than read, so it is no longer inherited by the provider process. Left set, an agent making its own nested `cdx run` would have claimed the outer run's id, deleting its registry record and truncating its stdout, stderr, and transcript files.
127
+ - The detached child receives the resolved absolute `--cwd`; a relative path would have resolved against the child's own working directory.
128
+ - `--detach` now detaches correctly on Windows (`DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP`), and always re-invokes through `sys.executable -m` rather than `sys.argv[0]`, which is not reliably executable there.
129
+ - `network_disabled_by_permission` now fires when no `--permission` is given at all — the most common invocation, where `codex exec` still applies its own sandboxed default. It previously exempted exactly the case it was written for.
130
+ - `cdx run-tail` on a run that has not written its first byte returns `lines: []` instead of failing, so the launch-then-tail flow does not report a fatal error during that window. A missing output file on a *finished* run is still an error.
131
+ - `run-tail` drops a partial leading line when the read window opens mid-line, rather than returning a fragment as if it were a whole line.
132
+ - `cdx schema --json` publishes every error code the run commands emit, grouped by area, plus the warning codes; and `--reasoning-effort`/`--power` moved from `mutually_exclusive` to a new `must_match` group, since cdx accepts both when they agree.
133
+ - An empty `--power ""` blames `--power` instead of `--reasoning-effort`.
134
+ - `format_json_error` emits the same error field set as the run payload, so callers do not need two shapes.
135
+ - The prompt a detached launch stages for its child is deleted once the child reads it, instead of leaving a permanent cleartext copy in the session log directory.
136
+
137
+ ## Validation
138
+
139
+ - `npm run lint`
140
+ - `npm test` — 565 tests.
141
+ - CI green on ubuntu-latest and windows-latest.
142
+ - End-to-end: detached launch returning `run_id`, `run-tail` against the in-flight run after the launcher exited, terminal status reached without a supervisor, and `runs --since` reporting the completion.
143
+ - End-to-end: with one session at 80% availability and priority 0 and another at 40% and priority 90, `cdx select`, `cdx next`, and `cdx status` all name the second, and `cdx select` reports `decided on priority`.
144
+ - The issue #8 condition reproduced against the new provider flag check: mapping `--experimental-yolo` for codex fails the check by name. All four provider CLIs verified against their real binaries.
145
+ - `logics-manager lint --require-status`
146
+ - `logics-manager audit --group-by-doc` — 0 blocking, 0 warnings.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cdx-manager",
3
- "version": "0.12.4",
3
+ "version": "0.14.0",
4
4
  "description": "Terminal session manager for Codex and Claude accounts.",
5
5
  "license": "MIT",
6
6
  "author": "Alexandre Agostini",
@@ -30,7 +30,7 @@
30
30
  "checksums",
31
31
  "changelogs",
32
32
  "src",
33
- "!src/__pycache__",
33
+ "!src/**/__pycache__",
34
34
  "!src/**/*.pyc",
35
35
  "install.sh",
36
36
  "install.ps1",
@@ -46,7 +46,7 @@
46
46
  "test": "npm run test:py",
47
47
  "test:py": "node bin/python-runner.js -m pytest",
48
48
  "test:coverage": "node bin/python-runner.js -m pytest --cov=src --cov-report=term-missing",
49
- "lint": "node --check bin/cdx.js && node --check bin/python-runner.js && node bin/python-runner.js scripts/verify_project_docs.py && node bin/python-runner.js -m ruff check . && node bin/python-runner.js -m py_compile bin/cdx src/*.py scripts/*.py test/test_*_py.py",
49
+ "lint": "node --check bin/cdx.js && node --check bin/python-runner.js && node bin/python-runner.js scripts/verify_project_docs.py && node bin/python-runner.js -m ruff check . && node bin/python-runner.js -m py_compile bin/cdx src/*.py src/commands/*.py scripts/*.py test/*.py",
50
50
  "release:validate": "node bin/python-runner.js scripts/verify_release_checksums.py",
51
51
  "prepublishOnly": "npm run release:validate && npm run lint && npm test",
52
52
  "link": "npm link",
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "cdx-manager"
7
- version = "0.12.4"
7
+ version = "0.14.0"
8
8
  description = "Terminal session manager for Codex and Claude accounts."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
package/src/cli.py CHANGED
@@ -43,7 +43,9 @@ from .cli_commands import (
43
43
  handle_run,
44
44
  handle_run_report,
45
45
  handle_run_status,
46
+ handle_run_tail,
46
47
  handle_runs,
48
+ handle_schema,
47
49
  handle_select,
48
50
  handle_set,
49
51
  handle_stats,
@@ -78,7 +80,38 @@ from .status_view import (
78
80
  )
79
81
  from .update_check import check_for_update, check_logics_manager_for_update
80
82
 
81
- VERSION = "0.12.4"
83
+
84
+ def _resolve_version():
85
+ """The version, resolved rather than restated.
86
+
87
+ This was a hardcoded string, a fourth copy alongside VERSION, package.json,
88
+ and pyproject.toml, with nothing keeping the four in step — so
89
+ `cdx --version` could report a release it was not.
90
+
91
+ The VERSION file next to the package wins when it exists, because that only
92
+ happens in a checkout, where it is the thing being edited and the installed
93
+ metadata is whatever was last `pip install`ed. Installed packages have no
94
+ such file and fall through to their own metadata.
95
+ """
96
+ version_file = os.path.join(
97
+ os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "VERSION"
98
+ )
99
+ try:
100
+ with open(version_file, encoding="utf-8") as handle:
101
+ text = handle.read().strip()
102
+ if text:
103
+ return text
104
+ except OSError:
105
+ pass
106
+ try:
107
+ from importlib.metadata import version
108
+
109
+ return version("cdx-manager")
110
+ except Exception: # noqa: BLE001 - any resolution failure falls back below
111
+ return "0.0.0"
112
+
113
+
114
+ VERSION = _resolve_version()
82
115
 
83
116
  # Public surface: this module is a facade. Names below are imported above
84
117
  # purely to be re-exported (consumed by tests and external callers); listing
@@ -130,7 +163,9 @@ _COMMAND_HANDLERS = {
130
163
  "run": handle_run,
131
164
  "run-report": handle_run_report,
132
165
  "run-status": handle_run_status,
166
+ "run-tail": handle_run_tail,
133
167
  "runs": handle_runs,
168
+ "schema": handle_schema,
134
169
  "select": handle_select,
135
170
  "set": handle_set,
136
171
  "stats": handle_stats,
@@ -183,10 +218,12 @@ def _print_help(use_color=False):
183
218
  f" {_style('cdx next [--json] [--refresh]', '36', use_color)}",
184
219
  f" {_style('cdx reset <name> [--yes] [--json]', '36', use_color)}",
185
220
  f" {_style('cdx select --provider PROVIDER [--min-reasoning-effort minimal|low|medium|high|xhigh] [--min-power minimal|low|medium|high|xhigh] [--require-ready] [--refresh] --json', '36', use_color)}",
186
- f" {_style('cdx run [session] --cwd PATH (--prompt-file PATH|--prompt TEXT) [--provider PROVIDER] [--model MODEL] [--kind assistant|code-review] [--reasoning-effort minimal|low|medium|high|xhigh] [--power minimal|low|medium|high|xhigh] [--permission review|default|auto|full|workspace-write|read-only|danger-full-access] [--timeout-seconds N] --json', '36', use_color)}",
187
- f" {_style('cdx runs [--limit N] --json', '36', use_color)}",
221
+ f" {_style('cdx run [session] --cwd PATH (--prompt-file PATH|--prompt TEXT|--prompt-file -) [--provider PROVIDER] [--model MODEL] [--kind assistant|code-review] [--reasoning-effort minimal|low|medium|high|xhigh] [--power minimal|low|medium|high|xhigh] [--permission review|default|auto|full|workspace-write|read-only|danger-full-access] [--timeout-seconds N] [--detach] [--refresh] --json', '36', use_color)}",
222
+ f" {_style('cdx runs [--limit N] [--since 7d|today|DATE] --json', '36', use_color)}",
188
223
  f" {_style('cdx run-status <run_id> --json', '36', use_color)}",
189
224
  f" {_style('cdx run-report <run_id> --json', '36', use_color)}",
225
+ f" {_style('cdx run-tail <run_id> [--lines N] --json', '36', use_color)}",
226
+ f" {_style('cdx schema --json', '36', use_color)}",
190
227
  f" {_style('cdx context show|path|init|edit|clear|set|append [text...] [--json]', '36', use_color)}",
191
228
  f" {_style('cdx memory [--global|--project NAME_OR_PATH] [show|view|path|init|edit|clear|set|append|list] [text...] [--json]', '36', use_color)}",
192
229
  f" {_style('cdx disk [profiles] [--candidates] [--json]', '36', use_color)}",
@@ -216,7 +253,7 @@ def _print_help(use_color=False):
216
253
  f" {_style('cdx clean profiles (--tmp|--old-logs DAYS) [--yes] [--json]', '36', use_color)}",
217
254
  f" {_style('cdx export <file> [--include-auth] [--sessions a,b] [--passphrase-env VAR|--passphrase-stdin] [--force] [--json]', '36', use_color)}",
218
255
  f" {_style('cdx import <file> [--sessions a,b] [--passphrase-env VAR|--passphrase-stdin] [--force|--merge] [--allow-authless-force] [--json]', '36', use_color)}",
219
- f" {_style('cdx doctor [--severity OK|WARN|FAIL[,OK|WARN|FAIL...]] [--json]', '36', use_color)}",
256
+ f" {_style('cdx doctor [--severity OK|WARN|FAIL[,OK|WARN|FAIL...]] [--check-provider-flags] [--json]', '36', use_color)}",
220
257
  f" {_style('cdx repair [--dry-run] [--force] [--json]', '36', use_color)}",
221
258
  f" {_style('cdx view [--json]', '36', use_color)}",
222
259
  f" {_style('cdx update [all] [--check] [--yes] [--json] [--version TAG]', '36', use_color)}",
@@ -239,6 +276,25 @@ def wants_json(argv):
239
276
 
240
277
  def format_json_error(error):
241
278
  message = str(error)
279
+ # An error that already knows its own code keeps it. Only errors that never
280
+ # declared one fall back to sniffing the message prefix below.
281
+ declared = getattr(error, "code", None)
282
+ if declared:
283
+ allowed = getattr(error, "allowed", None)
284
+ # Same field set as run_result_payload's error object, so a caller does
285
+ # not need two shapes depending on which command raised.
286
+ return json.dumps({
287
+ "schema_version": API_SCHEMA_VERSION,
288
+ "ok": False,
289
+ "error": {
290
+ "source": "cdx",
291
+ "code": declared,
292
+ "message": message,
293
+ "arguments": list(getattr(error, "arguments", ()) or []),
294
+ "allowed_values": list(allowed) if allowed else None,
295
+ "exit_code": error.exit_code,
296
+ },
297
+ }, indent=2)
242
298
  code = "cdx_error"
243
299
  if message.startswith("Usage:"):
244
300
  code = "invalid_usage"
@@ -506,7 +562,11 @@ def main(argv, options=None):
506
562
  "service": service,
507
563
  "signal_emitter": signal_emitter,
508
564
  "spawn": spawn,
565
+ "spawn_detached": options.get("spawn_detached"),
509
566
  "spawn_headless": options.get("spawn_headless"),
567
+ # Stream `--prompt-file -` reads from. Distinct from "stdin", which is
568
+ # the {"isTTY": ...} descriptor the interactive paths use.
569
+ "prompt_stdin": options.get("prompt_stdin"),
510
570
  "spawn_sync": spawn_sync,
511
571
  "stdin_is_tty": stdin_is_tty,
512
572
  "version": VERSION,