cctally 1.64.0 → 1.66.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/CHANGELOG.md +50 -0
- package/bin/_cctally_alerts.py +1 -1
- package/bin/_cctally_cache.py +262 -40
- package/bin/_cctally_cache_report.py +7 -5
- package/bin/_cctally_core.py +49 -14
- package/bin/_cctally_dashboard.py +827 -4911
- package/bin/_cctally_dashboard_cache_report.py +714 -0
- package/bin/_cctally_dashboard_conversation.py +771 -0
- package/bin/_cctally_dashboard_envelope.py +1520 -0
- package/bin/_cctally_dashboard_share.py +2012 -0
- package/bin/_cctally_db.py +127 -5
- package/bin/_cctally_doctor.py +104 -3
- package/bin/_cctally_five_hour.py +2 -1
- package/bin/_cctally_forecast.py +78 -135
- package/bin/_cctally_parser.py +919 -784
- package/bin/_cctally_percent_breakdown.py +1 -1
- package/bin/_cctally_pricing_check.py +39 -0
- package/bin/_cctally_project.py +8 -5
- package/bin/_cctally_record.py +279 -274
- package/bin/_cctally_reporting.py +1 -1
- package/bin/_cctally_sync_week.py +1 -1
- package/bin/_cctally_telemetry.py +3 -5
- package/bin/_cctally_tui.py +487 -254
- package/bin/_cctally_update.py +5 -0
- package/bin/_lib_alert_dispatch.py +1 -1
- package/bin/_lib_blocks.py +6 -1
- package/bin/_lib_conversation_query.py +349 -36
- package/bin/_lib_credit.py +133 -0
- package/bin/_lib_doctor.py +150 -1
- package/bin/_lib_five_hour.py +34 -0
- package/bin/_lib_forecast.py +144 -0
- package/bin/_lib_json_envelope.py +38 -0
- package/bin/_lib_jsonl.py +115 -73
- package/bin/_lib_log.py +96 -0
- package/bin/_lib_perf.py +180 -0
- package/bin/_lib_pricing.py +47 -1
- package/bin/_lib_pricing_check.py +25 -3
- package/bin/_lib_record.py +179 -0
- package/bin/_lib_render.py +18 -10
- package/bin/_lib_snapshot_cache.py +61 -0
- package/bin/_lib_transcript_access.py +23 -0
- package/bin/cctally +51 -7
- package/bin/cctally-dashboard +2 -2
- package/bin/cctally-statusline +1 -0
- package/bin/cctally-tui +2 -2
- package/dashboard/static/assets/index-CJTUCpKt.js +80 -0
- package/dashboard/static/assets/index-DQWNrIqu.css +1 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +11 -1
- package/dashboard/static/assets/index-0jzYm75p.css +0 -1
- package/dashboard/static/assets/index-D_Ylyqsf.js +0 -80
package/bin/_cctally_forecast.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Holds `cmd_report`, `cmd_forecast`, `cmd_budget`, the forecast core cluster
|
|
4
4
|
(window resolvers, ForecastInputs/ForecastOutput/BudgetRow, _compute_forecast,
|
|
5
|
-
the forecast render helpers
|
|
5
|
+
the forecast render helpers), and the 10 budget render/snapshot helpers.
|
|
6
6
|
|
|
7
7
|
Honest *name* imports are KERNEL-ONLY (_cctally_core) + stdlib. Every library
|
|
8
8
|
kernel + sibling helper this module needs (build_forecast_view, build_trend_view,
|
|
@@ -18,9 +18,10 @@ cluster (get_recent_weeks, _apply_reset_events_to_weekrefs,
|
|
|
18
18
|
_get_canonical_boundary_for_date) lives in _cctally_weekrefs.py (re-exported on
|
|
19
19
|
the cctally ns); both are reached via c.
|
|
20
20
|
|
|
21
|
-
_iso_z is
|
|
22
|
-
|
|
23
|
-
(the
|
|
21
|
+
_iso_z is honest-imported from the canonical _lib_json_envelope (#279 S6 W4);
|
|
22
|
+
the former intra-module dt-only copy collapsed onto it, and bin/cctally binds
|
|
23
|
+
that single canonical (the old dashboard-then-forecast double-bind is gone).
|
|
24
|
+
_lib_diff_kernel + _cctally_five_hour reach it unchanged via the cctally ns.
|
|
24
25
|
|
|
25
26
|
Spec: docs/superpowers/specs/2026-05-31-extract-forecast-budget-cmd-design.md
|
|
26
27
|
"""
|
|
@@ -51,37 +52,55 @@ from _cctally_core import (
|
|
|
51
52
|
BUDGET_PERIODS as _CCTALLY_BUDGET_PERIODS,
|
|
52
53
|
)
|
|
53
54
|
|
|
55
|
+
import importlib.util as _ilu
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _ensure_sibling_loaded(name: str) -> None:
|
|
59
|
+
"""Register a NON-eager-loaded ``_lib_*`` sibling in ``sys.modules``.
|
|
60
|
+
|
|
61
|
+
``_lib_forecast`` (#279 S4 F2) is a NEW consumer-only sibling —
|
|
62
|
+
deliberately kept out of ``bin/cctally``'s eager-load block so
|
|
63
|
+
``bin/cctally`` stays byte-untouched (spec §2 re-export continuity).
|
|
64
|
+
Under the ``SourceFileLoader`` harness path (``bin/`` absent from
|
|
65
|
+
``sys.path``) a bare ``from _lib_forecast import`` would miss, so this
|
|
66
|
+
pre-registers the sibling ``__file__``-relative when it is not already
|
|
67
|
+
importable (mirrors ``_cctally_cache._load_lib``). The honest import
|
|
68
|
+
that follows is a ``sys.modules`` hit in every load context.
|
|
69
|
+
"""
|
|
70
|
+
if name in sys.modules:
|
|
71
|
+
return
|
|
72
|
+
try:
|
|
73
|
+
__import__(name) # bin/ on sys.path: prod script / conftest / pytest
|
|
74
|
+
return
|
|
75
|
+
except ModuleNotFoundError:
|
|
76
|
+
pass
|
|
77
|
+
_p = os.path.join(os.path.dirname(__file__), f"{name}.py")
|
|
78
|
+
_spec = _ilu.spec_from_file_location(name, _p)
|
|
79
|
+
_mod = _ilu.module_from_spec(_spec)
|
|
80
|
+
sys.modules[name] = _mod
|
|
81
|
+
_spec.loader.exec_module(_mod)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
_ensure_sibling_loaded("_lib_forecast")
|
|
85
|
+
from _lib_forecast import ForecastInputs, BudgetRow, ForecastOutput, _compute_forecast
|
|
86
|
+
|
|
87
|
+
# #279 S6 W4: the canonical None-safe UTC-Z serializer. forecast's former local
|
|
88
|
+
# _iso_z (dt-only, no None guard) collapses to this single definition; the union
|
|
89
|
+
# behavior is exactly the canonical (forecast gains a None guard it never
|
|
90
|
+
# exercises). doctor's _iso_z deliberately keeps its own divergent copy.
|
|
91
|
+
_ensure_sibling_loaded("_lib_json_envelope")
|
|
92
|
+
from _lib_json_envelope import _iso_z
|
|
93
|
+
|
|
54
94
|
|
|
55
95
|
def _cctally():
|
|
56
96
|
"""Resolve the current `cctally` module at call-time (§2)."""
|
|
57
97
|
return sys.modules["cctally"]
|
|
58
98
|
|
|
59
99
|
|
|
60
|
-
#
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
week_start_at: dt.datetime
|
|
65
|
-
week_end_at: dt.datetime
|
|
66
|
-
elapsed_hours: float
|
|
67
|
-
elapsed_fraction: float
|
|
68
|
-
remaining_hours: float
|
|
69
|
-
remaining_days: float
|
|
70
|
-
# Current state
|
|
71
|
-
p_now: float
|
|
72
|
-
five_hour_percent: float | None
|
|
73
|
-
spent_usd: float
|
|
74
|
-
snapshot_count: int
|
|
75
|
-
latest_snapshot_at: dt.datetime
|
|
76
|
-
# Rate inputs
|
|
77
|
-
p_24h_ago: float | None
|
|
78
|
-
t_24h_actual_hours: float | None
|
|
79
|
-
# $/1% selection
|
|
80
|
-
dollars_per_percent: float
|
|
81
|
-
dollars_per_percent_source: str # "this_week" | "trailing_4wk_median" | "this_week_sparse"
|
|
82
|
-
# Confidence
|
|
83
|
-
confidence: str # "high" | "low"
|
|
84
|
-
low_confidence_reasons: list[str]
|
|
100
|
+
# ``ForecastInputs`` / ``BudgetRow`` / ``ForecastOutput`` / ``_compute_forecast``
|
|
101
|
+
# now live in ``bin/_lib_forecast.py`` (#279 S4 F2); honest-imported at module
|
|
102
|
+
# top so the ``bin/cctally`` re-exports and this module's own callers resolve
|
|
103
|
+
# them unchanged.
|
|
85
104
|
|
|
86
105
|
|
|
87
106
|
def _resolve_forecast_now(as_of: str | None) -> dt.datetime:
|
|
@@ -519,99 +538,7 @@ def _load_forecast_inputs(
|
|
|
519
538
|
)
|
|
520
539
|
|
|
521
540
|
|
|
522
|
-
|
|
523
|
-
class BudgetRow:
|
|
524
|
-
target_percent: int
|
|
525
|
-
pct_headroom: float | None # None when already past target
|
|
526
|
-
dollars_per_day: float | None
|
|
527
|
-
percent_per_day: float | None
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
@dataclass
|
|
531
|
-
class ForecastOutput:
|
|
532
|
-
inputs: ForecastInputs
|
|
533
|
-
r_avg: float # pct per hour, week-avg
|
|
534
|
-
r_recent: float | None # pct per hour, 24h recent; None if no prior sample
|
|
535
|
-
final_percent_low: float
|
|
536
|
-
final_percent_high: float
|
|
537
|
-
week_avg_projection_pct: float # p_now + r_avg*remaining (smooth estimator)
|
|
538
|
-
projected_cap: bool
|
|
539
|
-
already_capped: bool
|
|
540
|
-
cap_at: dt.datetime | None
|
|
541
|
-
budgets: list[BudgetRow]
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
def _compute_forecast(inputs: ForecastInputs, targets: list[int]) -> ForecastOutput:
|
|
545
|
-
"""Implements spec §2. targets are sorted desc for stable output (100, 90, …)."""
|
|
546
|
-
c = _cctally()
|
|
547
|
-
# Rate methods
|
|
548
|
-
r_avg = inputs.p_now / inputs.elapsed_hours if inputs.elapsed_hours > 0 else 0.0
|
|
549
|
-
if inputs.p_24h_ago is not None and inputs.t_24h_actual_hours:
|
|
550
|
-
r_recent: float | None = max(
|
|
551
|
-
0.0, (inputs.p_now - inputs.p_24h_ago) / inputs.t_24h_actual_hours
|
|
552
|
-
)
|
|
553
|
-
else:
|
|
554
|
-
r_recent = None
|
|
555
|
-
|
|
556
|
-
# Projected final % — routed through the shared project_linear primitive
|
|
557
|
-
# (spec F1). r_recent is None ⇒ collapse to the average projection.
|
|
558
|
-
if r_recent is None:
|
|
559
|
-
final_low, final_high = c.project_linear(
|
|
560
|
-
inputs.p_now, inputs.remaining_hours, r_avg, r_avg
|
|
561
|
-
)
|
|
562
|
-
else:
|
|
563
|
-
a, b = c.project_linear(
|
|
564
|
-
inputs.p_now, inputs.remaining_hours, r_avg, r_recent
|
|
565
|
-
)
|
|
566
|
-
final_low, final_high = min(a, b), max(a, b)
|
|
567
|
-
|
|
568
|
-
# Smooth week-average projection (additive surface field). Distinct from
|
|
569
|
-
# the displayed band (which keys off final_high): this is the conservative
|
|
570
|
-
# week-average value the projected-pace alert axis fires on.
|
|
571
|
-
# p_now + r_avg*remaining (== project_linear collapsed to the single rate).
|
|
572
|
-
week_avg_projection_pct = inputs.p_now + r_avg * inputs.remaining_hours
|
|
573
|
-
|
|
574
|
-
already_capped = inputs.p_now >= 100.0
|
|
575
|
-
projected_cap = already_capped or final_high >= 100.0
|
|
576
|
-
|
|
577
|
-
cap_at: dt.datetime | None = None
|
|
578
|
-
if not already_capped and projected_cap:
|
|
579
|
-
r_pessimistic = max(r_avg, r_recent or 0.0)
|
|
580
|
-
if r_pessimistic > 0:
|
|
581
|
-
hours_to_cap = (100.0 - inputs.p_now) / r_pessimistic
|
|
582
|
-
if hours_to_cap < inputs.remaining_hours:
|
|
583
|
-
cap_at = inputs.now_utc + dt.timedelta(hours=hours_to_cap)
|
|
584
|
-
|
|
585
|
-
# Budgets
|
|
586
|
-
budgets: list[BudgetRow] = []
|
|
587
|
-
if not already_capped:
|
|
588
|
-
for t in sorted(targets, reverse=True):
|
|
589
|
-
headroom = t - inputs.p_now
|
|
590
|
-
if headroom <= 0 or inputs.remaining_days <= 0:
|
|
591
|
-
budgets.append(BudgetRow(target_percent=t, pct_headroom=None,
|
|
592
|
-
dollars_per_day=None, percent_per_day=None))
|
|
593
|
-
continue
|
|
594
|
-
dollars_day = (headroom * inputs.dollars_per_percent) / inputs.remaining_days
|
|
595
|
-
pct_day = headroom / inputs.remaining_days
|
|
596
|
-
budgets.append(BudgetRow(
|
|
597
|
-
target_percent=t,
|
|
598
|
-
pct_headroom=headroom,
|
|
599
|
-
dollars_per_day=dollars_day,
|
|
600
|
-
percent_per_day=pct_day,
|
|
601
|
-
))
|
|
602
|
-
|
|
603
|
-
return ForecastOutput(
|
|
604
|
-
inputs=inputs,
|
|
605
|
-
r_avg=r_avg,
|
|
606
|
-
r_recent=r_recent,
|
|
607
|
-
final_percent_low=final_low,
|
|
608
|
-
final_percent_high=final_high,
|
|
609
|
-
week_avg_projection_pct=week_avg_projection_pct,
|
|
610
|
-
projected_cap=projected_cap,
|
|
611
|
-
already_capped=already_capped,
|
|
612
|
-
cap_at=cap_at,
|
|
613
|
-
budgets=budgets,
|
|
614
|
-
)
|
|
541
|
+
# (moved to bin/_lib_forecast.py, #279 S4 F2 — honest-imported at module top)
|
|
615
542
|
|
|
616
543
|
|
|
617
544
|
def _parse_forecast_targets(raw: str) -> list[int]:
|
|
@@ -636,10 +563,6 @@ def _parse_forecast_targets(raw: str) -> list[int]:
|
|
|
636
563
|
TOOL_VERSION = "forecast-v1" # Bumped on material JSON-schema changes.
|
|
637
564
|
|
|
638
565
|
|
|
639
|
-
def _iso_z(d: dt.datetime) -> str:
|
|
640
|
-
return d.astimezone(dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
641
|
-
|
|
642
|
-
|
|
643
566
|
def _build_forecast_json_payload(out: ForecastOutput) -> dict:
|
|
644
567
|
"""Dict shape for the forecast JSON endpoint and for the dashboard
|
|
645
568
|
envelope's ``forecast.explain`` subtree (design spec §2.2).
|
|
@@ -701,7 +624,11 @@ def _build_forecast_json_payload(out: ForecastOutput) -> dict:
|
|
|
701
624
|
|
|
702
625
|
|
|
703
626
|
def _emit_forecast_json(out: ForecastOutput) -> str:
|
|
704
|
-
|
|
627
|
+
# Stamp at the CLI boundary ONLY — never in _build_forecast_json_payload,
|
|
628
|
+
# which also feeds the dashboard `explain` subtree (spec gate F3).
|
|
629
|
+
return json.dumps(
|
|
630
|
+
_cctally().stamp_schema_version(_build_forecast_json_payload(out)),
|
|
631
|
+
indent=2)
|
|
705
632
|
|
|
706
633
|
|
|
707
634
|
def _render_forecast_status_line(out: ForecastOutput, color: bool) -> str:
|
|
@@ -1070,7 +997,9 @@ def cmd_report(args: argparse.Namespace) -> int:
|
|
|
1070
997
|
c._share_render_and_emit(snap, args)
|
|
1071
998
|
return 0
|
|
1072
999
|
if args.json:
|
|
1073
|
-
print(json.dumps(
|
|
1000
|
+
print(json.dumps(
|
|
1001
|
+
c.stamp_schema_version({"current": None, "trend": []}),
|
|
1002
|
+
indent=2))
|
|
1074
1003
|
else:
|
|
1075
1004
|
print("No data yet. Add record-usage to your status line script (see record-usage --help).")
|
|
1076
1005
|
return 0
|
|
@@ -1192,7 +1121,12 @@ def cmd_report(args: argparse.Namespace) -> int:
|
|
|
1192
1121
|
"current": current_row,
|
|
1193
1122
|
"trend": trend,
|
|
1194
1123
|
"weekStartRule": week_start_name,
|
|
1195
|
-
|
|
1124
|
+
# Honor the CCTALLY_AS_OF testing hook (matches cache-report's
|
|
1125
|
+
# generatedAt, #279 S7 W6) — real behavior is unchanged since
|
|
1126
|
+
# _command_as_of() returns wall-clock when the hook is unset, but the
|
|
1127
|
+
# field becomes deterministic under the hook so report --json is
|
|
1128
|
+
# golden-testable.
|
|
1129
|
+
"generatedAt": now_utc_iso(now_utc=_command_as_of()),
|
|
1196
1130
|
"currentWeek": {
|
|
1197
1131
|
"weekStartDate": current_ref.week_start.isoformat(),
|
|
1198
1132
|
"weekEndDate": current_ref.week_end.isoformat() if current_ref.week_end else None,
|
|
@@ -1258,7 +1192,7 @@ def cmd_report(args: argparse.Namespace) -> int:
|
|
|
1258
1192
|
return 0
|
|
1259
1193
|
|
|
1260
1194
|
if args.json:
|
|
1261
|
-
print(json.dumps(output, indent=2))
|
|
1195
|
+
print(json.dumps(c.stamp_schema_version(output), indent=2))
|
|
1262
1196
|
return 0
|
|
1263
1197
|
|
|
1264
1198
|
if current_row is not None:
|
|
@@ -1373,7 +1307,8 @@ def cmd_forecast(args: argparse.Namespace) -> int:
|
|
|
1373
1307
|
if args.json and args.status_line:
|
|
1374
1308
|
print("forecast: --json and --status-line are mutually exclusive",
|
|
1375
1309
|
file=sys.stderr)
|
|
1376
|
-
|
|
1310
|
+
# cctally-native usage error → exit 2 (docs/cli-contract.md; #279 S6 W2).
|
|
1311
|
+
return 2
|
|
1377
1312
|
|
|
1378
1313
|
# Resolve display tz via the unified --tz / config.display.tz pipeline.
|
|
1379
1314
|
# The renderer reads it back from args._resolved_tz.
|
|
@@ -1384,7 +1319,8 @@ def cmd_forecast(args: argparse.Namespace) -> int:
|
|
|
1384
1319
|
targets = _parse_forecast_targets(args.targets)
|
|
1385
1320
|
except ValueError as exc:
|
|
1386
1321
|
print(f"forecast: {exc}", file=sys.stderr)
|
|
1387
|
-
|
|
1322
|
+
# cctally-native usage error → exit 2 (docs/cli-contract.md; #279 S6 W2).
|
|
1323
|
+
return 2
|
|
1388
1324
|
|
|
1389
1325
|
now_utc = _resolve_forecast_now(args.as_of)
|
|
1390
1326
|
conn = open_db()
|
|
@@ -1457,10 +1393,10 @@ def cmd_forecast(args: argparse.Namespace) -> int:
|
|
|
1457
1393
|
c._share_render_and_emit(snap, args)
|
|
1458
1394
|
return 0
|
|
1459
1395
|
if args.json:
|
|
1460
|
-
print(json.dumps({
|
|
1396
|
+
print(json.dumps(c.stamp_schema_version({
|
|
1461
1397
|
"error": "no_current_week_data",
|
|
1462
1398
|
"meta": {"generated_at": _iso_z(now_utc), "tool_version": TOOL_VERSION},
|
|
1463
|
-
}, indent=2))
|
|
1399
|
+
}), indent=2))
|
|
1464
1400
|
elif args.status_line:
|
|
1465
1401
|
pass # silent segment
|
|
1466
1402
|
else:
|
|
@@ -2168,6 +2104,7 @@ def _cmd_budget_set_project(args: argparse.Namespace) -> int:
|
|
|
2168
2104
|
basename = os.path.basename(root) or root
|
|
2169
2105
|
if getattr(args, "json", False):
|
|
2170
2106
|
print(json.dumps({
|
|
2107
|
+
"schemaVersion": _BUDGET_JSON_SCHEMA_VERSION,
|
|
2171
2108
|
"status": "set",
|
|
2172
2109
|
"project_key": root,
|
|
2173
2110
|
"budget_usd": amount,
|
|
@@ -2227,6 +2164,7 @@ def _cmd_budget_unset_project(args: argparse.Namespace) -> int:
|
|
|
2227
2164
|
basename = os.path.basename(root) or root
|
|
2228
2165
|
if getattr(args, "json", False):
|
|
2229
2166
|
print(json.dumps({
|
|
2167
|
+
"schemaVersion": _BUDGET_JSON_SCHEMA_VERSION,
|
|
2230
2168
|
"status": "unset" if removed else "noop",
|
|
2231
2169
|
"project_key": root,
|
|
2232
2170
|
}))
|
|
@@ -2301,6 +2239,7 @@ def _cmd_budget_set(args: argparse.Namespace, period=None) -> int:
|
|
|
2301
2239
|
c._reconcile_budget_on_config_write(validated)
|
|
2302
2240
|
if getattr(args, "json", False):
|
|
2303
2241
|
print(json.dumps({
|
|
2242
|
+
"schemaVersion": _BUDGET_JSON_SCHEMA_VERSION,
|
|
2304
2243
|
"status": "set",
|
|
2305
2244
|
"weekly_usd": weekly_usd,
|
|
2306
2245
|
"period": stored_period,
|
|
@@ -2339,7 +2278,9 @@ def _cmd_budget_unset(args: argparse.Namespace) -> int:
|
|
|
2339
2278
|
c.save_config(config)
|
|
2340
2279
|
|
|
2341
2280
|
if getattr(args, "json", False):
|
|
2342
|
-
print(json.dumps({
|
|
2281
|
+
print(json.dumps({
|
|
2282
|
+
"schemaVersion": _BUDGET_JSON_SCHEMA_VERSION,
|
|
2283
|
+
"status": "unset", "weekly_usd": None}))
|
|
2343
2284
|
return 0
|
|
2344
2285
|
print("Weekly budget cleared")
|
|
2345
2286
|
return 0
|
|
@@ -2409,6 +2350,7 @@ def _cmd_budget_set_codex(args: argparse.Namespace, period=None) -> int:
|
|
|
2409
2350
|
thresholds = codex["alert_thresholds"]
|
|
2410
2351
|
if getattr(args, "json", False):
|
|
2411
2352
|
print(json.dumps({
|
|
2353
|
+
"schemaVersion": _BUDGET_JSON_SCHEMA_VERSION,
|
|
2412
2354
|
"status": "set",
|
|
2413
2355
|
"vendor": "codex",
|
|
2414
2356
|
"amount_usd": amount_usd,
|
|
@@ -2449,6 +2391,7 @@ def _cmd_budget_unset_codex(args: argparse.Namespace) -> int:
|
|
|
2449
2391
|
|
|
2450
2392
|
if getattr(args, "json", False):
|
|
2451
2393
|
print(json.dumps({
|
|
2394
|
+
"schemaVersion": _BUDGET_JSON_SCHEMA_VERSION,
|
|
2452
2395
|
"status": "unset" if removed else "noop", "vendor": "codex",
|
|
2453
2396
|
}))
|
|
2454
2397
|
return 0
|