cdx-manager 0.7.8 → 0.9.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 +14 -3
- package/changelogs/CHANGELOGS_0_7_8.md +12 -0
- package/changelogs/CHANGELOGS_0_8_0.md +39 -0
- package/changelogs/CHANGELOGS_0_9_0.md +14 -0
- package/checksums/release-archives.json +8 -0
- package/package.json +4 -3
- package/pyproject.toml +1 -1
- package/src/cli.py +22 -2
- package/src/cli_commands.py +125 -13
- package/src/cli_view.py +104 -0
- package/src/logics_view.py +37 -0
- package/src/run_registry.py +195 -0
- package/src/session_service.py +1 -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,6 +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; `cdx view --json` reports availability and update diagnostics without opening the viewer.
|
|
45
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.
|
|
46
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.
|
|
47
48
|
- **Session transcript capture.** Every launch is recorded to a local log file via `script`, giving you a full terminal transcript for each session.
|
|
@@ -136,7 +137,7 @@ For a specific version:
|
|
|
136
137
|
|
|
137
138
|
```bash
|
|
138
139
|
curl -fsSL https://raw.githubusercontent.com/AlexAgo83/cdx-manager/main/install.sh -o install.sh
|
|
139
|
-
CDX_VERSION=v0.
|
|
140
|
+
CDX_VERSION=v0.8.0 sh install.sh
|
|
140
141
|
```
|
|
141
142
|
|
|
142
143
|
From source:
|
|
@@ -189,6 +190,13 @@ Security note:
|
|
|
189
190
|
- Prefer `npm`, `pipx`, or `uv` when you want registry-backed install flows.
|
|
190
191
|
- If you use the standalone script, download it first, inspect it, and prefer a release with an official checksum entry.
|
|
191
192
|
|
|
193
|
+
Release maintainer note:
|
|
194
|
+
|
|
195
|
+
- Before publishing npm or PyPI packages, run `npm run release:validate`.
|
|
196
|
+
- The release tag must match `package.json`, `pyproject.toml`, `src/cli.py`, and `VERSION`.
|
|
197
|
+
- `checksums/release-archives.json` must include the matching `vX.Y.Z` entry with both `github_tarball_sha256` and `github_zip_sha256`.
|
|
198
|
+
- Use `python3 scripts/update_release_checksums.py --tag vX.Y.Z` after the GitHub tag archives exist, commit the checksum update to `main`, then publish the GitHub release only after `npm run release:validate`, `npm run lint`, and `npm test` pass.
|
|
199
|
+
|
|
192
200
|
### Environment
|
|
193
201
|
|
|
194
202
|
By default, `cdx` stores all data under `~/.cdx/`. Override with:
|
|
@@ -344,6 +352,7 @@ cdx history --summary --from 2026-05-01 --to 2026-05-28
|
|
|
344
352
|
| `cdx import <file> [--sessions a,b] [--passphrase-env VAR] [--force] [--json]` | Import sessions from a bundle into the current `CDX_HOME` |
|
|
345
353
|
| `cdx doctor [--json]` | Inspect CLI dependencies, CDX_HOME permissions, missing state, orphan profiles, and pending quarantines |
|
|
346
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 |
|
|
347
356
|
| `cdx update [--check] [--yes] [--json] [--version TAG]` | Update cdx-manager using the installer that matches how it was installed |
|
|
348
357
|
| `cdx ready [--refresh] [--json]` | Schedule an OS notification for the next cooling-down assistant that becomes ready, then return immediately |
|
|
349
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 |
|
|
@@ -387,6 +396,7 @@ Commands with machine-readable output:
|
|
|
387
396
|
- `cdx last --json`
|
|
388
397
|
- `cdx doctor --json`
|
|
389
398
|
- `cdx repair --json`
|
|
399
|
+
- `cdx view --json`
|
|
390
400
|
- `cdx update --json`
|
|
391
401
|
- `cdx ready --json`
|
|
392
402
|
- `cdx notify ... --json`
|
|
@@ -500,7 +510,8 @@ Notes:
|
|
|
500
510
|
|
|
501
511
|
- `npm test`: run the Python test suite
|
|
502
512
|
- `npm run test:py`: run the Python unit tests through the portable launcher
|
|
503
|
-
- `npm run lint`: check the Node launcher and byte-compile the Python sources and tests
|
|
513
|
+
- `npm run lint`: check project guidance, the Node launcher, and byte-compile the Python sources, scripts, and tests
|
|
514
|
+
- `npm run release:validate`: verify version alignment and required GitHub release archive checksum metadata before publication
|
|
504
515
|
- `npm run link`: link `cdx` globally for local development (`npm link`)
|
|
505
516
|
- `npm run unlink`: remove the global link
|
|
506
517
|
|
|
@@ -5,6 +5,7 @@ Release date: 2026-06-08
|
|
|
5
5
|
## Launch Guidance
|
|
6
6
|
|
|
7
7
|
- Added update guidance for `logics-manager` so `cdx` can surface a newer companion CLI version alongside the existing `cdx-manager` update notice.
|
|
8
|
+
- Added `cdx view` as a thin shortcut for `logics-manager view`, plus `cdx view --json` diagnostics for companion availability and update suggestions.
|
|
8
9
|
- Added RTK launch preference handling so noisy assistant shell commands can be wrapped with `rtk` when the session setting asks for filtered command output.
|
|
9
10
|
- Extended provider launch metadata and health checks so companion tool hints appear without making `logics-manager` or RTK hard runtime dependencies.
|
|
10
11
|
|
|
@@ -16,9 +17,20 @@ Release date: 2026-06-08
|
|
|
16
17
|
|
|
17
18
|
- Updated package metadata, CLI version output, README badge, pinned installer example, and release changelog to `v0.7.8`.
|
|
18
19
|
|
|
20
|
+
## Release Governance
|
|
21
|
+
|
|
22
|
+
- Added `npm run release:validate` as the required pre-publication gate for version alignment and GitHub archive checksum metadata.
|
|
23
|
+
- The npm and PyPI publication workflows now run the checksum/version gate before registry upload.
|
|
24
|
+
- Release prep now requires generating `checksums/release-archives.json` entries for the matching `vX.Y.Z` tag before publication.
|
|
25
|
+
|
|
26
|
+
## Maintainability
|
|
27
|
+
|
|
28
|
+
- Extracted the `cdx view` command domain into a dedicated module while keeping the existing CLI routing and JSON diagnostics unchanged.
|
|
29
|
+
|
|
19
30
|
## Validation and Regression Evidence
|
|
20
31
|
|
|
21
32
|
- `npm run lint`
|
|
33
|
+
- `npm run release:validate`
|
|
22
34
|
- `npm test`
|
|
23
35
|
- `python -m unittest discover -s test -p 'test_*_py.py'`
|
|
24
36
|
- `python3 -m logics_manager lint --require-status`
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Changelog (`0.7.8 -> 0.8.0`)
|
|
2
|
+
|
|
3
|
+
Release date: 2026-06-08
|
|
4
|
+
|
|
5
|
+
## Release Governance
|
|
6
|
+
|
|
7
|
+
- Added a pre-publication release checksum gate that validates version alignment across `package.json`, `pyproject.toml`, `src/cli.py`, and `VERSION`.
|
|
8
|
+
- Publication workflows now require matching GitHub archive checksum metadata before npm or PyPI upload.
|
|
9
|
+
- Documented the release ordering for version bump, tag/archive checksum generation, checksum commit, and publication.
|
|
10
|
+
|
|
11
|
+
## Logics Workflow
|
|
12
|
+
|
|
13
|
+
- Versioned `LOGICS.md` as normal project guidance instead of treating it as an ignored local artifact.
|
|
14
|
+
- Added project documentation validation to ensure core Logics command families stay documented.
|
|
15
|
+
- Added `cdx view` as a thin shortcut for `logics-manager view`.
|
|
16
|
+
- Added `cdx view --json` diagnostics for companion availability, delegated command details, failure reason, and update suggestions.
|
|
17
|
+
|
|
18
|
+
## Maintainability
|
|
19
|
+
|
|
20
|
+
- Extracted the `cdx view` command domain into `src/cli_view.py` while keeping `src/cli_commands.py` as a compatibility facade for handler routing.
|
|
21
|
+
|
|
22
|
+
## Release Metadata
|
|
23
|
+
|
|
24
|
+
- Updated package metadata, CLI version output, README badge, pinned installer example, and release changelog to `v0.8.0`.
|
|
25
|
+
|
|
26
|
+
## Validation and Regression Evidence
|
|
27
|
+
|
|
28
|
+
- `python -m py_compile bin/cdx src/*.py test/test_*_py.py`
|
|
29
|
+
- `python -m unittest discover -s test -p 'test_*_py.py'`
|
|
30
|
+
- `npm pack --dry-run`
|
|
31
|
+
- `npm run lint`
|
|
32
|
+
- `npm test`
|
|
33
|
+
- `logics-manager lint --require-status`
|
|
34
|
+
- `logics-manager audit`
|
|
35
|
+
- `git diff --check`
|
|
36
|
+
- `node bin/cdx.js --version`
|
|
37
|
+
- `python3 bin/cdx --version`
|
|
38
|
+
- `python -m build`
|
|
39
|
+
- `python -m twine check dist/*`
|
|
@@ -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`
|
|
@@ -56,6 +56,14 @@
|
|
|
56
56
|
"v0.7.6": {
|
|
57
57
|
"github_tarball_sha256": "cf6390c2071c710edd7c55d89fd75914e1d94ae5a62330d6fe15029484f88c13",
|
|
58
58
|
"github_zip_sha256": "f58be3078daaa9523053dab7b59d681dfb359d5f2a94276aa46000dd7f1360ed"
|
|
59
|
+
},
|
|
60
|
+
"v0.7.8": {
|
|
61
|
+
"github_tarball_sha256": "8afa0c1f0293a973973158d249aaa627d3aede785cd5a36941be067633580e2a",
|
|
62
|
+
"github_zip_sha256": "207e000dc1192d12324616cc4671f42cf07c22e2eb895556117b681ab5a6ab93"
|
|
63
|
+
},
|
|
64
|
+
"v0.8.0": {
|
|
65
|
+
"github_tarball_sha256": "62cd461f9e22636bb85d801e21209b1762859b6662623e74c01fd40c107ea29a",
|
|
66
|
+
"github_zip_sha256": "4674c8b61110f52b99c76312e9962202a2e8fc6928327f8927781f45eeaa980f"
|
|
59
67
|
}
|
|
60
68
|
}
|
|
61
69
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cdx-manager",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Terminal session manager for Codex and Claude accounts.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Alexandre Agostini",
|
|
@@ -44,8 +44,9 @@
|
|
|
44
44
|
"scripts": {
|
|
45
45
|
"test": "npm run test:py",
|
|
46
46
|
"test:py": "node bin/python-runner.js -m unittest discover -s test -p test_*_py.py",
|
|
47
|
-
"lint": "node --check bin/cdx.js && node --check bin/python-runner.js && node bin/python-runner.js -m py_compile bin/cdx src/*.py test/test_*_py.py",
|
|
48
|
-
"
|
|
47
|
+
"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 py_compile bin/cdx src/*.py scripts/*.py test/test_*_py.py",
|
|
48
|
+
"release:validate": "node bin/python-runner.js scripts/verify_release_checksums.py",
|
|
49
|
+
"prepublishOnly": "npm run release:validate && npm run lint && npm test",
|
|
49
50
|
"link": "npm link",
|
|
50
51
|
"unlink": "npm unlink -g cdx-manager"
|
|
51
52
|
}
|
package/pyproject.toml
CHANGED
package/src/cli.py
CHANGED
|
@@ -31,12 +31,16 @@ 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,
|
|
37
40
|
handle_set,
|
|
38
41
|
handle_unset,
|
|
39
42
|
handle_update,
|
|
43
|
+
handle_view,
|
|
40
44
|
)
|
|
41
45
|
from .cli_render import (
|
|
42
46
|
_format_sessions,
|
|
@@ -60,7 +64,7 @@ from .status_view import (
|
|
|
60
64
|
)
|
|
61
65
|
from .update_check import check_for_update, check_logics_manager_for_update
|
|
62
66
|
|
|
63
|
-
VERSION = "0.
|
|
67
|
+
VERSION = "0.9.0"
|
|
64
68
|
|
|
65
69
|
|
|
66
70
|
# ---------------------------------------------------------------------------
|
|
@@ -80,6 +84,9 @@ def _print_help(use_color=False):
|
|
|
80
84
|
f" {_style('cdx next [--json] [--refresh]', '36', use_color)}",
|
|
81
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)}",
|
|
82
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)}",
|
|
83
90
|
f" {_style('cdx context show|path|init|edit|clear|set [text...] [--json]', '36', use_color)}",
|
|
84
91
|
f" {_style('cdx config <name> [--json]', '36', use_color)}",
|
|
85
92
|
f" {_style('cdx configs [--json]', '36', use_color)}",
|
|
@@ -104,6 +111,7 @@ def _print_help(use_color=False):
|
|
|
104
111
|
f" {_style('cdx import <file> [--sessions a,b] [--passphrase-env VAR] [--force] [--json]', '36', use_color)}",
|
|
105
112
|
f" {_style('cdx doctor [--json]', '36', use_color)}",
|
|
106
113
|
f" {_style('cdx repair [--dry-run] [--force] [--json]', '36', use_color)}",
|
|
114
|
+
f" {_style('cdx view [--json]', '36', use_color)}",
|
|
107
115
|
f" {_style('cdx update [--check] [--yes] [--json] [--version TAG]', '36', use_color)}",
|
|
108
116
|
f" {_style('cdx ready [--refresh] [--json]', '36', use_color)}",
|
|
109
117
|
f" {_style('cdx notify <name> --at-reset [--schedule] [--refresh] [--json]', '36', use_color)}",
|
|
@@ -275,7 +283,7 @@ def main(argv, options=None):
|
|
|
275
283
|
"version": VERSION,
|
|
276
284
|
"cwd": options.get("cwd") or os.getcwd(),
|
|
277
285
|
"update_notices": _get_update_notices(service, env, options) if command not in (
|
|
278
|
-
"add", "cp", "ren", "rename", "mv", "rmv", "clean", "doctor", "repair", "update", "ready", "notify", "next", "context", "config", "configs", "set", "unset", "power", "perm", "fast", "model", "history", "stats", "handoff", "login", "logout", "disable", "enable", "export", "import", "select", "run", "help", "version"
|
|
286
|
+
"add", "cp", "ren", "rename", "mv", "rmv", "clean", "doctor", "repair", "view", "update", "ready", "notify", "next", "context", "config", "configs", "set", "unset", "power", "perm", "fast", "model", "history", "stats", "handoff", "login", "logout", "disable", "enable", "export", "import", "select", "run", "help", "version"
|
|
279
287
|
) else None,
|
|
280
288
|
"use_color": use_color,
|
|
281
289
|
}
|
|
@@ -313,6 +321,9 @@ def main(argv, options=None):
|
|
|
313
321
|
if command == "repair":
|
|
314
322
|
return handle_repair(rest, ctx)
|
|
315
323
|
|
|
324
|
+
if command == "view":
|
|
325
|
+
return handle_view(rest, ctx)
|
|
326
|
+
|
|
316
327
|
if command == "update":
|
|
317
328
|
return handle_update(rest, ctx)
|
|
318
329
|
|
|
@@ -366,6 +377,15 @@ def main(argv, options=None):
|
|
|
366
377
|
if command == "run":
|
|
367
378
|
return handle_run(rest, ctx)
|
|
368
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
|
+
|
|
369
389
|
if command == "login":
|
|
370
390
|
return handle_login(rest, ctx)
|
|
371
391
|
|
package/src/cli_commands.py
CHANGED
|
@@ -21,6 +21,7 @@ from .context_store import (
|
|
|
21
21
|
)
|
|
22
22
|
from .errors import CdxError
|
|
23
23
|
from .health import collect_health_report, format_health_report
|
|
24
|
+
from .cli_view import handle_view
|
|
24
25
|
from .notify import (
|
|
25
26
|
format_notify_event,
|
|
26
27
|
format_scheduled_notification,
|
|
@@ -32,6 +33,7 @@ from .notify import (
|
|
|
32
33
|
)
|
|
33
34
|
from .provider_runtime import (
|
|
34
35
|
_ensure_session_authentication,
|
|
36
|
+
_headless_artifact_paths,
|
|
35
37
|
_list_launch_transcript_paths,
|
|
36
38
|
_normalize_reasoning_effort,
|
|
37
39
|
_probe_provider_auth,
|
|
@@ -40,6 +42,7 @@ from .provider_runtime import (
|
|
|
40
42
|
)
|
|
41
43
|
from .repair import format_repair_report, repair_health
|
|
42
44
|
from .backup_bundle import read_bundle_meta
|
|
45
|
+
from .run_registry import RunRegistry, build_code_review_report
|
|
43
46
|
from .run_usage import extract_run_usage
|
|
44
47
|
from .run_command import read_run_prompt, run_cdx_error_code, run_result_payload
|
|
45
48
|
from .status_view import _format_status_detail, _format_status_rows, format_priority_instruction, recommend_priority_rows
|
|
@@ -65,7 +68,10 @@ STATS_USAGE = "Usage: cdx stats [name] [--since 7d|today|DATE] [--from DATE] [--
|
|
|
65
68
|
LAST_USAGE = "Usage: cdx last [--json]"
|
|
66
69
|
SELECT_USAGE = "Usage: cdx select --provider PROVIDER [--min-reasoning-effort low|medium|high] [--min-power low|medium|high] [--require-ready] [--refresh] --json"
|
|
67
70
|
NEXT_USAGE = "Usage: cdx next [--json] [--refresh]"
|
|
68
|
-
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"
|
|
69
75
|
API_SCHEMA_VERSION = 1
|
|
70
76
|
HANDOFF_TRANSCRIPT_CHARS = 120000
|
|
71
77
|
HANDOFF_NATIVE_TRANSCRIPT_CANDIDATES = 64
|
|
@@ -694,6 +700,7 @@ def _parse_run_args(args):
|
|
|
694
700
|
"--prompt": {"key": "prompt", "type": "str", "default": None},
|
|
695
701
|
"--provider": {"key": "provider", "type": "str", "default": None, "transform": lambda value: _parse_provider_filter(value, RUN_USAGE)},
|
|
696
702
|
"--model": {"key": "model", "type": "str", "default": None},
|
|
703
|
+
"--kind": {"key": "kind", "type": "str", "default": "assistant"},
|
|
697
704
|
"--reasoning-effort": {"key": "reasoning_effort", "type": "str", "default": None},
|
|
698
705
|
"--power": {"key": "power", "type": "str", "default": None},
|
|
699
706
|
"--permission": {"key": "permission", "type": "str", "default": None, "transform": _normalize_run_permission},
|
|
@@ -711,6 +718,8 @@ def _parse_run_args(args):
|
|
|
711
718
|
raise CdxError(RUN_USAGE)
|
|
712
719
|
if bool(parsed["prompt_file"]) == bool(parsed["prompt"]):
|
|
713
720
|
raise CdxError(RUN_USAGE)
|
|
721
|
+
if parsed["kind"] not in ("assistant", "code-review"):
|
|
722
|
+
raise CdxError(RUN_USAGE)
|
|
714
723
|
effort = _normalize_reasoning_effort(
|
|
715
724
|
reasoning_effort=parsed["reasoning_effort"],
|
|
716
725
|
power=parsed["power"],
|
|
@@ -722,6 +731,7 @@ def _parse_run_args(args):
|
|
|
722
731
|
"cwd": parsed["cwd"],
|
|
723
732
|
"prompt_file": parsed["prompt_file"],
|
|
724
733
|
"prompt": parsed["prompt"],
|
|
734
|
+
"kind": parsed["kind"],
|
|
725
735
|
"model": parsed["model"],
|
|
726
736
|
"permission": parsed["permission"],
|
|
727
737
|
"timeout_seconds": parsed["timeout_seconds"],
|
|
@@ -1337,7 +1347,65 @@ def handle_next(rest, ctx):
|
|
|
1337
1347
|
return 0
|
|
1338
1348
|
|
|
1339
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
|
+
|
|
1340
1406
|
def handle_run(rest, ctx):
|
|
1407
|
+
registry = None
|
|
1408
|
+
run_id = None
|
|
1341
1409
|
try:
|
|
1342
1410
|
parsed = _parse_run_args(rest)
|
|
1343
1411
|
session = None
|
|
@@ -1396,6 +1464,22 @@ def handle_run(rest, ctx):
|
|
|
1396
1464
|
signal_emitter=ctx.get("signal_emitter"),
|
|
1397
1465
|
trust_local_credentials=False,
|
|
1398
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
|
+
)
|
|
1399
1483
|
run_info = _run_headless_provider_command(
|
|
1400
1484
|
run_session,
|
|
1401
1485
|
cwd=cwd,
|
|
@@ -1403,29 +1487,32 @@ def handle_run(rest, ctx):
|
|
|
1403
1487
|
initial_prompt=prompt,
|
|
1404
1488
|
timeout_seconds=parsed.get("timeout_seconds"),
|
|
1405
1489
|
spawn=ctx.get("spawn_headless") or ctx.get("spawn"),
|
|
1490
|
+
run_id=run_id,
|
|
1406
1491
|
)
|
|
1407
1492
|
usage = extract_run_usage(run_session.get("provider"), run_info.get("stdout_path"))
|
|
1408
1493
|
run_info = {**run_info, "usage": usage}
|
|
1409
1494
|
ok = run_info.get("returncode") == 0
|
|
1410
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
|
+
)
|
|
1411
1505
|
ctx["service"]["record_launch_history"](session["name"], {
|
|
1412
1506
|
"status": "success",
|
|
1413
1507
|
"cwd": cwd,
|
|
1414
1508
|
"exit_code": 0,
|
|
1415
1509
|
**run_info,
|
|
1416
1510
|
})
|
|
1417
|
-
_write_json(ctx,
|
|
1511
|
+
_write_json(ctx, final_payload)
|
|
1418
1512
|
return 0
|
|
1419
1513
|
message = "Provider process timed out." if run_info.get("timed_out") else "Provider process exited with a non-zero status."
|
|
1420
1514
|
error = CdxError(message, run_info.get("returncode") or 1)
|
|
1421
|
-
|
|
1422
|
-
"status": "failed",
|
|
1423
|
-
"cwd": cwd,
|
|
1424
|
-
"error": str(error),
|
|
1425
|
-
"exit_code": error.exit_code,
|
|
1426
|
-
**run_info,
|
|
1427
|
-
})
|
|
1428
|
-
_write_json(ctx, run_result_payload(
|
|
1515
|
+
final_payload = run_result_payload(
|
|
1429
1516
|
API_SCHEMA_VERSION,
|
|
1430
1517
|
False,
|
|
1431
1518
|
parsed,
|
|
@@ -1434,11 +1521,27 @@ def handle_run(rest, ctx):
|
|
|
1434
1521
|
error=error,
|
|
1435
1522
|
error_source="provider",
|
|
1436
1523
|
error_code="provider_timeout" if run_info.get("timed_out") else "provider_failed",
|
|
1437
|
-
)
|
|
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)
|
|
1438
1541
|
return error.exit_code or 1
|
|
1439
1542
|
except CdxError as error:
|
|
1440
1543
|
run_info = getattr(error, "run_info", None)
|
|
1441
|
-
|
|
1544
|
+
final_payload = run_result_payload(
|
|
1442
1545
|
API_SCHEMA_VERSION,
|
|
1443
1546
|
False,
|
|
1444
1547
|
locals().get("parsed", {}) or {},
|
|
@@ -1447,7 +1550,16 @@ def handle_run(rest, ctx):
|
|
|
1447
1550
|
error=error,
|
|
1448
1551
|
error_source="cdx",
|
|
1449
1552
|
error_code=run_cdx_error_code(error),
|
|
1450
|
-
)
|
|
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)
|
|
1451
1563
|
return error.exit_code
|
|
1452
1564
|
|
|
1453
1565
|
|
package/src/cli_view.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from .cli_render import _warn
|
|
4
|
+
from .errors import CdxError
|
|
5
|
+
from .logics_view import (
|
|
6
|
+
LOGICS_MANAGER_INSTALL_HINT,
|
|
7
|
+
build_viewer_diagnostics,
|
|
8
|
+
missing_logics_manager_failure,
|
|
9
|
+
resolve_logics_manager,
|
|
10
|
+
run_logics_viewer,
|
|
11
|
+
)
|
|
12
|
+
from .update_check import check_logics_manager_for_update
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
VIEW_USAGE = "Usage: cdx view [--json]"
|
|
16
|
+
API_SCHEMA_VERSION = 1
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _json_success(action, message, warnings=None, **extra):
|
|
20
|
+
payload = {
|
|
21
|
+
"schema_version": API_SCHEMA_VERSION,
|
|
22
|
+
"ok": True,
|
|
23
|
+
"action": action,
|
|
24
|
+
"message": message,
|
|
25
|
+
"warnings": warnings or [],
|
|
26
|
+
}
|
|
27
|
+
payload.update(extra)
|
|
28
|
+
return payload
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _write_json(ctx, payload):
|
|
32
|
+
ctx["out"](f"{json.dumps(payload, indent=2)}\n")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _update_notice_warnings(notices):
|
|
36
|
+
warnings = []
|
|
37
|
+
for notice in notices or []:
|
|
38
|
+
if not notice:
|
|
39
|
+
continue
|
|
40
|
+
tool = notice.get("tool") or "logics-manager"
|
|
41
|
+
current = notice.get("current_version")
|
|
42
|
+
command = notice.get("update_command")
|
|
43
|
+
message = f"Update available: {tool} {notice['latest_version']}"
|
|
44
|
+
if current:
|
|
45
|
+
message = f"{message} (current {current})"
|
|
46
|
+
if command:
|
|
47
|
+
message = f"{message}. Run: {command}"
|
|
48
|
+
warnings.append({
|
|
49
|
+
"code": f"{tool.replace('-', '_')}_update_available",
|
|
50
|
+
"message": message,
|
|
51
|
+
"tool": tool,
|
|
52
|
+
"latest_version": notice["latest_version"],
|
|
53
|
+
"current_version": current,
|
|
54
|
+
"update_command": command,
|
|
55
|
+
"url": notice.get("url"),
|
|
56
|
+
})
|
|
57
|
+
return warnings
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _logics_manager_update_notice(ctx, env):
|
|
61
|
+
checker = ctx["options"].get("checkLogicsManagerForUpdate") or check_logics_manager_for_update
|
|
62
|
+
return checker(
|
|
63
|
+
ctx["service"]["base_dir"],
|
|
64
|
+
env=env,
|
|
65
|
+
now_fn=ctx["options"].get("now"),
|
|
66
|
+
runner=ctx["options"].get("runLogicsVersionCheck"),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def handle_view(rest, ctx):
|
|
71
|
+
json_flag = "--json" in rest
|
|
72
|
+
unknown = [arg for arg in rest if arg != "--json"]
|
|
73
|
+
if unknown:
|
|
74
|
+
raise CdxError(VIEW_USAGE)
|
|
75
|
+
|
|
76
|
+
env = ctx.get("env")
|
|
77
|
+
cwd = ctx.get("cwd")
|
|
78
|
+
executable = resolve_logics_manager(env=env)
|
|
79
|
+
update_notice = _logics_manager_update_notice(ctx, env) if executable else None
|
|
80
|
+
failure = None if executable else missing_logics_manager_failure()
|
|
81
|
+
diagnostics = build_viewer_diagnostics(executable, cwd, update_notice=update_notice, failure=failure)
|
|
82
|
+
warnings = _update_notice_warnings([update_notice])
|
|
83
|
+
|
|
84
|
+
if json_flag:
|
|
85
|
+
_write_json(ctx, _json_success("view", "Collected Logics viewer diagnostics", warnings=warnings, viewer=diagnostics))
|
|
86
|
+
return 0
|
|
87
|
+
|
|
88
|
+
if not executable:
|
|
89
|
+
raise CdxError(f"logics-manager is required for cdx view. {LOGICS_MANAGER_INSTALL_HINT}")
|
|
90
|
+
|
|
91
|
+
for warning in warnings:
|
|
92
|
+
ctx["out"](f"{_warn(warning['message'], ctx['use_color'])}\n")
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
result = run_logics_viewer(executable, cwd, env=env, runner=ctx.get("spawn_sync"))
|
|
96
|
+
except FileNotFoundError as error:
|
|
97
|
+
raise CdxError(f"logics-manager is required for cdx view. {LOGICS_MANAGER_INSTALL_HINT}") from error
|
|
98
|
+
except KeyboardInterrupt:
|
|
99
|
+
ctx["out"]("\n")
|
|
100
|
+
return 130
|
|
101
|
+
returncode = getattr(result, "returncode", 0)
|
|
102
|
+
if returncode not in (0, None):
|
|
103
|
+
raise CdxError("logics-manager view failed.", exit_code=returncode)
|
|
104
|
+
return 0
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
import subprocess
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
LOGICS_MANAGER_INSTALL_HINT = "Install or update it with: npm install -g @grifhinz/logics-manager"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def resolve_logics_manager(env=None):
|
|
9
|
+
env = env or {}
|
|
10
|
+
return shutil.which("logics-manager", path=env.get("PATH", ""))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def build_viewer_diagnostics(executable, cwd, update_notice=None, failure=None):
|
|
14
|
+
command = [executable or "logics-manager", "view"]
|
|
15
|
+
return {
|
|
16
|
+
"available": bool(executable),
|
|
17
|
+
"executable": executable,
|
|
18
|
+
"command": command,
|
|
19
|
+
"cwd": cwd,
|
|
20
|
+
"update": update_notice,
|
|
21
|
+
"failure": failure,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def missing_logics_manager_failure():
|
|
26
|
+
return {
|
|
27
|
+
"code": "logics_manager_missing",
|
|
28
|
+
"message": f"logics-manager is required for cdx view. {LOGICS_MANAGER_INSTALL_HINT}",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def run_logics_viewer(executable, cwd, env=None, runner=None):
|
|
33
|
+
runner = runner or subprocess.run
|
|
34
|
+
argv = [executable, "view"]
|
|
35
|
+
if runner is subprocess.run:
|
|
36
|
+
return subprocess.run(argv, cwd=cwd, env=env)
|
|
37
|
+
return runner(argv, cwd=cwd, env=env)
|
|
@@ -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
|
+
}
|