cdx-manager 0.8.0 → 0.9.1
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 +3 -3
- package/changelogs/CHANGELOGS_0_9_0.md +14 -0
- package/changelogs/CHANGELOGS_0_9_1.md +36 -0
- package/checksums/release-archives.json +4 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/cli.py +16 -1
- package/src/cli_commands.py +124 -13
- package/src/cli_view.py +36 -7
- package/src/logics_view.py +9 -7
- package/src/provider_runtime.py +1 -1
- package/src/run_registry.py +195 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# CDX Manager
|
|
2
2
|
|
|
3
|
-
[](LICENSE) ](LICENSE)  
|
|
4
4
|
|
|
5
5
|
**Run multiple Codex, Claude, Antigravity, and Ollama sessions from one terminal. Switch between accounts instantly.**
|
|
6
6
|
|
|
@@ -42,7 +42,7 @@ One command to launch any session. Zero auth juggling.
|
|
|
42
42
|
- **Persistent launch settings.** Pin per-session power, permission, and fast-mode preferences once; `cdx` reapplies them on every launch until you unset them.
|
|
43
43
|
- **Launch history.** Inspect recent launches with provider, result, duration, working directory, launch settings, and transcript path.
|
|
44
44
|
- **Update prompts.** Periodic update checks surface `cdx update` directly in the `cdx`, `cdx status`, and launch output when a newer release is available. When `logics-manager` is installed, `cdx` can also suggest `logics-manager self-update`.
|
|
45
|
-
- **Logics viewer shortcut.** `cdx view` opens the Logics browser/focus viewer through `logics-manager view` when the companion CLI is installed
|
|
45
|
+
- **Logics viewer shortcut.** `cdx view` opens the Logics browser/focus viewer through `logics-manager view` when the companion CLI is installed. All viewer flags are forwarded: `--lan`, `--lan-rw`, `--focus <ref>`, `--read`, `--port`, `--host`, `--refresh-interval`, `--tls`, `--tls-cert`, `--tls-key`, `--open`, `--no-open`. `cdx view --json` reports availability and update diagnostics without opening the viewer.
|
|
46
46
|
- **Shared handoff context.** Keep a per-workspace Markdown context, or build one from a source session transcript, and install it into another assistant session before switching providers or accounts.
|
|
47
47
|
- **Passive status resolution.** Codex status is read from the local Codex app-server rate-limit API when available, with legacy transcript/history parsing kept as a fallback.
|
|
48
48
|
- **Session transcript capture.** Every launch is recorded to a local log file via `script`, giving you a full terminal transcript for each session.
|
|
@@ -352,7 +352,7 @@ cdx history --summary --from 2026-05-01 --to 2026-05-28
|
|
|
352
352
|
| `cdx import <file> [--sessions a,b] [--passphrase-env VAR] [--force] [--json]` | Import sessions from a bundle into the current `CDX_HOME` |
|
|
353
353
|
| `cdx doctor [--json]` | Inspect CLI dependencies, CDX_HOME permissions, missing state, orphan profiles, and pending quarantines |
|
|
354
354
|
| `cdx repair [--dry-run] [--force] [--json]` | Plan or apply safe repairs for missing state files, quarantines, and orphan profiles |
|
|
355
|
-
| `cdx view [--json]` | Open the Logics browser/focus viewer by delegating to `logics-manager view`; JSON mode reports diagnostics without launching it |
|
|
355
|
+
| `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 |
|
|
356
356
|
| `cdx update [--check] [--yes] [--json] [--version TAG]` | Update cdx-manager using the installer that matches how it was installed |
|
|
357
357
|
| `cdx ready [--refresh] [--json]` | Schedule an OS notification for the next cooling-down assistant that becomes ready, then return immediately |
|
|
358
358
|
| `cdx notify <name> --at-reset [--poll seconds] [--once] [--schedule] [--refresh] [--json]` | Wait for a session reset time or schedule an OS wake-up notification when due |
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# CDX Manager 0.9.0
|
|
2
|
+
|
|
3
|
+
## Highlights
|
|
4
|
+
|
|
5
|
+
- Adds an observable assistant run registry for CDX sessions.
|
|
6
|
+
- Generates the Logics corpus for assistant-run observability so follow-up work can be tracked from requests through closeout.
|
|
7
|
+
- Closes the observable run registry workflow chain after validation.
|
|
8
|
+
|
|
9
|
+
## Validation
|
|
10
|
+
|
|
11
|
+
- `npm run prepublishOnly`
|
|
12
|
+
- `npm pack --dry-run`
|
|
13
|
+
- `logics-manager lint --require-status`
|
|
14
|
+
- `logics-manager audit --legacy-cutoff-version 1.1.0 --group-by-doc`
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# CDX Manager 0.9.1
|
|
2
|
+
|
|
3
|
+
## Highlights
|
|
4
|
+
|
|
5
|
+
- `cdx view` now forwards all `logics-manager view` flags instead of rejecting them.
|
|
6
|
+
- Fixes a silent PATH resolution bug that could prevent `logics-manager` from being found.
|
|
7
|
+
|
|
8
|
+
## Changes
|
|
9
|
+
|
|
10
|
+
### cdx view: full flag passthrough
|
|
11
|
+
|
|
12
|
+
`cdx view` previously accepted only `--json` and raised a usage error for every other argument. All `logics-manager view` flags are now supported and forwarded transparently:
|
|
13
|
+
|
|
14
|
+
- `--lan` / `--lan-rw` — expose the viewer on the local network
|
|
15
|
+
- `--focus <ref>` / `--read` — open the viewer centered on a workflow document
|
|
16
|
+
- `--port <port>` / `--host <host>` — bind to a custom address
|
|
17
|
+
- `--refresh-interval <s>` — override the auto-refresh interval
|
|
18
|
+
- `--tls` / `--tls-cert <path>` / `--tls-key <path>` — serve over HTTPS
|
|
19
|
+
- `--open` / `--no-open` — control browser auto-open
|
|
20
|
+
|
|
21
|
+
### Bug fixes
|
|
22
|
+
|
|
23
|
+
- `resolve_logics_manager`: `shutil.which` was called with `path=""` when `env` had no `PATH` key, causing it to search nothing and always return `None`. Now falls back to the process PATH.
|
|
24
|
+
- `run_logics_viewer`: the viewer subprocess now inherits the full OS environment (`os.environ` merged with any `env` overrides) instead of receiving a partial dict that stripped all inherited variables.
|
|
25
|
+
|
|
26
|
+
### Docs
|
|
27
|
+
|
|
28
|
+
- README command table and feature description updated to list all forwarded viewer flags.
|
|
29
|
+
- `LOGICS_PROMPT` updated to reference `cdx view` instead of `logics-manager view` directly.
|
|
30
|
+
|
|
31
|
+
## Validation
|
|
32
|
+
|
|
33
|
+
- `npm run prepublishOnly`
|
|
34
|
+
- `npm pack --dry-run`
|
|
35
|
+
- `logics-manager lint --require-status`
|
|
36
|
+
- `logics-manager audit --legacy-cutoff-version 1.1.0 --group-by-doc`
|
|
@@ -64,6 +64,10 @@
|
|
|
64
64
|
"v0.8.0": {
|
|
65
65
|
"github_tarball_sha256": "62cd461f9e22636bb85d801e21209b1762859b6662623e74c01fd40c107ea29a",
|
|
66
66
|
"github_zip_sha256": "4674c8b61110f52b99c76312e9962202a2e8fc6928327f8927781f45eeaa980f"
|
|
67
|
+
},
|
|
68
|
+
"v0.9.0": {
|
|
69
|
+
"github_tarball_sha256": "809af5746e1287f4c5dc7a2fb7583e67e212aa250c45be905c32374e5dee26d6",
|
|
70
|
+
"github_zip_sha256": "9118f81645c00a4a660923a1ae290f48bd458af7ebfcf9f6580a3b91023b7c76"
|
|
67
71
|
}
|
|
68
72
|
}
|
|
69
73
|
}
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
package/src/cli.py
CHANGED
|
@@ -31,6 +31,9 @@ from .cli_commands import (
|
|
|
31
31
|
handle_repair,
|
|
32
32
|
handle_rename,
|
|
33
33
|
handle_run,
|
|
34
|
+
handle_run_report,
|
|
35
|
+
handle_run_status,
|
|
36
|
+
handle_runs,
|
|
34
37
|
handle_select,
|
|
35
38
|
handle_stats,
|
|
36
39
|
handle_status,
|
|
@@ -61,7 +64,7 @@ from .status_view import (
|
|
|
61
64
|
)
|
|
62
65
|
from .update_check import check_for_update, check_logics_manager_for_update
|
|
63
66
|
|
|
64
|
-
VERSION = "0.
|
|
67
|
+
VERSION = "0.9.1"
|
|
65
68
|
|
|
66
69
|
|
|
67
70
|
# ---------------------------------------------------------------------------
|
|
@@ -81,6 +84,9 @@ def _print_help(use_color=False):
|
|
|
81
84
|
f" {_style('cdx next [--json] [--refresh]', '36', use_color)}",
|
|
82
85
|
f" {_style('cdx select --provider PROVIDER [--min-reasoning-effort low|medium|high] [--min-power low|medium|high] [--require-ready] [--refresh] --json', '36', use_color)}",
|
|
83
86
|
f" {_style('cdx run [session] --cwd PATH (--prompt-file PATH|--prompt TEXT) [--provider PROVIDER] [--model MODEL] [--reasoning-effort low|medium|high] [--power low|medium|high] [--permission review|default|auto|full|workspace-write|read-only|danger-full-access] [--timeout-seconds N] --json', '36', use_color)}",
|
|
87
|
+
f" {_style('cdx runs [--limit N] --json', '36', use_color)}",
|
|
88
|
+
f" {_style('cdx run-status <run_id> --json', '36', use_color)}",
|
|
89
|
+
f" {_style('cdx run-report <run_id> --json', '36', use_color)}",
|
|
84
90
|
f" {_style('cdx context show|path|init|edit|clear|set [text...] [--json]', '36', use_color)}",
|
|
85
91
|
f" {_style('cdx config <name> [--json]', '36', use_color)}",
|
|
86
92
|
f" {_style('cdx configs [--json]', '36', use_color)}",
|
|
@@ -371,6 +377,15 @@ def main(argv, options=None):
|
|
|
371
377
|
if command == "run":
|
|
372
378
|
return handle_run(rest, ctx)
|
|
373
379
|
|
|
380
|
+
if command == "runs":
|
|
381
|
+
return handle_runs(rest, ctx)
|
|
382
|
+
|
|
383
|
+
if command == "run-status":
|
|
384
|
+
return handle_run_status(rest, ctx)
|
|
385
|
+
|
|
386
|
+
if command == "run-report":
|
|
387
|
+
return handle_run_report(rest, ctx)
|
|
388
|
+
|
|
374
389
|
if command == "login":
|
|
375
390
|
return handle_login(rest, ctx)
|
|
376
391
|
|
package/src/cli_commands.py
CHANGED
|
@@ -33,6 +33,7 @@ from .notify import (
|
|
|
33
33
|
)
|
|
34
34
|
from .provider_runtime import (
|
|
35
35
|
_ensure_session_authentication,
|
|
36
|
+
_headless_artifact_paths,
|
|
36
37
|
_list_launch_transcript_paths,
|
|
37
38
|
_normalize_reasoning_effort,
|
|
38
39
|
_probe_provider_auth,
|
|
@@ -41,6 +42,7 @@ from .provider_runtime import (
|
|
|
41
42
|
)
|
|
42
43
|
from .repair import format_repair_report, repair_health
|
|
43
44
|
from .backup_bundle import read_bundle_meta
|
|
45
|
+
from .run_registry import RunRegistry, build_code_review_report
|
|
44
46
|
from .run_usage import extract_run_usage
|
|
45
47
|
from .run_command import read_run_prompt, run_cdx_error_code, run_result_payload
|
|
46
48
|
from .status_view import _format_status_detail, _format_status_rows, format_priority_instruction, recommend_priority_rows
|
|
@@ -66,7 +68,10 @@ STATS_USAGE = "Usage: cdx stats [name] [--since 7d|today|DATE] [--from DATE] [--
|
|
|
66
68
|
LAST_USAGE = "Usage: cdx last [--json]"
|
|
67
69
|
SELECT_USAGE = "Usage: cdx select --provider PROVIDER [--min-reasoning-effort low|medium|high] [--min-power low|medium|high] [--require-ready] [--refresh] --json"
|
|
68
70
|
NEXT_USAGE = "Usage: cdx next [--json] [--refresh]"
|
|
69
|
-
RUN_USAGE = "Usage: cdx run [session] --cwd PATH (--prompt-file PATH|--prompt TEXT) [--provider PROVIDER] [--model MODEL] [--reasoning-effort low|medium|high] [--power low|medium|high] [--permission review|default|auto|full|workspace-write|read-only|danger-full-access] [--timeout-seconds N] --json"
|
|
71
|
+
RUN_USAGE = "Usage: cdx run [session] --cwd PATH (--prompt-file PATH|--prompt TEXT) [--provider PROVIDER] [--model MODEL] [--kind assistant|code-review] [--reasoning-effort low|medium|high] [--power low|medium|high] [--permission review|default|auto|full|workspace-write|read-only|danger-full-access] [--timeout-seconds N] --json"
|
|
72
|
+
RUNS_USAGE = "Usage: cdx runs [--limit N] --json"
|
|
73
|
+
RUN_STATUS_USAGE = "Usage: cdx run-status <run_id> --json"
|
|
74
|
+
RUN_REPORT_USAGE = "Usage: cdx run-report <run_id> --json"
|
|
70
75
|
API_SCHEMA_VERSION = 1
|
|
71
76
|
HANDOFF_TRANSCRIPT_CHARS = 120000
|
|
72
77
|
HANDOFF_NATIVE_TRANSCRIPT_CANDIDATES = 64
|
|
@@ -695,6 +700,7 @@ def _parse_run_args(args):
|
|
|
695
700
|
"--prompt": {"key": "prompt", "type": "str", "default": None},
|
|
696
701
|
"--provider": {"key": "provider", "type": "str", "default": None, "transform": lambda value: _parse_provider_filter(value, RUN_USAGE)},
|
|
697
702
|
"--model": {"key": "model", "type": "str", "default": None},
|
|
703
|
+
"--kind": {"key": "kind", "type": "str", "default": "assistant"},
|
|
698
704
|
"--reasoning-effort": {"key": "reasoning_effort", "type": "str", "default": None},
|
|
699
705
|
"--power": {"key": "power", "type": "str", "default": None},
|
|
700
706
|
"--permission": {"key": "permission", "type": "str", "default": None, "transform": _normalize_run_permission},
|
|
@@ -712,6 +718,8 @@ def _parse_run_args(args):
|
|
|
712
718
|
raise CdxError(RUN_USAGE)
|
|
713
719
|
if bool(parsed["prompt_file"]) == bool(parsed["prompt"]):
|
|
714
720
|
raise CdxError(RUN_USAGE)
|
|
721
|
+
if parsed["kind"] not in ("assistant", "code-review"):
|
|
722
|
+
raise CdxError(RUN_USAGE)
|
|
715
723
|
effort = _normalize_reasoning_effort(
|
|
716
724
|
reasoning_effort=parsed["reasoning_effort"],
|
|
717
725
|
power=parsed["power"],
|
|
@@ -723,6 +731,7 @@ def _parse_run_args(args):
|
|
|
723
731
|
"cwd": parsed["cwd"],
|
|
724
732
|
"prompt_file": parsed["prompt_file"],
|
|
725
733
|
"prompt": parsed["prompt"],
|
|
734
|
+
"kind": parsed["kind"],
|
|
726
735
|
"model": parsed["model"],
|
|
727
736
|
"permission": parsed["permission"],
|
|
728
737
|
"timeout_seconds": parsed["timeout_seconds"],
|
|
@@ -1338,7 +1347,65 @@ def handle_next(rest, ctx):
|
|
|
1338
1347
|
return 0
|
|
1339
1348
|
|
|
1340
1349
|
|
|
1350
|
+
def _parse_runs_args(rest):
|
|
1351
|
+
return _parse_flag_args(rest, {
|
|
1352
|
+
"--limit": {"key": "limit", "type": "str", "default": 20, "transform": _parse_positive_int},
|
|
1353
|
+
"--json": {"key": "json", "type": "bool", "default": False},
|
|
1354
|
+
}, RUNS_USAGE, max_positionals=0)
|
|
1355
|
+
|
|
1356
|
+
|
|
1357
|
+
def _parse_run_id_json_args(rest, usage):
|
|
1358
|
+
parsed = _parse_flag_args(rest, {
|
|
1359
|
+
"--json": {"key": "json", "type": "bool", "default": False},
|
|
1360
|
+
}, usage, positionals_key="ids", max_positionals=1)
|
|
1361
|
+
if not parsed["json"] or len(parsed["ids"]) != 1:
|
|
1362
|
+
raise CdxError(usage)
|
|
1363
|
+
return {"run_id": parsed["ids"][0], "json": True}
|
|
1364
|
+
|
|
1365
|
+
|
|
1366
|
+
def _run_registry(ctx):
|
|
1367
|
+
return RunRegistry(ctx["service"]["base_dir"])
|
|
1368
|
+
|
|
1369
|
+
|
|
1370
|
+
def handle_runs(rest, ctx):
|
|
1371
|
+
parsed = _parse_runs_args(rest)
|
|
1372
|
+
if not parsed["json"]:
|
|
1373
|
+
raise CdxError(RUNS_USAGE)
|
|
1374
|
+
runs = _run_registry(ctx).list(limit=parsed["limit"])
|
|
1375
|
+
_write_json(ctx, _json_success("runs", "Runs loaded.", runs=runs))
|
|
1376
|
+
return 0
|
|
1377
|
+
|
|
1378
|
+
|
|
1379
|
+
def handle_run_status(rest, ctx):
|
|
1380
|
+
parsed = _parse_run_id_json_args(rest, RUN_STATUS_USAGE)
|
|
1381
|
+
run = _run_registry(ctx).get(parsed["run_id"])
|
|
1382
|
+
if not run:
|
|
1383
|
+
_write_json(ctx, _json_failure("run-status", "run_not_found", f"Unknown run: {parsed['run_id']}"))
|
|
1384
|
+
return 1
|
|
1385
|
+
_write_json(ctx, _json_success("run-status", "Run loaded.", run=run))
|
|
1386
|
+
return 0
|
|
1387
|
+
|
|
1388
|
+
|
|
1389
|
+
def handle_run_report(rest, ctx):
|
|
1390
|
+
parsed = _parse_run_id_json_args(rest, RUN_REPORT_USAGE)
|
|
1391
|
+
run = _run_registry(ctx).get(parsed["run_id"])
|
|
1392
|
+
if not run:
|
|
1393
|
+
_write_json(ctx, _json_failure("run-report", "run_not_found", f"Unknown run: {parsed['run_id']}"))
|
|
1394
|
+
return 1
|
|
1395
|
+
_write_json(ctx, _json_success("run-report", "Run report loaded.", report={
|
|
1396
|
+
"run": run,
|
|
1397
|
+
"final_payload": run.get("final_payload"),
|
|
1398
|
+
"task_report": run.get("task_report"),
|
|
1399
|
+
"artifacts": run.get("artifacts") or {},
|
|
1400
|
+
"usage": run.get("usage"),
|
|
1401
|
+
"error": run.get("error"),
|
|
1402
|
+
}))
|
|
1403
|
+
return 0
|
|
1404
|
+
|
|
1405
|
+
|
|
1341
1406
|
def handle_run(rest, ctx):
|
|
1407
|
+
registry = None
|
|
1408
|
+
run_id = None
|
|
1342
1409
|
try:
|
|
1343
1410
|
parsed = _parse_run_args(rest)
|
|
1344
1411
|
session = None
|
|
@@ -1397,6 +1464,22 @@ def handle_run(rest, ctx):
|
|
|
1397
1464
|
signal_emitter=ctx.get("signal_emitter"),
|
|
1398
1465
|
trust_local_credentials=False,
|
|
1399
1466
|
)
|
|
1467
|
+
registry = _run_registry(ctx)
|
|
1468
|
+
artifacts = _headless_artifact_paths(run_session)
|
|
1469
|
+
run_id = artifacts["run_id"]
|
|
1470
|
+
registry.start(
|
|
1471
|
+
run_id,
|
|
1472
|
+
kind=parsed.get("kind"),
|
|
1473
|
+
session=run_session.get("name"),
|
|
1474
|
+
provider=run_session.get("provider"),
|
|
1475
|
+
model=parsed.get("model") or ((run_session.get("launch") or {}).get("model")),
|
|
1476
|
+
cwd=cwd,
|
|
1477
|
+
artifacts={
|
|
1478
|
+
"transcript_path": artifacts.get("transcript_path"),
|
|
1479
|
+
"stdout_path": artifacts.get("stdout_path"),
|
|
1480
|
+
"stderr_path": artifacts.get("stderr_path"),
|
|
1481
|
+
},
|
|
1482
|
+
)
|
|
1400
1483
|
run_info = _run_headless_provider_command(
|
|
1401
1484
|
run_session,
|
|
1402
1485
|
cwd=cwd,
|
|
@@ -1404,29 +1487,32 @@ def handle_run(rest, ctx):
|
|
|
1404
1487
|
initial_prompt=prompt,
|
|
1405
1488
|
timeout_seconds=parsed.get("timeout_seconds"),
|
|
1406
1489
|
spawn=ctx.get("spawn_headless") or ctx.get("spawn"),
|
|
1490
|
+
run_id=run_id,
|
|
1407
1491
|
)
|
|
1408
1492
|
usage = extract_run_usage(run_session.get("provider"), run_info.get("stdout_path"))
|
|
1409
1493
|
run_info = {**run_info, "usage": usage}
|
|
1410
1494
|
ok = run_info.get("returncode") == 0
|
|
1411
1495
|
if ok:
|
|
1496
|
+
final_payload = run_result_payload(API_SCHEMA_VERSION, True, parsed, run_session, run_info=run_info)
|
|
1497
|
+
task_report = build_code_review_report(run_id, final_payload) if parsed.get("kind") == "code-review" else None
|
|
1498
|
+
registry.finish(
|
|
1499
|
+
run_id,
|
|
1500
|
+
status="succeeded",
|
|
1501
|
+
final_payload=final_payload,
|
|
1502
|
+
run_info=run_info,
|
|
1503
|
+
task_report=task_report,
|
|
1504
|
+
)
|
|
1412
1505
|
ctx["service"]["record_launch_history"](session["name"], {
|
|
1413
1506
|
"status": "success",
|
|
1414
1507
|
"cwd": cwd,
|
|
1415
1508
|
"exit_code": 0,
|
|
1416
1509
|
**run_info,
|
|
1417
1510
|
})
|
|
1418
|
-
_write_json(ctx,
|
|
1511
|
+
_write_json(ctx, final_payload)
|
|
1419
1512
|
return 0
|
|
1420
1513
|
message = "Provider process timed out." if run_info.get("timed_out") else "Provider process exited with a non-zero status."
|
|
1421
1514
|
error = CdxError(message, run_info.get("returncode") or 1)
|
|
1422
|
-
|
|
1423
|
-
"status": "failed",
|
|
1424
|
-
"cwd": cwd,
|
|
1425
|
-
"error": str(error),
|
|
1426
|
-
"exit_code": error.exit_code,
|
|
1427
|
-
**run_info,
|
|
1428
|
-
})
|
|
1429
|
-
_write_json(ctx, run_result_payload(
|
|
1515
|
+
final_payload = run_result_payload(
|
|
1430
1516
|
API_SCHEMA_VERSION,
|
|
1431
1517
|
False,
|
|
1432
1518
|
parsed,
|
|
@@ -1435,11 +1521,27 @@ def handle_run(rest, ctx):
|
|
|
1435
1521
|
error=error,
|
|
1436
1522
|
error_source="provider",
|
|
1437
1523
|
error_code="provider_timeout" if run_info.get("timed_out") else "provider_failed",
|
|
1438
|
-
)
|
|
1524
|
+
)
|
|
1525
|
+
registry.finish(
|
|
1526
|
+
run_id,
|
|
1527
|
+
status="timed_out" if run_info.get("timed_out") else "failed",
|
|
1528
|
+
final_payload=final_payload,
|
|
1529
|
+
run_info=run_info,
|
|
1530
|
+
error=final_payload.get("error"),
|
|
1531
|
+
task_report=build_code_review_report(run_id, final_payload) if parsed.get("kind") == "code-review" else None,
|
|
1532
|
+
)
|
|
1533
|
+
ctx["service"]["record_launch_history"](session["name"], {
|
|
1534
|
+
"status": "failed",
|
|
1535
|
+
"cwd": cwd,
|
|
1536
|
+
"error": str(error),
|
|
1537
|
+
"exit_code": error.exit_code,
|
|
1538
|
+
**run_info,
|
|
1539
|
+
})
|
|
1540
|
+
_write_json(ctx, final_payload)
|
|
1439
1541
|
return error.exit_code or 1
|
|
1440
1542
|
except CdxError as error:
|
|
1441
1543
|
run_info = getattr(error, "run_info", None)
|
|
1442
|
-
|
|
1544
|
+
final_payload = run_result_payload(
|
|
1443
1545
|
API_SCHEMA_VERSION,
|
|
1444
1546
|
False,
|
|
1445
1547
|
locals().get("parsed", {}) or {},
|
|
@@ -1448,7 +1550,16 @@ def handle_run(rest, ctx):
|
|
|
1448
1550
|
error=error,
|
|
1449
1551
|
error_source="cdx",
|
|
1450
1552
|
error_code=run_cdx_error_code(error),
|
|
1451
|
-
)
|
|
1553
|
+
)
|
|
1554
|
+
if registry and run_id:
|
|
1555
|
+
registry.finish(
|
|
1556
|
+
run_id,
|
|
1557
|
+
status="failed",
|
|
1558
|
+
final_payload=final_payload,
|
|
1559
|
+
run_info=run_info,
|
|
1560
|
+
error=final_payload.get("error"),
|
|
1561
|
+
)
|
|
1562
|
+
_write_json(ctx, final_payload)
|
|
1452
1563
|
return error.exit_code
|
|
1453
1564
|
|
|
1454
1565
|
|
package/src/cli_view.py
CHANGED
|
@@ -12,7 +12,7 @@ from .logics_view import (
|
|
|
12
12
|
from .update_check import check_logics_manager_for_update
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
VIEW_USAGE = "Usage: cdx view [--json]"
|
|
15
|
+
VIEW_USAGE = "Usage: cdx view [--json] [--lan] [--lan-rw] [--focus <ref>] [--read] [--port <port>] [--host <host>] [--refresh-interval <s>] [--tls] [--open] [--no-open]"
|
|
16
16
|
API_SCHEMA_VERSION = 1
|
|
17
17
|
|
|
18
18
|
|
|
@@ -67,18 +67,44 @@ def _logics_manager_update_notice(ctx, env):
|
|
|
67
67
|
)
|
|
68
68
|
|
|
69
69
|
|
|
70
|
+
_VIEWER_FLAGS = {
|
|
71
|
+
"--lan", "--lan-rw", "--tls", "--read", "--open", "--no-open",
|
|
72
|
+
}
|
|
73
|
+
_VIEWER_VALUE_FLAGS = {
|
|
74
|
+
"--host", "--port", "--tls-cert", "--tls-key", "--refresh-interval", "--focus",
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _parse_view_args(rest):
|
|
79
|
+
json_flag = False
|
|
80
|
+
viewer_args = []
|
|
81
|
+
i = 0
|
|
82
|
+
while i < len(rest):
|
|
83
|
+
arg = rest[i]
|
|
84
|
+
if arg == "--json":
|
|
85
|
+
json_flag = True
|
|
86
|
+
elif arg in _VIEWER_FLAGS:
|
|
87
|
+
viewer_args.append(arg)
|
|
88
|
+
elif arg in _VIEWER_VALUE_FLAGS:
|
|
89
|
+
if i + 1 >= len(rest):
|
|
90
|
+
raise CdxError(f"{arg} requires a value. {VIEW_USAGE}")
|
|
91
|
+
viewer_args.extend([arg, rest[i + 1]])
|
|
92
|
+
i += 1
|
|
93
|
+
else:
|
|
94
|
+
raise CdxError(f"Unknown option: {arg}\n{VIEW_USAGE}")
|
|
95
|
+
i += 1
|
|
96
|
+
return json_flag, viewer_args
|
|
97
|
+
|
|
98
|
+
|
|
70
99
|
def handle_view(rest, ctx):
|
|
71
|
-
json_flag =
|
|
72
|
-
unknown = [arg for arg in rest if arg != "--json"]
|
|
73
|
-
if unknown:
|
|
74
|
-
raise CdxError(VIEW_USAGE)
|
|
100
|
+
json_flag, viewer_args = _parse_view_args(rest)
|
|
75
101
|
|
|
76
102
|
env = ctx.get("env")
|
|
77
103
|
cwd = ctx.get("cwd")
|
|
78
104
|
executable = resolve_logics_manager(env=env)
|
|
79
105
|
update_notice = _logics_manager_update_notice(ctx, env) if executable else None
|
|
80
106
|
failure = None if executable else missing_logics_manager_failure()
|
|
81
|
-
diagnostics = build_viewer_diagnostics(executable, cwd, update_notice=update_notice, failure=failure)
|
|
107
|
+
diagnostics = build_viewer_diagnostics(executable, cwd, update_notice=update_notice, failure=failure, extra_args=viewer_args)
|
|
82
108
|
warnings = _update_notice_warnings([update_notice])
|
|
83
109
|
|
|
84
110
|
if json_flag:
|
|
@@ -92,9 +118,12 @@ def handle_view(rest, ctx):
|
|
|
92
118
|
ctx["out"](f"{_warn(warning['message'], ctx['use_color'])}\n")
|
|
93
119
|
|
|
94
120
|
try:
|
|
95
|
-
result = run_logics_viewer(executable, cwd, env=env, runner=ctx.get("spawn_sync"))
|
|
121
|
+
result = run_logics_viewer(executable, cwd, env=env, extra_args=viewer_args, runner=ctx.get("spawn_sync"))
|
|
96
122
|
except FileNotFoundError as error:
|
|
97
123
|
raise CdxError(f"logics-manager is required for cdx view. {LOGICS_MANAGER_INSTALL_HINT}") from error
|
|
124
|
+
except KeyboardInterrupt:
|
|
125
|
+
ctx["out"]("\n")
|
|
126
|
+
return 130
|
|
98
127
|
returncode = getattr(result, "returncode", 0)
|
|
99
128
|
if returncode not in (0, None):
|
|
100
129
|
raise CdxError("logics-manager view failed.", exit_code=returncode)
|
package/src/logics_view.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import os
|
|
1
2
|
import shutil
|
|
2
3
|
import subprocess
|
|
3
4
|
|
|
@@ -7,11 +8,11 @@ LOGICS_MANAGER_INSTALL_HINT = "Install or update it with: npm install -g @grifhi
|
|
|
7
8
|
|
|
8
9
|
def resolve_logics_manager(env=None):
|
|
9
10
|
env = env or {}
|
|
10
|
-
return shutil.which("logics-manager", path=env.get("PATH"
|
|
11
|
+
return shutil.which("logics-manager", path=env.get("PATH") or None)
|
|
11
12
|
|
|
12
13
|
|
|
13
|
-
def build_viewer_diagnostics(executable, cwd, update_notice=None, failure=None):
|
|
14
|
-
command = [executable or "logics-manager", "view"]
|
|
14
|
+
def build_viewer_diagnostics(executable, cwd, update_notice=None, failure=None, extra_args=None):
|
|
15
|
+
command = [executable or "logics-manager", "view"] + (extra_args or [])
|
|
15
16
|
return {
|
|
16
17
|
"available": bool(executable),
|
|
17
18
|
"executable": executable,
|
|
@@ -29,9 +30,10 @@ def missing_logics_manager_failure():
|
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
|
|
32
|
-
def run_logics_viewer(executable, cwd, env=None, runner=None):
|
|
33
|
+
def run_logics_viewer(executable, cwd, env=None, extra_args=None, runner=None):
|
|
33
34
|
runner = runner or subprocess.run
|
|
34
|
-
argv = [executable, "view"]
|
|
35
|
+
argv = [executable, "view"] + (extra_args or [])
|
|
36
|
+
merged_env = {**os.environ, **(env or {})}
|
|
35
37
|
if runner is subprocess.run:
|
|
36
|
-
return subprocess.run(argv, cwd=cwd, env=
|
|
37
|
-
return runner(argv, cwd=cwd, env=
|
|
38
|
+
return subprocess.run(argv, cwd=cwd, env=merged_env)
|
|
39
|
+
return runner(argv, cwd=cwd, env=merged_env)
|
package/src/provider_runtime.py
CHANGED
|
@@ -22,7 +22,7 @@ RTK_PROMPT = (
|
|
|
22
22
|
LOGICS_PROMPT = (
|
|
23
23
|
"When `logics-manager` is available, prefer it for Logics workflow operations: use "
|
|
24
24
|
"`logics-manager status`, `health`, `audit`, and `lint` for workflow state and validation; "
|
|
25
|
-
"`
|
|
25
|
+
"`cdx view` for the browser viewer and focus workflows (supports `--lan`, `--focus <ref>`, `--port`, and all other viewer flags); "
|
|
26
26
|
"`logics-manager sync read-doc|list-docs|search-docs|context-pack` for bounded document context; "
|
|
27
27
|
"`logics-manager flow ...` for request/backlog/task lifecycle changes; and `logics-manager mcp ...` "
|
|
28
28
|
"when an MCP surface is the right fit."
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import tempfile
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def utc_now_iso():
|
|
8
|
+
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def registry_path(base_dir):
|
|
12
|
+
return os.path.join(base_dir, "runs.json")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _read_registry(path):
|
|
16
|
+
try:
|
|
17
|
+
with open(path, "r", encoding="utf-8") as handle:
|
|
18
|
+
data = json.load(handle)
|
|
19
|
+
except FileNotFoundError:
|
|
20
|
+
return {"schema_version": 1, "runs": []}
|
|
21
|
+
if not isinstance(data, dict):
|
|
22
|
+
return {"schema_version": 1, "runs": []}
|
|
23
|
+
runs = data.get("runs")
|
|
24
|
+
if not isinstance(runs, list):
|
|
25
|
+
runs = []
|
|
26
|
+
return {"schema_version": int(data.get("schema_version") or 1), "runs": [run for run in runs if isinstance(run, dict)]}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _write_registry(path, data):
|
|
30
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
31
|
+
fd, temp_path = tempfile.mkstemp(prefix=".runs-", suffix=".json", dir=os.path.dirname(path))
|
|
32
|
+
try:
|
|
33
|
+
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
|
34
|
+
json.dump(data, handle, indent=2, sort_keys=True)
|
|
35
|
+
handle.write("\n")
|
|
36
|
+
os.replace(temp_path, path)
|
|
37
|
+
finally:
|
|
38
|
+
try:
|
|
39
|
+
os.unlink(temp_path)
|
|
40
|
+
except FileNotFoundError:
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _is_pid_alive(pid):
|
|
45
|
+
if not pid:
|
|
46
|
+
return False
|
|
47
|
+
try:
|
|
48
|
+
os.kill(int(pid), 0)
|
|
49
|
+
return True
|
|
50
|
+
except (OSError, ValueError):
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _refresh_stale_runs(data):
|
|
55
|
+
now = utc_now_iso()
|
|
56
|
+
changed = False
|
|
57
|
+
for run in data["runs"]:
|
|
58
|
+
if run.get("status") != "running":
|
|
59
|
+
continue
|
|
60
|
+
pid = run.get("pid")
|
|
61
|
+
if pid and _is_pid_alive(pid):
|
|
62
|
+
continue
|
|
63
|
+
run["status"] = "stale"
|
|
64
|
+
run["ended_at"] = now
|
|
65
|
+
run["error"] = {"code": "stale_process", "message": "Run was marked running but no live provider process was found."}
|
|
66
|
+
changed = True
|
|
67
|
+
return changed
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _base_record(run_id, *, kind, session, provider, model, cwd, artifacts=None):
|
|
71
|
+
return {
|
|
72
|
+
"run_id": run_id,
|
|
73
|
+
"kind": kind or "assistant",
|
|
74
|
+
"status": "running",
|
|
75
|
+
"session": session,
|
|
76
|
+
"provider": provider,
|
|
77
|
+
"model": model,
|
|
78
|
+
"cwd": os.path.abspath(cwd),
|
|
79
|
+
"pid": None,
|
|
80
|
+
"started_at": utc_now_iso(),
|
|
81
|
+
"ended_at": None,
|
|
82
|
+
"duration_seconds": None,
|
|
83
|
+
"exit_code": None,
|
|
84
|
+
"usage": None,
|
|
85
|
+
"artifacts": dict(artifacts or {}),
|
|
86
|
+
"error": None,
|
|
87
|
+
"task_report": None,
|
|
88
|
+
"final_payload": None,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class RunRegistry:
|
|
93
|
+
def __init__(self, base_dir):
|
|
94
|
+
self.path = registry_path(base_dir)
|
|
95
|
+
|
|
96
|
+
def start(self, run_id, *, kind, session, provider, model, cwd, artifacts=None):
|
|
97
|
+
data = _read_registry(self.path)
|
|
98
|
+
data["runs"] = [run for run in data["runs"] if run.get("run_id") != run_id]
|
|
99
|
+
record = _base_record(run_id, kind=kind, session=session, provider=provider, model=model, cwd=cwd, artifacts=artifacts)
|
|
100
|
+
data["runs"].insert(0, record)
|
|
101
|
+
_write_registry(self.path, data)
|
|
102
|
+
return record
|
|
103
|
+
|
|
104
|
+
def finish(self, run_id, *, status, final_payload=None, run_info=None, error=None, task_report=None):
|
|
105
|
+
data = _read_registry(self.path)
|
|
106
|
+
now = utc_now_iso()
|
|
107
|
+
for run in data["runs"]:
|
|
108
|
+
if run.get("run_id") != run_id:
|
|
109
|
+
continue
|
|
110
|
+
run["status"] = status
|
|
111
|
+
run["ended_at"] = now
|
|
112
|
+
if run.get("started_at"):
|
|
113
|
+
try:
|
|
114
|
+
start = datetime.fromisoformat(str(run["started_at"]).replace("Z", "+00:00"))
|
|
115
|
+
end = datetime.fromisoformat(now.replace("Z", "+00:00"))
|
|
116
|
+
run["duration_seconds"] = (end - start).total_seconds()
|
|
117
|
+
except ValueError:
|
|
118
|
+
run["duration_seconds"] = None
|
|
119
|
+
if run_info:
|
|
120
|
+
run["pid"] = run_info.get("pid")
|
|
121
|
+
run["exit_code"] = run_info.get("returncode")
|
|
122
|
+
run["artifacts"] = {
|
|
123
|
+
**(run.get("artifacts") or {}),
|
|
124
|
+
"transcript_path": run_info.get("transcript_path"),
|
|
125
|
+
"stdout_path": run_info.get("stdout_path"),
|
|
126
|
+
"stderr_path": run_info.get("stderr_path"),
|
|
127
|
+
}
|
|
128
|
+
if final_payload:
|
|
129
|
+
run["usage"] = final_payload.get("usage")
|
|
130
|
+
run["final_payload"] = final_payload
|
|
131
|
+
if error:
|
|
132
|
+
run["error"] = error
|
|
133
|
+
if task_report:
|
|
134
|
+
run["task_report"] = task_report
|
|
135
|
+
_write_registry(self.path, data)
|
|
136
|
+
return run
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
def list(self, limit=20):
|
|
140
|
+
data = _read_registry(self.path)
|
|
141
|
+
changed = _refresh_stale_runs(data)
|
|
142
|
+
if changed:
|
|
143
|
+
_write_registry(self.path, data)
|
|
144
|
+
return data["runs"][: max(0, int(limit or 20))]
|
|
145
|
+
|
|
146
|
+
def get(self, run_id):
|
|
147
|
+
data = _read_registry(self.path)
|
|
148
|
+
changed = _refresh_stale_runs(data)
|
|
149
|
+
if changed:
|
|
150
|
+
_write_registry(self.path, data)
|
|
151
|
+
for run in data["runs"]:
|
|
152
|
+
if run.get("run_id") == run_id:
|
|
153
|
+
return run
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def read_text_file(path, limit=120000):
|
|
158
|
+
if not path:
|
|
159
|
+
return ""
|
|
160
|
+
try:
|
|
161
|
+
with open(path, "r", encoding="utf-8", errors="replace") as handle:
|
|
162
|
+
return handle.read(limit)
|
|
163
|
+
except OSError:
|
|
164
|
+
return ""
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def build_code_review_report(run_id, final_payload):
|
|
168
|
+
stdout = read_text_file(final_payload.get("stdout_path") if final_payload else None)
|
|
169
|
+
parsed = None
|
|
170
|
+
try:
|
|
171
|
+
candidate = json.loads(stdout or "{}")
|
|
172
|
+
if isinstance(candidate, dict):
|
|
173
|
+
parsed = candidate
|
|
174
|
+
except json.JSONDecodeError:
|
|
175
|
+
parsed = None
|
|
176
|
+
findings = parsed.get("findings") if parsed else []
|
|
177
|
+
if not isinstance(findings, list):
|
|
178
|
+
findings = []
|
|
179
|
+
summary = parsed.get("summary") if parsed else None
|
|
180
|
+
next_steps = parsed.get("next_steps") if parsed else None
|
|
181
|
+
if not isinstance(next_steps, list):
|
|
182
|
+
next_steps = []
|
|
183
|
+
return {
|
|
184
|
+
"schema_version": 1,
|
|
185
|
+
"kind": "code-review",
|
|
186
|
+
"run_id": run_id,
|
|
187
|
+
"summary": summary or "Code review completed. Structured findings were not emitted by the provider.",
|
|
188
|
+
"findings": [finding for finding in findings if isinstance(finding, dict)],
|
|
189
|
+
"next_steps": [str(step) for step in next_steps],
|
|
190
|
+
"artifacts": {
|
|
191
|
+
"transcript_path": final_payload.get("transcript_path") if final_payload else None,
|
|
192
|
+
"stdout_path": final_payload.get("stdout_path") if final_payload else None,
|
|
193
|
+
"stderr_path": final_payload.get("stderr_path") if final_payload else None,
|
|
194
|
+
},
|
|
195
|
+
}
|